@mark1russell7/highways 0.4.0 → 0.4.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.
@@ -0,0 +1,220 @@
1
+ import { TOLERANCE } from "../bench/properties.ts";
2
+ import { dist, inflate, segmentHitsBox, type Box, type Point } from "../geom.ts";
3
+ import type { Grid } from "../grid.ts";
4
+ import type { Node } from "../model.ts";
5
+ import { nearestSide } from "../plugs.ts";
6
+
7
+ /** The sides of a box, as the indexes of two corners: north, east, south and west. */
8
+ const SIDES: readonly (readonly [number, number])[] = [
9
+ [0, 1],
10
+ [1, 2],
11
+ [3, 2],
12
+ [0, 3],
13
+ ];
14
+ const SIDE_INDEX = { north: 0, east: 1, south: 2, west: 3 } as const;
15
+
16
+ /** This function gives the four corners of a box: top left, top right, bottom right and bottom left. */
17
+ function corners(b: Box): Point[] {
18
+ return [
19
+ { x: b.x, y: b.y },
20
+ { x: b.x + b.w, y: b.y },
21
+ { x: b.x + b.w, y: b.y + b.h },
22
+ { x: b.x, y: b.y + b.h },
23
+ ];
24
+ }
25
+
26
+ const EPS = 1e-6;
27
+ const keyOf = (p: Point): string => `${Math.round(p.x * 1000)},${Math.round(p.y * 1000)}`;
28
+
29
+ /** This function tells if the point `c` is on the axis-parallel segment `pq`. */
30
+ function onSide(c: Point, p: Point, q: Point): boolean {
31
+ if (Math.abs(p.y - q.y) <= EPS) return Math.abs(c.y - p.y) <= EPS && c.x >= Math.min(p.x, q.x) - EPS && c.x <= Math.max(p.x, q.x) + EPS;
32
+ return Math.abs(c.x - p.x) <= EPS && c.y >= Math.min(p.y, q.y) - EPS && c.y <= Math.max(p.y, q.y) + EPS;
33
+ }
34
+
35
+ /** This function drops each point that is on the straight line between the point before it and the point after it. */
36
+ function straighten(p: readonly Point[]): Point[] {
37
+ const out: Point[] = [];
38
+ for (const v of p) {
39
+ if (out.length > 0 && dist(out[out.length - 1]!, v) <= EPS) continue;
40
+ while (out.length >= 2) {
41
+ const a = out[out.length - 2]!;
42
+ const b = out[out.length - 1]!;
43
+ const cross = (b.x - a.x) * (v.y - b.y) - (b.y - a.y) * (v.x - b.x);
44
+ const dot = (b.x - a.x) * (v.x - b.x) + (b.y - a.y) * (v.y - b.y);
45
+ if (Math.abs(cross) > EPS || dot < 0) break;
46
+ out.pop();
47
+ }
48
+ out.push(v);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /**
54
+ * The boundary walk of the repair step (RULE-046): a graph of the sides of the boxes.
55
+ * The vertices are the corners of the boxes. The edges are the parts of each side between two corners on it.
56
+ * A walk on this graph goes along the boundaries of the boxes, and not through a box. Thus it joins two boxes also when the boxes touch.
57
+ * The graph drops each part of a side that goes through another box, so overlapping boxes do not open a way.
58
+ */
59
+ export class BoundaryWalk {
60
+ readonly #points: Point[] = [];
61
+ readonly #index = new Map<string, number>();
62
+ readonly #next: { readonly to: number; readonly d: number }[][] = [];
63
+ /** The vertices on each side of each node, in the order along the side. */
64
+ readonly #sides = new Map<Node, number[][]>();
65
+
66
+ /** This constructor makes the graph of the sides of the nodes. `grid` holds the nodes by their boxes. */
67
+ constructor(nodes: readonly Node[], grid: Grid<Node>) {
68
+ for (const n of nodes) for (const c of corners(n.box)) this.#vertex(c);
69
+ for (const n of nodes) {
70
+ const cs = corners(n.box);
71
+ const lists: number[][] = [];
72
+ for (const [i, j] of SIDES) {
73
+ const p = cs[i]!;
74
+ const q = cs[j]!;
75
+ const on = new Set<number>([this.#vertex(p), this.#vertex(q)]);
76
+ const area = { x: Math.min(p.x, q.x) - 1, y: Math.min(p.y, q.y) - 1, w: Math.abs(q.x - p.x) + 2, h: Math.abs(q.y - p.y) + 2 };
77
+ grid.query(area, (m) => {
78
+ for (const c of corners(m.box)) if (onSide(c, p, q)) on.add(this.#vertex(c));
79
+ });
80
+ const list = [...on].sort((u, v) => dist(p, this.#points[u]!) - dist(p, this.#points[v]!) || u - v);
81
+ lists.push(list);
82
+ for (let k = 1; k < list.length; k++) {
83
+ const u = list[k - 1]!;
84
+ const v = list[k]!;
85
+ const a = this.#points[u]!;
86
+ const b = this.#points[v]!;
87
+ if (dist(a, b) <= EPS || this.#blocked(grid, a, b)) continue;
88
+ this.#link(u, v);
89
+ }
90
+ }
91
+ this.#sides.set(n, lists);
92
+ }
93
+ }
94
+
95
+ /** This method gives the index of the vertex at the point, and adds the vertex if it is new. */
96
+ #vertex(p: Point): number {
97
+ const k = keyOf(p);
98
+ let i = this.#index.get(k);
99
+ if (i === undefined) {
100
+ i = this.#points.length;
101
+ this.#points.push(p);
102
+ this.#next.push([]);
103
+ this.#index.set(k, i);
104
+ }
105
+ return i;
106
+ }
107
+
108
+ #link(u: number, v: number): void {
109
+ const d = dist(this.#points[u]!, this.#points[v]!);
110
+ if (this.#next[u]!.some((e) => e.to === v)) return;
111
+ this.#next[u]!.push({ to: v, d });
112
+ this.#next[v]!.push({ to: u, d });
113
+ }
114
+
115
+ /** This method tells if the segment goes through a box, as the rule `no-pass-through` tests it. */
116
+ #blocked(grid: Grid<Node>, a: Point, b: Point): boolean {
117
+ return grid.someSegment(a, b, 0, (n) => segmentHitsBox(a, b, inflate(n.box, -TOLERANCE)));
118
+ }
119
+
120
+ /** This method finds the side of `node` that is nearest to `p`. It gives the two vertices of that side that are next to `p`. */
121
+ #around(node: Node, p: Point): number[] {
122
+ const list = this.#sides.get(node)?.[SIDE_INDEX[nearestSide(node.box, p)]];
123
+ if (!list || list.length === 0) return [];
124
+ const from = this.#points[list[0]!]!;
125
+ const at = dist(from, p);
126
+ let k = 0;
127
+ while (k < list.length && dist(from, this.#points[list[k]!]!) < at) k++;
128
+ return [list[k - 1], list[k]].filter((v): v is number => v !== undefined);
129
+ }
130
+
131
+ /**
132
+ * This method gives a way along the boundaries of the boxes. The way goes from the point `a` on the box of `s` to the point `b` on the box of `t`.
133
+ * A point that is not on its box goes first to the nearest point of the boundary.
134
+ * The method gives `undefined` when no way joins the two boxes.
135
+ */
136
+ path(a: Point, s: Node, b: Point, t: Node): Point[] | undefined {
137
+ const a1 = nearestOnBoundary(s.box, a);
138
+ const b1 = nearestOnBoundary(t.box, b);
139
+ const n = this.#points.length;
140
+ const src = n;
141
+ const dst = n + 1;
142
+ const extra = new Map<number, { readonly to: number; readonly d: number }[]>();
143
+ const add = (u: number, v: number, d: number): void => {
144
+ if (!extra.has(u)) extra.set(u, []);
145
+ extra.get(u)!.push({ to: v, d });
146
+ };
147
+ for (const v of this.#around(s, a1)) add(src, v, dist(a1, this.#points[v]!));
148
+ for (const v of this.#around(t, b1)) add(v, dst, dist(b1, this.#points[v]!));
149
+ if (!extra.has(src)) return undefined;
150
+ const pointOf = (i: number): Point => (i === src ? a1 : i === dst ? b1 : this.#points[i]!);
151
+ const best = new Float64Array(n + 2).fill(Infinity);
152
+ const prev = new Int32Array(n + 2).fill(-1);
153
+ best[src] = 0;
154
+ const heap: [number, number][] = [[0, src]];
155
+ const less = (x: [number, number], y: [number, number]): boolean => x[0] < y[0] || (x[0] === y[0] && x[1] < y[1]);
156
+ const pop = (): [number, number] => {
157
+ const top = heap[0]!;
158
+ const last = heap.pop()!;
159
+ if (heap.length > 0) {
160
+ heap[0] = last;
161
+ let i = 0;
162
+ for (;;) {
163
+ const l = 2 * i + 1;
164
+ const r = l + 1;
165
+ let m = i;
166
+ if (l < heap.length && less(heap[l]!, heap[m]!)) m = l;
167
+ if (r < heap.length && less(heap[r]!, heap[m]!)) m = r;
168
+ if (m === i) break;
169
+ [heap[i], heap[m]] = [heap[m]!, heap[i]!];
170
+ i = m;
171
+ }
172
+ }
173
+ return top;
174
+ };
175
+ const push = (x: [number, number]): void => {
176
+ heap.push(x);
177
+ let i = heap.length - 1;
178
+ while (i > 0) {
179
+ const p = (i - 1) >> 1;
180
+ if (!less(heap[i]!, heap[p]!)) break;
181
+ [heap[i], heap[p]] = [heap[p]!, heap[i]!];
182
+ i = p;
183
+ }
184
+ };
185
+ while (heap.length > 0) {
186
+ const [d, u] = pop();
187
+ if (d > best[u]!) continue;
188
+ if (u === dst) break;
189
+ const edges = [...(u < n ? this.#next[u]! : []), ...(extra.get(u) ?? [])];
190
+ for (const e of edges) {
191
+ const nd = d + e.d;
192
+ if (nd < best[e.to]!) {
193
+ best[e.to] = nd;
194
+ prev[e.to] = u;
195
+ push([nd, e.to]);
196
+ }
197
+ }
198
+ }
199
+ if (prev[dst] === -1) return undefined;
200
+ const way: Point[] = [];
201
+ for (let i = dst; i !== -1; i = prev[i]!) way.push(pointOf(i));
202
+ way.reverse();
203
+ return straighten([a, ...way, b]);
204
+ }
205
+ }
206
+
207
+ /** This function gives the point of the boundary of the box that is nearest to `p`. A point on the boundary gives itself. */
208
+ export function nearestOnBoundary(box: Box, p: Point): Point {
209
+ const x0 = box.x;
210
+ const y0 = box.y;
211
+ const x1 = box.x + box.w;
212
+ const y1 = box.y + box.h;
213
+ const inside = p.x > x0 && p.x < x1 && p.y > y0 && p.y < y1;
214
+ if (!inside) return { x: Math.min(x1, Math.max(x0, p.x)), y: Math.min(y1, Math.max(y0, p.y)) };
215
+ const side = nearestSide(box, p);
216
+ if (side === "north") return { x: p.x, y: y0 };
217
+ if (side === "south") return { x: p.x, y: y1 };
218
+ if (side === "west") return { x: x0, y: p.y };
219
+ return { x: x1, y: p.y };
220
+ }
package/src/room/lanes.ts CHANGED
@@ -15,6 +15,19 @@ export type LaneOrder = "links" | "ends" | "crossings";
15
15
  /** The lane orders of `gapLanes`, in the order of the benchmark. */
16
16
  export const LANE_ORDERS: readonly LaneOrder[] = ["links", "ends", "crossings"];
17
17
 
18
+ /**
19
+ * The rule that gives each piece of a gap its lane (its track), after the lane order.
20
+ *
21
+ * - `stack`: a piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
22
+ * - `left-edge`: the constrained left-edge rule of chip routing (EDA-004). Two pieces that overlap keep their order only where a swap adds crossings.
23
+ * The rule fills the lowest lane first, with the pieces sorted by their start. Two pieces that do not overlap can share a lane.
24
+ * - `free`: the left-edge rule without the order. It gives the fewest lanes, but it can add crossings.
25
+ */
26
+ export type LaneTracks = "stack" | "left-edge" | "free";
27
+
28
+ /** The lane rules of `gapLanes`, in the order of the benchmark. */
29
+ export const LANE_TRACKS: readonly LaneTracks[] = ["stack", "left-edge", "free"];
30
+
18
31
  /** The options of `gapLanes`. */
19
32
  export interface GapLaneOptions {
20
33
  /** The largest distance between two lanes. A narrow gap puts its lanes nearer. The default is 4. */
@@ -23,6 +36,22 @@ export interface GapLaneOptions {
23
36
  readonly order?: LaneOrder;
24
37
  /** Two box edges nearer than this distance make one boundary of the sweep, as in `boardGaps`. The default is 0.5. */
25
38
  readonly snap?: number;
39
+ /** The rule that gives each piece its lane. The default is `stack`. */
40
+ readonly tracks?: LaneTracks;
41
+ }
42
+
43
+ /** The lanes of one gap: the axis and the place of the gap, and the number of lanes that its pieces use. */
44
+ export interface GapLaneCount {
45
+ readonly axis: "x" | "y";
46
+ readonly at: number;
47
+ /** The width of the gap. */
48
+ readonly width: number;
49
+ /** The number of lanes of the gap. */
50
+ readonly lanes: number;
51
+ /** The number of pieces in the gap: the legs of the links that run along it. */
52
+ readonly pieces: number;
53
+ /** The largest number of pieces that overlap at one place of the gap. No rule gives fewer lanes. */
54
+ readonly density: number;
26
55
  }
27
56
 
28
57
  /** A piece of a link: a leg of the link that runs along a gap. */
@@ -47,9 +76,21 @@ const ROUNDS = 8;
47
76
  * When a move puts a link through more boxes than before, the link keeps its points.
48
77
  */
49
78
  export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): LevelLink[] {
79
+ return laneGaps(items, links, options).links;
80
+ }
81
+
82
+ /** This function gives the lanes of each gap that holds a piece of a link, with the options of `gapLanes`. A gap without a piece is not in the list. */
83
+ export function gapLaneCounts(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions = {}): GapLaneCount[] {
84
+ return laneGaps(items, links, options).counts;
85
+ }
86
+
87
+ /** This function does the work of `gapLanes` and `gapLaneCounts`. It gives the moved links and the lanes of each gap. */
88
+ function laneGaps(items: readonly Boxed[], links: readonly LevelLink[], options: GapLaneOptions): { links: LevelLink[]; counts: GapLaneCount[] } {
50
89
  const spacing = Math.max(0, options.spacing ?? 4);
51
90
  const order = options.order ?? "crossings";
91
+ const tracks = options.tracks ?? "stack";
52
92
  const moved = links.map((l) => l.points.map((p) => ({ x: p.x, y: p.y })));
93
+ const counts: GapLaneCount[] = [];
53
94
  for (const g of boardGaps(items, options.snap)) {
54
95
  if (g.width <= EPS) continue;
55
96
  const across = g.axis;
@@ -80,15 +121,9 @@ export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], o
80
121
  });
81
122
  if (pieces.length === 0) continue;
82
123
  const list = orderPieces(pieces, order);
83
- // A piece goes one lane past each piece before it in the order that it overlaps. Thus two pieces that overlap keep their order.
84
- const lane: number[] = [];
85
- let n = 0;
86
- list.forEach((P, i) => {
87
- let k = 0;
88
- for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
89
- lane.push(k);
90
- n = Math.max(n, k + 1);
91
- });
124
+ const lane = tracks === "stack" ? stackLanes(list) : leftEdgeLanes(list, tracks === "left-edge");
125
+ const n = lane.reduce((m, k) => Math.max(m, k + 1), 0);
126
+ counts.push({ axis: g.axis, at: g.at, width: g.width, lanes: n, pieces: list.length, density: density(list) });
92
127
  const step = Math.min(spacing, g.width / (n + 1));
93
128
  const mid = (lo + hi) / 2;
94
129
  list.forEach((P, i) => {
@@ -99,7 +134,71 @@ export function gapLanes(items: readonly Boxed[], links: readonly LevelLink[], o
99
134
  }
100
135
  const boxes = new Grid<Box>(Math.max(20, meanSide(items)));
101
136
  for (const i of items) boxes.add(i.box, i.box);
102
- return links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! }));
137
+ return { links: links.map((l, i) => (hits(boxes, moved[i]!) > hits(boxes, l.points) ? l : { ...l, points: moved[i]! })), counts };
138
+ }
139
+
140
+ /** This function gives the lanes of the rule `stack`. A piece goes one lane past each piece before it in the order that it overlaps. */
141
+ function stackLanes(list: readonly Piece[]): number[] {
142
+ const lane: number[] = [];
143
+ list.forEach((P, i) => {
144
+ let k = 0;
145
+ for (let j = 0; j < i; j++) if (overlaps(list[j]!, P)) k = Math.max(k, lane[j]! + 1);
146
+ lane.push(k);
147
+ });
148
+ return lane;
149
+ }
150
+
151
+ /**
152
+ * This function gives the lanes of the left-edge rule (Hashimoto and Stevens, 1971) for the pieces in lane order.
153
+ * With `constrained`, a piece stays below a later piece that it overlaps when the swap of the two adds crossings (`cost`).
154
+ * These constraints follow the lane order, so they have no cycle. Each pass fills one lane, from the near side.
155
+ * A pass takes the free pieces whose constraints are met, in the order of their start. It puts each piece that does not overlap the last piece of the lane.
156
+ */
157
+ function leftEdgeLanes(list: readonly Piece[], constrained: boolean): number[] {
158
+ const n = list.length;
159
+ const below: number[][] = list.map(() => []);
160
+ const wait = new Array<number>(n).fill(0);
161
+ if (constrained)
162
+ for (let i = 0; i < n; i++)
163
+ for (let j = i + 1; j < n; j++) {
164
+ const P = list[i]!;
165
+ const Q = list[j]!;
166
+ if (overlaps(P, Q) && cost(Q, P) > cost(P, Q)) {
167
+ below[i]!.push(j);
168
+ wait[j]!++;
169
+ }
170
+ }
171
+ const byStart = list.map((_, i) => i).sort((a, b) => list[a]!.lo - list[b]!.lo || a - b);
172
+ const lane = new Array<number>(n).fill(-1);
173
+ let placed = 0;
174
+ for (let track = 0; placed < n; track++) {
175
+ const now: number[] = [];
176
+ let end = -Infinity;
177
+ for (const i of byStart) {
178
+ if (lane[i]! >= 0 || wait[i]! > 0 || list[i]!.lo < end - EPS) continue;
179
+ now.push(i);
180
+ end = list[i]!.hi;
181
+ }
182
+ for (const i of now) lane[i] = track;
183
+ for (const i of now) for (const j of below[i]!) wait[j]!--;
184
+ placed += now.length;
185
+ }
186
+ return lane;
187
+ }
188
+
189
+ /** This function gives the largest number of pieces that overlap at one place. Pieces that only touch do not overlap. */
190
+ function density(list: readonly Piece[]): number {
191
+ const events: [number, number][] = [];
192
+ for (const P of list) events.push([P.lo, 1], [P.hi, -1]);
193
+ // At one place, an end comes before a start, so pieces that only touch do not overlap.
194
+ events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
195
+ let open = 0;
196
+ let most = 0;
197
+ for (const [, d] of events) {
198
+ open += d;
199
+ most = Math.max(most, open);
200
+ }
201
+ return most;
103
202
  }
104
203
 
105
204
  const overlaps = (P: Piece, Q: Piece): boolean => Math.min(P.hi, Q.hi) - Math.max(P.lo, Q.lo) > EPS;
@@ -1,14 +1,29 @@
1
- import { center } from "../geom.ts";
2
- import { nodeIndex, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
- import { placePlug } from "../plugs.ts";
1
+ import { center, type Point } from "../geom.ts";
2
+ import { nodeIndex, type Edge, type Graph, type Node, type Result, type Route, type Router } from "../model.ts";
3
+ import { placePlug, type Plug } from "../plugs.ts";
4
+ import { justifyPlugs } from "../plugs/justify.ts";
5
+ import { sidePlugs } from "../plugs/side.ts";
4
6
  import { WayFinder, type WayOptions } from "../ways.ts";
5
7
 
8
+ /** The ways to place a free plug of the corner router. */
9
+ export type PlugPlace = "facing" | "side";
10
+
6
11
  /** The options of the corner router. */
7
12
  export interface CornersOptions extends WayOptions {
8
13
  /** The name in the benchmark. */
9
14
  readonly name: string;
10
15
  /** The benchmark skips a fixture with more nodes. */
11
16
  readonly maxNodes?: number;
17
+ /**
18
+ * The place of a free plug. The default is `facing`: the plug faces the other end (`placePlug`).
19
+ * With `side`, the search starts from the whole free side, and the plug goes where the way leaves the side (`sidePlugs`, WILD-001).
20
+ */
21
+ readonly plugs?: PlugPlace;
22
+ /**
23
+ * The least gap between two free plugs of one side. The default is 0: each plug stays where it is.
24
+ * Above 0, the free plugs of each side keep their order and move the least distance (`justifyPlugs`, WILD-002).
25
+ */
26
+ readonly plugGap?: number;
12
27
  }
13
28
 
14
29
  /**
@@ -23,16 +38,68 @@ export function cornersRouter(options: CornersOptions): Router {
23
38
  const byId = nodeIndex(graph);
24
39
  const ways = new WayFinder(graph.nodes, options);
25
40
  const routes: Route[] = [];
41
+ if ((options.plugs ?? "facing") === "facing" && !((options.plugGap ?? 0) > 0)) {
42
+ for (const e of graph.edges) {
43
+ const s = byId.get(e.source.node);
44
+ const t = byId.get(e.target.node);
45
+ if (!s || !t) continue;
46
+ const ps = placePlug(s, e.source, center(t.box));
47
+ const pt = placePlug(t, e.target, center(s.box));
48
+ routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
49
+ }
50
+ return { routes };
51
+ }
52
+ // First the plugs of each edge, then the gaps of the plugs of each side, then the ways between the plugs.
53
+ const ends: [Edge, Node, Node, Plug, Plug][] = [];
26
54
  for (const e of graph.edges) {
27
55
  const s = byId.get(e.source.node);
28
56
  const t = byId.get(e.target.node);
29
57
  if (!s || !t) continue;
30
- const ps = placePlug(s, e.source, center(t.box));
31
- const pt = placePlug(t, e.target, center(s.box));
32
- routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
58
+ const [ps, pt] =
59
+ options.plugs === "side" ? sidePlugs(ways, s, e.source, t, e.target) : [placePlug(s, e.source, center(t.box)), placePlug(t, e.target, center(s.box))];
60
+ ends.push([e, s, t, ps, pt]);
33
61
  }
62
+ if ((options.plugGap ?? 0) > 0) spacePlugs(ends, options.plugGap!);
63
+ for (const [e, s, t, ps, pt] of ends) routes.push({ edge: e.id, points: ways.between(ps, pt, new Set<Node>([s, t])) });
34
64
  return { routes };
35
65
  },
36
66
  };
37
67
  return options.maxNodes === undefined ? router : { ...router, maxNodes: options.maxNodes };
38
68
  }
69
+
70
+ /**
71
+ * This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
72
+ * 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.
73
+ */
74
+ function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number): void {
75
+ const inset = 6;
76
+ // 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.
77
+ const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
78
+ ends.forEach(([e, s, t, ps, pt], i) => {
79
+ for (const [end, node, plug, other, k] of [
80
+ [e.source, s, ps, pt, 3],
81
+ [e.target, t, pt, ps, 4],
82
+ ] as const) {
83
+ const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
84
+ if (port?.at !== undefined) continue;
85
+ const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
86
+ let side = sides.get(key);
87
+ if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
88
+ side.items.push([i, k, other.point]);
89
+ }
90
+ });
91
+ for (const { node, normal, items } of sides.values()) {
92
+ if (items.length < 2) continue;
93
+ const along = normal.x !== 0 ? "y" : "x";
94
+ const b = node.box;
95
+ const start = along === "y" ? b.y : b.x;
96
+ const len = along === "y" ? b.h : b.w;
97
+ const m = Math.min(inset, len / 4);
98
+ const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
99
+ const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
100
+ items.forEach(([i, k], j) => {
101
+ const old = ends[i]![k];
102
+ ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
103
+ });
104
+ }
105
+ }
@@ -0,0 +1,145 @@
1
+ import { dist, type Point } from "../geom.ts";
2
+
3
+ /**
4
+ * This function gives the Gilbert split point of a trunk with `targets.length` edges (EDA-030). The point makes the flow cost least:
5
+ * `len(start, s) * k^alpha + Σ_j len(s, t_j)`, with `k` the number of targets. Weighted Weiszfeld steps find it.
6
+ * The distance from `start` stays from `lo` to `hi` times the distance to the center of the targets.
7
+ * It is also at most half of the distance to the nearest target.
8
+ * The point moves back toward `start` out of the boxes, in steps of `step`.
9
+ */
10
+ export function gilbertPoint(
11
+ start: Point,
12
+ targets: readonly Point[],
13
+ alpha: number,
14
+ free: (p: Point) => boolean,
15
+ step: number,
16
+ lo = 0.2,
17
+ hi = 0.8,
18
+ ): Point {
19
+ if (targets.length === 0) return start;
20
+ const k = targets.length;
21
+ const c = { x: targets.reduce((s, p) => s + p.x, 0) / k, y: targets.reduce((s, p) => s + p.y, 0) / k };
22
+ const L = dist(start, c);
23
+ if (L === 0) return start;
24
+ const w0 = k ** Math.min(1, Math.max(0, alpha));
25
+ let s = { x: start.x + (c.x - start.x) * 0.5, y: start.y + (c.y - start.y) * 0.5 };
26
+ for (let it = 0; it < 32; it++) {
27
+ let sx = 0;
28
+ let sy = 0;
29
+ let sw = 0;
30
+ const pull = (p: Point, w: number): void => {
31
+ const d = Math.max(1e-6, dist(s, p));
32
+ sx += (w * p.x) / d;
33
+ sy += (w * p.y) / d;
34
+ sw += w / d;
35
+ };
36
+ pull(start, w0);
37
+ for (const t of targets) pull(t, 1);
38
+ const n = { x: sx / sw, y: sy / sw };
39
+ const moved = dist(n, s);
40
+ s = n;
41
+ if (moved < 1e-3) break;
42
+ }
43
+ // The clamp keeps the split from `lo` to `hi` of the way, and at most half of the way to the nearest target.
44
+ const r = dist(start, s);
45
+ const most = Math.min(hi * L, 0.5 * Math.min(...targets.map((p) => dist(start, p))));
46
+ const want = Math.min(most, Math.max(lo * L, r));
47
+ const dir = r > 1e-9 ? { x: (s.x - start.x) / r, y: (s.y - start.y) / r } : { x: (c.x - start.x) / L, y: (c.y - start.y) / L };
48
+ const at = (t: number): Point => ({ x: start.x + dir.x * t, y: start.y + dir.y * t });
49
+ let d = want;
50
+ while (d > 0 && !free(at(d))) d -= step;
51
+ return d > 0 ? at(d) : start;
52
+ }
53
+
54
+ /** One point of a balancing-factor tree. */
55
+ export interface FanPoint {
56
+ readonly point: Point;
57
+ /** The index of the point before, toward the root. The root has -1. */
58
+ parent: number;
59
+ /** The length of the tree from the root to this point. */
60
+ pl: number;
61
+ /** A new branch can join the tree at this point, and on the link from its parent. A plug of a target is not open. */
62
+ readonly open: boolean;
63
+ }
64
+
65
+ /** The result of `balancedFan`: the points of the tree, and the index of the last point of the route of each target. */
66
+ export interface Fan {
67
+ readonly points: readonly FanPoint[];
68
+ readonly leaf: readonly number[];
69
+ }
70
+
71
+ /**
72
+ * This function grows a balancing-factor tree from `root` to the targets (NAT-017, after Cuntz and others, 2010).
73
+ * At each step, the target `t` joins the place `v` of the tree with the least `|v - goal(t)| + bf * PL(v)`.
74
+ * `PL(v)` is the length of the tree from the root to `v`.
75
+ * A place is a point of the tree, or the foot of `goal(t)` on a link of the tree. `bf` 0 gives the least ink, and a large `bf` gives direct routes.
76
+ * `branch(v, t)` gives the way from `v` to the plug of the target `t`: its first point is `v`, and its last point is the plug.
77
+ */
78
+ export function balancedFan(root: Point, goals: readonly Point[], bf: number, branch: (from: Point, t: number) => Point[]): Fan {
79
+ const points: FanPoint[] = [{ point: root, parent: -1, pl: 0, open: true }];
80
+ const leaf: number[] = goals.map(() => -1);
81
+ const left = new Set(goals.map((_, i) => i));
82
+ while (left.size > 0) {
83
+ let best = Infinity;
84
+ let pick = -1;
85
+ let at = -1;
86
+ let foot: { point: Point; pl: number } | undefined;
87
+ for (const t of left) {
88
+ const g = goals[t]!;
89
+ for (let v = 0; v < points.length; v++) {
90
+ const p = points[v]!;
91
+ if (!p.open) continue;
92
+ const c = dist(p.point, g) + bf * p.pl;
93
+ if (c < best - 1e-9) {
94
+ best = c;
95
+ pick = t;
96
+ at = v;
97
+ foot = undefined;
98
+ }
99
+ // The foot of the goal on the link from the parent of v to v.
100
+ if (p.parent < 0) continue;
101
+ const a = points[p.parent]!.point;
102
+ const b = p.point;
103
+ const L = dist(a, b);
104
+ if (L <= 1e-9) continue;
105
+ const u = ((g.x - a.x) * (b.x - a.x) + (g.y - a.y) * (b.y - a.y)) / (L * L);
106
+ if (u <= 1e-6 || u >= 1 - 1e-6) continue;
107
+ const f = { x: a.x + u * (b.x - a.x), y: a.y + u * (b.y - a.y) };
108
+ const pl = points[p.parent]!.pl + u * L;
109
+ const cf = dist(f, g) + bf * pl;
110
+ if (cf < best - 1e-9) {
111
+ best = cf;
112
+ pick = t;
113
+ at = v;
114
+ foot = { point: f, pl };
115
+ }
116
+ }
117
+ }
118
+ left.delete(pick);
119
+ let from = at;
120
+ if (foot) {
121
+ // The foot splits the link from the parent of `at` to `at`.
122
+ points.push({ point: foot.point, parent: points[at]!.parent, pl: foot.pl, open: true });
123
+ from = points.length - 1;
124
+ points[at]!.parent = from;
125
+ }
126
+ const way = branch(points[from]!.point, pick);
127
+ let prev = from;
128
+ for (let i = 1; i < way.length; i++) {
129
+ const q = way[i]!;
130
+ const base = points[prev]!;
131
+ if (dist(q, base.point) <= 1e-9) continue;
132
+ points.push({ point: q, parent: prev, pl: base.pl + dist(base.point, q), open: i < way.length - 1 });
133
+ prev = points.length - 1;
134
+ }
135
+ leaf[pick] = prev;
136
+ }
137
+ return { points, leaf };
138
+ }
139
+
140
+ /** This function gives the points of the tree from the root to the point `i`. */
141
+ export function fanPath(fan: Fan, i: number): Point[] {
142
+ const out: Point[] = [];
143
+ for (let j = i; j >= 0; j = fan.points[j]!.parent) out.push(fan.points[j]!.point);
144
+ return out.reverse();
145
+ }
@@ -9,9 +9,9 @@ import { byCollection, trunksRouter } from "./trunks.ts";
9
9
  export { boxExit, straightRouter } from "./straight.ts";
10
10
  export { plugsRouter } from "./plugs.ts";
11
11
  export { cornersRouter, type CornersOptions } from "./corners.ts";
12
- export { sharedRouter, type SharedOptions } from "./shared.ts";
12
+ export { sharedRouter, type ShareCost, type SharedOptions, type ShareMerge, type ShareOrder } from "./shared.ts";
13
13
  export { sectionsRouter, type SectionsOptions } from "./sections.ts";
14
- export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunksOptions } from "./trunks.ts";
14
+ export { byCollection, groupEdges, laneOrder, splitPoint, trunksRouter, type TrunkFan, type TrunkSplit, type TrunksOptions } from "./trunks.ts";
15
15
 
16
16
  /** The built-in routers. The registry `STRATEGIES` holds the same routers as strategies, and the benchmark races the routers of the registry. */
17
17
  export const ROUTERS: readonly Router[] = [