Hook up layer tree structure with frontend (#372)

* Hook up layer tree structure with frontend (decoding and Vue are WIP)

* Fix off by one error

* Avoid leaking memory

* Parse layer structure into list of layers

* Fix thumbnail updates

* Correctly popagate deletions

* Fix selection state in layer tree

* Respect expansion during root serialization

* Allow expanding of subfolders

* Fix arrow direction

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers
2021-09-11 17:15:51 -07:00
co-authored by Dennis Kobert
parent 88bb24a206
commit f5e03df80b
19 changed files with 15777 additions and 336 deletions
+45 -88
View File
@@ -110,18 +110,18 @@
position: absolute;
width: 0;
height: 0;
top: 2px;
left: 3px;
top: 3px;
left: 4px;
border-style: solid;
border-width: 0 3px 6px 3px;
border-color: transparent transparent var(--color-2-mildblack) transparent;
border-width: 3px 0 3px 6px;
border-color: transparent transparent transparent var(--color-2-mildblack);
}
&.expanded::after {
top: 3px;
left: 4px;
border-width: 3px 0 3px 6px;
border-color: transparent transparent transparent var(--color-2-mildblack);
top: 4px;
left: 3px;
border-width: 6px 3px 0 3px;
border-color: var(--color-2-mildblack) transparent transparent transparent;
}
}
@@ -181,7 +181,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { ResponseType, registerResponseHandler, Response, BlendMode, ExpandFolder, CollapseFolder, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
import { ResponseType, registerResponseHandler, Response, BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerType } from "@/utilities/response-handler";
import { panicProxy } from "@/utilities/panic-proxy";
import { SeparatorType } from "@/components/widgets/widgets";
@@ -238,7 +238,24 @@ const blendModeEntries: SectionsOfMenuListEntries = [
];
export default defineComponent({
props: {},
data() {
return {
blendModeEntries,
blendModeSelectedIndex: 0,
blendModeDropdownDisabled: true,
opacityNumberInputDisabled: true,
// TODO: replace with BigUint64Array as index
layerCache: new Map() as Map<string, LayerPanelEntry>,
layers: [] as Array<LayerPanelEntry>,
layerDepths: [] as Array<number>,
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
opacity: 100,
MenuDirection,
SeparatorType,
LayerType,
};
},
methods: {
layerIndent(layer: LayerPanelEntry): string {
return `${(layer.path.length - 1) * 16}px`;
@@ -325,7 +342,6 @@ export default defineComponent({
output.set(path, i);
i += path.length;
if (index < paths.length) {
// eslint-disable-next-line no-bitwise
output[i] = (1n << 64n) - 1n;
}
i += 1;
@@ -374,99 +390,40 @@ export default defineComponent({
},
},
mounted() {
registerResponseHandler(ResponseType.ExpandFolder, (responseData: Response) => {
const expandData = responseData as ExpandFolder;
if (expandData) {
const responsePath = expandData.path;
const responseLayers = expandData.children as Array<LayerPanelEntry>;
// TODO: @Keavon Refactor this function
if (responseLayers.length === 0) return;
registerResponseHandler(ResponseType.DisplayFolderTreeStructure, (responseData: Response) => {
const expandData = responseData as DisplayFolderTreeStructure;
if (!expandData) return;
console.log(expandData);
const mergeIntoExisting = (elements: Array<LayerPanelEntry>, layers: Array<LayerPanelEntry>) => {
let lastInsertion = layers.findIndex((layer: LayerPanelEntry) => {
const pathLengthsEqual = elements[0].path.length - 1 === layer.path.length;
return pathLengthsEqual && elements[0].path.slice(0, -1).every((layerId, i) => layerId === layer.path[i]);
});
elements.forEach((nlayer) => {
const index = layers.findIndex((layer: LayerPanelEntry) => {
const pathLengthsEqual = nlayer.path.length === layer.path.length;
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
});
if (index >= 0) {
lastInsertion = index;
layers[index] = nlayer;
} else {
lastInsertion += 1;
layers.splice(lastInsertion, 0, nlayer);
}
});
};
mergeIntoExisting(responseLayers, this.layers);
const newLayers: Array<LayerPanelEntry> = [];
this.layers.forEach((layer) => {
const index = responseLayers.findIndex((nlayer: LayerPanelEntry) => {
const pathLengthsEqual = responsePath.length + 1 === layer.path.length;
return pathLengthsEqual && nlayer.path.every((layerId, i) => layerId === layer.path[i]);
});
if (index >= 0 || layer.path.length !== responsePath.length + 1) {
newLayers.push(layer);
}
const path = [] as Array<bigint>;
this.layers = [] as Array<LayerPanelEntry>;
function recurse(folder: DisplayFolderTreeStructure, layers: Array<LayerPanelEntry>, cache: Map<string, LayerPanelEntry>) {
folder.children.forEach((item) => {
// TODO: fix toString
path.push(BigInt(item.layerId.toString()));
const mapping = cache.get(path.toString());
if (mapping) layers.push(mapping);
if (item.children.length > 1) recurse(item, layers, cache);
path.pop();
});
this.layers = newLayers;
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
}
recurse(expandData, this.layers, this.layerCache);
});
registerResponseHandler(ResponseType.CollapseFolder, (responseData) => {
const collapseData = responseData as CollapseFolder;
if (collapseData) {
const responsePath = collapseData.path;
const newLayers: Array<LayerPanelEntry> = [];
this.layers.forEach((layer) => {
if (responsePath.length >= layer.path.length || !responsePath.every((layerId, i) => layerId === layer.path[i])) {
newLayers.push(layer);
}
});
this.layers = newLayers;
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
}
});
registerResponseHandler(ResponseType.UpdateLayer, (responseData) => {
const updateData = responseData as UpdateLayer;
if (updateData) {
const responsePath = updateData.path;
const responseLayer = updateData.data;
const index = this.layers.findIndex((layer: LayerPanelEntry) => {
const pathLengthsEqual = responsePath.length === layer.path.length;
return pathLengthsEqual && responsePath.every((layerId, i) => layerId === layer.path[i]);
});
if (index >= 0) this.layers[index] = responseLayer;
const layer = this.layerCache.get(responsePath.toString());
if (layer) Object.assign(this.layerCache.get(responsePath.toString()), responseLayer);
else this.layerCache.set(responsePath.toString(), responseLayer);
this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
}
});
},
data() {
return {
blendModeEntries,
blendModeSelectedIndex: 0,
blendModeDropdownDisabled: true,
opacityNumberInputDisabled: true,
layers: [] as Array<LayerPanelEntry>,
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
opacity: 100,
MenuDirection,
SeparatorType,
LayerType,
};
},
components: {
LayoutRow,
LayoutCol,
@@ -293,7 +293,6 @@ export default defineComponent({
this.setClosed();
}
// eslint-disable-next-line no-bitwise
const eventIncludesLmb = Boolean(e.buttons & 1);
// Clean up any messes from lost mouseup events
+23 -13
View File
@@ -4,22 +4,32 @@ import { fullscreenModeChanged } from "@/utilities/fullscreen";
import { onKeyUp, onKeyDown, onMouseMove, onMouseDown, onMouseUp, onMouseScroll, onWindowResize } from "@/utilities/input";
import "@/utilities/errors";
import App from "@/App.vue";
import { panicProxy } from "@/utilities/panic-proxy";
// Bind global browser events
window.addEventListener("resize", onWindowResize);
window.addEventListener("DOMContentLoaded", onWindowResize);
const wasm = import("@/../wasm/pkg").then(panicProxy);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).wasmMemory = undefined;
document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).wasmMemory = (await wasm).wasm_memory;
window.addEventListener("keyup", onKeyUp);
window.addEventListener("keydown", onKeyDown);
// Initialize the Vue application
createApp(App).mount("#app");
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
// Bind global browser events
window.addEventListener("resize", onWindowResize);
onWindowResize();
window.addEventListener("wheel", onMouseScroll, { passive: false });
document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
// Initialize the Vue application
createApp(App).mount("#app");
window.addEventListener("keyup", onKeyUp);
window.addEventListener("keydown", onKeyDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("wheel", onMouseScroll, { passive: false });
})();
-1
View File
@@ -126,6 +126,5 @@ export async function onWindowResize() {
}
export function makeModifiersBitfield(e: MouseEvent | KeyboardEvent): number {
// eslint-disable-next-line no-bitwise
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2);
}
+58 -24
View File
@@ -18,8 +18,7 @@ export enum ResponseType {
ExportDocument = "ExportDocument",
SaveDocument = "SaveDocument",
OpenDocumentBrowse = "OpenDocumentBrowse",
ExpandFolder = "ExpandFolder",
CollapseFolder = "CollapseFolder",
DisplayFolderTreeStructure = "DisplayFolderTreeStructure",
UpdateLayer = "UpdateLayer",
SetActiveTool = "SetActiveTool",
SetActiveDocument = "SetActiveDocument",
@@ -56,10 +55,8 @@ function parseResponse(responseType: string, data: any): Response {
switch (responseType) {
case "DocumentChanged":
return newDocumentChanged(data.DocumentChanged);
case "CollapseFolder":
return newCollapseFolder(data.CollapseFolder);
case "ExpandFolder":
return newExpandFolder(data.ExpandFolder);
case "DisplayFolderTreeStructure":
return newDisplayFolderTreeStructure(data.DisplayFolderTreeStructure);
case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool);
case "SetActiveDocument":
@@ -97,7 +94,7 @@ function parseResponse(responseType: string, data: any): Response {
}
}
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | DocumentChanged | CollapseFolder | ExpandFolder | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | UpdateLayer | DocumentChanged | DisplayFolderTreeStructure | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
export interface UpdateOpenDocumentsList {
open_documents: Array<string>;
@@ -239,13 +236,61 @@ function newDocumentChanged(_: any): DocumentChanged {
return {};
}
export interface CollapseFolder {
path: BigUint64Array;
export interface DisplayFolderTreeStructure {
layerId: BigInt;
children: DisplayFolderTreeStructure[];
}
function newCollapseFolder(input: any): CollapseFolder {
return {
path: newPath(input.path),
};
function newDisplayFolderTreeStructure(input: any): DisplayFolderTreeStructure {
const { ptr, len } = input.data_buffer;
const wasmMemoryBuffer = (window as any).wasmMemory().buffer;
// Decode the folder structure encoding
const encoding = new DataView(wasmMemoryBuffer, ptr, len);
// The structure section indicates how to read through the upcoming layer list and assign depths to each layer
const structureSectionLength = Number(encoding.getBigUint64(0, true));
const structureSectionMsbSigned = new DataView(wasmMemoryBuffer, ptr + 8, structureSectionLength * 8);
// The layer IDs section lists each layer ID sequentially in the tree, as it will show up in the panel
const layerIdsSection = new DataView(wasmMemoryBuffer, ptr + 8 + structureSectionLength * 8);
let layersEncountered = 0;
let currentFolder: DisplayFolderTreeStructure = { layerId: BigInt(-1), children: [] };
const currentFolderStack = [currentFolder];
for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
const msbMask = BigInt(1) << BigInt(63);
// Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
// Store child folders in the current folder (until we are interrupted by an indent)
for (let j = 0; j < numberOfLayersAtThisDepth; j += 1) {
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
layersEncountered += 1;
const childLayer = { layerId, children: [] };
currentFolder.children.push(childLayer);
}
// Check the sign of the MSB, where a 1 is a negative (outward) indent
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
// debugger;
// Inward
if (subsequentDirectionOfDepthChange) {
currentFolderStack.push(currentFolder);
currentFolder = currentFolder.children[currentFolder.children.length - 1];
}
// Outward
else {
const popped = currentFolderStack.pop();
if (!popped) throw Error("Too many negative indents in the folder structure");
if (popped) currentFolder = popped;
}
}
return currentFolder;
}
export interface UpdateLayer {
@@ -259,17 +304,6 @@ function newUpdateLayer(input: any): UpdateLayer {
};
}
export interface ExpandFolder {
path: BigUint64Array;
children: Array<LayerPanelEntry>;
}
function newExpandFolder(input: any): ExpandFolder {
return {
path: newPath(input.path),
children: input.children.map((child: any) => newLayerPanelEntry(child)),
};
}
export interface SetCanvasZoom {
new_zoom: number;
}