@neta-art/cohub 10.0.0 → 11.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.
Files changed (129) hide show
  1. package/README.md +11 -2
  2. package/dist/board/editor/animation.d.ts +3 -0
  3. package/dist/board/editor/animation.js +16 -1
  4. package/dist/board/editor/clipboard.js +7 -0
  5. package/dist/board/editor/copy.js +23 -18
  6. package/dist/board/editor/creation.d.ts +5 -1
  7. package/dist/board/editor/creation.js +2 -1
  8. package/dist/board/editor/deletion.js +3 -3
  9. package/dist/board/editor/document.d.ts +8 -2
  10. package/dist/board/editor/document.js +54 -7
  11. package/dist/board/editor/editor.d.ts +18 -3
  12. package/dist/board/editor/editor.js +11 -1
  13. package/dist/board/editor/query.d.ts +5 -5
  14. package/dist/board/editor/query.js +14 -12
  15. package/dist/board/editor/selection.js +2 -1
  16. package/dist/board/export/index.d.ts +25 -4
  17. package/dist/board/export/index.js +45 -12
  18. package/dist/board/export/node/index.d.ts +5 -4
  19. package/dist/board/export/node/index.js +16 -11
  20. package/dist/board/export/node/scripts.d.ts +16 -0
  21. package/dist/board/export/node/scripts.js +258 -0
  22. package/dist/board/export/scene.d.ts +2 -1
  23. package/dist/board/export/scene.js +20 -6
  24. package/dist/board/model/animation.d.ts +4 -1
  25. package/dist/board/model/animation.js +34 -7
  26. package/dist/board/model/authoring.d.ts +20 -0
  27. package/dist/board/model/authoring.js +185 -0
  28. package/dist/board/model/export-plan.js +1 -0
  29. package/dist/board/model/file-preview.js +1 -1
  30. package/dist/board/model/file-snapshot.js +3 -2
  31. package/dist/board/model/image-key.js +6 -2
  32. package/dist/board/model/index.d.ts +13 -6
  33. package/dist/board/model/index.js +14 -8
  34. package/dist/board/model/items/builtin.js +2 -2
  35. package/dist/board/model/items/create.d.ts +4 -2
  36. package/dist/board/model/items/create.js +6 -5
  37. package/dist/board/model/items/index.d.ts +2 -2
  38. package/dist/board/model/items/index.js +2 -2
  39. package/dist/board/model/media-playback.d.ts +2 -1
  40. package/dist/board/model/media-playback.js +14 -4
  41. package/dist/board/model/media.js +7 -1
  42. package/dist/board/model/sampling.d.ts +19 -0
  43. package/dist/board/model/sampling.js +96 -0
  44. package/dist/board/model/script-example.d.ts +4 -0
  45. package/dist/board/model/script-example.js +21 -0
  46. package/dist/board/model/script-hit.d.ts +10 -0
  47. package/dist/board/model/script-hit.js +52 -0
  48. package/dist/board/model/script-index.d.ts +7 -0
  49. package/dist/board/model/script-index.js +15 -0
  50. package/dist/board/model/script-scene.d.ts +24 -0
  51. package/dist/board/model/script-scene.js +267 -0
  52. package/dist/board/model/script.d.ts +105 -0
  53. package/dist/board/model/script.js +1 -0
  54. package/dist/board/render/css-color.d.ts +6 -1
  55. package/dist/board/render/css-color.js +34 -20
  56. package/dist/board/render/diagnostics.d.ts +20 -0
  57. package/dist/board/render/diagnostics.js +117 -0
  58. package/dist/board/render/index.d.ts +9 -6
  59. package/dist/board/render/index.js +9 -6
  60. package/dist/board/render/palette.d.ts +2 -1
  61. package/dist/board/render/palette.js +6 -2
  62. package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
  63. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  64. package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -5
  65. package/dist/board/render/renderers/board-renderer-registry.js +3 -3
  66. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  67. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
  68. package/dist/board/render/renderers/draw-card-renderer.js +3 -2
  69. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  70. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  71. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  72. package/dist/board/render/renderers/shape-card-renderer.js +10 -27
  73. package/dist/board/render/renderers/text-card-renderer.js +5 -3
  74. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  75. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  76. package/dist/board/render/sampling.d.ts +22 -0
  77. package/dist/board/render/sampling.js +21 -0
  78. package/dist/board/render/script-export.d.ts +11 -0
  79. package/dist/board/render/script-export.js +86 -0
  80. package/dist/board/render/script-host.d.ts +31 -0
  81. package/dist/board/render/script-host.js +421 -0
  82. package/dist/board/render/script-modules.d.ts +2 -0
  83. package/dist/board/render/script-modules.js +2 -0
  84. package/dist/board/render/script-runtime.d.ts +29 -0
  85. package/dist/board/render/script-runtime.js +213 -0
  86. package/dist/board/render/script-sandbox.d.ts +4 -0
  87. package/dist/board/render/script-sandbox.js +173 -0
  88. package/dist/board/render/stroke.d.ts +2 -1
  89. package/dist/board/render/stroke.js +22 -1
  90. package/dist/board/render/text-measurement.js +1 -1
  91. package/dist/board/stage/assets.js +6 -1
  92. package/dist/board/stage/maintenance.d.ts +5 -0
  93. package/dist/board/stage/maintenance.js +11 -0
  94. package/dist/board/stage/scene.js +5 -2
  95. package/dist/board/stage/script-pointer.d.ts +19 -0
  96. package/dist/board/stage/script-pointer.js +35 -0
  97. package/dist/board/stage/stage.d.ts +10 -0
  98. package/dist/board/stage/stage.js +249 -43
  99. package/dist/board/stage/texture-pool.js +2 -3
  100. package/dist/chunks/environment.d.ts +266 -23
  101. package/dist/chunks/host-bridge.d.ts +2 -2
  102. package/dist/chunks/http.d.ts +88 -7
  103. package/dist/chunks/http.js +155 -63
  104. package/dist/chunks/transport.js +3 -1
  105. package/dist/chunks/websocket.d.ts +9 -1
  106. package/dist/chunks/websocket.js +16 -2
  107. package/dist/host-bridge.js +2 -1
  108. package/dist/http.d.ts +2 -2
  109. package/dist/index.d.ts +182 -4
  110. package/dist/index.js +1374 -126
  111. package/dist/protocol/dist/board-assets.d.ts +1 -0
  112. package/dist/protocol/dist/board-constants.js +2 -2
  113. package/dist/protocol/dist/board-model.d.ts +201 -11
  114. package/dist/protocol/dist/board-model.js +33 -14
  115. package/dist/protocol/dist/board-patch.js +2 -1
  116. package/dist/protocol/dist/board-schema.d.ts +4 -2
  117. package/dist/protocol/dist/board-schema.js +18 -4
  118. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  119. package/dist/protocol/dist/board-script-modules.js +127 -0
  120. package/dist/protocol/dist/display/index.d.ts +1 -0
  121. package/dist/protocol/dist/index.d.ts +4 -2
  122. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  123. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  124. package/package.json +2 -1
  125. package/dist/board/export/node/sketch.d.ts +0 -19
  126. package/dist/board/export/node/sketch.js +0 -146
  127. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  128. package/dist/board/render/sketch-host.d.ts +0 -14
  129. package/dist/board/render/sketch-host.js +0 -216
@@ -0,0 +1,421 @@
1
+ import { boardScriptItems } from "../model/script-index.js";
2
+ import { boardScriptScope } from "../model/script-scene.js";
3
+ import { createBoardScriptModuleReader, loadBoardScriptSources } from "../../protocol/dist/board-script-modules.js";
4
+ import { boardScriptFrameSource } from "./script-sandbox.js";
5
+ import { Texture } from "pixi.js";
6
+ //#region src/board/render/script-host.ts
7
+ function createBoardScriptHost(options) {
8
+ const revisions = /* @__PURE__ */ new Map();
9
+ const entries = /* @__PURE__ */ new Map();
10
+ const restored = new Map(Object.entries(options.snapshots ?? {}));
11
+ const sources = /* @__PURE__ */ new Map();
12
+ const dependencies = /* @__PURE__ */ new Map();
13
+ const modules = createBoardScriptModuleReader(options.readModule);
14
+ const tokens = /* @__PURE__ */ new Map();
15
+ let nextToken = 0;
16
+ let brokerReady = false;
17
+ let broker = null;
18
+ const signatures = /* @__PURE__ */ new WeakMap();
19
+ function entrySignature(item) {
20
+ let value = signatures.get(item);
21
+ if (value === void 0) {
22
+ value = JSON.stringify(item.script);
23
+ signatures.set(item, value);
24
+ }
25
+ return value;
26
+ }
27
+ const focusTargets = /* @__PURE__ */ new Map();
28
+ const layer = document.createElement("div");
29
+ layer.style.cssText = "position:absolute;inset:0;pointer-events:none;overflow:hidden;z-index:2";
30
+ layer.dataset.boardScripts = "";
31
+ options.host.appendChild(layer);
32
+ let disposed = false;
33
+ function ensureBroker() {
34
+ if (broker) return;
35
+ broker = document.createElement("iframe");
36
+ broker.setAttribute("sandbox", "allow-scripts");
37
+ broker.setAttribute("aria-hidden", "true");
38
+ broker.tabIndex = -1;
39
+ broker.style.cssText = "position:absolute;visibility:hidden;width:0;height:0;border:0";
40
+ broker.srcdoc = boardScriptFrameSource();
41
+ layer.appendChild(broker);
42
+ }
43
+ function removeFrame(entry) {
44
+ if (entry.capture) {
45
+ clearTimeout(entry.capture.timer);
46
+ entry.capture.reject(/* @__PURE__ */ new Error("Script stopped before its snapshot completed."));
47
+ entry.capture = void 0;
48
+ }
49
+ tokens.delete(entry.token);
50
+ broker?.contentWindow?.postMessage({
51
+ token: entry.token,
52
+ data: { kind: "terminate" }
53
+ }, "*");
54
+ }
55
+ function release(entry) {
56
+ clearTimeout(entry.timer);
57
+ if (entry.ready && !entry.error) {
58
+ post(entry, { kind: "dispose" });
59
+ entry.timer = setTimeout(() => removeFrame(entry), 100);
60
+ } else removeFrame(entry);
61
+ entry.texture?.destroy(true);
62
+ entries.delete(entry.id);
63
+ revisions.set(entry.id, (revisions.get(entry.id) ?? 0) + 1);
64
+ options.onOutput(entry.id, null);
65
+ }
66
+ function fail(entry, error) {
67
+ clearTimeout(entry.timer);
68
+ entry.error = error;
69
+ revisions.set(entry.id, (revisions.get(entry.id) ?? 0) + 1);
70
+ entry.pending = false;
71
+ removeFrame(entry);
72
+ options.onOutput(entry.id, null);
73
+ options.onFrame();
74
+ }
75
+ function post(entry, data) {
76
+ broker?.contentWindow?.postMessage({
77
+ token: entry.token,
78
+ data
79
+ }, "*");
80
+ }
81
+ const onMessage = (event) => {
82
+ const data = event.data;
83
+ if (!data || typeof data !== "object") return;
84
+ const fromBroker = broker !== null && event.source === broker.contentWindow;
85
+ if (fromBroker && data.kind === "ready" && data.token === void 0) {
86
+ brokerReady = true;
87
+ for (const entry of tokens.values()) entry.ready = true;
88
+ options.onFrame();
89
+ return;
90
+ }
91
+ if (!fromBroker) return;
92
+ const entry = tokens.get(data.token);
93
+ if (!entry || entry.error) {
94
+ if (fromBroker || entry) data.bitmap?.close?.();
95
+ return;
96
+ }
97
+ if (data.kind === "disposed") {
98
+ clearTimeout(entry.timer);
99
+ removeFrame(entry);
100
+ return;
101
+ }
102
+ if (entries.get(entry.id) !== entry) return;
103
+ if (data.kind === "ready") {
104
+ entry.ready = true;
105
+ options.onFrame();
106
+ return;
107
+ }
108
+ if (data.kind === "dirty") {
109
+ entry.dirty = true;
110
+ options.onFrame();
111
+ return;
112
+ }
113
+ if (data.kind === "call") {
114
+ const capability = typeof data.name === "string" && options.capabilities && Object.hasOwn(options.capabilities, data.name) ? options.capabilities[data.name] : void 0;
115
+ Promise.resolve().then(() => {
116
+ if (!capability) throw new Error(`Capability unavailable: ${data.name}`);
117
+ return capability(entry.id, data.input);
118
+ }).then((value) => post(entry, {
119
+ kind: "result",
120
+ call: data.call,
121
+ value
122
+ }), (error) => post(entry, {
123
+ kind: "result",
124
+ call: data.call,
125
+ error: error instanceof Error ? error.message : String(error)
126
+ }));
127
+ return;
128
+ }
129
+ if (data.kind === "snapshot") {
130
+ const capture = entry.capture;
131
+ if (!capture) return;
132
+ clearTimeout(capture.timer);
133
+ entry.capture = void 0;
134
+ if (data.error) capture.reject(new Error(String(data.error)));
135
+ else capture.resolve(data.snapshot ? {
136
+ ...data.snapshot,
137
+ configuration: entry.signature
138
+ } : null);
139
+ return;
140
+ }
141
+ if (data.kind === "error") {
142
+ fail(entry, String(data.error));
143
+ return;
144
+ }
145
+ if (data.kind !== "frame" || data.request !== entry.request) {
146
+ data.bitmap?.close?.();
147
+ return;
148
+ }
149
+ clearTimeout(entry.timer);
150
+ entry.pending = false;
151
+ revisions.set(entry.id, (revisions.get(entry.id) ?? 0) + 1);
152
+ if (data.bitmap) {
153
+ const previous = entry.texture;
154
+ entry.texture = Texture.from(data.bitmap);
155
+ previous?.destroy(true);
156
+ }
157
+ try {
158
+ const output = data.output;
159
+ if (output) {
160
+ options.onOutput(entry.id, output);
161
+ entry.output = output;
162
+ }
163
+ } catch (error) {
164
+ fail(entry, error instanceof Error ? error.message : String(error));
165
+ return;
166
+ }
167
+ options.onFrame();
168
+ };
169
+ window.addEventListener("message", onMessage);
170
+ return {
171
+ revision: (id) => revisions.get(id) ?? 0,
172
+ ready: (id) => entries.get(id)?.output !== null && entries.get(id)?.output !== void 0,
173
+ frame: (item) => entries.get(item.id)?.texture ?? null,
174
+ error: (id) => entries.get(id)?.error ?? null,
175
+ release: () => {},
176
+ fault(id, message) {
177
+ const entry = entries.get(id);
178
+ if (entry) fail(entry, message);
179
+ },
180
+ snapshot(id) {
181
+ const entry = entries.get(id);
182
+ if (!entry?.output || entry.error) return Promise.resolve(null);
183
+ if (entry.capture) return entry.capture.promise;
184
+ let resolve = () => {};
185
+ let reject = () => {};
186
+ const promise = new Promise((yes, no) => {
187
+ resolve = yes;
188
+ reject = no;
189
+ });
190
+ const timer = setTimeout(() => {
191
+ entry.capture = void 0;
192
+ reject(/* @__PURE__ */ new Error("Script snapshot timed out."));
193
+ }, options.timeoutMs ?? 2e3);
194
+ entry.capture = {
195
+ promise,
196
+ resolve,
197
+ reject,
198
+ timer
199
+ };
200
+ post(entry, {
201
+ kind: "snapshot",
202
+ events: entry.events.splice(0)
203
+ });
204
+ return promise;
205
+ },
206
+ restore(id, snapshot) {
207
+ const entry = entries.get(id);
208
+ if (entry && snapshot.configuration && snapshot.configuration !== entry.signature) throw new Error("Snapshot configuration does not match the script.");
209
+ if (!Number.isFinite(snapshot.time) || snapshot.time < 0 || !Number.isInteger(snapshot.random)) throw new Error("Invalid script snapshot.");
210
+ restored.set(id, structuredClone(snapshot));
211
+ if (entry) release(entry);
212
+ options.onFrame();
213
+ },
214
+ output: (id) => entries.get(id)?.output ?? null,
215
+ get animated() {
216
+ return [...entries.values()].some((entry) => !entry.error && entry.output?.animated && (entry.visible || entry.output.updating) && !entry.pending);
217
+ },
218
+ sync(board, scene, camera, time, interactive, visibleIds) {
219
+ if (disposed) return;
220
+ const alive = /* @__PURE__ */ new Set();
221
+ const liveFocusTargets = /* @__PURE__ */ new Set();
222
+ for (const [id, item] of boardScriptItems(board)) {
223
+ if (!item.script?.enabled) continue;
224
+ alive.add(id);
225
+ const signature = entrySignature(item);
226
+ let entry = entries.get(id);
227
+ if (entry && entry.signature !== signature) {
228
+ release(entry);
229
+ entry = void 0;
230
+ }
231
+ const box = scene.layout.box(id);
232
+ const scoped = entry?.inputDocument === board ? entry.input.items : { [id]: item };
233
+ if (entry?.inputDocument !== board) for (const child of boardScriptScope(board, id)) {
234
+ const value = board.items[child];
235
+ if (value) scoped[child] = value;
236
+ }
237
+ const input = entry?.inputDocument === board && entry.input.width === box.width && entry.input.height === box.height ? entry.input : {
238
+ id,
239
+ item,
240
+ items: scoped,
241
+ width: box.width,
242
+ height: box.height,
243
+ capabilities: Object.keys(options.capabilities ?? {}),
244
+ snapshot: restored.get(id)
245
+ };
246
+ if (!entry) {
247
+ ensureBroker();
248
+ entry = {
249
+ id,
250
+ signature,
251
+ token: ++nextToken,
252
+ ready: brokerReady,
253
+ sources: null,
254
+ input,
255
+ inputKey: "",
256
+ inputDocument: null,
257
+ startedAt: (options.export ? 0 : time) - (input.snapshot?.time ?? 0),
258
+ texture: null,
259
+ error: null,
260
+ output: null,
261
+ events: [],
262
+ dirty: true,
263
+ request: 0,
264
+ pending: false,
265
+ time: -1,
266
+ sentSources: false,
267
+ visible: true
268
+ };
269
+ entries.set(id, entry);
270
+ restored.delete(id);
271
+ tokens.set(entry.token, entry);
272
+ const current = entry;
273
+ let loading = sources.get(item.script.src);
274
+ if (!loading) {
275
+ const paths = /* @__PURE__ */ new Set([item.script.src]);
276
+ dependencies.set(item.script.src, paths);
277
+ loading = loadBoardScriptSources(item.script.src, (path) => {
278
+ paths.add(path);
279
+ return modules.read(path);
280
+ });
281
+ sources.set(item.script.src, loading);
282
+ }
283
+ current.timer = setTimeout(() => fail(current, "Script loading timed out."), 15e3);
284
+ loading.then((value) => {
285
+ if (disposed || entries.get(id) !== current) return;
286
+ current.sources = value;
287
+ options.onFrame();
288
+ }, (error) => {
289
+ if (!disposed && entries.get(id) === current) fail(current, String(error));
290
+ });
291
+ }
292
+ if (entry.error) continue;
293
+ const visible = !visibleIds || visibleIds.has(id);
294
+ if (entry.visible !== visible) {
295
+ entry.visible = visible;
296
+ if (visible) entry.dirty = true;
297
+ }
298
+ if (entry.input !== input || entry.inputDocument !== board) {
299
+ const inputKey = JSON.stringify(input);
300
+ if (entry.inputKey !== inputKey) {
301
+ entry.inputKey = inputKey;
302
+ entry.input = input;
303
+ entry.dirty = true;
304
+ }
305
+ entry.inputDocument = board;
306
+ }
307
+ if (entry.output && !options.export) {
308
+ const hits = Object.entries(entry.output.hits).filter(([, hit]) => hit.label);
309
+ if (!hits.length && entry.output.listens.length) hits.push([id, {
310
+ x: 0,
311
+ y: 0,
312
+ width: box.width,
313
+ height: box.height,
314
+ label: "text" in item.props && typeof item.props.text === "string" ? item.props.text : item.script.src
315
+ }]);
316
+ for (const [target, hit] of hits) {
317
+ const key = `${id}/${target}`;
318
+ liveFocusTargets.add(key);
319
+ let button = focusTargets.get(key);
320
+ if (!button) {
321
+ button = document.createElement("button");
322
+ button.type = "button";
323
+ const emit = (type, key) => {
324
+ const current = entries.get(id);
325
+ if (!current || current.error) return;
326
+ current.events.push({
327
+ type,
328
+ target,
329
+ key,
330
+ point: {
331
+ x: hit.x + hit.width / 2,
332
+ y: hit.y + hit.height / 2
333
+ }
334
+ });
335
+ current.dirty = true;
336
+ options.onFrame();
337
+ };
338
+ button.onfocus = () => emit("focus");
339
+ button.onblur = () => emit("blur");
340
+ button.onclick = () => emit("click");
341
+ button.onkeydown = (event) => emit("keydown", event.key);
342
+ button.onkeyup = (event) => emit("keyup", event.key);
343
+ focusTargets.set(key, button);
344
+ layer.appendChild(button);
345
+ }
346
+ button.setAttribute("aria-label", hit.label ?? target);
347
+ button.tabIndex = interactive && visible ? 0 : -1;
348
+ const [a, b, c, d, x, y] = scene.layout.matrix(id);
349
+ button.style.cssText = `position:absolute;pointer-events:none;background:transparent;border:0;padding:0;transform-origin:0 0;width:${hit.width}px;height:${hit.height}px;transform:matrix(${a * camera.zoom},${b * camera.zoom},${c * camera.zoom},${d * camera.zoom},${(x + a * hit.x + c * hit.y) * camera.zoom + camera.x},${(y + b * hit.x + d * hit.y) * camera.zoom + camera.y})`;
350
+ }
351
+ }
352
+ if (!entry.ready || !entry.sources || entry.pending || !entry.dirty && (!entry.output?.animated || !visible && !entry.output.updating || entry.time === time)) continue;
353
+ entry.pending = true;
354
+ entry.dirty = false;
355
+ entry.time = time;
356
+ entry.request++;
357
+ const current = entry;
358
+ clearTimeout(entry.timer);
359
+ entry.timer = setTimeout(() => fail(current, "Script execution timed out."), options.timeoutMs ?? 2e3);
360
+ post(entry, {
361
+ request: entry.request,
362
+ input: entry.sentInput === entry.input ? void 0 : entry.input,
363
+ sources: entry.sentSources ? void 0 : entry.sources,
364
+ draw: visible,
365
+ t: time - entry.startedAt,
366
+ scale: Math.min(2, 2048 / Math.max(1, box.width, box.height)),
367
+ events: entry.events.splice(0)
368
+ });
369
+ entry.sentInput = entry.input;
370
+ entry.sentSources = true;
371
+ }
372
+ for (const [key, button] of focusTargets) if (!liveFocusTargets.has(key)) {
373
+ button.remove();
374
+ focusTargets.delete(key);
375
+ }
376
+ for (const entry of entries.values()) if (!alive.has(entry.id)) release(entry);
377
+ },
378
+ event(id, event) {
379
+ const entry = entries.get(id);
380
+ if (!entry || entry.error || entry.output && !entry.output.listens.includes(event.type)) return;
381
+ if (event.type === "pointermove" && entry.events.at(-1)?.type === "pointermove") entry.events.pop();
382
+ if (entry.events.length >= 128) return;
383
+ entry.events.push(event);
384
+ entry.dirty = true;
385
+ options.onFrame();
386
+ },
387
+ invalidate(path) {
388
+ modules.invalidate(path);
389
+ const affected = new Set([...sources.keys()].filter((src) => !path || src === path || dependencies.get(src)?.has(path)));
390
+ for (const src of affected) {
391
+ sources.delete(src);
392
+ dependencies.delete(src);
393
+ }
394
+ let changed = false;
395
+ for (const entry of entries.values()) {
396
+ if (path && !affected.has(entry.input.item.script?.src ?? "")) continue;
397
+ restored.delete(entry.id);
398
+ release(entry);
399
+ changed = true;
400
+ }
401
+ if (!path) restored.clear();
402
+ if (changed) options.onFrame();
403
+ },
404
+ destroy() {
405
+ disposed = true;
406
+ window.removeEventListener("message", onMessage);
407
+ for (const entry of entries.values()) release(entry);
408
+ layer.remove();
409
+ sources.clear();
410
+ dependencies.clear();
411
+ modules.destroy();
412
+ focusTargets.clear();
413
+ restored.clear();
414
+ tokens.clear();
415
+ revisions.clear();
416
+ broker = null;
417
+ }
418
+ };
419
+ }
420
+ //#endregion
421
+ export { createBoardScriptHost };
@@ -0,0 +1,2 @@
1
+ import { BOARD_SCRIPT_MAX_BYTES, BoardScriptSource, BoardScriptSources, createBoardScriptModuleReader, loadBoardScriptSources, resolveScriptImport, scriptModuleUrls } from "../../protocol/dist/board-script-modules.js";
2
+ export { BOARD_SCRIPT_MAX_BYTES, BoardScriptSource, BoardScriptSources, createBoardScriptModuleReader, loadBoardScriptSources, resolveScriptImport, scriptModuleUrls };
@@ -0,0 +1,2 @@
1
+ import { BOARD_SCRIPT_MAX_BYTES, createBoardScriptModuleReader, loadBoardScriptSources, resolveScriptImport, scriptModuleUrls } from "../../protocol/dist/board-script-modules.js";
2
+ export { BOARD_SCRIPT_MAX_BYTES, createBoardScriptModuleReader, loadBoardScriptSources, resolveScriptImport, scriptModuleUrls };
@@ -0,0 +1,29 @@
1
+ import { BoardItem } from "../../protocol/dist/board-model.js";
2
+ import "../../protocol/dist/index.js";
3
+ import { BoardScriptEvent, BoardScriptModule, BoardScriptOutput, BoardScriptSnapshot } from "../model/script.js";
4
+ //#region src/board/render/script-runtime.d.ts
5
+ type ScriptRuntimeInput = {
6
+ id: string;
7
+ item: BoardItem;
8
+ items: Record<string, BoardItem>;
9
+ width: number;
10
+ height: number;
11
+ capabilities?: string[];
12
+ snapshot?: BoardScriptSnapshot;
13
+ };
14
+ /** Self-contained so browser workers and Node execute exactly the same kernel. */
15
+ declare function createScriptRuntime(module: BoardScriptModule, initial: ScriptRuntimeInput, host: {
16
+ call(name: string, input: unknown): Promise<unknown>;
17
+ invalidate(): void;
18
+ }): {
19
+ readonly animated: boolean;
20
+ update(next: ScriptRuntimeInput): void;
21
+ step(t: number): void;
22
+ event(event: BoardScriptEvent): void;
23
+ draw(canvas: CanvasRenderingContext2D): void;
24
+ output(): BoardScriptOutput;
25
+ snapshot(includeEffects?: boolean, clone?: boolean): BoardScriptSnapshot;
26
+ destroy(): void;
27
+ };
28
+ //#endregion
29
+ export { ScriptRuntimeInput, createScriptRuntime };
@@ -0,0 +1,213 @@
1
+ //#region src/board/render/script-runtime.ts
2
+ /** Self-contained so browser workers and Node execute exactly the same kernel. */
3
+ function createScriptRuntime(module, initial, host) {
4
+ if ("mount" in module) throw new Error("Board scripts do not support DOM mount(). Use draw() or native scene items.");
5
+ let input = initial;
6
+ let disposed = false;
7
+ let revision = 0;
8
+ let randomState = 2166136261;
9
+ for (const char of initial.item.script?.seed ?? initial.id) randomState = Math.imul(randomState ^ char.charCodeAt(0), 16777619) >>> 0;
10
+ const parameterDefaults = Object.fromEntries(Object.entries(module.parameters ?? {}).filter(([, spec]) => spec.default !== void 0).map(([key, spec]) => [key, spec.default]));
11
+ let params = {
12
+ ...parameterDefaults,
13
+ ...initial.item.script?.params
14
+ };
15
+ const state = structuredClone(initial.snapshot?.state ?? {});
16
+ if (initial.snapshot) randomState = initial.snapshot.random;
17
+ let time = initial.snapshot?.time ?? 0;
18
+ let started = Boolean(initial.snapshot);
19
+ const stepMs = 1e3 / 60;
20
+ let frame = {
21
+ t: time,
22
+ dt: 0,
23
+ width: initial.width,
24
+ height: initial.height,
25
+ seed: initial.item.script?.seed ?? initial.id,
26
+ params,
27
+ state
28
+ };
29
+ const handlers = /* @__PURE__ */ new Map();
30
+ const frames = /* @__PURE__ */ new Set();
31
+ const cleanups = [];
32
+ const effects = structuredClone(initial.snapshot?.effects);
33
+ let patches = effects?.patches ?? {};
34
+ let items = effects?.items ?? {};
35
+ let space = effects?.space ?? "world";
36
+ let hits = effects?.hits ?? {};
37
+ const invalidate = () => {
38
+ if (disposed) return;
39
+ revision++;
40
+ host.invalidate();
41
+ };
42
+ const present = (id, patch) => {
43
+ if (patch === null) delete patches[id];
44
+ else patches[id] = structuredClone(patch);
45
+ invalidate();
46
+ };
47
+ const context = {
48
+ self: {
49
+ id: initial.id,
50
+ get base() {
51
+ return input.item;
52
+ },
53
+ present: (patch) => present(initial.id, patch)
54
+ },
55
+ get params() {
56
+ return params;
57
+ },
58
+ get targets() {
59
+ return input.item.script?.targets ?? {};
60
+ },
61
+ async emit(target, type, data) {
62
+ await host.call("board.emit", {
63
+ target,
64
+ type,
65
+ data
66
+ });
67
+ },
68
+ state,
69
+ get frame() {
70
+ return frame;
71
+ },
72
+ random() {
73
+ randomState = Math.imul(randomState, 1664525) + 1013904223 >>> 0;
74
+ return randomState / 4294967296;
75
+ },
76
+ on(type, handler) {
77
+ let listeners = handlers.get(type);
78
+ if (!listeners) {
79
+ listeners = /* @__PURE__ */ new Set();
80
+ handlers.set(type, listeners);
81
+ }
82
+ listeners.add(handler);
83
+ invalidate();
84
+ return () => {
85
+ listeners.delete(handler);
86
+ invalidate();
87
+ };
88
+ },
89
+ onFrame(handler) {
90
+ frames.add(handler);
91
+ invalidate();
92
+ return () => {
93
+ frames.delete(handler);
94
+ invalidate();
95
+ };
96
+ },
97
+ onDispose: (handler) => cleanups.push(handler),
98
+ invalidate,
99
+ get: (id) => input.items[id],
100
+ present,
101
+ scene(next, options) {
102
+ items = structuredClone(next);
103
+ space = options?.space ?? "world";
104
+ invalidate();
105
+ },
106
+ hitRegions(next) {
107
+ hits = structuredClone(next);
108
+ invalidate();
109
+ },
110
+ async commit(patch) {
111
+ await host.call("board.commit", patch);
112
+ },
113
+ capabilities: {
114
+ has: (name) => (input.capabilities ?? []).includes(name),
115
+ call: (name, value) => host.call(name, value)
116
+ }
117
+ };
118
+ const setup = module.default?.(context);
119
+ if (typeof setup === "function") cleanups.push(setup);
120
+ return {
121
+ get animated() {
122
+ return frames.size > 0 || module.animated === true;
123
+ },
124
+ update(next) {
125
+ input = next;
126
+ params = {
127
+ ...parameterDefaults,
128
+ ...next.item.script?.params
129
+ };
130
+ },
131
+ step(t) {
132
+ if (disposed) return;
133
+ if (!Number.isFinite(t) || t < time - .001) throw new Error("Reset the instance before seeking backwards.");
134
+ const update = (at, dt) => {
135
+ frame = {
136
+ ...frame,
137
+ t: at,
138
+ dt,
139
+ width: input.width,
140
+ height: input.height,
141
+ params: context.params
142
+ };
143
+ for (const handler of frames) handler(frame);
144
+ };
145
+ if (!started) {
146
+ started = true;
147
+ update(time, 0);
148
+ }
149
+ if (frames.size) while (time + stepMs <= t + 1e-6) {
150
+ time += stepMs;
151
+ update(time, stepMs);
152
+ }
153
+ else time = t;
154
+ frame = {
155
+ ...frame,
156
+ t,
157
+ dt: 0,
158
+ width: input.width,
159
+ height: input.height,
160
+ params: context.params
161
+ };
162
+ },
163
+ event(event) {
164
+ if (disposed) return;
165
+ const listeners = handlers.get(event.type);
166
+ if (!listeners?.size) return;
167
+ for (const handler of listeners) handler(event);
168
+ invalidate();
169
+ },
170
+ draw(canvas) {
171
+ const result = module.draw?.(canvas, frame);
172
+ if (result && typeof result.then === "function") throw new Error("draw() must be synchronous.");
173
+ },
174
+ output() {
175
+ return {
176
+ parameters: module.parameters,
177
+ space,
178
+ patches,
179
+ items,
180
+ hits,
181
+ listens: [...handlers].filter(([, callbacks]) => callbacks.size).map(([type]) => type),
182
+ animated: this.animated,
183
+ updating: frames.size > 0,
184
+ version: revision
185
+ };
186
+ },
187
+ snapshot(includeEffects = true, clone = true) {
188
+ const snapshot = {
189
+ state,
190
+ time,
191
+ random: randomState,
192
+ effects: includeEffects ? {
193
+ patches,
194
+ items,
195
+ hits,
196
+ space
197
+ } : void 0
198
+ };
199
+ return clone ? structuredClone(snapshot) : snapshot;
200
+ },
201
+ destroy() {
202
+ disposed = true;
203
+ for (const cleanup of cleanups.reverse()) cleanup();
204
+ handlers.clear();
205
+ frames.clear();
206
+ patches = {};
207
+ items = {};
208
+ hits = {};
209
+ }
210
+ };
211
+ }
212
+ //#endregion
213
+ export { createScriptRuntime };
@@ -0,0 +1,4 @@
1
+ //#region src/board/render/script-sandbox.d.ts
2
+ declare function boardScriptFrameSource(): string;
3
+ //#endregion
4
+ export { boardScriptFrameSource };