@mx-sose-front/mx-sose-graph 1.1.7 → 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.
Files changed (52) hide show
  1. package/dist/assets/edgeWorker-b57ca007.js +2 -0
  2. package/dist/assets/edgeWorker-b57ca007.js.map +1 -0
  3. package/dist/index.d.ts +633 -30
  4. package/dist/index.esm.js +8728 -4734
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.umd.js +1 -1
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/style.css +1 -1
  9. package/package.json +1 -1
  10. package/src/components/Common/Tree.vue +451 -0
  11. package/src/components/Common/index.ts +2 -0
  12. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +1 -2
  13. package/src/components/Edge/Edge.vue +172 -169
  14. package/src/components/Gantt/Gantt.vue +1544 -0
  15. package/src/components/GanttContextMenu/GanttContextMenu.vue +304 -0
  16. package/src/components/InteractionLayer.vue +343 -147
  17. package/src/components/Matrix/Matrix.vue +828 -0
  18. package/src/components/Matrix/index.ts +168 -0
  19. package/src/components/Shape/ConceptualRole.vue +2 -34
  20. package/src/components/Table/Table.vue +970 -0
  21. package/src/constants/edgeShapeKeys.ts +8 -5
  22. package/src/constants/index.ts +259 -45
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/useChartRowSelection.ts +456 -0
  25. package/src/hooks/useResize.ts +2 -2
  26. package/src/hooks/useVirtualScroll.ts +258 -0
  27. package/src/index.ts +1 -1
  28. package/src/render/shape-renderer.ts +62 -2
  29. package/src/statics/icons/childIcons//345/221/275/344/273/244@3x.png +0 -0
  30. package/src/statics/icons/childIcons//346/210/230/347/225/245/346/246/202/345/277/265/350/241/250@3x.png +0 -0
  31. package/src/statics/icons/childIcons//346/216/247/345/210/266@3x.png +0 -0
  32. package/src/statics/icons/createMenu/down.png +0 -0
  33. package/src/statics/icons/createMenu/remove.png +0 -0
  34. package/src/statics/icons/createMenu/up.png +0 -0
  35. package/src/store/graphStore.ts +217 -44
  36. package/src/types/index.ts +86 -4
  37. package/src/utils/batchAutoExpand.ts +9 -10
  38. package/src/utils/containers.ts +72 -17
  39. package/src/utils/contextMenuUtils.ts +7 -7
  40. package/src/utils/dateUtils.ts +160 -0
  41. package/src/utils/diagram.ts +10 -8
  42. package/src/utils/drag.ts +6 -5
  43. package/src/utils/edgeUtils.ts +344 -427
  44. package/src/utils/edgeWorker.ts +471 -0
  45. package/src/utils/hittest.ts +37 -38
  46. package/src/utils/index.ts +3 -0
  47. package/src/utils/keyboardUtils.ts +5 -5
  48. package/src/utils/packageOutline.ts +96 -0
  49. package/src/utils/rafThrottle.ts +162 -0
  50. package/src/utils/workerManager.ts +335 -0
  51. package/src/view/graph.vue +47 -33
  52. /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 驱动
@@ -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
- // 首先检查 pin 类型(优先级最高,因为 pin 通常在其他图元之上)
92
- const pinsOrdered = shapes
93
- .map((s, i) => ({ s, i }))
94
- .filter(x => isPin(x.s))
95
- .sort((a, b) => {
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 (const { s } of pinsOrdered) {
101
- if (inBounds(pt, getBounds(s))) return { kind: 'pin', shape: s }
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
- for (const { s } of shapesOrdered) {
114
- if (inBounds(pt, getBounds(s))) return { kind: 'shape', shape: s }
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
- for (const { s } of edgesOrdered) {
127
- if (isPointNearEdge(pt, s)) return { kind: 'edge', shape: s }
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
  }
@@ -18,3 +18,6 @@ export function getUuid(): string {
18
18
 
19
19
  return `${timeLow}${timeMid}${timeHigh}${clockSeqHex}${nodeHex}`
20
20
  }
21
+
22
+ // 导出日期工具类
23
+ export { DateUtils } from './dateUtils'
@@ -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.shapes.find((s: any) => s.id === id);
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.shapes.find((s: any) => s.id === currentParentId);
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.shapes.find((s: any) => s.id === id))
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.shapes.find(s => s.id === id))
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.shapes.find(s => s.id === childId);
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
+ };