@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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +52 -52
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -4
  5. package/dist/index.d.ts +23 -4
  6. package/dist/index.js +897 -849
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
  10. package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
  11. package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
  12. package/src/__tests__/c4-svg.test.ts +0 -337
  13. package/src/__tests__/c4-text-width-1210.test.ts +0 -43
  14. package/src/__tests__/class-component-order-1249.test.ts +0 -40
  15. package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
  16. package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
  17. package/src/__tests__/class-namespace-1196.test.ts +0 -165
  18. package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
  19. package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
  20. package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
  21. package/src/__tests__/elk-adapter-utils.test.ts +0 -166
  22. package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
  23. package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
  24. package/src/__tests__/er-default-direction-1250.test.ts +0 -69
  25. package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
  26. package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
  27. package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
  28. package/src/__tests__/pie-layout.test.ts +0 -215
  29. package/src/__tests__/pie-svg.test.ts +0 -194
  30. package/src/__tests__/render-mermaid-svg.test.ts +0 -64
  31. package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
  32. package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
  33. package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
  34. package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
  35. package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
  36. package/src/c4/arial-widths.ts +0 -59
  37. package/src/c4/layout.ts +0 -680
  38. package/src/c4/metrics.ts +0 -261
  39. package/src/c4/renderer.ts +0 -366
  40. package/src/class/layout.ts +0 -525
  41. package/src/class/renderer.ts +0 -669
  42. package/src/edge-curves.ts +0 -205
  43. package/src/elk-instance.ts +0 -292
  44. package/src/er/layout.ts +0 -216
  45. package/src/er/renderer.ts +0 -516
  46. package/src/generated/mono-font-subset.ts +0 -32
  47. package/src/index.ts +0 -277
  48. package/src/layout-engine/back-edges.ts +0 -73
  49. package/src/layout-engine/compound-flat.ts +0 -916
  50. package/src/layout-engine/constants.ts +0 -19
  51. package/src/layout-engine/edge-bundling.ts +0 -458
  52. package/src/layout-engine/elk-adapter-utils.ts +0 -81
  53. package/src/layout-engine/elk-graph-builder.ts +0 -245
  54. package/src/layout-engine/from-elk.ts +0 -767
  55. package/src/layout-engine/inner-edges.ts +0 -329
  56. package/src/layout-engine/jog-straightening.ts +0 -52
  57. package/src/layout-engine/layer-alignment.ts +0 -174
  58. package/src/layout-engine/layout-hints.ts +0 -27
  59. package/src/layout-engine/to-elk.ts +0 -855
  60. package/src/layout-engine.ts +0 -115
  61. package/src/pie/layout.ts +0 -234
  62. package/src/pie/renderer.ts +0 -161
  63. package/src/registry.ts +0 -441
  64. package/src/renderer.ts +0 -1510
  65. package/src/resolve-colors.ts +0 -339
  66. package/src/sequence/layout.ts +0 -766
  67. package/src/sequence/renderer.ts +0 -687
  68. package/src/shape-clipping.ts +0 -197
  69. package/src/styles.ts +0 -145
  70. package/src/title-gaps.ts +0 -161
  71. package/src/xychart/layout.ts +0 -727
  72. 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
- }