@neta-art/cohub 12.1.1 → 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +85 -8
  2. package/dist/board/editor/clipboard.js +1 -1
  3. package/dist/board/editor/creation.js +1 -1
  4. package/dist/board/editor/document.d.ts +3 -1
  5. package/dist/board/editor/document.js +8 -6
  6. package/dist/board/editor/editor.d.ts +8 -3
  7. package/dist/board/editor/editor.js +8 -2
  8. package/dist/board/editor/index.d.ts +3 -3
  9. package/dist/board/editor/index.js +2 -2
  10. package/dist/board/editor/interaction.d.ts +23 -2
  11. package/dist/board/editor/interaction.js +93 -4
  12. package/dist/board/editor/query.d.ts +1 -1
  13. package/dist/board/editor/query.js +3 -7
  14. package/dist/board/editor/selection.js +1 -1
  15. package/dist/board/editor/snapshots.js +5 -57
  16. package/dist/board/editor/tool.d.ts +4 -1
  17. package/dist/board/editor/tool.js +6 -1
  18. package/dist/board/export/index.d.ts +2 -2
  19. package/dist/board/export/index.js +1 -1
  20. package/dist/board/export/node/fonts.js +4 -0
  21. package/dist/board/export/node/index.d.ts +2 -0
  22. package/dist/board/export/node/index.js +8 -1
  23. package/dist/board/export/node/scripts.js +1 -1
  24. package/dist/board/export/scene.d.ts +2 -2
  25. package/dist/board/export/scene.js +2 -2
  26. package/dist/board/model/asset-key.d.ts +24 -0
  27. package/dist/board/model/asset-key.js +104 -0
  28. package/dist/board/model/export-assets.d.ts +7 -3
  29. package/dist/board/model/export-assets.js +34 -13
  30. package/dist/board/model/index.d.ts +9 -7
  31. package/dist/board/model/index.js +22 -20
  32. package/dist/board/model/items/create.js +3 -3
  33. package/dist/board/model/items/definition.d.ts +9 -1
  34. package/dist/board/model/items/index.d.ts +2 -2
  35. package/dist/board/model/items/registry.d.ts +2 -1
  36. package/dist/board/model/items/registry.js +1 -0
  37. package/dist/board/model/media-sizes.d.ts +28 -0
  38. package/dist/board/model/media-sizes.js +64 -0
  39. package/dist/board/model/palette.d.ts +1 -0
  40. package/dist/board/model/palette.js +20 -130
  41. package/dist/board/model/sampling.js +1 -1
  42. package/dist/board/model/text-metrics.d.ts +28 -3
  43. package/dist/board/model/text-metrics.js +36 -2
  44. package/dist/board/model/themes.d.ts +495 -0
  45. package/dist/board/model/themes.js +500 -0
  46. package/dist/board/render/card-tag.js +2 -2
  47. package/dist/board/render/index.d.ts +11 -7
  48. package/dist/board/render/index.js +9 -5
  49. package/dist/board/render/labels.d.ts +15 -0
  50. package/dist/board/render/labels.js +14 -0
  51. package/dist/board/render/lod.d.ts +30 -0
  52. package/dist/board/render/lod.js +33 -0
  53. package/dist/board/render/media-interaction.d.ts +5 -2
  54. package/dist/board/render/media-interaction.js +15 -3
  55. package/dist/board/render/palette.js +2 -25
  56. package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
  57. package/dist/board/render/renderers/audio-card-renderer.js +3 -2
  58. package/dist/board/render/renderers/board-renderer-registry.d.ts +22 -1
  59. package/dist/board/render/renderers/board-renderer-registry.js +9 -4
  60. package/dist/board/render/renderers/custom-card-renderer.js +5 -5
  61. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  62. package/dist/board/render/renderers/far-plate.d.ts +14 -1
  63. package/dist/board/render/renderers/far-plate.js +31 -8
  64. package/dist/board/render/renderers/file-card-renderer.js +49 -31
  65. package/dist/board/render/renderers/frame-card-renderer.js +9 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +34 -8
  67. package/dist/board/render/renderers/model-card-renderer.js +6 -6
  68. package/dist/board/render/renderers/shape-card-renderer.js +25 -24
  69. package/dist/board/render/renderers/task-card-renderer.js +9 -10
  70. package/dist/board/render/renderers/text-card-renderer.js +86 -41
  71. package/dist/board/render/renderers/video-card-renderer.js +5 -4
  72. package/dist/board/render/script-export.js +1 -1
  73. package/dist/board/render/script-host.js +3 -3
  74. package/dist/board/render/style.d.ts +8 -2
  75. package/dist/board/render/style.js +8 -2
  76. package/dist/board/render/surface.d.ts +5 -0
  77. package/dist/board/render/surface.js +13 -3
  78. package/dist/board/render/text-edit.d.ts +32 -0
  79. package/dist/board/render/text-edit.js +134 -0
  80. package/dist/board/render/text-fit.d.ts +1 -3
  81. package/dist/board/render/text-fit.js +6 -49
  82. package/dist/board/render/text-measurement.d.ts +25 -1
  83. package/dist/board/render/text-measurement.js +157 -19
  84. package/dist/board/render/text-resolution.d.ts +1 -6
  85. package/dist/board/render/text-resolution.js +1 -10
  86. package/dist/board/render/theme.d.ts +15 -0
  87. package/dist/board/render/theme.js +15 -0
  88. package/dist/board/replica/index.d.ts +4 -2
  89. package/dist/board/replica/index.js +28 -17
  90. package/dist/board/replica/indexeddb.d.ts +16 -0
  91. package/dist/board/replica/indexeddb.js +155 -0
  92. package/dist/board/stage/assets.d.ts +28 -5
  93. package/dist/board/stage/assets.js +293 -102
  94. package/dist/board/stage/bench.d.ts +35 -0
  95. package/dist/board/stage/bench.js +153 -0
  96. package/dist/board/stage/device.d.ts +41 -0
  97. package/dist/board/stage/device.js +62 -0
  98. package/dist/board/stage/frame-budget.d.ts +19 -0
  99. package/dist/board/stage/frame-budget.js +69 -0
  100. package/dist/board/stage/idle.d.ts +4 -0
  101. package/dist/board/stage/idle.js +26 -0
  102. package/dist/board/stage/index.d.ts +11 -7
  103. package/dist/board/stage/index.js +7 -4
  104. package/dist/board/stage/lru.d.ts +13 -9
  105. package/dist/board/stage/lru.js +38 -20
  106. package/dist/board/stage/presses.d.ts +21 -0
  107. package/dist/board/stage/presses.js +66 -0
  108. package/dist/board/stage/scene.d.ts +15 -1
  109. package/dist/board/stage/scene.js +248 -141
  110. package/dist/board/stage/stage.d.ts +28 -2
  111. package/dist/board/stage/stage.js +404 -112
  112. package/dist/board/stage/texture-pool.d.ts +10 -1
  113. package/dist/board/stage/texture-pool.js +72 -2
  114. package/dist/chunks/environment.d.ts +6 -0
  115. package/dist/chunks/http.d.ts +9 -1
  116. package/dist/chunks/http.js +791 -4
  117. package/dist/index.d.ts +52 -1
  118. package/dist/index.js +202 -862
  119. package/dist/media.d.ts +3 -27
  120. package/dist/media.js +3 -82
  121. package/dist/protocol/dist/board-constants.js +3 -3
  122. package/dist/protocol/dist/board-layout.d.ts +17 -3
  123. package/dist/protocol/dist/board-layout.js +68 -11
  124. package/dist/protocol/dist/board-model.d.ts +15 -1
  125. package/dist/protocol/dist/board-model.js +12 -2
  126. package/dist/protocol/dist/board-patch.js +1 -1
  127. package/dist/protocol/dist/board-schema.d.ts +2 -1
  128. package/dist/protocol/dist/board-schema.js +4 -1
  129. package/dist/protocol/dist/core/media.d.ts +35 -1
  130. package/dist/protocol/dist/core/media.js +127 -1
  131. package/dist/protocol/dist/index.d.ts +3 -3
  132. package/package.json +4 -2
  133. package/dist/board/model/image-key.d.ts +0 -9
  134. package/dist/board/model/image-key.js +0 -33
@@ -1,30 +1,37 @@
1
- import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
1
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.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";
2
+ import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.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 { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../model/palette.js";
9
- import { elementClientToLocal } from "../model/client-geometry.js";
10
- import { isCreationBoardTool } from "../editor/tool.js";
11
- import { resizeCursorForHandle } from "../editor/selection-transform.js";
5
+ import { pickBoardColor, resolveItemColor } from "../model/palette.js";
12
6
  import { parseBoardCssColor } from "../render/css-color.js";
13
- import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
7
+ import { itemAlpha } from "../render/palette.js";
8
+ import { resolveSceneArrow } from "../model/arrow-geometry.js";
9
+ import { onBoardTextFontsChange } from "../render/text-measurement.js";
10
+ import { boardLodRatio, boardLodSize } from "../render/lod.js";
14
11
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
15
12
  import { getBoardResolution } from "../render/text-resolution.js";
16
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";
17
16
  import { createBoardScriptHost } from "../render/script-host.js";
18
17
  import { defaultBoardRenderStyle } from "../render/style.js";
18
+ import { boardRenderTheme } from "../render/theme.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";
19
23
  import { createBoardPlayer } from "../player/index.js";
20
24
  import { createBoardScriptObservers } from "../model/script-observers.js";
21
- import { boardTextureEdge } from "./assets.js";
25
+ import { defaultBoardDeviceBudget } from "./device.js";
26
+ import { scheduleBoardIdle } from "./idle.js";
22
27
  import { boardCanvasSize, measureBoardCanvasBox } from "./canvas-size.js";
23
28
  import { observeDisplayDensity } from "./display-density.js";
29
+ import { createBoardFrameClock } from "./frame-budget.js";
24
30
  import { toBoardPointerEvent } from "./pointer.js";
25
31
  import { createBoardPointerGestures } from "./pointer-gestures.js";
26
32
  import { createBoardScene } from "./scene.js";
27
33
  import { scheduleBoardMaintenance } from "./maintenance.js";
34
+ import { boardStagePress, createBoardLongPress, createGhostClickGuard } from "./presses.js";
28
35
  import { createBoardScriptPointerRouter } from "./script-pointer.js";
29
36
  import { drawBoardOverlay } from "./overlay.js";
30
37
  import { createTextResolutionTracker } from "./text-resolution-tracker.js";
@@ -42,18 +49,26 @@ const LIVE_CHANNELS = [
42
49
  "editing",
43
50
  "playback"
44
51
  ];
45
- function defaultTheme() {
46
- return {
47
- key: "dark",
48
- palette: defaultBoardPalette("dark"),
49
- colors: buildFallbackShapeColors("dark"),
50
- colorScheme: "dark"
51
- };
52
- }
52
+ const DEFAULT_THEME = {
53
+ key: "dark",
54
+ ...boardRenderTheme("dark")
55
+ };
53
56
  const NO_IDS = /* @__PURE__ */ new Set();
54
57
  const MARCH_STEP_MS = 160;
55
58
  const MARCH_SMOOTH_MS = 40;
56
59
  const TEXTURE_SETTLE_MS = 150;
60
+ const ADOPT_PIXELS_PER_FRAME = 4194304;
61
+ const LOAD_LEAD_MS = 300;
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;
57
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`;
58
73
  const defaultStyles = /* @__PURE__ */ new Map();
59
74
  function styleOf(theme) {
@@ -66,7 +81,7 @@ function styleOf(theme) {
66
81
  }
67
82
  function mountBoardStage(host, options) {
68
83
  const { editor, assets } = options;
69
- const theme = options.theme ?? defaultTheme;
84
+ const theme = options.theme ?? (() => DEFAULT_THEME);
70
85
  const resolveRenderer = options.renderers ?? getBoardCardRenderer;
71
86
  let app = null;
72
87
  let world = null;
@@ -105,22 +120,40 @@ function mountBoardStage(host, options) {
105
120
  let stopDensity = null;
106
121
  let active = true;
107
122
  let disposed = false;
108
- let renderFrame = 0;
109
- let tickFrame = 0;
110
- let scriptTickFrame = 0;
111
- 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;
112
133
  let cardTickTimer = null;
113
134
  let cardTickDue = 0;
135
+ let prefetchKey = "";
136
+ let prefetchMovedAt = 0;
137
+ let prefetchContext = null;
138
+ let cancelPrefetch = null;
114
139
  const staleIds = /* @__PURE__ */ new Set();
115
- let resizeFrame = 0;
116
140
  let canvasBox = null;
117
141
  let cardEpoch = 0;
142
+ let fontEpoch = 0;
143
+ let shownEditingId = null;
118
144
  let surface = {
119
145
  width: 0,
120
146
  height: 0
121
147
  };
122
148
  let playbackCamera = null;
123
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
+ });
124
157
  const renderCamera = () => playbackCamera ?? editor.camera;
125
158
  const views = createBoardItemViewLayer(host, editor, options.views ?? [], renderCamera, scheduleTick);
126
159
  const player = createBoardPlayer({ resolveColor: (color) => {
@@ -192,6 +225,50 @@ function mountBoardStage(host, options) {
192
225
  }
193
226
  }, TEXTURE_SETTLE_MS);
194
227
  }
228
+ function cullView(camera) {
229
+ const view = visibleWorldRect(camera, surface.width, surface.height);
230
+ const pad = CULL_PADDING / camera.zoom;
231
+ return {
232
+ x: view.x - pad,
233
+ y: view.y - pad,
234
+ width: view.width + pad * 2,
235
+ height: view.height + pad * 2
236
+ };
237
+ }
238
+ const arrivals = /* @__PURE__ */ new Set();
239
+ const toned = /* @__PURE__ */ new Set();
240
+ function adoptArrivals() {
241
+ let pixels = 0;
242
+ for (const key of arrivals) {
243
+ const texture = assets.getTexture(key);
244
+ const size = texture ? texture.width * texture.height : 0;
245
+ if (pixels > 0 && pixels + size > ADOPT_PIXELS_PER_FRAME) {
246
+ scheduleTick();
247
+ return;
248
+ }
249
+ pixels += size;
250
+ arrivals.delete(key);
251
+ for (const id of scene?.holders(key) ?? []) {
252
+ staleIds.add(id);
253
+ naturalIds.add(id);
254
+ }
255
+ }
256
+ }
257
+ let lastCenter = null;
258
+ function loadFocus(center, camera) {
259
+ const time = performance.now();
260
+ const previous = lastCenter;
261
+ lastCenter = {
262
+ ...center,
263
+ time
264
+ };
265
+ const elapsed = previous ? time - previous.time : 0;
266
+ if (!previous || elapsed <= 0 || elapsed > 100) return center;
267
+ const lead = LOAD_LEAD_MS / elapsed;
268
+ const reach = Math.max(surface.width, surface.height) / 2 / camera.zoom;
269
+ const ahead = (from, to) => to + Math.max(-reach, Math.min(reach, (to - from) * lead));
270
+ return worldPoint(ahead(previous.x, center.x), ahead(previous.y, center.y));
271
+ }
195
272
  const naturalIds = /* @__PURE__ */ new Set();
196
273
  let liveVersion = -1;
197
274
  function adoptNaturalSizes() {
@@ -237,11 +314,12 @@ function mountBoardStage(host, options) {
237
314
  hoveredId: editor.hoverId,
238
315
  resizingIds: interaction.type === "resizing" ? new Set(interaction.origin.keys()) : /* @__PURE__ */ new Set(),
239
316
  liftedIds: liftedIds(),
317
+ editingId: editor.editingId,
240
318
  palette: current.palette,
241
319
  colors: current.colors,
242
320
  colorScheme: current.colorScheme,
243
321
  style: styleOf(current),
244
- themeKey: current.key,
322
+ themeKey: `${current.key}:${fontEpoch}`,
245
323
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
246
324
  zoom: renderCamera().zoom,
247
325
  textResolution: textResolution.value,
@@ -251,13 +329,15 @@ function mountBoardStage(host, options) {
251
329
  assetKey: assets.assetKey,
252
330
  getTexture: (key) => assets.getTexture(key),
253
331
  hasError: (key) => assets.hasError(key),
332
+ assetTone: (key) => assets.getTone(key),
254
333
  fileState: (path) => options.fileState?.(path) ?? "ok",
255
334
  acquireTexture: (key, item) => {
256
335
  assets.acquire(key);
257
336
  assets.requestItem(item, boardTextureEdge(item, textureScale()));
258
337
  if (assets.getTexture(key)) naturalIds.add(item.id);
259
338
  },
260
- releaseTexture: (key) => assets.release(key)
339
+ releaseTexture: (key) => assets.release(key),
340
+ hoveredHit
261
341
  };
262
342
  }
263
343
  function syncBackground(current) {
@@ -427,6 +507,36 @@ function mountBoardStage(host, options) {
427
507
  if (style.selection.dash > 0 && selected.length > 0 && !(options.reducedMotion?.() ?? false)) scheduleMarch(style);
428
508
  else stopMarch();
429
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
+ }
430
540
  function cursorFor() {
431
541
  if (scriptMode === "interact" && scriptHover) {
432
542
  const output = scriptOutputs.get(scriptHover.owner);
@@ -441,49 +551,191 @@ function mountBoardStage(host, options) {
441
551
  case "draggingArrowHandle":
442
552
  case "brushing": return "crosshair";
443
553
  }
444
- 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;
445
557
  const control = editor.hoveredTransformControl;
446
558
  if (control?.kind === "resize") return resizeCursorForHandle(control.handle, editor.selectionTransform?.frame.rotation ?? 0);
447
559
  if (control?.kind === "rotate") return ROTATE_CURSOR;
448
- if (isCreationBoardTool(editor.tool)) return "crosshair";
449
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";
450
564
  return hovered && !hovered.locked && editor.registry.capabilities(hovered).canMove ? "move" : "default";
451
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
+ }
452
641
  function scheduleRender() {
453
- if (renderFrame || !app || !active) return;
454
- renderFrame = requestAnimationFrame(() => {
455
- renderFrame = 0;
456
- app?.render();
457
- });
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;
458
660
  }
459
661
  function scheduleCardTick(wait) {
460
662
  if (!active || disposed) return;
461
663
  const due = performance.now() + wait;
462
- if (cardTickFrame || cardTickTimer !== null && cardTickDue <= due) return;
664
+ if (frameWork & FRAME_CARDS || cardTickTimer !== null && cardTickDue <= due) return;
463
665
  if (cardTickTimer !== null) clearTimeout(cardTickTimer);
464
666
  cardTickTimer = null;
465
667
  if (wait > 0) {
466
668
  cardTickDue = due;
467
669
  cardTickTimer = setTimeout(() => {
468
670
  cardTickTimer = null;
469
- scheduleCardTick(0);
671
+ requestFrame(FRAME_CARDS);
470
672
  }, wait);
471
673
  return;
472
674
  }
473
- cardTickFrame = requestAnimationFrame(() => {
474
- cardTickFrame = 0;
475
- if (!scene) return;
476
- const next = scene.tick(performance.now());
477
- scheduleRender();
478
- if (next !== null) scheduleCardTick(next);
479
- });
675
+ requestFrame(FRAME_CARDS);
480
676
  }
481
- function scheduleTick() {
482
- if (tickFrame || !active || disposed) return;
483
- tickFrame = requestAnimationFrame(() => {
484
- tickFrame = 0;
485
- sync();
486
- });
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));
487
739
  }
488
740
  function cancelScriptPointers(owner, retained) {
489
741
  scriptPointers.cancelScripts((pointerId, target) => {
@@ -512,15 +764,6 @@ function mountBoardStage(host, options) {
512
764
  }));
513
765
  scripts.sync(editor.document, editor.scene, camera, scriptTime, scriptMode === "interact", visibleIds());
514
766
  }
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
- }
524
767
  function createScripts() {
525
768
  return createBoardScriptHost({
526
769
  host,
@@ -636,8 +879,16 @@ function mountBoardStage(host, options) {
636
879
  world.position.set(camera.x, camera.y);
637
880
  world.scale.set(camera.zoom);
638
881
  const ids = visibleIds();
882
+ const center = pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera);
883
+ assets.focus(loadFocus(center, camera));
884
+ adoptArrivals();
885
+ if (toned.size > 0) {
886
+ scene?.repaintFar(toned, assets.assetKey);
887
+ toned.clear();
888
+ }
639
889
  textResolution.update(textureScale());
640
890
  const context = renderContext(current, renderScene, frame.time);
891
+ lastContext = context;
641
892
  if (animationId && effectSampler) {
642
893
  context.time = editor.playhead?.time ?? frame.time;
643
894
  context.frameAt = effectSampler.frameAt;
@@ -646,6 +897,10 @@ function mountBoardStage(host, options) {
646
897
  for (const item of renderScene.items) if (item.type === "effect" && item.props.kind === "trail" && renderScene.opacity(item.id) > 0) pinnedIds.add(item.id);
647
898
  for (const id of previews.keys()) pinnedIds.add(id);
648
899
  if (editor.editingId) pinnedIds.add(editor.editingId);
900
+ if (editor.editingId !== shownEditingId) {
901
+ for (const id of [shownEditingId, editor.editingId]) if (id) staleIds.add(id);
902
+ shownEditingId = editor.editingId;
903
+ }
649
904
  for (const id of frame.items.keys()) {
650
905
  pinnedIds.add(id);
651
906
  for (const child of renderScene.descendants(id)) pinnedIds.add(child);
@@ -659,14 +914,21 @@ function mountBoardStage(host, options) {
659
914
  getItem: (id) => renderScene.get(id) ?? null,
660
915
  visibleIds: ids,
661
916
  pinnedIds,
662
- globalSig: [cardEpoch, current.key].join("|"),
917
+ globalSig: [
918
+ cardEpoch,
919
+ current.key,
920
+ fontEpoch
921
+ ].join("|"),
663
922
  staleIds,
664
923
  structureVersion: editor.structureVersion,
665
- geometryVersion: editor.geometryVersion,
666
924
  gestureActive: editor.gestureActive && editor.interaction.type !== "panning",
667
- focus: pointToWorld(screenPoint(surface.width / 2, surface.height / 2), camera)
925
+ focus: center,
926
+ view: ids ? cullView(camera) : void 0,
927
+ lod: currentLod(),
928
+ budget: clock
668
929
  });
669
930
  staleIds.clear();
931
+ schedulePrefetch(camera, context);
670
932
  if (scene.liveVersion !== liveVersion) {
671
933
  liveVersion = scene.liveVersion;
672
934
  options.onLiveChange?.(scene.liveIds());
@@ -690,29 +952,26 @@ function mountBoardStage(host, options) {
690
952
  const cardWait = scene.tick(performance.now());
691
953
  if (cardWait !== null) scheduleCardTick(cardWait);
692
954
  }
693
- function resize() {
694
- cancelAnimationFrame(resizeFrame);
695
- resizeFrame = requestAnimationFrame(() => {
696
- if (!app || !active) return;
697
- const size = {
698
- width: host.clientWidth,
699
- height: host.clientHeight
700
- };
701
- if (size.width <= 0 || size.height <= 0) return;
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) {
710
- surface = size;
711
- editor.surfaceSize = surface;
712
- options.onSurfaceChange?.(surface);
713
- }
714
- sync();
715
- });
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;
716
975
  }
717
976
  const localPoint = (x, y) => elementClientToLocal(host)(x, y);
718
977
  const inViews = (event) => event.target instanceof Node && views.element.contains(event.target);
@@ -794,8 +1053,11 @@ function mountBoardStage(host, options) {
794
1053
  return;
795
1054
  }
796
1055
  if (options.onPointer?.("down", event, input) === false) return;
1056
+ if (pressHit(event, input)) return;
797
1057
  pointers.start(event);
798
1058
  editor.pointerDown(input);
1059
+ if (options.onLongPress && editor.itemAt(input.world)) longPress.start(event);
1060
+ else longPress.cancel();
799
1061
  }
800
1062
  function handlePointerMove(event) {
801
1063
  if (!active || pointers.has(event.pointerId) || inViews(event)) return;
@@ -804,21 +1066,28 @@ function mountBoardStage(host, options) {
804
1066
  function routePointerMove(event) {
805
1067
  const input = toInput(event);
806
1068
  if (scriptPointer("move", event, input)) return;
1069
+ longPress.move(event);
807
1070
  if (options.onPointer?.("move", event, input) === false) return;
808
1071
  editor.pointerMove(input);
1072
+ hoverHit(input);
809
1073
  }
810
1074
  function handlePointerUp(event) {
811
1075
  const input = toInput(event);
812
1076
  if (scriptPointer("up", event, input)) return;
1077
+ longPress.end(event);
813
1078
  if (options.onPointer?.("up", event, input) === false) return;
814
1079
  editor.pointerUp(input);
815
- if (event.type === "pointercancel" || event.pointerType !== "mouse") editor.pointerLeave();
1080
+ if (event.type === "pointercancel" || event.pointerType !== "mouse") {
1081
+ editor.pointerLeave();
1082
+ hoverHit(null);
1083
+ }
816
1084
  }
817
1085
  function handlePointerLeave(event) {
818
1086
  if (event.buttons !== 0) return;
819
1087
  scriptPointer("leave", event, toInput(event));
820
1088
  if (options.onPointer?.("leave", event, toInput(event)) === false) return;
821
1089
  editor.pointerLeave();
1090
+ hoverHit(null);
822
1091
  }
823
1092
  function handleWheel(event) {
824
1093
  if (inViews(event)) return;
@@ -837,7 +1106,7 @@ function mountBoardStage(host, options) {
837
1106
  return;
838
1107
  }
839
1108
  if (item && item.type !== "frame" && item.type !== "arrow") return;
840
- const label = editor.labelItemAt(point);
1109
+ const label = editor.labelItemAt(point, styleOf(theme()).frameLabelSize);
841
1110
  if (label) editor.editingId = label.id;
842
1111
  else if (!item) editor.beginTextDraft(point);
843
1112
  }
@@ -855,26 +1124,40 @@ function mountBoardStage(host, options) {
855
1124
  ["dblclick", handleDoubleClick]
856
1125
  ];
857
1126
  const stopEditor = editor.subscribe(LIVE_CHANNELS, scheduleTick);
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
- }
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
+ });
1137
+ const stopFonts = onBoardTextFontsChange(() => {
1138
+ editor.relayoutText();
1139
+ fontEpoch += 1;
1140
+ scene?.redrawText();
1141
+ scheduleTick();
1142
+ });
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;
865
1147
  scheduleTick();
866
1148
  });
867
1149
  const resizeObserver = new ResizeObserver((entries) => {
868
1150
  const entry = entries[entries.length - 1];
869
1151
  if (entry) canvasBox = measureBoardCanvasBox(entry, host.ownerDocument.defaultView?.devicePixelRatio ?? 1);
870
- resize();
1152
+ flushResize();
871
1153
  });
872
1154
  return {
873
1155
  ready: (async () => {
874
1156
  const instance = new Application();
1157
+ const density = host.ownerDocument.defaultView?.devicePixelRatio ?? 1;
875
1158
  try {
876
1159
  await instance.init({
877
- antialias: true,
1160
+ antialias: density < ANTIALIAS_MAX_DENSITY,
878
1161
  backgroundAlpha: 0,
879
1162
  width: host.clientWidth,
880
1163
  height: host.clientHeight,
@@ -982,16 +1265,8 @@ function mountBoardStage(host, options) {
982
1265
  stopMarch();
983
1266
  stopRefine();
984
1267
  cancelScriptPointers();
985
- cancelAnimationFrame(scriptTickFrame);
986
- scriptTickFrame = 0;
987
- cancelAnimationFrame(renderFrame);
988
- cancelAnimationFrame(tickFrame);
989
- cancelAnimationFrame(cardTickFrame);
990
- if (cardTickTimer !== null) clearTimeout(cardTickTimer);
991
- renderFrame = 0;
992
- tickFrame = 0;
993
- cardTickFrame = 0;
994
- cardTickTimer = null;
1268
+ cancelFrames();
1269
+ stopPrefetch();
995
1270
  return;
996
1271
  }
997
1272
  player.reset();
@@ -1004,6 +1279,24 @@ function mountBoardStage(host, options) {
1004
1279
  scheduleTick();
1005
1280
  },
1006
1281
  isMaterialized: (id) => Boolean(scene?.getNode(id)),
1282
+ stats() {
1283
+ const { textures, cooling, bytes, loading, prefetching } = assets.stats();
1284
+ return {
1285
+ ...scene?.counts() ?? {
1286
+ live: 0,
1287
+ far: 0
1288
+ },
1289
+ textures,
1290
+ cooling,
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
1298
+ };
1299
+ },
1007
1300
  toWorld: (clientX, clientY) => pointToWorld(localPoint(clientX, clientY), renderCamera()),
1008
1301
  destroy() {
1009
1302
  disposed = true;
@@ -1018,15 +1311,14 @@ function mountBoardStage(host, options) {
1018
1311
  stopMaintenance?.();
1019
1312
  stopDensity?.();
1020
1313
  stopEditor();
1314
+ stopActivations();
1315
+ longPress.cancel();
1316
+ ghostClicks.dispose();
1317
+ stopFonts();
1021
1318
  stopAssets();
1022
1319
  resizeObserver.disconnect();
1023
- cancelAnimationFrame(scriptTickFrame);
1024
- cancelAnimationFrame(resizeFrame);
1025
- cancelAnimationFrame(renderFrame);
1026
- cancelAnimationFrame(tickFrame);
1027
- cancelAnimationFrame(cardTickFrame);
1028
- if (cardTickTimer !== null) clearTimeout(cardTickTimer);
1029
- cardTickTimer = null;
1320
+ cancelFrames();
1321
+ stopPrefetch();
1030
1322
  for (const [type, listener] of listeners) host.removeEventListener(type, listener);
1031
1323
  editor.setPlayedItems(/* @__PURE__ */ new Map());
1032
1324
  editor.setScriptItems(/* @__PURE__ */ new Map());