@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.
@@ -1,4 +1,4 @@
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 __export = (target, all) => {
@@ -44,6 +44,8 @@ __export(index_exports, {
44
44
  luminance: () => luminance,
45
45
  orderedElements: () => orderedElements,
46
46
  pageIndex: () => pageIndex,
47
+ pageTheme: () => pageTheme,
48
+ paletteColor: () => paletteColor,
47
49
  parseDottie: () => parseDottie,
48
50
  plan: () => plan,
49
51
  posterTime: () => posterTime,
@@ -66,82 +68,332 @@ __export(index_exports, {
66
68
  withRoughness: () => withRoughness
67
69
  });
68
70
 
69
- // src/state.ts
71
+ // src/expression.ts
72
+ var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
73
+ var FUNCTIONS = { min: null, max: null, abs: 1, floor: 1, ceil: 1, round: 1, clamp: 3 };
70
74
  var MAX_NAME = 64;
71
- function isName(name) {
72
- return name.length <= MAX_NAME && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
75
+ var truthy = (v) => typeof v === "boolean" ? v : v !== 0 && !Number.isNaN(v);
76
+ var num = (v) => typeof v === "boolean" ? v ? 1 : 0 : v;
77
+ var parsed = /* @__PURE__ */ new Map();
78
+ function parseExpr(text) {
79
+ const hit = parsed.get(text);
80
+ if (hit !== void 0) return hit;
81
+ const p = new Parser(text);
82
+ let e = p.or();
83
+ if (e && !p.atEnd()) e = null;
84
+ if (parsed.size > 4096) parsed.clear();
85
+ parsed.set(text, e);
86
+ return e;
73
87
  }
74
- var METRIC_NAMES = ["throughput", "load", "delay", "backlog", "depth", "inFlight", "transit", "backup"];
75
- var OPS = [">=", "<=", "==", "!=", ">", "<"];
76
- function metricTerm(term) {
77
- const t = trim(term);
78
- if (!t.includes("(")) {
79
- const op2 = t.search(/[>=<!]/);
80
- if (op2 < 0) return null;
81
- const name2 = trim(t.slice(0, op2));
82
- if (!METRIC_NAMES.includes(name2)) return null;
83
- const m = metricTerm(name2 + "() " + t.slice(op2));
84
- return m ? { ...m, target: "" } : null;
85
- }
86
- const open = t.indexOf("("), close = t.indexOf(")");
87
- if (open < 0 || close < 0 || open >= close) return null;
88
- const name = trim(t.slice(0, open)), target = trim(t.slice(open + 1, close));
89
- let rest = trim(t.slice(close + 1));
90
- if (!METRIC_NAMES.includes(name) || target.includes("(")) return null;
91
- const op = OPS.find((o) => rest.startsWith(o));
92
- if (op === void 0) return null;
93
- rest = trim(rest.slice(op.length));
94
- if (!/^-?[0-9]+(\.[0-9]+)?$/.test(rest)) return null;
95
- return { metric: name, target, op, value: Number(rest) };
96
- }
97
- function passes(m, v) {
98
- switch (m.op) {
99
- case ">":
100
- return v > m.value;
101
- case ">=":
102
- return v >= m.value;
103
- case "<":
104
- return v < m.value;
105
- case "<=":
106
- return v <= m.value;
107
- case "==":
108
- return v === m.value;
109
- default:
110
- return v !== m.value;
88
+ function evaluate(e, state, metric = null) {
89
+ switch (e.k) {
90
+ case "lit":
91
+ return e.v;
92
+ case "name":
93
+ return state[e.name] ?? false;
94
+ case "metric":
95
+ return metric?.(e.metric, e.target) ?? NaN;
96
+ case "not":
97
+ return !truthy(evaluate(e.e, state, metric));
98
+ case "neg":
99
+ return -num(evaluate(e.e, state, metric));
100
+ case "call": {
101
+ const xs = e.args.map((a) => num(evaluate(a, state, metric)));
102
+ const fold = (better) => {
103
+ if (xs.some(Number.isNaN)) return NaN;
104
+ let out = xs[0];
105
+ for (const x of xs.slice(1)) if (better(x, out)) out = x;
106
+ return out;
107
+ };
108
+ switch (e.fn) {
109
+ case "min":
110
+ return fold((a, b) => a < b);
111
+ case "max":
112
+ return fold((a, b) => a > b);
113
+ case "abs":
114
+ return Math.abs(xs[0]);
115
+ case "floor":
116
+ return Math.floor(xs[0]);
117
+ case "ceil":
118
+ return Math.ceil(xs[0]);
119
+ case "round":
120
+ return Math.floor(xs[0] + 0.5);
121
+ default:
122
+ if (xs.some(Number.isNaN)) return NaN;
123
+ return xs[0] < xs[1] ? xs[1] : xs[0] > xs[2] ? xs[2] : xs[0];
124
+ }
125
+ }
126
+ case "bin": {
127
+ if (e.op === "&&") return truthy(evaluate(e.l, state, metric)) && truthy(evaluate(e.r, state, metric));
128
+ if (e.op === "||") return truthy(evaluate(e.l, state, metric)) || truthy(evaluate(e.r, state, metric));
129
+ const a = evaluate(e.l, state, metric), b = evaluate(e.r, state, metric);
130
+ const x = num(a), y = num(b);
131
+ switch (e.op) {
132
+ case "+":
133
+ return x + y;
134
+ case "-":
135
+ return x - y;
136
+ case "*":
137
+ return x * y;
138
+ case "/":
139
+ return x / y;
140
+ case "%":
141
+ return x % y;
142
+ }
143
+ if (Number.isNaN(x) || Number.isNaN(y)) return false;
144
+ switch (e.op) {
145
+ case "<":
146
+ return x < y;
147
+ case "<=":
148
+ return x <= y;
149
+ case ">":
150
+ return x > y;
151
+ case ">=":
152
+ return x >= y;
153
+ case "==":
154
+ return typeof a === "boolean" && typeof b === "boolean" ? a === b : x === y;
155
+ default:
156
+ return typeof a === "boolean" && typeof b === "boolean" ? a !== b : x !== y;
157
+ }
158
+ }
111
159
  }
112
160
  }
113
- function mentionsMetric(condition) {
114
- return condition.split("||").some((any) => any.split("&&").some((all) => metricTerm(all) !== null));
161
+ function exprNames(e) {
162
+ const out = [];
163
+ const walk = (x) => {
164
+ switch (x.k) {
165
+ case "name":
166
+ if (!out.includes(x.name)) out.push(x.name);
167
+ break;
168
+ case "not":
169
+ case "neg":
170
+ walk(x.e);
171
+ break;
172
+ case "bin":
173
+ walk(x.l);
174
+ walk(x.r);
175
+ break;
176
+ case "call":
177
+ x.args.forEach(walk);
178
+ break;
179
+ }
180
+ };
181
+ walk(e);
182
+ return out;
115
183
  }
116
- function conditionNames(condition) {
184
+ function exprMetrics(e) {
117
185
  const out = [];
118
- for (const any of condition.split("||")) {
119
- for (const all of any.split("&&")) {
120
- if (metricTerm(all) !== null) continue;
121
- let term = trim(all);
122
- if (term.startsWith("!")) term = trim(term.slice(1));
123
- if (!isName(term)) return null;
124
- out.push(term);
186
+ const walk = (x) => {
187
+ switch (x.k) {
188
+ case "metric":
189
+ out.push({ metric: x.metric, target: x.target });
190
+ break;
191
+ case "not":
192
+ case "neg":
193
+ walk(x.e);
194
+ break;
195
+ case "bin":
196
+ walk(x.l);
197
+ walk(x.r);
198
+ break;
199
+ case "call":
200
+ x.args.forEach(walk);
201
+ break;
125
202
  }
126
- }
203
+ };
204
+ walk(e);
127
205
  return out;
128
206
  }
207
+ var isDigit = (c) => c !== void 0 && c >= "0" && c <= "9";
208
+ var isLetter = (c) => c !== void 0 && (c === "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z");
209
+ var Parser = class {
210
+ constructor(text) {
211
+ __publicField(this, "s");
212
+ __publicField(this, "i", 0);
213
+ this.s = [...text];
214
+ }
215
+ atEnd() {
216
+ this.skip();
217
+ return this.i === this.s.length;
218
+ }
219
+ skip() {
220
+ while (this.i < this.s.length && " \n\r".includes(this.s[this.i])) this.i++;
221
+ }
222
+ eat(token) {
223
+ this.skip();
224
+ const t = [...token];
225
+ if (this.i + t.length > this.s.length) return false;
226
+ for (let k = 0; k < t.length; k++) if (this.s[this.i + k] !== t[k]) return false;
227
+ if (t.length === 1 && this.s[this.i + 1] === "=" && "<>!=".includes(t[0])) return false;
228
+ this.i += t.length;
229
+ return true;
230
+ }
231
+ or() {
232
+ let l = this.and();
233
+ if (!l) return null;
234
+ while (this.eat("||")) {
235
+ const r = this.and();
236
+ if (!r) return null;
237
+ l = { k: "bin", op: "||", l, r };
238
+ }
239
+ return l;
240
+ }
241
+ and() {
242
+ let l = this.compare();
243
+ if (!l) return null;
244
+ while (this.eat("&&")) {
245
+ const r = this.compare();
246
+ if (!r) return null;
247
+ l = { k: "bin", op: "&&", l, r };
248
+ }
249
+ return l;
250
+ }
251
+ compare() {
252
+ const l = this.sum();
253
+ if (!l) return null;
254
+ for (const op of ["<=", ">=", "==", "!=", "<", ">"]) {
255
+ if (this.eat(op)) {
256
+ const r = this.sum();
257
+ return r ? { k: "bin", op, l, r } : null;
258
+ }
259
+ }
260
+ return l;
261
+ }
262
+ sum() {
263
+ let l = this.product();
264
+ if (!l) return null;
265
+ for (; ; ) {
266
+ const op = this.eat("+") ? "+" : this.eat("-") ? "-" : null;
267
+ if (!op) return l;
268
+ const r = this.product();
269
+ if (!r) return null;
270
+ l = { k: "bin", op, l, r };
271
+ }
272
+ }
273
+ product() {
274
+ let l = this.unary();
275
+ if (!l) return null;
276
+ for (; ; ) {
277
+ const op = ["*", "/", "%"].find((o) => this.eat(o));
278
+ if (!op) return l;
279
+ const r = this.unary();
280
+ if (!r) return null;
281
+ l = { k: "bin", op, l, r };
282
+ }
283
+ }
284
+ unary() {
285
+ if (this.eat("!")) {
286
+ const e = this.unary();
287
+ return e ? { k: "not", e } : null;
288
+ }
289
+ if (this.eat("-")) {
290
+ const e = this.unary();
291
+ return e ? { k: "neg", e } : null;
292
+ }
293
+ return this.primary();
294
+ }
295
+ primary() {
296
+ this.skip();
297
+ if (this.i >= this.s.length) return null;
298
+ if (this.eat("(")) {
299
+ const e = this.or();
300
+ return e && this.eat(")") ? e : null;
301
+ }
302
+ const s = this.s;
303
+ const c = s[this.i];
304
+ if (isDigit(c)) {
305
+ const start3 = this.i;
306
+ while (isDigit(s[this.i])) this.i++;
307
+ if (s[this.i] === "." && isDigit(s[this.i + 1])) {
308
+ this.i++;
309
+ while (isDigit(s[this.i])) this.i++;
310
+ }
311
+ return { k: "lit", v: Number(s.slice(start3, this.i).join("")) };
312
+ }
313
+ if (!isLetter(c)) return null;
314
+ const start2 = this.i;
315
+ while (isLetter(s[this.i]) || isDigit(s[this.i])) this.i++;
316
+ const name = s.slice(start2, this.i).join("");
317
+ if (name.length > MAX_NAME) return null;
318
+ let j = this.i;
319
+ while (s[j] === " " || s[j] === " ") j++;
320
+ const called = s[j] === "(";
321
+ if (METRIC_NAMES.includes(name)) {
322
+ if (!called) return { k: "metric", metric: name, target: "" };
323
+ this.eat("(");
324
+ const from = this.i;
325
+ while (this.i < s.length && s[this.i] !== ")" && s[this.i] !== "(") this.i++;
326
+ if (s[this.i] !== ")") return null;
327
+ const target = s.slice(from, this.i).join("").replace(/^[ \t]+|[ \t]+$/g, "");
328
+ this.i++;
329
+ return { k: "metric", metric: name, target };
330
+ }
331
+ if (name in FUNCTIONS) {
332
+ if (!called || !this.eat("(")) return null;
333
+ const args = [];
334
+ if (!this.eat(")")) {
335
+ do {
336
+ const a = this.or();
337
+ if (!a) return null;
338
+ args.push(a);
339
+ } while (this.eat(","));
340
+ if (!this.eat(")")) return null;
341
+ }
342
+ const arity = FUNCTIONS[name];
343
+ if (arity !== null ? args.length !== arity : args.length === 0) return null;
344
+ return { k: "call", fn: name, args };
345
+ }
346
+ if (called) return null;
347
+ if (name === "true") return { k: "lit", v: true };
348
+ if (name === "false") return { k: "lit", v: false };
349
+ return { k: "name", name };
350
+ }
351
+ };
352
+ function describeVar(v) {
353
+ return typeof v === "boolean" ? v ? "true" : "false" : formatTwo(v);
354
+ }
355
+ function formatTwo(v) {
356
+ const n = Math.floor(Math.abs(v) * 100 + 0.5);
357
+ if (!(n < 1e15)) return v < 0 ? "-\u221E" : "\u221E";
358
+ const whole = Math.floor(n / 100), part = n - whole * 100;
359
+ let s = String(whole);
360
+ if (part > 0) s += "." + String(part).padStart(2, "0").replace(/0+$/, "");
361
+ return (v < 0 && n > 0 ? "-" : "") + s;
362
+ }
363
+ function filling(text, values) {
364
+ if (!text.includes("{")) return text;
365
+ let out = "", rest = text;
366
+ for (let open = rest.indexOf("{"); open >= 0; open = rest.indexOf("{")) {
367
+ out += rest.slice(0, open);
368
+ const close = rest.indexOf("}", open);
369
+ if (close < 0) {
370
+ out += rest.slice(open);
371
+ rest = "";
372
+ break;
373
+ }
374
+ const inner = rest.slice(open + 1, close);
375
+ const e = inner.includes("{") ? null : parseExpr(inner);
376
+ const names = e ? exprNames(e) : [];
377
+ if (e && exprMetrics(e).length === 0 && names.length > 0 && names.every((n) => values[n] !== void 0)) out += describeVar(evaluate(e, values));
378
+ else out += rest.slice(open, close + 1);
379
+ rest = rest.slice(close + 1);
380
+ }
381
+ return out + rest;
382
+ }
383
+
384
+ // src/state.ts
385
+ var MAX_NAME2 = 64;
386
+ function isName(name) {
387
+ return name.length <= MAX_NAME2 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
388
+ }
389
+ function mentionsMetric(condition) {
390
+ const e = parseExpr(condition);
391
+ return e !== null && exprMetrics(e).length > 0;
392
+ }
129
393
  function holds(condition, state, metric = null) {
130
- if (conditionNames(condition) === null) return false;
131
- return condition.split("||").some((any) => any.split("&&").every((all) => {
132
- const m = metricTerm(all);
133
- if (m !== null) {
134
- const v = metric ? metric(m) : null;
135
- return v !== null && passes(m, v);
136
- }
137
- let term = trim(all);
138
- let negated = false;
139
- if (term.startsWith("!")) {
140
- negated = true;
141
- term = trim(term.slice(1));
142
- }
143
- return (state[term] ?? false) !== negated;
144
- }));
394
+ const e = parseExpr(condition);
395
+ if (!e) return false;
396
+ return truthy(evaluate(e, state, metric ? (m, target) => metric({ metric: m, target, op: "", value: 0 }) : null));
145
397
  }
146
398
  function stateValues(page, choice) {
147
399
  if (choice === "all") return null;
@@ -180,7 +432,7 @@ function applyOverride(o, e) {
180
432
  return out;
181
433
  }
182
434
  function changesState(c) {
183
- return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0);
435
+ return !!c && (c.reset === true || Object.keys(c.set ?? {}).length > 0 || (c.toggle ?? []).length > 0 || !!c.run);
184
436
  }
185
437
  function applyClick(c, state, initial = {}) {
186
438
  let out = { ...state };
@@ -189,27 +441,91 @@ function applyClick(c, state, initial = {}) {
189
441
  for (const k of Object.keys(state)) out[k] = false;
190
442
  Object.assign(out, initial);
191
443
  }
192
- for (const [k, v] of Object.entries(c.set ?? {})) out[k] = v;
193
- for (const k of c.toggle ?? []) out[k] = !(out[k] ?? false);
444
+ const before = { ...out };
445
+ for (const [k, v] of Object.entries(c.set ?? {})) out[k] = clickValue(v, before, before[k]);
446
+ for (const k of c.toggle ?? []) out[k] = !truthy(out[k] ?? false);
194
447
  return out;
195
448
  }
196
- function trim(s) {
197
- return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
449
+ function clickValue(v, state, current) {
450
+ if (typeof v !== "string") return v;
451
+ const e = parseExpr(v);
452
+ return e ? evaluate(e, state) : current ?? false;
453
+ }
454
+
455
+ // src/theme.ts
456
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
457
+ function hex(h) {
458
+ const n = parseInt(h.slice(1), 16);
459
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
460
+ }
461
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
462
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
463
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
464
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
465
+ var themes = [
466
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
467
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
468
+ {
469
+ id: "sepia",
470
+ name: "Sepia",
471
+ background: rgb(0.953, 0.914, 0.824),
472
+ ink: rgb(0.23, 0.18, 0.13),
473
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
474
+ },
475
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
476
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
477
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
478
+ ];
479
+ function themeNamed(id) {
480
+ return themes.find((t) => t.id === id) ?? null;
481
+ }
482
+ function pageTheme(page) {
483
+ const c = page.customTheme;
484
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
485
+ return themeNamed(page.theme);
486
+ }
487
+ function paletteColor(page, name) {
488
+ const n = name.trim().toLowerCase();
489
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
490
+ }
491
+ function resolvingPalette(page) {
492
+ const named = (e) => !!e.style.named;
493
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
494
+ const resolve2 = (e) => {
495
+ const n = e.style.named;
496
+ if (!n) return e;
497
+ const style2 = { ...e.style };
498
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
499
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
500
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
501
+ if (f) style2.fill = { ...f };
502
+ if (s) style2.stroke = { ...s };
503
+ if (t) style2.textColor = { ...t };
504
+ return { ...e, style: style2 };
505
+ };
506
+ return {
507
+ ...page,
508
+ elements: page.elements.map(resolve2),
509
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve2) })) : page.symbols
510
+ };
198
511
  }
199
512
 
200
513
  // src/model.ts
201
- var SUPPORTED_VERSION = 2;
514
+ var SUPPORTED_VERSION = 3;
202
515
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
203
516
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
204
517
  var KNOWN_FIELDS = {
205
518
  file: ["version", "pages", "images"],
206
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
519
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "logic", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
207
520
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
208
521
  flowOverride: ["when", "volume"],
209
522
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
210
523
  layer: ["id", "name", "visible", "locked"],
211
524
  rgba: ["r", "g", "b", "a"],
212
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
525
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
526
+ colorNames: ["fill", "stroke", "textColor"],
527
+ customTheme: ["name", "background", "ink", "palette", "base"],
528
+ paletteColor: ["name", "color"],
213
529
  element: [
214
530
  "id",
215
531
  "kind",
@@ -247,8 +563,11 @@ var KNOWN_FIELDS = {
247
563
  "capacity",
248
564
  "delay",
249
565
  "queue",
250
- "gauge"
566
+ "gauge",
567
+ "containerID",
568
+ "container"
251
569
  ],
570
+ container: ["fixed", "lanes"],
252
571
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
253
572
  strokePoint: ["x", "y"],
254
573
  subpath: ["closed", "coords"],
@@ -257,7 +576,7 @@ var KNOWN_FIELDS = {
257
576
  extensionRef: ["id", "shape", "version", "params"],
258
577
  normalizedPoint: ["x", "y"],
259
578
  normalizedRect: ["x", "y", "width", "height"],
260
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
579
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
261
580
  waypoint: ["x", "y"],
262
581
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
263
582
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -266,7 +585,7 @@ var KNOWN_FIELDS = {
266
585
  repeat: ["start", "end", "count"],
267
586
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
268
587
  marker: ["t", "name", "loop"],
269
- click: ["scene", "url", "set", "toggle", "reset"],
588
+ click: ["scene", "url", "set", "toggle", "reset", "run"],
270
589
  hover: ["stroke", "fill", "opacity"],
271
590
  override: ["when", "stroke", "fill", "textColor", "opacity", "strokeWidth", "dash", "text", "reroute", "offline", "cost", "capacity", "delay"]
272
591
  };
@@ -280,7 +599,7 @@ var inkOnDark = { r: 0.93, g: 0.93, b: 0.94, a: 1 };
280
599
  var DottieFormatError = class extends Error {
281
600
  };
282
601
  var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
283
- var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
602
+ var num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
284
603
  var str = (v) => typeof v === "string" ? v : void 0;
285
604
  var strings = (v) => isObj(v) && Object.values(v).every((x) => typeof x === "string") ? { ...v } : null;
286
605
  var bool = (v) => typeof v === "boolean" ? v : void 0;
@@ -304,9 +623,9 @@ function webLink(url) {
304
623
  }
305
624
  function rgba(v) {
306
625
  if (!isObj(v)) return void 0;
307
- const r = num(v.r), g = num(v.g), b = num(v.b);
626
+ const r = num2(v.r), g = num2(v.g), b = num2(v.b);
308
627
  if (r === void 0 || g === void 0 || b === void 0) return void 0;
309
- return { r, g, b, a: num(v.a) ?? 1 };
628
+ return { r, g, b, a: num2(v.a) ?? 1 };
310
629
  }
311
630
  var Reader = class {
312
631
  constructor() {
@@ -335,28 +654,55 @@ var Reader = class {
335
654
  stroke: rgba(s.stroke) ?? { ...black },
336
655
  fill: rgba(s.fill) ?? { ...clear },
337
656
  fillStyle: ["solid", "hachure", "crossHatch", "grid"].includes(fillStyle) ? fillStyle : "solid",
338
- strokeWidth: num(s.strokeWidth) ?? 2,
657
+ strokeWidth: num2(s.strokeWidth) ?? 2,
339
658
  dash: ["solid", "dashed", "dotted"].includes(dash) ? dash : "solid",
340
- roughness: num(s.roughness) ?? 1,
341
- bowing: num(s.bowing) ?? 1,
342
- fontSize: num(s.fontSize) ?? 18,
659
+ roughness: num2(s.roughness) ?? 1,
660
+ bowing: num2(s.bowing) ?? 1,
661
+ fontSize: num2(s.fontSize) ?? 18,
343
662
  font: str(s.font) || null,
344
- gridSize: num(s.gridSize) ?? null,
345
- opacity: num(s.opacity) ?? null,
346
- dashOffset: num(s.dashOffset) ?? null,
347
- textColor: rgba(s.textColor) ?? null
663
+ gridSize: num2(s.gridSize) ?? null,
664
+ opacity: num2(s.opacity) ?? null,
665
+ dashOffset: num2(s.dashOffset) ?? null,
666
+ textColor: rgba(s.textColor) ?? null,
667
+ named: this.colorNames(s.named)
348
668
  };
349
669
  }
670
+ container(v) {
671
+ if (!isObj(v)) return null;
672
+ this.fields(v, "container");
673
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
674
+ }
675
+ colorNames(v) {
676
+ if (!isObj(v)) return null;
677
+ this.fields(v, "colorNames");
678
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
679
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
680
+ }
681
+ customTheme(v) {
682
+ if (!isObj(v)) return null;
683
+ this.fields(v, "customTheme");
684
+ const background = rgba(v.background), ink = rgba(v.ink);
685
+ if (!background || !ink) {
686
+ this.warn("A custom theme needs a background and an ink; drawn without it");
687
+ return null;
688
+ }
689
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
690
+ this.fields(p, "paletteColor");
691
+ const name = str(p.name), color2 = rgba(p.color);
692
+ return name && color2 ? [{ name, color: color2 }] : [];
693
+ });
694
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
695
+ }
350
696
  endpoint(v) {
351
697
  const e = isObj(v) ? v : {};
352
698
  const port = str(e.port);
353
- const portIndex = num(e.portIndex);
699
+ const portIndex = num2(e.portIndex);
354
700
  return {
355
701
  elementID: uuid(e.elementID) ?? null,
356
702
  port: port && ["top", "right", "bottom", "left"].includes(port) ? port : null,
357
703
  portIndex: portIndex !== void 0 && Number.isInteger(portIndex) && portIndex >= 0 ? portIndex : null,
358
- x: num(e.x) ?? 0,
359
- y: num(e.y) ?? 0
704
+ x: num2(e.x) ?? 0,
705
+ y: num2(e.y) ?? 0
360
706
  };
361
707
  }
362
708
  connector(v) {
@@ -375,9 +721,10 @@ var Reader = class {
375
721
  endHead: head(v.endHead, "arrow"),
376
722
  waypoints: Array.isArray(v.waypoints) ? v.waypoints.filter(isObj).map((w) => {
377
723
  this.fields(w, "waypoint");
378
- return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
724
+ return { x: num2(w.x) ?? 0, y: num2(w.y) ?? 0 };
379
725
  }) : null,
380
726
  hops: typeof v.hops === "boolean" ? v.hops : null,
727
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
381
728
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
382
729
  };
383
730
  }
@@ -410,20 +757,20 @@ var Reader = class {
410
757
  id,
411
758
  kind,
412
759
  layerID,
413
- x: num(v.x) ?? 0,
414
- y: num(v.y) ?? 0,
415
- width: num(v.width) ?? 0,
416
- height: num(v.height) ?? 0,
760
+ x: num2(v.x) ?? 0,
761
+ y: num2(v.y) ?? 0,
762
+ width: num2(v.width) ?? 0,
763
+ height: num2(v.height) ?? 0,
417
764
  style: this.style(v.style),
418
- seed: (num(v.seed) ?? 1) >>> 0,
765
+ seed: (num2(v.seed) ?? 1) >>> 0,
419
766
  text: str(v.text) ?? null,
420
767
  connector: this.connector(v.connector),
421
768
  groupID: uuid(v.groupID) ?? null,
422
- points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num(p.x) ?? 0, y: num(p.y) ?? 0 })) : null,
769
+ points: Array.isArray(v.points) ? v.points.filter(isObj).map((p) => ({ x: num2(p.x) ?? 0, y: num2(p.y) ?? 0 })) : null,
423
770
  path: Array.isArray(v.path) ? v.path.filter((c) => typeof c === "number") : null,
424
771
  subpaths,
425
772
  evenOdd: bool(v.evenOdd) ?? null,
426
- rotation: num(v.rotation) ?? null,
773
+ rotation: num2(v.rotation) ?? null,
427
774
  link: uuid(v.link) ?? null,
428
775
  url: str(v.url) ?? null,
429
776
  anchor: str(v.anchor) ?? null,
@@ -433,20 +780,22 @@ var Reader = class {
433
780
  overrides: this.overrides(v.overrides),
434
781
  symbol: uuid(v.symbol) ?? null,
435
782
  playback: this.playback(v.playback),
436
- offset: num(v.offset) ?? null,
783
+ offset: num2(v.offset) ?? null,
437
784
  args: strings(v.args),
438
785
  image: this.image(v.image, images),
439
- reveal: num(v.reveal) ?? null,
786
+ reveal: num2(v.reveal) ?? null,
440
787
  ports: Array.isArray(v.ports) ? v.ports.map((p) => {
441
788
  const o = isObj(p) ? p : {};
442
- return { x: num(o.x) ?? 0, y: num(o.y) ?? 0 };
789
+ return { x: num2(o.x) ?? 0, y: num2(o.y) ?? 0 };
443
790
  }) : null,
444
791
  labelRect: this.labelRect(v.labelRect),
445
792
  extension: this.extension(v.extension),
446
- capacity: num(v.capacity) ?? null,
447
- delay: num(v.delay) ?? null,
793
+ capacity: num2(v.capacity) ?? null,
794
+ delay: num2(v.delay) ?? null,
448
795
  queue: v.queue === true ? true : null,
449
- gauge: this.gauge(v.gauge)
796
+ gauge: this.gauge(v.gauge),
797
+ containerID: uuid(v.containerID) ?? null,
798
+ container: this.container(v.container)
450
799
  };
451
800
  }
452
801
  gauge(v) {
@@ -467,7 +816,7 @@ var Reader = class {
467
816
  labelRect(v) {
468
817
  if (!isObj(v)) return null;
469
818
  this.fields(v, "normalizedRect");
470
- return { x: num(v.x) ?? 0, y: num(v.y) ?? 0, width: num(v.width) ?? 0, height: num(v.height) ?? 0 };
819
+ return { x: num2(v.x) ?? 0, y: num2(v.y) ?? 0, width: num2(v.width) ?? 0, height: num2(v.height) ?? 0 };
471
820
  }
472
821
  extension(v) {
473
822
  if (!isObj(v)) return null;
@@ -488,7 +837,7 @@ var Reader = class {
488
837
  variables(v) {
489
838
  if (!isObj(v)) return null;
490
839
  const out = {};
491
- for (const [k, b] of Object.entries(v)) if (typeof b === "boolean") out[k] = b;
840
+ for (const [k, b] of Object.entries(v)) if (typeof b === "boolean" || typeof b === "number" && Number.isFinite(b)) out[k] = b;
492
841
  return Object.keys(out).length > 0 ? out : null;
493
842
  }
494
843
  click(v) {
@@ -500,12 +849,22 @@ var Reader = class {
500
849
  let set = null;
501
850
  if (isObj(v.set)) {
502
851
  set = {};
503
- for (const [k, b] of Object.entries(v.set)) if (typeof b === "boolean" && isName(k)) set[k] = b;
852
+ for (const [k, b] of Object.entries(v.set)) {
853
+ if (isName(k) && (typeof b === "boolean" || typeof b === "string" || typeof b === "number" && Number.isFinite(b))) set[k] = b;
854
+ }
504
855
  if (Object.keys(set).length === 0) set = null;
505
856
  }
506
857
  const raw = typeof v.toggle === "string" ? [v.toggle] : Array.isArray(v.toggle) && v.toggle.every((x) => typeof x === "string") ? v.toggle : null;
507
858
  const toggle = raw ? raw.filter(isName) : null;
508
- return { scene: str(v.scene) ?? null, url: safe, set, toggle: toggle && toggle.length > 0 ? toggle : null, reset: v.reset === true ? true : null };
859
+ const run = str(v.run);
860
+ return {
861
+ scene: str(v.scene) ?? null,
862
+ url: safe,
863
+ set,
864
+ toggle: toggle && toggle.length > 0 ? toggle : null,
865
+ reset: v.reset === true ? true : null,
866
+ run: run !== void 0 && isName(run) ? run : null
867
+ };
509
868
  }
510
869
  playback(v) {
511
870
  const s = str(v);
@@ -521,7 +880,7 @@ var Reader = class {
521
880
  return null;
522
881
  }
523
882
  const size = (x) => {
524
- const n = num(x);
883
+ const n = num2(x);
525
884
  return Math.max(1, n ?? 100);
526
885
  };
527
886
  const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
@@ -558,8 +917,8 @@ var Reader = class {
558
917
  stroke: rgba(o.stroke) ?? null,
559
918
  fill: rgba(o.fill) ?? null,
560
919
  textColor: rgba(o.textColor) ?? null,
561
- opacity: num(o.opacity) ?? null,
562
- strokeWidth: num(o.strokeWidth) ?? null,
920
+ opacity: num2(o.opacity) ?? null,
921
+ strokeWidth: num2(o.strokeWidth) ?? null,
563
922
  dash: dash !== void 0 && ["solid", "dashed", "dotted"].includes(dash) ? dash : null,
564
923
  text: str(o.text) ?? null,
565
924
  reroute,
@@ -573,7 +932,7 @@ var Reader = class {
573
932
  hover(v) {
574
933
  if (!isObj(v)) return null;
575
934
  this.fields(v, "hover");
576
- return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num(v.opacity) ?? null };
935
+ return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num2(v.opacity) ?? null };
577
936
  }
578
937
  ease(v) {
579
938
  const s = str(v);
@@ -587,11 +946,11 @@ var Reader = class {
587
946
  keyframe(v) {
588
947
  if (!isObj(v)) return null;
589
948
  this.fields(v, "keyframe");
590
- const t = num(v.t);
949
+ const t = num2(v.t);
591
950
  if (t === void 0) return null;
592
951
  const k = { t };
593
952
  for (const p of ["x", "y", "width", "height", "rotation", "strokeWidth", "reveal", "opacity"]) {
594
- const n = num(v[p]);
953
+ const n = num2(v[p]);
595
954
  if (n !== void 0) k[p] = n;
596
955
  }
597
956
  const stroke = rgba(v.stroke), fill = rgba(v.fill), visible = bool(v.visible), ease = this.ease(v.ease);
@@ -609,39 +968,39 @@ var Reader = class {
609
968
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
610
969
  stableSortBy(keys, (k) => k.t);
611
970
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
612
- const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
971
+ const path = uuid(f.path), start2 = num2(f.start), end = num2(f.end);
613
972
  if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
614
- return [{ path, start: start2, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
973
+ 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 }];
615
974
  }) : null;
616
975
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
617
- const start2 = num(r.start), end = num(r.end);
976
+ const start2 = num2(r.start), end = num2(r.end);
618
977
  if (start2 === void 0 || end === void 0) return [];
619
- const count = num(r.count);
978
+ const count = num2(r.count);
620
979
  const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
621
980
  return repeatIsValid(rep) ? [rep] : [];
622
981
  }) : null;
623
- return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
982
+ return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num2(v.march)) };
624
983
  }
625
984
  timeline(v) {
626
985
  if (!isObj(v)) return null;
627
986
  this.fields(v, "timeline");
628
- const boil = num(v.boil);
987
+ const boil = num2(v.boil);
629
988
  const camera = Array.isArray(v.camera) ? v.camera.filter(isObj).flatMap((c) => {
630
- const t = num(c.t), x = num(c.x), y = num(c.y), width = num(c.width), height = num(c.height);
989
+ const t = num2(c.t), x = num2(c.x), y = num2(c.y), width = num2(c.width), height = num2(c.height);
631
990
  if (t === void 0 || x === void 0 || y === void 0 || width === void 0 || height === void 0) return [];
632
991
  if (!(width > 1 && height > 1 && t >= 0)) return [];
633
992
  return [{ t, x, y, width, height, ease: this.ease(c.ease) }];
634
993
  }) : null;
635
994
  if (camera) stableSortBy(camera, (c) => c.t);
636
995
  const markers = Array.isArray(v.markers) ? v.markers.filter(isObj).flatMap((m) => {
637
- const t = num(m.t);
996
+ const t = num2(m.t);
638
997
  return t !== void 0 && t >= 0 ? [{ t, name: str(m.name) ?? "", loop: bool(m.loop) ?? null }] : [];
639
998
  }) : null;
640
999
  if (markers) stableSortBy(markers, (m) => m.t);
641
- const poster = num(v.poster);
1000
+ const poster = num2(v.poster);
642
1001
  return {
643
- fps: clampFPS(num(v.fps) ?? 12),
644
- duration: clampTime(num(v.duration) ?? 0),
1002
+ fps: clampFPS(num2(v.fps) ?? 12),
1003
+ duration: clampTime(num2(v.duration) ?? 0),
645
1004
  boil: boil !== void 0 && Number.isInteger(boil) && boil > 0 ? Math.min(boil, 1e4) : null,
646
1005
  settle: bool(v.settle) ?? null,
647
1006
  poster: poster === void 0 ? null : clampTime(poster),
@@ -667,6 +1026,8 @@ var Reader = class {
667
1026
  layers,
668
1027
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
669
1028
  theme: str(v.theme) ?? null,
1029
+ customTheme: this.customTheme(v.customTheme),
1030
+ logic: typeof v.logic === "string" && v.logic.length > 0 ? v.logic : null,
670
1031
  timeline: this.timeline(v.timeline),
671
1032
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
672
1033
  variables: this.variables(v.variables),
@@ -678,7 +1039,7 @@ var Reader = class {
678
1039
  this.fields(f, "flow");
679
1040
  const id = (x) => typeof x === "string" && UUID.test(x) ? x.toUpperCase() : NO_ID;
680
1041
  const finite = (x) => {
681
- const n = num(x);
1042
+ const n = num2(x);
682
1043
  return n !== void 0 && Number.isFinite(n) ? n : null;
683
1044
  };
684
1045
  return {
@@ -718,7 +1079,7 @@ function parseDottie(source) {
718
1079
  }
719
1080
  }
720
1081
  if (!isObj(json)) throw new DottieFormatError("Not a Dottie drawing");
721
- const version = num(json.version);
1082
+ const version = num2(json.version);
722
1083
  if (version === void 0) throw new DottieFormatError("Not a Dottie drawing (no version)");
723
1084
  if (!Array.isArray(json.pages) || json.pages.length === 0) throw new DottieFormatError("A drawing needs at least one page");
724
1085
  const reader = new Reader();
@@ -730,7 +1091,7 @@ function parseDottie(source) {
730
1091
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
731
1092
  }
732
1093
  }
733
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
1094
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
734
1095
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
735
1096
  return { version, pages, warnings: reader.warnings };
736
1097
  }
@@ -750,7 +1111,8 @@ function cloneElement(e) {
750
1111
  ...e.style,
751
1112
  stroke: { ...e.style.stroke },
752
1113
  fill: { ...e.style.fill },
753
- textColor: e.style.textColor ? { ...e.style.textColor } : null
1114
+ textColor: e.style.textColor ? { ...e.style.textColor } : null,
1115
+ named: e.style.named ? { ...e.style.named } : e.style.named
754
1116
  },
755
1117
  connector: e.connector ? {
756
1118
  ...e.connector,
@@ -1240,6 +1602,165 @@ function shapePath(e) {
1240
1602
  return path;
1241
1603
  }
1242
1604
 
1605
+ // src/elbow.ts
1606
+ var MARGIN = 16;
1607
+ var BEND = 24;
1608
+ var SMALLEST = 24;
1609
+ var MOST = 48;
1610
+ function obstacle(e) {
1611
+ if (e.kind === "connector" || e.kind === "text" || e.kind === "freehand") return null;
1612
+ if (e.kind === "path" && !e.extension) return null;
1613
+ if (e.style.opacity !== null && e.style.opacity <= 0) return null;
1614
+ const r = rotatedBounds(e);
1615
+ return r.width < SMALLEST || r.height < SMALLEST ? null : r;
1616
+ }
1617
+ function crosses(points, r) {
1618
+ const x1 = r.x + r.width, y1 = r.y + r.height;
1619
+ for (let k = 1; k < points.length; k++) {
1620
+ const p = points[k - 1], q = points[k];
1621
+ if (p.y === q.y) {
1622
+ 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;
1623
+ } else if (p.x === q.x) {
1624
+ 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;
1625
+ }
1626
+ }
1627
+ return false;
1628
+ }
1629
+ var padded = (r) => ({ x: r.x - MARGIN, y: r.y - MARGIN, width: r.width + 2 * MARGIN, height: r.height + 2 * MARGIN });
1630
+ var inside = (p, r) => r.x < p.x && p.x < r.x + r.width && r.y < p.y && p.y < r.y + r.height;
1631
+ var memo = /* @__PURE__ */ new Map();
1632
+ function avoiding(plain, a, b, startVertical, endVertical, elements, except) {
1633
+ const usable = (r) => !inside(a, padded(r)) && !inside(b, padded(r));
1634
+ const all = [];
1635
+ let crossed = false;
1636
+ for (const e of elements) {
1637
+ if (except.has(e.id)) continue;
1638
+ const r = obstacle(e);
1639
+ if (!r || !usable(r)) continue;
1640
+ all.push(r);
1641
+ if (!crossed && crosses(plain, r)) crossed = true;
1642
+ }
1643
+ if (!crossed) return plain;
1644
+ 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])]);
1645
+ const hit = memo.get(key);
1646
+ if (hit) return hit;
1647
+ const out = route(plain, a, b, startVertical, endVertical, all);
1648
+ if (memo.size >= 512) memo.clear();
1649
+ memo.set(key, out);
1650
+ return out;
1651
+ }
1652
+ function route(plain, a, b, startVertical, endVertical, usable) {
1653
+ let chosen = usable.map((_, i) => i).filter((i) => crosses(plain, usable[i]));
1654
+ if (chosen.length === 0) return plain;
1655
+ for (let round = 0; round < 4; round++) {
1656
+ if (chosen.length > MOST) return plain;
1657
+ const path = grid(a, b, startVertical, endVertical, chosen.map((i) => padded(usable[i])));
1658
+ if (!path) return plain;
1659
+ const more = usable.map((_, i) => i).filter((i) => !chosen.includes(i) && crosses(path, usable[i]));
1660
+ if (more.length === 0) return path;
1661
+ chosen = [...chosen, ...more].sort((p, q) => p - q);
1662
+ }
1663
+ return plain;
1664
+ }
1665
+ function grid(a, b, startVertical, endVertical, boxes) {
1666
+ let xs = [a.x, b.x, (a.x + b.x) / 2], ys = [a.y, b.y, (a.y + b.y) / 2];
1667
+ for (const r of boxes) {
1668
+ xs.push(r.x, r.x + r.width);
1669
+ ys.push(r.y, r.y + r.height);
1670
+ }
1671
+ xs = [...new Set(xs)].sort((p, q) => p - q);
1672
+ ys = [...new Set(ys)].sort((p, q) => p - q);
1673
+ const nx = xs.length, ny = ys.length;
1674
+ const ia = xs.indexOf(a.x), ja = ys.indexOf(a.y), ib = xs.indexOf(b.x), jb = ys.indexOf(b.y);
1675
+ if (ia < 0 || ja < 0 || ib < 0 || jb < 0) return null;
1676
+ const at = new Map(xs.map((x, k) => [x, k])), bt = new Map(ys.map((y, k) => [y, k]));
1677
+ const inner = new Uint8Array(nx * ny), acrossBlocked = new Uint8Array(nx * ny), downBlocked = new Uint8Array(nx * ny);
1678
+ for (const r of boxes) {
1679
+ 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);
1680
+ if (i0 === void 0 || i1 === void 0 || j0 === void 0 || j1 === void 0) continue;
1681
+ for (let i = i0; i < i1; i++) for (let j = j0; j < j1; j++) {
1682
+ if (j > j0) acrossBlocked[i * ny + j] = 1;
1683
+ if (i > i0) downBlocked[i * ny + j] = 1;
1684
+ if (i > i0 && j > j0) inner[i * ny + j] = 1;
1685
+ }
1686
+ }
1687
+ const blocked = (i, j) => inner[i * ny + j] === 1;
1688
+ const across = (i0, i1, j) => acrossBlocked[Math.min(i0, i1) * ny + j] === 1;
1689
+ const down = (j0, j1, i) => downBlocked[i * ny + Math.min(j0, j1)] === 1;
1690
+ const count = nx * ny * 2;
1691
+ const dist = new Array(count).fill(Infinity), prev = new Array(count).fill(-1);
1692
+ const done = new Array(count).fill(false);
1693
+ const heap = new Frontier();
1694
+ const first = (ia * ny + ja) * 2 + (startVertical ? 1 : 0);
1695
+ dist[first] = 0;
1696
+ heap.push(0, first);
1697
+ const want = (ib * ny + jb) * 2 + (endVertical ? 1 : 0);
1698
+ for (let top = heap.pop(); top; top = heap.pop()) {
1699
+ const [cost, s] = top;
1700
+ if (done[s]) continue;
1701
+ done[s] = true;
1702
+ if (s === want) break;
1703
+ const node = Math.floor(s / 2), axis = s % 2, i = Math.floor(node / ny), j = node % ny;
1704
+ for (const [di, dj] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
1705
+ const ni = i + di, nj = j + dj;
1706
+ if (ni < 0 || ni >= nx || nj < 0 || nj >= ny || blocked(ni, nj)) continue;
1707
+ const turn = di !== 0 ? 0 : 1;
1708
+ if (di !== 0 ? across(i, ni, j) : down(j, nj, i)) continue;
1709
+ const length = Math.abs(xs[ni] - xs[i]) + Math.abs(ys[nj] - ys[j]);
1710
+ const c = cost + length + (turn !== axis ? BEND : 0);
1711
+ const t = (ni * ny + nj) * 2 + turn;
1712
+ if (c < dist[t]) {
1713
+ dist[t] = c;
1714
+ prev[t] = s;
1715
+ heap.push(c, t);
1716
+ }
1717
+ }
1718
+ }
1719
+ if (!done[want]) return null;
1720
+ const points = [];
1721
+ for (let s = want; s >= 0; s = prev[s]) {
1722
+ const node = Math.floor(s / 2);
1723
+ points.push({ x: xs[Math.floor(node / ny)], y: ys[node % ny] });
1724
+ }
1725
+ return points.reverse();
1726
+ }
1727
+ var Frontier = class {
1728
+ constructor() {
1729
+ __publicField(this, "heap", []);
1730
+ __publicField(this, "next", 0);
1731
+ }
1732
+ less(i, j) {
1733
+ const a = this.heap[i], b = this.heap[j];
1734
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
1735
+ }
1736
+ push(cost, state) {
1737
+ this.heap.push([cost, this.next++, state]);
1738
+ for (let i = this.heap.length - 1; i > 0; ) {
1739
+ const up = i - 1 >> 1;
1740
+ if (!this.less(i, up)) break;
1741
+ [this.heap[i], this.heap[up]] = [this.heap[up], this.heap[i]];
1742
+ i = up;
1743
+ }
1744
+ }
1745
+ pop() {
1746
+ if (this.heap.length === 0) return null;
1747
+ const top = this.heap[0], last = this.heap.pop();
1748
+ if (this.heap.length > 0) {
1749
+ this.heap[0] = last;
1750
+ for (let i = 0; ; ) {
1751
+ const l = 2 * i + 1, r = l + 1;
1752
+ let m = i;
1753
+ if (l < this.heap.length && this.less(l, m)) m = l;
1754
+ if (r < this.heap.length && this.less(r, m)) m = r;
1755
+ if (m === i) break;
1756
+ [this.heap[i], this.heap[m]] = [this.heap[m], this.heap[i]];
1757
+ i = m;
1758
+ }
1759
+ }
1760
+ return [top[0], top[2]];
1761
+ }
1762
+ };
1763
+
1243
1764
  // src/connector.ts
1244
1765
  function portPoint(shape, port) {
1245
1766
  const r = frame(shape);
@@ -1383,7 +1904,7 @@ function routeThrough(p, c) {
1383
1904
  }
1384
1905
  }
1385
1906
  }
1386
- function route(e, index) {
1907
+ function route2(e, index) {
1387
1908
  const c = e.connector;
1388
1909
  if (!c) return { kind: "polyline", points: [] };
1389
1910
  const [a, b] = endpoints(e, index);
@@ -1399,16 +1920,19 @@ function route(e, index) {
1399
1920
  case "elbow": {
1400
1921
  const startV = vertical(c.start.port) ?? !horizontal;
1401
1922
  const endV = vertical(c.end.port) ?? !horizontal;
1923
+ let plain;
1402
1924
  if (startV && endV) {
1403
1925
  const my = a.y + dy / 2;
1404
- return { kind: "polyline", points: [a, { x: a.x, y: my }, { x: b.x, y: my }, b] };
1405
- }
1406
- if (!startV && !endV) {
1926
+ plain = [a, { x: a.x, y: my }, { x: b.x, y: my }, b];
1927
+ } else if (!startV && !endV) {
1407
1928
  const mx = a.x + dx / 2;
1408
- return { kind: "polyline", points: [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b] };
1409
- }
1410
- if (startV) return { kind: "polyline", points: [a, { x: a.x, y: b.y }, b] };
1411
- return { kind: "polyline", points: [a, { x: b.x, y: a.y }, b] };
1929
+ plain = [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b];
1930
+ } else if (startV) plain = [a, { x: a.x, y: b.y }, b];
1931
+ else plain = [a, { x: b.x, y: a.y }, b];
1932
+ if (c.avoid !== true) return { kind: "polyline", points: plain };
1933
+ const ends = new Set([c.start.elementID, c.end.elementID].filter((x) => x !== null));
1934
+ const routed = avoiding(plain, a, b, startV, endV, index.values(), ends);
1935
+ return { kind: "polyline", points: routed === plain ? plain : simplified(routed) };
1412
1936
  }
1413
1937
  }
1414
1938
  }
@@ -1419,7 +1943,7 @@ function arrowhead(tip, from, size) {
1419
1943
  return [wing(spread), wing(-spread)];
1420
1944
  }
1421
1945
  function connectorBounds(e, index) {
1422
- const pts = routePoints(route(e, index));
1946
+ const pts = routePoints(route2(e, index));
1423
1947
  if (pts.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
1424
1948
  let x0 = pts[0].x, y0 = pts[0].y, x1 = x0, y1 = y0;
1425
1949
  for (const p of pts) {
@@ -1431,7 +1955,7 @@ function connectorBounds(e, index) {
1431
1955
  return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1432
1956
  }
1433
1957
  function midpoint(e, index) {
1434
- const pts = routePoints(route(e, index));
1958
+ const pts = routePoints(route2(e, index));
1435
1959
  if (pts.length <= 1) return pts[0] ?? { x: 0, y: 0 };
1436
1960
  const lengths = [];
1437
1961
  let total = 0;
@@ -1580,7 +2104,7 @@ function graphOf(frame2) {
1580
2104
  graphs.set(els, { members: [...els], graph });
1581
2105
  return graph;
1582
2106
  }
1583
- var Frontier = class {
2107
+ var Frontier2 = class {
1584
2108
  constructor() {
1585
2109
  __publicField(this, "heap", []);
1586
2110
  }
@@ -1634,7 +2158,7 @@ function grow({ shapes: shapes2, next }, from, offline, until) {
1634
2158
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1635
2159
  let counter = 1;
1636
2160
  const tree = new FlowTree(from);
1637
- const open = new Frontier();
2161
+ const open = new Frontier2();
1638
2162
  open.push(from, 0, 0);
1639
2163
  while (open.size > 0) {
1640
2164
  const { id: n, order: o } = open.pop();
@@ -1740,6 +2264,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1740
2264
  return pts;
1741
2265
  };
1742
2266
  const metrics2 = emptyMetrics();
2267
+ const routes = /* @__PURE__ */ new Map();
1743
2268
  for (const e of frame2.elements) {
1744
2269
  if (e.kind === "connector" && !e.connector) continue;
1745
2270
  metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
@@ -1751,7 +2276,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1751
2276
  if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1752
2277
  }
1753
2278
  const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1754
- const route2 = (tree, goal) => {
2279
+ const route3 = (tree, goal) => {
1755
2280
  const hops = [], shapes2 = [];
1756
2281
  let at = goal;
1757
2282
  for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
@@ -1763,7 +2288,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1763
2288
  return { hops: hops.reverse(), shapes: shapes2.reverse() };
1764
2289
  };
1765
2290
  const way = (hops, shapes2) => {
1766
- const points = [], inside = [], stretches = [];
2291
+ const points = [], inside2 = [], stretches = [];
1767
2292
  let length = 0;
1768
2293
  hops.forEach(([id, back], k) => {
1769
2294
  const c = byID.get(id);
@@ -1774,7 +2299,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1774
2299
  if (last) {
1775
2300
  const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1776
2301
  const shape = byID.get(shapes2[k]);
1777
- inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
2302
+ inside2.push([length, length + gap, shape ? delayOf(shape) : 0]);
1778
2303
  length += gap;
1779
2304
  }
1780
2305
  const start2 = length;
@@ -1782,7 +2307,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1782
2307
  stretches.push([id, start2, length]);
1783
2308
  points.push(...line2);
1784
2309
  });
1785
- return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
2310
+ return length > 0 ? { line: new Polyline(points), length, inside: inside2, stretches } : null;
1786
2311
  };
1787
2312
  const along = (w, elapsedAt, speed) => {
1788
2313
  let elapsed = elapsedAt, d = 0;
@@ -1823,6 +2348,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1823
2348
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1824
2349
  const source = byID.get(flow.from);
1825
2350
  const base2 = source ? baseTree(flow.from) : null;
2351
+ routes.set(flow.id, []);
1826
2352
  if (!source || !base2) continue;
1827
2353
  const volume = amountIn(flow, values);
1828
2354
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
@@ -1837,7 +2363,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1837
2363
  const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1838
2364
  const goal2 = tree?.goals(candidates, false)[0];
1839
2365
  if (!tree || goal2 === void 0) break;
1840
- const r = route2(tree, goal2);
2366
+ const r = route3(tree, goal2);
1841
2367
  const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1842
2368
  const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1843
2369
  const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
@@ -1850,7 +2376,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1850
2376
  let at = flow.from;
1851
2377
  const goal = base2.goals(candidates, false)[0];
1852
2378
  if (goal !== void 0) {
1853
- const r = route2(base2, goal), done = full();
2379
+ const r = route3(base2, goal), done = full();
1854
2380
  const sequence = [[r.shapes[0], r.shapes[0]]];
1855
2381
  r.hops.forEach(([id], k) => {
1856
2382
  sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
@@ -1879,6 +2405,13 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1879
2405
  else waiting2.push([at, left]);
1880
2406
  }
1881
2407
  waitingBy.set(flow.id, waiting2);
2408
+ routes.set(flow.id, placements.map((p) => ({
2409
+ goal: p.goal,
2410
+ hops: p.hops,
2411
+ amount: p.amount,
2412
+ waits: p.waits === true,
2413
+ cost: p.hops.reduce((s, [id]) => s + routeCost(byID.get(id)?.connector?.cost ?? null), 0)
2414
+ })));
1882
2415
  for (const p of placements) {
1883
2416
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1884
2417
  m.throughput += p.amount;
@@ -1961,7 +2494,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1961
2494
  const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1962
2495
  const goal = tree?.goals(flow.to, false)[0];
1963
2496
  if (!tree || goal === void 0) break;
1964
- const r = route2(tree, goal);
2497
+ const r = route3(tree, goal);
1965
2498
  const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1966
2499
  const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1967
2500
  const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
@@ -2016,7 +2549,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
2016
2549
  if (c === null || !Number.isFinite(c) || !m) continue;
2017
2550
  m.load = c > 0 ? m.throughput / c : 1;
2018
2551
  }
2019
- return { packets, metrics: metrics2 };
2552
+ return { packets, metrics: metrics2, routes };
2020
2553
  }
2021
2554
  var BLANK = {
2022
2555
  seed: 1,
@@ -2110,16 +2643,16 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
2110
2643
  }
2111
2644
  var gaugeKey = (id) => "gauge|" + id;
2112
2645
  var GAUGE_EASE = 1;
2113
- var hex = (h) => {
2646
+ var hex2 = (h) => {
2114
2647
  const v = parseInt(h.slice(1), 16);
2115
2648
  return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
2116
2649
  };
2117
- var GAUGE_GREEN = hex("#16a34a");
2118
- var GAUGE_AMBER = hex("#d97706");
2119
- var GAUGE_RED = hex("#dc2626");
2120
- var GAUGE_BLUE = hex("#2f6fdf");
2121
- var GAUGE_TRACK = hex("#e5e7eb");
2122
- var GAUGE_GRAY = hex("#9ca3af");
2650
+ var GAUGE_GREEN = hex2("#16a34a");
2651
+ var GAUGE_AMBER = hex2("#d97706");
2652
+ var GAUGE_RED = hex2("#dc2626");
2653
+ var GAUGE_BLUE = hex2("#2f6fdf");
2654
+ var GAUGE_TRACK = hex2("#e5e7eb");
2655
+ var GAUGE_GRAY = hex2("#9ca3af");
2123
2656
  function formatted(metric, v) {
2124
2657
  if (v === null || !Number.isFinite(v)) return "\u2013";
2125
2658
  switch (metric) {
@@ -2405,17 +2938,17 @@ function clusterStrokes(c, angle2, size) {
2405
2938
  });
2406
2939
  }
2407
2940
  function contains(contours, p) {
2408
- let inside = false;
2941
+ let inside2 = false;
2409
2942
  for (const poly of contours) {
2410
2943
  if (poly.length < 3) continue;
2411
2944
  let k = poly.length - 1;
2412
2945
  for (let i = 0; i < poly.length; i++) {
2413
2946
  const a = poly[i], b = poly[k];
2414
- 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;
2947
+ 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;
2415
2948
  k = i;
2416
2949
  }
2417
2950
  }
2418
- return inside;
2951
+ return inside2;
2419
2952
  }
2420
2953
  function distance(p, contours) {
2421
2954
  let best = Infinity;
@@ -2692,7 +3225,7 @@ function connectorParts(e, index, page) {
2692
3225
  const c = e.connector;
2693
3226
  if (!c) return [new Path(), new Path()];
2694
3227
  const [a, b] = endpoints(e, index);
2695
- const r = route(e, index);
3228
+ const r = route2(e, index);
2696
3229
  const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
2697
3230
  const key = [
2698
3231
  e.seed,
@@ -2780,7 +3313,7 @@ function linesBeneath(e, page, index) {
2780
3313
  }
2781
3314
  const count = f.first.get(e.id) ?? f.under.length;
2782
3315
  while (f.lines.length < count) {
2783
- const points = routePoints(route(f.under[f.lines.length], index));
3316
+ const points = routePoints(route2(f.under[f.lines.length], index));
2784
3317
  f.lines.push({ points, box: boxOf(points) });
2785
3318
  }
2786
3319
  return { lines: f.lines, count };
@@ -2794,7 +3327,7 @@ function hopCrossings(e, r, page, index) {
2794
3327
  for (const samples of segments) {
2795
3328
  const along = runningLengths(samples);
2796
3329
  const length = along[along.length - 1];
2797
- const found = [];
3330
+ const found2 = [];
2798
3331
  const near = [];
2799
3332
  const whole = boxOf(samples);
2800
3333
  for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
@@ -2811,12 +3344,12 @@ function hopCrossings(e, r, page, index) {
2811
3344
  if (Math.abs(det) < 1e-9) continue;
2812
3345
  const wx = a.x - p.x, wy = a.y - p.y;
2813
3346
  const t = (wx * ey - wy * ex) / det, s = (wx * dy - wy * dx) / det;
2814
- if (t > 0 && t < 1 && s >= 0 && s <= 1) found.push(along[i] + t * (along[i + 1] - along[i]));
3347
+ if (t > 0 && t < 1 && s >= 0 && s <= 1) found2.push(along[i] + t * (along[i + 1] - along[i]));
2815
3348
  }
2816
3349
  }
2817
3350
  }
2818
3351
  const kept = [];
2819
- for (const at of found.sort((x, y) => x - y)) {
3352
+ for (const at of found2.sort((x, y) => x - y)) {
2820
3353
  if (at < rad + 2 || at > length - rad - 2) continue;
2821
3354
  if (kept.length > 0 && at - kept[kept.length - 1] < 2 * rad + 2) continue;
2822
3355
  kept.push(at);
@@ -3030,7 +3563,7 @@ var lerpColor = (a, b, u) => ({
3030
3563
  b: mix01(a.b, b.b, u),
3031
3564
  a: mix01(a.a, b.a, u)
3032
3565
  });
3033
- function num2(track, t, p, base2) {
3566
+ function num3(track, t, p, base2) {
3034
3567
  return value(track, t, (k) => k[p], base2, lerpNum);
3035
3568
  }
3036
3569
  function isChanging(track, t) {
@@ -3056,20 +3589,20 @@ function isVisible(track, t) {
3056
3589
  function applyTrack(track, e, t) {
3057
3590
  e = cloneElement(e);
3058
3591
  if (track.keys.some((k) => k.reveal !== void 0)) {
3059
- const r = num2(track, t, "reveal", e.reveal ?? 1);
3592
+ const r = num3(track, t, "reveal", e.reveal ?? 1);
3060
3593
  e.reveal = r >= 1 ? null : Math.max(0, r);
3061
3594
  }
3062
3595
  if (track.keys.some((k) => k.opacity !== void 0)) {
3063
- const o = Math.min(Math.max(num2(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3596
+ const o = Math.min(Math.max(num3(track, t, "opacity", e.style.opacity ?? 1), 0), 1);
3064
3597
  e.style.opacity = o >= 1 ? null : o;
3065
3598
  }
3066
3599
  if (e.kind === "connector") return e;
3067
- e.x = num2(track, t, "x", e.x);
3068
- e.y = num2(track, t, "y", e.y);
3069
- e.width = Math.max(0, num2(track, t, "width", e.width));
3070
- e.height = Math.max(0, num2(track, t, "height", e.height));
3071
- if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num2(track, t, "rotation", angle(e));
3072
- e.style.strokeWidth = Math.max(0, num2(track, t, "strokeWidth", e.style.strokeWidth));
3600
+ e.x = num3(track, t, "x", e.x);
3601
+ e.y = num3(track, t, "y", e.y);
3602
+ e.width = Math.max(0, num3(track, t, "width", e.width));
3603
+ e.height = Math.max(0, num3(track, t, "height", e.height));
3604
+ if (track.keys.some((k) => k.rotation !== void 0)) e.rotation = num3(track, t, "rotation", angle(e));
3605
+ e.style.strokeWidth = Math.max(0, num3(track, t, "strokeWidth", e.style.strokeWidth));
3073
3606
  e.style.stroke = value(track, t, (k) => k.stroke, e.style.stroke, lerpColor);
3074
3607
  e.style.fill = value(track, t, (k) => k.fill, e.style.fill, lerpColor);
3075
3608
  return e;
@@ -3084,7 +3617,7 @@ function followSpan(track, t) {
3084
3617
  return f;
3085
3618
  }
3086
3619
  function tracing(e, page, index = indexOf(page)) {
3087
- if (e.kind === "connector") return new Polyline(routePoints(route(e, index)));
3620
+ if (e.kind === "connector") return new Polyline(routePoints(route2(e, index)));
3088
3621
  const clean = cloneElement(e);
3089
3622
  clean.style.roughness = 0;
3090
3623
  const pts = outline(clean).contours()[0] ?? [];
@@ -3145,7 +3678,8 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3145
3678
  }
3146
3679
  const flowing = (page.flows ?? []).length > 0;
3147
3680
  const measuring = flowing || page.elements.some((e) => e.gauge || (e.overrides ?? []).some((o) => mentionsMetric(o.when)));
3148
- const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring ? null : EMPTY_TIMELINE);
3681
+ const fills = values !== null && Object.keys(values).length > 0 && page.elements.some((e) => e.text?.includes("{") || (e.overrides ?? []).some((o) => o.text?.includes("{")));
3682
+ const timeline = page.timeline ?? (off.size === 0 && active.size === 0 && !measuring && !fills ? null : EMPTY_TIMELINE);
3149
3683
  if (!timeline) return { frame: page, metrics: emptyMetrics() };
3150
3684
  t = clampTime(t);
3151
3685
  const tracks = trackMap(timeline);
@@ -3181,6 +3715,10 @@ function posedWithMetrics(page, t, state = "initial", since = null, carried = {}
3181
3715
  const track = tracks.get(e.id);
3182
3716
  let out = track ? applyTrack(track, e, t) : e;
3183
3717
  for (const o of active.get(e.id) ?? []) out = applyOverride(o, out);
3718
+ if (fills && values && out.text) {
3719
+ const text = filling(out.text, values);
3720
+ if (text !== out.text) out = { ...out, text };
3721
+ }
3184
3722
  const list = active.get(e.id);
3185
3723
  if (list) {
3186
3724
  const caps = list.map((o) => o.capacity).filter((c) => c !== null && Number.isFinite(c));
@@ -3364,10 +3902,10 @@ var Expansions = class {
3364
3902
  this.posing = posing;
3365
3903
  }
3366
3904
  };
3367
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo) {
3905
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo2) {
3368
3906
  if (!hasInstances(page)) return page;
3369
3907
  const budget = { left: MAX_ELEMENTS };
3370
- const m = memo && memo.page === page && memo.t === t && memo.posing === posing ? memo : void 0;
3908
+ const m = memo2 && memo2.page === page && memo2.t === t && memo2.posing === posing ? memo2 : void 0;
3371
3909
  return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3372
3910
  }
3373
3911
  function symbolTable(page) {
@@ -3375,7 +3913,7 @@ function symbolTable(page) {
3375
3913
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3376
3914
  return table;
3377
3915
  }
3378
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo) {
3916
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo2) {
3379
3917
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3380
3918
  if (instances.size === 0) {
3381
3919
  budget.left -= elements.length;
@@ -3401,7 +3939,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3401
3939
  continue;
3402
3940
  }
3403
3941
  const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3404
- const was = memo?.made.get(e);
3942
+ const was = memo2?.made.get(e);
3405
3943
  if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3406
3944
  budget.left -= was.before - was.after;
3407
3945
  for (const x of was.out) out.push(x);
@@ -3423,7 +3961,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3423
3961
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3424
3962
  };
3425
3963
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3426
- if (memo && stack.length === 0 && !was) memo.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3964
+ if (memo2 && stack.length === 0 && !was) memo2.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3427
3965
  }
3428
3966
  return out;
3429
3967
  }
@@ -3476,8 +4014,8 @@ function mix2(x) {
3476
4014
  return z ^ z >> 31n;
3477
4015
  }
3478
4016
  function halves(id) {
3479
- const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3480
- return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
4017
+ const hex3 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
4018
+ return [BigInt(`0x${hex3.slice(0, 16)}`), BigInt(`0x${hex3.slice(16)}`)];
3481
4019
  }
3482
4020
  function derivedID(instance, inner) {
3483
4021
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -3498,7 +4036,12 @@ function clickTargets(page, file) {
3498
4036
  let pageOf = null;
3499
4037
  for (const e of page.elements) {
3500
4038
  const c = e.click;
3501
- const state = c && changesState(c) ? { ...c.set ? { set: c.set } : {}, ...c.toggle ? { toggle: c.toggle } : {}, ...c.reset ? { reset: true } : {} } : null;
4039
+ const state = c && changesState(c) ? {
4040
+ ...c.set ? { set: c.set } : {},
4041
+ ...c.toggle ? { toggle: c.toggle } : {},
4042
+ ...c.reset ? { reset: true } : {},
4043
+ ...c.run ? { run: c.run } : {}
4044
+ } : null;
3502
4045
  if (c?.scene) {
3503
4046
  const i = sceneNamed(c.scene, markers);
3504
4047
  if (i >= 0) {
@@ -3805,12 +4348,23 @@ function inline(src, style2 = {}) {
3805
4348
  i += m[0].length;
3806
4349
  continue;
3807
4350
  }
3808
- if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4351
+ if (m = /^(\*\*\*|___)(?=\S)([\s\S]*?\S)\1(?!\*|_)/.exec(rest)) {
3809
4352
  emit();
3810
- out.push(...inline(m[2], { ...style2, bold: true }));
4353
+ out.push(...inline(m[2], { ...style2, bold: true, italic: true }));
3811
4354
  i += m[0].length;
3812
4355
  continue;
3813
4356
  }
4357
+ if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4358
+ let body = m[2], length = m[0].length;
4359
+ if (m[1] === "**" && rest[length] === "*" && (body.replace(/\*\*/g, "").match(/\*/g) ?? []).length % 2 === 1) {
4360
+ body += "*";
4361
+ length++;
4362
+ }
4363
+ emit();
4364
+ out.push(...inline(body, { ...style2, bold: true }));
4365
+ i += length;
4366
+ continue;
4367
+ }
3814
4368
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
3815
4369
  emit();
3816
4370
  out.push(...inline(m[2], { ...style2, italic: true }));
@@ -4194,20 +4748,6 @@ function sameStyle(a, b) {
4194
4748
  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;
4195
4749
  }
4196
4750
 
4197
- // src/theme.ts
4198
- var rgb = (r, g, b) => ({ r, g, b, a: 1 });
4199
- var themes = [
4200
- { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13) },
4201
- { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13) },
4202
- { id: "sepia", name: "Sepia", background: rgb(0.953, 0.914, 0.824), ink: rgb(0.23, 0.18, 0.13) },
4203
- { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984) },
4204
- { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918) },
4205
- { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94) }
4206
- ];
4207
- function themeNamed(id) {
4208
- return themes.find((t) => t.id === id) ?? null;
4209
- }
4210
-
4211
4751
  // src/svg.ts
4212
4752
  function bounds(page, padding = 24) {
4213
4753
  if (hasInstances(page)) return bounds(expandingSymbols(page, 0), padding);
@@ -4273,7 +4813,7 @@ function svgOpen(b, background, paint) {
4273
4813
  }
4274
4814
  function svgParts(page, background, fixed, links = false, only) {
4275
4815
  if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
4276
- const theme = themeNamed(page.theme);
4816
+ const theme = pageTheme(page);
4277
4817
  const bg = background ? theme?.background ?? background : null;
4278
4818
  const ink = theme?.ink ?? black;
4279
4819
  const d = { ...page, elements: page.elements.map((e) => resolvingInk(e, ink)) };
@@ -4309,7 +4849,7 @@ ${body}</a>
4309
4849
  }
4310
4850
  function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
4311
4851
  const { bounds: b, parts } = svgParts(page, background, fixed, true);
4312
- return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4852
+ return svgOpen(b, pageTheme(page)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4313
4853
  }
4314
4854
  function labelRect(e, index) {
4315
4855
  const m = midpoint(e, index);
@@ -4403,7 +4943,7 @@ function svgElement(e, index, labelBackground, page) {
4403
4943
  }
4404
4944
  s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
4405
4945
  `;
4406
- if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" ? pathLabelBox(e) : frame(e), e, true);
4946
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" || e.kind === "rectangle" ? pathLabelBox(e) : frame(e), e, true);
4407
4947
  return s;
4408
4948
  }
4409
4949
  default:
@@ -4539,6 +5079,40 @@ function servePlans() {
4539
5079
  });
4540
5080
  }
4541
5081
 
5082
+ // src/logic-loader.ts
5083
+ var scriptSrc = typeof document !== "undefined" ? document.currentScript?.src ?? null : null;
5084
+ var found = null;
5085
+ var present = () => globalThis.DottieLogic;
5086
+ function logicEngine() {
5087
+ found ?? (found = (async () => {
5088
+ if (!present()) {
5089
+ const here = import.meta.url;
5090
+ const base2 = here ?? scriptSrc;
5091
+ if (!base2) throw new Error("No dottie-logic.min.js on the page, and no address to find it from");
5092
+ const url = new URL("./dottie-logic.min.js", base2).href;
5093
+ if (here) await import(
5094
+ /* @vite-ignore */
5095
+ url
5096
+ );
5097
+ else await new Promise((resolve2, reject) => {
5098
+ const s = document.createElement("script");
5099
+ s.src = url;
5100
+ s.onload = () => resolve2();
5101
+ s.onerror = () => reject(new Error(`Couldn't load ${url}`));
5102
+ document.head.append(s);
5103
+ });
5104
+ }
5105
+ const engine = present();
5106
+ if (!engine) throw new Error("dottie-logic.min.js didn't load");
5107
+ await engine.loadLogic();
5108
+ return engine;
5109
+ })());
5110
+ found.catch(() => {
5111
+ found = null;
5112
+ });
5113
+ return found;
5114
+ }
5115
+
4542
5116
  // src/player.ts
4543
5117
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4544
5118
  function pageIndex(file, spec) {
@@ -4646,7 +5220,7 @@ var Playback = class {
4646
5220
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4647
5221
  this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4648
5222
  this.state = { ...page.variables ?? {} };
4649
- const theme = themeNamed(page.theme);
5223
+ const theme = pageTheme(page);
4650
5224
  this.background = theme?.background ?? WHITE;
4651
5225
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
4652
5226
  const ns = "http://www.w3.org/2000/svg";
@@ -4880,6 +5454,9 @@ var DottiePlayer = class extends ElementBase {
4880
5454
  __publicField(this, "stillTime", null);
4881
5455
  __publicField(this, "visible", true);
4882
5456
  __publicField(this, "observer", null);
5457
+ /** Runs handler `handler` from the page's logic for a click on `id` (DottieDocument.clicked). Clicks take turns, so
5458
+ * each handler sees the state the one before left. */
5459
+ __publicField(this, "logicQueue", Promise.resolve());
4883
5460
  this.root = this.attachShadow({ mode: "open" });
4884
5461
  this.root.innerHTML = `<style>${STYLE}</style><div class="stage" part="stage" role="img"></div>
4885
5462
  <div class="bar" part="controls" hidden>
@@ -5106,7 +5683,9 @@ var DottiePlayer = class extends ElementBase {
5106
5683
  this.scheduleReplay();
5107
5684
  }
5108
5685
  if (!click) return;
5109
- if (click.set || click.toggle || click.reset) {
5686
+ if (click.run && p.page.logic && this.hasAttribute("allow-logic")) {
5687
+ this.runLogic(click.run, id, click.reset === true);
5688
+ } else if (click.set || click.toggle || click.reset) {
5110
5689
  this.setState(applyClick(
5111
5690
  { scene: null, url: null, set: click.set ?? null, toggle: click.toggle ?? null, reset: click.reset ?? null },
5112
5691
  p.state,
@@ -5128,6 +5707,35 @@ var DottiePlayer = class extends ElementBase {
5128
5707
  }
5129
5708
  }
5130
5709
  }
5710
+ runLogic(handler, id, reset) {
5711
+ const p = this.playback;
5712
+ if (!p) return;
5713
+ const e = p.page.elements.find((x) => x.id === id);
5714
+ const s = p.sceneAt(p.frame);
5715
+ const event = {
5716
+ id,
5717
+ anchor: e?.anchor ?? null,
5718
+ text: e?.text?.split("\n")[0] ?? null,
5719
+ time: this.currentTime,
5720
+ scene: s < 0 ? null : p.sceneNames[s]
5721
+ };
5722
+ this.logicQueue = this.logicQueue.then(async () => {
5723
+ const engine = await logicEngine();
5724
+ const now = this.playback;
5725
+ if (!now || now !== p || !now.page.logic) return;
5726
+ let start2 = now.state;
5727
+ if (reset) {
5728
+ start2 = {};
5729
+ for (const k of Object.keys(now.state)) start2[k] = false;
5730
+ Object.assign(start2, now.page.variables ?? {});
5731
+ }
5732
+ const r = engine.runLogic(now.page.logic, handler, start2, event);
5733
+ if (r.ok) this.setState({ ...start2, ...r.set });
5734
+ else this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: r.error } }));
5735
+ }).catch((err) => {
5736
+ this.dispatchEvent(new CustomEvent("logicerror", { detail: { element: id, handler, error: err.message } }));
5737
+ });
5738
+ }
5131
5739
  /** The page's variables now. */
5132
5740
  get variables() {
5133
5741
  return { ...this.playback?.state ?? {} };
@@ -5234,7 +5842,7 @@ var DottiePlayer = class extends ElementBase {
5234
5842
  if (handled) e.preventDefault();
5235
5843
  }
5236
5844
  };
5237
- __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label"]);
5845
+ __publicField(DottiePlayer, "observedAttributes", ["src", "page", "at", "clean", "no-controls", "label", "allow-logic"]);
5238
5846
  function clickable(target) {
5239
5847
  const el = target;
5240
5848
  return el && typeof el.closest === "function" ? el.closest(".dottie-a") : null;
@@ -5300,7 +5908,7 @@ function inspectFrame(page, t) {
5300
5908
  const [a, b] = endpoints(e, index);
5301
5909
  d.start = pt(a);
5302
5910
  d.end = pt(b);
5303
- d.route = routePoints(route(e, index)).map(pt);
5911
+ d.route = routePoints(route2(e, index)).map(pt);
5304
5912
  }
5305
5913
  return d;
5306
5914
  });
@@ -5339,7 +5947,7 @@ function render(source, options = {}) {
5339
5947
  function isFile(v) {
5340
5948
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
5341
5949
  }
5342
- var VERSION = "0.9.0";
5950
+ var VERSION = "0.11.0";
5343
5951
 
5344
5952
  // src/player-entry.ts
5345
5953
  servePlans();
@@ -5380,6 +5988,8 @@ export {
5380
5988
  luminance,
5381
5989
  orderedElements,
5382
5990
  pageIndex,
5991
+ pageTheme,
5992
+ paletteColor,
5383
5993
  parseDottie,
5384
5994
  plan,
5385
5995
  posterTime,