@neta-art/cohub 11.5.1 → 12.1.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 (82) 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/scene.js +1 -0
  10. package/dist/board/model/client-geometry.d.ts +22 -0
  11. package/dist/board/model/client-geometry.js +94 -0
  12. package/dist/board/model/document-share.d.ts +5 -0
  13. package/dist/board/model/document-share.js +27 -0
  14. package/dist/board/model/index.d.ts +5 -4
  15. package/dist/board/model/index.js +4 -3
  16. package/dist/board/model/items/create.d.ts +5 -1
  17. package/dist/board/model/items/create.js +29 -58
  18. package/dist/board/model/items/index.d.ts +2 -2
  19. package/dist/board/model/items/index.js +2 -2
  20. package/dist/board/model/items/registry.js +2 -5
  21. package/dist/board/model/json.d.ts +4 -0
  22. package/dist/board/model/json.js +22 -0
  23. package/dist/board/model/media-playback.d.ts +6 -3
  24. package/dist/board/player/index.d.ts +6 -2
  25. package/dist/board/player/index.js +33 -15
  26. package/dist/board/render/card-tag.d.ts +0 -1
  27. package/dist/board/render/card-tag.js +15 -17
  28. package/dist/board/render/index.d.ts +3 -3
  29. package/dist/board/render/index.js +3 -3
  30. package/dist/board/render/renderers/arrow-card-renderer.js +8 -6
  31. package/dist/board/render/renderers/audio-card-renderer.js +10 -6
  32. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -1
  33. package/dist/board/render/renderers/base-card-renderer.js +2 -3
  34. package/dist/board/render/renderers/board-renderer-registry.d.ts +6 -2
  35. package/dist/board/render/renderers/custom-card-renderer.js +23 -18
  36. package/dist/board/render/renderers/file-card-renderer.js +14 -9
  37. package/dist/board/render/renderers/frame-card-renderer.js +7 -4
  38. package/dist/board/render/renderers/image-card-renderer.js +13 -49
  39. package/dist/board/render/renderers/model-card-renderer.js +4 -1
  40. package/dist/board/render/renderers/shape-card-renderer.js +8 -6
  41. package/dist/board/render/renderers/task-card-renderer.js +13 -10
  42. package/dist/board/render/renderers/text-card-renderer.js +7 -5
  43. package/dist/board/render/renderers/unknown-card-renderer.js +8 -6
  44. package/dist/board/render/renderers/video-card-renderer.js +19 -18
  45. package/dist/board/render/script-host.d.ts +2 -0
  46. package/dist/board/render/script-host.js +44 -9
  47. package/dist/board/render/script-resolution.d.ts +4 -0
  48. package/dist/board/render/script-resolution.js +18 -0
  49. package/dist/board/render/surface.d.ts +9 -2
  50. package/dist/board/render/surface.js +15 -1
  51. package/dist/board/render/text-resolution.d.ts +4 -7
  52. package/dist/board/render/text-resolution.js +20 -22
  53. package/dist/board/replica/index.js +4 -1
  54. package/dist/board/stage/assets.d.ts +12 -8
  55. package/dist/board/stage/assets.js +173 -88
  56. package/dist/board/stage/canvas-size.d.ts +18 -0
  57. package/dist/board/stage/canvas-size.js +31 -0
  58. package/dist/board/stage/index.d.ts +3 -3
  59. package/dist/board/stage/index.js +3 -3
  60. package/dist/board/stage/lru.d.ts +3 -2
  61. package/dist/board/stage/lru.js +8 -4
  62. package/dist/board/stage/pointer.d.ts +1 -1
  63. package/dist/board/stage/pointer.js +4 -5
  64. package/dist/board/stage/scene.d.ts +8 -1
  65. package/dist/board/stage/scene.js +72 -10
  66. package/dist/board/stage/stage.d.ts +1 -0
  67. package/dist/board/stage/stage.js +151 -63
  68. package/dist/board/stage/text-resolution-tracker.d.ts +8 -0
  69. package/dist/board/stage/text-resolution-tracker.js +33 -0
  70. package/dist/board/stage/views.d.ts +7 -2
  71. package/dist/board/stage/views.js +50 -21
  72. package/dist/chunks/environment.d.ts +16 -2
  73. package/dist/index.js +65 -22
  74. package/dist/protocol/dist/board-constants.js +56 -1
  75. package/dist/protocol/dist/board-layout.d.ts +8 -1
  76. package/dist/protocol/dist/board-layout.js +16 -4
  77. package/dist/protocol/dist/board-model.js +13 -19
  78. package/dist/protocol/dist/board-schema.d.ts +52 -0
  79. package/dist/protocol/dist/board-schema.js +8 -0
  80. package/dist/protocol/dist/core/media.js +1 -0
  81. package/dist/protocol/dist/index.d.ts +2 -2
  82. package/package.json +1 -1
@@ -0,0 +1,18 @@
1
+ //#region src/board/render/script-resolution.ts
2
+ const RESOLUTIONS = [
3
+ .125,
4
+ .25,
5
+ .5,
6
+ 1,
7
+ 2,
8
+ 4
9
+ ];
10
+ const MAX_EDGE = 2048;
11
+ const MAX_PIXELS = 2097152;
12
+ function scriptResolution(width, height, demand, current = 0) {
13
+ const target = Number.isFinite(demand) && demand > 0 ? demand : 1;
14
+ const tier = current > 0 && target <= current * 1.15 && target >= current * .4 ? current : RESOLUTIONS.find((value) => value >= target) ?? 4;
15
+ return Math.min(tier, MAX_EDGE / Math.max(1, width, height), Math.sqrt(MAX_PIXELS / Math.max(1, width * height)));
16
+ }
17
+ //#endregion
18
+ export { scriptResolution };
@@ -1,6 +1,6 @@
1
1
  import { WorldPoint } from "../model/geometry.js";
2
2
  import { BoardRenderStyle, BoardShadowStyle } from "./style.js";
3
- import { Graphics } from "pixi.js";
3
+ import { Graphics, Texture } from "pixi.js";
4
4
  //#region src/board/render/surface.d.ts
5
5
  declare const PIXEL_STEP = 2;
6
6
  type CardRect = {
@@ -10,6 +10,13 @@ type CardRect = {
10
10
  height: number;
11
11
  };
12
12
  declare function traceCardOutline(graphics: Graphics, rect: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, grow?: number): Graphics;
13
+ type CardCrop = {
14
+ x: number;
15
+ y: number;
16
+ w: number;
17
+ h: number;
18
+ };
19
+ declare function fillCardPicture(graphics: Graphics, texture: Texture, picture: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, crop?: CardCrop | null): Graphics;
13
20
  declare function tracePixelRect(graphics: Graphics, x: number, y: number, width: number, height: number, step: number): Graphics;
14
21
  declare function cardOutlinePoints(rect: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, grow?: number): WorldPoint[];
15
22
  declare function drawCardShadow(graphics: Graphics, rect: CardRect, style: Pick<BoardRenderStyle, "corner" | "radius">, shadow: BoardShadowStyle | null): void;
@@ -31,4 +38,4 @@ declare function isBoardCard(item: {
31
38
  type: string;
32
39
  }): boolean;
33
40
  //#endregion
34
- export { CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect };
41
+ export { CardCrop, CardElevation, CardRect, CardSurface, PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, fillCardPicture, isBoardCard, traceCardOutline, tracePixelRect };
@@ -1,5 +1,6 @@
1
1
  import { worldPoint } from "../model/geometry.js";
2
2
  import { roundRectPoints } from "./stroke.js";
3
+ import { Matrix } from "pixi.js";
3
4
  //#region src/board/render/surface.ts
4
5
  const PIXEL_STEP = 2;
5
6
  const SHADOW_LAYERS = 5;
@@ -12,6 +13,19 @@ function traceCardOutline(graphics, rect, style, grow = 0) {
12
13
  const radius = Math.min(Math.max(0, style.radius + grow), width / 2, height / 2);
13
14
  return radius > 0 ? graphics.roundRect(x, y, width, height, radius) : graphics.rect(x, y, width, height);
14
15
  }
16
+ function fillCardPicture(graphics, texture, picture, style, crop) {
17
+ const sampling = texture.source.style;
18
+ if (sampling.addressMode !== "mirror-repeat") {
19
+ sampling.addressMode = "mirror-repeat";
20
+ sampling.update();
21
+ }
22
+ const matrix = crop ? new Matrix(1 / crop.w, 0, 0, 1 / crop.h, -crop.x / crop.w, -crop.y / crop.h) : void 0;
23
+ return traceCardOutline(graphics, picture, style).fill({
24
+ texture,
25
+ textureSpace: "local",
26
+ ...matrix ? { matrix } : {}
27
+ });
28
+ }
15
29
  function tracePixelRect(graphics, x, y, width, height, step) {
16
30
  const s = Math.min(step, width / 4, height / 4);
17
31
  if (s <= 0) return graphics.rect(x, y, width, height);
@@ -129,4 +143,4 @@ function isBoardCard(item) {
129
143
  return !FREE_FORM_TYPES.has(item.type);
130
144
  }
131
145
  //#endregion
132
- export { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, isBoardCard, traceCardOutline, tracePixelRect };
146
+ export { PIXEL_STEP, cardElevation, cardOutlinePoints, cardShadow, containRect, drawCardShadow, drawCardSurface, fillCardPicture, isBoardCard, traceCardOutline, tracePixelRect };
@@ -6,15 +6,12 @@ declare function getBoardResolution(size?: {
6
6
  }, devicePixelRatio?: number): number;
7
7
  declare function textZoomBucket(zoom: number): number;
8
8
  declare function boardCardDetailed(zoom: number): boolean;
9
- /** Effective text resolution for a given camera zoom. */
10
- declare function textResolutionForZoom(zoom: number): number;
11
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
12
- declare function syncTextResolution(text: Text, state: {
13
- resolution: number;
14
- }, zoom: number): void;
9
+ /** Exact screen density at rest; reuse or step the raster while moving. */
10
+ declare function boardTextResolution(target: number, current: number, settled: boolean): number;
11
+ declare function syncTextResolution(text: Text, resolution: number): void;
15
12
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
16
13
  declare function syncTextWrapWidth(text: Text, state: {
17
14
  wrapWidth: number;
18
15
  }, width: number, defer: boolean): void;
19
16
  //#endregion
20
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
17
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -1,19 +1,16 @@
1
1
  //#region src/board/render/text-resolution.ts
2
2
  const MAX_BOARD_RESOLUTION = 3;
3
- const MAX_BOARD_PIXELS = 8e6;
4
- const MAX_TEXT_RESOLUTION = 6;
3
+ const MAX_BOARD_PIXELS = 1e7;
4
+ const MIN_TEXT_RESOLUTION = .125;
5
+ const MAX_TEXT_EDGE = 4096;
6
+ const TEXT_UPSCALE_TOLERANCE = 1.25;
5
7
  function getBoardResolution(size, devicePixelRatio = globalThis.devicePixelRatio) {
6
8
  const density = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
7
9
  const pixels = size ? size.width * size.height : 0;
8
- const budget = pixels > 0 ? Math.sqrt(MAX_BOARD_PIXELS / pixels) : MAX_BOARD_RESOLUTION;
10
+ const budget = pixels > 0 ? Math.max(1, Math.sqrt(MAX_BOARD_PIXELS / pixels)) : MAX_BOARD_RESOLUTION;
9
11
  return Math.min(density, MAX_BOARD_RESOLUTION, budget);
10
12
  }
11
- /**
12
- * Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
13
- * looks soft unless we raise its resolution. We quantise zoom into a few
14
- * buckets so small zoom changes don't thrash texture regeneration.
15
- */
16
- const TEXT_ZOOM_BUCKETS = [
13
+ const ZOOM_BUCKETS = [
17
14
  .5,
18
15
  1,
19
16
  1.5,
@@ -23,23 +20,24 @@ const TEXT_ZOOM_BUCKETS = [
23
20
  ];
24
21
  function textZoomBucket(zoom) {
25
22
  const clamped = Math.max(.1, Math.min(4, zoom));
26
- for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
27
- return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
23
+ for (const bucket of ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
24
+ return ZOOM_BUCKETS[ZOOM_BUCKETS.length - 1] ?? 4;
28
25
  }
29
26
  function boardCardDetailed(zoom) {
30
27
  return textZoomBucket(zoom) >= 1;
31
28
  }
32
- /** Effective text resolution for a given camera zoom. */
33
- function textResolutionForZoom(zoom) {
34
- const bucket = textZoomBucket(zoom);
35
- return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_TEXT_RESOLUTION);
29
+ /** Exact screen density at rest; reuse or step the raster while moving. */
30
+ function boardTextResolution(target, current, settled) {
31
+ if (!Number.isFinite(target) || target <= 0) return current > 0 ? current : 1;
32
+ if (settled || !(current > 0)) return target;
33
+ if (target <= current * TEXT_UPSCALE_TOLERANCE && target >= current / 2) return current;
34
+ return 2 ** Math.ceil(Math.log2(target));
36
35
  }
37
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
38
- function syncTextResolution(text, state, zoom) {
39
- const resolution = textResolutionForZoom(zoom);
40
- if (resolution === state.resolution && resolution === text.resolution) return;
41
- text.resolution = resolution;
42
- state.resolution = resolution;
36
+ function syncTextResolution(text, resolution) {
37
+ const { width, height } = text.bounds;
38
+ const limit = MAX_TEXT_EDGE / Math.max(1, Math.ceil(width + text.style.padding * 2), Math.ceil(height + text.style.padding * 2));
39
+ const next = Math.min(limit, Math.max(MIN_TEXT_RESOLUTION, resolution));
40
+ if (text.resolution !== next) text.resolution = next;
43
41
  }
44
42
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
45
43
  function syncTextWrapWidth(text, state, width, defer) {
@@ -48,4 +46,4 @@ function syncTextWrapWidth(text, state, width, defer) {
48
46
  state.wrapWidth = width;
49
47
  }
50
48
  //#endregion
51
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
49
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -1,5 +1,6 @@
1
1
  import { applyBoardDelta, applyBoardPatchToDocument, parseBoardDocument } from "../../protocol/dist/board-patch.js";
2
2
  import { createBoardEntityId } from "../model/items/id.js";
3
+ import { shareBoardDocument } from "../model/document-share.js";
3
4
  //#region src/board/replica/index.ts
4
5
  function memoryStorage() {
5
6
  return {
@@ -118,7 +119,9 @@ function createBoardReplica(options) {
118
119
  title = result.title;
119
120
  if (readPlaybackRevision === playbackRevision) playback = result.playback;
120
121
  loaded = true;
121
- adoptServer(parsed.document, result.version, true);
122
+ const next = shareBoardDocument(server, parsed.document);
123
+ if (next === server && result.version === version) return;
124
+ adoptServer(next, result.version, true);
122
125
  }
123
126
  function refresh() {
124
127
  if (refreshing) {
@@ -1,5 +1,6 @@
1
1
  import { ModelFormat } from "../../protocol/dist/fs/mime.js";
2
2
  import { BoardSceneItem } from "../model/scene.js";
3
+ import { BoardAssetUrlOptions } from "../model/media-playback.js";
3
4
  import "../model/index.js";
4
5
  import { LruBudget } from "./lru.js";
5
6
  import { BoardImageTexturePool } from "./texture-pool.js";
@@ -7,7 +8,12 @@ import { Texture } from "pixi.js";
7
8
  //#region src/board/stage/assets.d.ts
8
9
  type BoardUrlMedia = "image" | "video";
9
10
  type BoardAssetMedia = BoardUrlMedia | "model";
11
+ declare function boardTextureEdge(item: BoardSceneItem, scale: number): number;
10
12
  declare function boardAssetKey(item: BoardSceneItem): string | null;
13
+ type Size = {
14
+ width: number;
15
+ height: number;
16
+ };
11
17
  type BoardModelPreviewRequest = {
12
18
  key: string;
13
19
  src: string;
@@ -21,24 +27,22 @@ type BoardModelPreviews = {
21
27
  type BoardAssetManager = {
22
28
  assetKey: (item: BoardSceneItem) => string | null;
23
29
  invalidatePath: (path: string) => void;
24
- requestItem: (item: BoardSceneItem) => void;
30
+ requestItem: (item: BoardSceneItem, edge?: number) => void;
25
31
  getTexture: (key: string) => Texture | null;
26
- getNaturalSize: (key: string) => {
27
- width: number;
28
- height: number;
29
- } | null;
32
+ getNaturalSize: (key: string) => Size | null;
30
33
  hasError: (key: string) => boolean;
31
34
  acquire: (key: string) => void;
32
35
  release: (key: string) => void;
33
36
  withTextures: <T>(items: BoardSceneItem[], use: (textures: Map<string, Texture>) => T | Promise<T>, options?: {
34
37
  timeoutMs?: number;
38
+ edge?: (item: BoardSceneItem) => number;
35
39
  }) => Promise<T>;
36
- subscribe: (listener: () => void) => () => void;
40
+ subscribe: (listener: (key: string) => void) => () => void;
37
41
  destroy: () => void;
38
42
  };
39
43
  type BoardAssetManagerOptions = {
40
44
  /** Resolve a Space file path to a URL a texture can load from; null when unavailable. */
41
- resolveFileUrl: (path: string) => Promise<string | null>;
45
+ resolveFileUrl: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
42
46
  concurrency?: number;
43
47
  videoConcurrency?: number;
44
48
  loadVideoPreviews?: boolean;
@@ -51,4 +55,4 @@ type BoardAssetManagerOptions = {
51
55
  };
52
56
  declare function createBoardAssetManager(options: BoardAssetManagerOptions): BoardAssetManager;
53
57
  //#endregion
54
- export { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, createBoardAssetManager };
58
+ export { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, boardTextureEdge, createBoardAssetManager };
@@ -1,6 +1,6 @@
1
- import { mediaPreviewCandidates } from "../../media.js";
1
+ import { MEDIA_VARIANT_SIZES, imageVariantUrl, mediaVariantSize, probeMediaInfo, variantSource, videoFrameUrl } from "../../media.js";
2
2
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
3
- import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
+ import { featuredTaskArtifact, taskArtifactPreviewUrl } from "../../protocol/dist/board-task.js";
4
4
  import { boardModelFormat, isBoardModelItem } from "../model/media.js";
5
5
  import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
6
6
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
@@ -8,12 +8,23 @@ import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
8
8
  import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
9
9
  import { Texture } from "pixi.js";
10
10
  //#region src/board/stage/assets.ts
11
- const TASK_TEXTURE_SIZE = 1536;
12
11
  function taskPreviewKey(artifact) {
13
- if (!artifact || artifact.type === "text") return null;
14
- const [still] = mediaPreviewCandidates(artifact, { size: TASK_TEXTURE_SIZE });
15
- if (still) return `url:${still}`;
16
- return artifact.type === "video" ? `video-url:${encodeURIComponent(artifact.url)}` : null;
12
+ const cover = taskArtifactPreviewUrl(artifact);
13
+ if (cover) return `url:${cover}`;
14
+ if (artifact?.type !== "video") return null;
15
+ const frame = videoFrameUrl(artifact.url, 768);
16
+ return frame ? `url:${frame}` : `video-url:${encodeURIComponent(artifact.url)}`;
17
+ }
18
+ const LARGEST_VARIANT = MEDIA_VARIANT_SIZES[MEDIA_VARIANT_SIZES.length - 1];
19
+ function boardTextureEdge(item, scale) {
20
+ const crop = item.type === "image" ? item.props.crop : void 0;
21
+ const pixels = Math.max(item.frame.width / (crop?.w || 1), item.frame.height / (crop?.h || 1)) * scale;
22
+ return pixels > LARGEST_VARIANT ? Number.POSITIVE_INFINITY : mediaVariantSize(pixels);
23
+ }
24
+ function variantUrl(source, edge) {
25
+ if (!(edge <= LARGEST_VARIANT)) return null;
26
+ const url = imageVariantUrl(source, edge);
27
+ return url && url !== source ? url : null;
17
28
  }
18
29
  const STILL_MEDIA = ["video", "model"];
19
30
  function stillKey(media, src, mtimeMs) {
@@ -70,7 +81,7 @@ function footprintOf(texture) {
70
81
  return (texture.width || 0) * (texture.height || 0) * 4;
71
82
  }
72
83
  function createBoardAssetManager(options) {
73
- const concurrency = Math.max(1, options.concurrency ?? 4);
84
+ const concurrency = Math.max(1, options.concurrency ?? 6);
74
85
  const limits = {
75
86
  image: concurrency,
76
87
  video: Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency)),
@@ -103,12 +114,20 @@ function createBoardAssetManager(options) {
103
114
  });
104
115
  if (url) pendingUnloads.set(pendingKey, promise);
105
116
  }
117
+ const retiring = /* @__PURE__ */ new Set();
118
+ function retire(key, url, texture) {
119
+ const release = () => {
120
+ if (retiring.delete(release)) releaseTexture(key, url, texture);
121
+ };
122
+ retiring.add(release);
123
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
124
+ else setTimeout(release, 0);
125
+ }
106
126
  const resolveFileUrl = options.resolveFileUrl;
107
127
  const entries = /* @__PURE__ */ new Map();
108
128
  const pathVersions = /* @__PURE__ */ new Map();
109
- const loaders = /* @__PURE__ */ new Map();
129
+ const starters = /* @__PURE__ */ new Map();
110
130
  const failedModels = /* @__PURE__ */ new Set();
111
- const inflight = /* @__PURE__ */ new Set();
112
131
  const queue = [];
113
132
  const listeners = /* @__PURE__ */ new Set();
114
133
  let active = 0;
@@ -118,8 +137,8 @@ function createBoardAssetManager(options) {
118
137
  model: 0
119
138
  };
120
139
  let disposed = false;
121
- function notify() {
122
- for (const listener of listeners) listener();
140
+ function notify(key) {
141
+ for (const listener of listeners) listener(key);
123
142
  }
124
143
  function managedAssetKey(item) {
125
144
  const key = boardAssetKey(item);
@@ -141,8 +160,13 @@ function createBoardAssetManager(options) {
141
160
  let entry = entries.get(key);
142
161
  if (!entry) {
143
162
  entry = {
163
+ media: keySource(key)?.media ?? "image",
164
+ source: null,
144
165
  url: null,
145
166
  texture: null,
167
+ edge: 0,
168
+ want: 0,
169
+ natural: void 0,
146
170
  refs: 0,
147
171
  loading: false,
148
172
  error: false,
@@ -162,21 +186,15 @@ function createBoardAssetManager(options) {
162
186
  entry.retryTimer = null;
163
187
  if (disposed || entries.get(key) !== entry) return;
164
188
  if (entry.refs <= 0 || entry.texture || entry.loading) return;
165
- const load = loaders.get(key);
166
- if (!load) return;
189
+ const start = starters.get(key);
190
+ if (!start) return;
167
191
  entry.error = false;
168
- if (!queue.some((task) => task.key === key)) {
169
- queue.push({
170
- key,
171
- load
172
- });
173
- pump();
174
- }
192
+ start(entry);
175
193
  }, delay);
176
194
  }
177
195
  function evict(key, entry) {
178
196
  clearRetry(entry);
179
- loaders.delete(key);
197
+ starters.delete(key);
180
198
  if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
181
199
  entries.delete(key);
182
200
  }
@@ -193,69 +211,128 @@ function createBoardAssetManager(options) {
193
211
  if (entry) evict(key, entry);
194
212
  }
195
213
  }
196
- function settle(key, entry, texture, error) {
214
+ function adopt(key, entry, loaded, error) {
197
215
  entry.loading = false;
198
- inflight.delete(key);
199
- active -= 1;
200
- const media = keySource(key)?.media;
201
- if (media) activeByMedia[media] -= 1;
202
216
  if (entries.get(key) !== entry) {
203
- if (texture) releaseTexture(key, entry.url ?? "", texture);
204
- if (!disposed) pump();
217
+ if (loaded) releaseTexture(key, loaded.url, loaded.texture);
205
218
  return;
206
219
  }
207
- if (texture) {
220
+ if (loaded) {
221
+ const replaced = entry.texture;
222
+ const replacedUrl = entry.url ?? "";
223
+ const { texture } = loaded;
208
224
  entry.error = false;
209
225
  entry.attempts = 0;
210
226
  entry.texture = texture;
227
+ entry.url = loaded.url;
228
+ entry.edge = loaded.edge;
229
+ if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = videoTextureNaturalSize(texture) ?? {
230
+ width: texture.width,
231
+ height: texture.height
232
+ };
211
233
  entry.lastUsedAt = now();
212
234
  if (entry.refs <= 0) trim();
213
- else notify();
214
- } else {
215
- entry.error = true;
216
- entry.attempts += 1;
217
- entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
218
- console.warn(`[board] failed to load preview for ${key}`, error);
219
- if (media === "model") failedModels.add(key);
220
- if (entry.refs <= 0) evict(key, entry);
221
- else {
222
- if (media !== "model") scheduleRetry(key, entry);
223
- notify();
224
- }
235
+ else notify(key);
236
+ if (replaced) retire(key, replacedUrl, replaced);
237
+ refine(key, entry);
238
+ return;
225
239
  }
226
- if (!disposed) pump();
240
+ if (entry.texture) {
241
+ entry.edge = Number.POSITIVE_INFINITY;
242
+ notify(key);
243
+ return;
244
+ }
245
+ const media = entry.media;
246
+ entry.error = true;
247
+ entry.attempts += 1;
248
+ entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
249
+ console.warn(`[board] failed to load preview for ${key}`, error);
250
+ if (media === "model") failedModels.add(key);
251
+ if (entry.refs <= 0) evict(key, entry);
252
+ else {
253
+ if (media !== "model") scheduleRetry(key, entry);
254
+ notify(key);
255
+ }
256
+ }
257
+ function refinable(entry) {
258
+ return entry.media === "image" && entry.refs > 0 && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
259
+ }
260
+ function refine(key, entry) {
261
+ if (refinable(entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
262
+ }
263
+ function enqueue(key, media, load) {
264
+ if (queue.some((task) => task.key === key)) return;
265
+ queue.push({
266
+ key,
267
+ media,
268
+ load
269
+ });
270
+ pump();
227
271
  }
228
272
  function pump() {
229
273
  while (!disposed && active < concurrency && queue.length > 0) {
230
- const taskIndex = queue.findIndex(({ key }) => {
231
- const media = keySource(key)?.media;
232
- return !media || activeByMedia[media] < limits[media];
233
- });
274
+ const taskIndex = queue.findIndex(({ media }) => activeByMedia[media] < limits[media]);
234
275
  if (taskIndex < 0) return;
235
276
  const [task] = queue.splice(taskIndex, 1);
236
277
  if (!task) continue;
237
278
  const entry = entries.get(task.key);
238
- const source = keySource(task.key);
239
- if (!entry || !source || entry.texture || entry.loading) continue;
279
+ if (!entry || entry.loading || entry.texture && !refinable(entry)) continue;
240
280
  entry.loading = true;
241
- inflight.add(task.key);
242
281
  active += 1;
243
- activeByMedia[source.media] += 1;
244
- task.load(entry).then((texture) => settle(task.key, entry, texture), (error) => settle(task.key, entry, null, error));
282
+ activeByMedia[task.media] += 1;
283
+ const done = (loaded, error) => {
284
+ active -= 1;
285
+ activeByMedia[task.media] -= 1;
286
+ adopt(task.key, entry, loaded, error);
287
+ if (!disposed) pump();
288
+ };
289
+ task.load(entry).then((loaded) => done(loaded), (error) => done(null, error));
245
290
  }
246
291
  }
247
- function urlLoader(source) {
248
- const getUrl = source.kind === "url" ? () => Promise.resolve(source.value) : () => resolveFileUrl(source.value);
292
+ function textureLoad(media, source) {
249
293
  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}`);
294
+ entry.source = source;
295
+ const edge = entry.want;
296
+ const variant = media === "image" ? variantUrl(source, edge) : null;
297
+ const url = variant ?? source;
298
+ const pending = pendingUnloads.get(`${media}:${url}`);
254
299
  if (pending) await pending;
255
300
  if (disposed) return null;
256
- return loadTexture(url, source.media);
301
+ const texture = await loadTexture(url, media);
302
+ if (!texture) return null;
303
+ return {
304
+ texture,
305
+ url,
306
+ edge: !variant || Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge
307
+ };
308
+ };
309
+ }
310
+ function resolveFile(key, media, path) {
311
+ return (entry) => {
312
+ const fresh = entry.attempts > 0;
313
+ entry.loading = true;
314
+ resolveFileUrl(path, fresh ? { fresh } : void 0).then((url) => {
315
+ if (disposed || entries.get(key) !== entry) return;
316
+ if (entry.refs <= 0) return evict(key, entry);
317
+ if (!url) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
318
+ entry.loading = false;
319
+ enqueue(key, media, textureLoad(media, url));
320
+ }, (error) => {
321
+ if (!disposed) adopt(key, entry, null, error);
322
+ });
257
323
  };
258
324
  }
325
+ function probeNaturalSize(key, entry, source) {
326
+ entry.natural = null;
327
+ probeMediaInfo(source, { type: "image" }).then(({ width, height }) => {
328
+ if (disposed || entries.get(key) !== entry || !width || !height) return;
329
+ entry.natural = {
330
+ width,
331
+ height
332
+ };
333
+ notify(key);
334
+ });
335
+ }
259
336
  function modelPreviewable(item) {
260
337
  const previews = options.modelPreviews;
261
338
  if (!previews) return false;
@@ -274,20 +351,35 @@ function createBoardAssetManager(options) {
274
351
  format,
275
352
  size: snapshot?.size
276
353
  };
277
- return async () => Texture.from(await previews.load(request));
354
+ return async () => ({
355
+ texture: Texture.from(await previews.load(request)),
356
+ url: "",
357
+ edge: Number.POSITIVE_INFINITY
358
+ });
359
+ }
360
+ function starter(item, key, source) {
361
+ if (source.media === "model") {
362
+ const load = modelLoader(item, key);
363
+ return load ? () => enqueue(key, "model", load) : null;
364
+ }
365
+ const media = source.media;
366
+ if (source.kind === "file") return resolveFile(key, media, source.value);
367
+ const load = textureLoad(media, source.value);
368
+ return () => enqueue(key, media, load);
278
369
  }
279
370
  const manager = {
280
371
  assetKey: managedAssetKey,
281
372
  invalidatePath(path) {
282
373
  if (disposed) return;
283
- if (![...entries.keys()].some((key) => {
374
+ const stale = [...entries.keys()].filter((key) => {
284
375
  const source = keySource(key);
285
376
  return source?.kind === "file" && source.value === path;
286
- })) return;
377
+ });
378
+ if (stale.length === 0) return;
287
379
  pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
288
- notify();
380
+ for (const key of stale) notify(key);
289
381
  },
290
- requestItem(item) {
382
+ requestItem(item, edge = Number.POSITIVE_INFINITY) {
291
383
  if (disposed) return;
292
384
  const key = managedAssetKey(item);
293
385
  if (!key || failedModels.has(key)) return;
@@ -295,22 +387,16 @@ function createBoardAssetManager(options) {
295
387
  if (!source) return;
296
388
  const entry = ensureEntry(key);
297
389
  entry.lastUsedAt = now();
298
- if (entry.texture || entry.loading) return;
390
+ if (edge > entry.want) entry.want = edge;
391
+ if (entry.texture) return refine(key, entry);
392
+ if (entry.loading || entry.retryTimer) return;
299
393
  if (entry.error && now() < entry.retryAt) return;
300
- entry.error = false;
301
- clearRetry(entry);
302
394
  if (queue.some((queued) => queued.key === key)) return;
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);
309
- queue.push({
310
- key,
311
- load
312
- });
313
- pump();
395
+ const start = starter(item, key, source);
396
+ if (!start) return;
397
+ entry.error = false;
398
+ starters.set(key, start);
399
+ start(entry);
314
400
  },
315
401
  getTexture(key) {
316
402
  const entry = entries.get(key);
@@ -319,12 +405,10 @@ function createBoardAssetManager(options) {
319
405
  return entry.texture;
320
406
  },
321
407
  getNaturalSize(key) {
322
- const texture = manager.getTexture(key);
323
- if (!texture) return null;
324
- return videoTextureNaturalSize(texture) ?? {
325
- width: texture.width,
326
- height: texture.height
327
- };
408
+ const entry = entries.get(key);
409
+ if (!entry?.texture) return null;
410
+ if (entry.natural === void 0 && entry.source) probeNaturalSize(key, entry, entry.source);
411
+ return entry.natural ?? null;
328
412
  },
329
413
  hasError(key) {
330
414
  return entries.get(key)?.error ?? false;
@@ -355,10 +439,10 @@ function createBoardAssetManager(options) {
355
439
  if (keys.size === 0) return use(textures);
356
440
  for (const key of keys) manager.acquire(key);
357
441
  try {
358
- for (const item of items) manager.requestItem(item);
442
+ for (const item of items) manager.requestItem(item, loadOptions?.edge?.(item));
359
443
  const settled = () => [...keys].every((key) => {
360
444
  const entry = entries.get(key);
361
- return Boolean(entry?.texture) || Boolean(entry?.error);
445
+ return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(entry));
362
446
  });
363
447
  if (!settled()) {
364
448
  const timeoutMs = loadOptions?.timeoutMs ?? 15e3;
@@ -394,13 +478,14 @@ function createBoardAssetManager(options) {
394
478
  queue.length = 0;
395
479
  listeners.clear();
396
480
  pathVersions.clear();
397
- loaders.clear();
481
+ starters.clear();
398
482
  failedModels.clear();
399
483
  for (const [key, entry] of entries) evict(key, entry);
400
484
  entries.clear();
485
+ for (const release of [...retiring]) release();
401
486
  }
402
487
  };
403
488
  return manager;
404
489
  }
405
490
  //#endregion
406
- export { boardAssetKey, createBoardAssetManager };
491
+ export { boardAssetKey, boardTextureEdge, createBoardAssetManager };
@@ -0,0 +1,18 @@
1
+ //#region src/board/stage/canvas-size.d.ts
2
+ type BoardCanvasBox = {
3
+ width: number;
4
+ height: number;
5
+ density: number;
6
+ device: {
7
+ width: number;
8
+ height: number;
9
+ } | null;
10
+ };
11
+ declare function measureBoardCanvasBox(entry: ResizeObserverEntry, density: number): BoardCanvasBox;
12
+ declare function boardCanvasSize(box: BoardCanvasBox, density: number): {
13
+ width: number;
14
+ height: number;
15
+ resolution: number;
16
+ };
17
+ //#endregion
18
+ export { BoardCanvasBox, boardCanvasSize, measureBoardCanvasBox };