@neta-art/cohub 8.23.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 +7 -27
  30. package/dist/board/headless/index.js +20 -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 +2296 -3864
  91. package/dist/chunks/host-bridge.d.ts +253 -0
  92. package/dist/chunks/http.d.ts +238 -58
  93. package/dist/chunks/http.js +1061 -1034
  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 +76 -193
  101. package/dist/index.js +2037 -1191
  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
@@ -44,17 +44,9 @@ const DEFAULT_BOARD_VIEWPORT = {
44
44
  y: 0,
45
45
  zoom: 1
46
46
  };
47
- /** Smallest an item can be resized to, in world units. */
48
47
  const MIN_ITEM_SIZE = 24;
49
- /** Extra world-space padding applied when fitting content into view. */
50
48
  const FIT_PADDING = 64;
51
- /**
52
- * Fraction of the viewport's larger dimension used as an off-screen margin for
53
- * both card culling and texture preloading. Keeping them equal means a card's
54
- * texture is already requested before the card scrolls into view.
55
- */
56
49
  const VIEWPORT_MARGIN_RATIO = .5;
57
- /** Viewport plus margin, snapped to a grid so small pans return the same rect. */
58
50
  function stableCullRect(view) {
59
51
  const margin = Math.max(view.width, view.height, 1) * VIEWPORT_MARGIN_RATIO;
60
52
  const step = 2 ** Math.floor(Math.log2(margin / 2));
@@ -73,7 +65,6 @@ function clampZoom(zoom) {
73
65
  const value = Number.isFinite(zoom) ? zoom : DEFAULT_BOARD_VIEWPORT.zoom;
74
66
  return Math.min(8, Math.max(MIN_BOARD_ZOOM, value));
75
67
  }
76
- /** Keep transient camera state finite without discarding its last valid axes. */
77
68
  function normalizeViewport(viewport, fallback = DEFAULT_BOARD_VIEWPORT) {
78
69
  const fallbackX = Number.isFinite(fallback.x) ? fallback.x : DEFAULT_BOARD_VIEWPORT.x;
79
70
  const fallbackY = Number.isFinite(fallback.y) ? fallback.y : DEFAULT_BOARD_VIEWPORT.y;
@@ -166,7 +157,6 @@ function rotatePointAround(point, center, angleRad) {
166
157
  y: center.y + dx * sin + dy * cos
167
158
  };
168
159
  }
169
- /** Axis-aligned bounding box of a (possibly rotated) frame. */
170
160
  function itemBounds(frame) {
171
161
  const rotation = frame.rotation || 0;
172
162
  if (rotation === 0) return frameRect(frame);
@@ -188,7 +178,6 @@ function itemBounds(frame) {
188
178
  function selectionBounds(frames) {
189
179
  return unionRects(frames.map(itemBounds));
190
180
  }
191
- /** Exact point-in-frame test, accounting for rotation. */
192
181
  function frameContainsPoint(frame, point) {
193
182
  const rect = frameRect(frame);
194
183
  const rotation = frame.rotation || 0;
@@ -196,7 +185,6 @@ function frameContainsPoint(frame, point) {
196
185
  if (rotation !== 0) local = rotatePointAround(point, rectCenter(rect), -degToRad(rotation));
197
186
  return rectContainsPoint(rect, local);
198
187
  }
199
- /** World position of a resize handle on a single (possibly rotated) frame. */
200
188
  function frameHandlePosition(frame, handle) {
201
189
  const rect = frameRect(frame);
202
190
  const position = handlePosition(rect, handle);
@@ -204,7 +192,6 @@ function frameHandlePosition(frame, handle) {
204
192
  if (rotation === 0) return position;
205
193
  return rotatePointAround(position, rectCenter(rect), degToRad(rotation));
206
194
  }
207
- /** Corners in clockwise order, following the frame's rotation. */
208
195
  function frameCorners(frame) {
209
196
  return [
210
197
  "nw",
@@ -213,9 +200,7 @@ function frameCorners(frame) {
213
200
  "sw"
214
201
  ].map((handle) => frameHandlePosition(frame, handle));
215
202
  }
216
- /** Screen-space radius (px) within which a pointer grabs a handle. */
217
203
  const HANDLE_HIT_RADIUS = 8;
218
- /** Screen-space offset (px) from the selection's lower-facing edge. */
219
204
  const ROTATION_HANDLE_OFFSET = 28;
220
205
  function frameRayIntersection(frame, pointer) {
221
206
  const center = rectCenter(frame);
@@ -257,7 +242,6 @@ const EDGE_RESIZE_HANDLES = [
257
242
  "s",
258
243
  "w"
259
244
  ];
260
- /** Screen-space depth of the rotation zone outside each resize corner. */
261
245
  const CORNER_ROTATION_ZONE_WIDTH = 16;
262
246
  const RESIZE_HANDLES = [
263
247
  "nw",
@@ -269,10 +253,6 @@ const RESIZE_HANDLES = [
269
253
  "sw",
270
254
  "w"
271
255
  ];
272
- /**
273
- * Hit-test the outward quadrant around each corner, outside its resize target.
274
- * This gives fine pointers Figma-style corner rotation without adding chrome.
275
- */
276
256
  function frameCornerRotationHandleAt(frame, point, zoom, screenRadius = 8) {
277
257
  const rect = frameRect(frame);
278
258
  const center = rectCenter(rect);
@@ -291,10 +271,6 @@ function frameCornerRotationHandleAt(frame, point, zoom, screenRadius = 8) {
291
271
  }
292
272
  return null;
293
273
  }
294
- /**
295
- * Hit-test the continuous edge zones of a rotated frame. Corners are inset so
296
- * their resize handles retain priority even on small nodes.
297
- */
298
274
  function frameEdgeHandleAt(frame, point, zoom, screenRadius = 8) {
299
275
  const rect = frameRect(frame);
300
276
  const center = rectCenter(rect);
@@ -327,7 +303,6 @@ function frameEdgeHandleAt(frame, point, zoom, screenRadius = 8) {
327
303
  }
328
304
  return closest;
329
305
  }
330
- /** Direction of a handle from the rect center: -1, 0, or 1 per axis. */
331
306
  const HANDLE_DIRECTION = {
332
307
  nw: {
333
308
  x: -1,
@@ -362,12 +337,10 @@ const HANDLE_DIRECTION = {
362
337
  y: 0
363
338
  }
364
339
  };
365
- /** Position of a handle on an unrotated rect. */
366
340
  function handlePosition(rect, handle) {
367
341
  const direction = HANDLE_DIRECTION[handle];
368
342
  return worldPoint(rect.x + rect.width / 2 + direction.x * rect.width / 2, rect.y + rect.height / 2 + direction.y * rect.height / 2);
369
343
  }
370
- /** Resize a frame to exact dimensions while keeping the opposite handle fixed. */
371
344
  function resizeFrameToSize(frame, handle, width, height) {
372
345
  const direction = HANDLE_DIRECTION[handle];
373
346
  const rect = frameRect(frame);
@@ -397,11 +370,6 @@ function resizeFrameToSize(frame, handle, width, height) {
397
370
  rotation: frame.rotation || 0
398
371
  };
399
372
  }
400
- /**
401
- * Resize a frame by dragging a handle to a world-space pointer position.
402
- * The edge/corner opposite the handle stays anchored. Works correctly for
403
- * rotated frames by performing the resize in the frame's local space.
404
- */
405
373
  function resizeFrame(frame, handle, pointer, minSize = 24, keepAspect = false) {
406
374
  const direction = HANDLE_DIRECTION[handle];
407
375
  const rect = frameRect(frame);
@@ -434,10 +402,6 @@ function resizeFrame(frame, handle, pointer, minSize = 24, keepAspect = false) {
434
402
  }
435
403
  return resizeFrameToSize(frame, handle, width, height);
436
404
  }
437
- /**
438
- * Proportionally scale a group of frames by dragging a corner handle of their
439
- * combined bounds. Individual rotations are preserved.
440
- */
441
405
  function scaleFrames(frames, bounds, handle, pointer, minSize = 24, scaleRange) {
442
406
  const direction = HANDLE_DIRECTION[handle];
443
407
  if (direction.x === 0 || direction.y === 0) return frames;
@@ -470,14 +434,9 @@ function scaleFrames(frames, bounds, handle, pointer, minSize = 24, scaleRange)
470
434
  };
471
435
  });
472
436
  }
473
- /** Angle (degrees) from a center to a world-space pointer. */
474
437
  function angleFromCenter(center, pointer) {
475
438
  return radToDeg(Math.atan2(pointer.y - center.y, pointer.x - center.x));
476
439
  }
477
- /**
478
- * Rotate a set of frames around a pivot by a delta (degrees). Each frame's
479
- * center orbits the pivot and its own rotation increases by the delta.
480
- */
481
440
  function rotateFrames(frames, pivot, deltaDeg) {
482
441
  const rad = degToRad(deltaDeg);
483
442
  return frames.map((frame) => {
@@ -491,14 +450,12 @@ function rotateFrames(frames, pivot, deltaDeg) {
491
450
  };
492
451
  });
493
452
  }
494
- /** Normalize a rotation into the [-180, 180] range for tidy persistence. */
495
453
  function normalizeRotation(degrees) {
496
454
  let value = degrees % 360;
497
455
  if (value > 180) value -= 360;
498
456
  if (value < -180) value += 360;
499
457
  return Math.abs(value) < .01 ? 0 : value;
500
458
  }
501
- /** Convert a surface-relative screen point into world space. */
502
459
  function pointToWorld(point, viewport) {
503
460
  return worldPoint((point.x - viewport.x) / viewport.zoom, (point.y - viewport.y) / viewport.zoom);
504
461
  }
@@ -508,7 +465,6 @@ function worldToScreen(point, viewport) {
508
465
  function screenToWorld(clientX, clientY, rect, viewport) {
509
466
  return pointToWorld(screenPoint(clientX - rect.left, clientY - rect.top), viewport);
510
467
  }
511
- /** Zoom while keeping a surface-relative screen point fixed under the cursor. */
512
468
  function zoomAround(viewport, anchor, nextZoom) {
513
469
  const current = normalizeViewport(viewport);
514
470
  const zoom = Number.isFinite(nextZoom) ? clampZoom(nextZoom) : current.zoom;
@@ -534,10 +490,7 @@ function cameraForState(state, surface) {
534
490
  zoom
535
491
  });
536
492
  }
537
- function cameraForRect(content, surface, options = {
538
- fit: "contain",
539
- padding: 32
540
- }) {
493
+ function cameraForRect(content, surface, options = {}) {
541
494
  const padding = Math.max(0, Number.isFinite(options.padding) ? options.padding : 32);
542
495
  const availableW = Math.max(1, surface.width - padding * 2);
543
496
  const availableH = Math.max(1, surface.height - padding * 2);
@@ -553,26 +506,14 @@ function cameraForRect(content, surface, options = {
553
506
  }, surface);
554
507
  }
555
508
  function rectForCameraFocus(focus, getFrame) {
556
- if (focus.type === "rect") return focus.rect;
557
- if (focus.type === "item") {
558
- const frame = getFrame(focus.itemId);
559
- return frame ? itemBounds(frame) : null;
560
- }
561
- if (focus.type === "frame") {
562
- const frame = getFrame(focus.frameId);
563
- return frame ? itemBounds(frame) : null;
564
- }
565
- const frames = focus.itemIds.flatMap((id) => {
566
- const frame = getFrame(id);
567
- return frame ? [frame] : [];
568
- });
569
- return frames.length === focus.itemIds.length ? selectionBounds(frames) : null;
509
+ if (typeof focus !== "string") return focus;
510
+ const frame = getFrame(focus);
511
+ return frame ? itemBounds(frame) : null;
570
512
  }
571
- function cameraForFocus(params, getFrame, surface) {
572
- const rect = rectForCameraFocus(params.focus, getFrame);
573
- return rect ? cameraForRect(rect, surface, params) : null;
513
+ function cameraForFocus(focus, getFrame, surface, options = {}) {
514
+ const rect = rectForCameraFocus(focus, getFrame);
515
+ return rect ? cameraForRect(rect, surface, options) : null;
574
516
  }
575
- /** Compute a viewport that frames the given world rect within the surface. */
576
517
  function fitToContent(content, surface, padding = 64) {
577
518
  const availableW = Math.max(1, surface.width - padding * 2);
578
519
  const availableH = Math.max(1, surface.height - padding * 2);
@@ -583,7 +524,6 @@ function fitToContent(content, surface, padding = 64) {
583
524
  zoom
584
525
  };
585
526
  }
586
- /** World-space rectangle currently visible in the board stage. */
587
527
  function visibleWorldRect(viewport, surfaceWidth, surfaceHeight) {
588
528
  const width = Math.max(0, surfaceWidth);
589
529
  const height = Math.max(0, surfaceHeight);
@@ -1,5 +1,8 @@
1
- import { BoardDocument } from "../../protocol/dist/board-document.js";
1
+ import { BoardDocument } from "../../protocol/dist/board-model.js";
2
+ import "../../protocol/dist/index.js";
3
+ import { BoardSketchHost } from "../render/renderers/board-renderer-registry.js";
2
4
  import { BoardExportOptions, BoardExportResult } from "../export/index.js";
5
+ import { createBoardHeadlessSketchHost } from "./sketch.js";
3
6
  import { Renderer } from "pixi.js";
4
7
  //#region src/board/headless/index.d.ts
5
8
  type HeadlessCanvasModule = {
@@ -8,51 +11,29 @@ type HeadlessCanvasModule = {
8
11
  GlobalFonts: {
9
12
  registerFromPath: (path: string, name?: string) => unknown;
10
13
  has: (name: string) => boolean;
14
+ setAlias?: (fontName: string, alias: string) => boolean;
11
15
  };
12
16
  };
13
17
  type BoardHeadlessFont = {
14
- /** Absolute path to a font file (woff2, ttf and otf all work). */
15
18
  path: string;
16
- /** Family name renderers should ask for. Defaults to the board text family. */
17
19
  family?: string;
18
20
  };
19
21
  type BoardHeadlessRendererOptions = {
20
- /**
21
- * Fonts to register before the first measurement. Board text asks for
22
- * "Geist"; without a matching family the platform substitutes one, which
23
- * changes glyph shapes and text metrics.
24
- */
25
22
  fonts?: BoardHeadlessFont[];
26
- /** Override the canvas module (tests, or a pre-imported instance). */
27
23
  canvasModule?: HeadlessCanvasModule;
28
24
  };
29
- /**
30
- * A decoded image, opaque to callers.
31
- *
32
- * Node consumers (the CLI) should not need PixiJS in their own type surface just
33
- * to hand textures back to the exporter, so the concrete `Texture` stays inside
34
- * this module.
35
- */
36
25
  type BoardHeadlessTexture = {
37
26
  readonly __boardTexture: unique symbol;
38
27
  };
39
28
  type BoardHeadlessRenderer = {
40
29
  renderer: Renderer;
41
30
  canvasModule: HeadlessCanvasModule;
42
- /** Decode image bytes into a texture the exporter accepts. */
43
31
  decodeImage: (bytes: Uint8Array, mimeType?: string) => Promise<BoardHeadlessTexture>;
44
32
  destroy: () => void;
45
33
  };
46
- /**
47
- * Create a Canvas2D renderer backed by Skia.
48
- *
49
- * `skipExtensionImports` is essential: it stops Pixi from auto-loading its
50
- * browser environment bundle, which would pull in DOM-only pipes.
51
- */
52
34
  declare function createBoardHeadlessRenderer(options?: BoardHeadlessRendererOptions): Promise<BoardHeadlessRenderer>;
53
35
  type BoardHeadlessExportFormat = "png" | "jpeg" | "webp";
54
36
  type BoardHeadlessExportOptions = Omit<BoardExportOptions, "textures" | "backgroundImage"> & {
55
- /** Textures from `decodeImage`, keyed by image key. */
56
37
  textures?: Map<string, BoardHeadlessTexture>;
57
38
  backgroundImage?: {
58
39
  texture: BoardHeadlessTexture;
@@ -60,8 +41,8 @@ type BoardHeadlessExportOptions = Omit<BoardExportOptions, "textures" | "backgro
60
41
  position: "center" | "top" | "bottom" | "left" | "right";
61
42
  opacity: number;
62
43
  };
44
+ sketches?: BoardSketchHost;
63
45
  format?: BoardHeadlessExportFormat;
64
- /** JPEG/WebP quality, 0–1. Ignored for PNG. */
65
46
  quality?: number;
66
47
  };
67
48
  type BoardHeadlessExportResult = Omit<BoardExportResult, "canvas"> & {
@@ -69,7 +50,6 @@ type BoardHeadlessExportResult = Omit<BoardExportResult, "canvas"> & {
69
50
  format: BoardHeadlessExportFormat;
70
51
  };
71
52
  declare function boardHeadlessMimeType(format: BoardHeadlessExportFormat): string;
72
- /** Render a document to encoded image bytes. Returns null for an empty region. */
73
53
  declare function exportBoardImageBytes(headless: BoardHeadlessRenderer, document: BoardDocument, options?: BoardHeadlessExportOptions): BoardHeadlessExportResult | null;
74
54
  //#endregion
75
- export { BoardHeadlessExportFormat, BoardHeadlessExportOptions, BoardHeadlessExportResult, BoardHeadlessFont, BoardHeadlessRenderer, BoardHeadlessRendererOptions, BoardHeadlessTexture, HeadlessCanvasModule, boardHeadlessMimeType, createBoardHeadlessRenderer, exportBoardImageBytes };
55
+ export { BoardHeadlessExportFormat, BoardHeadlessExportOptions, BoardHeadlessExportResult, BoardHeadlessFont, BoardHeadlessRenderer, BoardHeadlessRendererOptions, BoardHeadlessTexture, HeadlessCanvasModule, boardHeadlessMimeType, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes };
@@ -1,15 +1,23 @@
1
1
  import "../../protocol/dist/board-constants.js";
2
2
  import { installBoardTextMeasurement } from "../render/text-measurement.js";
3
3
  import { renderBoardExport } from "../export/index.js";
4
+ import { createBoardHeadlessSketchHost } from "./sketch.js";
5
+ import { CanvasGraphicsContextSystem, CanvasGraphicsPipe, CanvasRenderer, CanvasRendererTextSystem, CanvasTextPipe, CanvasTextSystem, DOMAdapter, GraphicsContextSystem, GraphicsPipe, ImageSource, Texture, extensions } from "pixi.js";
4
6
  //#region src/board/headless/index.ts
5
- /**
6
- * A minimal WebGL stand-in.
7
- *
8
- * `ParticleContainerPipe` builds a `GlProgram` when the renderer registers its
9
- * pipes, and that probes shader precision through a throwaway context — even
10
- * though the Canvas2D backend never draws with it. Returning a stub keeps
11
- * registration working; nothing here is ever used to render.
12
- */
7
+ const SYSTEM_UI_FAMILIES = [
8
+ "Noto Sans",
9
+ "DejaVu Sans",
10
+ "Liberation Sans",
11
+ "FreeSans",
12
+ "Helvetica Neue",
13
+ "Arial",
14
+ "Segoe UI"
15
+ ];
16
+ function aliasSystemUi(fonts) {
17
+ if (!fonts.setAlias || fonts.has("system-ui")) return;
18
+ const family = SYSTEM_UI_FAMILIES.find((name) => fonts.has(name));
19
+ if (family) fonts.setAlias(family, "system-ui");
20
+ }
13
21
  const WEBGL_PROBE_STUB = {
14
22
  getShaderPrecisionFormat: () => ({
15
23
  precision: 1,
@@ -29,7 +37,6 @@ async function loadCanvasModule() {
29
37
  throw new Error("Board image export needs the optional '@napi-rs/canvas' package. Install it with: npm i @napi-rs/canvas", { cause });
30
38
  }
31
39
  }
32
- /** Node has no rAF; Pixi's scheduler only needs a callback pump. */
33
40
  function installAnimationFrameShim() {
34
41
  const globals = globalThis;
35
42
  if (globals.requestAnimationFrame) return () => {};
@@ -54,17 +61,11 @@ function installAnimationFrameShim() {
54
61
  globals.cancelAnimationFrame = void 0;
55
62
  };
56
63
  }
57
- /**
58
- * Create a Canvas2D renderer backed by Skia.
59
- *
60
- * `skipExtensionImports` is essential: it stops Pixi from auto-loading its
61
- * browser environment bundle, which would pull in DOM-only pipes.
62
- */
63
64
  async function createBoardHeadlessRenderer(options = {}) {
64
65
  const canvasModule = options.canvasModule ?? await loadCanvasModule();
65
- const { CanvasGraphicsContextSystem, CanvasGraphicsPipe, CanvasRenderer, CanvasRendererTextSystem, CanvasTextPipe, CanvasTextSystem, DOMAdapter, extensions, GraphicsContextSystem, GraphicsPipe, ImageSource, Texture } = await import("pixi.js");
66
66
  extensions.add(CanvasGraphicsPipe, GraphicsPipe, CanvasGraphicsContextSystem, GraphicsContextSystem, CanvasRendererTextSystem, CanvasTextSystem, CanvasTextPipe);
67
67
  for (const font of options.fonts ?? []) canvasModule.GlobalFonts.registerFromPath(font.path, font.family ?? "Geist");
68
+ aliasSystemUi(canvasModule.GlobalFonts);
68
69
  function createCanvas(width = 1, height = 1) {
69
70
  const canvas = canvasModule.createCanvas(Math.max(1, Math.ceil(width)), Math.max(1, Math.ceil(height)));
70
71
  const nativeGetContext = canvas.getContext.bind(canvas);
@@ -129,11 +130,11 @@ const MIME = {
129
130
  function boardHeadlessMimeType(format) {
130
131
  return MIME[format];
131
132
  }
132
- /** Render a document to encoded image bytes. Returns null for an empty region. */
133
133
  function exportBoardImageBytes(headless, document, options = {}) {
134
- const { format = "png", quality = .92, textures, backgroundImage, ...rest } = options;
134
+ const { format = "png", quality = .92, textures, sketches, backgroundImage, ...rest } = options;
135
135
  const result = renderBoardExport(headless.renderer, document, {
136
136
  ...rest,
137
+ ...sketches ? { sketches } : {},
137
138
  ...textures ? { textures } : {},
138
139
  ...backgroundImage ? { backgroundImage: {
139
140
  ...backgroundImage,
@@ -151,4 +152,4 @@ function exportBoardImageBytes(headless, document, options = {}) {
151
152
  };
152
153
  }
153
154
  //#endregion
154
- export { boardHeadlessMimeType, createBoardHeadlessRenderer, exportBoardImageBytes };
155
+ export { boardHeadlessMimeType, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes };
@@ -0,0 +1,19 @@
1
+ import { BoardSketchItem } from "../../protocol/dist/board-model.js";
2
+ import "../../protocol/dist/index.js";
3
+ import { SceneItem } from "../core/scene.js";
4
+ import { BoardSketchHost } from "../render/renderers/board-renderer-registry.js";
5
+ import { BoardHeadlessRenderer } from "./index.js";
6
+ //#region src/board/headless/sketch.d.ts
7
+ type SketchFrame = SceneItem<BoardSketchItem>;
8
+ type PreparedSketchHost = BoardSketchHost & {
9
+ prepare: (items: readonly SketchFrame[], times: readonly number[], zoom: number) => Promise<void>;
10
+ destroy: () => void;
11
+ };
12
+ type BoardHeadlessSketchOptions = {
13
+ readModule: (src: string) => Promise<string>;
14
+ timeoutMs?: number;
15
+ readTimeoutMs?: number;
16
+ };
17
+ declare function createBoardHeadlessSketchHost(headless: BoardHeadlessRenderer, options: BoardHeadlessSketchOptions): PreparedSketchHost;
18
+ //#endregion
19
+ export { BoardHeadlessSketchOptions, createBoardHeadlessSketchHost };
@@ -0,0 +1,146 @@
1
+ import { ImageSource, Texture } from "pixi.js";
2
+ import { Script, createContext } from "node:vm";
3
+ //#region src/board/headless/sketch.ts
4
+ const MAX_EDGE = 2048;
5
+ const DEFAULT_TIMEOUT_MS = 2e3;
6
+ const DEFAULT_READ_TIMEOUT_MS = 15e3;
7
+ const MAX_PREPARE_CONCURRENCY = 4;
8
+ function transformModule(code, src) {
9
+ return `(function(module, exports) {\n${code.replace(/export\s+default\s+/g, "module.exports.default = ").replace(/export\s+(async\s+)?function\s+draw\b/g, (_match, asyncKeyword = "") => `module.exports.draw = ${asyncKeyword}function draw`).replace(/export\s+(const|let|var)\s+draw\s*=/g, "module.exports.draw =")}\n})(module, module.exports);\n//# sourceURL=${JSON.stringify(src)}`;
10
+ }
11
+ function runModule(code, src, timeoutMs) {
12
+ const module = { exports: {} };
13
+ const context = createContext({
14
+ module,
15
+ exports: module.exports,
16
+ console: {
17
+ log() {},
18
+ warn() {},
19
+ error() {}
20
+ },
21
+ fetch: void 0,
22
+ XMLHttpRequest: void 0,
23
+ WebSocket: void 0,
24
+ EventSource: void 0,
25
+ importScripts: void 0,
26
+ indexedDB: void 0,
27
+ caches: void 0,
28
+ process: void 0,
29
+ require: void 0
30
+ });
31
+ new Script(transformModule(code, src), { filename: src }).runInContext(context, { timeout: timeoutMs });
32
+ const value = module.exports.default && typeof module.exports.default === "object" ? {
33
+ ...module.exports.default,
34
+ ...module.exports
35
+ } : module.exports;
36
+ if (typeof value.draw !== "function") throw new Error(`${src} does not export draw(ctx, frame)`);
37
+ if (value.draw.constructor?.name === "AsyncFunction") throw new Error(`${src} must export a synchronous draw(ctx, frame) for headless export.`);
38
+ return {
39
+ module: value,
40
+ context
41
+ };
42
+ }
43
+ function withTimeout(promise, timeoutMs, src) {
44
+ return new Promise((resolve, reject) => {
45
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${src} exceeded the ${timeoutMs}ms sketch timeout.`)), timeoutMs);
46
+ promise.then(resolve, reject).finally(() => clearTimeout(timer));
47
+ });
48
+ }
49
+ function frameKey(item, time, scale) {
50
+ return `${item.id}|${time}|${item.frame.width}|${item.frame.height}|${scale}|${item.props.seed ?? item.id}|${JSON.stringify(item.props.params)}`;
51
+ }
52
+ function frameScale(resolution, width, height) {
53
+ return Math.min(4, Math.max(.25, resolution), MAX_EDGE / Math.max(1, width), MAX_EDGE / Math.max(1, height));
54
+ }
55
+ function textureFromCanvas(canvas) {
56
+ return new Texture({ source: new ImageSource({
57
+ resource: canvas,
58
+ width: canvas.width,
59
+ height: canvas.height
60
+ }) });
61
+ }
62
+ function createBoardHeadlessSketchHost(headless, options) {
63
+ const textures = /* @__PURE__ */ new Map();
64
+ const errors = /* @__PURE__ */ new Map();
65
+ const lastErrors = /* @__PURE__ */ new Map();
66
+ const modules = /* @__PURE__ */ new Map();
67
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
68
+ const readTimeoutMs = options.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
69
+ const loadModule = (src) => {
70
+ let pending = modules.get(src);
71
+ if (!pending) {
72
+ pending = withTimeout(options.readModule(src), readTimeoutMs, src).then((code) => runModule(code, src, timeoutMs));
73
+ modules.set(src, pending);
74
+ pending.catch(() => modules.delete(src));
75
+ }
76
+ return pending;
77
+ };
78
+ const render = async (item, time, scale) => {
79
+ const key = frameKey(item, time, scale);
80
+ if (textures.has(key) || errors.has(key)) return;
81
+ try {
82
+ const runtime = await loadModule(item.props.src);
83
+ const width = Math.max(1, Math.round(item.frame.width * scale));
84
+ const height = Math.max(1, Math.round(item.frame.height * scale));
85
+ const canvas = headless.canvasModule.createCanvas(width, height);
86
+ const context = canvas.getContext("2d");
87
+ context.scale(scale, scale);
88
+ const frame = {
89
+ ...item.props,
90
+ t: time,
91
+ width: item.frame.width,
92
+ height: item.frame.height,
93
+ seed: item.props.seed ?? item.id
94
+ };
95
+ runtime.context.__boardDraw = runtime.module.draw;
96
+ runtime.context.__boardDrawContext = context;
97
+ runtime.context.__boardFrame = frame;
98
+ try {
99
+ const drawResult = new Script("__boardDraw(__boardDrawContext, __boardFrame)", { filename: item.props.src }).runInContext(runtime.context, { timeout: timeoutMs });
100
+ if (drawResult && typeof drawResult.then === "function") throw new Error(`${item.props.src} must export a synchronous draw(ctx, frame) for headless export.`);
101
+ } finally {
102
+ delete runtime.context.__boardDraw;
103
+ delete runtime.context.__boardDrawContext;
104
+ delete runtime.context.__boardFrame;
105
+ }
106
+ textures.set(key, textureFromCanvas(canvas));
107
+ lastErrors.delete(item.id);
108
+ } catch (error) {
109
+ errors.set(key, error instanceof Error ? error.message : String(error));
110
+ lastErrors.set(item.id, error instanceof Error ? error.message : String(error));
111
+ }
112
+ };
113
+ return {
114
+ frame(item, time, resolution) {
115
+ const scale = frameScale(resolution, item.frame.width, item.frame.height);
116
+ return textures.get(frameKey(item, time, scale)) ?? null;
117
+ },
118
+ error: (id) => lastErrors.get(id) ?? null,
119
+ release: () => {},
120
+ async prepare(items, times, zoom) {
121
+ const sketches = items.filter((item) => item.type === "sketch");
122
+ const jobs = [];
123
+ for (const item of sketches) {
124
+ const scale = frameScale(zoom, item.frame.width, item.frame.height);
125
+ for (const time of times) jobs.push(() => render(item, time, scale));
126
+ }
127
+ let next = 0;
128
+ const worker = async () => {
129
+ while (next < jobs.length) {
130
+ const job = jobs[next++];
131
+ if (job) await job();
132
+ }
133
+ };
134
+ await Promise.all(Array.from({ length: Math.min(MAX_PREPARE_CONCURRENCY, jobs.length) }, worker));
135
+ },
136
+ destroy() {
137
+ for (const texture of textures.values()) texture.destroy(true);
138
+ textures.clear();
139
+ errors.clear();
140
+ lastErrors.clear();
141
+ modules.clear();
142
+ }
143
+ };
144
+ }
145
+ //#endregion
146
+ export { createBoardHeadlessSketchHost };
@@ -1,16 +1,6 @@
1
- import { BoardItem } from "../protocol/dist/board-document.js";
1
+ import { BoardSceneItem } from "./core/scene.js";
2
2
  //#region src/board/image-key.d.ts
3
- /**
4
- * Stable cache key for an item's image resource, or null when it has none.
5
- *
6
- * Space files and remote URLs are namespaced so they can never collide, and file
7
- * cards contribute their cover image — which means covers ride the exact same
8
- * reference counting, LRU cooling pool and viewport preloading as image nodes,
9
- * with no second loader to keep in step. The exporter resolves the same keys, so
10
- * an exported card shows the same picture the editor does.
11
- */
12
- declare function imageAssetKey(item: BoardItem): string | null;
13
- /** The source a key resolves from, recovered from its namespace prefix. */
3
+ declare function imageAssetKey(item: BoardSceneItem): string | null;
14
4
  declare function boardImageKeySource(key: string): {
15
5
  kind: "file" | "url";
16
6
  value: string;
@@ -1,29 +1,19 @@
1
1
  import { featuredTaskArtifact, taskArtifactPreviewUrl } from "./task.js";
2
2
  //#region src/board/image-key.ts
3
- /**
4
- * Stable cache key for an item's image resource, or null when it has none.
5
- *
6
- * Space files and remote URLs are namespaced so they can never collide, and file
7
- * cards contribute their cover image — which means covers ride the exact same
8
- * reference counting, LRU cooling pool and viewport preloading as image nodes,
9
- * with no second loader to keep in step. The exporter resolves the same keys, so
10
- * an exported card shows the same picture the editor does.
11
- */
12
3
  function imageAssetKey(item) {
13
- if (item.type === "image") return `file:${item.ref.path}`;
4
+ if (item.type === "image") return `file:${item.props.src}`;
14
5
  if (item.type === "file") {
15
- const snapshot = item.snapshot;
6
+ const snapshot = item.props.snapshot;
16
7
  if (snapshot?.coverPath) return `file:${snapshot.coverPath}`;
17
8
  if (snapshot?.coverUrl) return `url:${snapshot.coverUrl}`;
18
9
  return null;
19
10
  }
20
11
  if (item.type === "task") {
21
- const url = taskArtifactPreviewUrl(featuredTaskArtifact(item.snapshot.artifacts));
12
+ const url = taskArtifactPreviewUrl(featuredTaskArtifact(item.props.snapshot.artifacts));
22
13
  return url ? `url:${url}` : null;
23
14
  }
24
15
  return null;
25
16
  }
26
- /** The source a key resolves from, recovered from its namespace prefix. */
27
17
  function boardImageKeySource(key) {
28
18
  if (key.startsWith("file:")) return {
29
19
  kind: "file",