Remove editor instances concept and clean up JS interop code
This commit is contained in:
parent
597c96a7db
commit
19eb6ce0ab
|
|
@ -9,7 +9,7 @@ pub struct Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Remember to provide a random seed with `editor::set_uuid_seed(seed)` before any editors can be used.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { dispatcher: Dispatcher::new() }
|
Self { dispatcher: Dispatcher::new() }
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ mod test {
|
||||||
let _ = env_logger::builder().is_test(true).try_init();
|
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
|
/// 1. A red rectangle
|
||||||
/// 2. A blue shape
|
/// 2. A blue shape
|
||||||
/// 3. A green ellipse
|
/// 3. A green ellipse
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
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";
|
import Editor from "@graphite/components/Editor.svelte";
|
||||||
|
|
||||||
let editor: ReturnType<typeof createEditor> | undefined = undefined;
|
let editor: GraphiteEditor | undefined = undefined;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await initWasm();
|
await initWasm();
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
// Destroy the WASM editor instance
|
// Destroy the WASM editor handle
|
||||||
editor?.instance.free();
|
editor?.handle.free();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@
|
||||||
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
||||||
import { createPortfolioState } from "@graphite/state-providers/portfolio";
|
import { createPortfolioState } from "@graphite/state-providers/portfolio";
|
||||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
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";
|
import MainWindow from "@graphite/components/window/MainWindow.svelte";
|
||||||
|
|
||||||
// Graphite WASM editor instance
|
// Graphite WASM editor
|
||||||
export let editor: ReturnType<typeof createEditor>;
|
export let editor: Editor;
|
||||||
setContext("editor", editor);
|
setContext("editor", editor);
|
||||||
|
|
||||||
// State provider systems
|
// State provider systems
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
// 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(() => {
|
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
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (!(window as any).EyeDropper) {
|
if (!(window as any).EyeDropper) {
|
||||||
editor.instance.eyedropperSampleForColorPicker();
|
editor.handle.eyedropperSampleForColorPicker();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,14 +127,14 @@
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file?.type.includes("svg")) {
|
if (file?.type.includes("svg")) {
|
||||||
const svgData = await file.text();
|
const svgData = await file.text();
|
||||||
editor.instance.pasteSvg(svgData, e.clientX, e.clientY);
|
editor.handle.pasteSvg(svgData, e.clientX, e.clientY);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
const imageData = await extractPixelData(file);
|
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) {
|
function panCanvasX(newValue: number) {
|
||||||
const delta = newValue - scrollbarPos.x;
|
const delta = newValue - scrollbarPos.x;
|
||||||
scrollbarPos.x = newValue;
|
scrollbarPos.x = newValue;
|
||||||
editor.instance.panCanvas(-delta * scrollbarMultiplier.x, 0);
|
editor.handle.panCanvas(-delta * scrollbarMultiplier.x, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function panCanvasY(newValue: number) {
|
function panCanvasY(newValue: number) {
|
||||||
const delta = newValue - scrollbarPos.y;
|
const delta = newValue - scrollbarPos.y;
|
||||||
scrollbarPos.y = newValue;
|
scrollbarPos.y = newValue;
|
||||||
editor.instance.panCanvas(0, -delta * scrollbarMultiplier.y);
|
editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageX(delta: number) {
|
function pageX(delta: number) {
|
||||||
const move = delta < 0 ? 1 : -1;
|
const move = delta < 0 ? 1 : -1;
|
||||||
editor.instance.panCanvasByFraction(move, 0);
|
editor.handle.panCanvasByFraction(move, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageY(delta: number) {
|
function pageY(delta: number) {
|
||||||
const move = delta < 0 ? 1 : -1;
|
const move = delta < 0 ? 1 : -1;
|
||||||
editor.instance.panCanvasByFraction(0, move);
|
editor.handle.panCanvasByFraction(0, move);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canvasPointerDown(e: PointerEvent) {
|
function canvasPointerDown(e: PointerEvent) {
|
||||||
|
|
@ -290,7 +290,7 @@
|
||||||
export function triggerTextCommit() {
|
export function triggerTextCommit() {
|
||||||
if (!textInput) return;
|
if (!textInput) return;
|
||||||
const textCleaned = textInputCleanup(textInput.innerText);
|
const textCleaned = textInputCleanup(textInput.innerText);
|
||||||
editor.instance.onChangeText(textCleaned);
|
editor.handle.onChangeText(textCleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) {
|
export async function displayEditableTextbox(displayEditableTextbox: DisplayEditableTextbox) {
|
||||||
|
|
@ -314,7 +314,7 @@
|
||||||
|
|
||||||
textInput.oninput = () => {
|
textInput.oninput = () => {
|
||||||
if (!textInput) return;
|
if (!textInput) return;
|
||||||
editor.instance.updateBounds(textInputCleanup(textInput.innerText));
|
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
|
||||||
};
|
};
|
||||||
textInputMatrix = displayEditableTextbox.transform;
|
textInputMatrix = displayEditableTextbox.transform;
|
||||||
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
|
const newFont = new FontFace("text-font", `url(${displayEditableTextbox.url})`);
|
||||||
|
|
@ -371,8 +371,8 @@
|
||||||
const rgb = await updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor);
|
const rgb = await updateEyedropperSamplingState(mousePosition, primaryColor, secondaryColor);
|
||||||
|
|
||||||
if (setColorChoice && rgb) {
|
if (setColorChoice && rgb) {
|
||||||
if (setColorChoice === "Primary") editor.instance.updatePrimaryColor(...rgb, 1);
|
if (setColorChoice === "Primary") editor.handle.updatePrimaryColor(...rgb, 1);
|
||||||
if (setColorChoice === "Secondary") editor.instance.updateSecondaryColor(...rgb, 1);
|
if (setColorChoice === "Secondary") editor.handle.updateSecondaryColor(...rgb, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,15 +130,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLayerVisibility(id: bigint) {
|
function toggleLayerVisibility(id: bigint) {
|
||||||
editor.instance.toggleLayerVisibility(id);
|
editor.handle.toggleLayerVisibility(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLayerLock(id: bigint) {
|
function toggleLayerLock(id: bigint) {
|
||||||
editor.instance.toggleLayerLock(id);
|
editor.handle.toggleLayerLock(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExpandArrowClick(id: bigint) {
|
function handleExpandArrowClick(id: bigint) {
|
||||||
editor.instance.toggleLayerExpansion(id);
|
editor.handle.toggleLayerExpansion(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onEditLayerName(listing: LayerListingInfo) {
|
async function onEditLayerName(listing: LayerListingInfo) {
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
layers = layers;
|
layers = layers;
|
||||||
|
|
||||||
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
|
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;
|
listing.entry.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,11 +196,11 @@
|
||||||
// Don't select while we are entering text to rename the layer
|
// Don't select while we are entering text to rename the layer
|
||||||
if (listing.editingName) return;
|
if (listing.editingName) return;
|
||||||
|
|
||||||
editor.instance.selectLayer(listing.entry.id, accel, shift);
|
editor.handle.selectLayer(listing.entry.id, accel, shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deselectAllLayers() {
|
async function deselectAllLayers() {
|
||||||
editor.instance.deselectAllLayers();
|
editor.handle.deselectAllLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNestingLayer(layerClassification: LayerClassification) {
|
function isNestingLayer(layerClassification: LayerClassification) {
|
||||||
|
|
@ -322,7 +322,7 @@
|
||||||
const { select, insertParentId, insertIndex } = draggingData;
|
const { select, insertParentId, insertIndex } = draggingData;
|
||||||
|
|
||||||
select?.();
|
select?.();
|
||||||
editor.instance.moveLayerInTree(insertParentId, insertIndex);
|
editor.handle.moveLayerInTree(insertParentId, insertIndex);
|
||||||
}
|
}
|
||||||
draggingData = undefined;
|
draggingData = undefined;
|
||||||
fakeHighlight = undefined;
|
fakeHighlight = undefined;
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@
|
||||||
|
|
||||||
// Alt-click sets the clicked node as previewed
|
// Alt-click sets the clicked node as previewed
|
||||||
if (lmb && e.altKey && nodeId !== undefined) {
|
if (lmb && e.altKey && nodeId !== undefined) {
|
||||||
editor.instance.togglePreview(nodeId);
|
editor.handle.togglePreview(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicked on a port dot
|
// Clicked on a port dot
|
||||||
|
|
@ -440,7 +440,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the selection in the backend if it was modified
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +449,7 @@
|
||||||
if (lmb) {
|
if (lmb) {
|
||||||
previousSelection = $nodeGraph.selected;
|
previousSelection = $nodeGraph.selected;
|
||||||
// Clear current selection
|
// 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();
|
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) };
|
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;
|
// const nodeId = node?.getAttribute("data-node") || undefined;
|
||||||
// if (nodeId !== undefined) {
|
// if (nodeId !== undefined) {
|
||||||
// const id = BigInt(nodeId);
|
// const id = BigInt(nodeId);
|
||||||
// editor.instance.enterNestedNetwork(id);
|
// editor.handle.enterNestedNetwork(id);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,7 +510,7 @@
|
||||||
completeBoxSelection();
|
completeBoxSelection();
|
||||||
boxSelection = undefined;
|
boxSelection = undefined;
|
||||||
} else if ((e.buttons & 2) !== 0) {
|
} else if ((e.buttons & 2) !== 0) {
|
||||||
editor.instance.selectNodes(new BigUint64Array(previousSelection));
|
editor.handle.selectNodes(new BigUint64Array(previousSelection));
|
||||||
boxSelection = undefined;
|
boxSelection = undefined;
|
||||||
} else {
|
} else {
|
||||||
const graphBounds = graph?.getBoundingClientRect();
|
const graphBounds = graph?.getBoundingClientRect();
|
||||||
|
|
@ -534,7 +534,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeBoxSelection() {
|
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 {
|
function showSelected(selected: bigint[], boxSelect: Box | undefined, node: bigint, nodeIndex: number): boolean {
|
||||||
|
|
@ -542,7 +542,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLayerVisibility(id: bigint) {
|
function toggleLayerVisibility(id: bigint) {
|
||||||
editor.instance.toggleLayerVisibility(id);
|
editor.handle.toggleLayerVisibility(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
|
function connectorToNodeIndex(svg: SVGSVGElement): { nodeId: bigint; index: number } | undefined {
|
||||||
|
|
@ -589,7 +589,7 @@
|
||||||
const selectedNodeBounds = selectedNode.getBoundingClientRect();
|
const selectedNodeBounds = selectedNode.getBoundingClientRect();
|
||||||
const containerBoundsBounds = theNodesContainer.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.x)),
|
||||||
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
new Float64Array(wireCurveLocations.map((loc) => loc.y)),
|
||||||
selectedNodeBounds.top - containerBoundsBounds.y,
|
selectedNodeBounds.top - containerBoundsBounds.y,
|
||||||
|
|
@ -603,9 +603,9 @@
|
||||||
if (link) {
|
if (link) {
|
||||||
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
|
const isLayer = $nodeGraph.nodes.find((n) => n.id === selectedNodeId)?.isLayer;
|
||||||
|
|
||||||
editor.instance.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
|
editor.handle.connectNodesByLink(link.linkStart, 0, selectedNodeId, isLayer ? 1 : 0);
|
||||||
editor.instance.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
|
editor.handle.connectNodesByLink(selectedNodeId, 0, link.linkEnd, Number(link.linkEndInputIndex));
|
||||||
if (!isLayer) editor.instance.shiftNode(selectedNodeId);
|
if (!isLayer) editor.handle.shiftNode(selectedNodeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -614,7 +614,7 @@
|
||||||
|
|
||||||
const initialDisconnecting = disconnecting;
|
const initialDisconnecting = disconnecting;
|
||||||
if (disconnecting) {
|
if (disconnecting) {
|
||||||
editor.instance.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
|
editor.handle.disconnectNodes(BigInt(disconnecting.nodeId), disconnecting.inputIndex);
|
||||||
}
|
}
|
||||||
disconnecting = undefined;
|
disconnecting = undefined;
|
||||||
|
|
||||||
|
|
@ -625,7 +625,7 @@
|
||||||
if (from !== undefined && to !== undefined) {
|
if (from !== undefined && to !== undefined) {
|
||||||
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
|
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
|
||||||
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
|
const { nodeId: inputConnectedNodeID, index: inputNodeConnectionIndex } = to;
|
||||||
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
|
editor.handle.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
|
||||||
}
|
}
|
||||||
} else if (linkInProgressFromConnector && !initialDisconnecting) {
|
} else if (linkInProgressFromConnector && !initialDisconnecting) {
|
||||||
// If the add node menu is already open, we don't want to open it again
|
// If the add node menu is already open, we don't want to open it again
|
||||||
|
|
@ -645,11 +645,11 @@
|
||||||
} else if (draggingNodes) {
|
} else if (draggingNodes) {
|
||||||
if (draggingNodes.startX === e.x && draggingNodes.startY === e.y) {
|
if (draggingNodes.startX === e.x && draggingNodes.startY === e.y) {
|
||||||
if (selectIfNotDragged !== undefined && ($nodeGraph.selected.length !== 1 || $nodeGraph.selected[0] !== selectIfNotDragged)) {
|
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();
|
checkInsertBetween();
|
||||||
|
|
||||||
|
|
@ -670,7 +670,7 @@
|
||||||
const inputNodeConnectionIndex = 0;
|
const inputNodeConnectionIndex = 0;
|
||||||
const x = Math.round(nodeListLocation.x / GRID_SIZE);
|
const x = Math.round(nodeListLocation.x / GRID_SIZE);
|
||||||
const y = Math.round(nodeListLocation.y / GRID_SIZE) - 1;
|
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;
|
nodeListLocation = undefined;
|
||||||
|
|
||||||
if (!linkInProgressFromConnector) return;
|
if (!linkInProgressFromConnector) return;
|
||||||
|
|
@ -678,7 +678,7 @@
|
||||||
|
|
||||||
if (from !== undefined) {
|
if (from !== undefined) {
|
||||||
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
|
const { nodeId: outputConnectedNodeID, index: outputNodeConnectionIndex } = from;
|
||||||
editor.instance.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
|
editor.handle.connectNodesByLink(outputConnectedNodeID, outputNodeConnectionIndex, inputConnectedNodeID, inputNodeConnectionIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
linkInProgressFromConnector = undefined;
|
linkInProgressFromConnector = undefined;
|
||||||
|
|
@ -882,7 +882,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
<!-- 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"}
|
{node.alias || "Layer"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -929,7 +929,7 @@
|
||||||
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
|
<div class="primary" class:no-parameter-section={exposedInputsOutputs.length === 0}>
|
||||||
<IconLabel icon={nodeIcon(node.name)} />
|
<IconLabel icon={nodeIcon(node.name)} />
|
||||||
<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
|
<!-- 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>
|
</div>
|
||||||
<!-- Parameter rows -->
|
<!-- Parameter rows -->
|
||||||
{#if exposedInputsOutputs.length > 0}
|
{#if exposedInputsOutputs.length > 0}
|
||||||
|
|
|
||||||
|
|
@ -58,15 +58,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetValueCommit(index: number, value: unknown) {
|
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) {
|
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) {
|
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
|
// 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) {
|
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) {
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
...entry,
|
...entry,
|
||||||
|
|
||||||
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
|
// 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,
|
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
|
||||||
|
|
||||||
// New fields in `MenuListEntry`
|
// New fields in `MenuListEntry`
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
||||||
|
|
@ -127,7 +127,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<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>
|
||||||
<td>
|
<td>
|
||||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
|
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
|
||||||
const name = doc.displayName;
|
const name = doc.displayName;
|
||||||
|
|
||||||
if (!editor.instance.inDevelopmentMode()) return { name };
|
if (!editor.handle.inDevelopmentMode()) return { name };
|
||||||
|
|
||||||
const tooltip = `Document ID: ${doc.id}`;
|
const tooltip = `Document ID: ${doc.id}`;
|
||||||
return { name, tooltip };
|
return { name, tooltip };
|
||||||
|
|
@ -105,8 +105,8 @@
|
||||||
tabCloseButtons={true}
|
tabCloseButtons={true}
|
||||||
tabMinWidths={true}
|
tabMinWidths={true}
|
||||||
tabLabels={documentTabLabels}
|
tabLabels={documentTabLabels}
|
||||||
clickAction={(tabIndex) => editor.instance.selectDocument($portfolio.documents[tabIndex].id)}
|
clickAction={(tabIndex) => editor.handle.selectDocument($portfolio.documents[tabIndex].id)}
|
||||||
closeAction={(tabIndex) => editor.instance.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
|
closeAction={(tabIndex) => editor.handle.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
|
||||||
tabActiveIndex={$portfolio.activeDocumentIndex}
|
tabActiveIndex={$portfolio.activeDocumentIndex}
|
||||||
bind:this={documentPanel}
|
bind:this={documentPanel}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (await shouldRedirectKeyboardEventToBackend(e)) {
|
if (await shouldRedirectKeyboardEventToBackend(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||||
editor.instance.onKeyDown(key, modifiers, e.repeat);
|
editor.handle.onKeyDown(key, modifiers, e.repeat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (await shouldRedirectKeyboardEventToBackend(e)) {
|
if (await shouldRedirectKeyboardEventToBackend(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
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);
|
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) {
|
function onMouseDown(e: MouseEvent) {
|
||||||
|
|
@ -170,13 +170,13 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inTextInput) {
|
if (!inTextInput) {
|
||||||
if (textToolInteractiveInputElement) editor.instance.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
|
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
|
||||||
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewportPointerInteractionOngoing) {
|
if (viewportPointerInteractionOngoing) {
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
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;
|
if (textToolInteractiveInputElement) return;
|
||||||
|
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
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) {
|
function onPotentialDoubleClick(e: MouseEvent) {
|
||||||
|
|
@ -202,7 +202,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (e.button === 2) buttons = 2; // RMB
|
if (e.button === 2) buttons = 2; // RMB
|
||||||
|
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||||
editor.instance.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
|
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse events
|
// Mouse events
|
||||||
|
|
@ -222,7 +222,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (isTargetingCanvas) {
|
if (isTargetingCanvas) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
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 flattened = boundsOfViewports.flat();
|
||||||
const data = Float64Array.from(flattened);
|
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) {
|
async function onBeforeUnload(e: BeforeUnloadEvent) {
|
||||||
const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex];
|
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
|
// 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
|
// 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);
|
const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true);
|
||||||
if (!allDocumentsSaved) {
|
if (!allDocumentsSaved) {
|
||||||
|
|
@ -279,9 +279,9 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
if (item.type === "text/plain") {
|
if (item.type === "text/plain") {
|
||||||
item.getAsString((text) => {
|
item.getAsString((text) => {
|
||||||
if (text.startsWith("graphite/layer: ")) {
|
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: ")) {
|
} 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") {
|
if (file?.type === "svg") {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
editor.instance.pasteSvg(text);
|
editor.handle.pasteSvg(text);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file?.type.startsWith("image")) {
|
if (file?.type.startsWith("image")) {
|
||||||
const imageData = await extractPixelData(file);
|
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;
|
const text = reader.result as string;
|
||||||
|
|
||||||
if (text.startsWith("graphite/layer: ")) {
|
if (text.startsWith("graphite/layer: ")) {
|
||||||
editor.instance.pasteSerializedData(text.substring(16, text.length));
|
editor.handle.pasteSerializedData(text.substring(16, text.length));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(blob);
|
reader.readAsText(blob);
|
||||||
|
|
@ -343,7 +343,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const text = reader.result as string;
|
const text = reader.result as string;
|
||||||
editor.instance.pasteSvg(text);
|
editor.handle.pasteSvg(text);
|
||||||
};
|
};
|
||||||
reader.readAsText(blob);
|
reader.readAsText(blob);
|
||||||
|
|
||||||
|
|
@ -356,7 +356,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
if (reader.result instanceof ArrayBuffer) {
|
if (reader.result instanceof ArrayBuffer) {
|
||||||
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
|
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);
|
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);
|
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
|
// Subscribe to process backend event
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
|
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
|
||||||
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
|
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]] : []));
|
const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : []));
|
||||||
|
|
||||||
orderedSavedDocuments?.forEach(async (doc: TriggerIndexedDbWriteDocument) => {
|
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);
|
const preferences = await get<Record<string, unknown>>("preferences", graphiteStore);
|
||||||
if (!preferences) return;
|
if (!preferences) return;
|
||||||
|
|
||||||
editor.instance.loadPreferences(JSON.stringify(preferences));
|
editor.handle.loadPreferences(JSON.stringify(preferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
// FRONTEND MESSAGE SUBSCRIPTIONS
|
// FRONTEND MESSAGE SUBSCRIPTIONS
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export function createDialogState(editor: Editor) {
|
||||||
buttons: defaultWidgetLayout(),
|
buttons: defaultWidgetLayout(),
|
||||||
column1: defaultWidgetLayout(),
|
column1: defaultWidgetLayout(),
|
||||||
column2: 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: "",
|
panicDetails: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function createDialogState(editor: Editor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a crash dialog from JS once the editor has panicked.
|
// 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) {
|
function createCrashDialog(panicDetails: string) {
|
||||||
update((state) => {
|
update((state) => {
|
||||||
state.visible = true;
|
state.visible = true;
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,9 @@ export function createFontsState(editor: Editor) {
|
||||||
const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle);
|
const url = await getFontFileUrl(triggerFontLoad.font.fontFamily, triggerFontLoad.font.fontStyle);
|
||||||
if (url) {
|
if (url) {
|
||||||
const response = await (await fetch(url)).arrayBuffer();
|
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 {
|
} 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 data = await fetch(url);
|
||||||
const content = await data.text();
|
const content = await data.text();
|
||||||
|
|
||||||
editor.instance.openDocumentFile(name, content);
|
editor.handle.openDocumentFile(name, content);
|
||||||
} catch {
|
} 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
|
// 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(() => {
|
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);
|
}, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
editor.subscriptions.subscribeJsMessage(TriggerOpenDocument, async () => {
|
||||||
const extension = editor.instance.fileSaveSuffix();
|
const extension = editor.handle.fileSaveSuffix();
|
||||||
const data = await upload(extension, "text");
|
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 () => {
|
editor.subscriptions.subscribeJsMessage(TriggerImport, async () => {
|
||||||
const data = await upload("image/*", "data");
|
const data = await upload("image/*", "data");
|
||||||
|
|
||||||
if (data.type.includes("svg")) {
|
if (data.type.includes("svg")) {
|
||||||
const svg = new TextDecoder().decode(data.content);
|
const svg = new TextDecoder().decode(data.content);
|
||||||
editor.instance.pasteSvg(svg);
|
editor.handle.pasteSvg(svg);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageData = await extractPixelData(new Blob([data.content], { type: data.type }));
|
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) => {
|
editor.subscriptions.subscribeJsMessage(TriggerDownloadTextFile, (triggerFileDownload) => {
|
||||||
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
downloadFileText(triggerFileDownload.name, triggerFileDownload.document);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,18 @@
|
||||||
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
|
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
|
||||||
import { type JsMessageType } from "@graphite/wasm-communication/messages";
|
import { type JsMessageType } from "@graphite/wasm-communication/messages";
|
||||||
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/wasm-communication/subscription-router";
|
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 Editor = {
|
||||||
export type WasmEditorInstance = JsEditorHandle;
|
raw: WebAssembly.Memory;
|
||||||
export type Editor = Readonly<ReturnType<typeof createEditor>>;
|
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()`
|
// `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;
|
let wasmImport: WebAssembly.Memory | undefined;
|
||||||
|
|
||||||
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");
|
// Should be called asynchronously before `createEditor()`.
|
||||||
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()`
|
|
||||||
export async function initWasm() {
|
export async function initWasm() {
|
||||||
// Skip if the WASM module is already initialized
|
// Skip if the WASM module is already initialized
|
||||||
if (wasmImport !== undefined) return;
|
if (wasmImport !== undefined) return;
|
||||||
|
|
@ -40,27 +28,26 @@ export async function initWasm() {
|
||||||
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||||
const randomSeed = BigInt(randomSeedFloat);
|
const randomSeed = BigInt(randomSeedFloat);
|
||||||
setRandomSeed(randomSeed);
|
setRandomSeed(randomSeed);
|
||||||
if (!tauri) return;
|
|
||||||
await (await tauri).invoke("set_random_seed", { seed: randomSeedFloat });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be called after running `initWasm()` and its promise resolving
|
// Should be called after running `initWasm()` and its promise resolving.
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
export function createEditor(): Editor {
|
||||||
export function createEditor() {
|
// Raw: object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the `EditorHandle` struct (generated by wasm-bindgen)
|
||||||
// Raw: Object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen)
|
|
||||||
if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup");
|
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)
|
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
|
||||||
const instance: WasmEditorInstance = new JsEditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
|
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 editor instance
|
// 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 `instance` context for the last two arguments
|
// 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, instance);
|
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();
|
const subscriptions: SubscriptionRouter = createSubscriptionRouter();
|
||||||
|
|
||||||
// Check if the URL hash fragment has any demo artwork to be loaded
|
// 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 filename = url.pathname.split("/").pop() || "Untitled";
|
||||||
const content = await data.text();
|
const content = await data.text();
|
||||||
instance.openDocumentFile(filename, content);
|
handle.openDocumentFile(filename, content);
|
||||||
|
|
||||||
// Remove the hash fragment from the URL
|
// Remove the hash fragment from the URL
|
||||||
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
||||||
|
|
@ -84,14 +71,10 @@ export function createEditor() {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return {
|
return { raw, handle, subscriptions };
|
||||||
raw,
|
|
||||||
instance,
|
|
||||||
subscriptions,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function injectImaginatePollServerStatus() {
|
export function injectImaginatePollServerStatus() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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 { Transform, Type, plainToClass } from "class-transformer";
|
||||||
|
|
||||||
import { type PopoverButtonStyle, type IconName, type IconSize } from "@graphite/utility-functions/icons";
|
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 {
|
export class JsMessage {
|
||||||
// The marker provides a way to check if an object is a sub-class constructor for a 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
|
// `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
|
// 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;
|
type MessageMaker = typeof JsMessage | JSMessageFactory;
|
||||||
|
|
||||||
export const messageMakers: Record<string, MessageMaker> = {
|
export const messageMakers: Record<string, MessageMaker> = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { plainToInstance } from "class-transformer";
|
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 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;
|
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
|
// 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;
|
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
|
// 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];
|
const messageMaker = messageMakers[messageType];
|
||||||
if (!messageMaker) {
|
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 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.
|
// 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`.
|
// 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.
|
// 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.
|
// 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.
|
// on the dispatcher messaging system and more complex Rust data types.
|
||||||
//
|
//
|
||||||
use crate::helpers::translate_key;
|
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::generate_uuid;
|
||||||
use editor::application::Editor;
|
use editor::application::Editor;
|
||||||
|
|
@ -47,108 +47,36 @@ pub fn wasm_memory() -> JsValue {
|
||||||
wasm_bindgen::memory()
|
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.
|
/// 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
|
||||||
/// Not doing this creates an issue when Rust calls into JS which calls back to Rust in the same call stack.
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct JsEditorHandle {
|
pub struct EditorHandle {
|
||||||
editor_id: u64,
|
/// This callback is called by the editor's dispatcher when directing FrontendMessages from Rust to JS
|
||||||
frontend_message_handler_callback: js_sys::Function,
|
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.
|
// Defined separately from the `impl` block below since this `impl` block lacks the `#[wasm_bindgen]` attribute.
|
||||||
fn call_closure_with_editor_and_handle(mut f: impl FnMut(&mut Editor, &mut JsEditorHandle)) {
|
// Quirks in wasm-bindgen prevent functions in `#[wasm_bindgen]` `impl` blocks from being made publicly accessible from Rust.
|
||||||
EDITOR_INSTANCES.with(|instances| {
|
impl EditorHandle {
|
||||||
JS_EDITOR_HANDLES.with(|handles| {
|
pub fn send_frontend_message_to_js_rust_proxy(&self, message: FrontendMessage) {
|
||||||
instances
|
self.send_frontend_message_to_js(message);
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]
|
#[wasm_bindgen]
|
||||||
impl JsEditorHandle {
|
impl EditorHandle {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
|
pub fn new(frontend_message_handler_callback: js_sys::Function) -> Self {
|
||||||
let editor_id = generate_uuid();
|
|
||||||
let editor = Editor::new();
|
let editor = Editor::new();
|
||||||
let editor_handle = JsEditorHandle {
|
let editor_handle = EditorHandle { frontend_message_handler_callback };
|
||||||
editor_id,
|
if EDITOR.with(|editor_cell| editor_cell.set(RefCell::new(editor))).is_err() {
|
||||||
frontend_message_handler_callback,
|
log::error!("Attempted to initialize the editor more than once");
|
||||||
};
|
}
|
||||||
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().insert(editor_id, editor));
|
if EDITOR_HANDLE.with(|handle_cell| handle_cell.set(RefCell::new(editor_handle.clone()))).is_err() {
|
||||||
JS_EDITOR_HANDLES.with(|instances| instances.borrow_mut().insert(editor_id, editor_handle.clone()));
|
log::error!("Attempted to initialize the editor handle more than once");
|
||||||
|
}
|
||||||
editor_handle
|
editor_handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,36 +87,16 @@ impl JsEditorHandle {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
// Get the editor, dispatch the message, and store the `FrontendMessage` queue response
|
||||||
{
|
editor(|editor| {
|
||||||
let message: Message = message.into();
|
// Get the editor, then dispatch the message to the backend, and return its response `FrontendMessage` queue
|
||||||
let message = ron::to_string(&message).unwrap();
|
let frontend_messages = editor.handle_message(message.into());
|
||||||
|
|
||||||
dispatchTauri(message);
|
// Send each `FrontendMessage` to the JavaScript frontend
|
||||||
}
|
for message in frontend_messages.into_iter() {
|
||||||
#[cfg(not(feature = "tauri"))]
|
self.send_frontend_message_to_js(message);
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends a FrontendMessage to JavaScript
|
// Sends a FrontendMessage to JavaScript
|
||||||
|
|
@ -238,7 +146,7 @@ impl JsEditorHandle {
|
||||||
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
|
*g.borrow_mut() = Some(Closure::new(move |timestamp| {
|
||||||
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
|
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 micros: f64 = timestamp * 1000.;
|
||||||
let timestamp = Duration::from_micros(micros.round() as u64);
|
let timestamp = Duration::from_micros(micros.round() as u64);
|
||||||
|
|
||||||
|
|
@ -778,51 +686,13 @@ impl JsEditorHandle {
|
||||||
self.dispatch(message);
|
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)]
|
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||||
pub fn inject_imaginate_poll_server_status(&self) {
|
pub fn inject_imaginate_poll_server_status(&self) {
|
||||||
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
|
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)]
|
#[wasm_bindgen(js_name = evaluateMathExpression)]
|
||||||
pub fn evaluate_math_expression(expression: &str) -> Option<f64> {
|
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")
|
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]
|
#[test]
|
||||||
fn implicit_multiplication_preprocess_tests() {
|
fn implicit_multiplication_preprocess_tests() {
|
||||||
assert_eq!(implicit_multiplication_preprocess("2pi"), "2*pi");
|
assert_eq!(implicit_multiplication_preprocess("2pi"), "2*pi");
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ pub mod helpers;
|
||||||
|
|
||||||
use editor::messages::prelude::*;
|
use editor::messages::prelude::*;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{OnceCell, RefCell};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::panic;
|
use std::panic;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
@ -19,9 +18,8 @@ use wasm_bindgen::prelude::*;
|
||||||
pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
|
pub static EDITOR_HAS_CRASHED: AtomicBool = AtomicBool::new(false);
|
||||||
pub static LOGGER: WasmLog = WasmLog;
|
pub static LOGGER: WasmLog = WasmLog;
|
||||||
thread_local! {
|
thread_local! {
|
||||||
// TODO: Remove the concept of multiple editor instances to simplify all of this
|
pub static EDITOR: OnceCell<RefCell<editor::application::Editor>> = OnceCell::new();
|
||||||
pub static EDITOR_INSTANCES: RefCell<HashMap<u64, editor::application::Editor>> = RefCell::new(HashMap::new());
|
pub static EDITOR_HANDLE: OnceCell<RefCell<editor_api::EditorHandle>> = OnceCell::new();
|
||||||
pub static JS_EDITOR_HANDLES: RefCell<HashMap<u64, editor_api::JsEditorHandle>> = RefCell::new(HashMap::new());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the backend
|
/// Initialize the backend
|
||||||
|
|
@ -41,11 +39,12 @@ pub fn panic_hook(info: &panic::PanicInfo) {
|
||||||
|
|
||||||
error!("{info}");
|
error!("{info}");
|
||||||
|
|
||||||
JS_EDITOR_HANDLES.with(|instances| {
|
EDITOR_HANDLE.with(|editor_handle| {
|
||||||
instances
|
editor_handle.get().map(|handle| {
|
||||||
.borrow_mut()
|
handle
|
||||||
.values_mut()
|
.borrow_mut()
|
||||||
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
|
.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>
|
<span>Imaginate (Stable Diffusion node/tool)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational ongoing" title="Development Ongoing">
|
<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="" />
|
<img class="atlas" style="--atlas-index: 7" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||||
<span>Graph data attribute spreadsheet</span>
|
<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>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 51" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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>
|
<span>Boolean operations for shapes</span>
|
||||||
</div>
|
</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">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<img class="atlas" style="--atlas-index: 12" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||||
<span>WebGPU accelerated rendering</span>
|
<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="" />
|
<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>
|
<span>Adaptive resolution raster rendering</span>
|
||||||
</div>
|
</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">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 26" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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>
|
<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>
|
<span>Fully-supported brush tool</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<img class="atlas" style="--atlas-index: 41" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||||
<span>Select mode (marquee masking)</span>
|
<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>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 54" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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>
|
<span>Local file browser for saving/loading</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 8" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||||
<span>Custom subgraph nodes</span>
|
<span>Local fonts access</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 17" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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>
|
<h3>— Alpha 4 —</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 53" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<img class="atlas" style="--atlas-index: 21" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
||||||
<span>Local fonts access</span>
|
<span>Select mode (marquee masking)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="informational">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 52" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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="" />
|
<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>
|
<span>Code editor for custom nodes</span>
|
||||||
</div>
|
</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 -->
|
<!-- Beta -->
|
||||||
<div class="informational heading">
|
<div class="informational heading">
|
||||||
<h3>— Beta —</h3>
|
<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="" />
|
<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>
|
<span>Node manager and marketplace</span>
|
||||||
</div>
|
</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">
|
<div class="informational">
|
||||||
<img class="atlas" style="--atlas-index: 35" src="https://static.graphite.rs/icons/icon-atlas-roadmap__2.png" alt="" />
|
<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>
|
<span>Predictive graph rendering/caching</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue