@diagrammo/dgmo 0.8.5 → 0.8.7
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.md +34 -27
- package/.cursorrules +20 -2
- package/.github/copilot-instructions.md +20 -2
- package/.windsurfrules +20 -2
- package/AGENTS.md +23 -3
- package/README.md +0 -1
- package/dist/cli.cjs +189 -190
- package/dist/editor.cjs +3 -18
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +3 -18
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +4 -21
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +4 -21
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +2791 -2999
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -56
- package/dist/index.d.ts +56 -56
- package/dist/index.js +2786 -2992
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +112 -121
- package/gallery/fixtures/boxes-and-lines.dgmo +64 -0
- package/package.json +1 -1
- package/src/boxes-and-lines/collapse.ts +78 -0
- package/src/boxes-and-lines/layout.ts +319 -0
- package/src/boxes-and-lines/parser.ts +697 -0
- package/src/boxes-and-lines/renderer.ts +848 -0
- package/src/boxes-and-lines/types.ts +40 -0
- package/src/c4/parser.ts +10 -5
- package/src/c4/renderer.ts +232 -56
- package/src/chart.ts +9 -4
- package/src/cli.ts +6 -5
- package/src/completion.ts +25 -33
- package/src/d3.ts +26 -27
- package/src/dgmo-router.ts +3 -7
- package/src/echarts.ts +38 -2
- package/src/editor/keywords.ts +4 -19
- package/src/er/parser.ts +10 -4
- package/src/gantt/parser.ts +10 -4
- package/src/gantt/renderer.ts +3 -5
- package/src/index.ts +17 -26
- package/src/infra/parser.ts +10 -5
- package/src/infra/renderer.ts +2 -2
- package/src/kanban/parser.ts +10 -5
- package/src/kanban/renderer.ts +43 -18
- package/src/org/parser.ts +7 -4
- package/src/org/renderer.ts +40 -29
- package/src/sequence/parser.ts +11 -5
- package/src/sequence/renderer.ts +114 -45
- package/src/sitemap/parser.ts +8 -4
- package/src/sitemap/renderer.ts +137 -57
- package/src/utils/legend-svg.ts +44 -20
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +59 -15
- package/gallery/fixtures/initiative-status-full.dgmo +0 -46
- package/gallery/fixtures/initiative-status-phases.dgmo +0 -29
- package/gallery/fixtures/initiative-status.dgmo +0 -9
- package/src/initiative-status/collapse.ts +0 -76
- package/src/initiative-status/filter.ts +0 -63
- package/src/initiative-status/layout.ts +0 -650
- package/src/initiative-status/parser.ts +0 -629
- package/src/initiative-status/renderer.ts +0 -1199
- package/src/initiative-status/types.ts +0 -57
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Boxes and Lines Diagram — Layout Engine
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import dagre from '@dagrejs/dagre';
|
|
6
|
+
import type { ParsedBoxesAndLines, BLNode } from './types';
|
|
7
|
+
|
|
8
|
+
// ── Constants ──────────────────────────────────────────────
|
|
9
|
+
const NODESEP = 60;
|
|
10
|
+
const RANKSEP = 100;
|
|
11
|
+
const MARGIN = 40;
|
|
12
|
+
const CONTAINER_PAD_X = 30;
|
|
13
|
+
const CONTAINER_PAD_TOP = 40;
|
|
14
|
+
const CONTAINER_PAD_BOTTOM = 24;
|
|
15
|
+
const MAX_PARALLEL_EDGES = 5;
|
|
16
|
+
const PARALLEL_SPACING = 12;
|
|
17
|
+
const PARALLEL_EDGE_MARGIN = 10;
|
|
18
|
+
|
|
19
|
+
// ── Result types ───────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface BLLayoutNode {
|
|
22
|
+
label: string;
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BLLayoutEdge {
|
|
30
|
+
source: string;
|
|
31
|
+
target: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
bidirectional: boolean;
|
|
34
|
+
lineNumber: number;
|
|
35
|
+
points: { x: number; y: number }[];
|
|
36
|
+
labelX?: number;
|
|
37
|
+
labelY?: number;
|
|
38
|
+
yOffset: number;
|
|
39
|
+
parallelCount: number;
|
|
40
|
+
metadata: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BLLayoutGroup {
|
|
44
|
+
label: string;
|
|
45
|
+
lineNumber: number;
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
collapsed: boolean;
|
|
51
|
+
childCount?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface BLLayoutResult {
|
|
55
|
+
nodes: BLLayoutNode[];
|
|
56
|
+
edges: BLLayoutEdge[];
|
|
57
|
+
groups: BLLayoutGroup[];
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Node sizing ────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function computeNodeSize(_node: BLNode): { width: number; height: number } {
|
|
65
|
+
// Golden ratio (φ ≈ 1.618), uniform size
|
|
66
|
+
const PHI = 1.618;
|
|
67
|
+
const NODE_HEIGHT = 60;
|
|
68
|
+
const NODE_WIDTH = Math.round(NODE_HEIGHT * PHI); // ≈ 97
|
|
69
|
+
|
|
70
|
+
return { width: NODE_WIDTH, height: NODE_HEIGHT };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Main layout ────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export function layoutBoxesAndLines(
|
|
76
|
+
parsed: ParsedBoxesAndLines,
|
|
77
|
+
collapseInfo?: {
|
|
78
|
+
collapsedChildCounts: Map<string, number>;
|
|
79
|
+
originalGroups: import('./types').BLGroup[];
|
|
80
|
+
}
|
|
81
|
+
): BLLayoutResult {
|
|
82
|
+
const g = new dagre.graphlib.Graph({ compound: true, multigraph: true });
|
|
83
|
+
g.setGraph({
|
|
84
|
+
rankdir: parsed.direction,
|
|
85
|
+
nodesep: NODESEP,
|
|
86
|
+
ranksep: RANKSEP,
|
|
87
|
+
marginx: MARGIN,
|
|
88
|
+
marginy: MARGIN,
|
|
89
|
+
});
|
|
90
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
91
|
+
|
|
92
|
+
// Determine which groups are collapsed
|
|
93
|
+
const collapsedGroupLabels = new Set<string>();
|
|
94
|
+
if (collapseInfo) {
|
|
95
|
+
for (const og of collapseInfo.originalGroups) {
|
|
96
|
+
if (!parsed.groups.some((g) => g.label === og.label)) {
|
|
97
|
+
collapsedGroupLabels.add(og.label);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add collapsed groups as regular nodes — same golden-ratio dimensions
|
|
103
|
+
const PHI = 1.618;
|
|
104
|
+
const COLLAPSED_H = 60;
|
|
105
|
+
const COLLAPSED_W = Math.round(COLLAPSED_H * PHI);
|
|
106
|
+
for (const label of collapsedGroupLabels) {
|
|
107
|
+
const gid = `__group_${label}`;
|
|
108
|
+
g.setNode(gid, { label, width: COLLAPSED_W, height: COLLAPSED_H });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add expanded group nodes as compound parents
|
|
112
|
+
for (const group of parsed.groups) {
|
|
113
|
+
const gid = `__group_${group.label}`;
|
|
114
|
+
g.setNode(gid, {
|
|
115
|
+
label: group.label,
|
|
116
|
+
paddingLeft: CONTAINER_PAD_X,
|
|
117
|
+
paddingRight: CONTAINER_PAD_X,
|
|
118
|
+
paddingTop: CONTAINER_PAD_TOP,
|
|
119
|
+
paddingBottom: CONTAINER_PAD_BOTTOM,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add nodes
|
|
124
|
+
for (const node of parsed.nodes) {
|
|
125
|
+
const size = computeNodeSize(node);
|
|
126
|
+
g.setNode(node.label, {
|
|
127
|
+
label: node.label,
|
|
128
|
+
width: size.width,
|
|
129
|
+
height: size.height,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Set parent relationships for nodes in groups
|
|
134
|
+
for (const group of parsed.groups) {
|
|
135
|
+
const gid = `__group_${group.label}`;
|
|
136
|
+
for (const child of group.children) {
|
|
137
|
+
if (g.hasNode(child)) {
|
|
138
|
+
g.setParent(child, gid);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build set of expanded compound parent IDs (dagre can't handle edges
|
|
144
|
+
// directly on compound parents — they have no rank of their own)
|
|
145
|
+
const expandedGroupIds = new Set<string>();
|
|
146
|
+
for (const group of parsed.groups) {
|
|
147
|
+
expandedGroupIds.add(`__group_${group.label}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add edges — skip edges where either endpoint is an expanded compound parent
|
|
151
|
+
const deferredEdgeIndices: number[] = [];
|
|
152
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
153
|
+
const edge = parsed.edges[i];
|
|
154
|
+
const src = edge.source;
|
|
155
|
+
const tgt = edge.target;
|
|
156
|
+
if (!g.hasNode(src) || !g.hasNode(tgt)) continue;
|
|
157
|
+
if (expandedGroupIds.has(src) || expandedGroupIds.has(tgt)) {
|
|
158
|
+
deferredEdgeIndices.push(i);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
g.setEdge(src, tgt, { label: edge.label ?? '', minlen: 1 }, `e${i}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Run dagre layout
|
|
165
|
+
dagre.layout(g);
|
|
166
|
+
|
|
167
|
+
// Extract node positions
|
|
168
|
+
const layoutNodes: BLLayoutNode[] = [];
|
|
169
|
+
for (const node of parsed.nodes) {
|
|
170
|
+
const dagreNode = g.node(node.label);
|
|
171
|
+
if (!dagreNode) continue;
|
|
172
|
+
layoutNodes.push({
|
|
173
|
+
label: node.label,
|
|
174
|
+
x: dagreNode.x,
|
|
175
|
+
y: dagreNode.y,
|
|
176
|
+
width: dagreNode.width,
|
|
177
|
+
height: dagreNode.height,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Extract group positions (expanded)
|
|
182
|
+
const layoutGroups: BLLayoutGroup[] = [];
|
|
183
|
+
for (const group of parsed.groups) {
|
|
184
|
+
const gid = `__group_${group.label}`;
|
|
185
|
+
const dagreNode = g.node(gid);
|
|
186
|
+
if (!dagreNode) continue;
|
|
187
|
+
layoutGroups.push({
|
|
188
|
+
label: group.label,
|
|
189
|
+
lineNumber: group.lineNumber,
|
|
190
|
+
x: dagreNode.x,
|
|
191
|
+
y: dagreNode.y,
|
|
192
|
+
width: dagreNode.width,
|
|
193
|
+
height: dagreNode.height,
|
|
194
|
+
collapsed: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Extract collapsed group positions
|
|
199
|
+
for (const label of collapsedGroupLabels) {
|
|
200
|
+
const gid = `__group_${label}`;
|
|
201
|
+
const dagreNode = g.node(gid);
|
|
202
|
+
if (!dagreNode) continue;
|
|
203
|
+
const og = collapseInfo?.originalGroups.find((g) => g.label === label);
|
|
204
|
+
layoutGroups.push({
|
|
205
|
+
label,
|
|
206
|
+
lineNumber: og?.lineNumber ?? 0,
|
|
207
|
+
x: dagreNode.x,
|
|
208
|
+
y: dagreNode.y,
|
|
209
|
+
width: dagreNode.width,
|
|
210
|
+
height: dagreNode.height,
|
|
211
|
+
collapsed: true,
|
|
212
|
+
childCount: collapseInfo?.collapsedChildCounts.get(label) ?? 0,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Compute parallel edge offsets
|
|
217
|
+
const edgeYOffsets: number[] = new Array(parsed.edges.length).fill(0);
|
|
218
|
+
const edgeParallelCounts: number[] = new Array(parsed.edges.length).fill(1);
|
|
219
|
+
const parallelGroups = new Map<string, number[]>();
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
222
|
+
const edge = parsed.edges[i];
|
|
223
|
+
// Normalize key so A→B and B→A are in the same parallel group
|
|
224
|
+
const [a, b] =
|
|
225
|
+
edge.source < edge.target
|
|
226
|
+
? [edge.source, edge.target]
|
|
227
|
+
: [edge.target, edge.source];
|
|
228
|
+
const key = `${a}\x00${b}`;
|
|
229
|
+
if (!parallelGroups.has(key)) parallelGroups.set(key, []);
|
|
230
|
+
parallelGroups.get(key)!.push(i);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const group of parallelGroups.values()) {
|
|
234
|
+
const capped = group.slice(0, MAX_PARALLEL_EDGES);
|
|
235
|
+
for (const idx of group.slice(MAX_PARALLEL_EDGES)) {
|
|
236
|
+
edgeParallelCounts[idx] = 0;
|
|
237
|
+
}
|
|
238
|
+
if (capped.length < 2) continue;
|
|
239
|
+
const effectiveSpacing = Math.min(
|
|
240
|
+
PARALLEL_SPACING,
|
|
241
|
+
(60 - PARALLEL_EDGE_MARGIN) / (capped.length - 1)
|
|
242
|
+
);
|
|
243
|
+
for (let j = 0; j < capped.length; j++) {
|
|
244
|
+
edgeYOffsets[capped[j]] =
|
|
245
|
+
(j - (capped.length - 1) / 2) * effectiveSpacing;
|
|
246
|
+
edgeParallelCounts[capped[j]] = capped.length;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Extract edge points
|
|
251
|
+
const deferredSet = new Set(deferredEdgeIndices);
|
|
252
|
+
const layoutEdges: BLLayoutEdge[] = [];
|
|
253
|
+
for (let i = 0; i < parsed.edges.length; i++) {
|
|
254
|
+
const edge = parsed.edges[i];
|
|
255
|
+
if (edgeParallelCounts[i] === 0) continue;
|
|
256
|
+
|
|
257
|
+
let points: { x: number; y: number }[];
|
|
258
|
+
|
|
259
|
+
if (deferredSet.has(i)) {
|
|
260
|
+
// Deferred edge (compound parent endpoint) — compute points from node positions
|
|
261
|
+
const srcNode = g.node(edge.source);
|
|
262
|
+
const tgtNode = g.node(edge.target);
|
|
263
|
+
if (!srcNode || !tgtNode) continue;
|
|
264
|
+
const midX = (srcNode.x + tgtNode.x) / 2;
|
|
265
|
+
const midY = (srcNode.y + tgtNode.y) / 2;
|
|
266
|
+
points = [
|
|
267
|
+
{ x: srcNode.x, y: srcNode.y },
|
|
268
|
+
{ x: midX, y: midY },
|
|
269
|
+
{ x: tgtNode.x, y: tgtNode.y },
|
|
270
|
+
];
|
|
271
|
+
} else {
|
|
272
|
+
const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
|
|
273
|
+
points = dagreEdge?.points ?? [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Compute label position at midpoint
|
|
277
|
+
let labelX: number | undefined;
|
|
278
|
+
let labelY: number | undefined;
|
|
279
|
+
if (edge.label && points.length >= 2) {
|
|
280
|
+
const mid = Math.floor(points.length / 2);
|
|
281
|
+
labelX = points[mid].x;
|
|
282
|
+
labelY = points[mid].y - 10;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
layoutEdges.push({
|
|
286
|
+
source: edge.source,
|
|
287
|
+
target: edge.target,
|
|
288
|
+
label: edge.label,
|
|
289
|
+
bidirectional: edge.bidirectional,
|
|
290
|
+
lineNumber: edge.lineNumber,
|
|
291
|
+
points,
|
|
292
|
+
labelX,
|
|
293
|
+
labelY,
|
|
294
|
+
yOffset: edgeYOffsets[i],
|
|
295
|
+
parallelCount: edgeParallelCounts[i],
|
|
296
|
+
metadata: edge.metadata,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Compute total dimensions
|
|
301
|
+
let maxX = 0;
|
|
302
|
+
let maxY = 0;
|
|
303
|
+
for (const node of layoutNodes) {
|
|
304
|
+
maxX = Math.max(maxX, node.x + node.width / 2);
|
|
305
|
+
maxY = Math.max(maxY, node.y + node.height / 2);
|
|
306
|
+
}
|
|
307
|
+
for (const group of layoutGroups) {
|
|
308
|
+
maxX = Math.max(maxX, group.x + group.width / 2);
|
|
309
|
+
maxY = Math.max(maxY, group.y + group.height / 2);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
nodes: layoutNodes,
|
|
314
|
+
edges: layoutEdges,
|
|
315
|
+
groups: layoutGroups,
|
|
316
|
+
width: maxX + MARGIN,
|
|
317
|
+
height: maxY + MARGIN,
|
|
318
|
+
};
|
|
319
|
+
}
|