@mx-sose-front/mx-sose-graph 1.2.8 → 1.3.0

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,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
+ }
@@ -836,85 +836,99 @@ export class EdgeUtils {
836
836
  }
837
837
  ): {
838
838
  connectionData: typeof connectionData;
839
- outputPinBounds: { x: number; y: number; width: number; height: number; direction?: string };
840
- inputPinBounds: { x: number; y: number; width: number; height: number; direction?: string };
839
+ outputPinBounds: { x: number; y: number; width: number; height: number; direction?: string } | null;
840
+ inputPinBounds: { x: number; y: number; width: number; height: number; direction?: string } | null;
841
841
  } {
842
- // 创建临时 OutputPin 对象用于计算 bounds(只包含必要属性)
843
- const outputPin: Shape = {
844
- shapeKey: 'OutputPin',
845
- shapeType: 'pin',
846
- bounds: {
847
- x: connectionData.sourcePoint.x,
848
- y: connectionData.sourcePoint.y,
842
+ const sourceIsPin = String(sourceShape.shapeType).trim().toLowerCase() === 'pin';
843
+ const targetIsPin = String(targetShape.shapeType).trim().toLowerCase() === 'pin';
844
+
845
+ let outputPin: Shape;
846
+ let outputPinBounds: { x: number; y: number; width: number; height: number; direction?: string } | null = null;
847
+
848
+ if (sourceIsPin) {
849
+ outputPin = sourceShape;
850
+ } else {
851
+ outputPin = {
852
+ shapeKey: 'OutputPin',
853
+ shapeType: 'pin',
854
+ bounds: {
855
+ x: connectionData.sourcePoint.x,
856
+ y: connectionData.sourcePoint.y,
857
+ width: 22,
858
+ height: 22,
859
+ },
860
+ } as Shape;
861
+
862
+ const adjustedOutputPinPos = snapPinToParentEdge(
863
+ connectionData.sourcePoint,
864
+ sourceShape,
865
+ outputPin
866
+ );
867
+ outputPin.bounds = {
868
+ x: adjustedOutputPinPos.x,
869
+ y: adjustedOutputPinPos.y,
849
870
  width: 22,
850
871
  height: 22,
851
- },
852
- } as Shape;
853
-
854
- // 使用 snapPinToParentEdge 调整 outputPin 的位置
855
- const adjustedOutputPinPos = snapPinToParentEdge(
856
- connectionData.sourcePoint,
857
- sourceShape,
858
- outputPin
859
- );
860
- outputPin.bounds = {
861
- x: adjustedOutputPinPos.x,
862
- y: adjustedOutputPinPos.y,
863
- width: 22,
864
- height: 22,
865
- };
872
+ };
866
873
 
867
- // 创建临时 InputPin 对象用于计算 bounds(只包含必要属性)
868
- const inputPin: Shape = {
869
- shapeKey: 'InputPin',
870
- shapeType: 'pin',
871
- bounds: {
872
- x: connectionData.targetPoint.x,
873
- y: connectionData.targetPoint.y,
874
+ outputPinBounds = {
875
+ x: outputPin.bounds.x ?? 0,
876
+ y: outputPin.bounds.y ?? 0,
877
+ width: outputPin.bounds.width ?? 22,
878
+ height: outputPin.bounds.height ?? 22,
879
+ direction: outputPin.direction,
880
+ };
881
+ }
882
+
883
+ let inputPin: Shape;
884
+ let inputPinBounds: { x: number; y: number; width: number; height: number; direction?: string } | null = null;
885
+
886
+ if (targetIsPin) {
887
+ inputPin = targetShape;
888
+ } else {
889
+ inputPin = {
890
+ shapeKey: 'InputPin',
891
+ shapeType: 'pin',
892
+ bounds: {
893
+ x: connectionData.targetPoint.x,
894
+ y: connectionData.targetPoint.y,
895
+ width: 22,
896
+ height: 22,
897
+ },
898
+ } as Shape;
899
+
900
+ const adjustedInputPinPos = snapPinToParentEdge(
901
+ connectionData.targetPoint,
902
+ targetShape,
903
+ inputPin
904
+ );
905
+ inputPin.bounds = {
906
+ x: adjustedInputPinPos.x,
907
+ y: adjustedInputPinPos.y,
874
908
  width: 22,
875
909
  height: 22,
876
- },
877
- } as Shape;
878
-
879
- // 使用 snapPinToParentEdge 调整 inputPin 的位置
880
- const adjustedInputPinPos = snapPinToParentEdge(
881
- connectionData.targetPoint,
882
- targetShape,
883
- inputPin
884
- );
885
- inputPin.bounds = {
886
- x: adjustedInputPinPos.x,
887
- y: adjustedInputPinPos.y,
888
- width: 22,
889
- height: 22,
890
- };
910
+ };
911
+
912
+ inputPinBounds = {
913
+ x: inputPin.bounds.x ?? 0,
914
+ y: inputPin.bounds.y ?? 0,
915
+ width: inputPin.bounds.width ?? 22,
916
+ height: inputPin.bounds.height ?? 22,
917
+ direction: inputPin.direction,
918
+ };
919
+ }
891
920
 
892
- // 根据 pin 的方向计算连接点位置
893
921
  const outputPinConnectionPoint = EdgeUtils.calculatePinConnectionPoint(outputPin);
894
922
  const inputPinConnectionPoint = EdgeUtils.calculatePinConnectionPoint(inputPin);
895
923
 
896
- // 修改 connectionData 的连接点
897
924
  connectionData.sourcePoint = outputPinConnectionPoint;
898
925
  connectionData.targetPoint = inputPinConnectionPoint;
899
- // 更新 waypoints
900
926
  connectionData.waypoints = [outputPinConnectionPoint, inputPinConnectionPoint];
901
927
 
902
928
  return {
903
929
  connectionData,
904
- outputPinBounds: {
905
- x: outputPin.bounds.x ?? 0,
906
- y: outputPin.bounds.y ?? 0,
907
- width: outputPin.bounds.width ?? 22,
908
- height: outputPin.bounds.height ?? 22,
909
- direction: outputPin.direction,
910
- },
911
- inputPinBounds: {
912
- x: inputPin.bounds.x ?? 0,
913
- y: inputPin.bounds.y ?? 0,
914
- width: inputPin.bounds.width ?? 22,
915
- height: inputPin.bounds.height ?? 22,
916
- direction: inputPin.direction,
917
- },
930
+ outputPinBounds,
931
+ inputPinBounds,
918
932
  };
919
933
  }
920
934
  }
@@ -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
+ }
@@ -123,7 +123,7 @@ const emit = defineEmits<{
123
123
  (e: 'shapes-property', value: { selectedShape: Shape, showPropertyPanel: boolean }): void
124
124
  (e: 'shapes-edit-name', value: { shape: Shape, newName: string, oldName: string }): void
125
125
  (e: 'connect-end', value: { sourceId: string, targetId: string, sourcePoint: { x: number, y: number }, targetPoint: { x: number, y: number }, waypoints: Array<{ x: number, y: number }> }): void
126
- (e: 'object-flow-connect-end', value: { connectionData: { sourceId: string; targetId: string; sourcePoint: { x: number; y: number }; targetPoint: { x: number; y: number }; waypoints: Array<{ x: number; y: number }> }, outputPinBounds: { x: number; y: number; width: number; height: number }, inputPinBounds: { x: number; y: number; width: number; height: number } }): void
126
+ (e: 'object-flow-connect-end', value: { connectionData: { sourceId: string; targetId: string; sourcePoint: { x: number; y: number }; targetPoint: { x: number; y: number }; waypoints: Array<{ x: number; y: number }> }, outputPinBounds: { x: number; y: number; width: number; height: number } | null, inputPinBounds: { x: number; y: number; width: number; height: number } | null }): void
127
127
  (e: 'action-button-click', value: string, shape: Shape): void
128
128
  (e: 'diagramDoubleClick', data: any): void
129
129
  (e: 'model-type-property-id-button-click', value: string, shape: Shape): void
@@ -288,7 +288,7 @@ const handleConnectEnd = (connectionData: { sourceId: string; targetId: string;
288
288
  console.log('连接结束事件已传递给父组件:', connectionData);
289
289
  }
290
290
 
291
- const handleObjectFlowConnectEnd = (connectionData: { connectionData: { sourceId: string; targetId: string; sourcePoint: { x: number; y: number }; targetPoint: { x: number; y: number }; waypoints: Array<{ x: number; y: number }> }, outputPinBounds: { x: number; y: number; width: number; height: number }, inputPinBounds: { x: number; y: number; width: number; height: number } }) => {
291
+ const handleObjectFlowConnectEnd = (connectionData: { connectionData: { sourceId: string; targetId: string; sourcePoint: { x: number; y: number }; targetPoint: { x: number; y: number }; waypoints: Array<{ x: number; y: number }> }, outputPinBounds: { x: number; y: number; width: number; height: number } | null, inputPinBounds: { x: number; y: number; width: number; height: number } | null }) => {
292
292
  emit('object-flow-connect-end', connectionData);
293
293
  }
294
294
 
@@ -588,6 +588,8 @@ defineExpose({
588
588
  position: absolute;
589
589
  top: 0;
590
590
  left: 0;
591
+ transform-origin: 0 0;
592
+ transform: scale(v-bind('currentScale'));
591
593
  pointer-events: none;
592
594
  z-index: 998;
593
595
  }