@mark1russell7/highways 0.2.0 → 0.4.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 +265 -9
- package/build/index.d.ts +2168 -101
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11624 -3422
- package/build/index.js.map +1 -1
- package/package.json +4 -1
- package/src/bench/compare.ts +180 -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/screen/forms.ts +372 -0
- package/src/bench/screen/generators.ts +54 -0
- package/src/bench/screen/hash.ts +129 -0
- package/src/bench/screen/index.ts +6 -0
- package/src/bench/screen/measures.ts +114 -0
- package/src/bench/screen/screen.ts +390 -0
- package/src/bench/screen/set.ts +136 -0
- package/src/bench/suite.ts +839 -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/highways/corridors/index.ts +305 -0
- package/src/highways/follow.ts +155 -0
- package/src/highways/index.ts +7 -0
- package/src/highways/keys/index.ts +15 -0
- package/src/highways/model.ts +111 -0
- package/src/highways/result.ts +34 -0
- package/src/highways/router.ts +40 -0
- package/src/highways/shared-ways/index.ts +15 -0
- package/src/index.ts +6 -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 +206 -9
- package/src/strategies/index.ts +1 -0
- package/src/strategies/model.ts +55 -7
- package/src/strategies/options.ts +296 -0
- package/src/strategies/schemas.ts +234 -0
- package/src/visibility.ts +190 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { dist, polylineLength } from "../../geom.ts";
|
|
2
|
+
import { rng } from "../../fixtures.ts";
|
|
3
|
+
import type { Graph, Route } from "../../model.ts";
|
|
4
|
+
import { drawnSegments, segmentKey } from "./routes.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This function gives the share of the ink that shared segments save (B1 of MEAS0): `inkSaved = 1 - ink(S) / Σ_e L(r_e)`.
|
|
8
|
+
* `ink(S)` counts a shared segment one time, as `measureQuality` does. The scale is 0 to 1, and higher is better. The cost is `n log n`.
|
|
9
|
+
*/
|
|
10
|
+
export function inkSaved(routes: readonly Route[]): number {
|
|
11
|
+
let total = 0;
|
|
12
|
+
for (const r of routes) total += polylineLength(r.points);
|
|
13
|
+
if (total <= 0) return 0;
|
|
14
|
+
let ink = 0;
|
|
15
|
+
for (const [a, b] of drawnSegments(routes)) ink += dist(a, b);
|
|
16
|
+
return Math.max(0, 1 - ink / total);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The ambiguity of a result (B4 of MEAS0). */
|
|
20
|
+
export interface Ambiguity {
|
|
21
|
+
/** The share of false neighbours, from 0 to 1. Lower is better. */
|
|
22
|
+
readonly value: number;
|
|
23
|
+
/** This value is true when the value comes from a sample of the nodes. */
|
|
24
|
+
readonly estimated: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The options of `ambiguity`. */
|
|
28
|
+
export interface AmbiguityOptions {
|
|
29
|
+
/** Up to this number of nodes, the function examines each node. The default is 1000. */
|
|
30
|
+
readonly exact?: number;
|
|
31
|
+
/** Above `exact` nodes, the function examines this number of nodes from a seeded sample. The default is 2000. */
|
|
32
|
+
readonly sample?: number;
|
|
33
|
+
/** The seed of the sample. The default is 1. */
|
|
34
|
+
readonly seed?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* This function measures the ambiguity of the routes (B4 of MEAS0), with one step of neighbours.
|
|
39
|
+
* `N(v)` holds the nodes whose routes share a drawn segment with a route of `v`.
|
|
40
|
+
* `amb = Σ_v count(w in N(v) : no edge v-w) / Σ_v count(N(v))`. The cost is quadratic, so a seeded sample serves above `exact` nodes.
|
|
41
|
+
*/
|
|
42
|
+
export function ambiguity(graph: Graph, routes: readonly Route[], options: AmbiguityOptions = {}): Ambiguity {
|
|
43
|
+
const n = graph.nodes.length;
|
|
44
|
+
const index = new Map(graph.nodes.map((v, i) => [v.id, i] as const));
|
|
45
|
+
const ends = new Map<string, readonly [number, number]>();
|
|
46
|
+
for (const e of graph.edges) {
|
|
47
|
+
const s = index.get(e.source.node);
|
|
48
|
+
const t = index.get(e.target.node);
|
|
49
|
+
if (s !== undefined && t !== undefined) ends.set(e.id, [s, t]);
|
|
50
|
+
}
|
|
51
|
+
// Each drawn segment gets a number. `keysOf` holds the segments of each edge, and `nodesOf` the distinct end nodes of the edges of each segment.
|
|
52
|
+
const keyIds = new Map<string, number>();
|
|
53
|
+
const nodesOf: number[][] = [];
|
|
54
|
+
const keysOf = new Map<string, number[]>();
|
|
55
|
+
for (const r of routes) {
|
|
56
|
+
const end = ends.get(r.edge);
|
|
57
|
+
if (!end) continue;
|
|
58
|
+
const own: number[] = [];
|
|
59
|
+
for (let i = 1; i < r.points.length; i++) {
|
|
60
|
+
const k = segmentKey(r.points[i - 1]!, r.points[i]!);
|
|
61
|
+
if (k === undefined) continue;
|
|
62
|
+
let id = keyIds.get(k);
|
|
63
|
+
if (id === undefined) {
|
|
64
|
+
id = nodesOf.length;
|
|
65
|
+
keyIds.set(k, id);
|
|
66
|
+
nodesOf.push([]);
|
|
67
|
+
}
|
|
68
|
+
if (own.includes(id)) continue;
|
|
69
|
+
own.push(id);
|
|
70
|
+
nodesOf[id]!.push(end[0], end[1]);
|
|
71
|
+
}
|
|
72
|
+
keysOf.set(r.edge, own);
|
|
73
|
+
}
|
|
74
|
+
const mark = new Int32Array(n).fill(-1);
|
|
75
|
+
nodesOf.forEach((list, k) => {
|
|
76
|
+
let j = 0;
|
|
77
|
+
for (const v of list)
|
|
78
|
+
if (mark[v] !== k) {
|
|
79
|
+
mark[v] = k;
|
|
80
|
+
list[j++] = v;
|
|
81
|
+
}
|
|
82
|
+
list.length = j;
|
|
83
|
+
});
|
|
84
|
+
const incident: string[][] = Array.from({ length: n }, () => []);
|
|
85
|
+
const adjacent: number[][] = Array.from({ length: n }, () => []);
|
|
86
|
+
for (const e of graph.edges) {
|
|
87
|
+
const end = ends.get(e.id);
|
|
88
|
+
if (!end) continue;
|
|
89
|
+
incident[end[0]]!.push(e.id);
|
|
90
|
+
incident[end[1]]!.push(e.id);
|
|
91
|
+
adjacent[end[0]]!.push(end[1]);
|
|
92
|
+
adjacent[end[1]]!.push(end[0]);
|
|
93
|
+
}
|
|
94
|
+
const exact = n <= (options.exact ?? 1000);
|
|
95
|
+
const R = rng(options.seed ?? 1);
|
|
96
|
+
const pick = exact ? Array.from({ length: n }, (_, i) => i) : Array.from({ length: options.sample ?? 2000 }, () => Math.floor(R() * n));
|
|
97
|
+
// The stamps mark the segments, the near nodes and the neighbours of the current node, with no new set for each node.
|
|
98
|
+
const keyStamp = new Int32Array(nodesOf.length).fill(-1);
|
|
99
|
+
const nearStamp = new Int32Array(n).fill(-1);
|
|
100
|
+
const linkStamp = new Int32Array(n).fill(-1);
|
|
101
|
+
let all = 0;
|
|
102
|
+
let wrong = 0;
|
|
103
|
+
pick.forEach((v, step) => {
|
|
104
|
+
for (const w of adjacent[v]!) linkStamp[w] = step;
|
|
105
|
+
nearStamp[v] = step;
|
|
106
|
+
for (const e of incident[v]!)
|
|
107
|
+
for (const k of keysOf.get(e) ?? []) {
|
|
108
|
+
if (keyStamp[k] === step) continue;
|
|
109
|
+
keyStamp[k] = step;
|
|
110
|
+
for (const w of nodesOf[k]!) {
|
|
111
|
+
if (nearStamp[w] === step) continue;
|
|
112
|
+
nearStamp[w] = step;
|
|
113
|
+
all++;
|
|
114
|
+
if (linkStamp[w] !== step) wrong++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return { value: all ? wrong / all : 0, estimated: !exact };
|
|
119
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { CurveSegment } from "../../curves/model.ts";
|
|
2
|
+
import type { CurveSpace } from "../../curves/space.ts";
|
|
3
|
+
import { dist, polylineLength, segmentsCross, turns, type Point } from "../../geom.ts";
|
|
4
|
+
import { Grid } from "../../grid.ts";
|
|
5
|
+
import { TOLERANCE } from "../properties.ts";
|
|
6
|
+
|
|
7
|
+
const EPS = 1e-9;
|
|
8
|
+
const DEG = 180 / Math.PI;
|
|
9
|
+
|
|
10
|
+
/** A joint of a curve: the end of one drawn segment and the start of the next drawn segment. */
|
|
11
|
+
export interface CurveJoint {
|
|
12
|
+
readonly at: Point;
|
|
13
|
+
/** The angle between the tangent at the end of the first segment and the tangent at the start of the second segment, in degrees. */
|
|
14
|
+
readonly angle: number;
|
|
15
|
+
/** The jump of the curvature at the joint, `abs(κ^- - κ^+)`, in 1 / units. */
|
|
16
|
+
readonly jump: number;
|
|
17
|
+
/** The unit tangent at the end of the first segment. */
|
|
18
|
+
readonly before: Point;
|
|
19
|
+
/** The unit tangent at the start of the second segment. */
|
|
20
|
+
readonly after: Point;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const unit = (x: number, y: number): Point | undefined => {
|
|
24
|
+
const l = Math.hypot(x, y);
|
|
25
|
+
return l > EPS ? { x: x / l, y: y / l } : undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** This function gives the unit tangent and the curvature at the start (`t = 0`) or at the end (`t = 1`) of a segment that starts at `from`. */
|
|
29
|
+
function frame(from: Point, s: CurveSegment, t: 0 | 1): { tangent: Point | undefined; curvature: number } {
|
|
30
|
+
if (s.kind === "line" || s.kind === "move") return { tangent: unit(s.to.x - from.x, s.to.y - from.y), curvature: 0 };
|
|
31
|
+
if (s.kind === "arc") {
|
|
32
|
+
const a = t === 0 ? s.start : s.start + s.sweep;
|
|
33
|
+
const sign = Math.sign(s.sweep) || 1;
|
|
34
|
+
return { tangent: { x: -Math.sin(a) * sign, y: Math.cos(a) * sign }, curvature: s.radius > EPS ? sign / s.radius : 0 };
|
|
35
|
+
}
|
|
36
|
+
// A cubic curve: B'(0) = 3 (c1 - p0), B''(0) = 6 (p0 - 2 c1 + c2), B'(1) = 3 (p3 - c2), B''(1) = 6 (c1 - 2 c2 + p3).
|
|
37
|
+
const [p0, p1, p2, p3] = [from, s.c1, s.c2, s.to];
|
|
38
|
+
const d = t === 0 ? { x: 3 * (p1.x - p0.x), y: 3 * (p1.y - p0.y) } : { x: 3 * (p3.x - p2.x), y: 3 * (p3.y - p2.y) };
|
|
39
|
+
const dd = t === 0 ? { x: 6 * (p0.x - 2 * p1.x + p2.x), y: 6 * (p0.y - 2 * p1.y + p2.y) } : { x: 6 * (p1.x - 2 * p2.x + p3.x), y: 6 * (p1.y - 2 * p2.y + p3.y) };
|
|
40
|
+
const speed = Math.hypot(d.x, d.y);
|
|
41
|
+
if (speed > EPS) return { tangent: { x: d.x / speed, y: d.y / speed }, curvature: (d.x * dd.y - d.y * dd.x) / speed ** 3 };
|
|
42
|
+
// A handle of length 0: the tangent comes from the next control point that differs.
|
|
43
|
+
const fallback = t === 0 ? (unit(p2.x - p0.x, p2.y - p0.y) ?? unit(p3.x - p0.x, p3.y - p0.y)) : (unit(p3.x - p1.x, p3.y - p1.y) ?? unit(p3.x - p0.x, p3.y - p0.y));
|
|
44
|
+
return { tangent: fallback, curvature: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const angleOf = (u: Point, v: Point): number => Math.acos(Math.max(-1, Math.min(1, u.x * v.x + u.y * v.y))) * DEG;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* This function gives the joints of a curve: each point where one drawn segment ends and the next one starts.
|
|
51
|
+
* A segment of length 0 has no tangent, so the joint skips it. The analytic tangents and curvatures of lines, cubic curves and arcs give the values.
|
|
52
|
+
*/
|
|
53
|
+
export function curveJoints(segments: readonly CurveSegment[]): CurveJoint[] {
|
|
54
|
+
const out: CurveJoint[] = [];
|
|
55
|
+
let at: Point | undefined;
|
|
56
|
+
let last: { tangent: Point; curvature: number } | undefined;
|
|
57
|
+
for (const s of segments) {
|
|
58
|
+
if (s.kind === "move" || at === undefined) {
|
|
59
|
+
at = s.to;
|
|
60
|
+
last = undefined;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const start = frame(at, s, 0);
|
|
64
|
+
const end = frame(at, s, 1);
|
|
65
|
+
if (start.tangent && end.tangent) {
|
|
66
|
+
if (last) out.push({ at, angle: angleOf(last.tangent, start.tangent), jump: Math.abs(last.curvature - start.curvature), before: last.tangent, after: start.tangent });
|
|
67
|
+
last = { tangent: end.tangent, curvature: end.curvature };
|
|
68
|
+
}
|
|
69
|
+
at = s.to;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* This function gives the G1 defect of a curve (C1 of MEAS0): `g1 = max_j angle(T_j^-, T_j^+)` over the joints, in degrees.
|
|
76
|
+
* Lower is better. A curve is G1 when `g1` is 0.5 degrees or less. The cost is linear.
|
|
77
|
+
*/
|
|
78
|
+
export function g1Defect(segments: readonly CurveSegment[]): number {
|
|
79
|
+
return curveJoints(segments).reduce((m, j) => Math.max(m, j.angle), 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This function gives the G2 defect of a curve (C2 of MEAS0): `g2 = max_j abs(κ_j^- - κ_j^+) · u` over the joints.
|
|
84
|
+
* The value has no unit, and lower is better. `u` is the unit of the board (`boardUnit`). The cost is linear.
|
|
85
|
+
*/
|
|
86
|
+
export function g2Defect(segments: readonly CurveSegment[], u: number): number {
|
|
87
|
+
return curveJoints(segments).reduce((m, j) => Math.max(m, j.jump * u), 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* This function gives the bending energy of a sampled curve (C3 of MEAS0): `E = L · ∫ κ(s)^2 ds`.
|
|
92
|
+
* At each inner sample, `κ = φ / l`, so `E = L · Σ φ_i^2 / l_i`. The length `l_i` is the mean of the two legs, with a limit of `8τ / φ_i`.
|
|
93
|
+
* Thus a sharp corner counts as an arc with a sagitta of `τ`, the tolerance of the samples.
|
|
94
|
+
*
|
|
95
|
+
* The value has no unit, and lower is better. A full circle gives `4π^2`. The cost is linear.
|
|
96
|
+
*/
|
|
97
|
+
export function bendingEnergy(points: readonly Point[], tolerance = 0.25): number {
|
|
98
|
+
const q: Point[] = [];
|
|
99
|
+
for (const v of points) if (q.length === 0 || dist(q[q.length - 1]!, v) > EPS) q.push(v);
|
|
100
|
+
if (q.length < 3) return 0;
|
|
101
|
+
const phi = turns(q);
|
|
102
|
+
let sum = 0;
|
|
103
|
+
for (let i = 1; i + 1 < q.length; i++) {
|
|
104
|
+
const t = Math.abs(phi[i - 1]!);
|
|
105
|
+
if (t < EPS) continue;
|
|
106
|
+
const l = Math.min((dist(q[i - 1]!, q[i]!) + dist(q[i]!, q[i + 1]!)) / 2, (8 * tolerance) / t);
|
|
107
|
+
sum += (t * t) / l;
|
|
108
|
+
}
|
|
109
|
+
return polylineLength(q) * sum;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** This function tells if a polyline crosses itself. Two legs next to each other do not count. The cost is `n log n`. */
|
|
113
|
+
export function crossesItself(p: readonly Point[]): boolean {
|
|
114
|
+
if (p.length < 4) return false;
|
|
115
|
+
const len = polylineLength(p) / (p.length - 1);
|
|
116
|
+
const grid = new Grid<number>(Math.max(1, len * 4));
|
|
117
|
+
for (let i = 1; i < p.length; i++) grid.addSegment(i, p[i - 1]!, p[i]!);
|
|
118
|
+
for (let i = 1; i < p.length; i++) {
|
|
119
|
+
const a = p[i - 1]!;
|
|
120
|
+
const b = p[i]!;
|
|
121
|
+
const hit = grid.someSegment(a, b, 0, (j) => j > i + 1 && segmentsCross(a, b, p[j - 1]!, p[j]!));
|
|
122
|
+
if (hit) return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** This function tells if a polyline turns back on itself at a vertex: a turn of more than 179 degrees, as at a cusp. */
|
|
128
|
+
export function hasCusp(p: readonly Point[]): boolean {
|
|
129
|
+
return turns(p).some((t) => Math.abs(t) * DEG > 179);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The input of `curveFaults`: one curve and the polyline that it draws. */
|
|
133
|
+
export interface CurveCheck {
|
|
134
|
+
/** The polyline of the route. */
|
|
135
|
+
readonly polyline: readonly Point[];
|
|
136
|
+
/** The segments of the curve. */
|
|
137
|
+
readonly segments: readonly CurveSegment[];
|
|
138
|
+
/** The samples of the curve (`sampleCurve`). */
|
|
139
|
+
readonly samples: readonly Point[];
|
|
140
|
+
/** The free space of the board. Without it, the check `clear` gives false. */
|
|
141
|
+
readonly space?: CurveSpace;
|
|
142
|
+
/** With these normals, the check `ends` also examines the direction of the curve at its two ends. */
|
|
143
|
+
readonly startNormal?: Point;
|
|
144
|
+
readonly endNormal?: Point;
|
|
145
|
+
/** With this value, the check `g1` examines the joints. Give it for a curve type that is G1 by its design. */
|
|
146
|
+
readonly smooth?: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The faults of one curve: one value for each proposed hard rule of the curves in MEAS0. */
|
|
150
|
+
export interface CurveFaults {
|
|
151
|
+
/** `curve-clear`: the curve goes through a box that its polyline avoids (C7). */
|
|
152
|
+
readonly clear: boolean;
|
|
153
|
+
/** `curve-ends`: the curve misses an end of its polyline, or an end tangent is more than 1 degree off its normal (C10). */
|
|
154
|
+
readonly ends: boolean;
|
|
155
|
+
/** `curve-g1`: a joint has a G1 defect of more than 0.5 degrees, and it is not a corner that the free space keeps sharp (C1). */
|
|
156
|
+
readonly g1: boolean;
|
|
157
|
+
/** `curve-simple`: the curve crosses itself or turns back at a cusp, and its polyline does not (C9). */
|
|
158
|
+
readonly simple: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* This function tells if a joint is a corner that the free space keeps sharp. The joint is a vertex of the polyline,
|
|
163
|
+
* and the curve on one side of it goes along the leg of the polyline.
|
|
164
|
+
*/
|
|
165
|
+
function keptSharp(j: CurveJoint, polyline: readonly Point[]): boolean {
|
|
166
|
+
for (let i = 1; i + 1 < polyline.length; i++) {
|
|
167
|
+
const v = polyline[i]!;
|
|
168
|
+
if (dist(v, j.at) > 1e-6) continue;
|
|
169
|
+
const a = unit(v.x - polyline[i - 1]!.x, v.y - polyline[i - 1]!.y);
|
|
170
|
+
const b = unit(polyline[i + 1]!.x - v.x, polyline[i + 1]!.y - v.y);
|
|
171
|
+
if ((a && angleOf(a, j.before) <= 0.5) || (b && angleOf(b, j.after) <= 0.5)) return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** This function checks one curve against the proposed hard rules of the curves of MEAS0. The cost is `n log n` for each curve. */
|
|
177
|
+
export function curveFaults(c: CurveCheck): CurveFaults {
|
|
178
|
+
const p = c.polyline;
|
|
179
|
+
const s = c.samples;
|
|
180
|
+
const clear = c.space !== undefined && s.length > 1 && c.space.addsHit(s, p);
|
|
181
|
+
let ends = p.length > 0 && (s.length === 0 || dist(s[0]!, p[0]!) > TOLERANCE || dist(s[s.length - 1]!, p[p.length - 1]!) > TOLERANCE);
|
|
182
|
+
if (!ends && (c.startNormal || c.endNormal)) {
|
|
183
|
+
const joints = drawnFrames(c.segments);
|
|
184
|
+
const n0 = c.startNormal && unit(c.startNormal.x, c.startNormal.y);
|
|
185
|
+
const n1 = c.endNormal && unit(-c.endNormal.x, -c.endNormal.y);
|
|
186
|
+
if (n0 && joints.first && angleOf(n0, joints.first) > 1) ends = true;
|
|
187
|
+
if (n1 && joints.last && angleOf(n1, joints.last) > 1) ends = true;
|
|
188
|
+
}
|
|
189
|
+
const g1 = c.smooth === true && curveJoints(c.segments).some((j) => j.angle > 0.5 && !keptSharp(j, p));
|
|
190
|
+
const simple = !crossesItself(p) && !hasCusp(p) && (crossesItself(s) || hasCusp(s));
|
|
191
|
+
return { clear, ends, g1, simple };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** This function gives the unit tangent at the start of the first drawn segment and at the end of the last drawn segment. */
|
|
195
|
+
function drawnFrames(segments: readonly CurveSegment[]): { first: Point | undefined; last: Point | undefined } {
|
|
196
|
+
let at: Point | undefined;
|
|
197
|
+
let first: Point | undefined;
|
|
198
|
+
let last: Point | undefined;
|
|
199
|
+
for (const s of segments) {
|
|
200
|
+
if (s.kind === "move" || at === undefined) {
|
|
201
|
+
at = s.to;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const t0 = frame(at, s, 0).tangent;
|
|
205
|
+
const t1 = frame(at, s, 1).tangent;
|
|
206
|
+
if (first === undefined && t0) first = t0;
|
|
207
|
+
if (t1) last = t1;
|
|
208
|
+
at = s.to;
|
|
209
|
+
}
|
|
210
|
+
return { first, last };
|
|
211
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Graph, Level } from "../../model.ts";
|
|
2
|
+
import type { Section } from "../../sections.ts";
|
|
3
|
+
import { adjustedRandIndex } from "./stats.ts";
|
|
4
|
+
|
|
5
|
+
/** A grouping of the nodes: a map from the id of each node to the id of its group. */
|
|
6
|
+
export type Grouping = ReadonlyMap<string, string>;
|
|
7
|
+
|
|
8
|
+
/** This function gives the grouping of one level: each node goes to the id of its item. */
|
|
9
|
+
export function levelGrouping(level: Level): Map<string, string> {
|
|
10
|
+
const m = new Map<string, string>();
|
|
11
|
+
for (const item of level.items) for (const n of item.nodes) m.set(n, item.id);
|
|
12
|
+
return m;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** This function gives the far view of a result: the first level with more than one item, in the order of depth. */
|
|
16
|
+
export function farLevel(levels: readonly Level[]): Level | undefined {
|
|
17
|
+
return [...levels].sort((a, b) => a.depth - b.depth).find((l) => l.items.length > 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This function gives the modularity of a grouping (G1 of MEAS0, Newman and Girvan, 2004).
|
|
22
|
+
* `Q = Σ_c (e_c / m - (d_c / (2m))^2)`, with `e_c` the edges inside `c` and `d_c` the sum of the degrees in `c`.
|
|
23
|
+
* The scale is -0.5 to 1, and higher is better. A node without a group is a group alone. The cost is linear.
|
|
24
|
+
*/
|
|
25
|
+
export function modularity(graph: Graph, groups: Grouping): number {
|
|
26
|
+
const m = graph.edges.length;
|
|
27
|
+
if (m === 0) return 0;
|
|
28
|
+
const inside = new Map<string, number>();
|
|
29
|
+
const degree = new Map<string, number>();
|
|
30
|
+
const of = (v: string): string => groups.get(v) ?? `\u0000${v}`;
|
|
31
|
+
for (const e of graph.edges) {
|
|
32
|
+
const a = of(e.source.node);
|
|
33
|
+
const b = of(e.target.node);
|
|
34
|
+
degree.set(a, (degree.get(a) ?? 0) + 1);
|
|
35
|
+
degree.set(b, (degree.get(b) ?? 0) + 1);
|
|
36
|
+
if (a === b) inside.set(a, (inside.get(a) ?? 0) + 1);
|
|
37
|
+
}
|
|
38
|
+
let q = 0;
|
|
39
|
+
for (const [c, d] of degree) q += (inside.get(c) ?? 0) / m - (d / (2 * m)) ** 2;
|
|
40
|
+
return q;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* This function gives the cut share of a grouping (G2 of MEAS0): the share of the edges between two groups.
|
|
45
|
+
* For the grouping of a level, it is the sum of the counts of the links of the level, divided by `m`. The scale is 0 to 1, and lower is better.
|
|
46
|
+
*/
|
|
47
|
+
export function cutShare(graph: Graph, groups: Grouping): number {
|
|
48
|
+
const m = graph.edges.length;
|
|
49
|
+
if (m === 0) return 0;
|
|
50
|
+
let cut = 0;
|
|
51
|
+
for (const e of graph.edges) if ((groups.get(e.source.node) ?? e.source.node) !== (groups.get(e.target.node) ?? e.target.node)) cut++;
|
|
52
|
+
return cut / m;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This function gives the balance of one level (G6 of MEAS0): `H(s) / log(k)`, with `s` the shares of the nodes of the `k` items and `H` the entropy.
|
|
57
|
+
* The scale is 0 to 1, and higher is better. A value of 1 is items of equal size. A level with one item gives 1.
|
|
58
|
+
*/
|
|
59
|
+
export function balance(level: Level): number {
|
|
60
|
+
const k = level.items.length;
|
|
61
|
+
const total = level.items.reduce((s, item) => s + item.nodes.length, 0);
|
|
62
|
+
if (k < 2 || total === 0) return 1;
|
|
63
|
+
let h = 0;
|
|
64
|
+
for (const item of level.items) {
|
|
65
|
+
const p = item.nodes.length / total;
|
|
66
|
+
if (p > 0) h -= p * Math.log(p);
|
|
67
|
+
}
|
|
68
|
+
return h / Math.log(k);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* This function gives the tree cost of the levels (G8 of MEAS0, Dasgupta, 2016): `D = Σ_(u,v) leaves(lca(u, v)) / (m n)`.
|
|
73
|
+
* The levels make the tree. `leaves(lca(u, v))` is the number of nodes of the deepest item that holds both ends.
|
|
74
|
+
* Without such an item, the value is `n`. The scale is 0 to 1, and lower is better. The cost is linear times the depth.
|
|
75
|
+
*/
|
|
76
|
+
export function treeCost(graph: Graph, levels: readonly Level[]): number {
|
|
77
|
+
const n = graph.nodes.length;
|
|
78
|
+
const m = graph.edges.length;
|
|
79
|
+
if (n === 0 || m === 0) return 0;
|
|
80
|
+
const list = [...levels].sort((a, b) => b.depth - a.depth);
|
|
81
|
+
const maps = list.map((l) => {
|
|
82
|
+
const size = new Map<string, number>();
|
|
83
|
+
const of = new Map<string, string>();
|
|
84
|
+
for (const item of l.items) {
|
|
85
|
+
size.set(item.id, item.nodes.length);
|
|
86
|
+
for (const v of item.nodes) of.set(v, item.id);
|
|
87
|
+
}
|
|
88
|
+
return { size, of };
|
|
89
|
+
});
|
|
90
|
+
let sum = 0;
|
|
91
|
+
for (const e of graph.edges) {
|
|
92
|
+
let leaves = n;
|
|
93
|
+
for (const { size, of } of maps) {
|
|
94
|
+
const a = of.get(e.source.node);
|
|
95
|
+
if (a !== undefined && a === of.get(e.target.node)) {
|
|
96
|
+
leaves = size.get(a)!;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
sum += leaves;
|
|
101
|
+
}
|
|
102
|
+
return sum / (m * n);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** This function gives the leaf of each node in a tree of sections: the id of the leaf, by the index of the node. */
|
|
106
|
+
export function leafGroups(root: Section, count: number): string[] {
|
|
107
|
+
const out = new Array<string>(count).fill("");
|
|
108
|
+
const walk = (s: Section): void => {
|
|
109
|
+
if (s.children.length === 0) for (const i of s.nodes) out[i] = s.id;
|
|
110
|
+
else for (const c of s.children) walk(c);
|
|
111
|
+
};
|
|
112
|
+
walk(root);
|
|
113
|
+
return out.map((id, i) => id || `#${i}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* This function gives the group stability of two trees of the same nodes (G9 of MEAS0): the adjusted Rand index of their leaves.
|
|
118
|
+
* The second tree comes from the graph after a small change (`changeGraph`). The scale is -1 to 1, and higher is better.
|
|
119
|
+
*/
|
|
120
|
+
export function groupStability(before: Section, after: Section, count: number): number {
|
|
121
|
+
return adjustedRandIndex(leafGroups(before, count), leafGroups(after, count));
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./stats.ts";
|
|
2
|
+
export * from "./routes.ts";
|
|
3
|
+
export * from "./bundles.ts";
|
|
4
|
+
export * from "./curves.ts";
|
|
5
|
+
export * from "./levels.ts";
|
|
6
|
+
export * from "./groups.ts";
|
|
7
|
+
export * from "./stability.ts";
|
|
8
|
+
export * from "./room.ts";
|
|
9
|
+
export * from "./pareto.ts";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { center, dist } from "../../geom.ts";
|
|
2
|
+
import type { Graph, Level } from "../../model.ts";
|
|
3
|
+
import { kendallTau } from "./stats.ts";
|
|
4
|
+
|
|
5
|
+
/** This function gives the levels in the order of their depth, from the far view to the near view. */
|
|
6
|
+
function byDepth(levels: readonly Level[]): Level[] {
|
|
7
|
+
return [...levels].sort((a, b) => a.depth - b.depth);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** This function gives a map from the id of each node to the index of its item in the level. */
|
|
11
|
+
function itemIndex(level: Level): Map<string, number> {
|
|
12
|
+
const m = new Map<string, number>();
|
|
13
|
+
level.items.forEach((item, i) => {
|
|
14
|
+
for (const n of item.nodes) m.set(n, i);
|
|
15
|
+
});
|
|
16
|
+
return m;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This function counts the faults of the nesting of the levels (Z6 of MEAS0, the proposed rule `level-nesting`).
|
|
21
|
+
* It counts the items of each level whose nodes are in more than one item of the level before it. Lower is better, and 0 is correct.
|
|
22
|
+
* The cost is linear.
|
|
23
|
+
*/
|
|
24
|
+
export function nestingFaults(levels: readonly Level[]): number {
|
|
25
|
+
const list = byDepth(levels);
|
|
26
|
+
let faults = 0;
|
|
27
|
+
for (let d = 1; d < list.length; d++) {
|
|
28
|
+
const coarse = itemIndex(list[d - 1]!);
|
|
29
|
+
for (const item of list[d]!.items) {
|
|
30
|
+
const parents = new Set<number | undefined>(item.nodes.map((n) => coarse.get(n)));
|
|
31
|
+
if (parents.size > 1) faults++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return faults;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* This function measures the position stability of the levels (Z7 of MEAS0).
|
|
39
|
+
* `ps = mean_(v,d) dist(center(item_d(v)), center(item_(d+1)(v))) / size(item_d(v))`, with `size` the longer side of the box.
|
|
40
|
+
* The value has no unit, and lower is better. A result with one level gives 0. The cost is linear.
|
|
41
|
+
*/
|
|
42
|
+
export function positionStability(levels: readonly Level[]): number {
|
|
43
|
+
const list = byDepth(levels);
|
|
44
|
+
let sum = 0;
|
|
45
|
+
let n = 0;
|
|
46
|
+
for (let d = 0; d + 1 < list.length; d++) {
|
|
47
|
+
const a = list[d]!;
|
|
48
|
+
const fine = itemIndex(list[d + 1]!);
|
|
49
|
+
const items = list[d + 1]!.items;
|
|
50
|
+
for (const item of a.items) {
|
|
51
|
+
const size = Math.max(item.box.w, item.box.h);
|
|
52
|
+
if (size <= 0) continue;
|
|
53
|
+
const c = center(item.box);
|
|
54
|
+
for (const v of item.nodes) {
|
|
55
|
+
const j = fine.get(v);
|
|
56
|
+
if (j === undefined) continue;
|
|
57
|
+
sum += dist(c, center(items[j]!.box)) / size;
|
|
58
|
+
n++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return n ? sum / n : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This function measures the order stability of the levels with groups (Z8 of MEAS0).
|
|
67
|
+
* For each level, `τ_x` and `τ_y` are the Kendall tau between the item centers and the centroids of their nodes on each axis.
|
|
68
|
+
* The value is the mean of `(τ_x + τ_y) / 2` over the levels with groups and two items or more. The scale is -1 to 1, and higher is better.
|
|
69
|
+
* A result without such a level gives 1. The cost is `n log n`, with a sample above 1500 items.
|
|
70
|
+
*/
|
|
71
|
+
export function orderStability(graph: Graph, levels: readonly Level[]): number {
|
|
72
|
+
const centers = new Map(graph.nodes.map((n) => [n.id, center(n.box)] as const));
|
|
73
|
+
let sum = 0;
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (const level of levels) {
|
|
76
|
+
if (level.items.length < 2 || !level.items.some((i) => i.nodes.length > 1)) continue;
|
|
77
|
+
const ix: number[] = [];
|
|
78
|
+
const iy: number[] = [];
|
|
79
|
+
const nx: number[] = [];
|
|
80
|
+
const ny: number[] = [];
|
|
81
|
+
for (const item of level.items) {
|
|
82
|
+
let x = 0;
|
|
83
|
+
let y = 0;
|
|
84
|
+
let k = 0;
|
|
85
|
+
for (const v of item.nodes) {
|
|
86
|
+
const c = centers.get(v);
|
|
87
|
+
if (!c) continue;
|
|
88
|
+
x += c.x;
|
|
89
|
+
y += c.y;
|
|
90
|
+
k++;
|
|
91
|
+
}
|
|
92
|
+
if (k === 0) continue;
|
|
93
|
+
const c = center(item.box);
|
|
94
|
+
ix.push(c.x);
|
|
95
|
+
iy.push(c.y);
|
|
96
|
+
nx.push(x / k);
|
|
97
|
+
ny.push(y / k);
|
|
98
|
+
}
|
|
99
|
+
sum += (kendallTau(ix, nx, level.depth + 1) + kendallTau(iy, ny, level.depth + 1)) / 2;
|
|
100
|
+
n++;
|
|
101
|
+
}
|
|
102
|
+
return n ? sum / n : 1;
|
|
103
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** One axis of a Pareto front: the key of a measure and the direction that is better. */
|
|
2
|
+
export interface Objective {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
/** `min`: lower is better. `max`: higher is better. */
|
|
5
|
+
readonly better: "min" | "max";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** This function gives the value of a measure as a cost: lower is better. A value that is not a number is the worst cost. */
|
|
9
|
+
function cost(values: Readonly<Record<string, number>>, o: Objective): number {
|
|
10
|
+
const v = values[o.key];
|
|
11
|
+
if (v === undefined || Number.isNaN(v)) return Infinity;
|
|
12
|
+
return o.better === "min" ? v : -v;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function tells if the values `a` dominate the values `b`: `a` is no worse on each objective and better on one objective.
|
|
17
|
+
* A missing value or `NaN` is the worst value.
|
|
18
|
+
*/
|
|
19
|
+
export function dominates(a: Readonly<Record<string, number>>, b: Readonly<Record<string, number>>, objectives: readonly Objective[]): boolean {
|
|
20
|
+
let better = false;
|
|
21
|
+
for (const o of objectives) {
|
|
22
|
+
const ca = cost(a, o);
|
|
23
|
+
const cb = cost(b, o);
|
|
24
|
+
if (ca > cb) return false;
|
|
25
|
+
if (ca < cb) better = true;
|
|
26
|
+
}
|
|
27
|
+
return better;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* This function gives the Pareto front of a list: the items that no other item dominates (MEAS0, "Pareto fronts").
|
|
32
|
+
* The function `values` gives the measures of an item. The front keeps the order of the list. The cost is quadratic in the items.
|
|
33
|
+
*/
|
|
34
|
+
export function paretoFront<T>(items: readonly T[], values: (item: T) => Readonly<Record<string, number>>, objectives: readonly Objective[]): T[] {
|
|
35
|
+
const v = items.map(values);
|
|
36
|
+
return items.filter((_, i) => !v.some((w, j) => j !== i && dominates(w, v[i]!, objectives)));
|
|
37
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { rng } from "../../fixtures.ts";
|
|
2
|
+
import type { Box } from "../../geom.ts";
|
|
3
|
+
import type { Boxed } from "../../room/index.ts";
|
|
4
|
+
|
|
5
|
+
/** Up to this number of boxes, `roomOrderFaults` examines each pair. */
|
|
6
|
+
const EXACT_PAIRS = 2000;
|
|
7
|
+
const SAMPLE_PAIRS = 2_000_000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This function counts the pairs of boxes whose order or alignment changes on an axis (M2 of MEAS0, the proposed rule `room-order`).
|
|
11
|
+
* On each axis, it compares the order of the low edges, the order of the high edges, and the relation "a ends before b starts".
|
|
12
|
+
* Two values nearer than `snap` are equal. Lower is better, and 0 is correct. Above 2000 boxes, a seeded sample of pairs gives an estimate.
|
|
13
|
+
*/
|
|
14
|
+
export function roomOrderFaults(before: readonly Boxed[], after: readonly Boxed[], snap = 0.5): number {
|
|
15
|
+
const next = new Map(after.map((b) => [b.id, b.box] as const));
|
|
16
|
+
const list: [Box, Box][] = [];
|
|
17
|
+
for (const b of before) {
|
|
18
|
+
const a = next.get(b.id);
|
|
19
|
+
if (a) list.push([b.box, a]);
|
|
20
|
+
}
|
|
21
|
+
const sign = (x: number): number => (x > snap ? 1 : x < -snap ? -1 : 0);
|
|
22
|
+
// The key packs the eight relations of a pair into one number in base 3.
|
|
23
|
+
const axis = (p0: number, p1: number, q0: number, q1: number): number =>
|
|
24
|
+
(sign(p0 - q0) + 1) * 27 + (sign(p1 - q1) + 1) * 9 + (q0 - p1 >= -snap ? 3 : 0) + (p0 - q1 >= -snap ? 1 : 0);
|
|
25
|
+
const key = (p: Box, q: Box): number => axis(p.x, p.x + p.w, q.x, q.x + q.w) * 81 + axis(p.y, p.y + p.h, q.y, q.y + q.h);
|
|
26
|
+
const changed = (i: number, j: number): boolean => key(list[i]![0], list[j]![0]) !== key(list[i]![1], list[j]![1]);
|
|
27
|
+
const n = list.length;
|
|
28
|
+
let faults = 0;
|
|
29
|
+
if (n <= EXACT_PAIRS) {
|
|
30
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) if (changed(i, j)) faults++;
|
|
31
|
+
return faults;
|
|
32
|
+
}
|
|
33
|
+
const R = rng(n);
|
|
34
|
+
let seen = 0;
|
|
35
|
+
for (let k = 0; k < SAMPLE_PAIRS; k++) {
|
|
36
|
+
const i = Math.floor(R() * n);
|
|
37
|
+
const j = Math.floor(R() * n);
|
|
38
|
+
if (i === j) continue;
|
|
39
|
+
seen++;
|
|
40
|
+
if (changed(i, j)) faults++;
|
|
41
|
+
}
|
|
42
|
+
return Math.round((faults * (n * (n - 1)) / 2) / Math.max(1, seen));
|
|
43
|
+
}
|