@phin-tech/dottie 0.6.3 → 0.8.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.6.3 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.8.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) => {
@@ -66,13 +66,146 @@ __export(index_exports, {
66
66
  withRoughness: () => withRoughness
67
67
  });
68
68
 
69
+ // src/state.ts
70
+ var MAX_NAME = 64;
71
+ function isName(name) {
72
+ return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
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
+ }
116
+ function conditionNames(condition) {
117
+ const out = [];
118
+ for (const any of condition.split("||")) {
119
+ for (const all of any.split("&&")) {
120
+ if (metricTerm(all) !== null) continue;
121
+ let term = trim(all);
122
+ if (term.startsWith("!")) term = trim(term.slice(1));
123
+ if (!isName(term)) return null;
124
+ out.push(term);
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ function holds(condition, state, metric = null) {
130
+ if (conditionNames(condition) === null) return false;
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
+ }
137
+ let term = trim(all);
138
+ let negated = false;
139
+ if (term.startsWith("!")) {
140
+ negated = true;
141
+ term = trim(term.slice(1));
142
+ }
143
+ return (state[term] ?? false) !== negated;
144
+ }));
145
+ }
146
+ function stateValues(page, choice) {
147
+ if (choice === "all") return null;
148
+ if (choice === "initial") return { ...page.variables ?? {} };
149
+ return { ...page.variables ?? {}, ...choice };
150
+ }
151
+ function stateHidden(page, choice) {
152
+ const values = stateValues(page, choice);
153
+ const out = /* @__PURE__ */ new Set();
154
+ if (!values) return out;
155
+ for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
156
+ return out;
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
+ }
182
+ function changesState(c) {
183
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
184
+ }
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
+ }
192
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
193
+ for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
194
+ return out;
195
+ }
196
+ function trim(s) {
197
+ return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
198
+ }
199
+
69
200
  // src/model.ts
70
201
  var SUPPORTED_VERSION = 2;
71
202
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
72
203
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
73
204
  var KNOWN_FIELDS = {
74
205
  file: ["version", "pages", "images"],
75
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
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"],
76
209
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
77
210
  layer: ["id", "name", "visible", "locked"],
78
211
  rgba: ["r", "g", "b", "a"],
@@ -100,6 +233,8 @@ var KNOWN_FIELDS = {
100
233
  "anchor",
101
234
  "click",
102
235
  "hover",
236
+ "when",
237
+ "overrides",
103
238
  "symbol",
104
239
  "playback",
105
240
  "offset",
@@ -108,8 +243,13 @@ var KNOWN_FIELDS = {
108
243
  "reveal",
109
244
  "ports",
110
245
  "labelRect",
111
- "extension"
246
+ "extension",
247
+ "capacity",
248
+ "delay",
249
+ "queue",
250
+ "gauge"
112
251
  ],
252
+ gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
113
253
  strokePoint: ["x", "y"],
114
254
  subpath: ["closed", "coords"],
115
255
  inlineImage: ["data", "format"],
@@ -117,7 +257,7 @@ var KNOWN_FIELDS = {
117
257
  extensionRef: ["id", "shape", "version", "params"],
118
258
  normalizedPoint: ["x", "y"],
119
259
  normalizedRect: ["x", "y", "width", "height"],
120
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
260
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
121
261
  waypoint: ["x", "y"],
122
262
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
123
263
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -125,9 +265,10 @@ var KNOWN_FIELDS = {
125
265
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
126
266
  repeat: ["start", "end", "count"],
127
267
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
128
- marker: ["t", "name"],
129
- click: ["scene", "url"],
130
- hover: ["stroke", "fill", "opacity"]
268
+ marker: ["t", "name", "loop"],
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"]
131
272
  };
132
273
  var MAX_DURATION = 3600;
133
274
  var MAX_MARCH = 1e4;
@@ -143,6 +284,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
143
284
  var str = (v) => typeof v === "string" ? v : void 0;
144
285
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
145
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}$/;
146
289
  var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
147
290
  function clampTime(t) {
148
291
  if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
@@ -234,7 +377,8 @@ var Reader = class {
234
377
  this.fields(w, "waypoint");
235
378
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
236
379
  }) : null,
237
- 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
238
382
  };
239
383
  }
240
384
  image(v, table) {
@@ -285,6 +429,8 @@ var Reader = class {
285
429
  anchor: str(v.anchor) ?? null,
286
430
  click: this.click(v.click),
287
431
  hover: this.hover(v.hover),
432
+ when: str(v.when) ?? null,
433
+ overrides: this.overrides(v.overrides),
288
434
  symbol: uuid(v.symbol) ?? null,
289
435
  playback: this.playback(v.playback),
290
436
  offset: num(v.offset) ?? null,
@@ -296,7 +442,26 @@ var Reader = class {
296
442
  return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
297
443
  }) : null,
298
444
  labelRect: this.labelRect(v.labelRect),
299
- 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
300
465
  };
301
466
  }
302
467
  labelRect(v) {
@@ -320,13 +485,27 @@ var Reader = class {
320
485
  }
321
486
  return { id, shape, version, params };
322
487
  }
488
+ variables(v) {
489
+ if (!isObj(v)) return null;
490
+ const out = {};
491
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
492
+ return Object.keys(out).length > 0 ? out : null;
493
+ }
323
494
  click(v) {
324
495
  if (!isObj(v)) return null;
325
496
  this.fields(v, "click");
326
497
  const url = str(v.url);
327
498
  const safe = url === void 0 ? null : allowedURL(url);
328
499
  if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
329
- return { scene: str(v.scene) ?? null, url: safe };
500
+ let set = null;
501
+ if (isObj(v.set)) {
502
+ set = {};
503
+ for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
504
+ if (Object.keys(set).length === 0) set = null;
505
+ }
506
+ const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
507
+ const toggle = raw ? raw.filter(isName) : null;
508
+ return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
330
509
  }
331
510
  playback(v) {
332
511
  const s = str(v);
@@ -362,6 +541,35 @@ var Reader = class {
362
541
  params: strings(v.params)
363
542
  };
364
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
+ }
365
573
  hover(v) {
366
574
  if (!isObj(v)) return null;
367
575
  this.fields(v, "hover");
@@ -427,7 +635,7 @@ var Reader = class {
427
635
  if (camera) stableSortBy(camera, (c) => c.t);
428
636
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
429
637
  const t = num(m.t);
430
- return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
638
+ return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
431
639
  }) : null;
432
640
  if (markers) stableSortBy(markers, (m) => m.t);
433
641
  const poster = num(v.poster);
@@ -460,7 +668,34 @@ var Reader = class {
460
668
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
461
669
  theme: str(v.theme) ?? null,
462
670
  timeline: this.timeline(v.timeline),
463
- symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
671
+ symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
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
464
699
  };
465
700
  }
466
701
  };
@@ -522,7 +757,8 @@ function cloneElement(e) {
522
757
  start: { ...e.connector.start },
523
758
  end: { ...e.connector.end },
524
759
  waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
525
- } : null
760
+ } : null,
761
+ gauge: e.gauge ? { ...e.gauge } : e.gauge
526
762
  };
527
763
  }
528
764
  function orderedElements(page) {
@@ -1273,8 +1509,617 @@ function mulConst(step) {
1273
1509
  return [p[0], p[1]];
1274
1510
  }
1275
1511
 
1512
+ // src/flows.ts
1513
+ var DEFAULT_EVERY = 0.5;
1514
+ var DEFAULT_SPEED = 160;
1515
+ var DEFAULT_SIZE = 12;
1516
+ var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
1517
+ var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
1518
+ function packetID(flow, k) {
1519
+ const n = (k + 2 ** 40) % 2 ** 48;
1520
+ return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1521
+ }
1522
+ var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1523
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1524
+ const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1525
+ if (!shapes2.has(from)) return null;
1526
+ const next = /* @__PURE__ */ new Map();
1527
+ const add2 = (a, edge) => {
1528
+ const list = next.get(a);
1529
+ if (list) list.push(edge);
1530
+ else next.set(a, [edge]);
1531
+ };
1532
+ for (const e of frame2.elements) {
1533
+ const c = e.connector;
1534
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1535
+ if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1536
+ const cost = routeCost(c.cost);
1537
+ add2(a, { to: b, via: e.id, back: false, cost });
1538
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1539
+ }
1540
+ const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1541
+ let counter = 1;
1542
+ const tree = new FlowTree(from);
1543
+ const open = [from];
1544
+ while (open.length > 0) {
1545
+ let best = 0;
1546
+ for (let i = 1; i < open.length; i++) {
1547
+ const da = dist.get(open[i]), db = dist.get(open[best]);
1548
+ if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1549
+ }
1550
+ const n = open.splice(best, 1)[0];
1551
+ tree.settled.add(n);
1552
+ for (const edge of next.get(n) ?? []) {
1553
+ if (tree.settled.has(edge.to)) continue;
1554
+ const d = dist.get(n) + edge.cost;
1555
+ const old = dist.get(edge.to);
1556
+ if (old !== void 0 && old <= d) continue;
1557
+ if (old === void 0) open.push(edge.to);
1558
+ dist.set(edge.to, d);
1559
+ order.set(edge.to, counter++);
1560
+ tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1561
+ }
1562
+ }
1563
+ return tree;
1564
+ }
1565
+ var FlowTree = class {
1566
+ constructor(from) {
1567
+ __publicField(this, "settled", /* @__PURE__ */ new Set());
1568
+ __publicField(this, "came", /* @__PURE__ */ new Map());
1569
+ __publicField(this, "from");
1570
+ this.from = from;
1571
+ }
1572
+ /** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
1573
+ goals(to, spread) {
1574
+ const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
1575
+ if (reachable.length === 0) return [];
1576
+ return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
1577
+ }
1578
+ /** The connectors to `goal`, as [connector, backwards] hops. */
1579
+ hops(goal) {
1580
+ const hops = [];
1581
+ for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
1582
+ return hops.reverse();
1583
+ }
1584
+ };
1585
+
1586
+ // src/measure.ts
1587
+ var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
1588
+ function metricValue(m, metric, id) {
1589
+ const f = m.flows.get(id);
1590
+ if (f) {
1591
+ switch (metric) {
1592
+ case "throughput":
1593
+ return f.placed;
1594
+ case "backlog":
1595
+ return f.backlog;
1596
+ case "backup":
1597
+ return f.backup;
1598
+ case "transit":
1599
+ return f.transit;
1600
+ default:
1601
+ return null;
1602
+ }
1603
+ }
1604
+ const e = m.elements.get(id);
1605
+ if (!e) return null;
1606
+ switch (metric) {
1607
+ case "throughput":
1608
+ return e.throughput;
1609
+ case "load":
1610
+ return e.load;
1611
+ case "delay":
1612
+ return e.delay;
1613
+ case "backlog":
1614
+ return e.backlog;
1615
+ case "inFlight":
1616
+ return e.inFlight;
1617
+ case "depth":
1618
+ return e.depth;
1619
+ default:
1620
+ return null;
1621
+ }
1622
+ }
1623
+ var TINY = 1e-9;
1624
+ var amount = (f) => clamp(f.volume, 1, 0, 1e12);
1625
+ function amountIn(f, values) {
1626
+ if (values === null || !f.overrides) return amount(f);
1627
+ let v = null;
1628
+ for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
1629
+ return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
1630
+ }
1631
+ function delayOf(e) {
1632
+ const d = e.delay ?? null;
1633
+ if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
1634
+ return Math.min(Math.max(d, 0), 3600);
1635
+ }
1636
+ var queueKey = (shape, flow) => shape + "|" + flow;
1637
+ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1638
+ const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1639
+ const byID = /* @__PURE__ */ new Map();
1640
+ for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1641
+ const metrics2 = emptyMetrics();
1642
+ for (const e of frame2.elements) {
1643
+ if (e.kind === "connector" && !e.connector) continue;
1644
+ metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
1645
+ }
1646
+ const list = flows ?? [];
1647
+ const room = /* @__PURE__ */ new Map();
1648
+ for (const e of frame2.elements) {
1649
+ const c = e.capacity ?? null;
1650
+ if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1651
+ }
1652
+ const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1653
+ const route2 = (tree, goal) => {
1654
+ const hops = [], shapes2 = [];
1655
+ let at = goal;
1656
+ for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
1657
+ hops.push([step.via, step.back]);
1658
+ shapes2.push(at);
1659
+ at = step.from;
1660
+ }
1661
+ shapes2.push(at);
1662
+ return { hops: hops.reverse(), shapes: shapes2.reverse() };
1663
+ };
1664
+ const way = (hops, shapes2) => {
1665
+ const points = [], inside = [], stretches = [];
1666
+ let length = 0;
1667
+ hops.forEach(([id, back], k) => {
1668
+ const c = byID.get(id);
1669
+ if (!c) return;
1670
+ const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
1671
+ if (line2.length === 0) return;
1672
+ const last = points[points.length - 1];
1673
+ if (last) {
1674
+ const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1675
+ const shape = byID.get(shapes2[k]);
1676
+ inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1677
+ length += gap;
1678
+ }
1679
+ const start = length;
1680
+ for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
1681
+ stretches.push([id, start, length]);
1682
+ points.push(...line2);
1683
+ });
1684
+ return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
1685
+ };
1686
+ const along = (w, elapsedAt, speed) => {
1687
+ let elapsed = elapsedAt, d = 0;
1688
+ for (const [a, b, delay] of w.inside) {
1689
+ const reach = (a - d) / speed;
1690
+ if (elapsed <= reach) break;
1691
+ elapsed -= reach;
1692
+ d = a;
1693
+ const stay = (b - a) / speed + delay;
1694
+ if (elapsed < stay) return null;
1695
+ elapsed -= stay;
1696
+ d = b;
1697
+ }
1698
+ d += elapsed * speed;
1699
+ return d >= 0 && d <= w.length ? d : null;
1700
+ };
1701
+ const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
1702
+ const bump = (id, f) => {
1703
+ const m = metrics2.elements.get(id);
1704
+ if (m) f(m);
1705
+ };
1706
+ const leaving = /* @__PURE__ */ new Map(), turn = [];
1707
+ for (const flow of list) {
1708
+ const n = leaving.get(flow.from) ?? 0;
1709
+ turn.push(n);
1710
+ leaving.set(flow.from, n + 1);
1711
+ }
1712
+ const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
1713
+ const packets = [];
1714
+ const waitingBy = /* @__PURE__ */ new Map();
1715
+ 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);
1716
+ for (const [i, flow] of list.entries()) {
1717
+ if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1718
+ const source = byID.get(flow.from);
1719
+ const base2 = source ? flowTree(flow.from, frame2, offline) : null;
1720
+ if (!source || !base2) continue;
1721
+ const volume = amountIn(flow, values);
1722
+ const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1723
+ const placements = [];
1724
+ const waiting = [];
1725
+ const goals = base2.goals(flow.to, flow.spread === true);
1726
+ const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1727
+ for (const [candidates, share] of shares) {
1728
+ if (!(share > TINY)) continue;
1729
+ let left = share;
1730
+ for (let n = 0; n < 32 && left > TINY; n++) {
1731
+ const tree = flowTree(flow.from, frame2, union2(offline, full()));
1732
+ const goal2 = tree?.goals(candidates, false)[0];
1733
+ if (!tree || goal2 === void 0) break;
1734
+ const r = route2(tree, goal2);
1735
+ const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1736
+ const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1737
+ const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
1738
+ if (!(fits > TINY)) break;
1739
+ for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
1740
+ placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
1741
+ left -= fits;
1742
+ }
1743
+ if (!(left > TINY)) continue;
1744
+ let at = flow.from;
1745
+ const goal = base2.goals(candidates, false)[0];
1746
+ if (goal !== void 0) {
1747
+ const r = route2(base2, goal), done = full();
1748
+ const sequence = [[r.shapes[0], r.shapes[0]]];
1749
+ r.hops.forEach(([id], k) => {
1750
+ sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
1751
+ });
1752
+ const first = sequence.find(([id]) => done.has(id));
1753
+ if (first) {
1754
+ at = first[1];
1755
+ const upTo = r.shapes.indexOf(at);
1756
+ for (let q = upTo; q >= 0; q--) {
1757
+ const s = byID.get(r.shapes[q]);
1758
+ if (s && s.queue === true && s.kind !== "connector") {
1759
+ at = r.shapes[q];
1760
+ break;
1761
+ }
1762
+ }
1763
+ }
1764
+ const n = r.shapes.indexOf(at);
1765
+ if (at !== flow.from && n >= 0) {
1766
+ const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
1767
+ for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
1768
+ placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1769
+ }
1770
+ }
1771
+ const existing = waiting.find(([s]) => s === at);
1772
+ if (existing) existing[1] += left;
1773
+ else waiting.push([at, left]);
1774
+ }
1775
+ waitingBy.set(flow.id, waiting);
1776
+ for (const p of placements) {
1777
+ for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1778
+ m.throughput += p.amount;
1779
+ });
1780
+ if (!p.waits) fm.placed += p.amount;
1781
+ }
1782
+ for (const [at, a] of waiting) {
1783
+ bump(at, (m) => {
1784
+ m.backlog += a;
1785
+ });
1786
+ fm.backlog += a;
1787
+ }
1788
+ const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
1789
+ fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
1790
+ const every = everyOf(flow);
1791
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1792
+ const phase = turn[i] / leaving.get(flow.from) * every;
1793
+ const color2 = flow.color ?? DEFAULT_COLOR;
1794
+ const cumulative = [];
1795
+ let sum = 0;
1796
+ for (const p of placements) {
1797
+ sum += p.amount / Math.max(volume, TINY);
1798
+ cumulative.push(sum);
1799
+ }
1800
+ if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
1801
+ let transitSum = 0;
1802
+ placements.forEach((p, r) => {
1803
+ const w = way(p.hops, p.shapes);
1804
+ if (!w) return;
1805
+ const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
1806
+ if (!p.waits) transitSum += travel * p.amount;
1807
+ const lo = Math.ceil((t - phase - travel) / every);
1808
+ const hi = Math.floor((t - phase) / every);
1809
+ const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
1810
+ const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
1811
+ for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
1812
+ if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
1813
+ const d = along(w, t - k * every - phase, speed);
1814
+ if (d === null) continue;
1815
+ const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
1816
+ if (s) bump(s[0], (m) => {
1817
+ m.inFlight += 1;
1818
+ });
1819
+ const pt2 = w.line.point(d / w.length);
1820
+ packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
1821
+ }
1822
+ });
1823
+ fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
1824
+ metrics2.flows.set(flow.id, fm);
1825
+ }
1826
+ const piles = /* @__PURE__ */ new Map();
1827
+ for (const flow of list) {
1828
+ const waiting = waitingBy.get(flow.id) ?? [];
1829
+ const shapes2 = waiting.map(([s]) => s);
1830
+ for (const e of frame2.elements) {
1831
+ const c = carried[queueKey(e.id, flow.id)];
1832
+ if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1833
+ }
1834
+ if (shapes2.length === 0) continue;
1835
+ const volume = amountIn(flow, values);
1836
+ const every = everyOf(flow);
1837
+ const unit = every * Math.max(volume, TINY);
1838
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1839
+ const color2 = flow.color ?? DEFAULT_COLOR;
1840
+ shapes2.forEach((at, j) => {
1841
+ const shape = byID.get(at);
1842
+ if (!shape) return;
1843
+ const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1844
+ const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
1845
+ const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1846
+ let depth = before + backlog * since;
1847
+ if (backlog <= TINY && before > TINY) {
1848
+ const trial = new Map(room);
1849
+ const outlets = [];
1850
+ let total = 0;
1851
+ for (let n2 = 0; n2 < 32; n2++) {
1852
+ const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1853
+ const tree = flowTree(at, frame2, union2(offline, blocked));
1854
+ const goal = tree?.goals(flow.to, false)[0];
1855
+ if (!tree || goal === void 0) break;
1856
+ const r = route2(tree, goal);
1857
+ const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1858
+ const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1859
+ const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
1860
+ const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
1861
+ if (!(rate > TINY)) break;
1862
+ for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
1863
+ outlets.push({ hops: r.hops, shapes: r.shapes, rate });
1864
+ total += rate;
1865
+ if (!Number.isFinite(spare)) break;
1866
+ }
1867
+ if (total > TINY) {
1868
+ depth = Math.max(0, before - total * since);
1869
+ if (depth > TINY) {
1870
+ for (const [id, v] of trial) room.set(id, v);
1871
+ for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
1872
+ m.throughput += o.rate;
1873
+ });
1874
+ }
1875
+ const lasts = before / total;
1876
+ outlets.forEach((o, i) => {
1877
+ const w = way(o.hops, o.shapes);
1878
+ if (!w) return;
1879
+ const spacing = Math.max(unit / o.rate, 0.05);
1880
+ for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
1881
+ const d = along(w, since - k * spacing, speed);
1882
+ if (d === null) continue;
1883
+ const pt2 = w.line.point(d / w.length);
1884
+ 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));
1885
+ }
1886
+ });
1887
+ }
1888
+ }
1889
+ if (!(depth > TINY || backlog > TINY)) return;
1890
+ bump(at, (m) => {
1891
+ m.depth += depth;
1892
+ });
1893
+ if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
1894
+ const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
1895
+ const rows = Math.trunc((n + 9) / 10);
1896
+ const base2 = piles.get(at) ?? 0;
1897
+ piles.set(at, base2 + rows);
1898
+ for (let q = 0; q < n; q++) {
1899
+ const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
1900
+ const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
1901
+ packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
1902
+ }
1903
+ });
1904
+ }
1905
+ for (const e of frame2.elements) {
1906
+ const c = e.capacity ?? null;
1907
+ const m = metrics2.elements.get(e.id);
1908
+ if (c === null || !Number.isFinite(c) || !m) continue;
1909
+ m.load = c > 0 ? m.throughput / c : 1;
1910
+ }
1911
+ return { packets, metrics: metrics2 };
1912
+ }
1913
+ var BLANK = {
1914
+ seed: 1,
1915
+ text: null,
1916
+ connector: null,
1917
+ groupID: null,
1918
+ points: null,
1919
+ path: null,
1920
+ subpaths: null,
1921
+ evenOdd: null,
1922
+ rotation: null,
1923
+ link: null,
1924
+ url: null,
1925
+ anchor: null,
1926
+ click: null,
1927
+ hover: null,
1928
+ when: null,
1929
+ overrides: null,
1930
+ symbol: null,
1931
+ playback: null,
1932
+ offset: null,
1933
+ args: null,
1934
+ image: null,
1935
+ reveal: null
1936
+ };
1937
+ function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
1938
+ return {
1939
+ stroke: { ...stroke },
1940
+ fill: { ...fill },
1941
+ fillStyle,
1942
+ strokeWidth,
1943
+ dash: "solid",
1944
+ roughness: 0,
1945
+ bowing: 1,
1946
+ fontSize: 18,
1947
+ font: null,
1948
+ gridSize: null,
1949
+ opacity: opacity2,
1950
+ dashOffset: null,
1951
+ textColor: null
1952
+ };
1953
+ }
1954
+ function packet(id, x, y, size, color2, layerID) {
1955
+ return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
1956
+ }
1957
+ function metricLookup(m, frame2) {
1958
+ const anchors = /* @__PURE__ */ new Map();
1959
+ for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
1960
+ 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}$/;
1961
+ return (term, own) => {
1962
+ const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
1963
+ return id === null ? null : metricValue(m, term.metric, id);
1964
+ };
1965
+ }
1966
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
1967
+ const lookup = metricLookup(metrics2, frame2);
1968
+ const out = [], readings = {};
1969
+ for (let e of elements) {
1970
+ if (values !== null && e.overrides) {
1971
+ for (const o of e.overrides) {
1972
+ const own = e.id;
1973
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1974
+ }
1975
+ }
1976
+ if (!e.gauge || values === null) {
1977
+ out.push(e);
1978
+ continue;
1979
+ }
1980
+ const g = e.gauge;
1981
+ let shown = metricValue(metrics2, g.metric, g.of);
1982
+ const then = carried[gaugeKey(e.id)];
1983
+ if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
1984
+ const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
1985
+ shown = then + (shown - then) * smooth;
1986
+ }
1987
+ if (shown !== null) readings[gaugeKey(e.id)] = shown;
1988
+ out.push(...gauge(e, g, shown, frame2, flows));
1989
+ }
1990
+ return { elements: out, readings };
1991
+ }
1992
+ var gaugeKey = (id) => "gauge|" + id;
1993
+ var GAUGE_EASE = 1;
1994
+ var hex = (h) => {
1995
+ const v = parseInt(h.slice(1), 16);
1996
+ return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
1997
+ };
1998
+ var GAUGE_GREEN = hex("#16a34a");
1999
+ var GAUGE_AMBER = hex("#d97706");
2000
+ var GAUGE_RED = hex("#dc2626");
2001
+ var GAUGE_BLUE = hex("#2f6fdf");
2002
+ var GAUGE_TRACK = hex("#e5e7eb");
2003
+ var GAUGE_GRAY = hex("#9ca3af");
2004
+ function formatted(metric, v) {
2005
+ if (v === null || !Number.isFinite(v)) return "\u2013";
2006
+ switch (metric) {
2007
+ case "load":
2008
+ case "backup":
2009
+ return formatNumber(v * 100, 0) + "%";
2010
+ case "transit":
2011
+ case "delay":
2012
+ return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
2013
+ default:
2014
+ return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
2015
+ }
2016
+ }
2017
+ function formatNumber(v, places) {
2018
+ const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
2019
+ const n = Math.floor(Math.abs(v) * scale + 0.5);
2020
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
2021
+ const whole = Math.trunc(n / scale), part = n - whole * scale;
2022
+ let s = String(whole);
2023
+ if (places > 0 && part > 0) {
2024
+ let digits = String(part);
2025
+ while (digits.length < places) digits = "0" + digits;
2026
+ while (digits.endsWith("0")) digits = digits.slice(0, -1);
2027
+ s += "." + digits;
2028
+ }
2029
+ return (v < 0 && n > 0 ? "-" : "") + s;
2030
+ }
2031
+ function gauge(e, g, value2, frame2, flows) {
2032
+ const target = frame2.elements.find((x) => x.id === g.of);
2033
+ const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
2034
+ const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
2035
+ const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
2036
+ let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
2037
+ if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
2038
+ if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
2039
+ if (value2 === null) color2 = GAUGE_GRAY;
2040
+ let name = g.label;
2041
+ if (name === null) {
2042
+ const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
2043
+ name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
2044
+ }
2045
+ const kind = g.style ?? "number";
2046
+ const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
2047
+ const line2 = (a, b) => [
2048
+ a.x / w,
2049
+ a.y / h,
2050
+ (a.x + (b.x - a.x) / 3) / w,
2051
+ (a.y + (b.y - a.y) / 3) / h,
2052
+ (a.x + 2 * (b.x - a.x) / 3) / w,
2053
+ (a.y + 2 * (b.y - a.y) / 3) / h,
2054
+ b.x / w,
2055
+ b.y / h
2056
+ ];
2057
+ const box = (x, y, bw, bh) => {
2058
+ const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
2059
+ let coords = line2(p[0], p[1]);
2060
+ for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
2061
+ return { closed: true, coords };
2062
+ };
2063
+ const arc = (c, radius, a0, a1) => {
2064
+ const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
2065
+ const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
2066
+ const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
2067
+ const s = at(a0);
2068
+ const coords = [s.x / w, s.y / h];
2069
+ for (let k = 0; k < pieces2; k++) {
2070
+ const a = a0 + k * step, b = a + step;
2071
+ const p0 = at(a), p3 = at(b);
2072
+ const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
2073
+ const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
2074
+ coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
2075
+ }
2076
+ return { closed: false, coords };
2077
+ };
2078
+ const part = (k, subpaths, stroke, fill, width) => ({
2079
+ ...BLANK,
2080
+ id: packetID(e.id, -(2 ** 38) - k),
2081
+ kind: "path",
2082
+ layerID: e.layerID,
2083
+ x: e.x,
2084
+ y: e.y,
2085
+ width: e.width,
2086
+ height: e.height,
2087
+ style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
2088
+ subpaths
2089
+ });
2090
+ const back = {
2091
+ ...e,
2092
+ gauge: null,
2093
+ kind: "path",
2094
+ subpaths: [box(0, 0, w, h)],
2095
+ text: name + "\n" + formatted(g.metric, value2),
2096
+ style: { ...e.style }
2097
+ };
2098
+ const parts = [];
2099
+ if (kind === "number") {
2100
+ back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
2101
+ back.style.textColor = { ...color2 };
2102
+ } else if (kind === "bar") {
2103
+ back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
2104
+ const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
2105
+ const trackW = w - 2 * inset2;
2106
+ parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
2107
+ if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
2108
+ } else {
2109
+ back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
2110
+ const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
2111
+ const thick = Math.max(2, radius * 0.16);
2112
+ parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
2113
+ if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
2114
+ const a = Math.PI + Math.PI * fraction;
2115
+ const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
2116
+ parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
2117
+ }
2118
+ return [back, ...parts];
2119
+ }
2120
+
1276
2121
  // src/rough.ts
1277
- var offset = (rng, amount, roughness) => rng.signed() * amount * roughness;
2122
+ var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
1278
2123
  function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1279
2124
  const length = Math.hypot(b.x - a.x, b.y - a.y);
1280
2125
  if (!(length > 0.5)) return;
@@ -2130,17 +2975,43 @@ function trackMap(tl) {
2130
2975
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2131
2976
  return m;
2132
2977
  }
2133
- function resolved(page, t) {
2134
- return expandingSymbols(posed(page, t), clampTime(t));
2978
+ function resolved(page, t, state = "initial", since = null, carried = {}) {
2979
+ return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
2135
2980
  }
2136
- function posed(page, t) {
2137
- const timeline = page.timeline;
2138
- if (!timeline) return page;
2981
+ var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2982
+ function posed(page, t, state = "initial", since = null, carried = {}) {
2983
+ return posedWithMetrics(page, t, state, since, carried).frame;
2984
+ }
2985
+ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
2986
+ const off = stateHidden(page, state);
2987
+ const values = stateValues(page, state);
2988
+ const active = /* @__PURE__ */ new Map();
2989
+ for (const e of page.elements) {
2990
+ const o = activeOverrides(e, values);
2991
+ if (o.length > 0) active.set(e.id, o);
2992
+ }
2993
+ const flowing = (page.flows ?? []).length > 0;
2994
+ const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
2995
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
2996
+ if (!timeline) return { frame: page, metrics: emptyMetrics() };
2139
2997
  t = clampTime(t);
2140
2998
  const tracks = trackMap(timeline);
2999
+ for (const [id, overrides] of active) {
3000
+ const track = tracks.get(id);
3001
+ if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
3002
+ tracks.set(id, {
3003
+ ...track,
3004
+ follow: track.follow.map((f) => {
3005
+ let path = f.path;
3006
+ for (const o of overrides) path = o.reroute?.[path] ?? path;
3007
+ return { ...f, path };
3008
+ })
3009
+ });
3010
+ }
2141
3011
  const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
2142
3012
  const hidden = /* @__PURE__ */ new Set();
2143
3013
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
3014
+ for (const id of off) hidden.add(id);
2144
3015
  let moving = null;
2145
3016
  if (timeline.settle === true) {
2146
3017
  moving = /* @__PURE__ */ new Set();
@@ -2156,6 +3027,14 @@ function posed(page, t) {
2156
3027
  let elements = page.elements.map((e) => {
2157
3028
  const track = tracks.get(e.id);
2158
3029
  let out = track ? applyTrack(track, e, t) : e;
3030
+ for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3031
+ const list = active.get(e.id);
3032
+ if (list) {
3033
+ const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
3034
+ const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
3035
+ if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
3036
+ if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
3037
+ }
2159
3038
  if (reseed !== null && reseed > 0 && boils(e)) {
2160
3039
  if (out === e) out = cloneElement(e);
2161
3040
  out.seed = boiled(e.seed, reseed);
@@ -2179,7 +3058,7 @@ function posed(page, t) {
2179
3058
  elements = elements.map((e) => {
2180
3059
  const c = e.connector;
2181
3060
  if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
2182
- if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
3061
+ if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false) || [c.start.elementID, c.end.elementID].some((id) => id !== null && off.has(id))) {
2183
3062
  gone.add(e.id);
2184
3063
  return e;
2185
3064
  }
@@ -2189,13 +3068,24 @@ function posed(page, t) {
2189
3068
  pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
2190
3069
  return pinned;
2191
3070
  });
2192
- return { ...page, elements: elements.filter((e) => !gone.has(e.id)), timeline: null };
3071
+ elements = elements.filter((e) => !gone.has(e.id));
3072
+ let metrics2 = emptyMetrics();
3073
+ if (measuring) {
3074
+ const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
3075
+ const frame2 = { ...page, elements, timeline: null };
3076
+ const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
3077
+ metrics2 = out.metrics;
3078
+ const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
3079
+ Object.assign(metrics2.carry, finished.readings);
3080
+ elements = [...finished.elements, ...out.packets];
3081
+ }
3082
+ return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
2193
3083
  }
2194
3084
  function posterTime(page) {
2195
3085
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
2196
3086
  }
2197
3087
  function still(page) {
2198
- return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
3088
+ return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
2199
3089
  }
2200
3090
  function cameraRect(tl, t) {
2201
3091
  const keys = tl.camera ?? [];
@@ -2304,7 +3194,7 @@ function filled(s, args) {
2304
3194
  }) };
2305
3195
  }
2306
3196
  function symbolPage(s) {
2307
- return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
3197
+ return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
2308
3198
  }
2309
3199
  function hasInstances(page) {
2310
3200
  return page.elements.some((e) => e.kind === "symbol");
@@ -2411,8 +3301,8 @@ function mix2(x) {
2411
3301
  return z ^ z >> 31n;
2412
3302
  }
2413
3303
  function halves(id) {
2414
- const hex = id.replace(/-/g, "").padStart(32, "0").slice(-32);
2415
- return [BigInt(`0x${hex.slice(0, 16)}`), BigInt(`0x${hex.slice(16)}`)];
3304
+ const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3305
+ return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
2416
3306
  }
2417
3307
  function derivedID(instance, inner) {
2418
3308
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -2433,16 +3323,21 @@ function clickTargets(page, file) {
2433
3323
  let pageOf = null;
2434
3324
  for (const e of page.elements) {
2435
3325
  const c = e.click;
3326
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
2436
3327
  if (c?.scene) {
2437
3328
  const i = sceneNamed(c.scene, markers);
2438
3329
  if (i >= 0) {
2439
- out.set(e.id, { scene: i });
3330
+ out.set(e.id, { scene: i, ...state });
2440
3331
  continue;
2441
3332
  }
2442
3333
  }
2443
3334
  const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
2444
3335
  if (url) {
2445
- out.set(e.id, { url });
3336
+ out.set(e.id, { url, ...state });
3337
+ continue;
3338
+ }
3339
+ if (state) {
3340
+ out.set(e.id, { state: true, ...state });
2446
3341
  continue;
2447
3342
  }
2448
3343
  if (e.link && file && file.pages.length > 1) {
@@ -2461,14 +3356,14 @@ function clickTargets(page, file) {
2461
3356
  function hoveredElement(e) {
2462
3357
  const h = e.hover;
2463
3358
  if (!h) return e;
2464
- const style = { ...e.style };
2465
- if (h.stroke) style.stroke = h.stroke;
2466
- if (h.fill) style.fill = h.fill;
3359
+ const style2 = { ...e.style };
3360
+ if (h.stroke) style2.stroke = h.stroke;
3361
+ if (h.fill) style2.fill = h.fill;
2467
3362
  if (h.opacity !== null && Number.isFinite(h.opacity)) {
2468
3363
  const o = Math.min(Math.max(h.opacity, 0), 1);
2469
- style.opacity = o >= 1 ? null : o;
3364
+ style2.opacity = o >= 1 ? null : o;
2470
3365
  }
2471
- return { ...e, style };
3366
+ return { ...e, style: style2 };
2472
3367
  }
2473
3368
  function hovered(page, ids) {
2474
3369
  return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
@@ -2672,14 +3567,14 @@ function parseList(lines, start) {
2672
3567
  }
2673
3568
  return { block: { kind: "list", ordered, items }, next: i };
2674
3569
  }
2675
- function inline(src, style = {}) {
3570
+ function inline(src, style2 = {}) {
2676
3571
  const out = [];
2677
3572
  const push = (text, s) => {
2678
3573
  if (text) out.push({ ...s, text });
2679
3574
  };
2680
3575
  let i = 0, buf = "";
2681
3576
  const emit = () => {
2682
- push(buf, style);
3577
+ push(buf, style2);
2683
3578
  buf = "";
2684
3579
  };
2685
3580
  while (i < src.length) {
@@ -2707,43 +3602,43 @@ function inline(src, style = {}) {
2707
3602
  emit();
2708
3603
  let code = m[2].replace(/\n/g, " ");
2709
3604
  if (/^ .* $/.test(code)) code = code.slice(1, -1);
2710
- push(code, { ...style, mono: true });
3605
+ push(code, { ...style2, mono: true });
2711
3606
  i += m[0].length;
2712
3607
  continue;
2713
3608
  }
2714
3609
  if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
2715
3610
  emit();
2716
- out.push(...inline(m[1], style));
3611
+ out.push(...inline(m[1], style2));
2717
3612
  i += m[0].length;
2718
3613
  continue;
2719
3614
  }
2720
3615
  if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
2721
3616
  emit();
2722
- out.push(...inline(m[1], { ...style, link: true }));
3617
+ out.push(...inline(m[1], { ...style2, link: true }));
2723
3618
  i += m[0].length;
2724
3619
  continue;
2725
3620
  }
2726
3621
  if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
2727
3622
  emit();
2728
- push(m[1], { ...style, link: true });
3623
+ push(m[1], { ...style2, link: true });
2729
3624
  i += m[0].length;
2730
3625
  continue;
2731
3626
  }
2732
3627
  if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
2733
3628
  emit();
2734
- out.push(...inline(m[1], { ...style, strike: true }));
3629
+ out.push(...inline(m[1], { ...style2, strike: true }));
2735
3630
  i += m[0].length;
2736
3631
  continue;
2737
3632
  }
2738
3633
  if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
2739
3634
  emit();
2740
- out.push(...inline(m[2], { ...style, bold: true }));
3635
+ out.push(...inline(m[2], { ...style2, bold: true }));
2741
3636
  i += m[0].length;
2742
3637
  continue;
2743
3638
  }
2744
3639
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
2745
3640
  emit();
2746
- out.push(...inline(m[2], { ...style, italic: true }));
3641
+ out.push(...inline(m[2], { ...style2, italic: true }));
2747
3642
  i += m[0].length;
2748
3643
  continue;
2749
3644
  }
@@ -3356,6 +4251,16 @@ var Playback = class {
3356
4251
  __publicField(this, "plan");
3357
4252
  __publicField(this, "scenes");
3358
4253
  __publicField(this, "sceneNames");
4254
+ /** Whether each scene loops (Marker.loop). */
4255
+ __publicField(this, "sceneLoops");
4256
+ /** The page's variables now: their starting values until clicks change them. */
4257
+ __publicField(this, "state");
4258
+ /** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
4259
+ __publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
4260
+ /** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
4261
+ __publicField(this, "still", false);
4262
+ /** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
4263
+ __publicField(this, "carried", {});
3359
4264
  __publicField(this, "background");
3360
4265
  __publicField(this, "ink");
3361
4266
  /** What clicking each clickable element does, by element id. */
@@ -3415,6 +4320,8 @@ var Playback = class {
3415
4320
  };
3416
4321
  this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
3417
4322
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4323
+ this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4324
+ this.state = { ...page.variables ?? {} };
3418
4325
  const theme = themeNamed(page.theme);
3419
4326
  this.background = theme?.background ?? WHITE;
3420
4327
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
@@ -3425,6 +4332,10 @@ var Playback = class {
3425
4332
  this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
3426
4333
  into.replaceChildren(this.svg);
3427
4334
  }
4335
+ /** Seconds since the variables last changed, or null for a still. */
4336
+ sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
4337
+ return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
4338
+ }
3428
4339
  get frameCount() {
3429
4340
  return this.plan.times.length;
3430
4341
  }
@@ -3465,7 +4376,7 @@ var Playback = class {
3465
4376
  const times = /* @__PURE__ */ new Map();
3466
4377
  for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
3467
4378
  const tc = clampTime(t);
3468
- const pose = posed(this.page, tc);
4379
+ const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
3469
4380
  const frame2 = expandingSymbols(pose, tc, true, times);
3470
4381
  const { parts } = svgParts(frame2, this.background, r);
3471
4382
  const owned = (m, parts2) => {
@@ -3587,6 +4498,20 @@ ${h}</g>
3587
4498
  });
3588
4499
  return s;
3589
4500
  }
4501
+ /** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
4502
+ sceneEnd(s) {
4503
+ return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
4504
+ }
4505
+ /** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
4506
+ * the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
4507
+ step(i, loops) {
4508
+ const next = i + 1;
4509
+ const s = this.sceneAt(i);
4510
+ if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
4511
+ if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
4512
+ const entered = this.scenes.indexOf(next);
4513
+ return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4514
+ }
3590
4515
  };
3591
4516
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3592
4517
  } : HTMLElement;
@@ -3708,6 +4633,7 @@ var DottiePlayer = class extends ElementBase {
3708
4633
  const file = this.file;
3709
4634
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
3710
4635
  const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
4636
+ p.still = this.hasAttribute("at");
3711
4637
  this.playback = p;
3712
4638
  this.style.setProperty("--dottie-page", color(p.background));
3713
4639
  this.style.setProperty("--dottie-ink", color(p.ink));
@@ -3740,7 +4666,7 @@ var DottiePlayer = class extends ElementBase {
3740
4666
  this.setPlaying(false);
3741
4667
  return;
3742
4668
  }
3743
- this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
4669
+ this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
3744
4670
  }
3745
4671
  // MARK: Playback
3746
4672
  get paused() {
@@ -3766,11 +4692,13 @@ var DottiePlayer = class extends ElementBase {
3766
4692
  pause() {
3767
4693
  this.setPlaying(false);
3768
4694
  }
3769
- /** Plays on to the next scene (from the start after the last). */
4695
+ /** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
3770
4696
  nextScene() {
3771
4697
  const p = this.playback;
3772
4698
  if (!p) return;
3773
- if (p.frame >= p.frameCount - 1) p.show(0);
4699
+ const s = p.sceneAt(p.frame);
4700
+ if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
4701
+ else if (p.frame >= p.frameCount - 1) p.show(0);
3774
4702
  this.setPlaying(true);
3775
4703
  }
3776
4704
  /** Back to the start of this scene, or the one before if already there. */
@@ -3799,6 +4727,14 @@ var DottiePlayer = class extends ElementBase {
3799
4727
  this.scheduleReplay();
3800
4728
  }
3801
4729
  if (!click) return;
4730
+ if (click.set || click.toggle || click.reset) {
4731
+ this.setState(applyClick(
4732
+ { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
4733
+ p.state,
4734
+ p.page.variables ?? {}
4735
+ ));
4736
+ }
4737
+ if ("state" in click) return;
3802
4738
  if ("url" in click) {
3803
4739
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
3804
4740
  } else if ("page" in click) {
@@ -3806,13 +4742,30 @@ var DottiePlayer = class extends ElementBase {
3806
4742
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
3807
4743
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
3808
4744
  this.setPlaying(false);
3809
- p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
4745
+ p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
3810
4746
  } else {
3811
4747
  p.show(p.scenes[click.scene]);
3812
4748
  this.setPlaying(true);
3813
4749
  }
3814
4750
  }
3815
4751
  }
4752
+ /** The page's variables now. */
4753
+ get variables() {
4754
+ return { ...this.playback?.state ?? {} };
4755
+ }
4756
+ /** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
4757
+ setState(values) {
4758
+ const p = this.playback;
4759
+ if (!p) return;
4760
+ const next = { ...p.state, ...values };
4761
+ if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
4762
+ const t = clampTime(p.last.t);
4763
+ p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
4764
+ p.state = next;
4765
+ p.stateSince = performance.now();
4766
+ p.redraw();
4767
+ this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
4768
+ }
3816
4769
  /** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
3817
4770
  scheduleReplay() {
3818
4771
  if (this.replayRaf) return;
@@ -3838,7 +4791,8 @@ var DottiePlayer = class extends ElementBase {
3838
4791
  schedule() {
3839
4792
  if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
3840
4793
  }
3841
- /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end. */
4794
+ /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
4795
+ * looping scene plays round (Playback.step). */
3842
4796
  tick(now) {
3843
4797
  this.raf = 0;
3844
4798
  const p = this.playback;
@@ -3849,14 +4803,14 @@ var DottiePlayer = class extends ElementBase {
3849
4803
  let advanced = false;
3850
4804
  while (this.playing && this.acc >= step) {
3851
4805
  this.acc -= step;
3852
- const next = p.frame + 1;
3853
- if (next >= p.frameCount && (p.scenes.length || this.hasAttribute("no-loop"))) {
4806
+ const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
4807
+ if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
3854
4808
  this.setPlaying(false);
3855
4809
  break;
3856
4810
  }
3857
- p.frame = next % p.frameCount;
4811
+ p.frame = frame2;
3858
4812
  advanced = true;
3859
- if (p.scenes.includes(p.frame)) {
4813
+ if (stop) {
3860
4814
  this.setPlaying(false);
3861
4815
  break;
3862
4816
  }
@@ -4006,7 +4960,7 @@ function render(source, options = {}) {
4006
4960
  function isFile(v) {
4007
4961
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4008
4962
  }
4009
- var VERSION = "0.6.3";
4963
+ var VERSION = "0.8.0";
4010
4964
 
4011
4965
  // src/player-entry.ts
4012
4966
  definePlayer();