@zombie-mermaid/svg-renderer 4.2.3 → 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 +890 -850
- package/dist/index.js.map +1 -1
- package/package.json +14 -6
- 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 -310
- 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/class/layout.ts
DELETED
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Class diagram layout engine (ELK.js).
|
|
3
|
-
*
|
|
4
|
-
* Each class box has 3 compartments:
|
|
5
|
-
* 1. Header (class name + optional annotation)
|
|
6
|
-
* 2. Attributes section
|
|
7
|
-
* 3. Methods section
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ElkNode, ElkExtendedEdge } from 'elkjs'
|
|
11
|
-
import type {
|
|
12
|
-
ClassDiagram,
|
|
13
|
-
ClassNode,
|
|
14
|
-
ClassMember,
|
|
15
|
-
PositionedClassDiagram,
|
|
16
|
-
PositionedClassNode,
|
|
17
|
-
PositionedClassNamespace,
|
|
18
|
-
PositionedClassNote,
|
|
19
|
-
PositionedClassRelationship,
|
|
20
|
-
} from '@zombie-mermaid/mermaid-parser'
|
|
21
|
-
import { formatClassMember } from '@zombie-mermaid/mermaid-parser'
|
|
22
|
-
import type { ClassRenderOptions } from '@zombie-mermaid/core'
|
|
23
|
-
import {
|
|
24
|
-
estimateTextWidth,
|
|
25
|
-
estimateMonoTextWidth,
|
|
26
|
-
FONT_WEIGHTS,
|
|
27
|
-
resolveFontSizes,
|
|
28
|
-
} from '../styles.ts'
|
|
29
|
-
import { elkLayoutSync } from '../elk-instance.ts'
|
|
30
|
-
import {
|
|
31
|
-
extractEdgePoints,
|
|
32
|
-
extractEdgeLabelPosition,
|
|
33
|
-
} from '../layout-engine/elk-adapter-utils.ts'
|
|
34
|
-
import {
|
|
35
|
-
ELK_DIRECTION_FALLBACK,
|
|
36
|
-
baseElkLayoutOptions,
|
|
37
|
-
buildElkEdge,
|
|
38
|
-
buildElkLeafNode,
|
|
39
|
-
directionToElk,
|
|
40
|
-
elkPadding,
|
|
41
|
-
} from '../layout-engine/elk-graph-builder.ts'
|
|
42
|
-
import { measureMultilineText, resolveNodeStyle } from '@zombie-mermaid/core'
|
|
43
|
-
|
|
44
|
-
/** Layout constants for class diagrams */
|
|
45
|
-
export const CLS = {
|
|
46
|
-
padding: 40,
|
|
47
|
-
boxPadX: 8,
|
|
48
|
-
headerBaseHeight: 32,
|
|
49
|
-
annotationHeight: 16,
|
|
50
|
-
memberRowHeight: 20,
|
|
51
|
-
sectionPadY: 8,
|
|
52
|
-
emptySectionHeight: 8,
|
|
53
|
-
minWidth: 120,
|
|
54
|
-
memberFontSize: 11,
|
|
55
|
-
memberFontWeight: 400,
|
|
56
|
-
nodeSpacing: 40,
|
|
57
|
-
layerSpacing: 60,
|
|
58
|
-
/** Horizontal / vertical padding inside a note box, around its text */
|
|
59
|
-
notePadX: 10,
|
|
60
|
-
notePadY: 6,
|
|
61
|
-
/** Space reserved above a namespace's classes for its title */
|
|
62
|
-
namespaceTitleHeight: 28,
|
|
63
|
-
/** Padding between a namespace frame and its classes (other three sides) */
|
|
64
|
-
namespacePad: 16,
|
|
65
|
-
namespaceTitleFontSize: 12,
|
|
66
|
-
} as const
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Layout id for the i-th note. A class id is a run of non-whitespace
|
|
70
|
-
* (`\S+` in the parser), so an id containing a space can never collide
|
|
71
|
-
* with one; ELK treats ids as opaque strings.
|
|
72
|
-
*/
|
|
73
|
-
export function classNoteId(index: number): string {
|
|
74
|
-
return `note ${index}`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Layout id for the dotted link joining note `noteId` to its class. */
|
|
78
|
-
function classNoteLinkId(noteId: string): string {
|
|
79
|
-
return `${noteId} link`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type ClassSizeMap = Map<
|
|
83
|
-
string,
|
|
84
|
-
{
|
|
85
|
-
width: number
|
|
86
|
-
height: number
|
|
87
|
-
headerHeight: number
|
|
88
|
-
attrHeight: number
|
|
89
|
-
methodHeight: number
|
|
90
|
-
}
|
|
91
|
-
>
|
|
92
|
-
|
|
93
|
-
/** Layout id for the i-th namespace; contains a space, so it can't match a class id. */
|
|
94
|
-
function classNamespaceId(index: number): string {
|
|
95
|
-
return `namespace ${index}`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A namespace that has at least one member class, ready for layout. */
|
|
99
|
-
interface LayoutNamespace {
|
|
100
|
-
id: string
|
|
101
|
-
name: string
|
|
102
|
-
classIds: string[]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Namespaces to draw: members that aren't classes of the diagram are dropped,
|
|
107
|
-
* a class claimed by two namespaces stays in the first (ELK nodes have one
|
|
108
|
-
* parent), and a namespace left with no members is skipped.
|
|
109
|
-
*/
|
|
110
|
-
function resolveNamespaces(diagram: ClassDiagram): LayoutNamespace[] {
|
|
111
|
-
const known = new Set(diagram.classes.map((c) => c.id))
|
|
112
|
-
const claimed = new Set<string>()
|
|
113
|
-
const out: LayoutNamespace[] = []
|
|
114
|
-
for (const [i, ns] of diagram.namespaces.entries()) {
|
|
115
|
-
const classIds: string[] = []
|
|
116
|
-
for (const id of ns.classIds) {
|
|
117
|
-
if (!known.has(id) || claimed.has(id)) continue
|
|
118
|
-
claimed.add(id)
|
|
119
|
-
classIds.push(id)
|
|
120
|
-
}
|
|
121
|
-
if (classIds.length > 0) {
|
|
122
|
-
out.push({ id: classNamespaceId(i), name: ns.name, classIds })
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return out
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Size of each note box, keyed by its layout id. */
|
|
129
|
-
type NoteSizeMap = Map<string, { width: number; height: number }>
|
|
130
|
-
|
|
131
|
-
/** Build ELK graph and size map from a class diagram. */
|
|
132
|
-
function buildClassElkGraph(
|
|
133
|
-
diagram: ClassDiagram,
|
|
134
|
-
options: ClassRenderOptions,
|
|
135
|
-
): {
|
|
136
|
-
elkGraph: ElkNode
|
|
137
|
-
classSizes: ClassSizeMap
|
|
138
|
-
noteSizes: NoteSizeMap
|
|
139
|
-
namespaces: LayoutNamespace[]
|
|
140
|
-
} {
|
|
141
|
-
const classSizes: ClassSizeMap = new Map()
|
|
142
|
-
const noteSizes: NoteSizeMap = new Map()
|
|
143
|
-
const fontSizes = resolveFontSizes(options.fontSizes)
|
|
144
|
-
|
|
145
|
-
for (const cls of diagram.classes) {
|
|
146
|
-
const headerHeight = cls.annotation
|
|
147
|
-
? CLS.headerBaseHeight + CLS.annotationHeight
|
|
148
|
-
: CLS.headerBaseHeight
|
|
149
|
-
|
|
150
|
-
const attrHeight =
|
|
151
|
-
cls.attributes.length > 0
|
|
152
|
-
? cls.attributes.length * CLS.memberRowHeight + CLS.sectionPadY
|
|
153
|
-
: CLS.emptySectionHeight
|
|
154
|
-
|
|
155
|
-
const methodHeight =
|
|
156
|
-
cls.methods.length > 0
|
|
157
|
-
? cls.methods.length * CLS.memberRowHeight + CLS.sectionPadY
|
|
158
|
-
: CLS.emptySectionHeight
|
|
159
|
-
|
|
160
|
-
const headerTextW = estimateTextWidth(
|
|
161
|
-
cls.label,
|
|
162
|
-
fontSizes.nodeLabel,
|
|
163
|
-
FONT_WEIGHTS.nodeLabel,
|
|
164
|
-
)
|
|
165
|
-
const maxAttrW = maxMemberWidth(cls.attributes)
|
|
166
|
-
const maxMethodW = maxMemberWidth(cls.methods)
|
|
167
|
-
const width = Math.max(
|
|
168
|
-
CLS.minWidth,
|
|
169
|
-
headerTextW + CLS.boxPadX * 2,
|
|
170
|
-
maxAttrW + CLS.boxPadX * 2,
|
|
171
|
-
maxMethodW + CLS.boxPadX * 2,
|
|
172
|
-
)
|
|
173
|
-
const height = headerHeight + attrHeight + methodHeight
|
|
174
|
-
|
|
175
|
-
classSizes.set(cls.id, {
|
|
176
|
-
width,
|
|
177
|
-
height,
|
|
178
|
-
headerHeight,
|
|
179
|
-
attrHeight,
|
|
180
|
-
methodHeight,
|
|
181
|
-
})
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Iterate classSizes directly (populated above, in diagram.classes order)
|
|
185
|
-
// rather than looking each class back up by id — sidesteps needing an
|
|
186
|
-
// assertion or invariant check for a lookup that can't actually miss.
|
|
187
|
-
const namespaces = resolveNamespaces(diagram)
|
|
188
|
-
const namespaceOf = new Map<string, string>()
|
|
189
|
-
for (const ns of namespaces) {
|
|
190
|
-
for (const id of ns.classIds) namespaceOf.set(id, ns.id)
|
|
191
|
-
}
|
|
192
|
-
const namespaceNodes = new Map<string, ElkNode>()
|
|
193
|
-
const children: ElkNode[] = []
|
|
194
|
-
for (const ns of namespaces) {
|
|
195
|
-
// Wide enough for the title; ELK grows it to fit the classes inside.
|
|
196
|
-
const titleW =
|
|
197
|
-
estimateTextWidth(
|
|
198
|
-
ns.name,
|
|
199
|
-
CLS.namespaceTitleFontSize,
|
|
200
|
-
FONT_WEIGHTS.groupHeader,
|
|
201
|
-
) +
|
|
202
|
-
CLS.namespacePad * 2
|
|
203
|
-
const node: ElkNode = {
|
|
204
|
-
id: ns.id,
|
|
205
|
-
children: [],
|
|
206
|
-
layoutOptions: {
|
|
207
|
-
'elk.padding': elkPadding({
|
|
208
|
-
top: CLS.namespaceTitleHeight + CLS.namespacePad / 2,
|
|
209
|
-
left: CLS.namespacePad,
|
|
210
|
-
bottom: CLS.namespacePad,
|
|
211
|
-
right: CLS.namespacePad,
|
|
212
|
-
}),
|
|
213
|
-
'elk.nodeSize.constraints': 'MINIMUM_SIZE',
|
|
214
|
-
'elk.nodeSize.minimum': `(${Math.ceil(titleW)}, 0)`,
|
|
215
|
-
},
|
|
216
|
-
}
|
|
217
|
-
namespaceNodes.set(ns.id, node)
|
|
218
|
-
children.push(node)
|
|
219
|
-
}
|
|
220
|
-
for (const [id, size] of classSizes) {
|
|
221
|
-
const leaf = buildElkLeafNode(id, size)
|
|
222
|
-
const parent = namespaceNodes.get(namespaceOf.get(id) ?? '')
|
|
223
|
-
if (parent?.children) parent.children.push(leaf)
|
|
224
|
-
else children.push(leaf)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Class edge labels carry no per-label layout options — placement is set
|
|
228
|
-
// once on the root graph below (`elk.edgeLabels.placement: CENTER`).
|
|
229
|
-
const labelStyle = { fontSize: fontSizes.edgeLabel }
|
|
230
|
-
|
|
231
|
-
const edges: ElkExtendedEdge[] = []
|
|
232
|
-
for (const [i, rel] of diagram.relationships.entries()) {
|
|
233
|
-
edges.push(
|
|
234
|
-
buildElkEdge({
|
|
235
|
-
id: `e${i}`,
|
|
236
|
-
source: rel.from,
|
|
237
|
-
target: rel.to,
|
|
238
|
-
label: rel.label,
|
|
239
|
-
labelStyle,
|
|
240
|
-
}),
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Notes. Mirrors Mermaid's classDb.getData(): every note is a node of its
|
|
245
|
-
// own, and `note for X` adds an arrowless dotted edge note→class so the
|
|
246
|
-
// layout keeps the two adjacent (with a DOWN layout the note lands above
|
|
247
|
-
// its class, as it does in Mermaid's TB rendering). The link edges go
|
|
248
|
-
// after the relationship edges so relationship indices stay positional.
|
|
249
|
-
const classIds = new Set(classSizes.keys())
|
|
250
|
-
for (const [i, note] of diagram.notes.entries()) {
|
|
251
|
-
const id = classNoteId(i)
|
|
252
|
-
const metrics = measureMultilineText(
|
|
253
|
-
note.text,
|
|
254
|
-
fontSizes.edgeLabel,
|
|
255
|
-
FONT_WEIGHTS.edgeLabel,
|
|
256
|
-
)
|
|
257
|
-
const size = {
|
|
258
|
-
width: metrics.width + CLS.notePadX * 2,
|
|
259
|
-
height: metrics.height + CLS.notePadY * 2,
|
|
260
|
-
}
|
|
261
|
-
noteSizes.set(id, size)
|
|
262
|
-
children.push(buildElkLeafNode(id, size))
|
|
263
|
-
if (note.forClass !== undefined && classIds.has(note.forClass)) {
|
|
264
|
-
edges.push(
|
|
265
|
-
buildElkEdge({
|
|
266
|
-
id: classNoteLinkId(id),
|
|
267
|
-
source: id,
|
|
268
|
-
target: note.forClass,
|
|
269
|
-
labelStyle,
|
|
270
|
-
}),
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const elkGraph: ElkNode = {
|
|
276
|
-
id: 'root',
|
|
277
|
-
layoutOptions: {
|
|
278
|
-
...baseElkLayoutOptions({
|
|
279
|
-
// Class diagrams have no `direction` concept — they always lay out
|
|
280
|
-
// top-down. See ELK_DIRECTION_FALLBACK for why that default is
|
|
281
|
-
// per-diagram-type rather than shared with ER's.
|
|
282
|
-
direction: directionToElk(undefined, ELK_DIRECTION_FALLBACK.class),
|
|
283
|
-
nodeSpacing: CLS.nodeSpacing,
|
|
284
|
-
layerSpacing: CLS.layerSpacing,
|
|
285
|
-
padding: CLS.padding,
|
|
286
|
-
}),
|
|
287
|
-
'elk.edgeLabels.placement': 'CENTER',
|
|
288
|
-
// Relationships are declared at the root and may cross namespace
|
|
289
|
-
// frames; INCLUDE_CHILDREN routes them in one pass through the nesting.
|
|
290
|
-
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
291
|
-
// Mermaid lays disconnected components out left to right in
|
|
292
|
-
// declaration order (#1249). ELK's default packs components by size
|
|
293
|
-
// into rows, which reorders them; laying them out as one graph with
|
|
294
|
-
// model order as the tie-break keeps source order.
|
|
295
|
-
'elk.separateConnectedComponents': 'false',
|
|
296
|
-
'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
|
|
297
|
-
},
|
|
298
|
-
children,
|
|
299
|
-
edges,
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return { elkGraph, classSizes, noteSizes, namespaces }
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/** Extract positioned classes, relationships, and notes from ELK result. */
|
|
306
|
-
function extractClassLayout(
|
|
307
|
-
result: ElkNode,
|
|
308
|
-
diagram: ClassDiagram,
|
|
309
|
-
classSizes: ClassSizeMap,
|
|
310
|
-
noteSizes: NoteSizeMap,
|
|
311
|
-
namespaces: LayoutNamespace[],
|
|
312
|
-
): PositionedClassDiagram {
|
|
313
|
-
const classLookup = new Map<string, ClassNode>()
|
|
314
|
-
for (const cls of diagram.classes) classLookup.set(cls.id, cls)
|
|
315
|
-
|
|
316
|
-
// ELK reports a nested node relative to its parent. Namespaces are one
|
|
317
|
-
// level deep, so flatten each member into diagram coordinates.
|
|
318
|
-
const namespaceIds = new Set(namespaces.map((ns) => ns.id))
|
|
319
|
-
const namespaceOffsets = new Map<string, { x: number; y: number }>()
|
|
320
|
-
const flatChildren: ElkNode[] = []
|
|
321
|
-
for (const child of result.children ?? []) {
|
|
322
|
-
if (namespaceIds.has(child.id)) {
|
|
323
|
-
const ox = child.x ?? 0
|
|
324
|
-
const oy = child.y ?? 0
|
|
325
|
-
namespaceOffsets.set(child.id, { x: ox, y: oy })
|
|
326
|
-
for (const inner of child.children ?? []) {
|
|
327
|
-
flatChildren.push({
|
|
328
|
-
...inner,
|
|
329
|
-
x: (inner.x ?? 0) + ox,
|
|
330
|
-
y: (inner.y ?? 0) + oy,
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
flatChildren.push(child)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// Under INCLUDE_CHILDREN an edge's points are relative to its container
|
|
338
|
-
// (the lowest common ancestor of its endpoints): a namespace for an edge
|
|
339
|
-
// inside one, the root otherwise.
|
|
340
|
-
const edgeOffset = (edge: ElkExtendedEdge): { x: number; y: number } => {
|
|
341
|
-
const container = (edge as { container?: string }).container
|
|
342
|
-
return (container && namespaceOffsets.get(container)) || { x: 0, y: 0 }
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const positionedNamespaces: PositionedClassNamespace[] = []
|
|
346
|
-
for (const ns of namespaces) {
|
|
347
|
-
const node = (result.children ?? []).find((c) => c.id === ns.id)
|
|
348
|
-
if (!node) continue
|
|
349
|
-
positionedNamespaces.push({
|
|
350
|
-
name: ns.name,
|
|
351
|
-
classIds: ns.classIds,
|
|
352
|
-
x: node.x ?? 0,
|
|
353
|
-
y: node.y ?? 0,
|
|
354
|
-
width: node.width ?? 0,
|
|
355
|
-
height: node.height ?? 0,
|
|
356
|
-
})
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const positionedClasses: PositionedClassNode[] = []
|
|
360
|
-
for (const child of flatChildren) {
|
|
361
|
-
const cls = classLookup.get(child.id)
|
|
362
|
-
if (cls) {
|
|
363
|
-
const size = classSizes.get(cls.id)
|
|
364
|
-
if (!size) {
|
|
365
|
-
// Unreachable — classSizes is populated for every diagram.classes
|
|
366
|
-
// entry, and classLookup/cls.id come from that same list.
|
|
367
|
-
/* v8 ignore next */
|
|
368
|
-
throw new Error(`Missing computed size for class "${cls.id}"`)
|
|
369
|
-
}
|
|
370
|
-
positionedClasses.push({
|
|
371
|
-
id: cls.id,
|
|
372
|
-
label: cls.label,
|
|
373
|
-
annotation: cls.annotation,
|
|
374
|
-
attributes: cls.attributes,
|
|
375
|
-
methods: cls.methods,
|
|
376
|
-
x: child.x ?? 0,
|
|
377
|
-
y: child.y ?? 0,
|
|
378
|
-
width: child.width ?? size.width,
|
|
379
|
-
height: child.height ?? size.height,
|
|
380
|
-
headerHeight: size.headerHeight,
|
|
381
|
-
attrHeight: size.attrHeight,
|
|
382
|
-
methodHeight: size.methodHeight,
|
|
383
|
-
interaction: diagram.interactions.get(cls.id),
|
|
384
|
-
// Same cascade the flowchart layout applies (src/layout-engine/
|
|
385
|
-
// from-elk.ts): classDef default → assigned class → `style`.
|
|
386
|
-
inlineStyle: resolveNodeStyle(cls.id, diagram),
|
|
387
|
-
// Kept separately from inlineStyle so the class name still reaches
|
|
388
|
-
// the SVG `class` attribute when it has no matching classDef.
|
|
389
|
-
className: diagram.classAssignments.get(cls.id),
|
|
390
|
-
})
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const relationships: PositionedClassRelationship[] = []
|
|
395
|
-
const elkEdges = result.edges ?? []
|
|
396
|
-
// The first diagram.relationships.length edges are the relationships, in
|
|
397
|
-
// order — buildClassElkGraph creates exactly one ELK edge per relationship
|
|
398
|
-
// before appending any note links.
|
|
399
|
-
for (const [i, rel] of diagram.relationships.entries()) {
|
|
400
|
-
const elkEdge = elkEdges[i]
|
|
401
|
-
if (!elkEdge) {
|
|
402
|
-
// Unreachable — ELK returns every edge it was given.
|
|
403
|
-
/* v8 ignore next */
|
|
404
|
-
throw new Error(`Missing ELK edge for relationship ${i}`)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const off = edgeOffset(elkEdge)
|
|
408
|
-
const points = extractEdgePoints(elkEdge, off.x, off.y)
|
|
409
|
-
const labelPosition = extractEdgeLabelPosition(elkEdge, off.x, off.y)
|
|
410
|
-
|
|
411
|
-
relationships.push({
|
|
412
|
-
from: rel.from,
|
|
413
|
-
to: rel.to,
|
|
414
|
-
type: rel.type,
|
|
415
|
-
markerAt: rel.markerAt,
|
|
416
|
-
label: rel.label,
|
|
417
|
-
fromCardinality: rel.fromCardinality,
|
|
418
|
-
toCardinality: rel.toCardinality,
|
|
419
|
-
points,
|
|
420
|
-
labelPosition,
|
|
421
|
-
})
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Note links are matched by id rather than position — only notes whose
|
|
425
|
-
// class exists got an edge, so their count isn't derivable from the note list.
|
|
426
|
-
const linkEdges = new Map<string, ElkExtendedEdge>()
|
|
427
|
-
for (const elkEdge of elkEdges.slice(diagram.relationships.length)) {
|
|
428
|
-
linkEdges.set(elkEdge.id, elkEdge)
|
|
429
|
-
}
|
|
430
|
-
const childById = new Map<string, ElkNode>()
|
|
431
|
-
for (const child of flatChildren) childById.set(child.id, child)
|
|
432
|
-
|
|
433
|
-
const notes: PositionedClassNote[] = []
|
|
434
|
-
for (const [i, note] of diagram.notes.entries()) {
|
|
435
|
-
const id = classNoteId(i)
|
|
436
|
-
const child = childById.get(id)
|
|
437
|
-
const size = noteSizes.get(id)
|
|
438
|
-
if (!child || !size) {
|
|
439
|
-
// Unreachable — a child and a size are recorded for every note.
|
|
440
|
-
/* v8 ignore next */
|
|
441
|
-
throw new Error(`Missing layout for note ${i}`)
|
|
442
|
-
}
|
|
443
|
-
const link = linkEdges.get(classNoteLinkId(id))
|
|
444
|
-
notes.push({
|
|
445
|
-
id,
|
|
446
|
-
text: note.text,
|
|
447
|
-
...(link && note.forClass !== undefined
|
|
448
|
-
? { forClass: note.forClass }
|
|
449
|
-
: {}),
|
|
450
|
-
x: child.x ?? 0,
|
|
451
|
-
y: child.y ?? 0,
|
|
452
|
-
width: child.width ?? size.width,
|
|
453
|
-
height: child.height ?? size.height,
|
|
454
|
-
...(link
|
|
455
|
-
? {
|
|
456
|
-
linkPoints: extractEdgePoints(
|
|
457
|
-
link,
|
|
458
|
-
edgeOffset(link).x,
|
|
459
|
-
edgeOffset(link).y,
|
|
460
|
-
),
|
|
461
|
-
}
|
|
462
|
-
: {}),
|
|
463
|
-
})
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
width: result.width ?? 600,
|
|
468
|
-
height: result.height ?? 400,
|
|
469
|
-
classes: positionedClasses,
|
|
470
|
-
relationships,
|
|
471
|
-
notes,
|
|
472
|
-
namespaces: positionedNamespaces,
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Lay out a parsed class diagram using ELK.js (synchronous).
|
|
478
|
-
*/
|
|
479
|
-
export function layoutClassDiagramSync(
|
|
480
|
-
diagram: ClassDiagram,
|
|
481
|
-
options: ClassRenderOptions = {},
|
|
482
|
-
): PositionedClassDiagram {
|
|
483
|
-
if (diagram.classes.length === 0 && diagram.notes.length === 0) {
|
|
484
|
-
return {
|
|
485
|
-
width: 0,
|
|
486
|
-
height: 0,
|
|
487
|
-
classes: [],
|
|
488
|
-
relationships: [],
|
|
489
|
-
notes: [],
|
|
490
|
-
namespaces: [],
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const { elkGraph, classSizes, noteSizes, namespaces } = buildClassElkGraph(
|
|
495
|
-
diagram,
|
|
496
|
-
options,
|
|
497
|
-
)
|
|
498
|
-
const result = elkLayoutSync(elkGraph, options.layoutCache)
|
|
499
|
-
return extractClassLayout(result, diagram, classSizes, noteSizes, namespaces)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Width of a member line sized for the worst-case glyph advance.
|
|
504
|
-
*
|
|
505
|
-
* The mono face advances 0.6em per glyph (6.6px at the 11px member size), but
|
|
506
|
-
* browsers that don't position glyphs at subpixel offsets (headless Chromium
|
|
507
|
-
* on Linux, notably) round each advance to a whole pixel, so a long member
|
|
508
|
-
* draws up to 0.4px per character wider than `estimateMonoTextWidth` says and
|
|
509
|
-
* ran into the box's right border (#1238). Rounding the advance up here keeps
|
|
510
|
-
* the full `CLS.boxPadX` of right padding whichever way the font is rasterized.
|
|
511
|
-
*/
|
|
512
|
-
function memberTextWidth(text: string): number {
|
|
513
|
-
return text.length * Math.ceil(estimateMonoTextWidth('M', CLS.memberFontSize))
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/** Calculate the max width of a list of class members (uses mono metrics) */
|
|
517
|
-
function maxMemberWidth(members: ClassMember[]): number {
|
|
518
|
-
if (members.length === 0) return 0
|
|
519
|
-
let maxW = 0
|
|
520
|
-
for (const m of members) {
|
|
521
|
-
const w = memberTextWidth(formatClassMember(m))
|
|
522
|
-
if (w > maxW) maxW = w
|
|
523
|
-
}
|
|
524
|
-
return maxW
|
|
525
|
-
}
|