@nodius/layouting 0.1.0 → 0.1.1

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 (38) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +280 -126
  3. package/dist/algorithms/component-packing.d.ts +9 -0
  4. package/dist/algorithms/component-packing.d.ts.map +1 -0
  5. package/dist/algorithms/coordinate-assignment.d.ts +7 -0
  6. package/dist/algorithms/coordinate-assignment.d.ts.map +1 -0
  7. package/dist/algorithms/crossing-minimization.d.ts +7 -0
  8. package/dist/algorithms/crossing-minimization.d.ts.map +1 -0
  9. package/dist/algorithms/cycle-breaking.d.ts +8 -0
  10. package/dist/algorithms/cycle-breaking.d.ts.map +1 -0
  11. package/dist/algorithms/edge-routing.d.ts +17 -0
  12. package/dist/algorithms/edge-routing.d.ts.map +1 -0
  13. package/dist/algorithms/layer-assignment.d.ts +20 -0
  14. package/dist/algorithms/layer-assignment.d.ts.map +1 -0
  15. package/dist/algorithms/value-cluster.d.ts +15 -0
  16. package/dist/algorithms/value-cluster.d.ts.map +1 -0
  17. package/dist/algorithms/value-placement.d.ts +25 -0
  18. package/dist/algorithms/value-placement.d.ts.map +1 -0
  19. package/dist/debug.d.ts +20 -0
  20. package/dist/debug.d.ts.map +1 -0
  21. package/dist/graph.d.ts +50 -0
  22. package/dist/graph.d.ts.map +1 -0
  23. package/dist/incremental.d.ts +33 -0
  24. package/dist/incremental.d.ts.map +1 -0
  25. package/dist/index.d.ts +7 -176
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +904 -149
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +901 -148
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/layout.d.ts +10 -0
  32. package/dist/layout.d.ts.map +1 -0
  33. package/dist/proposals.d.ts +31 -0
  34. package/dist/proposals.d.ts.map +1 -0
  35. package/dist/types.d.ts +155 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/package.json +5 -4
  38. package/dist/index.d.mts +0 -176
package/README.md CHANGED
@@ -1,145 +1,267 @@
1
- # nodius-layouting
1
+ # @nodius/layouting
2
2
 
3
- A **zero-dependency**, high-performance graph layouting library for node-based technical diagrams.
3
+ A **zero-dependency**, high-performance graph layouting library for node-based
4
+ technical diagrams.
4
5
 
5
- Built for real-world use cases: data pipelines, visual programming environments, workflow editors, and any system where nodes with typed handles are connected by edges.
6
+ Built for real-world use cases: data pipelines, visual programming environments,
7
+ workflow editors, and any system where typed-handle nodes are connected by
8
+ typed edges, with parent/child grouping and side-attached values.
6
9
 
7
10
  ## Features
8
11
 
9
- - **Zero runtime dependencies** - Pure TypeScript, nothing else
10
- - **Optimized Sugiyama-based layout** - Produces clean, hierarchical technical diagrams
11
- - **Handle-aware** - Each node has multiple handles (input/output) with precise positioning on any side (top, right, bottom, left) with configurable offsets
12
- - **Orthogonal edge routing** - Clean right-angle edge paths that respect handle positions
13
- - **Incremental layout** - Add or remove nodes without recomputing the entire layout
14
- - **4 layout directions** - Top-to-bottom, left-to-right, bottom-to-top, right-to-left
15
- - **Cycle support** - Automatically handles cyclic graphs
16
- - **Scales to 1000+ nodes** - Optimized algorithms (merge-sort crossing count, adaptive iterations)
12
+ - **Zero runtime dependencies** pure TypeScript, nothing else
13
+ - **Strict-axis Sugiyama** the chosen reading direction is honored end to end; compacity comes from packing and sidecars, never from local re-orientation
14
+ - **Typed edges** (`control` / `data`) `data` edges have light weight and pull their value nodes onto the consumer's layer instead of extending the rail
15
+ - **Compound layout** nodes can declare `parentId`; children are laid out inside their parent's bounding box recursively
16
+ - **Sidecar value placement** value nodes (only data edges) are attached to the flanks of their consumer, picked from the consumer's handle side
17
+ - **Component packing** disjoint components are packed along the order axis for a square-ish aspect ratio
18
+ - **Rotation proposals** when handle orientation doesn't match the chosen direction (or the value's sidecar slot), the engine asks the application via `onProposal` whether to rotate
19
+ - **Handle-aware** every node has multiple input/output handles with `top`/`right`/`bottom`/`left` positions and configurable offsets
20
+ - **Orthogonal edge routing** through dummy waypoints
21
+ - **Incremental layout** — keep position stability across edits via `IncrementalLayout`
22
+ - **4 layout directions** — `TB`, `LR`, `BT`, `RL`
23
+ - **Cycle support** — back edges are detected and reversed automatically
24
+ - **Scales** — ~150 ms on 1000 nodes; merge-sort based crossing counting; adaptive iteration cap on large graphs
17
25
 
18
26
  ## Performance
19
27
 
20
- Benchmarked on a standard development machine:
28
+ Measured on a standard dev machine (Node 22, vitest run):
21
29
 
22
- | Graph Size | Time |
23
- |---|---|
24
- | 100 nodes, 737 edges (dense) | ~70ms |
25
- | 200 nodes | ~25ms |
26
- | 500 nodes | ~100ms |
27
- | 1000 nodes | ~150ms |
30
+ | Graph size | Time |
31
+ |----------------------------------|--------|
32
+ | 100 nodes, 737 edges (dense) | ~70 ms |
33
+ | 200 nodes | ~25 ms |
34
+ | 500 nodes | ~90 ms |
35
+ | 1000 nodes | ~150 ms|
28
36
 
29
37
  ## Installation
30
38
 
31
39
  ```bash
32
- npm install nodius-layouting
40
+ npm install @nodius/layouting
33
41
  ```
34
42
 
35
- ## Quick Start
43
+ ## Quick start
36
44
 
37
- ```typescript
38
- import { layout } from 'nodius-layouting';
45
+ ```ts
46
+ import { layout } from '@nodius/layouting';
39
47
 
40
48
  const result = layout({
41
49
  nodes: [
42
- {
43
- id: 'source',
44
- width: 120,
45
- height: 60,
46
- handles: [
47
- { id: 'out', type: 'output', position: 'bottom', offset: 0.5 },
48
- ],
49
- },
50
- {
51
- id: 'transform',
52
- width: 150,
53
- height: 80,
54
- handles: [
55
- { id: 'in', type: 'input', position: 'top', offset: 0.5 },
56
- { id: 'out', type: 'output', position: 'bottom', offset: 0.5 },
57
- { id: 'err', type: 'output', position: 'right', offset: 0.5 },
58
- ],
59
- },
60
- {
61
- id: 'sink',
62
- width: 120,
63
- height: 60,
64
- handles: [
65
- { id: 'in', type: 'input', position: 'top', offset: 0.5 },
66
- ],
67
- },
50
+ { id: 'src', width: 120, height: 60, handles: [
51
+ { id: 'out', type: 'output', position: 'bottom' },
52
+ ]},
53
+ { id: 'transform', width: 150, height: 80, handles: [
54
+ { id: 'in', type: 'input', position: 'top' },
55
+ { id: 'out', type: 'output', position: 'bottom' },
56
+ ]},
57
+ { id: 'sink', width: 120, height: 60, handles: [
58
+ { id: 'in', type: 'input', position: 'top' },
59
+ ]},
68
60
  ],
69
61
  edges: [
70
- { id: 'e1', from: 'source', to: 'transform', fromHandle: 'out', toHandle: 'in' },
62
+ { id: 'e1', from: 'src', to: 'transform', fromHandle: 'out', toHandle: 'in' },
71
63
  { id: 'e2', from: 'transform', to: 'sink', fromHandle: 'out', toHandle: 'in' },
72
64
  ],
65
+ }, { direction: 'TB' });
66
+
67
+ // result.nodes → positioned nodes with absolute handle coordinates and parentId echoed
68
+ // result.edges → routed edges with waypoint arrays and `kind`
69
+ ```
70
+
71
+ ## Typed edges and value nodes
72
+
73
+ Edges accept a `kind` of `'control'` (default) or `'data'`. **Control edges**
74
+ define the execution rail and drive layer assignment. **Data edges** are weak
75
+ links — their endpoints are pulled onto the consumer's layer, and a node whose
76
+ every incident edge is a data edge becomes a **value**: it is attached as a
77
+ sidecar to the flank of its dominant consumer rather than living inside the
78
+ rail.
79
+
80
+ ```ts
81
+ import { layout } from '@nodius/layouting';
82
+
83
+ const result = layout({
84
+ nodes: [
85
+ { id: 'Start', width: 100, height: 50, handles: [
86
+ { id: 'out', type: 'output', position: 'bottom' },
87
+ ]},
88
+ { id: 'fetch', width: 140, height: 70, handles: [
89
+ { id: 'in', type: 'input', position: 'top' },
90
+ { id: 'key', type: 'input', position: 'left', offset: 0.3 },
91
+ { id: 'url', type: 'input', position: 'left', offset: 0.7 },
92
+ { id: 'out', type: 'output', position: 'bottom' },
93
+ ]},
94
+ { id: 'API_KEY', width: 100, height: 40, handles: [
95
+ { id: 'out', type: 'output', position: 'right' },
96
+ ]},
97
+ { id: 'BASE_URL', width: 100, height: 40, handles: [
98
+ { id: 'out', type: 'output', position: 'right' },
99
+ ]},
100
+ { id: 'Done', width: 100, height: 50, handles: [
101
+ { id: 'in', type: 'input', position: 'top' },
102
+ ]},
103
+ ],
104
+ edges: [
105
+ { id: 'c1', from: 'Start', to: 'fetch', fromHandle: 'out', toHandle: 'in', kind: 'control' },
106
+ { id: 'c2', from: 'fetch', to: 'Done', fromHandle: 'out', toHandle: 'in', kind: 'control' },
107
+ { id: 'd1', from: 'API_KEY', to: 'fetch', fromHandle: 'out', toHandle: 'key', kind: 'data' },
108
+ { id: 'd2', from: 'BASE_URL', to: 'fetch', fromHandle: 'out', toHandle: 'url', kind: 'data' },
109
+ ],
110
+ });
111
+ // Start, fetch and Done line up vertically. API_KEY and BASE_URL sit on
112
+ // fetch's left flank, at fetch's vertical band — they don't extend the rail.
113
+ ```
114
+
115
+ ## Compound (nested) layout
116
+
117
+ Set `parentId` on any node to make it a child of another. Children are laid out
118
+ inside their parent's bounding box; the parent grows to fit them plus an
119
+ optional padding and a header strip for its own label.
120
+
121
+ ```ts
122
+ const result = layout({
123
+ nodes: [
124
+ { id: 'Start', width: 110, height: 50, handles: [...] },
125
+ { id: 'parallel', width: 220, height: 140, handles: [
126
+ { id: 'in', type: 'input', position: 'top' },
127
+ { id: 'out', type: 'output', position: 'bottom' },
128
+ ]},
129
+ { id: 'fetch users', parentId: 'parallel', width: 130, height: 50, handles: [...] },
130
+ { id: 'fetch orders', parentId: 'parallel', width: 130, height: 50, handles: [...] },
131
+ { id: 'End', width: 100, height: 50, handles: [...] },
132
+ ],
133
+ edges: [
134
+ { id: 'c1', from: 'Start', to: 'parallel', fromHandle: 'out', toHandle: 'in', kind: 'control' },
135
+ { id: 'c2', from: 'parallel', to: 'End', fromHandle: 'out', toHandle: 'in', kind: 'control' },
136
+ ],
137
+ });
138
+
139
+ // Output nodes carry `parentId` so you can reconstruct the hierarchy in your renderer.
140
+ ```
141
+
142
+ ## Rotation proposals (`onProposal`)
143
+
144
+ When a node's handles don't match how the engine plans to place it, it emits a
145
+ **rotation proposal**. The application decides what to do:
146
+
147
+ ```ts
148
+ import { layout } from '@nodius/layouting';
149
+
150
+ layout(input, {
151
+ direction: 'LR',
152
+ // Accept every proposal and use the engine-suggested rotation:
153
+ onProposal: (p) => p.proposed,
154
+
155
+ // Or filter: only accept rotations for nodes you own
156
+ // onProposal: (p) => p.nodeId.startsWith('mine_') ? p.proposed : null,
157
+
158
+ // Or partial: rotate only some handles
159
+ // onProposal: (p) => ({
160
+ // ...p.current,
161
+ // handles: p.current.handles.map(h => h.id === 'main' ? { ...h, position: 'right' } : h),
162
+ // }),
73
163
  });
164
+ ```
165
+
166
+ The engine reasons about two roles:
74
167
 
75
- // result.nodes positioned nodes with absolute handle coordinates
76
- // result.edges → routed edges with waypoint arrays
168
+ - **Rail nodes** are aligned to the global flow: in `TB`, inputs go on top,
169
+ outputs on bottom.
170
+ - **Value nodes** are sidecars: their handles must face the consumer. A value
171
+ attached to a consumer's `left` handle will sit on the consumer's left
172
+ flank, so its output is expected on `right`.
173
+
174
+ Each proposal includes the original node, the rotated proposal, the rotation
175
+ angle (`90` / `-90` / `180`), and a human-readable reason.
176
+
177
+ ## Component packing
178
+
179
+ By default disjoint components are packed side-by-side along the order axis.
180
+ Disable with `packComponents: false`. Packing respects compound groups — a
181
+ compound and its children always travel as one block.
182
+
183
+ ```ts
184
+ layout(input, { packComponents: true }); // default
185
+ layout(input, { packComponents: false }); // long ribbon
77
186
  ```
78
187
 
79
188
  ## API
80
189
 
81
190
  ### `layout(input, options?)`
82
191
 
83
- Compute a complete layout for the given graph. Returns positioned nodes and routed edges.
192
+ Compute a complete layout for the given graph. Returns positioned nodes and
193
+ routed edges.
84
194
 
85
- ```typescript
195
+ ```ts
86
196
  function layout(input: LayoutInput, options?: LayoutOptions): LayoutResult;
87
197
  ```
88
198
 
89
199
  ### `IncrementalLayout`
90
200
 
91
- Maintains layout state for incremental updates. Use this when you need to add or remove nodes without full recomputation.
201
+ Maintains state for incremental updates with position stability:
92
202
 
93
- ```typescript
94
- import { IncrementalLayout } from 'nodius-layouting';
203
+ ```ts
204
+ import { IncrementalLayout } from '@nodius/layouting';
95
205
 
96
206
  const inc = new IncrementalLayout({ direction: 'LR' });
207
+ const r1 = inc.setGraph({ nodes: [...], edges: [...] });
208
+ const r2 = inc.addNodes([newNode], [newEdge]);
209
+ const r3 = inc.removeNodes(['stale_id']);
210
+ inc.addEdges([...]);
211
+ inc.removeEdges(['e1']);
212
+ inc.getResult();
213
+ ```
97
214
 
98
- // Set initial graph
99
- const result1 = inc.setGraph({ nodes: [...], edges: [...] });
215
+ ### `printLayout(result, options?)`
100
216
 
101
- // Add nodes and edges incrementally
102
- const result2 = inc.addNodes(
103
- [{ id: 'new_node', width: 100, height: 60, handles: [...] }],
104
- [{ id: 'new_edge', from: 'existing', to: 'new_node', fromHandle: 'out', toHandle: 'in' }]
105
- );
217
+ Render a layout result to a debug-friendly text block: per-Y-band summary,
218
+ node hierarchy, edges, overlaps and an optional ASCII grid. Useful when
219
+ iterating on layout strategies without opening a browser.
106
220
 
107
- // Remove nodes (connected edges are removed automatically)
108
- const result3 = inc.removeNodes(['node_to_remove']);
221
+ ```ts
222
+ import { layout, printLayout } from '@nodius/layouting';
223
+ const r = layout(input);
224
+ console.log(printLayout(r));
225
+ ```
109
226
 
110
- // Add/remove edges independently
111
- inc.addEdges([...]);
112
- inc.removeEdges(['edge_id']);
227
+ ### `rotateHandles(handles, rotation)`
113
228
 
114
- // Get current result
115
- const current = inc.getResult();
229
+ Utility re-exported for applications that build their own handle-rotation
230
+ logic in their `onProposal` callback.
231
+
232
+ ```ts
233
+ import { rotateHandles } from '@nodius/layouting';
234
+ const rotated = rotateHandles(node.handles, 90);
116
235
  ```
117
236
 
118
- ### Types
237
+ ## Types
119
238
 
120
- #### Input Types
239
+ ### Input
121
240
 
122
- ```typescript
241
+ ```ts
123
242
  interface NodeInput {
124
243
  id: string;
125
244
  width: number;
126
245
  height: number;
127
246
  handles: HandleInput[];
247
+ parentId?: string; // make this node a child of another
128
248
  }
129
249
 
130
250
  interface HandleInput {
131
251
  id: string;
132
252
  type: 'input' | 'output';
133
253
  position: 'top' | 'right' | 'bottom' | 'left';
134
- offset?: number; // 0-1 position along the side. Default: 0.5
254
+ offset?: number; // 0..1 along the side. Default: 0.5
135
255
  }
136
256
 
137
257
  interface EdgeInput {
138
258
  id: string;
139
- from: string; // Source node ID
140
- to: string; // Target node ID
141
- fromHandle: string; // Source handle ID
142
- toHandle: string; // Target handle ID
259
+ from: string;
260
+ to: string;
261
+ fromHandle: string;
262
+ toHandle: string;
263
+ kind?: 'control' | 'data'; // default: 'control'
264
+ weight?: number; // override (default: 1 control, 0.25 data)
143
265
  }
144
266
 
145
267
  interface LayoutInput {
@@ -148,22 +270,34 @@ interface LayoutInput {
148
270
  }
149
271
  ```
150
272
 
151
- #### Options
273
+ ### Options
152
274
 
153
- ```typescript
275
+ ```ts
154
276
  interface LayoutOptions {
155
- direction?: 'TB' | 'LR' | 'BT' | 'RL'; // Default: 'TB'
156
- nodeSpacing?: number; // Default: 40
157
- layerSpacing?: number; // Default: 60
158
- crossingMinimizationIterations?: number; // Default: 24
159
- coordinateOptimizationIterations?: number; // Default: 8
160
- edgeMargin?: number; // Default: 20
277
+ direction?: 'TB' | 'LR' | 'BT' | 'RL'; // Default: 'TB'
278
+ nodeSpacing?: number; // Default: 40
279
+ layerSpacing?: number; // Default: 60
280
+ crossingMinimizationIterations?: number; // Default: 24
281
+ coordinateOptimizationIterations?: number;// Default: 8
282
+ edgeMargin?: number; // Default: 20
283
+
284
+ // Typed edges
285
+ edgeWeights?: { control?: number; data?: number };
286
+
287
+ // Component packing
288
+ packComponents?: boolean; // Default: true
289
+
290
+ // Compound layout
291
+ compoundPadding?: number; // Default: 24
292
+
293
+ // Proposals
294
+ onProposal?: (p: LayoutProposal) => NodeInput | null | undefined | void;
161
295
  }
162
296
  ```
163
297
 
164
- #### Output Types
298
+ ### Output
165
299
 
166
- ```typescript
300
+ ```ts
167
301
  interface LayoutResult {
168
302
  nodes: NodeOutput[];
169
303
  edges: EdgeOutput[];
@@ -175,15 +309,16 @@ interface NodeOutput {
175
309
  y: number;
176
310
  width: number;
177
311
  height: number;
178
- handles: HandleOutput[]; // Absolute positions
312
+ handles: HandleOutput[]; // absolute positions
313
+ parentId?: string; // echoed from the input
179
314
  }
180
315
 
181
316
  interface HandleOutput {
182
317
  id: string;
183
318
  type: 'input' | 'output';
184
319
  position: 'top' | 'right' | 'bottom' | 'left';
185
- x: number; // Absolute x position
186
- y: number; // Absolute y position
320
+ x: number;
321
+ y: number;
187
322
  }
188
323
 
189
324
  interface EdgeOutput {
@@ -192,60 +327,79 @@ interface EdgeOutput {
192
327
  to: string;
193
328
  fromHandle: string;
194
329
  toHandle: string;
195
- points: Point[]; // Ordered waypoints from source to target
330
+ points: Point[]; // ordered waypoints
331
+ kind: 'control' | 'data';
196
332
  }
197
- ```
198
333
 
199
- ## Algorithm
200
-
201
- The layout engine uses a modified **Sugiyama algorithm** with five phases:
202
-
203
- 1. **Cycle Breaking** - DFS-based back edge detection and reversal
204
- 2. **Layer Assignment** - Longest-path layering with dummy node insertion for long edges
205
- 3. **Crossing Minimization** - Barycenter heuristic with up/down sweeps and transpose improvement. Uses merge-sort based O(E log V) inversion counting
206
- 4. **Coordinate Assignment** - Median-based iterative positioning with spacing constraints and multi-pass centering
207
- 5. **Edge Routing** - Orthogonal path computation through dummy node waypoints with handle-aware entry/exit directions
334
+ interface RotateProposal {
335
+ type: 'rotate';
336
+ nodeId: string;
337
+ current: NodeInput;
338
+ proposed: NodeInput;
339
+ rotation: 90 | -90 | 180;
340
+ reason: string;
341
+ }
208
342
 
209
- ### Incremental Layout
343
+ type LayoutProposal = RotateProposal;
344
+ ```
210
345
 
211
- The `IncrementalLayout` class provides position stability when modifying graphs:
346
+ ## Algorithm
212
347
 
213
- - New nodes are inserted into appropriate layers based on their connections
214
- - Existing node positions are blended (70% new / 30% old) to reduce visual disruption
215
- - Full crossing minimization and coordinate assignment run on the updated graph
216
- - Only affected edges are re-routed
348
+ The engine uses a modified **Sugiyama algorithm** with these phases:
349
+
350
+ 1. **Proposals** scan input nodes; emit rotation proposals when handle
351
+ orientation doesn't match the planned placement (rail vs. sidecar).
352
+ 2. **Compound resolution** — group nodes by `parentId`; recursively lay out
353
+ each compound's children bottom-up so the compound's bounding box is
354
+ known before it appears in the parent level.
355
+ 3. **Cycle breaking** — DFS-based back edge detection and reversal.
356
+ 4. **Two-pass layer assignment**:
357
+ - control rail via longest-path on control-only edges between non-value nodes
358
+ - values pulled onto the median layer of their non-value neighbors
359
+ 5. **Dummy insertion** for long control edges only — data edges route directly.
360
+ 6. **Crossing minimization on the rail** — barycenter heuristic with up/down
361
+ sweeps and transpose improvement; merge-sort inversion counting.
362
+ 7. **Coordinate assignment on the rail** — median-based iterative positioning
363
+ with spacing constraints.
364
+ 8. **Sidecar value placement** — values are attached to the flank of their
365
+ dominant consumer, side chosen from the consumer's handle position.
366
+ 9. **Edge routing** — orthogonal paths through dummy waypoints with
367
+ handle-aware entry/exit directions; `kind` is preserved.
368
+ 10. **Component packing** — disjoint components packed along the order axis;
369
+ compound groups travel as a single block.
217
370
 
218
371
  ## Playground
219
372
 
220
- A visual playground is included in the `playground/` directory:
221
-
222
373
  ```bash
223
374
  cd playground
224
375
  npm install
225
- npm run dev
376
+ npm run dev # → http://localhost:6501
226
377
  ```
227
378
 
228
- This opens a Vite dev server with:
229
- - Predefined example graphs (chain, diamond, data pipeline, hub, binary tree, cycle)
230
- - Editable JSON input
231
- - Before/after SVG visualization
232
- - Configurable layout direction and spacing
233
- - Real-time performance metrics
379
+ The playground ships with examples covering every layout feature:
380
+
381
+ - Compound · Promise.all
382
+ - Floating Values (auto-rotate demo)
383
+ - Compound + Values
384
+ - Try / Catch (compound)
385
+ - Switch / Case
386
+ - Map / Reduce pipeline
387
+ - HTTP middleware chain
388
+ - Disjoint Components
389
+ - Binary Tree
390
+ - Cycle Example
391
+ - ... and classics (Simple Chain, Diamond, Data Pipeline, Multi-Handle Hub)
392
+
393
+ Toggle **Pack components** and **Auto-rotate handles** in the toolbar to see
394
+ the corresponding features kick in.
234
395
 
235
396
  ## Development
236
397
 
237
398
  ```bash
238
- # Install dependencies
239
399
  npm install
240
-
241
- # Run tests
242
- npm test
243
-
244
- # Run tests in watch mode
400
+ npm test # 79 tests cover compound, sidecars, proposals, perf, cycles…
245
401
  npm run test:watch
246
-
247
- # Build the library
248
- npm run build
402
+ npm run build # tsup bundle + tsc declaration files
249
403
  ```
250
404
 
251
405
  ## License
@@ -0,0 +1,9 @@
1
+ import { EdgeOutput, NodeOutput, ResolvedOptions } from '../types';
2
+ /**
3
+ * Pack disjoint connected components side-by-side (in the order axis) so the
4
+ * overall layout is roughly square instead of a long ribbon.
5
+ *
6
+ * Compound parents are kept as atomic groups: children move with their parent.
7
+ */
8
+ export declare function packComponents(nodes: NodeOutput[], edges: EdgeOutput[], options: ResolvedOptions): void;
9
+ //# sourceMappingURL=component-packing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component-packing.d.ts","sourceRoot":"","sources":["../../src/algorithms/component-packing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEnE;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,EAAE,eAAe,GACvB,IAAI,CAqJN"}
@@ -0,0 +1,7 @@
1
+ import { Graph } from '../graph';
2
+ import { ResolvedOptions } from '../types';
3
+ /**
4
+ * Assign x,y coordinates to all nodes using a median-based iterative approach.
5
+ */
6
+ export declare function assignCoordinates(graph: Graph, layers: string[][], options: ResolvedOptions): void;
7
+ //# sourceMappingURL=coordinate-assignment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coordinate-assignment.d.ts","sourceRoot":"","sources":["../../src/algorithms/coordinate-assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACjC,OAAO,EAAmB,eAAe,EAAE,MAAM,UAAU,CAAC;AAE5D;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAAE,EAAE,EAClB,OAAO,EAAE,eAAe,GACvB,IAAI,CAON"}
@@ -0,0 +1,7 @@
1
+ import { Graph } from '../graph';
2
+ /**
3
+ * Minimize edge crossings using barycenter heuristic with transpose improvement.
4
+ */
5
+ export declare function minimizeCrossings(graph: Graph, layers: string[][], iterations: number): string[][];
6
+ export declare function countAllCrossings(graph: Graph, layers: string[][]): number;
7
+ //# sourceMappingURL=crossing-minimization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossing-minimization.d.ts","sourceRoot":"","sources":["../../src/algorithms/crossing-minimization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EAAE,EAAE,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,EAAE,CAqEZ;AAgMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAM1E"}
@@ -0,0 +1,8 @@
1
+ import { Graph } from '../graph';
2
+ /**
3
+ * Break cycles in the graph using DFS-based back edge detection.
4
+ * Reverses back edges to make the graph a DAG.
5
+ * Returns the set of reversed edge IDs.
6
+ */
7
+ export declare function breakCycles(graph: Graph): Set<string>;
8
+ //# sourceMappingURL=cycle-breaking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cycle-breaking.d.ts","sourceRoot":"","sources":["../../src/algorithms/cycle-breaking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CA4DrD"}
@@ -0,0 +1,17 @@
1
+ import { Graph } from '../graph';
2
+ import { Point, LayoutDirection, EdgeKind } from '../types';
3
+ export interface RoutedEdge {
4
+ id: string;
5
+ from: string;
6
+ to: string;
7
+ fromHandle: string;
8
+ toHandle: string;
9
+ points: Point[];
10
+ kind: EdgeKind;
11
+ }
12
+ /**
13
+ * Route all edges with orthogonal paths.
14
+ * Reconstructs original edges from dummy node chains.
15
+ */
16
+ export declare function routeEdges(graph: Graph, direction: LayoutDirection, edgeMargin: number): RoutedEdge[];
17
+ //# sourceMappingURL=edge-routing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-routing.d.ts","sourceRoot":"","sources":["../../src/algorithms/edge-routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAyC,MAAM,UAAU,CAAC;AACxE,OAAO,EAAE,KAAK,EAAc,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAExE,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,eAAe,EAC1B,UAAU,EAAE,MAAM,GACjB,UAAU,EAAE,CASd"}
@@ -0,0 +1,20 @@
1
+ import { Graph } from '../graph';
2
+ /**
3
+ * Two-pass layer assignment:
4
+ *
5
+ * Pass A (control rail): longest-path on the subgraph induced by control
6
+ * edges between non-value nodes. This defines the canonical execution
7
+ * timeline — value nodes do not extend it.
8
+ *
9
+ * Pass B (value pull): each value node is assigned the median layer of its
10
+ * non-value neighbors. This snaps floating constants/imports onto the same
11
+ * layer as the consumer that needs them, where they can be packed laterally.
12
+ */
13
+ export declare function assignLayers(graph: Graph): string[][];
14
+ /**
15
+ * Insert dummy nodes only for control edges that span multiple layers.
16
+ * Data edges keep their layer-span; routing handles them directly because
17
+ * they're typically short and decorative.
18
+ */
19
+ export declare function insertDummyNodes(graph: Graph, layers: string[][]): string[][];
20
+ //# sourceMappingURL=layer-assignment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer-assignment.d.ts","sourceRoot":"","sources":["../../src/algorithms/layer-assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,EAAE,CA8GrD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,CA0E7E"}
@@ -0,0 +1,15 @@
1
+ import { Graph } from '../graph';
2
+ /**
3
+ * After crossing minimization places nodes onto their layers, this pass moves
4
+ * value nodes (data-only) so they sit immediately next to their dominant
5
+ * consumer/producer within the same layer.
6
+ *
7
+ * Without this pass, barycenter ordering happens to scatter values across the
8
+ * layer because their only incident edges are within the same layer (and so
9
+ * provide no cross-layer signal to the barycenter heuristic).
10
+ *
11
+ * Values are placed alternately on either side of the target so a hub
12
+ * consuming multiple values stays balanced.
13
+ */
14
+ export declare function clusterValues(graph: Graph, layers: string[][]): void;
15
+ //# sourceMappingURL=value-cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-cluster.d.ts","sourceRoot":"","sources":["../../src/algorithms/value-cluster.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,IAAI,CAuFpE"}