@mark1russell7/highways 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/build/index.d.ts +110 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +995 -68
- package/build/index.js.map +1 -1
- package/package.json +2 -1
- package/src/bench/compare.ts +24 -1
- package/src/bench/measures/lanes.ts +95 -1
- package/src/bench/measures/stability.ts +47 -3
- package/src/bench/screen/measures.ts +8 -0
- package/src/bench/screen/screen.ts +4 -1
- package/src/index.ts +1 -0
- package/src/model.ts +21 -3
- package/src/plugs/assign/index.ts +494 -0
- package/src/plugs/fix.ts +55 -0
- package/src/plugs/justify.ts +39 -7
- package/src/plugs/range.ts +120 -0
- package/src/plugs/row-cards.ts +42 -0
- package/src/plugs/side.ts +7 -0
- package/src/plugs/spread/index.ts +28 -42
- package/src/plugs/walls.ts +47 -0
- package/src/plugs.ts +4 -2
- package/src/strategies/builtins.ts +11 -1
- package/src/strategies/schemas.ts +7 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plug strategy `assign` (PLUG-ASSIGN): the plugs of each wall go to the intervals of their ends, with a least gap.
|
|
3
|
+
* An interval is the range of the port (PORT-RANGE), or the room of the wall. The assignment is exact for the count of the ends in their intervals.
|
|
4
|
+
*/
|
|
5
|
+
import { center } from "../../geom.ts";
|
|
6
|
+
import { nodeIndex, portOffset, type End, type Node, type Side } from "../../model.ts";
|
|
7
|
+
import { nearestSide, placePlug, SIDE_NORMAL, sideToward, type Plug } from "../../plugs.ts";
|
|
8
|
+
import type { PlugInput, PlugPair } from "../../strategies/model.ts";
|
|
9
|
+
import { placeInIntervals, rangeInterval } from "../range.ts";
|
|
10
|
+
import { alongOf, keyOf, pointAt, roomOf } from "../walls.ts";
|
|
11
|
+
|
|
12
|
+
/** The options of the plug strategy `assign`. */
|
|
13
|
+
export interface AssignOptions {
|
|
14
|
+
/** The least gap between two plugs of one wall. When the room of the wall is too small for all its plugs, the gap shrinks evenly. The default is 8. */
|
|
15
|
+
readonly plugGap?: number;
|
|
16
|
+
/** The distance from a plug to a corner of its box, or a quarter of the wall when the wall is short. The default is 6. */
|
|
17
|
+
readonly inset?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The defaults of the plug strategy `assign`. */
|
|
21
|
+
export const ASSIGN_DEFAULTS: Required<AssignOptions> = Object.freeze({ plugGap: 8, inset: 6 });
|
|
22
|
+
|
|
23
|
+
/** One end of a wall for `assignWall`. The places are on the axis along the wall. */
|
|
24
|
+
export interface WallEnd {
|
|
25
|
+
/** The start of the interval of the end. */
|
|
26
|
+
readonly lo: number;
|
|
27
|
+
/** The end of the interval of the end. */
|
|
28
|
+
readonly hi: number;
|
|
29
|
+
/** The place of the plug that faces the far end. The plan moves the plugs the least distance from these places. */
|
|
30
|
+
readonly ideal: number;
|
|
31
|
+
/** The sort key of the far end. Plugs in the order of the keys do not cross at the wall. */
|
|
32
|
+
readonly key: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The plan of one wall: the place of each plug, and the ends with their plug in their interval. */
|
|
36
|
+
export interface WallPlan {
|
|
37
|
+
/** The place of the plug of each end, in the order of the ends. */
|
|
38
|
+
readonly at: number[];
|
|
39
|
+
/** For each end, `true` when its plug is in its interval. */
|
|
40
|
+
readonly anchored: boolean[];
|
|
41
|
+
/** The gap of the wall: the option `gap`, or less when the room of the wall is too short for all its plugs. */
|
|
42
|
+
readonly gap: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The tolerance of the comparisons of places along a wall. */
|
|
46
|
+
const EPS = 1e-9;
|
|
47
|
+
|
|
48
|
+
/** This function gives the index of the first value of the sorted list `list` that is `v` or more. */
|
|
49
|
+
function lowerBound(list: readonly number[], v: number): number {
|
|
50
|
+
let a = 0;
|
|
51
|
+
let b = list.length;
|
|
52
|
+
while (a < b) {
|
|
53
|
+
const mid = (a + b) >> 1;
|
|
54
|
+
if (list[mid]! < v) a = mid + 1;
|
|
55
|
+
else b = mid;
|
|
56
|
+
}
|
|
57
|
+
return a;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* This function moves the place `p` out of the zones of the fixed plugs: the open intervals of the width `2·s` around each fixed plug.
|
|
62
|
+
* The list `fixed` is sorted. The place only moves up.
|
|
63
|
+
*/
|
|
64
|
+
function pushOut(p: number, fixed: readonly number[], s: number): number {
|
|
65
|
+
for (const f of fixed) if (p > f - s + EPS && p < f + s - EPS) p = f + s;
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* This function gives the gap of a wall with `n` plugs: `gap`, or the largest gap that lets all the plugs go in the room.
|
|
71
|
+
* The plugs keep the gap to the fixed plugs too.
|
|
72
|
+
*/
|
|
73
|
+
function wallGap(n: number, fixed: readonly number[], lo: number, hi: number, gap: number): number {
|
|
74
|
+
if (n <= 1) return gap;
|
|
75
|
+
if (fixed.length === 0) return Math.max(0, Math.min(gap, (hi - lo) / (n - 1)));
|
|
76
|
+
const fits = (s: number): boolean => {
|
|
77
|
+
let p = lo;
|
|
78
|
+
for (let c = 0; c < n; c++) {
|
|
79
|
+
p = pushOut(p, fixed, s);
|
|
80
|
+
if (p > hi + EPS) return false;
|
|
81
|
+
p += s;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
if (fits(gap)) return gap;
|
|
86
|
+
// The count of plugs that fit goes down when the gap goes up. A bisection finds the largest gap that fits.
|
|
87
|
+
let a = 0;
|
|
88
|
+
let b = gap;
|
|
89
|
+
for (let i = 0; i < 60; i++) {
|
|
90
|
+
const mid = (a + b) / 2;
|
|
91
|
+
if (fits(mid)) a = mid;
|
|
92
|
+
else b = mid;
|
|
93
|
+
}
|
|
94
|
+
// The largest gap puts the last plug at the end of the room, within a rounding error. The places of a plan are a start plus a multiple of the gap,
|
|
95
|
+
// and these sums round in a different way. A small margin keeps the last place in the room.
|
|
96
|
+
return Math.max(0, a - 1e-7);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** One item of the order of a wall: an end with its plug in its interval or not, or a fixed plug. */
|
|
100
|
+
type Item = { readonly end: number; readonly anchored: boolean; at: number } | { readonly end: -1; readonly anchored: true; at: number };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* This function gives the plan of the ends of one wall in the order of their keys, with no crossing at the wall.
|
|
104
|
+
* The plan has the most ends in their intervals that this order lets. The answer is exact for this order: a table of the least last place
|
|
105
|
+
* for each count of ends in their intervals.
|
|
106
|
+
*/
|
|
107
|
+
function keyOrderPlan(order: readonly number[], win: readonly ([number, number] | undefined)[], fixed: readonly number[], lo: number, hi: number, s: number): Item[] | undefined {
|
|
108
|
+
const n = order.length;
|
|
109
|
+
const nf = fixed.length;
|
|
110
|
+
const at = (i: number, j: number, c: number): number => (i * (nf + 1) + j) * (n + 1) + c;
|
|
111
|
+
const best = new Float64Array((n + 1) * (nf + 1) * (n + 1)).fill(Infinity);
|
|
112
|
+
// The step to the state: 0 passes a fixed plug, 1 places an end in its interval, 2 places an end out of its interval.
|
|
113
|
+
const step = new Int8Array(best.length);
|
|
114
|
+
best[at(0, 0, 0)] = -Infinity;
|
|
115
|
+
const relax = (k: number, v: number, how: number): void => {
|
|
116
|
+
if (v < best[k]!) {
|
|
117
|
+
best[k] = v;
|
|
118
|
+
step[k] = how;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
for (let i = 0; i <= n; i++)
|
|
122
|
+
for (let j = 0; j <= nf; j++)
|
|
123
|
+
for (let c = 0; c <= i; c++) {
|
|
124
|
+
const v = best[at(i, j, c)]!;
|
|
125
|
+
if (v === Infinity) continue;
|
|
126
|
+
const next = v === -Infinity ? -Infinity : v + s;
|
|
127
|
+
if (j < nf && (v === -Infinity || v + s <= fixed[j]! + EPS)) relax(at(i, j + 1, c), fixed[j]!, 0);
|
|
128
|
+
if (i < n) {
|
|
129
|
+
const w = win[order[i]!];
|
|
130
|
+
if (w) {
|
|
131
|
+
const p = Math.max(w[0], next);
|
|
132
|
+
if (p <= w[1] + EPS) relax(at(i + 1, j, c + 1), p, 1);
|
|
133
|
+
}
|
|
134
|
+
const q = Math.max(lo, next);
|
|
135
|
+
if (q <= hi + EPS) relax(at(i + 1, j, c), q, 2);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let c = n;
|
|
139
|
+
while (c >= 0 && best[at(n, nf, c)] === Infinity) c--;
|
|
140
|
+
if (c < 0) return undefined;
|
|
141
|
+
const items: Item[] = [];
|
|
142
|
+
let i = n;
|
|
143
|
+
let j = nf;
|
|
144
|
+
while (i > 0 || j > 0) {
|
|
145
|
+
const k = at(i, j, c);
|
|
146
|
+
const how = step[k];
|
|
147
|
+
if (how === 0) {
|
|
148
|
+
items.push({ end: -1, anchored: true, at: fixed[--j]! });
|
|
149
|
+
} else {
|
|
150
|
+
items.push({ end: order[--i]!, anchored: how === 1, at: best[k]! });
|
|
151
|
+
if (how === 1) c--;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return items.reverse();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* This function gives the most ends of a wall that can have their plug in their interval, in any order. It also gives one plan with that count.
|
|
159
|
+
* Each end without its plug in its interval still gets a plug in the room of the wall: a free plug.
|
|
160
|
+
*
|
|
161
|
+
* The plan comes from two facts. First, a plan can move each plug down until it touches its interval, the room, a fixed zone or the plug before it.
|
|
162
|
+
* Thus each plug is at a start place plus a whole number of gaps. Second, a swap of two plugs makes the last plug of the plan the end with the latest interval end.
|
|
163
|
+
* Then the ends with an interval start up to its place are before it, and the others after it. This split gives a table over the ends and two places (the recursion `count`).
|
|
164
|
+
* The free plugs have the room as interval and the latest end. Thus they split the wall into parts, and a second table chains the parts.
|
|
165
|
+
*/
|
|
166
|
+
function bestPlan(order: readonly number[], win: readonly ([number, number] | undefined)[], ideals: readonly number[], fixed: readonly number[], lo: number, hi: number, s: number): Item[] {
|
|
167
|
+
const n = order.length;
|
|
168
|
+
const jobs = order.filter((e) => win[e] !== undefined).sort((a, b) => win[a]![1] - win[b]![1] || win[a]![0] - win[b]![0] || a - b);
|
|
169
|
+
const r = jobs.length;
|
|
170
|
+
const allowed = (t: number): boolean => t >= lo - EPS && t <= hi + EPS && fixed.every((f) => t <= f - s + EPS || t >= f + s - EPS);
|
|
171
|
+
// The places that a plan needs: a start place plus a whole number of gaps.
|
|
172
|
+
const starts = [lo, ...jobs.map((e) => win[e]![0]), ...fixed.map((f) => f + s)];
|
|
173
|
+
const raw: number[] = [];
|
|
174
|
+
for (const b of starts)
|
|
175
|
+
for (let q = 0; q <= n; q++) {
|
|
176
|
+
const t = b + q * s;
|
|
177
|
+
if (t > hi + EPS) break;
|
|
178
|
+
if (allowed(t)) raw.push(t);
|
|
179
|
+
if (s === 0) break;
|
|
180
|
+
}
|
|
181
|
+
raw.sort((a, b) => a - b);
|
|
182
|
+
const T: number[] = [];
|
|
183
|
+
for (const t of raw) if (T.length === 0 || t - T[T.length - 1]! > EPS) T.push(t);
|
|
184
|
+
const m = T.length;
|
|
185
|
+
const id = (k: number, ui: number, vi: number): number => (k * (m + 2) + ui + 1) * (m + 2) + vi + 1;
|
|
186
|
+
const memo = new Map<number, number>();
|
|
187
|
+
const pick = new Map<number, number>();
|
|
188
|
+
// The most of the first k jobs with an interval start after T[ui] and up to T[vi] that fit between the two places, with the gap.
|
|
189
|
+
// The index -1 is no place before, and the index m is no place after.
|
|
190
|
+
const count = (k: number, ui: number, vi: number): number => {
|
|
191
|
+
if (k === 0) return 0;
|
|
192
|
+
const key = id(k, ui, vi);
|
|
193
|
+
const got = memo.get(key);
|
|
194
|
+
if (got !== undefined) return got;
|
|
195
|
+
const e = jobs[k - 1]!;
|
|
196
|
+
const [a, b] = win[e]!;
|
|
197
|
+
const u = ui < 0 ? -Infinity : T[ui]!;
|
|
198
|
+
const v = vi >= m ? Infinity : T[vi]!;
|
|
199
|
+
let most = count(k - 1, ui, vi);
|
|
200
|
+
let choice = -1;
|
|
201
|
+
if (a > u + EPS && a <= v + EPS) {
|
|
202
|
+
const from = Math.max(a, ui < 0 ? lo : u + s);
|
|
203
|
+
const to = Math.min(b, vi >= m ? hi : v - s);
|
|
204
|
+
for (let ti = lowerBound(T, from - EPS); ti < m && T[ti]! <= to + EPS; ti++) {
|
|
205
|
+
const got2 = 1 + count(k - 1, ui, ti) + count(k - 1, ti, vi);
|
|
206
|
+
if (got2 > most) {
|
|
207
|
+
most = got2;
|
|
208
|
+
choice = ti;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
memo.set(key, most);
|
|
213
|
+
if (choice >= 0) pick.set(key, choice);
|
|
214
|
+
return most;
|
|
215
|
+
};
|
|
216
|
+
// The chain of the free plugs: `chain[q][i]` is the most ends in their intervals before the free plug number q at T[i].
|
|
217
|
+
// A plan with no free plug is complete only when all the ends are in their intervals. Otherwise the ends out of their intervals need free plugs.
|
|
218
|
+
let total = r === n && count(r, -1, m) === n ? n : -1;
|
|
219
|
+
let bestQ = 0;
|
|
220
|
+
let bestI = -1;
|
|
221
|
+
const chain: Float64Array[] = [];
|
|
222
|
+
const back: Int32Array[] = [];
|
|
223
|
+
for (let q = 1; q <= n; q++) {
|
|
224
|
+
const cur = new Float64Array(m).fill(-Infinity);
|
|
225
|
+
const from = new Int32Array(m).fill(-1);
|
|
226
|
+
for (let i = 0; i < m; i++) {
|
|
227
|
+
if (q === 1) {
|
|
228
|
+
cur[i] = count(r, -1, i);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const prev = chain[q - 2]!;
|
|
232
|
+
for (let h = 0; h < i; h++) {
|
|
233
|
+
if (prev[h] === -Infinity || T[i]! - T[h]! < s - EPS) continue;
|
|
234
|
+
const v = prev[h]! + count(r, h, i);
|
|
235
|
+
if (v > cur[i]!) {
|
|
236
|
+
cur[i] = v;
|
|
237
|
+
from[i] = h;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
chain.push(cur);
|
|
242
|
+
back.push(from);
|
|
243
|
+
for (let i = 0; i < m; i++) {
|
|
244
|
+
if (cur[i] === -Infinity) continue;
|
|
245
|
+
const a = cur[i]! + count(r, i, m);
|
|
246
|
+
if (a + q >= n && a > total) {
|
|
247
|
+
total = a;
|
|
248
|
+
bestQ = q;
|
|
249
|
+
bestI = i;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// The places of the plan: the jobs in their intervals, then the free plugs.
|
|
254
|
+
const placed = new Map<number, number>();
|
|
255
|
+
const place = (k: number, ui: number, vi: number): void => {
|
|
256
|
+
if (k === 0) return;
|
|
257
|
+
const t = pick.get(id(k, ui, vi));
|
|
258
|
+
if (t === undefined) return place(k - 1, ui, vi);
|
|
259
|
+
placed.set(jobs[k - 1]!, T[t]!);
|
|
260
|
+
place(k - 1, ui, t);
|
|
261
|
+
place(k - 1, t, vi);
|
|
262
|
+
};
|
|
263
|
+
const free: number[] = [];
|
|
264
|
+
let ui = bestI;
|
|
265
|
+
for (let q = bestQ; q >= 1; q--) {
|
|
266
|
+
free.push(T[ui]!);
|
|
267
|
+
const h = back[q - 1]![ui]!;
|
|
268
|
+
place(r, h, ui);
|
|
269
|
+
ui = h;
|
|
270
|
+
}
|
|
271
|
+
if (bestQ === 0) place(r, -1, m);
|
|
272
|
+
else place(r, bestI, m);
|
|
273
|
+
free.reverse();
|
|
274
|
+
// The ends out of their intervals take free places in the order of their keys, with the least sum of the squares of the moves.
|
|
275
|
+
const rest = order.filter((e) => !placed.has(e));
|
|
276
|
+
const slots = assignInOrder(rest, free);
|
|
277
|
+
const items: Item[] = [];
|
|
278
|
+
for (const [e, p] of placed) items.push({ end: e, anchored: true, at: p });
|
|
279
|
+
rest.forEach((e, i) => items.push({ end: e, anchored: false, at: slots[i]! }));
|
|
280
|
+
for (const f of fixed) items.push({ end: -1, anchored: true, at: f });
|
|
281
|
+
return items.sort((a, b) => a.at - b.at || (a.end < 0 ? -1 : b.end < 0 ? 1 : order.indexOf(a.end) - order.indexOf(b.end)));
|
|
282
|
+
|
|
283
|
+
/** This function gives each end of `list` one of the sorted places `free`, in order. The sum of the squares of the moves from the ideal places is the least. */
|
|
284
|
+
function assignInOrder(list: readonly number[], places: readonly number[]): number[] {
|
|
285
|
+
const a = list.length;
|
|
286
|
+
const b = places.length;
|
|
287
|
+
const cost = (i: number, j: number): number => (places[j]! - ideals[list[i]!]!) ** 2;
|
|
288
|
+
const g = Array.from({ length: a + 1 }, () => new Float64Array(b + 1).fill(Infinity));
|
|
289
|
+
for (let j = 0; j <= b; j++) g[0]![j] = 0;
|
|
290
|
+
for (let i = 1; i <= a; i++) for (let j = i; j <= b; j++) g[i]![j] = Math.min(g[i]![j - 1]!, g[i - 1]![j - 1]! + cost(i - 1, j - 1));
|
|
291
|
+
const out = new Array<number>(a);
|
|
292
|
+
let j = b;
|
|
293
|
+
for (let i = a; i >= 1; i--) {
|
|
294
|
+
while (j > i && g[i]![j - 1] === g[i]![j]) j--;
|
|
295
|
+
out[i - 1] = places[j - 1]!;
|
|
296
|
+
j--;
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* This function gives the plan of one wall (PLUG-ASSIGN). Each end has an interval on the axis along the wall. The plugs keep at least `gap` between two of them,
|
|
304
|
+
* and between a plug and a fixed plug at a place of `fixed`. When the room is too short for all the plugs, the gap shrinks evenly. The plan has three goals, in this order:
|
|
305
|
+
* 1. the most ends with their plug in their interval (exact).
|
|
306
|
+
* 2. no crossing at the wall: the order of the keys, when that order lets the count of goal 1.
|
|
307
|
+
* 3. the least sum of the squares of the moves from the ideal places, for the order and the ends of goals 1 and 2.
|
|
308
|
+
*
|
|
309
|
+
* An end that cannot get a plug in its interval still gets a plug in the room. When the order of the keys loses ends, the plugs swap their places
|
|
310
|
+
* to come nearer to that order, while each end keeps its interval.
|
|
311
|
+
*/
|
|
312
|
+
export function assignWall(ends: readonly WallEnd[], room: readonly [number, number], gap: number, fixed: readonly number[] = []): WallPlan {
|
|
313
|
+
const n = ends.length;
|
|
314
|
+
if (n === 0) return { at: [], anchored: [], gap };
|
|
315
|
+
const [lo, hi] = room;
|
|
316
|
+
const pins = [...fixed].sort((a, b) => a - b);
|
|
317
|
+
let s = wallGap(n, pins, lo, hi, Math.max(0, gap));
|
|
318
|
+
const win = ends.map((e): [number, number] | undefined => {
|
|
319
|
+
const a = Math.max(lo, e.lo);
|
|
320
|
+
const b = Math.min(hi, e.hi);
|
|
321
|
+
return a <= b + EPS ? [a, Math.max(a, b)] : undefined;
|
|
322
|
+
});
|
|
323
|
+
const order = ends.map((_, i) => i).sort((a, b) => ends[a]!.key - ends[b]!.key || a - b);
|
|
324
|
+
let items = keyOrderPlan(order, win, pins, lo, hi, s);
|
|
325
|
+
if (!items) {
|
|
326
|
+
// The bisection of the gap can miss by a rounding error. A gap of 0 always fits.
|
|
327
|
+
s = 0;
|
|
328
|
+
items = keyOrderPlan(order, win, pins, lo, hi, s)!;
|
|
329
|
+
}
|
|
330
|
+
const inKey = items.filter((x) => x.end >= 0 && x.anchored).length;
|
|
331
|
+
const can = win.filter((w) => w !== undefined).length;
|
|
332
|
+
if (inKey < can && inKey < unionCap(win, pins, lo, hi, s, n)) {
|
|
333
|
+
const best = bestPlan(
|
|
334
|
+
order,
|
|
335
|
+
win,
|
|
336
|
+
ends.map((e) => e.ideal),
|
|
337
|
+
pins,
|
|
338
|
+
lo,
|
|
339
|
+
hi,
|
|
340
|
+
s,
|
|
341
|
+
);
|
|
342
|
+
if (best.filter((x) => x.end >= 0 && x.anchored).length > inKey) items = untangle(best, ends, win);
|
|
343
|
+
}
|
|
344
|
+
// The places of the order: each end in its interval, or in the room. A fixed plug stays at its place.
|
|
345
|
+
const span = items.map((x): [number, number] => (x.end < 0 ? [x.at, x.at] : x.anchored ? win[x.end]! : [lo, hi]));
|
|
346
|
+
const placed = placeInIntervals(
|
|
347
|
+
items.map((x) => (x.end < 0 ? x.at : ends[x.end]!.ideal)),
|
|
348
|
+
span.map((v) => v[0]),
|
|
349
|
+
span.map((v) => v[1]),
|
|
350
|
+
s,
|
|
351
|
+
);
|
|
352
|
+
const at = new Array<number>(n);
|
|
353
|
+
items.forEach((x, i) => {
|
|
354
|
+
if (x.end >= 0) at[x.end] = placed[i]!;
|
|
355
|
+
});
|
|
356
|
+
const anchored = ends.map((e, i) => at[i]! >= e.lo - 1e-7 && at[i]! <= e.hi + 1e-7 && win[i] !== undefined);
|
|
357
|
+
return { at, anchored, gap: s };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** This function gives the most plugs with the gap `s` in the union of the intervals, out of the fixed zones, and at most `n`. It is a bound of goal 1. */
|
|
361
|
+
function unionCap(win: readonly ([number, number] | undefined)[], fixed: readonly number[], lo: number, hi: number, s: number, n: number): number {
|
|
362
|
+
const parts = win.filter((w): w is [number, number] => w !== undefined).sort((a, b) => a[0] - b[0]);
|
|
363
|
+
let count = 0;
|
|
364
|
+
let p = -Infinity;
|
|
365
|
+
for (const [a, b] of parts) {
|
|
366
|
+
let v = Math.max(a, p, lo);
|
|
367
|
+
for (;;) {
|
|
368
|
+
v = pushOut(v, fixed, s);
|
|
369
|
+
if (v > b + EPS || v > hi + EPS || count >= n) break;
|
|
370
|
+
count++;
|
|
371
|
+
p = v + s;
|
|
372
|
+
v = p;
|
|
373
|
+
if (s === 0) return n;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return count;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* This function swaps the places of two neighbours of a plan when their keys are in the wrong order, and each end keeps its interval.
|
|
381
|
+
* Thus the plan comes nearer to the order of the keys. Each swap removes one inversion, so the loop ends.
|
|
382
|
+
*/
|
|
383
|
+
function untangle(items: Item[], ends: readonly WallEnd[], win: readonly ([number, number] | undefined)[]): Item[] {
|
|
384
|
+
const own = items.filter((x) => x.end >= 0);
|
|
385
|
+
const ok = (x: Item, p: number): boolean => !x.anchored || (p >= win[x.end]![0] - EPS && p <= win[x.end]![1] + EPS);
|
|
386
|
+
for (let changed = true, rounds = 0; changed && rounds <= own.length * own.length; rounds++) {
|
|
387
|
+
changed = false;
|
|
388
|
+
for (let i = 0; i + 1 < own.length; i++) {
|
|
389
|
+
const x = own[i]!;
|
|
390
|
+
const y = own[i + 1]!;
|
|
391
|
+
if (ends[x.end]!.key <= ends[y.end]!.key) continue;
|
|
392
|
+
if (!ok(x, y.at) || !ok(y, x.at)) continue;
|
|
393
|
+
const t = x.at;
|
|
394
|
+
x.at = y.at;
|
|
395
|
+
y.at = t;
|
|
396
|
+
own[i] = y;
|
|
397
|
+
own[i + 1] = x;
|
|
398
|
+
changed = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return items.sort((a, b) => a.at - b.at || (a.end < 0 ? -1 : b.end < 0 ? 1 : ends[a.end]!.key - ends[b.end]!.key));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** A free plug: one port of a node, or one end without a port. All the ends of one port share its plug. */
|
|
405
|
+
interface Slot {
|
|
406
|
+
readonly node: Node;
|
|
407
|
+
readonly side: Side;
|
|
408
|
+
readonly ideal: number;
|
|
409
|
+
readonly range?: readonly [number, number];
|
|
410
|
+
far?: Plug | string;
|
|
411
|
+
at: number;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* This function gives the plugs of the plug strategy `assign` (PLUG-ASSIGN). A fixed plug stays at its point, and a plug with a side stays on that side.
|
|
416
|
+
* Each free plug goes on the side that faces the other end (`sideToward`). Each end of a wall has an interval: the range of its port, or the room of the wall.
|
|
417
|
+
* Then `assignWall` places the plugs of each wall, with at least `plugGap` between two of them. The ends of one port share one plug.
|
|
418
|
+
*
|
|
419
|
+
* The output has one entry for each edge with two known nodes, in the order of the edges.
|
|
420
|
+
*/
|
|
421
|
+
export function assignEnds(input: PlugInput, options: AssignOptions = {}): PlugPair[] {
|
|
422
|
+
const { graph } = input;
|
|
423
|
+
const gap = options.plugGap ?? ASSIGN_DEFAULTS.plugGap;
|
|
424
|
+
const inset = options.inset ?? ASSIGN_DEFAULTS.inset;
|
|
425
|
+
const byId = nodeIndex(graph);
|
|
426
|
+
const slots = new Map<string, Slot>();
|
|
427
|
+
const walls = new Map<string, { node: Node; side: Side; slots: Slot[]; fixed: number[] }>();
|
|
428
|
+
const wallOf = (node: Node, side: Side): { node: Node; side: Side; slots: Slot[]; fixed: number[] } => {
|
|
429
|
+
const key = JSON.stringify([node.id, side]);
|
|
430
|
+
let wall = walls.get(key);
|
|
431
|
+
if (!wall) {
|
|
432
|
+
const fixed: number[] = [];
|
|
433
|
+
for (const p of node.ports ?? []) {
|
|
434
|
+
const at = portOffset(node.box, p);
|
|
435
|
+
if (at === undefined) continue;
|
|
436
|
+
const point = { x: node.box.x + at.x, y: node.box.y + at.y };
|
|
437
|
+
if ((p.side ?? nearestSide(node.box, point)) === side) fixed.push(point[alongOf(side)]);
|
|
438
|
+
}
|
|
439
|
+
walls.set(key, (wall = { node, side, slots: [], fixed }));
|
|
440
|
+
}
|
|
441
|
+
return wall;
|
|
442
|
+
};
|
|
443
|
+
const plugOf = (node: Node, end: End, far: Node): Plug | string => {
|
|
444
|
+
const toward = center(far.box);
|
|
445
|
+
const port = end.port === undefined ? undefined : node.ports?.find((p) => p.id === end.port);
|
|
446
|
+
if (port && portOffset(node.box, port) !== undefined) return placePlug(node, end, toward);
|
|
447
|
+
const key = port ? JSON.stringify([node.id, port.id]) : JSON.stringify([node.id, slots.size]);
|
|
448
|
+
if (slots.has(key)) return key;
|
|
449
|
+
const side = port?.side ?? sideToward(node.box, toward);
|
|
450
|
+
const ideal = placePlug(node, end, toward).point[alongOf(side)];
|
|
451
|
+
const range = rangeInterval(node.box, side, port, inset);
|
|
452
|
+
const slot: Slot = { node, side, ideal, at: ideal, ...(range ? { range } : {}) };
|
|
453
|
+
slots.set(key, slot);
|
|
454
|
+
wallOf(node, side).slots.push(slot);
|
|
455
|
+
return key;
|
|
456
|
+
};
|
|
457
|
+
const ends: { edge: string; source: Plug | string; target: Plug | string }[] = [];
|
|
458
|
+
for (const e of graph.edges) {
|
|
459
|
+
const s = byId.get(e.source.node);
|
|
460
|
+
const t = byId.get(e.target.node);
|
|
461
|
+
if (!s || !t) continue;
|
|
462
|
+
const source = plugOf(s, e.source, t);
|
|
463
|
+
const target = plugOf(t, e.target, s);
|
|
464
|
+
for (const [own, far] of [
|
|
465
|
+
[source, target],
|
|
466
|
+
[target, source],
|
|
467
|
+
] as const) {
|
|
468
|
+
const slot = typeof own === "string" ? slots.get(own)! : undefined;
|
|
469
|
+
if (slot && slot.far === undefined) slot.far = far;
|
|
470
|
+
}
|
|
471
|
+
ends.push({ edge: e.id, source, target });
|
|
472
|
+
}
|
|
473
|
+
// The far point of a slot: a fixed plug, or the facing plug of the far slot.
|
|
474
|
+
const farPoint = (s: Slot): Plug["point"] => {
|
|
475
|
+
const far = s.far!;
|
|
476
|
+
if (typeof far !== "string") return far.point;
|
|
477
|
+
const f = slots.get(far)!;
|
|
478
|
+
return pointAt(f.node.box, f.side, f.ideal);
|
|
479
|
+
};
|
|
480
|
+
for (const wall of walls.values()) {
|
|
481
|
+
const room = roomOf(wall.node.box, wall.side, inset);
|
|
482
|
+
const list = wall.slots.map((s) => ({ lo: s.range?.[0] ?? room[0], hi: s.range?.[1] ?? room[1], ideal: s.ideal, key: keyOf(wall.node.box, wall.side, farPoint(s)) }));
|
|
483
|
+
const plan = assignWall(list, room, gap, wall.fixed);
|
|
484
|
+
wall.slots.forEach((s, i) => {
|
|
485
|
+
s.at = plan.at[i]!;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const resolve = (p: Plug | string): Plug => {
|
|
489
|
+
if (typeof p !== "string") return p;
|
|
490
|
+
const s = slots.get(p)!;
|
|
491
|
+
return { point: pointAt(s.node.box, s.side, s.at), normal: SIDE_NORMAL[s.side] };
|
|
492
|
+
};
|
|
493
|
+
return ends.map((x) => ({ edge: x.edge, source: resolve(x.source), target: resolve(x.target) }));
|
|
494
|
+
}
|
package/src/plugs/fix.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { nodeIndex, portOffset, resolvePorts, splitFrames, type Edge, type End,
|
|
|
7
7
|
import { STRATEGIES } from "../strategies/builtins.ts";
|
|
8
8
|
import { getStrategy, type Registry, type Strategy } from "../strategies/model.ts";
|
|
9
9
|
import { WayFinder, type WayOptions } from "../ways.ts";
|
|
10
|
+
import { alongAxis, placeInIntervals, rangeInterval } from "./range.ts";
|
|
10
11
|
|
|
11
12
|
/** The options of `fixPorts`. */
|
|
12
13
|
export interface FixPortsOptions {
|
|
@@ -21,6 +22,11 @@ export interface FixPortsOptions {
|
|
|
21
22
|
* The default is `false`: such an end stays free.
|
|
22
23
|
*/
|
|
23
24
|
readonly newPorts?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The plugs of ports with a `range` (PORT-RANGE). With `keep`, a plug out of its range moves into it, and the new plugs of its side keep their order.
|
|
27
|
+
* With `rule`, each plug stays where the rule puts it. The default is `keep`, but `rule` for the rule `assign`: its plugs out of their ranges are on purpose.
|
|
28
|
+
*/
|
|
29
|
+
readonly ranges?: "keep" | "rule";
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** This function gives the side of a plug from its normal. */
|
|
@@ -29,6 +35,43 @@ function sideOf(plug: Plug, node: Node): Side {
|
|
|
29
35
|
return nearestSide(node.box, plug.point);
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* This function moves the new fixed ports of a side of a node into their ranges (PORT-RANGE). It does this when one port is out of its range.
|
|
40
|
+
* The ports of the side keep their order, and two of them keep at least `gap` (`placeInIntervals`). A side with each port in its range does not change.
|
|
41
|
+
*/
|
|
42
|
+
function keepInRanges(node: Node, ports: Map<string, Port>, gap: number, inset: number): void {
|
|
43
|
+
const bySide = new Map<Side, Port[]>();
|
|
44
|
+
for (const p of ports.values()) {
|
|
45
|
+
const list = bySide.get(p.side!);
|
|
46
|
+
if (list) list.push(p);
|
|
47
|
+
else bySide.set(p.side!, [p]);
|
|
48
|
+
}
|
|
49
|
+
const box = node.box;
|
|
50
|
+
for (const [side, list] of bySide) {
|
|
51
|
+
const along = alongAxis(side);
|
|
52
|
+
const v = (p: Port): number => p.at![along] + (along === "y" ? box.y : box.x);
|
|
53
|
+
const out = list.some((p) => {
|
|
54
|
+
const r = rangeInterval(box, side, p, inset);
|
|
55
|
+
return r !== undefined && (v(p) < r[0] - 1e-6 || v(p) > r[1] + 1e-6);
|
|
56
|
+
});
|
|
57
|
+
if (!out) continue;
|
|
58
|
+
const start = along === "y" ? box.y : box.x;
|
|
59
|
+
const len = along === "y" ? box.h : box.w;
|
|
60
|
+
const sorted = [...list].sort((a, b) => v(a) - v(b));
|
|
61
|
+
const span = sorted.map((p): [number, number] => rangeInterval(box, side, p, inset) ?? [start, start + len]);
|
|
62
|
+
const at = placeInIntervals(
|
|
63
|
+
sorted.map(v),
|
|
64
|
+
span.map((r) => r[0]),
|
|
65
|
+
span.map((r) => r[1]),
|
|
66
|
+
gap,
|
|
67
|
+
);
|
|
68
|
+
sorted.forEach((p, i) => {
|
|
69
|
+
const point = { ...p.at!, [along]: at[i]! - start };
|
|
70
|
+
ports.set(p.id, { ...p, at: point });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
32
75
|
/** This function gives an id that no port of the node has, from `base`. */
|
|
33
76
|
function freeId(taken: Set<string>, base: string): string {
|
|
34
77
|
let id = base;
|
|
@@ -44,6 +87,7 @@ function freeId(taken: Set<string>, base: string): string {
|
|
|
44
87
|
*
|
|
45
88
|
* The rule sees the graph of `corners`: the plugs of `resolvePorts`, without the inner edges of the frames.
|
|
46
89
|
* The rule is a strategy of the step `plugs`, or the name of one in the registry.
|
|
90
|
+
* A new fixed port with a `range` is in its range (PORT-RANGE). When the rule gives a plug out of the range, the new plugs of that side move into their ranges.
|
|
47
91
|
* The output is a new graph. The function does not change the input.
|
|
48
92
|
*/
|
|
49
93
|
export function fixPorts(graph: Graph, rule: Strategy<"plugs"> | string, options: FixPortsOptions = {}): Graph {
|
|
@@ -101,6 +145,17 @@ export function fixPorts(graph: Graph, rule: Strategy<"plugs"> | string, options
|
|
|
101
145
|
const target = end("target", pair.target);
|
|
102
146
|
return source === e.source && target === e.target ? e : { ...e, source, target };
|
|
103
147
|
});
|
|
148
|
+
// A rule can give a plug out of the range of its port (PORT-RANGE). Then the new plugs of that side move into their ranges.
|
|
149
|
+
const own = strategy.defaults as { readonly plugGap?: unknown; readonly inset?: unknown };
|
|
150
|
+
const given = (options.plugOptions ?? {}) as { readonly plugGap?: unknown; readonly inset?: unknown };
|
|
151
|
+
const num = (a: unknown, b: unknown, c: number): number => (typeof a === "number" ? a : typeof b === "number" ? b : c);
|
|
152
|
+
const gap = num(given.plugGap, own.plugGap, 0);
|
|
153
|
+
const inset = num(given.inset, own.inset, 6);
|
|
154
|
+
const ranges = options.ranges ?? (strategy.name === "assign" ? "rule" : "keep");
|
|
155
|
+
for (const [id, m] of fixed) {
|
|
156
|
+
const node = byId.get(id);
|
|
157
|
+
if (node && ranges === "keep") keepInRanges(node, m, gap, inset);
|
|
158
|
+
}
|
|
104
159
|
const nodes = graph.nodes.map((n): Node => {
|
|
105
160
|
const m = fixed.get(n.id);
|
|
106
161
|
const more = added.get(n.id);
|
package/src/plugs/justify.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* They move the least distance from their ideal places.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { center, type Point } from "../geom.ts";
|
|
7
|
-
import { fixedPortPoint, nodeIndex, type Edge, type Node } from "../model.ts";
|
|
6
|
+
import { center, type Box, type Point } from "../geom.ts";
|
|
7
|
+
import { fixedPortPoint, nodeIndex, type Edge, type Node, type Port, type Side } from "../model.ts";
|
|
8
8
|
import { placePlug, type Plug } from "../plugs.ts";
|
|
9
9
|
import type { PlugInput, PlugPair } from "../strategies/model.ts";
|
|
10
|
+
import { alongAxis, placeInIntervals, portOfEnd, rangeInterval } from "./range.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* This function places `ideal.length` points on the segment from `lo` to `hi` of one side.
|
|
@@ -56,10 +57,11 @@ export function isotonic(v: readonly number[]): number[] {
|
|
|
56
57
|
* This function moves the free plugs of each side of each node, so that two of them are at least `gap` apart (`justifyPlugs`).
|
|
57
58
|
* The plugs keep their order on the side. Two plugs at one place take the order of their other ends. A fixed plug does not move.
|
|
58
59
|
* A plug stays `inset` from the corners of its side, or a quarter of the side when the side is short.
|
|
60
|
+
* A plug of a port with a `range` stays in its range (PORT-RANGE), and the plugs keep their order and the gap (`placeInIntervals`).
|
|
59
61
|
*/
|
|
60
62
|
export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number, inset = 6): void {
|
|
61
|
-
// The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target)
|
|
62
|
-
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point][] }>();
|
|
63
|
+
// The free plugs of each side: the index of the edge, the end (3 for the source, 4 for the target), the point of the other end and the port.
|
|
64
|
+
const sides = new Map<string, { node: Node; normal: Point; items: [number, 3 | 4, Point, Port | undefined][] }>();
|
|
63
65
|
ends.forEach(([e, s, t, ps, pt], i) => {
|
|
64
66
|
for (const [end, node, plug, other, k] of [
|
|
65
67
|
[e.source, s, ps, pt, 3],
|
|
@@ -69,18 +71,19 @@ export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number,
|
|
|
69
71
|
const key = JSON.stringify([node.id, plug.normal.x, plug.normal.y]);
|
|
70
72
|
let side = sides.get(key);
|
|
71
73
|
if (!side) sides.set(key, (side = { node, normal: plug.normal, items: [] }));
|
|
72
|
-
side.items.push([i, k, other.point]);
|
|
74
|
+
side.items.push([i, k, other.point, portOfEnd(node, end)]);
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
77
|
for (const { node, normal, items } of sides.values()) {
|
|
76
|
-
|
|
78
|
+
const ranged = items.some((x) => x[3]?.range !== undefined);
|
|
79
|
+
if (items.length < 2 && !ranged) continue;
|
|
77
80
|
const along = normal.x !== 0 ? "y" : "x";
|
|
78
81
|
const b = node.box;
|
|
79
82
|
const start = along === "y" ? b.y : b.x;
|
|
80
83
|
const len = along === "y" ? b.h : b.w;
|
|
81
84
|
const m = Math.min(inset, len / 4);
|
|
82
85
|
const ideal = items.map(([i, k]) => ends[i]![k].point[along]);
|
|
83
|
-
const placed = justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
|
|
86
|
+
const placed = ranged ? spaceInRanges(b, normal, items, ideal, start + m, start + len - m, gap, inset) : justifyPlugs(ideal, start + m, start + len - m, gap, (j) => items[j]![2][along]);
|
|
84
87
|
items.forEach(([i, k], j) => {
|
|
85
88
|
const old = ends[i]![k];
|
|
86
89
|
ends[i]![k] = { point: along === "y" ? { x: old.point.x, y: placed[j]! } : { x: placed[j]!, y: old.point.y }, normal: old.normal };
|
|
@@ -88,6 +91,35 @@ export function spacePlugs(ends: [Edge, Node, Node, Plug, Plug][], gap: number,
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* This function places the free plugs of one side with ranges (PORT-RANGE). The order is the order of `justifyPlugs`.
|
|
96
|
+
* Each plug stays in the interval of its range, or in the room of the side without a range.
|
|
97
|
+
*/
|
|
98
|
+
function spaceInRanges(box: Box, normal: Point, items: readonly [number, 3 | 4, Point, Port | undefined][], ideal: readonly number[], lo: number, hi: number, gap: number, inset: number): number[] {
|
|
99
|
+
const side = sideOfNormal(normal);
|
|
100
|
+
const along = alongAxis(side);
|
|
101
|
+
const order = ideal.map((_, i) => i).sort((a, b) => ideal[a]! - ideal[b]! || items[a]![2][along] - items[b]![2][along] || a - b);
|
|
102
|
+
const span = order.map((j): [number, number] => rangeInterval(box, side, items[j]![3], inset) ?? [lo, hi]);
|
|
103
|
+
const at = placeInIntervals(
|
|
104
|
+
order.map((j) => ideal[j]!),
|
|
105
|
+
span.map((r) => r[0]),
|
|
106
|
+
span.map((r) => r[1]),
|
|
107
|
+
gap,
|
|
108
|
+
);
|
|
109
|
+
const out = new Array<number>(ideal.length);
|
|
110
|
+
order.forEach((j, i) => {
|
|
111
|
+
out[j] = at[i]!;
|
|
112
|
+
});
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** This function gives the side of an outward normal. */
|
|
117
|
+
function sideOfNormal(n: Point): Side {
|
|
118
|
+
if (n.x > 0) return "east";
|
|
119
|
+
if (n.x < 0) return "west";
|
|
120
|
+
return n.y > 0 ? "south" : "north";
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
/** The options of the plug strategy `justify`. */
|
|
92
124
|
export interface JustifyOptions {
|
|
93
125
|
/** The least gap between two free plugs of one side. The default is 8. 0 keeps each plug where it faces. */
|