@neta-art/cohub 12.1.1 → 12.3.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 (134) hide show
  1. package/README.md +85 -8
  2. package/dist/board/editor/clipboard.js +1 -1
  3. package/dist/board/editor/creation.js +1 -1
  4. package/dist/board/editor/document.d.ts +3 -1
  5. package/dist/board/editor/document.js +8 -6
  6. package/dist/board/editor/editor.d.ts +8 -3
  7. package/dist/board/editor/editor.js +8 -2
  8. package/dist/board/editor/index.d.ts +3 -3
  9. package/dist/board/editor/index.js +2 -2
  10. package/dist/board/editor/interaction.d.ts +23 -2
  11. package/dist/board/editor/interaction.js +93 -4
  12. package/dist/board/editor/query.d.ts +1 -1
  13. package/dist/board/editor/query.js +3 -7
  14. package/dist/board/editor/selection.js +1 -1
  15. package/dist/board/editor/snapshots.js +5 -57
  16. package/dist/board/editor/tool.d.ts +4 -1
  17. package/dist/board/editor/tool.js +6 -1
  18. package/dist/board/export/index.d.ts +2 -2
  19. package/dist/board/export/index.js +1 -1
  20. package/dist/board/export/node/fonts.js +4 -0
  21. package/dist/board/export/node/index.d.ts +2 -0
  22. package/dist/board/export/node/index.js +8 -1
  23. package/dist/board/export/node/scripts.js +1 -1
  24. package/dist/board/export/scene.d.ts +2 -2
  25. package/dist/board/export/scene.js +2 -2
  26. package/dist/board/model/asset-key.d.ts +24 -0
  27. package/dist/board/model/asset-key.js +104 -0
  28. package/dist/board/model/export-assets.d.ts +7 -3
  29. package/dist/board/model/export-assets.js +34 -13
  30. package/dist/board/model/index.d.ts +9 -7
  31. package/dist/board/model/index.js +22 -20
  32. package/dist/board/model/items/create.js +3 -3
  33. package/dist/board/model/items/definition.d.ts +9 -1
  34. package/dist/board/model/items/index.d.ts +2 -2
  35. package/dist/board/model/items/registry.d.ts +2 -1
  36. package/dist/board/model/items/registry.js +1 -0
  37. package/dist/board/model/media-sizes.d.ts +28 -0
  38. package/dist/board/model/media-sizes.js +64 -0
  39. package/dist/board/model/palette.d.ts +1 -0
  40. package/dist/board/model/palette.js +20 -130
  41. package/dist/board/model/sampling.js +1 -1
  42. package/dist/board/model/text-metrics.d.ts +28 -3
  43. package/dist/board/model/text-metrics.js +36 -2
  44. package/dist/board/model/themes.d.ts +495 -0
  45. package/dist/board/model/themes.js +500 -0
  46. package/dist/board/render/card-tag.js +2 -2
  47. package/dist/board/render/index.d.ts +11 -7
  48. package/dist/board/render/index.js +9 -5
  49. package/dist/board/render/labels.d.ts +15 -0
  50. package/dist/board/render/labels.js +14 -0
  51. package/dist/board/render/lod.d.ts +30 -0
  52. package/dist/board/render/lod.js +33 -0
  53. package/dist/board/render/media-interaction.d.ts +5 -2
  54. package/dist/board/render/media-interaction.js +15 -3
  55. package/dist/board/render/palette.js +2 -25
  56. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  57. package/dist/board/render/renderers/audio-card-renderer.js +3 -2
  58. package/dist/board/render/renderers/board-renderer-registry.d.ts +22 -1
  59. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  60. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  61. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  62. package/dist/board/render/renderers/far-plate.d.ts +14 -1
  63. package/dist/board/render/renderers/far-plate.js +31 -8
  64. package/dist/board/render/renderers/file-card-renderer.js +49 -31
  65. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +34 -8
  67. package/dist/board/render/renderers/model-card-renderer.js +6 -6
  68. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  69. package/dist/board/render/renderers/task-card-renderer.js +9 -10
  70. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  71. package/dist/board/render/renderers/video-card-renderer.js +5 -4
  72. package/dist/board/render/script-export.js +1 -1
  73. package/dist/board/render/script-host.js +3 -3
  74. package/dist/board/render/style.d.ts +8 -2
  75. package/dist/board/render/style.js +8 -2
  76. package/dist/board/render/surface.d.ts +5 -0
  77. package/dist/board/render/surface.js +13 -3
  78. package/dist/board/render/text-edit.d.ts +32 -0
  79. package/dist/board/render/text-edit.js +134 -0
  80. package/dist/board/render/text-fit.d.ts +1 -3
  81. package/dist/board/render/text-fit.js +6 -49
  82. package/dist/board/render/text-measurement.d.ts +25 -1
  83. package/dist/board/render/text-measurement.js +157 -19
  84. package/dist/board/render/text-resolution.d.ts +1 -6
  85. package/dist/board/render/text-resolution.js +1 -10
  86. package/dist/board/render/theme.d.ts +15 -0
  87. package/dist/board/render/theme.js +15 -0
  88. package/dist/board/replica/index.d.ts +4 -2
  89. package/dist/board/replica/index.js +28 -17
  90. package/dist/board/replica/indexeddb.d.ts +16 -0
  91. package/dist/board/replica/indexeddb.js +155 -0
  92. package/dist/board/stage/assets.d.ts +28 -5
  93. package/dist/board/stage/assets.js +293 -102
  94. package/dist/board/stage/bench.d.ts +35 -0
  95. package/dist/board/stage/bench.js +153 -0
  96. package/dist/board/stage/device.d.ts +41 -0
  97. package/dist/board/stage/device.js +62 -0
  98. package/dist/board/stage/frame-budget.d.ts +19 -0
  99. package/dist/board/stage/frame-budget.js +69 -0
  100. package/dist/board/stage/idle.d.ts +4 -0
  101. package/dist/board/stage/idle.js +26 -0
  102. package/dist/board/stage/index.d.ts +11 -7
  103. package/dist/board/stage/index.js +7 -4
  104. package/dist/board/stage/lru.d.ts +13 -9
  105. package/dist/board/stage/lru.js +38 -20
  106. package/dist/board/stage/presses.d.ts +21 -0
  107. package/dist/board/stage/presses.js +66 -0
  108. package/dist/board/stage/scene.d.ts +15 -1
  109. package/dist/board/stage/scene.js +248 -141
  110. package/dist/board/stage/stage.d.ts +28 -2
  111. package/dist/board/stage/stage.js +404 -112
  112. package/dist/board/stage/texture-pool.d.ts +10 -1
  113. package/dist/board/stage/texture-pool.js +72 -2
  114. package/dist/chunks/environment.d.ts +6 -0
  115. package/dist/chunks/http.d.ts +9 -1
  116. package/dist/chunks/http.js +791 -4
  117. package/dist/index.d.ts +52 -1
  118. package/dist/index.js +202 -862
  119. package/dist/media.d.ts +3 -27
  120. package/dist/media.js +3 -82
  121. package/dist/protocol/dist/board-constants.js +3 -3
  122. package/dist/protocol/dist/board-layout.d.ts +17 -3
  123. package/dist/protocol/dist/board-layout.js +68 -11
  124. package/dist/protocol/dist/board-model.d.ts +15 -1
  125. package/dist/protocol/dist/board-model.js +12 -2
  126. package/dist/protocol/dist/board-patch.js +1 -1
  127. package/dist/protocol/dist/board-schema.d.ts +2 -1
  128. package/dist/protocol/dist/board-schema.js +4 -1
  129. package/dist/protocol/dist/core/media.d.ts +35 -1
  130. package/dist/protocol/dist/core/media.js +127 -1
  131. package/dist/protocol/dist/index.d.ts +3 -3
  132. package/package.json +4 -2
  133. package/dist/board/model/image-key.d.ts +0 -9
  134. package/dist/board/model/image-key.js +0 -33
@@ -1,97 +1,53 @@
1
- import { MEDIA_VARIANT_SIZES, imageVariantUrl, mediaVariantSize, probeMediaInfo, variantSource, videoFrameUrl } from "../../media.js";
2
- import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
3
- import { featuredTaskArtifact, taskArtifactPreviewUrl } from "../../protocol/dist/board-task.js";
4
- import { boardModelFormat, isBoardModelItem } from "../model/media.js";
5
- import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
1
+ import { probeMediaInfo } from "../../protocol/dist/core/media.js";
2
+ import { MEDIA_VARIANT_SIZES, imageVariantUrl, variantSource } from "../../media.js";
3
+ import { boardModelFormat } from "../model/media.js";
6
4
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
7
- import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
8
- import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
5
+ import { boardAssetKey, boardAssetKeySource, boardAssetRefKey } from "../model/asset-key.js";
6
+ import { planLruTrim } from "./lru.js";
7
+ import { defaultBoardDeviceBudget } from "./device.js";
8
+ import { scheduleBoardIdle } from "./idle.js";
9
+ import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
9
10
  import { Texture } from "pixi.js";
10
11
  //#region src/board/stage/assets.ts
11
- function taskPreviewKey(artifact) {
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);
12
+ function variantUrl(source, edge, cover) {
13
+ if (!(edge <= 1536)) return null;
14
+ const url = imageVariantUrl(source, edge, cover ? "cover" : "contain");
27
15
  return url && url !== source ? url : null;
28
16
  }
29
- const STILL_MEDIA = ["video", "model"];
30
- function stillKey(media, src, mtimeMs) {
31
- const source = parseBoardMediaSource(src);
32
- if (!source) return null;
33
- return source.kind === "url" ? `${media}-url:${encodeURIComponent(source.value)}` : `${media}:${encodeURIComponent(source.value)}:${mtimeMs ?? "unknown"}`;
17
+ const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
18
+ function boundEdge({ width, height }, cover) {
19
+ return cover ? Math.min(width, height) : Math.max(width, height);
34
20
  }
35
- function boardAssetKey(item) {
36
- if (item.type === "task") return taskPreviewKey(featuredTaskArtifact(item.props.snapshot.artifacts));
37
- if (item.type === "video") return stillKey("video", item.props.src, item.props.snapshot?.mtimeMs);
38
- if (item.type === "file" && isBoardModelItem(item)) return stillKey("model", item.props.src, item.props.snapshot?.mtimeMs);
39
- return imageAssetKey(item);
40
- }
41
- function keySource(key) {
42
- let baseKey = key;
43
- if (key.startsWith("local:")) {
44
- const separator = key.indexOf(":", 6);
45
- if (separator < 0) return null;
46
- baseKey = key.slice(separator + 1);
47
- }
48
- for (const media of STILL_MEDIA) {
49
- if (baseKey.startsWith(`${media}-url:`)) try {
50
- return {
51
- kind: "url",
52
- media,
53
- value: decodeURIComponent(baseKey.slice(media.length + 5))
54
- };
55
- } catch {
56
- return null;
57
- }
58
- if (baseKey.startsWith(`${media}:`)) {
59
- const separator = baseKey.lastIndexOf(":");
60
- if (separator <= media.length) return null;
61
- try {
62
- return {
63
- kind: "file",
64
- media,
65
- value: decodeURIComponent(baseKey.slice(media.length + 1, separator))
66
- };
67
- } catch {
68
- return null;
69
- }
70
- }
71
- }
72
- const image = boardImageKeySource(baseKey);
73
- return image ? {
74
- ...image,
75
- media: "image"
76
- } : null;
21
+ function sharpEdge(texture, edge, resized, cover) {
22
+ return resized && boundEdge(texture, cover) >= edge ? edge : Number.POSITIVE_INFINITY;
77
23
  }
78
24
  const MAX_RETRY_DELAY = 3e4;
25
+ const PREFETCH_CONCURRENCY = 2;
26
+ const PREFETCH_POOL_SHARE = .5;
27
+ const SHRINK_EDGE = 256;
28
+ const SHRINK_CONCURRENCY = 4;
29
+ const TRIM_DELAY = 400;
30
+ const TRIM_MAX_DELAY = 2e3;
31
+ const TONE_LIMIT = 8192;
79
32
  function footprintOf(texture) {
80
33
  if (!texture) return 0;
81
34
  return (texture.width || 0) * (texture.height || 0) * 4;
82
35
  }
83
36
  function createBoardAssetManager(options) {
84
- const concurrency = Math.max(1, options.concurrency ?? 6);
37
+ const concurrency = Math.max(1, options.concurrency ?? defaultBoardDeviceBudget().concurrency);
85
38
  const limits = {
86
39
  image: concurrency,
87
40
  video: Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency)),
88
41
  model: 1
89
42
  };
90
- const budget = options.lruBudget ?? DEFAULT_LRU_BUDGET;
43
+ const budget = options.lruBudget ?? defaultBoardDeviceBudget().lru;
44
+ const prefetchLimit = Math.min(PREFETCH_CONCURRENCY, concurrency - 1);
91
45
  const now = options.now ?? (() => Date.now());
92
46
  const imagePool = options.imageTexturePool ?? boardImageTexturePool;
93
47
  const useSharedImagePool = !options.loadTexture && !options.unloadTexture;
94
48
  const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
49
+ const shrinkTexture = options.shrinkTexture ?? (useSharedImagePool ? shrinkBoardTexture : null);
50
+ const readTone = options.readTone === void 0 ? typeof createImageBitmap === "function" ? readTextureTone : null : options.readTone;
95
51
  const pendingUnloads = /* @__PURE__ */ new Map();
96
52
  const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
97
53
  if (media !== "image" || !url) {
@@ -106,8 +62,7 @@ function createBoardAssetManager(options) {
106
62
  }
107
63
  await unloadBoardImageTexture(url, texture);
108
64
  });
109
- function releaseTexture(key, url, texture) {
110
- const media = keySource(key)?.media ?? "image";
65
+ function releaseTexture(media, url, texture) {
111
66
  const pendingKey = `${media}:${url}`;
112
67
  const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
113
68
  if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
@@ -115,9 +70,9 @@ function createBoardAssetManager(options) {
115
70
  if (url) pendingUnloads.set(pendingKey, promise);
116
71
  }
117
72
  const retiring = /* @__PURE__ */ new Set();
118
- function retire(key, url, texture) {
73
+ function retire(media, url, texture) {
119
74
  const release = () => {
120
- if (retiring.delete(release)) releaseTexture(key, url, texture);
75
+ if (retiring.delete(release)) releaseTexture(media, url, texture);
121
76
  };
122
77
  retiring.add(release);
123
78
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
@@ -137,13 +92,61 @@ function createBoardAssetManager(options) {
137
92
  model: 0
138
93
  };
139
94
  let disposed = false;
140
- function notify(key) {
141
- for (const listener of listeners) listener(key);
95
+ let focus = null;
96
+ let trimTimer = null;
97
+ let trimSince = 0;
98
+ let shrinking = 0;
99
+ let warm = /* @__PURE__ */ new Set();
100
+ let warming = 0;
101
+ const unwarmable = /* @__PURE__ */ new Set();
102
+ function isWarm(key) {
103
+ return warm.has(key) && (entries.get(key)?.refs ?? 0) <= 0;
104
+ }
105
+ function notify(key, change = "texture") {
106
+ for (const listener of listeners) listener(key, change);
107
+ }
108
+ const tones = /* @__PURE__ */ new Map();
109
+ const toneQueue = /* @__PURE__ */ new Set();
110
+ let cancelTone = null;
111
+ function queueTone(key) {
112
+ if (!readTone || tones.has(key)) return;
113
+ toneQueue.add(key);
114
+ cancelTone ??= scheduleBoardIdle(readNextTone, 0);
142
115
  }
116
+ async function readNextTone() {
117
+ const [key] = toneQueue;
118
+ if (key === void 0) {
119
+ cancelTone = null;
120
+ return;
121
+ }
122
+ toneQueue.delete(key);
123
+ const texture = entries.get(key)?.texture;
124
+ const tone = texture && readTone ? await readTone(texture).catch(() => null) : null;
125
+ if (disposed) return;
126
+ if (tone !== null) {
127
+ if (tones.size >= TONE_LIMIT) tones.delete(tones.keys().next().value);
128
+ tones.set(key, tone);
129
+ notify(key, "tone");
130
+ }
131
+ cancelTone = toneQueue.size > 0 ? scheduleBoardIdle(readNextTone, 0) : null;
132
+ }
133
+ const keys = /* @__PURE__ */ new WeakMap();
134
+ let keyEpoch = 0;
143
135
  function managedAssetKey(item) {
144
- const key = boardAssetKey(item);
136
+ const cached = keys.get(item);
137
+ if (cached?.epoch === keyEpoch) return cached.key;
138
+ const key = resolveAssetKey(item);
139
+ keys.set(item, {
140
+ epoch: keyEpoch,
141
+ key
142
+ });
143
+ return key;
144
+ }
145
+ function resolveAssetKey(item) {
146
+ const asset = options.itemAsset?.(item);
147
+ const key = asset === void 0 ? boardAssetKey(item) : asset && boardAssetRefKey(asset);
145
148
  if (!key) return null;
146
- const source = keySource(key);
149
+ const source = boardAssetKeySource(key);
147
150
  if (source?.media === "video" && options.loadVideoPreviews === false) return null;
148
151
  if (source?.media === "model" && !modelPreviewable(item)) return null;
149
152
  if (source?.kind !== "file") return key;
@@ -159,13 +162,16 @@ function createBoardAssetManager(options) {
159
162
  function ensureEntry(key) {
160
163
  let entry = entries.get(key);
161
164
  if (!entry) {
165
+ const source = boardAssetKeySource(key);
162
166
  entry = {
163
- media: keySource(key)?.media ?? "image",
167
+ media: source?.media ?? "image",
168
+ cover: source?.fit === "cover",
164
169
  source: null,
165
170
  url: null,
166
171
  texture: null,
167
172
  edge: 0,
168
173
  want: 0,
174
+ center: null,
169
175
  natural: void 0,
170
176
  refs: 0,
171
177
  loading: false,
@@ -195,28 +201,71 @@ function createBoardAssetManager(options) {
195
201
  function evict(key, entry) {
196
202
  clearRetry(entry);
197
203
  starters.delete(key);
198
- if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
204
+ if (entry.url || entry.texture) releaseTexture(entry.media, entry.url ?? "", entry.texture);
199
205
  entries.delete(key);
200
206
  }
207
+ function scheduleTrim() {
208
+ const at = now();
209
+ if (trimTimer === null) trimSince = at;
210
+ else if (at - trimSince >= TRIM_MAX_DELAY) return;
211
+ else clearTimeout(trimTimer);
212
+ trimTimer = setTimeout(() => {
213
+ trimTimer = null;
214
+ if (!disposed) trim();
215
+ }, TRIM_DELAY);
216
+ }
217
+ function shrunkFootprint({ media, texture }) {
218
+ if (!shrinkTexture || media !== "image" || !texture) return void 0;
219
+ const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
220
+ return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
221
+ }
201
222
  function trim() {
202
223
  const cooling = [];
203
224
  for (const [key, entry] of entries) if (entry.refs <= 0 && entry.texture && !entry.loading) cooling.push({
204
225
  key,
205
226
  lastUsedAt: entry.lastUsedAt,
206
- bytes: footprintOf(entry.texture)
227
+ bytes: footprintOf(entry.texture),
228
+ shrunkBytes: shrunkFootprint(entry)
207
229
  });
208
- const evictions = selectLruEvictions(cooling, budget);
209
- for (const key of evictions) {
230
+ const plan = planLruTrim(cooling, budget);
231
+ for (const key of plan.evict) {
210
232
  const entry = entries.get(key);
211
233
  if (entry) evict(key, entry);
212
234
  }
235
+ for (const key of plan.shrink.slice(0, Math.max(0, SHRINK_CONCURRENCY - shrinking))) {
236
+ const entry = entries.get(key);
237
+ if (entry) shrink(key, entry);
238
+ }
239
+ }
240
+ function shrink(key, entry) {
241
+ const { texture, url } = entry;
242
+ if (!shrinkTexture || !texture) return;
243
+ entry.loading = true;
244
+ shrinking += 1;
245
+ shrinkTexture(texture, SHRINK_EDGE).catch(() => null).then((small) => {
246
+ shrinking -= 1;
247
+ entry.loading = false;
248
+ const current = entries.get(key) === entry;
249
+ if (small && current && entry.refs <= 0) {
250
+ entry.texture = small;
251
+ entry.url = "";
252
+ entry.edge = SHRINK_EDGE;
253
+ releaseTexture(entry.media, url ?? "", texture);
254
+ } else {
255
+ if (small) releaseTexture(entry.media, "", small);
256
+ if (current && entry.refs > 0) refine(key, entry);
257
+ else if (current) evict(key, entry);
258
+ }
259
+ if (!disposed) trim();
260
+ });
213
261
  }
214
262
  function adopt(key, entry, loaded, error) {
215
263
  entry.loading = false;
216
264
  if (entries.get(key) !== entry) {
217
- if (loaded) releaseTexture(key, loaded.url, loaded.texture);
265
+ if (loaded) releaseTexture(entry.media, loaded.url, loaded.texture);
218
266
  return;
219
267
  }
268
+ if (warm.delete(key) && !loaded && !entry.texture && entry.refs <= 0) unwarmable.add(key);
220
269
  if (loaded) {
221
270
  const replaced = entry.texture;
222
271
  const replacedUrl = entry.url ?? "";
@@ -231,9 +280,10 @@ function createBoardAssetManager(options) {
231
280
  height: texture.height
232
281
  };
233
282
  entry.lastUsedAt = now();
234
- if (entry.refs <= 0) trim();
283
+ if (entry.refs <= 0) scheduleTrim();
235
284
  else notify(key);
236
- if (replaced) retire(key, replacedUrl, replaced);
285
+ if (replaced) retire(entry.media, replacedUrl, replaced);
286
+ queueTone(key);
237
287
  refine(key, entry);
238
288
  return;
239
289
  }
@@ -254,11 +304,11 @@ function createBoardAssetManager(options) {
254
304
  notify(key);
255
305
  }
256
306
  }
257
- function refinable(entry) {
258
- return entry.media === "image" && entry.refs > 0 && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
307
+ function refinable(key, entry) {
308
+ return entry.media === "image" && (entry.refs > 0 || warm.has(key)) && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
259
309
  }
260
310
  function refine(key, entry) {
261
- if (refinable(entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
311
+ if (refinable(key, entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
262
312
  }
263
313
  function enqueue(key, media, load) {
264
314
  if (queue.some((task) => task.key === key)) return;
@@ -269,20 +319,43 @@ function createBoardAssetManager(options) {
269
319
  });
270
320
  pump();
271
321
  }
322
+ function nextTask() {
323
+ let best = -1;
324
+ let bestWarm = true;
325
+ let nearest = Number.POSITIVE_INFINITY;
326
+ for (let index = 0; index < queue.length; index += 1) {
327
+ const task = queue[index];
328
+ if (activeByMedia[task.media] >= limits[task.media]) continue;
329
+ const prefetching = isWarm(task.key);
330
+ if (prefetching && (!bestWarm || warming >= prefetchLimit)) continue;
331
+ const center = entries.get(task.key)?.center;
332
+ const distance = focus && center ? (center.x - focus.x) ** 2 + (center.y - focus.y) ** 2 : 0;
333
+ if (best < 0 || bestWarm && !prefetching || distance < nearest) {
334
+ nearest = distance;
335
+ best = index;
336
+ bestWarm = prefetching;
337
+ if (!focus && !prefetching) return index;
338
+ }
339
+ }
340
+ return best;
341
+ }
272
342
  function pump() {
273
343
  while (!disposed && active < concurrency && queue.length > 0) {
274
- const taskIndex = queue.findIndex(({ media }) => activeByMedia[media] < limits[media]);
344
+ const taskIndex = nextTask();
275
345
  if (taskIndex < 0) return;
276
346
  const [task] = queue.splice(taskIndex, 1);
277
347
  if (!task) continue;
278
348
  const entry = entries.get(task.key);
279
- if (!entry || entry.loading || entry.texture && !refinable(entry)) continue;
349
+ if (!entry || entry.loading || entry.texture && !refinable(task.key, entry)) continue;
350
+ const prefetching = isWarm(task.key);
280
351
  entry.loading = true;
281
352
  active += 1;
282
353
  activeByMedia[task.media] += 1;
354
+ if (prefetching) warming += 1;
283
355
  const done = (loaded, error) => {
284
356
  active -= 1;
285
357
  activeByMedia[task.media] -= 1;
358
+ if (prefetching) warming -= 1;
286
359
  adopt(task.key, entry, loaded, error);
287
360
  if (!disposed) pump();
288
361
  };
@@ -292,18 +365,39 @@ function createBoardAssetManager(options) {
292
365
  function textureLoad(media, source) {
293
366
  return async (entry) => {
294
367
  entry.source = source;
295
- const edge = entry.want;
296
- const variant = media === "image" ? variantUrl(source, edge) : null;
368
+ const { cover, want: edge } = entry;
369
+ const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT), cover) : null;
297
370
  const url = variant ?? source;
298
371
  const pending = pendingUnloads.get(`${media}:${url}`);
299
372
  if (pending) await pending;
300
373
  if (disposed) return null;
374
+ if (media === "image" && !variant && useSharedImagePool && Number.isFinite(edge)) {
375
+ const fitted = await loadFittedImageTexture(url, edge, cover).catch(() => null);
376
+ if (fitted) return {
377
+ texture: fitted,
378
+ url: "",
379
+ edge: sharpEdge(fitted, edge, true, cover)
380
+ };
381
+ }
301
382
  const texture = await loadTexture(url, media);
302
383
  if (!texture) return null;
384
+ const bound = boundEdge(texture, cover);
385
+ if (media === "image" && shrinkTexture && edge < bound / 2) {
386
+ const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
387
+ const small = await shrinkTexture(texture, longEdge).catch(() => null);
388
+ if (small) {
389
+ releaseTexture(media, url, texture);
390
+ return {
391
+ texture: small,
392
+ url: "",
393
+ edge
394
+ };
395
+ }
396
+ }
303
397
  return {
304
398
  texture,
305
399
  url,
306
- edge: !variant || Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge
400
+ edge: sharpEdge(texture, edge, Boolean(variant), cover)
307
401
  };
308
402
  };
309
403
  }
@@ -313,7 +407,7 @@ function createBoardAssetManager(options) {
313
407
  entry.loading = true;
314
408
  resolveFileUrl(path, fresh ? { fresh } : void 0).then((url) => {
315
409
  if (disposed || entries.get(key) !== entry) return;
316
- if (entry.refs <= 0) return evict(key, entry);
410
+ if (entry.refs <= 0 && !warm.has(key)) return evict(key, entry);
317
411
  if (!url) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
318
412
  entry.loading = false;
319
413
  enqueue(key, media, textureLoad(media, url));
@@ -372,21 +466,29 @@ function createBoardAssetManager(options) {
372
466
  invalidatePath(path) {
373
467
  if (disposed) return;
374
468
  const stale = [...entries.keys()].filter((key) => {
375
- const source = keySource(key);
469
+ const source = boardAssetKeySource(key);
376
470
  return source?.kind === "file" && source.value === path;
377
471
  });
378
472
  if (stale.length === 0) return;
379
473
  pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
474
+ keyEpoch += 1;
380
475
  for (const key of stale) notify(key);
381
476
  },
477
+ invalidateItemAssets() {
478
+ if (options.itemAsset) keyEpoch += 1;
479
+ },
382
480
  requestItem(item, edge = Number.POSITIVE_INFINITY) {
383
481
  if (disposed) return;
384
482
  const key = managedAssetKey(item);
385
483
  if (!key || failedModels.has(key)) return;
386
- const source = keySource(key);
484
+ const source = boardAssetKeySource(key);
387
485
  if (!source) return;
388
486
  const entry = ensureEntry(key);
389
487
  entry.lastUsedAt = now();
488
+ entry.center = {
489
+ x: item.frame.x + item.frame.width / 2,
490
+ y: item.frame.y + item.frame.height / 2
491
+ };
390
492
  if (edge > entry.want) entry.want = edge;
391
493
  if (entry.texture) return refine(key, entry);
392
494
  if (entry.loading || entry.retryTimer) return;
@@ -413,8 +515,16 @@ function createBoardAssetManager(options) {
413
515
  hasError(key) {
414
516
  return entries.get(key)?.error ?? false;
415
517
  },
518
+ getTone(key) {
519
+ return tones.get(key) ?? null;
520
+ },
521
+ focus(point) {
522
+ focus = point;
523
+ },
416
524
  acquire(key) {
525
+ unwarmable.delete(key);
417
526
  const entry = ensureEntry(key);
527
+ if (entry.refs <= 0) entry.want = 0;
418
528
  entry.refs += 1;
419
529
  entry.lastUsedAt = now();
420
530
  },
@@ -427,7 +537,56 @@ function createBoardAssetManager(options) {
427
537
  clearRetry(entry);
428
538
  if (entry.loading) return;
429
539
  if (!entry.texture) evict(key, entry);
430
- else trim();
540
+ else scheduleTrim();
541
+ },
542
+ prefetch(requests) {
543
+ if (disposed) return;
544
+ const next = /* @__PURE__ */ new Set();
545
+ let room = budget.maxBytes * PREFETCH_POOL_SHARE;
546
+ for (const entry of entries.values()) if (entry.refs <= 0) room -= footprintOf(entry.texture);
547
+ for (const { item, edge } of requests) {
548
+ const key = managedAssetKey(item);
549
+ if (!key || !Number.isFinite(edge) || unwarmable.has(key) || boardAssetKeySource(key)?.media !== "image") continue;
550
+ const entry = entries.get(key);
551
+ const sharp = entry?.texture && entry.edge >= edge;
552
+ if (entry && (entry.refs > 0 || sharp || entry.error || entry.loading && !warm.has(key))) continue;
553
+ room -= edge * edge * 4;
554
+ if (room < 0) break;
555
+ next.add(key);
556
+ }
557
+ for (const key of warm) {
558
+ if (next.has(key)) continue;
559
+ const entry = entries.get(key);
560
+ if (!entry || entry.refs > 0 || entry.loading) continue;
561
+ const index = queue.findIndex((task) => task.key === key);
562
+ if (index >= 0) queue.splice(index, 1);
563
+ if (!entry.texture) evict(key, entry);
564
+ }
565
+ const fresh = new Set([...next].filter((key) => !warm.has(key)));
566
+ warm = next;
567
+ for (const { item, edge } of requests) {
568
+ const key = managedAssetKey(item);
569
+ if (!key || !next.has(key)) continue;
570
+ const entry = ensureEntry(key);
571
+ entry.want = edge;
572
+ entry.center = {
573
+ x: item.frame.x + item.frame.width / 2,
574
+ y: item.frame.y + item.frame.height / 2
575
+ };
576
+ if (!fresh.delete(key)) continue;
577
+ if (entry.texture) {
578
+ refine(key, entry);
579
+ continue;
580
+ }
581
+ const start = starter(item, key, boardAssetKeySource(key));
582
+ if (!start) {
583
+ warm.delete(key);
584
+ evict(key, entry);
585
+ continue;
586
+ }
587
+ starters.set(key, start);
588
+ start(entry);
589
+ }
431
590
  },
432
591
  async withTextures(items, use, loadOptions) {
433
592
  const keys = /* @__PURE__ */ new Set();
@@ -442,7 +601,7 @@ function createBoardAssetManager(options) {
442
601
  for (const item of items) manager.requestItem(item, loadOptions?.edge?.(item));
443
602
  const settled = () => [...keys].every((key) => {
444
603
  const entry = entries.get(key);
445
- return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(entry));
604
+ return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(key, entry));
446
605
  });
447
606
  if (!settled()) {
448
607
  const timeoutMs = loadOptions?.timeoutMs ?? 15e3;
@@ -467,6 +626,31 @@ function createBoardAssetManager(options) {
467
626
  for (const key of keys) manager.release(key);
468
627
  }
469
628
  },
629
+ stats() {
630
+ let textures = 0;
631
+ let cooling = 0;
632
+ let bytes = 0;
633
+ for (const entry of entries.values()) {
634
+ if (!entry.texture) continue;
635
+ textures += 1;
636
+ if (entry.refs <= 0) cooling += 1;
637
+ bytes += footprintOf(entry.texture);
638
+ }
639
+ let loading = active - warming;
640
+ let prefetching = warming;
641
+ for (const task of queue) {
642
+ if (!entries.has(task.key)) continue;
643
+ if (isWarm(task.key)) prefetching += 1;
644
+ else loading += 1;
645
+ }
646
+ return {
647
+ textures,
648
+ cooling,
649
+ bytes,
650
+ loading,
651
+ prefetching
652
+ };
653
+ },
470
654
  subscribe(listener) {
471
655
  listeners.add(listener);
472
656
  return () => {
@@ -475,7 +659,14 @@ function createBoardAssetManager(options) {
475
659
  },
476
660
  destroy() {
477
661
  disposed = true;
662
+ if (trimTimer !== null) clearTimeout(trimTimer);
663
+ trimTimer = null;
664
+ cancelTone?.();
665
+ toneQueue.clear();
666
+ tones.clear();
478
667
  queue.length = 0;
668
+ warm.clear();
669
+ unwarmable.clear();
479
670
  listeners.clear();
480
671
  pathVersions.clear();
481
672
  starters.clear();
@@ -488,4 +679,4 @@ function createBoardAssetManager(options) {
488
679
  return manager;
489
680
  }
490
681
  //#endregion
491
- export { boardAssetKey, boardTextureEdge, createBoardAssetManager };
682
+ export { createBoardAssetManager };
@@ -0,0 +1,35 @@
1
+ import { BoardStage } from "./stage.js";
2
+ //#region src/board/stage/bench.d.ts
3
+ type BoardBenchPhase = {
4
+ name: string;
5
+ frames: number;
6
+ wheel: Pick<WheelEventInit, "deltaX" | "deltaY" | "ctrlKey">;
7
+ rest?: number;
8
+ };
9
+ type BoardBenchPercentiles = {
10
+ p50: number;
11
+ p95: number;
12
+ max: number;
13
+ };
14
+ type BoardBenchSettle = {
15
+ shown: number;
16
+ ready: number;
17
+ loaded: number;
18
+ };
19
+ type BoardBenchPhaseReport = BoardBenchSettle & {
20
+ name: string;
21
+ frame: BoardBenchPercentiles;
22
+ work: BoardBenchPercentiles;
23
+ long: number;
24
+ };
25
+ type BoardBenchReport = {
26
+ open: BoardBenchSettle;
27
+ phases: BoardBenchPhaseReport[];
28
+ };
29
+ declare const BOARD_BENCH_PHASES: readonly BoardBenchPhase[];
30
+ declare function runBoardStageBench(stage: BoardStage, { since, phases }?: {
31
+ since?: number;
32
+ phases?: readonly BoardBenchPhase[];
33
+ }): Promise<BoardBenchReport>;
34
+ //#endregion
35
+ export { BOARD_BENCH_PHASES, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, runBoardStageBench };