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

@ -9,7 +9,7 @@ pub struct Editor {
}
impl Editor {
/// Construct a new editor instance.
/// Construct the editor.
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
pub fn new() -> Self {
Self { dispatcher: Dispatcher::new() }

View file

@ -273,7 +273,7 @@ mod test {
let _ = env_logger::builder().is_test(true).try_init();
}
/// Create an editor instance with three layers
/// Create an editor with three layers
/// 1. A red rectangle
/// 2. A blue shape
/// 3. A green ellipse

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.

View file

@ -5,7 +5,7 @@
// on the dispatcher messaging system and more complex Rust data types.
//
use crate::helpers::translate_key;
use crate::{Error, EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
use crate::{Error, EDITOR, EDITOR_HANDLE, EDITOR_HAS_CRASHED};
use editor::application::generate_uuid;
use editor::application::Editor;
@ -47,108 +47,36 @@ pub fn wasm_memory() -> JsValue {
wasm_bindgen::memory()
}
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
fn request_animation_frame(f: &Closure<dyn FnMut(f64)>) {
web_sys::window()
.expect("No global `window` exists")
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("Failed to call `requestAnimationFrame`");
}
/// Helper function for calling JS's `setTimeout` with the given closure and delay
fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
let delay = delay.clamp(Duration::ZERO, Duration::from_millis(i32::MAX as u64)).as_millis() as i32;
web_sys::window()
.expect("No global `window` exists")
.set_timeout_with_callback_and_timeout_and_arguments_0(f.as_ref().unchecked_ref(), delay)
.expect("Failed to call `setTimeout`");
}
// ============================================================================
/// To avoid wasm-bindgen from checking mutable reference issues using WasmRefCell we must make all methods take a non-mutable reference to self.
/// Not doing this creates an issue when Rust calls into JS which calls back to Rust in the same call stack.
/// This struct is, via wasm-bindgen, used by JS to interact with the editor backend. It does this by calling functions, which are `impl`ed
#[wasm_bindgen]
#[derive(Clone)]
pub struct JsEditorHandle {
editor_id: u64,
pub struct EditorHandle {
/// This callback is called by the editor's dispatcher when directing FrontendMessages from Rust to JS
frontend_message_handler_callback: js_sys::Function,
}
/// Provides access to the `Editor` instance and its `JsEditorHandle` by calling the given closure with them as arguments.
fn call_closure_with_editor_and_handle(mut f: impl FnMut(&mut Editor, &mut JsEditorHandle)) {
EDITOR_INSTANCES.with(|instances| {
JS_EDITOR_HANDLES.with(|handles| {
instances
.try_borrow_mut()
.map(|mut editors| {
for (id, editor) in editors.iter_mut() {
let Ok(mut handles) = handles.try_borrow_mut() else {
log::error!("Failed to borrow editor handles");
continue;
};
let Some(js_editor) = handles.get_mut(id) else {
log::error!("Editor ID ({id}) has no corresponding JsEditorHandle ID");
continue;
};
// Call the closure with the editor and its handle
f(editor, js_editor)
}
})
.unwrap_or_else(|_| log::error!("Failed to borrow editor instances"));
})
});
}
async fn poll_node_graph_evaluation() {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
// Defined separately from the `impl` block below since this `impl` block lacks the `#[wasm_bindgen]` attribute.
// Quirks in wasm-bindgen prevent functions in `#[wasm_bindgen]` `impl` blocks from being made publicly accessible from Rust.
impl EditorHandle {
pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) {
self.send_frontend_message_to_js(message);
}
editor::node_graph_executor::run_node_graph().await;
call_closure_with_editor_and_handle(|editor, handle| {
let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages);
// Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
handle.send_frontend_message_to_js(response);
}
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
})
}
fn auto_save_all_documents() {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
}
call_closure_with_editor_and_handle(|editor, handle| {
for message in editor.handle_message(PortfolioMessage::AutoSaveAllDocuments) {
handle.send_frontend_message_to_js(message);
}
});
}
// ============================================================================
#[wasm_bindgen]
impl JsEditorHandle {
impl EditorHandle {
#[wasm_bindgen(constructor)]
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
let editor_id = generate_uuid();
let editor = Editor::new();
let editor_handle = JsEditorHandle {
editor_id,
frontend_message_handler_callback,
};
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor));
JS_EDITOR_HANDLES.with(|instances| instances.borrow_mut().insert(editor_id, editor_handle.clone()));
let editor_handle = EditorHandle { frontend_message_handler_callback };
if EDITOR.with(|editor_cell| editor_cell.set(RefCell::new(editor))).is_err() {
log::error!("Attempted to initialize the editor more than once");
}
if EDITOR_HANDLE.with(|handle_cell| handle_cell.set(RefCell::new(editor_handle.clone()))).is_err() {
log::error!("Attempted to initialize the editor handle more than once");
}
editor_handle
}
@ -159,36 +87,16 @@ impl JsEditorHandle {
return;
}
#[cfg(feature = "tauri")]
{
let message: Message = message.into();
let message = ron::to_string(&message).unwrap();
// Get the editor, dispatch the message, and store the `FrontendMessage` queue response
editor(|editor| {
// Get the editor, then dispatch the message to the backend, and return its response `FrontendMessage` queue
let frontend_messages = editor.handle_message(message.into());
dispatchTauri(message);
}
#[cfg(not(feature = "tauri"))]
{
// Get the editor instances, dispatch the message, and store the `FrontendMessage` queue response
let frontend_messages = EDITOR_INSTANCES.with(|instances| {
// Mutably borrow the editors, and if successful, we can access them in the closure
instances.try_borrow_mut().map(|mut editors| {
// Get the editor instance for this editor ID, then dispatch the message to the backend, and return its response `FrontendMessage` queue
editors
.get_mut(&self.editor_id)
.expect("EDITOR_INSTANCES does not contain the current editor_id")
.handle_message(message.into())
})
});
// Process any `FrontendMessage` responses resulting from the backend processing the dispatched message
if let Ok(frontend_messages) = frontend_messages {
// Send each `FrontendMessage` to the JavaScript frontend
for message in frontend_messages.into_iter() {
self.send_frontend_message_to_js(message);
}
// Send each `FrontendMessage` to the JavaScript frontend
for message in frontend_messages.into_iter() {
self.send_frontend_message_to_js(message);
}
}
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
});
}
// Sends a FrontendMessage to JavaScript
@ -238,7 +146,7 @@ impl JsEditorHandle {
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
call_closure_with_editor_and_handle(|editor, handle| {
editor_and_handle(|editor, handle| {
let micros: f64 = timestamp * 1000.;
let timestamp = Duration::from_micros(micros.round() as u64);
@ -778,51 +686,13 @@ impl JsEditorHandle {
self.dispatch(message);
}
/// Returns the string representation of the nodes contents
#[wasm_bindgen(js_name = introspectNode)]
pub fn introspect_node(&self, node_path: Vec<u64>) -> JsValue {
let node_path = node_path.into_iter().map(NodeId).collect::<Vec<_>>();
let frontend_messages = EDITOR_INSTANCES.with(|instances| {
// Mutably borrow the editors, and if successful, we can access them in the closure
instances.try_borrow_mut().map(|mut editors| {
// Get the editor instance for this editor ID, then dispatch the message to the backend, and return its response `FrontendMessage` queue
let image = editors
.get_mut(&self.editor_id)
.expect("EDITOR_INSTANCES does not contain the current editor_id")
.dispatcher
.message_handlers
.portfolio_message_handler
.introspect_node(&node_path);
let image = image?;
let image = image.downcast_ref::<graphene_core::raster::ImageFrame<Color>>()?;
let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true);
let message_data = image.serialize(&serializer).expect("Failed to serialize FrontendMessage");
Some(message_data)
})
});
frontend_messages.unwrap().unwrap_or_default()
}
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
pub fn inject_imaginate_poll_server_status(&self) {
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
}
}
// Needed to make JsEditorHandle functions pub to Rust.
// The reason is not fully clear but it has to do with the #[wasm_bindgen] procedural macro.
impl JsEditorHandle {
pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) {
self.send_frontend_message_to_js(message);
}
}
impl Drop for JsEditorHandle {
fn drop(&mut self) {
// Consider removing after https://github.com/rustwasm/wasm-bindgen/pull/2984 is merged and released
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().remove(&self.editor_id));
}
}
// ============================================================================
#[wasm_bindgen(js_name = evaluateMathExpression)]
pub fn evaluate_math_expression(expression: &str) -> Option<f64> {
@ -894,6 +764,85 @@ pub fn implicit_multiplication_preprocess(expression: &str) -> String {
output_string.replace("logtwo(", "log2(").replace('π', "pi").replace('τ', "tau")
}
/// Helper function for calling JS's `requestAnimationFrame` with the given closure
fn request_animation_frame(f: &Closure<dyn FnMut(f64)>) {
web_sys::window()
.expect("No global `window` exists")
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("Failed to call `requestAnimationFrame`");
}
/// Helper function for calling JS's `setTimeout` with the given closure and delay
fn set_timeout(f: &Closure<dyn FnMut()>, delay: Duration) {
let delay = delay.clamp(Duration::ZERO, Duration::from_millis(i32::MAX as u64)).as_millis() as i32;
web_sys::window()
.expect("No global `window` exists")
.set_timeout_with_callback_and_timeout_and_arguments_0(f.as_ref().unchecked_ref(), delay)
.expect("Failed to call `setTimeout`");
}
/// Provides access to the `Editor` by calling the given closure with it as an argument.
fn editor<T: Default>(callback: impl FnOnce(&mut editor::application::Editor) -> T) -> T {
EDITOR.with(|editor| {
let Some(Ok(mut editor)) = editor.get().map(RefCell::try_borrow_mut) else {
// TODO: Investigate if this should just panic instead, and if not doing so right now may be the cause of silent crashes that don't inform the user that the app has panicked
log::error!("Failed to borrow the editor");
return T::default();
};
callback(&mut *editor)
})
}
/// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments.
fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) {
editor(|editor| {
EDITOR_HANDLE.with(|editor_handle| {
let Some(Ok(mut handle)) = editor_handle.get().map(RefCell::try_borrow_mut) else {
log::error!("Failed to borrow editor handle");
return;
};
// Call the closure with the editor and its handle
callback(editor, &mut handle);
})
});
}
async fn poll_node_graph_evaluation() {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
}
editor::node_graph_executor::run_node_graph().await;
editor_and_handle(|editor, handle| {
let mut messages = VecDeque::new();
editor.poll_node_graph_evaluation(&mut messages);
// Send each `FrontendMessage` to the JavaScript frontend
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
handle.send_frontend_message_to_js(response);
}
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
})
}
fn auto_save_all_documents() {
// Process no further messages after a crash to avoid spamming the console
if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) {
return;
}
editor_and_handle(|editor, handle| {
for message in editor.handle_message(PortfolioMessage::AutoSaveAllDocuments) {
handle.send_frontend_message_to_js(message);
}
});
}
#[test]
fn implicit_multiplication_preprocess_tests() {
assert_eq!(implicit_multiplication_preprocess("2pi"), "2*pi");

View file

@ -9,8 +9,7 @@ pub mod helpers;
use editor::messages::prelude::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::cell::{OnceCell, RefCell};
use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
use wasm_bindgen::prelude::*;
@ -19,9 +18,8 @@ use wasm_bindgen::prelude::*;
pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
pub static LOGGER: WasmLog = WasmLog;
thread_local! {
// TODO: Remove the concept of multiple editor instances to simplify all of this
pub static EDITOR_INSTANCES: RefCell<HashMap<u64, editor::application::Editor>> = RefCell::new(HashMap::new());
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, editor_api::JsEditorHandle>> = RefCell::new(HashMap::new());
pub static EDITOR: OnceCell<RefCell<editor::application::Editor>> = OnceCell::new();
pub static EDITOR_HANDLE: OnceCell<RefCell<editor_api::EditorHandle>> = OnceCell::new();
}
/// Initialize the backend
@ -41,11 +39,12 @@ pub fn panic_hook(info: &panic::PanicInfo) {
error!("{info}");
JS_EDITOR_HANDLES.with(|instances| {
instances
.borrow_mut()
.values_mut()
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
EDITOR_HANDLE.with(|editor_handle| {
editor_handle.get().map(|handle| {
handle
.borrow_mut()
.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() })
})
});
}

View file

@ -126,17 +126,17 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<span>Imaginate (Stable Diffusion node/tool)</span>
</div>
<div class="informational ongoing" title="Development Ongoing">
<img class="atlas" style="--atlas-index: 9" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Graph data attribute spreadsheet</span>
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Native desktop app (with <a target="_blank" href="https://tauri.app/">Tauri</a>)</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 8" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Custom subgraph nodes</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 51" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Boolean operations for shapes</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Native desktop app (with <a target="_blank" href="https://tauri.app/">Tauri</a>)</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>WebGPU accelerated rendering</span>
@ -145,10 +145,6 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 14" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Adaptive resolution raster rendering</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Timeline with animation channels</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 26" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Interactive graph auto-layout</span>
@ -170,16 +166,20 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<span>Fully-supported brush tool</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Select mode (marquee masking)</span>
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Timeline with animation channels</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 9" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Graph data attribute spreadsheet</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 54" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local file browser for saving/loading</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 8" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Custom subgraph nodes</span>
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local fonts access</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 17" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
@ -190,8 +190,8 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<h3>— Alpha 4 —</h3>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Local fonts access</span>
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Select mode (marquee masking)</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
@ -229,10 +229,6 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 16" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Code editor for custom nodes</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 45" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Portable, embeddable render engine</span>
</div>
<!-- Beta -->
<div class="informational heading">
<h3>— Beta —</h3>
@ -273,6 +269,10 @@ Always on the bleeding edge and built to last— Graphite is written on a robust
<img class="atlas" style="--atlas-index: 34" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Node manager and marketplace</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 45" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Portable, embeddable render engine</span>
</div>
<div class="informational">
<img class="atlas" style="--atlas-index: 35" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
<span>Predictive graph rendering/caching</span>