@neta-art/cohub 12.2.0 → 12.4.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 (93) hide show
  1. package/README.md +87 -3
  2. package/dist/board/editor/context.d.ts +2 -0
  3. package/dist/board/editor/document.d.ts +3 -1
  4. package/dist/board/editor/document.js +8 -6
  5. package/dist/board/editor/editor.d.ts +7 -3
  6. package/dist/board/editor/editor.js +8 -1
  7. package/dist/board/editor/index.d.ts +3 -3
  8. package/dist/board/editor/index.js +2 -2
  9. package/dist/board/editor/interaction.d.ts +23 -2
  10. package/dist/board/editor/interaction.js +99 -5
  11. package/dist/board/editor/query.d.ts +1 -1
  12. package/dist/board/editor/query.js +3 -7
  13. package/dist/board/editor/selection.js +13 -1
  14. package/dist/board/editor/tool.d.ts +4 -1
  15. package/dist/board/editor/tool.js +6 -1
  16. package/dist/board/model/asset-key.d.ts +9 -1
  17. package/dist/board/model/asset-key.js +27 -7
  18. package/dist/board/model/index.d.ts +5 -5
  19. package/dist/board/model/index.js +4 -4
  20. package/dist/board/model/items/create.js +1 -1
  21. package/dist/board/model/items/definition.d.ts +9 -1
  22. package/dist/board/model/items/index.d.ts +2 -2
  23. package/dist/board/model/items/registry.d.ts +2 -1
  24. package/dist/board/model/items/registry.js +1 -0
  25. package/dist/board/model/text-metrics.d.ts +21 -1
  26. package/dist/board/model/text-metrics.js +24 -1
  27. package/dist/board/model/themes.d.ts +49 -42
  28. package/dist/board/model/themes.js +49 -44
  29. package/dist/board/render/index.d.ts +8 -6
  30. package/dist/board/render/index.js +7 -5
  31. package/dist/board/render/labels.d.ts +15 -0
  32. package/dist/board/render/labels.js +14 -0
  33. package/dist/board/render/lod.d.ts +9 -1
  34. package/dist/board/render/lod.js +7 -1
  35. package/dist/board/render/media-interaction.d.ts +5 -2
  36. package/dist/board/render/media-interaction.js +14 -2
  37. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  38. package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
  39. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  40. package/dist/board/render/renderers/far-plate.d.ts +4 -1
  41. package/dist/board/render/renderers/far-plate.js +5 -1
  42. package/dist/board/render/renderers/file-card-renderer.js +44 -24
  43. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  44. package/dist/board/render/renderers/image-card-renderer.js +2 -2
  45. package/dist/board/render/renderers/model-card-renderer.js +2 -2
  46. package/dist/board/render/renderers/task-card-renderer.js +2 -2
  47. package/dist/board/render/renderers/video-card-renderer.js +2 -2
  48. package/dist/board/render/style.d.ts +1 -0
  49. package/dist/board/render/style.js +3 -1
  50. package/dist/board/render/text-edit.d.ts +32 -0
  51. package/dist/board/render/text-edit.js +134 -0
  52. package/dist/board/render/text-fit.d.ts +1 -3
  53. package/dist/board/render/text-fit.js +4 -43
  54. package/dist/board/render/text-measurement.d.ts +12 -2
  55. package/dist/board/render/text-measurement.js +55 -10
  56. package/dist/board/replica/index.d.ts +4 -2
  57. package/dist/board/replica/index.js +34 -21
  58. package/dist/board/replica/indexeddb.d.ts +16 -0
  59. package/dist/board/replica/indexeddb.js +155 -0
  60. package/dist/board/stage/assets.d.ts +16 -4
  61. package/dist/board/stage/assets.js +155 -30
  62. package/dist/board/stage/bench.d.ts +35 -0
  63. package/dist/board/stage/bench.js +153 -0
  64. package/dist/board/stage/device.d.ts +41 -0
  65. package/dist/board/stage/device.js +62 -0
  66. package/dist/board/stage/frame-budget.d.ts +19 -0
  67. package/dist/board/stage/frame-budget.js +69 -0
  68. package/dist/board/stage/idle.d.ts +4 -0
  69. package/dist/board/stage/idle.js +26 -0
  70. package/dist/board/stage/index.d.ts +9 -5
  71. package/dist/board/stage/index.js +5 -2
  72. package/dist/board/stage/presses.d.ts +21 -0
  73. package/dist/board/stage/presses.js +66 -0
  74. package/dist/board/stage/scene.d.ts +4 -0
  75. package/dist/board/stage/scene.js +45 -13
  76. package/dist/board/stage/stage.d.ts +21 -2
  77. package/dist/board/stage/stage.js +309 -89
  78. package/dist/board/stage/texture-pool.d.ts +6 -4
  79. package/dist/board/stage/texture-pool.js +42 -8
  80. package/dist/board/stage/views.d.ts +1 -1
  81. package/dist/board/stage/views.js +2 -2
  82. package/dist/chunks/environment.d.ts +6 -0
  83. package/dist/chunks/http.d.ts +9 -1
  84. package/dist/chunks/http.js +791 -4
  85. package/dist/index.d.ts +52 -1
  86. package/dist/index.js +65 -733
  87. package/dist/protocol/dist/board-model.d.ts +15 -1
  88. package/dist/protocol/dist/board-model.js +12 -2
  89. package/dist/protocol/dist/board-schema.d.ts +2 -1
  90. package/dist/protocol/dist/board-schema.js +4 -1
  91. package/dist/protocol/dist/core/media.js +4 -0
  92. package/dist/protocol/dist/index.d.ts +2 -2
  93. package/package.json +2 -1
@@ -0,0 +1,69 @@
1
+ //#region src/board/stage/frame-budget.ts
2
+ const INITIAL = {
3
+ materialize: 48,
4
+ plate: 2400,
5
+ resolution: 32
6
+ };
7
+ const KINDS = Object.keys(INITIAL);
8
+ const MIN_SHARE = 1 / 8;
9
+ const LATE_FRAMES = 1.5;
10
+ const MOVING_SHARE = .25;
11
+ const RESTING_SHARE = .6;
12
+ const COST_SMOOTHING = .3;
13
+ const DEFAULT_INTERVAL = 1e3 / 60;
14
+ const MIN_INTERVAL = 4;
15
+ const INTERVAL_DRIFT = 1.01;
16
+ const perKind = (value) => ({
17
+ materialize: value,
18
+ plate: value,
19
+ resolution: value
20
+ });
21
+ function fixedBoardFrameBudget() {
22
+ let done = perKind(0);
23
+ return {
24
+ allow: (kind) => Math.max(0, INITIAL[kind] - done[kind]),
25
+ spend(kind, units) {
26
+ done[kind] += units;
27
+ },
28
+ reset() {
29
+ done = perKind(0);
30
+ }
31
+ };
32
+ }
33
+ function createBoardFrameClock(now = () => performance.now()) {
34
+ let interval = DEFAULT_INTERVAL;
35
+ let previous = NaN;
36
+ let deadline = Number.POSITIVE_INFINITY;
37
+ let late = false;
38
+ const cost = perKind(0);
39
+ for (const kind of KINDS) cost[kind] = DEFAULT_INTERVAL * MOVING_SHARE / INITIAL[kind];
40
+ const last = perKind(0);
41
+ let done = perKind(0);
42
+ return {
43
+ get interval() {
44
+ return interval;
45
+ },
46
+ begin(frameTime, { moving, chained }) {
47
+ const delta = frameTime - previous;
48
+ previous = frameTime;
49
+ late = moving && chained && delta > interval * LATE_FRAMES;
50
+ if (chained && delta >= MIN_INTERVAL) interval = Math.min(interval * INTERVAL_DRIFT, delta);
51
+ deadline = now() + interval * (moving ? MOVING_SHARE : RESTING_SHARE);
52
+ for (const kind of KINDS) if (done[kind] > 0) last[kind] = done[kind];
53
+ done = perKind(0);
54
+ },
55
+ allow(kind) {
56
+ const floor = Math.ceil(INITIAL[kind] * MIN_SHARE);
57
+ const fit = Math.floor((deadline - now()) / cost[kind]);
58
+ const ceiling = late ? Math.floor(last[kind] / 2) : Math.max(INITIAL[kind], last[kind] * 2);
59
+ return Math.max(done[kind] === 0 ? floor : 0, Math.min(fit, ceiling - done[kind]));
60
+ },
61
+ spend(kind, units, ms) {
62
+ if (units <= 0) return;
63
+ done[kind] += units;
64
+ cost[kind] += (Math.max(ms, .001) / units - cost[kind]) * COST_SMOOTHING;
65
+ }
66
+ };
67
+ }
68
+ //#endregion
69
+ export { createBoardFrameClock, fixedBoardFrameBudget };
@@ -0,0 +1,4 @@
1
+ //#region src/board/stage/idle.d.ts
2
+ declare function scheduleBoardIdle(task: () => void, delay: number): () => void;
3
+ //#endregion
4
+ export { scheduleBoardIdle };
@@ -0,0 +1,26 @@
1
+ //#region src/board/stage/idle.ts
2
+ function scheduleBoardIdle(task, delay) {
3
+ const scheduler = globalThis.scheduler;
4
+ if (scheduler?.postTask) {
5
+ const controller = new AbortController();
6
+ scheduler.postTask(task, {
7
+ priority: "background",
8
+ delay,
9
+ signal: controller.signal
10
+ }).catch((error) => {
11
+ if (!controller.signal.aborted) throw error;
12
+ });
13
+ return () => controller.abort();
14
+ }
15
+ let idle = 0;
16
+ const timer = setTimeout(() => {
17
+ if (typeof requestIdleCallback === "function") idle = requestIdleCallback(task, { timeout: 1e3 });
18
+ else task();
19
+ }, delay);
20
+ return () => {
21
+ clearTimeout(timer);
22
+ if (idle) cancelIdleCallback(idle);
23
+ };
24
+ }
25
+ //#endregion
26
+ export { scheduleBoardIdle };
@@ -1,10 +1,14 @@
1
1
  import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions } from "./lru.js";
2
- import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
- import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, createBoardAssetManager } from "./assets.js";
4
- import { toBoardPointerEvent } from "./pointer.js";
5
- import { createBoardPointerGestures } from "./pointer-gestures.js";
2
+ import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
+ import { BoardAssetChange, BoardAssetManager, BoardAssetManagerOptions, BoardAssetPrefetch, BoardModelPreviewRequest, BoardModelPreviews, createBoardAssetManager } from "./assets.js";
4
+ import { BOARD_LONG_PRESS_MS, BoardStagePress } from "./presses.js";
5
+ import { BoardFrameBudget, BoardFrameWork } from "./frame-budget.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, BoardStageStats, BoardStageTheme, mountBoardStage } from "./stage.js";
9
+ import { BOARD_BENCH_PHASES, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, runBoardStageBench } from "./bench.js";
10
+ import { BOARD_DEVICE_TIERS, BoardDeviceBudget, BoardDeviceEnvironment, BoardDeviceSignals, BoardDeviceTier, boardDeviceBudget, boardDeviceTier, defaultBoardDeviceBudget, readBoardDeviceSignals } from "./device.js";
11
+ import { toBoardPointerEvent } from "./pointer.js";
12
+ import { createBoardPointerGestures } from "./pointer-gestures.js";
9
13
  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, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
14
+ export { BOARD_BENCH_PHASES, BOARD_DEVICE_TIERS, BOARD_LONG_PRESS_MS, BoardAssetChange, BoardAssetManager, BoardAssetManagerOptions, BoardAssetPrefetch, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, BoardDeviceBudget, BoardDeviceEnvironment, BoardDeviceSignals, BoardDeviceTier, type BoardFrameBudget, type BoardFrameWork, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, type BoardStagePress, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardDeviceBudget, boardDeviceTier, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, defaultBoardDeviceBudget, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, readBoardDeviceSignals, readTextureTone, runBoardStageBench, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
@@ -1,10 +1,13 @@
1
1
  import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, planLruTrim, selectLruEvictions } from "./lru.js";
2
- import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
2
+ import { BOARD_DEVICE_TIERS, boardDeviceBudget, boardDeviceTier, defaultBoardDeviceBudget, readBoardDeviceSignals } from "./device.js";
3
+ import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
3
4
  import { createBoardAssetManager } from "./assets.js";
5
+ import { BOARD_BENCH_PHASES, runBoardStageBench } from "./bench.js";
4
6
  import { toBoardPointerEvent } from "./pointer.js";
5
7
  import { createBoardPointerGestures } from "./pointer-gestures.js";
6
8
  import { createBoardScene } from "./scene.js";
9
+ import { BOARD_LONG_PRESS_MS } from "./presses.js";
7
10
  import { drawBoardOverlay, expandFrame } from "./overlay.js";
8
11
  import { createBoardItemViewLayer } from "./views.js";
9
12
  import { mountBoardStage } from "./stage.js";
10
- export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
13
+ export { BOARD_BENCH_PHASES, BOARD_DEVICE_TIERS, BOARD_LONG_PRESS_MS, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardDeviceBudget, boardDeviceTier, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, defaultBoardDeviceBudget, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, readBoardDeviceSignals, readTextureTone, runBoardStageBench, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
@@ -0,0 +1,21 @@
1
+ //#region src/board/stage/presses.d.ts
2
+ declare const BOARD_LONG_PRESS_MS = 450;
3
+ type BoardStagePress = {
4
+ pointerId: number;
5
+ pointerType: string;
6
+ clientX: number;
7
+ clientY: number;
8
+ };
9
+ declare function boardStagePress(event: PointerEvent): BoardStagePress;
10
+ declare function createBoardLongPress(fire: (press: BoardStagePress) => void): {
11
+ start(event: PointerEvent): void;
12
+ move(event: PointerEvent): void;
13
+ end(event: PointerEvent): void;
14
+ cancel: () => void;
15
+ };
16
+ declare function createGhostClickGuard(view: Window | null): {
17
+ arm(): void;
18
+ dispose: () => void | undefined;
19
+ };
20
+ //#endregion
21
+ export { BOARD_LONG_PRESS_MS, BoardStagePress, boardStagePress, createBoardLongPress, createGhostClickGuard };
@@ -0,0 +1,66 @@
1
+ import "../editor/tool.js";
2
+ //#region src/board/stage/presses.ts
3
+ const BOARD_LONG_PRESS_MS = 450;
4
+ const GHOST_CLICK_MS = 700;
5
+ function boardStagePress(event) {
6
+ return {
7
+ pointerId: event.pointerId,
8
+ pointerType: event.pointerType,
9
+ clientX: event.clientX,
10
+ clientY: event.clientY
11
+ };
12
+ }
13
+ function createBoardLongPress(fire) {
14
+ let pending = null;
15
+ const cancel = () => {
16
+ if (pending) clearTimeout(pending.timer);
17
+ pending = null;
18
+ };
19
+ return {
20
+ start(event) {
21
+ if (pending) return cancel();
22
+ if (event.pointerType === "mouse" || event.button !== 0) return;
23
+ const press = boardStagePress(event);
24
+ pending = {
25
+ press,
26
+ timer: setTimeout(() => {
27
+ pending = null;
28
+ fire(press);
29
+ }, 450)
30
+ };
31
+ },
32
+ move(event) {
33
+ if (!pending || event.pointerId !== pending.press.pointerId) return;
34
+ const { clientX, clientY } = pending.press;
35
+ if (Math.hypot(event.clientX - clientX, event.clientY - clientY) > 8) cancel();
36
+ },
37
+ end(event) {
38
+ if (pending?.press.pointerId === event.pointerId) cancel();
39
+ },
40
+ cancel
41
+ };
42
+ }
43
+ function createGhostClickGuard(view) {
44
+ let disarm = null;
45
+ return {
46
+ arm() {
47
+ disarm?.();
48
+ if (!view) return;
49
+ const drop = (event) => {
50
+ event.preventDefault();
51
+ event.stopImmediatePropagation();
52
+ disarm?.();
53
+ };
54
+ const timer = setTimeout(() => disarm?.(), GHOST_CLICK_MS);
55
+ view.addEventListener("click", drop, true);
56
+ disarm = () => {
57
+ clearTimeout(timer);
58
+ view.removeEventListener("click", drop, true);
59
+ disarm = null;
60
+ };
61
+ },
62
+ dispose: () => disarm?.()
63
+ };
64
+ }
65
+ //#endregion
66
+ export { BOARD_LONG_PRESS_MS, boardStagePress, createBoardLongPress, createGhostClickGuard };
@@ -5,6 +5,7 @@ import "../model/index.js";
5
5
  import { BoardLod } from "../render/lod.js";
6
6
  import { BoardCardRendererResolver, BoardRenderContext } from "../render/renderers/board-renderer-registry.js";
7
7
  import "../render/index.js";
8
+ import { BoardFrameBudget } from "./frame-budget.js";
8
9
  import { Container, Graphics } from "pixi.js";
9
10
  //#region src/board/stage/scene.d.ts
10
11
  type BoardLiveStroke = {
@@ -32,6 +33,7 @@ type SceneSyncInput = {
32
33
  /** The world area on screen; live cards outside it skip rendering. */
33
34
  view?: Rect;
34
35
  lod?: BoardLod;
36
+ budget?: BoardFrameBudget;
35
37
  };
36
38
  type BoardSceneNode = {
37
39
  item: BoardSceneItem;
@@ -41,6 +43,7 @@ type BoardScene = {
41
43
  sync: (input: SceneSyncInput) => void;
42
44
  getNode: (itemId: string) => BoardSceneNode | null;
43
45
  holders: (key: string) => ReadonlySet<string>;
46
+ repaintFar: (keys: ReadonlySet<string>, assetKey: (item: BoardSceneItem) => string | null) => void;
44
47
  readonly liveVersion: number;
45
48
  liveIds: () => ReadonlySet<string>;
46
49
  /** Live cards and cards drawn on far plates. */
@@ -48,6 +51,7 @@ type BoardScene = {
48
51
  live: number;
49
52
  far: number;
50
53
  };
54
+ readonly settling: boolean;
51
55
  readonly animated: boolean;
52
56
  tick: (clock: number) => number | null;
53
57
  redrawText: () => void;
@@ -1,16 +1,14 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
2
  import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
3
3
  import { itemBounds, rectsIntersect } from "../model/geometry.js";
4
- import { DEFAULT_BOARD_LOD, boardCardDetailed } from "../render/lod.js";
4
+ import { DEFAULT_BOARD_LOD, boardCardDetailed, boardLodRatio, boardLodSize } from "../render/lod.js";
5
5
  import { textZoomBucket } from "../render/text-resolution.js";
6
6
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
7
+ import { fixedBoardFrameBudget } from "./frame-budget.js";
7
8
  import { Graphics, Text } from "pixi.js";
8
9
  //#region src/board/stage/scene.ts
9
10
  const FAR_CHUNK = 256;
10
- const FAR_PAINT_BUDGET = 2400;
11
- const MATERIALIZE_BATCH = 48;
12
11
  const LOD_ZOOM_STEPS = 8;
13
- const RESOLUTION_BATCH = 32;
14
12
  const Z_LIFTED = 1e9;
15
13
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
16
14
  const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
@@ -47,6 +45,8 @@ function createBoardScene(options) {
47
45
  const pendingResolution = /* @__PURE__ */ new Set();
48
46
  let liveVersion = 0;
49
47
  let lifted = /* @__PURE__ */ new Set();
48
+ const fixedBudget = fixedBoardFrameBudget();
49
+ let budget = fixedBudget;
50
50
  const liveStrokes = /* @__PURE__ */ new Map();
51
51
  function setHeldKey(context, cardId, item) {
52
52
  const desiredKey = item ? context.assetKey(item) : null;
@@ -168,7 +168,7 @@ function createBoardScene(options) {
168
168
  function lodSize(renderer, item) {
169
169
  let size = lodSizes.get(item);
170
170
  if (size === void 0) {
171
- size = renderer.lod?.size?.(item) ?? Math.max(item.frame.width, item.frame.height);
171
+ size = boardLodSize(renderer, item);
172
172
  lodSizes.set(item, size);
173
173
  }
174
174
  return size;
@@ -210,19 +210,20 @@ function createBoardScene(options) {
210
210
  live.add(id);
211
211
  continue;
212
212
  }
213
- if (lodSize(renderer, item) * context.zoom / lod[renderer.lod?.kind ?? "card"] >= (nearIds.has(id) ? .8 : 1)) near.push(item);
213
+ if (boardLodRatio(renderer, lodSize(renderer, item), context.zoom, lod) >= (nearIds.has(id) ? .8 : 1)) near.push(item);
214
214
  }
215
215
  const room = Math.max(0, lod.live - live.size);
216
216
  const limited = near.length > room;
217
+ const allowance = budget.allow("materialize");
217
218
  const { focus, view } = input;
218
- const ranked = focus && (limited || near.length > MATERIALIZE_BATCH) ? near.map(({ id, frame }) => ({
219
+ const ranked = focus && (limited || near.length > allowance) ? near.map(({ id, frame }) => ({
219
220
  id,
220
221
  offscreen: view && !rectsIntersect(itemBounds(frame), view) ? 1 : 0,
221
222
  distance: (frame.x + frame.width / 2 - focus.x) ** 2 + (frame.y + frame.height / 2 - focus.y) ** 2
222
223
  })).sort((a, b) => a.offscreen - b.offscreen || a.distance - b.distance) : near;
223
224
  let admitted = 0;
224
225
  let deferred = false;
225
- for (const { id } of ranked.slice(0, room)) if (!cards.has(id) && admitted++ >= MATERIALIZE_BATCH) deferred = true;
226
+ for (const { id } of ranked.slice(0, room)) if (!cards.has(id) && admitted++ >= allowance) deferred = true;
226
227
  else live.add(id);
227
228
  if (deferred) options.requestFrame?.();
228
229
  nearIds = live;
@@ -255,6 +256,7 @@ function createBoardScene(options) {
255
256
  const plates = /* @__PURE__ */ new Map();
256
257
  let paintedPlan = null;
257
258
  let paintedSig = "";
259
+ let platesPending = false;
258
260
  let covered = EMPTY_IDS;
259
261
  /**
260
262
  * Repaint the plates whose cards changed, within a per-frame budget. A card
@@ -263,17 +265,19 @@ function createBoardScene(options) {
263
265
  function paintPlates(input, current) {
264
266
  const { context, globalSig, scene } = input;
265
267
  if (current === paintedPlan && globalSig === paintedSig) return false;
266
- let budget = FAR_PAINT_BUDGET;
268
+ const start = performance.now();
269
+ const allowance = budget.allow("plate");
270
+ let painted = 0;
267
271
  let pending = false;
268
272
  let changed = false;
269
273
  for (const run of current.runs.values()) {
270
274
  let plate = plates.get(run.key);
271
275
  if (plate?.sig === globalSig && sameItems(plate.items, run.items)) continue;
272
- if (budget <= 0) {
276
+ if (painted >= allowance) {
273
277
  pending = true;
274
278
  continue;
275
279
  }
276
- budget -= run.items.length;
280
+ painted += run.items.length;
277
281
  if (!plate) {
278
282
  plate = {
279
283
  graphics: new Graphics({ label: "board-far-plate" }),
@@ -292,6 +296,7 @@ function createBoardScene(options) {
292
296
  const host = hostFor(run.items[0], scene);
293
297
  if (graphics.parent !== host) host.addChild(graphics);
294
298
  }
299
+ const paintMs = performance.now() - start;
295
300
  for (const [key, plate] of plates) {
296
301
  if (current.runs.has(key)) continue;
297
302
  plates.delete(key);
@@ -301,6 +306,8 @@ function createBoardScene(options) {
301
306
  const next = /* @__PURE__ */ new Set();
302
307
  for (const plate of plates.values()) for (const item of plate.items) next.add(item.id);
303
308
  covered = next;
309
+ budget.spend("plate", painted, paintMs);
310
+ platesPending = pending;
304
311
  if (pending) options.requestFrame?.();
305
312
  else {
306
313
  paintedPlan = current;
@@ -308,11 +315,19 @@ function createBoardScene(options) {
308
315
  }
309
316
  return changed;
310
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
+ }
324
+ }
311
325
  function clearPlates() {
312
326
  for (const plate of plates.values()) plate.graphics.destroy();
313
327
  plates.clear();
314
328
  paintedPlan = null;
315
329
  paintedSig = "";
330
+ platesPending = false;
316
331
  covered = EMPTY_IDS;
317
332
  plan = null;
318
333
  planInputs = [];
@@ -379,6 +394,8 @@ function createBoardScene(options) {
379
394
  }
380
395
  function sync(input) {
381
396
  const { items, scene, context, getItem, pinnedIds, globalSig, structureVersion } = input;
397
+ fixedBudget.reset();
398
+ budget = input.budget ?? fixedBudget;
382
399
  const resolution = `${textZoomBucket(context.zoom)}:${boardCardDetailed(context.zoom)}:${context.textResolution}`;
383
400
  if (resolution !== lastResolution) {
384
401
  lastResolution = resolution;
@@ -398,6 +415,8 @@ function createBoardScene(options) {
398
415
  for (const id of current.live) wanted.add(id);
399
416
  for (const id of cards.keys()) if (current.far.has(id) && !covered.has(id)) wanted.add(id);
400
417
  let liveSetChanged = false;
418
+ let made = 0;
419
+ let making = 0;
401
420
  for (const [id, entry] of [...cards]) if (!wanted.has(id)) {
402
421
  recycle(id, entry, context, false);
403
422
  liveSetChanged = true;
@@ -414,7 +433,10 @@ function createBoardScene(options) {
414
433
  entry = void 0;
415
434
  }
416
435
  if (!entry) {
436
+ const start = performance.now();
417
437
  materialize(item, context, globalSig, scene);
438
+ making += performance.now() - start;
439
+ made += 1;
418
440
  liveSetChanged = true;
419
441
  continue;
420
442
  }
@@ -438,6 +460,7 @@ function createBoardScene(options) {
438
460
  pendingResolution.delete(id);
439
461
  }
440
462
  }
463
+ budget.spend("materialize", made, making);
441
464
  if (input.view) cullCards(input.view, pinnedIds);
442
465
  refreshResolution(context);
443
466
  if (liveSetChanged) liveVersion += 1;
@@ -469,15 +492,20 @@ function createBoardScene(options) {
469
492
  for (const [id] of ids) pendingResolution.add(id);
470
493
  }
471
494
  function refreshResolution(context) {
472
- 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;
473
499
  for (const id of pendingResolution) {
474
- if (budget-- === 0) break;
500
+ if (refreshed === allowance) break;
475
501
  pendingResolution.delete(id);
476
502
  const entry = cards.get(id);
477
503
  if (!entry) continue;
478
504
  entry.renderer.resolution?.(entry.container, entry.item, context);
479
505
  if (entry.renderer.tick) tickingIds.add(id);
506
+ refreshed += 1;
480
507
  }
508
+ budget.spend("resolution", refreshed, performance.now() - start);
481
509
  if (pendingResolution.size) options.requestFrame?.();
482
510
  }
483
511
  function tick(clock) {
@@ -540,6 +568,7 @@ function createBoardScene(options) {
540
568
  } : null;
541
569
  },
542
570
  holders: (key) => holders.get(key) ?? EMPTY_IDS,
571
+ repaintFar,
543
572
  get liveVersion() {
544
573
  return liveVersion;
545
574
  },
@@ -548,6 +577,9 @@ function createBoardScene(options) {
548
577
  live: cards.size,
549
578
  far: covered.size
550
579
  }),
580
+ get settling() {
581
+ return Boolean(plan?.deferred) || platesPending || pendingResolution.size > 0;
582
+ },
551
583
  get animated() {
552
584
  return animatedIds.size > 0;
553
585
  },
@@ -7,7 +7,7 @@ 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";
@@ -15,6 +15,7 @@ import { BoardLod } from "../render/lod.js";
15
15
  import { BoardCardRendererResolver, BoardRenderLabels, BoardRenderPalette } from "../render/renderers/board-renderer-registry.js";
16
16
  import "../render/index.js";
17
17
  import { BoardAssetManager } from "./assets.js";
18
+ import { BoardStagePress } from "./presses.js";
18
19
  import { BoardLiveStroke } from "./scene.js";
19
20
  import { BoardItemView } from "./views.js";
20
21
  import { Graphics, Renderer } from "pixi.js";
@@ -34,6 +35,8 @@ type BoardStageOptions = {
34
35
  source: BoardAssetSource;
35
36
  renderers?: BoardCardRendererResolver;
36
37
  views?: readonly BoardItemView[];
38
+ /** `below` needs a theme with `imageBackground`; views there get no input. */
39
+ viewLayer?: "above" | "below";
37
40
  theme?: () => BoardStageTheme;
38
41
  labels?: () => BoardRenderLabels;
39
42
  taskRun?: (taskRunId: string) => {
@@ -51,6 +54,12 @@ type BoardStageOptions = {
51
54
  scene: BoardScene;
52
55
  }) => void;
53
56
  onPointer?: (phase: BoardStagePointerPhase, event: PointerEvent, input: BoardPointerEvent) => boolean | undefined;
57
+ onActivate?: (item: BoardSceneItem, activation: BoardActivation) => void;
58
+ onLongPress?: (item: BoardSceneItem, press: BoardStagePress) => void;
59
+ /** A press on a card key (`BoardCardRenderer.hitAt`); return false to leave it to the editor. */
60
+ onHit?: (item: BoardSceneItem, action: string, press: BoardStagePress) => boolean | undefined;
61
+ /** Card keys `onHit` acts on; others go to the editor. */
62
+ handlesHit?: (item: BoardSceneItem, action: string) => boolean;
54
63
  onDoubleClick?: (item: BoardSceneItem | null, point: WorldPoint) => boolean | undefined;
55
64
  onSurfaceChange?: (size: {
56
65
  width: number;
@@ -60,8 +69,8 @@ type BoardStageOptions = {
60
69
  onLiveChange?: (ids: ReadonlySet<string>) => void;
61
70
  safeRect?: () => Rect;
62
71
  scriptCapabilities?: Record<string, (id: string, input: unknown) => Promise<unknown>>;
63
- /** Level-of-detail thresholds and live budget; read every frame. */
64
72
  lod?: () => BoardLod | undefined;
73
+ prefetch?: number;
65
74
  };
66
75
  type BoardStage = {
67
76
  readonly ready: Promise<boolean>;
@@ -92,6 +101,16 @@ type BoardStageStats = {
92
101
  textures: number;
93
102
  cooling: number;
94
103
  textureBytes: number;
104
+ loading: number;
105
+ prefetching: number;
106
+ waiting: number;
107
+ settling: boolean;
108
+ frame: {
109
+ p50: number;
110
+ p95: number;
111
+ max: number;
112
+ };
113
+ interval: number;
95
114
  };
96
115
  declare function mountBoardStage(host: HTMLElement, options: BoardStageOptions): BoardStage;
97
116
  //#endregion