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

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.
@@ -0,0 +1,463 @@
1
+ import { ref, type Ref } from 'vue';
2
+ import type { Shape, Rect } from '../types';
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';
12
+ import { eventBus } from '../store';
13
+
14
+ /**
15
+ * 缩放操作的全局最小尺寸限制
16
+ */
17
+ const minW = 50;
18
+ const minH = 30;
19
+
20
+ /**
21
+ * 最小尺寸配置接口
22
+ * @property minW - 最小宽度
23
+ * @property minH - 最小高度
24
+ */
25
+ export interface MinDimensions {
26
+ minW: number;
27
+ minH: number;
28
+ }
29
+
30
+ /**
31
+ * 最小尺寸计算模式
32
+ * @property RESIZE - 缩放过程中的最小尺寸
33
+ * @property STOP - 缩放结束时的最小尺寸
34
+ */
35
+ export enum MinDimensionMode {
36
+ RESIZE = 'resize',
37
+ STOP = 'stop'
38
+ }
39
+
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
+ /**
55
+ * 缩放配置接口
56
+ * @property packages - 需要特殊最小高度处理的 shapeKey 列表
57
+ * @property diagram - diagram 组件的 shapeKey 列表
58
+ * @property taggedValueLabels - taggedValueLabels 组件的 shapeKey 列表
59
+ */
60
+ export interface ResizeConfig {
61
+ packages?: string[];
62
+ diagram?: string[];
63
+ taggedValueLabels?: string[];
64
+ }
65
+
66
+ /**
67
+ * 缩放回调函数接口
68
+ */
69
+ export interface ResizeCallbacks {
70
+ onResizeStart?: (target: Shape) => void; // 缩放开始时回调
71
+ onResizeEnd?: (target: Shape) => void; // 缩放结束时回调
72
+ onShapeUpdate?: (id: string, updates: { bounds: Rect }) => void; // 元素更新时回调
73
+ }
74
+
75
+ /**
76
+ * 创建缩放工具函数
77
+ *
78
+ * 功能:
79
+ * - 封装缩放操作的所有状态和方法
80
+ * - 提供响应式的缩放状态供外部使用
81
+ * - 处理缩放的完整生命周期(开始、进行中、结束、取消)
82
+ *
83
+ * @param layerRef - 交互层的 DOM 引用
84
+ * @param config - 缩放配置(包含各类型组件的 shapeKey 列表)
85
+ * @param callbacks - 缩放过程的回调函数
86
+ * @returns 缩放工具对象,包含所有状态和方法
87
+ */
88
+ export function createResizeUtils(
89
+ layerRef: { value: HTMLDivElement | null },
90
+ config: ResizeConfig,
91
+ callbacks: ResizeCallbacks = {}
92
+ ) {
93
+ const graphStore = useGraphStore();
94
+
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>>({}); // 多选时的预览边界
104
+
105
+ let offDrag: (() => void) | null = null; // 拖拽清理函数
106
+
107
+ /**
108
+ * 获取指定元素的最小尺寸限制
109
+ *
110
+ * 根据元素的 shapeKey 类型返回不同的最小高度和宽度限制
111
+ * 不同类型的组件有不同的最小尺寸要求
112
+ *
113
+ * @param shape - 要计算最小尺寸的元素
114
+ * @param baseMinW - 基础最小宽度(默认 50)
115
+ * @param mode - 计算模式(RESIZE 或 STOP)
116
+ * @returns 最小尺寸配置 { minW, minH }
117
+ */
118
+ const getMinDimensions = (
119
+ shape: Shape,
120
+ baseMinW: number = minW,
121
+ mode: MinDimensionMode = MinDimensionMode.RESIZE
122
+ ): MinDimensions => {
123
+ let result: MinDimensions = { minW: baseMinW, minH: 60 };
124
+
125
+ if (shape.shapeKey) {
126
+ // packages 类型:缩放时最小高度 75,结束时 90
127
+ if (config.packages && config.packages.includes(shape.shapeKey)) {
128
+ result.minH = mode === MinDimensionMode.RESIZE ? 75 : 90;
129
+ }
130
+ // diagram 类型:缩放时最小高度 80,结束时 50
131
+ else if (config.diagram && config.diagram.includes(shape.shapeKey)) {
132
+ result.minH = mode === MinDimensionMode.RESIZE ? 80 : 50;
133
+ }
134
+ // taggedValueLabels 类型:最小高度 125
135
+ else if (config.taggedValueLabels && config.taggedValueLabels.includes(shape.shapeKey)) {
136
+ result.minH = 125;
137
+ }
138
+ // pin 类型:最小宽高 22
139
+ else if (graphStore.pinsTypes.includes(shape.shapeKey)) {
140
+ result.minH = 22;
141
+ result.minW = 22;
142
+ }
143
+ // port 类型:最小宽高 22
144
+ else if (graphStore.portsTypes.includes(shape.shapeKey)) {
145
+ result.minH = 22;
146
+ result.minW = 22;
147
+ }
148
+ }
149
+
150
+ return result;
151
+ };
152
+
153
+ /**
154
+ * 开始缩放
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
+ */
170
+ const startResize = (
171
+ e: MouseEvent,
172
+ dir: "nw" | "ne" | "sw" | "se",
173
+ target: Shape
174
+ ) => {
175
+ // ConceptRole 类型不允许缩放
176
+ if (target.shapeKey === 'ConceptRole') {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+ return;
180
+ }
181
+
182
+ // 多选状态下不允许缩放
183
+ if (graphStore.selectedIds.length > 1) {
184
+ e.preventDefault();
185
+ e.stopPropagation();
186
+ return;
187
+ }
188
+
189
+ // 阻止默认行为和事件冒泡
190
+ e.preventDefault();
191
+ e.stopPropagation();
192
+
193
+ // 设置缩放状态
194
+ isResizing.value = true;
195
+ resizingTarget.value = target;
196
+ resizeDirection.value = dir;
197
+
198
+ // 触发开始回调
199
+ callbacks.onResizeStart?.(target);
200
+
201
+ // 记录起始位置
202
+ const anchor = toLocalPoint(e, layerRef.value!);
203
+ startPos.value = { x: anchor.x, y: anchor.y };
204
+
205
+ // 记录起始边界
206
+ const b = target.bounds ?? {};
207
+ startBounds.value = {
208
+ x: b.x ?? 0,
209
+ y: b.y ?? 0,
210
+ width: b.width ?? 100,
211
+ height: b.height ?? 50,
212
+ };
213
+
214
+ // 初始化预览状态
215
+ groupBase.value = { [target.id]: { ...startBounds.value } };
216
+ groupGhost.value = { [target.id]: { ...startBounds.value } };
217
+
218
+ // 绑定拖拽事件
219
+ offDrag = withDrag(handleResize, stopResize);
220
+ };
221
+
222
+ /**
223
+ * 缩放进行中
224
+ *
225
+ * 触发时机:用户拖拽缩放手柄时(由 withDrag 调用)
226
+ *
227
+ * 执行步骤:
228
+ * 1. 获取当前鼠标位置
229
+ * 2. 计算新的边界(基于起始边界和鼠标偏移)
230
+ * 3. 应用各类约束(容器边界、文本最小宽度、隔间组件、父子间隙)
231
+ * 4. 更新 groupGhost 供 UI 显示预览
232
+ *
233
+ * @param e - 鼠标事件
234
+ */
235
+ const handleResize = (e: MouseEvent) => {
236
+ if (!isResizing.value || !resizingTarget.value) return;
237
+
238
+ const curr = toLocalPoint(e, layerRef.value!);
239
+ const base = startBounds.value;
240
+ const handle = resizeDirection.value as "nw" | "ne" | "sw" | "se";
241
+ const shape = resizingTarget.value;
242
+
243
+ // 容器边界约束(只限制左上角不小于 0,不限制右下角以允许无限放大)
244
+ const container = {
245
+ left: 0,
246
+ top: 0,
247
+ right: Infinity,
248
+ bottom: Infinity,
249
+ };
250
+
251
+ // 获取元素的边界约束策略
252
+ const edges = getPolicy(shape).constrainToDiagram;
253
+
254
+ // 计算基于文本内容的最小宽度
255
+ const dynamicMinW = calculateTextMinWidth(shape);
256
+
257
+ // 判断是放大还是缩小
258
+ const dx = curr.x - startPos.value.x;
259
+ const isEnlarging = (handle === 'ne' || handle === 'se') ? dx > 0 : dx < 0;
260
+ // 放大时使用较小约束,缩小时不小于文本所需宽度
261
+ const baseMinW = isEnlarging ? minW / 2 : Math.max(minW, dynamicMinW);
262
+
263
+ // 获取该类型的最小尺寸限制
264
+ const { minW: effectiveMinW, minH: effectiveMinH } = getMinDimensions(shape, baseMinW);
265
+
266
+ // 判断是否为 diagram 组件
267
+ const isDiagramComponent = shape.shapeKey && config.diagram && config.diagram.includes(shape.shapeKey);
268
+
269
+ // 计算预览边界
270
+ let next = ghostResizeStep(
271
+ { x: base.x, y: base.y, width: base.width, height: base.height },
272
+ handle,
273
+ { x: startPos.value.x, y: startPos.value.y },
274
+ curr,
275
+ container,
276
+ // 只限制 left 和 top
277
+ { left: edges.left, top: edges.top, right: false, bottom: false },
278
+ effectiveMinW,
279
+ effectiveMinH
280
+ );
281
+
282
+ // diagram 组件保持高度不变
283
+ if (isDiagramComponent && shape.shapeType === 'shape') {
284
+ next = { ...next, height: base.height };
285
+ }
286
+
287
+ // 画布类型(diagram)不移动位置
288
+ const isDiagram = (shape as any).shapeType?.toLowerCase?.() === "diagram";
289
+ if (isDiagram) {
290
+ next = { ...next, x: base.x, y: base.y };
291
+ next.x = Math.max(0, next.x);
292
+ next.y = Math.max(0, next.y);
293
+ }
294
+
295
+ // 隔间组件有特殊的缩放约束
296
+ if (isCompartment(shape)) {
297
+ next = clampCompartmentResize(
298
+ graphStore.shapes,
299
+ shape,
300
+ next,
301
+ resizeDirection.value as "nw" | "ne" | "sw" | "se"
302
+ );
303
+ }
304
+
305
+ // 确保父元素缩放后子元素仍然可见(有间隙约束)
306
+ next = clampParentRectToChildrenGap(
307
+ graphStore.shapes,
308
+ shape,
309
+ next,
310
+ resizeDirection.value as any,
311
+ 0,
312
+ effectiveMinW,
313
+ minH,
314
+ groupGhost.value
315
+ );
316
+
317
+ // 更新预览边界
318
+ groupGhost.value = {
319
+ ...groupGhost.value,
320
+ [shape.id]: next,
321
+ };
322
+ };
323
+
324
+ /**
325
+ * 停止缩放
326
+ *
327
+ * 触发时机:用户松开鼠标(由 withDrag 调用)
328
+ *
329
+ * 执行步骤:
330
+ * 1. 获取最终边界(groupGhost 或起始边界)
331
+ * 2. 应用最小尺寸约束
332
+ * 3. 更新元素实际边界
333
+ * 4. 处理可能的重父子关系
334
+ * 5. 清理缩放状态
335
+ *
336
+ * @param e - 鼠标事件(未使用,但 withDrag 要求)
337
+ */
338
+ const stopResize = () => {
339
+ if (!isResizing.value || !resizingTarget.value) return;
340
+
341
+ const id = resizingTarget.value.id;
342
+ const shape = resizingTarget.value;
343
+ const final = groupGhost.value[id] ?? startBounds.value;
344
+
345
+ // 计算最小尺寸约束
346
+ const dynamicMinW = calculateTextMinWidth(shape);
347
+ const { minW: effectiveMinW, minH: minHeight } = getMinDimensions(shape, dynamicMinW, MinDimensionMode.STOP);
348
+
349
+ const isDiagramComponent = shape.shapeKey && config.diagram && config.diagram.includes(shape.shapeKey);
350
+
351
+ // 应用最小尺寸约束
352
+ let finalBounds = { ...final };
353
+ if (final.width < effectiveMinW) {
354
+ finalBounds.width = effectiveMinW;
355
+ }
356
+ if (!isDiagramComponent && final.height < minHeight) {
357
+ finalBounds.height = minHeight;
358
+ }
359
+ if (isDiagramComponent && shape.shapeType === 'shape') {
360
+ finalBounds = { ...finalBounds, height: startBounds.value.height };
361
+ }
362
+
363
+ // 检查边界是否有变化
364
+ const shapeBefore = graphStore.shapes.find((s) => s.id === id);
365
+ const changed = shapeBefore && JSON.stringify(shapeBefore.bounds) !== JSON.stringify(finalBounds);
366
+
367
+ if (changed) {
368
+ // 更新元素边界
369
+ callbacks.onShapeUpdate?.(id, { bounds: finalBounds });
370
+ graphStore.updateShape(id, { bounds: finalBounds });
371
+
372
+ // 同步更新 selectedShape
373
+ if (graphStore.selectedShape?.id === id && graphStore.selectedShape) {
374
+ graphStore.selectedShape.bounds = { ...finalBounds };
375
+ }
376
+
377
+ // 处理重父子关系
378
+ const containerForFinalize =
379
+ graphStore.hoverContainerId && graphStore.hoverNestable ? graphStore.hoverContainerId : null;
380
+
381
+ const didReparent = finalizeAfterTransform(
382
+ graphStore.shapes,
383
+ [id],
384
+ { [id]: finalBounds },
385
+ [id],
386
+ graphStore.currentDiagramId,
387
+ containerForFinalize,
388
+ graphStore.updateShape
389
+ );
390
+
391
+ // 如果有重父子,需要重新计算 z-order
392
+ if (didReparent) {
393
+ normalizeZOrder(
394
+ graphStore.shapes,
395
+ graphStore.currentDiagramId,
396
+ graphStore.updateShape,
397
+ 1,
398
+ 1
399
+ );
400
+ }
401
+
402
+ // 通知 store 缩放结束
403
+ graphStore.endResizeShape(id);
404
+ }
405
+
406
+ // 触发结束回调
407
+ callbacks.onResizeEnd?.(resizingTarget.value);
408
+ eventBus.emit('resize-end', { target: resizingTarget.value });
409
+
410
+ // 清理状态
411
+ isResizing.value = false;
412
+ resizeDirection.value = "";
413
+ resizingTarget.value = null;
414
+ groupBase.value = {};
415
+ groupGhost.value = {};
416
+ offDrag?.();
417
+ offDrag = null;
418
+ };
419
+
420
+ /**
421
+ * 取消缩放
422
+ *
423
+ * 用于外部强制取消缩放操作(如切换选中元素)
424
+ *
425
+ * 执行步骤:
426
+ * 1. 清理拖拽事件监听
427
+ * 2. 重置所有缩放状态
428
+ */
429
+ const cancelResize = () => {
430
+ if (offDrag) {
431
+ offDrag();
432
+ offDrag = null;
433
+ }
434
+ isResizing.value = false;
435
+ resizeDirection.value = "";
436
+ resizingTarget.value = null;
437
+ groupBase.value = {};
438
+ groupGhost.value = {};
439
+ };
440
+
441
+ /**
442
+ * 返回值说明:
443
+ * - 响应式状态:供外部组件读取缩放状态(如 isBusy 计算属性)
444
+ * - startResize:供模板绑定缩放手柄的 mousedown 事件
445
+ * - groupGhost:供 allGhosts 计算属性获取预览框数据
446
+ * - handleResize/stopResize/cancelResize:内部使用
447
+ * - getMinDimensions:供外部计算最小尺寸
448
+ */
449
+ return {
450
+ isResizing,
451
+ resizeDirection,
452
+ startPos,
453
+ startBounds,
454
+ resizingTarget,
455
+ groupBase,
456
+ groupGhost,
457
+ startResize,
458
+ handleResize,
459
+ stopResize,
460
+ cancelResize,
461
+ getMinDimensions
462
+ };
463
+ }