@neta-art/cohub 12.1.1 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +85 -8
  2. package/dist/board/editor/clipboard.js +1 -1
  3. package/dist/board/editor/creation.js +1 -1
  4. package/dist/board/editor/document.d.ts +3 -1
  5. package/dist/board/editor/document.js +8 -6
  6. package/dist/board/editor/editor.d.ts +8 -3
  7. package/dist/board/editor/editor.js +8 -2
  8. package/dist/board/editor/index.d.ts +3 -3
  9. package/dist/board/editor/index.js +2 -2
  10. package/dist/board/editor/interaction.d.ts +23 -2
  11. package/dist/board/editor/interaction.js +93 -4
  12. package/dist/board/editor/query.d.ts +1 -1
  13. package/dist/board/editor/query.js +3 -7
  14. package/dist/board/editor/selection.js +1 -1
  15. package/dist/board/editor/snapshots.js +5 -57
  16. package/dist/board/editor/tool.d.ts +4 -1
  17. package/dist/board/editor/tool.js +6 -1
  18. package/dist/board/export/index.d.ts +2 -2
  19. package/dist/board/export/index.js +1 -1
  20. package/dist/board/export/node/fonts.js +4 -0
  21. package/dist/board/export/node/index.d.ts +2 -0
  22. package/dist/board/export/node/index.js +8 -1
  23. package/dist/board/export/node/scripts.js +1 -1
  24. package/dist/board/export/scene.d.ts +2 -2
  25. package/dist/board/export/scene.js +2 -2
  26. package/dist/board/model/asset-key.d.ts +24 -0
  27. package/dist/board/model/asset-key.js +104 -0
  28. package/dist/board/model/export-assets.d.ts +7 -3
  29. package/dist/board/model/export-assets.js +34 -13
  30. package/dist/board/model/index.d.ts +9 -7
  31. package/dist/board/model/index.js +22 -20
  32. package/dist/board/model/items/create.js +3 -3
  33. package/dist/board/model/items/definition.d.ts +9 -1
  34. package/dist/board/model/items/index.d.ts +2 -2
  35. package/dist/board/model/items/registry.d.ts +2 -1
  36. package/dist/board/model/items/registry.js +1 -0
  37. package/dist/board/model/media-sizes.d.ts +28 -0
  38. package/dist/board/model/media-sizes.js +64 -0
  39. package/dist/board/model/palette.d.ts +1 -0
  40. package/dist/board/model/palette.js +20 -130
  41. package/dist/board/model/sampling.js +1 -1
  42. package/dist/board/model/text-metrics.d.ts +28 -3
  43. package/dist/board/model/text-metrics.js +36 -2
  44. package/dist/board/model/themes.d.ts +495 -0
  45. package/dist/board/model/themes.js +500 -0
  46. package/dist/board/render/card-tag.js +2 -2
  47. package/dist/board/render/index.d.ts +11 -7
  48. package/dist/board/render/index.js +9 -5
  49. package/dist/board/render/labels.d.ts +15 -0
  50. package/dist/board/render/labels.js +14 -0
  51. package/dist/board/render/lod.d.ts +30 -0
  52. package/dist/board/render/lod.js +33 -0
  53. package/dist/board/render/media-interaction.d.ts +5 -2
  54. package/dist/board/render/media-interaction.js +15 -3
  55. package/dist/board/render/palette.js +2 -25
  56. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  57. package/dist/board/render/renderers/audio-card-renderer.js +3 -2
  58. package/dist/board/render/renderers/board-renderer-registry.d.ts +22 -1
  59. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  60. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  61. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  62. package/dist/board/render/renderers/far-plate.d.ts +14 -1
  63. package/dist/board/render/renderers/far-plate.js +31 -8
  64. package/dist/board/render/renderers/file-card-renderer.js +49 -31
  65. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +34 -8
  67. package/dist/board/render/renderers/model-card-renderer.js +6 -6
  68. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  69. package/dist/board/render/renderers/task-card-renderer.js +9 -10
  70. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  71. package/dist/board/render/renderers/video-card-renderer.js +5 -4
  72. package/dist/board/render/script-export.js +1 -1
  73. package/dist/board/render/script-host.js +3 -3
  74. package/dist/board/render/style.d.ts +8 -2
  75. package/dist/board/render/style.js +8 -2
  76. package/dist/board/render/surface.d.ts +5 -0
  77. package/dist/board/render/surface.js +13 -3
  78. package/dist/board/render/text-edit.d.ts +32 -0
  79. package/dist/board/render/text-edit.js +134 -0
  80. package/dist/board/render/text-fit.d.ts +1 -3
  81. package/dist/board/render/text-fit.js +6 -49
  82. package/dist/board/render/text-measurement.d.ts +25 -1
  83. package/dist/board/render/text-measurement.js +157 -19
  84. package/dist/board/render/text-resolution.d.ts +1 -6
  85. package/dist/board/render/text-resolution.js +1 -10
  86. package/dist/board/render/theme.d.ts +15 -0
  87. package/dist/board/render/theme.js +15 -0
  88. package/dist/board/replica/index.d.ts +4 -2
  89. package/dist/board/replica/index.js +28 -17
  90. package/dist/board/replica/indexeddb.d.ts +16 -0
  91. package/dist/board/replica/indexeddb.js +155 -0
  92. package/dist/board/stage/assets.d.ts +28 -5
  93. package/dist/board/stage/assets.js +293 -102
  94. package/dist/board/stage/bench.d.ts +35 -0
  95. package/dist/board/stage/bench.js +153 -0
  96. package/dist/board/stage/device.d.ts +41 -0
  97. package/dist/board/stage/device.js +62 -0
  98. package/dist/board/stage/frame-budget.d.ts +19 -0
  99. package/dist/board/stage/frame-budget.js +69 -0
  100. package/dist/board/stage/idle.d.ts +4 -0
  101. package/dist/board/stage/idle.js +26 -0
  102. package/dist/board/stage/index.d.ts +11 -7
  103. package/dist/board/stage/index.js +7 -4
  104. package/dist/board/stage/lru.d.ts +13 -9
  105. package/dist/board/stage/lru.js +38 -20
  106. package/dist/board/stage/presses.d.ts +21 -0
  107. package/dist/board/stage/presses.js +66 -0
  108. package/dist/board/stage/scene.d.ts +15 -1
  109. package/dist/board/stage/scene.js +248 -141
  110. package/dist/board/stage/stage.d.ts +28 -2
  111. package/dist/board/stage/stage.js +404 -112
  112. package/dist/board/stage/texture-pool.d.ts +10 -1
  113. package/dist/board/stage/texture-pool.js +72 -2
  114. package/dist/chunks/environment.d.ts +6 -0
  115. package/dist/chunks/http.d.ts +9 -1
  116. package/dist/chunks/http.js +791 -4
  117. package/dist/index.d.ts +52 -1
  118. package/dist/index.js +202 -862
  119. package/dist/media.d.ts +3 -27
  120. package/dist/media.js +3 -82
  121. package/dist/protocol/dist/board-constants.js +3 -3
  122. package/dist/protocol/dist/board-layout.d.ts +17 -3
  123. package/dist/protocol/dist/board-layout.js +68 -11
  124. package/dist/protocol/dist/board-model.d.ts +15 -1
  125. package/dist/protocol/dist/board-model.js +12 -2
  126. package/dist/protocol/dist/board-patch.js +1 -1
  127. package/dist/protocol/dist/board-schema.d.ts +2 -1
  128. package/dist/protocol/dist/board-schema.js +4 -1
  129. package/dist/protocol/dist/core/media.d.ts +35 -1
  130. package/dist/protocol/dist/core/media.js +127 -1
  131. package/dist/protocol/dist/index.d.ts +3 -3
  132. package/package.json +4 -2
  133. package/dist/board/model/image-key.d.ts +0 -9
  134. package/dist/board/model/image-key.js +0 -33
@@ -1,15 +1,14 @@
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, boardLodRatio, boardLodSize } 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 { fixedBoardFrameBudget } from "./frame-budget.js";
8
+ import { Graphics, Text } from "pixi.js";
6
9
  //#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;
11
- const RESOLUTION_BATCH = 32;
12
- const Z_FAR_LAYER = -1;
10
+ const FAR_CHUNK = 256;
11
+ const LOD_ZOOM_STEPS = 8;
13
12
  const Z_LIFTED = 1e9;
14
13
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
15
14
  const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
@@ -24,6 +23,9 @@ function idSetSignature(ids) {
24
23
  }
25
24
  return hash;
26
25
  }
26
+ function sameItems(a, b) {
27
+ return a.length === b.length && a.every((item, index) => item === b[index]);
28
+ }
27
29
  function createBoardScene(options) {
28
30
  const { world, overlay, getRenderer } = options;
29
31
  world.sortableChildren = true;
@@ -36,14 +38,15 @@ function createBoardScene(options) {
36
38
  const heldKeys = /* @__PURE__ */ new Map();
37
39
  const holders = /* @__PURE__ */ new Map();
38
40
  let orderById = /* @__PURE__ */ new Map();
39
- let farSig = null;
40
- let farActive = false;
41
+ let idsByOrder = [];
41
42
  let lastStructureVersion = -1;
42
43
  let lastZoom = NaN;
43
44
  let lastResolution = "";
44
45
  const pendingResolution = /* @__PURE__ */ new Set();
45
46
  let liveVersion = 0;
46
47
  let lifted = /* @__PURE__ */ new Set();
48
+ const fixedBudget = fixedBoardFrameBudget();
49
+ let budget = fixedBudget;
47
50
  const liveStrokes = /* @__PURE__ */ new Map();
48
51
  function setHeldKey(context, cardId, item) {
49
52
  const desiredKey = item ? context.assetKey(item) : null;
@@ -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,188 @@ 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 = boardLodSize(renderer, item);
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 (boardLodRatio(renderer, lodSize(renderer, item), context.zoom, lod) >= (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 allowance = budget.allow("materialize");
218
+ const { focus, view } = input;
219
+ const ranked = focus && (limited || near.length > allowance) ? near.map(({ id, frame }) => ({
220
+ id,
221
+ offscreen: view && !rectsIntersect(itemBounds(frame), view) ? 1 : 0,
222
+ distance: (frame.x + frame.width / 2 - focus.x) ** 2 + (frame.y + frame.height / 2 - focus.y) ** 2
223
+ })).sort((a, b) => a.offscreen - b.offscreen || a.distance - b.distance) : near;
224
+ let admitted = 0;
225
+ let deferred = false;
226
+ for (const { id } of ranked.slice(0, room)) if (!cards.has(id) && admitted++ >= allowance) deferred = true;
227
+ else live.add(id);
228
+ if (deferred) options.requestFrame?.();
229
+ nearIds = live;
230
+ const runs = /* @__PURE__ */ new Map();
231
+ const far = /* @__PURE__ */ new Set();
232
+ for (const id of ids) {
233
+ const item = getItem(id);
234
+ if (!item || live.has(id)) continue;
235
+ const order = orderById.get(id) ?? 0;
236
+ const key = `${Math.floor(order / FAR_CHUNK)}:${clippingAncestor(scene, item) ?? ""}`;
237
+ const run = runs.get(key);
238
+ if (run) run.items.push(item);
239
+ else runs.set(key, {
240
+ key,
241
+ order,
242
+ items: [item]
243
+ });
244
+ far.add(id);
245
+ }
246
+ plan = {
247
+ scene,
248
+ live,
249
+ far,
250
+ runs,
251
+ limited,
252
+ deferred
253
+ };
254
+ return plan;
255
+ }
256
+ const plates = /* @__PURE__ */ new Map();
257
+ let paintedPlan = null;
258
+ let paintedSig = "";
259
+ let platesPending = false;
260
+ let covered = EMPTY_IDS;
261
+ /**
262
+ * Repaint the plates whose cards changed, within a per-frame budget. A card
263
+ * is covered once a plate draws it; until then it stays live if it already is.
264
+ */
265
+ function paintPlates(input, current) {
266
+ const { context, globalSig, scene } = input;
267
+ if (current === paintedPlan && globalSig === paintedSig) return false;
268
+ const start = performance.now();
269
+ const allowance = budget.allow("plate");
270
+ let painted = 0;
271
+ let pending = false;
272
+ let changed = false;
273
+ for (const run of current.runs.values()) {
274
+ let plate = plates.get(run.key);
275
+ if (plate?.sig === globalSig && sameItems(plate.items, run.items)) continue;
276
+ if (painted >= allowance) {
277
+ pending = true;
278
+ continue;
190
279
  }
191
- if (target === farBack) {
192
- farBack = farFront;
193
- farFront = target;
194
- farBack.visible = false;
195
- farBack.clear();
196
- farCovered = painted;
280
+ painted += run.items.length;
281
+ if (!plate) {
282
+ plate = {
283
+ graphics: new Graphics({ label: "board-far-plate" }),
284
+ items: [],
285
+ sig: ""
286
+ };
287
+ plates.set(run.key, plate);
288
+ changed = true;
197
289
  }
198
- farFront.visible = farActive;
199
- farPainted = true;
200
- farRunning = false;
201
- options.requestFrame?.();
202
- if (farQueued) runFarPass();
203
- };
204
- step();
290
+ const { graphics } = plate;
291
+ graphics.clear();
292
+ for (const item of run.items) getRenderer(item, context).renderFar?.(graphics, item, context);
293
+ plate.items = run.items;
294
+ plate.sig = globalSig;
295
+ graphics.zIndex = run.order;
296
+ const host = hostFor(run.items[0], scene);
297
+ if (graphics.parent !== host) host.addChild(graphics);
298
+ }
299
+ const paintMs = performance.now() - start;
300
+ for (const [key, plate] of plates) {
301
+ if (current.runs.has(key)) continue;
302
+ plates.delete(key);
303
+ plate.graphics.destroy();
304
+ changed = true;
305
+ }
306
+ const next = /* @__PURE__ */ new Set();
307
+ for (const plate of plates.values()) for (const item of plate.items) next.add(item.id);
308
+ covered = next;
309
+ budget.spend("plate", painted, paintMs);
310
+ platesPending = pending;
311
+ if (pending) options.requestFrame?.();
312
+ else {
313
+ paintedPlan = current;
314
+ paintedSig = globalSig;
315
+ }
316
+ return changed;
317
+ }
318
+ function repaintFar(keys, assetKey) {
319
+ for (const plate of plates.values()) {
320
+ if (!plate.items.some((item) => keys.has(assetKey(item) ?? ""))) continue;
321
+ plate.sig = "";
322
+ paintedPlan = null;
323
+ }
205
324
  }
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();
325
+ function clearPlates() {
326
+ for (const plate of plates.values()) plate.graphics.destroy();
327
+ plates.clear();
328
+ paintedPlan = null;
329
+ paintedSig = "";
330
+ platesPending = false;
331
+ covered = EMPTY_IDS;
332
+ plan = null;
333
+ planInputs = [];
334
+ nearIds = EMPTY_IDS;
217
335
  }
218
336
  function syncLiveStrokes(input) {
219
337
  const { strokes, context, getItem } = input;
@@ -274,28 +392,11 @@ function createBoardScene(options) {
274
392
  world.removeChild(entry.container);
275
393
  entry.renderer.destroy?.(entry.container, context);
276
394
  }
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
395
  function sync(input) {
297
- const { items, scene, context, getItem, visibleIds, pinnedIds, globalSig, structureVersion, geometryVersion, gestureActive } = input;
298
- const resolution = `${textZoomBucket(context.zoom)}:${context.textResolution}`;
396
+ const { items, scene, context, getItem, pinnedIds, globalSig, structureVersion } = input;
397
+ fixedBudget.reset();
398
+ budget = input.budget ?? fixedBudget;
399
+ const resolution = `${textZoomBucket(context.zoom)}:${boardCardDetailed(context.zoom)}:${context.textResolution}`;
299
400
  if (resolution !== lastResolution) {
300
401
  lastResolution = resolution;
301
402
  queueResolution(input.focus);
@@ -305,45 +406,17 @@ function createBoardScene(options) {
305
406
  lastStructureVersion = structureVersion;
306
407
  const liveIds = new Set(items.map((item) => item.id));
307
408
  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]));
409
+ idsByOrder = items.map((item) => item.id);
410
+ orderById = new Map(idsByOrder.map((id, index) => [id, index]));
309
411
  }
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();
412
+ const current = lodPlan(input);
413
+ const platesChanged = paintPlates(input, current);
326
414
  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);
415
+ for (const id of current.live) wanted.add(id);
416
+ for (const id of cards.keys()) if (current.far.has(id) && !covered.has(id)) wanted.add(id);
346
417
  let liveSetChanged = false;
418
+ let made = 0;
419
+ let making = 0;
347
420
  for (const [id, entry] of [...cards]) if (!wanted.has(id)) {
348
421
  recycle(id, entry, context, false);
349
422
  liveSetChanged = true;
@@ -360,7 +433,10 @@ function createBoardScene(options) {
360
433
  entry = void 0;
361
434
  }
362
435
  if (!entry) {
436
+ const start = performance.now();
363
437
  materialize(item, context, globalSig, scene);
438
+ making += performance.now() - start;
439
+ made += 1;
364
440
  liveSetChanged = true;
365
441
  continue;
366
442
  }
@@ -384,18 +460,28 @@ function createBoardScene(options) {
384
460
  pendingResolution.delete(id);
385
461
  }
386
462
  }
463
+ budget.spend("materialize", made, making);
464
+ if (input.view) cullCards(input.view, pinnedIds);
387
465
  refreshResolution(context);
388
466
  if (liveSetChanged) liveVersion += 1;
389
- if (structureChanged || liveSetChanged) syncClips(scene);
467
+ if (structureChanged || liveSetChanged || platesChanged) syncClips(scene);
390
468
  if (context.zoom !== lastZoom) {
391
469
  lastZoom = context.zoom;
392
470
  for (const entry of cards.values()) entry.renderer.zoom?.(entry.container, lastZoom);
393
471
  }
394
472
  const liftChanged = context.liftedIds !== lifted;
395
473
  lifted = context.liftedIds;
396
- if (structureChanged || farModeChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
474
+ if (structureChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
397
475
  syncLiveStrokes(input);
398
476
  }
477
+ function cullCards(view, pinnedIds) {
478
+ for (const [id, entry] of cards) {
479
+ const culled = !pinnedIds.has(id) && !rectsIntersect(itemBounds(entry.item.frame), view);
480
+ if (entry.container.culled === culled) continue;
481
+ entry.container.culled = culled;
482
+ if (!culled && entry.renderer.tick) tickingIds.add(id);
483
+ }
484
+ }
399
485
  function queueResolution(focus) {
400
486
  const ids = [...cards].filter(([, entry]) => entry.renderer.resolution);
401
487
  if (focus) {
@@ -406,22 +492,27 @@ function createBoardScene(options) {
406
492
  for (const [id] of ids) pendingResolution.add(id);
407
493
  }
408
494
  function refreshResolution(context) {
409
- let budget = RESOLUTION_BATCH;
495
+ if (pendingResolution.size === 0) return;
496
+ const start = performance.now();
497
+ const allowance = budget.allow("resolution");
498
+ let refreshed = 0;
410
499
  for (const id of pendingResolution) {
411
- if (budget-- === 0) break;
500
+ if (refreshed === allowance) break;
412
501
  pendingResolution.delete(id);
413
502
  const entry = cards.get(id);
414
503
  if (!entry) continue;
415
504
  entry.renderer.resolution?.(entry.container, entry.item, context);
416
505
  if (entry.renderer.tick) tickingIds.add(id);
506
+ refreshed += 1;
417
507
  }
508
+ budget.spend("resolution", refreshed, performance.now() - start);
418
509
  if (pendingResolution.size) options.requestFrame?.();
419
510
  }
420
511
  function tick(clock) {
421
512
  let next = null;
422
513
  for (const id of tickingIds) {
423
514
  const entry = cards.get(id);
424
- const wait = entry?.container.visible ? entry.renderer.tick?.(entry.container, clock) ?? null : null;
515
+ const wait = entry?.container.visible && !entry.container.culled ? entry.renderer.tick?.(entry.container, clock) ?? null : null;
425
516
  if (wait === null) tickingIds.delete(id);
426
517
  else next = next === null ? wait : Math.min(next, wait);
427
518
  }
@@ -438,6 +529,14 @@ function createBoardScene(options) {
438
529
  }
439
530
  world.sortChildren();
440
531
  }
532
+ function redrawText() {
533
+ const redraw = (container) => {
534
+ for (const child of container.children) if (child instanceof Text) child.style.update();
535
+ else redraw(child);
536
+ };
537
+ for (const entry of cards.values()) redraw(entry.container);
538
+ for (const pool of pools.values()) for (const container of pool) redraw(container);
539
+ }
441
540
  function destroy(context) {
442
541
  for (const entry of cards.values()) entry.renderer.destroy?.(entry.container, context);
443
542
  cards.clear();
@@ -447,14 +546,13 @@ function createBoardScene(options) {
447
546
  heldKeys.clear();
448
547
  holders.clear();
449
548
  orderById = /* @__PURE__ */ new Map();
549
+ idsByOrder = [];
550
+ clearPlates();
450
551
  for (const entry of clips.values()) entry.group.destroy({ children: true });
451
552
  clips.clear();
452
553
  animatedIds.clear();
453
554
  tickingIds.clear();
454
555
  for (const id of [...liveStrokes.keys()]) removeLiveStroke(id, context);
455
- resetFarLayer();
456
- farActive = false;
457
- visibleMemo = null;
458
556
  lastStructureVersion = -1;
459
557
  lastZoom = NaN;
460
558
  lastResolution = "";
@@ -470,14 +568,23 @@ function createBoardScene(options) {
470
568
  } : null;
471
569
  },
472
570
  holders: (key) => holders.get(key) ?? EMPTY_IDS,
571
+ repaintFar,
473
572
  get liveVersion() {
474
573
  return liveVersion;
475
574
  },
476
575
  liveIds: () => new Set(cards.keys()),
576
+ counts: () => ({
577
+ live: cards.size,
578
+ far: covered.size
579
+ }),
580
+ get settling() {
581
+ return Boolean(plan?.deferred) || platesPending || pendingResolution.size > 0;
582
+ },
477
583
  get animated() {
478
584
  return animatedIds.size > 0;
479
585
  },
480
586
  tick,
587
+ redrawText,
481
588
  destroy
482
589
  };
483
590
  }
@@ -7,13 +7,15 @@ import { BoardScene, BoardSceneItem } from "../model/scene.js";
7
7
  import { BoardShapeColors } from "../model/palette.js";
8
8
  import { BoardAssetSource } from "../model/media-playback.js";
9
9
  import "../model/index.js";
10
- import { BoardPointerEvent } from "../editor/interaction.js";
10
+ import { BoardActivation, 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";
18
+ import { BoardStagePress } from "./presses.js";
17
19
  import { BoardLiveStroke } from "./scene.js";
18
20
  import { BoardItemView } from "./views.js";
19
21
  import { Graphics, Renderer } from "pixi.js";
@@ -50,6 +52,10 @@ type BoardStageOptions = {
50
52
  scene: BoardScene;
51
53
  }) => void;
52
54
  onPointer?: (phase: BoardStagePointerPhase, event: PointerEvent, input: BoardPointerEvent) => boolean | undefined;
55
+ onActivate?: (item: BoardSceneItem, activation: BoardActivation) => void;
56
+ onLongPress?: (item: BoardSceneItem, press: BoardStagePress) => void;
57
+ /** A press on a card key (`BoardCardRenderer.hitAt`); return false to leave it to the editor. */
58
+ onHit?: (item: BoardSceneItem, action: string, press: BoardStagePress) => boolean | undefined;
53
59
  onDoubleClick?: (item: BoardSceneItem | null, point: WorldPoint) => boolean | undefined;
54
60
  onSurfaceChange?: (size: {
55
61
  width: number;
@@ -59,6 +65,8 @@ type BoardStageOptions = {
59
65
  onLiveChange?: (ids: ReadonlySet<string>) => void;
60
66
  safeRect?: () => Rect;
61
67
  scriptCapabilities?: Record<string, (id: string, input: unknown) => Promise<unknown>>;
68
+ lod?: () => BoardLod | undefined;
69
+ prefetch?: number;
62
70
  };
63
71
  type BoardStage = {
64
72
  readonly ready: Promise<boolean>;
@@ -80,8 +88,26 @@ type BoardStage = {
80
88
  }): void;
81
89
  isMaterialized(id: string): boolean;
82
90
  toWorld(clientX: number, clientY: number): WorldPoint;
91
+ stats(): BoardStageStats;
83
92
  destroy(): void;
84
93
  };
94
+ type BoardStageStats = {
95
+ live: number;
96
+ far: number;
97
+ textures: number;
98
+ cooling: number;
99
+ textureBytes: number;
100
+ loading: number;
101
+ prefetching: number;
102
+ waiting: number;
103
+ settling: boolean;
104
+ frame: {
105
+ p50: number;
106
+ p95: number;
107
+ max: number;
108
+ };
109
+ interval: number;
110
+ };
85
111
  declare function mountBoardStage(host: HTMLElement, options: BoardStageOptions): BoardStage;
86
112
  //#endregion
87
- export { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage };
113
+ export { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, mountBoardStage };