@phin-tech/dottie 0.8.0 → 0.9.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.
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.8.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.9.0 | 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 __export = (target, all) => {
@@ -609,15 +609,15 @@ var Reader = class {
609
609
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
610
610
  stableSortBy(keys, (k) => k.t);
611
611
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
612
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
613
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
614
- return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
612
+ const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
613
+ if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
614
+ return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
615
615
  }) : null;
616
616
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
617
- const start = num(r.start), end = num(r.end);
618
- if (start === void 0 || end === void 0) return [];
617
+ const start2 = num(r.start), end = num(r.end);
618
+ if (start2 === void 0 || end === void 0) return [];
619
619
  const count = num(r.count);
620
- const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
620
+ const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
621
621
  return repeatIsValid(rep) ? [rep] : [];
622
622
  }) : null;
623
623
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -842,6 +842,21 @@ function pieces(para) {
842
842
  if (cur || out.length === 0) out.push(cur);
843
843
  return out;
844
844
  }
845
+ function prefixWidths(chars, fontSize, face, font) {
846
+ const fam = familyMetrics(font);
847
+ const f = fam.faces[face] ?? fam.faces.regular;
848
+ const fallback = f.w["n"];
849
+ const out = [0];
850
+ let w = 0;
851
+ let prev = "";
852
+ for (const ch of chars) {
853
+ w += f.w[ch] ?? fallback;
854
+ if (prev) w += f.k[prev + ch] ?? 0;
855
+ prev = ch;
856
+ out.push(w * fontSize / 1e3);
857
+ }
858
+ return out;
859
+ }
845
860
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
846
861
  const width = (t) => measure(t, fontSize, face, font);
847
862
  const out = [];
@@ -857,8 +872,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
857
872
  }
858
873
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
859
874
  const chars = [...line2];
875
+ const prefix = prefixWidths(chars, fontSize, face, font);
860
876
  let n = chars.length - 1;
861
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
877
+ while (n > 1 && prefix[n] > maxWidth) n--;
862
878
  out.push(chars.slice(0, n).join(""));
863
879
  line2 = chars.slice(n).join("");
864
880
  }
@@ -1013,13 +1029,13 @@ var Path = class _Path {
1013
1029
  contours() {
1014
1030
  const out = [];
1015
1031
  let current = [];
1016
- let start = { x: 0, y: 0 };
1032
+ let start2 = { x: 0, y: 0 };
1017
1033
  for (const s of this.segments) {
1018
- const last = current.length ? current[current.length - 1] : start;
1034
+ const last = current.length ? current[current.length - 1] : start2;
1019
1035
  switch (s.op) {
1020
1036
  case "M":
1021
1037
  if (current.length > 1) out.push(current);
1022
- start = s.p;
1038
+ start2 = s.p;
1023
1039
  current = [s.p];
1024
1040
  break;
1025
1041
  case "L":
@@ -1041,9 +1057,9 @@ var Path = class _Path {
1041
1057
  }
1042
1058
  break;
1043
1059
  case "Z":
1044
- current.push(start);
1060
+ current.push(start2);
1045
1061
  if (current.length > 1) out.push(current);
1046
- current = [start];
1062
+ current = [start2];
1047
1063
  break;
1048
1064
  }
1049
1065
  }
@@ -1435,9 +1451,17 @@ function midpoint(e, index) {
1435
1451
  }
1436
1452
  return pts[0];
1437
1453
  }
1454
+ var indexes = /* @__PURE__ */ new WeakMap();
1455
+ function isIndexOf(page, index) {
1456
+ return indexes.get(page.elements)?.index === index;
1457
+ }
1438
1458
  function indexOf(page) {
1459
+ const els = page.elements;
1460
+ const hit = indexes.get(els);
1461
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.index;
1439
1462
  const m = /* @__PURE__ */ new Map();
1440
- for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
1463
+ for (const e of els) if (!m.has(e.id)) m.set(e.id, e);
1464
+ indexes.set(els, { members: [...els], index: m });
1441
1465
  return m;
1442
1466
  }
1443
1467
 
@@ -1520,43 +1544,112 @@ function packetID(flow, k) {
1520
1544
  return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1521
1545
  }
1522
1546
  var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1523
- function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1524
- const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1525
- if (!shapes2.has(from)) return null;
1526
- const next = /* @__PURE__ */ new Map();
1527
- const add2 = (a, edge) => {
1528
- const list = next.get(a);
1529
- if (list) list.push(edge);
1530
- else next.set(a, [edge]);
1531
- };
1532
- for (const e of frame2.elements) {
1547
+ var graphs = /* @__PURE__ */ new WeakMap();
1548
+ var networks = /* @__PURE__ */ new Map();
1549
+ function graphOf(frame2) {
1550
+ const els = frame2.elements;
1551
+ const hit = graphs.get(els);
1552
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.graph;
1553
+ const shapes2 = new Set(els.filter((e) => e.kind !== "connector").map((e) => e.id));
1554
+ const lines = [];
1555
+ for (const e of els) {
1533
1556
  const c = e.connector;
1534
- const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1535
- if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1536
- const cost = routeCost(c.cost);
1537
- add2(a, { to: b, via: e.id, back: false, cost });
1538
- if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1557
+ if (c) lines.push([e.id, c.start.elementID, c.end.elementID, String(routeCost(c.cost)), c.startHead !== "none" ? "both" : "one"]);
1558
+ }
1559
+ const key = JSON.stringify([[...shapes2], lines]);
1560
+ let graph = networks.get(key);
1561
+ if (!graph) {
1562
+ const next = /* @__PURE__ */ new Map();
1563
+ const add2 = (a, edge) => {
1564
+ const list = next.get(a);
1565
+ if (list) list.push(edge);
1566
+ else next.set(a, [edge]);
1567
+ };
1568
+ for (const e of els) {
1569
+ const c = e.connector;
1570
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1571
+ if (!c || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1572
+ const cost = routeCost(c.cost);
1573
+ add2(a, { to: b, via: e.id, back: false, cost });
1574
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1575
+ }
1576
+ graph = { shapes: shapes2, next, trees: /* @__PURE__ */ new Map() };
1577
+ if (networks.size >= 16) networks.clear();
1578
+ networks.set(key, graph);
1579
+ }
1580
+ graphs.set(els, { members: [...els], graph });
1581
+ return graph;
1582
+ }
1583
+ var Frontier = class {
1584
+ constructor() {
1585
+ __publicField(this, "heap", []);
1586
+ }
1587
+ get size() {
1588
+ return this.heap.length;
1589
+ }
1590
+ less(i, j) {
1591
+ const a = this.heap[i], b = this.heap[j];
1592
+ return a.dist < b.dist || a.dist === b.dist && a.order < b.order;
1593
+ }
1594
+ swap(i, j) {
1595
+ [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
1596
+ }
1597
+ push(id, dist, order) {
1598
+ this.heap.push({ id, dist, order });
1599
+ for (let i = this.heap.length - 1; i > 0; ) {
1600
+ const up = i - 1 >> 1;
1601
+ if (!this.less(i, up)) break;
1602
+ this.swap(i, up);
1603
+ i = up;
1604
+ }
1605
+ }
1606
+ pop() {
1607
+ const top = this.heap[0], last = this.heap.pop();
1608
+ if (this.heap.length > 0) {
1609
+ this.heap[0] = last;
1610
+ for (let i = 0; ; ) {
1611
+ const l = 2 * i + 1, r = l + 1;
1612
+ let m = i;
1613
+ if (l < this.heap.length && this.less(l, m)) m = l;
1614
+ if (r < this.heap.length && this.less(r, m)) m = r;
1615
+ if (m === i) break;
1616
+ this.swap(i, m);
1617
+ i = m;
1618
+ }
1619
+ }
1620
+ return top;
1539
1621
  }
1622
+ };
1623
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set(), until) {
1624
+ const graph = graphOf(frame2);
1625
+ const key = JSON.stringify([from, until ?? null, [...offline].sort()]);
1626
+ if (graph.trees.has(key)) return graph.trees.get(key);
1627
+ const tree = grow(graph, from, offline, until);
1628
+ if (graph.trees.size >= 4096) graph.trees.clear();
1629
+ graph.trees.set(key, tree);
1630
+ return tree;
1631
+ }
1632
+ function grow({ shapes: shapes2, next }, from, offline, until) {
1633
+ if (!shapes2.has(from) || offline.has(from)) return null;
1540
1634
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1541
1635
  let counter = 1;
1542
1636
  const tree = new FlowTree(from);
1543
- const open = [from];
1544
- while (open.length > 0) {
1545
- let best = 0;
1546
- for (let i = 1; i < open.length; i++) {
1547
- const da = dist.get(open[i]), db = dist.get(open[best]);
1548
- if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1549
- }
1550
- const n = open.splice(best, 1)[0];
1637
+ const open = new Frontier();
1638
+ open.push(from, 0, 0);
1639
+ while (open.size > 0) {
1640
+ const { id: n, order: o } = open.pop();
1641
+ if (tree.settled.has(n) || order.get(n) !== o) continue;
1551
1642
  tree.settled.add(n);
1643
+ if (n === until && n !== from) break;
1552
1644
  for (const edge of next.get(n) ?? []) {
1645
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1553
1646
  if (tree.settled.has(edge.to)) continue;
1554
1647
  const d = dist.get(n) + edge.cost;
1555
1648
  const old = dist.get(edge.to);
1556
1649
  if (old !== void 0 && old <= d) continue;
1557
- if (old === void 0) open.push(edge.to);
1558
1650
  dist.set(edge.to, d);
1559
- order.set(edge.to, counter++);
1651
+ order.set(edge.to, counter);
1652
+ open.push(edge.to, d, counter++);
1560
1653
  tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1561
1654
  }
1562
1655
  }
@@ -1636,8 +1729,16 @@ function delayOf(e) {
1636
1729
  var queueKey = (shape, flow) => shape + "|" + flow;
1637
1730
  function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1638
1731
  const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1639
- const byID = /* @__PURE__ */ new Map();
1640
- for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1732
+ const byID = indexOf(frame2);
1733
+ const traced = /* @__PURE__ */ new Map();
1734
+ const traceOf = (c) => {
1735
+ let pts = traced.get(c.id);
1736
+ if (!pts) {
1737
+ pts = tracing(c, frame2, byID).points;
1738
+ traced.set(c.id, pts);
1739
+ }
1740
+ return pts;
1741
+ };
1641
1742
  const metrics2 = emptyMetrics();
1642
1743
  for (const e of frame2.elements) {
1643
1744
  if (e.kind === "connector" && !e.connector) continue;
@@ -1667,7 +1768,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1667
1768
  hops.forEach(([id, back], k) => {
1668
1769
  const c = byID.get(id);
1669
1770
  if (!c) return;
1670
- const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
1771
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1671
1772
  if (line2.length === 0) return;
1672
1773
  const last = points[points.length - 1];
1673
1774
  if (last) {
@@ -1676,9 +1777,9 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1676
1777
  inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1677
1778
  length += gap;
1678
1779
  }
1679
- const start = length;
1780
+ const start2 = length;
1680
1781
  for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
1681
- stretches.push([id, start, length]);
1782
+ stretches.push([id, start2, length]);
1682
1783
  points.push(...line2);
1683
1784
  });
1684
1785
  return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
@@ -1713,22 +1814,27 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1713
1814
  const packets = [];
1714
1815
  const waitingBy = /* @__PURE__ */ new Map();
1715
1816
  const everyOf = (flow) => Math.min(Math.max(clamp(flow.every, DEFAULT_EVERY, 0.05, 60) * biggest / Math.max(amountIn(flow, values), TINY), 0.05), 60);
1817
+ const bases = /* @__PURE__ */ new Map();
1818
+ const baseTree = (from) => {
1819
+ if (!bases.has(from)) bases.set(from, flowTree(from, frame2, offline));
1820
+ return bases.get(from);
1821
+ };
1716
1822
  for (const [i, flow] of list.entries()) {
1717
1823
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1718
1824
  const source = byID.get(flow.from);
1719
- const base2 = source ? flowTree(flow.from, frame2, offline) : null;
1825
+ const base2 = source ? baseTree(flow.from) : null;
1720
1826
  if (!source || !base2) continue;
1721
1827
  const volume = amountIn(flow, values);
1722
1828
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1723
1829
  const placements = [];
1724
- const waiting = [];
1830
+ const waiting2 = [];
1725
1831
  const goals = base2.goals(flow.to, flow.spread === true);
1726
1832
  const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1727
1833
  for (const [candidates, share] of shares) {
1728
1834
  if (!(share > TINY)) continue;
1729
1835
  let left = share;
1730
1836
  for (let n = 0; n < 32 && left > TINY; n++) {
1731
- const tree = flowTree(flow.from, frame2, union2(offline, full()));
1837
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1732
1838
  const goal2 = tree?.goals(candidates, false)[0];
1733
1839
  if (!tree || goal2 === void 0) break;
1734
1840
  const r = route2(tree, goal2);
@@ -1768,18 +1874,18 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1768
1874
  placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1769
1875
  }
1770
1876
  }
1771
- const existing = waiting.find(([s]) => s === at);
1877
+ const existing = waiting2.find(([s]) => s === at);
1772
1878
  if (existing) existing[1] += left;
1773
- else waiting.push([at, left]);
1879
+ else waiting2.push([at, left]);
1774
1880
  }
1775
- waitingBy.set(flow.id, waiting);
1881
+ waitingBy.set(flow.id, waiting2);
1776
1882
  for (const p of placements) {
1777
1883
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1778
1884
  m.throughput += p.amount;
1779
1885
  });
1780
1886
  if (!p.waits) fm.placed += p.amount;
1781
1887
  }
1782
- for (const [at, a] of waiting) {
1888
+ for (const [at, a] of waiting2) {
1783
1889
  bump(at, (m) => {
1784
1890
  m.backlog += a;
1785
1891
  });
@@ -1824,10 +1930,12 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1824
1930
  metrics2.flows.set(flow.id, fm);
1825
1931
  }
1826
1932
  const piles = /* @__PURE__ */ new Map();
1933
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1827
1934
  for (const flow of list) {
1828
- const waiting = waitingBy.get(flow.id) ?? [];
1829
- const shapes2 = waiting.map(([s]) => s);
1830
- for (const e of frame2.elements) {
1935
+ const waiting2 = waitingBy.get(flow.id) ?? [];
1936
+ const shapes2 = waiting2.map(([s]) => s);
1937
+ const suffix = "|" + flow.id;
1938
+ if (carriedKeys.some((k) => k.endsWith(suffix))) for (const e of frame2.elements) {
1831
1939
  const c = carried[queueKey(e.id, flow.id)];
1832
1940
  if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1833
1941
  }
@@ -1841,7 +1949,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1841
1949
  const shape = byID.get(at);
1842
1950
  if (!shape) return;
1843
1951
  const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1844
- const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
1952
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1845
1953
  const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1846
1954
  let depth = before + backlog * since;
1847
1955
  if (backlog <= TINY && before > TINY) {
@@ -1850,7 +1958,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1850
1958
  let total = 0;
1851
1959
  for (let n2 = 0; n2 < 32; n2++) {
1852
1960
  const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1853
- const tree = flowTree(at, frame2, union2(offline, blocked));
1961
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1854
1962
  const goal = tree?.goals(flow.to, false)[0];
1855
1963
  if (!tree || goal === void 0) break;
1856
1964
  const r = route2(tree, goal);
@@ -1963,21 +2071,21 @@ function metricLookup(m, frame2) {
1963
2071
  return id === null ? null : metricValue(m, term.metric, id);
1964
2072
  };
1965
2073
  }
1966
- function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2074
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
1967
2075
  const lookup = metricLookup(metrics2, frame2);
1968
- const out = [], readings = {};
1969
- for (let e of elements) {
2076
+ const plan2 = [], readings = {};
2077
+ for (const e of elements) {
2078
+ const applied = [];
1970
2079
  if (values !== null && e.overrides) {
1971
- for (const o of e.overrides) {
1972
- const own = e.id;
1973
- if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1974
- }
2080
+ e.overrides.forEach((o, k) => {
2081
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2082
+ });
1975
2083
  }
1976
- if (!e.gauge || values === null) {
1977
- out.push(e);
2084
+ const g = values === null ? null : e.gauge;
2085
+ if (!g) {
2086
+ plan2.push({ applied, gauge: null });
1978
2087
  continue;
1979
2088
  }
1980
- const g = e.gauge;
1981
2089
  let shown = metricValue(metrics2, g.metric, g.of);
1982
2090
  const then = carried[gaugeKey(e.id)];
1983
2091
  if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
@@ -1985,8 +2093,19 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
1985
2093
  shown = then + (shown - then) * smooth;
1986
2094
  }
1987
2095
  if (shown !== null) readings[gaugeKey(e.id)] = shown;
1988
- out.push(...gauge(e, g, shown, frame2, flows));
2096
+ plan2.push({ applied, gauge: { value: shown } });
1989
2097
  }
2098
+ return { plan: plan2, readings };
2099
+ }
2100
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2101
+ const { plan: plan2, readings } = finishPlan(elements, metrics2, values, frame2, since, carried);
2102
+ const out = [];
2103
+ elements.forEach((e, i) => {
2104
+ for (const k of plan2[i].applied) e = applyOverride(looksOnly(e.overrides[k]), e);
2105
+ const g = plan2[i].gauge;
2106
+ if (g && e.gauge) out.push(...gauge(e, e.gauge, g.value, frame2, flows));
2107
+ else out.push(e);
2108
+ });
1990
2109
  return { elements: out, readings };
1991
2110
  }
1992
2111
  var gaugeKey = (id) => "gauge|" + id;
@@ -2132,7 +2251,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2132
2251
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
2133
2252
  const dx = b.x - a.x, dy = b.y - a.y;
2134
2253
  const nx = -dy / length, ny = dx / length;
2135
- const start = preserveEnds ? a : (() => {
2254
+ const start2 = preserveEnds ? a : (() => {
2136
2255
  const x = a.x + j();
2137
2256
  return { x, y: a.y + j() };
2138
2257
  })();
@@ -2144,7 +2263,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2144
2263
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
2145
2264
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
2146
2265
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
2147
- into.moveTo(start);
2266
+ into.moveTo(start2);
2148
2267
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
2149
2268
  }
2150
2269
  }
@@ -2405,10 +2524,10 @@ var Cache = class {
2405
2524
  __publicField(this, "limit");
2406
2525
  this.limit = limit;
2407
2526
  }
2408
- get(key, make) {
2527
+ get(key, make2) {
2409
2528
  const hit = this.map.get(key);
2410
2529
  if (hit !== void 0) return hit;
2411
- const v = make();
2530
+ const v = make2();
2412
2531
  if (this.map.size >= this.limit) this.map.clear();
2413
2532
  this.map.set(key, v);
2414
2533
  return v;
@@ -2632,26 +2751,59 @@ function subCubic(p, t0, t1) {
2632
2751
  const left = split(p, t1)[0];
2633
2752
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
2634
2753
  }
2754
+ function boxOf(pts) {
2755
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
2756
+ for (const p of pts) {
2757
+ x0 = Math.min(x0, p.x);
2758
+ y0 = Math.min(y0, p.y);
2759
+ x1 = Math.max(x1, p.x);
2760
+ y1 = Math.max(y1, p.y);
2761
+ }
2762
+ return { x0, y0, x1, y1 };
2763
+ }
2764
+ function touching(a, b) {
2765
+ const eps = 1e-6;
2766
+ return a.x0 <= b.x1 + eps && b.x0 <= a.x1 + eps && a.y0 <= b.y1 + eps && b.y0 <= a.y1 + eps;
2767
+ }
2768
+ var hopFrames = /* @__PURE__ */ new WeakMap();
2769
+ function linesBeneath(e, page, index) {
2770
+ let f = hopFrames.get(index);
2771
+ if (!f || f.layers !== page.layers || !isIndexOf(page, index)) {
2772
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2773
+ const first = /* @__PURE__ */ new Map(), under = [];
2774
+ for (const o of orderedElements(page)) {
2775
+ if (!first.has(o.id)) first.set(o.id, under.length);
2776
+ if (o.kind === "connector" && visible.has(o.layerID)) under.push(o);
2777
+ }
2778
+ f = { layers: page.layers, first, under, lines: [] };
2779
+ if (isIndexOf(page, index)) hopFrames.set(index, f);
2780
+ }
2781
+ const count = f.first.get(e.id) ?? f.under.length;
2782
+ while (f.lines.length < count) {
2783
+ const points = routePoints(route(f.under[f.lines.length], index));
2784
+ f.lines.push({ points, box: boxOf(points) });
2785
+ }
2786
+ return { lines: f.lines, count };
2787
+ }
2635
2788
  function hopCrossings(e, r, page, index) {
2636
2789
  const segments = hopSamples(r);
2637
2790
  if (segments.length === 0) return [];
2638
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2639
- const beneath = [];
2640
- for (const o of orderedElements(page)) {
2641
- if (o.id === e.id) break;
2642
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
2643
- beneath.push(routePoints(route(o, index)));
2644
- }
2791
+ const { lines, count } = linesBeneath(e, page, index);
2645
2792
  const rad = hopRadius(e.style.strokeWidth);
2646
2793
  const out = [];
2647
2794
  for (const samples of segments) {
2648
2795
  const along = runningLengths(samples);
2649
2796
  const length = along[along.length - 1];
2650
2797
  const found = [];
2798
+ const near = [];
2799
+ const whole = boxOf(samples);
2800
+ for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
2651
2801
  for (let i = 0; i < samples.length - 1; i++) {
2652
2802
  const p = samples[i], q = samples[i + 1];
2653
2803
  const dx = q.x - p.x, dy = q.y - p.y;
2654
- for (const line2 of beneath) {
2804
+ const box = boxOf([p, q]);
2805
+ for (const { points: line2, box: lineBox } of near) {
2806
+ if (!touching(box, lineBox)) continue;
2655
2807
  for (let k = 0; k < line2.length - 1; k++) {
2656
2808
  const a = line2[k], b = line2[k + 1];
2657
2809
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2931,8 +3083,8 @@ function followSpan(track, t) {
2931
3083
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2932
3084
  return f;
2933
3085
  }
2934
- function tracing(e, page) {
2935
- if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
3086
+ function tracing(e, page, index = indexOf(page)) {
3087
+ if (e.kind === "connector") return new Polyline(routePoints(route(e, index)));
2936
3088
  const clean = cloneElement(e);
2937
3089
  clean.style.roughness = 0;
2938
3090
  const pts = outline(clean).contours()[0] ?? [];
@@ -2942,9 +3094,10 @@ function following(track, e, t, page) {
2942
3094
  const f = followSpan(track, t);
2943
3095
  if (!f) return e;
2944
3096
  t = localTime(track, t);
2945
- const pathElement = page.elements.find((x) => x.id === f.path);
3097
+ const index = indexOf(page);
3098
+ const pathElement = index.get(f.path);
2946
3099
  if (!pathElement || pathElement.id === e.id) return e;
2947
- const line2 = tracing(pathElement, page);
3100
+ const line2 = tracing(pathElement, page, index);
2948
3101
  const span = f.end - f.start;
2949
3102
  let u = Math.min(Math.max(applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1), 0), 1) * (f.laps ?? 1);
2950
3103
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -3199,17 +3352,30 @@ function symbolPage(s) {
3199
3352
  function hasInstances(page) {
3200
3353
  return page.elements.some((e) => e.kind === "symbol");
3201
3354
  }
3202
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map()) {
3355
+ var Expansions = class {
3356
+ constructor(page, t, posing) {
3357
+ __publicField(this, "page");
3358
+ __publicField(this, "t");
3359
+ __publicField(this, "posing");
3360
+ /** For each instance: its time override, the budget it found and left, and what it made. */
3361
+ __publicField(this, "made", /* @__PURE__ */ new Map());
3362
+ this.page = page;
3363
+ this.t = t;
3364
+ this.posing = posing;
3365
+ }
3366
+ };
3367
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo) {
3203
3368
  if (!hasInstances(page)) return page;
3204
3369
  const budget = { left: MAX_ELEMENTS };
3205
- return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times) };
3370
+ const m = memo && memo.page === page && memo.t === t && memo.posing === posing ? memo : void 0;
3371
+ return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3206
3372
  }
3207
3373
  function symbolTable(page) {
3208
3374
  const table = /* @__PURE__ */ new Map();
3209
3375
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3210
3376
  return table;
3211
3377
  }
3212
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map()) {
3378
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo) {
3213
3379
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3214
3380
  if (instances.size === 0) {
3215
3381
  budget.left -= elements.length;
@@ -3234,6 +3400,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3234
3400
  budget.left -= 1;
3235
3401
  continue;
3236
3402
  }
3403
+ const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3404
+ const was = memo?.made.get(e);
3405
+ if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3406
+ budget.left -= was.before - was.after;
3407
+ for (const x of was.out) out.push(x);
3408
+ continue;
3409
+ }
3410
+ const before = budget.left, from = out.length;
3237
3411
  budget.left -= 1;
3238
3412
  const id = e.symbol;
3239
3413
  const symbol = id === null ? void 0 : table.get(id);
@@ -3249,6 +3423,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3249
3423
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3250
3424
  };
3251
3425
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3426
+ if (memo && stack.length === 0 && !was) memo.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3252
3427
  }
3253
3428
  return out;
3254
3429
  }
@@ -3516,13 +3691,13 @@ function blocks(lines) {
3516
3691
  flush();
3517
3692
  return out;
3518
3693
  }
3519
- function parseList(lines, start) {
3520
- const first = reItem.exec(lines[start]);
3694
+ function parseList(lines, start2) {
3695
+ const first = reItem.exec(lines[start2]);
3521
3696
  const indent = first[1].length;
3522
3697
  const bullet = /^\d/.test(first[2]) ? null : first[2];
3523
3698
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
3524
3699
  const items = [];
3525
- let i = start;
3700
+ let i = start2;
3526
3701
  while (i < lines.length) {
3527
3702
  const m = reItem.exec(lines[i]);
3528
3703
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -3851,10 +4026,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
3851
4026
  let k = 0;
3852
4027
  let firstLine = true;
3853
4028
  while (k < chars.length || firstLine && chars.length === 0) {
3854
- const start = firstLine ? p.firstIndent : p.indent;
4029
+ const start2 = firstLine ? p.firstIndent : p.indent;
3855
4030
  const maxX2 = width;
3856
4031
  const glyphs = [];
3857
- let x = start, lastSpace = -1, end = k;
4032
+ let x = start2, lastSpace = -1, end = k;
3858
4033
  for (; end < chars.length; end++) {
3859
4034
  const { ch, run } = chars[end];
3860
4035
  const r = runs[run];
@@ -3903,7 +4078,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3903
4078
  }
3904
4079
  if (glyphs.length === 0 && end === k && k < chars.length) {
3905
4080
  const { ch, run } = chars[k];
3906
- glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run], fam) });
4081
+ glyphs.push({ ch, run, x: start2, w: charWidth(ch, runs[run], fam) });
3907
4082
  end = k + 1;
3908
4083
  }
3909
4084
  let asc = 0, desc = 0, lead = 0;
@@ -3929,13 +4104,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3929
4104
  });
3930
4105
  return lines;
3931
4106
  }
4107
+ var Memo = class {
4108
+ constructor(limit) {
4109
+ __publicField(this, "map", /* @__PURE__ */ new Map());
4110
+ __publicField(this, "limit");
4111
+ this.limit = limit;
4112
+ }
4113
+ get(key, make2) {
4114
+ const hit = this.map.get(key);
4115
+ if (hit !== void 0) return hit;
4116
+ const v = make2();
4117
+ if (this.map.size >= this.limit) this.map.clear();
4118
+ this.map.set(key, v);
4119
+ return v;
4120
+ }
4121
+ };
4122
+ var layouts = new Memo(500);
4123
+ var markups = new Memo(500);
3932
4124
  function noteMarkup(source, e) {
4125
+ const st = e.style, ink = st.stroke;
4126
+ const key = JSON.stringify([...[e.x, e.y, e.width, e.height, st.fontSize, ink.r, ink.g, ink.b, ink.a].map(String), st.font ?? "", source]);
4127
+ return markups.get(key, () => computeNoteMarkup(source, e));
4128
+ }
4129
+ function computeNoteMarkup(source, e) {
3933
4130
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3934
4131
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3935
4132
  const fam = familyMetrics(e.style.font);
3936
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3937
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3938
- const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4133
+ const layoutKey = JSON.stringify([...[rect2.width, rect2.height, e.style.fontSize].map(String), e.style.font ?? "", source]);
4134
+ const lines = layouts.get(layoutKey, () => {
4135
+ const b = new Builder(e.style.fontSize, rect2.width, fam);
4136
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
4137
+ return layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4138
+ });
3939
4139
  const ink = color(e.style.stroke);
3940
4140
  let s = "";
3941
4141
  const marks = [];
@@ -4024,6 +4224,26 @@ function bounds(page, padding = 24) {
4024
4224
  return inset(r, -padding, -padding);
4025
4225
  }
4026
4226
  function plan(page) {
4227
+ const hit = plans.get(page);
4228
+ if (hit && unchanged(page, hit.fields)) return hit.plan;
4229
+ const made = makePlan(page);
4230
+ plans.set(page, { fields: { ...page }, plan: made });
4231
+ return made;
4232
+ }
4233
+ var plans = /* @__PURE__ */ new WeakMap();
4234
+ function hasPlan(page) {
4235
+ const hit = plans.get(page);
4236
+ return !!hit && unchanged(page, hit.fields);
4237
+ }
4238
+ function rememberPlan(page, made) {
4239
+ plans.set(page, { fields: { ...page }, plan: made });
4240
+ }
4241
+ function unchanged(page, fields) {
4242
+ const a = page, b = fields;
4243
+ const keys = Object.keys(a);
4244
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
4245
+ }
4246
+ function makePlan(page) {
4027
4247
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
4028
4248
  const times = frameTimes(tl);
4029
4249
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -4051,8 +4271,8 @@ function svgOpen(b, background, paint) {
4051
4271
  }
4052
4272
  return out;
4053
4273
  }
4054
- function svgParts(page, background, fixed, links = false) {
4055
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
4274
+ function svgParts(page, background, fixed, links = false, only) {
4275
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
4056
4276
  const theme = themeNamed(page.theme);
4057
4277
  const bg = background ? theme?.background ?? background : null;
4058
4278
  const ink = theme?.ink ?? black;
@@ -4063,11 +4283,11 @@ function svgParts(page, background, fixed, links = false) {
4063
4283
  const index = indexOf(d);
4064
4284
  const ordered = orderedElements(d);
4065
4285
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
4066
- const rockMarkup = rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4286
+ const rockMarkup = only && !only("rock") ? "" : rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4067
4287
  `).join("");
4068
4288
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
4069
4289
  for (const e of ordered) {
4070
- if (!visible.has(e.layerID)) continue;
4290
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
4071
4291
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
4072
4292
  if (alpha <= 0) continue;
4073
4293
  let body = svgElement(e, index, bg, d);
@@ -4215,6 +4435,110 @@ function textElement(text, f, e, centered) {
4215
4435
  return s + "</text>\n";
4216
4436
  }
4217
4437
 
4438
+ // src/planner.ts
4439
+ var WORKER_WORK = 3e4;
4440
+ function planWork(page) {
4441
+ const tl = page.timeline;
4442
+ if (!tl || tl.camera?.some((k) => k.width > 1 && k.height > 1)) return 0;
4443
+ const elements = page.elements.length + (page.symbols ?? []).reduce((n, s) => n + s.elements.length, 0);
4444
+ return frameTimes(tl).length * elements;
4445
+ }
4446
+ function workerMaker() {
4447
+ if (typeof Worker === "undefined" || false) return null;
4448
+ if (false) {
4449
+ const source = `(${dottiePlayerScript.toString()})();
4450
+ `;
4451
+ return () => {
4452
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
4453
+ try {
4454
+ return new Worker(url);
4455
+ } finally {
4456
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
4457
+ }
4458
+ };
4459
+ }
4460
+ if (true) {
4461
+ const here = import.meta.url;
4462
+ if (!here) return null;
4463
+ return () => new Worker(new URL("./dottie-player.min.js", here));
4464
+ }
4465
+ return null;
4466
+ }
4467
+ var make;
4468
+ var worker = null;
4469
+ var failed = false;
4470
+ var nextID = 1;
4471
+ var waiting = /* @__PURE__ */ new Map();
4472
+ function plansInWorker(page) {
4473
+ if (failed || hasPlan(page) || planWork(page) < WORKER_WORK) return false;
4474
+ if (make === void 0) make = workerMaker();
4475
+ return make !== null;
4476
+ }
4477
+ function planInWorker(page) {
4478
+ return new Promise((resolve2) => {
4479
+ const done = (p) => {
4480
+ if (p) rememberPlan(page, p);
4481
+ resolve2();
4482
+ };
4483
+ let w;
4484
+ try {
4485
+ w = worker ?? start();
4486
+ } catch {
4487
+ failed = true;
4488
+ done(null);
4489
+ return;
4490
+ }
4491
+ const id = nextID++;
4492
+ waiting.set(id, { page, done });
4493
+ try {
4494
+ w.postMessage({ dottiePlan: id, page });
4495
+ } catch {
4496
+ waiting.delete(id);
4497
+ done(null);
4498
+ }
4499
+ });
4500
+ }
4501
+ function start() {
4502
+ const w = make();
4503
+ w.onmessage = (ev) => {
4504
+ const { dottiePlan: id, plan: p } = ev.data ?? {};
4505
+ const job = id === void 0 ? void 0 : waiting.get(id);
4506
+ if (!job) return;
4507
+ waiting.delete(id);
4508
+ job.done(p ?? null);
4509
+ if (waiting.size === 0 && worker === w) {
4510
+ w.terminate();
4511
+ worker = null;
4512
+ }
4513
+ };
4514
+ w.onerror = (ev) => {
4515
+ ev.preventDefault?.();
4516
+ failed = true;
4517
+ if (worker === w) worker = null;
4518
+ w.terminate();
4519
+ const jobs = [...waiting.values()];
4520
+ waiting.clear();
4521
+ for (const job of jobs) job.done(null);
4522
+ };
4523
+ worker = w;
4524
+ return w;
4525
+ }
4526
+ function servePlans() {
4527
+ const g = globalThis;
4528
+ if (typeof g.WorkerGlobalScope === "undefined" || !(g instanceof g.WorkerGlobalScope)) return;
4529
+ g.addEventListener("message", (ev) => {
4530
+ const id = ev.data?.dottiePlan;
4531
+ if (typeof id !== "number") return;
4532
+ let p = null;
4533
+ try {
4534
+ p = plan(ev.data.page);
4535
+ } catch {
4536
+ p = null;
4537
+ }
4538
+ g.postMessage({ dottiePlan: id, plan: p });
4539
+ });
4540
+ }
4541
+
4218
4542
  // src/player.ts
4219
4543
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4220
4544
  function pageIndex(file, spec) {
@@ -4284,7 +4608,7 @@ var Playback = class {
4284
4608
  __publicField(this, "order", []);
4285
4609
  __publicField(this, "frame", -1);
4286
4610
  __publicField(this, "onFrame", null);
4287
- this.page = options.clean ? withRoughness(page, 0) : page;
4611
+ this.page = options.clean ? cleanCopy(page) : page;
4288
4612
  addFonts(usedFonts(this.page));
4289
4613
  this.plan = plan(this.page);
4290
4614
  this.clicks = clickTargets(page, options.file);
@@ -4361,7 +4685,7 @@ var Playback = class {
4361
4685
  }
4362
4686
  /** Whether a copy is still playing through after a click. */
4363
4687
  replaying(now = performance.now()) {
4364
- for (const [id, start] of this.replayStarts) if (now - start < (this.replays.get(id) ?? 0) * 1e3) return true;
4688
+ for (const [id, start2] of this.replayStarts) if (now - start2 < (this.replays.get(id) ?? 0) * 1e3) return true;
4365
4689
  return false;
4366
4690
  }
4367
4691
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -4374,17 +4698,24 @@ var Playback = class {
4374
4698
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
4375
4699
  const now = typeof performance === "undefined" ? 0 : performance.now();
4376
4700
  const times = /* @__PURE__ */ new Map();
4377
- for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
4701
+ for (const [id, start2] of this.replayStarts) times.set(id, Math.min((now - start2) / 1e3, this.replays.get(id) ?? 0));
4378
4702
  const tc = clampTime(t);
4379
4703
  const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4380
- const frame2 = expandingSymbols(pose, tc, true, times);
4704
+ const made = new Expansions(pose, tc, true);
4705
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
4381
4706
  const { parts } = svgParts(frame2, this.background, r);
4382
4707
  const owned = (m, parts2) => {
4383
4708
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
4384
4709
  return m;
4385
4710
  };
4386
- const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(expandingSymbols(pose, tc, true, new Map([...times, ...st])), this.background, r).parts.filter((p) => st.has(this.owner.get(p.key) ?? "")));
4387
- const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r).parts.filter((p) => this.hoverIDs.has(p.key)));
4711
+ const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(
4712
+ expandingSymbols(pose, tc, true, new Map([...times, ...st]), made),
4713
+ this.background,
4714
+ r,
4715
+ false,
4716
+ (key) => st.has(this.owner.get(key) ?? "")
4717
+ ).parts);
4718
+ const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r, false, (key) => this.hoverIDs.has(key)).parts);
4388
4719
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
4389
4720
  const normal = owned(/* @__PURE__ */ new Map(), parts);
4390
4721
  const merged = [];
@@ -4513,6 +4844,14 @@ ${h}</g>
4513
4844
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4514
4845
  }
4515
4846
  };
4847
+ var cleanCopies = /* @__PURE__ */ new WeakMap();
4848
+ function cleanCopy(page) {
4849
+ const hit = cleanCopies.get(page);
4850
+ if (hit && unchanged(page, hit.fields)) return hit.page;
4851
+ const clean = withRoughness(page, 0);
4852
+ cleanCopies.set(page, { fields: { ...page }, page: clean });
4853
+ return clean;
4854
+ }
4516
4855
  var ElementBase = typeof HTMLElement === "undefined" ? class {
4517
4856
  } : HTMLElement;
4518
4857
  var DottiePlayer = class extends ElementBase {
@@ -4535,6 +4874,10 @@ var DottiePlayer = class extends ElementBase {
4535
4874
  __publicField(this, "raf", 0);
4536
4875
  __publicField(this, "replayRaf", 0);
4537
4876
  __publicField(this, "loadToken", 0);
4877
+ /** Counts mounts, so a plan that comes back from a worker after a newer one is dropped. */
4878
+ __publicField(this, "mountToken", 0);
4879
+ /** The time a still shows; null while the drawing is animated. */
4880
+ __publicField(this, "stillTime", null);
4538
4881
  __publicField(this, "visible", true);
4539
4882
  __publicField(this, "observer", null);
4540
4883
  this.root = this.attachShadow({ mode: "open" });
@@ -4611,9 +4954,19 @@ var DottiePlayer = class extends ElementBase {
4611
4954
  text = inline2.textContent ?? "";
4612
4955
  }
4613
4956
  if (token !== this.loadToken) return;
4614
- this.setFile(parseDottie(text));
4957
+ this.showFile(parseDottie(text));
4615
4958
  } catch (err) {
4616
4959
  if (token !== this.loadToken) return;
4960
+ this.setPlaying(false);
4961
+ cancelAnimationFrame(this.raf);
4962
+ cancelAnimationFrame(this.replayRaf);
4963
+ this.raf = 0;
4964
+ this.replayRaf = 0;
4965
+ this.playback = null;
4966
+ this.file = null;
4967
+ this.mountToken++;
4968
+ this.stillTime = null;
4969
+ this.bar.hidden = true;
4617
4970
  this.stage.innerHTML = "";
4618
4971
  const box = document.createElement("div");
4619
4972
  box.className = "error";
@@ -4624,15 +4977,35 @@ var DottiePlayer = class extends ElementBase {
4624
4977
  }
4625
4978
  /** Shows an already-parsed file. */
4626
4979
  setFile(file) {
4980
+ this.loadToken++;
4981
+ this.showFile(file);
4982
+ }
4983
+ showFile(file) {
4627
4984
  this.file = file;
4628
4985
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
4629
- this.mount();
4630
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
4986
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
4631
4987
  }
4632
- mount() {
4988
+ /** Shows the file's page; then `mounted`. A big animation is planned in a worker first (planner.ts), and what was
4989
+ * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
4990
+ mount(mounted) {
4633
4991
  const file = this.file;
4634
4992
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
4635
- const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
4993
+ const clean = this.hasAttribute("clean");
4994
+ const token = ++this.mountToken;
4995
+ const shown = clean ? cleanCopy(page) : page;
4996
+ if (!plansInWorker(shown)) {
4997
+ this.mountPage(file, page, clean);
4998
+ mounted?.();
4999
+ return;
5000
+ }
5001
+ void planInWorker(shown).then(() => {
5002
+ if (token !== this.mountToken || this.file !== file) return;
5003
+ this.mountPage(file, page, clean);
5004
+ mounted?.();
5005
+ });
5006
+ }
5007
+ mountPage(file, page, clean) {
5008
+ const p = new Playback(page, this.stage, { clean, file });
4636
5009
  p.still = this.hasAttribute("at");
4637
5010
  this.playback = p;
4638
5011
  this.style.setProperty("--dottie-page", color(p.background));
@@ -4655,10 +5028,14 @@ var DottiePlayer = class extends ElementBase {
4655
5028
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
4656
5029
  };
4657
5030
  if (!animated) {
4658
- p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
5031
+ const i = at !== null ? nearest(p, Number(at) || 0) : p.posterFrame;
5032
+ this.stillTime = at !== null ? Number(at) || 0 : posterTime(page);
5033
+ p.drawAt(this.stillTime, i);
5034
+ p.frame = i;
4659
5035
  this.setPlaying(false);
4660
5036
  return;
4661
5037
  }
5038
+ this.stillTime = null;
4662
5039
  p.show(0);
4663
5040
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4664
5041
  if (reduced) {
@@ -4672,8 +5049,10 @@ var DottiePlayer = class extends ElementBase {
4672
5049
  get paused() {
4673
5050
  return !this.playing;
4674
5051
  }
5052
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
4675
5053
  get currentTime() {
4676
5054
  const p = this.playback;
5055
+ if (p && this.stillTime !== null) return this.stillTime;
4677
5056
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
4678
5057
  }
4679
5058
  set currentTime(t) {
@@ -4960,9 +5339,10 @@ function render(source, options = {}) {
4960
5339
  function isFile(v) {
4961
5340
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4962
5341
  }
4963
- var VERSION = "0.8.0";
5342
+ var VERSION = "0.9.0";
4964
5343
 
4965
5344
  // src/player-entry.ts
5345
+ servePlans();
4966
5346
  definePlayer();
4967
5347
  globalThis.Dottie ?? (globalThis.Dottie = index_exports);
4968
5348
  export {