@fulate/tools 1.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 +356 -0
- package/dist/editor/interaction-controller.d.ts +339 -0
- package/dist/editor/overlay-transform.d.ts +6 -0
- package/dist/editor-layer.d.ts +18 -0
- package/dist/history/index.d.ts +88 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +4976 -0
- package/dist/line/index.d.ts +29 -0
- package/dist/rule/index.d.ts +35 -0
- package/dist/select/align.d.ts +3 -0
- package/dist/select/arrange.d.ts +11 -0
- package/dist/select/checkElement.d.ts +2 -0
- package/dist/select/clipboard.d.ts +40 -0
- package/dist/select/delete.d.ts +3 -0
- package/dist/select/grouping.d.ts +5 -0
- package/dist/select/index.d.ts +119 -0
- package/dist/select/paint.d.ts +4 -0
- package/dist/select/port-index.d.ts +42 -0
- package/dist/select/projection.d.ts +139 -0
- package/dist/select/snap.d.ts +97 -0
- package/dist/select/transform.d.ts +40 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4976 @@
|
|
|
1
|
+
import { Element, ElementProperties, ElementTransform, Layer, isFormControlTarget, projectConnectionPortPoint, resolveOrigin, resolveReferenceTransform } from "@fulate/core";
|
|
2
|
+
import { BaseLine, ContentEditingCapabilityToken, DEFAULT_LINE_TREE_BRANCH_STYLE, Group, Line, LineTree, PointEditingCapabilityToken, SelectionBehaviorCapabilityToken, SelectionBehaviorMode, SelectionDiveMode, SelectionSnapMode, SnapSourceCapabilityToken, TransformInteractionCapabilityToken, addLineTreeBranchAtTail, applyLineTreeTopologyDelta, connectionPortAccepts, createLineTreeTopology, createLineTreeTopologyDelta, getEditorCapability, getEditorFrameFlush, getEndpointRelationIndex, hasConnectionPortCapacity, isEndpointRelationSource, moveLineTreePoint, remapLineTreeRelations, resolveConnectEndpointChange, resolveConnectionPortChanges, resolveDisconnectEndpointChanges, workspaceFocusInsetsKey } from "@fulate/ui";
|
|
3
|
+
import { Bound, Intersection, makeBoundingBoxFromRects, qrDecompose, transformPoint } from "@fulate/share";
|
|
4
|
+
import { deserializeElement, deserializeElement as deserializeElement$1, exportToFile, importFromFile, parseFileData, restoreScene, serializeScene, serializeSceneToJSON } from "@fulate/import";
|
|
5
|
+
import { isNull, isString, isUndefined } from "lodash-es";
|
|
6
|
+
import RBush from "rbush";
|
|
7
|
+
//#region packages/tools/src/select/checkElement.ts
|
|
8
|
+
function isSelectable(element, excludes) {
|
|
9
|
+
return !(excludes.includes(element) || element.selectable === false || element.inject("selectable") === false || element.isLayerNode());
|
|
10
|
+
}
|
|
11
|
+
/** Resolve a raw hit to the direct owner of the active selection scope. */
|
|
12
|
+
function resolveSelectionOwner(element, scopeRootKey) {
|
|
13
|
+
if (scopeRootKey !== void 0) {
|
|
14
|
+
let directOwner = element;
|
|
15
|
+
for (let parent = element.parent; parent instanceof Element; parent = parent.parent) {
|
|
16
|
+
if (parent.key === scopeRootKey) return directOwner;
|
|
17
|
+
directOwner = parent;
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let contentChild = element;
|
|
22
|
+
let outerGroup = element;
|
|
23
|
+
for (let parent = element.parent; parent instanceof Element; parent = parent.parent) {
|
|
24
|
+
if (parent.type === "workspace" || parent.isLayerNode()) return contentChild;
|
|
25
|
+
contentChild = parent;
|
|
26
|
+
if (parent.type === "group") outerGroup = parent;
|
|
27
|
+
}
|
|
28
|
+
return outerGroup;
|
|
29
|
+
}
|
|
30
|
+
function checkElement(element, excludes = [], scopeRootKey) {
|
|
31
|
+
const resolved = resolveSelectionOwner(element, scopeRootKey);
|
|
32
|
+
if (!resolved || !isSelectable(resolved, excludes)) return;
|
|
33
|
+
return getEditorCapability(resolved, SelectionBehaviorCapabilityToken) ? resolved : void 0;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region packages/tools/src/select/projection.ts
|
|
37
|
+
var ROTATE_CURSOR = `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2v6h-6"></path><path d="M21 13a9 9 0 1 1-3-7.7L21 8"></path></svg>') 9 9, crosshair`;
|
|
38
|
+
var RESIZE_CURSORS = {
|
|
39
|
+
tl: "nwse-resize",
|
|
40
|
+
tr: "nesw-resize",
|
|
41
|
+
br: "nwse-resize",
|
|
42
|
+
bl: "nesw-resize",
|
|
43
|
+
mt: "ns-resize",
|
|
44
|
+
mr: "ew-resize",
|
|
45
|
+
mb: "ns-resize",
|
|
46
|
+
ml: "ew-resize"
|
|
47
|
+
};
|
|
48
|
+
var RESIZE_TARGETS = {
|
|
49
|
+
tl: {
|
|
50
|
+
kind: "resize",
|
|
51
|
+
handle: "tl"
|
|
52
|
+
},
|
|
53
|
+
tr: {
|
|
54
|
+
kind: "resize",
|
|
55
|
+
handle: "tr"
|
|
56
|
+
},
|
|
57
|
+
br: {
|
|
58
|
+
kind: "resize",
|
|
59
|
+
handle: "br"
|
|
60
|
+
},
|
|
61
|
+
bl: {
|
|
62
|
+
kind: "resize",
|
|
63
|
+
handle: "bl"
|
|
64
|
+
},
|
|
65
|
+
mt: {
|
|
66
|
+
kind: "resize",
|
|
67
|
+
handle: "mt"
|
|
68
|
+
},
|
|
69
|
+
mr: {
|
|
70
|
+
kind: "resize",
|
|
71
|
+
handle: "mr"
|
|
72
|
+
},
|
|
73
|
+
mb: {
|
|
74
|
+
kind: "resize",
|
|
75
|
+
handle: "mb"
|
|
76
|
+
},
|
|
77
|
+
ml: {
|
|
78
|
+
kind: "resize",
|
|
79
|
+
handle: "ml"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ROTATE_TARGET = { kind: "rotate" };
|
|
83
|
+
var PIVOT_TARGET = { kind: "pivot" };
|
|
84
|
+
function samePointEditTarget(left, right) {
|
|
85
|
+
return left.kind === right.kind && (left.kind === "anchor" ? right.kind === "anchor" && left.anchorId === right.anchorId : left.kind === "segment" ? right.kind === "segment" && left.segmentId === right.segmentId : right.kind === "handle" && left.segmentId === right.segmentId && left.role === right.role);
|
|
86
|
+
}
|
|
87
|
+
var ROTATED_RESIZE_CURSORS = [
|
|
88
|
+
"ns-resize",
|
|
89
|
+
"nesw-resize",
|
|
90
|
+
"ew-resize",
|
|
91
|
+
"nwse-resize",
|
|
92
|
+
"ns-resize",
|
|
93
|
+
"nesw-resize",
|
|
94
|
+
"ew-resize",
|
|
95
|
+
"nwse-resize"
|
|
96
|
+
];
|
|
97
|
+
function rotatedResizeCursor(cursor, angle) {
|
|
98
|
+
const index = ROTATED_RESIZE_CURSORS.indexOf(cursor);
|
|
99
|
+
if (index === -1) return cursor;
|
|
100
|
+
return ROTATED_RESIZE_CURSORS[(index + Math.round((angle % 360 + 360) % 360 / 45) % 8) % 8];
|
|
101
|
+
}
|
|
102
|
+
function createSelectionFrameProjection({ left, top, width, height, angle }) {
|
|
103
|
+
const worldCenter = {
|
|
104
|
+
x: left + width / 2,
|
|
105
|
+
y: top + height / 2
|
|
106
|
+
};
|
|
107
|
+
const matrix = new DOMMatrix().translate(worldCenter.x, worldCenter.y).rotate(0, 0, angle).translate(-width / 2, -height / 2);
|
|
108
|
+
return {
|
|
109
|
+
left,
|
|
110
|
+
top,
|
|
111
|
+
width,
|
|
112
|
+
height,
|
|
113
|
+
angle,
|
|
114
|
+
matrix,
|
|
115
|
+
inverseMatrix: matrix.inverse(),
|
|
116
|
+
outline: [
|
|
117
|
+
transformPoint(matrix, {
|
|
118
|
+
x: 0,
|
|
119
|
+
y: 0
|
|
120
|
+
}),
|
|
121
|
+
transformPoint(matrix, {
|
|
122
|
+
x: width,
|
|
123
|
+
y: 0
|
|
124
|
+
}),
|
|
125
|
+
transformPoint(matrix, {
|
|
126
|
+
x: width,
|
|
127
|
+
y: height
|
|
128
|
+
}),
|
|
129
|
+
transformPoint(matrix, {
|
|
130
|
+
x: 0,
|
|
131
|
+
y: height
|
|
132
|
+
})
|
|
133
|
+
],
|
|
134
|
+
worldCenter
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** @internal Build a command frame from the current owner bounds. */
|
|
138
|
+
function createWorldVisualBoundsSelectionFrame(owners, fallback) {
|
|
139
|
+
if (owners.length === 0) return fallback ?? null;
|
|
140
|
+
if (owners.length === 1) {
|
|
141
|
+
if (owners[0].transform?.getSelectionFrameBounds) {
|
|
142
|
+
if (fallback) return fallback;
|
|
143
|
+
return frameFromSingleOwner(owners[0]);
|
|
144
|
+
}
|
|
145
|
+
const bounds = owners[0].element.transform.getWorldVisualBoundsView();
|
|
146
|
+
if ((bounds.width === 0 || bounds.height === 0) && fallback) return fallback;
|
|
147
|
+
return createSelectionFrameProjection({
|
|
148
|
+
left: bounds.left,
|
|
149
|
+
top: bounds.top,
|
|
150
|
+
width: bounds.width,
|
|
151
|
+
height: bounds.height,
|
|
152
|
+
angle: 0
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
let left = Infinity;
|
|
156
|
+
let top = Infinity;
|
|
157
|
+
let right = -Infinity;
|
|
158
|
+
let bottom = -Infinity;
|
|
159
|
+
for (const { element } of owners) {
|
|
160
|
+
const bounds = element.transform.getWorldVisualBoundsView();
|
|
161
|
+
if (bounds.left < left) left = bounds.left;
|
|
162
|
+
if (bounds.top < top) top = bounds.top;
|
|
163
|
+
if (bounds.left + bounds.width > right) right = bounds.left + bounds.width;
|
|
164
|
+
if (bounds.top + bounds.height > bottom) bottom = bounds.top + bounds.height;
|
|
165
|
+
}
|
|
166
|
+
const width = right - left;
|
|
167
|
+
const height = bottom - top;
|
|
168
|
+
if ((width === 0 || height === 0) && fallback) return fallback;
|
|
169
|
+
return createSelectionFrameProjection({
|
|
170
|
+
left,
|
|
171
|
+
top,
|
|
172
|
+
width,
|
|
173
|
+
height,
|
|
174
|
+
angle: 0
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function createSelectionRotationPivotSnapMarkers(frame) {
|
|
178
|
+
const columns = [
|
|
179
|
+
{
|
|
180
|
+
horizontal: "left",
|
|
181
|
+
x: 0
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
horizontal: "center",
|
|
185
|
+
x: frame.width / 2
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
horizontal: "right",
|
|
189
|
+
x: frame.width
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
const rows = [
|
|
193
|
+
{
|
|
194
|
+
vertical: "top",
|
|
195
|
+
y: 0
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
vertical: "center",
|
|
199
|
+
y: frame.height / 2
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
vertical: "bottom",
|
|
203
|
+
y: frame.height
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
return columns.flatMap(({ horizontal, x }) => rows.map(({ vertical, y }) => ({
|
|
207
|
+
...transformPoint(frame.matrix, {
|
|
208
|
+
x,
|
|
209
|
+
y
|
|
210
|
+
}),
|
|
211
|
+
horizontal,
|
|
212
|
+
vertical
|
|
213
|
+
})));
|
|
214
|
+
}
|
|
215
|
+
function frameFromSingleOwner(owner) {
|
|
216
|
+
const geometry = owner.element.transform;
|
|
217
|
+
const bounds = owner.transform?.getSelectionFrameBounds?.() ?? geometry.getLocalVisualBoundsView() ?? geometry.getLayoutReferenceBoundsView();
|
|
218
|
+
const matrix = geometry.getWorldMatrixView();
|
|
219
|
+
const decomposition = qrDecompose(matrix);
|
|
220
|
+
const angleRadians = decomposition.angle * Math.PI / 180;
|
|
221
|
+
const skewScaleX = matrix.c * Math.cos(angleRadians) + matrix.d * Math.sin(angleRadians);
|
|
222
|
+
const width = Math.abs(decomposition.scaleX * bounds.width) + Math.abs(skewScaleX * bounds.height);
|
|
223
|
+
const height = Math.abs(decomposition.scaleY * bounds.height);
|
|
224
|
+
const worldCenter = transformPoint(matrix, {
|
|
225
|
+
x: bounds.left + bounds.width / 2,
|
|
226
|
+
y: bounds.top + bounds.height / 2
|
|
227
|
+
});
|
|
228
|
+
const angle = (decomposition.angle % 360 + 360) % 360;
|
|
229
|
+
return createSelectionFrameProjection({
|
|
230
|
+
left: worldCenter.x - width / 2,
|
|
231
|
+
top: worldCenter.y - height / 2,
|
|
232
|
+
width,
|
|
233
|
+
height,
|
|
234
|
+
angle
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function frameFromOwners(owners) {
|
|
238
|
+
if (owners.length === 0) return null;
|
|
239
|
+
if (owners.length === 1) return frameFromSingleOwner(owners[0]);
|
|
240
|
+
return createWorldVisualBoundsSelectionFrame(owners);
|
|
241
|
+
}
|
|
242
|
+
function resizeControls(frame, policy) {
|
|
243
|
+
const x = policy.resize === "x" || policy.resize === "both";
|
|
244
|
+
const y = policy.resize === "y" || policy.resize === "both";
|
|
245
|
+
const controls = [];
|
|
246
|
+
const addPoint = (handle, local) => {
|
|
247
|
+
controls.push({
|
|
248
|
+
world: transformPoint(frame.matrix, local),
|
|
249
|
+
cursor: rotatedResizeCursor(RESIZE_CURSORS[handle], frame.angle),
|
|
250
|
+
shape: "rect",
|
|
251
|
+
hit: { kind: "point" },
|
|
252
|
+
target: RESIZE_TARGETS[handle]
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
if (x && y && frame.width !== 0 && frame.height !== 0) {
|
|
256
|
+
addPoint("tl", {
|
|
257
|
+
x: 0,
|
|
258
|
+
y: 0
|
|
259
|
+
});
|
|
260
|
+
addPoint("tr", {
|
|
261
|
+
x: frame.width,
|
|
262
|
+
y: 0
|
|
263
|
+
});
|
|
264
|
+
addPoint("br", {
|
|
265
|
+
x: frame.width,
|
|
266
|
+
y: frame.height
|
|
267
|
+
});
|
|
268
|
+
addPoint("bl", {
|
|
269
|
+
x: 0,
|
|
270
|
+
y: frame.height
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (policy.rotate) controls.push(...rotationControls(frame));
|
|
274
|
+
const addEdge = (handle, from, to) => {
|
|
275
|
+
controls.push({
|
|
276
|
+
world: {
|
|
277
|
+
x: (from.x + to.x) / 2,
|
|
278
|
+
y: (from.y + to.y) / 2
|
|
279
|
+
},
|
|
280
|
+
cursor: rotatedResizeCursor(RESIZE_CURSORS[handle], frame.angle),
|
|
281
|
+
shape: null,
|
|
282
|
+
hit: {
|
|
283
|
+
kind: "segment",
|
|
284
|
+
from,
|
|
285
|
+
to
|
|
286
|
+
},
|
|
287
|
+
target: RESIZE_TARGETS[handle]
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
if (y && frame.height !== 0) {
|
|
291
|
+
addEdge("mt", frame.outline[0], frame.outline[1]);
|
|
292
|
+
addEdge("mb", frame.outline[3], frame.outline[2]);
|
|
293
|
+
}
|
|
294
|
+
if (x && frame.width !== 0) {
|
|
295
|
+
addEdge("mr", frame.outline[1], frame.outline[2]);
|
|
296
|
+
addEdge("ml", frame.outline[0], frame.outline[3]);
|
|
297
|
+
}
|
|
298
|
+
return controls;
|
|
299
|
+
}
|
|
300
|
+
function rotationControls(frame) {
|
|
301
|
+
return frame.outline.map((world) => ({
|
|
302
|
+
world,
|
|
303
|
+
cursor: ROTATE_CURSOR,
|
|
304
|
+
shape: null,
|
|
305
|
+
hit: { kind: "rotation-ring" },
|
|
306
|
+
target: ROTATE_TARGET
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
function pointControls(owner, topology, activePointTarget) {
|
|
310
|
+
const matrix = owner.element.transform.getWorldMatrixView();
|
|
311
|
+
return topology.pointControls.map((control) => {
|
|
312
|
+
return {
|
|
313
|
+
world: transformPoint(matrix, control.local),
|
|
314
|
+
cursor: control.cursor,
|
|
315
|
+
shape: control.shape,
|
|
316
|
+
...control.guideLocal ? { guideFrom: transformPoint(matrix, control.guideLocal) } : {},
|
|
317
|
+
active: control.target.deletable && activePointTarget?.ownerKey === control.target.ownerKey && samePointEditTarget(activePointTarget.target, control.target.target),
|
|
318
|
+
hit: { kind: "point" },
|
|
319
|
+
target: control.target
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function createSelectionProjectionMembership(owners, policy) {
|
|
324
|
+
return {
|
|
325
|
+
selectionKeys: owners.map(({ element }) => element.key),
|
|
326
|
+
selectedOutlineOwners: owners,
|
|
327
|
+
bodyEnabled: policy.move,
|
|
328
|
+
frameVisible: owners.every(({ frameVisible }) => frameVisible)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function createSelectionProjectionTopology(owners, isCurrent = () => true) {
|
|
332
|
+
if (owners.length !== 1 || !owners[0].pointEnabled || !owners[0].pointScope) return {
|
|
333
|
+
pointControls: [],
|
|
334
|
+
pointScopeOutline: null,
|
|
335
|
+
pointScopeValid: true
|
|
336
|
+
};
|
|
337
|
+
const owner = owners[0];
|
|
338
|
+
const capability = getEditorCapability(owner.element, PointEditingCapabilityToken);
|
|
339
|
+
if (!capability || !isCurrent(owner) || !capability.hasPointScope(owner.pointScope) || !isCurrent(owner)) return {
|
|
340
|
+
pointControls: [],
|
|
341
|
+
pointScopeOutline: null,
|
|
342
|
+
pointScopeValid: false
|
|
343
|
+
};
|
|
344
|
+
const descriptors = capability.getPointControls(owner.pointScope);
|
|
345
|
+
if (!isCurrent(owner)) return {
|
|
346
|
+
pointControls: [],
|
|
347
|
+
pointScopeOutline: null,
|
|
348
|
+
pointScopeValid: false
|
|
349
|
+
};
|
|
350
|
+
const pointScopeOutline = capability.getPointScopeOutline(owner.pointScope);
|
|
351
|
+
if (!isCurrent(owner)) return {
|
|
352
|
+
pointControls: [],
|
|
353
|
+
pointScopeOutline: null,
|
|
354
|
+
pointScopeValid: false
|
|
355
|
+
};
|
|
356
|
+
return {
|
|
357
|
+
pointControls: descriptors.map((descriptor) => ({
|
|
358
|
+
local: descriptor.local,
|
|
359
|
+
...descriptor.kind === "handle" ? { guideLocal: descriptor.anchorLocal } : {},
|
|
360
|
+
cursor: descriptor.kind === "segment" ? "copy" : descriptor.kind === "handle" ? "move" : descriptor.move === "locked" ? "not-allowed" : "move",
|
|
361
|
+
shape: descriptor.kind === "segment" ? "diamond" : descriptor.kind === "handle" ? "handle" : "circle",
|
|
362
|
+
target: {
|
|
363
|
+
kind: "point",
|
|
364
|
+
ownerKey: owner.element.key,
|
|
365
|
+
target: descriptor.target,
|
|
366
|
+
move: descriptor.kind === "segment" || descriptor.kind === "handle" ? "free" : descriptor.move,
|
|
367
|
+
deletable: descriptor.kind === "anchor" && descriptor.deletable,
|
|
368
|
+
role: descriptor.kind === "segment" ? "segment" : descriptor.kind === "handle" ? "handle" : descriptor.role,
|
|
369
|
+
insertT: descriptor.kind === "segment" ? descriptor.insertT : void 0
|
|
370
|
+
}
|
|
371
|
+
})),
|
|
372
|
+
pointScopeOutline,
|
|
373
|
+
pointScopeValid: true
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function createProjectionOwner(element, frameVisible = true, pointScope = null, pointEnabled = false) {
|
|
377
|
+
return {
|
|
378
|
+
element,
|
|
379
|
+
frameVisible,
|
|
380
|
+
transform: getEditorCapability(element, TransformInteractionCapabilityToken),
|
|
381
|
+
pointEnabled,
|
|
382
|
+
pointScope,
|
|
383
|
+
snap: getEditorCapability(element, SnapSourceCapabilityToken)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function buildSelectionOverlayProjection({ owners, membership, topology, frame: frameOverride, policy, pivot, showPivot, pivotSnapMarkers = [], activePointTarget = null, marquee }) {
|
|
387
|
+
const frame = frameOverride ?? frameFromOwners(owners);
|
|
388
|
+
const controls = [];
|
|
389
|
+
if (owners.length === 1) controls.push(...pointControls(owners[0], topology, activePointTarget));
|
|
390
|
+
const rotationPivotWorld = frame && owners.length === 1 && pivot.kind === "custom-owner-local" && pivot.ownerKey === owners[0].element.key ? transformPoint(owners[0].element.transform.getWorldMatrixView(), pivot.local) : frame?.worldCenter ?? {
|
|
391
|
+
x: 0,
|
|
392
|
+
y: 0
|
|
393
|
+
};
|
|
394
|
+
if (frame) {
|
|
395
|
+
if (showPivot && owners.length === 1) controls.push({
|
|
396
|
+
world: rotationPivotWorld,
|
|
397
|
+
cursor: "crosshair",
|
|
398
|
+
shape: "circle",
|
|
399
|
+
hit: { kind: "point" },
|
|
400
|
+
target: PIVOT_TARGET
|
|
401
|
+
});
|
|
402
|
+
controls.push(...resizeControls(frame, policy));
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
selectionKeys: membership.selectionKeys,
|
|
406
|
+
frame,
|
|
407
|
+
controls,
|
|
408
|
+
selectedOutlineOwners: membership.selectedOutlineOwners,
|
|
409
|
+
pointScopeOutline: topology.pointScopeOutline,
|
|
410
|
+
bodyEnabled: membership.bodyEnabled,
|
|
411
|
+
frameVisible: membership.frameVisible,
|
|
412
|
+
rotationPivotWorld,
|
|
413
|
+
rotationPivotSnapMarkers: pivotSnapMarkers,
|
|
414
|
+
marquee
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function hitSelectionOverlay(projection, point, controlRadius, edgePadding, rotatePadding) {
|
|
418
|
+
for (const control of projection.controls) {
|
|
419
|
+
const distance = Math.hypot(point.x - control.world.x, point.y - control.world.y);
|
|
420
|
+
if (control.hit.kind === "point" ? distance <= controlRadius : control.hit.kind === "segment" ? Intersection.pointToLineSegmentDistance(point, control.hit.from, control.hit.to) <= edgePadding : distance > controlRadius && distance <= controlRadius + rotatePadding && !(projection.frame && Intersection.isPointInPolygon(point, projection.frame.outline))) return {
|
|
421
|
+
target: control.target,
|
|
422
|
+
cursor: control.cursor
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const frame = projection.frame;
|
|
426
|
+
if (projection.bodyEnabled && frame && Intersection.isPointInPolygon(point, frame.outline)) return {
|
|
427
|
+
target: { kind: "body" },
|
|
428
|
+
cursor: "move"
|
|
429
|
+
};
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region packages/tools/src/select/transform.ts
|
|
434
|
+
function resolveSelectionFrameTransform(frame, worldDelta, kind) {
|
|
435
|
+
const worldCenter = transformPoint(worldDelta, frame.worldCenter);
|
|
436
|
+
let width = frame.width;
|
|
437
|
+
let height = frame.height;
|
|
438
|
+
let angle = frame.angle;
|
|
439
|
+
if (kind === "resize") {
|
|
440
|
+
const matrix = worldDelta.multiply(frame.matrix);
|
|
441
|
+
width *= Math.hypot(matrix.a, matrix.b);
|
|
442
|
+
height *= Math.hypot(matrix.c, matrix.d);
|
|
443
|
+
} else if (kind === "rotate") angle = (angle + Math.atan2(worldDelta.b, worldDelta.a) * 180 / Math.PI + 360) % 360;
|
|
444
|
+
return createSelectionFrameProjection({
|
|
445
|
+
left: worldCenter.x - width / 2,
|
|
446
|
+
top: worldCenter.y - height / 2,
|
|
447
|
+
width,
|
|
448
|
+
height,
|
|
449
|
+
angle
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function resolveResizeWorldDelta(frame, handle, worldPoint, proportional) {
|
|
453
|
+
const local = transformPoint(frame.inverseMatrix, worldPoint);
|
|
454
|
+
let fixedX = frame.width / 2;
|
|
455
|
+
let fixedY = frame.height / 2;
|
|
456
|
+
let scaleX = 1;
|
|
457
|
+
let scaleY = 1;
|
|
458
|
+
if (handle.includes("r")) {
|
|
459
|
+
fixedX = 0;
|
|
460
|
+
scaleX = local.x / frame.width;
|
|
461
|
+
} else if (handle.includes("l")) {
|
|
462
|
+
fixedX = frame.width;
|
|
463
|
+
scaleX = (frame.width - local.x) / frame.width;
|
|
464
|
+
}
|
|
465
|
+
if (handle.includes("b")) {
|
|
466
|
+
fixedY = 0;
|
|
467
|
+
scaleY = local.y / frame.height;
|
|
468
|
+
} else if (handle.includes("t")) {
|
|
469
|
+
fixedY = frame.height;
|
|
470
|
+
scaleY = (frame.height - local.y) / frame.height;
|
|
471
|
+
}
|
|
472
|
+
if (handle === "mr" || handle === "ml") scaleY = 1;
|
|
473
|
+
if (handle === "mt" || handle === "mb") scaleX = 1;
|
|
474
|
+
if (proportional && ![
|
|
475
|
+
"mt",
|
|
476
|
+
"mr",
|
|
477
|
+
"mb",
|
|
478
|
+
"ml"
|
|
479
|
+
].includes(handle)) {
|
|
480
|
+
const ratio = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
|
481
|
+
scaleX = Math.sign(scaleX) * ratio;
|
|
482
|
+
scaleY = Math.sign(scaleY) * ratio;
|
|
483
|
+
}
|
|
484
|
+
const localDelta = new DOMMatrix().translate(fixedX, fixedY).scale(scaleX, scaleY).translate(-fixedX, -fixedY);
|
|
485
|
+
return frame.matrix.multiply(localDelta).multiply(frame.inverseMatrix);
|
|
486
|
+
}
|
|
487
|
+
function resizeAnchorPoint(frame, anchor) {
|
|
488
|
+
return {
|
|
489
|
+
x: anchor.endsWith("left") ? 0 : anchor.endsWith("right") ? frame.width : frame.width / 2,
|
|
490
|
+
y: anchor.startsWith("top") ? 0 : anchor.startsWith("bottom") ? frame.height : frame.height / 2
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/** @internal 把 target frame 尺寸转为一个固定锚点的 world 线性变换。 */
|
|
494
|
+
function resolveResizeWorldDeltaForSize(frame, width, height, anchor = "top-left") {
|
|
495
|
+
const { x, y } = resizeAnchorPoint(frame, anchor);
|
|
496
|
+
const scaleX = frame.width === 0 ? 1 : width / frame.width;
|
|
497
|
+
const scaleY = frame.height === 0 ? 1 : height / frame.height;
|
|
498
|
+
const localDelta = new DOMMatrix().translate(x, y).scale(scaleX, scaleY).translate(-x, -y);
|
|
499
|
+
return frame.matrix.multiply(localDelta).multiply(frame.inverseMatrix);
|
|
500
|
+
}
|
|
501
|
+
/** @internal 围绕 selection frame 中心沿 frame 自身坐标轴镜像。 */
|
|
502
|
+
function resolveFlipWorldDelta(frame, direction) {
|
|
503
|
+
const centerX = frame.width / 2;
|
|
504
|
+
const centerY = frame.height / 2;
|
|
505
|
+
const localDelta = new DOMMatrix().translate(centerX, centerY).scale(direction === "horizontal" ? -1 : 1, direction === "vertical" ? -1 : 1).translate(-centerX, -centerY);
|
|
506
|
+
return frame.matrix.multiply(localDelta).multiply(frame.inverseMatrix);
|
|
507
|
+
}
|
|
508
|
+
function resolveRotationTransform({ frameAngle, pivot, unwrappedDelta, snapAngle, snapThreshold }) {
|
|
509
|
+
let target = frameAngle + unwrappedDelta;
|
|
510
|
+
if (snapAngle > 0) {
|
|
511
|
+
const lower = Math.floor(target / snapAngle) * snapAngle;
|
|
512
|
+
const upper = Math.ceil(target / snapAngle) * snapAngle;
|
|
513
|
+
if (Math.abs(target - lower) <= snapThreshold) target = lower;
|
|
514
|
+
else if (Math.abs(target - upper) <= snapThreshold) target = upper;
|
|
515
|
+
}
|
|
516
|
+
const rotationDelta = target - frameAngle;
|
|
517
|
+
return {
|
|
518
|
+
worldDelta: new DOMMatrix().translate(pivot.x, pivot.y).rotate(0, 0, rotationDelta).translate(-pivot.x, -pivot.y),
|
|
519
|
+
unwrappedDelta: rotationDelta
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region packages/tools/src/editor/interaction-controller.ts
|
|
524
|
+
var editorControllerKey = Symbol();
|
|
525
|
+
var NO_PORT_SNAP$1 = {
|
|
526
|
+
snap: null,
|
|
527
|
+
state: null
|
|
528
|
+
};
|
|
529
|
+
var NO_ELEMENTS = [];
|
|
530
|
+
var NO_POINTS = [];
|
|
531
|
+
function isBlockedPortProbe(probe) {
|
|
532
|
+
return probe?.state === "incompatible" || probe?.state === "forbidden";
|
|
533
|
+
}
|
|
534
|
+
function resolvePortProbeCursor(probe) {
|
|
535
|
+
return isBlockedPortProbe(probe) ? "not-allowed" : "crosshair";
|
|
536
|
+
}
|
|
537
|
+
var NO_PROJECTION_OWNERS = [];
|
|
538
|
+
function pointIntent(event) {
|
|
539
|
+
return {
|
|
540
|
+
point: {
|
|
541
|
+
x: event.detail.x,
|
|
542
|
+
y: event.detail.y
|
|
543
|
+
},
|
|
544
|
+
shiftKey: event.detail.shiftKey === true
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function matchesMarquee(element, rect, requireContainment) {
|
|
548
|
+
const topLeft = {
|
|
549
|
+
x: rect.left,
|
|
550
|
+
y: rect.top
|
|
551
|
+
};
|
|
552
|
+
const bottomRight = {
|
|
553
|
+
x: rect.left + rect.width,
|
|
554
|
+
y: rect.top + rect.height
|
|
555
|
+
};
|
|
556
|
+
const contained = Intersection.isContainedInRect(element.transform.getWorldVisualBoundsView(), topLeft, bottomRight);
|
|
557
|
+
if (contained || requireContainment) return contained;
|
|
558
|
+
return Intersection.intersectPolygonRectangle(element.transform.getWorldVisualPointsView(), topLeft, bottomRight).status === "Intersection" || element.transform.hasPointHint(topLeft) || element.transform.hasPointHint(bottomRight);
|
|
559
|
+
}
|
|
560
|
+
function sameTarget(left, right) {
|
|
561
|
+
if (left?.kind !== right?.kind) return false;
|
|
562
|
+
if (!left || !right) return true;
|
|
563
|
+
if (left.kind === "resize" && right.kind === "resize") return left.handle === right.handle;
|
|
564
|
+
if (left.kind === "point" && right.kind === "point") return left.ownerKey === right.ownerKey && samePointEditTarget(left.target, right.target);
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
function samePointScope(left, right) {
|
|
568
|
+
return left.kind === right.kind && (left.kind === "owner" || right.kind === "subpath" && left.subpathId === right.subpathId);
|
|
569
|
+
}
|
|
570
|
+
var TransformGesture = class {
|
|
571
|
+
lastIntent;
|
|
572
|
+
previousRotationAngle;
|
|
573
|
+
unwrappedRotationDelta = 0;
|
|
574
|
+
constructor(controller, kind, start, initialIntent, frame, records, snapPoints, movableLineEndpoints, snapExcludes, rotationPivotWorld, history, resizeHandle) {
|
|
575
|
+
this.controller = controller;
|
|
576
|
+
this.kind = kind;
|
|
577
|
+
this.start = start;
|
|
578
|
+
this.frame = frame;
|
|
579
|
+
this.records = records;
|
|
580
|
+
this.snapPoints = snapPoints;
|
|
581
|
+
this.movableLineEndpoints = movableLineEndpoints;
|
|
582
|
+
this.snapExcludes = snapExcludes;
|
|
583
|
+
this.rotationPivotWorld = rotationPivotWorld;
|
|
584
|
+
this.history = history;
|
|
585
|
+
this.resizeHandle = resizeHandle;
|
|
586
|
+
this.lastIntent = initialIntent;
|
|
587
|
+
if (kind === "rotate") this.previousRotationAngle = Math.atan2(initialIntent.point.y - rotationPivotWorld.y, initialIntent.point.x - rotationPivotWorld.x) * 180 / Math.PI;
|
|
588
|
+
}
|
|
589
|
+
consumeRotationDelta(point) {
|
|
590
|
+
const angle = Math.atan2(point.y - this.rotationPivotWorld.y, point.x - this.rotationPivotWorld.x) * 180 / Math.PI;
|
|
591
|
+
const previous = this.previousRotationAngle;
|
|
592
|
+
this.unwrappedRotationDelta += (angle - previous + 540) % 360 - 180;
|
|
593
|
+
this.previousRotationAngle = angle;
|
|
594
|
+
return this.unwrappedRotationDelta;
|
|
595
|
+
}
|
|
596
|
+
probeForbiddenPort(point) {
|
|
597
|
+
return this.controller.root.enablePorts && this.controller.select.connectionGesture !== false && this.kind === "move" && this.controller.snapTool?.probeForbiddenPortMove(this.movableLineEndpoints, point.x - this.start.x, point.y - this.start.y, this.snapExcludes) === true;
|
|
598
|
+
}
|
|
599
|
+
update(intent) {
|
|
600
|
+
if (this.lastIntent.point.x === intent.point.x && this.lastIntent.point.y === intent.point.y && this.lastIntent.shiftKey === intent.shiftKey) return;
|
|
601
|
+
this.lastIntent = intent;
|
|
602
|
+
this.controller.consumeTransform(this, intent);
|
|
603
|
+
}
|
|
604
|
+
ownsCurrentContext() {
|
|
605
|
+
return this.records.every(({ owner, parent }) => owner.activeRoot === this.controller.root && owner.parent === parent);
|
|
606
|
+
}
|
|
607
|
+
finishInvalidated() {
|
|
608
|
+
try {
|
|
609
|
+
this.controller.publishConfiguredChange(this.history?.finish(this.controller.selection));
|
|
610
|
+
} finally {
|
|
611
|
+
this.controller.snapTool?.stop();
|
|
612
|
+
this.controller.finishPointerGesture(this);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
finish(intent) {
|
|
616
|
+
try {
|
|
617
|
+
if (intent) this.update(intent);
|
|
618
|
+
this.controller.root.frameRuntime.drain();
|
|
619
|
+
this.controller.publishConfiguredChange(this.history?.finish(this.controller.selection));
|
|
620
|
+
} finally {
|
|
621
|
+
this.controller.snapTool?.stop();
|
|
622
|
+
this.controller.finishPointerGesture(this);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
var PointGesture = class {
|
|
627
|
+
latestIntent = null;
|
|
628
|
+
consumedIntent = null;
|
|
629
|
+
connectionTarget;
|
|
630
|
+
connectionTargetOwner;
|
|
631
|
+
connectionTargetParent;
|
|
632
|
+
constructor(controller, ownerKey, startParent, startTarget, insertT, role, connectionRole, snapExcludes, history, startPointerWorld, startTargetWorld) {
|
|
633
|
+
this.controller = controller;
|
|
634
|
+
this.ownerKey = ownerKey;
|
|
635
|
+
this.startParent = startParent;
|
|
636
|
+
this.startTarget = startTarget;
|
|
637
|
+
this.insertT = insertT;
|
|
638
|
+
this.role = role;
|
|
639
|
+
this.connectionRole = connectionRole;
|
|
640
|
+
this.snapExcludes = snapExcludes;
|
|
641
|
+
this.history = history;
|
|
642
|
+
this.startPointerWorld = startPointerWorld;
|
|
643
|
+
this.startTargetWorld = startTargetWorld;
|
|
644
|
+
this.currentTarget = startTarget;
|
|
645
|
+
}
|
|
646
|
+
currentTarget;
|
|
647
|
+
ownsCurrentContext() {
|
|
648
|
+
return this.controller.pointGestureOwnsCurrentContext(this);
|
|
649
|
+
}
|
|
650
|
+
get connectionSource() {
|
|
651
|
+
return this.connectionRole && this.currentTarget.kind === "anchor" ? {
|
|
652
|
+
elementKey: this.ownerKey,
|
|
653
|
+
anchorId: this.currentTarget.anchorId
|
|
654
|
+
} : void 0;
|
|
655
|
+
}
|
|
656
|
+
resolveWorldPoint(pointer) {
|
|
657
|
+
return {
|
|
658
|
+
x: this.startTargetWorld.x + pointer.x - this.startPointerWorld.x,
|
|
659
|
+
y: this.startTargetWorld.y + pointer.y - this.startPointerWorld.y
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
update(intent) {
|
|
663
|
+
if (!this.latestIntent && this.consumedIntent?.point.x === intent.point.x && this.consumedIntent.point.y === intent.point.y && this.consumedIntent.shiftKey === intent.shiftKey) return;
|
|
664
|
+
this.latestIntent = intent;
|
|
665
|
+
this.controller.frameFlush.registerGestureWork(this);
|
|
666
|
+
}
|
|
667
|
+
consumeLatestIntent() {
|
|
668
|
+
const intent = this.latestIntent;
|
|
669
|
+
this.latestIntent = null;
|
|
670
|
+
if (intent) {
|
|
671
|
+
const first = this.consumedIntent === null;
|
|
672
|
+
this.consumedIntent = intent;
|
|
673
|
+
this.controller.consumePoint(this, intent, first);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
finish(intent, cancelled = false) {
|
|
677
|
+
try {
|
|
678
|
+
if (cancelled && this.connectionRole) {
|
|
679
|
+
this.controller.publishConfiguredChange(this.history?.finish(this.controller.selection));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (intent && !cancelled) this.update(intent);
|
|
683
|
+
this.controller.root.frameRuntime.drain();
|
|
684
|
+
this.controller.finishPointConnectionGesture(this);
|
|
685
|
+
} finally {
|
|
686
|
+
this.controller.frameFlush.discardGestureWork(this);
|
|
687
|
+
this.controller.snapTool?.stop();
|
|
688
|
+
this.controller.finishPointerGesture(this);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
finishInvalidated() {
|
|
692
|
+
try {
|
|
693
|
+
this.controller.publishConfiguredChange(this.history?.finish(this.controller.selection));
|
|
694
|
+
} finally {
|
|
695
|
+
this.controller.frameFlush.discardGestureWork(this);
|
|
696
|
+
this.controller.snapTool?.stop();
|
|
697
|
+
this.controller.finishPointerGesture(this);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
function createToolsConnectionIntent(root, history, source, target) {
|
|
702
|
+
return {
|
|
703
|
+
source,
|
|
704
|
+
target,
|
|
705
|
+
connect() {
|
|
706
|
+
const change = resolveConnectEndpointChange(root, source, target);
|
|
707
|
+
if (!change) return;
|
|
708
|
+
const owner = root.getByKey(source.elementKey);
|
|
709
|
+
const selection = owner instanceof Element ? [owner] : [];
|
|
710
|
+
const scope = history.openScope(selection);
|
|
711
|
+
root.applyChangeBatch([change], [], scope);
|
|
712
|
+
root.frameRuntime.drain();
|
|
713
|
+
const current = root.getByKey(source.elementKey);
|
|
714
|
+
history.publishConfiguredChange(scope?.finish(current instanceof Element ? [current] : []));
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
var PivotGesture = class {
|
|
719
|
+
finalPoint;
|
|
720
|
+
constructor(controller, owner, startPointer, startPivot) {
|
|
721
|
+
this.controller = controller;
|
|
722
|
+
this.owner = owner;
|
|
723
|
+
this.startPointer = startPointer;
|
|
724
|
+
this.startPivot = startPivot;
|
|
725
|
+
this.finalPoint = startPivot;
|
|
726
|
+
}
|
|
727
|
+
update(intent) {
|
|
728
|
+
this.finalPoint = {
|
|
729
|
+
x: this.startPivot.x + intent.point.x - this.startPointer.x,
|
|
730
|
+
y: this.startPivot.y + intent.point.y - this.startPointer.y
|
|
731
|
+
};
|
|
732
|
+
this.controller.previewRotationPivot(this.owner, this.finalPoint);
|
|
733
|
+
}
|
|
734
|
+
finish(intent) {
|
|
735
|
+
try {
|
|
736
|
+
if (intent) this.update(intent);
|
|
737
|
+
const configuredChanged = this.controller.commitRotationPivot(this.owner, this.finalPoint);
|
|
738
|
+
this.controller.publishConfiguredChange(configuredChanged);
|
|
739
|
+
} finally {
|
|
740
|
+
this.controller.finishPointerGesture(this);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var SelectionPressGesture = class {
|
|
745
|
+
constructor(controller, start, candidate, contentTarget, directDragOwner) {
|
|
746
|
+
this.controller = controller;
|
|
747
|
+
this.start = start;
|
|
748
|
+
this.candidate = candidate;
|
|
749
|
+
this.contentTarget = contentTarget;
|
|
750
|
+
this.directDragOwner = directDragOwner;
|
|
751
|
+
}
|
|
752
|
+
update(intent) {
|
|
753
|
+
if (intent.point.x === this.start.point.x && intent.point.y === this.start.point.y) return;
|
|
754
|
+
this.controller.promoteSelectionPress(this)?.update(intent);
|
|
755
|
+
}
|
|
756
|
+
finish(intent, cancelled = false) {
|
|
757
|
+
if (cancelled || !intent) {
|
|
758
|
+
this.controller.finishPointerGesture(this);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (intent.point.x !== this.start.point.x || intent.point.y !== this.start.point.y) {
|
|
762
|
+
const transform = this.controller.promoteSelectionPress(this);
|
|
763
|
+
if (transform) transform.finish(intent);
|
|
764
|
+
else this.controller.finishPointerGesture(this);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
this.controller.finishSelectionPress(this);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
var MarqueeGesture = class {
|
|
771
|
+
current;
|
|
772
|
+
moved = false;
|
|
773
|
+
constructor(controller, start, candidate, additive, initialSelection) {
|
|
774
|
+
this.controller = controller;
|
|
775
|
+
this.start = start;
|
|
776
|
+
this.candidate = candidate;
|
|
777
|
+
this.additive = additive;
|
|
778
|
+
this.initialSelection = initialSelection;
|
|
779
|
+
this.current = start;
|
|
780
|
+
}
|
|
781
|
+
update(intent) {
|
|
782
|
+
this.current = intent.point;
|
|
783
|
+
this.moved ||= Math.hypot(this.current.x - this.start.x, this.current.y - this.start.y) > 4 / this.controller.root.viewport.scale;
|
|
784
|
+
if (this.moved) this.controller.setMarquee({
|
|
785
|
+
left: Math.min(this.start.x, this.current.x),
|
|
786
|
+
top: Math.min(this.start.y, this.current.y),
|
|
787
|
+
width: Math.abs(this.current.x - this.start.x),
|
|
788
|
+
height: Math.abs(this.current.y - this.start.y)
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
finish(intent, cancelled = false) {
|
|
792
|
+
if (cancelled || !intent) {
|
|
793
|
+
this.controller.setMarquee(null);
|
|
794
|
+
this.controller.finishSelectionGesture(this);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
this.update(intent);
|
|
798
|
+
let selection;
|
|
799
|
+
if (this.moved) {
|
|
800
|
+
const selected = new Set(this.additive ? this.initialSelection : []);
|
|
801
|
+
const marquee = this.controller.marquee;
|
|
802
|
+
for (const element of this.controller.root.queryArea(marquee, {
|
|
803
|
+
bounds: "visual",
|
|
804
|
+
order: "none"
|
|
805
|
+
})) {
|
|
806
|
+
if (!this.additive && !matchesMarquee(element, marquee, false)) continue;
|
|
807
|
+
const resolved = checkElement(element, this.controller.editorElements);
|
|
808
|
+
if (!resolved || selected.has(resolved)) continue;
|
|
809
|
+
if (this.additive && !matchesMarquee(resolved, marquee, true)) continue;
|
|
810
|
+
selected.add(resolved);
|
|
811
|
+
}
|
|
812
|
+
selection = [...selected];
|
|
813
|
+
} else if (this.additive && this.candidate) {
|
|
814
|
+
const selected = new Set(this.initialSelection);
|
|
815
|
+
if (selected.has(this.candidate)) selected.delete(this.candidate);
|
|
816
|
+
else selected.add(this.candidate);
|
|
817
|
+
selection = [...selected];
|
|
818
|
+
} else selection = this.candidate ? [this.candidate] : [];
|
|
819
|
+
this.controller.setMarquee(null);
|
|
820
|
+
this.controller.finishSelectionGesture(this, selection);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
var EditorInteractionController = class {
|
|
824
|
+
frameFlush;
|
|
825
|
+
cleanups = [];
|
|
826
|
+
selectionValue = NO_ELEMENTS;
|
|
827
|
+
owners = [];
|
|
828
|
+
policy = {
|
|
829
|
+
move: false,
|
|
830
|
+
resize: "none",
|
|
831
|
+
rotate: false,
|
|
832
|
+
rotationPivot: "center-only"
|
|
833
|
+
};
|
|
834
|
+
rotationPivotPreview = null;
|
|
835
|
+
selectionFrame = null;
|
|
836
|
+
preserveSelectionFrame = false;
|
|
837
|
+
projectionState = {
|
|
838
|
+
selectionKeys: [],
|
|
839
|
+
frame: null,
|
|
840
|
+
controls: [],
|
|
841
|
+
selectedOutlineOwners: [],
|
|
842
|
+
pointScopeOutline: null,
|
|
843
|
+
bodyEnabled: false,
|
|
844
|
+
frameVisible: false,
|
|
845
|
+
rotationPivotWorld: {
|
|
846
|
+
x: 0,
|
|
847
|
+
y: 0
|
|
848
|
+
},
|
|
849
|
+
rotationPivotSnapMarkers: [],
|
|
850
|
+
marquee: null
|
|
851
|
+
};
|
|
852
|
+
projectionOwners = null;
|
|
853
|
+
projectionMembership = null;
|
|
854
|
+
projectionTopology = null;
|
|
855
|
+
overlayDirty = true;
|
|
856
|
+
pendingSelectionUpdate = false;
|
|
857
|
+
pendingSelectionRepair = null;
|
|
858
|
+
rotationPivotDisplayOwner = null;
|
|
859
|
+
hoveredTarget = null;
|
|
860
|
+
hoveredContent = null;
|
|
861
|
+
controllerState = {
|
|
862
|
+
kind: "select",
|
|
863
|
+
interaction: {
|
|
864
|
+
kind: "idle",
|
|
865
|
+
gesture: null
|
|
866
|
+
},
|
|
867
|
+
selection: NO_ELEMENTS,
|
|
868
|
+
pivot: { kind: "selection-center" }
|
|
869
|
+
};
|
|
870
|
+
pasteTask = null;
|
|
871
|
+
pointerWorld = null;
|
|
872
|
+
marquee = null;
|
|
873
|
+
constructor(select, root, history) {
|
|
874
|
+
this.select = select;
|
|
875
|
+
this.root = root;
|
|
876
|
+
this.history = history;
|
|
877
|
+
this.frameFlush = getEditorFrameFlush(root);
|
|
878
|
+
this.frameFlush.setOverlayClient(this);
|
|
879
|
+
root.provide(editorControllerKey, this);
|
|
880
|
+
this.installInput();
|
|
881
|
+
this.cleanups.push(root.sceneRegistry.observeSceneChanges(this.handleSceneChange));
|
|
882
|
+
history.bindSelection({
|
|
883
|
+
finishInteraction: () => this.finishCurrentInteraction(),
|
|
884
|
+
getSelection: () => this.selection,
|
|
885
|
+
hasConfiguredChangeListener: () => this.select.hasEventListener("select:configured-change"),
|
|
886
|
+
publishConfiguredChange: () => this.notifySelect("select:configured-change"),
|
|
887
|
+
restoreSelection: (elements) => this.restoreSelectionForHistory(elements),
|
|
888
|
+
completeReplay: (type, elements, configuredChanged) => {
|
|
889
|
+
this.pendingSelectionRepair = null;
|
|
890
|
+
this.publishConfiguredChange(configuredChanged);
|
|
891
|
+
this.notifySelect("select:end");
|
|
892
|
+
this.notifySelect(type, { elements });
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
this.invalidateOverlay();
|
|
896
|
+
}
|
|
897
|
+
get selection() {
|
|
898
|
+
return this.selectionValue;
|
|
899
|
+
}
|
|
900
|
+
get projection() {
|
|
901
|
+
return this.projectionState;
|
|
902
|
+
}
|
|
903
|
+
publishConfiguredChange(changed) {
|
|
904
|
+
this.history.publishConfiguredChange(changed);
|
|
905
|
+
}
|
|
906
|
+
/** 把纯外部通知排到当前同步命令、回放或 frame completion 返回之后。 */
|
|
907
|
+
notifySelect(type, data) {
|
|
908
|
+
const select = this.select;
|
|
909
|
+
queueMicrotask(() => {
|
|
910
|
+
if (select.interactionController !== this) return;
|
|
911
|
+
select.dispatchEvent(type, data === void 0 ? void 0 : { data });
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
get contentHover() {
|
|
915
|
+
return this.hoveredContent;
|
|
916
|
+
}
|
|
917
|
+
get overlayTarget() {
|
|
918
|
+
return this.hoveredTarget;
|
|
919
|
+
}
|
|
920
|
+
get editorElements() {
|
|
921
|
+
const elements = [this.select];
|
|
922
|
+
const snap = this.snapTool;
|
|
923
|
+
if (snap) elements.push(snap);
|
|
924
|
+
const lineTool = this.select.lineTool;
|
|
925
|
+
if (lineTool) elements.push(lineTool);
|
|
926
|
+
return elements;
|
|
927
|
+
}
|
|
928
|
+
get snapTool() {
|
|
929
|
+
return this.select.snapTool;
|
|
930
|
+
}
|
|
931
|
+
get state() {
|
|
932
|
+
if (this.controllerState.kind !== "select") return this.controllerState;
|
|
933
|
+
return {
|
|
934
|
+
...this.controllerState,
|
|
935
|
+
selection: this.selection,
|
|
936
|
+
pivot: this.rotationPivotState
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/** @internal Captures the canonical selection and interaction identities. */
|
|
940
|
+
captureSelectionIdentity() {
|
|
941
|
+
return {
|
|
942
|
+
owners: this.owners,
|
|
943
|
+
interaction: this.controllerState
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/** @internal Makes one concrete clipboard Promise the latest paste intent. */
|
|
947
|
+
beginPaste(task) {
|
|
948
|
+
this.pasteTask = task;
|
|
949
|
+
}
|
|
950
|
+
/** @internal Tests whether one paste still owns the captured selection intent. */
|
|
951
|
+
ownsPasteSelection(identity, task) {
|
|
952
|
+
return this.owners === identity.owners && this.controllerState === identity.interaction && this.pasteTask === task;
|
|
953
|
+
}
|
|
954
|
+
/** @internal Releases the latest paste intent after its Promise settles. */
|
|
955
|
+
finishPaste(task) {
|
|
956
|
+
if (this.pasteTask === task) this.pasteTask = null;
|
|
957
|
+
}
|
|
958
|
+
get selectState() {
|
|
959
|
+
return this.controllerState.kind === "select" ? this.controllerState.interaction : null;
|
|
960
|
+
}
|
|
961
|
+
installSelectState(interaction) {
|
|
962
|
+
this.controllerState = {
|
|
963
|
+
kind: "select",
|
|
964
|
+
interaction,
|
|
965
|
+
selection: this.selection,
|
|
966
|
+
pivot: this.rotationPivotState
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
get activeGesture() {
|
|
970
|
+
const state = this.selectState;
|
|
971
|
+
return state && "gesture" in state ? state.gesture : null;
|
|
972
|
+
}
|
|
973
|
+
set activeGesture(gesture) {
|
|
974
|
+
const state = this.selectState;
|
|
975
|
+
if (gesture instanceof PointGesture && state?.kind === "point") {
|
|
976
|
+
const source = gesture.connectionSource;
|
|
977
|
+
this.installSelectState(source ? {
|
|
978
|
+
kind: "port",
|
|
979
|
+
ownerKey: state.ownerKey,
|
|
980
|
+
scope: state.scope,
|
|
981
|
+
gesture,
|
|
982
|
+
source,
|
|
983
|
+
activeTarget: state.activeTarget
|
|
984
|
+
} : {
|
|
985
|
+
...state,
|
|
986
|
+
gesture
|
|
987
|
+
});
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (gesture instanceof MarqueeGesture) {
|
|
991
|
+
this.installSelectState({
|
|
992
|
+
kind: "idle",
|
|
993
|
+
gesture
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (state?.kind === "point" && (gesture instanceof SelectionPressGesture || gesture instanceof TransformGesture)) {
|
|
998
|
+
this.installSelectState({
|
|
999
|
+
...state,
|
|
1000
|
+
gesture
|
|
1001
|
+
});
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (gesture === null && state?.kind === "point") {
|
|
1005
|
+
this.installSelectState({
|
|
1006
|
+
...state,
|
|
1007
|
+
gesture: null
|
|
1008
|
+
});
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (gesture === null && state?.kind === "port") {
|
|
1012
|
+
this.installSelectState({
|
|
1013
|
+
kind: "point",
|
|
1014
|
+
ownerKey: state.ownerKey,
|
|
1015
|
+
scope: state.scope,
|
|
1016
|
+
gesture: null,
|
|
1017
|
+
activeTarget: state.activeTarget
|
|
1018
|
+
});
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (this.selection.length === 0) this.installSelectState({
|
|
1022
|
+
kind: "idle",
|
|
1023
|
+
gesture: null
|
|
1024
|
+
});
|
|
1025
|
+
else this.installSelectState({
|
|
1026
|
+
kind: "transform",
|
|
1027
|
+
gesture
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
get activePointGesture() {
|
|
1031
|
+
const state = this.selectState;
|
|
1032
|
+
return (state?.kind === "point" || state?.kind === "port") && state.gesture instanceof PointGesture ? state.gesture : null;
|
|
1033
|
+
}
|
|
1034
|
+
get pointState() {
|
|
1035
|
+
const state = this.selectState;
|
|
1036
|
+
return state?.kind === "point" || state?.kind === "port" ? state : null;
|
|
1037
|
+
}
|
|
1038
|
+
set pointState(point) {
|
|
1039
|
+
const state = this.selectState;
|
|
1040
|
+
if (!point) {
|
|
1041
|
+
if (state?.kind === "point" || state?.kind === "port") this.installSelectState({
|
|
1042
|
+
kind: this.selection.length === 0 ? "idle" : "transform",
|
|
1043
|
+
gesture: null
|
|
1044
|
+
});
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.installSelectState({
|
|
1048
|
+
kind: "point",
|
|
1049
|
+
ownerKey: point.ownerKey,
|
|
1050
|
+
scope: point.scope,
|
|
1051
|
+
gesture: state?.kind === "point" || state?.kind === "port" ? state.gesture : null,
|
|
1052
|
+
activeTarget: state?.kind === "point" || state?.kind === "port" ? state.activeTarget : null
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
get activePointTarget() {
|
|
1056
|
+
const state = this.selectState;
|
|
1057
|
+
return state?.kind === "point" || state?.kind === "port" ? state.activeTarget : null;
|
|
1058
|
+
}
|
|
1059
|
+
set activePointTarget(target) {
|
|
1060
|
+
const state = this.selectState;
|
|
1061
|
+
if (state?.kind === "point" || state?.kind === "port") this.installSelectState({
|
|
1062
|
+
...state,
|
|
1063
|
+
activeTarget: target
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
get contentSession() {
|
|
1067
|
+
const state = this.selectState;
|
|
1068
|
+
return state?.kind === "content" ? state.session : null;
|
|
1069
|
+
}
|
|
1070
|
+
set contentSession(session) {
|
|
1071
|
+
const state = this.selectState;
|
|
1072
|
+
if (session) this.installSelectState({
|
|
1073
|
+
kind: "content",
|
|
1074
|
+
session,
|
|
1075
|
+
history: state?.kind === "content" ? state.history : void 0
|
|
1076
|
+
});
|
|
1077
|
+
else if (state?.kind === "content") this.installSelectState({
|
|
1078
|
+
kind: this.selection.length === 0 ? "idle" : "transform",
|
|
1079
|
+
gesture: null
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
get contentHistory() {
|
|
1083
|
+
const state = this.selectState;
|
|
1084
|
+
return state?.kind === "content" ? state.history : void 0;
|
|
1085
|
+
}
|
|
1086
|
+
set contentHistory(history) {
|
|
1087
|
+
const state = this.selectState;
|
|
1088
|
+
if (state?.kind === "content") this.installSelectState({
|
|
1089
|
+
...state,
|
|
1090
|
+
history
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
get lineSession() {
|
|
1094
|
+
const state = this.controllerState;
|
|
1095
|
+
return state.kind === "line-draw" && !("kind" in state.session) ? state.session : null;
|
|
1096
|
+
}
|
|
1097
|
+
set lineSession(session) {
|
|
1098
|
+
if (session) this.controllerState = {
|
|
1099
|
+
kind: "line-draw",
|
|
1100
|
+
session
|
|
1101
|
+
};
|
|
1102
|
+
else if (this.lineSession) this.installSelectState({
|
|
1103
|
+
kind: this.selection.length === 0 ? "idle" : "transform",
|
|
1104
|
+
gesture: null
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
get lineTreeSession() {
|
|
1108
|
+
const state = this.controllerState;
|
|
1109
|
+
return state.kind === "line-draw" && "kind" in state.session ? state.session : null;
|
|
1110
|
+
}
|
|
1111
|
+
set lineTreeSession(session) {
|
|
1112
|
+
if (session) this.controllerState = {
|
|
1113
|
+
kind: "line-draw",
|
|
1114
|
+
session
|
|
1115
|
+
};
|
|
1116
|
+
else if (this.lineTreeSession) this.installSelectState({
|
|
1117
|
+
kind: this.selection.length === 0 ? "idle" : "transform",
|
|
1118
|
+
gesture: null
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
get rotationPivotState() {
|
|
1122
|
+
if (this.owners.length === 1 && this.policy.rotate && this.policy.rotationPivot === "adjustable") {
|
|
1123
|
+
const owner = this.owners[0].element;
|
|
1124
|
+
const preview = this.rotationPivotPreview?.owner === owner ? this.rotationPivotPreview.local : null;
|
|
1125
|
+
if (!preview && owner.rotationPivotX === "center" && owner.rotationPivotY === "center") return { kind: "selection-center" };
|
|
1126
|
+
const bounds = owner.transform.getLocalVisualBoundsView() ?? owner.transform.getLayoutReferenceBoundsView();
|
|
1127
|
+
return {
|
|
1128
|
+
kind: "custom-owner-local",
|
|
1129
|
+
ownerKey: owner.key,
|
|
1130
|
+
local: preview ?? {
|
|
1131
|
+
x: bounds.left + (resolveOrigin(owner.rotationPivotX) + .5) * bounds.width,
|
|
1132
|
+
y: bounds.top + (resolveOrigin(owner.rotationPivotY) + .5) * bounds.height
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
return { kind: "selection-center" };
|
|
1137
|
+
}
|
|
1138
|
+
installInput() {
|
|
1139
|
+
const pointerdown = (event) => this.onPointerDown(event);
|
|
1140
|
+
const pointermove = (event) => this.onPointerMove(event);
|
|
1141
|
+
const pointerup = (event) => this.onPointerFinish(event);
|
|
1142
|
+
const pointercancel = (event) => this.onPointerFinish(event);
|
|
1143
|
+
const dblclick = (event) => this.onDoubleClick(event);
|
|
1144
|
+
this.root.addEventListener("pointerdown", pointerdown);
|
|
1145
|
+
this.root.addEventListener("pointermove", pointermove);
|
|
1146
|
+
this.root.addEventListener("pointerup", pointerup);
|
|
1147
|
+
this.root.addEventListener("pointercancel", pointercancel);
|
|
1148
|
+
this.root.addEventListener("dblclick", dblclick);
|
|
1149
|
+
this.cleanups.push(() => this.root.removeEventListener("pointerdown", pointerdown), () => this.root.removeEventListener("pointermove", pointermove), () => this.root.removeEventListener("pointerup", pointerup), () => this.root.removeEventListener("pointercancel", pointercancel), () => this.root.removeEventListener("dblclick", dblclick));
|
|
1150
|
+
const container = this.root.container;
|
|
1151
|
+
const hadTabIndex = container.hasAttribute("tabindex");
|
|
1152
|
+
const previousOutline = container.style.outline;
|
|
1153
|
+
if (!hadTabIndex) {
|
|
1154
|
+
container.tabIndex = 0;
|
|
1155
|
+
container.style.outline = "none";
|
|
1156
|
+
}
|
|
1157
|
+
const keydown = (event) => this.onKeyDown(event);
|
|
1158
|
+
container.addEventListener("keydown", keydown);
|
|
1159
|
+
this.cleanups.push(() => {
|
|
1160
|
+
container.removeEventListener("keydown", keydown);
|
|
1161
|
+
if (!hadTabIndex) {
|
|
1162
|
+
container.removeAttribute("tabindex");
|
|
1163
|
+
container.style.outline = previousOutline;
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
handleSceneChange = (node, affected) => {
|
|
1168
|
+
if (this.owners.some(({ element }) => element.activeRoot !== this.root)) {
|
|
1169
|
+
this.replaceSelection(this.selection);
|
|
1170
|
+
this.pendingSelectionRepair = this.owners;
|
|
1171
|
+
}
|
|
1172
|
+
const transformGesture = this.activeGesture;
|
|
1173
|
+
if (transformGesture instanceof TransformGesture && !transformGesture.ownsCurrentContext()) transformGesture.finishInvalidated();
|
|
1174
|
+
const pointGesture = this.activePointGesture;
|
|
1175
|
+
if (pointGesture && (!pointGesture.ownsCurrentContext() || pointGesture.connectionTarget !== void 0 && (this.root.getByKey(pointGesture.connectionTarget.elementKey) !== pointGesture.connectionTargetOwner || pointGesture.connectionTargetOwner.parent !== pointGesture.connectionTargetParent))) pointGesture.finishInvalidated();
|
|
1176
|
+
if (this.rotationPivotPreview && this.rotationPivotPreview.owner.activeRoot !== this.root) this.rotationPivotPreview = null;
|
|
1177
|
+
if (this.rotationPivotDisplayOwner && this.rotationPivotDisplayOwner.activeRoot !== this.root) this.rotationPivotDisplayOwner = null;
|
|
1178
|
+
if (this.contentSession && this.root.getByKey(this.contentSession.ownerKey) === void 0) this.finishContentEditing(true);
|
|
1179
|
+
if (this.lineTreeSession && (this.lineTreeSession.owner.activeRoot !== this.root || this.lineTreeSession.kind === "branch-draw" && !this.lineTreeSession.owner.getBranch(this.lineTreeSession.source.subpathId))) this.cancelLineDrawing();
|
|
1180
|
+
const rebuildSelectionFrame = !(this.activeGesture instanceof TransformGesture) && !this.activePointGesture && !this.preserveSelectionFrame;
|
|
1181
|
+
const updateSubscribed = this.select.hasEventListener("select:update");
|
|
1182
|
+
const affectsSelection = node ? this.sceneChangeAffectsSelection(node) : affected && affected.length > 0 ? affected.some((current) => this.sceneChangeAffectsSelection(current)) : true;
|
|
1183
|
+
const selectedGeometryDirty = affectsSelection && updateSubscribed && this.selectionGeometryIsDirty();
|
|
1184
|
+
if (updateSubscribed && selectedGeometryDirty) this.pendingSelectionUpdate = true;
|
|
1185
|
+
if (rebuildSelectionFrame && affectsSelection) {
|
|
1186
|
+
this.selectionFrame = null;
|
|
1187
|
+
this.invalidatePointTopology();
|
|
1188
|
+
}
|
|
1189
|
+
if (affectsSelection) this.invalidateOverlay();
|
|
1190
|
+
};
|
|
1191
|
+
sceneChangeAffectsSelection(node) {
|
|
1192
|
+
for (const { element } of this.owners) {
|
|
1193
|
+
for (let current = node; current; current = current.parent) if (current === element) return true;
|
|
1194
|
+
for (let current = element; current; current = current.parent) if (current === node) return true;
|
|
1195
|
+
}
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
selectionGeometryIsDirty() {
|
|
1199
|
+
for (const { element } of this.owners) {
|
|
1200
|
+
if (element.transform.needsCommit || element.transform.hasDirtyDescendant) return true;
|
|
1201
|
+
for (let current = element.parent; current instanceof Element; current = current.parent) if (current.transform.needsCommit) return true;
|
|
1202
|
+
}
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
invalidatePointTopology() {
|
|
1206
|
+
if (this.owners.some(({ pointEnabled }) => pointEnabled)) this.projectionTopology = null;
|
|
1207
|
+
}
|
|
1208
|
+
resolvePolicy(owners) {
|
|
1209
|
+
if (owners.length === 0 || owners.some(({ transform }) => !transform)) return {
|
|
1210
|
+
move: false,
|
|
1211
|
+
resize: "none",
|
|
1212
|
+
rotate: false,
|
|
1213
|
+
rotationPivot: "center-only"
|
|
1214
|
+
};
|
|
1215
|
+
const policies = owners.map(({ transform }) => transform.getTransformPolicy());
|
|
1216
|
+
const resizeX = policies.every(({ resize }) => resize === "x" || resize === "both");
|
|
1217
|
+
const resizeY = policies.every(({ resize }) => resize === "y" || resize === "both");
|
|
1218
|
+
return {
|
|
1219
|
+
move: policies.every(({ move }) => move),
|
|
1220
|
+
resize: resizeX && resizeY ? "both" : resizeX ? "x" : resizeY ? "y" : "none",
|
|
1221
|
+
rotate: policies.every(({ rotate }) => rotate),
|
|
1222
|
+
rotationPivot: policies.every(({ rotationPivot }) => rotationPivot === "adjustable") ? "adjustable" : "center-only"
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
setSelection(elements) {
|
|
1226
|
+
const previousOwners = this.owners;
|
|
1227
|
+
const gesture = this.activeGesture;
|
|
1228
|
+
if (gesture) {
|
|
1229
|
+
this.root.input.cancelSession();
|
|
1230
|
+
if (this.activeGesture === gesture) this.finishCurrentInteraction();
|
|
1231
|
+
} else if (this.contentSession || this.lineSession || this.lineTreeSession) this.finishCurrentInteraction();
|
|
1232
|
+
this.root.frameRuntime.drain();
|
|
1233
|
+
if (this.select.interactionController !== this || this.owners !== previousOwners) return;
|
|
1234
|
+
this.replaceSelection(elements);
|
|
1235
|
+
this.root.frameRuntime.drain();
|
|
1236
|
+
this.requestSelectionEnd();
|
|
1237
|
+
}
|
|
1238
|
+
pointProjectionOwnerIsCurrent = (owner) => this.owners.length === 1 && this.owners[0] === owner && this.pointState?.ownerKey === owner.element.key && this.pointState.scope === owner.pointScope && this.root.getByKey(owner.element.key) === owner.element;
|
|
1239
|
+
restoreSelectionForHistory(elements) {
|
|
1240
|
+
this.replaceSelection(elements);
|
|
1241
|
+
}
|
|
1242
|
+
replaceSelection(elements) {
|
|
1243
|
+
this.pendingSelectionRepair = null;
|
|
1244
|
+
if (this.selectState?.kind !== "content") this.installSelectState({
|
|
1245
|
+
kind: "idle",
|
|
1246
|
+
gesture: null
|
|
1247
|
+
});
|
|
1248
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1249
|
+
for (const element of elements) if (element.activeRoot === this.root) selected.add(element);
|
|
1250
|
+
const topLevel = [...selected].filter((element) => {
|
|
1251
|
+
for (let parent = element.parent; parent; parent = parent.parent) if (selected.has(parent)) return false;
|
|
1252
|
+
return true;
|
|
1253
|
+
});
|
|
1254
|
+
const owners = [];
|
|
1255
|
+
for (const element of topLevel) {
|
|
1256
|
+
const behavior = getEditorCapability(element, SelectionBehaviorCapabilityToken);
|
|
1257
|
+
if (!behavior) continue;
|
|
1258
|
+
const selectionBehavior = behavior.getSelectionBehavior();
|
|
1259
|
+
owners.push({
|
|
1260
|
+
...createProjectionOwner(element, selectionBehavior.mode === SelectionBehaviorMode.Standard),
|
|
1261
|
+
behavior: selectionBehavior
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
this.owners = owners;
|
|
1265
|
+
this.selectionValue = owners.length === 0 ? NO_ELEMENTS : owners.map(({ element }) => element);
|
|
1266
|
+
if (this.selectState?.kind !== "content") this.installSelectState({
|
|
1267
|
+
kind: owners.length === 0 ? "idle" : "transform",
|
|
1268
|
+
gesture: null
|
|
1269
|
+
});
|
|
1270
|
+
this.policy = this.resolvePolicy(owners);
|
|
1271
|
+
this.selectionFrame = null;
|
|
1272
|
+
if (owners.length !== 1 || owners[0].element !== this.rotationPivotDisplayOwner || !this.policy.rotate || this.policy.rotationPivot !== "adjustable") this.rotationPivotDisplayOwner = null;
|
|
1273
|
+
this.hoveredTarget = null;
|
|
1274
|
+
this.hoveredContent = null;
|
|
1275
|
+
this.invalidateOverlay();
|
|
1276
|
+
}
|
|
1277
|
+
requestSelectionEnd() {
|
|
1278
|
+
this.notifySelect("select:end");
|
|
1279
|
+
}
|
|
1280
|
+
pointContextIsCurrent(state, owner) {
|
|
1281
|
+
return this.pointState === state && this.root.getByKey(state.ownerKey) === owner && this.owners.length === 1 && this.owners[0].element === owner;
|
|
1282
|
+
}
|
|
1283
|
+
resolveCurrentPointContext(state = this.pointState) {
|
|
1284
|
+
if (!state) return null;
|
|
1285
|
+
const owner = this.root.getByKey(state.ownerKey);
|
|
1286
|
+
if (!(owner instanceof Element) || !this.pointContextIsCurrent(state, owner)) return null;
|
|
1287
|
+
const capability = getEditorCapability(owner, PointEditingCapabilityToken);
|
|
1288
|
+
if (!capability || !capability.hasPointScope(state.scope)) return null;
|
|
1289
|
+
return this.pointContextIsCurrent(state, owner) ? {
|
|
1290
|
+
state,
|
|
1291
|
+
owner,
|
|
1292
|
+
capability
|
|
1293
|
+
} : null;
|
|
1294
|
+
}
|
|
1295
|
+
resolvePointGestureContext(gesture) {
|
|
1296
|
+
const state = this.pointState;
|
|
1297
|
+
if (!state || this.activeGesture !== gesture || this.activePointGesture !== gesture || state.ownerKey !== gesture.ownerKey) return null;
|
|
1298
|
+
const context = this.resolveCurrentPointContext(state);
|
|
1299
|
+
return context?.owner.parent === gesture.startParent ? context : null;
|
|
1300
|
+
}
|
|
1301
|
+
pointGestureOwnsCurrentContext(gesture) {
|
|
1302
|
+
return this.resolvePointGestureContext(gesture) !== null;
|
|
1303
|
+
}
|
|
1304
|
+
setPointState(ownerKey, scope) {
|
|
1305
|
+
const owner = this.root.getByKey(ownerKey);
|
|
1306
|
+
if (!(owner instanceof Element)) return false;
|
|
1307
|
+
const selected = this.owners.length === 1 && this.owners[0].element === owner ? this.owners[0] : void 0;
|
|
1308
|
+
if (!selected) return false;
|
|
1309
|
+
const previousPointState = this.pointState;
|
|
1310
|
+
const capability = getEditorCapability(owner, PointEditingCapabilityToken);
|
|
1311
|
+
if (!capability || !capability.hasPointScope(scope)) return false;
|
|
1312
|
+
if (this.root.getByKey(ownerKey) !== owner || this.owners.length !== 1 || this.owners[0] !== selected || this.pointState !== previousPointState) return false;
|
|
1313
|
+
this.pointState = {
|
|
1314
|
+
ownerKey,
|
|
1315
|
+
scope
|
|
1316
|
+
};
|
|
1317
|
+
this.activePointTarget = null;
|
|
1318
|
+
this.owners = [{
|
|
1319
|
+
...createProjectionOwner(owner, false, scope, true),
|
|
1320
|
+
behavior: selected.behavior
|
|
1321
|
+
}];
|
|
1322
|
+
this.policy = this.resolvePolicy(this.owners);
|
|
1323
|
+
this.projectionOwners = null;
|
|
1324
|
+
this.projectionMembership = null;
|
|
1325
|
+
this.projectionTopology = null;
|
|
1326
|
+
this.hoveredTarget = null;
|
|
1327
|
+
this.invalidateOverlay();
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
exitPointState() {
|
|
1331
|
+
const state = this.pointState;
|
|
1332
|
+
if (!state) return;
|
|
1333
|
+
this.pointState = null;
|
|
1334
|
+
this.activePointTarget = null;
|
|
1335
|
+
const owner = this.root.getByKey(state.ownerKey);
|
|
1336
|
+
const selected = owner instanceof Element && this.owners.length === 1 && this.owners[0].element === owner ? this.owners[0] : void 0;
|
|
1337
|
+
if (selected && owner instanceof Element) {
|
|
1338
|
+
this.owners = [{
|
|
1339
|
+
...createProjectionOwner(owner, true),
|
|
1340
|
+
behavior: selected.behavior
|
|
1341
|
+
}];
|
|
1342
|
+
this.policy = this.resolvePolicy(this.owners);
|
|
1343
|
+
}
|
|
1344
|
+
this.projectionOwners = null;
|
|
1345
|
+
this.projectionMembership = null;
|
|
1346
|
+
this.projectionTopology = null;
|
|
1347
|
+
this.hoveredTarget = null;
|
|
1348
|
+
this.invalidateOverlay();
|
|
1349
|
+
}
|
|
1350
|
+
setActivePointTarget(target) {
|
|
1351
|
+
if (this.activePointTarget?.ownerKey === target?.ownerKey && (!this.activePointTarget || !target || samePointEditTarget(this.activePointTarget.target, target.target))) return;
|
|
1352
|
+
this.activePointTarget = target;
|
|
1353
|
+
this.invalidateOverlay();
|
|
1354
|
+
}
|
|
1355
|
+
invalidateOverlay() {
|
|
1356
|
+
if (this.overlayDirty) return;
|
|
1357
|
+
this.overlayDirty = true;
|
|
1358
|
+
this.select.transform.markPaintDirty();
|
|
1359
|
+
this.lineSession?.tool.transform.markPaintDirty();
|
|
1360
|
+
this.lineTreeSession?.tool.transform.markPaintDirty();
|
|
1361
|
+
if (!this.activeGesture && !this.lineSession && !this.lineTreeSession) this.root.scheduleHoverRefresh();
|
|
1362
|
+
}
|
|
1363
|
+
shouldShowRotationPivot() {
|
|
1364
|
+
return this.owners.length === 1 && this.rotationPivotDisplayOwner === this.owners[0].element && this.policy.rotate && this.policy.rotationPivot === "adjustable";
|
|
1365
|
+
}
|
|
1366
|
+
showRotationPivotForCurrentOwner() {
|
|
1367
|
+
const owner = this.owners.length === 1 ? this.owners[0].element : null;
|
|
1368
|
+
if (!owner || !this.policy.rotate || this.policy.rotationPivot !== "adjustable" || this.rotationPivotDisplayOwner === owner) return;
|
|
1369
|
+
this.rotationPivotDisplayOwner = owner;
|
|
1370
|
+
this.invalidateOverlay();
|
|
1371
|
+
}
|
|
1372
|
+
hasPendingOverlayWork() {
|
|
1373
|
+
return this.overlayDirty;
|
|
1374
|
+
}
|
|
1375
|
+
flushOverlayProjection() {
|
|
1376
|
+
if (!this.overlayDirty) return;
|
|
1377
|
+
this.overlayDirty = false;
|
|
1378
|
+
const pointState = this.pointState;
|
|
1379
|
+
const suppressSelectionProjection = this.contentSession !== null || this.lineSession !== null || this.lineTreeSession !== null;
|
|
1380
|
+
const owners = suppressSelectionProjection ? NO_PROJECTION_OWNERS : this.owners;
|
|
1381
|
+
const projectionPolicy = this.pointState ? {
|
|
1382
|
+
move: this.policy.move,
|
|
1383
|
+
resize: "none",
|
|
1384
|
+
rotate: false,
|
|
1385
|
+
rotationPivot: "center-only"
|
|
1386
|
+
} : this.policy;
|
|
1387
|
+
if (this.projectionOwners !== owners) {
|
|
1388
|
+
this.projectionOwners = owners;
|
|
1389
|
+
this.projectionMembership = createSelectionProjectionMembership(owners, projectionPolicy);
|
|
1390
|
+
this.projectionTopology = createSelectionProjectionTopology(owners, this.pointProjectionOwnerIsCurrent);
|
|
1391
|
+
} else if (!this.projectionTopology) this.projectionTopology = createSelectionProjectionTopology(owners, this.pointProjectionOwnerIsCurrent);
|
|
1392
|
+
if (pointState === this.pointState && pointState && !this.projectionTopology.pointScopeValid) {
|
|
1393
|
+
this.exitPointState();
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const pointGesture = this.activePointGesture;
|
|
1397
|
+
if (pointGesture && pointState === this.pointState && !this.projectionTopology.pointControls.some(({ target }) => samePointEditTarget(target.target, pointGesture.currentTarget))) pointGesture.finishInvalidated();
|
|
1398
|
+
if (pointState !== this.pointState || owners !== (suppressSelectionProjection ? NO_PROJECTION_OWNERS : this.owners)) return;
|
|
1399
|
+
const projection = buildSelectionOverlayProjection({
|
|
1400
|
+
owners,
|
|
1401
|
+
membership: this.projectionMembership,
|
|
1402
|
+
topology: this.projectionTopology,
|
|
1403
|
+
frame: owners.length > 0 ? this.selectionFrame : null,
|
|
1404
|
+
policy: projectionPolicy,
|
|
1405
|
+
pivot: this.rotationPivotState,
|
|
1406
|
+
showPivot: this.shouldShowRotationPivot(),
|
|
1407
|
+
pivotSnapMarkers: this.rotationPivotPreview?.markers,
|
|
1408
|
+
activePointTarget: this.activePointTarget,
|
|
1409
|
+
marquee: this.marquee
|
|
1410
|
+
});
|
|
1411
|
+
const lineTreeHighlight = this.lineTreeSession?.kind === "source-pick" ? this.lineTreeSession.hover?.subpathId : this.lineTreeSession?.source.subpathId;
|
|
1412
|
+
this.projectionState = lineTreeHighlight === void 0 || !this.lineTreeSession ? projection : {
|
|
1413
|
+
...projection,
|
|
1414
|
+
pointScopeOutline: this.lineTreeSession.owner.transform.getWorldBranchPointsView(lineTreeHighlight)
|
|
1415
|
+
};
|
|
1416
|
+
if (owners.length > 0) this.selectionFrame = this.projectionState.frame;
|
|
1417
|
+
}
|
|
1418
|
+
hasPendingCompletionWork() {
|
|
1419
|
+
return this.pendingSelectionUpdate || this.pendingSelectionRepair !== null;
|
|
1420
|
+
}
|
|
1421
|
+
flushCompletionWork() {
|
|
1422
|
+
if (this.pendingSelectionUpdate && !this.select.hasEventListener("select:update")) this.pendingSelectionUpdate = false;
|
|
1423
|
+
else if (this.pendingSelectionUpdate && !this.selectionGeometryIsDirty()) {
|
|
1424
|
+
this.pendingSelectionUpdate = false;
|
|
1425
|
+
this.select.dispatchEvent("select:update");
|
|
1426
|
+
}
|
|
1427
|
+
const repair = this.pendingSelectionRepair;
|
|
1428
|
+
if (!repair) return;
|
|
1429
|
+
this.pendingSelectionRepair = null;
|
|
1430
|
+
if (this.owners === repair) this.notifySelect("select:end");
|
|
1431
|
+
}
|
|
1432
|
+
resolveCursor(point, contentCursor) {
|
|
1433
|
+
const previousPointer = this.pointerWorld;
|
|
1434
|
+
this.pointerWorld = point ?? null;
|
|
1435
|
+
if (!point) {
|
|
1436
|
+
this.updateHover(null, null);
|
|
1437
|
+
return contentCursor;
|
|
1438
|
+
}
|
|
1439
|
+
const pointGesture = this.activePointGesture;
|
|
1440
|
+
if (pointGesture?.connectionRole) {
|
|
1441
|
+
const candidate = pointGesture.resolveWorldPoint(point);
|
|
1442
|
+
const probe = this.root.enablePorts ? this.snapTool?.probePortSnap(candidate.x, candidate.y, pointGesture.connectionRole, pointGesture.snapExcludes, pointGesture.connectionSource) : void 0;
|
|
1443
|
+
this.updateHover(null, null);
|
|
1444
|
+
return resolvePortProbeCursor(probe);
|
|
1445
|
+
}
|
|
1446
|
+
if (this.activeGesture instanceof TransformGesture) {
|
|
1447
|
+
if (this.activeGesture.kind === "move" && this.activeGesture.movableLineEndpoints.length > 0) return this.activeGesture.probeForbiddenPort(point) ? "not-allowed" : "move";
|
|
1448
|
+
return contentCursor;
|
|
1449
|
+
}
|
|
1450
|
+
if (this.activeGesture) return contentCursor;
|
|
1451
|
+
if (this.lineTreeSession?.kind === "source-pick") {
|
|
1452
|
+
const source = this.updateLineTreeSourceHover(point);
|
|
1453
|
+
this.updateHover(null, null);
|
|
1454
|
+
return source ? "crosshair" : contentCursor;
|
|
1455
|
+
}
|
|
1456
|
+
if (this.lineTreeSession?.kind === "branch-draw") {
|
|
1457
|
+
this.updateHover(null, null);
|
|
1458
|
+
return resolvePortProbeCursor(this.probeLineTreeTail(this.lineTreeSession, point));
|
|
1459
|
+
}
|
|
1460
|
+
if (this.lineSession) {
|
|
1461
|
+
this.updateHover(null, null);
|
|
1462
|
+
if (previousPointer !== null && previousPointer.x === point.x && previousPointer.y === point.y && this.lineSession.confirmed.length > 0 && this.lineSession.preview === null) return "crosshair";
|
|
1463
|
+
return resolvePortProbeCursor(this.probeLineAnchor(this.lineSession, point));
|
|
1464
|
+
}
|
|
1465
|
+
const rootContent = this.root.currentElement;
|
|
1466
|
+
const rawContent = rootContent && !this.editorElements.includes(rootContent) ? rootContent : null;
|
|
1467
|
+
const content = rawContent ? checkElement(rawContent, this.editorElements) ?? null : null;
|
|
1468
|
+
const hit = hitSelectionOverlay(this.projectionState, point, this.select.controlSize / this.root.viewport.scale, this.select.hitPadding / this.root.viewport.scale, 8 / this.root.viewport.scale);
|
|
1469
|
+
this.updateHover(hit?.target ?? null, hit ? null : content);
|
|
1470
|
+
return hit?.cursor ?? content?.cursor ?? contentCursor;
|
|
1471
|
+
}
|
|
1472
|
+
updateHover(target, content) {
|
|
1473
|
+
if (sameTarget(this.hoveredTarget, target) && this.hoveredContent === content) return;
|
|
1474
|
+
this.hoveredTarget = target;
|
|
1475
|
+
this.hoveredContent = content;
|
|
1476
|
+
this.select.transform.markPaintDirty();
|
|
1477
|
+
}
|
|
1478
|
+
applyAuthoredBatch(ownerEffects, structureEffects, scope) {
|
|
1479
|
+
this.root.applyChangeBatch(ownerEffects, structureEffects, scope);
|
|
1480
|
+
}
|
|
1481
|
+
onPointerDown(event) {
|
|
1482
|
+
if (this.activeGesture) return;
|
|
1483
|
+
this.root.container.focus({ preventScroll: true });
|
|
1484
|
+
if (this.lineTreeSession?.kind === "source-pick") {
|
|
1485
|
+
this.confirmLineTreeSource(event.detail);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (this.lineTreeSession?.kind === "branch-draw") {
|
|
1489
|
+
this.confirmLineTreePoint(event.detail);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (this.lineSession) {
|
|
1493
|
+
this.confirmLinePoint(event.detail);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (this.contentSession) {
|
|
1497
|
+
if (this.root.getByKey(this.contentSession.ownerKey) === this.root.currentElement) return;
|
|
1498
|
+
this.finishContentEditing(false);
|
|
1499
|
+
}
|
|
1500
|
+
const intent = pointIntent(event);
|
|
1501
|
+
const contentTarget = this.root.currentElement;
|
|
1502
|
+
const candidate = contentTarget ? checkElement(contentTarget, this.editorElements) : void 0;
|
|
1503
|
+
const additive = event.detail.ctrlKey === true || event.detail.metaKey === true;
|
|
1504
|
+
let target = this.hoveredTarget;
|
|
1505
|
+
if (this.pointState && target?.kind !== "point") {
|
|
1506
|
+
const pointState = this.pointState;
|
|
1507
|
+
const context = this.resolveCurrentPointContext(pointState);
|
|
1508
|
+
if (!context) {
|
|
1509
|
+
if (this.pointState !== pointState) return;
|
|
1510
|
+
this.exitPointState();
|
|
1511
|
+
target = null;
|
|
1512
|
+
} else if (additive || candidate && candidate !== context.owner) {
|
|
1513
|
+
this.exitPointState();
|
|
1514
|
+
target = null;
|
|
1515
|
+
} else {
|
|
1516
|
+
const scope = context.capability.resolvePointScope(intent.point, pointState.scope);
|
|
1517
|
+
if (!this.pointContextIsCurrent(pointState, context.owner)) return;
|
|
1518
|
+
if (scope) {
|
|
1519
|
+
if (!samePointScope(scope, pointState.scope)) {
|
|
1520
|
+
this.setPointState(context.owner.key, scope);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
this.setActivePointTarget(null);
|
|
1524
|
+
if (target?.kind === "body" && this.policy.move) this.activeGesture = new SelectionPressGesture(this, intent, context.owner, contentTarget);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (target?.kind === "body" && this.policy.move) {
|
|
1528
|
+
this.setActivePointTarget(null);
|
|
1529
|
+
this.activeGesture = new SelectionPressGesture(this, intent, context.owner, contentTarget);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
this.exitPointState();
|
|
1533
|
+
target = null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (target) {
|
|
1537
|
+
if (target.kind === "body" && this.policy.move) if (additive) this.activeGesture = new MarqueeGesture(this, intent.point, candidate, true, this.selection);
|
|
1538
|
+
else this.activeGesture = new SelectionPressGesture(this, intent, candidate, contentTarget);
|
|
1539
|
+
else if (target.kind === "resize") this.startTransform("resize", intent, target.handle);
|
|
1540
|
+
else if (target.kind === "rotate") this.startTransform("rotate", intent);
|
|
1541
|
+
else if (target.kind === "pivot") {
|
|
1542
|
+
const owner = this.owners.length === 1 ? this.owners[0].element : null;
|
|
1543
|
+
if (owner) {
|
|
1544
|
+
this.activeGesture = new PivotGesture(this, owner, intent.point, this.projectionState.rotationPivotWorld);
|
|
1545
|
+
this.invalidateOverlay();
|
|
1546
|
+
}
|
|
1547
|
+
} else if (target.kind === "point") {
|
|
1548
|
+
this.setActivePointTarget({
|
|
1549
|
+
ownerKey: target.ownerKey,
|
|
1550
|
+
target: target.target
|
|
1551
|
+
});
|
|
1552
|
+
if (target.move === "free") this.startPointGesture(target, intent);
|
|
1553
|
+
}
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
const behavior = candidate ? getEditorCapability(candidate, SelectionBehaviorCapabilityToken)?.getSelectionBehavior() : void 0;
|
|
1557
|
+
if (!additive && candidate && behavior?.mode === SelectionBehaviorMode.OwnerDirect) {
|
|
1558
|
+
this.activeGesture = new SelectionPressGesture(this, intent, candidate, contentTarget, candidate);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
this.activeGesture = new MarqueeGesture(this, intent.point, candidate, additive, this.selection);
|
|
1562
|
+
}
|
|
1563
|
+
onPointerMove(event) {
|
|
1564
|
+
if (this.lineTreeSession?.kind === "source-pick") {
|
|
1565
|
+
this.updateLineTreeSourceHover(event.detail);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (this.lineTreeSession?.kind === "branch-draw") {
|
|
1569
|
+
this.updateLineTreePreview(event.detail);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (this.lineSession) {
|
|
1573
|
+
this.updateLinePreview(event.detail);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
this.activeGesture?.update(pointIntent(event));
|
|
1577
|
+
}
|
|
1578
|
+
onPointerFinish(event) {
|
|
1579
|
+
if (event.type === "pointercancel" && (this.lineSession || this.lineTreeSession)) {
|
|
1580
|
+
this.cancelLineDrawing();
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
this.activeGesture?.finish(pointIntent(event), event.type === "pointercancel");
|
|
1584
|
+
}
|
|
1585
|
+
onDoubleClick(event) {
|
|
1586
|
+
const treeDrawing = this.lineTreeSession;
|
|
1587
|
+
if (treeDrawing) {
|
|
1588
|
+
const subpathId = treeDrawing.owner.resolveSubpathAtWorldPoint(event.detail);
|
|
1589
|
+
if (this.lineTreeSession !== treeDrawing || treeDrawing.owner.activeRoot !== this.root) return;
|
|
1590
|
+
if (subpathId === void 0) return;
|
|
1591
|
+
this.cancelLineDrawing();
|
|
1592
|
+
this.setPointState(treeDrawing.owner.key, {
|
|
1593
|
+
kind: "subpath",
|
|
1594
|
+
subpathId
|
|
1595
|
+
});
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (this.lineSession) return;
|
|
1599
|
+
if (this.selection.length !== 1 || this.activeGesture) return;
|
|
1600
|
+
const selected = this.owners[0];
|
|
1601
|
+
const selectedOwners = this.owners;
|
|
1602
|
+
const selectedElement = selected.element;
|
|
1603
|
+
if (selected.behavior.dive === SelectionDiveMode.DirectChild) {
|
|
1604
|
+
const checked = /* @__PURE__ */ new Set();
|
|
1605
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1606
|
+
for (const { key } of this.editorElements) excluded.add(key);
|
|
1607
|
+
for (const hit of this.root.iteratePoint(event.detail, {
|
|
1608
|
+
scopeRootKey: selected.element.key,
|
|
1609
|
+
exclude: excluded
|
|
1610
|
+
})) {
|
|
1611
|
+
if (this.owners !== selectedOwners || this.root.getByKey(selectedElement.key) !== selectedElement) return;
|
|
1612
|
+
let child = hit;
|
|
1613
|
+
while (child.parent instanceof Element && child.parent !== selected.element) child = child.parent;
|
|
1614
|
+
if (child.parent !== selected.element || checked.has(child)) continue;
|
|
1615
|
+
checked.add(child);
|
|
1616
|
+
const candidate = checkElement(child, this.editorElements, selected.element.key);
|
|
1617
|
+
if (this.owners !== selectedOwners || this.root.getByKey(selectedElement.key) !== selectedElement) return;
|
|
1618
|
+
if (!candidate) continue;
|
|
1619
|
+
this.setSelection([child]);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (this.owners !== selectedOwners || this.root.getByKey(selectedElement.key) !== selectedElement) return;
|
|
1624
|
+
const content = getEditorCapability(selectedElement, ContentEditingCapabilityToken);
|
|
1625
|
+
if (content) {
|
|
1626
|
+
if (this.beginContentEditing(content, selectedElement)) return;
|
|
1627
|
+
}
|
|
1628
|
+
if (this.owners !== selectedOwners || this.root.getByKey(selectedElement.key) !== selectedElement) return;
|
|
1629
|
+
const point = getEditorCapability(selectedElement, PointEditingCapabilityToken);
|
|
1630
|
+
if (!point) return;
|
|
1631
|
+
const scope = point.resolvePointScope(event.detail, this.pointState?.scope);
|
|
1632
|
+
if (this.owners !== selectedOwners || this.root.getByKey(selectedElement.key) !== selectedElement) return;
|
|
1633
|
+
if (scope) this.setPointState(selectedElement.key, scope);
|
|
1634
|
+
}
|
|
1635
|
+
onKeyDown(event) {
|
|
1636
|
+
const key = event.key.toLowerCase();
|
|
1637
|
+
const command = event.ctrlKey || event.metaKey;
|
|
1638
|
+
const editorContentTextarea = event.target instanceof HTMLTextAreaElement && event.target.dataset.fulateEditorContent !== void 0;
|
|
1639
|
+
if (isFormControlTarget(event.target) && !editorContentTextarea) return;
|
|
1640
|
+
if (command && (key === "z" || key === "y")) {
|
|
1641
|
+
event.preventDefault();
|
|
1642
|
+
this.finishCurrentInteraction();
|
|
1643
|
+
if (key === "y" || event.shiftKey) this.history.redo();
|
|
1644
|
+
else this.history.undo();
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (this.activeGesture) {
|
|
1648
|
+
if (event.key === "Escape") {
|
|
1649
|
+
event.preventDefault();
|
|
1650
|
+
const gesture = this.activeGesture;
|
|
1651
|
+
this.root.input.cancelSession();
|
|
1652
|
+
if (this.activeGesture === gesture) {
|
|
1653
|
+
const pointGesture = this.activePointGesture;
|
|
1654
|
+
if (pointGesture === gesture && pointGesture.connectionRole !== null) pointGesture.finish(void 0, true);
|
|
1655
|
+
else gesture.finish();
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (this.contentSession) {
|
|
1661
|
+
if (event.key === "Escape") {
|
|
1662
|
+
event.preventDefault();
|
|
1663
|
+
this.finishContentEditing(false);
|
|
1664
|
+
}
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
if (this.lineSession || this.lineTreeSession) {
|
|
1668
|
+
if (key === "l" && !command && !event.altKey && this.lineTreeSession?.kind === "source-pick" && this.pointerWorld) {
|
|
1669
|
+
event.preventDefault();
|
|
1670
|
+
this.confirmLineTreeSource(this.pointerWorld);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (event.key === "Escape" || event.key === "Enter") {
|
|
1674
|
+
event.preventDefault();
|
|
1675
|
+
this.stopLineDrawing();
|
|
1676
|
+
}
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (this.pointState && event.key === "Escape") {
|
|
1680
|
+
event.preventDefault();
|
|
1681
|
+
this.exitPointState();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (command && key === "g") {
|
|
1685
|
+
event.preventDefault();
|
|
1686
|
+
if (event.shiftKey) this.select.unGroup();
|
|
1687
|
+
else this.select.doGroup();
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (command && key === "c") {
|
|
1691
|
+
const commands = this.select.keyboardCommandOwner;
|
|
1692
|
+
if (!commands) return;
|
|
1693
|
+
event.preventDefault();
|
|
1694
|
+
commands.copy();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (command && key === "x") {
|
|
1698
|
+
const commands = this.select.keyboardCommandOwner;
|
|
1699
|
+
if (!commands) return;
|
|
1700
|
+
event.preventDefault();
|
|
1701
|
+
commands.cut();
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (command && key === "v") {
|
|
1705
|
+
const commands = this.select.keyboardCommandOwner;
|
|
1706
|
+
if (!commands) return;
|
|
1707
|
+
event.preventDefault();
|
|
1708
|
+
commands.paste();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (command && key === "a") {
|
|
1712
|
+
event.preventDefault();
|
|
1713
|
+
const elements = /* @__PURE__ */ new Set();
|
|
1714
|
+
for (const element of this.root.queryArea(this.root.viewport.getWorldRect(), {
|
|
1715
|
+
bounds: "visual",
|
|
1716
|
+
order: "none"
|
|
1717
|
+
})) {
|
|
1718
|
+
const resolved = checkElement(element, this.editorElements);
|
|
1719
|
+
if (resolved) elements.add(resolved);
|
|
1720
|
+
}
|
|
1721
|
+
this.setSelection([...elements]);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (key === "l" && !command && !event.altKey) {
|
|
1725
|
+
const tool = this.select.lineTool;
|
|
1726
|
+
if (tool) {
|
|
1727
|
+
event.preventDefault();
|
|
1728
|
+
const selectedLineTree = this.selection.length === 1 && this.selection[0] instanceof LineTree ? this.selection[0] : null;
|
|
1729
|
+
if (selectedLineTree) this.startLineTreeBranchDrawing(tool, selectedLineTree, this.pointState?.ownerKey === selectedLineTree.key && this.pointState.scope.kind === "subpath" ? this.pointState.scope.subpathId : void 0);
|
|
1730
|
+
else this.startLineDrawing(tool);
|
|
1731
|
+
}
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (event.key === "Escape") {
|
|
1735
|
+
event.preventDefault();
|
|
1736
|
+
this.setSelection([]);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
1740
|
+
const commands = this.select.keyboardCommandOwner;
|
|
1741
|
+
if (!commands && this.pointState && this.deletePointTarget()) {
|
|
1742
|
+
event.preventDefault();
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (!commands) return;
|
|
1746
|
+
event.preventDefault();
|
|
1747
|
+
commands.delete();
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if ([
|
|
1751
|
+
"ArrowUp",
|
|
1752
|
+
"ArrowDown",
|
|
1753
|
+
"ArrowLeft",
|
|
1754
|
+
"ArrowRight"
|
|
1755
|
+
].includes(event.key) && this.selection.length > 0) {
|
|
1756
|
+
event.preventDefault();
|
|
1757
|
+
const step = event.shiftKey ? 10 : 1;
|
|
1758
|
+
this.nudge(event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0, event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
captureTransformRecords() {
|
|
1762
|
+
const records = [];
|
|
1763
|
+
for (const owner of this.owners) {
|
|
1764
|
+
const capability = owner.transform;
|
|
1765
|
+
if (!capability) continue;
|
|
1766
|
+
records.push({
|
|
1767
|
+
owner: owner.element,
|
|
1768
|
+
parent: owner.element.parent,
|
|
1769
|
+
capability,
|
|
1770
|
+
baseline: capability.captureTransformBaseline()
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
return records;
|
|
1774
|
+
}
|
|
1775
|
+
startTransform(kind, intent, resizeHandle, selectionChanged = false) {
|
|
1776
|
+
if (kind === "rotate") this.showRotationPivotForCurrentOwner();
|
|
1777
|
+
this.root.frameRuntime.drain();
|
|
1778
|
+
if (selectionChanged) this.requestSelectionEnd();
|
|
1779
|
+
const frame = this.projectionState.frame;
|
|
1780
|
+
if (!frame) return;
|
|
1781
|
+
const records = this.captureTransformRecords();
|
|
1782
|
+
if (records.length !== this.owners.length) return;
|
|
1783
|
+
let snapPoints = this.owners.length === 1 ? this.owners[0].snap?.getWorldSnapPointsView() ?? frame.outline : frame.outline;
|
|
1784
|
+
const selection = this.selection;
|
|
1785
|
+
const snapExcludes = kind === "move" ? this.editorElements.concat(selection) : NO_ELEMENTS;
|
|
1786
|
+
const selectedLine = kind === "move" && this.owners.length === 1 && this.owners[0].element instanceof BaseLine ? this.owners[0].element : null;
|
|
1787
|
+
let movableLineEndpoints = NO_POINTS;
|
|
1788
|
+
if (selectedLine) movableLineEndpoints = [snapPoints[0], snapPoints[snapPoints.length - 1]];
|
|
1789
|
+
const history = this.history.openScope(selection);
|
|
1790
|
+
this.activeGesture = new TransformGesture(this, kind, intent.point, intent, frame, records, snapPoints, movableLineEndpoints, snapExcludes, this.projectionState.rotationPivotWorld, history, resizeHandle);
|
|
1791
|
+
if (kind === "move" && this.owners.every(({ behavior }) => behavior.snap === SelectionSnapMode.Enabled)) this.snapTool?.start(snapExcludes);
|
|
1792
|
+
}
|
|
1793
|
+
promoteSelectionPress(gesture) {
|
|
1794
|
+
if (this.activeGesture !== gesture) return null;
|
|
1795
|
+
let selectionChanged = false;
|
|
1796
|
+
if (gesture.directDragOwner && !this.selection.includes(gesture.directDragOwner)) {
|
|
1797
|
+
this.replaceSelection([gesture.directDragOwner]);
|
|
1798
|
+
selectionChanged = true;
|
|
1799
|
+
}
|
|
1800
|
+
this.startTransform("move", gesture.start, void 0, selectionChanged);
|
|
1801
|
+
const transform = this.activeGesture;
|
|
1802
|
+
if (!(transform instanceof TransformGesture)) return null;
|
|
1803
|
+
return transform;
|
|
1804
|
+
}
|
|
1805
|
+
finishSelectionPress(gesture) {
|
|
1806
|
+
let pressesCurrentOwner = false;
|
|
1807
|
+
if (this.selection.length === 1) for (let current = gesture.contentTarget ?? void 0; current instanceof Element; current = current.parent) {
|
|
1808
|
+
if (current !== this.selection[0]) continue;
|
|
1809
|
+
pressesCurrentOwner = true;
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
if (gesture.candidate && !this.selection.includes(gesture.candidate) && !pressesCurrentOwner) this.finishSelectionGesture(gesture, [gesture.candidate]);
|
|
1813
|
+
else this.finishPointerGesture(gesture);
|
|
1814
|
+
}
|
|
1815
|
+
finishSelectionGesture(gesture, selection) {
|
|
1816
|
+
if (this.activeGesture !== gesture) return;
|
|
1817
|
+
this.activeGesture = null;
|
|
1818
|
+
if (selection) this.replaceSelection(selection);
|
|
1819
|
+
this.root.frameRuntime.drain();
|
|
1820
|
+
if (selection) this.requestSelectionEnd();
|
|
1821
|
+
}
|
|
1822
|
+
consumeTransform(gesture, intent) {
|
|
1823
|
+
if (!gesture.ownsCurrentContext()) {
|
|
1824
|
+
gesture.finishInvalidated();
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
let worldDelta;
|
|
1828
|
+
let unwrappedRotationDelta;
|
|
1829
|
+
if (gesture.kind === "move") {
|
|
1830
|
+
let dx = intent.point.x - gesture.start.x;
|
|
1831
|
+
let dy = intent.point.y - gesture.start.y;
|
|
1832
|
+
const snap = gesture.probeForbiddenPort(intent.point) ? null : this.snapTool?.detect(gesture.snapPoints, dx, dy);
|
|
1833
|
+
if (snap) {
|
|
1834
|
+
dx += snap.dx;
|
|
1835
|
+
dy += snap.dy;
|
|
1836
|
+
}
|
|
1837
|
+
worldDelta = new DOMMatrix().translate(dx, dy);
|
|
1838
|
+
} else if (gesture.kind === "resize") worldDelta = resolveResizeWorldDelta(gesture.frame, gesture.resizeHandle, intent.point, intent.shiftKey);
|
|
1839
|
+
else {
|
|
1840
|
+
const rotation = resolveRotationTransform({
|
|
1841
|
+
frameAngle: gesture.frame.angle,
|
|
1842
|
+
pivot: gesture.rotationPivotWorld,
|
|
1843
|
+
unwrappedDelta: gesture.consumeRotationDelta(intent.point),
|
|
1844
|
+
snapAngle: this.select.snapAngle,
|
|
1845
|
+
snapThreshold: this.select.snapThreshold
|
|
1846
|
+
});
|
|
1847
|
+
worldDelta = rotation.worldDelta;
|
|
1848
|
+
unwrappedRotationDelta = rotation.unwrappedDelta;
|
|
1849
|
+
}
|
|
1850
|
+
const transformIntent = {
|
|
1851
|
+
worldDelta,
|
|
1852
|
+
...unwrappedRotationDelta === void 0 ? {} : { unwrappedRotationDelta }
|
|
1853
|
+
};
|
|
1854
|
+
const changes = gesture.records.map((record) => record.capability.resolveTransformChange(record.baseline, transformIntent));
|
|
1855
|
+
this.selectionFrame = resolveSelectionFrameTransform(gesture.frame, worldDelta, gesture.kind);
|
|
1856
|
+
this.invalidatePointTopology();
|
|
1857
|
+
this.applyAuthoredBatch(changes, [], gesture.history);
|
|
1858
|
+
this.invalidateOverlay();
|
|
1859
|
+
}
|
|
1860
|
+
startPointGesture(target, intent) {
|
|
1861
|
+
const owner = this.root.getByKey(target.ownerKey);
|
|
1862
|
+
if (!(owner instanceof Element)) return;
|
|
1863
|
+
const state = this.pointState;
|
|
1864
|
+
const context = state && this.resolveCurrentPointContext(state);
|
|
1865
|
+
if (!context || context.owner !== owner) return;
|
|
1866
|
+
const control = this.projectionState.controls.find(({ target: candidate }) => sameTarget(candidate, target));
|
|
1867
|
+
if (!control || control.target.kind !== "point") return;
|
|
1868
|
+
const currentTarget = control.target;
|
|
1869
|
+
const connectionRole = this.select.connectionGesture !== false && currentTarget.role === "endpoint" && currentTarget.target.kind === "anchor" ? isEndpointRelationSource(owner) ? owner.getEndpointRole(currentTarget.target.anchorId) : null : null;
|
|
1870
|
+
const snapExcludes = this.editorElements.concat(owner);
|
|
1871
|
+
this.activeGesture = new PointGesture(this, owner.key, owner.parent, currentTarget.target, currentTarget.insertT, currentTarget.role, connectionRole, snapExcludes, this.history.openScope(this.selection), intent.point, control.world);
|
|
1872
|
+
this.snapTool?.start(snapExcludes, void 0, connectionRole !== null && this.root.enablePorts);
|
|
1873
|
+
}
|
|
1874
|
+
consumePoint(gesture, intent, first) {
|
|
1875
|
+
const context = this.resolvePointGestureContext(gesture);
|
|
1876
|
+
if (!context) {
|
|
1877
|
+
if (this.activePointGesture === gesture) gesture.finishInvalidated();
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (first && gesture.currentTarget.kind !== "segment" && intent.point.x === gesture.startPointerWorld.x && intent.point.y === gesture.startPointerWorld.y) return;
|
|
1881
|
+
let point = gesture.resolveWorldPoint(intent.point);
|
|
1882
|
+
let target;
|
|
1883
|
+
let targetOwner;
|
|
1884
|
+
let targetParent;
|
|
1885
|
+
let portProbe = null;
|
|
1886
|
+
if (gesture.connectionRole && this.root.enablePorts) {
|
|
1887
|
+
portProbe = this.snapTool?.probePortSnap(point.x, point.y, gesture.connectionRole, gesture.snapExcludes, gesture.connectionSource) ?? NO_PORT_SNAP$1;
|
|
1888
|
+
const hit = portProbe.snap;
|
|
1889
|
+
if (hit) {
|
|
1890
|
+
point = hit;
|
|
1891
|
+
target = {
|
|
1892
|
+
elementKey: hit.elementKey,
|
|
1893
|
+
portId: hit.portId
|
|
1894
|
+
};
|
|
1895
|
+
targetOwner = this.root.getByKey(hit.elementKey);
|
|
1896
|
+
targetParent = targetOwner.parent;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
gesture.connectionTarget = target;
|
|
1900
|
+
gesture.connectionTargetOwner = targetOwner;
|
|
1901
|
+
gesture.connectionTargetParent = targetParent;
|
|
1902
|
+
if (isBlockedPortProbe(portProbe)) return;
|
|
1903
|
+
if (!target) {
|
|
1904
|
+
const offset = this.snapTool?.detect([point], 0, 0);
|
|
1905
|
+
if (offset) point = {
|
|
1906
|
+
x: point.x + offset.dx,
|
|
1907
|
+
y: point.y + offset.dy
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const local = context.owner.transform.toLocal(point);
|
|
1911
|
+
if (!local) return;
|
|
1912
|
+
const request = gesture.currentTarget.kind === "segment" ? {
|
|
1913
|
+
kind: "insert-on-segment",
|
|
1914
|
+
segmentId: gesture.currentTarget.segmentId,
|
|
1915
|
+
t: gesture.insertT,
|
|
1916
|
+
local
|
|
1917
|
+
} : gesture.currentTarget.kind === "handle" ? {
|
|
1918
|
+
kind: "move-handle",
|
|
1919
|
+
target: gesture.currentTarget,
|
|
1920
|
+
local
|
|
1921
|
+
} : {
|
|
1922
|
+
kind: "move-anchor",
|
|
1923
|
+
anchorId: gesture.currentTarget.anchorId,
|
|
1924
|
+
local
|
|
1925
|
+
};
|
|
1926
|
+
const edit = context.capability.resolvePointEdit(request);
|
|
1927
|
+
let current = this.resolvePointGestureContext(gesture);
|
|
1928
|
+
if (!current || current.owner !== context.owner) return;
|
|
1929
|
+
if (!edit) return;
|
|
1930
|
+
let change;
|
|
1931
|
+
if (edit.kind === "owner-change") change = edit.change;
|
|
1932
|
+
else {
|
|
1933
|
+
change = resolveDisconnectEndpointChanges(this.root, [edit.source], new Map([[edit.source.anchorId, edit.local]]))[0];
|
|
1934
|
+
current = this.resolvePointGestureContext(gesture);
|
|
1935
|
+
if (!current || current.owner !== context.owner || !change) return;
|
|
1936
|
+
}
|
|
1937
|
+
this.projectionTopology = null;
|
|
1938
|
+
this.selectionFrame = null;
|
|
1939
|
+
this.applyAuthoredBatch([change], [], gesture.history);
|
|
1940
|
+
current = this.resolvePointGestureContext(gesture);
|
|
1941
|
+
if (edit.kind === "owner-change" && edit.nextTarget && current?.owner === context.owner) {
|
|
1942
|
+
gesture.currentTarget = edit.nextTarget;
|
|
1943
|
+
this.activePointTarget = {
|
|
1944
|
+
ownerKey: context.owner.key,
|
|
1945
|
+
target: edit.nextTarget
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
this.invalidateOverlay();
|
|
1949
|
+
}
|
|
1950
|
+
deletePointTarget() {
|
|
1951
|
+
const state = this.pointState;
|
|
1952
|
+
if (!state) return false;
|
|
1953
|
+
const context = this.resolveCurrentPointContext(state);
|
|
1954
|
+
if (!context) {
|
|
1955
|
+
if (this.pointState === state) this.exitPointState();
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
const hovered = this.hoveredTarget?.kind === "point" ? {
|
|
1959
|
+
ownerKey: this.hoveredTarget.ownerKey,
|
|
1960
|
+
target: this.hoveredTarget.target
|
|
1961
|
+
} : null;
|
|
1962
|
+
const target = this.activePointTarget ?? hovered;
|
|
1963
|
+
if (target) {
|
|
1964
|
+
if (target.ownerKey !== context.owner.key) return true;
|
|
1965
|
+
if (target.target.kind !== "anchor") return true;
|
|
1966
|
+
const edit = context.capability.resolvePointEdit({
|
|
1967
|
+
kind: "delete-anchor",
|
|
1968
|
+
anchorId: target.target.anchorId
|
|
1969
|
+
});
|
|
1970
|
+
if (!this.pointContextIsCurrent(state, context.owner)) return true;
|
|
1971
|
+
if (!edit || edit.kind !== "owner-change") return true;
|
|
1972
|
+
this.projectionTopology = null;
|
|
1973
|
+
const scope = this.history.openScope(this.selection);
|
|
1974
|
+
this.applyAuthoredBatch([edit.change], [], scope);
|
|
1975
|
+
this.root.frameRuntime.drain();
|
|
1976
|
+
this.invalidateOverlay();
|
|
1977
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
const edit = context.capability.resolvePointEdit({
|
|
1981
|
+
kind: "delete-scope",
|
|
1982
|
+
scope: state.scope
|
|
1983
|
+
});
|
|
1984
|
+
if (!this.pointContextIsCurrent(state, context.owner)) return true;
|
|
1985
|
+
if (!edit) return false;
|
|
1986
|
+
if (edit.kind !== "owner-change") return true;
|
|
1987
|
+
this.projectionTopology = null;
|
|
1988
|
+
const scope = this.history.openScope(this.selection);
|
|
1989
|
+
this.applyAuthoredBatch([edit.change], [], scope);
|
|
1990
|
+
if (edit.nextScope && this.pointContextIsCurrent(state, context.owner)) this.setPointState(context.owner.key, edit.nextScope);
|
|
1991
|
+
this.root.frameRuntime.drain();
|
|
1992
|
+
this.invalidateOverlay();
|
|
1993
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
nudge(dx, dy) {
|
|
1997
|
+
this.root.frameRuntime.drain();
|
|
1998
|
+
const frame = this.projectionState.frame;
|
|
1999
|
+
const records = this.captureTransformRecords();
|
|
2000
|
+
if (records.length !== this.owners.length) return;
|
|
2001
|
+
const scope = this.history.openScope(this.selection);
|
|
2002
|
+
const worldDelta = new DOMMatrix().translate(dx, dy);
|
|
2003
|
+
const intent = { worldDelta };
|
|
2004
|
+
const changes = records.map((record) => record.capability.resolveTransformChange(record.baseline, intent));
|
|
2005
|
+
if (changes.length > 0) {
|
|
2006
|
+
if (frame) this.selectionFrame = resolveSelectionFrameTransform(frame, worldDelta, "move");
|
|
2007
|
+
this.invalidatePointTopology();
|
|
2008
|
+
this.preserveSelectionFrame = true;
|
|
2009
|
+
try {
|
|
2010
|
+
this.applyAuthoredBatch(changes, [], scope);
|
|
2011
|
+
this.root.frameRuntime.drain();
|
|
2012
|
+
} finally {
|
|
2013
|
+
this.preserveSelectionFrame = false;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
this.invalidateOverlay();
|
|
2017
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2018
|
+
}
|
|
2019
|
+
setProperties(patch) {
|
|
2020
|
+
if (this.owners.length === 0) return;
|
|
2021
|
+
this.finishCurrentInteraction();
|
|
2022
|
+
if (this.select.interactionController !== this) return;
|
|
2023
|
+
const selection = this.selection;
|
|
2024
|
+
if (selection.length === 0) return;
|
|
2025
|
+
const scope = this.history.openScope(selection);
|
|
2026
|
+
const changes = selection.map((owner) => ({
|
|
2027
|
+
owner,
|
|
2028
|
+
patch
|
|
2029
|
+
}));
|
|
2030
|
+
this.applyAuthoredBatch(resolveConnectionPortChanges(this.root, changes), [], scope);
|
|
2031
|
+
this.root.frameRuntime.drain();
|
|
2032
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2033
|
+
}
|
|
2034
|
+
prepareTransformCommandFrame() {
|
|
2035
|
+
const hadInteraction = this.activeGesture !== null || this.contentSession !== null || this.lineSession !== null || this.lineTreeSession !== null;
|
|
2036
|
+
this.finishCurrentInteraction();
|
|
2037
|
+
this.root.frameRuntime.drain();
|
|
2038
|
+
if (this.select.interactionController !== this) return;
|
|
2039
|
+
if (hadInteraction || this.overlayDirty) {
|
|
2040
|
+
this.selectionFrame = null;
|
|
2041
|
+
if (!this.overlayDirty) this.invalidateOverlay();
|
|
2042
|
+
this.flushOverlayProjection();
|
|
2043
|
+
}
|
|
2044
|
+
return this.projectionState.frame;
|
|
2045
|
+
}
|
|
2046
|
+
applyTransformCommand(worldDelta) {
|
|
2047
|
+
const records = this.captureTransformRecords();
|
|
2048
|
+
if (records.length !== this.owners.length) return;
|
|
2049
|
+
const intent = { worldDelta };
|
|
2050
|
+
const changes = records.map((record) => record.capability.resolveTransformChange(record.baseline, intent));
|
|
2051
|
+
this.invalidatePointTopology();
|
|
2052
|
+
const scope = this.history.openScope(this.selection);
|
|
2053
|
+
this.applyCommand(changes, [], scope, this.selection);
|
|
2054
|
+
}
|
|
2055
|
+
resize(options = {}) {
|
|
2056
|
+
const projectionFrame = this.prepareTransformCommandFrame();
|
|
2057
|
+
if (!projectionFrame) return;
|
|
2058
|
+
const frame = this.owners.length === 1 ? createWorldVisualBoundsSelectionFrame(this.owners, projectionFrame) : projectionFrame;
|
|
2059
|
+
if (!frame) return;
|
|
2060
|
+
const resizeX = this.policy.resize === "x" || this.policy.resize === "both";
|
|
2061
|
+
const resizeY = this.policy.resize === "y" || this.policy.resize === "both";
|
|
2062
|
+
const width = resizeX && options.width !== void 0 ? options.width : frame.width;
|
|
2063
|
+
const height = resizeY && options.height !== void 0 ? options.height : frame.height;
|
|
2064
|
+
if (width === frame.width && height === frame.height) return;
|
|
2065
|
+
const worldDelta = resolveResizeWorldDeltaForSize(frame, width, height, options.anchor);
|
|
2066
|
+
if (worldDelta.a === 1 && worldDelta.b === 0 && worldDelta.c === 0 && worldDelta.d === 1 && worldDelta.e === 0 && worldDelta.f === 0) return;
|
|
2067
|
+
this.applyTransformCommand(worldDelta);
|
|
2068
|
+
}
|
|
2069
|
+
flip(direction) {
|
|
2070
|
+
const frame = this.prepareTransformCommandFrame();
|
|
2071
|
+
if (!frame) return;
|
|
2072
|
+
if (!(direction === "horizontal" ? this.policy.resize === "x" || this.policy.resize === "both" : this.policy.resize === "y" || this.policy.resize === "both")) return;
|
|
2073
|
+
this.applyTransformCommand(resolveFlipWorldDelta(frame, direction));
|
|
2074
|
+
}
|
|
2075
|
+
previewRotationPivot(owner, point) {
|
|
2076
|
+
const preview = this.resolveRotationPivot(owner, point, true);
|
|
2077
|
+
if (!preview) return;
|
|
2078
|
+
this.rotationPivotPreview = {
|
|
2079
|
+
owner,
|
|
2080
|
+
...preview
|
|
2081
|
+
};
|
|
2082
|
+
this.invalidateOverlay();
|
|
2083
|
+
}
|
|
2084
|
+
clearRotationPivotPreview(owner) {
|
|
2085
|
+
if (this.rotationPivotPreview?.owner !== owner) return;
|
|
2086
|
+
this.rotationPivotPreview = null;
|
|
2087
|
+
this.invalidateOverlay();
|
|
2088
|
+
}
|
|
2089
|
+
commitRotationPivot(owner, point) {
|
|
2090
|
+
const resolved = this.rotationPivotPreview?.owner === owner ? this.rotationPivotPreview : this.resolveRotationPivot(owner, point, false);
|
|
2091
|
+
this.rotationPivotPreview = null;
|
|
2092
|
+
if (!resolved || owner.activeRoot !== this.root) {
|
|
2093
|
+
this.invalidateOverlay();
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (Object.is(owner.rotationPivotX, resolved.x) && Object.is(owner.rotationPivotY, resolved.y)) {
|
|
2097
|
+
this.invalidateOverlay();
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
const scope = this.history.openScope(this.selection);
|
|
2101
|
+
this.applyAuthoredBatch([{
|
|
2102
|
+
owner,
|
|
2103
|
+
patch: {
|
|
2104
|
+
rotationPivotX: resolved.x,
|
|
2105
|
+
rotationPivotY: resolved.y
|
|
2106
|
+
}
|
|
2107
|
+
}], [], scope);
|
|
2108
|
+
this.root.frameRuntime.drain();
|
|
2109
|
+
this.invalidateOverlay();
|
|
2110
|
+
return scope?.finish(this.selection);
|
|
2111
|
+
}
|
|
2112
|
+
resolveRotationPivot(owner, point, snap) {
|
|
2113
|
+
const local = owner.transform.toLocal(point);
|
|
2114
|
+
if (!local) return;
|
|
2115
|
+
const frame = this.projectionState.frame;
|
|
2116
|
+
if (!frame) return;
|
|
2117
|
+
const bounds = owner.transform.getLocalVisualBoundsView() ?? owner.transform.getLayoutReferenceBoundsView();
|
|
2118
|
+
const markers = createSelectionRotationPivotSnapMarkers(frame);
|
|
2119
|
+
if (snap) {
|
|
2120
|
+
const threshold = 8 / this.root.viewport.scale;
|
|
2121
|
+
let nearest = -1;
|
|
2122
|
+
let distance = threshold;
|
|
2123
|
+
markers.forEach((marker, index) => {
|
|
2124
|
+
const next = Math.hypot(marker.x - point.x, marker.y - point.y);
|
|
2125
|
+
if (next <= distance) {
|
|
2126
|
+
nearest = index;
|
|
2127
|
+
distance = next;
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
if (nearest !== -1) {
|
|
2131
|
+
const marker = markers[nearest];
|
|
2132
|
+
const x = marker.horizontal;
|
|
2133
|
+
const y = marker.vertical;
|
|
2134
|
+
return {
|
|
2135
|
+
local: {
|
|
2136
|
+
x: bounds.left + (resolveOrigin(x) + .5) * bounds.width,
|
|
2137
|
+
y: bounds.top + (resolveOrigin(y) + .5) * bounds.height
|
|
2138
|
+
},
|
|
2139
|
+
x,
|
|
2140
|
+
y,
|
|
2141
|
+
markers
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return {
|
|
2146
|
+
local,
|
|
2147
|
+
x: bounds.width === 0 ? 0 : (local.x - bounds.left) / bounds.width - .5,
|
|
2148
|
+
y: bounds.height === 0 ? 0 : (local.y - bounds.top) / bounds.height - .5,
|
|
2149
|
+
markers
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
setMarquee(rect) {
|
|
2153
|
+
this.marquee = rect;
|
|
2154
|
+
this.invalidateOverlay();
|
|
2155
|
+
}
|
|
2156
|
+
finishPointerGesture(gesture) {
|
|
2157
|
+
if (this.activeGesture === gesture) this.activeGesture = null;
|
|
2158
|
+
if (gesture instanceof PointGesture) this.frameFlush.discardGestureWork(gesture);
|
|
2159
|
+
}
|
|
2160
|
+
pointConnectionCandidateIsCurrent(gesture, source, target) {
|
|
2161
|
+
return this.root.enablePorts && this.resolvePointGestureContext(gesture) !== null && gesture.connectionSource?.elementKey === source.elementKey && gesture.connectionSource.anchorId === source.anchorId && gesture.connectionTarget?.elementKey === target.elementKey && gesture.connectionTarget.portId === target.portId && this.root.getByKey(target.elementKey) === gesture.connectionTargetOwner && gesture.connectionTargetOwner?.parent === gesture.connectionTargetParent;
|
|
2162
|
+
}
|
|
2163
|
+
finishPointConnectionGesture(gesture) {
|
|
2164
|
+
const source = gesture.connectionSource;
|
|
2165
|
+
const target = gesture.connectionTarget;
|
|
2166
|
+
const connectionGesture = this.select.connectionGesture;
|
|
2167
|
+
if (source && target && connectionGesture === "auto" && this.pointConnectionCandidateIsCurrent(gesture, source, target)) {
|
|
2168
|
+
const change = resolveConnectEndpointChange(this.root, source, target);
|
|
2169
|
+
if (change && this.pointConnectionCandidateIsCurrent(gesture, source, target)) {
|
|
2170
|
+
this.applyAuthoredBatch([change], [], gesture.history);
|
|
2171
|
+
this.root.frameRuntime.drain();
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const publishIntent = !!source && !!target && typeof connectionGesture === "function" && this.pointConnectionCandidateIsCurrent(gesture, source, target);
|
|
2175
|
+
this.finishPointerGesture(gesture);
|
|
2176
|
+
this.publishConfiguredChange(gesture.history?.finish(this.selection));
|
|
2177
|
+
if (publishIntent) connectionGesture(createToolsConnectionIntent(this.root, this.history, source, target));
|
|
2178
|
+
}
|
|
2179
|
+
beginContentEditing(capability, expectedOwner) {
|
|
2180
|
+
this.finishCurrentInteraction();
|
|
2181
|
+
const owners = this.owners;
|
|
2182
|
+
if (owners.length !== 1 || owners[0].element !== expectedOwner || this.root.getByKey(expectedOwner.key) !== expectedOwner) return true;
|
|
2183
|
+
let session;
|
|
2184
|
+
session = capability.beginContentEditing({
|
|
2185
|
+
submit: (value) => {
|
|
2186
|
+
const change = session.resolveTextChange(value);
|
|
2187
|
+
if (change && this.contentSession === session && this.root.getByKey(session.ownerKey) === expectedOwner) this.applyAuthoredBatch([change], [], this.contentHistory);
|
|
2188
|
+
},
|
|
2189
|
+
finish: () => this.finishContentEditing(false)
|
|
2190
|
+
});
|
|
2191
|
+
const current = this.owners === owners && this.root.getByKey(expectedOwner.key) === expectedOwner && this.contentSession === null;
|
|
2192
|
+
if (!session) return !current;
|
|
2193
|
+
if (!current) {
|
|
2194
|
+
session.close();
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
this.contentSession = session;
|
|
2198
|
+
this.contentHistory = this.history.openScope(this.selection);
|
|
2199
|
+
this.invalidateOverlay();
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
finishContentEditing(discard) {
|
|
2203
|
+
const session = this.contentSession;
|
|
2204
|
+
if (!session) return;
|
|
2205
|
+
discard ||= this.root.getByKey(session.ownerKey) === void 0;
|
|
2206
|
+
const scope = this.contentHistory;
|
|
2207
|
+
this.contentSession = null;
|
|
2208
|
+
this.contentHistory = void 0;
|
|
2209
|
+
try {
|
|
2210
|
+
session.close();
|
|
2211
|
+
if (this.owners.some(({ element }) => element.activeRoot !== this.root)) {
|
|
2212
|
+
this.replaceSelection(this.selection);
|
|
2213
|
+
this.pendingSelectionRepair = this.owners;
|
|
2214
|
+
}
|
|
2215
|
+
this.root.frameRuntime.drain();
|
|
2216
|
+
if (discard) scope?.discard();
|
|
2217
|
+
else this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2218
|
+
} finally {
|
|
2219
|
+
this.invalidateOverlay();
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
startLineDrawing(tool) {
|
|
2223
|
+
if (this.lineSession) this.cancelLineDrawing();
|
|
2224
|
+
if (this.lineTreeSession) this.cancelLineDrawing();
|
|
2225
|
+
this.finishCurrentInteraction();
|
|
2226
|
+
const history = this.history.openScope(this.selection);
|
|
2227
|
+
const confirmed = [];
|
|
2228
|
+
this.setSelection([]);
|
|
2229
|
+
this.lineSession = {
|
|
2230
|
+
tool,
|
|
2231
|
+
resultType: tool.getResultTypeForController(),
|
|
2232
|
+
tailDecoration: tool.getTailDecorationForController(),
|
|
2233
|
+
history,
|
|
2234
|
+
confirmed,
|
|
2235
|
+
headTarget: void 0,
|
|
2236
|
+
tailTarget: void 0,
|
|
2237
|
+
preview: null,
|
|
2238
|
+
snap: null
|
|
2239
|
+
};
|
|
2240
|
+
if (this.root.enablePorts && this.select.connectionGesture !== false) this.snapTool?.start(this.editorElements, void 0, true);
|
|
2241
|
+
this.invalidateOverlay();
|
|
2242
|
+
}
|
|
2243
|
+
startLineTreeBranchDrawing(tool, owner, activeSubpathId) {
|
|
2244
|
+
if (this.lineSession || this.lineTreeSession) this.cancelLineDrawing();
|
|
2245
|
+
this.finishCurrentInteraction();
|
|
2246
|
+
this.exitPointState();
|
|
2247
|
+
const tailDecoration = tool.getTailDecorationForController();
|
|
2248
|
+
if (activeSubpathId !== void 0) {
|
|
2249
|
+
const source = owner.getBranchTailSource(activeSubpathId);
|
|
2250
|
+
this.beginLineTreeBranchDraft(tool, owner, source, tailDecoration);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const source = this.pointerWorld ? owner.resolveBranchTailAtWorldPoint(this.pointerWorld) : void 0;
|
|
2254
|
+
if (source) {
|
|
2255
|
+
this.beginLineTreeBranchDraft(tool, owner, source, tailDecoration);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
this.lineTreeSession = {
|
|
2259
|
+
kind: "source-pick",
|
|
2260
|
+
tool,
|
|
2261
|
+
owner,
|
|
2262
|
+
tailDecoration,
|
|
2263
|
+
hover: null
|
|
2264
|
+
};
|
|
2265
|
+
this.invalidateOverlay();
|
|
2266
|
+
}
|
|
2267
|
+
updateLineTreeSourceHover(point) {
|
|
2268
|
+
const session = this.lineTreeSession;
|
|
2269
|
+
if (session?.kind !== "source-pick") return;
|
|
2270
|
+
const previousSubpathId = session.hover?.subpathId;
|
|
2271
|
+
session.hover = session.owner.resolveBranchTailAtWorldPoint(point) ?? null;
|
|
2272
|
+
if (session.hover?.subpathId !== previousSubpathId) this.invalidateOverlay();
|
|
2273
|
+
return session.hover;
|
|
2274
|
+
}
|
|
2275
|
+
confirmLineTreeSource(point) {
|
|
2276
|
+
const session = this.lineTreeSession;
|
|
2277
|
+
if (session?.kind !== "source-pick") return;
|
|
2278
|
+
const source = session.owner.resolveBranchTailAtWorldPoint(point);
|
|
2279
|
+
if (!source) return;
|
|
2280
|
+
this.beginLineTreeBranchDraft(session.tool, session.owner, source, session.tailDecoration);
|
|
2281
|
+
}
|
|
2282
|
+
beginLineTreeBranchDraft(tool, owner, source, tailDecoration) {
|
|
2283
|
+
this.lineTreeSession = {
|
|
2284
|
+
kind: "branch-draw",
|
|
2285
|
+
tool,
|
|
2286
|
+
owner,
|
|
2287
|
+
source,
|
|
2288
|
+
tailDecoration,
|
|
2289
|
+
confirmed: [source.world],
|
|
2290
|
+
tailTarget: void 0,
|
|
2291
|
+
preview: null,
|
|
2292
|
+
snap: null
|
|
2293
|
+
};
|
|
2294
|
+
if (this.root.enablePorts && this.select.connectionGesture !== false) this.snapTool?.start(this.editorElements.concat(owner), void 0, true);
|
|
2295
|
+
this.invalidateOverlay();
|
|
2296
|
+
}
|
|
2297
|
+
updateLineTreePreview(point) {
|
|
2298
|
+
const session = this.lineTreeSession;
|
|
2299
|
+
if (session?.kind !== "branch-draw") return;
|
|
2300
|
+
session.preview = {
|
|
2301
|
+
x: point.x,
|
|
2302
|
+
y: point.y
|
|
2303
|
+
};
|
|
2304
|
+
session.snap = this.probeLineTreeTail(session, point).snap;
|
|
2305
|
+
this.invalidateOverlay();
|
|
2306
|
+
}
|
|
2307
|
+
probeLineTreeTail(session, point) {
|
|
2308
|
+
if (!this.root.enablePorts || this.select.connectionGesture === false) return NO_PORT_SNAP$1;
|
|
2309
|
+
return this.snapTool?.probePortSnap(point.x, point.y, "input", this.editorElements.concat(session.owner)) ?? NO_PORT_SNAP$1;
|
|
2310
|
+
}
|
|
2311
|
+
confirmLineTreePoint(point) {
|
|
2312
|
+
const session = this.lineTreeSession;
|
|
2313
|
+
if (session?.kind !== "branch-draw") return;
|
|
2314
|
+
const probe = this.probeLineTreeTail(session, point);
|
|
2315
|
+
if (isBlockedPortProbe(probe)) return;
|
|
2316
|
+
const snap = probe.snap;
|
|
2317
|
+
session.snap = snap;
|
|
2318
|
+
session.tailTarget = snap ? {
|
|
2319
|
+
elementKey: snap.elementKey,
|
|
2320
|
+
portId: snap.portId
|
|
2321
|
+
} : void 0;
|
|
2322
|
+
session.confirmed.push(snap ? {
|
|
2323
|
+
x: snap.x,
|
|
2324
|
+
y: snap.y
|
|
2325
|
+
} : {
|
|
2326
|
+
x: point.x,
|
|
2327
|
+
y: point.y
|
|
2328
|
+
});
|
|
2329
|
+
if (probe.state !== null) {
|
|
2330
|
+
this.confirmLineTreeBranchDrawing();
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
this.invalidateOverlay();
|
|
2334
|
+
}
|
|
2335
|
+
confirmLineTreeBranchDrawing() {
|
|
2336
|
+
const session = this.lineTreeSession;
|
|
2337
|
+
if (session?.kind !== "branch-draw" || session.confirmed.length < 2) return;
|
|
2338
|
+
const points = session.confirmed.slice(1).map((point) => {
|
|
2339
|
+
const local = session.owner.transform.toLocal(point);
|
|
2340
|
+
if (!local) throw new Error("LineTool cannot extend a LineTree in a singular frame");
|
|
2341
|
+
return {
|
|
2342
|
+
x: local.x,
|
|
2343
|
+
y: local.y
|
|
2344
|
+
};
|
|
2345
|
+
});
|
|
2346
|
+
const topology = moveLineTreePoint(session.owner.topology, session.source.anchorId, session.source.local, void 0);
|
|
2347
|
+
const autoConnect = this.root.enablePorts && this.select.connectionGesture === "auto";
|
|
2348
|
+
const added = addLineTreeBranchAtTail(topology, session.source.subpathId, points, {
|
|
2349
|
+
...DEFAULT_LINE_TREE_BRANCH_STYLE,
|
|
2350
|
+
tailDecoration: session.tailDecoration
|
|
2351
|
+
}, autoConnect && session.tailTarget ? {
|
|
2352
|
+
kind: "endpoint-binding",
|
|
2353
|
+
target: session.tailTarget
|
|
2354
|
+
} : void 0);
|
|
2355
|
+
const tailAnchorId = added.topology.branches.find(({ subpathId }) => subpathId === added.subpathId).vertices.at(-1).anchorId;
|
|
2356
|
+
const scope = this.history.openScope(this.selection);
|
|
2357
|
+
this.applyAuthoredBatch([{
|
|
2358
|
+
owner: session.owner,
|
|
2359
|
+
patch: { topology: added.topology }
|
|
2360
|
+
}], [], scope);
|
|
2361
|
+
this.root.frameRuntime.drain();
|
|
2362
|
+
if (this.lineTreeSession !== session) {
|
|
2363
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
this.lineTreeSession = null;
|
|
2367
|
+
this.snapTool?.stop();
|
|
2368
|
+
const enteredPoint = this.setPointState(session.owner.key, {
|
|
2369
|
+
kind: "subpath",
|
|
2370
|
+
subpathId: added.subpathId
|
|
2371
|
+
});
|
|
2372
|
+
this.invalidateOverlay();
|
|
2373
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2374
|
+
const connectionGesture = this.select.connectionGesture;
|
|
2375
|
+
if (this.root.enablePorts && enteredPoint && session.tailTarget && typeof connectionGesture === "function") connectionGesture(createToolsConnectionIntent(this.root, this.history, {
|
|
2376
|
+
elementKey: session.owner.key,
|
|
2377
|
+
anchorId: tailAnchorId
|
|
2378
|
+
}, session.tailTarget));
|
|
2379
|
+
}
|
|
2380
|
+
updateLinePreview(point) {
|
|
2381
|
+
const session = this.lineSession;
|
|
2382
|
+
session.preview = {
|
|
2383
|
+
x: point.x,
|
|
2384
|
+
y: point.y
|
|
2385
|
+
};
|
|
2386
|
+
session.snap = this.probeLineAnchor(session, point).snap;
|
|
2387
|
+
this.invalidateOverlay();
|
|
2388
|
+
}
|
|
2389
|
+
probeLineAnchor(session, point) {
|
|
2390
|
+
if (!this.root.enablePorts || this.select.connectionGesture === false) return NO_PORT_SNAP$1;
|
|
2391
|
+
return this.snapTool?.probePortSnap(point.x, point.y, session.confirmed.length === 0 ? "output" : "input", this.editorElements) ?? NO_PORT_SNAP$1;
|
|
2392
|
+
}
|
|
2393
|
+
confirmLinePoint(point) {
|
|
2394
|
+
const session = this.lineSession;
|
|
2395
|
+
const probe = this.probeLineAnchor(session, point);
|
|
2396
|
+
if (isBlockedPortProbe(probe)) return;
|
|
2397
|
+
const snap = probe.snap;
|
|
2398
|
+
session.snap = snap;
|
|
2399
|
+
const confirmed = snap ? {
|
|
2400
|
+
x: snap.x,
|
|
2401
|
+
y: snap.y
|
|
2402
|
+
} : {
|
|
2403
|
+
x: point.x,
|
|
2404
|
+
y: point.y
|
|
2405
|
+
};
|
|
2406
|
+
const target = snap ? {
|
|
2407
|
+
elementKey: snap.elementKey,
|
|
2408
|
+
portId: snap.portId
|
|
2409
|
+
} : void 0;
|
|
2410
|
+
if (session.confirmed.length === 0) session.headTarget = target;
|
|
2411
|
+
else session.tailTarget = target;
|
|
2412
|
+
session.confirmed.push(confirmed);
|
|
2413
|
+
if (probe.state !== null && session.confirmed.length >= 2) {
|
|
2414
|
+
this.confirmLineDrawing();
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
session.preview = null;
|
|
2418
|
+
session.snap = null;
|
|
2419
|
+
this.invalidateOverlay();
|
|
2420
|
+
}
|
|
2421
|
+
stopLineDrawing() {
|
|
2422
|
+
if (this.lineTreeSession?.kind === "branch-draw") {
|
|
2423
|
+
if (this.lineTreeSession.confirmed.length >= 2) this.confirmLineTreeBranchDrawing();
|
|
2424
|
+
else this.cancelLineDrawing();
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
if (this.lineTreeSession) {
|
|
2428
|
+
this.cancelLineDrawing();
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (this.lineSession && this.lineSession.confirmed.length >= 2) this.confirmLineDrawing();
|
|
2432
|
+
else this.cancelLineDrawing();
|
|
2433
|
+
}
|
|
2434
|
+
cancelLineDrawing() {
|
|
2435
|
+
const session = this.lineSession;
|
|
2436
|
+
if (!session && !this.lineTreeSession) return;
|
|
2437
|
+
session?.history?.discard();
|
|
2438
|
+
this.snapTool?.stop();
|
|
2439
|
+
this.lineSession = null;
|
|
2440
|
+
this.lineTreeSession = null;
|
|
2441
|
+
this.invalidateOverlay();
|
|
2442
|
+
}
|
|
2443
|
+
confirmLineDrawing() {
|
|
2444
|
+
const session = this.lineSession;
|
|
2445
|
+
const parent = session.tool.getContentLayerForController();
|
|
2446
|
+
const localPoints = session.confirmed.map((point) => {
|
|
2447
|
+
const local = parent instanceof Element ? parent.transform.toLocal(point) : point;
|
|
2448
|
+
if (!local) throw new Error("LineTool cannot place a line in a singular parent");
|
|
2449
|
+
return {
|
|
2450
|
+
x: local.x,
|
|
2451
|
+
y: local.y
|
|
2452
|
+
};
|
|
2453
|
+
});
|
|
2454
|
+
const autoConnect = this.root.enablePorts && this.select.connectionGesture === "auto";
|
|
2455
|
+
const line = session.resultType === "line" ? new Line({
|
|
2456
|
+
path: localPoints,
|
|
2457
|
+
relations: {
|
|
2458
|
+
head: autoConnect && session.headTarget ? {
|
|
2459
|
+
kind: "endpoint-binding",
|
|
2460
|
+
target: session.headTarget
|
|
2461
|
+
} : void 0,
|
|
2462
|
+
tail: autoConnect && session.tailTarget ? {
|
|
2463
|
+
kind: "endpoint-binding",
|
|
2464
|
+
target: session.tailTarget
|
|
2465
|
+
} : void 0
|
|
2466
|
+
},
|
|
2467
|
+
strokeWidth: 2,
|
|
2468
|
+
tailDecoration: session.tailDecoration
|
|
2469
|
+
}) : new LineTree({ topology: createLineTreeTopology(localPoints, {
|
|
2470
|
+
...DEFAULT_LINE_TREE_BRANCH_STYLE,
|
|
2471
|
+
tailDecoration: session.tailDecoration
|
|
2472
|
+
}, {
|
|
2473
|
+
head: autoConnect && session.headTarget ? {
|
|
2474
|
+
kind: "endpoint-binding",
|
|
2475
|
+
target: session.headTarget
|
|
2476
|
+
} : void 0,
|
|
2477
|
+
tail: autoConnect && session.tailTarget ? {
|
|
2478
|
+
kind: "endpoint-binding",
|
|
2479
|
+
target: session.tailTarget
|
|
2480
|
+
} : void 0
|
|
2481
|
+
}) });
|
|
2482
|
+
const bindable = line.getBindableEndpoints();
|
|
2483
|
+
const headSource = bindable[0]?.ref;
|
|
2484
|
+
const tailSource = bindable.at(-1)?.ref;
|
|
2485
|
+
const structure = [{
|
|
2486
|
+
owner: parent,
|
|
2487
|
+
children: [...parent.children, line]
|
|
2488
|
+
}];
|
|
2489
|
+
this.applyAuthoredBatch([], structure, session.history);
|
|
2490
|
+
this.root.frameRuntime.drain();
|
|
2491
|
+
this.snapTool?.stop();
|
|
2492
|
+
this.lineSession = null;
|
|
2493
|
+
this.invalidateOverlay();
|
|
2494
|
+
this.publishConfiguredChange(session.history?.finish(this.selection));
|
|
2495
|
+
const connectionGesture = this.select.connectionGesture;
|
|
2496
|
+
if (!this.root.enablePorts || typeof connectionGesture !== "function") return;
|
|
2497
|
+
if (session.headTarget && headSource) connectionGesture(createToolsConnectionIntent(this.root, this.history, headSource, session.headTarget));
|
|
2498
|
+
if (session.tailTarget && tailSource && (!headSource || tailSource.anchorId !== headSource.anchorId)) connectionGesture(createToolsConnectionIntent(this.root, this.history, tailSource, session.tailTarget));
|
|
2499
|
+
}
|
|
2500
|
+
getLineDrawingView(tool) {
|
|
2501
|
+
const session = this.lineSession;
|
|
2502
|
+
if (session?.tool === tool) return session;
|
|
2503
|
+
const lineTree = this.lineTreeSession;
|
|
2504
|
+
return lineTree?.kind === "branch-draw" && lineTree.tool === tool ? lineTree : null;
|
|
2505
|
+
}
|
|
2506
|
+
hasLineToolSession(tool) {
|
|
2507
|
+
return this.lineSession?.tool === tool || this.lineTreeSession?.tool === tool;
|
|
2508
|
+
}
|
|
2509
|
+
applyCommand(ownerEffects, structureEffects, scope, selectionAfter, options) {
|
|
2510
|
+
const selectionOwners = this.owners;
|
|
2511
|
+
let installedSelection;
|
|
2512
|
+
this.applyAuthoredBatch(ownerEffects, structureEffects, scope);
|
|
2513
|
+
if (options?.installSelection ? options.installSelection() : this.owners === selectionOwners || this.pendingSelectionRepair === this.owners) {
|
|
2514
|
+
this.replaceSelection(selectionAfter);
|
|
2515
|
+
if (options?.installSelection) installedSelection = {
|
|
2516
|
+
owners: this.owners,
|
|
2517
|
+
interaction: this.controllerState
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
options?.beforeDrain?.();
|
|
2521
|
+
if (this.select.interactionController !== this) {
|
|
2522
|
+
scope?.discard();
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
this.root.frameRuntime.drain();
|
|
2526
|
+
this.publishConfiguredChange(scope?.finish(this.selection));
|
|
2527
|
+
options?.afterHistory?.();
|
|
2528
|
+
if (!options?.installSelection || installedSelection?.owners === this.owners && installedSelection.interaction === this.controllerState) this.notifySelect("select:end");
|
|
2529
|
+
}
|
|
2530
|
+
finishCurrentInteraction() {
|
|
2531
|
+
if (this.contentSession) {
|
|
2532
|
+
this.finishContentEditing(false);
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
if (this.lineSession) {
|
|
2536
|
+
this.cancelLineDrawing();
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (this.lineTreeSession) {
|
|
2540
|
+
this.cancelLineDrawing();
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const gesture = this.activeGesture;
|
|
2544
|
+
const pointGesture = this.activePointGesture;
|
|
2545
|
+
if (pointGesture) {
|
|
2546
|
+
pointGesture.finish(void 0, pointGesture.connectionRole !== null);
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
gesture?.finish();
|
|
2550
|
+
}
|
|
2551
|
+
destroy() {
|
|
2552
|
+
try {
|
|
2553
|
+
const pointGesture = this.activePointGesture;
|
|
2554
|
+
if (pointGesture) pointGesture.finish(void 0, pointGesture.connectionRole !== null);
|
|
2555
|
+
else this.finishCurrentInteraction();
|
|
2556
|
+
} finally {
|
|
2557
|
+
for (const cleanup of this.cleanups.splice(0)) cleanup();
|
|
2558
|
+
this.pendingSelectionRepair = null;
|
|
2559
|
+
this.frameFlush.setOverlayClient(null);
|
|
2560
|
+
this.history.bindSelection(null);
|
|
2561
|
+
this.snapTool?.stop();
|
|
2562
|
+
if (this.root.inject(editorControllerKey) === this) this.root.provide(editorControllerKey, void 0);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
function getEditorInteractionController(root) {
|
|
2567
|
+
return root.inject(editorControllerKey);
|
|
2568
|
+
}
|
|
2569
|
+
//#endregion
|
|
2570
|
+
//#region packages/tools/src/history/index.ts
|
|
2571
|
+
function cloneHistoryValue(value) {
|
|
2572
|
+
return value !== null && typeof value === "object" ? structuredClone(value) : value;
|
|
2573
|
+
}
|
|
2574
|
+
function selectionKeys(elements) {
|
|
2575
|
+
return elements.map((element) => element.key);
|
|
2576
|
+
}
|
|
2577
|
+
function releaseHistoryEntry(entry) {
|
|
2578
|
+
entry.changes.length = 0;
|
|
2579
|
+
}
|
|
2580
|
+
var ActiveHistoryScope = class {
|
|
2581
|
+
properties = /* @__PURE__ */ new Map();
|
|
2582
|
+
placements = /* @__PURE__ */ new Map();
|
|
2583
|
+
changes;
|
|
2584
|
+
completed = false;
|
|
2585
|
+
constructor(manager, selectionBefore, recordHistory) {
|
|
2586
|
+
this.manager = manager;
|
|
2587
|
+
this.selectionBefore = selectionBefore;
|
|
2588
|
+
this.recordHistory = recordHistory;
|
|
2589
|
+
}
|
|
2590
|
+
record(change) {
|
|
2591
|
+
if (this.completed || !this.recordHistory) return;
|
|
2592
|
+
(this.changes ??= []).push(change);
|
|
2593
|
+
}
|
|
2594
|
+
recordPropertyChange(owner, key, before) {
|
|
2595
|
+
if (this.completed) return;
|
|
2596
|
+
let fields = this.properties.get(owner);
|
|
2597
|
+
if (!fields) {
|
|
2598
|
+
fields = /* @__PURE__ */ new Map();
|
|
2599
|
+
this.properties.set(owner, fields);
|
|
2600
|
+
}
|
|
2601
|
+
if (!fields.has(key)) fields.set(key, {
|
|
2602
|
+
before,
|
|
2603
|
+
value: void 0
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
recordPlacementChange(node, beforeOwner, beforeIndex) {
|
|
2607
|
+
if (this.completed) return;
|
|
2608
|
+
if (!this.placements.has(node)) this.placements.set(node, { before: {
|
|
2609
|
+
owner: beforeOwner,
|
|
2610
|
+
index: beforeIndex
|
|
2611
|
+
} });
|
|
2612
|
+
}
|
|
2613
|
+
finish(selectionAfter) {
|
|
2614
|
+
if (this.completed) return false;
|
|
2615
|
+
this.completed = true;
|
|
2616
|
+
const properties = this.recordHistory ? [] : null;
|
|
2617
|
+
const lineTrees = this.recordHistory ? [] : null;
|
|
2618
|
+
let canvasChanged = false;
|
|
2619
|
+
for (const [owner, fields] of this.properties) {
|
|
2620
|
+
Element.readConfiguredPropertyValues(owner, fields);
|
|
2621
|
+
for (const [key, change] of fields) {
|
|
2622
|
+
const after = change.value;
|
|
2623
|
+
if (Object.is(change.before, after)) continue;
|
|
2624
|
+
if (key === "topology" && owner instanceof LineTree) {
|
|
2625
|
+
const delta = createLineTreeTopologyDelta(change.before, after);
|
|
2626
|
+
if (!delta) continue;
|
|
2627
|
+
canvasChanged = true;
|
|
2628
|
+
lineTrees?.push({
|
|
2629
|
+
owner,
|
|
2630
|
+
delta
|
|
2631
|
+
});
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
canvasChanged = true;
|
|
2635
|
+
properties?.push({
|
|
2636
|
+
owner,
|
|
2637
|
+
key,
|
|
2638
|
+
before: cloneHistoryValue(change.before),
|
|
2639
|
+
after: cloneHistoryValue(after)
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const placements = this.recordHistory ? [] : null;
|
|
2644
|
+
for (const [node, change] of this.placements) {
|
|
2645
|
+
const afterOwner = node.parent;
|
|
2646
|
+
const afterIndex = afterOwner ? afterOwner.children.indexOf(node) : -1;
|
|
2647
|
+
if (change.before.owner === afterOwner && change.before.index === afterIndex) continue;
|
|
2648
|
+
canvasChanged = true;
|
|
2649
|
+
placements?.push({
|
|
2650
|
+
node,
|
|
2651
|
+
before: change.before,
|
|
2652
|
+
after: {
|
|
2653
|
+
owner: afterOwner,
|
|
2654
|
+
index: afterIndex
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
if (!canvasChanged && (!this.changes || this.changes.length === 0)) return false;
|
|
2659
|
+
if (this.recordHistory) {
|
|
2660
|
+
const changes = this.changes ?? [];
|
|
2661
|
+
this.changes = void 0;
|
|
2662
|
+
this.manager.push({
|
|
2663
|
+
properties,
|
|
2664
|
+
lineTrees,
|
|
2665
|
+
placements,
|
|
2666
|
+
changes,
|
|
2667
|
+
selectionBefore: this.selectionBefore,
|
|
2668
|
+
selectionAfter: selectionKeys(selectionAfter)
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
return canvasChanged;
|
|
2672
|
+
}
|
|
2673
|
+
discard() {
|
|
2674
|
+
if (this.completed) return;
|
|
2675
|
+
this.completed = true;
|
|
2676
|
+
if (this.changes) {
|
|
2677
|
+
this.changes.length = 0;
|
|
2678
|
+
this.changes = void 0;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
var HistoryManager = class {
|
|
2683
|
+
enabled = true;
|
|
2684
|
+
undoStack = [];
|
|
2685
|
+
redoStack = [];
|
|
2686
|
+
selectionAdapter = null;
|
|
2687
|
+
constructor(root, limit = 50) {
|
|
2688
|
+
this.root = root;
|
|
2689
|
+
this.limit = limit;
|
|
2690
|
+
}
|
|
2691
|
+
get undoCount() {
|
|
2692
|
+
return this.undoStack.length;
|
|
2693
|
+
}
|
|
2694
|
+
get redoCount() {
|
|
2695
|
+
return this.redoStack.length;
|
|
2696
|
+
}
|
|
2697
|
+
bindSelection(adapter) {
|
|
2698
|
+
this.selectionAdapter = adapter;
|
|
2699
|
+
}
|
|
2700
|
+
openScope(selectionBefore = this.selectionAdapter?.getSelection() ?? []) {
|
|
2701
|
+
const recordHistory = this.enabled;
|
|
2702
|
+
if (!recordHistory && !this.selectionAdapter?.hasConfiguredChangeListener()) return void 0;
|
|
2703
|
+
return new ActiveHistoryScope(this, recordHistory ? selectionKeys(selectionBefore) : null, recordHistory);
|
|
2704
|
+
}
|
|
2705
|
+
/** @internal Publishes one completed actual configured semantic change. */
|
|
2706
|
+
publishConfiguredChange(changed) {
|
|
2707
|
+
if (changed && this.selectionAdapter?.hasConfiguredChangeListener()) this.selectionAdapter.publishConfiguredChange();
|
|
2708
|
+
}
|
|
2709
|
+
/** @internal 只由存在Canvas净变化或外部change的scope完成时写入。 */
|
|
2710
|
+
push(entry) {
|
|
2711
|
+
this.undoStack.push(entry);
|
|
2712
|
+
if (this.undoStack.length > this.limit) releaseHistoryEntry(this.undoStack.shift());
|
|
2713
|
+
for (const redo of this.redoStack) releaseHistoryEntry(redo);
|
|
2714
|
+
this.redoStack.length = 0;
|
|
2715
|
+
}
|
|
2716
|
+
clear() {
|
|
2717
|
+
for (const entry of this.undoStack) releaseHistoryEntry(entry);
|
|
2718
|
+
for (const entry of this.redoStack) releaseHistoryEntry(entry);
|
|
2719
|
+
this.undoStack.length = 0;
|
|
2720
|
+
this.redoStack.length = 0;
|
|
2721
|
+
}
|
|
2722
|
+
buildPropertyChanges(entry, direction) {
|
|
2723
|
+
const patches = /* @__PURE__ */ new Map();
|
|
2724
|
+
for (const change of entry.properties) {
|
|
2725
|
+
let patch = patches.get(change.owner);
|
|
2726
|
+
if (!patch) {
|
|
2727
|
+
patch = {};
|
|
2728
|
+
patches.set(change.owner, patch);
|
|
2729
|
+
}
|
|
2730
|
+
patch[change.key] = cloneHistoryValue(change[direction]);
|
|
2731
|
+
}
|
|
2732
|
+
return [...patches].map(([owner, patch]) => ({
|
|
2733
|
+
owner,
|
|
2734
|
+
patch
|
|
2735
|
+
}));
|
|
2736
|
+
}
|
|
2737
|
+
buildStructureChanges(entry, direction) {
|
|
2738
|
+
const parents = /* @__PURE__ */ new Set();
|
|
2739
|
+
for (const change of entry.placements) {
|
|
2740
|
+
if (change.node.parent) parents.add(change.node.parent);
|
|
2741
|
+
const target = change[direction].owner;
|
|
2742
|
+
if (target) parents.add(target);
|
|
2743
|
+
}
|
|
2744
|
+
const children = /* @__PURE__ */ new Map();
|
|
2745
|
+
for (const parent of parents) children.set(parent, [...parent.children]);
|
|
2746
|
+
for (const change of entry.placements) {
|
|
2747
|
+
const current = change.node.parent;
|
|
2748
|
+
if (!current) continue;
|
|
2749
|
+
const list = children.get(current);
|
|
2750
|
+
const index = list.indexOf(change.node);
|
|
2751
|
+
if (index !== -1) list.splice(index, 1);
|
|
2752
|
+
}
|
|
2753
|
+
const insertions = /* @__PURE__ */ new Map();
|
|
2754
|
+
for (const change of entry.placements) {
|
|
2755
|
+
const target = change[direction];
|
|
2756
|
+
if (!target.owner) continue;
|
|
2757
|
+
const list = insertions.get(target.owner);
|
|
2758
|
+
if (list) list.push(change);
|
|
2759
|
+
else insertions.set(target.owner, [change]);
|
|
2760
|
+
}
|
|
2761
|
+
for (const [parent, changes] of insertions) {
|
|
2762
|
+
changes.sort((left, right) => left[direction].index - right[direction].index);
|
|
2763
|
+
const list = children.get(parent);
|
|
2764
|
+
for (const change of changes) list.splice(change[direction].index, 0, change.node);
|
|
2765
|
+
}
|
|
2766
|
+
return [...children].map(([owner, finalChildren]) => ({
|
|
2767
|
+
owner,
|
|
2768
|
+
children: finalChildren
|
|
2769
|
+
}));
|
|
2770
|
+
}
|
|
2771
|
+
buildLineTreeChanges(entry, direction) {
|
|
2772
|
+
return entry.lineTrees.map(({ owner, delta }) => ({
|
|
2773
|
+
owner,
|
|
2774
|
+
patch: { topology: applyLineTreeTopologyDelta(owner.topology, delta, direction) }
|
|
2775
|
+
}));
|
|
2776
|
+
}
|
|
2777
|
+
replay(entry, direction, selection) {
|
|
2778
|
+
const properties = this.buildPropertyChanges(entry, direction);
|
|
2779
|
+
properties.push(...this.buildLineTreeChanges(entry, direction));
|
|
2780
|
+
const structure = this.buildStructureChanges(entry, direction);
|
|
2781
|
+
let configuredChanged = false;
|
|
2782
|
+
const actualChange = this.selectionAdapter?.hasConfiguredChangeListener() ? {
|
|
2783
|
+
recordPropertyChange: () => {
|
|
2784
|
+
configuredChanged = true;
|
|
2785
|
+
},
|
|
2786
|
+
recordPlacementChange: () => {
|
|
2787
|
+
configuredChanged = true;
|
|
2788
|
+
}
|
|
2789
|
+
} : void 0;
|
|
2790
|
+
this.root.applyChangeBatch(properties, structure, actualChange);
|
|
2791
|
+
for (const change of entry.changes) if (direction === "before") change.undo();
|
|
2792
|
+
else change.redo();
|
|
2793
|
+
const selected = [];
|
|
2794
|
+
for (const key of selection) {
|
|
2795
|
+
const element = this.root.getByKey(key);
|
|
2796
|
+
if (element instanceof Element) selected.push(element);
|
|
2797
|
+
}
|
|
2798
|
+
this.selectionAdapter?.restoreSelection(selected);
|
|
2799
|
+
this.root.frameRuntime.drain();
|
|
2800
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2801
|
+
for (const change of entry.properties) affected.add(change.owner);
|
|
2802
|
+
for (const change of entry.lineTrees) affected.add(change.owner);
|
|
2803
|
+
for (const change of entry.placements) if (change.node instanceof Element) affected.add(change.node);
|
|
2804
|
+
return {
|
|
2805
|
+
affected: [...affected],
|
|
2806
|
+
configuredChanged
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* 线性撤销栈顶 entry。当前调用返回前,不得同步重入本 manager 的
|
|
2811
|
+
* undo、redo、clear 或 push;HistoryChange 只恢复自己拥有的数据。
|
|
2812
|
+
*/
|
|
2813
|
+
undo() {
|
|
2814
|
+
this.selectionAdapter?.finishInteraction();
|
|
2815
|
+
const entry = this.undoStack.at(-1);
|
|
2816
|
+
if (!entry) return;
|
|
2817
|
+
const result = this.replay(entry, "before", entry.selectionBefore);
|
|
2818
|
+
this.undoStack.pop();
|
|
2819
|
+
this.redoStack.push(entry);
|
|
2820
|
+
this.selectionAdapter?.completeReplay("select:undo", result.affected, result.configuredChanged);
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* 线性重做栈顶 entry。当前调用返回前,不得同步重入本 manager 的
|
|
2824
|
+
* undo、redo、clear 或 push;HistoryChange 只恢复自己拥有的数据。
|
|
2825
|
+
*/
|
|
2826
|
+
redo() {
|
|
2827
|
+
this.selectionAdapter?.finishInteraction();
|
|
2828
|
+
const entry = this.redoStack.at(-1);
|
|
2829
|
+
if (!entry) return;
|
|
2830
|
+
const result = this.replay(entry, "after", entry.selectionAfter);
|
|
2831
|
+
this.redoStack.pop();
|
|
2832
|
+
this.undoStack.push(entry);
|
|
2833
|
+
this.selectionAdapter?.completeReplay("select:redo", result.affected, result.configuredChanged);
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
//#endregion
|
|
2837
|
+
//#region packages/tools/src/select/align.ts
|
|
2838
|
+
function resolveOffsets(rects, type) {
|
|
2839
|
+
if (type === "justify-start") {
|
|
2840
|
+
const target = Math.min(...rects.map(({ left }) => left));
|
|
2841
|
+
rects.forEach((rect) => {
|
|
2842
|
+
rect.dx = target - rect.left;
|
|
2843
|
+
});
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
if (type === "justify-center") {
|
|
2847
|
+
const target = (Math.min(...rects.map((rect) => rect.left)) + Math.max(...rects.map((rect) => rect.left + rect.width))) / 2;
|
|
2848
|
+
rects.forEach((rect) => {
|
|
2849
|
+
rect.dx = target - (rect.left + rect.width / 2);
|
|
2850
|
+
});
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
if (type === "justify-end") {
|
|
2854
|
+
const target = Math.max(...rects.map((rect) => rect.left + rect.width));
|
|
2855
|
+
rects.forEach((rect) => {
|
|
2856
|
+
rect.dx = target - (rect.left + rect.width);
|
|
2857
|
+
});
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
if (type === "justify-between") {
|
|
2861
|
+
rects.sort((left, right) => left.left - right.left);
|
|
2862
|
+
const start = rects[0].left;
|
|
2863
|
+
const end = rects.at(-1);
|
|
2864
|
+
const total = rects.reduce((sum, rect) => sum + rect.width, 0);
|
|
2865
|
+
const gap = (end.left + end.width - start - total) / (rects.length - 1);
|
|
2866
|
+
let x = start;
|
|
2867
|
+
for (const rect of rects) {
|
|
2868
|
+
rect.dx = x - rect.left;
|
|
2869
|
+
x += rect.width + gap;
|
|
2870
|
+
}
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
if (type === "align-start") {
|
|
2874
|
+
const target = Math.min(...rects.map(({ top }) => top));
|
|
2875
|
+
rects.forEach((rect) => {
|
|
2876
|
+
rect.dy = target - rect.top;
|
|
2877
|
+
});
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
if (type === "align-center") {
|
|
2881
|
+
const target = (Math.min(...rects.map((rect) => rect.top)) + Math.max(...rects.map((rect) => rect.top + rect.height))) / 2;
|
|
2882
|
+
rects.forEach((rect) => {
|
|
2883
|
+
rect.dy = target - (rect.top + rect.height / 2);
|
|
2884
|
+
});
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (type === "align-end") {
|
|
2888
|
+
const target = Math.max(...rects.map((rect) => rect.top + rect.height));
|
|
2889
|
+
rects.forEach((rect) => {
|
|
2890
|
+
rect.dy = target - (rect.top + rect.height);
|
|
2891
|
+
});
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
rects.sort((left, right) => left.top - right.top);
|
|
2895
|
+
const start = rects[0].top;
|
|
2896
|
+
const end = rects.at(-1);
|
|
2897
|
+
const total = rects.reduce((sum, rect) => sum + rect.height, 0);
|
|
2898
|
+
const gap = (end.top + end.height - start - total) / (rects.length - 1);
|
|
2899
|
+
let y = start;
|
|
2900
|
+
for (const rect of rects) {
|
|
2901
|
+
rect.dy = y - rect.top;
|
|
2902
|
+
y += rect.height + gap;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
function alignElements(select, type) {
|
|
2906
|
+
const controller = select.interactionController;
|
|
2907
|
+
let elements = controller.selection;
|
|
2908
|
+
if (elements.length < 2 || type.endsWith("between") && elements.length < 3) return;
|
|
2909
|
+
controller.finishCurrentInteraction();
|
|
2910
|
+
controller.root.frameRuntime.drain();
|
|
2911
|
+
if (select.interactionController !== controller) return;
|
|
2912
|
+
elements = controller.selection;
|
|
2913
|
+
if (elements.length < 2 || type.endsWith("between") && elements.length < 3) return;
|
|
2914
|
+
const rects = elements.map((element) => {
|
|
2915
|
+
const bounds = element.transform.getWorldVisualBoundsView();
|
|
2916
|
+
return {
|
|
2917
|
+
element,
|
|
2918
|
+
left: bounds.left,
|
|
2919
|
+
top: bounds.top,
|
|
2920
|
+
width: bounds.width,
|
|
2921
|
+
height: bounds.height,
|
|
2922
|
+
dx: 0,
|
|
2923
|
+
dy: 0
|
|
2924
|
+
};
|
|
2925
|
+
});
|
|
2926
|
+
resolveOffsets(rects, type);
|
|
2927
|
+
const changes = [];
|
|
2928
|
+
for (const rect of rects) {
|
|
2929
|
+
if (rect.dx === 0 && rect.dy === 0) continue;
|
|
2930
|
+
const capability = getEditorCapability(rect.element, TransformInteractionCapabilityToken);
|
|
2931
|
+
if (!capability) continue;
|
|
2932
|
+
const baseline = capability.captureTransformBaseline();
|
|
2933
|
+
changes.push(capability.resolveTransformChange(baseline, { worldDelta: new DOMMatrix().translate(rect.dx, rect.dy) }));
|
|
2934
|
+
}
|
|
2935
|
+
if (changes.length === 0) return;
|
|
2936
|
+
const scope = select.history.openScope(elements);
|
|
2937
|
+
controller.applyCommand(changes, [], scope, elements);
|
|
2938
|
+
}
|
|
2939
|
+
//#endregion
|
|
2940
|
+
//#region packages/tools/src/select/arrange.ts
|
|
2941
|
+
/** Select 支持的跨 Layer 与 sibling 排列方向。 */
|
|
2942
|
+
var ArrangeType = /* @__PURE__ */ function(ArrangeType) {
|
|
2943
|
+
ArrangeType["LayerUp"] = "layer-up";
|
|
2944
|
+
ArrangeType["LayerDown"] = "layer-down";
|
|
2945
|
+
ArrangeType["Forward"] = "forward";
|
|
2946
|
+
ArrangeType["Backward"] = "backward";
|
|
2947
|
+
ArrangeType["Top"] = "top";
|
|
2948
|
+
ArrangeType["Bottom"] = "bottom";
|
|
2949
|
+
return ArrangeType;
|
|
2950
|
+
}({});
|
|
2951
|
+
function sameSelection(left, right) {
|
|
2952
|
+
return left.length === right.length && left.every((element, index) => element === right[index]);
|
|
2953
|
+
}
|
|
2954
|
+
function adjacentLayer(root, source, type) {
|
|
2955
|
+
if (!source.elementArrangementEnabled) return;
|
|
2956
|
+
const step = type === ArrangeType.LayerUp ? 1 : -1;
|
|
2957
|
+
for (let index = root.layers.indexOf(source) + step; index >= 0 && index < root.layers.length; index += step) {
|
|
2958
|
+
const candidate = root.layers[index];
|
|
2959
|
+
if (candidate.elementArrangementEnabled) return candidate;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
function canArrange(root, selection, type) {
|
|
2963
|
+
if (type === ArrangeType.LayerUp || type === ArrangeType.LayerDown) return selection.some((element) => element.activeRoot === root && adjacentLayer(root, element.layer, type) !== void 0);
|
|
2964
|
+
const siblingKey = type === ArrangeType.Forward || type === ArrangeType.Top ? "nextSibling" : "previousSibling";
|
|
2965
|
+
const selected = new Set(selection);
|
|
2966
|
+
return selection.some((element) => {
|
|
2967
|
+
if (element.activeRoot !== root || !element.layer.elementArrangementEnabled) return false;
|
|
2968
|
+
const sibling = element[siblingKey];
|
|
2969
|
+
return sibling !== null && !selected.has(sibling);
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
function targetParentWorld(layer) {
|
|
2973
|
+
return layer.transform.getChildWorldInverseMatrixView().inverse();
|
|
2974
|
+
}
|
|
2975
|
+
function buildLayerMoves(select, selection, type) {
|
|
2976
|
+
const controller = select.interactionController;
|
|
2977
|
+
const root = controller.root;
|
|
2978
|
+
const targets = /* @__PURE__ */ new Map();
|
|
2979
|
+
const targetWorlds = /* @__PURE__ */ new Map();
|
|
2980
|
+
const moves = [];
|
|
2981
|
+
const getTarget = (sourceLayer) => {
|
|
2982
|
+
if (targets.has(sourceLayer)) return targets.get(sourceLayer);
|
|
2983
|
+
const targetLayer = adjacentLayer(root, sourceLayer, type);
|
|
2984
|
+
targets.set(sourceLayer, targetLayer);
|
|
2985
|
+
return targetLayer;
|
|
2986
|
+
};
|
|
2987
|
+
for (const element of selection) {
|
|
2988
|
+
const sourceParent = element.parent;
|
|
2989
|
+
if (!sourceParent || element.activeRoot !== root) continue;
|
|
2990
|
+
const sourceLayer = element.layer;
|
|
2991
|
+
const targetLayer = getTarget(sourceLayer);
|
|
2992
|
+
if (!targetLayer) continue;
|
|
2993
|
+
const capability = getEditorCapability(element, TransformInteractionCapabilityToken);
|
|
2994
|
+
if (!capability) continue;
|
|
2995
|
+
let targetParentWorldMatrix = targetWorlds.get(targetLayer);
|
|
2996
|
+
if (!targetParentWorldMatrix) {
|
|
2997
|
+
targetParentWorldMatrix = targetParentWorld(targetLayer);
|
|
2998
|
+
targetWorlds.set(targetLayer, targetParentWorldMatrix);
|
|
2999
|
+
}
|
|
3000
|
+
const intent = {
|
|
3001
|
+
worldDelta: new DOMMatrix(),
|
|
3002
|
+
targetParentWorldMatrix
|
|
3003
|
+
};
|
|
3004
|
+
moves.push({
|
|
3005
|
+
element,
|
|
3006
|
+
sourceParent,
|
|
3007
|
+
sourceLayer,
|
|
3008
|
+
targetLayer,
|
|
3009
|
+
change: capability.resolveTransformChange(capability.captureTransformBaseline(), intent)
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
if (select.interactionController !== controller || !sameSelection(controller.selection, selection) || moves.some(({ element, sourceParent, sourceLayer, targetLayer }) => element.parent !== sourceParent || element.layer !== sourceLayer || adjacentLayer(root, sourceLayer, type) !== targetLayer)) return;
|
|
3013
|
+
if (moves.length === 0) return;
|
|
3014
|
+
const moving = new Set(moves.map(({ element }) => element));
|
|
3015
|
+
const finalChildren = /* @__PURE__ */ new Map();
|
|
3016
|
+
for (const { sourceParent, targetLayer } of moves) {
|
|
3017
|
+
if (!finalChildren.has(sourceParent)) finalChildren.set(sourceParent, sourceParent.children.filter((child) => !moving.has(child)));
|
|
3018
|
+
if (!finalChildren.has(targetLayer)) finalChildren.set(targetLayer, targetLayer.children.filter((child) => !moving.has(child)));
|
|
3019
|
+
}
|
|
3020
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
3021
|
+
for (const { element, targetLayer } of moves) {
|
|
3022
|
+
const elements = byTarget.get(targetLayer);
|
|
3023
|
+
if (elements) elements.push(element);
|
|
3024
|
+
else byTarget.set(targetLayer, [element]);
|
|
3025
|
+
}
|
|
3026
|
+
for (const [targetLayer, elements] of byTarget) {
|
|
3027
|
+
elements.sort((left, right) => root.getPaintOrder(left) - root.getPaintOrder(right));
|
|
3028
|
+
const children = finalChildren.get(targetLayer);
|
|
3029
|
+
if (type === ArrangeType.LayerUp) children.unshift(...elements);
|
|
3030
|
+
else children.push(...elements);
|
|
3031
|
+
}
|
|
3032
|
+
return {
|
|
3033
|
+
changes: moves.map(({ change }) => change),
|
|
3034
|
+
structure: [...finalChildren].map(([owner, children]) => ({
|
|
3035
|
+
owner,
|
|
3036
|
+
children
|
|
3037
|
+
}))
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
function buildSiblingChanges(selection, type) {
|
|
3041
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
3042
|
+
for (const element of selection) {
|
|
3043
|
+
const parent = element.parent;
|
|
3044
|
+
if (!parent || !element.layer.elementArrangementEnabled) continue;
|
|
3045
|
+
const elements = byParent.get(parent);
|
|
3046
|
+
if (elements) elements.add(element);
|
|
3047
|
+
else byParent.set(parent, new Set([element]));
|
|
3048
|
+
}
|
|
3049
|
+
const structure = [];
|
|
3050
|
+
for (const [owner, ownerSelection] of byParent) {
|
|
3051
|
+
if (type === ArrangeType.Top || type === ArrangeType.Bottom) {
|
|
3052
|
+
const selectedChildren = [];
|
|
3053
|
+
const unselectedChildren = [];
|
|
3054
|
+
for (const child of owner.children) if (ownerSelection.has(child)) selectedChildren.push(child);
|
|
3055
|
+
else unselectedChildren.push(child);
|
|
3056
|
+
const children = type === ArrangeType.Top ? [...unselectedChildren, ...selectedChildren] : [...selectedChildren, ...unselectedChildren];
|
|
3057
|
+
if (children.some((child, index) => child !== owner.children[index])) structure.push({
|
|
3058
|
+
owner,
|
|
3059
|
+
children
|
|
3060
|
+
});
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
3063
|
+
const children = [...owner.children];
|
|
3064
|
+
let changed = false;
|
|
3065
|
+
if (type === ArrangeType.Forward) {
|
|
3066
|
+
for (let index = children.length - 2; index >= 0; index--) if (ownerSelection.has(children[index]) && !ownerSelection.has(children[index + 1])) {
|
|
3067
|
+
[children[index], children[index + 1]] = [children[index + 1], children[index]];
|
|
3068
|
+
changed = true;
|
|
3069
|
+
}
|
|
3070
|
+
} else for (let index = 1; index < children.length; index++) if (ownerSelection.has(children[index]) && !ownerSelection.has(children[index - 1])) {
|
|
3071
|
+
[children[index - 1], children[index]] = [children[index], children[index - 1]];
|
|
3072
|
+
changed = true;
|
|
3073
|
+
}
|
|
3074
|
+
if (changed) structure.push({
|
|
3075
|
+
owner,
|
|
3076
|
+
children
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
return structure;
|
|
3080
|
+
}
|
|
3081
|
+
function arrangeElements(select, type) {
|
|
3082
|
+
const controller = select.interactionController;
|
|
3083
|
+
const root = controller.root;
|
|
3084
|
+
if (!canArrange(root, controller.selection, type)) return;
|
|
3085
|
+
controller.finishCurrentInteraction();
|
|
3086
|
+
root.frameRuntime.drain();
|
|
3087
|
+
if (select.interactionController !== controller) return;
|
|
3088
|
+
const selection = controller.selection;
|
|
3089
|
+
if (!canArrange(root, selection, type)) return;
|
|
3090
|
+
const result = type === ArrangeType.LayerUp || type === ArrangeType.LayerDown ? buildLayerMoves(select, selection, type) : {
|
|
3091
|
+
changes: [],
|
|
3092
|
+
structure: buildSiblingChanges(selection, type)
|
|
3093
|
+
};
|
|
3094
|
+
if (!result || result.structure.length === 0) return;
|
|
3095
|
+
const scope = select.history.openScope(selection);
|
|
3096
|
+
controller.applyCommand(result.changes, result.structure, scope, selection);
|
|
3097
|
+
}
|
|
3098
|
+
//#endregion
|
|
3099
|
+
//#region packages/tools/src/select/clipboard.ts
|
|
3100
|
+
var PASTE_OFFSET = 20;
|
|
3101
|
+
var memoryClipboard = null;
|
|
3102
|
+
function collectSubtreeKeys(elements) {
|
|
3103
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3104
|
+
const visit = (element) => {
|
|
3105
|
+
keys.add(element.key);
|
|
3106
|
+
for (const child of element.children) visit(child);
|
|
3107
|
+
};
|
|
3108
|
+
elements.forEach(visit);
|
|
3109
|
+
return keys;
|
|
3110
|
+
}
|
|
3111
|
+
function materializeExternalRelations(source, json, includedKeys) {
|
|
3112
|
+
const data = json;
|
|
3113
|
+
if (source instanceof BaseLine) {
|
|
3114
|
+
const path = data.path;
|
|
3115
|
+
const relations = data.relations;
|
|
3116
|
+
const vertices = source.getEffectiveLineVerticesView();
|
|
3117
|
+
const materialize = (end, index, relation) => {
|
|
3118
|
+
if (!relation || includedKeys.has(relation.target.elementKey)) return;
|
|
3119
|
+
const point = vertices[index]?.point;
|
|
3120
|
+
if (point) path.anchors[index].point = {
|
|
3121
|
+
x: point.x,
|
|
3122
|
+
y: point.y
|
|
3123
|
+
};
|
|
3124
|
+
if (relations) delete relations[end];
|
|
3125
|
+
};
|
|
3126
|
+
materialize("head", 0, source.headRelation);
|
|
3127
|
+
materialize("tail", path.anchors.length - 1, source.tailRelation);
|
|
3128
|
+
if (relations && relations.head === void 0 && relations.tail === void 0) delete data.relations;
|
|
3129
|
+
} else if (source instanceof LineTree) {
|
|
3130
|
+
const topology = data.topology;
|
|
3131
|
+
source.topology.points.forEach((point, index) => {
|
|
3132
|
+
if (!point.relation || includedKeys.has(point.relation.target.elementKey)) return;
|
|
3133
|
+
const resolved = source.transform.getResolvedPoint(point.anchorId).point;
|
|
3134
|
+
topology.points[index].point = {
|
|
3135
|
+
x: resolved.x,
|
|
3136
|
+
y: resolved.y
|
|
3137
|
+
};
|
|
3138
|
+
delete topology.points[index].relation;
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
json.children?.forEach((child, index) => {
|
|
3142
|
+
materializeExternalRelations(source.children[index], child, includedKeys);
|
|
3143
|
+
});
|
|
3144
|
+
return json;
|
|
3145
|
+
}
|
|
3146
|
+
function snapshotElements(elements, includedKeys) {
|
|
3147
|
+
return elements.map((element) => materializeExternalRelations(element, element.toJSON(), includedKeys));
|
|
3148
|
+
}
|
|
3149
|
+
function isCurrentSelection(select, controller, sources) {
|
|
3150
|
+
const current = controller.selection;
|
|
3151
|
+
if (select.interactionController !== controller || current.length !== sources.length) return false;
|
|
3152
|
+
return sources.every((source, index) => current[index] === source);
|
|
3153
|
+
}
|
|
3154
|
+
function snapshotSelection(select, producer) {
|
|
3155
|
+
const controller = select.interactionController;
|
|
3156
|
+
const sources = [...controller.selection];
|
|
3157
|
+
if (sources.length === 0) return;
|
|
3158
|
+
const sourceKeys = collectSubtreeKeys(sources);
|
|
3159
|
+
const data = {
|
|
3160
|
+
elements: snapshotElements(sources, sourceKeys),
|
|
3161
|
+
parentKeys: sources.map((source) => source.parent?.key ?? null)
|
|
3162
|
+
};
|
|
3163
|
+
if (producer) data.applicationData = producer({
|
|
3164
|
+
sources,
|
|
3165
|
+
sourceKeys
|
|
3166
|
+
});
|
|
3167
|
+
if (!isCurrentSelection(select, controller, sources)) return;
|
|
3168
|
+
return data;
|
|
3169
|
+
}
|
|
3170
|
+
function writeSystemClipboard(data) {
|
|
3171
|
+
try {
|
|
3172
|
+
const text = JSON.stringify(data);
|
|
3173
|
+
Promise.resolve(navigator.clipboard.writeText(text)).catch(() => {});
|
|
3174
|
+
} catch {}
|
|
3175
|
+
}
|
|
3176
|
+
function copyElements(select, producer) {
|
|
3177
|
+
const data = snapshotSelection(select, producer);
|
|
3178
|
+
if (!data) return;
|
|
3179
|
+
memoryClipboard = data;
|
|
3180
|
+
writeSystemClipboard(data);
|
|
3181
|
+
}
|
|
3182
|
+
function removeKeys(data) {
|
|
3183
|
+
delete data.key;
|
|
3184
|
+
data.children?.forEach(removeKeys);
|
|
3185
|
+
}
|
|
3186
|
+
function offsetTopLevel(data) {
|
|
3187
|
+
if (typeof data.left === "number") data.left += PASTE_OFFSET;
|
|
3188
|
+
if (typeof data.top === "number") data.top += PASTE_OFFSET;
|
|
3189
|
+
}
|
|
3190
|
+
function mapClonedKeys(source, clone, keyMap) {
|
|
3191
|
+
keyMap.set(source.key, clone.key);
|
|
3192
|
+
(source.children ?? []).forEach((child, index) => {
|
|
3193
|
+
mapClonedKeys(child, clone.children[index], keyMap);
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
function referenceChanges(elements, keyMap) {
|
|
3197
|
+
const changes = [];
|
|
3198
|
+
const visit = (element) => {
|
|
3199
|
+
if (element instanceof BaseLine && (element.headRelation || element.tailRelation)) {
|
|
3200
|
+
const remap = (relation) => {
|
|
3201
|
+
if (!relation) return;
|
|
3202
|
+
const target = relation.target;
|
|
3203
|
+
const elementKey = keyMap.get(target.elementKey);
|
|
3204
|
+
return elementKey === void 0 ? void 0 : {
|
|
3205
|
+
...relation,
|
|
3206
|
+
target: {
|
|
3207
|
+
...target,
|
|
3208
|
+
elementKey
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
};
|
|
3212
|
+
changes.push({
|
|
3213
|
+
owner: element,
|
|
3214
|
+
patch: { relations: {
|
|
3215
|
+
head: remap(element.headRelation),
|
|
3216
|
+
tail: remap(element.tailRelation)
|
|
3217
|
+
} }
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
if (element instanceof LineTree) {
|
|
3221
|
+
const topology = remapLineTreeRelations(element.topology, keyMap);
|
|
3222
|
+
if (topology !== element.topology) changes.push({
|
|
3223
|
+
owner: element,
|
|
3224
|
+
patch: { topology }
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
for (const child of element.children) visit(child);
|
|
3228
|
+
};
|
|
3229
|
+
elements.forEach(visit);
|
|
3230
|
+
return changes;
|
|
3231
|
+
}
|
|
3232
|
+
function deserializeClones(entries, deserializers) {
|
|
3233
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
3234
|
+
const elements = entries.map((entry) => {
|
|
3235
|
+
const json = structuredClone(entry);
|
|
3236
|
+
removeKeys(json);
|
|
3237
|
+
offsetTopLevel(json);
|
|
3238
|
+
const element = deserializeElement$1(json, deserializers);
|
|
3239
|
+
mapClonedKeys(entry, element, keyMap);
|
|
3240
|
+
return element;
|
|
3241
|
+
});
|
|
3242
|
+
return {
|
|
3243
|
+
elements,
|
|
3244
|
+
keyMap,
|
|
3245
|
+
referenceChanges: referenceChanges(elements, keyMap)
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
function appendStructure(placements) {
|
|
3249
|
+
const children = /* @__PURE__ */ new Map();
|
|
3250
|
+
for (const { parent } of placements) if (!children.has(parent)) children.set(parent, [...parent.children]);
|
|
3251
|
+
for (const { element, parent } of placements) children.get(parent).push(element);
|
|
3252
|
+
return [...children].map(([owner, finalChildren]) => ({
|
|
3253
|
+
owner,
|
|
3254
|
+
children: finalChildren
|
|
3255
|
+
}));
|
|
3256
|
+
}
|
|
3257
|
+
function isCurrentPasteController(select, controller) {
|
|
3258
|
+
return select.interactionController === controller;
|
|
3259
|
+
}
|
|
3260
|
+
function isCurrentParent(root, parent) {
|
|
3261
|
+
return parent === root || parent.activeRoot === root && root.getByKey(parent.key) === parent;
|
|
3262
|
+
}
|
|
3263
|
+
function resolveSourceParent(root, parentKey) {
|
|
3264
|
+
if (parentKey === null || parentKey === void 0) return void 0;
|
|
3265
|
+
const parent = root.getByKey(parentKey);
|
|
3266
|
+
return parent && isCurrentParent(root, parent) ? parent : void 0;
|
|
3267
|
+
}
|
|
3268
|
+
function firstLayer(root) {
|
|
3269
|
+
return root.children.find((child) => child.isLayerNode());
|
|
3270
|
+
}
|
|
3271
|
+
function pasteData(select, controller, pasteSelection, task, data, deserializers, options) {
|
|
3272
|
+
const { elements, keyMap, referenceChanges: changes } = deserializeClones(data.elements, deserializers);
|
|
3273
|
+
if (!isCurrentPasteController(select, controller)) return;
|
|
3274
|
+
let defaultLayer;
|
|
3275
|
+
const placements = [];
|
|
3276
|
+
for (const [index, element] of elements.entries()) {
|
|
3277
|
+
if (element.isLayerNode()) {
|
|
3278
|
+
placements.push({
|
|
3279
|
+
element,
|
|
3280
|
+
parent: controller.root
|
|
3281
|
+
});
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
const sourceParent = resolveSourceParent(controller.root, data.parentKeys[index]);
|
|
3285
|
+
let parent;
|
|
3286
|
+
if (options?.resolveParent) {
|
|
3287
|
+
parent = options.resolveParent(sourceParent);
|
|
3288
|
+
if (!isCurrentPasteController(select, controller) || !isCurrentParent(controller.root, parent)) return;
|
|
3289
|
+
} else {
|
|
3290
|
+
parent = sourceParent;
|
|
3291
|
+
if (!parent) {
|
|
3292
|
+
defaultLayer ??= firstLayer(controller.root);
|
|
3293
|
+
parent = defaultLayer;
|
|
3294
|
+
}
|
|
3295
|
+
if (!parent || !isCurrentParent(controller.root, parent)) return;
|
|
3296
|
+
}
|
|
3297
|
+
placements.push({
|
|
3298
|
+
element,
|
|
3299
|
+
parent
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
if (!isCurrentPasteController(select, controller)) return;
|
|
3303
|
+
if (placements.some(({ parent }) => !isCurrentParent(controller.root, parent))) return;
|
|
3304
|
+
const recordHistory = select.history.enabled;
|
|
3305
|
+
const scope = controller.history.openScope(controller.selection);
|
|
3306
|
+
const installSelection = () => controller.ownsPasteSelection(pasteSelection, task);
|
|
3307
|
+
controller.applyCommand(changes, appendStructure(placements), scope, elements, options?.afterInstall ? {
|
|
3308
|
+
beforeDrain: () => options.afterInstall({
|
|
3309
|
+
applicationData: data.applicationData,
|
|
3310
|
+
keyMap,
|
|
3311
|
+
history: recordHistory ? scope : void 0
|
|
3312
|
+
}),
|
|
3313
|
+
installSelection
|
|
3314
|
+
} : { installSelection });
|
|
3315
|
+
}
|
|
3316
|
+
function pasteMemory(select, controller, pasteSelection, task, deserializers, options) {
|
|
3317
|
+
if (!memoryClipboard) return;
|
|
3318
|
+
pasteData(select, controller, pasteSelection, task, memoryClipboard, deserializers, options);
|
|
3319
|
+
}
|
|
3320
|
+
function pasteElements(select, deserializers, options) {
|
|
3321
|
+
const controller = select.interactionController;
|
|
3322
|
+
const pasteSelection = controller.captureSelectionIdentity();
|
|
3323
|
+
let task;
|
|
3324
|
+
task = Promise.resolve().then(async () => {
|
|
3325
|
+
try {
|
|
3326
|
+
let text;
|
|
3327
|
+
try {
|
|
3328
|
+
text = await navigator.clipboard.readText();
|
|
3329
|
+
} catch {
|
|
3330
|
+
if (isCurrentPasteController(select, controller)) pasteMemory(select, controller, pasteSelection, task, deserializers, options);
|
|
3331
|
+
return;
|
|
3332
|
+
}
|
|
3333
|
+
if (!isCurrentPasteController(select, controller)) return;
|
|
3334
|
+
const data = JSON.parse(text);
|
|
3335
|
+
pasteData(select, controller, pasteSelection, task, data, deserializers, options);
|
|
3336
|
+
} finally {
|
|
3337
|
+
controller.finishPaste(task);
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
controller.beginPaste(task);
|
|
3341
|
+
return task;
|
|
3342
|
+
}
|
|
3343
|
+
//#endregion
|
|
3344
|
+
//#region packages/tools/src/select/delete.ts
|
|
3345
|
+
function deleteElements(select, afterInstall) {
|
|
3346
|
+
const controller = select.interactionController;
|
|
3347
|
+
if (controller.deletePointTarget()) return;
|
|
3348
|
+
if (controller.selection.length === 0) return;
|
|
3349
|
+
if (controller.state.kind === "select" && controller.state.interaction.kind === "content") controller.finishContentEditing(true);
|
|
3350
|
+
else controller.finishCurrentInteraction();
|
|
3351
|
+
if (select.interactionController !== controller) return;
|
|
3352
|
+
const selected = [...controller.selection];
|
|
3353
|
+
if (selected.length === 0) return;
|
|
3354
|
+
const roots = [...selected];
|
|
3355
|
+
const covered = /* @__PURE__ */ new Set();
|
|
3356
|
+
const subjects = [];
|
|
3357
|
+
const cover = (element) => {
|
|
3358
|
+
if (covered.has(element)) return;
|
|
3359
|
+
covered.add(element);
|
|
3360
|
+
subjects.push(element);
|
|
3361
|
+
for (const child of element.children) if (child instanceof Element) cover(child);
|
|
3362
|
+
};
|
|
3363
|
+
roots.forEach(cover);
|
|
3364
|
+
const relationIndex = getEndpointRelationIndex(controller.root);
|
|
3365
|
+
for (let index = 0; index < subjects.length; index++) for (const source of relationIndex.relationsTargetingElement(subjects[index].key)) {
|
|
3366
|
+
const dependency = controller.root.getByKey(source.elementKey);
|
|
3367
|
+
if (!(dependency instanceof Element)) continue;
|
|
3368
|
+
if (covered.has(dependency)) continue;
|
|
3369
|
+
roots.push(dependency);
|
|
3370
|
+
cover(dependency);
|
|
3371
|
+
}
|
|
3372
|
+
const rootSet = new Set(roots);
|
|
3373
|
+
const parents = new Set(roots.map((element) => element.parent).filter(Boolean));
|
|
3374
|
+
const structure = [];
|
|
3375
|
+
for (const parent of parents) structure.push({
|
|
3376
|
+
owner: parent,
|
|
3377
|
+
children: parent.children.filter((child) => !rootSet.has(child))
|
|
3378
|
+
});
|
|
3379
|
+
if (structure.length === 0) return;
|
|
3380
|
+
const recordsHistory = select.history.enabled;
|
|
3381
|
+
const scope = select.history.openScope(selected);
|
|
3382
|
+
controller.applyCommand([], structure, scope, [], {
|
|
3383
|
+
beforeDrain: afterInstall ? () => afterInstall({
|
|
3384
|
+
elements: subjects,
|
|
3385
|
+
history: recordsHistory ? scope : void 0
|
|
3386
|
+
}) : void 0,
|
|
3387
|
+
afterHistory: () => controller.root.checkHit()
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
//#endregion
|
|
3391
|
+
//#region packages/tools/src/select/grouping.ts
|
|
3392
|
+
function isCurrentController(select, controller) {
|
|
3393
|
+
return select.interactionController === controller;
|
|
3394
|
+
}
|
|
3395
|
+
function compareSceneOrder(root, left, right) {
|
|
3396
|
+
const leftOrder = root.getPaintOrderKey(left);
|
|
3397
|
+
const rightOrder = root.getPaintOrderKey(right);
|
|
3398
|
+
return leftOrder.layer - rightOrder.layer || leftOrder.element - rightOrder.element;
|
|
3399
|
+
}
|
|
3400
|
+
function hasSelectedAncestor(element, selected) {
|
|
3401
|
+
for (let parent = element.parent; parent; parent = parent.parent) if (parent instanceof Element && selected.has(parent)) return true;
|
|
3402
|
+
return false;
|
|
3403
|
+
}
|
|
3404
|
+
function parentWorld(parent) {
|
|
3405
|
+
return parent instanceof Element ? parent.transform.getChildWorldInverseMatrixView().inverse() : new DOMMatrix();
|
|
3406
|
+
}
|
|
3407
|
+
function captureChangesForParent(elements, targetParentWorldMatrix) {
|
|
3408
|
+
const changes = [];
|
|
3409
|
+
const intent = {
|
|
3410
|
+
worldDelta: new DOMMatrix(),
|
|
3411
|
+
targetParentWorldMatrix
|
|
3412
|
+
};
|
|
3413
|
+
for (const element of elements) {
|
|
3414
|
+
const capability = getEditorCapability(element, TransformInteractionCapabilityToken);
|
|
3415
|
+
const baseline = capability.captureTransformBaseline();
|
|
3416
|
+
changes.push(capability.resolveTransformChange(baseline, intent));
|
|
3417
|
+
}
|
|
3418
|
+
return changes;
|
|
3419
|
+
}
|
|
3420
|
+
function doGroup(select) {
|
|
3421
|
+
const controller = select.interactionController;
|
|
3422
|
+
if (controller.selection.length <= 1) return null;
|
|
3423
|
+
controller.finishCurrentInteraction();
|
|
3424
|
+
controller.root.frameRuntime.drain();
|
|
3425
|
+
if (!isCurrentController(select, controller)) return null;
|
|
3426
|
+
const selection = controller.selection;
|
|
3427
|
+
if (selection.length <= 1) return null;
|
|
3428
|
+
const selectedElements = new Set(selection);
|
|
3429
|
+
const children = selection.filter((element) => !hasSelectedAncestor(element, selectedElements)).sort((left, right) => compareSceneOrder(controller.root, left, right));
|
|
3430
|
+
if (children.length <= 1) return null;
|
|
3431
|
+
const parent = children[0].parent;
|
|
3432
|
+
if (!parent) return null;
|
|
3433
|
+
const bounds = makeBoundingBoxFromRects(children.map((element) => element.transform.getContentBounds()));
|
|
3434
|
+
const targetWorld = new DOMMatrix().translate(bounds.left, bounds.top);
|
|
3435
|
+
const resolution = resolveReferenceTransform({
|
|
3436
|
+
writeMode: "pose-authored",
|
|
3437
|
+
startWorldMatrix: new DOMMatrix(),
|
|
3438
|
+
startParentWorldMatrix: new DOMMatrix(),
|
|
3439
|
+
startParentWorldInverseMatrix: new DOMMatrix(),
|
|
3440
|
+
referenceBounds: {
|
|
3441
|
+
left: 0,
|
|
3442
|
+
top: 0,
|
|
3443
|
+
width: bounds.width,
|
|
3444
|
+
height: bounds.height
|
|
3445
|
+
},
|
|
3446
|
+
frame: {
|
|
3447
|
+
left: 0,
|
|
3448
|
+
top: 0,
|
|
3449
|
+
width: bounds.width,
|
|
3450
|
+
height: bounds.height,
|
|
3451
|
+
angle: 0,
|
|
3452
|
+
scaleX: 1,
|
|
3453
|
+
scaleY: 1,
|
|
3454
|
+
skewX: 0,
|
|
3455
|
+
skewY: 0,
|
|
3456
|
+
originX: 0,
|
|
3457
|
+
originY: 0
|
|
3458
|
+
}
|
|
3459
|
+
}, {
|
|
3460
|
+
worldDelta: targetWorld,
|
|
3461
|
+
targetParentWorldMatrix: parentWorld(parent)
|
|
3462
|
+
});
|
|
3463
|
+
const group = new Group({
|
|
3464
|
+
width: bounds.width,
|
|
3465
|
+
height: bounds.height,
|
|
3466
|
+
originX: 0,
|
|
3467
|
+
originY: 0,
|
|
3468
|
+
...resolution.framePatch
|
|
3469
|
+
});
|
|
3470
|
+
const selected = new Set(children);
|
|
3471
|
+
const finalChildren = /* @__PURE__ */ new Map();
|
|
3472
|
+
const affectedParents = new Set(children.map((element) => element.parent));
|
|
3473
|
+
affectedParents.add(parent);
|
|
3474
|
+
for (const owner of affectedParents) finalChildren.set(owner, owner.children.filter((child) => !selected.has(child)));
|
|
3475
|
+
const target = finalChildren.get(parent);
|
|
3476
|
+
const originalIndices = children.filter((element) => element.parent === parent).map((element) => parent.children.indexOf(element));
|
|
3477
|
+
const insertionIndex = Math.min(...originalIndices);
|
|
3478
|
+
target.splice(insertionIndex, 0, group);
|
|
3479
|
+
const structure = [...[...finalChildren].map(([owner, ownerChildren]) => ({
|
|
3480
|
+
owner,
|
|
3481
|
+
children: ownerChildren
|
|
3482
|
+
})), {
|
|
3483
|
+
owner: group,
|
|
3484
|
+
children
|
|
3485
|
+
}];
|
|
3486
|
+
const changes = captureChangesForParent(children, targetWorld);
|
|
3487
|
+
const scope = select.history.openScope(selection);
|
|
3488
|
+
controller.applyCommand(changes, structure, scope, [group]);
|
|
3489
|
+
return group;
|
|
3490
|
+
}
|
|
3491
|
+
function unGroup(select) {
|
|
3492
|
+
const controller = select.interactionController;
|
|
3493
|
+
const candidate = controller.selection;
|
|
3494
|
+
if (candidate.length !== 1 || !(candidate[0] instanceof Group)) return null;
|
|
3495
|
+
controller.finishCurrentInteraction();
|
|
3496
|
+
controller.root.frameRuntime.drain();
|
|
3497
|
+
if (!isCurrentController(select, controller)) return null;
|
|
3498
|
+
const selection = controller.selection;
|
|
3499
|
+
if (selection.length !== 1) return null;
|
|
3500
|
+
const group = selection[0];
|
|
3501
|
+
if (!(group instanceof Group)) return null;
|
|
3502
|
+
const parent = group.parent;
|
|
3503
|
+
if (!parent) return null;
|
|
3504
|
+
const children = [...group.children];
|
|
3505
|
+
const changes = captureChangesForParent(children, parentWorld(parent));
|
|
3506
|
+
const parentChildren = [...parent.children];
|
|
3507
|
+
const index = parentChildren.indexOf(group);
|
|
3508
|
+
parentChildren.splice(index, 1, ...children);
|
|
3509
|
+
const structure = [{
|
|
3510
|
+
owner: parent,
|
|
3511
|
+
children: parentChildren
|
|
3512
|
+
}, {
|
|
3513
|
+
owner: group,
|
|
3514
|
+
children: []
|
|
3515
|
+
}];
|
|
3516
|
+
const scope = select.history.openScope(selection);
|
|
3517
|
+
controller.applyCommand(changes, structure, scope, children);
|
|
3518
|
+
return children;
|
|
3519
|
+
}
|
|
3520
|
+
//#endregion
|
|
3521
|
+
//#region packages/tools/src/select/paint.ts
|
|
3522
|
+
var CONTROL_FILL = "#FFFFFF";
|
|
3523
|
+
var PIVOT_STROKE = "#4B5563";
|
|
3524
|
+
var SNAP_CANDIDATE = "#FF6B6B";
|
|
3525
|
+
function traceOutline(ctx, points, closed = true) {
|
|
3526
|
+
if (points.length === 0) return;
|
|
3527
|
+
ctx.beginPath();
|
|
3528
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
3529
|
+
for (let index = 1; index < points.length; index++) ctx.lineTo(points[index].x, points[index].y);
|
|
3530
|
+
if (closed) ctx.closePath();
|
|
3531
|
+
}
|
|
3532
|
+
function paintRotationPivotSnapMarker(ctx, marker, angle, scale) {
|
|
3533
|
+
const half = 5 / scale;
|
|
3534
|
+
const cornerInset = 3.5 / scale;
|
|
3535
|
+
const reach = 12 / scale;
|
|
3536
|
+
ctx.save();
|
|
3537
|
+
ctx.translate(marker.x, marker.y);
|
|
3538
|
+
ctx.rotate(angle * Math.PI / 180);
|
|
3539
|
+
ctx.beginPath();
|
|
3540
|
+
if (marker.horizontal === "center" && marker.vertical === "center") {
|
|
3541
|
+
ctx.moveTo(-half, 0);
|
|
3542
|
+
ctx.lineTo(half, 0);
|
|
3543
|
+
ctx.moveTo(0, -half);
|
|
3544
|
+
ctx.lineTo(0, half);
|
|
3545
|
+
} else if (marker.horizontal === "center") {
|
|
3546
|
+
const direction = marker.vertical === "top" ? 1 : -1;
|
|
3547
|
+
ctx.moveTo(0, 0);
|
|
3548
|
+
ctx.lineTo(0, direction * reach);
|
|
3549
|
+
} else if (marker.vertical === "center") {
|
|
3550
|
+
const direction = marker.horizontal === "left" ? 1 : -1;
|
|
3551
|
+
ctx.moveTo(0, 0);
|
|
3552
|
+
ctx.lineTo(direction * reach, 0);
|
|
3553
|
+
} else {
|
|
3554
|
+
const x = marker.horizontal === "left" ? 1 : -1;
|
|
3555
|
+
const y = marker.vertical === "top" ? 1 : -1;
|
|
3556
|
+
const wing = 5 / scale;
|
|
3557
|
+
ctx.moveTo(x * reach, y * reach);
|
|
3558
|
+
ctx.lineTo(x * cornerInset, y * cornerInset);
|
|
3559
|
+
ctx.moveTo(x * cornerInset, y * cornerInset);
|
|
3560
|
+
ctx.lineTo(x * (cornerInset + wing), y * cornerInset);
|
|
3561
|
+
ctx.moveTo(x * cornerInset, y * cornerInset);
|
|
3562
|
+
ctx.lineTo(x * cornerInset, y * (cornerInset + wing));
|
|
3563
|
+
}
|
|
3564
|
+
ctx.stroke();
|
|
3565
|
+
ctx.restore();
|
|
3566
|
+
}
|
|
3567
|
+
function paintControl(ctx, control, frameAngle, size, scale, selectionColor) {
|
|
3568
|
+
const isPivot = control.target.kind === "pivot";
|
|
3569
|
+
ctx.save();
|
|
3570
|
+
ctx.translate(control.world.x, control.world.y);
|
|
3571
|
+
ctx.fillStyle = control.shape === "diamond" || control.shape === "rect" || control.shape === "handle" || isPivot ? CONTROL_FILL : selectionColor;
|
|
3572
|
+
ctx.strokeStyle = isPivot ? PIVOT_STROKE : selectionColor;
|
|
3573
|
+
ctx.lineWidth = 1.5 / scale;
|
|
3574
|
+
ctx.beginPath();
|
|
3575
|
+
if (control.shape === "rect") {
|
|
3576
|
+
if (frameAngle !== void 0) ctx.rotate(frameAngle * Math.PI / 180);
|
|
3577
|
+
ctx.roundRect(-size / 2, -size / 2, size, size, 1 / scale);
|
|
3578
|
+
} else if (control.shape === "diamond") {
|
|
3579
|
+
ctx.rotate(Math.PI / 4);
|
|
3580
|
+
ctx.rect(-size / 2, -size / 2, size, size);
|
|
3581
|
+
} else ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
|
|
3582
|
+
ctx.fill();
|
|
3583
|
+
if (control.shape === "diamond" || control.shape === "rect" || control.shape === "handle" || isPivot) ctx.stroke();
|
|
3584
|
+
if (control.active) {
|
|
3585
|
+
ctx.beginPath();
|
|
3586
|
+
ctx.arc(0, 0, size * .72, 0, Math.PI * 2);
|
|
3587
|
+
ctx.stroke();
|
|
3588
|
+
}
|
|
3589
|
+
if (isPivot) {
|
|
3590
|
+
const radius = size / 3;
|
|
3591
|
+
ctx.beginPath();
|
|
3592
|
+
ctx.moveTo(-radius, 0);
|
|
3593
|
+
ctx.lineTo(radius, 0);
|
|
3594
|
+
ctx.moveTo(0, -radius);
|
|
3595
|
+
ctx.lineTo(0, radius);
|
|
3596
|
+
ctx.stroke();
|
|
3597
|
+
}
|
|
3598
|
+
ctx.restore();
|
|
3599
|
+
}
|
|
3600
|
+
function paintInfoPanel(ctx, frame, viewport) {
|
|
3601
|
+
const first = frame.outline[0];
|
|
3602
|
+
let minX = first.x * viewport.scale + viewport.x;
|
|
3603
|
+
let maxX = minX;
|
|
3604
|
+
let maxY = first.y * viewport.scale + viewport.y;
|
|
3605
|
+
for (let index = 1; index < frame.outline.length; index++) {
|
|
3606
|
+
const point = frame.outline[index];
|
|
3607
|
+
const x = point.x * viewport.scale + viewport.x;
|
|
3608
|
+
const y = point.y * viewport.scale + viewport.y;
|
|
3609
|
+
if (x < minX) minX = x;
|
|
3610
|
+
if (x > maxX) maxX = x;
|
|
3611
|
+
if (y > maxY) maxY = y;
|
|
3612
|
+
}
|
|
3613
|
+
const text = `x: ${Math.round(frame.left)} y: ${Math.round(frame.top)} ${Math.round(frame.angle)}°`;
|
|
3614
|
+
const centerX = (minX + maxX) / 2;
|
|
3615
|
+
ctx.save();
|
|
3616
|
+
ctx.setTransform(viewport.dpr, 0, 0, viewport.dpr, 0, 0);
|
|
3617
|
+
ctx.font = "12px Arial";
|
|
3618
|
+
const width = ctx.measureText(text).width + 12;
|
|
3619
|
+
const left = centerX - width / 2;
|
|
3620
|
+
const top = maxY + 8;
|
|
3621
|
+
ctx.fillStyle = "rgba(0,0,0,0.75)";
|
|
3622
|
+
ctx.beginPath();
|
|
3623
|
+
ctx.roundRect(left, top, width, 22, 4);
|
|
3624
|
+
ctx.fill();
|
|
3625
|
+
ctx.fillStyle = "#fff";
|
|
3626
|
+
ctx.fillText(text, left + 6, top + 15);
|
|
3627
|
+
ctx.restore();
|
|
3628
|
+
}
|
|
3629
|
+
/** paint 与 hit 直接读取 controller 当前同一 projection object。 */
|
|
3630
|
+
function paintSelect(select, controller) {
|
|
3631
|
+
const projection = controller.projection;
|
|
3632
|
+
const ctx = select.layer.ctx;
|
|
3633
|
+
const viewport = select.root.viewport;
|
|
3634
|
+
const scale = viewport.scale;
|
|
3635
|
+
const selectionColor = select.root.skin.selection;
|
|
3636
|
+
ctx.save();
|
|
3637
|
+
ctx.setTransform(scale * viewport.dpr, 0, 0, scale * viewport.dpr, viewport.x * viewport.dpr, viewport.y * viewport.dpr);
|
|
3638
|
+
const hover = controller.contentHover;
|
|
3639
|
+
if (hover && !controller.selection.includes(hover)) hover.paintHover(ctx, scale);
|
|
3640
|
+
for (const { element } of projection.selectedOutlineOwners) element.paintHover(ctx, scale);
|
|
3641
|
+
if (projection.pointScopeOutline) {
|
|
3642
|
+
ctx.strokeStyle = selectionColor;
|
|
3643
|
+
ctx.lineWidth = 1 / scale;
|
|
3644
|
+
traceOutline(ctx, projection.pointScopeOutline, false);
|
|
3645
|
+
ctx.stroke();
|
|
3646
|
+
}
|
|
3647
|
+
ctx.strokeStyle = selectionColor;
|
|
3648
|
+
ctx.lineWidth = 1 / scale;
|
|
3649
|
+
if (projection.frame && projection.frameVisible) {
|
|
3650
|
+
traceOutline(ctx, projection.frame.outline);
|
|
3651
|
+
ctx.stroke();
|
|
3652
|
+
}
|
|
3653
|
+
for (const control of projection.controls) {
|
|
3654
|
+
if (!control.guideFrom) continue;
|
|
3655
|
+
ctx.beginPath();
|
|
3656
|
+
ctx.moveTo(control.guideFrom.x, control.guideFrom.y);
|
|
3657
|
+
ctx.lineTo(control.world.x, control.world.y);
|
|
3658
|
+
ctx.stroke();
|
|
3659
|
+
}
|
|
3660
|
+
const size = select.controlSize / scale;
|
|
3661
|
+
const frame = projection.frame;
|
|
3662
|
+
let pivotControl;
|
|
3663
|
+
for (const control of projection.controls) {
|
|
3664
|
+
if (!control.shape) continue;
|
|
3665
|
+
if (control.target.kind === "pivot") {
|
|
3666
|
+
pivotControl = control;
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
paintControl(ctx, control, frame?.angle, size, scale, selectionColor);
|
|
3670
|
+
}
|
|
3671
|
+
if (pivotControl) paintControl(ctx, pivotControl, frame?.angle, size, scale, selectionColor);
|
|
3672
|
+
if (frame && projection.rotationPivotSnapMarkers.length > 0) {
|
|
3673
|
+
ctx.strokeStyle = SNAP_CANDIDATE;
|
|
3674
|
+
ctx.lineWidth = 1 / scale;
|
|
3675
|
+
for (const marker of projection.rotationPivotSnapMarkers) paintRotationPivotSnapMarker(ctx, marker, frame.angle, scale);
|
|
3676
|
+
}
|
|
3677
|
+
if (projection.marquee) {
|
|
3678
|
+
const { left, top, width, height } = projection.marquee;
|
|
3679
|
+
ctx.fillStyle = selectionColor;
|
|
3680
|
+
ctx.strokeStyle = selectionColor;
|
|
3681
|
+
ctx.lineWidth = 1 / scale;
|
|
3682
|
+
ctx.save();
|
|
3683
|
+
ctx.globalAlpha *= .12;
|
|
3684
|
+
ctx.fillRect(left, top, width, height);
|
|
3685
|
+
ctx.restore();
|
|
3686
|
+
ctx.strokeRect(left, top, width, height);
|
|
3687
|
+
}
|
|
3688
|
+
if (frame && projection.frameVisible) paintInfoPanel(ctx, frame, viewport);
|
|
3689
|
+
ctx.restore();
|
|
3690
|
+
}
|
|
3691
|
+
//#endregion
|
|
3692
|
+
//#region packages/tools/src/editor/overlay-transform.ts
|
|
3693
|
+
/** Tools 画布 overlay surface 始终参与 Layer paint,但从不成为内容命中目标。 */
|
|
3694
|
+
var ToolOverlayTransform = class extends ElementTransform {
|
|
3695
|
+
hasPointHint() {
|
|
3696
|
+
return false;
|
|
3697
|
+
}
|
|
3698
|
+
hasInView() {
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
};
|
|
3702
|
+
//#endregion
|
|
3703
|
+
//#region packages/tools/src/select/index.ts
|
|
3704
|
+
/**
|
|
3705
|
+
* 轻量 Editor command/paint surface;交互状态由唯一 controller 拥有。
|
|
3706
|
+
*
|
|
3707
|
+
* `select:end` 在一次显式选择、交互选择、selection-producing command
|
|
3708
|
+
* 或 History replay 的内部工作和 selection projection 完成后异步通知。
|
|
3709
|
+
* `select:update` 仅在存在直接监听器时,于选中内容的几何、投影和绘制
|
|
3710
|
+
* 在当前 Root frame 内稳定后同步派发;同一显示帧的连续变化合并为一次通知。
|
|
3711
|
+
* `select:configured-change` 在一次 Select/History 语义操作产生最终 configured
|
|
3712
|
+
* 属性或结构净变化时异步通知一次;适用的 History entry/stack 已先安装。事件不携带
|
|
3713
|
+
* payload,普通 noop 与纯 runtime override 不派发;`select:end` 不承担持久化。
|
|
3714
|
+
* 每次显式 `select()` 都是一次独立操作,包括重复传入同一 selection。全部 Select
|
|
3715
|
+
* `end/configured-change/undo/redo` 通知只供外部读取派发时的 live state,不参与、阻止或使已完成的同步操作失败。
|
|
3716
|
+
*/
|
|
3717
|
+
var Select = class extends Element {
|
|
3718
|
+
controlSize = 6;
|
|
3719
|
+
hitPadding = 6;
|
|
3720
|
+
snapAngle = 45;
|
|
3721
|
+
snapThreshold = 5;
|
|
3722
|
+
history;
|
|
3723
|
+
connectionGesture;
|
|
3724
|
+
snapTool;
|
|
3725
|
+
lineTool;
|
|
3726
|
+
controller;
|
|
3727
|
+
keyboardCommands;
|
|
3728
|
+
clipboardDeserializers = /* @__PURE__ */ new Map();
|
|
3729
|
+
constructor(options = {}) {
|
|
3730
|
+
const { connectionGesture = "auto", snap, lineTool, ...baseOptions } = options;
|
|
3731
|
+
const elementOptions = {
|
|
3732
|
+
width: 0,
|
|
3733
|
+
height: 0,
|
|
3734
|
+
selectable: false,
|
|
3735
|
+
pickable: false,
|
|
3736
|
+
...baseOptions
|
|
3737
|
+
};
|
|
3738
|
+
super(elementOptions, new ElementProperties(elementOptions), ToolOverlayTransform);
|
|
3739
|
+
this.connectionGesture = connectionGesture;
|
|
3740
|
+
this.snapTool = snap;
|
|
3741
|
+
this.lineTool = lineTool;
|
|
3742
|
+
}
|
|
3743
|
+
get selectedElements() {
|
|
3744
|
+
return this.controller?.selection ?? [];
|
|
3745
|
+
}
|
|
3746
|
+
get editorState() {
|
|
3747
|
+
return this.controller?.state;
|
|
3748
|
+
}
|
|
3749
|
+
/** @internal command modules使用同一 controller/batch/History owner。 */
|
|
3750
|
+
get interactionController() {
|
|
3751
|
+
return this.controller;
|
|
3752
|
+
}
|
|
3753
|
+
/** 以一次独立操作选择给定元素,并在完成后异步通知 `select:end`。 */
|
|
3754
|
+
select(elements) {
|
|
3755
|
+
this.controller?.setSelection(elements);
|
|
3756
|
+
}
|
|
3757
|
+
/**
|
|
3758
|
+
* Groups the live multi-selection. Accepted active Transform work finishes
|
|
3759
|
+
* before the independent Group command; stale pointer continuation is ignored.
|
|
3760
|
+
* An obvious insufficient-selection noop does not drain the frame runtime.
|
|
3761
|
+
*/
|
|
3762
|
+
doGroup() {
|
|
3763
|
+
return doGroup(this);
|
|
3764
|
+
}
|
|
3765
|
+
/**
|
|
3766
|
+
* Ungroups the live single Group selection. Accepted active Transform work
|
|
3767
|
+
* finishes before the independent command; stale pointer continuation is ignored.
|
|
3768
|
+
* An obvious non-Group noop does not drain the frame runtime.
|
|
3769
|
+
*/
|
|
3770
|
+
unGroup() {
|
|
3771
|
+
return unGroup(this);
|
|
3772
|
+
}
|
|
3773
|
+
align(type) {
|
|
3774
|
+
return alignElements(this, type);
|
|
3775
|
+
}
|
|
3776
|
+
/** 按当前 Layer 与 sibling 顺序排列 live selection。 */
|
|
3777
|
+
arrange(type) {
|
|
3778
|
+
return arrangeElements(this, type);
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* 把一次 authored 顶层属性 intent 应用到全部 live selected elements。
|
|
3782
|
+
* 省略字段保持不变,`undefined` 恢复默认值,`null` 是普通领域值;
|
|
3783
|
+
* 对象和数组不 deep merge/patch;Tools 将同一份 patch 引用直接交给全部
|
|
3784
|
+
* selected owners。调用者需要独立副本时应在交付前复制;标量相同值仍是 noop。
|
|
3785
|
+
* Tools 不过滤元素是否支持字段。
|
|
3786
|
+
* 空 selection 不结束其他交互;非空 selection 先完成已接受交互,再以
|
|
3787
|
+
* 一个 batch 和最多一条 History 提交。本命令不改变 selection,也不派发
|
|
3788
|
+
* `select:end`;最终 configured 净变化异步通知一次 `select:configured-change`。
|
|
3789
|
+
* geometry impact 可触发 `select:update`,纯 paint 不触发该几何事件。
|
|
3790
|
+
* 世界有效尺寸与位置仍使用 resize/Transform 命令。
|
|
3791
|
+
*/
|
|
3792
|
+
setProperties(patch) {
|
|
3793
|
+
this.controller?.setProperties(patch);
|
|
3794
|
+
}
|
|
3795
|
+
/**
|
|
3796
|
+
* Resizes the live selection to world visual/frame dimensions in one
|
|
3797
|
+
* semantic command. Omitted axes keep their current size; `top-left` is the
|
|
3798
|
+
* default fixed anchor. Parent and nested Group transforms are resolved
|
|
3799
|
+
* internally.
|
|
3800
|
+
*/
|
|
3801
|
+
resize(options = {}) {
|
|
3802
|
+
this.controller?.resize(options);
|
|
3803
|
+
}
|
|
3804
|
+
/** 围绕当前 selection frame 中心沿 frame 自身轴镜像。 */
|
|
3805
|
+
flip(direction) {
|
|
3806
|
+
this.controller?.flip(direction);
|
|
3807
|
+
}
|
|
3808
|
+
/** 删除当前 selection,并在 Canvas 删除安装后、drain/finish 前交付一次应用接缝。 */
|
|
3809
|
+
delete(afterInstall) {
|
|
3810
|
+
return deleteElements(this, afterInstall);
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* 复制 live selection 到 Clipboard v2;producer 最多同步执行一次,不改 Canvas、selection 或 History。
|
|
3814
|
+
*/
|
|
3815
|
+
copy(producer) {
|
|
3816
|
+
return copyElements(this, producer);
|
|
3817
|
+
}
|
|
3818
|
+
/** 从 system Clipboard 或同一 memory payload 粘贴,并在安装后交付可选应用接缝。 */
|
|
3819
|
+
paste(options) {
|
|
3820
|
+
return pasteElements(this, this.clipboardDeserializers, options);
|
|
3821
|
+
}
|
|
3822
|
+
/** 绑定当前唯一应用键盘命令 owner;cleanup 只清除仍属于本次绑定的引用。 */
|
|
3823
|
+
bindKeyboardCommands(commands) {
|
|
3824
|
+
this.keyboardCommands = commands;
|
|
3825
|
+
return () => {
|
|
3826
|
+
if (this.keyboardCommands === commands) this.keyboardCommands = void 0;
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
/** @internal Controller 读取当前唯一键盘 command owner。 */
|
|
3830
|
+
get keyboardCommandOwner() {
|
|
3831
|
+
return this.keyboardCommands;
|
|
3832
|
+
}
|
|
3833
|
+
useClipboard(...plugins) {
|
|
3834
|
+
for (const plugin of plugins) {
|
|
3835
|
+
const type = plugin.type.startsWith("f-") ? plugin.type.slice(2) : plugin.type;
|
|
3836
|
+
this.clipboardDeserializers.set(type, plugin.deserialize);
|
|
3837
|
+
}
|
|
3838
|
+
return this;
|
|
3839
|
+
}
|
|
3840
|
+
onActivate(root, layer) {
|
|
3841
|
+
super.onActivate(root, layer);
|
|
3842
|
+
if (!root) return;
|
|
3843
|
+
this.history ??= new HistoryManager(root);
|
|
3844
|
+
this.controller = new EditorInteractionController(this, root, this.history);
|
|
3845
|
+
}
|
|
3846
|
+
onDeactivate(root, layer) {
|
|
3847
|
+
this.controller?.destroy();
|
|
3848
|
+
this.controller = void 0;
|
|
3849
|
+
super.onDeactivate(root, layer);
|
|
3850
|
+
}
|
|
3851
|
+
paint() {
|
|
3852
|
+
if (this.controller) paintSelect(this, this.controller);
|
|
3853
|
+
}
|
|
3854
|
+
};
|
|
3855
|
+
//#endregion
|
|
3856
|
+
//#region packages/tools/src/select/port-index.ts
|
|
3857
|
+
/** Tools 唯一按 Root spatial phase 增量更新的 committed Port projection。 */
|
|
3858
|
+
var PortIndex = class {
|
|
3859
|
+
tree = new RBush();
|
|
3860
|
+
itemsByOwner = /* @__PURE__ */ new Map();
|
|
3861
|
+
dirtyOwners = /* @__PURE__ */ new Set();
|
|
3862
|
+
ownerObservationCleanups = /* @__PURE__ */ new Map();
|
|
3863
|
+
cleanups = [];
|
|
3864
|
+
frameFlush;
|
|
3865
|
+
lastCandidateCount = 0;
|
|
3866
|
+
projectionFlushCount = 0;
|
|
3867
|
+
constructor(root) {
|
|
3868
|
+
this.root = root;
|
|
3869
|
+
this.frameFlush = getEditorFrameFlush(root);
|
|
3870
|
+
this.frameFlush.setSpatialClient(this);
|
|
3871
|
+
for (const node of root.sceneRegistry.keyElements.values()) if (node instanceof Element) this.refreshOwner(node);
|
|
3872
|
+
this.cleanups.push(root.sceneRegistry.observeNodeRegistrations((node, registered) => {
|
|
3873
|
+
if (!(node instanceof Element)) return;
|
|
3874
|
+
if (registered) this.refreshOwner(node);
|
|
3875
|
+
else this.unregisterOwner(node);
|
|
3876
|
+
}), root.sceneRegistry.observeSceneChanges((node, affected) => {
|
|
3877
|
+
if (node === void 0 && affected === void 0) {
|
|
3878
|
+
for (const candidate of root.sceneRegistry.keyElements.values()) if (candidate instanceof Element) this.refreshOwner(candidate);
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (node instanceof Element) this.refreshOwner(node);
|
|
3882
|
+
for (const candidate of affected ?? []) if (candidate instanceof Element) this.refreshOwner(candidate);
|
|
3883
|
+
}));
|
|
3884
|
+
}
|
|
3885
|
+
hasDeclaredPorts(owner) {
|
|
3886
|
+
return owner.ports === true || owner.ports !== false && owner.ports.length > 0;
|
|
3887
|
+
}
|
|
3888
|
+
refreshOwner(owner) {
|
|
3889
|
+
if (this.root.enablePorts && owner.activeRoot === this.root && !owner.isLayerNode() && this.hasDeclaredPorts(owner)) {
|
|
3890
|
+
this.registerOwner(owner);
|
|
3891
|
+
this.markOwnerDirty(owner);
|
|
3892
|
+
} else this.unregisterOwner(owner);
|
|
3893
|
+
}
|
|
3894
|
+
registerOwner(owner) {
|
|
3895
|
+
if (this.ownerObservationCleanups.has(owner)) return;
|
|
3896
|
+
const removeTransform = owner.transform.observeTransformCommit(() => this.markOwnerDirty(owner));
|
|
3897
|
+
this.ownerObservationCleanups.set(owner, removeTransform);
|
|
3898
|
+
}
|
|
3899
|
+
unregisterOwner(owner) {
|
|
3900
|
+
const cleanup = this.ownerObservationCleanups.get(owner);
|
|
3901
|
+
if (!cleanup) return;
|
|
3902
|
+
cleanup();
|
|
3903
|
+
this.ownerObservationCleanups.delete(owner);
|
|
3904
|
+
this.markOwnerDirty(owner);
|
|
3905
|
+
}
|
|
3906
|
+
markOwnerDirty(owner) {
|
|
3907
|
+
if (this.dirtyOwners.has(owner)) return;
|
|
3908
|
+
this.dirtyOwners.add(owner);
|
|
3909
|
+
this.root.frameRuntime.requestFrame();
|
|
3910
|
+
}
|
|
3911
|
+
hasPendingSpatialWork() {
|
|
3912
|
+
return this.dirtyOwners.size > 0;
|
|
3913
|
+
}
|
|
3914
|
+
flushSpatialProjection() {
|
|
3915
|
+
if (this.dirtyOwners.size === 0) return;
|
|
3916
|
+
const owners = [...this.dirtyOwners];
|
|
3917
|
+
this.dirtyOwners.clear();
|
|
3918
|
+
this.projectionFlushCount++;
|
|
3919
|
+
for (const owner of owners) this.replaceOwnerProjection(owner);
|
|
3920
|
+
}
|
|
3921
|
+
replaceOwnerProjection(owner) {
|
|
3922
|
+
const previous = this.itemsByOwner.get(owner);
|
|
3923
|
+
if (previous) {
|
|
3924
|
+
this.itemsByOwner.delete(owner);
|
|
3925
|
+
for (const item of previous) this.tree.remove(item);
|
|
3926
|
+
}
|
|
3927
|
+
if (owner.activeRoot !== this.root || !this.ownerObservationCleanups.has(owner) || !this.root.enablePorts || owner.isLayerNode() || !owner.visible || owner.silent || owner.pickable === false) return;
|
|
3928
|
+
if (!this.root.getPaintOrderKey(owner)) return;
|
|
3929
|
+
const matrix = owner.transform.getWorldMatrixView();
|
|
3930
|
+
const items = owner.getConnectionPorts().map((port) => {
|
|
3931
|
+
const worldPosition = projectConnectionPortPoint(matrix, port.localPosition);
|
|
3932
|
+
return {
|
|
3933
|
+
ref: port.ref,
|
|
3934
|
+
kind: "endpoint-binding",
|
|
3935
|
+
worldPosition,
|
|
3936
|
+
definition: port.definition,
|
|
3937
|
+
owner,
|
|
3938
|
+
minX: worldPosition.x,
|
|
3939
|
+
minY: worldPosition.y,
|
|
3940
|
+
maxX: worldPosition.x,
|
|
3941
|
+
maxY: worldPosition.y
|
|
3942
|
+
};
|
|
3943
|
+
});
|
|
3944
|
+
if (items.length === 0) return;
|
|
3945
|
+
this.itemsByOwner.set(owner, items);
|
|
3946
|
+
this.tree.load(items);
|
|
3947
|
+
}
|
|
3948
|
+
query(point, radius) {
|
|
3949
|
+
const candidates = this.tree.search({
|
|
3950
|
+
minX: point.x - radius,
|
|
3951
|
+
minY: point.y - radius,
|
|
3952
|
+
maxX: point.x + radius,
|
|
3953
|
+
maxY: point.y + radius
|
|
3954
|
+
});
|
|
3955
|
+
this.lastCandidateCount = candidates.length;
|
|
3956
|
+
const radiusSquared = radius * radius;
|
|
3957
|
+
return candidates.filter(({ worldPosition }) => {
|
|
3958
|
+
const dx = worldPosition.x - point.x;
|
|
3959
|
+
const dy = worldPosition.y - point.y;
|
|
3960
|
+
return dx * dx + dy * dy < radiusSquared;
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
portsForOwners(owners) {
|
|
3964
|
+
const ports = [];
|
|
3965
|
+
for (const owner of owners) {
|
|
3966
|
+
const items = this.itemsByOwner.get(owner);
|
|
3967
|
+
if (items) ports.push(...items);
|
|
3968
|
+
}
|
|
3969
|
+
return ports;
|
|
3970
|
+
}
|
|
3971
|
+
/** @internal 性能结构门禁。 */
|
|
3972
|
+
get debugStats() {
|
|
3973
|
+
return {
|
|
3974
|
+
indexedPorts: this.tree.all().length,
|
|
3975
|
+
observedOwners: this.ownerObservationCleanups.size,
|
|
3976
|
+
pendingOwners: this.dirtyOwners.size,
|
|
3977
|
+
lastQueryCandidates: this.lastCandidateCount,
|
|
3978
|
+
projectionFlushes: this.projectionFlushCount
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
dispose() {
|
|
3982
|
+
this.frameFlush.setSpatialClient(null);
|
|
3983
|
+
for (const cleanup of this.cleanups) cleanup();
|
|
3984
|
+
for (const cleanup of this.ownerObservationCleanups.values()) cleanup();
|
|
3985
|
+
this.ownerObservationCleanups.clear();
|
|
3986
|
+
this.dirtyOwners.clear();
|
|
3987
|
+
this.itemsByOwner.clear();
|
|
3988
|
+
this.tree.clear();
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3991
|
+
//#endregion
|
|
3992
|
+
//#region packages/tools/src/select/snap.ts
|
|
3993
|
+
var NO_PORT_SNAP = {
|
|
3994
|
+
snap: null,
|
|
3995
|
+
state: null
|
|
3996
|
+
};
|
|
3997
|
+
var PortSnapTarget = class {
|
|
3998
|
+
state = null;
|
|
3999
|
+
#element;
|
|
4000
|
+
#portId;
|
|
4001
|
+
#definition;
|
|
4002
|
+
constructor(x, y, element, portId, definition) {
|
|
4003
|
+
this.x = x;
|
|
4004
|
+
this.y = y;
|
|
4005
|
+
this.#element = element;
|
|
4006
|
+
this.#portId = portId;
|
|
4007
|
+
this.#definition = definition;
|
|
4008
|
+
}
|
|
4009
|
+
get element() {
|
|
4010
|
+
return this.#element;
|
|
4011
|
+
}
|
|
4012
|
+
get portId() {
|
|
4013
|
+
return this.#portId;
|
|
4014
|
+
}
|
|
4015
|
+
get definition() {
|
|
4016
|
+
return this.#definition;
|
|
4017
|
+
}
|
|
4018
|
+
};
|
|
4019
|
+
var AXIS_EPSILON = .5;
|
|
4020
|
+
var MATCH_EPSILON = 1e-4;
|
|
4021
|
+
var EMPTY_ELEMENTS = [];
|
|
4022
|
+
function isAxisAlignedRect(pts) {
|
|
4023
|
+
return pts.length === 4 && Math.abs(pts[0].x - pts[3].x) < .01 && Math.abs(pts[1].x - pts[2].x) < .01 && Math.abs(pts[0].y - pts[1].y) < .01 && Math.abs(pts[3].y - pts[2].y) < .01;
|
|
4024
|
+
}
|
|
4025
|
+
function addEdgeMidpoints(pts) {
|
|
4026
|
+
const result = [...pts];
|
|
4027
|
+
if (pts.length === 4) {
|
|
4028
|
+
const [p0, p1, p2, p3] = pts;
|
|
4029
|
+
result.push({
|
|
4030
|
+
x: (p0.x + p1.x) / 2,
|
|
4031
|
+
y: (p0.y + p1.y) / 2
|
|
4032
|
+
}, {
|
|
4033
|
+
x: (p1.x + p2.x) / 2,
|
|
4034
|
+
y: (p1.y + p2.y) / 2
|
|
4035
|
+
}, {
|
|
4036
|
+
x: (p2.x + p3.x) / 2,
|
|
4037
|
+
y: (p2.y + p3.y) / 2
|
|
4038
|
+
}, {
|
|
4039
|
+
x: (p3.x + p0.x) / 2,
|
|
4040
|
+
y: (p3.y + p0.y) / 2
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
return result;
|
|
4044
|
+
}
|
|
4045
|
+
function groupAxis(points, valueOf, rangeOf) {
|
|
4046
|
+
const segments = [];
|
|
4047
|
+
for (const point of points) {
|
|
4048
|
+
const value = valueOf(point);
|
|
4049
|
+
const range = rangeOf(point);
|
|
4050
|
+
const segment = segments.find((candidate) => Math.abs(candidate.value - value) < AXIS_EPSILON);
|
|
4051
|
+
if (segment) {
|
|
4052
|
+
segment.min = Math.min(segment.min, range);
|
|
4053
|
+
segment.max = Math.max(segment.max, range);
|
|
4054
|
+
} else segments.push({
|
|
4055
|
+
value,
|
|
4056
|
+
min: range,
|
|
4057
|
+
max: range
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
4060
|
+
return segments;
|
|
4061
|
+
}
|
|
4062
|
+
function createSnapAxes(snapPoints) {
|
|
4063
|
+
if (isAxisAlignedRect(snapPoints)) {
|
|
4064
|
+
const { minX, minY, maxX, maxY } = Bound.fromPoints(snapPoints);
|
|
4065
|
+
return {
|
|
4066
|
+
vertical: [
|
|
4067
|
+
{
|
|
4068
|
+
value: minX,
|
|
4069
|
+
min: minY,
|
|
4070
|
+
max: maxY
|
|
4071
|
+
},
|
|
4072
|
+
{
|
|
4073
|
+
value: (minX + maxX) / 2,
|
|
4074
|
+
min: minY,
|
|
4075
|
+
max: maxY
|
|
4076
|
+
},
|
|
4077
|
+
{
|
|
4078
|
+
value: maxX,
|
|
4079
|
+
min: minY,
|
|
4080
|
+
max: maxY
|
|
4081
|
+
}
|
|
4082
|
+
],
|
|
4083
|
+
horizontal: [
|
|
4084
|
+
{
|
|
4085
|
+
value: minY,
|
|
4086
|
+
min: minX,
|
|
4087
|
+
max: maxX
|
|
4088
|
+
},
|
|
4089
|
+
{
|
|
4090
|
+
value: (minY + maxY) / 2,
|
|
4091
|
+
min: minX,
|
|
4092
|
+
max: maxX
|
|
4093
|
+
},
|
|
4094
|
+
{
|
|
4095
|
+
value: maxY,
|
|
4096
|
+
min: minX,
|
|
4097
|
+
max: maxX
|
|
4098
|
+
}
|
|
4099
|
+
]
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4102
|
+
const points = addEdgeMidpoints(snapPoints);
|
|
4103
|
+
return {
|
|
4104
|
+
vertical: groupAxis(points, (point) => point.x, (point) => point.y),
|
|
4105
|
+
horizontal: groupAxis(points, (point) => point.y, (point) => point.x)
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
function translateAxis(segments, valueDelta, rangeDelta) {
|
|
4109
|
+
return segments.map(({ value, min, max }) => ({
|
|
4110
|
+
value: value + valueDelta,
|
|
4111
|
+
min: min + rangeDelta,
|
|
4112
|
+
max: max + rangeDelta
|
|
4113
|
+
}));
|
|
4114
|
+
}
|
|
4115
|
+
function findClosest(moving, targets, threshold) {
|
|
4116
|
+
let minDiff = Infinity;
|
|
4117
|
+
let best = null;
|
|
4118
|
+
for (const target of targets) for (const candidate of moving) {
|
|
4119
|
+
const diff = target.value - candidate.value;
|
|
4120
|
+
const absDiff = Math.abs(diff);
|
|
4121
|
+
if (absDiff >= threshold) continue;
|
|
4122
|
+
if (absDiff < minDiff - MATCH_EPSILON) {
|
|
4123
|
+
minDiff = absDiff;
|
|
4124
|
+
best = {
|
|
4125
|
+
diff,
|
|
4126
|
+
targetVal: target.value,
|
|
4127
|
+
movingRange: {
|
|
4128
|
+
min: candidate.min,
|
|
4129
|
+
max: candidate.max
|
|
4130
|
+
},
|
|
4131
|
+
segments: [{
|
|
4132
|
+
min: target.min,
|
|
4133
|
+
max: target.max
|
|
4134
|
+
}]
|
|
4135
|
+
};
|
|
4136
|
+
} else if (absDiff <= minDiff + MATCH_EPSILON && best && Math.abs(best.targetVal - target.value) < MATCH_EPSILON && !best.segments.some((range) => range.min === target.min && range.max === target.max)) best.segments.push({
|
|
4137
|
+
min: target.min,
|
|
4138
|
+
max: target.max
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
4141
|
+
return best;
|
|
4142
|
+
}
|
|
4143
|
+
function buildAxisSnapLines(best, movingRange, type, scale) {
|
|
4144
|
+
const val = best.targetVal;
|
|
4145
|
+
const isVert = type === "vertical";
|
|
4146
|
+
const hasRoom = (dist) => dist * scale >= `${Math.round(dist)}`.length * 7 + 10;
|
|
4147
|
+
const allRanges = [{
|
|
4148
|
+
...movingRange,
|
|
4149
|
+
isMoving: true
|
|
4150
|
+
}];
|
|
4151
|
+
for (const seg of best.segments) allRanges.push({
|
|
4152
|
+
min: seg.min,
|
|
4153
|
+
max: seg.max ?? seg.min,
|
|
4154
|
+
isMoving: false
|
|
4155
|
+
});
|
|
4156
|
+
allRanges.sort((a, b) => a.min - b.min);
|
|
4157
|
+
const movingIdx = allRanges.findIndex((r) => r.isMoving);
|
|
4158
|
+
const mkPt = (along) => isVert ? {
|
|
4159
|
+
x: val,
|
|
4160
|
+
y: along
|
|
4161
|
+
} : {
|
|
4162
|
+
x: along,
|
|
4163
|
+
y: val
|
|
4164
|
+
};
|
|
4165
|
+
const mkTextPos = (mid) => isVert ? {
|
|
4166
|
+
x: val + 5,
|
|
4167
|
+
y: mid
|
|
4168
|
+
} : {
|
|
4169
|
+
x: mid,
|
|
4170
|
+
y: val - 5
|
|
4171
|
+
};
|
|
4172
|
+
const mkLine = (start, end, neighbor) => {
|
|
4173
|
+
const dist = end - start;
|
|
4174
|
+
const show = neighbor && hasRoom(dist);
|
|
4175
|
+
return {
|
|
4176
|
+
type,
|
|
4177
|
+
value: val,
|
|
4178
|
+
start,
|
|
4179
|
+
end,
|
|
4180
|
+
points: neighbor ? [mkPt(start), mkPt(end)] : [],
|
|
4181
|
+
distanceText: show ? `${Math.round(dist)}` : void 0,
|
|
4182
|
+
textPos: show ? mkTextPos((start + end) / 2) : void 0
|
|
4183
|
+
};
|
|
4184
|
+
};
|
|
4185
|
+
const lines = [];
|
|
4186
|
+
for (let idx = 0; idx < allRanges.length; idx++) {
|
|
4187
|
+
const range = allRanges[idx];
|
|
4188
|
+
if (range.isMoving) continue;
|
|
4189
|
+
if (range.max - range.min > 1) lines.push(mkLine(range.min, range.max, idx === movingIdx - 1 || idx === movingIdx + 1));
|
|
4190
|
+
}
|
|
4191
|
+
let maxReach = allRanges[0].max;
|
|
4192
|
+
for (let i = 1; i < allRanges.length; i++) {
|
|
4193
|
+
const next = allRanges[i];
|
|
4194
|
+
if (next.min > maxReach && next.min - maxReach > 0) lines.push(mkLine(maxReach, next.min, i - 1 === movingIdx || i === movingIdx));
|
|
4195
|
+
maxReach = Math.max(maxReach, next.max);
|
|
4196
|
+
}
|
|
4197
|
+
return lines;
|
|
4198
|
+
}
|
|
4199
|
+
var Snap = class extends Element {
|
|
4200
|
+
type = "snap";
|
|
4201
|
+
threshold = 5;
|
|
4202
|
+
lineColor = "#ff00cc";
|
|
4203
|
+
lineWidth = 1;
|
|
4204
|
+
dashPattern = [4, 4];
|
|
4205
|
+
isSnapping = false;
|
|
4206
|
+
snapLines = [];
|
|
4207
|
+
snapCandidates = [];
|
|
4208
|
+
axisElementExcludes = /* @__PURE__ */ new Set();
|
|
4209
|
+
axisPointExcludes = /* @__PURE__ */ new Map();
|
|
4210
|
+
axisTargets = null;
|
|
4211
|
+
portIndex = null;
|
|
4212
|
+
showViewportPorts = false;
|
|
4213
|
+
viewportPortTargets = /* @__PURE__ */ new Map();
|
|
4214
|
+
activePortHighlight = null;
|
|
4215
|
+
portProbe = null;
|
|
4216
|
+
snapViewportState = null;
|
|
4217
|
+
sceneChangeCleanup;
|
|
4218
|
+
portHighlights = [];
|
|
4219
|
+
constructor(options) {
|
|
4220
|
+
const elementOptions = {
|
|
4221
|
+
selectable: false,
|
|
4222
|
+
...options
|
|
4223
|
+
};
|
|
4224
|
+
super(elementOptions, new ElementProperties(elementOptions), ToolOverlayTransform);
|
|
4225
|
+
}
|
|
4226
|
+
onActivate(root, layer) {
|
|
4227
|
+
super.onActivate(root, layer);
|
|
4228
|
+
if (root) this.portIndex = new PortIndex(root);
|
|
4229
|
+
}
|
|
4230
|
+
onDeactivate(root, layer) {
|
|
4231
|
+
this.stop();
|
|
4232
|
+
this.portIndex?.dispose();
|
|
4233
|
+
this.portIndex = null;
|
|
4234
|
+
super.onDeactivate(root, layer);
|
|
4235
|
+
}
|
|
4236
|
+
collectSnapCandidates() {
|
|
4237
|
+
const snapCandidates = [];
|
|
4238
|
+
const portOwners = /* @__PURE__ */ new Set();
|
|
4239
|
+
const processed = /* @__PURE__ */ new Set();
|
|
4240
|
+
for (const element of this.root.queryArea(this.root.viewport.getWorldRectView(), {
|
|
4241
|
+
bounds: "hit",
|
|
4242
|
+
order: "none"
|
|
4243
|
+
})) {
|
|
4244
|
+
portOwners.add(element);
|
|
4245
|
+
const resolved = checkElement(element);
|
|
4246
|
+
if (!resolved || processed.has(resolved)) continue;
|
|
4247
|
+
processed.add(resolved);
|
|
4248
|
+
snapCandidates.push(resolved);
|
|
4249
|
+
}
|
|
4250
|
+
return {
|
|
4251
|
+
snapCandidates,
|
|
4252
|
+
portOwners
|
|
4253
|
+
};
|
|
4254
|
+
}
|
|
4255
|
+
rebuildPortHighlights(portOwners) {
|
|
4256
|
+
this.activePortHighlight = null;
|
|
4257
|
+
this.viewportPortTargets = /* @__PURE__ */ new Map();
|
|
4258
|
+
if (!this.showViewportPorts) {
|
|
4259
|
+
this.portHighlights = [];
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
const highlights = [];
|
|
4263
|
+
for (const port of this.portIndex?.portsForOwners(portOwners) ?? []) {
|
|
4264
|
+
if (!this.root.isPointerTarget(port.owner)) continue;
|
|
4265
|
+
const target = new PortSnapTarget(port.worldPosition.x, port.worldPosition.y, port.owner, port.ref.portId, port.definition);
|
|
4266
|
+
this.viewportPortTargets.set(port, target);
|
|
4267
|
+
highlights.push(target);
|
|
4268
|
+
}
|
|
4269
|
+
this.portHighlights = highlights;
|
|
4270
|
+
}
|
|
4271
|
+
refreshSnapCandidates() {
|
|
4272
|
+
const viewport = this.root.viewport;
|
|
4273
|
+
const current = this.snapViewportState;
|
|
4274
|
+
if (current && current.x === viewport.x && current.y === viewport.y && current.scale === viewport.scale && current.width === this.root.width && current.height === this.root.height) return;
|
|
4275
|
+
this.snapViewportState = {
|
|
4276
|
+
x: viewport.x,
|
|
4277
|
+
y: viewport.y,
|
|
4278
|
+
scale: viewport.scale,
|
|
4279
|
+
width: this.root.width,
|
|
4280
|
+
height: this.root.height
|
|
4281
|
+
};
|
|
4282
|
+
const candidates = this.collectSnapCandidates();
|
|
4283
|
+
this.snapCandidates = candidates.snapCandidates;
|
|
4284
|
+
this.axisTargets = null;
|
|
4285
|
+
this.portProbe = null;
|
|
4286
|
+
this.rebuildPortHighlights(candidates.portOwners);
|
|
4287
|
+
}
|
|
4288
|
+
invalidateSnapshot() {
|
|
4289
|
+
if (isNull(this.snapViewportState)) return;
|
|
4290
|
+
this.snapViewportState = null;
|
|
4291
|
+
this.axisTargets = null;
|
|
4292
|
+
this.transform.markPaintDirty();
|
|
4293
|
+
}
|
|
4294
|
+
sceneChangeIsExcluded(node) {
|
|
4295
|
+
return node instanceof Element && (node === this || this.axisElementExcludes.has(node) || this.axisPointExcludes.has(node));
|
|
4296
|
+
}
|
|
4297
|
+
handleSceneChange = (node, affected) => {
|
|
4298
|
+
if (affected !== void 0 ? affected.length > 0 && affected.every(this.sceneChangeIsExcluded, this) : node !== void 0 && this.sceneChangeIsExcluded(node)) return;
|
|
4299
|
+
this.portProbe = null;
|
|
4300
|
+
if (!this.root.enablePorts) {
|
|
4301
|
+
this.viewportPortTargets = /* @__PURE__ */ new Map();
|
|
4302
|
+
this.activePortHighlight = null;
|
|
4303
|
+
this.portHighlights = [];
|
|
4304
|
+
}
|
|
4305
|
+
this.invalidateSnapshot();
|
|
4306
|
+
};
|
|
4307
|
+
start(excludeEls, excludePoints, showViewportPorts = false) {
|
|
4308
|
+
this.isSnapping = true;
|
|
4309
|
+
this.showViewportPorts = showViewportPorts;
|
|
4310
|
+
this.snapLines = [];
|
|
4311
|
+
this.portHighlights = [];
|
|
4312
|
+
this.portProbe = null;
|
|
4313
|
+
this.axisTargets = null;
|
|
4314
|
+
this.sceneChangeCleanup?.();
|
|
4315
|
+
this.sceneChangeCleanup = this.root.sceneRegistry.observeSceneChanges(this.handleSceneChange);
|
|
4316
|
+
this.snapViewportState = null;
|
|
4317
|
+
this.axisElementExcludes = new Set(excludeEls);
|
|
4318
|
+
for (const element of excludeEls) {
|
|
4319
|
+
const resolved = checkElement(element);
|
|
4320
|
+
if (resolved) this.axisElementExcludes.add(resolved);
|
|
4321
|
+
}
|
|
4322
|
+
this.axisPointExcludes = /* @__PURE__ */ new Map();
|
|
4323
|
+
if (excludePoints) for (const ep of excludePoints) this.axisPointExcludes.set(ep.element, new Set(ep.indices));
|
|
4324
|
+
this.refreshSnapCandidates();
|
|
4325
|
+
this.transform.markPaintDirty();
|
|
4326
|
+
}
|
|
4327
|
+
getAxisTargets() {
|
|
4328
|
+
if (this.axisTargets) return this.axisTargets;
|
|
4329
|
+
const cache = {
|
|
4330
|
+
vertical: [],
|
|
4331
|
+
horizontal: []
|
|
4332
|
+
};
|
|
4333
|
+
for (const node of this.snapCandidates) {
|
|
4334
|
+
if (this.axisElementExcludes.has(node)) continue;
|
|
4335
|
+
const capability = getEditorCapability(node, SnapSourceCapabilityToken);
|
|
4336
|
+
if (!capability) continue;
|
|
4337
|
+
let snapPoints = capability.getWorldSnapPointsView();
|
|
4338
|
+
if (snapPoints.length === 0) continue;
|
|
4339
|
+
const skipIndices = this.axisPointExcludes.get(node);
|
|
4340
|
+
if (skipIndices) snapPoints = snapPoints.filter((_, i) => !skipIndices.has(i));
|
|
4341
|
+
if (snapPoints.length === 0) continue;
|
|
4342
|
+
const axes = createSnapAxes(snapPoints);
|
|
4343
|
+
cache.vertical.push(...axes.vertical);
|
|
4344
|
+
cache.horizontal.push(...axes.horizontal);
|
|
4345
|
+
}
|
|
4346
|
+
this.axisTargets = cache;
|
|
4347
|
+
return cache;
|
|
4348
|
+
}
|
|
4349
|
+
detect(originalPoints, rawDx, rawDy) {
|
|
4350
|
+
if (!this.isSnapping) return {
|
|
4351
|
+
dx: 0,
|
|
4352
|
+
dy: 0
|
|
4353
|
+
};
|
|
4354
|
+
this.snapLines = [];
|
|
4355
|
+
this.refreshSnapCandidates();
|
|
4356
|
+
const moving = createSnapAxes(originalPoints);
|
|
4357
|
+
const threshold = this.threshold / this.root.viewport.scale;
|
|
4358
|
+
const targets = this.getAxisTargets();
|
|
4359
|
+
const vertical = findClosest(translateAxis(moving.vertical, rawDx, rawDy), targets.vertical, threshold);
|
|
4360
|
+
const horizontal = findClosest(translateAxis(moving.horizontal, rawDy, rawDx), targets.horizontal, threshold);
|
|
4361
|
+
const dx = vertical?.diff ?? 0;
|
|
4362
|
+
const dy = horizontal?.diff ?? 0;
|
|
4363
|
+
const scale = this.root.viewport.scale;
|
|
4364
|
+
if (vertical) this.snapLines.push(...buildAxisSnapLines(vertical, {
|
|
4365
|
+
min: vertical.movingRange.min + dy,
|
|
4366
|
+
max: vertical.movingRange.max + dy
|
|
4367
|
+
}, "vertical", scale));
|
|
4368
|
+
if (horizontal) this.snapLines.push(...buildAxisSnapLines(horizontal, {
|
|
4369
|
+
min: horizontal.movingRange.min + dx,
|
|
4370
|
+
max: horizontal.movingRange.max + dx
|
|
4371
|
+
}, "horizontal", scale));
|
|
4372
|
+
this.transform.markPaintDirty();
|
|
4373
|
+
return {
|
|
4374
|
+
dx,
|
|
4375
|
+
dy
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
stop() {
|
|
4379
|
+
this.isSnapping = false;
|
|
4380
|
+
this.snapLines = [];
|
|
4381
|
+
this.snapCandidates = [];
|
|
4382
|
+
this.axisElementExcludes = /* @__PURE__ */ new Set();
|
|
4383
|
+
this.axisPointExcludes = /* @__PURE__ */ new Map();
|
|
4384
|
+
this.axisTargets = null;
|
|
4385
|
+
this.sceneChangeCleanup?.();
|
|
4386
|
+
this.sceneChangeCleanup = void 0;
|
|
4387
|
+
this.snapViewportState = null;
|
|
4388
|
+
this.portProbe = null;
|
|
4389
|
+
this.showViewportPorts = false;
|
|
4390
|
+
this.viewportPortTargets = /* @__PURE__ */ new Map();
|
|
4391
|
+
this.activePortHighlight = null;
|
|
4392
|
+
this.portHighlights = [];
|
|
4393
|
+
this.transform.markPaintDirty();
|
|
4394
|
+
}
|
|
4395
|
+
findClosestPortTarget(worldX, worldY, excludeEls) {
|
|
4396
|
+
const threshold = 10 / this.root.viewport.scale;
|
|
4397
|
+
const excluded = new Set(excludeEls);
|
|
4398
|
+
let best = null;
|
|
4399
|
+
let bestLayerOrder = -1;
|
|
4400
|
+
let bestElementOrder = -1;
|
|
4401
|
+
let bestDistance = threshold * threshold;
|
|
4402
|
+
for (const candidate of this.portIndex?.query({
|
|
4403
|
+
x: worldX,
|
|
4404
|
+
y: worldY
|
|
4405
|
+
}, threshold) ?? []) {
|
|
4406
|
+
const visibleTarget = this.viewportPortTargets.get(candidate);
|
|
4407
|
+
if (this.showViewportPorts && !visibleTarget) continue;
|
|
4408
|
+
const element = visibleTarget?.element ?? candidate.owner;
|
|
4409
|
+
if (excluded.has(element) || !this.root.isPointerTarget(element)) continue;
|
|
4410
|
+
const paintOrder = this.root.getPaintOrderKey(element);
|
|
4411
|
+
if (!paintOrder) continue;
|
|
4412
|
+
const dx = candidate.worldPosition.x - worldX;
|
|
4413
|
+
const dy = candidate.worldPosition.y - worldY;
|
|
4414
|
+
const d2 = dx * dx + dy * dy;
|
|
4415
|
+
const topmost = best && d2 === bestDistance && (paintOrder.layer > bestLayerOrder || paintOrder.layer === bestLayerOrder && paintOrder.element > bestElementOrder);
|
|
4416
|
+
if (d2 >= bestDistance && !topmost) continue;
|
|
4417
|
+
best = candidate;
|
|
4418
|
+
bestLayerOrder = paintOrder.layer;
|
|
4419
|
+
bestElementOrder = paintOrder.element;
|
|
4420
|
+
bestDistance = d2;
|
|
4421
|
+
}
|
|
4422
|
+
return best ? this.viewportPortTargets.get(best) ?? new PortSnapTarget(best.worldPosition.x, best.worldPosition.y, best.owner, best.ref.portId, best.definition) : null;
|
|
4423
|
+
}
|
|
4424
|
+
clearActivePortHighlight() {
|
|
4425
|
+
if (this.activePortHighlight) this.activePortHighlight.state = null;
|
|
4426
|
+
this.activePortHighlight = null;
|
|
4427
|
+
if (!this.showViewportPorts) this.portHighlights = [];
|
|
4428
|
+
}
|
|
4429
|
+
probeForbiddenPortMove(points, dx, dy, excludeEls = EMPTY_ELEMENTS) {
|
|
4430
|
+
if (!this.isSnapping || !this.root.enablePorts || points.length === 0) return false;
|
|
4431
|
+
this.refreshSnapCandidates();
|
|
4432
|
+
this.clearActivePortHighlight();
|
|
4433
|
+
let bestTarget = null;
|
|
4434
|
+
let bestDistance = Infinity;
|
|
4435
|
+
for (const point of points) {
|
|
4436
|
+
const x = point.x + dx;
|
|
4437
|
+
const y = point.y + dy;
|
|
4438
|
+
const target = this.findClosestPortTarget(x, y, excludeEls);
|
|
4439
|
+
if (!target) continue;
|
|
4440
|
+
const distance = (target.x - x) ** 2 + (target.y - y) ** 2;
|
|
4441
|
+
if (distance >= bestDistance) continue;
|
|
4442
|
+
bestDistance = distance;
|
|
4443
|
+
bestTarget = target;
|
|
4444
|
+
}
|
|
4445
|
+
const result = bestTarget !== null;
|
|
4446
|
+
if (bestTarget) {
|
|
4447
|
+
bestTarget.state = "forbidden";
|
|
4448
|
+
this.activePortHighlight = bestTarget;
|
|
4449
|
+
}
|
|
4450
|
+
this.snapLines = [];
|
|
4451
|
+
if (!this.showViewportPorts) this.portHighlights = bestTarget ? [bestTarget] : [];
|
|
4452
|
+
this.transform.markPaintDirty();
|
|
4453
|
+
return result;
|
|
4454
|
+
}
|
|
4455
|
+
probePortSnap(worldX, worldY, role, excludeEls = EMPTY_ELEMENTS, excludeSource) {
|
|
4456
|
+
if (!this.isSnapping || !this.root.enablePorts) return NO_PORT_SNAP;
|
|
4457
|
+
this.refreshSnapCandidates();
|
|
4458
|
+
const cached = this.portProbe;
|
|
4459
|
+
if (cached && cached.x === worldX && cached.y === worldY && cached.scale === this.root.viewport.scale && cached.role === role && cached.excludeEls === excludeEls && cached.excludeSource?.elementKey === excludeSource?.elementKey && cached.excludeSource?.anchorId === excludeSource?.anchorId) return cached.result;
|
|
4460
|
+
this.clearActivePortHighlight();
|
|
4461
|
+
const target = this.findClosestPortTarget(worldX, worldY, excludeEls);
|
|
4462
|
+
let result;
|
|
4463
|
+
if (!target) result = {
|
|
4464
|
+
snap: null,
|
|
4465
|
+
state: null
|
|
4466
|
+
};
|
|
4467
|
+
else if (!connectionPortAccepts(target.definition, role)) result = {
|
|
4468
|
+
snap: null,
|
|
4469
|
+
state: "incompatible"
|
|
4470
|
+
};
|
|
4471
|
+
else if (!hasConnectionPortCapacity(this.root, {
|
|
4472
|
+
elementKey: target.element.key,
|
|
4473
|
+
portId: target.portId
|
|
4474
|
+
}, target.definition, role, excludeSource)) result = {
|
|
4475
|
+
snap: null,
|
|
4476
|
+
state: "forbidden"
|
|
4477
|
+
};
|
|
4478
|
+
else result = {
|
|
4479
|
+
snap: {
|
|
4480
|
+
x: target.x,
|
|
4481
|
+
y: target.y,
|
|
4482
|
+
elementKey: target.element.key,
|
|
4483
|
+
portId: target.portId
|
|
4484
|
+
},
|
|
4485
|
+
state: "available"
|
|
4486
|
+
};
|
|
4487
|
+
if (target) {
|
|
4488
|
+
target.state = result.state;
|
|
4489
|
+
this.activePortHighlight = target;
|
|
4490
|
+
}
|
|
4491
|
+
this.portProbe = {
|
|
4492
|
+
x: worldX,
|
|
4493
|
+
y: worldY,
|
|
4494
|
+
scale: this.root.viewport.scale,
|
|
4495
|
+
role,
|
|
4496
|
+
excludeEls,
|
|
4497
|
+
excludeSource,
|
|
4498
|
+
result
|
|
4499
|
+
};
|
|
4500
|
+
if (!this.showViewportPorts) this.portHighlights = target ? [target] : [];
|
|
4501
|
+
this.transform.markPaintDirty();
|
|
4502
|
+
return result;
|
|
4503
|
+
}
|
|
4504
|
+
detectPortSnap(worldX, worldY, role, excludeEls = EMPTY_ELEMENTS, excludeSource) {
|
|
4505
|
+
return this.probePortSnap(worldX, worldY, role, excludeEls, excludeSource).snap;
|
|
4506
|
+
}
|
|
4507
|
+
paint() {
|
|
4508
|
+
if ((!this.isSnapping || this.snapLines.length === 0) && this.portHighlights.length === 0) return;
|
|
4509
|
+
const ctx = this.layer.ctx;
|
|
4510
|
+
const scale = this.root.viewport.scale;
|
|
4511
|
+
ctx.save();
|
|
4512
|
+
this.root.viewport.applyTransform(ctx);
|
|
4513
|
+
ctx.lineWidth = this.lineWidth / scale;
|
|
4514
|
+
ctx.strokeStyle = this.lineColor;
|
|
4515
|
+
ctx.fillStyle = this.lineColor;
|
|
4516
|
+
ctx.font = `${12 / scale}px Arial`;
|
|
4517
|
+
for (const line of this.snapLines) {
|
|
4518
|
+
ctx.setLineDash(this.dashPattern.map((length) => length / scale));
|
|
4519
|
+
ctx.beginPath();
|
|
4520
|
+
if (line.type === "vertical") {
|
|
4521
|
+
ctx.moveTo(line.value, line.start);
|
|
4522
|
+
ctx.lineTo(line.value, line.end);
|
|
4523
|
+
} else {
|
|
4524
|
+
ctx.moveTo(line.start, line.value);
|
|
4525
|
+
ctx.lineTo(line.end, line.value);
|
|
4526
|
+
}
|
|
4527
|
+
ctx.stroke();
|
|
4528
|
+
ctx.setLineDash([]);
|
|
4529
|
+
const size = 3 / scale;
|
|
4530
|
+
for (const p of line.points) {
|
|
4531
|
+
ctx.beginPath();
|
|
4532
|
+
ctx.moveTo(p.x - size, p.y - size);
|
|
4533
|
+
ctx.lineTo(p.x + size, p.y + size);
|
|
4534
|
+
ctx.moveTo(p.x + size, p.y - size);
|
|
4535
|
+
ctx.lineTo(p.x - size, p.y + size);
|
|
4536
|
+
ctx.stroke();
|
|
4537
|
+
}
|
|
4538
|
+
if (line.distanceText && line.textPos) {
|
|
4539
|
+
ctx.textAlign = line.type === "vertical" ? "left" : "center";
|
|
4540
|
+
ctx.fillText(line.distanceText, line.textPos.x, line.textPos.y);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
if (this.portHighlights.length > 0) for (const h of this.portHighlights) if (h.state) {
|
|
4544
|
+
const stroke = h.state === "available" ? this.root.skin.selection : h.state === "incompatible" ? "#F59E0B" : "#EF4444";
|
|
4545
|
+
const fill = h.state === "available" ? this.root.skin.selection : h.state === "incompatible" ? "rgba(245, 158, 11, 0.2)" : "rgba(239, 68, 68, 0.2)";
|
|
4546
|
+
ctx.strokeStyle = stroke;
|
|
4547
|
+
ctx.lineWidth = 2 / scale;
|
|
4548
|
+
ctx.fillStyle = fill;
|
|
4549
|
+
ctx.beginPath();
|
|
4550
|
+
ctx.arc(h.x, h.y, 6 / scale, 0, Math.PI * 2);
|
|
4551
|
+
if (h.state === "available") {
|
|
4552
|
+
ctx.save();
|
|
4553
|
+
ctx.globalAlpha *= .2;
|
|
4554
|
+
}
|
|
4555
|
+
ctx.fill();
|
|
4556
|
+
if (h.state === "available") ctx.restore();
|
|
4557
|
+
ctx.stroke();
|
|
4558
|
+
if (h.state === "forbidden") {
|
|
4559
|
+
const offset = 4 / scale;
|
|
4560
|
+
ctx.beginPath();
|
|
4561
|
+
ctx.moveTo(h.x - offset, h.y + offset);
|
|
4562
|
+
ctx.lineTo(h.x + offset, h.y - offset);
|
|
4563
|
+
ctx.stroke();
|
|
4564
|
+
}
|
|
4565
|
+
} else {
|
|
4566
|
+
ctx.fillStyle = this.root.skin.selection;
|
|
4567
|
+
ctx.beginPath();
|
|
4568
|
+
ctx.arc(h.x, h.y, 3 / scale, 0, Math.PI * 2);
|
|
4569
|
+
ctx.save();
|
|
4570
|
+
ctx.globalAlpha *= .4;
|
|
4571
|
+
ctx.fill();
|
|
4572
|
+
ctx.restore();
|
|
4573
|
+
}
|
|
4574
|
+
ctx.restore();
|
|
4575
|
+
}
|
|
4576
|
+
};
|
|
4577
|
+
//#endregion
|
|
4578
|
+
//#region packages/tools/src/line/index.ts
|
|
4579
|
+
/**
|
|
4580
|
+
* Line command/paint surface。Root listeners、mode、draft 与 History 全由唯一
|
|
4581
|
+
* EditorInteractionController 拥有。
|
|
4582
|
+
*/
|
|
4583
|
+
var LineTool = class extends Element {
|
|
4584
|
+
type = "lineTool";
|
|
4585
|
+
contentLayer;
|
|
4586
|
+
resultType;
|
|
4587
|
+
tailDecoration;
|
|
4588
|
+
constructor(options = {}) {
|
|
4589
|
+
const { contentLayer, resultType = "line", tailDecoration = "arrow", ...elementOptions } = options;
|
|
4590
|
+
const elementOptionsWithDefaults = {
|
|
4591
|
+
width: 0,
|
|
4592
|
+
height: 0,
|
|
4593
|
+
selectable: false,
|
|
4594
|
+
pickable: false,
|
|
4595
|
+
...elementOptions
|
|
4596
|
+
};
|
|
4597
|
+
super(elementOptionsWithDefaults, new ElementProperties(elementOptionsWithDefaults), ToolOverlayTransform);
|
|
4598
|
+
this.contentLayer = contentLayer;
|
|
4599
|
+
this.resultType = resultType;
|
|
4600
|
+
this.tailDecoration = tailDecoration;
|
|
4601
|
+
}
|
|
4602
|
+
startDrawing() {
|
|
4603
|
+
getEditorInteractionController(this.root).startLineDrawing(this);
|
|
4604
|
+
}
|
|
4605
|
+
stopDrawing() {
|
|
4606
|
+
const controller = getEditorInteractionController(this.root);
|
|
4607
|
+
if (controller.hasLineToolSession(this)) controller.stopLineDrawing();
|
|
4608
|
+
}
|
|
4609
|
+
cancelDrawing() {
|
|
4610
|
+
const controller = getEditorInteractionController(this.root);
|
|
4611
|
+
if (controller.hasLineToolSession(this)) controller.cancelLineDrawing();
|
|
4612
|
+
}
|
|
4613
|
+
onDeactivate(root, layer) {
|
|
4614
|
+
const controller = root && getEditorInteractionController(root);
|
|
4615
|
+
if (controller?.hasLineToolSession(this)) controller.cancelLineDrawing();
|
|
4616
|
+
super.onDeactivate(root, layer);
|
|
4617
|
+
}
|
|
4618
|
+
/** @internal controller 在 final confirm 时解析唯一 structure destination。 */
|
|
4619
|
+
getContentLayerForController() {
|
|
4620
|
+
const destination = this.contentLayer;
|
|
4621
|
+
if (!isUndefined(destination)) {
|
|
4622
|
+
const layer = isString(destination) ? this.root.children.find((child) => child.key === destination) : destination;
|
|
4623
|
+
if (!layer || layer.parent !== this.root) throw new Error("LineTool contentLayer must be a direct child of its Root");
|
|
4624
|
+
return layer;
|
|
4625
|
+
}
|
|
4626
|
+
const layer = this.root.children.find((candidate) => candidate !== this.layer) ?? this.root.children[0];
|
|
4627
|
+
if (!layer) throw new Error("LineTool requires a content Layer");
|
|
4628
|
+
return layer;
|
|
4629
|
+
}
|
|
4630
|
+
/** @internal drawing session开始时读取一次,之后不再跟随配置变化。 */
|
|
4631
|
+
getResultTypeForController() {
|
|
4632
|
+
return this.resultType;
|
|
4633
|
+
}
|
|
4634
|
+
/** @internal drawing session 开始时读取一次。 */
|
|
4635
|
+
getTailDecorationForController() {
|
|
4636
|
+
return this.tailDecoration;
|
|
4637
|
+
}
|
|
4638
|
+
paint() {
|
|
4639
|
+
const session = getEditorInteractionController(this.root)?.getLineDrawingView(this);
|
|
4640
|
+
if (!session) return;
|
|
4641
|
+
const ctx = this.layer.ctx;
|
|
4642
|
+
const viewport = this.root.viewport;
|
|
4643
|
+
ctx.save();
|
|
4644
|
+
ctx.setTransform(viewport.scale * viewport.dpr, 0, 0, viewport.scale * viewport.dpr, viewport.x * viewport.dpr, viewport.y * viewport.dpr);
|
|
4645
|
+
const scale = viewport.scale;
|
|
4646
|
+
const pointSize = 4 / scale;
|
|
4647
|
+
const points = session.confirmed;
|
|
4648
|
+
if (points.length > 0) {
|
|
4649
|
+
ctx.strokeStyle = this.root.skin.lineStroke;
|
|
4650
|
+
ctx.lineWidth = 2 / scale;
|
|
4651
|
+
ctx.lineCap = "round";
|
|
4652
|
+
ctx.lineJoin = "round";
|
|
4653
|
+
ctx.beginPath();
|
|
4654
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
4655
|
+
for (let index = 1; index < points.length; index++) ctx.lineTo(points[index].x, points[index].y);
|
|
4656
|
+
const preview = session.snap ?? session.preview;
|
|
4657
|
+
if (preview) ctx.lineTo(preview.x, preview.y);
|
|
4658
|
+
ctx.stroke();
|
|
4659
|
+
ctx.fillStyle = this.root.skin.selection;
|
|
4660
|
+
for (const point of points) {
|
|
4661
|
+
ctx.beginPath();
|
|
4662
|
+
ctx.arc(point.x, point.y, pointSize, 0, Math.PI * 2);
|
|
4663
|
+
ctx.fill();
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
ctx.restore();
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
//#endregion
|
|
4670
|
+
//#region packages/tools/src/rule/index.ts
|
|
4671
|
+
var SELECTION_RANGE_SIZE = 5;
|
|
4672
|
+
var SELECTION_LABEL_OFFSET = 7;
|
|
4673
|
+
var TICK_LABEL_OFFSET = 14;
|
|
4674
|
+
function formatRulerValue(value, precision) {
|
|
4675
|
+
return Math.abs(value) < 1e-10 ? "0" : value.toFixed(precision);
|
|
4676
|
+
}
|
|
4677
|
+
function niceStep(pxPerTick, scale) {
|
|
4678
|
+
const raw = pxPerTick / scale;
|
|
4679
|
+
const pow = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
4680
|
+
const candidates = [
|
|
4681
|
+
1,
|
|
4682
|
+
2,
|
|
4683
|
+
5,
|
|
4684
|
+
10
|
|
4685
|
+
];
|
|
4686
|
+
let best = candidates[0] * pow;
|
|
4687
|
+
for (const candidate of candidates) {
|
|
4688
|
+
const value = candidate * pow;
|
|
4689
|
+
if (Math.abs(value - raw) < Math.abs(best - raw)) best = value;
|
|
4690
|
+
}
|
|
4691
|
+
return best;
|
|
4692
|
+
}
|
|
4693
|
+
var DEFAULT_RULER_SIZE = 25;
|
|
4694
|
+
var DEFAULT_BACKGROUND_COLOR = "#ffffff";
|
|
4695
|
+
var DEFAULT_CORNER_BACKGROUND_COLOR = "#f0f0f0";
|
|
4696
|
+
var DEFAULT_MAJOR_TICK_COLOR = "#c0c0c0";
|
|
4697
|
+
var DEFAULT_MINOR_TICK_COLOR = "#c0c0c0";
|
|
4698
|
+
var DEFAULT_BORDER_COLOR = "#dadada";
|
|
4699
|
+
var DEFAULT_TEXT_COLOR = "#333333";
|
|
4700
|
+
var Rule = class extends Element {
|
|
4701
|
+
type = "rule";
|
|
4702
|
+
appearance;
|
|
4703
|
+
select;
|
|
4704
|
+
workspaceFocusInsets = () => ({
|
|
4705
|
+
top: this.rulerSize,
|
|
4706
|
+
left: this.rulerSize
|
|
4707
|
+
});
|
|
4708
|
+
constructor(options = {}) {
|
|
4709
|
+
const { rulerSize, backgroundColor, cornerBackgroundColor, majorTickColor, minorTickColor, borderColor, textColor, selectionColor, select, ...baseOptions } = options;
|
|
4710
|
+
const elementOptions = {
|
|
4711
|
+
selectable: false,
|
|
4712
|
+
...baseOptions
|
|
4713
|
+
};
|
|
4714
|
+
super(elementOptions, new ElementProperties(elementOptions), ToolOverlayTransform);
|
|
4715
|
+
this.appearance = {
|
|
4716
|
+
rulerSize: rulerSize ?? DEFAULT_RULER_SIZE,
|
|
4717
|
+
backgroundColor: backgroundColor ?? DEFAULT_BACKGROUND_COLOR,
|
|
4718
|
+
cornerBackgroundColor: cornerBackgroundColor ?? DEFAULT_CORNER_BACKGROUND_COLOR,
|
|
4719
|
+
majorTickColor: majorTickColor ?? DEFAULT_MAJOR_TICK_COLOR,
|
|
4720
|
+
minorTickColor: minorTickColor ?? DEFAULT_MINOR_TICK_COLOR,
|
|
4721
|
+
borderColor: borderColor ?? DEFAULT_BORDER_COLOR,
|
|
4722
|
+
textColor: textColor ?? DEFAULT_TEXT_COLOR,
|
|
4723
|
+
selectionColor
|
|
4724
|
+
};
|
|
4725
|
+
this.select = select;
|
|
4726
|
+
}
|
|
4727
|
+
get rulerSize() {
|
|
4728
|
+
return this.appearance.rulerSize;
|
|
4729
|
+
}
|
|
4730
|
+
get backgroundColor() {
|
|
4731
|
+
return this.appearance.backgroundColor;
|
|
4732
|
+
}
|
|
4733
|
+
get cornerBackgroundColor() {
|
|
4734
|
+
return this.appearance.cornerBackgroundColor;
|
|
4735
|
+
}
|
|
4736
|
+
get majorTickColor() {
|
|
4737
|
+
return this.appearance.majorTickColor;
|
|
4738
|
+
}
|
|
4739
|
+
get minorTickColor() {
|
|
4740
|
+
return this.appearance.minorTickColor;
|
|
4741
|
+
}
|
|
4742
|
+
get borderColor() {
|
|
4743
|
+
return this.appearance.borderColor;
|
|
4744
|
+
}
|
|
4745
|
+
get textColor() {
|
|
4746
|
+
return this.appearance.textColor;
|
|
4747
|
+
}
|
|
4748
|
+
get selectionColor() {
|
|
4749
|
+
return this.appearance.selectionColor;
|
|
4750
|
+
}
|
|
4751
|
+
onActivate(root, layer) {
|
|
4752
|
+
super.onActivate(root, layer);
|
|
4753
|
+
root?.provide(workspaceFocusInsetsKey, this.workspaceFocusInsets);
|
|
4754
|
+
}
|
|
4755
|
+
onDeactivate(root, layer) {
|
|
4756
|
+
if (root?.inject(workspaceFocusInsetsKey) === this.workspaceFocusInsets) root.provide(workspaceFocusInsetsKey, void 0);
|
|
4757
|
+
super.onDeactivate(root, layer);
|
|
4758
|
+
}
|
|
4759
|
+
setOptions(options) {
|
|
4760
|
+
if (!options) return this;
|
|
4761
|
+
const { rulerSize, backgroundColor, cornerBackgroundColor, majorTickColor, minorTickColor, borderColor, textColor, selectionColor, ...baseOptions } = options;
|
|
4762
|
+
let changed = false;
|
|
4763
|
+
if (Object.hasOwn(options, "rulerSize")) changed = this.setAppearance("rulerSize", rulerSize ?? DEFAULT_RULER_SIZE) || changed;
|
|
4764
|
+
if (Object.hasOwn(options, "backgroundColor")) changed = this.setAppearance("backgroundColor", backgroundColor ?? DEFAULT_BACKGROUND_COLOR) || changed;
|
|
4765
|
+
if (Object.hasOwn(options, "cornerBackgroundColor")) changed = this.setAppearance("cornerBackgroundColor", cornerBackgroundColor ?? DEFAULT_CORNER_BACKGROUND_COLOR) || changed;
|
|
4766
|
+
if (Object.hasOwn(options, "majorTickColor")) changed = this.setAppearance("majorTickColor", majorTickColor ?? DEFAULT_MAJOR_TICK_COLOR) || changed;
|
|
4767
|
+
if (Object.hasOwn(options, "minorTickColor")) changed = this.setAppearance("minorTickColor", minorTickColor ?? DEFAULT_MINOR_TICK_COLOR) || changed;
|
|
4768
|
+
if (Object.hasOwn(options, "borderColor")) changed = this.setAppearance("borderColor", borderColor ?? DEFAULT_BORDER_COLOR) || changed;
|
|
4769
|
+
if (Object.hasOwn(options, "textColor")) changed = this.setAppearance("textColor", textColor ?? DEFAULT_TEXT_COLOR) || changed;
|
|
4770
|
+
if (Object.hasOwn(options, "selectionColor")) changed = this.setAppearance("selectionColor", selectionColor) || changed;
|
|
4771
|
+
super.setOptions(baseOptions);
|
|
4772
|
+
if (changed) this.transform.markPaintDirty();
|
|
4773
|
+
return this;
|
|
4774
|
+
}
|
|
4775
|
+
setAppearance(key, value) {
|
|
4776
|
+
if (Object.is(this.appearance[key], value)) return false;
|
|
4777
|
+
this.appearance[key] = value;
|
|
4778
|
+
return true;
|
|
4779
|
+
}
|
|
4780
|
+
paint() {
|
|
4781
|
+
const ctx = this.layer.ctx;
|
|
4782
|
+
const w = this.root.width || 0;
|
|
4783
|
+
const h = this.root.height || 0;
|
|
4784
|
+
const viewport = this.root.viewport;
|
|
4785
|
+
const rulerSize = this.rulerSize;
|
|
4786
|
+
const pxPerTick = 50;
|
|
4787
|
+
const minorCount = 5;
|
|
4788
|
+
ctx.save();
|
|
4789
|
+
ctx.resetTransform();
|
|
4790
|
+
ctx.scale(viewport.dpr, viewport.dpr);
|
|
4791
|
+
ctx.clearRect(rulerSize, 0, w - rulerSize, rulerSize);
|
|
4792
|
+
ctx.fillStyle = this.backgroundColor;
|
|
4793
|
+
ctx.fillRect(rulerSize, 0, Math.max(0, w - rulerSize), rulerSize);
|
|
4794
|
+
const vpRect = viewport.getWorldRect();
|
|
4795
|
+
const stepX = niceStep(pxPerTick, viewport.scale);
|
|
4796
|
+
const startWorldX = vpRect.left;
|
|
4797
|
+
const endWorldX = vpRect.left + vpRect.width;
|
|
4798
|
+
const firstTickX = Math.floor(startWorldX / stepX) * stepX;
|
|
4799
|
+
const precisionX = Math.max(0, -Math.floor(Math.log10(stepX)));
|
|
4800
|
+
ctx.save();
|
|
4801
|
+
ctx.beginPath();
|
|
4802
|
+
ctx.rect(rulerSize, 0, w - rulerSize, rulerSize);
|
|
4803
|
+
ctx.clip();
|
|
4804
|
+
ctx.beginPath();
|
|
4805
|
+
ctx.strokeStyle = this.minorTickColor;
|
|
4806
|
+
ctx.lineWidth = 1;
|
|
4807
|
+
for (let x = firstTickX; x <= endWorldX; x += stepX) for (let i = 1; i < minorCount; i++) {
|
|
4808
|
+
const value = x + i * stepX / minorCount;
|
|
4809
|
+
const minorX = Math.floor(value * viewport.scale + viewport.x) + .5;
|
|
4810
|
+
ctx.moveTo(minorX, rulerSize);
|
|
4811
|
+
ctx.lineTo(minorX, rulerSize - 5);
|
|
4812
|
+
}
|
|
4813
|
+
ctx.stroke();
|
|
4814
|
+
ctx.beginPath();
|
|
4815
|
+
ctx.strokeStyle = this.majorTickColor;
|
|
4816
|
+
ctx.font = "10px sans-serif";
|
|
4817
|
+
for (let x = firstTickX; x <= endWorldX; x += stepX) {
|
|
4818
|
+
const px = Math.floor(x * viewport.scale + viewport.x) + .5;
|
|
4819
|
+
ctx.moveTo(px, 0);
|
|
4820
|
+
ctx.lineTo(px, rulerSize);
|
|
4821
|
+
ctx.save();
|
|
4822
|
+
ctx.fillStyle = this.textColor;
|
|
4823
|
+
ctx.textAlign = "left";
|
|
4824
|
+
ctx.textBaseline = "top";
|
|
4825
|
+
ctx.fillText(formatRulerValue(x, precisionX), px + 2, TICK_LABEL_OFFSET);
|
|
4826
|
+
ctx.restore();
|
|
4827
|
+
}
|
|
4828
|
+
ctx.stroke();
|
|
4829
|
+
ctx.restore();
|
|
4830
|
+
ctx.clearRect(0, rulerSize, rulerSize, Math.max(0, h - rulerSize));
|
|
4831
|
+
ctx.fillStyle = this.backgroundColor;
|
|
4832
|
+
ctx.fillRect(0, rulerSize, rulerSize, Math.max(0, h - rulerSize));
|
|
4833
|
+
const stepY = niceStep(pxPerTick, viewport.scale);
|
|
4834
|
+
const startWorldY = vpRect.top;
|
|
4835
|
+
const endWorldY = vpRect.top + vpRect.height;
|
|
4836
|
+
const firstTickY = Math.floor(startWorldY / stepY) * stepY;
|
|
4837
|
+
const precisionY = Math.max(0, -Math.floor(Math.log10(stepY)));
|
|
4838
|
+
ctx.save();
|
|
4839
|
+
ctx.beginPath();
|
|
4840
|
+
ctx.rect(0, rulerSize, rulerSize, h - rulerSize);
|
|
4841
|
+
ctx.clip();
|
|
4842
|
+
ctx.beginPath();
|
|
4843
|
+
ctx.strokeStyle = this.minorTickColor;
|
|
4844
|
+
for (let y = firstTickY; y <= endWorldY; y += stepY) for (let i = 1; i < minorCount; i++) {
|
|
4845
|
+
const value = y + i * stepY / minorCount;
|
|
4846
|
+
const minorY = Math.floor(value * viewport.scale + viewport.y) + .5;
|
|
4847
|
+
ctx.moveTo(rulerSize, minorY);
|
|
4848
|
+
ctx.lineTo(rulerSize - 5, minorY);
|
|
4849
|
+
}
|
|
4850
|
+
ctx.stroke();
|
|
4851
|
+
ctx.beginPath();
|
|
4852
|
+
ctx.strokeStyle = this.majorTickColor;
|
|
4853
|
+
for (let y = firstTickY; y <= endWorldY; y += stepY) {
|
|
4854
|
+
const py = Math.floor(y * viewport.scale + viewport.y) + .5;
|
|
4855
|
+
ctx.moveTo(0, py);
|
|
4856
|
+
ctx.lineTo(rulerSize, py);
|
|
4857
|
+
ctx.save();
|
|
4858
|
+
ctx.translate(TICK_LABEL_OFFSET, py + 2);
|
|
4859
|
+
ctx.rotate(-Math.PI / 2);
|
|
4860
|
+
ctx.fillStyle = this.textColor;
|
|
4861
|
+
ctx.font = "10px sans-serif";
|
|
4862
|
+
ctx.textAlign = "right";
|
|
4863
|
+
ctx.textBaseline = "top";
|
|
4864
|
+
ctx.fillText(formatRulerValue(y, precisionY), 0, 0);
|
|
4865
|
+
ctx.restore();
|
|
4866
|
+
}
|
|
4867
|
+
ctx.stroke();
|
|
4868
|
+
ctx.restore();
|
|
4869
|
+
ctx.fillStyle = this.cornerBackgroundColor;
|
|
4870
|
+
ctx.fillRect(0, 0, rulerSize, rulerSize);
|
|
4871
|
+
ctx.fillStyle = this.textColor;
|
|
4872
|
+
ctx.textAlign = "center";
|
|
4873
|
+
ctx.textBaseline = "middle";
|
|
4874
|
+
ctx.fillText("px", rulerSize / 2, rulerSize / 2);
|
|
4875
|
+
ctx.beginPath();
|
|
4876
|
+
ctx.strokeStyle = this.borderColor;
|
|
4877
|
+
ctx.lineWidth = 1;
|
|
4878
|
+
ctx.moveTo(0, .5);
|
|
4879
|
+
ctx.lineTo(w, .5);
|
|
4880
|
+
ctx.moveTo(.5, 0);
|
|
4881
|
+
ctx.lineTo(.5, h);
|
|
4882
|
+
ctx.moveTo(0, rulerSize - .5);
|
|
4883
|
+
ctx.lineTo(w, rulerSize - .5);
|
|
4884
|
+
ctx.moveTo(rulerSize - .5, 0);
|
|
4885
|
+
ctx.lineTo(rulerSize - .5, h);
|
|
4886
|
+
ctx.stroke();
|
|
4887
|
+
const frame = this.select?.interactionController.projection.frame;
|
|
4888
|
+
if (frame) {
|
|
4889
|
+
const outline = frame.outline;
|
|
4890
|
+
let left = outline[0].x;
|
|
4891
|
+
let top = outline[0].y;
|
|
4892
|
+
let right = left;
|
|
4893
|
+
let bottom = top;
|
|
4894
|
+
for (let index = 1; index < outline.length; index++) {
|
|
4895
|
+
const point = outline[index];
|
|
4896
|
+
if (point.x < left) left = point.x;
|
|
4897
|
+
if (point.y < top) top = point.y;
|
|
4898
|
+
if (point.x > right) right = point.x;
|
|
4899
|
+
if (point.y > bottom) bottom = point.y;
|
|
4900
|
+
}
|
|
4901
|
+
const leftPx = left * viewport.scale + viewport.x;
|
|
4902
|
+
const topPx = top * viewport.scale + viewport.y;
|
|
4903
|
+
const rightPx = right * viewport.scale + viewport.x;
|
|
4904
|
+
const bottomPx = bottom * viewport.scale + viewport.y;
|
|
4905
|
+
const selectionColor = this.selectionColor ?? this.root.skin.selection;
|
|
4906
|
+
ctx.save();
|
|
4907
|
+
ctx.beginPath();
|
|
4908
|
+
ctx.rect(rulerSize, 0, w - rulerSize, rulerSize);
|
|
4909
|
+
ctx.clip();
|
|
4910
|
+
ctx.fillStyle = selectionColor;
|
|
4911
|
+
ctx.fillRect(leftPx, 0, rightPx - leftPx, SELECTION_RANGE_SIZE);
|
|
4912
|
+
ctx.font = "10px sans-serif";
|
|
4913
|
+
ctx.textAlign = "center";
|
|
4914
|
+
ctx.textBaseline = "top";
|
|
4915
|
+
ctx.fillText(formatRulerValue(left, precisionX), leftPx, SELECTION_LABEL_OFFSET);
|
|
4916
|
+
ctx.fillText(formatRulerValue(right, precisionX), rightPx, SELECTION_LABEL_OFFSET);
|
|
4917
|
+
ctx.restore();
|
|
4918
|
+
ctx.save();
|
|
4919
|
+
ctx.beginPath();
|
|
4920
|
+
ctx.rect(0, rulerSize, rulerSize, h - rulerSize);
|
|
4921
|
+
ctx.clip();
|
|
4922
|
+
ctx.fillStyle = selectionColor;
|
|
4923
|
+
ctx.fillRect(0, topPx, SELECTION_RANGE_SIZE, bottomPx - topPx);
|
|
4924
|
+
ctx.font = "10px sans-serif";
|
|
4925
|
+
ctx.textAlign = "center";
|
|
4926
|
+
ctx.textBaseline = "top";
|
|
4927
|
+
ctx.save();
|
|
4928
|
+
ctx.translate(SELECTION_LABEL_OFFSET, topPx);
|
|
4929
|
+
ctx.rotate(-Math.PI / 2);
|
|
4930
|
+
ctx.fillText(formatRulerValue(top, precisionY), 0, 0);
|
|
4931
|
+
ctx.restore();
|
|
4932
|
+
ctx.save();
|
|
4933
|
+
ctx.translate(SELECTION_LABEL_OFFSET, bottomPx);
|
|
4934
|
+
ctx.rotate(-Math.PI / 2);
|
|
4935
|
+
ctx.fillText(formatRulerValue(bottom, precisionY), 0, 0);
|
|
4936
|
+
ctx.restore();
|
|
4937
|
+
ctx.restore();
|
|
4938
|
+
}
|
|
4939
|
+
ctx.restore();
|
|
4940
|
+
}
|
|
4941
|
+
};
|
|
4942
|
+
//#endregion
|
|
4943
|
+
//#region packages/tools/src/editor-layer.ts
|
|
4944
|
+
/**
|
|
4945
|
+
* Rendering and input boundary for editor tools.
|
|
4946
|
+
*
|
|
4947
|
+
* Tool painters draw outside their node geometry (selection handles,
|
|
4948
|
+
* guides, rulers and previews), so dirty-rectangle repainting cannot
|
|
4949
|
+
* describe their damage. Keep this layer on the full-frame path and avoid
|
|
4950
|
+
* CSS viewport previews that would scale screen-space affordances. While the
|
|
4951
|
+
* layer is active, its subtree becomes an interaction scope: content hits
|
|
4952
|
+
* remain visible to Root observers without reaching content handlers.
|
|
4953
|
+
*/
|
|
4954
|
+
var EditorLayer = class extends Layer {
|
|
4955
|
+
type = "editor-layer";
|
|
4956
|
+
releaseInteractionScope;
|
|
4957
|
+
constructor(options = {}) {
|
|
4958
|
+
super({
|
|
4959
|
+
...options,
|
|
4960
|
+
elementArrangementEnabled: false,
|
|
4961
|
+
enableDirtyRect: false,
|
|
4962
|
+
cssTransformable: false
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
onActivate(root) {
|
|
4966
|
+
super.onActivate(root);
|
|
4967
|
+
if (root) this.releaseInteractionScope = root.input.registerInteractionScope(this);
|
|
4968
|
+
}
|
|
4969
|
+
onDeactivate(root) {
|
|
4970
|
+
this.releaseInteractionScope?.();
|
|
4971
|
+
this.releaseInteractionScope = void 0;
|
|
4972
|
+
super.onDeactivate(root);
|
|
4973
|
+
}
|
|
4974
|
+
};
|
|
4975
|
+
//#endregion
|
|
4976
|
+
export { ArrangeType, EditorLayer, HistoryManager, LineTool, Rule, Select, Snap, alignElements, checkElement, deserializeElement, exportToFile, importFromFile, parseFileData, restoreScene, serializeScene, serializeSceneToJSON };
|