reladraw 0.23.0 → 0.24.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/.claude/skills/reladraw/SKILL.md +2 -2
- package/.claude/skills/reladraw/reference/syntax.md +16 -6
- package/README.md +6 -4
- package/SYNTAX.md +16 -6
- package/dist/cli.js +108 -13
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/format.js +5 -3
- package/dist/parser.js +14 -7
- package/dist/search.d.ts +104 -0
- package/dist/search.js +490 -0
- package/package.json +1 -2
|
@@ -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.
|
|
18
|
+
This skill teaches reladraw 0.24.0. Once per session, before the first render, run:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
reladraw --version
|
|
@@ -23,7 +23,7 @@ reladraw --version
|
|
|
23
23
|
|
|
24
24
|
- **The same:** say nothing.
|
|
25
25
|
- **Older, or `--version` isn't recognized** (it arrived in 0.17.1): the tool is older than this page and will refuse some of what it teaches. Tell the user, and give them `npm install -g reladraw@latest`.
|
|
26
|
-
- **Newer:** this skill is out of date. Tell the user, and give them `reladraw install-skill
|
|
26
|
+
- **Newer:** this skill is out of date. Tell the user, and give them `reladraw install-skill`, which asks which agents to install for and replaces it with the skill that came with their reladraw. Carry on meanwhile; where an error names a replacement spelling, trust the error over this page.
|
|
27
27
|
|
|
28
28
|
If `reladraw` isn't installed, `npx reladraw` works without installing, and `npm install -g reladraw` installs it.
|
|
29
29
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.24.0
|
|
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
|
|
|
@@ -59,7 +59,7 @@ What every statement of a kind has is positional; what is optional is a key. A w
|
|
|
59
59
|
|
|
60
60
|
### Names and words
|
|
61
61
|
|
|
62
|
-
A name is unique across nodes, points, regions, lines and bundles. It may not contain `:`, which would make it a key, or `;`, which is reserved. A dot means a child: `server.docker` is inside `server`, and the parent must be declared first. Moving a node to another parent is a rename.
|
|
62
|
+
A name is unique across nodes, points, regions, lines and bundles. It may not contain `:`, which would make it a key, `,`, which separates the items of a list, or `;`, which is reserved. A dot means a child: `server.docker` is inside `server`, and the parent must be declared first. Moving a node to another parent is a rename.
|
|
63
63
|
|
|
64
64
|
Every word the language owns that has more than one part is hyphenated (`from-mark:`, `bottom-left`, `min-height:`, `align-children:`), while a phrase of separate words stays spaced (`left of`, `even with`). A name you choose should look chosen: the examples use underscores (`some_region`). `to_mark:` is refused by name, pointing to `to-mark:`.
|
|
65
65
|
|
|
@@ -1145,9 +1145,9 @@ A thing can have several links, written one after another, each with a name and
|
|
|
1145
1145
|
|
|
1146
1146
|
```
|
|
1147
1147
|
node api "Orders API"
|
|
1148
|
-
url: "https://
|
|
1148
|
+
url: "https://docs.example.com/orders" (name: "Docs", icon: "D" (fill: #3a6ea5, text: white))
|
|
1149
1149
|
"https://github.com/…" (name: "GitHub", icon: gh)
|
|
1150
|
-
"https://
|
|
1150
|
+
"https://wiki.example.com/orders" (name: "Wiki")
|
|
1151
1151
|
```
|
|
1152
1152
|
|
|
1153
1153
|
- **`name:`** is what a reader is shown, in quotes. A link with none shows its site, such as `github.com`.
|
|
@@ -1203,7 +1203,7 @@ reladraw check diagram.reladraw
|
|
|
1203
1203
|
| `--no-checks` | print no findings; errors still print |
|
|
1204
1204
|
| `-v`, `--version` | print the version |
|
|
1205
1205
|
|
|
1206
|
-
`reladraw install-skill` installs the agent skill that came with this version of reladraw, by running `npx skills add` on it with `--copy`.
|
|
1206
|
+
`reladraw install-skill` installs the agent skill that came with this version of reladraw, for every project, by running `npx skills add` on it with `--copy`. `--claude` installs it in `~/.claude/skills`, `--agents` in `~/.agents/skills` (Codex and others), and both may be given; with neither, it asks, or, with no terminal to ask in, refuses and names them. For any other agent it prints the `skills` command to run.
|
|
1207
1207
|
|
|
1208
1208
|
The output is a standalone SVG: every picture is drawn into it as path data, never from a font or a linked file. A web page can draw a diagram in place with `<reladraw-diagram>`; see "In a web page" in the README.
|
|
1209
1209
|
|
|
@@ -1338,9 +1338,18 @@ Designed and absent from the code.
|
|
|
1338
1338
|
|
|
1339
1339
|
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.
|
|
1340
1340
|
|
|
1341
|
+
**0.24.0**
|
|
1342
|
+
|
|
1343
|
+
- **`reladraw install-skill` installs for Claude Code, the `~/.agents` folder, or both, and asks which.** It used to pass everything on to `skills`, which, given no agent, put a copy in the folder of every agent it knows, more than fifty of them, and in the current project unless `-g` was written. It now always installs for every project, in the places `--claude` and `--agents` name; `-a` and the other `skills` options are refused with the `skills` command that does the same, and `-g` is still accepted.
|
|
1344
|
+
|
|
1345
|
+
**0.23.1**
|
|
1346
|
+
|
|
1347
|
+
- **`reladraw format` no longer fails on a comment between a placement and its bracket**, such as `right of a`, then `// why wide`, then `(gap: wide)` on lines of their own. It stopped with "Cannot read properties of undefined"; it now keeps the comment where it was. Reported in #22.
|
|
1348
|
+
- **A comma in a name is refused by name.** `node a,b` has been refused since 0.19.0, but the message said "a text is quoted — write "b" rather than b"; it now says a name may not contain a comma, because a comma separates the items of a list. Reported in #23.
|
|
1349
|
+
|
|
1341
1350
|
**0.23.0**
|
|
1342
1351
|
|
|
1343
|
-
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://
|
|
1352
|
+
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://docs.example.com/…" (name: "Docs")`. In an SVG a click follows the first and the tooltip lists them all. See [Destinations](#destinations).
|
|
1344
1353
|
- **A click on the page selects a box or a line.** The page made with `render --page` lights what feeds it in green and what it feeds in red, and a panel beside the drawing lists its links and the boxes at either end. A click no longer follows a link on the page; the panel does. See [A page for looking at a diagram](#a-page-for-looking-at-a-diagram).
|
|
1345
1354
|
- **A line with no text can have a link.** It used to be refused as too thin to click; an unseen wider copy of the line now takes the click.
|
|
1346
1355
|
- **A letter icon**: `icon gh letters: "GH" fill: #24292f text: white` is a few letters in a circle, usable anywhere an icon is. See [Your own icons](#your-own-icons).
|
|
@@ -1386,6 +1395,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal is r
|
|
|
1386
1395
|
|
|
1387
1396
|
- **`reladraw format`** lays a file out again: each statement on one line, neighboring statements with the same keyword lined up in columns, comments kept beside what they are about. No word is respelled, and the result is checked to draw what the original did. `--check` fails on a file that would change, and `.reladraw-format` holds a project's settings; `reladraw format --init` writes one with every setting explained. `reladraw format -` reads standard input and prints the result, for editors. A long statement can wrap past a width (`wrap-at`) or go one clause to a line (`clause-per-line-at`), and `clause-order` puts placements and keys in a chosen order. The playground has a Format button (Shift+Alt+F, undone with Ctrl+Z) and a panel for its settings, which can copy them as a `.reladraw-format`. Spacing inside a clause is evened out too: `gap:tight` becomes `gap: tight`, `( gap: tight )` becomes `(gap: tight)`. Prompted by #14, where a long statement was hard to read in a plain editor.
|
|
1388
1397
|
- **A comma may touch the next word or stand apart from both.** `(corners: rounded,path: square)` used to be refused with "`corners` has no value", and `(at: start , above edge)` was refused in a text's bracket; both now read as `rounded, path:` and `start, above` do.
|
|
1398
|
+
- Breaking, and missing from this list when 0.19.0 shipped: a name may no longer contain `,`. `node a,b` used to declare a node called `a,b`; the comma now ends the name, so the file is refused. Added after the fact, from #23.
|
|
1389
1399
|
- **The playground opens and saves `.reladraw` files**, with Open… and Save… above the source. Save suggests the name of the file last opened or saved; Chrome and Edge ask where to put it, other browsers save it with the downloads. Opening a file over edits that haven't been saved to one asks first.
|
|
1390
1400
|
- **The playground's error message can be copied**, with a Copy button beside it or by selecting it. Only the underlined line number jumps to the line now; a click anywhere on the message used to, which took a selection away.
|
|
1391
1401
|
- **The playground no longer fails on some diagrams with 0.18.0's shapes**, which stopped with "insidePolygon is not defined". The command line was unaffected.
|
package/README.md
CHANGED
|
@@ -77,22 +77,24 @@ Each `<reladraw-diagram>` is replaced by its SVG. Indent the source however suit
|
|
|
77
77
|
To install a skill to let your agent know how to use reladraw:
|
|
78
78
|
|
|
79
79
|
```
|
|
80
|
-
reladraw install-skill
|
|
80
|
+
reladraw install-skill
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
That installs the skill that came with your copy of reladraw, so it teaches what your reladraw accepts, for every
|
|
83
|
+
That installs the skill that came with your copy of reladraw, so it teaches what your reladraw accepts, for every project. It asks where: `~/.claude/skills` for Claude Code, `~/.agents/skills` for Codex and the other agents that read it, or both. To say so without being asked, as when an agent runs it, write `--claude`, `--agents`, or both:
|
|
84
84
|
|
|
85
85
|
```
|
|
86
|
-
reladraw install-skill
|
|
86
|
+
reladraw install-skill --claude --agents
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
For any other agent, it prints the [skills](https://www.npmjs.com/package/skills) command that installs this same skill there.
|
|
90
|
+
|
|
89
91
|
The skill is a copy, so after upgrading reladraw, run the command again to bring the skill up to date with it.
|
|
90
92
|
|
|
91
93
|
To install the skill from the main branch instead, which can run ahead of the release on npm, run `npx skills add reladraw/reladraw -g`.
|
|
92
94
|
|
|
93
95
|
## Status
|
|
94
96
|
|
|
95
|
-
Version 0.
|
|
97
|
+
Version 0.24.0. 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.
|
|
96
98
|
|
|
97
99
|
## License
|
|
98
100
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.24.0
|
|
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
|
|
|
@@ -59,7 +59,7 @@ What every statement of a kind has is positional; what is optional is a key. A w
|
|
|
59
59
|
|
|
60
60
|
### Names and words
|
|
61
61
|
|
|
62
|
-
A name is unique across nodes, points, regions, lines and bundles. It may not contain `:`, which would make it a key, or `;`, which is reserved. A dot means a child: `server.docker` is inside `server`, and the parent must be declared first. Moving a node to another parent is a rename.
|
|
62
|
+
A name is unique across nodes, points, regions, lines and bundles. It may not contain `:`, which would make it a key, `,`, which separates the items of a list, or `;`, which is reserved. A dot means a child: `server.docker` is inside `server`, and the parent must be declared first. Moving a node to another parent is a rename.
|
|
63
63
|
|
|
64
64
|
Every word the language owns that has more than one part is hyphenated (`from-mark:`, `bottom-left`, `min-height:`, `align-children:`), while a phrase of separate words stays spaced (`left of`, `even with`). A name you choose should look chosen: the examples use underscores (`some_region`). `to_mark:` is refused by name, pointing to `to-mark:`.
|
|
65
65
|
|
|
@@ -1145,9 +1145,9 @@ A thing can have several links, written one after another, each with a name and
|
|
|
1145
1145
|
|
|
1146
1146
|
```
|
|
1147
1147
|
node api "Orders API"
|
|
1148
|
-
url: "https://
|
|
1148
|
+
url: "https://docs.example.com/orders" (name: "Docs", icon: "D" (fill: #3a6ea5, text: white))
|
|
1149
1149
|
"https://github.com/…" (name: "GitHub", icon: gh)
|
|
1150
|
-
"https://
|
|
1150
|
+
"https://wiki.example.com/orders" (name: "Wiki")
|
|
1151
1151
|
```
|
|
1152
1152
|
|
|
1153
1153
|
- **`name:`** is what a reader is shown, in quotes. A link with none shows its site, such as `github.com`.
|
|
@@ -1203,7 +1203,7 @@ reladraw check diagram.reladraw
|
|
|
1203
1203
|
| `--no-checks` | print no findings; errors still print |
|
|
1204
1204
|
| `-v`, `--version` | print the version |
|
|
1205
1205
|
|
|
1206
|
-
`reladraw install-skill` installs the agent skill that came with this version of reladraw, by running `npx skills add` on it with `--copy`.
|
|
1206
|
+
`reladraw install-skill` installs the agent skill that came with this version of reladraw, for every project, by running `npx skills add` on it with `--copy`. `--claude` installs it in `~/.claude/skills`, `--agents` in `~/.agents/skills` (Codex and others), and both may be given; with neither, it asks, or, with no terminal to ask in, refuses and names them. For any other agent it prints the `skills` command to run.
|
|
1207
1207
|
|
|
1208
1208
|
The output is a standalone SVG: every picture is drawn into it as path data, never from a font or a linked file. A web page can draw a diagram in place with `<reladraw-diagram>`; see "In a web page" in the README.
|
|
1209
1209
|
|
|
@@ -1338,9 +1338,18 @@ Designed and absent from the code.
|
|
|
1338
1338
|
|
|
1339
1339
|
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.
|
|
1340
1340
|
|
|
1341
|
+
**0.24.0**
|
|
1342
|
+
|
|
1343
|
+
- **`reladraw install-skill` installs for Claude Code, the `~/.agents` folder, or both, and asks which.** It used to pass everything on to `skills`, which, given no agent, put a copy in the folder of every agent it knows, more than fifty of them, and in the current project unless `-g` was written. It now always installs for every project, in the places `--claude` and `--agents` name; `-a` and the other `skills` options are refused with the `skills` command that does the same, and `-g` is still accepted.
|
|
1344
|
+
|
|
1345
|
+
**0.23.1**
|
|
1346
|
+
|
|
1347
|
+
- **`reladraw format` no longer fails on a comment between a placement and its bracket**, such as `right of a`, then `// why wide`, then `(gap: wide)` on lines of their own. It stopped with "Cannot read properties of undefined"; it now keeps the comment where it was. Reported in #22.
|
|
1348
|
+
- **A comma in a name is refused by name.** `node a,b` has been refused since 0.19.0, but the message said "a text is quoted — write "b" rather than b"; it now says a name may not contain a comma, because a comma separates the items of a list. Reported in #23.
|
|
1349
|
+
|
|
1341
1350
|
**0.23.0**
|
|
1342
1351
|
|
|
1343
|
-
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://
|
|
1352
|
+
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://docs.example.com/…" (name: "Docs")`. In an SVG a click follows the first and the tooltip lists them all. See [Destinations](#destinations).
|
|
1344
1353
|
- **A click on the page selects a box or a line.** The page made with `render --page` lights what feeds it in green and what it feeds in red, and a panel beside the drawing lists its links and the boxes at either end. A click no longer follows a link on the page; the panel does. See [A page for looking at a diagram](#a-page-for-looking-at-a-diagram).
|
|
1345
1354
|
- **A line with no text can have a link.** It used to be refused as too thin to click; an unseen wider copy of the line now takes the click.
|
|
1346
1355
|
- **A letter icon**: `icon gh letters: "GH" fill: #24292f text: white` is a few letters in a circle, usable anywhere an icon is. See [Your own icons](#your-own-icons).
|
|
@@ -1386,6 +1395,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal is r
|
|
|
1386
1395
|
|
|
1387
1396
|
- **`reladraw format`** lays a file out again: each statement on one line, neighboring statements with the same keyword lined up in columns, comments kept beside what they are about. No word is respelled, and the result is checked to draw what the original did. `--check` fails on a file that would change, and `.reladraw-format` holds a project's settings; `reladraw format --init` writes one with every setting explained. `reladraw format -` reads standard input and prints the result, for editors. A long statement can wrap past a width (`wrap-at`) or go one clause to a line (`clause-per-line-at`), and `clause-order` puts placements and keys in a chosen order. The playground has a Format button (Shift+Alt+F, undone with Ctrl+Z) and a panel for its settings, which can copy them as a `.reladraw-format`. Spacing inside a clause is evened out too: `gap:tight` becomes `gap: tight`, `( gap: tight )` becomes `(gap: tight)`. Prompted by #14, where a long statement was hard to read in a plain editor.
|
|
1388
1397
|
- **A comma may touch the next word or stand apart from both.** `(corners: rounded,path: square)` used to be refused with "`corners` has no value", and `(at: start , above edge)` was refused in a text's bracket; both now read as `rounded, path:` and `start, above` do.
|
|
1398
|
+
- Breaking, and missing from this list when 0.19.0 shipped: a name may no longer contain `,`. `node a,b` used to declare a node called `a,b`; the comma now ends the name, so the file is refused. Added after the fact, from #23.
|
|
1389
1399
|
- **The playground opens and saves `.reladraw` files**, with Open… and Save… above the source. Save suggests the name of the file last opened or saved; Chrome and Edge ask where to put it, other browsers save it with the downloads. Opening a file over edits that haven't been saved to one asks first.
|
|
1390
1400
|
- **The playground's error message can be copied**, with a Copy button beside it or by selecting it. Only the underlined line number jumps to the line now; a click anywhere on the message used to, which took a selection away.
|
|
1391
1401
|
- **The playground no longer fails on some diagrams with 0.18.0's shapes**, which stopped with "insidePolygon is not defined". The command line was unaffected.
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
19
19
|
reladraw format <input.reladraw>... [--check]
|
|
20
20
|
reladraw format - [--check]
|
|
21
21
|
reladraw format --init
|
|
22
|
-
reladraw install-skill [
|
|
22
|
+
reladraw install-skill [--claude] [--agents]
|
|
23
23
|
|
|
24
24
|
render draw the diagram to an SVG, and print any findings: places
|
|
25
25
|
where the picture, drawn as written, may not read as meant.
|
|
@@ -37,9 +37,10 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
37
37
|
default with what it does.
|
|
38
38
|
install-skill
|
|
39
39
|
install the agent skill that came with this version of
|
|
40
|
-
reladraw, so it teaches what this tool accepts
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
reladraw, so it teaches what this tool accepts, for every
|
|
41
|
+
project: --claude in ~/.claude/skills, --agents in
|
|
42
|
+
~/.agents/skills (Codex and others), or both. With neither,
|
|
43
|
+
it asks.
|
|
43
44
|
|
|
44
45
|
-o, --out where render writes the SVG. Defaults to the input path with
|
|
45
46
|
its extension replaced by .svg. Use - for standard output.
|
|
@@ -62,17 +63,113 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
62
63
|
const COMMANDS = ['render', 'check', 'format', 'install-skill'];
|
|
63
64
|
// The version of skills this was tried with, so a change there can't break it.
|
|
64
65
|
const SKILLS_PACKAGE = 'skills@1.7.6';
|
|
66
|
+
// The two places the skill is installed, each picked by its flag. skills
|
|
67
|
+
// knows a folder by an agent that reads it: ~/.agents/skills is its "codex",
|
|
68
|
+
// and other agents read it too. Left to itself, skills installs for every
|
|
69
|
+
// agent it knows of, more than fifty, so the agents are always named.
|
|
70
|
+
const SKILL_TARGETS = [
|
|
71
|
+
{ flag: '--claude', agent: 'claude-code', label: 'Claude Code', dir: '~/.claude/skills' },
|
|
72
|
+
{ flag: '--agents', agent: 'codex', label: 'Codex and others', dir: '~/.agents/skills' },
|
|
73
|
+
];
|
|
74
|
+
function ownVersion() {
|
|
75
|
+
// package.json ships beside dist/, so the number is the installed one.
|
|
76
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
77
|
+
}
|
|
78
|
+
// For any other agent, or a single project, skills is run by hand on this
|
|
79
|
+
// install's skill, so it still matches this reladraw.
|
|
80
|
+
function skillsCommand(skills, agent = '<agent>') {
|
|
81
|
+
const path = /\s/.test(skills) ? JSON.stringify(skills) : skills;
|
|
82
|
+
return `npx ${SKILLS_PACKAGE} add ${path} --copy -g -a ${agent}`;
|
|
83
|
+
}
|
|
84
|
+
const NAME_THE_TARGETS = SKILL_TARGETS.map((t) => `${t.flag} (${t.dir}, for ${t.label})`).join(', ') + ', or both';
|
|
85
|
+
// In a terminal, a bare install-skill asks; otherwise, as when an agent runs
|
|
86
|
+
// it, the flags must say, because there is no one to ask.
|
|
87
|
+
async function askTargets(skills) {
|
|
88
|
+
const { createInterface } = await import('node:readline/promises');
|
|
89
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
|
+
const closed = new Promise((done) => rl.once('close', () => done(undefined)));
|
|
91
|
+
const both = SKILL_TARGETS.length + 1;
|
|
92
|
+
process.stdout.write(`Install the reladraw ${ownVersion()} skill for which agents?\n` +
|
|
93
|
+
SKILL_TARGETS.map((t, i) => ` ${i + 1} ${t.label.padEnd(17)} ${t.dir}\n`).join('') +
|
|
94
|
+
` ${both} both\n ${both + 1} another agent\n`);
|
|
95
|
+
try {
|
|
96
|
+
for (;;) {
|
|
97
|
+
const answer = await Promise.race([rl.question(`Choose [${both}]: `), closed]);
|
|
98
|
+
if (answer === undefined) {
|
|
99
|
+
process.stdout.write('\n');
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const n = Number(answer.trim() || both);
|
|
103
|
+
if (n >= 1 && n <= SKILL_TARGETS.length)
|
|
104
|
+
return [SKILL_TARGETS[n - 1].agent];
|
|
105
|
+
if (n === both)
|
|
106
|
+
return SKILL_TARGETS.map((t) => t.agent);
|
|
107
|
+
if (n === both + 1)
|
|
108
|
+
return 'other';
|
|
109
|
+
process.stdout.write(`Type a number from 1 to ${both + 1}.\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
rl.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
65
116
|
// The skill ships in the package beside dist/, as the one in the repository.
|
|
66
|
-
// skills does the work of
|
|
67
|
-
//
|
|
68
|
-
function installSkill(args) {
|
|
117
|
+
// skills does the work of copying it in; --copy because an npx run leaves the
|
|
118
|
+
// package in a cache, where a link would break.
|
|
119
|
+
async function installSkill(args) {
|
|
69
120
|
const skills = fileURLToPath(new URL('../.claude/skills/', import.meta.url));
|
|
70
121
|
if (!existsSync(skills)) {
|
|
71
122
|
process.stderr.write(`reladraw: the skill isn't in this install of reladraw (looked in ${skills})\n`);
|
|
72
123
|
return 1;
|
|
73
124
|
}
|
|
125
|
+
let agents = [];
|
|
126
|
+
for (let i = 0; i < args.length; i++) {
|
|
127
|
+
const arg = args[i];
|
|
128
|
+
const target = SKILL_TARGETS.find((t) => t.flag === arg);
|
|
129
|
+
if (target) {
|
|
130
|
+
if (!agents.includes(target.agent))
|
|
131
|
+
agents.push(target.agent);
|
|
132
|
+
}
|
|
133
|
+
else if (arg === '-g' || arg === '--global') {
|
|
134
|
+
// Always global now; skills from before 0.24 tell the user to write -g.
|
|
135
|
+
}
|
|
136
|
+
else if (arg === '-h' || arg === '--help') {
|
|
137
|
+
process.stdout.write(`reladraw install-skill [--claude] [--agents]\n\n` +
|
|
138
|
+
`Installs the skill that came with reladraw ${ownVersion()}, for every project:\n` +
|
|
139
|
+
SKILL_TARGETS.map((t) => ` ${t.flag.padEnd(9)} ${t.dir}, for ${t.label}\n`).join('') +
|
|
140
|
+
`With neither, it asks. For another agent, run:\n ${skillsCommand(skills)}\n`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
else if (arg === '-a' || arg === '--agent') {
|
|
144
|
+
const agent = args[i + 1];
|
|
145
|
+
process.stderr.write(`reladraw: install-skill takes ${NAME_THE_TARGETS}, not -a. ` +
|
|
146
|
+
`For ${agent ? `the agent ${agent}` : 'another agent'}, run:\n ${skillsCommand(skills, agent)}\n`);
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
process.stderr.write(`reladraw: install-skill doesn't take ${arg}; it takes ${NAME_THE_TARGETS}. ` +
|
|
151
|
+
`For another agent, run:\n ${skillsCommand(skills)}\n`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (agents.length === 0) {
|
|
156
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
157
|
+
process.stderr.write(`reladraw: say which agents to install the skill for: ${NAME_THE_TARGETS}. ` +
|
|
158
|
+
`For another agent, run:\n ${skillsCommand(skills)}\n`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const picked = await askTargets(skills);
|
|
162
|
+
if (picked === undefined)
|
|
163
|
+
return 1;
|
|
164
|
+
if (picked === 'other') {
|
|
165
|
+
process.stdout.write(`For another agent, run this, with its name from skills' list:\n ${skillsCommand(skills)}\n`);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
agents = picked;
|
|
169
|
+
}
|
|
74
170
|
const windows = process.platform === 'win32';
|
|
75
|
-
|
|
171
|
+
// -y because the choice is already made; skills would otherwise ask again.
|
|
172
|
+
const command = [SKILLS_PACKAGE, 'add', skills, '--copy', '-g', '-y', '-a', ...agents];
|
|
76
173
|
// On Windows npx is a .cmd, which Node runs only through a shell.
|
|
77
174
|
const result = spawnSync(windows ? 'npx.cmd' : 'npx', windows ? command.map((a) => `"${a}"`) : command, {
|
|
78
175
|
stdio: 'inherit',
|
|
@@ -85,7 +182,7 @@ function installSkill(args) {
|
|
|
85
182
|
return result.status ?? 1;
|
|
86
183
|
}
|
|
87
184
|
async function main(argv) {
|
|
88
|
-
//
|
|
185
|
+
// install-skill reads its own flags, -h included.
|
|
89
186
|
if (argv[0] === 'install-skill')
|
|
90
187
|
return installSkill(argv.slice(1));
|
|
91
188
|
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
@@ -93,9 +190,7 @@ async function main(argv) {
|
|
|
93
190
|
return argv.length === 0 ? 1 : 0;
|
|
94
191
|
}
|
|
95
192
|
if (argv.includes('-v') || argv.includes('--version')) {
|
|
96
|
-
|
|
97
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
98
|
-
process.stdout.write(`${pkg.version}\n`);
|
|
193
|
+
process.stdout.write(`${ownVersion()}\n`);
|
|
99
194
|
return 0;
|
|
100
195
|
}
|
|
101
196
|
const verb = argv[0];
|
|
@@ -420,7 +515,7 @@ function nearestSettings(folder, found) {
|
|
|
420
515
|
// The exit code is set, not exited with: process.exit ends the process at
|
|
421
516
|
// once, and a write to a pipe that hasn't drained yet is lost, which cut
|
|
422
517
|
// `format -` and `render -o -` off at 64 KB. Node exits on its own once the
|
|
423
|
-
// output is written.
|
|
518
|
+
// output is written.
|
|
424
519
|
main(process.argv.slice(2)).then((code) => {
|
|
425
520
|
process.exitCode = code;
|
|
426
521
|
}, (error) => {
|
package/dist/constants.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export declare const DEFAULT_FONT_SIZE = 14;
|
|
|
93
93
|
* `small` is sampled rather than chosen. In
|
|
94
94
|
* `examples/reference/arch.png` the box and container texts run 25
|
|
95
95
|
* pixels baseline to baseline and every annotation runs 21, which is this
|
|
96
|
-
* ratio
|
|
96
|
+
* ratio. `large` is the same step
|
|
97
97
|
* taken the other way, so the scale is symmetric about the document size.
|
|
98
98
|
*/
|
|
99
99
|
export declare const TEXT_SIZES: Record<string, number>;
|
package/dist/constants.js
CHANGED
|
@@ -111,7 +111,7 @@ export const DEFAULT_FONT_SIZE = 14;
|
|
|
111
111
|
* `small` is sampled rather than chosen. In
|
|
112
112
|
* `examples/reference/arch.png` the box and container texts run 25
|
|
113
113
|
* pixels baseline to baseline and every annotation runs 21, which is this
|
|
114
|
-
* ratio
|
|
114
|
+
* ratio. `large` is the same step
|
|
115
115
|
* taken the other way, so the scale is symmetric about the document size.
|
|
116
116
|
*/
|
|
117
117
|
export const TEXT_SIZES = {
|
package/dist/format.js
CHANGED
|
@@ -216,9 +216,11 @@ function clauseRuns(tokens, starts) {
|
|
|
216
216
|
let inKey = false;
|
|
217
217
|
for (const entry of tokens) {
|
|
218
218
|
const { token } = entry;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
// A continued line can begin with a bracket, the one that belongs to a
|
|
220
|
+
// placement on the line before (a comment may sit between them), so the
|
|
221
|
+
// first token always opens a run, bracket or not.
|
|
222
|
+
const opens = runs.length === 0 ||
|
|
223
|
+
(depth === 0 && token.text !== '(' && (starts.has(token) || (token.quoted && !inKey)));
|
|
222
224
|
if (opens) {
|
|
223
225
|
runs.push([entry]);
|
|
224
226
|
inKey = isAttrKey(token);
|
package/dist/parser.js
CHANGED
|
@@ -1231,7 +1231,7 @@ function takeTitlePlacements(tokens, open, subject, line) {
|
|
|
1231
1231
|
* is the text.
|
|
1232
1232
|
*/
|
|
1233
1233
|
function parseNode(head, line) {
|
|
1234
|
-
const name = requireName(head[1], 'node', line);
|
|
1234
|
+
const name = requireName(head[1], 'node', line, head[2]);
|
|
1235
1235
|
const written = head[2];
|
|
1236
1236
|
const textToken = written?.quoted ? written : undefined;
|
|
1237
1237
|
// A bare word here is a text somebody forgot to quote far more often than
|
|
@@ -1727,7 +1727,7 @@ function parseGuide(head, line) {
|
|
|
1727
1727
|
* between two boxes, which copies of one line share.
|
|
1728
1728
|
*/
|
|
1729
1729
|
function parseBundle(head, line) {
|
|
1730
|
-
const name = requireName(head[1], 'bundle', line);
|
|
1730
|
+
const name = requireName(head[1], 'bundle', line, head[2]);
|
|
1731
1731
|
const subject = `bundle "${name}"`;
|
|
1732
1732
|
const attrs = {};
|
|
1733
1733
|
let via;
|
|
@@ -1838,7 +1838,7 @@ function parseBundle(head, line) {
|
|
|
1838
1838
|
}
|
|
1839
1839
|
/** `point funnel between c2 and api at 2/3` */
|
|
1840
1840
|
function parsePoint(head, line) {
|
|
1841
|
-
const name = requireName(head[1], 'point', line);
|
|
1841
|
+
const name = requireName(head[1], 'point', line, head[2]);
|
|
1842
1842
|
const subject = `point "${name}"`;
|
|
1843
1843
|
const marked = readMarkText(head, 'point', subject, line);
|
|
1844
1844
|
head = marked.tokens;
|
|
@@ -1857,7 +1857,7 @@ function parsePoint(head, line) {
|
|
|
1857
1857
|
}
|
|
1858
1858
|
/** `region r c bottom-left to d top-right`, or `region r between a and b (span: both)` */
|
|
1859
1859
|
function parseRegion(head, line) {
|
|
1860
|
-
const name = requireName(head[1], 'region', line);
|
|
1860
|
+
const name = requireName(head[1], 'region', line, head[2]);
|
|
1861
1861
|
const subject = `region "${name}"`;
|
|
1862
1862
|
const marked = readMarkText(head, 'region', subject, line);
|
|
1863
1863
|
head = marked.tokens;
|
|
@@ -1904,7 +1904,7 @@ function refuseKeys(attrs, keys, subject, what, line) {
|
|
|
1904
1904
|
}
|
|
1905
1905
|
/** `style <name> <attributes>` */
|
|
1906
1906
|
function parseStyle(head, line) {
|
|
1907
|
-
const name = requireName(head[1], 'style', line);
|
|
1907
|
+
const name = requireName(head[1], 'style', line, head[2]);
|
|
1908
1908
|
const attrs = attrsOnly(head, 2, line, `style "${name}"`);
|
|
1909
1909
|
if (Object.keys(attrs).length === 0) {
|
|
1910
1910
|
throw new SourceError(`style "${name}" sets nothing`, line);
|
|
@@ -1960,7 +1960,7 @@ function readLetterIcon(head, start, name, line) {
|
|
|
1960
1960
|
* resolved, since only the caller knows whether files can be read at all.
|
|
1961
1961
|
*/
|
|
1962
1962
|
function parseIcon(head, line) {
|
|
1963
|
-
const name = requireName(head[1], 'icon', line);
|
|
1963
|
+
const name = requireName(head[1], 'icon', line, head[2]);
|
|
1964
1964
|
const picture = head[2];
|
|
1965
1965
|
if (picture !== undefined && isAttrKey(picture) && picture.text === 'letters:') {
|
|
1966
1966
|
return { kind: 'icon', name, source: '', pasted: false, letters: readLetterIcon(head, 2, name, line), line };
|
|
@@ -2089,11 +2089,18 @@ function defaultRefusal(kind, key) {
|
|
|
2089
2089
|
}
|
|
2090
2090
|
return `which a default does not set — ${takes}`;
|
|
2091
2091
|
}
|
|
2092
|
-
function requireName(token, keyword, line) {
|
|
2092
|
+
function requireName(token, keyword, line, next) {
|
|
2093
2093
|
if (!token || token.quoted) {
|
|
2094
2094
|
throw new SourceError(`${keyword} needs a name`, line);
|
|
2095
2095
|
}
|
|
2096
2096
|
refuseNameMark(token, `${keyword} "${token.joined ?? token.text}"`, line);
|
|
2097
|
+
// A comma separates the items of a list (0.19.0), so `node a,b` reads as
|
|
2098
|
+
// the name `a` and a stray `b`. Say so, rather than whatever `b` turns into.
|
|
2099
|
+
if (token.text.endsWith(',')) {
|
|
2100
|
+
const touching = next && !next.quoted && next.start !== undefined && next.start === token.end;
|
|
2101
|
+
const written = touching ? `${token.text}${next.text}` : token.text;
|
|
2102
|
+
throw new SourceError(`${keyword} "${written}": a name may not contain ",", because a comma separates the items of a list`, line);
|
|
2103
|
+
}
|
|
2097
2104
|
return token.text;
|
|
2098
2105
|
}
|
|
2099
2106
|
/**
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reladraw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "A diagram language where you say where things go.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && tsc -p tsconfig.element.json",
|
|
37
37
|
"prepublishOnly": "npm run build",
|
|
38
|
-
"preversion": "./dev.sh check-version \"$npm_new_version\"",
|
|
39
38
|
"dev": "tsc --watch",
|
|
40
39
|
"example": "npm run build && node dist/cli.js render examples/arch.reladraw -o examples/out/arch.svg"
|
|
41
40
|
},
|