@mx-sose-front/mx-sose-graph 1.2.9 → 1.3.1
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.esm.js +94373 -1365
- 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/src/components/InteractionLayer.vue.d.ts.map +1 -1
- package/dist/src/components/Matrix/Matrix.vue.d.ts.map +1 -1
- package/dist/src/hooks/useConnectionPreview.d.ts +500 -0
- package/dist/src/hooks/useConnectionPreview.d.ts.map +1 -0
- package/dist/src/store/graphStore.d.ts +5 -1
- package/dist/src/store/graphStore.d.ts.map +1 -1
- package/dist/src/utils/autoLayout.d.ts +44 -0
- package/dist/src/utils/autoLayout.d.ts.map +1 -0
- package/dist/src/utils/pinUtils.d.ts +2 -0
- package/dist/src/utils/pinUtils.d.ts.map +1 -1
- package/dist/src/utils/shapeInitialBounds.d.ts +10 -0
- package/dist/src/utils/shapeInitialBounds.d.ts.map +1 -0
- package/dist/src/view/graph.vue.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +2 -1
- package/src/components/InteractionLayer.vue +34 -406
- package/src/components/Matrix/Matrix.vue +71 -5
- package/src/hooks/useConnectionPreview.ts +405 -0
- package/src/store/graphStore.ts +24 -0
- package/src/utils/autoLayout.ts +388 -0
- package/src/utils/pinUtils.ts +5 -0
- package/src/utils/shapeInitialBounds.ts +42 -0
- package/src/view/graph.vue +2 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import ELK, { type ElkNode, type ElkExtendedEdge } from 'elkjs/lib/elk.bundled.js'
|
|
2
|
+
import type { Shape, Bounds } from '../types'
|
|
3
|
+
import { getShapeInitialDimensions } from './shapeInitialBounds'
|
|
4
|
+
import { attachPinToBlock, isPortShape } from './pinUtils'
|
|
5
|
+
|
|
6
|
+
export type LayoutDirection = 'horizontal' | 'vertical'
|
|
7
|
+
|
|
8
|
+
/** ELK 布局算法:layered 分层布局;mrtree 多根树布局 */
|
|
9
|
+
export type LayoutAlgorithm = 'layered' | 'mrtree'
|
|
10
|
+
|
|
11
|
+
export interface AutoLayoutOptions {
|
|
12
|
+
/** ELK 布局算法,默认 layered */
|
|
13
|
+
algorithm?: LayoutAlgorithm
|
|
14
|
+
direction?: LayoutDirection
|
|
15
|
+
/** 同层节点间距(像素) */
|
|
16
|
+
nodeSpacing?: number
|
|
17
|
+
/** 层间距(像素) */
|
|
18
|
+
layerSpacing?: number
|
|
19
|
+
/** 布局结果左上角偏移 */
|
|
20
|
+
padding?: number
|
|
21
|
+
/** 子图元在父容器内的内边距 */
|
|
22
|
+
childPadding?: number
|
|
23
|
+
/** 子图元之间的间距 */
|
|
24
|
+
childSpacing?: number
|
|
25
|
+
/** 每行最多子图元数量 */
|
|
26
|
+
maxChildrenPerRow?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LayoutResult {
|
|
30
|
+
/** shapeId → 新的 bounds(x/y 为布局结果;width/height 重置为各图元类型的默认尺寸) */
|
|
31
|
+
nodeUpdates: Map<string, Bounds>
|
|
32
|
+
/** 所有参与布局的节点 ID(用于批量刷新连线) */
|
|
33
|
+
affectedNodeIds: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_NODE_WIDTH = 100
|
|
37
|
+
const DEFAULT_NODE_HEIGHT = 50
|
|
38
|
+
|
|
39
|
+
/** 布局用端点:端口映射到父图元,普通图元保持自身 id */
|
|
40
|
+
function resolveLayoutEndpoint(
|
|
41
|
+
shapeId: string,
|
|
42
|
+
shapeById: Map<string, Shape>
|
|
43
|
+
): { layoutId: string; portId?: string } {
|
|
44
|
+
const shape = shapeById.get(shapeId)
|
|
45
|
+
if (!shape) return { layoutId: shapeId }
|
|
46
|
+
if (shape.shapeType === 'pin' && isPortShape(shape) && shape.parenShapeId) {
|
|
47
|
+
return { layoutId: shape.parenShapeId, portId: shape.id }
|
|
48
|
+
}
|
|
49
|
+
return { layoutId: shapeId }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLayoutBounds(
|
|
53
|
+
shapeId: string,
|
|
54
|
+
nodeUpdates: Map<string, Bounds>,
|
|
55
|
+
shapeById: Map<string, Shape>
|
|
56
|
+
): Bounds {
|
|
57
|
+
const updated = nodeUpdates.get(shapeId)
|
|
58
|
+
if (updated) return updated
|
|
59
|
+
const shape = shapeById.get(shapeId)
|
|
60
|
+
const b = shape?.bounds
|
|
61
|
+
return {
|
|
62
|
+
x: b?.x ?? 0,
|
|
63
|
+
y: b?.y ?? 0,
|
|
64
|
+
width: b?.width ?? DEFAULT_NODE_WIDTH,
|
|
65
|
+
height: b?.height ?? DEFAULT_NODE_HEIGHT,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function blockCenter(bounds: Bounds): { x: number; y: number } {
|
|
70
|
+
const x = bounds.x ?? 0
|
|
71
|
+
const y = bounds.y ?? 0
|
|
72
|
+
const w = bounds.width ?? DEFAULT_NODE_WIDTH
|
|
73
|
+
const h = bounds.height ?? DEFAULT_NODE_HEIGHT
|
|
74
|
+
return { x: x + w / 2, y: y + h / 2 }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface EdgePortLayoutInfo {
|
|
78
|
+
sourcePortId?: string
|
|
79
|
+
targetPortId?: string
|
|
80
|
+
layoutSourceId: string
|
|
81
|
+
layoutTargetId: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 布局完成后,将端口吸附到父图元朝向对端的边上 */
|
|
85
|
+
function positionPortsOnEdges(
|
|
86
|
+
edgePortInfos: EdgePortLayoutInfo[],
|
|
87
|
+
shapes: Shape[],
|
|
88
|
+
nodeUpdates: Map<string, Bounds>,
|
|
89
|
+
affectedNodeIds: string[]
|
|
90
|
+
): void {
|
|
91
|
+
const shapeById = new Map(shapes.map(s => [s.id, s]))
|
|
92
|
+
const affectedSet = new Set(affectedNodeIds)
|
|
93
|
+
|
|
94
|
+
for (const info of edgePortInfos) {
|
|
95
|
+
const otherSourceBounds = getLayoutBounds(info.layoutSourceId, nodeUpdates, shapeById)
|
|
96
|
+
const otherTargetBounds = getLayoutBounds(info.layoutTargetId, nodeUpdates, shapeById)
|
|
97
|
+
|
|
98
|
+
if (info.sourcePortId) {
|
|
99
|
+
const port = shapeById.get(info.sourcePortId)
|
|
100
|
+
const parentId = port?.parenShapeId
|
|
101
|
+
const parent = parentId ? shapeById.get(parentId) : undefined
|
|
102
|
+
if (port && parent) {
|
|
103
|
+
const parentWithBounds: Shape = {
|
|
104
|
+
...parent,
|
|
105
|
+
bounds: getLayoutBounds(parent.id, nodeUpdates, shapeById),
|
|
106
|
+
}
|
|
107
|
+
const portBounds = attachPinToBlock(port, parentWithBounds, blockCenter(otherTargetBounds))
|
|
108
|
+
nodeUpdates.set(port.id, portBounds)
|
|
109
|
+
if (!affectedSet.has(port.id)) {
|
|
110
|
+
affectedNodeIds.push(port.id)
|
|
111
|
+
affectedSet.add(port.id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (info.targetPortId) {
|
|
117
|
+
const port = shapeById.get(info.targetPortId)
|
|
118
|
+
const parentId = port?.parenShapeId
|
|
119
|
+
const parent = parentId ? shapeById.get(parentId) : undefined
|
|
120
|
+
if (port && parent) {
|
|
121
|
+
const parentWithBounds: Shape = {
|
|
122
|
+
...parent,
|
|
123
|
+
bounds: getLayoutBounds(parent.id, nodeUpdates, shapeById),
|
|
124
|
+
}
|
|
125
|
+
const portBounds = attachPinToBlock(port, parentWithBounds, blockCenter(otherSourceBounds))
|
|
126
|
+
nodeUpdates.set(port.id, portBounds)
|
|
127
|
+
if (!affectedSet.has(port.id)) {
|
|
128
|
+
affectedNodeIds.push(port.id)
|
|
129
|
+
affectedSet.add(port.id)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 根据子图元排列计算父容器所需尺寸 */
|
|
137
|
+
function computeParentSize(
|
|
138
|
+
children: Shape[],
|
|
139
|
+
maxPerRow: number,
|
|
140
|
+
childSpacing: number,
|
|
141
|
+
childPadding: number,
|
|
142
|
+
headerHeight: number,
|
|
143
|
+
): { width: number; height: number } {
|
|
144
|
+
if (!children.length) return { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT }
|
|
145
|
+
|
|
146
|
+
const childDims = children.map(c => getShapeInitialDimensions(c))
|
|
147
|
+
const rows: { width: number; height: number }[][] = []
|
|
148
|
+
for (let i = 0; i < childDims.length; i += maxPerRow) {
|
|
149
|
+
rows.push(childDims.slice(i, i + maxPerRow))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let totalWidth = 0
|
|
153
|
+
let totalHeight = 0
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const rowW = row.reduce((sum, d) => sum + d.width, 0) + (row.length - 1) * childSpacing
|
|
156
|
+
const rowH = Math.max(...row.map(d => d.height))
|
|
157
|
+
totalWidth = Math.max(totalWidth, rowW)
|
|
158
|
+
totalHeight += rowH
|
|
159
|
+
}
|
|
160
|
+
totalHeight += (rows.length - 1) * childSpacing
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
width: totalWidth + childPadding * 2,
|
|
164
|
+
height: totalHeight + childPadding * 2 + headerHeight,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 在父容器内将子图元横向排列(每行最多 maxPerRow 个) */
|
|
169
|
+
function arrangeChildrenInParent(
|
|
170
|
+
parentBounds: Bounds,
|
|
171
|
+
children: Shape[],
|
|
172
|
+
maxPerRow: number,
|
|
173
|
+
childSpacing: number,
|
|
174
|
+
childPadding: number,
|
|
175
|
+
headerHeight: number,
|
|
176
|
+
): Map<string, Bounds> {
|
|
177
|
+
const result = new Map<string, Bounds>()
|
|
178
|
+
if (!children.length) return result
|
|
179
|
+
|
|
180
|
+
const childDims = children.map(c => getShapeInitialDimensions(c))
|
|
181
|
+
const startX = (parentBounds.x ?? 0) + childPadding
|
|
182
|
+
const startY = (parentBounds.y ?? 0) + childPadding + headerHeight
|
|
183
|
+
|
|
184
|
+
let curY = startY
|
|
185
|
+
for (let i = 0; i < children.length; i += maxPerRow) {
|
|
186
|
+
const rowChildren = children.slice(i, i + maxPerRow)
|
|
187
|
+
const rowDims = childDims.slice(i, i + maxPerRow)
|
|
188
|
+
const rowH = Math.max(...rowDims.map(d => d.height))
|
|
189
|
+
|
|
190
|
+
let curX = startX
|
|
191
|
+
for (let j = 0; j < rowChildren.length; j++) {
|
|
192
|
+
result.set(rowChildren[j].id, {
|
|
193
|
+
x: curX,
|
|
194
|
+
y: curY,
|
|
195
|
+
width: rowDims[j].width,
|
|
196
|
+
height: rowDims[j].height,
|
|
197
|
+
})
|
|
198
|
+
curX += rowDims[j].width + childSpacing
|
|
199
|
+
}
|
|
200
|
+
curY += rowH + childSpacing
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 基于 ELK 的自动布局
|
|
208
|
+
*
|
|
209
|
+
* 从 shapes 中分离出节点与边(排除 edge、diagram、pin/端口),交由 ELK 计算布局(layered / mrtree),
|
|
210
|
+
* 返回每个节点的新坐标。
|
|
211
|
+
*
|
|
212
|
+
* 端口不参与节点布局:构建 ELK 边时将连到端口的 source/target 换为其父图元 id;
|
|
213
|
+
* 布局完成后边的 sourceId/targetId 仍指向端口,再按对端父图元中心用 attachPinToBlock 吸附端口。
|
|
214
|
+
*
|
|
215
|
+
* 对于有嵌套子图元的父图元:
|
|
216
|
+
* 1. 先根据子图元数量计算父图元所需宽高
|
|
217
|
+
* 2. 以计算后的尺寸参与 ELK 布局
|
|
218
|
+
* 3. 布局完成后将子图元在父内横向排列(每行最多 maxChildrenPerRow 个)
|
|
219
|
+
*/
|
|
220
|
+
export async function autoLayout(
|
|
221
|
+
shapes: Shape[],
|
|
222
|
+
options: AutoLayoutOptions = {}
|
|
223
|
+
): Promise<LayoutResult> {
|
|
224
|
+
const {
|
|
225
|
+
algorithm = 'layered',
|
|
226
|
+
direction = 'vertical',
|
|
227
|
+
nodeSpacing = 50,
|
|
228
|
+
layerSpacing = 80,
|
|
229
|
+
padding = 40,
|
|
230
|
+
childPadding = 16,
|
|
231
|
+
childSpacing = 12,
|
|
232
|
+
maxChildrenPerRow = 3,
|
|
233
|
+
} = options
|
|
234
|
+
|
|
235
|
+
const HEADER_HEIGHT = 30
|
|
236
|
+
|
|
237
|
+
const shapeById = new Map(shapes.map(s => [s.id, s]))
|
|
238
|
+
|
|
239
|
+
// 排除边、画布、端口(pin)——端口不参与 ELK 节点布局
|
|
240
|
+
const nodeShapes = shapes.filter(
|
|
241
|
+
s => s.shapeType !== 'edge' && s.shapeType !== 'diagram' && s.shapeType !== 'pin'
|
|
242
|
+
)
|
|
243
|
+
const edgeShapes = shapes.filter(
|
|
244
|
+
s => s.shapeType === 'edge' && s.sourceId && s.targetId
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// 构建父子关系
|
|
248
|
+
const parentChildMap = new Map<string, Shape[]>()
|
|
249
|
+
const childIdSet = new Set<string>()
|
|
250
|
+
for (const s of nodeShapes) {
|
|
251
|
+
if (s.parenShapeId && s.shapeType === 'shape') {
|
|
252
|
+
const parent = nodeShapes.find(p => p.id === s.parenShapeId)
|
|
253
|
+
if (parent) {
|
|
254
|
+
if (!parentChildMap.has(s.parenShapeId)) {
|
|
255
|
+
parentChildMap.set(s.parenShapeId, [])
|
|
256
|
+
}
|
|
257
|
+
parentChildMap.get(s.parenShapeId)!.push(s)
|
|
258
|
+
childIdSet.add(s.id)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 顶层图元:不是别人的子图元
|
|
264
|
+
const topLevelShapes = nodeShapes.filter(s => !childIdSet.has(s.id))
|
|
265
|
+
const topLevelIdSet = new Set(topLevelShapes.map(s => s.id))
|
|
266
|
+
|
|
267
|
+
// 为父图元计算基于子图元排列的尺寸
|
|
268
|
+
const parentSizeMap = new Map<string, { width: number; height: number }>()
|
|
269
|
+
for (const [parentId, children] of parentChildMap) {
|
|
270
|
+
parentSizeMap.set(
|
|
271
|
+
parentId,
|
|
272
|
+
computeParentSize(children, maxChildrenPerRow, childSpacing, childPadding, HEADER_HEIGHT)
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const elkNodes: ElkNode[] = topLevelShapes.map(s => {
|
|
277
|
+
const overrideSize = parentSizeMap.get(s.id)
|
|
278
|
+
if (overrideSize) {
|
|
279
|
+
return {
|
|
280
|
+
id: s.id,
|
|
281
|
+
width: overrideSize.width,
|
|
282
|
+
height: overrideSize.height,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const { width, height } = getShapeInitialDimensions(s)
|
|
286
|
+
return {
|
|
287
|
+
id: s.id,
|
|
288
|
+
width: width || DEFAULT_NODE_WIDTH,
|
|
289
|
+
height: height || DEFAULT_NODE_HEIGHT,
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// 边的端点:端口换为其父图元 id,供 ELK 排序;并记录需事后吸附的端口
|
|
294
|
+
const edgePortInfos: EdgePortLayoutInfo[] = []
|
|
295
|
+
const elkEdges: ElkExtendedEdge[] = edgeShapes
|
|
296
|
+
.map(e => {
|
|
297
|
+
const src = resolveLayoutEndpoint(e.sourceId!, shapeById)
|
|
298
|
+
const tgt = resolveLayoutEndpoint(e.targetId!, shapeById)
|
|
299
|
+
return { edge: e, src, tgt }
|
|
300
|
+
})
|
|
301
|
+
.filter(({ src, tgt }) => topLevelIdSet.has(src.layoutId) && topLevelIdSet.has(tgt.layoutId))
|
|
302
|
+
.map(({ edge, src, tgt }) => {
|
|
303
|
+
if (src.portId || tgt.portId) {
|
|
304
|
+
edgePortInfos.push({
|
|
305
|
+
sourcePortId: src.portId,
|
|
306
|
+
targetPortId: tgt.portId,
|
|
307
|
+
layoutSourceId: src.layoutId,
|
|
308
|
+
layoutTargetId: tgt.layoutId,
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
id: edge.id,
|
|
313
|
+
sources: [src.layoutId],
|
|
314
|
+
targets: [tgt.layoutId],
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const elkDirection = direction === 'horizontal' ? 'RIGHT' : 'DOWN'
|
|
319
|
+
|
|
320
|
+
const elk = new ELK()
|
|
321
|
+
|
|
322
|
+
const baseLayoutOptions: Record<string, string> = {
|
|
323
|
+
'elk.algorithm': algorithm,
|
|
324
|
+
'elk.direction': elkDirection,
|
|
325
|
+
'elk.spacing.nodeNode': String(nodeSpacing),
|
|
326
|
+
'elk.padding': `[top=${padding},left=${padding},bottom=${padding},right=${padding}]`,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const layoutOptions: Record<string, string> =
|
|
330
|
+
algorithm === 'layered'
|
|
331
|
+
? {
|
|
332
|
+
...baseLayoutOptions,
|
|
333
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': String(layerSpacing),
|
|
334
|
+
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
|
|
335
|
+
'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF',
|
|
336
|
+
'elk.edgeRouting': 'ORTHOGONAL',
|
|
337
|
+
}
|
|
338
|
+
: baseLayoutOptions
|
|
339
|
+
|
|
340
|
+
const graph: ElkNode = {
|
|
341
|
+
id: 'root',
|
|
342
|
+
layoutOptions,
|
|
343
|
+
children: elkNodes,
|
|
344
|
+
edges: elkEdges,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const layoutResult = await elk.layout(graph)
|
|
348
|
+
|
|
349
|
+
const nodeUpdates = new Map<string, Bounds>()
|
|
350
|
+
const affectedNodeIds: string[] = []
|
|
351
|
+
|
|
352
|
+
if (layoutResult.children) {
|
|
353
|
+
for (const child of layoutResult.children) {
|
|
354
|
+
const original = topLevelShapes.find(s => s.id === child.id)
|
|
355
|
+
if (!original) continue
|
|
356
|
+
|
|
357
|
+
const overrideSize = parentSizeMap.get(child.id)
|
|
358
|
+
const width = overrideSize?.width ?? (getShapeInitialDimensions(original).width || DEFAULT_NODE_WIDTH)
|
|
359
|
+
const height = overrideSize?.height ?? (getShapeInitialDimensions(original).height || DEFAULT_NODE_HEIGHT)
|
|
360
|
+
|
|
361
|
+
const parentBounds: Bounds = {
|
|
362
|
+
x: child.x ?? 0,
|
|
363
|
+
y: child.y ?? 0,
|
|
364
|
+
width,
|
|
365
|
+
height,
|
|
366
|
+
}
|
|
367
|
+
nodeUpdates.set(child.id, parentBounds)
|
|
368
|
+
affectedNodeIds.push(child.id)
|
|
369
|
+
|
|
370
|
+
// 布局子图元:横向排列
|
|
371
|
+
const children = parentChildMap.get(child.id)
|
|
372
|
+
if (children?.length) {
|
|
373
|
+
const childBoundsMap = arrangeChildrenInParent(
|
|
374
|
+
parentBounds, children, maxChildrenPerRow, childSpacing, childPadding, HEADER_HEIGHT
|
|
375
|
+
)
|
|
376
|
+
for (const [childId, bounds] of childBoundsMap) {
|
|
377
|
+
nodeUpdates.set(childId, bounds)
|
|
378
|
+
affectedNodeIds.push(childId)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 将端口放到连线两端:朝向对端父图元中心吸附到父图元边缘
|
|
385
|
+
positionPortsOnEdges(edgePortInfos, shapes, nodeUpdates, affectedNodeIds)
|
|
386
|
+
|
|
387
|
+
return { nodeUpdates, affectedNodeIds }
|
|
388
|
+
}
|
package/src/utils/pinUtils.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { Shape, Bounds } from '../types'
|
|
|
2
2
|
import { getBounds } from './geom'
|
|
3
3
|
|
|
4
4
|
const PortKeys = ['OperationalPort', 'ServicePort', 'ResourcePort']
|
|
5
|
+
|
|
6
|
+
/** 是否为业务/服务/资源端口图元 */
|
|
7
|
+
export function isPortShape(shape: Shape): boolean {
|
|
8
|
+
return PortKeys.includes(shape.shapeKey)
|
|
9
|
+
}
|
|
5
10
|
/**
|
|
6
11
|
* 计算点到矩形边的距离
|
|
7
12
|
*/
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Shape } from '../types'
|
|
2
|
+
import { PinKeyMap, ShapeKeyMap } from '../constants'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 各渲染组件中与「原始 / 兜底」bounds 宽高一致的默认值
|
|
6
|
+
* (对应 Block.vue、Package.vue、DividingLine.vue、ActivityAction.vue、ConceptualRole.vue、Diagram.vue 等)
|
|
7
|
+
*/
|
|
8
|
+
const RENDERER_DEFAULTS: Record<string, { width: number; height: number }> = {
|
|
9
|
+
Block: { width: 150, height: 60 },
|
|
10
|
+
Package: { width: 150, height: 85 },
|
|
11
|
+
DividingLine: { width: 150, height: 135 },
|
|
12
|
+
ActivityAction: { width: 100, height: 40 },
|
|
13
|
+
ConceptualRole: { width: 30, height: 30 },
|
|
14
|
+
Diagram: { width: 100, height: 120 },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Pin.vue 中 vbW/vbH 的兜底 */
|
|
18
|
+
const PIN_DEFAULTS = { width: 20, height: 20 }
|
|
19
|
+
/** Port.vue 中 vbW/vbH 的兜底 */
|
|
20
|
+
const PORT_DEFAULTS = { width: 20, height: 20 }
|
|
21
|
+
|
|
22
|
+
const FALLBACK = { width: 100, height: 50 }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 返回图元用于布局/重置时的「初始」宽高(与对应 Vue 组件默认一致),不读取当前可能被拉伸的 bounds。
|
|
26
|
+
*/
|
|
27
|
+
export function getShapeInitialDimensions(shape: Shape): { width: number; height: number } {
|
|
28
|
+
if (shape.shapeType === 'pin') {
|
|
29
|
+
const kind = (PinKeyMap as Record<string, string>)[shape.shapeKey]
|
|
30
|
+
if (kind === 'Port') return { ...PORT_DEFAULTS }
|
|
31
|
+
if (kind === 'Pin') return { ...PIN_DEFAULTS }
|
|
32
|
+
return { ...PIN_DEFAULTS }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (shape.shapeType === 'shape') {
|
|
36
|
+
const renderer = (ShapeKeyMap as Record<string, string>)[shape.shapeKey]
|
|
37
|
+
const d = renderer ? RENDERER_DEFAULTS[renderer] : undefined
|
|
38
|
+
if (d) return { ...d }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { ...FALLBACK }
|
|
42
|
+
}
|