@mx-sose-front/mx-sose-graph 1.1.8 → 1.1.9
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/dist/assets/edgeWorker-b57ca007.js +2 -0
- package/dist/assets/edgeWorker-b57ca007.js.map +1 -0
- package/dist/index.d.ts +633 -30
- package/dist/index.esm.js +8728 -4734
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Common/Tree.vue +451 -0
- package/src/components/Common/index.ts +2 -0
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -2
- package/src/components/Edge/Edge.vue +172 -169
- package/src/components/Gantt/Gantt.vue +1544 -0
- package/src/components/GanttContextMenu/GanttContextMenu.vue +304 -0
- package/src/components/InteractionLayer.vue +343 -147
- package/src/components/Matrix/Matrix.vue +828 -0
- package/src/components/Matrix/index.ts +168 -0
- package/src/components/Shape/ConceptualRole.vue +2 -34
- package/src/components/Table/Table.vue +970 -0
- package/src/constants/edgeShapeKeys.ts +8 -5
- package/src/constants/index.ts +259 -45
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useChartRowSelection.ts +456 -0
- package/src/hooks/useResize.ts +2 -2
- package/src/hooks/useVirtualScroll.ts +258 -0
- package/src/index.ts +1 -1
- package/src/render/shape-renderer.ts +62 -2
- package/src/statics/icons/childIcons//345/221/275/344/273/244@3x.png +0 -0
- package/src/statics/icons/childIcons//346/210/230/347/225/245/346/246/202/345/277/265/350/241/250@3x.png +0 -0
- package/src/statics/icons/childIcons//346/216/247/345/210/266@3x.png +0 -0
- package/src/statics/icons/createMenu/down.png +0 -0
- package/src/statics/icons/createMenu/remove.png +0 -0
- package/src/statics/icons/createMenu/up.png +0 -0
- package/src/store/graphStore.ts +217 -44
- package/src/types/index.ts +86 -4
- package/src/utils/batchAutoExpand.ts +9 -10
- package/src/utils/containers.ts +72 -17
- package/src/utils/contextMenuUtils.ts +7 -7
- package/src/utils/dateUtils.ts +160 -0
- package/src/utils/diagram.ts +10 -8
- package/src/utils/drag.ts +6 -5
- package/src/utils/edgeUtils.ts +344 -427
- package/src/utils/edgeWorker.ts +471 -0
- package/src/utils/hittest.ts +37 -38
- package/src/utils/index.ts +3 -0
- package/src/utils/keyboardUtils.ts +5 -5
- package/src/utils/packageOutline.ts +96 -0
- package/src/utils/rafThrottle.ts +162 -0
- package/src/utils/workerManager.ts +335 -0
- package/src/view/graph.vue +47 -33
- /package/src/statics/icons/childIcons//346/210/230/347/225/{245@3x.png" → 245/345/261/202@3x.png"} +0 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
// edgeWorker.ts - Web Worker for handling complex edge calculations
|
|
2
|
+
|
|
3
|
+
interface Shape {
|
|
4
|
+
id: string;
|
|
5
|
+
shapeType: string;
|
|
6
|
+
bounds?: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
sourceId?: string;
|
|
13
|
+
targetId?: string;
|
|
14
|
+
modelId?: string;
|
|
15
|
+
shapeKey?: string;
|
|
16
|
+
direction?: string;
|
|
17
|
+
parenShapeId?: string;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WorkerMessage {
|
|
22
|
+
type: string;
|
|
23
|
+
data: any;
|
|
24
|
+
id?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface WorkerResponse {
|
|
28
|
+
id: number;
|
|
29
|
+
result: any;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class EdgeWorkerUtils {
|
|
34
|
+
/**
|
|
35
|
+
* 获取图形的四个中心点(上下左右)
|
|
36
|
+
*/
|
|
37
|
+
static getShapeCenterPoints(bounds: { x: number; y: number; width: number; height: number }) {
|
|
38
|
+
const { x, y, width, height } = bounds;
|
|
39
|
+
return {
|
|
40
|
+
top: { x: x + width / 2, y: y },
|
|
41
|
+
bottom: { x: x + width / 2, y: y + height },
|
|
42
|
+
left: { x: x, y: y + height / 2 },
|
|
43
|
+
right: { x: x + width, y: y + height / 2 },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static getDistance(p1: { x: number; y: number }, p2: { x: number; y: number }) {
|
|
48
|
+
const dx = p1.x - p2.x;
|
|
49
|
+
const dy = p1.y - p2.y;
|
|
50
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 生成图元自连接时使用的半环路径。
|
|
55
|
+
* Worker 和主线程保持同一套正交折线规则,避免异步回填后形态不一致。
|
|
56
|
+
*/
|
|
57
|
+
static buildSelfLoopWaypoints(
|
|
58
|
+
bounds: { x: number; y: number; width: number; height: number },
|
|
59
|
+
groupIndex: number = 0
|
|
60
|
+
): {
|
|
61
|
+
sourcePoint: { x: number; y: number };
|
|
62
|
+
targetPoint: { x: number; y: number };
|
|
63
|
+
waypoints: Array<{ x: number; y: number }>;
|
|
64
|
+
} {
|
|
65
|
+
const { x, y, width, height } = bounds;
|
|
66
|
+
const centerX = x + width / 2;
|
|
67
|
+
const sourcePoint = { x: centerX, y };
|
|
68
|
+
const targetPoint = { x: centerX, y: y + height };
|
|
69
|
+
|
|
70
|
+
// 保持上下抬升高度不变,只缩短右侧外扩距离,让右边那段线更贴近图元。
|
|
71
|
+
const outwardOffset = Math.max(width * 0.24, 20) + groupIndex * 12;
|
|
72
|
+
const verticalPadding = Math.max(height * 0.45, 26) + groupIndex * 10;
|
|
73
|
+
const upperPoint = {
|
|
74
|
+
x: centerX,
|
|
75
|
+
y: y - verticalPadding,
|
|
76
|
+
};
|
|
77
|
+
const upperRightPoint = {
|
|
78
|
+
x: x + width + outwardOffset,
|
|
79
|
+
y: y - verticalPadding,
|
|
80
|
+
};
|
|
81
|
+
const lowerRightPoint = {
|
|
82
|
+
x: x + width + outwardOffset,
|
|
83
|
+
y: y + height + verticalPadding,
|
|
84
|
+
};
|
|
85
|
+
const lowerPoint = {
|
|
86
|
+
x: centerX,
|
|
87
|
+
y: y + height + verticalPadding,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
sourcePoint,
|
|
92
|
+
targetPoint,
|
|
93
|
+
waypoints: [sourcePoint, upperPoint, upperRightPoint, lowerRightPoint, lowerPoint, targetPoint],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 统一生成边的折点数据。
|
|
99
|
+
* 普通连线走最近边中点,自连接走专用半环路径。
|
|
100
|
+
*/
|
|
101
|
+
static buildEdgeWaypoints(
|
|
102
|
+
sBounds: { x: number; y: number; width: number; height: number },
|
|
103
|
+
tBounds: { x: number; y: number; width: number; height: number },
|
|
104
|
+
groupIndex: number,
|
|
105
|
+
isSelfLoop: boolean
|
|
106
|
+
): {
|
|
107
|
+
sourcePoint: { x: number; y: number };
|
|
108
|
+
targetPoint: { x: number; y: number };
|
|
109
|
+
waypoints: Array<{ x: number; y: number }>;
|
|
110
|
+
} {
|
|
111
|
+
if (isSelfLoop) {
|
|
112
|
+
return this.buildSelfLoopWaypoints(sBounds, groupIndex);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { sourcePoint, targetPoint } = this.calcBestEndpoints(
|
|
116
|
+
sBounds,
|
|
117
|
+
tBounds,
|
|
118
|
+
groupIndex
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
sourcePoint,
|
|
123
|
+
targetPoint,
|
|
124
|
+
waypoints: [sourcePoint, targetPoint],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 计算两个图元之间的最佳连接端点(共用算法,与主线程 EdgeUtils.calcBestEndpoints 一致)
|
|
130
|
+
*/
|
|
131
|
+
static calcBestEndpoints(
|
|
132
|
+
sBounds: { x: number; y: number; width: number; height: number },
|
|
133
|
+
tBounds: { x: number; y: number; width: number; height: number },
|
|
134
|
+
groupIndex: number
|
|
135
|
+
): { sourcePoint: { x: number; y: number }; targetPoint: { x: number; y: number } } {
|
|
136
|
+
const sourcePoints = this.getShapeCenterPoints(sBounds);
|
|
137
|
+
const targetPoints = this.getShapeCenterPoints(tBounds);
|
|
138
|
+
|
|
139
|
+
let minDist = Infinity;
|
|
140
|
+
let bestSrc = sourcePoints.top;
|
|
141
|
+
let bestTgt = targetPoints.top;
|
|
142
|
+
for (const sp of Object.values(sourcePoints)) {
|
|
143
|
+
for (const tp of Object.values(targetPoints)) {
|
|
144
|
+
const d = this.getDistance(sp, tp);
|
|
145
|
+
if (d < minDist) {
|
|
146
|
+
minDist = d;
|
|
147
|
+
bestSrc = sp;
|
|
148
|
+
bestTgt = tp;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (groupIndex > 0) {
|
|
154
|
+
const dx = (tBounds.x + tBounds.width / 2) - (sBounds.x + sBounds.width / 2);
|
|
155
|
+
const dy = (tBounds.y + tBounds.height / 2) - (sBounds.y + sBounds.height / 2);
|
|
156
|
+
const offsetDistance = 5 * (groupIndex + 1);
|
|
157
|
+
let offsetX = 0, offsetY = 0;
|
|
158
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
159
|
+
offsetY = offsetDistance;
|
|
160
|
+
} else {
|
|
161
|
+
offsetX = offsetDistance;
|
|
162
|
+
}
|
|
163
|
+
if (groupIndex % 2 === 1) {
|
|
164
|
+
offsetX = -offsetX;
|
|
165
|
+
offsetY = -offsetY;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
sourcePoint: { x: bestSrc.x + offsetX, y: bestSrc.y + offsetY },
|
|
169
|
+
targetPoint: { x: bestTgt.x + offsetX, y: bestTgt.y + offsetY },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { sourcePoint: bestSrc, targetPoint: bestTgt };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 更新与指定图元相关的连线端点
|
|
178
|
+
*/
|
|
179
|
+
static updateRelatedEdges(
|
|
180
|
+
shapes: Shape[],
|
|
181
|
+
changedIds: string[],
|
|
182
|
+
edgeIndex?: Map<string, Shape[]>,
|
|
183
|
+
shapeMapIndex?: Map<string, Shape>
|
|
184
|
+
): Shape[] {
|
|
185
|
+
let relatedEdges: Shape[];
|
|
186
|
+
if (edgeIndex) {
|
|
187
|
+
const seen = new Set<string>();
|
|
188
|
+
relatedEdges = [];
|
|
189
|
+
for (const id of changedIds) {
|
|
190
|
+
const edges = edgeIndex.get(id);
|
|
191
|
+
if (edges) {
|
|
192
|
+
for (const e of edges) {
|
|
193
|
+
if (!seen.has(e.id)) {
|
|
194
|
+
seen.add(e.id);
|
|
195
|
+
relatedEdges.push(e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
relatedEdges = shapes.filter((shape) => shape.shapeType === "edge");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const findShape = shapeMapIndex
|
|
205
|
+
? (id: string) => shapeMapIndex.get(id) ?? null
|
|
206
|
+
: (id: string) => shapes.find(s => s.id === id) ?? null;
|
|
207
|
+
|
|
208
|
+
const edgeGroups = new Map<string, Shape[]>();
|
|
209
|
+
relatedEdges.forEach((edge) => {
|
|
210
|
+
if (edge.sourceId && edge.targetId) {
|
|
211
|
+
const isForward = edge.sourceId.localeCompare(edge.targetId) <= 0;
|
|
212
|
+
const groupKey = isForward
|
|
213
|
+
? `${edge.sourceId}-${edge.targetId}`
|
|
214
|
+
: `${edge.targetId}-${edge.sourceId}`;
|
|
215
|
+
if (!edgeGroups.has(groupKey)) edgeGroups.set(groupKey, []);
|
|
216
|
+
edgeGroups.get(groupKey)!.push(edge);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const changedSet = new Set(changedIds);
|
|
221
|
+
const updatedEdges: Shape[] = [];
|
|
222
|
+
|
|
223
|
+
edgeGroups.forEach((groupEdges) => {
|
|
224
|
+
groupEdges.forEach((edge, index) => {
|
|
225
|
+
const sourceChanged = edge.sourceId && changedSet.has(edge.sourceId);
|
|
226
|
+
const targetChanged = edge.targetId && changedSet.has(edge.targetId);
|
|
227
|
+
|
|
228
|
+
if (sourceChanged || targetChanged) {
|
|
229
|
+
const sourceShape = edge.sourceId ? findShape(edge.sourceId) : null;
|
|
230
|
+
const targetShape = edge.targetId ? findShape(edge.targetId) : null;
|
|
231
|
+
|
|
232
|
+
if (sourceShape && targetShape && sourceShape.bounds && targetShape.bounds) {
|
|
233
|
+
const groupIndex = groupEdges.length > 1 ? index : 0;
|
|
234
|
+
const { waypoints } = this.buildEdgeWaypoints(
|
|
235
|
+
sourceShape.bounds,
|
|
236
|
+
targetShape.bounds,
|
|
237
|
+
groupIndex,
|
|
238
|
+
sourceShape.id === targetShape.id
|
|
239
|
+
);
|
|
240
|
+
updatedEdges.push({
|
|
241
|
+
...edge,
|
|
242
|
+
waypointId: JSON.stringify(waypoints),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return updatedEdges;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 初始化所有连线的端点
|
|
254
|
+
*/
|
|
255
|
+
static initializeAllEdgeEndpoints(
|
|
256
|
+
shapes: Shape[],
|
|
257
|
+
shapeMapIndex?: Map<string, Shape>
|
|
258
|
+
): Shape[] {
|
|
259
|
+
const edges = shapes.filter((shape) => shape.shapeType === "edge");
|
|
260
|
+
|
|
261
|
+
const findShape = shapeMapIndex
|
|
262
|
+
? (id: string) => shapeMapIndex.get(id) ?? null
|
|
263
|
+
: (id: string) => shapes.find(s => s.id === id) ?? null;
|
|
264
|
+
|
|
265
|
+
const edgeGroups = new Map<string, Shape[]>();
|
|
266
|
+
edges.forEach((edge) => {
|
|
267
|
+
if (edge.sourceId && edge.targetId) {
|
|
268
|
+
const isForward = edge.sourceId.localeCompare(edge.targetId) <= 0;
|
|
269
|
+
const groupKey = isForward
|
|
270
|
+
? `${edge.sourceId}-${edge.targetId}`
|
|
271
|
+
: `${edge.targetId}-${edge.sourceId}`;
|
|
272
|
+
if (!edgeGroups.has(groupKey)) edgeGroups.set(groupKey, []);
|
|
273
|
+
edgeGroups.get(groupKey)!.push(edge);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const updatedEdges: Shape[] = [];
|
|
278
|
+
|
|
279
|
+
edgeGroups.forEach((groupEdges) => {
|
|
280
|
+
groupEdges.forEach((edge, index) => {
|
|
281
|
+
const sourceShape = edge.sourceId ? findShape(edge.sourceId) : null;
|
|
282
|
+
const targetShape = edge.targetId ? findShape(edge.targetId) : null;
|
|
283
|
+
|
|
284
|
+
if (sourceShape && targetShape && sourceShape.bounds && targetShape.bounds) {
|
|
285
|
+
const groupIndex = groupEdges.length > 1 ? index : 0;
|
|
286
|
+
const { waypoints } = this.buildEdgeWaypoints(
|
|
287
|
+
sourceShape.bounds,
|
|
288
|
+
targetShape.bounds,
|
|
289
|
+
groupIndex,
|
|
290
|
+
sourceShape.id === targetShape.id
|
|
291
|
+
);
|
|
292
|
+
updatedEdges.push({
|
|
293
|
+
...edge,
|
|
294
|
+
waypointId: JSON.stringify(waypoints),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return updatedEdges;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 完成连接操作,计算最终连接点
|
|
305
|
+
*/
|
|
306
|
+
static completeConnection(
|
|
307
|
+
sourceShape: Shape | undefined,
|
|
308
|
+
targetShape: Shape,
|
|
309
|
+
clickPoint: { x: number; y: number },
|
|
310
|
+
currentConnectPoint: { x: number; y: number },
|
|
311
|
+
shapes: Shape[] = []
|
|
312
|
+
) {
|
|
313
|
+
if (!targetShape.bounds || !sourceShape?.bounds) return null;
|
|
314
|
+
|
|
315
|
+
const existingEdges = shapes.filter(
|
|
316
|
+
shape => shape.shapeType === 'edge' &&
|
|
317
|
+
((shape.sourceId === sourceShape?.id && shape.targetId === targetShape.id) ||
|
|
318
|
+
(shape.sourceId === targetShape.id && shape.targetId === sourceShape?.id))
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const groupIndex = existingEdges.length;
|
|
322
|
+
const { sourcePoint, targetPoint, waypoints } = this.buildEdgeWaypoints(
|
|
323
|
+
sourceShape.bounds,
|
|
324
|
+
targetShape.bounds,
|
|
325
|
+
groupIndex,
|
|
326
|
+
sourceShape.id === targetShape.id
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
sourceId: sourceShape?.id,
|
|
331
|
+
sourceModelId: sourceShape?.modelId,
|
|
332
|
+
targetId: targetShape.id,
|
|
333
|
+
targetModelId: targetShape.modelId,
|
|
334
|
+
sourcePoint,
|
|
335
|
+
targetPoint,
|
|
336
|
+
waypoints,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 找到距离鼠标位置最近的连接点
|
|
342
|
+
*/
|
|
343
|
+
static findNearestConnectPoint(
|
|
344
|
+
mousePos: { x: number; y: number },
|
|
345
|
+
shape: Shape | undefined
|
|
346
|
+
) {
|
|
347
|
+
if (!shape?.bounds) return null;
|
|
348
|
+
|
|
349
|
+
const mouseX = mousePos.x;
|
|
350
|
+
const mouseY = mousePos.y;
|
|
351
|
+
const { x = 0, y = 0, width = 0, height = 0 } = shape.bounds;
|
|
352
|
+
|
|
353
|
+
const left = x;
|
|
354
|
+
const right = x + width;
|
|
355
|
+
const top = y;
|
|
356
|
+
const bottom = y + height;
|
|
357
|
+
|
|
358
|
+
const relativeX = mouseX - x;
|
|
359
|
+
const relativeY = mouseY - y;
|
|
360
|
+
|
|
361
|
+
const isInTopArea = relativeY < 0;
|
|
362
|
+
const isInBottomArea = relativeY > height;
|
|
363
|
+
const isInLeftArea = relativeX < 0;
|
|
364
|
+
const isInRightArea = relativeX > width;
|
|
365
|
+
|
|
366
|
+
const connectPoints = this.getShapeCenterPoints({
|
|
367
|
+
x: shape.bounds.x ?? 0,
|
|
368
|
+
y: shape.bounds.y ?? 0,
|
|
369
|
+
width: shape.bounds.width ?? 0,
|
|
370
|
+
height: shape.bounds.height ?? 0,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const mousePoint = { x: mouseX, y: mouseY };
|
|
374
|
+
|
|
375
|
+
if (isInTopArea || isInBottomArea || isInLeftArea || isInRightArea) {
|
|
376
|
+
const distances = {
|
|
377
|
+
top: this.getDistance(mousePoint, connectPoints.top),
|
|
378
|
+
bottom: this.getDistance(mousePoint, connectPoints.bottom),
|
|
379
|
+
left: this.getDistance(mousePoint, connectPoints.left),
|
|
380
|
+
right: this.getDistance(mousePoint, connectPoints.right),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const nearestPoint = Object.entries(distances).reduce(
|
|
384
|
+
(min, [key, distance]) =>
|
|
385
|
+
distance < min.distance ? { key, distance } : min,
|
|
386
|
+
{ key: "top", distance: Infinity }
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return connectPoints[nearestPoint.key as keyof typeof connectPoints];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 鼠标在图形内部,使用最近的边
|
|
393
|
+
const distances = {
|
|
394
|
+
top: Math.abs(mouseY - top),
|
|
395
|
+
bottom: Math.abs(mouseY - bottom),
|
|
396
|
+
left: Math.abs(mouseX - left),
|
|
397
|
+
right: Math.abs(mouseX - right),
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const nearestEdge = Object.entries(distances).reduce(
|
|
401
|
+
(min, [key, distance]) =>
|
|
402
|
+
distance < min.distance ? { key, distance } : min,
|
|
403
|
+
{ key: "top", distance: Infinity }
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
switch (nearestEdge.key) {
|
|
407
|
+
case "top":
|
|
408
|
+
return { x: mouseX, y: top };
|
|
409
|
+
case "bottom":
|
|
410
|
+
return { x: mouseX, y: bottom };
|
|
411
|
+
case "left":
|
|
412
|
+
return { x: left, y: mouseY };
|
|
413
|
+
case "right":
|
|
414
|
+
return { x: right, y: mouseY };
|
|
415
|
+
default:
|
|
416
|
+
return connectPoints.top;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 监听主线程消息
|
|
422
|
+
self.addEventListener('message', (event: MessageEvent<WorkerMessage>) => {
|
|
423
|
+
const { type, data, id } = event.data;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
let result;
|
|
427
|
+
|
|
428
|
+
switch (type) {
|
|
429
|
+
case 'updateRelatedEdges': {
|
|
430
|
+
const { shapes, changedIds, edgeIndex: edgeIndexData, shapeMapIndex: shapeMapData } = data;
|
|
431
|
+
const edgeIndex = edgeIndexData ? new Map(Object.entries(edgeIndexData)) : undefined;
|
|
432
|
+
const shapeMapIndex = shapeMapData ? new Map(Object.entries(shapeMapData)) : undefined;
|
|
433
|
+
result = EdgeWorkerUtils.updateRelatedEdges(
|
|
434
|
+
shapes, changedIds,
|
|
435
|
+
edgeIndex as Map<string, Shape[]> | undefined,
|
|
436
|
+
shapeMapIndex as Map<string, Shape> | undefined
|
|
437
|
+
);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case 'initializeAllEdgeEndpoints': {
|
|
441
|
+
const { shapes: initShapes, shapeMapIndex: initShapeMapData } = data;
|
|
442
|
+
const initShapeMapIndex = initShapeMapData ? new Map(Object.entries(initShapeMapData)) : undefined;
|
|
443
|
+
result = EdgeWorkerUtils.initializeAllEdgeEndpoints(
|
|
444
|
+
initShapes,
|
|
445
|
+
initShapeMapIndex as Map<string, Shape> | undefined
|
|
446
|
+
);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case 'completeConnection':
|
|
450
|
+
result = EdgeWorkerUtils.completeConnection(
|
|
451
|
+
data.sourceShape, data.targetShape,
|
|
452
|
+
data.clickPoint, data.currentConnectPoint, data.shapes
|
|
453
|
+
);
|
|
454
|
+
break;
|
|
455
|
+
case 'findNearestConnectPoint':
|
|
456
|
+
result = EdgeWorkerUtils.findNearestConnectPoint(data.mousePos, data.shape);
|
|
457
|
+
break;
|
|
458
|
+
default:
|
|
459
|
+
throw new Error(`Unknown message type: ${type}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
self.postMessage({ id, result } as WorkerResponse);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
self.postMessage({
|
|
465
|
+
id,
|
|
466
|
+
error: error instanceof Error ? error.message : String(error)
|
|
467
|
+
} as WorkerResponse);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Worker 文件不需要 export,由 self.addEventListener 驱动
|
package/src/utils/hittest.ts
CHANGED
|
@@ -88,48 +88,47 @@ function isPointNearEdge(pt: { x: number; y: number }, edge: Shape, threshold: n
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function pickTarget(shapes: readonly Shape[], pt: { x: number; y: number }): HitResult {
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const dz = getZ(b.s) - getZ(a.s)
|
|
97
|
-
return dz !== 0 ? dz : (b.i - a.i)
|
|
98
|
-
})
|
|
91
|
+
// 单次遍历:对每种类型只保留"最上层命中"的那个(zIndex 最大、索引最大)
|
|
92
|
+
let bestPin: Shape | null = null, bestPinZ = -Infinity, bestPinI = -1
|
|
93
|
+
let bestShape: Shape | null = null, bestShapeZ = -Infinity, bestShapeI = -1
|
|
94
|
+
let bestEdge: Shape | null = null, bestEdgeZ = -Infinity, bestEdgeI = -1
|
|
95
|
+
let diagram: Shape | null = null
|
|
99
96
|
|
|
100
|
-
for (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// 然后检查普通图形(shape类型)
|
|
105
|
-
const shapesOrdered = shapes
|
|
106
|
-
.map((s, i) => ({ s, i }))
|
|
107
|
-
.filter(x => isShape(x.s))
|
|
108
|
-
.sort((a, b) => {
|
|
109
|
-
const dz = getZ(b.s) - getZ(a.s)
|
|
110
|
-
return dz !== 0 ? dz : (b.i - a.i)
|
|
111
|
-
})
|
|
97
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
98
|
+
const s = shapes[i]
|
|
99
|
+
const type = String(s?.shapeType).trim().toLowerCase()
|
|
112
100
|
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
if (type === 'pin') {
|
|
102
|
+
if (inBounds(pt, getBounds(s))) {
|
|
103
|
+
const z = getZ(s)
|
|
104
|
+
if (z > bestPinZ || (z === bestPinZ && i > bestPinI)) {
|
|
105
|
+
bestPin = s; bestPinZ = z; bestPinI = i
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else if (type === 'shape') {
|
|
109
|
+
if (inBounds(pt, getBounds(s))) {
|
|
110
|
+
const z = getZ(s)
|
|
111
|
+
if (z > bestShapeZ || (z === bestShapeZ && i > bestShapeI)) {
|
|
112
|
+
bestShape = s; bestShapeZ = z; bestShapeI = i
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} else if (type === 'edge') {
|
|
116
|
+
if (isPointNearEdge(pt, s)) {
|
|
117
|
+
const z = getZ(s)
|
|
118
|
+
if (z > bestEdgeZ || (z === bestEdgeZ && i > bestEdgeI)) {
|
|
119
|
+
bestEdge = s; bestEdgeZ = z; bestEdgeI = i
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (type === 'diagram') {
|
|
123
|
+
diagram = s
|
|
124
|
+
}
|
|
115
125
|
}
|
|
116
|
-
|
|
117
|
-
// 然后检查线条(edge类型)
|
|
118
|
-
const edgesOrdered = shapes
|
|
119
|
-
.map((s, i) => ({ s, i }))
|
|
120
|
-
.filter(x => isEdge(x.s))
|
|
121
|
-
.sort((a, b) => {
|
|
122
|
-
const dz = getZ(b.s) - getZ(a.s)
|
|
123
|
-
return dz !== 0 ? dz : (b.i - a.i)
|
|
124
|
-
})
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 最后检查画布(diagram类型)
|
|
131
|
-
const diagram = shapes.find(isDiagram)
|
|
127
|
+
// 优先级:pin > shape > edge > diagram
|
|
128
|
+
if (bestPin) return { kind: 'pin', shape: bestPin }
|
|
129
|
+
if (bestShape) return { kind: 'shape', shape: bestShape }
|
|
130
|
+
if (bestEdge) return { kind: 'edge', shape: bestEdge }
|
|
132
131
|
if (diagram && inBounds(pt, getBounds(diagram))) return { kind: 'diagram', shape: diagram }
|
|
133
|
-
|
|
132
|
+
|
|
134
133
|
return { kind: 'none', shape: null }
|
|
135
134
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -35,7 +35,7 @@ const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<st
|
|
|
35
35
|
movedIds.forEach(id => {
|
|
36
36
|
affectedIds.add(id);
|
|
37
37
|
|
|
38
|
-
const shape = graphStore.
|
|
38
|
+
const shape = graphStore.shapeMap.get(id);
|
|
39
39
|
if (!shape) return;
|
|
40
40
|
|
|
41
41
|
// 添加父元素
|
|
@@ -58,7 +58,7 @@ const collectAffectedShapeIds = (graphStore: any, movedIds: Set<string>): Set<st
|
|
|
58
58
|
let guard = 0;
|
|
59
59
|
while (currentParentId && guard++ < 100) {
|
|
60
60
|
affectedIds.add(currentParentId);
|
|
61
|
-
const parent = graphStore.
|
|
61
|
+
const parent = graphStore.shapeMap.get(currentParentId);
|
|
62
62
|
currentParentId = parent?.parenShapeId;
|
|
63
63
|
}
|
|
64
64
|
});
|
|
@@ -77,7 +77,7 @@ const emitKeyboardMoveEnd = (graphStore: any) => {
|
|
|
77
77
|
|
|
78
78
|
// 组装 payloads(深拷贝)
|
|
79
79
|
const payloads = Array.from(affectedIds)
|
|
80
|
-
.map(id => graphStore.
|
|
80
|
+
.map(id => graphStore.shapeMap.get(id))
|
|
81
81
|
.filter(Boolean)
|
|
82
82
|
.map(s => _.cloneDeep(s));
|
|
83
83
|
|
|
@@ -301,7 +301,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
301
301
|
|
|
302
302
|
// 获取所有选中的图元
|
|
303
303
|
const selectedShapes = graphStore.selectedIds
|
|
304
|
-
.map(id => graphStore.
|
|
304
|
+
.map(id => graphStore.shapeMap.get(id))
|
|
305
305
|
.filter((shape): shape is NonNullable<typeof shape> => shape != null);
|
|
306
306
|
|
|
307
307
|
// ==================== 防抖机制:记录初始状态 ====================
|
|
@@ -362,7 +362,7 @@ export const createKeyboardHandler = (config: KeyboardConfig): KeyboardEventHand
|
|
|
362
362
|
// 查找并计算所有子图元的新位置
|
|
363
363
|
const childIds = graphStore.parentChildMap.get(shape.id) || [];
|
|
364
364
|
childIds.forEach(childId => {
|
|
365
|
-
const child = graphStore.
|
|
365
|
+
const child = graphStore.shapeMap.get(childId);
|
|
366
366
|
if (child && child.bounds) {
|
|
367
367
|
// 如果子图元也在选中列表中,跳过它
|
|
368
368
|
// 因为它会在遍历 selectedShapes 时被单独处理
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CSSProperties } from "vue";
|
|
2
|
+
import type { Rect } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 描述包轮廓覆盖层所需的几何输入。
|
|
6
|
+
*/
|
|
7
|
+
export interface PackageOutlineOverlayOptions {
|
|
8
|
+
bounds: Rect | null | undefined;
|
|
9
|
+
strokeWidth: number;
|
|
10
|
+
padding?: number;
|
|
11
|
+
zIndex: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 供交互层直接消费的包轮廓覆盖层结果。
|
|
16
|
+
*/
|
|
17
|
+
export interface PackageOutlineOverlay {
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
path: string;
|
|
21
|
+
style: CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 与 Package 组件保持一致的“标签页”高度。
|
|
25
|
+
export const PACKAGE_OUTLINE_TAB_HEIGHT = 18;
|
|
26
|
+
|
|
27
|
+
// hover 轮廓会略微向外扩一圈,避免描边显得过于贴边。
|
|
28
|
+
export const PACKAGE_OUTLINE_PADDING = 5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 根据给定宽高生成包形状的 SVG 外轮廓路径。
|
|
32
|
+
*/
|
|
33
|
+
export const buildPackageOutlinePath = (
|
|
34
|
+
width: number,
|
|
35
|
+
height: number,
|
|
36
|
+
strokeWidth: number,
|
|
37
|
+
) => {
|
|
38
|
+
// 兜底,避免图形过小导致轮廓塌陷。
|
|
39
|
+
const safeWidth = Math.max(width, strokeWidth + 1);
|
|
40
|
+
const safeHeight = Math.max(height, strokeWidth + 1);
|
|
41
|
+
|
|
42
|
+
// SVG 描边是压在线两侧的,所以路径需要向内缩半个描边宽度。
|
|
43
|
+
const halfStroke = strokeWidth / 2;
|
|
44
|
+
|
|
45
|
+
// 标签页高度优先固定,但当图形过小时需要自动收缩。
|
|
46
|
+
const tabHeight = Math.max(
|
|
47
|
+
strokeWidth,
|
|
48
|
+
Math.min(PACKAGE_OUTLINE_TAB_HEIGHT, safeHeight - strokeWidth),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// 标签页宽度大致取整体宽度的 40%,同时保证不会越界或过窄。
|
|
52
|
+
const tabWidth = Math.max(
|
|
53
|
+
tabHeight + strokeWidth,
|
|
54
|
+
Math.min(safeWidth - strokeWidth, Math.round(safeWidth * 0.4)),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return `M${halfStroke},${halfStroke} H${tabWidth - halfStroke} V${tabHeight - halfStroke} H${safeWidth - halfStroke} V${safeHeight - halfStroke} H${halfStroke} Z`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 一次性计算包轮廓覆盖层需要的尺寸、路径和绝对定位样式。
|
|
62
|
+
*/
|
|
63
|
+
export const buildPackageOutlineOverlay = ({
|
|
64
|
+
bounds,
|
|
65
|
+
strokeWidth,
|
|
66
|
+
padding = 0,
|
|
67
|
+
zIndex,
|
|
68
|
+
}: PackageOutlineOverlayOptions): PackageOutlineOverlay => {
|
|
69
|
+
if (!bounds) {
|
|
70
|
+
return {
|
|
71
|
+
width: 0,
|
|
72
|
+
height: 0,
|
|
73
|
+
path: "",
|
|
74
|
+
style: { display: "none" },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const width = bounds.width + padding * 2;
|
|
79
|
+
const height = bounds.height + padding * 2;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
width,
|
|
83
|
+
height,
|
|
84
|
+
path: buildPackageOutlinePath(width, height, strokeWidth),
|
|
85
|
+
style: {
|
|
86
|
+
position: "absolute",
|
|
87
|
+
left: `${bounds.x - padding}px`,
|
|
88
|
+
top: `${bounds.y - padding}px`,
|
|
89
|
+
width: `${width}px`,
|
|
90
|
+
height: `${height}px`,
|
|
91
|
+
pointerEvents: "none",
|
|
92
|
+
zIndex,
|
|
93
|
+
overflow: "visible",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
};
|