reladraw 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SYNTAX.md +58 -11
- package/dist/checks.js +30 -9
- package/dist/cli.js +8 -0
- package/dist/icons.d.ts +13 -13
- package/dist/icons.js +61 -15
- package/dist/lines.d.ts +8 -0
- package/dist/lines.js +50 -4
- package/dist/outline.d.ts +123 -0
- package/dist/outline.js +564 -0
- package/dist/parser.js +51 -1
- package/dist/place.d.ts +5 -0
- package/dist/place.js +44 -14
- package/dist/render.js +43 -18
- package/dist/resolve.js +43 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
86
86
|
|
|
87
87
|
## Status
|
|
88
88
|
|
|
89
|
-
Version 0.
|
|
89
|
+
Version 0.18.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.
|
|
90
90
|
|
|
91
91
|
The language isn't stable yet, so expect the syntax to change.
|
|
92
92
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.18.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
|
|
|
@@ -104,14 +104,44 @@ node <name> ["<text>" [(<text settings>)]] [<placement> | <attribute>] ...
|
|
|
104
104
|
|
|
105
105
|
Every node has one body, the thing drawn where it is, named by one of two keys.
|
|
106
106
|
|
|
107
|
-
`shape:` is the outline:
|
|
107
|
+
`shape:` is the outline. A shape geometry has a word for is named by it, and takes settings in a bracket after its name for what its box doesn't decide: an amount, in the file's unit, and a direction, whose first word below is the default.
|
|
108
|
+
|
|
109
|
+
| shape | draws | its bracket |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `rectangle` | the plain box, its corners rounded unless `corners: sharp`; the default | |
|
|
112
|
+
| `document` | the box with its top-right corner folded, for an artifact rather than a process; its other corners follow `corners:` | `fold:` |
|
|
113
|
+
| `circle` | a circle sized to hold its text; given a `width:` and a `height:` that differ, an ellipse | |
|
|
114
|
+
| `diamond` | a diamond, a flowchart's decision | |
|
|
115
|
+
| `pill` | a box with both ends round, a flowchart's start or end | |
|
|
116
|
+
| `parallelogram` | a slanted box, a flowchart's input or output | `slant:`, `lean: right \| left` |
|
|
117
|
+
| `hexagon` | a box with pointed ends, a flowchart's preparation | `point:` |
|
|
118
|
+
| `trapezoid` | a box narrower at one end, a flowchart's manual operation | `taper:`, `narrow: bottom \| top` |
|
|
119
|
+
| `triangle` | a triangle, a flowchart's merge (down) or extract (up) | `aim: down \| up \| left \| right` |
|
|
120
|
+
| `cylinder` | a cylinder standing, or `lying` on its side, for stored data | `cap:`, `lying` |
|
|
121
|
+
| `none` | no outline, fill or padding: bare text, or an invisible container | |
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
node in "read input" shape: parallelogram (slant: 2, lean: left)
|
|
125
|
+
node queue "queue" shape: cylinder (cap: 1.5, lying) right of in
|
|
126
|
+
node flat "valid?" shape: diamond width: 20 height: 5 below in
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A flowchart shape geometry has no word for is named `flow-` and what it means, and drawn the way the flowchart standard draws it, with no settings:
|
|
108
130
|
|
|
109
131
|
| shape | draws |
|
|
110
132
|
|---|---|
|
|
111
|
-
| `
|
|
112
|
-
| `document` |
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
133
|
+
| `flow-subprocess` | a step drawn as its own chart: a box with a bar inside each side |
|
|
134
|
+
| `flow-document` | a document as the standard draws it, its bottom wavy |
|
|
135
|
+
| `flow-off-page` | continued on another page: a box pointed at the bottom |
|
|
136
|
+
| `flow-delay` | a delay or a wait: a box with its right end round |
|
|
137
|
+
| `flow-manual-input` | typed in by hand: a box whose top slopes |
|
|
138
|
+
| `flow-stored-data` | the standard's other storage: curved ends, the right one hollow |
|
|
139
|
+
| `flow-loop-start`, `flow-loop-end` | the start and end of a loop: a box with two corners cut, at the top or the bottom |
|
|
140
|
+
| `flow-card` | a punched card: a box with its top-left corner cut |
|
|
141
|
+
| `flow-display` | a screen: pointed on the left, round on the right |
|
|
142
|
+
| `flow-tape` | magnetic tape: a circle with a tail |
|
|
143
|
+
| `flow-punched-tape` | paper tape: wavy top and bottom |
|
|
144
|
+
| `flow-collate` | collate: two triangles point to point, its text on a patch where they meet |
|
|
115
145
|
|
|
116
146
|
`icon:` is a picture the node is drawn *as*, with no box:
|
|
117
147
|
|
|
@@ -131,11 +161,15 @@ node aside "Written by a nightly cron job." (wrap: 30) shape: none below dump
|
|
|
131
161
|
node svc icon: cube right of dump
|
|
132
162
|
```
|
|
133
163
|
|
|
134
|
-
Writing both keys is an error naming both. An unknown name is refused with the whole set
|
|
164
|
+
Writing both keys is an error naming both. An unknown name is refused with the whole set, and so is a setting the shape doesn't take. A shape is named for how it looks, `diamond` and never `decision`, and no picture has two names. An icon is named for what it is: `package`, not `open-box`.
|
|
135
165
|
|
|
136
|
-
A
|
|
166
|
+
**Size and text.** A shape grows until its text, with its padding, fits at the shape's middle, the center of its area: a diamond is twice the padded text each way, and a triangle's text sits a third of the way from its base. A stated `width:` and `height:` stretch the shape to fill them, which is how a diamond is made flatter; a text that no longer fits is drawn as it is and reported as `text-overflow`. The text-position words of a [node's text](#where-it-sits), such as `(inside bottom)`, move it off the middle. `flow-collate`'s text sits on a patch of its own fill where its two triangles meet; `(fill: none)` in the text's bracket draws it without one.
|
|
137
167
|
|
|
138
|
-
**
|
|
168
|
+
**Sides.** A side of a shape is the stretch of its outline between the lines from its middle to two corners of its box: a rectangle's sides, a circle's quarters, and on a diamond the stretch round each point. So `from: right` leaves a diamond from its right point, as a flowchart's "no" does, and means the same on every shape. A corner such as `top-right` is where the line from the middle to that corner of the box meets the outline: on a circle halfway between its sides, on a diamond the middle of an edge. Several line ends on one side spread along it.
|
|
169
|
+
|
|
170
|
+
A picture can't contain anything. A `shape: none` node with children is an invisible container ([Containers](#containers)). A circle can hold other nodes, centered in it; the other shapes don't hold nodes yet, and one with children is refused.
|
|
171
|
+
|
|
172
|
+
**Corners.** `corners: sharp` draws a box's corners square, and `corners: rounded`, the default, draws them as they always were. It is the same word and values as a line's `corners:`. It reaches every outline with corners that round: a rectangle, a container, a `flow-subprocess`, and a document's three round corners, whose fold stays as it is. Every other shape is drawn with its points sharp, so `corners:` written on one is refused; from a style or a default it lands only on the nodes it can. A picture and bare text have no outline, so they refuse it too. Like a color, it never moves anything.
|
|
139
173
|
|
|
140
174
|
```
|
|
141
175
|
style plain corners: sharp
|
|
@@ -324,7 +358,7 @@ left-center center right-center
|
|
|
324
358
|
bottom-left bottom-center bottom-right
|
|
325
359
|
```
|
|
326
360
|
|
|
327
|
-
**A side is a segment and a point is a point.** Bare `top` is the whole side; `top-center` is the one point halfway along it, which is why every midpoint carries `-center`. Naming a side leaves the other axis free. On a
|
|
361
|
+
**A side is a segment and a point is a point.** Bare `top` is the whole side; `top-center` is the one point halfway along it, which is why every midpoint carries `-center`. Naming a side leaves the other axis free. On any shape but a box, a point is where the line from the shape's middle toward that point of its box meets the outline ([Sides](#the-body)): on a circle a corner is halfway between its two sides.
|
|
328
362
|
|
|
329
363
|
A part is written as the name, then the part: `b top`, `e bottom-left`, `hub text`.
|
|
330
364
|
|
|
@@ -588,6 +622,7 @@ node aside "Written nightly." (size: small, wrap: 30) shape: none
|
|
|
588
622
|
| `size` | `small`, `normal` or `large`; a multiple of normal (`1.2`); or pixels (`16px`) |
|
|
589
623
|
| `wrap` | a count of characters to fold at, on top of ` / ` |
|
|
590
624
|
| `align` | how the text's own lines range: `left`, `center`, `right`, or where along a side for a title (below) |
|
|
625
|
+
| `fill` | on a `flow-collate` only, the patch behind its text: a color, or `none` |
|
|
591
626
|
|
|
592
627
|
`small` is 21/25 of normal and `large` the same step the other way. The names are preferred: bundled into a style, a size says *why* one text is smaller. `wrap:` is a character count, not a distance: it is how a text is made narrow and tall to sit beside something.
|
|
593
628
|
|
|
@@ -1123,6 +1158,7 @@ reladraw check diagram.reladraw
|
|
|
1123
1158
|
| `--guides` | draw the guides |
|
|
1124
1159
|
| `--checks "<kind>: on, …"` | turn kinds of finding on or off, over the file |
|
|
1125
1160
|
| `--no-checks` | print no findings; errors still print |
|
|
1161
|
+
| `-v`, `--version` | print the version |
|
|
1126
1162
|
|
|
1127
1163
|
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.
|
|
1128
1164
|
|
|
@@ -1138,7 +1174,7 @@ Every attribute, and what takes one. A node's kind is what its body is: a box (`
|
|
|
1138
1174
|
| `badge` | ✓ | | | | a picture joined to the text |
|
|
1139
1175
|
| `cards` | ✓ | | | | a deck's cards, when they are only texts, front first |
|
|
1140
1176
|
| `fill`, `border` | ✓ | | | | colors |
|
|
1141
|
-
| `corners` | ✓ | | | | `sharp` or `rounded`;
|
|
1177
|
+
| `corners` | ✓ | | | | `sharp` or `rounded`; only on a box's outline. A line's go in `line: (…)` |
|
|
1142
1178
|
| `text` | ✓ | ✓ | ✓ | ✓ | in a style only; on a statement, the bracket after the text |
|
|
1143
1179
|
| `layout` | ✓ | | ✓ | | how the children are arranged |
|
|
1144
1180
|
| `gather-children` | ✓ | | ✓ | | where the children sit in spare room |
|
|
@@ -1192,6 +1228,17 @@ Designed and absent from the code.
|
|
|
1192
1228
|
|
|
1193
1229
|
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.
|
|
1194
1230
|
|
|
1231
|
+
**0.18.0**
|
|
1232
|
+
|
|
1233
|
+
- **More shapes, for flowcharts and the rest** (#18): `diamond`, `pill`, `parallelogram`, `hexagon`, `trapezoid`, `triangle` and `cylinder`, named for how they look, and the flowchart standard's shapes that have no geometric name as `flow-` and what they mean, from `flow-subprocess` and `flow-delay` to `flow-collate`. A shape's settings go in a bracket after its name: `shape: parallelogram (slant: 2, lean: left)`, `shape: cylinder (lying)`, `shape: document (fold: 1.5)`.
|
|
1234
|
+
- **A shape grows to hold its text at its middle**, and a stated `width:` and `height:` stretch it. Its sides are the stretches of outline round its points, so `from: right` leaves a diamond from its right point.
|
|
1235
|
+
- **A circle given a `width:` and a `height:` that differ is an ellipse.** It used to stay round and say nothing about the height.
|
|
1236
|
+
|
|
1237
|
+
**0.17.1**
|
|
1238
|
+
|
|
1239
|
+
- **`reladraw --version`**, or `-v`, prints the version.
|
|
1240
|
+
- **The playground waits for a pause in typing** before it redraws a diagram that is slow to draw, so the keystrokes aren't held up behind it. A quick one still redraws on every keystroke. Suggested in #6.
|
|
1241
|
+
|
|
1195
1242
|
**0.17.0**
|
|
1196
1243
|
|
|
1197
1244
|
The redesign. The tool no longer decides anything the file doesn't say: a gap is exact, nothing moves a box to make room, lines are straight unless told and go only where they are told, and anything that may not read as meant is drawn as written and reported as a finding. Almost every diagram written for 0.16.0 needs changes; the table below gives each old spelling and what to write instead.
|
package/dist/checks.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { isWithin, joinedToText } from './model.js';
|
|
11
11
|
import { ARROW_MARKER_WIDTH, LINE_WIDTH, UNIT, arrowLength } from './constants.js';
|
|
12
|
-
import { crossingPoints, describeEdge, planSides, traceRoute, traceVisible } from './lines.js';
|
|
12
|
+
import { centerOf, crossingPoints, describeEdge, faceOfNode, planSides, traceRoute, traceVisible } from './lines.js';
|
|
13
|
+
import { clearOf, insideOutline, sizeFor } from './outline.js';
|
|
13
14
|
import { placeLineTexts } from './linetexts.js';
|
|
14
15
|
/**
|
|
15
16
|
* The kinds of finding, each with whether it is on when nothing says. Named in
|
|
@@ -135,8 +136,8 @@ function edgeNodeOverlaps(layout, routes) {
|
|
|
135
136
|
if (holdsEnd(node, edge))
|
|
136
137
|
continue;
|
|
137
138
|
const inner = { x: node.x + 1, y: node.y + 1, width: node.width - 2, height: node.height - 2 };
|
|
138
|
-
const
|
|
139
|
-
const through = round ? crossingCircle(trace, node) : crossing(trace, inner);
|
|
139
|
+
const face = faceOfNode(node);
|
|
140
|
+
const through = face.round || face.outline ? crossingCircle(trace, node, face) : crossing(trace, inner);
|
|
140
141
|
if (through === undefined)
|
|
141
142
|
continue;
|
|
142
143
|
const own = node === edge.from || node === edge.to;
|
|
@@ -249,14 +250,18 @@ function crossing(trace, box) {
|
|
|
249
250
|
return { enters, leaves };
|
|
250
251
|
}
|
|
251
252
|
/**
|
|
252
|
-
* The same for a circle,
|
|
253
|
-
* of pixels at a time, a point counting as
|
|
254
|
-
* outline, so a line ending on
|
|
253
|
+
* The same for a circle, or any outline but a rectangle's, which is drawn in
|
|
254
|
+
* its node's box: walked a couple of pixels at a time, a point counting as
|
|
255
|
+
* inside only past a pixel within the outline, so a line ending on it doesn't
|
|
256
|
+
* cross it.
|
|
255
257
|
*/
|
|
256
|
-
function crossingCircle(trace, node) {
|
|
258
|
+
function crossingCircle(trace, node, face) {
|
|
257
259
|
const radius = node.width / 2 - 1;
|
|
258
|
-
const center =
|
|
259
|
-
const
|
|
260
|
+
const center = centerOf(face);
|
|
261
|
+
const polygon = face.outline;
|
|
262
|
+
const inside = polygon
|
|
263
|
+
? (p) => insideOutline(polygon, p) && clearOf(polygon, p, 1)
|
|
264
|
+
: (p) => Math.hypot(p.x - center.x, p.y - center.y) < radius;
|
|
260
265
|
const sideAt = (p) => {
|
|
261
266
|
const dx = p.x - center.x;
|
|
262
267
|
const dy = p.y - center.y;
|
|
@@ -564,6 +569,22 @@ function textOverflows(layout) {
|
|
|
564
569
|
const place = node.textAttrs['place'];
|
|
565
570
|
if (place !== undefined && place !== 'center' && !place.startsWith('inside'))
|
|
566
571
|
continue;
|
|
572
|
+
const face = faceOfNode(node);
|
|
573
|
+
if (face.outline && node.body.outline !== 'flow-collate') {
|
|
574
|
+
// Any other outline than a box's: the text spills where it passes the outline.
|
|
575
|
+
const { outline, look } = node.body;
|
|
576
|
+
const corners = [0, 0.5, 1].flatMap((fx) => [0, 0.5, 1].map((fy) => ({ x: node.x + text.x + text.width * fx, y: node.y + text.y + text.height * fy })));
|
|
577
|
+
if (corners.every((p) => insideOutline(face.outline, p)))
|
|
578
|
+
continue;
|
|
579
|
+
const needs = sizeFor(outline, look, text.width, text.height);
|
|
580
|
+
const wider = Math.max(0, needs.width - node.width);
|
|
581
|
+
const taller = Math.max(0, needs.height - node.height);
|
|
582
|
+
const more = [wider > EPSILON ? `${units(wider)} more width` : '', taller > EPSILON ? `${units(taller)} more height` : ''].filter((part) => part.length > 0);
|
|
583
|
+
found.push(new Finding('text-overflow', `the text of node ${node.name} runs past the outline of its ${outline}` + (more.length > 0 ? `; it needs ${more.join(' and ')}` : ''), node.line));
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (node.body.outline === 'flow-collate')
|
|
587
|
+
continue;
|
|
567
588
|
const spill = {
|
|
568
589
|
left: Math.max(0, -text.x),
|
|
569
590
|
right: Math.max(0, text.x + text.width - node.width),
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
25
25
|
--checks turn kinds of finding on or off, over what the file says:
|
|
26
26
|
--checks "edge-crossings: on, node-overlap: off"
|
|
27
27
|
--no-checks print no findings. Errors still print.
|
|
28
|
+
-v, --version
|
|
29
|
+
print the version.
|
|
28
30
|
-h, --help print this.
|
|
29
31
|
`;
|
|
30
32
|
const COMMANDS = ['render', 'check'];
|
|
@@ -33,6 +35,12 @@ async function main(argv) {
|
|
|
33
35
|
process.stdout.write(USAGE);
|
|
34
36
|
return argv.length === 0 ? 1 : 0;
|
|
35
37
|
}
|
|
38
|
+
if (argv.includes('-v') || argv.includes('--version')) {
|
|
39
|
+
// package.json ships beside dist/, so the number is the installed one.
|
|
40
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
41
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
36
44
|
const verb = argv[0];
|
|
37
45
|
if (!COMMANDS.includes(verb)) {
|
|
38
46
|
// A file where the command goes is the old form, and is told the new one;
|
package/dist/icons.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* which caps the useful vocabulary at something that fits in a head. Add a name
|
|
19
19
|
* when a diagram asks for a distinction it cannot otherwise make.
|
|
20
20
|
*/
|
|
21
|
+
import { type ShapeLook } from './outline.js';
|
|
21
22
|
/**
|
|
22
23
|
* Icons carry three tones rather than colors. `ink` is the drawn line, `shade`
|
|
23
24
|
* the body it encloses, and `void` is the page showing through — the same
|
|
@@ -57,23 +58,21 @@ export declare const ICON_NAMES: string[];
|
|
|
57
58
|
* The outlines a node's body can take. `rectangle` is the plain one and needs no
|
|
58
59
|
* word, since it is what a node is when it says nothing.
|
|
59
60
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* A shape geometry has a word for is named by that word, how it looks:
|
|
62
|
+
* `diamond`, never `decision`, since someone may just want a diamond, and
|
|
63
|
+
* reading `decision` as a diamond is a translation every reader has to make.
|
|
64
|
+
* Those take settings in a bracket for what the box doesn't decide. A
|
|
65
|
+
* flowchart shape geometry has no word for is named `flow-` and what it means,
|
|
66
|
+
* drawn the way the standard draws it, with no settings. No picture has two
|
|
67
|
+
* names. `document`, the folded corner, is older than the rule and keeps its
|
|
68
|
+
* name; the standard's wavy one is `flow-document`.
|
|
66
69
|
*
|
|
67
|
-
*
|
|
68
|
-
* meaning to name it after — it is a state, a step, a person, whatever the
|
|
69
|
-
* diagram's own convention makes it. `diamond` joins these when a diagram
|
|
70
|
-
* asks. The set being short is a fact about what has been drawn, not about the
|
|
71
|
-
* key.
|
|
70
|
+
* The geometry of each is in `outline.ts`.
|
|
72
71
|
*/
|
|
73
|
-
export declare const OUTLINES: readonly ["rectangle", "document", "circle"];
|
|
72
|
+
export declare const OUTLINES: readonly ["rectangle", "document", "circle", "diamond", "pill", "parallelogram", "hexagon", "trapezoid", "triangle", "cylinder", "flow-subprocess", "flow-document", "flow-off-page", "flow-delay", "flow-manual-input", "flow-stored-data", "flow-loop-start", "flow-loop-end", "flow-card", "flow-display", "flow-tape", "flow-punched-tape", "flow-collate"];
|
|
74
73
|
export type Outline = (typeof OUTLINES)[number];
|
|
75
74
|
/** Every value `shape:` accepts, `none` included. */
|
|
76
|
-
export declare const SHAPE_WORDS: readonly ["rectangle", "document", "circle", "none"];
|
|
75
|
+
export declare const SHAPE_WORDS: readonly ["rectangle", "document", "circle", "diamond", "pill", "parallelogram", "hexagon", "trapezoid", "triangle", "cylinder", "flow-subprocess", "flow-document", "flow-off-page", "flow-delay", "flow-manual-input", "flow-stored-data", "flow-loop-start", "flow-loop-end", "flow-card", "flow-display", "flow-tape", "flow-punched-tape", "flow-collate", "none"];
|
|
77
76
|
/**
|
|
78
77
|
* What a node is drawn as: an outline, a picture, or nothing at all.
|
|
79
78
|
*
|
|
@@ -88,6 +87,7 @@ export declare const SHAPE_WORDS: readonly ["rectangle", "document", "circle", "
|
|
|
88
87
|
export type Body = {
|
|
89
88
|
kind: 'shape';
|
|
90
89
|
outline: Outline;
|
|
90
|
+
look: ShapeLook;
|
|
91
91
|
} | {
|
|
92
92
|
kind: 'icon';
|
|
93
93
|
icon: Icon;
|
package/dist/icons.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* when a diagram asks for a distinction it cannot otherwise make.
|
|
20
20
|
*/
|
|
21
21
|
import { SourceError } from './errors.js';
|
|
22
|
+
import { readShapeLook } from './outline.js';
|
|
22
23
|
const GRID = 24;
|
|
23
24
|
/** Line width on the 24-unit grid, scaled with everything else. */
|
|
24
25
|
export const ICON_STROKE = 1.1;
|
|
@@ -119,23 +120,45 @@ export const ICON_NAMES = Object.keys(ICONS);
|
|
|
119
120
|
* The outlines a node's body can take. `rectangle` is the plain one and needs no
|
|
120
121
|
* word, since it is what a node is when it says nothing.
|
|
121
122
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* A shape geometry has a word for is named by that word, how it looks:
|
|
124
|
+
* `diamond`, never `decision`, since someone may just want a diamond, and
|
|
125
|
+
* reading `decision` as a diamond is a translation every reader has to make.
|
|
126
|
+
* Those take settings in a bracket for what the box doesn't decide. A
|
|
127
|
+
* flowchart shape geometry has no word for is named `flow-` and what it means,
|
|
128
|
+
* drawn the way the standard draws it, with no settings. No picture has two
|
|
129
|
+
* names. `document`, the folded corner, is older than the rule and keeps its
|
|
130
|
+
* name; the standard's wavy one is `flow-document`.
|
|
128
131
|
*
|
|
129
|
-
*
|
|
130
|
-
* meaning to name it after — it is a state, a step, a person, whatever the
|
|
131
|
-
* diagram's own convention makes it. `diamond` joins these when a diagram
|
|
132
|
-
* asks. The set being short is a fact about what has been drawn, not about the
|
|
133
|
-
* key.
|
|
132
|
+
* The geometry of each is in `outline.ts`.
|
|
134
133
|
*/
|
|
135
|
-
export const OUTLINES = [
|
|
134
|
+
export const OUTLINES = [
|
|
135
|
+
'rectangle',
|
|
136
|
+
'document',
|
|
137
|
+
'circle',
|
|
138
|
+
'diamond',
|
|
139
|
+
'pill',
|
|
140
|
+
'parallelogram',
|
|
141
|
+
'hexagon',
|
|
142
|
+
'trapezoid',
|
|
143
|
+
'triangle',
|
|
144
|
+
'cylinder',
|
|
145
|
+
'flow-subprocess',
|
|
146
|
+
'flow-document',
|
|
147
|
+
'flow-off-page',
|
|
148
|
+
'flow-delay',
|
|
149
|
+
'flow-manual-input',
|
|
150
|
+
'flow-stored-data',
|
|
151
|
+
'flow-loop-start',
|
|
152
|
+
'flow-loop-end',
|
|
153
|
+
'flow-card',
|
|
154
|
+
'flow-display',
|
|
155
|
+
'flow-tape',
|
|
156
|
+
'flow-punched-tape',
|
|
157
|
+
'flow-collate',
|
|
158
|
+
];
|
|
136
159
|
/** Every value `shape:` accepts, `none` included. */
|
|
137
160
|
export const SHAPE_WORDS = [...OUTLINES, 'none'];
|
|
138
|
-
const PLAIN = { kind: 'shape', outline: 'rectangle' };
|
|
161
|
+
const PLAIN = { kind: 'shape', outline: 'rectangle', look: {} };
|
|
139
162
|
/**
|
|
140
163
|
* Which body a node has, from what it wrote and what its style carried.
|
|
141
164
|
*
|
|
@@ -158,14 +181,37 @@ export function bodyFor(attrs, appearance, line, declared = new Map()) {
|
|
|
158
181
|
throw new SourceError(`shape: ${shape} and icon: ${icon} both say what this node is drawn as, and it has one ` +
|
|
159
182
|
'body — `shape:` is the outline it is drawn with, `icon:` is the picture it is drawn as', line);
|
|
160
183
|
}
|
|
184
|
+
// The settings come from wherever the shape did: a node's own `shape:` takes
|
|
185
|
+
// its own bracket, never a style's.
|
|
186
|
+
const from = wroteShape ? attrs : appearance;
|
|
187
|
+
const settings = {};
|
|
188
|
+
for (const [key, value] of Object.entries(from))
|
|
189
|
+
if (key.startsWith('shape.'))
|
|
190
|
+
settings[key.slice('shape.'.length)] = value;
|
|
161
191
|
if (icon !== undefined)
|
|
162
192
|
return { kind: 'icon', icon: iconNamed(icon, line, declared) };
|
|
163
193
|
if (shape === undefined)
|
|
164
194
|
return PLAIN;
|
|
165
|
-
if (shape === 'none')
|
|
195
|
+
if (shape === 'none') {
|
|
196
|
+
if (Object.keys(settings).length > 0)
|
|
197
|
+
refuse(`\`shape: none\` draws no outline, so it takes no settings`, line);
|
|
166
198
|
return { kind: 'none' };
|
|
199
|
+
}
|
|
167
200
|
if (OUTLINES.includes(shape)) {
|
|
168
|
-
return { kind: 'shape', outline: shape };
|
|
201
|
+
return { kind: 'shape', outline: shape, look: readShapeLook(shape, settings, `shape: ${shape}`, line) };
|
|
202
|
+
}
|
|
203
|
+
if (shape === 'hourglass') {
|
|
204
|
+
return refuse('`shape: hourglass` is the flowchart\'s collate symbol, and it is `flow-collate`', line);
|
|
205
|
+
}
|
|
206
|
+
if (shape === 'pentagon' || shape === 'pointed' || shape === 'home-plate') {
|
|
207
|
+
return refuse(`there is no shape "${shape}" — the flowchart's off-page connector, a box with one end pointed, is \`flow-off-page\``, line);
|
|
208
|
+
}
|
|
209
|
+
if (shape === 'ellipse' || shape === 'oval') {
|
|
210
|
+
return refuse(`there is no shape "${shape}" — an ellipse is a circle given a width and a height: \`shape: circle width: 20 height: 8\``, line);
|
|
211
|
+
}
|
|
212
|
+
if (shape === 'decision' || shape === 'terminal' || shape === 'process') {
|
|
213
|
+
const look = { decision: 'diamond', terminal: 'pill', process: 'rectangle' }[shape];
|
|
214
|
+
return refuse(`a shape is named for how it looks, so a ${shape} is \`shape: ${look}\``, line);
|
|
169
215
|
}
|
|
170
216
|
// Until 0.3.0 `shape:` also took an icon name and drew the node as that
|
|
171
217
|
// picture. The two jobs are two parts and now have two keys, so an older file
|
package/dist/lines.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface Face {
|
|
|
11
11
|
height: number;
|
|
12
12
|
/** A circle is drawn in this square, so a line meets the circle. */
|
|
13
13
|
round: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Any other outline but a rectangle's, as a polygon on the page, with the
|
|
16
|
+
* shape's middle: a line meets the polygon, and the lines from the middle
|
|
17
|
+
* to the box's corners cut it into its four sides.
|
|
18
|
+
*/
|
|
19
|
+
outline?: Point[];
|
|
20
|
+
middle?: Point;
|
|
14
21
|
}
|
|
15
22
|
export type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
16
23
|
/** One piece of a drawn line, from where the last one ended: straight, or a cubic. */
|
|
@@ -69,6 +76,7 @@ export declare function planSides(layout: Layout): {
|
|
|
69
76
|
export declare function faceOfNode(node: LayoutNode): Face;
|
|
70
77
|
/** The point a share of the way along a polyline, bends included, or its start or end. */
|
|
71
78
|
export declare function alongLength(trace: Point[], at: number | 'start' | 'end'): Point;
|
|
79
|
+
/** A face's middle: its box's, or a shape's own, which is a triangle's a third of the way from its base. */
|
|
72
80
|
export declare function centerOf(face: Face): Point;
|
|
73
81
|
/** The points with repeats and points in line with both neighbors dropped. */
|
|
74
82
|
export declare function tidyCorners(points: Point[]): Point[];
|
package/dist/lines.js
CHANGED
|
@@ -16,6 +16,7 @@ import { GAPS } from './constants.js';
|
|
|
16
16
|
import { EDGE_ORDER_KEYS, PART_SIDES, WAYS, isPosition } from './ast.js';
|
|
17
17
|
import { SourceError } from './errors.js';
|
|
18
18
|
import { gapPixels } from './place.js';
|
|
19
|
+
import { isBoxy, isRound, lineCrossings, middleOf, rayExit, shapeOf, sideOfPoint, sideStretch, towardOutline } from './outline.js';
|
|
19
20
|
/**
|
|
20
21
|
* How far apart copies of one line run, like lanes on a road (6.5). A fixed
|
|
21
22
|
* step, so adding a copy widens the set by one lane and moves no other lane.
|
|
@@ -464,6 +465,9 @@ function meetOutline(face, at, way) {
|
|
|
464
465
|
if (disc >= 0)
|
|
465
466
|
hits.push(-b - Math.sqrt(disc), -b + Math.sqrt(disc));
|
|
466
467
|
}
|
|
468
|
+
else if (face.outline) {
|
|
469
|
+
hits.push(...lineCrossings(face.outline, at, way));
|
|
470
|
+
}
|
|
467
471
|
else {
|
|
468
472
|
const within = (value, low, size) => value >= low - 0.5 && value <= low + size + 0.5;
|
|
469
473
|
for (const x of [face.x, face.x + face.width]) {
|
|
@@ -497,6 +501,10 @@ function leaving(face, from, way) {
|
|
|
497
501
|
const t = -b + Math.sqrt(Math.max(0, b * b - (fx * fx + fy * fy - r * r)));
|
|
498
502
|
return { x: from.x + way.x * t, y: from.y + way.y * t };
|
|
499
503
|
}
|
|
504
|
+
if (face.outline) {
|
|
505
|
+
const t = rayExit(face.outline, from, way) ?? 0;
|
|
506
|
+
return { x: from.x + way.x * t, y: from.y + way.y * t };
|
|
507
|
+
}
|
|
500
508
|
const tx = way.x > 1e-9 ? (face.x + face.width - from.x) / way.x : way.x < -1e-9 ? (face.x - from.x) / way.x : Infinity;
|
|
501
509
|
const ty = way.y > 1e-9 ? (face.y + face.height - from.y) / way.y : way.y < -1e-9 ? (face.y - from.y) / way.y : Infinity;
|
|
502
510
|
const t = Math.max(0, Math.min(tx, ty));
|
|
@@ -576,13 +584,20 @@ const WAY_VECTOR = {
|
|
|
576
584
|
const MET_SIDE = { down: 'top', up: 'bottom', left: 'right', right: 'left' };
|
|
577
585
|
/** The drawn rectangle of a node. */
|
|
578
586
|
export function faceOfNode(node) {
|
|
579
|
-
|
|
587
|
+
const face = {
|
|
580
588
|
x: node.x,
|
|
581
589
|
y: node.y,
|
|
582
590
|
width: node.width,
|
|
583
591
|
height: node.height,
|
|
584
|
-
round: node.body.kind === 'shape' && node.body.outline
|
|
592
|
+
round: node.body.kind === 'shape' && isRound(node.body.outline, node.width, node.height),
|
|
585
593
|
};
|
|
594
|
+
if (node.body.kind === 'shape' && !isBoxy(node.body.outline) && !face.round) {
|
|
595
|
+
const { outline, look } = node.body;
|
|
596
|
+
face.outline = shapeOf(outline, look, node.width, node.height).polygon.map((p) => ({ x: node.x + p.x, y: node.y + p.y }));
|
|
597
|
+
const middle = middleOf(outline, look, node.width, node.height);
|
|
598
|
+
face.middle = { x: node.x + middle.x, y: node.y + middle.y };
|
|
599
|
+
}
|
|
600
|
+
return face;
|
|
586
601
|
}
|
|
587
602
|
function endOf(edge, which) {
|
|
588
603
|
const node = which === 'from' ? edge.from : edge.to;
|
|
@@ -1027,8 +1042,9 @@ function distance(a, b) {
|
|
|
1027
1042
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
1028
1043
|
}
|
|
1029
1044
|
// --- points on a box ------------------------------------------------------------
|
|
1045
|
+
/** A face's middle: its box's, or a shape's own, which is a triangle's a third of the way from its base. */
|
|
1030
1046
|
export function centerOf(face) {
|
|
1031
|
-
return { x: face.x + face.width / 2, y: face.y + face.height / 2 };
|
|
1047
|
+
return face.middle ?? { x: face.x + face.width / 2, y: face.y + face.height / 2 };
|
|
1032
1048
|
}
|
|
1033
1049
|
/** The sides a position sits on: `bottom-right` is on the bottom and the right, `top-center` on the top only. */
|
|
1034
1050
|
function cornerSides(position) {
|
|
@@ -1046,9 +1062,12 @@ function positionPoint(face, position) {
|
|
|
1046
1062
|
x: horizontal === 'left' ? face.x : horizontal === 'right' ? face.x + face.width : face.x + face.width / 2,
|
|
1047
1063
|
y: vertical === 'top' ? face.y : vertical === 'bottom' ? face.y + face.height : face.y + face.height / 2,
|
|
1048
1064
|
};
|
|
1049
|
-
// A circle has no corners; its corner is where the diagonal meets it.
|
|
1065
|
+
// A circle has no corners; its corner is where the diagonal meets it. On
|
|
1066
|
+
// another shape every point is where the line from its middle meets it.
|
|
1050
1067
|
if (face.round && vertical && horizontal)
|
|
1051
1068
|
return outlineToward(face, point);
|
|
1069
|
+
if (face.outline)
|
|
1070
|
+
return outlineToward(face, point);
|
|
1052
1071
|
return point;
|
|
1053
1072
|
}
|
|
1054
1073
|
/** A point a fraction of the way along a side, from its left or top end. */
|
|
@@ -1066,6 +1085,29 @@ function onSide(face, side, along) {
|
|
|
1066
1085
|
const angle = { right: turn, bottom: Math.PI / 2 - turn, left: Math.PI - turn, top: -Math.PI / 2 + turn }[side];
|
|
1067
1086
|
return { x: center.x + radius * Math.cos(angle), y: center.y + radius * Math.sin(angle) };
|
|
1068
1087
|
}
|
|
1088
|
+
if (face.outline) {
|
|
1089
|
+
// The side is the stretch of outline between the lines from the middle to
|
|
1090
|
+
// its two corners. `along` lands where the stretch passes it, so a line
|
|
1091
|
+
// down from the middle of one box's bottom meets the next square on; one
|
|
1092
|
+
// past the stretch's ends is placed on it in proportion instead.
|
|
1093
|
+
const stretch = sideStretch(face.outline, centerOf(face), face, side);
|
|
1094
|
+
const key = side === 'top' || side === 'bottom' ? 'x' : 'y';
|
|
1095
|
+
const low = key === 'x' ? face.x : face.y;
|
|
1096
|
+
const size = key === 'x' ? face.width : face.height;
|
|
1097
|
+
const values = stretch.map((p) => p[key]);
|
|
1098
|
+
const lo = Math.min(...values);
|
|
1099
|
+
const hi = Math.max(...values);
|
|
1100
|
+
const target = along >= lo && along <= hi ? along : lo + ((along - low) / size) * (hi - lo);
|
|
1101
|
+
for (let i = 1; i < stretch.length; i++) {
|
|
1102
|
+
const a = stretch[i - 1];
|
|
1103
|
+
const b = stretch[i];
|
|
1104
|
+
if ((a[key] - target) * (b[key] - target) <= 0 && a[key] !== b[key]) {
|
|
1105
|
+
const f = (target - a[key]) / (b[key] - a[key]);
|
|
1106
|
+
return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return Math.abs(stretch[0][key] - target) < Math.abs(stretch[stretch.length - 1][key] - target) ? stretch[0] : stretch[stretch.length - 1];
|
|
1110
|
+
}
|
|
1069
1111
|
switch (side) {
|
|
1070
1112
|
case 'top':
|
|
1071
1113
|
return { x: along, y: face.y };
|
|
@@ -1089,6 +1131,8 @@ function outlineToward(face, target) {
|
|
|
1089
1131
|
const length = Math.hypot(dx, dy);
|
|
1090
1132
|
return { x: center.x + (dx / length) * radius, y: center.y + (dy / length) * radius };
|
|
1091
1133
|
}
|
|
1134
|
+
if (face.outline)
|
|
1135
|
+
return towardOutline(face.outline, center, target);
|
|
1092
1136
|
const scale = Math.min(dx === 0 ? Infinity : face.width / 2 / Math.abs(dx), dy === 0 ? Infinity : face.height / 2 / Math.abs(dy));
|
|
1093
1137
|
return { x: center.x + dx * scale, y: center.y + dy * scale };
|
|
1094
1138
|
}
|
|
@@ -1106,6 +1150,8 @@ function headingAt(face, at) {
|
|
|
1106
1150
|
const dy = at.y - center.y;
|
|
1107
1151
|
return Math.abs(dx) >= Math.abs(dy) ? SIDE_NORMAL[dx >= 0 ? 'right' : 'left'] : SIDE_NORMAL[dy >= 0 ? 'bottom' : 'top'];
|
|
1108
1152
|
}
|
|
1153
|
+
if (face.outline)
|
|
1154
|
+
return SIDE_NORMAL[sideOfPoint(centerOf(face), face, at)];
|
|
1109
1155
|
distances.sort((a, b) => a[0] - b[0]);
|
|
1110
1156
|
return SIDE_NORMAL[distances[0][1]];
|
|
1111
1157
|
}
|