reladraw 0.14.0 → 0.14.1
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 +8 -1
- package/dist/render.js +15 -4
- 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.1. 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.1
|
|
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,13 @@ 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.1**
|
|
1116
|
+
|
|
1117
|
+
No syntax changes, and no file that worked is refused.
|
|
1118
|
+
|
|
1119
|
+
- 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.
|
|
1120
|
+
- 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.
|
|
1121
|
+
|
|
1115
1122
|
**0.14.0**
|
|
1116
1123
|
|
|
1117
1124
|
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
|
@@ -1850,15 +1850,21 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1850
1850
|
// Which line takes the inside of a shared stretch depends on which was placed
|
|
1851
1851
|
// first, and the wrong order makes two lines cross where they need not. So a
|
|
1852
1852
|
// pair that crosses is placed again the other way round, and kept that way if
|
|
1853
|
-
// it crosses less.
|
|
1853
|
+
// it crosses less. This is tidying, not a promise: the pairs are tried a
|
|
1854
|
+
// fixed number of times over, however tangled the diagram, since trying every
|
|
1855
|
+
// pair that crossed made a diagram of forty lines take minutes. A count, not a
|
|
1856
|
+
// clock, so a file draws the same on every machine. A writer who wants a line
|
|
1857
|
+
// somewhere in particular says so.
|
|
1854
1858
|
const crossingsOf = (edge) => order.reduce((sum, other) => (other === edge ? sum : sum + linesCross(found.get(edge).points, found.get(other).points)), 0);
|
|
1855
|
-
|
|
1859
|
+
let tries = order.length * UNCROSS_TRIES;
|
|
1860
|
+
for (let pass = 0; pass < 3 && tries > 0; pass += 1) {
|
|
1856
1861
|
let better = false;
|
|
1857
|
-
for (let i = 0; i < order.length; i += 1) {
|
|
1858
|
-
for (let j = i + 1; j < order.length; j += 1) {
|
|
1862
|
+
for (let i = 0; i < order.length && tries > 0; i += 1) {
|
|
1863
|
+
for (let j = i + 1; j < order.length && tries > 0; j += 1) {
|
|
1859
1864
|
const [first, second] = [order[i], order[j]];
|
|
1860
1865
|
if (linesCross(found.get(first).points, found.get(second).points) === 0)
|
|
1861
1866
|
continue;
|
|
1867
|
+
tries -= 1;
|
|
1862
1868
|
const before = crossingsOf(first) + crossingsOf(second);
|
|
1863
1869
|
const kept = [found.get(first), found.get(second)];
|
|
1864
1870
|
const rest = order.filter((edge) => edge !== first && edge !== second);
|
|
@@ -2196,6 +2202,8 @@ function textOnWay(points, edge, nodes, measurer, fontSize) {
|
|
|
2196
2202
|
}
|
|
2197
2203
|
/** How much a turn costs the search, in the same units as length. */
|
|
2198
2204
|
const SEARCH_TURN = SEPARATION_GAP * 2;
|
|
2205
|
+
/** How many crossing pairs, per line placed, may be placed again the other way round. */
|
|
2206
|
+
const UNCROSS_TRIES = 1;
|
|
2199
2207
|
/**
|
|
2200
2208
|
* The searched way for one edge, or one half of a `between` edge. A named side
|
|
2201
2209
|
* is fixed; an end with no side named may leave or arrive by any of its box's
|
|
@@ -2302,6 +2310,9 @@ function searchWay(edge, nodes, start, startNode, end, endNode, taken, lane, use
|
|
|
2302
2310
|
taken,
|
|
2303
2311
|
lane,
|
|
2304
2312
|
...more,
|
|
2313
|
+
// Only a way cheaper than the best so far is taken, so a search that
|
|
2314
|
+
// cannot find one stops early.
|
|
2315
|
+
limit: best ? best.cost - 1e-6 : Infinity,
|
|
2305
2316
|
bounds: wallOf(grow(nodes.map((node) => extentOfBox(faceOf(node))).reduce(union), ROUTE_STUB * 4)),
|
|
2306
2317
|
});
|
|
2307
2318
|
if (found && (!best || found.cost < best.cost - 1e-6))
|
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) {
|