@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.
package/dist/dottie.js CHANGED
@@ -1,15 +1,148 @@
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 __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
5
5
 
6
+ // src/state.ts
7
+ var MAX_NAME = 64;
8
+ function isName(name) {
9
+ return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
10
+ }
11
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
12
+ var OPS = [">=", "<=", "==", "!=", ">", "<"];
13
+ function metricTerm(term) {
14
+ const t = trim(term);
15
+ if (!t.includes("(")) {
16
+ const op2 = t.search(/[>=<!]/);
17
+ if (op2 < 0) return null;
18
+ const name2 = trim(t.slice(0, op2));
19
+ if (!METRIC_NAMES.includes(name2)) return null;
20
+ const m = metricTerm(name2 + "() " + t.slice(op2));
21
+ return m ? { ...m, target: "" } : null;
22
+ }
23
+ const open = t.indexOf("("), close = t.indexOf(")");
24
+ if (open < 0 || close < 0 || open >= close) return null;
25
+ const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
26
+ let rest = trim(t.slice(close + 1));
27
+ if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
28
+ const op = OPS.find((o) => rest.startsWith(o));
29
+ if (op === void 0) return null;
30
+ rest = trim(rest.slice(op.length));
31
+ if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
32
+ return { metric: name, target, op, value: Number(rest) };
33
+ }
34
+ function passes(m, v) {
35
+ switch (m.op) {
36
+ case ">":
37
+ return v > m.value;
38
+ case ">=":
39
+ return v >= m.value;
40
+ case "<":
41
+ return v < m.value;
42
+ case "<=":
43
+ return v <= m.value;
44
+ case "==":
45
+ return v === m.value;
46
+ default:
47
+ return v !== m.value;
48
+ }
49
+ }
50
+ function mentionsMetric(condition) {
51
+ return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
52
+ }
53
+ function conditionNames(condition) {
54
+ const out = [];
55
+ for (const any of condition.split("||")) {
56
+ for (const all of any.split("&&")) {
57
+ if (metricTerm(all) !== null) continue;
58
+ let term = trim(all);
59
+ if (term.startsWith("!")) term = trim(term.slice(1));
60
+ if (!isName(term)) return null;
61
+ out.push(term);
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+ function holds(condition, state, metric = null) {
67
+ if (conditionNames(condition) === null) return false;
68
+ return condition.split("||").some((any) => any.split("&&").every((all) => {
69
+ const m = metricTerm(all);
70
+ if (m !== null) {
71
+ const v = metric ? metric(m) : null;
72
+ return v !== null && passes(m, v);
73
+ }
74
+ let term = trim(all);
75
+ let negated = false;
76
+ if (term.startsWith("!")) {
77
+ negated = true;
78
+ term = trim(term.slice(1));
79
+ }
80
+ return (state[term] ?? false) !== negated;
81
+ }));
82
+ }
83
+ function stateValues(page, choice) {
84
+ if (choice === "all") return null;
85
+ if (choice === "initial") return { ...page.variables ?? {} };
86
+ return { ...page.variables ?? {}, ...choice };
87
+ }
88
+ function stateHidden(page, choice) {
89
+ const values = stateValues(page, choice);
90
+ const out = /* @__PURE__ */ new Set();
91
+ if (!values) return out;
92
+ for (const e of page.elements) if (e.when !== null && !holds(e.when, values)) out.add(e.id);
93
+ return out;
94
+ }
95
+ function activeOverrides(e, values) {
96
+ if (!values || !e.overrides) return [];
97
+ return e.overrides.filter((o) => !mentionsMetric(o.when) && holds(o.when, values));
98
+ }
99
+ function looksOnly(o) {
100
+ return { ...o, reroute: null, offline: null, cost: null, capacity: null, delay: null };
101
+ }
102
+ function applyOverride(o, e) {
103
+ const out = cloneElement(e);
104
+ if (o.stroke) out.style.stroke = { ...o.stroke };
105
+ if (o.fill) out.style.fill = { ...o.fill };
106
+ if (o.textColor) out.style.textColor = { ...o.textColor };
107
+ if (o.strokeWidth !== null) out.style.strokeWidth = Math.max(0, o.strokeWidth);
108
+ if (o.dash !== null) out.style.dash = o.dash;
109
+ if (o.text !== null) out.text = o.text;
110
+ if (o.cost !== null && out.connector) out.connector.cost = Math.min(Math.max(o.cost, 0), 1e6);
111
+ if (o.capacity !== null) out.capacity = Math.max(o.capacity, 0);
112
+ if (o.delay !== null && out.kind !== "connector") out.delay = Math.max(o.delay, 0);
113
+ if (o.opacity !== null) {
114
+ const v = Math.min(Math.max(o.opacity, 0), 1) * (out.style.opacity ?? 1);
115
+ out.style.opacity = v >= 1 ? null : v;
116
+ }
117
+ return out;
118
+ }
119
+ function changesState(c) {
120
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
121
+ }
122
+ function applyClick(c, state, initial = {}) {
123
+ let out = { ...state };
124
+ if (c.reset === true) {
125
+ out = {};
126
+ for (const k of Object.keys(state)) out[k] = false;
127
+ Object.assign(out, initial);
128
+ }
129
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
130
+ for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
131
+ return out;
132
+ }
133
+ function trim(s) {
134
+ return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
135
+ }
136
+
6
137
  // src/model.ts
7
138
  var SUPPORTED_VERSION = 2;
8
139
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
9
140
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
10
141
  var KNOWN_FIELDS = {
11
142
  file: ["version", "pages", "images"],
12
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
143
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
144
+ flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
145
+ flowOverride: ["when", "volume"],
13
146
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
14
147
  layer: ["id", "name", "visible", "locked"],
15
148
  rgba: ["r", "g", "b", "a"],
@@ -37,6 +170,8 @@ var KNOWN_FIELDS = {
37
170
  "anchor",
38
171
  "click",
39
172
  "hover",
173
+ "when",
174
+ "overrides",
40
175
  "symbol",
41
176
  "playback",
42
177
  "offset",
@@ -45,8 +180,13 @@ var KNOWN_FIELDS = {
45
180
  "reveal",
46
181
  "ports",
47
182
  "labelRect",
48
- "extension"
183
+ "extension",
184
+ "capacity",
185
+ "delay",
186
+ "queue",
187
+ "gauge"
49
188
  ],
189
+ gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
50
190
  strokePoint: ["x", "y"],
51
191
  subpath: ["closed", "coords"],
52
192
  inlineImage: ["data", "format"],
@@ -54,7 +194,7 @@ var KNOWN_FIELDS = {
54
194
  extensionRef: ["id", "shape", "version", "params"],
55
195
  normalizedPoint: ["x", "y"],
56
196
  normalizedRect: ["x", "y", "width", "height"],
57
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops"],
197
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
58
198
  waypoint: ["x", "y"],
59
199
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
60
200
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -62,9 +202,10 @@ var KNOWN_FIELDS = {
62
202
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
63
203
  repeat: ["start", "end", "count"],
64
204
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
65
- marker: ["t", "name"],
66
- click: ["scene", "url"],
67
- hover: ["stroke", "fill", "opacity"]
205
+ marker: ["t", "name", "loop"],
206
+ click: ["scene", "url", "set", "toggle", "reset"],
207
+ hover: ["stroke", "fill", "opacity"],
208
+ override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
68
209
  };
69
210
  var MAX_DURATION = 3600;
70
211
  var MAX_MARCH = 1e4;
@@ -80,6 +221,8 @@ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
80
221
  var str = (v) => typeof v === "string" ? v : void 0;
81
222
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
82
223
  var bool = (v) => typeof v === "boolean" ? v : void 0;
224
+ var NO_ID = "00000000-0000-0000-0000-000000000000";
225
+ var UUID = /^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/;
83
226
  var uuid = (v) => typeof v === "string" ? v.toUpperCase() : void 0;
84
227
  function clampTime(t) {
85
228
  if (!Number.isFinite(t)) return t === Infinity ? MAX_DURATION : 0;
@@ -171,7 +314,8 @@ var Reader = class {
171
314
  this.fields(w, "waypoint");
172
315
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
173
316
  }) : null,
174
- hops: typeof v.hops === "boolean" ? v.hops : null
317
+ hops: typeof v.hops === "boolean" ? v.hops : null,
318
+ cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
175
319
  };
176
320
  }
177
321
  image(v, table) {
@@ -222,6 +366,8 @@ var Reader = class {
222
366
  anchor: str(v.anchor) ?? null,
223
367
  click: this.click(v.click),
224
368
  hover: this.hover(v.hover),
369
+ when: str(v.when) ?? null,
370
+ overrides: this.overrides(v.overrides),
225
371
  symbol: uuid(v.symbol) ?? null,
226
372
  playback: this.playback(v.playback),
227
373
  offset: num(v.offset) ?? null,
@@ -233,7 +379,26 @@ var Reader = class {
233
379
  return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
234
380
  }) : null,
235
381
  labelRect: this.labelRect(v.labelRect),
236
- extension: this.extension(v.extension)
382
+ extension: this.extension(v.extension),
383
+ capacity: num(v.capacity) ?? null,
384
+ delay: num(v.delay) ?? null,
385
+ queue: v.queue === true ? true : null,
386
+ gauge: this.gauge(v.gauge)
387
+ };
388
+ }
389
+ gauge(v) {
390
+ if (!isObj(v)) return null;
391
+ this.fields(v, "gauge");
392
+ const finite = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
393
+ const style2 = str(v.style);
394
+ return {
395
+ metric: str(v.metric) ?? "",
396
+ of: typeof v.of === "string" && UUID.test(v.of) ? v.of.toUpperCase() : NO_ID,
397
+ max: finite(v.max),
398
+ warn: finite(v.warn),
399
+ alarm: finite(v.alarm),
400
+ style: style2 === "number" || style2 === "bar" || style2 === "dial" ? style2 : null,
401
+ label: str(v.label) ?? null
237
402
  };
238
403
  }
239
404
  labelRect(v) {
@@ -257,13 +422,27 @@ var Reader = class {
257
422
  }
258
423
  return { id, shape, version, params };
259
424
  }
425
+ variables(v) {
426
+ if (!isObj(v)) return null;
427
+ const out = {};
428
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
429
+ return Object.keys(out).length > 0 ? out : null;
430
+ }
260
431
  click(v) {
261
432
  if (!isObj(v)) return null;
262
433
  this.fields(v, "click");
263
434
  const url = str(v.url);
264
435
  const safe = url === void 0 ? null : allowedURL(url);
265
436
  if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
266
- return { scene: str(v.scene) ?? null, url: safe };
437
+ let set = null;
438
+ if (isObj(v.set)) {
439
+ set = {};
440
+ for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
441
+ if (Object.keys(set).length === 0) set = null;
442
+ }
443
+ const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
444
+ const toggle = raw ? raw.filter(isName) : null;
445
+ return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
267
446
  }
268
447
  playback(v) {
269
448
  const s = str(v);
@@ -299,6 +478,35 @@ var Reader = class {
299
478
  params: strings(v.params)
300
479
  };
301
480
  }
481
+ /** As DottieKit reads them: a reroute whose targets aren't all ids is dropped, as are keys that aren't ids. */
482
+ overrides(v) {
483
+ if (!Array.isArray(v)) return null;
484
+ return v.filter(isObj).map((o) => {
485
+ this.fields(o, "override");
486
+ const dash = str(o.dash);
487
+ let reroute = null;
488
+ if (isObj(o.reroute) && Object.values(o.reroute).every((x) => typeof x === "string" && UUID.test(x))) {
489
+ reroute = {};
490
+ for (const [k, x] of Object.entries(o.reroute)) if (UUID.test(k)) reroute[k.toUpperCase()] = x.toUpperCase();
491
+ if (Object.keys(reroute).length === 0) reroute = null;
492
+ }
493
+ return {
494
+ when: str(o.when) ?? "",
495
+ stroke: rgba(o.stroke) ?? null,
496
+ fill: rgba(o.fill) ?? null,
497
+ textColor: rgba(o.textColor) ?? null,
498
+ opacity: num(o.opacity) ?? null,
499
+ strokeWidth: num(o.strokeWidth) ?? null,
500
+ dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
501
+ text: str(o.text) ?? null,
502
+ reroute,
503
+ offline: o.offline === true ? true : null,
504
+ cost: typeof o.cost === "number" && Number.isFinite(o.cost) ? o.cost : null,
505
+ capacity: typeof o.capacity === "number" && Number.isFinite(o.capacity) ? o.capacity : null,
506
+ delay: typeof o.delay === "number" && Number.isFinite(o.delay) ? o.delay : null
507
+ };
508
+ });
509
+ }
302
510
  hover(v) {
303
511
  if (!isObj(v)) return null;
304
512
  this.fields(v, "hover");
@@ -364,7 +572,7 @@ var Reader = class {
364
572
  if (camera) stableSortBy(camera, (c) => c.t);
365
573
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
366
574
  const t = num(m.t);
367
- return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "" }] : [];
575
+ return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
368
576
  }) : null;
369
577
  if (markers) stableSortBy(markers, (m) => m.t);
370
578
  const poster = num(v.poster);
@@ -397,7 +605,34 @@ var Reader = class {
397
605
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
398
606
  theme: str(v.theme) ?? null,
399
607
  timeline: this.timeline(v.timeline),
400
- symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
608
+ symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
609
+ variables: this.variables(v.variables),
610
+ flows: Array.isArray(v.flows) ? v.flows.filter(isObj).map((f) => this.flow(f)) : null
611
+ };
612
+ }
613
+ /** As DottieKit reads one: a missing or bad id, source or destination sends nothing; bad destinations are dropped. */
614
+ flow(f) {
615
+ this.fields(f, "flow");
616
+ const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
617
+ const finite = (x) => {
618
+ const n = num(x);
619
+ return n !== void 0 && Number.isFinite(n) ? n : null;
620
+ };
621
+ return {
622
+ id: id(f.id),
623
+ from: id(f.from),
624
+ to: Array.isArray(f.to) && f.to.every((x) => typeof x === "string") ? f.to.filter((x) => UUID.test(x)).map((x) => x.toUpperCase()) : [],
625
+ every: finite(f.every),
626
+ speed: finite(f.speed),
627
+ size: finite(f.size),
628
+ color: isObj(f.color) && ["r", "g", "b", "a"].every((k) => typeof f.color[k] === "number") ? rgba(f.color) ?? null : null,
629
+ when: str(f.when) ?? null,
630
+ spread: f.spread === true ? true : null,
631
+ volume: finite(f.volume),
632
+ overrides: Array.isArray(f.overrides) && f.overrides.length > 0 ? f.overrides.filter(isObj).map((o) => {
633
+ this.fields(o, "flowOverride");
634
+ return { when: str(o.when) ?? "", volume: finite(o.volume) };
635
+ }) : null
401
636
  };
402
637
  }
403
638
  };
@@ -459,7 +694,8 @@ function cloneElement(e) {
459
694
  start: { ...e.connector.start },
460
695
  end: { ...e.connector.end },
461
696
  waypoints: e.connector.waypoints?.map((w) => ({ ...w })) ?? null
462
- } : null
697
+ } : null,
698
+ gauge: e.gauge ? { ...e.gauge } : e.gauge
463
699
  };
464
700
  }
465
701
  function orderedElements(page) {
@@ -1210,8 +1446,617 @@ function mulConst(step) {
1210
1446
  return [p[0], p[1]];
1211
1447
  }
1212
1448
 
1449
+ // src/flows.ts
1450
+ var DEFAULT_EVERY = 0.5;
1451
+ var DEFAULT_SPEED = 160;
1452
+ var DEFAULT_SIZE = 12;
1453
+ var DEFAULT_COLOR = { r: 0.95, g: 0.36, b: 0.33, a: 1 };
1454
+ var clamp = (v, fallback, lo, hi) => v === null || !Number.isFinite(v) ? fallback : Math.min(Math.max(v, lo), hi);
1455
+ function packetID(flow, k) {
1456
+ const n = (k + 2 ** 40) % 2 ** 48;
1457
+ return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1458
+ }
1459
+ var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1460
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1461
+ const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1462
+ if (!shapes2.has(from)) return null;
1463
+ const next = /* @__PURE__ */ new Map();
1464
+ const add2 = (a, edge) => {
1465
+ const list = next.get(a);
1466
+ if (list) list.push(edge);
1467
+ else next.set(a, [edge]);
1468
+ };
1469
+ for (const e of frame2.elements) {
1470
+ const c = e.connector;
1471
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1472
+ if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1473
+ const cost = routeCost(c.cost);
1474
+ add2(a, { to: b, via: e.id, back: false, cost });
1475
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1476
+ }
1477
+ const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1478
+ let counter = 1;
1479
+ const tree = new FlowTree(from);
1480
+ const open = [from];
1481
+ while (open.length > 0) {
1482
+ let best = 0;
1483
+ for (let i = 1; i < open.length; i++) {
1484
+ const da = dist.get(open[i]), db = dist.get(open[best]);
1485
+ if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1486
+ }
1487
+ const n = open.splice(best, 1)[0];
1488
+ tree.settled.add(n);
1489
+ for (const edge of next.get(n) ?? []) {
1490
+ if (tree.settled.has(edge.to)) continue;
1491
+ const d = dist.get(n) + edge.cost;
1492
+ const old = dist.get(edge.to);
1493
+ if (old !== void 0 && old <= d) continue;
1494
+ if (old === void 0) open.push(edge.to);
1495
+ dist.set(edge.to, d);
1496
+ order.set(edge.to, counter++);
1497
+ tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1498
+ }
1499
+ }
1500
+ return tree;
1501
+ }
1502
+ var FlowTree = class {
1503
+ constructor(from) {
1504
+ __publicField(this, "settled", /* @__PURE__ */ new Set());
1505
+ __publicField(this, "came", /* @__PURE__ */ new Map());
1506
+ __publicField(this, "from");
1507
+ this.from = from;
1508
+ }
1509
+ /** The first of `to` that can be reached; with `spread`, when the first can't be, every other one that can. */
1510
+ goals(to, spread) {
1511
+ const reachable = to.filter((x) => x !== this.from && this.settled.has(x));
1512
+ if (reachable.length === 0) return [];
1513
+ return spread && reachable[0] !== to[0] ? reachable : [reachable[0]];
1514
+ }
1515
+ /** The connectors to `goal`, as [connector, backwards] hops. */
1516
+ hops(goal) {
1517
+ const hops = [];
1518
+ for (let at = goal, step = this.came.get(at); step; at = step.from, step = this.came.get(at)) hops.push([step.via, step.back]);
1519
+ return hops.reverse();
1520
+ }
1521
+ };
1522
+
1523
+ // src/measure.ts
1524
+ var emptyMetrics = () => ({ elements: /* @__PURE__ */ new Map(), flows: /* @__PURE__ */ new Map(), carry: {} });
1525
+ function metricValue(m, metric, id) {
1526
+ const f = m.flows.get(id);
1527
+ if (f) {
1528
+ switch (metric) {
1529
+ case "throughput":
1530
+ return f.placed;
1531
+ case "backlog":
1532
+ return f.backlog;
1533
+ case "backup":
1534
+ return f.backup;
1535
+ case "transit":
1536
+ return f.transit;
1537
+ default:
1538
+ return null;
1539
+ }
1540
+ }
1541
+ const e = m.elements.get(id);
1542
+ if (!e) return null;
1543
+ switch (metric) {
1544
+ case "throughput":
1545
+ return e.throughput;
1546
+ case "load":
1547
+ return e.load;
1548
+ case "delay":
1549
+ return e.delay;
1550
+ case "backlog":
1551
+ return e.backlog;
1552
+ case "inFlight":
1553
+ return e.inFlight;
1554
+ case "depth":
1555
+ return e.depth;
1556
+ default:
1557
+ return null;
1558
+ }
1559
+ }
1560
+ var TINY = 1e-9;
1561
+ var amount = (f) => clamp(f.volume, 1, 0, 1e12);
1562
+ function amountIn(f, values) {
1563
+ if (values === null || !f.overrides) return amount(f);
1564
+ let v = null;
1565
+ for (const o of f.overrides) if (o.volume !== null && !mentionsMetric(o.when) && holds(o.when, values)) v = o.volume;
1566
+ return v === null ? amount(f) : Math.min(Math.max(Number.isFinite(v) ? v : 1, 0), 1e12);
1567
+ }
1568
+ function delayOf(e) {
1569
+ const d = e.delay ?? null;
1570
+ if (e.kind === "connector" || d === null || !Number.isFinite(d)) return 0;
1571
+ return Math.min(Math.max(d, 0), 3600);
1572
+ }
1573
+ var queueKey = (shape, flow) => shape + "|" + flow;
1574
+ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1575
+ const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1576
+ const byID = /* @__PURE__ */ new Map();
1577
+ for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1578
+ const metrics2 = emptyMetrics();
1579
+ for (const e of frame2.elements) {
1580
+ if (e.kind === "connector" && !e.connector) continue;
1581
+ metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
1582
+ }
1583
+ const list = flows ?? [];
1584
+ const room = /* @__PURE__ */ new Map();
1585
+ for (const e of frame2.elements) {
1586
+ const c = e.capacity ?? null;
1587
+ if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1588
+ }
1589
+ const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1590
+ const route2 = (tree, goal) => {
1591
+ const hops = [], shapes2 = [];
1592
+ let at = goal;
1593
+ for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
1594
+ hops.push([step.via, step.back]);
1595
+ shapes2.push(at);
1596
+ at = step.from;
1597
+ }
1598
+ shapes2.push(at);
1599
+ return { hops: hops.reverse(), shapes: shapes2.reverse() };
1600
+ };
1601
+ const way = (hops, shapes2) => {
1602
+ const points = [], inside = [], stretches = [];
1603
+ let length = 0;
1604
+ hops.forEach(([id, back], k) => {
1605
+ const c = byID.get(id);
1606
+ if (!c) return;
1607
+ const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
1608
+ if (line2.length === 0) return;
1609
+ const last = points[points.length - 1];
1610
+ if (last) {
1611
+ const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1612
+ const shape = byID.get(shapes2[k]);
1613
+ inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1614
+ length += gap;
1615
+ }
1616
+ const start = length;
1617
+ 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);
1618
+ stretches.push([id, start, length]);
1619
+ points.push(...line2);
1620
+ });
1621
+ return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
1622
+ };
1623
+ const along = (w, elapsedAt, speed) => {
1624
+ let elapsed = elapsedAt, d = 0;
1625
+ for (const [a, b, delay] of w.inside) {
1626
+ const reach = (a - d) / speed;
1627
+ if (elapsed <= reach) break;
1628
+ elapsed -= reach;
1629
+ d = a;
1630
+ const stay = (b - a) / speed + delay;
1631
+ if (elapsed < stay) return null;
1632
+ elapsed -= stay;
1633
+ d = b;
1634
+ }
1635
+ d += elapsed * speed;
1636
+ return d >= 0 && d <= w.length ? d : null;
1637
+ };
1638
+ const union2 = (a, b) => /* @__PURE__ */ new Set([...a, ...b]);
1639
+ const bump = (id, f) => {
1640
+ const m = metrics2.elements.get(id);
1641
+ if (m) f(m);
1642
+ };
1643
+ const leaving = /* @__PURE__ */ new Map(), turn = [];
1644
+ for (const flow of list) {
1645
+ const n = leaving.get(flow.from) ?? 0;
1646
+ turn.push(n);
1647
+ leaving.set(flow.from, n + 1);
1648
+ }
1649
+ const biggest = list.length > 0 ? Math.max(...list.map(amount)) : 1;
1650
+ const packets = [];
1651
+ const waitingBy = /* @__PURE__ */ new Map();
1652
+ 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);
1653
+ for (const [i, flow] of list.entries()) {
1654
+ if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1655
+ const source = byID.get(flow.from);
1656
+ const base2 = source ? flowTree(flow.from, frame2, offline) : null;
1657
+ if (!source || !base2) continue;
1658
+ const volume = amountIn(flow, values);
1659
+ const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1660
+ const placements = [];
1661
+ const waiting = [];
1662
+ const goals = base2.goals(flow.to, flow.spread === true);
1663
+ const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1664
+ for (const [candidates, share] of shares) {
1665
+ if (!(share > TINY)) continue;
1666
+ let left = share;
1667
+ for (let n = 0; n < 32 && left > TINY; n++) {
1668
+ const tree = flowTree(flow.from, frame2, union2(offline, full()));
1669
+ const goal2 = tree?.goals(candidates, false)[0];
1670
+ if (!tree || goal2 === void 0) break;
1671
+ const r = route2(tree, goal2);
1672
+ const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1673
+ const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1674
+ const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
1675
+ if (!(fits > TINY)) break;
1676
+ for (const id of ids) if (room.has(id)) room.set(id, room.get(id) - fits);
1677
+ placements.push({ hops: r.hops, shapes: r.shapes, goal: goal2, amount: fits });
1678
+ left -= fits;
1679
+ }
1680
+ if (!(left > TINY)) continue;
1681
+ let at = flow.from;
1682
+ const goal = base2.goals(candidates, false)[0];
1683
+ if (goal !== void 0) {
1684
+ const r = route2(base2, goal), done = full();
1685
+ const sequence = [[r.shapes[0], r.shapes[0]]];
1686
+ r.hops.forEach(([id], k) => {
1687
+ sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
1688
+ });
1689
+ const first = sequence.find(([id]) => done.has(id));
1690
+ if (first) {
1691
+ at = first[1];
1692
+ const upTo = r.shapes.indexOf(at);
1693
+ for (let q = upTo; q >= 0; q--) {
1694
+ const s = byID.get(r.shapes[q]);
1695
+ if (s && s.queue === true && s.kind !== "connector") {
1696
+ at = r.shapes[q];
1697
+ break;
1698
+ }
1699
+ }
1700
+ }
1701
+ const n = r.shapes.indexOf(at);
1702
+ if (at !== flow.from && n >= 0) {
1703
+ const shapes2 = r.shapes.slice(0, n + 1), hops = r.hops.slice(0, n);
1704
+ for (const id of [...shapes2, ...hops.map(([h]) => h)]) if (room.has(id)) room.set(id, Math.max(0, room.get(id) - left));
1705
+ placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1706
+ }
1707
+ }
1708
+ const existing = waiting.find(([s]) => s === at);
1709
+ if (existing) existing[1] += left;
1710
+ else waiting.push([at, left]);
1711
+ }
1712
+ waitingBy.set(flow.id, waiting);
1713
+ for (const p of placements) {
1714
+ for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1715
+ m.throughput += p.amount;
1716
+ });
1717
+ if (!p.waits) fm.placed += p.amount;
1718
+ }
1719
+ for (const [at, a] of waiting) {
1720
+ bump(at, (m) => {
1721
+ m.backlog += a;
1722
+ });
1723
+ fm.backlog += a;
1724
+ }
1725
+ const reachedFirst = placements.filter((p) => !p.waits && p.goal === flow.to[0]).reduce((s, p) => s + p.amount, 0);
1726
+ fm.backup = volume > TINY ? Math.max(0, (volume - reachedFirst) / volume) : 0;
1727
+ const every = everyOf(flow);
1728
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1729
+ const phase = turn[i] / leaving.get(flow.from) * every;
1730
+ const color2 = flow.color ?? DEFAULT_COLOR;
1731
+ const cumulative = [];
1732
+ let sum = 0;
1733
+ for (const p of placements) {
1734
+ sum += p.amount / Math.max(volume, TINY);
1735
+ cumulative.push(sum);
1736
+ }
1737
+ if (cumulative.length > 0 && Math.abs(cumulative[cumulative.length - 1] - 1) < 1e-9) cumulative[cumulative.length - 1] = 1;
1738
+ let transitSum = 0;
1739
+ placements.forEach((p, r) => {
1740
+ const w = way(p.hops, p.shapes);
1741
+ if (!w) return;
1742
+ const travel = w.length / speed + w.inside.reduce((s, x) => s + x[2], 0);
1743
+ if (!p.waits) transitSum += travel * p.amount;
1744
+ const lo = Math.ceil((t - phase - travel) / every);
1745
+ const hi = Math.floor((t - phase) / every);
1746
+ const below = r === 0 ? 0 : cumulative[r - 1], upTo = cumulative[r];
1747
+ const dealt = (c, k) => Math.floor((k + 1) * c) - Math.floor(k * c);
1748
+ for (let k = Math.max(lo, hi - 3999); k <= hi; k++) {
1749
+ if (dealt(upTo, k) - dealt(below, k) !== 1) continue;
1750
+ const d = along(w, t - k * every - phase, speed);
1751
+ if (d === null) continue;
1752
+ const s = w.stretches.find(([, a, b]) => d >= a && d <= b);
1753
+ if (s) bump(s[0], (m) => {
1754
+ m.inFlight += 1;
1755
+ });
1756
+ const pt2 = w.line.point(d / w.length);
1757
+ packets.push(packet(packetID(flow.id, k), pt2.x - size / 2, pt2.y - size / 2, size, color2, source.layerID));
1758
+ }
1759
+ });
1760
+ fm.transit = fm.placed > TINY ? transitSum / fm.placed : null;
1761
+ metrics2.flows.set(flow.id, fm);
1762
+ }
1763
+ const piles = /* @__PURE__ */ new Map();
1764
+ for (const flow of list) {
1765
+ const waiting = waitingBy.get(flow.id) ?? [];
1766
+ const shapes2 = waiting.map(([s]) => s);
1767
+ for (const e of frame2.elements) {
1768
+ const c = carried[queueKey(e.id, flow.id)];
1769
+ if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1770
+ }
1771
+ if (shapes2.length === 0) continue;
1772
+ const volume = amountIn(flow, values);
1773
+ const every = everyOf(flow);
1774
+ const unit = every * Math.max(volume, TINY);
1775
+ const speed = clamp(flow.speed, DEFAULT_SPEED, 1, 1e4), size = clamp(flow.size, DEFAULT_SIZE, 2, 200);
1776
+ const color2 = flow.color ?? DEFAULT_COLOR;
1777
+ shapes2.forEach((at, j) => {
1778
+ const shape = byID.get(at);
1779
+ if (!shape) return;
1780
+ const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1781
+ const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
1782
+ const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1783
+ let depth = before + backlog * since;
1784
+ if (backlog <= TINY && before > TINY) {
1785
+ const trial = new Map(room);
1786
+ const outlets = [];
1787
+ let total = 0;
1788
+ for (let n2 = 0; n2 < 32; n2++) {
1789
+ const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1790
+ const tree = flowTree(at, frame2, union2(offline, blocked));
1791
+ const goal = tree?.goals(flow.to, false)[0];
1792
+ if (!tree || goal === void 0) break;
1793
+ const r = route2(tree, goal);
1794
+ const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1795
+ const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1796
+ const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
1797
+ const rate = Number.isFinite(spare) ? spare : Math.max(before / 0.5 - total, TINY * 2);
1798
+ if (!(rate > TINY)) break;
1799
+ for (const id of ids) if (trial.has(id)) trial.set(id, trial.get(id) - rate);
1800
+ outlets.push({ hops: r.hops, shapes: r.shapes, rate });
1801
+ total += rate;
1802
+ if (!Number.isFinite(spare)) break;
1803
+ }
1804
+ if (total > TINY) {
1805
+ depth = Math.max(0, before - total * since);
1806
+ if (depth > TINY) {
1807
+ for (const [id, v] of trial) room.set(id, v);
1808
+ for (const o of outlets) for (const id of [...o.shapes, ...o.hops.map(([h]) => h)]) bump(id, (m) => {
1809
+ m.throughput += o.rate;
1810
+ });
1811
+ }
1812
+ const lasts = before / total;
1813
+ outlets.forEach((o, i) => {
1814
+ const w = way(o.hops, o.shapes);
1815
+ if (!w) return;
1816
+ const spacing = Math.max(unit / o.rate, 0.05);
1817
+ for (let k = 0; k * spacing < lasts && k * spacing <= since && k < 2048; k++) {
1818
+ const d = along(w, since - k * spacing, speed);
1819
+ if (d === null) continue;
1820
+ const pt2 = w.line.point(d / w.length);
1821
+ 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));
1822
+ }
1823
+ });
1824
+ }
1825
+ }
1826
+ if (!(depth > TINY || backlog > TINY)) return;
1827
+ bump(at, (m) => {
1828
+ m.depth += depth;
1829
+ });
1830
+ if (depth > TINY) metrics2.carry[queueKey(at, flow.id)] = depth;
1831
+ const n = Math.min(Math.max(Math.ceil(depth / unit - 1e-9), 1), 30);
1832
+ const rows = Math.trunc((n + 9) / 10);
1833
+ const base2 = piles.get(at) ?? 0;
1834
+ piles.set(at, base2 + rows);
1835
+ for (let q = 0; q < n; q++) {
1836
+ const cx = shape.x + 2 + size / 2 + q % 10 * (size + 3);
1837
+ const cy = shape.y - 4 - size / 2 - (base2 + Math.trunc(q / 10)) * (size + 3);
1838
+ packets.push(packet(packetID(flow.id, 2 ** 39 + j * 64 + q), cx - size / 2, cy - size / 2, size, color2, layer));
1839
+ }
1840
+ });
1841
+ }
1842
+ for (const e of frame2.elements) {
1843
+ const c = e.capacity ?? null;
1844
+ const m = metrics2.elements.get(e.id);
1845
+ if (c === null || !Number.isFinite(c) || !m) continue;
1846
+ m.load = c > 0 ? m.throughput / c : 1;
1847
+ }
1848
+ return { packets, metrics: metrics2 };
1849
+ }
1850
+ var BLANK = {
1851
+ seed: 1,
1852
+ text: null,
1853
+ connector: null,
1854
+ groupID: null,
1855
+ points: null,
1856
+ path: null,
1857
+ subpaths: null,
1858
+ evenOdd: null,
1859
+ rotation: null,
1860
+ link: null,
1861
+ url: null,
1862
+ anchor: null,
1863
+ click: null,
1864
+ hover: null,
1865
+ when: null,
1866
+ overrides: null,
1867
+ symbol: null,
1868
+ playback: null,
1869
+ offset: null,
1870
+ args: null,
1871
+ image: null,
1872
+ reveal: null
1873
+ };
1874
+ function style(stroke, fill, strokeWidth, fillStyle = "solid", opacity2 = null) {
1875
+ return {
1876
+ stroke: { ...stroke },
1877
+ fill: { ...fill },
1878
+ fillStyle,
1879
+ strokeWidth,
1880
+ dash: "solid",
1881
+ roughness: 0,
1882
+ bowing: 1,
1883
+ fontSize: 18,
1884
+ font: null,
1885
+ gridSize: null,
1886
+ opacity: opacity2,
1887
+ dashOffset: null,
1888
+ textColor: null
1889
+ };
1890
+ }
1891
+ function packet(id, x, y, size, color2, layerID) {
1892
+ return { ...BLANK, id, kind: "ellipse", layerID, x, y, width: size, height: size, style: style(color2, color2, 1.5) };
1893
+ }
1894
+ function metricLookup(m, frame2) {
1895
+ const anchors = /* @__PURE__ */ new Map();
1896
+ for (const e of frame2.elements) if (e.anchor !== null && !anchors.has(e.anchor)) anchors.set(e.anchor, e.id);
1897
+ 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}$/;
1898
+ return (term, own) => {
1899
+ const id = term.target === "" ? own : anchors.get(term.target) ?? (UUID2.test(term.target) ? term.target.toUpperCase() : null);
1900
+ return id === null ? null : metricValue(m, term.metric, id);
1901
+ };
1902
+ }
1903
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
1904
+ const lookup = metricLookup(metrics2, frame2);
1905
+ const out = [], readings = {};
1906
+ for (let e of elements) {
1907
+ if (values !== null && e.overrides) {
1908
+ for (const o of e.overrides) {
1909
+ const own = e.id;
1910
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1911
+ }
1912
+ }
1913
+ if (!e.gauge || values === null) {
1914
+ out.push(e);
1915
+ continue;
1916
+ }
1917
+ const g = e.gauge;
1918
+ let shown = metricValue(metrics2, g.metric, g.of);
1919
+ const then = carried[gaugeKey(e.id)];
1920
+ if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
1921
+ const f = Math.max(0, since / GAUGE_EASE), smooth = f * f * (3 - 2 * f);
1922
+ shown = then + (shown - then) * smooth;
1923
+ }
1924
+ if (shown !== null) readings[gaugeKey(e.id)] = shown;
1925
+ out.push(...gauge(e, g, shown, frame2, flows));
1926
+ }
1927
+ return { elements: out, readings };
1928
+ }
1929
+ var gaugeKey = (id) => "gauge|" + id;
1930
+ var GAUGE_EASE = 1;
1931
+ var hex = (h) => {
1932
+ const v = parseInt(h.slice(1), 16);
1933
+ return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
1934
+ };
1935
+ var GAUGE_GREEN = hex("#16a34a");
1936
+ var GAUGE_AMBER = hex("#d97706");
1937
+ var GAUGE_RED = hex("#dc2626");
1938
+ var GAUGE_BLUE = hex("#2f6fdf");
1939
+ var GAUGE_TRACK = hex("#e5e7eb");
1940
+ var GAUGE_GRAY = hex("#9ca3af");
1941
+ function formatted(metric, v) {
1942
+ if (v === null || !Number.isFinite(v)) return "\u2013";
1943
+ switch (metric) {
1944
+ case "load":
1945
+ case "backup":
1946
+ return formatNumber(v * 100, 0) + "%";
1947
+ case "transit":
1948
+ case "delay":
1949
+ return formatNumber(v, Math.abs(v) >= 10 ? 1 : 2) + "s";
1950
+ default:
1951
+ return formatNumber(v, Math.abs(v) >= 100 ? 0 : Math.abs(v) >= 10 ? 1 : 2);
1952
+ }
1953
+ }
1954
+ function formatNumber(v, places) {
1955
+ const scale = places === 0 ? 1 : places === 1 ? 10 : 100;
1956
+ const n = Math.floor(Math.abs(v) * scale + 0.5);
1957
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
1958
+ const whole = Math.trunc(n / scale), part = n - whole * scale;
1959
+ let s = String(whole);
1960
+ if (places > 0 && part > 0) {
1961
+ let digits = String(part);
1962
+ while (digits.length < places) digits = "0" + digits;
1963
+ while (digits.endsWith("0")) digits = digits.slice(0, -1);
1964
+ s += "." + digits;
1965
+ }
1966
+ return (v < 0 && n > 0 ? "-" : "") + s;
1967
+ }
1968
+ function gauge(e, g, value2, frame2, flows) {
1969
+ const target = frame2.elements.find((x) => x.id === g.of);
1970
+ const full = g.max ?? (g.metric === "load" ? 1 : target?.capacity ?? value2 ?? 1);
1971
+ const fraction = value2 === null ? 0 : full > 0 ? Math.min(Math.max(value2 / full, 0), 1) : 0;
1972
+ const warn = g.warn ?? (g.metric === "load" ? 0.7 : null), alarm = g.alarm ?? (g.metric === "load" ? 0.9 : null);
1973
+ let color2 = warn === null && alarm === null ? GAUGE_BLUE : GAUGE_GREEN;
1974
+ if (value2 !== null && warn !== null && value2 >= warn) color2 = GAUGE_AMBER;
1975
+ if (value2 !== null && alarm !== null && value2 >= alarm) color2 = GAUGE_RED;
1976
+ if (value2 === null) color2 = GAUGE_GRAY;
1977
+ let name = g.label;
1978
+ if (name === null) {
1979
+ const what = target ? target.text !== null ? target.text.split("\n")[0] : target.anchor : null;
1980
+ name = `${g.metric} ${what ?? (flows.some((f) => f.id === g.of) ? "flow" : "?")}`;
1981
+ }
1982
+ const kind = g.style ?? "number";
1983
+ const w = Math.max(e.width, 1), h = Math.max(e.height, 1);
1984
+ const line2 = (a, b) => [
1985
+ a.x / w,
1986
+ a.y / h,
1987
+ (a.x + (b.x - a.x) / 3) / w,
1988
+ (a.y + (b.y - a.y) / 3) / h,
1989
+ (a.x + 2 * (b.x - a.x) / 3) / w,
1990
+ (a.y + 2 * (b.y - a.y) / 3) / h,
1991
+ b.x / w,
1992
+ b.y / h
1993
+ ];
1994
+ const box = (x, y, bw, bh) => {
1995
+ const p = [{ x, y }, { x: x + bw, y }, { x: x + bw, y: y + bh }, { x, y: y + bh }];
1996
+ let coords = line2(p[0], p[1]);
1997
+ for (let k = 1; k < 4; k++) coords = coords.concat(line2(p[k], p[(k + 1) % 4]).slice(2));
1998
+ return { closed: true, coords };
1999
+ };
2000
+ const arc = (c, radius, a0, a1) => {
2001
+ const pieces2 = Math.max(1, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 2) - 1e-9));
2002
+ const step = (a1 - a0) / pieces2, kappa = 4 / 3 * Math.tan(step / 4);
2003
+ const at = (a) => ({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });
2004
+ const s = at(a0);
2005
+ const coords = [s.x / w, s.y / h];
2006
+ for (let k = 0; k < pieces2; k++) {
2007
+ const a = a0 + k * step, b = a + step;
2008
+ const p0 = at(a), p3 = at(b);
2009
+ const c1 = { x: p0.x - kappa * radius * Math.sin(a), y: p0.y + kappa * radius * Math.cos(a) };
2010
+ const c2 = { x: p3.x + kappa * radius * Math.sin(b), y: p3.y - kappa * radius * Math.cos(b) };
2011
+ coords.push(c1.x / w, c1.y / h, c2.x / w, c2.y / h, p3.x / w, p3.y / h);
2012
+ }
2013
+ return { closed: false, coords };
2014
+ };
2015
+ const part = (k, subpaths, stroke, fill, width) => ({
2016
+ ...BLANK,
2017
+ id: packetID(e.id, -(2 ** 38) - k),
2018
+ kind: "path",
2019
+ layerID: e.layerID,
2020
+ x: e.x,
2021
+ y: e.y,
2022
+ width: e.width,
2023
+ height: e.height,
2024
+ style: style(stroke, fill ?? { r: 0, g: 0, b: 0, a: 0 }, width, "solid", e.style.opacity),
2025
+ subpaths
2026
+ });
2027
+ const back = {
2028
+ ...e,
2029
+ gauge: null,
2030
+ kind: "path",
2031
+ subpaths: [box(0, 0, w, h)],
2032
+ text: name + "\n" + formatted(g.metric, value2),
2033
+ style: { ...e.style }
2034
+ };
2035
+ const parts = [];
2036
+ if (kind === "number") {
2037
+ back.labelRect = { x: 0, y: 0, width: 1, height: 1 };
2038
+ back.style.textColor = { ...color2 };
2039
+ } else if (kind === "bar") {
2040
+ back.labelRect = { x: 0, y: 0, width: 1, height: 0.66 };
2041
+ const inset2 = Math.min(10, w * 0.08), barH = Math.min(12, h * 0.18), top = h * 0.72;
2042
+ const trackW = w - 2 * inset2;
2043
+ parts.push(part(1, [box(inset2, top, trackW, barH)], GAUGE_TRACK, GAUGE_TRACK, 0.5));
2044
+ if (fraction > 0) parts.push(part(2, [box(inset2, top, trackW * fraction, barH)], color2, color2, 0.5));
2045
+ } else {
2046
+ back.labelRect = { x: 0, y: 0.54, width: 1, height: 0.46 };
2047
+ const radius = Math.min(w * 0.38, h * 0.4), center2 = { x: w / 2, y: h * 0.5 };
2048
+ const thick = Math.max(2, radius * 0.16);
2049
+ parts.push(part(1, [arc(center2, radius, Math.PI, 2 * Math.PI)], GAUGE_TRACK, null, thick));
2050
+ if (fraction > 0) parts.push(part(2, [arc(center2, radius, Math.PI, Math.PI + Math.PI * fraction)], color2, null, thick));
2051
+ const a = Math.PI + Math.PI * fraction;
2052
+ const tip = { x: center2.x + radius * 0.85 * Math.cos(a), y: center2.y + radius * 0.85 * Math.sin(a) };
2053
+ parts.push(part(3, [{ closed: false, coords: line2(center2, tip) }], { r: 0.2, g: 0.2, b: 0.2, a: 1 }, null, 2));
2054
+ }
2055
+ return [back, ...parts];
2056
+ }
2057
+
1213
2058
  // src/rough.ts
1214
- var offset = (rng, amount, roughness) => rng.signed() * amount * roughness;
2059
+ var offset = (rng, amount2, roughness) => rng.signed() * amount2 * roughness;
1215
2060
  function line(a, b, rng, roughness, bowing, preserveEnds, into) {
1216
2061
  const length = Math.hypot(b.x - a.x, b.y - a.y);
1217
2062
  if (!(length > 0.5)) return;
@@ -2067,17 +2912,43 @@ function trackMap(tl) {
2067
2912
  for (const t of tl.tracks) if (!m.has(t.element)) m.set(t.element, t);
2068
2913
  return m;
2069
2914
  }
2070
- function resolved(page, t) {
2071
- return expandingSymbols(posed(page, t), clampTime(t));
2915
+ function resolved(page, t, state = "initial", since = null, carried = {}) {
2916
+ return expandingSymbols(posed(page, t, state, since, carried), clampTime(t));
2072
2917
  }
2073
- function posed(page, t) {
2074
- const timeline = page.timeline;
2075
- if (!timeline) return page;
2918
+ var EMPTY_TIMELINE = { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
2919
+ function posed(page, t, state = "initial", since = null, carried = {}) {
2920
+ return posedWithMetrics(page, t, state, since, carried).frame;
2921
+ }
2922
+ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}) {
2923
+ const off = stateHidden(page, state);
2924
+ const values = stateValues(page, state);
2925
+ const active = /* @__PURE__ */ new Map();
2926
+ for (const e of page.elements) {
2927
+ const o = activeOverrides(e, values);
2928
+ if (o.length > 0) active.set(e.id, o);
2929
+ }
2930
+ const flowing = (page.flows ?? []).length > 0;
2931
+ const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
2932
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
2933
+ if (!timeline) return { frame: page, metrics: emptyMetrics() };
2076
2934
  t = clampTime(t);
2077
2935
  const tracks = trackMap(timeline);
2936
+ for (const [id, overrides] of active) {
2937
+ const track = tracks.get(id);
2938
+ if (!track?.follow || !overrides.some((o) => o.reroute)) continue;
2939
+ tracks.set(id, {
2940
+ ...track,
2941
+ follow: track.follow.map((f) => {
2942
+ let path = f.path;
2943
+ for (const o of overrides) path = o.reroute?.[path] ?? path;
2944
+ return { ...f, path };
2945
+ })
2946
+ });
2947
+ }
2078
2948
  const reseed = timeline.boil === null ? null : Math.trunc(Math.floor(t * clampFPS(timeline.fps) + 1e-9) / Math.max(1, timeline.boil)) % BOIL_DRAWINGS >>> 0;
2079
2949
  const hidden = /* @__PURE__ */ new Set();
2080
2950
  for (const tr of tracks.values()) if (!isVisible(tr, t)) hidden.add(tr.element);
2951
+ for (const id of off) hidden.add(id);
2081
2952
  let moving = null;
2082
2953
  if (timeline.settle === true) {
2083
2954
  moving = /* @__PURE__ */ new Set();
@@ -2093,6 +2964,14 @@ function posed(page, t) {
2093
2964
  let elements = page.elements.map((e) => {
2094
2965
  const track = tracks.get(e.id);
2095
2966
  let out = track ? applyTrack(track, e, t) : e;
2967
+ for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
2968
+ const list = active.get(e.id);
2969
+ if (list) {
2970
+ const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
2971
+ const delays = list.map((o) => o.delay).filter((d) => d !== null && Number.isFinite(d));
2972
+ if (caps.length > 0) out.capacity = Math.max(Math.min(...caps), 0);
2973
+ if (delays.length > 0 && out.kind !== "connector") out.delay = Math.max(Math.max(...delays), 0);
2974
+ }
2096
2975
  if (reseed !== null && reseed > 0 && boils(e)) {
2097
2976
  if (out === e) out = cloneElement(e);
2098
2977
  out.seed = boiled(e.seed, reseed);
@@ -2116,7 +2995,7 @@ function posed(page, t) {
2116
2995
  elements = elements.map((e) => {
2117
2996
  const c = e.connector;
2118
2997
  if (!c || ![c.start.elementID, c.end.elementID].some((id) => id !== null && hidden.has(id))) return e;
2119
- if (!(tracks.get(e.id)?.keys.some((k) => k.visible !== void 0) ?? false)) {
2998
+ 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))) {
2120
2999
  gone.add(e.id);
2121
3000
  return e;
2122
3001
  }
@@ -2126,13 +3005,24 @@ function posed(page, t) {
2126
3005
  pinned.connector.end = { elementID: null, port: c.end.port, x: b.x, y: b.y };
2127
3006
  return pinned;
2128
3007
  });
2129
- return { ...page, elements: elements.filter((e) => !gone.has(e.id)), timeline: null };
3008
+ elements = elements.filter((e) => !gone.has(e.id));
3009
+ let metrics2 = emptyMetrics();
3010
+ if (measuring) {
3011
+ const offline = new Set([...active].filter(([, list]) => list.some((o) => o.offline === true)).map(([id]) => id));
3012
+ const frame2 = { ...page, elements, timeline: null };
3013
+ const out = flowFrame(page.flows, frame2, t, values, offline, since, carried);
3014
+ metrics2 = out.metrics;
3015
+ const finished = finishing(elements, out.metrics, values, frame2, page.flows ?? [], Math.max(0, Math.min(since ?? t, 1e6)), carried);
3016
+ Object.assign(metrics2.carry, finished.readings);
3017
+ elements = [...finished.elements, ...out.packets];
3018
+ }
3019
+ return { frame: { ...page, elements, timeline: null, flows: measuring ? null : page.flows }, metrics: metrics2 };
2130
3020
  }
2131
3021
  function posterTime(page) {
2132
3022
  return page.timeline ? page.timeline.poster ?? page.timeline.duration : 0;
2133
3023
  }
2134
3024
  function still(page) {
2135
- return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(page, 0);
3025
+ return page.timeline ? resolved(page, posterTime(page)) : expandingSymbols(posed(page, 0), 0);
2136
3026
  }
2137
3027
  function cameraRect(tl, t) {
2138
3028
  const keys = tl.camera ?? [];
@@ -2241,7 +3131,7 @@ function filled(s, args) {
2241
3131
  }) };
2242
3132
  }
2243
3133
  function symbolPage(s) {
2244
- return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null };
3134
+ return { id: s.id, name: s.name, layers: s.layers, elements: s.elements, theme: null, timeline: s.timeline, symbols: null, variables: null, flows: null };
2245
3135
  }
2246
3136
  function hasInstances(page) {
2247
3137
  return page.elements.some((e) => e.kind === "symbol");
@@ -2348,8 +3238,8 @@ function mix2(x) {
2348
3238
  return z ^ z >> 31n;
2349
3239
  }
2350
3240
  function halves(id) {
2351
- const hex = id.replace(/-/g, "").padStart(32, "0").slice(-32);
2352
- return [BigInt(`0x${hex.slice(0, 16)}`), BigInt(`0x${hex.slice(16)}`)];
3241
+ const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3242
+ return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
2353
3243
  }
2354
3244
  function derivedID(instance, inner) {
2355
3245
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -2370,16 +3260,21 @@ function clickTargets(page, file) {
2370
3260
  let pageOf = null;
2371
3261
  for (const e of page.elements) {
2372
3262
  const c = e.click;
3263
+ const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
2373
3264
  if (c?.scene) {
2374
3265
  const i = sceneNamed(c.scene, markers);
2375
3266
  if (i >= 0) {
2376
- out.set(e.id, { scene: i });
3267
+ out.set(e.id, { scene: i, ...state });
2377
3268
  continue;
2378
3269
  }
2379
3270
  }
2380
3271
  const url = (c?.url ? allowedURL(c.url) : null) ?? webLink(e.url);
2381
3272
  if (url) {
2382
- out.set(e.id, { url });
3273
+ out.set(e.id, { url, ...state });
3274
+ continue;
3275
+ }
3276
+ if (state) {
3277
+ out.set(e.id, { state: true, ...state });
2383
3278
  continue;
2384
3279
  }
2385
3280
  if (e.link && file && file.pages.length > 1) {
@@ -2398,14 +3293,14 @@ function clickTargets(page, file) {
2398
3293
  function hoveredElement(e) {
2399
3294
  const h = e.hover;
2400
3295
  if (!h) return e;
2401
- const style = { ...e.style };
2402
- if (h.stroke) style.stroke = h.stroke;
2403
- if (h.fill) style.fill = h.fill;
3296
+ const style2 = { ...e.style };
3297
+ if (h.stroke) style2.stroke = h.stroke;
3298
+ if (h.fill) style2.fill = h.fill;
2404
3299
  if (h.opacity !== null && Number.isFinite(h.opacity)) {
2405
3300
  const o = Math.min(Math.max(h.opacity, 0), 1);
2406
- style.opacity = o >= 1 ? null : o;
3301
+ style2.opacity = o >= 1 ? null : o;
2407
3302
  }
2408
- return { ...e, style };
3303
+ return { ...e, style: style2 };
2409
3304
  }
2410
3305
  function hovered(page, ids) {
2411
3306
  return { ...page, elements: page.elements.map((e) => ids.has(e.id) ? hoveredElement(e) : e) };
@@ -2609,14 +3504,14 @@ function parseList(lines, start) {
2609
3504
  }
2610
3505
  return { block: { kind: "list", ordered, items }, next: i };
2611
3506
  }
2612
- function inline(src, style = {}) {
3507
+ function inline(src, style2 = {}) {
2613
3508
  const out = [];
2614
3509
  const push = (text, s) => {
2615
3510
  if (text) out.push({ ...s, text });
2616
3511
  };
2617
3512
  let i = 0, buf = "";
2618
3513
  const emit = () => {
2619
- push(buf, style);
3514
+ push(buf, style2);
2620
3515
  buf = "";
2621
3516
  };
2622
3517
  while (i < src.length) {
@@ -2644,43 +3539,43 @@ function inline(src, style = {}) {
2644
3539
  emit();
2645
3540
  let code = m[2].replace(/\n/g, " ");
2646
3541
  if (/^ .* $/.test(code)) code = code.slice(1, -1);
2647
- push(code, { ...style, mono: true });
3542
+ push(code, { ...style2, mono: true });
2648
3543
  i += m[0].length;
2649
3544
  continue;
2650
3545
  }
2651
3546
  if (m = /^!\[([^\]]*)\]\([^)]*\)/.exec(rest)) {
2652
3547
  emit();
2653
- out.push(...inline(m[1], style));
3548
+ out.push(...inline(m[1], style2));
2654
3549
  i += m[0].length;
2655
3550
  continue;
2656
3551
  }
2657
3552
  if (m = /^\[([^\]]+)\]\([^)]*\)/.exec(rest)) {
2658
3553
  emit();
2659
- out.push(...inline(m[1], { ...style, link: true }));
3554
+ out.push(...inline(m[1], { ...style2, link: true }));
2660
3555
  i += m[0].length;
2661
3556
  continue;
2662
3557
  }
2663
3558
  if (m = /^<((?:https?|ftp|mailto):[^>\s]+)>/.exec(rest)) {
2664
3559
  emit();
2665
- push(m[1], { ...style, link: true });
3560
+ push(m[1], { ...style2, link: true });
2666
3561
  i += m[0].length;
2667
3562
  continue;
2668
3563
  }
2669
3564
  if (m = /^~~(?=\S)([\s\S]*?\S)~~/.exec(rest)) {
2670
3565
  emit();
2671
- out.push(...inline(m[1], { ...style, strike: true }));
3566
+ out.push(...inline(m[1], { ...style2, strike: true }));
2672
3567
  i += m[0].length;
2673
3568
  continue;
2674
3569
  }
2675
3570
  if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
2676
3571
  emit();
2677
- out.push(...inline(m[2], { ...style, bold: true }));
3572
+ out.push(...inline(m[2], { ...style2, bold: true }));
2678
3573
  i += m[0].length;
2679
3574
  continue;
2680
3575
  }
2681
3576
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
2682
3577
  emit();
2683
- out.push(...inline(m[2], { ...style, italic: true }));
3578
+ out.push(...inline(m[2], { ...style2, italic: true }));
2684
3579
  i += m[0].length;
2685
3580
  continue;
2686
3581
  }
@@ -3293,6 +4188,16 @@ var Playback = class {
3293
4188
  __publicField(this, "plan");
3294
4189
  __publicField(this, "scenes");
3295
4190
  __publicField(this, "sceneNames");
4191
+ /** Whether each scene loops (Marker.loop). */
4192
+ __publicField(this, "sceneLoops");
4193
+ /** The page's variables now: their starting values until clicks change them. */
4194
+ __publicField(this, "state");
4195
+ /** When the variables last changed (performance.now() milliseconds): backlog piles up from then (ADR 0003). */
4196
+ __publicField(this, "stateSince", typeof performance === "undefined" ? 0 : performance.now());
4197
+ /** A still (the `at` attribute): backlog is drawn for the page time, not the wall clock. */
4198
+ __publicField(this, "still", false);
4199
+ /** What each queue held when the variables last changed, which it drains from (FrameMetrics.carry). */
4200
+ __publicField(this, "carried", {});
3296
4201
  __publicField(this, "background");
3297
4202
  __publicField(this, "ink");
3298
4203
  /** What clicking each clickable element does, by element id. */
@@ -3352,6 +4257,8 @@ var Playback = class {
3352
4257
  };
3353
4258
  this.scenes = (page.timeline?.markers ?? []).map((m) => near(m.t));
3354
4259
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4260
+ this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4261
+ this.state = { ...page.variables ?? {} };
3355
4262
  const theme = themeNamed(page.theme);
3356
4263
  this.background = theme?.background ?? WHITE;
3357
4264
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
@@ -3362,6 +4269,10 @@ var Playback = class {
3362
4269
  this.svg.style.maxWidth = `${Math.ceil(this.plan.size.width)}px`;
3363
4270
  into.replaceChildren(this.svg);
3364
4271
  }
4272
+ /** Seconds since the variables last changed, or null for a still. */
4273
+ sinceChange(now = typeof performance === "undefined" ? 0 : performance.now()) {
4274
+ return this.still || typeof performance === "undefined" ? null : (now - this.stateSince) / 1e3;
4275
+ }
3365
4276
  get frameCount() {
3366
4277
  return this.plan.times.length;
3367
4278
  }
@@ -3402,7 +4313,7 @@ var Playback = class {
3402
4313
  const times = /* @__PURE__ */ new Map();
3403
4314
  for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
3404
4315
  const tc = clampTime(t);
3405
- const pose = posed(this.page, tc);
4316
+ const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
3406
4317
  const frame2 = expandingSymbols(pose, tc, true, times);
3407
4318
  const { parts } = svgParts(frame2, this.background, r);
3408
4319
  const owned = (m, parts2) => {
@@ -3524,6 +4435,20 @@ ${h}</g>
3524
4435
  });
3525
4436
  return s;
3526
4437
  }
4438
+ /** The frame after scene `s`'s last: the next scene's, or the frame count after the last scene. */
4439
+ sceneEnd(s) {
4440
+ return this.scenes.find((f, j) => j > s && f > this.scenes[s]) ?? this.frameCount;
4441
+ }
4442
+ /** Where playing goes from frame `i`, and whether it stops there: on into a looping scene, round to the start of
4443
+ * the looping scene `i` ends, or a stop at a scene or the end (`loops`: whether the timeline loops at its end). */
4444
+ step(i, loops) {
4445
+ const next = i + 1;
4446
+ const s = this.sceneAt(i);
4447
+ if (s >= 0 && this.sceneLoops[s] && next >= this.sceneEnd(s)) return { frame: this.scenes[s], stop: false };
4448
+ if (next >= this.frameCount) return { frame: 0, stop: !loops || this.scenes.length > 0 };
4449
+ const entered = this.scenes.indexOf(next);
4450
+ return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4451
+ }
3527
4452
  };
3528
4453
  var ElementBase = typeof HTMLElement === "undefined" ? class {
3529
4454
  } : HTMLElement;
@@ -3645,6 +4570,7 @@ var DottiePlayer = class extends ElementBase {
3645
4570
  const file = this.file;
3646
4571
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
3647
4572
  const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
4573
+ p.still = this.hasAttribute("at");
3648
4574
  this.playback = p;
3649
4575
  this.style.setProperty("--dottie-page", color(p.background));
3650
4576
  this.style.setProperty("--dottie-ink", color(p.ink));
@@ -3677,7 +4603,7 @@ var DottiePlayer = class extends ElementBase {
3677
4603
  this.setPlaying(false);
3678
4604
  return;
3679
4605
  }
3680
- this.setPlaying(!this.hasAttribute("no-autoplay") && !p.scenes.includes(0));
4606
+ this.setPlaying(!this.hasAttribute("no-autoplay") && (!p.scenes.includes(0) || p.sceneLoops[p.scenes.indexOf(0)]));
3681
4607
  }
3682
4608
  // MARK: Playback
3683
4609
  get paused() {
@@ -3703,11 +4629,13 @@ var DottiePlayer = class extends ElementBase {
3703
4629
  pause() {
3704
4630
  this.setPlaying(false);
3705
4631
  }
3706
- /** Plays on to the next scene (from the start after the last). */
4632
+ /** Plays on to the next scene (from the start after the last). Out of a looping scene it jumps to the next. */
3707
4633
  nextScene() {
3708
4634
  const p = this.playback;
3709
4635
  if (!p) return;
3710
- if (p.frame >= p.frameCount - 1) p.show(0);
4636
+ const s = p.sceneAt(p.frame);
4637
+ if (s >= 0 && p.sceneLoops[s]) p.show(s + 1 < p.scenes.length ? p.scenes[s + 1] : 0);
4638
+ else if (p.frame >= p.frameCount - 1) p.show(0);
3711
4639
  this.setPlaying(true);
3712
4640
  }
3713
4641
  /** Back to the start of this scene, or the one before if already there. */
@@ -3736,6 +4664,14 @@ var DottiePlayer = class extends ElementBase {
3736
4664
  this.scheduleReplay();
3737
4665
  }
3738
4666
  if (!click) return;
4667
+ if (click.set || click.toggle || click.reset) {
4668
+ this.setState(applyClick(
4669
+ { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
4670
+ p.state,
4671
+ p.page.variables ?? {}
4672
+ ));
4673
+ }
4674
+ if ("state" in click) return;
3739
4675
  if ("url" in click) {
3740
4676
  if (/^(https?:|mailto:)/i.test(click.url)) window.open(click.url, "_blank", "noopener");
3741
4677
  } else if ("page" in click) {
@@ -3743,13 +4679,30 @@ var DottiePlayer = class extends ElementBase {
3743
4679
  } else if (click.scene >= 0 && click.scene < p.scenes.length) {
3744
4680
  if (reduced || this.hasAttribute("at") || p.frameCount < 2) {
3745
4681
  this.setPlaying(false);
3746
- p.show(click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
4682
+ p.show(p.sceneLoops[click.scene] ? p.scenes[click.scene] : click.scene + 1 < p.scenes.length ? p.scenes[click.scene + 1] : p.frameCount - 1);
3747
4683
  } else {
3748
4684
  p.show(p.scenes[click.scene]);
3749
4685
  this.setPlaying(true);
3750
4686
  }
3751
4687
  }
3752
4688
  }
4689
+ /** The page's variables now. */
4690
+ get variables() {
4691
+ return { ...this.playback?.state ?? {} };
4692
+ }
4693
+ /** Gives the page's variables these values (the others keep theirs), redraws, and fires `state`. */
4694
+ setState(values) {
4695
+ const p = this.playback;
4696
+ if (!p) return;
4697
+ const next = { ...p.state, ...values };
4698
+ if (Object.keys(next).every((k) => next[k] === p.state[k])) return;
4699
+ const t = clampTime(p.last.t);
4700
+ p.carried = posedWithMetrics(p.page, t, p.state, p.sinceChange(), p.carried).metrics.carry;
4701
+ p.state = next;
4702
+ p.stateSince = performance.now();
4703
+ p.redraw();
4704
+ this.dispatchEvent(new CustomEvent("state", { detail: { variables: { ...next } } }));
4705
+ }
3753
4706
  /** Redraws, while the page itself stands still, as long as a clicked copy is playing through. */
3754
4707
  scheduleReplay() {
3755
4708
  if (this.replayRaf) return;
@@ -3775,7 +4728,8 @@ var DottiePlayer = class extends ElementBase {
3775
4728
  schedule() {
3776
4729
  if (this.playing && this.visible && !this.raf && this.isConnected) this.raf = requestAnimationFrame((now) => this.tick(now));
3777
4730
  }
3778
- /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end. */
4731
+ /** Steps whole frames at the timeline's rate; with scenes, playing stops at each one and at the end, except that a
4732
+ * looping scene plays round (Playback.step). */
3779
4733
  tick(now) {
3780
4734
  this.raf = 0;
3781
4735
  const p = this.playback;
@@ -3786,14 +4740,14 @@ var DottiePlayer = class extends ElementBase {
3786
4740
  let advanced = false;
3787
4741
  while (this.playing && this.acc >= step) {
3788
4742
  this.acc -= step;
3789
- const next = p.frame + 1;
3790
- if (next >= p.frameCount && (p.scenes.length || this.hasAttribute("no-loop"))) {
4743
+ const { frame: frame2, stop } = p.step(p.frame, !this.hasAttribute("no-loop"));
4744
+ if (stop && frame2 === 0 && p.frame + 1 >= p.frameCount) {
3791
4745
  this.setPlaying(false);
3792
4746
  break;
3793
4747
  }
3794
- p.frame = next % p.frameCount;
4748
+ p.frame = frame2;
3795
4749
  advanced = true;
3796
- if (p.scenes.includes(p.frame)) {
4750
+ if (stop) {
3797
4751
  this.setPlaying(false);
3798
4752
  break;
3799
4753
  }
@@ -3943,7 +4897,7 @@ function render(source, options = {}) {
3943
4897
  function isFile(v) {
3944
4898
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
3945
4899
  }
3946
- var VERSION = "0.6.3";
4900
+ var VERSION = "0.8.0";
3947
4901
  export {
3948
4902
  BOIL_DRAWINGS,
3949
4903
  BUNDLED_FONTS,