mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 13:58:13 +08:00
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:
co-authored by
Dennis Kobert
parent
88bb24a206
commit
f5e03df80b
@@ -62,6 +62,7 @@ module.exports = {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-param-reassign": ["error", { props: false }],
|
||||
"no-bitwise": "off",
|
||||
|
||||
// TypeScript plugin config
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
|
||||
Generated
+15394
-28
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 });
|
||||
})();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@ function htmlDecode(input) {
|
||||
}
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if ((match = entityCode.match(/^#(\d+)$/))) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return String.fromCharCode(~~match[1]);
|
||||
}
|
||||
return entity;
|
||||
|
||||
@@ -20,6 +20,11 @@ pub fn intentional_panic() {
|
||||
panic!();
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn wasm_memory() -> JsValue {
|
||||
wasm_bindgen::memory()
|
||||
}
|
||||
|
||||
/// Modify the currently selected tool in the document state store
|
||||
#[wasm_bindgen]
|
||||
pub fn select_tool(tool: String) -> Result<(), JsValue> {
|
||||
|
||||
Reference in New Issue
Block a user