@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,201 @@
1
+ import { rectContainsPoint, unionRects } from "../model/geometry.js";
2
+ import { arrowHitRadius, arrowPathBounds, distanceToArrow, resolveSceneArrow } from "../model/arrow-geometry.js";
3
+ import { createSpatialIndex } from "./spatial.js";
4
+ //#region src/board/editor/query.ts
5
+ const NO_IDS = /* @__PURE__ */ new Set();
6
+ function createQueryModule(ctx) {
7
+ const { state, registry } = ctx;
8
+ const spatial = createSpatialIndex();
9
+ let indexedScene = null;
10
+ let spatialDirty = null;
11
+ let mediaIdsByPath = null;
12
+ function invalidate(ids) {
13
+ if (!ids) {
14
+ spatialDirty = null;
15
+ mediaIdsByPath = null;
16
+ return;
17
+ }
18
+ if (!spatialDirty) return;
19
+ const scene = state.scene;
20
+ const affected = /* @__PURE__ */ new Set();
21
+ const pending = [...ids];
22
+ while (pending.length) {
23
+ const id = pending.pop();
24
+ if (affected.has(id)) continue;
25
+ affected.add(id);
26
+ spatialDirty.add(id);
27
+ pending.push(...scene.children(id), ...scene.binders(id));
28
+ }
29
+ }
30
+ function ensureSpatial() {
31
+ const scene = state.scene;
32
+ if (indexedScene === scene) return;
33
+ const dirty = indexedScene ? spatialDirty : null;
34
+ indexedScene = scene;
35
+ spatialDirty = /* @__PURE__ */ new Set();
36
+ if (dirty === null) {
37
+ const entries = scene.items.map((item, order) => ({
38
+ id: item.id,
39
+ order,
40
+ rect: boundsOf(item)
41
+ }));
42
+ spatial.rebuild(entries);
43
+ return;
44
+ }
45
+ const upserts = /* @__PURE__ */ new Map();
46
+ for (const id of dirty) {
47
+ const item = scene.get(id);
48
+ upserts.set(id, item ? {
49
+ id,
50
+ order: scene.indexOf(id),
51
+ rect: boundsOf(item)
52
+ } : null);
53
+ }
54
+ spatial.upsert(upserts);
55
+ }
56
+ function boundsOf(item) {
57
+ if (item.type === "arrow") return arrowPathBounds(resolveSceneArrow(item, state.scene), item.style.strokeWidth);
58
+ return registry.bounds(item);
59
+ }
60
+ function itemById(id) {
61
+ return state.scene.get(id) ?? null;
62
+ }
63
+ function isLocked(item) {
64
+ return item.locked === true;
65
+ }
66
+ function unlockedIds(ids) {
67
+ const result = [];
68
+ for (const id of ids) {
69
+ const item = state.scene.get(id);
70
+ if (item && !isLocked(item)) result.push(id);
71
+ }
72
+ return result;
73
+ }
74
+ function rootsOf(ids) {
75
+ const scene = state.scene;
76
+ const set = new Set(ids);
77
+ return [...set].filter((id) => {
78
+ let cursor = scene.get(id)?.parent;
79
+ for (let depth = 0; cursor && depth < 64; depth += 1) {
80
+ if (set.has(cursor)) return false;
81
+ cursor = scene.get(cursor)?.parent;
82
+ }
83
+ return true;
84
+ });
85
+ }
86
+ function withSubtrees(ids) {
87
+ const result = /* @__PURE__ */ new Set();
88
+ for (const id of ids) {
89
+ result.add(id);
90
+ for (const child of state.scene.descendants(id)) result.add(child);
91
+ }
92
+ return result;
93
+ }
94
+ function mediaIdsForPath(path) {
95
+ if (!mediaIdsByPath) {
96
+ mediaIdsByPath = /* @__PURE__ */ new Map();
97
+ for (const item of state.scene.items) {
98
+ if (item.type !== "image" && item.type !== "video" && item.type !== "audio") continue;
99
+ const src = item.props.src;
100
+ const ids = mediaIdsByPath.get(src) ?? /* @__PURE__ */ new Set();
101
+ ids.add(item.id);
102
+ mediaIdsByPath.set(src, ids);
103
+ }
104
+ }
105
+ return mediaIdsByPath.get(path) ?? NO_IDS;
106
+ }
107
+ function contentBounds(ids) {
108
+ const scene = state.scene;
109
+ const list = ids ? [...ids].flatMap((id) => scene.get(id) ? [scene.get(id)] : []) : scene.items;
110
+ return unionRects(list.map(boundsOf));
111
+ }
112
+ function idsInRect(rect) {
113
+ ensureSpatial();
114
+ return spatial.idsInRect(rect);
115
+ }
116
+ function frameAt(point, exclude) {
117
+ ensureSpatial();
118
+ for (const id of spatial.idsAtPoint(point)) {
119
+ const item = state.scene.get(id);
120
+ if (item?.type !== "frame" || exclude?.has(id)) continue;
121
+ if (registry.hitTest(item, point)) return item;
122
+ }
123
+ return null;
124
+ }
125
+ function hitsItem(item, point) {
126
+ if (item.type === "arrow") {
127
+ const resolved = resolveSceneArrow(item, state.scene);
128
+ return distanceToArrow(resolved, point) <= arrowHitRadius(item.style.strokeWidth) / Math.min(1, state.camera.zoom);
129
+ }
130
+ return registry.hitTest(item, point);
131
+ }
132
+ function itemAt(point, exclude) {
133
+ ensureSpatial();
134
+ for (const id of spatial.idsAtPoint(point)) {
135
+ const item = state.scene.get(id);
136
+ if (item && !exclude?.has(id) && hitsItem(item, point)) return item;
137
+ }
138
+ return null;
139
+ }
140
+ function labelItemAt(point) {
141
+ ensureSpatial();
142
+ const { scene, selection, camera } = state;
143
+ const nearby = spatial.idsAtPoint(point);
144
+ const candidates = selection.length === 1 ? [...selection, ...nearby] : nearby;
145
+ for (const id of new Set(candidates)) {
146
+ const item = scene.get(id);
147
+ if (!item || isLocked(item)) continue;
148
+ if (item.type === "frame" && rectContainsPoint({
149
+ x: item.frame.x,
150
+ y: item.frame.y - 22,
151
+ width: Math.max(48, item.frame.width * .5),
152
+ height: 24
153
+ }, point)) return item;
154
+ if (item.type === "arrow") {
155
+ const resolved = resolveSceneArrow(item, scene);
156
+ const radius = Math.max(10, (item.props.fontSize ?? 14) * .9) / Math.min(1, camera.zoom);
157
+ if (Math.hypot(resolved.mid.x - point.x, resolved.mid.y - point.y) <= radius) return item;
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ function arrowHandleAt(point) {
163
+ if (state.selection.length !== 1) return null;
164
+ const item = state.scene.get(state.selection[0]);
165
+ if (item?.type !== "arrow" || isLocked(item)) return null;
166
+ const resolved = resolveSceneArrow(item, state.scene);
167
+ const radius = 10 / state.camera.zoom;
168
+ const distance = (target) => Math.hypot(target.x - point.x, target.y - point.y);
169
+ const start = distance(resolved.start.point);
170
+ const end = distance(resolved.end.point);
171
+ const mid = distance(resolved.mid);
172
+ if (start <= radius && start <= end && start <= mid) return "start";
173
+ if (end <= radius && end <= mid) return "end";
174
+ if (mid <= radius) return "mid";
175
+ return null;
176
+ }
177
+ function connectTargetAt(point, exclude = NO_IDS) {
178
+ const item = itemAt(point, exclude);
179
+ if (!item || isLocked(item) || item.type === "arrow") return null;
180
+ return registry.capabilities(item).canConnect ? item.id : null;
181
+ }
182
+ return {
183
+ invalidate,
184
+ boundsOf,
185
+ itemById,
186
+ isLocked,
187
+ unlockedIds,
188
+ rootsOf,
189
+ withSubtrees,
190
+ mediaIdsForPath,
191
+ contentBounds,
192
+ idsInRect,
193
+ frameAt,
194
+ itemAt,
195
+ labelItemAt,
196
+ arrowHandleAt,
197
+ connectTargetAt
198
+ };
199
+ }
200
+ //#endregion
201
+ export { createQueryModule };
@@ -0,0 +1,11 @@
1
+ import { BoardDocument } from "../../protocol/dist/board-model.js";
2
+ import "../model/index.js";
3
+ import { EditorContext } from "./context.js";
4
+ //#region src/board/editor/remote.d.ts
5
+ declare function createRemoteModule(ctx: EditorContext): {
6
+ loadDocument: (next: BoardDocument, key?: string) => void;
7
+ flushPending: () => void;
8
+ };
9
+ type RemoteModule = ReturnType<typeof createRemoteModule>;
10
+ //#endregion
11
+ export { RemoteModule, createRemoteModule };
@@ -0,0 +1,55 @@
1
+ //#region src/board/editor/remote.ts
2
+ function createRemoteModule(ctx) {
3
+ const { state } = ctx;
4
+ let currentKey = ctx.options.key;
5
+ let pending = null;
6
+ function apply(next, key) {
7
+ const sameDocument = key !== void 0 && key === currentKey;
8
+ currentKey = key;
9
+ pending = null;
10
+ if (sameDocument) {
11
+ const { base } = state;
12
+ const added = Object.keys(next.items).filter((id) => !base.items[id]);
13
+ ctx.document.markAdded(added);
14
+ const changed = /* @__PURE__ */ new Set([...added, ...Object.keys(base.items).filter((id) => next.items[id] !== base.items[id])]);
15
+ const draftTextId = state.draftTextId;
16
+ const keep = draftTextId ? ctx.document.draftItem(draftTextId) : void 0;
17
+ ctx.document.adopt(next, changed);
18
+ if (draftTextId && keep) ctx.document.writeDraft(/* @__PURE__ */ new Map([[draftTextId, keep]]), true);
19
+ state.selection = state.selection.filter((id) => state.scene.get(id));
20
+ if (state.playhead && !next.animations[state.playhead.animationId]) ctx.animation.setPlayhead(null);
21
+ return;
22
+ }
23
+ state.playhead = null;
24
+ state.recording = false;
25
+ ctx.document.reset(next);
26
+ state.undoStack = [];
27
+ state.redoStack = [];
28
+ state.selection = [];
29
+ state.editingId = null;
30
+ state.draftTextId = null;
31
+ state.saveError = null;
32
+ }
33
+ function loadDocument(next, key) {
34
+ if (next === state.base) return;
35
+ if (key !== void 0 && key === currentKey && (state.interaction.type !== "idle" || state.editingId)) {
36
+ pending = {
37
+ document: next,
38
+ key
39
+ };
40
+ return;
41
+ }
42
+ apply(next, key);
43
+ }
44
+ function flushPending() {
45
+ if (!pending) return;
46
+ const { document, key } = pending;
47
+ apply(document, key);
48
+ }
49
+ return {
50
+ loadDocument,
51
+ flushPending
52
+ };
53
+ }
54
+ //#endregion
55
+ export { createRemoteModule };
@@ -0,0 +1,24 @@
1
+ import { BoardFrame } from "../../protocol/dist/board-layout.js";
2
+ import { Rect, ResizeHandle, WorldPoint } from "../model/geometry.js";
3
+ import { BoardSceneItem } from "../model/scene.js";
4
+ import { BoardItemResizeMode } from "../model/items/definition.js";
5
+ import { BoardRegistry } from "../model/items/registry.js";
6
+ import "../model/index.js";
7
+ //#region src/board/editor/selection-transform.d.ts
8
+ type BoardTransformControl = {
9
+ kind: "resize";
10
+ handle: ResizeHandle;
11
+ } | {
12
+ kind: "rotate";
13
+ };
14
+ declare const RESIZE_CURSORS: readonly ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
15
+ declare function resizeCursorForHandle(handle: ResizeHandle, rotation: number): (typeof RESIZE_CURSORS)[number];
16
+ type BoardSelectionTransform = {
17
+ frame: BoardFrame;
18
+ resizeMode: BoardItemResizeMode;
19
+ canRotate: boolean;
20
+ };
21
+ declare function resolveSelectionTransform(registry: BoardRegistry, items: BoardSceneItem[], bounds: Rect | null): BoardSelectionTransform | null;
22
+ declare function selectionTransformControlAt(transform: BoardSelectionTransform | null, point: WorldPoint, zoom: number, pointerType: string): BoardTransformControl | null;
23
+ //#endregion
24
+ export { BoardSelectionTransform, BoardTransformControl, resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt };
@@ -0,0 +1,81 @@
1
+ import { CORNER_RESIZE_HANDLES, frameCornerRotationHandleAt, frameEdgeHandleAt, frameHandlePosition, rotationHandlePosition } from "../model/geometry.js";
2
+ //#region src/board/editor/selection-transform.ts
3
+ const HANDLE_ANGLE = {
4
+ e: 0,
5
+ w: 0,
6
+ se: 45,
7
+ nw: 45,
8
+ n: 90,
9
+ s: 90,
10
+ ne: 135,
11
+ sw: 135
12
+ };
13
+ const RESIZE_CURSORS = [
14
+ "ew-resize",
15
+ "nwse-resize",
16
+ "ns-resize",
17
+ "nesw-resize"
18
+ ];
19
+ function resizeCursorForHandle(handle, rotation) {
20
+ const angle = HANDLE_ANGLE[handle] + rotation;
21
+ const bucket = (Math.round(angle / 45) % 4 + 4) % 4;
22
+ return RESIZE_CURSORS[bucket] ?? "ew-resize";
23
+ }
24
+ function resolveSelectionTransform(registry, items, bounds) {
25
+ if (!bounds || items.length === 0) return null;
26
+ const frame = items.length === 1 ? items[0]?.frame : {
27
+ ...bounds,
28
+ rotation: 0
29
+ };
30
+ if (!frame) return null;
31
+ let canResize = true;
32
+ let canRotate = true;
33
+ let singleResizeMode = "none";
34
+ for (let index = 0; index < items.length; index += 1) {
35
+ const item = items[index];
36
+ if (!item) continue;
37
+ const capabilities = registry.capabilities(item);
38
+ const resizeMode = registry.resizeMode(item);
39
+ if (index === 0) singleResizeMode = resizeMode;
40
+ if (item.locked || resizeMode === "none") canResize = false;
41
+ if (item.locked || !capabilities.canRotate) canRotate = false;
42
+ }
43
+ return {
44
+ frame,
45
+ resizeMode: canResize ? items.length === 1 ? singleResizeMode : "uniform" : "none",
46
+ canRotate
47
+ };
48
+ }
49
+ function controlHitRadius(pointerType) {
50
+ if (pointerType === "touch") return 20;
51
+ if (pointerType === "pen") return 12;
52
+ return 8;
53
+ }
54
+ function selectionTransformControlAt(transform, point, zoom, pointerType) {
55
+ if (!transform) return null;
56
+ const screenRadius = controlHitRadius(pointerType);
57
+ const radius = screenRadius / Math.max(zoom, 1e-4);
58
+ if (transform.resizeMode !== "none") for (const handle of CORNER_RESIZE_HANDLES) {
59
+ const position = frameHandlePosition(transform.frame, handle);
60
+ if (Math.hypot(position.x - point.x, position.y - point.y) <= radius) return {
61
+ kind: "resize",
62
+ handle
63
+ };
64
+ }
65
+ if (transform.canRotate) {
66
+ if (pointerType === "touch" || transform.resizeMode === "none") {
67
+ const position = rotationHandlePosition(transform.frame, zoom);
68
+ if (Math.hypot(position.x - point.x, position.y - point.y) <= radius) return { kind: "rotate" };
69
+ } else if (frameCornerRotationHandleAt(transform.frame, point, zoom, screenRadius)) return { kind: "rotate" };
70
+ }
71
+ if (transform.resizeMode === "free") {
72
+ const handle = frameEdgeHandleAt(transform.frame, point, zoom, screenRadius);
73
+ if (handle) return {
74
+ kind: "resize",
75
+ handle
76
+ };
77
+ }
78
+ return null;
79
+ }
80
+ //#endregion
81
+ export { resizeCursorForHandle, resolveSelectionTransform, selectionTransformControlAt };
@@ -0,0 +1,33 @@
1
+ import { BoardItem } from "../../protocol/dist/board-model.js";
2
+ import { BoardFrame } from "../../protocol/dist/board-layout.js";
3
+ import { Rect } from "../model/geometry.js";
4
+ import { BoardSceneItem } from "../model/scene.js";
5
+ import "../model/index.js";
6
+ import { AlignMode, DistributeAxis } from "./align.js";
7
+ import { BoardSelectionTransform } from "./selection-transform.js";
8
+ import { EditorContext } from "./context.js";
9
+ //#region src/board/editor/selection.d.ts
10
+ declare function createSelectionModule(ctx: EditorContext): {
11
+ items: () => BoardSceneItem[];
12
+ bounds: () => Rect | null;
13
+ transform: () => BoardSelectionTransform | null;
14
+ set: (ids: string[]) => void;
15
+ toggle: (id: string) => void;
16
+ clear: () => void;
17
+ selectAll: () => void;
18
+ framesFor: (ids: Iterable<string>) => Map<string, BoardFrame>;
19
+ placed: (frames: ReadonlyMap<string, BoardFrame>) => Map<string, BoardItem>;
20
+ movable: () => string[];
21
+ nudge: (dx: number, dy: number, large: boolean) => void;
22
+ align: (mode: AlignMode) => void;
23
+ distribute: (axis: DistributeAxis) => void;
24
+ toggleLock: () => void;
25
+ setColor: (color: string) => void;
26
+ setStyle: (style: Partial<BoardItem["style"]>) => void;
27
+ setProps: (type: string, props: Record<string, unknown>) => void;
28
+ bringToFront: () => void;
29
+ sendToBack: () => void;
30
+ };
31
+ type SelectionModule = ReturnType<typeof createSelectionModule>;
32
+ //#endregion
33
+ export { SelectionModule, createSelectionModule };
@@ -0,0 +1,196 @@
1
+ import { selectionBounds } from "../model/geometry.js";
2
+ import { sceneItemToItem } from "../model/scene.js";
3
+ import { alignFrames, distributeFrames } from "./align.js";
4
+ import { resolveSelectionTransform } from "./selection-transform.js";
5
+ import { memo } from "./state.js";
6
+ //#region src/board/editor/selection.ts
7
+ const NUDGE_STEP = 1;
8
+ const NUDGE_STEP_LARGE = 10;
9
+ function createSelectionModule(ctx) {
10
+ const { state, registry } = ctx;
11
+ const items = memo(() => [state.scene, state.selection], (scene, selection) => {
12
+ const result = [];
13
+ for (const id of selection) {
14
+ const item = scene.get(id);
15
+ if (item) result.push(item);
16
+ }
17
+ return result;
18
+ });
19
+ const bounds = memo(() => [items()], (selected) => selectionBounds(selected.map((item) => item.frame)));
20
+ const transform = memo(() => [items(), bounds()], (selected, rect) => resolveSelectionTransform(registry, selected, rect));
21
+ function set(ids) {
22
+ state.selection = ids;
23
+ }
24
+ function toggle(id) {
25
+ const { selection } = state;
26
+ state.selection = selection.includes(id) ? selection.filter((selected) => selected !== id) : [...selection, id];
27
+ }
28
+ function framesFor(ids) {
29
+ const frames = /* @__PURE__ */ new Map();
30
+ for (const id of ids) {
31
+ const frame = state.scene.get(id)?.frame;
32
+ if (frame) frames.set(id, { ...frame });
33
+ }
34
+ return frames;
35
+ }
36
+ function placed(frames) {
37
+ const changes = /* @__PURE__ */ new Map();
38
+ for (const [id, frame] of frames) {
39
+ const item = state.scene.get(id);
40
+ if (item) changes.set(id, sceneItemToItem({
41
+ ...item,
42
+ frame
43
+ }, state.scene.layout));
44
+ }
45
+ return changes;
46
+ }
47
+ function movable() {
48
+ return ctx.query.rootsOf(ctx.query.unlockedIds(state.selection));
49
+ }
50
+ function nudge(dx, dy, large) {
51
+ const ids = movable();
52
+ if (ids.length === 0) return;
53
+ const step = large ? NUDGE_STEP_LARGE : NUDGE_STEP;
54
+ const frames = /* @__PURE__ */ new Map();
55
+ for (const [id, frame] of framesFor(ids)) frames.set(id, {
56
+ ...frame,
57
+ x: frame.x + dx * step,
58
+ y: frame.y + dy * step
59
+ });
60
+ ctx.document.commitItems(placed(frames));
61
+ }
62
+ function align(mode) {
63
+ const ids = movable();
64
+ if (ids.length < 2) return;
65
+ ctx.document.commitItems(placed(alignFrames(framesFor(ids), mode)));
66
+ }
67
+ function distribute(axis) {
68
+ const ids = movable();
69
+ if (ids.length < 3) return;
70
+ ctx.document.commitItems(placed(distributeFrames(framesFor(ids), axis)));
71
+ }
72
+ function edit(update, ids = ctx.query.unlockedIds(state.selection)) {
73
+ const changes = /* @__PURE__ */ new Map();
74
+ for (const id of ids) {
75
+ const item = ctx.document.displayItem(id);
76
+ const next = item ? update(item) : null;
77
+ if (next && next !== item) changes.set(id, next);
78
+ }
79
+ ctx.document.commitItems(changes);
80
+ }
81
+ function toggleLock() {
82
+ if (state.selection.length === 0) return;
83
+ const lock = items().some((item) => !item.locked);
84
+ edit((item) => {
85
+ if (lock) return {
86
+ ...item,
87
+ locked: true
88
+ };
89
+ const { locked: _locked, ...unlocked } = item;
90
+ return unlocked;
91
+ }, state.selection);
92
+ }
93
+ function setColor(color) {
94
+ edit((item) => {
95
+ switch (item.type) {
96
+ case "text":
97
+ case "effect": return {
98
+ ...item,
99
+ style: {
100
+ ...item.style,
101
+ fill: color
102
+ }
103
+ };
104
+ case "shape":
105
+ case "frame": return {
106
+ ...item,
107
+ style: {
108
+ ...item.style,
109
+ stroke: color,
110
+ ...item.style.fill ? { fill: color } : {}
111
+ }
112
+ };
113
+ case "draw":
114
+ case "arrow": return {
115
+ ...item,
116
+ style: {
117
+ ...item.style,
118
+ stroke: color
119
+ }
120
+ };
121
+ default: return null;
122
+ }
123
+ });
124
+ }
125
+ function setStyle(style) {
126
+ edit((item) => ({
127
+ ...item,
128
+ style: {
129
+ ...item.style,
130
+ ...style
131
+ }
132
+ }));
133
+ }
134
+ function setProps(type, props) {
135
+ edit((item) => {
136
+ if (item.type !== type) return null;
137
+ const next = {
138
+ ...item.props,
139
+ ...props
140
+ };
141
+ const valid = registry.validateProps(type, next);
142
+ if (!valid.ok) throw new Error(valid.message);
143
+ return {
144
+ ...item,
145
+ props: next
146
+ };
147
+ });
148
+ }
149
+ function restack(front) {
150
+ const { scene } = state;
151
+ const ids = ctx.query.rootsOf(state.selection);
152
+ if (ids.length === 0) return;
153
+ const changes = /* @__PURE__ */ new Map();
154
+ const byParent = /* @__PURE__ */ new Map();
155
+ for (const id of ids) {
156
+ const parent = scene.get(id)?.parent;
157
+ byParent.set(parent, [...byParent.get(parent) ?? [], id]);
158
+ }
159
+ for (const [parent, members] of byParent) {
160
+ const siblings = scene.children(parent).map((id) => ctx.document.displayItem(id)?.z ?? 0);
161
+ let z = front ? Math.max(0, ...siblings) + 1 : Math.min(0, ...siblings) - members.length;
162
+ const ordered = members.sort((a, b) => scene.indexOf(a) - scene.indexOf(b));
163
+ for (const id of ordered) {
164
+ const item = ctx.document.displayItem(id);
165
+ if (item) changes.set(id, {
166
+ ...item,
167
+ z: z++
168
+ });
169
+ }
170
+ }
171
+ ctx.document.commitItems(changes);
172
+ }
173
+ return {
174
+ items,
175
+ bounds,
176
+ transform,
177
+ set,
178
+ toggle,
179
+ clear: () => set([]),
180
+ selectAll: () => set(state.scene.children(void 0).slice()),
181
+ framesFor,
182
+ placed,
183
+ movable,
184
+ nudge,
185
+ align,
186
+ distribute,
187
+ toggleLock,
188
+ setColor,
189
+ setStyle,
190
+ setProps,
191
+ bringToFront: () => restack(true),
192
+ sendToBack: () => restack(false)
193
+ };
194
+ }
195
+ //#endregion
196
+ export { createSelectionModule };
@@ -0,0 +1,33 @@
1
+ import { Rect } from "../model/geometry.js";
2
+ import "../model/index.js";
3
+ //#region src/board/editor/snapping.d.ts
4
+ type SnapAxis = "x" | "y";
5
+ /** A rendered alignment guide: a segment along one axis at a world position. */
6
+ type SnapGuide = {
7
+ axis: SnapAxis;
8
+ /** World position of the guide line (x for vertical, y for horizontal). */
9
+ at: number;
10
+ /** Extent of the guide segment (the other axis), inclusive. */
11
+ from: number;
12
+ to: number;
13
+ };
14
+ type SnapResult = {
15
+ /** Correction to apply to the moving selection (0 when no snap on that axis). */
16
+ dx: number;
17
+ dy: number;
18
+ guides: SnapGuide[];
19
+ };
20
+ type SnapOptions = {
21
+ /** World-space snap threshold (typically screenPx / zoom). */
22
+ threshold: number;
23
+ /** Snap to a regular grid of this size (0 disables grid snapping). */
24
+ gridSize?: number;
25
+ };
26
+ /**
27
+ * Compute the snap for a moving selection bounds against candidate target
28
+ * bounds. Each axis is solved independently: the nearest matching edge/center
29
+ * within `threshold` wins. Grid lines are included as targets when enabled.
30
+ */
31
+ declare function computeSnap(moving: Rect, targets: Rect[], options: SnapOptions): SnapResult;
32
+ //#endregion
33
+ export { SnapAxis, SnapGuide, SnapOptions, SnapResult, computeSnap };