@mx-sose-front/mx-sose-graph 1.1.1 → 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.
- package/dist/index.d.ts +103 -10
- package/dist/index.esm.js +6618 -5593
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +7 -7
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/{ContextMenu.vue → ContextMenu/ContextMenu.vue} +243 -70
- package/src/components/DiagramListTooltip/DiagramListTooltip.vue +138 -0
- package/src/components/Edge/Edge.vue +38 -49
- package/src/components/InteractionLayer.vue +432 -838
- package/src/components/Shape/Block.vue +8 -8
- package/src/components/ZoomSlider/ZoomSlider.vue +229 -0
- package/src/constants/index.ts +12 -0
- package/src/statics/icons/childIcons//351/241/271/347/233/256/351/241/272/345/272/217@3x.png +0 -0
- package/src/store/graphStore.ts +98 -21
- package/src/types/index.ts +14 -1
- package/src/types/interactionLayer.ts +35 -0
- package/src/utils/contextMenuUtils.ts +264 -0
- package/src/utils/diagram.ts +93 -4
- package/src/utils/edgeUtils.ts +228 -0
- package/src/utils/geom.ts +34 -0
- package/src/utils/graphDragService.ts +14 -17
- package/src/utils/keyboardUtils.ts +229 -0
- package/src/utils/license-guard.ts +50 -0
- package/src/utils/nameEditUtils.ts +132 -0
- package/src/utils/resizeUtils.ts +463 -0
- package/src/view/graph.vue +102 -134
- package/src/components/Label.vue +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue';
|
|
2
|
+
import { eventBus } from '../store';
|
|
3
|
+
|
|
4
|
+
// 菜单位置接口
|
|
5
|
+
export interface MenuPosition {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 菜单项配置接口
|
|
11
|
+
export interface MenuItemConfig {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
divider?: boolean;
|
|
17
|
+
handler?: () => void;
|
|
18
|
+
submenu?: MenuItemConfig[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 菜单配置接口
|
|
22
|
+
export interface MenuConfig {
|
|
23
|
+
items: MenuItemConfig[];
|
|
24
|
+
width?: number;
|
|
25
|
+
itemHeight?: number;
|
|
26
|
+
padding?: number;
|
|
27
|
+
maxVisibleItems?: number;
|
|
28
|
+
safeMargin?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 默认菜单配置
|
|
32
|
+
const DEFAULT_MENU_CONFIG: Required<MenuConfig> = {
|
|
33
|
+
items: [],
|
|
34
|
+
width: 180,
|
|
35
|
+
itemHeight: 36,
|
|
36
|
+
padding: 6,
|
|
37
|
+
maxVisibleItems: 8,
|
|
38
|
+
safeMargin: 10
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// 右键菜单工具函数
|
|
42
|
+
export class ContextMenuUtils {
|
|
43
|
+
/**
|
|
44
|
+
* 处理右键菜单点击事件
|
|
45
|
+
* @param event 鼠标事件
|
|
46
|
+
* @param layerRef 图层引用
|
|
47
|
+
* @param pickTarget 命中测试函数
|
|
48
|
+
* @param shapes 图元列表
|
|
49
|
+
* @param selectShape 选中图元函数
|
|
50
|
+
* @param isDragging 是否正在拖拽
|
|
51
|
+
* @param isResizing 是否正在缩放
|
|
52
|
+
* @returns 命中的图元(如果有)
|
|
53
|
+
*/
|
|
54
|
+
static handleContextMenuClick(
|
|
55
|
+
event: MouseEvent,
|
|
56
|
+
layerRef: Ref<HTMLElement | null>,
|
|
57
|
+
pickTarget: (shapes: any[], point: { x: number; y: number }) => { kind: string; shape?: any },
|
|
58
|
+
shapes: any[],
|
|
59
|
+
selectShape?: (shape: any) => void,
|
|
60
|
+
isDragging?: boolean,
|
|
61
|
+
isResizing?: boolean
|
|
62
|
+
): any | null {
|
|
63
|
+
// 如果图元正在移动或缩放,则不显示菜单
|
|
64
|
+
if (isDragging || isResizing) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
|
|
70
|
+
// 获取点击位置
|
|
71
|
+
const point = ContextMenuUtils.toLocalPoint(event, layerRef.value);
|
|
72
|
+
|
|
73
|
+
// 使用命中测试找到点击的图元
|
|
74
|
+
const hit = pickTarget(shapes, point);
|
|
75
|
+
|
|
76
|
+
if (hit.kind === "shape" && hit.shape) {
|
|
77
|
+
// 选中图元
|
|
78
|
+
if (selectShape) {
|
|
79
|
+
selectShape(hit.shape);
|
|
80
|
+
}
|
|
81
|
+
return hit.shape;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 将鼠标事件坐标转换为本地坐标
|
|
89
|
+
*/
|
|
90
|
+
static toLocalPoint(event: MouseEvent, layerRef: HTMLElement | null): { x: number; y: number } {
|
|
91
|
+
if (!layerRef) {
|
|
92
|
+
return { x: event.clientX, y: event.clientY };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rect = layerRef.getBoundingClientRect();
|
|
96
|
+
return {
|
|
97
|
+
x: event.clientX - rect.left,
|
|
98
|
+
y: event.clientY - rect.top
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 检查点击是否在菜单外部
|
|
104
|
+
*/
|
|
105
|
+
static isClickOutsideMenu(event: MouseEvent, menuSelector: string = '.context-menu'): boolean {
|
|
106
|
+
const menuElement = document.querySelector(menuSelector);
|
|
107
|
+
return !menuElement || !menuElement.contains(event.target as Node);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 计算菜单位置(避免超出屏幕边界)
|
|
112
|
+
*/
|
|
113
|
+
static calculateMenuPosition(
|
|
114
|
+
x: number,
|
|
115
|
+
y: number,
|
|
116
|
+
menuWidth: number = 180,
|
|
117
|
+
menuHeight: number = 300,
|
|
118
|
+
safeMargin: number = 10
|
|
119
|
+
): { x: number; y: number } {
|
|
120
|
+
const viewportWidth = window.innerWidth;
|
|
121
|
+
const viewportHeight = window.innerHeight;
|
|
122
|
+
|
|
123
|
+
let left = x;
|
|
124
|
+
let top = y;
|
|
125
|
+
|
|
126
|
+
// 水平方向调整
|
|
127
|
+
if (left + menuWidth > viewportWidth) {
|
|
128
|
+
left = viewportWidth - menuWidth - safeMargin;
|
|
129
|
+
}
|
|
130
|
+
left = Math.max(safeMargin, left);
|
|
131
|
+
|
|
132
|
+
// 垂直方向调整
|
|
133
|
+
if (top + menuHeight > viewportHeight) {
|
|
134
|
+
top = viewportHeight - menuHeight - safeMargin;
|
|
135
|
+
}
|
|
136
|
+
top = Math.max(safeMargin, top);
|
|
137
|
+
|
|
138
|
+
return { x: left, y: top };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 处理删除操作
|
|
143
|
+
*/
|
|
144
|
+
static handleDelete(target: any) {
|
|
145
|
+
if (target) {
|
|
146
|
+
eventBus.emit('shapes-delete', target);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 处理属性配置 (暂时不用)
|
|
152
|
+
*/
|
|
153
|
+
static handleShowPropertyPanel(target: any) {
|
|
154
|
+
if (target) {
|
|
155
|
+
eventBus.emit('show-property-panel', target);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 处理树上高亮
|
|
161
|
+
*/
|
|
162
|
+
static handleHighlight(target: any) {
|
|
163
|
+
if (target) {
|
|
164
|
+
eventBus.emit('highlight-shape', target);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 处理所在图表
|
|
170
|
+
*/
|
|
171
|
+
static handleLocateChart(target: any) {
|
|
172
|
+
if (target) {
|
|
173
|
+
eventBus.emit('locate-chart', target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 处理复制
|
|
179
|
+
*/
|
|
180
|
+
static handleCopy(target: any) {
|
|
181
|
+
if (target) {
|
|
182
|
+
eventBus.emit('copy-shape', target);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 处理粘贴
|
|
188
|
+
*/
|
|
189
|
+
static handlePaste(target: any) {
|
|
190
|
+
if (target) {
|
|
191
|
+
eventBus.emit('paste-shape', target);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 处理剪切
|
|
197
|
+
*/
|
|
198
|
+
static handleCut(target: any) {
|
|
199
|
+
if (target) {
|
|
200
|
+
eventBus.emit('cut-shape', target);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 记录日志
|
|
206
|
+
*/
|
|
207
|
+
static logSelectedShape(target: any) {
|
|
208
|
+
console.log('当前选中的图元:', target);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 默认菜单配置
|
|
213
|
+
export const defaultMenuItems: MenuItemConfig[] = [
|
|
214
|
+
{
|
|
215
|
+
id: 'property',
|
|
216
|
+
label: '属性配置',
|
|
217
|
+
icon: 'config'
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'highlight',
|
|
221
|
+
label: '树上高亮',
|
|
222
|
+
icon: 'delete'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'locate-chart',
|
|
226
|
+
label: '所在图表',
|
|
227
|
+
icon: 'locateChart',
|
|
228
|
+
divider: true
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'new-chart',
|
|
232
|
+
label: '新建图表',
|
|
233
|
+
icon: 'diagram',
|
|
234
|
+
disabled: true
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'copy',
|
|
238
|
+
label: '复制',
|
|
239
|
+
icon: 'copy',
|
|
240
|
+
disabled: true
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'paste',
|
|
244
|
+
label: '粘贴',
|
|
245
|
+
icon: 'paste',
|
|
246
|
+
disabled: true
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'cut',
|
|
250
|
+
label: '剪切',
|
|
251
|
+
icon: 'scissors',
|
|
252
|
+
disabled: true
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: 'delete',
|
|
256
|
+
label: '删除',
|
|
257
|
+
icon: 'delete'
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
// 标准右键菜单配置
|
|
262
|
+
export const standardContextMenuConfig: MenuConfig = {
|
|
263
|
+
items: defaultMenuItems
|
|
264
|
+
};
|
package/src/utils/diagram.ts
CHANGED
|
@@ -218,17 +218,27 @@ const ensureMinimumCanvasSize = () => {
|
|
|
218
218
|
* 计算操作按钮的位置样式
|
|
219
219
|
*/
|
|
220
220
|
export const actionButtonsStyle = (shape: Shape): Record<string, string> => {
|
|
221
|
+
// 前置校验:shapeId不存在/元素已销毁,直接返回空样式
|
|
222
|
+
if (!shape.scenarioMenus) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
|
|
221
226
|
const shapeBounds = shape.bounds ?? {};
|
|
222
227
|
const shapeWidth = shapeBounds.width ?? 100;
|
|
223
228
|
const shapeHeight = shapeBounds.height ?? 40;
|
|
224
|
-
if (!shape.scenarioMenus) {
|
|
225
|
-
return {}
|
|
226
|
-
}
|
|
227
229
|
// 按钮宽度:28px,间距4px,共5个按钮 (根据接口动态替换)
|
|
228
230
|
const hasTypeButton = !!shape.modelTypePropertyId;
|
|
229
231
|
const totalButtons = (shape.scenarioMenus ? shape.scenarioMenus!.length : 0) + (hasTypeButton ? 1 : 0);
|
|
230
232
|
const buttonsHeight = 28 * totalButtons + 4 * (totalButtons - (totalButtons > 0 ? 1 : 0));
|
|
231
|
-
|
|
233
|
+
const shapeTop = shape.bounds?.y ?? 0;
|
|
234
|
+
if (shapeTop < 40) { // 固定阈值,不受按钮DOM样式修改影响
|
|
235
|
+
return {
|
|
236
|
+
position: "absolute",
|
|
237
|
+
left: `${shapeWidth + 15}px`, // 紧贴形状右侧,间距5px
|
|
238
|
+
top: `10px`, // 顶部对齐
|
|
239
|
+
zIndex: "1000",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
232
242
|
return {
|
|
233
243
|
position: "absolute",
|
|
234
244
|
left: `${shapeWidth + 15}px`, // 紧贴形状右侧,间距5px
|
|
@@ -341,6 +351,26 @@ export const nameTextBoxStyle = (shape: Shape): Record<string, string> => {
|
|
|
341
351
|
};
|
|
342
352
|
};
|
|
343
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Pin 名称编辑输入框样式(放大,随字体自适应),非 Pin 返回空由 CSS 控制
|
|
356
|
+
*/
|
|
357
|
+
export const nameInputStyle = (shape: Shape): Record<string, string> => {
|
|
358
|
+
if (!shape) return {};
|
|
359
|
+
const isPin = String(shape?.shapeType || '').toLowerCase() === 'pin';
|
|
360
|
+
const ns = shape?.nameStyle || {};
|
|
361
|
+
const nb = shape?.nameBounds || {};
|
|
362
|
+
const fs = Number(ns.fontSize || nb.height || 12);
|
|
363
|
+
if (!isPin) return {};
|
|
364
|
+
return {
|
|
365
|
+
width: '100%',
|
|
366
|
+
height: '100%',
|
|
367
|
+
fontSize: `${fs}px`,
|
|
368
|
+
lineHeight: `${Math.ceil(fs + 8)}px`,
|
|
369
|
+
padding: '4px 6px',
|
|
370
|
+
boxSizing: 'border-box' as const,
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
|
|
344
374
|
/**
|
|
345
375
|
* 名称编辑容器样式 - 确保在父组件内水平居中
|
|
346
376
|
*/
|
|
@@ -401,3 +431,62 @@ export const nameEditorContainerStyle = (shape: Shape): Record<string, string> =
|
|
|
401
431
|
zIndex: "1001",
|
|
402
432
|
};
|
|
403
433
|
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* 计算文本所需最小宽度 - 应用于所有图形类型
|
|
437
|
+
*/
|
|
438
|
+
export const calculateTextMinWidth = (shape: Shape): number => {
|
|
439
|
+
const minW = 50;
|
|
440
|
+
// 获取字体大小(与Block.vue中逻辑一致)
|
|
441
|
+
const nameStyle = shape.nameStyle || {};
|
|
442
|
+
const nameBounds = shape.nameBounds || {};
|
|
443
|
+
const fontSize = nameStyle.fontSize || nameBounds.height || 12;
|
|
444
|
+
// 假设每个字符的平均宽度是字体大小的1倍
|
|
445
|
+
const charWidth = fontSize * 1;
|
|
446
|
+
|
|
447
|
+
// 计算名称文本的宽度
|
|
448
|
+
const nameTextWidth = (shape.name?.length || 0) * charWidth;
|
|
449
|
+
// 计算关键词文本的宽度
|
|
450
|
+
const keywordsTextWidth = (shape.keywords?.length || 0) * charWidth;
|
|
451
|
+
|
|
452
|
+
// 返回较大的宽度,并添加一些边距(左右各10px)
|
|
453
|
+
const maxTextWidth = Math.max(nameTextWidth, keywordsTextWidth);
|
|
454
|
+
const estimatedTextWidth = maxTextWidth + 60; // 左右各10px边距
|
|
455
|
+
|
|
456
|
+
// 确保最小宽度不小于minW
|
|
457
|
+
return Math.max(minW, estimatedTextWidth);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 框选矩形的样式
|
|
462
|
+
*/
|
|
463
|
+
export const getMarqueeStyle = (r: { x: number; y: number; width: number; height: number }): Record<string, string> => ({
|
|
464
|
+
position: "absolute",
|
|
465
|
+
left: `${r.x}px`,
|
|
466
|
+
top: `${r.y}px`,
|
|
467
|
+
width: `${r.width}px`,
|
|
468
|
+
height: `${r.height}px`,
|
|
469
|
+
border: "2px dashed #a6ddff",
|
|
470
|
+
pointerEvents: "none",
|
|
471
|
+
zIndex: "20",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 计算交互层的定位样式
|
|
476
|
+
* @param diagramBounds 画布边界
|
|
477
|
+
* @returns CSS样式对象
|
|
478
|
+
*/
|
|
479
|
+
export const getLayerStyle = (diagramBounds: { x?: number; y?: number; width?: number; height?: number } | undefined): Record<string, string> => {
|
|
480
|
+
if (!diagramBounds) {
|
|
481
|
+
return { display: 'none' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
position: 'absolute',
|
|
486
|
+
left: `${diagramBounds.x ?? 0}px`,
|
|
487
|
+
top: `${diagramBounds.y ?? 0}px`,
|
|
488
|
+
width: `${diagramBounds.width ?? 0}px`,
|
|
489
|
+
height: `${diagramBounds.height ?? 0}px`,
|
|
490
|
+
pointerEvents: 'none',
|
|
491
|
+
};
|
|
492
|
+
};
|
package/src/utils/edgeUtils.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Shape } from "../types";
|
|
3
3
|
import { pickTarget } from "./hittest";
|
|
4
4
|
import { useGraphStore } from '../store/graphStore';
|
|
5
|
+
import { snapPinToParentEdge } from './pinUtils';
|
|
5
6
|
|
|
6
7
|
export class EdgeUtils {
|
|
7
8
|
/**
|
|
@@ -597,6 +598,40 @@ export class EdgeUtils {
|
|
|
597
598
|
};
|
|
598
599
|
}
|
|
599
600
|
|
|
601
|
+
/**
|
|
602
|
+
* 取消连线的方法(完全重置所有相关状态)
|
|
603
|
+
*/
|
|
604
|
+
static cancelConnection(
|
|
605
|
+
state: {
|
|
606
|
+
isConnecting: { value: boolean };
|
|
607
|
+
currentConnectPoint: { value: { x: number; y: number } };
|
|
608
|
+
mousePosition: { value: { x: number; y: number } };
|
|
609
|
+
targetConnectPoint: { value: { x: number; y: number } };
|
|
610
|
+
targetShape: { value: any };
|
|
611
|
+
showLine: { value: boolean };
|
|
612
|
+
},
|
|
613
|
+
highlightUtils: {
|
|
614
|
+
highlightShape: (shape: any, highlight: boolean) => void;
|
|
615
|
+
clearHighlightTimeout: () => void;
|
|
616
|
+
}
|
|
617
|
+
) {
|
|
618
|
+
// 1. 先停止连线状态,避免后续计算触发
|
|
619
|
+
state.isConnecting.value = false;
|
|
620
|
+
|
|
621
|
+
// 2. 重置关键坐标状态(核心:清空无效坐标,让 linePoints 无值可算)
|
|
622
|
+
state.currentConnectPoint.value = { x: 0, y: 0 }; // 重置为无效默认值
|
|
623
|
+
state.mousePosition.value = { x: 0, y: 0 }; // 重置鼠标位置
|
|
624
|
+
state.targetConnectPoint.value = { x: 0, y: 0 }; // 重置目标连接点
|
|
625
|
+
state.targetShape.value = null; // 清空目标图元
|
|
626
|
+
|
|
627
|
+
// 3. 清除高亮状态和定时器
|
|
628
|
+
highlightUtils.highlightShape(null, false); // 取消图元高亮,恢复原始样式
|
|
629
|
+
highlightUtils.clearHighlightTimeout();
|
|
630
|
+
|
|
631
|
+
// 4. 最后隐藏线条(确保状态重置后再隐藏,避免延迟渲染)
|
|
632
|
+
state.showLine.value = false;
|
|
633
|
+
}
|
|
634
|
+
|
|
600
635
|
static isEndPointInShape(shapes: Shape[], endPoint: { x: number; y: number }) {
|
|
601
636
|
for (const shape of shapes) {
|
|
602
637
|
// 排除 diagram 类型的图形
|
|
@@ -773,4 +808,197 @@ export class EdgeUtils {
|
|
|
773
808
|
}
|
|
774
809
|
});
|
|
775
810
|
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* 根据 pin 的方向和位置计算连接点
|
|
814
|
+
* 连接点位于 pin 的外侧边缘(远离父图元的一侧):
|
|
815
|
+
* - OutputPin: direction 表示箭头朝外的方向,连接点在该方向的外侧边缘
|
|
816
|
+
* - InputPin: direction 表示箭头朝内的方向,连接点在相反方向的外侧边缘
|
|
817
|
+
*
|
|
818
|
+
* 例如:
|
|
819
|
+
* - InputPin direction='down' (箭头朝下,pin 在父图元上方) -> 连接点在 pin 的上边缘(y)
|
|
820
|
+
* - InputPin direction='up' (箭头朝上,pin 在父图元下方) -> 连接点在 pin 的下边缘(y + height)
|
|
821
|
+
* - InputPin direction='right' (箭头朝右,pin 在父图元左边) -> 连接点在 pin 的左边缘(x)
|
|
822
|
+
* - InputPin direction='left' (箭头朝左,pin 在父图元右边) -> 连接点在 pin 的右边缘(x + width)
|
|
823
|
+
*
|
|
824
|
+
* @param pinShape pin 图元对象
|
|
825
|
+
* @returns 连接点坐标 { x, y }
|
|
826
|
+
*/
|
|
827
|
+
static calculatePinConnectionPoint(pinShape: Shape): { x: number; y: number } {
|
|
828
|
+
if (!pinShape.bounds) {
|
|
829
|
+
return { x: 0, y: 0 };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const { x = 0, y = 0, width = 0, height = 0 } = pinShape.bounds;
|
|
833
|
+
const direction = pinShape.direction || 'right';
|
|
834
|
+
const isInputPin = (pinShape.shapeKey as string)?.toLowerCase().includes('input');
|
|
835
|
+
|
|
836
|
+
// 计算 pin 的中心点(用于水平和垂直方向的坐标)
|
|
837
|
+
const centerX = x + width / 2;
|
|
838
|
+
const centerY = y + height / 2;
|
|
839
|
+
|
|
840
|
+
// 对于 InputPin,direction 表示箭头朝内的方向,连接点应该在相反方向的外侧边缘
|
|
841
|
+
if (isInputPin) {
|
|
842
|
+
switch (direction) {
|
|
843
|
+
case 'up':
|
|
844
|
+
// InputPin: 箭头朝上(朝内),pin 在父图元下方,连接点在 pin 的下边缘(外侧)
|
|
845
|
+
return {
|
|
846
|
+
x: centerX,
|
|
847
|
+
y: y + height
|
|
848
|
+
};
|
|
849
|
+
case 'down':
|
|
850
|
+
// InputPin: 箭头朝下(朝内),pin 在父图元上方,连接点在 pin 的上边缘(外侧)
|
|
851
|
+
return {
|
|
852
|
+
x: centerX,
|
|
853
|
+
y: y
|
|
854
|
+
};
|
|
855
|
+
case 'left':
|
|
856
|
+
// InputPin: 箭头朝左(朝内),pin 在父图元右边,连接点在 pin 的右边缘(外侧)
|
|
857
|
+
return {
|
|
858
|
+
x: x + width,
|
|
859
|
+
y: centerY
|
|
860
|
+
};
|
|
861
|
+
case 'right':
|
|
862
|
+
// InputPin: 箭头朝右(朝内),pin 在父图元左边,连接点在 pin 的左边缘(外侧)
|
|
863
|
+
return {
|
|
864
|
+
x: x,
|
|
865
|
+
y: centerY
|
|
866
|
+
};
|
|
867
|
+
default:
|
|
868
|
+
return { x: centerX, y: centerY };
|
|
869
|
+
}
|
|
870
|
+
} else {
|
|
871
|
+
// OutputPin: direction 表示箭头朝外的方向,连接点在该方向的外侧边缘
|
|
872
|
+
switch (direction) {
|
|
873
|
+
case 'up':
|
|
874
|
+
// OutputPin: 箭头朝上,pin 在父图元上方,连接点在 pin 的上边缘中点
|
|
875
|
+
return {
|
|
876
|
+
x: centerX,
|
|
877
|
+
y: y
|
|
878
|
+
};
|
|
879
|
+
case 'down':
|
|
880
|
+
// OutputPin: 箭头朝下,pin 在父图元下方,连接点在 pin 的下边缘中点
|
|
881
|
+
return {
|
|
882
|
+
x: centerX,
|
|
883
|
+
y: y + height
|
|
884
|
+
};
|
|
885
|
+
case 'left':
|
|
886
|
+
// OutputPin: 箭头朝左,pin 在父图元左边,连接点在 pin 的左边缘中点
|
|
887
|
+
return {
|
|
888
|
+
x: x,
|
|
889
|
+
y: centerY
|
|
890
|
+
};
|
|
891
|
+
case 'right':
|
|
892
|
+
// OutputPin: 箭头朝右,pin 在父图元右边,连接点在 pin 的右边缘中点
|
|
893
|
+
return {
|
|
894
|
+
x: x + width,
|
|
895
|
+
y: centerY
|
|
896
|
+
};
|
|
897
|
+
default:
|
|
898
|
+
return { x: centerX, y: centerY };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* 处理 ServiceObjectFlow 连接:计算 OutputPin 和 InputPin 的 bounds 和连接点
|
|
905
|
+
* @param sourceShape 源图元
|
|
906
|
+
* @param targetShape 目标图元
|
|
907
|
+
* @param connectionData 连接数据
|
|
908
|
+
* @returns 处理后的连接数据和 pin 的 bounds
|
|
909
|
+
*/
|
|
910
|
+
static handleServiceObjectFlowConnection(
|
|
911
|
+
sourceShape: Shape,
|
|
912
|
+
targetShape: Shape,
|
|
913
|
+
connectionData: {
|
|
914
|
+
sourceId?: string;
|
|
915
|
+
targetId?: string;
|
|
916
|
+
sourcePoint: { x: number; y: number };
|
|
917
|
+
targetPoint: { x: number; y: number };
|
|
918
|
+
waypoints?: Array<{ x: number; y: number }>;
|
|
919
|
+
[key: string]: any;
|
|
920
|
+
}
|
|
921
|
+
): {
|
|
922
|
+
connectionData: typeof connectionData;
|
|
923
|
+
outputPinBounds: { x: number; y: number; width: number; height: number; direction?: string };
|
|
924
|
+
inputPinBounds: { x: number; y: number; width: number; height: number; direction?: string };
|
|
925
|
+
} {
|
|
926
|
+
// 创建临时 OutputPin 对象用于计算 bounds(只包含必要属性)
|
|
927
|
+
const outputPin: Shape = {
|
|
928
|
+
shapeKey: 'OutputPin',
|
|
929
|
+
shapeType: 'pin',
|
|
930
|
+
bounds: {
|
|
931
|
+
x: connectionData.sourcePoint.x,
|
|
932
|
+
y: connectionData.sourcePoint.y,
|
|
933
|
+
width: 22,
|
|
934
|
+
height: 22,
|
|
935
|
+
},
|
|
936
|
+
} as Shape;
|
|
937
|
+
|
|
938
|
+
// 使用 snapPinToParentEdge 调整 outputPin 的位置
|
|
939
|
+
const adjustedOutputPinPos = snapPinToParentEdge(
|
|
940
|
+
connectionData.sourcePoint,
|
|
941
|
+
sourceShape,
|
|
942
|
+
outputPin
|
|
943
|
+
);
|
|
944
|
+
outputPin.bounds = {
|
|
945
|
+
x: adjustedOutputPinPos.x,
|
|
946
|
+
y: adjustedOutputPinPos.y,
|
|
947
|
+
width: 22,
|
|
948
|
+
height: 22,
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// 创建临时 InputPin 对象用于计算 bounds(只包含必要属性)
|
|
952
|
+
const inputPin: Shape = {
|
|
953
|
+
shapeKey: 'InputPin',
|
|
954
|
+
shapeType: 'pin',
|
|
955
|
+
bounds: {
|
|
956
|
+
x: connectionData.targetPoint.x,
|
|
957
|
+
y: connectionData.targetPoint.y,
|
|
958
|
+
width: 22,
|
|
959
|
+
height: 22,
|
|
960
|
+
},
|
|
961
|
+
} as Shape;
|
|
962
|
+
|
|
963
|
+
// 使用 snapPinToParentEdge 调整 inputPin 的位置
|
|
964
|
+
const adjustedInputPinPos = snapPinToParentEdge(
|
|
965
|
+
connectionData.targetPoint,
|
|
966
|
+
targetShape,
|
|
967
|
+
inputPin
|
|
968
|
+
);
|
|
969
|
+
inputPin.bounds = {
|
|
970
|
+
x: adjustedInputPinPos.x,
|
|
971
|
+
y: adjustedInputPinPos.y,
|
|
972
|
+
width: 22,
|
|
973
|
+
height: 22,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
// 根据 pin 的方向计算连接点位置
|
|
977
|
+
const outputPinConnectionPoint = EdgeUtils.calculatePinConnectionPoint(outputPin);
|
|
978
|
+
const inputPinConnectionPoint = EdgeUtils.calculatePinConnectionPoint(inputPin);
|
|
979
|
+
|
|
980
|
+
// 修改 connectionData 的连接点
|
|
981
|
+
connectionData.sourcePoint = outputPinConnectionPoint;
|
|
982
|
+
connectionData.targetPoint = inputPinConnectionPoint;
|
|
983
|
+
// 更新 waypoints
|
|
984
|
+
connectionData.waypoints = [outputPinConnectionPoint, inputPinConnectionPoint];
|
|
985
|
+
|
|
986
|
+
return {
|
|
987
|
+
connectionData,
|
|
988
|
+
outputPinBounds: {
|
|
989
|
+
x: outputPin.bounds.x ?? 0,
|
|
990
|
+
y: outputPin.bounds.y ?? 0,
|
|
991
|
+
width: outputPin.bounds.width ?? 22,
|
|
992
|
+
height: outputPin.bounds.height ?? 22,
|
|
993
|
+
direction: outputPin.direction,
|
|
994
|
+
},
|
|
995
|
+
inputPinBounds: {
|
|
996
|
+
x: inputPin.bounds.x ?? 0,
|
|
997
|
+
y: inputPin.bounds.y ?? 0,
|
|
998
|
+
width: inputPin.bounds.width ?? 22,
|
|
999
|
+
height: inputPin.bounds.height ?? 22,
|
|
1000
|
+
direction: inputPin.direction,
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
776
1004
|
}
|
package/src/utils/geom.ts
CHANGED
|
@@ -19,6 +19,40 @@ export const toLocalPoint = (evt: MouseEvent, el: HTMLElement | null) => {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// 客户端坐标转换到容器本地坐标(带scale处理)
|
|
23
|
+
export const clientToLocalPoint = (clientX: number, clientY: number, el: HTMLElement | null) => {
|
|
24
|
+
if (!el) return { x: clientX, y: clientY } // 若还没挂载,直接用屏幕坐标兜底
|
|
25
|
+
const rect = el.getBoundingClientRect()
|
|
26
|
+
// 获取当前缩放比例
|
|
27
|
+
const graphStore = useGraphStore()
|
|
28
|
+
const scale = graphStore.getScale() || 1
|
|
29
|
+
// 计算本地坐标并除以缩放比例
|
|
30
|
+
return { x: (clientX - rect.left) / scale, y: (clientY - rect.top) / scale }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 本地坐标转换到客户端坐标(带scale处理)
|
|
34
|
+
export const localToClientPoint = (x: number, y: number, el: HTMLElement | null) => {
|
|
35
|
+
if (!el) return { clientX: x, clientY: y } // 若还没挂载,直接用屏幕坐标兜底
|
|
36
|
+
const rect = el.getBoundingClientRect()
|
|
37
|
+
// 获取当前缩放比例
|
|
38
|
+
const graphStore = useGraphStore()
|
|
39
|
+
const scale = graphStore.getScale() || 1
|
|
40
|
+
// 计算客户端坐标并乘以缩放比例
|
|
41
|
+
return { clientX: rect.left + x * scale, clientY: rect.top + y * scale }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 判断点是否在容器范围内(客户端坐标)
|
|
45
|
+
export const isInsideCanvasClient = (clientX: number, clientY: number, el: HTMLElement | null) => {
|
|
46
|
+
if (!el) return false
|
|
47
|
+
const rect = el.getBoundingClientRect()
|
|
48
|
+
return (
|
|
49
|
+
clientX >= rect.left &&
|
|
50
|
+
clientX <= rect.right &&
|
|
51
|
+
clientY >= rect.top &&
|
|
52
|
+
clientY <= rect.bottom
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
22
56
|
// 点是否位于矩形边界内
|
|
23
57
|
export const inBounds = (
|
|
24
58
|
pt: { x: number; y: number },
|