@diagrammo/dgmo 0.26.0 → 0.28.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 +3 -3
- package/dist/advanced.cjs +5651 -3193
- package/dist/advanced.d.cts +272 -58
- package/dist/advanced.d.ts +272 -58
- package/dist/advanced.js +5650 -3186
- package/dist/auto.cjs +5511 -3070
- package/dist/auto.js +116 -137
- package/dist/auto.mjs +5510 -3069
- package/dist/cli.cjs +168 -189
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +5536 -3072
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +5535 -3071
- package/dist/internal.cjs +5651 -3193
- package/dist/internal.d.cts +272 -58
- package/dist/internal.d.ts +272 -58
- package/dist/internal.js +5650 -3186
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +7 -3
- package/src/advanced.ts +1 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout-layered.ts +722 -0
- package/src/boxes-and-lines/layout-search.ts +1200 -0
- package/src/boxes-and-lines/layout.ts +202 -571
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -0
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion.ts +26 -12
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -0
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +34 -1
- package/src/graph/flowchart-renderer.ts +78 -64
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +78 -64
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +101 -25
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1212 -96
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +34 -68
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
package/src/cli.ts
CHANGED
|
@@ -136,7 +136,7 @@ Key options:
|
|
|
136
136
|
- \`-o <file>\` — output file; format inferred from extension (\`.svg\` → SVG, else PNG)
|
|
137
137
|
- \`-o url\` — output a shareable diagrammo.app URL
|
|
138
138
|
- \`--theme <theme>\` — \`light\` (default), \`dark\`, \`transparent\`
|
|
139
|
-
- \`--palette <name>\` — \`
|
|
139
|
+
- \`--palette <name>\` — \`slate\` (default), \`atlas\`, \`blueprint\`, \`nord\`, \`tidewater\`, \`catppuccin\`, \`tokyo-night\`
|
|
140
140
|
- \`--copy\` — copy the URL to clipboard (use with \`-o url\`)
|
|
141
141
|
- \`--chart-types\` — list all supported chart types
|
|
142
142
|
|
|
@@ -309,7 +309,7 @@ end ❌ not needed — indentation closes blocks in sequence dia
|
|
|
309
309
|
|
|
310
310
|
## Tips
|
|
311
311
|
|
|
312
|
-
- Default theme: \`light\`, default palette: \`
|
|
312
|
+
- Default theme: \`light\`, default palette: \`slate\` — ask the user their preference before a final export.
|
|
313
313
|
- Stdin mode for quick renders: \`echo "..." | dgmo -o out.png\`
|
|
314
314
|
- For C4, \`--c4-level\` drills from context → containers → components → deployment.
|
|
315
315
|
- When auto-detection picks the wrong chart type, add an explicit \`chart:\` directive.
|
|
@@ -477,7 +477,7 @@ Options:
|
|
|
477
477
|
Use -o url to output a shareable diagrammo.app URL
|
|
478
478
|
With stdin and no -o, PNG is written to stdout
|
|
479
479
|
--theme <theme> Theme: ${THEMES.join(', ')} (default: light)
|
|
480
|
-
--palette <name> Palette: ${PALETTES.join(', ')} (default:
|
|
480
|
+
--palette <name> Palette: ${PALETTES.join(', ')} (default: slate)
|
|
481
481
|
--copy Copy URL to clipboard (only with -o url)
|
|
482
482
|
--json Output structured JSON to stdout
|
|
483
483
|
--chart-types List all supported chart types
|
|
@@ -528,7 +528,7 @@ function parseArgs(argv: string[]): {
|
|
|
528
528
|
input: undefined as string | undefined,
|
|
529
529
|
output: undefined as string | undefined,
|
|
530
530
|
theme: 'light' as (typeof THEMES)[number],
|
|
531
|
-
palette: '
|
|
531
|
+
palette: 'slate',
|
|
532
532
|
help: false,
|
|
533
533
|
version: false,
|
|
534
534
|
copy: false,
|
package/src/completion.ts
CHANGED
|
@@ -16,7 +16,11 @@ import { extractSymbols as extractFlowchartSymbols } from './graph/flowchart-par
|
|
|
16
16
|
import { extractSymbols as extractInfraSymbols } from './infra/parser';
|
|
17
17
|
import { extractSymbols as extractClassSymbols } from './class/parser';
|
|
18
18
|
import { extractPertSymbols } from './pert/parser';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
parseFirstLine,
|
|
21
|
+
ALL_CHART_TYPES,
|
|
22
|
+
measureIndent,
|
|
23
|
+
} from './utils/parsing';
|
|
20
24
|
import { RECOGNIZED_COLOR_NAMES } from './colors';
|
|
21
25
|
|
|
22
26
|
const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
|
|
@@ -92,14 +96,8 @@ const GLOBAL_DIRECTIVES: Record<string, DirectiveValueSpec> = {
|
|
|
92
96
|
description: 'Color palette name',
|
|
93
97
|
values: [
|
|
94
98
|
'nord',
|
|
95
|
-
'solarized',
|
|
96
99
|
'catppuccin',
|
|
97
|
-
'rose-pine',
|
|
98
|
-
'gruvbox',
|
|
99
100
|
'tokyo-night',
|
|
100
|
-
'one-dark',
|
|
101
|
-
'dracula',
|
|
102
|
-
'monokai',
|
|
103
101
|
'atlas',
|
|
104
102
|
'blueprint',
|
|
105
103
|
'slate',
|
|
@@ -289,12 +287,13 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
289
287
|
],
|
|
290
288
|
[
|
|
291
289
|
'flowchart',
|
|
292
|
-
// Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill
|
|
290
|
+
// Spec §5 §4.6: direction-lr, orientation-vertical, solid-fill, no-notes
|
|
293
291
|
withGlobals({
|
|
294
292
|
'direction-lr': { description: 'Switch to left-to-right layout' },
|
|
295
293
|
'orientation-vertical': {
|
|
296
294
|
description: 'Use vertical orientation for ranks',
|
|
297
295
|
},
|
|
296
|
+
'no-notes': { description: 'Suppress all node note boxes' },
|
|
298
297
|
}),
|
|
299
298
|
],
|
|
300
299
|
['class', withGlobals({})],
|
|
@@ -363,9 +362,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
363
362
|
],
|
|
364
363
|
[
|
|
365
364
|
'state',
|
|
366
|
-
// Spec §6 §5.
|
|
365
|
+
// Spec §6 §5.6: direction-tb, solid-fill, no-notes.
|
|
367
366
|
withGlobals({
|
|
368
367
|
'direction-tb': { description: 'Switch to top-to-bottom layout' },
|
|
368
|
+
'no-notes': { description: 'Suppress all state note boxes' },
|
|
369
369
|
}),
|
|
370
370
|
],
|
|
371
371
|
[
|
|
@@ -447,7 +447,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
447
447
|
direction: { description: 'Layout direction', values: ['LR', 'TB'] },
|
|
448
448
|
'active-tag': { description: 'Active tag group name' },
|
|
449
449
|
hide: { description: 'Hide tag:value pairs' },
|
|
450
|
-
'box-metric': {
|
|
450
|
+
'box-metric': {
|
|
451
|
+
description:
|
|
452
|
+
'Metric label for the value ramp, with an optional trailing [low] [high] color pair',
|
|
453
|
+
},
|
|
451
454
|
'show-values': { description: 'Print box values as text' },
|
|
452
455
|
}),
|
|
453
456
|
],
|
|
@@ -519,7 +522,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
519
522
|
// content keywords, not directives; metadata keys (value/label/style) live
|
|
520
523
|
// in the reserved-key registry.
|
|
521
524
|
withGlobals({
|
|
522
|
-
'region-metric': {
|
|
525
|
+
'region-metric': {
|
|
526
|
+
description:
|
|
527
|
+
'Label for the region value ramp, with an optional trailing [low] [high] color pair',
|
|
528
|
+
},
|
|
523
529
|
'poi-metric': {
|
|
524
530
|
description: 'Label for the POI value (marker size) channel',
|
|
525
531
|
},
|
|
@@ -548,11 +554,19 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
|
|
|
548
554
|
'no-region-labels': {
|
|
549
555
|
description: 'Turn off subdivision name labels (on by default)',
|
|
550
556
|
},
|
|
557
|
+
'no-region-value': {
|
|
558
|
+
description:
|
|
559
|
+
'Turn off the metric value shown under each region (on by default)',
|
|
560
|
+
},
|
|
551
561
|
'no-poi-labels': { description: 'Turn off POI labels (on by default)' },
|
|
552
562
|
'no-colorize': {
|
|
553
563
|
description:
|
|
554
564
|
'Force plain green-land reference dress (regions are auto-coloured by default)',
|
|
555
565
|
},
|
|
566
|
+
'no-cities': {
|
|
567
|
+
description:
|
|
568
|
+
'Turn off the subtle city dots scattered across the basemap (on by default)',
|
|
569
|
+
},
|
|
556
570
|
'no-cluster-pois': {
|
|
557
571
|
description:
|
|
558
572
|
'Always fan out coincident POI markers instead of collapsing them into a count badge',
|
|
@@ -2212,7 +2226,7 @@ function extractRaciSymbols(docText: string): DiagramSymbols {
|
|
|
2212
2226
|
continue;
|
|
2213
2227
|
}
|
|
2214
2228
|
|
|
2215
|
-
const indent = line
|
|
2229
|
+
const indent = measureIndent(line);
|
|
2216
2230
|
|
|
2217
2231
|
// Header directives
|
|
2218
2232
|
if (indent === 0) {
|
package/src/cycle/layout.ts
CHANGED
|
@@ -16,23 +16,18 @@ import {
|
|
|
16
16
|
wrapDescriptionLines,
|
|
17
17
|
type WrappedDescLine,
|
|
18
18
|
} from '../utils/wrapped-desc';
|
|
19
|
+
import { measureText, wrapTextToWidth } from '../utils/text-measure';
|
|
19
20
|
|
|
20
21
|
/** Minimum arc angle in radians (~15°) to keep arcs readable. */
|
|
21
22
|
const MIN_ARC_ANGLE = (15 * Math.PI) / 180;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* Estimated character width at 11px description font (Inter).
|
|
31
|
-
* Average glyph width is ~5.5–6.0 px for typical English text — using 6.0
|
|
32
|
-
* gives us a small margin of safety for wide glyphs (m, w) without leaving
|
|
33
|
-
* obvious dead space on the right side of the rectangle.
|
|
34
|
-
*/
|
|
35
|
-
const DESC_CHAR_W = 6.0;
|
|
24
|
+
// ── Font sizes (must match cycle/renderer.ts) ──
|
|
25
|
+
// Layout sizes nodes/labels from text; the renderer draws them. Both measure
|
|
26
|
+
// the same string at the same font size so reserved space matches ink.
|
|
27
|
+
const LABEL_FONT_SIZE = 13;
|
|
28
|
+
const CIRCLE_LABEL_FONT_SIZE = 16;
|
|
29
|
+
const DESC_FONT_SIZE = 11;
|
|
30
|
+
const EDGE_LABEL_FONT_SIZE = 11;
|
|
36
31
|
|
|
37
32
|
/** Minimum node width. */
|
|
38
33
|
const MIN_NODE_WIDTH = 70;
|
|
@@ -105,7 +100,7 @@ export function computeCycleLayout(
|
|
|
105
100
|
const hasDesc = !hideDescriptions && node.description.length > 0;
|
|
106
101
|
const labelWidth = Math.max(
|
|
107
102
|
MIN_NODE_WIDTH,
|
|
108
|
-
node.label
|
|
103
|
+
measureText(node.label, LABEL_FONT_SIZE) + NODE_PAD_X * 2
|
|
109
104
|
);
|
|
110
105
|
|
|
111
106
|
if (circleNodes) {
|
|
@@ -422,9 +417,13 @@ function wrapDescForWidth(
|
|
|
422
417
|
description: readonly string[],
|
|
423
418
|
nodeWidth: number
|
|
424
419
|
): WrappedDescLine[] {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
420
|
+
// Wrap to a real pixel budget. `wrapDescriptionLines` is bullet-aware (it
|
|
421
|
+
// emits bullet-first/cont kinds the renderer relies on), so feed it a pixel
|
|
422
|
+
// `lengthFn` + pixel limit instead of a character count.
|
|
423
|
+
const textWidth = Math.max(40, nodeWidth - NODE_PAD_X * 2);
|
|
424
|
+
return wrapDescriptionLines([...description], textWidth, (s) =>
|
|
425
|
+
measureText(s, DESC_FONT_SIZE)
|
|
426
|
+
);
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
// ── Renderer-aligned font/line-height clamps ──
|
|
@@ -463,11 +462,12 @@ function renderedDescNodeHeight(numLines: number, scale: number): number {
|
|
|
463
462
|
// ── Edge-label wrapping (shared with renderer) ──
|
|
464
463
|
|
|
465
464
|
/**
|
|
466
|
-
* Maximum
|
|
465
|
+
* Maximum pixel width per line for edge labels and edge descriptions.
|
|
467
466
|
* Long single-line text gets wrapped to multiple lines so it doesn't
|
|
468
|
-
* shoot off-canvas when positioned at a cycle quadrant.
|
|
467
|
+
* shoot off-canvas when positioned at a cycle quadrant. ~197px ≈ the old
|
|
468
|
+
* 32-char budget at the 11px edge-label font.
|
|
469
469
|
*/
|
|
470
|
-
export const
|
|
470
|
+
export const EDGE_LABEL_MAX_WIDTH = 197;
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
473
|
* Wrap an edge label string + description lines into rendered lines.
|
|
@@ -478,37 +478,18 @@ export const EDGE_LABEL_MAX_CHARS = 32;
|
|
|
478
478
|
export function wrapEdgeLabelText(
|
|
479
479
|
label: string | undefined,
|
|
480
480
|
description: readonly string[],
|
|
481
|
-
|
|
481
|
+
maxWidth: number = EDGE_LABEL_MAX_WIDTH
|
|
482
482
|
): { labelLines: string[]; descLines: string[] } {
|
|
483
|
-
const labelLines = label
|
|
483
|
+
const labelLines = label
|
|
484
|
+
? wrapTextToWidth(label, EDGE_LABEL_FONT_SIZE, maxWidth)
|
|
485
|
+
: [];
|
|
484
486
|
const descLines: string[] = [];
|
|
485
487
|
for (const d of description) {
|
|
486
|
-
descLines.push(...
|
|
488
|
+
descLines.push(...wrapTextToWidth(d, EDGE_LABEL_FONT_SIZE, maxWidth));
|
|
487
489
|
}
|
|
488
490
|
return { labelLines, descLines };
|
|
489
491
|
}
|
|
490
492
|
|
|
491
|
-
// ── Helper: word-wrap lines ──
|
|
492
|
-
|
|
493
|
-
function wrapLines(lines: readonly string[], charsPerLine: number): string[] {
|
|
494
|
-
const result: string[] = [];
|
|
495
|
-
for (const line of lines) {
|
|
496
|
-
const words = line.split(/\s+/);
|
|
497
|
-
let current = '';
|
|
498
|
-
for (const word of words) {
|
|
499
|
-
const test = current ? `${current} ${word}` : word;
|
|
500
|
-
if (test.length > charsPerLine && current) {
|
|
501
|
-
result.push(current);
|
|
502
|
-
current = word;
|
|
503
|
-
} else {
|
|
504
|
-
current = test;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (current) result.push(current);
|
|
508
|
-
}
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
493
|
// ── Helper: circle node dimensions ──
|
|
513
494
|
|
|
514
495
|
function computeCircleNodeDims(
|
|
@@ -516,7 +497,7 @@ function computeCircleNodeDims(
|
|
|
516
497
|
hasDesc: boolean
|
|
517
498
|
): { width: number; height: number; wrappedDesc: WrappedDescLine[] } {
|
|
518
499
|
if (!hasDesc) {
|
|
519
|
-
const textW = node.label
|
|
500
|
+
const textW = measureText(node.label, CIRCLE_LABEL_FONT_SIZE);
|
|
520
501
|
const r = Math.max(MIN_CIRCLE_RADIUS, textW / 2 + CIRCLE_PAD);
|
|
521
502
|
return { width: r * 2, height: r * 2, wrappedDesc: [] };
|
|
522
503
|
}
|
|
@@ -529,7 +510,7 @@ function computeCircleNodeDims(
|
|
|
529
510
|
const textBlockH = totalLines * DESC_LINE_HEIGHT + CIRCLE_PAD;
|
|
530
511
|
|
|
531
512
|
if (textBlockH / 2 <= r * 0.85) {
|
|
532
|
-
const labelW = node.label
|
|
513
|
+
const labelW = measureText(node.label, CIRCLE_LABEL_FONT_SIZE);
|
|
533
514
|
const labelY = -textBlockH / 2 + DESC_LINE_HEIGHT;
|
|
534
515
|
const availW = 2 * Math.sqrt(Math.max(0, r * r - labelY * labelY));
|
|
535
516
|
if (labelW <= availW - CIRCLE_PAD) {
|
|
@@ -563,29 +544,32 @@ function wrapLinesForCircle(
|
|
|
563
544
|
descriptions: readonly string[],
|
|
564
545
|
radius: number
|
|
565
546
|
): string[] {
|
|
566
|
-
// First pass: wrap
|
|
547
|
+
// First pass: wrap to the center pixel-width to estimate line count.
|
|
567
548
|
const centerWidth = radius * 2 * 0.75;
|
|
568
|
-
const
|
|
569
|
-
|
|
549
|
+
const roughWrapped = descriptions.flatMap((d) =>
|
|
550
|
+
wrapTextToWidth(d, DESC_FONT_SIZE, centerWidth)
|
|
551
|
+
);
|
|
570
552
|
const totalLines = 1 + roughWrapped.length; // +1 for label line
|
|
571
553
|
const blockH = totalLines * DESC_LINE_HEIGHT;
|
|
572
554
|
|
|
573
|
-
// Second pass: re-wrap each source line with position-aware width
|
|
555
|
+
// Second pass: re-wrap each source line with a position-aware pixel width —
|
|
556
|
+
// wider near the circle's vertical center, narrower toward the edges.
|
|
574
557
|
const result: string[] = [];
|
|
575
558
|
let lineIdx = 1; // start after label line
|
|
576
559
|
for (const srcLine of descriptions) {
|
|
577
|
-
const words = srcLine.split(/\s+/);
|
|
560
|
+
const words = srcLine.split(/\s+/).filter((w) => w.length > 0);
|
|
578
561
|
let current = '';
|
|
579
562
|
for (const word of words) {
|
|
580
|
-
// Compute available width at this line's y position
|
|
563
|
+
// Compute available pixel width at this line's y position.
|
|
581
564
|
const y = -blockH / 2 + (lineIdx + 0.5) * DESC_LINE_HEIGHT;
|
|
582
565
|
const rSq = radius * radius;
|
|
583
|
-
const availPx =
|
|
584
|
-
|
|
585
|
-
|
|
566
|
+
const availPx = Math.max(
|
|
567
|
+
20,
|
|
568
|
+
y * y < rSq ? 2 * Math.sqrt(rSq - y * y) - CIRCLE_PAD * 2 : centerWidth
|
|
569
|
+
);
|
|
586
570
|
|
|
587
571
|
const test = current ? `${current} ${word}` : word;
|
|
588
|
-
if (test
|
|
572
|
+
if (measureText(test, DESC_FONT_SIZE) > availPx && current) {
|
|
589
573
|
result.push(current);
|
|
590
574
|
lineIdx++;
|
|
591
575
|
current = word;
|
|
@@ -725,16 +709,18 @@ function computeEdgePaths(
|
|
|
725
709
|
edge.description
|
|
726
710
|
);
|
|
727
711
|
const lineCount = labelLines.length + descLines.length;
|
|
728
|
-
let
|
|
729
|
-
for (const l of labelLines)
|
|
730
|
-
|
|
712
|
+
let labelPxW = 0;
|
|
713
|
+
for (const l of labelLines)
|
|
714
|
+
labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
715
|
+
for (const l of descLines)
|
|
716
|
+
labelPxW = Math.max(labelPxW, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
731
717
|
const { labelX, labelY, labelAngle } = computeEdgeLabelPosition(
|
|
732
718
|
midAngle,
|
|
733
719
|
radius,
|
|
734
720
|
cx,
|
|
735
721
|
cy,
|
|
736
722
|
lineCount,
|
|
737
|
-
|
|
723
|
+
labelPxW,
|
|
738
724
|
layoutNodes
|
|
739
725
|
);
|
|
740
726
|
const layoutEdge: CycleLayoutEdge = {
|
|
@@ -769,10 +755,10 @@ function computeEdgeLabelPosition(
|
|
|
769
755
|
cx: number,
|
|
770
756
|
cy: number,
|
|
771
757
|
lineCount: number,
|
|
772
|
-
|
|
758
|
+
labelPxW: number,
|
|
773
759
|
layoutNodes: CycleLayoutNode[]
|
|
774
760
|
): { labelX: number; labelY: number; labelAngle: number } {
|
|
775
|
-
if (lineCount === 0 ||
|
|
761
|
+
if (lineCount === 0 || labelPxW === 0) {
|
|
776
762
|
return {
|
|
777
763
|
labelX: cx + radius * Math.cos(midAngle),
|
|
778
764
|
labelY: cy + radius * Math.sin(midAngle),
|
|
@@ -781,7 +767,7 @@ function computeEdgeLabelPosition(
|
|
|
781
767
|
}
|
|
782
768
|
|
|
783
769
|
const EDGE_LABEL_CORNER_OFFSET = 10;
|
|
784
|
-
const labelW =
|
|
770
|
+
const labelW = labelPxW;
|
|
785
771
|
const labelH = lineCount * 15;
|
|
786
772
|
const cosT = Math.cos(midAngle);
|
|
787
773
|
const sinT = Math.sin(midAngle);
|
|
@@ -867,9 +853,6 @@ function computeEdgeLabelPosition(
|
|
|
867
853
|
};
|
|
868
854
|
}
|
|
869
855
|
|
|
870
|
-
/** Estimated character width at 11px edge label font. */
|
|
871
|
-
const EDGE_LABEL_CHAR_W = 7;
|
|
872
|
-
|
|
873
856
|
/**
|
|
874
857
|
* Check if edge labels overflow the canvas and return a reduced radius if needed.
|
|
875
858
|
* Returns null if everything fits.
|
|
@@ -910,12 +893,12 @@ function fitToCanvas(
|
|
|
910
893
|
le.label,
|
|
911
894
|
edge.description
|
|
912
895
|
);
|
|
913
|
-
let
|
|
914
|
-
for (const l of labelLines)
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
896
|
+
let textWidth = 0;
|
|
897
|
+
for (const l of labelLines)
|
|
898
|
+
textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
899
|
+
for (const l of descLines)
|
|
900
|
+
textWidth = Math.max(textWidth, measureText(l, EDGE_LABEL_FONT_SIZE));
|
|
901
|
+
if (textWidth === 0) continue;
|
|
919
902
|
|
|
920
903
|
// Determine text-anchor direction from label angle (mirrors renderer logic)
|
|
921
904
|
const normAngle =
|
package/src/cycle/renderer.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './types';
|
|
32
32
|
import { computeCycleLayout, wrapEdgeLabelText } from './layout';
|
|
33
33
|
import { ScaleContext } from '../utils/scaling';
|
|
34
|
+
import { measureText } from '../utils/text-measure';
|
|
34
35
|
|
|
35
36
|
// ── Constants ────────────────────────────────────────────────
|
|
36
37
|
const NODE_FONT_SIZE = 13;
|
|
@@ -502,12 +503,16 @@ export function renderCycle(
|
|
|
502
503
|
const anchor = isRight ? 'start' : isLeft ? 'end' : 'middle';
|
|
503
504
|
|
|
504
505
|
const lineCount = labelLines.length + descLines.length;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
506
|
+
// Measure the widest rendered line in pixels at the scaled edge-label font
|
|
507
|
+
// so the background box matches the actual ink (same measurer the layout
|
|
508
|
+
// uses to size + place the label).
|
|
509
|
+
let maxLineW = 0;
|
|
510
|
+
for (const l of labelLines)
|
|
511
|
+
maxLineW = Math.max(maxLineW, measureText(l, scaledEdgeLabelFont));
|
|
512
|
+
for (const l of descLines)
|
|
513
|
+
maxLineW = Math.max(maxLineW, measureText(l, scaledDescFont));
|
|
514
|
+
|
|
515
|
+
const bgW = maxLineW + 12;
|
|
511
516
|
const bgH = lineCount * scaledEdgeLineH + 6;
|
|
512
517
|
const bgX = isRight
|
|
513
518
|
? le.labelX - 4
|