reladraw 0.14.0 → 0.14.2
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 +1 -1
- package/SYNTAX.md +15 -1
- package/dist/render.js +122 -5
- package/dist/search.d.ts +6 -0
- package/dist/search.js +147 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
-
Version 0.14.
|
|
85
|
+
Version 0.14.2. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG, and an element that does the same inside a web page.
|
|
86
86
|
|
|
87
87
|
The language isn't stable yet, so expect the syntax to change.
|
|
88
88
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.14.
|
|
1
|
+
# Syntax reference — 0.14.2
|
|
2
2
|
|
|
3
3
|
What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
|
|
4
4
|
|
|
@@ -1112,6 +1112,20 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1112
1112
|
|
|
1113
1113
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal below is refused by name with the replacement quoted, rather than dropped in silence — an older file stops with an error saying what to write instead.
|
|
1114
1114
|
|
|
1115
|
+
**0.14.2**
|
|
1116
|
+
|
|
1117
|
+
No syntax changes, and no file that worked is refused.
|
|
1118
|
+
|
|
1119
|
+
- Many copies of one line draw far faster. Lines with the same two ends, the same sides, the same clauses and no text are now drawn as one line and its copies, each a lane further out, instead of each finding its own way round the copies before it. Fifteen copies of `edge a -> b from: left to: right` took about a third of a second and forty took two and a half; forty now take half a second. Where such copies used to split, some going over the top and some underneath, they now stay together. A copy that would run into a box or cross a line the first one does not is still found as before.
|
|
1120
|
+
- Curved lines in busy diagrams draw faster. No picture changes.
|
|
1121
|
+
|
|
1122
|
+
**0.14.1**
|
|
1123
|
+
|
|
1124
|
+
No syntax changes, and no file that worked is refused.
|
|
1125
|
+
|
|
1126
|
+
- Diagrams with many lines draw far faster. Since 0.13.0, the time to find the lines' ways grew much faster than the number of lines: a tangled diagram of 45 boxes and 48 lines took over half an hour. It now takes a few seconds, and ten lines take under one. No example diagram draws differently.
|
|
1127
|
+
- After every line is placed, lines that cross are placed again in the other order, to cross fewer. That now stops after as many attempts as there are lines. In a very tangled diagram a line may cross another where 0.13.0 and 0.14.0 found a way round; `from:`/`to:`, `below` and `between` say where a line should go.
|
|
1128
|
+
|
|
1115
1129
|
**0.14.0**
|
|
1116
1130
|
|
|
1117
1131
|
One breaking change: on an edge written with `<-`, `from:` and `to:` now name the sides of the first and second names written. They used to follow the arrow, so `disk <- writer from: bottom` meant Writer's bottom; it now means Disk's. Swap the two on any `<-` edge that names a side. No other file draws differently.
|
package/dist/render.js
CHANGED
|
@@ -1704,6 +1704,9 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1704
1704
|
...(edge.passes ?? []).map((pass) => pass.written),
|
|
1705
1705
|
].join(' ');
|
|
1706
1706
|
};
|
|
1707
|
+
// Each line of such a group after the first, and the line it is drawn as a
|
|
1708
|
+
// copy of: the one a lane inside it.
|
|
1709
|
+
const copies = new Map();
|
|
1707
1710
|
for (let index = 0; index < wanted.length; index += 1) {
|
|
1708
1711
|
const key = sameWay(wanted[index]);
|
|
1709
1712
|
const group = wanted.filter((edge) => sameWay(edge) === key);
|
|
@@ -1723,6 +1726,14 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1723
1726
|
rest.splice(index, 0, ...ordered);
|
|
1724
1727
|
wanted.splice(0, wanted.length, ...rest);
|
|
1725
1728
|
index += group.length - 1;
|
|
1729
|
+
// A text widens the lane it rides in, which a copy cannot know; those are searched.
|
|
1730
|
+
const named = (edge) => {
|
|
1731
|
+
const { start, end } = ends.get(edge);
|
|
1732
|
+
return start.side !== undefined && end.side !== undefined;
|
|
1733
|
+
};
|
|
1734
|
+
if (ordered.every((edge) => edge.lines === undefined && named(edge))) {
|
|
1735
|
+
ordered.slice(1).forEach((edge, at) => copies.set(edge, ordered[at]));
|
|
1736
|
+
}
|
|
1726
1737
|
}
|
|
1727
1738
|
// Where several lines leave one side, their ends may be squeezed closer than
|
|
1728
1739
|
// a lane; the lines then run out of them that close together, and the lanes
|
|
@@ -1843,22 +1854,90 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1843
1854
|
found.set(edge, way);
|
|
1844
1855
|
ends.set(edge, { start: way.start, end: way.end });
|
|
1845
1856
|
};
|
|
1857
|
+
// A line identical to one already placed is that line moved out by a lane,
|
|
1858
|
+
// not a search of its own: the same way, every stretch shifted to the same
|
|
1859
|
+
// side by the distance between their ends. Searching each afresh, keeping
|
|
1860
|
+
// clear of every copy before it, made fifteen identical lines take as long as
|
|
1861
|
+
// a tangled diagram. Where the copy would not do — its ends do not line up
|
|
1862
|
+
// with the shift, a stretch folds up, it meets a box, crowds or crosses a line
|
|
1863
|
+
// the first does not — the line is searched for as any other.
|
|
1864
|
+
const copyOf = (edge, inside, placed) => {
|
|
1865
|
+
const { start, end } = planned.get(edge);
|
|
1866
|
+
const base = found.get(inside).points;
|
|
1867
|
+
const count = base.length - 1;
|
|
1868
|
+
if (count < 1)
|
|
1869
|
+
return undefined;
|
|
1870
|
+
const heading = base.slice(1).map((q, index) => {
|
|
1871
|
+
const p = base[index];
|
|
1872
|
+
const length = Math.hypot(q.x - p.x, q.y - p.y);
|
|
1873
|
+
return { x: (q.x - p.x) / length, y: (q.y - p.y) / length, length };
|
|
1874
|
+
});
|
|
1875
|
+
if (heading.some((one) => one.length < 0.5 || (Math.abs(one.x) > 1e-6 && Math.abs(one.y) > 1e-6)))
|
|
1876
|
+
return undefined;
|
|
1877
|
+
const normal = heading.map((one) => ({ x: -one.y, y: one.x }));
|
|
1878
|
+
const along = (from, to, axis) => (to.x - from.x) * axis.x + (to.y - from.y) * axis.y;
|
|
1879
|
+
const shift = along(base[0], start, normal[0]);
|
|
1880
|
+
if (Math.abs(shift) < 0.5 || Math.abs(along(base[0], start, heading[0])) > 0.5)
|
|
1881
|
+
return undefined;
|
|
1882
|
+
if (Math.abs(along(base[count], end, normal[count - 1]) - shift) > 0.5 || Math.abs(along(base[count], end, heading[count - 1])) > 0.5) {
|
|
1883
|
+
return undefined;
|
|
1884
|
+
}
|
|
1885
|
+
const points = [{ x: start.x, y: start.y }];
|
|
1886
|
+
for (let index = 1; index < count; index += 1) {
|
|
1887
|
+
const [p, q] = [normal[index - 1], normal[index]];
|
|
1888
|
+
const turns = Math.abs(p.x * q.x + p.y * q.y) < 0.5;
|
|
1889
|
+
const v = base[index];
|
|
1890
|
+
points.push(turns ? { x: v.x + shift * (p.x + q.x), y: v.y + shift * (p.y + q.y) } : { x: v.x + shift * q.x, y: v.y + shift * q.y });
|
|
1891
|
+
}
|
|
1892
|
+
points.push({ x: end.x, y: end.y });
|
|
1893
|
+
for (let index = 0; index < count; index += 1) {
|
|
1894
|
+
const [p, q] = [points[index], points[index + 1]];
|
|
1895
|
+
if (along(p, q, heading[index]) < 0.5)
|
|
1896
|
+
return undefined;
|
|
1897
|
+
}
|
|
1898
|
+
const inner = points.slice(1, -1);
|
|
1899
|
+
const walls = nodes
|
|
1900
|
+
.filter((node) => !contains(node, edge.from) && !contains(node, edge.to))
|
|
1901
|
+
.map((node) => grow(extentOfBox(faceOf(node)), ATTACH_MARGIN - 0.5));
|
|
1902
|
+
const own = [edge.from, edge.to].map((node) => grow(extentOfBox(faceOf(node)), ATTACH_MARGIN - 0.5));
|
|
1903
|
+
const fences = (edge.passes ?? []).map((pass) => fenceOf(pass, 0));
|
|
1904
|
+
if (!wayClear(points, [...obstaclesFor(edge, nodes), ...walls]) || !wayClear(inner, [...own, ...fences]) || !passesAll(edge, points)) {
|
|
1905
|
+
return undefined;
|
|
1906
|
+
}
|
|
1907
|
+
const lane = laneFor(edge);
|
|
1908
|
+
for (const other of placed) {
|
|
1909
|
+
const theirs = found.get(other).points;
|
|
1910
|
+
if (linesCross(points, theirs) > linesCross(base, theirs))
|
|
1911
|
+
return undefined;
|
|
1912
|
+
if (other !== inside && crowds(points, theirs, lane))
|
|
1913
|
+
return undefined;
|
|
1914
|
+
}
|
|
1915
|
+
return { start, end, points, mid: textOnWay(points, edge, nodes, measurer, fontSize) };
|
|
1916
|
+
};
|
|
1846
1917
|
for (const edge of wanted) {
|
|
1847
|
-
|
|
1918
|
+
const inside = copies.get(edge);
|
|
1919
|
+
const copy = inside && found.has(inside) ? copyOf(edge, inside, [...order]) : undefined;
|
|
1920
|
+
settle(edge, copy ?? place(edge, [...order]));
|
|
1848
1921
|
order.push(edge);
|
|
1849
1922
|
}
|
|
1850
1923
|
// Which line takes the inside of a shared stretch depends on which was placed
|
|
1851
1924
|
// first, and the wrong order makes two lines cross where they need not. So a
|
|
1852
1925
|
// pair that crosses is placed again the other way round, and kept that way if
|
|
1853
|
-
// it crosses less.
|
|
1926
|
+
// it crosses less. This is tidying, not a promise: the pairs are tried a
|
|
1927
|
+
// fixed number of times over, however tangled the diagram, since trying every
|
|
1928
|
+
// pair that crossed made a diagram of forty lines take minutes. A count, not a
|
|
1929
|
+
// clock, so a file draws the same on every machine. A writer who wants a line
|
|
1930
|
+
// somewhere in particular says so.
|
|
1854
1931
|
const crossingsOf = (edge) => order.reduce((sum, other) => (other === edge ? sum : sum + linesCross(found.get(edge).points, found.get(other).points)), 0);
|
|
1855
|
-
|
|
1932
|
+
let tries = order.length * UNCROSS_TRIES;
|
|
1933
|
+
for (let pass = 0; pass < 3 && tries > 0; pass += 1) {
|
|
1856
1934
|
let better = false;
|
|
1857
|
-
for (let i = 0; i < order.length; i += 1) {
|
|
1858
|
-
for (let j = i + 1; j < order.length; j += 1) {
|
|
1935
|
+
for (let i = 0; i < order.length && tries > 0; i += 1) {
|
|
1936
|
+
for (let j = i + 1; j < order.length && tries > 0; j += 1) {
|
|
1859
1937
|
const [first, second] = [order[i], order[j]];
|
|
1860
1938
|
if (linesCross(found.get(first).points, found.get(second).points) === 0)
|
|
1861
1939
|
continue;
|
|
1940
|
+
tries -= 1;
|
|
1862
1941
|
const before = crossingsOf(first) + crossingsOf(second);
|
|
1863
1942
|
const kept = [found.get(first), found.get(second)];
|
|
1864
1943
|
const rest = order.filter((edge) => edge !== first && edge !== second);
|
|
@@ -1894,6 +1973,27 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1894
1973
|
}
|
|
1895
1974
|
return routes;
|
|
1896
1975
|
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Whether two lines run side by side, along the same stretch, nearer than a
|
|
1978
|
+
* lane apart — which a line found by search never does.
|
|
1979
|
+
*/
|
|
1980
|
+
function crowds(a, b, lane) {
|
|
1981
|
+
for (let i = 0; i + 1 < a.length; i += 1) {
|
|
1982
|
+
const [p, q] = [a[i], a[i + 1]];
|
|
1983
|
+
const level = Math.abs(p.y - q.y) < 0.5;
|
|
1984
|
+
for (let j = 0; j + 1 < b.length; j += 1) {
|
|
1985
|
+
const [r, s] = [b[j], b[j + 1]];
|
|
1986
|
+
if (level !== Math.abs(r.y - s.y) < 0.5)
|
|
1987
|
+
continue;
|
|
1988
|
+
const [across, lo, hi, otherAcross, otherLo, otherHi] = level
|
|
1989
|
+
? [p.y, Math.min(p.x, q.x), Math.max(p.x, q.x), r.y, Math.min(r.x, s.x), Math.max(r.x, s.x)]
|
|
1990
|
+
: [p.x, Math.min(p.y, q.y), Math.max(p.y, q.y), r.x, Math.min(r.y, s.y), Math.max(r.y, s.y)];
|
|
1991
|
+
if (Math.min(hi, otherHi) - Math.max(lo, otherLo) > 0.5 && Math.abs(across - otherAcross) < lane - 0.5)
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1897
1997
|
/** How many times two lines cross, each drawn as straight pieces through its points. */
|
|
1898
1998
|
function linesCross(a, b) {
|
|
1899
1999
|
let count = 0;
|
|
@@ -2196,6 +2296,8 @@ function textOnWay(points, edge, nodes, measurer, fontSize) {
|
|
|
2196
2296
|
}
|
|
2197
2297
|
/** How much a turn costs the search, in the same units as length. */
|
|
2198
2298
|
const SEARCH_TURN = SEPARATION_GAP * 2;
|
|
2299
|
+
/** How many crossing pairs, per line placed, may be placed again the other way round. */
|
|
2300
|
+
const UNCROSS_TRIES = 1;
|
|
2199
2301
|
/**
|
|
2200
2302
|
* The searched way for one edge, or one half of a `between` edge. A named side
|
|
2201
2303
|
* is fixed; an end with no side named may leave or arrive by any of its box's
|
|
@@ -2302,6 +2404,9 @@ function searchWay(edge, nodes, start, startNode, end, endNode, taken, lane, use
|
|
|
2302
2404
|
taken,
|
|
2303
2405
|
lane,
|
|
2304
2406
|
...more,
|
|
2407
|
+
// Only a way cheaper than the best so far is taken, so a search that
|
|
2408
|
+
// cannot find one stops early.
|
|
2409
|
+
limit: best ? best.cost - 1e-6 : Infinity,
|
|
2305
2410
|
bounds: wallOf(grow(nodes.map((node) => extentOfBox(faceOf(node))).reduce(union), ROUTE_STUB * 4)),
|
|
2306
2411
|
});
|
|
2307
2412
|
if (found && (!best || found.cost < best.cost - 1e-6))
|
|
@@ -2551,9 +2656,21 @@ function sweptPath(points, boxes, neighbors, arrow, marks) {
|
|
|
2551
2656
|
return false;
|
|
2552
2657
|
}
|
|
2553
2658
|
}
|
|
2659
|
+
// A piece of another line wholly beyond the curve's reach, and its air,
|
|
2660
|
+
// can neither cross it nor come near it.
|
|
2661
|
+
const reach = {
|
|
2662
|
+
minX: Math.min(...trace.map((point) => point.x)) - SWEEP_AIR,
|
|
2663
|
+
maxX: Math.max(...trace.map((point) => point.x)) + SWEEP_AIR,
|
|
2664
|
+
minY: Math.min(...trace.map((point) => point.y)) - SWEEP_AIR,
|
|
2665
|
+
maxY: Math.max(...trace.map((point) => point.y)) + SWEEP_AIR,
|
|
2666
|
+
};
|
|
2554
2667
|
for (const line of neighbors) {
|
|
2555
2668
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
2556
2669
|
const [p, q] = [line[index], line[index + 1]];
|
|
2670
|
+
if (Math.max(p.x, q.x) < reach.minX || Math.min(p.x, q.x) > reach.maxX)
|
|
2671
|
+
continue;
|
|
2672
|
+
if (Math.max(p.y, q.y) < reach.minY || Math.min(p.y, q.y) > reach.maxY)
|
|
2673
|
+
continue;
|
|
2557
2674
|
for (let step = 0; step < steps; step += 1) {
|
|
2558
2675
|
if (intersect(trace[step], trace[step + 1], p, q))
|
|
2559
2676
|
return false;
|
package/dist/search.d.ts
CHANGED
|
@@ -79,6 +79,12 @@ export interface SearchOptions {
|
|
|
79
79
|
* cannot be gone round at its far end.
|
|
80
80
|
*/
|
|
81
81
|
bounds?: SearchWall;
|
|
82
|
+
/**
|
|
83
|
+
* A cost the line is no use at unless it comes in under: the cheapest way
|
|
84
|
+
* already found between another pair of sides. The search gives up as soon
|
|
85
|
+
* as it cannot.
|
|
86
|
+
*/
|
|
87
|
+
limit?: number;
|
|
82
88
|
}
|
|
83
89
|
/** A gap a line must pass through. See `SearchOptions.gate`. */
|
|
84
90
|
export interface SearchGate {
|
package/dist/search.js
CHANGED
|
@@ -45,6 +45,10 @@ export function searchRoute(start, end, walls, options) {
|
|
|
45
45
|
return undefined;
|
|
46
46
|
if (inside(out, walls) || inside(back, walls))
|
|
47
47
|
return undefined;
|
|
48
|
+
// No way between the two is shorter than straight across and straight down.
|
|
49
|
+
const limit = options.limit ?? Infinity;
|
|
50
|
+
if (Math.abs(back.x - out.x) + Math.abs(back.y - out.y) - 1e-6 >= limit)
|
|
51
|
+
return undefined;
|
|
48
52
|
const bounds = options.bounds;
|
|
49
53
|
const gate = options.gate;
|
|
50
54
|
// How far a stretch keeps from a placed one beside it: a lane, or clear of
|
|
@@ -68,7 +72,8 @@ export function searchRoute(start, end, walls, options) {
|
|
|
68
72
|
const iy = new Map(ys.map((y, index) => [y, index]));
|
|
69
73
|
const width = xs.length;
|
|
70
74
|
const height = ys.length;
|
|
71
|
-
const
|
|
75
|
+
const steps = stepTable(xs, ys, walls, clear, options.taken, spacing, ownHalf, (a, b) => (inGate(a, b) ? ownHalf : 0));
|
|
76
|
+
const open = (i, j) => steps.open[j * width + i] === 1;
|
|
72
77
|
// Four headings: 0 right, 1 down, 2 left, 3 up.
|
|
73
78
|
const STEP = [
|
|
74
79
|
[1, 0],
|
|
@@ -95,26 +100,40 @@ export function searchRoute(start, end, walls, options) {
|
|
|
95
100
|
const from = new Int32Array(size).fill(-1);
|
|
96
101
|
const state = (i, j, h, p) => ((j * width + i) * 4 + h) * 2 + p;
|
|
97
102
|
const done = gate ? 1 : 0;
|
|
103
|
+
// The search reaches out toward the end first: a state waits by what it has
|
|
104
|
+
// cost so far plus the least the rest could cost, straight across and
|
|
105
|
+
// straight down to the end. It does not settle each state once, since a
|
|
106
|
+
// way just as long can arrive later with a better tie-break; it keeps on
|
|
107
|
+
// until nothing waiting could come in as cheap as the end already has, and
|
|
108
|
+
// what it finds is exactly what searching every direction alike would.
|
|
109
|
+
const toGo = (current) => {
|
|
110
|
+
const cell = (current - (current % 8)) / 8;
|
|
111
|
+
const i = cell % width;
|
|
112
|
+
return Math.abs(xs[i] - back.x) + Math.abs(ys[(cell - i) / width] - back.y);
|
|
113
|
+
};
|
|
98
114
|
const heap = new MinHeap();
|
|
99
115
|
const begin = state(si, sj, first, 0);
|
|
116
|
+
const goal = state(ti, tj, last, done);
|
|
100
117
|
cost[begin] = 0;
|
|
101
118
|
tie[begin] = 0;
|
|
102
|
-
heap.push(begin,
|
|
103
|
-
let goal = -1;
|
|
119
|
+
heap.push(begin, toGo(begin), 0);
|
|
104
120
|
while (heap.size > 0) {
|
|
105
|
-
const [current,
|
|
106
|
-
|
|
121
|
+
const [current, f, t] = heap.pop();
|
|
122
|
+
const c = cost[current];
|
|
123
|
+
const due = c + toGo(current);
|
|
124
|
+
if (f > due || (f === due && t > tie[current]))
|
|
107
125
|
continue;
|
|
126
|
+
// Everything still waiting costs at least this much.
|
|
127
|
+
if (f >= limit || f > cost[goal] + 1e-9)
|
|
128
|
+
break;
|
|
108
129
|
const p = current % 2;
|
|
109
130
|
const rest = (current - p) / 2;
|
|
110
131
|
const h = rest % 4;
|
|
111
132
|
const cell = (rest - h) / 4;
|
|
112
133
|
const i = cell % width;
|
|
113
134
|
const j = (cell - i) / width;
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
135
|
+
if (current === goal)
|
|
136
|
+
continue;
|
|
118
137
|
for (let next = 0; next < 4; next += 1) {
|
|
119
138
|
// A line never doubles straight back on itself.
|
|
120
139
|
if ((next + 2) % 4 === h)
|
|
@@ -128,11 +147,13 @@ export function searchRoute(start, end, walls, options) {
|
|
|
128
147
|
const nj = j + STEP[next][1];
|
|
129
148
|
if (ni < 0 || nj < 0 || ni >= width || nj >= height || !open(ni, nj))
|
|
130
149
|
continue;
|
|
150
|
+
// A step is filed under the point it starts from on its way right or down.
|
|
151
|
+
const axis = next % 2;
|
|
152
|
+
const key = next < 2 ? j * width + i : nj * width + ni;
|
|
153
|
+
if (steps.closed[axis][key] === 1)
|
|
154
|
+
continue;
|
|
131
155
|
const a = { x: xs[i], y: ys[j] };
|
|
132
156
|
const b = { x: xs[ni], y: ys[nj] };
|
|
133
|
-
const own = inGate(a, b) ? ownHalf : 0;
|
|
134
|
-
if (blocked(a, b, walls) || alongTaken(a, b, options.taken, spacing, own))
|
|
135
|
-
continue;
|
|
136
157
|
// Passing the gate: a stretch through it, reaching the line at `at`.
|
|
137
158
|
const through = inGate(a, b) &&
|
|
138
159
|
Math.min(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) <= gate.at + 0.01 &&
|
|
@@ -142,8 +163,8 @@ export function searchRoute(start, end, walls, options) {
|
|
|
142
163
|
// a narrow gap, but where there is room it keeps its distance. Crossing a
|
|
143
164
|
// line already placed costs what a turn does, so of two ways much the
|
|
144
165
|
// same length the one that crosses nothing wins.
|
|
145
|
-
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) +
|
|
146
|
-
turn *
|
|
166
|
+
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) + steps.crowd[axis][key] +
|
|
167
|
+
turn * steps.cross[axis][key];
|
|
147
168
|
// Higher up for a stretch across, further right for one down: a smaller
|
|
148
169
|
// tie-break is preferred, so the across level counts as it is and the
|
|
149
170
|
// down level counts negated.
|
|
@@ -151,14 +172,14 @@ export function searchRoute(start, end, walls, options) {
|
|
|
151
172
|
relax(current, state(ni, nj, next, np), c + length, t + lean);
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
|
-
if (goal <
|
|
175
|
+
if (!(cost[goal] < limit))
|
|
155
176
|
return undefined;
|
|
156
177
|
function relax(previous, next, c, t) {
|
|
157
178
|
if (c < cost[next] - 1e-9 || (Math.abs(c - cost[next]) <= 1e-9 && t < tie[next])) {
|
|
158
179
|
cost[next] = c;
|
|
159
180
|
tie[next] = t;
|
|
160
181
|
from[next] = previous;
|
|
161
|
-
heap.push(next, c, t);
|
|
182
|
+
heap.push(next, c + toGo(next), t);
|
|
162
183
|
}
|
|
163
184
|
}
|
|
164
185
|
const path = [];
|
|
@@ -255,83 +276,139 @@ function inside(point, walls) {
|
|
|
255
276
|
}
|
|
256
277
|
/** Whether a level or upright stretch passes through the inside of any wall. */
|
|
257
278
|
function blocked(a, b, walls) {
|
|
279
|
+
return walls.some((wall) => blocks(a, b, wall));
|
|
280
|
+
}
|
|
281
|
+
/** Whether a level or upright stretch passes through the inside of one wall. */
|
|
282
|
+
function blocks(a, b, wall) {
|
|
258
283
|
const minX = Math.min(a.x, b.x);
|
|
259
284
|
const maxX = Math.max(a.x, b.x);
|
|
260
285
|
const minY = Math.min(a.y, b.y);
|
|
261
286
|
const maxY = Math.max(a.y, b.y);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return minX > wall.minX && minX < wall.maxX && maxY > wall.minY && minY < wall.maxY;
|
|
266
|
-
});
|
|
287
|
+
if (minY === maxY)
|
|
288
|
+
return minY > wall.minY && minY < wall.maxY && maxX > wall.minX && minX < wall.maxX;
|
|
289
|
+
return minX > wall.minX && minX < wall.maxX && maxY > wall.minY && minY < wall.maxY;
|
|
267
290
|
}
|
|
268
291
|
/**
|
|
269
|
-
*
|
|
270
|
-
* the
|
|
292
|
+
* Every single step through the grid — from a point to its neighbor on the
|
|
293
|
+
* right, or the one below — and what it costs beyond its length: whether it is
|
|
294
|
+
* closed, by a wall or by running along a placed line; how far it runs close
|
|
295
|
+
* beside a wall; how many placed lines it crosses. The search takes each step
|
|
296
|
+
* many times over, once for each heading it arrives with, so these are worked
|
|
297
|
+
* out here once, and each wall and each placed stretch is tested only against
|
|
298
|
+
* the steps that lie near it. A step is filed at the index of the point it
|
|
299
|
+
* starts from: `[0]` the step right, `[1]` the step down.
|
|
271
300
|
*/
|
|
272
|
-
function
|
|
301
|
+
function stepTable(xs, ys, walls, clear, taken, spacing, widest, ownAt) {
|
|
302
|
+
const width = xs.length;
|
|
303
|
+
const height = ys.length;
|
|
304
|
+
const cells = width * height;
|
|
305
|
+
const open = new Uint8Array(cells).fill(1);
|
|
306
|
+
const closed = [new Uint8Array(cells), new Uint8Array(cells)];
|
|
307
|
+
const crowd = [new Float64Array(cells), new Float64Array(cells)];
|
|
308
|
+
const cross = [new Int32Array(cells), new Int32Array(cells)];
|
|
309
|
+
// The indices of the tracks lying within `lo`..`hi`, with one to spare either
|
|
310
|
+
// side: a range to test exactly, never a test itself.
|
|
311
|
+
const near = (values, lo, hi) => [Math.max(0, firstAtLeast(values, lo) - 1), Math.min(values.length - 1, firstAtLeast(values, hi) + 1)];
|
|
312
|
+
const each = ([i0, i1], [j0, j1], visit) => {
|
|
313
|
+
for (let j = j0; j <= j1; j += 1) {
|
|
314
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
315
|
+
const a = { x: xs[i], y: ys[j] };
|
|
316
|
+
if (i + 1 < width)
|
|
317
|
+
visit(0, a, { x: xs[i + 1], y: ys[j] }, j * width + i);
|
|
318
|
+
if (j + 1 < height)
|
|
319
|
+
visit(1, a, { x: xs[i], y: ys[j + 1] }, j * width + i);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
for (const wall of walls) {
|
|
324
|
+
const [i0, i1] = near(xs, wall.minX, wall.maxX);
|
|
325
|
+
const [j0, j1] = near(ys, wall.minY, wall.maxY);
|
|
326
|
+
for (let j = j0; j <= j1; j += 1) {
|
|
327
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
328
|
+
const x = xs[i];
|
|
329
|
+
const y = ys[j];
|
|
330
|
+
if (x > wall.minX && x < wall.maxX && y > wall.minY && y < wall.maxY)
|
|
331
|
+
open[j * width + i] = 0;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
each([i0, i1], [j0, j1], (axis, a, b, key) => {
|
|
335
|
+
if (blocks(a, b, wall))
|
|
336
|
+
closed[axis][key] = 1;
|
|
337
|
+
});
|
|
338
|
+
each(near(xs, wall.minX - clear, wall.maxX + clear), near(ys, wall.minY - clear, wall.maxY + clear), (axis, a, b, key) => {
|
|
339
|
+
crowd[axis][key] = Math.max(crowd[axis][key], crowding(a, b, wall, clear));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
taken.forEach((line, which) => {
|
|
343
|
+
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
344
|
+
const [p, q] = [line[index], line[index + 1]];
|
|
345
|
+
const reach = spacing(which, index, widest);
|
|
346
|
+
const xr = near(xs, Math.min(p.x, q.x) - reach, Math.max(p.x, q.x) + reach);
|
|
347
|
+
const yr = near(ys, Math.min(p.y, q.y) - reach, Math.max(p.y, q.y) + reach);
|
|
348
|
+
each(xr, yr, (axis, a, b, key) => {
|
|
349
|
+
if (runsAlong(a, b, p, q, spacing(which, index, ownAt(a, b))))
|
|
350
|
+
closed[axis][key] = 1;
|
|
351
|
+
if (crosses(a, b, p, q))
|
|
352
|
+
cross[axis][key] = cross[axis][key] + 1;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return { open, closed, crowd, cross };
|
|
357
|
+
}
|
|
358
|
+
/** The index of the first of the sorted values at least `value`, or their count if none is. */
|
|
359
|
+
function firstAtLeast(values, value) {
|
|
360
|
+
let lo = 0;
|
|
361
|
+
let hi = values.length;
|
|
362
|
+
while (lo < hi) {
|
|
363
|
+
const mid = (lo + hi) >> 1;
|
|
364
|
+
if (values[mid] < value)
|
|
365
|
+
lo = mid + 1;
|
|
366
|
+
else
|
|
367
|
+
hi = mid;
|
|
368
|
+
}
|
|
369
|
+
return lo;
|
|
370
|
+
}
|
|
371
|
+
/** How much of a stretch runs within `clear` of one wall's side, alongside it; 0 if none does. */
|
|
372
|
+
function crowding(a, b, wall, clear) {
|
|
273
373
|
const across = Math.abs(a.y - b.y) < 0.5;
|
|
274
374
|
const level = across ? a.y : a.x;
|
|
275
375
|
const from = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
276
376
|
const to = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const near = (level > lo - clear + 0.01 && level <= lo) || (level >= hi && level < hi + clear - 0.01);
|
|
282
|
-
if (!near)
|
|
283
|
-
continue;
|
|
284
|
-
worst = Math.max(worst, Math.min(to, ahi) - Math.max(from, alo));
|
|
285
|
-
}
|
|
286
|
-
return worst;
|
|
377
|
+
const [lo, hi] = across ? [wall.minY, wall.maxY] : [wall.minX, wall.maxX];
|
|
378
|
+
const [alo, ahi] = across ? [wall.minX, wall.maxX] : [wall.minY, wall.maxY];
|
|
379
|
+
const near = (level > lo - clear + 0.01 && level <= lo) || (level >= hi && level < hi + clear - 0.01);
|
|
380
|
+
return near ? Math.max(0, Math.min(to, ahi) - Math.max(from, alo)) : 0;
|
|
287
381
|
}
|
|
288
382
|
/**
|
|
289
|
-
* Whether a stretch runs along a placed line, for any distance,
|
|
290
|
-
*
|
|
383
|
+
* Whether a stretch runs along one stretch of a placed line, for any distance,
|
|
384
|
+
* closer than `spacing`: a lane, or clear of a text either carries.
|
|
291
385
|
*/
|
|
292
|
-
function
|
|
386
|
+
function runsAlong(a, b, p, q, spacing) {
|
|
293
387
|
const across = Math.abs(a.y - b.y) < 0.5;
|
|
388
|
+
if ((Math.abs(p.y - q.y) < 0.5) !== across)
|
|
389
|
+
return false;
|
|
294
390
|
const level = across ? a.y : a.x;
|
|
391
|
+
const other = across ? p.y : p.x;
|
|
392
|
+
if (Math.abs(other - level) >= spacing - 0.5)
|
|
393
|
+
return false;
|
|
295
394
|
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
296
395
|
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const pAcross = Math.abs(p.y - q.y) < 0.5;
|
|
301
|
-
if (pAcross !== across)
|
|
302
|
-
continue;
|
|
303
|
-
const other = across ? p.y : p.x;
|
|
304
|
-
if (Math.abs(other - level) >= spacing(which, index, own) - 0.5)
|
|
305
|
-
continue;
|
|
306
|
-
const pLo = across ? Math.min(p.x, q.x) : Math.min(p.y, q.y);
|
|
307
|
-
const pHi = across ? Math.max(p.x, q.x) : Math.max(p.y, q.y);
|
|
308
|
-
if (Math.min(hi, pHi) - Math.max(lo, pLo) > 0.5)
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return false;
|
|
396
|
+
const pLo = across ? Math.min(p.x, q.x) : Math.min(p.y, q.y);
|
|
397
|
+
const pHi = across ? Math.max(p.x, q.x) : Math.max(p.y, q.y);
|
|
398
|
+
return Math.min(hi, pHi) - Math.max(lo, pLo) > 0.5;
|
|
313
399
|
}
|
|
314
|
-
/**
|
|
315
|
-
function
|
|
400
|
+
/** Whether a stretch crosses one stretch of a placed line square on, touching neither end of either. */
|
|
401
|
+
function crosses(a, b, p, q) {
|
|
316
402
|
const across = Math.abs(a.y - b.y) < 0.5;
|
|
403
|
+
if ((Math.abs(p.y - q.y) < 0.5) === across)
|
|
404
|
+
return false;
|
|
317
405
|
const level = across ? a.y : a.x;
|
|
318
406
|
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
319
407
|
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const pAcross = Math.abs(p.y - q.y) < 0.5;
|
|
325
|
-
if (pAcross === across)
|
|
326
|
-
continue;
|
|
327
|
-
const at = across ? p.x : p.y;
|
|
328
|
-
const pLo = across ? Math.min(p.y, q.y) : Math.min(p.x, q.x);
|
|
329
|
-
const pHi = across ? Math.max(p.y, q.y) : Math.max(p.x, q.x);
|
|
330
|
-
if (at > lo + 0.5 && at < hi - 0.5 && level > pLo + 0.5 && level < pHi - 0.5)
|
|
331
|
-
found += 1;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return found;
|
|
408
|
+
const at = across ? p.x : p.y;
|
|
409
|
+
const pLo = across ? Math.min(p.y, q.y) : Math.min(p.x, q.x);
|
|
410
|
+
const pHi = across ? Math.max(p.y, q.y) : Math.max(p.x, q.x);
|
|
411
|
+
return at > lo + 0.5 && at < hi - 0.5 && level > pLo + 0.5 && level < pHi - 0.5;
|
|
335
412
|
}
|
|
336
413
|
/** The points with every one dropped that sits on a straight run between its neighbors. */
|
|
337
414
|
function straightRuns(points) {
|