@phin-tech/dottie 0.7.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.
package/dist/dottie.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! dottie.js 0.7.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 __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
@@ -8,10 +8,53 @@ var MAX_NAME = 64;
8
8
  function isName(name) {
9
9
  return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
10
10
  }
11
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
12
+ var OPS = [">=", "<=", "==", "!=", ">", "<"];
13
+ function metricTerm(term) {
14
+ const t = trim(term);
15
+ if (!t.includes("(")) {
16
+ const op2 = t.search(/[>=<!]/);
17
+ if (op2 < 0) return null;
18
+ const name2 = trim(t.slice(0, op2));
19
+ if (!METRIC_NAMES.includes(name2)) return null;
20
+ const m = metricTerm(name2 + "() " + t.slice(op2));
21
+ return m ? { ...m, target: "" } : null;
22
+ }
23
+ const open = t.indexOf("("), close = t.indexOf(")");
24
+ if (open < 0 || close < 0 || open >= close) return null;
25
+ const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
26
+ let rest = trim(t.slice(close + 1));
27
+ if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
28
+ const op = OPS.find((o) => rest.startsWith(o));
29
+ if (op === void 0) return null;
30
+ rest = trim(rest.slice(op.length));
31
+ if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
32
+ return { metric: name, target, op, value: Number(rest) };
33
+ }
34
+ function passes(m, v) {
35
+ switch (m.op) {
36
+ case ">":
37
+ return v > m.value;
38
+ case ">=":
39
+ return v >= m.value;
40
+ case "<":
41
+ return v < m.value;
42
+ case "<=":
43
+ return v <= m.value;
44
+ case "==":
45
+ return v === m.value;
46
+ default:
47
+ return v !== m.value;
48
+ }
49
+ }
50
+ function mentionsMetric(condition) {
51
+ return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
52
+ }
11
53
  function conditionNames(condition) {
12
54
  const out = [];
13
55
  for (const any of condition.split("||")) {
14
56
  for (const all of any.split("&&")) {
57
+ if (metricTerm(all) !== null) continue;
15
58
  let term = trim(all);
16
59
  if (term.startsWith("!")) term = trim(term.slice(1));
17
60
  if (!isName(term)) return null;
@@ -20,9 +63,14 @@ function conditionNames(condition) {
20
63
  }
21
64
  return out;
22
65
  }
23
- function holds(condition, state) {
66
+ function holds(condition, state, metric = null) {
24
67
  if (conditionNames(condition) === null) return false;
25
68
  return condition.split("||").some((any) => any.split("&&").every((all) => {
69
+ const m = metricTerm(all);
70
+ if (m !== null) {
71
+ const v = metric ? metric(m) : null;
72
+ return v !== null && passes(m, v);
73
+ }
26
74
  let term = trim(all);
27
75
  let negated = false;
28
76
  if (term.startsWith("!")) {
@@ -44,11 +92,40 @@ function stateHidden(page, choice) {
44
92
  for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
45
93
  return out;
46
94
  }
95
+ function activeOverrides(e, values) {
96
+ if (!values || !e.overrides) return [];
97
+ return e.overrides.filter((o) => !mentionsMetric(o.when) && holds(o.when, values));
98
+ }
99
+ function looksOnly(o) {
100
+ return { ...o, reroute: null, offline: null, cost: null, capacity: null, delay: null };
101
+ }
102
+ function applyOverride(o, e) {
103
+ const out = cloneElement(e);
104
+ if (o.stroke) out.style.stroke = { ...o.stroke };
105
+ if (o.fill) out.style.fill = { ...o.fill };
106
+ if (o.textColor) out.style.textColor = { ...o.textColor };
107
+ if (o.strokeWidth !== null) out.style.strokeWidth = Math.max(0, o.strokeWidth);
108
+ if (o.dash !== null) out.style.dash = o.dash;
109
+ if (o.text !== null) out.text = o.text;
110
+ if (o.cost !== null && out.connector) out.connector.cost = Math.min(Math.max(o.cost, 0), 1e6);
111
+ if (o.capacity !== null) out.capacity = Math.max(o.capacity, 0);
112
+ if (o.delay !== null && out.kind !== "connector") out.delay = Math.max(o.delay, 0);
113
+ if (o.opacity !== null) {
114
+ const v = Math.min(Math.max(o.opacity, 0), 1) * (out.style.opacity ?? 1);
115
+ out.style.opacity = v >= 1 ? null : v;
116
+ }
117
+ return out;
118
+ }
47
119
  function changesState(c) {
48
- return !!c && (Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
120
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
49
121
  }
50
- function applyClick(c, state) {
51
- const out = { ...state };
122
+ function applyClick(c, state, initial = {}) {
123
+ let out = { ...state };
124
+ if (c.reset === true) {
125
+ out = {};
126
+ for (const k of Object.keys(state)) out[k] = false;
127
+ Object.assign(out, initial);
128
+ }
52
129
  for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
53
130
  for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
54
131
  return out;
@@ -63,7 +140,9 @@ var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand",
63
140
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
64
141
  var KNOWN_FIELDS = {
65
142
  file: ["version", "pages", "images"],
66
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
143
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
144
+ flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
145
+ flowOverride: ["when", "volume"],
67
146
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
68
147
  layer: ["id", "name", "visible", "locked"],
69
148
  rgba: ["r", "g", "b", "a"],
@@ -92,6 +171,7 @@ var KNOWN_FIELDS = {
92
171
  "click",
93
172
  "hover",
94
173
  "when",
174
+ "overrides",
95
175
  "symbol",
96
176
  "playback",
97
177
  "offset",
@@ -100,8 +180,13 @@ var KNOWN_FIELDS = {
100
180
  "reveal",
101
181
  "ports",
102
182
  "labelRect",
103
- "extension"
183
+ "extension",
184
+ "capacity",
185
+ "delay",
186
+ "queue",
187
+ "gauge"
104
188
  ],
189
+ gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
105
190
  strokePoint: ["x", "y"],
106
191
  subpath: ["closed", "coords"],
107
192
  inlineImage: ["data", "format"],
@@ -109,7 +194,7 @@ var KNOWN_FIELDS = {
109
194
  extensionRef: ["id", "shape", "version", "params"],
110
195
  normalizedPoint: ["x", "y"],
111
196
  normalizedRect: ["x", "y", "width", "height"],
112
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
197
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
113
198
  waypoint: ["x", "y"],
114
199
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
115
200
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -118,8 +203,9 @@ var KNOWN_FIELDS = {
118
203
  repeat: ["start", "end", "count"],
119
204
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
120
205
  marker: ["t", "name", "loop"],
121
- click: ["scene", "url", "set", "toggle"],
122
- hover: ["stroke", "fill", "opacity"]
206
+ click: ["scene", "url", "set", "toggle", "reset"],
207
+ hover: ["stroke", "fill", "opacity"],
208
+ override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
123
209
  };
124
210
  var MAX_DURATION = 3600;
125
211
  var MAX_MARCH = 1e4;
@@ -135,6 +221,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
135
221
  var str = (v) => typeof v === "string" ? v : void 0;
136
222
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
137
223
  var bool = (v) => typeof v === "boolean" ? v : void 0;
224
+ var NO_ID = "00000000-0000-0000-0000-000000000000";
225
+ var UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
138
226
  var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
139
227
  function clampTime(t) {
140
228
  if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
@@ -226,7 +314,8 @@ var Reader = class {
226
314
  this.fields(w, "waypoint");
227
315
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
228
316
  }) : null,
229
- hops: typeof v.hops === "boolean" ? v.hops : null
317
+ hops: typeof v.hops === "boolean" ? v.hops : null,
318
+ cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
230
319
  };
231
320
  }
232
321
  image(v, table) {
@@ -278,6 +367,7 @@ var Reader = class {
278
367
  click: this.click(v.click),
279
368
  hover: this.hover(v.hover),
280
369
  when: str(v.when) ?? null,
370
+ overrides: this.overrides(v.overrides),
281
371
  symbol: uuid(v.symbol) ?? null,
282
372
  playback: this.playback(v.playback),
283
373
  offset: num(v.offset) ?? null,
@@ -289,7 +379,26 @@ var Reader = class {
289
379
  return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
290
380
  }) : null,
291
381
  labelRect: this.labelRect(v.labelRect),
292
- extension: this.extension(v.extension)
382
+ extension: this.extension(v.extension),
383
+ capacity: num(v.capacity) ?? null,
384
+ delay: num(v.delay) ?? null,
385
+ queue: v.queue === true ? true : null,
386
+ gauge: this.gauge(v.gauge)
387
+ };
388
+ }
389
+ gauge(v) {
390
+ if (!isObj(v)) return null;
391
+ this.fields(v, "gauge");
392
+ const finite = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
393
+ const style2 = str(v.style);
394
+ return {
395
+ metric: str(v.metric) ?? "",
396
+ of: typeof v.of === "string" && UUID.test(v.of) ? v.of.toUpperCase() : NO_ID,
397
+ max: finite(v.max),
398
+ warn: finite(v.warn),
399
+ alarm: finite(v.alarm),
400
+ style: style2 === "number" || style2 === "bar" || style2 === "dial" ? style2 : null,
401
+ label: str(v.label) ?? null
293
402
  };
294
403
  }
295
404
  labelRect(v) {
@@ -333,7 +442,7 @@ var Reader = class {
333
442
  }
334
443
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
335
444
  const toggle = raw ? raw.filter(isName) : null;
336
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
445
+ return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
337
446
  }
338
447
  playback(v) {
339
448
  const s = str(v);
@@ -369,6 +478,35 @@ var Reader = class {
369
478
  params: strings(v.params)
370
479
  };
371
480
  }
481
+ /** As DottieKit reads them: a reroute whose targets aren't all ids is dropped, as are keys that aren't ids. */
482
+ overrides(v) {
483
+ if (!Array.isArray(v)) return null;
484
+ return v.filter(isObj).map((o) => {
485
+ this.fields(o, "override");
486
+ const dash = str(o.dash);
487
+ let reroute = null;
488
+ if (isObj(o.reroute) && Object.values(o.reroute).every((x) => typeof x === "string" && UUID.test(x))) {
489
+ reroute = {};
490
+ for (const [k, x] of Object.entries(o.reroute)) if (UUID.test(k)) reroute[k.toUpperCase()] = x.toUpperCase();
491
+ if (Object.keys(reroute).length === 0) reroute = null;
492
+ }
493
+ return {
494
+ when: str(o.when) ?? "",
495
+ stroke: rgba(o.stroke) ?? null,
496
+ fill: rgba(o.fill) ?? null,
497
+ textColor: rgba(o.textColor) ?? null,
498
+ opacity: num(o.opacity) ?? null,
499
+ strokeWidth: num(o.strokeWidth) ?? null,
500
+ dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
501
+ text: str(o.text) ?? null,
502
+ reroute,
503
+ offline: o.offline === true ? true : null,
504
+ cost: typeof o.cost === "number" && Number.isFinite(o.cost) ? o.cost : null,
505
+ capacity: typeof o.capacity === "number" && Number.isFinite(o.capacity) ? o.capacity : null,
506
+ delay: typeof o.delay === "number" && Number.isFinite(o.delay) ? o.delay : null
507
+ };
508
+ });
509
+ }
372
510
  hover(v) {
373
511
  if (!isObj(v)) return null;
374
512
  this.fields(v, "hover");
@@ -408,15 +546,15 @@ var Reader = class {
408
546
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
409
547
  stableSortBy(keys, (k) => k.t);
410
548
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
411
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
412
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
413
- return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
549
+ const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
550
+ if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
551
+ 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 }];
414
552
  }) : null;
415
553
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
416
- const start = num(r.start), end = num(r.end);
417
- if (start === void 0 || end === void 0) return [];
554
+ const start2 = num(r.start), end = num(r.end);
555
+ if (start2 === void 0 || end === void 0) return [];
418
556
  const count = num(r.count);
419
- const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
557
+ const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
420
558
  return repeatIsValid(rep) ? [rep] : [];
421
559
  }) : null;
422
560
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -468,7 +606,33 @@ var Reader = class {
468
606
  theme: str(v.theme) ?? null,
469
607
  timeline: this.timeline(v.timeline),
470
608
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
471
- variables: this.variables(v.variables)
609
+ variables: this.variables(v.variables),
610
+ flows: Array.isArray(v.flows) ? v.flows.filter(isObj).map((f) => this.flow(f)) : null
611
+ };
612
+ }
613
+ /** As DottieKit reads one: a missing or bad id, source or destination sends nothing; bad destinations are dropped. */
614
+ flow(f) {
615
+ this.fields(f, "flow");
616
+ const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
617
+ const finite = (x) => {
618
+ const n = num(x);
619
+ return n !== void 0 && Number.isFinite(n) ? n : null;
620
+ };
621
+ return {
622
+ id: id(f.id),
623
+ from: id(f.from),
624
+ to: Array.isArray(f.to) && f.to.every((x) => typeof x === "string") ? f.to.filter((x) => UUID.test(x)).map((x) => x.toUpperCase()) : [],
625
+ every: finite(f.every),
626
+ speed: finite(f.speed),
627
+ size: finite(f.size),
628
+ color: isObj(f.color) && ["r", "g", "b", "a"].every((k) => typeof f.color[k] === "number") ? rgba(f.color) ?? null : null,
629
+ when: str(f.when) ?? null,
630
+ spread: f.spread === true ? true : null,
631
+ volume: finite(f.volume),
632
+ overrides: Array.isArray(f.overrides) && f.overrides.length > 0 ? f.overrides.filter(isObj).map((o) => {
633
+ this.fields(o, "flowOverride");
634
+ return { when: str(o.when) ?? "", volume: finite(o.volume) };
635
+ }) : null
472
636
  };
473
637
  }
474
638
  };
@@ -530,7 +694,8 @@ function cloneElement(e) {
530
694
  start: { ...e.connector.start },
531
695
  end: { ...e.connector.end },
532
696
  waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
533
- } : null
697
+ } : null,
698
+ gauge: e.gauge ? { ...e.gauge } : e.gauge
534
699
  };
535
700
  }
536
701
  function orderedElements(page) {
@@ -614,6 +779,21 @@ function pieces(para) {
614
779
  if (cur || out.length === 0) out.push(cur);
615
780
  return out;
616
781
  }
782
+ function prefixWidths(chars, fontSize, face, font) {
783
+ const fam = familyMetrics(font);
784
+ const f = fam.faces[face] ?? fam.faces.regular;
785
+ const fallback = f.w["n"];
786
+ const out = [0];
787
+ let w = 0;
788
+ let prev = "";
789
+ for (const ch of chars) {
790
+ w += f.w[ch] ?? fallback;
791
+ if (prev) w += f.k[prev + ch] ?? 0;
792
+ prev = ch;
793
+ out.push(w * fontSize / 1e3);
794
+ }
795
+ return out;
796
+ }
617
797
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
618
798
  const width = (t) => measure(t, fontSize, face, font);
619
799
  const out = [];
@@ -629,8 +809,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
629
809
  }
630
810
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
631
811
  const chars = [...line2];
812
+ const prefix = prefixWidths(chars, fontSize, face, font);
632
813
  let n = chars.length - 1;
633
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
814
+ while (n > 1 && prefix[n] > maxWidth) n--;
634
815
  out.push(chars.slice(0, n).join(""));
635
816
  line2 = chars.slice(n).join("");
636
817
  }
@@ -785,13 +966,13 @@ var Path = class _Path {
785
966
  contours() {
786
967
  const out = [];
787
968
  let current = [];
788
- let start = { x: 0, y: 0 };
969
+ let start2 = { x: 0, y: 0 };
789
970
  for (const s of this.segments) {
790
- const last = current.length ? current[current.length - 1] : start;
971
+ const last = current.length ? current[current.length - 1] : start2;
791
972
  switch (s.op) {
792
973
  case "M":
793
974
  if (current.length > 1) out.push(current);
794
- start = s.p;
975
+ start2 = s.p;
795
976
  current = [s.p];
796
977
  break;
797
978
  case "L":
@@ -813,9 +994,9 @@ var Path = class _Path {
813
994
  }
814
995
  break;
815
996
  case "Z":
816
- current.push(start);
997
+ current.push(start2);
817
998
  if (current.length > 1) out.push(current);
818
- current = [start];
999
+ current = [start2];
819
1000
  break;
820
1001
  }
821
1002
  }
@@ -1207,9 +1388,17 @@ function midpoint(e, index) {
1207
1388
  }
1208
1389
  return pts[0];
1209
1390
  }
1391
+ var indexes = /* @__PURE__ */ new WeakMap();
1392
+ function isIndexOf(page, index) {
1393
+ return indexes.get(page.elements)?.index === index;
1394
+ }
1210
1395
  function indexOf(page) {
1396
+ const els = page.elements;
1397
+ const hit = indexes.get(els);
1398
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.index;
1211
1399
  const m = /* @__PURE__ */ new Map();
1212
- for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
1400
+ for (const e of els) if (!m.has(e.id)) m.set(e.id, e);
1401
+ indexes.set(els, { members: [...els], index: m });
1213
1402
  return m;
1214
1403
  }
1215
1404
 
@@ -1281,8 +1470,712 @@ function mulConst(step) {
1281
1470
  return [p[0], p[1]];
1282
1471
  }
1283
1472
 
1473
+ // src/flows.ts
1474
+ var DEFAULT_EVERY = 0.5;
1475
+ var DEFAULT_SPEED = 160;
1476
+ var DEFAULT_SIZE = 12;
1477
+ var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
1478
+ var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
1479
+ function packetID(flow, k) {
1480
+ const n = (k + 2 ** 40) % 2 ** 48;
1481
+ return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1482
+ }
1483
+ var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1484
+ var graphs = /* @__PURE__ */ new WeakMap();
1485
+ var networks = /* @__PURE__ */ new Map();
1486
+ function graphOf(frame2) {
1487
+ const els = frame2.elements;
1488
+ const hit = graphs.get(els);
1489
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.graph;
1490
+ const shapes2 = new Set(els.filter((e) => e.kind !== "connector").map((e) => e.id));
1491
+ const lines = [];
1492
+ for (const e of els) {
1493
+ const c = e.connector;
1494
+ if (c) lines.push([e.id, c.start.elementID, c.end.elementID, String(routeCost(c.cost)), c.startHead !== "none" ? "both" : "one"]);
1495
+ }
1496
+ const key = JSON.stringify([[...shapes2], lines]);
1497
+ let graph = networks.get(key);
1498
+ if (!graph) {
1499
+ const next = /* @__PURE__ */ new Map();
1500
+ const add2 = (a, edge) => {
1501
+ const list = next.get(a);
1502
+ if (list) list.push(edge);
1503
+ else next.set(a, [edge]);
1504
+ };
1505
+ for (const e of els) {
1506
+ const c = e.connector;
1507
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1508
+ if (!c || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1509
+ const cost = routeCost(c.cost);
1510
+ add2(a, { to: b, via: e.id, back: false, cost });
1511
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1512
+ }
1513
+ graph = { shapes: shapes2, next, trees: /* @__PURE__ */ new Map() };
1514
+ if (networks.size >= 16) networks.clear();
1515
+ networks.set(key, graph);
1516
+ }
1517
+ graphs.set(els, { members: [...els], graph });
1518
+ return graph;
1519
+ }
1520
+ var Frontier = class {
1521
+ constructor() {
1522
+ __publicField(this, "heap", []);
1523
+ }
1524
+ get size() {
1525
+ return this.heap.length;
1526
+ }
1527
+ less(i, j) {
1528
+ const a = this.heap[i], b = this.heap[j];
1529
+ return a.dist < b.dist || a.dist === b.dist && a.order < b.order;
1530
+ }
1531
+ swap(i, j) {
1532
+ [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
1533
+ }
1534
+ push(id, dist, order) {
1535
+ this.heap.push({ id, dist, order });
1536
+ for (let i = this.heap.length - 1; i > 0; ) {
1537
+ const up = i - 1 >> 1;
1538
+ if (!this.less(i, up)) break;
1539
+ this.swap(i, up);
1540
+ i = up;
1541
+ }
1542
+ }
1543
+ pop() {
1544
+ const top = this.heap[0], last = this.heap.pop();
1545
+ if (this.heap.length > 0) {
1546
+ this.heap[0] = last;
1547
+ for (let i = 0; ; ) {
1548
+ const l = 2 * i + 1, r = l + 1;
1549
+ let m = i;
1550
+ if (l < this.heap.length && this.less(l, m)) m = l;
1551
+ if (r < this.heap.length && this.less(r, m)) m = r;
1552
+ if (m === i) break;
1553
+ this.swap(i, m);
1554
+ i = m;
1555
+ }
1556
+ }
1557
+ return top;
1558
+ }
1559
+ };
1560
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set(), until) {
1561
+ const graph = graphOf(frame2);
1562
+ const key = JSON.stringify([from, until ?? null, [...offline].sort()]);
1563
+ if (graph.trees.has(key)) return graph.trees.get(key);
1564
+ const tree = grow(graph, from, offline, until);
1565
+ if (graph.trees.size >= 4096) graph.trees.clear();
1566
+ graph.trees.set(key, tree);
1567
+ return tree;
1568
+ }
1569
+ function grow({ shapes: shapes2, next }, from, offline, until) {
1570
+ if (!shapes2.has(from) || offline.has(from)) return null;
1571
+ const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1572
+ let counter = 1;
1573
+ const tree = new FlowTree(from);
1574
+ const open = new Frontier();
1575
+ open.push(from, 0, 0);
1576
+ while (open.size > 0) {
1577
+ const { id: n, order: o } = open.pop();
1578
+ if (tree.settled.has(n) || order.get(n) !== o) continue;
1579
+ tree.settled.add(n);
1580
+ if (n === until && n !== from) break;
1581
+ for (const edge of next.get(n) ?? []) {
1582
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1583
+ if (tree.settled.has(edge.to)) continue;
1584
+ const d = dist.get(n) + edge.cost;
1585
+ const old = dist.get(edge.to);
1586
+ if (old !== void 0 && old <= d) continue;
1587
+ dist.set(edge.to, d);
1588
+ order.set(edge.to, counter);
1589
+ open.push(edge.to, d, counter++);
1590
+ tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1591
+ }
1592
+ }
1593
+ return tree;
1594
+ }
1595
+ var FlowTree = class {
1596
+ constructor(from) {
1597
+ __publicField(this, "settled", /* @__PURE__ */ new Set());
1598
+ __publicField(this, "came", /* @__PURE__ */ new Map());
1599
+ __publicField(this, "from");
1600
+ this.from = from;
1601
+ }
1602
+ /** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
1603
+ goals(to, spread) {
1604
+ const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
1605
+ if (reachable.length === 0) return [];
1606
+ return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
1607
+ }
1608
+ /** The connectors to `goal`, as [connector, backwards] hops. */
1609
+ hops(goal) {
1610
+ const hops = [];
1611
+ for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
1612
+ return hops.reverse();
1613
+ }
1614
+ };
1615
+
1616
+ // src/measure.ts
1617
+ var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
1618
+ function metricValue(m, metric, id) {
1619
+ const f = m.flows.get(id);
1620
+ if (f) {
1621
+ switch (metric) {
1622
+ case "throughput":
1623
+ return f.placed;
1624
+ case "backlog":
1625
+ return f.backlog;
1626
+ case "backup":
1627
+ return f.backup;
1628
+ case "transit":
1629
+ return f.transit;
1630
+ default:
1631
+ return null;
1632
+ }
1633
+ }
1634
+ const e = m.elements.get(id);
1635
+ if (!e) return null;
1636
+ switch (metric) {
1637
+ case "throughput":
1638
+ return e.throughput;
1639
+ case "load":
1640
+ return e.load;
1641
+ case "delay":
1642
+ return e.delay;
1643
+ case "backlog":
1644
+ return e.backlog;
1645
+ case "inFlight":
1646
+ return e.inFlight;
1647
+ case "depth":
1648
+ return e.depth;
1649
+ default:
1650
+ return null;
1651
+ }
1652
+ }
1653
+ var TINY = 1e-9;
1654
+ var amount = (f) => clamp(f.volume, 1, 0, 1e12);
1655
+ function amountIn(f, values) {
1656
+ if (values === null || !f.overrides) return amount(f);
1657
+ let v = null;
1658
+ for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
1659
+ return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
1660
+ }
1661
+ function delayOf(e) {
1662
+ const d = e.delay ?? null;
1663
+ if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
1664
+ return Math.min(Math.max(d, 0), 3600);
1665
+ }
1666
+ var queueKey = (shape, flow) => shape + "|" + flow;
1667
+ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1668
+ const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1669
+ const byID = indexOf(frame2);
1670
+ const traced = /* @__PURE__ */ new Map();
1671
+ const traceOf = (c) => {
1672
+ let pts = traced.get(c.id);
1673
+ if (!pts) {
1674
+ pts = tracing(c, frame2, byID).points;
1675
+ traced.set(c.id, pts);
1676
+ }
1677
+ return pts;
1678
+ };
1679
+ const metrics2 = emptyMetrics();
1680
+ for (const e of frame2.elements) {
1681
+ if (e.kind === "connector" && !e.connector) continue;
1682
+ metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
1683
+ }
1684
+ const list = flows ?? [];
1685
+ const room = /* @__PURE__ */ new Map();
1686
+ for (const e of frame2.elements) {
1687
+ const c = e.capacity ?? null;
1688
+ if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1689
+ }
1690
+ const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1691
+ const route2 = (tree, goal) => {
1692
+ const hops = [], shapes2 = [];
1693
+ let at = goal;
1694
+ for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
1695
+ hops.push([step.via, step.back]);
1696
+ shapes2.push(at);
1697
+ at = step.from;
1698
+ }
1699
+ shapes2.push(at);
1700
+ return { hops: hops.reverse(), shapes: shapes2.reverse() };
1701
+ };
1702
+ const way = (hops, shapes2) => {
1703
+ const points = [], inside = [], stretches = [];
1704
+ let length = 0;
1705
+ hops.forEach(([id, back], k) => {
1706
+ const c = byID.get(id);
1707
+ if (!c) return;
1708
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1709
+ if (line2.length === 0) return;
1710
+ const last = points[points.length - 1];
1711
+ if (last) {
1712
+ const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1713
+ const shape = byID.get(shapes2[k]);
1714
+ inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1715
+ length += gap;
1716
+ }
1717
+ const start2 = length;
1718
+ 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);
1719
+ stretches.push([id, start2, length]);
1720
+ points.push(...line2);
1721
+ });
1722
+ return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
1723
+ };
1724
+ const along = (w, elapsedAt, speed) => {
1725
+ let elapsed = elapsedAt, d = 0;
1726
+ for (const [a, b, delay] of w.inside) {
1727
+ const reach = (a - d) / speed;
1728
+ if (elapsed <= reach) break;
1729
+ elapsed -= reach;
1730
+ d = a;
1731
+ const stay = (b - a) / speed + delay;
1732
+ if (elapsed < stay) return null;
1733
+ elapsed -= stay;
1734
+ d = b;
1735
+ }
1736
+ d += elapsed * speed;
1737
+ return d >= 0 && d <= w.length ? d : null;
1738
+ };
1739
+ const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
1740
+ const bump = (id, f) => {
1741
+ const m = metrics2.elements.get(id);
1742
+ if (m) f(m);
1743
+ };
1744
+ const leaving = /* @__PURE__ */ new Map(), turn = [];
1745
+ for (const flow of list) {
1746
+ const n = leaving.get(flow.from) ?? 0;
1747
+ turn.push(n);
1748
+ leaving.set(flow.from, n + 1);
1749
+ }
1750
+ const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
1751
+ const packets = [];
1752
+ const waitingBy = /* @__PURE__ */ new Map();
1753
+ 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);
1754
+ const bases = /* @__PURE__ */ new Map();
1755
+ const baseTree = (from) => {
1756
+ if (!bases.has(from)) bases.set(from, flowTree(from, frame2, offline));
1757
+ return bases.get(from);
1758
+ };
1759
+ for (const [i, flow] of list.entries()) {
1760
+ if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1761
+ const source = byID.get(flow.from);
1762
+ const base2 = source ? baseTree(flow.from) : null;
1763
+ if (!source || !base2) continue;
1764
+ const volume = amountIn(flow, values);
1765
+ const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1766
+ const placements = [];
1767
+ const waiting2 = [];
1768
+ const goals = base2.goals(flow.to, flow.spread === true);
1769
+ const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1770
+ for (const [candidates, share] of shares) {
1771
+ if (!(share > TINY)) continue;
1772
+ let left = share;
1773
+ for (let n = 0; n < 32 && left > TINY; n++) {
1774
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1775
+ const goal2 = tree?.goals(candidates, false)[0];
1776
+ if (!tree || goal2 === void 0) break;
1777
+ const r = route2(tree, goal2);
1778
+ const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1779
+ const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1780
+ const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
1781
+ if (!(fits > TINY)) break;
1782
+ for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
1783
+ placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
1784
+ left -= fits;
1785
+ }
1786
+ if (!(left > TINY)) continue;
1787
+ let at = flow.from;
1788
+ const goal = base2.goals(candidates, false)[0];
1789
+ if (goal !== void 0) {
1790
+ const r = route2(base2, goal), done = full();
1791
+ const sequence = [[r.shapes[0], r.shapes[0]]];
1792
+ r.hops.forEach(([id], k) => {
1793
+ sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
1794
+ });
1795
+ const first = sequence.find(([id]) => done.has(id));
1796
+ if (first) {
1797
+ at = first[1];
1798
+ const upTo = r.shapes.indexOf(at);
1799
+ for (let q = upTo; q >= 0; q--) {
1800
+ const s = byID.get(r.shapes[q]);
1801
+ if (s && s.queue === true && s.kind !== "connector") {
1802
+ at = r.shapes[q];
1803
+ break;
1804
+ }
1805
+ }
1806
+ }
1807
+ const n = r.shapes.indexOf(at);
1808
+ if (at !== flow.from && n >= 0) {
1809
+ const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
1810
+ for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
1811
+ placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1812
+ }
1813
+ }
1814
+ const existing = waiting2.find(([s]) => s === at);
1815
+ if (existing) existing[1] += left;
1816
+ else waiting2.push([at, left]);
1817
+ }
1818
+ waitingBy.set(flow.id, waiting2);
1819
+ for (const p of placements) {
1820
+ for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1821
+ m.throughput += p.amount;
1822
+ });
1823
+ if (!p.waits) fm.placed += p.amount;
1824
+ }
1825
+ for (const [at, a] of waiting2) {
1826
+ bump(at, (m) => {
1827
+ m.backlog += a;
1828
+ });
1829
+ fm.backlog += a;
1830
+ }
1831
+ const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
1832
+ fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
1833
+ const every = everyOf(flow);
1834
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1835
+ const phase = turn[i] / leaving.get(flow.from) * every;
1836
+ const color2 = flow.color ?? DEFAULT_COLOR;
1837
+ const cumulative = [];
1838
+ let sum = 0;
1839
+ for (const p of placements) {
1840
+ sum += p.amount / Math.max(volume, TINY);
1841
+ cumulative.push(sum);
1842
+ }
1843
+ if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
1844
+ let transitSum = 0;
1845
+ placements.forEach((p, r) => {
1846
+ const w = way(p.hops, p.shapes);
1847
+ if (!w) return;
1848
+ const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
1849
+ if (!p.waits) transitSum += travel * p.amount;
1850
+ const lo = Math.ceil((t - phase - travel) / every);
1851
+ const hi = Math.floor((t - phase) / every);
1852
+ const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
1853
+ const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
1854
+ for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
1855
+ if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
1856
+ const d = along(w, t - k * every - phase, speed);
1857
+ if (d === null) continue;
1858
+ const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
1859
+ if (s) bump(s[0], (m) => {
1860
+ m.inFlight += 1;
1861
+ });
1862
+ const pt2 = w.line.point(d / w.length);
1863
+ packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
1864
+ }
1865
+ });
1866
+ fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
1867
+ metrics2.flows.set(flow.id, fm);
1868
+ }
1869
+ const piles = /* @__PURE__ */ new Map();
1870
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1871
+ for (const flow of list) {
1872
+ const waiting2 = waitingBy.get(flow.id) ?? [];
1873
+ const shapes2 = waiting2.map(([s]) => s);
1874
+ const suffix = "|" + flow.id;
1875
+ if (carriedKeys.some((k) => k.endsWith(suffix))) for (const e of frame2.elements) {
1876
+ const c = carried[queueKey(e.id, flow.id)];
1877
+ if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1878
+ }
1879
+ if (shapes2.length === 0) continue;
1880
+ const volume = amountIn(flow, values);
1881
+ const every = everyOf(flow);
1882
+ const unit = every * Math.max(volume, TINY);
1883
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1884
+ const color2 = flow.color ?? DEFAULT_COLOR;
1885
+ shapes2.forEach((at, j) => {
1886
+ const shape = byID.get(at);
1887
+ if (!shape) return;
1888
+ const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1889
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1890
+ const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1891
+ let depth = before + backlog * since;
1892
+ if (backlog <= TINY && before > TINY) {
1893
+ const trial = new Map(room);
1894
+ const outlets = [];
1895
+ let total = 0;
1896
+ for (let n2 = 0; n2 < 32; n2++) {
1897
+ const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1898
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1899
+ const goal = tree?.goals(flow.to, false)[0];
1900
+ if (!tree || goal === void 0) break;
1901
+ const r = route2(tree, goal);
1902
+ const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1903
+ const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1904
+ const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
1905
+ const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
1906
+ if (!(rate > TINY)) break;
1907
+ for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
1908
+ outlets.push({ hops: r.hops, shapes: r.shapes, rate });
1909
+ total += rate;
1910
+ if (!Number.isFinite(spare)) break;
1911
+ }
1912
+ if (total > TINY) {
1913
+ depth = Math.max(0, before - total * since);
1914
+ if (depth > TINY) {
1915
+ for (const [id, v] of trial) room.set(id, v);
1916
+ for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
1917
+ m.throughput += o.rate;
1918
+ });
1919
+ }
1920
+ const lasts = before / total;
1921
+ outlets.forEach((o, i) => {
1922
+ const w = way(o.hops, o.shapes);
1923
+ if (!w) return;
1924
+ const spacing = Math.max(unit / o.rate, 0.05);
1925
+ for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
1926
+ const d = along(w, since - k * spacing, speed);
1927
+ if (d === null) continue;
1928
+ const pt2 = w.line.point(d / w.length);
1929
+ packets.push(packet(packetID(flow.id, 2 ** 39 + 2 ** 20 + j * 2 ** 14 + i * 2 ** 11 + k), pt2.x - size / 2, pt2.y - size / 2, size, color2, layer));
1930
+ }
1931
+ });
1932
+ }
1933
+ }
1934
+ if (!(depth > TINY || backlog > TINY)) return;
1935
+ bump(at, (m) => {
1936
+ m.depth += depth;
1937
+ });
1938
+ if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
1939
+ const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
1940
+ const rows = Math.trunc((n + 9) / 10);
1941
+ const base2 = piles.get(at) ?? 0;
1942
+ piles.set(at, base2 + rows);
1943
+ for (let q = 0; q < n; q++) {
1944
+ const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
1945
+ const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
1946
+ packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
1947
+ }
1948
+ });
1949
+ }
1950
+ for (const e of frame2.elements) {
1951
+ const c = e.capacity ?? null;
1952
+ const m = metrics2.elements.get(e.id);
1953
+ if (c === null || !Number.isFinite(c) || !m) continue;
1954
+ m.load = c > 0 ? m.throughput / c : 1;
1955
+ }
1956
+ return { packets, metrics: metrics2 };
1957
+ }
1958
+ var BLANK = {
1959
+ seed: 1,
1960
+ text: null,
1961
+ connector: null,
1962
+ groupID: null,
1963
+ points: null,
1964
+ path: null,
1965
+ subpaths: null,
1966
+ evenOdd: null,
1967
+ rotation: null,
1968
+ link: null,
1969
+ url: null,
1970
+ anchor: null,
1971
+ click: null,
1972
+ hover: null,
1973
+ when: null,
1974
+ overrides: null,
1975
+ symbol: null,
1976
+ playback: null,
1977
+ offset: null,
1978
+ args: null,
1979
+ image: null,
1980
+ reveal: null
1981
+ };
1982
+ function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
1983
+ return {
1984
+ stroke: { ...stroke },
1985
+ fill: { ...fill },
1986
+ fillStyle,
1987
+ strokeWidth,
1988
+ dash: "solid",
1989
+ roughness: 0,
1990
+ bowing: 1,
1991
+ fontSize: 18,
1992
+ font: null,
1993
+ gridSize: null,
1994
+ opacity: opacity2,
1995
+ dashOffset: null,
1996
+ textColor: null
1997
+ };
1998
+ }
1999
+ function packet(id, x, y, size, color2, layerID) {
2000
+ return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
2001
+ }
2002
+ function metricLookup(m, frame2) {
2003
+ const anchors = /* @__PURE__ */ new Map();
2004
+ for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
2005
+ const UUID2 = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
2006
+ return (term, own) => {
2007
+ const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
2008
+ return id === null ? null : metricValue(m, term.metric, id);
2009
+ };
2010
+ }
2011
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
2012
+ const lookup = metricLookup(metrics2, frame2);
2013
+ const plan2 = [], readings = {};
2014
+ for (const e of elements) {
2015
+ const applied = [];
2016
+ if (values !== null && e.overrides) {
2017
+ e.overrides.forEach((o, k) => {
2018
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2019
+ });
2020
+ }
2021
+ const g = values === null ? null : e.gauge;
2022
+ if (!g) {
2023
+ plan2.push({ applied, gauge: null });
2024
+ continue;
2025
+ }
2026
+ let shown = metricValue(metrics2, g.metric, g.of);
2027
+ const then = carried[gaugeKey(e.id)];
2028
+ if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
2029
+ const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
2030
+ shown = then + (shown - then) * smooth;
2031
+ }
2032
+ if (shown !== null) readings[gaugeKey(e.id)] = shown;
2033
+ plan2.push({ applied, gauge: { value: shown } });
2034
+ }
2035
+ return { plan: plan2, readings };
2036
+ }
2037
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2038
+ const { plan: plan2, readings } = finishPlan(elements, metrics2, values, frame2, since, carried);
2039
+ const out = [];
2040
+ elements.forEach((e, i) => {
2041
+ for (const k of plan2[i].applied) e = applyOverride(looksOnly(e.overrides[k]), e);
2042
+ const g = plan2[i].gauge;
2043
+ if (g && e.gauge) out.push(...gauge(e, e.gauge, g.value, frame2, flows));
2044
+ else out.push(e);
2045
+ });
2046
+ return { elements: out, readings };
2047
+ }
2048
+ var gaugeKey = (id) => "gauge|" + id;
2049
+ var GAUGE_EASE = 1;
2050
+ var hex = (h) => {
2051
+ const v = parseInt(h.slice(1), 16);
2052
+ return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
2053
+ };
2054
+ var GAUGE_GREEN = hex("#16a34a");
2055
+ var GAUGE_AMBER = hex("#d97706");
2056
+ var GAUGE_RED = hex("#dc2626");
2057
+ var GAUGE_BLUE = hex("#2f6fdf");
2058
+ var GAUGE_TRACK = hex("#e5e7eb");
2059
+ var GAUGE_GRAY = hex("#9ca3af");
2060
+ function formatted(metric, v) {
2061
+ if (v === null || !Number.isFinite(v)) return "\u2013";
2062
+ switch (metric) {
2063
+ case "load":
2064
+ case "backup":
2065
+ return formatNumber(v * 100, 0) + "%";
2066
+ case "transit":
2067
+ case "delay":
2068
+ return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
2069
+ default:
2070
+ return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
2071
+ }
2072
+ }
2073
+ function formatNumber(v, places) {
2074
+ const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
2075
+ const n = Math.floor(Math.abs(v) * scale + 0.5);
2076
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
2077
+ const whole = Math.trunc(n / scale), part = n - whole * scale;
2078
+ let s = String(whole);
2079
+ if (places > 0 && part > 0) {
2080
+ let digits = String(part);
2081
+ while (digits.length < places) digits = "0" + digits;
2082
+ while (digits.endsWith("0")) digits = digits.slice(0, -1);
2083
+ s += "." + digits;
2084
+ }
2085
+ return (v < 0 && n > 0 ? "-" : "") + s;
2086
+ }
2087
+ function gauge(e, g, value2, frame2, flows) {
2088
+ const target = frame2.elements.find((x) => x.id === g.of);
2089
+ const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
2090
+ const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
2091
+ const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
2092
+ let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
2093
+ if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
2094
+ if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
2095
+ if (value2 === null) color2 = GAUGE_GRAY;
2096
+ let name = g.label;
2097
+ if (name === null) {
2098
+ const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
2099
+ name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
2100
+ }
2101
+ const kind = g.style ?? "number";
2102
+ const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
2103
+ const line2 = (a, b) => [
2104
+ a.x / w,
2105
+ a.y / h,
2106
+ (a.x + (b.x - a.x) / 3) / w,
2107
+ (a.y + (b.y - a.y) / 3) / h,
2108
+ (a.x + 2 * (b.x - a.x) / 3) / w,
2109
+ (a.y + 2 * (b.y - a.y) / 3) / h,
2110
+ b.x / w,
2111
+ b.y / h
2112
+ ];
2113
+ const box = (x, y, bw, bh) => {
2114
+ const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
2115
+ let coords = line2(p[0], p[1]);
2116
+ for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
2117
+ return { closed: true, coords };
2118
+ };
2119
+ const arc = (c, radius, a0, a1) => {
2120
+ const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
2121
+ const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
2122
+ const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
2123
+ const s = at(a0);
2124
+ const coords = [s.x / w, s.y / h];
2125
+ for (let k = 0; k < pieces2; k++) {
2126
+ const a = a0 + k * step, b = a + step;
2127
+ const p0 = at(a), p3 = at(b);
2128
+ const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
2129
+ const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
2130
+ coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
2131
+ }
2132
+ return { closed: false, coords };
2133
+ };
2134
+ const part = (k, subpaths, stroke, fill, width) => ({
2135
+ ...BLANK,
2136
+ id: packetID(e.id, -(2 ** 38) - k),
2137
+ kind: "path",
2138
+ layerID: e.layerID,
2139
+ x: e.x,
2140
+ y: e.y,
2141
+ width: e.width,
2142
+ height: e.height,
2143
+ style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
2144
+ subpaths
2145
+ });
2146
+ const back = {
2147
+ ...e,
2148
+ gauge: null,
2149
+ kind: "path",
2150
+ subpaths: [box(0, 0, w, h)],
2151
+ text: name + "\n" + formatted(g.metric, value2),
2152
+ style: { ...e.style }
2153
+ };
2154
+ const parts = [];
2155
+ if (kind === "number") {
2156
+ back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
2157
+ back.style.textColor = { ...color2 };
2158
+ } else if (kind === "bar") {
2159
+ back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
2160
+ const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
2161
+ const trackW = w - 2 * inset2;
2162
+ parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
2163
+ if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
2164
+ } else {
2165
+ back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
2166
+ const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
2167
+ const thick = Math.max(2, radius * 0.16);
2168
+ parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
2169
+ if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
2170
+ const a = Math.PI + Math.PI * fraction;
2171
+ const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
2172
+ parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
2173
+ }
2174
+ return [back, ...parts];
2175
+ }
2176
+
1284
2177
  // src/rough.ts
1285
- var offset = (rng, amount, roughness) => rng.signed() * amount * roughness;
2178
+ var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
1286
2179
  function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1287
2180
  const length = Math.hypot(b.x - a.x, b.y - a.y);
1288
2181
  if (!(length > 0.5)) return;
@@ -1295,7 +2188,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1295
2188
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
1296
2189
  const dx = b.x - a.x, dy = b.y - a.y;
1297
2190
  const nx = -dy / length, ny = dx / length;
1298
- const start = preserveEnds ? a : (() => {
2191
+ const start2 = preserveEnds ? a : (() => {
1299
2192
  const x = a.x + j();
1300
2193
  return { x, y: a.y + j() };
1301
2194
  })();
@@ -1307,7 +2200,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1307
2200
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
1308
2201
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
1309
2202
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
1310
- into.moveTo(start);
2203
+ into.moveTo(start2);
1311
2204
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1312
2205
  }
1313
2206
  }
@@ -1568,10 +2461,10 @@ var Cache = class {
1568
2461
  __publicField(this, "limit");
1569
2462
  this.limit = limit;
1570
2463
  }
1571
- get(key, make) {
2464
+ get(key, make2) {
1572
2465
  const hit = this.map.get(key);
1573
2466
  if (hit !== void 0) return hit;
1574
- const v = make();
2467
+ const v = make2();
1575
2468
  if (this.map.size >= this.limit) this.map.clear();
1576
2469
  this.map.set(key, v);
1577
2470
  return v;
@@ -1795,26 +2688,59 @@ function subCubic(p, t0, t1) {
1795
2688
  const left = split(p, t1)[0];
1796
2689
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
1797
2690
  }
2691
+ function boxOf(pts) {
2692
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
2693
+ for (const p of pts) {
2694
+ x0 = Math.min(x0, p.x);
2695
+ y0 = Math.min(y0, p.y);
2696
+ x1 = Math.max(x1, p.x);
2697
+ y1 = Math.max(y1, p.y);
2698
+ }
2699
+ return { x0, y0, x1, y1 };
2700
+ }
2701
+ function touching(a, b) {
2702
+ const eps = 1e-6;
2703
+ return a.x0 <= b.x1 + eps && b.x0 <= a.x1 + eps && a.y0 <= b.y1 + eps && b.y0 <= a.y1 + eps;
2704
+ }
2705
+ var hopFrames = /* @__PURE__ */ new WeakMap();
2706
+ function linesBeneath(e, page, index) {
2707
+ let f = hopFrames.get(index);
2708
+ if (!f || f.layers !== page.layers || !isIndexOf(page, index)) {
2709
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2710
+ const first = /* @__PURE__ */ new Map(), under = [];
2711
+ for (const o of orderedElements(page)) {
2712
+ if (!first.has(o.id)) first.set(o.id, under.length);
2713
+ if (o.kind === "connector" && visible.has(o.layerID)) under.push(o);
2714
+ }
2715
+ f = { layers: page.layers, first, under, lines: [] };
2716
+ if (isIndexOf(page, index)) hopFrames.set(index, f);
2717
+ }
2718
+ const count = f.first.get(e.id) ?? f.under.length;
2719
+ while (f.lines.length < count) {
2720
+ const points = routePoints(route(f.under[f.lines.length], index));
2721
+ f.lines.push({ points, box: boxOf(points) });
2722
+ }
2723
+ return { lines: f.lines, count };
2724
+ }
1798
2725
  function hopCrossings(e, r, page, index) {
1799
2726
  const segments = hopSamples(r);
1800
2727
  if (segments.length === 0) return [];
1801
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
1802
- const beneath = [];
1803
- for (const o of orderedElements(page)) {
1804
- if (o.id === e.id) break;
1805
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
1806
- beneath.push(routePoints(route(o, index)));
1807
- }
2728
+ const { lines, count } = linesBeneath(e, page, index);
1808
2729
  const rad = hopRadius(e.style.strokeWidth);
1809
2730
  const out = [];
1810
2731
  for (const samples of segments) {
1811
2732
  const along = runningLengths(samples);
1812
2733
  const length = along[along.length - 1];
1813
2734
  const found = [];
2735
+ const near = [];
2736
+ const whole = boxOf(samples);
2737
+ for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
1814
2738
  for (let i = 0; i < samples.length - 1; i++) {
1815
2739
  const p = samples[i], q = samples[i + 1];
1816
2740
  const dx = q.x - p.x, dy = q.y - p.y;
1817
- for (const line2 of beneath) {
2741
+ const box = boxOf([p, q]);
2742
+ for (const { points: line2, box: lineBox } of near) {
2743
+ if (!touching(box, lineBox)) continue;
1818
2744
  for (let k = 0; k < line2.length - 1; k++) {
1819
2745
  const a = line2[k], b = line2[k + 1];
1820
2746
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2094,8 +3020,8 @@ function followSpan(track, t) {
2094
3020
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2095
3021
  return f;
2096
3022
  }
2097
- function tracing(e, page) {
2098
- if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
3023
+ function tracing(e, page, index = indexOf(page)) {
3024
+ if (e.kind === "connector") return new Polyline(routePoints(route(e, index)));
2099
3025
  const clean = cloneElement(e);
2100
3026
  clean.style.roughness = 0;
2101
3027
  const pts = outline(clean).contours()[0] ?? [];
@@ -2105,9 +3031,10 @@ function following(track, e, t, page) {
2105
3031
  const f = followSpan(track, t);
2106
3032
  if (!f) return e;
2107
3033
  t = localTime(track, t);
2108
- const pathElement = page.elements.find((x) => x.id === f.path);
3034
+ const index = indexOf(page);
3035
+ const pathElement = index.get(f.path);
2109
3036
  if (!pathElement || pathElement.id === e.id) return e;
2110
- const line2 = tracing(pathElement, page);
3037
+ const line2 = tracing(pathElement, page, index);
2111
3038
  const span = f.end - f.start;
2112
3039
  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);
2113
3040
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -2138,16 +3065,39 @@ function trackMap(tl) {
2138
3065
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2139
3066
  return m;
2140
3067
  }
2141
- function resolved(page, t, state = "initial") {
2142
- return expandingSymbols(posed(page, t, state), clampTime(t));
3068
+ function resolved(page, t, state = "initial", since = null, carried = {}) {
3069
+ return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
2143
3070
  }
2144
3071
  var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2145
- function posed(page, t, state = "initial") {
3072
+ function posed(page, t, state = "initial", since = null, carried = {}) {
3073
+ return posedWithMetrics(page, t, state, since, carried).frame;
3074
+ }
3075
+ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
2146
3076
  const off = stateHidden(page, state);
2147
- const timeline = page.timeline ?? (off.size === 0 ? null : EMPTY_TIMELINE);
2148
- if (!timeline) return page;
3077
+ const values = stateValues(page, state);
3078
+ const active = /* @__PURE__ */ new Map();
3079
+ for (const e of page.elements) {
3080
+ const o = activeOverrides(e, values);
3081
+ if (o.length > 0) active.set(e.id, o);
3082
+ }
3083
+ const flowing = (page.flows ?? []).length > 0;
3084
+ const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
3085
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
3086
+ if (!timeline) return { frame: page, metrics: emptyMetrics() };
2149
3087
  t = clampTime(t);
2150
3088
  const tracks = trackMap(timeline);
3089
+ for (const [id, overrides] of active) {
3090
+ const track = tracks.get(id);
3091
+ if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
3092
+ tracks.set(id, {
3093
+ ...track,
3094
+ follow: track.follow.map((f) => {
3095
+ let path = f.path;
3096
+ for (const o of overrides) path = o.reroute?.[path] ?? path;
3097
+ return { ...f, path };
3098
+ })
3099
+ });
3100
+ }
2151
3101
  const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
2152
3102
  const hidden = /* @__PURE__ */ new Set();
2153
3103
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
@@ -2167,6 +3117,14 @@ function posed(page, t, state = "initial") {
2167
3117
  let elements = page.elements.map((e) => {
2168
3118
  const track = tracks.get(e.id);
2169
3119
  let out = track ? applyTrack(track, e, t) : e;
3120
+ for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3121
+ const list = active.get(e.id);
3122
+ if (list) {
3123
+ const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
3124
+ const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
3125
+ if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
3126
+ if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
3127
+ }
2170
3128
  if (reseed !== null && reseed > 0 && boils(e)) {
2171
3129
  if (out === e) out = cloneElement(e);
2172
3130
  out.seed = boiled(e.seed, reseed);
@@ -2200,7 +3158,18 @@ function posed(page, t, state = "initial") {
2200
3158
  pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
2201
3159
  return pinned;
2202
3160
  });
2203
- return { ...page, elements: elements.filter((e) => !gone.has(e.id)), timeline: null };
3161
+ elements = elements.filter((e) => !gone.has(e.id));
3162
+ let metrics2 = emptyMetrics();
3163
+ if (measuring) {
3164
+ const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
3165
+ const frame2 = { ...page, elements, timeline: null };
3166
+ const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
3167
+ metrics2 = out.metrics;
3168
+ const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
3169
+ Object.assign(metrics2.carry, finished.readings);
3170
+ elements = [...finished.elements, ...out.packets];
3171
+ }
3172
+ return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
2204
3173
  }
2205
3174
  function posterTime(page) {
2206
3175
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
@@ -2315,22 +3284,35 @@ function filled(s, args) {
2315
3284
  }) };
2316
3285
  }
2317
3286
  function symbolPage(s) {
2318
- return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null };
3287
+ return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
2319
3288
  }
2320
3289
  function hasInstances(page) {
2321
3290
  return page.elements.some((e) => e.kind === "symbol");
2322
3291
  }
2323
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map()) {
3292
+ var Expansions = class {
3293
+ constructor(page, t, posing) {
3294
+ __publicField(this, "page");
3295
+ __publicField(this, "t");
3296
+ __publicField(this, "posing");
3297
+ /** For each instance: its time override, the budget it found and left, and what it made. */
3298
+ __publicField(this, "made", /* @__PURE__ */ new Map());
3299
+ this.page = page;
3300
+ this.t = t;
3301
+ this.posing = posing;
3302
+ }
3303
+ };
3304
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo) {
2324
3305
  if (!hasInstances(page)) return page;
2325
3306
  const budget = { left: MAX_ELEMENTS };
2326
- return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times) };
3307
+ const m = memo && memo.page === page && memo.t === t && memo.posing === posing ? memo : void 0;
3308
+ return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
2327
3309
  }
2328
3310
  function symbolTable(page) {
2329
3311
  const table = /* @__PURE__ */ new Map();
2330
3312
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
2331
3313
  return table;
2332
3314
  }
2333
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map()) {
3315
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo) {
2334
3316
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
2335
3317
  if (instances.size === 0) {
2336
3318
  budget.left -= elements.length;
@@ -2355,6 +3337,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
2355
3337
  budget.left -= 1;
2356
3338
  continue;
2357
3339
  }
3340
+ const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3341
+ const was = memo?.made.get(e);
3342
+ if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3343
+ budget.left -= was.before - was.after;
3344
+ for (const x of was.out) out.push(x);
3345
+ continue;
3346
+ }
3347
+ const before = budget.left, from = out.length;
2358
3348
  budget.left -= 1;
2359
3349
  const id = e.symbol;
2360
3350
  const symbol = id === null ? void 0 : table.get(id);
@@ -2370,6 +3360,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
2370
3360
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
2371
3361
  };
2372
3362
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3363
+ if (memo && stack.length === 0 && !was) memo.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
2373
3364
  }
2374
3365
  return out;
2375
3366
  }
@@ -2422,8 +3413,8 @@ function mix2(x) {
2422
3413
  return z ^ z >> 31n;
2423
3414
  }
2424
3415
  function halves(id) {
2425
- const hex = id.replace(/-/g, "").padStart(32, "0").slice(-32);
2426
- return [BigInt(`0x${hex.slice(0, 16)}`), BigInt(`0x${hex.slice(16)}`)];
3416
+ const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3417
+ return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
2427
3418
  }
2428
3419
  function derivedID(instance, inner) {
2429
3420
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -2444,7 +3435,7 @@ function clickTargets(page, file) {
2444
3435
  let pageOf = null;
2445
3436
  for (const e of page.elements) {
2446
3437
  const c = e.click;
2447
- const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {} } : null;
3438
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
2448
3439
  if (c?.scene) {
2449
3440
  const i = sceneNamed(c.scene, markers);
2450
3441
  if (i >= 0) {
@@ -2477,14 +3468,14 @@ function clickTargets(page, file) {
2477
3468
  function hoveredElement(e) {
2478
3469
  const h = e.hover;
2479
3470
  if (!h) return e;
2480
- const style = { ...e.style };
2481
- if (h.stroke) style.stroke = h.stroke;
2482
- if (h.fill) style.fill = h.fill;
3471
+ const style2 = { ...e.style };
3472
+ if (h.stroke) style2.stroke = h.stroke;
3473
+ if (h.fill) style2.fill = h.fill;
2483
3474
  if (h.opacity !== null && Number.isFinite(h.opacity)) {
2484
3475
  const o = Math.min(Math.max(h.opacity, 0), 1);
2485
- style.opacity = o >= 1 ? null : o;
3476
+ style2.opacity = o >= 1 ? null : o;
2486
3477
  }
2487
- return { ...e, style };
3478
+ return { ...e, style: style2 };
2488
3479
  }
2489
3480
  function hovered(page, ids) {
2490
3481
  return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
@@ -2637,13 +3628,13 @@ function blocks(lines) {
2637
3628
  flush();
2638
3629
  return out;
2639
3630
  }
2640
- function parseList(lines, start) {
2641
- const first = reItem.exec(lines[start]);
3631
+ function parseList(lines, start2) {
3632
+ const first = reItem.exec(lines[start2]);
2642
3633
  const indent = first[1].length;
2643
3634
  const bullet = /^\d/.test(first[2]) ? null : first[2];
2644
3635
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
2645
3636
  const items = [];
2646
- let i = start;
3637
+ let i = start2;
2647
3638
  while (i < lines.length) {
2648
3639
  const m = reItem.exec(lines[i]);
2649
3640
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -2688,14 +3679,14 @@ function parseList(lines, start) {
2688
3679
  }
2689
3680
  return { block: { kind: "list", ordered, items }, next: i };
2690
3681
  }
2691
- function inline(src, style = {}) {
3682
+ function inline(src, style2 = {}) {
2692
3683
  const out = [];
2693
3684
  const push = (text, s) => {
2694
3685
  if (text) out.push({ ...s, text });
2695
3686
  };
2696
3687
  let i = 0, buf = "";
2697
3688
  const emit = () => {
2698
- push(buf, style);
3689
+ push(buf, style2);
2699
3690
  buf = "";
2700
3691
  };
2701
3692
  while (i < src.length) {
@@ -2723,43 +3714,43 @@ function inline(src, style = {}) {
2723
3714
  emit();
2724
3715
  let code = m[2].replace(/\n/g, " ");
2725
3716
  if (/^ .* $/.test(code)) code = code.slice(1, -1);
2726
- push(code, { ...style, mono: true });
3717
+ push(code, { ...style2, mono: true });
2727
3718
  i += m[0].length;
2728
3719
  continue;
2729
3720
  }
2730
3721
  if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
2731
3722
  emit();
2732
- out.push(...inline(m[1], style));
3723
+ out.push(...inline(m[1], style2));
2733
3724
  i += m[0].length;
2734
3725
  continue;
2735
3726
  }
2736
3727
  if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
2737
3728
  emit();
2738
- out.push(...inline(m[1], { ...style, link: true }));
3729
+ out.push(...inline(m[1], { ...style2, link: true }));
2739
3730
  i += m[0].length;
2740
3731
  continue;
2741
3732
  }
2742
3733
  if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
2743
3734
  emit();
2744
- push(m[1], { ...style, link: true });
3735
+ push(m[1], { ...style2, link: true });
2745
3736
  i += m[0].length;
2746
3737
  continue;
2747
3738
  }
2748
3739
  if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
2749
3740
  emit();
2750
- out.push(...inline(m[1], { ...style, strike: true }));
3741
+ out.push(...inline(m[1], { ...style2, strike: true }));
2751
3742
  i += m[0].length;
2752
3743
  continue;
2753
3744
  }
2754
3745
  if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
2755
3746
  emit();
2756
- out.push(...inline(m[2], { ...style, bold: true }));
3747
+ out.push(...inline(m[2], { ...style2, bold: true }));
2757
3748
  i += m[0].length;
2758
3749
  continue;
2759
3750
  }
2760
3751
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
2761
3752
  emit();
2762
- out.push(...inline(m[2], { ...style, italic: true }));
3753
+ out.push(...inline(m[2], { ...style2, italic: true }));
2763
3754
  i += m[0].length;
2764
3755
  continue;
2765
3756
  }
@@ -2972,10 +3963,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
2972
3963
  let k = 0;
2973
3964
  let firstLine = true;
2974
3965
  while (k < chars.length || firstLine && chars.length === 0) {
2975
- const start = firstLine ? p.firstIndent : p.indent;
3966
+ const start2 = firstLine ? p.firstIndent : p.indent;
2976
3967
  const maxX2 = width;
2977
3968
  const glyphs = [];
2978
- let x = start, lastSpace = -1, end = k;
3969
+ let x = start2, lastSpace = -1, end = k;
2979
3970
  for (; end < chars.length; end++) {
2980
3971
  const { ch, run } = chars[end];
2981
3972
  const r = runs[run];
@@ -3024,7 +4015,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3024
4015
  }
3025
4016
  if (glyphs.length === 0 && end === k && k < chars.length) {
3026
4017
  const { ch, run } = chars[k];
3027
- glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run], fam) });
4018
+ glyphs.push({ ch, run, x: start2, w: charWidth(ch, runs[run], fam) });
3028
4019
  end = k + 1;
3029
4020
  }
3030
4021
  let asc = 0, desc = 0, lead = 0;
@@ -3050,13 +4041,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3050
4041
  });
3051
4042
  return lines;
3052
4043
  }
4044
+ var Memo = class {
4045
+ constructor(limit) {
4046
+ __publicField(this, "map", /* @__PURE__ */ new Map());
4047
+ __publicField(this, "limit");
4048
+ this.limit = limit;
4049
+ }
4050
+ get(key, make2) {
4051
+ const hit = this.map.get(key);
4052
+ if (hit !== void 0) return hit;
4053
+ const v = make2();
4054
+ if (this.map.size >= this.limit) this.map.clear();
4055
+ this.map.set(key, v);
4056
+ return v;
4057
+ }
4058
+ };
4059
+ var layouts = new Memo(500);
4060
+ var markups = new Memo(500);
3053
4061
  function noteMarkup(source, e) {
4062
+ const st = e.style, ink = st.stroke;
4063
+ 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]);
4064
+ return markups.get(key, () => computeNoteMarkup(source, e));
4065
+ }
4066
+ function computeNoteMarkup(source, e) {
3054
4067
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3055
4068
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3056
4069
  const fam = familyMetrics(e.style.font);
3057
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3058
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3059
- const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4070
+ const layoutKey = JSON.stringify([...[rect2.width, rect2.height, e.style.fontSize].map(String), e.style.font ?? "", source]);
4071
+ const lines = layouts.get(layoutKey, () => {
4072
+ const b = new Builder(e.style.fontSize, rect2.width, fam);
4073
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
4074
+ return layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4075
+ });
3060
4076
  const ink = color(e.style.stroke);
3061
4077
  let s = "";
3062
4078
  const marks = [];
@@ -3145,6 +4161,26 @@ function bounds(page, padding = 24) {
3145
4161
  return inset(r, -padding, -padding);
3146
4162
  }
3147
4163
  function plan(page) {
4164
+ const hit = plans.get(page);
4165
+ if (hit && unchanged(page, hit.fields)) return hit.plan;
4166
+ const made = makePlan(page);
4167
+ plans.set(page, { fields: { ...page }, plan: made });
4168
+ return made;
4169
+ }
4170
+ var plans = /* @__PURE__ */ new WeakMap();
4171
+ function hasPlan(page) {
4172
+ const hit = plans.get(page);
4173
+ return !!hit && unchanged(page, hit.fields);
4174
+ }
4175
+ function rememberPlan(page, made) {
4176
+ plans.set(page, { fields: { ...page }, plan: made });
4177
+ }
4178
+ function unchanged(page, fields) {
4179
+ const a = page, b = fields;
4180
+ const keys = Object.keys(a);
4181
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
4182
+ }
4183
+ function makePlan(page) {
3148
4184
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
3149
4185
  const times = frameTimes(tl);
3150
4186
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -3172,8 +4208,8 @@ function svgOpen(b, background, paint) {
3172
4208
  }
3173
4209
  return out;
3174
4210
  }
3175
- function svgParts(page, background, fixed, links = false) {
3176
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
4211
+ function svgParts(page, background, fixed, links = false, only) {
4212
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
3177
4213
  const theme = themeNamed(page.theme);
3178
4214
  const bg = background ? theme?.background ?? background : null;
3179
4215
  const ink = theme?.ink ?? black;
@@ -3184,11 +4220,11 @@ function svgParts(page, background, fixed, links = false) {
3184
4220
  const index = indexOf(d);
3185
4221
  const ordered = orderedElements(d);
3186
4222
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
3187
- 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"/>
4223
+ 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"/>
3188
4224
  `).join("");
3189
4225
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
3190
4226
  for (const e of ordered) {
3191
- if (!visible.has(e.layerID)) continue;
4227
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
3192
4228
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
3193
4229
  if (alpha <= 0) continue;
3194
4230
  let body = svgElement(e, index, bg, d);
@@ -3336,6 +4372,95 @@ function textElement(text, f, e, centered) {
3336
4372
  return s + "</text>\n";
3337
4373
  }
3338
4374
 
4375
+ // src/planner.ts
4376
+ var WORKER_WORK = 3e4;
4377
+ function planWork(page) {
4378
+ const tl = page.timeline;
4379
+ if (!tl || tl.camera?.some((k) => k.width > 1 && k.height > 1)) return 0;
4380
+ const elements = page.elements.length + (page.symbols ?? []).reduce((n, s) => n + s.elements.length, 0);
4381
+ return frameTimes(tl).length * elements;
4382
+ }
4383
+ function workerMaker() {
4384
+ if (typeof Worker === "undefined" || typeof __DOTTIE_WORKER__ === "undefined") return null;
4385
+ if (__DOTTIE_WORKER__ === "self" && typeof dottiePlayerScript === "function") {
4386
+ const source = `(${dottiePlayerScript.toString()})();
4387
+ `;
4388
+ return () => {
4389
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
4390
+ try {
4391
+ return new Worker(url);
4392
+ } finally {
4393
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
4394
+ }
4395
+ };
4396
+ }
4397
+ if (__DOTTIE_WORKER__ === "sibling") {
4398
+ const here = import.meta.url;
4399
+ if (!here) return null;
4400
+ return () => new Worker(new URL("./dottie-player.min.js", here));
4401
+ }
4402
+ return null;
4403
+ }
4404
+ var make;
4405
+ var worker = null;
4406
+ var failed = false;
4407
+ var nextID = 1;
4408
+ var waiting = /* @__PURE__ */ new Map();
4409
+ function plansInWorker(page) {
4410
+ if (failed || hasPlan(page) || planWork(page) < WORKER_WORK) return false;
4411
+ if (make === void 0) make = workerMaker();
4412
+ return make !== null;
4413
+ }
4414
+ function planInWorker(page) {
4415
+ return new Promise((resolve2) => {
4416
+ const done = (p) => {
4417
+ if (p) rememberPlan(page, p);
4418
+ resolve2();
4419
+ };
4420
+ let w;
4421
+ try {
4422
+ w = worker ?? start();
4423
+ } catch {
4424
+ failed = true;
4425
+ done(null);
4426
+ return;
4427
+ }
4428
+ const id = nextID++;
4429
+ waiting.set(id, { page, done });
4430
+ try {
4431
+ w.postMessage({ dottiePlan: id, page });
4432
+ } catch {
4433
+ waiting.delete(id);
4434
+ done(null);
4435
+ }
4436
+ });
4437
+ }
4438
+ function start() {
4439
+ const w = make();
4440
+ w.onmessage = (ev) => {
4441
+ const { dottiePlan: id, plan: p } = ev.data ?? {};
4442
+ const job = id === void 0 ? void 0 : waiting.get(id);
4443
+ if (!job) return;
4444
+ waiting.delete(id);
4445
+ job.done(p ?? null);
4446
+ if (waiting.size === 0 && worker === w) {
4447
+ w.terminate();
4448
+ worker = null;
4449
+ }
4450
+ };
4451
+ w.onerror = (ev) => {
4452
+ ev.preventDefault?.();
4453
+ failed = true;
4454
+ if (worker === w) worker = null;
4455
+ w.terminate();
4456
+ const jobs = [...waiting.values()];
4457
+ waiting.clear();
4458
+ for (const job of jobs) job.done(null);
4459
+ };
4460
+ worker = w;
4461
+ return w;
4462
+ }
4463
+
3339
4464
  // src/player.ts
3340
4465
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
3341
4466
  function pageIndex(file, spec) {
@@ -3376,6 +4501,12 @@ var Playback = class {
3376
4501
  __publicField(this, "sceneLoops");
3377
4502
  /** The page's variables now: their starting values until clicks change them. */
3378
4503
  __publicField(this, "state");
4504
+ /** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
4505
+ __publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
4506
+ /** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
4507
+ __publicField(this, "still", false);
4508
+ /** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
4509
+ __publicField(this, "carried", {});
3379
4510
  __publicField(this, "background");
3380
4511
  __publicField(this, "ink");
3381
4512
  /** What clicking each clickable element does, by element id. */
@@ -3399,7 +4530,7 @@ var Playback = class {
3399
4530
  __publicField(this, "order", []);
3400
4531
  __publicField(this, "frame", -1);
3401
4532
  __publicField(this, "onFrame", null);
3402
- this.page = options.clean ? withRoughness(page, 0) : page;
4533
+ this.page = options.clean ? cleanCopy(page) : page;
3403
4534
  addFonts(usedFonts(this.page));
3404
4535
  this.plan = plan(this.page);
3405
4536
  this.clicks = clickTargets(page, options.file);
@@ -3447,6 +4578,10 @@ var Playback = class {
3447
4578
  this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
3448
4579
  into.replaceChildren(this.svg);
3449
4580
  }
4581
+ /** Seconds since the variables last changed, or null for a still. */
4582
+ sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
4583
+ return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
4584
+ }
3450
4585
  get frameCount() {
3451
4586
  return this.plan.times.length;
3452
4587
  }
@@ -3472,7 +4607,7 @@ var Playback = class {
3472
4607
  }
3473
4608
  /** Whether a copy is still playing through after a click. */
3474
4609
  replaying(now = performance.now()) {
3475
- for (const [id, start] of this.replayStarts) if (now - start < (this.replays.get(id) ?? 0) * 1e3) return true;
4610
+ for (const [id, start2] of this.replayStarts) if (now - start2 < (this.replays.get(id) ?? 0) * 1e3) return true;
3476
4611
  return false;
3477
4612
  }
3478
4613
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -3485,17 +4620,24 @@ var Playback = class {
3485
4620
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
3486
4621
  const now = typeof performance === "undefined" ? 0 : performance.now();
3487
4622
  const times = /* @__PURE__ */ new Map();
3488
- for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
4623
+ for (const [id, start2] of this.replayStarts) times.set(id, Math.min((now - start2) / 1e3, this.replays.get(id) ?? 0));
3489
4624
  const tc = clampTime(t);
3490
- const pose = posed(this.page, tc, this.state);
3491
- const frame2 = expandingSymbols(pose, tc, true, times);
4625
+ const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4626
+ const made = new Expansions(pose, tc, true);
4627
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
3492
4628
  const { parts } = svgParts(frame2, this.background, r);
3493
4629
  const owned = (m, parts2) => {
3494
4630
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
3495
4631
  return m;
3496
4632
  };
3497
- 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) ?? "")));
3498
- 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)));
4633
+ const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(
4634
+ expandingSymbols(pose, tc, true, new Map([...times, ...st]), made),
4635
+ this.background,
4636
+ r,
4637
+ false,
4638
+ (key) => st.has(this.owner.get(key) ?? "")
4639
+ ).parts);
4640
+ 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);
3499
4641
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
3500
4642
  const normal = owned(/* @__PURE__ */ new Map(), parts);
3501
4643
  const merged = [];
@@ -3624,6 +4766,14 @@ ${h}</g>
3624
4766
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
3625
4767
  }
3626
4768
  };
4769
+ var cleanCopies = /* @__PURE__ */ new WeakMap();
4770
+ function cleanCopy(page) {
4771
+ const hit = cleanCopies.get(page);
4772
+ if (hit && unchanged(page, hit.fields)) return hit.page;
4773
+ const clean = withRoughness(page, 0);
4774
+ cleanCopies.set(page, { fields: { ...page }, page: clean });
4775
+ return clean;
4776
+ }
3627
4777
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3628
4778
  } : HTMLElement;
3629
4779
  var DottiePlayer = class extends ElementBase {
@@ -3646,6 +4796,10 @@ var DottiePlayer = class extends ElementBase {
3646
4796
  __publicField(this, "raf", 0);
3647
4797
  __publicField(this, "replayRaf", 0);
3648
4798
  __publicField(this, "loadToken", 0);
4799
+ /** Counts mounts, so a plan that comes back from a worker after a newer one is dropped. */
4800
+ __publicField(this, "mountToken", 0);
4801
+ /** The time a still shows; null while the drawing is animated. */
4802
+ __publicField(this, "stillTime", null);
3649
4803
  __publicField(this, "visible", true);
3650
4804
  __publicField(this, "observer", null);
3651
4805
  this.root = this.attachShadow({ mode: "open" });
@@ -3722,9 +4876,19 @@ var DottiePlayer = class extends ElementBase {
3722
4876
  text = inline2.textContent ?? "";
3723
4877
  }
3724
4878
  if (token !== this.loadToken) return;
3725
- this.setFile(parseDottie(text));
4879
+ this.showFile(parseDottie(text));
3726
4880
  } catch (err) {
3727
4881
  if (token !== this.loadToken) return;
4882
+ this.setPlaying(false);
4883
+ cancelAnimationFrame(this.raf);
4884
+ cancelAnimationFrame(this.replayRaf);
4885
+ this.raf = 0;
4886
+ this.replayRaf = 0;
4887
+ this.playback = null;
4888
+ this.file = null;
4889
+ this.mountToken++;
4890
+ this.stillTime = null;
4891
+ this.bar.hidden = true;
3728
4892
  this.stage.innerHTML = "";
3729
4893
  const box = document.createElement("div");
3730
4894
  box.className = "error";
@@ -3735,15 +4899,36 @@ var DottiePlayer = class extends ElementBase {
3735
4899
  }
3736
4900
  /** Shows an already-parsed file. */
3737
4901
  setFile(file) {
4902
+ this.loadToken++;
4903
+ this.showFile(file);
4904
+ }
4905
+ showFile(file) {
3738
4906
  this.file = file;
3739
4907
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
3740
- this.mount();
3741
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
4908
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
3742
4909
  }
3743
- mount() {
4910
+ /** Shows the file's page; then `mounted`. A big animation is planned in a worker first (planner.ts), and what was
4911
+ * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
4912
+ mount(mounted) {
3744
4913
  const file = this.file;
3745
4914
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
3746
- const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
4915
+ const clean = this.hasAttribute("clean");
4916
+ const token = ++this.mountToken;
4917
+ const shown = clean ? cleanCopy(page) : page;
4918
+ if (!plansInWorker(shown)) {
4919
+ this.mountPage(file, page, clean);
4920
+ mounted?.();
4921
+ return;
4922
+ }
4923
+ void planInWorker(shown).then(() => {
4924
+ if (token !== this.mountToken || this.file !== file) return;
4925
+ this.mountPage(file, page, clean);
4926
+ mounted?.();
4927
+ });
4928
+ }
4929
+ mountPage(file, page, clean) {
4930
+ const p = new Playback(page, this.stage, { clean, file });
4931
+ p.still = this.hasAttribute("at");
3747
4932
  this.playback = p;
3748
4933
  this.style.setProperty("--dottie-page", color(p.background));
3749
4934
  this.style.setProperty("--dottie-ink", color(p.ink));
@@ -3765,10 +4950,14 @@ var DottiePlayer = class extends ElementBase {
3765
4950
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
3766
4951
  };
3767
4952
  if (!animated) {
3768
- p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
4953
+ const i = at !== null ? nearest(p, Number(at) || 0) : p.posterFrame;
4954
+ this.stillTime = at !== null ? Number(at) || 0 : posterTime(page);
4955
+ p.drawAt(this.stillTime, i);
4956
+ p.frame = i;
3769
4957
  this.setPlaying(false);
3770
4958
  return;
3771
4959
  }
4960
+ this.stillTime = null;
3772
4961
  p.show(0);
3773
4962
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
3774
4963
  if (reduced) {
@@ -3782,8 +4971,10 @@ var DottiePlayer = class extends ElementBase {
3782
4971
  get paused() {
3783
4972
  return !this.playing;
3784
4973
  }
4974
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
3785
4975
  get currentTime() {
3786
4976
  const p = this.playback;
4977
+ if (p && this.stillTime !== null) return this.stillTime;
3787
4978
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
3788
4979
  }
3789
4980
  set currentTime(t) {
@@ -3837,7 +5028,13 @@ var DottiePlayer = class extends ElementBase {
3837
5028
  this.scheduleReplay();
3838
5029
  }
3839
5030
  if (!click) return;
3840
- if (click.set || click.toggle) this.setState(applyClick({ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null }, p.state));
5031
+ if (click.set || click.toggle || click.reset) {
5032
+ this.setState(applyClick(
5033
+ { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
5034
+ p.state,
5035
+ p.page.variables ?? {}
5036
+ ));
5037
+ }
3841
5038
  if ("state" in click) return;
3842
5039
  if ("url" in click) {
3843
5040
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
@@ -3863,7 +5060,10 @@ var DottiePlayer = class extends ElementBase {
3863
5060
  if (!p) return;
3864
5061
  const next = { ...p.state, ...values };
3865
5062
  if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
5063
+ const t = clampTime(p.last.t);
5064
+ p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
3866
5065
  p.state = next;
5066
+ p.stateSince = performance.now();
3867
5067
  p.redraw();
3868
5068
  this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
3869
5069
  }
@@ -4061,7 +5261,7 @@ function render(source, options = {}) {
4061
5261
  function isFile(v) {
4062
5262
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4063
5263
  }
4064
- var VERSION = "0.7.0";
5264
+ var VERSION = "0.9.0";
4065
5265
  export {
4066
5266
  BOIL_DRAWINGS,
4067
5267
  BUNDLED_FONTS,