reladraw 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SYNTAX.md +80 -21
- package/dist/ast.d.ts +24 -2
- package/dist/ast.js +17 -1
- package/dist/grammar.d.ts +2 -10
- package/dist/grammar.js +8 -3
- package/dist/model.d.ts +6 -2
- package/dist/parser.js +100 -17
- package/dist/render.js +742 -657
- package/dist/resolve.js +41 -7
- package/dist/search.d.ts +34 -5
- package/dist/search.js +90 -30
- package/package.json +1 -1
package/dist/resolve.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALL_ATTR_KEYS, ATTR_KEYS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
|
|
1
|
+
import { ALL_ATTR_KEYS, ATTR_KEYS, MARKS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
|
|
2
2
|
import { BARE_EDGE_RUN, CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
|
|
3
3
|
import { fix, reachability, tightest } from './constrain.js';
|
|
4
4
|
import { SourceError } from './errors.js';
|
|
@@ -430,6 +430,34 @@ function appearanceOf(attrs, styles, line) {
|
|
|
430
430
|
}
|
|
431
431
|
return { ...merged, ...attrs };
|
|
432
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* What is drawn at one end of an edge. A mark written in the arrow is final:
|
|
435
|
+
* the edge's own `from-mark:` or `to-mark:` may repeat it but not contradict
|
|
436
|
+
* it, since both halves are on one line and whichever lost would be dead text.
|
|
437
|
+
* An end the arrow leaves blank is unsaid, and the edge's attribute, then its
|
|
438
|
+
* style, then nothing, fills it. Layering is between places, never within one.
|
|
439
|
+
*/
|
|
440
|
+
function markAt(end, stmt, appearance, what) {
|
|
441
|
+
const key = `${end}-mark`;
|
|
442
|
+
const check = (value) => {
|
|
443
|
+
if (!MARKS.includes(value)) {
|
|
444
|
+
throw new SourceError(`edge ${what}: "${key}: ${value}" is not a mark — use ${MARKS.join(', ')}`, stmt.line);
|
|
445
|
+
}
|
|
446
|
+
return value;
|
|
447
|
+
};
|
|
448
|
+
const written = stmt.marks[end];
|
|
449
|
+
const own = stmt.attrs[key];
|
|
450
|
+
if (written !== undefined) {
|
|
451
|
+
if (own !== undefined && check(own) !== written) {
|
|
452
|
+
throw new SourceError(`edge ${what}: the arrow already puts ${written === 'none' ? 'nothing' : `a${/^[aeiou]/.test(written) ? 'n' : ''} ${written}`} ` +
|
|
453
|
+
`at ${stmt[end]}, and "${key}: ${own}" says otherwise. Say it once: leave that end of the ` +
|
|
454
|
+
`arrow blank and keep "${key}: ${own}", or drop "${key}:"`, stmt.line);
|
|
455
|
+
}
|
|
456
|
+
return written;
|
|
457
|
+
}
|
|
458
|
+
const given = appearance[key];
|
|
459
|
+
return given === undefined ? 'none' : check(given);
|
|
460
|
+
}
|
|
433
461
|
function buildEdges(statements, byName, styles, defaults) {
|
|
434
462
|
const edges = [];
|
|
435
463
|
for (const stmt of statements) {
|
|
@@ -477,7 +505,7 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
477
505
|
};
|
|
478
506
|
});
|
|
479
507
|
const appearance = { ...defaults.get('edge'), ...appearanceOf(stmt.attrs, styles, stmt.line) };
|
|
480
|
-
const what = `${stmt.from}
|
|
508
|
+
const what = `${stmt.from} ${stmt.arrow} ${stmt.to}`;
|
|
481
509
|
checkAttrs('edge', what, stmt.attrs, stmt.line);
|
|
482
510
|
checkStyleUse('edge', what, stmt.attrs, styles, stmt.line);
|
|
483
511
|
if (stmt.attrs['url'] !== undefined && stmt.text === undefined) {
|
|
@@ -492,7 +520,10 @@ function buildEdges(statements, byName, styles, defaults) {
|
|
|
492
520
|
edges.push({
|
|
493
521
|
from,
|
|
494
522
|
to,
|
|
495
|
-
|
|
523
|
+
marks: {
|
|
524
|
+
from: markAt('from', stmt, appearance, what),
|
|
525
|
+
to: markAt('to', stmt, appearance, what),
|
|
526
|
+
},
|
|
496
527
|
textAttrs,
|
|
497
528
|
...(stmt.text !== undefined
|
|
498
529
|
? { text: stmt.text, lines: linesFor(stmt.text, textAttrs, `edge ${what}`, stmt.line) }
|
|
@@ -1468,10 +1499,11 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1468
1499
|
const to = locate(edge.to);
|
|
1469
1500
|
if (!from || !to || from.index === to.index)
|
|
1470
1501
|
continue;
|
|
1471
|
-
// With no text, the gap still has to show a run of line behind each
|
|
1502
|
+
// With no text, the gap still has to show a run of line behind each mark,
|
|
1472
1503
|
// or two stacked children with an arrow between them draw as a head alone.
|
|
1504
|
+
const marked = [edge.marks.from, edge.marks.to].filter((mark) => mark !== 'none').length;
|
|
1473
1505
|
if (edge.text === undefined) {
|
|
1474
|
-
const bare = BARE_EDGE_RUN + arrowLength(edge.look.thickness) *
|
|
1506
|
+
const bare = BARE_EDGE_RUN + arrowLength(edge.look.thickness) * marked;
|
|
1475
1507
|
corridors.push({
|
|
1476
1508
|
edge,
|
|
1477
1509
|
from,
|
|
@@ -1486,9 +1518,11 @@ function corridorsIn(edges, locate, measurer, fontSize) {
|
|
|
1486
1518
|
// at either end. The arrowhead is charged on both sides for the same reason:
|
|
1487
1519
|
// it covers the head's length of the line it arrives on, and reserving that
|
|
1488
1520
|
// at one end only would move the midpoint rather than lengthen the run. A
|
|
1489
|
-
// thick line's head is longer, and takes the room it needs.
|
|
1521
|
+
// thick line's head is longer, and takes the room it needs. A line with no
|
|
1522
|
+
// mark at either end is charged for none.
|
|
1523
|
+
const head = marked > 0 ? arrowLength(edge.look.thickness) : 0;
|
|
1490
1524
|
const extent = (axis) => textExtent(edge.lines, edge.textAttrs, axis, measurer, fontSize, edge.line) +
|
|
1491
|
-
(TEXT_CLEARANCE +
|
|
1525
|
+
(TEXT_CLEARANCE + head) * 2;
|
|
1492
1526
|
corridors.push({
|
|
1493
1527
|
edge,
|
|
1494
1528
|
from,
|
package/dist/search.d.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
12
|
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
13
|
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
14
|
-
* for every turn
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* than the left.
|
|
14
|
+
* for every turn and for every line already placed that it crosses. On a
|
|
15
|
+
* right-angled grid many ways tie on length — a staircase is exactly as long as
|
|
16
|
+
* one bend — and the turn charge breaks those ties toward the fewest bends,
|
|
17
|
+
* which is the line a person would draw. What still ties after that goes over
|
|
18
|
+
* the top rather than under, and round the right rather than the left.
|
|
19
19
|
*
|
|
20
20
|
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
21
|
*/
|
|
@@ -57,6 +57,35 @@ export interface SearchOptions {
|
|
|
57
57
|
*/
|
|
58
58
|
taken: SearchPoint[][];
|
|
59
59
|
lane: number;
|
|
60
|
+
/**
|
|
61
|
+
* For each line in `taken`, for each of its stretches, half the depth across
|
|
62
|
+
* it of a text riding there, and 0 where none does. A line running beside a
|
|
63
|
+
* stretch carrying a text keeps clear of the text, not just of the line.
|
|
64
|
+
*/
|
|
65
|
+
takenHalves?: number[][];
|
|
66
|
+
/**
|
|
67
|
+
* A gap the line must pass through: somewhere it crosses the line at `at`
|
|
68
|
+
* on the `run` axis, travelling along `run`, at a level between `lo` and `hi`.
|
|
69
|
+
*/
|
|
70
|
+
gate?: SearchGate;
|
|
71
|
+
/**
|
|
72
|
+
* Half the depth of this line's own text, which rides in the gate: inside it,
|
|
73
|
+
* the line keeps that much further from the lines beside it.
|
|
74
|
+
*/
|
|
75
|
+
ownHalf?: number;
|
|
76
|
+
/**
|
|
77
|
+
* How far the line may go at all: the diagram with room round it. A wall
|
|
78
|
+
* reaching past this — a clause's fence, which runs to the edge of the page —
|
|
79
|
+
* cannot be gone round at its far end.
|
|
80
|
+
*/
|
|
81
|
+
bounds?: SearchWall;
|
|
82
|
+
}
|
|
83
|
+
/** A gap a line must pass through. See `SearchOptions.gate`. */
|
|
84
|
+
export interface SearchGate {
|
|
85
|
+
run: 'x' | 'y';
|
|
86
|
+
at: number;
|
|
87
|
+
lo: number;
|
|
88
|
+
hi: number;
|
|
60
89
|
}
|
|
61
90
|
export interface SearchResult {
|
|
62
91
|
points: SearchPoint[];
|
package/dist/search.js
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
12
|
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
13
|
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
14
|
-
* for every turn
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* than the left.
|
|
14
|
+
* for every turn and for every line already placed that it crosses. On a
|
|
15
|
+
* right-angled grid many ways tie on length — a staircase is exactly as long as
|
|
16
|
+
* one bend — and the turn charge breaks those ties toward the fewest bends,
|
|
17
|
+
* which is the line a person would draw. What still ties after that goes over
|
|
18
|
+
* the top rather than under, and round the right rather than the left.
|
|
19
19
|
*
|
|
20
20
|
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
21
|
*/
|
|
@@ -45,8 +45,25 @@ 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
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const bounds = options.bounds;
|
|
49
|
+
const gate = options.gate;
|
|
50
|
+
// How far a stretch keeps from a placed one beside it: a lane, or clear of
|
|
51
|
+
// the text either of them carries there.
|
|
52
|
+
const halves = options.takenHalves ?? [];
|
|
53
|
+
const ownHalf = options.ownHalf ?? 0;
|
|
54
|
+
const inGate = (a, b) => {
|
|
55
|
+
if (!gate)
|
|
56
|
+
return false;
|
|
57
|
+
const along = gate.run === 'x' ? Math.abs(a.y - b.y) < 0.5 : Math.abs(a.x - b.x) < 0.5;
|
|
58
|
+
const level = gate.run === 'x' ? a.y : a.x;
|
|
59
|
+
return along && level > gate.lo && level < gate.hi;
|
|
60
|
+
};
|
|
61
|
+
const spacing = (line, stretch, own) => Math.max(options.lane, (halves[line]?.[stretch] ?? 0) + own);
|
|
62
|
+
const gateTracks = (axis) => !gate ? [] : axis === gate.run ? [gate.at] : [(gate.lo + gate.hi) / 2];
|
|
63
|
+
const xs = tracks(walls, 'x', clear, [out.x, back.x, ...gateTracks('x')], options.taken, spacing, ownHalf)
|
|
64
|
+
.filter((x) => !bounds || (x >= bounds.minX && x <= bounds.maxX) || x === out.x || x === back.x);
|
|
65
|
+
const ys = tracks(walls, 'y', clear, [out.y, back.y, ...gateTracks('y')], options.taken, spacing, ownHalf)
|
|
66
|
+
.filter((y) => !bounds || (y >= bounds.minY && y <= bounds.maxY) || y === out.y || y === back.y);
|
|
50
67
|
const ix = new Map(xs.map((x, index) => [x, index]));
|
|
51
68
|
const iy = new Map(ys.map((y, index) => [y, index]));
|
|
52
69
|
const width = xs.length;
|
|
@@ -69,14 +86,17 @@ export function searchRoute(start, end, walls, options) {
|
|
|
69
86
|
const tj = iy.get(back.y);
|
|
70
87
|
// Cost is compared first on length and turns, then on a tie-break that
|
|
71
88
|
// prefers a stretch higher up the page, or further right, so that of two
|
|
72
|
-
// ways round that are exactly as long the line goes over the top.
|
|
73
|
-
|
|
89
|
+
// ways round that are exactly as long the line goes over the top. A state
|
|
90
|
+
// also records whether the line has passed its gate yet; it may only finish
|
|
91
|
+
// once it has.
|
|
92
|
+
const size = width * height * 8;
|
|
74
93
|
const cost = new Float64Array(size).fill(Infinity);
|
|
75
94
|
const tie = new Float64Array(size).fill(Infinity);
|
|
76
95
|
const from = new Int32Array(size).fill(-1);
|
|
77
|
-
const state = (i, j, h) => (j * width + i) * 4 + h;
|
|
96
|
+
const state = (i, j, h, p) => ((j * width + i) * 4 + h) * 2 + p;
|
|
97
|
+
const done = gate ? 1 : 0;
|
|
78
98
|
const heap = new MinHeap();
|
|
79
|
-
const begin = state(si, sj, first);
|
|
99
|
+
const begin = state(si, sj, first, 0);
|
|
80
100
|
cost[begin] = 0;
|
|
81
101
|
tie[begin] = 0;
|
|
82
102
|
heap.push(begin, 0, 0);
|
|
@@ -85,11 +105,13 @@ export function searchRoute(start, end, walls, options) {
|
|
|
85
105
|
const [current, c, t] = heap.pop();
|
|
86
106
|
if (c > cost[current] || (c === cost[current] && t > tie[current]))
|
|
87
107
|
continue;
|
|
88
|
-
const
|
|
89
|
-
const
|
|
108
|
+
const p = current % 2;
|
|
109
|
+
const rest = (current - p) / 2;
|
|
110
|
+
const h = rest % 4;
|
|
111
|
+
const cell = (rest - h) / 4;
|
|
90
112
|
const i = cell % width;
|
|
91
113
|
const j = (cell - i) / width;
|
|
92
|
-
if (i === ti && j === tj && h === last) {
|
|
114
|
+
if (i === ti && j === tj && h === last && p === done) {
|
|
93
115
|
goal = current;
|
|
94
116
|
break;
|
|
95
117
|
}
|
|
@@ -99,7 +121,7 @@ export function searchRoute(start, end, walls, options) {
|
|
|
99
121
|
continue;
|
|
100
122
|
if (next !== h) {
|
|
101
123
|
// Turning on the spot costs a turn and moves nowhere.
|
|
102
|
-
relax(current, state(i, j, next), c + turn, t);
|
|
124
|
+
relax(current, state(i, j, next, p), c + turn, t);
|
|
103
125
|
continue;
|
|
104
126
|
}
|
|
105
127
|
const ni = i + STEP[next][0];
|
|
@@ -108,16 +130,25 @@ export function searchRoute(start, end, walls, options) {
|
|
|
108
130
|
continue;
|
|
109
131
|
const a = { x: xs[i], y: ys[j] };
|
|
110
132
|
const b = { x: xs[ni], y: ys[nj] };
|
|
111
|
-
|
|
133
|
+
const own = inGate(a, b) ? ownHalf : 0;
|
|
134
|
+
if (blocked(a, b, walls) || alongTaken(a, b, options.taken, spacing, own))
|
|
112
135
|
continue;
|
|
136
|
+
// Passing the gate: a stretch through it, reaching the line at `at`.
|
|
137
|
+
const through = inGate(a, b) &&
|
|
138
|
+
Math.min(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) <= gate.at + 0.01 &&
|
|
139
|
+
Math.max(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) >= gate.at - 0.01;
|
|
140
|
+
const np = p === 1 || through ? 1 : 0;
|
|
113
141
|
// Running closer to a wall than `clear` counts double: the line may pass
|
|
114
|
-
// a narrow gap, but where there is room it keeps its distance.
|
|
115
|
-
|
|
142
|
+
// a narrow gap, but where there is room it keeps its distance. Crossing a
|
|
143
|
+
// line already placed costs what a turn does, so of two ways much the
|
|
144
|
+
// same length the one that crosses nothing wins.
|
|
145
|
+
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) + crowding(a, b, walls, clear) +
|
|
146
|
+
turn * crossings(a, b, options.taken);
|
|
116
147
|
// Higher up for a stretch across, further right for one down: a smaller
|
|
117
148
|
// tie-break is preferred, so the across level counts as it is and the
|
|
118
149
|
// down level counts negated.
|
|
119
150
|
const lean = next % 2 === 0 ? a.y * length : -a.x * length;
|
|
120
|
-
relax(current, state(ni, nj, next), c + length, t + lean);
|
|
151
|
+
relax(current, state(ni, nj, next, np), c + length, t + lean);
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
if (goal < 0)
|
|
@@ -132,7 +163,8 @@ export function searchRoute(start, end, walls, options) {
|
|
|
132
163
|
}
|
|
133
164
|
const path = [];
|
|
134
165
|
for (let at = goal; at >= 0; at = from[at]) {
|
|
135
|
-
const
|
|
166
|
+
const rest = (at - (at % 2)) / 2;
|
|
167
|
+
const cell = (rest - (rest % 4)) / 4;
|
|
136
168
|
const i = cell % width;
|
|
137
169
|
const j = (cell - i) / width;
|
|
138
170
|
const point = { x: xs[i], y: ys[j] };
|
|
@@ -146,9 +178,10 @@ export function searchRoute(start, end, walls, options) {
|
|
|
146
178
|
/**
|
|
147
179
|
* The tracks on one axis: beside each wall at `clear` where that is open,
|
|
148
180
|
* down the middle of every gap between walls, through the ends, and a lane
|
|
149
|
-
* either side of each line already placed
|
|
181
|
+
* either side of each line already placed — a wider one where a text rides,
|
|
182
|
+
* and wider again by `own` for this line's own text.
|
|
150
183
|
*/
|
|
151
|
-
function tracks(walls, axis, clear, ends, taken,
|
|
184
|
+
function tracks(walls, axis, clear, ends, taken, spacing, own) {
|
|
152
185
|
const lo = (wall) => (axis === 'x' ? wall.minX : wall.minY);
|
|
153
186
|
const hi = (wall) => (axis === 'x' ? wall.maxX : wall.maxY);
|
|
154
187
|
// The other axis, to tell a real gap — two walls facing each other across
|
|
@@ -166,7 +199,7 @@ function tracks(walls, axis, clear, ends, taken, lane) {
|
|
|
166
199
|
found.add((hi(a) + lo(b)) / 2);
|
|
167
200
|
}
|
|
168
201
|
}
|
|
169
|
-
|
|
202
|
+
taken.forEach((line, which) => {
|
|
170
203
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
171
204
|
const [a, b] = [line[index], line[index + 1]];
|
|
172
205
|
// A stretch across sits at a level on y; one down, at a level on x.
|
|
@@ -176,10 +209,12 @@ function tracks(walls, axis, clear, ends, taken, lane) {
|
|
|
176
209
|
// A lane either side of a placed line. One that runs close beside a box
|
|
177
210
|
// is paid for as crowding, like any other track.
|
|
178
211
|
const level = axis === 'y' ? a.y : a.x;
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
for (const by of new Set([spacing(which, index, 0), spacing(which, index, own)])) {
|
|
213
|
+
found.add(level - by);
|
|
214
|
+
found.add(level + by);
|
|
215
|
+
}
|
|
181
216
|
}
|
|
182
|
-
}
|
|
217
|
+
});
|
|
183
218
|
return [...new Set([...found].map(snap))].sort((p, q) => p - q);
|
|
184
219
|
}
|
|
185
220
|
/** Coordinates to a hundredth, so a track and the end it runs through compare equal. */
|
|
@@ -250,20 +285,23 @@ function crowding(a, b, walls, clear) {
|
|
|
250
285
|
}
|
|
251
286
|
return worst;
|
|
252
287
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Whether a stretch runs along a placed line, for any distance, closer than
|
|
290
|
+
* the spacing between the two there: a lane, or clear of a text either carries.
|
|
291
|
+
*/
|
|
292
|
+
function alongTaken(a, b, taken, spacing, own) {
|
|
255
293
|
const across = Math.abs(a.y - b.y) < 0.5;
|
|
256
294
|
const level = across ? a.y : a.x;
|
|
257
295
|
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
258
296
|
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
259
|
-
for (const line of taken) {
|
|
297
|
+
for (const [which, line] of taken.entries()) {
|
|
260
298
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
261
299
|
const [p, q] = [line[index], line[index + 1]];
|
|
262
300
|
const pAcross = Math.abs(p.y - q.y) < 0.5;
|
|
263
301
|
if (pAcross !== across)
|
|
264
302
|
continue;
|
|
265
303
|
const other = across ? p.y : p.x;
|
|
266
|
-
if (Math.abs(other - level) >=
|
|
304
|
+
if (Math.abs(other - level) >= spacing(which, index, own) - 0.5)
|
|
267
305
|
continue;
|
|
268
306
|
const pLo = across ? Math.min(p.x, q.x) : Math.min(p.y, q.y);
|
|
269
307
|
const pHi = across ? Math.max(p.x, q.x) : Math.max(p.y, q.y);
|
|
@@ -273,6 +311,28 @@ function alongTaken(a, b, taken, lane) {
|
|
|
273
311
|
}
|
|
274
312
|
return false;
|
|
275
313
|
}
|
|
314
|
+
/** How many stretches of placed lines a stretch crosses square on, touching neither end of either. */
|
|
315
|
+
function crossings(a, b, taken) {
|
|
316
|
+
const across = Math.abs(a.y - b.y) < 0.5;
|
|
317
|
+
const level = across ? a.y : a.x;
|
|
318
|
+
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
319
|
+
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
320
|
+
let found = 0;
|
|
321
|
+
for (const line of taken) {
|
|
322
|
+
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
323
|
+
const [p, q] = [line[index], line[index + 1]];
|
|
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;
|
|
335
|
+
}
|
|
276
336
|
/** The points with every one dropped that sits on a straight run between its neighbors. */
|
|
277
337
|
function straightRuns(points) {
|
|
278
338
|
const kept = [];
|