File size: 3,389 Bytes
2409829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
import { type JsMessageType } from "@graphite/messages";
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router";
import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";

export type Editor = {
	raw: WebAssembly.Memory;
	handle: EditorHandle;
	subscriptions: SubscriptionRouter;
};

// `wasmImport` starts uninitialized because its initialization needs to occur asynchronously, and thus needs to occur by manually calling and awaiting `initWasm()`
let wasmImport: WebAssembly.Memory | undefined;

// 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
	// eslint-disable-next-line import/no-cycle
	const wasm = await init();
	for (const [name, f] of Object.entries(wasm)) {
		if (name.startsWith("__node_registry")) f();
	}

	wasmImport = await wasmMemory();
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	(window as any).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);
}

// 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;

	// Handle: object containing many functions from `editor_api.rs` that are part of the `EditorHandle` struct (generated by wasm-bindgen)
	const handle: EditorHandle = new EditorHandle((messageType: JsMessageType, messageData: Record<string, unknown>) => {
		// This callback is called by WASM when a FrontendMessage is received from the WASM wrapper `EditorHandle`
		// We pass along the first two arguments then add our own `raw` and `handle` context for the last two arguments
		subscriptions.handleJsMessage(messageType, messageData, raw, handle);
	});

	// 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(`/${demoArtwork}.graphite`, document.location.href);
			const data = await fetch(url);
			if (!data.ok) throw new Error();

			const filename = url.pathname.split("/").pop() || "Untitled";
			const content = await data.text();
			handle.openDocumentFile(filename, content);

			// Remove the hash fragment from the URL
			history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
		} catch {
			// Do nothing
		}
	})();

	return { raw, handle, subscriptions };
}