@phin-tech/dottie 0.9.0 → 0.11.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,84 +1,334 @@
1
- /*! dottie.js 0.9.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.11.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
6
+ // src/expression.ts
7
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
8
+ var FUNCTIONS = { min: null, max: null, abs: 1, floor: 1, ceil: 1, round: 1, clamp: 3 };
7
9
  var MAX_NAME = 64;
8
- function isName(name) {
9
- return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
10
+ var truthy = (v) => typeof v === "boolean" ? v : v !== 0 && !Number.isNaN(v);
11
+ var num = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
12
+ var parsed = /* @__PURE__ */ new Map();
13
+ function parseExpr(text) {
14
+ const hit = parsed.get(text);
15
+ if (hit !== void 0) return hit;
16
+ const p = new Parser(text);
17
+ let e = p.or();
18
+ if (e && !p.atEnd()) e = null;
19
+ if (parsed.size > 4096) parsed.clear();
20
+ parsed.set(text, e);
21
+ return e;
10
22
  }
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;
23
+ function evaluate(e, state, metric = null) {
24
+ switch (e.k) {
25
+ case "lit":
26
+ return e.v;
27
+ case "name":
28
+ return state[e.name] ?? false;
29
+ case "metric":
30
+ return metric?.(e.metric, e.target) ?? NaN;
31
+ case "not":
32
+ return !truthy(evaluate(e.e, state, metric));
33
+ case "neg":
34
+ return -num(evaluate(e.e, state, metric));
35
+ case "call": {
36
+ const xs = e.args.map((a) => num(evaluate(a, state, metric)));
37
+ const fold = (better) => {
38
+ if (xs.some(Number.isNaN)) return NaN;
39
+ let out = xs[0];
40
+ for (const x of xs.slice(1)) if (better(x, out)) out = x;
41
+ return out;
42
+ };
43
+ switch (e.fn) {
44
+ case "min":
45
+ return fold((a, b) => a < b);
46
+ case "max":
47
+ return fold((a, b) => a > b);
48
+ case "abs":
49
+ return Math.abs(xs[0]);
50
+ case "floor":
51
+ return Math.floor(xs[0]);
52
+ case "ceil":
53
+ return Math.ceil(xs[0]);
54
+ case "round":
55
+ return Math.floor(xs[0] + 0.5);
56
+ default:
57
+ if (xs.some(Number.isNaN)) return NaN;
58
+ return xs[0] < xs[1] ? xs[1] : xs[0] > xs[2] ? xs[2] : xs[0];
59
+ }
60
+ }
61
+ case "bin": {
62
+ if (e.op === "&&") return truthy(evaluate(e.l, state, metric)) && truthy(evaluate(e.r, state, metric));
63
+ if (e.op === "||") return truthy(evaluate(e.l, state, metric)) || truthy(evaluate(e.r, state, metric));
64
+ const a = evaluate(e.l, state, metric), b = evaluate(e.r, state, metric);
65
+ const x = num(a), y = num(b);
66
+ switch (e.op) {
67
+ case "+":
68
+ return x + y;
69
+ case "-":
70
+ return x - y;
71
+ case "*":
72
+ return x * y;
73
+ case "/":
74
+ return x / y;
75
+ case "%":
76
+ return x % y;
77
+ }
78
+ if (Number.isNaN(x) || Number.isNaN(y)) return false;
79
+ switch (e.op) {
80
+ case "<":
81
+ return x < y;
82
+ case "<=":
83
+ return x <= y;
84
+ case ">":
85
+ return x > y;
86
+ case ">=":
87
+ return x >= y;
88
+ case "==":
89
+ return typeof a === "boolean" && typeof b === "boolean" ? a === b : x === y;
90
+ default:
91
+ return typeof a === "boolean" && typeof b === "boolean" ? a !== b : x !== y;
92
+ }
93
+ }
48
94
  }
49
95
  }
50
- function mentionsMetric(condition) {
51
- return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
96
+ function exprNames(e) {
97
+ const out = [];
98
+ const walk = (x) => {
99
+ switch (x.k) {
100
+ case "name":
101
+ if (!out.includes(x.name)) out.push(x.name);
102
+ break;
103
+ case "not":
104
+ case "neg":
105
+ walk(x.e);
106
+ break;
107
+ case "bin":
108
+ walk(x.l);
109
+ walk(x.r);
110
+ break;
111
+ case "call":
112
+ x.args.forEach(walk);
113
+ break;
114
+ }
115
+ };
116
+ walk(e);
117
+ return out;
52
118
  }
53
- function conditionNames(condition) {
119
+ function exprMetrics(e) {
54
120
  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);
121
+ const walk = (x) => {
122
+ switch (x.k) {
123
+ case "metric":
124
+ out.push({ metric: x.metric, target: x.target });
125
+ break;
126
+ case "not":
127
+ case "neg":
128
+ walk(x.e);
129
+ break;
130
+ case "bin":
131
+ walk(x.l);
132
+ walk(x.r);
133
+ break;
134
+ case "call":
135
+ x.args.forEach(walk);
136
+ break;
62
137
  }
63
- }
138
+ };
139
+ walk(e);
64
140
  return out;
65
141
  }
142
+ var isDigit = (c) => c !== void 0 && c >= "0" && c <= "9";
143
+ var isLetter = (c) => c !== void 0 && (c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z");
144
+ var Parser = class {
145
+ constructor(text) {
146
+ __publicField(this, "s");
147
+ __publicField(this, "i", 0);
148
+ this.s = [...text];
149
+ }
150
+ atEnd() {
151
+ this.skip();
152
+ return this.i === this.s.length;
153
+ }
154
+ skip() {
155
+ while (this.i < this.s.length && " \n\r".includes(this.s[this.i])) this.i++;
156
+ }
157
+ eat(token) {
158
+ this.skip();
159
+ const t = [...token];
160
+ if (this.i + t.length > this.s.length) return false;
161
+ for (let k = 0; k < t.length; k++) if (this.s[this.i + k] !== t[k]) return false;
162
+ if (t.length === 1 && this.s[this.i + 1] === "=" && "<>!=".includes(t[0])) return false;
163
+ this.i += t.length;
164
+ return true;
165
+ }
166
+ or() {
167
+ let l = this.and();
168
+ if (!l) return null;
169
+ while (this.eat("||")) {
170
+ const r = this.and();
171
+ if (!r) return null;
172
+ l = { k: "bin", op: "||", l, r };
173
+ }
174
+ return l;
175
+ }
176
+ and() {
177
+ let l = this.compare();
178
+ if (!l) return null;
179
+ while (this.eat("&&")) {
180
+ const r = this.compare();
181
+ if (!r) return null;
182
+ l = { k: "bin", op: "&&", l, r };
183
+ }
184
+ return l;
185
+ }
186
+ compare() {
187
+ const l = this.sum();
188
+ if (!l) return null;
189
+ for (const op of ["<=", ">=", "==", "!=", "<", ">"]) {
190
+ if (this.eat(op)) {
191
+ const r = this.sum();
192
+ return r ? { k: "bin", op, l, r } : null;
193
+ }
194
+ }
195
+ return l;
196
+ }
197
+ sum() {
198
+ let l = this.product();
199
+ if (!l) return null;
200
+ for (; ; ) {
201
+ const op = this.eat("+") ? "+" : this.eat("-") ? "-" : null;
202
+ if (!op) return l;
203
+ const r = this.product();
204
+ if (!r) return null;
205
+ l = { k: "bin", op, l, r };
206
+ }
207
+ }
208
+ product() {
209
+ let l = this.unary();
210
+ if (!l) return null;
211
+ for (; ; ) {
212
+ const op = ["*", "/", "%"].find((o) => this.eat(o));
213
+ if (!op) return l;
214
+ const r = this.unary();
215
+ if (!r) return null;
216
+ l = { k: "bin", op, l, r };
217
+ }
218
+ }
219
+ unary() {
220
+ if (this.eat("!")) {
221
+ const e = this.unary();
222
+ return e ? { k: "not", e } : null;
223
+ }
224
+ if (this.eat("-")) {
225
+ const e = this.unary();
226
+ return e ? { k: "neg", e } : null;
227
+ }
228
+ return this.primary();
229
+ }
230
+ primary() {
231
+ this.skip();
232
+ if (this.i >= this.s.length) return null;
233
+ if (this.eat("(")) {
234
+ const e = this.or();
235
+ return e && this.eat(")") ? e : null;
236
+ }
237
+ const s = this.s;
238
+ const c = s[this.i];
239
+ if (isDigit(c)) {
240
+ const start3 = this.i;
241
+ while (isDigit(s[this.i])) this.i++;
242
+ if (s[this.i] === "." && isDigit(s[this.i + 1])) {
243
+ this.i++;
244
+ while (isDigit(s[this.i])) this.i++;
245
+ }
246
+ return { k: "lit", v: Number(s.slice(start3, this.i).join("")) };
247
+ }
248
+ if (!isLetter(c)) return null;
249
+ const start2 = this.i;
250
+ while (isLetter(s[this.i]) || isDigit(s[this.i])) this.i++;
251
+ const name = s.slice(start2, this.i).join("");
252
+ if (name.length > MAX_NAME) return null;
253
+ let j = this.i;
254
+ while (s[j] === " " || s[j] === " ") j++;
255
+ const called = s[j] === "(";
256
+ if (METRIC_NAMES.includes(name)) {
257
+ if (!called) return { k: "metric", metric: name, target: "" };
258
+ this.eat("(");
259
+ const from = this.i;
260
+ while (this.i < s.length && s[this.i] !== ")" && s[this.i] !== "(") this.i++;
261
+ if (s[this.i] !== ")") return null;
262
+ const target = s.slice(from, this.i).join("").replace(/^[ \t]+|[ \t]+$/g, "");
263
+ this.i++;
264
+ return { k: "metric", metric: name, target };
265
+ }
266
+ if (name in FUNCTIONS) {
267
+ if (!called || !this.eat("(")) return null;
268
+ const args = [];
269
+ if (!this.eat(")")) {
270
+ do {
271
+ const a = this.or();
272
+ if (!a) return null;
273
+ args.push(a);
274
+ } while (this.eat(","));
275
+ if (!this.eat(")")) return null;
276
+ }
277
+ const arity = FUNCTIONS[name];
278
+ if (arity !== null ? args.length !== arity : args.length === 0) return null;
279
+ return { k: "call", fn: name, args };
280
+ }
281
+ if (called) return null;
282
+ if (name === "true") return { k: "lit", v: true };
283
+ if (name === "false") return { k: "lit", v: false };
284
+ return { k: "name", name };
285
+ }
286
+ };
287
+ function describeVar(v) {
288
+ return typeof v === "boolean" ? v ? "true" : "false" : formatTwo(v);
289
+ }
290
+ function formatTwo(v) {
291
+ const n = Math.floor(Math.abs(v) * 100 + 0.5);
292
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
293
+ const whole = Math.floor(n / 100), part = n - whole * 100;
294
+ let s = String(whole);
295
+ if (part > 0) s += "." + String(part).padStart(2, "0").replace(/0+$/, "");
296
+ return (v < 0 && n > 0 ? "-" : "") + s;
297
+ }
298
+ function filling(text, values) {
299
+ if (!text.includes("{")) return text;
300
+ let out = "", rest = text;
301
+ for (let open = rest.indexOf("{"); open >= 0; open = rest.indexOf("{")) {
302
+ out += rest.slice(0, open);
303
+ const close = rest.indexOf("}", open);
304
+ if (close < 0) {
305
+ out += rest.slice(open);
306
+ rest = "";
307
+ break;
308
+ }
309
+ const inner = rest.slice(open + 1, close);
310
+ const e = inner.includes("{") ? null : parseExpr(inner);
311
+ const names = e ? exprNames(e) : [];
312
+ if (e && exprMetrics(e).length === 0 && names.length > 0 && names.every((n) => values[n] !== void 0)) out += describeVar(evaluate(e, values));
313
+ else out += rest.slice(open, close + 1);
314
+ rest = rest.slice(close + 1);
315
+ }
316
+ return out + rest;
317
+ }
318
+
319
+ // src/state.ts
320
+ var MAX_NAME2 = 64;
321
+ function isName(name) {
322
+ return name.length <= MAX_NAME2 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
323
+ }
324
+ function mentionsMetric(condition) {
325
+ const e = parseExpr(condition);
326
+ return e !== null && exprMetrics(e).length > 0;
327
+ }
66
328
  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
- }));
329
+ const e = parseExpr(condition);
330
+ if (!e) return false;
331
+ return truthy(evaluate(e, state, metric ? (m, target) => metric({ metric: m, target, op: "", value: 0 }) : null));
82
332
  }
83
333
  function stateValues(page, choice) {
84
334
  if (choice === "all") return null;
@@ -117,7 +367,7 @@ function applyOverride(o, e) {
117
367
  return out;
118
368
  }
119
369
  function changesState(c) {
120
- return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
370
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0 || !!c.run);
121
371
  }
122
372
  function applyClick(c, state, initial = {}) {
123
373
  let out = { ...state };
@@ -126,27 +376,91 @@ function applyClick(c, state, initial = {}) {
126
376
  for (const k of Object.keys(state)) out[k] = false;
127
377
  Object.assign(out, initial);
128
378
  }
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);
379
+ const before = { ...out };
380
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = clickValue(v, before, before[k]);
381
+ for (const k of c.toggle ?? []) out[k] = !truthy(out[k] ?? false);
131
382
  return out;
132
383
  }
133
- function trim(s) {
134
- return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
384
+ function clickValue(v, state, current) {
385
+ if (typeof v !== "string") return v;
386
+ const e = parseExpr(v);
387
+ return e ? evaluate(e, state) : current ?? false;
388
+ }
389
+
390
+ // src/theme.ts
391
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
392
+ function hex(h) {
393
+ const n = parseInt(h.slice(1), 16);
394
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
395
+ }
396
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
397
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
398
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
399
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
400
+ var themes = [
401
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
402
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
403
+ {
404
+ id: "sepia",
405
+ name: "Sepia",
406
+ background: rgb(0.953, 0.914, 0.824),
407
+ ink: rgb(0.23, 0.18, 0.13),
408
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
409
+ },
410
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
411
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
412
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
413
+ ];
414
+ function themeNamed(id) {
415
+ return themes.find((t) => t.id === id) ?? null;
416
+ }
417
+ function pageTheme(page) {
418
+ const c = page.customTheme;
419
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
420
+ return themeNamed(page.theme);
421
+ }
422
+ function paletteColor(page, name) {
423
+ const n = name.trim().toLowerCase();
424
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
425
+ }
426
+ function resolvingPalette(page) {
427
+ const named = (e) => !!e.style.named;
428
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
429
+ const resolve2 = (e) => {
430
+ const n = e.style.named;
431
+ if (!n) return e;
432
+ const style2 = { ...e.style };
433
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
434
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
435
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
436
+ if (f) style2.fill = { ...f };
437
+ if (s) style2.stroke = { ...s };
438
+ if (t) style2.textColor = { ...t };
439
+ return { ...e, style: style2 };
440
+ };
441
+ return {
442
+ ...page,
443
+ elements: page.elements.map(resolve2),
444
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve2) })) : page.symbols
445
+ };
135
446
  }
136
447
 
137
448
  // src/model.ts
138
- var SUPPORTED_VERSION = 2;
449
+ var SUPPORTED_VERSION = 3;
139
450
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
140
451
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
141
452
  var KNOWN_FIELDS = {
142
453
  file: ["version", "pages", "images"],
143
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
454
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
144
455
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
145
456
  flowOverride: ["when", "volume"],
146
457
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
147
458
  layer: ["id", "name", "visible", "locked"],
148
459
  rgba: ["r", "g", "b", "a"],
149
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
460
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
461
+ colorNames: ["fill", "stroke", "textColor"],
462
+ customTheme: ["name", "background", "ink", "palette", "base"],
463
+ paletteColor: ["name", "color"],
150
464
  element: [
151
465
  "id",
152
466
  "kind",
@@ -184,8 +498,11 @@ var KNOWN_FIELDS = {
184
498
  "capacity",
185
499
  "delay",
186
500
  "queue",
187
- "gauge"
501
+ "gauge",
502
+ "containerID",
503
+ "container"
188
504
  ],
505
+ container: ["fixed", "lanes"],
189
506
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
190
507
  strokePoint: ["x", "y"],
191
508
  subpath: ["closed", "coords"],
@@ -194,7 +511,7 @@ var KNOWN_FIELDS = {
194
511
  extensionRef: ["id", "shape", "version", "params"],
195
512
  normalizedPoint: ["x", "y"],
196
513
  normalizedRect: ["x", "y", "width", "height"],
197
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
514
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
198
515
  waypoint: ["x", "y"],
199
516
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
200
517
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -203,7 +520,7 @@ var KNOWN_FIELDS = {
203
520
  repeat: ["start", "end", "count"],
204
521
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
205
522
  marker: ["t", "name", "loop"],
206
- click: ["scene", "url", "set", "toggle", "reset"],
523
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
207
524
  hover: ["stroke", "fill", "opacity"],
208
525
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
209
526
  };
@@ -217,7 +534,7 @@ var inkOnDark = { r: 0.93, g: 0.93, b: 0.94, a: 1 };
217
534
  var DottieFormatError = class extends Error {
218
535
  };
219
536
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
220
- var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
537
+ var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
221
538
  var str = (v) => typeof v === "string" ? v : void 0;
222
539
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
223
540
  var bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -241,9 +558,9 @@ function webLink(url) {
241
558
  }
242
559
  function rgba(v) {
243
560
  if (!isObj(v)) return void 0;
244
- const r = num(v.r), g = num(v.g), b = num(v.b);
561
+ const r = num2(v.r), g = num2(v.g), b = num2(v.b);
245
562
  if (r === void 0 || g === void 0 || b === void 0) return void 0;
246
- return { r, g, b, a: num(v.a) ?? 1 };
563
+ return { r, g, b, a: num2(v.a) ?? 1 };
247
564
  }
248
565
  var Reader = class {
249
566
  constructor() {
@@ -272,28 +589,55 @@ var Reader = class {
272
589
  stroke: rgba(s.stroke) ?? { ...black },
273
590
  fill: rgba(s.fill) ?? { ...clear },
274
591
  fillStyle: ["solid", "hachure", "crossHatch", "grid"].includes(fillStyle) ? fillStyle : "solid",
275
- strokeWidth: num(s.strokeWidth) ?? 2,
592
+ strokeWidth: num2(s.strokeWidth) ?? 2,
276
593
  dash: ["solid", "dashed", "dotted"].includes(dash) ? dash : "solid",
277
- roughness: num(s.roughness) ?? 1,
278
- bowing: num(s.bowing) ?? 1,
279
- fontSize: num(s.fontSize) ?? 18,
594
+ roughness: num2(s.roughness) ?? 1,
595
+ bowing: num2(s.bowing) ?? 1,
596
+ fontSize: num2(s.fontSize) ?? 18,
280
597
  font: str(s.font) || null,
281
- gridSize: num(s.gridSize) ?? null,
282
- opacity: num(s.opacity) ?? null,
283
- dashOffset: num(s.dashOffset) ?? null,
284
- textColor: rgba(s.textColor) ?? null
598
+ gridSize: num2(s.gridSize) ?? null,
599
+ opacity: num2(s.opacity) ?? null,
600
+ dashOffset: num2(s.dashOffset) ?? null,
601
+ textColor: rgba(s.textColor) ?? null,
602
+ named: this.colorNames(s.named)
285
603
  };
286
604
  }
605
+ container(v) {
606
+ if (!isObj(v)) return null;
607
+ this.fields(v, "container");
608
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
609
+ }
610
+ colorNames(v) {
611
+ if (!isObj(v)) return null;
612
+ this.fields(v, "colorNames");
613
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
614
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
615
+ }
616
+ customTheme(v) {
617
+ if (!isObj(v)) return null;
618
+ this.fields(v, "customTheme");
619
+ const background = rgba(v.background), ink = rgba(v.ink);
620
+ if (!background || !ink) {
621
+ this.warn("A custom theme needs a background and an ink; drawn without it");
622
+ return null;
623
+ }
624
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
625
+ this.fields(p, "paletteColor");
626
+ const name = str(p.name), color2 = rgba(p.color);
627
+ return name && color2 ? [{ name, color: color2 }] : [];
628
+ });
629
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
630
+ }
287
631
  endpoint(v) {
288
632
  const e = isObj(v) ? v : {};
289
633
  const port = str(e.port);
290
- const portIndex = num(e.portIndex);
634
+ const portIndex = num2(e.portIndex);
291
635
  return {
292
636
  elementID: uuid(e.elementID) ?? null,
293
637
  port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
294
638
  portIndex: portIndex !== void 0 && Number.isInteger(portIndex) && portIndex >= 0 ? portIndex : null,
295
- x: num(e.x) ?? 0,
296
- y: num(e.y) ?? 0
639
+ x: num2(e.x) ?? 0,
640
+ y: num2(e.y) ?? 0
297
641
  };
298
642
  }
299
643
  connector(v) {
@@ -312,9 +656,10 @@ var Reader = class {
312
656
  endHead: head(v.endHead, "arrow"),
313
657
  waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
314
658
  this.fields(w, "waypoint");
315
- return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
659
+ return { x: num2(w.x) ?? 0, y: num2(w.y) ?? 0 };
316
660
  }) : null,
317
661
  hops: typeof v.hops === "boolean" ? v.hops : null,
662
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
318
663
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
319
664
  };
320
665
  }
@@ -347,20 +692,20 @@ var Reader = class {
347
692
  id,
348
693
  kind,
349
694
  layerID,
350
- x: num(v.x) ?? 0,
351
- y: num(v.y) ?? 0,
352
- width: num(v.width) ?? 0,
353
- height: num(v.height) ?? 0,
695
+ x: num2(v.x) ?? 0,
696
+ y: num2(v.y) ?? 0,
697
+ width: num2(v.width) ?? 0,
698
+ height: num2(v.height) ?? 0,
354
699
  style: this.style(v.style),
355
- seed: (num(v.seed) ?? 1) >>> 0,
700
+ seed: (num2(v.seed) ?? 1) >>> 0,
356
701
  text: str(v.text) ?? null,
357
702
  connector: this.connector(v.connector),
358
703
  groupID: uuid(v.groupID) ?? null,
359
- points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num(p.x) ?? 0, y: num(p.y) ?? 0 })) : null,
704
+ points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num2(p.x) ?? 0, y: num2(p.y) ?? 0 })) : null,
360
705
  path: Array.isArray(v.path) ? v.path.filter((c) => typeof c === "number") : null,
361
706
  subpaths,
362
707
  evenOdd: bool(v.evenOdd) ?? null,
363
- rotation: num(v.rotation) ?? null,
708
+ rotation: num2(v.rotation) ?? null,
364
709
  link: uuid(v.link) ?? null,
365
710
  url: str(v.url) ?? null,
366
711
  anchor: str(v.anchor) ?? null,
@@ -370,20 +715,22 @@ var Reader = class {
370
715
  overrides: this.overrides(v.overrides),
371
716
  symbol: uuid(v.symbol) ?? null,
372
717
  playback: this.playback(v.playback),
373
- offset: num(v.offset) ?? null,
718
+ offset: num2(v.offset) ?? null,
374
719
  args: strings(v.args),
375
720
  image: this.image(v.image, images),
376
- reveal: num(v.reveal) ?? null,
721
+ reveal: num2(v.reveal) ?? null,
377
722
  ports: Array.isArray(v.ports) ? v.ports.map((p) => {
378
723
  const o = isObj(p) ? p : {};
379
- return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
724
+ return { x: num2(o.x) ?? 0, y: num2(o.y) ?? 0 };
380
725
  }) : null,
381
726
  labelRect: this.labelRect(v.labelRect),
382
727
  extension: this.extension(v.extension),
383
- capacity: num(v.capacity) ?? null,
384
- delay: num(v.delay) ?? null,
728
+ capacity: num2(v.capacity) ?? null,
729
+ delay: num2(v.delay) ?? null,
385
730
  queue: v.queue === true ? true : null,
386
- gauge: this.gauge(v.gauge)
731
+ gauge: this.gauge(v.gauge),
732
+ containerID: uuid(v.containerID) ?? null,
733
+ container: this.container(v.container)
387
734
  };
388
735
  }
389
736
  gauge(v) {
@@ -404,7 +751,7 @@ var Reader = class {
404
751
  labelRect(v) {
405
752
  if (!isObj(v)) return null;
406
753
  this.fields(v, "normalizedRect");
407
- return { x: num(v.x) ?? 0, y: num(v.y) ?? 0, width: num(v.width) ?? 0, height: num(v.height) ?? 0 };
754
+ return { x: num2(v.x) ?? 0, y: num2(v.y) ?? 0, width: num2(v.width) ?? 0, height: num2(v.height) ?? 0 };
408
755
  }
409
756
  extension(v) {
410
757
  if (!isObj(v)) return null;
@@ -425,7 +772,7 @@ var Reader = class {
425
772
  variables(v) {
426
773
  if (!isObj(v)) return null;
427
774
  const out = {};
428
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
775
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
429
776
  return Object.keys(out).length > 0 ? out : null;
430
777
  }
431
778
  click(v) {
@@ -437,12 +784,22 @@ var Reader = class {
437
784
  let set = null;
438
785
  if (isObj(v.set)) {
439
786
  set = {};
440
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
787
+ for (const [k, b] of Object.entries(v.set)) {
788
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
789
+ }
441
790
  if (Object.keys(set).length === 0) set = null;
442
791
  }
443
792
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
444
793
  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 };
794
+ const run = str(v.run);
795
+ return {
796
+ scene: str(v.scene) ?? null,
797
+ url: safe,
798
+ set,
799
+ toggle: toggle && toggle.length > 0 ? toggle : null,
800
+ reset: v.reset === true ? true : null,
801
+ run: run !== void 0 && isName(run) ? run : null
802
+ };
446
803
  }
447
804
  playback(v) {
448
805
  const s = str(v);
@@ -458,7 +815,7 @@ var Reader = class {
458
815
  return null;
459
816
  }
460
817
  const size = (x) => {
461
- const n = num(x);
818
+ const n = num2(x);
462
819
  return Math.max(1, n ?? 100);
463
820
  };
464
821
  const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
@@ -495,8 +852,8 @@ var Reader = class {
495
852
  stroke: rgba(o.stroke) ?? null,
496
853
  fill: rgba(o.fill) ?? null,
497
854
  textColor: rgba(o.textColor) ?? null,
498
- opacity: num(o.opacity) ?? null,
499
- strokeWidth: num(o.strokeWidth) ?? null,
855
+ opacity: num2(o.opacity) ?? null,
856
+ strokeWidth: num2(o.strokeWidth) ?? null,
500
857
  dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
501
858
  text: str(o.text) ?? null,
502
859
  reroute,
@@ -510,7 +867,7 @@ var Reader = class {
510
867
  hover(v) {
511
868
  if (!isObj(v)) return null;
512
869
  this.fields(v, "hover");
513
- return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num(v.opacity) ?? null };
870
+ return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num2(v.opacity) ?? null };
514
871
  }
515
872
  ease(v) {
516
873
  const s = str(v);
@@ -524,11 +881,11 @@ var Reader = class {
524
881
  keyframe(v) {
525
882
  if (!isObj(v)) return null;
526
883
  this.fields(v, "keyframe");
527
- const t = num(v.t);
884
+ const t = num2(v.t);
528
885
  if (t === void 0) return null;
529
886
  const k = { t };
530
887
  for (const p of ["x", "y", "width", "height", "rotation", "strokeWidth", "reveal", "opacity"]) {
531
- const n = num(v[p]);
888
+ const n = num2(v[p]);
532
889
  if (n !== void 0) k[p] = n;
533
890
  }
534
891
  const stroke = rgba(v.stroke), fill = rgba(v.fill), visible = bool(v.visible), ease = this.ease(v.ease);
@@ -546,39 +903,39 @@ var Reader = class {
546
903
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
547
904
  stableSortBy(keys, (k) => k.t);
548
905
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
549
- const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
906
+ const path = uuid(f.path), start2 = num2(f.start), end = num2(f.end);
550
907
  if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
551
- return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
908
+ return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num2(f.laps) ?? null }];
552
909
  }) : null;
553
910
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
554
- const start2 = num(r.start), end = num(r.end);
911
+ const start2 = num2(r.start), end = num2(r.end);
555
912
  if (start2 === void 0 || end === void 0) return [];
556
- const count = num(r.count);
913
+ const count = num2(r.count);
557
914
  const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
558
915
  return repeatIsValid(rep) ? [rep] : [];
559
916
  }) : null;
560
- return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
917
+ return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num2(v.march)) };
561
918
  }
562
919
  timeline(v) {
563
920
  if (!isObj(v)) return null;
564
921
  this.fields(v, "timeline");
565
- const boil = num(v.boil);
922
+ const boil = num2(v.boil);
566
923
  const camera = Array.isArray(v.camera) ? v.camera.filter(isObj).flatMap((c) => {
567
- const t = num(c.t), x = num(c.x), y = num(c.y), width = num(c.width), height = num(c.height);
924
+ const t = num2(c.t), x = num2(c.x), y = num2(c.y), width = num2(c.width), height = num2(c.height);
568
925
  if (t === void 0 || x === void 0 || y === void 0 || width === void 0 || height === void 0) return [];
569
926
  if (!(width > 1 && height > 1 && t >= 0)) return [];
570
927
  return [{ t, x, y, width, height, ease: this.ease(c.ease) }];
571
928
  }) : null;
572
929
  if (camera) stableSortBy(camera, (c) => c.t);
573
930
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
574
- const t = num(m.t);
931
+ const t = num2(m.t);
575
932
  return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
576
933
  }) : null;
577
934
  if (markers) stableSortBy(markers, (m) => m.t);
578
- const poster = num(v.poster);
935
+ const poster = num2(v.poster);
579
936
  return {
580
- fps: clampFPS(num(v.fps) ?? 12),
581
- duration: clampTime(num(v.duration) ?? 0),
937
+ fps: clampFPS(num2(v.fps) ?? 12),
938
+ duration: clampTime(num2(v.duration) ?? 0),
582
939
  boil: boil !== void 0 && Number.isInteger(boil) && boil > 0 ? Math.min(boil, 1e4) : null,
583
940
  settle: bool(v.settle) ?? null,
584
941
  poster: poster === void 0 ? null : clampTime(poster),
@@ -604,6 +961,8 @@ var Reader = class {
604
961
  layers,
605
962
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
606
963
  theme: str(v.theme) ?? null,
964
+ customTheme: this.customTheme(v.customTheme),
965
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
607
966
  timeline: this.timeline(v.timeline),
608
967
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
609
968
  variables: this.variables(v.variables),
@@ -615,7 +974,7 @@ var Reader = class {
615
974
  this.fields(f, "flow");
616
975
  const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
617
976
  const finite = (x) => {
618
- const n = num(x);
977
+ const n = num2(x);
619
978
  return n !== void 0 && Number.isFinite(n) ? n : null;
620
979
  };
621
980
  return {
@@ -655,7 +1014,7 @@ function parseDottie(source) {
655
1014
  }
656
1015
  }
657
1016
  if (!isObj(json)) throw new DottieFormatError("Not a Dottie drawing");
658
- const version = num(json.version);
1017
+ const version = num2(json.version);
659
1018
  if (version === void 0) throw new DottieFormatError("Not a Dottie drawing (no version)");
660
1019
  if (!Array.isArray(json.pages) || json.pages.length === 0) throw new DottieFormatError("A drawing needs at least one page");
661
1020
  const reader = new Reader();
@@ -667,7 +1026,7 @@ function parseDottie(source) {
667
1026
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
668
1027
  }
669
1028
  }
670
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
1029
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
671
1030
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
672
1031
  return { version, pages, warnings: reader.warnings };
673
1032
  }
@@ -687,7 +1046,8 @@ function cloneElement(e) {
687
1046
  ...e.style,
688
1047
  stroke: { ...e.style.stroke },
689
1048
  fill: { ...e.style.fill },
690
- textColor: e.style.textColor ? { ...e.style.textColor } : null
1049
+ textColor: e.style.textColor ? { ...e.style.textColor } : null,
1050
+ named: e.style.named ? { ...e.style.named } : e.style.named
691
1051
  },
692
1052
  connector: e.connector ? {
693
1053
  ...e.connector,
@@ -1177,6 +1537,165 @@ function shapePath(e) {
1177
1537
  return path;
1178
1538
  }
1179
1539
 
1540
+ // src/elbow.ts
1541
+ var MARGIN = 16;
1542
+ var BEND = 24;
1543
+ var SMALLEST = 24;
1544
+ var MOST = 48;
1545
+ function obstacle(e) {
1546
+ if (e.kind === "connector" || e.kind === "text" || e.kind === "freehand") return null;
1547
+ if (e.kind === "path" && !e.extension) return null;
1548
+ if (e.style.opacity !== null && e.style.opacity <= 0) return null;
1549
+ const r = rotatedBounds(e);
1550
+ return r.width < SMALLEST || r.height < SMALLEST ? null : r;
1551
+ }
1552
+ function crosses(points, r) {
1553
+ const x1 = r.x + r.width, y1 = r.y + r.height;
1554
+ for (let k = 1; k < points.length; k++) {
1555
+ const p = points[k - 1], q = points[k];
1556
+ if (p.y === q.y) {
1557
+ if (r.y < p.y && p.y < y1 && Math.min(p.x, q.x) < x1 && Math.max(p.x, q.x) > r.x) return true;
1558
+ } else if (p.x === q.x) {
1559
+ if (r.x < p.x && p.x < x1 && Math.min(p.y, q.y) < y1 && Math.max(p.y, q.y) > r.y) return true;
1560
+ }
1561
+ }
1562
+ return false;
1563
+ }
1564
+ var padded = (r) => ({ x: r.x - MARGIN, y: r.y - MARGIN, width: r.width + 2 * MARGIN, height: r.height + 2 * MARGIN });
1565
+ var inside = (p, r) => r.x < p.x && p.x < r.x + r.width && r.y < p.y && p.y < r.y + r.height;
1566
+ var memo = /* @__PURE__ */ new Map();
1567
+ function avoiding(plain, a, b, startVertical, endVertical, elements, except) {
1568
+ const usable = (r) => !inside(a, padded(r)) && !inside(b, padded(r));
1569
+ const all = [];
1570
+ let crossed = false;
1571
+ for (const e of elements) {
1572
+ if (except.has(e.id)) continue;
1573
+ const r = obstacle(e);
1574
+ if (!r || !usable(r)) continue;
1575
+ all.push(r);
1576
+ if (!crossed && crosses(plain, r)) crossed = true;
1577
+ }
1578
+ if (!crossed) return plain;
1579
+ const key = JSON.stringify([a.x, a.y, b.x, b.y, startVertical, endVertical, all.map((r) => [r.x, r.y, r.width, r.height])]);
1580
+ const hit = memo.get(key);
1581
+ if (hit) return hit;
1582
+ const out = route(plain, a, b, startVertical, endVertical, all);
1583
+ if (memo.size >= 512) memo.clear();
1584
+ memo.set(key, out);
1585
+ return out;
1586
+ }
1587
+ function route(plain, a, b, startVertical, endVertical, usable) {
1588
+ let chosen = usable.map((_, i) => i).filter((i) => crosses(plain, usable[i]));
1589
+ if (chosen.length === 0) return plain;
1590
+ for (let round = 0; round < 4; round++) {
1591
+ if (chosen.length > MOST) return plain;
1592
+ const path = grid(a, b, startVertical, endVertical, chosen.map((i) => padded(usable[i])));
1593
+ if (!path) return plain;
1594
+ const more = usable.map((_, i) => i).filter((i) => !chosen.includes(i) && crosses(path, usable[i]));
1595
+ if (more.length === 0) return path;
1596
+ chosen = [...chosen, ...more].sort((p, q) => p - q);
1597
+ }
1598
+ return plain;
1599
+ }
1600
+ function grid(a, b, startVertical, endVertical, boxes) {
1601
+ let xs = [a.x, b.x, (a.x + b.x) / 2], ys = [a.y, b.y, (a.y + b.y) / 2];
1602
+ for (const r of boxes) {
1603
+ xs.push(r.x, r.x + r.width);
1604
+ ys.push(r.y, r.y + r.height);
1605
+ }
1606
+ xs = [...new Set(xs)].sort((p, q) => p - q);
1607
+ ys = [...new Set(ys)].sort((p, q) => p - q);
1608
+ const nx = xs.length, ny = ys.length;
1609
+ const ia = xs.indexOf(a.x), ja = ys.indexOf(a.y), ib = xs.indexOf(b.x), jb = ys.indexOf(b.y);
1610
+ if (ia < 0 || ja < 0 || ib < 0 || jb < 0) return null;
1611
+ const at = new Map(xs.map((x, k) => [x, k])), bt = new Map(ys.map((y, k) => [y, k]));
1612
+ const inner = new Uint8Array(nx * ny), acrossBlocked = new Uint8Array(nx * ny), downBlocked = new Uint8Array(nx * ny);
1613
+ for (const r of boxes) {
1614
+ const i0 = at.get(r.x), i1 = at.get(r.x + r.width), j0 = bt.get(r.y), j1 = bt.get(r.y + r.height);
1615
+ if (i0 === void 0 || i1 === void 0 || j0 === void 0 || j1 === void 0) continue;
1616
+ for (let i = i0; i < i1; i++) for (let j = j0; j < j1; j++) {
1617
+ if (j > j0) acrossBlocked[i * ny + j] = 1;
1618
+ if (i > i0) downBlocked[i * ny + j] = 1;
1619
+ if (i > i0 && j > j0) inner[i * ny + j] = 1;
1620
+ }
1621
+ }
1622
+ const blocked = (i, j) => inner[i * ny + j] === 1;
1623
+ const across = (i0, i1, j) => acrossBlocked[Math.min(i0, i1) * ny + j] === 1;
1624
+ const down = (j0, j1, i) => downBlocked[i * ny + Math.min(j0, j1)] === 1;
1625
+ const count = nx * ny * 2;
1626
+ const dist = new Array(count).fill(Infinity), prev = new Array(count).fill(-1);
1627
+ const done = new Array(count).fill(false);
1628
+ const heap = new Frontier();
1629
+ const first = (ia * ny + ja) * 2 + (startVertical ? 1 : 0);
1630
+ dist[first] = 0;
1631
+ heap.push(0, first);
1632
+ const want = (ib * ny + jb) * 2 + (endVertical ? 1 : 0);
1633
+ for (let top = heap.pop(); top; top = heap.pop()) {
1634
+ const [cost, s] = top;
1635
+ if (done[s]) continue;
1636
+ done[s] = true;
1637
+ if (s === want) break;
1638
+ const node = Math.floor(s / 2), axis = s % 2, i = Math.floor(node / ny), j = node % ny;
1639
+ for (const [di, dj] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
1640
+ const ni = i + di, nj = j + dj;
1641
+ if (ni < 0 || ni >= nx || nj < 0 || nj >= ny || blocked(ni, nj)) continue;
1642
+ const turn = di !== 0 ? 0 : 1;
1643
+ if (di !== 0 ? across(i, ni, j) : down(j, nj, i)) continue;
1644
+ const length = Math.abs(xs[ni] - xs[i]) + Math.abs(ys[nj] - ys[j]);
1645
+ const c = cost + length + (turn !== axis ? BEND : 0);
1646
+ const t = (ni * ny + nj) * 2 + turn;
1647
+ if (c < dist[t]) {
1648
+ dist[t] = c;
1649
+ prev[t] = s;
1650
+ heap.push(c, t);
1651
+ }
1652
+ }
1653
+ }
1654
+ if (!done[want]) return null;
1655
+ const points = [];
1656
+ for (let s = want; s >= 0; s = prev[s]) {
1657
+ const node = Math.floor(s / 2);
1658
+ points.push({ x: xs[Math.floor(node / ny)], y: ys[node % ny] });
1659
+ }
1660
+ return points.reverse();
1661
+ }
1662
+ var Frontier = class {
1663
+ constructor() {
1664
+ __publicField(this, "heap", []);
1665
+ __publicField(this, "next", 0);
1666
+ }
1667
+ less(i, j) {
1668
+ const a = this.heap[i], b = this.heap[j];
1669
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
1670
+ }
1671
+ push(cost, state) {
1672
+ this.heap.push([cost, this.next++, state]);
1673
+ for (let i = this.heap.length - 1; i > 0; ) {
1674
+ const up = i - 1 >> 1;
1675
+ if (!this.less(i, up)) break;
1676
+ [this.heap[i], this.heap[up]] = [this.heap[up], this.heap[i]];
1677
+ i = up;
1678
+ }
1679
+ }
1680
+ pop() {
1681
+ if (this.heap.length === 0) return null;
1682
+ const top = this.heap[0], last = this.heap.pop();
1683
+ if (this.heap.length > 0) {
1684
+ this.heap[0] = last;
1685
+ for (let i = 0; ; ) {
1686
+ const l = 2 * i + 1, r = l + 1;
1687
+ let m = i;
1688
+ if (l < this.heap.length && this.less(l, m)) m = l;
1689
+ if (r < this.heap.length && this.less(r, m)) m = r;
1690
+ if (m === i) break;
1691
+ [this.heap[i], this.heap[m]] = [this.heap[m], this.heap[i]];
1692
+ i = m;
1693
+ }
1694
+ }
1695
+ return [top[0], top[2]];
1696
+ }
1697
+ };
1698
+
1180
1699
  // src/connector.ts
1181
1700
  function portPoint(shape, port) {
1182
1701
  const r = frame(shape);
@@ -1320,7 +1839,7 @@ function routeThrough(p, c) {
1320
1839
  }
1321
1840
  }
1322
1841
  }
1323
- function route(e, index) {
1842
+ function route2(e, index) {
1324
1843
  const c = e.connector;
1325
1844
  if (!c) return { kind: "polyline", points: [] };
1326
1845
  const [a, b] = endpoints(e, index);
@@ -1336,16 +1855,19 @@ function route(e, index) {
1336
1855
  case "elbow": {
1337
1856
  const startV = vertical(c.start.port) ?? !horizontal;
1338
1857
  const endV = vertical(c.end.port) ?? !horizontal;
1858
+ let plain;
1339
1859
  if (startV && endV) {
1340
1860
  const my = a.y + dy / 2;
1341
- return { kind: "polyline", points: [a, { x: a.x, y: my }, { x: b.x, y: my }, b] };
1342
- }
1343
- if (!startV && !endV) {
1861
+ plain = [a, { x: a.x, y: my }, { x: b.x, y: my }, b];
1862
+ } else if (!startV && !endV) {
1344
1863
  const mx = a.x + dx / 2;
1345
- return { kind: "polyline", points: [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b] };
1346
- }
1347
- if (startV) return { kind: "polyline", points: [a, { x: a.x, y: b.y }, b] };
1348
- return { kind: "polyline", points: [a, { x: b.x, y: a.y }, b] };
1864
+ plain = [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b];
1865
+ } else if (startV) plain = [a, { x: a.x, y: b.y }, b];
1866
+ else plain = [a, { x: b.x, y: a.y }, b];
1867
+ if (c.avoid !== true) return { kind: "polyline", points: plain };
1868
+ const ends = new Set([c.start.elementID, c.end.elementID].filter((x) => x !== null));
1869
+ const routed = avoiding(plain, a, b, startV, endV, index.values(), ends);
1870
+ return { kind: "polyline", points: routed === plain ? plain : simplified(routed) };
1349
1871
  }
1350
1872
  }
1351
1873
  }
@@ -1356,7 +1878,7 @@ function arrowhead(tip, from, size) {
1356
1878
  return [wing(spread), wing(-spread)];
1357
1879
  }
1358
1880
  function connectorBounds(e, index) {
1359
- const pts = routePoints(route(e, index));
1881
+ const pts = routePoints(route2(e, index));
1360
1882
  if (pts.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
1361
1883
  let x0 = pts[0].x, y0 = pts[0].y, x1 = x0, y1 = y0;
1362
1884
  for (const p of pts) {
@@ -1368,7 +1890,7 @@ function connectorBounds(e, index) {
1368
1890
  return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1369
1891
  }
1370
1892
  function midpoint(e, index) {
1371
- const pts = routePoints(route(e, index));
1893
+ const pts = routePoints(route2(e, index));
1372
1894
  if (pts.length <= 1) return pts[0] ?? { x: 0, y: 0 };
1373
1895
  const lengths = [];
1374
1896
  let total = 0;
@@ -1517,7 +2039,7 @@ function graphOf(frame2) {
1517
2039
  graphs.set(els, { members: [...els], graph });
1518
2040
  return graph;
1519
2041
  }
1520
- var Frontier = class {
2042
+ var Frontier2 = class {
1521
2043
  constructor() {
1522
2044
  __publicField(this, "heap", []);
1523
2045
  }
@@ -1571,7 +2093,7 @@ function grow({ shapes: shapes2, next }, from, offline, until) {
1571
2093
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1572
2094
  let counter = 1;
1573
2095
  const tree = new FlowTree(from);
1574
- const open = new Frontier();
2096
+ const open = new Frontier2();
1575
2097
  open.push(from, 0, 0);
1576
2098
  while (open.size > 0) {
1577
2099
  const { id: n, order: o } = open.pop();
@@ -1677,6 +2199,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1677
2199
  return pts;
1678
2200
  };
1679
2201
  const metrics2 = emptyMetrics();
2202
+ const routes = /* @__PURE__ */ new Map();
1680
2203
  for (const e of frame2.elements) {
1681
2204
  if (e.kind === "connector" && !e.connector) continue;
1682
2205
  metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
@@ -1688,7 +2211,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1688
2211
  if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1689
2212
  }
1690
2213
  const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1691
- const route2 = (tree, goal) => {
2214
+ const route3 = (tree, goal) => {
1692
2215
  const hops = [], shapes2 = [];
1693
2216
  let at = goal;
1694
2217
  for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
@@ -1700,7 +2223,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1700
2223
  return { hops: hops.reverse(), shapes: shapes2.reverse() };
1701
2224
  };
1702
2225
  const way = (hops, shapes2) => {
1703
- const points = [], inside = [], stretches = [];
2226
+ const points = [], inside2 = [], stretches = [];
1704
2227
  let length = 0;
1705
2228
  hops.forEach(([id, back], k) => {
1706
2229
  const c = byID.get(id);
@@ -1711,7 +2234,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1711
2234
  if (last) {
1712
2235
  const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1713
2236
  const shape = byID.get(shapes2[k]);
1714
- inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
2237
+ inside2.push([length, length + gap, shape ? delayOf(shape) : 0]);
1715
2238
  length += gap;
1716
2239
  }
1717
2240
  const start2 = length;
@@ -1719,7 +2242,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1719
2242
  stretches.push([id, start2, length]);
1720
2243
  points.push(...line2);
1721
2244
  });
1722
- return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
2245
+ return length > 0 ? { line: new Polyline(points), length, inside: inside2, stretches } : null;
1723
2246
  };
1724
2247
  const along = (w, elapsedAt, speed) => {
1725
2248
  let elapsed = elapsedAt, d = 0;
@@ -1760,6 +2283,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1760
2283
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1761
2284
  const source = byID.get(flow.from);
1762
2285
  const base2 = source ? baseTree(flow.from) : null;
2286
+ routes.set(flow.id, []);
1763
2287
  if (!source || !base2) continue;
1764
2288
  const volume = amountIn(flow, values);
1765
2289
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
@@ -1774,7 +2298,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1774
2298
  const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1775
2299
  const goal2 = tree?.goals(candidates, false)[0];
1776
2300
  if (!tree || goal2 === void 0) break;
1777
- const r = route2(tree, goal2);
2301
+ const r = route3(tree, goal2);
1778
2302
  const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1779
2303
  const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1780
2304
  const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
@@ -1787,7 +2311,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1787
2311
  let at = flow.from;
1788
2312
  const goal = base2.goals(candidates, false)[0];
1789
2313
  if (goal !== void 0) {
1790
- const r = route2(base2, goal), done = full();
2314
+ const r = route3(base2, goal), done = full();
1791
2315
  const sequence = [[r.shapes[0], r.shapes[0]]];
1792
2316
  r.hops.forEach(([id], k) => {
1793
2317
  sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
@@ -1816,6 +2340,13 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1816
2340
  else waiting2.push([at, left]);
1817
2341
  }
1818
2342
  waitingBy.set(flow.id, waiting2);
2343
+ routes.set(flow.id, placements.map((p) => ({
2344
+ goal: p.goal,
2345
+ hops: p.hops,
2346
+ amount: p.amount,
2347
+ waits: p.waits === true,
2348
+ cost: p.hops.reduce((s, [id]) => s + routeCost(byID.get(id)?.connector?.cost ?? null), 0)
2349
+ })));
1819
2350
  for (const p of placements) {
1820
2351
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1821
2352
  m.throughput += p.amount;
@@ -1898,7 +2429,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1898
2429
  const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1899
2430
  const goal = tree?.goals(flow.to, false)[0];
1900
2431
  if (!tree || goal === void 0) break;
1901
- const r = route2(tree, goal);
2432
+ const r = route3(tree, goal);
1902
2433
  const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1903
2434
  const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1904
2435
  const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
@@ -1953,7 +2484,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1953
2484
  if (c === null || !Number.isFinite(c) || !m) continue;
1954
2485
  m.load = c > 0 ? m.throughput / c : 1;
1955
2486
  }
1956
- return { packets, metrics: metrics2 };
2487
+ return { packets, metrics: metrics2, routes };
1957
2488
  }
1958
2489
  var BLANK = {
1959
2490
  seed: 1,
@@ -2047,16 +2578,16 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
2047
2578
  }
2048
2579
  var gaugeKey = (id) => "gauge|" + id;
2049
2580
  var GAUGE_EASE = 1;
2050
- var hex = (h) => {
2581
+ var hex2 = (h) => {
2051
2582
  const v = parseInt(h.slice(1), 16);
2052
2583
  return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
2053
2584
  };
2054
- var GAUGE_GREEN = hex("#16a34a");
2055
- var GAUGE_AMBER = hex("#d97706");
2056
- var GAUGE_RED = hex("#dc2626");
2057
- var GAUGE_BLUE = hex("#2f6fdf");
2058
- var GAUGE_TRACK = hex("#e5e7eb");
2059
- var GAUGE_GRAY = hex("#9ca3af");
2585
+ var GAUGE_GREEN = hex2("#16a34a");
2586
+ var GAUGE_AMBER = hex2("#d97706");
2587
+ var GAUGE_RED = hex2("#dc2626");
2588
+ var GAUGE_BLUE = hex2("#2f6fdf");
2589
+ var GAUGE_TRACK = hex2("#e5e7eb");
2590
+ var GAUGE_GRAY = hex2("#9ca3af");
2060
2591
  function formatted(metric, v) {
2061
2592
  if (v === null || !Number.isFinite(v)) return "\u2013";
2062
2593
  switch (metric) {
@@ -2342,17 +2873,17 @@ function clusterStrokes(c, angle2, size) {
2342
2873
  });
2343
2874
  }
2344
2875
  function contains(contours, p) {
2345
- let inside = false;
2876
+ let inside2 = false;
2346
2877
  for (const poly of contours) {
2347
2878
  if (poly.length < 3) continue;
2348
2879
  let k = poly.length - 1;
2349
2880
  for (let i = 0; i < poly.length; i++) {
2350
2881
  const a = poly[i], b = poly[k];
2351
- if (a.y > p.y !== b.y > p.y && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x) inside = !inside;
2882
+ if (a.y > p.y !== b.y > p.y && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x) inside2 = !inside2;
2352
2883
  k = i;
2353
2884
  }
2354
2885
  }
2355
- return inside;
2886
+ return inside2;
2356
2887
  }
2357
2888
  function distance(p, contours) {
2358
2889
  let best = Infinity;
@@ -2629,7 +3160,7 @@ function connectorParts(e, index, page) {
2629
3160
  const c = e.connector;
2630
3161
  if (!c) return [new Path(), new Path()];
2631
3162
  const [a, b] = endpoints(e, index);
2632
- const r = route(e, index);
3163
+ const r = route2(e, index);
2633
3164
  const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
2634
3165
  const key = [
2635
3166
  e.seed,
@@ -2717,7 +3248,7 @@ function linesBeneath(e, page, index) {
2717
3248
  }
2718
3249
  const count = f.first.get(e.id) ?? f.under.length;
2719
3250
  while (f.lines.length < count) {
2720
- const points = routePoints(route(f.under[f.lines.length], index));
3251
+ const points = routePoints(route2(f.under[f.lines.length], index));
2721
3252
  f.lines.push({ points, box: boxOf(points) });
2722
3253
  }
2723
3254
  return { lines: f.lines, count };
@@ -2731,7 +3262,7 @@ function hopCrossings(e, r, page, index) {
2731
3262
  for (const samples of segments) {
2732
3263
  const along = runningLengths(samples);
2733
3264
  const length = along[along.length - 1];
2734
- const found = [];
3265
+ const found2 = [];
2735
3266
  const near = [];
2736
3267
  const whole = boxOf(samples);
2737
3268
  for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
@@ -2748,12 +3279,12 @@ function hopCrossings(e, r, page, index) {
2748
3279
  if (Math.abs(det) < 1e-9) continue;
2749
3280
  const wx = a.x - p.x, wy = a.y - p.y;
2750
3281
  const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
2751
- if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
3282
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found2.push(along[i] + t * (along[i + 1] - along[i]));
2752
3283
  }
2753
3284
  }
2754
3285
  }
2755
3286
  const kept = [];
2756
- for (const at of found.sort((x, y) => x - y)) {
3287
+ for (const at of found2.sort((x, y) => x - y)) {
2757
3288
  if (at < rad + 2 || at > length - rad - 2) continue;
2758
3289
  if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
2759
3290
  kept.push(at);
@@ -2967,7 +3498,7 @@ var lerpColor = (a, b, u) => ({
2967
3498
  b: mix01(a.b, b.b, u),
2968
3499
  a: mix01(a.a, b.a, u)
2969
3500
  });
2970
- function num2(track, t, p, base2) {
3501
+ function num3(track, t, p, base2) {
2971
3502
  return value(track, t, (k) => k[p], base2, lerpNum);
2972
3503
  }
2973
3504
  function isChanging(track, t) {
@@ -2993,20 +3524,20 @@ function isVisible(track, t) {
2993
3524
  function applyTrack(track, e, t) {
2994
3525
  e = cloneElement(e);
2995
3526
  if (track.keys.some((k) => k.reveal !== void 0)) {
2996
- const r = num2(track, t, "reveal", e.reveal ?? 1);
3527
+ const r = num3(track, t, "reveal", e.reveal ?? 1);
2997
3528
  e.reveal = r >= 1 ? null : Math.max(0, r);
2998
3529
  }
2999
3530
  if (track.keys.some((k) => k.opacity !== void 0)) {
3000
- const o = Math.min(Math.max(num2(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3531
+ const o = Math.min(Math.max(num3(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3001
3532
  e.style.opacity = o >= 1 ? null : o;
3002
3533
  }
3003
3534
  if (e.kind === "connector") return e;
3004
- e.x = num2(track, t, "x", e.x);
3005
- e.y = num2(track, t, "y", e.y);
3006
- e.width = Math.max(0, num2(track, t, "width", e.width));
3007
- e.height = Math.max(0, num2(track, t, "height", e.height));
3008
- if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num2(track, t, "rotation", angle(e));
3009
- e.style.strokeWidth = Math.max(0, num2(track, t, "strokeWidth", e.style.strokeWidth));
3535
+ e.x = num3(track, t, "x", e.x);
3536
+ e.y = num3(track, t, "y", e.y);
3537
+ e.width = Math.max(0, num3(track, t, "width", e.width));
3538
+ e.height = Math.max(0, num3(track, t, "height", e.height));
3539
+ if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num3(track, t, "rotation", angle(e));
3540
+ e.style.strokeWidth = Math.max(0, num3(track, t, "strokeWidth", e.style.strokeWidth));
3010
3541
  e.style.stroke = value(track, t, (k) => k.stroke, e.style.stroke, lerpColor);
3011
3542
  e.style.fill = value(track, t, (k) => k.fill, e.style.fill, lerpColor);
3012
3543
  return e;
@@ -3021,7 +3552,7 @@ function followSpan(track, t) {
3021
3552
  return f;
3022
3553
  }
3023
3554
  function tracing(e, page, index = indexOf(page)) {
3024
- if (e.kind === "connector") return new Polyline(routePoints(route(e, index)));
3555
+ if (e.kind === "connector") return new Polyline(routePoints(route2(e, index)));
3025
3556
  const clean = cloneElement(e);
3026
3557
  clean.style.roughness = 0;
3027
3558
  const pts = outline(clean).contours()[0] ?? [];
@@ -3082,7 +3613,8 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3082
3613
  }
3083
3614
  const flowing = (page.flows ?? []).length > 0;
3084
3615
  const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
3085
- const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
3616
+ const fills = values !== null && Object.keys(values).length > 0 && page.elements.some((e) => e.text?.includes("{") || (e.overrides ?? []).some((o) => o.text?.includes("{")));
3617
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring && !fills ? null : EMPTY_TIMELINE);
3086
3618
  if (!timeline) return { frame: page, metrics: emptyMetrics() };
3087
3619
  t = clampTime(t);
3088
3620
  const tracks = trackMap(timeline);
@@ -3118,6 +3650,10 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3118
3650
  const track = tracks.get(e.id);
3119
3651
  let out = track ? applyTrack(track, e, t) : e;
3120
3652
  for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3653
+ if (fills && values && out.text) {
3654
+ const text = filling(out.text, values);
3655
+ if (text !== out.text) out = { ...out, text };
3656
+ }
3121
3657
  const list = active.get(e.id);
3122
3658
  if (list) {
3123
3659
  const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
@@ -3301,10 +3837,10 @@ var Expansions = class {
3301
3837
  this.posing = posing;
3302
3838
  }
3303
3839
  };
3304
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo) {
3840
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo2) {
3305
3841
  if (!hasInstances(page)) return page;
3306
3842
  const budget = { left: MAX_ELEMENTS };
3307
- const m = memo && memo.page === page && memo.t === t && memo.posing === posing ? memo : void 0;
3843
+ const m = memo2 && memo2.page === page && memo2.t === t && memo2.posing === posing ? memo2 : void 0;
3308
3844
  return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3309
3845
  }
3310
3846
  function symbolTable(page) {
@@ -3312,7 +3848,7 @@ function symbolTable(page) {
3312
3848
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3313
3849
  return table;
3314
3850
  }
3315
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo) {
3851
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo2) {
3316
3852
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3317
3853
  if (instances.size === 0) {
3318
3854
  budget.left -= elements.length;
@@ -3338,7 +3874,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3338
3874
  continue;
3339
3875
  }
3340
3876
  const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3341
- const was = memo?.made.get(e);
3877
+ const was = memo2?.made.get(e);
3342
3878
  if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3343
3879
  budget.left -= was.before - was.after;
3344
3880
  for (const x of was.out) out.push(x);
@@ -3360,7 +3896,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3360
3896
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3361
3897
  };
3362
3898
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3363
- if (memo && stack.length === 0 && !was) memo.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3899
+ if (memo2 && stack.length === 0 && !was) memo2.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3364
3900
  }
3365
3901
  return out;
3366
3902
  }
@@ -3413,8 +3949,8 @@ function mix2(x) {
3413
3949
  return z ^ z >> 31n;
3414
3950
  }
3415
3951
  function halves(id) {
3416
- const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3417
- return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
3952
+ const hex3 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3953
+ return [BigInt(`0x${hex3.slice(0, 16)}`), BigInt(`0x${hex3.slice(16)}`)];
3418
3954
  }
3419
3955
  function derivedID(instance, inner) {
3420
3956
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -3435,7 +3971,12 @@ function clickTargets(page, file) {
3435
3971
  let pageOf = null;
3436
3972
  for (const e of page.elements) {
3437
3973
  const c = e.click;
3438
- const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
3974
+ const state = c && changesState(c) ? {
3975
+ ...c.set ? { set: c.set } : {},
3976
+ ...c.toggle ? { toggle: c.toggle } : {},
3977
+ ...c.reset ? { reset: true } : {},
3978
+ ...c.run ? { run: c.run } : {}
3979
+ } : null;
3439
3980
  if (c?.scene) {
3440
3981
  const i = sceneNamed(c.scene, markers);
3441
3982
  if (i >= 0) {
@@ -3742,12 +4283,23 @@ function inline(src, style2 = {}) {
3742
4283
  i += m[0].length;
3743
4284
  continue;
3744
4285
  }
3745
- if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4286
+ if (m = /^(\*\*\*|___)(?=\S)([\s\S]*?\S)\1(?!\*|_)/.exec(rest)) {
3746
4287
  emit();
3747
- out.push(...inline(m[2], { ...style2, bold: true }));
4288
+ out.push(...inline(m[2], { ...style2, bold: true, italic: true }));
3748
4289
  i += m[0].length;
3749
4290
  continue;
3750
4291
  }
4292
+ if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4293
+ let body = m[2], length = m[0].length;
4294
+ if (m[1] === "**" && rest[length] === "*" && (body.replace(/\*\*/g, "").match(/\*/g) ?? []).length % 2 === 1) {
4295
+ body += "*";
4296
+ length++;
4297
+ }
4298
+ emit();
4299
+ out.push(...inline(body, { ...style2, bold: true }));
4300
+ i += length;
4301
+ continue;
4302
+ }
3751
4303
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
3752
4304
  emit();
3753
4305
  out.push(...inline(m[2], { ...style2, italic: true }));
@@ -4131,20 +4683,6 @@ function sameStyle(a, b) {
4131
4683
  return a === b || a.size === b.size && a.bold === b.bold && a.italic === b.italic && a.mono === b.mono && a.strike === b.strike && a.underline === b.underline && a.alpha === b.alpha;
4132
4684
  }
4133
4685
 
4134
- // src/theme.ts
4135
- var rgb = (r, g, b) => ({ r, g, b, a: 1 });
4136
- var themes = [
4137
- { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13) },
4138
- { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13) },
4139
- { id: "sepia", name: "Sepia", background: rgb(0.953, 0.914, 0.824), ink: rgb(0.23, 0.18, 0.13) },
4140
- { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984) },
4141
- { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918) },
4142
- { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94) }
4143
- ];
4144
- function themeNamed(id) {
4145
- return themes.find((t) => t.id === id) ?? null;
4146
- }
4147
-
4148
4686
  // src/svg.ts
4149
4687
  function bounds(page, padding = 24) {
4150
4688
  if (hasInstances(page)) return bounds(expandingSymbols(page, 0), padding);
@@ -4210,7 +4748,7 @@ function svgOpen(b, background, paint) {
4210
4748
  }
4211
4749
  function svgParts(page, background, fixed, links = false, only) {
4212
4750
  if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
4213
- const theme = themeNamed(page.theme);
4751
+ const theme = pageTheme(page);
4214
4752
  const bg = background ? theme?.background ?? background : null;
4215
4753
  const ink = theme?.ink ?? black;
4216
4754
  const d = { ...page, elements: page.elements.map((e) => resolvingInk(e, ink)) };
@@ -4246,7 +4784,7 @@ ${body}</a>
4246
4784
  }
4247
4785
  function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
4248
4786
  const { bounds: b, parts } = svgParts(page, background, fixed, true);
4249
- return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4787
+ return svgOpen(b, pageTheme(page)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4250
4788
  }
4251
4789
  function labelRect(e, index) {
4252
4790
  const m = midpoint(e, index);
@@ -4340,7 +4878,7 @@ function svgElement(e, index, labelBackground, page) {
4340
4878
  }
4341
4879
  s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
4342
4880
  `;
4343
- if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" ? pathLabelBox(e) : frame(e), e, true);
4881
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" || e.kind === "rectangle" ? pathLabelBox(e) : frame(e), e, true);
4344
4882
  return s;
4345
4883
  }
4346
4884
  default:
@@ -4461,6 +4999,40 @@ function start() {
4461
4999
  return w;
4462
5000
  }
4463
5001
 
5002
+ // src/logic-loader.ts
5003
+ var scriptSrc = typeof document !== "undefined" ? document.currentScript?.src ?? null : null;
5004
+ var found = null;
5005
+ var present = () => globalThis.DottieLogic;
5006
+ function logicEngine() {
5007
+ found ?? (found = (async () => {
5008
+ if (!present()) {
5009
+ const here = import.meta.url;
5010
+ const base2 = here ?? scriptSrc;
5011
+ if (!base2) throw new Error("No dottie-logic.min.js on the page, and no address to find it from");
5012
+ const url = new URL("./dottie-logic.min.js", base2).href;
5013
+ if (here) await import(
5014
+ /* @vite-ignore */
5015
+ url
5016
+ );
5017
+ else await new Promise((resolve2, reject) => {
5018
+ const s = document.createElement("script");
5019
+ s.src = url;
5020
+ s.onload = () => resolve2();
5021
+ s.onerror = () => reject(new Error(`Couldn't load ${url}`));
5022
+ document.head.append(s);
5023
+ });
5024
+ }
5025
+ const engine = present();
5026
+ if (!engine) throw new Error("dottie-logic.min.js didn't load");
5027
+ await engine.loadLogic();
5028
+ return engine;
5029
+ })());
5030
+ found.catch(() => {
5031
+ found = null;
5032
+ });
5033
+ return found;
5034
+ }
5035
+
4464
5036
  // src/player.ts
4465
5037
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4466
5038
  function pageIndex(file, spec) {
@@ -4568,7 +5140,7 @@ var Playback = class {
4568
5140
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4569
5141
  this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4570
5142
  this.state = { ...page.variables ?? {} };
4571
- const theme = themeNamed(page.theme);
5143
+ const theme = pageTheme(page);
4572
5144
  this.background = theme?.background ?? WHITE;
4573
5145
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
4574
5146
  const ns = "http://www.w3.org/2000/svg";
@@ -4802,6 +5374,9 @@ var DottiePlayer = class extends ElementBase {
4802
5374
  __publicField(this, "stillTime", null);
4803
5375
  __publicField(this, "visible", true);
4804
5376
  __publicField(this, "observer", null);
5377
+ /** Runs handler `handler` from the page's logic for a click on `id` (DottieDocument.clicked). Clicks take turns, so
5378
+ * each handler sees the state the one before left. */
5379
+ __publicField(this, "logicQueue", Promise.resolve());
4805
5380
  this.root = this.attachShadow({ mode: "open" });
4806
5381
  this.root.innerHTML = `<style>${STYLE}</style><div class="stage" part="stage" role="img"></div>
4807
5382
  <div class="bar" part="controls" hidden>
@@ -5028,7 +5603,9 @@ var DottiePlayer = class extends ElementBase {
5028
5603
  this.scheduleReplay();
5029
5604
  }
5030
5605
  if (!click) return;
5031
- if (click.set || click.toggle || click.reset) {
5606
+ if (click.run && p.page.logic && this.hasAttribute("allow-logic")) {
5607
+ this.runLogic(click.run, id, click.reset === true);
5608
+ } else if (click.set || click.toggle || click.reset) {
5032
5609
  this.setState(applyClick(
5033
5610
  { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
5034
5611
  p.state,
@@ -5050,6 +5627,35 @@ var DottiePlayer = class extends ElementBase {
5050
5627
  }
5051
5628
  }
5052
5629
  }
5630
+ runLogic(handler, id, reset) {
5631
+ const p = this.playback;
5632
+ if (!p) return;
5633
+ const e = p.page.elements.find((x) => x.id === id);
5634
+ const s = p.sceneAt(p.frame);
5635
+ const event = {
5636
+ id,
5637
+ anchor: e?.anchor ?? null,
5638
+ text: e?.text?.split("\n")[0] ?? null,
5639
+ time: this.currentTime,
5640
+ scene: s < 0 ? null : p.sceneNames[s]
5641
+ };
5642
+ this.logicQueue = this.logicQueue.then(async () => {
5643
+ const engine = await logicEngine();
5644
+ const now = this.playback;
5645
+ if (!now || now !== p || !now.page.logic) return;
5646
+ let start2 = now.state;
5647
+ if (reset) {
5648
+ start2 = {};
5649
+ for (const k of Object.keys(now.state)) start2[k] = false;
5650
+ Object.assign(start2, now.page.variables ?? {});
5651
+ }
5652
+ const r = engine.runLogic(now.page.logic, handler, start2, event);
5653
+ if (r.ok) this.setState({ ...start2, ...r.set });
5654
+ else this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: r.error } }));
5655
+ }).catch((err) => {
5656
+ this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: err.message } }));
5657
+ });
5658
+ }
5053
5659
  /** The page's variables now. */
5054
5660
  get variables() {
5055
5661
  return { ...this.playback?.state ?? {} };
@@ -5156,7 +5762,7 @@ var DottiePlayer = class extends ElementBase {
5156
5762
  if (handled) e.preventDefault();
5157
5763
  }
5158
5764
  };
5159
- __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label"]);
5765
+ __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label", "allow-logic"]);
5160
5766
  function clickable(target) {
5161
5767
  const el = target;
5162
5768
  return el && typeof el.closest === "function" ? el.closest(".dottie-a") : null;
@@ -5222,7 +5828,7 @@ function inspectFrame(page, t) {
5222
5828
  const [a, b] = endpoints(e, index);
5223
5829
  d.start = pt(a);
5224
5830
  d.end = pt(b);
5225
- d.route = routePoints(route(e, index)).map(pt);
5831
+ d.route = routePoints(route2(e, index)).map(pt);
5226
5832
  }
5227
5833
  return d;
5228
5834
  });
@@ -5261,7 +5867,7 @@ function render(source, options = {}) {
5261
5867
  function isFile(v) {
5262
5868
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
5263
5869
  }
5264
- var VERSION = "0.9.0";
5870
+ var VERSION = "0.11.0";
5265
5871
  export {
5266
5872
  BOIL_DRAWINGS,
5267
5873
  BUNDLED_FONTS,
@@ -5297,6 +5903,8 @@ export {
5297
5903
  luminance,
5298
5904
  orderedElements,
5299
5905
  pageIndex,
5906
+ pageTheme,
5907
+ paletteColor,
5300
5908
  parseDottie,
5301
5909
  plan,
5302
5910
  posterTime,