Remove editor instances concept and clean up JS interop code

This commit is contained in:
Keavon Chambers 2024-04-29 03:31:39 -07:00
parent 597c96a7db
commit 19eb6ce0ab
25 changed files with 256 additions and 325 deletions

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { initWasm, createEditor } from "@graphite/wasm-communication/editor";
import { type Editor as GraphiteEditor, initWasm, createEditor } from "@graphite/wasm-communication/editor";
import Editor from "@graphite/components/Editor.svelte";
let editor: ReturnType<typeof createEditor> | undefined = undefined;
let editor: GraphiteEditor | undefined = undefined;
onMount(async () => {
await initWasm();
@ -14,8 +14,8 @@
});
onDestroy(() => {
// Destroy the WASM editor instance
editor?.instance.free();
// Destroy the WASM editor handle
editor?.handle.free();
});
</script>

View file

@ -15,12 +15,12 @@
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
import { createPortfolioState } from "@graphite/state-providers/portfolio";
import { operatingSystem } from "@graphite/utility-functions/platform";
import type { createEditor } from "@graphite/wasm-communication/editor";
import { type Editor } from "@graphite/wasm-communication/editor";
import MainWindow from "@graphite/components/window/MainWindow.svelte";
// Graphite WASM editor instance
export let editor: ReturnType<typeof createEditor>;
// Graphite WASM editor
export let editor: Editor;
setContext("editor", editor);
// State provider systems
@ -48,7 +48,7 @@
onMount(() => {
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
editor.instance.initAfterFrontendReady(operatingSystem());
editor.handle.initAfterFrontendReady(operatingSystem());
});
onDestroy(() => {

View file

@ -252,7 +252,7 @@
// TODO: Replace this temporary solution that only works in Chromium-based browsers with the custom color sampler used by the Eyedropper tool
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!(window as any).EyeDropper) {
editor.instance.eyedropperSampleForColorPicker();
editor.handle.eyedropperSampleForColorPicker();
return;
}

View file

@ -127,14 +127,14 @@
const file = item.getAsFile();
if (file?.type.includes("svg")) {
const svgData = await file.text();
editor.instance.pasteSvg(svgData, e.clientX, e.clientY);
editor.handle.pasteSvg(svgData, e.clientX, e.clientY);
return;
}
if (file?.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height, e.clientX, e.clientY);
}
});
}
@ -142,23 +142,23 @@
function panCanvasX(newValue: number) {
const delta = newValue - scrollbarPos.x;
scrollbarPos.x = newValue;
editor.instance.panCanvas(-delta * scrollbarMultiplier.x, 0);
editor.handle.panCanvas(-delta * scrollbarMultiplier.x, 0);
}
function panCanvasY(newValue: number) {
const delta = newValue - scrollbarPos.y;
scrollbarPos.y = newValue;
editor.instance.panCanvas(0, -delta * scrollbarMultiplier.y);
editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y);
}
function pageX(delta: number) {
const move = delta < 0 ? 1 : -1;
editor.instance.panCanvasByFraction(move, 0);
editor.handle.panCanvasByFraction(move, 0);
}
function pageY(delta: number) {
const move = delta < 0 ? 1 : -1;
editor.instance.panCanvasByFraction(0, move);
editor.handle.panCanvasByFraction(0, move);
}
function canvasPointerDown(e: PointerEvent) {
@ -290,7 +290,7 @@
export function triggerTextCommit() {
if (!textInput) return;
const textCleaned = textInputCleanup(textInput.innerText);
editor.instance.onChangeText(textCleaned);
editor.handle.onChangeText(textCleaned);
}
export async function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) {
@ -314,7 +314,7 @@
textInput.oninput = () => {
if (!textInput) return;
editor.instance.updateBounds(textInputCleanup(textInput.innerText));
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
};
textInputMatrix = displayEditableTextbox.transform;
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
@ -371,8 +371,8 @@
const rgb = await updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor);
if (setColorChoice && rgb) {
if (setColorChoice === "Primary") editor.instance.updatePrimaryColor(...rgb, 1);
if (setColorChoice === "Secondary") editor.instance.updateSecondaryColor(...rgb, 1);
if (setColorChoice === "Primary") editor.handle.updatePrimaryColor(...rgb, 1);
if (setColorChoice === "Secondary") editor.handle.updateSecondaryColor(...rgb, 1);
}
});

View file

@ -130,15 +130,15 @@
}
function toggleLayerVisibility(id: bigint) {
editor.instance.toggleLayerVisibility(id);
editor.handle.toggleLayerVisibility(id);
}
function toggleLayerLock(id: bigint) {
editor.instance.toggleLayerLock(id);
editor.handle.toggleLayerLock(id);
}
function handleExpandArrowClick(id: bigint) {
editor.instance.toggleLayerExpansion(id);
editor.handle.toggleLayerExpansion(id);
}
async function onEditLayerName(listing: LayerListingInfo) {
@ -164,7 +164,7 @@
layers = layers;
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
editor.instance.setLayerName(listing.entry.id, name);
editor.handle.setLayerName(listing.entry.id, name);
listing.entry.name = name;
}
@ -196,11 +196,11 @@
// Don't select while we are entering text to rename the layer
if (listing.editingName) return;
editor.instance.selectLayer(listing.entry.id, accel, shift);
editor.handle.selectLayer(listing.entry.id, accel, shift);
}
async function deselectAllLayers() {
editor.instance.deselectAllLayers();
editor.handle.deselectAllLayers();
}
function isNestingLayer(layerClassification: LayerClassification) {
@ -322,7 +322,7 @@
const { select, insertParentId, insertIndex } = draggingData;
select?.();
editor.instance.moveLayerInTree(insertParentId, insertIndex);
editor.handle.moveLayerInTree(insertParentId, insertIndex);
}
draggingData = undefined;
fakeHighlight = undefined;

View file

@ -363,7 +363,7 @@
// Alt-click sets the clicked node as previewed
if (lmb && e.altKey && nodeId !== undefined) {
editor.instance.togglePreview(nodeId);
editor.handle.togglePreview(nodeId);
}
// Clicked on a port dot
@ -440,7 +440,7 @@
}
// Update the selection in the backend if it was modified
if (modifiedSelected) editor.instance.selectNodes(new BigUint64Array(updatedSelected));
if (modifiedSelected) editor.handle.selectNodes(new BigUint64Array(updatedSelected));
return;
}
@ -449,7 +449,7 @@
if (lmb) {
previousSelection = $nodeGraph.selected;
// Clear current selection
if (!e.shiftKey) editor.instance.selectNodes(new BigUint64Array(0));
if (!e.shiftKey) editor.handle.selectNodes(new BigUint64Array(0));
const graphBounds = graph?.getBoundingClientRect();
boxSelection = { startX: e.x - (graphBounds?.x || 0), startY: e.y - (graphBounds?.y || 0), endX: e.x - (graphBounds?.x || 0), endY: e.y - (graphBounds?.y || 0) };
@ -466,7 +466,7 @@
// const nodeId = node?.getAttribute("data-node") || undefined;
// if (nodeId !== undefined) {
// const id = BigInt(nodeId);
// editor.instance.enterNestedNetwork(id);
// editor.handle.enterNestedNetwork(id);
// }
}
@ -510,7 +510,7 @@
completeBoxSelection();
boxSelection = undefined;
} else if ((e.buttons & 2) !== 0) {
editor.instance.selectNodes(new BigUint64Array(previousSelection));
editor.handle.selectNodes(new BigUint64Array(previousSelection));
boxSelection = undefined;
} else {
const graphBounds = graph?.getBoundingClientRect();
@ -534,7 +534,7 @@
}
function completeBoxSelection() {
editor.instance.selectNodes(new BigUint64Array($nodeGraph.selected.concat($nodeGraph.nodes.filter((_, nodeIndex) => intersetNodeAABB(boxSelection, nodeIndex)).map((node) => node.id))));
editor.handle.selectNodes(new BigUint64Array($nodeGraph.selected.concat($nodeGraph.nodes.filter((_, nodeIndex) => intersetNodeAABB(boxSelection, nodeIndex)).map((node) => node.id))));
}
function showSelected(selected: bigint[], boxSelect: Box | undefined, node: bigint, nodeIndex: number): boolean {
@ -542,7 +542,7 @@
}
function toggleLayerVisibility(id: bigint) {
editor.instance.toggleLayerVisibility(id);
editor.handle.toggleLayerVisibility(id);
}
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
@ -589,7 +589,7 @@
const selectedNodeBounds = selectedNode.getBoundingClientRect();
const containerBoundsBounds = theNodesContainer.getBoundingClientRect();
return editor.instance.rectangleIntersects(
return editor.handle.rectangleIntersects(
new Float64Array(wireCurveLocations.map((loc) => loc.x)),
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
selectedNodeBounds.top - containerBoundsBounds.y,
@ -603,9 +603,9 @@
if (link) {
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
if (!isLayer) editor.instance.shiftNode(selectedNodeId);
editor.handle.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
editor.handle.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
if (!isLayer) editor.handle.shiftNode(selectedNodeId);
}
}
@ -614,7 +614,7 @@
const initialDisconnecting = disconnecting;
if (disconnecting) {
editor.instance.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
editor.handle.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
}
disconnecting = undefined;
@ -625,7 +625,7 @@
if (from !== undefined && to !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
editor.handle.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
} else if (linkInProgressFromConnector && !initialDisconnecting) {
// If the add node menu is already open, we don't want to open it again
@ -645,11 +645,11 @@
} else if (draggingNodes) {
if (draggingNodes.startX === e.x && draggingNodes.startY === e.y) {
if (selectIfNotDragged !== undefined && ($nodeGraph.selected.length !== 1 || $nodeGraph.selected[0] !== selectIfNotDragged)) {
editor.instance.selectNodes(new BigUint64Array([selectIfNotDragged]));
editor.handle.selectNodes(new BigUint64Array([selectIfNotDragged]));
}
}
if ($nodeGraph.selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.instance.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
if ($nodeGraph.selected.length > 0 && (draggingNodes.roundX !== 0 || draggingNodes.roundY !== 0)) editor.handle.moveSelectedNodes(draggingNodes.roundX, draggingNodes.roundY);
checkInsertBetween();
@ -670,7 +670,7 @@
const inputNodeConnectionIndex = 0;
const x = Math.round(nodeListLocation.x / GRID_SIZE);
const y = Math.round(nodeListLocation.y / GRID_SIZE) - 1;
const inputConnectedNodeID = editor.instance.createNode(nodeType, x, y);
const inputConnectedNodeID = editor.handle.createNode(nodeType, x, y);
nodeListLocation = undefined;
if (!linkInProgressFromConnector) return;
@ -678,7 +678,7 @@
if (from !== undefined) {
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
editor.handle.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
}
linkInProgressFromConnector = undefined;
@ -882,7 +882,7 @@
</div>
<div class="details">
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<span title={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}>
<span title={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined} bind:offsetWidth={layerNameLabelWidths[String(node.id)]}>
{node.alias || "Layer"}
</span>
</div>
@ -929,7 +929,7 @@
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
<IconLabel icon={nodeIcon(node.name)} />
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
<TextLabel tooltip={editor.instance.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.alias || node.name}</TextLabel>
<TextLabel tooltip={editor.handle.inDevelopmentMode() ? `Node ID: ${node.id}` : undefined}>{node.alias || node.name}</TextLabel>
</div>
<!-- Parameter rows -->
{#if exposedInputsOutputs.length > 0}

View file

@ -58,15 +58,15 @@
}
function widgetValueCommit(index: number, value: unknown) {
editor.instance.widgetValueCommit(layoutTarget, widgets[index].widgetId, value);
editor.handle.widgetValueCommit(layoutTarget, widgets[index].widgetId, value);
}
function widgetValueUpdate(index: number, value: unknown) {
editor.instance.widgetValueUpdate(layoutTarget, widgets[index].widgetId, value);
editor.handle.widgetValueUpdate(layoutTarget, widgets[index].widgetId, value);
}
function widgetValueCommitAndUpdate(index: number, value: unknown) {
editor.instance.widgetValueCommitAndUpdate(layoutTarget, widgets[index].widgetId, value);
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[index].widgetId, value);
}
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283

View file

@ -27,11 +27,11 @@
}
function primaryColorChanged(color: Color) {
editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
editor.handle.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
}
function secondaryColorChanged(color: Color) {
editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
editor.handle.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
}
</script>

View file

@ -54,7 +54,7 @@
...entry,
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
action: () => editor.instance.widgetValueCommitAndUpdate(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
action: () => editor.handle.widgetValueCommitAndUpdate(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
// New fields in `MenuListEntry`

View file

@ -119,7 +119,7 @@
<table>
<tr>
<td>
<TextButton label="New Document" icon="File" flush={true} action={() => editor.instance.newDocumentDialog()} />
<TextButton label="New Document" icon="File" flush={true} action={() => editor.handle.newDocumentDialog()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
@ -127,7 +127,7 @@
</tr>
<tr>
<td>
<TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.instance.openDocument()} />
<TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.handle.openDocument()} />
</td>
<td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
@ -135,7 +135,7 @@
</tr>
<tr>
<td colspan="2">
<TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.instance.demoArtworkDialog()} />
<TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.handle.demoArtworkDialog()} />
</td>
</tr>
</table>

View file

@ -30,7 +30,7 @@
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
const name = doc.displayName;
if (!editor.instance.inDevelopmentMode()) return { name };
if (!editor.handle.inDevelopmentMode()) return { name };
const tooltip = `Document ID: ${doc.id}`;
return { name, tooltip };
@ -105,8 +105,8 @@
tabCloseButtons={true}
tabMinWidths={true}
tabLabels={documentTabLabels}
clickAction={(tabIndex) => editor.instance.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.instance.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
clickAction={(tabIndex) => editor.handle.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.handle.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
tabActiveIndex={$portfolio.activeDocumentIndex}
bind:this={documentPanel}
/>

View file

@ -108,7 +108,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (await shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onKeyDown(key, modifiers, e.repeat);
editor.handle.onKeyDown(key, modifiers, e.repeat);
return;
}
@ -123,7 +123,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (await shouldRedirectKeyboardEventToBackend(e)) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onKeyUp(key, modifiers, e.repeat);
editor.handle.onKeyUp(key, modifiers, e.repeat);
}
}
@ -149,7 +149,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
}
function onMouseDown(e: MouseEvent) {
@ -170,13 +170,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
}
if (!inTextInput) {
if (textToolInteractiveInputElement) editor.instance.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
}
if (viewportPointerInteractionOngoing) {
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
editor.handle.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
}
}
@ -186,7 +186,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (textToolInteractiveInputElement) return;
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
editor.handle.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
}
function onPotentialDoubleClick(e: MouseEvent) {
@ -202,7 +202,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (e.button === 2) buttons = 2; // RMB
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
}
// Mouse events
@ -222,7 +222,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (isTargetingCanvas) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.onWheelScroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
editor.handle.onWheelScroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
}
}
@ -250,18 +250,18 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
const flattened = boundsOfViewports.flat();
const data = Float64Array.from(flattened);
if (boundsOfViewports.length > 0) editor.instance.boundsOfViewports(data);
if (boundsOfViewports.length > 0) editor.handle.boundsOfViewports(data);
}
async function onBeforeUnload(e: BeforeUnloadEvent) {
const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex];
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
if (activeDocument && !activeDocument.isAutoSaved) editor.handle.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost
if (await editor.instance.hasCrashed()) return;
if (await editor.handle.hasCrashed()) return;
// Skip the message during development, since it's annoying when testing
if (await editor.instance.inDevelopmentMode()) return;
if (await editor.handle.inDevelopmentMode()) return;
const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {
@ -279,9 +279,9 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (item.type === "text/plain") {
item.getAsString((text) => {
if (text.startsWith("graphite/layer: ")) {
editor.instance.pasteSerializedData(text.substring(16, text.length));
editor.handle.pasteSerializedData(text.substring(16, text.length));
} else if (text.startsWith("graphite/nodes: ")) {
editor.instance.pasteSerializedNodes(text.substring(16, text.length));
editor.handle.pasteSerializedNodes(text.substring(16, text.length));
}
});
}
@ -290,14 +290,14 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
if (file?.type === "svg") {
const text = await file.text();
editor.instance.pasteSvg(text);
editor.handle.pasteSvg(text);
return;
}
if (file?.type.startsWith("image")) {
const imageData = await extractPixelData(file);
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
}
});
}
@ -329,7 +329,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
const text = reader.result as string;
if (text.startsWith("graphite/layer: ")) {
editor.instance.pasteSerializedData(text.substring(16, text.length));
editor.handle.pasteSerializedData(text.substring(16, text.length));
}
};
reader.readAsText(blob);
@ -343,7 +343,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
const reader = new FileReader();
reader.onload = () => {
const text = reader.result as string;
editor.instance.pasteSvg(text);
editor.handle.pasteSvg(text);
};
reader.readAsText(blob);
@ -356,7 +356,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
reader.onload = async () => {
if (reader.result instanceof ArrayBuffer) {
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
}
};
reader.readAsArrayBuffer(blob);
@ -381,7 +381,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
};
const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);
editor.instance.errorDialog("Cannot access clipboard", message);
editor.handle.errorDialog("Cannot access clipboard", message);
}
});

View file

@ -20,6 +20,6 @@ export function createLocalizationManager(editor: Editor) {
// Subscribe to process backend event
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
editor.handle.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
});
}

View file

@ -52,7 +52,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
orderedSavedDocuments?.forEach(async (doc: TriggerIndexedDbWriteDocument) => {
editor.instance.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
editor.handle.openAutoSavedDocument(BigInt(doc.details.id), doc.details.name, doc.details.isSaved, doc.document);
});
}
@ -66,7 +66,7 @@ export function createPersistenceManager(editor: Editor, portfolio: PortfolioSta
const preferences = await get<Record<string, unknown>>("preferences", graphiteStore);
if (!preferences) return;
editor.instance.loadPreferences(JSON.stringify(preferences));
editor.handle.loadPreferences(JSON.stringify(preferences));
}
// FRONTEND MESSAGE SUBSCRIPTIONS

View file

@ -13,7 +13,7 @@ export function createDialogState(editor: Editor) {
buttons: defaultWidgetLayout(),
column1: defaultWidgetLayout(),
column2: defaultWidgetLayout(),
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor has panicked
panicDetails: "",
});
@ -27,7 +27,7 @@ export function createDialogState(editor: Editor) {
}
// Creates a crash dialog from JS once the editor has panicked.
// Normal dialogs are created in the Rust backend, but for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
// Normal dialogs are created in the Rust backend, but for the crash dialog, the editor has panicked so it cannot respond to widget callbacks.
function createCrashDialog(panicDetails: string) {
update((state) => {
state.visible = true;

View file

@ -52,9 +52,9 @@ export function createFontsState(editor: Editor) {
const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle);
if (url) {
const response = await (await fetch(url)).arrayBuffer();
editor.instance.onFontLoad(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle, url, new Uint8Array(response), triggerFontLoad.isDefault);
editor.handle.onFontLoad(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle, url, new Uint8Array(response), triggerFontLoad.isDefault);
} else {
editor.instance.errorDialog("Failed to load font", `The font ${triggerFontLoad.font.fontFamily} with style ${triggerFontLoad.font.fontStyle} does not exist`);
editor.handle.errorDialog("Failed to load font", `The font ${triggerFontLoad.font.fontFamily} with style ${triggerFontLoad.font.fontStyle} does not exist`);
}
});

View file

@ -50,31 +50,31 @@ export function createPortfolioState(editor: Editor) {
const data = await fetch(url);
const content = await data.text();
editor.instance.openDocumentFile(name, content);
editor.handle.openDocumentFile(name, content);
} catch {
// Needs to be delayed until the end of the current call stack so the existing demo artwork dialog can be closed first, otherwise this dialog won't show
setTimeout(() => {
editor.instance.errorDialog("Failed to open document", "The file could not be reached over the internet. You may be offline, or it may be missing.");
editor.handle.errorDialog("Failed to open document", "The file could not be reached over the internet. You may be offline, or it may be missing.");
}, 0);
}
});
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
const extension = editor.instance.fileSaveSuffix();
const extension = editor.handle.fileSaveSuffix();
const data = await upload(extension, "text");
editor.instance.openDocumentFile(data.filename, data.content);
editor.handle.openDocumentFile(data.filename, data.content);
});
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
const data = await upload("image/*", "data");
if (data.type.includes("svg")) {
const svg = new TextDecoder().decode(data.content);
editor.instance.pasteSvg(svg);
editor.handle.pasteSvg(svg);
return;
}
const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
editor.instance.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
editor.handle.pasteImage(new Uint8Array(imageData.data), imageData.width, imageData.height);
});
editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => {
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);

View file

@ -1,30 +1,18 @@
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
import { type JsMessageType } from "@graphite/wasm-communication/messages";
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/wasm-communication/subscription-router";
import init, { setRandomSeed, wasmMemory, JsEditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
export type WasmRawInstance = WebAssembly.Memory;
export type WasmEditorInstance = JsEditorHandle;
export type Editor = Readonly<ReturnType<typeof createEditor>>;
export type Editor = {
raw: WebAssembly.Memory;
handle: EditorHandle;
subscriptions: SubscriptionRouter;
};
// `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()`
let wasmImport: WebAssembly.Memory | undefined;
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
export async function dispatchTauri(message: unknown) {
if (!tauri) return;
try {
const response = await (await tauri).invoke("handle_message", { message });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance?.tauriResponse(response);
} catch {
// eslint-disable-next-line no-console
console.error("Failed to dispatch Tauri message");
}
}
// Should be called asynchronously before `createEditor()`
// Should be called asynchronously before `createEditor()`.
export async function initWasm() {
// Skip if the WASM module is already initialized
if (wasmImport !== undefined) return;
@ -40,27 +28,26 @@ export async function initWasm() {
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
setRandomSeed(randomSeed);
if (!tauri) return;
await (await tauri).invoke("set_random_seed", { seed: randomSeedFloat });
}
// Should be called after running `initWasm()` and its promise resolving
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createEditor() {
// Raw: Object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen)
// Should be called after running `initWasm()` and its promise resolving.
export function createEditor(): Editor {
// Raw: object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the `EditorHandle` struct (generated by wasm-bindgen)
if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup");
const raw: WasmRawInstance = wasmImport;
const raw: WebAssembly.Memory = wasmImport;
// Instance: Object containing many functions from `editor_api.rs` that are part of the editor instance (generated by wasm-bindgen)
const instance: WasmEditorInstance = new JsEditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper editor instance
// We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, instance);
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
const handle: EditorHandle = new EditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper `EditorHandle`
// We pass along the first two arguments then add our own `raw` and `handle` context for the last two arguments
subscriptions.handleJsMessage(messageType, messageData, raw, handle);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance = instance;
// Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend
// TODO: Remove?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorHandle = handle;
// Subscriptions: allows subscribing to messages in JS that are sent from the WASM backend
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
// Check if the URL hash fragment has any demo artwork to be loaded
@ -75,7 +62,7 @@ export function createEditor() {
const filename = url.pathname.split("/").pop() || "Untitled";
const content = await data.text();
instance.openDocumentFile(filename, content);
handle.openDocumentFile(filename, content);
// Remove the hash fragment from the URL
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
@ -84,14 +71,10 @@ export function createEditor() {
}
})();
return {
raw,
instance,
subscriptions,
};
return { raw, handle, subscriptions };
}
export function injectImaginatePollServerStatus() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).editorInstance?.injectImaginatePollServerStatus();
(window as any).editorHandle?.injectImaginatePollServerStatus();
}

View file

@ -4,7 +4,7 @@
import { Transform, Type, plainToClass } from "class-transformer";
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/utility-functions/icons";
import { type WasmEditorInstance, type WasmRawInstance } from "@graphite/wasm-communication/editor";
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
export class JsMessage {
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
@ -1275,7 +1275,7 @@ function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
// `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: WasmRawInstance, instance: WasmEditorInstance) => JsMessage;
type JSMessageFactory = (data: any, wasm: WebAssembly.Memory, handle: EditorHandle) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageMakers: Record<string, MessageMaker> = {

View file

@ -1,7 +1,7 @@
import { plainToInstance } from "class-transformer";
import { type WasmEditorInstance, type WasmRawInstance } from "@graphite/wasm-communication/editor";
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/wasm-communication/messages";
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
// Don't know a better way of typing this since it can be any subclass of JsMessage
@ -17,7 +17,7 @@ export function createSubscriptionRouter() {
subscriptions[messageType.name] = callback;
};
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmRawInstance, instance: WasmEditorInstance) => {
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WebAssembly.Memory, handle: EditorHandle) => {
// Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
const messageMaker = messageMakers[messageType];
if (!messageMaker) {
@ -42,7 +42,7 @@ export function createSubscriptionRouter() {
// If the `messageMaker` is a `JsMessage` class then we use the class-transformer library's `plainToInstance` function in order to convert the JSON data into the destination class.
// If it is not a `JsMessage` then it should be a custom function that creates a JsMessage from a JSON, so we call the function itself with the raw JSON as an argument.
// The resulting `message` is an instance of a class that extends `JsMessage`.
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, handle);
// If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
// The frontend should always have a callback for all messages, but due to message ordering, we might have to delay a few stack frames until we do.