mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 23:08:12 +08:00
Rework wasm initialization and reduce global state (#379)
* wasm: do the async initialization only once This allows the rest of the app to access wasm synchronously. This allows removing of a global. * provide the wasm via vue provide/inject. There's still code directly accessing the wasm. That will be changed later. * MenuBarInput: use injected wasm instead of the global instance * Let the App handle event listeners * move stateful modules into state/ * state/fullscreen: create per instance * App: load the initial document list on mount. This got lost a few commits ago. Now it's back. * state/dialog: create per instance * util/input: remove dependency on global dialog instance * state/documents: create per instance * reponse-handler: move into EditorWasm * comingSoon: move into dialog * wasm: allow instantiating multiple editors * input handlers: do not look at canvases outside the mounted App * input: listen on the container instead of the window when possible * - removed proxy from wasm-loader - integrated with js-dispatcher - state functions to classes - integrated some upstream changes * fix errors caused by merge * Getting closer: - added global state to track all instances - fix fullscreen close trigger - wasm-loader is statefull - panic across instanes * - fix outline while using editor - removed circular import rule - added editorInstance to js message constructor * - changed input handler to a class - still need a better way of handeling it in App.vue * - fixed single instance of inputManager to weakmap * - fix no-explicit-any in a few places - removed global state from input.ts * simplified two long lines * removed global state * removed $data from App * add mut self to functions in api.rs * Update Workspace.vue remove outdated import * fixed missing import * Changes throughout code review; note this causes some bugs to be fixed in a later commit * PR review round 1 * - fix coming soon bugs - changed folder structure * moved declaration to .d.ts * - changed from classes to functions - moved decs back to app.vue * removed need to export js function to rust * changed folder structure * fixed indentation breaking multiline strings * Fix eslint rule to whitelist @/../ * Simplify strip-indents implementation * replace type assertions with better annotations or proper runtime checks * Small tweaks and code rearranging improvements after second code review pass * maybe fix mouse events * Add back preventDefault for mouse scroll * code review round 2 * Comment improvements * -removed runtime checks - fixed layers not showing * - extened proxy to cover classes - stopped multiple panics from logging - Stop wasm-bindgen from mut ref counting our struct * cleaned up messageConstructors exports * Fix input and fullscreen regressions Co-authored-by: Max Fisher <maxmfishernj@gmail.com> Co-authored-by: mfish33 <32677537+mfish33@users.noreply.github.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Max Fisher
mfish33
Keavon Chambers
parent
3c1ed1a235
commit
0aefa1bb07
@@ -0,0 +1,58 @@
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { JsMessageType, messageConstructors, JsMessage } from "@/dispatcher/js-messages";
|
||||
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
|
||||
|
||||
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
|
||||
type JsMessageCallbackMap = {
|
||||
// Don't know a better way of typing this since it can be any subclass of JsMessage
|
||||
// The functions interacting with this map are strongly typed though around JsMessage
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[message: string]: JsMessageCallback<any> | undefined;
|
||||
};
|
||||
|
||||
export function createJsDispatcher() {
|
||||
const subscriptions: JsMessageCallbackMap = {};
|
||||
|
||||
const subscribeJsMessage = <T extends JsMessage, Args extends unknown[]>(messageType: new (...args: Args) => T, callback: JsMessageCallback<T>) => {
|
||||
subscriptions[messageType.name] = callback;
|
||||
};
|
||||
|
||||
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmInstance, instance: RustEditorInstance) => {
|
||||
const messageConstructor = messageConstructors[messageType];
|
||||
if (!messageConstructor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a frontend message of type "${messageType}" but but was not able to parse the data.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
|
||||
// Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage"
|
||||
const unwrappedMessageData = messageData[messageType] || {};
|
||||
|
||||
const isJsMessageConstructor = (fn: typeof messageConstructor): fn is typeof JsMessage => {
|
||||
return "jsMessageMarker" in fn;
|
||||
};
|
||||
let message: JsMessage;
|
||||
if (isJsMessageConstructor(messageConstructor)) {
|
||||
message = plainToInstance(messageConstructor, unwrappedMessageData);
|
||||
} else {
|
||||
message = messageConstructor(unwrappedMessageData, wasm, instance);
|
||||
}
|
||||
|
||||
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler
|
||||
const callback = subscriptions[message.constructor.name];
|
||||
|
||||
if (callback && message) {
|
||||
callback(message);
|
||||
} else if (message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
subscribeJsMessage,
|
||||
handleJsMessage,
|
||||
};
|
||||
}
|
||||
export type JsDispatcher = ReturnType<typeof createJsDispatcher>;
|
||||
@@ -0,0 +1,289 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { Transform, Type } from "class-transformer";
|
||||
|
||||
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
|
||||
|
||||
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 DisplayAboutGraphiteDialog 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();
|
||||
}
|
||||
}
|
||||
|
||||
interface DataBuffer {
|
||||
pointer: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export function newDisplayFolderTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayFolderTreeStructure {
|
||||
const { pointer, length } = input.data_buffer;
|
||||
const wasmMemoryBuffer = wasm.wasm_memory().buffer;
|
||||
|
||||
// Decode the folder structure encoding
|
||||
const encoding = new DataView(wasmMemoryBuffer, pointer, length);
|
||||
|
||||
// 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, pointer + 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, pointer + 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: number[][]): 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"
|
||||
| "ColorBurn"
|
||||
| "Screen"
|
||||
| "Lighten"
|
||||
| "ColorDodge"
|
||||
| "Overlay"
|
||||
| "SoftLight"
|
||||
| "HardLight"
|
||||
| "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];
|
||||
|
||||
// Any is used since the type of the object should be known from the rust side
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
|
||||
type MessageMaker = typeof JsMessage | JSMessageFactory;
|
||||
|
||||
export const messageConstructors: Record<string, MessageMaker> = {
|
||||
UpdateCanvas,
|
||||
UpdateScrollbars,
|
||||
UpdateRulers,
|
||||
ExportDocument,
|
||||
SaveDocument,
|
||||
OpenDocumentBrowse,
|
||||
DisplayFolderTreeStructure: newDisplayFolderTreeStructure,
|
||||
UpdateLayer,
|
||||
SetActiveTool,
|
||||
SetActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdateWorkingColors,
|
||||
SetCanvasZoom,
|
||||
SetCanvasRotation,
|
||||
DisplayError,
|
||||
DisplayPanic,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
DisplayAboutGraphiteDialog,
|
||||
} as const;
|
||||
export type JsMessageType = keyof typeof messageConstructors;
|
||||
Reference in New Issue
Block a user