@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.
- package/README.md +100 -103
- package/dist/board/editor/align.d.ts +11 -0
- package/dist/board/editor/align.js +72 -0
- package/dist/board/editor/animation.d.ts +14 -0
- package/dist/board/editor/animation.js +172 -0
- package/dist/board/editor/camera.d.ts +32 -0
- package/dist/board/editor/camera.js +137 -0
- package/dist/board/editor/clipboard.d.ts +41 -0
- package/dist/board/editor/clipboard.js +122 -0
- package/dist/board/editor/connection-ports.d.ts +19 -0
- package/dist/board/editor/connection-ports.js +85 -0
- package/dist/board/editor/context.d.ts +111 -0
- package/dist/board/editor/context.js +30 -0
- package/dist/board/editor/copy.d.ts +17 -0
- package/dist/board/editor/copy.js +116 -0
- package/dist/board/editor/creation.d.ts +47 -0
- package/dist/board/editor/creation.js +224 -0
- package/dist/board/editor/deletion.d.ts +9 -0
- package/dist/board/editor/deletion.js +90 -0
- package/dist/board/editor/document-edits.d.ts +22 -0
- package/dist/board/editor/document-edits.js +193 -0
- package/dist/board/editor/document.d.ts +35 -0
- package/dist/board/editor/document.js +255 -0
- package/dist/board/editor/editor.d.ts +210 -0
- package/dist/board/editor/editor.js +429 -0
- package/dist/board/editor/index.d.ts +18 -0
- package/dist/board/editor/index.js +12 -0
- package/dist/board/editor/interaction.d.ts +115 -0
- package/dist/board/editor/interaction.js +584 -0
- package/dist/board/editor/query.d.ts +27 -0
- package/dist/board/editor/query.js +201 -0
- package/dist/board/editor/remote.d.ts +11 -0
- package/dist/board/editor/remote.js +55 -0
- package/dist/board/editor/selection-transform.d.ts +24 -0
- package/dist/board/editor/selection-transform.js +81 -0
- package/dist/board/editor/selection.d.ts +33 -0
- package/dist/board/editor/selection.js +196 -0
- package/dist/board/editor/snapping.d.ts +33 -0
- package/dist/board/editor/snapping.js +75 -0
- package/dist/board/editor/snapshots.d.ts +27 -0
- package/dist/board/editor/snapshots.js +132 -0
- package/dist/board/editor/spatial.d.ts +41 -0
- package/dist/board/editor/spatial.js +191 -0
- package/dist/board/editor/state.d.ts +12 -0
- package/dist/board/editor/state.js +49 -0
- package/dist/board/editor/tool.d.ts +10 -0
- package/dist/board/editor/tool.js +19 -0
- package/dist/board/editor/wheel.d.ts +5 -0
- package/dist/board/editor/wheel.js +17 -0
- package/dist/board/export/index.d.ts +5 -4
- package/dist/board/export/index.js +4 -3
- package/dist/board/export/node/index.d.ts +55 -0
- package/dist/board/{headless → export/node}/index.js +12 -12
- package/dist/board/export/node/sketch.d.ts +19 -0
- package/dist/board/{headless → export/node}/sketch.js +6 -6
- package/dist/board/export/scene.d.ts +6 -4
- package/dist/board/export/scene.js +4 -3
- package/dist/board/{animation.d.ts → model/animation.d.ts} +4 -4
- package/dist/board/{animation.js → model/animation.js} +3 -3
- package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
- package/dist/board/{core → model}/arrow-geometry.js +2 -2
- package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
- package/dist/board/{core → model}/draw-geometry.js +1 -1
- package/dist/board/model/draw-input.d.ts +14 -0
- package/dist/board/model/draw-input.js +28 -0
- package/dist/board/{core → model}/export-assets.d.ts +1 -1
- package/dist/board/{core → model}/export-assets.js +1 -1
- package/dist/board/{core → model}/export-plan.d.ts +2 -2
- package/dist/board/{core → model}/export-plan.js +2 -2
- package/dist/board/{core → model}/file-preview.d.ts +1 -1
- package/dist/board/{core → model}/file-preview.js +1 -1
- package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
- package/dist/board/{core → model}/file-snapshot.js +1 -1
- package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
- package/dist/board/{geometry.js → model/geometry.js} +1 -1
- package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
- package/dist/board/{image-key.js → model/image-key.js} +1 -1
- package/dist/board/model/index.d.ts +35 -0
- package/dist/board/{index.js → model/index.js} +26 -22
- package/dist/board/model/items/builtin.d.ts +5 -0
- package/dist/board/model/items/builtin.js +119 -0
- package/dist/board/model/items/create.d.ts +59 -0
- package/dist/board/model/items/create.js +271 -0
- package/dist/board/model/items/definition.d.ts +35 -0
- package/dist/board/model/items/definition.js +18 -0
- package/dist/board/model/items/id.d.ts +5 -0
- package/dist/board/model/items/id.js +11 -0
- package/dist/board/model/items/index.d.ts +6 -0
- package/dist/board/model/items/index.js +6 -0
- package/dist/board/model/items/registry.d.ts +35 -0
- package/dist/board/model/items/registry.js +84 -0
- package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +2 -2
- package/dist/board/{media-playback.js → model/media-playback.js} +1 -1
- package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
- package/dist/board/{media.js → model/media.js} +1 -1
- package/dist/board/{core → model}/palette.d.ts +1 -1
- package/dist/board/{core → model}/palette.js +1 -1
- package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
- package/dist/board/{presets.js → model/presets.js} +1 -1
- package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
- package/dist/board/{replay.js → model/replay.js} +2 -2
- package/dist/board/{core → model}/scene.d.ts +1 -1
- package/dist/board/{core → model}/scene.js +1 -1
- package/dist/board/model/shape-types.d.ts +8 -0
- package/dist/board/model/shape-types.js +8 -0
- package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
- package/dist/board/{task.js → model/task.js} +3 -3
- package/dist/board/{core → model}/text-metrics.d.ts +1 -1
- package/dist/board/{core → model}/text-metrics.js +1 -1
- package/dist/board/{core → model}/tool-styles.d.ts +1 -1
- package/dist/board/{core → model}/tool-styles.js +1 -1
- package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
- package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
- package/dist/board/render/board-background.d.ts +1 -1
- package/dist/board/render/clip.d.ts +1 -1
- package/dist/board/render/index.d.ts +2 -2
- package/dist/board/render/index.js +2 -2
- package/dist/board/render/media-interaction.d.ts +2 -2
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.d.ts +1 -1
- package/dist/board/render/palette.js +1 -1
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +11 -6
- package/dist/board/render/renderers/board-renderer-registry.js +15 -15
- package/dist/board/render/renderers/draw-card-renderer.js +1 -1
- package/dist/board/render/renderers/far-plate.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +2 -2
- package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +1 -1
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
- package/dist/board/render/stroke.d.ts +1 -1
- package/dist/board/render/stroke.js +1 -1
- package/dist/board/render/text-measurement.js +1 -1
- package/dist/board/replica/index.d.ts +70 -0
- package/dist/board/replica/index.js +276 -0
- package/dist/board/stage/assets.d.ts +41 -0
- package/dist/board/stage/assets.js +350 -0
- package/dist/board/stage/index.d.ts +8 -0
- package/dist/board/stage/index.js +8 -0
- package/dist/board/stage/lru.d.ts +32 -0
- package/dist/board/stage/lru.js +29 -0
- package/dist/board/stage/pointer.d.ts +8 -0
- package/dist/board/stage/pointer.js +33 -0
- package/dist/board/stage/scene.d.ts +85 -0
- package/dist/board/stage/scene.js +512 -0
- package/dist/board/stage/stage.d.ts +66 -0
- package/dist/board/stage/stage.js +568 -0
- package/dist/board/stage/texture-pool.d.ts +24 -0
- package/dist/board/stage/texture-pool.js +154 -0
- package/dist/board/stage/views.d.ts +38 -0
- package/dist/board/stage/views.js +93 -0
- package/dist/chunks/environment.d.ts +25 -1
- package/dist/chunks/http.d.ts +99 -66
- package/dist/chunks/http.js +214 -207
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +20 -4
- package/dist/media.d.ts +1 -10
- package/dist/media.js +16 -27
- package/dist/protocol/dist/board-patch.js +1 -1
- package/dist/protocol/dist/board.d.ts +28 -2
- package/dist/protocol/dist/board.js +3 -1
- package/dist/protocol/dist/core/media.d.ts +5 -0
- package/dist/protocol/dist/core/media.js +39 -0
- package/dist/protocol/dist/index.d.ts +5 -5
- package/dist/types.d.ts +1 -1
- package/package.json +26 -14
- package/dist/board/core/shape-definition.d.ts +0 -23
- package/dist/board/core/shape-definition.js +0 -45
- package/dist/board/core/shape-types.d.ts +0 -38
- package/dist/board/core/shape-types.js +0 -22
- package/dist/board/headless/index.d.ts +0 -55
- package/dist/board/headless/sketch.d.ts +0 -19
- 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 };
|