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 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.17.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.
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.17.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
- | `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:` |
113
- | `circle` | a circle sized to hold its text, always round |
114
- | `none` | no outline, fill or padding: bare text, or an invisible container |
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. A name says what a thing is, never what the picture looks like: `document`, not `folded-corner`. The exception is a picture with no single meaning, such as `circle` or `cube`.
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 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. A side of a circle is the quarter of it about that compass point, so `from: right` means the same on every shape; several line ends on one side spread round that arc, each meeting the circle square on.
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
- **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: a rectangle, a container, and a document's three round corners, whose fold stays as it is. A circle has none, so `corners:` written on a circle is refused; from a style or a default it lands only on the nodes with corners. A picture and bare text have no outline, so they refuse it too. Like a color, it never moves anything.
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 circle, a corner is the point on the circle halfway between its two sides.
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`; not on a circle. A line's go in `line: (…)` |
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 round = node.body.kind === 'shape' && node.body.outline === 'circle';
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, which is drawn in its node's square: walked a couple
253
- * of pixels at a time, a point counting as inside only past a pixel within the
254
- * outline, so a line ending on the circle doesn't cross it.
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 = { x: node.x + node.width / 2, y: node.y + node.height / 2 };
259
- const inside = (p) => Math.hypot(p.x - center.x, p.y - center.y) < radius;
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
- * Named for what a node *is*, never for the geometry, which is the same rule the
61
- * icon names follow: `document` and not `folded-corner`. A shape carrying a
62
- * conventional meaning is a second channel alongside color, and a stronger one
63
- * — a fill is whatever the author assigned and has to be learnt from the
64
- * diagram, while a folded corner has meant "an artifact, not a process" in
65
- * flowcharts for decades and reads with no legend at all.
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
- * `circle` is the one geometric name, because a circle carries no single
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
- * Named for what a node *is*, never for the geometry, which is the same rule the
123
- * icon names follow: `document` and not `folded-corner`. A shape carrying a
124
- * conventional meaning is a second channel alongside color, and a stronger one
125
- * — a fill is whatever the author assigned and has to be learnt from the
126
- * diagram, while a folded corner has meant "an artifact, not a process" in
127
- * flowcharts for decades and reads with no legend at all.
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
- * `circle` is the one geometric name, because a circle carries no single
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 = ['rectangle', 'document', 'circle'];
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
- return {
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 === 'circle',
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
  }