@neta-art/cohub 8.24.0 → 9.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 (159) hide show
  1. package/dist/board/animation.d.ts +57 -181
  2. package/dist/board/animation.js +374 -220
  3. package/dist/board/core/arrow-geometry.d.ts +33 -18
  4. package/dist/board/core/arrow-geometry.js +283 -52
  5. package/dist/board/core/draw-geometry.d.ts +22 -36
  6. package/dist/board/core/draw-geometry.js +148 -85
  7. package/dist/board/core/export-assets.d.ts +3 -6
  8. package/dist/board/core/export-assets.js +0 -2
  9. package/dist/board/core/export-plan.d.ts +12 -63
  10. package/dist/board/core/export-plan.js +55 -94
  11. package/dist/board/core/palette.d.ts +4 -21
  12. package/dist/board/core/palette.js +8 -13
  13. package/dist/board/core/scene.d.ts +33 -0
  14. package/dist/board/core/scene.js +302 -0
  15. package/dist/board/core/shape-definition.d.ts +12 -24
  16. package/dist/board/core/shape-definition.js +0 -19
  17. package/dist/board/core/shape-types.d.ts +6 -120
  18. package/dist/board/core/shape-types.js +5 -5
  19. package/dist/board/core/text-metrics.d.ts +8 -11
  20. package/dist/board/core/text-metrics.js +5 -34
  21. package/dist/board/core/tool-styles.d.ts +6 -16
  22. package/dist/board/core/tool-styles.js +17 -25
  23. package/dist/board/export/index.d.ts +10 -17
  24. package/dist/board/export/index.js +22 -21
  25. package/dist/board/export/scene.d.ts +10 -16
  26. package/dist/board/export/scene.js +17 -30
  27. package/dist/board/geometry.d.ts +27 -56
  28. package/dist/board/geometry.js +7 -67
  29. package/dist/board/headless/index.d.ts +6 -27
  30. package/dist/board/headless/index.js +4 -19
  31. package/dist/board/headless/sketch.d.ts +19 -0
  32. package/dist/board/headless/sketch.js +146 -0
  33. package/dist/board/image-key.d.ts +2 -12
  34. package/dist/board/image-key.js +3 -13
  35. package/dist/board/index.d.ts +18 -20
  36. package/dist/board/index.js +17 -17
  37. package/dist/board/media-playback.d.ts +3 -6
  38. package/dist/board/media-playback.js +6 -6
  39. package/dist/board/presets.d.ts +150 -0
  40. package/dist/board/presets.js +226 -0
  41. package/dist/board/render/board-background.d.ts +5 -3
  42. package/dist/board/render/board-background.js +8 -7
  43. package/dist/board/render/clip.d.ts +14 -0
  44. package/dist/board/render/clip.js +47 -0
  45. package/dist/board/render/index.d.ts +8 -5
  46. package/dist/board/render/index.js +7 -4
  47. package/dist/board/render/media-interaction.d.ts +4 -5
  48. package/dist/board/render/media-interaction.js +1 -2
  49. package/dist/board/render/palette.d.ts +6 -2
  50. package/dist/board/render/palette.js +7 -1
  51. package/dist/board/render/renderers/arrow-card-renderer.js +69 -71
  52. package/dist/board/render/renderers/audio-card-renderer.js +3 -3
  53. package/dist/board/render/renderers/base-card-renderer.d.ts +6 -17
  54. package/dist/board/render/renderers/base-card-renderer.js +1 -24
  55. package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -50
  56. package/dist/board/render/renderers/board-renderer-registry.js +6 -7
  57. package/dist/board/render/renderers/draw-card-renderer.js +89 -56
  58. package/dist/board/render/renderers/effect-card-renderer.d.ts +6 -0
  59. package/dist/board/render/renderers/effect-card-renderer.js +197 -0
  60. package/dist/board/render/renderers/far-plate.d.ts +4 -21
  61. package/dist/board/render/renderers/far-plate.js +15 -34
  62. package/dist/board/render/renderers/file-card-renderer.d.ts +4 -25
  63. package/dist/board/render/renderers/file-card-renderer.js +8 -58
  64. package/dist/board/render/renderers/frame-card-renderer.js +28 -28
  65. package/dist/board/render/renderers/image-card-renderer.d.ts +4 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +0 -9
  67. package/dist/board/render/renderers/shape-card-renderer.d.ts +9 -0
  68. package/dist/board/render/renderers/shape-card-renderer.js +240 -0
  69. package/dist/board/render/renderers/sketch-card-renderer.d.ts +5 -0
  70. package/dist/board/render/renderers/sketch-card-renderer.js +98 -0
  71. package/dist/board/render/renderers/task-card-renderer.d.ts +0 -1
  72. package/dist/board/render/renderers/task-card-renderer.js +14 -16
  73. package/dist/board/render/renderers/text-card-renderer.d.ts +1 -3
  74. package/dist/board/render/renderers/text-card-renderer.js +56 -46
  75. package/dist/board/render/renderers/unknown-card-renderer.js +4 -10
  76. package/dist/board/render/renderers/video-card-renderer.d.ts +4 -2
  77. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  78. package/dist/board/render/sketch-host.d.ts +14 -0
  79. package/dist/board/render/sketch-host.js +216 -0
  80. package/dist/board/render/stroke.d.ts +12 -0
  81. package/dist/board/render/stroke.js +90 -0
  82. package/dist/board/render/text-measurement.d.ts +6 -26
  83. package/dist/board/render/text-measurement.js +11 -30
  84. package/dist/board/replay.d.ts +6 -31
  85. package/dist/board/replay.js +45 -203
  86. package/dist/board/runtime/board-player.d.ts +24 -0
  87. package/dist/board/runtime/board-player.js +161 -0
  88. package/dist/board/task.d.ts +2 -10
  89. package/dist/board/task.js +1 -10
  90. package/dist/chunks/environment.d.ts +2291 -3864
  91. package/dist/chunks/host-bridge.d.ts +253 -0
  92. package/dist/chunks/http.d.ts +233 -58
  93. package/dist/chunks/http.js +1046 -1028
  94. package/dist/chunks/transport.js +113 -59
  95. package/dist/chunks/websocket.d.ts +1 -1
  96. package/dist/chunks/websocket.js +1 -1
  97. package/dist/host-bridge.d.ts +54 -0
  98. package/dist/host-bridge.js +322 -0
  99. package/dist/http.d.ts +3 -3
  100. package/dist/index.d.ts +74 -191
  101. package/dist/index.js +2032 -1186
  102. package/dist/protocol/dist/board-constants.d.ts +1 -29
  103. package/dist/protocol/dist/board-constants.js +2 -115
  104. package/dist/protocol/dist/board-layout.d.ts +59 -0
  105. package/dist/protocol/dist/board-layout.js +482 -0
  106. package/dist/protocol/dist/board-model.d.ts +2052 -0
  107. package/dist/protocol/dist/board-model.js +592 -0
  108. package/dist/protocol/dist/board-patch.d.ts +86 -0
  109. package/dist/protocol/dist/board-patch.js +734 -0
  110. package/dist/protocol/dist/board-schema.d.ts +33 -0
  111. package/dist/protocol/dist/board-schema.js +59 -0
  112. package/dist/protocol/dist/board-upgrade.d.ts +1 -0
  113. package/dist/protocol/dist/board-url.d.ts +1 -3
  114. package/dist/protocol/dist/board-url.js +2 -2
  115. package/dist/protocol/dist/board.d.ts +37 -232
  116. package/dist/protocol/dist/board.js +62 -160
  117. package/dist/protocol/dist/display/actions.d.ts +1 -0
  118. package/dist/protocol/dist/display/index.d.ts +2 -0
  119. package/dist/protocol/dist/index.d.ts +11 -12
  120. package/dist/protocol/dist/model/metrics.d.ts +2 -0
  121. package/dist/protocol/dist/model/session.d.ts +2 -1
  122. package/dist/protocol/dist/realtime/types.d.ts +2 -2
  123. package/dist/protocol/dist/runtime/index.d.ts +1 -0
  124. package/dist/space-picker.d.ts +1 -0
  125. package/dist/space-picker.js +1 -0
  126. package/dist/types.d.ts +2 -5
  127. package/package.json +9 -1
  128. package/dist/board/core/connections.d.ts +0 -108
  129. package/dist/board/core/connections.js +0 -455
  130. package/dist/board/mutation.d.ts +0 -6
  131. package/dist/board/mutation.js +0 -14
  132. package/dist/board/render/connection-layer.d.ts +0 -53
  133. package/dist/board/render/connection-layer.js +0 -375
  134. package/dist/board/render/renderers/geo-card-renderer.d.ts +0 -5
  135. package/dist/board/render/renderers/geo-card-renderer.js +0 -127
  136. package/dist/board/semantic-document.d.ts +0 -37
  137. package/dist/board/semantic-document.js +0 -288
  138. package/dist/board/semantic-mutation.d.ts +0 -10
  139. package/dist/board/semantic-mutation.js +0 -209
  140. package/dist/protocol/dist/board-animation.js +0 -34
  141. package/dist/protocol/dist/board-authoring.d.ts +0 -1326
  142. package/dist/protocol/dist/board-authoring.js +0 -302
  143. package/dist/protocol/dist/board-capability-registry.d.ts +0 -2
  144. package/dist/protocol/dist/board-capability-registry.js +0 -123
  145. package/dist/protocol/dist/board-codec.d.ts +0 -3
  146. package/dist/protocol/dist/board-codec.js +0 -383
  147. package/dist/protocol/dist/board-composition.d.ts +0 -383
  148. package/dist/protocol/dist/board-composition.js +0 -310
  149. package/dist/protocol/dist/board-connection.d.ts +0 -317
  150. package/dist/protocol/dist/board-connection.js +0 -219
  151. package/dist/protocol/dist/board-document.d.ts +0 -1566
  152. package/dist/protocol/dist/board-document.js +0 -456
  153. package/dist/protocol/dist/board-effect.d.ts +0 -96
  154. package/dist/protocol/dist/board-effect.js +0 -54
  155. package/dist/protocol/dist/board-geometry.d.ts +0 -15
  156. package/dist/protocol/dist/board-geometry.js +0 -137
  157. package/dist/protocol/dist/board-node.d.ts +0 -8
  158. package/dist/protocol/dist/board-node.js +0 -252
  159. /package/dist/protocol/dist/{board-animation.d.ts → host-bridge.d.ts} +0 -0
@@ -0,0 +1,12 @@
1
+ import { BoardDash } from "../../protocol/dist/board-model.js";
2
+ import "../../protocol/dist/index.js";
3
+ import { WorldPoint } from "../geometry.js";
4
+ import { Graphics } from "pixi.js";
5
+ //#region src/board/render/stroke.d.ts
6
+ declare function dashPattern(dash: BoardDash | undefined, width: number): [number, number] | null;
7
+ declare function trimPolyline(path: readonly WorldPoint[], trim: number | undefined): readonly WorldPoint[];
8
+ declare function tracePolyline(graphics: Graphics, path: readonly WorldPoint[], pattern: [number, number] | null, closed?: boolean): void;
9
+ declare function traceArrowhead(graphics: Graphics, tip: WorldPoint, angle: number, size: number): void;
10
+ declare function endAngle(path: readonly WorldPoint[], atStart: boolean): number | null;
11
+ //#endregion
12
+ export { dashPattern, endAngle, traceArrowhead, tracePolyline, trimPolyline };
@@ -0,0 +1,90 @@
1
+ import { worldPoint } from "../geometry.js";
2
+ //#region src/board/render/stroke.ts
3
+ const HEAD_SPREAD = Math.PI / 6;
4
+ function dashPattern(dash, width) {
5
+ if (dash === "dashed") return [Math.max(6, width * 4), Math.max(4, width * 3)];
6
+ if (dash === "dotted") return [Math.max(.5, width * .01), Math.max(3, width * 2)];
7
+ return null;
8
+ }
9
+ function trimPolyline(path, trim) {
10
+ if (trim === void 0 || trim >= 1 || path.length < 2) return path;
11
+ if (trim <= 0) return [];
12
+ let total = 0;
13
+ for (let index = 1; index < path.length; index += 1) {
14
+ const a = path[index - 1];
15
+ const b = path[index];
16
+ total += Math.hypot(b.x - a.x, b.y - a.y);
17
+ }
18
+ let remaining = total * trim;
19
+ const out = [path[0]];
20
+ for (let index = 1; index < path.length; index += 1) {
21
+ const a = path[index - 1];
22
+ const b = path[index];
23
+ const length = Math.hypot(b.x - a.x, b.y - a.y);
24
+ if (length >= remaining) {
25
+ const t = length > 0 ? remaining / length : 0;
26
+ out.push(worldPoint(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t));
27
+ return out;
28
+ }
29
+ out.push(b);
30
+ remaining -= length;
31
+ }
32
+ return out;
33
+ }
34
+ function tracePolyline(graphics, path, pattern, closed = false) {
35
+ const points = closed && path.length > 2 ? [...path, path[0]] : path;
36
+ if (points.length < 2) return;
37
+ if (!pattern) {
38
+ const first = points[0];
39
+ graphics.moveTo(first.x, first.y);
40
+ for (let index = 1; index < points.length; index += 1) {
41
+ const point = points[index];
42
+ graphics.lineTo(point.x, point.y);
43
+ }
44
+ if (closed) graphics.closePath();
45
+ return;
46
+ }
47
+ const [dash, gap] = pattern;
48
+ let penDown = true;
49
+ let fresh = true;
50
+ let remaining = dash;
51
+ for (let index = 1; index < points.length; index += 1) {
52
+ const a = points[index - 1];
53
+ const b = points[index];
54
+ const dx = b.x - a.x;
55
+ const dy = b.y - a.y;
56
+ const length = Math.hypot(dx, dy);
57
+ let travelled = 0;
58
+ while (length - travelled > 1e-4) {
59
+ const step = Math.min(length - travelled, remaining);
60
+ if (penDown) {
61
+ const t0 = travelled / length;
62
+ const t1 = (travelled + step) / length;
63
+ if (fresh) graphics.moveTo(a.x + dx * t0, a.y + dy * t0);
64
+ fresh = false;
65
+ graphics.lineTo(a.x + dx * t1, a.y + dy * t1);
66
+ }
67
+ travelled += step;
68
+ remaining -= step;
69
+ if (remaining <= 1e-4) {
70
+ penDown = !penDown;
71
+ fresh = penDown;
72
+ remaining = penDown ? dash : gap;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ function traceArrowhead(graphics, tip, angle, size) {
78
+ graphics.moveTo(tip.x - size * Math.cos(angle - HEAD_SPREAD), tip.y - size * Math.sin(angle - HEAD_SPREAD)).lineTo(tip.x, tip.y).lineTo(tip.x - size * Math.cos(angle + HEAD_SPREAD), tip.y - size * Math.sin(angle + HEAD_SPREAD));
79
+ }
80
+ function endAngle(path, atStart) {
81
+ if (path.length < 2) return null;
82
+ const tip = atStart ? path[0] : path[path.length - 1];
83
+ for (let offset = 1; offset < path.length; offset += 1) {
84
+ const other = atStart ? path[offset] : path[path.length - 1 - offset];
85
+ if (Math.hypot(tip.x - other.x, tip.y - other.y) > .001) return Math.atan2(tip.y - other.y, tip.x - other.x);
86
+ }
87
+ return null;
88
+ }
89
+ //#endregion
90
+ export { dashPattern, endAngle, traceArrowhead, tracePolyline, trimPolyline };
@@ -1,30 +1,10 @@
1
1
  //#region src/board/render/text-measurement.d.ts
2
- /**
3
- * Canvas-backed text measurement.
4
- *
5
- * Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
6
- * browser and a headless export produce the same numbers. The core metrics
7
- * module owns the layout maths and only borrows this measurer, which is what
8
- * lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
9
- */
10
- /**
11
- * Route board text measurement through the active Pixi DOM adapter.
12
- *
13
- * Call this after swapping the adapter (see `../headless`), which drops the
14
- * cached measuring context. Anything that goes through `getBoardCardRenderer`
15
- * is already covered by `ensureBoardTextMeasurement`.
16
- */
2
+ declare const BOARD_FONT_STACKS: {
3
+ sans: string;
4
+ serif: string;
5
+ mono: string;
6
+ };
17
7
  declare function installBoardTextMeasurement(): void;
18
- /**
19
- * Install the canvas measurer once, unless it already is.
20
- *
21
- * The core metrics module falls back to a per-character estimate when no
22
- * measurer is set, which is off by ~90% for real text — wrong enough to
23
- * misplace every label. Rather than make that a step consumers can forget, the
24
- * renderer registry calls this before handing a renderer out. It cannot be an
25
- * import-time side effect: the package is marked side-effect free, so a bundler
26
- * is entitled to drop a module whose only job happens at load time.
27
- */
28
8
  declare function ensureBoardTextMeasurement(): void;
29
9
  //#endregion
30
- export { ensureBoardTextMeasurement, installBoardTextMeasurement };
10
+ export { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement };
@@ -1,15 +1,13 @@
1
- import { BOARD_FONT_STACK } from "../../protocol/dist/board-constants.js";
2
- import { setBoardTextMeasurer } from "../core/text-metrics.js";
1
+ import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../protocol/dist/board-constants.js";
2
+ import { setBoardTextMeasurer } from "../../protocol/dist/board-layout.js";
3
+ import { boardFontFamily } from "../core/text-metrics.js";
3
4
  import { DOMAdapter } from "pixi.js";
4
5
  //#region src/board/render/text-measurement.ts
5
- /**
6
- * Canvas-backed text measurement.
7
- *
8
- * Measurement goes through Pixi's `DOMAdapter` rather than `document`, so the
9
- * browser and a headless export produce the same numbers. The core metrics
10
- * module owns the layout maths and only borrows this measurer, which is what
11
- * lets `@neta-art/cohub/board` lay text out with no PixiJS installed.
12
- */
6
+ const BOARD_FONT_STACKS = {
7
+ sans: BOARD_FONT_STACK,
8
+ serif: "ui-serif, Georgia, \"Noto Serif CJK SC\", \"Songti SC\", serif",
9
+ mono: BOARD_MONO_FONT_STACK
10
+ };
13
11
  let measureContext;
14
12
  let installed = false;
15
13
  function getMeasureContext() {
@@ -21,37 +19,20 @@ function getMeasureContext() {
21
19
  }
22
20
  return measureContext;
23
21
  }
24
- function measureText(text, fontSize) {
22
+ function measureText(text, fontSize, fontWeight, font) {
25
23
  const context = getMeasureContext();
26
24
  if (!context) return null;
27
- context.font = `500 ${fontSize}px ${BOARD_FONT_STACK}`;
25
+ context.font = `${fontWeight} ${fontSize}px ${boardFontFamily(font, BOARD_FONT_STACKS)}`;
28
26
  return context.measureText(text).width;
29
27
  }
30
- /**
31
- * Route board text measurement through the active Pixi DOM adapter.
32
- *
33
- * Call this after swapping the adapter (see `../headless`), which drops the
34
- * cached measuring context. Anything that goes through `getBoardCardRenderer`
35
- * is already covered by `ensureBoardTextMeasurement`.
36
- */
37
28
  function installBoardTextMeasurement() {
38
29
  measureContext = void 0;
39
30
  installed = true;
40
31
  setBoardTextMeasurer(measureText);
41
32
  }
42
- /**
43
- * Install the canvas measurer once, unless it already is.
44
- *
45
- * The core metrics module falls back to a per-character estimate when no
46
- * measurer is set, which is off by ~90% for real text — wrong enough to
47
- * misplace every label. Rather than make that a step consumers can forget, the
48
- * renderer registry calls this before handing a renderer out. It cannot be an
49
- * import-time side effect: the package is marked side-effect free, so a bundler
50
- * is entitled to drop a module whose only job happens at load time.
51
- */
52
33
  function ensureBoardTextMeasurement() {
53
34
  if (installed) return;
54
35
  installBoardTextMeasurement();
55
36
  }
56
37
  //#endregion
57
- export { ensureBoardTextMeasurement, installBoardTextMeasurement };
38
+ export { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement };
@@ -1,52 +1,27 @@
1
+ import { BoardDocument } from "../protocol/dist/board-model.js";
1
2
  import { RequestSource } from "../protocol/dist/provenance.js";
2
- import { BoardTransactionsPage } from "../protocol/dist/board.js";
3
- import { BoardDocument } from "../protocol/dist/board-document.js";
3
+ import { BoardHistoryPage } from "../protocol/dist/board.js";
4
4
  import "../protocol/dist/index.js";
5
5
  //#region src/board/replay.d.ts
6
6
  type BoardReplayActorKind = "human" | "cli" | "agent";
7
- /** One step on the replay timeline. */
8
7
  type BoardReplayEntry = {
9
8
  version: number;
10
9
  actorId: string;
11
10
  kind: BoardReplayActorKind;
12
- /** Epoch milliseconds. */
13
11
  at: number;
14
- /** Whether the step changed anything visible in the render document. */
15
- visual: boolean;
16
12
  };
17
13
  type BoardReplayPlayer = ReturnType<typeof createBoardReplayPlayer>;
18
14
  declare function boardReplayActorKind(source: RequestSource | null): BoardReplayActorKind;
19
- /**
20
- * Create a player positioned at the live version described by the first page.
21
- *
22
- * `transactions` are newest-first as served; the player keeps them oldest-first.
23
- * Older pages can be added with `prepend`, and newer transactions that arrive
24
- * live can be added with `append`, so the timeline grows in both directions
25
- * without a reset.
26
- */
27
- declare function createBoardReplayPlayer(page: BoardTransactionsPage): {
28
- /** Oldest version the loaded transactions can rewind to. */
15
+ declare function createBoardReplayPlayer(live: BoardDocument, page: BoardHistoryPage): {
29
16
  readonly floor: number;
30
- /** Newest version known to the player. */
31
17
  readonly head: number;
32
18
  readonly entries: readonly BoardReplayEntry[];
33
- /** Current position. */
34
19
  readonly version: number;
35
20
  seek: (version: number) => number;
36
- documentAt: (version: number) => BoardDocument;
37
- /** Item ids touched by the transaction that produced `version`, for camera follow. */
21
+ documentAt(version: number): BoardDocument;
38
22
  changedItemIds(version: number): string[];
39
- /** Add an older page (as served, newest-first). */
40
- prepend(older: BoardTransactionsPage): void;
41
- /**
42
- * Add transactions that landed after the current head. Forward payloads are
43
- * enough to extend the timeline; the scrub position is untouched.
44
- *
45
- * Returns false when the page does not reach back to the head: the chain
46
- * would have a hole, so nothing is appended and the caller must fetch an
47
- * older page (`before` = the page's oldest version) and try again.
48
- */
49
- append(latest: BoardTransactionsPage): boolean;
23
+ prepend(older: BoardHistoryPage): void;
24
+ append(latest: BoardHistoryPage): boolean;
50
25
  };
51
26
  //#endregion
52
27
  export { BoardReplayActorKind, BoardReplayEntry, BoardReplayPlayer, boardReplayActorKind, createBoardReplayPlayer };
@@ -1,162 +1,39 @@
1
- import { BOARD_DOCUMENT_KIND } from "../protocol/dist/board.js";
2
- import { BoardAppearanceSchema, parseBoardDocument } from "../protocol/dist/board-document.js";
3
- import { boardNodeToAuthoringItem } from "../protocol/dist/board-codec.js";
4
- import { DEFAULT_BOARD_APPEARANCE, boardAuthoringItemToDocumentItem } from "./semantic-document.js";
1
+ import { applyBoardDelta } from "../protocol/dist/board-patch.js";
5
2
  //#region src/board/replay.ts
6
3
  function boardReplayActorKind(source) {
7
4
  if (!source) return "human";
8
5
  if (source.toolCallId) return "agent";
9
6
  return source.via === "cli" ? "cli" : "human";
10
7
  }
11
- function isRecord(value) {
12
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
- }
14
- /** Apply one stored operation (forward payload or inverse) to the mutable state. */
15
- function applyOperation(state, type, payload) {
16
- switch (type) {
17
- case "board.patch": {
18
- const patch = isRecord(payload.patch) ? payload.patch : {};
19
- if (isRecord(patch.metadata)) state.metadata = patch.metadata;
20
- if (isRecord(patch.metadataPatch)) state.metadata = {
21
- ...state.metadata,
22
- ...patch.metadataPatch
23
- };
24
- return;
25
- }
26
- case "node.create": {
27
- const node = payload.node;
28
- if (node) state.nodes.set(node.nodeId, node);
29
- return;
30
- }
31
- case "node.patch": {
32
- const nodeId = payload.nodeId;
33
- const current = state.nodes.get(nodeId);
34
- if (current && isRecord(payload.patch)) state.nodes.set(nodeId, {
35
- ...current,
36
- ...payload.patch,
37
- nodeId
38
- });
39
- return;
40
- }
41
- case "node.delete":
42
- state.nodes.delete(payload.nodeId);
43
- return;
44
- case "connection.create": {
45
- const connection = payload.connection;
46
- if (connection) state.connections.set(connection.id, connection);
47
- return;
48
- }
49
- case "connection.patch": {
50
- const connectionId = payload.connectionId;
51
- const current = state.connections.get(connectionId);
52
- if (current && isRecord(payload.patch)) state.connections.set(connectionId, {
53
- ...current,
54
- ...payload.patch,
55
- id: connectionId
56
- });
57
- return;
58
- }
59
- case "connection.delete":
60
- state.connections.delete(payload.connectionId);
61
- return;
62
- default: return;
63
- }
64
- }
65
- /**
66
- * Inverses are stored either as a full operation (`{ type, payload }`) or, for
67
- * `board.patch`, as a bare `{ patch }`. Normalise both to `(type, payload)`.
68
- */
69
- function inverseOf(operation) {
70
- const inverse = operation.inverse;
71
- if (!inverse) return null;
72
- if (typeof inverse.type === "string" && isRecord(inverse.payload)) return {
73
- type: inverse.type,
74
- payload: inverse.payload
75
- };
76
- if (operation.type === "board.patch" && isRecord(inverse.patch)) return {
77
- type: "board.patch",
78
- payload: { patch: inverse.patch }
79
- };
80
- return null;
81
- }
82
- function operationItemIds(operation) {
83
- const payload = operation.payload;
84
- switch (operation.type) {
85
- case "node.create": return isRecord(payload.node) && typeof payload.node.nodeId === "string" ? [payload.node.nodeId] : [];
86
- case "node.patch":
87
- case "node.delete": return typeof payload.nodeId === "string" ? [payload.nodeId] : [];
88
- default: return [];
89
- }
90
- }
91
- const VISUAL_OPERATION_TYPES = /* @__PURE__ */ new Set([
92
- "board.patch",
93
- "node.create",
94
- "node.patch",
95
- "node.delete",
96
- "connection.create",
97
- "connection.patch",
98
- "connection.delete"
99
- ]);
100
8
  function entryOf(transaction) {
101
9
  return {
102
10
  version: transaction.version,
103
11
  actorId: transaction.actorId,
104
12
  kind: boardReplayActorKind(transaction.source),
105
- at: Date.parse(transaction.createdAt),
106
- visual: transaction.operations.some((operation) => VISUAL_OPERATION_TYPES.has(operation.type))
107
- };
108
- }
109
- function appearanceOf(metadata) {
110
- const parsed = BoardAppearanceSchema.safeParse(metadata.appearance);
111
- return parsed.success ? parsed.data : DEFAULT_BOARD_APPEARANCE;
112
- }
113
- function project(state) {
114
- const nodes = [...state.nodes.values()].sort((a, b) => (a.orderKey ?? "").localeCompare(b.orderKey ?? ""));
115
- return parseBoardDocument({
116
- kind: BOARD_DOCUMENT_KIND,
117
- version: 1,
118
- appearance: appearanceOf(state.metadata),
119
- viewport: {
120
- x: 0,
121
- y: 0,
122
- zoom: 1
123
- },
124
- items: nodes.map((node) => boardAuthoringItemToDocumentItem(boardNodeToAuthoringItem(node))),
125
- connections: [...state.connections.values()]
126
- });
127
- }
128
- function stateFromSnapshot(snapshot) {
129
- return {
130
- nodes: new Map(snapshot.nodes.map(({ boardId: _boardId, version: _version, createdAt: _c, updatedAt: _u, ...node }) => [node.nodeId, node])),
131
- connections: new Map(snapshot.connections.map(({ boardId: _boardId, revision: _revision, createdAt: _c, updatedAt: _u, ...connection }) => [connection.id, connection])),
132
- metadata: snapshot.board.metadata
13
+ at: Date.parse(transaction.createdAt)
133
14
  };
134
15
  }
135
- /**
136
- * Create a player positioned at the live version described by the first page.
137
- *
138
- * `transactions` are newest-first as served; the player keeps them oldest-first.
139
- * Older pages can be added with `prepend`, and newer transactions that arrive
140
- * live can be added with `append`, so the timeline grows in both directions
141
- * without a reset.
142
- */
143
- function createBoardReplayPlayer(page) {
144
- if (!page.snapshot) throw new Error("Board replay needs the first transactions page (with snapshot).");
145
- const liveVersion = page.board.version;
146
- const newest = page.transactions.reduce((max, transaction) => Math.max(max, transaction.version), 0);
147
- if (page.transactions.length > 0 && newest !== liveVersion) throw new Error(`Board replay page is inconsistent: board is at v${liveVersion}, newest transaction is v${newest}.`);
148
- let transactions = [...page.transactions].sort((a, b) => a.version - b.version);
149
- let entries = transactions.map(entryOf);
150
- const state = stateFromSnapshot(page.snapshot);
151
- let cursor = liveVersion;
152
- const documents = /* @__PURE__ */ new Map();
153
- /** Oldest version reachable with the transactions loaded so far. */
154
- let floor = transactions[0]?.baseVersion ?? liveVersion;
155
- /**
156
- * Index of the first transaction whose version is `>= version`. Versions are
157
- * strictly increasing but not dense (a no-op mutation leaves a gap), so
158
- * binary-search by value rather than indexing by offset.
159
- */
16
+ function createBoardReplayPlayer(live, page) {
17
+ let transactions = [];
18
+ let entries = [];
19
+ let floor = page.version;
20
+ let document = live;
21
+ let cursor = page.version;
22
+ const documents = /* @__PURE__ */ new Map([[cursor, live]]);
23
+ function addOlder(older) {
24
+ const usable = [];
25
+ for (const transaction of [...older].sort((a, b) => b.version - a.version)) {
26
+ if (transaction.version > floor) continue;
27
+ if (!transaction.before || !transaction.after) break;
28
+ if (transaction.version !== floor) break;
29
+ usable.push(transaction);
30
+ floor = transaction.baseVersion;
31
+ }
32
+ usable.reverse();
33
+ transactions = [...usable, ...transactions];
34
+ entries = [...usable.map(entryOf), ...entries];
35
+ }
36
+ addOlder(page.transactions);
160
37
  function lowerBound(version) {
161
38
  let low = 0;
162
39
  let high = transactions.length;
@@ -167,85 +44,50 @@ function createBoardReplayPlayer(page) {
167
44
  }
168
45
  return low;
169
46
  }
170
- function transactionProducing(version) {
171
- const candidate = transactions[lowerBound(version)];
172
- return candidate?.version === version ? candidate : void 0;
173
- }
174
- /** Undo the transaction that produced `cursor`. */
175
- function stepBackward() {
176
- const transaction = transactionProducing(cursor);
177
- if (!transaction) return false;
178
- for (let index = transaction.operations.length - 1; index >= 0; index -= 1) {
179
- const inverse = inverseOf(transaction.operations[index]);
180
- if (inverse) applyOperation(state, inverse.type, inverse.payload);
181
- }
182
- cursor = transaction.baseVersion;
183
- return true;
184
- }
185
- function stepForward(next) {
186
- for (const operation of next.operations) applyOperation(state, operation.type, operation.payload);
187
- cursor = next.version;
188
- }
189
- function head() {
190
- return transactions.at(-1)?.version ?? liveVersion;
191
- }
192
- /**
193
- * Move to `version`, clamped to the loaded range. Returns the version reached,
194
- * which is the nearest loaded version at or below the request.
195
- */
47
+ const head = () => transactions.at(-1)?.version ?? page.version;
196
48
  function seek(version) {
197
49
  const target = Math.max(floor, Math.min(head(), version));
198
- while (cursor > target && stepBackward());
199
- for (let next = transactions[lowerBound(cursor + 1)]; next && next.version <= target; next = transactions[lowerBound(cursor + 1)]) stepForward(next);
50
+ while (cursor > target) {
51
+ const transaction = transactions[lowerBound(cursor)];
52
+ if (!transaction || transaction.version !== cursor || !transaction.before) break;
53
+ document = applyBoardDelta(document, transaction.before);
54
+ cursor = transaction.baseVersion;
55
+ }
56
+ for (let next = transactions[lowerBound(cursor + 1)]; next && next.version <= target; next = transactions[lowerBound(cursor + 1)]) {
57
+ if (!next.after) break;
58
+ document = applyBoardDelta(document, next.after);
59
+ cursor = next.version;
60
+ }
200
61
  return cursor;
201
62
  }
202
- function documentAt(version) {
203
- const reached = seek(version);
204
- const cached = documents.get(reached);
205
- if (cached) return cached;
206
- const document = project(state);
207
- documents.set(reached, document);
208
- return document;
209
- }
210
63
  return {
211
- /** Oldest version the loaded transactions can rewind to. */
212
64
  get floor() {
213
65
  return floor;
214
66
  },
215
- /** Newest version known to the player. */
216
67
  get head() {
217
68
  return head();
218
69
  },
219
70
  get entries() {
220
71
  return entries;
221
72
  },
222
- /** Current position. */
223
73
  get version() {
224
74
  return cursor;
225
75
  },
226
76
  seek,
227
- documentAt,
228
- /** Item ids touched by the transaction that produced `version`, for camera follow. */
77
+ documentAt(version) {
78
+ const reached = seek(version);
79
+ const cached = documents.get(reached);
80
+ if (cached) return cached;
81
+ documents.set(reached, document);
82
+ return document;
83
+ },
229
84
  changedItemIds(version) {
230
- return [...new Set(transactionProducing(version)?.operations.flatMap(operationItemIds))];
85
+ const transaction = transactions[lowerBound(version)];
86
+ return transaction?.version === version ? Object.keys(transaction.after?.items ?? {}) : [];
231
87
  },
232
- /** Add an older page (as served, newest-first). */
233
88
  prepend(older) {
234
- const current = floor;
235
- const fresh = older.transactions.filter((transaction) => transaction.version <= current).sort((a, b) => a.version - b.version);
236
- if (fresh.length === 0) return;
237
- transactions = [...fresh, ...transactions];
238
- entries = [...fresh.map(entryOf), ...entries];
239
- floor = fresh[0].baseVersion;
89
+ addOlder(older.transactions);
240
90
  },
241
- /**
242
- * Add transactions that landed after the current head. Forward payloads are
243
- * enough to extend the timeline; the scrub position is untouched.
244
- *
245
- * Returns false when the page does not reach back to the head: the chain
246
- * would have a hole, so nothing is appended and the caller must fetch an
247
- * older page (`before` = the page's oldest version) and try again.
248
- */
249
91
  append(latest) {
250
92
  const current = head();
251
93
  const fresh = latest.transactions.filter((transaction) => transaction.version > current).sort((a, b) => a.version - b.version);
@@ -0,0 +1,24 @@
1
+ import { BoardDocument, BoardItem } from "../../protocol/dist/board-model.js";
2
+ import { BoardPlaybackCommand, BoardPlaybackSnapshot } from "../../protocol/dist/board.js";
3
+ import "../../protocol/dist/index.js";
4
+ import { BoardCameraOverride, BoardColorResolver } from "../animation.js";
5
+ //#region src/board/runtime/board-player.d.ts
6
+ type BoardPlayerFrame = {
7
+ items: ReadonlyMap<string, BoardItem>;
8
+ camera: BoardCameraOverride;
9
+ time: number;
10
+ playbackTime: number | null;
11
+ running: boolean;
12
+ };
13
+ declare function playbackTimeAt(playback: BoardPlaybackSnapshot, document: BoardDocument, now: number): number | null;
14
+ declare function createBoardPlayer(options?: {
15
+ resolveColor?: BoardColorResolver;
16
+ }): {
17
+ reset(): void;
18
+ enter(ids: readonly string[], current: BoardDocument): void;
19
+ frame(current: BoardDocument, playback: BoardPlaybackSnapshot | null, now: number, wallNow: number, exclude: string | null): BoardPlayerFrame;
20
+ };
21
+ type BoardPlayer = ReturnType<typeof createBoardPlayer>;
22
+ declare function nextLocalPlayback(current: BoardPlaybackSnapshot | null, command: BoardPlaybackCommand, document: BoardDocument, now?: number): BoardPlaybackSnapshot | null;
23
+ //#endregion
24
+ export { BoardPlayer, BoardPlayerFrame, createBoardPlayer, nextLocalPlayback, playbackTimeAt };