reladraw 0.14.1 → 0.14.3
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 +14 -1
- package/dist/render.js +113 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
-
Version 0.14.
|
|
85
|
+
Version 0.14.3. 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.
|
|
86
86
|
|
|
87
87
|
The language isn't stable yet, so expect the syntax to change.
|
|
88
88
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.14.
|
|
1
|
+
# Syntax reference — 0.14.3
|
|
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
|
|
|
@@ -1112,6 +1112,19 @@ That one was found by testing the lexer, not by rendering — and it could not h
|
|
|
1112
1112
|
|
|
1113
1113
|
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.
|
|
1114
1114
|
|
|
1115
|
+
**0.14.3**
|
|
1116
|
+
|
|
1117
|
+
No syntax changes, and no file that worked is refused. No example diagram draws differently.
|
|
1118
|
+
|
|
1119
|
+
- A fix: many curved lines into one side could hang the page. With twenty-two copies of `edge a -> b from: right to: left`, some lines had no room left to curve before the arrowhead, and the tool kept trying ever smaller curves thousands of times. It now stops at the smallest, and the same file draws in a tenth of a second.
|
|
1120
|
+
|
|
1121
|
+
**0.14.2**
|
|
1122
|
+
|
|
1123
|
+
No syntax changes, and no file that worked is refused.
|
|
1124
|
+
|
|
1125
|
+
- Many copies of one line draw far faster. Lines with the same two ends, the same sides, the same clauses and no text are now drawn as one line and its copies, each a lane further out, instead of each finding its own way round the copies before it. Fifteen copies of `edge a -> b from: left to: right` took about a third of a second and forty took two and a half; forty now take half a second. Where such copies used to split, some going over the top and some underneath, they now stay together. A copy that would run into a box or cross a line the first one does not is still found as before.
|
|
1126
|
+
- Curved lines in busy diagrams draw faster. No picture changes.
|
|
1127
|
+
|
|
1115
1128
|
**0.14.1**
|
|
1116
1129
|
|
|
1117
1130
|
No syntax changes, and no file that worked is refused.
|
package/dist/render.js
CHANGED
|
@@ -1704,6 +1704,9 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1704
1704
|
...(edge.passes ?? []).map((pass) => pass.written),
|
|
1705
1705
|
].join(' ');
|
|
1706
1706
|
};
|
|
1707
|
+
// Each line of such a group after the first, and the line it is drawn as a
|
|
1708
|
+
// copy of: the one a lane inside it.
|
|
1709
|
+
const copies = new Map();
|
|
1707
1710
|
for (let index = 0; index < wanted.length; index += 1) {
|
|
1708
1711
|
const key = sameWay(wanted[index]);
|
|
1709
1712
|
const group = wanted.filter((edge) => sameWay(edge) === key);
|
|
@@ -1723,6 +1726,14 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1723
1726
|
rest.splice(index, 0, ...ordered);
|
|
1724
1727
|
wanted.splice(0, wanted.length, ...rest);
|
|
1725
1728
|
index += group.length - 1;
|
|
1729
|
+
// A text widens the lane it rides in, which a copy cannot know; those are searched.
|
|
1730
|
+
const named = (edge) => {
|
|
1731
|
+
const { start, end } = ends.get(edge);
|
|
1732
|
+
return start.side !== undefined && end.side !== undefined;
|
|
1733
|
+
};
|
|
1734
|
+
if (ordered.every((edge) => edge.lines === undefined && named(edge))) {
|
|
1735
|
+
ordered.slice(1).forEach((edge, at) => copies.set(edge, ordered[at]));
|
|
1736
|
+
}
|
|
1726
1737
|
}
|
|
1727
1738
|
// Where several lines leave one side, their ends may be squeezed closer than
|
|
1728
1739
|
// a lane; the lines then run out of them that close together, and the lanes
|
|
@@ -1843,8 +1854,70 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1843
1854
|
found.set(edge, way);
|
|
1844
1855
|
ends.set(edge, { start: way.start, end: way.end });
|
|
1845
1856
|
};
|
|
1857
|
+
// A line identical to one already placed is that line moved out by a lane,
|
|
1858
|
+
// not a search of its own: the same way, every stretch shifted to the same
|
|
1859
|
+
// side by the distance between their ends. Searching each afresh, keeping
|
|
1860
|
+
// clear of every copy before it, made fifteen identical lines take as long as
|
|
1861
|
+
// a tangled diagram. Where the copy would not do — its ends do not line up
|
|
1862
|
+
// with the shift, a stretch folds up, it meets a box, crowds or crosses a line
|
|
1863
|
+
// the first does not — the line is searched for as any other.
|
|
1864
|
+
const copyOf = (edge, inside, placed) => {
|
|
1865
|
+
const { start, end } = planned.get(edge);
|
|
1866
|
+
const base = found.get(inside).points;
|
|
1867
|
+
const count = base.length - 1;
|
|
1868
|
+
if (count < 1)
|
|
1869
|
+
return undefined;
|
|
1870
|
+
const heading = base.slice(1).map((q, index) => {
|
|
1871
|
+
const p = base[index];
|
|
1872
|
+
const length = Math.hypot(q.x - p.x, q.y - p.y);
|
|
1873
|
+
return { x: (q.x - p.x) / length, y: (q.y - p.y) / length, length };
|
|
1874
|
+
});
|
|
1875
|
+
if (heading.some((one) => one.length < 0.5 || (Math.abs(one.x) > 1e-6 && Math.abs(one.y) > 1e-6)))
|
|
1876
|
+
return undefined;
|
|
1877
|
+
const normal = heading.map((one) => ({ x: -one.y, y: one.x }));
|
|
1878
|
+
const along = (from, to, axis) => (to.x - from.x) * axis.x + (to.y - from.y) * axis.y;
|
|
1879
|
+
const shift = along(base[0], start, normal[0]);
|
|
1880
|
+
if (Math.abs(shift) < 0.5 || Math.abs(along(base[0], start, heading[0])) > 0.5)
|
|
1881
|
+
return undefined;
|
|
1882
|
+
if (Math.abs(along(base[count], end, normal[count - 1]) - shift) > 0.5 || Math.abs(along(base[count], end, heading[count - 1])) > 0.5) {
|
|
1883
|
+
return undefined;
|
|
1884
|
+
}
|
|
1885
|
+
const points = [{ x: start.x, y: start.y }];
|
|
1886
|
+
for (let index = 1; index < count; index += 1) {
|
|
1887
|
+
const [p, q] = [normal[index - 1], normal[index]];
|
|
1888
|
+
const turns = Math.abs(p.x * q.x + p.y * q.y) < 0.5;
|
|
1889
|
+
const v = base[index];
|
|
1890
|
+
points.push(turns ? { x: v.x + shift * (p.x + q.x), y: v.y + shift * (p.y + q.y) } : { x: v.x + shift * q.x, y: v.y + shift * q.y });
|
|
1891
|
+
}
|
|
1892
|
+
points.push({ x: end.x, y: end.y });
|
|
1893
|
+
for (let index = 0; index < count; index += 1) {
|
|
1894
|
+
const [p, q] = [points[index], points[index + 1]];
|
|
1895
|
+
if (along(p, q, heading[index]) < 0.5)
|
|
1896
|
+
return undefined;
|
|
1897
|
+
}
|
|
1898
|
+
const inner = points.slice(1, -1);
|
|
1899
|
+
const walls = nodes
|
|
1900
|
+
.filter((node) => !contains(node, edge.from) && !contains(node, edge.to))
|
|
1901
|
+
.map((node) => grow(extentOfBox(faceOf(node)), ATTACH_MARGIN - 0.5));
|
|
1902
|
+
const own = [edge.from, edge.to].map((node) => grow(extentOfBox(faceOf(node)), ATTACH_MARGIN - 0.5));
|
|
1903
|
+
const fences = (edge.passes ?? []).map((pass) => fenceOf(pass, 0));
|
|
1904
|
+
if (!wayClear(points, [...obstaclesFor(edge, nodes), ...walls]) || !wayClear(inner, [...own, ...fences]) || !passesAll(edge, points)) {
|
|
1905
|
+
return undefined;
|
|
1906
|
+
}
|
|
1907
|
+
const lane = laneFor(edge);
|
|
1908
|
+
for (const other of placed) {
|
|
1909
|
+
const theirs = found.get(other).points;
|
|
1910
|
+
if (linesCross(points, theirs) > linesCross(base, theirs))
|
|
1911
|
+
return undefined;
|
|
1912
|
+
if (other !== inside && crowds(points, theirs, lane))
|
|
1913
|
+
return undefined;
|
|
1914
|
+
}
|
|
1915
|
+
return { start, end, points, mid: textOnWay(points, edge, nodes, measurer, fontSize) };
|
|
1916
|
+
};
|
|
1846
1917
|
for (const edge of wanted) {
|
|
1847
|
-
|
|
1918
|
+
const inside = copies.get(edge);
|
|
1919
|
+
const copy = inside && found.has(inside) ? copyOf(edge, inside, [...order]) : undefined;
|
|
1920
|
+
settle(edge, copy ?? place(edge, [...order]));
|
|
1848
1921
|
order.push(edge);
|
|
1849
1922
|
}
|
|
1850
1923
|
// Which line takes the inside of a shared stretch depends on which was placed
|
|
@@ -1900,6 +1973,27 @@ function planWays(edges, nodes, ends, corridors, measurer, fontSize) {
|
|
|
1900
1973
|
}
|
|
1901
1974
|
return routes;
|
|
1902
1975
|
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Whether two lines run side by side, along the same stretch, nearer than a
|
|
1978
|
+
* lane apart — which a line found by search never does.
|
|
1979
|
+
*/
|
|
1980
|
+
function crowds(a, b, lane) {
|
|
1981
|
+
for (let i = 0; i + 1 < a.length; i += 1) {
|
|
1982
|
+
const [p, q] = [a[i], a[i + 1]];
|
|
1983
|
+
const level = Math.abs(p.y - q.y) < 0.5;
|
|
1984
|
+
for (let j = 0; j + 1 < b.length; j += 1) {
|
|
1985
|
+
const [r, s] = [b[j], b[j + 1]];
|
|
1986
|
+
if (level !== Math.abs(r.y - s.y) < 0.5)
|
|
1987
|
+
continue;
|
|
1988
|
+
const [across, lo, hi, otherAcross, otherLo, otherHi] = level
|
|
1989
|
+
? [p.y, Math.min(p.x, q.x), Math.max(p.x, q.x), r.y, Math.min(r.x, s.x), Math.max(r.x, s.x)]
|
|
1990
|
+
: [p.x, Math.min(p.y, q.y), Math.max(p.y, q.y), r.x, Math.min(r.y, s.y), Math.max(r.y, s.y)];
|
|
1991
|
+
if (Math.min(hi, otherHi) - Math.max(lo, otherLo) > 0.5 && Math.abs(across - otherAcross) < lane - 0.5)
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1903
1997
|
/** How many times two lines cross, each drawn as straight pieces through its points. */
|
|
1904
1998
|
function linesCross(a, b) {
|
|
1905
1999
|
let count = 0;
|
|
@@ -2562,9 +2656,21 @@ function sweptPath(points, boxes, neighbors, arrow, marks) {
|
|
|
2562
2656
|
return false;
|
|
2563
2657
|
}
|
|
2564
2658
|
}
|
|
2659
|
+
// A piece of another line wholly beyond the curve's reach, and its air,
|
|
2660
|
+
// can neither cross it nor come near it.
|
|
2661
|
+
const reach = {
|
|
2662
|
+
minX: Math.min(...trace.map((point) => point.x)) - SWEEP_AIR,
|
|
2663
|
+
maxX: Math.max(...trace.map((point) => point.x)) + SWEEP_AIR,
|
|
2664
|
+
minY: Math.min(...trace.map((point) => point.y)) - SWEEP_AIR,
|
|
2665
|
+
maxY: Math.max(...trace.map((point) => point.y)) + SWEEP_AIR,
|
|
2666
|
+
};
|
|
2565
2667
|
for (const line of neighbors) {
|
|
2566
2668
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
2567
2669
|
const [p, q] = [line[index], line[index + 1]];
|
|
2670
|
+
if (Math.max(p.x, q.x) < reach.minX || Math.min(p.x, q.x) > reach.maxX)
|
|
2671
|
+
continue;
|
|
2672
|
+
if (Math.max(p.y, q.y) < reach.minY || Math.min(p.y, q.y) > reach.maxY)
|
|
2673
|
+
continue;
|
|
2568
2674
|
for (let step = 0; step < steps; step += 1) {
|
|
2569
2675
|
if (intersect(trace[step], trace[step + 1], p, q))
|
|
2570
2676
|
return false;
|
|
@@ -2632,9 +2738,13 @@ function sweptPath(points, boxes, neighbors, arrow, marks) {
|
|
|
2632
2738
|
const p3 = along(corner, dout, b);
|
|
2633
2739
|
return [p0, along(p0, din, a * kappa), along(p3, dout, -b * kappa), p3];
|
|
2634
2740
|
};
|
|
2741
|
+
// Within half a pixel of the smallest is the smallest: where an end piece
|
|
2742
|
+
// has no room left, the smallest is zero, which shrinking never reaches,
|
|
2743
|
+
// and twenty-two lines into one side took the page down.
|
|
2744
|
+
const shrink = (size) => (size * 0.85 < least + 0.5 ? least : size * 0.85);
|
|
2635
2745
|
while ((a > least || b > least) && !clear(...quarter(a, b))) {
|
|
2636
|
-
a =
|
|
2637
|
-
b =
|
|
2746
|
+
a = shrink(a);
|
|
2747
|
+
b = shrink(b);
|
|
2638
2748
|
}
|
|
2639
2749
|
curve(...quarter(a, b));
|
|
2640
2750
|
}
|