diff --git a/client/web/.eslintrc.js b/client/web/.eslintrc.js index 181a3892b..e4eb13e19 100644 --- a/client/web/.eslintrc.js +++ b/client/web/.eslintrc.js @@ -1,7 +1,9 @@ module.exports = { root: true, env: { + browser: true, node: true, + es2020: true, }, extends: ["plugin:vue/vue3-essential", "@vue/airbnb", "@vue/typescript/recommended", "plugin:prettier-vue/recommended", "prettier"], parserOptions: { diff --git a/client/web/src/components/panels/LayerTree.vue b/client/web/src/components/panels/LayerTree.vue index 40b612012..7641f3586 100644 --- a/client/web/src/components/panels/LayerTree.vue +++ b/client/web/src/components/panels/LayerTree.vue @@ -20,7 +20,14 @@
-
+
@@ -68,6 +75,10 @@ margin-left: 4px; padding-left: 16px; } + .selected { + background: var(--color-accent); + color: var(--color-f-white); + } & + .layer-row { margin-top: 2px; @@ -118,6 +129,65 @@ export default defineComponent({ const { toggle_layer_visibility } = await wasm; toggle_layer_visibility(path); }, + async handleControlClick(clickedLayer: LayerPanelEntry) { + const index = this.layers.indexOf(clickedLayer); + clickedLayer.layer_data.selected = !clickedLayer.layer_data.selected; + this.selectionRangeEndLayer = undefined; + this.selectionRangeStartLayer = + this.layers.slice(index).filter((layer) => layer.layer_data.selected)[0] || + this.layers + .slice(0, index) + .reverse() + .filter((layer) => layer.layer_data.selected)[0]; + this.updateSelection(); + }, + async handleShiftClick(clickedLayer: LayerPanelEntry) { + // The two paths of the range are stored in selectionRangeStartLayer and selectionRangeEndLayer + // So for a new Shift+Click, select all layers between selectionRangeStartLayer and selectionRangeEndLayer(stored in prev Sft+C) + this.selectionRangeEndLayer = clickedLayer; + this.selectionRangeStartLayer = (this.selectionRangeStartLayer as LayerPanelEntry) || clickedLayer; + this.clearSelection(); + this.fillSelectionRange(this.selectionRangeStartLayer, this.selectionRangeEndLayer, true); + this.updateSelection(); + }, + + async handleClick(clickedLayer: LayerPanelEntry) { + this.selectionRangeStartLayer = clickedLayer; + this.selectionRangeEndLayer = clickedLayer; + this.clearSelection(); + clickedLayer.layer_data.selected = true; + this.updateSelection(); + }, + async fillSelectionRange(start: LayerPanelEntry, end: LayerPanelEntry, selected = true) { + const startIndex = this.layers.indexOf(start); + const endIndex = this.layers.indexOf(end); + const [min, max] = [startIndex, endIndex].sort(); + for (let i = min; i <= max; i += 1) { + this.layers[i].layer_data.selected = selected; + } + }, + async clearSelection() { + this.layers.forEach((layer) => { + layer.layer_data.selected = false; + }); + }, + async updateSelection() { + const paths = this.layers.filter((layer) => layer.layer_data.selected).map((layer) => layer.path); + const length = paths.reduce((acc, cur) => acc + cur.length, 0) + paths.length - 1; + const output = new BigUint64Array(length); + let i = 0; + paths.forEach((path, index) => { + output.set(path, i); + i += path.length; + if (index < paths.length) { + // eslint-disable-next-line no-bitwise + output[i] = (1n << 64n) - 1n; + } + i += 1; + }); + const { select_layers } = await wasm; + select_layers(output); + }, }, mounted() { registerResponseHandler(ResponseType.ExpandFolder, (responseData: Response) => { @@ -140,6 +210,8 @@ export default defineComponent({ MenuDirection, SeparatorType, layers: [] as Array, + selectionRangeStartLayer: undefined as LayerPanelEntry | undefined, + selectionRangeEndLayer: undefined as LayerPanelEntry | undefined, }; }, components: { diff --git a/client/web/src/response-handler.ts b/client/web/src/response-handler.ts index ea78365ea..528d7730e 100644 --- a/client/web/src/response-handler.ts +++ b/client/web/src/response-handler.ts @@ -118,19 +118,30 @@ export interface LayerPanelEntry { name: string; visible: boolean; layer_type: LayerType; - collapsed: boolean; path: BigUint64Array; + layer_data: LayerData; } function newLayerPanelEntry(input: any): LayerPanelEntry { return { name: input.name, visible: input.visible, layer_type: newLayerType(input.layer_type), - collapsed: input.collapsed, + layer_data: newLayerData(input.layer_data), path: new BigUint64Array(input.path.map((n: number) => BigInt(n))), }; } +export interface LayerData { + expanded: boolean; + selected: boolean; +} +function newLayerData(input: any): LayerData { + return { + expanded: input.expanded, + selected: input.selected, + }; +} + export enum LayerType { Folder = "Folder", Shape = "Shape",