@mark1russell7/highways 0.9.5 → 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 +22 -1
- package/build/index.d.ts +68 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +786 -65
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -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/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 +28 -42
- package/src/plugs/walls.ts +47 -0
- package/src/plugs.ts +4 -2
- package/src/strategies/builtins.ts +11 -1
- package/src/strategies/schemas.ts +7 -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,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,47 +47,6 @@ 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
|
-
/**
|
|
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
50
|
/**
|
|
88
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.
|
|
89
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.
|
|
@@ -128,6 +91,7 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
|
|
|
128
91
|
part = Math.max(part, p);
|
|
129
92
|
parts[part]!.push(s);
|
|
130
93
|
}
|
|
94
|
+
const ranged = slots.some((s) => s.range !== undefined);
|
|
131
95
|
parts.forEach((list, p) => {
|
|
132
96
|
if (list.length === 0) return;
|
|
133
97
|
const a = ends[p]!;
|
|
@@ -138,6 +102,27 @@ function placeWall(slots: readonly Slot[], fixed: readonly number[], lo: number,
|
|
|
138
102
|
const s = steps > 0 ? Math.max(0, Math.min(gap, (b - a) / steps)) : gap;
|
|
139
103
|
const lower = a + fa * s;
|
|
140
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
|
+
}
|
|
141
126
|
const at = placeInOrder(
|
|
142
127
|
list.map((x) => idealOf(x)),
|
|
143
128
|
lower,
|
|
@@ -205,7 +190,8 @@ export function spreadWith(input: PlugInput, options: SpreadOptions = {}, leader
|
|
|
205
190
|
if (slots.has(key)) return key;
|
|
206
191
|
const side = port?.side ?? sideToward(node.box, toward);
|
|
207
192
|
const ideal = placePlug(node, end, toward).point[alongOf(side)];
|
|
208
|
-
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 } : {}) };
|
|
209
195
|
slots.set(key, slot);
|
|
210
196
|
wallOf(node, side).slots.push(slot);
|
|
211
197
|
return key;
|
|
@@ -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
|
}
|
|
@@ -83,6 +83,7 @@ import { facingEnds } from "../plugs/facing/index.ts";
|
|
|
83
83
|
import { justifyEnds, type JustifyOptions } from "../plugs/justify.ts";
|
|
84
84
|
import { sideEnds, type SidePlugOptions } from "../plugs/side.ts";
|
|
85
85
|
import { SPREAD_DEFAULTS, spreadEnds, type SpreadOptions } from "../plugs/spread/index.ts";
|
|
86
|
+
import { ASSIGN_DEFAULTS, assignEnds, type AssignOptions } from "../plugs/assign/index.ts";
|
|
86
87
|
import { LEADER_DEFAULTS, leaderEnds, type LeaderOptions } from "../plugs/leaders/index.ts";
|
|
87
88
|
import { roomDemand, type RoomMethod, type RoomOptions } from "../room/index.ts";
|
|
88
89
|
import { levelRoom, type LevelRoomOptions } from "../room/levels/index.ts";
|
|
@@ -103,7 +104,7 @@ import { TRACING_DEFAULTS, tracing, type TracingOptions } from "../whole/tracing
|
|
|
103
104
|
import { createRegistry, defineStrategy, type AnyStrategy, type CurveInput, type LaneBoard, type Registry, type Strategy } from "./model.ts";
|
|
104
105
|
import type { OptionSchema } from "./options.ts";
|
|
105
106
|
import { AREA_SCHEMA, CONFLUENT_SCHEMA, CROSSBAR_SCHEMA, FRECHET_SCHEMA, MLSEB_SCHEMA, POWER_SCHEMA, SBEB_SCHEMA } from "./schemas.ts";
|
|
106
|
-
import { DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA, 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, KNEE_SCHEMA, BRONCHIAL_SCHEMA, BOARD_PORTFOLIO_SCHEMA, VENATION_SCHEMA, BALANCE_SCHEMA, MANIFOLD_SCHEMA, LEADER_SCHEMA, DETAIL_SCHEMA, CURRICULUM_SCHEMA, SLOT_SCHEMA, PREFERENCE_SCHEMA, FIREFLY_SCHEMA, TRACING_SCHEMA } from "./schemas.ts";
|
|
107
|
+
import { ASSIGN_SCHEMA, DEPTHS_SCHEMA, JUSTIFY_SCHEMA, PIPELINE_SCHEMA, SIDE_PLUG_SCHEMA, SPREAD_SCHEMA, 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, KNEE_SCHEMA, BRONCHIAL_SCHEMA, BOARD_PORTFOLIO_SCHEMA, VENATION_SCHEMA, BALANCE_SCHEMA, MANIFOLD_SCHEMA, LEADER_SCHEMA, DETAIL_SCHEMA, CURRICULUM_SCHEMA, SLOT_SCHEMA, PREFERENCE_SCHEMA, FIREFLY_SCHEMA, TRACING_SCHEMA } from "./schemas.ts";
|
|
107
108
|
|
|
108
109
|
/** The options of a router strategy: the options of its router, without the name and the limit of the benchmark. */
|
|
109
110
|
export type RouterPreset<O> = Omit<O, "name" | "maxNodes">;
|
|
@@ -1026,6 +1027,7 @@ export const plugStrategies: {
|
|
|
1026
1027
|
readonly justify: Strategy<"plugs", JustifyOptions>;
|
|
1027
1028
|
readonly spread: Strategy<"plugs", SpreadOptions>;
|
|
1028
1029
|
readonly leaders: Strategy<"plugs", LeaderOptions>;
|
|
1030
|
+
readonly assign: Strategy<"plugs", AssignOptions>;
|
|
1029
1031
|
} = Object.freeze({
|
|
1030
1032
|
facing: defineStrategy({
|
|
1031
1033
|
step: "plugs",
|
|
@@ -1067,6 +1069,14 @@ export const plugStrategies: {
|
|
|
1067
1069
|
schema: LEADER_SCHEMA,
|
|
1068
1070
|
run: (input, o) => leaderEnds(input, o),
|
|
1069
1071
|
}),
|
|
1072
|
+
assign: defineStrategy<"plugs", AssignOptions>({
|
|
1073
|
+
step: "plugs",
|
|
1074
|
+
name: "assign",
|
|
1075
|
+
describe: "PLUG-ASSIGN. The plugs of each wall go into the ranges of their ports with a minimum gap. The most ends are in their range, then the order of the far ends.",
|
|
1076
|
+
defaults: { ...ASSIGN_DEFAULTS },
|
|
1077
|
+
schema: ASSIGN_SCHEMA,
|
|
1078
|
+
run: (input, o) => assignEnds(input, o),
|
|
1079
|
+
}),
|
|
1070
1080
|
});
|
|
1071
1081
|
|
|
1072
1082
|
/** The built-in level rules: the strategies of the step `levels`. The router `sections` makes its zoom levels with its option `level`. */
|
|
@@ -9,6 +9,7 @@ import type { CutOptions } from "../groups/cut/index.ts";
|
|
|
9
9
|
import type { JustifyOptions } from "../plugs/justify.ts";
|
|
10
10
|
import type { SidePlugOptions } from "../plugs/side.ts";
|
|
11
11
|
import type { SpreadOptions } from "../plugs/spread/index.ts";
|
|
12
|
+
import type { AssignOptions } from "../plugs/assign/index.ts";
|
|
12
13
|
import type { LeaderOptions } from "../plugs/leaders/index.ts";
|
|
13
14
|
import type { BalanceOptions } from "../highways/balance/index.ts";
|
|
14
15
|
import type { ManifoldOptions } from "../highways/manifolds/index.ts";
|
|
@@ -977,6 +978,12 @@ export const SPREAD_SCHEMA: OptionSchema<SpreadOptions> = {
|
|
|
977
978
|
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." },
|
|
978
979
|
};
|
|
979
980
|
|
|
981
|
+
/** The declarations of the options of the plug strategy `assign` (PLUG-ASSIGN). */
|
|
982
|
+
export const ASSIGN_SCHEMA: OptionSchema<AssignOptions> = {
|
|
983
|
+
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." },
|
|
984
|
+
inset: { type: "number", min: 0, max: 12, step: 1, default: 6, describe: "A plug stays this far from the corners of its wall." },
|
|
985
|
+
};
|
|
986
|
+
|
|
980
987
|
/** The declarations of the options of the whole strategy `pipeline`. */
|
|
981
988
|
export const PIPELINE_SCHEMA: OptionSchema<PipelineOptions> = {
|
|
982
989
|
router: { type: "strategy", step: "router", describe: "The router. A highway plan takes its place." },
|