@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,15 +1,16 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
2
  import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
3
+ import { itemBounds, rectsIntersect } from "../model/geometry.js";
4
+ import { DEFAULT_BOARD_LOD, boardCardDetailed } from "../render/lod.js";
3
5
  import { textZoomBucket } from "../render/text-resolution.js";
4
6
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
- import { Graphics } from "pixi.js";
7
+ import { Graphics, Text } from "pixi.js";
6
8
  //#region src/board/stage/scene.ts
7
- const FAR_LAYER_ENTER = 450;
8
- const FAR_LAYER_EXIT = 350;
9
- const FAR_PASS_BATCH = 240;
10
- const FAR_LIVE_LIMIT = 64;
9
+ const FAR_CHUNK = 256;
10
+ const FAR_PAINT_BUDGET = 2400;
11
+ const MATERIALIZE_BATCH = 48;
12
+ const LOD_ZOOM_STEPS = 8;
11
13
  const RESOLUTION_BATCH = 32;
12
- const Z_FAR_LAYER = -1;
13
14
  const Z_LIFTED = 1e9;
14
15
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
15
16
  const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
@@ -24,6 +25,9 @@ function idSetSignature(ids) {
24
25
  }
25
26
  return hash;
26
27
  }
28
+ function sameItems(a, b) {
29
+ return a.length === b.length && a.every((item, index) => item === b[index]);
30
+ }
27
31
  function createBoardScene(options) {
28
32
  const { world, overlay, getRenderer } = options;
29
33
  world.sortableChildren = true;
@@ -36,8 +40,7 @@ function createBoardScene(options) {
36
40
  const heldKeys = /* @__PURE__ */ new Map();
37
41
  const holders = /* @__PURE__ */ new Map();
38
42
  let orderById = /* @__PURE__ */ new Map();
39
- let farSig = null;
40
- let farActive = false;
43
+ let idsByOrder = [];
41
44
  let lastStructureVersion = -1;
42
45
  let lastZoom = NaN;
43
46
  let lastResolution = "";
@@ -105,6 +108,11 @@ function createBoardScene(options) {
105
108
  const host = hostFor(entry.item, scene);
106
109
  if (entry.container.parent !== host) host.addChild(entry.container);
107
110
  }
111
+ for (const plate of plates.values()) {
112
+ const first = plate.items[0];
113
+ const host = first && hostFor(first, scene);
114
+ if (host && plate.graphics.parent !== host) host.addChild(plate.graphics);
115
+ }
108
116
  for (const [id, entry] of clips) {
109
117
  const frame = scene.get(id);
110
118
  if (!frame || !isClippingFrame(frame) || entry.group.children.length <= 1) {
@@ -121,6 +129,7 @@ function createBoardScene(options) {
121
129
  const pooled = takeFromPool(renderer.id);
122
130
  const container = pooled ?? renderer.create(item, context);
123
131
  container.visible = true;
132
+ container.culled = false;
124
133
  if (pooled) renderer.update(container, item, context);
125
134
  container.alpha = scene.opacity(item.id);
126
135
  hostFor(item, scene).addChild(container);
@@ -141,79 +150,173 @@ function createBoardScene(options) {
141
150
  renderer.zoom?.(container, context.zoom);
142
151
  return entry;
143
152
  }
144
- let farFront = createFarLayer();
145
- let farBack = createFarLayer();
146
- let farPainted = false;
147
- let farCovered = /* @__PURE__ */ new Set();
148
- let farInput = null;
149
- let farRunning = false;
150
- let farQueued = false;
151
- let farFrame = 0;
152
- function createFarLayer() {
153
- const layer = new Graphics({ label: "board-far-layer" });
154
- layer.zIndex = Z_FAR_LAYER;
155
- layer.visible = false;
156
- world.addChild(layer);
157
- return layer;
153
+ const lodSizes = /* @__PURE__ */ new WeakMap();
154
+ let nearIds = EMPTY_IDS;
155
+ let plan = null;
156
+ let planInputs = [];
157
+ /** Visible ids in z-order; sorting indices beats comparing ids through the map. */
158
+ function orderedIds(visibleIds) {
159
+ if (visibleIds === null) return idsByOrder;
160
+ const order = new Int32Array(visibleIds.size);
161
+ let length = 0;
162
+ for (const id of visibleIds) {
163
+ const index = orderById.get(id);
164
+ if (index !== void 0) order[length++] = index;
165
+ }
166
+ return Array.from(order.subarray(0, length).sort(), (index) => idsByOrder[index]);
158
167
  }
159
- function requestFarPass(signature) {
160
- farSig = signature;
161
- if (farRunning) farQueued = true;
162
- else runFarPass();
168
+ function lodSize(renderer, item) {
169
+ let size = lodSizes.get(item);
170
+ if (size === void 0) {
171
+ size = renderer.lod?.size?.(item) ?? Math.max(item.frame.width, item.frame.height);
172
+ lodSizes.set(item, size);
173
+ }
174
+ return size;
163
175
  }
164
- function runFarPass() {
165
- if (!farInput) return;
166
- farRunning = true;
167
- farQueued = false;
168
- const { context, getItem, pinnedIds } = farInput;
169
- const ids = visibleFacts(farInput).orderedIds;
170
- const target = farPainted ? farBack : farFront;
171
- const painted = /* @__PURE__ */ new Set();
172
- target.clear();
173
- if (target === farFront) farCovered = painted;
174
- let index = 0;
175
- const step = () => {
176
- farFrame = 0;
177
- const end = Math.min(ids.length, index + FAR_PASS_BATCH);
178
- for (; index < end; index += 1) {
179
- const id = ids[index];
180
- if (pinnedIds.has(id)) continue;
181
- const item = getItem(id);
182
- if (!item) continue;
183
- getRenderer(item, context).renderFar?.(target, item, context);
184
- painted.add(id);
176
+ /**
177
+ * Split the visible cards into live ones and z-ordered chunks of far stand-ins.
178
+ * A card is live while it is legible on screen, until the live budget keeps
179
+ * the on-screen ones nearest the focus; pinned cards and cards without a far
180
+ * stand-in stay live.
181
+ */
182
+ function lodPlan(input) {
183
+ const { visibleIds, context, getItem, pinnedIds, scene } = input;
184
+ const lod = input.lod ?? DEFAULT_BOARD_LOD;
185
+ const step = Math.round(Math.log2(Math.max(context.zoom, 1e-4)) * LOD_ZOOM_STEPS);
186
+ const cell = input.view && input.focus && plan?.limited ? input.view.width / 8 : 0;
187
+ const at = cell ? `${Math.round((input.focus?.x ?? 0) / cell)},${Math.round((input.focus?.y ?? 0) / cell)}` : "";
188
+ const inputs = [
189
+ visibleIds,
190
+ input.structureVersion,
191
+ idSetSignature(pinnedIds),
192
+ step,
193
+ at,
194
+ lod.media,
195
+ lod.text,
196
+ lod.card,
197
+ lod.live
198
+ ];
199
+ const reusable = plan && inputs.every((value, index) => value === planInputs[index]);
200
+ if (plan && reusable && !plan.deferred && (input.gestureActive || plan.scene === scene)) return plan;
201
+ planInputs = inputs;
202
+ const ids = orderedIds(visibleIds);
203
+ const live = /* @__PURE__ */ new Set();
204
+ const near = [];
205
+ for (const id of ids) {
206
+ const item = getItem(id);
207
+ if (!item) continue;
208
+ const renderer = getRenderer(item, context);
209
+ if (pinnedIds.has(id) || !renderer.renderFar) {
210
+ live.add(id);
211
+ continue;
185
212
  }
186
- if (index < ids.length) {
187
- farFrame = requestAnimationFrame(step);
188
- if (target === farFront) options.requestFrame?.();
189
- return;
213
+ if (lodSize(renderer, item) * context.zoom / lod[renderer.lod?.kind ?? "card"] >= (nearIds.has(id) ? .8 : 1)) near.push(item);
214
+ }
215
+ const room = Math.max(0, lod.live - live.size);
216
+ const limited = near.length > room;
217
+ const { focus, view } = input;
218
+ const ranked = focus && (limited || near.length > MATERIALIZE_BATCH) ? near.map(({ id, frame }) => ({
219
+ id,
220
+ offscreen: view && !rectsIntersect(itemBounds(frame), view) ? 1 : 0,
221
+ distance: (frame.x + frame.width / 2 - focus.x) ** 2 + (frame.y + frame.height / 2 - focus.y) ** 2
222
+ })).sort((a, b) => a.offscreen - b.offscreen || a.distance - b.distance) : near;
223
+ let admitted = 0;
224
+ let deferred = false;
225
+ for (const { id } of ranked.slice(0, room)) if (!cards.has(id) && admitted++ >= MATERIALIZE_BATCH) deferred = true;
226
+ else live.add(id);
227
+ if (deferred) options.requestFrame?.();
228
+ nearIds = live;
229
+ const runs = /* @__PURE__ */ new Map();
230
+ const far = /* @__PURE__ */ new Set();
231
+ for (const id of ids) {
232
+ const item = getItem(id);
233
+ if (!item || live.has(id)) continue;
234
+ const order = orderById.get(id) ?? 0;
235
+ const key = `${Math.floor(order / FAR_CHUNK)}:${clippingAncestor(scene, item) ?? ""}`;
236
+ const run = runs.get(key);
237
+ if (run) run.items.push(item);
238
+ else runs.set(key, {
239
+ key,
240
+ order,
241
+ items: [item]
242
+ });
243
+ far.add(id);
244
+ }
245
+ plan = {
246
+ scene,
247
+ live,
248
+ far,
249
+ runs,
250
+ limited,
251
+ deferred
252
+ };
253
+ return plan;
254
+ }
255
+ const plates = /* @__PURE__ */ new Map();
256
+ let paintedPlan = null;
257
+ let paintedSig = "";
258
+ let covered = EMPTY_IDS;
259
+ /**
260
+ * Repaint the plates whose cards changed, within a per-frame budget. A card
261
+ * is covered once a plate draws it; until then it stays live if it already is.
262
+ */
263
+ function paintPlates(input, current) {
264
+ const { context, globalSig, scene } = input;
265
+ if (current === paintedPlan && globalSig === paintedSig) return false;
266
+ let budget = FAR_PAINT_BUDGET;
267
+ let pending = false;
268
+ let changed = false;
269
+ for (const run of current.runs.values()) {
270
+ let plate = plates.get(run.key);
271
+ if (plate?.sig === globalSig && sameItems(plate.items, run.items)) continue;
272
+ if (budget <= 0) {
273
+ pending = true;
274
+ continue;
190
275
  }
191
- if (target === farBack) {
192
- farBack = farFront;
193
- farFront = target;
194
- farBack.visible = false;
195
- farBack.clear();
196
- farCovered = painted;
276
+ budget -= run.items.length;
277
+ if (!plate) {
278
+ plate = {
279
+ graphics: new Graphics({ label: "board-far-plate" }),
280
+ items: [],
281
+ sig: ""
282
+ };
283
+ plates.set(run.key, plate);
284
+ changed = true;
197
285
  }
198
- farFront.visible = farActive;
199
- farPainted = true;
200
- farRunning = false;
201
- options.requestFrame?.();
202
- if (farQueued) runFarPass();
203
- };
204
- step();
286
+ const { graphics } = plate;
287
+ graphics.clear();
288
+ for (const item of run.items) getRenderer(item, context).renderFar?.(graphics, item, context);
289
+ plate.items = run.items;
290
+ plate.sig = globalSig;
291
+ graphics.zIndex = run.order;
292
+ const host = hostFor(run.items[0], scene);
293
+ if (graphics.parent !== host) host.addChild(graphics);
294
+ }
295
+ for (const [key, plate] of plates) {
296
+ if (current.runs.has(key)) continue;
297
+ plates.delete(key);
298
+ plate.graphics.destroy();
299
+ changed = true;
300
+ }
301
+ const next = /* @__PURE__ */ new Set();
302
+ for (const plate of plates.values()) for (const item of plate.items) next.add(item.id);
303
+ covered = next;
304
+ if (pending) options.requestFrame?.();
305
+ else {
306
+ paintedPlan = current;
307
+ paintedSig = globalSig;
308
+ }
309
+ return changed;
205
310
  }
206
- function resetFarLayer() {
207
- cancelAnimationFrame(farFrame);
208
- farFrame = 0;
209
- farRunning = false;
210
- farQueued = false;
211
- farInput = null;
212
- farPainted = false;
213
- farCovered = /* @__PURE__ */ new Set();
214
- farSig = null;
215
- farFront.clear();
216
- farBack.clear();
311
+ function clearPlates() {
312
+ for (const plate of plates.values()) plate.graphics.destroy();
313
+ plates.clear();
314
+ paintedPlan = null;
315
+ paintedSig = "";
316
+ covered = EMPTY_IDS;
317
+ plan = null;
318
+ planInputs = [];
319
+ nearIds = EMPTY_IDS;
217
320
  }
218
321
  function syncLiveStrokes(input) {
219
322
  const { strokes, context, getItem } = input;
@@ -274,28 +377,9 @@ function createBoardScene(options) {
274
377
  world.removeChild(entry.container);
275
378
  entry.renderer.destroy?.(entry.container, context);
276
379
  }
277
- let visibleMemo = null;
278
- function visibleFacts(input) {
279
- const { items, visibleIds, structureVersion, context, getItem } = input;
280
- if (visibleMemo?.ids === visibleIds && visibleMemo.structureVersion === structureVersion) return visibleMemo;
281
- const orderedIds = visibleIds === null ? items.map((item) => item.id) : [...visibleIds].sort((a, b) => (orderById.get(a) ?? 0) - (orderById.get(b) ?? 0));
282
- const unbatched = /* @__PURE__ */ new Set();
283
- for (const id of orderedIds) {
284
- const item = getItem(id);
285
- if (item && !getRenderer(item, context).renderFar) unbatched.add(id);
286
- }
287
- visibleMemo = {
288
- ids: visibleIds,
289
- structureVersion,
290
- signature: visibleIds === null ? 0 : idSetSignature(visibleIds),
291
- orderedIds,
292
- unbatched
293
- };
294
- return visibleMemo;
295
- }
296
380
  function sync(input) {
297
- const { items, scene, context, getItem, visibleIds, pinnedIds, globalSig, structureVersion, geometryVersion, gestureActive } = input;
298
- const resolution = `${textZoomBucket(context.zoom)}:${context.textResolution}`;
381
+ const { items, scene, context, getItem, pinnedIds, globalSig, structureVersion } = input;
382
+ const resolution = `${textZoomBucket(context.zoom)}:${boardCardDetailed(context.zoom)}:${context.textResolution}`;
299
383
  if (resolution !== lastResolution) {
300
384
  lastResolution = resolution;
301
385
  queueResolution(input.focus);
@@ -305,44 +389,14 @@ function createBoardScene(options) {
305
389
  lastStructureVersion = structureVersion;
306
390
  const liveIds = new Set(items.map((item) => item.id));
307
391
  for (const [id, entry] of [...cards]) if (!liveIds.has(id)) recycle(id, entry, context, false);
308
- orderById = new Map(items.map((item, index) => [item.id, index]));
392
+ idsByOrder = items.map((item) => item.id);
393
+ orderById = new Map(idsByOrder.map((id, index) => [id, index]));
309
394
  }
310
- const visibleCount = visibleIds === null ? items.length : visibleIds.size;
311
- const nextFarActive = farActive ? visibleCount > FAR_LAYER_EXIT : visibleCount > FAR_LAYER_ENTER;
312
- const farModeChanged = nextFarActive !== farActive;
313
- farActive = nextFarActive;
314
- farFront.visible = farActive;
315
- if (farActive) {
316
- farInput = input;
317
- const nextFarSig = [
318
- structureVersion,
319
- geometryVersion,
320
- globalSig,
321
- idSetSignature(pinnedIds),
322
- visibleIds === null ? "all" : visibleFacts(input).signature
323
- ].join("|");
324
- if (farSig === null || nextFarSig !== farSig && !gestureActive) requestFarPass(nextFarSig);
325
- } else if (farModeChanged) resetFarLayer();
395
+ const current = lodPlan(input);
396
+ const platesChanged = paintPlates(input, current);
326
397
  const wanted = new Set(pinnedIds);
327
- if (farActive) {
328
- const { orderedIds, unbatched } = visibleFacts(input);
329
- for (const id of unbatched) wanted.add(id);
330
- let retained = 0;
331
- if (!farPainted) {
332
- for (const id of cards.keys()) if ((!visibleIds || visibleIds.has(id)) && !farCovered.has(id) && !wanted.has(id)) {
333
- wanted.add(id);
334
- retained++;
335
- }
336
- }
337
- let budget = Math.max(0, FAR_LIVE_LIMIT - retained);
338
- for (const id of orderedIds) {
339
- if (budget === 0) break;
340
- if (farCovered.has(id) || wanted.has(id)) continue;
341
- wanted.add(id);
342
- budget -= 1;
343
- }
344
- } else if (visibleIds === null) for (const item of items) wanted.add(item.id);
345
- else for (const id of visibleIds) wanted.add(id);
398
+ for (const id of current.live) wanted.add(id);
399
+ for (const id of cards.keys()) if (current.far.has(id) && !covered.has(id)) wanted.add(id);
346
400
  let liveSetChanged = false;
347
401
  for (const [id, entry] of [...cards]) if (!wanted.has(id)) {
348
402
  recycle(id, entry, context, false);
@@ -384,18 +438,27 @@ function createBoardScene(options) {
384
438
  pendingResolution.delete(id);
385
439
  }
386
440
  }
441
+ if (input.view) cullCards(input.view, pinnedIds);
387
442
  refreshResolution(context);
388
443
  if (liveSetChanged) liveVersion += 1;
389
- if (structureChanged || liveSetChanged) syncClips(scene);
444
+ if (structureChanged || liveSetChanged || platesChanged) syncClips(scene);
390
445
  if (context.zoom !== lastZoom) {
391
446
  lastZoom = context.zoom;
392
447
  for (const entry of cards.values()) entry.renderer.zoom?.(entry.container, lastZoom);
393
448
  }
394
449
  const liftChanged = context.liftedIds !== lifted;
395
450
  lifted = context.liftedIds;
396
- if (structureChanged || farModeChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
451
+ if (structureChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
397
452
  syncLiveStrokes(input);
398
453
  }
454
+ function cullCards(view, pinnedIds) {
455
+ for (const [id, entry] of cards) {
456
+ const culled = !pinnedIds.has(id) && !rectsIntersect(itemBounds(entry.item.frame), view);
457
+ if (entry.container.culled === culled) continue;
458
+ entry.container.culled = culled;
459
+ if (!culled && entry.renderer.tick) tickingIds.add(id);
460
+ }
461
+ }
399
462
  function queueResolution(focus) {
400
463
  const ids = [...cards].filter(([, entry]) => entry.renderer.resolution);
401
464
  if (focus) {
@@ -421,7 +484,7 @@ function createBoardScene(options) {
421
484
  let next = null;
422
485
  for (const id of tickingIds) {
423
486
  const entry = cards.get(id);
424
- const wait = entry?.container.visible ? entry.renderer.tick?.(entry.container, clock) ?? null : null;
487
+ const wait = entry?.container.visible && !entry.container.culled ? entry.renderer.tick?.(entry.container, clock) ?? null : null;
425
488
  if (wait === null) tickingIds.delete(id);
426
489
  else next = next === null ? wait : Math.min(next, wait);
427
490
  }
@@ -438,6 +501,14 @@ function createBoardScene(options) {
438
501
  }
439
502
  world.sortChildren();
440
503
  }
504
+ function redrawText() {
505
+ const redraw = (container) => {
506
+ for (const child of container.children) if (child instanceof Text) child.style.update();
507
+ else redraw(child);
508
+ };
509
+ for (const entry of cards.values()) redraw(entry.container);
510
+ for (const pool of pools.values()) for (const container of pool) redraw(container);
511
+ }
441
512
  function destroy(context) {
442
513
  for (const entry of cards.values()) entry.renderer.destroy?.(entry.container, context);
443
514
  cards.clear();
@@ -447,14 +518,13 @@ function createBoardScene(options) {
447
518
  heldKeys.clear();
448
519
  holders.clear();
449
520
  orderById = /* @__PURE__ */ new Map();
521
+ idsByOrder = [];
522
+ clearPlates();
450
523
  for (const entry of clips.values()) entry.group.destroy({ children: true });
451
524
  clips.clear();
452
525
  animatedIds.clear();
453
526
  tickingIds.clear();
454
527
  for (const id of [...liveStrokes.keys()]) removeLiveStroke(id, context);
455
- resetFarLayer();
456
- farActive = false;
457
- visibleMemo = null;
458
528
  lastStructureVersion = -1;
459
529
  lastZoom = NaN;
460
530
  lastResolution = "";
@@ -474,10 +544,15 @@ function createBoardScene(options) {
474
544
  return liveVersion;
475
545
  },
476
546
  liveIds: () => new Set(cards.keys()),
547
+ counts: () => ({
548
+ live: cards.size,
549
+ far: covered.size
550
+ }),
477
551
  get animated() {
478
552
  return animatedIds.size > 0;
479
553
  },
480
554
  tick,
555
+ redrawText,
481
556
  destroy
482
557
  };
483
558
  }
@@ -11,6 +11,7 @@ import { BoardPointerEvent } from "../editor/interaction.js";
11
11
  import { BoardEditor } from "../editor/editor.js";
12
12
  import "../editor/index.js";
13
13
  import { BoardRenderStyle } from "../render/style.js";
14
+ import { BoardLod } from "../render/lod.js";
14
15
  import { BoardCardRendererResolver, BoardRenderLabels, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
15
16
  import "../render/index.js";
16
17
  import { BoardAssetManager } from "./assets.js";
@@ -59,6 +60,8 @@ type BoardStageOptions = {
59
60
  onLiveChange?: (ids: ReadonlySet<string>) => void;
60
61
  safeRect?: () => Rect;
61
62
  scriptCapabilities?: Record<string, (id: string, input: unknown) => Promise<unknown>>;
63
+ /** Level-of-detail thresholds and live budget; read every frame. */
64
+ lod?: () => BoardLod | undefined;
62
65
  };
63
66
  type BoardStage = {
64
67
  readonly ready: Promise<boolean>;
@@ -80,8 +83,16 @@ type BoardStage = {
80
83
  }): void;
81
84
  isMaterialized(id: string): boolean;
82
85
  toWorld(clientX: number, clientY: number): WorldPoint;
86
+ stats(): BoardStageStats;
83
87
  destroy(): void;
84
88
  };
89
+ type BoardStageStats = {
90
+ live: number;
91
+ far: number;
92
+ textures: number;
93
+ cooling: number;
94
+ textureBytes: number;
95
+ };
85
96
  declare function mountBoardStage(host: HTMLElement, options: BoardStageOptions): BoardStage;
86
97
  //#endregion
87
- export { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage };
98
+ export { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, mountBoardStage };