@zombie-mermaid/svg-renderer 3.0.0 → 3.2.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/dist/index.cjs +49 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1611 -1118
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/c4-mermaid-reference.test.ts +85 -0
- package/src/__tests__/c4-svg.test.ts +337 -0
- package/src/__tests__/class-diamond-markers-1232.test.ts +59 -0
- package/src/__tests__/er-cardinality-markers-1233.test.ts +35 -0
- package/src/__tests__/fixtures/c4-mermaid-reference.json +626 -0
- package/src/__tests__/xychart-smooth-curve.test.ts +42 -0
- package/src/c4/layout.ts +490 -0
- package/src/c4/metrics.ts +235 -0
- package/src/c4/renderer.ts +366 -0
- package/src/class/renderer.ts +72 -68
- package/src/edge-curves.ts +17 -16
- package/src/er/renderer.ts +64 -50
- package/src/layout-engine/elk-graph-builder.ts +4 -0
- package/src/registry.ts +60 -0
- package/src/renderer.ts +190 -189
- package/src/sequence/renderer.ts +66 -65
- package/src/xychart/layout.ts +27 -11
- package/src/xychart/renderer.ts +33 -42
package/src/c4/layout.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
C4Boundary,
|
|
3
|
+
C4Diagram,
|
|
4
|
+
C4Element,
|
|
5
|
+
C4Relationship,
|
|
6
|
+
PositionedC4Boundary,
|
|
7
|
+
PositionedC4Diagram,
|
|
8
|
+
PositionedC4Element,
|
|
9
|
+
PositionedC4Relationship,
|
|
10
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
11
|
+
import {
|
|
12
|
+
c4BoundaryTypeLine,
|
|
13
|
+
c4RelLabelLines,
|
|
14
|
+
c4TypeLine,
|
|
15
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
16
|
+
import type { Point, RenderOptions } from '@zombie-mermaid/core'
|
|
17
|
+
import {
|
|
18
|
+
C4,
|
|
19
|
+
C4_TEXT_WIDTH,
|
|
20
|
+
c4PersonGeometry,
|
|
21
|
+
c4ShapeSize,
|
|
22
|
+
c4TextWidth,
|
|
23
|
+
wrapToWidth,
|
|
24
|
+
} from './metrics.ts'
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// C4 layout, following Mermaid's own C4 renderer.
|
|
28
|
+
//
|
|
29
|
+
// Mermaid does not run a graph layout for C4. It places shapes in rows, in
|
|
30
|
+
// the order they are declared: up to `c4ShapeInRow` per row, boundaries
|
|
31
|
+
// `c4BoundaryInRow` to a row, each boundary wide enough for what it holds.
|
|
32
|
+
// Relationships are drawn afterwards between the shapes and never move
|
|
33
|
+
// anything, so `Rel_U/D/L/R` (and a render direction) have no effect on
|
|
34
|
+
// placement, as in Mermaid. This is a port of that algorithm
|
|
35
|
+
// (packages/mermaid/src/diagrams/c4/c4Renderer.ts), with text measured by
|
|
36
|
+
// estimate instead of in a browser.
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/** Mermaid's `Bounds`: the extent of what has been placed in one boundary. */
|
|
40
|
+
interface Bounds {
|
|
41
|
+
startx?: number
|
|
42
|
+
stopx?: number
|
|
43
|
+
starty?: number
|
|
44
|
+
stopy?: number
|
|
45
|
+
widthLimit?: number
|
|
46
|
+
/** Where the next shape would go (Mermaid's `nextData`). */
|
|
47
|
+
next: {
|
|
48
|
+
startx?: number
|
|
49
|
+
stopx?: number
|
|
50
|
+
starty?: number
|
|
51
|
+
stopy?: number
|
|
52
|
+
cnt: number
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function newBounds(): Bounds {
|
|
57
|
+
return { next: { cnt: 0 } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setData(
|
|
61
|
+
b: Bounds,
|
|
62
|
+
startx: number,
|
|
63
|
+
stopx: number,
|
|
64
|
+
starty: number,
|
|
65
|
+
stopy: number,
|
|
66
|
+
): void {
|
|
67
|
+
b.next.startx = b.startx = startx
|
|
68
|
+
b.next.stopx = b.stopx = stopx
|
|
69
|
+
b.next.starty = b.starty = starty
|
|
70
|
+
b.next.stopy = b.stopy = stopy
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function grow(
|
|
74
|
+
obj: Bounds | Bounds['next'],
|
|
75
|
+
key: 'startx' | 'stopx' | 'starty' | 'stopy',
|
|
76
|
+
val: number,
|
|
77
|
+
pick: (a: number, b: number) => number,
|
|
78
|
+
): void {
|
|
79
|
+
const cur = obj[key]
|
|
80
|
+
obj[key] = cur === undefined ? val : pick(val, cur)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Mermaid's `Bounds.insert`: give a shape the next free slot. */
|
|
84
|
+
function insert(b: Bounds, shape: Placed): void {
|
|
85
|
+
b.next.cnt += 1
|
|
86
|
+
const margin = C4.shapeMargin
|
|
87
|
+
let startx =
|
|
88
|
+
b.next.startx === b.next.stopx
|
|
89
|
+
? b.next.stopx! + margin
|
|
90
|
+
: b.next.stopx! + margin * 2
|
|
91
|
+
let stopx = startx + shape.width
|
|
92
|
+
let starty = b.next.starty! + margin * 2
|
|
93
|
+
let stopy = starty + shape.height
|
|
94
|
+
if (
|
|
95
|
+
startx >= b.widthLimit! ||
|
|
96
|
+
stopx >= b.widthLimit! ||
|
|
97
|
+
b.next.cnt > C4.shapeInRow
|
|
98
|
+
) {
|
|
99
|
+
startx = b.next.startx! + margin + C4.nextLinePaddingX
|
|
100
|
+
starty = b.next.stopy! + margin * 2
|
|
101
|
+
b.next.stopx = stopx = startx + shape.width
|
|
102
|
+
b.next.starty = b.next.stopy
|
|
103
|
+
b.next.stopy = stopy = starty + shape.height
|
|
104
|
+
b.next.cnt = 1
|
|
105
|
+
}
|
|
106
|
+
shape.x = startx
|
|
107
|
+
shape.y = starty
|
|
108
|
+
grow(b, 'startx', startx, Math.min)
|
|
109
|
+
grow(b, 'starty', starty, Math.min)
|
|
110
|
+
grow(b, 'stopx', stopx, Math.max)
|
|
111
|
+
grow(b, 'stopy', stopy, Math.max)
|
|
112
|
+
grow(b.next, 'startx', startx, Math.min)
|
|
113
|
+
grow(b.next, 'starty', starty, Math.min)
|
|
114
|
+
grow(b.next, 'stopx', stopx, Math.max)
|
|
115
|
+
grow(b.next, 'stopy', stopy, Math.max)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface Placed {
|
|
119
|
+
x: number
|
|
120
|
+
y: number
|
|
121
|
+
width: number
|
|
122
|
+
height: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface MeasuredElement extends Placed {
|
|
126
|
+
el: C4Element
|
|
127
|
+
nameLines: string[]
|
|
128
|
+
descriptionLines: string[]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function measureElement(el: C4Element): MeasuredElement {
|
|
132
|
+
const nameLines = wrapToWidth(el.label, C4_TEXT_WIDTH, C4.nameSize, 700)
|
|
133
|
+
const descriptionLines = el.description
|
|
134
|
+
? wrapToWidth(el.description, C4_TEXT_WIDTH, C4.descrSize, 400)
|
|
135
|
+
: []
|
|
136
|
+
const textWidth = Math.max(
|
|
137
|
+
...nameLines.map((l) => c4TextWidth(l, C4.nameSize, 700)),
|
|
138
|
+
c4TextWidth(c4TypeLine(el), C4.typeSize, 400),
|
|
139
|
+
...descriptionLines.map((l) => c4TextWidth(l, C4.descrSize, 400)),
|
|
140
|
+
)
|
|
141
|
+
const size = c4ShapeSize(
|
|
142
|
+
el,
|
|
143
|
+
nameLines.length,
|
|
144
|
+
descriptionLines.length,
|
|
145
|
+
textWidth,
|
|
146
|
+
)
|
|
147
|
+
return { el, nameLines, descriptionLines, x: 0, y: 0, ...size }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A boundary as the layout walks it: its own shapes, then its children.
|
|
151
|
+
interface Frame {
|
|
152
|
+
alias: string
|
|
153
|
+
boundary?: C4Boundary
|
|
154
|
+
elements: C4Element[]
|
|
155
|
+
children: Frame[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildFrames(diagram: C4Diagram): Frame {
|
|
159
|
+
const inBoundary = new Set<string>()
|
|
160
|
+
const byAlias = new Map(diagram.elements.map((e) => [e.alias, e]))
|
|
161
|
+
const frame = (b: C4Boundary): Frame => {
|
|
162
|
+
const elements = b.elementAliases.flatMap((a) => {
|
|
163
|
+
inBoundary.add(a)
|
|
164
|
+
const el = byAlias.get(a)
|
|
165
|
+
return el ? [el] : []
|
|
166
|
+
})
|
|
167
|
+
return {
|
|
168
|
+
alias: b.alias,
|
|
169
|
+
boundary: b,
|
|
170
|
+
elements,
|
|
171
|
+
children: b.children.map(frame),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const children = diagram.boundaries.map(frame)
|
|
175
|
+
return {
|
|
176
|
+
alias: 'global',
|
|
177
|
+
elements: diagram.elements.filter((e) => !inBoundary.has(e.alias)),
|
|
178
|
+
children,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface LaidOutBoundary extends PositionedC4Boundary {
|
|
183
|
+
labelY: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface Layout {
|
|
187
|
+
elements: Map<string, MeasuredElement>
|
|
188
|
+
boundaries: LaidOutBoundary[]
|
|
189
|
+
maxX: number
|
|
190
|
+
maxY: number
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function layoutFrames(root: Frame): Layout {
|
|
194
|
+
const elements = new Map<string, MeasuredElement>()
|
|
195
|
+
const boundaries: LaidOutBoundary[] = []
|
|
196
|
+
let maxX: number = C4.diagramMarginX
|
|
197
|
+
let maxY: number = C4.diagramMarginY
|
|
198
|
+
|
|
199
|
+
// Mermaid's `drawC4ShapeArray`: measure, insert each, then bump the margin.
|
|
200
|
+
const placeShapes = (bounds: Bounds, els: C4Element[]): void => {
|
|
201
|
+
for (const el of els) {
|
|
202
|
+
const m = measureElement(el)
|
|
203
|
+
elements.set(el.alias, m)
|
|
204
|
+
insert(bounds, m)
|
|
205
|
+
}
|
|
206
|
+
bounds.stopx! += C4.shapeMargin
|
|
207
|
+
bounds.stopy! += C4.shapeMargin
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Mermaid's `drawInsideBoundary`.
|
|
211
|
+
const inside = (parent: Bounds, frames: Frame[], depth: number): void => {
|
|
212
|
+
const bounds = newBounds()
|
|
213
|
+
bounds.widthLimit =
|
|
214
|
+
parent.widthLimit! / Math.min(C4.boundaryInRow, frames.length)
|
|
215
|
+
for (const [i, fr] of frames.entries()) {
|
|
216
|
+
const b = fr.boundary
|
|
217
|
+
// Height of the boundary's heading, which the shapes start below.
|
|
218
|
+
let Y = 0
|
|
219
|
+
const labelY = Y + 8
|
|
220
|
+
Y = labelY + C4.boundaryLabelHeight
|
|
221
|
+
let typeY: number | undefined
|
|
222
|
+
const typeLine = b ? c4BoundaryTypeLine(b) : undefined
|
|
223
|
+
if (typeLine) {
|
|
224
|
+
typeY = Y + 5
|
|
225
|
+
Y = typeY + C4.boundaryTypeHeight
|
|
226
|
+
}
|
|
227
|
+
let descrY: number | undefined
|
|
228
|
+
if (b?.description) {
|
|
229
|
+
descrY = Y + 20
|
|
230
|
+
Y = descrY + C4.boundaryDescrHeight
|
|
231
|
+
}
|
|
232
|
+
// The unnamed root keeps a fixed heading height.
|
|
233
|
+
if (!b) Y = C4.rootHeadingHeight
|
|
234
|
+
|
|
235
|
+
if (i === 0 || i % C4.boundaryInRow === 0) {
|
|
236
|
+
const x = parent.startx! + C4.diagramMarginX
|
|
237
|
+
const y = parent.stopy! + C4.diagramMarginY + Y
|
|
238
|
+
setData(bounds, x, x, y, y)
|
|
239
|
+
} else {
|
|
240
|
+
const x =
|
|
241
|
+
bounds.stopx !== bounds.startx
|
|
242
|
+
? bounds.stopx! + C4.diagramMarginX
|
|
243
|
+
: bounds.startx!
|
|
244
|
+
setData(bounds, x, x, bounds.starty!, bounds.starty!)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (fr.elements.length > 0) placeShapes(bounds, fr.elements)
|
|
248
|
+
if (fr.children.length > 0) inside(bounds, fr.children, depth + 1)
|
|
249
|
+
|
|
250
|
+
if (b) {
|
|
251
|
+
let width = bounds.stopx! - bounds.startx!
|
|
252
|
+
let height = bounds.stopy! - bounds.starty!
|
|
253
|
+
// A boundary with nothing in it still gets a visible frame.
|
|
254
|
+
if (width === 0) width = C4.width
|
|
255
|
+
if (height === 0) height = Y + C4.shapeMargin
|
|
256
|
+
boundaries.push({
|
|
257
|
+
alias: b.alias,
|
|
258
|
+
label: b.label,
|
|
259
|
+
...(b.type ? { type: b.type } : {}),
|
|
260
|
+
...(b.description ? { description: b.description } : {}),
|
|
261
|
+
x: bounds.startx!,
|
|
262
|
+
y: bounds.starty!,
|
|
263
|
+
width,
|
|
264
|
+
height,
|
|
265
|
+
depth,
|
|
266
|
+
labelY,
|
|
267
|
+
...(typeY === undefined ? {} : { typeY }),
|
|
268
|
+
...(descrY === undefined ? {} : { descrY }),
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
parent.stopy = Math.max(bounds.stopy! + C4.shapeMargin, parent.stopy!)
|
|
272
|
+
parent.stopx = Math.max(bounds.stopx! + C4.shapeMargin, parent.stopx!)
|
|
273
|
+
maxX = Math.max(maxX, parent.stopx)
|
|
274
|
+
maxY = Math.max(maxY, parent.stopy)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const screen = newBounds()
|
|
279
|
+
setData(
|
|
280
|
+
screen,
|
|
281
|
+
C4.diagramMarginX,
|
|
282
|
+
C4.diagramMarginX,
|
|
283
|
+
C4.diagramMarginY,
|
|
284
|
+
C4.diagramMarginY,
|
|
285
|
+
)
|
|
286
|
+
screen.widthLimit = C4.screenWidth
|
|
287
|
+
// The unnamed root is not drawn, so its children are the depth-0 boundaries.
|
|
288
|
+
inside(screen, [root], -1)
|
|
289
|
+
return { elements, boundaries, maxX, maxY }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Where the line from a box's centre toward `toward` leaves the box. */
|
|
293
|
+
function rectIntersect(box: Placed, toward: Point): Point {
|
|
294
|
+
const cx = box.x + box.width / 2
|
|
295
|
+
const cy = box.y + box.height / 2
|
|
296
|
+
const dx = toward.x - cx
|
|
297
|
+
const dy = toward.y - cy
|
|
298
|
+
const w = box.width / 2
|
|
299
|
+
const h = box.height / 2
|
|
300
|
+
if (dx === 0 && dy === 0) return { x: cx, y: cy }
|
|
301
|
+
let sx: number
|
|
302
|
+
let sy: number
|
|
303
|
+
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
304
|
+
const half = dy < 0 ? -h : h
|
|
305
|
+
sx = (half * dx) / dy
|
|
306
|
+
sy = half
|
|
307
|
+
} else {
|
|
308
|
+
const half = dx < 0 ? -w : w
|
|
309
|
+
sx = half
|
|
310
|
+
sy = (half * dy) / dx
|
|
311
|
+
}
|
|
312
|
+
return { x: cx + sx, y: cy + sy }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Where the line from a person's centre toward `toward` leaves the figure,
|
|
317
|
+
* which is a round head over a pill, not the box around both (Mermaid clips
|
|
318
|
+
* a relationship against what is drawn).
|
|
319
|
+
*/
|
|
320
|
+
function personIntersect(box: Placed, toward: Point): Point {
|
|
321
|
+
const g = c4PersonGeometry(box.width)
|
|
322
|
+
const cx = box.x + box.width / 2
|
|
323
|
+
const cy = box.y + box.height / 2
|
|
324
|
+
const headY = box.y + g.headRadius
|
|
325
|
+
const pillTop = box.y + g.pillTop
|
|
326
|
+
const pillBottom = box.y + box.height
|
|
327
|
+
const inside = (x: number, y: number): boolean => {
|
|
328
|
+
if (Math.hypot(x - cx, y - headY) <= g.headRadius) return true
|
|
329
|
+
if (y < pillTop || y > pillBottom) return false
|
|
330
|
+
// Round the pill's ends: each is a circle of radius `rx` (clamped).
|
|
331
|
+
const r = Math.min(g.rx, (pillBottom - pillTop) / 2)
|
|
332
|
+
const left = box.x + r
|
|
333
|
+
const right = box.x + box.width - r
|
|
334
|
+
const nearY = Math.min(Math.max(y, pillTop + r), pillBottom - r)
|
|
335
|
+
const nearX = Math.min(Math.max(x, left), right)
|
|
336
|
+
return (
|
|
337
|
+
x >= box.x &&
|
|
338
|
+
x <= box.x + box.width &&
|
|
339
|
+
Math.hypot(x - nearX, y - nearY) <= r
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
const len = Math.hypot(toward.x - cx, toward.y - cy)
|
|
343
|
+
if (len === 0 || !inside(cx, cy)) return rectIntersect(box, toward)
|
|
344
|
+
const ux = (toward.x - cx) / len
|
|
345
|
+
const uy = (toward.y - cy) / len
|
|
346
|
+
const at = (t: number): boolean => inside(cx + ux * t, cy + uy * t)
|
|
347
|
+
let lo = 0
|
|
348
|
+
let hi = 0.5
|
|
349
|
+
while (at(hi) && hi < box.width + box.height) {
|
|
350
|
+
lo = hi
|
|
351
|
+
hi += 0.5
|
|
352
|
+
}
|
|
353
|
+
for (let i = 0; i < 30; i++) {
|
|
354
|
+
const mid = (lo + hi) / 2
|
|
355
|
+
if (at(mid)) lo = mid
|
|
356
|
+
else hi = mid
|
|
357
|
+
}
|
|
358
|
+
return { x: cx + ux * lo, y: cy + uy * lo }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function leave(box: Placed, toward: Point): Point {
|
|
362
|
+
return (box as Partial<MeasuredElement>).el?.kind === 'person'
|
|
363
|
+
? personIntersect(box, toward)
|
|
364
|
+
: rectIntersect(box, toward)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const centre = (b: Placed): Point => ({
|
|
368
|
+
x: b.x + b.width / 2,
|
|
369
|
+
y: b.y + b.height / 2,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
function routeRelationship(
|
|
373
|
+
rel: C4Relationship,
|
|
374
|
+
index: number,
|
|
375
|
+
boxes: Map<string, Placed>,
|
|
376
|
+
shift: number,
|
|
377
|
+
): PositionedC4Relationship {
|
|
378
|
+
const a = boxes.get(rel.from)!
|
|
379
|
+
const b = boxes.get(rel.to)!
|
|
380
|
+
const start = leave(a, centre(b))
|
|
381
|
+
const end = leave(b, centre(a))
|
|
382
|
+
const dx = end.x - start.x
|
|
383
|
+
const dy = end.y - start.y
|
|
384
|
+
const shifted = (p: Point): Point => ({ x: p.x, y: p.y + shift })
|
|
385
|
+
const out: PositionedC4Relationship = {
|
|
386
|
+
...rel,
|
|
387
|
+
points: [shifted(start), shifted(end)],
|
|
388
|
+
}
|
|
389
|
+
// Mermaid draws the first relationship straight and every other one as a
|
|
390
|
+
// quadratic curve bent toward the start of the chord.
|
|
391
|
+
if (index > 0) {
|
|
392
|
+
out.curve = shifted({
|
|
393
|
+
x: start.x + dx / 2 - dx / 4,
|
|
394
|
+
y: start.y + dy / 2,
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
if (c4RelLabelLines(rel).length > 0) {
|
|
398
|
+
const mid = {
|
|
399
|
+
x: Math.min(start.x, end.x) + Math.abs(dx) / 2,
|
|
400
|
+
y: Math.min(start.y, end.y) + Math.abs(dy) / 2,
|
|
401
|
+
}
|
|
402
|
+
// Mermaid hands its text routine the chord midpoint as the block's left
|
|
403
|
+
// edge and centres the text in the block's width, so a label starts at
|
|
404
|
+
// the midpoint and runs along the line rather than sitting on it.
|
|
405
|
+
const head = c4RelLabelLines(rel)[0]
|
|
406
|
+
const hasHead = rel.label !== '' || rel.index !== undefined
|
|
407
|
+
const labelWidth = hasHead
|
|
408
|
+
? c4TextWidth(head ?? '', C4.messageSize, 400)
|
|
409
|
+
: 0
|
|
410
|
+
out.labelPosition = shifted({ x: mid.x + labelWidth / 2, y: mid.y })
|
|
411
|
+
if (rel.technology) {
|
|
412
|
+
const techWidth = c4TextWidth(`[${rel.technology}]`, C4.messageSize, 400)
|
|
413
|
+
out.technologyX = mid.x + Math.max(labelWidth, techWidth) / 2
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return out
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Lay out a parsed C4 diagram the way Mermaid's C4 renderer does. */
|
|
420
|
+
export function layoutC4DiagramSync(
|
|
421
|
+
diagram: C4Diagram,
|
|
422
|
+
_options: RenderOptions = {},
|
|
423
|
+
): PositionedC4Diagram {
|
|
424
|
+
if (diagram.elements.length === 0 && diagram.boundaries.length === 0) {
|
|
425
|
+
const empty: PositionedC4Diagram = {
|
|
426
|
+
variant: diagram.variant,
|
|
427
|
+
width: 0,
|
|
428
|
+
height: 0,
|
|
429
|
+
elements: [],
|
|
430
|
+
boundaries: [],
|
|
431
|
+
relationships: [],
|
|
432
|
+
}
|
|
433
|
+
if (diagram.title) empty.title = diagram.title
|
|
434
|
+
return empty
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const layout = layoutFrames(buildFrames(diagram))
|
|
438
|
+
const titleExtra = diagram.title ? C4.titleExtra : 0
|
|
439
|
+
// Mermaid's viewBox starts `diagramMarginY + titleExtra` above the content.
|
|
440
|
+
const shift = C4.diagramMarginY + titleExtra
|
|
441
|
+
|
|
442
|
+
const elements: PositionedC4Element[] = diagram.elements.flatMap((el) => {
|
|
443
|
+
const m = layout.elements.get(el.alias)
|
|
444
|
+
if (!m) return []
|
|
445
|
+
return [
|
|
446
|
+
{
|
|
447
|
+
...el,
|
|
448
|
+
x: m.x,
|
|
449
|
+
y: m.y + shift,
|
|
450
|
+
width: m.width,
|
|
451
|
+
height: m.height,
|
|
452
|
+
nameLines: m.nameLines,
|
|
453
|
+
descriptionLines: m.descriptionLines,
|
|
454
|
+
},
|
|
455
|
+
]
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
// Outer boundaries first, so inner ones paint over them.
|
|
459
|
+
const boundaries = [...layout.boundaries]
|
|
460
|
+
.sort((p, q) => p.depth - q.depth)
|
|
461
|
+
.map((b) => ({ ...b, y: b.y + shift }))
|
|
462
|
+
|
|
463
|
+
const boxes = new Map<string, Placed>()
|
|
464
|
+
for (const m of layout.elements.values()) boxes.set(m.el.alias, m)
|
|
465
|
+
for (const b of layout.boundaries) boxes.set(b.alias, b)
|
|
466
|
+
const relationships = diagram.relationships.map((r, i) =>
|
|
467
|
+
routeRelationship(r, i, boxes, shift),
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
const width = layout.maxX + C4.diagramMarginX
|
|
471
|
+
const height = layout.maxY + C4.diagramMarginY + titleExtra
|
|
472
|
+
const positioned: PositionedC4Diagram = {
|
|
473
|
+
variant: diagram.variant,
|
|
474
|
+
width,
|
|
475
|
+
height,
|
|
476
|
+
elements,
|
|
477
|
+
boundaries,
|
|
478
|
+
relationships,
|
|
479
|
+
}
|
|
480
|
+
if (diagram.title) {
|
|
481
|
+
positioned.title = diagram.title
|
|
482
|
+
// Mermaid starts the title at `boxWidth / 2 - 4 * marginX`.
|
|
483
|
+
const boxWidth = layout.maxX - C4.diagramMarginX
|
|
484
|
+
positioned.titlePosition = {
|
|
485
|
+
x: boxWidth / 2 - 4 * C4.diagramMarginX,
|
|
486
|
+
y: C4.diagramMarginY + C4.diagramMarginY + shift,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return positioned
|
|
490
|
+
}
|