@mark1russell7/highways 0.2.0 → 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 +240 -8
- package/build/index.d.ts +1459 -78
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9315 -3411
- 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/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 +2 -0
- package/src/groups/kd/index.ts +207 -0
- package/src/index.ts +4 -0
- package/src/levels.ts +163 -5
- 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 +15 -40
- package/src/routers/shared.ts +7 -2
- package/src/routers/trunks.ts +131 -6
- package/src/sections.ts +47 -1
- package/src/segments.ts +81 -31
- package/src/strategies/builtins.ts +129 -8
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +19 -1
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +221 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { CutOptions } from "../groups/cut/index.ts";
|
|
2
|
+
import type { KdOptions } from "../groups/kd/index.ts";
|
|
3
|
+
import type { KeyOptions } from "../groups/keys/index.ts";
|
|
4
|
+
import type { CornersOptions } from "../routers/corners.ts";
|
|
5
|
+
import type { SectionsOptions } from "../routers/sections.ts";
|
|
6
|
+
import type { SharedOptions } from "../routers/shared.ts";
|
|
7
|
+
import type { TrunksOptions } from "../routers/trunks.ts";
|
|
8
|
+
import type { SectionOptions } from "../sections.ts";
|
|
9
|
+
import type { RunOrder } from "../segments.ts";
|
|
10
|
+
import type { ClothoidStrategyOptions, HighwayStrategyOptions, LevelRoomPreset, RadiusOptions, RoomPreset, RouterPreset, SmoothStrategyOptions, TensionOptions } from "./builtins.ts";
|
|
11
|
+
import type { EnumOption, OptionSchema } from "./options.ts";
|
|
12
|
+
|
|
13
|
+
// The declarations of the options of the built-in strategies. Each `default` is the value that the function uses without the option.
|
|
14
|
+
// The test of the defaults runs each strategy with `optionDefaults` and with no option, and compares the output.
|
|
15
|
+
|
|
16
|
+
/** The options of the way finder (`WayOptions`), for each router that goes around the nodes. */
|
|
17
|
+
const WAY_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = {
|
|
18
|
+
margin: { type: "number", min: 1, max: 20, step: 1, default: 5, describe: "The distance from a box to the corners of the graph." },
|
|
19
|
+
k: { type: "integer", min: 2, max: 16, step: 1, default: 8, describe: "The number of visible neighbours of each corner." },
|
|
20
|
+
smooth: { type: "boolean", default: false, describe: "With `true`, a way in clear view uses the inflection rule, and the other ways get round corners." },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The declaration of the lane order of the shared ways of the routers `trunks` and `shared`. */
|
|
24
|
+
const LANE_ORDER: EnumOption<RunOrder> = {
|
|
25
|
+
type: "enum",
|
|
26
|
+
values: ["bundle", "run"],
|
|
27
|
+
default: "bundle",
|
|
28
|
+
describe: "With `bundle`, two edges keep one lane order on each shared way of their common stretch. With `run`, each shared way gets its order alone.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The declaration of the lane order of the runs of the router `sections`. The default keeps the lane order of version 0.2.0. */
|
|
32
|
+
const RUN_LANE_ORDER: EnumOption<RunOrder> = { ...LANE_ORDER, default: "run" };
|
|
33
|
+
|
|
34
|
+
/** The declarations of the options of the routers `corners` and `corners-smooth`. */
|
|
35
|
+
export const CORNERS_SCHEMA: OptionSchema<RouterPreset<CornersOptions>> = WAY_SCHEMA;
|
|
36
|
+
|
|
37
|
+
/** The declarations of the options of the router `trunks`. */
|
|
38
|
+
export const TRUNKS_SCHEMA: OptionSchema<RouterPreset<TrunksOptions>> = {
|
|
39
|
+
...WAY_SCHEMA,
|
|
40
|
+
rule: { type: "function", describe: "The grouping rule. The edges from one plug with one key share a trunk." },
|
|
41
|
+
split: { type: "number", min: 0.1, max: 0.9, step: 0.05, default: 0.5, describe: "The split point is at this share of the way to the center of the targets." },
|
|
42
|
+
minSize: { type: "integer", min: 1, max: 8, step: 1, default: 2, describe: "A group with fewer edges stays unbundled." },
|
|
43
|
+
laneOrder: LANE_ORDER,
|
|
44
|
+
leave: {
|
|
45
|
+
type: "enum",
|
|
46
|
+
values: ["part", "split"],
|
|
47
|
+
default: "part",
|
|
48
|
+
describe: "With `part`, a lane leaves the trunk where its way parts from the other ways. With `split`, each lane leaves at the split point.",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The declarations of the options of the routers `shared` and `shared-all`. */
|
|
53
|
+
export const SHARED_SCHEMA: OptionSchema<RouterPreset<SharedOptions>> = {
|
|
54
|
+
...WAY_SCHEMA,
|
|
55
|
+
rule: { type: "function", describe: "The edges with one key share their ways. Without a rule, all edges share." },
|
|
56
|
+
discount: {
|
|
57
|
+
type: "number",
|
|
58
|
+
min: 0.05,
|
|
59
|
+
max: 1,
|
|
60
|
+
step: 0.05,
|
|
61
|
+
default: 0.5,
|
|
62
|
+
describe: "The cost of a link that an edge with the same key uses already, as a share of its length.",
|
|
63
|
+
},
|
|
64
|
+
laneOrder: LANE_ORDER,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The declarations of the limits of the tree of sections (`SectionOptions`). */
|
|
68
|
+
export const SECTION_SCHEMA: OptionSchema<Required<SectionOptions>> = {
|
|
69
|
+
cap: { type: "integer", min: 1, max: 32, step: 1, scale: "log", marks: [4, 6, 8, 12, 16], default: 8, describe: "The section limit. A section with more nodes splits in four." },
|
|
70
|
+
maxDepth: { type: "integer", min: 1, max: 32, step: 1, default: 16, describe: "The depth limit of the tree." },
|
|
71
|
+
collapse: { type: "integer", min: 1, max: 64, step: 1, default: 32, describe: "A cell shrinks to one quarter at most this many times without a split." },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The declarations of the options of the routers `sections` and `sections-tree`. */
|
|
75
|
+
export const SECTIONS_SCHEMA: OptionSchema<RouterPreset<SectionsOptions>> = {
|
|
76
|
+
...WAY_SCHEMA,
|
|
77
|
+
...SECTION_SCHEMA,
|
|
78
|
+
hubs: {
|
|
79
|
+
type: "integer",
|
|
80
|
+
min: 0,
|
|
81
|
+
max: 8,
|
|
82
|
+
step: 1,
|
|
83
|
+
special: [Infinity],
|
|
84
|
+
default: Infinity,
|
|
85
|
+
describe: "The number of hubs on each side of the lowest common section that a way passes. `Infinity` passes each hub.",
|
|
86
|
+
},
|
|
87
|
+
stretch: {
|
|
88
|
+
type: "number",
|
|
89
|
+
min: 1,
|
|
90
|
+
max: 4,
|
|
91
|
+
step: 0.25,
|
|
92
|
+
special: [Infinity],
|
|
93
|
+
marks: [1.25, 1.5, 2, 3, Infinity],
|
|
94
|
+
default: 2,
|
|
95
|
+
describe: "The hub limit. A way through the hubs is at most this many times the straight line. `Infinity` keeps each such way.",
|
|
96
|
+
},
|
|
97
|
+
stretchPad: { type: "number", min: 0, max: 16, step: 1, default: 4, describe: "A way through the hubs can be this many margins longer than the hub limit." },
|
|
98
|
+
hubSlack: {
|
|
99
|
+
type: "number",
|
|
100
|
+
min: 0,
|
|
101
|
+
max: 320,
|
|
102
|
+
step: 10,
|
|
103
|
+
special: [Infinity],
|
|
104
|
+
marks: [0, 40, 80, 160, Infinity],
|
|
105
|
+
default: 80,
|
|
106
|
+
describe: "A way skips a hub that is this many units farther from the target than the way was before.",
|
|
107
|
+
},
|
|
108
|
+
tightMargin: { type: "number", min: 0, max: 5, step: 0.5, default: 1, describe: "The margin of the second corner graph of the level links." },
|
|
109
|
+
plugInset: { type: "number", min: 0, max: 12, step: 1, default: 6, describe: "A plug of a level link stays this far from the corners of its side." },
|
|
110
|
+
throughCost: {
|
|
111
|
+
type: "number",
|
|
112
|
+
min: 0,
|
|
113
|
+
max: 16,
|
|
114
|
+
step: 1,
|
|
115
|
+
marks: [0, 2, 4, 8, 16],
|
|
116
|
+
default: 8,
|
|
117
|
+
describe: "The cost of a level link through a far box, in mean box sizes. The value 0 gives the links of version 0.1.5.",
|
|
118
|
+
},
|
|
119
|
+
group: { type: "strategy", step: "group", describe: "The grouping rule: a strategy of the step `group`. Without it, the router uses the quadtree." },
|
|
120
|
+
groupOptions: { type: "object", describe: "The options of the strategy `group`." },
|
|
121
|
+
levels: { type: "list", describe: "The depths of the tree that get a level with groups. Without it, each depth gets a level." },
|
|
122
|
+
laneOrder: RUN_LANE_ORDER,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** The declarations of the option of the curves `rounded` and `arc`. */
|
|
126
|
+
export const RADIUS_SCHEMA: OptionSchema<RadiusOptions> = {
|
|
127
|
+
radius: { type: "number", min: 0, max: 48, step: 1, describe: "The radius of the corners." },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** The declarations of the options of the curve `highway`. */
|
|
131
|
+
export const HIGHWAY_SCHEMA: OptionSchema<HighwayStrategyOptions> = {
|
|
132
|
+
...RADIUS_SCHEMA,
|
|
133
|
+
minRadius: { type: "number", min: 0, max: 24, step: 1, describe: "The smallest radius of a lane at a bend of a bundle." },
|
|
134
|
+
jog: {
|
|
135
|
+
type: "enum",
|
|
136
|
+
values: ["biarc", "cubic", "quintic"],
|
|
137
|
+
default: "biarc",
|
|
138
|
+
describe: "The shape of the S curve where a lane changes its offset. A `quintic` S curve has no jump of the curvature.",
|
|
139
|
+
},
|
|
140
|
+
jogPieces: { type: "integer", min: 1, max: 8, step: 1, default: 4, describe: "The number of cubic pieces of a `quintic` S curve." },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The declarations of the options of the curve `smooth`. */
|
|
144
|
+
export const SMOOTH_SCHEMA: OptionSchema<SmoothStrategyOptions> = {
|
|
145
|
+
...RADIUS_SCHEMA,
|
|
146
|
+
smoothing: { type: "number", min: 0, max: 1, step: 0.05, describe: "The share of the turn of a corner in its two spirals. The value 0 gives the curve `arc`." },
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** The declarations of the options of the curve `clothoid`. */
|
|
150
|
+
export const CLOTHOID_SCHEMA: OptionSchema<ClothoidStrategyOptions> = {
|
|
151
|
+
...RADIUS_SCHEMA,
|
|
152
|
+
spiral: { type: "number", min: 0, max: 1, step: 0.05, describe: "The share of the turn of a corner in its two clothoids. The value 0 gives the curve `arc`." },
|
|
153
|
+
tolerance: { type: "number", min: 0.05, max: 1, step: 0.05, describe: "The largest distance from the cubic pieces to the true clothoid." },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** The declarations of the option of the curve `spline`. */
|
|
157
|
+
export const TENSION_SCHEMA: OptionSchema<TensionOptions> = {
|
|
158
|
+
tension: { type: "number", min: 0, max: 1, step: 0.05, describe: "The tension of the spline, from 0 (the spline) to 1 (the polyline)." },
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** The declarations of the options of the room strategies: the options of `roomDemand`, without the method. */
|
|
162
|
+
export const ROOM_SCHEMA: OptionSchema<RoomPreset> = {
|
|
163
|
+
gutter: { type: "number", min: 0, max: 32, step: 1, marks: [4, 8, 12, 16], default: 8, describe: "The smallest width of each gap." },
|
|
164
|
+
spacing: { type: "number", min: 1, max: 12, step: 1, marks: [2, 4, 6, 8], default: 4, describe: "The width of one lane: the distance between two lines that run side by side." },
|
|
165
|
+
maxGrowth: {
|
|
166
|
+
type: "number",
|
|
167
|
+
min: 0,
|
|
168
|
+
max: 1,
|
|
169
|
+
step: 0.01,
|
|
170
|
+
special: [Infinity],
|
|
171
|
+
marks: [0, 0.06, 0.09, 0.12, 0.15, 0.18, 0.21, 0.25, 0.3, 0.35, 0.5, Infinity],
|
|
172
|
+
default: Infinity,
|
|
173
|
+
describe: "The largest growth of the board on each axis, as a share of its size. `Infinity` sets no limit.",
|
|
174
|
+
},
|
|
175
|
+
router: { type: "object", describe: "The router of the method `routes`. Without it, the links go as the links of a zoom level." },
|
|
176
|
+
lanes: { type: "enum", values: ["link", "count"], default: "link", describe: "With `count`, a link takes one lane for each of its edges. With `link`, a link takes one lane." },
|
|
177
|
+
snap: { type: "number", min: 0, max: 4, step: 0.25, default: 0.5, describe: "Two box edges nearer than this distance make one boundary of the sweep." },
|
|
178
|
+
passes: { type: "integer", min: 1, max: 4, step: 1, marks: [1, 2], default: 1, describe: "The number of passes of the demand. Each pass after the first finds the lanes on the open board." },
|
|
179
|
+
bends: {
|
|
180
|
+
type: "object",
|
|
181
|
+
describe: "The room for the bends of the bundles: a router that bundles, and the lane options of `highwayCurves`. Without it, the bends get no room.",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** The declarations of the options of the room strategy `levels`. They are the options of the room, the levels with room and the limits of the quadtree. */
|
|
186
|
+
export const LEVEL_ROOM_SCHEMA: OptionSchema<LevelRoomPreset> = {
|
|
187
|
+
...ROOM_SCHEMA,
|
|
188
|
+
depths: { type: "list", describe: "The depths of the tree that get room. Without it, each depth with groups gets room." },
|
|
189
|
+
nodes: { type: "boolean", default: false, describe: "With `true`, the last level, with each node alone, also gets room." },
|
|
190
|
+
board: { type: "enum", values: ["shared", "each"], default: "shared", describe: "With `shared`, each level uses the board of the nearest level. With `each`, each level has its own board." },
|
|
191
|
+
...SECTION_SCHEMA,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** The declarations of the options of the grouping rule `keys`. */
|
|
195
|
+
export const KEY_SCHEMA: OptionSchema<KeyOptions> = {
|
|
196
|
+
key: { type: "function", describe: "This function gives the key of the group of each node." },
|
|
197
|
+
split: { type: "object", describe: "With these limits of the quadtree, a large group splits into cells. Without them, each group is one leaf." },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** The declarations of the limits of the tree of the grouping rules `kd` and `cut`. */
|
|
201
|
+
const SPLIT_TREE_SCHEMA: OptionSchema<Required<Pick<KdOptions, "cap" | "maxDepth" | "fanout">>> = {
|
|
202
|
+
cap: { ...SECTION_SCHEMA.cap, describe: "The section limit. A section with more nodes splits." },
|
|
203
|
+
maxDepth: SECTION_SCHEMA.maxDepth,
|
|
204
|
+
fanout: { type: "integer", min: 2, max: 4, step: 2, default: 4, describe: "The number of children of a section. With 4, a section splits two times on one level." },
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** The declarations of the options of the grouping rule `kd`. */
|
|
208
|
+
export const KD_SCHEMA: OptionSchema<KdOptions> = {
|
|
209
|
+
...SPLIT_TREE_SCHEMA,
|
|
210
|
+
line: { type: "enum", values: ["median", "middle", "slide"], default: "slide", describe: "The place of the line in the cell: the median node, the middle, or the middle that slides to the nearest node." },
|
|
211
|
+
axis: { type: "enum", values: ["longer", "alternate"], default: "longer", describe: "The axis of each split: across the longer side of the cell, or x and y in turn." },
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** The declarations of the options of the grouping rule `cut`. */
|
|
215
|
+
export const CUT_SCHEMA: OptionSchema<CutOptions> = {
|
|
216
|
+
...SPLIT_TREE_SCHEMA,
|
|
217
|
+
balance: { type: "number", min: 0.05, max: 0.5, step: 0.05, marks: [0.1, 0.25, 0.4], default: 0.25, describe: "The smallest share of the nodes in each part of a split." },
|
|
218
|
+
objective: { type: "enum", values: ["ratio", "normalized", "sparsest"], default: "ratio", describe: "The cost of a line: the ratio cut, the normalized cut or the sparsest cut." },
|
|
219
|
+
pull: { type: "number", min: 0, max: 4, step: 0.25, marks: [0, 0.5, 1, 2, 4], default: 0.5, describe: "The cost of a line grows with its distance from the middle of the cell, by this factor." },
|
|
220
|
+
guard: { type: "boolean", default: true, describe: "With `true`, each section also tests the quadtree split, and keeps the split that cuts fewer edges." },
|
|
221
|
+
};
|
package/src/visibility.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOLERANCE } from "./bench/properties.ts";
|
|
2
|
+
import { dist, inflate, segmentHitsBox, segmentHitsRect, type Box, type Point } from "./geom.ts";
|
|
2
3
|
import { Grid } from "./grid.ts";
|
|
3
4
|
import type { Node } from "./model.ts";
|
|
4
5
|
|
|
@@ -37,6 +38,17 @@ export class Obstacles {
|
|
|
37
38
|
return true;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* This method gives each node whose box the segment `ab` goes through to `visit`, one time each.
|
|
43
|
+
* The segment goes through a box when a part of it longer than `TOLERANCE` is inside the box made smaller by `TOLERANCE`.
|
|
44
|
+
* The link measures (`measureLinks`) use the same test.
|
|
45
|
+
*/
|
|
46
|
+
through(a: Point, b: Point, visit: (n: Node) => void): void {
|
|
47
|
+
this.#grid.querySegment(a, b, 0, (n) => {
|
|
48
|
+
if (segmentHitsBox(a, b, inflate(n.box, -TOLERANCE), TOLERANCE)) visit(n);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
/** This method gives each node whose bucket the box touches to `visit`. A node can be outside the box. */
|
|
41
53
|
query(box: Box, visit: (n: Node) => void): void {
|
|
42
54
|
this.#grid.query(box, visit);
|
|
@@ -493,6 +505,183 @@ export class CornerGraph {
|
|
|
493
505
|
}
|
|
494
506
|
}
|
|
495
507
|
|
|
508
|
+
/** The options of the soft graph. */
|
|
509
|
+
export interface SoftGraphOptions {
|
|
510
|
+
/** The distance from a box to its corners. */
|
|
511
|
+
readonly margin: number;
|
|
512
|
+
/** The number of nearest neighbours of each vertex. */
|
|
513
|
+
readonly k: number;
|
|
514
|
+
/**
|
|
515
|
+
* The spacing of a lattice of points in the bounding box of the boxes, in units of the mean node size. The default is 3, and 0 turns it off.
|
|
516
|
+
* The lattice links groups of boxes across empty space.
|
|
517
|
+
*/
|
|
518
|
+
readonly lattice?: number;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A graph whose links can go through the boxes. Its vertices are the corners of the boxes, made larger by `margin`,
|
|
523
|
+
* and a lattice of points in the bounding box of the boxes. Each vertex links to its `k` nearest vertices, in view or not.
|
|
524
|
+
* The graph keeps the boxes that each link goes through (`Obstacles.through`).
|
|
525
|
+
* A search gives each box a cost, so a path goes through few boxes, and only through boxes of a low cost.
|
|
526
|
+
*/
|
|
527
|
+
export class SoftGraph {
|
|
528
|
+
readonly points: Point[] = [];
|
|
529
|
+
readonly #nodes: readonly Node[];
|
|
530
|
+
readonly #obstacles: Obstacles;
|
|
531
|
+
readonly #adj: number[][] = [];
|
|
532
|
+
readonly #len: number[][] = [];
|
|
533
|
+
// The indices of the nodes that each link goes through, in the order of #adj.
|
|
534
|
+
readonly #through: (readonly number[])[][] = [];
|
|
535
|
+
readonly #grid: Grid<number>;
|
|
536
|
+
readonly #cell: number;
|
|
537
|
+
readonly #k: number;
|
|
538
|
+
|
|
539
|
+
/** This constructor makes the graph of the nodes. */
|
|
540
|
+
constructor(nodes: readonly Node[], obstacles: Obstacles, options: SoftGraphOptions) {
|
|
541
|
+
this.#nodes = nodes;
|
|
542
|
+
this.#obstacles = obstacles;
|
|
543
|
+
this.#k = options.k;
|
|
544
|
+
let size = 0;
|
|
545
|
+
for (const n of nodes) size += (n.box.w + n.box.h) / 2;
|
|
546
|
+
this.#cell = Math.max(1, size / Math.max(1, nodes.length) + 2 * options.margin);
|
|
547
|
+
this.#grid = new Grid<number>(this.#cell);
|
|
548
|
+
let x0 = Infinity;
|
|
549
|
+
let y0 = Infinity;
|
|
550
|
+
let x1 = -Infinity;
|
|
551
|
+
let y1 = -Infinity;
|
|
552
|
+
for (const n of nodes) {
|
|
553
|
+
const b = inflate(n.box, options.margin);
|
|
554
|
+
for (const c of [
|
|
555
|
+
{ x: b.x, y: b.y },
|
|
556
|
+
{ x: b.x + b.w, y: b.y },
|
|
557
|
+
{ x: b.x + b.w, y: b.y + b.h },
|
|
558
|
+
{ x: b.x, y: b.y + b.h },
|
|
559
|
+
])
|
|
560
|
+
this.#add(c);
|
|
561
|
+
x0 = Math.min(x0, n.box.x);
|
|
562
|
+
y0 = Math.min(y0, n.box.y);
|
|
563
|
+
x1 = Math.max(x1, n.box.x + n.box.w);
|
|
564
|
+
y1 = Math.max(y1, n.box.y + n.box.h);
|
|
565
|
+
}
|
|
566
|
+
// The lattice stays in the bounding box of the boxes, so a path does not go around the outer edge of the board.
|
|
567
|
+
const spacing = (options.lattice ?? 3) * this.#cell;
|
|
568
|
+
if (spacing > 0) for (let x = x0 + spacing / 2; x < x1; x += spacing) for (let y = y0 + spacing / 2; y < y1; y += spacing) this.#add({ x, y });
|
|
569
|
+
const links: Set<number>[] = this.points.map(() => new Set<number>());
|
|
570
|
+
for (let i = 0; i < this.points.length; i++)
|
|
571
|
+
for (const j of this.#nearest(this.points[i]!, this.#k, i)) {
|
|
572
|
+
links[i]!.add(j);
|
|
573
|
+
links[j]!.add(i);
|
|
574
|
+
}
|
|
575
|
+
const index = new Map(nodes.map((n, i) => [n, i]));
|
|
576
|
+
// The boxes of each link, found one time for the two directions of the link.
|
|
577
|
+
const known = new Map<number, readonly number[]>();
|
|
578
|
+
const count = this.points.length;
|
|
579
|
+
links.forEach((set, i) => {
|
|
580
|
+
const adj = [...set].sort((a, b) => a - b);
|
|
581
|
+
this.#adj.push(adj);
|
|
582
|
+
this.#len.push(adj.map((j) => dist(this.points[i]!, this.points[j]!)));
|
|
583
|
+
this.#through.push(
|
|
584
|
+
adj.map((j) => {
|
|
585
|
+
const key = Math.min(i, j) * count + Math.max(i, j);
|
|
586
|
+
let list = known.get(key);
|
|
587
|
+
if (list === undefined) {
|
|
588
|
+
const found: number[] = [];
|
|
589
|
+
obstacles.through(this.points[i]!, this.points[j]!, (n) => found.push(index.get(n)!));
|
|
590
|
+
list = found;
|
|
591
|
+
known.set(key, list);
|
|
592
|
+
}
|
|
593
|
+
return list;
|
|
594
|
+
}),
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#add(c: Point): void {
|
|
600
|
+
this.#grid.add(this.points.length, { x: c.x, y: c.y, w: 0, h: 0 });
|
|
601
|
+
this.points.push(c);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** This method gives at most `k` vertices nearest to `p`, in view or not, nearest first, and equal distances by index. The vertex `self` does not count. */
|
|
605
|
+
#nearest(p: Point, k: number, self = -1): number[] {
|
|
606
|
+
const all = this.points.length - (self >= 0 ? 1 : 0);
|
|
607
|
+
const want = Math.min(k, all);
|
|
608
|
+
if (want <= 0) return [];
|
|
609
|
+
for (let r = this.#cell; ; r *= 2) {
|
|
610
|
+
const found: number[] = [];
|
|
611
|
+
this.#grid.query({ x: p.x - r, y: p.y - r, w: 2 * r, h: 2 * r }, (j) => {
|
|
612
|
+
if (j !== self && dist(p, this.points[j]!) <= r) found.push(j);
|
|
613
|
+
});
|
|
614
|
+
if (found.length >= want) {
|
|
615
|
+
const d = (j: number): number => dist(p, this.points[j]!);
|
|
616
|
+
return found.sort((a, b) => d(a) - d(b) || a - b).slice(0, want);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* This method gives a path of least cost from `from` to `to` (A*), with the two points as its ends.
|
|
623
|
+
* The cost of a path is its length, plus `cost(n)` for each link of the path and each box `n` of that link.
|
|
624
|
+
* Each cost must be 0 or more. The straight line from `from` to `to` is a path too, so the method always gives a path.
|
|
625
|
+
*/
|
|
626
|
+
path(from: Point, to: Point, cost: (n: Node) => number): Point[] {
|
|
627
|
+
const points = this.points;
|
|
628
|
+
const nodes = this.#nodes;
|
|
629
|
+
const extra = (a: Point, b: Point): number => {
|
|
630
|
+
let c = 0;
|
|
631
|
+
this.#obstacles.through(a, b, (m) => {
|
|
632
|
+
c += cost(m);
|
|
633
|
+
});
|
|
634
|
+
return c;
|
|
635
|
+
};
|
|
636
|
+
let best = dist(from, to) + extra(from, to);
|
|
637
|
+
let bestEnd = -1;
|
|
638
|
+
const goal = new Map<number, number>();
|
|
639
|
+
for (const j of this.#nearest(to, 2 * this.#k)) goal.set(j, dist(points[j]!, to) + extra(points[j]!, to));
|
|
640
|
+
const n = points.length;
|
|
641
|
+
const g = new Float64Array(n).fill(Infinity);
|
|
642
|
+
const parent = new Int32Array(n).fill(-1);
|
|
643
|
+
const closed = new Uint8Array(n);
|
|
644
|
+
const heap = new MinHeap();
|
|
645
|
+
for (const j of this.#nearest(from, 2 * this.#k)) {
|
|
646
|
+
const c = dist(from, points[j]!) + extra(from, points[j]!);
|
|
647
|
+
if (c < g[j]!) {
|
|
648
|
+
g[j] = c;
|
|
649
|
+
heap.push(c + dist(points[j]!, to), j);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
while (heap.size > 0) {
|
|
653
|
+
if (heap.topKey >= best) break;
|
|
654
|
+
const i = heap.pop();
|
|
655
|
+
if (closed[i] === 1) continue;
|
|
656
|
+
closed[i] = 1;
|
|
657
|
+
const gi = g[i]!;
|
|
658
|
+
const tail = goal.get(i);
|
|
659
|
+
if (tail !== undefined && gi + tail < best) {
|
|
660
|
+
best = gi + tail;
|
|
661
|
+
bestEnd = i;
|
|
662
|
+
}
|
|
663
|
+
const adj = this.#adj[i]!;
|
|
664
|
+
const len = this.#len[i]!;
|
|
665
|
+
const through = this.#through[i]!;
|
|
666
|
+
for (let m = 0; m < adj.length; m++) {
|
|
667
|
+
const j = adj[m]!;
|
|
668
|
+
if (closed[j] === 1) continue;
|
|
669
|
+
let c = gi + len[m]!;
|
|
670
|
+
for (const b of through[m]!) c += cost(nodes[b]!);
|
|
671
|
+
if (c < g[j]!) {
|
|
672
|
+
g[j] = c;
|
|
673
|
+
parent[j] = i;
|
|
674
|
+
heap.push(c + dist(points[j]!, to), j);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const out: Point[] = [to];
|
|
679
|
+
for (let i = bestEnd; i >= 0; i = parent[i]!) out.push(points[i]!);
|
|
680
|
+
out.push(from);
|
|
681
|
+
return out.reverse();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
496
685
|
/** A link of a cone is at most this number of cells long. A longer search costs time on a dense level and finds few new links. */
|
|
497
686
|
const CONE_CELLS = 3;
|
|
498
687
|
|