@phin-tech/dottie 0.8.0 → 0.10.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.8.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.10.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,
@@ -197,19 +199,80 @@ function trim(s) {
197
199
  return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
198
200
  }
199
201
 
202
+ // src/theme.ts
203
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
204
+ function hex(h) {
205
+ const n = parseInt(h.slice(1), 16);
206
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
207
+ }
208
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
209
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
210
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
211
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
212
+ var themes = [
213
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
214
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
215
+ {
216
+ id: "sepia",
217
+ name: "Sepia",
218
+ background: rgb(0.953, 0.914, 0.824),
219
+ ink: rgb(0.23, 0.18, 0.13),
220
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
221
+ },
222
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
223
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
224
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
225
+ ];
226
+ function themeNamed(id) {
227
+ return themes.find((t) => t.id === id) ?? null;
228
+ }
229
+ function pageTheme(page) {
230
+ const c = page.customTheme;
231
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
232
+ return themeNamed(page.theme);
233
+ }
234
+ function paletteColor(page, name) {
235
+ const n = name.trim().toLowerCase();
236
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
237
+ }
238
+ function resolvingPalette(page) {
239
+ const named = (e) => !!e.style.named;
240
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
241
+ const resolve2 = (e) => {
242
+ const n = e.style.named;
243
+ if (!n) return e;
244
+ const style2 = { ...e.style };
245
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
246
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
247
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
248
+ if (f) style2.fill = { ...f };
249
+ if (s) style2.stroke = { ...s };
250
+ if (t) style2.textColor = { ...t };
251
+ return { ...e, style: style2 };
252
+ };
253
+ return {
254
+ ...page,
255
+ elements: page.elements.map(resolve2),
256
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve2) })) : page.symbols
257
+ };
258
+ }
259
+
200
260
  // src/model.ts
201
261
  var SUPPORTED_VERSION = 2;
202
262
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
203
263
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
204
264
  var KNOWN_FIELDS = {
205
265
  file: ["version", "pages", "images"],
206
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
266
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
207
267
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
208
268
  flowOverride: ["when", "volume"],
209
269
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
210
270
  layer: ["id", "name", "visible", "locked"],
211
271
  rgba: ["r", "g", "b", "a"],
212
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
272
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
273
+ colorNames: ["fill", "stroke", "textColor"],
274
+ customTheme: ["name", "background", "ink", "palette", "base"],
275
+ paletteColor: ["name", "color"],
213
276
  element: [
214
277
  "id",
215
278
  "kind",
@@ -247,8 +310,11 @@ var KNOWN_FIELDS = {
247
310
  "capacity",
248
311
  "delay",
249
312
  "queue",
250
- "gauge"
313
+ "gauge",
314
+ "containerID",
315
+ "container"
251
316
  ],
317
+ container: ["fixed", "lanes"],
252
318
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
253
319
  strokePoint: ["x", "y"],
254
320
  subpath: ["closed", "coords"],
@@ -257,7 +323,7 @@ var KNOWN_FIELDS = {
257
323
  extensionRef: ["id", "shape", "version", "params"],
258
324
  normalizedPoint: ["x", "y"],
259
325
  normalizedRect: ["x", "y", "width", "height"],
260
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
326
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
261
327
  waypoint: ["x", "y"],
262
328
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
263
329
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -344,9 +410,36 @@ var Reader = class {
344
410
  gridSize: num(s.gridSize) ?? null,
345
411
  opacity: num(s.opacity) ?? null,
346
412
  dashOffset: num(s.dashOffset) ?? null,
347
- textColor: rgba(s.textColor) ?? null
413
+ textColor: rgba(s.textColor) ?? null,
414
+ named: this.colorNames(s.named)
348
415
  };
349
416
  }
417
+ container(v) {
418
+ if (!isObj(v)) return null;
419
+ this.fields(v, "container");
420
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
421
+ }
422
+ colorNames(v) {
423
+ if (!isObj(v)) return null;
424
+ this.fields(v, "colorNames");
425
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
426
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
427
+ }
428
+ customTheme(v) {
429
+ if (!isObj(v)) return null;
430
+ this.fields(v, "customTheme");
431
+ const background = rgba(v.background), ink = rgba(v.ink);
432
+ if (!background || !ink) {
433
+ this.warn("A custom theme needs a background and an ink; drawn without it");
434
+ return null;
435
+ }
436
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
437
+ this.fields(p, "paletteColor");
438
+ const name = str(p.name), color2 = rgba(p.color);
439
+ return name && color2 ? [{ name, color: color2 }] : [];
440
+ });
441
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
442
+ }
350
443
  endpoint(v) {
351
444
  const e = isObj(v) ? v : {};
352
445
  const port = str(e.port);
@@ -378,6 +471,7 @@ var Reader = class {
378
471
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
379
472
  }) : null,
380
473
  hops: typeof v.hops === "boolean" ? v.hops : null,
474
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
381
475
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
382
476
  };
383
477
  }
@@ -446,7 +540,9 @@ var Reader = class {
446
540
  capacity: num(v.capacity) ?? null,
447
541
  delay: num(v.delay) ?? null,
448
542
  queue: v.queue === true ? true : null,
449
- gauge: this.gauge(v.gauge)
543
+ gauge: this.gauge(v.gauge),
544
+ containerID: uuid(v.containerID) ?? null,
545
+ container: this.container(v.container)
450
546
  };
451
547
  }
452
548
  gauge(v) {
@@ -609,15 +705,15 @@ var Reader = class {
609
705
  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
706
  stableSortBy(keys, (k) => k.t);
611
707
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
612
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
613
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
614
- return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
708
+ const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
709
+ if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
710
+ 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 }];
615
711
  }) : null;
616
712
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
617
- const start = num(r.start), end = num(r.end);
618
- if (start === void 0 || end === void 0) return [];
713
+ const start2 = num(r.start), end = num(r.end);
714
+ if (start2 === void 0 || end === void 0) return [];
619
715
  const count = num(r.count);
620
- const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
716
+ const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
621
717
  return repeatIsValid(rep) ? [rep] : [];
622
718
  }) : null;
623
719
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -667,6 +763,7 @@ var Reader = class {
667
763
  layers,
668
764
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
669
765
  theme: str(v.theme) ?? null,
766
+ customTheme: this.customTheme(v.customTheme),
670
767
  timeline: this.timeline(v.timeline),
671
768
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
672
769
  variables: this.variables(v.variables),
@@ -730,7 +827,7 @@ function parseDottie(source) {
730
827
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
731
828
  }
732
829
  }
733
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
830
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
734
831
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
735
832
  return { version, pages, warnings: reader.warnings };
736
833
  }
@@ -750,7 +847,8 @@ function cloneElement(e) {
750
847
  ...e.style,
751
848
  stroke: { ...e.style.stroke },
752
849
  fill: { ...e.style.fill },
753
- textColor: e.style.textColor ? { ...e.style.textColor } : null
850
+ textColor: e.style.textColor ? { ...e.style.textColor } : null,
851
+ named: e.style.named ? { ...e.style.named } : e.style.named
754
852
  },
755
853
  connector: e.connector ? {
756
854
  ...e.connector,
@@ -842,6 +940,21 @@ function pieces(para) {
842
940
  if (cur || out.length === 0) out.push(cur);
843
941
  return out;
844
942
  }
943
+ function prefixWidths(chars, fontSize, face, font) {
944
+ const fam = familyMetrics(font);
945
+ const f = fam.faces[face] ?? fam.faces.regular;
946
+ const fallback = f.w["n"];
947
+ const out = [0];
948
+ let w = 0;
949
+ let prev = "";
950
+ for (const ch of chars) {
951
+ w += f.w[ch] ?? fallback;
952
+ if (prev) w += f.k[prev + ch] ?? 0;
953
+ prev = ch;
954
+ out.push(w * fontSize / 1e3);
955
+ }
956
+ return out;
957
+ }
845
958
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
846
959
  const width = (t) => measure(t, fontSize, face, font);
847
960
  const out = [];
@@ -857,8 +970,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
857
970
  }
858
971
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
859
972
  const chars = [...line2];
973
+ const prefix = prefixWidths(chars, fontSize, face, font);
860
974
  let n = chars.length - 1;
861
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
975
+ while (n > 1 && prefix[n] > maxWidth) n--;
862
976
  out.push(chars.slice(0, n).join(""));
863
977
  line2 = chars.slice(n).join("");
864
978
  }
@@ -1013,13 +1127,13 @@ var Path = class _Path {
1013
1127
  contours() {
1014
1128
  const out = [];
1015
1129
  let current = [];
1016
- let start = { x: 0, y: 0 };
1130
+ let start2 = { x: 0, y: 0 };
1017
1131
  for (const s of this.segments) {
1018
- const last = current.length ? current[current.length - 1] : start;
1132
+ const last = current.length ? current[current.length - 1] : start2;
1019
1133
  switch (s.op) {
1020
1134
  case "M":
1021
1135
  if (current.length > 1) out.push(current);
1022
- start = s.p;
1136
+ start2 = s.p;
1023
1137
  current = [s.p];
1024
1138
  break;
1025
1139
  case "L":
@@ -1041,9 +1155,9 @@ var Path = class _Path {
1041
1155
  }
1042
1156
  break;
1043
1157
  case "Z":
1044
- current.push(start);
1158
+ current.push(start2);
1045
1159
  if (current.length > 1) out.push(current);
1046
- current = [start];
1160
+ current = [start2];
1047
1161
  break;
1048
1162
  }
1049
1163
  }
@@ -1224,6 +1338,165 @@ function shapePath(e) {
1224
1338
  return path;
1225
1339
  }
1226
1340
 
1341
+ // src/elbow.ts
1342
+ var MARGIN = 16;
1343
+ var BEND = 24;
1344
+ var SMALLEST = 24;
1345
+ var MOST = 48;
1346
+ function obstacle(e) {
1347
+ if (e.kind === "connector" || e.kind === "text" || e.kind === "freehand") return null;
1348
+ if (e.kind === "path" && !e.extension) return null;
1349
+ if (e.style.opacity !== null && e.style.opacity <= 0) return null;
1350
+ const r = rotatedBounds(e);
1351
+ return r.width < SMALLEST || r.height < SMALLEST ? null : r;
1352
+ }
1353
+ function crosses(points, r) {
1354
+ const x1 = r.x + r.width, y1 = r.y + r.height;
1355
+ for (let k = 1; k < points.length; k++) {
1356
+ const p = points[k - 1], q = points[k];
1357
+ if (p.y === q.y) {
1358
+ 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;
1359
+ } else if (p.x === q.x) {
1360
+ 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;
1361
+ }
1362
+ }
1363
+ return false;
1364
+ }
1365
+ var padded = (r) => ({ x: r.x - MARGIN, y: r.y - MARGIN, width: r.width + 2 * MARGIN, height: r.height + 2 * MARGIN });
1366
+ var inside = (p, r) => r.x < p.x && p.x < r.x + r.width && r.y < p.y && p.y < r.y + r.height;
1367
+ var memo = /* @__PURE__ */ new Map();
1368
+ function avoiding(plain, a, b, startVertical, endVertical, elements, except) {
1369
+ const usable = (r) => !inside(a, padded(r)) && !inside(b, padded(r));
1370
+ const all = [];
1371
+ let crossed = false;
1372
+ for (const e of elements) {
1373
+ if (except.has(e.id)) continue;
1374
+ const r = obstacle(e);
1375
+ if (!r || !usable(r)) continue;
1376
+ all.push(r);
1377
+ if (!crossed && crosses(plain, r)) crossed = true;
1378
+ }
1379
+ if (!crossed) return plain;
1380
+ 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])]);
1381
+ const hit = memo.get(key);
1382
+ if (hit) return hit;
1383
+ const out = route(plain, a, b, startVertical, endVertical, all);
1384
+ if (memo.size >= 512) memo.clear();
1385
+ memo.set(key, out);
1386
+ return out;
1387
+ }
1388
+ function route(plain, a, b, startVertical, endVertical, usable) {
1389
+ let chosen = usable.map((_, i) => i).filter((i) => crosses(plain, usable[i]));
1390
+ if (chosen.length === 0) return plain;
1391
+ for (let round = 0; round < 4; round++) {
1392
+ if (chosen.length > MOST) return plain;
1393
+ const path = grid(a, b, startVertical, endVertical, chosen.map((i) => padded(usable[i])));
1394
+ if (!path) return plain;
1395
+ const more = usable.map((_, i) => i).filter((i) => !chosen.includes(i) && crosses(path, usable[i]));
1396
+ if (more.length === 0) return path;
1397
+ chosen = [...chosen, ...more].sort((p, q) => p - q);
1398
+ }
1399
+ return plain;
1400
+ }
1401
+ function grid(a, b, startVertical, endVertical, boxes) {
1402
+ let xs = [a.x, b.x, (a.x + b.x) / 2], ys = [a.y, b.y, (a.y + b.y) / 2];
1403
+ for (const r of boxes) {
1404
+ xs.push(r.x, r.x + r.width);
1405
+ ys.push(r.y, r.y + r.height);
1406
+ }
1407
+ xs = [...new Set(xs)].sort((p, q) => p - q);
1408
+ ys = [...new Set(ys)].sort((p, q) => p - q);
1409
+ const nx = xs.length, ny = ys.length;
1410
+ const ia = xs.indexOf(a.x), ja = ys.indexOf(a.y), ib = xs.indexOf(b.x), jb = ys.indexOf(b.y);
1411
+ if (ia < 0 || ja < 0 || ib < 0 || jb < 0) return null;
1412
+ const at = new Map(xs.map((x, k) => [x, k])), bt = new Map(ys.map((y, k) => [y, k]));
1413
+ const inner = new Uint8Array(nx * ny), acrossBlocked = new Uint8Array(nx * ny), downBlocked = new Uint8Array(nx * ny);
1414
+ for (const r of boxes) {
1415
+ 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);
1416
+ if (i0 === void 0 || i1 === void 0 || j0 === void 0 || j1 === void 0) continue;
1417
+ for (let i = i0; i < i1; i++) for (let j = j0; j < j1; j++) {
1418
+ if (j > j0) acrossBlocked[i * ny + j] = 1;
1419
+ if (i > i0) downBlocked[i * ny + j] = 1;
1420
+ if (i > i0 && j > j0) inner[i * ny + j] = 1;
1421
+ }
1422
+ }
1423
+ const blocked = (i, j) => inner[i * ny + j] === 1;
1424
+ const across = (i0, i1, j) => acrossBlocked[Math.min(i0, i1) * ny + j] === 1;
1425
+ const down = (j0, j1, i) => downBlocked[i * ny + Math.min(j0, j1)] === 1;
1426
+ const count = nx * ny * 2;
1427
+ const dist = new Array(count).fill(Infinity), prev = new Array(count).fill(-1);
1428
+ const done = new Array(count).fill(false);
1429
+ const heap = new Frontier();
1430
+ const first = (ia * ny + ja) * 2 + (startVertical ? 1 : 0);
1431
+ dist[first] = 0;
1432
+ heap.push(0, first);
1433
+ const want = (ib * ny + jb) * 2 + (endVertical ? 1 : 0);
1434
+ for (let top = heap.pop(); top; top = heap.pop()) {
1435
+ const [cost, s] = top;
1436
+ if (done[s]) continue;
1437
+ done[s] = true;
1438
+ if (s === want) break;
1439
+ const node = Math.floor(s / 2), axis = s % 2, i = Math.floor(node / ny), j = node % ny;
1440
+ for (const [di, dj] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
1441
+ const ni = i + di, nj = j + dj;
1442
+ if (ni < 0 || ni >= nx || nj < 0 || nj >= ny || blocked(ni, nj)) continue;
1443
+ const turn = di !== 0 ? 0 : 1;
1444
+ if (di !== 0 ? across(i, ni, j) : down(j, nj, i)) continue;
1445
+ const length = Math.abs(xs[ni] - xs[i]) + Math.abs(ys[nj] - ys[j]);
1446
+ const c = cost + length + (turn !== axis ? BEND : 0);
1447
+ const t = (ni * ny + nj) * 2 + turn;
1448
+ if (c < dist[t]) {
1449
+ dist[t] = c;
1450
+ prev[t] = s;
1451
+ heap.push(c, t);
1452
+ }
1453
+ }
1454
+ }
1455
+ if (!done[want]) return null;
1456
+ const points = [];
1457
+ for (let s = want; s >= 0; s = prev[s]) {
1458
+ const node = Math.floor(s / 2);
1459
+ points.push({ x: xs[Math.floor(node / ny)], y: ys[node % ny] });
1460
+ }
1461
+ return points.reverse();
1462
+ }
1463
+ var Frontier = class {
1464
+ constructor() {
1465
+ __publicField(this, "heap", []);
1466
+ __publicField(this, "next", 0);
1467
+ }
1468
+ less(i, j) {
1469
+ const a = this.heap[i], b = this.heap[j];
1470
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
1471
+ }
1472
+ push(cost, state) {
1473
+ this.heap.push([cost, this.next++, state]);
1474
+ for (let i = this.heap.length - 1; i > 0; ) {
1475
+ const up = i - 1 >> 1;
1476
+ if (!this.less(i, up)) break;
1477
+ [this.heap[i], this.heap[up]] = [this.heap[up], this.heap[i]];
1478
+ i = up;
1479
+ }
1480
+ }
1481
+ pop() {
1482
+ if (this.heap.length === 0) return null;
1483
+ const top = this.heap[0], last = this.heap.pop();
1484
+ if (this.heap.length > 0) {
1485
+ this.heap[0] = last;
1486
+ for (let i = 0; ; ) {
1487
+ const l = 2 * i + 1, r = l + 1;
1488
+ let m = i;
1489
+ if (l < this.heap.length && this.less(l, m)) m = l;
1490
+ if (r < this.heap.length && this.less(r, m)) m = r;
1491
+ if (m === i) break;
1492
+ [this.heap[i], this.heap[m]] = [this.heap[m], this.heap[i]];
1493
+ i = m;
1494
+ }
1495
+ }
1496
+ return [top[0], top[2]];
1497
+ }
1498
+ };
1499
+
1227
1500
  // src/connector.ts
1228
1501
  function portPoint(shape, port) {
1229
1502
  const r = frame(shape);
@@ -1367,7 +1640,7 @@ function routeThrough(p, c) {
1367
1640
  }
1368
1641
  }
1369
1642
  }
1370
- function route(e, index) {
1643
+ function route2(e, index) {
1371
1644
  const c = e.connector;
1372
1645
  if (!c) return { kind: "polyline", points: [] };
1373
1646
  const [a, b] = endpoints(e, index);
@@ -1383,16 +1656,19 @@ function route(e, index) {
1383
1656
  case "elbow": {
1384
1657
  const startV = vertical(c.start.port) ?? !horizontal;
1385
1658
  const endV = vertical(c.end.port) ?? !horizontal;
1659
+ let plain;
1386
1660
  if (startV && endV) {
1387
1661
  const my = a.y + dy / 2;
1388
- return { kind: "polyline", points: [a, { x: a.x, y: my }, { x: b.x, y: my }, b] };
1389
- }
1390
- if (!startV && !endV) {
1662
+ plain = [a, { x: a.x, y: my }, { x: b.x, y: my }, b];
1663
+ } else if (!startV && !endV) {
1391
1664
  const mx = a.x + dx / 2;
1392
- return { kind: "polyline", points: [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b] };
1393
- }
1394
- if (startV) return { kind: "polyline", points: [a, { x: a.x, y: b.y }, b] };
1395
- return { kind: "polyline", points: [a, { x: b.x, y: a.y }, b] };
1665
+ plain = [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b];
1666
+ } else if (startV) plain = [a, { x: a.x, y: b.y }, b];
1667
+ else plain = [a, { x: b.x, y: a.y }, b];
1668
+ if (c.avoid !== true) return { kind: "polyline", points: plain };
1669
+ const ends = new Set([c.start.elementID, c.end.elementID].filter((x) => x !== null));
1670
+ const routed = avoiding(plain, a, b, startV, endV, index.values(), ends);
1671
+ return { kind: "polyline", points: routed === plain ? plain : simplified(routed) };
1396
1672
  }
1397
1673
  }
1398
1674
  }
@@ -1403,7 +1679,7 @@ function arrowhead(tip, from, size) {
1403
1679
  return [wing(spread), wing(-spread)];
1404
1680
  }
1405
1681
  function connectorBounds(e, index) {
1406
- const pts = routePoints(route(e, index));
1682
+ const pts = routePoints(route2(e, index));
1407
1683
  if (pts.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
1408
1684
  let x0 = pts[0].x, y0 = pts[0].y, x1 = x0, y1 = y0;
1409
1685
  for (const p of pts) {
@@ -1415,7 +1691,7 @@ function connectorBounds(e, index) {
1415
1691
  return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1416
1692
  }
1417
1693
  function midpoint(e, index) {
1418
- const pts = routePoints(route(e, index));
1694
+ const pts = routePoints(route2(e, index));
1419
1695
  if (pts.length <= 1) return pts[0] ?? { x: 0, y: 0 };
1420
1696
  const lengths = [];
1421
1697
  let total = 0;
@@ -1435,9 +1711,17 @@ function midpoint(e, index) {
1435
1711
  }
1436
1712
  return pts[0];
1437
1713
  }
1714
+ var indexes = /* @__PURE__ */ new WeakMap();
1715
+ function isIndexOf(page, index) {
1716
+ return indexes.get(page.elements)?.index === index;
1717
+ }
1438
1718
  function indexOf(page) {
1719
+ const els = page.elements;
1720
+ const hit = indexes.get(els);
1721
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.index;
1439
1722
  const m = /* @__PURE__ */ new Map();
1440
- for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
1723
+ for (const e of els) if (!m.has(e.id)) m.set(e.id, e);
1724
+ indexes.set(els, { members: [...els], index: m });
1441
1725
  return m;
1442
1726
  }
1443
1727
 
@@ -1520,43 +1804,112 @@ function packetID(flow, k) {
1520
1804
  return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1521
1805
  }
1522
1806
  var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1523
- function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1524
- const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1525
- if (!shapes2.has(from)) return null;
1526
- const next = /* @__PURE__ */ new Map();
1527
- const add2 = (a, edge) => {
1528
- const list = next.get(a);
1529
- if (list) list.push(edge);
1530
- else next.set(a, [edge]);
1531
- };
1532
- for (const e of frame2.elements) {
1807
+ var graphs = /* @__PURE__ */ new WeakMap();
1808
+ var networks = /* @__PURE__ */ new Map();
1809
+ function graphOf(frame2) {
1810
+ const els = frame2.elements;
1811
+ const hit = graphs.get(els);
1812
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.graph;
1813
+ const shapes2 = new Set(els.filter((e) => e.kind !== "connector").map((e) => e.id));
1814
+ const lines = [];
1815
+ for (const e of els) {
1533
1816
  const c = e.connector;
1534
- const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1535
- if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1536
- const cost = routeCost(c.cost);
1537
- add2(a, { to: b, via: e.id, back: false, cost });
1538
- if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1817
+ if (c) lines.push([e.id, c.start.elementID, c.end.elementID, String(routeCost(c.cost)), c.startHead !== "none" ? "both" : "one"]);
1818
+ }
1819
+ const key = JSON.stringify([[...shapes2], lines]);
1820
+ let graph = networks.get(key);
1821
+ if (!graph) {
1822
+ const next = /* @__PURE__ */ new Map();
1823
+ const add2 = (a, edge) => {
1824
+ const list = next.get(a);
1825
+ if (list) list.push(edge);
1826
+ else next.set(a, [edge]);
1827
+ };
1828
+ for (const e of els) {
1829
+ const c = e.connector;
1830
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1831
+ if (!c || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1832
+ const cost = routeCost(c.cost);
1833
+ add2(a, { to: b, via: e.id, back: false, cost });
1834
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1835
+ }
1836
+ graph = { shapes: shapes2, next, trees: /* @__PURE__ */ new Map() };
1837
+ if (networks.size >= 16) networks.clear();
1838
+ networks.set(key, graph);
1839
+ }
1840
+ graphs.set(els, { members: [...els], graph });
1841
+ return graph;
1842
+ }
1843
+ var Frontier2 = class {
1844
+ constructor() {
1845
+ __publicField(this, "heap", []);
1846
+ }
1847
+ get size() {
1848
+ return this.heap.length;
1849
+ }
1850
+ less(i, j) {
1851
+ const a = this.heap[i], b = this.heap[j];
1852
+ return a.dist < b.dist || a.dist === b.dist && a.order < b.order;
1853
+ }
1854
+ swap(i, j) {
1855
+ [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
1856
+ }
1857
+ push(id, dist, order) {
1858
+ this.heap.push({ id, dist, order });
1859
+ for (let i = this.heap.length - 1; i > 0; ) {
1860
+ const up = i - 1 >> 1;
1861
+ if (!this.less(i, up)) break;
1862
+ this.swap(i, up);
1863
+ i = up;
1864
+ }
1865
+ }
1866
+ pop() {
1867
+ const top = this.heap[0], last = this.heap.pop();
1868
+ if (this.heap.length > 0) {
1869
+ this.heap[0] = last;
1870
+ for (let i = 0; ; ) {
1871
+ const l = 2 * i + 1, r = l + 1;
1872
+ let m = i;
1873
+ if (l < this.heap.length && this.less(l, m)) m = l;
1874
+ if (r < this.heap.length && this.less(r, m)) m = r;
1875
+ if (m === i) break;
1876
+ this.swap(i, m);
1877
+ i = m;
1878
+ }
1879
+ }
1880
+ return top;
1539
1881
  }
1882
+ };
1883
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set(), until) {
1884
+ const graph = graphOf(frame2);
1885
+ const key = JSON.stringify([from, until ?? null, [...offline].sort()]);
1886
+ if (graph.trees.has(key)) return graph.trees.get(key);
1887
+ const tree = grow(graph, from, offline, until);
1888
+ if (graph.trees.size >= 4096) graph.trees.clear();
1889
+ graph.trees.set(key, tree);
1890
+ return tree;
1891
+ }
1892
+ function grow({ shapes: shapes2, next }, from, offline, until) {
1893
+ if (!shapes2.has(from) || offline.has(from)) return null;
1540
1894
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1541
1895
  let counter = 1;
1542
1896
  const tree = new FlowTree(from);
1543
- const open = [from];
1544
- while (open.length > 0) {
1545
- let best = 0;
1546
- for (let i = 1; i < open.length; i++) {
1547
- const da = dist.get(open[i]), db = dist.get(open[best]);
1548
- if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1549
- }
1550
- const n = open.splice(best, 1)[0];
1897
+ const open = new Frontier2();
1898
+ open.push(from, 0, 0);
1899
+ while (open.size > 0) {
1900
+ const { id: n, order: o } = open.pop();
1901
+ if (tree.settled.has(n) || order.get(n) !== o) continue;
1551
1902
  tree.settled.add(n);
1903
+ if (n === until && n !== from) break;
1552
1904
  for (const edge of next.get(n) ?? []) {
1905
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1553
1906
  if (tree.settled.has(edge.to)) continue;
1554
1907
  const d = dist.get(n) + edge.cost;
1555
1908
  const old = dist.get(edge.to);
1556
1909
  if (old !== void 0 && old <= d) continue;
1557
- if (old === void 0) open.push(edge.to);
1558
1910
  dist.set(edge.to, d);
1559
- order.set(edge.to, counter++);
1911
+ order.set(edge.to, counter);
1912
+ open.push(edge.to, d, counter++);
1560
1913
  tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1561
1914
  }
1562
1915
  }
@@ -1636,9 +1989,18 @@ function delayOf(e) {
1636
1989
  var queueKey = (shape, flow) => shape + "|" + flow;
1637
1990
  function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1638
1991
  const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1639
- const byID = /* @__PURE__ */ new Map();
1640
- for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1992
+ const byID = indexOf(frame2);
1993
+ const traced = /* @__PURE__ */ new Map();
1994
+ const traceOf = (c) => {
1995
+ let pts = traced.get(c.id);
1996
+ if (!pts) {
1997
+ pts = tracing(c, frame2, byID).points;
1998
+ traced.set(c.id, pts);
1999
+ }
2000
+ return pts;
2001
+ };
1641
2002
  const metrics2 = emptyMetrics();
2003
+ const routes = /* @__PURE__ */ new Map();
1642
2004
  for (const e of frame2.elements) {
1643
2005
  if (e.kind === "connector" && !e.connector) continue;
1644
2006
  metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
@@ -1650,7 +2012,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1650
2012
  if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1651
2013
  }
1652
2014
  const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1653
- const route2 = (tree, goal) => {
2015
+ const route3 = (tree, goal) => {
1654
2016
  const hops = [], shapes2 = [];
1655
2017
  let at = goal;
1656
2018
  for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
@@ -1662,26 +2024,26 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1662
2024
  return { hops: hops.reverse(), shapes: shapes2.reverse() };
1663
2025
  };
1664
2026
  const way = (hops, shapes2) => {
1665
- const points = [], inside = [], stretches = [];
2027
+ const points = [], inside2 = [], stretches = [];
1666
2028
  let length = 0;
1667
2029
  hops.forEach(([id, back], k) => {
1668
2030
  const c = byID.get(id);
1669
2031
  if (!c) return;
1670
- const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
2032
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1671
2033
  if (line2.length === 0) return;
1672
2034
  const last = points[points.length - 1];
1673
2035
  if (last) {
1674
2036
  const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1675
2037
  const shape = byID.get(shapes2[k]);
1676
- inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
2038
+ inside2.push([length, length + gap, shape ? delayOf(shape) : 0]);
1677
2039
  length += gap;
1678
2040
  }
1679
- const start = length;
2041
+ const start2 = length;
1680
2042
  for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
1681
- stretches.push([id, start, length]);
2043
+ stretches.push([id, start2, length]);
1682
2044
  points.push(...line2);
1683
2045
  });
1684
- return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
2046
+ return length > 0 ? { line: new Polyline(points), length, inside: inside2, stretches } : null;
1685
2047
  };
1686
2048
  const along = (w, elapsedAt, speed) => {
1687
2049
  let elapsed = elapsedAt, d = 0;
@@ -1713,25 +2075,31 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1713
2075
  const packets = [];
1714
2076
  const waitingBy = /* @__PURE__ */ new Map();
1715
2077
  const everyOf = (flow) => Math.min(Math.max(clamp(flow.every, DEFAULT_EVERY, 0.05, 60) * biggest / Math.max(amountIn(flow, values), TINY), 0.05), 60);
2078
+ const bases = /* @__PURE__ */ new Map();
2079
+ const baseTree = (from) => {
2080
+ if (!bases.has(from)) bases.set(from, flowTree(from, frame2, offline));
2081
+ return bases.get(from);
2082
+ };
1716
2083
  for (const [i, flow] of list.entries()) {
1717
2084
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1718
2085
  const source = byID.get(flow.from);
1719
- const base2 = source ? flowTree(flow.from, frame2, offline) : null;
2086
+ const base2 = source ? baseTree(flow.from) : null;
2087
+ routes.set(flow.id, []);
1720
2088
  if (!source || !base2) continue;
1721
2089
  const volume = amountIn(flow, values);
1722
2090
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1723
2091
  const placements = [];
1724
- const waiting = [];
2092
+ const waiting2 = [];
1725
2093
  const goals = base2.goals(flow.to, flow.spread === true);
1726
2094
  const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1727
2095
  for (const [candidates, share] of shares) {
1728
2096
  if (!(share > TINY)) continue;
1729
2097
  let left = share;
1730
2098
  for (let n = 0; n < 32 && left > TINY; n++) {
1731
- const tree = flowTree(flow.from, frame2, union2(offline, full()));
2099
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1732
2100
  const goal2 = tree?.goals(candidates, false)[0];
1733
2101
  if (!tree || goal2 === void 0) break;
1734
- const r = route2(tree, goal2);
2102
+ const r = route3(tree, goal2);
1735
2103
  const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1736
2104
  const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1737
2105
  const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
@@ -1744,7 +2112,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1744
2112
  let at = flow.from;
1745
2113
  const goal = base2.goals(candidates, false)[0];
1746
2114
  if (goal !== void 0) {
1747
- const r = route2(base2, goal), done = full();
2115
+ const r = route3(base2, goal), done = full();
1748
2116
  const sequence = [[r.shapes[0], r.shapes[0]]];
1749
2117
  r.hops.forEach(([id], k) => {
1750
2118
  sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
@@ -1768,18 +2136,25 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1768
2136
  placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1769
2137
  }
1770
2138
  }
1771
- const existing = waiting.find(([s]) => s === at);
2139
+ const existing = waiting2.find(([s]) => s === at);
1772
2140
  if (existing) existing[1] += left;
1773
- else waiting.push([at, left]);
1774
- }
1775
- waitingBy.set(flow.id, waiting);
2141
+ else waiting2.push([at, left]);
2142
+ }
2143
+ waitingBy.set(flow.id, waiting2);
2144
+ routes.set(flow.id, placements.map((p) => ({
2145
+ goal: p.goal,
2146
+ hops: p.hops,
2147
+ amount: p.amount,
2148
+ waits: p.waits === true,
2149
+ cost: p.hops.reduce((s, [id]) => s + routeCost(byID.get(id)?.connector?.cost ?? null), 0)
2150
+ })));
1776
2151
  for (const p of placements) {
1777
2152
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1778
2153
  m.throughput += p.amount;
1779
2154
  });
1780
2155
  if (!p.waits) fm.placed += p.amount;
1781
2156
  }
1782
- for (const [at, a] of waiting) {
2157
+ for (const [at, a] of waiting2) {
1783
2158
  bump(at, (m) => {
1784
2159
  m.backlog += a;
1785
2160
  });
@@ -1824,10 +2199,12 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1824
2199
  metrics2.flows.set(flow.id, fm);
1825
2200
  }
1826
2201
  const piles = /* @__PURE__ */ new Map();
2202
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1827
2203
  for (const flow of list) {
1828
- const waiting = waitingBy.get(flow.id) ?? [];
1829
- const shapes2 = waiting.map(([s]) => s);
1830
- for (const e of frame2.elements) {
2204
+ const waiting2 = waitingBy.get(flow.id) ?? [];
2205
+ const shapes2 = waiting2.map(([s]) => s);
2206
+ const suffix = "|" + flow.id;
2207
+ if (carriedKeys.some((k) => k.endsWith(suffix))) for (const e of frame2.elements) {
1831
2208
  const c = carried[queueKey(e.id, flow.id)];
1832
2209
  if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1833
2210
  }
@@ -1841,7 +2218,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1841
2218
  const shape = byID.get(at);
1842
2219
  if (!shape) return;
1843
2220
  const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1844
- const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
2221
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1845
2222
  const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1846
2223
  let depth = before + backlog * since;
1847
2224
  if (backlog <= TINY && before > TINY) {
@@ -1850,10 +2227,10 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1850
2227
  let total = 0;
1851
2228
  for (let n2 = 0; n2 < 32; n2++) {
1852
2229
  const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1853
- const tree = flowTree(at, frame2, union2(offline, blocked));
2230
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1854
2231
  const goal = tree?.goals(flow.to, false)[0];
1855
2232
  if (!tree || goal === void 0) break;
1856
- const r = route2(tree, goal);
2233
+ const r = route3(tree, goal);
1857
2234
  const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1858
2235
  const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1859
2236
  const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
@@ -1908,7 +2285,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1908
2285
  if (c === null || !Number.isFinite(c) || !m) continue;
1909
2286
  m.load = c > 0 ? m.throughput / c : 1;
1910
2287
  }
1911
- return { packets, metrics: metrics2 };
2288
+ return { packets, metrics: metrics2, routes };
1912
2289
  }
1913
2290
  var BLANK = {
1914
2291
  seed: 1,
@@ -1963,21 +2340,21 @@ function metricLookup(m, frame2) {
1963
2340
  return id === null ? null : metricValue(m, term.metric, id);
1964
2341
  };
1965
2342
  }
1966
- function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2343
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
1967
2344
  const lookup = metricLookup(metrics2, frame2);
1968
- const out = [], readings = {};
1969
- for (let e of elements) {
2345
+ const plan2 = [], readings = {};
2346
+ for (const e of elements) {
2347
+ const applied = [];
1970
2348
  if (values !== null && e.overrides) {
1971
- for (const o of e.overrides) {
1972
- const own = e.id;
1973
- if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1974
- }
2349
+ e.overrides.forEach((o, k) => {
2350
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2351
+ });
1975
2352
  }
1976
- if (!e.gauge || values === null) {
1977
- out.push(e);
2353
+ const g = values === null ? null : e.gauge;
2354
+ if (!g) {
2355
+ plan2.push({ applied, gauge: null });
1978
2356
  continue;
1979
2357
  }
1980
- const g = e.gauge;
1981
2358
  let shown = metricValue(metrics2, g.metric, g.of);
1982
2359
  const then = carried[gaugeKey(e.id)];
1983
2360
  if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
@@ -1985,22 +2362,33 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
1985
2362
  shown = then + (shown - then) * smooth;
1986
2363
  }
1987
2364
  if (shown !== null) readings[gaugeKey(e.id)] = shown;
1988
- out.push(...gauge(e, g, shown, frame2, flows));
2365
+ plan2.push({ applied, gauge: { value: shown } });
1989
2366
  }
2367
+ return { plan: plan2, readings };
2368
+ }
2369
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2370
+ const { plan: plan2, readings } = finishPlan(elements, metrics2, values, frame2, since, carried);
2371
+ const out = [];
2372
+ elements.forEach((e, i) => {
2373
+ for (const k of plan2[i].applied) e = applyOverride(looksOnly(e.overrides[k]), e);
2374
+ const g = plan2[i].gauge;
2375
+ if (g && e.gauge) out.push(...gauge(e, e.gauge, g.value, frame2, flows));
2376
+ else out.push(e);
2377
+ });
1990
2378
  return { elements: out, readings };
1991
2379
  }
1992
2380
  var gaugeKey = (id) => "gauge|" + id;
1993
2381
  var GAUGE_EASE = 1;
1994
- var hex = (h) => {
2382
+ var hex2 = (h) => {
1995
2383
  const v = parseInt(h.slice(1), 16);
1996
2384
  return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
1997
2385
  };
1998
- var GAUGE_GREEN = hex("#16a34a");
1999
- var GAUGE_AMBER = hex("#d97706");
2000
- var GAUGE_RED = hex("#dc2626");
2001
- var GAUGE_BLUE = hex("#2f6fdf");
2002
- var GAUGE_TRACK = hex("#e5e7eb");
2003
- var GAUGE_GRAY = hex("#9ca3af");
2386
+ var GAUGE_GREEN = hex2("#16a34a");
2387
+ var GAUGE_AMBER = hex2("#d97706");
2388
+ var GAUGE_RED = hex2("#dc2626");
2389
+ var GAUGE_BLUE = hex2("#2f6fdf");
2390
+ var GAUGE_TRACK = hex2("#e5e7eb");
2391
+ var GAUGE_GRAY = hex2("#9ca3af");
2004
2392
  function formatted(metric, v) {
2005
2393
  if (v === null || !Number.isFinite(v)) return "\u2013";
2006
2394
  switch (metric) {
@@ -2132,7 +2520,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2132
2520
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
2133
2521
  const dx = b.x - a.x, dy = b.y - a.y;
2134
2522
  const nx = -dy / length, ny = dx / length;
2135
- const start = preserveEnds ? a : (() => {
2523
+ const start2 = preserveEnds ? a : (() => {
2136
2524
  const x = a.x + j();
2137
2525
  return { x, y: a.y + j() };
2138
2526
  })();
@@ -2144,7 +2532,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2144
2532
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
2145
2533
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
2146
2534
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
2147
- into.moveTo(start);
2535
+ into.moveTo(start2);
2148
2536
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
2149
2537
  }
2150
2538
  }
@@ -2286,17 +2674,17 @@ function clusterStrokes(c, angle2, size) {
2286
2674
  });
2287
2675
  }
2288
2676
  function contains(contours, p) {
2289
- let inside = false;
2677
+ let inside2 = false;
2290
2678
  for (const poly of contours) {
2291
2679
  if (poly.length < 3) continue;
2292
2680
  let k = poly.length - 1;
2293
2681
  for (let i = 0; i < poly.length; i++) {
2294
2682
  const a = poly[i], b = poly[k];
2295
- 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;
2683
+ 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;
2296
2684
  k = i;
2297
2685
  }
2298
2686
  }
2299
- return inside;
2687
+ return inside2;
2300
2688
  }
2301
2689
  function distance(p, contours) {
2302
2690
  let best = Infinity;
@@ -2405,10 +2793,10 @@ var Cache = class {
2405
2793
  __publicField(this, "limit");
2406
2794
  this.limit = limit;
2407
2795
  }
2408
- get(key, make) {
2796
+ get(key, make2) {
2409
2797
  const hit = this.map.get(key);
2410
2798
  if (hit !== void 0) return hit;
2411
- const v = make();
2799
+ const v = make2();
2412
2800
  if (this.map.size >= this.limit) this.map.clear();
2413
2801
  this.map.set(key, v);
2414
2802
  return v;
@@ -2573,7 +2961,7 @@ function connectorParts(e, index, page) {
2573
2961
  const c = e.connector;
2574
2962
  if (!c) return [new Path(), new Path()];
2575
2963
  const [a, b] = endpoints(e, index);
2576
- const r = route(e, index);
2964
+ const r = route2(e, index);
2577
2965
  const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
2578
2966
  const key = [
2579
2967
  e.seed,
@@ -2632,26 +3020,59 @@ function subCubic(p, t0, t1) {
2632
3020
  const left = split(p, t1)[0];
2633
3021
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
2634
3022
  }
3023
+ function boxOf(pts) {
3024
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3025
+ for (const p of pts) {
3026
+ x0 = Math.min(x0, p.x);
3027
+ y0 = Math.min(y0, p.y);
3028
+ x1 = Math.max(x1, p.x);
3029
+ y1 = Math.max(y1, p.y);
3030
+ }
3031
+ return { x0, y0, x1, y1 };
3032
+ }
3033
+ function touching(a, b) {
3034
+ const eps = 1e-6;
3035
+ return a.x0 <= b.x1 + eps && b.x0 <= a.x1 + eps && a.y0 <= b.y1 + eps && b.y0 <= a.y1 + eps;
3036
+ }
3037
+ var hopFrames = /* @__PURE__ */ new WeakMap();
3038
+ function linesBeneath(e, page, index) {
3039
+ let f = hopFrames.get(index);
3040
+ if (!f || f.layers !== page.layers || !isIndexOf(page, index)) {
3041
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
3042
+ const first = /* @__PURE__ */ new Map(), under = [];
3043
+ for (const o of orderedElements(page)) {
3044
+ if (!first.has(o.id)) first.set(o.id, under.length);
3045
+ if (o.kind === "connector" && visible.has(o.layerID)) under.push(o);
3046
+ }
3047
+ f = { layers: page.layers, first, under, lines: [] };
3048
+ if (isIndexOf(page, index)) hopFrames.set(index, f);
3049
+ }
3050
+ const count = f.first.get(e.id) ?? f.under.length;
3051
+ while (f.lines.length < count) {
3052
+ const points = routePoints(route2(f.under[f.lines.length], index));
3053
+ f.lines.push({ points, box: boxOf(points) });
3054
+ }
3055
+ return { lines: f.lines, count };
3056
+ }
2635
3057
  function hopCrossings(e, r, page, index) {
2636
3058
  const segments = hopSamples(r);
2637
3059
  if (segments.length === 0) return [];
2638
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2639
- const beneath = [];
2640
- for (const o of orderedElements(page)) {
2641
- if (o.id === e.id) break;
2642
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
2643
- beneath.push(routePoints(route(o, index)));
2644
- }
3060
+ const { lines, count } = linesBeneath(e, page, index);
2645
3061
  const rad = hopRadius(e.style.strokeWidth);
2646
3062
  const out = [];
2647
3063
  for (const samples of segments) {
2648
3064
  const along = runningLengths(samples);
2649
3065
  const length = along[along.length - 1];
2650
3066
  const found = [];
3067
+ const near = [];
3068
+ const whole = boxOf(samples);
3069
+ for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
2651
3070
  for (let i = 0; i < samples.length - 1; i++) {
2652
3071
  const p = samples[i], q = samples[i + 1];
2653
3072
  const dx = q.x - p.x, dy = q.y - p.y;
2654
- for (const line2 of beneath) {
3073
+ const box = boxOf([p, q]);
3074
+ for (const { points: line2, box: lineBox } of near) {
3075
+ if (!touching(box, lineBox)) continue;
2655
3076
  for (let k = 0; k < line2.length - 1; k++) {
2656
3077
  const a = line2[k], b = line2[k + 1];
2657
3078
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2931,8 +3352,8 @@ function followSpan(track, t) {
2931
3352
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2932
3353
  return f;
2933
3354
  }
2934
- function tracing(e, page) {
2935
- if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
3355
+ function tracing(e, page, index = indexOf(page)) {
3356
+ if (e.kind === "connector") return new Polyline(routePoints(route2(e, index)));
2936
3357
  const clean = cloneElement(e);
2937
3358
  clean.style.roughness = 0;
2938
3359
  const pts = outline(clean).contours()[0] ?? [];
@@ -2942,9 +3363,10 @@ function following(track, e, t, page) {
2942
3363
  const f = followSpan(track, t);
2943
3364
  if (!f) return e;
2944
3365
  t = localTime(track, t);
2945
- const pathElement = page.elements.find((x) => x.id === f.path);
3366
+ const index = indexOf(page);
3367
+ const pathElement = index.get(f.path);
2946
3368
  if (!pathElement || pathElement.id === e.id) return e;
2947
- const line2 = tracing(pathElement, page);
3369
+ const line2 = tracing(pathElement, page, index);
2948
3370
  const span = f.end - f.start;
2949
3371
  let u = Math.min(Math.max(applyEase(f.ease ?? "easeInOut", span > 0 ? Math.min(1, (t - f.start) / span) : 1), 0), 1) * (f.laps ?? 1);
2950
3372
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -3199,17 +3621,30 @@ function symbolPage(s) {
3199
3621
  function hasInstances(page) {
3200
3622
  return page.elements.some((e) => e.kind === "symbol");
3201
3623
  }
3202
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map()) {
3624
+ var Expansions = class {
3625
+ constructor(page, t, posing) {
3626
+ __publicField(this, "page");
3627
+ __publicField(this, "t");
3628
+ __publicField(this, "posing");
3629
+ /** For each instance: its time override, the budget it found and left, and what it made. */
3630
+ __publicField(this, "made", /* @__PURE__ */ new Map());
3631
+ this.page = page;
3632
+ this.t = t;
3633
+ this.posing = posing;
3634
+ }
3635
+ };
3636
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo2) {
3203
3637
  if (!hasInstances(page)) return page;
3204
3638
  const budget = { left: MAX_ELEMENTS };
3205
- return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times) };
3639
+ const m = memo2 && memo2.page === page && memo2.t === t && memo2.posing === posing ? memo2 : void 0;
3640
+ return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3206
3641
  }
3207
3642
  function symbolTable(page) {
3208
3643
  const table = /* @__PURE__ */ new Map();
3209
3644
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3210
3645
  return table;
3211
3646
  }
3212
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map()) {
3647
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo2) {
3213
3648
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3214
3649
  if (instances.size === 0) {
3215
3650
  budget.left -= elements.length;
@@ -3234,6 +3669,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3234
3669
  budget.left -= 1;
3235
3670
  continue;
3236
3671
  }
3672
+ const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3673
+ const was = memo2?.made.get(e);
3674
+ if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3675
+ budget.left -= was.before - was.after;
3676
+ for (const x of was.out) out.push(x);
3677
+ continue;
3678
+ }
3679
+ const before = budget.left, from = out.length;
3237
3680
  budget.left -= 1;
3238
3681
  const id = e.symbol;
3239
3682
  const symbol = id === null ? void 0 : table.get(id);
@@ -3249,6 +3692,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3249
3692
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3250
3693
  };
3251
3694
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3695
+ if (memo2 && stack.length === 0 && !was) memo2.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3252
3696
  }
3253
3697
  return out;
3254
3698
  }
@@ -3301,8 +3745,8 @@ function mix2(x) {
3301
3745
  return z ^ z >> 31n;
3302
3746
  }
3303
3747
  function halves(id) {
3304
- const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3305
- return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
3748
+ const hex3 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3749
+ return [BigInt(`0x${hex3.slice(0, 16)}`), BigInt(`0x${hex3.slice(16)}`)];
3306
3750
  }
3307
3751
  function derivedID(instance, inner) {
3308
3752
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -3516,13 +3960,13 @@ function blocks(lines) {
3516
3960
  flush();
3517
3961
  return out;
3518
3962
  }
3519
- function parseList(lines, start) {
3520
- const first = reItem.exec(lines[start]);
3963
+ function parseList(lines, start2) {
3964
+ const first = reItem.exec(lines[start2]);
3521
3965
  const indent = first[1].length;
3522
3966
  const bullet = /^\d/.test(first[2]) ? null : first[2];
3523
3967
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
3524
3968
  const items = [];
3525
- let i = start;
3969
+ let i = start2;
3526
3970
  while (i < lines.length) {
3527
3971
  const m = reItem.exec(lines[i]);
3528
3972
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -3630,12 +4074,23 @@ function inline(src, style2 = {}) {
3630
4074
  i += m[0].length;
3631
4075
  continue;
3632
4076
  }
3633
- if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4077
+ if (m = /^(\*\*\*|___)(?=\S)([\s\S]*?\S)\1(?!\*|_)/.exec(rest)) {
3634
4078
  emit();
3635
- out.push(...inline(m[2], { ...style2, bold: true }));
4079
+ out.push(...inline(m[2], { ...style2, bold: true, italic: true }));
3636
4080
  i += m[0].length;
3637
4081
  continue;
3638
4082
  }
4083
+ if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4084
+ let body = m[2], length = m[0].length;
4085
+ if (m[1] === "**" && rest[length] === "*" && (body.replace(/\*\*/g, "").match(/\*/g) ?? []).length % 2 === 1) {
4086
+ body += "*";
4087
+ length++;
4088
+ }
4089
+ emit();
4090
+ out.push(...inline(body, { ...style2, bold: true }));
4091
+ i += length;
4092
+ continue;
4093
+ }
3639
4094
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
3640
4095
  emit();
3641
4096
  out.push(...inline(m[2], { ...style2, italic: true }));
@@ -3851,10 +4306,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
3851
4306
  let k = 0;
3852
4307
  let firstLine = true;
3853
4308
  while (k < chars.length || firstLine && chars.length === 0) {
3854
- const start = firstLine ? p.firstIndent : p.indent;
4309
+ const start2 = firstLine ? p.firstIndent : p.indent;
3855
4310
  const maxX2 = width;
3856
4311
  const glyphs = [];
3857
- let x = start, lastSpace = -1, end = k;
4312
+ let x = start2, lastSpace = -1, end = k;
3858
4313
  for (; end < chars.length; end++) {
3859
4314
  const { ch, run } = chars[end];
3860
4315
  const r = runs[run];
@@ -3903,7 +4358,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3903
4358
  }
3904
4359
  if (glyphs.length === 0 && end === k && k < chars.length) {
3905
4360
  const { ch, run } = chars[k];
3906
- glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run], fam) });
4361
+ glyphs.push({ ch, run, x: start2, w: charWidth(ch, runs[run], fam) });
3907
4362
  end = k + 1;
3908
4363
  }
3909
4364
  let asc = 0, desc = 0, lead = 0;
@@ -3929,13 +4384,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3929
4384
  });
3930
4385
  return lines;
3931
4386
  }
4387
+ var Memo = class {
4388
+ constructor(limit) {
4389
+ __publicField(this, "map", /* @__PURE__ */ new Map());
4390
+ __publicField(this, "limit");
4391
+ this.limit = limit;
4392
+ }
4393
+ get(key, make2) {
4394
+ const hit = this.map.get(key);
4395
+ if (hit !== void 0) return hit;
4396
+ const v = make2();
4397
+ if (this.map.size >= this.limit) this.map.clear();
4398
+ this.map.set(key, v);
4399
+ return v;
4400
+ }
4401
+ };
4402
+ var layouts = new Memo(500);
4403
+ var markups = new Memo(500);
3932
4404
  function noteMarkup(source, e) {
4405
+ const st = e.style, ink = st.stroke;
4406
+ const key = JSON.stringify([...[e.x, e.y, e.width, e.height, st.fontSize, ink.r, ink.g, ink.b, ink.a].map(String), st.font ?? "", source]);
4407
+ return markups.get(key, () => computeNoteMarkup(source, e));
4408
+ }
4409
+ function computeNoteMarkup(source, e) {
3933
4410
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3934
4411
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3935
4412
  const fam = familyMetrics(e.style.font);
3936
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3937
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3938
- const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4413
+ const layoutKey = JSON.stringify([...[rect2.width, rect2.height, e.style.fontSize].map(String), e.style.font ?? "", source]);
4414
+ const lines = layouts.get(layoutKey, () => {
4415
+ const b = new Builder(e.style.fontSize, rect2.width, fam);
4416
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
4417
+ return layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4418
+ });
3939
4419
  const ink = color(e.style.stroke);
3940
4420
  let s = "";
3941
4421
  const marks = [];
@@ -3994,20 +4474,6 @@ function sameStyle(a, b) {
3994
4474
  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;
3995
4475
  }
3996
4476
 
3997
- // src/theme.ts
3998
- var rgb = (r, g, b) => ({ r, g, b, a: 1 });
3999
- var themes = [
4000
- { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13) },
4001
- { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13) },
4002
- { id: "sepia", name: "Sepia", background: rgb(0.953, 0.914, 0.824), ink: rgb(0.23, 0.18, 0.13) },
4003
- { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984) },
4004
- { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918) },
4005
- { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94) }
4006
- ];
4007
- function themeNamed(id) {
4008
- return themes.find((t) => t.id === id) ?? null;
4009
- }
4010
-
4011
4477
  // src/svg.ts
4012
4478
  function bounds(page, padding = 24) {
4013
4479
  if (hasInstances(page)) return bounds(expandingSymbols(page, 0), padding);
@@ -4024,6 +4490,26 @@ function bounds(page, padding = 24) {
4024
4490
  return inset(r, -padding, -padding);
4025
4491
  }
4026
4492
  function plan(page) {
4493
+ const hit = plans.get(page);
4494
+ if (hit && unchanged(page, hit.fields)) return hit.plan;
4495
+ const made = makePlan(page);
4496
+ plans.set(page, { fields: { ...page }, plan: made });
4497
+ return made;
4498
+ }
4499
+ var plans = /* @__PURE__ */ new WeakMap();
4500
+ function hasPlan(page) {
4501
+ const hit = plans.get(page);
4502
+ return !!hit && unchanged(page, hit.fields);
4503
+ }
4504
+ function rememberPlan(page, made) {
4505
+ plans.set(page, { fields: { ...page }, plan: made });
4506
+ }
4507
+ function unchanged(page, fields) {
4508
+ const a = page, b = fields;
4509
+ const keys = Object.keys(a);
4510
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
4511
+ }
4512
+ function makePlan(page) {
4027
4513
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
4028
4514
  const times = frameTimes(tl);
4029
4515
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -4051,9 +4537,9 @@ function svgOpen(b, background, paint) {
4051
4537
  }
4052
4538
  return out;
4053
4539
  }
4054
- function svgParts(page, background, fixed, links = false) {
4055
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
4056
- const theme = themeNamed(page.theme);
4540
+ function svgParts(page, background, fixed, links = false, only) {
4541
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
4542
+ const theme = pageTheme(page);
4057
4543
  const bg = background ? theme?.background ?? background : null;
4058
4544
  const ink = theme?.ink ?? black;
4059
4545
  const d = { ...page, elements: page.elements.map((e) => resolvingInk(e, ink)) };
@@ -4063,11 +4549,11 @@ function svgParts(page, background, fixed, links = false) {
4063
4549
  const index = indexOf(d);
4064
4550
  const ordered = orderedElements(d);
4065
4551
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
4066
- const rockMarkup = rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4552
+ const rockMarkup = only && !only("rock") ? "" : rock(floors).map((r) => `<path d="${r.path.data()}" fill="none" stroke="${color(r.ink)}"${opacity(r.ink, "stroke-opacity")} stroke-width="${fmt(r.width)}" stroke-linecap="round"/>
4067
4553
  `).join("");
4068
4554
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
4069
4555
  for (const e of ordered) {
4070
- if (!visible.has(e.layerID)) continue;
4556
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
4071
4557
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
4072
4558
  if (alpha <= 0) continue;
4073
4559
  let body = svgElement(e, index, bg, d);
@@ -4089,7 +4575,7 @@ ${body}</a>
4089
4575
  }
4090
4576
  function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
4091
4577
  const { bounds: b, parts } = svgParts(page, background, fixed, true);
4092
- return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4578
+ return svgOpen(b, pageTheme(page)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4093
4579
  }
4094
4580
  function labelRect(e, index) {
4095
4581
  const m = midpoint(e, index);
@@ -4183,7 +4669,7 @@ function svgElement(e, index, labelBackground, page) {
4183
4669
  }
4184
4670
  s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
4185
4671
  `;
4186
- if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" ? pathLabelBox(e) : frame(e), e, true);
4672
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" || e.kind === "rectangle" ? pathLabelBox(e) : frame(e), e, true);
4187
4673
  return s;
4188
4674
  }
4189
4675
  default:
@@ -4215,6 +4701,110 @@ function textElement(text, f, e, centered) {
4215
4701
  return s + "</text>\n";
4216
4702
  }
4217
4703
 
4704
+ // src/planner.ts
4705
+ var WORKER_WORK = 3e4;
4706
+ function planWork(page) {
4707
+ const tl = page.timeline;
4708
+ if (!tl || tl.camera?.some((k) => k.width > 1 && k.height > 1)) return 0;
4709
+ const elements = page.elements.length + (page.symbols ?? []).reduce((n, s) => n + s.elements.length, 0);
4710
+ return frameTimes(tl).length * elements;
4711
+ }
4712
+ function workerMaker() {
4713
+ if (typeof Worker === "undefined" || false) return null;
4714
+ if (false) {
4715
+ const source = `(${dottiePlayerScript.toString()})();
4716
+ `;
4717
+ return () => {
4718
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
4719
+ try {
4720
+ return new Worker(url);
4721
+ } finally {
4722
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
4723
+ }
4724
+ };
4725
+ }
4726
+ if (true) {
4727
+ const here = import.meta.url;
4728
+ if (!here) return null;
4729
+ return () => new Worker(new URL("./dottie-player.min.js", here));
4730
+ }
4731
+ return null;
4732
+ }
4733
+ var make;
4734
+ var worker = null;
4735
+ var failed = false;
4736
+ var nextID = 1;
4737
+ var waiting = /* @__PURE__ */ new Map();
4738
+ function plansInWorker(page) {
4739
+ if (failed || hasPlan(page) || planWork(page) < WORKER_WORK) return false;
4740
+ if (make === void 0) make = workerMaker();
4741
+ return make !== null;
4742
+ }
4743
+ function planInWorker(page) {
4744
+ return new Promise((resolve2) => {
4745
+ const done = (p) => {
4746
+ if (p) rememberPlan(page, p);
4747
+ resolve2();
4748
+ };
4749
+ let w;
4750
+ try {
4751
+ w = worker ?? start();
4752
+ } catch {
4753
+ failed = true;
4754
+ done(null);
4755
+ return;
4756
+ }
4757
+ const id = nextID++;
4758
+ waiting.set(id, { page, done });
4759
+ try {
4760
+ w.postMessage({ dottiePlan: id, page });
4761
+ } catch {
4762
+ waiting.delete(id);
4763
+ done(null);
4764
+ }
4765
+ });
4766
+ }
4767
+ function start() {
4768
+ const w = make();
4769
+ w.onmessage = (ev) => {
4770
+ const { dottiePlan: id, plan: p } = ev.data ?? {};
4771
+ const job = id === void 0 ? void 0 : waiting.get(id);
4772
+ if (!job) return;
4773
+ waiting.delete(id);
4774
+ job.done(p ?? null);
4775
+ if (waiting.size === 0 && worker === w) {
4776
+ w.terminate();
4777
+ worker = null;
4778
+ }
4779
+ };
4780
+ w.onerror = (ev) => {
4781
+ ev.preventDefault?.();
4782
+ failed = true;
4783
+ if (worker === w) worker = null;
4784
+ w.terminate();
4785
+ const jobs = [...waiting.values()];
4786
+ waiting.clear();
4787
+ for (const job of jobs) job.done(null);
4788
+ };
4789
+ worker = w;
4790
+ return w;
4791
+ }
4792
+ function servePlans() {
4793
+ const g = globalThis;
4794
+ if (typeof g.WorkerGlobalScope === "undefined" || !(g instanceof g.WorkerGlobalScope)) return;
4795
+ g.addEventListener("message", (ev) => {
4796
+ const id = ev.data?.dottiePlan;
4797
+ if (typeof id !== "number") return;
4798
+ let p = null;
4799
+ try {
4800
+ p = plan(ev.data.page);
4801
+ } catch {
4802
+ p = null;
4803
+ }
4804
+ g.postMessage({ dottiePlan: id, plan: p });
4805
+ });
4806
+ }
4807
+
4218
4808
  // src/player.ts
4219
4809
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4220
4810
  function pageIndex(file, spec) {
@@ -4284,7 +4874,7 @@ var Playback = class {
4284
4874
  __publicField(this, "order", []);
4285
4875
  __publicField(this, "frame", -1);
4286
4876
  __publicField(this, "onFrame", null);
4287
- this.page = options.clean ? withRoughness(page, 0) : page;
4877
+ this.page = options.clean ? cleanCopy(page) : page;
4288
4878
  addFonts(usedFonts(this.page));
4289
4879
  this.plan = plan(this.page);
4290
4880
  this.clicks = clickTargets(page, options.file);
@@ -4322,7 +4912,7 @@ var Playback = class {
4322
4912
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4323
4913
  this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4324
4914
  this.state = { ...page.variables ?? {} };
4325
- const theme = themeNamed(page.theme);
4915
+ const theme = pageTheme(page);
4326
4916
  this.background = theme?.background ?? WHITE;
4327
4917
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
4328
4918
  const ns = "http://www.w3.org/2000/svg";
@@ -4361,7 +4951,7 @@ var Playback = class {
4361
4951
  }
4362
4952
  /** Whether a copy is still playing through after a click. */
4363
4953
  replaying(now = performance.now()) {
4364
- for (const [id, start] of this.replayStarts) if (now - start < (this.replays.get(id) ?? 0) * 1e3) return true;
4954
+ for (const [id, start2] of this.replayStarts) if (now - start2 < (this.replays.get(id) ?? 0) * 1e3) return true;
4365
4955
  return false;
4366
4956
  }
4367
4957
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -4374,17 +4964,24 @@ var Playback = class {
4374
4964
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
4375
4965
  const now = typeof performance === "undefined" ? 0 : performance.now();
4376
4966
  const times = /* @__PURE__ */ new Map();
4377
- for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
4967
+ for (const [id, start2] of this.replayStarts) times.set(id, Math.min((now - start2) / 1e3, this.replays.get(id) ?? 0));
4378
4968
  const tc = clampTime(t);
4379
4969
  const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4380
- const frame2 = expandingSymbols(pose, tc, true, times);
4970
+ const made = new Expansions(pose, tc, true);
4971
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
4381
4972
  const { parts } = svgParts(frame2, this.background, r);
4382
4973
  const owned = (m, parts2) => {
4383
4974
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
4384
4975
  return m;
4385
4976
  };
4386
- const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(expandingSymbols(pose, tc, true, new Map([...times, ...st])), this.background, r).parts.filter((p) => st.has(this.owner.get(p.key) ?? "")));
4387
- const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r).parts.filter((p) => this.hoverIDs.has(p.key)));
4977
+ const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(
4978
+ expandingSymbols(pose, tc, true, new Map([...times, ...st]), made),
4979
+ this.background,
4980
+ r,
4981
+ false,
4982
+ (key) => st.has(this.owner.get(key) ?? "")
4983
+ ).parts);
4984
+ const hover = this.hoverIDs.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(hovered(frame2, this.hoverIDs), this.background, r, false, (key) => this.hoverIDs.has(key)).parts);
4388
4985
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
4389
4986
  const normal = owned(/* @__PURE__ */ new Map(), parts);
4390
4987
  const merged = [];
@@ -4513,6 +5110,14 @@ ${h}</g>
4513
5110
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4514
5111
  }
4515
5112
  };
5113
+ var cleanCopies = /* @__PURE__ */ new WeakMap();
5114
+ function cleanCopy(page) {
5115
+ const hit = cleanCopies.get(page);
5116
+ if (hit && unchanged(page, hit.fields)) return hit.page;
5117
+ const clean = withRoughness(page, 0);
5118
+ cleanCopies.set(page, { fields: { ...page }, page: clean });
5119
+ return clean;
5120
+ }
4516
5121
  var ElementBase = typeof HTMLElement === "undefined" ? class {
4517
5122
  } : HTMLElement;
4518
5123
  var DottiePlayer = class extends ElementBase {
@@ -4535,6 +5140,10 @@ var DottiePlayer = class extends ElementBase {
4535
5140
  __publicField(this, "raf", 0);
4536
5141
  __publicField(this, "replayRaf", 0);
4537
5142
  __publicField(this, "loadToken", 0);
5143
+ /** Counts mounts, so a plan that comes back from a worker after a newer one is dropped. */
5144
+ __publicField(this, "mountToken", 0);
5145
+ /** The time a still shows; null while the drawing is animated. */
5146
+ __publicField(this, "stillTime", null);
4538
5147
  __publicField(this, "visible", true);
4539
5148
  __publicField(this, "observer", null);
4540
5149
  this.root = this.attachShadow({ mode: "open" });
@@ -4611,9 +5220,19 @@ var DottiePlayer = class extends ElementBase {
4611
5220
  text = inline2.textContent ?? "";
4612
5221
  }
4613
5222
  if (token !== this.loadToken) return;
4614
- this.setFile(parseDottie(text));
5223
+ this.showFile(parseDottie(text));
4615
5224
  } catch (err) {
4616
5225
  if (token !== this.loadToken) return;
5226
+ this.setPlaying(false);
5227
+ cancelAnimationFrame(this.raf);
5228
+ cancelAnimationFrame(this.replayRaf);
5229
+ this.raf = 0;
5230
+ this.replayRaf = 0;
5231
+ this.playback = null;
5232
+ this.file = null;
5233
+ this.mountToken++;
5234
+ this.stillTime = null;
5235
+ this.bar.hidden = true;
4617
5236
  this.stage.innerHTML = "";
4618
5237
  const box = document.createElement("div");
4619
5238
  box.className = "error";
@@ -4624,15 +5243,35 @@ var DottiePlayer = class extends ElementBase {
4624
5243
  }
4625
5244
  /** Shows an already-parsed file. */
4626
5245
  setFile(file) {
5246
+ this.loadToken++;
5247
+ this.showFile(file);
5248
+ }
5249
+ showFile(file) {
4627
5250
  this.file = file;
4628
5251
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
4629
- this.mount();
4630
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
5252
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
4631
5253
  }
4632
- mount() {
5254
+ /** Shows the file's page; then `mounted`. A big animation is planned in a worker first (planner.ts), and what was
5255
+ * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
5256
+ mount(mounted) {
4633
5257
  const file = this.file;
4634
5258
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
4635
- const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
5259
+ const clean = this.hasAttribute("clean");
5260
+ const token = ++this.mountToken;
5261
+ const shown = clean ? cleanCopy(page) : page;
5262
+ if (!plansInWorker(shown)) {
5263
+ this.mountPage(file, page, clean);
5264
+ mounted?.();
5265
+ return;
5266
+ }
5267
+ void planInWorker(shown).then(() => {
5268
+ if (token !== this.mountToken || this.file !== file) return;
5269
+ this.mountPage(file, page, clean);
5270
+ mounted?.();
5271
+ });
5272
+ }
5273
+ mountPage(file, page, clean) {
5274
+ const p = new Playback(page, this.stage, { clean, file });
4636
5275
  p.still = this.hasAttribute("at");
4637
5276
  this.playback = p;
4638
5277
  this.style.setProperty("--dottie-page", color(p.background));
@@ -4655,10 +5294,14 @@ var DottiePlayer = class extends ElementBase {
4655
5294
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
4656
5295
  };
4657
5296
  if (!animated) {
4658
- p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
5297
+ const i = at !== null ? nearest(p, Number(at) || 0) : p.posterFrame;
5298
+ this.stillTime = at !== null ? Number(at) || 0 : posterTime(page);
5299
+ p.drawAt(this.stillTime, i);
5300
+ p.frame = i;
4659
5301
  this.setPlaying(false);
4660
5302
  return;
4661
5303
  }
5304
+ this.stillTime = null;
4662
5305
  p.show(0);
4663
5306
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4664
5307
  if (reduced) {
@@ -4672,8 +5315,10 @@ var DottiePlayer = class extends ElementBase {
4672
5315
  get paused() {
4673
5316
  return !this.playing;
4674
5317
  }
5318
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
4675
5319
  get currentTime() {
4676
5320
  const p = this.playback;
5321
+ if (p && this.stillTime !== null) return this.stillTime;
4677
5322
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
4678
5323
  }
4679
5324
  set currentTime(t) {
@@ -4921,7 +5566,7 @@ function inspectFrame(page, t) {
4921
5566
  const [a, b] = endpoints(e, index);
4922
5567
  d.start = pt(a);
4923
5568
  d.end = pt(b);
4924
- d.route = routePoints(route(e, index)).map(pt);
5569
+ d.route = routePoints(route2(e, index)).map(pt);
4925
5570
  }
4926
5571
  return d;
4927
5572
  });
@@ -4960,9 +5605,10 @@ function render(source, options = {}) {
4960
5605
  function isFile(v) {
4961
5606
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4962
5607
  }
4963
- var VERSION = "0.8.0";
5608
+ var VERSION = "0.10.0";
4964
5609
 
4965
5610
  // src/player-entry.ts
5611
+ servePlans();
4966
5612
  definePlayer();
4967
5613
  globalThis.Dottie ?? (globalThis.Dottie = index_exports);
4968
5614
  export {
@@ -5000,6 +5646,8 @@ export {
5000
5646
  luminance,
5001
5647
  orderedElements,
5002
5648
  pageIndex,
5649
+ pageTheme,
5650
+ paletteColor,
5003
5651
  parseDottie,
5004
5652
  plan,
5005
5653
  posterTime,