@neta-art/cohub 11.3.0 → 11.3.2

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 (38) hide show
  1. package/dist/board/export/index.js +13 -2
  2. package/dist/board/export/node/fonts.d.ts +13 -0
  3. package/dist/board/export/node/fonts.js +44 -0
  4. package/dist/board/export/node/index.d.ts +4 -10
  5. package/dist/board/export/node/index.js +12 -20
  6. package/dist/board/export/node/scripts.d.ts +1 -0
  7. package/dist/board/export/node/scripts.js +21 -8
  8. package/dist/board/model/authoring.js +1 -2
  9. package/dist/board/model/index.d.ts +2 -2
  10. package/dist/board/model/index.js +2 -2
  11. package/dist/board/model/items/create.js +6 -2
  12. package/dist/board/model/media.d.ts +5 -1
  13. package/dist/board/model/media.js +12 -1
  14. package/dist/board/render/card-tag.d.ts +3 -2
  15. package/dist/board/render/card-tag.js +31 -1
  16. package/dist/board/render/diagnostics.d.ts +20 -2
  17. package/dist/board/render/diagnostics.js +17 -4
  18. package/dist/board/render/index.d.ts +3 -3
  19. package/dist/board/render/index.js +3 -3
  20. package/dist/board/render/media-interaction.js +12 -3
  21. package/dist/board/render/renderers/board-renderer-registry.js +2 -0
  22. package/dist/board/render/renderers/media-key.d.ts +2 -1
  23. package/dist/board/render/renderers/media-key.js +21 -1
  24. package/dist/board/render/renderers/model-card-renderer.d.ts +10 -0
  25. package/dist/board/render/renderers/model-card-renderer.js +132 -0
  26. package/dist/board/stage/assets.d.ts +16 -3
  27. package/dist/board/stage/assets.js +98 -47
  28. package/dist/board/stage/index.d.ts +2 -2
  29. package/dist/chunks/environment.d.ts +46 -3
  30. package/dist/index.d.ts +30 -1
  31. package/dist/index.js +237 -6
  32. package/dist/protocol/dist/board-patch.js +4 -4
  33. package/dist/protocol/dist/board-schema.js +13 -2
  34. package/dist/protocol/dist/board.js +2 -1
  35. package/dist/protocol/dist/fs/mime.d.ts +4 -0
  36. package/dist/protocol/dist/fs/mime.js +113 -0
  37. package/dist/protocol/dist/index.d.ts +2 -1
  38. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
1
  import { BoardRenderStyle, BoardSelectionStyle, BoardShadowStyle, defaultBoardRenderStyle, resolveBoardRenderStyle } from "./style.js";
2
2
  import { BoardCardRenderer, BoardCardRendererResolver, BoardRenderContext, BoardRenderLabels, BoardRenderPalette, BoardScriptHost, DEFAULT_BOARD_RENDER_LABELS, createBoardCardRendererResolver, getBoardCardRenderer } from "./renderers/board-renderer-registry.js";
3
3
  import { createBoardRenderSampler } from "./sampling.js";
4
- import { BoardRenderDiagnostic, inspectBoardScene, measureBoardText } from "./diagnostics.js";
4
+ import { BoardRenderDiagnostic, inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
5
5
  import { BoardBackgroundContext, createBoardBackground, updateBoardBackground } from "./board-background.js";
6
- import { CARD_TAG_SIZE, CardGlyph, CardTag, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag } from "./card-tag.js";
6
+ import { CARD_TAG_SIZE, CardGlyph, CardTag, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, traceCube, zoomCardTag } from "./card-tag.js";
7
7
  import { ClipGroup, clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
8
8
  import { BoardCssPaint, parseBoardCssColor, parseBoardCssPaint } from "./css-color.js";
9
9
  import { BoardMediaAction, boardMediaActionAt, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter } from "./media-interaction.js";
@@ -17,4 +17,4 @@ import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWi
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, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, 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, 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 };
@@ -2,11 +2,11 @@ import { parseBoardCssColor, parseBoardCssPaint } from "./css-color.js";
2
2
  import { defaultBoardPalette, itemAlpha, itemColor } from "./palette.js";
3
3
  import { createBoardRenderSampler } from "./sampling.js";
4
4
  import { BOARD_FONT_STACKS, ensureBoardTextMeasurement, installBoardTextMeasurement } from "./text-measurement.js";
5
- import { inspectBoardScene, measureBoardText } from "./diagnostics.js";
5
+ import { inspectBoardItems, inspectBoardScene, measureBoardText } from "./diagnostics.js";
6
6
  import { createBoardBackground, updateBoardBackground } from "./board-background.js";
7
7
  import { ellipsizeWrappedLines, fitLineWithEllipsis, fitTextToLines, fitTextToWidth } from "./text-fit.js";
8
8
  import { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket } from "./text-resolution.js";
9
- import { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, zoomCardTag } from "./card-tag.js";
9
+ import { CARD_TAG_SIZE, createCardTag, drawCardGlyph, formatBoardDuration, syncCardTag, traceCube, zoomCardTag } from "./card-tag.js";
10
10
  import { clippingAncestor, isClippingFrame, syncClipGroup, traceFrameOutline } from "./clip.js";
11
11
  import { dashPattern, endAngle, roundRectPoints, traceArrowhead, tracePolyline, trimPolyline } from "./stroke.js";
12
12
  import { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect } from "./surface.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, inspectBoardScene, installBoardTextMeasurement, isBoardCard, isClippingFrame, itemAlpha, itemColor, loadVideoThumbnailTexture, measureBoardText, mediaPlayBadgeHit, mediaPlayBadgeVisible, mediaPlayKeyCenter, parseBoardCssColor, parseBoardCssPaint, prepareBrowserBoardScripts, resolveBoardRenderStyle, roundRectPoints, syncCardTag, syncClipGroup, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket, traceArrowhead, traceCardOutline, 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, 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,25 +1,34 @@
1
1
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
2
2
  import { degToRad, rotatePointAround, worldPoint } from "../model/geometry.js";
3
+ import { isBoardModelItem } from "../model/media.js";
3
4
  import { boardCardDetailed } from "./text-resolution.js";
4
5
  import { audioKeyCenter } from "./renderers/audio-card-renderer.js";
6
+ import { modelKeyCenter } from "./renderers/model-card-renderer.js";
5
7
  //#region src/board/render/media-interaction.ts
6
8
  function mediaPlayBadgeVisible(item, zoom, options) {
7
9
  if (!options.materialized) return false;
8
10
  if (item.type === "video" || item.type === "audio") return true;
11
+ if (isBoardModelItem(item)) return boardCardDetailed(zoom);
9
12
  if (item.type !== "task" || !boardCardDetailed(zoom)) return false;
10
13
  const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
11
14
  if (artifact?.type === "audio") return true;
12
15
  return artifact?.type === "video" && (Boolean(artifact.previewUrl) || options.hasVideoPreview);
13
16
  }
17
+ function cornerKey(item) {
18
+ const { width, height } = item.frame;
19
+ if (item.type === "audio") return audioKeyCenter(width, height);
20
+ return isBoardModelItem(item) ? modelKeyCenter(width, height) : null;
21
+ }
14
22
  function mediaPlayKeyCenter(item) {
15
23
  const { x, y, width, height, rotation } = item.frame;
16
24
  const center = worldPoint(x + width / 2, y + height / 2);
17
- if (item.type !== "audio") return center;
18
- const key = audioKeyCenter(width, height);
25
+ const key = cornerKey(item);
26
+ if (!key) return center;
19
27
  return rotatePointAround(worldPoint(x + key.x, y + key.y), center, degToRad(rotation || 0));
20
28
  }
21
29
  function mediaPlayBadgeHit(item, point, zoom) {
22
- const radius = 28 / Math.max(zoom, .05);
30
+ const scale = Math.max(zoom, .05);
31
+ const radius = isBoardModelItem(item) ? Math.max(cornerKey(item)?.radius ?? 0, 16 / scale) : 28 / scale;
23
32
  const center = mediaPlayKeyCenter(item);
24
33
  return Math.hypot(point.x - center.x, point.y - center.y) <= radius;
25
34
  }
@@ -1,5 +1,6 @@
1
1
  import { ensureBoardTextMeasurement } from "../text-measurement.js";
2
2
  import { audioCardRenderer } from "./audio-card-renderer.js";
3
+ import { modelCardRenderer } from "./model-card-renderer.js";
3
4
  import { arrowCardRenderer } from "./arrow-card-renderer.js";
4
5
  import { drawCardRenderer } from "./draw-card-renderer.js";
5
6
  import { effectCardRenderer } from "./effect-card-renderer.js";
@@ -25,6 +26,7 @@ const builtinCardRenderers = [
25
26
  imageCardRenderer,
26
27
  videoCardRenderer,
27
28
  audioCardRenderer,
29
+ modelCardRenderer,
28
30
  fileCardRenderer,
29
31
  taskCardRenderer,
30
32
  shapeCardRenderer,
@@ -2,5 +2,6 @@ import { BoardRenderContext } from "./board-renderer-registry.js";
2
2
  import { Graphics } from "pixi.js";
3
3
  //#region src/board/render/renderers/media-key.d.ts
4
4
  declare function drawMediaPlayKey(graphics: Graphics, cx: number, cy: number, radius: number, context: BoardRenderContext, solid?: boolean): void;
5
+ declare function drawModelKey(graphics: Graphics, cx: number, cy: number, radius: number, context: BoardRenderContext): void;
5
6
  //#endregion
6
- export { drawMediaPlayKey };
7
+ export { drawMediaPlayKey, drawModelKey };
@@ -1,3 +1,4 @@
1
+ import { traceCube } from "../card-tag.js";
1
2
  import { tracePixelRect } from "../surface.js";
2
3
  //#region src/board/render/renderers/media-key.ts
3
4
  const MEDIA_SCRIM = 0;
@@ -35,5 +36,24 @@ function drawMediaPlayKey(graphics, cx, cy, radius, context, solid = false) {
35
36
  alpha: .96
36
37
  });
37
38
  }
39
+ function drawModelKey(graphics, cx, cy, radius, context) {
40
+ if (context.style.corner === "pixel") {
41
+ const size = Math.round(radius * 1.6);
42
+ tracePixelRect(graphics, Math.round(cx - size / 2), Math.round(cy - size / 2), size, size, 2).fill({
43
+ color: MEDIA_SCRIM,
44
+ alpha: .62
45
+ });
46
+ } else graphics.circle(cx, cy, radius).fill({
47
+ color: MEDIA_SCRIM,
48
+ alpha: .45
49
+ });
50
+ traceCube(graphics, cx, cy, radius * .5).stroke({
51
+ color: MEDIA_INK,
52
+ width: Math.max(1, radius / 10),
53
+ alpha: .96,
54
+ join: "round",
55
+ cap: "round"
56
+ });
57
+ }
38
58
  //#endregion
39
- export { drawMediaPlayKey };
59
+ export { drawMediaPlayKey, drawModelKey };
@@ -0,0 +1,10 @@
1
+ import { BoardCardRenderer } from "./board-renderer-registry.js";
2
+ //#region src/board/render/renderers/model-card-renderer.d.ts
3
+ declare function modelKeyCenter(width: number, height: number): {
4
+ x: number;
5
+ y: number;
6
+ radius: number;
7
+ };
8
+ declare const modelCardRenderer: BoardCardRenderer;
9
+ //#endregion
10
+ export { modelCardRenderer, modelKeyCenter };
@@ -0,0 +1,132 @@
1
+ import { isBoardModelItem } from "../../model/media.js";
2
+ import { fileStem } from "../../model/file-snapshot.js";
3
+ import { fileMetaLine } from "../../model/file-preview.js";
4
+ import { boardCardDetailed } from "../text-resolution.js";
5
+ import { createCardTag, syncCardTag, traceCube, zoomCardTag } from "../card-tag.js";
6
+ import { cardElevation, containRect, drawCardSurface } from "../surface.js";
7
+ import { positionShell } from "./base-card-renderer.js";
8
+ import { drawFarPlate } from "./far-plate.js";
9
+ import { drawModelKey } from "./media-key.js";
10
+ import { Container, Graphics, Sprite, Texture } from "pixi.js";
11
+ //#region src/board/render/renderers/model-card-renderer.ts
12
+ const partsByContainer = /* @__PURE__ */ new WeakMap();
13
+ const INSET_RATIO = .06;
14
+ const KEY_RADIUS = 14;
15
+ const KEY_MARGIN = 10;
16
+ function modelKeyCenter(width, height) {
17
+ const radius = Math.max(8, Math.min(KEY_RADIUS, Math.min(width, height) * .12));
18
+ const margin = Math.min(KEY_MARGIN, radius * .7);
19
+ return {
20
+ x: width - margin - radius,
21
+ y: height - margin - radius,
22
+ radius
23
+ };
24
+ }
25
+ function sync(container, item, context) {
26
+ const parts = partsByContainer.get(container);
27
+ if (!parts) return;
28
+ positionShell(parts.root, item);
29
+ const { width, height } = item.frame;
30
+ const { src, snapshot } = item.props;
31
+ const meta = fileMetaLine(src, snapshot?.size);
32
+ const title = snapshot?.title || fileStem(src);
33
+ syncCardTag(parts.tag, {
34
+ glyph: "model",
35
+ text: `${title} · ${meta}`,
36
+ width
37
+ }, context);
38
+ const key = context.assetKey(item);
39
+ const texture = key ? context.getTexture(key) : null;
40
+ const failed = Boolean(key && !texture && context.hasError(key));
41
+ const detailed = boardCardDetailed(context.zoom);
42
+ const elevation = cardElevation(context, item.id);
43
+ const sig = [
44
+ key ?? "",
45
+ texture ? `${texture.width}x${texture.height}` : "none",
46
+ failed,
47
+ width,
48
+ height,
49
+ elevation,
50
+ detailed,
51
+ context.themeKey
52
+ ].join("|");
53
+ if (sig === parts.sig) return;
54
+ parts.sig = sig;
55
+ drawCardSurface(parts.plate.clear(), {
56
+ x: 0,
57
+ y: 0,
58
+ width,
59
+ height
60
+ }, context.style, {
61
+ fill: context.palette.surface,
62
+ border: context.palette.border,
63
+ borderAlpha: .85,
64
+ elevation
65
+ });
66
+ if (texture && parts.sprite.texture !== texture) parts.sprite.texture = texture;
67
+ parts.sprite.visible = Boolean(texture);
68
+ if (texture) {
69
+ const inset = Math.min(width, height) * INSET_RATIO;
70
+ const fitted = containRect(width - inset * 2, height - inset * 2, texture.width, texture.height);
71
+ parts.sprite.position.set(inset + fitted.x, inset + fitted.y);
72
+ parts.sprite.width = fitted.width;
73
+ parts.sprite.height = fitted.height;
74
+ } else {
75
+ const radius = Math.max(6, Math.min(28, Math.min(width, height) * .14));
76
+ traceCube(parts.plate, width / 2, height / 2, radius).stroke({
77
+ color: context.palette.muted,
78
+ width: 1.5,
79
+ alpha: failed ? .35 : .6,
80
+ join: "round",
81
+ cap: "round"
82
+ });
83
+ }
84
+ parts.chrome.clear();
85
+ if (detailed) {
86
+ const center = modelKeyCenter(width, height);
87
+ drawModelKey(parts.chrome, center.x, center.y, center.radius, context);
88
+ }
89
+ }
90
+ const modelCardRenderer = {
91
+ id: "model-card",
92
+ canRender: (item) => isBoardModelItem(item),
93
+ create: (item, context) => {
94
+ const root = new Container();
95
+ const plate = new Graphics();
96
+ const sprite = new Sprite(Texture.EMPTY);
97
+ const chrome = new Graphics();
98
+ const tag = createCardTag();
99
+ root.addChild(plate, sprite, chrome, tag.root);
100
+ partsByContainer.set(root, {
101
+ root,
102
+ plate,
103
+ sprite,
104
+ chrome,
105
+ tag,
106
+ sig: ""
107
+ });
108
+ if (item.type === "file") sync(root, item, context);
109
+ return root;
110
+ },
111
+ update: (container, item, context) => {
112
+ if (item.type === "file") sync(container, item, context);
113
+ },
114
+ zoom: (container, zoom) => {
115
+ const tag = partsByContainer.get(container)?.tag;
116
+ if (tag) zoomCardTag(tag, zoom);
117
+ },
118
+ renderFar: (graphics, item, context) => {
119
+ drawFarPlate(graphics, item.frame, {
120
+ fill: context.palette.surface,
121
+ fillAlpha: .96,
122
+ accent: context.palette.legendary,
123
+ accentAlpha: .45
124
+ });
125
+ },
126
+ destroy: (container) => {
127
+ partsByContainer.get(container)?.root.destroy({ children: true });
128
+ partsByContainer.delete(container);
129
+ }
130
+ };
131
+ //#endregion
132
+ export { modelCardRenderer, modelKeyCenter };
@@ -1,11 +1,23 @@
1
+ import { ModelFormat } from "../../protocol/dist/fs/mime.js";
1
2
  import { BoardSceneItem } from "../model/scene.js";
2
3
  import "../model/index.js";
3
4
  import { LruBudget } from "./lru.js";
4
5
  import { BoardImageTexturePool } from "./texture-pool.js";
5
6
  import { Texture } from "pixi.js";
6
7
  //#region src/board/stage/assets.d.ts
7
- type BoardAssetMedia = "image" | "video";
8
+ type BoardUrlMedia = "image" | "video";
9
+ type BoardAssetMedia = BoardUrlMedia | "model";
8
10
  declare function boardAssetKey(item: BoardSceneItem): string | null;
11
+ type BoardModelPreviewRequest = {
12
+ key: string;
13
+ src: string;
14
+ format: ModelFormat;
15
+ size?: number;
16
+ };
17
+ type BoardModelPreviews = {
18
+ load: (request: BoardModelPreviewRequest) => Promise<ImageBitmap>;
19
+ maxBytes?: number;
20
+ };
9
21
  type BoardAssetManager = {
10
22
  assetKey: (item: BoardSceneItem) => string | null;
11
23
  invalidatePath: (path: string) => void;
@@ -30,12 +42,13 @@ type BoardAssetManagerOptions = {
30
42
  concurrency?: number;
31
43
  videoConcurrency?: number;
32
44
  loadVideoPreviews?: boolean;
45
+ modelPreviews?: BoardModelPreviews;
33
46
  lruBudget?: LruBudget;
34
47
  imageTexturePool?: BoardImageTexturePool;
35
- loadTexture?: (url: string, media: BoardAssetMedia) => Promise<Texture | null>;
48
+ loadTexture?: (url: string, media: BoardUrlMedia) => Promise<Texture | null>;
36
49
  unloadTexture?: (url: string, texture: Texture | null, media: BoardAssetMedia) => Promise<void>;
37
50
  now?: () => number;
38
51
  };
39
52
  declare function createBoardAssetManager(options: BoardAssetManagerOptions): BoardAssetManager;
40
53
  //#endregion
41
- export { BoardAssetManager, BoardAssetManagerOptions, boardAssetKey, createBoardAssetManager };
54
+ export { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, createBoardAssetManager };
@@ -1,10 +1,12 @@
1
1
  import { mediaPreviewCandidates } from "../../media.js";
2
2
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
3
3
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
4
+ import { boardModelFormat, isBoardModelItem } from "../model/media.js";
4
5
  import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
5
6
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
6
7
  import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
7
8
  import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
9
+ import { Texture } from "pixi.js";
8
10
  //#region src/board/stage/assets.ts
9
11
  const TASK_TEXTURE_SIZE = 1536;
10
12
  function taskPreviewKey(artifact) {
@@ -13,13 +15,16 @@ function taskPreviewKey(artifact) {
13
15
  if (still) return `url:${still}`;
14
16
  return artifact.type === "video" ? `video-url:${encodeURIComponent(artifact.url)}` : null;
15
17
  }
18
+ const STILL_MEDIA = ["video", "model"];
19
+ function stillKey(media, src, mtimeMs) {
20
+ const source = parseBoardMediaSource(src);
21
+ if (!source) return null;
22
+ return source.kind === "url" ? `${media}-url:${encodeURIComponent(source.value)}` : `${media}:${encodeURIComponent(source.value)}:${mtimeMs ?? "unknown"}`;
23
+ }
16
24
  function boardAssetKey(item) {
17
25
  if (item.type === "task") return taskPreviewKey(featuredTaskArtifact(item.props.snapshot.artifacts));
18
- if (item.type === "video") {
19
- const source = parseBoardMediaSource(item.props.src);
20
- if (!source) return null;
21
- return source.kind === "url" ? `video-url:${encodeURIComponent(source.value)}` : `video:${encodeURIComponent(source.value)}:${item.props.snapshot?.mtimeMs ?? "unknown"}`;
22
- }
26
+ if (item.type === "video") return stillKey("video", item.props.src, item.props.snapshot?.mtimeMs);
27
+ if (item.type === "file" && isBoardModelItem(item)) return stillKey("model", item.props.src, item.props.snapshot?.mtimeMs);
23
28
  return imageAssetKey(item);
24
29
  }
25
30
  function keySource(key) {
@@ -29,27 +34,29 @@ function keySource(key) {
29
34
  if (separator < 0) return null;
30
35
  baseKey = key.slice(separator + 1);
31
36
  }
32
- if (baseKey.startsWith("video-url:")) try {
33
- return {
34
- kind: "url",
35
- media: "video",
36
- value: decodeURIComponent(baseKey.slice(10))
37
- };
38
- } catch {
39
- return null;
40
- }
41
- if (baseKey.startsWith("video:")) {
42
- const separator = baseKey.lastIndexOf(":");
43
- if (separator <= 6) return null;
44
- try {
37
+ for (const media of STILL_MEDIA) {
38
+ if (baseKey.startsWith(`${media}-url:`)) try {
45
39
  return {
46
- kind: "file",
47
- media: "video",
48
- value: decodeURIComponent(baseKey.slice(6, separator))
40
+ kind: "url",
41
+ media,
42
+ value: decodeURIComponent(baseKey.slice(media.length + 5))
49
43
  };
50
44
  } catch {
51
45
  return null;
52
46
  }
47
+ if (baseKey.startsWith(`${media}:`)) {
48
+ const separator = baseKey.lastIndexOf(":");
49
+ if (separator <= media.length) return null;
50
+ try {
51
+ return {
52
+ kind: "file",
53
+ media,
54
+ value: decodeURIComponent(baseKey.slice(media.length + 1, separator))
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
53
60
  }
54
61
  const image = boardImageKeySource(baseKey);
55
62
  return image ? {
@@ -64,7 +71,11 @@ function footprintOf(texture) {
64
71
  }
65
72
  function createBoardAssetManager(options) {
66
73
  const concurrency = Math.max(1, options.concurrency ?? 4);
67
- const videoConcurrency = Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency));
74
+ const limits = {
75
+ image: concurrency,
76
+ video: Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency)),
77
+ model: 1
78
+ };
68
79
  const budget = options.lruBudget ?? DEFAULT_LRU_BUDGET;
69
80
  const now = options.now ?? (() => Date.now());
70
81
  const imagePool = options.imageTexturePool ?? boardImageTexturePool;
@@ -72,8 +83,10 @@ function createBoardAssetManager(options) {
72
83
  const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
73
84
  const pendingUnloads = /* @__PURE__ */ new Map();
74
85
  const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
75
- if (media === "video" || !url) {
86
+ if (media !== "image" || !url) {
87
+ const resource = texture?.source.resource;
76
88
  texture?.destroy(true);
89
+ resource?.close?.();
77
90
  return;
78
91
  }
79
92
  if (useSharedImagePool) {
@@ -93,12 +106,17 @@ function createBoardAssetManager(options) {
93
106
  const resolveFileUrl = options.resolveFileUrl;
94
107
  const entries = /* @__PURE__ */ new Map();
95
108
  const pathVersions = /* @__PURE__ */ new Map();
96
- const resolvers = /* @__PURE__ */ new Map();
109
+ const loaders = /* @__PURE__ */ new Map();
110
+ const failedModels = /* @__PURE__ */ new Set();
97
111
  const inflight = /* @__PURE__ */ new Set();
98
112
  const queue = [];
99
113
  const listeners = /* @__PURE__ */ new Set();
100
114
  let active = 0;
101
- let activeVideos = 0;
115
+ const activeByMedia = {
116
+ image: 0,
117
+ video: 0,
118
+ model: 0
119
+ };
102
120
  let disposed = false;
103
121
  function notify() {
104
122
  for (const listener of listeners) listener();
@@ -108,6 +126,7 @@ function createBoardAssetManager(options) {
108
126
  if (!key) return null;
109
127
  const source = keySource(key);
110
128
  if (source?.media === "video" && options.loadVideoPreviews === false) return null;
129
+ if (source?.media === "model" && !modelPreviewable(item)) return null;
111
130
  if (source?.kind !== "file") return key;
112
131
  const version = pathVersions.get(source.value) ?? 0;
113
132
  return version > 0 ? `local:${version}:${key}` : key;
@@ -143,13 +162,13 @@ function createBoardAssetManager(options) {
143
162
  entry.retryTimer = null;
144
163
  if (disposed || entries.get(key) !== entry) return;
145
164
  if (entry.refs <= 0 || entry.texture || entry.loading) return;
146
- const getUrl = resolvers.get(key);
147
- if (!getUrl) return;
165
+ const load = loaders.get(key);
166
+ if (!load) return;
148
167
  entry.error = false;
149
168
  if (!queue.some((task) => task.key === key)) {
150
169
  queue.push({
151
170
  key,
152
- getUrl
171
+ load
153
172
  });
154
173
  pump();
155
174
  }
@@ -157,7 +176,7 @@ function createBoardAssetManager(options) {
157
176
  }
158
177
  function evict(key, entry) {
159
178
  clearRetry(entry);
160
- resolvers.delete(key);
179
+ loaders.delete(key);
161
180
  if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
162
181
  entries.delete(key);
163
182
  }
@@ -178,7 +197,8 @@ function createBoardAssetManager(options) {
178
197
  entry.loading = false;
179
198
  inflight.delete(key);
180
199
  active -= 1;
181
- if (keySource(key)?.media === "video") activeVideos -= 1;
200
+ const media = keySource(key)?.media;
201
+ if (media) activeByMedia[media] -= 1;
182
202
  if (entries.get(key) !== entry) {
183
203
  if (texture) releaseTexture(key, entry.url ?? "", texture);
184
204
  if (!disposed) pump();
@@ -196,9 +216,10 @@ function createBoardAssetManager(options) {
196
216
  entry.attempts += 1;
197
217
  entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
198
218
  console.warn(`[board] failed to load preview for ${key}`, error);
219
+ if (media === "model") failedModels.add(key);
199
220
  if (entry.refs <= 0) evict(key, entry);
200
221
  else {
201
- scheduleRetry(key, entry);
222
+ if (media !== "model") scheduleRetry(key, entry);
202
223
  notify();
203
224
  }
204
225
  }
@@ -207,7 +228,8 @@ function createBoardAssetManager(options) {
207
228
  function pump() {
208
229
  while (!disposed && active < concurrency && queue.length > 0) {
209
230
  const taskIndex = queue.findIndex(({ key }) => {
210
- return keySource(key)?.media !== "video" || activeVideos < videoConcurrency;
231
+ const media = keySource(key)?.media;
232
+ return !media || activeByMedia[media] < limits[media];
211
233
  });
212
234
  if (taskIndex < 0) return;
213
235
  const [task] = queue.splice(taskIndex, 1);
@@ -218,18 +240,42 @@ function createBoardAssetManager(options) {
218
240
  entry.loading = true;
219
241
  inflight.add(task.key);
220
242
  active += 1;
221
- if (source.media === "video") activeVideos += 1;
222
- task.getUrl().then(async (url) => {
223
- if (!url || disposed) return null;
224
- entry.url = url;
225
- const pendingKey = `${source.media}:${url}`;
226
- const pending = pendingUnloads.get(pendingKey);
227
- if (pending) await pending;
228
- if (disposed) return null;
229
- return loadTexture(url, source.media);
230
- }).then((texture) => settle(task.key, entry, texture ?? null), (error) => settle(task.key, entry, null, error));
243
+ activeByMedia[source.media] += 1;
244
+ task.load(entry).then((texture) => settle(task.key, entry, texture), (error) => settle(task.key, entry, null, error));
231
245
  }
232
246
  }
247
+ function urlLoader(source) {
248
+ const getUrl = source.kind === "url" ? () => Promise.resolve(source.value) : () => resolveFileUrl(source.value);
249
+ return async (entry) => {
250
+ const url = await getUrl();
251
+ if (!url || disposed) return null;
252
+ entry.url = url;
253
+ const pending = pendingUnloads.get(`${source.media}:${url}`);
254
+ if (pending) await pending;
255
+ if (disposed) return null;
256
+ return loadTexture(url, source.media);
257
+ };
258
+ }
259
+ function modelPreviewable(item) {
260
+ const previews = options.modelPreviews;
261
+ if (!previews) return false;
262
+ const size = item.type === "file" ? item.props.snapshot?.size : void 0;
263
+ return size === void 0 || size <= (previews.maxBytes ?? Number.POSITIVE_INFINITY);
264
+ }
265
+ function modelLoader(item, key) {
266
+ const previews = options.modelPreviews;
267
+ if (!previews || item.type !== "file") return null;
268
+ const { src, snapshot } = item.props;
269
+ const format = boardModelFormat(src, snapshot?.mimeType);
270
+ if (!format) return null;
271
+ const request = {
272
+ key,
273
+ src,
274
+ format,
275
+ size: snapshot?.size
276
+ };
277
+ return async () => Texture.from(await previews.load(request));
278
+ }
233
279
  const manager = {
234
280
  assetKey: managedAssetKey,
235
281
  invalidatePath(path) {
@@ -244,7 +290,7 @@ function createBoardAssetManager(options) {
244
290
  requestItem(item) {
245
291
  if (disposed) return;
246
292
  const key = managedAssetKey(item);
247
- if (!key) return;
293
+ if (!key || failedModels.has(key)) return;
248
294
  const source = keySource(key);
249
295
  if (!source) return;
250
296
  const entry = ensureEntry(key);
@@ -254,11 +300,15 @@ function createBoardAssetManager(options) {
254
300
  entry.error = false;
255
301
  clearRetry(entry);
256
302
  if (queue.some((queued) => queued.key === key)) return;
257
- const getUrl = source.kind === "url" ? () => Promise.resolve(source.value) : () => resolveFileUrl(source.value);
258
- resolvers.set(key, getUrl);
303
+ const load = source.media === "model" ? modelLoader(item, key) : urlLoader({
304
+ ...source,
305
+ media: source.media
306
+ });
307
+ if (!load) return;
308
+ loaders.set(key, load);
259
309
  queue.push({
260
310
  key,
261
- getUrl
311
+ load
262
312
  });
263
313
  pump();
264
314
  },
@@ -344,7 +394,8 @@ function createBoardAssetManager(options) {
344
394
  queue.length = 0;
345
395
  listeners.clear();
346
396
  pathVersions.clear();
347
- resolvers.clear();
397
+ loaders.clear();
398
+ failedModels.clear();
348
399
  for (const [key, entry] of entries) evict(key, entry);
349
400
  entries.clear();
350
401
  }
@@ -1,9 +1,9 @@
1
1
  import { DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
2
2
  import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { BoardAssetManager, BoardAssetManagerOptions, boardAssetKey, createBoardAssetManager } from "./assets.js";
3
+ import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, createBoardAssetManager } from "./assets.js";
4
4
  import { toBoardPointerEvent } from "./pointer.js";
5
5
  import { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene } from "./scene.js";
6
6
  import { BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, createBoardItemViewLayer } from "./views.js";
7
7
  import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage } from "./stage.js";
8
8
  import { BoardOverlayInput, drawBoardOverlay, expandFrame } from "./overlay.js";
9
- export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, SceneSyncInput, boardAssetKey, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
9
+ export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, SceneSyncInput, boardAssetKey, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };