mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 02:28:13 +08:00
Change responses to use classes instead of interfaces (#394)
* ability to mark an open document as unsaved * unsaved detection now being triggered based on layer tree height * Changed responses to use classes instead of interfaces * - rust implementation of unsaved markers - upgraded eslint * updated eslint in package.json * - Renamed GetOpenDocumentsList -> UpdateOpenDocumentsList - is not -> was not * changed hash to current identifier to better reflect its meaning * resolve some merge conflicts * removed console.log statement leftover from debuging * - changed Response to jsMessage - split files - Array<> -> [] * -remove path from UpdateLayer * - remove unused if statements * - comment for reflect-metadata - registerJsMessageHandler -> subscribeJsMessage - readonly message properties - fixed binding filename and comment - toRgb -> toRgba * - newOpacity -> transformer - added comments * MessageMaker -> messageMaker
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
||||
import { subscribeJsMessage } from "@/utilities/js-message-dispatcher";
|
||||
import {
|
||||
ResponseType,
|
||||
registerResponseHandler,
|
||||
Response,
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
SetActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
ExportDocument,
|
||||
SaveDocument,
|
||||
} from "@/utilities/response-handler";
|
||||
OpenDocumentBrowse,
|
||||
} from "@/utilities/js-messages";
|
||||
import { download, upload } from "@/utilities/files";
|
||||
import { panicProxy } from "@/utilities/panic-proxy";
|
||||
|
||||
@@ -94,41 +94,34 @@ export async function closeAllDocumentsWithConfirmation() {
|
||||
|
||||
export default readonly(state);
|
||||
|
||||
registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Response) => {
|
||||
const documentListData = responseData as UpdateOpenDocumentsList;
|
||||
state.documents = documentListData.open_documents.map(({ name, isSaved }) => new DocumentState(name, isSaved));
|
||||
subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => {
|
||||
state.documents = updateOpenDocumentList.open_documents.map(({ name, isSaved }) => new DocumentState(name, isSaved));
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
||||
const documentData = responseData as SetActiveDocument;
|
||||
if (documentData) {
|
||||
state.activeDocumentIndex = documentData.document_index;
|
||||
}
|
||||
subscribeJsMessage(SetActiveDocument, (setActiveDocument) => {
|
||||
state.activeDocumentIndex = setActiveDocument.document_index;
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.DisplayConfirmationToCloseDocument, (responseData: Response) => {
|
||||
const data = responseData as DisplayConfirmationToCloseDocument;
|
||||
closeDocumentWithConfirmation(data.document_index);
|
||||
subscribeJsMessage(DisplayConfirmationToCloseDocument, (displayConfirmationToCloseDocument) => {
|
||||
closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_index);
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.DisplayConfirmationToCloseAllDocuments, (_: Response) => {
|
||||
subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => {
|
||||
closeAllDocumentsWithConfirmation();
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.OpenDocumentBrowse, async (_: Response) => {
|
||||
subscribeJsMessage(OpenDocumentBrowse, async () => {
|
||||
const extension = (await wasm).file_save_suffix();
|
||||
const data = await upload(extension);
|
||||
(await wasm).open_document_file(data.filename, data.content);
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.ExportDocument, (responseData: Response) => {
|
||||
const updateData = responseData as ExportDocument;
|
||||
if (updateData) download(updateData.name, updateData.document);
|
||||
subscribeJsMessage(ExportDocument, (exportDocument) => {
|
||||
download(exportDocument.name, exportDocument.document);
|
||||
});
|
||||
|
||||
registerResponseHandler(ResponseType.SaveDocument, (responseData: Response) => {
|
||||
const saveData = responseData as SaveDocument;
|
||||
if (saveData) download(saveData.name, saveData.document);
|
||||
subscribeJsMessage(SaveDocument, (saveDocument) => {
|
||||
download(saveDocument.name, saveDocument.document);
|
||||
});
|
||||
|
||||
(async () => (await wasm).get_open_documents_list())();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
||||
import { TextButtonWidget } from "@/components/widgets/widgets";
|
||||
import { ResponseType, registerResponseHandler, Response, DisplayError, DisplayPanic } from "@/utilities/response-handler";
|
||||
import { subscribeJsMessage } from "@/utilities/js-message-dispatcher";
|
||||
import { DisplayError, DisplayPanic } from "./js-messages";
|
||||
|
||||
// Coming soon dialog
|
||||
export function comingSoon(issueNumber?: number) {
|
||||
@@ -24,9 +25,7 @@ export function comingSoon(issueNumber?: number) {
|
||||
}
|
||||
|
||||
// Graphite error dialog
|
||||
registerResponseHandler(ResponseType.DisplayError, (responseData: Response) => {
|
||||
const data = responseData as DisplayError;
|
||||
|
||||
subscribeJsMessage(DisplayError, (displayError) => {
|
||||
const okButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => dismissDialog(),
|
||||
@@ -34,17 +33,15 @@ registerResponseHandler(ResponseType.DisplayError, (responseData: Response) => {
|
||||
};
|
||||
const buttons = [okButton];
|
||||
|
||||
createDialog("Warning", data.title, data.description, buttons);
|
||||
createDialog("Warning", displayError.title, displayError.description, buttons);
|
||||
});
|
||||
|
||||
// Code panic dialog and console error
|
||||
registerResponseHandler(ResponseType.DisplayPanic, (responseData: Response) => {
|
||||
const data = responseData as DisplayPanic;
|
||||
|
||||
subscribeJsMessage(DisplayPanic, (displayPanic) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Error as any).stackTraceLimit = Infinity;
|
||||
const stackTrace = new Error().stack || "";
|
||||
const panicDetails = `${data.panic_info}\n\n${stackTrace}`;
|
||||
const panicDetails = `${displayPanic.panic_info}\n\n${stackTrace}`;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(panicDetails);
|
||||
@@ -66,7 +63,7 @@ registerResponseHandler(ResponseType.DisplayPanic, (responseData: Response) => {
|
||||
};
|
||||
const buttons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
||||
|
||||
createDialog("Warning", data.title, data.description, buttons);
|
||||
createDialog("Warning", displayPanic.title, displayPanic.description, buttons);
|
||||
});
|
||||
|
||||
function githubUrl(panicDetails: string) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file is instantiated by wasm-bindgen in `/frontend/wasm/src/lib.rs` and re-exports the `handleJsMessage` function to
|
||||
// provide access to the global copy of `js-message-dispatcher.ts` with its shared state, not an isolated duplicate with empty state
|
||||
|
||||
export { handleJsMessage } from "@/utilities/js-message-dispatcher";
|
||||
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { reactive } from "vue";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import {
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
DisplayError,
|
||||
DisplayPanic,
|
||||
ExportDocument,
|
||||
newDisplayFolderTreeStructure,
|
||||
OpenDocumentBrowse,
|
||||
SaveDocument,
|
||||
SetActiveDocument,
|
||||
SetActiveTool,
|
||||
SetCanvasRotation,
|
||||
SetCanvasZoom,
|
||||
UpdateCanvas,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateRulers,
|
||||
UpdateScrollbars,
|
||||
UpdateWorkingColors,
|
||||
UpdateLayer,
|
||||
JsMessage,
|
||||
} from "./js-messages";
|
||||
|
||||
type JsMessageCallback<T extends JsMessage> = (responseData: T) => void;
|
||||
type JsMessageCallbackMap = {
|
||||
[response: string]: JsMessageCallback<any> | undefined;
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
responseMap: {} as JsMessageCallbackMap,
|
||||
});
|
||||
|
||||
type Constructs<T> = new (...args: any[]) => T;
|
||||
type ConstructsJsMessage = Constructs<JsMessage> & typeof JsMessage;
|
||||
|
||||
const responseMap = {
|
||||
UpdateCanvas,
|
||||
UpdateScrollbars,
|
||||
UpdateRulers,
|
||||
ExportDocument,
|
||||
SaveDocument,
|
||||
OpenDocumentBrowse,
|
||||
DisplayFolderTreeStructure: newDisplayFolderTreeStructure,
|
||||
UpdateLayer,
|
||||
SetActiveTool,
|
||||
SetActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateWorkingColors,
|
||||
SetCanvasZoom,
|
||||
SetCanvasRotation,
|
||||
DisplayError,
|
||||
DisplayPanic,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
} as const;
|
||||
|
||||
export type JsMessageType = keyof typeof responseMap;
|
||||
|
||||
function isJsMessageConstructor(fn: ConstructsJsMessage | ((data: any) => JsMessage)): fn is ConstructsJsMessage {
|
||||
return (fn as ConstructsJsMessage).jsMessageMarker !== undefined;
|
||||
}
|
||||
|
||||
export function handleJsMessage(responseType: JsMessageType, responseData: any) {
|
||||
const messageMaker = responseMap[responseType];
|
||||
let message: JsMessage;
|
||||
|
||||
if (!messageMaker) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a Response of type "${responseType}" but but was not able to parse the data.`);
|
||||
}
|
||||
|
||||
if (isJsMessageConstructor(messageMaker)) {
|
||||
message = plainToInstance(messageMaker, responseData[responseType]);
|
||||
} else {
|
||||
message = messageMaker(responseData[responseType]);
|
||||
}
|
||||
|
||||
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
|
||||
const callback = state.responseMap[message.constructor.name];
|
||||
|
||||
if (callback && message) {
|
||||
callback(message);
|
||||
} else if (message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeJsMessage<T extends JsMessage>(responseType: Constructs<T>, callback: JsMessageCallback<T>) {
|
||||
state.responseMap[responseType.name] = callback;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Transform, Type } from "class-transformer";
|
||||
|
||||
export class JsMessage {
|
||||
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
|
||||
static readonly jsMessageMarker = true;
|
||||
}
|
||||
|
||||
export class UpdateOpenDocumentsList extends JsMessage {
|
||||
@Transform(({ value }) => value.map((tuple: [string, boolean]) => ({ name: tuple[0], isSaved: tuple[1] })))
|
||||
readonly open_documents!: { name: string; isSaved: boolean }[];
|
||||
}
|
||||
|
||||
const To255Scale = Transform(({ value }) => value * 255);
|
||||
export class Color {
|
||||
@To255Scale
|
||||
readonly red!: number;
|
||||
|
||||
@To255Scale
|
||||
readonly green!: number;
|
||||
|
||||
@To255Scale
|
||||
readonly blue!: number;
|
||||
|
||||
readonly alpha!: number;
|
||||
|
||||
toRgba() {
|
||||
return { r: this.red, g: this.green, b: this.blue, a: this.alpha };
|
||||
}
|
||||
|
||||
toRgbaCSS() {
|
||||
const { r, g, b, a } = this.toRgba();
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateWorkingColors extends JsMessage {
|
||||
@Type(() => Color)
|
||||
readonly primary!: Color;
|
||||
|
||||
@Type(() => Color)
|
||||
readonly secondary!: Color;
|
||||
}
|
||||
|
||||
export class SetActiveTool extends JsMessage {
|
||||
readonly tool_name!: string;
|
||||
|
||||
readonly tool_options!: object;
|
||||
}
|
||||
|
||||
export class SetActiveDocument extends JsMessage {
|
||||
readonly document_index!: number;
|
||||
}
|
||||
|
||||
export class DisplayError extends JsMessage {
|
||||
readonly title!: string;
|
||||
|
||||
readonly description!: string;
|
||||
}
|
||||
|
||||
export class DisplayPanic extends JsMessage {
|
||||
readonly panic_info!: string;
|
||||
|
||||
readonly title!: string;
|
||||
|
||||
readonly description!: string;
|
||||
}
|
||||
|
||||
export class DisplayConfirmationToCloseDocument extends JsMessage {
|
||||
readonly document_index!: number;
|
||||
}
|
||||
|
||||
export class DisplayConfirmationToCloseAllDocuments extends JsMessage {}
|
||||
|
||||
export class UpdateCanvas extends JsMessage {
|
||||
readonly document!: string;
|
||||
}
|
||||
|
||||
const TupleToVec2 = Transform(({ value }) => ({ x: value[0], y: value[1] }));
|
||||
|
||||
export class UpdateScrollbars extends JsMessage {
|
||||
@TupleToVec2
|
||||
readonly position!: { x: number; y: number };
|
||||
|
||||
@TupleToVec2
|
||||
readonly size!: { x: number; y: number };
|
||||
|
||||
@TupleToVec2
|
||||
readonly multiplier!: { x: number; y: number };
|
||||
}
|
||||
|
||||
export class UpdateRulers extends JsMessage {
|
||||
@TupleToVec2
|
||||
readonly origin!: { x: number; y: number };
|
||||
|
||||
readonly spacing!: number;
|
||||
|
||||
readonly interval!: number;
|
||||
}
|
||||
|
||||
export class ExportDocument extends JsMessage {
|
||||
readonly document!: string;
|
||||
|
||||
readonly name!: string;
|
||||
}
|
||||
|
||||
export class SaveDocument extends JsMessage {
|
||||
readonly document!: string;
|
||||
|
||||
readonly name!: string;
|
||||
}
|
||||
|
||||
export class OpenDocumentBrowse extends JsMessage {}
|
||||
|
||||
export class DocumentChanged extends JsMessage {}
|
||||
|
||||
export class DisplayFolderTreeStructure extends JsMessage {
|
||||
constructor(readonly layerId: BigInt, readonly children: DisplayFolderTreeStructure[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
export 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 = new DisplayFolderTreeStructure(BigInt(-1), []);
|
||||
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 = new DisplayFolderTreeStructure(layerId, []);
|
||||
currentFolder.children.push(childLayer);
|
||||
}
|
||||
|
||||
// Check the sign of the MSB, where a 1 is a negative (outward) indent
|
||||
const subsequentDirectionOfDepthChange = (msbSigned & msbMask) === BigInt(0);
|
||||
// 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 class UpdateLayer extends JsMessage {
|
||||
@Type(() => LayerPanelEntry)
|
||||
readonly data!: LayerPanelEntry;
|
||||
}
|
||||
|
||||
export class SetCanvasZoom extends JsMessage {
|
||||
readonly new_zoom!: number;
|
||||
}
|
||||
|
||||
export class SetCanvasRotation extends JsMessage {
|
||||
readonly new_radians!: number;
|
||||
}
|
||||
|
||||
function newPath(input: any): BigUint64Array {
|
||||
// eslint-disable-next-line
|
||||
const u32CombinedPairs = input.map((n: number[]) => BigInt((BigInt(n[0]) << BigInt(32)) | BigInt(n[1])));
|
||||
return new BigUint64Array(u32CombinedPairs);
|
||||
}
|
||||
|
||||
export type BlendMode =
|
||||
| "normal"
|
||||
| "multiply"
|
||||
| "darken"
|
||||
| "color-burn"
|
||||
| "screen"
|
||||
| "lighten"
|
||||
| "color-dodge"
|
||||
| "overlay"
|
||||
| "soft-light"
|
||||
| "hard-light"
|
||||
| "difference"
|
||||
| "exclusion"
|
||||
| "hue"
|
||||
| "saturation"
|
||||
| "color"
|
||||
| "luminosity";
|
||||
|
||||
export class LayerPanelEntry {
|
||||
name!: string;
|
||||
|
||||
visible!: boolean;
|
||||
|
||||
blend_mode!: BlendMode;
|
||||
|
||||
// On the rust side opacity is out of 1 rather than 100
|
||||
@Transform(({ value }) => value * 100)
|
||||
opacity!: number;
|
||||
|
||||
layer_type!: LayerType;
|
||||
|
||||
@Transform(({ value }) => newPath(value))
|
||||
path!: BigUint64Array;
|
||||
|
||||
@Type(() => LayerData)
|
||||
layer_data!: LayerData;
|
||||
|
||||
thumbnail!: string;
|
||||
}
|
||||
|
||||
export class LayerData {
|
||||
expanded!: boolean;
|
||||
|
||||
selected!: boolean;
|
||||
}
|
||||
|
||||
export const LayerTypeOptions = {
|
||||
Folder: "Folder",
|
||||
Shape: "Shape",
|
||||
Circle: "Circle",
|
||||
Rect: "Rect",
|
||||
Line: "Line",
|
||||
PolyLine: "PolyLine",
|
||||
Ellipse: "Ellipse",
|
||||
} as const;
|
||||
|
||||
export type LayerType = typeof LayerTypeOptions[keyof typeof LayerTypeOptions];
|
||||
@@ -1,4 +0,0 @@
|
||||
// This file is instantiated by wasm-bindgen in `/frontend/wasm/src/lib.rs` and re-exports the `handleResponse` function to
|
||||
// provide access to the global copy of `response-handler.ts` with its shared state, not an isolated duplicate with empty state
|
||||
|
||||
export { handleResponse } from "@/utilities/response-handler";
|
||||
@@ -1,478 +0,0 @@
|
||||
/* 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",
|
||||
UpdateRulers = "UpdateRulers",
|
||||
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 "UpdateRulers":
|
||||
return newUpdateRulers(data.UpdateRulers);
|
||||
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 =
|
||||
| DocumentChanged
|
||||
| DisplayFolderTreeStructure
|
||||
| SetActiveTool
|
||||
| SetActiveDocument
|
||||
| UpdateOpenDocumentsList
|
||||
| UpdateCanvas
|
||||
| UpdateScrollbars
|
||||
| UpdateRulers
|
||||
| UpdateLayer
|
||||
| SetCanvasZoom
|
||||
| SetCanvasRotation
|
||||
| ExportDocument
|
||||
| SaveDocument
|
||||
| OpenDocumentBrowse
|
||||
| UpdateWorkingColors
|
||||
| DisplayError
|
||||
| DisplayPanic
|
||||
| DisplayConfirmationToCloseDocument
|
||||
| DisplayConfirmationToCloseAllDocuments;
|
||||
|
||||
export interface UpdateOpenDocumentsList {
|
||||
open_documents: { name: string; isSaved: boolean }[];
|
||||
}
|
||||
function newUpdateOpenDocumentsList(input: any): UpdateOpenDocumentsList {
|
||||
const openDocuments = input.open_documents.map((docData: [string, boolean]) => ({ name: docData[0], isSaved: docData[1] }));
|
||||
return { open_documents: openDocuments };
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export type DisplayConfirmationToCloseAllDocuments = Record<string, never>;
|
||||
|
||||
function newDisplayConfirmationToCloseAllDocuments(_input: any): DisplayConfirmationToCloseAllDocuments {
|
||||
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 UpdateRulers {
|
||||
origin: { x: number; y: number };
|
||||
spacing: number;
|
||||
interval: number;
|
||||
}
|
||||
function newUpdateRulers(input: any): UpdateRulers {
|
||||
return {
|
||||
origin: { x: input.origin[0], y: input.origin[1] },
|
||||
spacing: input.spacing,
|
||||
interval: input.interval,
|
||||
};
|
||||
}
|
||||
|
||||
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 = Record<string, never>;
|
||||
function newOpenDocumentBrowse(_: any): OpenDocumentBrowse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export type DocumentChanged = Record<string, never>;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user