@mark1russell7/highways 0.9.4 → 0.9.6
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/README.md +59 -7
- package/build/index.d.ts +617 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3785 -274
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/screen/exercise.ts +2 -1
- package/src/highways/balance/index.ts +93 -0
- package/src/highways/index.ts +3 -0
- package/src/highways/manifolds/index.ts +157 -0
- package/src/highways/slots/index.ts +178 -0
- package/src/index.ts +7 -0
- package/src/levels/detail/index.ts +140 -0
- package/src/model.ts +21 -3
- package/src/plugs/assign/index.ts +494 -0
- package/src/plugs/fix.ts +55 -0
- package/src/plugs/justify.ts +39 -7
- package/src/plugs/leaders/index.ts +42 -0
- package/src/plugs/range.ts +120 -0
- package/src/plugs/row-cards.ts +42 -0
- package/src/plugs/side.ts +7 -0
- package/src/plugs/spread/index.ts +61 -41
- package/src/plugs/walls.ts +47 -0
- package/src/plugs.ts +4 -2
- package/src/routers/adam/index.ts +294 -0
- package/src/routers/canals/index.ts +191 -0
- package/src/routers/index.ts +3 -0
- package/src/routers/reroute/index.ts +164 -0
- package/src/routers/shared.ts +3 -0
- package/src/strategies/builtins.ts +112 -1
- package/src/strategies/schemas.ts +96 -0
- package/src/whole/curriculum/index.ts +243 -0
- package/src/whole/fireflies/index.ts +177 -0
- package/src/whole/preferences/index.ts +247 -0
- package/src/whole/tracing/index.ts +144 -0
package/src/plugs/justify.ts
CHANGED
|
@@ -3,10 +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";
|
|
6
|
+
import { center, type Box, type Point } from "../geom.ts";
|
|
7
|
+
import { fixedPortPoint, nodeIndex, type Edge, type Node, type Port, type Side } from "../model.ts";
|
|
8
8
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
9
9
|
import type { PlugInput, PlugPair } from "../strategies/model.ts";
|
|
10
|
+
import { alongAxis, placeInIntervals, portOfEnd, rangeInterval } from "./range.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
|
|
@@ -56,10 +57,11 @@ export function isotonic(v: readonly number[]): number[] {
|
|
|
56
57
|
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
57
58
|
* 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
59
|
* A plug stays `inset` from the corners of its side, or a quarter of the side when the side is short.
|
|
60
|
+
* A plug of a port with a `range` stays in its range (PORT-RANGE), and the plugs keep their order and the gap (`placeInIntervals`).
|
|
59
61
|
*/
|
|
60
62
|
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)
|
|
62
|
-
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
|
|
63
|
+
// The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target), the point of the other end and the port.
|
|
64
|
+
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point, Port | undefined][] }>();
|
|
63
65
|
ends.forEach(([e, s, t, ps, pt], i) => {
|
|
64
66
|
for (const [end, node, plug, other, k] of [
|
|
65
67
|
[e.source, s, ps, pt, 3],
|
|
@@ -69,18 +71,19 @@ export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number,
|
|
|
69
71
|
const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
|
|
70
72
|
let side = sides.get(key);
|
|
71
73
|
if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
|
|
72
|
-
side.items.push([i, k, other.point]);
|
|
74
|
+
side.items.push([i, k, other.point, portOfEnd(node, end)]);
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
for (const { node, normal, items } of sides.values()) {
|
|
76
|
-
|
|
78
|
+
const ranged = items.some((x) => x[3]?.range !== undefined);
|
|
79
|
+
if (items.length < 2 && !ranged) continue;
|
|
77
80
|
const along = normal.x !== 0 ? "y" : "x";
|
|
78
81
|
const b = node.box;
|
|
79
82
|
const start = along === "y" ? b.y : b.x;
|
|
80
83
|
const len = along === "y" ? b.h : b.w;
|
|
81
84
|
const m = Math.min(inset, len / 4);
|
|
82
85
|
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]);
|
|
86
|
+
const placed = ranged ? spaceInRanges(b, normal, items, ideal, start + m, start + len - m, gap, inset) : justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
|
|
84
87
|
items.forEach(([i, k], j) => {
|
|
85
88
|
const old = ends[i]![k];
|
|
86
89
|
ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
|
|
@@ -88,6 +91,35 @@ export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number,
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* This function places the free plugs of one side with ranges (PORT-RANGE). The order is the order of `justifyPlugs`.
|
|
96
|
+
* Each plug stays in the interval of its range, or in the room of the side without a range.
|
|
97
|
+
*/
|
|
98
|
+
function spaceInRanges(box: Box, normal: Point, items: readonly [number, 3 | 4, Point, Port | undefined][], ideal: readonly number[], lo: number, hi: number, gap: number, inset: number): number[] {
|
|
99
|
+
const side = sideOfNormal(normal);
|
|
100
|
+
const along = alongAxis(side);
|
|
101
|
+
const order = ideal.map((_, i) => i).sort((a, b) => ideal[a]! - ideal[b]! || items[a]![2][along] - items[b]![2][along] || a - b);
|
|
102
|
+
const span = order.map((j): [number, number] => rangeInterval(box, side, items[j]![3], inset) ?? [lo, hi]);
|
|
103
|
+
const at = placeInIntervals(
|
|
104
|
+
order.map((j) => ideal[j]!),
|
|
105
|
+
span.map((r) => r[0]),
|
|
106
|
+
span.map((r) => r[1]),
|
|
107
|
+
gap,
|
|
108
|
+
);
|
|
109
|
+
const out = new Array<number>(ideal.length);
|
|
110
|
+
order.forEach((j, i) => {
|
|
111
|
+
out[j] = at[i]!;
|
|
112
|
+
});
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** This function gives the side of an outward normal. */
|
|
117
|
+
function sideOfNormal(n: Point): Side {
|
|
118
|
+
if (n.x > 0) return "east";
|
|
119
|
+
if (n.x < 0) return "west";
|
|
120
|
+
return n.y > 0 ? "south" : "north";
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
/** The options of the plug strategy `justify`. */
|
|
92
124
|
export interface JustifyOptions {
|
|
93
125
|
/** The least gap between two free plugs of one side. The default is 8. 0 keeps each plug where it faces. */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plug strategy `leaders` (EDA-041): boundary leaders of the labeling of maps, used as the stubs of the plugs.
|
|
3
|
+
* Each wall places its plugs in an order that gives leaders with no crossing, and the least length of the leaders.
|
|
4
|
+
*/
|
|
5
|
+
import type { PlugInput, PlugPair } from "../../strategies/model.ts";
|
|
6
|
+
import { spreadWith, SPREAD_DEFAULTS } from "../spread/index.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The type of leader of a plug. With `s`, the order of a wall is the order of the angles of the far ends, as in `spread`.
|
|
10
|
+
* With `po`, the stub leaves along the normal and turns once, so the order is the order of the far ends along the wall.
|
|
11
|
+
* With `opo`, the plugs of a wall go at even places, in the order of the angles.
|
|
12
|
+
*/
|
|
13
|
+
export type LeaderType = "s" | "po" | "opo";
|
|
14
|
+
|
|
15
|
+
/** The values of `LeaderType`. */
|
|
16
|
+
export const LEADER_TYPES: readonly LeaderType[] = Object.freeze(["s", "po", "opo"]);
|
|
17
|
+
|
|
18
|
+
/** The options of the plug strategy `leaders`. */
|
|
19
|
+
export interface LeaderOptions {
|
|
20
|
+
/** The type of leader. The default is `po`. */
|
|
21
|
+
readonly leader: LeaderType;
|
|
22
|
+
/** The least gap between two plugs of one wall, from 2 to 8. The default is 8. */
|
|
23
|
+
readonly slotSpacing: number;
|
|
24
|
+
/** 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. */
|
|
25
|
+
readonly inset: number;
|
|
26
|
+
/** The number of passes that sort each wall again by the new places of the far ends, from 0 to 4. The default is 2. */
|
|
27
|
+
readonly rounds: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The defaults of the plug strategy `leaders`. */
|
|
31
|
+
export const LEADER_DEFAULTS: Readonly<LeaderOptions> = Object.freeze({ leader: "po", slotSpacing: 8, inset: SPREAD_DEFAULTS.inset, rounds: SPREAD_DEFAULTS.rounds });
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* This function gives the plugs of the plug strategy `leaders` (EDA-041, after Bekos 2007). A fixed plug stays at its point, and a plug with a side stays on that side.
|
|
35
|
+
* Each free plug goes on the side that faces the other end. The plugs of each wall keep at least `slotSpacing` between two of them.
|
|
36
|
+
* The type of leader sets their order and their places. The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
37
|
+
*/
|
|
38
|
+
export function leaderEnds(input: PlugInput, options: Partial<LeaderOptions> = {}): PlugPair[] {
|
|
39
|
+
const o = { ...LEADER_DEFAULTS, ...options };
|
|
40
|
+
const spread = { plugGap: o.slotSpacing, inset: o.inset, rounds: o.rounds };
|
|
41
|
+
return spreadWith(input, spread, o.leader === "s" ? undefined : o.leader);
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plug ranges (PORT-RANGE): a free plug of a port with `range` stays in that interval of its side.
|
|
3
|
+
* The plug rules read the interval of each plug, and place the plugs of a side in their order with a least gap.
|
|
4
|
+
*/
|
|
5
|
+
import type { Box, Point } from "../geom.ts";
|
|
6
|
+
import type { End, Node, Port, Side } from "../model.ts";
|
|
7
|
+
|
|
8
|
+
/** The tolerance of the comparisons of places along a side. */
|
|
9
|
+
const EPS = 1e-9;
|
|
10
|
+
|
|
11
|
+
/** This function gives the axis along a side. */
|
|
12
|
+
export function alongAxis(side: Side): "x" | "y" {
|
|
13
|
+
return side === "east" || side === "west" ? "y" : "x";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** This function gives the port of an end, or `undefined` when the end has no port or the node has no such port. */
|
|
17
|
+
export function portOfEnd(node: Node, end: End): Port | undefined {
|
|
18
|
+
return end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This function gives the interval of a plug with a range, on the axis along its side, or `undefined` for a port without a range.
|
|
23
|
+
* The interval is the range of the port, without the corner insets of the side.
|
|
24
|
+
* When the range is all in a corner inset, the interval is the range.
|
|
25
|
+
*/
|
|
26
|
+
export function rangeInterval(box: Box, side: Side, port: Port | undefined, inset = 6): [number, number] | undefined {
|
|
27
|
+
const range = port?.range;
|
|
28
|
+
if (range === undefined) return undefined;
|
|
29
|
+
const along = alongAxis(side);
|
|
30
|
+
const start = along === "y" ? box.y : box.x;
|
|
31
|
+
const len = along === "y" ? box.h : box.w;
|
|
32
|
+
const m = Math.min(inset, len / 4);
|
|
33
|
+
const lo = start + range[0] * len;
|
|
34
|
+
const hi = start + range[1] * len;
|
|
35
|
+
const a = Math.max(lo, start + m);
|
|
36
|
+
const b = Math.min(hi, start + len - m);
|
|
37
|
+
return a <= b ? [a, b] : [lo, hi];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** This function moves a point of a side into the interval of its range, on the axis along the side. A point of a port without a range stays. */
|
|
41
|
+
export function clampToRange(box: Box, side: Side, port: Port | undefined, point: Point, inset = 6): Point {
|
|
42
|
+
const r = rangeInterval(box, side, port, inset);
|
|
43
|
+
if (!r) return point;
|
|
44
|
+
const along = alongAxis(side);
|
|
45
|
+
const v = Math.max(r[0], Math.min(r[1], point[along]));
|
|
46
|
+
return along === "y" ? { x: point.x, y: v } : { x: v, y: point.y };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function places `ideal.length` plugs of one side in their given order. The plug `i` stays in the interval from `lo[i]` to `hi[i]`.
|
|
51
|
+
* Two neighbours are at least `gap` apart. When the intervals are too short for the gap, the gap shrinks evenly to the largest gap that they let.
|
|
52
|
+
* The sum of the squares of the moves from the ideal places is the least.
|
|
53
|
+
*
|
|
54
|
+
* When the order cannot keep each plug in its interval, the plugs take the order of their ideal places in their intervals. Thus the intervals win over the order.
|
|
55
|
+
* The result is in the order of `ideal`.
|
|
56
|
+
*/
|
|
57
|
+
export function placeInIntervals(ideal: readonly number[], lo: readonly number[], hi: readonly number[], gap: number): number[] {
|
|
58
|
+
const k = ideal.length;
|
|
59
|
+
if (k === 0) return [];
|
|
60
|
+
let order = ideal.map((_, i) => i);
|
|
61
|
+
// A later plug with an interval wholly before the interval of an earlier plug breaks the order.
|
|
62
|
+
let most = -Infinity;
|
|
63
|
+
let broken = false;
|
|
64
|
+
for (const i of order) {
|
|
65
|
+
if (hi[i]! < most - EPS) broken = true;
|
|
66
|
+
most = Math.max(most, lo[i]!);
|
|
67
|
+
}
|
|
68
|
+
if (broken) {
|
|
69
|
+
const c = order.map((i) => Math.max(lo[i]!, Math.min(hi[i]!, ideal[i]!)));
|
|
70
|
+
order = [...order].sort((a, b) => c[a]! - c[b]! || a - b);
|
|
71
|
+
}
|
|
72
|
+
// The largest gap that the order lets: the plug j and the later plug i need (i - j)·s <= hi_i - lo_j.
|
|
73
|
+
let s = Math.max(0, gap);
|
|
74
|
+
for (let i = 1; i < k; i++) {
|
|
75
|
+
const hiI = hi[order[i]!]!;
|
|
76
|
+
for (let j = 0; j < i; j++) s = Math.min(s, (hiI - lo[order[j]!]!) / (i - j));
|
|
77
|
+
}
|
|
78
|
+
s = Math.max(0, s);
|
|
79
|
+
// With y_i = x_i - i·s, the gap becomes the rule y_i <= y_(i+1), and each y_i has the bounds L_i and U_i.
|
|
80
|
+
// The bounds become monotone: L_i is the most of the lower bounds before i, and U_i is the least of the upper bounds after i.
|
|
81
|
+
const lower = new Array<number>(k);
|
|
82
|
+
const upper = new Array<number>(k);
|
|
83
|
+
let run = -Infinity;
|
|
84
|
+
for (let i = 0; i < k; i++) lower[i] = run = Math.max(run, lo[order[i]!]! - i * s);
|
|
85
|
+
run = Infinity;
|
|
86
|
+
for (let i = k - 1; i >= 0; i--) upper[i] = run = Math.min(run, hi[order[i]!]! - i * s);
|
|
87
|
+
// Pool adjacent violators with bounds. The value of a block is its mean, clamped to the bounds of the block.
|
|
88
|
+
// The lower bound of a block is the lower bound of its last item, and its upper bound is the upper bound of its first item.
|
|
89
|
+
const sum: number[] = [];
|
|
90
|
+
const size: number[] = [];
|
|
91
|
+
const blockLo: number[] = [];
|
|
92
|
+
const blockHi: number[] = [];
|
|
93
|
+
const value: number[] = [];
|
|
94
|
+
const clamp = (v: number, a: number, b: number): number => Math.max(a, Math.min(b, v));
|
|
95
|
+
for (let i = 0; i < k; i++) {
|
|
96
|
+
sum.push(ideal[order[i]!]! - i * s);
|
|
97
|
+
size.push(1);
|
|
98
|
+
blockLo.push(lower[i]!);
|
|
99
|
+
blockHi.push(upper[i]!);
|
|
100
|
+
value.push(clamp(sum[sum.length - 1]!, lower[i]!, upper[i]!));
|
|
101
|
+
while (value.length > 1 && value[value.length - 2]! > value[value.length - 1]!) {
|
|
102
|
+
const t = value.length - 1;
|
|
103
|
+
sum[t - 1]! += sum[t]!;
|
|
104
|
+
size[t - 1]! += size[t]!;
|
|
105
|
+
blockLo[t - 1] = blockLo[t]!;
|
|
106
|
+
value[t - 1] = clamp(sum[t - 1]! / size[t - 1]!, blockLo[t - 1]!, blockHi[t - 1]!);
|
|
107
|
+
sum.pop();
|
|
108
|
+
size.pop();
|
|
109
|
+
blockLo.pop();
|
|
110
|
+
blockHi.pop();
|
|
111
|
+
value.pop();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const out = new Array<number>(k);
|
|
115
|
+
let i = 0;
|
|
116
|
+
value.forEach((v, b) => {
|
|
117
|
+
for (let j = 0; j < size[b]!; j++, i++) out[order[i]!] = v + i * s;
|
|
118
|
+
});
|
|
119
|
+
return out;
|
|
120
|
+
}
|
package/src/plugs/row-cards.ts
CHANGED
|
@@ -44,3 +44,45 @@ export function rowCardsBoard(options: RowCardsOptions = {}): Graph {
|
|
|
44
44
|
}
|
|
45
45
|
return { nodes, edges };
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/** The options of `tallCardsBoard`. */
|
|
49
|
+
export interface TallCardsOptions {
|
|
50
|
+
/** The number of rows of each card. The default is 20. */
|
|
51
|
+
readonly rows?: number;
|
|
52
|
+
/** The height of one row. The default is 55. */
|
|
53
|
+
readonly rowHeight?: number;
|
|
54
|
+
/** The number of rows that the box of the rows of each card shows. The list repeats for more cards. The default is `[3, 4, 5, 3, 4, 5]`. */
|
|
55
|
+
readonly visible?: readonly number[];
|
|
56
|
+
/** The height of the header of a card, above the box of its rows. The default is 40. */
|
|
57
|
+
readonly header?: number;
|
|
58
|
+
/** The height of the footer of a card, below the box of its rows. The default is 24. */
|
|
59
|
+
readonly footer?: number;
|
|
60
|
+
/** With `true`, each port has the range of its row (PORT-RANGE). The default is `true`. */
|
|
61
|
+
readonly ranges?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* This function gives a board of 6 cards with tall rows (PORT-RANGE, PLUG-ASSIGN), in 3 columns and 2 lines. It has the links of `rowCardsBoard`.
|
|
66
|
+
* The box of the rows of a card is smaller than the card, because the card has a header and a footer. The box shows only some rows.
|
|
67
|
+
* The port `o<row>` is on the east side, and the port `i<row>` is on the west side.
|
|
68
|
+
* The range of a port is the reach of its row. It goes from the top of the box of the rows plus half a row to its bottom minus half a row.
|
|
69
|
+
*/
|
|
70
|
+
export function tallCardsBoard(options: TallCardsOptions = {}): Graph {
|
|
71
|
+
const rows = options.rows ?? 20;
|
|
72
|
+
const rowHeight = options.rowHeight ?? 55;
|
|
73
|
+
const visible = options.visible ?? [3, 4, 5, 3, 4, 5];
|
|
74
|
+
const header = options.header ?? 40;
|
|
75
|
+
const footer = options.footer ?? 24;
|
|
76
|
+
const ranged = options.ranges ?? true;
|
|
77
|
+
const base = rowCardsBoard({ rows });
|
|
78
|
+
const w = 160;
|
|
79
|
+
const tall = header + Math.max(...visible) * rowHeight + footer;
|
|
80
|
+
const nodes = base.nodes.map((n, c): Node => {
|
|
81
|
+
const v = visible[c % visible.length]!;
|
|
82
|
+
const h = header + v * rowHeight + footer;
|
|
83
|
+
const range: [number, number] = [(header + rowHeight / 2) / h, (header + v * rowHeight - rowHeight / 2) / h];
|
|
84
|
+
const ports = (n.ports ?? []).map((p): Port => ({ id: p.id, side: p.id.startsWith("o") ? "east" : "west", ...(ranged ? { range } : {}) }));
|
|
85
|
+
return { id: n.id, box: { x: (c % 3) * (w + 200), y: Math.floor(c / 3) * (tall + 160), w, h }, ports };
|
|
86
|
+
});
|
|
87
|
+
return { nodes, edges: base.edges };
|
|
88
|
+
}
|
package/src/plugs/side.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { placePlug, pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "../p
|
|
|
8
8
|
import type { PlugInput, PlugPair } from "../strategies/model.ts";
|
|
9
9
|
import type { Terminal } from "../visibility.ts";
|
|
10
10
|
import { WayFinder } from "../ways.ts";
|
|
11
|
+
import { clampToRange, rangeInterval } from "./range.ts";
|
|
11
12
|
|
|
12
13
|
/** The options of `sidePlugs`. */
|
|
13
14
|
export interface SidePlugOptions {
|
|
@@ -50,6 +51,12 @@ function freeSide(node: Node, end: End, toward: Point, inset: number): FreeSide
|
|
|
50
51
|
const m = Math.min(inset, len / 4);
|
|
51
52
|
const at = side === "east" ? b.x + b.w : side === "west" ? b.x : side === "south" ? b.y + b.h : b.y;
|
|
52
53
|
const point = pointOnSide(b, side, toward, inset);
|
|
54
|
+
// A port with a range (PORT-RANGE) keeps its plug in the interval of the range.
|
|
55
|
+
const range = port?.range === undefined ? undefined : rangeInterval(b, side, port, inset);
|
|
56
|
+
if (range) {
|
|
57
|
+
const facing = clampToRange(b, side, port, point, inset);
|
|
58
|
+
return { node, side, normal: SIDE_NORMAL[side], along, at, lo: range[0], hi: range[1], facing: { point: facing, normal: SIDE_NORMAL[side] } };
|
|
59
|
+
}
|
|
53
60
|
return { node, side, normal: SIDE_NORMAL[side], along, at, lo: start + m, hi: start + len - m, facing: { point, normal: SIDE_NORMAL[side] } };
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -7,6 +7,8 @@ import { nodeIndex, portOffset, type End, type Node, type Side } from "../../mod
|
|
|
7
7
|
import { nearestSide, placePlug, SIDE_NORMAL, sideToward, type Plug } from "../../plugs.ts";
|
|
8
8
|
import type { PlugInput, PlugPair } from "../../strategies/model.ts";
|
|
9
9
|
import { isotonic } from "../justify.ts";
|
|
10
|
+
import { placeInIntervals, rangeInterval } from "../range.ts";
|
|
11
|
+
import { alongOf, keyOf, pointAt, roomOf } from "../walls.ts";
|
|
10
12
|
|
|
11
13
|
/** The options of the plug strategy `spread`. */
|
|
12
14
|
export interface SpreadOptions {
|
|
@@ -29,6 +31,8 @@ interface Slot {
|
|
|
29
31
|
readonly ideal: number;
|
|
30
32
|
/** 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
33
|
readonly rank: readonly [number, number];
|
|
34
|
+
/** The interval of the range of the port on the axis along the wall (PORT-RANGE), or `undefined` for a port without a range. */
|
|
35
|
+
readonly range?: readonly [number, number];
|
|
32
36
|
/** The far end of the first end of the slot: a fixed plug, or the key of its slot. */
|
|
33
37
|
far?: Plug | string;
|
|
34
38
|
/** The place of the plug on the axis along the wall. */
|
|
@@ -43,45 +47,18 @@ interface Wall {
|
|
|
43
47
|
readonly fixed: number[];
|
|
44
48
|
}
|
|
45
49
|
|
|
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
50
|
/**
|
|
69
|
-
* This function gives the sort key of a far point
|
|
70
|
-
*
|
|
51
|
+
* This function gives the sort key of a far point for a po-leader (EDA-041): its place on the axis along the wall.
|
|
52
|
+
* A far point behind the wall gets the key of `keyOf` with a large shift, so it goes to an end of the wall.
|
|
71
53
|
*/
|
|
72
|
-
function
|
|
54
|
+
function poKeyOf(box: Box, side: Side, far: Point): number {
|
|
73
55
|
const c = center(box);
|
|
74
56
|
const n = SIDE_NORMAL[side];
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
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;
|
|
57
|
+
const out = (far.x - c.x) * n.x + (far.y - c.y) * n.y;
|
|
58
|
+
const along = alongOf(side) === "y" ? far.y : far.x;
|
|
59
|
+
if (out > 0) return along;
|
|
60
|
+
const k = keyOf(box, side, far);
|
|
61
|
+
return k >= 0 ? 1e12 + k : -1e12 + k;
|
|
85
62
|
}
|
|
86
63
|
|
|
87
64
|
/**
|
|
@@ -102,7 +79,7 @@ function placeInOrder(ideal: readonly number[], lo: number, hi: number, gap: num
|
|
|
102
79
|
* 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
80
|
* The gap of a part shrinks evenly when the part is too short.
|
|
104
81
|
*/
|
|
105
|
-
function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number): void {
|
|
82
|
+
function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number, hi: number, gap: number, idealOf: (s: Slot) => number = (s) => s.ideal): void {
|
|
106
83
|
const cuts = [...new Set(fixed.filter((f) => f > lo - 1e-9 && f < hi + 1e-9))].sort((a, b) => a - b);
|
|
107
84
|
// 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
85
|
const ends = [lo, ...cuts, hi];
|
|
@@ -110,10 +87,11 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
|
|
|
110
87
|
let part = 0;
|
|
111
88
|
for (const s of slots) {
|
|
112
89
|
let p = 0;
|
|
113
|
-
while (p < cuts.length && s
|
|
90
|
+
while (p < cuts.length && idealOf(s) > cuts[p]!) p++;
|
|
114
91
|
part = Math.max(part, p);
|
|
115
92
|
parts[part]!.push(s);
|
|
116
93
|
}
|
|
94
|
+
const ranged = slots.some((s) => s.range !== undefined);
|
|
117
95
|
parts.forEach((list, p) => {
|
|
118
96
|
if (list.length === 0) return;
|
|
119
97
|
const a = ends[p]!;
|
|
@@ -124,8 +102,29 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
|
|
|
124
102
|
const s = steps > 0 ? Math.max(0, Math.min(gap, (b - a) / steps)) : gap;
|
|
125
103
|
const lower = a + fa * s;
|
|
126
104
|
const upper = Math.max(lower, b - fb * s);
|
|
105
|
+
if (ranged) {
|
|
106
|
+
// Each plug with a range stays in the part of its range (PORT-RANGE). A range out of the part gives the nearest place of the part.
|
|
107
|
+
const span = list.map((x): [number, number] => {
|
|
108
|
+
if (!x.range) return [lower, upper];
|
|
109
|
+
const lo = Math.max(lower, x.range[0]);
|
|
110
|
+
const hi = Math.min(upper, x.range[1]);
|
|
111
|
+
if (lo <= hi) return [lo, hi];
|
|
112
|
+
const v = x.range[1] < lower ? lower : upper;
|
|
113
|
+
return [v, v];
|
|
114
|
+
});
|
|
115
|
+
const at = placeInIntervals(
|
|
116
|
+
list.map((x) => idealOf(x)),
|
|
117
|
+
span.map((r) => r[0]),
|
|
118
|
+
span.map((r) => r[1]),
|
|
119
|
+
s,
|
|
120
|
+
);
|
|
121
|
+
list.forEach((x, i) => {
|
|
122
|
+
x.at = at[i]!;
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
127
126
|
const at = placeInOrder(
|
|
128
|
-
list.map((x) => x
|
|
127
|
+
list.map((x) => idealOf(x)),
|
|
129
128
|
lower,
|
|
130
129
|
upper,
|
|
131
130
|
s,
|
|
@@ -144,6 +143,21 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
|
|
|
144
143
|
* The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
145
144
|
*/
|
|
146
145
|
export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugPair[] {
|
|
146
|
+
return spreadWith(input, options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The order and the places of the plugs of one wall in the strategy `leaders` (EDA-041).
|
|
151
|
+
* With `po`, the sort key of a far end in front of the wall is its place on the axis along the wall.
|
|
152
|
+
* With `opo`, the plugs of a wall go at even places in the order of the keys of `spread`.
|
|
153
|
+
*/
|
|
154
|
+
export type WallLeader = "po" | "opo";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* This function does the work of `spreadEnds`, and of the strategy `leaders` with the argument `leader`.
|
|
158
|
+
* Without `leader`, it gives the plugs of `spread`.
|
|
159
|
+
*/
|
|
160
|
+
export function spreadWith(input: PlugInput, options: SpreadOptions = {}, leader?: WallLeader): PlugPair[] {
|
|
147
161
|
const { graph } = input;
|
|
148
162
|
const gap = options.plugGap ?? SPREAD_DEFAULTS.plugGap;
|
|
149
163
|
const inset = options.inset ?? SPREAD_DEFAULTS.inset;
|
|
@@ -176,7 +190,8 @@ export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugP
|
|
|
176
190
|
if (slots.has(key)) return key;
|
|
177
191
|
const side = port?.side ?? sideToward(node.box, toward);
|
|
178
192
|
const ideal = placePlug(node, end, toward).point[alongOf(side)];
|
|
179
|
-
const
|
|
193
|
+
const range = rangeInterval(node.box, side, port, inset);
|
|
194
|
+
const slot: Slot = { node, side, ideal, rank: [port ? node.ports!.indexOf(port) : -1, slots.size], at: ideal, ...(range ? { range } : {}) };
|
|
180
195
|
slots.set(key, slot);
|
|
181
196
|
wallOf(node, side).slots.push(slot);
|
|
182
197
|
return key;
|
|
@@ -206,11 +221,16 @@ export function spreadEnds(input: PlugInput, options: SpreadOptions = {}): PlugP
|
|
|
206
221
|
for (let round = 0; round <= rounds; round++) {
|
|
207
222
|
for (const wall of walls.values()) {
|
|
208
223
|
const [lo, hi] = roomOf(wall.node.box, wall.side, inset);
|
|
209
|
-
const
|
|
224
|
+
const key = leader === "po" ? poKeyOf : keyOf;
|
|
225
|
+
const keys = new Map<Slot, number>(wall.slots.map((s) => [s, key(wall.node.box, wall.side, resolve(s.far!).point)]));
|
|
210
226
|
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
227
|
const sorted = [...wall.slots].sort((a, b) => keys.get(a)! - keys.get(b)! || tie(a, b));
|
|
212
228
|
sorted.forEach((s, i) => order.set(s, i));
|
|
213
|
-
|
|
229
|
+
if (leader === "opo") {
|
|
230
|
+
// The even places: the plugs split the room into equal parts, so the plug spacing of the wall is the largest that it can be.
|
|
231
|
+
const even = new Map<Slot, number>(sorted.map((s, i) => [s, lo + ((i + 0.5) * (hi - lo)) / sorted.length]));
|
|
232
|
+
placeWall(sorted, wall.fixed, lo, hi, gap, (s) => even.get(s)!);
|
|
233
|
+
} else placeWall(sorted, wall.fixed, lo, hi, gap);
|
|
214
234
|
}
|
|
215
235
|
}
|
|
216
236
|
return ends.map((x) => ({ edge: x.edge, source: resolve(x.source), target: resolve(x.target) }));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The walls of the plug strategies `spread`, `leaders` and `assign`: one side of one node, its room, and the order of its far ends.
|
|
3
|
+
*/
|
|
4
|
+
import { center, type Box, type Point } from "../geom.ts";
|
|
5
|
+
import type { Side } from "../model.ts";
|
|
6
|
+
import { SIDE_NORMAL } from "../plugs.ts";
|
|
7
|
+
|
|
8
|
+
/** This function gives the axis along a side. */
|
|
9
|
+
export function alongOf(side: Side): "x" | "y" {
|
|
10
|
+
return side === "east" || side === "west" ? "y" : "x";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** This function gives the room of a wall: the segment of the axis along the side, without the corner insets. */
|
|
14
|
+
export function roomOf(box: Box, side: Side, inset: number): [number, number] {
|
|
15
|
+
const along = alongOf(side);
|
|
16
|
+
const start = along === "y" ? box.y : box.x;
|
|
17
|
+
const len = along === "y" ? box.h : box.w;
|
|
18
|
+
const m = Math.min(inset, len / 4);
|
|
19
|
+
return [start + m, start + len - m];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** This function gives the point of a side at `v` on the axis along the side. */
|
|
23
|
+
export function pointAt(box: Box, side: Side, v: number): Point {
|
|
24
|
+
if (side === "east") return { x: box.x + box.w, y: v };
|
|
25
|
+
if (side === "west") return { x: box.x, y: v };
|
|
26
|
+
if (side === "south") return { x: v, y: box.y + box.h };
|
|
27
|
+
return { x: v, y: box.y };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 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.
|
|
32
|
+
* 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.
|
|
33
|
+
*/
|
|
34
|
+
export function keyOf(box: Box, side: Side, far: Point): number {
|
|
35
|
+
const c = center(box);
|
|
36
|
+
const n = SIDE_NORMAL[side];
|
|
37
|
+
const dx = far.x - c.x;
|
|
38
|
+
const dy = far.y - c.y;
|
|
39
|
+
const out = dx * n.x + dy * n.y;
|
|
40
|
+
const along = alongOf(side) === "y" ? dy : dx;
|
|
41
|
+
// A pseudo-angle with the order of the angle: a sum and one division, with no transcendental call. It goes from -2 to 2.
|
|
42
|
+
const size = Math.abs(out) + Math.abs(along);
|
|
43
|
+
if (size === 0) return 0;
|
|
44
|
+
const p = along / size;
|
|
45
|
+
if (out >= 0) return p;
|
|
46
|
+
return along >= 0 ? 2 - p : -2 - p;
|
|
47
|
+
}
|
package/src/plugs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { center, type Box, type Point } from "./geom.ts";
|
|
2
2
|
import { fixedPortPoint, type End, type Node, type Side } from "./model.ts";
|
|
3
|
+
import { clampToRange } from "./plugs/range.ts";
|
|
3
4
|
|
|
4
5
|
/** A plug: the point where a route leaves or enters a node, and the outward normal of its side. */
|
|
5
6
|
export interface Plug {
|
|
@@ -64,12 +65,13 @@ export function pointOnSide(box: Box, side: Side, toward: Point, inset = 6): Poi
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* This function places the plug of one end of an edge. A fixed plug stays at its point. A plug with a side stays on that side.
|
|
67
|
-
* The router places a free plug on the side that faces `toward`.
|
|
68
|
+
* The router places a free plug on the side that faces `toward`. A free plug of a port with a `range` stays in that range (`clampToRange`).
|
|
68
69
|
*/
|
|
69
70
|
export function placePlug(node: Node, end: End, toward: Point): Plug {
|
|
70
71
|
const fixed = fixedPortPoint(node, end);
|
|
71
72
|
const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
|
|
72
73
|
if (fixed) return { point: fixed, normal: SIDE_NORMAL[port?.side ?? nearestSide(node.box, fixed)] };
|
|
73
74
|
const side = port?.side ?? sideToward(node.box, toward);
|
|
74
|
-
|
|
75
|
+
const point = pointOnSide(node.box, side, toward);
|
|
76
|
+
return { point: port?.range === undefined ? point : clampToRange(node.box, side, port, point), normal: SIDE_NORMAL[side] };
|
|
75
77
|
}
|