@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
@@ -1,6 +1,6 @@
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";
3
+ import { itemBounds, pointToWorld, rectsIntersect, screenPoint, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
4
4
  import { boardScriptScope, composeBoardScriptItems, prepareBoardScriptOutput, validateBoardScriptCommit } from "../model/script-scene.js";
5
5
  import { boardScriptTarget } from "../model/script-hit.js";
6
6
  import { createBoardAnimationSampler, resolveBoardAnimationCamera } from "../model/sampling.js";
@@ -13,20 +13,25 @@ import { resizeCursorForHandle } from "../editor/selection-transform.js";
13
13
  import { parseBoardCssColor } from "../render/css-color.js";
14
14
  import { itemAlpha } from "../render/palette.js";
15
15
  import { onBoardTextFontsChange } from "../render/text-measurement.js";
16
+ import { boardLodRatio, boardLodSize } from "../render/lod.js";
16
17
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
17
18
  import { getBoardResolution } from "../render/text-resolution.js";
18
19
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
19
20
  import { createBoardScriptHost } from "../render/script-host.js";
20
21
  import { defaultBoardRenderStyle } from "../render/style.js";
21
22
  import { boardRenderTheme } from "../render/theme.js";
22
- import { createBoardScriptObservers } from "../model/script-observers.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,8 +147,15 @@ 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
- const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera, scheduleTick);
158
+ const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera, scheduleTick, options.viewLayer);
129
159
  const player = createBoardPlayer({ resolveColor: (color) => {
130
160
  const current = theme();
131
161
  return {
@@ -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,37 @@ 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
+ const hit = resolveRenderer(item, lastContext).hitAt?.(item, point, lastContext);
516
+ return hit && (options.handlesHit?.(item, hit.action) ?? true) ? hit : null;
517
+ }
518
+ function hoverHit(input) {
519
+ if (!options.onHit) return;
520
+ const item = input && editor.interaction.type === "idle" && editor.hoverId ? editor.itemById(editor.hoverId) : null;
521
+ const hit = item && input ? hitAt(item, input.world) : null;
522
+ const next = item && hit ? {
523
+ itemId: item.id,
524
+ action: hit.action,
525
+ cursor: hit.cursor ?? "pointer"
526
+ } : null;
527
+ if (next?.itemId === hoveredHit?.itemId && next?.action === hoveredHit?.action) return;
528
+ for (const id of [hoveredHit?.itemId, next?.itemId]) if (id) staleIds.add(id);
529
+ hoveredHit = next;
530
+ scheduleTick();
531
+ }
532
+ function pressHit(event, input) {
533
+ if (!options.onHit || event.button !== 0) return false;
534
+ const item = editor.itemAt(input.world);
535
+ const hit = item ? hitAt(item, input.world) : null;
536
+ if (!item || !hit || options.onHit(item, hit.action, boardStagePress(event)) === false) return false;
537
+ event.preventDefault();
538
+ if (event.pointerType !== "mouse") ghostClicks.arm();
539
+ return true;
540
+ }
477
541
  function cursorFor() {
478
542
  if (scriptMode === "interact" && scriptHover) {
479
543
  const output = scriptOutputs.get(scriptHover.owner);
@@ -488,49 +552,191 @@ function mountBoardStage(host, options) {
488
552
  case "draggingArrowHandle":
489
553
  case "brushing": return "crosshair";
490
554
  }
491
- if (editor.spaceHeld || editor.tool === "hand") return "grab";
555
+ if (interaction.type === "pressing") return "pointer";
556
+ if (editor.spaceHeld) return "grab";
557
+ if (hoveredHit) return hoveredHit.cursor;
492
558
  const control = editor.hoveredTransformControl;
493
559
  if (control?.kind === "resize") return resizeCursorForHandle(control.handle, editor.selectionTransform?.frame.rotation ?? 0);
494
560
  if (control?.kind === "rotate") return ROTATE_CURSOR;
495
- if (isCreationBoardTool(editor.tool)) return "crosshair";
496
561
  const hovered = editor.hoverId ? editor.itemById(editor.hoverId) : null;
562
+ if (hovered && (editor.tool === "select" || editor.tool === "hand") && opensOnTap(hovered)) return "pointer";
563
+ if (editor.tool === "hand") return "grab";
564
+ if (isCreationBoardTool(editor.tool)) return "crosshair";
497
565
  return hovered && !hovered.locked && editor.registry.capabilities(hovered).canMove ? "move" : "default";
498
566
  }
567
+ function requestFrame(work) {
568
+ if (!active || disposed) return;
569
+ frameWork |= work;
570
+ if (!frameHandle && !inFrame) {
571
+ chained = false;
572
+ frameHandle = requestAnimationFrame(runFrame);
573
+ }
574
+ }
575
+ function cancelFrames() {
576
+ cancelAnimationFrame(frameHandle);
577
+ frameHandle = 0;
578
+ frameWork = 0;
579
+ if (cardTickTimer !== null) clearTimeout(cardTickTimer);
580
+ cardTickTimer = null;
581
+ }
582
+ function runFrame(now) {
583
+ frameHandle = 0;
584
+ if (!app) return;
585
+ const start = performance.now();
586
+ let work = frameWork;
587
+ frameWork = 0;
588
+ inFrame = true;
589
+ try {
590
+ const camera = renderCamera();
591
+ const moving = editor.gestureActive || !frameCamera || camera.x !== frameCamera.x || camera.y !== frameCamera.y || camera.zoom !== frameCamera.zoom;
592
+ frameCamera = camera;
593
+ clock.begin(now, {
594
+ moving,
595
+ chained
596
+ });
597
+ if (work & FRAME_RESIZE && !applyResize()) work &= -17;
598
+ const synced = Boolean(work & 17);
599
+ if (synced) sync();
600
+ else if (work & FRAME_SCRIPTS) {
601
+ syncScripts();
602
+ if (scriptMode !== "paused" && scripts?.animated) frameWork |= FRAME_SCRIPTS;
603
+ }
604
+ if (work & FRAME_CARDS && !synced && scene) {
605
+ const next = scene.tick(performance.now());
606
+ work |= FRAME_RENDER;
607
+ if (next !== null) scheduleCardTick(next);
608
+ }
609
+ if ((work | frameWork) & FRAME_RENDER) {
610
+ frameWork &= -3;
611
+ app.render();
612
+ }
613
+ } finally {
614
+ inFrame = false;
615
+ frameTimes[frameCount++ % FRAME_SAMPLES] = performance.now() - start;
616
+ if (frameWork && active && !disposed) {
617
+ chained = true;
618
+ frameHandle = requestAnimationFrame(runFrame);
619
+ }
620
+ }
621
+ }
622
+ function waitingCards() {
623
+ if (!scene || surface.width === 0) return 0;
624
+ const view = visibleWorldRect(renderCamera(), surface.width, surface.height);
625
+ let waiting = 0;
626
+ for (const id of scene.liveIds()) {
627
+ const item = editor.scene.get(id);
628
+ const key = item && assets.assetKey(item);
629
+ if (key && !assets.hasError(key) && !assets.getTexture(key) && rectsIntersect(itemBounds(item.frame), view)) waiting += 1;
630
+ }
631
+ return waiting;
632
+ }
633
+ function frameStats() {
634
+ const samples = Array.from(frameTimes.subarray(0, Math.min(frameCount, FRAME_SAMPLES))).sort((a, b) => a - b);
635
+ const at = (ratio) => samples[Math.min(samples.length - 1, Math.floor(samples.length * ratio))] ?? 0;
636
+ return {
637
+ p50: at(.5),
638
+ p95: at(.95),
639
+ max: samples.at(-1) ?? 0
640
+ };
641
+ }
499
642
  function scheduleRender() {
500
- if (renderFrame || !app || !active) return;
501
- renderFrame = requestAnimationFrame(() => {
502
- renderFrame = 0;
503
- app?.render();
504
- });
643
+ requestFrame(FRAME_RENDER);
644
+ }
645
+ function scheduleTick() {
646
+ requestFrame(FRAME_SYNC);
647
+ }
648
+ function scheduleScriptTick() {
649
+ requestFrame(FRAME_SCRIPTS);
650
+ }
651
+ function resize() {
652
+ requestFrame(FRAME_RESIZE);
653
+ }
654
+ function flushResize() {
655
+ if (!active || disposed) return;
656
+ cancelAnimationFrame(frameHandle);
657
+ frameWork |= FRAME_RESIZE;
658
+ chained = false;
659
+ runFrame(performance.now());
660
+ chained = false;
505
661
  }
506
662
  function scheduleCardTick(wait) {
507
663
  if (!active || disposed) return;
508
664
  const due = performance.now() + wait;
509
- if (cardTickFrame || cardTickTimer !== null && cardTickDue <= due) return;
665
+ if (frameWork & FRAME_CARDS || cardTickTimer !== null && cardTickDue <= due) return;
510
666
  if (cardTickTimer !== null) clearTimeout(cardTickTimer);
511
667
  cardTickTimer = null;
512
668
  if (wait > 0) {
513
669
  cardTickDue = due;
514
670
  cardTickTimer = setTimeout(() => {
515
671
  cardTickTimer = null;
516
- scheduleCardTick(0);
672
+ requestFrame(FRAME_CARDS);
517
673
  }, wait);
518
674
  return;
519
675
  }
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
- });
676
+ requestFrame(FRAME_CARDS);
527
677
  }
528
- function scheduleTick() {
529
- if (tickFrame || !active || disposed) return;
530
- tickFrame = requestAnimationFrame(() => {
531
- tickFrame = 0;
532
- sync();
533
- });
678
+ function schedulePrefetch(camera, context) {
679
+ const margin = assets.prefetch ? options.prefetch ?? device.prefetch : 0;
680
+ if (margin <= 0 || surface.width === 0 || surface.height === 0) return;
681
+ const key = `${camera.x}|${camera.y}|${camera.zoom}|${surface.width}|${surface.height}|${editor.structureVersion}`;
682
+ if (key === prefetchKey) return;
683
+ prefetchKey = key;
684
+ prefetchContext = context;
685
+ prefetchMovedAt = performance.now();
686
+ if (!cancelPrefetch) awaitRest(margin, PREFETCH_REST_MS);
687
+ }
688
+ function awaitRest(margin, delay) {
689
+ cancelPrefetch = scheduleBoardIdle(() => {
690
+ cancelPrefetch = null;
691
+ const remaining = PREFETCH_REST_MS - (performance.now() - prefetchMovedAt);
692
+ if (remaining > 0) awaitRest(margin, remaining);
693
+ else if (prefetchContext) prefetch(margin, prefetchContext);
694
+ }, delay);
695
+ }
696
+ function stopPrefetch() {
697
+ cancelPrefetch?.();
698
+ cancelPrefetch = null;
699
+ prefetchKey = "";
700
+ prefetchContext = null;
701
+ assets.prefetch?.([]);
702
+ }
703
+ function prefetch(margin, context) {
704
+ if (!scene || !active || disposed) return;
705
+ if (editor.gestureActive) {
706
+ prefetchKey = "";
707
+ return;
708
+ }
709
+ const camera = renderCamera();
710
+ const view = visibleWorldRect(camera, surface.width, surface.height);
711
+ const culled = stableCullRect(view);
712
+ const padX = view.width * margin;
713
+ const padY = view.height * margin;
714
+ const ring = {
715
+ x: culled.x - padX,
716
+ y: culled.y - padY,
717
+ width: culled.width + padX * 2,
718
+ height: culled.height + padY * 2
719
+ };
720
+ const centerX = view.x + view.width / 2;
721
+ const centerY = view.y + view.height / 2;
722
+ const lod = currentLod();
723
+ const scale = textureScale();
724
+ const wanted = [];
725
+ for (const id of editor.idsInRect(ring)) {
726
+ const item = editor.scene.get(id);
727
+ if (!item || rectsIntersect(itemBounds(item.frame), culled)) continue;
728
+ const renderer = resolveRenderer(item, context);
729
+ if (renderer.renderFar && boardLodRatio(renderer, boardLodSize(renderer, item), camera.zoom, lod) < 1) continue;
730
+ const { frame } = item;
731
+ const distance = (frame.x + frame.width / 2 - centerX) ** 2 + (frame.y + frame.height / 2 - centerY) ** 2;
732
+ wanted.push({
733
+ item,
734
+ edge: boardTextureEdge(item, scale),
735
+ distance
736
+ });
737
+ }
738
+ wanted.sort((a, b) => a.distance - b.distance);
739
+ assets.prefetch?.(wanted.slice(0, PREFETCH_LIMIT));
534
740
  }
535
741
  function cancelScriptPointers(owner, retained) {
536
742
  scriptPointers.cancelScripts((pointerId, target) => {
@@ -559,15 +765,6 @@ function mountBoardStage(host, options) {
559
765
  }));
560
766
  scripts.sync(editor.document, editor.scene, camera, scriptTime, scriptMode === "interact", visibleIds());
561
767
  }
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
768
  function createScripts() {
572
769
  return createBoardScriptHost({
573
770
  host,
@@ -686,8 +883,13 @@ function mountBoardStage(host, options) {
686
883
  const center = pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera);
687
884
  assets.focus(loadFocus(center, camera));
688
885
  adoptArrivals();
886
+ if (toned.size > 0) {
887
+ scene?.repaintFar(toned, assets.assetKey);
888
+ toned.clear();
889
+ }
689
890
  textResolution.update(textureScale());
690
891
  const context = renderContext(current, renderScene, frame.time);
892
+ lastContext = context;
691
893
  if (animationId && effectSampler) {
692
894
  context.time = editor.playhead?.time ?? frame.time;
693
895
  context.frameAt = effectSampler.frameAt;
@@ -723,9 +925,11 @@ function mountBoardStage(host, options) {
723
925
  gestureActive: editor.gestureActive && editor.interaction.type !== "panning",
724
926
  focus: center,
725
927
  view: ids ? cullView(camera) : void 0,
726
- lod: options.lod?.()
928
+ lod: currentLod(),
929
+ budget: clock
727
930
  });
728
931
  staleIds.clear();
932
+ schedulePrefetch(camera, context);
729
933
  if (scene.liveVersion !== liveVersion) {
730
934
  liveVersion = scene.liveVersion;
731
935
  options.onLiveChange?.(scene.liveIds());
@@ -749,29 +953,26 @@ function mountBoardStage(host, options) {
749
953
  const cardWait = scene.tick(performance.now());
750
954
  if (cardWait !== null) scheduleCardTick(cardWait);
751
955
  }
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
- });
956
+ function applyResize() {
957
+ if (!app) return false;
958
+ const size = {
959
+ width: host.clientWidth,
960
+ height: host.clientHeight
961
+ };
962
+ if (size.width <= 0 || size.height <= 0) return false;
963
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
964
+ const canvas = boardCanvasSize(canvasBox ?? {
965
+ ...size,
966
+ density,
967
+ device: null
968
+ }, density);
969
+ 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);
970
+ if (size.width !== surface.width || size.height !== surface.height) {
971
+ surface = size;
972
+ editor.surfaceSize = surface;
973
+ options.onSurfaceChange?.(surface);
974
+ }
975
+ return true;
775
976
  }
776
977
  const localPoint = (x, y) => elementClientToLocal(host)(x, y);
777
978
  const inViews = (event) => event.target instanceof Node && views.element.contains(event.target);
@@ -853,8 +1054,11 @@ function mountBoardStage(host, options) {
853
1054
  return;
854
1055
  }
855
1056
  if (options.onPointer?.("down", event, input) === false) return;
1057
+ if (pressHit(event, input)) return;
856
1058
  pointers.start(event);
857
1059
  editor.pointerDown(input);
1060
+ if (options.onLongPress && editor.itemAt(input.world)) longPress.start(event);
1061
+ else longPress.cancel();
858
1062
  }
859
1063
  function handlePointerMove(event) {
860
1064
  if (!active || pointers.has(event.pointerId) || inViews(event)) return;
@@ -863,21 +1067,28 @@ function mountBoardStage(host, options) {
863
1067
  function routePointerMove(event) {
864
1068
  const input = toInput(event);
865
1069
  if (scriptPointer("move", event, input)) return;
1070
+ longPress.move(event);
866
1071
  if (options.onPointer?.("move", event, input) === false) return;
867
1072
  editor.pointerMove(input);
1073
+ hoverHit(input);
868
1074
  }
869
1075
  function handlePointerUp(event) {
870
1076
  const input = toInput(event);
871
1077
  if (scriptPointer("up", event, input)) return;
1078
+ longPress.end(event);
872
1079
  if (options.onPointer?.("up", event, input) === false) return;
873
1080
  editor.pointerUp(input);
874
- if (event.type === "pointercancel" || event.pointerType !== "mouse") editor.pointerLeave();
1081
+ if (event.type === "pointercancel" || event.pointerType !== "mouse") {
1082
+ editor.pointerLeave();
1083
+ hoverHit(null);
1084
+ }
875
1085
  }
876
1086
  function handlePointerLeave(event) {
877
1087
  if (event.buttons !== 0) return;
878
1088
  scriptPointer("leave", event, toInput(event));
879
1089
  if (options.onPointer?.("leave", event, toInput(event)) === false) return;
880
1090
  editor.pointerLeave();
1091
+ hoverHit(null);
881
1092
  }
882
1093
  function handleWheel(event) {
883
1094
  if (inViews(event)) return;
@@ -896,7 +1107,7 @@ function mountBoardStage(host, options) {
896
1107
  return;
897
1108
  }
898
1109
  if (item && item.type !== "frame" && item.type !== "arrow") return;
899
- const label = editor.labelItemAt(point);
1110
+ const label = editor.labelItemAt(point, styleOf(theme()).frameLabelSize);
900
1111
  if (label) editor.editingId = label.id;
901
1112
  else if (!item) editor.beginTextDraft(point);
902
1113
  }
@@ -914,27 +1125,40 @@ function mountBoardStage(host, options) {
914
1125
  ["dblclick", handleDoubleClick]
915
1126
  ];
916
1127
  const stopEditor = editor.subscribe(LIVE_CHANNELS, scheduleTick);
1128
+ const stopActivations = editor.subscribeActivations(({ itemId, pointerType, selected }) => {
1129
+ const item = editor.itemById(itemId);
1130
+ if (!item || !options.onActivate) return;
1131
+ if (pointerType !== "mouse") ghostClicks.arm();
1132
+ options.onActivate(item, {
1133
+ itemId,
1134
+ pointerType,
1135
+ selected
1136
+ });
1137
+ });
917
1138
  const stopFonts = onBoardTextFontsChange(() => {
918
1139
  editor.relayoutText();
919
1140
  fontEpoch += 1;
920
1141
  scene?.redrawText();
921
1142
  scheduleTick();
922
1143
  });
923
- const stopAssets = assets.subscribe((key) => {
924
- arrivals.add(key);
1144
+ const stopAssets = assets.subscribe((key, change) => {
1145
+ if (change === "texture") arrivals.add(key);
1146
+ else if (scene?.counts().far) toned.add(key);
1147
+ else return;
925
1148
  scheduleTick();
926
1149
  });
927
1150
  const resizeObserver = new ResizeObserver((entries) => {
928
1151
  const entry = entries[entries.length - 1];
929
1152
  if (entry) canvasBox = measureBoardCanvasBox(entry, host.ownerDocument.defaultView?.devicePixelRatio ?? 1);
930
- resize();
1153
+ flushResize();
931
1154
  });
932
1155
  return {
933
1156
  ready: (async () => {
934
1157
  const instance = new Application();
1158
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
935
1159
  try {
936
1160
  await instance.init({
937
- antialias: true,
1161
+ antialias: density < ANTIALIAS_MAX_DENSITY,
938
1162
  backgroundAlpha: 0,
939
1163
  width: host.clientWidth,
940
1164
  height: host.clientHeight,
@@ -1042,16 +1266,8 @@ function mountBoardStage(host, options) {
1042
1266
  stopMarch();
1043
1267
  stopRefine();
1044
1268
  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;
1269
+ cancelFrames();
1270
+ stopPrefetch();
1055
1271
  return;
1056
1272
  }
1057
1273
  player.reset();
@@ -1065,7 +1281,7 @@ function mountBoardStage(host, options) {
1065
1281
  },
1066
1282
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
1067
1283
  stats() {
1068
- const { textures, cooling, bytes } = assets.stats();
1284
+ const { textures, cooling, bytes, loading, prefetching } = assets.stats();
1069
1285
  return {
1070
1286
  ...scene?.counts() ?? {
1071
1287
  live: 0,
@@ -1073,7 +1289,13 @@ function mountBoardStage(host, options) {
1073
1289
  },
1074
1290
  textures,
1075
1291
  cooling,
1076
- textureBytes: bytes
1292
+ textureBytes: bytes,
1293
+ loading: loading ?? 0,
1294
+ prefetching: prefetching ?? 0,
1295
+ waiting: waitingCards(),
1296
+ settling: scene?.settling ?? false,
1297
+ frame: frameStats(),
1298
+ interval: clock.interval
1077
1299
  };
1078
1300
  },
1079
1301
  toWorld: (clientX, clientY) => pointToWorld(localPoint(clientX, clientY), renderCamera()),
@@ -1090,16 +1312,14 @@ function mountBoardStage(host, options) {
1090
1312
  stopMaintenance?.();
1091
1313
  stopDensity?.();
1092
1314
  stopEditor();
1315
+ stopActivations();
1316
+ longPress.cancel();
1317
+ ghostClicks.dispose();
1093
1318
  stopFonts();
1094
1319
  stopAssets();
1095
1320
  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;
1321
+ cancelFrames();
1322
+ stopPrefetch();
1103
1323
  for (const [type, listener] of listeners) host.removeEventListener(type, listener);
1104
1324
  editor.setPlayedItems(/* @__PURE__ */ new Map());
1105
1325
  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 };