@mocanvas/wasm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Symbio Digital
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @mocanvas/wasm
2
+
3
+ The Rust/WebAssembly engine behind [mocanvas](https://github.com/SYMBIO/mocanvas)
4
+ and its TypeScript bridge. The engine owns the scene: it holds shape geometry,
5
+ maintains a spatial index, hit-tests, culls to the viewport, tessellates and
6
+ batches, and writes vertex/index/batch buffers into linear memory that the host
7
+ uploads to the GPU by pointer — no per-frame copying.
8
+
9
+ Most applications use `mocanvas` or `@mocanvas/editor` instead of this package
10
+ directly. No React, no other dependencies.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @mocanvas/wasm
16
+ ```
17
+
18
+ ## Use
19
+
20
+ ```ts
21
+ import { loadEngine } from "@mocanvas/wasm"
22
+
23
+ const bridge = await loadEngine()
24
+
25
+ // handle, kind, parent, z-lo, z-hi, flags, x, y, rotation, w, h
26
+ bridge.cmd.upsert(1, 0, 0, 0, 0, 0, 0, 0, 0, 200, 120)
27
+ bridge.cmd.setStyle(1, { fill: 0x4465e9ff, stroke: 0x000000ff, strokeWidth: 2, dash: 0, opacity: 1 })
28
+ bridge.cmd.flush()
29
+
30
+ const frame = bridge.frame({ x: 0, y: 0, z: 1 }, 800, 600)
31
+ console.log(frame.drawn, frame.batches.length / 7)
32
+ ```
33
+
34
+ ## Loading the `.wasm` file
35
+
36
+ `loadEngine()` needs no configuration in any bundler.
37
+
38
+ With no argument it resolves the module as
39
+ `new URL("../pkg/mocanvas_bg.wasm", import.meta.url)` — Vite, webpack 5 and
40
+ Rollup all recognise that form and emit the file as an asset — then fetches it
41
+ and checks the first four bytes are the WebAssembly magic number. The file is
42
+ ~256 KB and is fetched separately; it is deliberately not inlined into the
43
+ JavaScript bundle.
44
+
45
+ If those bytes are not a module, the URL is not usable: a dev server answering
46
+ every unknown path with `index.html`, a 404 page, a JSON error. Rather than let
47
+ a `CompileError: expected magic word 00 61 73 6d` escape, the loader falls back
48
+ to a base64 copy of the engine that ships in the package, pulled in with a
49
+ dynamic `import()` so it sits in its own chunk and is never downloaded on the
50
+ happy path. It logs one warning when it does.
51
+
52
+ ### Avoiding the fallback
53
+
54
+ The one common way to end up on that path is Vite's dependency optimizer, which
55
+ pre-bundles with esbuild and rewrites `import.meta.url` without moving the
56
+ asset. Excluding the package skips the extra download:
57
+
58
+ ```ts
59
+ // vite.config.ts — an optimisation, not a requirement
60
+ export default defineConfig({ optimizeDeps: { exclude: ["@mocanvas/wasm"] } })
61
+ ```
62
+
63
+ `vite build` is unaffected either way: Rollup emits the `.wasm` as an asset and
64
+ the URL resolves.
65
+
66
+ ### Pointing at the file yourself
67
+
68
+ A bundler that does not understand `new URL(..., import.meta.url)` at all, or a
69
+ setup that serves the asset from somewhere specific, can pass the location in.
70
+ Copy or serve `node_modules/@mocanvas/wasm/pkg/mocanvas_bg.wasm` and pass its
71
+ URL — or a `Response`, or the compiled bytes, or a `WebAssembly.Module`:
72
+
73
+ ```ts
74
+ await loadEngine("/assets/mocanvas_bg.wasm")
75
+ await loadEngine(fetch("/assets/mocanvas_bg.wasm"))
76
+ ```
77
+
78
+ An explicit input is taken at face value: it is handed straight to the glue,
79
+ with no validation and no fallback, so a wrong location surfaces as its own
80
+ error instead of being papered over.
81
+
82
+ ### Node
83
+
84
+ In Node `fetch` does not read `file:` URLs, so `loadEngine()` with no argument
85
+ reaches the embedded copy (with the warning). For tests and scripts, load the
86
+ bytes yourself instead:
87
+
88
+ ```ts
89
+ import { readFileSync } from "node:fs"
90
+ import { loadEngineSync } from "@mocanvas/wasm"
91
+
92
+ const bridge = loadEngineSync(readFileSync("node_modules/@mocanvas/wasm/pkg/mocanvas_bg.wasm"))
93
+ ```
94
+
95
+ The engine is a process-wide singleton: the first `loadEngine` call decides how
96
+ the module is loaded, and later calls reuse it.
97
+
98
+ ESM only.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,253 @@
1
+ import { Engine, InitInput, SyncInitInput } from '../pkg/mocanvas.js';
2
+ export { Engine } from '../pkg/mocanvas.js';
3
+
4
+ /**
5
+ * Typed loader and zero-copy bridge over the mocanvas WebAssembly engine.
6
+ *
7
+ * See docs/ARCHITECTURE.md → "Bridge ABI".
8
+ */
9
+
10
+ /** Command opcodes; must match `crates/mocanvas-wasm/src/lib.rs::op`. */
11
+ declare const OP: {
12
+ readonly UPSERT_SHAPE: 1;
13
+ readonly REMOVE_SHAPE: 2;
14
+ readonly SET_GEOMETRY: 3;
15
+ readonly SET_STYLE: 4;
16
+ readonly CLEAR: 5;
17
+ readonly SET_TEXTURE: 6;
18
+ };
19
+ /** Path opcodes; must match `mocanvas-geo::PathCmd`. */
20
+ declare const PATH_OP: {
21
+ readonly MOVE: 0;
22
+ readonly LINE: 1;
23
+ readonly QUAD: 2;
24
+ readonly CUBIC: 3;
25
+ readonly CLOSE: 4;
26
+ };
27
+ /** Shape flags; must match `mocanvas-scene`. */
28
+ declare const FLAG: {
29
+ readonly HIDDEN: number;
30
+ readonly LOCKED: number;
31
+ readonly OVERLAY: number;
32
+ readonly NO_FILL: number;
33
+ /** GPU-drawn and also reported to the DOM overlay (labels). */
34
+ readonly LABEL: number;
35
+ /** Descendants are clipped to this shape's page-space geometry AABB (frames). */
36
+ readonly CLIP: number;
37
+ };
38
+ /** Hit-test filter bits. */
39
+ declare const HIT_FILTER: {
40
+ readonly INCLUDE_LOCKED: 1;
41
+ readonly INCLUDE_HIDDEN: 2;
42
+ readonly HOLLOW_ONLY: 4;
43
+ };
44
+ type Handle = number;
45
+ interface CameraState {
46
+ /** Page-space offset. screen = (page + cam) * z */
47
+ x: number;
48
+ y: number;
49
+ z: number;
50
+ }
51
+ /** Floats per vertex in `FrameBuffers.vertices`: `x y u v r g b a`. */
52
+ declare const VERTEX_FLOATS = 8;
53
+ /** `u32` words per record in `FrameBuffers.batches`. */
54
+ declare const BATCH_WORDS = 7;
55
+ /** `u32` words per record in `FrameBuffers.overlay`. */
56
+ declare const OVERLAY_WORDS = 10;
57
+ /** Page-space clip rectangle `[minX, minY, maxX, maxY]`. */
58
+ type ClipRect = [number, number, number, number];
59
+ interface FrameBuffers {
60
+ /**
61
+ * Interleaved `x y u v r g b a` (8 floats) in page space; solid geometry has
62
+ * `u = v = 0`. View into WASM memory; valid until the next engine call.
63
+ */
64
+ vertices: Float32Array;
65
+ indices: Uint32Array;
66
+ /**
67
+ * `BATCH_WORDS` (7) words per batch: `firstIndex indexCount texture clipMinX
68
+ * clipMinY clipMaxX clipMaxY`. The clip words are f32 bits in page space; all
69
+ * four zero means unclipped. Texture 0 = solid color. A new batch starts
70
+ * whenever the texture or the clip rect changes — use `readBatch`.
71
+ */
72
+ batches: Uint32Array;
73
+ /**
74
+ * `OVERLAY_WORDS` (10) words per entry: `handle x y w h rot clipMinX clipMinY
75
+ * clipMaxX clipMaxY` with floats as bits — use `readOverlay`.
76
+ */
77
+ overlay: Uint32Array;
78
+ drawn: number;
79
+ culled: number;
80
+ /**
81
+ * Build counter, bumped only when the buffers are rebuilt. The vertex data is
82
+ * page-space and the camera is a shader uniform, so a moving camera alone does
83
+ * not change it: a backend that has already uploaded version `v` can skip the
84
+ * upload for as long as this reads `v`.
85
+ */
86
+ version: number;
87
+ /** Whether the call that produced this frame rebuilt the buffers. */
88
+ dirty: boolean;
89
+ /**
90
+ * Shapes were deferred by the per-frame tessellation budget and are drawn as
91
+ * flat placeholder quads meanwhile. Keep scheduling frames until this is false.
92
+ */
93
+ pending: boolean;
94
+ }
95
+ interface Batch {
96
+ firstIndex: number;
97
+ indexCount: number;
98
+ /** Host texture id, 0 = solid color. */
99
+ texture: number;
100
+ /** Page-space clip rect, or undefined when unclipped. */
101
+ clip?: ClipRect;
102
+ }
103
+ interface OverlayEntry {
104
+ handle: Handle;
105
+ x: number;
106
+ y: number;
107
+ w: number;
108
+ h: number;
109
+ rotation: number;
110
+ /** Page-space clip rect inherited from the nearest clipping ancestor, if any. */
111
+ clip?: ClipRect;
112
+ }
113
+ interface StyleWords {
114
+ /** 0xRRGGBBAA, alpha 0 = none */
115
+ fill: number;
116
+ stroke: number;
117
+ strokeWidth: number;
118
+ dash: number;
119
+ opacity: number;
120
+ /**
121
+ * Host texture id (0 or undefined = none). Not part of SET_STYLE; send it with
122
+ * `CommandWriter.setTexture`. When set, the fill is drawn as one textured quad
123
+ * over the shape's local bounds (uv 0..1), tinted white × opacity.
124
+ */
125
+ texture?: number;
126
+ /**
127
+ * Per-shape random seed (0 or undefined = 0). Only `dash: 3` (hand-drawn) reads
128
+ * it: it picks that shape's wobble, so the same seed always redraws the same
129
+ * outline. Derive it from the shape's stable id, never from its handle or its
130
+ * position in the scene, or a shape will change shape when it is re-added.
131
+ */
132
+ seed?: number;
133
+ }
134
+ /** Bit-cast a float to u32 without allocation. */
135
+ declare function f32bits(v: number): number;
136
+ /** Bit-cast a u32 to float without allocation. */
137
+ declare function bitsf32(v: number): number;
138
+ /** Four clip words (f32 bits) at `offset` → rect, or undefined when all zero (unclipped). */
139
+ declare function readClip(words: Uint32Array, offset: number): ClipRect | undefined;
140
+ /**
141
+ * Writes commands straight into the engine's command buffer in WASM memory.
142
+ * Call `flush()` once per store transaction.
143
+ */
144
+ declare class CommandWriter {
145
+ private readonly engine;
146
+ private readonly memory;
147
+ private view;
148
+ private cap;
149
+ private len;
150
+ constructor(engine: Engine, memory: WebAssembly.Memory, initialWords?: number);
151
+ /** Words queued but not yet applied. */
152
+ get pending(): number;
153
+ private ensure;
154
+ upsert(handle: Handle, kind: number, parent: Handle, zlo: number, zhi: number, flags: number, x: number, y: number, rotation: number, w: number, h: number): void;
155
+ remove(handle: Handle): void;
156
+ /** `pathWords` is the flat f32 path encoding (opcode, args...). */
157
+ setGeometry(handle: Handle, pathWords: ArrayLike<number>): void;
158
+ setStyle(handle: Handle, s: StyleWords): void;
159
+ /** Set the fill texture of a shape (0 = solid fill). Independent of `setStyle`. */
160
+ setTexture(handle: Handle, texture: number): void;
161
+ clear(): void;
162
+ /** Apply queued commands. Returns the number of commands applied. Throws on a malformed stream. */
163
+ flush(): number;
164
+ }
165
+ /** High-level wrapper: owns the engine, the command writer, and typed views. */
166
+ declare class EngineBridge {
167
+ readonly engine: Engine;
168
+ readonly memory: WebAssembly.Memory;
169
+ readonly cmd: CommandWriter;
170
+ /**
171
+ * Shapes the engine may tessellate in one `frame()` call; the rest are drawn as
172
+ * level-of-detail quads and picked up by later frames, which keeps a viewport
173
+ * full of never-seen shapes from stalling on one frame. Read from the engine so
174
+ * `mocanvas_render::DEFAULT_TESS_BUDGET` stays the single source of truth.
175
+ */
176
+ tessBudget: number;
177
+ private lastFrame;
178
+ constructor(engine: Engine, memory: WebAssembly.Memory);
179
+ get shapeCount(): number;
180
+ get epoch(): number;
181
+ /**
182
+ * Build (or reuse) the frame for a camera. `tessBudget` overrides
183
+ * {@link EngineBridge.tessBudget} for this call; pass `0` for no cap, which is
184
+ * what tests want when they need one deterministic frame.
185
+ *
186
+ * When the engine reports the buffers unchanged the previous `FrameBuffers`
187
+ * object is returned as-is, views included, so `frame.version` is a stable
188
+ * identity a backend can compare against what it last uploaded.
189
+ */
190
+ frame(cam: CameraState, viewportW: number, viewportH: number, tessBudget?: number): FrameBuffers;
191
+ /** Decode the overlay buffer of a frame. */
192
+ static readOverlay(overlay: Uint32Array): OverlayEntry[];
193
+ /** Decode one batch record starting at word `offset` (a multiple of `BATCH_WORDS`). */
194
+ static readBatch(batches: Uint32Array, offset: number): Batch;
195
+ /** Decode the batch buffer of a frame. */
196
+ static readBatches(batches: Uint32Array): Batch[];
197
+ /**
198
+ * Grow the box each frame is built for by `pad` (a fraction of the viewport size)
199
+ * on every side, so a camera panning inside that margin reuses the buffers instead
200
+ * of rebuilding and re-uploading them.
201
+ *
202
+ * Zero by default: the pad submits `(1 + 2 * pad)²` more geometry on every frame in
203
+ * exchange for skipping the upload on some of them, which only pays when the host's
204
+ * upload is expensive relative to its per-triangle cost. It is on a hardware GPU;
205
+ * it is emphatically not under software rasterisation.
206
+ */
207
+ setViewportPad(pad: number): void;
208
+ /** The current viewport pad. */
209
+ get viewportPad(): number;
210
+ hitTest(pageX: number, pageY: number, tolerance: number, filter?: number): Handle;
211
+ /** Handles in draw order. `mode` 0 = intersects, 1 = contains. Returns a copy. */
212
+ queryBox(minX: number, minY: number, maxX: number, maxY: number, mode?: 0 | 1, filter?: number): Uint32Array;
213
+ private readBox;
214
+ /**
215
+ * Ink page bounds `[minX, minY, maxX, maxY]` or null: the shape's outline
216
+ * expanded by half its stroke width. This is what the spatial index, the
217
+ * viewport cull and clipping run on. For a user-facing measurement of where
218
+ * the shape *is*, use {@link geometryBounds}.
219
+ */
220
+ bounds(handle: Handle): [number, number, number, number] | null;
221
+ /** Geometry page bounds `[minX, minY, maxX, maxY]` or null: `bounds` without the stroke pad. */
222
+ geometryBounds(handle: Handle): [number, number, number, number] | null;
223
+ unionBounds(handles: ArrayLike<number>): [number, number, number, number] | null;
224
+ /** Union of every shape's ink bounds, or null when the scene is empty. */
225
+ allBounds(): [number, number, number, number] | null;
226
+ /** Union of every shape's geometry bounds, or null when the scene is empty. */
227
+ allGeometryBounds(): [number, number, number, number] | null;
228
+ /** Page transform `[a b c d e f]` or null. */
229
+ pageTransform(handle: Handle): [number, number, number, number, number, number] | null;
230
+ dispose(): void;
231
+ }
232
+ /**
233
+ * Load the WASM module (once) and create an engine.
234
+ *
235
+ * With no argument the module is resolved as
236
+ * `new URL("../pkg/mocanvas_bg.wasm", import.meta.url)` — which Vite, webpack 5
237
+ * and Rollup all recognise: they emit the `.wasm` file as an asset and rewrite
238
+ * the URL — then fetched and checked for the WebAssembly magic number. If those
239
+ * bytes are not a module (a dev server's HTML fallback, a 404 page, a JSON
240
+ * error), an embedded base64 copy is used instead and a one-time warning is
241
+ * logged. No consumer configuration is required either way.
242
+ *
243
+ * Passing an input takes over completely: `loadEngine("/assets/mocanvas_bg.wasm")`,
244
+ * a `URL`, a `Response`, the compiled bytes or a `WebAssembly.Module` are handed
245
+ * to the glue as-is, with no validation and no fallback, so a mistake in the
246
+ * location you chose surfaces as its own error. See the package README.
247
+ */
248
+ declare function loadEngine(input?: InitInput): Promise<EngineBridge>;
249
+ /** Synchronous variant for tests / Node: pass the compiled bytes. */
250
+ declare function loadEngineSync(bytes: SyncInitInput): EngineBridge;
251
+ declare function engineVersion(): string;
252
+
253
+ export { BATCH_WORDS, type Batch, type CameraState, type ClipRect, CommandWriter, EngineBridge, FLAG, type FrameBuffers, HIT_FILTER, type Handle, OP, OVERLAY_WORDS, type OverlayEntry, PATH_OP, type StyleWords, VERTEX_FLOATS, bitsf32, engineVersion, f32bits, loadEngine, loadEngineSync, readClip };