@neta-art/cohub 9.0.0 → 10.0.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 (178) hide show
  1. package/README.md +100 -103
  2. package/dist/board/editor/align.d.ts +11 -0
  3. package/dist/board/editor/align.js +72 -0
  4. package/dist/board/editor/animation.d.ts +14 -0
  5. package/dist/board/editor/animation.js +172 -0
  6. package/dist/board/editor/camera.d.ts +32 -0
  7. package/dist/board/editor/camera.js +137 -0
  8. package/dist/board/editor/clipboard.d.ts +41 -0
  9. package/dist/board/editor/clipboard.js +122 -0
  10. package/dist/board/editor/connection-ports.d.ts +19 -0
  11. package/dist/board/editor/connection-ports.js +85 -0
  12. package/dist/board/editor/context.d.ts +111 -0
  13. package/dist/board/editor/context.js +30 -0
  14. package/dist/board/editor/copy.d.ts +17 -0
  15. package/dist/board/editor/copy.js +116 -0
  16. package/dist/board/editor/creation.d.ts +47 -0
  17. package/dist/board/editor/creation.js +224 -0
  18. package/dist/board/editor/deletion.d.ts +9 -0
  19. package/dist/board/editor/deletion.js +90 -0
  20. package/dist/board/editor/document-edits.d.ts +22 -0
  21. package/dist/board/editor/document-edits.js +193 -0
  22. package/dist/board/editor/document.d.ts +35 -0
  23. package/dist/board/editor/document.js +255 -0
  24. package/dist/board/editor/editor.d.ts +210 -0
  25. package/dist/board/editor/editor.js +429 -0
  26. package/dist/board/editor/index.d.ts +18 -0
  27. package/dist/board/editor/index.js +12 -0
  28. package/dist/board/editor/interaction.d.ts +115 -0
  29. package/dist/board/editor/interaction.js +584 -0
  30. package/dist/board/editor/query.d.ts +27 -0
  31. package/dist/board/editor/query.js +201 -0
  32. package/dist/board/editor/remote.d.ts +11 -0
  33. package/dist/board/editor/remote.js +55 -0
  34. package/dist/board/editor/selection-transform.d.ts +24 -0
  35. package/dist/board/editor/selection-transform.js +81 -0
  36. package/dist/board/editor/selection.d.ts +33 -0
  37. package/dist/board/editor/selection.js +196 -0
  38. package/dist/board/editor/snapping.d.ts +33 -0
  39. package/dist/board/editor/snapping.js +75 -0
  40. package/dist/board/editor/snapshots.d.ts +27 -0
  41. package/dist/board/editor/snapshots.js +132 -0
  42. package/dist/board/editor/spatial.d.ts +41 -0
  43. package/dist/board/editor/spatial.js +191 -0
  44. package/dist/board/editor/state.d.ts +12 -0
  45. package/dist/board/editor/state.js +49 -0
  46. package/dist/board/editor/tool.d.ts +10 -0
  47. package/dist/board/editor/tool.js +19 -0
  48. package/dist/board/editor/wheel.d.ts +5 -0
  49. package/dist/board/editor/wheel.js +17 -0
  50. package/dist/board/export/index.d.ts +5 -4
  51. package/dist/board/export/index.js +4 -3
  52. package/dist/board/export/node/index.d.ts +55 -0
  53. package/dist/board/{headless → export/node}/index.js +12 -12
  54. package/dist/board/export/node/sketch.d.ts +19 -0
  55. package/dist/board/{headless → export/node}/sketch.js +6 -6
  56. package/dist/board/export/scene.d.ts +6 -4
  57. package/dist/board/export/scene.js +4 -3
  58. package/dist/board/{animation.d.ts → model/animation.d.ts} +4 -4
  59. package/dist/board/{animation.js → model/animation.js} +3 -3
  60. package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
  61. package/dist/board/{core → model}/arrow-geometry.js +2 -2
  62. package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
  63. package/dist/board/{core → model}/draw-geometry.js +1 -1
  64. package/dist/board/model/draw-input.d.ts +14 -0
  65. package/dist/board/model/draw-input.js +28 -0
  66. package/dist/board/{core → model}/export-assets.d.ts +1 -1
  67. package/dist/board/{core → model}/export-assets.js +1 -1
  68. package/dist/board/{core → model}/export-plan.d.ts +2 -2
  69. package/dist/board/{core → model}/export-plan.js +2 -2
  70. package/dist/board/{core → model}/file-preview.d.ts +1 -1
  71. package/dist/board/{core → model}/file-preview.js +1 -1
  72. package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
  73. package/dist/board/{core → model}/file-snapshot.js +1 -1
  74. package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
  75. package/dist/board/{geometry.js → model/geometry.js} +1 -1
  76. package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
  77. package/dist/board/{image-key.js → model/image-key.js} +1 -1
  78. package/dist/board/model/index.d.ts +35 -0
  79. package/dist/board/{index.js → model/index.js} +26 -22
  80. package/dist/board/model/items/builtin.d.ts +5 -0
  81. package/dist/board/model/items/builtin.js +119 -0
  82. package/dist/board/model/items/create.d.ts +59 -0
  83. package/dist/board/model/items/create.js +271 -0
  84. package/dist/board/model/items/definition.d.ts +35 -0
  85. package/dist/board/model/items/definition.js +18 -0
  86. package/dist/board/model/items/id.d.ts +5 -0
  87. package/dist/board/model/items/id.js +11 -0
  88. package/dist/board/model/items/index.d.ts +6 -0
  89. package/dist/board/model/items/index.js +6 -0
  90. package/dist/board/model/items/registry.d.ts +35 -0
  91. package/dist/board/model/items/registry.js +84 -0
  92. package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +2 -2
  93. package/dist/board/{media-playback.js → model/media-playback.js} +1 -1
  94. package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
  95. package/dist/board/{media.js → model/media.js} +1 -1
  96. package/dist/board/{core → model}/palette.d.ts +1 -1
  97. package/dist/board/{core → model}/palette.js +1 -1
  98. package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
  99. package/dist/board/{presets.js → model/presets.js} +1 -1
  100. package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
  101. package/dist/board/{replay.js → model/replay.js} +2 -2
  102. package/dist/board/{core → model}/scene.d.ts +1 -1
  103. package/dist/board/{core → model}/scene.js +1 -1
  104. package/dist/board/model/shape-types.d.ts +8 -0
  105. package/dist/board/model/shape-types.js +8 -0
  106. package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
  107. package/dist/board/{task.js → model/task.js} +3 -3
  108. package/dist/board/{core → model}/text-metrics.d.ts +1 -1
  109. package/dist/board/{core → model}/text-metrics.js +1 -1
  110. package/dist/board/{core → model}/tool-styles.d.ts +1 -1
  111. package/dist/board/{core → model}/tool-styles.js +1 -1
  112. package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
  113. package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
  114. package/dist/board/render/board-background.d.ts +1 -1
  115. package/dist/board/render/clip.d.ts +1 -1
  116. package/dist/board/render/index.d.ts +2 -2
  117. package/dist/board/render/index.js +2 -2
  118. package/dist/board/render/media-interaction.d.ts +2 -2
  119. package/dist/board/render/media-interaction.js +1 -1
  120. package/dist/board/render/palette.d.ts +1 -1
  121. package/dist/board/render/palette.js +1 -1
  122. package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
  123. package/dist/board/render/renderers/board-renderer-registry.d.ts +11 -6
  124. package/dist/board/render/renderers/board-renderer-registry.js +15 -15
  125. package/dist/board/render/renderers/draw-card-renderer.js +1 -1
  126. package/dist/board/render/renderers/far-plate.js +1 -1
  127. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
  128. package/dist/board/render/renderers/file-card-renderer.js +2 -2
  129. package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
  130. package/dist/board/render/renderers/shape-card-renderer.js +1 -1
  131. package/dist/board/render/renderers/task-card-renderer.js +1 -1
  132. package/dist/board/render/renderers/text-card-renderer.js +1 -1
  133. package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
  134. package/dist/board/render/stroke.d.ts +1 -1
  135. package/dist/board/render/stroke.js +1 -1
  136. package/dist/board/render/text-measurement.js +1 -1
  137. package/dist/board/replica/index.d.ts +70 -0
  138. package/dist/board/replica/index.js +276 -0
  139. package/dist/board/stage/assets.d.ts +41 -0
  140. package/dist/board/stage/assets.js +350 -0
  141. package/dist/board/stage/index.d.ts +8 -0
  142. package/dist/board/stage/index.js +8 -0
  143. package/dist/board/stage/lru.d.ts +32 -0
  144. package/dist/board/stage/lru.js +29 -0
  145. package/dist/board/stage/pointer.d.ts +8 -0
  146. package/dist/board/stage/pointer.js +33 -0
  147. package/dist/board/stage/scene.d.ts +85 -0
  148. package/dist/board/stage/scene.js +512 -0
  149. package/dist/board/stage/stage.d.ts +66 -0
  150. package/dist/board/stage/stage.js +568 -0
  151. package/dist/board/stage/texture-pool.d.ts +24 -0
  152. package/dist/board/stage/texture-pool.js +154 -0
  153. package/dist/board/stage/views.d.ts +38 -0
  154. package/dist/board/stage/views.js +93 -0
  155. package/dist/chunks/environment.d.ts +25 -1
  156. package/dist/chunks/http.d.ts +99 -66
  157. package/dist/chunks/http.js +214 -207
  158. package/dist/chunks/websocket.d.ts +1 -1
  159. package/dist/http.d.ts +2 -2
  160. package/dist/index.d.ts +3 -3
  161. package/dist/index.js +20 -4
  162. package/dist/media.d.ts +1 -10
  163. package/dist/media.js +16 -27
  164. package/dist/protocol/dist/board-patch.js +1 -1
  165. package/dist/protocol/dist/board.d.ts +28 -2
  166. package/dist/protocol/dist/board.js +3 -1
  167. package/dist/protocol/dist/core/media.d.ts +5 -0
  168. package/dist/protocol/dist/core/media.js +39 -0
  169. package/dist/protocol/dist/index.d.ts +5 -5
  170. package/dist/types.d.ts +1 -1
  171. package/package.json +26 -14
  172. package/dist/board/core/shape-definition.d.ts +0 -23
  173. package/dist/board/core/shape-definition.js +0 -45
  174. package/dist/board/core/shape-types.d.ts +0 -38
  175. package/dist/board/core/shape-types.js +0 -22
  176. package/dist/board/headless/index.d.ts +0 -55
  177. package/dist/board/headless/sketch.d.ts +0 -19
  178. package/dist/board/index.d.ts +0 -30
@@ -0,0 +1,75 @@
1
+ //#region src/board/editor/snapping.ts
2
+ /** The three snap positions (near/center/far) of a bounds rect on one axis. */
3
+ function axisPositions(rect, axis) {
4
+ const origin = axis === "x" ? rect.x : rect.y;
5
+ const size = axis === "x" ? rect.width : rect.height;
6
+ return [
7
+ origin,
8
+ origin + size / 2,
9
+ origin + size
10
+ ];
11
+ }
12
+ function axisExtent(rect, axis) {
13
+ if (axis === "x") return [rect.y, rect.y + rect.height];
14
+ return [rect.x, rect.x + rect.width];
15
+ }
16
+ /**
17
+ * Compute the snap for a moving selection bounds against candidate target
18
+ * bounds. Each axis is solved independently: the nearest matching edge/center
19
+ * within `threshold` wins. Grid lines are included as targets when enabled.
20
+ */
21
+ function computeSnap(moving, targets, options) {
22
+ const { threshold } = options;
23
+ const guides = [];
24
+ let dx = 0;
25
+ let dy = 0;
26
+ for (const axis of ["x", "y"]) {
27
+ const movingPositions = axisPositions(moving, axis);
28
+ const candidates = [];
29
+ for (const target of targets) {
30
+ const [from, to] = axisExtent(target, axis);
31
+ for (const at of axisPositions(target, axis)) candidates.push({
32
+ at,
33
+ from,
34
+ to
35
+ });
36
+ }
37
+ if (options.gridSize && options.gridSize > 0) {
38
+ const [from, to] = axisExtent(moving, axis);
39
+ for (const pos of movingPositions) {
40
+ const grid = Math.round(pos / options.gridSize) * options.gridSize;
41
+ candidates.push({
42
+ at: grid,
43
+ from,
44
+ to
45
+ });
46
+ }
47
+ }
48
+ let best = null;
49
+ for (const movingPos of movingPositions) for (const candidate of candidates) {
50
+ const delta = candidate.at - movingPos;
51
+ if (Math.abs(delta) > threshold) continue;
52
+ if (!best || Math.abs(delta) < Math.abs(best.delta)) best = {
53
+ delta,
54
+ ...candidate
55
+ };
56
+ }
57
+ if (!best) continue;
58
+ if (axis === "x") dx = best.delta;
59
+ else dy = best.delta;
60
+ const [movingFrom, movingTo] = axisExtent(moving, axis);
61
+ guides.push({
62
+ axis,
63
+ at: best.at,
64
+ from: Math.min(best.from, movingFrom),
65
+ to: Math.max(best.to, movingTo)
66
+ });
67
+ }
68
+ return {
69
+ dx,
70
+ dy,
71
+ guides
72
+ };
73
+ }
74
+ //#endregion
75
+ export { computeSnap };
@@ -0,0 +1,27 @@
1
+ import { BoardTaskSnapshot } from "../../protocol/dist/board-model.js";
2
+ import { BoardFileSnapshotFacts } from "../model/file-snapshot.js";
3
+ import "../model/index.js";
4
+ import { EditorContext } from "./context.js";
5
+ //#region src/board/editor/snapshots.d.ts
6
+ type BoardNaturalSize = {
7
+ id: string;
8
+ width: number;
9
+ height: number;
10
+ };
11
+ declare function createSnapshotsModule(ctx: EditorContext): {
12
+ applyMediaFileChange: (path: string, change: {
13
+ size?: number;
14
+ mtimeMs?: number;
15
+ removed?: boolean;
16
+ }) => void;
17
+ adoptMediaNaturalSizes: (sizes: BoardNaturalSize[]) => void;
18
+ applyFileSnapshots: (snapshots: Array<{
19
+ id: string;
20
+ snapshot: BoardFileSnapshotFacts;
21
+ replace?: boolean;
22
+ }>) => void;
23
+ applyTaskSnapshots: (snapshots: ReadonlyMap<string, BoardTaskSnapshot>) => void;
24
+ };
25
+ type SnapshotsModule = ReturnType<typeof createSnapshotsModule>;
26
+ //#endregion
27
+ export { BoardNaturalSize, SnapshotsModule, createSnapshotsModule };
@@ -0,0 +1,132 @@
1
+ import { mergeFileSnapshot } from "../model/file-preview.js";
2
+ import { featuredTaskArtifact } from "../model/task.js";
3
+ import { sameJson } from "./document-edits.js";
4
+ //#region src/board/editor/snapshots.ts
5
+ function fitHeight(item, natural) {
6
+ const height = item.size.width * natural.height / natural.width;
7
+ if (!Number.isFinite(height) || height <= 0) return null;
8
+ return {
9
+ ...item,
10
+ position: {
11
+ x: item.position.x,
12
+ y: item.position.y + (item.size.height - height) / 2
13
+ },
14
+ size: {
15
+ ...item.size,
16
+ height
17
+ }
18
+ };
19
+ }
20
+ function createSnapshotsModule(ctx) {
21
+ const { state } = ctx;
22
+ const commit = (changes) => ctx.document.commitItems(changes, false);
23
+ function applyMediaFileChange(path, change) {
24
+ if (change.removed) return;
25
+ const changes = /* @__PURE__ */ new Map();
26
+ for (const id of ctx.query.mediaIdsForPath(path)) {
27
+ const item = state.base.items[id];
28
+ if (!item || item.type !== "image" && item.type !== "video" && item.type !== "audio") continue;
29
+ const snapshot = { ...item.props.snapshot };
30
+ if (change.size !== void 0) snapshot.size = change.size;
31
+ if (change.mtimeMs !== void 0) snapshot.mtimeMs = change.mtimeMs;
32
+ delete snapshot.naturalWidth;
33
+ delete snapshot.naturalHeight;
34
+ if (item.type === "audio") delete snapshot.durationMs;
35
+ if (!sameJson(snapshot, item.props.snapshot ?? {})) changes.set(id, {
36
+ ...item,
37
+ props: {
38
+ ...item.props,
39
+ snapshot
40
+ }
41
+ });
42
+ }
43
+ commit(changes);
44
+ }
45
+ function adoptMediaNaturalSizes(sizes) {
46
+ const changes = /* @__PURE__ */ new Map();
47
+ for (const natural of sizes) {
48
+ const item = state.base.items[natural.id];
49
+ if (!item || natural.width <= 0 || natural.height <= 0) continue;
50
+ const dimensions = {
51
+ naturalWidth: natural.width,
52
+ naturalHeight: natural.height
53
+ };
54
+ if (item.type === "task") {
55
+ const artifact = featuredTaskArtifact(item.props.snapshot.artifacts);
56
+ if (artifact?.type !== "image" && artifact?.type !== "video" || artifact.naturalWidth && artifact.naturalHeight) continue;
57
+ const fitted = fitHeight(item, natural);
58
+ if (!fitted) continue;
59
+ const artifacts = item.props.snapshot.artifacts.map((entry) => entry.id === artifact.id ? {
60
+ ...entry,
61
+ ...dimensions
62
+ } : entry);
63
+ changes.set(natural.id, {
64
+ ...fitted,
65
+ props: {
66
+ ...item.props,
67
+ snapshot: {
68
+ ...item.props.snapshot,
69
+ artifacts
70
+ }
71
+ }
72
+ });
73
+ continue;
74
+ }
75
+ if (item.type !== "image" && item.type !== "video") continue;
76
+ if (item.props.snapshot?.naturalWidth && item.props.snapshot.naturalHeight) continue;
77
+ const fitted = fitHeight(item, natural);
78
+ if (!fitted) continue;
79
+ changes.set(natural.id, {
80
+ ...fitted,
81
+ props: {
82
+ ...item.props,
83
+ snapshot: {
84
+ ...item.props.snapshot,
85
+ ...dimensions
86
+ }
87
+ }
88
+ });
89
+ }
90
+ commit(changes);
91
+ }
92
+ function applyFileSnapshots(snapshots) {
93
+ const changes = /* @__PURE__ */ new Map();
94
+ for (const entry of snapshots) {
95
+ const item = state.base.items[entry.id];
96
+ if (item?.type !== "file") continue;
97
+ const merged = mergeFileSnapshot(item.props.snapshot, entry.snapshot, entry.replace === true);
98
+ if (!sameJson(merged, item.props.snapshot ?? {})) changes.set(entry.id, {
99
+ ...item,
100
+ props: {
101
+ ...item.props,
102
+ snapshot: merged
103
+ }
104
+ });
105
+ }
106
+ commit(changes);
107
+ }
108
+ function applyTaskSnapshots(snapshots) {
109
+ if (snapshots.size === 0) return;
110
+ const changes = /* @__PURE__ */ new Map();
111
+ for (const [id, item] of Object.entries(state.base.items)) {
112
+ if (item.type !== "task") continue;
113
+ const snapshot = snapshots.get(item.props.taskRunId);
114
+ if (snapshot && !sameJson(snapshot, item.props.snapshot)) changes.set(id, {
115
+ ...item,
116
+ props: {
117
+ ...item.props,
118
+ snapshot
119
+ }
120
+ });
121
+ }
122
+ commit(changes);
123
+ }
124
+ return {
125
+ applyMediaFileChange,
126
+ adoptMediaNaturalSizes,
127
+ applyFileSnapshots,
128
+ applyTaskSnapshots
129
+ };
130
+ }
131
+ //#endregion
132
+ export { createSnapshotsModule };
@@ -0,0 +1,41 @@
1
+ import { Point, Rect } from "../model/geometry.js";
2
+ import "../model/index.js";
3
+ //#region src/board/editor/spatial.d.ts
4
+ /**
5
+ * An indexed board item: its stable id, z-order (array index, higher = on
6
+ * top), and an axis-aligned bounding box. The AABB already accounts for
7
+ * rotation (see itemBounds), so the tree operates in world space and never
8
+ * reasons about rotation directly — callers refine rotated hits themselves.
9
+ */
10
+ type SpatialEntry = {
11
+ id: string;
12
+ order: number;
13
+ rect: Rect;
14
+ };
15
+ type SpatialIndex = {
16
+ /** Rebuild the tree from scratch. */
17
+ rebuild: (entries: SpatialEntry[]) => void;
18
+ /**
19
+ * Apply a set of dirty entries: remove-then-insert each (or remove if
20
+ * absent from the map). Falls back to a full rebuild when the dirty set is
21
+ * large or when new entries fall outside the current root bounds.
22
+ */
23
+ upsert: (entries: Map<string, SpatialEntry | null>) => void;
24
+ /** Ids whose AABB intersects the range, in no particular order. */
25
+ idsInRect: (range: Rect) => string[];
26
+ /**
27
+ * Ids whose AABB contains the point, ordered topmost-first (highest
28
+ * z-order first) so callers can refine with an exact rotated hit test and
29
+ * take the first match.
30
+ */
31
+ idsAtPoint: (point: Point) => string[];
32
+ readonly size: number;
33
+ };
34
+ /**
35
+ * A point quadtree over item bounding boxes. Supports both wholesale rebuild
36
+ * (document load / large batch changes) and dirty-entry upsert (gesture frames)
37
+ * so a drag never pays a full O(n) rebuild every frame.
38
+ */
39
+ declare function createSpatialIndex(): SpatialIndex;
40
+ //#endregion
41
+ export { SpatialEntry, SpatialIndex, createSpatialIndex };
@@ -0,0 +1,191 @@
1
+ import { rectContainsPoint, rectsIntersect } from "../model/geometry.js";
2
+ //#region src/board/editor/spatial.ts
3
+ /** Max entries in a leaf before it subdivides. */
4
+ const NODE_CAPACITY = 8;
5
+ /** Deepest the tree may subdivide (bounds get tiny; stop splitting). */
6
+ const MAX_DEPTH = 10;
7
+ /** Subdivision stops once a cell is smaller than this (degenerate bounds). */
8
+ const MIN_CELL_SIZE = 1;
9
+ /**
10
+ * When dirty entries exceed this fraction of the tree (or this absolute count),
11
+ * a full rebuild is cheaper and more compact than many surgical removes.
12
+ */
13
+ const REBUILD_DIRTY_RATIO = .25;
14
+ const REBUILD_DIRTY_MIN = 32;
15
+ function rectContainsRect(outer, inner) {
16
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
17
+ }
18
+ function quadrantBounds(bounds) {
19
+ const halfW = bounds.width / 2;
20
+ const halfH = bounds.height / 2;
21
+ const { x, y } = bounds;
22
+ return [
23
+ {
24
+ x,
25
+ y,
26
+ width: halfW,
27
+ height: halfH
28
+ },
29
+ {
30
+ x: x + halfW,
31
+ y,
32
+ width: halfW,
33
+ height: halfH
34
+ },
35
+ {
36
+ x,
37
+ y: y + halfH,
38
+ width: halfW,
39
+ height: halfH
40
+ },
41
+ {
42
+ x: x + halfW,
43
+ y: y + halfH,
44
+ width: halfW,
45
+ height: halfH
46
+ }
47
+ ];
48
+ }
49
+ function createNode(bounds, depth) {
50
+ return {
51
+ bounds,
52
+ depth,
53
+ entries: [],
54
+ children: null
55
+ };
56
+ }
57
+ function canSubdivide(node) {
58
+ return node.depth < MAX_DEPTH && node.bounds.width > MIN_CELL_SIZE && node.bounds.height > MIN_CELL_SIZE;
59
+ }
60
+ function insertEntry(node, entry) {
61
+ if (node.children) {
62
+ for (const child of node.children) if (rectContainsRect(child.bounds, entry.rect)) {
63
+ insertEntry(child, entry);
64
+ return;
65
+ }
66
+ node.entries.push(entry);
67
+ return;
68
+ }
69
+ node.entries.push(entry);
70
+ if (node.entries.length <= NODE_CAPACITY || !canSubdivide(node)) return;
71
+ node.children = quadrantBounds(node.bounds).map((bounds) => createNode(bounds, node.depth + 1));
72
+ const kept = [];
73
+ for (const existing of node.entries) {
74
+ let placed = false;
75
+ for (const child of node.children) if (rectContainsRect(child.bounds, existing.rect)) {
76
+ insertEntry(child, existing);
77
+ placed = true;
78
+ break;
79
+ }
80
+ if (!placed) kept.push(existing);
81
+ }
82
+ node.entries = kept;
83
+ }
84
+ /** Remove an entry by id. Returns true if found. */
85
+ function removeEntry(node, id) {
86
+ const index = node.entries.findIndex((entry) => entry.id === id);
87
+ if (index >= 0) {
88
+ node.entries.splice(index, 1);
89
+ return true;
90
+ }
91
+ if (!node.children) return false;
92
+ for (const child of node.children) if (removeEntry(child, id)) return true;
93
+ return false;
94
+ }
95
+ function queryRect(node, range, out) {
96
+ if (!rectsIntersect(node.bounds, range)) return;
97
+ for (const entry of node.entries) if (rectsIntersect(entry.rect, range)) out.push(entry);
98
+ if (!node.children) return;
99
+ for (const child of node.children) queryRect(child, range, out);
100
+ }
101
+ function queryPoint(node, point, out) {
102
+ if (!rectContainsPoint(node.bounds, point)) return;
103
+ for (const entry of node.entries) if (rectContainsPoint(entry.rect, point)) out.push(entry);
104
+ if (!node.children) return;
105
+ for (const child of node.children) queryPoint(child, point, out);
106
+ }
107
+ /**
108
+ * A point quadtree over item bounding boxes. Supports both wholesale rebuild
109
+ * (document load / large batch changes) and dirty-entry upsert (gesture frames)
110
+ * so a drag never pays a full O(n) rebuild every frame.
111
+ */
112
+ function createSpatialIndex() {
113
+ let root = null;
114
+ /** Authoritative entry map — source of truth for rebuilds. */
115
+ const byId = /* @__PURE__ */ new Map();
116
+ function rebuildFromMap() {
117
+ const entries = [...byId.values()];
118
+ if (entries.length === 0) {
119
+ root = null;
120
+ return;
121
+ }
122
+ let minX = Number.POSITIVE_INFINITY;
123
+ let minY = Number.POSITIVE_INFINITY;
124
+ let maxX = Number.NEGATIVE_INFINITY;
125
+ let maxY = Number.NEGATIVE_INFINITY;
126
+ for (const entry of entries) {
127
+ minX = Math.min(minX, entry.rect.x);
128
+ minY = Math.min(minY, entry.rect.y);
129
+ maxX = Math.max(maxX, entry.rect.x + entry.rect.width);
130
+ maxY = Math.max(maxY, entry.rect.y + entry.rect.height);
131
+ }
132
+ const width = Math.max(maxX - minX, MIN_CELL_SIZE);
133
+ const height = Math.max(maxY - minY, MIN_CELL_SIZE);
134
+ const pad = Math.max(width, height) * .05;
135
+ root = createNode({
136
+ x: minX - pad,
137
+ y: minY - pad,
138
+ width: width + pad * 2,
139
+ height: height + pad * 2
140
+ }, 0);
141
+ for (const entry of entries) insertEntry(root, entry);
142
+ }
143
+ function rebuild(entries) {
144
+ byId.clear();
145
+ for (const entry of entries) byId.set(entry.id, entry);
146
+ rebuildFromMap();
147
+ }
148
+ function upsert(entries) {
149
+ if (entries.size === 0) return;
150
+ const dirtyCount = entries.size;
151
+ const total = Math.max(byId.size, 1);
152
+ const shouldRebuild = dirtyCount >= REBUILD_DIRTY_MIN && dirtyCount / total >= REBUILD_DIRTY_RATIO;
153
+ let expandsRoot = false;
154
+ for (const [id, entry] of entries) if (entry) {
155
+ byId.set(id, entry);
156
+ if (root && !rectContainsRect(root.bounds, entry.rect)) expandsRoot = true;
157
+ } else byId.delete(id);
158
+ if (!root || shouldRebuild || expandsRoot) {
159
+ rebuildFromMap();
160
+ return;
161
+ }
162
+ for (const [id, entry] of entries) {
163
+ removeEntry(root, id);
164
+ if (entry) insertEntry(root, entry);
165
+ }
166
+ }
167
+ function idsInRect(range) {
168
+ if (!root) return [];
169
+ const hits = [];
170
+ queryRect(root, range, hits);
171
+ return hits.map((entry) => entry.id);
172
+ }
173
+ function idsAtPoint(point) {
174
+ if (!root) return [];
175
+ const hits = [];
176
+ queryPoint(root, point, hits);
177
+ hits.sort((a, b) => b.order - a.order);
178
+ return hits.map((entry) => entry.id);
179
+ }
180
+ return {
181
+ rebuild,
182
+ upsert,
183
+ idsInRect,
184
+ idsAtPoint,
185
+ get size() {
186
+ return byId.size;
187
+ }
188
+ };
189
+ }
190
+ //#endregion
191
+ export { createSpatialIndex };
@@ -0,0 +1,12 @@
1
+ //#region src/board/editor/state.d.ts
2
+ type BoardEditorChannel = "document" | "scene" | "selection" | "camera" | "tool" | "interaction" | "hover" | "editing" | "playback" | "history" | "status";
3
+ type BoardEditorListener = () => void;
4
+ type BoardEditorChannels = {
5
+ emit(channel: BoardEditorChannel): void;
6
+ subscribe(channels: BoardEditorChannel | readonly BoardEditorChannel[], listener: BoardEditorListener): () => void;
7
+ };
8
+ declare function createChannels(): BoardEditorChannels;
9
+ declare function observable<T extends Record<string, unknown>>(initial: T, channelOf: { readonly [K in keyof T]: BoardEditorChannel; }, channels: BoardEditorChannels): T;
10
+ declare function memo<const D extends readonly unknown[], R>(inputs: () => D, compute: (...inputs: D) => R): () => R;
11
+ //#endregion
12
+ export { BoardEditorChannel, BoardEditorChannels, BoardEditorListener, createChannels, memo, observable };
@@ -0,0 +1,49 @@
1
+ //#region src/board/editor/state.ts
2
+ function createChannels() {
3
+ const listeners = /* @__PURE__ */ new Map();
4
+ return {
5
+ emit(channel) {
6
+ const subscribed = listeners.get(channel);
7
+ if (subscribed) for (const listener of [...subscribed]) listener();
8
+ },
9
+ subscribe(channels, listener) {
10
+ const list = typeof channels === "string" ? [channels] : channels;
11
+ for (const channel of list) {
12
+ const set = listeners.get(channel) ?? /* @__PURE__ */ new Set();
13
+ set.add(listener);
14
+ listeners.set(channel, set);
15
+ }
16
+ return () => {
17
+ for (const channel of list) listeners.get(channel)?.delete(listener);
18
+ };
19
+ }
20
+ };
21
+ }
22
+ function observable(initial, channelOf, channels) {
23
+ const values = { ...initial };
24
+ const state = {};
25
+ for (const key of Object.keys(initial)) Object.defineProperty(state, key, {
26
+ enumerable: true,
27
+ get: () => values[key],
28
+ set: (value) => {
29
+ if (Object.is(values[key], value)) return;
30
+ values[key] = value;
31
+ channels.emit(channelOf[key]);
32
+ }
33
+ });
34
+ return state;
35
+ }
36
+ function memo(inputs, compute) {
37
+ let previous = null;
38
+ let value;
39
+ return () => {
40
+ const next = inputs();
41
+ if (previous === null || next.length !== previous.length || next.some((input, index) => !Object.is(input, previous?.[index]))) {
42
+ previous = next;
43
+ value = compute(...next);
44
+ }
45
+ return value;
46
+ };
47
+ }
48
+ //#endregion
49
+ export { createChannels, memo, observable };
@@ -0,0 +1,10 @@
1
+ //#region src/board/editor/tool.d.ts
2
+ type BoardToolId = "select" | "hand" | "text" | "shape" | "draw" | "arrow" | "frame";
3
+ declare const BOARD_HAND_TAP_SLOP = 8;
4
+ declare function defaultBoardTool(isMobile: boolean): BoardToolId;
5
+ declare function canTapSelectWithHand(pointerType: string): boolean;
6
+ declare function isWithinHandTapSlop(dx: number, dy: number): boolean;
7
+ declare function isContinuousBoardTool(tool: BoardToolId): boolean;
8
+ declare function isCreationBoardTool(tool: BoardToolId): boolean;
9
+ //#endregion
10
+ export { BOARD_HAND_TAP_SLOP, BoardToolId, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
@@ -0,0 +1,19 @@
1
+ //#region src/board/editor/tool.ts
2
+ const BOARD_HAND_TAP_SLOP = 8;
3
+ function defaultBoardTool(isMobile) {
4
+ return isMobile ? "hand" : "select";
5
+ }
6
+ function canTapSelectWithHand(pointerType) {
7
+ return pointerType === "touch" || pointerType === "pen";
8
+ }
9
+ function isWithinHandTapSlop(dx, dy) {
10
+ return Number.isFinite(dx) && Number.isFinite(dy) && Math.hypot(dx, dy) <= 8;
11
+ }
12
+ function isContinuousBoardTool(tool) {
13
+ return tool === "draw";
14
+ }
15
+ function isCreationBoardTool(tool) {
16
+ return tool === "text" || tool === "shape" || tool === "draw" || tool === "arrow" || tool === "frame";
17
+ }
18
+ //#endregion
19
+ export { BOARD_HAND_TAP_SLOP, canTapSelectWithHand, defaultBoardTool, isContinuousBoardTool, isCreationBoardTool, isWithinHandTapSlop };
@@ -0,0 +1,5 @@
1
+ //#region src/board/editor/wheel.d.ts
2
+ declare function normalizeWheelDelta(delta: number, deltaMode?: number): number;
3
+ declare function wheelZoomFactor(deltaY: number, deltaMode?: number): number;
4
+ //#endregion
5
+ export { normalizeWheelDelta, wheelZoomFactor };
@@ -0,0 +1,17 @@
1
+ //#region src/board/editor/wheel.ts
2
+ const WHEEL_ZOOM_INTENSITY = .012;
3
+ const MAX_WHEEL_ZOOM_FACTOR = 1.35;
4
+ const LINE_DELTA_PX = 16;
5
+ const PAGE_DELTA_PX = 80;
6
+ function normalizeWheelDelta(delta, deltaMode = 0) {
7
+ if (!Number.isFinite(delta)) return 0;
8
+ if (deltaMode === 1) return delta * LINE_DELTA_PX;
9
+ if (deltaMode === 2) return delta * PAGE_DELTA_PX;
10
+ return delta;
11
+ }
12
+ function wheelZoomFactor(deltaY, deltaMode = 0) {
13
+ const factor = Math.exp(-normalizeWheelDelta(deltaY, deltaMode) * WHEEL_ZOOM_INTENSITY);
14
+ return Math.min(MAX_WHEEL_ZOOM_FACTOR, Math.max(1 / MAX_WHEEL_ZOOM_FACTOR, factor));
15
+ }
16
+ //#endregion
17
+ export { normalizeWheelDelta, wheelZoomFactor };
@@ -1,9 +1,9 @@
1
1
  import { BoardDocument } from "../../protocol/dist/board-model.js";
2
2
  import "../../protocol/dist/index.js";
3
- import { BoardSceneItem } from "../core/scene.js";
4
- import { BoardExportPlan, BoardExportPlanInput, BoardExportRegion } from "../core/export-plan.js";
5
- import { BoardShapeColors } from "../core/palette.js";
6
- import { BoardRenderPalette, BoardSketchHost } from "../render/renderers/board-renderer-registry.js";
3
+ import { BoardSceneItem } from "../model/scene.js";
4
+ import { BoardExportPlan, BoardExportPlanInput, BoardExportRegion } from "../model/export-plan.js";
5
+ import { BoardShapeColors } from "../model/palette.js";
6
+ import { BoardCardRendererResolver, BoardRenderPalette, BoardSketchHost } from "../render/renderers/board-renderer-registry.js";
7
7
  import "../render/index.js";
8
8
  import { BoardExportScene, BoardExportSceneInput, createBoardExportScene } from "./scene.js";
9
9
  import { ICanvas, Renderer, Texture } from "pixi.js";
@@ -24,6 +24,7 @@ type BoardExportOptions = {
24
24
  textures?: Map<string, Texture>;
25
25
  sketches?: BoardSketchHost;
26
26
  assetKey?: (item: BoardSceneItem) => string | null;
27
+ renderers?: BoardCardRendererResolver;
27
28
  backgroundImage?: {
28
29
  texture: Texture;
29
30
  fit: "cover" | "contain" | "repeat";
@@ -1,6 +1,6 @@
1
- import { boardDocumentAt } from "../animation.js";
2
- import { normalizeBoardDocument, planBoardExport } from "../core/export-plan.js";
3
- import { buildBoardScene } from "../core/scene.js";
1
+ import { boardDocumentAt } from "../model/animation.js";
2
+ import { normalizeBoardDocument, planBoardExport } from "../model/export-plan.js";
3
+ import { buildBoardScene } from "../model/scene.js";
4
4
  import { parseBoardCssColor } from "../render/css-color.js";
5
5
  import { defaultBoardPalette } from "../render/palette.js";
6
6
  import { ensureBoardTextMeasurement } from "../render/text-measurement.js";
@@ -46,6 +46,7 @@ function renderBoardExport(renderer, input, options = {}) {
46
46
  textures: options.textures,
47
47
  sketches: options.sketches,
48
48
  assetKey: options.assetKey,
49
+ renderers: options.renderers,
49
50
  background: resolveBackground(options.background, palette, document, colorScheme),
50
51
  backgroundImage: options.backgroundImage
51
52
  });
@@ -0,0 +1,55 @@
1
+ import { BoardDocument } from "../../../protocol/dist/board-model.js";
2
+ import "../../../protocol/dist/index.js";
3
+ import { BoardSketchHost } from "../../render/renderers/board-renderer-registry.js";
4
+ import { BoardExportOptions, BoardExportResult } from "../index.js";
5
+ import { createNodeBoardSketchHost } from "./sketch.js";
6
+ import { Renderer } from "pixi.js";
7
+ //#region src/board/export/node/index.d.ts
8
+ type NodeCanvasModule = {
9
+ createCanvas: (width: number, height: number) => unknown;
10
+ Image: new () => unknown;
11
+ GlobalFonts: {
12
+ registerFromPath: (path: string, name?: string) => unknown;
13
+ has: (name: string) => boolean;
14
+ setAlias?: (fontName: string, alias: string) => boolean;
15
+ };
16
+ };
17
+ type BoardNodeFont = {
18
+ path: string;
19
+ family?: string;
20
+ };
21
+ type BoardNodeRendererOptions = {
22
+ fonts?: BoardNodeFont[];
23
+ canvasModule?: NodeCanvasModule;
24
+ };
25
+ type BoardNodeTexture = {
26
+ readonly __boardTexture: unique symbol;
27
+ };
28
+ type BoardNodeRenderer = {
29
+ renderer: Renderer;
30
+ canvasModule: NodeCanvasModule;
31
+ decodeImage: (bytes: Uint8Array, mimeType?: string) => Promise<BoardNodeTexture>;
32
+ destroy: () => void;
33
+ };
34
+ declare function createNodeBoardRenderer(options?: BoardNodeRendererOptions): Promise<BoardNodeRenderer>;
35
+ type BoardNodeExportFormat = "png" | "jpeg" | "webp";
36
+ type BoardNodeExportOptions = Omit<BoardExportOptions, "textures" | "backgroundImage"> & {
37
+ textures?: Map<string, BoardNodeTexture>;
38
+ backgroundImage?: {
39
+ texture: BoardNodeTexture;
40
+ fit: "cover" | "contain" | "repeat";
41
+ position: "center" | "top" | "bottom" | "left" | "right";
42
+ opacity: number;
43
+ };
44
+ sketches?: BoardSketchHost;
45
+ format?: BoardNodeExportFormat;
46
+ quality?: number;
47
+ };
48
+ type BoardNodeExportResult = Omit<BoardExportResult, "canvas"> & {
49
+ bytes: Uint8Array;
50
+ format: BoardNodeExportFormat;
51
+ };
52
+ declare function boardImageMimeType(format: BoardNodeExportFormat): string;
53
+ declare function exportBoardImageBytes(renderer: BoardNodeRenderer, document: BoardDocument, options?: BoardNodeExportOptions): BoardNodeExportResult | null;
54
+ //#endregion
55
+ export { BoardNodeExportFormat, BoardNodeExportOptions, BoardNodeExportResult, BoardNodeFont, BoardNodeRenderer, BoardNodeRendererOptions, BoardNodeTexture, NodeCanvasModule, boardImageMimeType, createNodeBoardRenderer, createNodeBoardSketchHost, exportBoardImageBytes };