@fazelstudio/floe 1.0.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/LICENSE +21 -0
- package/dist/src/cli/check.d.ts +14 -0
- package/dist/src/cli/check.d.ts.map +1 -0
- package/dist/src/cli/check.js +88 -0
- package/dist/src/cli/check.js.map +1 -0
- package/dist/src/cli/format.d.ts +9 -0
- package/dist/src/cli/format.d.ts.map +1 -0
- package/dist/src/cli/format.js +114 -0
- package/dist/src/cli/format.js.map +1 -0
- package/dist/src/cli/main.d.ts +19 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +207 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/render.d.ts +7 -0
- package/dist/src/cli/render.d.ts.map +1 -0
- package/dist/src/cli/render.js +90 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/diagnostics.d.ts +32 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +4 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/index.d.ts +40 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +64 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/language/completion.d.ts +6 -0
- package/dist/src/language/completion.d.ts.map +1 -0
- package/dist/src/language/completion.js +363 -0
- package/dist/src/language/completion.js.map +1 -0
- package/dist/src/language/definitions.d.ts +9 -0
- package/dist/src/language/definitions.d.ts.map +1 -0
- package/dist/src/language/definitions.js +70 -0
- package/dist/src/language/definitions.js.map +1 -0
- package/dist/src/language/diagnosticsService.d.ts +14 -0
- package/dist/src/language/diagnosticsService.d.ts.map +1 -0
- package/dist/src/language/diagnosticsService.js +37 -0
- package/dist/src/language/diagnosticsService.js.map +1 -0
- package/dist/src/language/folding.d.ts +8 -0
- package/dist/src/language/folding.d.ts.map +1 -0
- package/dist/src/language/folding.js +58 -0
- package/dist/src/language/folding.js.map +1 -0
- package/dist/src/language/formatting.d.ts +5 -0
- package/dist/src/language/formatting.d.ts.map +1 -0
- package/dist/src/language/formatting.js +274 -0
- package/dist/src/language/formatting.js.map +1 -0
- package/dist/src/language/highlighting.d.ts +9 -0
- package/dist/src/language/highlighting.d.ts.map +1 -0
- package/dist/src/language/highlighting.js +74 -0
- package/dist/src/language/highlighting.js.map +1 -0
- package/dist/src/language/hover.d.ts +6 -0
- package/dist/src/language/hover.d.ts.map +1 -0
- package/dist/src/language/hover.js +197 -0
- package/dist/src/language/hover.js.map +1 -0
- package/dist/src/language/indentation.d.ts +15 -0
- package/dist/src/language/indentation.d.ts.map +1 -0
- package/dist/src/language/indentation.js +97 -0
- package/dist/src/language/indentation.js.map +1 -0
- package/dist/src/language/index.d.ts +54 -0
- package/dist/src/language/index.d.ts.map +1 -0
- package/dist/src/language/index.js +85 -0
- package/dist/src/language/index.js.map +1 -0
- package/dist/src/language/references.d.ts +12 -0
- package/dist/src/language/references.d.ts.map +1 -0
- package/dist/src/language/references.js +35 -0
- package/dist/src/language/references.js.map +1 -0
- package/dist/src/language/rename.d.ts +18 -0
- package/dist/src/language/rename.d.ts.map +1 -0
- package/dist/src/language/rename.js +53 -0
- package/dist/src/language/rename.js.map +1 -0
- package/dist/src/language/semanticModel.d.ts +21 -0
- package/dist/src/language/semanticModel.d.ts.map +1 -0
- package/dist/src/language/semanticModel.js +43 -0
- package/dist/src/language/semanticModel.js.map +1 -0
- package/dist/src/language/symbols.d.ts +9 -0
- package/dist/src/language/symbols.d.ts.map +1 -0
- package/dist/src/language/symbols.js +160 -0
- package/dist/src/language/symbols.js.map +1 -0
- package/dist/src/language/types.d.ts +49 -0
- package/dist/src/language/types.d.ts.map +1 -0
- package/dist/src/language/types.js +2 -0
- package/dist/src/language/types.js.map +1 -0
- package/dist/src/language/utils.d.ts +24 -0
- package/dist/src/language/utils.d.ts.map +1 -0
- package/dist/src/language/utils.js +154 -0
- package/dist/src/language/utils.js.map +1 -0
- package/dist/src/layout/dagre.d.ts +13 -0
- package/dist/src/layout/dagre.d.ts.map +1 -0
- package/dist/src/layout/dagre.js +229 -0
- package/dist/src/layout/dagre.js.map +1 -0
- package/dist/src/layout/index.d.ts +15 -0
- package/dist/src/layout/index.d.ts.map +1 -0
- package/dist/src/layout/index.js +22 -0
- package/dist/src/layout/index.js.map +1 -0
- package/dist/src/layout/simple.d.ts +20 -0
- package/dist/src/layout/simple.d.ts.map +1 -0
- package/dist/src/layout/simple.js +674 -0
- package/dist/src/layout/simple.js.map +1 -0
- package/dist/src/layout/types.d.ts +65 -0
- package/dist/src/layout/types.d.ts.map +1 -0
- package/dist/src/layout/types.js +2 -0
- package/dist/src/layout/types.js.map +1 -0
- package/dist/src/lexer.d.ts +25 -0
- package/dist/src/lexer.d.ts.map +1 -0
- package/dist/src/lexer.js +280 -0
- package/dist/src/lexer.js.map +1 -0
- package/dist/src/lsp/documents.d.ts +24 -0
- package/dist/src/lsp/documents.d.ts.map +1 -0
- package/dist/src/lsp/documents.js +97 -0
- package/dist/src/lsp/documents.js.map +1 -0
- package/dist/src/lsp/server.d.ts +70 -0
- package/dist/src/lsp/server.d.ts.map +1 -0
- package/dist/src/lsp/server.js +623 -0
- package/dist/src/lsp/server.js.map +1 -0
- package/dist/src/lsp/utils.d.ts +47 -0
- package/dist/src/lsp/utils.d.ts.map +1 -0
- package/dist/src/lsp/utils.js +142 -0
- package/dist/src/lsp/utils.js.map +1 -0
- package/dist/src/parser.d.ts +106 -0
- package/dist/src/parser.d.ts.map +1 -0
- package/dist/src/parser.js +1018 -0
- package/dist/src/parser.js.map +1 -0
- package/dist/src/pipeline.d.ts +51 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +65 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/range.d.ts +24 -0
- package/dist/src/range.d.ts.map +1 -0
- package/dist/src/range.js +20 -0
- package/dist/src/range.js.map +1 -0
- package/dist/src/render/index.d.ts +3 -0
- package/dist/src/render/index.d.ts.map +1 -0
- package/dist/src/render/index.js +3 -0
- package/dist/src/render/index.js.map +1 -0
- package/dist/src/render/shapes.d.ts +20 -0
- package/dist/src/render/shapes.d.ts.map +1 -0
- package/dist/src/render/shapes.js +110 -0
- package/dist/src/render/shapes.js.map +1 -0
- package/dist/src/render/svg.d.ts +19 -0
- package/dist/src/render/svg.d.ts.map +1 -0
- package/dist/src/render/svg.js +227 -0
- package/dist/src/render/svg.js.map +1 -0
- package/dist/src/security/url.d.ts +7 -0
- package/dist/src/security/url.d.ts.map +1 -0
- package/dist/src/security/url.js +27 -0
- package/dist/src/security/url.js.map +1 -0
- package/dist/src/types.d.ts +111 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +16 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validator.d.ts +47 -0
- package/dist/src/validator.d.ts.map +1 -0
- package/dist/src/validator.js +235 -0
- package/dist/src/validator.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
const DEFAULT_OPTS = {
|
|
2
|
+
nodeWidth: 120,
|
|
3
|
+
nodeHeight: 48,
|
|
4
|
+
rankSep: 80,
|
|
5
|
+
nodeSep: 32,
|
|
6
|
+
margin: 32,
|
|
7
|
+
minNodeWidth: 80,
|
|
8
|
+
maxNodeWidth: 200,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Deterministic, zero-dependency layered layout.
|
|
12
|
+
* - No random, no timestamps, no nondeterministic ordering
|
|
13
|
+
* - Supports TB, BT, LR, RL via coordinate transform
|
|
14
|
+
* - Handles cycles via bounded relaxation
|
|
15
|
+
*/
|
|
16
|
+
export class SimpleLayoutEngine {
|
|
17
|
+
opts;
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
this.opts = { ...DEFAULT_OPTS, ...opts };
|
|
20
|
+
}
|
|
21
|
+
layout(diagram) {
|
|
22
|
+
// Empty diagram: minimal SVG size, deterministic (include groups empty)
|
|
23
|
+
if (diagram.nodes.length === 0 && (diagram.groups?.length ?? 0) === 0) {
|
|
24
|
+
return {
|
|
25
|
+
diagram,
|
|
26
|
+
direction: diagram.direction,
|
|
27
|
+
nodes: [],
|
|
28
|
+
edges: [],
|
|
29
|
+
groups: [],
|
|
30
|
+
width: this.opts.margin * 2 + 100,
|
|
31
|
+
height: this.opts.margin * 2 + 60,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Include isolated groups handling: even if nodes empty but groups with members? But members are nodes, so nodes empty => groups empty too fallback.
|
|
35
|
+
// Deterministic node ordering: sort by id for rank grouping stability,
|
|
36
|
+
// but preserve original rank discovery order? We sort copy for processing.
|
|
37
|
+
const nodesById = new Map();
|
|
38
|
+
for (const n of diagram.nodes)
|
|
39
|
+
nodesById.set(n.id, n);
|
|
40
|
+
// Deterministic edge ordering: sort by source, target, label
|
|
41
|
+
const edgesSorted = [...diagram.edges].sort((a, b) => {
|
|
42
|
+
if (a.source !== b.source)
|
|
43
|
+
return a.source.localeCompare(b.source);
|
|
44
|
+
if (a.target !== b.target)
|
|
45
|
+
return a.target.localeCompare(b.target);
|
|
46
|
+
return (a.label ?? "").localeCompare(b.label ?? "");
|
|
47
|
+
});
|
|
48
|
+
// Compute node sizes deterministically based on label length
|
|
49
|
+
const nodeSizes = new Map();
|
|
50
|
+
for (const n of diagram.nodes) {
|
|
51
|
+
const label = n.label ?? n.id;
|
|
52
|
+
const size = estimateNodeSize(label, n.type, this.opts);
|
|
53
|
+
nodeSizes.set(n.id, size);
|
|
54
|
+
}
|
|
55
|
+
// ---- Rank assignment (longest path layering, deterministic) ----
|
|
56
|
+
const ranks = this.computeRanks(diagram.nodes, edgesSorted);
|
|
57
|
+
// Group by rank
|
|
58
|
+
const rankGroups = new Map();
|
|
59
|
+
for (const [id, rank] of ranks.entries()) {
|
|
60
|
+
const arr = rankGroups.get(rank) ?? [];
|
|
61
|
+
arr.push(id);
|
|
62
|
+
rankGroups.set(rank, arr);
|
|
63
|
+
}
|
|
64
|
+
// Sort ranks ascending
|
|
65
|
+
const sortedRanks = Array.from(rankGroups.keys()).sort((a, b) => a - b);
|
|
66
|
+
// Within each rank, sort ids alphabetically for determinism (stable)
|
|
67
|
+
for (const r of sortedRanks) {
|
|
68
|
+
rankGroups.get(r).sort((a, b) => a.localeCompare(b));
|
|
69
|
+
}
|
|
70
|
+
// ---- Coordinate assignment ----
|
|
71
|
+
// We first compute TB layout, then transform for other directions
|
|
72
|
+
const tbPositions = this.assignTbPositions(rankGroups, sortedRanks, nodeSizes);
|
|
73
|
+
// Transform for direction
|
|
74
|
+
const positioned = this.applyDirection(tbPositions, diagram.direction, nodeSizes);
|
|
75
|
+
// Build LayoutNodes
|
|
76
|
+
const layoutNodes = diagram.nodes
|
|
77
|
+
.slice()
|
|
78
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
79
|
+
.map((n) => {
|
|
80
|
+
const p = positioned.get(n.id);
|
|
81
|
+
const sz = nodeSizes.get(n.id);
|
|
82
|
+
return {
|
|
83
|
+
id: n.id,
|
|
84
|
+
label: n.label ?? n.id,
|
|
85
|
+
type: n.type,
|
|
86
|
+
x: p.x,
|
|
87
|
+
y: p.y,
|
|
88
|
+
width: sz.w,
|
|
89
|
+
height: sz.h,
|
|
90
|
+
data: n,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
// For deterministic output, sort layoutNodes by id (already)
|
|
94
|
+
// Need width/height: compute bounds
|
|
95
|
+
let width;
|
|
96
|
+
let height;
|
|
97
|
+
if (positioned.size === 0) {
|
|
98
|
+
width = this.opts.margin * 2 + 100;
|
|
99
|
+
height = this.opts.margin * 2 + 60;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Compute max extents from positioned nodes
|
|
103
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
104
|
+
for (const [id, pos] of positioned.entries()) {
|
|
105
|
+
const sz = nodeSizes.get(id);
|
|
106
|
+
const left = pos.x - sz.w / 2;
|
|
107
|
+
const right = pos.x + sz.w / 2;
|
|
108
|
+
const top = pos.y - sz.h / 2;
|
|
109
|
+
const bottom = pos.y + sz.h / 2;
|
|
110
|
+
if (left < minX)
|
|
111
|
+
minX = left;
|
|
112
|
+
if (right > maxX)
|
|
113
|
+
maxX = right;
|
|
114
|
+
if (top < minY)
|
|
115
|
+
minY = top;
|
|
116
|
+
if (bottom > maxY)
|
|
117
|
+
maxY = bottom;
|
|
118
|
+
}
|
|
119
|
+
width = maxX + this.opts.margin;
|
|
120
|
+
height = maxY + this.opts.margin;
|
|
121
|
+
// Also account for min if not at margin (should be margin due to assignment, but ensure >=)
|
|
122
|
+
// Our positions already start at margin + w/2, so minX >= margin.
|
|
123
|
+
// Ensure deterministic rounding
|
|
124
|
+
width = Math.ceil(width);
|
|
125
|
+
height = Math.ceil(height);
|
|
126
|
+
// Ensure minimal size
|
|
127
|
+
if (width < this.opts.margin * 2 + 100)
|
|
128
|
+
width = this.opts.margin * 2 + 100;
|
|
129
|
+
if (height < this.opts.margin * 2 + 60)
|
|
130
|
+
height = this.opts.margin * 2 + 60;
|
|
131
|
+
}
|
|
132
|
+
// Build LayoutEdges with points
|
|
133
|
+
const nodePosMap = positioned;
|
|
134
|
+
const layoutEdges = edgesSorted.map((e) => {
|
|
135
|
+
const srcPos = nodePosMap.get(e.source);
|
|
136
|
+
const tgtPos = nodePosMap.get(e.target);
|
|
137
|
+
// Fallback if missing (should not happen, but handle deterministically)
|
|
138
|
+
if (!srcPos || !tgtPos) {
|
|
139
|
+
return {
|
|
140
|
+
source: e.source,
|
|
141
|
+
target: e.target,
|
|
142
|
+
label: e.label,
|
|
143
|
+
points: [
|
|
144
|
+
{ x: 0, y: 0 },
|
|
145
|
+
{ x: 0, y: 0 },
|
|
146
|
+
],
|
|
147
|
+
data: e,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const srcSize = nodeSizes.get(e.source);
|
|
151
|
+
const tgtSize = nodeSizes.get(e.target);
|
|
152
|
+
const points = this.computeEdgePoints({ x: srcPos.x, y: srcPos.y, w: srcSize.w, h: srcSize.h }, { x: tgtPos.x, y: tgtPos.y, w: tgtSize.w, h: tgtSize.h }, diagram.direction, srcPos, tgtPos);
|
|
153
|
+
// label position: midpoint of polyline
|
|
154
|
+
let labelPos;
|
|
155
|
+
if (e.label) {
|
|
156
|
+
labelPos = this.computeLabelPos(points);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
source: e.source,
|
|
160
|
+
target: e.target,
|
|
161
|
+
label: e.label,
|
|
162
|
+
points,
|
|
163
|
+
labelPos,
|
|
164
|
+
data: e,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
// Sort layoutEdges already sorted
|
|
168
|
+
// ---- Compute group layouts ----
|
|
169
|
+
const layoutGroups = this.computeGroups(diagram.groups ?? [], positioned, nodeSizes);
|
|
170
|
+
// Expand width/height to include groups if needed
|
|
171
|
+
let finalWidth = width;
|
|
172
|
+
let finalHeight = height;
|
|
173
|
+
for (const lg of layoutGroups) {
|
|
174
|
+
// flatten recursively
|
|
175
|
+
const all = flattenGroups([lg]);
|
|
176
|
+
for (const g of all) {
|
|
177
|
+
const right = g.x + g.width / 2;
|
|
178
|
+
const bottom = g.y + g.height / 2;
|
|
179
|
+
if (right + this.opts.margin > finalWidth)
|
|
180
|
+
finalWidth = Math.ceil(right + this.opts.margin);
|
|
181
|
+
if (bottom + this.opts.margin > finalHeight)
|
|
182
|
+
finalHeight = Math.ceil(bottom + this.opts.margin);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
diagram,
|
|
187
|
+
direction: diagram.direction,
|
|
188
|
+
nodes: layoutNodes,
|
|
189
|
+
edges: layoutEdges,
|
|
190
|
+
groups: layoutGroups,
|
|
191
|
+
width: finalWidth,
|
|
192
|
+
height: finalHeight,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
computeRanks(nodes, edges) {
|
|
196
|
+
const ranks = new Map();
|
|
197
|
+
for (const n of nodes)
|
|
198
|
+
ranks.set(n.id, 0);
|
|
199
|
+
// Build indegree and adjacency for Kahn's topological layering (deterministic)
|
|
200
|
+
const indeg = new Map();
|
|
201
|
+
const adj = new Map();
|
|
202
|
+
for (const n of nodes) {
|
|
203
|
+
indeg.set(n.id, 0);
|
|
204
|
+
adj.set(n.id, []);
|
|
205
|
+
}
|
|
206
|
+
// Sort edges deterministically before building graph
|
|
207
|
+
const sortedEdges = [...edges].sort((a, b) => {
|
|
208
|
+
if (a.source !== b.source)
|
|
209
|
+
return a.source.localeCompare(b.source);
|
|
210
|
+
if (a.target !== b.target)
|
|
211
|
+
return a.target.localeCompare(b.target);
|
|
212
|
+
return (a.label ?? "").localeCompare(b.label ?? "");
|
|
213
|
+
});
|
|
214
|
+
for (const e of sortedEdges) {
|
|
215
|
+
// Only count edges where both nodes exist (should be all)
|
|
216
|
+
if (!indeg.has(e.target) || !adj.has(e.source))
|
|
217
|
+
continue;
|
|
218
|
+
indeg.set(e.target, (indeg.get(e.target) ?? 0) + 1);
|
|
219
|
+
adj.get(e.source).push(e.target);
|
|
220
|
+
}
|
|
221
|
+
// Sort adjacency lists for determinism
|
|
222
|
+
for (const list of adj.values())
|
|
223
|
+
list.sort((a, b) => a.localeCompare(b));
|
|
224
|
+
// Queue with indegree 0, sorted alphabetically
|
|
225
|
+
const queue = [];
|
|
226
|
+
for (const [id, d] of indeg.entries())
|
|
227
|
+
if (d === 0)
|
|
228
|
+
queue.push(id);
|
|
229
|
+
queue.sort((a, b) => a.localeCompare(b));
|
|
230
|
+
const processed = new Set();
|
|
231
|
+
while (queue.length > 0) {
|
|
232
|
+
// Deterministic: always take smallest alphabetically
|
|
233
|
+
// Since we keep queue sorted after each insertion, shift gives deterministic order
|
|
234
|
+
const u = queue.shift();
|
|
235
|
+
processed.add(u);
|
|
236
|
+
const uRank = ranks.get(u) ?? 0;
|
|
237
|
+
const neighbors = adj.get(u) ?? [];
|
|
238
|
+
for (const v of neighbors) {
|
|
239
|
+
// longest path: rank[v] = max(rank[v], rank[u]+1)
|
|
240
|
+
const cur = ranks.get(v) ?? 0;
|
|
241
|
+
const proposed = uRank + 1;
|
|
242
|
+
if (proposed > cur)
|
|
243
|
+
ranks.set(v, proposed);
|
|
244
|
+
const newDeg = (indeg.get(v) ?? 1) - 1;
|
|
245
|
+
indeg.set(v, newDeg);
|
|
246
|
+
if (newDeg === 0) {
|
|
247
|
+
queue.push(v);
|
|
248
|
+
queue.sort((a, b) => a.localeCompare(b));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Handle cycles: remaining nodes not processed (part of cycles)
|
|
253
|
+
if (processed.size < nodes.length) {
|
|
254
|
+
const remaining = nodes
|
|
255
|
+
.map((n) => n.id)
|
|
256
|
+
.filter((id) => !processed.has(id))
|
|
257
|
+
.sort((a, b) => a.localeCompare(b));
|
|
258
|
+
// Current max rank among processed
|
|
259
|
+
let maxRank = 0;
|
|
260
|
+
for (const v of ranks.values())
|
|
261
|
+
if (v > maxRank)
|
|
262
|
+
maxRank = v;
|
|
263
|
+
// For remaining nodes, assign ranks based on predecessors that are already ranked
|
|
264
|
+
// Iterate in sorted order, allowing cycle nodes to reference each other if earlier in order
|
|
265
|
+
for (const id of remaining) {
|
|
266
|
+
let maxPred = -1;
|
|
267
|
+
for (const e of sortedEdges) {
|
|
268
|
+
if (e.target === id) {
|
|
269
|
+
const predRank = ranks.get(e.source);
|
|
270
|
+
if (predRank !== undefined) {
|
|
271
|
+
if (predRank > maxPred)
|
|
272
|
+
maxPred = predRank;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (maxPred >= 0) {
|
|
277
|
+
// If predecessor exists, put at maxPred+1, but if that predecessor is also in remaining and not yet assigned beyond initial 0,
|
|
278
|
+
// we still use its current rank (which may be 0). To avoid artificially high ranks, we clamp to maxRank+1 max
|
|
279
|
+
const proposed = maxPred + 1;
|
|
280
|
+
// Ensure not too high: if proposed > maxRank + remaining.length, clamp?
|
|
281
|
+
ranks.set(id, proposed);
|
|
282
|
+
if (proposed > maxRank)
|
|
283
|
+
maxRank = proposed;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// No predecessor or isolated cycle entry
|
|
287
|
+
const proposed = maxRank + 1;
|
|
288
|
+
ranks.set(id, proposed);
|
|
289
|
+
maxRank = proposed;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Normalize to start at 0 (should already, but ensure)
|
|
294
|
+
const minRank = Math.min(...Array.from(ranks.values()));
|
|
295
|
+
if (minRank !== 0) {
|
|
296
|
+
for (const [k, v] of ranks.entries())
|
|
297
|
+
ranks.set(k, v - minRank);
|
|
298
|
+
}
|
|
299
|
+
return ranks;
|
|
300
|
+
}
|
|
301
|
+
assignTbPositions(rankGroups, sortedRanks, nodeSizes) {
|
|
302
|
+
const pos = new Map();
|
|
303
|
+
const { nodeHeight, rankSep, nodeSep, margin } = this.opts;
|
|
304
|
+
// Compute max group width for centering
|
|
305
|
+
let maxGroupWidth = 0;
|
|
306
|
+
for (const r of sortedRanks) {
|
|
307
|
+
const ids = rankGroups.get(r);
|
|
308
|
+
let w = 0;
|
|
309
|
+
for (const id of ids) {
|
|
310
|
+
const sz = nodeSizes.get(id);
|
|
311
|
+
w += sz.w;
|
|
312
|
+
}
|
|
313
|
+
w += Math.max(0, ids.length - 1) * nodeSep;
|
|
314
|
+
if (w > maxGroupWidth)
|
|
315
|
+
maxGroupWidth = w;
|
|
316
|
+
}
|
|
317
|
+
// Assign y per rank, x per node within rank
|
|
318
|
+
for (const rank of sortedRanks) {
|
|
319
|
+
const ids = rankGroups.get(rank);
|
|
320
|
+
// compute group width
|
|
321
|
+
let groupWidth = 0;
|
|
322
|
+
for (const id of ids)
|
|
323
|
+
groupWidth += nodeSizes.get(id).w;
|
|
324
|
+
groupWidth += Math.max(0, ids.length - 1) * nodeSep;
|
|
325
|
+
// y center for this rank
|
|
326
|
+
const y = margin + rank * (nodeHeight + rankSep) + nodeHeight / 2;
|
|
327
|
+
// starting x to center group
|
|
328
|
+
const startX = margin + (maxGroupWidth - groupWidth) / 2;
|
|
329
|
+
let cursor = startX;
|
|
330
|
+
for (const id of ids) {
|
|
331
|
+
const sz = nodeSizes.get(id);
|
|
332
|
+
const x = cursor + sz.w / 2;
|
|
333
|
+
pos.set(id, { x: round2(x), y: round2(y) });
|
|
334
|
+
cursor += sz.w + nodeSep;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// For TB, the maxGroupWidth ensures centered, but height computed from ranks
|
|
338
|
+
return pos;
|
|
339
|
+
}
|
|
340
|
+
applyDirection(tbPos, direction, nodeSizes) {
|
|
341
|
+
if (direction === "TB")
|
|
342
|
+
return tbPos;
|
|
343
|
+
// Compute bounds for TB to allow flipping
|
|
344
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
345
|
+
for (const [id, p] of tbPos.entries()) {
|
|
346
|
+
const sz = nodeSizes.get(id);
|
|
347
|
+
const left = p.x - sz.w / 2;
|
|
348
|
+
const right = p.x + sz.w / 2;
|
|
349
|
+
const top = p.y - sz.h / 2;
|
|
350
|
+
const bottom = p.y + sz.h / 2;
|
|
351
|
+
if (left < minX)
|
|
352
|
+
minX = left;
|
|
353
|
+
if (right > maxX)
|
|
354
|
+
maxX = right;
|
|
355
|
+
if (top < minY)
|
|
356
|
+
minY = top;
|
|
357
|
+
if (bottom > maxY)
|
|
358
|
+
maxY = bottom;
|
|
359
|
+
}
|
|
360
|
+
const tbWidth = maxX + this.opts.margin;
|
|
361
|
+
const tbHeight = maxY + this.opts.margin;
|
|
362
|
+
const result = new Map();
|
|
363
|
+
for (const [id, p] of tbPos.entries()) {
|
|
364
|
+
let x = p.x;
|
|
365
|
+
let y = p.y;
|
|
366
|
+
if (direction === "BT") {
|
|
367
|
+
// flip Y: y' = tbHeight - y
|
|
368
|
+
y = tbHeight - y;
|
|
369
|
+
}
|
|
370
|
+
else if (direction === "LR") {
|
|
371
|
+
// swap axes: x' = y, y' = x but need to maintain margin? Simpler: transpose
|
|
372
|
+
// For LR, rank becomes horizontal: so original y (rank) becomes x
|
|
373
|
+
// Original x (within rank) becomes y
|
|
374
|
+
// We can just swap
|
|
375
|
+
const tmp = x;
|
|
376
|
+
x = y;
|
|
377
|
+
y = tmp;
|
|
378
|
+
}
|
|
379
|
+
else if (direction === "RL") {
|
|
380
|
+
const tmp = x;
|
|
381
|
+
x = tbHeight - y; // since tbHeight is the extent in y direction which becomes x
|
|
382
|
+
y = tmp;
|
|
383
|
+
}
|
|
384
|
+
result.set(id, { x: round2(x), y: round2(y) });
|
|
385
|
+
}
|
|
386
|
+
// For LR/RL, we swapped but need to re-normalize to ensure margin? The swap already includes margin, so fine.
|
|
387
|
+
// However for RL we used tbHeight as width after transpose; ensure not negative.
|
|
388
|
+
// Simpler alternative for LR/RL: recompute via same method but swapped rankSep logic.
|
|
389
|
+
// But our transpose approach is deterministic and preserves relative spacing.
|
|
390
|
+
// For LR, after transpose, we may want to ensure left margin still applies: x values start at margin, which they do after swap because y originally started at margin+...
|
|
391
|
+
// It's okay.
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
computeEdgePoints(src, tgt, direction, srcPos, tgtPos) {
|
|
395
|
+
// Determine exit/entry sides based on direction and relative positions
|
|
396
|
+
// For TB/BT, use vertical sides unless same rank then use horizontal
|
|
397
|
+
// For LR/RL, use horizontal sides unless same "rank" (now column) then vertical
|
|
398
|
+
// If direction is TB: source bottom, target top
|
|
399
|
+
// BT: source top, target bottom
|
|
400
|
+
// LR: source right, target left
|
|
401
|
+
// RL: source left, target right
|
|
402
|
+
// For edges that go backwards (e.g., cycle where source rank > target rank), we still use direction's side? That may cause overlap but deterministic.
|
|
403
|
+
// To handle backwards, we could choose side based on vector between nodes.
|
|
404
|
+
// We'll use direction default, but if edge is reverse rank, we flip side to keep line going backwards visually.
|
|
405
|
+
// Determine if edge is "backward" by comparing rank order approximated via positions
|
|
406
|
+
let isBackward = false;
|
|
407
|
+
if (direction === "TB")
|
|
408
|
+
isBackward = srcPos.y > tgtPos.y;
|
|
409
|
+
else if (direction === "BT")
|
|
410
|
+
isBackward = srcPos.y < tgtPos.y;
|
|
411
|
+
else if (direction === "LR")
|
|
412
|
+
isBackward = srcPos.x > tgtPos.x;
|
|
413
|
+
else if (direction === "RL")
|
|
414
|
+
isBackward = srcPos.x < tgtPos.x;
|
|
415
|
+
// If same rank (positions equal in rank axis), use horizontal for TB/BT and vertical for LR/RL
|
|
416
|
+
const sameRank = (direction === "TB" || direction === "BT") ? Math.abs(srcPos.y - tgtPos.y) < 1 : Math.abs(srcPos.x - tgtPos.x) < 1;
|
|
417
|
+
let sx, sy, tx, ty;
|
|
418
|
+
if (sameRank) {
|
|
419
|
+
// horizontal edge within same rank for TB
|
|
420
|
+
if (srcPos.x < tgtPos.x) {
|
|
421
|
+
sx = src.x + src.w / 2;
|
|
422
|
+
sy = src.y;
|
|
423
|
+
tx = tgt.x - tgt.w / 2;
|
|
424
|
+
ty = tgt.y;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
sx = src.x - src.w / 2;
|
|
428
|
+
sy = src.y;
|
|
429
|
+
tx = tgt.x + tgt.w / 2;
|
|
430
|
+
ty = tgt.y;
|
|
431
|
+
}
|
|
432
|
+
// For LR same column, vertical
|
|
433
|
+
if (direction === "LR" || direction === "RL") {
|
|
434
|
+
if (srcPos.y < tgtPos.y) {
|
|
435
|
+
sx = src.x;
|
|
436
|
+
sy = src.y + src.h / 2;
|
|
437
|
+
tx = tgt.x;
|
|
438
|
+
ty = tgt.y - tgt.h / 2;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
sx = src.x;
|
|
442
|
+
sy = src.y - src.h / 2;
|
|
443
|
+
tx = tgt.x;
|
|
444
|
+
ty = tgt.y + tgt.h / 2;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (direction === "TB") {
|
|
449
|
+
if (!isBackward) {
|
|
450
|
+
sx = src.x;
|
|
451
|
+
sy = src.y + src.h / 2;
|
|
452
|
+
tx = tgt.x;
|
|
453
|
+
ty = tgt.y - tgt.h / 2;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// backward: exit top, enter bottom
|
|
457
|
+
sx = src.x;
|
|
458
|
+
sy = src.y - src.h / 2;
|
|
459
|
+
tx = tgt.x;
|
|
460
|
+
ty = tgt.y + tgt.h / 2;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else if (direction === "BT") {
|
|
464
|
+
if (!isBackward) {
|
|
465
|
+
sx = src.x;
|
|
466
|
+
sy = src.y - src.h / 2;
|
|
467
|
+
tx = tgt.x;
|
|
468
|
+
ty = tgt.y + tgt.h / 2;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
sx = src.x;
|
|
472
|
+
sy = src.y + src.h / 2;
|
|
473
|
+
tx = tgt.x;
|
|
474
|
+
ty = tgt.y - tgt.h / 2;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else if (direction === "LR") {
|
|
478
|
+
if (!isBackward) {
|
|
479
|
+
sx = src.x + src.w / 2;
|
|
480
|
+
sy = src.y;
|
|
481
|
+
tx = tgt.x - tgt.w / 2;
|
|
482
|
+
ty = tgt.y;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
sx = src.x - src.w / 2;
|
|
486
|
+
sy = src.y;
|
|
487
|
+
tx = tgt.x + tgt.w / 2;
|
|
488
|
+
ty = tgt.y;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else { // RL
|
|
492
|
+
if (!isBackward) {
|
|
493
|
+
sx = src.x - src.w / 2;
|
|
494
|
+
sy = src.y;
|
|
495
|
+
tx = tgt.x + tgt.w / 2;
|
|
496
|
+
ty = tgt.y;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
sx = src.x + src.w / 2;
|
|
500
|
+
sy = src.y;
|
|
501
|
+
tx = tgt.x - tgt.w / 2;
|
|
502
|
+
ty = tgt.y;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Round
|
|
506
|
+
sx = round2(sx);
|
|
507
|
+
sy = round2(sy);
|
|
508
|
+
tx = round2(tx);
|
|
509
|
+
ty = round2(ty);
|
|
510
|
+
// For straight edge, points are [start, end]
|
|
511
|
+
// For more interesting routing, if edge spans multiple ranks, we could add intermediate bend points
|
|
512
|
+
// For now, provide straight line. For orthogonal look, we could add one bend if needed for RL/LR?
|
|
513
|
+
// Keep simple deterministic: two points
|
|
514
|
+
// If edge is not straight due to needing to avoid overlapping nodes? Keep straight for simplicity
|
|
515
|
+
// Optionally add midpoint for label? But points length 2 is fine
|
|
516
|
+
// If source and target are same node (self-loop), create loop points
|
|
517
|
+
if (src.x === tgt.x && src.y === tgt.y) {
|
|
518
|
+
// self-loop: create small loop to the right
|
|
519
|
+
const loopSize = 24;
|
|
520
|
+
return [
|
|
521
|
+
{ x: sx, y: sy },
|
|
522
|
+
{ x: sx + loopSize, y: sy },
|
|
523
|
+
{ x: sx + loopSize, y: sy + loopSize },
|
|
524
|
+
{ x: src.x, y: src.y + src.h / 2 }, // bottom
|
|
525
|
+
];
|
|
526
|
+
}
|
|
527
|
+
return [{ x: sx, y: sy }, { x: tx, y: ty }];
|
|
528
|
+
}
|
|
529
|
+
computeLabelPos(points) {
|
|
530
|
+
if (points.length === 0)
|
|
531
|
+
return { x: 0, y: 0 };
|
|
532
|
+
if (points.length === 1)
|
|
533
|
+
return { x: points[0].x, y: points[0].y };
|
|
534
|
+
if (points.length === 2) {
|
|
535
|
+
const a = points[0], b = points[1];
|
|
536
|
+
return { x: round2((a.x + b.x) / 2), y: round2((a.y + b.y) / 2 - 8) };
|
|
537
|
+
}
|
|
538
|
+
const mid = Math.floor(points.length / 2);
|
|
539
|
+
const a = points[mid - 1], b = points[mid];
|
|
540
|
+
return { x: round2((a.x + b.x) / 2), y: round2((a.y + b.y) / 2 - 8) };
|
|
541
|
+
}
|
|
542
|
+
computeGroups(groups, positioned, nodeSizes) {
|
|
543
|
+
const groupPadding = 20;
|
|
544
|
+
const headerHeight = 24; // space for group label
|
|
545
|
+
const recurse = (grp) => {
|
|
546
|
+
// Compute child layouts first
|
|
547
|
+
const childLayouts = grp.groups.map(recurse);
|
|
548
|
+
// Compute bounds of members + children
|
|
549
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
550
|
+
let hasContent = false;
|
|
551
|
+
for (const nid of grp.nodeIds) {
|
|
552
|
+
const pos = positioned.get(nid);
|
|
553
|
+
const sz = nodeSizes.get(nid);
|
|
554
|
+
if (!pos || !sz)
|
|
555
|
+
continue;
|
|
556
|
+
hasContent = true;
|
|
557
|
+
const l = pos.x - sz.w / 2;
|
|
558
|
+
const r = pos.x + sz.w / 2;
|
|
559
|
+
const t = pos.y - sz.h / 2;
|
|
560
|
+
const b = pos.y + sz.h / 2;
|
|
561
|
+
if (l < minX)
|
|
562
|
+
minX = l;
|
|
563
|
+
if (r > maxX)
|
|
564
|
+
maxX = r;
|
|
565
|
+
if (t < minY)
|
|
566
|
+
minY = t;
|
|
567
|
+
if (b > maxY)
|
|
568
|
+
maxY = b;
|
|
569
|
+
}
|
|
570
|
+
for (const cl of childLayouts) {
|
|
571
|
+
hasContent = true;
|
|
572
|
+
const l = cl.x - cl.width / 2;
|
|
573
|
+
const r = cl.x + cl.width / 2;
|
|
574
|
+
const t = cl.y - cl.height / 2;
|
|
575
|
+
const b = cl.y + cl.height / 2;
|
|
576
|
+
if (l < minX)
|
|
577
|
+
minX = l;
|
|
578
|
+
if (r > maxX)
|
|
579
|
+
maxX = r;
|
|
580
|
+
if (t < minY)
|
|
581
|
+
minY = t;
|
|
582
|
+
if (b > maxY)
|
|
583
|
+
maxY = b;
|
|
584
|
+
}
|
|
585
|
+
if (!hasContent) {
|
|
586
|
+
// Empty group: small placeholder centered near margin
|
|
587
|
+
const cx = this.opts.margin + 50;
|
|
588
|
+
const cy = this.opts.margin + 30;
|
|
589
|
+
return {
|
|
590
|
+
id: grp.id,
|
|
591
|
+
label: grp.label ?? grp.id,
|
|
592
|
+
type: grp.type,
|
|
593
|
+
x: round2(cx),
|
|
594
|
+
y: round2(cy),
|
|
595
|
+
width: 100,
|
|
596
|
+
height: 60,
|
|
597
|
+
data: grp,
|
|
598
|
+
memberIds: [...grp.nodeIds],
|
|
599
|
+
children: childLayouts,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
// Add padding and header
|
|
603
|
+
minX -= groupPadding;
|
|
604
|
+
maxX += groupPadding;
|
|
605
|
+
minY -= groupPadding + headerHeight;
|
|
606
|
+
maxY += groupPadding;
|
|
607
|
+
const w = maxX - minX;
|
|
608
|
+
const h = maxY - minY;
|
|
609
|
+
const cx = (minX + maxX) / 2;
|
|
610
|
+
const cy = (minY + maxY) / 2;
|
|
611
|
+
return {
|
|
612
|
+
id: grp.id,
|
|
613
|
+
label: grp.label ?? grp.id,
|
|
614
|
+
type: grp.type,
|
|
615
|
+
x: round2(cx),
|
|
616
|
+
y: round2(cy),
|
|
617
|
+
width: round2(w),
|
|
618
|
+
height: round2(h),
|
|
619
|
+
data: grp,
|
|
620
|
+
memberIds: [...grp.nodeIds],
|
|
621
|
+
children: childLayouts,
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
// For top-level groups, compute deterministically sorted by id
|
|
625
|
+
const sorted = [...groups].sort((a, b) => a.id.localeCompare(b.id));
|
|
626
|
+
return sorted.map(recurse);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function estimateNodeSize(label, type, opts) {
|
|
630
|
+
// Deterministic size based on label length and type
|
|
631
|
+
// Use char width 7, padding
|
|
632
|
+
const charW = 7;
|
|
633
|
+
const paddingX = 24;
|
|
634
|
+
const paddingY = 12;
|
|
635
|
+
const baseH = 36;
|
|
636
|
+
let minW = opts.minNodeWidth;
|
|
637
|
+
let maxW = opts.maxNodeWidth;
|
|
638
|
+
let h = opts.nodeHeight;
|
|
639
|
+
// Type-specific adjustments
|
|
640
|
+
if (type === "database") {
|
|
641
|
+
// database slightly wider
|
|
642
|
+
minW = Math.max(minW, 90);
|
|
643
|
+
h = 48;
|
|
644
|
+
}
|
|
645
|
+
else if (type === "person") {
|
|
646
|
+
// person as circle-like, make square
|
|
647
|
+
// width based on label but ensure at least height
|
|
648
|
+
minW = Math.max(minW, 80);
|
|
649
|
+
h = 48;
|
|
650
|
+
}
|
|
651
|
+
else if (type === "service") {
|
|
652
|
+
minW = Math.max(minW, 90);
|
|
653
|
+
}
|
|
654
|
+
const textW = label.length * charW + paddingX;
|
|
655
|
+
let w = Math.max(minW, Math.min(maxW, textW));
|
|
656
|
+
// Ensure width handles type decorations: database needs extra for cylinder? Keep same
|
|
657
|
+
// Snap to even for determinism
|
|
658
|
+
w = Math.round(w);
|
|
659
|
+
h = Math.round(h);
|
|
660
|
+
return { w, h };
|
|
661
|
+
}
|
|
662
|
+
function flattenGroups(groups) {
|
|
663
|
+
const out = [];
|
|
664
|
+
for (const g of groups) {
|
|
665
|
+
out.push(g);
|
|
666
|
+
if (g.children.length > 0)
|
|
667
|
+
out.push(...flattenGroups(g.children));
|
|
668
|
+
}
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function round2(n) {
|
|
672
|
+
return Math.round(n * 100) / 100;
|
|
673
|
+
}
|
|
674
|
+
//# sourceMappingURL=simple.js.map
|