@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/class/renderer.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
escapeXml as escapeXmlUtil,
|
|
17
17
|
escapeAttr,
|
|
18
18
|
safeHref,
|
|
19
|
+
f,
|
|
19
20
|
} from '@zombie-mermaid/core'
|
|
20
21
|
import { withDataSrc } from '../renderer.ts'
|
|
21
22
|
import {
|
|
@@ -161,13 +162,14 @@ function relationshipMarkerDefs(): string {
|
|
|
161
162
|
` <marker id="cls-inherit" markerWidth="12" markerHeight="10" refX="12" refY="5" orient="auto-start-reverse">` +
|
|
162
163
|
`\n <polygon points="0 0, 12 5, 0 10" fill="var(--bg)" stroke="var(--_arrow)" stroke-width="1.5" />` +
|
|
163
164
|
`\n </marker>` +
|
|
164
|
-
// Filled diamond (composition) —
|
|
165
|
-
|
|
166
|
-
`\n
|
|
165
|
+
// Filled diamond (composition) — tip sits on the endpoint, body runs along the line;
|
|
166
|
+
// the polygon is inset so the stroke isn't clipped by the marker viewport
|
|
167
|
+
`\n <marker id="cls-composition" markerWidth="18" markerHeight="14" refX="16.5" refY="7" orient="auto-start-reverse">` +
|
|
168
|
+
`\n <polygon points="9 1.5, 16.5 7, 9 12.5, 1.5 7" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="1" />` +
|
|
167
169
|
`\n </marker>` +
|
|
168
|
-
// Hollow diamond (aggregation) —
|
|
169
|
-
`\n <marker id="cls-aggregation" markerWidth="
|
|
170
|
-
`\n <polygon points="
|
|
170
|
+
// Hollow diamond (aggregation) — tip sits on the endpoint, body runs along the line
|
|
171
|
+
`\n <marker id="cls-aggregation" markerWidth="18" markerHeight="14" refX="16.5" refY="7" orient="auto-start-reverse">` +
|
|
172
|
+
`\n <polygon points="9 1.5, 16.5 7, 9 12.5, 1.5 7" fill="var(--bg)" stroke="var(--_arrow)" stroke-width="1.5" />` +
|
|
171
173
|
`\n </marker>` +
|
|
172
174
|
// Open arrow (association, dependency)
|
|
173
175
|
`\n <marker id="cls-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto-start-reverse">` +
|
|
@@ -230,20 +232,20 @@ function renderClassBox(
|
|
|
230
232
|
// data-label: class name
|
|
231
233
|
// data-annotation: stereotype (interface, abstract, etc.)
|
|
232
234
|
const annotationAttr = cls.annotation
|
|
233
|
-
? ` data-annotation="${escapeAttr(cls.annotation)}"`
|
|
235
|
+
? f` data-annotation="${escapeAttr(cls.annotation)}"`
|
|
234
236
|
: ''
|
|
235
237
|
// A style class from `cssClass`/`class A name`/`:::name` is emitted onto
|
|
236
238
|
// the group so external CSS can target it, after the same allowlist the
|
|
237
239
|
// flowchart renderer applies.
|
|
238
240
|
const safeClassName = sanitizeClassName(cls.className)
|
|
239
241
|
const groupAttrs = [
|
|
240
|
-
`class="${safeClassName ? `class-node ${safeClassName}` : 'class-node'}"`,
|
|
241
|
-
`data-id="${escapeAttr(cls.id)}"`,
|
|
242
|
-
`data-label="${escapeAttr(cls.label)}"`,
|
|
242
|
+
f`class="${safeClassName ? f`class-node ${safeClassName}` : 'class-node'}"`,
|
|
243
|
+
f`data-id="${escapeAttr(cls.id)}"`,
|
|
244
|
+
f`data-label="${escapeAttr(cls.label)}"`,
|
|
243
245
|
]
|
|
244
246
|
// `click ClassName call fn()` is parsed, never invoked, and never written
|
|
245
247
|
// into the markup — see renderNode() in packages/svg-renderer/src/renderer.ts for the rationale.
|
|
246
|
-
parts.push(`<g ${groupAttrs.join(' ')}${annotationAttr}>`)
|
|
248
|
+
parts.push(f`<g ${groupAttrs.join(' ')}${annotationAttr}>`)
|
|
247
249
|
|
|
248
250
|
// An href becomes a real SVG link, which needs no script to work.
|
|
249
251
|
// `interactivity: 'none'` strips it — a link is meaningless in
|
|
@@ -251,28 +253,28 @@ function renderClassBox(
|
|
|
251
253
|
const href = linksEnabled ? safeHref(interaction?.href) : undefined
|
|
252
254
|
if (href) {
|
|
253
255
|
const targetAttr = interaction?.target
|
|
254
|
-
? ` target="${escapeAttr(interaction.target)}"`
|
|
256
|
+
? f` target="${escapeAttr(interaction.target)}"`
|
|
255
257
|
: ''
|
|
256
|
-
parts.push(` <a href="${escapeAttr(href)}"${targetAttr}>`)
|
|
258
|
+
parts.push(f` <a href="${escapeAttr(href)}"${targetAttr}>`)
|
|
257
259
|
}
|
|
258
260
|
|
|
259
261
|
if (linksEnabled && interaction?.tooltip) {
|
|
260
262
|
// <title> is SVG's native tooltip — no script, no CSS. Gated by
|
|
261
263
|
// linksEnabled alongside href above: both come from the same `click`
|
|
262
264
|
// statement, and 'none' strips both.
|
|
263
|
-
parts.push(` <title>${escapeXml(interaction.tooltip)}</title>`)
|
|
265
|
+
parts.push(f` <title>${escapeXml(interaction.tooltip)}</title>`)
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
// Outer rectangle (full box)
|
|
267
269
|
parts.push(
|
|
268
|
-
` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
|
|
269
|
-
`rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
|
|
270
|
+
f` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
|
|
271
|
+
f`rx="0" ry="0" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
|
|
270
272
|
)
|
|
271
273
|
|
|
272
274
|
// Header background
|
|
273
275
|
parts.push(
|
|
274
|
-
` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
|
|
275
|
-
`rx="0" ry="0" fill="${headerFill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
|
|
276
|
+
f` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
|
|
277
|
+
f`rx="0" ry="0" fill="${headerFill}" stroke="${stroke}" stroke-width="${strokeWidth}" />`,
|
|
276
278
|
)
|
|
277
279
|
|
|
278
280
|
// Annotation (<<interface>>, <<abstract>>, etc.)
|
|
@@ -280,9 +282,9 @@ function renderClassBox(
|
|
|
280
282
|
if (cls.annotation) {
|
|
281
283
|
const annotY = y + 12
|
|
282
284
|
parts.push(
|
|
283
|
-
` <text x="${x + width / 2}" y="${annotY}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
|
|
284
|
-
`font-size="${CLS_FONT.annotationSize}" font-weight="${CLS_FONT.annotationWeight}" ` +
|
|
285
|
-
`font-style="italic" fill="${textColor ?? 'var(--_text-muted)'}"><<${escapeXml(cls.annotation)}>></text>`,
|
|
285
|
+
f` <text x="${x + width / 2}" y="${annotY}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
|
|
286
|
+
f`font-size="${CLS_FONT.annotationSize}" font-weight="${CLS_FONT.annotationWeight}" ` +
|
|
287
|
+
f`font-style="italic" fill="${textColor ?? 'var(--_text-muted)'}"><<${escapeXml(cls.annotation)}>></text>`,
|
|
286
288
|
)
|
|
287
289
|
nameY = y + headerHeight / 2 + 6
|
|
288
290
|
}
|
|
@@ -295,15 +297,15 @@ function renderClassBox(
|
|
|
295
297
|
x + width / 2,
|
|
296
298
|
nameY,
|
|
297
299
|
fontSizes.nodeLabel,
|
|
298
|
-
`text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="${textColor ?? 'var(--_text)'}"`,
|
|
300
|
+
f`text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="${textColor ?? 'var(--_text)'}"`,
|
|
299
301
|
),
|
|
300
302
|
)
|
|
301
303
|
|
|
302
304
|
// Divider line between header and attributes
|
|
303
305
|
const attrTop = y + headerHeight
|
|
304
306
|
parts.push(
|
|
305
|
-
` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
|
|
306
|
-
`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
|
|
307
|
+
f` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
|
|
308
|
+
f`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
|
|
307
309
|
)
|
|
308
310
|
|
|
309
311
|
// Attributes
|
|
@@ -317,8 +319,8 @@ function renderClassBox(
|
|
|
317
319
|
// Divider line between attributes and methods
|
|
318
320
|
const methodTop = attrTop + attrHeight
|
|
319
321
|
parts.push(
|
|
320
|
-
` <line x1="${x}" y1="${methodTop}" x2="${x + width}" y2="${methodTop}" ` +
|
|
321
|
-
`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
|
|
322
|
+
f` <line x1="${x}" y1="${methodTop}" x2="${x + width}" y2="${methodTop}" ` +
|
|
323
|
+
f`stroke="${stroke}" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
|
|
322
324
|
)
|
|
323
325
|
|
|
324
326
|
// Methods
|
|
@@ -366,27 +368,27 @@ function renderMember(
|
|
|
366
368
|
|
|
367
369
|
if (member.visibility) {
|
|
368
370
|
spans.push(
|
|
369
|
-
`<tspan fill="${faint}">${escapeXml(member.visibility)} </tspan>`,
|
|
371
|
+
f`<tspan fill="${faint}">${escapeXml(member.visibility)} </tspan>`,
|
|
370
372
|
)
|
|
371
373
|
}
|
|
372
374
|
|
|
373
375
|
// Add parentheses for methods to distinguish from attributes, including parameters if present
|
|
374
376
|
const displayName = member.isMethod
|
|
375
|
-
? `${member.name}(${member.params || ''})`
|
|
377
|
+
? f`${member.name}(${member.params || ''})`
|
|
376
378
|
: member.name
|
|
377
379
|
// False positive: displayName is passed through escapeXml() (see packages/core/src/multiline-utils.ts),
|
|
378
380
|
// which escapes &, <, >, ", ' before interpolation, so this is not raw/unescaped HTML.
|
|
379
|
-
spans.push(`<tspan fill="${secondary}">${escapeXml(displayName)}</tspan>`) // nosemgrep: javascript.express.security.injection.raw-html-format.raw-html-format
|
|
381
|
+
spans.push(f`<tspan fill="${secondary}">${escapeXml(displayName)}</tspan>`) // nosemgrep: javascript.express.security.injection.raw-html-format.raw-html-format
|
|
380
382
|
|
|
381
383
|
if (member.type) {
|
|
382
|
-
spans.push(`<tspan fill="${faint}">: </tspan>`)
|
|
383
|
-
spans.push(`<tspan fill="${muted}">${escapeXml(member.type)}</tspan>`)
|
|
384
|
+
spans.push(f`<tspan fill="${faint}">: </tspan>`)
|
|
385
|
+
spans.push(f`<tspan fill="${muted}">${escapeXml(member.type)}</tspan>`)
|
|
384
386
|
}
|
|
385
387
|
|
|
386
388
|
return (
|
|
387
|
-
`<text x="${x}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
|
|
388
|
-
`font-size="${CLS_FONT.memberSize}" font-weight="${CLS_FONT.memberWeight}"${fontStyle}${decoration}>` +
|
|
389
|
-
`${spans.join('')}</text>`
|
|
389
|
+
f`<text x="${x}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
|
|
390
|
+
f`font-size="${CLS_FONT.memberSize}" font-weight="${CLS_FONT.memberWeight}"${fontStyle}${decoration}>` +
|
|
391
|
+
f`${spans.join('')}</text>`
|
|
390
392
|
)
|
|
391
393
|
}
|
|
392
394
|
|
|
@@ -408,33 +410,33 @@ function renderNote(note: PositionedClassNote, fontSizes: FontSizes): string {
|
|
|
408
410
|
const { x, y, width: w, height: h } = note
|
|
409
411
|
const forAttr =
|
|
410
412
|
note.forClass !== undefined
|
|
411
|
-
? ` data-for="${escapeAttr(note.forClass)}"`
|
|
413
|
+
? f` data-for="${escapeAttr(note.forClass)}"`
|
|
412
414
|
: ''
|
|
413
415
|
|
|
414
416
|
// Note body: (x,y) → (x+w-fold,y) → (x+w,y+fold) → (x+w,y+h) → (x,y+h)
|
|
415
417
|
const bodyPoints = [
|
|
416
|
-
`${x},${y}`,
|
|
417
|
-
`${x + w - NOTE_FOLD},${y}`,
|
|
418
|
-
`${x + w},${y + NOTE_FOLD}`,
|
|
419
|
-
`${x + w},${y + h}`,
|
|
420
|
-
`${x},${y + h}`,
|
|
418
|
+
f`${x},${y}`,
|
|
419
|
+
f`${x + w - NOTE_FOLD},${y}`,
|
|
420
|
+
f`${x + w},${y + NOTE_FOLD}`,
|
|
421
|
+
f`${x + w},${y + h}`,
|
|
422
|
+
f`${x},${y + h}`,
|
|
421
423
|
].join(' ')
|
|
422
424
|
const foldPoints =
|
|
423
|
-
`${x + w - NOTE_FOLD},${y} ${x + w},${y + NOTE_FOLD} ` +
|
|
424
|
-
`${x + w - NOTE_FOLD},${y + NOTE_FOLD}`
|
|
425
|
+
f`${x + w - NOTE_FOLD},${y} ${x + w},${y + NOTE_FOLD} ` +
|
|
426
|
+
f`${x + w - NOTE_FOLD},${y + NOTE_FOLD}`
|
|
425
427
|
|
|
426
428
|
return (
|
|
427
|
-
`<g class="class-note"${forAttr}>` +
|
|
428
|
-
`\n <polygon points="${bodyPoints}" ` +
|
|
429
|
-
`fill="var(--bg)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
|
|
430
|
-
`\n <polygon points="${foldPoints}" ` +
|
|
431
|
-
`fill="var(--_inner-stroke)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
|
|
432
|
-
`\n ${renderMultilineText(
|
|
429
|
+
f`<g class="class-note"${forAttr}>` +
|
|
430
|
+
f`\n <polygon points="${bodyPoints}" ` +
|
|
431
|
+
f`fill="var(--bg)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
|
|
432
|
+
f`\n <polygon points="${foldPoints}" ` +
|
|
433
|
+
f`fill="var(--_inner-stroke)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />` +
|
|
434
|
+
f`\n ${renderMultilineText(
|
|
433
435
|
note.text,
|
|
434
436
|
x + w / 2,
|
|
435
437
|
y + h / 2,
|
|
436
438
|
fontSizes.edgeLabel,
|
|
437
|
-
`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
439
|
+
f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
438
440
|
)}` +
|
|
439
441
|
`\n</g>`
|
|
440
442
|
)
|
|
@@ -446,14 +448,14 @@ function renderNote(note: PositionedClassNote, fontSizes: FontSizes): string {
|
|
|
446
448
|
*/
|
|
447
449
|
function renderNoteLink(note: PositionedClassNote): string {
|
|
448
450
|
if (!note.linkPoints || note.linkPoints.length < 2) return ''
|
|
449
|
-
const pathData = note.linkPoints.map((p) => `${p.x},${p.y}`).join(' ')
|
|
451
|
+
const pathData = note.linkPoints.map((p) => f`${p.x},${p.y}`).join(' ')
|
|
450
452
|
const forAttr =
|
|
451
453
|
note.forClass !== undefined
|
|
452
|
-
? ` data-for="${escapeAttr(note.forClass)}"`
|
|
454
|
+
? f` data-for="${escapeAttr(note.forClass)}"`
|
|
453
455
|
: ''
|
|
454
456
|
return (
|
|
455
|
-
`<polyline class="class-note-link"${forAttr} points="${pathData}" ` +
|
|
456
|
-
`fill="none" stroke="var(--_line)" stroke-width="${STROKE_WIDTHS.connector}" stroke-dasharray="2 3" />`
|
|
457
|
+
f`<polyline class="class-note-link"${forAttr} points="${pathData}" ` +
|
|
458
|
+
f`fill="none" stroke="var(--_line)" stroke-width="${STROKE_WIDTHS.connector}" stroke-dasharray="2 3" />`
|
|
457
459
|
)
|
|
458
460
|
}
|
|
459
461
|
|
|
@@ -468,7 +470,7 @@ function renderNoteLink(note: PositionedClassNote): string {
|
|
|
468
470
|
function renderRelationship(rel: PositionedClassRelationship): string {
|
|
469
471
|
if (rel.points.length < 2) return ''
|
|
470
472
|
|
|
471
|
-
const pathData = rel.points.map((p) => `${p.x},${p.y}`).join(' ')
|
|
473
|
+
const pathData = rel.points.map((p) => f`${p.x},${p.y}`).join(' ')
|
|
472
474
|
const isDashed = rel.type === 'dependency' || rel.type === 'realization'
|
|
473
475
|
const dashArray = isDashed ? ' stroke-dasharray="6 4"' : ''
|
|
474
476
|
|
|
@@ -484,24 +486,26 @@ function renderRelationship(rel: PositionedClassRelationship): string {
|
|
|
484
486
|
// - data-label: relationship label if present
|
|
485
487
|
const dataAttrs = [
|
|
486
488
|
'class="class-relationship"',
|
|
487
|
-
`data-from="${escapeAttr(rel.from)}"`,
|
|
488
|
-
`data-to="${escapeAttr(rel.to)}"`,
|
|
489
|
-
`data-type="${rel.type}"`,
|
|
490
|
-
`data-marker-at="${rel.markerAt}"`,
|
|
489
|
+
f`data-from="${escapeAttr(rel.from)}"`,
|
|
490
|
+
f`data-to="${escapeAttr(rel.to)}"`,
|
|
491
|
+
f`data-type="${rel.type}"`,
|
|
492
|
+
f`data-marker-at="${rel.markerAt}"`,
|
|
491
493
|
]
|
|
492
494
|
if (rel.label) {
|
|
493
|
-
dataAttrs.push(`data-label="${escapeAttr(rel.label)}"`)
|
|
495
|
+
dataAttrs.push(f`data-label="${escapeAttr(rel.label)}"`)
|
|
494
496
|
}
|
|
495
497
|
if (rel.fromCardinality) {
|
|
496
|
-
dataAttrs.push(
|
|
498
|
+
dataAttrs.push(
|
|
499
|
+
f`data-from-cardinality="${escapeAttr(rel.fromCardinality)}"`,
|
|
500
|
+
)
|
|
497
501
|
}
|
|
498
502
|
if (rel.toCardinality) {
|
|
499
|
-
dataAttrs.push(`data-to-cardinality="${escapeAttr(rel.toCardinality)}"`)
|
|
503
|
+
dataAttrs.push(f`data-to-cardinality="${escapeAttr(rel.toCardinality)}"`)
|
|
500
504
|
}
|
|
501
505
|
|
|
502
506
|
return (
|
|
503
|
-
`<polyline ${dataAttrs.join(' ')} points="${pathData}" fill="none" stroke="var(--_line)" ` +
|
|
504
|
-
`stroke-width="${STROKE_WIDTHS.connector}"${dashArray}${markers} />`
|
|
507
|
+
f`<polyline ${dataAttrs.join(' ')} points="${pathData}" fill="none" stroke="var(--_line)" ` +
|
|
508
|
+
f`stroke-width="${STROKE_WIDTHS.connector}"${dashArray}${markers} />`
|
|
505
509
|
)
|
|
506
510
|
}
|
|
507
511
|
|
|
@@ -519,9 +523,9 @@ function getRelationshipMarkers(
|
|
|
519
523
|
if (!markerId) return ''
|
|
520
524
|
|
|
521
525
|
if (markerAt === 'from') {
|
|
522
|
-
return ` marker-start="url(#${markerId})"`
|
|
526
|
+
return f` marker-start="url(#${markerId})"`
|
|
523
527
|
} else {
|
|
524
|
-
return ` marker-end="url(#${markerId})"`
|
|
528
|
+
return f` marker-end="url(#${markerId})"`
|
|
525
529
|
}
|
|
526
530
|
}
|
|
527
531
|
|
|
@@ -562,7 +566,7 @@ function renderRelationshipLabels(
|
|
|
562
566
|
pos.x,
|
|
563
567
|
pos.y - 8,
|
|
564
568
|
fontSizes.edgeLabel,
|
|
565
|
-
`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
569
|
+
f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
566
570
|
),
|
|
567
571
|
)
|
|
568
572
|
}
|
|
@@ -578,7 +582,7 @@ function renderRelationshipLabels(
|
|
|
578
582
|
p.x + offset.x,
|
|
579
583
|
p.y + offset.y,
|
|
580
584
|
fontSizes.edgeLabel,
|
|
581
|
-
`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
585
|
+
f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
582
586
|
),
|
|
583
587
|
)
|
|
584
588
|
}
|
|
@@ -594,7 +598,7 @@ function renderRelationshipLabels(
|
|
|
594
598
|
p.x + offset.x,
|
|
595
599
|
p.y + offset.y,
|
|
596
600
|
fontSizes.edgeLabel,
|
|
597
|
-
`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
601
|
+
f`font-size="${fontSizes.edgeLabel}" text-anchor="middle" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
|
|
598
602
|
),
|
|
599
603
|
)
|
|
600
604
|
}
|
package/src/edge-curves.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
12
|
import type { Point, CurveStyle } from '@zombie-mermaid/core'
|
|
13
|
+
import { f } from '@zombie-mermaid/core'
|
|
13
14
|
|
|
14
15
|
/** `M x y` for the first point. */
|
|
15
16
|
function moveTo(p: Point): string {
|
|
16
|
-
return `M ${p.x} ${p.y}`
|
|
17
|
+
return f`M ${p.x} ${p.y}`
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/** Straight segments through every point — the historical default. */
|
|
@@ -22,7 +23,7 @@ function linearPath(points: Point[]): string {
|
|
|
22
23
|
moveTo(points[0]!) +
|
|
23
24
|
points
|
|
24
25
|
.slice(1)
|
|
25
|
-
.map((p) => ` L ${p.x} ${p.y}`)
|
|
26
|
+
.map((p) => f` L ${p.x} ${p.y}`)
|
|
26
27
|
.join('')
|
|
27
28
|
)
|
|
28
29
|
}
|
|
@@ -51,14 +52,14 @@ function basisPath(points: Point[]): string {
|
|
|
51
52
|
|
|
52
53
|
/** One B-spline segment across three successive control points. */
|
|
53
54
|
const segment = (p0: Point, p1: Point, p2: Point): string =>
|
|
54
|
-
` C ${(2 * p0.x + p1.x) / 3} ${(2 * p0.y + p1.y) / 3},` +
|
|
55
|
-
` ${(p0.x + 2 * p1.x) / 3} ${(p0.y + 2 * p1.y) / 3},` +
|
|
56
|
-
` ${(p0.x + 4 * p1.x + p2.x) / 6} ${(p0.y + 4 * p1.y + p2.y) / 6}`
|
|
55
|
+
f` C ${(2 * p0.x + p1.x) / 3} ${(2 * p0.y + p1.y) / 3},` +
|
|
56
|
+
f` ${(p0.x + 2 * p1.x) / 3} ${(p0.y + 2 * p1.y) / 3},` +
|
|
57
|
+
f` ${(p0.x + 4 * p1.x + p2.x) / 6} ${(p0.y + 4 * p1.y + p2.y) / 6}`
|
|
57
58
|
|
|
58
59
|
// Lead-in to the first knot, one sixth of the way along the opening leg.
|
|
59
60
|
parts.push(
|
|
60
|
-
` L ${(5 * points[0]!.x + points[1]!.x) / 6}` +
|
|
61
|
-
` ${(5 * points[0]!.y + points[1]!.y) / 6}`,
|
|
61
|
+
f` L ${(5 * points[0]!.x + points[1]!.x) / 6}` +
|
|
62
|
+
f` ${(5 * points[0]!.y + points[1]!.y) / 6}`,
|
|
62
63
|
)
|
|
63
64
|
|
|
64
65
|
for (let i = 2; i < points.length; i++) {
|
|
@@ -67,7 +68,7 @@ function basisPath(points: Point[]): string {
|
|
|
67
68
|
|
|
68
69
|
// d3 closes by feeding the last point twice, then drawing to it.
|
|
69
70
|
parts.push(segment(points[points.length - 2]!, last, last))
|
|
70
|
-
parts.push(` L ${last.x} ${last.y}`)
|
|
71
|
+
parts.push(f` L ${last.x} ${last.y}`)
|
|
71
72
|
|
|
72
73
|
return parts.join('')
|
|
73
74
|
}
|
|
@@ -109,7 +110,7 @@ function naturalPath(points: Point[]): string {
|
|
|
109
110
|
// A zero-length leg means duplicated routed points; there is no corner
|
|
110
111
|
// to round, so pass straight through.
|
|
111
112
|
if (inLen === 0 || outLen === 0) {
|
|
112
|
-
parts.push(` L ${corner.x} ${corner.y}`)
|
|
113
|
+
parts.push(f` L ${corner.x} ${corner.y}`)
|
|
113
114
|
continue
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -123,12 +124,12 @@ function naturalPath(points: Point[]): string {
|
|
|
123
124
|
const endY = corner.y + ((next.y - corner.y) / outLen) * radius
|
|
124
125
|
|
|
125
126
|
// Straight up to the fillet, then a quadratic through the corner.
|
|
126
|
-
parts.push(` L ${startX} ${startY}`)
|
|
127
|
-
parts.push(` Q ${corner.x} ${corner.y}, ${endX} ${endY}`)
|
|
127
|
+
parts.push(f` L ${startX} ${startY}`)
|
|
128
|
+
parts.push(f` Q ${corner.x} ${corner.y}, ${endX} ${endY}`)
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
const last = points[points.length - 1]!
|
|
131
|
-
parts.push(` L ${last.x} ${last.y}`)
|
|
132
|
+
parts.push(f` L ${last.x} ${last.y}`)
|
|
132
133
|
|
|
133
134
|
return parts.join('')
|
|
134
135
|
}
|
|
@@ -155,17 +156,17 @@ function stepPath(points: Point[], where: 'mid' | 'before' | 'after'): string {
|
|
|
155
156
|
const b = points[i + 1]!
|
|
156
157
|
|
|
157
158
|
if (i === lastIndex - 1) {
|
|
158
|
-
parts.push(` L ${b.x} ${b.y}`)
|
|
159
|
+
parts.push(f` L ${b.x} ${b.y}`)
|
|
159
160
|
break
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
if (where === 'before') {
|
|
163
|
-
parts.push(` L ${a.x} ${b.y} L ${b.x} ${b.y}`)
|
|
164
|
+
parts.push(f` L ${a.x} ${b.y} L ${b.x} ${b.y}`)
|
|
164
165
|
} else if (where === 'after') {
|
|
165
|
-
parts.push(` L ${b.x} ${a.y} L ${b.x} ${b.y}`)
|
|
166
|
+
parts.push(f` L ${b.x} ${a.y} L ${b.x} ${b.y}`)
|
|
166
167
|
} else {
|
|
167
168
|
const midY = (a.y + b.y) / 2
|
|
168
|
-
parts.push(` L ${a.x} ${midY} L ${b.x} ${midY} L ${b.x} ${b.y}`)
|
|
169
|
+
parts.push(f` L ${a.x} ${midY} L ${b.x} ${midY} L ${b.x} ${b.y}`)
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|