import type WasmBindgenPackage from "@graphite-frontend/wasm/pkg"; import init, { setRandomSeed, wasmMemory, JsEditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js"; import { panicProxy } from "@graphite/utility-functions/panic-proxy"; import { type JsMessageType } from "@graphite/wasm-communication/messages"; import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/wasm-communication/subscription-router"; export type WasmRawInstance = WebAssembly.Memory; export type WasmEditorInstance = JsEditorHandle; export type Editor = Readonly>; // `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; export async function updateImage(path: BigUint64Array, nodeId: bigint, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint): Promise { const blob = new Blob([imageData], { type: mime }); const blobURL = URL.createObjectURL(blob); // Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG const image = new Image(); image.src = blobURL; await image.decode(); window["editorInstance"]?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, transform); } export async function fetchImage(path: BigUint64Array, nodeId: bigint, mime: string, documentId: bigint, url: string): Promise { const data = await fetch(url); const blob = await data.blob(); const blobURL = URL.createObjectURL(blob); // Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG const image = new Image(); image.src = blobURL; await image.decode(); window["editorInstance"]?.setImageBlobURL(documentId, path, nodeId, blobURL, image.naturalWidth, image.naturalHeight, undefined); } const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api"); export async function dispatchTauri(message: unknown): Promise { if (!tauri) return; try { const response = await (await tauri).invoke("handle_message", { message }); window["editorInstance"]?.tauriResponse(response); } catch { // eslint-disable-next-line no-console console.error("Failed to dispatch Tauri message"); } } // Should be called asynchronously before `createEditor()` export async function initWasm(): Promise { // 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 // eslint-disable-next-line import/no-cycle await init(); wasmImport = await wasmMemory(); window["imageCanvases"] = {}; // 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); setRandomSeed(randomSeed); if (!tauri) return; await (await tauri).invoke("set_random_seed", { seed: randomSeedFloat }); } // Should be called after running `initWasm()` and its promise resolving // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createEditor() { // Raw: Object containing several callable functions from `editor_api.rs` defined directly on the WASM module, not the editor instance (generated by wasm-bindgen) if (!wasmImport) throw new Error("Editor WASM backend was not initialized at application startup"); const raw: WasmRawInstance = wasmImport; // Instance: Object containing many functions from `editor_api.rs` that are part of the editor instance (generated by wasm-bindgen) const instance: WasmEditorInstance = new JsEditorHandle((messageType: JsMessageType, messageData: Record): void => { // This callback is called by WASM when a FrontendMessage is received from the WASM wrapper editor instance // We pass along the first two arguments then add our own `raw` and `instance` context for the last two arguments subscriptions.handleJsMessage(messageType, messageData, raw, instance); }); window["editorInstance"] = instance; // Subscriptions: Allows subscribing to messages in JS that are sent from the WASM backend const subscriptions: SubscriptionRouter = createSubscriptionRouter(); // Check if the URL hash fragment has any demo artwork to be loaded (async () => { const demoArtwork = window.location.hash.trim().match(/#demo\/(.*)/)?.[1]; if (!demoArtwork) return; try { const url = new URL(`https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/demo-artwork/${demoArtwork}.graphite`); const data = await fetch(url); const filename = url.pathname.split("/").pop() || "Untitled"; const content = await data.text(); instance.openDocumentFile(filename, content); // Remove the hash fragment from the URL window.location.hash = ""; } catch {} })(); return { raw, instance, subscriptions, }; } export function injectImaginatePollServerStatus() { window["editorInstance"]?.injectImaginatePollServerStatus() }