@diagrammo/dgmo 0.26.0 → 0.27.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 +4182 -2704
- package/dist/advanced.d.cts +266 -58
- package/dist/advanced.d.ts +266 -58
- package/dist/advanced.js +4182 -2698
- package/dist/auto.cjs +4042 -2581
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4042 -2581
- package/dist/cli.cjs +172 -170
- 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 +4067 -2583
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4067 -2583
- package/dist/internal.cjs +4182 -2704
- package/dist/internal.d.cts +266 -58
- package/dist/internal.d.ts +266 -58
- package/dist/internal.js +4182 -2698
- 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 +1 -1
- package/src/advanced.ts +1 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- 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 +57 -12
- 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 +1196 -90
- 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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
|
-
import
|
|
6
|
+
import { appendArrowheadMarkers } from '../utils/arrow-markers';
|
|
7
|
+
import { fitDiagramToCanvas } from '../utils/fit-canvas';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
8
9
|
import type { PaletteColors } from '../palettes';
|
|
9
10
|
import { contrastText, shapeFill } from '../palettes/color-utils';
|
|
@@ -11,12 +12,21 @@ import type { ParsedGraph, GraphShape } from './types';
|
|
|
11
12
|
import type { LayoutResult, LayoutNode } from './layout';
|
|
12
13
|
import { parseFlowchart } from './flowchart-parser';
|
|
13
14
|
import { layoutGraph } from './layout';
|
|
15
|
+
import { edgeSplinePath } from './edge-spline';
|
|
14
16
|
import {
|
|
15
17
|
TITLE_FONT_SIZE,
|
|
16
18
|
TITLE_FONT_WEIGHT,
|
|
17
19
|
TITLE_Y,
|
|
18
20
|
} from '../utils/title-constants';
|
|
19
21
|
import { ScaleContext } from '../utils/scaling';
|
|
22
|
+
import { measureText } from '../utils/text-measure';
|
|
23
|
+
import {
|
|
24
|
+
renderNoteBox,
|
|
25
|
+
renderNoteConnector,
|
|
26
|
+
renderNoteBadge,
|
|
27
|
+
noteConnectorPoints,
|
|
28
|
+
NOTE_BADGE_RADIUS,
|
|
29
|
+
} from '../utils/note-box';
|
|
20
30
|
|
|
21
31
|
// ============================================================
|
|
22
32
|
// Constants
|
|
@@ -427,16 +437,6 @@ function renderNodeShape(
|
|
|
427
437
|
}
|
|
428
438
|
}
|
|
429
439
|
|
|
430
|
-
// ============================================================
|
|
431
|
-
// Edge path generator
|
|
432
|
-
// ============================================================
|
|
433
|
-
|
|
434
|
-
const lineGenerator = d3Shape
|
|
435
|
-
.line<{ x: number; y: number }>()
|
|
436
|
-
.x((d) => d.x)
|
|
437
|
-
.y((d) => d.y)
|
|
438
|
-
.curve(d3Shape.curveBasis);
|
|
439
|
-
|
|
440
440
|
// ============================================================
|
|
441
441
|
// Main renderer
|
|
442
442
|
// ============================================================
|
|
@@ -476,28 +476,16 @@ export function renderFlowchart(
|
|
|
476
476
|
|
|
477
477
|
const diagramW = layout.width;
|
|
478
478
|
const diagramH = layout.height;
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
scale = Math.min(MAX_SCALE, scaleX);
|
|
490
|
-
canvasHeight = titleHeight + diagramH * scale + sDiagramPadding * 2;
|
|
491
|
-
} else {
|
|
492
|
-
const availH = height - titleHeight;
|
|
493
|
-
const scaleY = (availH - sDiagramPadding * 2) / diagramH;
|
|
494
|
-
scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
495
|
-
canvasHeight = height;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const scaledW = diagramW * scale;
|
|
499
|
-
const offsetX = (width - scaledW) / 2;
|
|
500
|
-
const offsetY = titleHeight + sDiagramPadding;
|
|
479
|
+
const { scale, offsetX, offsetY, canvasHeight } = fitDiagramToCanvas({
|
|
480
|
+
width,
|
|
481
|
+
height,
|
|
482
|
+
diagramW,
|
|
483
|
+
diagramH,
|
|
484
|
+
padding: sDiagramPadding,
|
|
485
|
+
titleHeight,
|
|
486
|
+
maxScale: MAX_SCALE,
|
|
487
|
+
exportMode: !!exportDims,
|
|
488
|
+
});
|
|
501
489
|
|
|
502
490
|
const svg = d3Selection
|
|
503
491
|
.select(container)
|
|
@@ -514,35 +502,14 @@ export function renderFlowchart(
|
|
|
514
502
|
|
|
515
503
|
const defs = svg.append('defs');
|
|
516
504
|
|
|
517
|
-
defs
|
|
518
|
-
.append('marker')
|
|
519
|
-
.attr('id', 'fc-arrow')
|
|
520
|
-
.attr('viewBox', `0 0 ${sArrowheadW} ${sArrowheadH}`)
|
|
521
|
-
.attr('refX', sArrowheadW)
|
|
522
|
-
.attr('refY', sArrowheadH / 2)
|
|
523
|
-
.attr('markerWidth', sArrowheadW)
|
|
524
|
-
.attr('markerHeight', sArrowheadH)
|
|
525
|
-
.attr('orient', 'auto')
|
|
526
|
-
.append('polygon')
|
|
527
|
-
.attr('points', `0,0 ${sArrowheadW},${sArrowheadH / 2} 0,${sArrowheadH}`)
|
|
528
|
-
.attr('fill', palette.textMuted);
|
|
529
|
-
|
|
530
505
|
const edgeColors = new Set<string>();
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
.attr('refY', sArrowheadH / 2)
|
|
539
|
-
.attr('markerWidth', sArrowheadW)
|
|
540
|
-
.attr('markerHeight', sArrowheadH)
|
|
541
|
-
.attr('orient', 'auto')
|
|
542
|
-
.append('polygon')
|
|
543
|
-
.attr('points', `0,0 ${sArrowheadW},${sArrowheadH / 2} 0,${sArrowheadH}`)
|
|
544
|
-
.attr('fill', color);
|
|
545
|
-
}
|
|
506
|
+
appendArrowheadMarkers(defs, {
|
|
507
|
+
idPrefix: 'fc',
|
|
508
|
+
width: sArrowheadW,
|
|
509
|
+
height: sArrowheadH,
|
|
510
|
+
baseFill: palette.textMuted,
|
|
511
|
+
colors: edgeColors,
|
|
512
|
+
});
|
|
546
513
|
|
|
547
514
|
if (showTitle) {
|
|
548
515
|
const titleEl = svg
|
|
@@ -579,7 +546,6 @@ export function renderFlowchart(
|
|
|
579
546
|
.append('g')
|
|
580
547
|
.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
581
548
|
|
|
582
|
-
const LABEL_CHAR_W = 7;
|
|
583
549
|
const LABEL_PAD = 8;
|
|
584
550
|
const LABEL_H = 16;
|
|
585
551
|
const PERP_OFFSET = 10;
|
|
@@ -598,7 +564,7 @@ export function renderFlowchart(
|
|
|
598
564
|
if (!edge.label || edge.points.length < 2) continue;
|
|
599
565
|
const midIdx = Math.floor(edge.points.length / 2);
|
|
600
566
|
const midPt = edge.points[midIdx]!;
|
|
601
|
-
const bgW = edge.label
|
|
567
|
+
const bgW = measureText(edge.label, sEdgeLabelFontSize) + LABEL_PAD;
|
|
602
568
|
|
|
603
569
|
const prev = edge.points[Math.max(0, midIdx - 1)]!;
|
|
604
570
|
const next = edge.points[Math.min(edge.points.length - 1, midIdx + 1)]!;
|
|
@@ -642,7 +608,7 @@ export function renderFlowchart(
|
|
|
642
608
|
const edgeColor = palette.textMuted;
|
|
643
609
|
const markerId = 'fc-arrow';
|
|
644
610
|
|
|
645
|
-
const pathD =
|
|
611
|
+
const pathD = edgeSplinePath(edge.points);
|
|
646
612
|
if (pathD) {
|
|
647
613
|
edgeG
|
|
648
614
|
.append('path')
|
|
@@ -690,6 +656,7 @@ export function renderFlowchart(
|
|
|
690
656
|
|
|
691
657
|
const colorOff = graph.options?.['color'] === 'off';
|
|
692
658
|
const solid = graph.options?.['solid-fill'] === 'on';
|
|
659
|
+
const noNotes = graph.options?.['no-notes'] === 'on';
|
|
693
660
|
for (const node of layout.nodes) {
|
|
694
661
|
const nodeG = contentG
|
|
695
662
|
.append('g')
|
|
@@ -704,6 +671,10 @@ export function renderFlowchart(
|
|
|
704
671
|
});
|
|
705
672
|
}
|
|
706
673
|
|
|
674
|
+
// The shape is drawn at its dagre position — a note never moves it,
|
|
675
|
+
// so its edges stay connected. The note floats beside it.
|
|
676
|
+
const hasNote = !!node.note && !noNotes;
|
|
677
|
+
|
|
707
678
|
renderNodeShape(
|
|
708
679
|
nodeG as GSelection,
|
|
709
680
|
node,
|
|
@@ -744,6 +715,49 @@ export function renderFlowchart(
|
|
|
744
715
|
)
|
|
745
716
|
.attr('font-size', sNodeFontSize)
|
|
746
717
|
.text(node.label);
|
|
718
|
+
|
|
719
|
+
if (hasNote && node.note) {
|
|
720
|
+
if (node.note.collapsed) {
|
|
721
|
+
// Collapsed → comment-bubble badge in the node's top-right corner.
|
|
722
|
+
renderNoteBadge(
|
|
723
|
+
nodeG as GSelection,
|
|
724
|
+
{
|
|
725
|
+
x: node.width / 2 - NOTE_BADGE_RADIUS - 3,
|
|
726
|
+
y: -node.height / 2 + NOTE_BADGE_RADIUS + 3,
|
|
727
|
+
},
|
|
728
|
+
palette,
|
|
729
|
+
{
|
|
730
|
+
isDark,
|
|
731
|
+
...(node.note.color && { color: node.note.color }),
|
|
732
|
+
lineNumber: node.note.lineNumber,
|
|
733
|
+
endLineNumber: node.note.endLineNumber,
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
// Solid tether from the shape edge to the floated note, on whichever
|
|
738
|
+
// side the collision-aware placement chose.
|
|
739
|
+
const [cx1, cy1, cx2, cy2] = noteConnectorPoints(node, node.note);
|
|
740
|
+
renderNoteConnector(nodeG as GSelection, cx1, cy1, cx2, cy2, palette);
|
|
741
|
+
renderNoteBox(
|
|
742
|
+
nodeG as GSelection,
|
|
743
|
+
{
|
|
744
|
+
x: node.note.x,
|
|
745
|
+
y: node.note.y,
|
|
746
|
+
width: node.note.width,
|
|
747
|
+
height: node.note.height,
|
|
748
|
+
},
|
|
749
|
+
node.note.lines,
|
|
750
|
+
palette,
|
|
751
|
+
{
|
|
752
|
+
isDark,
|
|
753
|
+
...(node.note.color && { color: node.note.color }),
|
|
754
|
+
lineNumber: node.note.lineNumber,
|
|
755
|
+
endLineNumber: node.note.endLineNumber,
|
|
756
|
+
interactive: true,
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
747
761
|
}
|
|
748
762
|
}
|
|
749
763
|
|
package/src/graph/layout.ts
CHANGED
|
@@ -6,6 +6,33 @@ import type {
|
|
|
6
6
|
GraphGroup,
|
|
7
7
|
GraphShape,
|
|
8
8
|
} from './types';
|
|
9
|
+
import { resolveNotes } from './notes';
|
|
10
|
+
import { noteBoxSize } from '../utils/note-box';
|
|
11
|
+
import { placeNotes, noteCanvasShift } from '../utils/notes';
|
|
12
|
+
import type { WrappedDescLine } from '../utils/wrapped-desc';
|
|
13
|
+
|
|
14
|
+
export type NoteSide = 'above' | 'below' | 'left' | 'right';
|
|
15
|
+
|
|
16
|
+
/** A note box positioned relative to its anchor node's center. */
|
|
17
|
+
export interface NoteLayout {
|
|
18
|
+
readonly x: number;
|
|
19
|
+
readonly y: number;
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
/** Which side of the node the box sits on (drives the connector). */
|
|
23
|
+
readonly side: NoteSide;
|
|
24
|
+
/** Resolved hex accent (border + faded fill); default yellow if absent. */
|
|
25
|
+
readonly color?: string;
|
|
26
|
+
readonly lines: readonly WrappedDescLine[];
|
|
27
|
+
readonly lineNumber: number;
|
|
28
|
+
readonly endLineNumber: number;
|
|
29
|
+
/**
|
|
30
|
+
* When true the note is collapsed: the renderer draws a small badge at
|
|
31
|
+
* the node corner instead of the floated box, and `x/y/width/height/side/
|
|
32
|
+
* lines` are unused. Collapsed notes reserve no layout space.
|
|
33
|
+
*/
|
|
34
|
+
readonly collapsed?: boolean;
|
|
35
|
+
}
|
|
9
36
|
|
|
10
37
|
export interface LayoutNode {
|
|
11
38
|
readonly id: string;
|
|
@@ -18,6 +45,13 @@ export interface LayoutNode {
|
|
|
18
45
|
readonly y: number;
|
|
19
46
|
readonly width: number;
|
|
20
47
|
readonly height: number;
|
|
48
|
+
/**
|
|
49
|
+
* A note floated beside this node. The shape keeps its natural dagre
|
|
50
|
+
* position and dimensions (so its edges stay connected) — the note is
|
|
51
|
+
* placed in adjacent space and the canvas bounds are expanded to fit
|
|
52
|
+
* it. Absent on un-annotated nodes.
|
|
53
|
+
*/
|
|
54
|
+
readonly note?: NoteLayout;
|
|
21
55
|
}
|
|
22
56
|
|
|
23
57
|
export interface LayoutEdge {
|
|
@@ -45,6 +79,11 @@ export interface LayoutOptions {
|
|
|
45
79
|
collapsedChildCounts?: Map<string, number>;
|
|
46
80
|
/** Original groups before collapse (includes collapsed ones) */
|
|
47
81
|
originalGroups?: readonly GraphGroup[];
|
|
82
|
+
/**
|
|
83
|
+
* 1-based source line numbers of notes the user has collapsed. A
|
|
84
|
+
* collapsed note renders as a corner badge and reserves no space.
|
|
85
|
+
*/
|
|
86
|
+
collapsedNotes?: ReadonlySet<number>;
|
|
48
87
|
}
|
|
49
88
|
|
|
50
89
|
export interface LayoutResult {
|
|
@@ -75,6 +114,7 @@ export function layoutGraph(
|
|
|
75
114
|
): LayoutResult {
|
|
76
115
|
const collapsedChildCounts = options?.collapsedChildCounts;
|
|
77
116
|
const originalGroups = options?.originalGroups;
|
|
117
|
+
const collapsedNotes = options?.collapsedNotes;
|
|
78
118
|
|
|
79
119
|
// Collapsed groups become synthetic nodes in the graph
|
|
80
120
|
const collapsedGroupNodes: GraphNode[] = [];
|
|
@@ -124,12 +164,48 @@ export function layoutGraph(
|
|
|
124
164
|
}
|
|
125
165
|
}
|
|
126
166
|
|
|
127
|
-
//
|
|
167
|
+
// Resolve note anchors (no diagnostics here — the parser already
|
|
168
|
+
// emitted them). `no-notes` drops the reserved footprint entirely so
|
|
169
|
+
// layout matches an un-annotated diagram (ADR-4 / AC8).
|
|
170
|
+
const notesSuppressed = graph.options?.['no-notes'] === 'on';
|
|
171
|
+
const noteByNode =
|
|
172
|
+
notesSuppressed || !graph.notes
|
|
173
|
+
? new Map()
|
|
174
|
+
: resolveNotes(graph.notes, graph.nodes);
|
|
175
|
+
|
|
176
|
+
// Pre-computed note geometry, keyed by node id, threaded into LayoutNode.
|
|
177
|
+
// The note does NOT change the node's dagre dims — the shape keeps its
|
|
178
|
+
// position and its edge connections; the note floats beside it and the
|
|
179
|
+
// canvas bounds are expanded to fit it.
|
|
180
|
+
interface NoteGeom {
|
|
181
|
+
noteW: number;
|
|
182
|
+
noteH: number;
|
|
183
|
+
color?: string;
|
|
184
|
+
lines: WrappedDescLine[];
|
|
185
|
+
lineNumber: number;
|
|
186
|
+
endLineNumber: number;
|
|
187
|
+
}
|
|
188
|
+
const noteGeoms = new Map<string, NoteGeom>();
|
|
189
|
+
|
|
190
|
+
// Add nodes with their natural dimensions (notes never inflate them).
|
|
128
191
|
for (const node of allNodes) {
|
|
129
192
|
const width = computeNodeWidth(node.label, node.shape);
|
|
130
193
|
const height = computeNodeHeight(node.shape);
|
|
131
194
|
g.setNode(node.id, { label: node.label, width, height });
|
|
132
195
|
|
|
196
|
+
const note = noteByNode.get(node.id);
|
|
197
|
+
if (note) {
|
|
198
|
+
const size = noteBoxSize(note.body);
|
|
199
|
+
noteGeoms.set(node.id, {
|
|
200
|
+
noteW: size.width,
|
|
201
|
+
noteH: size.height,
|
|
202
|
+
...(note.color && { color: note.color }),
|
|
203
|
+
lines: size.lines,
|
|
204
|
+
lineNumber: note.lineNumber,
|
|
205
|
+
endLineNumber: note.endLineNumber,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
133
209
|
// Set parent for grouped nodes (only for non-collapsed groups)
|
|
134
210
|
if (node.group && graph.groups?.some((gr) => gr.id === node.group)) {
|
|
135
211
|
g.setParent(node.id, node.group);
|
|
@@ -154,8 +230,44 @@ export function layoutGraph(
|
|
|
154
230
|
? new Set(collapsedChildCounts.keys())
|
|
155
231
|
: new Set<string>();
|
|
156
232
|
|
|
157
|
-
|
|
233
|
+
// Positioned nodes (no notes yet) — feeds collision-aware placement.
|
|
234
|
+
const basePositioned = allNodes.map((node) => {
|
|
158
235
|
const pos = g.node(node.id);
|
|
236
|
+
return { node, x: pos.x, y: pos.y, width: pos.width, height: pos.height };
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── Collision-aware note placement (shared `utils/notes`) ───
|
|
240
|
+
// The note floats beside its node WITHOUT moving it: try the default
|
|
241
|
+
// side, flip to the opposite, then push outward past blockers — each
|
|
242
|
+
// placed note becoming an obstacle for later notes. Collapsed notes
|
|
243
|
+
// reserve no space (drawn as a corner badge).
|
|
244
|
+
const noteRequests = basePositioned
|
|
245
|
+
.filter((p) => noteGeoms.has(p.node.id))
|
|
246
|
+
.map((p) => {
|
|
247
|
+
const ng = noteGeoms.get(p.node.id)!;
|
|
248
|
+
return {
|
|
249
|
+
key: p.node.id,
|
|
250
|
+
node: { x: p.x, y: p.y, width: p.width, height: p.height },
|
|
251
|
+
noteW: ng.noteW,
|
|
252
|
+
noteH: ng.noteH,
|
|
253
|
+
collapsed: collapsedNotes?.has(ng.lineNumber) ?? false,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
const placements = placeNotes(
|
|
257
|
+
basePositioned.map((p) => ({
|
|
258
|
+
x: p.x,
|
|
259
|
+
y: p.y,
|
|
260
|
+
width: p.width,
|
|
261
|
+
height: p.height,
|
|
262
|
+
})),
|
|
263
|
+
noteRequests,
|
|
264
|
+
graph.direction === 'LR' ? 'LR' : 'TB'
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const layoutNodes: LayoutNode[] = basePositioned.map((p): LayoutNode => {
|
|
268
|
+
const node = p.node;
|
|
269
|
+
const ng = noteGeoms.get(node.id);
|
|
270
|
+
const placed = placements.get(node.id);
|
|
159
271
|
return {
|
|
160
272
|
id: node.id,
|
|
161
273
|
label: node.label,
|
|
@@ -163,10 +275,38 @@ export function layoutGraph(
|
|
|
163
275
|
...(node.color !== undefined && { color: node.color }),
|
|
164
276
|
...(node.group !== undefined && { group: node.group }),
|
|
165
277
|
lineNumber: node.lineNumber,
|
|
166
|
-
x:
|
|
167
|
-
y:
|
|
168
|
-
width:
|
|
169
|
-
height:
|
|
278
|
+
x: p.x,
|
|
279
|
+
y: p.y,
|
|
280
|
+
width: p.width,
|
|
281
|
+
height: p.height,
|
|
282
|
+
...(ng &&
|
|
283
|
+
placed && {
|
|
284
|
+
// Local coords relative to the node center (translate origin).
|
|
285
|
+
note: placed.collapsed
|
|
286
|
+
? {
|
|
287
|
+
x: 0,
|
|
288
|
+
y: 0,
|
|
289
|
+
width: 0,
|
|
290
|
+
height: 0,
|
|
291
|
+
side: 'right' as NoteSide,
|
|
292
|
+
...(ng.color && { color: ng.color }),
|
|
293
|
+
lines: [],
|
|
294
|
+
lineNumber: ng.lineNumber,
|
|
295
|
+
endLineNumber: ng.endLineNumber,
|
|
296
|
+
collapsed: true,
|
|
297
|
+
}
|
|
298
|
+
: {
|
|
299
|
+
x: placed.x,
|
|
300
|
+
y: placed.y,
|
|
301
|
+
width: ng.noteW,
|
|
302
|
+
height: ng.noteH,
|
|
303
|
+
side: placed.side,
|
|
304
|
+
...(ng.color && { color: ng.color }),
|
|
305
|
+
lines: ng.lines,
|
|
306
|
+
lineNumber: ng.lineNumber,
|
|
307
|
+
endLineNumber: ng.endLineNumber,
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
170
310
|
};
|
|
171
311
|
});
|
|
172
312
|
|
|
@@ -260,29 +400,72 @@ export function layoutGraph(
|
|
|
260
400
|
}
|
|
261
401
|
}
|
|
262
402
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
403
|
+
// Content bounding box over nodes, their (floated) notes, and groups.
|
|
404
|
+
// Notes placed above/left of a node can land at negative coordinates;
|
|
405
|
+
// when they do, everything is shifted so nothing clips on export. Edges
|
|
406
|
+
// are intentionally excluded (matching the prior bounds behavior) so
|
|
407
|
+
// un-annotated layouts are byte-for-byte unchanged.
|
|
408
|
+
let bbMinX = Infinity;
|
|
409
|
+
let bbMinY = Infinity;
|
|
410
|
+
let bbMaxX = -Infinity;
|
|
411
|
+
let bbMaxY = -Infinity;
|
|
412
|
+
const extend = (l: number, t: number, r: number, b: number): void => {
|
|
413
|
+
if (l < bbMinX) bbMinX = l;
|
|
414
|
+
if (t < bbMinY) bbMinY = t;
|
|
415
|
+
if (r > bbMaxX) bbMaxX = r;
|
|
416
|
+
if (b > bbMaxY) bbMaxY = b;
|
|
417
|
+
};
|
|
266
418
|
for (const node of layoutNodes) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
419
|
+
extend(
|
|
420
|
+
node.x - node.width / 2,
|
|
421
|
+
node.y - node.height / 2,
|
|
422
|
+
node.x + node.width / 2,
|
|
423
|
+
node.y + node.height / 2
|
|
424
|
+
);
|
|
425
|
+
if (node.note) {
|
|
426
|
+
extend(
|
|
427
|
+
node.x + node.note.x,
|
|
428
|
+
node.y + node.note.y,
|
|
429
|
+
node.x + node.note.x + node.note.width,
|
|
430
|
+
node.y + node.note.y + node.note.height
|
|
431
|
+
);
|
|
432
|
+
}
|
|
271
433
|
}
|
|
272
434
|
for (const group of layoutGroups) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
435
|
+
extend(group.x, group.y, group.x + group.width, group.y + group.height);
|
|
436
|
+
}
|
|
437
|
+
if (!Number.isFinite(bbMinX)) {
|
|
438
|
+
bbMinX = 0;
|
|
439
|
+
bbMinY = 0;
|
|
440
|
+
bbMaxX = 0;
|
|
441
|
+
bbMaxY = 0;
|
|
277
442
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
443
|
+
|
|
444
|
+
// Shift only when content runs off the top/left (note placed above/left).
|
|
445
|
+
const { shiftX, shiftY } = noteCanvasShift(bbMinX, bbMinY);
|
|
446
|
+
|
|
447
|
+
const shifted = shiftX !== 0 || shiftY !== 0;
|
|
448
|
+
const finalNodes = shifted
|
|
449
|
+
? layoutNodes.map((n) => ({ ...n, x: n.x + shiftX, y: n.y + shiftY }))
|
|
450
|
+
: layoutNodes;
|
|
451
|
+
const finalEdges = shifted
|
|
452
|
+
? layoutEdges.map((e) => ({
|
|
453
|
+
...e,
|
|
454
|
+
points: e.points.map((pt) => ({ x: pt.x + shiftX, y: pt.y + shiftY })),
|
|
455
|
+
}))
|
|
456
|
+
: layoutEdges;
|
|
457
|
+
const finalGroups = shifted
|
|
458
|
+
? layoutGroups.map((gr) => ({ ...gr, x: gr.x + shiftX, y: gr.y + shiftY }))
|
|
459
|
+
: layoutGroups;
|
|
460
|
+
|
|
461
|
+
// Add margin (matches prior behavior: max-edge + 40).
|
|
462
|
+
const totalWidth = bbMaxX + shiftX + 40;
|
|
463
|
+
const totalHeight = bbMaxY + shiftY + 40;
|
|
281
464
|
|
|
282
465
|
return {
|
|
283
|
-
nodes:
|
|
284
|
-
edges:
|
|
285
|
-
groups:
|
|
466
|
+
nodes: finalNodes,
|
|
467
|
+
edges: finalEdges,
|
|
468
|
+
groups: finalGroups,
|
|
286
469
|
width: totalWidth,
|
|
287
470
|
height: totalHeight,
|
|
288
471
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Graph notes — thin re-export over the shared note pipeline
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// The note model, grammar, and resolver are now chart-neutral and live in
|
|
6
|
+
// `utils/notes/`. This module stays as the graph family's import site so
|
|
7
|
+
// the flowchart/state parsers (and their tests) keep one stable path. New
|
|
8
|
+
// charts should import from `utils/notes` directly.
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
parseNoteHeader,
|
|
12
|
+
collectNoteBody,
|
|
13
|
+
tryCollectNote,
|
|
14
|
+
resolveNotes,
|
|
15
|
+
} from '../utils/notes';
|
|
16
|
+
export type {
|
|
17
|
+
DiagramNote,
|
|
18
|
+
CollectedNoteBody,
|
|
19
|
+
TryCollectNoteResult,
|
|
20
|
+
NoteTarget,
|
|
21
|
+
} from '../utils/notes';
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from '../utils/parsing';
|
|
19
19
|
import { normalizeName, displayName } from '../utils/name-normalize';
|
|
20
20
|
import type { Writable } from '../utils/brand';
|
|
21
|
-
import type { ParsedGraph, GraphNode, GraphGroup } from './types';
|
|
21
|
+
import type { ParsedGraph, GraphNode, GraphGroup, GraphNote } from './types';
|
|
22
|
+
import { tryCollectNote, resolveNotes } from './notes';
|
|
22
23
|
|
|
23
24
|
// ============================================================
|
|
24
25
|
// Constants
|
|
@@ -203,6 +204,7 @@ export function parseState(
|
|
|
203
204
|
|
|
204
205
|
const nodeMap = new Map<string, Writable<GraphNode>>();
|
|
205
206
|
const indentStack: { nodeId: string; indent: number }[] = [];
|
|
207
|
+
const notes: GraphNote[] = [];
|
|
206
208
|
let currentGroup: Writable<GraphGroup> | null = null;
|
|
207
209
|
let groupIndent = -1;
|
|
208
210
|
const groups: Writable<GraphGroup>[] = [];
|
|
@@ -309,6 +311,23 @@ export function parseState(
|
|
|
309
311
|
}
|
|
310
312
|
}
|
|
311
313
|
|
|
314
|
+
// Note annotation: `note <ref> [inline body]` + optional indented
|
|
315
|
+
// body. Only `note -> X` (arrow immediately after `note`) is excluded
|
|
316
|
+
// so a transition FROM a state named "note" still parses; arrows are
|
|
317
|
+
// allowed inside a note body.
|
|
318
|
+
const noteResult = tryCollectNote(
|
|
319
|
+
lines,
|
|
320
|
+
i,
|
|
321
|
+
indent,
|
|
322
|
+
palette,
|
|
323
|
+
result.diagnostics
|
|
324
|
+
);
|
|
325
|
+
if (noteResult) {
|
|
326
|
+
if (noteResult.note) notes.push(noteResult.note);
|
|
327
|
+
i = noteResult.lastIndex;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
312
331
|
// Group brackets: [Name] or [Name](color)
|
|
313
332
|
const groupMatch = trimmed.match(GROUP_BRACKET_RE);
|
|
314
333
|
// Regex capture group 1 is mandatory in GROUP_BRACKET_RE.
|
|
@@ -492,6 +511,12 @@ export function parseState(
|
|
|
492
511
|
|
|
493
512
|
if (groups.length > 0) result.groups = groups;
|
|
494
513
|
|
|
514
|
+
// Resolve note refs against the state node map (forward refs OK).
|
|
515
|
+
if (notes.length > 0) {
|
|
516
|
+
result.notes = notes;
|
|
517
|
+
resolveNotes(notes, result.nodes, result.diagnostics);
|
|
518
|
+
}
|
|
519
|
+
|
|
495
520
|
// Validation: no nodes found
|
|
496
521
|
if (result.nodes.length === 0 && !result.error) {
|
|
497
522
|
const diag = makeDgmoError(
|