@neta-art/cohub 11.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 (71) hide show
  1. package/README.md +31 -5
  2. package/dist/board/editor/creation.d.ts +2 -2
  3. package/dist/board/editor/creation.js +3 -3
  4. package/dist/board/editor/editor.d.ts +4 -4
  5. package/dist/board/editor/editor.js +1 -1
  6. package/dist/board/editor/index.d.ts +2 -2
  7. package/dist/board/editor/interaction.d.ts +8 -2
  8. package/dist/board/editor/interaction.js +34 -1
  9. package/dist/board/editor/snapshots.d.ts +0 -2
  10. package/dist/board/editor/snapshots.js +4 -20
  11. package/dist/board/export/scene.js +4 -1
  12. package/dist/board/model/image-key.js +1 -1
  13. package/dist/board/model/index.d.ts +8 -7
  14. package/dist/board/model/index.js +6 -5
  15. package/dist/board/model/items/create.d.ts +16 -3
  16. package/dist/board/model/items/create.js +125 -5
  17. package/dist/board/model/items/index.d.ts +2 -2
  18. package/dist/board/model/items/index.js +2 -2
  19. package/dist/board/model/media-playback.js +1 -1
  20. package/dist/board/model/overlay-placement.d.ts +25 -0
  21. package/dist/board/model/overlay-placement.js +35 -0
  22. package/dist/board/model/replay.d.ts +1 -1
  23. package/dist/board/model/replay.js +1 -0
  24. package/dist/board/model/script-hit.d.ts +20 -3
  25. package/dist/board/model/script-hit.js +81 -24
  26. package/dist/board/model/script-observers.d.ts +18 -0
  27. package/dist/board/model/script-observers.js +92 -0
  28. package/dist/board/model/script-scene.d.ts +1 -1
  29. package/dist/board/model/script-scene.js +35 -8
  30. package/dist/board/model/script.d.ts +51 -7
  31. package/dist/board/render/board-background.js +5 -4
  32. package/dist/board/render/index.d.ts +2 -2
  33. package/dist/board/render/index.js +2 -2
  34. package/dist/board/render/media-interaction.js +1 -1
  35. package/dist/board/render/renderers/board-renderer-registry.d.ts +17 -2
  36. package/dist/board/render/renderers/board-renderer-registry.js +8 -1
  37. package/dist/board/render/renderers/custom-card-renderer.js +2 -1
  38. package/dist/board/render/renderers/task-card-renderer.d.ts +2 -1
  39. package/dist/board/render/renderers/task-card-renderer.js +220 -71
  40. package/dist/board/render/script-host.d.ts +1 -1
  41. package/dist/board/render/script-host.js +53 -28
  42. package/dist/board/render/script-runtime.js +13 -3
  43. package/dist/board/render/script-texture.d.ts +11 -0
  44. package/dist/board/render/script-texture.js +33 -0
  45. package/dist/board/stage/assets.js +1 -1
  46. package/dist/board/stage/scene.d.ts +2 -0
  47. package/dist/board/stage/scene.js +17 -1
  48. package/dist/board/stage/script-pointer.d.ts +1 -0
  49. package/dist/board/stage/script-pointer.js +8 -2
  50. package/dist/board/stage/stage.d.ts +11 -3
  51. package/dist/board/stage/stage.js +118 -21
  52. package/dist/chunks/environment.d.ts +40 -5
  53. package/dist/chunks/environment.js +12 -4
  54. package/dist/chunks/http.d.ts +3 -1
  55. package/dist/chunks/http.js +2 -1
  56. package/dist/chunks/transport.js +1 -1
  57. package/dist/chunks/websocket.d.ts +1 -1
  58. package/dist/http.d.ts +2 -2
  59. package/dist/index.d.ts +2 -2
  60. package/dist/index.js +151 -139
  61. package/dist/protocol/dist/board-model.d.ts +36 -1
  62. package/dist/protocol/dist/board-model.js +14 -4
  63. package/dist/protocol/dist/board-task.d.ts +22 -0
  64. package/dist/{board/model/task.js → protocol/dist/board-task.js} +34 -7
  65. package/dist/protocol/dist/generation/blocks.d.ts +8 -0
  66. package/dist/{generation-blocks.js → protocol/dist/generation/blocks.js} +14 -15
  67. package/dist/protocol/dist/index.d.ts +5 -2
  68. package/dist/protocol/dist/task/index.d.ts +4 -0
  69. package/package.json +1 -1
  70. package/dist/board/model/task.d.ts +0 -11
  71. package/dist/types.d.ts +0 -36
@@ -33,6 +33,8 @@ function createScriptRuntime(module, initial, host) {
33
33
  let patches = effects?.patches ?? {};
34
34
  let items = effects?.items ?? {};
35
35
  let space = effects?.space ?? "world";
36
+ let anchor = effects?.anchor;
37
+ let view = null;
36
38
  let hits = effects?.hits ?? {};
37
39
  const invalidate = () => {
38
40
  if (disposed) return;
@@ -69,6 +71,9 @@ function createScriptRuntime(module, initial, host) {
69
71
  get frame() {
70
72
  return frame;
71
73
  },
74
+ get view() {
75
+ return view;
76
+ },
72
77
  random() {
73
78
  randomState = Math.imul(randomState, 1664525) + 1013904223 >>> 0;
74
79
  return randomState / 4294967296;
@@ -79,10 +84,11 @@ function createScriptRuntime(module, initial, host) {
79
84
  listeners = /* @__PURE__ */ new Set();
80
85
  handlers.set(type, listeners);
81
86
  }
82
- listeners.add(handler);
87
+ const listener = (event) => handler(event);
88
+ listeners.add(listener);
83
89
  invalidate();
84
90
  return () => {
85
- listeners.delete(handler);
91
+ listeners.delete(listener);
86
92
  invalidate();
87
93
  };
88
94
  },
@@ -101,6 +107,7 @@ function createScriptRuntime(module, initial, host) {
101
107
  scene(next, options) {
102
108
  items = structuredClone(next);
103
109
  space = options?.space ?? "world";
110
+ anchor = options?.anchor ? structuredClone(options.anchor) : void 0;
104
111
  invalidate();
105
112
  },
106
113
  hitRegions(next) {
@@ -162,6 +169,7 @@ function createScriptRuntime(module, initial, host) {
162
169
  },
163
170
  event(event) {
164
171
  if (disposed) return;
172
+ if (event.type === "viewchange") view = event.data;
165
173
  const listeners = handlers.get(event.type);
166
174
  if (!listeners?.size) return;
167
175
  for (const handler of listeners) handler(event);
@@ -175,6 +183,7 @@ function createScriptRuntime(module, initial, host) {
175
183
  return {
176
184
  parameters: module.parameters,
177
185
  space,
186
+ anchor,
178
187
  patches,
179
188
  items,
180
189
  hits,
@@ -193,7 +202,8 @@ function createScriptRuntime(module, initial, host) {
193
202
  patches,
194
203
  items,
195
204
  hits,
196
- space
205
+ space,
206
+ anchor
197
207
  } : void 0
198
208
  };
199
209
  return clone ? structuredClone(snapshot) : snapshot;
@@ -0,0 +1,11 @@
1
+ import { Texture } from "pixi.js";
2
+ //#region src/board/render/script-texture.d.ts
3
+ declare class BoardScriptTexture {
4
+ private texture;
5
+ private bitmap;
6
+ get current(): Texture | null;
7
+ update(bitmap: ImageBitmap): void;
8
+ destroy(): void;
9
+ }
10
+ //#endregion
11
+ export { BoardScriptTexture };
@@ -0,0 +1,33 @@
1
+ import { ImageSource, Texture } from "pixi.js";
2
+ //#region src/board/render/script-texture.ts
3
+ var BoardScriptTexture = class {
4
+ texture = null;
5
+ bitmap = null;
6
+ get current() {
7
+ return this.texture;
8
+ }
9
+ update(bitmap) {
10
+ const previous = this.bitmap;
11
+ this.bitmap = bitmap;
12
+ if (!this.texture) {
13
+ this.texture = new Texture({
14
+ source: new ImageSource({ resource: bitmap }),
15
+ dynamic: true
16
+ });
17
+ return;
18
+ }
19
+ const source = this.texture.source;
20
+ source.resource = bitmap;
21
+ source.update();
22
+ if (previous !== bitmap) previous?.close();
23
+ }
24
+ destroy() {
25
+ if (!this.texture) return;
26
+ this.texture.destroy(true);
27
+ this.bitmap?.close();
28
+ this.bitmap = null;
29
+ this.texture = null;
30
+ }
31
+ };
32
+ //#endregion
33
+ export { BoardScriptTexture };
@@ -1,6 +1,6 @@
1
1
  import { mediaPreviewCandidates } from "../../media.js";
2
2
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
3
- import { featuredTaskArtifact } from "../model/task.js";
3
+ import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
4
4
  import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
5
5
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
6
6
  import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
@@ -24,6 +24,7 @@ type SceneSyncInput = {
24
24
  visibleIds: Set<string> | null;
25
25
  pinnedIds: Set<string>;
26
26
  globalSig: string;
27
+ staleIds?: ReadonlySet<string>;
27
28
  structureVersion: number;
28
29
  geometryVersion: number;
29
30
  gestureActive: boolean;
@@ -72,6 +73,7 @@ type BoardScene = {
72
73
  sync: (input: SceneSyncInput) => void;
73
74
  getNode: (itemId: string) => BoardSceneNode | null;
74
75
  readonly animated: boolean;
76
+ tick: (clock: number) => number | null;
75
77
  drawOverlay: (input: SceneOverlayInput, palette: BoardRenderPalette) => void;
76
78
  destroy: (context: BoardRenderContext) => void;
77
79
  };
@@ -48,6 +48,7 @@ function createBoardScene(options) {
48
48
  const cards = /* @__PURE__ */ new Map();
49
49
  const clips = /* @__PURE__ */ new Map();
50
50
  const animatedIds = /* @__PURE__ */ new Set();
51
+ const tickingIds = /* @__PURE__ */ new Set();
51
52
  const pools = /* @__PURE__ */ new Map();
52
53
  const heldKeys = /* @__PURE__ */ new Map();
53
54
  let orderById = /* @__PURE__ */ new Map();
@@ -72,6 +73,7 @@ function createBoardScene(options) {
72
73
  setHeldKey(context, id, null);
73
74
  cards.delete(id);
74
75
  animatedIds.delete(id);
76
+ tickingIds.delete(id);
75
77
  if (destroyed) return;
76
78
  entry.container.parent?.removeChild(entry.container);
77
79
  entry.container.visible = false;
@@ -123,6 +125,7 @@ function createBoardScene(options) {
123
125
  container.alpha = scene.opacity(item.id);
124
126
  hostFor(item, scene).addChild(container);
125
127
  if (renderer.animated) animatedIds.add(item.id);
128
+ if (renderer.tick) tickingIds.add(item.id);
126
129
  const entry = {
127
130
  item,
128
131
  container,
@@ -352,7 +355,7 @@ function createBoardScene(options) {
352
355
  const hovered = context.hoveredId === id;
353
356
  const resizing = context.resizingIds.has(id);
354
357
  const scriptRevision = context.scripts?.revision?.(id) ?? 0;
355
- const changed = scriptRevision !== entry.scriptRevision || item !== entry.item || entry.renderer.animated || selected !== entry.selected || hovered !== entry.hovered || resizing !== entry.resizing || globalSig !== entry.globalSig;
358
+ const changed = input.staleIds?.has(id) || scriptRevision !== entry.scriptRevision || item !== entry.item || entry.renderer.animated || selected !== entry.selected || hovered !== entry.hovered || resizing !== entry.resizing || globalSig !== entry.globalSig;
356
359
  entry.scriptRevision = scriptRevision;
357
360
  entry.item = item;
358
361
  entry.selected = selected;
@@ -363,12 +366,23 @@ function createBoardScene(options) {
363
366
  if (changed) {
364
367
  entry.renderer.update(entry.container, item, context);
365
368
  entry.container.alpha = scene.opacity(id);
369
+ if (entry.renderer.tick) tickingIds.add(id);
366
370
  }
367
371
  }
368
372
  if (structureChanged || liveSetChanged) syncClips(scene);
369
373
  if (structureChanged || farModeChanged || liveSetChanged) applyChildOrder();
370
374
  syncLiveStrokes(input);
371
375
  }
376
+ function tick(clock) {
377
+ let next = null;
378
+ for (const id of tickingIds) {
379
+ const entry = cards.get(id);
380
+ const wait = entry?.container.visible ? entry.renderer.tick?.(entry.container, clock) ?? null : null;
381
+ if (wait === null) tickingIds.delete(id);
382
+ else next = next === null ? wait : Math.min(next, wait);
383
+ }
384
+ return next;
385
+ }
372
386
  function applyChildOrder() {
373
387
  for (const [id, entry] of cards) entry.container.zIndex = orderById.get(id) ?? 0;
374
388
  world.sortableChildren = true;
@@ -489,6 +503,7 @@ function createBoardScene(options) {
489
503
  for (const entry of clips.values()) entry.group.destroy({ children: true });
490
504
  clips.clear();
491
505
  animatedIds.clear();
506
+ tickingIds.clear();
492
507
  for (const id of [...liveStrokes.keys()]) removeLiveStroke(id, context);
493
508
  resetFarLayer();
494
509
  farActive = false;
@@ -507,6 +522,7 @@ function createBoardScene(options) {
507
522
  get animated() {
508
523
  return animatedIds.size > 0;
509
524
  },
525
+ tick,
510
526
  drawOverlay,
511
527
  destroy
512
528
  };
@@ -13,6 +13,7 @@ declare function createBoardScriptPointerRouter(): {
13
13
  recipient: BoardScriptPointerTarget | null;
14
14
  click: BoardScriptPointerTarget | null;
15
15
  };
16
+ cancelScripts(cancel: (pointerId: number, target: BoardScriptPointerTarget) => void, owner?: string, retained?: Readonly<Record<string, unknown>>): void;
16
17
  clear(): void;
17
18
  };
18
19
  //#endregion
@@ -15,17 +15,23 @@ function createBoardScriptPointerRouter() {
15
15
  ...point
16
16
  });
17
17
  const gesture = gestures.get(pointerId);
18
- const recipient = gesture ? gesture.target : phase === "move" ? target : null;
18
+ const recipient = gesture ? gesture.cancelled ? null : gesture.target : phase === "move" ? target : null;
19
19
  let click = null;
20
20
  if (phase === "up") {
21
21
  gestures.delete(pointerId);
22
- if (!cancelled && gesture?.target && target?.owner === gesture.target.owner && target.target === gesture.target.target && Math.hypot(point.x - gesture.x, point.y - gesture.y) < 5) click = gesture.target;
22
+ if (!cancelled && !gesture?.cancelled && gesture?.target && target?.owner === gesture.target.owner && target.target === gesture.target.target && Math.hypot(point.x - gesture.x, point.y - gesture.y) < 5) click = gesture.target;
23
23
  }
24
24
  return {
25
25
  recipient,
26
26
  click
27
27
  };
28
28
  },
29
+ cancelScripts(cancel, owner, retained) {
30
+ for (const [pointerId, gesture] of gestures) if (!gesture.cancelled && gesture.target && (!owner || gesture.target.owner === owner) && (!retained || !Object.hasOwn(retained, gesture.target.target))) {
31
+ gesture.cancelled = true;
32
+ cancel(pointerId, gesture.target);
33
+ }
34
+ },
29
35
  clear() {
30
36
  gestures.clear();
31
37
  }
@@ -1,8 +1,8 @@
1
- import { BoardItem } from "../../protocol/dist/board-model.js";
1
+ import { BoardItem, BoardTaskItem } from "../../protocol/dist/board-model.js";
2
2
  import { BoardPlaybackSnapshot } from "../../protocol/dist/board.js";
3
3
  import "../../protocol/dist/index.js";
4
+ import { BoardViewport, Rect, WorldPoint } from "../model/geometry.js";
4
5
  import { BoardScriptOutput, BoardScriptSnapshot } from "../model/script.js";
5
- import { BoardViewport, WorldPoint } from "../model/geometry.js";
6
6
  import { BoardScene, BoardSceneItem } from "../model/scene.js";
7
7
  import { BoardShapeColors } from "../model/palette.js";
8
8
  import { BoardAssetSource } from "../model/media-playback.js";
@@ -10,7 +10,7 @@ import "../model/index.js";
10
10
  import { BoardPointerEvent } from "../editor/interaction.js";
11
11
  import { BoardEditor } from "../editor/editor.js";
12
12
  import "../editor/index.js";
13
- import { BoardCardRendererResolver, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
13
+ import { BoardCardRendererResolver, BoardRenderLabels, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
14
14
  import "../render/index.js";
15
15
  import { BoardAssetManager } from "./assets.js";
16
16
  import { BoardLiveStroke } from "./scene.js";
@@ -32,6 +32,12 @@ type BoardStageOptions = {
32
32
  renderers?: BoardCardRendererResolver;
33
33
  views?: readonly BoardItemView[];
34
34
  theme?: () => BoardStageTheme;
35
+ labels?: () => BoardRenderLabels;
36
+ taskRun?: (taskRunId: string) => {
37
+ status: BoardTaskItem["props"]["snapshot"]["status"];
38
+ startedAt?: string;
39
+ } | null;
40
+ reducedMotion?: () => boolean;
35
41
  playback?: () => BoardPlaybackSnapshot | null;
36
42
  fileState?: (path: string) => "ok" | "missing" | "unavailable";
37
43
  previews?: () => ReadonlyMap<string, BoardItem>;
@@ -48,6 +54,7 @@ type BoardStageOptions = {
48
54
  height: number;
49
55
  }) => void;
50
56
  onVisibleChange?: (ids: ReadonlySet<string> | null) => void;
57
+ safeRect?: () => Rect;
51
58
  scriptCapabilities?: Record<string, (id: string, input: unknown) => Promise<unknown>>;
52
59
  };
53
60
  type BoardStage = {
@@ -66,6 +73,7 @@ type BoardStage = {
66
73
  subscribeScripts(listener: () => void): () => void;
67
74
  invalidate(options?: {
68
75
  cards?: boolean;
76
+ items?: Iterable<string>;
69
77
  }): void;
70
78
  isMaterialized(id: string): boolean;
71
79
  toWorld(clientX: number, clientY: number): WorldPoint;
@@ -1,20 +1,21 @@
1
1
  import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
+ import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
2
3
  import { screenPoint, screenToWorld, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
3
4
  import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
4
5
  import { boardScriptTarget } from "../model/script-hit.js";
5
6
  import { createBoardAnimationSampler, resolveBoardAnimationCamera } from "../model/sampling.js";
6
7
  import { resolveSceneArrow } from "../model/arrow-geometry.js";
7
8
  import { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../model/palette.js";
8
- import { featuredTaskArtifact } from "../model/task.js";
9
9
  import { isCreationBoardTool } from "../editor/tool.js";
10
10
  import { resizeCursorForHandle } from "../editor/selection-transform.js";
11
11
  import { parseBoardCssColor } from "../render/css-color.js";
12
12
  import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
13
13
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
14
14
  import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
15
- import { getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
15
+ import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
16
16
  import { createBoardScriptHost } from "../render/script-host.js";
17
17
  import { createBoardPlayer } from "../player/index.js";
18
+ import { createBoardScriptObservers } from "../model/script-observers.js";
18
19
  import { toBoardPointerEvent } from "./pointer.js";
19
20
  import { createBoardScene } from "./scene.js";
20
21
  import { scheduleBoardMaintenance } from "./maintenance.js";
@@ -56,12 +57,26 @@ function mountBoardStage(host, options) {
56
57
  let scriptTime = 0;
57
58
  let scriptLastTick = performance.now();
58
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
+ }
59
73
  const scriptListeners = /* @__PURE__ */ new Set();
60
74
  let scriptVersion = 0;
61
75
  let composedVersion = -1;
62
76
  let composedZoom = 0;
63
77
  let composedPlayhead = "";
64
78
  let composedMoving = false;
79
+ let composedGesture = false;
65
80
  let composedDocument = null;
66
81
  let scriptHover = null;
67
82
  const scriptPointers = createBoardScriptPointerRouter();
@@ -70,6 +85,10 @@ function mountBoardStage(host, options) {
70
85
  let disposed = false;
71
86
  let renderFrame = 0;
72
87
  let tickFrame = 0;
88
+ let cardTickFrame = 0;
89
+ let cardTickTimer = null;
90
+ let cardTickDue = 0;
91
+ const staleIds = /* @__PURE__ */ new Set();
73
92
  let resizeFrame = 0;
74
93
  let cardEpoch = 0;
75
94
  let assetVersion = 0;
@@ -144,6 +163,9 @@ function mountBoardStage(host, options) {
144
163
  colorScheme: current.colorScheme,
145
164
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
146
165
  zoom: renderCamera().zoom,
166
+ reducedMotion: options.reducedMotion?.() ?? false,
167
+ labels: options.labels?.() ?? DEFAULT_BOARD_RENDER_LABELS,
168
+ taskRun: options.taskRun ?? (() => null),
147
169
  assetKey: assets.assetKey,
148
170
  getTexture: (key) => assets.getTexture(key),
149
171
  hasError: (key) => assets.hasError(key),
@@ -282,6 +304,28 @@ function mountBoardStage(host, options) {
282
304
  app?.render();
283
305
  });
284
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
+ }
285
329
  function scheduleTick() {
286
330
  if (tickFrame || !active || disposed) return;
287
331
  tickFrame = requestAnimationFrame(() => {
@@ -289,6 +333,16 @@ function mountBoardStage(host, options) {
289
333
  sync();
290
334
  });
291
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
+ }
292
346
  function createScripts() {
293
347
  return createBoardScriptHost({
294
348
  host,
@@ -310,8 +364,20 @@ function mountBoardStage(host, options) {
310
364
  onOutput: (id, output) => {
311
365
  if (output) {
312
366
  prepareBoardScriptOutput(editor.document, id, output);
367
+ cancelScriptPointers(id, output.hits);
313
368
  scriptOutputs.set(id, output);
314
- } else scriptOutputs.delete(id);
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();
315
381
  scriptVersion++;
316
382
  scheduleTick();
317
383
  },
@@ -340,6 +406,10 @@ function mountBoardStage(host, options) {
340
406
  const previous = scripts;
341
407
  scripts = null;
342
408
  previous.destroy();
409
+ scriptObservers.clear();
410
+ syncTransformSubscription();
411
+ interactiveScripts.clear();
412
+ screenScripts.clear();
343
413
  scriptOutputs.clear();
344
414
  editor.setScriptItems(/* @__PURE__ */ new Map());
345
415
  scriptHover = null;
@@ -354,38 +424,48 @@ function mountBoardStage(host, options) {
354
424
  const moving = new Map(previews);
355
425
  for (const [id, item] of frame.items) moving.set(id, item);
356
426
  editor.setPlayedItems(moving);
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;
438
+ const camera = renderCamera();
357
439
  if (scripts && (scriptOutputs.size || composedVersion !== scriptVersion)) {
358
440
  const playheadKey = `${editor.playhead?.animationId ?? ""}:${editor.playhead?.time ?? ""}`;
359
- if (scriptVersion !== composedVersion || composedDocument !== editor.document || moving.size || composedMoving || composedZoom !== editor.camera.zoom || composedPlayhead !== playheadKey) {
441
+ if (scriptVersion !== composedVersion || composedDocument !== editor.document || screenScripts.size > 0 && editor.gestureActive || composedGesture || moving.size || composedMoving || composedZoom !== camera.zoom || composedPlayhead !== playheadKey) {
360
442
  const animationItems = editor.animationItems();
361
- const composed = composeBoardScriptItems(editor.document, editor.scene, scriptOutputs, animationItems, editor.camera, (owner, error) => scripts?.fault(owner, error instanceof Error ? error.message : String(error)));
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);
362
444
  const scriptedIds = new Set([...scriptOutputs.values()].flatMap((output) => Object.keys(output.patches)));
363
445
  for (const id of animationItems.keys()) if (!scriptedIds.has(id)) composed.delete(id);
364
446
  editor.setScriptItems(composed);
365
447
  composedMoving = moving.size > 0;
366
448
  composedPlayhead = playheadKey;
367
- composedZoom = editor.camera.zoom;
449
+ composedZoom = camera.zoom;
450
+ composedGesture = screenScripts.size > 0 && editor.gestureActive;
368
451
  composedVersion = scriptVersion;
369
452
  composedDocument = editor.document;
370
453
  }
371
454
  }
372
455
  const renderScene = editor.scene;
373
- const animationId = editor.playhead?.animationId ?? (frame.playbackTime !== null ? playback?.animationId : null) ?? null;
374
- if (animationId && (effectDocument !== editor.document || effectAnimation !== animationId)) {
375
- effectDocument = editor.document;
376
- effectAnimation = animationId;
377
- effectSampler = createBoardAnimationSampler(editor.document, animationId);
378
- }
379
- if (editor.playhead && effectSampler) {
380
- frame.camera = effectSampler.at(editor.playhead.time).evaluation.camera;
381
- frame.time = editor.playhead.time;
382
- }
383
- playbackCamera = editor.cameraPolicy === "follow" && Object.keys(frame.camera).length > 0 ? followCamera(frame, renderScene) : null;
384
- const camera = renderCamera();
385
456
  if (scripts) {
386
457
  const now = performance.now();
387
458
  if (scriptMode !== "paused") scriptTime += Math.min(250, Math.max(0, now - scriptLastTick));
388
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
+ }));
389
469
  scripts.sync(editor.document, renderScene, camera, scriptTime, scriptMode === "interact", visibleIds());
390
470
  }
391
471
  world.position.set(camera.x, camera.y);
@@ -420,10 +500,12 @@ function mountBoardStage(host, options) {
420
500
  current.key,
421
501
  textZoomBucket(camera.zoom)
422
502
  ].join("|"),
503
+ staleIds,
423
504
  structureVersion: editor.structureVersion,
424
505
  geometryVersion: editor.geometryVersion,
425
506
  gestureActive: editor.gestureActive && editor.interaction.type !== "panning"
426
507
  });
508
+ staleIds.clear();
427
509
  const single = editor.selection.length === 1 ? renderScene.get(editor.selection[0]) : null;
428
510
  let arrowEndpoints;
429
511
  if (single?.type === "arrow" && !single.locked) {
@@ -467,6 +549,8 @@ function mountBoardStage(host, options) {
467
549
  if (nextCursor !== cursor) host.style.cursor = cursor = nextCursor;
468
550
  scheduleRender();
469
551
  if (frame.running || scene.animated || scriptMode !== "paused" && scripts?.animated) scheduleTick();
552
+ const cardWait = scene.tick(performance.now());
553
+ if (cardWait !== null) scheduleCardTick(cardWait);
470
554
  }
471
555
  function resize() {
472
556
  cancelAnimationFrame(resizeFrame);
@@ -517,7 +601,7 @@ function mountBoardStage(host, options) {
517
601
  });
518
602
  }
519
603
  function scriptPointer(type, event, input) {
520
- if (!scriptPointers.needsRouting(event.pointerId, scripts !== null)) return false;
604
+ if (!scriptPointers.needsRouting(event.pointerId, interactiveScripts.size > 0)) return false;
521
605
  const enabled = scriptMode === "interact" && !editor.gestureActive && !editor.spaceHeld && event.button !== 1;
522
606
  const target = type === "leave" || !enabled || scriptPointers.owner(event.pointerId) === "editor" ? null : scriptTarget(input);
523
607
  if (target?.owner !== scriptHover?.owner || target?.target !== scriptHover?.target) {
@@ -525,6 +609,7 @@ function mountBoardStage(host, options) {
525
609
  if (target) sendScriptEvent(target, "pointerenter", event, input, scriptHover);
526
610
  scriptHover = target;
527
611
  }
612
+ const consumed = scriptPointers.owner(event.pointerId) === "script";
528
613
  const { recipient, click } = scriptPointers.route(type, event.pointerId, target, {
529
614
  x: event.clientX,
530
615
  y: event.clientY
@@ -535,10 +620,10 @@ function mountBoardStage(host, options) {
535
620
  }
536
621
  if (recipient && type !== "leave") sendScriptEvent(recipient, input.cancelled ? "pointercancel" : `pointer${type}`, event, input);
537
622
  if (click) sendScriptEvent(click, "click", event, input);
538
- return recipient !== null;
623
+ return consumed || recipient !== null;
539
624
  }
540
625
  function handleScriptKey(event) {
541
- if (scriptMode !== "interact" || !scriptHover || event.target !== host) return;
626
+ if (scriptMode !== "interact" || !scriptHover || event.target !== host || !scriptOutputs.get(scriptHover.owner)?.listens.includes(event.type)) return;
542
627
  const input = {
543
628
  type: event.type,
544
629
  target: scriptHover.target,
@@ -689,6 +774,7 @@ function mountBoardStage(host, options) {
689
774
  },
690
775
  setScriptMode(mode) {
691
776
  if (mode === scriptMode) return;
777
+ if (mode !== "interact") cancelScriptPointers();
692
778
  scriptMode = mode;
693
779
  scriptLastTick = performance.now();
694
780
  scheduleTick();
@@ -723,10 +809,15 @@ function mountBoardStage(host, options) {
723
809
  if (active === next) return;
724
810
  active = next;
725
811
  if (!active) {
812
+ cancelScriptPointers();
726
813
  cancelAnimationFrame(renderFrame);
727
814
  cancelAnimationFrame(tickFrame);
815
+ cancelAnimationFrame(cardTickFrame);
816
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
728
817
  renderFrame = 0;
729
818
  tickFrame = 0;
819
+ cardTickFrame = 0;
820
+ cardTickTimer = null;
730
821
  return;
731
822
  }
732
823
  player.reset();
@@ -738,6 +829,7 @@ function mountBoardStage(host, options) {
738
829
  cardEpoch += 1;
739
830
  requestAssets = true;
740
831
  }
832
+ for (const id of invalidation?.items ?? []) staleIds.add(id);
741
833
  scheduleTick();
742
834
  },
743
835
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
@@ -745,6 +837,8 @@ function mountBoardStage(host, options) {
745
837
  destroy() {
746
838
  disposed = true;
747
839
  scriptPointers.clear();
840
+ stopTransforms?.();
841
+ scriptObservers.clear();
748
842
  scriptListeners.clear();
749
843
  stopMaintenance?.();
750
844
  stopEditor();
@@ -753,6 +847,9 @@ function mountBoardStage(host, options) {
753
847
  cancelAnimationFrame(resizeFrame);
754
848
  cancelAnimationFrame(renderFrame);
755
849
  cancelAnimationFrame(tickFrame);
850
+ cancelAnimationFrame(cardTickFrame);
851
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
852
+ cardTickTimer = null;
756
853
  for (const [type, listener] of listeners) host.removeEventListener(type, listener);
757
854
  editor.setPlayedItems(/* @__PURE__ */ new Map());
758
855
  editor.setScriptItems(/* @__PURE__ */ new Map());