@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.
@@ -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 __export = (target, all) => {
@@ -71,10 +71,53 @@ var MAX_NAME = 64;
71
71
  function isName(name) {
72
72
  return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
73
73
  }
74
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
75
+ var OPS = [">=", "<=", "==", "!=", ">", "<"];
76
+ function metricTerm(term) {
77
+ const t = trim(term);
78
+ if (!t.includes("(")) {
79
+ const op2 = t.search(/[>=<!]/);
80
+ if (op2 < 0) return null;
81
+ const name2 = trim(t.slice(0, op2));
82
+ if (!METRIC_NAMES.includes(name2)) return null;
83
+ const m = metricTerm(name2 + "() " + t.slice(op2));
84
+ return m ? { ...m, target: "" } : null;
85
+ }
86
+ const open = t.indexOf("("), close = t.indexOf(")");
87
+ if (open < 0 || close < 0 || open >= close) return null;
88
+ const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
89
+ let rest = trim(t.slice(close + 1));
90
+ if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
91
+ const op = OPS.find((o) => rest.startsWith(o));
92
+ if (op === void 0) return null;
93
+ rest = trim(rest.slice(op.length));
94
+ if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
95
+ return { metric: name, target, op, value: Number(rest) };
96
+ }
97
+ function passes(m, v) {
98
+ switch (m.op) {
99
+ case ">":
100
+ return v > m.value;
101
+ case ">=":
102
+ return v >= m.value;
103
+ case "<":
104
+ return v < m.value;
105
+ case "<=":
106
+ return v <= m.value;
107
+ case "==":
108
+ return v === m.value;
109
+ default:
110
+ return v !== m.value;
111
+ }
112
+ }
113
+ function mentionsMetric(condition) {
114
+ return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
115
+ }
74
116
  function conditionNames(condition) {
75
117
  const out = [];
76
118
  for (const any of condition.split("||")) {
77
119
  for (const all of any.split("&&")) {
120
+ if (metricTerm(all) !== null) continue;
78
121
  let term = trim(all);
79
122
  if (term.startsWith("!")) term = trim(term.slice(1));
80
123
  if (!isName(term)) return null;
@@ -83,9 +126,14 @@ function conditionNames(condition) {
83
126
  }
84
127
  return out;
85
128
  }
86
- function holds(condition, state) {
129
+ function holds(condition, state, metric = null) {
87
130
  if (conditionNames(condition) === null) return false;
88
131
  return condition.split("||").some((any) => any.split("&&").every((all) => {
132
+ const m = metricTerm(all);
133
+ if (m !== null) {
134
+ const v = metric ? metric(m) : null;
135
+ return v !== null && passes(m, v);
136
+ }
89
137
  let term = trim(all);
90
138
  let negated = false;
91
139
  if (term.startsWith("!")) {
@@ -107,11 +155,40 @@ function stateHidden(page, choice) {
107
155
  for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
108
156
  return out;
109
157
  }
158
+ function activeOverrides(e, values) {
159
+ if (!values || !e.overrides) return [];
160
+ return e.overrides.filter((o) => !mentionsMetric(o.when) && holds(o.when, values));
161
+ }
162
+ function looksOnly(o) {
163
+ return { ...o, reroute: null, offline: null, cost: null, capacity: null, delay: null };
164
+ }
165
+ function applyOverride(o, e) {
166
+ const out = cloneElement(e);
167
+ if (o.stroke) out.style.stroke = { ...o.stroke };
168
+ if (o.fill) out.style.fill = { ...o.fill };
169
+ if (o.textColor) out.style.textColor = { ...o.textColor };
170
+ if (o.strokeWidth !== null) out.style.strokeWidth = Math.max(0, o.strokeWidth);
171
+ if (o.dash !== null) out.style.dash = o.dash;
172
+ if (o.text !== null) out.text = o.text;
173
+ if (o.cost !== null && out.connector) out.connector.cost = Math.min(Math.max(o.cost, 0), 1e6);
174
+ if (o.capacity !== null) out.capacity = Math.max(o.capacity, 0);
175
+ if (o.delay !== null && out.kind !== "connector") out.delay = Math.max(o.delay, 0);
176
+ if (o.opacity !== null) {
177
+ const v = Math.min(Math.max(o.opacity, 0), 1) * (out.style.opacity ?? 1);
178
+ out.style.opacity = v >= 1 ? null : v;
179
+ }
180
+ return out;
181
+ }
110
182
  function changesState(c) {
111
- return !!c && (Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
183
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
112
184
  }
113
- function applyClick(c, state) {
114
- const out = { ...state };
185
+ function applyClick(c, state, initial = {}) {
186
+ let out = { ...state };
187
+ if (c.reset === true) {
188
+ out = {};
189
+ for (const k of Object.keys(state)) out[k] = false;
190
+ Object.assign(out, initial);
191
+ }
115
192
  for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
116
193
  for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
117
194
  return out;
@@ -126,7 +203,9 @@ var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand",
126
203
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
127
204
  var KNOWN_FIELDS = {
128
205
  file: ["version", "pages", "images"],
129
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables"],
206
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
207
+ flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
208
+ flowOverride: ["when", "volume"],
130
209
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
131
210
  layer: ["id", "name", "visible", "locked"],
132
211
  rgba: ["r", "g", "b", "a"],
@@ -155,6 +234,7 @@ var KNOWN_FIELDS = {
155
234
  "click",
156
235
  "hover",
157
236
  "when",
237
+ "overrides",
158
238
  "symbol",
159
239
  "playback",
160
240
  "offset",
@@ -163,8 +243,13 @@ var KNOWN_FIELDS = {
163
243
  "reveal",
164
244
  "ports",
165
245
  "labelRect",
166
- "extension"
246
+ "extension",
247
+ "capacity",
248
+ "delay",
249
+ "queue",
250
+ "gauge"
167
251
  ],
252
+ gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
168
253
  strokePoint: ["x", "y"],
169
254
  subpath: ["closed", "coords"],
170
255
  inlineImage: ["data", "format"],
@@ -172,7 +257,7 @@ var KNOWN_FIELDS = {
172
257
  extensionRef: ["id", "shape", "version", "params"],
173
258
  normalizedPoint: ["x", "y"],
174
259
  normalizedRect: ["x", "y", "width", "height"],
175
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
260
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
176
261
  waypoint: ["x", "y"],
177
262
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
178
263
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -181,8 +266,9 @@ var KNOWN_FIELDS = {
181
266
  repeat: ["start", "end", "count"],
182
267
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
183
268
  marker: ["t", "name", "loop"],
184
- click: ["scene", "url", "set", "toggle"],
185
- hover: ["stroke", "fill", "opacity"]
269
+ click: ["scene", "url", "set", "toggle", "reset"],
270
+ hover: ["stroke", "fill", "opacity"],
271
+ override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
186
272
  };
187
273
  var MAX_DURATION = 3600;
188
274
  var MAX_MARCH = 1e4;
@@ -198,6 +284,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
198
284
  var str = (v) => typeof v === "string" ? v : void 0;
199
285
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
200
286
  var bool = (v) => typeof v === "boolean" ? v : void 0;
287
+ var NO_ID = "00000000-0000-0000-0000-000000000000";
288
+ 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}$/;
201
289
  var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
202
290
  function clampTime(t) {
203
291
  if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
@@ -289,7 +377,8 @@ var Reader = class {
289
377
  this.fields(w, "waypoint");
290
378
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
291
379
  }) : null,
292
- hops: typeof v.hops === "boolean" ? v.hops : null
380
+ hops: typeof v.hops === "boolean" ? v.hops : null,
381
+ cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
293
382
  };
294
383
  }
295
384
  image(v, table) {
@@ -341,6 +430,7 @@ var Reader = class {
341
430
  click: this.click(v.click),
342
431
  hover: this.hover(v.hover),
343
432
  when: str(v.when) ?? null,
433
+ overrides: this.overrides(v.overrides),
344
434
  symbol: uuid(v.symbol) ?? null,
345
435
  playback: this.playback(v.playback),
346
436
  offset: num(v.offset) ?? null,
@@ -352,7 +442,26 @@ var Reader = class {
352
442
  return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
353
443
  }) : null,
354
444
  labelRect: this.labelRect(v.labelRect),
355
- extension: this.extension(v.extension)
445
+ extension: this.extension(v.extension),
446
+ capacity: num(v.capacity) ?? null,
447
+ delay: num(v.delay) ?? null,
448
+ queue: v.queue === true ? true : null,
449
+ gauge: this.gauge(v.gauge)
450
+ };
451
+ }
452
+ gauge(v) {
453
+ if (!isObj(v)) return null;
454
+ this.fields(v, "gauge");
455
+ const finite = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
456
+ const style2 = str(v.style);
457
+ return {
458
+ metric: str(v.metric) ?? "",
459
+ of: typeof v.of === "string" && UUID.test(v.of) ? v.of.toUpperCase() : NO_ID,
460
+ max: finite(v.max),
461
+ warn: finite(v.warn),
462
+ alarm: finite(v.alarm),
463
+ style: style2 === "number" || style2 === "bar" || style2 === "dial" ? style2 : null,
464
+ label: str(v.label) ?? null
356
465
  };
357
466
  }
358
467
  labelRect(v) {
@@ -396,7 +505,7 @@ var Reader = class {
396
505
  }
397
506
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
398
507
  const toggle = raw ? raw.filter(isName) : null;
399
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null };
508
+ return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
400
509
  }
401
510
  playback(v) {
402
511
  const s = str(v);
@@ -432,6 +541,35 @@ var Reader = class {
432
541
  params: strings(v.params)
433
542
  };
434
543
  }
544
+ /** As DottieKit reads them: a reroute whose targets aren't all ids is dropped, as are keys that aren't ids. */
545
+ overrides(v) {
546
+ if (!Array.isArray(v)) return null;
547
+ return v.filter(isObj).map((o) => {
548
+ this.fields(o, "override");
549
+ const dash = str(o.dash);
550
+ let reroute = null;
551
+ if (isObj(o.reroute) && Object.values(o.reroute).every((x) => typeof x === "string" && UUID.test(x))) {
552
+ reroute = {};
553
+ for (const [k, x] of Object.entries(o.reroute)) if (UUID.test(k)) reroute[k.toUpperCase()] = x.toUpperCase();
554
+ if (Object.keys(reroute).length === 0) reroute = null;
555
+ }
556
+ return {
557
+ when: str(o.when) ?? "",
558
+ stroke: rgba(o.stroke) ?? null,
559
+ fill: rgba(o.fill) ?? null,
560
+ textColor: rgba(o.textColor) ?? null,
561
+ opacity: num(o.opacity) ?? null,
562
+ strokeWidth: num(o.strokeWidth) ?? null,
563
+ dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
564
+ text: str(o.text) ?? null,
565
+ reroute,
566
+ offline: o.offline === true ? true : null,
567
+ cost: typeof o.cost === "number" && Number.isFinite(o.cost) ? o.cost : null,
568
+ capacity: typeof o.capacity === "number" && Number.isFinite(o.capacity) ? o.capacity : null,
569
+ delay: typeof o.delay === "number" && Number.isFinite(o.delay) ? o.delay : null
570
+ };
571
+ });
572
+ }
435
573
  hover(v) {
436
574
  if (!isObj(v)) return null;
437
575
  this.fields(v, "hover");
@@ -471,15 +609,15 @@ var Reader = class {
471
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);
472
610
  stableSortBy(keys, (k) => k.t);
473
611
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
474
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
475
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
476
- 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 }];
477
615
  }) : null;
478
616
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
479
- const start = num(r.start), end = num(r.end);
480
- 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 [];
481
619
  const count = num(r.count);
482
- 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) };
483
621
  return repeatIsValid(rep) ? [rep] : [];
484
622
  }) : null;
485
623
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -531,7 +669,33 @@ var Reader = class {
531
669
  theme: str(v.theme) ?? null,
532
670
  timeline: this.timeline(v.timeline),
533
671
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
534
- variables: this.variables(v.variables)
672
+ variables: this.variables(v.variables),
673
+ flows: Array.isArray(v.flows) ? v.flows.filter(isObj).map((f) => this.flow(f)) : null
674
+ };
675
+ }
676
+ /** As DottieKit reads one: a missing or bad id, source or destination sends nothing; bad destinations are dropped. */
677
+ flow(f) {
678
+ this.fields(f, "flow");
679
+ const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
680
+ const finite = (x) => {
681
+ const n = num(x);
682
+ return n !== void 0 && Number.isFinite(n) ? n : null;
683
+ };
684
+ return {
685
+ id: id(f.id),
686
+ from: id(f.from),
687
+ to: Array.isArray(f.to) && f.to.every((x) => typeof x === "string") ? f.to.filter((x) => UUID.test(x)).map((x) => x.toUpperCase()) : [],
688
+ every: finite(f.every),
689
+ speed: finite(f.speed),
690
+ size: finite(f.size),
691
+ color: isObj(f.color) && ["r", "g", "b", "a"].every((k) => typeof f.color[k] === "number") ? rgba(f.color) ?? null : null,
692
+ when: str(f.when) ?? null,
693
+ spread: f.spread === true ? true : null,
694
+ volume: finite(f.volume),
695
+ overrides: Array.isArray(f.overrides) && f.overrides.length > 0 ? f.overrides.filter(isObj).map((o) => {
696
+ this.fields(o, "flowOverride");
697
+ return { when: str(o.when) ?? "", volume: finite(o.volume) };
698
+ }) : null
535
699
  };
536
700
  }
537
701
  };
@@ -593,7 +757,8 @@ function cloneElement(e) {
593
757
  start: { ...e.connector.start },
594
758
  end: { ...e.connector.end },
595
759
  waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
596
- } : null
760
+ } : null,
761
+ gauge: e.gauge ? { ...e.gauge } : e.gauge
597
762
  };
598
763
  }
599
764
  function orderedElements(page) {
@@ -677,6 +842,21 @@ function pieces(para) {
677
842
  if (cur || out.length === 0) out.push(cur);
678
843
  return out;
679
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
+ }
680
860
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
681
861
  const width = (t) => measure(t, fontSize, face, font);
682
862
  const out = [];
@@ -692,8 +872,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
692
872
  }
693
873
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
694
874
  const chars = [...line2];
875
+ const prefix = prefixWidths(chars, fontSize, face, font);
695
876
  let n = chars.length - 1;
696
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
877
+ while (n > 1 && prefix[n] > maxWidth) n--;
697
878
  out.push(chars.slice(0, n).join(""));
698
879
  line2 = chars.slice(n).join("");
699
880
  }
@@ -848,13 +1029,13 @@ var Path = class _Path {
848
1029
  contours() {
849
1030
  const out = [];
850
1031
  let current = [];
851
- let start = { x: 0, y: 0 };
1032
+ let start2 = { x: 0, y: 0 };
852
1033
  for (const s of this.segments) {
853
- const last = current.length ? current[current.length - 1] : start;
1034
+ const last = current.length ? current[current.length - 1] : start2;
854
1035
  switch (s.op) {
855
1036
  case "M":
856
1037
  if (current.length > 1) out.push(current);
857
- start = s.p;
1038
+ start2 = s.p;
858
1039
  current = [s.p];
859
1040
  break;
860
1041
  case "L":
@@ -876,9 +1057,9 @@ var Path = class _Path {
876
1057
  }
877
1058
  break;
878
1059
  case "Z":
879
- current.push(start);
1060
+ current.push(start2);
880
1061
  if (current.length > 1) out.push(current);
881
- current = [start];
1062
+ current = [start2];
882
1063
  break;
883
1064
  }
884
1065
  }
@@ -1270,9 +1451,17 @@ function midpoint(e, index) {
1270
1451
  }
1271
1452
  return pts[0];
1272
1453
  }
1454
+ var indexes = /* @__PURE__ */ new WeakMap();
1455
+ function isIndexOf(page, index) {
1456
+ return indexes.get(page.elements)?.index === index;
1457
+ }
1273
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;
1274
1462
  const m = /* @__PURE__ */ new Map();
1275
- 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 });
1276
1465
  return m;
1277
1466
  }
1278
1467
 
@@ -1344,8 +1533,712 @@ function mulConst(step) {
1344
1533
  return [p[0], p[1]];
1345
1534
  }
1346
1535
 
1536
+ // src/flows.ts
1537
+ var DEFAULT_EVERY = 0.5;
1538
+ var DEFAULT_SPEED = 160;
1539
+ var DEFAULT_SIZE = 12;
1540
+ var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
1541
+ var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
1542
+ function packetID(flow, k) {
1543
+ const n = (k + 2 ** 40) % 2 ** 48;
1544
+ return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1545
+ }
1546
+ var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
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) {
1556
+ const c = e.connector;
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;
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;
1634
+ const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1635
+ let counter = 1;
1636
+ const tree = new FlowTree(from);
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;
1642
+ tree.settled.add(n);
1643
+ if (n === until && n !== from) break;
1644
+ for (const edge of next.get(n) ?? []) {
1645
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1646
+ if (tree.settled.has(edge.to)) continue;
1647
+ const d = dist.get(n) + edge.cost;
1648
+ const old = dist.get(edge.to);
1649
+ if (old !== void 0 && old <= d) continue;
1650
+ dist.set(edge.to, d);
1651
+ order.set(edge.to, counter);
1652
+ open.push(edge.to, d, counter++);
1653
+ tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1654
+ }
1655
+ }
1656
+ return tree;
1657
+ }
1658
+ var FlowTree = class {
1659
+ constructor(from) {
1660
+ __publicField(this, "settled", /* @__PURE__ */ new Set());
1661
+ __publicField(this, "came", /* @__PURE__ */ new Map());
1662
+ __publicField(this, "from");
1663
+ this.from = from;
1664
+ }
1665
+ /** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
1666
+ goals(to, spread) {
1667
+ const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
1668
+ if (reachable.length === 0) return [];
1669
+ return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
1670
+ }
1671
+ /** The connectors to `goal`, as [connector, backwards] hops. */
1672
+ hops(goal) {
1673
+ const hops = [];
1674
+ for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
1675
+ return hops.reverse();
1676
+ }
1677
+ };
1678
+
1679
+ // src/measure.ts
1680
+ var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
1681
+ function metricValue(m, metric, id) {
1682
+ const f = m.flows.get(id);
1683
+ if (f) {
1684
+ switch (metric) {
1685
+ case "throughput":
1686
+ return f.placed;
1687
+ case "backlog":
1688
+ return f.backlog;
1689
+ case "backup":
1690
+ return f.backup;
1691
+ case "transit":
1692
+ return f.transit;
1693
+ default:
1694
+ return null;
1695
+ }
1696
+ }
1697
+ const e = m.elements.get(id);
1698
+ if (!e) return null;
1699
+ switch (metric) {
1700
+ case "throughput":
1701
+ return e.throughput;
1702
+ case "load":
1703
+ return e.load;
1704
+ case "delay":
1705
+ return e.delay;
1706
+ case "backlog":
1707
+ return e.backlog;
1708
+ case "inFlight":
1709
+ return e.inFlight;
1710
+ case "depth":
1711
+ return e.depth;
1712
+ default:
1713
+ return null;
1714
+ }
1715
+ }
1716
+ var TINY = 1e-9;
1717
+ var amount = (f) => clamp(f.volume, 1, 0, 1e12);
1718
+ function amountIn(f, values) {
1719
+ if (values === null || !f.overrides) return amount(f);
1720
+ let v = null;
1721
+ for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
1722
+ return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
1723
+ }
1724
+ function delayOf(e) {
1725
+ const d = e.delay ?? null;
1726
+ if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
1727
+ return Math.min(Math.max(d, 0), 3600);
1728
+ }
1729
+ var queueKey = (shape, flow) => shape + "|" + flow;
1730
+ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1731
+ const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
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
+ };
1742
+ const metrics2 = emptyMetrics();
1743
+ for (const e of frame2.elements) {
1744
+ if (e.kind === "connector" && !e.connector) continue;
1745
+ metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
1746
+ }
1747
+ const list = flows ?? [];
1748
+ const room = /* @__PURE__ */ new Map();
1749
+ for (const e of frame2.elements) {
1750
+ const c = e.capacity ?? null;
1751
+ if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1752
+ }
1753
+ const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1754
+ const route2 = (tree, goal) => {
1755
+ const hops = [], shapes2 = [];
1756
+ let at = goal;
1757
+ for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
1758
+ hops.push([step.via, step.back]);
1759
+ shapes2.push(at);
1760
+ at = step.from;
1761
+ }
1762
+ shapes2.push(at);
1763
+ return { hops: hops.reverse(), shapes: shapes2.reverse() };
1764
+ };
1765
+ const way = (hops, shapes2) => {
1766
+ const points = [], inside = [], stretches = [];
1767
+ let length = 0;
1768
+ hops.forEach(([id, back], k) => {
1769
+ const c = byID.get(id);
1770
+ if (!c) return;
1771
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1772
+ if (line2.length === 0) return;
1773
+ const last = points[points.length - 1];
1774
+ if (last) {
1775
+ const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1776
+ const shape = byID.get(shapes2[k]);
1777
+ inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1778
+ length += gap;
1779
+ }
1780
+ const start2 = length;
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);
1782
+ stretches.push([id, start2, length]);
1783
+ points.push(...line2);
1784
+ });
1785
+ return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
1786
+ };
1787
+ const along = (w, elapsedAt, speed) => {
1788
+ let elapsed = elapsedAt, d = 0;
1789
+ for (const [a, b, delay] of w.inside) {
1790
+ const reach = (a - d) / speed;
1791
+ if (elapsed <= reach) break;
1792
+ elapsed -= reach;
1793
+ d = a;
1794
+ const stay = (b - a) / speed + delay;
1795
+ if (elapsed < stay) return null;
1796
+ elapsed -= stay;
1797
+ d = b;
1798
+ }
1799
+ d += elapsed * speed;
1800
+ return d >= 0 && d <= w.length ? d : null;
1801
+ };
1802
+ const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
1803
+ const bump = (id, f) => {
1804
+ const m = metrics2.elements.get(id);
1805
+ if (m) f(m);
1806
+ };
1807
+ const leaving = /* @__PURE__ */ new Map(), turn = [];
1808
+ for (const flow of list) {
1809
+ const n = leaving.get(flow.from) ?? 0;
1810
+ turn.push(n);
1811
+ leaving.set(flow.from, n + 1);
1812
+ }
1813
+ const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
1814
+ const packets = [];
1815
+ const waitingBy = /* @__PURE__ */ new Map();
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
+ };
1822
+ for (const [i, flow] of list.entries()) {
1823
+ if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1824
+ const source = byID.get(flow.from);
1825
+ const base2 = source ? baseTree(flow.from) : null;
1826
+ if (!source || !base2) continue;
1827
+ const volume = amountIn(flow, values);
1828
+ const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1829
+ const placements = [];
1830
+ const waiting2 = [];
1831
+ const goals = base2.goals(flow.to, flow.spread === true);
1832
+ const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1833
+ for (const [candidates, share] of shares) {
1834
+ if (!(share > TINY)) continue;
1835
+ let left = share;
1836
+ for (let n = 0; n < 32 && left > TINY; n++) {
1837
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1838
+ const goal2 = tree?.goals(candidates, false)[0];
1839
+ if (!tree || goal2 === void 0) break;
1840
+ const r = route2(tree, goal2);
1841
+ const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1842
+ const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1843
+ const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
1844
+ if (!(fits > TINY)) break;
1845
+ for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
1846
+ placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
1847
+ left -= fits;
1848
+ }
1849
+ if (!(left > TINY)) continue;
1850
+ let at = flow.from;
1851
+ const goal = base2.goals(candidates, false)[0];
1852
+ if (goal !== void 0) {
1853
+ const r = route2(base2, goal), done = full();
1854
+ const sequence = [[r.shapes[0], r.shapes[0]]];
1855
+ r.hops.forEach(([id], k) => {
1856
+ sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
1857
+ });
1858
+ const first = sequence.find(([id]) => done.has(id));
1859
+ if (first) {
1860
+ at = first[1];
1861
+ const upTo = r.shapes.indexOf(at);
1862
+ for (let q = upTo; q >= 0; q--) {
1863
+ const s = byID.get(r.shapes[q]);
1864
+ if (s && s.queue === true && s.kind !== "connector") {
1865
+ at = r.shapes[q];
1866
+ break;
1867
+ }
1868
+ }
1869
+ }
1870
+ const n = r.shapes.indexOf(at);
1871
+ if (at !== flow.from && n >= 0) {
1872
+ const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
1873
+ for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
1874
+ placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1875
+ }
1876
+ }
1877
+ const existing = waiting2.find(([s]) => s === at);
1878
+ if (existing) existing[1] += left;
1879
+ else waiting2.push([at, left]);
1880
+ }
1881
+ waitingBy.set(flow.id, waiting2);
1882
+ for (const p of placements) {
1883
+ for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1884
+ m.throughput += p.amount;
1885
+ });
1886
+ if (!p.waits) fm.placed += p.amount;
1887
+ }
1888
+ for (const [at, a] of waiting2) {
1889
+ bump(at, (m) => {
1890
+ m.backlog += a;
1891
+ });
1892
+ fm.backlog += a;
1893
+ }
1894
+ const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
1895
+ fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
1896
+ const every = everyOf(flow);
1897
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1898
+ const phase = turn[i] / leaving.get(flow.from) * every;
1899
+ const color2 = flow.color ?? DEFAULT_COLOR;
1900
+ const cumulative = [];
1901
+ let sum = 0;
1902
+ for (const p of placements) {
1903
+ sum += p.amount / Math.max(volume, TINY);
1904
+ cumulative.push(sum);
1905
+ }
1906
+ if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
1907
+ let transitSum = 0;
1908
+ placements.forEach((p, r) => {
1909
+ const w = way(p.hops, p.shapes);
1910
+ if (!w) return;
1911
+ const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
1912
+ if (!p.waits) transitSum += travel * p.amount;
1913
+ const lo = Math.ceil((t - phase - travel) / every);
1914
+ const hi = Math.floor((t - phase) / every);
1915
+ const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
1916
+ const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
1917
+ for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
1918
+ if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
1919
+ const d = along(w, t - k * every - phase, speed);
1920
+ if (d === null) continue;
1921
+ const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
1922
+ if (s) bump(s[0], (m) => {
1923
+ m.inFlight += 1;
1924
+ });
1925
+ const pt2 = w.line.point(d / w.length);
1926
+ packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
1927
+ }
1928
+ });
1929
+ fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
1930
+ metrics2.flows.set(flow.id, fm);
1931
+ }
1932
+ const piles = /* @__PURE__ */ new Map();
1933
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1934
+ for (const flow of list) {
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) {
1939
+ const c = carried[queueKey(e.id, flow.id)];
1940
+ if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1941
+ }
1942
+ if (shapes2.length === 0) continue;
1943
+ const volume = amountIn(flow, values);
1944
+ const every = everyOf(flow);
1945
+ const unit = every * Math.max(volume, TINY);
1946
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1947
+ const color2 = flow.color ?? DEFAULT_COLOR;
1948
+ shapes2.forEach((at, j) => {
1949
+ const shape = byID.get(at);
1950
+ if (!shape) return;
1951
+ const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1952
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1953
+ const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1954
+ let depth = before + backlog * since;
1955
+ if (backlog <= TINY && before > TINY) {
1956
+ const trial = new Map(room);
1957
+ const outlets = [];
1958
+ let total = 0;
1959
+ for (let n2 = 0; n2 < 32; n2++) {
1960
+ const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1961
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1962
+ const goal = tree?.goals(flow.to, false)[0];
1963
+ if (!tree || goal === void 0) break;
1964
+ const r = route2(tree, goal);
1965
+ const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1966
+ const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1967
+ const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
1968
+ const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
1969
+ if (!(rate > TINY)) break;
1970
+ for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
1971
+ outlets.push({ hops: r.hops, shapes: r.shapes, rate });
1972
+ total += rate;
1973
+ if (!Number.isFinite(spare)) break;
1974
+ }
1975
+ if (total > TINY) {
1976
+ depth = Math.max(0, before - total * since);
1977
+ if (depth > TINY) {
1978
+ for (const [id, v] of trial) room.set(id, v);
1979
+ for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
1980
+ m.throughput += o.rate;
1981
+ });
1982
+ }
1983
+ const lasts = before / total;
1984
+ outlets.forEach((o, i) => {
1985
+ const w = way(o.hops, o.shapes);
1986
+ if (!w) return;
1987
+ const spacing = Math.max(unit / o.rate, 0.05);
1988
+ for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
1989
+ const d = along(w, since - k * spacing, speed);
1990
+ if (d === null) continue;
1991
+ const pt2 = w.line.point(d / w.length);
1992
+ 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));
1993
+ }
1994
+ });
1995
+ }
1996
+ }
1997
+ if (!(depth > TINY || backlog > TINY)) return;
1998
+ bump(at, (m) => {
1999
+ m.depth += depth;
2000
+ });
2001
+ if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
2002
+ const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
2003
+ const rows = Math.trunc((n + 9) / 10);
2004
+ const base2 = piles.get(at) ?? 0;
2005
+ piles.set(at, base2 + rows);
2006
+ for (let q = 0; q < n; q++) {
2007
+ const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
2008
+ const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
2009
+ packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
2010
+ }
2011
+ });
2012
+ }
2013
+ for (const e of frame2.elements) {
2014
+ const c = e.capacity ?? null;
2015
+ const m = metrics2.elements.get(e.id);
2016
+ if (c === null || !Number.isFinite(c) || !m) continue;
2017
+ m.load = c > 0 ? m.throughput / c : 1;
2018
+ }
2019
+ return { packets, metrics: metrics2 };
2020
+ }
2021
+ var BLANK = {
2022
+ seed: 1,
2023
+ text: null,
2024
+ connector: null,
2025
+ groupID: null,
2026
+ points: null,
2027
+ path: null,
2028
+ subpaths: null,
2029
+ evenOdd: null,
2030
+ rotation: null,
2031
+ link: null,
2032
+ url: null,
2033
+ anchor: null,
2034
+ click: null,
2035
+ hover: null,
2036
+ when: null,
2037
+ overrides: null,
2038
+ symbol: null,
2039
+ playback: null,
2040
+ offset: null,
2041
+ args: null,
2042
+ image: null,
2043
+ reveal: null
2044
+ };
2045
+ function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
2046
+ return {
2047
+ stroke: { ...stroke },
2048
+ fill: { ...fill },
2049
+ fillStyle,
2050
+ strokeWidth,
2051
+ dash: "solid",
2052
+ roughness: 0,
2053
+ bowing: 1,
2054
+ fontSize: 18,
2055
+ font: null,
2056
+ gridSize: null,
2057
+ opacity: opacity2,
2058
+ dashOffset: null,
2059
+ textColor: null
2060
+ };
2061
+ }
2062
+ function packet(id, x, y, size, color2, layerID) {
2063
+ return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
2064
+ }
2065
+ function metricLookup(m, frame2) {
2066
+ const anchors = /* @__PURE__ */ new Map();
2067
+ for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
2068
+ 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}$/;
2069
+ return (term, own) => {
2070
+ const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
2071
+ return id === null ? null : metricValue(m, term.metric, id);
2072
+ };
2073
+ }
2074
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
2075
+ const lookup = metricLookup(metrics2, frame2);
2076
+ const plan2 = [], readings = {};
2077
+ for (const e of elements) {
2078
+ const applied = [];
2079
+ if (values !== null && e.overrides) {
2080
+ e.overrides.forEach((o, k) => {
2081
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2082
+ });
2083
+ }
2084
+ const g = values === null ? null : e.gauge;
2085
+ if (!g) {
2086
+ plan2.push({ applied, gauge: null });
2087
+ continue;
2088
+ }
2089
+ let shown = metricValue(metrics2, g.metric, g.of);
2090
+ const then = carried[gaugeKey(e.id)];
2091
+ if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
2092
+ const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
2093
+ shown = then + (shown - then) * smooth;
2094
+ }
2095
+ if (shown !== null) readings[gaugeKey(e.id)] = shown;
2096
+ plan2.push({ applied, gauge: { value: shown } });
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
+ });
2109
+ return { elements: out, readings };
2110
+ }
2111
+ var gaugeKey = (id) => "gauge|" + id;
2112
+ var GAUGE_EASE = 1;
2113
+ var hex = (h) => {
2114
+ const v = parseInt(h.slice(1), 16);
2115
+ return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
2116
+ };
2117
+ var GAUGE_GREEN = hex("#16a34a");
2118
+ var GAUGE_AMBER = hex("#d97706");
2119
+ var GAUGE_RED = hex("#dc2626");
2120
+ var GAUGE_BLUE = hex("#2f6fdf");
2121
+ var GAUGE_TRACK = hex("#e5e7eb");
2122
+ var GAUGE_GRAY = hex("#9ca3af");
2123
+ function formatted(metric, v) {
2124
+ if (v === null || !Number.isFinite(v)) return "\u2013";
2125
+ switch (metric) {
2126
+ case "load":
2127
+ case "backup":
2128
+ return formatNumber(v * 100, 0) + "%";
2129
+ case "transit":
2130
+ case "delay":
2131
+ return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
2132
+ default:
2133
+ return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
2134
+ }
2135
+ }
2136
+ function formatNumber(v, places) {
2137
+ const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
2138
+ const n = Math.floor(Math.abs(v) * scale + 0.5);
2139
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
2140
+ const whole = Math.trunc(n / scale), part = n - whole * scale;
2141
+ let s = String(whole);
2142
+ if (places > 0 && part > 0) {
2143
+ let digits = String(part);
2144
+ while (digits.length < places) digits = "0" + digits;
2145
+ while (digits.endsWith("0")) digits = digits.slice(0, -1);
2146
+ s += "." + digits;
2147
+ }
2148
+ return (v < 0 && n > 0 ? "-" : "") + s;
2149
+ }
2150
+ function gauge(e, g, value2, frame2, flows) {
2151
+ const target = frame2.elements.find((x) => x.id === g.of);
2152
+ const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
2153
+ const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
2154
+ const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
2155
+ let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
2156
+ if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
2157
+ if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
2158
+ if (value2 === null) color2 = GAUGE_GRAY;
2159
+ let name = g.label;
2160
+ if (name === null) {
2161
+ const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
2162
+ name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
2163
+ }
2164
+ const kind = g.style ?? "number";
2165
+ const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
2166
+ const line2 = (a, b) => [
2167
+ a.x / w,
2168
+ a.y / h,
2169
+ (a.x + (b.x - a.x) / 3) / w,
2170
+ (a.y + (b.y - a.y) / 3) / h,
2171
+ (a.x + 2 * (b.x - a.x) / 3) / w,
2172
+ (a.y + 2 * (b.y - a.y) / 3) / h,
2173
+ b.x / w,
2174
+ b.y / h
2175
+ ];
2176
+ const box = (x, y, bw, bh) => {
2177
+ const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
2178
+ let coords = line2(p[0], p[1]);
2179
+ for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
2180
+ return { closed: true, coords };
2181
+ };
2182
+ const arc = (c, radius, a0, a1) => {
2183
+ const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
2184
+ const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
2185
+ const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
2186
+ const s = at(a0);
2187
+ const coords = [s.x / w, s.y / h];
2188
+ for (let k = 0; k < pieces2; k++) {
2189
+ const a = a0 + k * step, b = a + step;
2190
+ const p0 = at(a), p3 = at(b);
2191
+ const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
2192
+ const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
2193
+ coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
2194
+ }
2195
+ return { closed: false, coords };
2196
+ };
2197
+ const part = (k, subpaths, stroke, fill, width) => ({
2198
+ ...BLANK,
2199
+ id: packetID(e.id, -(2 ** 38) - k),
2200
+ kind: "path",
2201
+ layerID: e.layerID,
2202
+ x: e.x,
2203
+ y: e.y,
2204
+ width: e.width,
2205
+ height: e.height,
2206
+ style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
2207
+ subpaths
2208
+ });
2209
+ const back = {
2210
+ ...e,
2211
+ gauge: null,
2212
+ kind: "path",
2213
+ subpaths: [box(0, 0, w, h)],
2214
+ text: name + "\n" + formatted(g.metric, value2),
2215
+ style: { ...e.style }
2216
+ };
2217
+ const parts = [];
2218
+ if (kind === "number") {
2219
+ back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
2220
+ back.style.textColor = { ...color2 };
2221
+ } else if (kind === "bar") {
2222
+ back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
2223
+ const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
2224
+ const trackW = w - 2 * inset2;
2225
+ parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
2226
+ if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
2227
+ } else {
2228
+ back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
2229
+ const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
2230
+ const thick = Math.max(2, radius * 0.16);
2231
+ parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
2232
+ if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
2233
+ const a = Math.PI + Math.PI * fraction;
2234
+ const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
2235
+ parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
2236
+ }
2237
+ return [back, ...parts];
2238
+ }
2239
+
1347
2240
  // src/rough.ts
1348
- var offset = (rng, amount, roughness) => rng.signed() * amount * roughness;
2241
+ var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
1349
2242
  function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1350
2243
  const length = Math.hypot(b.x - a.x, b.y - a.y);
1351
2244
  if (!(length > 0.5)) return;
@@ -1358,7 +2251,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1358
2251
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
1359
2252
  const dx = b.x - a.x, dy = b.y - a.y;
1360
2253
  const nx = -dy / length, ny = dx / length;
1361
- const start = preserveEnds ? a : (() => {
2254
+ const start2 = preserveEnds ? a : (() => {
1362
2255
  const x = a.x + j();
1363
2256
  return { x, y: a.y + j() };
1364
2257
  })();
@@ -1370,7 +2263,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1370
2263
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
1371
2264
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
1372
2265
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
1373
- into.moveTo(start);
2266
+ into.moveTo(start2);
1374
2267
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
1375
2268
  }
1376
2269
  }
@@ -1631,10 +2524,10 @@ var Cache = class {
1631
2524
  __publicField(this, "limit");
1632
2525
  this.limit = limit;
1633
2526
  }
1634
- get(key, make) {
2527
+ get(key, make2) {
1635
2528
  const hit = this.map.get(key);
1636
2529
  if (hit !== void 0) return hit;
1637
- const v = make();
2530
+ const v = make2();
1638
2531
  if (this.map.size >= this.limit) this.map.clear();
1639
2532
  this.map.set(key, v);
1640
2533
  return v;
@@ -1858,26 +2751,59 @@ function subCubic(p, t0, t1) {
1858
2751
  const left = split(p, t1)[0];
1859
2752
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
1860
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
+ }
1861
2788
  function hopCrossings(e, r, page, index) {
1862
2789
  const segments = hopSamples(r);
1863
2790
  if (segments.length === 0) return [];
1864
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
1865
- const beneath = [];
1866
- for (const o of orderedElements(page)) {
1867
- if (o.id === e.id) break;
1868
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
1869
- beneath.push(routePoints(route(o, index)));
1870
- }
2791
+ const { lines, count } = linesBeneath(e, page, index);
1871
2792
  const rad = hopRadius(e.style.strokeWidth);
1872
2793
  const out = [];
1873
2794
  for (const samples of segments) {
1874
2795
  const along = runningLengths(samples);
1875
2796
  const length = along[along.length - 1];
1876
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]);
1877
2801
  for (let i = 0; i < samples.length - 1; i++) {
1878
2802
  const p = samples[i], q = samples[i + 1];
1879
2803
  const dx = q.x - p.x, dy = q.y - p.y;
1880
- 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;
1881
2807
  for (let k = 0; k < line2.length - 1; k++) {
1882
2808
  const a = line2[k], b = line2[k + 1];
1883
2809
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2157,8 +3083,8 @@ function followSpan(track, t) {
2157
3083
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2158
3084
  return f;
2159
3085
  }
2160
- function tracing(e, page) {
2161
- 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)));
2162
3088
  const clean = cloneElement(e);
2163
3089
  clean.style.roughness = 0;
2164
3090
  const pts = outline(clean).contours()[0] ?? [];
@@ -2168,9 +3094,10 @@ function following(track, e, t, page) {
2168
3094
  const f = followSpan(track, t);
2169
3095
  if (!f) return e;
2170
3096
  t = localTime(track, t);
2171
- const pathElement = page.elements.find((x) => x.id === f.path);
3097
+ const index = indexOf(page);
3098
+ const pathElement = index.get(f.path);
2172
3099
  if (!pathElement || pathElement.id === e.id) return e;
2173
- const line2 = tracing(pathElement, page);
3100
+ const line2 = tracing(pathElement, page, index);
2174
3101
  const span = f.end - f.start;
2175
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);
2176
3103
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -2201,16 +3128,39 @@ function trackMap(tl) {
2201
3128
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2202
3129
  return m;
2203
3130
  }
2204
- function resolved(page, t, state = "initial") {
2205
- return expandingSymbols(posed(page, t, state), clampTime(t));
3131
+ function resolved(page, t, state = "initial", since = null, carried = {}) {
3132
+ return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
2206
3133
  }
2207
3134
  var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2208
- function posed(page, t, state = "initial") {
3135
+ function posed(page, t, state = "initial", since = null, carried = {}) {
3136
+ return posedWithMetrics(page, t, state, since, carried).frame;
3137
+ }
3138
+ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
2209
3139
  const off = stateHidden(page, state);
2210
- const timeline = page.timeline ?? (off.size === 0 ? null : EMPTY_TIMELINE);
2211
- if (!timeline) return page;
3140
+ const values = stateValues(page, state);
3141
+ const active = /* @__PURE__ */ new Map();
3142
+ for (const e of page.elements) {
3143
+ const o = activeOverrides(e, values);
3144
+ if (o.length > 0) active.set(e.id, o);
3145
+ }
3146
+ const flowing = (page.flows ?? []).length > 0;
3147
+ const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
3148
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
3149
+ if (!timeline) return { frame: page, metrics: emptyMetrics() };
2212
3150
  t = clampTime(t);
2213
3151
  const tracks = trackMap(timeline);
3152
+ for (const [id, overrides] of active) {
3153
+ const track = tracks.get(id);
3154
+ if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
3155
+ tracks.set(id, {
3156
+ ...track,
3157
+ follow: track.follow.map((f) => {
3158
+ let path = f.path;
3159
+ for (const o of overrides) path = o.reroute?.[path] ?? path;
3160
+ return { ...f, path };
3161
+ })
3162
+ });
3163
+ }
2214
3164
  const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
2215
3165
  const hidden = /* @__PURE__ */ new Set();
2216
3166
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
@@ -2230,6 +3180,14 @@ function posed(page, t, state = "initial") {
2230
3180
  let elements = page.elements.map((e) => {
2231
3181
  const track = tracks.get(e.id);
2232
3182
  let out = track ? applyTrack(track, e, t) : e;
3183
+ for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3184
+ const list = active.get(e.id);
3185
+ if (list) {
3186
+ const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
3187
+ const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
3188
+ if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
3189
+ if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
3190
+ }
2233
3191
  if (reseed !== null && reseed > 0 && boils(e)) {
2234
3192
  if (out === e) out = cloneElement(e);
2235
3193
  out.seed = boiled(e.seed, reseed);
@@ -2263,7 +3221,18 @@ function posed(page, t, state = "initial") {
2263
3221
  pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
2264
3222
  return pinned;
2265
3223
  });
2266
- return { ...page, elements: elements.filter((e) => !gone.has(e.id)), timeline: null };
3224
+ elements = elements.filter((e) => !gone.has(e.id));
3225
+ let metrics2 = emptyMetrics();
3226
+ if (measuring) {
3227
+ const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
3228
+ const frame2 = { ...page, elements, timeline: null };
3229
+ const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
3230
+ metrics2 = out.metrics;
3231
+ const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
3232
+ Object.assign(metrics2.carry, finished.readings);
3233
+ elements = [...finished.elements, ...out.packets];
3234
+ }
3235
+ return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
2267
3236
  }
2268
3237
  function posterTime(page) {
2269
3238
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
@@ -2378,22 +3347,35 @@ function filled(s, args) {
2378
3347
  }) };
2379
3348
  }
2380
3349
  function symbolPage(s) {
2381
- return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null };
3350
+ return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
2382
3351
  }
2383
3352
  function hasInstances(page) {
2384
3353
  return page.elements.some((e) => e.kind === "symbol");
2385
3354
  }
2386
- 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) {
2387
3368
  if (!hasInstances(page)) return page;
2388
3369
  const budget = { left: MAX_ELEMENTS };
2389
- 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) };
2390
3372
  }
2391
3373
  function symbolTable(page) {
2392
3374
  const table = /* @__PURE__ */ new Map();
2393
3375
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
2394
3376
  return table;
2395
3377
  }
2396
- 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) {
2397
3379
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
2398
3380
  if (instances.size === 0) {
2399
3381
  budget.left -= elements.length;
@@ -2418,6 +3400,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
2418
3400
  budget.left -= 1;
2419
3401
  continue;
2420
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;
2421
3411
  budget.left -= 1;
2422
3412
  const id = e.symbol;
2423
3413
  const symbol = id === null ? void 0 : table.get(id);
@@ -2433,6 +3423,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
2433
3423
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
2434
3424
  };
2435
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) });
2436
3427
  }
2437
3428
  return out;
2438
3429
  }
@@ -2485,8 +3476,8 @@ function mix2(x) {
2485
3476
  return z ^ z >> 31n;
2486
3477
  }
2487
3478
  function halves(id) {
2488
- const hex = id.replace(/-/g, "").padStart(32, "0").slice(-32);
2489
- return [BigInt(`0x${hex.slice(0, 16)}`), BigInt(`0x${hex.slice(16)}`)];
3479
+ const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3480
+ return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
2490
3481
  }
2491
3482
  function derivedID(instance, inner) {
2492
3483
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -2507,7 +3498,7 @@ function clickTargets(page, file) {
2507
3498
  let pageOf = null;
2508
3499
  for (const e of page.elements) {
2509
3500
  const c = e.click;
2510
- const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {} } : null;
3501
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
2511
3502
  if (c?.scene) {
2512
3503
  const i = sceneNamed(c.scene, markers);
2513
3504
  if (i >= 0) {
@@ -2540,14 +3531,14 @@ function clickTargets(page, file) {
2540
3531
  function hoveredElement(e) {
2541
3532
  const h = e.hover;
2542
3533
  if (!h) return e;
2543
- const style = { ...e.style };
2544
- if (h.stroke) style.stroke = h.stroke;
2545
- if (h.fill) style.fill = h.fill;
3534
+ const style2 = { ...e.style };
3535
+ if (h.stroke) style2.stroke = h.stroke;
3536
+ if (h.fill) style2.fill = h.fill;
2546
3537
  if (h.opacity !== null && Number.isFinite(h.opacity)) {
2547
3538
  const o = Math.min(Math.max(h.opacity, 0), 1);
2548
- style.opacity = o >= 1 ? null : o;
3539
+ style2.opacity = o >= 1 ? null : o;
2549
3540
  }
2550
- return { ...e, style };
3541
+ return { ...e, style: style2 };
2551
3542
  }
2552
3543
  function hovered(page, ids) {
2553
3544
  return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
@@ -2700,13 +3691,13 @@ function blocks(lines) {
2700
3691
  flush();
2701
3692
  return out;
2702
3693
  }
2703
- function parseList(lines, start) {
2704
- const first = reItem.exec(lines[start]);
3694
+ function parseList(lines, start2) {
3695
+ const first = reItem.exec(lines[start2]);
2705
3696
  const indent = first[1].length;
2706
3697
  const bullet = /^\d/.test(first[2]) ? null : first[2];
2707
3698
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
2708
3699
  const items = [];
2709
- let i = start;
3700
+ let i = start2;
2710
3701
  while (i < lines.length) {
2711
3702
  const m = reItem.exec(lines[i]);
2712
3703
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -2751,14 +3742,14 @@ function parseList(lines, start) {
2751
3742
  }
2752
3743
  return { block: { kind: "list", ordered, items }, next: i };
2753
3744
  }
2754
- function inline(src, style = {}) {
3745
+ function inline(src, style2 = {}) {
2755
3746
  const out = [];
2756
3747
  const push = (text, s) => {
2757
3748
  if (text) out.push({ ...s, text });
2758
3749
  };
2759
3750
  let i = 0, buf = "";
2760
3751
  const emit = () => {
2761
- push(buf, style);
3752
+ push(buf, style2);
2762
3753
  buf = "";
2763
3754
  };
2764
3755
  while (i < src.length) {
@@ -2786,43 +3777,43 @@ function inline(src, style = {}) {
2786
3777
  emit();
2787
3778
  let code = m[2].replace(/\n/g, " ");
2788
3779
  if (/^ .* $/.test(code)) code = code.slice(1, -1);
2789
- push(code, { ...style, mono: true });
3780
+ push(code, { ...style2, mono: true });
2790
3781
  i += m[0].length;
2791
3782
  continue;
2792
3783
  }
2793
3784
  if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
2794
3785
  emit();
2795
- out.push(...inline(m[1], style));
3786
+ out.push(...inline(m[1], style2));
2796
3787
  i += m[0].length;
2797
3788
  continue;
2798
3789
  }
2799
3790
  if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
2800
3791
  emit();
2801
- out.push(...inline(m[1], { ...style, link: true }));
3792
+ out.push(...inline(m[1], { ...style2, link: true }));
2802
3793
  i += m[0].length;
2803
3794
  continue;
2804
3795
  }
2805
3796
  if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
2806
3797
  emit();
2807
- push(m[1], { ...style, link: true });
3798
+ push(m[1], { ...style2, link: true });
2808
3799
  i += m[0].length;
2809
3800
  continue;
2810
3801
  }
2811
3802
  if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
2812
3803
  emit();
2813
- out.push(...inline(m[1], { ...style, strike: true }));
3804
+ out.push(...inline(m[1], { ...style2, strike: true }));
2814
3805
  i += m[0].length;
2815
3806
  continue;
2816
3807
  }
2817
3808
  if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
2818
3809
  emit();
2819
- out.push(...inline(m[2], { ...style, bold: true }));
3810
+ out.push(...inline(m[2], { ...style2, bold: true }));
2820
3811
  i += m[0].length;
2821
3812
  continue;
2822
3813
  }
2823
3814
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
2824
3815
  emit();
2825
- out.push(...inline(m[2], { ...style, italic: true }));
3816
+ out.push(...inline(m[2], { ...style2, italic: true }));
2826
3817
  i += m[0].length;
2827
3818
  continue;
2828
3819
  }
@@ -3035,10 +4026,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
3035
4026
  let k = 0;
3036
4027
  let firstLine = true;
3037
4028
  while (k < chars.length || firstLine && chars.length === 0) {
3038
- const start = firstLine ? p.firstIndent : p.indent;
4029
+ const start2 = firstLine ? p.firstIndent : p.indent;
3039
4030
  const maxX2 = width;
3040
4031
  const glyphs = [];
3041
- let x = start, lastSpace = -1, end = k;
4032
+ let x = start2, lastSpace = -1, end = k;
3042
4033
  for (; end < chars.length; end++) {
3043
4034
  const { ch, run } = chars[end];
3044
4035
  const r = runs[run];
@@ -3087,7 +4078,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3087
4078
  }
3088
4079
  if (glyphs.length === 0 && end === k && k < chars.length) {
3089
4080
  const { ch, run } = chars[k];
3090
- 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) });
3091
4082
  end = k + 1;
3092
4083
  }
3093
4084
  let asc = 0, desc = 0, lead = 0;
@@ -3113,13 +4104,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3113
4104
  });
3114
4105
  return lines;
3115
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);
3116
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) {
3117
4130
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3118
4131
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3119
4132
  const fam = familyMetrics(e.style.font);
3120
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3121
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3122
- 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
+ });
3123
4139
  const ink = color(e.style.stroke);
3124
4140
  let s = "";
3125
4141
  const marks = [];
@@ -3208,6 +4224,26 @@ function bounds(page, padding = 24) {
3208
4224
  return inset(r, -padding, -padding);
3209
4225
  }
3210
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) {
3211
4247
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
3212
4248
  const times = frameTimes(tl);
3213
4249
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -3235,8 +4271,8 @@ function svgOpen(b, background, paint) {
3235
4271
  }
3236
4272
  return out;
3237
4273
  }
3238
- function svgParts(page, background, fixed, links = false) {
3239
- 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);
3240
4276
  const theme = themeNamed(page.theme);
3241
4277
  const bg = background ? theme?.background ?? background : null;
3242
4278
  const ink = theme?.ink ?? black;
@@ -3247,11 +4283,11 @@ function svgParts(page, background, fixed, links = false) {
3247
4283
  const index = indexOf(d);
3248
4284
  const ordered = orderedElements(d);
3249
4285
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
3250
- 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"/>
3251
4287
  `).join("");
3252
4288
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
3253
4289
  for (const e of ordered) {
3254
- if (!visible.has(e.layerID)) continue;
4290
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
3255
4291
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
3256
4292
  if (alpha <= 0) continue;
3257
4293
  let body = svgElement(e, index, bg, d);
@@ -3399,6 +4435,110 @@ function textElement(text, f, e, centered) {
3399
4435
  return s + "</text>\n";
3400
4436
  }
3401
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
+
3402
4542
  // src/player.ts
3403
4543
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
3404
4544
  function pageIndex(file, spec) {
@@ -3439,6 +4579,12 @@ var Playback = class {
3439
4579
  __publicField(this, "sceneLoops");
3440
4580
  /** The page's variables now: their starting values until clicks change them. */
3441
4581
  __publicField(this, "state");
4582
+ /** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
4583
+ __publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
4584
+ /** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
4585
+ __publicField(this, "still", false);
4586
+ /** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
4587
+ __publicField(this, "carried", {});
3442
4588
  __publicField(this, "background");
3443
4589
  __publicField(this, "ink");
3444
4590
  /** What clicking each clickable element does, by element id. */
@@ -3462,7 +4608,7 @@ var Playback = class {
3462
4608
  __publicField(this, "order", []);
3463
4609
  __publicField(this, "frame", -1);
3464
4610
  __publicField(this, "onFrame", null);
3465
- this.page = options.clean ? withRoughness(page, 0) : page;
4611
+ this.page = options.clean ? cleanCopy(page) : page;
3466
4612
  addFonts(usedFonts(this.page));
3467
4613
  this.plan = plan(this.page);
3468
4614
  this.clicks = clickTargets(page, options.file);
@@ -3510,6 +4656,10 @@ var Playback = class {
3510
4656
  this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
3511
4657
  into.replaceChildren(this.svg);
3512
4658
  }
4659
+ /** Seconds since the variables last changed, or null for a still. */
4660
+ sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
4661
+ return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
4662
+ }
3513
4663
  get frameCount() {
3514
4664
  return this.plan.times.length;
3515
4665
  }
@@ -3535,7 +4685,7 @@ var Playback = class {
3535
4685
  }
3536
4686
  /** Whether a copy is still playing through after a click. */
3537
4687
  replaying(now = performance.now()) {
3538
- 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;
3539
4689
  return false;
3540
4690
  }
3541
4691
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -3548,17 +4698,24 @@ var Playback = class {
3548
4698
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
3549
4699
  const now = typeof performance === "undefined" ? 0 : performance.now();
3550
4700
  const times = /* @__PURE__ */ new Map();
3551
- 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));
3552
4702
  const tc = clampTime(t);
3553
- const pose = posed(this.page, tc, this.state);
3554
- const frame2 = expandingSymbols(pose, tc, true, times);
4703
+ const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4704
+ const made = new Expansions(pose, tc, true);
4705
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
3555
4706
  const { parts } = svgParts(frame2, this.background, r);
3556
4707
  const owned = (m, parts2) => {
3557
4708
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
3558
4709
  return m;
3559
4710
  };
3560
- 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) ?? "")));
3561
- 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);
3562
4719
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
3563
4720
  const normal = owned(/* @__PURE__ */ new Map(), parts);
3564
4721
  const merged = [];
@@ -3687,6 +4844,14 @@ ${h}</g>
3687
4844
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
3688
4845
  }
3689
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
+ }
3690
4855
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3691
4856
  } : HTMLElement;
3692
4857
  var DottiePlayer = class extends ElementBase {
@@ -3709,6 +4874,10 @@ var DottiePlayer = class extends ElementBase {
3709
4874
  __publicField(this, "raf", 0);
3710
4875
  __publicField(this, "replayRaf", 0);
3711
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);
3712
4881
  __publicField(this, "visible", true);
3713
4882
  __publicField(this, "observer", null);
3714
4883
  this.root = this.attachShadow({ mode: "open" });
@@ -3785,9 +4954,19 @@ var DottiePlayer = class extends ElementBase {
3785
4954
  text = inline2.textContent ?? "";
3786
4955
  }
3787
4956
  if (token !== this.loadToken) return;
3788
- this.setFile(parseDottie(text));
4957
+ this.showFile(parseDottie(text));
3789
4958
  } catch (err) {
3790
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;
3791
4970
  this.stage.innerHTML = "";
3792
4971
  const box = document.createElement("div");
3793
4972
  box.className = "error";
@@ -3798,15 +4977,36 @@ var DottiePlayer = class extends ElementBase {
3798
4977
  }
3799
4978
  /** Shows an already-parsed file. */
3800
4979
  setFile(file) {
4980
+ this.loadToken++;
4981
+ this.showFile(file);
4982
+ }
4983
+ showFile(file) {
3801
4984
  this.file = file;
3802
4985
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
3803
- this.mount();
3804
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
4986
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
3805
4987
  }
3806
- 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) {
3807
4991
  const file = this.file;
3808
4992
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
3809
- 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 });
5009
+ p.still = this.hasAttribute("at");
3810
5010
  this.playback = p;
3811
5011
  this.style.setProperty("--dottie-page", color(p.background));
3812
5012
  this.style.setProperty("--dottie-ink", color(p.ink));
@@ -3828,10 +5028,14 @@ var DottiePlayer = class extends ElementBase {
3828
5028
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
3829
5029
  };
3830
5030
  if (!animated) {
3831
- 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;
3832
5035
  this.setPlaying(false);
3833
5036
  return;
3834
5037
  }
5038
+ this.stillTime = null;
3835
5039
  p.show(0);
3836
5040
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
3837
5041
  if (reduced) {
@@ -3845,8 +5049,10 @@ var DottiePlayer = class extends ElementBase {
3845
5049
  get paused() {
3846
5050
  return !this.playing;
3847
5051
  }
5052
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
3848
5053
  get currentTime() {
3849
5054
  const p = this.playback;
5055
+ if (p && this.stillTime !== null) return this.stillTime;
3850
5056
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
3851
5057
  }
3852
5058
  set currentTime(t) {
@@ -3900,7 +5106,13 @@ var DottiePlayer = class extends ElementBase {
3900
5106
  this.scheduleReplay();
3901
5107
  }
3902
5108
  if (!click) return;
3903
- if (click.set || click.toggle) this.setState(applyClick({ scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null }, p.state));
5109
+ if (click.set || click.toggle || click.reset) {
5110
+ this.setState(applyClick(
5111
+ { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
5112
+ p.state,
5113
+ p.page.variables ?? {}
5114
+ ));
5115
+ }
3904
5116
  if ("state" in click) return;
3905
5117
  if ("url" in click) {
3906
5118
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
@@ -3926,7 +5138,10 @@ var DottiePlayer = class extends ElementBase {
3926
5138
  if (!p) return;
3927
5139
  const next = { ...p.state, ...values };
3928
5140
  if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
5141
+ const t = clampTime(p.last.t);
5142
+ p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
3929
5143
  p.state = next;
5144
+ p.stateSince = performance.now();
3930
5145
  p.redraw();
3931
5146
  this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
3932
5147
  }
@@ -4124,9 +5339,10 @@ function render(source, options = {}) {
4124
5339
  function isFile(v) {
4125
5340
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4126
5341
  }
4127
- var VERSION = "0.7.0";
5342
+ var VERSION = "0.9.0";
4128
5343
 
4129
5344
  // src/player-entry.ts
5345
+ servePlans();
4130
5346
  definePlayer();
4131
5347
  globalThis.Dottie ?? (globalThis.Dottie = index_exports);
4132
5348
  export {