@mark1russell7/highways 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -8
- package/build/index.d.ts +1774 -236
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9307 -3090
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +145 -0
- package/src/bench/curves.ts +514 -2
- package/src/bench/links.ts +28 -6
- package/src/bench/measures/bundles.ts +119 -0
- package/src/bench/measures/curves.ts +211 -0
- package/src/bench/measures/groups.ts +122 -0
- package/src/bench/measures/index.ts +9 -0
- package/src/bench/measures/levels.ts +103 -0
- package/src/bench/measures/pareto.ts +37 -0
- package/src/bench/measures/room.ts +43 -0
- package/src/bench/measures/routes.ts +226 -0
- package/src/bench/measures/stability.ts +104 -0
- package/src/bench/measures/stats.ts +114 -0
- package/src/bench/properties.ts +18 -2
- package/src/bench/room.ts +209 -2
- package/src/bench/run.ts +86 -11
- package/src/bench/sections.ts +121 -0
- package/src/bench/suite.ts +781 -0
- package/src/cases.ts +41 -5
- package/src/curves/clothoid/index.ts +150 -0
- package/src/curves/corner.ts +116 -0
- package/src/curves/fit.ts +79 -0
- package/src/curves/highway/index.ts +636 -0
- package/src/curves/index.ts +16 -1
- package/src/curves/model.ts +4 -2
- package/src/curves/smooth/index.ts +50 -0
- package/src/curves/space.ts +76 -0
- package/src/groups/cut/index.ts +292 -0
- package/src/groups/index.ts +4 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/groups/keys/index.ts +81 -0
- package/src/groups/quadtree/index.ts +10 -0
- package/src/index.ts +7 -0
- package/src/levels.ts +178 -9
- package/src/model.ts +2 -0
- package/src/pick.ts +36 -0
- package/src/room/bends.ts +203 -0
- package/src/room/index.ts +28 -0
- package/src/room/levels/index.ts +155 -0
- package/src/routers/sections.ts +78 -55
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +93 -4
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +153 -7
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +29 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Box } from "../../geom.ts";
|
|
2
|
+
import type { Graph, Node } from "../../model.ts";
|
|
3
|
+
import { buildSections, type Section, type SectionOptions } from "../../sections.ts";
|
|
4
|
+
|
|
5
|
+
/** A key rule gives the key of the group of a node. A node without a key is a group alone. */
|
|
6
|
+
export type NodeKey = (node: Node, index: number) => string | undefined;
|
|
7
|
+
|
|
8
|
+
/** The options of the grouping rule `keys`. */
|
|
9
|
+
export interface KeyOptions {
|
|
10
|
+
/** This function gives the key of the group of each node. The default is `byDataGroup`. */
|
|
11
|
+
readonly key?: NodeKey;
|
|
12
|
+
/**
|
|
13
|
+
* With this value, a group with more nodes than its `cap` splits into the cells of a quadtree, as `buildSections` does.
|
|
14
|
+
* Without it, each group is one leaf. The default is no split.
|
|
15
|
+
*/
|
|
16
|
+
readonly split?: SectionOptions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** This key rule gives the string `data.group` of the node, or `undefined` when the node has no such string. */
|
|
20
|
+
export const byDataGroup: NodeKey = (node) => {
|
|
21
|
+
const group = node.data?.["group"];
|
|
22
|
+
return typeof group === "string" ? group : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** This function gives the bounding box of the boxes of the nodes of the list. */
|
|
26
|
+
function boundsOf(nodes: readonly Node[], list: readonly number[]): Box {
|
|
27
|
+
let x0 = Infinity;
|
|
28
|
+
let y0 = Infinity;
|
|
29
|
+
let x1 = -Infinity;
|
|
30
|
+
let y1 = -Infinity;
|
|
31
|
+
for (const i of list) {
|
|
32
|
+
const b = nodes[i]!.box;
|
|
33
|
+
x0 = Math.min(x0, b.x);
|
|
34
|
+
y0 = Math.min(y0, b.y);
|
|
35
|
+
x1 = Math.max(x1, b.x + b.w);
|
|
36
|
+
y1 = Math.max(y1, b.y + b.h);
|
|
37
|
+
}
|
|
38
|
+
return list.length ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : { x: 0, y: 0, w: 0, h: 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This function gives the tree of sections of the grouping rule `keys`. The root holds each node. Each child of the root holds the nodes of one key,
|
|
43
|
+
* in the order of their first node. The id of a group is `s:` and its key. A node without a key is a group alone, with the id `s#` and its index.
|
|
44
|
+
* Thus the far view of the router `sections` shows one item for each group.
|
|
45
|
+
*/
|
|
46
|
+
export function keyedSections(nodes: readonly Node[], options: KeyOptions = {}): Section {
|
|
47
|
+
const key = options.key ?? byDataGroup;
|
|
48
|
+
const groups = new Map<string, number[]>();
|
|
49
|
+
nodes.forEach((n, i) => {
|
|
50
|
+
const k = key(n, i);
|
|
51
|
+
const id = k === undefined ? `s#${i}` : `s:${k}`;
|
|
52
|
+
const list = groups.get(id);
|
|
53
|
+
if (list) list.push(i);
|
|
54
|
+
else groups.set(id, [i]);
|
|
55
|
+
});
|
|
56
|
+
const children = [...groups].map(([id, list]) => group(nodes, id, list, options.split));
|
|
57
|
+
return { id: "s", depth: 0, cell: boundsOf(nodes, nodes.map((_, i) => i)), nodes: nodes.map((_, i) => i), children };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function gives the section of one group at depth 1. With `split`, the section has the quadtree of its nodes below it. */
|
|
61
|
+
function group(nodes: readonly Node[], id: string, list: readonly number[], split: SectionOptions | undefined): Section {
|
|
62
|
+
if (!split) return { id, depth: 1, cell: boundsOf(nodes, list), nodes: list, children: [] };
|
|
63
|
+
const tree = buildSections(
|
|
64
|
+
list.map((i) => nodes[i]!),
|
|
65
|
+
split,
|
|
66
|
+
);
|
|
67
|
+
// The quadtree numbers the nodes of the group from 0. Each section gets the indices of the graph, one more level and the id of the group before its path.
|
|
68
|
+
const rebase = (s: Section): Section => ({
|
|
69
|
+
id: s.id === "s" ? id : `${id}/${s.id.slice(1)}`,
|
|
70
|
+
depth: s.depth + 1,
|
|
71
|
+
cell: s.cell,
|
|
72
|
+
nodes: s.nodes.map((j) => list[j]!),
|
|
73
|
+
children: s.children.map(rebase),
|
|
74
|
+
});
|
|
75
|
+
return rebase(tree);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** This function gives the tree of sections of the grouping rule `keys` for a graph. */
|
|
79
|
+
export function keyGroups(graph: Graph, options: KeyOptions = {}): Section {
|
|
80
|
+
return keyedSections(graph.nodes, options);
|
|
81
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Graph } from "../../model.ts";
|
|
2
|
+
import { buildSections, type Section, type SectionOptions } from "../../sections.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This function gives the tree of sections of the grouping rule `quadtree`: the cells of a quadtree of the plane (`buildSections`).
|
|
6
|
+
* It is the default grouping rule of the router `sections`.
|
|
7
|
+
*/
|
|
8
|
+
export function quadtreeGroups(graph: Graph, options: SectionOptions = {}): Section {
|
|
9
|
+
return buildSections(graph.nodes, options);
|
|
10
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,13 +10,20 @@ export * from "./visibility.ts";
|
|
|
10
10
|
export * from "./ways.ts";
|
|
11
11
|
export * from "./segments.ts";
|
|
12
12
|
export * from "./sections.ts";
|
|
13
|
+
export * from "./groups/index.ts";
|
|
14
|
+
export * from "./pick.ts";
|
|
13
15
|
export * from "./routers/index.ts";
|
|
14
16
|
export * from "./room/index.ts";
|
|
15
17
|
export * from "./room/lanes.ts";
|
|
18
|
+
export * from "./room/levels/index.ts";
|
|
16
19
|
export * from "./bench/properties.ts";
|
|
17
20
|
export * from "./bench/quality.ts";
|
|
18
21
|
export * from "./bench/links.ts";
|
|
19
22
|
export * from "./bench/run.ts";
|
|
20
23
|
export * from "./bench/curves.ts";
|
|
21
24
|
export * from "./bench/room.ts";
|
|
25
|
+
export * from "./bench/sections.ts";
|
|
26
|
+
export * from "./bench/measures/index.ts";
|
|
27
|
+
export * from "./bench/suite.ts";
|
|
22
28
|
export * from "./strategies/index.ts";
|
|
29
|
+
export * from "./bench/compare.ts";
|
package/src/levels.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { boxesOverlap, goesOutAndBack } from "./bench/links.ts";
|
|
1
2
|
import { center, dist, polylineLength, type Box, type Point } from "./geom.ts";
|
|
2
3
|
import type { LevelItem, LevelLink, Node, Side } from "./model.ts";
|
|
3
4
|
import { pointOnSide, SIDE_NORMAL, sideToward, type Plug } from "./plugs.ts";
|
|
4
|
-
import { CornerGraph, Obstacles } from "./visibility.ts";
|
|
5
|
+
import { CornerGraph, Obstacles, SoftGraph } from "./visibility.ts";
|
|
5
6
|
import type { WayFinder, WayOptions } from "./ways.ts";
|
|
6
7
|
|
|
7
8
|
/** Two items of a level that a link joins, and the count of the edges between them. */
|
|
@@ -26,18 +27,54 @@ interface LevelWays extends Ways {
|
|
|
26
27
|
readonly tight: () => CornerGraph;
|
|
27
28
|
/** The free plugs of each node that a link needed. */
|
|
28
29
|
readonly free: Map<Node, FreePlugs>;
|
|
30
|
+
/** This function gives the graph of `SoftGraph`. The first link that needs it makes it. */
|
|
31
|
+
readonly soft: () => SoftGraph;
|
|
32
|
+
/** The mean size of the boxes: the mean of the larger side of each box. */
|
|
33
|
+
readonly size: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
/** The
|
|
32
|
-
|
|
36
|
+
/** The options of the links of a level: the options of the ways, and two limits of the level links. */
|
|
37
|
+
export interface LevelLinkOptions extends WayOptions {
|
|
38
|
+
/**
|
|
39
|
+
* The margin of the second corner graph. A gap between two boxes wider than approximately 1.5 times this margin lets a link through.
|
|
40
|
+
* The second graph uses the smaller of this value and `margin`. The default is 1.
|
|
41
|
+
*/
|
|
42
|
+
readonly tightMargin?: number;
|
|
43
|
+
/** A plug stays at least this far from the corners of its side. The level links use the smaller of this value and `margin`. The default is 6. */
|
|
44
|
+
readonly plugInset?: number;
|
|
45
|
+
/**
|
|
46
|
+
* The cost of a level link through a far box, in units of the mean box size. A far box overlaps neither end of the link.
|
|
47
|
+
* A link through a far box gets a second way that can go through boxes (`SoftGraph`). The link keeps the way with fewer far boxes.
|
|
48
|
+
* The value 0 turns the second way off and gives the links of version 0.1.5. The default is 8.
|
|
49
|
+
*/
|
|
50
|
+
readonly throughCost?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The default values of the three limits of `LevelLinkOptions`. */
|
|
54
|
+
export const LEVEL_LINK_DEFAULTS: Readonly<Required<Pick<LevelLinkOptions, "tightMargin" | "plugInset" | "throughCost">>> = Object.freeze({
|
|
55
|
+
tightMargin: 1,
|
|
56
|
+
plugInset: 6,
|
|
57
|
+
throughCost: 8,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** The values of `throughCost` of the sweep of ER3d in the benchmark. The value 0 gives the links of version 0.1.5. */
|
|
61
|
+
export const THROUGH_COST_SWEEP: readonly number[] = [0, 2, 4, 8, 16];
|
|
33
62
|
|
|
34
63
|
const EPS = 1e-6;
|
|
35
64
|
|
|
36
65
|
/**
|
|
37
66
|
* This function removes the repeated points, the points in the middle of a straight run,
|
|
38
67
|
* and the points where the way turns back on itself.
|
|
68
|
+
* A way of two points or more keeps two points: its first point and its last point.
|
|
69
|
+
* Two items that touch can get a link of zero length, and such a link keeps an end on each item.
|
|
39
70
|
*/
|
|
40
71
|
function tidy(p: readonly Point[]): Point[] {
|
|
72
|
+
const out = tidyPoints(p);
|
|
73
|
+
return out.length === 1 && p.length > 1 ? [p[0]!, p[p.length - 1]!] : out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** This function does the work of `tidy`. It can give one point only. */
|
|
77
|
+
function tidyPoints(p: readonly Point[]): Point[] {
|
|
41
78
|
const out: Point[] = [];
|
|
42
79
|
for (const q of p) {
|
|
43
80
|
if (out.length > 0 && dist(q, out[out.length - 1]!) <= EPS) continue;
|
|
@@ -60,12 +97,13 @@ function tidy(p: readonly Point[]): Point[] {
|
|
|
60
97
|
* A link starts on the boundary of the box of its source and ends on the boundary of the box of its target.
|
|
61
98
|
* It goes around the boxes of the other items. Where there is space, its legs are parallel to the axes.
|
|
62
99
|
*/
|
|
63
|
-
export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair[], options:
|
|
100
|
+
export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair[], options: LevelLinkOptions = {}): LevelLink[] {
|
|
64
101
|
if (pairs.length === 0) return [];
|
|
65
102
|
const nodes: Node[] = items.map((i) => ({ id: i.id, box: i.box }));
|
|
66
103
|
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
67
104
|
const ways = levelWays(nodes, options);
|
|
68
|
-
const inset = Math.min(
|
|
105
|
+
const inset = Math.min(options.plugInset ?? LEVEL_LINK_DEFAULTS.plugInset, ways.margin);
|
|
106
|
+
const throughCost = options.throughCost ?? LEVEL_LINK_DEFAULTS.throughCost;
|
|
69
107
|
const out: LevelLink[] = [];
|
|
70
108
|
for (const p of pairs) {
|
|
71
109
|
const s = byId.get(p.source);
|
|
@@ -78,7 +116,16 @@ export function levelLinks(items: readonly LevelItem[], pairs: readonly ItemPair
|
|
|
78
116
|
points ??= sideWay(ways, s, t, inset);
|
|
79
117
|
out.push({ source: p.source, target: p.target, count: p.count, points });
|
|
80
118
|
}
|
|
81
|
-
return out;
|
|
119
|
+
if (throughCost <= 0) return out;
|
|
120
|
+
// The second way never makes the worst detour of the level longer.
|
|
121
|
+
const limit = out.reduce((m, l) => Math.max(m, detour(l.points)), 1);
|
|
122
|
+
return out.map((l) => ({ ...l, points: fewerFar(ways, byId.get(l.source)!, byId.get(l.target)!, l.points, throughCost, limit) }));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** This function gives the length of the polyline divided by the distance between its ends, as the measure `worstDetour` gives it. */
|
|
126
|
+
function detour(p: readonly Point[]): number {
|
|
127
|
+
const span = p.length > 1 ? dist(p[0]!, p[p.length - 1]!) : 0;
|
|
128
|
+
return span > 1 ? polylineLength(p) / span : 1;
|
|
82
129
|
}
|
|
83
130
|
|
|
84
131
|
/**
|
|
@@ -101,7 +148,7 @@ function sideWay(ways: LevelWays, s: Node, t: Node, inset: number): Point[] {
|
|
|
101
148
|
* The buckets of the obstacles grow with the items, so a long link visits few buckets.
|
|
102
149
|
* The corner graphs use `cones`, so a link goes along the corridors between the groups, not around the outer edge of the board.
|
|
103
150
|
*/
|
|
104
|
-
function levelWays(nodes: readonly Node[], options:
|
|
151
|
+
function levelWays(nodes: readonly Node[], options: LevelLinkOptions): LevelWays {
|
|
105
152
|
const margin = options.margin ?? 5;
|
|
106
153
|
const k = options.k ?? 8;
|
|
107
154
|
let size = 0;
|
|
@@ -110,8 +157,130 @@ function levelWays(nodes: readonly Node[], options: WayOptions): LevelWays {
|
|
|
110
157
|
const corners = new CornerGraph(nodes, obstacles, { margin, k, cones: true });
|
|
111
158
|
const out = (p: Plug): Point => ({ x: p.point.x + p.normal.x * margin, y: p.point.y + p.normal.y * margin });
|
|
112
159
|
let tight: CornerGraph | undefined;
|
|
113
|
-
const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin,
|
|
114
|
-
|
|
160
|
+
const tightGraph = (): CornerGraph => (tight ??= new CornerGraph(nodes, obstacles, { margin: Math.min(margin, options.tightMargin ?? LEVEL_LINK_DEFAULTS.tightMargin), k, cones: true }));
|
|
161
|
+
let soft: SoftGraph | undefined;
|
|
162
|
+
const softGraph = (): SoftGraph => (soft ??= new SoftGraph(nodes, obstacles, { margin, k }));
|
|
163
|
+
return { obstacles, corners, margin, out, tight: tightGraph, free: new Map(), soft: softGraph, size: size / Math.max(1, nodes.length) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** This function gives the boxes that the polyline goes through, without the boxes of `s` and `t`, and the number of far boxes among them. */
|
|
167
|
+
function boxesThrough(ways: Ways, p: readonly Point[], s: Node, t: Node, near: ReadonlySet<Node>): { boxes: Set<Node>; far: number } {
|
|
168
|
+
const boxes = new Set<Node>();
|
|
169
|
+
let far = 0;
|
|
170
|
+
for (let i = 1; i < p.length; i++)
|
|
171
|
+
ways.obstacles.through(p[i - 1]!, p[i]!, (n) => {
|
|
172
|
+
if (n === s || n === t || boxes.has(n)) return;
|
|
173
|
+
boxes.add(n);
|
|
174
|
+
if (!near.has(n)) far++;
|
|
175
|
+
});
|
|
176
|
+
return { boxes, far };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** This function gives the nodes whose boxes overlap the box of `s` or the box of `t`, as `boxesOverlap` sees it. */
|
|
180
|
+
function nearBoxes(ways: Ways, s: Node, t: Node): Set<Node> {
|
|
181
|
+
const near = new Set<Node>();
|
|
182
|
+
for (const end of [s, t])
|
|
183
|
+
ways.obstacles.query(end.box, (n) => {
|
|
184
|
+
if (n !== s && n !== t && boxesOverlap(n.box, end.box)) near.add(n);
|
|
185
|
+
});
|
|
186
|
+
return near;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* This function gives a way with fewer far boxes than `way`, or `way`. A far box overlaps neither end of the link.
|
|
191
|
+
* On a walled-in level, each box overlaps its neighbours, so a link can find no way around the boxes.
|
|
192
|
+
* Then a search of `SoftGraph` goes from the center of `s` to the center of `t`.
|
|
193
|
+
*
|
|
194
|
+
* In the search, a far box costs `cost` mean box sizes, and a near box costs one mean box size.
|
|
195
|
+
* The way starts where the path leaves the box of `s` for the last time. It ends where the path then first enters the box of `t`.
|
|
196
|
+
* The link keeps `way` when the new way goes out and back and `way` does not.
|
|
197
|
+
* It also keeps `way` when the detour of the new way is longer than `limit`.
|
|
198
|
+
*/
|
|
199
|
+
function fewerFar(ways: LevelWays, s: Node, t: Node, way: readonly Point[], cost: number, limit: number): readonly Point[] {
|
|
200
|
+
const near = nearBoxes(ways, s, t);
|
|
201
|
+
const before = boxesThrough(ways, way, s, t, near);
|
|
202
|
+
if (before.far === 0) return way;
|
|
203
|
+
const far = cost * ways.size;
|
|
204
|
+
const path = ways.soft().path(center(s.box), center(t.box), (n) => (n === s || n === t ? 0 : near.has(n) ? ways.size : far));
|
|
205
|
+
const clipped = clip(path, s.box, t.box);
|
|
206
|
+
if (!clipped) return way;
|
|
207
|
+
const soft = tidy(pullThrough(ways, clipped, s, t));
|
|
208
|
+
// A way that goes out and back is a new fault, so the link keeps the old way.
|
|
209
|
+
if (goesOutAndBack(soft) && !goesOutAndBack(way)) return way;
|
|
210
|
+
if (detour(soft) > limit) return way;
|
|
211
|
+
return boxesThrough(ways, soft, s, t, near).far < before.far ? soft : way;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** This function tells if the point is inside the box, not on its boundary. */
|
|
215
|
+
const within = (p: Point, b: Box): boolean => p.x > b.x && p.x < b.x + b.w && p.y > b.y && p.y < b.y + b.h;
|
|
216
|
+
|
|
217
|
+
/** This function gives the point where the segment from `a`, inside the box, to `b` leaves the box. */
|
|
218
|
+
function exitPoint(a: Point, b: Point, box: Box): Point {
|
|
219
|
+
const dx = b.x - a.x;
|
|
220
|
+
const dy = b.y - a.y;
|
|
221
|
+
let u = 1;
|
|
222
|
+
if (dx > 0) u = Math.min(u, (box.x + box.w - a.x) / dx);
|
|
223
|
+
if (dx < 0) u = Math.min(u, (box.x - a.x) / dx);
|
|
224
|
+
if (dy > 0) u = Math.min(u, (box.y + box.h - a.y) / dy);
|
|
225
|
+
if (dy < 0) u = Math.min(u, (box.y - a.y) / dy);
|
|
226
|
+
u = Math.max(0, u);
|
|
227
|
+
return { x: a.x + u * dx, y: a.y + u * dy };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* This function cuts a path from the inside of the box `a` to the inside of the box `b`. The cut path starts where the path leaves `a` for the last time.
|
|
232
|
+
* It ends where the path then first enters `b`. It gives `undefined` when no such part is left.
|
|
233
|
+
*/
|
|
234
|
+
function clip(p: readonly Point[], a: Box, b: Box): Point[] | undefined {
|
|
235
|
+
let i = -1;
|
|
236
|
+
for (let k = 0; k < p.length; k++) if (within(p[k]!, a)) i = k;
|
|
237
|
+
if (i < 0 || i === p.length - 1) return undefined;
|
|
238
|
+
let j = -1;
|
|
239
|
+
for (let k = i + 1; k < p.length && j < 0; k++) if (within(p[k]!, b)) j = k;
|
|
240
|
+
if (j < 0) return undefined;
|
|
241
|
+
const start = exitPoint(p[i]!, p[i + 1]!, a);
|
|
242
|
+
const end = exitPoint(p[j]!, p[j - 1]!, b);
|
|
243
|
+
if (within(start, b) || within(end, a)) return undefined;
|
|
244
|
+
return [start, ...p.slice(i + 1, j), end];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* This function pulls a soft path tight. From each vertex, it goes to the farthest vertex of the next 16 vertices with a straight line.
|
|
249
|
+
* The line can go only through the boxes that the path between the two vertices goes through.
|
|
250
|
+
*/
|
|
251
|
+
function pullThrough(ways: Ways, p: readonly Point[], s: Node, t: Node, lookahead = 16): Point[] {
|
|
252
|
+
if (p.length < 3) return [...p];
|
|
253
|
+
const boxesOf = (a: Point, b: Point): Node[] => {
|
|
254
|
+
const out: Node[] = [];
|
|
255
|
+
ways.obstacles.through(a, b, (n) => {
|
|
256
|
+
if (n !== s && n !== t) out.push(n);
|
|
257
|
+
});
|
|
258
|
+
return out;
|
|
259
|
+
};
|
|
260
|
+
// The boxes of each leg of the path, found one time.
|
|
261
|
+
const legs = p.map((q, m) => (m === 0 ? [] : boxesOf(p[m - 1]!, q)));
|
|
262
|
+
const out: Point[] = [p[0]!];
|
|
263
|
+
let i = 0;
|
|
264
|
+
while (i < p.length - 1) {
|
|
265
|
+
let next = i + 1;
|
|
266
|
+
const last = Math.min(p.length - 1, i + lookahead);
|
|
267
|
+
// The boxes of the path from vertex i to each vertex j.
|
|
268
|
+
const along: Set<Node>[] = [];
|
|
269
|
+
let seen = new Set<Node>();
|
|
270
|
+
for (let j = i + 1; j <= last; j++) {
|
|
271
|
+
seen = new Set([...seen, ...legs[j]!]);
|
|
272
|
+
along[j] = seen;
|
|
273
|
+
}
|
|
274
|
+
for (let j = last; j > i + 1; j--) {
|
|
275
|
+
if (boxesOf(p[i]!, p[j]!).every((n) => along[j]!.has(n))) {
|
|
276
|
+
next = j;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
out.push(p[next]!);
|
|
281
|
+
i = next;
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
115
284
|
}
|
|
116
285
|
|
|
117
286
|
/**
|
package/src/model.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface Node {
|
|
|
20
20
|
readonly id: string;
|
|
21
21
|
readonly box: Box;
|
|
22
22
|
readonly ports?: readonly Port[];
|
|
23
|
+
/** This data is free for the grouping rules of the nodes. The rule `keys` reads the string `data.group` by default. */
|
|
24
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** One end of an edge: a node, and optionally one plug of that node. */
|
package/src/pick.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Level } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
/** This function gives the median width of the shown items of each level, in units of the plane. */
|
|
4
|
+
export function itemSizes(levels: readonly Level[]): number[] {
|
|
5
|
+
return levels.map((level) => {
|
|
6
|
+
const sizes = level.items.map((i) => Math.max(i.box.w, i.box.h)).sort((a, b) => a - b);
|
|
7
|
+
return sizes.length ? sizes[Math.floor(sizes.length / 2)]! : 0;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The far-view thresholds of `pickLevel`, in pixels on the screen. */
|
|
12
|
+
export interface PickOptions {
|
|
13
|
+
/** When a node is at least this wide on the screen, the view shows each route. */
|
|
14
|
+
readonly routesAt: number;
|
|
15
|
+
/** A level is fine enough when its median item is at least this wide on the screen. */
|
|
16
|
+
readonly itemAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The default thresholds: a node of 22 pixels shows its routes, and a group of 120 pixels shows its parts. */
|
|
20
|
+
export const PICK: PickOptions = Object.freeze({ routesAt: 22, itemAt: 120 });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This function picks the level that a camera shows at a scale, in pixels for each unit of the plane. The sizes come from `itemSizes`.
|
|
24
|
+
* The value `sizes.length` stands for the routes of each edge. The last level holds the nodes alone.
|
|
25
|
+
* Each threshold that `options` does not give gets its value from `PICK`.
|
|
26
|
+
*/
|
|
27
|
+
export function pickLevel(sizes: readonly number[], scale: number, options: Partial<PickOptions> = PICK): number {
|
|
28
|
+
const routesAt = options.routesAt ?? PICK.routesAt;
|
|
29
|
+
const itemAt = options.itemAt ?? PICK.itemAt;
|
|
30
|
+
const n = sizes.length;
|
|
31
|
+
if (n === 0) return 0;
|
|
32
|
+
if (sizes[n - 1]! * scale >= routesAt) return n;
|
|
33
|
+
let pick = 0;
|
|
34
|
+
for (let d = 0; d < n - 1; d++) if (sizes[d]! * scale >= itemAt) pick = d;
|
|
35
|
+
return pick;
|
|
36
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Box, Point } from "../geom.ts";
|
|
2
|
+
import { Grid } from "../grid.ts";
|
|
3
|
+
import type { Edge, Router } from "../model.ts";
|
|
4
|
+
import { segmentsOf, type SegmentGraph } from "../segments.ts";
|
|
5
|
+
import type { Boxed, Gap, RoomLink } from "./index.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The options of the room for the bends of the bundles (HWC-D). With them, `roomDemand` also opens the gaps where a bundle bends.
|
|
9
|
+
* The values agree with the options of `highwayCurves` that draw the bundles.
|
|
10
|
+
*/
|
|
11
|
+
export interface RoomBends {
|
|
12
|
+
/** The router that bundles the edges. Each segment of its routes with two or more edges is a bundle. */
|
|
13
|
+
readonly router: Router;
|
|
14
|
+
/** The edges that the router routes. Without them, each link gives `count` edges, with no data. */
|
|
15
|
+
readonly edges?: readonly Edge[];
|
|
16
|
+
/** The distance between two lanes of a bundle, as `laneWidth` of `highwayCurves`. The default is 4. */
|
|
17
|
+
readonly spacing?: number;
|
|
18
|
+
/** The largest width of a bundle, from its outer lane to its outer lane, as `maxWidth` of `highwayCurves`. The default is `Infinity`. */
|
|
19
|
+
readonly maxWidth?: number;
|
|
20
|
+
/** The smallest radius of each lane, as `minRadius` of `highwayCurves`. The default is 4. */
|
|
21
|
+
readonly minRadius?: number;
|
|
22
|
+
/** The number of passes of the demand of the bends. Each pass routes the board that the passes before it opened. The default is 1. */
|
|
23
|
+
readonly passes?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One leg of the route of an edge: a straight piece of the center line of one segment. */
|
|
27
|
+
interface BendLeg {
|
|
28
|
+
readonly a: Point;
|
|
29
|
+
readonly b: Point;
|
|
30
|
+
readonly u: Point;
|
|
31
|
+
readonly len: number;
|
|
32
|
+
/** The number of lanes of the segment of the leg. A segment of one edge has 1. */
|
|
33
|
+
readonly lanes: number;
|
|
34
|
+
/** The offset of the lane of the edge from the center line, to the left of the leg. */
|
|
35
|
+
readonly off: number;
|
|
36
|
+
/** The key of the leg: its segment and its place in the segment. Two edges of one segment share the key. */
|
|
37
|
+
readonly key: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A turn smaller than this angle, in radians, is no corner. A turn nearer to a half turn than this angle is a spike, and it gets no arc. */
|
|
41
|
+
const FLAT = 1e-6;
|
|
42
|
+
const SPIKE = 0.01;
|
|
43
|
+
const SAME = 1e-6;
|
|
44
|
+
/** A leg with a smaller share of its length on an axis does not grow on that axis. */
|
|
45
|
+
const SLANT = 0.2;
|
|
46
|
+
|
|
47
|
+
const turnOf = (u: Point, w: Point): number => Math.atan2(u.x * w.y - u.y * w.x, u.x * w.x + u.y * w.y);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function gives the length along a leg of an S of two arcs with the radius `r` that shifts a lane by `shift`.
|
|
51
|
+
* It is the length of the shift of `highwayCurves` at a merge or a split.
|
|
52
|
+
*/
|
|
53
|
+
function jogLength(r: number, shift: number): number {
|
|
54
|
+
const d = Math.abs(shift);
|
|
55
|
+
if (d < SAME || r <= 0) return 0;
|
|
56
|
+
if (r < d / 2) return 2 * r;
|
|
57
|
+
return 2 * r * Math.sin(Math.acos(1 - d / (2 * r)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function gives the legs of the route of each edge, with the lanes and the offset of the lane on each leg. */
|
|
61
|
+
function legsOf(graph: SegmentGraph, spacing: number, maxWidth: number): BendLeg[][] {
|
|
62
|
+
const byId = new Map(graph.segments.map((s) => [s.id, s]));
|
|
63
|
+
return graph.paths.map((path) => {
|
|
64
|
+
const legs: BendLeg[] = [];
|
|
65
|
+
for (const ref of path.segments) {
|
|
66
|
+
const s = byId.get(ref.segment);
|
|
67
|
+
if (!s) continue;
|
|
68
|
+
const sp = s.count > 1 ? Math.min(spacing, maxWidth / (s.count - 1)) : 0;
|
|
69
|
+
const offset = ((s.count - 1) / 2 - ref.lane) * sp;
|
|
70
|
+
const c: Point[] = [];
|
|
71
|
+
for (const p of s.points) if (c.length === 0 || Math.hypot(p.x - c[c.length - 1]!.x, p.y - c[c.length - 1]!.y) > SAME) c.push(p);
|
|
72
|
+
const n = c.length;
|
|
73
|
+
for (let j = 0; j + 1 < n; j++) {
|
|
74
|
+
const from = ref.reverse ? n - 1 - j : j;
|
|
75
|
+
const to = ref.reverse ? n - 2 - j : j + 1;
|
|
76
|
+
const a = c[from]!;
|
|
77
|
+
const b = c[to]!;
|
|
78
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
79
|
+
legs.push({
|
|
80
|
+
a,
|
|
81
|
+
b,
|
|
82
|
+
u: { x: (b.x - a.x) / len, y: (b.y - a.y) / len },
|
|
83
|
+
len,
|
|
84
|
+
lanes: sp > 0 ? s.count : 1,
|
|
85
|
+
off: ref.reverse ? -offset : offset,
|
|
86
|
+
key: `${s.id}:${Math.min(from, to)}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return legs;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* This function gives the length that the arcs at one end of a leg use on the leg, as the lanes of `highwayCurves` use it.
|
|
96
|
+
* `A` is the leg before the vertex, `B` the leg after it, and `X` the leg of the two that the length is for.
|
|
97
|
+
*
|
|
98
|
+
* - At a bend of two bundles, the center line has the radius `R = W/2 + minRadius`, with `W` the width of the wider bundle. The tangent length is `T = R tan(φ/2)`.
|
|
99
|
+
* Each lane uses the same part of the leg, because the arcs are concentric.
|
|
100
|
+
* - At another corner, the lane has the radius `minRadius` at its own corner, at the offset `c` of the lane. The tangent length is `(minRadius + |c|) tan(φ/2)`.
|
|
101
|
+
* - A lane that joins or leaves a bundle shifts on the leg with fewer lanes. The S of the shift adds its length on that leg.
|
|
102
|
+
*/
|
|
103
|
+
function endLength(A: BendLeg, B: BendLeg, X: BendLeg, minRadius: number): number {
|
|
104
|
+
const phi = Math.abs(turnOf(A.u, B.u));
|
|
105
|
+
if (Math.PI - phi < SPIKE) return 0;
|
|
106
|
+
const k = phi < FLAT ? 0 : Math.tan(phi / 2);
|
|
107
|
+
const c = A.lanes < B.lanes ? B.off : A.off;
|
|
108
|
+
if (A.lanes > 1 && B.lanes > 1) return k * (Math.max(Math.abs(A.off), Math.abs(B.off), Math.abs(c)) + minRadius);
|
|
109
|
+
return k * (minRadius + Math.abs(c)) + jogLength(minRadius, c - X.off);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* This function gives, for each leg of the routes, the length that the leg lacks for the arcs at its two ends.
|
|
114
|
+
* `highwayCurves` gives each end of a leg at most the half of the leg. So a leg needs `2 max(T_A, T_B)`, and this length is at least `T_A + T_B`.
|
|
115
|
+
* The first and the last leg of a route end at a plug, and the plug end needs no length.
|
|
116
|
+
*/
|
|
117
|
+
function shortLegs(legs: readonly (readonly BendLeg[])[], minRadius: number): Map<string, { leg: BendLeg; short: number }> {
|
|
118
|
+
const out = new Map<string, { leg: BendLeg; short: number }>();
|
|
119
|
+
for (const path of legs) {
|
|
120
|
+
if (path.every((l) => l.lanes < 2)) continue;
|
|
121
|
+
for (let j = 0; j < path.length; j++) {
|
|
122
|
+
const X = path[j]!;
|
|
123
|
+
const start = j > 0 ? endLength(path[j - 1]!, X, X, minRadius) : 0;
|
|
124
|
+
const end = j + 1 < path.length ? endLength(X, path[j + 1]!, X, minRadius) : 0;
|
|
125
|
+
const short = 2 * Math.max(start, end) - X.len;
|
|
126
|
+
if (short <= SAME) continue;
|
|
127
|
+
const known = out.get(X.key);
|
|
128
|
+
if (!known || known.short < short) out.set(X.key, { leg: X, short });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** This function gives the mean of the longer side of the boxes. */
|
|
135
|
+
function meanSide(items: readonly Boxed[]): number {
|
|
136
|
+
let s = 0;
|
|
137
|
+
for (const i of items) s += Math.max(i.box.w, i.box.h);
|
|
138
|
+
return items.length ? s / items.length : 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** This function gives the box nearest to a point, among the boxes within `reach` of it. */
|
|
142
|
+
function nearestBox(grid: Grid<Box>, p: Point, reach: number): Box | undefined {
|
|
143
|
+
let best: Box | undefined;
|
|
144
|
+
let bestD = Infinity;
|
|
145
|
+
grid.query({ x: p.x - reach, y: p.y - reach, w: 2 * reach, h: 2 * reach }, (b) => {
|
|
146
|
+
const dx = Math.max(b.x - p.x, 0, p.x - b.x - b.w);
|
|
147
|
+
const dy = Math.max(b.y - p.y, 0, p.y - b.y - b.h);
|
|
148
|
+
const d = Math.hypot(dx, dy);
|
|
149
|
+
if (d < bestD || (d === bestD && best && (b.x < best.x || (b.x === best.x && b.y < best.y)))) [best, bestD] = [b, d];
|
|
150
|
+
});
|
|
151
|
+
return bestD <= reach ? best : undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* This function gives the growth that each gap of the board needs for the bends of the bundles, in the order of `gaps`.
|
|
156
|
+
* The router routes the board. Each segment with two or more edges is a bundle, as `highwayCurves` opens it.
|
|
157
|
+
*
|
|
158
|
+
* A leg that is too short for the arcs at its ends grows by the gaps between its two ends.
|
|
159
|
+
* Each end of a leg moves with its nearest box, as `openGaps` moves the boxes. So a gap between the left (or top) edges of the two boxes lengthens the leg.
|
|
160
|
+
* The leg grows on the axis of its longer side. The gap nearest to the middle of the leg takes the growth.
|
|
161
|
+
* A gap that more legs need takes the largest growth, not the sum.
|
|
162
|
+
*/
|
|
163
|
+
export function bendGrowth(items: readonly Boxed[], links: readonly RoomLink[], gaps: readonly Gap[], bends: RoomBends, snap: number): number[] {
|
|
164
|
+
const spacing = Math.max(0, bends.spacing ?? 4);
|
|
165
|
+
const maxWidth = bends.maxWidth ?? Infinity;
|
|
166
|
+
const minRadius = Math.max(0, bends.minRadius ?? 4);
|
|
167
|
+
const edges: Edge[] = bends.edges
|
|
168
|
+
? [...bends.edges]
|
|
169
|
+
: links.flatMap((l, i) => Array.from({ length: Math.max(1, l.count ?? 1) }, (_, k) => ({ id: `l${i}-${k}`, source: { node: l.source }, target: { node: l.target } })));
|
|
170
|
+
const graph = segmentsOf(bends.router.route({ nodes: items, edges }));
|
|
171
|
+
const short = shortLegs(legsOf(graph, spacing, maxWidth), minRadius);
|
|
172
|
+
const grow: number[] = gaps.map(() => 0);
|
|
173
|
+
if (short.size === 0) return grow;
|
|
174
|
+
const side = meanSide(items);
|
|
175
|
+
const grid = new Grid<Box>(Math.max(20, side));
|
|
176
|
+
for (const i of items) grid.add(i.box, i.box);
|
|
177
|
+
const anchor = (p: Point): Box | undefined => nearestBox(grid, p, side) ?? nearestBox(grid, p, 4 * side);
|
|
178
|
+
const byAxis = { x: [] as number[], y: [] as number[] };
|
|
179
|
+
gaps.forEach((g, i) => byAxis[g.axis].push(i));
|
|
180
|
+
for (const { leg, short: s } of short.values()) {
|
|
181
|
+
const A = anchor(leg.a);
|
|
182
|
+
const B = anchor(leg.b);
|
|
183
|
+
if (!A || !B) continue;
|
|
184
|
+
const axes = (["x", "y"] as const).filter((x) => Math.abs(leg.u[x]) >= SLANT).sort((p, q) => Math.abs(leg.u[q]) - Math.abs(leg.u[p]));
|
|
185
|
+
for (const axis of axes) {
|
|
186
|
+
const la = A[axis];
|
|
187
|
+
const lb = B[axis];
|
|
188
|
+
const lo = Math.min(la, lb);
|
|
189
|
+
const hi = Math.max(la, lb);
|
|
190
|
+
const mid = (leg.a[axis] + leg.b[axis]) / 2;
|
|
191
|
+
let pick = -1;
|
|
192
|
+
for (const i of byAxis[axis]) {
|
|
193
|
+
const g = gaps[i]!;
|
|
194
|
+
if (g.at <= lo + snap || g.at > hi + snap) continue;
|
|
195
|
+
if (pick < 0 || Math.abs(g.at - mid) < Math.abs(gaps[pick]!.at - mid)) pick = i;
|
|
196
|
+
}
|
|
197
|
+
if (pick < 0) continue;
|
|
198
|
+
grow[pick] = Math.max(grow[pick]!, s / Math.abs(leg.u[axis]));
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return grow;
|
|
203
|
+
}
|
package/src/room/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { center, type Box, type Point } from "../geom.ts";
|
|
2
2
|
import { levelLinks } from "../levels.ts";
|
|
3
3
|
import type { Edge, Graph, Router } from "../model.ts";
|
|
4
|
+
import { bendGrowth, type RoomBends } from "./bends.ts";
|
|
5
|
+
|
|
6
|
+
export type { RoomBends } from "./bends.ts";
|
|
4
7
|
|
|
5
8
|
/** An item with a box: a node of a graph, an item of a level or a box of a host layout. */
|
|
6
9
|
export interface Boxed {
|
|
@@ -70,6 +73,12 @@ export interface RoomOptions {
|
|
|
70
73
|
* The limit `maxGrowth` applies to the sum of the passes.
|
|
71
74
|
*/
|
|
72
75
|
readonly passes?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The room for the bends of the bundles (HWC-D). Without it, the default, the demand counts only the lanes of the links.
|
|
78
|
+
* With it, the router of `bends` routes the open board. Each leg of a bundle that is too short for its arcs grows by a gap.
|
|
79
|
+
* The limit `maxGrowth` applies to the sum of the growth of the links and of the bends.
|
|
80
|
+
*/
|
|
81
|
+
readonly bends?: RoomBends;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
/** The demand of a board and the board with its gaps open. */
|
|
@@ -163,9 +172,28 @@ export function roomDemand(items: readonly Boxed[], links: readonly RoomLink[],
|
|
|
163
172
|
if (next.length !== total.length || next.some((g, i) => g.axis !== total[i]!.axis)) break;
|
|
164
173
|
total = total.map((g, i) => ({ ...g, lanes: Math.max(g.lanes, next[i]!.lanes), need: g.need + Math.max(0, next[i]!.need - next[i]!.width) }));
|
|
165
174
|
}
|
|
175
|
+
if (options.bends) total = withBends(items, links, total, options.bends, snap);
|
|
166
176
|
return capGrowth(items, total, gutter, options.maxGrowth ?? Infinity);
|
|
167
177
|
}
|
|
168
178
|
|
|
179
|
+
/**
|
|
180
|
+
* This function adds the growth for the bends of the bundles to the need of each gap. Each pass routes the board with each gap open to its need.
|
|
181
|
+
* The open board has the same gaps in the same order. So the growth of each gap adds to the need of that gap.
|
|
182
|
+
*/
|
|
183
|
+
export function withBends(items: readonly Boxed[], links: readonly RoomLink[], gaps: readonly Gap[], bends: RoomBends, snap: number = DEFAULT_SNAP): Gap[] {
|
|
184
|
+
let total = [...gaps];
|
|
185
|
+
const passes = Math.max(1, Math.floor(bends.passes ?? 1));
|
|
186
|
+
for (let p = 0; p < passes; p++) {
|
|
187
|
+
const board = openGaps(items, total, snap);
|
|
188
|
+
const open = boardGaps(board, snap);
|
|
189
|
+
if (open.length !== total.length || open.some((g, i) => g.axis !== total[i]!.axis)) break;
|
|
190
|
+
const grow = bendGrowth(board, links, open, bends, snap);
|
|
191
|
+
if (grow.every((g) => g <= 0)) break;
|
|
192
|
+
total = total.map((g, i) => (grow[i]! > 0 ? { ...g, need: g.need + grow[i]! } : g));
|
|
193
|
+
}
|
|
194
|
+
return total;
|
|
195
|
+
}
|
|
196
|
+
|
|
169
197
|
/** This function gives the demand of one pass, without the limit `maxGrowth`. */
|
|
170
198
|
function demandOnce(items: readonly Boxed[], links: readonly RoomLink[], options: RoomOptions): Gap[] {
|
|
171
199
|
const gutter = options.gutter ?? DEFAULT_GUTTER;
|