@mx-sose-front/mx-sose-graph 1.1.2 → 1.1.4

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 (48) hide show
  1. package/dist/index.d.ts +178 -10
  2. package/dist/index.esm.js +4944 -63227
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.umd.js +1 -38
  5. package/dist/index.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +10 -1
  8. package/src/components/ContextMenu/ContextMenu.vue +27 -13
  9. package/src/components/DiagramListTooltip/DiagramListTooltip.vue +7 -12
  10. package/src/components/InteractionLayer.vue +656 -496
  11. package/src/components/LineStyle/LineStyleMarker.vue +1 -1
  12. package/src/components/NameEditor/NameEditor.vue +212 -0
  13. package/src/components/SelectionBox/SelectionBox.vue +189 -0
  14. package/src/components/Shape/Block.vue +1 -1
  15. package/src/constants/edgeShapeKeys.ts +43 -3
  16. package/src/constants/index.ts +21 -4
  17. package/src/hooks/index.ts +3 -0
  18. package/src/hooks/useHighlight.ts +223 -0
  19. package/src/hooks/useNameEdit.ts +234 -0
  20. package/src/{utils/resizeUtils.ts → hooks/useResize.ts} +55 -155
  21. package/src/index.ts +4 -1
  22. package/src/render/shape-renderer.ts +59 -46
  23. package/src/statics/icons/createMenu/show.png +0 -0
  24. package/src/statics/icons/createMenu/tree.png +0 -0
  25. package/src/statics/icons/createMenu//345/261/225/347/244/272/347/253/257/345/217/243/345/261/236/346/200/247@3x.png +0 -0
  26. package/src/statics/icons/createMenu//345/261/225/347/244/272/350/277/236/347/272/277@3x.png +0 -0
  27. package/src/statics/icons/createMenu//346/211/200/345/234/250/345/233/276/350/241/250@3x.png +0 -0
  28. package/src/store/graphStore.ts +185 -65
  29. package/src/types/index.ts +4 -2
  30. package/src/types/interactionLayer.ts +1 -0
  31. package/src/utils/batchAutoExpand.ts +65 -0
  32. package/src/utils/compartment.ts +78 -4
  33. package/src/utils/containers.ts +24 -10
  34. package/src/utils/contextMenuUtils.ts +126 -110
  35. package/src/utils/diagram.ts +19 -15
  36. package/src/utils/drag.ts +10 -5
  37. package/src/utils/edgeUtils.ts +3 -4
  38. package/src/utils/graphDragService.ts +27 -23
  39. package/src/utils/iconLoader.ts +7 -7
  40. package/src/utils/keyboardUtils.ts +221 -30
  41. package/src/utils/pinUtils.ts +1 -2
  42. package/src/utils/shapeOps/shapeOps.ts +168 -0
  43. package/src/utils/viewportCulling.ts +193 -0
  44. package/src/view/graph.vue +115 -60
  45. package/src/utils/highlightUtils.ts +0 -162
  46. package/src/utils/nameEditUtils.ts +0 -132
  47. package/src/utils/packgeMap.ts +0 -1
  48. /package/src/statics/icons/createMenu/{scissors.png → cut.png} +0 -0
@@ -1,14 +1,14 @@
1
- import { ref, type Ref } from 'vue';
1
+ import { ref, onUnmounted, type Ref } from 'vue';
2
2
  import type { Shape, Rect } from '../types';
3
3
  import { useGraphStore } from '../store/graphStore';
4
- import { toLocalPoint, ghostResizeStep } from './geom';
5
- import { calculateTextMinWidth } from './diagram';
6
- import { getPolicy } from './policy';
7
- import { withDrag } from './dom';
8
- import { isCompartment, clampCompartmentResize } from './compartment';
9
- import { clampParentRectToChildrenGap } from './containers';
10
- import { finalizeAfterTransform } from './drag';
11
- import { normalizeZOrder } from './zorder';
4
+ import { toLocalPoint, ghostResizeStep } from '../utils/geom';
5
+ import { calculateTextMinWidth } from '../utils/diagram';
6
+ import { getPolicy } from '../utils/policy';
7
+ import { withDrag } from '../utils/dom';
8
+ import { isCompartment, clampCompartmentResize } from '../utils/compartment';
9
+ import { clampParentRectToChildrenGap } from '../utils/containers';
10
+ import { finalizeAfterTransform } from '../utils/drag';
11
+ import { normalizeZOrder } from '../utils/zorder';
12
12
  import { eventBus } from '../store';
13
13
 
14
14
  /**
@@ -19,8 +19,6 @@ const minH = 30;
19
19
 
20
20
  /**
21
21
  * 最小尺寸配置接口
22
- * @property minW - 最小宽度
23
- * @property minH - 最小高度
24
22
  */
25
23
  export interface MinDimensions {
26
24
  minW: number;
@@ -29,35 +27,16 @@ export interface MinDimensions {
29
27
 
30
28
  /**
31
29
  * 最小尺寸计算模式
32
- * @property RESIZE - 缩放过程中的最小尺寸
33
- * @property STOP - 缩放结束时的最小尺寸
34
30
  */
35
31
  export enum MinDimensionMode {
36
32
  RESIZE = 'resize',
37
33
  STOP = 'stop'
38
34
  }
39
35
 
40
- /**
41
- * 缩放状态接口
42
- * 包含缩放过程中需要追踪的所有响应式状态
43
- */
44
- export interface ResizeState {
45
- isResizing: Ref<boolean>; // 是否正在缩放
46
- resizeDirection: Ref<string>; // 当前缩放方向 (nw/ne/sw/se)
47
- startPos: Ref<{ x: number; y: number }>; // 缩放起始鼠标位置
48
- startBounds: Ref<Rect>; // 缩放起始时的元素边界
49
- resizingTarget: Ref<Shape | null>; // 当前缩放的目标元素
50
- groupBase: Ref<Record<string, Rect>>; // 缩放前的基础边界(用于多选)
51
- groupGhost: Ref<Record<string, Rect>>; // 缩放预览边界(用于多选)
52
- }
53
-
54
36
  /**
55
37
  * 缩放配置接口
56
- * @property packages - 需要特殊最小高度处理的 shapeKey 列表
57
- * @property diagram - diagram 组件的 shapeKey 列表
58
- * @property taggedValueLabels - taggedValueLabels 组件的 shapeKey 列表
59
38
  */
60
- export interface ResizeConfig {
39
+ export interface UseResizeConfig {
61
40
  packages?: string[];
62
41
  diagram?: string[];
63
42
  taggedValueLabels?: string[];
@@ -66,54 +45,58 @@ export interface ResizeConfig {
66
45
  /**
67
46
  * 缩放回调函数接口
68
47
  */
69
- export interface ResizeCallbacks {
70
- onResizeStart?: (target: Shape) => void; // 缩放开始时回调
71
- onResizeEnd?: (target: Shape) => void; // 缩放结束时回调
72
- onShapeUpdate?: (id: string, updates: { bounds: Rect }) => void; // 元素更新时回调
48
+ export interface UseResizeCallbacks {
49
+ onResizeStart?: (target: Shape) => void;
50
+ onResizeEnd?: (target: Shape) => void;
51
+ onShapeUpdate?: (id: string, updates: { bounds: Rect }) => void;
73
52
  }
74
53
 
75
54
  /**
76
- * 创建缩放工具函数
77
- *
78
- * 功能:
79
- * - 封装缩放操作的所有状态和方法
80
- * - 提供响应式的缩放状态供外部使用
81
- * - 处理缩放的完整生命周期(开始、进行中、结束、取消)
55
+ * useResize 返回类型
56
+ */
57
+ export interface UseResizeReturn {
58
+ isResizing: Ref<boolean>;
59
+ resizeDirection: Ref<string>;
60
+ startPos: Ref<{ x: number; y: number }>;
61
+ startBounds: Ref<Rect>;
62
+ resizingTarget: Ref<Shape | null>;
63
+ groupBase: Ref<Record<string, Rect>>;
64
+ groupGhost: Ref<Record<string, Rect>>;
65
+ startResize: (e: MouseEvent, dir: "nw" | "ne" | "sw" | "se", target: Shape) => void;
66
+ handleResize: (e: MouseEvent) => void;
67
+ stopResize: () => void;
68
+ cancelResize: () => void;
69
+ getMinDimensions: (shape: Shape, baseMinW?: number, mode?: MinDimensionMode) => MinDimensions;
70
+ }
71
+
72
+ /**
73
+ * 图元缩放 Composable
82
74
  *
83
75
  * @param layerRef - 交互层的 DOM 引用
84
- * @param config - 缩放配置(包含各类型组件的 shapeKey 列表)
76
+ * @param config - 缩放配置
85
77
  * @param callbacks - 缩放过程的回调函数
86
- * @returns 缩放工具对象,包含所有状态和方法
78
+ * @returns 缩放相关的状态和方法
87
79
  */
88
- export function createResizeUtils(
89
- layerRef: { value: HTMLDivElement | null },
90
- config: ResizeConfig,
91
- callbacks: ResizeCallbacks = {}
92
- ) {
80
+ export function useResize(
81
+ layerRef: Ref<HTMLDivElement | null>,
82
+ config: UseResizeConfig,
83
+ callbacks: UseResizeCallbacks = {}
84
+ ): UseResizeReturn {
93
85
  const graphStore = useGraphStore();
94
86
 
95
- // ========== 缩放状态 ==========
96
-
97
- const isResizing = ref(false); // 是否正在缩放
98
- const resizeDirection = ref<"nw" | "ne" | "sw" | "se" | "">(""); // 当前缩放方向
99
- const startPos = ref({ x: 0, y: 0 }); // 鼠标按下时的位置
100
- const startBounds = ref({ x: 0, y: 0, width: 0, height: 0 }); // 元素原始边界
101
- const resizingTarget = ref<Shape | null>(null); // 当前缩放的目标元素
102
- const groupBase = ref<Record<string, Rect>>({}); // 多选时的原始边界
103
- const groupGhost = ref<Record<string, Rect>>({}); // 多选时的预览边界
87
+ // 缩放状态
88
+ const isResizing = ref(false);
89
+ const resizeDirection = ref<"nw" | "ne" | "sw" | "se" | "">("");
90
+ const startPos = ref({ x: 0, y: 0 });
91
+ const startBounds = ref<Rect>({ x: 0, y: 0, width: 0, height: 0 });
92
+ const resizingTarget = ref<Shape | null>(null);
93
+ const groupBase = ref<Record<string, Rect>>({});
94
+ const groupGhost = ref<Record<string, Rect>>({});
104
95
 
105
- let offDrag: (() => void) | null = null; // 拖拽清理函数
96
+ let offDrag: (() => void) | null = null;
106
97
 
107
98
  /**
108
99
  * 获取指定元素的最小尺寸限制
109
- *
110
- * 根据元素的 shapeKey 类型返回不同的最小高度和宽度限制
111
- * 不同类型的组件有不同的最小尺寸要求
112
- *
113
- * @param shape - 要计算最小尺寸的元素
114
- * @param baseMinW - 基础最小宽度(默认 50)
115
- * @param mode - 计算模式(RESIZE 或 STOP)
116
- * @returns 最小尺寸配置 { minW, minH }
117
100
  */
118
101
  const getMinDimensions = (
119
102
  shape: Shape,
@@ -123,24 +106,19 @@ export function createResizeUtils(
123
106
  let result: MinDimensions = { minW: baseMinW, minH: 60 };
124
107
 
125
108
  if (shape.shapeKey) {
126
- // packages 类型:缩放时最小高度 75,结束时 90
127
109
  if (config.packages && config.packages.includes(shape.shapeKey)) {
128
110
  result.minH = mode === MinDimensionMode.RESIZE ? 75 : 90;
129
111
  }
130
- // diagram 类型:缩放时最小高度 80,结束时 50
131
112
  else if (config.diagram && config.diagram.includes(shape.shapeKey)) {
132
113
  result.minH = mode === MinDimensionMode.RESIZE ? 80 : 50;
133
114
  }
134
- // taggedValueLabels 类型:最小高度 125
135
115
  else if (config.taggedValueLabels && config.taggedValueLabels.includes(shape.shapeKey)) {
136
116
  result.minH = 125;
137
117
  }
138
- // pin 类型:最小宽高 22
139
118
  else if (graphStore.pinsTypes.includes(shape.shapeKey)) {
140
119
  result.minH = 22;
141
120
  result.minW = 22;
142
121
  }
143
- // port 类型:最小宽高 22
144
122
  else if (graphStore.portsTypes.includes(shape.shapeKey)) {
145
123
  result.minH = 22;
146
124
  result.minW = 22;
@@ -152,57 +130,36 @@ export function createResizeUtils(
152
130
 
153
131
  /**
154
132
  * 开始缩放
155
- *
156
- * 触发时机:用户按下缩放手柄时
157
- *
158
- * 执行步骤:
159
- * 1. 检查是否允许缩放(ConceptRole 或多选时不允)
160
- * 2. 设置缩放状态(isResizing = true)
161
- * 3. 记录缩放方向和目标元素
162
- * 4. 记录起始位置和元素边界
163
- * 5. 初始化 groupBase 和 groupGhost
164
- * 6. 绑定拖拽事件监听(mousemove 和 mouseup)
165
- *
166
- * @param e - 鼠标事件
167
- * @param dir - 缩放方向 (nw/ne/sw/se)
168
- * @param target - 目标元素
169
133
  */
170
134
  const startResize = (
171
135
  e: MouseEvent,
172
136
  dir: "nw" | "ne" | "sw" | "se",
173
137
  target: Shape
174
138
  ) => {
175
- // ConceptRole 类型不允许缩放
176
139
  if (target.shapeKey === 'ConceptRole') {
177
140
  e.preventDefault();
178
141
  e.stopPropagation();
179
142
  return;
180
143
  }
181
144
 
182
- // 多选状态下不允许缩放
183
145
  if (graphStore.selectedIds.length > 1) {
184
146
  e.preventDefault();
185
147
  e.stopPropagation();
186
148
  return;
187
149
  }
188
150
 
189
- // 阻止默认行为和事件冒泡
190
151
  e.preventDefault();
191
152
  e.stopPropagation();
192
153
 
193
- // 设置缩放状态
194
154
  isResizing.value = true;
195
155
  resizingTarget.value = target;
196
156
  resizeDirection.value = dir;
197
157
 
198
- // 触发开始回调
199
158
  callbacks.onResizeStart?.(target);
200
159
 
201
- // 记录起始位置
202
160
  const anchor = toLocalPoint(e, layerRef.value!);
203
161
  startPos.value = { x: anchor.x, y: anchor.y };
204
162
 
205
- // 记录起始边界
206
163
  const b = target.bounds ?? {};
207
164
  startBounds.value = {
208
165
  x: b.x ?? 0,
@@ -211,26 +168,14 @@ export function createResizeUtils(
211
168
  height: b.height ?? 50,
212
169
  };
213
170
 
214
- // 初始化预览状态
215
171
  groupBase.value = { [target.id]: { ...startBounds.value } };
216
172
  groupGhost.value = { [target.id]: { ...startBounds.value } };
217
173
 
218
- // 绑定拖拽事件
219
174
  offDrag = withDrag(handleResize, stopResize);
220
175
  };
221
176
 
222
177
  /**
223
178
  * 缩放进行中
224
- *
225
- * 触发时机:用户拖拽缩放手柄时(由 withDrag 调用)
226
- *
227
- * 执行步骤:
228
- * 1. 获取当前鼠标位置
229
- * 2. 计算新的边界(基于起始边界和鼠标偏移)
230
- * 3. 应用各类约束(容器边界、文本最小宽度、隔间组件、父子间隙)
231
- * 4. 更新 groupGhost 供 UI 显示预览
232
- *
233
- * @param e - 鼠标事件
234
179
  */
235
180
  const handleResize = (e: MouseEvent) => {
236
181
  if (!isResizing.value || !resizingTarget.value) return;
@@ -240,7 +185,6 @@ export function createResizeUtils(
240
185
  const handle = resizeDirection.value as "nw" | "ne" | "sw" | "se";
241
186
  const shape = resizingTarget.value;
242
187
 
243
- // 容器边界约束(只限制左上角不小于 0,不限制右下角以允许无限放大)
244
188
  const container = {
245
189
  left: 0,
246
190
  top: 0,
@@ -248,43 +192,32 @@ export function createResizeUtils(
248
192
  bottom: Infinity,
249
193
  };
250
194
 
251
- // 获取元素的边界约束策略
252
195
  const edges = getPolicy(shape).constrainToDiagram;
253
-
254
- // 计算基于文本内容的最小宽度
255
196
  const dynamicMinW = calculateTextMinWidth(shape);
256
197
 
257
- // 判断是放大还是缩小
258
198
  const dx = curr.x - startPos.value.x;
259
199
  const isEnlarging = (handle === 'ne' || handle === 'se') ? dx > 0 : dx < 0;
260
- // 放大时使用较小约束,缩小时不小于文本所需宽度
261
200
  const baseMinW = isEnlarging ? minW / 2 : Math.max(minW, dynamicMinW);
262
201
 
263
- // 获取该类型的最小尺寸限制
264
202
  const { minW: effectiveMinW, minH: effectiveMinH } = getMinDimensions(shape, baseMinW);
265
203
 
266
- // 判断是否为 diagram 组件
267
204
  const isDiagramComponent = shape.shapeKey && config.diagram && config.diagram.includes(shape.shapeKey);
268
205
 
269
- // 计算预览边界
270
206
  let next = ghostResizeStep(
271
207
  { x: base.x, y: base.y, width: base.width, height: base.height },
272
208
  handle,
273
209
  { x: startPos.value.x, y: startPos.value.y },
274
210
  curr,
275
211
  container,
276
- // 只限制 left 和 top
277
212
  { left: edges.left, top: edges.top, right: false, bottom: false },
278
213
  effectiveMinW,
279
214
  effectiveMinH
280
215
  );
281
216
 
282
- // diagram 组件保持高度不变
283
217
  if (isDiagramComponent && shape.shapeType === 'shape') {
284
218
  next = { ...next, height: base.height };
285
219
  }
286
220
 
287
- // 画布类型(diagram)不移动位置
288
221
  const isDiagram = (shape as any).shapeType?.toLowerCase?.() === "diagram";
289
222
  if (isDiagram) {
290
223
  next = { ...next, x: base.x, y: base.y };
@@ -292,7 +225,6 @@ export function createResizeUtils(
292
225
  next.y = Math.max(0, next.y);
293
226
  }
294
227
 
295
- // 隔间组件有特殊的缩放约束
296
228
  if (isCompartment(shape)) {
297
229
  next = clampCompartmentResize(
298
230
  graphStore.shapes,
@@ -302,7 +234,6 @@ export function createResizeUtils(
302
234
  );
303
235
  }
304
236
 
305
- // 确保父元素缩放后子元素仍然可见(有间隙约束)
306
237
  next = clampParentRectToChildrenGap(
307
238
  graphStore.shapes,
308
239
  shape,
@@ -314,7 +245,6 @@ export function createResizeUtils(
314
245
  groupGhost.value
315
246
  );
316
247
 
317
- // 更新预览边界
318
248
  groupGhost.value = {
319
249
  ...groupGhost.value,
320
250
  [shape.id]: next,
@@ -323,17 +253,6 @@ export function createResizeUtils(
323
253
 
324
254
  /**
325
255
  * 停止缩放
326
- *
327
- * 触发时机:用户松开鼠标(由 withDrag 调用)
328
- *
329
- * 执行步骤:
330
- * 1. 获取最终边界(groupGhost 或起始边界)
331
- * 2. 应用最小尺寸约束
332
- * 3. 更新元素实际边界
333
- * 4. 处理可能的重父子关系
334
- * 5. 清理缩放状态
335
- *
336
- * @param e - 鼠标事件(未使用,但 withDrag 要求)
337
256
  */
338
257
  const stopResize = () => {
339
258
  if (!isResizing.value || !resizingTarget.value) return;
@@ -342,13 +261,11 @@ export function createResizeUtils(
342
261
  const shape = resizingTarget.value;
343
262
  const final = groupGhost.value[id] ?? startBounds.value;
344
263
 
345
- // 计算最小尺寸约束
346
264
  const dynamicMinW = calculateTextMinWidth(shape);
347
265
  const { minW: effectiveMinW, minH: minHeight } = getMinDimensions(shape, dynamicMinW, MinDimensionMode.STOP);
348
266
 
349
267
  const isDiagramComponent = shape.shapeKey && config.diagram && config.diagram.includes(shape.shapeKey);
350
268
 
351
- // 应用最小尺寸约束
352
269
  let finalBounds = { ...final };
353
270
  if (final.width < effectiveMinW) {
354
271
  finalBounds.width = effectiveMinW;
@@ -360,21 +277,17 @@ export function createResizeUtils(
360
277
  finalBounds = { ...finalBounds, height: startBounds.value.height };
361
278
  }
362
279
 
363
- // 检查边界是否有变化
364
280
  const shapeBefore = graphStore.shapes.find((s) => s.id === id);
365
281
  const changed = shapeBefore && JSON.stringify(shapeBefore.bounds) !== JSON.stringify(finalBounds);
366
282
 
367
283
  if (changed) {
368
- // 更新元素边界
369
284
  callbacks.onShapeUpdate?.(id, { bounds: finalBounds });
370
285
  graphStore.updateShape(id, { bounds: finalBounds });
371
286
 
372
- // 同步更新 selectedShape
373
287
  if (graphStore.selectedShape?.id === id && graphStore.selectedShape) {
374
288
  graphStore.selectedShape.bounds = { ...finalBounds };
375
289
  }
376
290
 
377
- // 处理重父子关系
378
291
  const containerForFinalize =
379
292
  graphStore.hoverContainerId && graphStore.hoverNestable ? graphStore.hoverContainerId : null;
380
293
 
@@ -388,7 +301,6 @@ export function createResizeUtils(
388
301
  graphStore.updateShape
389
302
  );
390
303
 
391
- // 如果有重父子,需要重新计算 z-order
392
304
  if (didReparent) {
393
305
  normalizeZOrder(
394
306
  graphStore.shapes,
@@ -399,15 +311,12 @@ export function createResizeUtils(
399
311
  );
400
312
  }
401
313
 
402
- // 通知 store 缩放结束
403
314
  graphStore.endResizeShape(id);
404
315
  }
405
316
 
406
- // 触发结束回调
407
317
  callbacks.onResizeEnd?.(resizingTarget.value);
408
318
  eventBus.emit('resize-end', { target: resizingTarget.value });
409
319
 
410
- // 清理状态
411
320
  isResizing.value = false;
412
321
  resizeDirection.value = "";
413
322
  resizingTarget.value = null;
@@ -419,12 +328,6 @@ export function createResizeUtils(
419
328
 
420
329
  /**
421
330
  * 取消缩放
422
- *
423
- * 用于外部强制取消缩放操作(如切换选中元素)
424
- *
425
- * 执行步骤:
426
- * 1. 清理拖拽事件监听
427
- * 2. 重置所有缩放状态
428
331
  */
429
332
  const cancelResize = () => {
430
333
  if (offDrag) {
@@ -438,14 +341,11 @@ export function createResizeUtils(
438
341
  groupGhost.value = {};
439
342
  };
440
343
 
441
- /**
442
- * 返回值说明:
443
- * - 响应式状态:供外部组件读取缩放状态(如 isBusy 计算属性)
444
- * - startResize:供模板绑定缩放手柄的 mousedown 事件
445
- * - groupGhost:供 allGhosts 计算属性获取预览框数据
446
- * - handleResize/stopResize/cancelResize:内部使用
447
- * - getMinDimensions:供外部计算最小尺寸
448
- */
344
+ // 组件卸载时自动清理
345
+ onUnmounted(() => {
346
+ cancelResize();
347
+ });
348
+
449
349
  return {
450
350
  isResizing,
451
351
  resizeDirection,
@@ -458,6 +358,6 @@ export function createResizeUtils(
458
358
  handleResize,
459
359
  stopResize,
460
360
  cancelResize,
461
- getMinDimensions
361
+ getMinDimensions,
462
362
  };
463
363
  }
package/src/index.ts CHANGED
@@ -10,6 +10,9 @@ export { GraphView }
10
10
  // 导出 store
11
11
  export { useGraphStore, eventBus } from './store'
12
12
 
13
+ // 导出工具类
14
+ export { ContextMenuUtils } from './utils/contextMenuUtils'
15
+
13
16
  // 导出类型
14
17
  export type { Shape, Bounds, Style, NameObject, TaggedValueLabel, Comparent, GraphEvents } from './types'
15
18
  /**
@@ -35,7 +38,7 @@ export const MxSoseGraphPlugin: Plugin<[MxSoseGraphOptions?]> = {
35
38
  if (import.meta.env.DEV) {
36
39
  console.warn(
37
40
  '[mx-sose-graph] 未找到 pinia 实例,请确保:' +
38
- '1)先 app.use(pinia),2)再 app.use(MxSoseGraphPlugin, { pinia })'
41
+ '1)先 app.use(pinia),2)再 app.use(MxSoseGraphPlugin, { pinia })'
39
42
  )
40
43
  }
41
44
  }
@@ -36,68 +36,81 @@ export const getShapeComponent = (shape: Shape) => {
36
36
  return "ShapeComponent";
37
37
  }
38
38
  };
39
- // 根据 shape bounds 生成样式
40
- export const getShapeStyle = (shape: Shape): CSSProperties => {
41
- const { bounds, shapeType } = shape;
39
+ // Edge waypoints 解析结果缓存,避免每帧重复 JSON.parse(约 60+ 图元时明显)
40
+ const EDGE_STYLE_CACHE_MAX = 128
41
+ const edgeStyleCache = new Map<string, CSSProperties>()
42
42
 
43
- // Edge组件需要特殊处理
44
- if (shapeType === "edge") {
45
- // 解析waypoints计算边界框
46
- let waypoints: Array<{ x: number; y: number }> = [];
43
+ function getEdgeShapeStyle(shape: Shape): CSSProperties {
44
+ const raw = shape.waypointId
45
+ const cacheKey = shape.id + (typeof raw === "string" ? raw : raw ? JSON.stringify(raw) : "")
46
+ const cached = edgeStyleCache.get(cacheKey)
47
+ if (cached) return cached
47
48
 
48
- if (shape.waypointId) {
49
- if (typeof shape.waypointId === "string") {
50
- try {
51
- waypoints = JSON.parse(shape.waypointId);
52
- } catch (error) {
53
- console.error("解析waypointId失败:", error);
54
- waypoints = [];
55
- }
56
- } else if (Array.isArray(shape.waypointId)) {
57
- waypoints = shape.waypointId;
49
+ let waypoints: Array<{ x: number; y: number }> = []
50
+ if (raw) {
51
+ if (typeof raw === "string") {
52
+ try {
53
+ waypoints = JSON.parse(raw)
54
+ } catch (error) {
55
+ console.error("解析waypointId失败:", error)
58
56
  }
57
+ } else if (Array.isArray(raw)) {
58
+ waypoints = raw
59
59
  }
60
+ }
60
61
 
61
- if (waypoints.length === 0) {
62
- return {
63
- position: "absolute",
64
- left: "0px",
65
- top: "0px",
66
- width: "100px",
67
- height: "50px",
68
- opacity: 0, // 初始时隐藏
69
- };
62
+ let result: CSSProperties
63
+ if (waypoints.length === 0) {
64
+ result = {
65
+ position: "absolute",
66
+ left: "0px",
67
+ top: "0px",
68
+ width: "100px",
69
+ height: "50px",
70
+ opacity: 0,
70
71
  }
71
-
72
- // 计算边界框
73
- const xs = waypoints.map((wp) => wp.x);
74
- const ys = waypoints.map((wp) => wp.y);
75
-
76
- const minX = Math.min(...xs);
77
- const maxX = Math.max(...xs);
78
- const minY = Math.min(...ys);
79
- const maxY = Math.max(...ys);
80
-
81
- // 计算容器尺寸和位置
82
- const padding = 10;
83
- const width = Math.max(maxX - minX + padding * 2, 20); // 最小宽度20
84
- const height = maxY - minY + padding * 2;
85
-
86
- return {
72
+ } else {
73
+ const xs = waypoints.map((wp) => wp.x)
74
+ const ys = waypoints.map((wp) => wp.y)
75
+ const minX = Math.min(...xs)
76
+ const maxX = Math.max(...xs)
77
+ const minY = Math.min(...ys)
78
+ const maxY = Math.max(...ys)
79
+ const padding = 10
80
+ const width = Math.max(maxX - minX + padding * 2, 20)
81
+ const height = maxY - minY + padding * 2
82
+ result = {
87
83
  position: "absolute",
88
84
  left: `${minX - padding}px`,
89
85
  top: `${minY - padding}px`,
90
86
  width: `${width}px`,
91
87
  height: `${height}px`,
92
- };
88
+ }
89
+ }
90
+ if (edgeStyleCache.size >= EDGE_STYLE_CACHE_MAX) {
91
+ const firstKey = edgeStyleCache.keys().next().value
92
+ if (firstKey != null) edgeStyleCache.delete(firstKey)
93
93
  }
94
+ edgeStyleCache.set(cacheKey, result)
95
+ return result
96
+ }
97
+
98
+ /** 批量更新图元时调用,避免缓存无限增长 */
99
+ export function clearEdgeStyleCache(): void {
100
+ edgeStyleCache.clear()
101
+ }
102
+
103
+ // 根据 shape 的 bounds 生成样式
104
+ export const getShapeStyle = (shape: Shape): CSSProperties => {
105
+ const { bounds, shapeType } = shape
106
+
107
+ if (shapeType === "edge") return getEdgeShapeStyle(shape)
94
108
 
95
- // 其他组件使用bounds
96
109
  return {
97
110
  position: "absolute",
98
111
  left: `${bounds?.x || 0}px`,
99
112
  top: `${bounds?.y || 0}px`,
100
113
  width: `${bounds?.width || 100}px`,
101
114
  height: `${bounds?.height || 50}px`,
102
- };
103
- };
115
+ }
116
+ }