reladraw 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,26 @@
1
1
  # reladraw
2
2
 
3
- A text language for diagrams where **you say where things go**.
3
+ reladraw is a text language for diagrams where you say where things go.
4
4
 
5
- **[Try it in your browser →](https://reladraw.github.io/reladraw/)** — edit the source on the left, watch the layout re-solve on the right. Nothing to install.
5
+ **[Try it in your browser →](https://reladraw.github.io/reladraw/)**
6
6
 
7
- A diagram drawn by hand in draw.io:
7
+ I wanted to be able to create custom, expressive diagrams where I decided how to arrange the diagram, but without the inefficiency of manually drawing in draw.io. For example, this is a diagram drawn in draw.io:
8
8
 
9
- ![The reference diagram, drawn by hand](https://raw.githubusercontent.com/reladraw/reladraw/main/examples/reference/arch.png)
9
+ ![Hand-drawn reference diagram](https://raw.githubusercontent.com/reladraw/reladraw/main/examples/reference/arch.png)
10
10
 
11
- And the same diagram written down in reladraw and rendered from the text — [`examples/arch.reladraw`](examples/arch.reladraw), 44 statements, no coordinates anywhere in it:
11
+ This is the same diagram, but written in reladraw ([`examples/arch.reladraw`](examples/arch.reladraw)).
12
12
 
13
- ![The same diagram, rendered from reladraw source](https://raw.githubusercontent.com/reladraw/reladraw/main/docs/arch-render.png)
13
+ ![reference diagram via reladraw](https://raw.githubusercontent.com/reladraw/reladraw/main/docs/arch-render.png)
14
14
 
15
- Every distance in the second picture was worked out from statements like `above-left of cluster.hub` and `between cluster.desktop1 and cluster.laptop1`. Nothing chose the arrangement; the file states it.
15
+ ## Why not Mermaid or draw.io?
16
16
 
17
- ## The gap
17
+ Mermaid, Graphviz and D2 let you declare boxes and connections, then determine positions for you. If you have a particular picture in mind, these aren't the right tool.
18
18
 
19
- Mermaid, Graphviz and D2 have you declare entities and connections, and then place everything for you. That is a superpower, and for most diagrams it is the right one. It stops being the right one as soon as you have a particular picture in mind and care where things go. Say you are actively building your understanding of a system by diagramming it, and you want some module over to the right with its connections placed just so: the auto-layout languages have no way to say it.
20
-
21
- On the other end of the spectrum are the absolute-positioning tools — draw.io, Excalidraw, Figma. They give you total control of placement, at the cost of making every edit to a complex diagram slow hand-work. And it is slow for a human but expensive for an agent, which has to work the picture out from the coordinates before it can decide which ones to change.
19
+ On the other hand, tools like draw.io or Excalidraw allow absolute placement, but that means much more effort, whether for humans clicking and dragging nodes around or agents recalculating coordinates and editing verbose XML source code files.
22
20
 
23
21
  ![The two ends of the spectrum, with reladraw between them](https://raw.githubusercontent.com/reladraw/reladraw/main/docs/gap.png)
24
22
 
25
- reladraw aims at the middle. Every position is stated relative to something else, and nothing in the file is a coordinate:
23
+ reladraw sits between those two extremes, aiming to have the benefits of a diagram language, like Mermaid, but also having the expressiveness and custom placement that you can get with draw.io. Positions are relative, so you don't have to manually pick coordinates. For example:
26
24
 
27
25
  ```
28
26
  node app "Web app"
@@ -34,41 +32,9 @@ node store "Database" right of app level with app
34
32
  edge app.api -> store "queries" from: right to: left
35
33
  ```
36
34
 
37
- Nothing is nested, so no line depends on another line's position or indentation.
38
-
39
- The draft is in [SYNTAX.md](SYNTAX.md), with a worked example in [examples/](examples/).
40
-
41
- ## Why an agent needs this
42
-
43
- The common case is not drawing a diagram, it is changing one. Ask for the auth service to move left and a queue to go behind it. With pixel coordinates, an agent has to rebuild the picture from the numbers before it can work out which numbers to change. With auto-layout there is nothing to read at all, because the arrangement was never written down — it can only reword the source and re-render. With stated placement the arrangement is in the file as sentences, and changing the picture is changing the sentence that says where the thing goes.
44
-
45
- Writing has the same shape. An agent emitting Mermaid is guessing at a layout that an algorithm settles later, and its only way to find out is to render and look — a round trip that comes back as a picture rather than as a list of what is wrong.
46
-
47
- Intent is confirmable, outcomes are not, and the difference is worth being precise about. An agent can re-read its own file and see that the database is under the API and all four machines hang off the sync hub. It cannot see that two clusters anchored to different things now overlap, that a text overflowed its node, or that an edge crosses four others — those are resolved from the statements rather than stated, so they need the diagnostics in the scope section below.
48
-
49
- ### Using it with an agent
50
-
51
- reladraw is too new to be in any model's training data, so an agent has to be told the language before it can write it. This repository ships an [agent skill](https://agentskills.io) that does exactly that — the syntax, when to reach for the language, and what re-reading its own source can and cannot confirm.
52
-
53
- ```
54
- npx skills add reladraw/reladraw -g
55
- ```
56
-
57
- That installs it for whichever agent you use — Claude Code, Codex, Cursor, Copilot and others — each into its own skills directory. Drop the `-g` to install it into the current project instead.
58
-
59
- To install it for one agent rather than all of them, name it with `-a`:
60
-
61
- ```
62
- npx skills add reladraw/reladraw -g -a claude-code
63
- ```
64
-
65
- Re-run whichever command you used after a release that changes the syntax. The skill is a copy taken at install time, not a live link, so nothing refreshes it on its own.
66
-
67
- It is [plain Markdown](.claude/skills/reladraw/SKILL.md) with the syntax reference beside it, so it is worth reading whatever you use, and copying the directory by hand works just as well.
68
-
69
- ## Status
35
+ See also [SYNTAX.md](SYNTAX.md) and [examples/](examples/).
70
36
 
71
- Version 0.7.1. Early, but it runs: a parser, resolver and SVG renderer in TypeScript with no runtime dependencies, and a command-line tool that takes a text file and writes a standalone SVG. The comparison at the top of this page is that pipeline run on [`examples/arch.reladraw`](examples/arch.reladraw). What is still visibly off there is typography, not placement.
37
+ ## Install
72
38
 
73
39
  ```
74
40
  npm install -g reladraw
@@ -82,65 +48,32 @@ npm install && npm run build
82
48
  node dist/cli.js examples/arch.reladraw -o out.svg
83
49
  ```
84
50
 
85
- Not built yet, roughly in the order they are missed:
51
+ ## Using it with an agent
86
52
 
87
- - **The diagnostics report.** The scope section below says what it is for. Today the tool either renders or fails; it will not tell you what is wrong with a picture it drew successfully.
88
- - **Edge routing around nodes.** An edge can be told which side of a node to leave and arrive on, and which gap to run down on the way. An edge that says none of that is a straight line between two centers, and it will cut through whatever stands in the way.
89
- - **More pictures.** Icons and shapes are closed sets drawn from path data inside the tool, so a diagram wanting one that is not there has nowhere to go.
53
+ To install a skill to let your agent know how to use reladraw:
90
54
 
91
- The language is not stable. Expect the syntax to change.
92
-
93
- ## How it works
94
-
95
- A gap is a *minimum* distance, never an exact one. Say two things sit side by side, then say a third goes between them, and the first two are pushed apart by exactly what the third needs; delete the third and they close back up. That is the step an author otherwise does by hand — shove things apart to make room, then drag everything back so the diagram is not full of holes — and no number goes stale when a text grows.
96
-
97
- So the resolver solves a system rather than walking a chain. Each axis is a set of minimum distances, and the tightest arrangement satisfying all of them is found by longest paths: one answer, no search, no arrangement ever tried and rejected. The engine works out distances; which side of what a thing sits on came from the file.
98
-
99
- That is also what makes non-overlap affordable, so it holds for every pair of nodes without anyone writing it down. On its own "these two must not overlap" is a choice among four directions, which is the search this design refuses — but the file has usually settled it already: if your arrangement lets one node travel away from another and offers no way back, that is the only separation it permits. Where the file orders a pair on neither axis, the tool names them rather than guessing; where it orders them on both, the tie breaks toward the axis of least overlap, which is the smallest movement and the one place the tool decides something nobody wrote.
100
-
101
- Nothing is nudged. Each round derives the separations the file already implied, adds them as ordinary minimum distances, and solves the whole thing again from scratch — repairing a solved layout in place is the thing being avoided.
102
-
103
- ## Scope for a first version
104
-
105
- - Parser *(done)*
106
- - Deterministic resolver: minimum distances in, tightest arrangement out *(done)*
107
- - Static SVG renderer *(done)*
108
- - A command-line tool: text file in, SVG out *(done)*
109
- - A placement grammar that can say what a real diagram needs: several placements on one node, one thing between two others, exact side-to-side alignment *(done)*
110
- - Nodes that do not overlap by default, with the separation direction derived from the stated arrangement *(done)*
111
- - Minimal node-avoiding edge routing
112
- - Machine-readable diagnostics from the solved geometry
113
-
114
- Diagnostics are a real output rather than a debugging aid. What they cannot do is stand in for the grammar: a check catches only what the language genuinely leaves open, and "these must not overlap" rules arrangements out without naming one, so it can never place anything. Everything the source cannot tell you is computable once the geometry is solved, with no image involved: overlapping nodes, crossed edges, text exceeding its container, anything off-canvas, large dead regions. So the tool reports `hub overlaps laptop1` and `edge auth->db crosses 4 edges`, and the fix is written in the same vocabulary as the source. An agent working this way reads a report about a text file it wrote and edits that text file — no rendering, no vision model, no pixel arithmetic.
115
-
116
- A diagnostic never repairs a solved layout in place. That is the line the design holds: a checker allowed to nudge nodes is a layout algorithm with a bad search strategy, fixing one overlap into the next with no view of the whole. Deriving a constraint the file already implied and solving the whole system again is a different thing, and is how non-overlap works. What is left over — anything the source genuinely does not settle — is reported, naming the statement that was broken, and the author edits the source. Open, and it decides how far this goes: may a diagnostic describe a fix in words, or only name the symptom? Describing one means the tool has an opinion about layout, which is the auto-layout instinct coming back in through the side door.
117
-
118
- Three design problems decide how much machinery this needs, and the first outranks the other two:
119
-
120
- **Saying enough.** The benchmark contains arrangements the grammar cannot express at all, which is why some nodes land in the wrong place no matter how the file is written. So the work is adding statements, not restricting them. Expressiveness is not the danger; the engine *choosing* an arrangement is.
121
-
122
- **What the engine is allowed to decide.** Auto-layout is refused, because a picture chosen by an algorithm is not predictable from its source, and that predictability is the entire point. Working out coordinates from an arrangement the author stated is a different thing and is simply the job. The test between them: the engine's freedom may affect distances and never relationships. If a default can change which side of something a node sits on, the language was short a statement and the tool should say so rather than guess.
123
-
124
- **Overlap and edge routing.** Relative placement with default spacing collides as soon as two clusters grow toward each other, and nodes are kept apart for exactly that reason. Edges are different: which side of a node a line passes is a relationship, so the tool does not pick a route around an obstacle by itself. You say where a line goes instead — the side it leaves and arrives on, the gap it runs through, the nodes it passes above or below — and the line is drawn curved, square or straight through those points. A line that crosses something it should not is one you have not said enough about yet. Whether the tool should at least point that out, naming the clause that would fix it, is an open question.
55
+ ```
56
+ npx skills add reladraw/reladraw -g
57
+ ```
125
58
 
126
- ## Prior art
59
+ That installs it for every agent you use (Claude Code, Codex, Cursor, Copilot and others), each in its own skills directory. Leave off `-g` to install it into the current project only. To install it for just one agent, name it with `-a`:
127
60
 
128
- Four things sit near this, and each answers a different part of the problem.
61
+ ```
62
+ npx skills add reladraw/reladraw -g -a claude-code
63
+ ```
129
64
 
130
- **[Archify](https://github.com/tt-a1i/archify).** Built for agents to write: an agent emits typed JSON, a validator checks it against a schema and lints the layout, and it compiles deterministically to a good-looking, self-contained HTML file, with stepped playback and nodes pinned to git-verified source lines. Its positioning is grid or free coordinates, though — auto-arrangement on one side, absolute pixels on the other — and a JSON template says nothing about where the picture will end up, so the loop is still emit, render, look, tweak.
65
+ That gets you a copy of the skill at the time you run it, so you'll need to re-run that command to get the latest skill when there is a new release.
131
66
 
132
- **PIC and [pikchr](https://pikchr.org).** A text diagram format with no layout engine, where placement is stated and deterministic. But it is turtle graphics — a movable cursor that drops shapes and steps along — so a diagram is a sequence of pen movements rather than a set of stated relationships between named things. There is no group that reflows when a member is added.
67
+ ## Status
133
68
 
134
- **Graphviz `rank` and `cluster`.** Constraints on an auto-layout engine rather than a replacement for one, so output stays emergent and unpredictable from the source.
69
+ Version 0.8.1. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG.
135
70
 
136
- **Structurizr.** Has real manual layout, but is bound to the C4 model, which makes it a modeling notation with a renderer attached rather than a general placement language.
71
+ The language isn't stable yet, so expect the syntax to change.
137
72
 
138
73
  ## License
139
74
 
140
- Apache-2.0. This is a reusable primitive where adoption is the value, so restricting commercial use would defeat the purpose. See [LICENSE](LICENSE).
141
-
142
- The license covers the code, not the name: it grants no rights to use "reladraw", the project logo, or the project's other marks. Forks are welcome and should carry a different name. See [NOTICE](NOTICE).
75
+ Apache-2.0. See [LICENSE](LICENSE). The license covers the code, not the name. It grants no rights to "reladraw", the project logo or the project's other marks. See [NOTICE](NOTICE).
143
76
 
144
77
  ## Contributing
145
78
 
146
- Issues are wanted — especially a diagram you tried to write and could not. While the language is changing quickly, an issue is more useful than a pull request; [CONTRIBUTING.md](CONTRIBUTING.md) says why.
79
+ Issues are welcome. Particularly helpful is a diagram you could not represent in reladraw. While the language is still changing quickly, an issue helps more than a pull request. See [CONTRIBUTING.md](CONTRIBUTING.md).
package/SYNTAX.md CHANGED
@@ -1,4 +1,4 @@
1
- # Syntax reference — 0.7.1
1
+ # Syntax reference — 0.8.1
2
2
 
3
3
  What the language accepts. The parser, resolver and SVG renderer implement all of it; the sections at the end record what is defective, unchecked or undecided.
4
4
 
@@ -442,6 +442,10 @@ Several such lines over the same row take a lane each, the shorter inside the lo
442
442
 
443
443
  Sides at right angles can face away too. `from: left to: top` with the far node to the right leaves heading away from it, so the line steps out of the left, goes up past both nodes, runs along above them and comes straight down into the top. The side named at the far end says which way round: `to: bottom` takes it underneath instead.
444
444
 
445
+ When the two nodes are stacked instead — `from: bottom to: left` into a node directly below — going out past both and coming straight in would pass through the lower node. There the line is one curve, bending round the corner into the side named. If even the curve would cross a node, the line goes round the node on the side it names, as `left of` that node would send it.
446
+
447
+ Both ends can name the same side. `from: left to: left` with the far node to the right leaves heading away from it, and has to get round the node behind to reach the far side. It goes over the top, as the lines above do, and down into the gap; in a column, `from: bottom to: bottom` goes round the right.
448
+
445
449
  Over the top is the rule, not a guess the tool makes each time — it does not look for the shorter way round. To send the line underneath or round the left, say which side of a node it passes: `below resolver`. See "Passing on one side of a node".
446
450
 
447
451
  #### Several edges between the same two sides
@@ -1017,7 +1021,13 @@ That one was found by testing the lexer, not by rendering — and it could not h
1017
1021
 
1018
1022
  Pre-1.0, so the minor number is where a breaking change goes. Every removal below 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.
1019
1023
 
1020
- **Unreleased**
1024
+ **0.8.1**
1025
+
1026
+ - A fix, no syntax change. An edge naming a side at both ends could be drawn through one of its own nodes in two cases. With the nodes stacked and the sides at right angles, such as `from: bottom to: left`, the line went down past the lower node and came back up through it; it is now one curve round the corner. With both ends naming the same side, such as `from: left to: left`, the line ran straight through the node behind; it now goes over the top of that node, or round the right in a column. Existing diagrams with such an edge will look different, and correct.
1027
+
1028
+ - A fix, no syntax change. Children of a container stack close together, and an edge with no text between two of them used to get only that step, so it drew as an arrowhead with no line behind it. The gap now widens to show a short run of line behind each head, as an edge with text already did for its text. Existing diagrams with such an edge will look different, and correct.
1029
+
1030
+ **0.8.0**
1021
1031
 
1022
1032
  - `shape: circle`. A circle is sized to hold its text and is always round. Edges on one of its sides spread round that quarter of the circle, and a corner of it is the point on the circle halfway round. It does not yet take a badge, a deck or a node placed against its own edge. Nothing that already renders changes.
1023
1033
 
@@ -59,6 +59,14 @@ export declare const THICKNESS: Record<string, number>;
59
59
  * thickness is a size, like a text's, and a size is allowed to take room.
60
60
  */
61
61
  export declare function arrowLength(thickness: number): number;
62
+ /**
63
+ * Line left showing behind the head of an edge with no text, so a gap an arrow
64
+ * crosses is at least this plus its heads. Stacked children sit `CHILD_GAP`
65
+ * apart, which is less than one head, and an arrow between them drew as a head
66
+ * with no line. This is what the tightest named gap already leaves behind a
67
+ * normal head, so any gap an author could have asked for holds it unchanged.
68
+ */
69
+ export declare const BARE_EDGE_RUN: number;
62
70
  /**
63
71
  * Line left showing between an edge's text and the box at that end of the
64
72
  * corridor it crosses.
package/dist/constants.js CHANGED
@@ -71,6 +71,14 @@ export const THICKNESS = {
71
71
  export function arrowLength(thickness) {
72
72
  return ARROW_MARKER_WIDTH * thickness;
73
73
  }
74
+ /**
75
+ * Line left showing behind the head of an edge with no text, so a gap an arrow
76
+ * crosses is at least this plus its heads. Stacked children sit `CHILD_GAP`
77
+ * apart, which is less than one head, and an arrow between them drew as a head
78
+ * with no line. This is what the tightest named gap already leaves behind a
79
+ * normal head, so any gap an author could have asked for holds it unchanged.
80
+ */
81
+ export const BARE_EDGE_RUN = GAPS['tight'] - ARROW_LENGTH;
74
82
  /**
75
83
  * Line left showing between an edge's text and the box at that end of the
76
84
  * corridor it crosses.
package/dist/render.js CHANGED
@@ -1560,14 +1560,37 @@ function planLoops(edges, nodes, ends, corridors, routes, measurer, fontSize) {
1560
1560
  const { start, end } = ends.get(edge);
1561
1561
  if (start.side === undefined || end.side === undefined)
1562
1562
  continue;
1563
- // Sides at right angles, one facing away from the other end.
1563
+ // Sides at right angles, one facing away from the other end. Going out
1564
+ // past both boxes and coming straight in is right when the boxes are side
1565
+ // by side. When the other end lies within the span of the box facing away
1566
+ // — stacked boxes — coming straight in would pass through that box, so
1567
+ // the curve is kept, which has room to bend round the corner; and if even
1568
+ // the curve hits a box, the line goes round the facing-away box on the
1569
+ // side it names.
1564
1570
  if (start.tx * end.tx + start.ty * end.ty === 0) {
1571
+ const away = awayEnd(edge, start, end);
1572
+ if (away && within(faceOf(away.node), away.anchor, away.other)) {
1573
+ if (curveHits(edge, ends.get(edge), nodes)) {
1574
+ implyRoute(edge, away.node, sidePass(away.anchor.side), nodes, ends, routes, measurer, fontSize);
1575
+ }
1576
+ continue;
1577
+ }
1565
1578
  const inWay = nodes.filter((node) => !(contains(node, edge.from) && contains(node, edge.to)));
1566
1579
  const plan = turnBack(edge, start, end, inWay, routes, measurer, fontSize);
1567
1580
  if (plan)
1568
1581
  tops.push(plan);
1569
1582
  continue;
1570
1583
  }
1584
+ // Both sides facing the same way, one box behind the other: the line has
1585
+ // to get round that box to reach the far side, which a curve cannot. It
1586
+ // goes over the top, or round the right for a column, as a loop does.
1587
+ if (start.tx === end.tx && start.ty === end.ty) {
1588
+ const away = awayEnd(edge, start, end);
1589
+ if (away && curveHits(edge, ends.get(edge), nodes)) {
1590
+ implyRoute(edge, away.node, start.tx !== 0 ? 'above' : 'right', nodes, ends, routes, measurer, fontSize);
1591
+ }
1592
+ continue;
1593
+ }
1571
1594
  // The two ends point opposite ways along one axis, each away from the other.
1572
1595
  if (start.tx !== -end.tx || start.ty !== -end.ty)
1573
1596
  continue;
@@ -1755,6 +1778,90 @@ function turnBack(edge, start, end, inWay, routes, measurer, fontSize) {
1755
1778
  },
1756
1779
  };
1757
1780
  }
1781
+ /**
1782
+ * Whether the single curve an edge would otherwise be drawn as passes through
1783
+ * a box: either of its own, or any other that holds neither end. Measured on
1784
+ * the curve itself, so the answer is the picture's and not a rule about which
1785
+ * sides were named.
1786
+ *
1787
+ * Only passing through counts. Also counting a curve that merely comes close,
1788
+ * or whose text touches a node, was tried (2026-09-27) and sent nearly every
1789
+ * such edge the long way round with square corners, which the user rejected
1790
+ * outright.
1791
+ */
1792
+ function curveHits(edge, { start, end, bow }, nodes) {
1793
+ const reach = controlReach(start, end);
1794
+ const lift = 4 / 3;
1795
+ const bx = (bow?.x ?? 0) * lift;
1796
+ const by = (bow?.y ?? 0) * lift;
1797
+ const c1 = { x: start.x + start.tx * reach + bx, y: start.y + start.ty * reach + by };
1798
+ const c2 = { x: end.x + end.tx * reach + bx, y: end.y + end.ty * reach + by };
1799
+ const at = (t) => {
1800
+ const u = 1 - t;
1801
+ return {
1802
+ x: u * u * u * start.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * end.x,
1803
+ y: u * u * u * start.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * end.y,
1804
+ };
1805
+ };
1806
+ // The line starts and ends on its own boxes' sides, so those are shrunk a
1807
+ // pixel to let it touch them there.
1808
+ const boxes = [
1809
+ ...[edge.from, edge.to].map((node) => grow(extentOfBox(faceOf(node)), -1)),
1810
+ ...nodes
1811
+ .filter((node) => !contains(node, edge.from) && !contains(node, edge.to))
1812
+ .map((node) => extentOfBox(faceOf(node))),
1813
+ ];
1814
+ const steps = 32;
1815
+ let previous = start;
1816
+ for (let step = 1; step <= steps; step += 1) {
1817
+ const point = at(step / steps);
1818
+ if (boxes.some((box) => segmentHits(previous, point, box)))
1819
+ return true;
1820
+ previous = point;
1821
+ }
1822
+ return false;
1823
+ }
1824
+ /**
1825
+ * The end of an edge whose side faces away from the other end, with its node
1826
+ * and the other end's anchor; the start if both do.
1827
+ */
1828
+ function awayEnd(edge, start, end) {
1829
+ const facesAway = (from, to) => from.tx * (to.x - from.x) + from.ty * (to.y - from.y) < 0;
1830
+ if (facesAway(start, end))
1831
+ return { node: edge.from, anchor: start, other: end };
1832
+ if (facesAway(end, start))
1833
+ return { node: edge.to, anchor: end, other: start };
1834
+ return undefined;
1835
+ }
1836
+ /**
1837
+ * Whether a line coming straight in to `other`, across the way `anchor`'s side
1838
+ * faces, would pass through `box` — that is, `other` lies within the box's
1839
+ * span along that way.
1840
+ */
1841
+ function within(box, anchor, other) {
1842
+ const run = anchor.tx !== 0 ? 'x' : 'y';
1843
+ return lo(box, run) < other[run] && other[run] < hi(box, run);
1844
+ }
1845
+ /** The clause that passes a node on the side named. */
1846
+ function sidePass(side) {
1847
+ return side === 'top' ? 'above' : side === 'bottom' ? 'below' : side;
1848
+ }
1849
+ /**
1850
+ * Route an edge as if it carried one clause the author did not write, passing
1851
+ * `node` on that side. If that cannot be drawn, the edge keeps its curve: a
1852
+ * refusal would name a clause that is not in the file, and would refuse a file
1853
+ * earlier versions drew.
1854
+ */
1855
+ function implyRoute(edge, node, direction, nodes, ends, routes, measurer, fontSize) {
1856
+ const written = `${direction === 'left' || direction === 'right' ? `${direction} of` : direction} ${node.name}`;
1857
+ try {
1858
+ routes.set(edge, planRoute(edge, [{ direction, nodes: [node], written }], nodes, ends.get(edge), measurer, fontSize));
1859
+ }
1860
+ catch (error) {
1861
+ if (!(error instanceof SourceError))
1862
+ throw error;
1863
+ }
1864
+ }
1758
1865
  /** The most a route's corner is rounded by. */
1759
1866
  const ROUTE_RADIUS = 20;
1760
1867
  /**
package/dist/resolve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ALL_ATTR_KEYS, ATTR_KEYS, PART_SIDES, COLOR_KEYS, COLOR_PARTS, CONTENT_ALIGNMENTS, CONTENT_WIDTHS, LINE_VALUES, describePlacement, nameTarget, } from './ast.js';
2
- import { CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
2
+ import { BARE_EDGE_RUN, CHILD_GAP, DECK_STEP, DEFAULT_FONT_SIZE, DEFAULT_MARGIN, GAPS, HEADER_GAP, ICON_GAP, ICON_LINES, TEXT_CLEARANCE, PAD, SEPARATION_GAP, THICKNESS, arrowLength, fontSizeFor, leafTop, textExtent, textStyleFor, widestLine, } from './constants.js';
3
3
  import { fix, reachability, tightest } from './constrain.js';
4
4
  import { SourceError } from './errors.js';
5
5
  import { bodyFor } from './icons.js';
@@ -1404,6 +1404,9 @@ function memberOn(target, axis) {
1404
1404
  * away together or not at all (`left right` with `from` first); sides at right
1405
1405
  * angles each may, and either is enough (`left top` with `from` first, where
1406
1406
  * the edge turns back over the top, or with `to` below, where it goes round).
1407
+ * The same side on both is left out on purpose: that edge goes round the box
1408
+ * behind, and its text rides on the stretch over that box and the gap, so the
1409
+ * room made in the gap is what keeps a long text clear of the line's turns.
1407
1410
  */
1408
1411
  function facingAway(fromSide, toSide) {
1409
1412
  // The axis a side is on, and whether it faces away when its own end is first.
@@ -1450,22 +1453,34 @@ function liftTo(node, indexOf, local) {
1450
1453
  height: node.height,
1451
1454
  };
1452
1455
  }
1453
- /** The edges with text whose two ends are different members of this group. */
1456
+ /** The edges whose two ends are different members of this group. */
1454
1457
  function corridorsIn(edges, locate, measurer, fontSize) {
1455
1458
  const corridors = [];
1456
1459
  for (const edge of edges) {
1457
- // An edge with no text asks for nothing: every gap holds an arrowhead. And a
1458
- // edge told to pass between two named things carries its text in *that*
1460
+ // An edge told to pass between two named things carries its text in *that*
1459
1461
  // corridor rather than in the gap between its own ends, so widening this one
1460
1462
  // would make room where the text never goes.
1461
1463
  // An edge that says which side of something it passes is routed round it,
1462
1464
  // and its text rides on that route for the same reason.
1463
- if (edge.text === undefined || edge.between || edge.passes)
1465
+ if (edge.between || edge.passes)
1464
1466
  continue;
1465
1467
  const from = locate(edge.from);
1466
1468
  const to = locate(edge.to);
1467
1469
  if (!from || !to || from.index === to.index)
1468
1470
  continue;
1471
+ // With no text, the gap still has to show a run of line behind each head,
1472
+ // or two stacked children with an arrow between them draw as a head alone.
1473
+ if (edge.text === undefined) {
1474
+ const bare = BARE_EDGE_RUN + arrowLength(edge.look.thickness) * (edge.both ? 2 : 1);
1475
+ corridors.push({
1476
+ edge,
1477
+ from,
1478
+ to,
1479
+ need: { x: bare, y: bare },
1480
+ away: facingAway(edge.attrs['from'], edge.attrs['to']),
1481
+ });
1482
+ continue;
1483
+ }
1469
1484
  // The clearance is doubled because the text is drawn at the *midpoint* of
1470
1485
  // the line, so the room it needs is symmetric about that point whatever sits
1471
1486
  // at either end. The arrowhead is charged on both sides for the same reason:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reladraw",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "A diagram language where you say where things go.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "scripts": {
35
35
  "build": "tsc",
36
36
  "prepublishOnly": "npm run build",
37
+ "preversion": "./dev.sh check-version \"$npm_new_version\"",
37
38
  "dev": "tsc --watch",
38
39
  "example": "npm run build && node dist/cli.js examples/arch.reladraw -o examples/out/arch.svg"
39
40
  },