@neta-art/cohub 12.1.0 → 12.2.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 (85) hide show
  1. package/README.md +8 -5
  2. package/dist/board/editor/editor.d.ts +1 -0
  3. package/dist/board/editor/editor.js +4 -0
  4. package/dist/board/editor/snapshots.js +5 -57
  5. package/dist/board/export/index.d.ts +2 -2
  6. package/dist/board/export/node/fonts.js +4 -0
  7. package/dist/board/export/node/index.d.ts +3 -0
  8. package/dist/board/export/node/index.js +13 -1
  9. package/dist/board/export/scene.d.ts +2 -2
  10. package/dist/board/export/scene.js +2 -2
  11. package/dist/board/model/asset-key.d.ts +16 -0
  12. package/dist/board/model/asset-key.js +92 -0
  13. package/dist/board/model/export-assets.d.ts +7 -3
  14. package/dist/board/model/export-assets.js +34 -13
  15. package/dist/board/model/index.d.ts +7 -5
  16. package/dist/board/model/index.js +9 -7
  17. package/dist/board/model/items/create.js +1 -1
  18. package/dist/board/model/media-sizes.d.ts +28 -0
  19. package/dist/board/model/media-sizes.js +64 -0
  20. package/dist/board/model/palette.d.ts +1 -0
  21. package/dist/board/model/palette.js +20 -130
  22. package/dist/board/model/text-metrics.d.ts +8 -3
  23. package/dist/board/model/text-metrics.js +13 -2
  24. package/dist/board/model/themes.d.ts +488 -0
  25. package/dist/board/model/themes.js +495 -0
  26. package/dist/board/render/card-tag.js +2 -2
  27. package/dist/board/render/index.d.ts +6 -4
  28. package/dist/board/render/index.js +5 -3
  29. package/dist/board/render/lod.d.ts +22 -0
  30. package/dist/board/render/lod.js +27 -0
  31. package/dist/board/render/media-interaction.js +1 -1
  32. package/dist/board/render/palette.js +2 -25
  33. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  34. package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -0
  35. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  36. package/dist/board/render/renderers/far-plate.d.ts +11 -1
  37. package/dist/board/render/renderers/far-plate.js +27 -8
  38. package/dist/board/render/renderers/file-card-renderer.js +3 -5
  39. package/dist/board/render/renderers/image-card-renderer.js +32 -6
  40. package/dist/board/render/renderers/model-card-renderer.js +1 -1
  41. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  42. package/dist/board/render/renderers/task-card-renderer.js +7 -8
  43. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  44. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  45. package/dist/board/render/style.d.ts +7 -2
  46. package/dist/board/render/style.js +5 -1
  47. package/dist/board/render/surface.d.ts +5 -0
  48. package/dist/board/render/surface.js +13 -3
  49. package/dist/board/render/text-fit.js +8 -12
  50. package/dist/board/render/text-measurement.d.ts +15 -1
  51. package/dist/board/render/text-measurement.js +113 -20
  52. package/dist/board/render/text-resolution.d.ts +1 -6
  53. package/dist/board/render/text-resolution.js +1 -10
  54. package/dist/board/render/theme.d.ts +15 -0
  55. package/dist/board/render/theme.js +15 -0
  56. package/dist/board/stage/assets.d.ts +15 -4
  57. package/dist/board/stage/assets.js +155 -89
  58. package/dist/board/stage/index.d.ts +5 -5
  59. package/dist/board/stage/index.js +4 -4
  60. package/dist/board/stage/lru.d.ts +13 -9
  61. package/dist/board/stage/lru.js +38 -20
  62. package/dist/board/stage/scene.d.ts +11 -1
  63. package/dist/board/stage/scene.js +213 -138
  64. package/dist/board/stage/stage.d.ts +12 -1
  65. package/dist/board/stage/stage.js +97 -24
  66. package/dist/board/stage/texture-pool.d.ts +8 -1
  67. package/dist/board/stage/texture-pool.js +37 -1
  68. package/dist/chunks/app-realtime.js +1 -1
  69. package/dist/chunks/{transport.js → http-error.js} +37 -1
  70. package/dist/chunks/http.js +30 -10
  71. package/dist/http.js +1 -1
  72. package/dist/index.js +139 -131
  73. package/dist/media.d.ts +3 -27
  74. package/dist/media.js +3 -82
  75. package/dist/protocol/dist/board-constants.js +3 -3
  76. package/dist/protocol/dist/board-layout.d.ts +17 -3
  77. package/dist/protocol/dist/board-layout.js +68 -11
  78. package/dist/protocol/dist/core/media.d.ts +35 -1
  79. package/dist/protocol/dist/core/media.js +123 -1
  80. package/dist/protocol/dist/index.d.ts +2 -2
  81. package/dist/websocket.js +951 -1
  82. package/package.json +3 -2
  83. package/dist/board/model/image-key.d.ts +0 -9
  84. package/dist/board/model/image-key.js +0 -33
  85. package/dist/chunks/websocket.js +0 -988
@@ -1,81 +1,26 @@
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";
4
+ import { boardAssetKey, boardAssetKeySource } from "../model/asset-key.js";
6
5
  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";
6
+ import { DEFAULT_LRU_BUDGET, planLruTrim } from "./lru.js";
7
+ import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
9
8
  import { Texture } from "pixi.js";
10
9
  //#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
10
  function variantUrl(source, edge) {
25
- if (!(edge <= LARGEST_VARIANT)) return null;
11
+ if (!(edge <= 1536)) return null;
26
12
  const url = imageVariantUrl(source, edge);
27
13
  return url && url !== source ? url : null;
28
14
  }
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"}`;
34
- }
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;
15
+ const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
16
+ function sharpEdge(texture, edge, resized) {
17
+ return resized && Math.max(texture.width, texture.height) >= edge ? edge : Number.POSITIVE_INFINITY;
77
18
  }
78
19
  const MAX_RETRY_DELAY = 3e4;
20
+ const SHRINK_EDGE = 256;
21
+ const SHRINK_CONCURRENCY = 4;
22
+ const TRIM_DELAY = 400;
23
+ const TRIM_MAX_DELAY = 2e3;
79
24
  function footprintOf(texture) {
80
25
  if (!texture) return 0;
81
26
  return (texture.width || 0) * (texture.height || 0) * 4;
@@ -92,6 +37,7 @@ function createBoardAssetManager(options) {
92
37
  const imagePool = options.imageTexturePool ?? boardImageTexturePool;
93
38
  const useSharedImagePool = !options.loadTexture && !options.unloadTexture;
94
39
  const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
40
+ const shrinkTexture = options.shrinkTexture ?? (useSharedImagePool ? shrinkBoardTexture : null);
95
41
  const pendingUnloads = /* @__PURE__ */ new Map();
96
42
  const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
97
43
  if (media !== "image" || !url) {
@@ -106,8 +52,7 @@ function createBoardAssetManager(options) {
106
52
  }
107
53
  await unloadBoardImageTexture(url, texture);
108
54
  });
109
- function releaseTexture(key, url, texture) {
110
- const media = keySource(key)?.media ?? "image";
55
+ function releaseTexture(media, url, texture) {
111
56
  const pendingKey = `${media}:${url}`;
112
57
  const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
113
58
  if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
@@ -115,9 +60,9 @@ function createBoardAssetManager(options) {
115
60
  if (url) pendingUnloads.set(pendingKey, promise);
116
61
  }
117
62
  const retiring = /* @__PURE__ */ new Set();
118
- function retire(key, url, texture) {
63
+ function retire(media, url, texture) {
119
64
  const release = () => {
120
- if (retiring.delete(release)) releaseTexture(key, url, texture);
65
+ if (retiring.delete(release)) releaseTexture(media, url, texture);
121
66
  };
122
67
  retiring.add(release);
123
68
  if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
@@ -137,13 +82,29 @@ function createBoardAssetManager(options) {
137
82
  model: 0
138
83
  };
139
84
  let disposed = false;
85
+ let focus = null;
86
+ let trimTimer = null;
87
+ let trimSince = 0;
88
+ let shrinking = 0;
140
89
  function notify(key) {
141
90
  for (const listener of listeners) listener(key);
142
91
  }
92
+ const keys = /* @__PURE__ */ new WeakMap();
93
+ let keyEpoch = 0;
143
94
  function managedAssetKey(item) {
95
+ const cached = keys.get(item);
96
+ if (cached?.epoch === keyEpoch) return cached.key;
97
+ const key = resolveAssetKey(item);
98
+ keys.set(item, {
99
+ epoch: keyEpoch,
100
+ key
101
+ });
102
+ return key;
103
+ }
104
+ function resolveAssetKey(item) {
144
105
  const key = boardAssetKey(item);
145
106
  if (!key) return null;
146
- const source = keySource(key);
107
+ const source = boardAssetKeySource(key);
147
108
  if (source?.media === "video" && options.loadVideoPreviews === false) return null;
148
109
  if (source?.media === "model" && !modelPreviewable(item)) return null;
149
110
  if (source?.kind !== "file") return key;
@@ -160,12 +121,13 @@ function createBoardAssetManager(options) {
160
121
  let entry = entries.get(key);
161
122
  if (!entry) {
162
123
  entry = {
163
- media: keySource(key)?.media ?? "image",
124
+ media: boardAssetKeySource(key)?.media ?? "image",
164
125
  source: null,
165
126
  url: null,
166
127
  texture: null,
167
128
  edge: 0,
168
129
  want: 0,
130
+ center: null,
169
131
  natural: void 0,
170
132
  refs: 0,
171
133
  loading: false,
@@ -195,26 +157,68 @@ function createBoardAssetManager(options) {
195
157
  function evict(key, entry) {
196
158
  clearRetry(entry);
197
159
  starters.delete(key);
198
- if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
160
+ if (entry.url || entry.texture) releaseTexture(entry.media, entry.url ?? "", entry.texture);
199
161
  entries.delete(key);
200
162
  }
163
+ function scheduleTrim() {
164
+ const at = now();
165
+ if (trimTimer === null) trimSince = at;
166
+ else if (at - trimSince >= TRIM_MAX_DELAY) return;
167
+ else clearTimeout(trimTimer);
168
+ trimTimer = setTimeout(() => {
169
+ trimTimer = null;
170
+ if (!disposed) trim();
171
+ }, TRIM_DELAY);
172
+ }
173
+ function shrunkFootprint({ media, texture }) {
174
+ if (!shrinkTexture || media !== "image" || !texture) return void 0;
175
+ const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
176
+ return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
177
+ }
201
178
  function trim() {
202
179
  const cooling = [];
203
180
  for (const [key, entry] of entries) if (entry.refs <= 0 && entry.texture && !entry.loading) cooling.push({
204
181
  key,
205
182
  lastUsedAt: entry.lastUsedAt,
206
- bytes: footprintOf(entry.texture)
183
+ bytes: footprintOf(entry.texture),
184
+ shrunkBytes: shrunkFootprint(entry)
207
185
  });
208
- const evictions = selectLruEvictions(cooling, budget);
209
- for (const key of evictions) {
186
+ const plan = planLruTrim(cooling, budget);
187
+ for (const key of plan.evict) {
210
188
  const entry = entries.get(key);
211
189
  if (entry) evict(key, entry);
212
190
  }
191
+ for (const key of plan.shrink.slice(0, Math.max(0, SHRINK_CONCURRENCY - shrinking))) {
192
+ const entry = entries.get(key);
193
+ if (entry) shrink(key, entry);
194
+ }
195
+ }
196
+ function shrink(key, entry) {
197
+ const { texture, url } = entry;
198
+ if (!shrinkTexture || !texture) return;
199
+ entry.loading = true;
200
+ shrinking += 1;
201
+ shrinkTexture(texture, SHRINK_EDGE).catch(() => null).then((small) => {
202
+ shrinking -= 1;
203
+ entry.loading = false;
204
+ const current = entries.get(key) === entry;
205
+ if (small && current && entry.refs <= 0) {
206
+ entry.texture = small;
207
+ entry.url = "";
208
+ entry.edge = SHRINK_EDGE;
209
+ releaseTexture(entry.media, url ?? "", texture);
210
+ } else {
211
+ if (small) releaseTexture(entry.media, "", small);
212
+ if (current && entry.refs > 0) refine(key, entry);
213
+ else if (current) evict(key, entry);
214
+ }
215
+ if (!disposed) trim();
216
+ });
213
217
  }
214
218
  function adopt(key, entry, loaded, error) {
215
219
  entry.loading = false;
216
220
  if (entries.get(key) !== entry) {
217
- if (loaded) releaseTexture(key, loaded.url, loaded.texture);
221
+ if (loaded) releaseTexture(entry.media, loaded.url, loaded.texture);
218
222
  return;
219
223
  }
220
224
  if (loaded) {
@@ -231,9 +235,9 @@ function createBoardAssetManager(options) {
231
235
  height: texture.height
232
236
  };
233
237
  entry.lastUsedAt = now();
234
- if (entry.refs <= 0) trim();
238
+ if (entry.refs <= 0) scheduleTrim();
235
239
  else notify(key);
236
- if (replaced) retire(key, replacedUrl, replaced);
240
+ if (replaced) retire(entry.media, replacedUrl, replaced);
237
241
  refine(key, entry);
238
242
  return;
239
243
  }
@@ -269,9 +273,25 @@ function createBoardAssetManager(options) {
269
273
  });
270
274
  pump();
271
275
  }
276
+ function nextTask() {
277
+ let best = -1;
278
+ let nearest = Number.POSITIVE_INFINITY;
279
+ for (let index = 0; index < queue.length; index += 1) {
280
+ const task = queue[index];
281
+ if (activeByMedia[task.media] >= limits[task.media]) continue;
282
+ const center = entries.get(task.key)?.center;
283
+ if (!focus || !center) return index;
284
+ const distance = (center.x - focus.x) ** 2 + (center.y - focus.y) ** 2;
285
+ if (distance < nearest) {
286
+ nearest = distance;
287
+ best = index;
288
+ }
289
+ }
290
+ return best;
291
+ }
272
292
  function pump() {
273
293
  while (!disposed && active < concurrency && queue.length > 0) {
274
- const taskIndex = queue.findIndex(({ media }) => activeByMedia[media] < limits[media]);
294
+ const taskIndex = nextTask();
275
295
  if (taskIndex < 0) return;
276
296
  const [task] = queue.splice(taskIndex, 1);
277
297
  if (!task) continue;
@@ -293,17 +313,36 @@ function createBoardAssetManager(options) {
293
313
  return async (entry) => {
294
314
  entry.source = source;
295
315
  const edge = entry.want;
296
- const variant = media === "image" ? variantUrl(source, edge) : null;
316
+ const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT)) : null;
297
317
  const url = variant ?? source;
298
318
  const pending = pendingUnloads.get(`${media}:${url}`);
299
319
  if (pending) await pending;
300
320
  if (disposed) return null;
321
+ if (media === "image" && !variant && useSharedImagePool && Number.isFinite(edge)) {
322
+ const fitted = await loadFittedImageTexture(url, edge).catch(() => null);
323
+ if (fitted) return {
324
+ texture: fitted,
325
+ url: "",
326
+ edge: sharpEdge(fitted, edge, true)
327
+ };
328
+ }
301
329
  const texture = await loadTexture(url, media);
302
330
  if (!texture) return null;
331
+ if (media === "image" && shrinkTexture && edge < Math.max(texture.width, texture.height) / 2) {
332
+ const small = await shrinkTexture(texture, edge).catch(() => null);
333
+ if (small) {
334
+ releaseTexture(media, url, texture);
335
+ return {
336
+ texture: small,
337
+ url: "",
338
+ edge
339
+ };
340
+ }
341
+ }
303
342
  return {
304
343
  texture,
305
344
  url,
306
- edge: !variant || Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge
345
+ edge: sharpEdge(texture, edge, Boolean(variant))
307
346
  };
308
347
  };
309
348
  }
@@ -372,21 +411,26 @@ function createBoardAssetManager(options) {
372
411
  invalidatePath(path) {
373
412
  if (disposed) return;
374
413
  const stale = [...entries.keys()].filter((key) => {
375
- const source = keySource(key);
414
+ const source = boardAssetKeySource(key);
376
415
  return source?.kind === "file" && source.value === path;
377
416
  });
378
417
  if (stale.length === 0) return;
379
418
  pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
419
+ keyEpoch += 1;
380
420
  for (const key of stale) notify(key);
381
421
  },
382
422
  requestItem(item, edge = Number.POSITIVE_INFINITY) {
383
423
  if (disposed) return;
384
424
  const key = managedAssetKey(item);
385
425
  if (!key || failedModels.has(key)) return;
386
- const source = keySource(key);
426
+ const source = boardAssetKeySource(key);
387
427
  if (!source) return;
388
428
  const entry = ensureEntry(key);
389
429
  entry.lastUsedAt = now();
430
+ entry.center = {
431
+ x: item.frame.x + item.frame.width / 2,
432
+ y: item.frame.y + item.frame.height / 2
433
+ };
390
434
  if (edge > entry.want) entry.want = edge;
391
435
  if (entry.texture) return refine(key, entry);
392
436
  if (entry.loading || entry.retryTimer) return;
@@ -413,8 +457,12 @@ function createBoardAssetManager(options) {
413
457
  hasError(key) {
414
458
  return entries.get(key)?.error ?? false;
415
459
  },
460
+ focus(point) {
461
+ focus = point;
462
+ },
416
463
  acquire(key) {
417
464
  const entry = ensureEntry(key);
465
+ if (entry.refs <= 0) entry.want = 0;
418
466
  entry.refs += 1;
419
467
  entry.lastUsedAt = now();
420
468
  },
@@ -427,7 +475,7 @@ function createBoardAssetManager(options) {
427
475
  clearRetry(entry);
428
476
  if (entry.loading) return;
429
477
  if (!entry.texture) evict(key, entry);
430
- else trim();
478
+ else scheduleTrim();
431
479
  },
432
480
  async withTextures(items, use, loadOptions) {
433
481
  const keys = /* @__PURE__ */ new Set();
@@ -467,6 +515,22 @@ function createBoardAssetManager(options) {
467
515
  for (const key of keys) manager.release(key);
468
516
  }
469
517
  },
518
+ stats() {
519
+ let textures = 0;
520
+ let cooling = 0;
521
+ let bytes = 0;
522
+ for (const entry of entries.values()) {
523
+ if (!entry.texture) continue;
524
+ textures += 1;
525
+ if (entry.refs <= 0) cooling += 1;
526
+ bytes += footprintOf(entry.texture);
527
+ }
528
+ return {
529
+ textures,
530
+ cooling,
531
+ bytes
532
+ };
533
+ },
470
534
  subscribe(listener) {
471
535
  listeners.add(listener);
472
536
  return () => {
@@ -475,6 +539,8 @@ function createBoardAssetManager(options) {
475
539
  },
476
540
  destroy() {
477
541
  disposed = true;
542
+ if (trimTimer !== null) clearTimeout(trimTimer);
543
+ trimTimer = null;
478
544
  queue.length = 0;
479
545
  listeners.clear();
480
546
  pathVersions.clear();
@@ -488,4 +554,4 @@ function createBoardAssetManager(options) {
488
554
  return manager;
489
555
  }
490
556
  //#endregion
491
- export { boardAssetKey, boardTextureEdge, createBoardAssetManager };
557
+ export { createBoardAssetManager };
@@ -1,10 +1,10 @@
1
- import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
2
- import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, boardTextureEdge, createBoardAssetManager } from "./assets.js";
1
+ import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions } from "./lru.js";
2
+ import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
+ import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, createBoardAssetManager } from "./assets.js";
4
4
  import { toBoardPointerEvent } from "./pointer.js";
5
5
  import { createBoardPointerGestures } from "./pointer-gestures.js";
6
6
  import { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene } from "./scene.js";
7
7
  import { BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, createBoardItemViewLayer } from "./views.js";
8
- import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage } from "./stage.js";
8
+ import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, mountBoardStage } from "./stage.js";
9
9
  import { BoardOverlayInput, drawBoardOverlay, expandFrame } from "./overlay.js";
10
- export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, SceneSyncInput, boardAssetKey, boardImageTexturePool, boardTextureEdge, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
10
+ export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
@@ -1,10 +1,10 @@
1
- import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
2
- import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { boardAssetKey, boardTextureEdge, createBoardAssetManager } from "./assets.js";
1
+ import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, planLruTrim, selectLruEvictions } from "./lru.js";
2
+ import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
+ import { createBoardAssetManager } from "./assets.js";
4
4
  import { toBoardPointerEvent } from "./pointer.js";
5
5
  import { createBoardPointerGestures } from "./pointer-gestures.js";
6
6
  import { createBoardScene } from "./scene.js";
7
7
  import { drawBoardOverlay, expandFrame } from "./overlay.js";
8
8
  import { createBoardItemViewLayer } from "./views.js";
9
9
  import { mountBoardStage } from "./stage.js";
10
- export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardAssetKey, boardImageTexturePool, boardTextureEdge, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
10
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
@@ -6,8 +6,8 @@
6
6
  * they enter a "cooling" pool (reference count zero but still on the GPU) so a
7
7
  * quick pan back is instant instead of re-fetching. The pool is bounded by
8
8
  * both entry count and approximate GPU bytes; when a budget is exceeded the least
9
- * recently used entries are evicted first. Keeping this decision pure makes the
10
- * release-vs-churn trade-off trivial to test.
9
+ * recently used entries shrink or leave first. Keeping this decision pure makes
10
+ * the release-vs-churn trade-off trivial to test.
11
11
  */
12
12
  type LruEntry = {
13
13
  key: string;
@@ -15,19 +15,23 @@ type LruEntry = {
15
15
  lastUsedAt: number;
16
16
  /** Approximate GPU footprint in bytes. */
17
17
  bytes: number;
18
+ shrunkBytes?: number;
18
19
  };
19
20
  type LruBudget = {
20
21
  maxCount: number;
21
22
  maxBytes: number;
22
23
  };
23
- /** Default cooling-pool ceiling: 512 images or ~192 MB of GPU memory. */
24
- declare const DEFAULT_LRU_BUDGET: LruBudget;
25
- declare const COMPACT_LRU_BUDGET: LruBudget;
26
24
  /**
27
- * Given the unreferenced (cooling) entries and a budget, return the keys to
28
- * evict — oldest first — so the remaining pool fits within both limits. Returns
29
- * an empty array when the pool already fits.
25
+ * Default cooling-pool ceiling: 2048 images or ~192 MB of GPU memory. Bytes
26
+ * bind first for full-size textures; shrunk copies let many more stay warm.
30
27
  */
28
+ declare const DEFAULT_LRU_BUDGET: LruBudget;
29
+ declare const COMPACT_LRU_BUDGET: LruBudget;
30
+ type LruTrim = {
31
+ shrink: string[];
32
+ evict: string[];
33
+ };
34
+ declare function planLruTrim(entries: LruEntry[], budget: LruBudget): LruTrim;
31
35
  declare function selectLruEvictions(entries: LruEntry[], budget: LruBudget): string[];
32
36
  //#endregion
33
- export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions };
37
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions };
@@ -1,33 +1,51 @@
1
1
  //#region src/board/stage/lru.ts
2
- /** Default cooling-pool ceiling: 512 images or ~192 MB of GPU memory. */
2
+ /**
3
+ * Default cooling-pool ceiling: 2048 images or ~192 MB of GPU memory. Bytes
4
+ * bind first for full-size textures; shrunk copies let many more stay warm.
5
+ */
3
6
  const DEFAULT_LRU_BUDGET = {
4
- maxCount: 512,
7
+ maxCount: 2048,
5
8
  maxBytes: 201326592
6
9
  };
7
10
  const COMPACT_LRU_BUDGET = {
8
- maxCount: 256,
11
+ maxCount: 1024,
9
12
  maxBytes: 100663296
10
13
  };
11
- /**
12
- * Given the unreferenced (cooling) entries and a budget, return the keys to
13
- * evict — oldest first — so the remaining pool fits within both limits. Returns
14
- * an empty array when the pool already fits.
15
- */
16
- function selectLruEvictions(entries, budget) {
17
- if (entries.length === 0) return [];
18
- let count = entries.length;
14
+ function planLruTrim(entries, budget) {
19
15
  let bytes = 0;
20
16
  for (const entry of entries) bytes += entry.bytes;
21
- if (count <= budget.maxCount && bytes <= budget.maxBytes) return [];
17
+ const over = entries.length - budget.maxCount;
18
+ if (over <= 0 && bytes <= budget.maxBytes) return {
19
+ shrink: [],
20
+ evict: []
21
+ };
22
22
  const sorted = [...entries].sort((a, b) => a.lastUsedAt - b.lastUsedAt);
23
- const evict = [];
24
- for (const entry of sorted) {
25
- if (count <= budget.maxCount && bytes <= budget.maxBytes) break;
26
- evict.push(entry.key);
27
- count -= 1;
28
- bytes -= entry.bytes;
23
+ const evict = sorted.slice(0, Math.max(0, over));
24
+ const kept = sorted.slice(evict.length);
25
+ for (const entry of evict) bytes -= entry.bytes;
26
+ const shrink = /* @__PURE__ */ new Set();
27
+ for (const entry of kept) {
28
+ if (bytes <= budget.maxBytes) break;
29
+ if (entry.shrunkBytes === void 0 || entry.shrunkBytes >= entry.bytes) continue;
30
+ shrink.add(entry);
31
+ bytes -= entry.bytes - entry.shrunkBytes;
32
+ }
33
+ for (const entry of kept) {
34
+ if (bytes <= budget.maxBytes) break;
35
+ bytes -= shrink.delete(entry) ? entry.shrunkBytes : entry.bytes;
36
+ evict.push(entry);
29
37
  }
30
- return evict;
38
+ return {
39
+ shrink: [...shrink].map(({ key }) => key),
40
+ evict: evict.map(({ key }) => key)
41
+ };
42
+ }
43
+ function selectLruEvictions(entries, budget) {
44
+ return planLruTrim(entries.map(({ key, lastUsedAt, bytes }) => ({
45
+ key,
46
+ lastUsedAt,
47
+ bytes
48
+ })), budget).evict;
31
49
  }
32
50
  //#endregion
33
- export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions };
51
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, planLruTrim, selectLruEvictions };
@@ -1,6 +1,8 @@
1
1
  import { BoardDrawPoint } from "../../protocol/dist/board-model.js";
2
+ import { Rect } from "../model/geometry.js";
2
3
  import { BoardScene as BoardScene$1, BoardSceneItem } from "../model/scene.js";
3
4
  import "../model/index.js";
5
+ import { BoardLod } from "../render/lod.js";
4
6
  import { BoardCardRendererResolver, BoardRenderContext } from "../render/renderers/board-renderer-registry.js";
5
7
  import "../render/index.js";
6
8
  import { Container, Graphics } from "pixi.js";
@@ -22,12 +24,14 @@ type SceneSyncInput = {
22
24
  globalSig: string;
23
25
  staleIds?: ReadonlySet<string>;
24
26
  structureVersion: number;
25
- geometryVersion: number;
26
27
  gestureActive: boolean;
27
28
  focus?: {
28
29
  x: number;
29
30
  y: number;
30
31
  };
32
+ /** The world area on screen; live cards outside it skip rendering. */
33
+ view?: Rect;
34
+ lod?: BoardLod;
31
35
  };
32
36
  type BoardSceneNode = {
33
37
  item: BoardSceneItem;
@@ -39,8 +43,14 @@ type BoardScene = {
39
43
  holders: (key: string) => ReadonlySet<string>;
40
44
  readonly liveVersion: number;
41
45
  liveIds: () => ReadonlySet<string>;
46
+ /** Live cards and cards drawn on far plates. */
47
+ counts: () => {
48
+ live: number;
49
+ far: number;
50
+ };
42
51
  readonly animated: boolean;
43
52
  tick: (clock: number) => number | null;
53
+ redrawText: () => void;
44
54
  destroy: (context: BoardRenderContext) => void;
45
55
  };
46
56
  declare function createBoardScene(options: {