@neta-art/cohub 12.0.0 → 12.1.1

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 (37) hide show
  1. package/dist/board/export/index.js +1 -1
  2. package/dist/board/export/node/index.d.ts +1 -0
  3. package/dist/board/export/node/index.js +5 -0
  4. package/dist/board/export/scene.js +2 -1
  5. package/dist/board/render/card-tag.d.ts +0 -1
  6. package/dist/board/render/card-tag.js +15 -17
  7. package/dist/board/render/index.d.ts +2 -2
  8. package/dist/board/render/index.js +6 -6
  9. package/dist/board/render/renderers/arrow-card-renderer.js +5 -7
  10. package/dist/board/render/renderers/audio-card-renderer.js +8 -7
  11. package/dist/board/render/renderers/base-card-renderer.d.ts +1 -1
  12. package/dist/board/render/renderers/base-card-renderer.js +2 -3
  13. package/dist/board/render/renderers/board-renderer-registry.d.ts +3 -1
  14. package/dist/board/render/renderers/custom-card-renderer.js +19 -17
  15. package/dist/board/render/renderers/file-card-renderer.js +10 -8
  16. package/dist/board/render/renderers/frame-card-renderer.js +5 -6
  17. package/dist/board/render/renderers/shape-card-renderer.js +5 -7
  18. package/dist/board/render/renderers/task-card-renderer.js +9 -9
  19. package/dist/board/render/renderers/text-card-renderer.js +5 -7
  20. package/dist/board/render/renderers/unknown-card-renderer.js +5 -7
  21. package/dist/board/render/text-resolution.d.ts +4 -7
  22. package/dist/board/render/text-resolution.js +20 -22
  23. package/dist/board/stage/canvas-size.d.ts +18 -0
  24. package/dist/board/stage/canvas-size.js +31 -0
  25. package/dist/board/stage/scene.d.ts +5 -1
  26. package/dist/board/stage/scene.js +35 -9
  27. package/dist/board/stage/stage.js +34 -13
  28. package/dist/board/stage/text-resolution-tracker.d.ts +8 -0
  29. package/dist/board/stage/text-resolution-tracker.js +33 -0
  30. package/dist/chunks/app-realtime.js +1 -1
  31. package/dist/chunks/{transport.js → http-error.js} +37 -1
  32. package/dist/chunks/http.js +30 -10
  33. package/dist/http.js +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/websocket.js +951 -1
  36. package/package.json +1 -1
  37. package/dist/chunks/websocket.js +0 -988
@@ -1,8 +1,8 @@
1
1
  import { layoutBoardText } from "../../../protocol/dist/board-layout.js";
2
2
  import { boardFontFamily } from "../../model/text-metrics.js";
3
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
4
- import { BOARD_FONT_STACKS } from "../text-measurement.js";
5
3
  import { itemAlpha, itemColor } from "../palette.js";
4
+ import { BOARD_FONT_STACKS } from "../text-measurement.js";
5
+ import { syncTextResolution } from "../text-resolution.js";
6
6
  import { positionShell } from "./base-card-renderer.js";
7
7
  import { drawFarPlate } from "./far-plate.js";
8
8
  import { Container, Graphics, Text } from "pixi.js";
@@ -32,7 +32,6 @@ function sync(container, item, context) {
32
32
  const anchorX = props.align === "center" ? .5 : props.align === "right" ? 1 : 0;
33
33
  parts.body.anchor.set(anchorX, 0);
34
34
  parts.body.position.set(item.frame.width * anchorX, 0);
35
- syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
36
35
  parts.body.alpha = itemAlpha(context, item.style.fill);
37
36
  parts.caret.alpha = parts.body.alpha;
38
37
  const ink = itemColor(context, item.style.fill, "neutral");
@@ -62,6 +61,7 @@ function sync(container, item, context) {
62
61
  breakWords: true
63
62
  });
64
63
  }
64
+ syncTextResolution(parts.body, context.textResolution * previewScale);
65
65
  syncCaret(parts, item, context);
66
66
  }
67
67
  const textCardRenderer = {
@@ -69,11 +69,10 @@ const textCardRenderer = {
69
69
  canRender: (item) => item.type === "text",
70
70
  create: (item, context) => {
71
71
  const root = new Container();
72
- const resolution = textResolutionForZoom(context.zoom);
73
72
  const body = new Text({
74
73
  text: "",
75
74
  style: { fontFamily: BOARD_FONT_STACKS.sans },
76
- resolution,
75
+ resolution: context.textResolution,
77
76
  roundPixels: true
78
77
  });
79
78
  const caret = new Graphics();
@@ -82,7 +81,6 @@ const textCardRenderer = {
82
81
  root,
83
82
  body,
84
83
  caret,
85
- resolution,
86
84
  contentSig: ""
87
85
  });
88
86
  if (item.type === "text") sync(root, item, context);
@@ -93,7 +91,7 @@ const textCardRenderer = {
93
91
  },
94
92
  resolution: (container, _item, context) => {
95
93
  const parts = partsByContainer.get(container);
96
- if (parts) syncTextResolution(parts.body, parts, context.zoom * Math.max(1, parts.body.scale.x));
94
+ if (parts) syncTextResolution(parts.body, context.textResolution * parts.body.scale.x);
97
95
  },
98
96
  renderFar: (graphics, item, context) => {
99
97
  if (item.type !== "text") return;
@@ -1,5 +1,5 @@
1
1
  import { BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
2
- import { syncTextResolution, textResolutionForZoom } from "../text-resolution.js";
2
+ import { syncTextResolution } from "../text-resolution.js";
3
3
  import { cardElevation, drawCardSurface, traceCardOutline } from "../surface.js";
4
4
  import { positionShell } from "./base-card-renderer.js";
5
5
  import { drawFarPlate } from "./far-plate.js";
@@ -12,7 +12,6 @@ function sync(container, item, context) {
12
12
  positionShell(parts.root, item);
13
13
  const { width, height } = item.frame;
14
14
  const realType = item.type;
15
- syncTextResolution(parts.label, parts, context.zoom);
16
15
  const elevation = cardElevation(context, item.id);
17
16
  const visualSig = [
18
17
  width,
@@ -47,6 +46,7 @@ function sync(container, item, context) {
47
46
  parts.label.text = realType;
48
47
  parts.label.style.fill = context.palette.muted;
49
48
  }
49
+ syncTextResolution(parts.label, context.textResolution);
50
50
  parts.label.position.set(width / 2, height / 2);
51
51
  }
52
52
  const unknownCardRenderer = {
@@ -56,7 +56,6 @@ const unknownCardRenderer = {
56
56
  const root = new Container();
57
57
  const box = new Graphics();
58
58
  box.context.batchMode = "batch";
59
- const resolution = textResolutionForZoom(context.zoom);
60
59
  const label = new Text({
61
60
  text: "",
62
61
  style: {
@@ -64,7 +63,7 @@ const unknownCardRenderer = {
64
63
  fontSize: 11,
65
64
  fontWeight: "600"
66
65
  },
67
- resolution,
66
+ resolution: context.textResolution,
68
67
  roundPixels: true
69
68
  });
70
69
  label.anchor.set(.5);
@@ -74,8 +73,7 @@ const unknownCardRenderer = {
74
73
  box,
75
74
  label,
76
75
  visualSig: "",
77
- textSig: "",
78
- resolution
76
+ textSig: ""
79
77
  });
80
78
  sync(root, item, context);
81
79
  return root;
@@ -85,7 +83,7 @@ const unknownCardRenderer = {
85
83
  },
86
84
  resolution: (container, _item, context) => {
87
85
  const parts = partsByContainer.get(container);
88
- if (parts) syncTextResolution(parts.label, parts, context.zoom);
86
+ if (parts) syncTextResolution(parts.label, context.textResolution);
89
87
  },
90
88
  renderFar: (graphics, item, context) => {
91
89
  drawFarPlate(graphics, item.frame, {
@@ -6,15 +6,12 @@ declare function getBoardResolution(size?: {
6
6
  }, devicePixelRatio?: number): number;
7
7
  declare function textZoomBucket(zoom: number): number;
8
8
  declare function boardCardDetailed(zoom: number): boolean;
9
- /** Effective text resolution for a given camera zoom. */
10
- declare function textResolutionForZoom(zoom: number): number;
11
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
12
- declare function syncTextResolution(text: Text, state: {
13
- resolution: number;
14
- }, zoom: number): void;
9
+ /** Exact screen density at rest; reuse or step the raster while moving. */
10
+ declare function boardTextResolution(target: number, current: number, settled: boolean): number;
11
+ declare function syncTextResolution(text: Text, resolution: number): void;
15
12
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
16
13
  declare function syncTextWrapWidth(text: Text, state: {
17
14
  wrapWidth: number;
18
15
  }, width: number, defer: boolean): void;
19
16
  //#endregion
20
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
17
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -1,19 +1,16 @@
1
1
  //#region src/board/render/text-resolution.ts
2
2
  const MAX_BOARD_RESOLUTION = 3;
3
- const MAX_BOARD_PIXELS = 8e6;
4
- const MAX_TEXT_RESOLUTION = 6;
3
+ const MAX_BOARD_PIXELS = 1e7;
4
+ const MIN_TEXT_RESOLUTION = .125;
5
+ const MAX_TEXT_EDGE = 4096;
6
+ const TEXT_UPSCALE_TOLERANCE = 1.25;
5
7
  function getBoardResolution(size, devicePixelRatio = globalThis.devicePixelRatio) {
6
8
  const density = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
7
9
  const pixels = size ? size.width * size.height : 0;
8
- const budget = pixels > 0 ? Math.sqrt(MAX_BOARD_PIXELS / pixels) : MAX_BOARD_RESOLUTION;
10
+ const budget = pixels > 0 ? Math.max(1, Math.sqrt(MAX_BOARD_PIXELS / pixels)) : MAX_BOARD_RESOLUTION;
9
11
  return Math.min(density, MAX_BOARD_RESOLUTION, budget);
10
12
  }
11
- /**
12
- * Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
13
- * looks soft unless we raise its resolution. We quantise zoom into a few
14
- * buckets so small zoom changes don't thrash texture regeneration.
15
- */
16
- const TEXT_ZOOM_BUCKETS = [
13
+ const ZOOM_BUCKETS = [
17
14
  .5,
18
15
  1,
19
16
  1.5,
@@ -23,23 +20,24 @@ const TEXT_ZOOM_BUCKETS = [
23
20
  ];
24
21
  function textZoomBucket(zoom) {
25
22
  const clamped = Math.max(.1, Math.min(4, zoom));
26
- for (const bucket of TEXT_ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
27
- return TEXT_ZOOM_BUCKETS[TEXT_ZOOM_BUCKETS.length - 1] ?? 4;
23
+ for (const bucket of ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
24
+ return ZOOM_BUCKETS[ZOOM_BUCKETS.length - 1] ?? 4;
28
25
  }
29
26
  function boardCardDetailed(zoom) {
30
27
  return textZoomBucket(zoom) >= 1;
31
28
  }
32
- /** Effective text resolution for a given camera zoom. */
33
- function textResolutionForZoom(zoom) {
34
- const bucket = textZoomBucket(zoom);
35
- return Math.min(getBoardResolution() * Math.max(1, bucket), MAX_TEXT_RESOLUTION);
29
+ /** Exact screen density at rest; reuse or step the raster while moving. */
30
+ function boardTextResolution(target, current, settled) {
31
+ if (!Number.isFinite(target) || target <= 0) return current > 0 ? current : 1;
32
+ if (settled || !(current > 0)) return target;
33
+ if (target <= current * TEXT_UPSCALE_TOLERANCE && target >= current / 2) return current;
34
+ return 2 ** Math.ceil(Math.log2(target));
36
35
  }
37
- /** Update a Pixi text texture only when zoom crosses a resolution bucket. */
38
- function syncTextResolution(text, state, zoom) {
39
- const resolution = textResolutionForZoom(zoom);
40
- if (resolution === state.resolution && resolution === text.resolution) return;
41
- text.resolution = resolution;
42
- state.resolution = resolution;
36
+ function syncTextResolution(text, resolution) {
37
+ const { width, height } = text.bounds;
38
+ const limit = MAX_TEXT_EDGE / Math.max(1, Math.ceil(width + text.style.padding * 2), Math.ceil(height + text.style.padding * 2));
39
+ const next = Math.min(limit, Math.max(MIN_TEXT_RESOLUTION, resolution));
40
+ if (text.resolution !== next) text.resolution = next;
43
41
  }
44
42
  /** Defer expensive Pixi word-wrap rasterisation during a live resize. */
45
43
  function syncTextWrapWidth(text, state, width, defer) {
@@ -48,4 +46,4 @@ function syncTextWrapWidth(text, state, width, defer) {
48
46
  state.wrapWidth = width;
49
47
  }
50
48
  //#endregion
51
- export { boardCardDetailed, getBoardResolution, syncTextResolution, syncTextWrapWidth, textResolutionForZoom, textZoomBucket };
49
+ export { boardCardDetailed, boardTextResolution, getBoardResolution, syncTextResolution, syncTextWrapWidth, textZoomBucket };
@@ -0,0 +1,18 @@
1
+ //#region src/board/stage/canvas-size.d.ts
2
+ type BoardCanvasBox = {
3
+ width: number;
4
+ height: number;
5
+ density: number;
6
+ device: {
7
+ width: number;
8
+ height: number;
9
+ } | null;
10
+ };
11
+ declare function measureBoardCanvasBox(entry: ResizeObserverEntry, density: number): BoardCanvasBox;
12
+ declare function boardCanvasSize(box: BoardCanvasBox, density: number): {
13
+ width: number;
14
+ height: number;
15
+ resolution: number;
16
+ };
17
+ //#endregion
18
+ export { BoardCanvasBox, boardCanvasSize, measureBoardCanvasBox };
@@ -0,0 +1,31 @@
1
+ import { getBoardResolution } from "../render/text-resolution.js";
2
+ //#region src/board/stage/canvas-size.ts
3
+ function measureBoardCanvasBox(entry, density) {
4
+ const css = entry.contentBoxSize?.[0];
5
+ const device = entry.devicePixelContentBoxSize?.[0];
6
+ return {
7
+ width: css?.inlineSize ?? entry.contentRect.width,
8
+ height: css?.blockSize ?? entry.contentRect.height,
9
+ density,
10
+ device: device ? {
11
+ width: device.inlineSize,
12
+ height: device.blockSize
13
+ } : null
14
+ };
15
+ }
16
+ function boardCanvasSize(box, density) {
17
+ const resolution = getBoardResolution(box, density);
18
+ const { device } = box;
19
+ if (device && box.density === density && resolution === density) return {
20
+ width: device.width / density,
21
+ height: device.height / density,
22
+ resolution
23
+ };
24
+ return {
25
+ width: box.width,
26
+ height: box.height,
27
+ resolution
28
+ };
29
+ }
30
+ //#endregion
31
+ export { boardCanvasSize, measureBoardCanvasBox };
@@ -24,6 +24,10 @@ type SceneSyncInput = {
24
24
  structureVersion: number;
25
25
  geometryVersion: number;
26
26
  gestureActive: boolean;
27
+ focus?: {
28
+ x: number;
29
+ y: number;
30
+ };
27
31
  };
28
32
  type BoardSceneNode = {
29
33
  item: BoardSceneItem;
@@ -43,7 +47,7 @@ declare function createBoardScene(options: {
43
47
  world: Container;
44
48
  overlay: Graphics;
45
49
  getRenderer: BoardCardRendererResolver;
46
- onFarLayerFrame?: () => void;
50
+ requestFrame?: () => void;
47
51
  }): BoardScene;
48
52
  //#endregion
49
53
  export { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene };
@@ -1,6 +1,6 @@
1
1
  import { parseBoardItem } from "../../protocol/dist/board-model.js";
2
2
  import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
3
- import { getBoardResolution, textZoomBucket } from "../render/text-resolution.js";
3
+ import { textZoomBucket } from "../render/text-resolution.js";
4
4
  import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
5
5
  import { Graphics } from "pixi.js";
6
6
  //#region src/board/stage/scene.ts
@@ -8,6 +8,7 @@ const FAR_LAYER_ENTER = 450;
8
8
  const FAR_LAYER_EXIT = 350;
9
9
  const FAR_PASS_BATCH = 240;
10
10
  const FAR_LIVE_LIMIT = 64;
11
+ const RESOLUTION_BATCH = 32;
11
12
  const Z_FAR_LAYER = -1;
12
13
  const Z_LIFTED = 1e9;
13
14
  const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
@@ -40,6 +41,7 @@ function createBoardScene(options) {
40
41
  let lastStructureVersion = -1;
41
42
  let lastZoom = NaN;
42
43
  let lastResolution = "";
44
+ const pendingResolution = /* @__PURE__ */ new Set();
43
45
  let liveVersion = 0;
44
46
  let lifted = /* @__PURE__ */ new Set();
45
47
  const liveStrokes = /* @__PURE__ */ new Map();
@@ -69,6 +71,7 @@ function createBoardScene(options) {
69
71
  function recycle(id, entry, context, destroyed) {
70
72
  setHeldKey(context, id, null);
71
73
  cards.delete(id);
74
+ pendingResolution.delete(id);
72
75
  animatedIds.delete(id);
73
76
  tickingIds.delete(id);
74
77
  if (destroyed) return;
@@ -182,7 +185,7 @@ function createBoardScene(options) {
182
185
  }
183
186
  if (index < ids.length) {
184
187
  farFrame = requestAnimationFrame(step);
185
- if (target === farFront) options.onFarLayerFrame?.();
188
+ if (target === farFront) options.requestFrame?.();
186
189
  return;
187
190
  }
188
191
  if (target === farBack) {
@@ -195,7 +198,7 @@ function createBoardScene(options) {
195
198
  farFront.visible = farActive;
196
199
  farPainted = true;
197
200
  farRunning = false;
198
- options.onFarLayerFrame?.();
201
+ options.requestFrame?.();
199
202
  if (farQueued) runFarPass();
200
203
  };
201
204
  step();
@@ -292,9 +295,11 @@ function createBoardScene(options) {
292
295
  }
293
296
  function sync(input) {
294
297
  const { items, scene, context, getItem, visibleIds, pinnedIds, globalSig, structureVersion, geometryVersion, gestureActive } = input;
295
- const resolution = `${textZoomBucket(context.zoom)}:${getBoardResolution()}`;
296
- const resolutionChanged = resolution !== lastResolution;
297
- lastResolution = resolution;
298
+ const resolution = `${textZoomBucket(context.zoom)}:${context.textResolution}`;
299
+ if (resolution !== lastResolution) {
300
+ lastResolution = resolution;
301
+ queueResolution(input.focus);
302
+ }
298
303
  const structureChanged = structureVersion !== lastStructureVersion;
299
304
  if (structureChanged) {
300
305
  lastStructureVersion = structureVersion;
@@ -376,11 +381,10 @@ function createBoardScene(options) {
376
381
  entry.renderer.update(entry.container, item, context);
377
382
  entry.container.alpha = scene.opacity(id);
378
383
  if (entry.renderer.tick) tickingIds.add(id);
379
- } else if (resolutionChanged && entry.renderer.resolution) {
380
- entry.renderer.resolution(entry.container, item, context);
381
- if (entry.renderer.tick) tickingIds.add(id);
384
+ pendingResolution.delete(id);
382
385
  }
383
386
  }
387
+ refreshResolution(context);
384
388
  if (liveSetChanged) liveVersion += 1;
385
389
  if (structureChanged || liveSetChanged) syncClips(scene);
386
390
  if (context.zoom !== lastZoom) {
@@ -392,6 +396,27 @@ function createBoardScene(options) {
392
396
  if (structureChanged || farModeChanged || liveSetChanged || liftChanged) applyChildOrder(scene);
393
397
  syncLiveStrokes(input);
394
398
  }
399
+ function queueResolution(focus) {
400
+ const ids = [...cards].filter(([, entry]) => entry.renderer.resolution);
401
+ if (focus) {
402
+ const distance = ({ item: { frame } }) => (frame.x + frame.width / 2 - focus.x) ** 2 + (frame.y + frame.height / 2 - focus.y) ** 2;
403
+ ids.sort(([, a], [, b]) => distance(a) - distance(b));
404
+ }
405
+ pendingResolution.clear();
406
+ for (const [id] of ids) pendingResolution.add(id);
407
+ }
408
+ function refreshResolution(context) {
409
+ let budget = RESOLUTION_BATCH;
410
+ for (const id of pendingResolution) {
411
+ if (budget-- === 0) break;
412
+ pendingResolution.delete(id);
413
+ const entry = cards.get(id);
414
+ if (!entry) continue;
415
+ entry.renderer.resolution?.(entry.container, entry.item, context);
416
+ if (entry.renderer.tick) tickingIds.add(id);
417
+ }
418
+ if (pendingResolution.size) options.requestFrame?.();
419
+ }
395
420
  function tick(clock) {
396
421
  let next = null;
397
422
  for (const id of tickingIds) {
@@ -433,6 +458,7 @@ function createBoardScene(options) {
433
458
  lastStructureVersion = -1;
434
459
  lastZoom = NaN;
435
460
  lastResolution = "";
461
+ pendingResolution.clear();
436
462
  }
437
463
  return {
438
464
  sync,
@@ -1,6 +1,6 @@
1
1
  import { applyMatrix, invertMatrix } from "../../protocol/dist/board-layout.js";
2
2
  import { featuredTaskArtifact } from "../../protocol/dist/board-task.js";
3
- import { pointToWorld, 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";
@@ -9,16 +9,17 @@ import { buildFallbackShapeColors, pickBoardColor, resolveItemColor } from "../m
9
9
  import { elementClientToLocal } from "../model/client-geometry.js";
10
10
  import { isCreationBoardTool } from "../editor/tool.js";
11
11
  import { resizeCursorForHandle } from "../editor/selection-transform.js";
12
- import { createBoardPlayer } from "../player/index.js";
13
- import { createBoardScriptObservers } from "../model/script-observers.js";
14
12
  import { parseBoardCssColor } from "../render/css-color.js";
13
+ import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
15
14
  import { createBoardBackground, updateBoardBackground } from "../render/board-background.js";
16
15
  import { getBoardResolution } from "../render/text-resolution.js";
17
- import { defaultBoardPalette, itemAlpha } from "../render/palette.js";
18
16
  import { DEFAULT_BOARD_RENDER_LABELS, getBoardCardRenderer } from "../render/renderers/board-renderer-registry.js";
19
17
  import { createBoardScriptHost } from "../render/script-host.js";
20
18
  import { defaultBoardRenderStyle } from "../render/style.js";
19
+ import { createBoardPlayer } from "../player/index.js";
20
+ import { createBoardScriptObservers } from "../model/script-observers.js";
21
21
  import { boardTextureEdge } from "./assets.js";
22
+ import { boardCanvasSize, measureBoardCanvasBox } from "./canvas-size.js";
22
23
  import { observeDisplayDensity } from "./display-density.js";
23
24
  import { toBoardPointerEvent } from "./pointer.js";
24
25
  import { createBoardPointerGestures } from "./pointer-gestures.js";
@@ -26,6 +27,7 @@ import { createBoardScene } from "./scene.js";
26
27
  import { scheduleBoardMaintenance } from "./maintenance.js";
27
28
  import { createBoardScriptPointerRouter } from "./script-pointer.js";
28
29
  import { drawBoardOverlay } from "./overlay.js";
30
+ import { createTextResolutionTracker } from "./text-resolution-tracker.js";
29
31
  import { createBoardItemViewLayer } from "./views.js";
30
32
  import { Application, Container, Graphics, RendererType } from "pixi.js";
31
33
  //#region src/board/stage/stage.ts
@@ -111,6 +113,7 @@ function mountBoardStage(host, options) {
111
113
  let cardTickDue = 0;
112
114
  const staleIds = /* @__PURE__ */ new Set();
113
115
  let resizeFrame = 0;
116
+ let canvasBox = null;
114
117
  let cardEpoch = 0;
115
118
  let surface = {
116
119
  width: 0,
@@ -146,6 +149,7 @@ function mountBoardStage(host, options) {
146
149
  return cull.ids;
147
150
  }
148
151
  const textureScale = () => renderCamera().zoom * (app?.renderer.resolution ?? 1);
152
+ const textResolution = createTextResolutionTracker(TEXTURE_SETTLE_MS, scheduleTick);
149
153
  let refined = {
150
154
  scale: 0,
151
155
  geometry: 0
@@ -240,6 +244,7 @@ function mountBoardStage(host, options) {
240
244
  themeKey: current.key,
241
245
  rendererType: app?.renderer.type === RendererType.CANVAS ? "canvas" : "gpu",
242
246
  zoom: renderCamera().zoom,
247
+ textResolution: textResolution.value,
243
248
  reducedMotion: options.reducedMotion?.() ?? false,
244
249
  labels: options.labels?.() ?? DEFAULT_BOARD_RENDER_LABELS,
245
250
  taskRun: options.taskRun ?? (() => null),
@@ -631,6 +636,7 @@ function mountBoardStage(host, options) {
631
636
  world.position.set(camera.x, camera.y);
632
637
  world.scale.set(camera.zoom);
633
638
  const ids = visibleIds();
639
+ textResolution.update(textureScale());
634
640
  const context = renderContext(current, renderScene, frame.time);
635
641
  if (animationId && effectSampler) {
636
642
  context.time = editor.playhead?.time ?? frame.time;
@@ -657,7 +663,8 @@ function mountBoardStage(host, options) {
657
663
  staleIds,
658
664
  structureVersion: editor.structureVersion,
659
665
  geometryVersion: editor.geometryVersion,
660
- 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)
661
668
  });
662
669
  staleIds.clear();
663
670
  if (scene.liveVersion !== liveVersion) {
@@ -692,10 +699,14 @@ function mountBoardStage(host, options) {
692
699
  height: host.clientHeight
693
700
  };
694
701
  if (size.width <= 0 || size.height <= 0) return;
695
- const resolution = getBoardResolution(size);
696
- const sizeChanged = size.width !== surface.width || size.height !== surface.height;
697
- if (sizeChanged || app.renderer.resolution !== resolution) app.renderer.resize(size.width, size.height, resolution);
698
- 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) {
699
710
  surface = size;
700
711
  editor.surfaceSize = surface;
701
712
  options.onSurfaceChange?.(surface);
@@ -853,14 +864,17 @@ function mountBoardStage(host, options) {
853
864
  }
854
865
  scheduleTick();
855
866
  });
856
- 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
+ });
857
872
  return {
858
873
  ready: (async () => {
859
874
  const instance = new Application();
860
875
  try {
861
876
  await instance.init({
862
877
  antialias: true,
863
- autoDensity: true,
864
878
  backgroundAlpha: 0,
865
879
  width: host.clientWidth,
866
880
  height: host.clientHeight,
@@ -885,6 +899,8 @@ function mountBoardStage(host, options) {
885
899
  position: "relative",
886
900
  zIndex: "1",
887
901
  display: "block",
902
+ width: "100%",
903
+ height: "100%",
888
904
  touchAction: "none"
889
905
  });
890
906
  host.insertBefore(instance.canvas, views.element);
@@ -903,10 +919,14 @@ function mountBoardStage(host, options) {
903
919
  world: nodes,
904
920
  overlay,
905
921
  getRenderer: resolveRenderer,
906
- onFarLayerFrame: scheduleTick
922
+ requestFrame: scheduleTick
907
923
  });
908
924
  for (const [type, listener, listenerOptions] of listeners) host.addEventListener(type, listener, listenerOptions);
909
- resizeObserver.observe(host);
925
+ try {
926
+ resizeObserver.observe(instance.canvas, { box: "device-pixel-content-box" });
927
+ } catch {
928
+ resizeObserver.observe(instance.canvas);
929
+ }
910
930
  const window = host.ownerDocument.defaultView;
911
931
  if (window) stopDensity = observeDisplayDensity(window, resize);
912
932
  resize();
@@ -990,6 +1010,7 @@ function mountBoardStage(host, options) {
990
1010
  pointers.cancel();
991
1011
  stopMarch();
992
1012
  stopRefine();
1013
+ textResolution.dispose();
993
1014
  scriptPointers.clear();
994
1015
  stopTransforms?.();
995
1016
  scriptObservers.clear();
@@ -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 };
@@ -1,5 +1,5 @@
1
1
  import { O as readRtcTransportStats, k as waitForIceGathering } from "./http.js";
2
- import { o as REALTIME_ROOM_EVENT_NAME_PATTERN, t as HttpError } from "./transport.js";
2
+ import { c as HttpError, p as REALTIME_ROOM_EVENT_NAME_PATTERN } from "./http-error.js";
3
3
  import { z } from "zod";
4
4
  //#region \0rolldown/runtime.js
5
5
  var __defProp = Object.defineProperty;
@@ -509,4 +509,40 @@ var HttpTransport = class {
509
509
  }
510
510
  };
511
511
  //#endregion
512
- export { requestSourceToHeaders as A, isRequestSourceClientId as C, normalizeRequestSource as D, mergeRequestSourceIntoMeta as E, isUuid as M, isUuidLike as N, parseRequestSourceFromHeaders as O, isUuidOrShortUuid as P, hasRequestSourceIdentity as S, isRequestSourceUuid as T, isRealtimeDomain as _, sanitizeAccessToken as a, COHUB_SOURCE_HEADER_NAMES as b, WS_BOARD_AWARENESS_CAPABILITY as c, WS_REALTIME_ROOM_HISTORY_CAPABILITY as d, WS_REALTIME_ROOM_RTC_CAPABILITY as f, getSessionTurnPatchStreamKey as g, getRealtimeSpaceRoom as h, matchesUnauthorizedErrorToken as i, resolveRequestSourceChannel as j, readRequestSourceFromEnv as k, WS_COMPACT_STREAM_CAPABILITY as l, getRealtimeBoardRoom as m, HttpTransport as n, REALTIME_ROOM_EVENT_NAME_PATTERN as o, WS_ROOM_SUBSCRIPTION_CAPABILITY as p, joinApiUrl as r, REALTIME_ROOM_MAX_PAYLOAD_BYTES as s, HttpError as t, WS_REALTIME_ROOM_CAPABILITY as u, normalizeRealtimeRooms as v, isRequestSourceEmpty as w, REQUEST_SOURCE_VIA_MAX_LENGTH as x, COHUB_SOURCE_HEADER as y };
512
+ //#region src/http-error.ts
513
+ /** Shared HTTP error code for every plan entitlement gate (402). */
514
+ const FEATURE_NOT_ENTITLED_ERROR_CODE = "feature_not_entitled";
515
+ /** Shared HTTP error code for a blocked usage gate (negative balance limit, 402). */
516
+ const BILLING_ACCESS_BLOCKED_ERROR_CODE = "billing_credit_limit_exceeded";
517
+ function isHttpErrorCode(error, code) {
518
+ return error instanceof HttpError && error.code === code;
519
+ }
520
+ function isFeatureNotEntitledError(error) {
521
+ return isHttpErrorCode(error, FEATURE_NOT_ENTITLED_ERROR_CODE);
522
+ }
523
+ function isBillingAccessBlockedError(error) {
524
+ return isHttpErrorCode(error, BILLING_ACCESS_BLOCKED_ERROR_CODE);
525
+ }
526
+ function isBillingAccessBlockedCode(code) {
527
+ return code === BILLING_ACCESS_BLOCKED_ERROR_CODE;
528
+ }
529
+ const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
530
+ function isBillingConversionIntent(value) {
531
+ if (!isRecord(value)) return false;
532
+ return (value.level === "soft" || value.level === "hard") && typeof value.title === "string" && typeof value.message === "string" && isRecord(value.primaryAction) && value.primaryAction.action === "open_billing_conversion";
533
+ }
534
+ /**
535
+ * Extracts the standard `billing` payload from a response body, an
536
+ * `HttpError`, or a bare billing payload (e.g. a realtime error event's
537
+ * `billing` field). Returns `null` unless it carries a valid conversion
538
+ * intent.
539
+ */
540
+ function extractBillingPayload(source) {
541
+ const root = source instanceof HttpError ? source.body : source;
542
+ if (!isRecord(root)) return null;
543
+ const billing = isRecord(root.billing) ? root.billing : root;
544
+ if (!isBillingConversionIntent(billing.conversion)) return null;
545
+ return billing;
546
+ }
547
+ //#endregion
548
+ export { isRequestSourceClientId as A, isUuidLike as B, getSessionTurnPatchStreamKey as C, COHUB_SOURCE_HEADER_NAMES as D, COHUB_SOURCE_HEADER as E, parseRequestSourceFromHeaders as F, readRequestSourceFromEnv as I, requestSourceToHeaders as L, isRequestSourceUuid as M, mergeRequestSourceIntoMeta as N, REQUEST_SOURCE_VIA_MAX_LENGTH as O, normalizeRequestSource as P, resolveRequestSourceChannel as R, getRealtimeSpaceRoom as S, normalizeRealtimeRooms as T, isUuidOrShortUuid as V, WS_REALTIME_ROOM_CAPABILITY as _, isBillingAccessBlockedError as a, WS_ROOM_SUBSCRIPTION_CAPABILITY as b, HttpError as c, matchesUnauthorizedErrorToken as d, sanitizeAccessToken as f, WS_COMPACT_STREAM_CAPABILITY as g, WS_BOARD_AWARENESS_CAPABILITY as h, isBillingAccessBlockedCode as i, isRequestSourceEmpty as j, hasRequestSourceIdentity as k, HttpTransport as l, REALTIME_ROOM_MAX_PAYLOAD_BYTES as m, FEATURE_NOT_ENTITLED_ERROR_CODE as n, isFeatureNotEntitledError as o, REALTIME_ROOM_EVENT_NAME_PATTERN as p, extractBillingPayload as r, isHttpErrorCode as s, BILLING_ACCESS_BLOCKED_ERROR_CODE as t, joinApiUrl as u, WS_REALTIME_ROOM_HISTORY_CAPABILITY as v, isRealtimeDomain as w, getRealtimeBoardRoom as x, WS_REALTIME_ROOM_RTC_CAPABILITY as y, isUuid as z };