@zombie-mermaid/svg-renderer 4.2.2 → 5.0.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.
- package/README.md +15 -1
- package/dist/index.cjs +52 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +897 -849
- package/dist/index.js.map +1 -1
- package/package.json +14 -5
- package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
- package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
- package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
- package/src/__tests__/c4-svg.test.ts +0 -337
- package/src/__tests__/c4-text-width-1210.test.ts +0 -43
- package/src/__tests__/class-component-order-1249.test.ts +0 -40
- package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
- package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
- package/src/__tests__/class-namespace-1196.test.ts +0 -165
- package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
- package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
- package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
- package/src/__tests__/elk-adapter-utils.test.ts +0 -166
- package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
- package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
- package/src/__tests__/er-default-direction-1250.test.ts +0 -69
- package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
- package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
- package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
- package/src/__tests__/pie-layout.test.ts +0 -215
- package/src/__tests__/pie-svg.test.ts +0 -194
- package/src/__tests__/render-mermaid-svg.test.ts +0 -64
- package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
- package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
- package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
- package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
- package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
- package/src/c4/arial-widths.ts +0 -59
- package/src/c4/layout.ts +0 -680
- package/src/c4/metrics.ts +0 -261
- package/src/c4/renderer.ts +0 -366
- package/src/class/layout.ts +0 -525
- package/src/class/renderer.ts +0 -669
- package/src/edge-curves.ts +0 -205
- package/src/elk-instance.ts +0 -292
- package/src/er/layout.ts +0 -216
- package/src/er/renderer.ts +0 -516
- package/src/generated/mono-font-subset.ts +0 -32
- package/src/index.ts +0 -277
- package/src/layout-engine/back-edges.ts +0 -73
- package/src/layout-engine/compound-flat.ts +0 -916
- package/src/layout-engine/constants.ts +0 -19
- package/src/layout-engine/edge-bundling.ts +0 -458
- package/src/layout-engine/elk-adapter-utils.ts +0 -81
- package/src/layout-engine/elk-graph-builder.ts +0 -245
- package/src/layout-engine/from-elk.ts +0 -767
- package/src/layout-engine/inner-edges.ts +0 -329
- package/src/layout-engine/jog-straightening.ts +0 -52
- package/src/layout-engine/layer-alignment.ts +0 -174
- package/src/layout-engine/layout-hints.ts +0 -27
- package/src/layout-engine/to-elk.ts +0 -855
- package/src/layout-engine.ts +0 -115
- package/src/pie/layout.ts +0 -234
- package/src/pie/renderer.ts +0 -161
- package/src/registry.ts +0 -441
- package/src/renderer.ts +0 -1510
- package/src/resolve-colors.ts +0 -339
- package/src/sequence/layout.ts +0 -766
- package/src/sequence/renderer.ts +0 -687
- package/src/shape-clipping.ts +0 -197
- package/src/styles.ts +0 -145
- package/src/title-gaps.ts +0 -161
- package/src/xychart/layout.ts +0 -727
- package/src/xychart/renderer.ts +0 -676
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared defaults for the ELK-based layout engine.
|
|
3
|
-
* Split out of layout-engine.ts so both the ELK conversion and the
|
|
4
|
-
* post-processing stages (layer alignment, elk-result conversion) can
|
|
5
|
-
* reference the same values without importing the top-level module.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { FONT_SIZES } from '../styles.ts'
|
|
9
|
-
|
|
10
|
-
/** Default render options (layout-only) */
|
|
11
|
-
export const DEFAULTS = {
|
|
12
|
-
font: 'Inter',
|
|
13
|
-
padding: 40,
|
|
14
|
-
nodeSpacing: 28,
|
|
15
|
-
layerSpacing: 48,
|
|
16
|
-
mergeEdges: true,
|
|
17
|
-
thoroughness: 3,
|
|
18
|
-
fontSizes: FONT_SIZES,
|
|
19
|
-
} as const
|
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Edge bundling — merge fan-out / fan-in edge paths into shared trunks.
|
|
3
|
-
* Split out of layout-engine.ts as a self-contained post-processing pass
|
|
4
|
-
* over a PositionedGraph's nodes/edges/groups.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type {
|
|
8
|
-
PositionedEdge,
|
|
9
|
-
PositionedNode,
|
|
10
|
-
PositionedGroup,
|
|
11
|
-
Direction,
|
|
12
|
-
Point,
|
|
13
|
-
} from '@zombie-mermaid/core'
|
|
14
|
-
|
|
15
|
-
/*
|
|
16
|
-
* Shrink applied to a node box before testing it against a bundled path, in px.
|
|
17
|
-
* A trunk that merely grazes a node's border reads as clean, so only a genuine
|
|
18
|
-
* overlap should disqualify a bundle.
|
|
19
|
-
*/
|
|
20
|
-
const NODE_CLEARANCE = 0.5
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* True when every segment of `points` stays clear of all `nodes`, ignoring the
|
|
24
|
-
* ones named in `skipIds` (an edge's own endpoints, which it must touch).
|
|
25
|
-
*
|
|
26
|
-
* Bundling replaces a routed path with a trunk plus a straight branch to each
|
|
27
|
-
* target. That branch is only safe when it spans the gap between two adjacent
|
|
28
|
-
* layers; a target further away makes it cross every layer in between — and
|
|
29
|
-
* whatever nodes sit in them. The layout engine already routed those edges
|
|
30
|
-
* around the obstacles, so a bundle that would collide is rejected and the
|
|
31
|
-
* original routing kept.
|
|
32
|
-
*
|
|
33
|
-
* Paths here are rectilinear, so each segment is tested as a degenerate
|
|
34
|
-
* rectangle against each node's box.
|
|
35
|
-
*/
|
|
36
|
-
function pathClearOfNodes(
|
|
37
|
-
points: Point[],
|
|
38
|
-
nodes: PositionedNode[],
|
|
39
|
-
skipIds: Set<string>,
|
|
40
|
-
): boolean {
|
|
41
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
42
|
-
const a = points[i]!
|
|
43
|
-
const b = points[i + 1]!
|
|
44
|
-
const segMinX = Math.min(a.x, b.x)
|
|
45
|
-
const segMaxX = Math.max(a.x, b.x)
|
|
46
|
-
const segMinY = Math.min(a.y, b.y)
|
|
47
|
-
const segMaxY = Math.max(a.y, b.y)
|
|
48
|
-
|
|
49
|
-
for (const node of nodes) {
|
|
50
|
-
if (skipIds.has(node.id)) continue
|
|
51
|
-
const minX = node.x + NODE_CLEARANCE
|
|
52
|
-
const maxX = node.x + node.width - NODE_CLEARANCE
|
|
53
|
-
const minY = node.y + NODE_CLEARANCE
|
|
54
|
-
const maxY = node.y + node.height - NODE_CLEARANCE
|
|
55
|
-
// A node smaller than the clearance on either axis can't block anything.
|
|
56
|
-
if (minX >= maxX || minY >= maxY) continue
|
|
57
|
-
if (
|
|
58
|
-
segMinX < maxX &&
|
|
59
|
-
segMaxX > minX &&
|
|
60
|
-
segMinY < maxY &&
|
|
61
|
-
segMaxY > minY
|
|
62
|
-
) {
|
|
63
|
-
return false
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return true
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Find all groups (outermost first) that geometrically contain the given point.
|
|
72
|
-
*/
|
|
73
|
-
function findGroupsContainingPoint(
|
|
74
|
-
x: number,
|
|
75
|
-
y: number,
|
|
76
|
-
groups: PositionedGroup[],
|
|
77
|
-
): PositionedGroup[] {
|
|
78
|
-
const result: PositionedGroup[] = []
|
|
79
|
-
for (const g of groups) {
|
|
80
|
-
if (x >= g.x && x <= g.x + g.width && y >= g.y && y <= g.y + g.height) {
|
|
81
|
-
result.push(g)
|
|
82
|
-
result.push(...findGroupsContainingPoint(x, y, g.children))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return result
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* If `junction` falls inside a group that doesn't contain the reference node,
|
|
90
|
-
* move it just outside the outermost such group boundary.
|
|
91
|
-
*/
|
|
92
|
-
function adjustJunctionForGroups(
|
|
93
|
-
junctionMain: number, // the junction coordinate along the flow axis (Y for TD, X for LR)
|
|
94
|
-
refX: number, // reference node center X (for finding its groups)
|
|
95
|
-
refY: number, // reference node center Y
|
|
96
|
-
groups: PositionedGroup[],
|
|
97
|
-
direction: Direction,
|
|
98
|
-
): number {
|
|
99
|
-
const GAP = 12
|
|
100
|
-
const isLR = direction === 'LR'
|
|
101
|
-
const isRL = direction === 'RL'
|
|
102
|
-
const isBT = direction === 'BT'
|
|
103
|
-
const isHorizontal = isLR || isRL
|
|
104
|
-
|
|
105
|
-
// Groups containing the reference node
|
|
106
|
-
const refGroupIds = new Set(
|
|
107
|
-
findGroupsContainingPoint(refX, refY, groups).map((g) => g.id),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
// Check where the junction point would be along the trunk
|
|
111
|
-
const probeX = isHorizontal ? junctionMain : refX
|
|
112
|
-
const probeY = isHorizontal ? refY : junctionMain
|
|
113
|
-
const junctionGroups = findGroupsContainingPoint(probeX, probeY, groups)
|
|
114
|
-
|
|
115
|
-
// Find outermost group containing the junction but NOT the reference node
|
|
116
|
-
const crossingGroup = junctionGroups.find((g) => !refGroupIds.has(g.id))
|
|
117
|
-
if (!crossingGroup) return junctionMain
|
|
118
|
-
|
|
119
|
-
// Move junction just outside this group
|
|
120
|
-
if (isLR) return crossingGroup.x - GAP
|
|
121
|
-
if (isRL) return crossingGroup.x + crossingGroup.width + GAP
|
|
122
|
-
if (isBT) return crossingGroup.y + crossingGroup.height + GAP
|
|
123
|
-
return crossingGroup.y - GAP // TD
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/*
|
|
127
|
-
* How far an edge end may sit from a node's side and still count as attached
|
|
128
|
-
* to that side, in px (ELK routes to the box edge; alignment can shift it a
|
|
129
|
-
* little).
|
|
130
|
-
*/
|
|
131
|
-
const SIDE_TOLERANCE = 2
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* True when an edge outside the bundle also attaches to `hub` on the side the
|
|
135
|
-
* bundle would use. Bundling collapses the group onto the side's midpoint, so
|
|
136
|
-
* an outside edge ending there (a back edge returning to the node, say) would
|
|
137
|
-
* land on the trunk and read as part of it. ELK's own lanes keep them apart,
|
|
138
|
-
* so that group is left as routed.
|
|
139
|
-
*
|
|
140
|
-
* `side` is the coordinate of the hub's side along the flow axis and
|
|
141
|
-
* `horizontal` says which axis that is (x for a left-to-right graph).
|
|
142
|
-
*/
|
|
143
|
-
function hasOutsideEdgeOnSide(
|
|
144
|
-
edges: PositionedEdge[],
|
|
145
|
-
bundleEdges: Set<PositionedEdge>,
|
|
146
|
-
hubId: string,
|
|
147
|
-
side: number,
|
|
148
|
-
horizontal: boolean,
|
|
149
|
-
): boolean {
|
|
150
|
-
for (const e of edges) {
|
|
151
|
-
if (bundleEdges.has(e) || e.source === e.target) continue
|
|
152
|
-
const end =
|
|
153
|
-
e.source === hubId
|
|
154
|
-
? e.points[0]
|
|
155
|
-
: e.target === hubId
|
|
156
|
-
? e.points[e.points.length - 1]
|
|
157
|
-
: undefined
|
|
158
|
-
if (!end) continue
|
|
159
|
-
if (Math.abs((horizontal ? end.x : end.y) - side) <= SIDE_TOLERANCE)
|
|
160
|
-
return true
|
|
161
|
-
}
|
|
162
|
-
return false
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Bundle fan-out and fan-in edge paths so they share a common trunk segment.
|
|
167
|
-
*
|
|
168
|
-
* For fan-out (one source → N targets), all edges exit the source at the same
|
|
169
|
-
* point, travel along a shared trunk, then branch to their individual targets.
|
|
170
|
-
* The overlapping trunk segments render as a single visible line.
|
|
171
|
-
*
|
|
172
|
-
* Junction points are placed outside subgraph boundaries so branches split
|
|
173
|
-
* before entering a group, not inside it.
|
|
174
|
-
*
|
|
175
|
-
* Constraints: edges in a bundle must share the same style and have no labels.
|
|
176
|
-
* Self-loops and backward edges (against the graph direction) are excluded.
|
|
177
|
-
*/
|
|
178
|
-
export function bundleEdgePaths(
|
|
179
|
-
edges: PositionedEdge[],
|
|
180
|
-
nodes: PositionedNode[],
|
|
181
|
-
groups: PositionedGroup[],
|
|
182
|
-
direction: Direction,
|
|
183
|
-
): void {
|
|
184
|
-
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
|
185
|
-
const processed = new Set<PositionedEdge>()
|
|
186
|
-
|
|
187
|
-
const isLR = direction === 'LR'
|
|
188
|
-
const isRL = direction === 'RL'
|
|
189
|
-
const isBT = direction === 'BT'
|
|
190
|
-
const isHorizontal = isLR || isRL
|
|
191
|
-
|
|
192
|
-
// --- Fan-out: group edges by shared source ---
|
|
193
|
-
const fanOutGroups = new Map<string, PositionedEdge[]>()
|
|
194
|
-
for (const edge of edges) {
|
|
195
|
-
if (edge.source === edge.target) continue
|
|
196
|
-
const group = fanOutGroups.get(edge.source) ?? []
|
|
197
|
-
fanOutGroups.set(edge.source, group)
|
|
198
|
-
group.push(edge)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
for (const [sourceId, group] of fanOutGroups) {
|
|
202
|
-
if (group.length < 2) continue
|
|
203
|
-
|
|
204
|
-
const style = group[0]!.style
|
|
205
|
-
if (group.some((e) => e.label || e.style !== style)) continue
|
|
206
|
-
|
|
207
|
-
const source = nodeMap.get(sourceId)
|
|
208
|
-
if (!source) continue
|
|
209
|
-
|
|
210
|
-
// Only bundle edges going in the forward direction. Collects the
|
|
211
|
-
// resolved target node alongside each edge as it filters, rather than
|
|
212
|
-
// re-querying nodeMap afterward — the two lookups can't be proven to
|
|
213
|
-
// agree from a map() over a separately-filtered array.
|
|
214
|
-
const targets: { edge: PositionedEdge; node: PositionedNode }[] = []
|
|
215
|
-
for (const e of group) {
|
|
216
|
-
const t = nodeMap.get(e.target)
|
|
217
|
-
if (!t) continue
|
|
218
|
-
const isForward = isLR
|
|
219
|
-
? t.x > source.x + source.width
|
|
220
|
-
: isRL
|
|
221
|
-
? t.x + t.width < source.x
|
|
222
|
-
: isBT
|
|
223
|
-
? t.y + t.height < source.y
|
|
224
|
-
: t.y > source.y + source.height // TD/TB
|
|
225
|
-
if (isForward) targets.push({ edge: e, node: t })
|
|
226
|
-
}
|
|
227
|
-
if (targets.length < 2) continue
|
|
228
|
-
|
|
229
|
-
const srcCX = source.x + source.width / 2
|
|
230
|
-
const srcCY = source.y + source.height / 2
|
|
231
|
-
|
|
232
|
-
if (isHorizontal) {
|
|
233
|
-
const exitX = isLR ? source.x + source.width : source.x
|
|
234
|
-
const exitY = srcCY
|
|
235
|
-
if (
|
|
236
|
-
hasOutsideEdgeOnSide(
|
|
237
|
-
edges,
|
|
238
|
-
new Set(targets.map((t) => t.edge)),
|
|
239
|
-
sourceId,
|
|
240
|
-
exitX,
|
|
241
|
-
true,
|
|
242
|
-
)
|
|
243
|
-
)
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
const nearestX = isLR
|
|
247
|
-
? Math.min(...targets.map((t) => t.node.x))
|
|
248
|
-
: Math.max(...targets.map((t) => t.node.x + t.node.width))
|
|
249
|
-
let junctionX = exitX + (nearestX - exitX) / 2
|
|
250
|
-
junctionX = adjustJunctionForGroups(
|
|
251
|
-
junctionX,
|
|
252
|
-
srcCX,
|
|
253
|
-
srcCY,
|
|
254
|
-
groups,
|
|
255
|
-
direction,
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
const bundled: { edge: PositionedEdge; points: Point[] }[] = []
|
|
259
|
-
for (const { edge, node: target } of targets) {
|
|
260
|
-
const entryX = isLR ? target.x : target.x + target.width
|
|
261
|
-
const entryY = target.y + target.height / 2
|
|
262
|
-
const points = [
|
|
263
|
-
{ x: exitX, y: exitY },
|
|
264
|
-
{ x: junctionX, y: exitY },
|
|
265
|
-
{ x: junctionX, y: entryY },
|
|
266
|
-
{ x: entryX, y: entryY },
|
|
267
|
-
]
|
|
268
|
-
if (pathClearOfNodes(points, nodes, new Set([sourceId, target.id]))) {
|
|
269
|
-
bundled.push({ edge, points })
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// One clear branch is not a bundle — leave the whole group as routed.
|
|
273
|
-
if (bundled.length < 2) continue
|
|
274
|
-
for (const { edge, points } of bundled) {
|
|
275
|
-
edge.points = points
|
|
276
|
-
processed.add(edge)
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
const exitX = srcCX
|
|
280
|
-
const exitY = isBT ? source.y : source.y + source.height
|
|
281
|
-
if (
|
|
282
|
-
hasOutsideEdgeOnSide(
|
|
283
|
-
edges,
|
|
284
|
-
new Set(targets.map((t) => t.edge)),
|
|
285
|
-
sourceId,
|
|
286
|
-
exitY,
|
|
287
|
-
false,
|
|
288
|
-
)
|
|
289
|
-
)
|
|
290
|
-
continue
|
|
291
|
-
|
|
292
|
-
const nearestY = isBT
|
|
293
|
-
? Math.max(...targets.map((t) => t.node.y + t.node.height))
|
|
294
|
-
: Math.min(...targets.map((t) => t.node.y))
|
|
295
|
-
let junctionY = exitY + (nearestY - exitY) / 2
|
|
296
|
-
junctionY = adjustJunctionForGroups(
|
|
297
|
-
junctionY,
|
|
298
|
-
srcCX,
|
|
299
|
-
srcCY,
|
|
300
|
-
groups,
|
|
301
|
-
direction,
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
const bundled: { edge: PositionedEdge; points: Point[] }[] = []
|
|
305
|
-
for (const { edge, node: target } of targets) {
|
|
306
|
-
const entryX = target.x + target.width / 2
|
|
307
|
-
const entryY = isBT ? target.y + target.height : target.y
|
|
308
|
-
const points = [
|
|
309
|
-
{ x: exitX, y: exitY },
|
|
310
|
-
{ x: exitX, y: junctionY },
|
|
311
|
-
{ x: entryX, y: junctionY },
|
|
312
|
-
{ x: entryX, y: entryY },
|
|
313
|
-
]
|
|
314
|
-
if (pathClearOfNodes(points, nodes, new Set([sourceId, target.id]))) {
|
|
315
|
-
bundled.push({ edge, points })
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// One clear branch is not a bundle — leave the whole group as routed.
|
|
319
|
-
if (bundled.length < 2) continue
|
|
320
|
-
for (const { edge, points } of bundled) {
|
|
321
|
-
edge.points = points
|
|
322
|
-
processed.add(edge)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// --- Fan-in: group edges by shared target (skip already-bundled edges) ---
|
|
328
|
-
const fanInGroups = new Map<string, PositionedEdge[]>()
|
|
329
|
-
for (const edge of edges) {
|
|
330
|
-
if (processed.has(edge) || edge.source === edge.target) continue
|
|
331
|
-
const group = fanInGroups.get(edge.target) ?? []
|
|
332
|
-
fanInGroups.set(edge.target, group)
|
|
333
|
-
group.push(edge)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
for (const [targetId, group] of fanInGroups) {
|
|
337
|
-
if (group.length < 2) continue
|
|
338
|
-
|
|
339
|
-
const style = group[0]!.style
|
|
340
|
-
if (group.some((e) => e.label || e.style !== style)) continue
|
|
341
|
-
|
|
342
|
-
const target = nodeMap.get(targetId)
|
|
343
|
-
if (!target) continue
|
|
344
|
-
|
|
345
|
-
// Collects the resolved source node alongside each edge as it filters,
|
|
346
|
-
// rather than re-querying nodeMap afterward (see the fan-out loop above
|
|
347
|
-
// for why: a map() over a separately-filtered array can't be proven to
|
|
348
|
-
// agree with the filter's own lookup).
|
|
349
|
-
const sources: { edge: PositionedEdge; node: PositionedNode }[] = []
|
|
350
|
-
for (const e of group) {
|
|
351
|
-
const s = nodeMap.get(e.source)
|
|
352
|
-
if (!s) continue
|
|
353
|
-
const isForward = isLR
|
|
354
|
-
? s.x + s.width < target.x
|
|
355
|
-
: isRL
|
|
356
|
-
? s.x > target.x + target.width
|
|
357
|
-
: isBT
|
|
358
|
-
? s.y > target.y + target.height
|
|
359
|
-
: s.y + s.height < target.y // TD/TB
|
|
360
|
-
if (isForward) sources.push({ edge: e, node: s })
|
|
361
|
-
}
|
|
362
|
-
if (sources.length < 2) continue
|
|
363
|
-
const tgtCX = target.x + target.width / 2
|
|
364
|
-
const tgtCY = target.y + target.height / 2
|
|
365
|
-
|
|
366
|
-
if (isHorizontal) {
|
|
367
|
-
const entryX = isLR ? target.x : target.x + target.width
|
|
368
|
-
const entryY = tgtCY
|
|
369
|
-
if (
|
|
370
|
-
hasOutsideEdgeOnSide(
|
|
371
|
-
edges,
|
|
372
|
-
new Set(sources.map((s) => s.edge)),
|
|
373
|
-
targetId,
|
|
374
|
-
entryX,
|
|
375
|
-
true,
|
|
376
|
-
)
|
|
377
|
-
)
|
|
378
|
-
continue
|
|
379
|
-
|
|
380
|
-
const farthestX = isLR
|
|
381
|
-
? Math.max(...sources.map((s) => s.node.x + s.node.width))
|
|
382
|
-
: Math.min(...sources.map((s) => s.node.x))
|
|
383
|
-
let junctionX = farthestX + (entryX - farthestX) / 2
|
|
384
|
-
junctionX = adjustJunctionForGroups(
|
|
385
|
-
junctionX,
|
|
386
|
-
tgtCX,
|
|
387
|
-
tgtCY,
|
|
388
|
-
groups,
|
|
389
|
-
direction,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
const bundled: { edge: PositionedEdge; points: Point[] }[] = []
|
|
393
|
-
for (const { edge, node: src } of sources) {
|
|
394
|
-
const exitX = isLR ? src.x + src.width : src.x
|
|
395
|
-
const exitY = src.y + src.height / 2
|
|
396
|
-
const points = [
|
|
397
|
-
{ x: exitX, y: exitY },
|
|
398
|
-
{ x: junctionX, y: exitY },
|
|
399
|
-
{ x: junctionX, y: entryY },
|
|
400
|
-
{ x: entryX, y: entryY },
|
|
401
|
-
]
|
|
402
|
-
if (pathClearOfNodes(points, nodes, new Set([src.id, targetId]))) {
|
|
403
|
-
bundled.push({ edge, points })
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
// One clear branch is not a bundle — leave the whole group as routed.
|
|
407
|
-
if (bundled.length < 2) continue
|
|
408
|
-
for (const { edge, points } of bundled) {
|
|
409
|
-
edge.points = points
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
const entryX = tgtCX
|
|
413
|
-
const entryY = isBT ? target.y + target.height : target.y
|
|
414
|
-
if (
|
|
415
|
-
hasOutsideEdgeOnSide(
|
|
416
|
-
edges,
|
|
417
|
-
new Set(sources.map((s) => s.edge)),
|
|
418
|
-
targetId,
|
|
419
|
-
entryY,
|
|
420
|
-
false,
|
|
421
|
-
)
|
|
422
|
-
)
|
|
423
|
-
continue
|
|
424
|
-
|
|
425
|
-
const farthestY = isBT
|
|
426
|
-
? Math.min(...sources.map((s) => s.node.y))
|
|
427
|
-
: Math.max(...sources.map((s) => s.node.y + s.node.height))
|
|
428
|
-
let junctionY = farthestY + (entryY - farthestY) / 2
|
|
429
|
-
junctionY = adjustJunctionForGroups(
|
|
430
|
-
junctionY,
|
|
431
|
-
tgtCX,
|
|
432
|
-
tgtCY,
|
|
433
|
-
groups,
|
|
434
|
-
direction,
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
const bundled: { edge: PositionedEdge; points: Point[] }[] = []
|
|
438
|
-
for (const { edge, node: src } of sources) {
|
|
439
|
-
const exitX = src.x + src.width / 2
|
|
440
|
-
const exitY = isBT ? src.y : src.y + src.height
|
|
441
|
-
const points = [
|
|
442
|
-
{ x: exitX, y: exitY },
|
|
443
|
-
{ x: exitX, y: junctionY },
|
|
444
|
-
{ x: entryX, y: junctionY },
|
|
445
|
-
{ x: entryX, y: entryY },
|
|
446
|
-
]
|
|
447
|
-
if (pathClearOfNodes(points, nodes, new Set([src.id, targetId]))) {
|
|
448
|
-
bundled.push({ edge, points })
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
// One clear branch is not a bundle — leave the whole group as routed.
|
|
452
|
-
if (bundled.length < 2) continue
|
|
453
|
-
for (const { edge, points } of bundled) {
|
|
454
|
-
edge.points = points
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared ELK-edge-geometry extraction helpers.
|
|
3
|
-
*
|
|
4
|
-
* `from-elk.ts` (flowchart/state), `../class/layout.ts`, and
|
|
5
|
-
* `../er/layout.ts` each independently walk an ELK edge's
|
|
6
|
-
* `section.startPoint → bendPoints → endPoint` into a `Point[]`, plus
|
|
7
|
-
* near-identical edge-label-position math. This module is the single
|
|
8
|
-
* place that logic lives, so the three call sites can't drift.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { ElkExtendedEdge } from 'elkjs'
|
|
12
|
-
import type { Point } from '@zombie-mermaid/core'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Walk an ELK edge's first routed section into a flat point path:
|
|
16
|
-
* `startPoint → bendPoints → endPoint`.
|
|
17
|
-
*
|
|
18
|
-
* ELK can theoretically produce multiple sections per edge (for edges
|
|
19
|
-
* split across hierarchy boundaries via ports), but all three call sites
|
|
20
|
-
* only ever read `sections[0]` — hierarchical decomposition in this
|
|
21
|
-
* codebase is handled by emitting separate ELK edges (see
|
|
22
|
-
* `to-elk.ts`/`parseHopEdgeId` in `from-elk.ts`), not multi-section edges.
|
|
23
|
-
*
|
|
24
|
-
* `offsetX`/`offsetY` translate the section's coordinates into an
|
|
25
|
-
* ancestor's coordinate space, for callers walking a nested ELK result
|
|
26
|
-
* (`from-elk.ts`). Callers with a flat (non-hierarchical) ELK graph
|
|
27
|
-
* (`class/layout.ts`, `er/layout.ts`) can omit them.
|
|
28
|
-
*
|
|
29
|
-
* Returns an empty array if the edge has no routed sections.
|
|
30
|
-
*/
|
|
31
|
-
export function extractEdgePoints(
|
|
32
|
-
elkEdge: ElkExtendedEdge,
|
|
33
|
-
offsetX = 0,
|
|
34
|
-
offsetY = 0,
|
|
35
|
-
): Point[] {
|
|
36
|
-
const points: Point[] = []
|
|
37
|
-
if (!elkEdge.sections || elkEdge.sections.length === 0) return points
|
|
38
|
-
|
|
39
|
-
const section = elkEdge.sections[0]!
|
|
40
|
-
points.push({
|
|
41
|
-
x: section.startPoint.x + offsetX,
|
|
42
|
-
y: section.startPoint.y + offsetY,
|
|
43
|
-
})
|
|
44
|
-
if (section.bendPoints) {
|
|
45
|
-
for (const bp of section.bendPoints) {
|
|
46
|
-
points.push({ x: bp.x + offsetX, y: bp.y + offsetY })
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
points.push({
|
|
50
|
-
x: section.endPoint.x + offsetX,
|
|
51
|
-
y: section.endPoint.y + offsetY,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return points
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Compute an edge label's center position from ELK's placed label box
|
|
59
|
-
* (`label.x/y` is the box's top-left corner).
|
|
60
|
-
*
|
|
61
|
-
* `offsetX`/`offsetY` translate into an ancestor's coordinate space, same
|
|
62
|
-
* as `extractEdgePoints`.
|
|
63
|
-
*
|
|
64
|
-
* Returns `undefined` if ELK didn't place a label (no label on the edge,
|
|
65
|
-
* or ELK left `x`/`y` unset).
|
|
66
|
-
*/
|
|
67
|
-
export function extractEdgeLabelPosition(
|
|
68
|
-
elkEdge: ElkExtendedEdge,
|
|
69
|
-
offsetX = 0,
|
|
70
|
-
offsetY = 0,
|
|
71
|
-
): Point | undefined {
|
|
72
|
-
if (!elkEdge.labels || elkEdge.labels.length === 0) return undefined
|
|
73
|
-
|
|
74
|
-
const label = elkEdge.labels[0]!
|
|
75
|
-
if (label.x == null || label.y == null) return undefined
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
x: label.x + (label.width ?? 0) / 2 + offsetX,
|
|
79
|
-
y: label.y + (label.height ?? 0) / 2 + offsetY,
|
|
80
|
-
}
|
|
81
|
-
}
|