@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
@@ -1,32 +1,12 @@
1
- import { BoardFileItem, BoardItem } from "../../../protocol/dist/board-document.js";
1
+ import { BoardFileItem } from "../../../protocol/dist/board-model.js";
2
+ import "../../../protocol/dist/index.js";
3
+ import { BoardSceneItem, SceneItem } from "../../core/scene.js";
2
4
  import { BoardCardRenderer } from "./board-renderer-registry.js";
3
5
  import { Text } from "pixi.js";
4
6
  //#region src/board/render/renderers/file-card-renderer.d.ts
5
- /**
6
- * Join already-wrapped lines and append an ellipsis to the last one.
7
- * Pure helper so the clamp can be unit-tested without a Pixi canvas.
8
- */
9
7
  declare function ellipsizeWrappedLines(lines: string[], maxLines: number): string;
10
- /**
11
- * Shorten `line` so `line + "…"` fits in `wrapWidth`.
12
- *
13
- * Binary search over the prefix: O(log n) single-line measures. Callers pass a
14
- * measure callback so this stays free of Pixi types and easy to unit-test.
15
- */
16
8
  declare function fitLineWithEllipsis(line: string, wrapWidth: number, measureWidth: (value: string) => number): string;
17
- /**
18
- * Fit a Pixi text node into a fixed line budget.
19
- *
20
- * Pixi Text has no built-in max-lines / ellipsis, so we measure with the same
21
- * style the node will render, keep the leading wrapped lines, and mark a cut
22
- * with an ellipsis. Returning early when everything fits avoids an extra
23
- * texture rebuild on the common path.
24
- *
25
- * Cost is one wrap of a length-capped sample, plus at most a binary search on
26
- * the last kept line. No character-by-character multi-line remeasure.
27
- */
28
9
  declare function fitTextToLines(text: Text, value: string, maxLines: number, wrapWidth: number): void;
29
- /** Scale an image into a fixed cover band without cropping or distortion. */
30
10
  declare function containCoverRect(width: number, height: number, imageWidth: number, imageHeight: number): {
31
11
  x: number;
32
12
  y: number;
@@ -34,7 +14,6 @@ declare function containCoverRect(width: number, height: number, imageWidth: num
34
14
  height: number;
35
15
  };
36
16
  declare const fileCardRenderer: BoardCardRenderer;
37
- /** Helper kept for type narrowing in tests. */
38
- declare function isFileItem(item: BoardItem): item is BoardFileItem;
17
+ declare function isFileItem(item: BoardSceneItem): item is SceneItem<BoardFileItem>;
39
18
  //#endregion
40
19
  export { containCoverRect, ellipsizeWrappedLines, fileCardRenderer, fitLineWithEllipsis, fitTextToLines, isFileItem };
@@ -6,26 +6,9 @@ import { positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate } from "./far-plate.js";
7
7
  import { CanvasTextMetrics, Container, Graphics, Sprite, Text, Texture } from "pixi.js";
8
8
  //#region src/board/render/renderers/file-card-renderer.ts
9
- /**
10
- * File card renderer.
11
- *
12
- * The card is an *entry point* to a workspace file, so it shows only what helps
13
- * you recognise and pick it: an optional cover, the name, a couple of lines of
14
- * excerpt. Everything else belongs to the file preview that opens
15
- * when the card is activated.
16
- *
17
- * Cost is managed through three levels of detail keyed on zoom, because a board
18
- * is expected to hold thousands of these. Pixi `Text` is a rasterised texture,
19
- * so below roughly one CSS pixel per glyph it can only ever resolve to a smudge;
20
- * the tiers below simply stop paying for what cannot be read anyway.
21
- *
22
- * Absolute floor: content never paints outside the node frame. A rounded clip
23
- * mask is the hard boundary; line-clamped title/excerpt keep the interior tidy.
24
- */
25
9
  const RADIUS = 4;
26
10
  const PADDING = 10;
27
11
  const COVER_RATIO = .56;
28
- /** Minimum body height reserved under a cover so the title stays readable. */
29
12
  const MIN_BODY_FOR_COVER = 38;
30
13
  const TITLE_SIZE = 13;
31
14
  const TITLE_LINE = TITLE_SIZE * 1.35;
@@ -36,29 +19,20 @@ const EXCERPT_MAX_LINES = 3;
36
19
  const TYPE_MARK_SIZE = 28;
37
20
  const GAP = 4;
38
21
  const STRIPE = 2;
39
- /** Zoom below which the title is dropped (glyphs are sub-pixel). */
40
22
  const LOD_TITLE_ZOOM = .35;
41
- /** Zoom below which the excerpt is dropped. */
42
23
  const LOD_BODY_ZOOM = .6;
43
- /** Upper bound on dashes in the unavailable-state edge, regardless of width. */
44
24
  const MAX_DASHES = 40;
45
25
  const partsByContainer = /* @__PURE__ */ new WeakMap();
46
- /** Detail tier for a zoom level. */
47
26
  function detailFor(zoom) {
48
27
  if (zoom < LOD_TITLE_ZOOM) return "plate";
49
28
  if (zoom < LOD_BODY_ZOOM) return "title";
50
29
  return "full";
51
30
  }
52
- /** Cover band height for a card, or 0 when it has no cover. */
53
31
  function coverHeight(item, height) {
54
- if (filePreviewKind(item.snapshot) !== "cover") return 0;
32
+ if (filePreviewKind(item.props.snapshot) !== "cover") return 0;
55
33
  const ideal = Math.round(height * COVER_RATIO);
56
34
  return Math.max(0, Math.min(ideal, height - MIN_BODY_FOR_COVER));
57
35
  }
58
- /**
59
- * Join already-wrapped lines and append an ellipsis to the last one.
60
- * Pure helper so the clamp can be unit-tested without a Pixi canvas.
61
- */
62
36
  function ellipsizeWrappedLines(lines, maxLines) {
63
37
  if (maxLines <= 0 || lines.length === 0) return "";
64
38
  if (lines.length <= maxLines) return lines.join("\n");
@@ -68,12 +42,6 @@ function ellipsizeWrappedLines(lines, maxLines) {
68
42
  kept[lastIndex] = last ? `${last}…` : "…";
69
43
  return kept.join("\n");
70
44
  }
71
- /**
72
- * Shorten `line` so `line + "…"` fits in `wrapWidth`.
73
- *
74
- * Binary search over the prefix: O(log n) single-line measures. Callers pass a
75
- * measure callback so this stays free of Pixi types and easy to unit-test.
76
- */
77
45
  function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
78
46
  const trimmed = line.replace(/\s+$/u, "");
79
47
  if (!trimmed) return "…";
@@ -92,17 +60,6 @@ function fitLineWithEllipsis(line, wrapWidth, measureWidth) {
92
60
  }
93
61
  return best;
94
62
  }
95
- /**
96
- * Fit a Pixi text node into a fixed line budget.
97
- *
98
- * Pixi Text has no built-in max-lines / ellipsis, so we measure with the same
99
- * style the node will render, keep the leading wrapped lines, and mark a cut
100
- * with an ellipsis. Returning early when everything fits avoids an extra
101
- * texture rebuild on the common path.
102
- *
103
- * Cost is one wrap of a length-capped sample, plus at most a binary search on
104
- * the last kept line. No character-by-character multi-line remeasure.
105
- */
106
63
  function fitTextToLines(text, value, maxLines, wrapWidth) {
107
64
  const width = Math.max(1, wrapWidth);
108
65
  text.style.wordWrapWidth = width;
@@ -126,12 +83,10 @@ function fitTextToLines(text, value, maxLines, wrapWidth) {
126
83
  const fitted = kept.join("\n");
127
84
  if (text.text !== fitted) text.text = fitted;
128
85
  }
129
- /** How many full lines of `lineHeight` fit in `room`, capped at `max`. */
130
86
  function linesInRoom(room, lineHeight, max) {
131
87
  if (room < lineHeight * .85) return 0;
132
88
  return Math.max(0, Math.min(max, Math.floor((room + .5) / lineHeight)));
133
89
  }
134
- /** Scale an image into a fixed cover band without cropping or distortion. */
135
90
  function containCoverRect(width, height, imageWidth, imageHeight) {
136
91
  if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return {
137
92
  x: 0,
@@ -169,10 +124,10 @@ function sync(container, item, context) {
169
124
  const key = context.assetKey(item);
170
125
  const texture = key ? context.getTexture(key) : null;
171
126
  const coverFailed = Boolean(key && !texture && context.hasError(key));
172
- const fileState = context.fileState(item.ref.path);
127
+ const fileState = context.fileState(item.props.src);
173
128
  const band = coverHeight(item, height);
174
- const kind = filePreviewKind(item.snapshot);
175
- const category = fileCategory(item.ref.path, item.snapshot?.mimeType);
129
+ const kind = filePreviewKind(item.props.snapshot);
130
+ const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
176
131
  const accent = fileCategoryAccent(category, context.palette);
177
132
  syncTextResolution(parts.title, parts.titleRes, context.zoom);
178
133
  syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
@@ -240,9 +195,9 @@ function sync(container, item, context) {
240
195
  parts.typeMark.visible = false;
241
196
  return;
242
197
  }
243
- const title = item.snapshot?.title || fileStem(item.ref.path);
244
- const excerpt = item.snapshot?.excerpt ?? "";
245
- const mark = fileTypeLabel(item.ref.path);
198
+ const title = item.props.snapshot?.title || fileStem(item.props.src);
199
+ const excerpt = item.props.snapshot?.excerpt ?? "";
200
+ const mark = fileTypeLabel(item.props.src);
246
201
  const innerWidth = Math.max(1, width - 20);
247
202
  const showTypeMark = kind === "blank";
248
203
  const textSig = [
@@ -369,12 +324,8 @@ const fileCardRenderer = {
369
324
  update: (container, item, context) => {
370
325
  if (item.type === "file") sync(container, item, context);
371
326
  },
372
- /**
373
- * Far LOD: a plate with a muted accent band. Sampling the real cover here
374
- * would mean one draw call per distinct image and defeat the batch.
375
- */
376
327
  renderFar: (graphics, item, context) => {
377
- const category = fileCategory(item.type === "file" ? item.ref.path : "", item.type === "file" ? item.snapshot?.mimeType : void 0);
328
+ const category = fileCategory(item.type === "file" ? item.props.src : "", item.type === "file" ? item.props.snapshot?.mimeType : void 0);
378
329
  drawFarPlate(graphics, item.frame, {
379
330
  fill: context.palette.surface,
380
331
  fillAlpha: .96,
@@ -387,7 +338,6 @@ const fileCardRenderer = {
387
338
  partsByContainer.delete(container);
388
339
  }
389
340
  };
390
- /** Helper kept for type narrowing in tests. */
391
341
  function isFileItem(item) {
392
342
  return item.type === "file";
393
343
  }
@@ -1,49 +1,58 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { pickBoardColor } from "../../core/palette.js";
3
2
  import { syncTextResolution } from "../text-resolution.js";
4
3
  import { createLabel, positionShell } from "./base-card-renderer.js";
5
4
  import { drawFarPlate } from "./far-plate.js";
5
+ import { itemColor } from "../palette.js";
6
6
  import { Container, Graphics } from "pixi.js";
7
7
  //#region src/board/render/renderers/frame-card-renderer.ts
8
8
  const partsByContainer = /* @__PURE__ */ new WeakMap();
9
+ const DEFAULT_WASH = .04;
9
10
  function sync(container, item, context) {
10
11
  const parts = partsByContainer.get(container);
11
- if (!parts || item.type !== "frame") return;
12
- const { frame, label, color, locked } = item;
12
+ if (!parts) return;
13
+ const { frame, style, props, locked } = item;
13
14
  positionShell(parts.root, item);
14
15
  syncTextResolution(parts.label, parts, context.zoom);
15
16
  const selected = context.selectedIds.has(item.id);
16
17
  const hovered = context.hoveredId === item.id;
17
- const palette = pickBoardColor(context.colors, color, context.colorScheme);
18
- const stroke = selected ? context.palette.brand : palette.stroke;
18
+ const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
19
+ const fillAlpha = style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH);
20
+ const baseStroke = itemColor(context, style.stroke, "neutral");
21
+ const stroke = selected ? context.palette.brand : baseStroke;
19
22
  const alpha = selected ? 1 : hovered ? .85 : .55;
23
+ const radius = style.radius ?? 4;
24
+ const strokeWidth = style.strokeWidth ?? 1.5;
20
25
  const visualSig = [
21
26
  frame.width,
22
27
  frame.height,
23
28
  selected,
24
29
  hovered,
25
- palette.fill,
26
- stroke
30
+ fill,
31
+ fillAlpha,
32
+ stroke,
33
+ radius,
34
+ strokeWidth
27
35
  ].join("|");
28
36
  if (visualSig !== parts.visualSig) {
29
37
  parts.visualSig = visualSig;
30
38
  parts.box.clear();
31
- parts.box.roundRect(0, 0, frame.width, frame.height, 4).fill({
32
- color: palette.fill,
33
- alpha: .04
39
+ parts.box.roundRect(0, 0, frame.width, frame.height, radius).fill({
40
+ color: fill,
41
+ alpha: fillAlpha
34
42
  });
35
- parts.box.roundRect(0, 0, frame.width, frame.height, 4).stroke({
43
+ if (strokeWidth > 0 || selected) parts.box.roundRect(0, 0, frame.width, frame.height, radius).stroke({
36
44
  color: stroke,
37
- width: selected ? 2 : 1.5,
45
+ width: selected ? Math.max(2, strokeWidth) : strokeWidth,
38
46
  alpha
39
47
  });
40
48
  }
41
- const title = locked ? `🔒 ${label || "Frame"}` : label || "Frame";
42
- const textSig = [title, palette.stroke].join("|");
49
+ const title = locked ? `🔒 ${props.label || "Frame"}` : props.label || "Frame";
50
+ const labelColor = style.stroke ? baseStroke : context.palette.muted;
51
+ const textSig = [title, labelColor].join("|");
43
52
  if (textSig !== parts.textSig) {
44
53
  parts.textSig = textSig;
45
54
  parts.label.text = title;
46
- parts.label.style.fill = palette.stroke;
55
+ parts.label.style.fill = labelColor;
47
56
  }
48
57
  parts.label.position.set(4, -18);
49
58
  }
@@ -68,26 +77,17 @@ const frameCardRenderer = {
68
77
  textSig: "",
69
78
  resolution: label.resolution
70
79
  });
71
- sync(root, item, context);
80
+ if (item.type === "frame") sync(root, item, context);
72
81
  return root;
73
82
  },
74
83
  update: (container, item, context) => {
75
- sync(container, item, context);
84
+ if (item.type === "frame") sync(container, item, context);
76
85
  },
77
- /**
78
- * Far LOD: a frame is a background region, so it draws as a faint wash with no
79
- * label.
80
- *
81
- * Batching it is what keeps it *behind* its contents. As a live container it
82
- * would be drawn above the whole far layer no matter where the document places
83
- * it, so a frame would hide every card inside it.
84
- */
85
86
  renderFar: (graphics, item, context) => {
86
87
  if (item.type !== "frame") return;
87
- const palette = pickBoardColor(context.colors, item.color, context.colorScheme);
88
88
  drawFarPlate(graphics, item.frame, {
89
- fill: palette.fill,
90
- fillAlpha: .07
89
+ fill: itemColor(context, item.style.fill ?? item.style.stroke, "neutral", "fill"),
90
+ fillAlpha: Math.max(.07, item.style.fillOpacity ?? (item.style.fill ? 1 : 0))
91
91
  });
92
92
  },
93
93
  destroy: (container) => {
@@ -1,8 +1,9 @@
1
- import { BoardImageItem, BoardItem } from "../../../protocol/dist/board-document.js";
1
+ import { BoardImageItem } from "../../../protocol/dist/board-model.js";
2
+ import "../../../protocol/dist/index.js";
3
+ import { BoardSceneItem, SceneItem } from "../../core/scene.js";
2
4
  import { BoardCardRenderer } from "./board-renderer-registry.js";
3
5
  //#region src/board/render/renderers/image-card-renderer.d.ts
4
6
  declare const imageCardRenderer: BoardCardRenderer;
5
- /** Helper kept for type narrowing in tests. */
6
- declare function isImageItem(item: BoardItem): item is BoardImageItem;
7
+ declare function isImageItem(item: BoardSceneItem): item is SceneItem<BoardImageItem>;
7
8
  //#endregion
8
9
  export { imageCardRenderer, isImageItem };
@@ -4,14 +4,6 @@ import { Container, Graphics, Sprite, Texture } from "pixi.js";
4
4
  //#region src/board/render/renderers/image-card-renderer.ts
5
5
  const partsByContainer = /* @__PURE__ */ new WeakMap();
6
6
  const RADIUS = 2;
7
- /**
8
- * Lay the sprite out inside the frame.
9
- *
10
- * The frame normally already matches the image's pixel aspect (see the editor's
11
- * media size adoption), so this fills it exactly. Until that correction lands
12
- * for a freshly dropped file, `contain` keeps the whole image visible rather
13
- * than cropping it — the brief letterbox is self-correcting.
14
- */
15
7
  function layoutContain(sprite, width, height) {
16
8
  const texture = sprite.texture;
17
9
  const tw = texture.width;
@@ -130,7 +122,6 @@ const imageCardRenderer = {
130
122
  partsByContainer.delete(container);
131
123
  }
132
124
  };
133
- /** Helper kept for type narrowing in tests. */
134
125
  function isImageItem(item) {
135
126
  return item.type === "image";
136
127
  }
@@ -0,0 +1,9 @@
1
+ import { BoardShapeItem } from "../../../protocol/dist/board-model.js";
2
+ import "../../../protocol/dist/index.js";
3
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
4
+ import { Graphics } from "pixi.js";
5
+ //#region src/board/render/renderers/shape-card-renderer.d.ts
6
+ declare function traceShapeOutline(graphics: Graphics, props: BoardShapeItem["props"], width: number, height: number, radius?: number): void;
7
+ declare const shapeCardRenderer: BoardCardRenderer;
8
+ //#endregion
9
+ export { shapeCardRenderer, traceShapeOutline };
@@ -0,0 +1,240 @@
1
+ import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
+ import { worldPoint } from "../../geometry.js";
3
+ import { syncTextResolution, syncTextWrapWidth, textResolutionForZoom } from "../text-resolution.js";
4
+ import { positionShell } from "./base-card-renderer.js";
5
+ import { drawFarPlate } from "./far-plate.js";
6
+ import { itemColor } from "../palette.js";
7
+ import { dashPattern, tracePolyline, trimPolyline } from "../stroke.js";
8
+ import { Container, Graphics, GraphicsPath, Matrix, Text } from "pixi.js";
9
+ //#region src/board/render/renderers/shape-card-renderer.ts
10
+ const DEFAULT_ROUNDED_RADIUS = 8;
11
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
12
+ const pathCache = /* @__PURE__ */ new Map();
13
+ function parsedPath(d) {
14
+ if (pathCache.has(d)) return pathCache.get(d) ?? null;
15
+ let entry = null;
16
+ try {
17
+ const path = new GraphicsPath(d);
18
+ const probe = new Graphics().path(path).fill(0);
19
+ const bounds = probe.getLocalBounds();
20
+ probe.destroy();
21
+ entry = {
22
+ path,
23
+ bounds: {
24
+ x: bounds.x,
25
+ y: bounds.y,
26
+ width: Math.max(1e-6, bounds.width),
27
+ height: Math.max(1e-6, bounds.height)
28
+ }
29
+ };
30
+ } catch {
31
+ entry = null;
32
+ }
33
+ if (pathCache.size > 256) pathCache.clear();
34
+ pathCache.set(d, entry);
35
+ return entry;
36
+ }
37
+ function traceShapeOutline(graphics, props, width, height, radius) {
38
+ switch (props.geometry) {
39
+ case "ellipse":
40
+ graphics.ellipse(width / 2, height / 2, width / 2, height / 2);
41
+ return;
42
+ case "diamond":
43
+ graphics.moveTo(width / 2, 0).lineTo(width, height / 2).lineTo(width / 2, height).lineTo(0, height / 2).closePath();
44
+ return;
45
+ case "triangle":
46
+ graphics.moveTo(width / 2, 0).lineTo(width, height).lineTo(0, height).closePath();
47
+ return;
48
+ case "rounded":
49
+ graphics.roundRect(0, 0, width, height, radius ?? DEFAULT_ROUNDED_RADIUS);
50
+ return;
51
+ case "path": {
52
+ const parsed = props.path ? parsedPath(props.path) : null;
53
+ if (!parsed) {
54
+ graphics.rect(0, 0, width, height);
55
+ return;
56
+ }
57
+ const sx = width / parsed.bounds.width;
58
+ const sy = height / parsed.bounds.height;
59
+ graphics.path(parsed.path.clone(true).transform(new Matrix(sx, 0, 0, sy, -parsed.bounds.x * sx, -parsed.bounds.y * sy)));
60
+ return;
61
+ }
62
+ default: if (radius) graphics.roundRect(0, 0, width, height, radius);
63
+ else graphics.rect(0, 0, width, height);
64
+ }
65
+ }
66
+ function outlinePoints(geometry, width, height, radius) {
67
+ const p = (x, y) => worldPoint(x, y);
68
+ switch (geometry) {
69
+ case "ellipse": {
70
+ const segments = 64;
71
+ return Array.from({ length: 65 }, (_, index) => {
72
+ const angle = -Math.PI / 2 + index / segments * Math.PI * 2;
73
+ return p(width / 2 + Math.cos(angle) * width / 2, height / 2 + Math.sin(angle) * height / 2);
74
+ });
75
+ }
76
+ case "diamond": return [
77
+ p(width / 2, 0),
78
+ p(width, height / 2),
79
+ p(width / 2, height),
80
+ p(0, height / 2),
81
+ p(width / 2, 0)
82
+ ];
83
+ case "triangle": return [
84
+ p(width / 2, 0),
85
+ p(width, height),
86
+ p(0, height),
87
+ p(width / 2, 0)
88
+ ];
89
+ default: {
90
+ const r = Math.min(radius, width / 2, height / 2);
91
+ if (r <= 0) return [
92
+ p(0, 0),
93
+ p(width, 0),
94
+ p(width, height),
95
+ p(0, height),
96
+ p(0, 0)
97
+ ];
98
+ const corner = (cx, cy, from) => Array.from({ length: 9 }, (_, index) => {
99
+ const angle = from + index / 8 * (Math.PI / 2);
100
+ return p(cx + Math.cos(angle) * r, cy + Math.sin(angle) * r);
101
+ });
102
+ return [
103
+ ...corner(width - r, r, -Math.PI / 2),
104
+ ...corner(width - r, height - r, 0),
105
+ ...corner(r, height - r, Math.PI / 2),
106
+ ...corner(r, r, Math.PI),
107
+ p(width - r, 0)
108
+ ];
109
+ }
110
+ }
111
+ }
112
+ function sync(container, item, context) {
113
+ const parts = partsByContainer.get(container);
114
+ if (!parts) return;
115
+ positionShell(parts.root, item);
116
+ const { width, height } = item.frame;
117
+ const { props, style } = item;
118
+ const selected = context.selectedIds.has(item.id);
119
+ const hovered = context.hoveredId === item.id;
120
+ const stroke = itemColor(context, style.stroke ?? style.fill, "brand");
121
+ const fill = itemColor(context, style.fill ?? style.stroke, "brand", "fill");
122
+ const fillOpacity = style.fillOpacity ?? (style.fill ? 1 : 0);
123
+ const strokeWidth = style.strokeWidth ?? 1.75;
124
+ syncTextResolution(parts.label, parts, context.zoom);
125
+ const visualSig = [
126
+ width,
127
+ height,
128
+ selected,
129
+ hovered,
130
+ props.geometry,
131
+ props.path,
132
+ fill,
133
+ fillOpacity,
134
+ stroke,
135
+ strokeWidth,
136
+ style.dash,
137
+ style.radius,
138
+ style.trim,
139
+ context.palette.muted
140
+ ].join("|");
141
+ if (visualSig !== parts.visualSig) {
142
+ parts.visualSig = visualSig;
143
+ parts.shape.clear();
144
+ if (fillOpacity > 0) {
145
+ traceShapeOutline(parts.shape, props, width, height, style.radius);
146
+ parts.shape.fill({
147
+ color: fill,
148
+ alpha: fillOpacity
149
+ });
150
+ }
151
+ const lineWidth = selected ? Math.max(2.5, strokeWidth) : strokeWidth;
152
+ const pattern = dashPattern(style.dash, lineWidth);
153
+ const partial = style.trim !== void 0 && style.trim < 1;
154
+ if (strokeWidth > 0 && (!partial || style.trim)) {
155
+ if ((pattern || partial) && props.geometry !== "path") {
156
+ const radius = style.radius ?? (props.geometry === "rounded" ? DEFAULT_ROUNDED_RADIUS : 0);
157
+ tracePolyline(parts.shape, trimPolyline(outlinePoints(props.geometry, width, height, radius), style.trim), pattern);
158
+ } else traceShapeOutline(parts.shape, props, width, height, style.radius);
159
+ parts.shape.stroke({
160
+ color: hovered && !selected ? context.palette.muted : stroke,
161
+ width: lineWidth,
162
+ alpha: selected ? 1 : hovered ? .95 : .85,
163
+ cap: partial || pattern ? "round" : "butt",
164
+ join: "round"
165
+ });
166
+ }
167
+ }
168
+ const label = itemColor(context, style.stroke, "neutral", "label");
169
+ const textSig = [
170
+ props.text,
171
+ label,
172
+ props.fontSize,
173
+ props.align
174
+ ].join("|");
175
+ if (textSig !== parts.textSig) {
176
+ parts.textSig = textSig;
177
+ parts.label.text = props.text;
178
+ parts.label.visible = props.text.length > 0;
179
+ parts.label.style.fill = label;
180
+ parts.label.style.fontSize = props.fontSize;
181
+ parts.label.style.lineHeight = props.fontSize * 1.4;
182
+ parts.label.style.align = props.align;
183
+ }
184
+ syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), context.resizingIds.has(item.id));
185
+ parts.label.position.set(width / 2, height / 2);
186
+ }
187
+ const shapeCardRenderer = {
188
+ id: "shape-card",
189
+ canRender: (item) => item.type === "shape",
190
+ create: (item, context) => {
191
+ const root = new Container();
192
+ const shape = new Graphics();
193
+ const resolution = textResolutionForZoom(context.zoom);
194
+ const label = new Text({
195
+ text: "",
196
+ style: {
197
+ fill: 16777215,
198
+ fontFamily: BOARD_FONT_STACK,
199
+ fontSize: 20,
200
+ fontWeight: "500",
201
+ align: "center",
202
+ wordWrap: true,
203
+ lineHeight: 28
204
+ },
205
+ resolution,
206
+ roundPixels: true
207
+ });
208
+ label.anchor.set(.5);
209
+ root.addChild(shape, label);
210
+ partsByContainer.set(root, {
211
+ root,
212
+ shape,
213
+ label,
214
+ visualSig: "",
215
+ textSig: "",
216
+ wrapWidth: 0,
217
+ resolution
218
+ });
219
+ if (item.type === "shape") sync(root, item, context);
220
+ return root;
221
+ },
222
+ update: (container, item, context) => {
223
+ if (item.type === "shape") sync(container, item, context);
224
+ },
225
+ renderFar: (graphics, item, context) => {
226
+ if (item.type !== "shape") return;
227
+ drawFarPlate(graphics, item.frame, {
228
+ fill: itemColor(context, item.style.fill ?? item.style.stroke, "brand", "fill"),
229
+ fillAlpha: Math.max(.18, item.style.fillOpacity ?? 0),
230
+ accent: itemColor(context, item.style.stroke, "brand"),
231
+ accentAlpha: .9
232
+ });
233
+ },
234
+ destroy: (container) => {
235
+ partsByContainer.get(container)?.root.destroy({ children: true });
236
+ partsByContainer.delete(container);
237
+ }
238
+ };
239
+ //#endregion
240
+ export { shapeCardRenderer, traceShapeOutline };
@@ -0,0 +1,5 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/sketch-card-renderer.d.ts
3
+ declare const sketchCardRenderer: BoardCardRenderer;
4
+ //#endregion
5
+ export { sketchCardRenderer };