@neta-art/cohub 11.5.1 → 12.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 (73) hide show
  1. package/dist/board/editor/animation.js +2 -1
  2. package/dist/board/editor/creation.js +1 -1
  3. package/dist/board/editor/document-edits.d.ts +1 -1
  4. package/dist/board/editor/document-edits.js +1 -19
  5. package/dist/board/editor/document.js +2 -1
  6. package/dist/board/editor/index.d.ts +2 -1
  7. package/dist/board/editor/index.js +2 -1
  8. package/dist/board/editor/snapshots.js +11 -13
  9. package/dist/board/export/index.js +1 -1
  10. package/dist/board/export/scene.js +1 -1
  11. package/dist/board/model/client-geometry.d.ts +22 -0
  12. package/dist/board/model/client-geometry.js +94 -0
  13. package/dist/board/model/document-share.d.ts +5 -0
  14. package/dist/board/model/document-share.js +27 -0
  15. package/dist/board/model/index.d.ts +5 -4
  16. package/dist/board/model/index.js +4 -3
  17. package/dist/board/model/items/create.d.ts +5 -1
  18. package/dist/board/model/items/create.js +29 -58
  19. package/dist/board/model/items/index.d.ts +2 -2
  20. package/dist/board/model/items/index.js +2 -2
  21. package/dist/board/model/items/registry.js +2 -5
  22. package/dist/board/model/json.d.ts +4 -0
  23. package/dist/board/model/json.js +22 -0
  24. package/dist/board/model/media-playback.d.ts +6 -3
  25. package/dist/board/player/index.d.ts +6 -2
  26. package/dist/board/player/index.js +33 -15
  27. package/dist/board/render/index.d.ts +2 -2
  28. package/dist/board/render/index.js +6 -6
  29. package/dist/board/render/renderers/arrow-card-renderer.js +5 -1
  30. package/dist/board/render/renderers/audio-card-renderer.js +4 -1
  31. package/dist/board/render/renderers/board-renderer-registry.d.ts +4 -2
  32. package/dist/board/render/renderers/custom-card-renderer.js +5 -2
  33. package/dist/board/render/renderers/file-card-renderer.js +4 -1
  34. package/dist/board/render/renderers/frame-card-renderer.js +5 -1
  35. package/dist/board/render/renderers/image-card-renderer.js +13 -49
  36. package/dist/board/render/renderers/model-card-renderer.js +4 -1
  37. package/dist/board/render/renderers/shape-card-renderer.js +5 -1
  38. package/dist/board/render/renderers/task-card-renderer.js +6 -3
  39. package/dist/board/render/renderers/text-card-renderer.js +6 -2
  40. package/dist/board/render/renderers/unknown-card-renderer.js +4 -0
  41. package/dist/board/render/renderers/video-card-renderer.js +19 -18
  42. package/dist/board/render/script-host.d.ts +2 -0
  43. package/dist/board/render/script-host.js +44 -9
  44. package/dist/board/render/script-resolution.d.ts +4 -0
  45. package/dist/board/render/script-resolution.js +18 -0
  46. package/dist/board/render/surface.d.ts +9 -2
  47. package/dist/board/render/surface.js +15 -1
  48. package/dist/board/replica/index.js +4 -1
  49. package/dist/board/stage/assets.d.ts +12 -8
  50. package/dist/board/stage/assets.js +173 -88
  51. package/dist/board/stage/index.d.ts +3 -3
  52. package/dist/board/stage/index.js +3 -3
  53. package/dist/board/stage/lru.d.ts +3 -2
  54. package/dist/board/stage/lru.js +8 -4
  55. package/dist/board/stage/pointer.d.ts +1 -1
  56. package/dist/board/stage/pointer.js +4 -5
  57. package/dist/board/stage/scene.d.ts +3 -0
  58. package/dist/board/stage/scene.js +44 -8
  59. package/dist/board/stage/stage.d.ts +1 -0
  60. package/dist/board/stage/stage.js +123 -56
  61. package/dist/board/stage/views.d.ts +7 -2
  62. package/dist/board/stage/views.js +50 -21
  63. package/dist/chunks/environment.d.ts +16 -2
  64. package/dist/index.js +65 -22
  65. package/dist/protocol/dist/board-constants.js +56 -1
  66. package/dist/protocol/dist/board-layout.d.ts +8 -1
  67. package/dist/protocol/dist/board-layout.js +16 -4
  68. package/dist/protocol/dist/board-model.js +13 -19
  69. package/dist/protocol/dist/board-schema.d.ts +52 -0
  70. package/dist/protocol/dist/board-schema.js +8 -0
  71. package/dist/protocol/dist/core/media.js +1 -0
  72. package/dist/protocol/dist/index.d.ts +2 -2
  73. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { mediaPreviewCandidates } from "../../media.js";
1
+ import { MEDIA_VARIANT_SIZES, imageVariantUrl, mediaVariantSize, probeMediaInfo, variantSource, videoFrameUrl } from "../../media.js";
2
2
  import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
3
- import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
+ import { featuredTaskArtifact, taskArtifactPreviewUrl } from "../../protocol/dist/board-task.js";
4
4
  import { boardModelFormat, isBoardModelItem } from "../model/media.js";
5
5
  import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
6
6
  import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
@@ -8,12 +8,23 @@ import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
8
8
  import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
9
9
  import { Texture } from "pixi.js";
10
10
  //#region src/board/stage/assets.ts
11
- const TASK_TEXTURE_SIZE = 1536;
12
11
  function taskPreviewKey(artifact) {
13
- if (!artifact || artifact.type === "text") return null;
14
- const [still] = mediaPreviewCandidates(artifact, { size: TASK_TEXTURE_SIZE });
15
- if (still) return `url:${still}`;
16
- return artifact.type === "video" ? `video-url:${encodeURIComponent(artifact.url)}` : null;
12
+ const cover = taskArtifactPreviewUrl(artifact);
13
+ if (cover) return `url:${cover}`;
14
+ if (artifact?.type !== "video") return null;
15
+ const frame = videoFrameUrl(artifact.url, 768);
16
+ return frame ? `url:${frame}` : `video-url:${encodeURIComponent(artifact.url)}`;
17
+ }
18
+ const LARGEST_VARIANT = MEDIA_VARIANT_SIZES[MEDIA_VARIANT_SIZES.length - 1];
19
+ function boardTextureEdge(item, scale) {
20
+ const crop = item.type === "image" ? item.props.crop : void 0;
21
+ const pixels = Math.max(item.frame.width / (crop?.w || 1), item.frame.height / (crop?.h || 1)) * scale;
22
+ return pixels > LARGEST_VARIANT ? Number.POSITIVE_INFINITY : mediaVariantSize(pixels);
23
+ }
24
+ function variantUrl(source, edge) {
25
+ if (!(edge <= LARGEST_VARIANT)) return null;
26
+ const url = imageVariantUrl(source, edge);
27
+ return url && url !== source ? url : null;
17
28
  }
18
29
  const STILL_MEDIA = ["video", "model"];
19
30
  function stillKey(media, src, mtimeMs) {
@@ -70,7 +81,7 @@ function footprintOf(texture) {
70
81
  return (texture.width || 0) * (texture.height || 0) * 4;
71
82
  }
72
83
  function createBoardAssetManager(options) {
73
- const concurrency = Math.max(1, options.concurrency ?? 4);
84
+ const concurrency = Math.max(1, options.concurrency ?? 6);
74
85
  const limits = {
75
86
  image: concurrency,
76
87
  video: Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency)),
@@ -103,12 +114,20 @@ function createBoardAssetManager(options) {
103
114
  });
104
115
  if (url) pendingUnloads.set(pendingKey, promise);
105
116
  }
117
+ const retiring = /* @__PURE__ */ new Set();
118
+ function retire(key, url, texture) {
119
+ const release = () => {
120
+ if (retiring.delete(release)) releaseTexture(key, url, texture);
121
+ };
122
+ retiring.add(release);
123
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
124
+ else setTimeout(release, 0);
125
+ }
106
126
  const resolveFileUrl = options.resolveFileUrl;
107
127
  const entries = /* @__PURE__ */ new Map();
108
128
  const pathVersions = /* @__PURE__ */ new Map();
109
- const loaders = /* @__PURE__ */ new Map();
129
+ const starters = /* @__PURE__ */ new Map();
110
130
  const failedModels = /* @__PURE__ */ new Set();
111
- const inflight = /* @__PURE__ */ new Set();
112
131
  const queue = [];
113
132
  const listeners = /* @__PURE__ */ new Set();
114
133
  let active = 0;
@@ -118,8 +137,8 @@ function createBoardAssetManager(options) {
118
137
  model: 0
119
138
  };
120
139
  let disposed = false;
121
- function notify() {
122
- for (const listener of listeners) listener();
140
+ function notify(key) {
141
+ for (const listener of listeners) listener(key);
123
142
  }
124
143
  function managedAssetKey(item) {
125
144
  const key = boardAssetKey(item);
@@ -141,8 +160,13 @@ function createBoardAssetManager(options) {
141
160
  let entry = entries.get(key);
142
161
  if (!entry) {
143
162
  entry = {
163
+ media: keySource(key)?.media ?? "image",
164
+ source: null,
144
165
  url: null,
145
166
  texture: null,
167
+ edge: 0,
168
+ want: 0,
169
+ natural: void 0,
146
170
  refs: 0,
147
171
  loading: false,
148
172
  error: false,
@@ -162,21 +186,15 @@ function createBoardAssetManager(options) {
162
186
  entry.retryTimer = null;
163
187
  if (disposed || entries.get(key) !== entry) return;
164
188
  if (entry.refs <= 0 || entry.texture || entry.loading) return;
165
- const load = loaders.get(key);
166
- if (!load) return;
189
+ const start = starters.get(key);
190
+ if (!start) return;
167
191
  entry.error = false;
168
- if (!queue.some((task) => task.key === key)) {
169
- queue.push({
170
- key,
171
- load
172
- });
173
- pump();
174
- }
192
+ start(entry);
175
193
  }, delay);
176
194
  }
177
195
  function evict(key, entry) {
178
196
  clearRetry(entry);
179
- loaders.delete(key);
197
+ starters.delete(key);
180
198
  if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
181
199
  entries.delete(key);
182
200
  }
@@ -193,69 +211,128 @@ function createBoardAssetManager(options) {
193
211
  if (entry) evict(key, entry);
194
212
  }
195
213
  }
196
- function settle(key, entry, texture, error) {
214
+ function adopt(key, entry, loaded, error) {
197
215
  entry.loading = false;
198
- inflight.delete(key);
199
- active -= 1;
200
- const media = keySource(key)?.media;
201
- if (media) activeByMedia[media] -= 1;
202
216
  if (entries.get(key) !== entry) {
203
- if (texture) releaseTexture(key, entry.url ?? "", texture);
204
- if (!disposed) pump();
217
+ if (loaded) releaseTexture(key, loaded.url, loaded.texture);
205
218
  return;
206
219
  }
207
- if (texture) {
220
+ if (loaded) {
221
+ const replaced = entry.texture;
222
+ const replacedUrl = entry.url ?? "";
223
+ const { texture } = loaded;
208
224
  entry.error = false;
209
225
  entry.attempts = 0;
210
226
  entry.texture = texture;
227
+ entry.url = loaded.url;
228
+ entry.edge = loaded.edge;
229
+ if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = videoTextureNaturalSize(texture) ?? {
230
+ width: texture.width,
231
+ height: texture.height
232
+ };
211
233
  entry.lastUsedAt = now();
212
234
  if (entry.refs <= 0) trim();
213
- else notify();
214
- } else {
215
- entry.error = true;
216
- entry.attempts += 1;
217
- entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
218
- console.warn(`[board] failed to load preview for ${key}`, error);
219
- if (media === "model") failedModels.add(key);
220
- if (entry.refs <= 0) evict(key, entry);
221
- else {
222
- if (media !== "model") scheduleRetry(key, entry);
223
- notify();
224
- }
235
+ else notify(key);
236
+ if (replaced) retire(key, replacedUrl, replaced);
237
+ refine(key, entry);
238
+ return;
225
239
  }
226
- if (!disposed) pump();
240
+ if (entry.texture) {
241
+ entry.edge = Number.POSITIVE_INFINITY;
242
+ notify(key);
243
+ return;
244
+ }
245
+ const media = entry.media;
246
+ entry.error = true;
247
+ entry.attempts += 1;
248
+ entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
249
+ console.warn(`[board] failed to load preview for ${key}`, error);
250
+ if (media === "model") failedModels.add(key);
251
+ if (entry.refs <= 0) evict(key, entry);
252
+ else {
253
+ if (media !== "model") scheduleRetry(key, entry);
254
+ notify(key);
255
+ }
256
+ }
257
+ function refinable(entry) {
258
+ return entry.media === "image" && entry.refs > 0 && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
259
+ }
260
+ function refine(key, entry) {
261
+ if (refinable(entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
262
+ }
263
+ function enqueue(key, media, load) {
264
+ if (queue.some((task) => task.key === key)) return;
265
+ queue.push({
266
+ key,
267
+ media,
268
+ load
269
+ });
270
+ pump();
227
271
  }
228
272
  function pump() {
229
273
  while (!disposed && active < concurrency && queue.length > 0) {
230
- const taskIndex = queue.findIndex(({ key }) => {
231
- const media = keySource(key)?.media;
232
- return !media || activeByMedia[media] < limits[media];
233
- });
274
+ const taskIndex = queue.findIndex(({ media }) => activeByMedia[media] < limits[media]);
234
275
  if (taskIndex < 0) return;
235
276
  const [task] = queue.splice(taskIndex, 1);
236
277
  if (!task) continue;
237
278
  const entry = entries.get(task.key);
238
- const source = keySource(task.key);
239
- if (!entry || !source || entry.texture || entry.loading) continue;
279
+ if (!entry || entry.loading || entry.texture && !refinable(entry)) continue;
240
280
  entry.loading = true;
241
- inflight.add(task.key);
242
281
  active += 1;
243
- activeByMedia[source.media] += 1;
244
- task.load(entry).then((texture) => settle(task.key, entry, texture), (error) => settle(task.key, entry, null, error));
282
+ activeByMedia[task.media] += 1;
283
+ const done = (loaded, error) => {
284
+ active -= 1;
285
+ activeByMedia[task.media] -= 1;
286
+ adopt(task.key, entry, loaded, error);
287
+ if (!disposed) pump();
288
+ };
289
+ task.load(entry).then((loaded) => done(loaded), (error) => done(null, error));
245
290
  }
246
291
  }
247
- function urlLoader(source) {
248
- const getUrl = source.kind === "url" ? () => Promise.resolve(source.value) : () => resolveFileUrl(source.value);
292
+ function textureLoad(media, source) {
249
293
  return async (entry) => {
250
- const url = await getUrl();
251
- if (!url || disposed) return null;
252
- entry.url = url;
253
- const pending = pendingUnloads.get(`${source.media}:${url}`);
294
+ entry.source = source;
295
+ const edge = entry.want;
296
+ const variant = media === "image" ? variantUrl(source, edge) : null;
297
+ const url = variant ?? source;
298
+ const pending = pendingUnloads.get(`${media}:${url}`);
254
299
  if (pending) await pending;
255
300
  if (disposed) return null;
256
- return loadTexture(url, source.media);
301
+ const texture = await loadTexture(url, media);
302
+ if (!texture) return null;
303
+ return {
304
+ texture,
305
+ url,
306
+ edge: !variant || Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge
307
+ };
308
+ };
309
+ }
310
+ function resolveFile(key, media, path) {
311
+ return (entry) => {
312
+ const fresh = entry.attempts > 0;
313
+ entry.loading = true;
314
+ resolveFileUrl(path, fresh ? { fresh } : void 0).then((url) => {
315
+ if (disposed || entries.get(key) !== entry) return;
316
+ if (entry.refs <= 0) return evict(key, entry);
317
+ if (!url) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
318
+ entry.loading = false;
319
+ enqueue(key, media, textureLoad(media, url));
320
+ }, (error) => {
321
+ if (!disposed) adopt(key, entry, null, error);
322
+ });
257
323
  };
258
324
  }
325
+ function probeNaturalSize(key, entry, source) {
326
+ entry.natural = null;
327
+ probeMediaInfo(source, { type: "image" }).then(({ width, height }) => {
328
+ if (disposed || entries.get(key) !== entry || !width || !height) return;
329
+ entry.natural = {
330
+ width,
331
+ height
332
+ };
333
+ notify(key);
334
+ });
335
+ }
259
336
  function modelPreviewable(item) {
260
337
  const previews = options.modelPreviews;
261
338
  if (!previews) return false;
@@ -274,20 +351,35 @@ function createBoardAssetManager(options) {
274
351
  format,
275
352
  size: snapshot?.size
276
353
  };
277
- return async () => Texture.from(await previews.load(request));
354
+ return async () => ({
355
+ texture: Texture.from(await previews.load(request)),
356
+ url: "",
357
+ edge: Number.POSITIVE_INFINITY
358
+ });
359
+ }
360
+ function starter(item, key, source) {
361
+ if (source.media === "model") {
362
+ const load = modelLoader(item, key);
363
+ return load ? () => enqueue(key, "model", load) : null;
364
+ }
365
+ const media = source.media;
366
+ if (source.kind === "file") return resolveFile(key, media, source.value);
367
+ const load = textureLoad(media, source.value);
368
+ return () => enqueue(key, media, load);
278
369
  }
279
370
  const manager = {
280
371
  assetKey: managedAssetKey,
281
372
  invalidatePath(path) {
282
373
  if (disposed) return;
283
- if (![...entries.keys()].some((key) => {
374
+ const stale = [...entries.keys()].filter((key) => {
284
375
  const source = keySource(key);
285
376
  return source?.kind === "file" && source.value === path;
286
- })) return;
377
+ });
378
+ if (stale.length === 0) return;
287
379
  pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
288
- notify();
380
+ for (const key of stale) notify(key);
289
381
  },
290
- requestItem(item) {
382
+ requestItem(item, edge = Number.POSITIVE_INFINITY) {
291
383
  if (disposed) return;
292
384
  const key = managedAssetKey(item);
293
385
  if (!key || failedModels.has(key)) return;
@@ -295,22 +387,16 @@ function createBoardAssetManager(options) {
295
387
  if (!source) return;
296
388
  const entry = ensureEntry(key);
297
389
  entry.lastUsedAt = now();
298
- if (entry.texture || entry.loading) return;
390
+ if (edge > entry.want) entry.want = edge;
391
+ if (entry.texture) return refine(key, entry);
392
+ if (entry.loading || entry.retryTimer) return;
299
393
  if (entry.error && now() < entry.retryAt) return;
300
- entry.error = false;
301
- clearRetry(entry);
302
394
  if (queue.some((queued) => queued.key === key)) return;
303
- const load = source.media === "model" ? modelLoader(item, key) : urlLoader({
304
- ...source,
305
- media: source.media
306
- });
307
- if (!load) return;
308
- loaders.set(key, load);
309
- queue.push({
310
- key,
311
- load
312
- });
313
- pump();
395
+ const start = starter(item, key, source);
396
+ if (!start) return;
397
+ entry.error = false;
398
+ starters.set(key, start);
399
+ start(entry);
314
400
  },
315
401
  getTexture(key) {
316
402
  const entry = entries.get(key);
@@ -319,12 +405,10 @@ function createBoardAssetManager(options) {
319
405
  return entry.texture;
320
406
  },
321
407
  getNaturalSize(key) {
322
- const texture = manager.getTexture(key);
323
- if (!texture) return null;
324
- return videoTextureNaturalSize(texture) ?? {
325
- width: texture.width,
326
- height: texture.height
327
- };
408
+ const entry = entries.get(key);
409
+ if (!entry?.texture) return null;
410
+ if (entry.natural === void 0 && entry.source) probeNaturalSize(key, entry, entry.source);
411
+ return entry.natural ?? null;
328
412
  },
329
413
  hasError(key) {
330
414
  return entries.get(key)?.error ?? false;
@@ -355,10 +439,10 @@ function createBoardAssetManager(options) {
355
439
  if (keys.size === 0) return use(textures);
356
440
  for (const key of keys) manager.acquire(key);
357
441
  try {
358
- for (const item of items) manager.requestItem(item);
442
+ for (const item of items) manager.requestItem(item, loadOptions?.edge?.(item));
359
443
  const settled = () => [...keys].every((key) => {
360
444
  const entry = entries.get(key);
361
- return Boolean(entry?.texture) || Boolean(entry?.error);
445
+ return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(entry));
362
446
  });
363
447
  if (!settled()) {
364
448
  const timeoutMs = loadOptions?.timeoutMs ?? 15e3;
@@ -394,13 +478,14 @@ function createBoardAssetManager(options) {
394
478
  queue.length = 0;
395
479
  listeners.clear();
396
480
  pathVersions.clear();
397
- loaders.clear();
481
+ starters.clear();
398
482
  failedModels.clear();
399
483
  for (const [key, entry] of entries) evict(key, entry);
400
484
  entries.clear();
485
+ for (const release of [...retiring]) release();
401
486
  }
402
487
  };
403
488
  return manager;
404
489
  }
405
490
  //#endregion
406
- export { boardAssetKey, createBoardAssetManager };
491
+ export { boardAssetKey, boardTextureEdge, createBoardAssetManager };
@@ -1,10 +1,10 @@
1
- import { DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
1
+ import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
2
2
  import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, createBoardAssetManager } from "./assets.js";
3
+ import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, boardAssetKey, boardTextureEdge, 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
8
  import { BoardStage, BoardStageOptions, BoardStagePointerPhase, 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, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, SceneSyncInput, boardAssetKey, boardImageTexturePool, 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, 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 };
@@ -1,10 +1,10 @@
1
- import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
1
+ import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
2
2
  import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { boardAssetKey, createBoardAssetManager } from "./assets.js";
3
+ import { boardAssetKey, boardTextureEdge, 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 { DEFAULT_LRU_BUDGET, boardAssetKey, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
10
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardAssetKey, boardImageTexturePool, boardTextureEdge, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
@@ -20,8 +20,9 @@ type LruBudget = {
20
20
  maxCount: number;
21
21
  maxBytes: number;
22
22
  };
23
- /** Default cooling-pool ceiling: 64 images or ~128 MB of GPU memory. */
23
+ /** Default cooling-pool ceiling: 512 images or ~192 MB of GPU memory. */
24
24
  declare const DEFAULT_LRU_BUDGET: LruBudget;
25
+ declare const COMPACT_LRU_BUDGET: LruBudget;
25
26
  /**
26
27
  * Given the unreferenced (cooling) entries and a budget, return the keys to
27
28
  * evict — oldest first — so the remaining pool fits within both limits. Returns
@@ -29,4 +30,4 @@ declare const DEFAULT_LRU_BUDGET: LruBudget;
29
30
  */
30
31
  declare function selectLruEvictions(entries: LruEntry[], budget: LruBudget): string[];
31
32
  //#endregion
32
- export { DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions };
33
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions };
@@ -1,8 +1,12 @@
1
1
  //#region src/board/stage/lru.ts
2
- /** Default cooling-pool ceiling: 64 images or ~128 MB of GPU memory. */
2
+ /** Default cooling-pool ceiling: 512 images or ~192 MB of GPU memory. */
3
3
  const DEFAULT_LRU_BUDGET = {
4
- maxCount: 64,
5
- maxBytes: 134217728
4
+ maxCount: 512,
5
+ maxBytes: 201326592
6
+ };
7
+ const COMPACT_LRU_BUDGET = {
8
+ maxCount: 256,
9
+ maxBytes: 100663296
6
10
  };
7
11
  /**
8
12
  * Given the unreferenced (cooling) entries and a budget, return the keys to
@@ -26,4 +30,4 @@ function selectLruEvictions(entries, budget) {
26
30
  return evict;
27
31
  }
28
32
  //#endregion
29
- export { DEFAULT_LRU_BUDGET, selectLruEvictions };
33
+ export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions };
@@ -3,6 +3,6 @@ import "../model/index.js";
3
3
  import { BoardPointerEvent } from "../editor/interaction.js";
4
4
  import "../editor/index.js";
5
5
  //#region src/board/stage/pointer.d.ts
6
- declare function toBoardPointerEvent(event: PointerEvent, rect: DOMRect, camera: BoardViewport, coalesce?: boolean): BoardPointerEvent;
6
+ declare function toBoardPointerEvent(event: PointerEvent, rect: DOMRect | HTMLElement, camera: BoardViewport, coalesce?: boolean): BoardPointerEvent;
7
7
  //#endregion
8
8
  export { toBoardPointerEvent };
@@ -1,16 +1,15 @@
1
1
  import { pointToWorld, screenPoint } from "../model/geometry.js";
2
+ import { elementClientToLocal } from "../model/client-geometry.js";
2
3
  //#region src/board/stage/pointer.ts
3
4
  function pressureOf(event) {
4
5
  return event.pointerType === "pen" && event.pressure > 0 ? event.pressure : .5;
5
6
  }
6
- function screenOf(event, rect) {
7
- return screenPoint(event.clientX - rect.left, event.clientY - rect.top);
8
- }
9
7
  function toBoardPointerEvent(event, rect, camera, coalesce = false) {
10
- const screen = screenOf(event, rect);
8
+ const local = "getBoundingClientRect" in rect ? elementClientToLocal(rect) : (x, y) => screenPoint(x - rect.left, y - rect.top);
9
+ const screen = local(event.clientX, event.clientY);
11
10
  const coalesced = coalesce ? event.getCoalescedEvents?.() : void 0;
12
11
  const samples = coalesced && coalesced.length > 1 ? coalesced.map((sample) => ({
13
- world: pointToWorld(screenOf(sample, rect), camera),
12
+ world: pointToWorld(local(sample.clientX, sample.clientY), camera),
14
13
  pressure: pressureOf(sample)
15
14
  })) : void 0;
16
15
  return {
@@ -32,6 +32,9 @@ type BoardSceneNode = {
32
32
  type BoardScene = {
33
33
  sync: (input: SceneSyncInput) => void;
34
34
  getNode: (itemId: string) => BoardSceneNode | null;
35
+ holders: (key: string) => ReadonlySet<string>;
36
+ readonly liveVersion: number;
37
+ liveIds: () => ReadonlySet<string>;
35
38
  readonly animated: boolean;
36
39
  tick: (clock: number) => number | null;
37
40
  destroy: (context: BoardRenderContext) => void;
@@ -1,5 +1,6 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
2
  import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
3
+ import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
3
4
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
4
5
  import { Graphics } from "pixi.js";
5
6
  //#region src/board/stage/scene.ts
@@ -12,6 +13,7 @@ const Z_LIFTED = 1e9;
12
13
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
13
14
  const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
14
15
  const POOL_LIMIT_PER_RENDERER = 48;
16
+ const EMPTY_IDS = /* @__PURE__ */ new Set();
15
17
  function idSetSignature(ids) {
16
18
  let hash = ids.size;
17
19
  for (const id of ids) {
@@ -31,20 +33,33 @@ function createBoardScene(options) {
31
33
  const tickingIds = /* @__PURE__ */ new Set();
32
34
  const pools = /* @__PURE__ */ new Map();
33
35
  const heldKeys = /* @__PURE__ */ new Map();
36
+ const holders = /* @__PURE__ */ new Map();
34
37
  let orderById = /* @__PURE__ */ new Map();
35
38
  let farSig = null;
36
39
  let farActive = false;
37
40
  let lastStructureVersion = -1;
38
41
  let lastZoom = NaN;
42
+ let lastResolution = "";
43
+ let liveVersion = 0;
39
44
  let lifted = /* @__PURE__ */ new Set();
40
45
  const liveStrokes = /* @__PURE__ */ new Map();
41
- function setHeldKey(context, cardId, desiredKey) {
46
+ function setHeldKey(context, cardId, item) {
47
+ const desiredKey = item ? context.assetKey(item) : null;
42
48
  const heldKey = heldKeys.get(cardId) ?? null;
43
49
  if (desiredKey === heldKey) return;
44
- if (heldKey) context.releaseTexture(heldKey);
45
- if (desiredKey) context.acquireTexture(desiredKey);
46
- if (desiredKey) heldKeys.set(cardId, desiredKey);
47
- else heldKeys.delete(cardId);
50
+ if (heldKey) {
51
+ context.releaseTexture(heldKey);
52
+ const ids = holders.get(heldKey);
53
+ ids?.delete(cardId);
54
+ if (ids?.size === 0) holders.delete(heldKey);
55
+ }
56
+ if (desiredKey && item) {
57
+ heldKeys.set(cardId, desiredKey);
58
+ const ids = holders.get(desiredKey);
59
+ if (ids) ids.add(cardId);
60
+ else holders.set(desiredKey, /* @__PURE__ */ new Set([cardId]));
61
+ context.acquireTexture(desiredKey, item);
62
+ } else heldKeys.delete(cardId);
48
63
  }
49
64
  function takeFromPool(rendererId) {
50
65
  const pool = pools.get(rendererId);
@@ -119,7 +134,7 @@ function createBoardScene(options) {
119
134
  scriptRevision: context.scripts?.revision?.(item.id) ?? 0
120
135
  };
121
136
  cards.set(item.id, entry);
122
- setHeldKey(context, item.id, context.assetKey(item));
137
+ setHeldKey(context, item.id, item);
123
138
  renderer.zoom?.(container, context.zoom);
124
139
  return entry;
125
140
  }
@@ -277,6 +292,9 @@ function createBoardScene(options) {
277
292
  }
278
293
  function sync(input) {
279
294
  const { items, scene, context, getItem, visibleIds, pinnedIds, globalSig, structureVersion, geometryVersion, gestureActive } = input;
295
+ const resolution = `${textZoomBucket(context.zoom)}:${getBoardResolution()}`;
296
+ const resolutionChanged = resolution !== lastResolution;
297
+ lastResolution = resolution;
280
298
  const structureChanged = structureVersion !== lastStructureVersion;
281
299
  if (structureChanged) {
282
300
  lastStructureVersion = structureVersion;
@@ -304,7 +322,14 @@ function createBoardScene(options) {
304
322
  if (farActive) {
305
323
  const { orderedIds, unbatched } = visibleFacts(input);
306
324
  for (const id of unbatched) wanted.add(id);
307
- let budget = FAR_LIVE_LIMIT;
325
+ let retained = 0;
326
+ if (!farPainted) {
327
+ for (const id of cards.keys()) if ((!visibleIds || visibleIds.has(id)) && !farCovered.has(id) && !wanted.has(id)) {
328
+ wanted.add(id);
329
+ retained++;
330
+ }
331
+ }
332
+ let budget = Math.max(0, FAR_LIVE_LIMIT - retained);
308
333
  for (const id of orderedIds) {
309
334
  if (budget === 0) break;
310
335
  if (farCovered.has(id) || wanted.has(id)) continue;
@@ -346,13 +371,17 @@ function createBoardScene(options) {
346
371
  entry.hovered = hovered;
347
372
  entry.resizing = resizing;
348
373
  entry.globalSig = globalSig;
349
- setHeldKey(context, id, context.assetKey(item));
374
+ setHeldKey(context, id, item);
350
375
  if (changed) {
351
376
  entry.renderer.update(entry.container, item, context);
352
377
  entry.container.alpha = scene.opacity(id);
353
378
  if (entry.renderer.tick) tickingIds.add(id);
379
+ } else if (resolutionChanged && entry.renderer.resolution) {
380
+ entry.renderer.resolution(entry.container, item, context);
381
+ if (entry.renderer.tick) tickingIds.add(id);
354
382
  }
355
383
  }
384
+ if (liveSetChanged) liveVersion += 1;
356
385
  if (structureChanged || liveSetChanged) syncClips(scene);
357
386
  if (context.zoom !== lastZoom) {
358
387
  lastZoom = context.zoom;
@@ -391,6 +420,7 @@ function createBoardScene(options) {
391
420
  pools.clear();
392
421
  for (const key of heldKeys.values()) context.releaseTexture(key);
393
422
  heldKeys.clear();
423
+ holders.clear();
394
424
  orderById = /* @__PURE__ */ new Map();
395
425
  for (const entry of clips.values()) entry.group.destroy({ children: true });
396
426
  clips.clear();
@@ -402,6 +432,7 @@ function createBoardScene(options) {
402
432
  visibleMemo = null;
403
433
  lastStructureVersion = -1;
404
434
  lastZoom = NaN;
435
+ lastResolution = "";
405
436
  }
406
437
  return {
407
438
  sync,
@@ -412,6 +443,11 @@ function createBoardScene(options) {
412
443
  container: entry.container
413
444
  } : null;
414
445
  },
446
+ holders: (key) => holders.get(key) ?? EMPTY_IDS,
447
+ get liveVersion() {
448
+ return liveVersion;
449
+ },
450
+ liveIds: () => new Set(cards.keys()),
415
451
  get animated() {
416
452
  return animatedIds.size > 0;
417
453
  },
@@ -56,6 +56,7 @@ type BoardStageOptions = {
56
56
  height: number;
57
57
  }) => void;
58
58
  onVisibleChange?: (ids: ReadonlySet<string> | null) => void;
59
+ onLiveChange?: (ids: ReadonlySet<string>) => void;
59
60
  safeRect?: () => Rect;
60
61
  scriptCapabilities?: Record<string, (id: string, input: unknown) => Promise<unknown>>;
61
62
  };