@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
package/src/c4/layout.ts
DELETED
|
@@ -1,680 +0,0 @@
|
|
|
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
|
-
c4CylinderCap,
|
|
21
|
-
c4PersonGeometry,
|
|
22
|
-
c4QueueCap,
|
|
23
|
-
c4ShapeSize,
|
|
24
|
-
c4TextWidth,
|
|
25
|
-
wrapToWidth,
|
|
26
|
-
} from './metrics.ts'
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// C4 layout, following Mermaid's own C4 renderer.
|
|
30
|
-
//
|
|
31
|
-
// Mermaid does not run a graph layout for C4. It places shapes in rows, in
|
|
32
|
-
// the order they are declared: up to `c4ShapeInRow` per row, boundaries
|
|
33
|
-
// `c4BoundaryInRow` to a row, each boundary wide enough for what it holds.
|
|
34
|
-
// Relationships are drawn afterwards between the shapes and never move
|
|
35
|
-
// anything, so `Rel_U/D/L/R` (and a render direction) have no effect on
|
|
36
|
-
// placement, as in Mermaid. This is a port of that algorithm
|
|
37
|
-
// (packages/mermaid/src/diagrams/c4/c4Renderer.ts), with text measured by
|
|
38
|
-
// estimate instead of in a browser.
|
|
39
|
-
// ============================================================================
|
|
40
|
-
|
|
41
|
-
/** Mermaid's `Bounds`: the extent of what has been placed in one boundary. */
|
|
42
|
-
interface Bounds {
|
|
43
|
-
startx?: number
|
|
44
|
-
stopx?: number
|
|
45
|
-
starty?: number
|
|
46
|
-
stopy?: number
|
|
47
|
-
widthLimit?: number
|
|
48
|
-
/** Where the next shape would go (Mermaid's `nextData`). */
|
|
49
|
-
next: {
|
|
50
|
-
startx?: number
|
|
51
|
-
stopx?: number
|
|
52
|
-
starty?: number
|
|
53
|
-
stopy?: number
|
|
54
|
-
cnt: number
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function newBounds(): Bounds {
|
|
59
|
-
return { next: { cnt: 0 } }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function setData(
|
|
63
|
-
b: Bounds,
|
|
64
|
-
startx: number,
|
|
65
|
-
stopx: number,
|
|
66
|
-
starty: number,
|
|
67
|
-
stopy: number,
|
|
68
|
-
): void {
|
|
69
|
-
b.next.startx = b.startx = startx
|
|
70
|
-
b.next.stopx = b.stopx = stopx
|
|
71
|
-
b.next.starty = b.starty = starty
|
|
72
|
-
b.next.stopy = b.stopy = stopy
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function grow(
|
|
76
|
-
obj: Bounds | Bounds['next'],
|
|
77
|
-
key: 'startx' | 'stopx' | 'starty' | 'stopy',
|
|
78
|
-
val: number,
|
|
79
|
-
pick: (a: number, b: number) => number,
|
|
80
|
-
): void {
|
|
81
|
-
const cur = obj[key]
|
|
82
|
-
obj[key] = cur === undefined ? val : pick(val, cur)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Mermaid's `Bounds.insert`: give a shape the next free slot. */
|
|
86
|
-
function insert(b: Bounds, shape: Placed): void {
|
|
87
|
-
b.next.cnt += 1
|
|
88
|
-
const margin = C4.shapeMargin
|
|
89
|
-
let startx =
|
|
90
|
-
b.next.startx === b.next.stopx
|
|
91
|
-
? b.next.stopx! + margin
|
|
92
|
-
: b.next.stopx! + margin * 2
|
|
93
|
-
let stopx = startx + shape.width
|
|
94
|
-
let starty = b.next.starty! + margin * 2
|
|
95
|
-
let stopy = starty + shape.height
|
|
96
|
-
if (
|
|
97
|
-
startx >= b.widthLimit! ||
|
|
98
|
-
stopx >= b.widthLimit! ||
|
|
99
|
-
b.next.cnt > C4.shapeInRow
|
|
100
|
-
) {
|
|
101
|
-
startx = b.next.startx! + margin + C4.nextLinePaddingX
|
|
102
|
-
starty = b.next.stopy! + margin * 2
|
|
103
|
-
b.next.stopx = stopx = startx + shape.width
|
|
104
|
-
b.next.starty = b.next.stopy
|
|
105
|
-
b.next.stopy = stopy = starty + shape.height
|
|
106
|
-
b.next.cnt = 1
|
|
107
|
-
}
|
|
108
|
-
shape.x = startx
|
|
109
|
-
shape.y = starty
|
|
110
|
-
grow(b, 'startx', startx, Math.min)
|
|
111
|
-
grow(b, 'starty', starty, Math.min)
|
|
112
|
-
grow(b, 'stopx', stopx, Math.max)
|
|
113
|
-
grow(b, 'stopy', stopy, Math.max)
|
|
114
|
-
grow(b.next, 'startx', startx, Math.min)
|
|
115
|
-
grow(b.next, 'starty', starty, Math.min)
|
|
116
|
-
grow(b.next, 'stopx', stopx, Math.max)
|
|
117
|
-
grow(b.next, 'stopy', stopy, Math.max)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
interface Placed {
|
|
121
|
-
x: number
|
|
122
|
-
y: number
|
|
123
|
-
width: number
|
|
124
|
-
height: number
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interface MeasuredElement extends Placed {
|
|
128
|
-
el: C4Element
|
|
129
|
-
nameLines: string[]
|
|
130
|
-
descriptionLines: string[]
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function measureElement(el: C4Element): MeasuredElement {
|
|
134
|
-
const nameLines = wrapToWidth(el.label, C4_TEXT_WIDTH, C4.nameSize)
|
|
135
|
-
const descriptionLines = el.description
|
|
136
|
-
? wrapToWidth(el.description, C4_TEXT_WIDTH, C4.descrSize)
|
|
137
|
-
: []
|
|
138
|
-
const textWidth = Math.max(
|
|
139
|
-
...nameLines.map((l) => c4TextWidth(l, C4.nameSize, 700)),
|
|
140
|
-
c4TextWidth(c4TypeLine(el), C4.typeSize, 400),
|
|
141
|
-
...descriptionLines.map((l) => c4TextWidth(l, C4.descrSize, 400)),
|
|
142
|
-
)
|
|
143
|
-
const size = c4ShapeSize(
|
|
144
|
-
el,
|
|
145
|
-
nameLines.length,
|
|
146
|
-
descriptionLines.length,
|
|
147
|
-
textWidth,
|
|
148
|
-
)
|
|
149
|
-
return { el, nameLines, descriptionLines, x: 0, y: 0, ...size }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// A boundary as the layout walks it: its own shapes, then its children.
|
|
153
|
-
interface Frame {
|
|
154
|
-
alias: string
|
|
155
|
-
boundary?: C4Boundary
|
|
156
|
-
elements: C4Element[]
|
|
157
|
-
children: Frame[]
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function buildFrames(diagram: C4Diagram): Frame {
|
|
161
|
-
const inBoundary = new Set<string>()
|
|
162
|
-
const byAlias = new Map(diagram.elements.map((e) => [e.alias, e]))
|
|
163
|
-
const frame = (b: C4Boundary): Frame => {
|
|
164
|
-
const elements = b.elementAliases.flatMap((a) => {
|
|
165
|
-
inBoundary.add(a)
|
|
166
|
-
const el = byAlias.get(a)
|
|
167
|
-
return el ? [el] : []
|
|
168
|
-
})
|
|
169
|
-
return {
|
|
170
|
-
alias: b.alias,
|
|
171
|
-
boundary: b,
|
|
172
|
-
elements,
|
|
173
|
-
children: b.children.map(frame),
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const children = diagram.boundaries.map(frame)
|
|
177
|
-
return {
|
|
178
|
-
alias: 'global',
|
|
179
|
-
elements: diagram.elements.filter((e) => !inBoundary.has(e.alias)),
|
|
180
|
-
children,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
interface LaidOutBoundary extends PositionedC4Boundary {
|
|
185
|
-
labelY: number
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
interface Layout {
|
|
189
|
-
elements: Map<string, MeasuredElement>
|
|
190
|
-
boundaries: LaidOutBoundary[]
|
|
191
|
-
maxX: number
|
|
192
|
-
maxY: number
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function layoutFrames(root: Frame): Layout {
|
|
196
|
-
const elements = new Map<string, MeasuredElement>()
|
|
197
|
-
const boundaries: LaidOutBoundary[] = []
|
|
198
|
-
let maxX: number = C4.diagramMarginX
|
|
199
|
-
let maxY: number = C4.diagramMarginY
|
|
200
|
-
|
|
201
|
-
// Mermaid's `drawC4ShapeArray`: measure, insert each, then bump the margin.
|
|
202
|
-
const placeShapes = (bounds: Bounds, els: C4Element[]): void => {
|
|
203
|
-
for (const el of els) {
|
|
204
|
-
const m = measureElement(el)
|
|
205
|
-
elements.set(el.alias, m)
|
|
206
|
-
insert(bounds, m)
|
|
207
|
-
}
|
|
208
|
-
bounds.stopx! += C4.shapeMargin
|
|
209
|
-
bounds.stopy! += C4.shapeMargin
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Mermaid's `drawInsideBoundary`.
|
|
213
|
-
const inside = (parent: Bounds, frames: Frame[], depth: number): void => {
|
|
214
|
-
const bounds = newBounds()
|
|
215
|
-
bounds.widthLimit =
|
|
216
|
-
parent.widthLimit! / Math.min(C4.boundaryInRow, frames.length)
|
|
217
|
-
for (const [i, fr] of frames.entries()) {
|
|
218
|
-
const b = fr.boundary
|
|
219
|
-
// Height of the boundary's heading, which the shapes start below.
|
|
220
|
-
let Y = 0
|
|
221
|
-
const labelY = Y + 8
|
|
222
|
-
Y = labelY + C4.boundaryLabelHeight
|
|
223
|
-
let typeY: number | undefined
|
|
224
|
-
const typeLine = b ? c4BoundaryTypeLine(b) : undefined
|
|
225
|
-
if (typeLine) {
|
|
226
|
-
typeY = Y + 5
|
|
227
|
-
Y = typeY + C4.boundaryTypeHeight
|
|
228
|
-
}
|
|
229
|
-
let descrY: number | undefined
|
|
230
|
-
if (b?.description) {
|
|
231
|
-
descrY = Y + 20
|
|
232
|
-
Y = descrY + C4.boundaryDescrHeight
|
|
233
|
-
}
|
|
234
|
-
// The unnamed root keeps a fixed heading height.
|
|
235
|
-
if (!b) Y = C4.rootHeadingHeight
|
|
236
|
-
|
|
237
|
-
if (i === 0 || i % C4.boundaryInRow === 0) {
|
|
238
|
-
const x = parent.startx! + C4.diagramMarginX
|
|
239
|
-
const y = parent.stopy! + C4.diagramMarginY + Y
|
|
240
|
-
setData(bounds, x, x, y, y)
|
|
241
|
-
} else {
|
|
242
|
-
const x =
|
|
243
|
-
bounds.stopx !== bounds.startx
|
|
244
|
-
? bounds.stopx! + C4.diagramMarginX
|
|
245
|
-
: bounds.startx!
|
|
246
|
-
setData(bounds, x, x, bounds.starty!, bounds.starty!)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (fr.elements.length > 0) placeShapes(bounds, fr.elements)
|
|
250
|
-
if (fr.children.length > 0) inside(bounds, fr.children, depth + 1)
|
|
251
|
-
|
|
252
|
-
if (b) {
|
|
253
|
-
let width = bounds.stopx! - bounds.startx!
|
|
254
|
-
let height = bounds.stopy! - bounds.starty!
|
|
255
|
-
// A boundary with nothing in it still gets a visible frame.
|
|
256
|
-
if (width === 0) width = C4.width
|
|
257
|
-
if (height === 0) height = Y + C4.shapeMargin
|
|
258
|
-
boundaries.push({
|
|
259
|
-
alias: b.alias,
|
|
260
|
-
label: b.label,
|
|
261
|
-
...(b.type ? { type: b.type } : {}),
|
|
262
|
-
...(b.description ? { description: b.description } : {}),
|
|
263
|
-
x: bounds.startx!,
|
|
264
|
-
y: bounds.starty!,
|
|
265
|
-
width,
|
|
266
|
-
height,
|
|
267
|
-
depth,
|
|
268
|
-
labelY,
|
|
269
|
-
...(typeY === undefined ? {} : { typeY }),
|
|
270
|
-
...(descrY === undefined ? {} : { descrY }),
|
|
271
|
-
})
|
|
272
|
-
}
|
|
273
|
-
parent.stopy = Math.max(bounds.stopy! + C4.shapeMargin, parent.stopy!)
|
|
274
|
-
parent.stopx = Math.max(bounds.stopx! + C4.shapeMargin, parent.stopx!)
|
|
275
|
-
maxX = Math.max(maxX, parent.stopx)
|
|
276
|
-
maxY = Math.max(maxY, parent.stopy)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const screen = newBounds()
|
|
281
|
-
setData(
|
|
282
|
-
screen,
|
|
283
|
-
C4.diagramMarginX,
|
|
284
|
-
C4.diagramMarginX,
|
|
285
|
-
C4.diagramMarginY,
|
|
286
|
-
C4.diagramMarginY,
|
|
287
|
-
)
|
|
288
|
-
screen.widthLimit = C4.screenWidth
|
|
289
|
-
// The unnamed root is not drawn, so its children are the depth-0 boundaries.
|
|
290
|
-
inside(screen, [root], -1)
|
|
291
|
-
return { elements, boundaries, maxX, maxY }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/** Where the line from a box's centre toward `toward` leaves the box. */
|
|
295
|
-
function rectIntersect(box: Placed, toward: Point): Point {
|
|
296
|
-
const cx = box.x + box.width / 2
|
|
297
|
-
const cy = box.y + box.height / 2
|
|
298
|
-
const dx = toward.x - cx
|
|
299
|
-
const dy = toward.y - cy
|
|
300
|
-
const w = box.width / 2
|
|
301
|
-
const h = box.height / 2
|
|
302
|
-
if (dx === 0 && dy === 0) return { x: cx, y: cy }
|
|
303
|
-
let sx: number
|
|
304
|
-
let sy: number
|
|
305
|
-
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
306
|
-
const half = dy < 0 ? -h : h
|
|
307
|
-
sx = (half * dx) / dy
|
|
308
|
-
sy = half
|
|
309
|
-
} else {
|
|
310
|
-
const half = dx < 0 ? -w : w
|
|
311
|
-
sx = half
|
|
312
|
-
sy = (half * dy) / dx
|
|
313
|
-
}
|
|
314
|
-
return { x: cx + sx, y: cy + sy }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Where a line from the centre of a person toward `toward` leaves the figure.
|
|
319
|
-
* Mermaid clips against the polygon it builds for the person (a round head
|
|
320
|
-
* over a rounded body, 24 points on the head and 12 on each body corner), so
|
|
321
|
-
* this builds the same polygon and returns the crossing nearest `toward`.
|
|
322
|
-
*/
|
|
323
|
-
function personIntersect(box: Placed, toward: Point): Point {
|
|
324
|
-
const g = c4PersonGeometry(box.width, box.height)
|
|
325
|
-
const cx = box.x + box.width / 2
|
|
326
|
-
const cy = box.y + box.height / 2
|
|
327
|
-
const w = box.width
|
|
328
|
-
const h = box.height
|
|
329
|
-
const headCentre = -h / 2 + g.headRadius
|
|
330
|
-
const bodyTop = -h / 2 + g.pillTop
|
|
331
|
-
const br = g.rx
|
|
332
|
-
// `n` points on the circle at (ox, oy), sweeping from angle `from` to `to`
|
|
333
|
-
// in degrees, with Mermaid's sign convention (`generateCirclePoints`).
|
|
334
|
-
const arc = (
|
|
335
|
-
ox: number,
|
|
336
|
-
oy: number,
|
|
337
|
-
r: number,
|
|
338
|
-
n: number,
|
|
339
|
-
from: number,
|
|
340
|
-
to: number,
|
|
341
|
-
): Point[] =>
|
|
342
|
-
Array.from({ length: n }, (_, i) => {
|
|
343
|
-
const a = ((from + (i * (to - from)) / (n - 1)) * Math.PI) / 180
|
|
344
|
-
return { x: ox - r * Math.cos(a), y: oy - r * Math.sin(a) }
|
|
345
|
-
})
|
|
346
|
-
// Where the head meets the body's top edge, below the head's centre.
|
|
347
|
-
const phi =
|
|
348
|
-
(Math.asin(Math.min(1, (bodyTop - headCentre) / g.headRadius)) * 180) /
|
|
349
|
-
Math.PI
|
|
350
|
-
const outline: Point[] = [
|
|
351
|
-
...arc(0, headCentre, g.headRadius, 24, 180 + phi, -phi),
|
|
352
|
-
...arc(-w / 2 + br, bodyTop + br, br, 12, 90, 0),
|
|
353
|
-
...arc(-w / 2 + br, h / 2 - br, br, 12, 360, 270),
|
|
354
|
-
...arc(w / 2 - br, h / 2 - br, br, 12, 270, 180),
|
|
355
|
-
...arc(w / 2 - br, bodyTop + br, br, 12, 180, 90),
|
|
356
|
-
]
|
|
357
|
-
return nearestCrossing(
|
|
358
|
-
{ x: cx, y: cy },
|
|
359
|
-
toward,
|
|
360
|
-
outline.map((p) => ({ x: cx + p.x, y: cy + p.y })),
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* The crossing of segment `from`-`toward` with an edge `q1`-`q2`, as Mermaid's
|
|
366
|
-
* (dagre's) `intersectLine` computes it: it adds half the determinant before
|
|
367
|
-
* dividing, which rounds the result by up to half a pixel.
|
|
368
|
-
*/
|
|
369
|
-
function lineCrossing(
|
|
370
|
-
from: Point,
|
|
371
|
-
toward: Point,
|
|
372
|
-
q1: Point,
|
|
373
|
-
q2: Point,
|
|
374
|
-
): Point | undefined {
|
|
375
|
-
const a1 = toward.y - from.y
|
|
376
|
-
const b1 = from.x - toward.x
|
|
377
|
-
const c1 = toward.x * from.y - from.x * toward.y
|
|
378
|
-
const r3 = a1 * q1.x + b1 * q1.y + c1
|
|
379
|
-
const r4 = a1 * q2.x + b1 * q2.y + c1
|
|
380
|
-
if (r3 !== 0 && r4 !== 0 && r3 * r4 > 0) return undefined
|
|
381
|
-
const a2 = q2.y - q1.y
|
|
382
|
-
const b2 = q1.x - q2.x
|
|
383
|
-
const c2 = q2.x * q1.y - q1.x * q2.y
|
|
384
|
-
const r1 = a2 * from.x + b2 * from.y + c2
|
|
385
|
-
const r2 = a2 * toward.x + b2 * toward.y + c2
|
|
386
|
-
if (r1 !== 0 && r2 !== 0 && r1 * r2 > 0) return undefined
|
|
387
|
-
const denom = a1 * b2 - a2 * b1
|
|
388
|
-
if (denom === 0) return undefined
|
|
389
|
-
const offset = Math.abs(denom / 2)
|
|
390
|
-
let num = b1 * c2 - b2 * c1
|
|
391
|
-
const x = num < 0 ? (num - offset) / denom : (num + offset) / denom
|
|
392
|
-
num = a2 * c1 - a1 * c2
|
|
393
|
-
const y = num < 0 ? (num - offset) / denom : (num + offset) / denom
|
|
394
|
-
return { x, y }
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/** The crossing of segment `from`-`toward` with the polygon nearest `toward`. */
|
|
398
|
-
function nearestCrossing(from: Point, toward: Point, poly: Point[]): Point {
|
|
399
|
-
let best: Point | undefined
|
|
400
|
-
let bestDist = Infinity
|
|
401
|
-
poly.forEach((p, i) => {
|
|
402
|
-
const hit = lineCrossing(from, toward, p, poly[(i + 1) % poly.length]!)
|
|
403
|
-
if (!hit) return
|
|
404
|
-
const d = Math.hypot(hit.x - toward.x, hit.y - toward.y)
|
|
405
|
-
if (d < bestDist) {
|
|
406
|
-
best = hit
|
|
407
|
-
bestDist = d
|
|
408
|
-
}
|
|
409
|
-
})
|
|
410
|
-
return best ?? from
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** Slack for deciding that a point sits exactly on a box edge. */
|
|
414
|
-
const EDGE = 1e-6
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* A cylinder (`SystemDb`, `ContainerDb`) clips like Mermaid's: the point on
|
|
418
|
-
* the bounding rectangle, then moved along the curve of the top or bottom cap
|
|
419
|
-
* where the line meets a cap rather than the straight side.
|
|
420
|
-
*/
|
|
421
|
-
function cylinderIntersect(box: Placed, toward: Point): Point {
|
|
422
|
-
const pos = rectIntersect(box, toward)
|
|
423
|
-
const cx = box.x + box.width / 2
|
|
424
|
-
const cy = box.y + box.height / 2
|
|
425
|
-
const { rx, ry } = c4CylinderCap(box.width)
|
|
426
|
-
const x = pos.x - cx
|
|
427
|
-
const onCap =
|
|
428
|
-
Math.abs(x) < box.width / 2 - EDGE ||
|
|
429
|
-
(Math.abs(Math.abs(x) - box.width / 2) <= EDGE &&
|
|
430
|
-
Math.abs(pos.y - cy) > box.height / 2 - ry)
|
|
431
|
-
if (rx === 0 || !onCap) return pos
|
|
432
|
-
const under = ry * ry * (1 - (x * x) / (rx * rx))
|
|
433
|
-
let drop = ry - (under > 0 ? Math.sqrt(under) : under)
|
|
434
|
-
if (toward.y - cy > 0) drop = -drop
|
|
435
|
-
return { x: pos.x, y: pos.y + drop }
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/** The same for a queue (`SystemQueue`, `ContainerQueue`), a cylinder on its side. */
|
|
439
|
-
function queueIntersect(box: Placed, toward: Point): Point {
|
|
440
|
-
const pos = rectIntersect(box, toward)
|
|
441
|
-
const cx = box.x + box.width / 2
|
|
442
|
-
const cy = box.y + box.height / 2
|
|
443
|
-
const { rx, ry } = c4QueueCap(box.height)
|
|
444
|
-
const y = pos.y - cy
|
|
445
|
-
const onCap =
|
|
446
|
-
Math.abs(y) < box.height / 2 - EDGE ||
|
|
447
|
-
(Math.abs(Math.abs(y) - box.height / 2) <= EDGE &&
|
|
448
|
-
Math.abs(pos.x - cx) > box.width / 2 - rx)
|
|
449
|
-
if (ry === 0 || !onCap) return pos
|
|
450
|
-
const under = rx * rx * (1 - (y * y) / (ry * ry))
|
|
451
|
-
let drop = rx - Math.sqrt(Math.abs(under))
|
|
452
|
-
if (toward.x - cx > 0) drop = -drop
|
|
453
|
-
return { x: pos.x + drop, y: pos.y }
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function leave(box: Placed, toward: Point): Point {
|
|
457
|
-
const el = (box as Partial<MeasuredElement>).el
|
|
458
|
-
if (el?.kind === 'person') return personIntersect(box, toward)
|
|
459
|
-
if (el?.shape === 'db') return cylinderIntersect(box, toward)
|
|
460
|
-
if (el?.shape === 'queue') return queueIntersect(box, toward)
|
|
461
|
-
return rectIntersect(box, toward)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const centre = (b: Placed): Point => ({
|
|
465
|
-
x: b.x + b.width / 2,
|
|
466
|
-
y: b.y + b.height / 2,
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
function routeRelationship(
|
|
470
|
-
rel: C4Relationship,
|
|
471
|
-
index: number,
|
|
472
|
-
boxes: Map<string, Placed>,
|
|
473
|
-
shift: number,
|
|
474
|
-
): PositionedC4Relationship {
|
|
475
|
-
const a = boxes.get(rel.from)!
|
|
476
|
-
const b = boxes.get(rel.to)!
|
|
477
|
-
const start = leave(a, centre(b))
|
|
478
|
-
const end = leave(b, centre(a))
|
|
479
|
-
const dx = end.x - start.x
|
|
480
|
-
const dy = end.y - start.y
|
|
481
|
-
const shifted = (p: Point): Point => ({ x: p.x, y: p.y + shift })
|
|
482
|
-
const out: PositionedC4Relationship = {
|
|
483
|
-
...rel,
|
|
484
|
-
points: [shifted(start), shifted(end)],
|
|
485
|
-
}
|
|
486
|
-
// Mermaid draws the first relationship straight and every other one as a
|
|
487
|
-
// quadratic curve bent toward the start of the chord.
|
|
488
|
-
if (index > 0) {
|
|
489
|
-
out.curve = shifted({
|
|
490
|
-
x: start.x + dx / 2 - dx / 4,
|
|
491
|
-
y: start.y + dy / 2,
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
if (c4RelLabelLines(rel).length > 0) {
|
|
495
|
-
const mid = {
|
|
496
|
-
x: Math.min(start.x, end.x) + Math.abs(dx) / 2,
|
|
497
|
-
y: Math.min(start.y, end.y) + Math.abs(dy) / 2,
|
|
498
|
-
}
|
|
499
|
-
// Mermaid hands its text routine the chord midpoint as the block's left
|
|
500
|
-
// edge and centres the text in the block's width, so a label starts at
|
|
501
|
-
// the midpoint and runs along the line rather than sitting on it.
|
|
502
|
-
const head = c4RelLabelLines(rel)[0]
|
|
503
|
-
const hasHead = rel.label !== '' || rel.index !== undefined
|
|
504
|
-
const labelWidth = hasHead
|
|
505
|
-
? c4TextWidth(head ?? '', C4.messageSize, 400)
|
|
506
|
-
: 0
|
|
507
|
-
out.labelPosition = shifted({ x: mid.x + labelWidth / 2, y: mid.y })
|
|
508
|
-
if (rel.technology) {
|
|
509
|
-
const techWidth = c4TextWidth(`[${rel.technology}]`, C4.messageSize, 400)
|
|
510
|
-
out.technologyX = mid.x + Math.max(labelWidth, techWidth) / 2
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return out
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** A text line's box: centre x, centre y, width and height. */
|
|
517
|
-
interface TextBox {
|
|
518
|
-
cx: number
|
|
519
|
-
cy: number
|
|
520
|
-
w: number
|
|
521
|
-
h: number
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** Clear space kept between a label and a shape or another label. */
|
|
525
|
-
const LABEL_CLEARANCE = 2
|
|
526
|
-
/** Extra space kept between a label and a shape, for the arrowhead. */
|
|
527
|
-
const SHAPE_CLEARANCE = 6
|
|
528
|
-
/** How far along the chord each trial position is from the last. */
|
|
529
|
-
const LABEL_STEP = 2
|
|
530
|
-
|
|
531
|
-
/** The boxes a relationship's label lines occupy, as the renderer draws them. */
|
|
532
|
-
function labelBoxes(rel: PositionedC4Relationship, dx: number): TextBox[] {
|
|
533
|
-
const pos = rel.labelPosition
|
|
534
|
-
if (!pos) return []
|
|
535
|
-
return c4RelLabelLines(rel).map((line, i, all) => {
|
|
536
|
-
const isTech = rel.technology !== undefined && i === all.length - 1
|
|
537
|
-
return {
|
|
538
|
-
cx:
|
|
539
|
-
(isTech && rel.technologyX !== undefined ? rel.technologyX : pos.x) +
|
|
540
|
-
dx,
|
|
541
|
-
cy: pos.y + (i === 0 ? 0 : C4.messageSize + 5),
|
|
542
|
-
w: c4TextWidth(line, C4.messageSize, 400),
|
|
543
|
-
h: C4.messageSize,
|
|
544
|
-
}
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const boxesOverlap = (a: TextBox, b: TextBox): boolean =>
|
|
549
|
-
Math.abs(a.cx - b.cx) < (a.w + b.w) / 2 + LABEL_CLEARANCE &&
|
|
550
|
-
Math.abs(a.cy - b.cy) < (a.h + b.h) / 2 + LABEL_CLEARANCE
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Mermaid starts a label at the chord's midpoint, so a label can land on a
|
|
554
|
-
* shape the line passes beside or through (#1290). Slide it along the chord,
|
|
555
|
-
* nearest the midpoint first, to the first spot clear of every shape and of
|
|
556
|
-
* the labels already placed; if there is none, leave it where Mermaid does.
|
|
557
|
-
*/
|
|
558
|
-
function clearLabel(
|
|
559
|
-
rel: PositionedC4Relationship,
|
|
560
|
-
shapes: TextBox[],
|
|
561
|
-
placed: TextBox[],
|
|
562
|
-
): void {
|
|
563
|
-
const start = rel.points[0]
|
|
564
|
-
const end = rel.points[rel.points.length - 1]
|
|
565
|
-
if (!rel.labelPosition || !start || !end) return
|
|
566
|
-
const dx = end.x - start.x
|
|
567
|
-
const dy = end.y - start.y
|
|
568
|
-
const len = Math.hypot(dx, dy)
|
|
569
|
-
if (len === 0) return
|
|
570
|
-
const ux = dx / len
|
|
571
|
-
const uy = dy / len
|
|
572
|
-
const clear = (t: number): TextBox[] | undefined => {
|
|
573
|
-
const boxes = labelBoxes(rel, t * ux).map((b) => ({
|
|
574
|
-
...b,
|
|
575
|
-
cy: b.cy + t * uy,
|
|
576
|
-
}))
|
|
577
|
-
const hit = boxes.some((b) =>
|
|
578
|
-
[...shapes, ...placed].some((o) => boxesOverlap(b, o)),
|
|
579
|
-
)
|
|
580
|
-
return hit ? undefined : boxes
|
|
581
|
-
}
|
|
582
|
-
for (let t = 0; t <= len / 2; t += LABEL_STEP) {
|
|
583
|
-
for (const s of t === 0 ? [1] : [1, -1]) {
|
|
584
|
-
const boxes = clear(s * t)
|
|
585
|
-
if (!boxes) continue
|
|
586
|
-
if (t !== 0) {
|
|
587
|
-
rel.labelPosition = {
|
|
588
|
-
x: rel.labelPosition.x + s * t * ux,
|
|
589
|
-
y: rel.labelPosition.y + s * t * uy,
|
|
590
|
-
}
|
|
591
|
-
if (rel.technologyX !== undefined) rel.technologyX += s * t * ux
|
|
592
|
-
}
|
|
593
|
-
placed.push(...boxes)
|
|
594
|
-
return
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
placed.push(...labelBoxes(rel, 0))
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** Lay out a parsed C4 diagram the way Mermaid's C4 renderer does. */
|
|
601
|
-
export function layoutC4DiagramSync(
|
|
602
|
-
diagram: C4Diagram,
|
|
603
|
-
_options: RenderOptions = {},
|
|
604
|
-
): PositionedC4Diagram {
|
|
605
|
-
if (diagram.elements.length === 0 && diagram.boundaries.length === 0) {
|
|
606
|
-
const empty: PositionedC4Diagram = {
|
|
607
|
-
variant: diagram.variant,
|
|
608
|
-
width: 0,
|
|
609
|
-
height: 0,
|
|
610
|
-
elements: [],
|
|
611
|
-
boundaries: [],
|
|
612
|
-
relationships: [],
|
|
613
|
-
}
|
|
614
|
-
if (diagram.title) empty.title = diagram.title
|
|
615
|
-
return empty
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const layout = layoutFrames(buildFrames(diagram))
|
|
619
|
-
const titleExtra = diagram.title ? C4.titleExtra : 0
|
|
620
|
-
// Mermaid's viewBox starts `diagramMarginY + titleExtra` above the content.
|
|
621
|
-
const shift = C4.diagramMarginY + titleExtra
|
|
622
|
-
|
|
623
|
-
const elements: PositionedC4Element[] = diagram.elements.flatMap((el) => {
|
|
624
|
-
const m = layout.elements.get(el.alias)
|
|
625
|
-
if (!m) return []
|
|
626
|
-
return [
|
|
627
|
-
{
|
|
628
|
-
...el,
|
|
629
|
-
x: m.x,
|
|
630
|
-
y: m.y + shift,
|
|
631
|
-
width: m.width,
|
|
632
|
-
height: m.height,
|
|
633
|
-
nameLines: m.nameLines,
|
|
634
|
-
descriptionLines: m.descriptionLines,
|
|
635
|
-
},
|
|
636
|
-
]
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
// Outer boundaries first, so inner ones paint over them.
|
|
640
|
-
const boundaries = [...layout.boundaries]
|
|
641
|
-
.sort((p, q) => p.depth - q.depth)
|
|
642
|
-
.map((b) => ({ ...b, y: b.y + shift }))
|
|
643
|
-
|
|
644
|
-
const boxes = new Map<string, Placed>()
|
|
645
|
-
for (const m of layout.elements.values()) boxes.set(m.el.alias, m)
|
|
646
|
-
for (const b of layout.boundaries) boxes.set(b.alias, b)
|
|
647
|
-
const relationships = diagram.relationships.map((r, i) =>
|
|
648
|
-
routeRelationship(r, i, boxes, shift),
|
|
649
|
-
)
|
|
650
|
-
const shapeBoxes: TextBox[] = elements.map((e) => ({
|
|
651
|
-
cx: e.x + e.width / 2,
|
|
652
|
-
cy: e.y + e.height / 2,
|
|
653
|
-
// Padded so a label also keeps off the arrowhead that ends a line on it.
|
|
654
|
-
w: e.width + 2 * SHAPE_CLEARANCE,
|
|
655
|
-
h: e.height + 2 * SHAPE_CLEARANCE,
|
|
656
|
-
}))
|
|
657
|
-
const placedLabels: TextBox[] = []
|
|
658
|
-
for (const r of relationships) clearLabel(r, shapeBoxes, placedLabels)
|
|
659
|
-
|
|
660
|
-
const width = layout.maxX + C4.diagramMarginX
|
|
661
|
-
const height = layout.maxY + C4.diagramMarginY + titleExtra
|
|
662
|
-
const positioned: PositionedC4Diagram = {
|
|
663
|
-
variant: diagram.variant,
|
|
664
|
-
width,
|
|
665
|
-
height,
|
|
666
|
-
elements,
|
|
667
|
-
boundaries,
|
|
668
|
-
relationships,
|
|
669
|
-
}
|
|
670
|
-
if (diagram.title) {
|
|
671
|
-
positioned.title = diagram.title
|
|
672
|
-
// Mermaid starts the title at `boxWidth / 2 - 4 * marginX`.
|
|
673
|
-
const boxWidth = layout.maxX - C4.diagramMarginX
|
|
674
|
-
positioned.titlePosition = {
|
|
675
|
-
x: boxWidth / 2 - 4 * C4.diagramMarginX,
|
|
676
|
-
y: C4.diagramMarginY + C4.diagramMarginY + shift,
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return positioned
|
|
680
|
-
}
|