@neta-art/cohub 10.0.0 → 11.1.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 (155) hide show
  1. package/README.md +42 -7
  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 +7 -3
  7. package/dist/board/editor/creation.js +5 -4
  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 +22 -7
  12. package/dist/board/editor/editor.js +12 -2
  13. package/dist/board/editor/index.d.ts +2 -2
  14. package/dist/board/editor/interaction.d.ts +8 -2
  15. package/dist/board/editor/interaction.js +34 -1
  16. package/dist/board/editor/query.d.ts +5 -5
  17. package/dist/board/editor/query.js +14 -12
  18. package/dist/board/editor/selection.js +2 -1
  19. package/dist/board/editor/snapshots.d.ts +0 -2
  20. package/dist/board/editor/snapshots.js +4 -20
  21. package/dist/board/export/index.d.ts +25 -4
  22. package/dist/board/export/index.js +45 -12
  23. package/dist/board/export/node/index.d.ts +5 -4
  24. package/dist/board/export/node/index.js +16 -11
  25. package/dist/board/export/node/scripts.d.ts +16 -0
  26. package/dist/board/export/node/scripts.js +258 -0
  27. package/dist/board/export/scene.d.ts +2 -1
  28. package/dist/board/export/scene.js +24 -7
  29. package/dist/board/model/animation.d.ts +4 -1
  30. package/dist/board/model/animation.js +34 -7
  31. package/dist/board/model/authoring.d.ts +20 -0
  32. package/dist/board/model/authoring.js +185 -0
  33. package/dist/board/model/export-plan.js +1 -0
  34. package/dist/board/model/file-preview.js +1 -1
  35. package/dist/board/model/file-snapshot.js +3 -2
  36. package/dist/board/model/image-key.js +7 -3
  37. package/dist/board/model/index.d.ts +15 -7
  38. package/dist/board/model/index.js +16 -9
  39. package/dist/board/model/items/builtin.js +2 -2
  40. package/dist/board/model/items/create.d.ts +19 -4
  41. package/dist/board/model/items/create.js +130 -9
  42. package/dist/board/model/items/index.d.ts +2 -2
  43. package/dist/board/model/items/index.js +2 -2
  44. package/dist/board/model/media-playback.d.ts +2 -1
  45. package/dist/board/model/media-playback.js +15 -5
  46. package/dist/board/model/media.js +7 -1
  47. package/dist/board/model/overlay-placement.d.ts +25 -0
  48. package/dist/board/model/overlay-placement.js +35 -0
  49. package/dist/board/model/replay.d.ts +1 -1
  50. package/dist/board/model/replay.js +1 -0
  51. package/dist/board/model/sampling.d.ts +19 -0
  52. package/dist/board/model/sampling.js +96 -0
  53. package/dist/board/model/script-example.d.ts +4 -0
  54. package/dist/board/model/script-example.js +21 -0
  55. package/dist/board/model/script-hit.d.ts +27 -0
  56. package/dist/board/model/script-hit.js +109 -0
  57. package/dist/board/model/script-index.d.ts +7 -0
  58. package/dist/board/model/script-index.js +15 -0
  59. package/dist/board/model/script-observers.d.ts +18 -0
  60. package/dist/board/model/script-observers.js +92 -0
  61. package/dist/board/model/script-scene.d.ts +24 -0
  62. package/dist/board/model/script-scene.js +294 -0
  63. package/dist/board/model/script.d.ts +149 -0
  64. package/dist/board/model/script.js +1 -0
  65. package/dist/board/render/board-background.js +5 -4
  66. package/dist/board/render/css-color.d.ts +6 -1
  67. package/dist/board/render/css-color.js +34 -20
  68. package/dist/board/render/diagnostics.d.ts +20 -0
  69. package/dist/board/render/diagnostics.js +117 -0
  70. package/dist/board/render/index.d.ts +9 -6
  71. package/dist/board/render/index.js +10 -7
  72. package/dist/board/render/media-interaction.js +1 -1
  73. package/dist/board/render/palette.d.ts +2 -1
  74. package/dist/board/render/palette.js +6 -2
  75. package/dist/board/render/renderers/arrow-card-renderer.js +3 -2
  76. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  77. package/dist/board/render/renderers/board-renderer-registry.d.ts +25 -5
  78. package/dist/board/render/renderers/board-renderer-registry.js +11 -4
  79. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  80. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +18 -16
  81. package/dist/board/render/renderers/draw-card-renderer.js +3 -2
  82. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  83. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  84. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  85. package/dist/board/render/renderers/shape-card-renderer.js +10 -27
  86. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  87. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  88. package/dist/board/render/renderers/text-card-renderer.js +5 -3
  89. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  90. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  91. package/dist/board/render/sampling.d.ts +22 -0
  92. package/dist/board/render/sampling.js +21 -0
  93. package/dist/board/render/script-export.d.ts +11 -0
  94. package/dist/board/render/script-export.js +86 -0
  95. package/dist/board/render/script-host.d.ts +31 -0
  96. package/dist/board/render/script-host.js +446 -0
  97. package/dist/board/render/script-modules.d.ts +2 -0
  98. package/dist/board/render/script-modules.js +2 -0
  99. package/dist/board/render/script-runtime.d.ts +29 -0
  100. package/dist/board/render/script-runtime.js +223 -0
  101. package/dist/board/render/script-sandbox.d.ts +4 -0
  102. package/dist/board/render/script-sandbox.js +173 -0
  103. package/dist/board/render/script-texture.d.ts +11 -0
  104. package/dist/board/render/script-texture.js +33 -0
  105. package/dist/board/render/stroke.d.ts +2 -1
  106. package/dist/board/render/stroke.js +22 -1
  107. package/dist/board/render/text-measurement.js +1 -1
  108. package/dist/board/stage/assets.js +7 -2
  109. package/dist/board/stage/maintenance.d.ts +5 -0
  110. package/dist/board/stage/maintenance.js +11 -0
  111. package/dist/board/stage/scene.d.ts +2 -0
  112. package/dist/board/stage/scene.js +21 -2
  113. package/dist/board/stage/script-pointer.d.ts +20 -0
  114. package/dist/board/stage/script-pointer.js +41 -0
  115. package/dist/board/stage/stage.d.ts +21 -3
  116. package/dist/board/stage/stage.js +350 -47
  117. package/dist/board/stage/texture-pool.js +2 -3
  118. package/dist/chunks/environment.d.ts +305 -27
  119. package/dist/chunks/environment.js +12 -4
  120. package/dist/chunks/host-bridge.d.ts +2 -2
  121. package/dist/chunks/http.d.ts +90 -7
  122. package/dist/chunks/http.js +157 -64
  123. package/dist/chunks/transport.js +4 -2
  124. package/dist/chunks/websocket.d.ts +9 -1
  125. package/dist/chunks/websocket.js +16 -2
  126. package/dist/host-bridge.js +2 -1
  127. package/dist/http.d.ts +3 -3
  128. package/dist/index.d.ts +182 -4
  129. package/dist/index.js +1559 -299
  130. package/dist/protocol/dist/board-assets.d.ts +1 -0
  131. package/dist/protocol/dist/board-constants.js +2 -2
  132. package/dist/protocol/dist/board-model.d.ts +236 -11
  133. package/dist/protocol/dist/board-model.js +46 -17
  134. package/dist/protocol/dist/board-patch.js +2 -1
  135. package/dist/protocol/dist/board-schema.d.ts +4 -2
  136. package/dist/protocol/dist/board-schema.js +18 -4
  137. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  138. package/dist/protocol/dist/board-script-modules.js +127 -0
  139. package/dist/protocol/dist/board-task.d.ts +22 -0
  140. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  141. package/dist/protocol/dist/display/index.d.ts +1 -0
  142. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  143. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  144. package/dist/protocol/dist/index.d.ts +7 -2
  145. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  146. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  147. package/dist/protocol/dist/task/index.d.ts +4 -0
  148. package/package.json +2 -1
  149. package/dist/board/export/node/sketch.d.ts +0 -19
  150. package/dist/board/export/node/sketch.js +0 -146
  151. package/dist/board/model/task.d.ts +0 -11
  152. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  153. package/dist/board/render/sketch-host.d.ts +0 -14
  154. package/dist/board/render/sketch-host.js +0 -216
  155. package/dist/types.d.ts +0 -36
@@ -1,18 +1,25 @@
1
- import { cameraForFocus, cameraForState, screenPoint, screenToWorld, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
1
+ import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
+ import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
+ import { screenPoint, screenToWorld, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
4
+ import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
5
+ import { boardScriptTarget } from "../model/script-hit.js";
6
+ import { createBoardAnimationSampler, resolveBoardAnimationCamera } from "../model/sampling.js";
2
7
  import { resolveSceneArrow } from "../model/arrow-geometry.js";
3
8
  import { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../model/palette.js";
4
- import { featuredTaskArtifact } from "../model/task.js";
5
9
  import { isCreationBoardTool } from "../editor/tool.js";
6
10
  import { resizeCursorForHandle } from "../editor/selection-transform.js";
7
11
  import { parseBoardCssColor } from "../render/css-color.js";
12
+ import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
8
13
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
9
14
  import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
10
- import { defaultBoardPalette } from "../render/palette.js";
11
- import { getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
12
- import { createBoardSketchHost } from "../render/sketch-host.js";
15
+ import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
16
+ import { createBoardScriptHost } from "../render/script-host.js";
13
17
  import { createBoardPlayer } from "../player/index.js";
18
+ import { createBoardScriptObservers } from "../model/script-observers.js";
14
19
  import { toBoardPointerEvent } from "./pointer.js";
15
20
  import { createBoardScene } from "./scene.js";
21
+ import { scheduleBoardMaintenance } from "./maintenance.js";
22
+ import { createBoardScriptPointerRouter } from "./script-pointer.js";
16
23
  import { createBoardItemViewLayer } from "./views.js";
17
24
  import { Application, Container, Graphics, RendererType } from "pixi.js";
18
25
  //#region src/board/stage/stage.ts
@@ -45,11 +52,43 @@ function mountBoardStage(host, options) {
45
52
  let overlay = null;
46
53
  let background = null;
47
54
  let scene = null;
48
- let sketches = null;
55
+ let scripts = null;
56
+ let scriptMode = editor.readonly ? "interact" : "edit";
57
+ let scriptTime = 0;
58
+ let scriptLastTick = performance.now();
59
+ const scriptOutputs = /* @__PURE__ */ new Map();
60
+ const interactiveScripts = /* @__PURE__ */ new Set();
61
+ const screenScripts = /* @__PURE__ */ new Set();
62
+ const scriptObservers = createBoardScriptObservers((owner, event) => scripts?.event(owner, event));
63
+ let stopTransforms = null;
64
+ function syncTransformSubscription() {
65
+ if (scriptObservers.transforms && !stopTransforms) stopTransforms = editor.subscribeTransforms((event) => {
66
+ if (active && scriptMode !== "paused") scriptObservers.transform(editor.document, event);
67
+ });
68
+ else if (!scriptObservers.transforms && stopTransforms) {
69
+ stopTransforms();
70
+ stopTransforms = null;
71
+ }
72
+ }
73
+ const scriptListeners = /* @__PURE__ */ new Set();
74
+ let scriptVersion = 0;
75
+ let composedVersion = -1;
76
+ let composedZoom = 0;
77
+ let composedPlayhead = "";
78
+ let composedMoving = false;
79
+ let composedGesture = false;
80
+ let composedDocument = null;
81
+ let scriptHover = null;
82
+ const scriptPointers = createBoardScriptPointerRouter();
83
+ let stopMaintenance = null;
49
84
  let active = true;
50
85
  let disposed = false;
51
86
  let renderFrame = 0;
52
87
  let tickFrame = 0;
88
+ let cardTickFrame = 0;
89
+ let cardTickTimer = null;
90
+ let cardTickDue = 0;
91
+ const staleIds = /* @__PURE__ */ new Set();
53
92
  let resizeFrame = 0;
54
93
  let cardEpoch = 0;
55
94
  let assetVersion = 0;
@@ -63,7 +102,10 @@ function mountBoardStage(host, options) {
63
102
  const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera);
64
103
  const player = createBoardPlayer({ resolveColor: (color) => {
65
104
  const current = theme();
66
- return resolveItemColor(color, "brand", current.colors, current.colorScheme, parseBoardCssColor);
105
+ return {
106
+ color: resolveItemColor(color, "brand", current.colors, current.colorScheme, parseBoardCssColor),
107
+ alpha: itemAlpha(current, color)
108
+ };
67
109
  } });
68
110
  let cull = {
69
111
  rect: null,
@@ -77,7 +119,7 @@ function mountBoardStage(host, options) {
77
119
  if (same && cull.scene === editor.scene) return cull.ids;
78
120
  cull = {
79
121
  rect: same ? cull.rect : rect,
80
- ids: new Set(editor.idsInRect(rect)),
122
+ ids: new Set(editor.idsInRect(rect, true)),
81
123
  scene: editor.scene
82
124
  };
83
125
  options.onVisibleChange?.(cull.ids);
@@ -112,7 +154,7 @@ function mountBoardStage(host, options) {
112
154
  settings: editor.settings,
113
155
  scene: renderScene,
114
156
  time,
115
- ...sketches ? { sketches } : {},
157
+ ...scripts ? { scripts } : {},
116
158
  selectedIds: new Set(editor.selection),
117
159
  hoveredId: editor.hoverId,
118
160
  resizingIds: interaction.type === "resizing" ? new Set(interaction.origin.keys()) : /* @__PURE__ */ new Set(),
@@ -121,6 +163,9 @@ function mountBoardStage(host, options) {
121
163
  colorScheme: current.colorScheme,
122
164
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
123
165
  zoom: renderCamera().zoom,
166
+ reducedMotion: options.reducedMotion?.() ?? false,
167
+ labels: options.labels?.() ?? DEFAULT_BOARD_RENDER_LABELS,
168
+ taskRun: options.taskRun ?? (() => null),
124
169
  assetKey: assets.assetKey,
125
170
  getTexture: (key) => assets.getTexture(key),
126
171
  hasError: (key) => assets.hasError(key),
@@ -175,28 +220,11 @@ function mountBoardStage(host, options) {
175
220
  return strokes;
176
221
  }
177
222
  function followCamera(frame, renderScene) {
178
- let camera = { ...editor.camera };
179
- const focus = frame.camera.focus;
180
- if (focus !== void 0 && surface.width > 0 && surface.height > 0) camera = cameraForFocus(focus, (id) => renderScene.get(id)?.frame, surface, { padding: 48 }) ?? camera;
181
- if (frame.camera.zoom !== void 0) camera = {
182
- ...camera,
183
- zoom: frame.camera.zoom
184
- };
185
- if (frame.camera.center) camera = cameraForState({
186
- centerX: frame.camera.center.x,
187
- centerY: frame.camera.center.y,
188
- zoom: camera.zoom
189
- }, surface);
190
- if (frame.camera.shake) {
191
- const t = frame.time / 1e3;
192
- camera = {
193
- ...camera,
194
- x: camera.x + Math.sin(t * 91.3) * frame.camera.shake,
195
- y: camera.y + Math.cos(t * 77.9) * frame.camera.shake
196
- };
197
- }
198
- return camera;
223
+ return resolveBoardAnimationCamera(frame.camera, (id) => renderScene.get(id)?.frame, surface, frame.time, editor.camera);
199
224
  }
225
+ let effectSampler;
226
+ let effectDocument;
227
+ let effectAnimation = null;
200
228
  function arrowHead(target, from, to, color, width, inv, alpha) {
201
229
  const angle = Math.atan2(to.y - from.y, to.x - from.x);
202
230
  const head = Math.max(14, 16 * inv);
@@ -248,6 +276,10 @@ function mountBoardStage(host, options) {
248
276
  }
249
277
  }
250
278
  function cursorFor() {
279
+ if (scriptMode === "interact" && scriptHover) {
280
+ const output = scriptOutputs.get(scriptHover.owner);
281
+ return output?.hits[scriptHover.target]?.cursor ?? (output?.listens.includes("click") ? "pointer" : "default");
282
+ }
251
283
  const interaction = editor.interaction;
252
284
  switch (interaction.type) {
253
285
  case "panning":
@@ -272,6 +304,28 @@ function mountBoardStage(host, options) {
272
304
  app?.render();
273
305
  });
274
306
  }
307
+ function scheduleCardTick(wait) {
308
+ if (!active || disposed) return;
309
+ const due = performance.now() + wait;
310
+ if (cardTickFrame || cardTickTimer !== null && cardTickDue <= due) return;
311
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
312
+ cardTickTimer = null;
313
+ if (wait > 0) {
314
+ cardTickDue = due;
315
+ cardTickTimer = setTimeout(() => {
316
+ cardTickTimer = null;
317
+ scheduleCardTick(0);
318
+ }, wait);
319
+ return;
320
+ }
321
+ cardTickFrame = requestAnimationFrame(() => {
322
+ cardTickFrame = 0;
323
+ if (!scene) return;
324
+ const next = scene.tick(performance.now());
325
+ scheduleRender();
326
+ if (next !== null) scheduleCardTick(next);
327
+ });
328
+ }
275
329
  function scheduleTick() {
276
330
  if (tickFrame || !active || disposed) return;
277
331
  tickFrame = requestAnimationFrame(() => {
@@ -279,9 +333,89 @@ function mountBoardStage(host, options) {
279
333
  sync();
280
334
  });
281
335
  }
336
+ function cancelScriptPointers(owner, retained) {
337
+ scriptPointers.cancelScripts((pointerId, target) => {
338
+ scripts?.event(target.owner, {
339
+ type: "pointercancel",
340
+ target: target.target,
341
+ pointerId
342
+ });
343
+ }, owner, retained);
344
+ if (!retained || scriptHover && scriptHover.owner === owner && !Object.hasOwn(retained, scriptHover.target)) scriptHover = null;
345
+ }
346
+ function createScripts() {
347
+ return createBoardScriptHost({
348
+ host,
349
+ capabilities: {
350
+ ...options.scriptCapabilities,
351
+ "board.emit": async (id, value) => {
352
+ const message = value;
353
+ if (!message || typeof message.target !== "string" || typeof message.type !== "string" || !boardScriptScope(editor.document, id).has(message.target)) throw new Error("Invalid script event target.");
354
+ scripts?.event(message.target, {
355
+ type: message.type,
356
+ data: message.data
357
+ });
358
+ },
359
+ ...!editor.readonly ? { "board.commit": async (id, value) => {
360
+ validateBoardScriptCommit(editor.document, id, value);
361
+ await editor.commitPatch(value);
362
+ } } : {}
363
+ },
364
+ onOutput: (id, output) => {
365
+ if (output) {
366
+ prepareBoardScriptOutput(editor.document, id, output);
367
+ cancelScriptPointers(id, output.hits);
368
+ scriptOutputs.set(id, output);
369
+ if (Object.keys(output.hits).length) interactiveScripts.add(id);
370
+ else interactiveScripts.delete(id);
371
+ if (output.space === "screen" && Object.keys(output.items).length) screenScripts.add(id);
372
+ else screenScripts.delete(id);
373
+ } else {
374
+ cancelScriptPointers(id);
375
+ scriptOutputs.delete(id);
376
+ interactiveScripts.delete(id);
377
+ screenScripts.delete(id);
378
+ }
379
+ scriptObservers.update(id, output);
380
+ syncTransformSubscription();
381
+ scriptVersion++;
382
+ scheduleTick();
383
+ },
384
+ readModule: async (src) => {
385
+ const url = await options.source.resolveFileUrl(src);
386
+ if (!url) throw new Error(`${src} is not available.`);
387
+ const response = await fetch(url, { signal: AbortSignal.timeout(15e3) });
388
+ if (!response.ok) throw new Error(`${src} could not be read.`);
389
+ return response.text();
390
+ },
391
+ onFrame: () => {
392
+ scheduleTick();
393
+ for (const listener of scriptListeners) listener();
394
+ }
395
+ });
396
+ }
282
397
  function sync() {
283
398
  if (!app || !world || !scene || !overlay) return;
284
399
  const current = theme();
400
+ const hasScripts = editor.hasScripts;
401
+ if (hasScripts && !scripts) {
402
+ scripts = createScripts();
403
+ scriptTime = 0;
404
+ scriptLastTick = performance.now();
405
+ } else if (!hasScripts && scripts) {
406
+ const previous = scripts;
407
+ scripts = null;
408
+ previous.destroy();
409
+ scriptObservers.clear();
410
+ syncTransformSubscription();
411
+ interactiveScripts.clear();
412
+ screenScripts.clear();
413
+ scriptOutputs.clear();
414
+ editor.setScriptItems(/* @__PURE__ */ new Map());
415
+ scriptHover = null;
416
+ composedVersion = -1;
417
+ composedDocument = null;
418
+ }
285
419
  syncBackground(current);
286
420
  player.enter(editor.consumeRecentlyAdded(), editor.document);
287
421
  const playback = options.playback?.() ?? null;
@@ -290,15 +424,61 @@ function mountBoardStage(host, options) {
290
424
  const moving = new Map(previews);
291
425
  for (const [id, item] of frame.items) moving.set(id, item);
292
426
  editor.setPlayedItems(moving);
293
- const renderScene = editor.scene;
294
- playbackCamera = editor.cameraPolicy === "follow" && Object.keys(frame.camera).length > 0 ? followCamera(frame, renderScene) : null;
427
+ const animationId = editor.playhead?.animationId ?? (frame.playbackTime !== null ? playback?.animationId : null) ?? null;
428
+ if (animationId && (effectDocument !== editor.document || effectAnimation !== animationId)) {
429
+ effectDocument = editor.document;
430
+ effectAnimation = animationId;
431
+ effectSampler = createBoardAnimationSampler(editor.document, animationId);
432
+ }
433
+ if (editor.playhead && effectSampler) {
434
+ frame.camera = effectSampler.at(editor.playhead.time).evaluation.camera;
435
+ frame.time = editor.playhead.time;
436
+ }
437
+ playbackCamera = editor.cameraPolicy === "follow" && Object.keys(frame.camera).length > 0 ? followCamera(frame, editor.scene) : null;
295
438
  const camera = renderCamera();
439
+ if (scripts && (scriptOutputs.size || composedVersion !== scriptVersion)) {
440
+ const playheadKey = `${editor.playhead?.animationId ?? ""}:${editor.playhead?.time ?? ""}`;
441
+ if (scriptVersion !== composedVersion || composedDocument !== editor.document || screenScripts.size > 0 && editor.gestureActive || composedGesture || moving.size || composedMoving || composedZoom !== camera.zoom || composedPlayhead !== playheadKey) {
442
+ const animationItems = editor.animationItems();
443
+ const composed = composeBoardScriptItems(editor.document, editor.scene, scriptOutputs, animationItems, camera, (owner, error) => scripts?.fault(owner, error instanceof Error ? error.message : String(error)), editor.gestureActive ? editor.scene.layout : void 0);
444
+ const scriptedIds = new Set([...scriptOutputs.values()].flatMap((output) => Object.keys(output.patches)));
445
+ for (const id of animationItems.keys()) if (!scriptedIds.has(id)) composed.delete(id);
446
+ editor.setScriptItems(composed);
447
+ composedMoving = moving.size > 0;
448
+ composedPlayhead = playheadKey;
449
+ composedZoom = camera.zoom;
450
+ composedGesture = screenScripts.size > 0 && editor.gestureActive;
451
+ composedVersion = scriptVersion;
452
+ composedDocument = editor.document;
453
+ }
454
+ }
455
+ const renderScene = editor.scene;
456
+ if (scripts) {
457
+ const now = performance.now();
458
+ if (scriptMode !== "paused") scriptTime += Math.min(250, Math.max(0, now - scriptLastTick));
459
+ scriptLastTick = now;
460
+ if (scriptMode !== "paused" && scriptObservers.size) scriptObservers.sync(editor.document, editor.selection, () => ({
461
+ camera: { ...camera },
462
+ ...surface,
463
+ safeRect: options.safeRect?.() ?? {
464
+ x: 0,
465
+ y: 0,
466
+ ...surface
467
+ }
468
+ }));
469
+ scripts.sync(editor.document, renderScene, camera, scriptTime, scriptMode === "interact", visibleIds());
470
+ }
296
471
  world.position.set(camera.x, camera.y);
297
472
  world.scale.set(camera.zoom);
298
473
  const ids = visibleIds();
299
474
  syncAssets(ids);
300
475
  const context = renderContext(current, renderScene, frame.time);
476
+ if (animationId && effectSampler) {
477
+ context.time = editor.playhead?.time ?? frame.time;
478
+ context.frameAt = effectSampler.frameAt;
479
+ }
301
480
  const pinnedIds = new Set(editor.selection);
481
+ for (const item of renderScene.items) if (item.type === "effect" && item.props.kind === "trail" && renderScene.opacity(item.id) > 0) pinnedIds.add(item.id);
302
482
  for (const id of previews.keys()) pinnedIds.add(id);
303
483
  if (editor.editingId) pinnedIds.add(editor.editingId);
304
484
  for (const id of frame.items.keys()) {
@@ -320,10 +500,12 @@ function mountBoardStage(host, options) {
320
500
  current.key,
321
501
  textZoomBucket(camera.zoom)
322
502
  ].join("|"),
503
+ staleIds,
323
504
  structureVersion: editor.structureVersion,
324
505
  geometryVersion: editor.geometryVersion,
325
506
  gestureActive: editor.gestureActive && editor.interaction.type !== "panning"
326
507
  });
508
+ staleIds.clear();
327
509
  const single = editor.selection.length === 1 ? renderScene.get(editor.selection[0]) : null;
328
510
  let arrowEndpoints;
329
511
  if (single?.type === "arrow" && !single.locked) {
@@ -366,7 +548,9 @@ function mountBoardStage(host, options) {
366
548
  const nextCursor = cursorFor();
367
549
  if (nextCursor !== cursor) host.style.cursor = cursor = nextCursor;
368
550
  scheduleRender();
369
- if (frame.running || scene.animated) scheduleTick();
551
+ if (frame.running || scene.animated || scriptMode !== "paused" && scripts?.animated) scheduleTick();
552
+ const cardWait = scene.tick(performance.now());
553
+ if (cardWait !== null) scheduleCardTick(cardWait);
370
554
  }
371
555
  function resize() {
372
556
  cancelAnimationFrame(resizeFrame);
@@ -387,10 +571,76 @@ function mountBoardStage(host, options) {
387
571
  function toInput(event) {
388
572
  return toBoardPointerEvent(event, rectOf(), renderCamera(), event.type === "pointermove" && editor.interaction.type === "drawing");
389
573
  }
574
+ function scriptTarget(input) {
575
+ if (!scriptOutputs.size) return null;
576
+ const point = input.world;
577
+ const candidates = editor.idsInRect({
578
+ x: point.x - .01,
579
+ y: point.y - .01,
580
+ width: .02,
581
+ height: .02
582
+ }, true);
583
+ return boardScriptTarget(editor.scene, candidates, point, scriptOutputs, editor.registry.hitTest);
584
+ }
585
+ function sendScriptEvent(target, type, event, input, related) {
586
+ if (!scripts) return;
587
+ const point = applyMatrix(invertMatrix(editor.scene.layout.matrix(target.owner)), input.world);
588
+ scripts?.event(target.owner, {
589
+ type,
590
+ target: target.target,
591
+ relatedTarget: related?.target,
592
+ relatedOwner: related?.owner,
593
+ point,
594
+ pointerId: event.pointerId,
595
+ pointerType: event.pointerType,
596
+ buttons: event.buttons,
597
+ shiftKey: event.shiftKey,
598
+ altKey: event.altKey,
599
+ ctrlKey: event.ctrlKey,
600
+ metaKey: event.metaKey
601
+ });
602
+ }
603
+ function scriptPointer(type, event, input) {
604
+ if (!scriptPointers.needsRouting(event.pointerId, interactiveScripts.size > 0)) return false;
605
+ const enabled = scriptMode === "interact" && !editor.gestureActive && !editor.spaceHeld && event.button !== 1;
606
+ const target = type === "leave" || !enabled || scriptPointers.owner(event.pointerId) === "editor" ? null : scriptTarget(input);
607
+ if (target?.owner !== scriptHover?.owner || target?.target !== scriptHover?.target) {
608
+ if (scriptHover) sendScriptEvent(scriptHover, "pointerleave", event, input, target);
609
+ if (target) sendScriptEvent(target, "pointerenter", event, input, scriptHover);
610
+ scriptHover = target;
611
+ }
612
+ const consumed = scriptPointers.owner(event.pointerId) === "script";
613
+ const { recipient, click } = scriptPointers.route(type, event.pointerId, target, {
614
+ x: event.clientX,
615
+ y: event.clientY
616
+ }, event.type !== "pointerup");
617
+ if (type === "down" && recipient) {
618
+ host.setPointerCapture(event.pointerId);
619
+ host.focus({ preventScroll: true });
620
+ }
621
+ if (recipient && type !== "leave") sendScriptEvent(recipient, input.cancelled ? "pointercancel" : `pointer${type}`, event, input);
622
+ if (click) sendScriptEvent(click, "click", event, input);
623
+ return consumed || recipient !== null;
624
+ }
625
+ function handleScriptKey(event) {
626
+ if (scriptMode !== "interact" || !scriptHover || event.target !== host || !scriptOutputs.get(scriptHover.owner)?.listens.includes(event.type)) return;
627
+ const input = {
628
+ type: event.type,
629
+ target: scriptHover.target,
630
+ key: event.key,
631
+ shiftKey: event.shiftKey,
632
+ altKey: event.altKey,
633
+ ctrlKey: event.ctrlKey,
634
+ metaKey: event.metaKey
635
+ };
636
+ scripts?.event(scriptHover.owner, input);
637
+ event.stopPropagation();
638
+ }
390
639
  function handlePointerDown(event) {
391
640
  if (inViews(event)) return;
392
641
  editor.takeCameraControl();
393
642
  const input = toInput(event);
643
+ if (scriptPointer("down", event, input)) return;
394
644
  if (options.onPointer?.("down", event, input) === false) return;
395
645
  host.setPointerCapture(event.pointerId);
396
646
  editor.pointerDown(input);
@@ -398,18 +648,21 @@ function mountBoardStage(host, options) {
398
648
  function handlePointerMove(event) {
399
649
  if (inViews(event)) return;
400
650
  const input = toInput(event);
651
+ if (scriptPointer("move", event, input)) return;
401
652
  if (options.onPointer?.("move", event, input) === false) return;
402
653
  editor.pointerMove(input);
403
654
  }
404
655
  function handlePointerUp(event) {
405
656
  if (inViews(event)) return;
406
657
  const input = toInput(event);
658
+ if (scriptPointer("up", event, input)) return;
407
659
  if (options.onPointer?.("up", event, input) === false) return;
408
660
  editor.pointerUp(input);
409
661
  if (event.type === "pointercancel" || event.pointerType !== "mouse") editor.pointerLeave();
410
662
  }
411
663
  function handlePointerLeave(event) {
412
664
  if (event.buttons !== 0) return;
665
+ scriptPointer("leave", event, toInput(event));
413
666
  if (options.onPointer?.("leave", event, toInput(event)) === false) return;
414
667
  editor.pointerLeave();
415
668
  }
@@ -436,6 +689,8 @@ function mountBoardStage(host, options) {
436
689
  else if (!item) editor.beginTextDraft(point);
437
690
  }
438
691
  const listeners = [
692
+ ["keydown", handleScriptKey],
693
+ ["keyup", handleScriptKey],
439
694
  ["pointerdown", handlePointerDown],
440
695
  ["pointermove", handlePointerMove],
441
696
  ["pointerup", handlePointerUp],
@@ -478,13 +733,22 @@ function mountBoardStage(host, options) {
478
733
  return false;
479
734
  }
480
735
  app = instance;
481
- instance.canvas.style.cssText = "position:relative;z-index:1;display:block";
736
+ stopMaintenance = scheduleBoardMaintenance(instance.renderer);
737
+ Object.assign(instance.canvas.style, {
738
+ position: "relative",
739
+ zIndex: "1",
740
+ display: "block",
741
+ touchAction: "none"
742
+ });
482
743
  host.insertBefore(instance.canvas, views.element);
483
744
  world = new Container({
484
745
  isRenderGroup: true,
485
746
  label: "board-world"
486
747
  });
487
- const nodes = new Container({ label: "board-nodes" });
748
+ const nodes = new Container({
749
+ isRenderGroup: true,
750
+ label: "board-nodes"
751
+ });
488
752
  overlay = new Graphics({ label: "board-interaction-overlay" });
489
753
  world.addChild(nodes, overlay);
490
754
  instance.stage.addChild(world);
@@ -494,16 +758,6 @@ function mountBoardStage(host, options) {
494
758
  getRenderer: resolveRenderer,
495
759
  onFarLayerFrame: scheduleTick
496
760
  });
497
- sketches = createBoardSketchHost({
498
- readModule: async (src) => {
499
- const url = await options.source.resolveFileUrl(src);
500
- if (!url) throw new Error(`${src} is not available.`);
501
- const response = await fetch(url);
502
- if (!response.ok) throw new Error(`${src} could not be read.`);
503
- return response.text();
504
- },
505
- onFrame: () => sync()
506
- });
507
761
  for (const [type, listener, listenerOptions] of listeners) host.addEventListener(type, listener, listenerOptions);
508
762
  resizeObserver.observe(host);
509
763
  resize();
@@ -518,17 +772,56 @@ function mountBoardStage(host, options) {
518
772
  get theme() {
519
773
  return theme();
520
774
  },
775
+ setScriptMode(mode) {
776
+ if (mode === scriptMode) return;
777
+ if (mode !== "interact") cancelScriptPointers();
778
+ scriptMode = mode;
779
+ scriptLastTick = performance.now();
780
+ scheduleTick();
781
+ },
782
+ resetScripts() {
783
+ scriptTime = 0;
784
+ scripts?.invalidate();
785
+ },
786
+ invalidateScript(path) {
787
+ scripts?.invalidate(path);
788
+ },
789
+ scriptOutput: (id) => scripts?.output(id) ?? null,
790
+ async snapshotScripts() {
791
+ const result = {};
792
+ await Promise.all([...scriptOutputs.keys()].map(async (id) => {
793
+ const snapshot = await scripts?.snapshot(id);
794
+ if (snapshot) result[id] = snapshot;
795
+ }));
796
+ return result;
797
+ },
798
+ restoreScript(id, snapshot) {
799
+ scripts?.restore(id, snapshot);
800
+ },
801
+ subscribeScripts(listener) {
802
+ scriptListeners.add(listener);
803
+ return () => {
804
+ scriptListeners.delete(listener);
805
+ };
806
+ },
807
+ scriptError: (id) => scripts?.error(id) ?? null,
521
808
  setActive(next) {
522
809
  if (active === next) return;
523
810
  active = next;
524
811
  if (!active) {
812
+ cancelScriptPointers();
525
813
  cancelAnimationFrame(renderFrame);
526
814
  cancelAnimationFrame(tickFrame);
815
+ cancelAnimationFrame(cardTickFrame);
816
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
527
817
  renderFrame = 0;
528
818
  tickFrame = 0;
819
+ cardTickFrame = 0;
820
+ cardTickTimer = null;
529
821
  return;
530
822
  }
531
823
  player.reset();
824
+ scriptLastTick = performance.now();
532
825
  resize();
533
826
  },
534
827
  invalidate(invalidation) {
@@ -536,24 +829,34 @@ function mountBoardStage(host, options) {
536
829
  cardEpoch += 1;
537
830
  requestAssets = true;
538
831
  }
832
+ for (const id of invalidation?.items ?? []) staleIds.add(id);
539
833
  scheduleTick();
540
834
  },
541
835
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
542
836
  toWorld: (clientX, clientY) => screenToWorld(clientX, clientY, rectOf(), renderCamera()),
543
837
  destroy() {
544
838
  disposed = true;
839
+ scriptPointers.clear();
840
+ stopTransforms?.();
841
+ scriptObservers.clear();
842
+ scriptListeners.clear();
843
+ stopMaintenance?.();
545
844
  stopEditor();
546
845
  stopAssets();
547
846
  resizeObserver.disconnect();
548
847
  cancelAnimationFrame(resizeFrame);
549
848
  cancelAnimationFrame(renderFrame);
550
849
  cancelAnimationFrame(tickFrame);
850
+ cancelAnimationFrame(cardTickFrame);
851
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
852
+ cardTickTimer = null;
551
853
  for (const [type, listener] of listeners) host.removeEventListener(type, listener);
552
854
  editor.setPlayedItems(/* @__PURE__ */ new Map());
855
+ editor.setScriptItems(/* @__PURE__ */ new Map());
553
856
  scene?.destroy(renderContext(theme(), editor.scene, 0));
554
857
  scene = null;
555
- sketches?.destroy();
556
- sketches = null;
858
+ scripts?.destroy();
859
+ scripts = null;
557
860
  background?.destroy({ children: true });
558
861
  background = null;
559
862
  views.destroy();
@@ -34,11 +34,10 @@ async function loadUncachedTexture(url) {
34
34
  */
35
35
  async function loadImageTexture(url) {
36
36
  try {
37
- const asset = /[?&]x-oss-process=/.test(url) ? {
37
+ return await Assets.load({
38
38
  src: url,
39
39
  parser: "texture"
40
- } : url;
41
- return await Assets.load(asset);
40
+ });
42
41
  } catch (workerError) {
43
42
  const errors = [workerError];
44
43
  for (const load of [loadImageElementTexture, loadUncachedTexture]) try {