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 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.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, and an element that does the same inside a web page.
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
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
- settle(edge, place(edge, [...order]));
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 = Math.max(least, a * 0.85);
2637
- b = Math.max(least, b * 0.85);
2746
+ a = shrink(a);
2747
+ b = shrink(b);
2638
2748
  }
2639
2749
  curve(...quarter(a, b));
2640
2750
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reladraw",
3
- "version": "0.14.1",
3
+ "version": "0.14.3",
4
4
  "description": "A diagram language where you say where things go.",
5
5
  "type": "module",
6
6
  "bin": {