@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
package/README.md CHANGED
@@ -131,7 +131,7 @@ The editor has no framework. Read state through its getters and listen with
131
131
  change, so they work directly with `useSyncExternalStore`:
132
132
 
133
133
  ```ts
134
- import { createBoardRegistry, defineBoardItem } from "@neta-art/cohub/board";
134
+ import { createBoardRegistry, defineBoardItem, worldPoint } from "@neta-art/cohub/board";
135
135
  import { createBoardEditor } from "@neta-art/cohub/board/editor";
136
136
  import { createBoardReplica } from "@neta-art/cohub/board/replica";
137
137
  import { createBoardAssetManager, mountBoardStage } from "@neta-art/cohub/board/stage";
@@ -157,7 +157,16 @@ const editor = createBoardEditor({
157
157
  });
158
158
  replica.subscribe((state) => state.document && editor.loadDocument(state.document, boardId));
159
159
 
160
- // Where Space file paths resolve; a published copy could resolve elsewhere.
160
+ // Prefer stable media URLs. Existing Space paths remain supported by addFile.
161
+ editor.addMedia("https://cdn.example.com/cover.webp", worldPoint(0, 0));
162
+ // Supply a kind for sources without a file extension.
163
+ editor.addMedia("https://cdn.example.com/media?id=42", worldPoint(400, 0), {
164
+ kind: "video",
165
+ snapshot: { title: "Launch", naturalWidth: 1920, naturalHeight: 1080 },
166
+ });
167
+
168
+ // Media URLs load directly; these callbacks only receive Space paths.
169
+ // A published copy can resolve paths against captured artifacts instead.
161
170
  const source = {
162
171
  resolveFileUrl: (path: string) => space.files.resolveUrl(path, { purpose: "preview" }),
163
172
  resolvePlaybackUrl: (path: string) => space.files.resolveUrl(path, { purpose: "playback" }),
@@ -1,3 +1,5 @@
1
+ import { BoardChapter } from "../model/authoring.js";
2
+ import "../model/index.js";
1
3
  import { BoardPlayhead, EditorContext } from "./context.js";
2
4
  //#region src/board/editor/animation.d.ts
3
5
  declare function createAnimationModule(ctx: EditorContext): {
@@ -8,6 +10,7 @@ declare function createAnimationModule(ctx: EditorContext): {
8
10
  removeKeyframe: (trackId: string, at: number) => void;
9
11
  setKeyframeEase: (trackId: string, at: number, ease: string | null) => void;
10
12
  createAnimation: (name: string, duration?: number) => string;
13
+ createSequence: (name: string, chapters: BoardChapter[]) => string | null;
11
14
  };
12
15
  type AnimationModule = ReturnType<typeof createAnimationModule>;
13
16
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { boardSequence } from "../model/authoring.js";
1
2
  import { createBoardItemId } from "../model/items/id.js";
2
3
  import { readPath, sameJson } from "./document-edits.js";
3
4
  //#region src/board/editor/animation.ts
@@ -158,6 +159,19 @@ function createAnimationModule(ctx) {
158
159
  });
159
160
  return id;
160
161
  }
162
+ function createSequence(name, chapters) {
163
+ if (options.readonly) return null;
164
+ const id = createBoardItemId();
165
+ commitAnimation(id, {
166
+ ...boardSequence(state.base, chapters),
167
+ name
168
+ });
169
+ setPlayhead({
170
+ animationId: id,
171
+ time: 0
172
+ });
173
+ return id;
174
+ }
161
175
  return {
162
176
  setPlayhead,
163
177
  setRecording,
@@ -165,7 +179,8 @@ function createAnimationModule(ctx) {
165
179
  toggleKeyframe,
166
180
  removeKeyframe,
167
181
  setKeyframeEase,
168
- createAnimation
182
+ createAnimation,
183
+ createSequence
169
184
  };
170
185
  }
171
186
  //#endregion
@@ -65,6 +65,13 @@ function remapItems(items, idMap, place = (item) => item, resolveEnd = () => nul
65
65
  let item = place(source);
66
66
  const { locked: _locked, ...unlocked } = item;
67
67
  item = unlocked;
68
+ if (item.script) item = {
69
+ ...item,
70
+ script: {
71
+ ...item.script,
72
+ targets: Object.fromEntries(Object.entries(item.script.targets).map(([name, target]) => [name, idMap.get(target) ?? target]))
73
+ }
74
+ };
68
75
  if (item.parent) {
69
76
  const parent = idMap.get(item.parent);
70
77
  if (parent) item = {
@@ -7,35 +7,39 @@ function createCopyModule(ctx) {
7
7
  const { state, options } = ctx;
8
8
  let internalClipboard = null;
9
9
  let pasteCount = 0;
10
- function detachedSubtrees(ids) {
11
- const { scene } = state;
10
+ function detachedSubtrees(ids, scene) {
12
11
  const items = {};
13
12
  for (const root of ctx.query.rootsOf(ids)) {
14
13
  const item = scene.get(root);
15
- if (!item) continue;
14
+ if (!item || !state.base.items[root]) continue;
16
15
  const { id: _id, frame, ...stored } = item;
17
16
  items[root] = ctx.document.reparent(stored, frame, void 0, scene.layout.box(root));
18
17
  for (const child of scene.descendants(root)) {
19
- const value = ctx.document.displayItem(child);
20
- if (value) items[child] = value;
18
+ if (!state.base.items[child]) continue;
19
+ const shown = scene.get(child);
20
+ if (shown) {
21
+ const { id: _id, frame: _frame, ...value } = shown;
22
+ items[child] = value;
23
+ }
21
24
  }
22
25
  }
23
26
  return items;
24
27
  }
25
- function arrowEndPoint(id, which) {
26
- const arrow = state.scene.get(id);
28
+ function arrowEndPoint(scene, id, which) {
29
+ const arrow = scene.get(id);
27
30
  if (arrow?.type !== "arrow") return null;
28
- const resolved = resolveSceneArrow(arrow, state.scene);
29
- return applyMatrix(invertMatrix(state.scene.layout.matrix(id)), (which === "start" ? resolved.start : resolved.end).point);
31
+ const resolved = resolveSceneArrow(arrow, scene);
32
+ return applyMatrix(invertMatrix(scene.layout.matrix(id)), (which === "start" ? resolved.start : resolved.end).point);
30
33
  }
31
34
  function duplicates(ids, offset) {
32
- const { scene } = state;
33
- const copied = detachedSubtrees(ids);
35
+ const scene = ctx.document.copyScene();
36
+ const copied = detachedSubtrees(ids, scene);
34
37
  for (const id of Object.keys(copied)) for (const arrowId of scene.binders(id)) {
35
- const arrow = ctx.document.displayItem(arrowId);
38
+ if (!state.base.items[arrowId]) continue;
36
39
  const shown = scene.get(arrowId);
37
- if (copied[arrowId] || !arrow || !shown || !arrowBindings(arrow).every((bound) => copied[bound])) continue;
38
- copied[arrowId] = ctx.document.reparent(arrow, shown.frame, void 0, scene.layout.box(arrowId));
40
+ if (copied[arrowId] || !shown || !arrowBindings(shown).every((bound) => copied[bound])) continue;
41
+ const { id: _id, frame, ...arrow } = shown;
42
+ copied[arrowId] = ctx.document.reparent(arrow, frame, void 0, scene.layout.box(arrowId));
39
43
  }
40
44
  const idMap = new Map(Object.keys(copied).map((id) => [id, createBoardItemId()]));
41
45
  const moved = remapItems(copied, idMap, (item) => item.parent && copied[item.parent] ? item : {
@@ -44,8 +48,8 @@ function createCopyModule(ctx) {
44
48
  x: item.position.x + offset,
45
49
  y: item.position.y + offset
46
50
  }
47
- }, arrowEndPoint);
48
- let z = ctx.document.topZ(void 0);
51
+ }, (id, which) => arrowEndPoint(scene, id, which));
52
+ let z = ctx.document.topZ(void 0, scene);
49
53
  return new Map(Object.entries(moved).map(([id, item]) => [id, item.parent ? item : {
50
54
  ...item,
51
55
  z: z++
@@ -66,8 +70,9 @@ function createCopyModule(ctx) {
66
70
  function copySelection() {
67
71
  const { selection } = state;
68
72
  if (selection.length === 0) return null;
69
- const copied = detachedSubtrees(selection);
70
- const origin = ctx.query.contentBounds(ctx.query.rootsOf(selection));
73
+ const scene = ctx.document.copyScene();
74
+ const copied = detachedSubtrees(selection, scene);
75
+ const origin = ctx.query.contentBounds(ctx.query.rootsOf(selection), scene);
71
76
  const payload = origin ? encodeClipboard(copied, {
72
77
  x: origin.x,
73
78
  y: origin.y
@@ -3,7 +3,7 @@ import { BoardMatrix } from "../../protocol/dist/board-layout.js";
3
3
  import { WorldPoint } from "../model/geometry.js";
4
4
  import { BoardFileSnapshotFacts } from "../model/file-snapshot.js";
5
5
  import { ShapeKind } from "../model/shape-types.js";
6
- import { BoardItemEntry, createAppBoardItem } from "../model/items/create.js";
6
+ import { BoardItemEntry, BoardMediaItemKind, createAppBoardItem } from "../model/items/create.js";
7
7
  import { BoardItemInput } from "../model/items/registry.js";
8
8
  import "../model/index.js";
9
9
  import { BoardInteraction } from "./interaction.js";
@@ -23,6 +23,10 @@ declare function createCreationModule(ctx: EditorContext): {
23
23
  }) => void;
24
24
  addItem: (type: string, input: BoardItemInput) => string | null;
25
25
  addApp: (app: BoardAppMetadata, at: WorldPoint) => string;
26
+ addMedia: (src: string, at: WorldPoint, options?: {
27
+ kind?: BoardMediaItemKind;
28
+ snapshot?: BoardMediaSnapshot;
29
+ }) => string;
26
30
  addFile: (path: string, at: WorldPoint, snapshot?: BoardMediaSnapshot & BoardFileSnapshotFacts) => string;
27
31
  addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>) => string;
28
32
  addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>) => string | null;
@@ -3,7 +3,7 @@ import { rectCenter } from "../model/geometry.js";
3
3
  import { buildBoardScene } from "../model/scene.js";
4
4
  import { isShapeKind } from "../model/shape-types.js";
5
5
  import { createBoardItemId } from "../model/items/id.js";
6
- import { createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem } from "../model/items/create.js";
6
+ import { createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTextBoardItem } from "../model/items/create.js";
7
7
  import { sameJson } from "./document-edits.js";
8
8
  import { isContinuousBoardTool } from "./tool.js";
9
9
  //#region src/board/editor/creation.ts
@@ -197,6 +197,7 @@ function createCreationModule(ctx) {
197
197
  add,
198
198
  addItem,
199
199
  addApp: (app, at) => addOne(createAppBoardItem(app, at.x, at.y)),
200
+ addMedia: (src, at, options = {}) => addOne(createMediaBoardItem(src, at.x, at.y, options.snapshot, void 0, options.kind)),
200
201
  addFile: (path, at, snapshot) => addOne(createFileNodeForPath(path, at.x, at.y, snapshot)),
201
202
  addTask: (taskRunId, snapshot, at, metadata) => addOne(createTaskBoardItem(taskRunId, snapshot, at.x, at.y, metadata)),
202
203
  addTaskWithSources,
@@ -8,14 +8,14 @@ function createDeletionModule(ctx) {
8
8
  const { scene, base } = state;
9
9
  const doomed = /* @__PURE__ */ new Set();
10
10
  for (const id of ids) {
11
- if (!scene.get(id)) continue;
11
+ if (!base.items[id]) continue;
12
12
  doomed.add(id);
13
- for (const child of scene.descendants(id)) doomed.add(child);
13
+ for (const child of scene.descendants(id)) if (base.items[child]) doomed.add(child);
14
14
  }
15
15
  const changes = /* @__PURE__ */ new Map();
16
16
  for (const id of doomed) changes.set(id, null);
17
17
  for (const id of doomed) for (const arrowId of scene.binders(id)) {
18
- if (doomed.has(arrowId) || changes.has(arrowId)) continue;
18
+ if (!base.items[arrowId] || doomed.has(arrowId) || changes.has(arrowId)) continue;
19
19
  const arrow = scene.get(arrowId);
20
20
  if (!arrow) continue;
21
21
  const resolved = resolveSceneArrow(arrow, scene);
@@ -1,6 +1,7 @@
1
- import { BoardAnimation, BoardDocument, BoardItem, BoardSettings } from "../../protocol/dist/board-model.js";
1
+ import { BoardAnimation, BoardDocument, BoardItem, BoardPatch, BoardSettings } from "../../protocol/dist/board-model.js";
2
2
  import { BoardFrame } from "../../protocol/dist/board-layout.js";
3
3
  import { Rect } from "../model/geometry.js";
4
+ import { BoardScene } from "../model/scene.js";
4
5
  import "../model/index.js";
5
6
  import { EditorContext } from "./context.js";
6
7
  //#region src/board/editor/document.d.ts
@@ -8,8 +9,13 @@ declare function createDocumentModule(ctx: EditorContext): {
8
9
  markAdded: (ids: readonly string[]) => void;
9
10
  consumeRecentlyAdded: () => string[];
10
11
  displayItem: (id: string) => BoardItem | undefined;
12
+ copyScene: () => BoardScene;
11
13
  reevaluate: () => void;
12
14
  setPlayedItems: (items: ReadonlyMap<string, BoardItem>) => void;
15
+ readonly hasScriptItems: boolean;
16
+ setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
17
+ animationItems: () => Map<string, BoardItem>;
18
+ commitPatch: (patch: BoardPatch) => Promise<void> | undefined;
13
19
  rescene: (ids?: Iterable<string>) => void;
14
20
  reset: (next: BoardDocument) => void;
15
21
  setDraftItem: (id: string, item: BoardItem | null) => void;
@@ -28,7 +34,7 @@ declare function createDocumentModule(ctx: EditorContext): {
28
34
  previewSettings: (settings: BoardSettings) => void;
29
35
  setSettings: (settings: BoardSettings) => void;
30
36
  reparent: (item: BoardItem, frame: BoardFrame, parent: string | undefined, box: Rect) => BoardItem;
31
- topZ: (parent: string | undefined) => number;
37
+ topZ: (parent: string | undefined, scene?: BoardScene) => number;
32
38
  };
33
39
  type DocumentModule = ReturnType<typeof createDocumentModule>;
34
40
  //#endregion
@@ -11,6 +11,7 @@ function createDocumentModule(ctx) {
11
11
  const recentlyAdded = /* @__PURE__ */ new Set();
12
12
  let evaluated = /* @__PURE__ */ new Map();
13
13
  let played = /* @__PURE__ */ new Map();
14
+ let scripted = /* @__PURE__ */ new Map();
14
15
  let compiled = null;
15
16
  function markAdded(ids) {
16
17
  if (ids.length === 0) return;
@@ -24,14 +25,15 @@ function createDocumentModule(ctx) {
24
25
  }
25
26
  function displayItem(id) {
26
27
  if (draft.has(id)) return draft.get(id) ?? void 0;
27
- return evaluated.get(id) ?? played.get(id) ?? state.base.items[id];
28
+ return scripted.get(id) ?? evaluated.get(id) ?? played.get(id) ?? state.base.items[id];
28
29
  }
29
- function displayDocument() {
30
+ function displayDocument(includeScripts = true) {
30
31
  const base = state.base;
31
- if (draft.size === 0 && evaluated.size === 0 && played.size === 0) return base;
32
+ if (draft.size === 0 && evaluated.size === 0 && played.size === 0 && (!includeScripts || scripted.size === 0)) return base;
32
33
  const items = { ...base.items };
33
34
  for (const [id, item] of played) items[id] = item;
34
35
  for (const [id, item] of evaluated) items[id] = item;
36
+ if (includeScripts) for (const [id, item] of scripted) items[id] = item;
35
37
  for (const [id, item] of draft) if (item) items[id] = item;
36
38
  else delete items[id];
37
39
  return {
@@ -59,6 +61,17 @@ function createDocumentModule(ctx) {
59
61
  for (const id of changed) if (draft.has(id) || evaluated.has(id) || sameJson(previous.get(id), played.get(id))) changed.delete(id);
60
62
  if (changed.size) rescene(changed);
61
63
  }
64
+ function setScriptItems(items) {
65
+ const previous = scripted;
66
+ scripted = items;
67
+ const changed = [.../* @__PURE__ */ new Set([...previous.keys(), ...items.keys()])].filter((id) => !sameJson(previous.get(id), items.get(id)));
68
+ if (!changed.length) return;
69
+ rescene(changed.some((id) => {
70
+ const before = previous.get(id) ?? state.base.items[id];
71
+ const after = items.get(id) ?? state.base.items[id];
72
+ return !before || !after || before.parent !== after.parent || before.z !== after.z || !sameJson(arrowBindings(before), arrowBindings(after));
73
+ }) ? void 0 : changed);
74
+ }
62
75
  function rescene(ids) {
63
76
  if (ids) {
64
77
  const changed = [...ids];
@@ -76,6 +89,7 @@ function createDocumentModule(ctx) {
76
89
  draft.clear();
77
90
  draftOrigins.clear();
78
91
  played = /* @__PURE__ */ new Map();
92
+ scripted = /* @__PURE__ */ new Map();
79
93
  reevaluate();
80
94
  state.scene = buildBoardScene(next);
81
95
  ctx.query.invalidate();
@@ -114,12 +128,20 @@ function createDocumentModule(ctx) {
114
128
  rescene();
115
129
  }
116
130
  function send(patch) {
117
- if (options.readonly || isEmptyBoardPatch(patch)) return;
131
+ if (options.readonly || isEmptyBoardPatch(patch)) return Promise.resolve(null);
118
132
  state.pendingCommits += 1;
119
- Promise.resolve(options.onCommit(patch)).then(() => {
133
+ let completion;
134
+ try {
135
+ completion = Promise.resolve(options.onCommit(patch));
136
+ } catch (error) {
137
+ completion = Promise.reject(error);
138
+ }
139
+ return completion.then(() => {
120
140
  state.saveError = null;
141
+ return null;
121
142
  }).catch((error) => {
122
143
  state.saveError = error instanceof Error ? error.message : "Failed to sync board";
144
+ return error instanceof Error ? error : new Error(state.saveError);
123
145
  }).finally(() => {
124
146
  state.pendingCommits -= 1;
125
147
  });
@@ -183,6 +205,24 @@ function createDocumentModule(ctx) {
183
205
  for (const [id, item] of changes) setDraftItem(id, item);
184
206
  commit({}, record);
185
207
  }
208
+ function commitPatch(patch) {
209
+ if (options.readonly) throw new Error("Board is read-only.");
210
+ const before = state.base;
211
+ const result = applyBoardPatchToDocument(before, patch);
212
+ if (!result.ok) throw new Error(result.diagnostics[0]?.message ?? "Invalid board patch.");
213
+ const ids = Object.keys(patch.items ?? {});
214
+ const redo = diffBoardEdits(before, result.document, ids);
215
+ if (isEmptyBoardPatch(redo)) return;
216
+ state.undoStack = [...state.undoStack.slice(-199), {
217
+ undo: diffBoardEdits(result.document, before, ids),
218
+ redo
219
+ }];
220
+ state.redoStack = [];
221
+ adopt(result.document, ids);
222
+ return send(redo).then((error) => {
223
+ if (error) throw error;
224
+ });
225
+ }
186
226
  function applyPatch(patch) {
187
227
  const result = applyBoardPatchToDocument(state.base, patch);
188
228
  if (!result.ok) {
@@ -222,17 +262,24 @@ function createDocumentModule(ctx) {
222
262
  const matrix = parent ? state.scene.layout.matrix(parent) : IDENTITY_MATRIX;
223
263
  return placeItem(next, frame, matrix, box);
224
264
  }
225
- function topZ(parent) {
265
+ function topZ(parent, scene = state.scene) {
226
266
  let z = 0;
227
- for (const id of state.scene.children(parent)) z = Math.max(z, displayItem(id)?.z ?? 0);
267
+ for (const id of scene.children(parent)) z = Math.max(z, scene.get(id)?.z ?? 0);
228
268
  return z + 1;
229
269
  }
230
270
  return {
231
271
  markAdded,
232
272
  consumeRecentlyAdded,
233
273
  displayItem,
274
+ copyScene: () => scripted.size ? buildBoardScene(displayDocument(false)) : state.scene,
234
275
  reevaluate,
235
276
  setPlayedItems,
277
+ get hasScriptItems() {
278
+ return scripted.size > 0;
279
+ },
280
+ setScriptItems,
281
+ animationItems: () => new Map([...played, ...evaluated]),
282
+ commitPatch,
236
283
  rescene,
237
284
  reset,
238
285
  setDraftItem,
@@ -1,9 +1,11 @@
1
- import { BoardDocument, BoardItem, BoardMediaSnapshot, BoardSettings, BoardTaskSnapshot } from "../../protocol/dist/board-model.js";
1
+ import { BoardDocument, BoardItem, BoardMediaSnapshot, BoardPatch, BoardSettings, BoardTaskSnapshot } from "../../protocol/dist/board-model.js";
2
2
  import { BoardFrame } from "../../protocol/dist/board-layout.js";
3
3
  import { BoardViewport, Rect, ScreenPoint, WorldPoint } from "../model/geometry.js";
4
4
  import { BoardScene, BoardSceneItem } from "../model/scene.js";
5
+ import { BoardChapter } from "../model/authoring.js";
5
6
  import { BoardFileSnapshotFacts } from "../model/file-snapshot.js";
6
7
  import { ShapeKind } from "../model/shape-types.js";
8
+ import { BoardItemEntry, BoardMediaItemKind } from "../model/items/create.js";
7
9
  import { BoardItemInput, BoardRegistry } from "../model/items/registry.js";
8
10
  import "../model/index.js";
9
11
  import { AlignMode, DistributeAxis } from "./align.js";
@@ -22,6 +24,7 @@ declare function createBoardEditor(options: BoardEditorOptions): {
22
24
  registry: BoardRegistry;
23
25
  readonly: boolean;
24
26
  subscribe: (channels: BoardEditorChannel | readonly BoardEditorChannel[], listener: BoardEditorListener) => () => void;
27
+ readonly hasScripts: boolean;
25
28
  readonly document: BoardDocument;
26
29
  readonly settings: {
27
30
  title?: string | undefined;
@@ -130,11 +133,14 @@ declare function createBoardEditor(options: BoardEditorOptions): {
130
133
  setSettings: (settings: BoardSettings) => void;
131
134
  previewSettings: (settings: BoardSettings) => void;
132
135
  setPlayedItems: (items: ReadonlyMap<string, BoardItem>) => void;
136
+ setScriptItems: (items: ReadonlyMap<string, BoardItem>) => void;
137
+ animationItems: () => Map<string, BoardItem>;
138
+ commitPatch: (patch: BoardPatch) => Promise<void> | undefined;
133
139
  consumeRecentlyAdded: () => string[];
134
- itemAt: (point: WorldPoint, exclude?: ReadonlySet<string> | undefined) => BoardSceneItem | null;
140
+ itemAt: (point: WorldPoint, exclude?: ReadonlySet<string> | undefined, includeTemporary?: boolean | undefined) => BoardSceneItem | null;
135
141
  itemById: (id: string) => BoardSceneItem | null;
136
142
  labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
137
- idsInRect: (rect: Rect) => string[];
143
+ idsInRect: (rect: Rect, includeTemporary?: boolean | undefined) => string[];
138
144
  setCamera: (viewport: BoardViewport) => void;
139
145
  setCameraPolicy: (policy: BoardCameraPolicy) => void;
140
146
  takeCameraControl: () => void;
@@ -162,8 +168,16 @@ declare function createBoardEditor(options: BoardEditorOptions): {
162
168
  bringToFront: () => void;
163
169
  sendToBack: () => void;
164
170
  addItem: (type: string, input: BoardItemInput) => string | null;
171
+ addItems: (entries: BoardItemEntry[], { select, enter }?: {
172
+ select?: boolean;
173
+ enter?: boolean;
174
+ }) => void;
165
175
  addApp: (app: BoardAppMetadata, at: WorldPoint) => string;
166
176
  addFile: (path: string, at: WorldPoint, snapshot?: BoardMediaSnapshot & BoardFileSnapshotFacts) => string;
177
+ addMedia: (src: string, at: WorldPoint, options?: {
178
+ kind?: BoardMediaItemKind;
179
+ snapshot?: BoardMediaSnapshot;
180
+ }) => string;
167
181
  addTask: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, metadata?: Record<string, unknown>) => string;
168
182
  addTaskWithSources: (taskRunId: string, snapshot: BoardTaskSnapshot, at: WorldPoint, sources: BoardTaskSource[], metadata?: Record<string, unknown>) => string | null;
169
183
  addText: (text: string, at: WorldPoint) => void;
@@ -198,6 +212,7 @@ declare function createBoardEditor(options: BoardEditorOptions): {
198
212
  removeKeyframe: (trackId: string, at: number) => void;
199
213
  setKeyframeEase: (trackId: string, at: number, ease: string | null) => void;
200
214
  createAnimation: (name: string, duration?: number) => string;
215
+ createSequence: (name: string, chapters: BoardChapter[]) => string | null;
201
216
  pointerDown: (event: BoardPointerEvent) => void;
202
217
  pointerMove: (event: BoardPointerEvent) => void;
203
218
  pointerUp: (event: BoardPointerEvent) => void;
@@ -1,7 +1,8 @@
1
1
  import { clampBoardStrokeSize } from "../../protocol/dist/board-constants.js";
2
+ import { hasBoardScripts } from "../model/script-index.js";
2
3
  import { normalizeViewport } from "../model/geometry.js";
3
- import { isBoardColorId } from "../model/palette.js";
4
4
  import { buildBoardScene } from "../model/scene.js";
5
+ import { isBoardColorId } from "../model/palette.js";
5
6
  import { isShapeKind } from "../model/shape-types.js";
6
7
  import { createBoardToolStyles } from "../model/tool-styles.js";
7
8
  import { titleForBoardItem } from "../model/items/create.js";
@@ -145,6 +146,9 @@ function createBoardEditor(options) {
145
146
  registry,
146
147
  readonly: options.readonly === true,
147
148
  subscribe: channels.subscribe,
149
+ get hasScripts() {
150
+ return read("document", () => hasBoardScripts(state.base));
151
+ },
148
152
  get document() {
149
153
  return read("document", () => state.base);
150
154
  },
@@ -344,6 +348,9 @@ function createBoardEditor(options) {
344
348
  setSettings: document.setSettings,
345
349
  previewSettings: document.previewSettings,
346
350
  setPlayedItems: document.setPlayedItems,
351
+ setScriptItems: document.setScriptItems,
352
+ animationItems: document.animationItems,
353
+ commitPatch: document.commitPatch,
347
354
  consumeRecentlyAdded: document.consumeRecentlyAdded,
348
355
  itemAt: tracked(["scene", "camera"], query.itemAt),
349
356
  itemById: tracked(["scene"], query.itemById),
@@ -380,8 +387,10 @@ function createBoardEditor(options) {
380
387
  bringToFront: selection.bringToFront,
381
388
  sendToBack: selection.sendToBack,
382
389
  addItem: creation.addItem,
390
+ addItems: creation.add,
383
391
  addApp: creation.addApp,
384
392
  addFile: creation.addFile,
393
+ addMedia: creation.addMedia,
385
394
  addTask: creation.addTask,
386
395
  addTaskWithSources: creation.addTaskWithSources,
387
396
  addText: creation.addText,
@@ -413,6 +422,7 @@ function createBoardEditor(options) {
413
422
  removeKeyframe: animation.removeKeyframe,
414
423
  setKeyframeEase: animation.setKeyframeEase,
415
424
  createAnimation: animation.createAnimation,
425
+ createSequence: animation.createSequence,
416
426
  pointerDown: interaction.pointerDown,
417
427
  pointerMove: interaction.pointerMove,
418
428
  pointerUp: interaction.pointerUp,
@@ -1,11 +1,11 @@
1
1
  import { Rect, WorldPoint } from "../model/geometry.js";
2
- import { BoardSceneItem } from "../model/scene.js";
2
+ import { BoardScene, BoardSceneItem } from "../model/scene.js";
3
3
  import "../model/index.js";
4
4
  import { EditorContext } from "./context.js";
5
5
  //#region src/board/editor/query.d.ts
6
6
  declare function createQueryModule(ctx: EditorContext): {
7
7
  invalidate: (ids?: readonly string[]) => void;
8
- boundsOf: (item: BoardSceneItem) => Rect;
8
+ boundsOf: (item: BoardSceneItem, scene?: BoardScene) => Rect;
9
9
  itemById: (id: string) => BoardSceneItem | null;
10
10
  isLocked: (item: {
11
11
  locked?: boolean;
@@ -14,10 +14,10 @@ declare function createQueryModule(ctx: EditorContext): {
14
14
  rootsOf: (ids: Iterable<string>) => string[];
15
15
  withSubtrees: (ids: Iterable<string>) => Set<string>;
16
16
  mediaIdsForPath: (path: string) => ReadonlySet<string>;
17
- contentBounds: (ids?: Iterable<string>) => Rect | null;
18
- idsInRect: (rect: Rect) => string[];
17
+ contentBounds: (ids?: Iterable<string>, scene?: BoardScene) => Rect | null;
18
+ idsInRect: (rect: Rect, includeTemporary?: boolean) => string[];
19
19
  frameAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
20
- itemAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => BoardSceneItem | null;
20
+ itemAt: (point: WorldPoint, exclude?: ReadonlySet<string>, includeTemporary?: boolean) => BoardSceneItem | null;
21
21
  labelItemAt: (point: WorldPoint) => BoardSceneItem | null;
22
22
  arrowHandleAt: (point: WorldPoint) => "start" | "end" | "mid" | null;
23
23
  connectTargetAt: (point: WorldPoint, exclude?: ReadonlySet<string>) => string | null;
@@ -1,3 +1,4 @@
1
+ import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
1
2
  import { rectContainsPoint, unionRects } from "../model/geometry.js";
2
3
  import { arrowHitRadius, arrowPathBounds, distanceToArrow, resolveSceneArrow } from "../model/arrow-geometry.js";
3
4
  import { createSpatialIndex } from "./spatial.js";
@@ -53,8 +54,8 @@ function createQueryModule(ctx) {
53
54
  }
54
55
  spatial.upsert(upserts);
55
56
  }
56
- function boundsOf(item) {
57
- if (item.type === "arrow") return arrowPathBounds(resolveSceneArrow(item, state.scene), item.style.strokeWidth);
57
+ function boundsOf(item, scene = state.scene) {
58
+ if (item.type === "arrow") return arrowPathBounds(resolveSceneArrow(item, scene), item.style.strokeWidth);
58
59
  return registry.bounds(item);
59
60
  }
60
61
  function itemById(id) {
@@ -87,7 +88,7 @@ function createQueryModule(ctx) {
87
88
  const result = /* @__PURE__ */ new Set();
88
89
  for (const id of ids) {
89
90
  result.add(id);
90
- for (const child of state.scene.descendants(id)) result.add(child);
91
+ for (const child of state.scene.descendants(id)) if (state.base.items[child]) result.add(child);
91
92
  }
92
93
  return result;
93
94
  }
@@ -97,6 +98,7 @@ function createQueryModule(ctx) {
97
98
  for (const item of state.scene.items) {
98
99
  if (item.type !== "image" && item.type !== "video" && item.type !== "audio") continue;
99
100
  const src = item.props.src;
101
+ if (parseBoardMediaSource(src)?.kind !== "file") continue;
100
102
  const ids = mediaIdsByPath.get(src) ?? /* @__PURE__ */ new Set();
101
103
  ids.add(item.id);
102
104
  mediaIdsByPath.set(src, ids);
@@ -104,20 +106,20 @@ function createQueryModule(ctx) {
104
106
  }
105
107
  return mediaIdsByPath.get(path) ?? NO_IDS;
106
108
  }
107
- function contentBounds(ids) {
108
- const scene = state.scene;
109
+ function contentBounds(ids, scene = state.scene) {
109
110
  const list = ids ? [...ids].flatMap((id) => scene.get(id) ? [scene.get(id)] : []) : scene.items;
110
- return unionRects(list.map(boundsOf));
111
+ return unionRects((ctx.document.hasScriptItems ? list.filter((item) => Boolean(state.base.items[item.id])) : list).map((item) => boundsOf(item, scene)));
111
112
  }
112
- function idsInRect(rect) {
113
+ function idsInRect(rect, includeTemporary = false) {
113
114
  ensureSpatial();
114
- return spatial.idsInRect(rect);
115
+ const ids = spatial.idsInRect(rect);
116
+ return includeTemporary || !ctx.document.hasScriptItems ? ids : ids.filter((id) => Boolean(state.base.items[id]));
115
117
  }
116
118
  function frameAt(point, exclude) {
117
119
  ensureSpatial();
118
120
  for (const id of spatial.idsAtPoint(point)) {
119
121
  const item = state.scene.get(id);
120
- if (item?.type !== "frame" || exclude?.has(id)) continue;
122
+ if (!state.base.items[id] || item?.type !== "frame" || exclude?.has(id)) continue;
121
123
  if (registry.hitTest(item, point)) return item;
122
124
  }
123
125
  return null;
@@ -129,11 +131,11 @@ function createQueryModule(ctx) {
129
131
  }
130
132
  return registry.hitTest(item, point);
131
133
  }
132
- function itemAt(point, exclude) {
134
+ function itemAt(point, exclude, includeTemporary = false) {
133
135
  ensureSpatial();
134
136
  for (const id of spatial.idsAtPoint(point)) {
135
137
  const item = state.scene.get(id);
136
- if (item && !exclude?.has(id) && hitsItem(item, point)) return item;
138
+ if (item && (includeTemporary || state.base.items[id]) && !exclude?.has(id) && hitsItem(item, point)) return item;
137
139
  }
138
140
  return null;
139
141
  }
@@ -144,7 +146,7 @@ function createQueryModule(ctx) {
144
146
  const candidates = selection.length === 1 ? [...selection, ...nearby] : nearby;
145
147
  for (const id of new Set(candidates)) {
146
148
  const item = scene.get(id);
147
- if (!item || isLocked(item)) continue;
149
+ if (!item || !state.base.items[id] || isLocked(item)) continue;
148
150
  if (item.type === "frame" && rectContainsPoint({
149
151
  x: item.frame.x,
150
152
  y: item.frame.y - 22,
@@ -19,9 +19,10 @@ function createSelectionModule(ctx) {
19
19
  const bounds = memo(() => [items()], (selected) => selectionBounds(selected.map((item) => item.frame)));
20
20
  const transform = memo(() => [items(), bounds()], (selected, rect) => resolveSelectionTransform(registry, selected, rect));
21
21
  function set(ids) {
22
- state.selection = ids;
22
+ state.selection = ids.filter((id) => Boolean(state.base.items[id]));
23
23
  }
24
24
  function toggle(id) {
25
+ if (!state.base.items[id]) return;
25
26
  const { selection } = state;
26
27
  state.selection = selection.includes(id) ? selection.filter((selected) => selected !== id) : [...selection, id];
27
28
  }