@neta-art/cohub 12.6.0 → 13.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 (68) hide show
  1. package/README.md +19 -4
  2. package/dist/board/editor/editor.d.ts +7 -5
  3. package/dist/board/editor/index.js +1 -1
  4. package/dist/board/editor/interaction.js +1 -1
  5. package/dist/board/export/index.d.ts +1 -6
  6. package/dist/board/export/index.js +16 -10
  7. package/dist/board/export/node/index.d.ts +2 -7
  8. package/dist/board/export/node/index.js +2 -5
  9. package/dist/board/export/scene.d.ts +3 -7
  10. package/dist/board/export/scene.js +22 -26
  11. package/dist/board/model/asset-key.d.ts +5 -2
  12. package/dist/board/model/asset-key.js +13 -5
  13. package/dist/board/model/index.d.ts +5 -4
  14. package/dist/board/model/index.js +4 -3
  15. package/dist/board/model/media-playback.d.ts +3 -6
  16. package/dist/board/model/media-playback.js +4 -4
  17. package/dist/board/model/read.d.ts +7 -0
  18. package/dist/board/model/read.js +18 -0
  19. package/dist/board/model/themes.d.ts +50 -0
  20. package/dist/board/model/themes.js +44 -6
  21. package/dist/board/render/backdrop-layer.d.ts +21 -0
  22. package/dist/board/render/backdrop-layer.js +227 -0
  23. package/dist/board/render/backdrop.d.ts +45 -0
  24. package/dist/board/render/backdrop.js +62 -0
  25. package/dist/board/render/index.d.ts +3 -2
  26. package/dist/board/render/index.js +7 -6
  27. package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
  28. package/dist/board/render/renderers/audio-card-renderer.js +2 -2
  29. package/dist/board/render/renderers/file-card-renderer.js +1 -1
  30. package/dist/board/render/renderers/frame-card-renderer.js +1 -1
  31. package/dist/board/render/renderers/model-card-renderer.js +1 -1
  32. package/dist/board/render/renderers/shape-card-renderer.js +1 -1
  33. package/dist/board/render/renderers/task-card-renderer.js +2 -2
  34. package/dist/board/render/renderers/text-card-renderer.js +2 -2
  35. package/dist/board/render/renderers/video-card-renderer.js +1 -1
  36. package/dist/board/render/theme.d.ts +2 -0
  37. package/dist/board/render/theme.js +4 -2
  38. package/dist/board/replica/index.d.ts +2 -2
  39. package/dist/board/replica/index.js +38 -39
  40. package/dist/board/stage/assets.d.ts +2 -2
  41. package/dist/board/stage/assets.js +136 -65
  42. package/dist/board/stage/backdrop.d.ts +25 -0
  43. package/dist/board/stage/backdrop.js +104 -0
  44. package/dist/board/stage/index.d.ts +2 -1
  45. package/dist/board/stage/scene.js +1 -1
  46. package/dist/board/stage/stage.d.ts +7 -2
  47. package/dist/board/stage/stage.js +45 -25
  48. package/dist/board/stage/texture-pool.d.ts +1 -1
  49. package/dist/board/stage/texture-pool.js +4 -5
  50. package/dist/chunks/environment.d.ts +26 -15
  51. package/dist/chunks/http.d.ts +31 -23
  52. package/dist/chunks/http.js +1205 -46
  53. package/dist/chunks/websocket.d.ts +1 -1
  54. package/dist/http.d.ts +2 -2
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +1 -1077
  57. package/dist/media.d.ts +18 -32
  58. package/dist/media.js +81 -67
  59. package/dist/protocol/dist/board-model.d.ts +33 -26
  60. package/dist/protocol/dist/board-model.js +21 -18
  61. package/dist/protocol/dist/board.d.ts +2 -0
  62. package/dist/protocol/dist/board.js +5 -1
  63. package/dist/protocol/dist/core/media.d.ts +32 -9
  64. package/dist/protocol/dist/core/media.js +203 -48
  65. package/dist/protocol/dist/index.d.ts +2 -2
  66. package/package.json +1 -1
  67. package/dist/board/render/board-background.d.ts +0 -18
  68. package/dist/board/render/board-background.js +0 -104
@@ -1,10 +1,10 @@
1
1
  import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
2
  import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
3
- import { boardCardDetailed } from "../lod.js";
3
+ import { drawAudioWaveform } from "../audio-waveform.js";
4
4
  import { fitTextToLines, fitTextToWidth } from "../text-fit.js";
5
5
  import { syncTextResolution } from "../text-resolution.js";
6
6
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
7
- import { drawAudioWaveform } from "../audio-waveform.js";
7
+ import { boardCardDetailed } from "../lod.js";
8
8
  import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
9
9
  import { positionShell } from "./base-card-renderer.js";
10
10
  import { drawFarPlate, farPictureTone } from "./far-plate.js";
@@ -1,8 +1,8 @@
1
1
  import { estimateTextWidth, layoutBoardText } from "../../../protocol/dist/board-layout.js";
2
- import { itemAlpha, itemColor } from "../palette.js";
3
2
  import { boardTextStyle, canvasFont, cssBaselineShift, layoutTextLines } from "../text-measurement.js";
4
- import { boardTextSize } from "../lod.js";
5
3
  import { syncTextResolution } from "../text-resolution.js";
4
+ import { itemAlpha, itemColor } from "../palette.js";
5
+ import { boardTextSize } from "../lod.js";
6
6
  import { positionShell } from "./base-card-renderer.js";
7
7
  import { drawFarBars } from "./far-plate.js";
8
8
  import { Container, Graphics, Text } from "pixi.js";
@@ -1,6 +1,6 @@
1
1
  import { getMediaResourceTitle } from "../../model/media.js";
2
- import { boardCardDetailed } from "../lod.js";
3
2
  import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
3
+ import { boardCardDetailed } from "../lod.js";
4
4
  import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
5
5
  import { positionShell } from "./base-card-renderer.js";
6
6
  import { drawFarPlate, farPictureTone } from "./far-plate.js";
@@ -2,12 +2,14 @@ import { BoardShapeColors } from "../model/palette.js";
2
2
  import { BoardThemeId } from "../model/themes.js";
3
3
  import { BoardRenderStyle } from "./style.js";
4
4
  import { BoardRenderPalette } from "./renderers/board-renderer-registry.js";
5
+ import { BoardThemeBackdrop } from "./backdrop.js";
5
6
  //#region src/board/render/theme.d.ts
6
7
  type BoardRenderTheme = {
7
8
  colorScheme: "dark" | "light";
8
9
  palette: BoardRenderPalette;
9
10
  colors: BoardShapeColors;
10
11
  style: BoardRenderStyle;
12
+ backdrop?: BoardThemeBackdrop;
11
13
  };
12
14
  /** A built-in Cohub theme as the app draws it; spread it into export or sampler options. */
13
15
  declare function boardRenderTheme(id: BoardThemeId): BoardRenderTheme;
@@ -3,12 +3,14 @@ import { resolveBoardRenderStyle } from "./style.js";
3
3
  //#region src/board/render/theme.ts
4
4
  /** A built-in Cohub theme as the app draws it; spread it into export or sampler options. */
5
5
  function boardRenderTheme(id) {
6
- const { colorScheme, palette, colors, style } = BOARD_THEME_DATA[id];
6
+ const data = BOARD_THEME_DATA[id];
7
+ const { colorScheme, palette, colors, style } = data;
7
8
  return {
8
9
  colorScheme,
9
10
  palette: { ...palette },
10
11
  colors: Object.fromEntries(Object.entries(colors).map(([key, value]) => [key, { ...value }])),
11
- style: resolveBoardRenderStyle(colorScheme, style)
12
+ style: resolveBoardRenderStyle(colorScheme, style),
13
+ ..."backdrop" in data ? { backdrop: structuredClone(data.backdrop) } : {}
12
14
  };
13
15
  }
14
16
  //#endregion
@@ -14,8 +14,8 @@ type BoardChangedPayload = {
14
14
  after?: BoardDelta;
15
15
  };
16
16
  type BoardRemote = {
17
- /** `only: []` reads metadata only; remotes may return the full Board. */
18
- get(read?: Pick<BoardReadInput, "only">): Promise<BoardReadResult>;
17
+ /** `since` asks only for the changes after that version; remotes may return the full Board. */
18
+ get(read?: Pick<BoardReadInput, "since">): Promise<BoardReadResult>;
19
19
  apply(patch: BoardPatch, options: {
20
20
  mutationId: string;
21
21
  clientId?: string;
@@ -1,6 +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
+ import { readBoardDocument } from "../model/read.js";
4
4
  import { clearBoardReplicaStorage, createIndexedDbBoardReplicaStorage } from "./indexeddb.js";
5
5
  //#region src/board/replica/index.ts
6
6
  function memoryStorage() {
@@ -15,6 +15,7 @@ function memoryStorage() {
15
15
  const RETRY_BASE_MS = 1e3;
16
16
  const RETRY_MAX_MS = 3e4;
17
17
  const PERSIST_INTERVAL_MS = 500;
18
+ const CACHE_WAIT_MS = 250;
18
19
  function isRejection(error) {
19
20
  const status = error.status;
20
21
  return typeof status === "number" && status >= 400 && status < 500 && ![
@@ -34,6 +35,7 @@ function createBoardReplica(options) {
34
35
  let unsubscribeRemote = null;
35
36
  let server = null;
36
37
  let version = 0;
38
+ let sent = [];
37
39
  let title = null;
38
40
  let playback = null;
39
41
  let playbackRevision = 0;
@@ -57,7 +59,7 @@ function createBoardReplica(options) {
57
59
  return remote.subscribe({
58
60
  changed: (event) => receive(event.payload),
59
61
  playback: (event) => receivePlayback(event.payload.playback),
60
- resync: () => void resync()
62
+ resync: () => void retry()
61
63
  });
62
64
  } catch {
63
65
  return null;
@@ -69,7 +71,7 @@ function createBoardReplica(options) {
69
71
  return;
70
72
  }
71
73
  let next = server;
72
- for (const entry of pending) {
74
+ for (const entry of [...sent, ...pending]) {
73
75
  const result = applyBoardPatchToDocument(next, entry.patch);
74
76
  if (result.ok) next = result.document;
75
77
  }
@@ -95,6 +97,7 @@ function createBoardReplica(options) {
95
97
  function adoptServer(next, nextVersion, persist) {
96
98
  server = next;
97
99
  version = nextVersion;
100
+ sent = sent.filter((entry) => entry.version > nextVersion);
98
101
  recompute();
99
102
  if (persist) schedulePersist(nextVersion, next);
100
103
  }
@@ -112,21 +115,16 @@ function createBoardReplica(options) {
112
115
  unpersisted = null;
113
116
  if (entry) storage.writeDocument(entry.version, entry.document).catch(() => void 0);
114
117
  }
115
- function adoptRead(result, readPlaybackRevision) {
116
- const parsed = parseBoardDocument({
117
- board: result.board,
118
- items: result.items ?? {},
119
- animations: result.animations ?? {}
120
- });
121
- if (!parsed.ok) throw new Error(`Board ${result.id} could not be read.`);
118
+ function adoptRead(result, base, readPlaybackRevision) {
119
+ const next = readBoardDocument(result, result.changes ? base : server);
122
120
  title = result.title;
123
121
  if (readPlaybackRevision === playbackRevision) playback = result.playback;
124
122
  loaded = true;
125
- const next = shareBoardDocument(server, parsed.document);
126
123
  if (next === server && result.version === version) return;
127
124
  adoptServer(next, result.version, true);
128
125
  }
129
126
  function refresh() {
127
+ if (disposed) return Promise.resolve();
130
128
  if (refreshing) {
131
129
  refreshAgain = true;
132
130
  return refreshing;
@@ -136,9 +134,10 @@ function createBoardReplica(options) {
136
134
  do {
137
135
  refreshAgain = false;
138
136
  const readPlaybackRevision = playbackRevision;
139
- const result = await remote.get();
137
+ const base = server;
138
+ const result = await remote.get(base ? { since: version } : void 0);
140
139
  if (disposed) return;
141
- if (result.version >= version || !loaded) adoptRead(result, readPlaybackRevision);
140
+ if (result.version >= version || !loaded) adoptRead(result, base, readPlaybackRevision);
142
141
  readError = null;
143
142
  emit();
144
143
  } while (refreshAgain && !disposed);
@@ -162,6 +161,7 @@ function createBoardReplica(options) {
162
161
  async function flush() {
163
162
  if (sending || disposed || !loaded || !restored) return;
164
163
  sending = true;
164
+ let latest = 0;
165
165
  try {
166
166
  while (pending.length && !disposed) {
167
167
  const entry = pending[0];
@@ -173,12 +173,12 @@ function createBoardReplica(options) {
173
173
  const stillPending = pending.some((item) => item.mutationId === entry.mutationId);
174
174
  pending = pending.filter((item) => item.mutationId !== entry.mutationId);
175
175
  storage.deletePending(entry.mutationId).catch(() => void 0);
176
- if (stillPending && server && !result.replayed && result.status === "applied" && result.version === version + 1) {
177
- const next = applyBoardPatchToDocument(server, entry.patch);
178
- if (next.ok) adoptServer(next.document, result.version, true);
179
- else refresh();
180
- } else if (result.version > version) refresh();
181
- else recompute();
176
+ if (stillPending && result.status === "applied" && result.version > version) sent = [...sent, {
177
+ version: result.version,
178
+ patch: entry.patch
179
+ }];
180
+ latest = Math.max(latest, result.version);
181
+ recompute();
182
182
  retryDelay = RETRY_BASE_MS;
183
183
  writeError = null;
184
184
  emit();
@@ -198,6 +198,7 @@ function createBoardReplica(options) {
198
198
  return;
199
199
  }
200
200
  }
201
+ if (latest > version && !disposed) refresh();
201
202
  } finally {
202
203
  sending = false;
203
204
  }
@@ -220,7 +221,7 @@ function createBoardReplica(options) {
220
221
  }
221
222
  emit();
222
223
  }
223
- function retry(read = true) {
224
+ function retry() {
224
225
  if (disposed) return Promise.resolve();
225
226
  if (retryTimer) clearTimeout(retryTimer);
226
227
  retryTimer = null;
@@ -228,31 +229,24 @@ function createBoardReplica(options) {
228
229
  readError = null;
229
230
  writeError = null;
230
231
  emit();
231
- return (read ? refresh() : Promise.resolve()).then(flush);
232
- }
233
- async function resync() {
234
- await refreshing;
235
- if (disposed) return;
236
- if (!loaded || readError) return retry();
237
- const revision = playbackRevision;
238
- const head = await remote.get({ only: [] }).catch(() => null);
239
- if (disposed) return;
240
- if (!head || head.version > version) return retry();
241
- if (revision === playbackRevision) playback = head.playback;
242
- return retry(false);
232
+ return refresh().then(flush);
243
233
  }
244
234
  function onOnline() {
245
235
  if (pending.length || readError || writeError) retry();
246
236
  }
247
- async function restore() {
248
- const [cached, stored] = await Promise.all([storage.readDocument().catch(() => null), storage.listPending().catch(() => [])]);
237
+ async function restoreDocument() {
238
+ const cached = await storage.readDocument().catch(() => null);
239
+ if (disposed || server || !cached) return;
240
+ const parsed = parseBoardDocument(cached.document);
241
+ if (parsed.ok) adoptServer(parsed.document, cached.version, false);
242
+ }
243
+ async function restore(document) {
244
+ const [, stored] = await Promise.all([document, storage.listPending().catch(() => [])]);
249
245
  if (disposed) return;
250
246
  const known = new Set(stored.map((entry) => entry.mutationId));
251
247
  pending = [...stored, ...pending.filter((entry) => !known.has(entry.mutationId))];
252
248
  restored = true;
253
- const parsed = cached && !server ? parseBoardDocument(cached.document) : null;
254
- if (cached && parsed?.ok) adoptServer(parsed.document, cached.version, false);
255
- else recompute();
249
+ recompute();
256
250
  emit();
257
251
  }
258
252
  function receivePlayback(next) {
@@ -271,7 +265,12 @@ function createBoardReplica(options) {
271
265
  async start() {
272
266
  unsubscribeRemote ??= subscribeRemote();
273
267
  globalThis.addEventListener?.("online", onOnline);
274
- await Promise.all([restore(), refresh()]);
268
+ const document = restoreDocument();
269
+ const cacheWait = new Promise((resolve) => {
270
+ const timer = setTimeout(resolve, CACHE_WAIT_MS);
271
+ document.then(() => clearTimeout(timer));
272
+ });
273
+ await Promise.all([restore(document), Promise.race([document, cacheWait]).then(refresh)]);
275
274
  await flush();
276
275
  },
277
276
  apply(patch) {
@@ -287,7 +286,7 @@ function createBoardReplica(options) {
287
286
  receive,
288
287
  receivePlayback,
289
288
  refresh,
290
- retry: () => retry(),
289
+ retry,
291
290
  dispose() {
292
291
  disposed = true;
293
292
  globalThis.removeEventListener?.("online", onOnline);
@@ -1,7 +1,7 @@
1
1
  import { ModelFormat } from "../../protocol/dist/fs/mime.js";
2
2
  import { BoardSceneItem } from "../model/scene.js";
3
3
  import { BoardAssetMedia, BoardAssetRef } from "../model/asset-key.js";
4
- import { BoardAssetSource, BoardAssetUrlOptions } from "../model/media-playback.js";
4
+ import { BoardAssetSource } from "../model/media-playback.js";
5
5
  import "../model/index.js";
6
6
  import { LruBudget } from "./lru.js";
7
7
  import { BoardImageTexturePool } from "./texture-pool.js";
@@ -60,7 +60,7 @@ type BoardAssetPrefetch = {
60
60
  type BoardAssetChange = "texture" | "tone";
61
61
  type BoardAssetManagerOptions = {
62
62
  /** Resolve a Space file path to a URL a texture can load from; null when unavailable. */
63
- resolveFileUrl: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
63
+ resolveFile: BoardAssetSource["resolveFile"];
64
64
  subscribe?: BoardAssetSource["subscribe"];
65
65
  /** undefined keeps the built-in rule, null shows none; cached until `invalidateItemAssets`. */
66
66
  itemAsset?: (item: BoardSceneItem) => BoardAssetRef | null | undefined;
@@ -1,7 +1,7 @@
1
- import { probeMediaInfo } from "../../protocol/dist/core/media.js";
2
- import { MEDIA_VARIANT_SIZES, imageVariantUrl, variantSource } from "../../media.js";
1
+ import { MEDIA_VARIANT_SIZES, fitsVideoFrames, imageVariantUrl, mediaHost, mediaVariantSize, variantSource, videoFrameUrl } from "../../protocol/dist/core/media.js";
2
+ import { loadMediaInfo, loadMediaSize } from "../../media.js";
3
3
  import { boardModelFormat } from "../model/media.js";
4
- import { boardAssetKey, boardAssetKeySource, boardAssetRefKey } from "../model/asset-key.js";
4
+ import { boardAssetKey, boardAssetKeySource, boardAssetRefKey, boardMediaNaturalSize } from "../model/asset-key.js";
5
5
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
6
6
  import { planLruTrim } from "./lru.js";
7
7
  import { defaultBoardDeviceBudget } from "./device.js";
@@ -9,12 +9,43 @@ import { scheduleBoardIdle } from "./idle.js";
9
9
  import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
10
10
  import { Texture } from "pixi.js";
11
11
  //#region src/board/stage/assets.ts
12
+ const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
12
13
  function variantUrl(source, edge, cover) {
13
14
  if (!(edge <= 1536)) return null;
14
15
  const url = imageVariantUrl(source, edge, cover ? "cover" : "contain");
15
16
  return url && url !== source ? url : null;
16
17
  }
17
- const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
18
+ const frameAspects = /* @__PURE__ */ new Map();
19
+ const FRAME_ASPECT_LIMIT = 2048;
20
+ const FRAME_SLACK = 1.25;
21
+ function learnAspect(source, { width, height }) {
22
+ if (!width || !height || frameAspects.has(source)) return;
23
+ frameAspects.set(source, width / height);
24
+ if (frameAspects.size > FRAME_ASPECT_LIMIT) frameAspects.delete(frameAspects.keys().next().value);
25
+ }
26
+ const FRAME_FACTS_WAIT_MS = 300;
27
+ async function factSize(source) {
28
+ const timeout = new Promise((resolve) => setTimeout(resolve, FRAME_FACTS_WAIT_MS, null));
29
+ const info = await Promise.race([loadMediaInfo(source, "video"), timeout]);
30
+ return info?.width && info.height ? {
31
+ width: info.width,
32
+ height: info.height
33
+ } : void 0;
34
+ }
35
+ function frameUrl(source, edge, natural) {
36
+ return videoFrameUrl(source, mediaVariantSize(edge), {
37
+ natural,
38
+ aspect: natural ? null : frameAspects.get(source)
39
+ });
40
+ }
41
+ function textureMedia(media, url) {
42
+ return media === "video" && url && variantSource(url)?.type === "video" ? "image" : media;
43
+ }
44
+ function slotOf(media, source, frameless = false) {
45
+ return media === "video" && !frameless && videoFrameUrl(source, SMALLEST_VARIANT) ? "image" : media;
46
+ }
47
+ const FRAMELESS = /* @__PURE__ */ new Error("Video frame failed");
48
+ const NO_PREVIEW = /* @__PURE__ */ new Error("No preview without decoding the video");
18
49
  function boundEdge({ width, height }, cover) {
19
50
  return cover ? Math.min(width, height) : Math.max(width, height);
20
51
  }
@@ -62,7 +93,8 @@ function createBoardAssetManager(options) {
62
93
  }
63
94
  await unloadBoardImageTexture(url, texture);
64
95
  });
65
- function releaseTexture(media, url, texture) {
96
+ function releaseTexture(asset, url, texture) {
97
+ const media = textureMedia(asset, url);
66
98
  const pendingKey = `${media}:${url}`;
67
99
  const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
68
100
  if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
@@ -78,12 +110,11 @@ function createBoardAssetManager(options) {
78
110
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
79
111
  else setTimeout(release, 0);
80
112
  }
81
- const resolveFileUrl = options.resolveFileUrl;
82
113
  const entries = /* @__PURE__ */ new Map();
83
114
  const keysByPath = /* @__PURE__ */ new Map();
84
115
  const pathVersions = /* @__PURE__ */ new Map();
85
116
  const starters = /* @__PURE__ */ new Map();
86
- const failedModels = /* @__PURE__ */ new Set();
117
+ const unavailable = /* @__PURE__ */ new Set();
87
118
  const queue = [];
88
119
  const listeners = /* @__PURE__ */ new Set();
89
120
  let active = 0;
@@ -139,7 +170,7 @@ function createBoardAssetManager(options) {
139
170
  const itemEpoch = itemEpochs.get(item.id) ?? 0;
140
171
  let record = keys.get(item);
141
172
  if (record?.epoch !== keyEpoch || record.itemEpoch !== itemEpoch) {
142
- const base = resolveAssetKey(item);
173
+ const { key: base, natural } = resolveAsset(item);
143
174
  const source = base ? boardAssetKeySource(base) : null;
144
175
  const path = source?.kind === "file" ? source.value : null;
145
176
  record = {
@@ -148,7 +179,8 @@ function createBoardAssetManager(options) {
148
179
  base,
149
180
  path,
150
181
  version: 0,
151
- key: base
182
+ key: base,
183
+ natural
152
184
  };
153
185
  keys.set(item, record);
154
186
  }
@@ -161,14 +193,23 @@ function createBoardAssetManager(options) {
161
193
  }
162
194
  return record.key;
163
195
  }
164
- function resolveAssetKey(item) {
196
+ function resolveAsset(item) {
165
197
  const asset = options.itemAsset?.(item);
198
+ const natural = (asset === void 0 ? boardMediaNaturalSize(item) : asset?.natural) ?? null;
166
199
  const key = asset === void 0 ? boardAssetKey(item) : asset && boardAssetRefKey(asset);
167
- if (!key) return null;
168
- const source = boardAssetKeySource(key);
169
- if (source?.media === "video" && options.loadVideoPreviews === false) return null;
170
- if (source?.media === "model" && !modelPreviewable(item)) return null;
171
- return key;
200
+ const source = key ? boardAssetKeySource(key) : null;
201
+ if (source?.media === "video" && source.kind === "url" && options.loadVideoPreviews === false && slotOf("video", source.value) === "video") return {
202
+ key: null,
203
+ natural
204
+ };
205
+ if (source?.media === "model" && !modelPreviewable(item)) return {
206
+ key: null,
207
+ natural
208
+ };
209
+ return {
210
+ key,
211
+ natural
212
+ };
172
213
  }
173
214
  function clearRetry(entry) {
174
215
  if (entry.retryTimer) {
@@ -196,6 +237,7 @@ function createBoardAssetManager(options) {
196
237
  loading: false,
197
238
  error: false,
198
239
  attempts: 0,
240
+ frameless: false,
199
241
  retryAt: 0,
200
242
  retryTimer: null,
201
243
  lastUsedAt: now()
@@ -242,8 +284,8 @@ function createBoardAssetManager(options) {
242
284
  if (!disposed) trim();
243
285
  }, TRIM_DELAY);
244
286
  }
245
- function shrunkFootprint({ media, texture }) {
246
- if (!shrinkTexture || media !== "image" || !texture) return void 0;
287
+ function shrunkFootprint({ media, texture, url }) {
288
+ if (!shrinkTexture || textureMedia(media, url ?? "") !== "image" || !texture) return void 0;
247
289
  const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
248
290
  return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
249
291
  }
@@ -303,7 +345,9 @@ function createBoardAssetManager(options) {
303
345
  entry.texture = texture;
304
346
  entry.url = loaded.url;
305
347
  entry.edge = loaded.edge;
306
- if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = videoTextureNaturalSize(texture) ?? {
348
+ const decoded = videoTextureNaturalSize(texture);
349
+ if (decoded) entry.natural = decoded;
350
+ else if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = {
307
351
  width: texture.width,
308
352
  height: texture.height
309
353
  };
@@ -320,23 +364,27 @@ function createBoardAssetManager(options) {
320
364
  notify(key);
321
365
  return;
322
366
  }
323
- const media = entry.media;
367
+ if (error === FRAMELESS && entry.source && entry.refs > 0) return enqueue(key, "video", textureLoad("video", entry.source));
368
+ const final = entry.media === "model" || error === NO_PREVIEW;
324
369
  entry.error = true;
325
370
  entry.attempts += 1;
326
371
  entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
327
- console.warn(`[board] failed to load preview for ${key}`, error);
328
- if (media === "model") failedModels.add(key);
372
+ if (error !== NO_PREVIEW) console.warn(`[board] failed to load preview for ${key}`, error);
373
+ if (final) unavailable.add(key);
329
374
  if (entry.refs <= 0) evict(key, entry);
330
375
  else {
331
- if (media !== "model") scheduleRetry(key, entry);
376
+ if (!final) scheduleRetry(key, entry);
332
377
  notify(key);
333
378
  }
334
379
  }
335
380
  function refinable(key, entry) {
336
- return entry.media === "image" && (entry.refs > 0 || warm.has(key)) && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
381
+ if (entry.media === "model" || !(entry.refs > 0 || warm.has(key)) || !entry.texture || !entry.source || entry.want <= entry.edge) return false;
382
+ return entry.media === "image" || frameUrl(entry.source, entry.want, entry.natural) !== entry.url;
337
383
  }
338
384
  function refine(key, entry) {
339
- if (refinable(key, entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
385
+ if (entry.loading || !entry.source || entry.media === "model") return;
386
+ if (refinable(key, entry)) enqueue(key, slotOf(entry.media, entry.source, entry.frameless), textureLoad(entry.media, entry.source));
387
+ else if (entry.media === "video" && entry.natural === void 0 && entry.texture && boundEdge(entry.texture, false) < entry.want && !fitsVideoFrames(entry.source)) probeNaturalSize(key, entry, entry.source);
340
388
  }
341
389
  function enqueue(key, media, load) {
342
390
  if (queue.some((task) => task.key === key)) return;
@@ -390,55 +438,79 @@ function createBoardAssetManager(options) {
390
438
  task.load(entry).then((loaded) => done(loaded), (error) => done(null, error));
391
439
  }
392
440
  }
441
+ async function loadSized(entry, url, media, resized, slack = 2) {
442
+ const { cover, want: edge } = entry;
443
+ const pending = pendingUnloads.get(`${media}:${url}`);
444
+ if (pending) await pending;
445
+ if (disposed) return null;
446
+ const texture = await loadTexture(url, media);
447
+ if (!texture) return null;
448
+ const bound = boundEdge(texture, cover);
449
+ if (media === "image" && shrinkTexture && edge < bound / slack) {
450
+ const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
451
+ const small = await shrinkTexture(texture, longEdge).catch(() => null);
452
+ if (small) {
453
+ releaseTexture(media, url, texture);
454
+ return {
455
+ texture: small,
456
+ url: "",
457
+ edge
458
+ };
459
+ }
460
+ }
461
+ return {
462
+ texture,
463
+ url,
464
+ edge: sharpEdge(texture, edge, resized, cover)
465
+ };
466
+ }
393
467
  function textureLoad(media, source) {
394
468
  return async (entry) => {
395
469
  entry.source = source;
396
470
  const { cover, want: edge } = entry;
397
- const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT), cover) : null;
398
- const url = variant ?? source;
399
- const pending = pendingUnloads.get(`${media}:${url}`);
400
- if (pending) await pending;
401
- if (disposed) return null;
402
- if (media === "image" && !variant && useSharedImagePool && Number.isFinite(edge)) {
403
- const fitted = await loadFittedImageTexture(url, edge, cover).catch(() => null);
471
+ if (media === "video") {
472
+ if (entry.natural === void 0 && !entry.frameless && mediaHost(source)?.metaHeaders) entry.natural = await factSize(source);
473
+ const frame = entry.frameless ? null : frameUrl(source, edge, entry.natural);
474
+ if (frame) {
475
+ const loaded = await loadSized(entry, frame, "image", true, FRAME_SLACK).catch(() => null);
476
+ if (loaded) {
477
+ learnAspect(source, loaded.texture);
478
+ if (!entry.natural && !fitsVideoFrames(source) && loaded.edge === Number.POSITIVE_INFINITY) loaded.edge = boundEdge(loaded.texture, false);
479
+ }
480
+ if (loaded || entry.texture) return loaded;
481
+ entry.frameless = true;
482
+ throw FRAMELESS;
483
+ }
484
+ if (options.loadVideoPreviews === false) throw NO_PREVIEW;
485
+ return loadSized(entry, source, "video", false);
486
+ }
487
+ const variant = variantUrl(source, Math.max(edge, SMALLEST_VARIANT), cover);
488
+ if (!variant && useSharedImagePool && Number.isFinite(edge)) {
489
+ const fitted = await loadFittedImageTexture(source, edge, cover).catch(() => null);
404
490
  if (fitted) return {
405
491
  texture: fitted,
406
492
  url: "",
407
493
  edge: sharpEdge(fitted, edge, true, cover)
408
494
  };
409
495
  }
410
- const texture = await loadTexture(url, media);
411
- if (!texture) return null;
412
- const bound = boundEdge(texture, cover);
413
- if (media === "image" && shrinkTexture && edge < bound / 2) {
414
- const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
415
- const small = await shrinkTexture(texture, longEdge).catch(() => null);
416
- if (small) {
417
- releaseTexture(media, url, texture);
418
- return {
419
- texture: small,
420
- url: "",
421
- edge
422
- };
423
- }
424
- }
425
- return {
426
- texture,
427
- url,
428
- edge: sharpEdge(texture, edge, Boolean(variant), cover)
429
- };
496
+ return loadSized(entry, variant ?? source, "image", Boolean(variant));
430
497
  };
431
498
  }
432
- function resolveFile(key, media, path) {
499
+ function startFile(key, media, path) {
433
500
  return (entry) => {
434
501
  const fresh = entry.attempts > 0;
435
502
  entry.loading = true;
436
- resolveFileUrl(path, fresh ? { fresh } : void 0).then((url) => {
503
+ options.resolveFile(path, fresh ? { fresh } : void 0).then((file) => {
437
504
  if (disposed || entries.get(key) !== entry) return;
438
505
  if (entry.refs <= 0 && !warm.has(key)) return evict(key, entry);
439
- if (!url) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
506
+ if (!file) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
507
+ const { url, media: facts } = file;
508
+ if (facts && !entry.natural) entry.natural = {
509
+ width: facts.width,
510
+ height: facts.height
511
+ };
440
512
  entry.loading = false;
441
- enqueue(key, media, textureLoad(media, url));
513
+ enqueue(key, slotOf(media, url, entry.frameless), textureLoad(media, url));
442
514
  }, (error) => {
443
515
  if (!disposed) adopt(key, entry, null, error);
444
516
  });
@@ -446,12 +518,10 @@ function createBoardAssetManager(options) {
446
518
  }
447
519
  function probeNaturalSize(key, entry, source) {
448
520
  entry.natural = null;
449
- probeMediaInfo(source, { type: "image" }).then(({ width, height }) => {
450
- if (disposed || entries.get(key) !== entry || !width || !height) return;
451
- entry.natural = {
452
- width,
453
- height
454
- };
521
+ loadMediaSize(source, entry.media === "video" ? "video" : "image").then((size) => {
522
+ if (disposed || entries.get(key) !== entry || !size) return;
523
+ entry.natural = size;
524
+ refine(key, entry);
455
525
  notify(key);
456
526
  });
457
527
  }
@@ -485,9 +555,9 @@ function createBoardAssetManager(options) {
485
555
  return load ? () => enqueue(key, "model", load) : null;
486
556
  }
487
557
  const media = source.media;
488
- if (source.kind === "file") return resolveFile(key, media, source.value);
558
+ if (source.kind === "file") return startFile(key, media, source.value);
489
559
  const load = textureLoad(media, source.value);
490
- return () => enqueue(key, media, load);
560
+ return (entry) => enqueue(key, slotOf(media, source.value, entry.frameless), load);
491
561
  }
492
562
  const manager = {
493
563
  assetKey: managedAssetKey,
@@ -509,11 +579,12 @@ function createBoardAssetManager(options) {
509
579
  requestItem(item, edge = Number.POSITIVE_INFINITY) {
510
580
  if (disposed) return;
511
581
  const key = managedAssetKey(item);
512
- if (!key || failedModels.has(key)) return;
582
+ if (!key || unavailable.has(key)) return;
513
583
  const source = boardAssetKeySource(key);
514
584
  if (!source) return;
515
585
  const entry = ensureEntry(key);
516
586
  entry.lastUsedAt = now();
587
+ if (!entry.natural) entry.natural = keys.get(item)?.natural ?? entry.natural;
517
588
  entry.center = {
518
589
  x: item.frame.x + item.frame.width / 2,
519
590
  y: item.frame.y + item.frame.height / 2
@@ -701,7 +772,7 @@ function createBoardAssetManager(options) {
701
772
  pathVersions.clear();
702
773
  itemEpochs.clear();
703
774
  starters.clear();
704
- failedModels.clear();
775
+ unavailable.clear();
705
776
  for (const [key, entry] of entries) evict(key, entry);
706
777
  entries.clear();
707
778
  for (const release of [...retiring]) release();