@diagrammo/dgmo 0.8.2 → 0.8.4
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/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +189 -194
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/c4/layout.ts
CHANGED
|
@@ -3,9 +3,20 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import dagre from '@dagrejs/dagre';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ParsedC4,
|
|
8
|
+
C4Element,
|
|
9
|
+
C4Relationship,
|
|
10
|
+
C4ArrowType,
|
|
11
|
+
C4Shape,
|
|
12
|
+
C4DeploymentNode,
|
|
13
|
+
} from './types';
|
|
7
14
|
import type { TagGroup } from '../utils/tag-groups';
|
|
8
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
LEGEND_PILL_FONT_SIZE,
|
|
17
|
+
LEGEND_ENTRY_FONT_SIZE,
|
|
18
|
+
measureLegendText,
|
|
19
|
+
} from '../utils/legend-constants';
|
|
9
20
|
|
|
10
21
|
// ============================================================
|
|
11
22
|
// Types
|
|
@@ -141,7 +152,10 @@ function computeEdgePenalty(
|
|
|
141
152
|
const tx = nodePositions.get(edge.target);
|
|
142
153
|
if (sx == null || tx == null) continue;
|
|
143
154
|
const dist = Math.abs(sx - tx);
|
|
144
|
-
const weight = Math.min(
|
|
155
|
+
const weight = Math.min(
|
|
156
|
+
degrees.get(edge.source) ?? 1,
|
|
157
|
+
degrees.get(edge.target) ?? 1
|
|
158
|
+
);
|
|
145
159
|
penalty += dist * weight;
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -216,7 +230,12 @@ function reduceCrossings(
|
|
|
216
230
|
const nodeGeometry = new Map<string, NodeGeometry>();
|
|
217
231
|
for (const name of g.nodes()) {
|
|
218
232
|
const pos = g.node(name);
|
|
219
|
-
if (pos)
|
|
233
|
+
if (pos)
|
|
234
|
+
nodeGeometry.set(name, {
|
|
235
|
+
y: pos.y,
|
|
236
|
+
width: pos.width,
|
|
237
|
+
height: pos.height,
|
|
238
|
+
});
|
|
220
239
|
}
|
|
221
240
|
|
|
222
241
|
// Group nodes by rank
|
|
@@ -265,7 +284,9 @@ function reduceCrossings(
|
|
|
265
284
|
if (partition.length < 2) continue;
|
|
266
285
|
|
|
267
286
|
// Collect the x-slots for this partition (sorted)
|
|
268
|
-
const xSlots = partition
|
|
287
|
+
const xSlots = partition
|
|
288
|
+
.map((name) => g.node(name).x)
|
|
289
|
+
.sort((a, b) => a - b);
|
|
269
290
|
|
|
270
291
|
// Build position map snapshot
|
|
271
292
|
const basePositions = new Map<string, number>();
|
|
@@ -275,7 +296,12 @@ function reduceCrossings(
|
|
|
275
296
|
}
|
|
276
297
|
|
|
277
298
|
// Current penalty
|
|
278
|
-
const currentPenalty = computeEdgePenalty(
|
|
299
|
+
const currentPenalty = computeEdgePenalty(
|
|
300
|
+
edgeList,
|
|
301
|
+
basePositions,
|
|
302
|
+
degrees,
|
|
303
|
+
nodeGeometry
|
|
304
|
+
);
|
|
279
305
|
|
|
280
306
|
// Try permutations (feasible for partition sizes ≤ 8)
|
|
281
307
|
let bestPerm = [...partition];
|
|
@@ -288,7 +314,12 @@ function reduceCrossings(
|
|
|
288
314
|
for (let i = 0; i < perm.length; i++) {
|
|
289
315
|
testPositions.set(perm[i]!, xSlots[i]!);
|
|
290
316
|
}
|
|
291
|
-
const penalty = computeEdgePenalty(
|
|
317
|
+
const penalty = computeEdgePenalty(
|
|
318
|
+
edgeList,
|
|
319
|
+
testPositions,
|
|
320
|
+
degrees,
|
|
321
|
+
nodeGeometry
|
|
322
|
+
);
|
|
292
323
|
if (penalty < bestPenalty) {
|
|
293
324
|
bestPenalty = penalty;
|
|
294
325
|
bestPerm = [...perm];
|
|
@@ -307,14 +338,27 @@ function reduceCrossings(
|
|
|
307
338
|
for (let k = 0; k < workingOrder.length; k++) {
|
|
308
339
|
testPositions.set(workingOrder[k]!, xSlots[k]!);
|
|
309
340
|
}
|
|
310
|
-
const before = computeEdgePenalty(
|
|
311
|
-
|
|
312
|
-
|
|
341
|
+
const before = computeEdgePenalty(
|
|
342
|
+
edgeList,
|
|
343
|
+
testPositions,
|
|
344
|
+
degrees,
|
|
345
|
+
nodeGeometry
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
[workingOrder[i], workingOrder[i + 1]] = [
|
|
349
|
+
workingOrder[i + 1]!,
|
|
350
|
+
workingOrder[i]!,
|
|
351
|
+
];
|
|
313
352
|
const testPositions2 = new Map(basePositions);
|
|
314
353
|
for (let k = 0; k < workingOrder.length; k++) {
|
|
315
354
|
testPositions2.set(workingOrder[k]!, xSlots[k]!);
|
|
316
355
|
}
|
|
317
|
-
const after = computeEdgePenalty(
|
|
356
|
+
const after = computeEdgePenalty(
|
|
357
|
+
edgeList,
|
|
358
|
+
testPositions2,
|
|
359
|
+
degrees,
|
|
360
|
+
nodeGeometry
|
|
361
|
+
);
|
|
318
362
|
|
|
319
363
|
if (after < before) {
|
|
320
364
|
improved = true;
|
|
@@ -323,7 +367,10 @@ function reduceCrossings(
|
|
|
323
367
|
bestPerm = [...workingOrder];
|
|
324
368
|
}
|
|
325
369
|
} else {
|
|
326
|
-
[workingOrder[i], workingOrder[i + 1]] = [
|
|
370
|
+
[workingOrder[i], workingOrder[i + 1]] = [
|
|
371
|
+
workingOrder[i + 1]!,
|
|
372
|
+
workingOrder[i]!,
|
|
373
|
+
];
|
|
327
374
|
}
|
|
328
375
|
}
|
|
329
376
|
}
|
|
@@ -438,7 +485,7 @@ function buildOwnershipMap(elements: C4Element[]): Map<string, string> {
|
|
|
438
485
|
*/
|
|
439
486
|
function collectAllRelationships(
|
|
440
487
|
elements: C4Element[],
|
|
441
|
-
|
|
488
|
+
_ownerMap: Map<string, string>
|
|
442
489
|
): { sourceName: string; rel: C4Relationship }[] {
|
|
443
490
|
const result: { sourceName: string; rel: C4Relationship }[] = [];
|
|
444
491
|
|
|
@@ -469,7 +516,9 @@ function collectAllRelationships(
|
|
|
469
516
|
* - Deduplicates: same source→target pair keeps only one (first seen).
|
|
470
517
|
* - Explicit system-level relationships override rolled-up ones.
|
|
471
518
|
*/
|
|
472
|
-
export function rollUpContextRelationships(
|
|
519
|
+
export function rollUpContextRelationships(
|
|
520
|
+
parsed: ParsedC4
|
|
521
|
+
): ContextRelationship[] {
|
|
473
522
|
const ownerMap = buildOwnershipMap(parsed.elements);
|
|
474
523
|
const allRels = collectAllRelationships(parsed.elements, ownerMap);
|
|
475
524
|
|
|
@@ -581,7 +630,12 @@ function wrapText(text: string, maxWidth: number, charWidth: number): string[] {
|
|
|
581
630
|
}
|
|
582
631
|
|
|
583
632
|
/** Keys to exclude from the below-divider metadata display. */
|
|
584
|
-
const META_EXCLUDE_KEYS = new Set([
|
|
633
|
+
const META_EXCLUDE_KEYS = new Set([
|
|
634
|
+
'description',
|
|
635
|
+
'tech',
|
|
636
|
+
'technology',
|
|
637
|
+
'is a',
|
|
638
|
+
]);
|
|
585
639
|
|
|
586
640
|
/** Collect displayable metadata entries for a container card. */
|
|
587
641
|
export function collectCardMetadata(
|
|
@@ -629,7 +683,9 @@ export function computeC4NodeDimensions(
|
|
|
629
683
|
// Widen card if metadata rows need more space
|
|
630
684
|
const maxMetaWidth = Math.max(
|
|
631
685
|
...metaEntries.map(
|
|
632
|
-
(e) =>
|
|
686
|
+
(e) =>
|
|
687
|
+
(e.key.length + 2 + e.value.length) * META_CHAR_WIDTH +
|
|
688
|
+
CARD_H_PAD * 2
|
|
633
689
|
)
|
|
634
690
|
);
|
|
635
691
|
if (maxMetaWidth > width) width = Math.min(MAX_NODE_WIDTH, maxMetaWidth);
|
|
@@ -669,7 +725,9 @@ function computeLegendGroups(tagGroups: TagGroup[]): C4LegendGroup[] {
|
|
|
669
725
|
if (entries.length === 0) continue;
|
|
670
726
|
|
|
671
727
|
// Compute pill width: group name + entries
|
|
672
|
-
const nameW =
|
|
728
|
+
const nameW =
|
|
729
|
+
measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
|
|
730
|
+
LEGEND_PILL_PAD * 2;
|
|
673
731
|
let capsuleW = LEGEND_CAPSULE_PAD;
|
|
674
732
|
for (const e of entries) {
|
|
675
733
|
capsuleW +=
|
|
@@ -738,7 +796,14 @@ export function layoutC4Context(
|
|
|
738
796
|
);
|
|
739
797
|
|
|
740
798
|
if (contextElements.length === 0) {
|
|
741
|
-
return {
|
|
799
|
+
return {
|
|
800
|
+
nodes: [],
|
|
801
|
+
edges: [],
|
|
802
|
+
legend: [],
|
|
803
|
+
groupBoundaries: [],
|
|
804
|
+
width: 0,
|
|
805
|
+
height: 0,
|
|
806
|
+
};
|
|
742
807
|
}
|
|
743
808
|
|
|
744
809
|
// Roll up relationships
|
|
@@ -768,7 +833,10 @@ export function layoutC4Context(
|
|
|
768
833
|
// Add edges — only between known nodes
|
|
769
834
|
const validRels: ContextRelationship[] = [];
|
|
770
835
|
for (const rel of contextRels) {
|
|
771
|
-
if (
|
|
836
|
+
if (
|
|
837
|
+
nameToElement.has(rel.sourceName) &&
|
|
838
|
+
nameToElement.has(rel.targetName)
|
|
839
|
+
) {
|
|
772
840
|
validRels.push(rel);
|
|
773
841
|
g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
|
|
774
842
|
}
|
|
@@ -786,7 +854,11 @@ export function layoutC4Context(
|
|
|
786
854
|
// Extract positioned nodes
|
|
787
855
|
const nodes: C4LayoutNode[] = contextElements.map((el) => {
|
|
788
856
|
const pos = g.node(el.name);
|
|
789
|
-
const color = resolveNodeColor(
|
|
857
|
+
const color = resolveNodeColor(
|
|
858
|
+
el,
|
|
859
|
+
parsed.tagGroups,
|
|
860
|
+
activeTagGroup ?? null
|
|
861
|
+
);
|
|
790
862
|
const hasContainers =
|
|
791
863
|
el.children.some((c) => c.type === 'container') ||
|
|
792
864
|
el.groups.some((g) => g.children.some((c) => c.type === 'container'));
|
|
@@ -822,7 +894,10 @@ export function layoutC4Context(
|
|
|
822
894
|
});
|
|
823
895
|
|
|
824
896
|
// Compute bounding box of all content (nodes + edge points)
|
|
825
|
-
let minX = Infinity,
|
|
897
|
+
let minX = Infinity,
|
|
898
|
+
minY = Infinity,
|
|
899
|
+
maxX = -Infinity,
|
|
900
|
+
maxY = -Infinity;
|
|
826
901
|
for (const node of nodes) {
|
|
827
902
|
const left = node.x - node.width / 2;
|
|
828
903
|
const top = node.y - node.height / 2;
|
|
@@ -880,7 +955,14 @@ export function layoutC4Context(
|
|
|
880
955
|
if (legendBottom > totalHeight) totalHeight = legendBottom;
|
|
881
956
|
}
|
|
882
957
|
|
|
883
|
-
return {
|
|
958
|
+
return {
|
|
959
|
+
nodes,
|
|
960
|
+
edges,
|
|
961
|
+
legend: legendGroups,
|
|
962
|
+
groupBoundaries: [],
|
|
963
|
+
width: totalWidth,
|
|
964
|
+
height: totalHeight,
|
|
965
|
+
};
|
|
884
966
|
}
|
|
885
967
|
|
|
886
968
|
// ============================================================
|
|
@@ -901,7 +983,14 @@ export function layoutC4Containers(
|
|
|
901
983
|
(el) => el.name.toLowerCase() === systemName.toLowerCase()
|
|
902
984
|
);
|
|
903
985
|
if (!system) {
|
|
904
|
-
return {
|
|
986
|
+
return {
|
|
987
|
+
nodes: [],
|
|
988
|
+
edges: [],
|
|
989
|
+
legend: [],
|
|
990
|
+
groupBoundaries: [],
|
|
991
|
+
width: 0,
|
|
992
|
+
height: 0,
|
|
993
|
+
};
|
|
905
994
|
}
|
|
906
995
|
|
|
907
996
|
// Collect all containers: direct children + group children
|
|
@@ -916,7 +1005,14 @@ export function layoutC4Containers(
|
|
|
916
1005
|
}
|
|
917
1006
|
|
|
918
1007
|
if (containers.length === 0) {
|
|
919
|
-
return {
|
|
1008
|
+
return {
|
|
1009
|
+
nodes: [],
|
|
1010
|
+
edges: [],
|
|
1011
|
+
legend: [],
|
|
1012
|
+
groupBoundaries: [],
|
|
1013
|
+
width: 0,
|
|
1014
|
+
height: 0,
|
|
1015
|
+
};
|
|
920
1016
|
}
|
|
921
1017
|
|
|
922
1018
|
const containerNames = new Set(containers.map((c) => c.name.toLowerCase()));
|
|
@@ -962,11 +1058,17 @@ export function layoutC4Containers(
|
|
|
962
1058
|
}
|
|
963
1059
|
|
|
964
1060
|
// Build element-to-group mapping for compound graph
|
|
965
|
-
const elementToGroup = new Map<
|
|
1061
|
+
const elementToGroup = new Map<
|
|
1062
|
+
string,
|
|
1063
|
+
{ name: string; lineNumber: number }
|
|
1064
|
+
>();
|
|
966
1065
|
for (const group of system.groups) {
|
|
967
1066
|
for (const child of group.children) {
|
|
968
1067
|
if (child.type === 'container') {
|
|
969
|
-
elementToGroup.set(child.name, {
|
|
1068
|
+
elementToGroup.set(child.name, {
|
|
1069
|
+
name: group.name,
|
|
1070
|
+
lineNumber: group.lineNumber,
|
|
1071
|
+
});
|
|
970
1072
|
}
|
|
971
1073
|
}
|
|
972
1074
|
}
|
|
@@ -1075,7 +1177,10 @@ export function layoutC4Containers(
|
|
|
1075
1177
|
|
|
1076
1178
|
// Add edges to dagre
|
|
1077
1179
|
for (const rel of containerRels) {
|
|
1078
|
-
if (
|
|
1180
|
+
if (
|
|
1181
|
+
nameToElement.has(rel.sourceName) &&
|
|
1182
|
+
nameToElement.has(rel.targetName)
|
|
1183
|
+
) {
|
|
1079
1184
|
g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
|
|
1080
1185
|
}
|
|
1081
1186
|
}
|
|
@@ -1090,7 +1195,10 @@ export function layoutC4Containers(
|
|
|
1090
1195
|
reduceCrossings(
|
|
1091
1196
|
g,
|
|
1092
1197
|
containerRels
|
|
1093
|
-
.filter(
|
|
1198
|
+
.filter(
|
|
1199
|
+
(r) =>
|
|
1200
|
+
nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
|
|
1201
|
+
)
|
|
1094
1202
|
.map((r) => ({ source: r.sourceName, target: r.targetName })),
|
|
1095
1203
|
nodeGroupMap
|
|
1096
1204
|
);
|
|
@@ -1099,7 +1207,11 @@ export function layoutC4Containers(
|
|
|
1099
1207
|
const nodes: C4LayoutNode[] = [];
|
|
1100
1208
|
for (const el of containers) {
|
|
1101
1209
|
const pos = g.node(el.name);
|
|
1102
|
-
const color = resolveNodeColor(
|
|
1210
|
+
const color = resolveNodeColor(
|
|
1211
|
+
el,
|
|
1212
|
+
parsed.tagGroups,
|
|
1213
|
+
activeTagGroup ?? null
|
|
1214
|
+
);
|
|
1103
1215
|
const tech = el.metadata['tech'] ?? el.metadata['technology'];
|
|
1104
1216
|
const hasComponents =
|
|
1105
1217
|
el.children.some((c) => c.type === 'component') ||
|
|
@@ -1125,7 +1237,11 @@ export function layoutC4Containers(
|
|
|
1125
1237
|
|
|
1126
1238
|
for (const el of externals) {
|
|
1127
1239
|
const pos = g.node(el.name);
|
|
1128
|
-
const color = resolveNodeColor(
|
|
1240
|
+
const color = resolveNodeColor(
|
|
1241
|
+
el,
|
|
1242
|
+
parsed.tagGroups,
|
|
1243
|
+
activeTagGroup ?? null
|
|
1244
|
+
);
|
|
1129
1245
|
nodes.push({
|
|
1130
1246
|
id: el.name,
|
|
1131
1247
|
name: el.name,
|
|
@@ -1143,7 +1259,10 @@ export function layoutC4Containers(
|
|
|
1143
1259
|
|
|
1144
1260
|
// Extract edges
|
|
1145
1261
|
const edges: C4LayoutEdge[] = containerRels
|
|
1146
|
-
.filter(
|
|
1262
|
+
.filter(
|
|
1263
|
+
(rel) =>
|
|
1264
|
+
nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
|
|
1265
|
+
)
|
|
1147
1266
|
.map((rel) => {
|
|
1148
1267
|
const edgeData = g.edge(rel.sourceName, rel.targetName);
|
|
1149
1268
|
return {
|
|
@@ -1159,7 +1278,10 @@ export function layoutC4Containers(
|
|
|
1159
1278
|
|
|
1160
1279
|
// Compute boundary box from container nodes only
|
|
1161
1280
|
const containerNodes = nodes.filter((n) => n.type === 'container');
|
|
1162
|
-
let bMinX = Infinity,
|
|
1281
|
+
let bMinX = Infinity,
|
|
1282
|
+
bMinY = Infinity,
|
|
1283
|
+
bMaxX = -Infinity,
|
|
1284
|
+
bMaxY = -Infinity;
|
|
1163
1285
|
for (const n of containerNodes) {
|
|
1164
1286
|
const left = n.x - n.width / 2;
|
|
1165
1287
|
const top = n.y - n.height / 2;
|
|
@@ -1177,15 +1299,18 @@ export function layoutC4Containers(
|
|
|
1177
1299
|
lineNumber: system.lineNumber,
|
|
1178
1300
|
x: bMinX - BOUNDARY_PAD,
|
|
1179
1301
|
y: bMinY - BOUNDARY_PAD,
|
|
1180
|
-
width:
|
|
1181
|
-
height:
|
|
1302
|
+
width: bMaxX - bMinX + BOUNDARY_PAD * 2,
|
|
1303
|
+
height: bMaxY - bMinY + BOUNDARY_PAD * 2,
|
|
1182
1304
|
};
|
|
1183
1305
|
|
|
1184
1306
|
// Compute group boundaries from member node positions
|
|
1185
1307
|
const groupBoundaries: C4LayoutBoundary[] = [];
|
|
1186
1308
|
if (hasGroups) {
|
|
1187
1309
|
const nodeMap = new Map(containerNodes.map((n) => [n.name, n]));
|
|
1188
|
-
const seenGroups = new Map<
|
|
1310
|
+
const seenGroups = new Map<
|
|
1311
|
+
string,
|
|
1312
|
+
{ lineNumber: number; members: C4LayoutNode[] }
|
|
1313
|
+
>();
|
|
1189
1314
|
for (const [elName, grp] of elementToGroup) {
|
|
1190
1315
|
const node = nodeMap.get(elName);
|
|
1191
1316
|
if (!node) continue;
|
|
@@ -1196,7 +1321,10 @@ export function layoutC4Containers(
|
|
|
1196
1321
|
}
|
|
1197
1322
|
for (const [groupName, { lineNumber, members }] of seenGroups) {
|
|
1198
1323
|
if (members.length === 0) continue;
|
|
1199
|
-
let gMinX = Infinity,
|
|
1324
|
+
let gMinX = Infinity,
|
|
1325
|
+
gMinY = Infinity,
|
|
1326
|
+
gMaxX = -Infinity,
|
|
1327
|
+
gMaxY = -Infinity;
|
|
1200
1328
|
for (const m of members) {
|
|
1201
1329
|
const left = m.x - m.width / 2;
|
|
1202
1330
|
const top = m.y - m.height / 2;
|
|
@@ -1213,14 +1341,17 @@ export function layoutC4Containers(
|
|
|
1213
1341
|
lineNumber,
|
|
1214
1342
|
x: gMinX - GROUP_BOUNDARY_PAD,
|
|
1215
1343
|
y: gMinY - GROUP_BOUNDARY_PAD,
|
|
1216
|
-
width:
|
|
1217
|
-
height:
|
|
1344
|
+
width: gMaxX - gMinX + GROUP_BOUNDARY_PAD * 2,
|
|
1345
|
+
height: gMaxY - gMinY + GROUP_BOUNDARY_PAD * 2,
|
|
1218
1346
|
});
|
|
1219
1347
|
}
|
|
1220
1348
|
}
|
|
1221
1349
|
|
|
1222
1350
|
// Compute bounding box of all content (nodes + boundary + group boundaries + edge points)
|
|
1223
|
-
let minX = Infinity,
|
|
1351
|
+
let minX = Infinity,
|
|
1352
|
+
minY = Infinity,
|
|
1353
|
+
maxX = -Infinity,
|
|
1354
|
+
maxY = -Infinity;
|
|
1224
1355
|
for (const node of nodes) {
|
|
1225
1356
|
const left = node.x - node.width / 2;
|
|
1226
1357
|
const top = node.y - node.height / 2;
|
|
@@ -1290,7 +1421,15 @@ export function layoutC4Containers(
|
|
|
1290
1421
|
if (legendBottom > totalHeight) totalHeight = legendBottom;
|
|
1291
1422
|
}
|
|
1292
1423
|
|
|
1293
|
-
return {
|
|
1424
|
+
return {
|
|
1425
|
+
nodes,
|
|
1426
|
+
edges,
|
|
1427
|
+
legend: legendGroups,
|
|
1428
|
+
boundary,
|
|
1429
|
+
groupBoundaries,
|
|
1430
|
+
width: totalWidth,
|
|
1431
|
+
height: totalHeight,
|
|
1432
|
+
};
|
|
1294
1433
|
}
|
|
1295
1434
|
|
|
1296
1435
|
// ============================================================
|
|
@@ -1312,13 +1451,23 @@ export function layoutC4Components(
|
|
|
1312
1451
|
(el) => el.name.toLowerCase() === systemName.toLowerCase()
|
|
1313
1452
|
);
|
|
1314
1453
|
if (!system) {
|
|
1315
|
-
return {
|
|
1454
|
+
return {
|
|
1455
|
+
nodes: [],
|
|
1456
|
+
edges: [],
|
|
1457
|
+
legend: [],
|
|
1458
|
+
groupBoundaries: [],
|
|
1459
|
+
width: 0,
|
|
1460
|
+
height: 0,
|
|
1461
|
+
};
|
|
1316
1462
|
}
|
|
1317
1463
|
|
|
1318
1464
|
// Find the container within the system (direct children + group children)
|
|
1319
1465
|
let targetContainer: C4Element | undefined;
|
|
1320
1466
|
for (const child of system.children) {
|
|
1321
|
-
if (
|
|
1467
|
+
if (
|
|
1468
|
+
child.type === 'container' &&
|
|
1469
|
+
child.name.toLowerCase() === containerName.toLowerCase()
|
|
1470
|
+
) {
|
|
1322
1471
|
targetContainer = child;
|
|
1323
1472
|
break;
|
|
1324
1473
|
}
|
|
@@ -1326,7 +1475,10 @@ export function layoutC4Components(
|
|
|
1326
1475
|
if (!targetContainer) {
|
|
1327
1476
|
for (const group of system.groups) {
|
|
1328
1477
|
for (const child of group.children) {
|
|
1329
|
-
if (
|
|
1478
|
+
if (
|
|
1479
|
+
child.type === 'container' &&
|
|
1480
|
+
child.name.toLowerCase() === containerName.toLowerCase()
|
|
1481
|
+
) {
|
|
1330
1482
|
targetContainer = child;
|
|
1331
1483
|
break;
|
|
1332
1484
|
}
|
|
@@ -1335,7 +1487,14 @@ export function layoutC4Components(
|
|
|
1335
1487
|
}
|
|
1336
1488
|
}
|
|
1337
1489
|
if (!targetContainer) {
|
|
1338
|
-
return {
|
|
1490
|
+
return {
|
|
1491
|
+
nodes: [],
|
|
1492
|
+
edges: [],
|
|
1493
|
+
legend: [],
|
|
1494
|
+
groupBoundaries: [],
|
|
1495
|
+
width: 0,
|
|
1496
|
+
height: 0,
|
|
1497
|
+
};
|
|
1339
1498
|
}
|
|
1340
1499
|
|
|
1341
1500
|
// Collect all components: direct children + group children
|
|
@@ -1350,7 +1509,14 @@ export function layoutC4Components(
|
|
|
1350
1509
|
}
|
|
1351
1510
|
|
|
1352
1511
|
if (components.length === 0) {
|
|
1353
|
-
return {
|
|
1512
|
+
return {
|
|
1513
|
+
nodes: [],
|
|
1514
|
+
edges: [],
|
|
1515
|
+
legend: [],
|
|
1516
|
+
groupBoundaries: [],
|
|
1517
|
+
width: 0,
|
|
1518
|
+
height: 0,
|
|
1519
|
+
};
|
|
1354
1520
|
}
|
|
1355
1521
|
|
|
1356
1522
|
const componentNames = new Set(components.map((c) => c.name.toLowerCase()));
|
|
@@ -1400,7 +1566,8 @@ export function layoutC4Components(
|
|
|
1400
1566
|
// otherwise roll up to system ancestor
|
|
1401
1567
|
const sourceAncestor = ownerMap.get(sourceName) ?? sourceName;
|
|
1402
1568
|
// If source is inside the same container, skip
|
|
1403
|
-
if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
|
|
1569
|
+
if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
|
|
1570
|
+
continue;
|
|
1404
1571
|
if (sourceAncestor.toLowerCase() === system.name.toLowerCase()) {
|
|
1405
1572
|
// Source is in same system — try to resolve to container level
|
|
1406
1573
|
const sourceLower = sourceName.toLowerCase();
|
|
@@ -1423,11 +1590,17 @@ export function layoutC4Components(
|
|
|
1423
1590
|
}
|
|
1424
1591
|
|
|
1425
1592
|
// Build element-to-group mapping for compound graph
|
|
1426
|
-
const elementToGroup = new Map<
|
|
1593
|
+
const elementToGroup = new Map<
|
|
1594
|
+
string,
|
|
1595
|
+
{ name: string; lineNumber: number }
|
|
1596
|
+
>();
|
|
1427
1597
|
for (const group of targetContainer.groups) {
|
|
1428
1598
|
for (const child of group.children) {
|
|
1429
1599
|
if (child.type === 'component') {
|
|
1430
|
-
elementToGroup.set(child.name, {
|
|
1600
|
+
elementToGroup.set(child.name, {
|
|
1601
|
+
name: group.name,
|
|
1602
|
+
lineNumber: group.lineNumber,
|
|
1603
|
+
});
|
|
1431
1604
|
}
|
|
1432
1605
|
}
|
|
1433
1606
|
}
|
|
@@ -1509,7 +1682,8 @@ export function layoutC4Components(
|
|
|
1509
1682
|
if (!componentNames.has(rel.target.toLowerCase())) continue;
|
|
1510
1683
|
|
|
1511
1684
|
const sourceAncestor = ownerMap.get(sourceName) ?? sourceName;
|
|
1512
|
-
if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
|
|
1685
|
+
if (sourceAncestor.toLowerCase() === targetContainer.name.toLowerCase())
|
|
1686
|
+
continue;
|
|
1513
1687
|
|
|
1514
1688
|
// Resolve source to the external element we added
|
|
1515
1689
|
let resolvedSource: string | undefined;
|
|
@@ -1545,7 +1719,10 @@ export function layoutC4Components(
|
|
|
1545
1719
|
|
|
1546
1720
|
// Add edges to dagre
|
|
1547
1721
|
for (const rel of componentRels) {
|
|
1548
|
-
if (
|
|
1722
|
+
if (
|
|
1723
|
+
nameToElement.has(rel.sourceName) &&
|
|
1724
|
+
nameToElement.has(rel.targetName)
|
|
1725
|
+
) {
|
|
1549
1726
|
g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
|
|
1550
1727
|
}
|
|
1551
1728
|
}
|
|
@@ -1560,7 +1737,10 @@ export function layoutC4Components(
|
|
|
1560
1737
|
reduceCrossings(
|
|
1561
1738
|
g,
|
|
1562
1739
|
componentRels
|
|
1563
|
-
.filter(
|
|
1740
|
+
.filter(
|
|
1741
|
+
(r) =>
|
|
1742
|
+
nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
|
|
1743
|
+
)
|
|
1564
1744
|
.map((r) => ({ source: r.sourceName, target: r.targetName })),
|
|
1565
1745
|
nodeGroupMap
|
|
1566
1746
|
);
|
|
@@ -1572,7 +1752,12 @@ export function layoutC4Components(
|
|
|
1572
1752
|
const nodes: C4LayoutNode[] = [];
|
|
1573
1753
|
for (const el of components) {
|
|
1574
1754
|
const pos = g.node(el.name);
|
|
1575
|
-
const color = resolveNodeColor(
|
|
1755
|
+
const color = resolveNodeColor(
|
|
1756
|
+
el,
|
|
1757
|
+
parsed.tagGroups,
|
|
1758
|
+
activeTagGroup ?? null,
|
|
1759
|
+
ancestors
|
|
1760
|
+
);
|
|
1576
1761
|
const tech = el.metadata['tech'] ?? el.metadata['technology'];
|
|
1577
1762
|
const hasComponents =
|
|
1578
1763
|
el.children.some((c) => c.type === 'component') ||
|
|
@@ -1598,7 +1783,11 @@ export function layoutC4Components(
|
|
|
1598
1783
|
|
|
1599
1784
|
for (const el of externals) {
|
|
1600
1785
|
const pos = g.node(el.name);
|
|
1601
|
-
const color = resolveNodeColor(
|
|
1786
|
+
const color = resolveNodeColor(
|
|
1787
|
+
el,
|
|
1788
|
+
parsed.tagGroups,
|
|
1789
|
+
activeTagGroup ?? null
|
|
1790
|
+
);
|
|
1602
1791
|
nodes.push({
|
|
1603
1792
|
id: el.name,
|
|
1604
1793
|
name: el.name,
|
|
@@ -1616,7 +1805,10 @@ export function layoutC4Components(
|
|
|
1616
1805
|
|
|
1617
1806
|
// Extract edges
|
|
1618
1807
|
const edges: C4LayoutEdge[] = componentRels
|
|
1619
|
-
.filter(
|
|
1808
|
+
.filter(
|
|
1809
|
+
(rel) =>
|
|
1810
|
+
nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
|
|
1811
|
+
)
|
|
1620
1812
|
.map((rel) => {
|
|
1621
1813
|
const edgeData = g.edge(rel.sourceName, rel.targetName);
|
|
1622
1814
|
return {
|
|
@@ -1632,7 +1824,10 @@ export function layoutC4Components(
|
|
|
1632
1824
|
|
|
1633
1825
|
// Compute boundary box from component nodes only
|
|
1634
1826
|
const componentNodes = nodes.filter((n) => n.type === 'component');
|
|
1635
|
-
let bMinX = Infinity,
|
|
1827
|
+
let bMinX = Infinity,
|
|
1828
|
+
bMinY = Infinity,
|
|
1829
|
+
bMaxX = -Infinity,
|
|
1830
|
+
bMaxY = -Infinity;
|
|
1636
1831
|
for (const n of componentNodes) {
|
|
1637
1832
|
const left = n.x - n.width / 2;
|
|
1638
1833
|
const top = n.y - n.height / 2;
|
|
@@ -1650,15 +1845,18 @@ export function layoutC4Components(
|
|
|
1650
1845
|
lineNumber: targetContainer.lineNumber,
|
|
1651
1846
|
x: bMinX - BOUNDARY_PAD,
|
|
1652
1847
|
y: bMinY - BOUNDARY_PAD,
|
|
1653
|
-
width:
|
|
1654
|
-
height:
|
|
1848
|
+
width: bMaxX - bMinX + BOUNDARY_PAD * 2,
|
|
1849
|
+
height: bMaxY - bMinY + BOUNDARY_PAD * 2,
|
|
1655
1850
|
};
|
|
1656
1851
|
|
|
1657
1852
|
// Compute group boundaries from member node positions
|
|
1658
1853
|
const groupBoundaries: C4LayoutBoundary[] = [];
|
|
1659
1854
|
if (hasGroups) {
|
|
1660
1855
|
const nodeMap = new Map(componentNodes.map((n) => [n.name, n]));
|
|
1661
|
-
const seenGroups = new Map<
|
|
1856
|
+
const seenGroups = new Map<
|
|
1857
|
+
string,
|
|
1858
|
+
{ lineNumber: number; members: C4LayoutNode[] }
|
|
1859
|
+
>();
|
|
1662
1860
|
for (const [elName, grp] of elementToGroup) {
|
|
1663
1861
|
const node = nodeMap.get(elName);
|
|
1664
1862
|
if (!node) continue;
|
|
@@ -1669,7 +1867,10 @@ export function layoutC4Components(
|
|
|
1669
1867
|
}
|
|
1670
1868
|
for (const [groupName, { lineNumber, members }] of seenGroups) {
|
|
1671
1869
|
if (members.length === 0) continue;
|
|
1672
|
-
let gMinX = Infinity,
|
|
1870
|
+
let gMinX = Infinity,
|
|
1871
|
+
gMinY = Infinity,
|
|
1872
|
+
gMaxX = -Infinity,
|
|
1873
|
+
gMaxY = -Infinity;
|
|
1673
1874
|
for (const m of members) {
|
|
1674
1875
|
const left = m.x - m.width / 2;
|
|
1675
1876
|
const top = m.y - m.height / 2;
|
|
@@ -1686,14 +1887,17 @@ export function layoutC4Components(
|
|
|
1686
1887
|
lineNumber,
|
|
1687
1888
|
x: gMinX - GROUP_BOUNDARY_PAD,
|
|
1688
1889
|
y: gMinY - GROUP_BOUNDARY_PAD,
|
|
1689
|
-
width:
|
|
1690
|
-
height:
|
|
1890
|
+
width: gMaxX - gMinX + GROUP_BOUNDARY_PAD * 2,
|
|
1891
|
+
height: gMaxY - gMinY + GROUP_BOUNDARY_PAD * 2,
|
|
1691
1892
|
});
|
|
1692
1893
|
}
|
|
1693
1894
|
}
|
|
1694
1895
|
|
|
1695
1896
|
// Compute bounding box of all content (nodes + boundary + group boundaries + edge points)
|
|
1696
|
-
let minX = Infinity,
|
|
1897
|
+
let minX = Infinity,
|
|
1898
|
+
minY = Infinity,
|
|
1899
|
+
maxX = -Infinity,
|
|
1900
|
+
maxY = -Infinity;
|
|
1697
1901
|
for (const node of nodes) {
|
|
1698
1902
|
const left = node.x - node.width / 2;
|
|
1699
1903
|
const top = node.y - node.height / 2;
|
|
@@ -1746,7 +1950,6 @@ export function layoutC4Components(
|
|
|
1746
1950
|
let totalWidth = maxX - minX + MARGIN * 2;
|
|
1747
1951
|
let totalHeight = maxY - minY + MARGIN * 2;
|
|
1748
1952
|
|
|
1749
|
-
|
|
1750
1953
|
const legendGroups = computeLegendGroups(parsed.tagGroups);
|
|
1751
1954
|
|
|
1752
1955
|
// Position legend below diagram
|
|
@@ -1764,7 +1967,15 @@ export function layoutC4Components(
|
|
|
1764
1967
|
if (legendBottom > totalHeight) totalHeight = legendBottom;
|
|
1765
1968
|
}
|
|
1766
1969
|
|
|
1767
|
-
return {
|
|
1970
|
+
return {
|
|
1971
|
+
nodes,
|
|
1972
|
+
edges,
|
|
1973
|
+
legend: legendGroups,
|
|
1974
|
+
boundary,
|
|
1975
|
+
groupBoundaries,
|
|
1976
|
+
width: totalWidth,
|
|
1977
|
+
height: totalHeight,
|
|
1978
|
+
};
|
|
1768
1979
|
}
|
|
1769
1980
|
|
|
1770
1981
|
// ============================================================
|
|
@@ -1775,15 +1986,20 @@ export function layoutC4Components(
|
|
|
1775
1986
|
* Resolve a container reference name to its C4Element by walking the parsed
|
|
1776
1987
|
* element tree. Matches container names case-insensitively.
|
|
1777
1988
|
*/
|
|
1778
|
-
function resolveContainerRef(
|
|
1989
|
+
function resolveContainerRef(
|
|
1990
|
+
parsed: ParsedC4,
|
|
1991
|
+
refName: string
|
|
1992
|
+
): C4Element | undefined {
|
|
1779
1993
|
const lower = refName.toLowerCase();
|
|
1780
1994
|
for (const el of parsed.elements) {
|
|
1781
1995
|
for (const child of el.children) {
|
|
1782
|
-
if (child.type === 'container' && child.name.toLowerCase() === lower)
|
|
1996
|
+
if (child.type === 'container' && child.name.toLowerCase() === lower)
|
|
1997
|
+
return child;
|
|
1783
1998
|
}
|
|
1784
1999
|
for (const group of el.groups) {
|
|
1785
2000
|
for (const child of group.children) {
|
|
1786
|
-
if (child.type === 'container' && child.name.toLowerCase() === lower)
|
|
2001
|
+
if (child.type === 'container' && child.name.toLowerCase() === lower)
|
|
2002
|
+
return child;
|
|
1787
2003
|
}
|
|
1788
2004
|
}
|
|
1789
2005
|
}
|
|
@@ -1808,7 +2024,7 @@ function collectDeploymentRefs(
|
|
|
1808
2024
|
parentId: string | null,
|
|
1809
2025
|
refs: DeploymentRefEntry[],
|
|
1810
2026
|
infraIds: Map<string, C4DeploymentNode>,
|
|
1811
|
-
infraParents: Map<string, string | null
|
|
2027
|
+
infraParents: Map<string, string | null>
|
|
1812
2028
|
): void {
|
|
1813
2029
|
for (const node of nodes) {
|
|
1814
2030
|
const infraId = `__infra_${node.name}`;
|
|
@@ -1818,11 +2034,23 @@ function collectDeploymentRefs(
|
|
|
1818
2034
|
for (const ref of node.containerRefs) {
|
|
1819
2035
|
const el = resolveContainerRef(parsed, ref);
|
|
1820
2036
|
if (el) {
|
|
1821
|
-
refs.push({
|
|
2037
|
+
refs.push({
|
|
2038
|
+
refName: ref,
|
|
2039
|
+
element: el,
|
|
2040
|
+
infraId,
|
|
2041
|
+
deployLineNumber: node.lineNumber,
|
|
2042
|
+
});
|
|
1822
2043
|
}
|
|
1823
2044
|
}
|
|
1824
2045
|
|
|
1825
|
-
collectDeploymentRefs(
|
|
2046
|
+
collectDeploymentRefs(
|
|
2047
|
+
node.children,
|
|
2048
|
+
parsed,
|
|
2049
|
+
infraId,
|
|
2050
|
+
refs,
|
|
2051
|
+
infraIds,
|
|
2052
|
+
infraParents
|
|
2053
|
+
);
|
|
1826
2054
|
}
|
|
1827
2055
|
}
|
|
1828
2056
|
|
|
@@ -1835,20 +2063,41 @@ function collectDeploymentRefs(
|
|
|
1835
2063
|
*/
|
|
1836
2064
|
export function layoutC4Deployment(
|
|
1837
2065
|
parsed: ParsedC4,
|
|
1838
|
-
activeTagGroup?: string | null
|
|
2066
|
+
activeTagGroup?: string | null
|
|
1839
2067
|
): C4LayoutResult {
|
|
1840
2068
|
if (parsed.deployment.length === 0) {
|
|
1841
|
-
return {
|
|
2069
|
+
return {
|
|
2070
|
+
nodes: [],
|
|
2071
|
+
edges: [],
|
|
2072
|
+
legend: [],
|
|
2073
|
+
groupBoundaries: [],
|
|
2074
|
+
width: 0,
|
|
2075
|
+
height: 0,
|
|
2076
|
+
};
|
|
1842
2077
|
}
|
|
1843
2078
|
|
|
1844
2079
|
// Collect all refs and infra node info
|
|
1845
2080
|
const refs: DeploymentRefEntry[] = [];
|
|
1846
2081
|
const infraIds = new Map<string, C4DeploymentNode>();
|
|
1847
2082
|
const infraParents = new Map<string, string | null>();
|
|
1848
|
-
collectDeploymentRefs(
|
|
2083
|
+
collectDeploymentRefs(
|
|
2084
|
+
parsed.deployment,
|
|
2085
|
+
parsed,
|
|
2086
|
+
null,
|
|
2087
|
+
refs,
|
|
2088
|
+
infraIds,
|
|
2089
|
+
infraParents
|
|
2090
|
+
);
|
|
1849
2091
|
|
|
1850
2092
|
if (refs.length === 0) {
|
|
1851
|
-
return {
|
|
2093
|
+
return {
|
|
2094
|
+
nodes: [],
|
|
2095
|
+
edges: [],
|
|
2096
|
+
legend: [],
|
|
2097
|
+
groupBoundaries: [],
|
|
2098
|
+
width: 0,
|
|
2099
|
+
height: 0,
|
|
2100
|
+
};
|
|
1852
2101
|
}
|
|
1853
2102
|
|
|
1854
2103
|
// Deduplicate refs by element name (a container can appear in multiple infra
|
|
@@ -1927,7 +2176,10 @@ export function layoutC4Deployment(
|
|
|
1927
2176
|
|
|
1928
2177
|
// Add edges to dagre
|
|
1929
2178
|
for (const rel of deployRels) {
|
|
1930
|
-
if (
|
|
2179
|
+
if (
|
|
2180
|
+
nameToElement.has(rel.sourceName) &&
|
|
2181
|
+
nameToElement.has(rel.targetName)
|
|
2182
|
+
) {
|
|
1931
2183
|
g.setEdge(rel.sourceName, rel.targetName, { label: rel.label ?? '' });
|
|
1932
2184
|
}
|
|
1933
2185
|
}
|
|
@@ -1941,16 +2193,23 @@ export function layoutC4Deployment(
|
|
|
1941
2193
|
reduceCrossings(
|
|
1942
2194
|
g,
|
|
1943
2195
|
deployRels
|
|
1944
|
-
.filter(
|
|
2196
|
+
.filter(
|
|
2197
|
+
(r) =>
|
|
2198
|
+
nameToElement.has(r.sourceName) && nameToElement.has(r.targetName)
|
|
2199
|
+
)
|
|
1945
2200
|
.map((r) => ({ source: r.sourceName, target: r.targetName })),
|
|
1946
|
-
nodeInfraMap
|
|
2201
|
+
nodeInfraMap
|
|
1947
2202
|
);
|
|
1948
2203
|
|
|
1949
2204
|
// Extract positioned nodes
|
|
1950
2205
|
const nodes: C4LayoutNode[] = [];
|
|
1951
2206
|
for (const r of refEntries) {
|
|
1952
2207
|
const pos = g.node(r.element.name);
|
|
1953
|
-
const color = resolveNodeColor(
|
|
2208
|
+
const color = resolveNodeColor(
|
|
2209
|
+
r.element,
|
|
2210
|
+
parsed.tagGroups,
|
|
2211
|
+
activeTagGroup ?? null
|
|
2212
|
+
);
|
|
1954
2213
|
const tech = r.element.metadata['tech'] ?? r.element.metadata['technology'];
|
|
1955
2214
|
nodes.push({
|
|
1956
2215
|
id: r.element.name,
|
|
@@ -1971,7 +2230,10 @@ export function layoutC4Deployment(
|
|
|
1971
2230
|
|
|
1972
2231
|
// Extract edges
|
|
1973
2232
|
const edges: C4LayoutEdge[] = deployRels
|
|
1974
|
-
.filter(
|
|
2233
|
+
.filter(
|
|
2234
|
+
(rel) =>
|
|
2235
|
+
nameToElement.has(rel.sourceName) && nameToElement.has(rel.targetName)
|
|
2236
|
+
)
|
|
1975
2237
|
.map((rel) => {
|
|
1976
2238
|
const edgeData = g.edge(rel.sourceName, rel.targetName);
|
|
1977
2239
|
return {
|
|
@@ -1999,12 +2261,20 @@ export function layoutC4Deployment(
|
|
|
1999
2261
|
}
|
|
2000
2262
|
|
|
2001
2263
|
// Compute boundaries bottom-up: leaf infra nodes first, then parents.
|
|
2002
|
-
const infraBounds = new Map<
|
|
2003
|
-
|
|
2004
|
-
|
|
2264
|
+
const infraBounds = new Map<
|
|
2265
|
+
string,
|
|
2266
|
+
{ x: number; y: number; width: number; height: number }
|
|
2267
|
+
>();
|
|
2268
|
+
|
|
2269
|
+
function computeInfraBounds(
|
|
2270
|
+
infraId: string
|
|
2271
|
+
): { x: number; y: number; width: number; height: number } | null {
|
|
2005
2272
|
if (infraBounds.has(infraId)) return infraBounds.get(infraId)!;
|
|
2006
2273
|
|
|
2007
|
-
let bMinX = Infinity,
|
|
2274
|
+
let bMinX = Infinity,
|
|
2275
|
+
bMinY = Infinity,
|
|
2276
|
+
bMaxX = -Infinity,
|
|
2277
|
+
bMaxY = -Infinity;
|
|
2008
2278
|
let hasContent = false;
|
|
2009
2279
|
|
|
2010
2280
|
// Direct container ref members
|
|
@@ -2029,8 +2299,10 @@ export function layoutC4Deployment(
|
|
|
2029
2299
|
hasContent = true;
|
|
2030
2300
|
if (childBounds.x < bMinX) bMinX = childBounds.x;
|
|
2031
2301
|
if (childBounds.y < bMinY) bMinY = childBounds.y;
|
|
2032
|
-
if (childBounds.x + childBounds.width > bMaxX)
|
|
2033
|
-
|
|
2302
|
+
if (childBounds.x + childBounds.width > bMaxX)
|
|
2303
|
+
bMaxX = childBounds.x + childBounds.width;
|
|
2304
|
+
if (childBounds.y + childBounds.height > bMaxY)
|
|
2305
|
+
bMaxY = childBounds.y + childBounds.height;
|
|
2034
2306
|
}
|
|
2035
2307
|
}
|
|
2036
2308
|
}
|
|
@@ -2040,8 +2312,8 @@ export function layoutC4Deployment(
|
|
|
2040
2312
|
const bounds = {
|
|
2041
2313
|
x: bMinX - BOUNDARY_PAD,
|
|
2042
2314
|
y: bMinY - BOUNDARY_PAD,
|
|
2043
|
-
width:
|
|
2044
|
-
height:
|
|
2315
|
+
width: bMaxX - bMinX + BOUNDARY_PAD * 2,
|
|
2316
|
+
height: bMaxY - bMinY + BOUNDARY_PAD * 2,
|
|
2045
2317
|
};
|
|
2046
2318
|
infraBounds.set(infraId, bounds);
|
|
2047
2319
|
return bounds;
|
|
@@ -2062,10 +2334,13 @@ export function layoutC4Deployment(
|
|
|
2062
2334
|
}
|
|
2063
2335
|
|
|
2064
2336
|
// Sort boundaries so outermost (largest area) are first — rendered bottom to top
|
|
2065
|
-
groupBoundaries.sort((a, b) =>
|
|
2337
|
+
groupBoundaries.sort((a, b) => b.width * b.height - a.width * a.height);
|
|
2066
2338
|
|
|
2067
2339
|
// Compute total bounding box
|
|
2068
|
-
let minX = Infinity,
|
|
2340
|
+
let minX = Infinity,
|
|
2341
|
+
minY = Infinity,
|
|
2342
|
+
maxX = -Infinity,
|
|
2343
|
+
maxY = -Infinity;
|
|
2069
2344
|
for (const node of nodes) {
|
|
2070
2345
|
const left = node.x - node.width / 2;
|
|
2071
2346
|
const top = node.y - node.height / 2;
|
|
@@ -2128,5 +2403,12 @@ export function layoutC4Deployment(
|
|
|
2128
2403
|
if (legendBottom > totalHeight) totalHeight = legendBottom;
|
|
2129
2404
|
}
|
|
2130
2405
|
|
|
2131
|
-
return {
|
|
2406
|
+
return {
|
|
2407
|
+
nodes,
|
|
2408
|
+
edges,
|
|
2409
|
+
legend: legendGroups,
|
|
2410
|
+
groupBoundaries,
|
|
2411
|
+
width: totalWidth,
|
|
2412
|
+
height: totalHeight,
|
|
2413
|
+
};
|
|
2132
2414
|
}
|