@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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/check.d.ts +14 -0
  3. package/dist/src/cli/check.d.ts.map +1 -0
  4. package/dist/src/cli/check.js +88 -0
  5. package/dist/src/cli/check.js.map +1 -0
  6. package/dist/src/cli/format.d.ts +9 -0
  7. package/dist/src/cli/format.d.ts.map +1 -0
  8. package/dist/src/cli/format.js +114 -0
  9. package/dist/src/cli/format.js.map +1 -0
  10. package/dist/src/cli/main.d.ts +19 -0
  11. package/dist/src/cli/main.d.ts.map +1 -0
  12. package/dist/src/cli/main.js +207 -0
  13. package/dist/src/cli/main.js.map +1 -0
  14. package/dist/src/cli/render.d.ts +7 -0
  15. package/dist/src/cli/render.d.ts.map +1 -0
  16. package/dist/src/cli/render.js +90 -0
  17. package/dist/src/cli/render.js.map +1 -0
  18. package/dist/src/diagnostics.d.ts +32 -0
  19. package/dist/src/diagnostics.d.ts.map +1 -0
  20. package/dist/src/diagnostics.js +4 -0
  21. package/dist/src/diagnostics.js.map +1 -0
  22. package/dist/src/index.d.ts +40 -0
  23. package/dist/src/index.d.ts.map +1 -0
  24. package/dist/src/index.js +64 -0
  25. package/dist/src/index.js.map +1 -0
  26. package/dist/src/language/completion.d.ts +6 -0
  27. package/dist/src/language/completion.d.ts.map +1 -0
  28. package/dist/src/language/completion.js +363 -0
  29. package/dist/src/language/completion.js.map +1 -0
  30. package/dist/src/language/definitions.d.ts +9 -0
  31. package/dist/src/language/definitions.d.ts.map +1 -0
  32. package/dist/src/language/definitions.js +70 -0
  33. package/dist/src/language/definitions.js.map +1 -0
  34. package/dist/src/language/diagnosticsService.d.ts +14 -0
  35. package/dist/src/language/diagnosticsService.d.ts.map +1 -0
  36. package/dist/src/language/diagnosticsService.js +37 -0
  37. package/dist/src/language/diagnosticsService.js.map +1 -0
  38. package/dist/src/language/folding.d.ts +8 -0
  39. package/dist/src/language/folding.d.ts.map +1 -0
  40. package/dist/src/language/folding.js +58 -0
  41. package/dist/src/language/folding.js.map +1 -0
  42. package/dist/src/language/formatting.d.ts +5 -0
  43. package/dist/src/language/formatting.d.ts.map +1 -0
  44. package/dist/src/language/formatting.js +274 -0
  45. package/dist/src/language/formatting.js.map +1 -0
  46. package/dist/src/language/highlighting.d.ts +9 -0
  47. package/dist/src/language/highlighting.d.ts.map +1 -0
  48. package/dist/src/language/highlighting.js +74 -0
  49. package/dist/src/language/highlighting.js.map +1 -0
  50. package/dist/src/language/hover.d.ts +6 -0
  51. package/dist/src/language/hover.d.ts.map +1 -0
  52. package/dist/src/language/hover.js +197 -0
  53. package/dist/src/language/hover.js.map +1 -0
  54. package/dist/src/language/indentation.d.ts +15 -0
  55. package/dist/src/language/indentation.d.ts.map +1 -0
  56. package/dist/src/language/indentation.js +97 -0
  57. package/dist/src/language/indentation.js.map +1 -0
  58. package/dist/src/language/index.d.ts +54 -0
  59. package/dist/src/language/index.d.ts.map +1 -0
  60. package/dist/src/language/index.js +85 -0
  61. package/dist/src/language/index.js.map +1 -0
  62. package/dist/src/language/references.d.ts +12 -0
  63. package/dist/src/language/references.d.ts.map +1 -0
  64. package/dist/src/language/references.js +35 -0
  65. package/dist/src/language/references.js.map +1 -0
  66. package/dist/src/language/rename.d.ts +18 -0
  67. package/dist/src/language/rename.d.ts.map +1 -0
  68. package/dist/src/language/rename.js +53 -0
  69. package/dist/src/language/rename.js.map +1 -0
  70. package/dist/src/language/semanticModel.d.ts +21 -0
  71. package/dist/src/language/semanticModel.d.ts.map +1 -0
  72. package/dist/src/language/semanticModel.js +43 -0
  73. package/dist/src/language/semanticModel.js.map +1 -0
  74. package/dist/src/language/symbols.d.ts +9 -0
  75. package/dist/src/language/symbols.d.ts.map +1 -0
  76. package/dist/src/language/symbols.js +160 -0
  77. package/dist/src/language/symbols.js.map +1 -0
  78. package/dist/src/language/types.d.ts +49 -0
  79. package/dist/src/language/types.d.ts.map +1 -0
  80. package/dist/src/language/types.js +2 -0
  81. package/dist/src/language/types.js.map +1 -0
  82. package/dist/src/language/utils.d.ts +24 -0
  83. package/dist/src/language/utils.d.ts.map +1 -0
  84. package/dist/src/language/utils.js +154 -0
  85. package/dist/src/language/utils.js.map +1 -0
  86. package/dist/src/layout/dagre.d.ts +13 -0
  87. package/dist/src/layout/dagre.d.ts.map +1 -0
  88. package/dist/src/layout/dagre.js +229 -0
  89. package/dist/src/layout/dagre.js.map +1 -0
  90. package/dist/src/layout/index.d.ts +15 -0
  91. package/dist/src/layout/index.d.ts.map +1 -0
  92. package/dist/src/layout/index.js +22 -0
  93. package/dist/src/layout/index.js.map +1 -0
  94. package/dist/src/layout/simple.d.ts +20 -0
  95. package/dist/src/layout/simple.d.ts.map +1 -0
  96. package/dist/src/layout/simple.js +674 -0
  97. package/dist/src/layout/simple.js.map +1 -0
  98. package/dist/src/layout/types.d.ts +65 -0
  99. package/dist/src/layout/types.d.ts.map +1 -0
  100. package/dist/src/layout/types.js +2 -0
  101. package/dist/src/layout/types.js.map +1 -0
  102. package/dist/src/lexer.d.ts +25 -0
  103. package/dist/src/lexer.d.ts.map +1 -0
  104. package/dist/src/lexer.js +280 -0
  105. package/dist/src/lexer.js.map +1 -0
  106. package/dist/src/lsp/documents.d.ts +24 -0
  107. package/dist/src/lsp/documents.d.ts.map +1 -0
  108. package/dist/src/lsp/documents.js +97 -0
  109. package/dist/src/lsp/documents.js.map +1 -0
  110. package/dist/src/lsp/server.d.ts +70 -0
  111. package/dist/src/lsp/server.d.ts.map +1 -0
  112. package/dist/src/lsp/server.js +623 -0
  113. package/dist/src/lsp/server.js.map +1 -0
  114. package/dist/src/lsp/utils.d.ts +47 -0
  115. package/dist/src/lsp/utils.d.ts.map +1 -0
  116. package/dist/src/lsp/utils.js +142 -0
  117. package/dist/src/lsp/utils.js.map +1 -0
  118. package/dist/src/parser.d.ts +106 -0
  119. package/dist/src/parser.d.ts.map +1 -0
  120. package/dist/src/parser.js +1018 -0
  121. package/dist/src/parser.js.map +1 -0
  122. package/dist/src/pipeline.d.ts +51 -0
  123. package/dist/src/pipeline.d.ts.map +1 -0
  124. package/dist/src/pipeline.js +65 -0
  125. package/dist/src/pipeline.js.map +1 -0
  126. package/dist/src/range.d.ts +24 -0
  127. package/dist/src/range.d.ts.map +1 -0
  128. package/dist/src/range.js +20 -0
  129. package/dist/src/range.js.map +1 -0
  130. package/dist/src/render/index.d.ts +3 -0
  131. package/dist/src/render/index.d.ts.map +1 -0
  132. package/dist/src/render/index.js +3 -0
  133. package/dist/src/render/index.js.map +1 -0
  134. package/dist/src/render/shapes.d.ts +20 -0
  135. package/dist/src/render/shapes.d.ts.map +1 -0
  136. package/dist/src/render/shapes.js +110 -0
  137. package/dist/src/render/shapes.js.map +1 -0
  138. package/dist/src/render/svg.d.ts +19 -0
  139. package/dist/src/render/svg.d.ts.map +1 -0
  140. package/dist/src/render/svg.js +227 -0
  141. package/dist/src/render/svg.js.map +1 -0
  142. package/dist/src/security/url.d.ts +7 -0
  143. package/dist/src/security/url.d.ts.map +1 -0
  144. package/dist/src/security/url.js +27 -0
  145. package/dist/src/security/url.js.map +1 -0
  146. package/dist/src/types.d.ts +111 -0
  147. package/dist/src/types.d.ts.map +1 -0
  148. package/dist/src/types.js +16 -0
  149. package/dist/src/types.js.map +1 -0
  150. package/dist/src/validator.d.ts +47 -0
  151. package/dist/src/validator.d.ts.map +1 -0
  152. package/dist/src/validator.js +235 -0
  153. package/dist/src/validator.js.map +1 -0
  154. 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