@diagrammo/dgmo 0.30.0 → 0.32.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/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/README.md +21 -3
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1853 -623
- package/dist/advanced.d.cts +143 -16
- package/dist/advanced.d.ts +143 -16
- package/dist/advanced.js +1846 -623
- package/dist/auto.cjs +1640 -581
- package/dist/auto.js +99 -99
- package/dist/auto.mjs +1640 -581
- package/dist/cli.cjs +148 -147
- package/dist/index.cjs +1643 -662
- package/dist/index.js +1643 -662
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +10 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +345 -65
- package/src/boxes-and-lines/layout.ts +11 -1
- package/src/boxes-and-lines/parser.ts +97 -4
- package/src/boxes-and-lines/renderer.ts +111 -8
- package/src/boxes-and-lines/types.ts +9 -0
- package/src/c4/parser.ts +8 -7
- package/src/c4/renderer.ts +7 -5
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +3 -3
- package/src/chart.ts +18 -1
- package/src/class/renderer.ts +4 -2
- package/src/cli-banner.ts +107 -0
- package/src/cli.ts +13 -0
- package/src/colors.ts +247 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/er/renderer.ts +4 -2
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/flowchart-renderer.ts +4 -2
- package/src/graph/state-renderer.ts +6 -4
- package/src/infra/parser.ts +80 -0
- package/src/infra/renderer.ts +8 -4
- package/src/journey-map/parser.ts +23 -8
- package/src/journey-map/renderer.ts +1 -1
- package/src/kanban/parser.ts +8 -7
- package/src/kanban/renderer.ts +1 -1
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +49 -25
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +55 -15
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +89 -127
- package/src/palettes/color-utils.ts +19 -4
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +15 -10
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +5 -5
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +37 -39
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/card.ts +183 -0
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +48 -10
- package/src/utils/visual-conventions.ts +61 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
boxes-and-lines E-Commerce Platform
|
|
2
2
|
|
|
3
|
-
tag Team t Backend blue, Frontend green, Platform purple
|
|
4
|
-
tag Priority p High red, Medium orange, Low gray
|
|
3
|
+
tag Team as t Backend blue, Frontend green, Platform purple
|
|
4
|
+
tag Priority as p High red, Medium orange, Low gray
|
|
5
5
|
|
|
6
6
|
active-tag Team
|
|
7
7
|
hide priority:Low
|
|
@@ -14,12 +14,16 @@ tag Team as t
|
|
|
14
14
|
|
|
15
15
|
Customer is a person t: Frontend
|
|
16
16
|
description: Browses and purchases books online
|
|
17
|
+
-Browses catalog-> Bookstore
|
|
17
18
|
|
|
18
19
|
Admin is a person t: Backend
|
|
19
20
|
description: Manages inventory and orders
|
|
21
|
+
-Manages inventory-> Bookstore
|
|
20
22
|
|
|
21
23
|
Bookstore is a system t: Backend
|
|
22
24
|
description: Core e-commerce platform for book sales
|
|
25
|
+
-Processes payments via-> PaymentGW tech: REST
|
|
26
|
+
~Sends order confirmations~> EmailSvc tech: SMTP
|
|
23
27
|
containers
|
|
24
28
|
WebApp is a container tech: React, t: Frontend
|
|
25
29
|
description: Single-page storefront application
|
|
@@ -45,8 +49,3 @@ PaymentGW is a system t: Platform
|
|
|
45
49
|
|
|
46
50
|
EmailSvc is a system t: Platform
|
|
47
51
|
description: SendGrid — transactional email delivery
|
|
48
|
-
|
|
49
|
-
Customer -Browses catalog-> Bookstore
|
|
50
|
-
Admin -Manages inventory-> Bookstore
|
|
51
|
-
Bookstore -Processes payments via-> PaymentGW tech: REST
|
|
52
|
-
Bookstore ~Sends order confirmations~> EmailSvc tech: SMTP
|
package/gallery/fixtures/c4.dgmo
CHANGED
|
@@ -2,9 +2,11 @@ c4 Internet Banking System
|
|
|
2
2
|
|
|
3
3
|
Customer is a person
|
|
4
4
|
description: A customer of the bank
|
|
5
|
+
-Uses-> Banking
|
|
5
6
|
|
|
6
7
|
Banking is a system
|
|
7
8
|
description: Core internet banking system
|
|
9
|
+
-Sends emails via-> Email
|
|
8
10
|
containers
|
|
9
11
|
WebApp is a container tech: React
|
|
10
12
|
API is a container tech: Node.js
|
|
@@ -12,6 +14,3 @@ Banking is a system
|
|
|
12
14
|
|
|
13
15
|
Email is a system
|
|
14
16
|
description: External email delivery service
|
|
15
|
-
|
|
16
|
-
Customer -Uses-> Banking
|
|
17
|
-
Banking -Sends emails via-> Email
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diagrammo/dgmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "DGMO diagram markup language — parser, renderer, and color system",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"dev": "DGMO_DEV_RELOAD=1 tsup --watch",
|
|
112
112
|
"pretest": "pnpm codegen",
|
|
113
113
|
"test": "vitest run --coverage",
|
|
114
|
+
"bench:perf": "vitest run --config vitest.perf.config.ts tests/perf-bench.ts",
|
|
114
115
|
"bench:bl": "vitest run --config vitest.bench.config.ts tests/bl-layout-bench.ts",
|
|
115
116
|
"bench:bl-collapse": "vitest run --config vitest.bench.config.ts tests/bl-collapse-bench.ts",
|
|
116
117
|
"bench:bl-options": "vitest run --config vitest.bench.config.ts tests/bl-options.ts",
|
|
@@ -157,6 +158,11 @@
|
|
|
157
158
|
"lz-string": "^1.5.0",
|
|
158
159
|
"topojson-client": "^3.1.0"
|
|
159
160
|
},
|
|
161
|
+
"pnpm": {
|
|
162
|
+
"overrides": {
|
|
163
|
+
"undici": "^7.28.0"
|
|
164
|
+
}
|
|
165
|
+
},
|
|
160
166
|
"devDependencies": {
|
|
161
167
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
162
168
|
"@codemirror/language": "^6.12.3",
|
package/src/advanced.ts
CHANGED
|
@@ -174,6 +174,9 @@ export type {
|
|
|
174
174
|
} from './graph/types';
|
|
175
175
|
|
|
176
176
|
export type { TagGroup, TagEntry } from './utils/tag-groups';
|
|
177
|
+
// The canonical categorical auto-color rotation (RGB-seeded, max-contrast,
|
|
178
|
+
// neutrals excluded) — so app/editor swatch cyclers share dgmo's exact order.
|
|
179
|
+
export { autoTagColorCycle } from './utils/tag-groups';
|
|
177
180
|
|
|
178
181
|
export { parseInlineMarkdown, truncateBareUrl } from './utils/inline-markdown';
|
|
179
182
|
export type { InlineSpan } from './utils/inline-markdown';
|
|
@@ -275,6 +278,8 @@ export {
|
|
|
275
278
|
|
|
276
279
|
export { collapseBoxesAndLines } from './boxes-and-lines/collapse';
|
|
277
280
|
export type { BLCollapseResult } from './boxes-and-lines/collapse';
|
|
281
|
+
export { focusBoxesAndLines } from './boxes-and-lines/focus';
|
|
282
|
+
export type { FocusTarget, FocusResult } from './boxes-and-lines/focus';
|
|
278
283
|
|
|
279
284
|
export { parseSitemap, looksLikeSitemap } from './sitemap/parser';
|
|
280
285
|
|
|
@@ -702,6 +707,11 @@ export {
|
|
|
702
707
|
seriesColors,
|
|
703
708
|
RECOGNIZED_COLOR_NAMES,
|
|
704
709
|
isRecognizedColorName,
|
|
710
|
+
INVALID_COLOR_CODE,
|
|
711
|
+
nearestNamedColor,
|
|
712
|
+
isInvalidColorToken,
|
|
713
|
+
invalidColorDiagnostic,
|
|
714
|
+
INVALID_CSS_COLOR_HEX,
|
|
705
715
|
} from './colors';
|
|
706
716
|
|
|
707
717
|
export {
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Boxes and Lines — Focus (1-hop neighborhood) Transform
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// Pure transform that filters a parsed boxes-and-lines diagram down to a single
|
|
6
|
+
// focused element plus its direct (1-hop) graph neighbours, mirroring the shape
|
|
7
|
+
// of `org/collapse.ts#focusOrgTree` but for a general graph instead of a tree.
|
|
8
|
+
//
|
|
9
|
+
// Unlike the org tree (subtree extraction), boxes-and-lines is a general graph,
|
|
10
|
+
// so "focus" = the focused element + everything one edge away. Neighbour groups
|
|
11
|
+
// come back COLLAPSED (reusing `collapseBoxesAndLines`'s edge-redirect + dedup),
|
|
12
|
+
// the focused group comes back EXPANDED, and everything else is hidden.
|
|
13
|
+
//
|
|
14
|
+
// Composition: this owns ALL collapse decisions for the focused view, so it is
|
|
15
|
+
// fed the un-(manually-)collapsed parsed model — focus supersedes the user's
|
|
16
|
+
// manual collapse selection for its duration (Decision 12). It runs after
|
|
17
|
+
// tag-hide in the app pipeline (FM10).
|
|
18
|
+
|
|
19
|
+
import type { ParsedBoxesAndLines, BLGroup, BLEdge } from './types';
|
|
20
|
+
import { collapseBoxesAndLines } from './collapse';
|
|
21
|
+
|
|
22
|
+
const GROUP_PREFIX = '__group_';
|
|
23
|
+
const groupKey = (label: string): string => `${GROUP_PREFIX}${label}`;
|
|
24
|
+
const isGroupKey = (k: string): boolean => k.startsWith(GROUP_PREFIX);
|
|
25
|
+
const groupLabelOf = (k: string): string => k.slice(GROUP_PREFIX.length);
|
|
26
|
+
|
|
27
|
+
export interface FocusTarget {
|
|
28
|
+
readonly kind: 'box' | 'group';
|
|
29
|
+
/** Canonical endpoint key the parser uses for edges: a node label for a box,
|
|
30
|
+
* or `__group_<label>` for a group. */
|
|
31
|
+
readonly id: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FocusResult {
|
|
35
|
+
/** Filtered model to lay out + render (neighbour groups already collapsed via
|
|
36
|
+
* `collapseBoxesAndLines`; `nodePositions` cleared so the subset auto-lays). */
|
|
37
|
+
readonly parsed: ParsedBoxesAndLines;
|
|
38
|
+
/** Canonical keys of the 1-hop neighbours kept in view (box labels +
|
|
39
|
+
* `__group_<label>` for neighbour groups). */
|
|
40
|
+
readonly neighborIds: Set<string>;
|
|
41
|
+
/** Group LABELS of neighbours rendered collapsed. */
|
|
42
|
+
readonly collapsedNeighborGroupIds: Set<string>;
|
|
43
|
+
/** GLOBAL value-ramp domain computed from the ORIGINAL model before filtering
|
|
44
|
+
* (Decision 20 / FM1); null when the diagram has no `value:` data. */
|
|
45
|
+
readonly rampDomain: { min: number; max: number } | null;
|
|
46
|
+
/** Collapse metadata for `layoutBoxesAndLines` so neighbour groups materialise
|
|
47
|
+
* as collapsed boxes — mirrors the manual-collapse path's `collapseInfo`. */
|
|
48
|
+
readonly collapseInfo: {
|
|
49
|
+
collapsedChildCounts: Map<string, number>;
|
|
50
|
+
originalGroups: readonly BLGroup[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Filter `parsed` to the focused element + its 1-hop neighbours.
|
|
56
|
+
*
|
|
57
|
+
* Pure, synchronous, no I/O. Tolerant of dangling/alias endpoints (skips them,
|
|
58
|
+
* never throws). For an edge-less target it returns the lone element (the app
|
|
59
|
+
* decides the "no connections" affordance, Decision 19).
|
|
60
|
+
*/
|
|
61
|
+
export function focusBoxesAndLines(
|
|
62
|
+
parsed: ParsedBoxesAndLines,
|
|
63
|
+
target: FocusTarget
|
|
64
|
+
): FocusResult {
|
|
65
|
+
// ── Step 1: GLOBAL ramp domain, computed BEFORE any filtering (Dec 20/FM1) ──
|
|
66
|
+
const allValues = parsed.nodes
|
|
67
|
+
.filter((n) => n.value !== undefined)
|
|
68
|
+
.map((n) => n.value!);
|
|
69
|
+
const rampDomain =
|
|
70
|
+
allValues.length > 0
|
|
71
|
+
? { min: Math.min(...allValues), max: Math.max(...allValues) }
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
// ── Lookups ──
|
|
75
|
+
const nodeLabelSet = new Set(parsed.nodes.map((n) => n.label));
|
|
76
|
+
const groupByLabel = new Map<string, BLGroup>();
|
|
77
|
+
for (const g of parsed.groups) groupByLabel.set(g.label, g);
|
|
78
|
+
// child label (node or sub-group) → its direct parent group label
|
|
79
|
+
const parentOf = new Map<string, string>();
|
|
80
|
+
for (const g of parsed.groups)
|
|
81
|
+
for (const child of g.children) parentOf.set(child, g.label);
|
|
82
|
+
|
|
83
|
+
/** Top-most ancestor group of a node/group label (undefined if top-level). */
|
|
84
|
+
const topAncestor = (label: string): string | undefined => {
|
|
85
|
+
let p = parentOf.get(label);
|
|
86
|
+
if (p === undefined) return undefined;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const up = parentOf.get(p);
|
|
89
|
+
if (up === undefined) return p;
|
|
90
|
+
p = up;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** All descendant node + sub-group labels of a group (recursive, cycle-safe). */
|
|
95
|
+
const descendantsOf = (
|
|
96
|
+
groupLabel: string
|
|
97
|
+
): { nodes: Set<string>; groups: Set<string> } => {
|
|
98
|
+
const nodes = new Set<string>();
|
|
99
|
+
const groups = new Set<string>();
|
|
100
|
+
const seen = new Set<string>([groupLabel]);
|
|
101
|
+
const stack = [groupLabel];
|
|
102
|
+
while (stack.length) {
|
|
103
|
+
const cur = stack.pop()!;
|
|
104
|
+
const g = groupByLabel.get(cur);
|
|
105
|
+
if (!g) continue;
|
|
106
|
+
for (const child of g.children) {
|
|
107
|
+
if (groupByLabel.has(child)) {
|
|
108
|
+
if (!seen.has(child)) {
|
|
109
|
+
seen.add(child);
|
|
110
|
+
groups.add(child);
|
|
111
|
+
stack.push(child);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
nodes.add(child);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { nodes, groups };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// ── Step 2: resolve the focused element ──
|
|
122
|
+
// focusNodeLabels — focus boxes shown standalone (focused box, or a group's members)
|
|
123
|
+
// focusGroupLabels — groups kept EXPANDED (focused group + its sub-groups)
|
|
124
|
+
// focusEndpointSet — canonical keys that count as "the focus" for edge adjacency
|
|
125
|
+
const focusNodeLabels = new Set<string>();
|
|
126
|
+
const focusGroupLabels = new Set<string>();
|
|
127
|
+
const focusEndpointSet = new Set<string>();
|
|
128
|
+
|
|
129
|
+
if (target.kind === 'group') {
|
|
130
|
+
const gl = isGroupKey(target.id) ? groupLabelOf(target.id) : target.id;
|
|
131
|
+
if (groupByLabel.has(gl)) {
|
|
132
|
+
focusGroupLabels.add(gl);
|
|
133
|
+
focusEndpointSet.add(groupKey(gl));
|
|
134
|
+
const desc = descendantsOf(gl);
|
|
135
|
+
for (const n of desc.nodes) {
|
|
136
|
+
focusNodeLabels.add(n);
|
|
137
|
+
focusEndpointSet.add(n);
|
|
138
|
+
}
|
|
139
|
+
for (const sg of desc.groups) {
|
|
140
|
+
focusGroupLabels.add(sg);
|
|
141
|
+
focusEndpointSet.add(groupKey(sg));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// box — shown standalone even if it belongs to a group (framing stripped).
|
|
146
|
+
if (nodeLabelSet.has(target.id)) {
|
|
147
|
+
focusNodeLabels.add(target.id);
|
|
148
|
+
focusEndpointSet.add(target.id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Step 3: walk edges → neighbours + kept (focus-incident / internal) edges ──
|
|
153
|
+
const neighborIds = new Set<string>();
|
|
154
|
+
const collapsedNeighborGroupIds = new Set<string>();
|
|
155
|
+
const neighborBoxes = new Set<string>();
|
|
156
|
+
const keepGroupLabels = new Set<string>(focusGroupLabels);
|
|
157
|
+
const keptEdges: BLEdge[] = [];
|
|
158
|
+
const selfLoops: BLEdge[] = [];
|
|
159
|
+
|
|
160
|
+
const keepGroupAndSubgroups = (gl: string): void => {
|
|
161
|
+
keepGroupLabels.add(gl);
|
|
162
|
+
for (const sg of descendantsOf(gl).groups) keepGroupLabels.add(sg);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Classify a neighbour endpoint; returns whether the incident edge survives. */
|
|
166
|
+
const classifyNeighbor = (key: string): boolean => {
|
|
167
|
+
if (isGroupKey(key)) {
|
|
168
|
+
const gl = groupLabelOf(key);
|
|
169
|
+
if (!groupByLabel.has(gl)) return false; // dangling group ref (FM7)
|
|
170
|
+
if (focusGroupLabels.has(gl)) return true; // part of focus (internal)
|
|
171
|
+
neighborIds.add(key);
|
|
172
|
+
collapsedNeighborGroupIds.add(gl);
|
|
173
|
+
keepGroupAndSubgroups(gl);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (focusNodeLabels.has(key)) return true; // part of focus
|
|
177
|
+
if (!nodeLabelSet.has(key)) return false; // dangling box endpoint (FM7)
|
|
178
|
+
// A neighbour box inside a group → collapse that group so the edge redirects
|
|
179
|
+
// to a single collapsed box (AC3 / Dec 11); never expand the group (FM4).
|
|
180
|
+
const top = topAncestor(key);
|
|
181
|
+
if (top !== undefined && !focusGroupLabels.has(top)) {
|
|
182
|
+
neighborIds.add(groupKey(top));
|
|
183
|
+
collapsedNeighborGroupIds.add(top);
|
|
184
|
+
keepGroupAndSubgroups(top);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
// Standalone neighbour box (no group, or already inside the focused group).
|
|
188
|
+
neighborIds.add(key);
|
|
189
|
+
neighborBoxes.add(key);
|
|
190
|
+
return true;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
for (const edge of parsed.edges) {
|
|
194
|
+
const inS = focusEndpointSet.has(edge.source);
|
|
195
|
+
const inT = focusEndpointSet.has(edge.target);
|
|
196
|
+
if (edge.source === edge.target) {
|
|
197
|
+
// Self-loop: kept iff incident to focus, re-added after the collapse pass
|
|
198
|
+
// (which drops src===tgt) — FM5.
|
|
199
|
+
if (inS) selfLoops.push(edge);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (inS && inT) {
|
|
203
|
+
keptEdges.push(edge); // internal (member↔member inside focused group) — FM3
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!inS && !inT) continue; // unrelated to focus
|
|
207
|
+
const other = inS ? edge.target : edge.source;
|
|
208
|
+
if (classifyNeighbor(other)) keptEdges.push(edge);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Step 4: build the filtered model ──
|
|
212
|
+
const keepNodeLabels = new Set<string>([
|
|
213
|
+
...focusNodeLabels,
|
|
214
|
+
...neighborBoxes,
|
|
215
|
+
]);
|
|
216
|
+
const nodes = parsed.nodes.filter((n) => keepNodeLabels.has(n.label));
|
|
217
|
+
const groups = parsed.groups.filter((g) => keepGroupLabels.has(g.label));
|
|
218
|
+
// Notes follow their owner: kept iff the owning box survives (FM9).
|
|
219
|
+
const notes = parsed.notes?.filter((note) => keepNodeLabels.has(note.ref));
|
|
220
|
+
|
|
221
|
+
const filtered: ParsedBoxesAndLines = {
|
|
222
|
+
...parsed,
|
|
223
|
+
nodes,
|
|
224
|
+
edges: keptEdges,
|
|
225
|
+
groups,
|
|
226
|
+
...(notes !== undefined && { notes }),
|
|
227
|
+
};
|
|
228
|
+
// Clear pinned positions so the subset auto-lays-out (Dec 21 / FM2). Pins are
|
|
229
|
+
// restored on exit because exit re-renders the original unfiltered model.
|
|
230
|
+
delete (filtered as { nodePositions?: unknown }).nodePositions;
|
|
231
|
+
|
|
232
|
+
// ── Step 5: collapse neighbour groups via the shared redirect+dedup (ADR-3) ──
|
|
233
|
+
const collapsed = collapseBoxesAndLines(filtered, collapsedNeighborGroupIds);
|
|
234
|
+
|
|
235
|
+
// ── Step 6: re-add self-loops the collapse pass dropped (FM5) ──
|
|
236
|
+
let resultParsed = collapsed.parsed;
|
|
237
|
+
if (selfLoops.length > 0) {
|
|
238
|
+
const visible = new Set(resultParsed.nodes.map((n) => n.label));
|
|
239
|
+
const survivors = selfLoops.filter((e) => visible.has(e.source));
|
|
240
|
+
if (survivors.length > 0)
|
|
241
|
+
resultParsed = {
|
|
242
|
+
...resultParsed,
|
|
243
|
+
edges: [...resultParsed.edges, ...survivors],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
parsed: resultParsed,
|
|
249
|
+
neighborIds,
|
|
250
|
+
collapsedNeighborGroupIds,
|
|
251
|
+
rampDomain,
|
|
252
|
+
collapseInfo: {
|
|
253
|
+
collapsedChildCounts: collapsed.collapsedChildCounts,
|
|
254
|
+
originalGroups: collapsed.originalGroups,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|