mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-09-11 23:46:18 +00:00

* 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>
440 lines
12 KiB
TypeScript
440 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable camelcase */
|
|
|
|
import { reactive } from "vue";
|
|
|
|
type ResponseCallback = (responseData: Response) => void;
|
|
type ResponseMap = {
|
|
[response: string]: ResponseCallback | undefined;
|
|
};
|
|
|
|
const state = reactive({
|
|
responseMap: {} as ResponseMap,
|
|
});
|
|
|
|
export enum ResponseType {
|
|
UpdateCanvas = "UpdateCanvas",
|
|
UpdateScrollbars = "UpdateScrollbars",
|
|
ExportDocument = "ExportDocument",
|
|
SaveDocument = "SaveDocument",
|
|
OpenDocumentBrowse = "OpenDocumentBrowse",
|
|
DisplayFolderTreeStructure = "DisplayFolderTreeStructure",
|
|
UpdateLayer = "UpdateLayer",
|
|
SetActiveTool = "SetActiveTool",
|
|
SetActiveDocument = "SetActiveDocument",
|
|
UpdateOpenDocumentsList = "UpdateOpenDocumentsList",
|
|
UpdateWorkingColors = "UpdateWorkingColors",
|
|
SetCanvasZoom = "SetCanvasZoom",
|
|
SetCanvasRotation = "SetCanvasRotation",
|
|
DisplayError = "DisplayError",
|
|
DisplayPanic = "DisplayPanic",
|
|
DisplayConfirmationToCloseDocument = "DisplayConfirmationToCloseDocument",
|
|
DisplayConfirmationToCloseAllDocuments = "DisplayConfirmationToCloseAllDocuments",
|
|
}
|
|
|
|
export function registerResponseHandler(responseType: ResponseType, callback: ResponseCallback) {
|
|
state.responseMap[responseType] = callback;
|
|
}
|
|
|
|
export function handleResponse(responseType: string, responseData: any) {
|
|
const callback = state.responseMap[responseType];
|
|
const data = parseResponse(responseType, responseData);
|
|
|
|
if (callback && data) {
|
|
callback(data);
|
|
} else if (data) {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`);
|
|
} else {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`Received a Response of type "${responseType}" but but was not able to parse the data.`);
|
|
}
|
|
}
|
|
|
|
function parseResponse(responseType: string, data: any): Response {
|
|
switch (responseType) {
|
|
case "DocumentChanged":
|
|
return newDocumentChanged(data.DocumentChanged);
|
|
case "DisplayFolderTreeStructure":
|
|
return newDisplayFolderTreeStructure(data.DisplayFolderTreeStructure);
|
|
case "SetActiveTool":
|
|
return newSetActiveTool(data.SetActiveTool);
|
|
case "SetActiveDocument":
|
|
return newSetActiveDocument(data.SetActiveDocument);
|
|
case "UpdateOpenDocumentsList":
|
|
return newUpdateOpenDocumentsList(data.UpdateOpenDocumentsList);
|
|
case "UpdateCanvas":
|
|
return newUpdateCanvas(data.UpdateCanvas);
|
|
case "UpdateScrollbars":
|
|
return newUpdateScrollbars(data.UpdateScrollbars);
|
|
case "UpdateLayer":
|
|
return newUpdateLayer(data.UpdateLayer);
|
|
case "SetCanvasZoom":
|
|
return newSetCanvasZoom(data.SetCanvasZoom);
|
|
case "SetCanvasRotation":
|
|
return newSetCanvasRotation(data.SetCanvasRotation);
|
|
case "ExportDocument":
|
|
return newExportDocument(data.ExportDocument);
|
|
case "SaveDocument":
|
|
return newSaveDocument(data.SaveDocument);
|
|
case "OpenDocumentBrowse":
|
|
return newOpenDocumentBrowse(data.OpenDocumentBrowse);
|
|
case "UpdateWorkingColors":
|
|
return newUpdateWorkingColors(data.UpdateWorkingColors);
|
|
case "DisplayError":
|
|
return newDisplayError(data.DisplayError);
|
|
case "DisplayPanic":
|
|
return newDisplayPanic(data.DisplayPanic);
|
|
case "DisplayConfirmationToCloseDocument":
|
|
return newDisplayConfirmationToCloseDocument(data.DisplayConfirmationToCloseDocument);
|
|
case "DisplayConfirmationToCloseAllDocuments":
|
|
return newDisplayConfirmationToCloseAllDocuments(data.DisplayConfirmationToCloseAllDocuments);
|
|
default:
|
|
throw new Error(`Unrecognized origin/responseType pair: ${origin}, '${responseType}'`);
|
|
}
|
|
}
|
|
|
|
export type Response = SetActiveTool | UpdateCanvas | UpdateScrollbars | UpdateLayer | DocumentChanged | DisplayFolderTreeStructure | UpdateWorkingColors | SetCanvasZoom | SetCanvasRotation;
|
|
|
|
export interface UpdateOpenDocumentsList {
|
|
open_documents: Array<string>;
|
|
}
|
|
function newUpdateOpenDocumentsList(input: any): UpdateOpenDocumentsList {
|
|
return { open_documents: input.open_documents };
|
|
}
|
|
|
|
export interface Color {
|
|
red: number;
|
|
green: number;
|
|
blue: number;
|
|
alpha: number;
|
|
}
|
|
function newColor(input: any): Color {
|
|
// TODO: Possibly change this in the Rust side to avoid any pitfalls
|
|
return { red: input.red * 255, green: input.green * 255, blue: input.blue * 255, alpha: input.alpha };
|
|
}
|
|
|
|
export interface UpdateWorkingColors {
|
|
primary: Color;
|
|
secondary: Color;
|
|
}
|
|
function newUpdateWorkingColors(input: any): UpdateWorkingColors {
|
|
return {
|
|
primary: newColor(input.primary),
|
|
secondary: newColor(input.secondary),
|
|
};
|
|
}
|
|
|
|
export interface SetActiveTool {
|
|
tool_name: string;
|
|
tool_options: object;
|
|
}
|
|
function newSetActiveTool(input: any): SetActiveTool {
|
|
return {
|
|
tool_name: input.tool_name,
|
|
tool_options: input.tool_options,
|
|
};
|
|
}
|
|
|
|
export interface SetActiveDocument {
|
|
document_index: number;
|
|
}
|
|
function newSetActiveDocument(input: any): SetActiveDocument {
|
|
return {
|
|
document_index: input.document_index,
|
|
};
|
|
}
|
|
|
|
export interface DisplayError {
|
|
title: string;
|
|
description: string;
|
|
}
|
|
function newDisplayError(input: any): DisplayError {
|
|
return {
|
|
title: input.title,
|
|
description: input.description,
|
|
};
|
|
}
|
|
|
|
export interface DisplayPanic {
|
|
panic_info: string;
|
|
title: string;
|
|
description: string;
|
|
}
|
|
function newDisplayPanic(input: any): DisplayPanic {
|
|
return {
|
|
panic_info: input.panic_info,
|
|
title: input.title,
|
|
description: input.description,
|
|
};
|
|
}
|
|
|
|
export interface DisplayConfirmationToCloseDocument {
|
|
document_index: number;
|
|
}
|
|
function newDisplayConfirmationToCloseDocument(input: any): DisplayConfirmationToCloseDocument {
|
|
return {
|
|
document_index: input.document_index,
|
|
};
|
|
}
|
|
|
|
function newDisplayConfirmationToCloseAllDocuments(_input: any): {} {
|
|
return {};
|
|
}
|
|
|
|
export interface UpdateCanvas {
|
|
document: string;
|
|
}
|
|
function newUpdateCanvas(input: any): UpdateCanvas {
|
|
return {
|
|
document: input.document,
|
|
};
|
|
}
|
|
|
|
export interface UpdateScrollbars {
|
|
position: { x: number; y: number };
|
|
size: { x: number; y: number };
|
|
multiplier: { x: number; y: number };
|
|
}
|
|
function newUpdateScrollbars(input: any): UpdateScrollbars {
|
|
return {
|
|
position: { x: input.position[0], y: input.position[1] },
|
|
size: { x: input.size[0], y: input.size[1] },
|
|
multiplier: { x: input.multiplier[0], y: input.multiplier[1] },
|
|
};
|
|
}
|
|
|
|
export interface ExportDocument {
|
|
document: string;
|
|
name: string;
|
|
}
|
|
function newExportDocument(input: any): ExportDocument {
|
|
return {
|
|
document: input.document,
|
|
name: input.name,
|
|
};
|
|
}
|
|
|
|
export interface SaveDocument {
|
|
document: string;
|
|
name: string;
|
|
}
|
|
function newSaveDocument(input: any): SaveDocument {
|
|
return {
|
|
document: input.document,
|
|
name: input.name,
|
|
};
|
|
}
|
|
|
|
export type OpenDocumentBrowse = {};
|
|
function newOpenDocumentBrowse(_: any): OpenDocumentBrowse {
|
|
return {};
|
|
}
|
|
|
|
export type DocumentChanged = {};
|
|
function newDocumentChanged(_: any): DocumentChanged {
|
|
return {};
|
|
}
|
|
|
|
export interface DisplayFolderTreeStructure {
|
|
layerId: BigInt;
|
|
children: DisplayFolderTreeStructure[];
|
|
}
|
|
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 {
|
|
path: BigUint64Array;
|
|
data: LayerPanelEntry;
|
|
}
|
|
function newUpdateLayer(input: any): UpdateLayer {
|
|
return {
|
|
path: newPath(input.data.path),
|
|
data: newLayerPanelEntry(input.data),
|
|
};
|
|
}
|
|
|
|
export interface SetCanvasZoom {
|
|
new_zoom: number;
|
|
}
|
|
function newSetCanvasZoom(input: any): SetCanvasZoom {
|
|
return {
|
|
new_zoom: input.new_zoom,
|
|
};
|
|
}
|
|
|
|
export interface SetCanvasRotation {
|
|
new_radians: number;
|
|
}
|
|
function newSetCanvasRotation(input: any): SetCanvasRotation {
|
|
return {
|
|
new_radians: input.new_radians,
|
|
};
|
|
}
|
|
|
|
function newPath(input: any): BigUint64Array {
|
|
// eslint-disable-next-line
|
|
const u32CombinedPairs = input.map((n: Array<number>) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1])));
|
|
return new BigUint64Array(u32CombinedPairs);
|
|
}
|
|
|
|
export enum BlendMode {
|
|
Normal = "normal",
|
|
Multiply = "multiply",
|
|
Darken = "darken",
|
|
ColorBurn = "color-burn",
|
|
Screen = "screen",
|
|
Lighten = "lighten",
|
|
ColorDodge = "color-dodge",
|
|
Overlay = "overlay",
|
|
SoftLight = "soft-light",
|
|
HardLight = "hard-light",
|
|
Difference = "difference",
|
|
Exclusion = "exclusion",
|
|
Hue = "hue",
|
|
Saturation = "saturation",
|
|
Color = "color",
|
|
Luminosity = "luminosity",
|
|
}
|
|
function newBlendMode(input: string): BlendMode {
|
|
const blendMode = {
|
|
Normal: BlendMode.Normal,
|
|
Multiply: BlendMode.Multiply,
|
|
Darken: BlendMode.Darken,
|
|
ColorBurn: BlendMode.ColorBurn,
|
|
Screen: BlendMode.Screen,
|
|
Lighten: BlendMode.Lighten,
|
|
ColorDodge: BlendMode.ColorDodge,
|
|
Overlay: BlendMode.Overlay,
|
|
SoftLight: BlendMode.SoftLight,
|
|
HardLight: BlendMode.HardLight,
|
|
Difference: BlendMode.Difference,
|
|
Exclusion: BlendMode.Exclusion,
|
|
Hue: BlendMode.Hue,
|
|
Saturation: BlendMode.Saturation,
|
|
Color: BlendMode.Color,
|
|
Luminosity: BlendMode.Luminosity,
|
|
}[input];
|
|
|
|
if (!blendMode) throw new Error(`Invalid blend mode "${blendMode}"`);
|
|
|
|
return blendMode;
|
|
}
|
|
|
|
function newOpacity(input: number): number {
|
|
return input * 100;
|
|
}
|
|
|
|
export interface LayerPanelEntry {
|
|
name: string;
|
|
visible: boolean;
|
|
blend_mode: BlendMode;
|
|
opacity: number;
|
|
layer_type: LayerType;
|
|
path: BigUint64Array;
|
|
layer_data: LayerData;
|
|
thumbnail: string;
|
|
}
|
|
function newLayerPanelEntry(input: any): LayerPanelEntry {
|
|
return {
|
|
name: input.name,
|
|
visible: input.visible,
|
|
blend_mode: newBlendMode(input.blend_mode),
|
|
opacity: newOpacity(input.opacity),
|
|
layer_type: newLayerType(input.layer_type),
|
|
layer_data: newLayerData(input.layer_data),
|
|
path: newPath(input.path),
|
|
thumbnail: input.thumbnail,
|
|
};
|
|
}
|
|
|
|
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",
|
|
Circle = "Circle",
|
|
Rect = "Rect",
|
|
Line = "Line",
|
|
PolyLine = "PolyLine",
|
|
Ellipse = "Ellipse",
|
|
}
|
|
function newLayerType(input: any): LayerType {
|
|
switch (input) {
|
|
case "Folder":
|
|
return LayerType.Folder;
|
|
case "Shape":
|
|
return LayerType.Shape;
|
|
case "Circle":
|
|
return LayerType.Circle;
|
|
case "Rect":
|
|
return LayerType.Rect;
|
|
case "Line":
|
|
return LayerType.Line;
|
|
case "PolyLine":
|
|
return LayerType.PolyLine;
|
|
case "Ellipse":
|
|
return LayerType.Ellipse;
|
|
default:
|
|
throw Error(`Received invalid input as an enum variant for LayerType: ${input}`);
|
|
}
|
|
}
|