@neta-art/cohub 11.5.1 → 12.1.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 (82) hide show
  1. package/dist/board/editor/animation.js +2 -1
  2. package/dist/board/editor/creation.js +1 -1
  3. package/dist/board/editor/document-edits.d.ts +1 -1
  4. package/dist/board/editor/document-edits.js +1 -19
  5. package/dist/board/editor/document.js +2 -1
  6. package/dist/board/editor/index.d.ts +2 -1
  7. package/dist/board/editor/index.js +2 -1
  8. package/dist/board/editor/snapshots.js +11 -13
  9. package/dist/board/export/scene.js +1 -0
  10. package/dist/board/model/client-geometry.d.ts +22 -0
  11. package/dist/board/model/client-geometry.js +94 -0
  12. package/dist/board/model/document-share.d.ts +5 -0
  13. package/dist/board/model/document-share.js +27 -0
  14. package/dist/board/model/index.d.ts +5 -4
  15. package/dist/board/model/index.js +4 -3
  16. package/dist/board/model/items/create.d.ts +5 -1
  17. package/dist/board/model/items/create.js +29 -58
  18. package/dist/board/model/items/index.d.ts +2 -2
  19. package/dist/board/model/items/index.js +2 -2
  20. package/dist/board/model/items/registry.js +2 -5
  21. package/dist/board/model/json.d.ts +4 -0
  22. package/dist/board/model/json.js +22 -0
  23. package/dist/board/model/media-playback.d.ts +6 -3
  24. package/dist/board/player/index.d.ts +6 -2
  25. package/dist/board/player/index.js +33 -15
  26. package/dist/board/render/card-tag.d.ts +0 -1
  27. package/dist/board/render/card-tag.js +15 -17
  28. package/dist/board/render/index.d.ts +3 -3
  29. package/dist/board/render/index.js +3 -3
  30. package/dist/board/render/renderers/arrow-card-renderer.js +8 -6
  31. package/dist/board/render/renderers/audio-card-renderer.js +10 -6
  32. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -1
  33. package/dist/board/render/renderers/base-card-renderer.js +2 -3
  34. package/dist/board/render/renderers/board-renderer-registry.d.ts +6 -2
  35. package/dist/board/render/renderers/custom-card-renderer.js +23 -18
  36. package/dist/board/render/renderers/file-card-renderer.js +14 -9
  37. package/dist/board/render/renderers/frame-card-renderer.js +7 -4
  38. package/dist/board/render/renderers/image-card-renderer.js +13 -49
  39. package/dist/board/render/renderers/model-card-renderer.js +4 -1
  40. package/dist/board/render/renderers/shape-card-renderer.js +8 -6
  41. package/dist/board/render/renderers/task-card-renderer.js +13 -10
  42. package/dist/board/render/renderers/text-card-renderer.js +7 -5
  43. package/dist/board/render/renderers/unknown-card-renderer.js +8 -6
  44. package/dist/board/render/renderers/video-card-renderer.js +19 -18
  45. package/dist/board/render/script-host.d.ts +2 -0
  46. package/dist/board/render/script-host.js +44 -9
  47. package/dist/board/render/script-resolution.d.ts +4 -0
  48. package/dist/board/render/script-resolution.js +18 -0
  49. package/dist/board/render/surface.d.ts +9 -2
  50. package/dist/board/render/surface.js +15 -1
  51. package/dist/board/render/text-resolution.d.ts +4 -7
  52. package/dist/board/render/text-resolution.js +20 -22
  53. package/dist/board/replica/index.js +4 -1
  54. package/dist/board/stage/assets.d.ts +12 -8
  55. package/dist/board/stage/assets.js +173 -88
  56. package/dist/board/stage/canvas-size.d.ts +18 -0
  57. package/dist/board/stage/canvas-size.js +31 -0
  58. package/dist/board/stage/index.d.ts +3 -3
  59. package/dist/board/stage/index.js +3 -3
  60. package/dist/board/stage/lru.d.ts +3 -2
  61. package/dist/board/stage/lru.js +8 -4
  62. package/dist/board/stage/pointer.d.ts +1 -1
  63. package/dist/board/stage/pointer.js +4 -5
  64. package/dist/board/stage/scene.d.ts +8 -1
  65. package/dist/board/stage/scene.js +72 -10
  66. package/dist/board/stage/stage.d.ts +1 -0
  67. package/dist/board/stage/stage.js +151 -63
  68. package/dist/board/stage/text-resolution-tracker.d.ts +8 -0
  69. package/dist/board/stage/text-resolution-tracker.js +33 -0
  70. package/dist/board/stage/views.d.ts +7 -2
  71. package/dist/board/stage/views.js +50 -21
  72. package/dist/chunks/environment.d.ts +16 -2
  73. package/dist/index.js +65 -22
  74. package/dist/protocol/dist/board-constants.js +56 -1
  75. package/dist/protocol/dist/board-layout.d.ts +8 -1
  76. package/dist/protocol/dist/board-layout.js +16 -4
  77. package/dist/protocol/dist/board-model.js +13 -19
  78. package/dist/protocol/dist/board-schema.d.ts +52 -0
  79. package/dist/protocol/dist/board-schema.js +8 -0
  80. package/dist/protocol/dist/core/media.js +1 -0
  81. package/dist/protocol/dist/index.d.ts +2 -2
  82. package/package.json +1 -1
@@ -1,22 +1,25 @@
1
1
  import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
2
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
- import { screenPoint, screenToWorld, stableCullRect, visibleWorldRect, worldPoint } from "../model/geometry.js";
3
+ import { pointToWorld, 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";
7
7
  import { resolveSceneArrow } from "../model/arrow-geometry.js";
8
8
  import { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../model/palette.js";
9
+ import { elementClientToLocal } from "../model/client-geometry.js";
9
10
  import { isCreationBoardTool } from "../editor/tool.js";
10
11
  import { resizeCursorForHandle } from "../editor/selection-transform.js";
11
12
  import { parseBoardCssColor } from "../render/css-color.js";
12
13
  import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
13
14
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
14
- import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
15
+ import { getBoardResolution } from "../render/text-resolution.js";
15
16
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
16
17
  import { createBoardScriptHost } from "../render/script-host.js";
17
18
  import { defaultBoardRenderStyle } from "../render/style.js";
18
- import { createBoardScriptObservers } from "../model/script-observers.js";
19
19
  import { createBoardPlayer } from "../player/index.js";
20
+ import { createBoardScriptObservers } from "../model/script-observers.js";
21
+ import { boardTextureEdge } from "./assets.js";
22
+ import { boardCanvasSize, measureBoardCanvasBox } from "./canvas-size.js";
20
23
  import { observeDisplayDensity } from "./display-density.js";
21
24
  import { toBoardPointerEvent } from "./pointer.js";
22
25
  import { createBoardPointerGestures } from "./pointer-gestures.js";
@@ -24,6 +27,7 @@ import { createBoardScene } from "./scene.js";
24
27
  import { scheduleBoardMaintenance } from "./maintenance.js";
25
28
  import { createBoardScriptPointerRouter } from "./script-pointer.js";
26
29
  import { drawBoardOverlay } from "./overlay.js";
30
+ import { createTextResolutionTracker } from "./text-resolution-tracker.js";
27
31
  import { createBoardItemViewLayer } from "./views.js";
28
32
  import { Application, Container, Graphics, RendererType } from "pixi.js";
29
33
  //#region src/board/stage/stage.ts
@@ -49,6 +53,7 @@ function defaultTheme() {
49
53
  const NO_IDS = /* @__PURE__ */ new Set();
50
54
  const MARCH_STEP_MS = 160;
51
55
  const MARCH_SMOOTH_MS = 40;
56
+ const TEXTURE_SETTLE_MS = 150;
52
57
  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`;
53
58
  const defaultStyles = /* @__PURE__ */ new Map();
54
59
  function styleOf(theme) {
@@ -102,13 +107,14 @@ function mountBoardStage(host, options) {
102
107
  let disposed = false;
103
108
  let renderFrame = 0;
104
109
  let tickFrame = 0;
110
+ let scriptTickFrame = 0;
105
111
  let cardTickFrame = 0;
106
112
  let cardTickTimer = null;
107
113
  let cardTickDue = 0;
108
114
  const staleIds = /* @__PURE__ */ new Set();
109
115
  let resizeFrame = 0;
116
+ let canvasBox = null;
110
117
  let cardEpoch = 0;
111
- let assetVersion = 0;
112
118
  let surface = {
113
119
  width: 0,
114
120
  height: 0
@@ -116,7 +122,7 @@ function mountBoardStage(host, options) {
116
122
  let playbackCamera = null;
117
123
  let cursor = "";
118
124
  const renderCamera = () => playbackCamera ?? editor.camera;
119
- const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera);
125
+ const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera, scheduleTick);
120
126
  const player = createBoardPlayer({ resolveColor: (color) => {
121
127
  const current = theme();
122
128
  return {
@@ -140,29 +146,71 @@ function mountBoardStage(host, options) {
140
146
  scene: editor.scene
141
147
  };
142
148
  options.onVisibleChange?.(cull.ids);
143
- requestAssets = true;
144
149
  return cull.ids;
145
150
  }
146
- let requestAssets = true;
147
- function syncAssets(ids) {
148
- if (!requestAssets || !ids) return;
149
- requestAssets = false;
151
+ const textureScale = () => renderCamera().zoom * (app?.renderer.resolution ?? 1);
152
+ const textResolution = createTextResolutionTracker(TEXTURE_SETTLE_MS, scheduleTick);
153
+ let refined = {
154
+ scale: 0,
155
+ geometry: 0
156
+ };
157
+ let pendingRefine = "";
158
+ let refineTimer = null;
159
+ function stopRefine() {
160
+ if (refineTimer !== null) clearTimeout(refineTimer);
161
+ refineTimer = null;
162
+ refined = {
163
+ scale: 0,
164
+ geometry: 0
165
+ };
166
+ pendingRefine = "";
167
+ }
168
+ function scheduleRefine() {
169
+ const scale = textureScale();
170
+ const geometry = editor.geometryVersion;
171
+ const key = `${scale}:${geometry}`;
172
+ if (key === pendingRefine) return;
173
+ pendingRefine = key;
174
+ if (refineTimer !== null) clearTimeout(refineTimer);
175
+ refineTimer = null;
176
+ if (refined.scale === 0 || scale <= refined.scale && geometry === refined.geometry) {
177
+ refined = {
178
+ scale,
179
+ geometry
180
+ };
181
+ return;
182
+ }
183
+ refineTimer = setTimeout(() => {
184
+ refineTimer = null;
185
+ refined = {
186
+ scale,
187
+ geometry
188
+ };
189
+ for (const id of scene?.liveIds() ?? []) {
190
+ const item = editor.scene.get(id);
191
+ if (item) assets.requestItem(item, boardTextureEdge(item, scale));
192
+ }
193
+ }, TEXTURE_SETTLE_MS);
194
+ }
195
+ const naturalIds = /* @__PURE__ */ new Set();
196
+ let liveVersion = -1;
197
+ function adoptNaturalSizes() {
198
+ if (naturalIds.size === 0) return;
150
199
  const sizes = [];
151
- for (const id of ids) {
200
+ for (const id of naturalIds) {
152
201
  const item = editor.itemById(id);
153
- if (!item) continue;
154
- const key = assets.assetKey(item);
155
- if (!key) continue;
156
- assets.requestItem(item);
202
+ const key = item && assets.assetKey(item);
203
+ if (!item || !key) continue;
157
204
  const artifact = item.type === "task" ? featuredTaskArtifact(item.props.snapshot.artifacts) : null;
158
205
  const recorded = artifact?.type === "image" || artifact?.type === "video" ? artifact : item.type === "image" || item.type === "video" ? item.props.snapshot : null;
159
206
  if (!recorded || recorded.naturalWidth && recorded.naturalHeight) continue;
160
207
  const natural = assets.getNaturalSize(key);
161
208
  if (natural?.width && natural.height) sizes.push({
162
- id: item.id,
209
+ id,
163
210
  ...natural
164
211
  });
165
212
  }
213
+ naturalIds.clear();
166
214
  if (sizes.length > 0) editor.adoptMediaNaturalSizes(sizes);
167
215
  }
168
216
  let lifted = null;
@@ -196,6 +244,7 @@ function mountBoardStage(host, options) {
196
244
  themeKey: current.key,
197
245
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
198
246
  zoom: renderCamera().zoom,
247
+ textResolution: textResolution.value,
199
248
  reducedMotion: options.reducedMotion?.() ?? false,
200
249
  labels: options.labels?.() ?? DEFAULT_BOARD_RENDER_LABELS,
201
250
  taskRun: options.taskRun ?? (() => null),
@@ -203,7 +252,11 @@ function mountBoardStage(host, options) {
203
252
  getTexture: (key) => assets.getTexture(key),
204
253
  hasError: (key) => assets.hasError(key),
205
254
  fileState: (path) => options.fileState?.(path) ?? "ok",
206
- acquireTexture: (key) => assets.acquire(key),
255
+ acquireTexture: (key, item) => {
256
+ assets.acquire(key);
257
+ assets.requestItem(item, boardTextureEdge(item, textureScale()));
258
+ if (assets.getTexture(key)) naturalIds.add(item.id);
259
+ },
207
260
  releaseTexture: (key) => assets.release(key)
208
261
  };
209
262
  }
@@ -442,9 +495,37 @@ function mountBoardStage(host, options) {
442
495
  }, owner, retained);
443
496
  if (!retained || scriptHover && scriptHover.owner === owner && !Object.hasOwn(retained, scriptHover.target)) scriptHover = null;
444
497
  }
498
+ function syncScripts() {
499
+ if (!scripts) return;
500
+ const now = performance.now();
501
+ if (scriptMode !== "paused") scriptTime += Math.min(250, Math.max(0, now - scriptLastTick));
502
+ scriptLastTick = now;
503
+ const camera = renderCamera();
504
+ if (scriptMode !== "paused" && scriptObservers.size) scriptObservers.sync(editor.document, editor.selection, () => ({
505
+ camera: { ...camera },
506
+ ...surface,
507
+ safeRect: options.safeRect?.() ?? {
508
+ x: 0,
509
+ y: 0,
510
+ ...surface
511
+ }
512
+ }));
513
+ scripts.sync(editor.document, editor.scene, camera, scriptTime, scriptMode === "interact", visibleIds());
514
+ }
515
+ function scheduleScriptTick() {
516
+ if (scriptTickFrame || !active || disposed) return;
517
+ scriptTickFrame = requestAnimationFrame(() => {
518
+ scriptTickFrame = 0;
519
+ if (tickFrame) return;
520
+ syncScripts();
521
+ if (scriptMode !== "paused" && scripts?.animated) scheduleScriptTick();
522
+ });
523
+ }
445
524
  function createScripts() {
446
525
  return createBoardScriptHost({
447
526
  host,
527
+ isMaterialized: (id) => Boolean(scene?.getNode(id)),
528
+ onSchedule: scheduleScriptTick,
448
529
  capabilities: {
449
530
  ...options.scriptCapabilities,
450
531
  "board.emit": async (id, value) => {
@@ -552,25 +633,10 @@ function mountBoardStage(host, options) {
552
633
  }
553
634
  }
554
635
  const renderScene = editor.scene;
555
- if (scripts) {
556
- const now = performance.now();
557
- if (scriptMode !== "paused") scriptTime += Math.min(250, Math.max(0, now - scriptLastTick));
558
- scriptLastTick = now;
559
- if (scriptMode !== "paused" && scriptObservers.size) scriptObservers.sync(editor.document, editor.selection, () => ({
560
- camera: { ...camera },
561
- ...surface,
562
- safeRect: options.safeRect?.() ?? {
563
- x: 0,
564
- y: 0,
565
- ...surface
566
- }
567
- }));
568
- scripts.sync(editor.document, renderScene, camera, scriptTime, scriptMode === "interact", visibleIds());
569
- }
570
636
  world.position.set(camera.x, camera.y);
571
637
  world.scale.set(camera.zoom);
572
638
  const ids = visibleIds();
573
- syncAssets(ids);
639
+ textResolution.update(textureScale());
574
640
  const context = renderContext(current, renderScene, frame.time);
575
641
  if (animationId && effectSampler) {
576
642
  context.time = editor.playhead?.time ?? frame.time;
@@ -593,19 +659,21 @@ function mountBoardStage(host, options) {
593
659
  getItem: (id) => renderScene.get(id) ?? null,
594
660
  visibleIds: ids,
595
661
  pinnedIds,
596
- globalSig: [
597
- assetVersion,
598
- cardEpoch,
599
- current.key,
600
- textZoomBucket(camera.zoom),
601
- getBoardResolution()
602
- ].join("|"),
662
+ globalSig: [cardEpoch, current.key].join("|"),
603
663
  staleIds,
604
664
  structureVersion: editor.structureVersion,
605
665
  geometryVersion: editor.geometryVersion,
606
- gestureActive: editor.gestureActive && editor.interaction.type !== "panning"
666
+ gestureActive: editor.gestureActive && editor.interaction.type !== "panning",
667
+ focus: pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera)
607
668
  });
608
669
  staleIds.clear();
670
+ if (scene.liveVersion !== liveVersion) {
671
+ liveVersion = scene.liveVersion;
672
+ options.onLiveChange?.(scene.liveIds());
673
+ }
674
+ adoptNaturalSizes();
675
+ scheduleRefine();
676
+ syncScripts();
609
677
  drawInteraction();
610
678
  views.sync({
611
679
  ids,
@@ -617,7 +685,8 @@ function mountBoardStage(host, options) {
617
685
  const nextCursor = cursorFor();
618
686
  if (nextCursor !== cursor) host.style.cursor = cursor = nextCursor;
619
687
  scheduleRender();
620
- if (frame.running || scene.animated || scriptMode !== "paused" && scripts?.animated) scheduleTick();
688
+ if (frame.running || scene.animated) scheduleTick();
689
+ if (scriptMode !== "paused" && scripts?.animated) scheduleScriptTick();
621
690
  const cardWait = scene.tick(performance.now());
622
691
  if (cardWait !== null) scheduleCardTick(cardWait);
623
692
  }
@@ -630,10 +699,14 @@ function mountBoardStage(host, options) {
630
699
  height: host.clientHeight
631
700
  };
632
701
  if (size.width <= 0 || size.height <= 0) return;
633
- const resolution = getBoardResolution(size);
634
- const sizeChanged = size.width !== surface.width || size.height !== surface.height;
635
- if (sizeChanged || app.renderer.resolution !== resolution) app.renderer.resize(size.width, size.height, resolution);
636
- if (sizeChanged) {
702
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
703
+ const canvas = boardCanvasSize(canvasBox ?? {
704
+ ...size,
705
+ density,
706
+ device: null
707
+ }, density);
708
+ 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);
709
+ if (size.width !== surface.width || size.height !== surface.height) {
637
710
  surface = size;
638
711
  editor.surfaceSize = surface;
639
712
  options.onSurfaceChange?.(surface);
@@ -641,10 +714,10 @@ function mountBoardStage(host, options) {
641
714
  sync();
642
715
  });
643
716
  }
644
- const rectOf = () => host.getBoundingClientRect();
717
+ const localPoint = (x, y) => elementClientToLocal(host)(x, y);
645
718
  const inViews = (event) => event.target instanceof Node && views.element.contains(event.target);
646
719
  function toInput(event) {
647
- return toBoardPointerEvent(event, rectOf(), renderCamera(), event.type === "pointermove" && editor.interaction.type === "drawing");
720
+ return toBoardPointerEvent(event, host, renderCamera(), event.type === "pointermove" && editor.interaction.type === "drawing");
648
721
  }
649
722
  function scriptTarget(input) {
650
723
  if (!scriptOutputs.size) return null;
@@ -751,13 +824,12 @@ function mountBoardStage(host, options) {
751
824
  if (inViews(event)) return;
752
825
  event.preventDefault();
753
826
  editor.takeCameraControl();
754
- const rect = rectOf();
755
- const point = screenPoint(event.clientX - rect.left, event.clientY - rect.top);
827
+ const point = localPoint(event.clientX, event.clientY);
756
828
  editor.wheel(point, event.deltaX, event.deltaY, event.ctrlKey || event.metaKey, event.deltaMode);
757
829
  }
758
830
  function handleDoubleClick(event) {
759
831
  if (inViews(event)) return;
760
- const point = screenToWorld(event.clientX, event.clientY, rectOf(), renderCamera());
832
+ const point = pointToWorld(localPoint(event.clientX, event.clientY), renderCamera());
761
833
  const item = editor.itemAt(point);
762
834
  if (options.onDoubleClick?.(item, point) === true || editor.readonly) return;
763
835
  if (item && !item.locked && editor.registry.capabilities(item).canEdit) {
@@ -783,19 +855,26 @@ function mountBoardStage(host, options) {
783
855
  ["dblclick", handleDoubleClick]
784
856
  ];
785
857
  const stopEditor = editor.subscribe(LIVE_CHANNELS, scheduleTick);
786
- const stopAssets = assets.subscribe(() => {
787
- assetVersion += 1;
788
- requestAssets = true;
858
+ const stopAssets = assets.subscribe((key) => {
859
+ const ids = scene?.holders(key);
860
+ if (!ids?.size) return;
861
+ for (const id of ids) {
862
+ staleIds.add(id);
863
+ naturalIds.add(id);
864
+ }
789
865
  scheduleTick();
790
866
  });
791
- const resizeObserver = new ResizeObserver(resize);
867
+ const resizeObserver = new ResizeObserver((entries) => {
868
+ const entry = entries[entries.length - 1];
869
+ if (entry) canvasBox = measureBoardCanvasBox(entry, host.ownerDocument.defaultView?.devicePixelRatio ?? 1);
870
+ resize();
871
+ });
792
872
  return {
793
873
  ready: (async () => {
794
874
  const instance = new Application();
795
875
  try {
796
876
  await instance.init({
797
877
  antialias: true,
798
- autoDensity: true,
799
878
  backgroundAlpha: 0,
800
879
  width: host.clientWidth,
801
880
  height: host.clientHeight,
@@ -820,6 +899,8 @@ function mountBoardStage(host, options) {
820
899
  position: "relative",
821
900
  zIndex: "1",
822
901
  display: "block",
902
+ width: "100%",
903
+ height: "100%",
823
904
  touchAction: "none"
824
905
  });
825
906
  host.insertBefore(instance.canvas, views.element);
@@ -838,10 +919,14 @@ function mountBoardStage(host, options) {
838
919
  world: nodes,
839
920
  overlay,
840
921
  getRenderer: resolveRenderer,
841
- onFarLayerFrame: scheduleTick
922
+ requestFrame: scheduleTick
842
923
  });
843
924
  for (const [type, listener, listenerOptions] of listeners) host.addEventListener(type, listener, listenerOptions);
844
- resizeObserver.observe(host);
925
+ try {
926
+ resizeObserver.observe(instance.canvas, { box: "device-pixel-content-box" });
927
+ } catch {
928
+ resizeObserver.observe(instance.canvas);
929
+ }
845
930
  const window = host.ownerDocument.defaultView;
846
931
  if (window) stopDensity = observeDisplayDensity(window, resize);
847
932
  resize();
@@ -895,7 +980,10 @@ function mountBoardStage(host, options) {
895
980
  if (!active) {
896
981
  pointers.cancel();
897
982
  stopMarch();
983
+ stopRefine();
898
984
  cancelScriptPointers();
985
+ cancelAnimationFrame(scriptTickFrame);
986
+ scriptTickFrame = 0;
899
987
  cancelAnimationFrame(renderFrame);
900
988
  cancelAnimationFrame(tickFrame);
901
989
  cancelAnimationFrame(cardTickFrame);
@@ -911,19 +999,18 @@ function mountBoardStage(host, options) {
911
999
  resize();
912
1000
  },
913
1001
  invalidate(invalidation) {
914
- if (invalidation?.cards) {
915
- cardEpoch += 1;
916
- requestAssets = true;
917
- }
1002
+ if (invalidation?.cards) cardEpoch += 1;
918
1003
  for (const id of invalidation?.items ?? []) staleIds.add(id);
919
1004
  scheduleTick();
920
1005
  },
921
1006
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
922
- toWorld: (clientX, clientY) => screenToWorld(clientX, clientY, rectOf(), renderCamera()),
1007
+ toWorld: (clientX, clientY) => pointToWorld(localPoint(clientX, clientY), renderCamera()),
923
1008
  destroy() {
924
1009
  disposed = true;
925
1010
  pointers.cancel();
926
1011
  stopMarch();
1012
+ stopRefine();
1013
+ textResolution.dispose();
927
1014
  scriptPointers.clear();
928
1015
  stopTransforms?.();
929
1016
  scriptObservers.clear();
@@ -933,6 +1020,7 @@ function mountBoardStage(host, options) {
933
1020
  stopEditor();
934
1021
  stopAssets();
935
1022
  resizeObserver.disconnect();
1023
+ cancelAnimationFrame(scriptTickFrame);
936
1024
  cancelAnimationFrame(resizeFrame);
937
1025
  cancelAnimationFrame(renderFrame);
938
1026
  cancelAnimationFrame(tickFrame);
@@ -0,0 +1,8 @@
1
+ //#region src/board/stage/text-resolution-tracker.d.ts
2
+ declare function createTextResolutionTracker(settleMs: number, onSettle: () => void): {
3
+ readonly value: number;
4
+ update(next: number): number;
5
+ dispose(): void;
6
+ };
7
+ //#endregion
8
+ export { createTextResolutionTracker };
@@ -0,0 +1,33 @@
1
+ import { boardTextResolution } from "../render/text-resolution.js";
2
+ //#region src/board/stage/text-resolution-tracker.ts
3
+ function createTextResolutionTracker(settleMs, onSettle) {
4
+ let target = 0;
5
+ let value = 0;
6
+ let settled = false;
7
+ let timer = null;
8
+ return {
9
+ get value() {
10
+ return value;
11
+ },
12
+ update(next) {
13
+ if (next !== target) {
14
+ target = next;
15
+ settled = false;
16
+ if (timer !== null) clearTimeout(timer);
17
+ timer = setTimeout(() => {
18
+ timer = null;
19
+ settled = true;
20
+ onSettle();
21
+ }, settleMs);
22
+ }
23
+ value = boardTextResolution(target, value, settled);
24
+ return value;
25
+ },
26
+ dispose() {
27
+ if (timer !== null) clearTimeout(timer);
28
+ timer = null;
29
+ }
30
+ };
31
+ }
32
+ //#endregion
33
+ export { createTextResolutionTracker };
@@ -7,7 +7,7 @@ import "../editor/index.js";
7
7
  //#region src/board/stage/views.d.ts
8
8
  type BoardItemViewContext = {
9
9
  editor: BoardEditor;
10
- zoom: number;
10
+ detailed: boolean;
11
11
  selected: boolean;
12
12
  toPointerEvent(event: PointerEvent): BoardPointerEvent;
13
13
  };
@@ -17,6 +17,11 @@ type BoardItemViewInstance = {
17
17
  };
18
18
  type BoardItemView = {
19
19
  type: string | ((item: BoardSceneItem) => boolean);
20
+ /** Optional screen-pixel threshold for expensive content, with built-in hysteresis. */
21
+ contentSize?: {
22
+ width: number;
23
+ height: number;
24
+ };
20
25
  mount(element: HTMLElement, item: BoardSceneItem, context: BoardItemViewContext): BoardItemViewInstance;
21
26
  };
22
27
  type BoardItemViewLayer = {
@@ -33,6 +38,6 @@ type BoardItemViewLayer = {
33
38
  }): void;
34
39
  destroy(): void;
35
40
  };
36
- declare function createBoardItemViewLayer(host: HTMLElement, editor: BoardEditor, views: readonly BoardItemView[], camera: () => BoardViewport): BoardItemViewLayer;
41
+ declare function createBoardItemViewLayer(host: HTMLElement, editor: BoardEditor, views: readonly BoardItemView[], camera: () => BoardViewport, onInvalidate?: () => void): BoardItemViewLayer;
37
42
  //#endregion
38
43
  export { BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, createBoardItemViewLayer };
@@ -1,55 +1,80 @@
1
+ import { itemBounds } from "../model/geometry.js";
1
2
  import { toBoardPointerEvent } from "./pointer.js";
2
3
  //#region src/board/stage/views.ts
3
4
  const VISIBLE_MARGIN = 240;
4
- function screenStyle(frame, camera, order) {
5
- const { zoom } = camera;
5
+ function worldStyle(frame, order) {
6
6
  return [
7
7
  `z-index:${order}`,
8
- `left:${frame.x * zoom + camera.x}px`,
9
- `top:${frame.y * zoom + camera.y}px`,
10
- `width:${frame.width * zoom}px`,
11
- `height:${frame.height * zoom}px`,
12
- `--board-zoom:${zoom}`,
8
+ `left:${frame.x}px`,
9
+ `top:${frame.y}px`,
10
+ `width:${frame.width}px`,
11
+ `height:${frame.height}px`,
13
12
  `transform:rotate(${frame.rotation}deg)`
14
13
  ].join(";");
15
14
  }
16
15
  function onScreen(frame, camera, surface) {
17
- const left = frame.x * camera.zoom + camera.x;
18
- const top = frame.y * camera.zoom + camera.y;
19
- return left + frame.width * camera.zoom >= -240 && top + frame.height * camera.zoom >= -240 && left <= surface.width + VISIBLE_MARGIN && top <= surface.height + VISIBLE_MARGIN;
16
+ const bounds = itemBounds(frame);
17
+ const left = bounds.x * camera.zoom + camera.x;
18
+ const top = bounds.y * camera.zoom + camera.y;
19
+ return left + bounds.width * camera.zoom >= -240 && top + bounds.height * camera.zoom >= -240 && left <= surface.width + VISIBLE_MARGIN && top <= surface.height + VISIBLE_MARGIN;
20
20
  }
21
- function createBoardItemViewLayer(host, editor, views, camera) {
21
+ function createBoardItemViewLayer(host, editor, views, camera, onInvalidate = () => {}) {
22
22
  const element = host.ownerDocument.createElement("div");
23
23
  element.dataset.boardViews = "";
24
24
  element.style.cssText = "position:absolute;inset:0;z-index:2;overflow:hidden;pointer-events:none";
25
25
  host.appendChild(element);
26
+ const world = host.ownerDocument.createElement("div");
27
+ world.style.cssText = "position:absolute;inset:0;transform-origin:0 0;pointer-events:none";
28
+ element.appendChild(world);
29
+ let cameraStyle = "";
26
30
  const mounted = /* @__PURE__ */ new Map();
27
- const toPointerEvent = (event) => toBoardPointerEvent(event, host.getBoundingClientRect(), camera());
31
+ const pointers = /* @__PURE__ */ new Map();
32
+ const retained = (id, entry) => entry.element.contains(host.ownerDocument.activeElement) || [...pointers.values()].includes(id);
33
+ const pointerDown = (event) => {
34
+ const id = event.target?.closest("[data-board-item]")?.dataset.boardItem;
35
+ if (id && mounted.has(id)) pointers.set(event.pointerId, id);
36
+ };
37
+ const pointerEnd = (event) => {
38
+ if (pointers.delete(event.pointerId)) onInvalidate();
39
+ };
40
+ element.addEventListener("pointerdown", pointerDown, true);
41
+ element.addEventListener("focusout", onInvalidate);
42
+ host.ownerDocument.addEventListener("pointerup", pointerEnd, true);
43
+ host.ownerDocument.addEventListener("pointercancel", pointerEnd, true);
44
+ const toPointerEvent = (event) => toBoardPointerEvent(event, host, camera());
28
45
  const viewFor = (item) => views.find((view) => typeof view.type === "string" ? view.type === item.type : view.type(item));
29
46
  function unmount(id, entry) {
30
47
  entry.instance.destroy?.();
31
48
  entry.element.remove();
32
49
  mounted.delete(id);
50
+ for (const [pointer, owner] of pointers) if (owner === id) pointers.delete(pointer);
33
51
  }
34
52
  return {
35
53
  element,
36
54
  sync({ ids, scene, camera: view, surface, selection }) {
37
55
  if (views.length === 0) return;
56
+ const transform = `translate(${view.x}px,${view.y}px) scale(${view.zoom})`;
57
+ if (transform !== cameraStyle) world.style.transform = cameraStyle = transform;
38
58
  const live = /* @__PURE__ */ new Set();
39
- for (const id of ids ?? scene.items.map((item) => item.id)) {
59
+ const candidates = new Set(ids ?? scene.items.map((item) => item.id));
60
+ for (const [id, entry] of mounted) if (retained(id, entry)) candidates.add(id);
61
+ for (const id of candidates) {
40
62
  const item = scene.get(id);
41
63
  if (!item) continue;
42
64
  const itemView = viewFor(item);
43
- if (!itemView || !onScreen(item.frame, view, surface)) continue;
65
+ let entry = mounted.get(item.id);
66
+ const held = entry ? retained(id, entry) : false;
67
+ if (!itemView || !held && !onScreen(item.frame, view, surface)) continue;
44
68
  live.add(item.id);
69
+ const style = worldStyle(item.frame, scene.indexOf(item.id));
70
+ const minimum = itemView.contentSize;
71
+ const threshold = entry?.detailed ? .85 : 1;
45
72
  const context = {
46
73
  editor,
47
- zoom: view.zoom,
74
+ detailed: held || !minimum || item.frame.width * view.zoom >= minimum.width * threshold && item.frame.height * view.zoom >= minimum.height * threshold,
48
75
  selected: selection.has(item.id),
49
76
  toPointerEvent
50
77
  };
51
- const style = screenStyle(item.frame, view, scene.indexOf(item.id));
52
- let entry = mounted.get(item.id);
53
78
  if (entry && entry.view !== itemView) {
54
79
  unmount(item.id, entry);
55
80
  entry = void 0;
@@ -58,13 +83,13 @@ function createBoardItemViewLayer(host, editor, views, camera) {
58
83
  const node = host.ownerDocument.createElement("div");
59
84
  node.dataset.boardItem = item.id;
60
85
  node.style.cssText = `position:absolute;pointer-events:none;transform-origin:center;${style}`;
61
- element.appendChild(node);
86
+ world.appendChild(node);
62
87
  mounted.set(item.id, {
63
88
  view: itemView,
64
89
  element: node,
65
90
  instance: itemView.mount(node, item, context),
66
91
  item,
67
- zoom: context.zoom,
92
+ detailed: context.detailed,
68
93
  selected: context.selected,
69
94
  style
70
95
  });
@@ -74,9 +99,9 @@ function createBoardItemViewLayer(host, editor, views, camera) {
74
99
  entry.element.style.cssText = `position:absolute;pointer-events:none;transform-origin:center;${style}`;
75
100
  entry.style = style;
76
101
  }
77
- if (entry.item !== item || entry.zoom !== context.zoom || entry.selected !== context.selected) {
102
+ if (entry.item !== item || entry.detailed !== context.detailed || entry.selected !== context.selected) {
78
103
  entry.item = item;
79
- entry.zoom = context.zoom;
104
+ entry.detailed = context.detailed;
80
105
  entry.selected = context.selected;
81
106
  entry.instance.update?.(item, context);
82
107
  }
@@ -84,6 +109,10 @@ function createBoardItemViewLayer(host, editor, views, camera) {
84
109
  for (const [id, entry] of mounted) if (!live.has(id)) unmount(id, entry);
85
110
  },
86
111
  destroy() {
112
+ element.removeEventListener("pointerdown", pointerDown, true);
113
+ element.removeEventListener("focusout", onInvalidate);
114
+ host.ownerDocument.removeEventListener("pointerup", pointerEnd, true);
115
+ host.ownerDocument.removeEventListener("pointercancel", pointerEnd, true);
87
116
  for (const [id, entry] of mounted) unmount(id, entry);
88
117
  element.remove();
89
118
  }