Split apart the frontend Editor type into SubscriptionsRouter and EditorHandle (#3923)

* Remove the unused Editor.raw/wasmMemory/wasmImport

* Split out Editor.subscriptions

* Replace editor.handle.* with editor.* (1 of 2)

* Replace editor.handle.* with editor.* (2 of 2)

* Replace Editor typedef with EditorHandle import

* Pluralize subscription-router and rename subscriptionsRef->subscriptionsRouter and editorRef->editorHandle

* Remove editor.ts

* Update the readme

* Fix demo art loading bug
This commit is contained in:
Keavon Chambers
2026-03-20 23:34:13 -07:00
committed by GitHub
parent 64fd12a1a0
commit ed7987c881
40 changed files with 549 additions and 584 deletions

View File

@@ -1,24 +1,42 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { initWasm, createEditor } from "@graphite/editor";
import type { Editor as GraphiteEditor } from "@graphite/editor";
import init, { EditorHandle, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import { loadDemoArtwork } from "@graphite/utility-functions/network";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { createSubscriptionsRouter } from "/src/subscriptions-router";
import type { MessageName, SubscriptionsRouter } from "/src/subscriptions-router";
import Editor from "@graphite/components/Editor.svelte";
let editor: GraphiteEditor | undefined = undefined;
let subscriptions: SubscriptionsRouter | undefined = undefined;
let editor: EditorHandle | undefined = undefined;
onMount(async () => {
await initWasm();
// Initialize the Wasm module
const wasm = await init();
for (const [name, f] of Object.entries(wasm)) {
if (name.startsWith("__node_registry")) f();
}
window.imageCanvases = {};
window.receiveNativeMessage = receiveNativeMessage;
editor = createEditor();
// Create the editor and subscriptions router
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
subscriptions = createSubscriptionsRouter();
editor = EditorHandle.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => {
subscriptions?.handleFrontendMessage(messageType, messageData);
});
await loadDemoArtwork(editor);
});
onDestroy(() => {
editor?.destroy();
editor?.free();
});
</script>
{#if editor !== undefined}
<Editor {editor} />
{#if subscriptions !== undefined && editor !== undefined}
<Editor {subscriptions} {editor} />
{/if}

View File

@@ -2,45 +2,39 @@
## Svelte components: `components/`
Svelte components that build the Graphite editor GUI. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
Svelte components that build the Graphite editor GUI from layouts, panels, widgets, and menus. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
## Managers: `managers/`
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`).
TypeScript files, constructed by the editor frontend, which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to frontend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions in the backend (defined in `/frontend/wasm/editor_api.rs`).
Each manager module exports a factory function (e.g. `createClipboardManager(editor)`) that sets up message subscriptions and returns a `{ destroy }` object. In `Editor.svelte`, each manager is created at startup and its `destroy()` method is called on unmount to clean up subscriptions and side-effects (e.g. event listeners). Managers use self-accepting HMR to tear down and re-create with updated code during development.
Each manager module stores its dependencies (like `subscriptionsRouter` and `editorHandle`) in module-level variables and exports a `create*()` and `destroy*()` function pair. `Editor.svelte` calls each `create*()` constructor in its `onMount` and calls each `destroy*()` in its `onDestroy`. Managers replace themselves during HMR updates if they are modified live during development.
## Stores: `stores/`
TypeScript files which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a factory function (e.g. `createDialogStore(editor)`) that sets up backend message subscriptions and returns an object containing the store's `subscribe` method, any action methods for components to call, and a `destroy` method.
TypeScript files, constructed by the editor frontend, which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a `create*()` function that sets up frontend message subscriptions and returns `{ subscribe }` (the shape required by Svelte's custom store contract). A corresponding `destroy*()` function is also exported. Some stores also export standalone action functions (like `createCrashDialog()` or `toggleFullscreen()`) as module-level exports.
In `Editor.svelte`, each store is created and passed to Svelte's `setContext()`. Components access stores via `getContext<DialogStore>("dialog")` and use the `subscribe` method for reactive state and action methods (like `createCrashDialog()`) to trigger state changes.
In `Editor.svelte`, each store is created synchronously during component initialization (not in `onMount`, since child components need `getContext` access during their own initialization) and passed to Svelte's `setContext()`. Components access stores via calls like `getContext<DialogStore>("dialog")`. Unlike managers, stores do not replace themselves during HMR; instead, `Editor.svelte` is remounted to replace them entirely.
## *Managers vs. stores*
*Both managers and stores subscribe to backend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.*
*Both managers and stores subscribe to frontend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.*
## Utility functions: `utility-functions/`
TypeScript files which define and `export` individual helper functions for use elsewhere in the codebase. These files should not persist state outside each function.
## Wasm editor: `editor.ts`
## Subscriptions router: `subscriptions-router.ts`
Instantiates the Wasm and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the Wasm bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same Wasm module instance. The function returns an object where `raw` is the Wasm memory, `handle` provides access to callable backend functions, and `subscriptions` is the subscription router (described below).
`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the stores described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`.
## Subscription router: `subscription-router.ts`
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. The router's other function, `handleFrontendMessage(messageType, messageData)`, is called via the callback passed to `EditorHandle.create()` in `editor.ts` when the backend sends a `FrontendMessage`. When this occurs, the subscription router delivers the message to the subscriber by executing its registered `callback` function.
Associates messages from the backend with subscribers in the frontend, and routes messages to subscriber callbacks. This module provides a `subscribeFrontendMessage(messageType, callback)` function which JS code throughout the frontend can call to be registered as the exclusive handler for a chosen message type. The router's other function, `handleFrontendMessage(messageType, messageData)`, is called via the callback passed to `EditorHandle.create()` in `App.svelte` when the backend sends a `FrontendMessage`. When this occurs, the subscriptions router delivers the message to the subscriber by executing its registered `callback` function.
## Svelte app entry point: `App.svelte`
The entry point for the Svelte application.
The entry point for the Svelte application. Initializes the Wasm module, creates the `EditorHandle` backend instance and the subscriptions router, and renders `Editor.svelte` once both are ready. The `EditorHandle` is the wasm-bindgen interface to the Rust editor backend (defined in `/frontend/wasm/editor_api.rs`), providing access to callable backend functions. Both the editor and subscriptions router are passed as props to `Editor.svelte` and set as Svelte contexts for use throughout the component tree.
## Editor base instance: `Editor.svelte`
This is where we define global CSS style rules, construct all stores and managers with the editor instance, set store contexts for component access, and clean up all `destroy()` methods on unmount.
This is where we define global CSS style rules, construct all stores and managers, set store contexts for component access, and call each module's `destroy*()` function on unmount (on HMR during development).
## Global type augmentations: `global.d.ts`
@@ -48,4 +42,4 @@ Extends built-in browser type definitions using TypeScript's interface merging.
## JS bundle entry point: `main.ts`
The entry point for the entire project's code bundle. Here we simply mount the Svelte application with `export default mount(App, { target: document.body });`.
The entry point for the entire project's code bundle. Mounts the Svelte application with `export default mount(App, { target: document.body })`.

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, onDestroy, setContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { createClipboardManager, destroyClipboardManager } from "@graphite/managers/clipboard";
import { createFontsManager, destroyFontsManager } from "@graphite/managers/fonts";
import { createHyperlinkManager, destroyHyperlinkManager } from "@graphite/managers/hyperlink";
@@ -16,39 +16,42 @@
import { createNodeGraphStore, destroyNodeGraphStore } from "@graphite/stores/node-graph";
import { createPortfolioStore, destroyPortfolioStore } from "@graphite/stores/portfolio";
import { createTooltipStore, destroyTooltipStore } from "@graphite/stores/tooltip";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import MainWindow from "@graphite/components/window/MainWindow.svelte";
// Graphite Wasm editor
export let editor: Editor;
// Graphite Wasm editor and subscriptions router
export let subscriptions: SubscriptionsRouter;
export let editor: EditorHandle;
setContext("subscriptions", subscriptions);
setContext("editor", editor);
const stores = {
dialog: createDialogStore(editor),
tooltip: createTooltipStore(editor),
document: createDocumentStore(editor),
fullscreen: createFullscreenStore(editor),
nodeGraph: createNodeGraphStore(editor),
portfolio: createPortfolioStore(editor),
appWindow: createAppWindowStore(editor),
dialog: createDialogStore(subscriptions, editor),
tooltip: createTooltipStore(subscriptions),
document: createDocumentStore(subscriptions),
fullscreen: createFullscreenStore(subscriptions),
nodeGraph: createNodeGraphStore(subscriptions),
portfolio: createPortfolioStore(subscriptions, editor),
appWindow: createAppWindowStore(subscriptions),
};
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
onMount(() => {
createClipboardManager(editor);
createHyperlinkManager(editor);
createLocalizationManager(editor);
createPanicManager(editor);
createPersistenceManager(editor, stores.portfolio);
createFontsManager(editor);
createInputManager(editor, stores.dialog, stores.portfolio, stores.document);
createClipboardManager(subscriptions, editor);
createHyperlinkManager(subscriptions);
createLocalizationManager(subscriptions, editor);
createPanicManager(subscriptions);
createPersistenceManager(subscriptions, editor, stores.portfolio);
createFontsManager(subscriptions, editor);
createInputManager(subscriptions, editor, stores.dialog, stores.portfolio, stores.document);
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
// The backend handles idempotency, so this is safe to call again during HMR re-mounts.
editor.handle.initAfterFrontendReady();
editor.initAfterFrontendReady();
// Re-send all UI layouts from Rust so the frontend has them after an HMR re-mount
editor.handle.resendAllLayouts();
editor.resendAllLayouts();
});
onDestroy(() => {

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { TooltipStore } from "@graphite/stores/tooltip";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@@ -11,7 +10,7 @@
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
const tooltip = getContext<TooltipStore>("tooltip");
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
let self: FloatingMenu | undefined;
@@ -32,7 +31,7 @@
// TODO: Once all TODOs are replaced with real text, remove this function
function filterTodo(text: string | undefined): string | undefined {
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
if (text?.trim().toUpperCase() === "TODO" && !editor.inDevelopmentMode()) return "";
return text;
}

View File

@@ -2,25 +2,25 @@
import { getContext, onMount, onDestroy } from "svelte";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
let dataPanelLayout: Layout = [];
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
subscriptions.subscribeLayoutUpdate("DataPanel", (data) => {
patchLayout(dataPanelLayout, data);
dataPanelLayout = dataPanelLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("DataPanel");
subscriptions.unsubscribeLayoutUpdate("DataPanel");
});
</script>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { getContext, onMount, onDestroy, tick } from "svelte";
import type { Color, MenuDirection, MouseCursorIcon } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Color, EditorHandle, MenuDirection, MouseCursorIcon } from "@graphite/../wasm/pkg/graphite_wasm";
import type { AppWindowStore } from "@graphite/stores/app-window";
import type { DocumentStore } from "@graphite/stores/document";
import type { MessageBody } from "@graphite/subscription-router";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import type { MessageBody } from "/src/subscriptions-router";
import { fillChoiceColor, createColor } from "@graphite/utility-functions/colors";
import { pasteFile } from "@graphite/utility-functions/files";
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
@@ -26,7 +26,8 @@
let viewport: HTMLDivElement | undefined;
let gradientStopPicker: ColorPicker | undefined;
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
const editor = getContext<EditorHandle>("editor");
const appWindow = getContext<AppWindowStore>("appWindow");
const document = getContext<DocumentStore>("document");
@@ -142,13 +143,13 @@
function panCanvasX(newValue: number) {
const delta = newValue - scrollbarPos.x;
scrollbarPos.x = newValue;
editor.handle.panCanvas(-delta * scrollbarMultiplier.x, 0);
editor.panCanvas(-delta * scrollbarMultiplier.x, 0);
}
function panCanvasY(newValue: number) {
const delta = newValue - scrollbarPos.y;
scrollbarPos.y = newValue;
editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y);
editor.panCanvas(0, -delta * scrollbarMultiplier.y);
}
function canvasPointerDown(e: PointerEvent) {
@@ -342,7 +343,7 @@
export function triggerTextCommit() {
if (!textInput) return;
const textCleaned = textInputCleanup(textInput.innerText);
editor.handle.onChangeText(textCleaned, false);
editor.onChangeText(textCleaned, false);
}
export async function displayEditableTextbox(data: MessageBody<"DisplayEditableTextbox">) {
@@ -372,7 +373,7 @@
textInput.oninput = () => {
if (!textInput) return;
editor.handle.updateBounds(textInputCleanup(textInput.innerText));
editor.updateBounds(textInputCleanup(textInput.innerText));
};
textInputMatrix = data.transform;
@@ -454,12 +455,12 @@
updatePixelRatio();
// Update rendered SVGs
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentArtwork", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateDocumentArtwork", async (data) => {
await tick();
updateDocumentArtwork(data.svg);
});
editor.subscriptions.subscribeFrontendMessage("UpdateEyedropperSamplingState", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateEyedropperSamplingState", async (data) => {
await tick();
const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data;
@@ -467,25 +468,25 @@
const rgb = await updateEyedropperSamplingState(imageData, mousePosition, primaryColor, secondaryColor);
if (setColorChoice && rgb) {
if (setColorChoice === "Primary") editor.handle.updatePrimaryColor(...rgb, 1);
if (setColorChoice === "Secondary") editor.handle.updateSecondaryColor(...rgb, 1);
if (setColorChoice === "Primary") editor.updatePrimaryColor(...rgb, 1);
if (setColorChoice === "Secondary") editor.updateSecondaryColor(...rgb, 1);
}
});
// Gradient stop color picker
editor.subscriptions.subscribeFrontendMessage("UpdateGradientStopColorPickerPosition", (data) => {
subscriptions.subscribeFrontendMessage("UpdateGradientStopColorPickerPosition", (data) => {
gradientStopPickerColor = data.color;
gradientStopPickerPosition = { x: data.position[0], y: data.position[1] };
});
// Update scrollbars and rulers
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentScrollbars", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateDocumentScrollbars", async (data) => {
await tick();
const { position, size, multiplier } = data;
updateDocumentScrollbars(position, size, multiplier);
});
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateDocumentRulers", async (data) => {
await tick();
const { origin, spacing, interval, visible } = data;
@@ -493,24 +494,24 @@
});
// Update mouse cursor icon
editor.subscriptions.subscribeFrontendMessage("UpdateMouseCursor", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateMouseCursor", async (data) => {
await tick();
updateMouseCursor(data.cursor);
});
// Text entry
editor.subscriptions.subscribeFrontendMessage("TriggerTextCommit", async () => {
subscriptions.subscribeFrontendMessage("TriggerTextCommit", async () => {
await tick();
triggerTextCommit();
});
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextbox", async (data) => {
subscriptions.subscribeFrontendMessage("DisplayEditableTextbox", async (data) => {
await tick();
displayEditableTextbox(data);
});
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxUpdateFontData", async (data) => {
subscriptions.subscribeFrontendMessage("DisplayEditableTextboxUpdateFontData", async (data) => {
await tick();
if (textInput && data.fontData.length > 0 && data.fontData.buffer instanceof ArrayBuffer) {
@@ -521,10 +522,10 @@
textInput.style.fontFamily = "text-font";
}
});
editor.subscriptions.subscribeFrontendMessage("DisplayEditableTextboxTransform", async (data) => {
subscriptions.subscribeFrontendMessage("DisplayEditableTextboxTransform", async (data) => {
textInputMatrix = data.transform;
});
editor.subscriptions.subscribeFrontendMessage("DisplayRemoveEditableTextbox", async () => {
subscriptions.subscribeFrontendMessage("DisplayRemoveEditableTextbox", async () => {
await tick();
displayRemoveEditableTextbox();
@@ -547,17 +548,17 @@
removeUpdatePixelRatio?.();
addedFontFaces.forEach((face) => window.document.fonts.delete(face));
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentArtwork");
editor.subscriptions.unsubscribeFrontendMessage("UpdateEyedropperSamplingState");
editor.subscriptions.unsubscribeFrontendMessage("UpdateGradientStopColorPickerPosition");
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentScrollbars");
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentRulers");
editor.subscriptions.unsubscribeFrontendMessage("UpdateMouseCursor");
editor.subscriptions.unsubscribeFrontendMessage("TriggerTextCommit");
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextbox");
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxUpdateFontData");
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxTransform");
editor.subscriptions.unsubscribeFrontendMessage("DisplayRemoveEditableTextbox");
subscriptions.unsubscribeFrontendMessage("UpdateDocumentArtwork");
subscriptions.unsubscribeFrontendMessage("UpdateEyedropperSamplingState");
subscriptions.unsubscribeFrontendMessage("UpdateGradientStopColorPickerPosition");
subscriptions.unsubscribeFrontendMessage("UpdateDocumentScrollbars");
subscriptions.unsubscribeFrontendMessage("UpdateDocumentRulers");
subscriptions.unsubscribeFrontendMessage("UpdateMouseCursor");
subscriptions.unsubscribeFrontendMessage("TriggerTextCommit");
subscriptions.unsubscribeFrontendMessage("DisplayEditableTextbox");
subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxUpdateFontData");
subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxTransform");
subscriptions.unsubscribeFrontendMessage("DisplayRemoveEditableTextbox");
});
</script>
@@ -628,7 +629,7 @@
open={Boolean(gradientStopPickerPosition && gradientStopPickerColor)}
on:open={({ detail }) => {
if (!detail) {
editor.handle.closeGradientStopColorPicker();
editor.closeGradientStopColorPicker();
gradientStopPickerPosition = undefined;
gradientStopPickerColor = undefined;
}
@@ -636,10 +637,10 @@
colorOrGradient={{ Solid: gradientStopPickerColor || createColor(0, 0, 0, 1) }}
on:colorOrGradient={({ detail }) => {
const color = fillChoiceColor(detail);
if (color) editor.handle.updateGradientStopColor(color.red, color.green, color.blue, color.alpha);
if (color) editor.updateGradientStopColor(color.red, color.green, color.blue, color.alpha);
}}
on:startHistoryTransaction={() => editor.handle.startGradientStopColorTransaction()}
on:commitHistoryTransaction={() => editor.handle.commitGradientStopColorTransaction()}
on:startHistoryTransaction={() => editor.startGradientStopColorTransaction()}
on:commitHistoryTransaction={() => editor.commitGradientStopColorTransaction()}
bind:this={gradientStopPicker}
/>
</div>
@@ -682,10 +683,10 @@
direction="Vertical"
thumbLength={scrollbarSize.y}
thumbPosition={scrollbarPos.y}
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(0, detail)}
on:trackShift={({ detail }) => editor.panCanvasByFraction(0, detail)}
on:thumbPosition={({ detail }) => panCanvasY(detail)}
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
on:thumbDragAbort={() => editor.handle.panCanvasAbort(false)}
on:thumbDragStart={() => editor.panCanvasAbortPrepare(false)}
on:thumbDragAbort={() => editor.panCanvasAbort(false)}
/>
</LayoutCol>
</LayoutRow>
@@ -694,10 +695,10 @@
direction="Horizontal"
thumbLength={scrollbarSize.x}
thumbPosition={scrollbarPos.x}
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(detail, 0)}
on:trackShift={({ detail }) => editor.panCanvasByFraction(detail, 0)}
on:thumbPosition={({ detail }) => panCanvasX(detail)}
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)}
on:thumbDragStart={() => editor.panCanvasAbortPrepare(true)}
on:thumbDragAbort={() => editor.panCanvasAbort(true)}
/>
</LayoutRow>
</LayoutCol>

View File

@@ -2,10 +2,10 @@
import { getContext, onMount, onDestroy, tick } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { NodeGraphStore } from "@graphite/stores/node-graph";
import type { TooltipStore } from "@graphite/stores/tooltip";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { pasteFile } from "@graphite/utility-functions/files";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { patchLayout } from "@graphite/utility-functions/widgets";
@@ -41,7 +41,8 @@
startY: number;
};
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
const editor = getContext<EditorHandle>("editor");
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
const tooltip = getContext<TooltipStore>("tooltip");
@@ -69,26 +70,26 @@
let layersPanelBottomBarLayout: Layout = [];
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlLeftBar", (data) => {
subscriptions.subscribeLayoutUpdate("LayersPanelControlLeftBar", (data) => {
patchLayout(layersPanelControlBarLeftLayout, data);
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
});
editor.subscriptions.subscribeLayoutUpdate("LayersPanelControlRightBar", (data) => {
subscriptions.subscribeLayoutUpdate("LayersPanelControlRightBar", (data) => {
patchLayout(layersPanelControlBarRightLayout, data);
layersPanelControlBarRightLayout = layersPanelControlBarRightLayout;
});
editor.subscriptions.subscribeLayoutUpdate("LayersPanelBottomBar", (data) => {
subscriptions.subscribeLayoutUpdate("LayersPanelBottomBar", (data) => {
patchLayout(layersPanelBottomBarLayout, data);
layersPanelBottomBarLayout = layersPanelBottomBarLayout;
});
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerStructure", (data) => {
subscriptions.subscribeFrontendMessage("UpdateDocumentLayerStructure", (data) => {
rebuildLayerHierarchy(data.layerStructure);
});
editor.subscriptions.subscribeFrontendMessage("UpdateDocumentLayerDetails", (data) => {
subscriptions.subscribeFrontendMessage("UpdateDocumentLayerDetails", (data) => {
const targetLayer = data.data;
const targetId = targetLayer.id;
@@ -107,11 +108,11 @@
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlLeftBar");
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelControlRightBar");
editor.subscriptions.unsubscribeLayoutUpdate("LayersPanelBottomBar");
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerStructure");
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerDetails");
subscriptions.unsubscribeLayoutUpdate("LayersPanelControlLeftBar");
subscriptions.unsubscribeLayoutUpdate("LayersPanelControlRightBar");
subscriptions.unsubscribeLayoutUpdate("LayersPanelBottomBar");
subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerStructure");
subscriptions.unsubscribeFrontendMessage("UpdateDocumentLayerDetails");
removeEventListener("pointerup", draggingPointerUp);
removeEventListener("pointermove", draggingPointerMove);
@@ -125,17 +126,17 @@
});
function toggleNodeVisibilityLayerPanel(id: bigint) {
editor.handle.toggleNodeVisibilityLayerPanel(id);
editor.toggleNodeVisibilityLayerPanel(id);
}
function toggleLayerLock(id: bigint) {
editor.handle.toggleLayerLock(id);
editor.toggleLayerLock(id);
}
function handleExpandArrowClickWithModifiers(e: MouseEvent, id: bigint) {
const accel = operatingSystem() === "Mac" ? e.metaKey : e.ctrlKey;
const collapseRecursive = e.altKey || accel;
editor.handle.toggleLayerExpansion(id, collapseRecursive);
editor.toggleLayerExpansion(id, collapseRecursive);
e.stopPropagation();
}
@@ -162,7 +163,7 @@
layers = layers;
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
editor.handle.setLayerName(listing.entry.id, name);
editor.setLayerName(listing.entry.id, name);
listing.entry.alias = name;
}
@@ -200,7 +201,7 @@
}
function clipLayer(listing: LayerListingInfo) {
editor.handle.clipLayer(listing.entry.id);
editor.clipLayer(listing.entry.id);
}
function clippingKeyPress(e: KeyboardEvent) {
@@ -247,7 +248,7 @@
// Don't select while we are entering text to rename the layer
if (listing.editingName) return;
editor.handle.selectLayer(listing.entry.id, accel, shift);
editor.selectLayer(listing.entry.id, accel, shift);
}
async function deselectAllLayers() {
@@ -256,7 +257,7 @@
return;
}
editor.handle.deselectAllLayers();
editor.deselectAllLayers();
}
function calculateDragIndex(tree: LayoutCol, clientY: number, select?: () => void): DraggingData {
@@ -389,7 +390,7 @@
// Commit the move
select?.();
editor.handle.moveLayerInTree(insertParentId, insertIndex);
editor.moveLayerInTree(insertParentId, insertIndex);
// Prevent the subsequent click event from processing
justFinishedDrag = true;
@@ -445,7 +446,7 @@
const inputElement = document.activeElement;
if (inputElement instanceof HTMLInputElement) {
const name = inputElement.value || "";
editor.handle.setLayerName(currentListing.entry.id, name);
editor.setLayerName(currentListing.entry.id, name);
currentListing.entry.alias = name;
}

View File

@@ -2,25 +2,25 @@
import { getContext, onMount, onDestroy } from "svelte";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
let propertiesPanelLayout: Layout = [];
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("PropertiesPanel", (data) => {
subscriptions.subscribeLayoutUpdate("PropertiesPanel", (data) => {
patchLayout(propertiesPanelLayout, data);
propertiesPanelLayout = propertiesPanelLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("PropertiesPanel");
subscriptions.unsubscribeLayoutUpdate("PropertiesPanel");
});
</script>

View File

@@ -2,8 +2,8 @@
import { getContext, onMount, onDestroy } from "svelte";
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { pasteFile } from "@graphite/utility-functions/files";
import { patchLayout } from "@graphite/utility-functions/widgets";
@@ -13,19 +13,20 @@
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
const editor = getContext<EditorHandle>("editor");
let welcomePanelButtonsLayout: Layout = [];
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("WelcomeScreenButtons", (data) => {
subscriptions.subscribeLayoutUpdate("WelcomeScreenButtons", (data) => {
patchLayout(welcomePanelButtonsLayout, data);
welcomePanelButtonsLayout = welcomePanelButtonsLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("WelcomeScreenButtons");
subscriptions.unsubscribeLayoutUpdate("WelcomeScreenButtons");
});
function dropFile(e: DragEvent) {

View File

@@ -3,8 +3,7 @@
import { cubicInOut } from "svelte/easing";
import { fade } from "svelte/transition";
import type { FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/../wasm/pkg/graphite_wasm";
import type { DocumentStore } from "@graphite/stores/document";
import { closeContextMenu } from "@graphite/stores/node-graph";
import type { NodeGraphStore } from "@graphite/stores/node-graph";
@@ -20,7 +19,7 @@
const GRID_SIZE = 24;
const FADE_TRANSITION = { duration: 200, easing: cubicInOut };
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
const documentState = getContext<DocumentStore>("document");
@@ -83,7 +82,7 @@
if (editingNameImportIndex !== undefined) {
if (!(event.target instanceof HTMLInputElement)) return;
let text = event.target.value;
editor.handle.setImportName(editingNameImportIndex, text);
editor.setImportName(editingNameImportIndex, text);
editingNameImportIndex = undefined;
}
}
@@ -92,7 +91,7 @@
if (editingNameExportIndex !== undefined) {
if (!(event.target instanceof HTMLInputElement)) return;
let text = event.target.value;
editor.handle.setExportName(editingNameExportIndex, text);
editor.setExportName(editingNameExportIndex, text);
editingNameExportIndex = undefined;
}
}
@@ -111,7 +110,7 @@
function createNode(identifier: string) {
if ($nodeGraph.contextMenuInformation === undefined) return;
editor.handle.createNode(identifier, $nodeGraph.contextMenuInformation.contextMenuCoordinates[0], $nodeGraph.contextMenuInformation.contextMenuCoordinates[1]);
editor.createNode(identifier, $nodeGraph.contextMenuInformation.contextMenuCoordinates[0], $nodeGraph.contextMenuInformation.contextMenuCoordinates[1]);
}
function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string {
@@ -174,11 +173,11 @@
}
function outputConnectedToText(output: FrontendGraphOutput): string {
return editor.handle.inDevelopmentMode() ? output.connectedTo.join("\n") : "";
return editor.inDevelopmentMode() ? output.connectedTo.join("\n") : "";
}
function inputConnectedToText(input: FrontendGraphInput): string {
return editor.handle.inDevelopmentMode() ? input.connectedTo : "";
return editor.inDevelopmentMode() ? input.connectedTo : "";
}
function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
@@ -220,7 +219,7 @@
<TextButton
label="Merge Selected Nodes"
action={() => {
editor.handle.mergeSelectedNodes();
editor.mergeSelectedNodes();
closeContextMenu();
}}
flush={true}
@@ -230,7 +229,7 @@
label={currentlyIsNode ? "Display as Layer" : "Display as Node"}
action={() => {
if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") {
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
editor.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
}
closeContextMenu();
}}
@@ -244,9 +243,9 @@
label={allLocked ? "Unlock" : "Lock"}
action={() => {
if ($nodeGraph.selected.includes(nodeId)) {
editor.handle.toggleSelectedLocked();
editor.toggleSelectedLocked();
} else {
editor.handle.toggleLayerLock(nodeId);
editor.toggleLayerLock(nodeId);
}
closeContextMenu();
}}
@@ -383,7 +382,7 @@
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24}
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryImport()} />
<IconButton size={24} icon="Add" action={() => editor.addPrimaryImport()} />
</div>
{/if}
{/each}
@@ -454,7 +453,7 @@
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addPrimaryExport()} />
<IconButton size={24} icon="Add" action={() => editor.addPrimaryExport()} />
</div>
{/if}
{/each}
@@ -465,14 +464,14 @@
style:--offset-left={($nodeGraph.updateImportsExports.importPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.importPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.imports.length}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryImport()} />
<IconButton size={24} icon="Add" action={() => editor.addSecondaryImport()} />
</div>
<div
class="plus"
style:--offset-left={($nodeGraph.updateImportsExports.exportPosition[0] - 12) / 24}
style:--offset-top={($nodeGraph.updateImportsExports.exportPosition[1] - 12) / 24 + $nodeGraph.updateImportsExports.exports.length}
>
<IconButton size={24} icon="Add" action={() => editor.handle.addSecondaryExport()} />
<IconButton size={24} icon="Add" action={() => editor.addSecondaryExport()} />
</div>
{/if}
@@ -522,7 +521,7 @@
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
data-tooltip-label={nodeNameTooltipLabel(node)}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
${(description || "").trim()}${editor.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
`.trim()}
data-node={node.id}
>
@@ -685,7 +684,7 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
data-tooltip-label={nodeNameTooltipLabel(node)}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
${(description || "").trim()}${editor.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).` : ""}
`.trim()}
data-node={node.id}
>

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LayoutTarget, WidgetSection as WidgetSectionData } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, LayoutTarget, WidgetSection as WidgetSectionData } from "@graphite/../wasm/pkg/graphite_wasm";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
@@ -18,7 +17,7 @@
let expanded = true;
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
</script>
<!-- TODO: Implement collapsable sections with properties system -->
@@ -31,7 +30,7 @@
tooltipDescription={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected." : "Pin this node so it's shown here when nothing is selected."}
size={24}
action={(e) => {
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
editor.setNodePinned(widgetData.id, !widgetData.pinned);
e?.stopPropagation();
}}
class="show-only-on-hover"
@@ -41,7 +40,7 @@
tooltipDescription="Delete this node from the layer chain."
size={24}
action={(e) => {
editor.handle.deleteNode(widgetData.id);
editor.deleteNode(widgetData.id);
e?.stopPropagation();
}}
class="show-only-on-hover"
@@ -52,7 +51,7 @@
tooltipDescription={widgetData.visible ? "Hide this node." : "Show this node."}
size={24}
action={(e) => {
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
editor.toggleNodeVisibilityLayerPanel(widgetData.id);
e?.stopPropagation();
}}
class={widgetData.visible ? "show-only-on-hover" : ""}

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getContext } from "svelte";
import type { LayoutTarget, Widget, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, LayoutTarget, Widget, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm";
import { parseFillChoice } from "@graphite/utility-functions/colors";
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
@@ -35,7 +34,7 @@
// A Widget tagged enum unwrapped into a correlated [kind, props] tuple
type UnwrappedWidget = { [K in WidgetKind]: [kind: K, props: WidgetProps<K>] }[WidgetKind];
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
export let widgets: WidgetInstance[];
export let direction: "row" | "column";
@@ -52,15 +51,15 @@
.join(" ");
function widgetValueCommit(widgetIndex: number, value: unknown) {
editor.handle.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
editor.widgetValueCommit(layoutTarget, widgets[widgetIndex].widgetId, value);
}
function widgetValueUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
editor.handle.widgetValueUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
editor.widgetValueUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
}
function widgetValueCommitAndUpdate(widgetIndex: number, value: unknown, resendWidget: boolean) {
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
editor.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
}
// Extracts the kind and props from a Widget tagged enum, validated against the widget registry.

View File

@@ -2,8 +2,7 @@
import { createEventDispatcher, onMount, onDestroy, getContext } from "svelte";
import { evaluateMathExpression, isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { ActionShortcut, EditorHandle, NumberInputIncrementBehavior, NumberInputMode } from "@graphite/../wasm/pkg/graphite_wasm";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/managers/input";
import { browserVersion } from "@graphite/utility-functions/platform";
@@ -17,7 +16,7 @@
const dispatch = createEventDispatcher<{ value: number | undefined; startHistoryTransaction: undefined }>();
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
// Content
/// When `value` is not provided (i.e. it's `undefined`), a dash is displayed.
@@ -408,7 +407,7 @@
// Enter dragging state
if (usePointerLock) target.requestPointerLock();
if (isPlatformNative()) {
editor.handle.appWindowPointerLock();
editor.appWindowPointerLock();
}
initialValueBeforeDragging = value;
cumulativeDragDelta = 0;

View File

@@ -1,15 +1,14 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Color } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { Color, EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { fillChoiceColor, colorToRgbaCSS } from "@graphite/utility-functions/colors";
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
// Content
export let primary: Color;
@@ -29,11 +28,11 @@
}
function primaryColorChanged(color: Color) {
editor.handle.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
editor.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
}
function secondaryColorChanged(color: Color) {
editor.handle.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
editor.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
}
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext, tick } from "svelte";
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -25,7 +25,7 @@
const BUTTON_LEFT = 0;
const BUTTON_MIDDLE = 1;
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
export let tabMinWidths = false;
export let tabCloseButtons = false;
@@ -56,7 +56,7 @@
}
</script>
<LayoutCol on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}>
<LayoutCol on:pointerdown={() => panelType && editor.setActivePanel(panelType)} class={`panel ${className}`.trim()} {classes} style={styleName} {styles}>
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-group" scrollableX={true} on:click={onEmptySpaceAction} on:auxclick={onEmptySpaceAction}>
{#each tabLabels as tabLabel, tabIndex}

View File

@@ -2,32 +2,33 @@
import { getContext, onMount, onDestroy } from "svelte";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
const editor = getContext<Editor>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
let statusBarHintsLayout: Layout = [];
let statusBarInfoLayout: Layout = [];
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("StatusBarHints", (data) => {
subscriptions.subscribeLayoutUpdate("StatusBarHints", (data) => {
patchLayout(statusBarHintsLayout, data);
statusBarHintsLayout = statusBarHintsLayout;
});
editor.subscriptions.subscribeLayoutUpdate("StatusBarInfo", (data) => {
subscriptions.subscribeLayoutUpdate("StatusBarInfo", (data) => {
patchLayout(statusBarInfoLayout, data);
statusBarInfoLayout = statusBarInfoLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("StatusBarHints");
editor.subscriptions.unsubscribeLayoutUpdate("StatusBarInfo");
subscriptions.unsubscribeLayoutUpdate("StatusBarHints");
subscriptions.unsubscribeLayoutUpdate("StatusBarInfo");
});
</script>

View File

@@ -2,12 +2,12 @@
import { getContext, onMount, onDestroy } from "svelte";
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { AppWindowStore } from "@graphite/stores/app-window";
import { enterFullscreen, exitFullscreen } from "@graphite/stores/fullscreen";
import type { FullscreenStore } from "@graphite/stores/fullscreen";
import type { TooltipStore } from "@graphite/stores/tooltip";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
@@ -16,8 +16,9 @@
const keyboardLockApiSupported = navigator.keyboard !== undefined && "lock" in navigator.keyboard;
const editor = getContext<EditorHandle>("editor");
const subscriptions = getContext<SubscriptionsRouter>("subscriptions");
const appWindow = getContext<AppWindowStore>("appWindow");
const editor = getContext<Editor>("editor");
const fullscreen = getContext<FullscreenStore>("fullscreen");
const tooltip = getContext<TooltipStore>("tooltip");
@@ -29,14 +30,14 @@
$: height = $appWindow.platform === "Mac" ? 28 * (1 / $appWindow.uiScale) : 28;
onMount(() => {
editor.subscriptions.subscribeLayoutUpdate("MenuBar", (data) => {
subscriptions.subscribeLayoutUpdate("MenuBar", (data) => {
patchLayout(menuBarLayout, data);
menuBarLayout = menuBarLayout;
});
});
onDestroy(() => {
editor.subscriptions.unsubscribeLayoutUpdate("MenuBar");
subscriptions.unsubscribeLayoutUpdate("MenuBar");
});
</script>
@@ -48,7 +49,7 @@
{/if}
</LayoutRow>
<!-- Window frame -->
<LayoutRow class="window-frame" on:mousedown={() => !isFullscreen && editor.handle.appWindowDrag()} on:dblclick={() => !isFullscreen && editor.handle.appWindowMaximize()} />
<LayoutRow class="window-frame" on:mousedown={() => !isFullscreen && editor.appWindowDrag()} on:dblclick={() => !isFullscreen && editor.appWindowMaximize()} />
<!-- Window buttons -->
<LayoutRow class="window-buttons" classes={{ fullscreen: showFullscreenButton, windows: $appWindow.platform === "Windows", linux: $appWindow.platform === "Linux" }}>
{#if $appWindow.platform !== "Mac"}
@@ -60,20 +61,20 @@
: undefined}
tooltipShortcut={$tooltip.fullscreenShortcut}
on:click={() => {
if (isPlatformNative()) editor.handle.appWindowFullscreen();
if (isPlatformNative()) editor.appWindowFullscreen();
else ($fullscreen.windowFullscreen ? exitFullscreen : enterFullscreen)();
}}
>
<IconLabel icon={isFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
</LayoutRow>
{:else}
<LayoutRow tooltipLabel="Minimize" on:click={() => editor.handle.appWindowMinimize()}>
<LayoutRow tooltipLabel="Minimize" on:click={() => editor.appWindowMinimize()}>
<IconLabel icon="WindowButtonWinMinimize" />
</LayoutRow>
<LayoutRow tooltipLabel={$appWindow.maximized ? ($appWindow.platform === "Windows" ? "Restore Down" : "Unmaximize") : "Maximize"} on:click={() => editor.handle.appWindowMaximize()}>
<LayoutRow tooltipLabel={$appWindow.maximized ? ($appWindow.platform === "Windows" ? "Restore Down" : "Unmaximize") : "Maximize"} on:click={() => editor.appWindowMaximize()}>
<IconLabel icon={$appWindow.maximized ? "WindowButtonWinRestoreDown" : "WindowButtonWinMaximize"} />
</LayoutRow>
<LayoutRow tooltipLabel="Close" on:click={() => editor.handle.appWindowClose()}>
<LayoutRow tooltipLabel="Close" on:click={() => editor.appWindowClose()}>
<IconLabel icon="WindowButtonWinClose" />
</LayoutRow>
{/if}

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getContext, onDestroy } from "svelte";
import type { OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
import type { PortfolioStore } from "@graphite/stores/portfolio";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -35,13 +34,13 @@
$: documentTabLabels = $portfolio.documents.map((doc: OpenDocument) => {
const name = doc.details.name;
const unsaved = !doc.details.isSaved;
if (!editor.handle.inDevelopmentMode()) return { name, unsaved };
if (!editor.inDevelopmentMode()) return { name, unsaved };
const tooltipDescription = `Document ID: ${doc.id}`;
return { name, unsaved, tooltipLabel: name, tooltipDescription };
});
const editor = getContext<Editor>("editor");
const editor = getContext<EditorHandle>("editor");
const portfolio = getContext<PortfolioStore>("portfolio");
function resizePanel(e: PointerEvent) {
@@ -151,9 +150,9 @@
tabCloseButtons={true}
tabMinWidths={true}
tabLabels={documentTabLabels}
emptySpaceAction={() => editor.handle.newDocumentDialog()}
clickAction={(tabIndex) => editor.handle.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.handle.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
emptySpaceAction={() => editor.newDocumentDialog()}
clickAction={(tabIndex) => editor.selectDocument($portfolio.documents[tabIndex].id)}
closeAction={(tabIndex) => editor.closeDocumentWithConfirmation($portfolio.documents[tabIndex].id)}
tabActiveIndex={$portfolio.activeDocumentIndex}
bind:this={documentPanel}
/>

View File

@@ -1,86 +0,0 @@
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
import init, { EditorHandle, wasmMemory, receiveNativeMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import type { FrontendMessage } from "@graphite/../wasm/pkg/graphite_wasm";
import { createSubscriptionRouter } from "@graphite/subscription-router";
import type { MessageName, SubscriptionRouter } from "@graphite/subscription-router";
import { operatingSystem } from "@graphite/utility-functions/platform";
// TODO: Remove `raw`, split out `subscriptions`, and unwrap the remaining `handle` so `EditorHandle` can replace `Editor` and then it can also be renamed to `Editor` to fully remove `EditorHandle`.
export type Editor = {
raw: WebAssembly.Memory;
handle: EditorHandle;
subscriptions: SubscriptionRouter;
destroy: () => void;
};
// `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;
// Should be called asynchronously before `createEditor()`.
export async function initWasm() {
// Skip if the Wasm module is already initialized
if (wasmImport !== undefined) return;
// Import the Wasm module JS bindings and wrap them in the panic proxy
const wasm = await init();
for (const [name, f] of Object.entries(wasm)) {
if (name.startsWith("__node_registry")) f();
}
wasmImport = await wasmMemory();
window.imageCanvases = {};
window.receiveNativeMessage = receiveNativeMessage;
}
// 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: WebAssembly.Memory = wasmImport;
// Provide a random starter seed which must occur after initializing the Wasm module, since Wasm can't generate its own random numbers
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
const handle = EditorHandle.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => {
// This callback is called by Wasm when a FrontendMessage is received from the Wasm wrapper `EditorHandle`
subscriptions.handleFrontendMessage(messageType, messageData);
});
// Subscriptions: allows subscribing to messages in JS that are sent from the Wasm backend
const subscriptions = createSubscriptionRouter();
// Check if the URL hash fragment has any demo artwork to be loaded
const demoArtworkAbortController = new AbortController();
(async () => {
const demoArtwork = window.location.hash.trim().match(/#demo\/(.*)/)?.[1];
if (!demoArtwork) return;
try {
const url = new URL(`/demo-artwork/${demoArtwork}.${handle.fileExtension()}`, document.location.href);
const data = await fetch(url, { signal: demoArtworkAbortController.signal });
if (!data.ok) throw new Error();
const filename = url.pathname.split("/").pop() || "Untitled";
const content = await data.bytes();
handle.openFile(`${filename}.${handle.fileExtension()}`, content);
// Remove the hash fragment from the URL
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
} catch {
// Do nothing
}
})();
function destroy() {
handle.free();
demoArtworkAbortController.abort();
}
return { raw, handle, subscriptions, destroy };
}
// Wasm state can't be hot-replaced, so we tell Vite to do a full page reload when this module changes
import.meta.hot?.accept(() => location.reload());

View File

@@ -1,37 +1,40 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { insertAtCaret, readAtCaret } from "@graphite/utility-functions/clipboard";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let editorHandle: EditorHandle | undefined = undefined;
export function createClipboardManager(editor: Editor) {
export function createClipboardManager(subscriptions: SubscriptionsRouter, editor: EditorHandle) {
destroyClipboardManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editorHandle = editor;
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
// If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(data.content);
});
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
editor.readSelection(readAtCaret(data.cut), data.cut);
});
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
insertAtCaret(data.content);
});
}
export function destroyClipboardManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef) newModule?.createClipboardManager(editorRef);
if (subscriptionsRouter && editorHandle) newModule?.createClipboardManager(subscriptionsRouter, editorHandle);
});

View File

@@ -1,19 +1,22 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
type ApiResponse = { family: string; variants: string[]; files: Record<string, string> }[];
const FONT_LIST_API = "https://api.graphite.art/font-list";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let editorHandle: EditorHandle | undefined = undefined;
let abortController: AbortController | undefined = undefined;
export function createFontsManager(editor: Editor) {
export function createFontsManager(subscriptions: SubscriptionsRouter, editor: EditorHandle) {
destroyFontsManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editorHandle = editor;
abortController = new AbortController();
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
try {
const response = await fetch(FONT_LIST_API, abortController ? { signal: abortController.signal } : undefined);
if (!response.ok) throw new Error(`Font catalog request failed with status ${response.status}`);
@@ -31,14 +34,14 @@ export function createFontsManager(editor: Editor) {
return { name: font.family, styles };
});
editor.handle.onFontCatalogLoad(catalog);
editor.onFontCatalogLoad(catalog);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
throw error;
}
});
editor.subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerFontDataLoad", async (data) => {
const { fontFamily, fontStyle } = data.font;
try {
@@ -48,7 +51,7 @@ export function createFontsManager(editor: Editor) {
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
editor.handle.onFontLoad(fontFamily, fontStyle, bytes);
editor.onFontLoad(fontFamily, fontStyle, bytes);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
// eslint-disable-next-line no-console
@@ -58,15 +61,15 @@ export function createFontsManager(editor: Editor) {
}
export function destroyFontsManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
abortController?.abort();
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef) newModule?.createFontsManager(editorRef);
if (subscriptionsRouter && editorHandle) newModule?.createFontsManager(subscriptionsRouter, editorHandle);
});

View File

@@ -1,25 +1,25 @@
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
export function createHyperlinkManager(editor: Editor) {
export function createHyperlinkManager(subscriptions: SubscriptionsRouter) {
destroyHyperlinkManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
window.open(data.url, "_blank", "noopener");
});
}
export function destroyHyperlinkManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef) newModule?.createHyperlinkManager(editorRef);
if (subscriptionsRouter) newModule?.createHyperlinkManager(subscriptionsRouter);
});

View File

@@ -1,8 +1,9 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { DialogStore } from "@graphite/stores/dialog";
import type { DocumentStore } from "@graphite/stores/document";
import { fullscreenModeChanged } from "@graphite/stores/fullscreen";
import type { PortfolioStore } from "@graphite/stores/portfolio";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { triggerClipboardRead } from "@graphite/utility-functions/clipboard";
import {
onBeforeUnload,
@@ -32,42 +33,44 @@ export const PRESS_REPEAT_DELAY_MS = 400;
export const PRESS_REPEAT_INTERVAL_MS = 72;
export const PRESS_REPEAT_INTERVAL_RAPID_MS = 10;
const listeners: Listener[] = [
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => editorRef && portfolioStore && onBeforeUnload(e, editorRef, portfolioStore) },
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => editorRef && dialogStore && onKeyUp(e, editorRef, dialogStore) },
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => editorRef && dialogStore && onKeyDown(e, editorRef, dialogStore) },
{ target: window, eventName: "pointermove", action: (e: PointerEvent) => editorRef && documentStore && onPointerMove(e, editorRef, documentStore) },
{ target: window, eventName: "pointerdown", action: (e: PointerEvent) => editorRef && dialogStore && onPointerDown(e, editorRef, dialogStore) },
{ target: window, eventName: "pointerup", action: (e: PointerEvent) => editorRef && onPointerUp(e, editorRef) },
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => editorHandle && portfolioStore && onBeforeUnload(e, editorHandle, portfolioStore) },
{ target: window, eventName: "keyup", action: (e: KeyboardEvent) => editorHandle && dialogStore && onKeyUp(e, editorHandle, dialogStore) },
{ target: window, eventName: "keydown", action: (e: KeyboardEvent) => editorHandle && dialogStore && onKeyDown(e, editorHandle, dialogStore) },
{ target: window, eventName: "pointermove", action: (e: PointerEvent) => editorHandle && documentStore && onPointerMove(e, editorHandle, documentStore) },
{ target: window, eventName: "pointerdown", action: (e: PointerEvent) => editorHandle && dialogStore && onPointerDown(e, editorHandle, dialogStore) },
{ target: window, eventName: "pointerup", action: (e: PointerEvent) => editorHandle && onPointerUp(e, editorHandle) },
{ target: window, eventName: "mousedown", action: (e: MouseEvent) => onMouseDown(e) },
{ target: window, eventName: "mouseup", action: (e: MouseEvent) => editorRef && onPotentialDoubleClick(e, editorRef) },
{ target: window, eventName: "wheel", action: (e: WheelEvent) => editorRef && onWheelScroll(e, editorRef), options: { passive: false } },
{ target: window, eventName: "mouseup", action: (e: MouseEvent) => editorHandle && onPotentialDoubleClick(e, editorHandle) },
{ target: window, eventName: "wheel", action: (e: WheelEvent) => editorHandle && onWheelScroll(e, editorHandle), options: { passive: false } },
{ target: window, eventName: "modifyinputfield", action: (e: CustomEvent) => onModifyInputField(e) },
{ target: window, eventName: "focusout", action: () => onFocusOut() },
{ target: window.document, eventName: "contextmenu", action: (e: MouseEvent) => onContextMenu(e) },
{ target: window.document, eventName: "fullscreenchange", action: () => fullscreenModeChanged() },
{ target: window.document.body, eventName: "paste", action: (e: ClipboardEvent) => editorRef && onPaste(e, editorRef) },
{ target: window.document.body, eventName: "paste", action: (e: ClipboardEvent) => editorHandle && onPaste(e, editorHandle) },
{ target: window.document, eventName: "pointerlockchange", action: onPointerLockChange },
{ target: window.document, eventName: "pointerlockerror", action: onPointerLockChange },
];
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let editorHandle: EditorHandle | undefined = undefined;
let dialogStore: DialogStore | undefined = undefined;
let portfolioStore: PortfolioStore | undefined = undefined;
let documentStore: DocumentStore | undefined = undefined;
export function createInputManager(editor: Editor, dialog: DialogStore, portfolio: PortfolioStore, doc: DocumentStore) {
export function createInputManager(subscriptions: SubscriptionsRouter, editor: EditorHandle, dialog: DialogStore, portfolio: PortfolioStore, doc: DocumentStore) {
destroyInputManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editorHandle = editor;
dialogStore = dialog;
portfolioStore = portfolio;
documentStore = doc;
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardRead", () => {
subscriptions.subscribeFrontendMessage("TriggerClipboardRead", () => {
triggerClipboardRead(editor);
});
editor.subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => {
subscriptions.subscribeFrontendMessage("WindowPointerLockMove", (data) => {
// Desktop app only: dispatch custom pointer lock movement events
const event = new CustomEvent("pointerlockmove", { detail: { x: data.position[0], y: data.position[1] } });
window.dispatchEvent(event);
@@ -83,11 +86,11 @@ export function createInputManager(editor: Editor, dialog: DialogStore, portfoli
// Return the destructor
export function destroyInputManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardRead");
editor.subscriptions.unsubscribeFrontendMessage("WindowPointerLockMove");
subscriptions.unsubscribeFrontendMessage("TriggerClipboardRead");
subscriptions.unsubscribeFrontendMessage("WindowPointerLockMove");
// Remove event bindings after the lifetime of the application (or on hot-module replacement during development)
listeners.forEach(({ target, eventName, action, options }) => target.removeEventListener(eventName, action, options));
@@ -95,5 +98,6 @@ export function destroyInputManager() {
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef && dialogStore && portfolioStore && documentStore) newModule?.createInputManager(editorRef, dialogStore, portfolioStore, documentStore);
if (subscriptionsRouter && editorHandle && dialogStore && portfolioStore && documentStore)
newModule?.createInputManager(subscriptionsRouter, editorHandle, dialogStore, portfolioStore, documentStore);
});

View File

@@ -1,27 +1,30 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { localizeTimestamp } from "@graphite/utility-functions/time";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let editorHandle: EditorHandle | undefined = undefined;
export function createLocalizationManager(editor: Editor) {
export function createLocalizationManager(subscriptions: SubscriptionsRouter, editor: EditorHandle) {
destroyLocalizationManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editorHandle = editor;
editor.subscriptions.subscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate", (data) => {
subscriptions.subscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate", (data) => {
const localized = localizeTimestamp(data.commitDate);
editor.handle.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
editor.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
});
}
export function destroyLocalizationManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate");
subscriptions.unsubscribeFrontendMessage("TriggerAboutGraphiteLocalizedCommitDate");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef) newModule?.createLocalizationManager(editorRef);
if (subscriptionsRouter && editorHandle) newModule?.createLocalizationManager(subscriptionsRouter, editorHandle);
});

View File

@@ -1,14 +1,14 @@
import type { Editor } from "@graphite/editor";
import { createCrashDialog } from "@graphite/stores/dialog";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
export function createPanicManager(editor: Editor) {
export function createPanicManager(subscriptions: SubscriptionsRouter) {
destroyPanicManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => {
subscriptions.subscribeFrontendMessage("DisplayDialogPanic", (data) => {
// `Error.stackTraceLimit` is only available in V8/Chromium
const previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = Infinity;
@@ -25,13 +25,13 @@ export function createPanicManager(editor: Editor) {
}
export function destroyPanicManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("DisplayDialogPanic");
subscriptions.unsubscribeFrontendMessage("DisplayDialogPanic");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef) newModule?.createPanicManager(editorRef);
if (subscriptionsRouter) newModule?.createPanicManager(subscriptionsRouter);
});

View File

@@ -1,64 +1,67 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { PortfolioStore } from "@graphite/stores/portfolio";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { saveEditorPreferences, loadEditorPreferences, storeDocument, removeDocument, loadFirstDocument, loadRestDocuments, saveActiveDocument } from "@graphite/utility-functions/persistence";
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let editorHandle: EditorHandle | undefined = undefined;
let portfolioStore: PortfolioStore | undefined = undefined;
export function createPersistenceManager(editor: Editor, portfolio: PortfolioStore) {
export function createPersistenceManager(subscriptions: SubscriptionsRouter, editor: EditorHandle, portfolio: PortfolioStore) {
destroyPersistenceManager();
editorRef = editor;
subscriptionsRouter = subscriptions;
editorHandle = editor;
portfolioStore = portfolio;
editor.subscriptions.subscribeFrontendMessage("TriggerSavePreferences", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerSavePreferences", async (data) => {
await saveEditorPreferences(data.preferences);
});
editor.subscriptions.subscribeFrontendMessage("TriggerLoadPreferences", async () => {
subscriptions.subscribeFrontendMessage("TriggerLoadPreferences", async () => {
await loadEditorPreferences(editor);
});
editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerPersistenceWriteDocument", async (data) => {
await storeDocument(data, portfolio);
});
editor.subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerPersistenceRemoveDocument", async (data) => {
await removeDocument(String(data.documentId), portfolio);
});
editor.subscriptions.subscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument", async () => {
subscriptions.subscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument", async () => {
await loadFirstDocument(editor);
});
editor.subscriptions.subscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments", async () => {
subscriptions.subscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments", async () => {
await loadRestDocuments(editor);
});
editor.subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => {
subscriptions.subscribeFrontendMessage("TriggerOpenLaunchDocuments", async () => {
// TODO: Could be used to load documents from URL params or similar on launch
});
editor.subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerSaveActiveDocument", async (data) => {
await saveActiveDocument(data.documentId);
});
}
export function destroyPersistenceManager() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
editor.subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
editor.subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments");
editor.subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSaveActiveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerSavePreferences");
subscriptions.unsubscribeFrontendMessage("TriggerLoadPreferences");
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceWriteDocument");
subscriptions.unsubscribeFrontendMessage("TriggerPersistenceRemoveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerLoadFirstAutoSaveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerLoadRestAutoSaveDocuments");
subscriptions.unsubscribeFrontendMessage("TriggerOpenLaunchDocuments");
subscriptions.unsubscribeFrontendMessage("TriggerSaveActiveDocument");
}
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
if (editorRef && portfolioStore) newModule?.createPersistenceManager(editorRef, portfolioStore);
if (subscriptionsRouter && editorHandle && portfolioStore) newModule?.createPersistenceManager(subscriptionsRouter, editorHandle, portfolioStore);
});

View File

@@ -2,7 +2,7 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { AppWindowPlatform } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
export type AppWindowStore = ReturnType<typeof createAppWindowStore>;
@@ -21,47 +21,47 @@ const initialState: AppWindowStoreState = {
uiScale: 1,
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<AppWindowStoreState> = import.meta.hot?.data?.store || writable<AppWindowStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createAppWindowStore(editor: Editor) {
export function createAppWindowStore(subscriptions: SubscriptionsRouter) {
destroyAppWindowStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("UpdatePlatform", (data) => {
subscriptions.subscribeFrontendMessage("UpdatePlatform", (data) => {
update((state) => {
state.platform = data.platform;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateMaximized", (data) => {
subscriptions.subscribeFrontendMessage("UpdateMaximized", (data) => {
update((state) => {
state.maximized = data.maximized;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateFullscreen", (data) => {
subscriptions.subscribeFrontendMessage("UpdateFullscreen", (data) => {
update((state) => {
state.fullscreen = data.fullscreen;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateViewportHolePunch", (data) => {
subscriptions.subscribeFrontendMessage("UpdateViewportHolePunch", (data) => {
update((state) => {
state.viewportHolePunch = data.active;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateUIScale", (data) => {
subscriptions.subscribeFrontendMessage("UpdateUIScale", (data) => {
update((state) => {
state.uiScale = data.scale;
return state;
@@ -72,12 +72,12 @@ export function createAppWindowStore(editor: Editor) {
}
export function destroyAppWindowStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("UpdatePlatform");
editor.subscriptions.unsubscribeFrontendMessage("UpdateMaximized");
editor.subscriptions.unsubscribeFrontendMessage("UpdateFullscreen");
editor.subscriptions.unsubscribeFrontendMessage("UpdateViewportHolePunch");
editor.subscriptions.unsubscribeFrontendMessage("UpdateUIScale");
subscriptions.unsubscribeFrontendMessage("UpdatePlatform");
subscriptions.unsubscribeFrontendMessage("UpdateMaximized");
subscriptions.unsubscribeFrontendMessage("UpdateFullscreen");
subscriptions.unsubscribeFrontendMessage("UpdateViewportHolePunch");
subscriptions.unsubscribeFrontendMessage("UpdateUIScale");
}

View File

@@ -2,9 +2,9 @@ import { tick } from "svelte";
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { IconName } from "@graphite/icons";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
export type DialogStore = ReturnType<typeof createDialogStore>;
@@ -29,19 +29,19 @@ const initialState: DialogStoreState = {
panicDetails: "",
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<DialogStoreState> = import.meta.hot?.data?.store || writable<DialogStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createDialogStore(editor: Editor) {
export function createDialogStore(subscriptions: SubscriptionsRouter, editor: EditorHandle) {
destroyDialogStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("DisplayDialog", (data) => {
subscriptions.subscribeFrontendMessage("DisplayDialog", (data) => {
update((state) => {
state.visible = true;
@@ -52,7 +52,7 @@ export function createDialogStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("DialogButtons", async (data) => {
subscriptions.subscribeLayoutUpdate("DialogButtons", async (data) => {
await tick();
update((state) => {
@@ -62,7 +62,7 @@ export function createDialogStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("DialogColumn1", async (data) => {
subscriptions.subscribeLayoutUpdate("DialogColumn1", async (data) => {
await tick();
update((state) => {
@@ -72,7 +72,7 @@ export function createDialogStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("DialogColumn2", async (data) => {
subscriptions.subscribeLayoutUpdate("DialogColumn2", async (data) => {
await tick();
update((state) => {
@@ -82,7 +82,7 @@ export function createDialogStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("DialogClose", () => {
subscriptions.subscribeFrontendMessage("DialogClose", () => {
update((state) => {
// Disallow dismissing the crash dialog since it should remain as the final notification
if (state.panicDetails === "") state.visible = false;
@@ -91,7 +91,7 @@ export function createDialogStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("TriggerDisplayThirdPartyLicensesDialog", async () => {
subscriptions.subscribeFrontendMessage("TriggerDisplayThirdPartyLicensesDialog", async () => {
const BACKUP_URL = "https://editor.graphite.art/third-party-licenses.txt";
let licenseText = `Content was not able to load. Please check your network connection and try again.\n\nOr visit ${BACKUP_URL} for the license notices.`;
@@ -102,22 +102,22 @@ export function createDialogStore(editor: Editor) {
// Do nothing on network error
}
editor.handle.requestLicensesThirdPartyDialogWithLicenseText(licenseText);
editor.requestLicensesThirdPartyDialogWithLicenseText(licenseText);
});
return { subscribe };
}
export function destroyDialogStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("DisplayDialog");
editor.subscriptions.unsubscribeFrontendMessage("DialogClose");
editor.subscriptions.unsubscribeFrontendMessage("TriggerDisplayThirdPartyLicensesDialog");
editor.subscriptions.unsubscribeLayoutUpdate("DialogButtons");
editor.subscriptions.unsubscribeLayoutUpdate("DialogColumn1");
editor.subscriptions.unsubscribeLayoutUpdate("DialogColumn2");
subscriptions.unsubscribeFrontendMessage("DisplayDialog");
subscriptions.unsubscribeFrontendMessage("DialogClose");
subscriptions.unsubscribeFrontendMessage("TriggerDisplayThirdPartyLicensesDialog");
subscriptions.unsubscribeLayoutUpdate("DialogButtons");
subscriptions.unsubscribeLayoutUpdate("DialogColumn1");
subscriptions.unsubscribeLayoutUpdate("DialogColumn2");
}
// Creates a crash dialog from JS once the editor has panicked.

View File

@@ -3,7 +3,7 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { patchLayout } from "@graphite/utility-functions/widgets";
export type DocumentStore = ReturnType<typeof createDocumentStore>;
@@ -27,26 +27,26 @@ const initialState: DocumentStoreState = {
fadeArtwork: 100,
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<DocumentStoreState> = import.meta.hot?.data?.store || writable<DocumentStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createDocumentStore(editor: Editor) {
export function createDocumentStore(subscriptions: SubscriptionsRouter) {
destroyDocumentStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("UpdateGraphFadeArtwork", (data) => {
subscriptions.subscribeFrontendMessage("UpdateGraphFadeArtwork", (data) => {
update((state) => {
state.fadeArtwork = data.percentage;
return state;
});
});
editor.subscriptions.subscribeLayoutUpdate("ToolOptions", async (data) => {
subscriptions.subscribeLayoutUpdate("ToolOptions", async (data) => {
await tick();
update((state) => {
@@ -55,7 +55,7 @@ export function createDocumentStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("DocumentBar", async (data) => {
subscriptions.subscribeLayoutUpdate("DocumentBar", async (data) => {
await tick();
update((state) => {
@@ -64,7 +64,7 @@ export function createDocumentStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("ToolShelf", async (data) => {
subscriptions.subscribeLayoutUpdate("ToolShelf", async (data) => {
await tick();
update((state) => {
@@ -73,7 +73,7 @@ export function createDocumentStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("WorkingColors", async (data) => {
subscriptions.subscribeLayoutUpdate("WorkingColors", async (data) => {
await tick();
update((state) => {
@@ -82,7 +82,7 @@ export function createDocumentStore(editor: Editor) {
});
});
editor.subscriptions.subscribeLayoutUpdate("NodeGraphControlBar", async (data) => {
subscriptions.subscribeLayoutUpdate("NodeGraphControlBar", async (data) => {
await tick();
update((state) => {
@@ -91,7 +91,7 @@ export function createDocumentStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateGraphViewOverlay", (data) => {
subscriptions.subscribeFrontendMessage("UpdateGraphViewOverlay", (data) => {
update((state) => {
state.graphViewOverlayOpen = data.open;
return state;
@@ -102,14 +102,14 @@ export function createDocumentStore(editor: Editor) {
}
export function destroyDocumentStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("UpdateGraphFadeArtwork");
editor.subscriptions.unsubscribeFrontendMessage("UpdateGraphViewOverlay");
editor.subscriptions.unsubscribeLayoutUpdate("ToolOptions");
editor.subscriptions.unsubscribeLayoutUpdate("DocumentBar");
editor.subscriptions.unsubscribeLayoutUpdate("ToolShelf");
editor.subscriptions.unsubscribeLayoutUpdate("WorkingColors");
editor.subscriptions.unsubscribeLayoutUpdate("NodeGraphControlBar");
subscriptions.unsubscribeFrontendMessage("UpdateGraphFadeArtwork");
subscriptions.unsubscribeFrontendMessage("UpdateGraphViewOverlay");
subscriptions.unsubscribeLayoutUpdate("ToolOptions");
subscriptions.unsubscribeLayoutUpdate("DocumentBar");
subscriptions.unsubscribeLayoutUpdate("ToolShelf");
subscriptions.unsubscribeLayoutUpdate("WorkingColors");
subscriptions.unsubscribeLayoutUpdate("NodeGraphControlBar");
}

View File

@@ -1,7 +1,7 @@
import { get, writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
export type FullscreenStore = ReturnType<typeof createFullscreenStore>;
@@ -14,19 +14,19 @@ const initialState: FullscreenStoreState = {
keyboardLocked: false,
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<FullscreenStoreState> = import.meta.hot?.data?.store || writable<FullscreenStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createFullscreenStore(editor: Editor) {
export function createFullscreenStore(subscriptions: SubscriptionsRouter) {
destroyFullscreenStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("WindowFullscreen", () => {
subscriptions.subscribeFrontendMessage("WindowFullscreen", () => {
toggleFullscreen();
});
@@ -34,10 +34,10 @@ export function createFullscreenStore(editor: Editor) {
}
export function destroyFullscreenStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("WindowFullscreen");
subscriptions.unsubscribeFrontendMessage("WindowFullscreen");
}
export function fullscreenModeChanged() {

View File

@@ -2,8 +2,8 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { NodeGraphErrorDiagnostic, BoxSelection, FrontendClickTargets, ContextMenuInformation, FrontendNode, FrontendNodeType, WirePath } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { MessageBody } from "@graphite/subscription-router";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import type { MessageBody } from "/src/subscriptions-router";
export type NodeGraphStore = ReturnType<typeof createNodeGraphStore>;
@@ -53,19 +53,19 @@ const initialState: NodeGraphStoreState = {
reorderExportIndex: undefined,
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<NodeGraphStoreState> = import.meta.hot?.data?.store || writable<NodeGraphStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createNodeGraphStore(editor: Editor) {
export function createNodeGraphStore(subscriptions: SubscriptionsRouter) {
destroyNodeGraphStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("SendUIMetadata", (data) => {
subscriptions.subscribeFrontendMessage("SendUIMetadata", (data) => {
update((state) => {
state.nodeDescriptions = new Map(data.nodeDescriptions);
state.nodeTypes = data.nodeTypes;
@@ -73,56 +73,56 @@ export function createNodeGraphStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateBox", (data) => {
subscriptions.subscribeFrontendMessage("UpdateBox", (data) => {
update((state) => {
state.box = data.box;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateClickTargets", (data) => {
subscriptions.subscribeFrontendMessage("UpdateClickTargets", (data) => {
update((state) => {
state.clickTargets = data.clickTargets;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateContextMenuInformation", (data) => {
subscriptions.subscribeFrontendMessage("UpdateContextMenuInformation", (data) => {
update((state) => {
state.contextMenuInformation = data.contextMenuInformation;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateImportReorderIndex", (data) => {
subscriptions.subscribeFrontendMessage("UpdateImportReorderIndex", (data) => {
update((state) => {
state.reorderImportIndex = data.importIndex;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateExportReorderIndex", (data) => {
subscriptions.subscribeFrontendMessage("UpdateExportReorderIndex", (data) => {
update((state) => {
state.reorderExportIndex = data.exportIndex;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateImportsExports", (data) => {
subscriptions.subscribeFrontendMessage("UpdateImportsExports", (data) => {
update((state) => {
state.updateImportsExports = data;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateInSelectedNetwork", (data) => {
subscriptions.subscribeFrontendMessage("UpdateInSelectedNetwork", (data) => {
update((state) => {
state.inSelectedNetwork = data.inSelectedNetwork;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateLayerWidths", (data) => {
subscriptions.subscribeFrontendMessage("UpdateLayerWidths", (data) => {
update((state) => {
state.layerWidths = data.layerWidths;
state.chainWidths = data.chainWidths;
@@ -131,7 +131,7 @@ export function createNodeGraphStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphNodes", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeGraphNodes", (data) => {
update((state) => {
state.nodes.clear();
data.nodes.forEach((node) => {
@@ -141,21 +141,21 @@ export function createNodeGraphStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic", (data) => {
update((state) => {
state.error = data.error;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateVisibleNodes", (data) => {
subscriptions.subscribeFrontendMessage("UpdateVisibleNodes", (data) => {
update((state) => {
state.visibleNodes = new Set<bigint>(data.nodes);
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphWires", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeGraphWires", (data) => {
update((state) => {
data.wires.forEach((wireUpdate) => {
let inputMap = state.wires.get(wireUpdate.id);
@@ -174,35 +174,35 @@ export function createNodeGraphStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("ClearAllNodeGraphWires", () => {
subscriptions.subscribeFrontendMessage("ClearAllNodeGraphWires", () => {
update((state) => {
state.wires.clear();
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphSelection", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeGraphSelection", (data) => {
update((state) => {
state.selected = data.selected;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeGraphTransform", (data) => {
update((state) => {
state.transform = { scale: data.scale, x: data.translation[0], y: data.translation[1] };
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateNodeThumbnail", (data) => {
subscriptions.subscribeFrontendMessage("UpdateNodeThumbnail", (data) => {
update((state) => {
state.thumbnails.set(data.id, data.value);
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateWirePathInProgress", (data) => {
subscriptions.subscribeFrontendMessage("UpdateWirePathInProgress", (data) => {
update((state) => {
state.wirePathInProgress = data.wirePath;
return state;
@@ -213,27 +213,27 @@ export function createNodeGraphStore(editor: Editor) {
}
export function destroyNodeGraphStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("SendUIMetadata");
editor.subscriptions.unsubscribeFrontendMessage("UpdateBox");
editor.subscriptions.unsubscribeFrontendMessage("UpdateClickTargets");
editor.subscriptions.unsubscribeFrontendMessage("UpdateContextMenuInformation");
editor.subscriptions.unsubscribeFrontendMessage("UpdateImportReorderIndex");
editor.subscriptions.unsubscribeFrontendMessage("UpdateExportReorderIndex");
editor.subscriptions.unsubscribeFrontendMessage("UpdateImportsExports");
editor.subscriptions.unsubscribeFrontendMessage("UpdateInSelectedNetwork");
editor.subscriptions.unsubscribeFrontendMessage("UpdateLayerWidths");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphNodes");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic");
editor.subscriptions.unsubscribeFrontendMessage("UpdateVisibleNodes");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphWires");
editor.subscriptions.unsubscribeFrontendMessage("ClearAllNodeGraphWires");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphSelection");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphTransform");
editor.subscriptions.unsubscribeFrontendMessage("UpdateNodeThumbnail");
editor.subscriptions.unsubscribeFrontendMessage("UpdateWirePathInProgress");
subscriptions.unsubscribeFrontendMessage("SendUIMetadata");
subscriptions.unsubscribeFrontendMessage("UpdateBox");
subscriptions.unsubscribeFrontendMessage("UpdateClickTargets");
subscriptions.unsubscribeFrontendMessage("UpdateContextMenuInformation");
subscriptions.unsubscribeFrontendMessage("UpdateImportReorderIndex");
subscriptions.unsubscribeFrontendMessage("UpdateExportReorderIndex");
subscriptions.unsubscribeFrontendMessage("UpdateImportsExports");
subscriptions.unsubscribeFrontendMessage("UpdateInSelectedNetwork");
subscriptions.unsubscribeFrontendMessage("UpdateLayerWidths");
subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphNodes");
subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphErrorDiagnostic");
subscriptions.unsubscribeFrontendMessage("UpdateVisibleNodes");
subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphWires");
subscriptions.unsubscribeFrontendMessage("ClearAllNodeGraphWires");
subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphSelection");
subscriptions.unsubscribeFrontendMessage("UpdateNodeGraphTransform");
subscriptions.unsubscribeFrontendMessage("UpdateNodeThumbnail");
subscriptions.unsubscribeFrontendMessage("UpdateWirePathInProgress");
}
export function closeContextMenu() {

View File

@@ -1,8 +1,8 @@
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle, OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { downloadFile, downloadFileBlob, upload } from "@graphite/utility-functions/files";
import { rasterizeSVG } from "@graphite/utility-functions/rasterization";
@@ -25,26 +25,26 @@ const initialState: PortfolioStoreState = {
layersPanelOpen: true,
};
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
const store: Writable<PortfolioStoreState> = import.meta.hot?.data?.store || writable<PortfolioStoreState>(initialState);
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createPortfolioStore(editor: Editor) {
export function createPortfolioStore(subscriptions: SubscriptionsRouter, editor: EditorHandle) {
destroyPortfolioStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("UpdateOpenDocumentsList", (data) => {
subscriptions.subscribeFrontendMessage("UpdateOpenDocumentsList", (data) => {
update((state) => {
state.documents = data.openDocuments;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {
subscriptions.subscribeFrontendMessage("UpdateActiveDocument", (data) => {
update((state) => {
// Assume we receive a correct document id
const activeId = state.documents.findIndex((doc) => doc.id === data.documentId);
@@ -53,39 +53,39 @@ export function createPortfolioStore(editor: Editor) {
});
});
editor.subscriptions.subscribeFrontendMessage("TriggerFetchAndOpenDocument", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerFetchAndOpenDocument", async (data) => {
try {
const url = new URL(`demo-artwork/${data.filename}`, document.location.href);
const response = await fetch(url);
editor.handle.openFile(data.filename, await response.bytes());
editor.openFile(data.filename, await response.bytes());
} 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.handle.errorDialog("Failed to open document", "The file could not be reached over the internet. You may be offline, or it may be missing.");
editor.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.subscribeFrontendMessage("TriggerOpen", async () => {
const data = await upload(`image/*,.${editor.handle.fileExtension()}`, "data");
editor.handle.openFile(data.filename, data.content);
subscriptions.subscribeFrontendMessage("TriggerOpen", async () => {
const data = await upload(`image/*,.${editor.fileExtension()}`, "data");
editor.openFile(data.filename, data.content);
});
editor.subscriptions.subscribeFrontendMessage("TriggerImport", async () => {
subscriptions.subscribeFrontendMessage("TriggerImport", async () => {
// TODO: Use the same `accept` string as in the `TriggerOpen` handler once importing Graphite documents as nodes is supported
const data = await upload("image/*", "data");
editor.handle.importFile(data.filename, data.content);
editor.importFile(data.filename, data.content);
});
editor.subscriptions.subscribeFrontendMessage("TriggerSaveDocument", (data) => {
subscriptions.subscribeFrontendMessage("TriggerSaveDocument", (data) => {
downloadFile(data.name, data.content);
});
editor.subscriptions.subscribeFrontendMessage("TriggerSaveFile", (data) => {
subscriptions.subscribeFrontendMessage("TriggerSaveFile", (data) => {
downloadFile(data.name, data.content);
});
editor.subscriptions.subscribeFrontendMessage("TriggerExportImage", async (data) => {
subscriptions.subscribeFrontendMessage("TriggerExportImage", async (data) => {
const { svg, name, mime, size } = data;
// Fill the canvas with white if it'll be a JPEG (which does not support transparency and defaults to black)
@@ -102,21 +102,21 @@ export function createPortfolioStore(editor: Editor) {
}
});
editor.subscriptions.subscribeFrontendMessage("UpdateDataPanelState", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateDataPanelState", async (data) => {
update((state) => {
state.dataPanelOpen = data.open;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdatePropertiesPanelState", async (data) => {
subscriptions.subscribeFrontendMessage("UpdatePropertiesPanelState", async (data) => {
update((state) => {
state.propertiesPanelOpen = data.open;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("UpdateLayersPanelState", async (data) => {
subscriptions.subscribeFrontendMessage("UpdateLayersPanelState", async (data) => {
update((state) => {
state.layersPanelOpen = data.open;
return state;
@@ -127,18 +127,18 @@ export function createPortfolioStore(editor: Editor) {
}
export function destroyPortfolioStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
editor.subscriptions.unsubscribeFrontendMessage("UpdateOpenDocumentsList");
editor.subscriptions.unsubscribeFrontendMessage("UpdateActiveDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerFetchAndOpenDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerOpen");
editor.subscriptions.unsubscribeFrontendMessage("TriggerImport");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSaveDocument");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSaveFile");
editor.subscriptions.unsubscribeFrontendMessage("TriggerExportImage");
editor.subscriptions.unsubscribeFrontendMessage("UpdateDataPanelState");
editor.subscriptions.unsubscribeFrontendMessage("UpdatePropertiesPanelState");
editor.subscriptions.unsubscribeFrontendMessage("UpdateLayersPanelState");
subscriptions.unsubscribeFrontendMessage("UpdateOpenDocumentsList");
subscriptions.unsubscribeFrontendMessage("UpdateActiveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerFetchAndOpenDocument");
subscriptions.unsubscribeFrontendMessage("TriggerOpen");
subscriptions.unsubscribeFrontendMessage("TriggerImport");
subscriptions.unsubscribeFrontendMessage("TriggerSaveDocument");
subscriptions.unsubscribeFrontendMessage("TriggerSaveFile");
subscriptions.unsubscribeFrontendMessage("TriggerExportImage");
subscriptions.unsubscribeFrontendMessage("UpdateDataPanelState");
subscriptions.unsubscribeFrontendMessage("UpdatePropertiesPanelState");
subscriptions.unsubscribeFrontendMessage("UpdateLayersPanelState");
}

View File

@@ -2,7 +2,7 @@ import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import type { ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { SubscriptionsRouter } from "/src/subscriptions-router";
import { operatingSystem } from "@graphite/utility-functions/platform";
export type TooltipStore = ReturnType<typeof createTooltipStore>;
@@ -36,7 +36,7 @@ const tooltipEventListeners: Listener[] = [
{ eventName: "wheel", action: closeTooltip },
];
let editorRef: Editor | undefined = undefined;
let subscriptionsRouter: SubscriptionsRouter | undefined = undefined;
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
// Store state persisted across HMR to maintain reactive subscriptions in the component tree
@@ -44,26 +44,26 @@ const store: Writable<TooltipStoreState> = import.meta.hot?.data?.store || writa
if (import.meta.hot) import.meta.hot.data.store = store;
const { subscribe, update } = store;
export function createTooltipStore(editor: Editor) {
export function createTooltipStore(subscriptions: SubscriptionsRouter) {
destroyTooltipStore();
editorRef = editor;
subscriptionsRouter = subscriptions;
editor.subscriptions.subscribeFrontendMessage("SendShortcutShiftClick", async (data) => {
subscriptions.subscribeFrontendMessage("SendShortcutShiftClick", async (data) => {
update((state) => {
state.shiftClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("SendShortcutAltClick", async (data) => {
subscriptions.subscribeFrontendMessage("SendShortcutAltClick", async (data) => {
update((state) => {
state.altClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeFrontendMessage("SendShortcutFullscreen", async (data) => {
subscriptions.subscribeFrontendMessage("SendShortcutFullscreen", async (data) => {
update((state) => {
state.fullscreenShortcut = operatingSystem() === "Mac" ? data.shortcutMac : data.shortcut;
return state;
@@ -76,14 +76,14 @@ export function createTooltipStore(editor: Editor) {
}
export function destroyTooltipStore() {
const editor = editorRef;
if (!editor) return;
const subscriptions = subscriptionsRouter;
if (!subscriptions) return;
if (tooltipTimeout) clearTimeout(tooltipTimeout);
editor.subscriptions.unsubscribeFrontendMessage("SendShortcutShiftClick");
editor.subscriptions.unsubscribeFrontendMessage("SendShortcutAltClick");
editor.subscriptions.unsubscribeFrontendMessage("SendShortcutFullscreen");
subscriptions.unsubscribeFrontendMessage("SendShortcutShiftClick");
subscriptions.unsubscribeFrontendMessage("SendShortcutAltClick");
subscriptions.unsubscribeFrontendMessage("SendShortcutFullscreen");
tooltipEventListeners.forEach(({ eventName, action }) => document.removeEventListener(eventName, action));
}

View File

@@ -9,7 +9,7 @@ export type MessageMap = ToMessageMap<FrontendMessage>;
export type MessageName = keyof MessageMap;
export type MessageBody<T extends MessageName> = Extract<FrontendMessage, Record<T, unknown>>[T];
export function createSubscriptionRouter() {
export function createSubscriptionsRouter() {
// Callbacks are wrapped at subscription time to capture their type-specific data extraction in a closure,
// so the stored function has a uniform signature and the map doesn't need per-key generic value types.
const subscriptions = new Map<MessageName, (taggedMessage: MessageMap) => void>();
@@ -99,4 +99,4 @@ export function createSubscriptionRouter() {
handleFrontendMessage,
};
}
export type SubscriptionRouter = ReturnType<typeof createSubscriptionRouter>;
export type SubscriptionsRouter = ReturnType<typeof createSubscriptionsRouter>;

View File

@@ -1,4 +1,4 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
import { stripIndents } from "@graphite/utility-functions/strip-indents";
@@ -83,7 +83,7 @@ export function insertAtCaret(text: string) {
element.dispatchEvent(new Event("input", { bubbles: true }));
}
export async function triggerClipboardRead(editor: Editor) {
export async function triggerClipboardRead(editor: EditorHandle) {
// In the try block, attempt to read from the Clipboard API, which may not have permission and may not be supported in all browsers
// In the catch block, explain to the user why the paste failed and how to fix or work around the problem
try {
@@ -105,7 +105,7 @@ export async function triggerClipboardRead(editor: Editor) {
const blob = await item.getType("text/plain");
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") editor.handle.pasteText(reader.result);
if (typeof reader.result === "string") editor.pasteText(reader.result);
};
reader.readAsText(blob);
return true;
@@ -119,7 +119,7 @@ export async function triggerClipboardRead(editor: Editor) {
const blob = await item.getType("text/plain");
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") editor.handle.pasteSvg(undefined, reader.result);
if (typeof reader.result === "string") editor.pasteSvg(undefined, reader.result);
};
reader.readAsText(blob);
return true;
@@ -132,7 +132,7 @@ export async function triggerClipboardRead(editor: Editor) {
reader.onload = async () => {
if (reader.result instanceof ArrayBuffer) {
const imageData = await extractPixelData(new Blob([reader.result], { type: imageType }));
editor.handle.pasteImage(undefined, new Uint8Array(imageData.data), imageData.width, imageData.height);
editor.pasteImage(undefined, new Uint8Array(imageData.data), imageData.width, imageData.height);
}
};
reader.readAsArrayBuffer(blob);
@@ -170,6 +170,6 @@ export async function triggerClipboardRead(editor: Editor) {
};
const message = Object.entries(matchMessage).find(([key]) => String(err).includes(key))?.[1] || String(err);
editor.handle.errorDialog("Cannot access clipboard", message);
editor.errorDialog("Cannot access clipboard", message);
}
}

View File

@@ -1,4 +1,4 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
export function downloadFileURL(filename: string, url: string) {
@@ -66,18 +66,18 @@ export async function upload(accept: string, textOrData: "text" | "data" | "both
}
export type UploadResult<T> = { filename: string; type: string; content: T };
export async function pasteFile(item: DataTransferItem, editor: Editor, mouse?: [number, number], insertParentId?: bigint, insertIndex?: number) {
export async function pasteFile(item: DataTransferItem, editor: EditorHandle, mouse?: [number, number], insertParentId?: bigint, insertIndex?: number) {
const file = item.getAsFile();
if (!file) return;
if (file.type.startsWith("image/svg")) {
const svg = await file.text();
editor.handle.pasteSvg(file.name, svg, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
editor.pasteSvg(file.name, svg, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
} else if (file.type.startsWith("image/")) {
const imageData = await extractPixelData(file);
editor.handle.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
} else if (file.name.endsWith("." + editor.handle.fileExtension())) {
editor.pasteImage(file.name, new Uint8Array(imageData.data), imageData.width, imageData.height, mouse?.[0], mouse?.[1], insertParentId, insertIndex);
} else if (file.name.endsWith("." + editor.fileExtension())) {
// TODO: When we eventually have sub-documents, this should be changed to import the document as a node instead of opening it in a separate tab
editor.handle.openFile(file.name, await file.bytes());
editor.openFile(file.name, await file.bytes());
}
}

View File

@@ -1,7 +1,7 @@
import { get } from "svelte/store";
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { DialogStore } from "@graphite/stores/dialog";
import type { DocumentStore } from "@graphite/stores/document";
import { toggleFullscreen } from "@graphite/stores/fullscreen";
@@ -79,7 +79,7 @@ export async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent, dia
return true;
}
export async function onKeyDown(e: KeyboardEvent, editor: Editor, dialogStore: DialogStore) {
export async function onKeyDown(e: KeyboardEvent, editor: EditorHandle, dialogStore: DialogStore) {
const key = await getLocalizedScanCode(e);
const NO_KEY_REPEAT_MODIFIER_KEYS = ["ControlLeft", "ControlRight", "ShiftLeft", "ShiftRight", "MetaLeft", "MetaRight", "AltLeft", "AltRight", "AltGraph", "CapsLock", "Fn", "FnLock"];
@@ -88,29 +88,29 @@ export async function onKeyDown(e: KeyboardEvent, editor: Editor, dialogStore: D
if (await shouldRedirectKeyboardEventToBackend(e, dialogStore)) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onKeyDown(key, modifiers, e.repeat);
editor.onKeyDown(key, modifiers, e.repeat);
return;
}
if (get(dialogStore).visible && key === "Escape") {
editor.handle.onDialogDismiss();
editor.onDialogDismiss();
}
}
export async function onKeyUp(e: KeyboardEvent, editor: Editor, dialogStore: DialogStore) {
export async function onKeyUp(e: KeyboardEvent, editor: EditorHandle, dialogStore: DialogStore) {
const key = await getLocalizedScanCode(e);
if (await shouldRedirectKeyboardEventToBackend(e, dialogStore)) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onKeyUp(key, modifiers, e.repeat);
editor.onKeyUp(key, modifiers, e.repeat);
}
}
// Pointer events
// While any pointer button is already down, additional button down events are not reported, but they are sent as `pointermove` events and these are handled in the backend
export function onPointerMove(e: PointerEvent, editor: Editor, documentStore: DocumentStore) {
export function onPointerMove(e: PointerEvent, editor: EditorHandle, documentStore: DocumentStore) {
potentiallyRestoreCanvasFocus(e);
if (!e.buttons) viewportPointerInteractionOngoing = false;
@@ -124,11 +124,11 @@ export function onPointerMove(e: PointerEvent, editor: Editor, documentStore: Do
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
const modifiers = makeKeyboardModifiersBitfield(e);
if (detectShake(e)) editor.handle.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers);
editor.handle.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
if (detectShake(e)) editor.onMouseShake(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseMove(e.clientX, e.clientY, e.buttons, modifiers);
}
export function onPointerDown(e: PointerEvent, editor: Editor, dialogStore: DialogStore) {
export function onPointerDown(e: PointerEvent, editor: EditorHandle, dialogStore: DialogStore) {
potentiallyRestoreCanvasFocus(e);
const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]");
@@ -138,7 +138,7 @@ export function onPointerDown(e: PointerEvent, editor: Editor, dialogStore: Dial
const inTextInput = e.target === textToolInteractiveInputElement;
if (get(dialogStore).visible && !inDialog) {
editor.handle.onDialogDismiss();
editor.onDialogDismiss();
e.preventDefault();
e.stopPropagation();
}
@@ -146,7 +146,7 @@ export function onPointerDown(e: PointerEvent, editor: Editor, dialogStore: Dial
if (!inTextInput && !inContextMenu) {
if (textToolInteractiveInputElement) {
const isLeftOrRightClick = e.button === BUTTON_RIGHT || e.button === BUTTON_LEFT;
editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
editor.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText), isLeftOrRightClick);
} else {
viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
}
@@ -154,11 +154,11 @@ export function onPointerDown(e: PointerEvent, editor: Editor, dialogStore: Dial
if (viewportPointerInteractionOngoing && isTargetingCanvas instanceof Element) {
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseDown(e.clientX, e.clientY, e.buttons, modifiers);
}
}
export function onPointerUp(e: PointerEvent, editor: Editor) {
export function onPointerUp(e: PointerEvent, editor: EditorHandle) {
potentiallyRestoreCanvasFocus(e);
// Don't let the browser navigate back or forward when using the buttons on some mice
@@ -172,12 +172,12 @@ export function onPointerUp(e: PointerEvent, editor: Editor) {
if (textToolInteractiveInputElement) return;
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
editor.onMouseUp(e.clientX, e.clientY, e.buttons, modifiers);
}
// Mouse events
export function onPotentialDoubleClick(e: MouseEvent, editor: Editor) {
export function onPotentialDoubleClick(e: MouseEvent, editor: EditorHandle) {
if (textToolInteractiveInputElement || inPointerLock) return;
// Allow only events within the viewport or node graph boundaries
@@ -196,7 +196,7 @@ export function onPotentialDoubleClick(e: MouseEvent, editor: Editor) {
if (e.button === BUTTON_FORWARD) buttons = 16; // Forward
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
editor.onDoubleClick(e.clientX, e.clientY, buttons, modifiers);
}
export function onMouseDown(e: MouseEvent) {
@@ -216,7 +216,7 @@ export function onPointerLockChange() {
// Wheel events
export function onWheelScroll(e: WheelEvent, editor: Editor) {
export function onWheelScroll(e: WheelEvent, editor: EditorHandle) {
const isTargetingCanvas = e.target instanceof Element && e.target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
// Prevent zooming the entire page when using Ctrl + scroll wheel outside of the viewport
@@ -235,7 +235,7 @@ export function onWheelScroll(e: WheelEvent, editor: Editor) {
if (isTargetingCanvas) {
e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e);
editor.handle.onWheelScroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
editor.onWheelScroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
}
}
@@ -247,15 +247,15 @@ export function onModifyInputField(e: CustomEvent) {
// Window events
export async function onBeforeUnload(e: BeforeUnloadEvent, editor: Editor, portfolioStore: PortfolioStore) {
export async function onBeforeUnload(e: BeforeUnloadEvent, editor: EditorHandle, portfolioStore: PortfolioStore) {
const activeDocument = get(portfolioStore).documents[get(portfolioStore).activeDocumentIndex];
if (activeDocument && !activeDocument.details.isAutoSaved) editor.handle.triggerAutoSave(activeDocument.id);
if (activeDocument && !activeDocument.details.isAutoSaved) editor.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost
if (await editor.handle.hasCrashed()) return;
if (await editor.hasCrashed()) return;
// Skip the message during development, since it's annoying when testing
if (await editor.handle.inDevelopmentMode()) return;
if (await editor.inDevelopmentMode()) return;
const allDocumentsSaved = get(portfolioStore).documents.reduce((acc, doc) => acc && doc.details.isSaved, true);
if (!allDocumentsSaved) {
@@ -264,13 +264,13 @@ export async function onBeforeUnload(e: BeforeUnloadEvent, editor: Editor, portf
}
}
export function onPaste(e: ClipboardEvent, editor: Editor) {
export function onPaste(e: ClipboardEvent, editor: EditorHandle) {
const dataTransfer = e.clipboardData;
if (!dataTransfer || targetIsTextField(e.target || undefined)) return;
e.preventDefault();
Array.from(dataTransfer.items).forEach(async (item) => {
if (item.type === "text/plain") item.getAsString((text) => editor.handle.pasteText(text));
if (item.type === "text/plain") item.getAsString((text) => editor.pasteText(text));
await pasteFile(item, editor);
});
}

View File

@@ -1,3 +1,5 @@
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
export type RequestResult = { body: string; status: number };
// Special implementation using the legacy XMLHttpRequest API that provides callbacks to get:
@@ -31,3 +33,23 @@ export function requestWithUploadDownloadProgress(
return [promise, xhrValue];
}
// If the URL hash fragment contains a demo artwork path (e.g. #demo/isometric-light), fetch and open it
export async function loadDemoArtwork(editor: EditorHandle) {
const demoArtwork = window.location.hash.trim().match(/#demo\/(.*)/)?.[1];
if (!demoArtwork) return;
try {
const url = new URL(`/demo-artwork/${demoArtwork}.${editor.fileExtension()}`, document.location.href);
const response = await fetch(url);
if (!response.ok) throw new Error();
const filename = url.pathname.split("/").pop() || `Untitled.${editor.fileExtension()}`;
const content = await response.bytes();
editor.openFile(filename, content);
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
} catch {
// Do nothing
}
}

View File

@@ -1,9 +1,9 @@
import * as idb from "idb-keyval";
import { get } from "svelte/store";
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
import type { PortfolioStore } from "@graphite/stores/portfolio";
import type { MessageBody } from "@graphite/subscription-router";
import type { MessageBody } from "/src/subscriptions-router";
export async function storeCurrentDocumentId(documentId: string) {
const indexedDbStorage = idb.createStore("graphite", "store");
@@ -65,7 +65,7 @@ export async function removeDocument(id: string, portfolio: PortfolioStore) {
}
}
export async function loadFirstDocument(editor: Editor) {
export async function loadFirstDocument(editor: EditorHandle) {
const indexedDbStorage = idb.createStore("graphite", "store");
const previouslySavedDocuments = await idb.get<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>("documents", indexedDbStorage);
@@ -87,19 +87,19 @@ export async function loadFirstDocument(editor: Editor) {
if (currentDocumentId !== undefined && String(currentDocumentId) in previouslySavedDocuments) {
const doc = previouslySavedDocuments[String(currentDocumentId)];
editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.handle.selectDocument(currentDocumentId);
editor.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.selectDocument(currentDocumentId);
} else {
const len = orderedSavedDocuments.length;
if (len > 0) {
const doc = orderedSavedDocuments[len - 1];
editor.handle.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.handle.selectDocument(doc.documentId);
editor.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false);
editor.selectDocument(doc.documentId);
}
}
}
export async function loadRestDocuments(editor: Editor) {
export async function loadRestDocuments(editor: EditorHandle) {
const indexedDbStorage = idb.createStore("graphite", "store");
const previouslySavedDocuments = await idb.get<Record<string, MessageBody<"TriggerPersistenceWriteDocument">>>("documents", indexedDbStorage);
@@ -126,15 +126,15 @@ export async function loadRestDocuments(editor: Editor) {
for (let i = currentIndex - 1; i >= 0; i--) {
const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details;
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, true);
editor.openAutoSavedDocument(documentId, name, isSaved, document, true);
}
for (let i = currentIndex + 1; i < orderedSavedDocuments.length; i++) {
const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details;
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, false);
editor.openAutoSavedDocument(documentId, name, isSaved, document, false);
}
editor.handle.selectDocument(currentDocumentId);
editor.selectDocument(currentDocumentId);
}
// No valid current document: open all remaining documents and select the last one
else {
@@ -143,10 +143,10 @@ export async function loadRestDocuments(editor: Editor) {
for (let i = length - 2; i >= 0; i--) {
const { documentId, document, details } = orderedSavedDocuments[i];
const { name, isSaved } = details;
editor.handle.openAutoSavedDocument(documentId, name, isSaved, document, true);
editor.openAutoSavedDocument(documentId, name, isSaved, document, true);
}
if (length > 0) editor.handle.selectDocument(orderedSavedDocuments[length - 1].documentId);
if (length > 0) editor.selectDocument(orderedSavedDocuments[length - 1].documentId);
}
}
@@ -177,11 +177,11 @@ export async function saveEditorPreferences(preferences: unknown) {
await idb.set("preferences", preferences, indexedDbStorage);
}
export async function loadEditorPreferences(editor: Editor) {
export async function loadEditorPreferences(editor: EditorHandle) {
const indexedDbStorage = idb.createStore("graphite", "store");
const preferences = await idb.get<Record<string, unknown>>("preferences", indexedDbStorage);
editor.handle.loadPreferences(preferences ? JSON.stringify(preferences) : undefined);
editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined);
}
export async function wipeDocuments() {

View File

@@ -1,6 +1,6 @@
import type { Editor } from "@graphite/editor";
import type { EditorHandle } from "@graphite/../wasm/pkg/graphite_wasm";
export function setupViewportResizeObserver(editor: Editor): () => void {
export function setupViewportResizeObserver(editor: EditorHandle): () => void {
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
if (viewports.length <= 0) return () => {};
@@ -40,7 +40,7 @@ export function setupViewportResizeObserver(editor: Editor): () => void {
continue;
}
editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale);
editor.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale);
}
});