@mark1russell7/highways 0.9.2 → 0.9.3

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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "homepage": "https://mark1russell7.github.io/highways/",
12
- "version": "0.9.2",
12
+ "version": "0.9.3",
13
13
  "type": "module",
14
14
  "main": "./build/index.js",
15
15
  "types": "./build/index.d.ts",
package/src/index.ts CHANGED
@@ -18,6 +18,8 @@ export * from "./room/index.ts";
18
18
  export * from "./room/lanes.ts";
19
19
  export * from "./room/levels/index.ts";
20
20
  export * from "./plugs/facing/index.ts";
21
+ export * from "./plugs/spread/index.ts";
22
+ export * from "./plugs/fix.ts";
21
23
  export * from "./levels/depths/index.ts";
22
24
  export * from "./whole/pipeline/index.ts";
23
25
  export * from "./bench/properties.ts";
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Fixed ports from a plug rule (FIX-PORTS): the plugs of the rule become the fixed `at` of the ports.
3
+ * Then each router takes each plug rule, because each router keeps a fixed plug at its point.
4
+ */
5
+ import { nearestSide, SIDE_NORMAL, type Plug } from "../plugs.ts";
6
+ import { nodeIndex, portOffset, resolvePorts, splitFrames, type Edge, type End, type Graph, type Node, type Port, type Side } from "../model.ts";
7
+ import { STRATEGIES } from "../strategies/builtins.ts";
8
+ import { getStrategy, type Registry, type Strategy } from "../strategies/model.ts";
9
+ import { WayFinder, type WayOptions } from "../ways.ts";
10
+
11
+ /** The options of `fixPorts`. */
12
+ export interface FixPortsOptions {
13
+ /** The options of the plug rule. The rule gets its `defaults` for each option that this object does not give. */
14
+ readonly plugOptions?: object;
15
+ /** The registry that gives the rule by its name. The default is `STRATEGIES`. */
16
+ readonly registry?: Registry;
17
+ /** The options of the way finder that a rule reads, for example `side`. The defaults are the defaults of the router `corners`. */
18
+ readonly ways?: WayOptions;
19
+ /**
20
+ * With `true`, an end without a port gets a new fixed port, with the id `<edge id>:source` or `<edge id>:target`.
21
+ * The default is `false`: such an end stays free.
22
+ */
23
+ readonly newPorts?: boolean;
24
+ }
25
+
26
+ /** This function gives the side of a plug from its normal. */
27
+ function sideOf(plug: Plug, node: Node): Side {
28
+ for (const [side, n] of Object.entries(SIDE_NORMAL) as [Side, { x: number; y: number }][]) if (n.x === plug.normal.x && n.y === plug.normal.y) return side;
29
+ return nearestSide(node.box, plug.point);
30
+ }
31
+
32
+ /** This function gives an id that no port of the node has, from `base`. */
33
+ function freeId(taken: Set<string>, base: string): string {
34
+ let id = base;
35
+ for (let i = 2; taken.has(id); i++) id = `${base}#${i}`;
36
+ taken.add(id);
37
+ return id;
38
+ }
39
+
40
+ /**
41
+ * This function runs the plug rule. It gives each free port a fixed `at` and a `side` from the plugs of the rule.
42
+ * A free port has no `at` and no `along`. A port of several ends takes the plug of its first end, in the order of the edges.
43
+ * A fixed port keeps its values, and a port of no edge stays free.
44
+ *
45
+ * The rule sees the graph of `corners`: the plugs of `resolvePorts`, without the inner edges of the frames.
46
+ * The rule is a strategy of the step `plugs`, or the name of one in the registry.
47
+ * The output is a new graph. The function does not change the input.
48
+ */
49
+ export function fixPorts(graph: Graph, rule: Strategy<"plugs"> | string, options: FixPortsOptions = {}): Graph {
50
+ const strategy = typeof rule === "string" ? getStrategy(options.registry ?? STRATEGIES, "plugs", rule) : rule;
51
+ const outer = splitFrames(resolvePorts(graph)).graph;
52
+ const pairs = strategy.run({ graph: outer, ways: new WayFinder(outer.nodes, options.ways) }, options.plugOptions);
53
+ const byEdge = new Map<string, (typeof pairs)[number][]>();
54
+ for (const p of pairs) {
55
+ const list = byEdge.get(p.edge);
56
+ if (list) list.push(p);
57
+ else byEdge.set(p.edge, [p]);
58
+ }
59
+ const byId = nodeIndex(graph);
60
+ // The new fixed ports of each node, by the port id, and the new ports for the ends without a port.
61
+ const fixed = new Map<string, Map<string, Port>>();
62
+ const added = new Map<string, Port[]>();
63
+ const taken = new Map<string, Set<string>>();
64
+ const portsOf = (node: Node): Map<string, Port> => {
65
+ let m = fixed.get(node.id);
66
+ if (!m) fixed.set(node.id, (m = new Map()));
67
+ return m;
68
+ };
69
+ const fix = (node: Node, plug: Plug, base: Port): Port => ({
70
+ ...base,
71
+ side: sideOf(plug, node),
72
+ at: { x: plug.point.x - node.box.x, y: plug.point.y - node.box.y },
73
+ });
74
+ // The outer graph has the edge objects of the input. An inner edge keeps its ends, because the rule did not see it.
75
+ const outerEdges = new Set(outer.edges);
76
+ const edges = graph.edges.map((e): Edge => {
77
+ if (!outerEdges.has(e)) return e;
78
+ const pair = byEdge.get(e.id)?.shift();
79
+ if (!pair) return e;
80
+ const end = (which: "source" | "target", plug: Plug): End => {
81
+ const own = e[which];
82
+ const node = byId.get(own.node);
83
+ if (!node) return own;
84
+ if (own.port === undefined) {
85
+ if (!options.newPorts) return own;
86
+ let ids = taken.get(node.id);
87
+ if (!ids) taken.set(node.id, (ids = new Set((node.ports ?? []).map((p) => p.id))));
88
+ const id = freeId(ids, `${e.id}:${which}`);
89
+ const list = added.get(node.id) ?? [];
90
+ list.push(fix(node, plug, { id }));
91
+ added.set(node.id, list);
92
+ return { ...own, port: id };
93
+ }
94
+ const port = node.ports?.find((p) => p.id === own.port);
95
+ if (!port || portOffset(node.box, port) !== undefined) return own;
96
+ const m = portsOf(node);
97
+ if (!m.has(port.id)) m.set(port.id, fix(node, plug, port));
98
+ return own;
99
+ };
100
+ const source = end("source", pair.source);
101
+ const target = end("target", pair.target);
102
+ return source === e.source && target === e.target ? e : { ...e, source, target };
103
+ });
104
+ const nodes = graph.nodes.map((n): Node => {
105
+ const m = fixed.get(n.id);
106
+ const more = added.get(n.id);
107
+ if (!m && !more) return n;
108
+ const ports = (n.ports ?? []).map((p) => m?.get(p.id) ?? p);
109
+ return { ...n, ports: more ? [...ports, ...more] : ports };
110
+ });
111
+ return { ...graph, nodes, edges };
112
+ }
@@ -3,6 +3,11 @@
3
3
  * They move the least distance from their ideal places.
4
4
  */
5
5
 
6
+ import { center, type Point } from "../geom.ts";
7
+ import { fixedPortPoint, nodeIndex, type Edge, type Node } from "../model.ts";
8
+ import { placePlug, type Plug } from "../plugs.ts";
9
+ import type { PlugInput, PlugPair } from "../strategies/model.ts";
10
+
6
11
  /**
7
12
  * This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
8
13
  * The points keep the order of their ideal places. Two neighbours are at least `gap` apart, or the side divided evenly when it is too short.
@@ -46,3 +51,64 @@ export function isotonic(v: readonly number[]): number[] {
46
51
  });
47
52
  return out;
48
53
  }
54
+
55
+ /**
56
+ * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
57
+ * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
58
+ * A plug stays `inset` from the corners of its side, or a quarter of the side when the side is short.
59
+ */
60
+ export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number, inset = 6): void {
61
+ // The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target) and the point of the other end.
62
+ const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
63
+ ends.forEach(([e, s, t, ps, pt], i) => {
64
+ for (const [end, node, plug, other, k] of [
65
+ [e.source, s, ps, pt, 3],
66
+ [e.target, t, pt, ps, 4],
67
+ ] as const) {
68
+ if (fixedPortPoint(node, end) !== undefined) continue;
69
+ const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
70
+ let side = sides.get(key);
71
+ if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
72
+ side.items.push([i, k, other.point]);
73
+ }
74
+ });
75
+ for (const { node, normal, items } of sides.values()) {
76
+ if (items.length < 2) continue;
77
+ const along = normal.x !== 0 ? "y" : "x";
78
+ const b = node.box;
79
+ const start = along === "y" ? b.y : b.x;
80
+ const len = along === "y" ? b.h : b.w;
81
+ const m = Math.min(inset, len / 4);
82
+ const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
83
+ const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
84
+ items.forEach(([i, k], j) => {
85
+ const old = ends[i]![k];
86
+ ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
87
+ });
88
+ }
89
+ }
90
+
91
+ /** The options of the plug strategy `justify`. */
92
+ export interface JustifyOptions {
93
+ /** The least gap between two free plugs of one side. The default is 8. 0 keeps each plug where it faces. */
94
+ readonly plugGap?: number;
95
+ }
96
+
97
+ /**
98
+ * This function gives the plugs of the plug strategy `justify` (WILD-002): the plugs of `facing`, then the gaps of `spacePlugs`.
99
+ * The router `corners` with the option `plugGap` gives the same plugs.
100
+ */
101
+ export function justifyEnds(input: PlugInput, options: JustifyOptions = {}): PlugPair[] {
102
+ const { graph } = input;
103
+ const byId = nodeIndex(graph);
104
+ const ends: [Edge, Node, Node, Plug, Plug][] = [];
105
+ for (const e of graph.edges) {
106
+ const s = byId.get(e.source.node);
107
+ const t = byId.get(e.target.node);
108
+ if (!s || !t) continue;
109
+ ends.push([e, s, t, placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))]);
110
+ }
111
+ const gap = options.plugGap ?? 8;
112
+ if (gap > 0) spacePlugs(ends, gap);
113
+ return ends.map(([e, , , source, target]) => ({ edge: e.id, source, target }));
114
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A made-up board of cards with rows (PLUG-SPREAD, FIX-PORTS): each row of a card links to one row of another card.
3
+ * The tests of the plug rules read it.
4
+ */
5
+ import type { Edge, Graph, Node, Port } from "../model.ts";
6
+
7
+ /** The options of `rowCardsBoard`. */
8
+ export interface RowCardsOptions {
9
+ /** The number of columns of cards. The default is 3. */
10
+ readonly columns?: number;
11
+ /** The number of cards. The default is 6. */
12
+ readonly cards?: number;
13
+ /** The number of rows of each card. The default is 20. */
14
+ readonly rows?: number;
15
+ /** The height of one row. The default is 12. */
16
+ readonly rowHeight?: number;
17
+ }
18
+
19
+ /**
20
+ * This function gives a board of cards with rows. Each row has a free port `o<row>` for its link out and a free port `i<row>` for a link in.
21
+ * The row `r` of the card `c` links to the card `(c + 1 + r mod 5) mod cards`. Each card takes its links in on its rows in order.
22
+ * With the defaults, the board has 6 cards and 120 edges, and each card is 160 by 240.
23
+ */
24
+ export function rowCardsBoard(options: RowCardsOptions = {}): Graph {
25
+ const columns = options.columns ?? 3;
26
+ const count = options.cards ?? 6;
27
+ const rows = options.rows ?? 20;
28
+ const rowHeight = options.rowHeight ?? 12;
29
+ const w = 160;
30
+ const h = rows * rowHeight;
31
+ const nodes: Node[] = [];
32
+ for (let c = 0; c < count; c++) {
33
+ const ports: Port[] = [];
34
+ for (let r = 0; r < rows; r++) ports.push({ id: `o${r}` }, { id: `i${r}` });
35
+ nodes.push({ id: `card${c}`, box: { x: (c % columns) * (w + 160), y: Math.floor(c / columns) * (h + 160), w, h }, ports });
36
+ }
37
+ const taken = new Array<number>(count).fill(0);
38
+ const edges: Edge[] = [];
39
+ for (let c = 0; c < count; c++)
40
+ for (let r = 0; r < rows; r++) {
41
+ const t = (c + 1 + (r % Math.min(5, count - 1))) % count;
42
+ const row = taken[t]!++ % rows;
43
+ edges.push({ id: `card${c}.r${r}`, source: { node: `card${c}`, port: `o${r}` }, target: { node: `card${t}`, port: `i${row}` } });
44
+ }
45
+ return { nodes, edges };
46
+ }
package/src/plugs/side.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * Thus the first leg goes straight out to the first corner of the way, and the plug is the foot of that leg.
4
4
  */
5
5
  import { center, dist, type Box, type Point } from "../geom.ts";
6
- import { fixedPortPoint, type End, type Node, type Side } from "../model.ts";
6
+ import { fixedPortPoint, nodeIndex, type End, type Node, type Side } from "../model.ts";
7
7
  import { placePlug, pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "../plugs.ts";
8
+ import type { PlugInput, PlugPair } from "../strategies/model.ts";
8
9
  import type { Terminal } from "../visibility.ts";
9
- import type { WayFinder } from "../ways.ts";
10
+ import { WayFinder } from "../ways.ts";
10
11
 
11
12
  /** The options of `sidePlugs`. */
12
13
  export interface SidePlugOptions {
@@ -136,3 +137,23 @@ export function sidePlugs(ways: WayFinder, s: Node, es: End, t: Node, et: End, o
136
137
  if (!way) return [ps, pt];
137
138
  return [from[way.from]!.plug, to[way.to]!.plug];
138
139
  }
140
+
141
+ /**
142
+ * This function gives the plugs of the plug strategy `side` (WILD-001): the plugs of `sidePlugs` for each edge.
143
+ * It uses the way finder of the input, or a way finder with the default margin when the input has none.
144
+ * The router `corners` with the option `plugs: "side"` gives the same plugs.
145
+ */
146
+ export function sideEnds(input: PlugInput, options: SidePlugOptions = {}): PlugPair[] {
147
+ const { graph } = input;
148
+ const byId = nodeIndex(graph);
149
+ const ways = input.ways ?? new WayFinder(graph.nodes);
150
+ const out: PlugPair[] = [];
151
+ for (const e of graph.edges) {
152
+ const s = byId.get(e.source.node);
153
+ const t = byId.get(e.target.node);
154
+ if (!s || !t) continue;
155
+ const [source, target] = sidePlugs(ways, s, e.source, t, e.target, options);
156
+ out.push({ edge: e.id, source, target });
157
+ }
158
+ return out;
159
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * The plug strategy `spread` (PLUG-SPREAD): the free plugs of each wall share the room of that wall with a minimum gap.
3
+ * The order on the wall keeps the order of the far ends, so two routes do not cross at the wall. No plug drops.
4
+ */
5
+ import { center, type Box, type Point } from "../../geom.ts";
6
+ import { nodeIndex, portOffset, type End, type Node, type Side } from "../../model.ts";
7
+ import { nearestSide, placePlug, SIDE_NORMAL, sideToward, type Plug } from "../../plugs.ts";
8
+ import type { PlugInput, PlugPair } from "../../strategies/model.ts";
9
+ import { isotonic } from "../justify.ts";
10
+
11
+ /** The options of the plug strategy `spread`. */
12
+ export interface SpreadOptions {
13
+ /** The least gap between two plugs of one wall. When the room of the wall is too small, the gap shrinks evenly. The default is 8. */
14
+ readonly plugGap?: number;
15
+ /** The distance from a plug to a corner of its box, or a quarter of the wall when the wall is short. The default is 6. */
16
+ readonly inset?: number;
17
+ /** The number of passes that sort each wall again by the new places of the far ends. The default is 2. */
18
+ readonly rounds?: number;
19
+ }
20
+
21
+ /** The defaults of the plug strategy `spread`. */
22
+ export const SPREAD_DEFAULTS: Required<SpreadOptions> = Object.freeze({ plugGap: 8, inset: 6, rounds: 2 });
23
+
24
+ /** A free plug: one port of a node, or one end without a port. All the ends of one port share its plug. */
25
+ interface Slot {
26
+ readonly node: Node;
27
+ readonly side: Side;
28
+ /** The place of the plug that faces the far end (`placePlug`), on the axis along the wall. */
29
+ readonly ideal: number;
30
+ /** The index of the port in the ports of its node, then the index of the slot. Thus the rows of a card keep their order in a tie. */
31
+ readonly rank: readonly [number, number];
32
+ /** The far end of the first end of the slot: a fixed plug, or the key of its slot. */
33
+ far?: Plug | string;
34
+ /** The place of the plug on the axis along the wall. */
35
+ at: number;
36
+ }
37
+
38
+ /** A wall: one side of one node, with its free slots and the places of its fixed plugs on the axis along the wall. */
39
+ interface Wall {
40
+ readonly node: Node;
41
+ readonly side: Side;
42
+ readonly slots: Slot[];
43
+ readonly fixed: number[];
44
+ }
45
+
46
+ /** This function gives the axis along a side. */
47
+ function alongOf(side: Side): "x" | "y" {
48
+ return side === "east" || side === "west" ? "y" : "x";
49
+ }
50
+
51
+ /** This function gives the room of a wall: the segment of the axis along the side, without the corner insets. */
52
+ function roomOf(box: Box, side: Side, inset: number): [number, number] {
53
+ const along = alongOf(side);
54
+ const start = along === "y" ? box.y : box.x;
55
+ const len = along === "y" ? box.h : box.w;
56
+ const m = Math.min(inset, len / 4);
57
+ return [start + m, start + len - m];
58
+ }
59
+
60
+ /** This function gives the point of a side at `v` on the axis along the side. */
61
+ function pointAt(box: Box, side: Side, v: number): Point {
62
+ if (side === "east") return { x: box.x + box.w, y: v };
63
+ if (side === "west") return { x: box.x, y: v };
64
+ if (side === "south") return { x: v, y: box.y + box.h };
65
+ return { x: v, y: box.y };
66
+ }
67
+
68
+ /**
69
+ * This function gives the sort key of a far point on a wall. The key has the order of the angle from the normal of the wall, from the center of the box.
70
+ * The key grows in the direction of the axis along the wall. Thus the plugs in the order of the keys do not cross at the wall.
71
+ */
72
+ function keyOf(box: Box, side: Side, far: Point): number {
73
+ const c = center(box);
74
+ const n = SIDE_NORMAL[side];
75
+ const dx = far.x - c.x;
76
+ const dy = far.y - c.y;
77
+ const out = dx * n.x + dy * n.y;
78
+ const along = alongOf(side) === "y" ? dy : dx;
79
+ // A pseudo-angle with the order of the angle: a sum and one division, with no transcendental call. It goes from -2 to 2.
80
+ const size = Math.abs(out) + Math.abs(along);
81
+ if (size === 0) return 0;
82
+ const p = along / size;
83
+ if (out >= 0) return p;
84
+ return along >= 0 ? 2 - p : -2 - p;
85
+ }
86
+
87
+ /**
88
+ * This function places `ideal.length` plugs in their given order on the segment from `lo` to `hi`. Two neighbours are at least `gap` apart.
89
+ * The gap shrinks evenly when the segment is too short. The sum of the squares of the moves from the ideal places is the least.
90
+ */
91
+ function placeInOrder(ideal: readonly number[], lo: number, hi: number, gap: number): number[] {
92
+ const k = ideal.length;
93
+ if (k === 0) return [];
94
+ const s = k > 1 ? Math.max(0, Math.min(gap, (hi - lo) / (k - 1))) : 0;
95
+ const y = isotonic(ideal.map((v, i) => v - i * s));
96
+ const top = Math.max(lo, hi - (k - 1) * s);
97
+ return y.map((v, i) => Math.max(lo, Math.min(top, v)) + i * s);
98
+ }
99
+
100
+ /**
101
+ * This function places the slots of one wall in their order. The fixed plugs cut the room into parts.
102
+ * Each slot goes into the part of its ideal place, and the parts keep the order of the slots. In each part, two plugs and a plug and a fixed plug are at least `gap` apart.
103
+ * The gap of a part shrinks evenly when the part is too short.
104
+ */
105
+ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number): void {
106
+ const cuts = [...new Set(fixed.filter((f) => f > lo - 1e-9 && f < hi + 1e-9))].sort((a, b) => a - b);
107
+ // The part i is from cuts[i - 1] to cuts[i]. The first part starts at lo, and the last part ends at hi.
108
+ const ends = [lo, ...cuts, hi];
109
+ const parts: Slot[][] = cuts.map(() => []).concat([[]]);
110
+ let part = 0;
111
+ for (const s of slots) {
112
+ let p = 0;
113
+ while (p < cuts.length && s.ideal > cuts[p]!) p++;
114
+ part = Math.max(part, p);
115
+ parts[part]!.push(s);
116
+ }
117
+ parts.forEach((list, p) => {
118
+ if (list.length === 0) return;
119
+ const a = ends[p]!;
120
+ const b = ends[p + 1]!;
121
+ const fa = p > 0 ? 1 : 0;
122
+ const fb = p < cuts.length ? 1 : 0;
123
+ const steps = list.length - 1 + fa + fb;
124
+ const s = steps > 0 ? Math.max(0, Math.min(gap, (b - a) / steps)) : gap;
125
+ const lower = a + fa * s;
126
+ const upper = Math.max(lower, b - fb * s);
127
+ const at = placeInOrder(
128
+ list.map((x) => x.ideal),
129
+ lower,
130
+ upper,
131
+ s,
132
+ );
133
+ list.forEach((x, i) => {
134
+ x.at = at[i]!;
135
+ });
136
+ });
137
+ }
138
+
139
+ /**
140
+ * This function gives the plugs of the plug strategy `spread`. A fixed plug stays at its point, and a plug with a side stays on that side.
141
+ * Each free plug goes on the side that faces the other end (`sideToward`). Then the free plugs of each wall share its room with at least `plugGap` between two of them.
142
+ * They keep the order of their far ends. The ends of one port share one plug.
143
+ *
144
+ * The output has one entry for each edge with two known nodes, in the order of the edges.
145
+ */
146
+ export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugPair[] {
147
+ const { graph } = input;
148
+ const gap = options.plugGap ?? SPREAD_DEFAULTS.plugGap;
149
+ const inset = options.inset ?? SPREAD_DEFAULTS.inset;
150
+ const rounds = options.rounds ?? SPREAD_DEFAULTS.rounds;
151
+ const byId = nodeIndex(graph);
152
+ const slots = new Map<string, Slot>();
153
+ const walls = new Map<string, Wall>();
154
+ // The plug of each end: a fixed plug, or the key of its slot.
155
+ const ends: { edge: string; source: Plug | string; target: Plug | string }[] = [];
156
+ const wallOf = (node: Node, side: Side): Wall => {
157
+ const key = JSON.stringify([node.id, side]);
158
+ let wall = walls.get(key);
159
+ if (!wall) {
160
+ const fixed: number[] = [];
161
+ for (const p of node.ports ?? []) {
162
+ const at = portOffset(node.box, p);
163
+ if (at === undefined) continue;
164
+ const point = { x: node.box.x + at.x, y: node.box.y + at.y };
165
+ if ((p.side ?? nearestSide(node.box, point)) === side) fixed.push(point[alongOf(side)]);
166
+ }
167
+ walls.set(key, (wall = { node, side, slots: [], fixed }));
168
+ }
169
+ return wall;
170
+ };
171
+ const plugOf = (node: Node, end: End, far: Node): Plug | string => {
172
+ const toward = center(far.box);
173
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
174
+ if (port && portOffset(node.box, port) !== undefined) return placePlug(node, end, toward);
175
+ const key = port ? JSON.stringify([node.id, port.id]) : JSON.stringify([node.id, slots.size]);
176
+ if (slots.has(key)) return key;
177
+ const side = port?.side ?? sideToward(node.box, toward);
178
+ const ideal = placePlug(node, end, toward).point[alongOf(side)];
179
+ const slot: Slot = { node, side, ideal, rank: [port ? node.ports!.indexOf(port) : -1, slots.size], at: ideal };
180
+ slots.set(key, slot);
181
+ wallOf(node, side).slots.push(slot);
182
+ return key;
183
+ };
184
+ for (const e of graph.edges) {
185
+ const s = byId.get(e.source.node);
186
+ const t = byId.get(e.target.node);
187
+ if (!s || !t) continue;
188
+ const source = plugOf(s, e.source, t);
189
+ const target = plugOf(t, e.target, s);
190
+ for (const [own, far] of [
191
+ [source, target],
192
+ [target, source],
193
+ ] as const) {
194
+ const slot = typeof own === "string" ? slots.get(own)! : undefined;
195
+ if (slot && slot.far === undefined) slot.far = far;
196
+ }
197
+ ends.push({ edge: e.id, source, target });
198
+ }
199
+ const resolve = (p: Plug | string): Plug => {
200
+ if (typeof p !== "string") return p;
201
+ const s = slots.get(p)!;
202
+ return { point: pointAt(s.node.box, s.side, s.at), normal: SIDE_NORMAL[s.side] };
203
+ };
204
+ // A pass sorts each wall by the places of the far ends as they are now, and places its plugs. A tie keeps the order of the last pass.
205
+ const order = new Map<Slot, number>();
206
+ for (let round = 0; round <= rounds; round++) {
207
+ for (const wall of walls.values()) {
208
+ const [lo, hi] = roomOf(wall.node.box, wall.side, inset);
209
+ const keys = new Map<Slot, number>(wall.slots.map((s) => [s, keyOf(wall.node.box, wall.side, resolve(s.far!).point)]));
210
+ const tie = (a: Slot, b: Slot): number => (round === 0 ? a.rank[0] - b.rank[0] || a.rank[1] - b.rank[1] : order.get(a)! - order.get(b)!);
211
+ const sorted = [...wall.slots].sort((a, b) => keys.get(a)! - keys.get(b)! || tie(a, b));
212
+ sorted.forEach((s, i) => order.set(s, i));
213
+ placeWall(sorted, wall.fixed, lo, hi, gap);
214
+ }
215
+ }
216
+ return ends.map((x) => ({ edge: x.edge, source: resolve(x.source), target: resolve(x.target) }));
217
+ }
@@ -1,7 +1,7 @@
1
- import { center, type Point } from "../geom.ts";
2
- import { nodeIndex, fixedPortPoint, resolvePorts, splitFrames, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
1
+ import { center } from "../geom.ts";
2
+ import { nodeIndex, resolvePorts, splitFrames, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
3
  import { placePlug, type Plug } from "../plugs.ts";
4
- import { justifyPlugs } from "../plugs/justify.ts";
4
+ import { spacePlugs } from "../plugs/justify.ts";
5
5
  import { sidePlugs } from "../plugs/side.ts";
6
6
  import type { PlugPair, Strategy } from "../strategies/model.ts";
7
7
  import { WayFinder } from "../ways.ts";
@@ -98,39 +98,3 @@ function plugsByEdge(list: readonly PlugPair[]): Map<string, PlugPair[]> {
98
98
  }
99
99
  return out;
100
100
  }
101
-
102
- /**
103
- * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
104
- * The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
105
- */
106
- function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number): void {
107
- const inset = 6;
108
- // The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target) and the point of the other end.
109
- const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
110
- ends.forEach(([e, s, t, ps, pt], i) => {
111
- for (const [end, node, plug, other, k] of [
112
- [e.source, s, ps, pt, 3],
113
- [e.target, t, pt, ps, 4],
114
- ] as const) {
115
- if (fixedPortPoint(node, end) !== undefined) continue;
116
- const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
117
- let side = sides.get(key);
118
- if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
119
- side.items.push([i, k, other.point]);
120
- }
121
- });
122
- for (const { node, normal, items } of sides.values()) {
123
- if (items.length < 2) continue;
124
- const along = normal.x !== 0 ? "y" : "x";
125
- const b = node.box;
126
- const start = along === "y" ? b.y : b.x;
127
- const len = along === "y" ? b.h : b.w;
128
- const m = Math.min(inset, len / 4);
129
- const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
130
- const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
131
- items.forEach(([i, k], j) => {
132
- const old = ends[i]![k];
133
- ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
134
- });
135
- }
136
- }
@@ -74,6 +74,9 @@ import { ZIPPER_DEFAULTS, zipperPlan, type ZipperOptions } from "../highways/zip
74
74
  import { depthLevels, type DepthLevelOptions } from "../levels/depths/index.ts";
75
75
  import type { Graph, Result, Router } from "../model.ts";
76
76
  import { facingEnds } from "../plugs/facing/index.ts";
77
+ import { justifyEnds, type JustifyOptions } from "../plugs/justify.ts";
78
+ import { sideEnds, type SidePlugOptions } from "../plugs/side.ts";
79
+ import { SPREAD_DEFAULTS, spreadEnds, type SpreadOptions } from "../plugs/spread/index.ts";
77
80
  import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
78
81
  import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
79
82
  import { cornersRouter, type CornersOptions } from "../routers/corners.ts";
@@ -87,7 +90,7 @@ import { runPipeline, type PipelineOptions } from "../whole/pipeline/index.ts";
87
90
  import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
88
91
  import type { OptionSchema } from "./options.ts";
89
92
  import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
90
- import { DEPTHS_SCHEMA, PIPELINE_SCHEMA } from "./schemas.ts";
93
+ import { DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA } from "./schemas.ts";
91
94
  import { CLOTHOID_SCHEMA, CORNERS_SCHEMA, CORRIDOR_SCHEMA, CONSENSUS_SCHEMA, CUBU_SCHEMA, CUT_SCHEMA, DIFFUSION_SCHEMA, DIVIDED_SCHEMA, EDGE_PATH_SCHEMA, FDEB_SCHEMA, FLOW_CLUSTER_SCHEMA, FLOW_FIELD_SCHEMA, GROWTH_SCHEMA, HDBSCAN_SCHEMA, HIERARCHY_SCHEMA, HIGHWAY_SCHEMA, HOUGH_SCHEMA, HUB_ARC_SCHEMA, KD_SCHEMA, KDEEB_SCHEMA, KEY_SCHEMA, LANES_SCHEMA, LEVEL_HIGHWAY_SCHEMA, LEVEL_ROOM_SCHEMA, LINK_COMMUNITY_SCHEMA, MEET_SCHEMA, MESH_SCHEMA, MINGLE_SCHEMA, NESTED_SCHEMA, OD_CELL_SCHEMA, P_HUB_SCHEMA, QUICKBUNDLES_SCHEMA, RADIUS_SCHEMA, RESISTANCE_SCHEMA, RIDGE_SCHEMA, ROAD_CLASS_SCHEMA, ROOM_SCHEMA, SCALE_SPACE_SCHEMA, SECTION_SCHEMA, SELECT_SCHEMA, SECTIONS_SCHEMA, SHARED_ALL_SCHEMA, SHARED_SCHEMA, SMOOTH_SCHEMA, SOFT_KEY_SCHEMA, SPECTRAL_SCHEMA, SPIRAL_SCHEMA, STUB_SCHEMA, TENSION_SCHEMA, TERRITORY_SCHEMA, TRACLUS_SCHEMA, TRUNKS_SCHEMA, WEISZFELD_SCHEMA, WSPD_SCHEMA, ZIPPER_SCHEMA, STEINER_SCHEMA, GAP_SCHEMA, STREET_SCHEMA, SEARCH_SCHEMA, INTERCHANGE_SCHEMA } from "./schemas.ts";
92
95
 
93
96
  /** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
@@ -957,6 +960,9 @@ export const highwayStrategies: {
957
960
  /** The built-in plug rules: the strategies of the step `plugs`. The router `corners` routes between their plugs with its option `plug`. */
958
961
  export const plugStrategies: {
959
962
  readonly facing: Strategy<"plugs", object>;
963
+ readonly side: Strategy<"plugs", SidePlugOptions>;
964
+ readonly justify: Strategy<"plugs", JustifyOptions>;
965
+ readonly spread: Strategy<"plugs", SpreadOptions>;
960
966
  } = Object.freeze({
961
967
  facing: defineStrategy({
962
968
  step: "plugs",
@@ -966,6 +972,30 @@ export const plugStrategies: {
966
972
  schema: {},
967
973
  run: (input) => facingEnds(input),
968
974
  }),
975
+ side: defineStrategy<"plugs", SidePlugOptions>({
976
+ step: "plugs",
977
+ name: "side",
978
+ describe: "WILD-001. The search of a route starts from the whole free side, and the plug goes where the way leaves the side. It is `corners` with `plugs: side`.",
979
+ defaults: { bend: 1, inset: 6, most: 16 },
980
+ schema: SIDE_PLUG_SCHEMA,
981
+ run: (input, o) => sideEnds(input, o),
982
+ }),
983
+ justify: defineStrategy<"plugs", JustifyOptions>({
984
+ step: "plugs",
985
+ name: "justify",
986
+ describe: "WILD-002. The plugs of `facing`, then the free plugs of each side keep their order and a minimum gap. They move the least distance.",
987
+ defaults: { plugGap: 8 },
988
+ schema: JUSTIFY_SCHEMA,
989
+ run: (input, o) => justifyEnds(input, o),
990
+ }),
991
+ spread: defineStrategy<"plugs", SpreadOptions>({
992
+ step: "plugs",
993
+ name: "spread",
994
+ describe: "PLUG-SPREAD. The free plugs of each wall share its room with a minimum gap, in the order of their far ends. On a short wall the gap shrinks, and no plug drops.",
995
+ defaults: { ...SPREAD_DEFAULTS },
996
+ schema: SPREAD_SCHEMA,
997
+ run: (input, o) => spreadEnds(input, o),
998
+ }),
969
999
  });
970
1000
 
971
1001
  /** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
@@ -6,6 +6,9 @@ import type { MlsebOptions } from "../highways/mlseb/index.ts";
6
6
  import type { PowerOptions } from "../highways/power/index.ts";
7
7
  import type { SbebOptions } from "../highways/sbeb/index.ts";
8
8
  import type { CutOptions } from "../groups/cut/index.ts";
9
+ import type { JustifyOptions } from "../plugs/justify.ts";
10
+ import type { SidePlugOptions } from "../plugs/side.ts";
11
+ import type { SpreadOptions } from "../plugs/spread/index.ts";
9
12
  import type { KdOptions } from "../groups/kd/index.ts";
10
13
  import type { KeyOptions } from "../groups/keys/index.ts";
11
14
  import type { HighwayBase } from "../highways/base.ts";
@@ -928,6 +931,25 @@ export const DEPTHS_SCHEMA: OptionSchema<DepthLevelOptions> = {
928
931
  levels: SECTIONS_SCHEMA.levels,
929
932
  };
930
933
 
934
+ /** The declarations of the options of the plug strategy `side` (WILD-001). The defaults are the plugs of `corners` with `plugs: "side"`. */
935
+ export const SIDE_PLUG_SCHEMA: OptionSchema<SidePlugOptions> = {
936
+ bend: { type: "number", min: 0, max: 4, step: 0.25, default: 1, describe: "The cost of a slant first leg: its length grows by this value times its angle to the normal." },
937
+ inset: { type: "number", min: 0, max: 12, step: 1, default: 6, describe: "A plug stays this far from the corners of its side." },
938
+ most: { type: "integer", min: 1, max: 64, step: 1, default: 16, describe: "The largest number of places that the search tries on each side." },
939
+ };
940
+
941
+ /** The declarations of the options of the plug strategy `justify` (WILD-002). */
942
+ export const JUSTIFY_SCHEMA: OptionSchema<JustifyOptions> = {
943
+ plugGap: { type: "number", min: 0, max: 24, step: 1, default: 8, describe: "The free plugs of a side keep their order and at least this gap. 0 keeps each plug where it faces." },
944
+ };
945
+
946
+ /** The declarations of the options of the plug strategy `spread` (PLUG-SPREAD). */
947
+ export const SPREAD_SCHEMA: OptionSchema<SpreadOptions> = {
948
+ plugGap: { type: "number", min: 0, max: 24, step: 1, default: 8, describe: "The least gap between two plugs of one wall. On a short wall, the gap shrinks evenly." },
949
+ inset: { type: "number", min: 0, max: 12, step: 1, default: 6, describe: "A plug stays this far from the corners of its wall." },
950
+ rounds: { type: "integer", min: 0, max: 8, step: 1, default: 2, describe: "The passes that sort each wall again by the new places of the far ends." },
951
+ };
952
+
931
953
  /** The declarations of the options of the whole strategy `pipeline`. */
932
954
  export const PIPELINE_SCHEMA: OptionSchema<PipelineOptions> = {
933
955
  router: { type: "strategy", step: "router", describe: "The router. A highway plan takes its place." },