grid-router 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geptyro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # grid-router
2
+
3
+ Orthogonal edge routing on a grid for node-link diagrams: A\* with
4
+ per-direction cell occupancy, Steiner-style bus joins, and crossing "hops".
5
+ Framework-free core + an optional Svelte 5 canvas where **the consumer owns
6
+ the chips**.
7
+
8
+ ## Concepts
9
+
10
+ - **Grid & occupancy** — the canvas is rasterized into square cells
11
+ (`res`, default 12px). Node boxes (inflated by `BOX_INFLATE`) hard-block
12
+ cells. Each free cell can host **one horizontal and one vertical** run;
13
+ same-direction reuse by another bus is practically prohibited, perpendicular
14
+ crossings pay a small toll. Lanes separate themselves; crossings happen only
15
+ where they're worth it.
16
+ - **Buses & joins** — edges sharing `(source, bus)` form a bus. Later edges of
17
+ a bus seed their search from every cell the bus already routed (cost ~0), so
18
+ branches leave the trunk at the optimal point. `exitCost` prices opening a
19
+ *new* trunk out of a source: raise it to force merges even over shorter
20
+ direct paths.
21
+ - **Endpoints are exclusive** — two buses never share a start/goal cell, so
22
+ stubs and arrowheads never stack.
23
+ - **Nothing is dropped** — if a dense node row walls the grid off, a desperate
24
+ pass routes through obstacles at a huge cost and the result counts it in
25
+ `violations`. **`violations > 0` is a layout problem, not a router bug**:
26
+ the layout does not supply enough corridor space.
27
+ - **Corridor supply** — `corridorGaps(res)` returns the empirically-validated
28
+ minimum `rowGap` / `chipGap` / `sidePad` the *layout* must provide so that
29
+ routing stays violation-free. A chip gap must fit one aligned free cell
30
+ column (`2·res + 2·inflate`); side padding keeps the outer columns routable.
31
+ - **Hops** — crossings are disambiguated without geometry: each bus paints a
32
+ background-colored halo under its paths, cutting a small gap into every bus
33
+ drawn before it (the cut line reads as tunneling under). Per-bus grouping
34
+ keeps a bus's own T-junctions intact.
35
+ - **Cost layers** — the router is deliberately agnostic of higher-level layout
36
+ concepts (lanes, sections, keep-out zones). `opts.costRegions` lets the
37
+ consumer express them anyway: rects with a per-cell `bias` — positive repels
38
+ routes, negative attracts them (e.g. a "lanes layer" biasing its corridors
39
+ negative). The step cost is floored, so negative biases are safe.
40
+ - **Port constraints** — `opts.nodeSides` restricts which box sides a node's
41
+ edges may attach to (schematic pins, side-anchored UML). For POSITIONED
42
+ named ports, register tiny pad elements at the pin locations as their own
43
+ nodes (`mcu:pb0`) and connect edges to those — the body registers separately
44
+ as a pure obstacle (see the circuit example); small boxes are first-class.
45
+ - **Pre-built layers** — `withLayers(base, portsLayer(…), keepOutLayer(…),
46
+ corridorLayer(…))` composes declarative routing facts into opts; falsy
47
+ entries are skipped so layers toggle inline
48
+ (`withLayers(base, zoneOn && keepOutLayer([zone]))`).
49
+
50
+ ## Core API (framework-free)
51
+
52
+ ```ts
53
+ import { routeGrid, corridorGaps } from 'grid-router';
54
+
55
+ const result = routeGrid(
56
+ boxes, // Map<string, GridBox> — measured node rects (t/b/l/r/cx/cy)
57
+ edges, // { id, source, target, bus?, data? }[]
58
+ width, // routable canvas size in px
59
+ height,
60
+ { res: 12, costOwn: 0.02, costTurn: 1, exitCost: 0 } // defaults shown
61
+ );
62
+ // result.conns: { id, source, target, bus, data, d /* SVG path */ }[]
63
+ // result.violations: number (0 = clean; see corridorGaps)
64
+ // result.bottom, result.debug (occupancy cells for overlays)
65
+ ```
66
+
67
+ ## Svelte canvas
68
+
69
+ The component draws **only connectors**. You lay out arbitrary chip markup in
70
+ the default snippet and tag each connectable element with the provided
71
+ `register` action; chip events stay on your own DOM. Size the component's
72
+ container with CSS — it measures itself (no width/height props).
73
+
74
+ ```svelte
75
+ <script lang="ts">
76
+ import { GridDiagram, type GridConn } from 'grid-router/svelte';
77
+
78
+ let hovered = $state<string | undefined>();
79
+ const edges = [{ id: 'e1', source: 'a', target: 'b', bus: 'ok', data: { tone: 'ok' } }];
80
+ </script>
81
+
82
+ <GridDiagram
83
+ {edges}
84
+ opts={{ res: 12 }}
85
+ connStyle={(c) => ({
86
+ color: (c.data as { tone?: string })?.tone === 'ok' ? '#3fb950' : '#8b94a1',
87
+ dashed: false,
88
+ arrowAt: 'end',
89
+ class: hovered && hovered !== c.id ? 'dim' : ''
90
+ })}
91
+ onconnenter={(c) => (hovered = c.id)}
92
+ onconnleave={() => (hovered = undefined)}
93
+ onrouted={({ violations, ms }) => console.log(violations, ms)}
94
+ >
95
+ {#snippet children(register)}
96
+ <div class="my-layout">
97
+ <div use:register={'a'}>chip A</div>
98
+ <div use:register={'b'}>chip B</div>
99
+ </div>
100
+ {/snippet}
101
+ </GridDiagram>
102
+
103
+ <style>
104
+ /* consumer-driven highlight: connStyle classes land on package paths */
105
+ :global(path.dim) { opacity: 0.15; }
106
+ </style>
107
+ ```
108
+
109
+ Layout contract: use the published CSS vars `--gr-row-gap` / `--gr-chip-gap`
110
+ (from `corridorGaps(res)`) as your row/chip gaps, and set `--grid-diagram-bg`
111
+ to your panel background so crossing hops cut with the right color.
112
+
113
+ ## Examples
114
+
115
+ ```sh
116
+ cd examples && npm install && npm run dev # http://localhost:5200
117
+ ```
118
+
119
+ - **Electronic circuit** — nets as buses (VCC/GND rails share trunks), no
120
+ arrowheads (`arrowAt: 'none'`), hover a wire to trace its whole net.
121
+ - **Org chart** — one bus per manager, reporting lines merge; hover a line to
122
+ light the whole team.
123
+ - **Pipeline + cost layers** — absolutely-positioned left→right DAG; toggle a
124
+ maintenance keep-out zone (`costRegions`) and watch routes detour, or hit
125
+ *randomize layout* — chips scatter and the router re-routes live (the
126
+ `revision` prop triggers re-measure when chips move without resizing the
127
+ canvas).
128
+
129
+ ## Development
130
+
131
+ ```sh
132
+ npm install
133
+ npm run build # tsc → dist (core)
134
+ npm test # vitest
135
+ ```
136
+
137
+ The Svelte entry ships as source (`src/svelte`) and is compiled by the
138
+ consumer's bundler (Vite + svelte plugin resolve the `svelte` export
139
+ condition).
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Corridor supply rules — the layout side of the routing contract.
3
+ *
4
+ * The router can only use free cells; the LAYOUT decides how many exist. These
5
+ * formulas are the empirically-validated minimums (violations = 0 across cell
6
+ * sizes 6–12 on real diagrams):
7
+ *
8
+ * - rowGap: a gap between node rows must hold several free horizontal lanes
9
+ * after box inflation eats its borders;
10
+ * - chipGap: a gap between nodes in a row must fit at least one ALIGNED free
11
+ * cell column (2·res + 2·inflate), or a dense row walls the grid off;
12
+ * - sidePad: canvas side padding keeps the outer columns routable so paths
13
+ * can go AROUND a row instead of only through it.
14
+ */
15
+ export interface CorridorGaps {
16
+ rowGap: number;
17
+ chipGap: number;
18
+ sidePad: number;
19
+ }
20
+ export declare function corridorGaps(res: number, inflate?: number): CorridorGaps;
@@ -0,0 +1,8 @@
1
+ import { BOX_INFLATE } from './router.js';
2
+ export function corridorGaps(res, inflate = BOX_INFLATE) {
3
+ return {
4
+ rowGap: Math.max(34, 4.5 * res),
5
+ chipGap: Math.max(12, 2 * res + 2 * inflate),
6
+ sidePad: 1.5 * res
7
+ };
8
+ }
@@ -0,0 +1,3 @@
1
+ export { routeGrid, BOX_INFLATE, type GridBox, type GridEdge, type GridConn, type GridOpts, type GridResult, type GridDebugCell, type BoxSide } from './router.js';
2
+ export { corridorGaps, type CorridorGaps } from './corridors.js';
3
+ export { withLayers, portsLayer, keepOutLayer, corridorLayer, type Rect, type RouterLayer } from './layers.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { routeGrid, BOX_INFLATE } from './router.js';
2
+ export { corridorGaps } from './corridors.js';
3
+ export { withLayers, portsLayer, keepOutLayer, corridorLayer } from './layers.js';
@@ -0,0 +1,39 @@
1
+ import type { BoxSide, GridOpts } from './router.js';
2
+ /**
3
+ * Layers: pre-built, composable producers of declarative routing facts.
4
+ *
5
+ * The router core stays agnostic — it only understands two substrates:
6
+ * per-cell cost bias (`costRegions`) and per-node port constraints
7
+ * (`nodeSides`). A layer is a small factory that expresses a CONSUMER concept
8
+ * (keep-out zone, preferred corridor, schematic pins…) in those substrates;
9
+ * `withLayers` merges any number of them into the opts.
10
+ *
11
+ * const opts = withLayers(
12
+ * { res: 12 },
13
+ * portsLayer({ r1: ['left', 'right'] }),
14
+ * maintenance && keepOutLayer([zoneRect])
15
+ * );
16
+ */
17
+ export interface Rect {
18
+ l: number;
19
+ t: number;
20
+ r: number;
21
+ b: number;
22
+ }
23
+ export interface RouterLayer {
24
+ costRegions?: NonNullable<GridOpts['costRegions']>;
25
+ nodeSides?: Record<string, BoxSide[]>;
26
+ }
27
+ /** Merge layers into base opts. Falsy entries are skipped, so layers can be
28
+ * toggled inline: `withLayers(base, zoneOn && keepOutLayer([zone]))`. */
29
+ export declare function withLayers(base: GridOpts, ...layers: (RouterLayer | false | null | undefined)[]): GridOpts;
30
+ /** Port constraints: which box sides each node's edges may attach to
31
+ * (schematic pins, UML side-anchoring, …). */
32
+ export declare function portsLayer(sides: Record<string, BoxSide[]>): RouterLayer;
33
+ /** Keep-out zones: routes strongly avoid these rects (they still cross when
34
+ * there is no alternative — routing never fails). */
35
+ export declare function keepOutLayer(rects: Rect[], bias?: number): RouterLayer;
36
+ /** Preferred corridors: routes are gently attracted into these rects — the
37
+ * "lanes layer": express your layout's gaps as corridors and buses gravitate
38
+ * there. Negative bias is floored by the router, so any value is safe. */
39
+ export declare function corridorLayer(rects: Rect[], bias?: number): RouterLayer;
package/dist/layers.js ADDED
@@ -0,0 +1,36 @@
1
+ /** Merge layers into base opts. Falsy entries are skipped, so layers can be
2
+ * toggled inline: `withLayers(base, zoneOn && keepOutLayer([zone]))`. */
3
+ export function withLayers(base, ...layers) {
4
+ const costRegions = [...(base.costRegions ?? [])];
5
+ const nodeSides = { ...(base.nodeSides ?? {}) };
6
+ for (const l of layers) {
7
+ if (!l)
8
+ continue;
9
+ if (l.costRegions)
10
+ costRegions.push(...l.costRegions);
11
+ if (l.nodeSides)
12
+ Object.assign(nodeSides, l.nodeSides);
13
+ }
14
+ const out = { ...base };
15
+ if (costRegions.length)
16
+ out.costRegions = costRegions;
17
+ if (Object.keys(nodeSides).length)
18
+ out.nodeSides = nodeSides;
19
+ return out;
20
+ }
21
+ /** Port constraints: which box sides each node's edges may attach to
22
+ * (schematic pins, UML side-anchoring, …). */
23
+ export function portsLayer(sides) {
24
+ return { nodeSides: sides };
25
+ }
26
+ /** Keep-out zones: routes strongly avoid these rects (they still cross when
27
+ * there is no alternative — routing never fails). */
28
+ export function keepOutLayer(rects, bias = 40) {
29
+ return { costRegions: rects.map((r) => ({ ...r, bias })) };
30
+ }
31
+ /** Preferred corridors: routes are gently attracted into these rects — the
32
+ * "lanes layer": express your layout's gaps as corridors and buses gravitate
33
+ * there. Negative bias is floored by the router, so any value is safe. */
34
+ export function corridorLayer(rects, bias = -0.4) {
35
+ return { costRegions: rects.map((r) => ({ ...r, bias })) };
36
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Orthogonal edge router on a grid.
3
+ *
4
+ * Rasterizes a canvas into square cells and routes every edge with A*:
5
+ *
6
+ * - node rects ("boxes", inflated) hard-block cells;
7
+ * - per-direction occupancy: a cell can host ONE horizontal and ONE vertical
8
+ * run — another bus reusing the same direction pays a near-prohibitive
9
+ * penalty (soft block, so routing never fails), while perpendicular
10
+ * crossings only pay a small toll → lanes separate themselves and crossings
11
+ * happen only where they're worth it;
12
+ * - same-bus discount + tree seeding: a bus's later edges branch from the
13
+ * OPTIMAL point of the already-routed trunk (Steiner-style joins), and
14
+ * `exitCost` prices opening a new trunk out of a source, so merges can win
15
+ * even over shorter direct paths;
16
+ * - turn penalty biases toward long straight runs;
17
+ * - endpoints are exclusive per bus (no stacked stubs/arrowheads);
18
+ * - an edge is NEVER dropped: when a node row walls the grid off, a desperate
19
+ * pass routes through obstacles at a huge cost and reports a violation.
20
+ *
21
+ * Pure geometry: the caller measures its DOM into {@link GridBox}es and
22
+ * renders the returned SVG path strings. Framework-free.
23
+ */
24
+ export interface GridBox {
25
+ t: number;
26
+ b: number;
27
+ l: number;
28
+ r: number;
29
+ cx: number;
30
+ cy: number;
31
+ }
32
+ export interface GridEdge {
33
+ id: string;
34
+ source: string;
35
+ target: string;
36
+ /** Bus discriminator: edges sharing (source, bus) merge into one trunk
37
+ * tree. Defaults to the source alone. */
38
+ bus?: string;
39
+ /** Opaque consumer payload, passed through untouched onto the conn. */
40
+ data?: unknown;
41
+ }
42
+ export interface GridConn {
43
+ id: string;
44
+ source: string;
45
+ target: string;
46
+ /** Full bus key (`source|bus`) — stable grouping handle for rendering
47
+ * (e.g. crossing-hop halos are painted per bus). */
48
+ bus: string;
49
+ data?: unknown;
50
+ /** Orthogonal SVG path: source contact → routed cells → target contact. */
51
+ d: string;
52
+ }
53
+ export interface GridDebugCell {
54
+ x: number;
55
+ y: number;
56
+ kind: 'chip' | 'h' | 'v' | 'hv' | 'bad';
57
+ }
58
+ export interface GridResult {
59
+ conns: GridConn[];
60
+ /** deepest y any path reaches (callers reserve layout height for it) */
61
+ bottom: number;
62
+ /** lane-sharing incidents (should be 0 — non-zero means the layout does
63
+ * not supply enough corridor space, see corridorGaps()) */
64
+ violations: number;
65
+ debug: {
66
+ res: number;
67
+ cols: number;
68
+ rows: number;
69
+ cells: GridDebugCell[];
70
+ };
71
+ }
72
+ /** Box inflation in px: routed runs keep this clearance off every box. */
73
+ export declare const BOX_INFLATE = 3;
74
+ export interface GridOpts {
75
+ /** grid cell size in px (default 12) */
76
+ res?: number;
77
+ /** cost per cell when riding cells the bus already owns (default 0.02) */
78
+ costOwn?: number;
79
+ /** penalty per 90° turn (default 1) */
80
+ costTurn?: number;
81
+ /** penalty for opening a NEW trunk out of a source (default 0) — raise it
82
+ * to make a bus's edges merge even when direct paths would be shorter */
83
+ exitCost?: number;
84
+ /**
85
+ * Cost layers: arbitrary rects (px) adding a per-cell bias to every step
86
+ * inside them — positive repels routes, negative attracts them. This is
87
+ * the generic hook for consumer concepts the router deliberately does not
88
+ * know about (e.g. a "lanes" layer biasing corridors negative, keep-out
89
+ * zones, preferred highways). The effective step cost is floored so
90
+ * negative biases can't break A*.
91
+ */
92
+ costRegions?: {
93
+ l: number;
94
+ t: number;
95
+ r: number;
96
+ b: number;
97
+ bias: number;
98
+ }[];
99
+ /**
100
+ * Port constraints: restrict which box sides edges may attach to, per node
101
+ * id (schematic semantics — a horizontal resistor only connects via its
102
+ * left/right leads, an IC via its pin sides). Unlisted nodes accept all
103
+ * four sides. The desperate fallback relaxes the constraint rather than
104
+ * dropping the edge.
105
+ */
106
+ nodeSides?: Record<string, BoxSide[]>;
107
+ }
108
+ export type BoxSide = 'top' | 'bottom' | 'left' | 'right';
109
+ export declare function routeGrid(boxes: Map<string, GridBox>, edges: GridEdge[], width: number, height: number, opts?: GridOpts): GridResult;