@expofp/wayfinding-compiler 0.0.0-experimental.3b0db16 → 0.0.0-stage

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.
Files changed (100) hide show
  1. package/README.md +2 -445
  2. package/package.json +3 -34
  3. package/dist/index.d.ts +0 -14
  4. package/dist/index.js +0 -3
  5. package/dist/lib/attach.d.ts +0 -43
  6. package/dist/lib/attach.js +0 -199
  7. package/dist/lib/axes.d.ts +0 -76
  8. package/dist/lib/axes.js +0 -103
  9. package/dist/lib/blockers.d.ts +0 -134
  10. package/dist/lib/blockers.js +0 -289
  11. package/dist/lib/cell-array.d.ts +0 -14
  12. package/dist/lib/cell-array.js +0 -25
  13. package/dist/lib/compile-layer.d.ts +0 -41
  14. package/dist/lib/compile-layer.js +0 -241
  15. package/dist/lib/compile-wayfinding.d.ts +0 -90
  16. package/dist/lib/compile-wayfinding.js +0 -203
  17. package/dist/lib/connect.d.ts +0 -35
  18. package/dist/lib/connect.js +0 -206
  19. package/dist/lib/diagnostics.d.ts +0 -190
  20. package/dist/lib/diagnostics.js +0 -634
  21. package/dist/lib/doorways.d.ts +0 -78
  22. package/dist/lib/doorways.js +0 -331
  23. package/dist/lib/emit-network.d.ts +0 -47
  24. package/dist/lib/emit-network.js +0 -105
  25. package/dist/lib/emit-v1.d.ts +0 -31
  26. package/dist/lib/emit-v1.js +0 -56
  27. package/dist/lib/entries.d.ts +0 -123
  28. package/dist/lib/entries.js +0 -448
  29. package/dist/lib/floor-grids.d.ts +0 -48
  30. package/dist/lib/floor-grids.js +0 -185
  31. package/dist/lib/framing.d.ts +0 -20
  32. package/dist/lib/framing.js +0 -187
  33. package/dist/lib/geom/boolean-ops.d.ts +0 -21
  34. package/dist/lib/geom/boolean-ops.js +0 -1
  35. package/dist/lib/geom/clipper2-ops.d.ts +0 -7
  36. package/dist/lib/geom/clipper2-ops.js +0 -50
  37. package/dist/lib/geom/footprint.d.ts +0 -26
  38. package/dist/lib/geom/footprint.js +0 -203
  39. package/dist/lib/geom/frames.d.ts +0 -116
  40. package/dist/lib/geom/frames.js +0 -233
  41. package/dist/lib/geom/mesh-outline.d.ts +0 -13
  42. package/dist/lib/geom/mesh-outline.js +0 -90
  43. package/dist/lib/geom/normalize.d.ts +0 -28
  44. package/dist/lib/geom/normalize.js +0 -118
  45. package/dist/lib/geom/point-key.d.ts +0 -16
  46. package/dist/lib/geom/point-key.js +0 -15
  47. package/dist/lib/geom/primitives.d.ts +0 -105
  48. package/dist/lib/geom/primitives.js +0 -184
  49. package/dist/lib/geom/split-crossings.d.ts +0 -14
  50. package/dist/lib/geom/split-crossings.js +0 -171
  51. package/dist/lib/geom/types.d.ts +0 -9
  52. package/dist/lib/geom/types.js +0 -1
  53. package/dist/lib/geom/walls.d.ts +0 -31
  54. package/dist/lib/geom/walls.js +0 -71
  55. package/dist/lib/graph-invariants.d.ts +0 -49
  56. package/dist/lib/graph-invariants.js +0 -129
  57. package/dist/lib/graph-types.d.ts +0 -143
  58. package/dist/lib/graph-types.js +0 -7
  59. package/dist/lib/mask.d.ts +0 -54
  60. package/dist/lib/mask.js +0 -210
  61. package/dist/lib/min-heap.d.ts +0 -24
  62. package/dist/lib/min-heap.js +0 -73
  63. package/dist/lib/network-build.d.ts +0 -21
  64. package/dist/lib/network-build.js +0 -4
  65. package/dist/lib/network-graph.d.ts +0 -80
  66. package/dist/lib/network-graph.js +0 -467
  67. package/dist/lib/network-sizes.d.ts +0 -29
  68. package/dist/lib/network-sizes.js +0 -22
  69. package/dist/lib/network-store.d.ts +0 -173
  70. package/dist/lib/network-store.js +0 -430
  71. package/dist/lib/openings.d.ts +0 -54
  72. package/dist/lib/openings.js +0 -387
  73. package/dist/lib/place-path.d.ts +0 -47
  74. package/dist/lib/place-path.js +0 -346
  75. package/dist/lib/post-passes.d.ts +0 -31
  76. package/dist/lib/post-passes.js +0 -305
  77. package/dist/lib/reachability.d.ts +0 -58
  78. package/dist/lib/reachability.js +0 -220
  79. package/dist/lib/report.d.ts +0 -49
  80. package/dist/lib/report.js +0 -374
  81. package/dist/lib/router.d.ts +0 -111
  82. package/dist/lib/router.js +0 -388
  83. package/dist/lib/runs.d.ts +0 -92
  84. package/dist/lib/runs.js +0 -232
  85. package/dist/lib/scale.d.ts +0 -58
  86. package/dist/lib/scale.js +0 -185
  87. package/dist/lib/steiner-network.d.ts +0 -19
  88. package/dist/lib/steiner-network.js +0 -183
  89. package/dist/lib/strip.d.ts +0 -43
  90. package/dist/lib/strip.js +0 -200
  91. package/dist/lib/stubs.d.ts +0 -84
  92. package/dist/lib/stubs.js +0 -316
  93. package/dist/lib/transitions.d.ts +0 -72
  94. package/dist/lib/transitions.js +0 -191
  95. package/dist/lib/types.d.ts +0 -126
  96. package/dist/lib/types.js +0 -1
  97. package/dist/lib/validate-source.d.ts +0 -41
  98. package/dist/lib/validate-source.js +0 -133
  99. package/dist/lib/walkable.d.ts +0 -104
  100. package/dist/lib/walkable.js +0 -140
@@ -1,24 +0,0 @@
1
- /**
2
- * Allocation-free binary min-heap of (key, value) numbers: `pop` returns the value and leaves the key in
3
- * {@link poppedKey}.
4
- */
5
- export declare class MinHeap {
6
- private readonly k;
7
- private readonly v;
8
- /** Key of the last popped entry. */
9
- poppedKey: number;
10
- /** Entry count. */
11
- get size(): number;
12
- /**
13
- * Add an entry.
14
- * @param key What the heap orders by.
15
- * @param val The value carried with it.
16
- */
17
- push(key: number, val: number): void;
18
- /**
19
- * Remove the smallest-key entry.
20
- * @returns Its value; the key goes to {@link poppedKey}.
21
- */
22
- pop(): number;
23
- }
24
- //# sourceMappingURL=min-heap.d.ts.map
@@ -1,73 +0,0 @@
1
- /**
2
- * Allocation-free binary min-heap of (key, value) numbers: `pop` returns the value and leaves the key in
3
- * {@link poppedKey}.
4
- */
5
- export class MinHeap {
6
- k = [];
7
- v = [];
8
- /** Key of the last popped entry. */
9
- poppedKey = 0;
10
- /** Entry count. */
11
- get size() {
12
- return this.k.length;
13
- }
14
- /**
15
- * Add an entry.
16
- * @param key What the heap orders by.
17
- * @param val The value carried with it.
18
- */
19
- push(key, val) {
20
- const { k, v } = this;
21
- let i = k.length;
22
- k.push(key);
23
- v.push(val);
24
- while (i > 0) {
25
- const p = (i - 1) >> 1;
26
- if (k[p] <= k[i])
27
- break;
28
- const tk = k[p];
29
- const tv = v[p];
30
- k[p] = k[i];
31
- v[p] = v[i];
32
- k[i] = tk;
33
- v[i] = tv;
34
- i = p;
35
- }
36
- }
37
- /**
38
- * Remove the smallest-key entry.
39
- * @returns Its value; the key goes to {@link poppedKey}.
40
- */
41
- pop() {
42
- const { k, v } = this;
43
- const topK = k[0];
44
- const topV = v[0];
45
- const lk = k.pop();
46
- const lv = v.pop();
47
- if (k.length) {
48
- k[0] = lk;
49
- v[0] = lv;
50
- let i = 0;
51
- for (;;) {
52
- const l = 2 * i + 1;
53
- const r = l + 1;
54
- let m = i;
55
- if (l < k.length && k[l] < k[m])
56
- m = l;
57
- if (r < k.length && k[r] < k[m])
58
- m = r;
59
- if (m === i)
60
- break;
61
- const tk = k[m];
62
- const tv = v[m];
63
- k[m] = k[i];
64
- v[m] = v[i];
65
- k[i] = tk;
66
- v[i] = tv;
67
- i = m;
68
- }
69
- }
70
- this.poppedKey = topK;
71
- return topV;
72
- }
73
- }
@@ -1,21 +0,0 @@
1
- import type { Blockers } from './blockers.js';
2
- import type { Frame } from './geom/frames.js';
3
- import type { RouteNetworkDestination, Unreached, UnreachedReason } from './graph-types.js';
4
- import type { NetworkSizes } from './network-sizes.js';
5
- /** The layer as every pass of the network build reads it. */
6
- export interface NetworkBuild {
7
- /** Network sizes. */
8
- readonly cfg: NetworkSizes;
9
- /** The directions the layer is drawn in, the plan's first. */
10
- readonly frames: readonly Frame[];
11
- /** What a line or a dock may not cross. */
12
- readonly blockers: Blockers;
13
- /** The layer's destinations, by id. */
14
- readonly destinationOf: ReadonlyMap<string, RouteNetworkDestination>;
15
- /**
16
- * Records why a destination has no dock yet, with where and by which check; the most informative
17
- * reason is kept, with its facts.
18
- */
19
- readonly note: (id: string, reason: UnreachedReason, facts?: Omit<Unreached, 'id' | 'reason'>) => void;
20
- }
21
- //# sourceMappingURL=network-build.d.ts.map
@@ -1,4 +0,0 @@
1
- // What every pass of the network build reads about its layer: the sizes, the directions the layer
2
- // is drawn in, what a line may not cross, the destinations by id, and where to note a destination
3
- // the build could not serve.
4
- export {};
@@ -1,80 +0,0 @@
1
- import { type Frame } from './geom/frames.js';
2
- import type { Segment } from './geom/types.js';
3
- /** A run of network points: a chain of segments through vertices no third segment meets. */
4
- export interface Chain {
5
- /** Its points, in order. */
6
- pts: [number, number][];
7
- }
8
- /**
9
- * Reads a segment soup as chains: every maximal run through vertices of degree two, straightened
10
- * where it runs straight through a point.
11
- * @param soup The segments.
12
- * @param protect Keys of points a chain keeps as vertices however straight it runs through them.
13
- * @returns The chains, coordinates on the centimeter grid.
14
- */
15
- export declare function chainsFromEdges(soup: readonly Segment[], protect?: ReadonlySet<string>): Chain[];
16
- /** What the joining may read of the floor. */
17
- interface JoinOptions {
18
- /** The directions the layer is drawn in. */
19
- readonly frames?: readonly Frame[];
20
- /** Whether a segment lies on free floor: across no wall or stand. Everything is free by default. */
21
- readonly free?: (seg: Segment) => boolean;
22
- /**
23
- * How far aside, and how far ahead, the run continuing another may start: a stub's length. Without
24
- * it no run is pulled over.
25
- */
26
- readonly lane?: number;
27
- }
28
- /** The lines joined into straight runs, and where that moved a point. */
29
- interface JoinedRuns {
30
- /** The runs, each straight stretch one chord. */
31
- readonly chains: Chain[];
32
- /** Every vertex a chord replaced, by its key, and where it now lies on the chord. */
33
- readonly moved: Map<string, [number, number]>;
34
- }
35
- /**
36
- * Joins the chains that continue one another through a shared end, and makes each straight stretch
37
- * one chord: an aisle is one line from end to end, the stubs meeting it end on its span, and two aisles
38
- * cross whole. Two pieces continue one another when they run along the same axis of the same frame
39
- * in opposite senses, or, off every axis, when the joint lies on the chord between its neighbors.
40
- * Every vertex a chord replaces is moved onto the chord, and so are the ends of the lines that stood
41
- * on it. A run then continues through the next run along its axis when nothing stands between them
42
- * on the floor: that run is pulled onto its line, longest runs first, so a line never starts where the
43
- * one before it could go on. The converter that builds wf.data.js cuts a line where another ends on
44
- * it or crosses it.
45
- * @param chains The chains, ends on shared vertices.
46
- * @param options The layer's directions and its free floor.
47
- * @returns The joined runs, and where the vertices they replaced now lie.
48
- */
49
- export declare function joinStraightRuns(chains: readonly Chain[], options?: JoinOptions): JoinedRuns;
50
- /** The network as a graph of its vertices. */
51
- export interface Graph {
52
- /** Vertex positions, by vertex id. */
53
- readonly nodes: [number, number][];
54
- /** Neighbors of each vertex: vertex, length, edge id. */
55
- readonly adj: [number, number, number][][];
56
- /** Edge ends, by edge id. */
57
- readonly edges: [number, number][];
58
- /** Vertex id by point key. */
59
- readonly nodeId: Map<string, number>;
60
- }
61
- /**
62
- * Chains as segments, one per pair of neighboring points.
63
- * @param chains The chains.
64
- * @returns Their segments, in chain order.
65
- */
66
- export declare function chainSegments(chains: readonly Chain[]): Segment[];
67
- /**
68
- * Reads segments as a graph; vertices merge by point key.
69
- * @param segments The segments.
70
- * @returns The graph.
71
- */
72
- export declare function graphOf(segments: readonly Segment[]): Graph;
73
- /**
74
- * The connected pieces of a graph.
75
- * @param graph The graph.
76
- * @returns Each vertex's piece, by vertex id.
77
- */
78
- export declare function components(graph: Graph): Int32Array;
79
- export {};
80
- //# sourceMappingURL=network-graph.d.ts.map
@@ -1,467 +0,0 @@
1
- // The network read as a graph: its segments joined into chains, the chains as vertices and edges,
2
- // and the connected pieces.
3
- import { nearestFrame, toFrame } from './geom/frames.js';
4
- import { POINT_EPS, pointKey } from './geom/point-key.js';
5
- /**
6
- * Reads a segment soup as chains: every maximal run through vertices of degree two, straightened
7
- * where it runs straight through a point.
8
- * @param soup The segments.
9
- * @param protect Keys of points a chain keeps as vertices however straight it runs through them.
10
- * @returns The chains, coordinates on the centimeter grid.
11
- */
12
- export function chainsFromEdges(soup, protect) {
13
- const nodePos = new Map();
14
- const adjK = new Map();
15
- const ek = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
16
- const edgeSet = new Set();
17
- for (const [x0, y0, x1, y1] of soup) {
18
- const a = pointKey(x0, y0);
19
- const b = pointKey(x1, y1);
20
- if (a === b || edgeSet.has(ek(a, b)))
21
- continue;
22
- edgeSet.add(ek(a, b));
23
- nodePos.set(a, [x0, y0]);
24
- nodePos.set(b, [x1, y1]);
25
- if (!adjK.has(a))
26
- adjK.set(a, []);
27
- if (!adjK.has(b))
28
- adjK.set(b, []);
29
- adjK.get(a).push(b);
30
- adjK.get(b).push(a);
31
- }
32
- const used = new Set();
33
- const chains = [];
34
- const trace = (start, next) => {
35
- const pts = [nodePos.get(start), nodePos.get(next)];
36
- used.add(ek(start, next));
37
- let prev = start;
38
- let cur = next;
39
- while (adjK.get(cur).length === 2) {
40
- const nxt = adjK.get(cur).find((k) => k !== prev);
41
- if (nxt === undefined || used.has(ek(cur, nxt)))
42
- break;
43
- used.add(ek(cur, nxt));
44
- pts.push(nodePos.get(nxt));
45
- prev = cur;
46
- cur = nxt;
47
- }
48
- return pts;
49
- };
50
- for (const [k, nb] of adjK) {
51
- if (nb.length === 2)
52
- continue;
53
- for (const n of nb)
54
- if (!used.has(ek(k, n)))
55
- chains.push({ pts: trace(k, n) });
56
- }
57
- for (const [k, nb] of adjK)
58
- for (const n of nb)
59
- if (!used.has(ek(k, n)))
60
- chains.push({ pts: trace(k, n) });
61
- for (const c of chains) {
62
- const out = [c.pts[0]];
63
- for (let i = 1; i + 1 < c.pts.length; i++) {
64
- const [ax, ay] = out[out.length - 1];
65
- const [bx, by] = c.pts[i];
66
- const [cx, cy] = c.pts[i + 1];
67
- const kept = protect?.has(pointKey(bx, by));
68
- const cross = Math.abs((bx - ax) * (cy - ay) - (by - ay) * (cx - ax));
69
- const len = Math.hypot(cx - ax, cy - ay) || 1;
70
- // Drop a point only where the chain runs straight through it (within POINT_EPS): a few-cm jog
71
- // is a corner, and a chord across it would be off every axis.
72
- if (kept || cross / len > POINT_EPS)
73
- out.push(c.pts[i]);
74
- }
75
- out.push(c.pts[c.pts.length - 1]);
76
- c.pts = out.map((p) => [+p[0].toFixed(2), +p[1].toFixed(2)]);
77
- }
78
- return chains.filter((c) => c.pts.length >= 2);
79
- }
80
- /** How far off a frame axis a line may point and still run along it: about two degrees. */
81
- const AXIS_TAN = 0.035;
82
- /**
83
- * Which frame axis a direction runs along, if any.
84
- * @param frames The directions the layer is drawn in.
85
- * @param dx Direction x.
86
- * @param dy Direction y.
87
- * @returns The axis, or null off every axis.
88
- */
89
- function axisOf(frames, dx, dy) {
90
- if (!frames.length)
91
- return null;
92
- const frame = nearestFrame(frames, dx, dy);
93
- const [u, v] = toFrame(frames[frame], dx, dy);
94
- if (Math.abs(v) <= AXIS_TAN * Math.abs(u))
95
- return { frame, axis: 0, sign: Math.sign(u) };
96
- if (Math.abs(u) <= AXIS_TAN * Math.abs(v))
97
- return { frame, axis: 1, sign: Math.sign(v) };
98
- return null;
99
- }
100
- /**
101
- * Joins the chains that continue one another through a shared end, and makes each straight stretch
102
- * one chord: an aisle is one line from end to end, the stubs meeting it end on its span, and two aisles
103
- * cross whole. Two pieces continue one another when they run along the same axis of the same frame
104
- * in opposite senses, or, off every axis, when the joint lies on the chord between its neighbors.
105
- * Every vertex a chord replaces is moved onto the chord, and so are the ends of the lines that stood
106
- * on it. A run then continues through the next run along its axis when nothing stands between them
107
- * on the floor: that run is pulled onto its line, longest runs first, so a line never starts where the
108
- * one before it could go on. The converter that builds wf.data.js cuts a line where another ends on
109
- * it or crosses it.
110
- * @param chains The chains, ends on shared vertices.
111
- * @param options The layer's directions and its free floor.
112
- * @returns The joined runs, and where the vertices they replaced now lie.
113
- */
114
- export function joinStraightRuns(chains, options = {}) {
115
- const frames = options.frames ?? [];
116
- const free = options.free ?? (() => true);
117
- const runs = chains.map((c) => c.pts.map((p) => [p[0], p[1]]));
118
- const owner = runs.map((_, i) => i);
119
- const find = (i) => {
120
- while (owner[i] !== i)
121
- i = owner[i];
122
- return i;
123
- };
124
- const meetings = new Map();
125
- runs.forEach((pts, i) => {
126
- if (!pts || pts.length < 2)
127
- return;
128
- for (const p of [pts[0], pts[pts.length - 1]]) {
129
- const key = pointKey(p[0], p[1]);
130
- meetings.set(key, [...(meetings.get(key) ?? []), i]);
131
- }
132
- });
133
- // A run turned to end at `key`, and its point next to that end.
134
- const endAt = (i, key) => {
135
- const pts = runs[i];
136
- const last = pts[pts.length - 1];
137
- if (pointKey(last[0], last[1]) === key)
138
- return { pts, next: pts[pts.length - 2] };
139
- const first = pts[0];
140
- if (pointKey(first[0], first[1]) === key)
141
- return { pts: [...pts].reverse(), next: pts[1] };
142
- return null;
143
- };
144
- // How far from straight two pieces meeting at a joint are: the angle between them, or Infinity.
145
- const bend = (joint, a, c) => {
146
- const [ax, ay] = [a[0] - joint[0], a[1] - joint[1]];
147
- const [cx, cy] = [c[0] - joint[0], c[1] - joint[1]];
148
- const la = Math.hypot(ax, ay);
149
- const lc = Math.hypot(cx, cy);
150
- if (!la || !lc)
151
- return Infinity;
152
- const sameAxis = (p, q) => p !== null && q !== null && p.frame === q.frame && p.axis === q.axis && p.sign === -q.sign;
153
- const angle = Math.acos(Math.max(-1, Math.min(1, -(ax * cx + ay * cy) / (la * lc))));
154
- if (sameAxis(axisOf(frames, ax, ay), axisOf(frames, cx, cy)))
155
- return angle;
156
- // Off the axes: straight through within a centimeter of the chord.
157
- const len = Math.hypot(c[0] - a[0], c[1] - a[1]) || 1;
158
- const off = Math.abs((joint[0] - a[0]) * (c[1] - a[1]) - (joint[1] - a[1]) * (c[0] - a[0])) / len;
159
- const between = ax * cx + ay * cy < 0;
160
- return off <= POINT_EPS && between ? angle : Infinity;
161
- };
162
- for (const key of [...meetings.keys()].sort()) {
163
- // Every continuing pair at the meeting joins, straightest first: a crossing keeps both lines whole.
164
- for (;;) {
165
- const incident = [...new Set(meetings.get(key).map(find))];
166
- let best = null;
167
- for (let a = 0; a < incident.length; a++)
168
- for (let b = a + 1; b < incident.length; b++) {
169
- const i = incident[a];
170
- const j = incident[b];
171
- const ei = endAt(i, key);
172
- const ej = endAt(j, key);
173
- if (!ei || !ej)
174
- continue;
175
- const angle = bend(ei.pts[ei.pts.length - 1], ei.next, ej.next);
176
- if (!isFinite(angle))
177
- continue;
178
- if (!best || angle < best.angle)
179
- best = { angle, i, j };
180
- }
181
- if (!best)
182
- break;
183
- const ei = endAt(best.i, key);
184
- const ej = endAt(best.j, key);
185
- runs[best.i] = [...ei.pts, ...[...ej.pts].reverse().slice(1)];
186
- runs[best.j] = null;
187
- owner[best.j] = best.i;
188
- }
189
- }
190
- // Each straight stretch of a run becomes its chord; the vertices on it move onto the chord.
191
- const moved = new Map();
192
- const round = (p) => [+p[0].toFixed(2), +p[1].toFixed(2)];
193
- // Each straight stretch of a run becomes its chord, a run of its own; the vertices on it move onto
194
- // the chord, and the stretch carries their keys.
195
- const stretches = [];
196
- const onRun = [];
197
- const straighten = (pts) => {
198
- let from = 0;
199
- for (let i = 1; i < pts.length; i++) {
200
- const last = i === pts.length - 1;
201
- if (!last && isFinite(bend(pts[i], pts[i - 1], pts[i + 1])))
202
- continue;
203
- const [ax, ay] = pts[from];
204
- const [cx, cy] = pts[i];
205
- const len2 = (cx - ax) ** 2 + (cy - ay) ** 2 || 1;
206
- const keys = [];
207
- for (let k = from + 1; k < i; k++) {
208
- const [px, py] = pts[k];
209
- const t = ((px - ax) * (cx - ax) + (py - ay) * (cy - ay)) / len2;
210
- moved.set(pointKey(px, py), round([ax + t * (cx - ax), ay + t * (cy - ay)]));
211
- keys.push(pointKey(px, py));
212
- }
213
- stretches.push([pts[from], pts[i]]);
214
- onRun.push(keys);
215
- from = i;
216
- }
217
- };
218
- for (const pts of runs)
219
- if (pts && pts.length >= 2)
220
- straighten(pts);
221
- const lanes = options.lane
222
- ? mergeLanes(stretches, onRun, frames, free, options.lane, moved)
223
- : stretches;
224
- // A run's own ends may have stood on another run's chord; a run both ends meet at is gone.
225
- const at = (p) => moved.get(pointKey(p[0], p[1])) ?? p;
226
- return {
227
- chains: lanes
228
- .map((pts) => ({ pts: pts.map(at) }))
229
- .filter(({ pts }) => Math.hypot(pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]) > POINT_EPS),
230
- moved,
231
- };
232
- }
233
- /**
234
- * Pulls each straight run onto the line of the run it continues: the next run along the same axis,
235
- * when the floor between them is free. Longest runs first, so the longer line keeps its place and the
236
- * shorter one moves, together with every point that stood on it. Runs keep their authored points as
237
- * keys; where a point lies now is always read through `moved`.
238
- * @param runs The straight runs, two points each.
239
- * @param onRun The keys of the vertices each run's chords replaced.
240
- * @param frames The directions the layer is drawn in.
241
- * @param free Whether a segment lies on free floor.
242
- * @param lane How far aside, and how far ahead, a continuing run may start.
243
- * @param moved Where the vertices replaced so far lie; extended with every point pulled over.
244
- * @returns The runs left, the pulled ones merged into the lines they continue, ends where they lie now.
245
- */
246
- function mergeLanes(runs, onRun, frames, free, lane, moved) {
247
- const keyOf = (p) => pointKey(p[0], p[1]);
248
- const at = (p) => moved.get(keyOf(p)) ?? p;
249
- // Each run's ends: the authored point (its key) and where it lies now.
250
- const ends = runs.map((pts) => [pts[0], pts[pts.length - 1]].map((p) => ({ key: keyOf(p), raw: p })));
251
- const now = (i) => ends[i].map((e) => at(e.raw));
252
- const axis = runs.map((_, i) => {
253
- const [a, c] = now(i);
254
- return axisOf(frames, c[0] - a[0], c[1] - a[1]);
255
- });
256
- const length = (i) => {
257
- const [a, c] = now(i);
258
- return Math.hypot(c[0] - a[0], c[1] - a[1]);
259
- };
260
- const alive = runs.map(() => true);
261
- const order = runs.map((_, i) => i).sort((a, b) => length(b) - length(a) || a - b);
262
- const round = (p) => [+p[0].toFixed(2), +p[1].toFixed(2)];
263
- // The keys of the points on each run's chords, carried along when the run is pulled over.
264
- const carried = onRun.map((keys) => [...keys]);
265
- // Runs by the keys of their ends, and by the keys on their chords: what a run takes along when it
266
- // is pulled over.
267
- const atKey = new Map();
268
- const carriedBy = new Map();
269
- ends.forEach((pair, i) => {
270
- for (const e of pair)
271
- atKey.set(e.key, [...(atKey.get(e.key) ?? []), i]);
272
- });
273
- carried.forEach((keys, i) => {
274
- for (const key of keys)
275
- carriedBy.set(key, [...(carriedBy.get(key) ?? []), i]);
276
- });
277
- // Whether everything B takes along can follow it onto A's line. A run ending on one of B's points
278
- // follows only when it meets B square, and when the move lengthens it or it carries nothing: a run
279
- // shortened could end before the points that stand on it. A run that carries one of B's ends on
280
- // its chord would lose that point, so B stays.
281
- const detachable = (b, project) => {
282
- const [b0, b1] = now(b);
283
- const follows = (was, r) => {
284
- const [p, q] = now(r);
285
- const dot = (q[0] - p[0]) * (b1[0] - b0[0]) + (q[1] - p[1]) * (b1[1] - b0[1]);
286
- const lens = Math.hypot(q[0] - p[0], q[1] - p[1]) * Math.hypot(b1[0] - b0[0], b1[1] - b0[1]);
287
- if (lens === 0 || Math.abs(dot) > AXIS_TAN * lens)
288
- return false;
289
- const other = keyOf(p) === keyOf(was) ? q : p;
290
- const to = project(was);
291
- const [wx, wy] = [was[0] - other[0], was[1] - other[1]];
292
- const [tx, ty] = [to[0] - other[0], to[1] - other[1]];
293
- if (tx * wx + ty * wy >= wx * wx + wy * wy)
294
- return true;
295
- return carried[r].length === 0 && tx * wx + ty * wy >= 0;
296
- };
297
- for (const e of ends[b]) {
298
- if ((carriedBy.get(e.key) ?? []).some((r) => r !== b && alive[r]))
299
- return false;
300
- if (!(atKey.get(e.key) ?? []).every((r) => r === b || !alive[r] || follows(at(e.raw), r)))
301
- return false;
302
- }
303
- for (const key of carried[b]) {
304
- const was = moved.get(key);
305
- if (!was)
306
- continue;
307
- if (!(atKey.get(key) ?? []).every((r) => r === b || !alive[r] || follows(was, r)))
308
- return false;
309
- }
310
- return true;
311
- };
312
- let settled = false;
313
- while (!settled) {
314
- settled = true;
315
- for (const a of order) {
316
- if (!alive[a] || !axis[a])
317
- continue;
318
- for (;;) {
319
- let merged = false;
320
- for (const endIdx of [0, 1]) {
321
- const [p0, p1] = now(a);
322
- const end = endIdx === 1 ? p1 : p0;
323
- const start = endIdx === 1 ? p0 : p1;
324
- const len = Math.hypot(end[0] - start[0], end[1] - start[1]) || 1;
325
- const ux = (end[0] - start[0]) / len;
326
- const uy = (end[1] - start[1]) / len;
327
- const along = (p) => (p[0] - end[0]) * ux + (p[1] - end[1]) * uy;
328
- const aside = (p) => Math.abs((p[0] - end[0]) * uy - (p[1] - end[1]) * ux);
329
- const onLine = (s) => round([end[0] + ux * s, end[1] + uy * s]);
330
- let best = null;
331
- for (const b of order) {
332
- if (b === a || !alive[b] || !axis[b])
333
- continue;
334
- if (axis[b].frame !== axis[a].frame || axis[b].axis !== axis[a].axis)
335
- continue;
336
- const [q0, q1] = now(b);
337
- const near = Math.min(along(q0), along(q1));
338
- const far = Math.max(along(q0), along(q1));
339
- // B continues A: it starts at A's end or within a lane ahead, no farther aside than a lane,
340
- // and goes on from there. A run alongside A is a twin, not a continuation.
341
- if (near < -POINT_EPS || near > lane || far <= near)
342
- continue;
343
- if (Math.max(aside(q0), aside(q1)) > lane)
344
- continue;
345
- if (!detachable(b, (p) => onLine(along(p))))
346
- continue;
347
- if (!best || near < best.near)
348
- best = { b, near, far };
349
- }
350
- if (!best)
351
- continue;
352
- const [q0, q1] = now(best.b);
353
- const nearQ = along(q0) <= along(q1) ? q0 : q1;
354
- const farQ = nearQ === q0 ? q1 : q0;
355
- const nearP = onLine(Math.max(0, best.near));
356
- const farP = onLine(best.far);
357
- const seg = (p, q) => [
358
- p[0],
359
- p[1],
360
- q[0],
361
- q[1],
362
- ];
363
- // Free floor between the two: from B's ends across to A's line, along the gap, and along B
364
- // once pulled over.
365
- if (!free(seg(nearQ, onLine(best.near))) || !free(seg(farQ, farP)))
366
- continue;
367
- if (best.near > 0 && !free(seg(end, nearP)))
368
- continue;
369
- if (!free(seg(nearP, farP)))
370
- continue;
371
- // B's own ends and everything on its chord move onto A's line, keeping their keys.
372
- for (const key of [...ends[best.b].map((e) => e.key), ...carried[best.b]]) {
373
- const was = moved.get(key) ?? ends[best.b].find((e) => e.key === key).raw;
374
- moved.set(key, onLine(along(was)));
375
- carried[a].push(key);
376
- carriedBy.set(key, [...(carriedBy.get(key) ?? []).filter((r) => r !== best.b), a]);
377
- }
378
- // A now reaches B's far end: that end's key becomes A's, so later moves follow it.
379
- const farEnd = ends[best.b].find((e) => keyOf(at(e.raw)) === keyOf(farP)) ??
380
- ends[best.b][along(ends[best.b][0].raw) >= along(ends[best.b][1].raw) ? 0 : 1];
381
- atKey.set(ends[a][endIdx].key, (atKey.get(ends[a][endIdx].key) ?? []).filter((r) => r !== a));
382
- ends[a][endIdx] = farEnd;
383
- atKey.set(farEnd.key, [...(atKey.get(farEnd.key) ?? []), a]);
384
- moved.set(farEnd.key, farP);
385
- alive[best.b] = false;
386
- merged = true;
387
- settled = false;
388
- }
389
- if (!merged)
390
- break;
391
- }
392
- }
393
- }
394
- return runs.flatMap((_, i) => (alive[i] ? [now(i)] : []));
395
- }
396
- /**
397
- * Chains as segments, one per pair of neighboring points.
398
- * @param chains The chains.
399
- * @returns Their segments, in chain order.
400
- */
401
- export function chainSegments(chains) {
402
- const segs = [];
403
- for (const ch of chains)
404
- for (let i = 0; i + 1 < ch.pts.length; i++)
405
- segs.push([ch.pts[i][0], ch.pts[i][1], ch.pts[i + 1][0], ch.pts[i + 1][1]]);
406
- return segs;
407
- }
408
- /**
409
- * Reads segments as a graph; vertices merge by point key.
410
- * @param segments The segments.
411
- * @returns The graph.
412
- */
413
- export function graphOf(segments) {
414
- const nodeId = new Map();
415
- const nodes = [];
416
- const adj = [];
417
- const edges = [];
418
- const getNode = (x, y) => {
419
- const k = pointKey(x, y);
420
- let id = nodeId.get(k);
421
- if (id === undefined) {
422
- id = nodes.length;
423
- nodeId.set(k, id);
424
- nodes.push([x, y]);
425
- adj.push([]);
426
- }
427
- return id;
428
- };
429
- const link = (u, v) => {
430
- if (u === v)
431
- return;
432
- const d = Math.hypot(nodes[u][0] - nodes[v][0], nodes[u][1] - nodes[v][1]);
433
- const eid = edges.length;
434
- edges.push([u, v]);
435
- adj[u].push([v, d, eid]);
436
- adj[v].push([u, d, eid]);
437
- };
438
- for (const [x0, y0, x1, y1] of segments)
439
- link(getNode(x0, y0), getNode(x1, y1));
440
- return { nodes, adj, edges, nodeId };
441
- }
442
- /**
443
- * The connected pieces of a graph.
444
- * @param graph The graph.
445
- * @returns Each vertex's piece, by vertex id.
446
- */
447
- export function components(graph) {
448
- const comp = new Int32Array(graph.nodes.length).fill(-1);
449
- const stack = [];
450
- let c = 0;
451
- for (let s = 0; s < graph.nodes.length; s++) {
452
- if (comp[s] >= 0)
453
- continue;
454
- stack.push(s);
455
- comp[s] = c;
456
- while (stack.length) {
457
- const u = stack.pop();
458
- for (const [v] of graph.adj[u])
459
- if (comp[v] < 0) {
460
- comp[v] = c;
461
- stack.push(v);
462
- }
463
- }
464
- c++;
465
- }
466
- return comp;
467
- }