@neta-art/cohub 12.2.0 → 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 (102) hide show
  1. package/README.md +77 -3
  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 +7 -3
  7. package/dist/board/editor/editor.js +4 -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/tool.d.ts +4 -1
  16. package/dist/board/editor/tool.js +6 -1
  17. package/dist/board/export/index.js +1 -1
  18. package/dist/board/export/node/scripts.js +1 -1
  19. package/dist/board/export/scene.js +1 -1
  20. package/dist/board/model/asset-key.d.ts +9 -1
  21. package/dist/board/model/asset-key.js +15 -3
  22. package/dist/board/model/index.d.ts +5 -5
  23. package/dist/board/model/index.js +18 -18
  24. package/dist/board/model/items/create.js +2 -2
  25. package/dist/board/model/items/definition.d.ts +9 -1
  26. package/dist/board/model/items/index.d.ts +2 -2
  27. package/dist/board/model/items/registry.d.ts +2 -1
  28. package/dist/board/model/items/registry.js +1 -0
  29. package/dist/board/model/sampling.js +1 -1
  30. package/dist/board/model/text-metrics.d.ts +21 -1
  31. package/dist/board/model/text-metrics.js +24 -1
  32. package/dist/board/model/themes.d.ts +15 -8
  33. package/dist/board/model/themes.js +15 -10
  34. package/dist/board/render/index.d.ts +8 -6
  35. package/dist/board/render/index.js +7 -5
  36. package/dist/board/render/labels.d.ts +15 -0
  37. package/dist/board/render/labels.js +14 -0
  38. package/dist/board/render/lod.d.ts +9 -1
  39. package/dist/board/render/lod.js +7 -1
  40. package/dist/board/render/media-interaction.d.ts +5 -2
  41. package/dist/board/render/media-interaction.js +15 -3
  42. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  43. package/dist/board/render/renderers/audio-card-renderer.js +1 -1
  44. package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
  45. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  46. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  47. package/dist/board/render/renderers/far-plate.d.ts +4 -1
  48. package/dist/board/render/renderers/far-plate.js +5 -1
  49. package/dist/board/render/renderers/file-card-renderer.js +46 -26
  50. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  51. package/dist/board/render/renderers/image-card-renderer.js +2 -2
  52. package/dist/board/render/renderers/model-card-renderer.js +5 -5
  53. package/dist/board/render/renderers/shape-card-renderer.js +1 -1
  54. package/dist/board/render/renderers/task-card-renderer.js +2 -2
  55. package/dist/board/render/renderers/video-card-renderer.js +3 -3
  56. package/dist/board/render/script-export.js +1 -1
  57. package/dist/board/render/script-host.js +3 -3
  58. package/dist/board/render/style.d.ts +1 -0
  59. package/dist/board/render/style.js +3 -1
  60. package/dist/board/render/text-edit.d.ts +32 -0
  61. package/dist/board/render/text-edit.js +134 -0
  62. package/dist/board/render/text-fit.d.ts +1 -3
  63. package/dist/board/render/text-fit.js +4 -43
  64. package/dist/board/render/text-measurement.d.ts +12 -2
  65. package/dist/board/render/text-measurement.js +55 -10
  66. package/dist/board/replica/index.d.ts +4 -2
  67. package/dist/board/replica/index.js +28 -17
  68. package/dist/board/replica/indexeddb.d.ts +16 -0
  69. package/dist/board/replica/indexeddb.js +155 -0
  70. package/dist/board/stage/assets.d.ts +16 -4
  71. package/dist/board/stage/assets.js +155 -30
  72. package/dist/board/stage/bench.d.ts +35 -0
  73. package/dist/board/stage/bench.js +153 -0
  74. package/dist/board/stage/device.d.ts +41 -0
  75. package/dist/board/stage/device.js +62 -0
  76. package/dist/board/stage/frame-budget.d.ts +19 -0
  77. package/dist/board/stage/frame-budget.js +69 -0
  78. package/dist/board/stage/idle.d.ts +4 -0
  79. package/dist/board/stage/idle.js +26 -0
  80. package/dist/board/stage/index.d.ts +9 -5
  81. package/dist/board/stage/index.js +5 -2
  82. package/dist/board/stage/presses.d.ts +21 -0
  83. package/dist/board/stage/presses.js +66 -0
  84. package/dist/board/stage/scene.d.ts +4 -0
  85. package/dist/board/stage/scene.js +45 -13
  86. package/dist/board/stage/stage.d.ts +17 -2
  87. package/dist/board/stage/stage.js +314 -95
  88. package/dist/board/stage/texture-pool.d.ts +6 -4
  89. package/dist/board/stage/texture-pool.js +42 -8
  90. package/dist/chunks/environment.d.ts +6 -0
  91. package/dist/chunks/http.d.ts +9 -1
  92. package/dist/chunks/http.js +791 -4
  93. package/dist/index.d.ts +52 -1
  94. package/dist/index.js +65 -733
  95. package/dist/protocol/dist/board-model.d.ts +15 -1
  96. package/dist/protocol/dist/board-model.js +12 -2
  97. package/dist/protocol/dist/board-patch.js +1 -1
  98. package/dist/protocol/dist/board-schema.d.ts +2 -1
  99. package/dist/protocol/dist/board-schema.js +4 -1
  100. package/dist/protocol/dist/core/media.js +4 -0
  101. package/dist/protocol/dist/index.d.ts +2 -2
  102. package/package.json +2 -1
@@ -1,32 +1,37 @@
1
1
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
2
2
  import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
3
- import { pointToWorld, screenPoint, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
4
- import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
5
- import { boardScriptTarget } from "../model/script-hit.js";
3
+ import { itemBounds, pointToWorld, rectsIntersect, screenPoint, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
6
4
  import { createBoardAnimationSampler, resolveBoardAnimationCamera } from "../model/sampling.js";
7
- import { resolveSceneArrow } from "../model/arrow-geometry.js";
8
- import { boardTextureEdge } from "../model/asset-key.js";
9
5
  import { pickBoardColor, resolveItemColor } from "../model/palette.js";
10
- import { elementClientToLocal } from "../model/client-geometry.js";
11
- import { isCreationBoardTool } from "../editor/tool.js";
12
- import { resizeCursorForHandle } from "../editor/selection-transform.js";
13
6
  import { parseBoardCssColor } from "../render/css-color.js";
14
7
  import { itemAlpha } from "../render/palette.js";
8
+ import { resolveSceneArrow } from "../model/arrow-geometry.js";
15
9
  import { onBoardTextFontsChange } from "../render/text-measurement.js";
10
+ import { boardLodRatio, boardLodSize } from "../render/lod.js";
16
11
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
17
12
  import { getBoardResolution } from "../render/text-resolution.js";
18
13
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
14
+ import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
15
+ import { boardScriptTarget } from "../model/script-hit.js";
19
16
  import { createBoardScriptHost } from "../render/script-host.js";
20
17
  import { defaultBoardRenderStyle } from "../render/style.js";
21
18
  import { boardRenderTheme } from "../render/theme.js";
22
- import { createBoardScriptObservers } from "../model/script-observers.js";
19
+ import { boardTextureEdge } from "../model/asset-key.js";
20
+ import { elementClientToLocal } from "../model/client-geometry.js";
21
+ import { isCreationBoardTool } from "../editor/tool.js";
22
+ import { resizeCursorForHandle } from "../editor/selection-transform.js";
23
23
  import { createBoardPlayer } from "../player/index.js";
24
+ import { createBoardScriptObservers } from "../model/script-observers.js";
25
+ import { defaultBoardDeviceBudget } from "./device.js";
26
+ import { scheduleBoardIdle } from "./idle.js";
24
27
  import { boardCanvasSize, measureBoardCanvasBox } from "./canvas-size.js";
25
28
  import { observeDisplayDensity } from "./display-density.js";
29
+ import { createBoardFrameClock } from "./frame-budget.js";
26
30
  import { toBoardPointerEvent } from "./pointer.js";
27
31
  import { createBoardPointerGestures } from "./pointer-gestures.js";
28
32
  import { createBoardScene } from "./scene.js";
29
33
  import { scheduleBoardMaintenance } from "./maintenance.js";
34
+ import { boardStagePress, createBoardLongPress, createGhostClickGuard } from "./presses.js";
30
35
  import { createBoardScriptPointerRouter } from "./script-pointer.js";
31
36
  import { drawBoardOverlay } from "./overlay.js";
32
37
  import { createTextResolutionTracker } from "./text-resolution-tracker.js";
@@ -55,6 +60,15 @@ const TEXTURE_SETTLE_MS = 150;
55
60
  const ADOPT_PIXELS_PER_FRAME = 4194304;
56
61
  const LOAD_LEAD_MS = 300;
57
62
  const CULL_PADDING = 96;
63
+ const PREFETCH_REST_MS = 200;
64
+ const PREFETCH_LIMIT = 96;
65
+ const FRAME_SAMPLES = 120;
66
+ const ANTIALIAS_MAX_DENSITY = 3;
67
+ const FRAME_SYNC = 1;
68
+ const FRAME_RENDER = 2;
69
+ const FRAME_CARDS = 4;
70
+ const FRAME_SCRIPTS = 8;
71
+ const FRAME_RESIZE = 16;
58
72
  const ROTATE_CURSOR = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M21 12a9 9 0 1 1-2.64-6.36L21 8M21 3v5h-5' fill='none' stroke='%23fff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M21 12a9 9 0 1 1-2.64-6.36L21 8M21 3v5h-5' fill='none' stroke='%231d1d1f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") 12 12, crosshair`;
59
73
  const defaultStyles = /* @__PURE__ */ new Map();
60
74
  function styleOf(theme) {
@@ -106,14 +120,23 @@ function mountBoardStage(host, options) {
106
120
  let stopDensity = null;
107
121
  let active = true;
108
122
  let disposed = false;
109
- let renderFrame = 0;
110
- let tickFrame = 0;
111
- let scriptTickFrame = 0;
112
- let cardTickFrame = 0;
123
+ let frameHandle = 0;
124
+ let frameWork = 0;
125
+ let inFrame = false;
126
+ let chained = false;
127
+ const clock = createBoardFrameClock();
128
+ const device = defaultBoardDeviceBudget();
129
+ const currentLod = () => options.lod?.() ?? device.lod;
130
+ const frameTimes = new Float32Array(FRAME_SAMPLES);
131
+ let frameCount = 0;
132
+ let frameCamera = null;
113
133
  let cardTickTimer = null;
114
134
  let cardTickDue = 0;
135
+ let prefetchKey = "";
136
+ let prefetchMovedAt = 0;
137
+ let prefetchContext = null;
138
+ let cancelPrefetch = null;
115
139
  const staleIds = /* @__PURE__ */ new Set();
116
- let resizeFrame = 0;
117
140
  let canvasBox = null;
118
141
  let cardEpoch = 0;
119
142
  let fontEpoch = 0;
@@ -124,6 +147,13 @@ function mountBoardStage(host, options) {
124
147
  };
125
148
  let playbackCamera = null;
126
149
  let cursor = "";
150
+ let lastContext = null;
151
+ let hoveredHit = null;
152
+ const ghostClicks = createGhostClickGuard(host.ownerDocument.defaultView);
153
+ const longPress = createBoardLongPress((press) => {
154
+ const item = editor.itemAt(pointToWorld(localPoint(press.clientX, press.clientY), renderCamera()));
155
+ if (item) options.onLongPress?.(item, press);
156
+ });
127
157
  const renderCamera = () => playbackCamera ?? editor.camera;
128
158
  const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera, scheduleTick);
129
159
  const player = createBoardPlayer({ resolveColor: (color) => {
@@ -206,6 +236,7 @@ function mountBoardStage(host, options) {
206
236
  };
207
237
  }
208
238
  const arrivals = /* @__PURE__ */ new Set();
239
+ const toned = /* @__PURE__ */ new Set();
209
240
  function adoptArrivals() {
210
241
  let pixels = 0;
211
242
  for (const key of arrivals) {
@@ -298,13 +329,15 @@ function mountBoardStage(host, options) {
298
329
  assetKey: assets.assetKey,
299
330
  getTexture: (key) => assets.getTexture(key),
300
331
  hasError: (key) => assets.hasError(key),
332
+ assetTone: (key) => assets.getTone(key),
301
333
  fileState: (path) => options.fileState?.(path) ?? "ok",
302
334
  acquireTexture: (key, item) => {
303
335
  assets.acquire(key);
304
336
  assets.requestItem(item, boardTextureEdge(item, textureScale()));
305
337
  if (assets.getTexture(key)) naturalIds.add(item.id);
306
338
  },
307
- releaseTexture: (key) => assets.release(key)
339
+ releaseTexture: (key) => assets.release(key),
340
+ hoveredHit
308
341
  };
309
342
  }
310
343
  function syncBackground(current) {
@@ -474,6 +507,36 @@ function mountBoardStage(host, options) {
474
507
  if (style.selection.dash > 0 && selected.length > 0 && !(options.reducedMotion?.() ?? false)) scheduleMarch(style);
475
508
  else stopMarch();
476
509
  }
510
+ function opensOnTap(item) {
511
+ return !editor.selection.includes(item.id) && editor.registry.activation(item) === "tap";
512
+ }
513
+ function hitAt(item, point) {
514
+ if (!lastContext || !scene?.getNode(item.id)) return null;
515
+ return resolveRenderer(item, lastContext).hitAt?.(item, point, lastContext) ?? null;
516
+ }
517
+ function hoverHit(input) {
518
+ if (!options.onHit) return;
519
+ const item = input && editor.interaction.type === "idle" && editor.hoverId ? editor.itemById(editor.hoverId) : null;
520
+ const hit = item && input ? hitAt(item, input.world) : null;
521
+ const next = item && hit ? {
522
+ itemId: item.id,
523
+ action: hit.action,
524
+ cursor: hit.cursor ?? "pointer"
525
+ } : null;
526
+ if (next?.itemId === hoveredHit?.itemId && next?.action === hoveredHit?.action) return;
527
+ for (const id of [hoveredHit?.itemId, next?.itemId]) if (id) staleIds.add(id);
528
+ hoveredHit = next;
529
+ scheduleTick();
530
+ }
531
+ function pressHit(event, input) {
532
+ if (!options.onHit || event.button !== 0) return false;
533
+ const item = editor.itemAt(input.world);
534
+ const hit = item ? hitAt(item, input.world) : null;
535
+ if (!item || !hit || options.onHit(item, hit.action, boardStagePress(event)) === false) return false;
536
+ event.preventDefault();
537
+ if (event.pointerType !== "mouse") ghostClicks.arm();
538
+ return true;
539
+ }
477
540
  function cursorFor() {
478
541
  if (scriptMode === "interact" && scriptHover) {
479
542
  const output = scriptOutputs.get(scriptHover.owner);
@@ -488,49 +551,191 @@ function mountBoardStage(host, options) {
488
551
  case "draggingArrowHandle":
489
552
  case "brushing": return "crosshair";
490
553
  }
491
- if (editor.spaceHeld || editor.tool === "hand") return "grab";
554
+ if (interaction.type === "pressing") return "pointer";
555
+ if (editor.spaceHeld) return "grab";
556
+ if (hoveredHit) return hoveredHit.cursor;
492
557
  const control = editor.hoveredTransformControl;
493
558
  if (control?.kind === "resize") return resizeCursorForHandle(control.handle, editor.selectionTransform?.frame.rotation ?? 0);
494
559
  if (control?.kind === "rotate") return ROTATE_CURSOR;
495
- if (isCreationBoardTool(editor.tool)) return "crosshair";
496
560
  const hovered = editor.hoverId ? editor.itemById(editor.hoverId) : null;
561
+ if (hovered && (editor.tool === "select" || editor.tool === "hand") && opensOnTap(hovered)) return "pointer";
562
+ if (editor.tool === "hand") return "grab";
563
+ if (isCreationBoardTool(editor.tool)) return "crosshair";
497
564
  return hovered && !hovered.locked && editor.registry.capabilities(hovered).canMove ? "move" : "default";
498
565
  }
566
+ function requestFrame(work) {
567
+ if (!active || disposed) return;
568
+ frameWork |= work;
569
+ if (!frameHandle && !inFrame) {
570
+ chained = false;
571
+ frameHandle = requestAnimationFrame(runFrame);
572
+ }
573
+ }
574
+ function cancelFrames() {
575
+ cancelAnimationFrame(frameHandle);
576
+ frameHandle = 0;
577
+ frameWork = 0;
578
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
579
+ cardTickTimer = null;
580
+ }
581
+ function runFrame(now) {
582
+ frameHandle = 0;
583
+ if (!app) return;
584
+ const start = performance.now();
585
+ let work = frameWork;
586
+ frameWork = 0;
587
+ inFrame = true;
588
+ try {
589
+ const camera = renderCamera();
590
+ const moving = editor.gestureActive || !frameCamera || camera.x !== frameCamera.x || camera.y !== frameCamera.y || camera.zoom !== frameCamera.zoom;
591
+ frameCamera = camera;
592
+ clock.begin(now, {
593
+ moving,
594
+ chained
595
+ });
596
+ if (work & FRAME_RESIZE && !applyResize()) work &= -17;
597
+ const synced = Boolean(work & 17);
598
+ if (synced) sync();
599
+ else if (work & FRAME_SCRIPTS) {
600
+ syncScripts();
601
+ if (scriptMode !== "paused" && scripts?.animated) frameWork |= FRAME_SCRIPTS;
602
+ }
603
+ if (work & FRAME_CARDS && !synced && scene) {
604
+ const next = scene.tick(performance.now());
605
+ work |= FRAME_RENDER;
606
+ if (next !== null) scheduleCardTick(next);
607
+ }
608
+ if ((work | frameWork) & FRAME_RENDER) {
609
+ frameWork &= -3;
610
+ app.render();
611
+ }
612
+ } finally {
613
+ inFrame = false;
614
+ frameTimes[frameCount++ % FRAME_SAMPLES] = performance.now() - start;
615
+ if (frameWork && active && !disposed) {
616
+ chained = true;
617
+ frameHandle = requestAnimationFrame(runFrame);
618
+ }
619
+ }
620
+ }
621
+ function waitingCards() {
622
+ if (!scene || surface.width === 0) return 0;
623
+ const view = visibleWorldRect(renderCamera(), surface.width, surface.height);
624
+ let waiting = 0;
625
+ for (const id of scene.liveIds()) {
626
+ const item = editor.scene.get(id);
627
+ const key = item && assets.assetKey(item);
628
+ if (key && !assets.hasError(key) && !assets.getTexture(key) && rectsIntersect(itemBounds(item.frame), view)) waiting += 1;
629
+ }
630
+ return waiting;
631
+ }
632
+ function frameStats() {
633
+ const samples = Array.from(frameTimes.subarray(0, Math.min(frameCount, FRAME_SAMPLES))).sort((a, b) => a - b);
634
+ const at = (ratio) => samples[Math.min(samples.length - 1, Math.floor(samples.length * ratio))] ?? 0;
635
+ return {
636
+ p50: at(.5),
637
+ p95: at(.95),
638
+ max: samples.at(-1) ?? 0
639
+ };
640
+ }
499
641
  function scheduleRender() {
500
- if (renderFrame || !app || !active) return;
501
- renderFrame = requestAnimationFrame(() => {
502
- renderFrame = 0;
503
- app?.render();
504
- });
642
+ requestFrame(FRAME_RENDER);
643
+ }
644
+ function scheduleTick() {
645
+ requestFrame(FRAME_SYNC);
646
+ }
647
+ function scheduleScriptTick() {
648
+ requestFrame(FRAME_SCRIPTS);
649
+ }
650
+ function resize() {
651
+ requestFrame(FRAME_RESIZE);
652
+ }
653
+ function flushResize() {
654
+ if (!active || disposed) return;
655
+ cancelAnimationFrame(frameHandle);
656
+ frameWork |= FRAME_RESIZE;
657
+ chained = false;
658
+ runFrame(performance.now());
659
+ chained = false;
505
660
  }
506
661
  function scheduleCardTick(wait) {
507
662
  if (!active || disposed) return;
508
663
  const due = performance.now() + wait;
509
- if (cardTickFrame || cardTickTimer !== null && cardTickDue <= due) return;
664
+ if (frameWork & FRAME_CARDS || cardTickTimer !== null && cardTickDue <= due) return;
510
665
  if (cardTickTimer !== null) clearTimeout(cardTickTimer);
511
666
  cardTickTimer = null;
512
667
  if (wait > 0) {
513
668
  cardTickDue = due;
514
669
  cardTickTimer = setTimeout(() => {
515
670
  cardTickTimer = null;
516
- scheduleCardTick(0);
671
+ requestFrame(FRAME_CARDS);
517
672
  }, wait);
518
673
  return;
519
674
  }
520
- cardTickFrame = requestAnimationFrame(() => {
521
- cardTickFrame = 0;
522
- if (!scene) return;
523
- const next = scene.tick(performance.now());
524
- scheduleRender();
525
- if (next !== null) scheduleCardTick(next);
526
- });
675
+ requestFrame(FRAME_CARDS);
527
676
  }
528
- function scheduleTick() {
529
- if (tickFrame || !active || disposed) return;
530
- tickFrame = requestAnimationFrame(() => {
531
- tickFrame = 0;
532
- sync();
533
- });
677
+ function schedulePrefetch(camera, context) {
678
+ const margin = assets.prefetch ? options.prefetch ?? device.prefetch : 0;
679
+ if (margin <= 0 || surface.width === 0 || surface.height === 0) return;
680
+ const key = `${camera.x}|${camera.y}|${camera.zoom}|${surface.width}|${surface.height}|${editor.structureVersion}`;
681
+ if (key === prefetchKey) return;
682
+ prefetchKey = key;
683
+ prefetchContext = context;
684
+ prefetchMovedAt = performance.now();
685
+ if (!cancelPrefetch) awaitRest(margin, PREFETCH_REST_MS);
686
+ }
687
+ function awaitRest(margin, delay) {
688
+ cancelPrefetch = scheduleBoardIdle(() => {
689
+ cancelPrefetch = null;
690
+ const remaining = PREFETCH_REST_MS - (performance.now() - prefetchMovedAt);
691
+ if (remaining > 0) awaitRest(margin, remaining);
692
+ else if (prefetchContext) prefetch(margin, prefetchContext);
693
+ }, delay);
694
+ }
695
+ function stopPrefetch() {
696
+ cancelPrefetch?.();
697
+ cancelPrefetch = null;
698
+ prefetchKey = "";
699
+ prefetchContext = null;
700
+ assets.prefetch?.([]);
701
+ }
702
+ function prefetch(margin, context) {
703
+ if (!scene || !active || disposed) return;
704
+ if (editor.gestureActive) {
705
+ prefetchKey = "";
706
+ return;
707
+ }
708
+ const camera = renderCamera();
709
+ const view = visibleWorldRect(camera, surface.width, surface.height);
710
+ const culled = stableCullRect(view);
711
+ const padX = view.width * margin;
712
+ const padY = view.height * margin;
713
+ const ring = {
714
+ x: culled.x - padX,
715
+ y: culled.y - padY,
716
+ width: culled.width + padX * 2,
717
+ height: culled.height + padY * 2
718
+ };
719
+ const centerX = view.x + view.width / 2;
720
+ const centerY = view.y + view.height / 2;
721
+ const lod = currentLod();
722
+ const scale = textureScale();
723
+ const wanted = [];
724
+ for (const id of editor.idsInRect(ring)) {
725
+ const item = editor.scene.get(id);
726
+ if (!item || rectsIntersect(itemBounds(item.frame), culled)) continue;
727
+ const renderer = resolveRenderer(item, context);
728
+ if (renderer.renderFar && boardLodRatio(renderer, boardLodSize(renderer, item), camera.zoom, lod) < 1) continue;
729
+ const { frame } = item;
730
+ const distance = (frame.x + frame.width / 2 - centerX) ** 2 + (frame.y + frame.height / 2 - centerY) ** 2;
731
+ wanted.push({
732
+ item,
733
+ edge: boardTextureEdge(item, scale),
734
+ distance
735
+ });
736
+ }
737
+ wanted.sort((a, b) => a.distance - b.distance);
738
+ assets.prefetch?.(wanted.slice(0, PREFETCH_LIMIT));
534
739
  }
535
740
  function cancelScriptPointers(owner, retained) {
536
741
  scriptPointers.cancelScripts((pointerId, target) => {
@@ -559,15 +764,6 @@ function mountBoardStage(host, options) {
559
764
  }));
560
765
  scripts.sync(editor.document, editor.scene, camera, scriptTime, scriptMode === "interact", visibleIds());
561
766
  }
562
- function scheduleScriptTick() {
563
- if (scriptTickFrame || !active || disposed) return;
564
- scriptTickFrame = requestAnimationFrame(() => {
565
- scriptTickFrame = 0;
566
- if (tickFrame) return;
567
- syncScripts();
568
- if (scriptMode !== "paused" && scripts?.animated) scheduleScriptTick();
569
- });
570
- }
571
767
  function createScripts() {
572
768
  return createBoardScriptHost({
573
769
  host,
@@ -686,8 +882,13 @@ function mountBoardStage(host, options) {
686
882
  const center = pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera);
687
883
  assets.focus(loadFocus(center, camera));
688
884
  adoptArrivals();
885
+ if (toned.size > 0) {
886
+ scene?.repaintFar(toned, assets.assetKey);
887
+ toned.clear();
888
+ }
689
889
  textResolution.update(textureScale());
690
890
  const context = renderContext(current, renderScene, frame.time);
891
+ lastContext = context;
691
892
  if (animationId && effectSampler) {
692
893
  context.time = editor.playhead?.time ?? frame.time;
693
894
  context.frameAt = effectSampler.frameAt;
@@ -723,9 +924,11 @@ function mountBoardStage(host, options) {
723
924
  gestureActive: editor.gestureActive && editor.interaction.type !== "panning",
724
925
  focus: center,
725
926
  view: ids ? cullView(camera) : void 0,
726
- lod: options.lod?.()
927
+ lod: currentLod(),
928
+ budget: clock
727
929
  });
728
930
  staleIds.clear();
931
+ schedulePrefetch(camera, context);
729
932
  if (scene.liveVersion !== liveVersion) {
730
933
  liveVersion = scene.liveVersion;
731
934
  options.onLiveChange?.(scene.liveIds());
@@ -749,29 +952,26 @@ function mountBoardStage(host, options) {
749
952
  const cardWait = scene.tick(performance.now());
750
953
  if (cardWait !== null) scheduleCardTick(cardWait);
751
954
  }
752
- function resize() {
753
- cancelAnimationFrame(resizeFrame);
754
- resizeFrame = requestAnimationFrame(() => {
755
- if (!app || !active) return;
756
- const size = {
757
- width: host.clientWidth,
758
- height: host.clientHeight
759
- };
760
- if (size.width <= 0 || size.height <= 0) return;
761
- const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
762
- const canvas = boardCanvasSize(canvasBox ?? {
763
- ...size,
764
- density,
765
- device: null
766
- }, density);
767
- if (canvas.width !== app.screen.width || canvas.height !== app.screen.height || canvas.resolution !== app.renderer.resolution) app.renderer.resize(canvas.width, canvas.height, canvas.resolution);
768
- if (size.width !== surface.width || size.height !== surface.height) {
769
- surface = size;
770
- editor.surfaceSize = surface;
771
- options.onSurfaceChange?.(surface);
772
- }
773
- sync();
774
- });
955
+ function applyResize() {
956
+ if (!app) return false;
957
+ const size = {
958
+ width: host.clientWidth,
959
+ height: host.clientHeight
960
+ };
961
+ if (size.width <= 0 || size.height <= 0) return false;
962
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
963
+ const canvas = boardCanvasSize(canvasBox ?? {
964
+ ...size,
965
+ density,
966
+ device: null
967
+ }, density);
968
+ if (canvas.width !== app.screen.width || canvas.height !== app.screen.height || canvas.resolution !== app.renderer.resolution) app.renderer.resize(canvas.width, canvas.height, canvas.resolution);
969
+ if (size.width !== surface.width || size.height !== surface.height) {
970
+ surface = size;
971
+ editor.surfaceSize = surface;
972
+ options.onSurfaceChange?.(surface);
973
+ }
974
+ return true;
775
975
  }
776
976
  const localPoint = (x, y) => elementClientToLocal(host)(x, y);
777
977
  const inViews = (event) => event.target instanceof Node && views.element.contains(event.target);
@@ -853,8 +1053,11 @@ function mountBoardStage(host, options) {
853
1053
  return;
854
1054
  }
855
1055
  if (options.onPointer?.("down", event, input) === false) return;
1056
+ if (pressHit(event, input)) return;
856
1057
  pointers.start(event);
857
1058
  editor.pointerDown(input);
1059
+ if (options.onLongPress && editor.itemAt(input.world)) longPress.start(event);
1060
+ else longPress.cancel();
858
1061
  }
859
1062
  function handlePointerMove(event) {
860
1063
  if (!active || pointers.has(event.pointerId) || inViews(event)) return;
@@ -863,21 +1066,28 @@ function mountBoardStage(host, options) {
863
1066
  function routePointerMove(event) {
864
1067
  const input = toInput(event);
865
1068
  if (scriptPointer("move", event, input)) return;
1069
+ longPress.move(event);
866
1070
  if (options.onPointer?.("move", event, input) === false) return;
867
1071
  editor.pointerMove(input);
1072
+ hoverHit(input);
868
1073
  }
869
1074
  function handlePointerUp(event) {
870
1075
  const input = toInput(event);
871
1076
  if (scriptPointer("up", event, input)) return;
1077
+ longPress.end(event);
872
1078
  if (options.onPointer?.("up", event, input) === false) return;
873
1079
  editor.pointerUp(input);
874
- if (event.type === "pointercancel" || event.pointerType !== "mouse") editor.pointerLeave();
1080
+ if (event.type === "pointercancel" || event.pointerType !== "mouse") {
1081
+ editor.pointerLeave();
1082
+ hoverHit(null);
1083
+ }
875
1084
  }
876
1085
  function handlePointerLeave(event) {
877
1086
  if (event.buttons !== 0) return;
878
1087
  scriptPointer("leave", event, toInput(event));
879
1088
  if (options.onPointer?.("leave", event, toInput(event)) === false) return;
880
1089
  editor.pointerLeave();
1090
+ hoverHit(null);
881
1091
  }
882
1092
  function handleWheel(event) {
883
1093
  if (inViews(event)) return;
@@ -896,7 +1106,7 @@ function mountBoardStage(host, options) {
896
1106
  return;
897
1107
  }
898
1108
  if (item && item.type !== "frame" && item.type !== "arrow") return;
899
- const label = editor.labelItemAt(point);
1109
+ const label = editor.labelItemAt(point, styleOf(theme()).frameLabelSize);
900
1110
  if (label) editor.editingId = label.id;
901
1111
  else if (!item) editor.beginTextDraft(point);
902
1112
  }
@@ -914,27 +1124,40 @@ function mountBoardStage(host, options) {
914
1124
  ["dblclick", handleDoubleClick]
915
1125
  ];
916
1126
  const stopEditor = editor.subscribe(LIVE_CHANNELS, scheduleTick);
1127
+ const stopActivations = editor.subscribeActivations(({ itemId, pointerType, selected }) => {
1128
+ const item = editor.itemById(itemId);
1129
+ if (!item || !options.onActivate) return;
1130
+ if (pointerType !== "mouse") ghostClicks.arm();
1131
+ options.onActivate(item, {
1132
+ itemId,
1133
+ pointerType,
1134
+ selected
1135
+ });
1136
+ });
917
1137
  const stopFonts = onBoardTextFontsChange(() => {
918
1138
  editor.relayoutText();
919
1139
  fontEpoch += 1;
920
1140
  scene?.redrawText();
921
1141
  scheduleTick();
922
1142
  });
923
- const stopAssets = assets.subscribe((key) => {
924
- arrivals.add(key);
1143
+ const stopAssets = assets.subscribe((key, change) => {
1144
+ if (change === "texture") arrivals.add(key);
1145
+ else if (scene?.counts().far) toned.add(key);
1146
+ else return;
925
1147
  scheduleTick();
926
1148
  });
927
1149
  const resizeObserver = new ResizeObserver((entries) => {
928
1150
  const entry = entries[entries.length - 1];
929
1151
  if (entry) canvasBox = measureBoardCanvasBox(entry, host.ownerDocument.defaultView?.devicePixelRatio ?? 1);
930
- resize();
1152
+ flushResize();
931
1153
  });
932
1154
  return {
933
1155
  ready: (async () => {
934
1156
  const instance = new Application();
1157
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
935
1158
  try {
936
1159
  await instance.init({
937
- antialias: true,
1160
+ antialias: density < ANTIALIAS_MAX_DENSITY,
938
1161
  backgroundAlpha: 0,
939
1162
  width: host.clientWidth,
940
1163
  height: host.clientHeight,
@@ -1042,16 +1265,8 @@ function mountBoardStage(host, options) {
1042
1265
  stopMarch();
1043
1266
  stopRefine();
1044
1267
  cancelScriptPointers();
1045
- cancelAnimationFrame(scriptTickFrame);
1046
- scriptTickFrame = 0;
1047
- cancelAnimationFrame(renderFrame);
1048
- cancelAnimationFrame(tickFrame);
1049
- cancelAnimationFrame(cardTickFrame);
1050
- if (cardTickTimer !== null) clearTimeout(cardTickTimer);
1051
- renderFrame = 0;
1052
- tickFrame = 0;
1053
- cardTickFrame = 0;
1054
- cardTickTimer = null;
1268
+ cancelFrames();
1269
+ stopPrefetch();
1055
1270
  return;
1056
1271
  }
1057
1272
  player.reset();
@@ -1065,7 +1280,7 @@ function mountBoardStage(host, options) {
1065
1280
  },
1066
1281
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
1067
1282
  stats() {
1068
- const { textures, cooling, bytes } = assets.stats();
1283
+ const { textures, cooling, bytes, loading, prefetching } = assets.stats();
1069
1284
  return {
1070
1285
  ...scene?.counts() ?? {
1071
1286
  live: 0,
@@ -1073,7 +1288,13 @@ function mountBoardStage(host, options) {
1073
1288
  },
1074
1289
  textures,
1075
1290
  cooling,
1076
- textureBytes: bytes
1291
+ textureBytes: bytes,
1292
+ loading: loading ?? 0,
1293
+ prefetching: prefetching ?? 0,
1294
+ waiting: waitingCards(),
1295
+ settling: scene?.settling ?? false,
1296
+ frame: frameStats(),
1297
+ interval: clock.interval
1077
1298
  };
1078
1299
  },
1079
1300
  toWorld: (clientX, clientY) => pointToWorld(localPoint(clientX, clientY), renderCamera()),
@@ -1090,16 +1311,14 @@ function mountBoardStage(host, options) {
1090
1311
  stopMaintenance?.();
1091
1312
  stopDensity?.();
1092
1313
  stopEditor();
1314
+ stopActivations();
1315
+ longPress.cancel();
1316
+ ghostClicks.dispose();
1093
1317
  stopFonts();
1094
1318
  stopAssets();
1095
1319
  resizeObserver.disconnect();
1096
- cancelAnimationFrame(scriptTickFrame);
1097
- cancelAnimationFrame(resizeFrame);
1098
- cancelAnimationFrame(renderFrame);
1099
- cancelAnimationFrame(tickFrame);
1100
- cancelAnimationFrame(cardTickFrame);
1101
- if (cardTickTimer !== null) clearTimeout(cardTickTimer);
1102
- cardTickTimer = null;
1320
+ cancelFrames();
1321
+ stopPrefetch();
1103
1322
  for (const [type, listener] of listeners) host.removeEventListener(type, listener);
1104
1323
  editor.setPlayedItems(/* @__PURE__ */ new Map());
1105
1324
  editor.setScriptItems(/* @__PURE__ */ new Map());
@@ -8,12 +8,14 @@ type BoardImageTexturePool = {
8
8
  declare function loadBoardImageTexture(url: string): Promise<Texture>;
9
9
  declare function unloadBoardImageTexture(url: string, texture: Texture | null): Promise<void>;
10
10
  /**
11
- * Decode an image no larger than `edge` along its long side, for sources
12
- * without CDN variants. The texture is unshared; destroy it to release it.
11
+ * Decode an image no larger than `edge` along its long side (short side with
12
+ * `cover`), for sources without CDN variants. The texture is unshared; destroy
13
+ * it to release it.
13
14
  */
14
- declare function loadFittedImageTexture(url: string, edge: number): Promise<Texture>;
15
+ declare function loadFittedImageTexture(url: string, edge: number, cover?: boolean): Promise<Texture>;
15
16
  /** A smaller unshared copy of a decoded texture, or null when it cannot be read back. */
16
17
  declare function shrinkBoardTexture(texture: Texture, edge: number): Promise<Texture | null>;
18
+ declare function readTextureTone(texture: Texture): Promise<number | null>;
17
19
  /**
18
20
  * Share image textures by resolved URL without sharing Board ownership.
19
21
  *
@@ -28,4 +30,4 @@ declare function createBoardImageTexturePool(options?: {
28
30
  }): BoardImageTexturePool;
29
31
  declare const boardImageTexturePool: BoardImageTexturePool;
30
32
  //#endregion
31
- export { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture };
33
+ export { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture };