@neta-art/cohub 11.5.1 → 12.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 (73) hide show
  1. package/dist/board/editor/animation.js +2 -1
  2. package/dist/board/editor/creation.js +1 -1
  3. package/dist/board/editor/document-edits.d.ts +1 -1
  4. package/dist/board/editor/document-edits.js +1 -19
  5. package/dist/board/editor/document.js +2 -1
  6. package/dist/board/editor/index.d.ts +2 -1
  7. package/dist/board/editor/index.js +2 -1
  8. package/dist/board/editor/snapshots.js +11 -13
  9. package/dist/board/export/index.js +1 -1
  10. package/dist/board/export/scene.js +1 -1
  11. package/dist/board/model/client-geometry.d.ts +22 -0
  12. package/dist/board/model/client-geometry.js +94 -0
  13. package/dist/board/model/document-share.d.ts +5 -0
  14. package/dist/board/model/document-share.js +27 -0
  15. package/dist/board/model/index.d.ts +5 -4
  16. package/dist/board/model/index.js +4 -3
  17. package/dist/board/model/items/create.d.ts +5 -1
  18. package/dist/board/model/items/create.js +29 -58
  19. package/dist/board/model/items/index.d.ts +2 -2
  20. package/dist/board/model/items/index.js +2 -2
  21. package/dist/board/model/items/registry.js +2 -5
  22. package/dist/board/model/json.d.ts +4 -0
  23. package/dist/board/model/json.js +22 -0
  24. package/dist/board/model/media-playback.d.ts +6 -3
  25. package/dist/board/player/index.d.ts +6 -2
  26. package/dist/board/player/index.js +33 -15
  27. package/dist/board/render/index.d.ts +2 -2
  28. package/dist/board/render/index.js +6 -6
  29. package/dist/board/render/renderers/arrow-card-renderer.js +5 -1
  30. package/dist/board/render/renderers/audio-card-renderer.js +4 -1
  31. package/dist/board/render/renderers/board-renderer-registry.d.ts +4 -2
  32. package/dist/board/render/renderers/custom-card-renderer.js +5 -2
  33. package/dist/board/render/renderers/file-card-renderer.js +4 -1
  34. package/dist/board/render/renderers/frame-card-renderer.js +5 -1
  35. package/dist/board/render/renderers/image-card-renderer.js +13 -49
  36. package/dist/board/render/renderers/model-card-renderer.js +4 -1
  37. package/dist/board/render/renderers/shape-card-renderer.js +5 -1
  38. package/dist/board/render/renderers/task-card-renderer.js +6 -3
  39. package/dist/board/render/renderers/text-card-renderer.js +6 -2
  40. package/dist/board/render/renderers/unknown-card-renderer.js +4 -0
  41. package/dist/board/render/renderers/video-card-renderer.js +19 -18
  42. package/dist/board/render/script-host.d.ts +2 -0
  43. package/dist/board/render/script-host.js +44 -9
  44. package/dist/board/render/script-resolution.d.ts +4 -0
  45. package/dist/board/render/script-resolution.js +18 -0
  46. package/dist/board/render/surface.d.ts +9 -2
  47. package/dist/board/render/surface.js +15 -1
  48. package/dist/board/replica/index.js +4 -1
  49. package/dist/board/stage/assets.d.ts +12 -8
  50. package/dist/board/stage/assets.js +173 -88
  51. package/dist/board/stage/index.d.ts +3 -3
  52. package/dist/board/stage/index.js +3 -3
  53. package/dist/board/stage/lru.d.ts +3 -2
  54. package/dist/board/stage/lru.js +8 -4
  55. package/dist/board/stage/pointer.d.ts +1 -1
  56. package/dist/board/stage/pointer.js +4 -5
  57. package/dist/board/stage/scene.d.ts +3 -0
  58. package/dist/board/stage/scene.js +44 -8
  59. package/dist/board/stage/stage.d.ts +1 -0
  60. package/dist/board/stage/stage.js +123 -56
  61. package/dist/board/stage/views.d.ts +7 -2
  62. package/dist/board/stage/views.js +50 -21
  63. package/dist/chunks/environment.d.ts +16 -2
  64. package/dist/index.js +65 -22
  65. package/dist/protocol/dist/board-constants.js +56 -1
  66. package/dist/protocol/dist/board-layout.d.ts +8 -1
  67. package/dist/protocol/dist/board-layout.js +16 -4
  68. package/dist/protocol/dist/board-model.js +13 -19
  69. package/dist/protocol/dist/board-schema.d.ts +52 -0
  70. package/dist/protocol/dist/board-schema.js +8 -0
  71. package/dist/protocol/dist/core/media.js +1 -0
  72. package/dist/protocol/dist/index.d.ts +2 -2
  73. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
+ import { BOARD_DEFAULT_SIZES } from "../../../protocol/dist/board-constants.js";
1
2
  import { BoardMediaOriginSchema, parseBoardItem } from "../../../protocol/dist/board-model.js";
2
- import { pointsRect } from "../../../protocol/dist/board-layout.js";
3
+ import { fitBoardMediaSize, pointsRect } from "../../../protocol/dist/board-layout.js";
3
4
  import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
5
+ import "../geometry.js";
4
6
  import { quantizeDrawCoordinate } from "../draw-input.js";
5
7
  import { boardModelFormat, getMediaResourceTitle, inferBoardMediaKind } from "../media.js";
6
8
  import { fileStem } from "../file-snapshot.js";
@@ -8,30 +10,6 @@ import { filePreviewKind } from "../file-preview.js";
8
10
  import { DEFAULT_BOARD_TOOL_STYLES } from "../tool-styles.js";
9
11
  import { createBoardItemId } from "./id.js";
10
12
  //#region src/board/model/items/create.ts
11
- const DEFAULT_MEDIA_SIZE = {
12
- width: 320,
13
- height: 200
14
- };
15
- const DEFAULT_TASK_SIZE = {
16
- width: 300,
17
- height: 188
18
- };
19
- const DEFAULT_TASK_MEDIA_SIZE = {
20
- width: 320,
21
- height: 180
22
- };
23
- const DEFAULT_VIDEO_SIZE = {
24
- width: 320,
25
- height: 180
26
- };
27
- const DEFAULT_AUDIO_SIZE = {
28
- width: 320,
29
- height: 112
30
- };
31
- const DEFAULT_FILE_SIZE = {
32
- width: 260,
33
- height: 132
34
- };
35
13
  const DEFAULT_MODEL_SIZE = {
36
14
  width: 264,
37
15
  height: 264
@@ -40,14 +18,6 @@ const DEFAULT_FILE_COVER_SIZE = {
40
18
  width: 260,
41
19
  height: 208
42
20
  };
43
- const DEFAULT_SHAPE_SIZE = {
44
- width: 200,
45
- height: 140
46
- };
47
- const DEFAULT_FRAME_SIZE = {
48
- width: 480,
49
- height: 320
50
- };
51
21
  function item(value) {
52
22
  const parsed = parseBoardItem(value);
53
23
  if (!parsed.ok) throw new Error(`Invalid Board item: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
@@ -62,12 +32,16 @@ function centered(x, y, size) {
62
32
  size: { ...size }
63
33
  };
64
34
  }
65
- function mediaFrameSize(naturalWidth, naturalHeight, maxEdge = 480, fallback = DEFAULT_MEDIA_SIZE) {
66
- if (!naturalWidth || !naturalHeight || !Number.isFinite(naturalWidth) || !Number.isFinite(naturalHeight) || naturalWidth <= 0 || naturalHeight <= 0) return { ...fallback };
67
- const scale = Math.min(1, maxEdge / Math.max(naturalWidth, naturalHeight));
35
+ function mediaFrameSize(naturalWidth, naturalHeight, maxEdge = 480, fallback = BOARD_DEFAULT_SIZES.image) {
36
+ const natural = {
37
+ width: naturalWidth ?? 0,
38
+ height: naturalHeight ?? 0
39
+ };
40
+ const size = fitBoardMediaSize(natural, Math.min(maxEdge, Math.max(natural.width, natural.height)));
41
+ if (!size) return { ...fallback };
68
42
  return {
69
- width: Math.max(24, naturalWidth * scale),
70
- height: Math.max(24, naturalHeight * scale)
43
+ width: Math.max(24, size.width),
44
+ height: Math.max(24, size.height)
71
45
  };
72
46
  }
73
47
  function cleanSnapshot(snapshot) {
@@ -77,7 +51,7 @@ function createMediaBoardItem(path, x, y, snapshot = {}, id = createBoardItemId(
77
51
  const kind = mediaKind ?? inferBoardMediaKind(path, snapshot.mimeType);
78
52
  const title = snapshot.title ?? (kind === "file" || kind === "text" ? fileStem(path) : getMediaResourceTitle(path));
79
53
  if (kind === "image" || kind === "video") {
80
- const size = mediaFrameSize(snapshot.naturalWidth, snapshot.naturalHeight, 480, kind === "video" ? DEFAULT_VIDEO_SIZE : DEFAULT_MEDIA_SIZE);
54
+ const size = mediaFrameSize(snapshot.naturalWidth, snapshot.naturalHeight, 480, BOARD_DEFAULT_SIZES[kind]);
81
55
  const mimeType = snapshot.mimeType ?? (kind === "video" ? "video/*" : void 0);
82
56
  return {
83
57
  id,
@@ -99,7 +73,7 @@ function createMediaBoardItem(path, x, y, snapshot = {}, id = createBoardItemId(
99
73
  id,
100
74
  item: item({
101
75
  type: "audio",
102
- ...centered(x, y, DEFAULT_AUDIO_SIZE),
76
+ ...centered(x, y, BOARD_DEFAULT_SIZES.audio),
103
77
  props: {
104
78
  src: path,
105
79
  snapshot: cleanSnapshot({
@@ -114,7 +88,7 @@ function createMediaBoardItem(path, x, y, snapshot = {}, id = createBoardItemId(
114
88
  id,
115
89
  item: item({
116
90
  type: "file",
117
- ...centered(x, y, boardModelFormat(path, snapshot.mimeType) ? DEFAULT_MODEL_SIZE : filePreviewKind(snapshot) === "cover" ? DEFAULT_FILE_COVER_SIZE : DEFAULT_FILE_SIZE),
91
+ ...centered(x, y, boardModelFormat(path, snapshot.mimeType) ? DEFAULT_MODEL_SIZE : filePreviewKind(snapshot) === "cover" ? DEFAULT_FILE_COVER_SIZE : BOARD_DEFAULT_SIZES.file),
118
92
  props: {
119
93
  src: path,
120
94
  snapshot: cleanSnapshot({
@@ -126,14 +100,20 @@ function createMediaBoardItem(path, x, y, snapshot = {}, id = createBoardItemId(
126
100
  };
127
101
  }
128
102
  const createFileNodeForPath = createMediaBoardItem;
129
- function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoardItemId(), aspect) {
103
+ function boardTaskCardSize(snapshot, aspect) {
130
104
  const artifact = featuredTaskArtifact(snapshot.artifacts);
131
- const visual = artifact?.type === "image" || artifact?.type === "video" ? artifact : null;
105
+ const expected = fitBoardMediaSize({
106
+ width: aspect ?? 0,
107
+ height: 1
108
+ }, 480) ?? { ...BOARD_DEFAULT_SIZES.task };
109
+ return artifact?.type === "image" || artifact?.type === "video" ? mediaFrameSize(artifact.naturalWidth, artifact.naturalHeight, 480, expected) : expected;
110
+ }
111
+ function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoardItemId(), aspect) {
132
112
  return {
133
113
  id,
134
114
  item: item({
135
115
  type: "task",
136
- ...centered(x, y, visual ? mediaFrameSize(visual.naturalWidth, visual.naturalHeight, 480, DEFAULT_TASK_MEDIA_SIZE) : artifact?.type === "audio" ? DEFAULT_TASK_MEDIA_SIZE : expectedTaskSize(aspect)),
116
+ ...centered(x, y, boardTaskCardSize(snapshot, aspect)),
137
117
  ...metadata ? { metadata } : {},
138
118
  props: {
139
119
  taskRunId,
@@ -142,15 +122,6 @@ function createTaskBoardItem(taskRunId, snapshot, x, y, metadata, id = createBoa
142
122
  })
143
123
  };
144
124
  }
145
- function expectedTaskSize(aspect) {
146
- if (!aspect || !Number.isFinite(aspect) || aspect <= 0) return { ...DEFAULT_TASK_SIZE };
147
- const width = DEFAULT_TASK_MEDIA_SIZE.width;
148
- const height = Math.round(width / aspect);
149
- return height > 0 && height <= width * 4 ? {
150
- width,
151
- height
152
- } : { ...DEFAULT_TASK_SIZE };
153
- }
154
125
  function createTextBoardItem(text, x, y, color = DEFAULT_BOARD_TOOL_STYLES.text.color, id = createBoardItemId()) {
155
126
  return {
156
127
  id,
@@ -179,7 +150,7 @@ function createShapeBoardItem(geometry, x, y, color = DEFAULT_BOARD_TOOL_STYLES.
179
150
  width: box.width,
180
151
  height: box.height
181
152
  }
182
- } : centered(x, y, DEFAULT_SHAPE_SIZE),
153
+ } : centered(x, y, BOARD_DEFAULT_SIZES.shape),
183
154
  style: { stroke: color },
184
155
  props: { geometry }
185
156
  })
@@ -199,7 +170,7 @@ function createFrameBoardItem(x, y, color = DEFAULT_BOARD_TOOL_STYLES.frame.colo
199
170
  width: box.width,
200
171
  height: box.height
201
172
  }
202
- } : centered(x, y, DEFAULT_FRAME_SIZE),
173
+ } : centered(x, y, BOARD_DEFAULT_SIZES.frame),
203
174
  ...color === "neutral" ? {} : { style: { stroke: color } },
204
175
  props: { label }
205
176
  })
@@ -362,7 +333,7 @@ function taskResultItem(taskRunId, artifact, x, y, rowHeight) {
362
333
  x,
363
334
  y
364
335
  },
365
- size: DEFAULT_AUDIO_SIZE,
336
+ size: { ...BOARD_DEFAULT_SIZES.audio },
366
337
  props: {
367
338
  src: artifact.url,
368
339
  snapshot,
@@ -370,7 +341,7 @@ function taskResultItem(taskRunId, artifact, x, y, rowHeight) {
370
341
  }
371
342
  })
372
343
  };
373
- const fallback = artifact.type === "video" ? DEFAULT_VIDEO_SIZE : DEFAULT_MEDIA_SIZE;
344
+ const fallback = BOARD_DEFAULT_SIZES[artifact.type];
374
345
  const ratio = artifact.naturalWidth && artifact.naturalHeight ? artifact.naturalWidth / artifact.naturalHeight : fallback.width / fallback.height;
375
346
  return {
376
347
  id,
@@ -393,4 +364,4 @@ function taskResultItem(taskRunId, artifact, x, y, rowHeight) {
393
364
  };
394
365
  }
395
366
  //#endregion
396
- export { boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem };
367
+ export { boardItemOrigin, boardOriginKey, boardTaskCardSize, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,6 +1,6 @@
1
1
  import { BoardItemCapabilities, BoardItemDefinition, BoardItemOf, BoardItemResizeMode, FULL_CAPABILITIES, defineBoardItem } from "./definition.js";
2
2
  import { builtinBoardItems } from "./builtin.js";
3
- import { BoardAppMetadata, BoardItemEntry, BoardMediaItemKind, boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
3
+ import { BoardAppMetadata, BoardItemEntry, BoardMediaItemKind, boardItemOrigin, boardOriginKey, boardTaskCardSize, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
4
4
  import { createBoardEntityId, createBoardItemId } from "./id.js";
5
5
  import { BoardItemInput, BoardRegistry, createBoardRegistry, defaultBoardRegistry } from "./registry.js";
6
- export { BoardAppMetadata, BoardItemCapabilities, BoardItemDefinition, BoardItemEntry, BoardItemInput, BoardItemOf, BoardItemResizeMode, BoardMediaItemKind, BoardRegistry, FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
6
+ export { BoardAppMetadata, BoardItemCapabilities, BoardItemDefinition, BoardItemEntry, BoardItemInput, BoardItemOf, BoardItemResizeMode, BoardMediaItemKind, BoardRegistry, FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, boardTaskCardSize, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,6 +1,6 @@
1
1
  import { FULL_CAPABILITIES, defineBoardItem } from "./definition.js";
2
2
  import { builtinBoardItems } from "./builtin.js";
3
3
  import { createBoardEntityId, createBoardItemId } from "./id.js";
4
- import { boardItemOrigin, boardOriginKey, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
4
+ import { boardItemOrigin, boardOriginKey, boardTaskCardSize, createAppBoardItem, createArrowBoardItem, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, mediaFrameSize, titleForBoardItem } from "./create.js";
5
5
  import { createBoardRegistry, defaultBoardRegistry } from "./registry.js";
6
- export { FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
6
+ export { FULL_CAPABILITIES, boardItemOrigin, boardOriginKey, boardTaskCardSize, builtinBoardItems, createAppBoardItem, createArrowBoardItem, createBoardEntityId, createBoardItemId, createBoardRegistry, createDrawBoardItem, createFileNodeForPath, createFrameBoardItem, createMediaBoardItem, createShapeBoardItem, createTaskBoardItem, createTaskResultItems, createTextBoardItem, defaultBoardRegistry, defineBoardItem, mediaFrameSize, titleForBoardItem };
@@ -1,13 +1,10 @@
1
+ import { boardDefaultSize } from "../../../protocol/dist/board-constants.js";
1
2
  import { hasAuthoredSize, parseBoardItem } from "../../../protocol/dist/board-model.js";
2
3
  import { frameContainsPoint, itemBounds } from "../geometry.js";
3
4
  import { FULL_CAPABILITIES } from "./definition.js";
4
5
  import { builtinBoardItems } from "./builtin.js";
5
6
  import { createBoardItemId } from "./id.js";
6
7
  //#region src/board/model/items/registry.ts
7
- const DEFAULT_ITEM_SIZE = {
8
- width: 240,
9
- height: 160
10
- };
11
8
  function createBoardRegistry(definitions = []) {
12
9
  const byType = /* @__PURE__ */ new Map();
13
10
  for (const definition of [...builtinBoardItems, ...definitions]) byType.set(definition.type, definition);
@@ -52,7 +49,7 @@ function createBoardRegistry(definitions = []) {
52
49
  create(type, { props = {}, at, size, id = createBoardItemId() }) {
53
50
  const valid = validateProps(type, props);
54
51
  if (!valid.ok) throw new Error(valid.message);
55
- const box = size ?? byType.get(type)?.size ?? DEFAULT_ITEM_SIZE;
52
+ const box = size ?? byType.get(type)?.size ?? boardDefaultSize(type);
56
53
  const placement = hasAuthoredSize(type) ? {
57
54
  position: {
58
55
  x: at.x - box.width / 2,
@@ -0,0 +1,4 @@
1
+ //#region src/board/model/json.d.ts
2
+ declare function sameJson(a: unknown, b: unknown): boolean;
3
+ //#endregion
4
+ export { sameJson };
@@ -0,0 +1,22 @@
1
+ //#region src/board/model/json.ts
2
+ function sameJson(a, b) {
3
+ if (a === b) return true;
4
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
5
+ if (Array.isArray(a) || Array.isArray(b)) {
6
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
7
+ for (let index = 0; index < a.length; index += 1) if (!sameJson(a[index], b[index])) return false;
8
+ return true;
9
+ }
10
+ const left = a;
11
+ const right = b;
12
+ let members = 0;
13
+ for (const key of Object.keys(left)) {
14
+ if (left[key] === void 0) continue;
15
+ members += 1;
16
+ if (!sameJson(left[key], right[key])) return false;
17
+ }
18
+ for (const key of Object.keys(right)) if (right[key] !== void 0) members -= 1;
19
+ return members === 0;
20
+ }
21
+ //#endregion
22
+ export { sameJson };
@@ -1,8 +1,11 @@
1
1
  import { BoardSceneItem } from "./scene.js";
2
2
  //#region src/board/model/media-playback.d.ts
3
+ type BoardAssetUrlOptions = {
4
+ fresh?: boolean;
5
+ };
3
6
  type BoardAssetSource = {
4
- resolveFileUrl: (path: string) => Promise<string | null>;
5
- resolvePlaybackUrl?: (path: string) => Promise<string | null>;
7
+ resolveFileUrl: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
8
+ resolvePlaybackUrl?: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
6
9
  };
7
10
  declare function resolveBoardMediaUrl(src: string, source: BoardAssetSource, purpose?: "preview" | "playback"): Promise<string | null>;
8
11
  type BoardPlayableMedia = {
@@ -17,4 +20,4 @@ declare function playableBoardMediaList(item: BoardSceneItem | null, assetSource
17
20
  declare function playableBoardMedia(item: BoardSceneItem | null, assetSource: BoardAssetSource, artifactId?: string | null): BoardPlayableMedia | null;
18
21
  declare function resetBoardPlaybackUrlCache(): void;
19
22
  //#endregion
20
- export { BoardAssetSource, BoardPlayableMedia, playableBoardMedia, playableBoardMediaList, resetBoardPlaybackUrlCache, resolveBoardMediaUrl };
23
+ export { BoardAssetSource, BoardAssetUrlOptions, BoardPlayableMedia, playableBoardMedia, playableBoardMediaList, resetBoardPlaybackUrlCache, resolveBoardMediaUrl };
@@ -10,7 +10,11 @@ type BoardPlayerFrame = {
10
10
  playbackTime: number | null;
11
11
  running: boolean;
12
12
  };
13
- declare function playbackTimeAt(playback: BoardPlaybackSnapshot, document: BoardDocument, now: number): number | null;
13
+ type BoardPlaybackClock = {
14
+ time: number | null;
15
+ advancing: boolean;
16
+ };
17
+ declare function playbackClockAt(playback: BoardPlaybackSnapshot, document: BoardDocument, now: number): BoardPlaybackClock;
14
18
  declare function createBoardPlayer(options?: {
15
19
  resolveColor?: BoardColorResolver;
16
20
  }): {
@@ -21,4 +25,4 @@ declare function createBoardPlayer(options?: {
21
25
  type BoardPlayer = ReturnType<typeof createBoardPlayer>;
22
26
  declare function nextLocalPlayback(current: BoardPlaybackSnapshot | null, command: BoardPlaybackCommand, document: BoardDocument, now?: number): BoardPlaybackSnapshot | null;
23
27
  //#endregion
24
- export { BoardPlayer, BoardPlayerFrame, createBoardPlayer, nextLocalPlayback, playbackTimeAt };
28
+ export { BoardPlaybackClock, BoardPlayer, BoardPlayerFrame, createBoardPlayer, nextLocalPlayback, playbackClockAt };
@@ -5,16 +5,35 @@ import { createBoardEntityId } from "../model/items/id.js";
5
5
  const ENTER_ANIMATION = "\0enter";
6
6
  const ENTER_STAGGER_MS = 45;
7
7
  const ENTER_MAX_ITEMS = 80;
8
- function playbackTimeAt(playback, document, now) {
8
+ const SETTLED = {
9
+ time: null,
10
+ advancing: false
11
+ };
12
+ function playbackClockAt(playback, document, now) {
9
13
  const animation = document.animations[playback.animationId];
10
- if (!animation || playback.status === "stopped") return null;
11
- if (playback.status !== "playing") return Math.min(playback.position, animation.duration);
12
- let position = playback.position + Math.max(0, now - playback.effectiveAt) * playback.timeScale;
14
+ if (!animation || playback.status === "stopped") return SETTLED;
15
+ if (playback.status !== "playing") return {
16
+ time: Math.min(playback.position, animation.duration),
17
+ advancing: false
18
+ };
19
+ const position = playback.position + Math.max(0, now - playback.effectiveAt) * playback.timeScale;
13
20
  const pause = animation.markers.find((marker) => marker.pause && marker.at > playback.position && marker.at <= position);
14
- if (pause) return pause.at;
15
- if (animation.loop || animation.play === "always") return animation.duration > 0 ? position % animation.duration : 0;
16
- position = Math.min(position, animation.duration);
17
- return animation.end === "reset" && position >= animation.duration ? null : position;
21
+ if (pause) return {
22
+ time: pause.at,
23
+ advancing: false
24
+ };
25
+ if (animation.loop || animation.play === "always") return {
26
+ time: position % animation.duration,
27
+ advancing: true
28
+ };
29
+ if (position < animation.duration) return {
30
+ time: position,
31
+ advancing: true
32
+ };
33
+ return {
34
+ time: animation.end === "reset" ? null : animation.duration,
35
+ advancing: false
36
+ };
18
37
  }
19
38
  function createBoardPlayer(options = {}) {
20
39
  let document = null;
@@ -89,11 +108,10 @@ function createBoardPlayer(options = {}) {
89
108
  }
90
109
  let playbackTime = null;
91
110
  if (playback && playback.animationId !== exclude) {
92
- playbackTime = playbackTimeAt(playback, current, wallNow);
93
- if (playbackTime !== null) {
94
- times.set(playback.animationId, playbackTime);
95
- if (playback.status === "playing") running = true;
96
- }
111
+ const clock = playbackClockAt(playback, current, wallNow);
112
+ playbackTime = clock.time;
113
+ if (playbackTime !== null) times.set(playback.animationId, playbackTime);
114
+ if (clock.advancing) running = true;
97
115
  }
98
116
  if (entrance) {
99
117
  times.set(ENTER_ANIMATION, now - entrance.startedAt);
@@ -111,7 +129,7 @@ function createBoardPlayer(options = {}) {
111
129
  };
112
130
  }
113
131
  function nextLocalPlayback(current, command, document, now = Date.now()) {
114
- const position = current ? playbackTimeAt(current, document, now) ?? 0 : 0;
132
+ const position = current ? playbackClockAt(current, document, now).time ?? 0 : 0;
115
133
  const base = {
116
134
  effectiveAt: now,
117
135
  commandId: command.commandId,
@@ -159,4 +177,4 @@ function nextLocalPlayback(current, command, document, now = Date.now()) {
159
177
  }
160
178
  }
161
179
  //#endregion
162
- export { createBoardPlayer, nextLocalPlayback, playbackTimeAt };
180
+ export { createBoardPlayer, nextLocalPlayback, playbackClockAt };
@@ -11,10 +11,10 @@ import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
11
11
  import { BoardScriptHostOptions, BrowserBoardScriptHost, createBoardScriptHost } from "./script-host.js";
12
12
  import { prepareBrowserBoardScripts } from "./script-export.js";
13
13
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
14
- import { CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
14
+ import { CardCrop, CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, fillCardPicture, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
15
15
  import { BOARD_EFFECT_MAX_PARTICLES } from "./renderers/effect-card-renderer.js";
16
16
  import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
17
17
  import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
18
18
  import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
19
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
20
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, BoardBackgroundContext, BoardCardRenderer, BoardCardRendererResolver, BoardCssPaint, BoardMediaAction, BoardRenderContext, BoardRenderDiagnostic, BoardRenderLabels, BoardRenderPalette, BoardRenderStyle, BoardScriptHost, BoardScriptHostOptions, BoardSelectionStyle, BoardShadowStyle, BrowserBoardScriptHost, CARD_TAG_SIZE, CardElevation, CardGlyph, CardRect, CardSurface, CardTag, ClipGroup, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
20
+ export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, BoardBackgroundContext, BoardCardRenderer, BoardCardRendererResolver, BoardCssPaint, BoardMediaAction, BoardRenderContext, BoardRenderDiagnostic, BoardRenderLabels, BoardRenderPalette, BoardRenderStyle, BoardScriptHost, BoardScriptHostOptions, BoardSelectionStyle, BoardShadowStyle, BrowserBoardScriptHost, CARD_TAG_SIZE, CardCrop, CardElevation, CardGlyph, CardRect, CardSurface, CardTag, ClipGroup, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, VideoNaturalSize, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -1,15 +1,15 @@
1
1
  import { parseBoardCssColor, parseBoardCssPaint } from "./css-color.js";
2
- import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
3
- import { createBoardRenderSampler } from "./sampling.js";
4
- import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
- import { inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
6
2
  import { createBoardBackground, updateBoardBackground } from "./board-background.js";
7
3
  import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
8
4
  import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
9
5
  import { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, traceCube, zoomCardTag } from "./card-tag.js";
10
6
  import { clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
7
+ import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
8
+ import { inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
9
+ import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
10
+ import { createBoardRenderSampler } from "./sampling.js";
11
11
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
12
- import { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
12
+ import { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, fillCardPicture, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.js";
13
13
  import { boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter } from "./media-interaction.js";
14
14
  import { BOARD_EFFECT_MAX_PARTICLES } from "./renderers/effect-card-renderer.js";
15
15
  import { DEFAULT_BOARD_RENDER_LABELS, createBoardCardRendererResolver, getBoardCardRenderer } from "./renderers/board-renderer-registry.js";
@@ -17,4 +17,4 @@ import { createBoardScriptHost } from "./script-host.js";
17
17
  import { prepareBrowserBoardScripts } from "./script-export.js";
18
18
  import { defaultBoardRenderStyle, resolveBoardRenderStyle } from "./style.js";
19
19
  import { VIDEO_THUMBNAIL_MAX_EDGE, loadVideoThumbnailTexture, videoTextureNaturalSize, videoThumbnailSize } from "./video-thumbnail.js";
20
- export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, CARD_TAG_SIZE, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
20
+ export { BOARD_EFFECT_MAX_PARTICLES, BOARD_FONT_STACKS, CARD_TAG_SIZE, DEFAULT_BOARD_RENDER_LABELS, PIXEL_STEP, VIDEO_THUMBNAIL_MAX_EDGE, boardCardDetailed, boardMediaActionAt, cardElevation, cardOutlinePoints, cardShadow, clippingAncestor, containRect, createBoardBackground, createBoardCardRendererResolver, createBoardRenderSampler, createBoardScriptHost, createCardTag, dashPattern, defaultBoardPalette, defaultBoardRenderStyle, drawCardGlyph, drawCardShadow, drawCardSurface, ellipsizeWrappedLines, endAngle, ensureBoardTextMeasurement, fillCardPicture, fitLineWithEllipsis, fitTextToLines, fitTextToWidth, formatBoardDuration, getBoardCardRenderer, getBoardResolution, inspectBoardItems, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, traceCube, traceFrameOutline, tracePixelRect, tracePolyline, trimPolyline, updateBoardBackground, videoTextureNaturalSize, videoThumbnailSize, zoomCardTag };
@@ -1,7 +1,7 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { pathPointAt, resolveSceneArrow } from "../../model/arrow-geometry.js";
3
- import { itemAlpha, itemColor } from "../palette.js";
4
3
  import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
+ import { itemAlpha, itemColor } from "../palette.js";
5
5
  import { dashPattern, endAngle, traceArrowhead, tracePolyline, trimPolyline } from "../stroke.js";
6
6
  import { drawCardSurface } from "../surface.js";
7
7
  import { drawFarStroke } from "./far-plate.js";
@@ -128,6 +128,10 @@ const arrowCardRenderer = {
128
128
  update: (container, item, context) => {
129
129
  if (item.type === "arrow") sync(container, item, context);
130
130
  },
131
+ resolution: (container, _item, context) => {
132
+ const parts = partsByContainer.get(container);
133
+ if (parts) syncTextResolution(parts.label, parts, context.zoom);
134
+ },
131
135
  renderFar: (graphics, item, context) => {
132
136
  if (item.type !== "arrow") return;
133
137
  const arrow = item;
@@ -1,8 +1,8 @@
1
1
  import { getMediaResourceTitle } from "../../model/media.js";
2
+ import { drawAudioWaveform } from "../audio-waveform.js";
2
3
  import { fitTextToWidth } from "../text-fit.js";
3
4
  import { boardCardDetailed, syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
5
  import { formatBoardDuration } from "../card-tag.js";
5
- import { drawAudioWaveform } from "../audio-waveform.js";
6
6
  import { cardElevation, drawCardSurface } from "../surface.js";
7
7
  import { positionShell } from "./base-card-renderer.js";
8
8
  import { drawFarPlate } from "./far-plate.js";
@@ -108,6 +108,9 @@ const audioCardRenderer = {
108
108
  update: (container, item, context) => {
109
109
  if (item.type === "audio") sync(container, item, context);
110
110
  },
111
+ resolution: (container, item, context) => {
112
+ if (item.type === "audio") sync(container, item, context);
113
+ },
111
114
  renderFar: (graphics, item, context) => {
112
115
  drawFarPlate(graphics, item.frame, {
113
116
  fill: context.palette.surface,
@@ -22,7 +22,7 @@ type BoardScriptHost = {
22
22
  document?: (document: BoardDocument, time: number) => BoardDocument;
23
23
  ready?: (id: string) => boolean;
24
24
  revision?: (id: string) => number;
25
- frame: (item: SceneItem<BoardCustomItem>, time: number, resolution: number) => Texture | null;
25
+ frame: (item: SceneItem<BoardCustomItem>, time: number) => Texture | null;
26
26
  error: (id: string) => string | null;
27
27
  release: (id: string) => void;
28
28
  };
@@ -61,7 +61,7 @@ type BoardRenderContext = {
61
61
  getTexture: (key: string) => Texture | null;
62
62
  hasError: (key: string) => boolean;
63
63
  fileState: (path: string) => "ok" | "missing" | "unavailable";
64
- acquireTexture: (key: string) => void;
64
+ acquireTexture: (key: string, item: BoardSceneItem) => void;
65
65
  releaseTexture: (key: string) => void;
66
66
  };
67
67
  type BoardCardRenderer = {
@@ -73,6 +73,8 @@ type BoardCardRenderer = {
73
73
  tick?: (container: Container, clock: number) => number | null;
74
74
  create: (item: BoardSceneItem, context: BoardRenderContext) => Container;
75
75
  update: (container: Container, item: BoardSceneItem, context: BoardRenderContext) => void;
76
+ /** Called only when the zoom bucket or display density changes. */
77
+ resolution?: (container: Container, item: BoardSceneItem, context: BoardRenderContext) => void;
76
78
  /** Runs on every zoom step; only update transforms and visibility. */
77
79
  zoom?: (container: Container, zoom: number) => void;
78
80
  renderFar?: (graphics: Graphics, item: BoardSceneItem, context: BoardRenderContext) => void;
@@ -15,8 +15,7 @@ function sync(container, item, context) {
15
15
  positionShell(parts.root, item);
16
16
  syncTextResolution(parts.label, parts.label, context.zoom);
17
17
  const { width, height } = item.frame;
18
- const resolution = Math.min(4, Math.max(.25, context.zoom * getBoardResolution()));
19
- const texture = context.scripts?.frame(item, context.time, resolution) ?? null;
18
+ const texture = context.scripts?.frame(item, context.time) ?? null;
20
19
  const error = context.scripts?.error(item.id) ?? null;
21
20
  parts.sprite.visible = Boolean(texture);
22
21
  const display = texture ?? Texture.EMPTY;
@@ -81,6 +80,10 @@ const customCardRenderer = {
81
80
  update: (container, item, context) => {
82
81
  if (item.type === "custom") sync(container, item, context);
83
82
  },
83
+ resolution: (container, _item, context) => {
84
+ const parts = partsByContainer.get(container);
85
+ if (parts) syncTextResolution(parts.label, parts.label, context.zoom);
86
+ },
84
87
  renderFar: (graphics, item, context) => {
85
88
  drawFarPlate(graphics, item.frame, {
86
89
  fill: context.palette.muted,
@@ -73,7 +73,7 @@ function sync(container, item, context) {
73
73
  height,
74
74
  detail,
75
75
  elevation,
76
- texture ? `${texture.width}x${texture.height}` : "none",
76
+ texture?.uid ?? "none",
77
77
  coverFailed,
78
78
  fileState,
79
79
  kind,
@@ -258,6 +258,9 @@ const fileCardRenderer = {
258
258
  update: (container, item, context) => {
259
259
  if (item.type === "file") sync(container, item, context);
260
260
  },
261
+ resolution: (container, item, context) => {
262
+ if (item.type === "file") sync(container, item, context);
263
+ },
261
264
  zoom: (container, zoom) => {
262
265
  const tag = partsByContainer.get(container)?.tag;
263
266
  if (tag) zoomCardTag(tag, zoom);
@@ -1,6 +1,6 @@
1
1
  import { BOARD_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { itemAlpha, itemColor } from "../palette.js";
3
2
  import { syncTextResolution } from "../text-resolution.js";
3
+ import { itemAlpha, itemColor } from "../palette.js";
4
4
  import { dashPattern, roundRectPoints, tracePolyline, trimPolyline } from "../stroke.js";
5
5
  import { createLabel, positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate } from "./far-plate.js";
@@ -98,6 +98,10 @@ const frameCardRenderer = {
98
98
  update: (container, item, context) => {
99
99
  if (item.type === "frame") sync(container, item, context);
100
100
  },
101
+ resolution: (container, _item, context) => {
102
+ const parts = partsByContainer.get(container);
103
+ if (parts) syncTextResolution(parts.label, parts, context.zoom);
104
+ },
101
105
  renderFar: (graphics, item, context) => {
102
106
  if (item.type !== "frame" || item.props.decoration === "none") return;
103
107
  drawFarPlate(graphics, item.frame, {