@phin-tech/dottie 0.6.2 → 0.6.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/dist/dottie.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.6.2 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.6.3 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value2) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value: value2 }) : obj[key] = value2;
4
4
  var __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
@@ -13,7 +13,7 @@ var KNOWN_FIELDS = {
13
13
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
14
14
  layer: ["id", "name", "visible", "locked"],
15
15
  rgba: ["r", "g", "b", "a"],
16
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset"],
16
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
17
17
  element: [
18
18
  "id",
19
19
  "kind",
@@ -54,7 +54,8 @@ var KNOWN_FIELDS = {
54
54
  extensionRef: ["id", "shape", "version", "params"],
55
55
  normalizedPoint: ["x", "y"],
56
56
  normalizedRect: ["x", "y", "width", "height"],
57
- connector: ["start", "end", "routing", "startHead", "endHead"],
57
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
58
+ waypoint: ["x", "y"],
58
59
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
59
60
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
60
61
  keyframe: ["t", "x", "y", "width", "height", "rotation", "strokeWidth", "stroke", "fill", "visible", "reveal", "opacity", "ease"],
@@ -136,7 +137,8 @@ var Reader = class {
136
137
  font: str(s.font) || null,
137
138
  gridSize: num(s.gridSize) ?? null,
138
139
  opacity: num(s.opacity) ?? null,
139
- dashOffset: num(s.dashOffset) ?? null
140
+ dashOffset: num(s.dashOffset) ?? null,
141
+ textColor: rgba(s.textColor) ?? null
140
142
  };
141
143
  }
142
144
  endpoint(v) {
@@ -164,7 +166,12 @@ var Reader = class {
164
166
  end: this.endpoint(v.end),
165
167
  routing: routing && ["straight", "curved", "elbow"].includes(routing) ? routing : "straight",
166
168
  startHead: head(v.startHead, "none"),
167
- endHead: head(v.endHead, "arrow")
169
+ endHead: head(v.endHead, "arrow"),
170
+ waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
171
+ this.fields(w, "waypoint");
172
+ return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
173
+ }) : null,
174
+ hops: typeof v.hops === "boolean" ? v.hops : null
168
175
  };
169
176
  }
170
177
  image(v, table) {
@@ -441,8 +448,18 @@ function isAutomaticInk(c) {
441
448
  function cloneElement(e) {
442
449
  return {
443
450
  ...e,
444
- style: { ...e.style, stroke: { ...e.style.stroke }, fill: { ...e.style.fill } },
445
- connector: e.connector ? { ...e.connector, start: { ...e.connector.start }, end: { ...e.connector.end } } : null
451
+ style: {
452
+ ...e.style,
453
+ stroke: { ...e.style.stroke },
454
+ fill: { ...e.style.fill },
455
+ textColor: e.style.textColor ? { ...e.style.textColor } : null
456
+ },
457
+ connector: e.connector ? {
458
+ ...e.connector,
459
+ start: { ...e.connector.start },
460
+ end: { ...e.connector.end },
461
+ waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
462
+ } : null
446
463
  };
447
464
  }
448
465
  function orderedElements(page) {
@@ -872,13 +889,19 @@ function rotatedBounds(e) {
872
889
  return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
873
890
  }
874
891
  function resolvingInk(e, ink) {
875
- if (!isAutomaticInk(e.style.stroke)) return e;
892
+ const text = e.style.textColor !== null && isAutomaticInk(e.style.textColor);
893
+ if (!isAutomaticInk(e.style.stroke) && !text) return e;
876
894
  const copy = cloneElement(e);
877
895
  const covers = e.style.fillStyle === "solid" || e.style.fillStyle === "grid";
878
896
  const onLightFill = covers && e.style.fill.a >= 0.5 && luminance(e.style.fill) > 0.5;
879
- copy.style.stroke = luminance(ink) > 0.5 && onLightFill ? { ...black } : { ...ink };
897
+ const resolved2 = luminance(ink) > 0.5 && onLightFill ? black : ink;
898
+ if (isAutomaticInk(e.style.stroke)) copy.style.stroke = { ...resolved2 };
899
+ if (text) copy.style.textColor = { ...resolved2 };
880
900
  return copy;
881
901
  }
902
+ function labelColor(e) {
903
+ return e.kind === "text" || e.kind === "note" ? e.style.stroke : e.style.textColor ?? e.style.stroke;
904
+ }
882
905
  function shapeContours(e) {
883
906
  if (e.kind !== "path" || !e.subpaths) return [];
884
907
  const out = [];
@@ -972,34 +995,93 @@ function resolve(ep, other, index) {
972
995
  function endpoints(e, index) {
973
996
  const c = e.connector;
974
997
  if (!c) return [{ x: 0, y: 0 }, { x: 0, y: 0 }];
975
- return [resolve(c.start, reference(c.end, index), index), resolve(c.end, reference(c.start, index), index)];
998
+ const w = c.waypoints ?? [];
999
+ return [
1000
+ resolve(c.start, w.length > 0 ? w[0] : reference(c.end, index), index),
1001
+ resolve(c.end, w.length > 0 ? w[w.length - 1] : reference(c.start, index), index)
1002
+ ];
976
1003
  }
977
1004
  function routePoints(r) {
978
1005
  if (r.kind === "polyline") return r.points;
979
- const out = [];
980
- for (let i = 0; i <= 24; i++) {
981
- const t = i / 24, u = 1 - t;
982
- out.push({
983
- x: u * u * u * r.a.x + 3 * u * u * t * r.b.x + 3 * u * t * t * r.c.x + t * t * t * r.d.x,
984
- y: u * u * u * r.a.y + 3 * u * u * t * r.b.y + 3 * u * t * t * r.c.y + t * t * t * r.d.y
985
- });
1006
+ const out = [], p = r.points;
1007
+ for (let s = 0; s < Math.floor((p.length - 1) / 3); s++) {
1008
+ const a = p[3 * s], b = p[3 * s + 1], c = p[3 * s + 2], d = p[3 * s + 3];
1009
+ for (let i = s === 0 ? 0 : 1; i <= 24; i++) {
1010
+ const t = i / 24, u = 1 - t;
1011
+ out.push({
1012
+ x: u * u * u * a.x + 3 * u * u * t * b.x + 3 * u * t * t * c.x + t * t * t * d.x,
1013
+ y: u * u * u * a.y + 3 * u * u * t * b.y + 3 * u * t * t * c.y + t * t * t * d.y
1014
+ });
1015
+ }
986
1016
  }
987
1017
  return out;
988
1018
  }
1019
+ var vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1020
+ function simplified(pts) {
1021
+ const out = [];
1022
+ for (const q of pts) {
1023
+ const l = out[out.length - 1];
1024
+ if (l && Math.abs(l.x - q.x) < 0.5 && Math.abs(l.y - q.y) < 0.5) continue;
1025
+ if (out.length >= 2) {
1026
+ const p = out[out.length - 2], m = out[out.length - 1];
1027
+ if (Math.abs(p.x - m.x) < 0.5 && Math.abs(m.x - q.x) < 0.5 || Math.abs(p.y - m.y) < 0.5 && Math.abs(m.y - q.y) < 0.5) {
1028
+ out[out.length - 1] = q;
1029
+ continue;
1030
+ }
1031
+ }
1032
+ out.push(q);
1033
+ }
1034
+ return out.length >= 2 ? out : pts;
1035
+ }
1036
+ function routeThrough(p, c) {
1037
+ switch (c.routing) {
1038
+ case "straight":
1039
+ return { kind: "polyline", points: p };
1040
+ case "curved": {
1041
+ const out = [p[0]];
1042
+ for (let i = 0; i < p.length - 1; i++) {
1043
+ const p0 = p[Math.max(i - 1, 0)], p1 = p[i], p2 = p[i + 1], p3 = p[Math.min(i + 2, p.length - 1)];
1044
+ out.push({ x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6 });
1045
+ out.push({ x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6 });
1046
+ out.push(p2);
1047
+ }
1048
+ return { kind: "curve", points: out };
1049
+ }
1050
+ case "elbow": {
1051
+ const a = p[0], first = p[1];
1052
+ let goVertical = vertical(c.start.port) ?? Math.abs(first.y - a.y) > Math.abs(first.x - a.x);
1053
+ const endV = vertical(c.end.port);
1054
+ const out = [a];
1055
+ for (let i = 1; i < p.length; i++) {
1056
+ const q = p[i], cur = out[out.length - 1];
1057
+ const alignedX = Math.abs(cur.x - q.x) < 0.5, alignedY = Math.abs(cur.y - q.y) < 0.5;
1058
+ if (alignedX || alignedY) {
1059
+ out.push(q);
1060
+ if (alignedX !== alignedY) goVertical = !alignedX;
1061
+ continue;
1062
+ }
1063
+ const up = i === p.length - 1 && endV !== null ? !endV : goVertical;
1064
+ out.push(up ? { x: cur.x, y: q.y } : { x: q.x, y: cur.y }, q);
1065
+ goVertical = up;
1066
+ }
1067
+ return { kind: "polyline", points: simplified(out) };
1068
+ }
1069
+ }
1070
+ }
989
1071
  function route(e, index) {
990
1072
  const c = e.connector;
991
1073
  if (!c) return { kind: "polyline", points: [] };
992
1074
  const [a, b] = endpoints(e, index);
1075
+ if (c.waypoints && c.waypoints.length > 0) return routeThrough([a, ...c.waypoints.map((w) => ({ x: w.x, y: w.y })), b], c);
993
1076
  const dx = b.x - a.x, dy = b.y - a.y;
994
1077
  const horizontal = Math.abs(dx) >= Math.abs(dy);
995
1078
  switch (c.routing) {
996
1079
  case "straight":
997
1080
  return { kind: "polyline", points: [a, b] };
998
1081
  case "curved":
999
- if (horizontal) return { kind: "cubic", a, b: { x: a.x + dx / 2, y: a.y }, c: { x: b.x - dx / 2, y: b.y }, d: b };
1000
- return { kind: "cubic", a, b: { x: a.x, y: a.y + dy / 2 }, c: { x: b.x, y: b.y - dy / 2 }, d: b };
1082
+ if (horizontal) return { kind: "curve", points: [a, { x: a.x + dx / 2, y: a.y }, { x: b.x - dx / 2, y: b.y }, b] };
1083
+ return { kind: "curve", points: [a, { x: a.x, y: a.y + dy / 2 }, { x: b.x, y: b.y - dy / 2 }, b] };
1001
1084
  case "elbow": {
1002
- const vertical = (p) => p === null ? null : p === "top" || p === "bottom";
1003
1085
  const startV = vertical(c.start.port) ?? !horizontal;
1004
1086
  const endV = vertical(c.end.port) ?? !horizontal;
1005
1087
  if (startV && endV) {
@@ -1579,10 +1661,12 @@ function dashPattern(dash, width) {
1579
1661
  return [0.01, Math.max(5, width * 2.6)];
1580
1662
  }
1581
1663
  }
1582
- function connectorParts(e, index) {
1664
+ function connectorParts(e, index, page) {
1583
1665
  const c = e.connector;
1584
1666
  if (!c) return [new Path(), new Path()];
1585
1667
  const [a, b] = endpoints(e, index);
1668
+ const r = route(e, index);
1669
+ const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
1586
1670
  const key = [
1587
1671
  e.seed,
1588
1672
  e.style.roughness,
@@ -1596,28 +1680,166 @@ function connectorParts(e, index) {
1596
1680
  a.x,
1597
1681
  a.y,
1598
1682
  b.x,
1599
- b.y
1683
+ b.y,
1684
+ JSON.stringify(c.waypoints),
1685
+ JSON.stringify(hops)
1600
1686
  ].join("|");
1601
- return connectors.get(key, () => computeConnector(e, index));
1687
+ return connectors.get(key, () => computeConnector(e, r, hops));
1688
+ }
1689
+ function hopRadius(strokeWidth) {
1690
+ return Math.max(8, strokeWidth * 4);
1691
+ }
1692
+ function hopSamples(r) {
1693
+ const pts = r.points, out = [];
1694
+ if (r.kind === "polyline") {
1695
+ for (let i = 0; i < pts.length - 1; i++) out.push([pts[i], pts[i + 1]]);
1696
+ return out;
1697
+ }
1698
+ for (let s = 0; s < Math.floor((pts.length - 1) / 3); s++) {
1699
+ const seg = pts.slice(3 * s, 3 * s + 4), samples = [];
1700
+ for (let i = 0; i <= 24; i++) samples.push(cubicPoint(seg, i / 24));
1701
+ out.push(samples);
1702
+ }
1703
+ return out;
1704
+ }
1705
+ function runningLengths(samples) {
1706
+ const out = [0];
1707
+ for (let i = 1; i < samples.length; i++) out.push(out[i - 1] + Math.hypot(samples[i].x - samples[i - 1].x, samples[i].y - samples[i - 1].y));
1708
+ return out;
1709
+ }
1710
+ function cubicPoint(p, t) {
1711
+ const u = 1 - t;
1712
+ return {
1713
+ x: u * u * u * p[0].x + 3 * u * u * t * p[1].x + 3 * u * t * t * p[2].x + t * t * t * p[3].x,
1714
+ y: u * u * u * p[0].y + 3 * u * u * t * p[1].y + 3 * u * t * t * p[2].y + t * t * t * p[3].y
1715
+ };
1716
+ }
1717
+ function subCubic(p, t0, t1) {
1718
+ const split = (q, t) => {
1719
+ const lerp = (a, b) => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });
1720
+ const ab = lerp(q[0], q[1]), bc = lerp(q[1], q[2]), cd = lerp(q[2], q[3]);
1721
+ const abc = lerp(ab, bc), bcd = lerp(bc, cd), m = lerp(abc, bcd);
1722
+ return [[q[0], ab, abc, m], [m, bcd, cd, q[3]]];
1723
+ };
1724
+ const left = split(p, t1)[0];
1725
+ return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
1726
+ }
1727
+ function hopCrossings(e, r, page, index) {
1728
+ const segments = hopSamples(r);
1729
+ if (segments.length === 0) return [];
1730
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
1731
+ const beneath = [];
1732
+ for (const o of orderedElements(page)) {
1733
+ if (o.id === e.id) break;
1734
+ if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
1735
+ beneath.push(routePoints(route(o, index)));
1736
+ }
1737
+ const rad = hopRadius(e.style.strokeWidth);
1738
+ const out = [];
1739
+ for (const samples of segments) {
1740
+ const along = runningLengths(samples);
1741
+ const length = along[along.length - 1];
1742
+ const found = [];
1743
+ for (let i = 0; i < samples.length - 1; i++) {
1744
+ const p = samples[i], q = samples[i + 1];
1745
+ const dx = q.x - p.x, dy = q.y - p.y;
1746
+ for (const line2 of beneath) {
1747
+ for (let k = 0; k < line2.length - 1; k++) {
1748
+ const a = line2[k], b = line2[k + 1];
1749
+ const ex = b.x - a.x, ey = b.y - a.y;
1750
+ const det = dx * ey - dy * ex;
1751
+ if (Math.abs(det) < 1e-9) continue;
1752
+ const wx = a.x - p.x, wy = a.y - p.y;
1753
+ const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
1754
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
1755
+ }
1756
+ }
1757
+ }
1758
+ const kept = [];
1759
+ for (const at of found.sort((x, y) => x - y)) {
1760
+ if (at < rad + 2 || at > length - rad - 2) continue;
1761
+ if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
1762
+ kept.push(at);
1763
+ }
1764
+ out.push(kept);
1765
+ }
1766
+ return out;
1602
1767
  }
1603
- function computeConnector(e, index) {
1768
+ function computeConnector(e, r, hops) {
1604
1769
  const c = e.connector;
1605
1770
  const rng = new SeededRNG(e.seed);
1606
1771
  const rough = e.style.roughness, bow = e.style.bowing;
1607
1772
  const path = new Path(), solid = new Path();
1608
- const r = route(e, index);
1773
+ const w = rough * 3;
1774
+ const j = () => rng.signed() * w;
1775
+ const hop = (s, t, u, rad2) => {
1776
+ let n = { x: u.y, y: -u.x };
1777
+ if (n.y > 1e-9 || Math.abs(n.y) <= 1e-9 && n.x > 0) n = { x: -n.x, y: -n.y };
1778
+ const k = rad2 * 0.5523;
1779
+ const m = { x: (s.x + t.x) / 2, y: (s.y + t.y) / 2 };
1780
+ const top = { x: m.x + n.x * rad2, y: m.y + n.y * rad2 };
1781
+ path.moveTo(s);
1782
+ const a1x = s.x + n.x * k + j(), a1y = s.y + n.y * k + j(), a2x = top.x - u.x * k + j(), a2y = top.y - u.y * k + j();
1783
+ path.curveTo(top, { x: a1x, y: a1y }, { x: a2x, y: a2y });
1784
+ const b1x = top.x + u.x * k + j(), b1y = top.y + u.y * k + j(), b2x = t.x + n.x * k + j(), b2y = t.y + n.y * k + j();
1785
+ path.curveTo(t, { x: b1x, y: b1y }, { x: b2x, y: b2y });
1786
+ };
1787
+ const rad = hopRadius(e.style.strokeWidth);
1609
1788
  if (r.kind === "polyline") {
1610
1789
  const pts2 = r.points;
1790
+ const straight = (p, q) => {
1791
+ if (rough <= 0) path.moveTo(p).lineTo(q);
1792
+ else line(p, q, rng, rough, bow, true, path);
1793
+ };
1611
1794
  for (let i = 0; i < Math.max(pts2.length - 1, 0); i++) {
1612
- if (rough <= 0) path.moveTo(pts2[i]).lineTo(pts2[i + 1]);
1613
- else line(pts2[i], pts2[i + 1], rng, rough, bow, true, path);
1795
+ const p = pts2[i], q = pts2[i + 1];
1796
+ const length = Math.hypot(q.x - p.x, q.y - p.y);
1797
+ const u = length > 0 ? { x: (q.x - p.x) / length, y: (q.y - p.y) / length } : { x: 0, y: 0 };
1798
+ const at = (d) => ({ x: p.x + u.x * d, y: p.y + u.y * d });
1799
+ let from = p;
1800
+ for (const d of i < hops.length ? hops[i] : []) {
1801
+ const s = at(d - rad), t = at(d + rad);
1802
+ straight(from, s);
1803
+ hop(s, t, u, rad);
1804
+ from = t;
1805
+ }
1806
+ straight(from, q);
1614
1807
  }
1615
1808
  } else {
1616
- path.moveTo(r.a);
1617
- const w = rough * 3;
1618
- const j = () => rng.signed() * w;
1619
- const c1x = r.b.x + j(), c1y = r.b.y + j(), c2x = r.c.x + j(), c2y = r.c.y + j();
1620
- path.curveTo(r.d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1809
+ const pts2 = r.points;
1810
+ const samples = hopSamples(r);
1811
+ path.moveTo(pts2[0]);
1812
+ for (let s = 0; s < Math.floor((pts2.length - 1) / 3); s++) {
1813
+ const b = pts2[3 * s + 1], c2 = pts2[3 * s + 2], d = pts2[3 * s + 3];
1814
+ const c1x = b.x + j(), c1y = b.y + j(), c2x = c2.x + j(), c2y = c2.y + j();
1815
+ const here = s < hops.length ? hops[s] : [];
1816
+ if (here.length === 0) {
1817
+ path.curveTo(d, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1818
+ continue;
1819
+ }
1820
+ const cubic = [pts2[3 * s], { x: c1x, y: c1y }, { x: c2x, y: c2y }, d];
1821
+ const along = runningLengths(samples[s]);
1822
+ const param = (at) => {
1823
+ let i = 0;
1824
+ while (i < along.length - 2 && along[i + 1] < at) i++;
1825
+ const span = along[i + 1] - along[i];
1826
+ return (i + (span > 0 ? (at - along[i]) / span : 0)) / 24;
1827
+ };
1828
+ let from = 0;
1829
+ for (const at of here) {
1830
+ const t0 = param(at - rad), t1 = param(at + rad);
1831
+ const piece = subCubic(cubic, from, t0);
1832
+ if (s > 0 || from > 0) path.moveTo(piece[0]);
1833
+ path.curveTo(piece[3], piece[1], piece[2]);
1834
+ const a = cubicPoint(cubic, t0), z = cubicPoint(cubic, t1);
1835
+ const width = Math.hypot(z.x - a.x, z.y - a.y);
1836
+ const u = width > 0 ? { x: (z.x - a.x) / width, y: (z.y - a.y) / width } : { x: 0, y: 0 };
1837
+ hop(a, z, u, width / 2);
1838
+ from = t1;
1839
+ }
1840
+ const rest = subCubic(cubic, from, 1);
1841
+ path.moveTo(rest[0]).curveTo(rest[3], rest[1], rest[2]);
1842
+ }
1621
1843
  }
1622
1844
  const pts = routePoints(r);
1623
1845
  const size = Math.max(10, e.style.strokeWidth * 5);
@@ -1639,10 +1861,10 @@ function computeConnector(e, index) {
1639
1861
  solid.moveTo(tip).lineTo(w1).lineTo(w2).close();
1640
1862
  break;
1641
1863
  case "dot": {
1642
- const rad = size * 0.32;
1864
+ const rad2 = size * 0.32;
1643
1865
  const ang = Math.atan2(tip.y - from.y, tip.x - from.x);
1644
- const cx = tip.x - rad * Math.cos(ang), cy = tip.y - rad * Math.sin(ang);
1645
- solid.addEllipse({ x: cx - rad, y: cy - rad, width: 2 * rad, height: 2 * rad });
1866
+ const cx = tip.x - rad2 * Math.cos(ang), cy = tip.y - rad2 * Math.sin(ang);
1867
+ solid.addEllipse({ x: cx - rad2, y: cy - rad2, width: 2 * rad2, height: 2 * rad2 });
1646
1868
  break;
1647
1869
  }
1648
1870
  case "bar": {
@@ -2109,7 +2331,12 @@ function placed(inner, instance, symbol) {
2109
2331
  const q = point(p);
2110
2332
  return { elementID: null, port: p.port, x: q.x, y: q.y };
2111
2333
  };
2112
- e.connector = { ...inner.connector, start: end(inner.connector.start), end: end(inner.connector.end) };
2334
+ e.connector = {
2335
+ ...inner.connector,
2336
+ start: end(inner.connector.start),
2337
+ end: end(inner.connector.end),
2338
+ waypoints: inner.connector.waypoints?.map(point) ?? null
2339
+ };
2113
2340
  }
2114
2341
  return e;
2115
2342
  }
@@ -2885,7 +3112,7 @@ function svgParts(page, background, fixed, links = false) {
2885
3112
  if (!visible.has(e.layerID)) continue;
2886
3113
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
2887
3114
  if (alpha <= 0) continue;
2888
- let body = svgElement(e, index, bg);
3115
+ let body = svgElement(e, index, bg, d);
2889
3116
  if (angle(e) !== 0 && e.kind !== "connector") {
2890
3117
  body = `<g transform="rotate(${fmt(angle(e) * 180 / Math.PI)} ${fmt(midX(frame(e)))} ${fmt(midY(frame(e)))})">
2891
3118
  ${body}</g>
@@ -2920,7 +3147,7 @@ function typed(s, progress) {
2920
3147
  function revealProgress(e) {
2921
3148
  return e.reveal !== null && e.reveal < 1 ? Math.max(0, e.reveal) : null;
2922
3149
  }
2923
- function svgElement(e, index, labelBackground) {
3150
+ function svgElement(e, index, labelBackground, page) {
2924
3151
  const stroke = color(e.style.stroke);
2925
3152
  let strokeAttrs = `stroke="${stroke}" stroke-width="${fmt(e.style.strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"`;
2926
3153
  const dash = dashPattern(e.style.dash, e.style.strokeWidth);
@@ -2931,7 +3158,7 @@ function svgElement(e, index, labelBackground) {
2931
3158
  if (r !== null) {
2932
3159
  if (e.kind === "text") text = typed(text ?? "", r);
2933
3160
  else if (e.kind === "connector") {
2934
- return `<path d="${connectorParts(e, index)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
3161
+ return `<path d="${connectorParts(e, index, page)[0].trimmed(r).data()}" fill="none" ${strokeAttrs}/>
2935
3162
  `;
2936
3163
  } else {
2937
3164
  return `<path d="${outline(e).trimmed(r).data()}" fill="none" ${strokeAttrs}/>
@@ -2940,7 +3167,7 @@ function svgElement(e, index, labelBackground) {
2940
3167
  }
2941
3168
  switch (e.kind) {
2942
3169
  case "connector": {
2943
- const [line2, filled2] = connectorParts(e, index);
3170
+ const [line2, filled2] = connectorParts(e, index, page);
2944
3171
  let s = `<path d="${line2.data()}" fill="none" ${strokeAttrs}/>
2945
3172
  `;
2946
3173
  if (!filled2.isEmpty) s += `<path d="${filled2.data()}" fill="${stroke}" stroke="none"/>
@@ -3023,7 +3250,7 @@ function textElement(text, f, e, centered) {
3023
3250
  const total = lineHeight * lines.length;
3024
3251
  const x = centered ? midX(f) : f.x;
3025
3252
  const top = centered ? midY(f) - total / 2 : f.y;
3026
- let s = `<text x="${fmt(x)}" font-family="${cssFamily(e.style.font)}" font-size="${fmt(size)}" fill="${color(e.style.stroke)}"${centered ? ` text-anchor="middle"` : ""}>`;
3253
+ let s = `<text x="${fmt(x)}" font-family="${cssFamily(e.style.font)}" font-size="${fmt(size)}" fill="${color(labelColor(e))}"${centered ? ` text-anchor="middle"` : ""}>`;
3027
3254
  lines.forEach((line2, i) => {
3028
3255
  s += `<tspan x="${fmt(x)}" y="${fmt(top + lineHeight * (i + 0.8))}">${escapeXML(line2)}</tspan>`;
3029
3256
  });
@@ -3672,6 +3899,7 @@ function inspectFrame(page, t) {
3672
3899
  d.strokeWidth = r4(e.style.strokeWidth);
3673
3900
  d.stroke = rgba2(e.style.stroke);
3674
3901
  d.fill = rgba2(e.style.fill);
3902
+ if (e.style.textColor !== null) d.textColor = rgba2(e.style.textColor);
3675
3903
  if (e.kind === "connector") {
3676
3904
  const [a, b] = endpoints(e, index);
3677
3905
  d.start = pt(a);
@@ -3715,7 +3943,7 @@ function render(source, options = {}) {
3715
3943
  function isFile(v) {
3716
3944
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3717
3945
  }
3718
- var VERSION = "0.6.2";
3946
+ var VERSION = "0.6.3";
3719
3947
  export {
3720
3948
  BOIL_DRAWINGS,
3721
3949
  BUNDLED_FONTS,
@@ -6,18 +6,18 @@ export declare function portPoint(shape: Element, port: Port): Point;
6
6
  /** Where the line from the shape's center toward `target` crosses its outline. */
7
7
  export declare function edgePoint(shape: Element, toward: Point): Point;
8
8
  export declare function endpoints(e: Element, index: Index): [Point, Point];
9
+ /** A curve is joined cubic Béziers: the start, then two controls and an end per segment. */
9
10
  export type Route = {
10
11
  kind: "polyline";
11
12
  points: Point[];
12
13
  } | {
13
- kind: "cubic";
14
- a: Point;
15
- b: Point;
16
- c: Point;
17
- d: Point;
14
+ kind: "curve";
15
+ points: Point[];
18
16
  };
19
- /** A route's centerline as points; a cubic is sampled at 25 points. */
17
+ /** A route's centerline as points; each cubic segment is sampled at 25 points. */
20
18
  export declare function routePoints(r: Route): Point[];
19
+ /** Drops repeated points and the middle of three in a line, so dragged bends that fall into line disappear. */
20
+ export declare function simplified(pts: Point[]): Point[];
21
21
  export declare function route(e: Element, index: Index): Route;
22
22
  /** Arrowhead wing points for a tip at `tip` arriving from `from`. */
23
23
  export declare function arrowhead(tip: Point, from: Point, size: number): [Point, Point];
@@ -11,6 +11,9 @@ export declare const unrotated: (e: Element, p: Point) => Point;
11
11
  export declare function rotatedBounds(e: Element): Rect;
12
12
  /** The element with automatic ink drawn as `ink`; a light ink is not used on a light, mostly opaque fill. */
13
13
  export declare function resolvingInk(e: Element, ink: RGBA): Element;
14
+ /** What the element's text is drawn in: a text box's and a note's stroke, any other label's `textColor` or,
15
+ * without one, its stroke. */
16
+ export declare function labelColor(e: Element): RGBA;
14
17
  export interface Contour {
15
18
  closed: boolean;
16
19
  points: Point[];
@@ -20,4 +20,4 @@ export interface RenderOptions {
20
20
  /** One page of a drawing as an SVG document, as Dottie's own SVG export draws it. */
21
21
  export declare function render(source: string | DottieFile | unknown, options?: RenderOptions): string;
22
22
  /** The library's version. */
23
- export declare const VERSION = "0.6.2";
23
+ export declare const VERSION = "0.6.3";
@@ -27,6 +27,8 @@ export interface Style {
27
27
  opacity: number | null;
28
28
  /** How far into its dash pattern a dashed or dotted line starts; marching ants set it on every frame. */
29
29
  dashOffset: number | null;
30
+ /** The color of a shape's or connector's label; null follows `stroke`. Text boxes and notes ignore it. */
31
+ textColor: RGBA | null;
30
32
  }
31
33
  /** `portIndex` pins the end to one of the bound element's `ports`, and wins over `port`. */
32
34
  export interface Endpoint {
@@ -49,6 +51,10 @@ export interface ConnectorData {
49
51
  routing: Routing;
50
52
  startHead: Arrowhead;
51
53
  endHead: Arrowhead;
54
+ /** Bends on the canvas the line passes through, start to end. */
55
+ waypoints: StrokePoint[] | null;
56
+ /** Hops where the line crosses a connector drawn beneath it. */
57
+ hops: boolean | null;
52
58
  }
53
59
  export interface PathSubpath {
54
60
  closed: boolean;
@@ -1,5 +1,5 @@
1
- import { type Index } from "./connector.ts";
2
- import type { DashStyle, Element } from "./model.ts";
1
+ import { type Index, type Route } from "./connector.ts";
2
+ import { type DashStyle, type Element, type Page } from "./model.ts";
3
3
  import { Path, type Point } from "./path.ts";
4
4
  /** The `.grid` cell size actually drawn. */
5
5
  export declare const cellSize: (e: Element) => number;
@@ -13,5 +13,18 @@ export declare function fillPath(e: Element): Path;
13
13
  export declare function hachurePath(e: Element): Path;
14
14
  /** Dash lengths for a stroke width; null for a solid line. */
15
15
  export declare function dashPattern(dash: DashStyle, width: number): number[] | null;
16
- /** A connector as the stroked line (with open arrowheads) and the closed heads filled in the stroke color. */
17
- export declare function connectorParts(e: Element, index: Index): [Path, Path];
16
+ /**
17
+ * A connector as the stroked line (with open arrowheads) and the closed heads filled in the stroke color. `page`
18
+ * holds the connectors a hopping line hops over; without it a line draws no hops.
19
+ */
20
+ export declare function connectorParts(e: Element, index: Index, page?: Page): [Path, Path];
21
+ /** How big a hop is for a line `strokeWidth` wide: its radius. */
22
+ export declare function hopRadius(strokeWidth: number): number;
23
+ /** The points a route's segments are measured along: a straight segment's two ends, or a cubic sampled at 25. */
24
+ export declare function hopSamples(r: Route): Point[][];
25
+ /**
26
+ * Where a hopping connector's segments (straight, or each cubic of a curve) cross the connectors drawn beneath it
27
+ * (earlier on a visible layer): for each segment, the distances along it, ascending, leaving out crossings too close
28
+ * to its ends or the hop before.
29
+ */
30
+ export declare function hopCrossings(e: Element, r: Route, page: Page, index: Index): number[][];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phin-tech/dottie",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Render, play and compile Dottie (.dottie) drawings and animations in the browser",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",