reladraw 0.22.0 → 0.22.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/.claude/skills/reladraw/SKILL.md +23 -2
- package/.claude/skills/reladraw/reference/syntax.md +8 -2
- package/README.md +1 -1
- package/SYNTAX.md +8 -2
- package/dist/cli.js +8 -2
- package/dist/format.js +67 -20
- package/package.json +1 -1
- package/dist/search.d.ts +0 -104
- package/dist/search.js +0 -490
|
@@ -15,7 +15,7 @@ That matters here specifically. You can't see the SVG you produced. With auto-la
|
|
|
15
15
|
|
|
16
16
|
## Check it is installed, and the same version as this skill
|
|
17
17
|
|
|
18
|
-
This skill teaches reladraw 0.22.
|
|
18
|
+
This skill teaches reladraw 0.22.1. Once per session, before the first render, run:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
reladraw --version
|
|
@@ -136,7 +136,7 @@ edge [<name>] <from> -> <to> ["<text>" [(<placement>)]] [attributes]
|
|
|
136
136
|
|
|
137
137
|
```
|
|
138
138
|
node bob "Bob the builder" (inside top) height: 7
|
|
139
|
-
node
|
|
139
|
+
node bob_role "builder" (size: small) inside bob bottom-center shape: none
|
|
140
140
|
node count "3" on bob top-right shape: circle
|
|
141
141
|
```
|
|
142
142
|
|
|
@@ -153,6 +153,27 @@ node records "Records" style: store
|
|
|
153
153
|
|
|
154
154
|
`fill` is a box's area, `border` its outline, `line` an edge's line, and a text's color goes in its bracket. `corners: sharp` squares a box's rounded corners, and `border: (color: theme-primary, thickness: thick)` thickens its outline, the box growing to keep its padding. Prefer theme colors, which read in light and dark themes alike: `theme-primary` and `theme-secondary` are the accents, `-subtle` is an accent softened toward the page (the one to fill a box with), `theme-muted` is quieter text. A hex or CSS color stays fixed in every theme.
|
|
155
155
|
|
|
156
|
+
### Links
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
node docs "Documentation" url: "https://example.com/docs"
|
|
160
|
+
edge api -> db "SQL" url: "https://example.com/schema"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**A link goes on the thing it is about, never in a text of its own.** Give the node a `url:`, or give it to a line's text when the link is about the connection. The whole box, or the line's text, becomes clickable. When asked to add links to a diagram, put `url:` on the nodes and lines already there. Don't add nodes to hold links: no column of link texts beside the diagram, and no caption with an address under each box. Add one only when the user asks to see the addresses written out.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
// Wrong: a new text beside the thing the link belongs to
|
|
167
|
+
node api_link "github.com/acme/api" shape: none right of api
|
|
168
|
+
|
|
169
|
+
// Right: the link is on the thing itself
|
|
170
|
+
node api "API server" url: "https://github.com/acme/api"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The address is never drawn; a reader follows it by clicking. A line with no text can't carry one, and neither can a style. Quote the value, since an unquoted `//` starts a comment.
|
|
174
|
+
|
|
175
|
+
**Offer links; don't add them unasked.** When the things in a diagram have real web addresses, such as the files of a pull request on GitHub, end your reply with one line offering to link each node to its file. Never guess an address, and don't link to files on the local machine.
|
|
176
|
+
|
|
156
177
|
## A complete small file
|
|
157
178
|
|
|
158
179
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.22.
|
|
1
|
+
# Syntax reference — 0.22.1
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -1240,7 +1240,7 @@ clause-gap: 2
|
|
|
1240
1240
|
|
|
1241
1241
|
An unknown setting, one written twice, or a value it doesn't take is an error naming it.
|
|
1242
1242
|
|
|
1243
|
-
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured
|
|
1243
|
+
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured as it would be printed, lined up with its neighbors, so a short statement pushed past the width by a long neighbor's columns breaks too; its first line still lines up with theirs. A statement that can't break, such as a node's name and text with nothing after them, is left as it is, even when its columns carry it past the width. A break falls only between clauses, never inside one or inside a bracket, so a clause longer than the width stands alone on its line, past it. A statement broken by hand is broken again by these rules, or joined when they leave it short enough; one kept on its lines for a comment among them is left as it is. A comment at the end of a broken statement follows its last line.
|
|
1244
1244
|
|
|
1245
1245
|
```
|
|
1246
1246
|
// wrap-at: 60
|
|
@@ -1317,6 +1317,12 @@ Designed and absent from the code.
|
|
|
1317
1317
|
|
|
1318
1318
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal 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.
|
|
1319
1319
|
|
|
1320
|
+
**0.22.1**
|
|
1321
|
+
|
|
1322
|
+
- **`reladraw format` keeps to `wrap-at` once the columns are lined up.** A statement's length used to be measured on its own, before it was lined up with its neighbors, so a short one like `node api "API" layout: column` stayed on one line and was then padded out by a long neighbor's name to nearly twice the width. It is now measured as printed and breaks like any other long statement; `clause-per-line-at` is measured the same way.
|
|
1323
|
+
- **Output sent to another program is no longer cut off.** `reladraw format -` and `reladraw render -o -` stopped after the first 64 KB when their output went into a pipe rather than a file, and still reported success, so an editor formatting a large diagram could replace it with a shortened copy. All of it now comes through.
|
|
1324
|
+
- **The agent skill puts a link on the thing it belongs to.** Asked to add links, an agent would add a new text node for each one; the skill now says to give the existing node or line a `url:`. When a diagram's things have real web addresses, such as a pull request's files, it offers to link them rather than doing it unasked.
|
|
1325
|
+
|
|
1320
1326
|
**0.22.0**
|
|
1321
1327
|
|
|
1322
1328
|
- **`reladraw install-skill`** installs the agent skill that came with your reladraw, so it teaches what your copy accepts rather than what the main branch does. It hands the work to `skills`, passing on `-g`, `-a` and the rest. The npm package now carries the skill.
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ To install the skill from the main branch instead, which can run ahead of the re
|
|
|
90
90
|
|
|
91
91
|
## Status
|
|
92
92
|
|
|
93
|
-
Version 0.22.
|
|
93
|
+
Version 0.22.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.
|
|
94
94
|
|
|
95
95
|
## License
|
|
96
96
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.22.
|
|
1
|
+
# Syntax reference — 0.22.1
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -1240,7 +1240,7 @@ clause-gap: 2
|
|
|
1240
1240
|
|
|
1241
1241
|
An unknown setting, one written twice, or a value it doesn't take is an error naming it.
|
|
1242
1242
|
|
|
1243
|
-
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured
|
|
1243
|
+
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured as it would be printed, lined up with its neighbors, so a short statement pushed past the width by a long neighbor's columns breaks too; its first line still lines up with theirs. A statement that can't break, such as a node's name and text with nothing after them, is left as it is, even when its columns carry it past the width. A break falls only between clauses, never inside one or inside a bracket, so a clause longer than the width stands alone on its line, past it. A statement broken by hand is broken again by these rules, or joined when they leave it short enough; one kept on its lines for a comment among them is left as it is. A comment at the end of a broken statement follows its last line.
|
|
1244
1244
|
|
|
1245
1245
|
```
|
|
1246
1246
|
// wrap-at: 60
|
|
@@ -1317,6 +1317,12 @@ Designed and absent from the code.
|
|
|
1317
1317
|
|
|
1318
1318
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal 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.
|
|
1319
1319
|
|
|
1320
|
+
**0.22.1**
|
|
1321
|
+
|
|
1322
|
+
- **`reladraw format` keeps to `wrap-at` once the columns are lined up.** A statement's length used to be measured on its own, before it was lined up with its neighbors, so a short one like `node api "API" layout: column` stayed on one line and was then padded out by a long neighbor's name to nearly twice the width. It is now measured as printed and breaks like any other long statement; `clause-per-line-at` is measured the same way.
|
|
1323
|
+
- **Output sent to another program is no longer cut off.** `reladraw format -` and `reladraw render -o -` stopped after the first 64 KB when their output went into a pipe rather than a file, and still reported success, so an editor formatting a large diagram could replace it with a shortened copy. All of it now comes through.
|
|
1324
|
+
- **The agent skill puts a link on the thing it belongs to.** Asked to add links, an agent would add a new text node for each one; the skill now says to give the existing node or line a `url:`. When a diagram's things have real web addresses, such as a pull request's files, it offers to link them rather than doing it unasked.
|
|
1325
|
+
|
|
1320
1326
|
**0.22.0**
|
|
1321
1327
|
|
|
1322
1328
|
- **`reladraw install-skill`** installs the agent skill that came with your reladraw, so it teaches what your copy accepts rather than what the main branch does. It hands the work to `skills`, passing on `-g`, `-a` and the rest. The npm package now carries the skill.
|
package/dist/cli.js
CHANGED
|
@@ -417,7 +417,13 @@ function nearestSettings(folder, found) {
|
|
|
417
417
|
found.set(folder, result);
|
|
418
418
|
return result;
|
|
419
419
|
}
|
|
420
|
-
|
|
420
|
+
// The exit code is set, not exited with: process.exit ends the process at
|
|
421
|
+
// once, and a write to a pipe that hasn't drained yet is lost, which cut
|
|
422
|
+
// `format -` and `render -o -` off at 64 KB. Node exits on its own once the
|
|
423
|
+
// output is written. `./dev.sh pipe` checks it.
|
|
424
|
+
main(process.argv.slice(2)).then((code) => {
|
|
425
|
+
process.exitCode = code;
|
|
426
|
+
}, (error) => {
|
|
421
427
|
process.stderr.write(`reladraw: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
422
|
-
process.
|
|
428
|
+
process.exitCode = 1;
|
|
423
429
|
});
|
package/dist/format.js
CHANGED
|
@@ -280,16 +280,15 @@ function textWidth(text) {
|
|
|
280
280
|
/** How wide a tab counts as, where a width has to be known. */
|
|
281
281
|
const TAB_WIDTH = 4;
|
|
282
282
|
/**
|
|
283
|
-
* Whether a statement goes past `wrap-at` or
|
|
284
|
-
* where its first line ends.
|
|
285
|
-
*
|
|
286
|
-
* holds the keyword and name, and the texts
|
|
287
|
-
* so, and shares columns with its neighbors
|
|
288
|
-
* clauses after it are laid out by
|
|
283
|
+
* Whether a statement `length` characters long goes past `wrap-at` or
|
|
284
|
+
* `clause-per-line-at`, and if so where its first line ends. The length is
|
|
285
|
+
* the line as it would be printed, lined up with its neighbors (see
|
|
286
|
+
* `layOutItems`). The first line holds the keyword and name, and the texts
|
|
287
|
+
* after them if `first-line` says so, and shares columns with its neighbors
|
|
288
|
+
* like a one-line statement; the clauses after it are laid out by
|
|
289
|
+
* `continuedLines`.
|
|
289
290
|
*/
|
|
290
|
-
function breakStatement(item, settings,
|
|
291
|
-
const texts = item.cells.map((cell) => cell.text);
|
|
292
|
-
const length = textWidth(texts.filter((text, at) => text !== '' || at === 0).join(gap));
|
|
291
|
+
function breakStatement(item, settings, length) {
|
|
293
292
|
const perLine = settings['clause-per-line-at'];
|
|
294
293
|
const wrap = settings['wrap-at'];
|
|
295
294
|
const oneEach = perLine === 'always' || (typeof perLine === 'number' && length > perLine);
|
|
@@ -338,7 +337,8 @@ function continuedLines(item, indent, settings, gap, gapWidth) {
|
|
|
338
337
|
function layOutItems(written, settings) {
|
|
339
338
|
const gap = settings['clause-gap'] === 'tab' ? '\t' : ' '.repeat(settings['clause-gap']);
|
|
340
339
|
const gapWidth = settings['clause-gap'] === 'tab' ? 1 : settings['clause-gap'];
|
|
341
|
-
|
|
340
|
+
// Which statements break is decided further down, once the columns are known.
|
|
341
|
+
const items = [...written];
|
|
342
342
|
// What lines up in columns: a one-line statement whole, a broken one's first line.
|
|
343
343
|
const aligned = (item) => item.kind === 'statement' ? item.cells : item.kind === 'broken' ? item.first : undefined;
|
|
344
344
|
// Which statements share columns: each is given a group number. A comment
|
|
@@ -372,17 +372,20 @@ function layOutItems(written, settings) {
|
|
|
372
372
|
previous = item;
|
|
373
373
|
});
|
|
374
374
|
// Each group's column widths, and the column its trailing comments start at.
|
|
375
|
-
|
|
375
|
+
let widths = new Map();
|
|
376
376
|
const commentAt = new Map();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
377
|
+
const measureColumns = () => {
|
|
378
|
+
widths = new Map();
|
|
379
|
+
items.forEach((item, index) => {
|
|
380
|
+
const cells = aligned(item);
|
|
381
|
+
if (cells === undefined)
|
|
382
|
+
return;
|
|
383
|
+
const id = group.get(index);
|
|
384
|
+
const own = widths.get(id) ?? [];
|
|
385
|
+
cells.forEach((cell, at) => (own[at] = Math.max(own[at] ?? 0, textWidth(cell.text))));
|
|
386
|
+
widths.set(id, own);
|
|
387
|
+
});
|
|
388
|
+
};
|
|
386
389
|
// Each cell's text, and where on the line it starts.
|
|
387
390
|
const placed = (cells, id) => {
|
|
388
391
|
const own = widths.get(id);
|
|
@@ -407,6 +410,50 @@ function layOutItems(written, settings) {
|
|
|
407
410
|
line += ' '.repeat(one.at - textWidth(line)) + one.text;
|
|
408
411
|
return line;
|
|
409
412
|
};
|
|
413
|
+
// Which statements break. A statement is measured as it would be printed,
|
|
414
|
+
// lined up with its neighbors, so a short statement padded out past
|
|
415
|
+
// `wrap-at` by a long neighbor's columns breaks too. Breaking one takes its
|
|
416
|
+
// later clauses out of the columns, which can only narrow them and shorten
|
|
417
|
+
// the rest, so they are broken one at a time, the longest on its own first:
|
|
418
|
+
// that is the one most likely to be widening a column the others are past
|
|
419
|
+
// only because of. A statement whose break is settled by its unaligned
|
|
420
|
+
// length alone, since lining it up can only lengthen it, breaks before the
|
|
421
|
+
// first measure: one past `clause-per-line-at`, or past `wrap-at` with no
|
|
422
|
+
// `clause-per-line-at` above it to reach.
|
|
423
|
+
const ownLength = (item) => textWidth(item.cells.map((cell) => cell.text).filter((text, at) => text !== '' || at === 0).join(gap));
|
|
424
|
+
const breakAt = (index, length) => {
|
|
425
|
+
const item = items[index];
|
|
426
|
+
if (item.kind !== 'statement')
|
|
427
|
+
return false;
|
|
428
|
+
const broken = breakStatement(item, settings, length);
|
|
429
|
+
items[index] = broken;
|
|
430
|
+
return broken !== item;
|
|
431
|
+
};
|
|
432
|
+
const perLine = settings['clause-per-line-at'];
|
|
433
|
+
const settled = (length) => typeof perLine !== 'number' || length > perLine;
|
|
434
|
+
items.forEach((item, index) => {
|
|
435
|
+
if (item.kind !== 'statement')
|
|
436
|
+
return;
|
|
437
|
+
const own = ownLength(item);
|
|
438
|
+
if (settled(own))
|
|
439
|
+
breakAt(index, own);
|
|
440
|
+
});
|
|
441
|
+
for (;;) {
|
|
442
|
+
measureColumns();
|
|
443
|
+
let longest;
|
|
444
|
+
items.forEach((item, index) => {
|
|
445
|
+
if (item.kind !== 'statement')
|
|
446
|
+
return;
|
|
447
|
+
const printed = textWidth(lineOf(item.cells, group.get(index)));
|
|
448
|
+
if (breakStatement(item, settings, printed) === item)
|
|
449
|
+
return;
|
|
450
|
+
const own = ownLength(item);
|
|
451
|
+
if (longest === undefined || own > longest.own)
|
|
452
|
+
longest = { index, own, printed };
|
|
453
|
+
});
|
|
454
|
+
if (longest === undefined || !breakAt(longest.index, longest.printed))
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
410
457
|
items.forEach((item, index) => {
|
|
411
458
|
if (item.kind !== 'statement')
|
|
412
459
|
return;
|
package/package.json
CHANGED
package/dist/search.d.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finding a line's way through the open space between boxes.
|
|
3
|
-
*
|
|
4
|
-
* Every box is a wall. A line may turn only where two tracks cross, and the
|
|
5
|
-
* tracks are few: a line a little way out from each side of each wall, one
|
|
6
|
-
* down the middle of each gap between walls, and the lines through the two
|
|
7
|
-
* ends. That is the whole of the open space a right-angled line ever needs,
|
|
8
|
-
* reduced to a grid of a few hundred points for a diagram of ordinary size.
|
|
9
|
-
*
|
|
10
|
-
* The line leaves its start heading straight out and arrives at its end heading
|
|
11
|
-
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
|
-
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
|
-
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
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
|
-
*
|
|
20
|
-
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
|
-
*/
|
|
22
|
-
export interface SearchPoint {
|
|
23
|
-
x: number;
|
|
24
|
-
y: number;
|
|
25
|
-
}
|
|
26
|
-
/** A box the line may not enter, as its extent. Touching its border is allowed. */
|
|
27
|
-
export interface SearchWall {
|
|
28
|
-
minX: number;
|
|
29
|
-
minY: number;
|
|
30
|
-
maxX: number;
|
|
31
|
-
maxY: number;
|
|
32
|
-
}
|
|
33
|
-
/** One end of the line: a point on a box's side, and the unit direction out of it. */
|
|
34
|
-
export interface SearchEnd {
|
|
35
|
-
x: number;
|
|
36
|
-
y: number;
|
|
37
|
-
dx: number;
|
|
38
|
-
dy: number;
|
|
39
|
-
}
|
|
40
|
-
export interface SearchOptions {
|
|
41
|
-
/** How far the line runs straight out of each end before it may turn. */
|
|
42
|
-
stub: number;
|
|
43
|
-
/** How far outside a wall a track runs beside it, where there is room. */
|
|
44
|
-
clear: number;
|
|
45
|
-
/** What a turn costs, in the same units as length. */
|
|
46
|
-
turn: number;
|
|
47
|
-
/**
|
|
48
|
-
* Walls the stretches at the two ends may pass through: the line's own boxes,
|
|
49
|
-
* which it has to cross the margin of to leave, and any box holding an end.
|
|
50
|
-
*/
|
|
51
|
-
ownStart: SearchWall[];
|
|
52
|
-
ownEnd: SearchWall[];
|
|
53
|
-
/**
|
|
54
|
-
* Lines already placed. A line may cross one but not run along it within
|
|
55
|
-
* `lane` of it; tracks are added a lane either side of each, so a later line
|
|
56
|
-
* sharing a stretch takes the next lane out.
|
|
57
|
-
*/
|
|
58
|
-
taken: SearchPoint[][];
|
|
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 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;
|
|
88
|
-
}
|
|
89
|
-
/** A gap a line must pass through. See `SearchOptions.gate`. */
|
|
90
|
-
export interface SearchGate {
|
|
91
|
-
run: 'x' | 'y';
|
|
92
|
-
at: number;
|
|
93
|
-
lo: number;
|
|
94
|
-
hi: number;
|
|
95
|
-
}
|
|
96
|
-
export interface SearchResult {
|
|
97
|
-
points: SearchPoint[];
|
|
98
|
-
cost: number;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* The cheapest right-angled line from `start` to `end` that enters no wall, or
|
|
102
|
-
* undefined when there is none.
|
|
103
|
-
*/
|
|
104
|
-
export declare function searchRoute(start: SearchEnd, end: SearchEnd, walls: SearchWall[], options: SearchOptions): SearchResult | undefined;
|
package/dist/search.js
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finding a line's way through the open space between boxes.
|
|
3
|
-
*
|
|
4
|
-
* Every box is a wall. A line may turn only where two tracks cross, and the
|
|
5
|
-
* tracks are few: a line a little way out from each side of each wall, one
|
|
6
|
-
* down the middle of each gap between walls, and the lines through the two
|
|
7
|
-
* ends. That is the whole of the open space a right-angled line ever needs,
|
|
8
|
-
* reduced to a grid of a few hundred points for a diagram of ordinary size.
|
|
9
|
-
*
|
|
10
|
-
* The line leaves its start heading straight out and arrives at its end heading
|
|
11
|
-
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
|
-
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
|
-
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
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
|
-
*
|
|
20
|
-
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
|
-
*/
|
|
22
|
-
/**
|
|
23
|
-
* The cheapest right-angled line from `start` to `end` that enters no wall, or
|
|
24
|
-
* undefined when there is none.
|
|
25
|
-
*/
|
|
26
|
-
export function searchRoute(start, end, walls, options) {
|
|
27
|
-
const { clear, turn } = options;
|
|
28
|
-
// The stretches at the ends are fixed. Each crosses its own box's margin and
|
|
29
|
-
// runs `stub` further, or, where another box is closer than that, stops in
|
|
30
|
-
// the middle of the gap between the two margins: a node placed a `tight` gap
|
|
31
|
-
// away still leaves room to get out.
|
|
32
|
-
const others = (own) => walls.filter((wall) => !own.includes(wall));
|
|
33
|
-
const reach = (from, own) => {
|
|
34
|
-
const exit = Math.max(0, ...own.map((wall) => depth(from, wall)));
|
|
35
|
-
const wanted = Math.max(options.stub, exit + 1);
|
|
36
|
-
const hit = Math.min(Infinity, ...others(own).map((wall) => distanceTo(from, wall, wanted)));
|
|
37
|
-
const length = hit >= wanted ? wanted : (exit + hit) / 2;
|
|
38
|
-
if (length <= exit)
|
|
39
|
-
return undefined;
|
|
40
|
-
return { x: snap(from.x + from.dx * length), y: snap(from.y + from.dy * length) };
|
|
41
|
-
};
|
|
42
|
-
const out = reach(start, options.ownStart);
|
|
43
|
-
const back = reach(end, options.ownEnd);
|
|
44
|
-
if (!out || !back)
|
|
45
|
-
return undefined;
|
|
46
|
-
if (inside(out, walls) || inside(back, walls))
|
|
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;
|
|
52
|
-
const bounds = options.bounds;
|
|
53
|
-
const gate = options.gate;
|
|
54
|
-
// How far a stretch keeps from a placed one beside it: a lane, or clear of
|
|
55
|
-
// the text either of them carries there.
|
|
56
|
-
const halves = options.takenHalves ?? [];
|
|
57
|
-
const ownHalf = options.ownHalf ?? 0;
|
|
58
|
-
const inGate = (a, b) => {
|
|
59
|
-
if (!gate)
|
|
60
|
-
return false;
|
|
61
|
-
const along = gate.run === 'x' ? Math.abs(a.y - b.y) < 0.5 : Math.abs(a.x - b.x) < 0.5;
|
|
62
|
-
const level = gate.run === 'x' ? a.y : a.x;
|
|
63
|
-
return along && level > gate.lo && level < gate.hi;
|
|
64
|
-
};
|
|
65
|
-
const spacing = (line, stretch, own) => Math.max(options.lane, (halves[line]?.[stretch] ?? 0) + own);
|
|
66
|
-
const gateTracks = (axis) => !gate ? [] : axis === gate.run ? [gate.at] : [(gate.lo + gate.hi) / 2];
|
|
67
|
-
const xs = tracks(walls, 'x', clear, [out.x, back.x, ...gateTracks('x')], options.taken, spacing, ownHalf)
|
|
68
|
-
.filter((x) => !bounds || (x >= bounds.minX && x <= bounds.maxX) || x === out.x || x === back.x);
|
|
69
|
-
const ys = tracks(walls, 'y', clear, [out.y, back.y, ...gateTracks('y')], options.taken, spacing, ownHalf)
|
|
70
|
-
.filter((y) => !bounds || (y >= bounds.minY && y <= bounds.maxY) || y === out.y || y === back.y);
|
|
71
|
-
const ix = new Map(xs.map((x, index) => [x, index]));
|
|
72
|
-
const iy = new Map(ys.map((y, index) => [y, index]));
|
|
73
|
-
const width = xs.length;
|
|
74
|
-
const height = ys.length;
|
|
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;
|
|
77
|
-
// Four headings: 0 right, 1 down, 2 left, 3 up.
|
|
78
|
-
const STEP = [
|
|
79
|
-
[1, 0],
|
|
80
|
-
[0, 1],
|
|
81
|
-
[-1, 0],
|
|
82
|
-
[0, -1],
|
|
83
|
-
];
|
|
84
|
-
const heading = (dx, dy) => (dx > 0 ? 0 : dy > 0 ? 1 : dx < 0 ? 2 : 3);
|
|
85
|
-
const first = heading(start.dx, start.dy);
|
|
86
|
-
// The last stretch travels into the end, against the way its side faces.
|
|
87
|
-
const last = heading(-end.dx, -end.dy);
|
|
88
|
-
const si = ix.get(out.x);
|
|
89
|
-
const sj = iy.get(out.y);
|
|
90
|
-
const ti = ix.get(back.x);
|
|
91
|
-
const tj = iy.get(back.y);
|
|
92
|
-
// Cost is compared first on length and turns, then on a tie-break that
|
|
93
|
-
// prefers a stretch higher up the page, or further right, so that of two
|
|
94
|
-
// ways round that are exactly as long the line goes over the top. A state
|
|
95
|
-
// also records whether the line has passed its gate yet; it may only finish
|
|
96
|
-
// once it has.
|
|
97
|
-
const size = width * height * 8;
|
|
98
|
-
const cost = new Float64Array(size).fill(Infinity);
|
|
99
|
-
const tie = new Float64Array(size).fill(Infinity);
|
|
100
|
-
const from = new Int32Array(size).fill(-1);
|
|
101
|
-
const state = (i, j, h, p) => ((j * width + i) * 4 + h) * 2 + p;
|
|
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
|
-
// Until the line has passed its gate, that is by way of the gate.
|
|
110
|
-
const toGo = (current) => {
|
|
111
|
-
const cell = (current - (current % 8)) / 8;
|
|
112
|
-
const i = cell % width;
|
|
113
|
-
const point = { x: xs[i], y: ys[(cell - i) / width] };
|
|
114
|
-
return leastLength(point, back, current % 2 === 1 ? undefined : gate);
|
|
115
|
-
};
|
|
116
|
-
const heap = new MinHeap();
|
|
117
|
-
const begin = state(si, sj, first, 0);
|
|
118
|
-
const goal = state(ti, tj, last, done);
|
|
119
|
-
cost[begin] = 0;
|
|
120
|
-
tie[begin] = 0;
|
|
121
|
-
heap.push(begin, toGo(begin), 0);
|
|
122
|
-
while (heap.size > 0) {
|
|
123
|
-
const [current, f, t] = heap.pop();
|
|
124
|
-
const c = cost[current];
|
|
125
|
-
const due = c + toGo(current);
|
|
126
|
-
if (f > due || (f === due && t > tie[current]))
|
|
127
|
-
continue;
|
|
128
|
-
// Everything still waiting costs at least this much.
|
|
129
|
-
if (f >= limit || f > cost[goal] + 1e-9)
|
|
130
|
-
break;
|
|
131
|
-
const p = current % 2;
|
|
132
|
-
const rest = (current - p) / 2;
|
|
133
|
-
const h = rest % 4;
|
|
134
|
-
const cell = (rest - h) / 4;
|
|
135
|
-
const i = cell % width;
|
|
136
|
-
const j = (cell - i) / width;
|
|
137
|
-
if (current === goal)
|
|
138
|
-
continue;
|
|
139
|
-
for (let next = 0; next < 4; next += 1) {
|
|
140
|
-
// A line never doubles straight back on itself.
|
|
141
|
-
if ((next + 2) % 4 === h)
|
|
142
|
-
continue;
|
|
143
|
-
if (next !== h) {
|
|
144
|
-
// Turning on the spot costs a turn and moves nowhere.
|
|
145
|
-
relax(current, state(i, j, next, p), c + turn, t);
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const ni = i + STEP[next][0];
|
|
149
|
-
const nj = j + STEP[next][1];
|
|
150
|
-
if (ni < 0 || nj < 0 || ni >= width || nj >= height || !open(ni, nj))
|
|
151
|
-
continue;
|
|
152
|
-
// A step is filed under the point it starts from on its way right or down.
|
|
153
|
-
const axis = next % 2;
|
|
154
|
-
const key = next < 2 ? j * width + i : nj * width + ni;
|
|
155
|
-
if (steps.closed[axis][key] === 1)
|
|
156
|
-
continue;
|
|
157
|
-
const a = { x: xs[i], y: ys[j] };
|
|
158
|
-
const b = { x: xs[ni], y: ys[nj] };
|
|
159
|
-
// Passing the gate: a stretch through it, reaching the line at `at`.
|
|
160
|
-
const through = inGate(a, b) &&
|
|
161
|
-
Math.min(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) <= gate.at + 0.01 &&
|
|
162
|
-
Math.max(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) >= gate.at - 0.01;
|
|
163
|
-
const np = p === 1 || through ? 1 : 0;
|
|
164
|
-
// Running closer to a wall than `clear` counts double: the line may pass
|
|
165
|
-
// a narrow gap, but where there is room it keeps its distance. Crossing a
|
|
166
|
-
// line already placed costs what a turn does, so of two ways much the
|
|
167
|
-
// same length the one that crosses nothing wins.
|
|
168
|
-
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) + steps.crowd[axis][key] +
|
|
169
|
-
turn * steps.cross[axis][key];
|
|
170
|
-
// Higher up for a stretch across, further right for one down: a smaller
|
|
171
|
-
// tie-break is preferred, so the across level counts as it is and the
|
|
172
|
-
// down level counts negated.
|
|
173
|
-
const lean = next % 2 === 0 ? a.y * length : -a.x * length;
|
|
174
|
-
relax(current, state(ni, nj, next, np), c + length, t + lean);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (!(cost[goal] < limit))
|
|
178
|
-
return undefined;
|
|
179
|
-
function relax(previous, next, c, t) {
|
|
180
|
-
if (c < cost[next] - 1e-9 || (Math.abs(c - cost[next]) <= 1e-9 && t < tie[next])) {
|
|
181
|
-
cost[next] = c;
|
|
182
|
-
tie[next] = t;
|
|
183
|
-
from[next] = previous;
|
|
184
|
-
heap.push(next, c + toGo(next), t);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
const path = [];
|
|
188
|
-
for (let at = goal; at >= 0; at = from[at]) {
|
|
189
|
-
const rest = (at - (at % 2)) / 2;
|
|
190
|
-
const cell = (rest - (rest % 4)) / 4;
|
|
191
|
-
const i = cell % width;
|
|
192
|
-
const j = (cell - i) / width;
|
|
193
|
-
const point = { x: xs[i], y: ys[j] };
|
|
194
|
-
const previous = path[path.length - 1];
|
|
195
|
-
if (!previous || previous.x !== point.x || previous.y !== point.y)
|
|
196
|
-
path.push(point);
|
|
197
|
-
}
|
|
198
|
-
path.reverse();
|
|
199
|
-
return { points: straightRuns([start, ...path, end]), cost: cost[goal] };
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* The shortest a right-angled line from `a` to `b` could be: straight across
|
|
203
|
-
* and straight down, or with a gate, across to its line at `at` and to a
|
|
204
|
-
* level within it on the way.
|
|
205
|
-
*/
|
|
206
|
-
function leastLength(a, b, gate) {
|
|
207
|
-
if (!gate)
|
|
208
|
-
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
|
209
|
-
const [u, v, bu, bv] = gate.run === 'x' ? [a.x, a.y, b.x, b.y] : [a.y, a.x, b.y, b.x];
|
|
210
|
-
const off = Math.max(0, gate.lo - Math.max(v, bv), Math.min(v, bv) - gate.hi);
|
|
211
|
-
return Math.abs(u - gate.at) + Math.abs(gate.at - bu) + Math.abs(v - bv) + 2 * off;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* The tracks on one axis: beside each wall at `clear` where that is open,
|
|
215
|
-
* down the middle of every gap between walls, through the ends, and a lane
|
|
216
|
-
* either side of each line already placed — a wider one where a text rides,
|
|
217
|
-
* and wider again by `own` for this line's own text.
|
|
218
|
-
*/
|
|
219
|
-
function tracks(walls, axis, clear, ends, taken, spacing, own) {
|
|
220
|
-
const lo = (wall) => (axis === 'x' ? wall.minX : wall.minY);
|
|
221
|
-
const hi = (wall) => (axis === 'x' ? wall.maxX : wall.maxY);
|
|
222
|
-
// The other axis, to tell a real gap — two walls facing each other across
|
|
223
|
-
// it — from two edges that merely happen to lie near each other on this one.
|
|
224
|
-
const olo = (wall) => (axis === 'x' ? wall.minY : wall.minX);
|
|
225
|
-
const ohi = (wall) => (axis === 'x' ? wall.maxY : wall.maxX);
|
|
226
|
-
const found = new Set(ends);
|
|
227
|
-
for (const wall of walls) {
|
|
228
|
-
found.add(lo(wall) - clear);
|
|
229
|
-
found.add(hi(wall) + clear);
|
|
230
|
-
}
|
|
231
|
-
for (const a of walls) {
|
|
232
|
-
for (const b of walls) {
|
|
233
|
-
if (hi(a) < lo(b) && olo(a) < ohi(b) && olo(b) < ohi(a))
|
|
234
|
-
found.add((hi(a) + lo(b)) / 2);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
taken.forEach((line, which) => {
|
|
238
|
-
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
239
|
-
const [a, b] = [line[index], line[index + 1]];
|
|
240
|
-
// A stretch across sits at a level on y; one down, at a level on x.
|
|
241
|
-
const across = Math.abs(a.y - b.y) < 0.5;
|
|
242
|
-
if ((axis === 'y') !== across)
|
|
243
|
-
continue;
|
|
244
|
-
// A lane either side of a placed line. One that runs close beside a box
|
|
245
|
-
// is paid for as crowding, like any other track.
|
|
246
|
-
const level = axis === 'y' ? a.y : a.x;
|
|
247
|
-
for (const by of new Set([spacing(which, index, 0), spacing(which, index, own)])) {
|
|
248
|
-
found.add(level - by);
|
|
249
|
-
found.add(level + by);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
return [...new Set([...found].map(snap))].sort((p, q) => p - q);
|
|
254
|
-
}
|
|
255
|
-
/** Coordinates to a hundredth, so a track and the end it runs through compare equal. */
|
|
256
|
-
function snap(value) {
|
|
257
|
-
return Math.round(value * 100) / 100;
|
|
258
|
-
}
|
|
259
|
-
/** How far along its heading an end runs before it leaves a wall it starts inside; 0 if it is outside. */
|
|
260
|
-
function depth(from, wall) {
|
|
261
|
-
if (!(from.x >= wall.minX && from.x <= wall.maxX && from.y >= wall.minY && from.y <= wall.maxY))
|
|
262
|
-
return 0;
|
|
263
|
-
if (from.dx > 0)
|
|
264
|
-
return wall.maxX - from.x;
|
|
265
|
-
if (from.dx < 0)
|
|
266
|
-
return from.x - wall.minX;
|
|
267
|
-
if (from.dy > 0)
|
|
268
|
-
return wall.maxY - from.y;
|
|
269
|
-
return from.y - wall.minY;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* How far along its heading an end runs before it meets a wall it is outside
|
|
273
|
-
* of, looking no further than `limit`; Infinity if it meets none.
|
|
274
|
-
*/
|
|
275
|
-
function distanceTo(from, wall, limit) {
|
|
276
|
-
const tip = { x: from.x + from.dx * limit, y: from.y + from.dy * limit };
|
|
277
|
-
if (!blocked(from, tip, [wall]))
|
|
278
|
-
return Infinity;
|
|
279
|
-
if (from.dx > 0)
|
|
280
|
-
return wall.minX - from.x;
|
|
281
|
-
if (from.dx < 0)
|
|
282
|
-
return from.x - wall.maxX;
|
|
283
|
-
if (from.dy > 0)
|
|
284
|
-
return wall.minY - from.y;
|
|
285
|
-
return from.y - wall.maxY;
|
|
286
|
-
}
|
|
287
|
-
/** Whether a point lies strictly inside any wall. */
|
|
288
|
-
function inside(point, walls) {
|
|
289
|
-
return walls.some((wall) => point.x > wall.minX && point.x < wall.maxX && point.y > wall.minY && point.y < wall.maxY);
|
|
290
|
-
}
|
|
291
|
-
/** Whether a level or upright stretch passes through the inside of any wall. */
|
|
292
|
-
function blocked(a, b, walls) {
|
|
293
|
-
return walls.some((wall) => blocks(a, b, wall));
|
|
294
|
-
}
|
|
295
|
-
/** Whether a level or upright stretch passes through the inside of one wall. */
|
|
296
|
-
function blocks(a, b, wall) {
|
|
297
|
-
const minX = Math.min(a.x, b.x);
|
|
298
|
-
const maxX = Math.max(a.x, b.x);
|
|
299
|
-
const minY = Math.min(a.y, b.y);
|
|
300
|
-
const maxY = Math.max(a.y, b.y);
|
|
301
|
-
if (minY === maxY)
|
|
302
|
-
return minY > wall.minY && minY < wall.maxY && maxX > wall.minX && minX < wall.maxX;
|
|
303
|
-
return minX > wall.minX && minX < wall.maxX && maxY > wall.minY && minY < wall.maxY;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Every single step through the grid — from a point to its neighbor on the
|
|
307
|
-
* right, or the one below — and what it costs beyond its length: whether it is
|
|
308
|
-
* closed, by a wall or by running along a placed line; how far it runs close
|
|
309
|
-
* beside a wall; how many placed lines it crosses. The search takes each step
|
|
310
|
-
* many times over, once for each heading it arrives with, so these are worked
|
|
311
|
-
* out here once, and each wall and each placed stretch is tested only against
|
|
312
|
-
* the steps that lie near it. A step is filed at the index of the point it
|
|
313
|
-
* starts from: `[0]` the step right, `[1]` the step down.
|
|
314
|
-
*/
|
|
315
|
-
function stepTable(xs, ys, walls, clear, taken, spacing, widest, ownAt) {
|
|
316
|
-
const width = xs.length;
|
|
317
|
-
const height = ys.length;
|
|
318
|
-
const cells = width * height;
|
|
319
|
-
const open = new Uint8Array(cells).fill(1);
|
|
320
|
-
const closed = [new Uint8Array(cells), new Uint8Array(cells)];
|
|
321
|
-
const crowd = [new Float64Array(cells), new Float64Array(cells)];
|
|
322
|
-
const cross = [new Int32Array(cells), new Int32Array(cells)];
|
|
323
|
-
// The indices of the tracks lying within `lo`..`hi`, with one to spare either
|
|
324
|
-
// side: a range to test exactly, never a test itself.
|
|
325
|
-
const near = (values, lo, hi) => [Math.max(0, firstAtLeast(values, lo) - 1), Math.min(values.length - 1, firstAtLeast(values, hi) + 1)];
|
|
326
|
-
const each = ([i0, i1], [j0, j1], visit) => {
|
|
327
|
-
for (let j = j0; j <= j1; j += 1) {
|
|
328
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
329
|
-
const a = { x: xs[i], y: ys[j] };
|
|
330
|
-
if (i + 1 < width)
|
|
331
|
-
visit(0, a, { x: xs[i + 1], y: ys[j] }, j * width + i);
|
|
332
|
-
if (j + 1 < height)
|
|
333
|
-
visit(1, a, { x: xs[i], y: ys[j + 1] }, j * width + i);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
for (const wall of walls) {
|
|
338
|
-
const [i0, i1] = near(xs, wall.minX, wall.maxX);
|
|
339
|
-
const [j0, j1] = near(ys, wall.minY, wall.maxY);
|
|
340
|
-
for (let j = j0; j <= j1; j += 1) {
|
|
341
|
-
for (let i = i0; i <= i1; i += 1) {
|
|
342
|
-
const x = xs[i];
|
|
343
|
-
const y = ys[j];
|
|
344
|
-
if (x > wall.minX && x < wall.maxX && y > wall.minY && y < wall.maxY)
|
|
345
|
-
open[j * width + i] = 0;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
each([i0, i1], [j0, j1], (axis, a, b, key) => {
|
|
349
|
-
if (blocks(a, b, wall))
|
|
350
|
-
closed[axis][key] = 1;
|
|
351
|
-
});
|
|
352
|
-
each(near(xs, wall.minX - clear, wall.maxX + clear), near(ys, wall.minY - clear, wall.maxY + clear), (axis, a, b, key) => {
|
|
353
|
-
crowd[axis][key] = Math.max(crowd[axis][key], crowding(a, b, wall, clear));
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
taken.forEach((line, which) => {
|
|
357
|
-
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
358
|
-
const [p, q] = [line[index], line[index + 1]];
|
|
359
|
-
const reach = spacing(which, index, widest);
|
|
360
|
-
const xr = near(xs, Math.min(p.x, q.x) - reach, Math.max(p.x, q.x) + reach);
|
|
361
|
-
const yr = near(ys, Math.min(p.y, q.y) - reach, Math.max(p.y, q.y) + reach);
|
|
362
|
-
each(xr, yr, (axis, a, b, key) => {
|
|
363
|
-
if (runsAlong(a, b, p, q, spacing(which, index, ownAt(a, b))))
|
|
364
|
-
closed[axis][key] = 1;
|
|
365
|
-
if (crosses(a, b, p, q))
|
|
366
|
-
cross[axis][key] = cross[axis][key] + 1;
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
return { open, closed, crowd, cross };
|
|
371
|
-
}
|
|
372
|
-
/** The index of the first of the sorted values at least `value`, or their count if none is. */
|
|
373
|
-
function firstAtLeast(values, value) {
|
|
374
|
-
let lo = 0;
|
|
375
|
-
let hi = values.length;
|
|
376
|
-
while (lo < hi) {
|
|
377
|
-
const mid = (lo + hi) >> 1;
|
|
378
|
-
if (values[mid] < value)
|
|
379
|
-
lo = mid + 1;
|
|
380
|
-
else
|
|
381
|
-
hi = mid;
|
|
382
|
-
}
|
|
383
|
-
return lo;
|
|
384
|
-
}
|
|
385
|
-
/** How much of a stretch runs within `clear` of one wall's side, alongside it; 0 if none does. */
|
|
386
|
-
function crowding(a, b, wall, clear) {
|
|
387
|
-
const across = Math.abs(a.y - b.y) < 0.5;
|
|
388
|
-
const level = across ? a.y : a.x;
|
|
389
|
-
const from = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
390
|
-
const to = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
391
|
-
const [lo, hi] = across ? [wall.minY, wall.maxY] : [wall.minX, wall.maxX];
|
|
392
|
-
const [alo, ahi] = across ? [wall.minX, wall.maxX] : [wall.minY, wall.maxY];
|
|
393
|
-
const near = (level > lo - clear + 0.01 && level <= lo) || (level >= hi && level < hi + clear - 0.01);
|
|
394
|
-
return near ? Math.max(0, Math.min(to, ahi) - Math.max(from, alo)) : 0;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Whether a stretch runs along one stretch of a placed line, for any distance,
|
|
398
|
-
* closer than `spacing`: a lane, or clear of a text either carries.
|
|
399
|
-
*/
|
|
400
|
-
function runsAlong(a, b, p, q, spacing) {
|
|
401
|
-
const across = Math.abs(a.y - b.y) < 0.5;
|
|
402
|
-
if ((Math.abs(p.y - q.y) < 0.5) !== across)
|
|
403
|
-
return false;
|
|
404
|
-
const level = across ? a.y : a.x;
|
|
405
|
-
const other = across ? p.y : p.x;
|
|
406
|
-
if (Math.abs(other - level) >= spacing - 0.5)
|
|
407
|
-
return false;
|
|
408
|
-
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
409
|
-
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
410
|
-
const pLo = across ? Math.min(p.x, q.x) : Math.min(p.y, q.y);
|
|
411
|
-
const pHi = across ? Math.max(p.x, q.x) : Math.max(p.y, q.y);
|
|
412
|
-
return Math.min(hi, pHi) - Math.max(lo, pLo) > 0.5;
|
|
413
|
-
}
|
|
414
|
-
/** Whether a stretch crosses one stretch of a placed line square on, touching neither end of either. */
|
|
415
|
-
function crosses(a, b, p, q) {
|
|
416
|
-
const across = Math.abs(a.y - b.y) < 0.5;
|
|
417
|
-
if ((Math.abs(p.y - q.y) < 0.5) === across)
|
|
418
|
-
return false;
|
|
419
|
-
const level = across ? a.y : a.x;
|
|
420
|
-
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
421
|
-
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
422
|
-
const at = across ? p.x : p.y;
|
|
423
|
-
const pLo = across ? Math.min(p.y, q.y) : Math.min(p.x, q.x);
|
|
424
|
-
const pHi = across ? Math.max(p.y, q.y) : Math.max(p.x, q.x);
|
|
425
|
-
return at > lo + 0.5 && at < hi - 0.5 && level > pLo + 0.5 && level < pHi - 0.5;
|
|
426
|
-
}
|
|
427
|
-
/** The points with every one dropped that sits on a straight run between its neighbors. */
|
|
428
|
-
function straightRuns(points) {
|
|
429
|
-
const kept = [];
|
|
430
|
-
for (const point of points) {
|
|
431
|
-
const previous = kept[kept.length - 1];
|
|
432
|
-
if (previous && Math.abs(previous.x - point.x) < 0.01 && Math.abs(previous.y - point.y) < 0.01)
|
|
433
|
-
continue;
|
|
434
|
-
kept.push(point);
|
|
435
|
-
while (kept.length >= 3) {
|
|
436
|
-
const [a, b, c] = kept.slice(-3);
|
|
437
|
-
const level = Math.abs(a.y - b.y) < 0.01 && Math.abs(b.y - c.y) < 0.01;
|
|
438
|
-
const upright = Math.abs(a.x - b.x) < 0.01 && Math.abs(b.x - c.x) < 0.01;
|
|
439
|
-
if (!level && !upright)
|
|
440
|
-
break;
|
|
441
|
-
kept.splice(kept.length - 2, 1);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return kept;
|
|
445
|
-
}
|
|
446
|
-
/** A binary heap of states, cheapest first, ties broken by the second cost. */
|
|
447
|
-
class MinHeap {
|
|
448
|
-
items = [];
|
|
449
|
-
get size() {
|
|
450
|
-
return this.items.length;
|
|
451
|
-
}
|
|
452
|
-
push(state, cost, tie) {
|
|
453
|
-
const items = this.items;
|
|
454
|
-
items.push([state, cost, tie]);
|
|
455
|
-
let at = items.length - 1;
|
|
456
|
-
while (at > 0) {
|
|
457
|
-
const parent = (at - 1) >> 1;
|
|
458
|
-
if (!before(items[at], items[parent]))
|
|
459
|
-
break;
|
|
460
|
-
[items[at], items[parent]] = [items[parent], items[at]];
|
|
461
|
-
at = parent;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
pop() {
|
|
465
|
-
const items = this.items;
|
|
466
|
-
const top = items[0];
|
|
467
|
-
const tail = items.pop();
|
|
468
|
-
if (items.length > 0) {
|
|
469
|
-
items[0] = tail;
|
|
470
|
-
let at = 0;
|
|
471
|
-
for (;;) {
|
|
472
|
-
const left = at * 2 + 1;
|
|
473
|
-
const right = left + 1;
|
|
474
|
-
let best = at;
|
|
475
|
-
if (left < items.length && before(items[left], items[best]))
|
|
476
|
-
best = left;
|
|
477
|
-
if (right < items.length && before(items[right], items[best]))
|
|
478
|
-
best = right;
|
|
479
|
-
if (best === at)
|
|
480
|
-
break;
|
|
481
|
-
[items[at], items[best]] = [items[best], items[at]];
|
|
482
|
-
at = best;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return top;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
function before(p, q) {
|
|
489
|
-
return p[1] < q[1] - 1e-9 || (Math.abs(p[1] - q[1]) <= 1e-9 && p[2] < q[2]);
|
|
490
|
-
}
|