mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-31 02:07:21 +00:00
Remove editor instances concept and clean up JS interop code
This commit is contained in:
parent
597c96a7db
commit
19eb6ce0ab
25 changed files with 256 additions and 325 deletions
|
@ -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() }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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> = {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() })
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue