@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.
package/dist/dottie.js CHANGED
@@ -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 __publicField = (obj, key, value2) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value2);
@@ -134,19 +134,80 @@ function trim(s) {
134
134
  return s.replace(/^[ \t ]+|[ \t ]+$/g, "");
135
135
  }
136
136
 
137
+ // src/theme.ts
138
+ var rgb = (r, g, b) => ({ r, g, b, a: 1 });
139
+ function hex(h) {
140
+ const n = parseInt(h.slice(1), 16);
141
+ return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 };
142
+ }
143
+ var PALETTE_NAMES = ["accent", "muted", "danger", "warning", "success", "info"];
144
+ var palette = (colors) => PALETTE_NAMES.map((name, i) => ({ name, color: hex(colors[i]) }));
145
+ var LIGHT_PALETTE = palette(["#2563eb", "#6b7280", "#dc2626", "#d97706", "#16a34a", "#0891b2"]);
146
+ var DARK_PALETTE = palette(["#93c5fd", "#cbd5e1", "#fca5a5", "#fcd34d", "#86efac", "#67e8f9"]);
147
+ var themes = [
148
+ { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
149
+ { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13), palette: LIGHT_PALETTE },
150
+ {
151
+ id: "sepia",
152
+ name: "Sepia",
153
+ background: rgb(0.953, 0.914, 0.824),
154
+ ink: rgb(0.23, 0.18, 0.13),
155
+ palette: palette(["#9a3412", "#78716c", "#b91c1c", "#b45309", "#4d7c0f", "#0e7490"])
156
+ },
157
+ { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984), palette: DARK_PALETTE },
158
+ { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918), palette: DARK_PALETTE },
159
+ { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94), palette: DARK_PALETTE }
160
+ ];
161
+ function themeNamed(id) {
162
+ return themes.find((t) => t.id === id) ?? null;
163
+ }
164
+ function pageTheme(page) {
165
+ const c = page.customTheme;
166
+ if (c) return { id: "custom", name: c.name, background: c.background, ink: c.ink, palette: c.palette };
167
+ return themeNamed(page.theme);
168
+ }
169
+ function paletteColor(page, name) {
170
+ const n = name.trim().toLowerCase();
171
+ return (pageTheme(page)?.palette ?? LIGHT_PALETTE).find((p) => p.name.toLowerCase() === n)?.color ?? null;
172
+ }
173
+ function resolvingPalette(page) {
174
+ const named = (e) => !!e.style.named;
175
+ if (!page.elements.some(named) && !(page.symbols ?? []).some((s) => s.elements.some(named))) return page;
176
+ const resolve2 = (e) => {
177
+ const n = e.style.named;
178
+ if (!n) return e;
179
+ const style2 = { ...e.style };
180
+ const f = n.fill !== null ? paletteColor(page, n.fill) : null;
181
+ const s = n.stroke !== null ? paletteColor(page, n.stroke) : null;
182
+ const t = n.textColor !== null ? paletteColor(page, n.textColor) : null;
183
+ if (f) style2.fill = { ...f };
184
+ if (s) style2.stroke = { ...s };
185
+ if (t) style2.textColor = { ...t };
186
+ return { ...e, style: style2 };
187
+ };
188
+ return {
189
+ ...page,
190
+ elements: page.elements.map(resolve2),
191
+ symbols: page.symbols ? page.symbols.map((s) => ({ ...s, elements: s.elements.map(resolve2) })) : page.symbols
192
+ };
193
+ }
194
+
137
195
  // src/model.ts
138
196
  var SUPPORTED_VERSION = 2;
139
197
  var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
140
198
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
141
199
  var KNOWN_FIELDS = {
142
200
  file: ["version", "pages", "images"],
143
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
201
+ page: ["id", "name", "layers", "elements", "theme", "customTheme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols", "variables", "flows"],
144
202
  flow: ["id", "from", "to", "every", "speed", "color", "size", "when", "spread", "volume", "overrides"],
145
203
  flowOverride: ["when", "volume"],
146
204
  symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button", "params"],
147
205
  layer: ["id", "name", "visible", "locked"],
148
206
  rgba: ["r", "g", "b", "a"],
149
- style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor"],
207
+ style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "font", "gridSize", "opacity", "dashOffset", "textColor", "named"],
208
+ colorNames: ["fill", "stroke", "textColor"],
209
+ customTheme: ["name", "background", "ink", "palette", "base"],
210
+ paletteColor: ["name", "color"],
150
211
  element: [
151
212
  "id",
152
213
  "kind",
@@ -184,8 +245,11 @@ var KNOWN_FIELDS = {
184
245
  "capacity",
185
246
  "delay",
186
247
  "queue",
187
- "gauge"
248
+ "gauge",
249
+ "containerID",
250
+ "container"
188
251
  ],
252
+ container: ["fixed", "lanes"],
189
253
  gauge: ["metric", "of", "max", "warn", "alarm", "style", "label"],
190
254
  strokePoint: ["x", "y"],
191
255
  subpath: ["closed", "coords"],
@@ -194,7 +258,7 @@ var KNOWN_FIELDS = {
194
258
  extensionRef: ["id", "shape", "version", "params"],
195
259
  normalizedPoint: ["x", "y"],
196
260
  normalizedRect: ["x", "y", "width", "height"],
197
- connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "cost"],
261
+ connector: ["start", "end", "routing", "startHead", "endHead", "waypoints", "hops", "avoid", "cost"],
198
262
  waypoint: ["x", "y"],
199
263
  timeline: ["fps", "duration", "boil", "settle", "poster", "tracks", "camera", "markers"],
200
264
  track: ["element", "keys", "boil", "follow", "repeats", "march"],
@@ -281,9 +345,36 @@ var Reader = class {
281
345
  gridSize: num(s.gridSize) ?? null,
282
346
  opacity: num(s.opacity) ?? null,
283
347
  dashOffset: num(s.dashOffset) ?? null,
284
- textColor: rgba(s.textColor) ?? null
348
+ textColor: rgba(s.textColor) ?? null,
349
+ named: this.colorNames(s.named)
285
350
  };
286
351
  }
352
+ container(v) {
353
+ if (!isObj(v)) return null;
354
+ this.fields(v, "container");
355
+ return { fixed: v.fixed === true ? true : null, lanes: v.lanes === "rows" || v.lanes === "columns" ? v.lanes : null };
356
+ }
357
+ colorNames(v) {
358
+ if (!isObj(v)) return null;
359
+ this.fields(v, "colorNames");
360
+ const n = { fill: str(v.fill) ?? null, stroke: str(v.stroke) ?? null, textColor: str(v.textColor) ?? null };
361
+ return n.fill === null && n.stroke === null && n.textColor === null ? null : n;
362
+ }
363
+ customTheme(v) {
364
+ if (!isObj(v)) return null;
365
+ this.fields(v, "customTheme");
366
+ const background = rgba(v.background), ink = rgba(v.ink);
367
+ if (!background || !ink) {
368
+ this.warn("A custom theme needs a background and an ink; drawn without it");
369
+ return null;
370
+ }
371
+ const palette2 = (Array.isArray(v.palette) ? v.palette : []).filter(isObj).flatMap((p) => {
372
+ this.fields(p, "paletteColor");
373
+ const name = str(p.name), color2 = rgba(p.color);
374
+ return name && color2 ? [{ name, color: color2 }] : [];
375
+ });
376
+ return { name: str(v.name) ?? "Custom", background, ink, palette: palette2, base: str(v.base) ?? null };
377
+ }
287
378
  endpoint(v) {
288
379
  const e = isObj(v) ? v : {};
289
380
  const port = str(e.port);
@@ -315,6 +406,7 @@ var Reader = class {
315
406
  return { x: num(w.x) ?? 0, y: num(w.y) ?? 0 };
316
407
  }) : null,
317
408
  hops: typeof v.hops === "boolean" ? v.hops : null,
409
+ avoid: typeof v.avoid === "boolean" ? v.avoid : null,
318
410
  cost: typeof v.cost === "number" && Number.isFinite(v.cost) ? v.cost : null
319
411
  };
320
412
  }
@@ -383,7 +475,9 @@ var Reader = class {
383
475
  capacity: num(v.capacity) ?? null,
384
476
  delay: num(v.delay) ?? null,
385
477
  queue: v.queue === true ? true : null,
386
- gauge: this.gauge(v.gauge)
478
+ gauge: this.gauge(v.gauge),
479
+ containerID: uuid(v.containerID) ?? null,
480
+ container: this.container(v.container)
387
481
  };
388
482
  }
389
483
  gauge(v) {
@@ -546,15 +640,15 @@ var Reader = class {
546
640
  const keys = (Array.isArray(v.keys) ? v.keys : []).map((k) => this.keyframe(k)).filter((k) => k !== null && k.t >= 0 && k.t <= MAX_DURATION);
547
641
  stableSortBy(keys, (k) => k.t);
548
642
  const follow = Array.isArray(v.follow) ? v.follow.filter(isObj).flatMap((f) => {
549
- const path = uuid(f.path), start = num(f.start), end = num(f.end);
550
- if (!path || start === void 0 || end === void 0 || start < 0 || end > MAX_DURATION) return [];
551
- return [{ path, start, end, ease: this.ease(f.ease), reverse: bool(f.reverse) ?? null, orient: bool(f.orient) ?? null, laps: num(f.laps) ?? null }];
643
+ const path = uuid(f.path), start2 = num(f.start), end = num(f.end);
644
+ if (!path || start2 === void 0 || end === void 0 || start2 < 0 || end > MAX_DURATION) return [];
645
+ 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 }];
552
646
  }) : null;
553
647
  const repeats = Array.isArray(v.repeats) ? v.repeats.filter(isObj).flatMap((r) => {
554
- const start = num(r.start), end = num(r.end);
555
- if (start === void 0 || end === void 0) return [];
648
+ const start2 = num(r.start), end = num(r.end);
649
+ if (start2 === void 0 || end === void 0) return [];
556
650
  const count = num(r.count);
557
- const rep = { start, end, count: count === void 0 ? null : Math.trunc(count) };
651
+ const rep = { start: start2, end, count: count === void 0 ? null : Math.trunc(count) };
558
652
  return repeatIsValid(rep) ? [rep] : [];
559
653
  }) : null;
560
654
  return { element, keys, boil: bool(v.boil) ?? null, follow, repeats, march: clampMarch(num(v.march)) };
@@ -604,6 +698,7 @@ var Reader = class {
604
698
  layers,
605
699
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
606
700
  theme: str(v.theme) ?? null,
701
+ customTheme: this.customTheme(v.customTheme),
607
702
  timeline: this.timeline(v.timeline),
608
703
  symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null,
609
704
  variables: this.variables(v.variables),
@@ -667,7 +762,7 @@ function parseDottie(source) {
667
762
  if (isObj(v) && typeof v.data === "string") images[k] = reader.picture(v.data, str(v.format));
668
763
  }
669
764
  }
670
- const pages = json.pages.map((p, i) => reader.page(p, images, i + 1));
765
+ const pages = json.pages.map((p, i) => resolvingPalette(reader.page(p, images, i + 1)));
671
766
  if (new Set(pages.map((p) => p.id)).size !== pages.length) throw new DottieFormatError("Two pages share an id");
672
767
  return { version, pages, warnings: reader.warnings };
673
768
  }
@@ -687,7 +782,8 @@ function cloneElement(e) {
687
782
  ...e.style,
688
783
  stroke: { ...e.style.stroke },
689
784
  fill: { ...e.style.fill },
690
- textColor: e.style.textColor ? { ...e.style.textColor } : null
785
+ textColor: e.style.textColor ? { ...e.style.textColor } : null,
786
+ named: e.style.named ? { ...e.style.named } : e.style.named
691
787
  },
692
788
  connector: e.connector ? {
693
789
  ...e.connector,
@@ -779,6 +875,21 @@ function pieces(para) {
779
875
  if (cur || out.length === 0) out.push(cur);
780
876
  return out;
781
877
  }
878
+ function prefixWidths(chars, fontSize, face, font) {
879
+ const fam = familyMetrics(font);
880
+ const f = fam.faces[face] ?? fam.faces.regular;
881
+ const fallback = f.w["n"];
882
+ const out = [0];
883
+ let w = 0;
884
+ let prev = "";
885
+ for (const ch of chars) {
886
+ w += f.w[ch] ?? fallback;
887
+ if (prev) w += f.k[prev + ch] ?? 0;
888
+ prev = ch;
889
+ out.push(w * fontSize / 1e3);
890
+ }
891
+ return out;
892
+ }
782
893
  function wrap(s, fontSize, maxWidth, face = "regular", font) {
783
894
  const width = (t) => measure(t, fontSize, face, font);
784
895
  const out = [];
@@ -794,8 +905,9 @@ function wrap(s, fontSize, maxWidth, face = "regular", font) {
794
905
  }
795
906
  while (width(line2.trimEnd()) > maxWidth && [...line2.trimEnd()].length > 1) {
796
907
  const chars = [...line2];
908
+ const prefix = prefixWidths(chars, fontSize, face, font);
797
909
  let n = chars.length - 1;
798
- while (n > 1 && width(chars.slice(0, n).join("")) > maxWidth) n--;
910
+ while (n > 1 && prefix[n] > maxWidth) n--;
799
911
  out.push(chars.slice(0, n).join(""));
800
912
  line2 = chars.slice(n).join("");
801
913
  }
@@ -950,13 +1062,13 @@ var Path = class _Path {
950
1062
  contours() {
951
1063
  const out = [];
952
1064
  let current = [];
953
- let start = { x: 0, y: 0 };
1065
+ let start2 = { x: 0, y: 0 };
954
1066
  for (const s of this.segments) {
955
- const last = current.length ? current[current.length - 1] : start;
1067
+ const last = current.length ? current[current.length - 1] : start2;
956
1068
  switch (s.op) {
957
1069
  case "M":
958
1070
  if (current.length > 1) out.push(current);
959
- start = s.p;
1071
+ start2 = s.p;
960
1072
  current = [s.p];
961
1073
  break;
962
1074
  case "L":
@@ -978,9 +1090,9 @@ var Path = class _Path {
978
1090
  }
979
1091
  break;
980
1092
  case "Z":
981
- current.push(start);
1093
+ current.push(start2);
982
1094
  if (current.length > 1) out.push(current);
983
- current = [start];
1095
+ current = [start2];
984
1096
  break;
985
1097
  }
986
1098
  }
@@ -1161,6 +1273,165 @@ function shapePath(e) {
1161
1273
  return path;
1162
1274
  }
1163
1275
 
1276
+ // src/elbow.ts
1277
+ var MARGIN = 16;
1278
+ var BEND = 24;
1279
+ var SMALLEST = 24;
1280
+ var MOST = 48;
1281
+ function obstacle(e) {
1282
+ if (e.kind === "connector" || e.kind === "text" || e.kind === "freehand") return null;
1283
+ if (e.kind === "path" && !e.extension) return null;
1284
+ if (e.style.opacity !== null && e.style.opacity <= 0) return null;
1285
+ const r = rotatedBounds(e);
1286
+ return r.width < SMALLEST || r.height < SMALLEST ? null : r;
1287
+ }
1288
+ function crosses(points, r) {
1289
+ const x1 = r.x + r.width, y1 = r.y + r.height;
1290
+ for (let k = 1; k < points.length; k++) {
1291
+ const p = points[k - 1], q = points[k];
1292
+ if (p.y === q.y) {
1293
+ 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;
1294
+ } else if (p.x === q.x) {
1295
+ 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;
1296
+ }
1297
+ }
1298
+ return false;
1299
+ }
1300
+ var padded = (r) => ({ x: r.x - MARGIN, y: r.y - MARGIN, width: r.width + 2 * MARGIN, height: r.height + 2 * MARGIN });
1301
+ var inside = (p, r) => r.x < p.x && p.x < r.x + r.width && r.y < p.y && p.y < r.y + r.height;
1302
+ var memo = /* @__PURE__ */ new Map();
1303
+ function avoiding(plain, a, b, startVertical, endVertical, elements, except) {
1304
+ const usable = (r) => !inside(a, padded(r)) && !inside(b, padded(r));
1305
+ const all = [];
1306
+ let crossed = false;
1307
+ for (const e of elements) {
1308
+ if (except.has(e.id)) continue;
1309
+ const r = obstacle(e);
1310
+ if (!r || !usable(r)) continue;
1311
+ all.push(r);
1312
+ if (!crossed && crosses(plain, r)) crossed = true;
1313
+ }
1314
+ if (!crossed) return plain;
1315
+ 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])]);
1316
+ const hit = memo.get(key);
1317
+ if (hit) return hit;
1318
+ const out = route(plain, a, b, startVertical, endVertical, all);
1319
+ if (memo.size >= 512) memo.clear();
1320
+ memo.set(key, out);
1321
+ return out;
1322
+ }
1323
+ function route(plain, a, b, startVertical, endVertical, usable) {
1324
+ let chosen = usable.map((_, i) => i).filter((i) => crosses(plain, usable[i]));
1325
+ if (chosen.length === 0) return plain;
1326
+ for (let round = 0; round < 4; round++) {
1327
+ if (chosen.length > MOST) return plain;
1328
+ const path = grid(a, b, startVertical, endVertical, chosen.map((i) => padded(usable[i])));
1329
+ if (!path) return plain;
1330
+ const more = usable.map((_, i) => i).filter((i) => !chosen.includes(i) && crosses(path, usable[i]));
1331
+ if (more.length === 0) return path;
1332
+ chosen = [...chosen, ...more].sort((p, q) => p - q);
1333
+ }
1334
+ return plain;
1335
+ }
1336
+ function grid(a, b, startVertical, endVertical, boxes) {
1337
+ let xs = [a.x, b.x, (a.x + b.x) / 2], ys = [a.y, b.y, (a.y + b.y) / 2];
1338
+ for (const r of boxes) {
1339
+ xs.push(r.x, r.x + r.width);
1340
+ ys.push(r.y, r.y + r.height);
1341
+ }
1342
+ xs = [...new Set(xs)].sort((p, q) => p - q);
1343
+ ys = [...new Set(ys)].sort((p, q) => p - q);
1344
+ const nx = xs.length, ny = ys.length;
1345
+ const ia = xs.indexOf(a.x), ja = ys.indexOf(a.y), ib = xs.indexOf(b.x), jb = ys.indexOf(b.y);
1346
+ if (ia < 0 || ja < 0 || ib < 0 || jb < 0) return null;
1347
+ const at = new Map(xs.map((x, k) => [x, k])), bt = new Map(ys.map((y, k) => [y, k]));
1348
+ const inner = new Uint8Array(nx * ny), acrossBlocked = new Uint8Array(nx * ny), downBlocked = new Uint8Array(nx * ny);
1349
+ for (const r of boxes) {
1350
+ 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);
1351
+ if (i0 === void 0 || i1 === void 0 || j0 === void 0 || j1 === void 0) continue;
1352
+ for (let i = i0; i < i1; i++) for (let j = j0; j < j1; j++) {
1353
+ if (j > j0) acrossBlocked[i * ny + j] = 1;
1354
+ if (i > i0) downBlocked[i * ny + j] = 1;
1355
+ if (i > i0 && j > j0) inner[i * ny + j] = 1;
1356
+ }
1357
+ }
1358
+ const blocked = (i, j) => inner[i * ny + j] === 1;
1359
+ const across = (i0, i1, j) => acrossBlocked[Math.min(i0, i1) * ny + j] === 1;
1360
+ const down = (j0, j1, i) => downBlocked[i * ny + Math.min(j0, j1)] === 1;
1361
+ const count = nx * ny * 2;
1362
+ const dist = new Array(count).fill(Infinity), prev = new Array(count).fill(-1);
1363
+ const done = new Array(count).fill(false);
1364
+ const heap = new Frontier();
1365
+ const first = (ia * ny + ja) * 2 + (startVertical ? 1 : 0);
1366
+ dist[first] = 0;
1367
+ heap.push(0, first);
1368
+ const want = (ib * ny + jb) * 2 + (endVertical ? 1 : 0);
1369
+ for (let top = heap.pop(); top; top = heap.pop()) {
1370
+ const [cost, s] = top;
1371
+ if (done[s]) continue;
1372
+ done[s] = true;
1373
+ if (s === want) break;
1374
+ const node = Math.floor(s / 2), axis = s % 2, i = Math.floor(node / ny), j = node % ny;
1375
+ for (const [di, dj] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
1376
+ const ni = i + di, nj = j + dj;
1377
+ if (ni < 0 || ni >= nx || nj < 0 || nj >= ny || blocked(ni, nj)) continue;
1378
+ const turn = di !== 0 ? 0 : 1;
1379
+ if (di !== 0 ? across(i, ni, j) : down(j, nj, i)) continue;
1380
+ const length = Math.abs(xs[ni] - xs[i]) + Math.abs(ys[nj] - ys[j]);
1381
+ const c = cost + length + (turn !== axis ? BEND : 0);
1382
+ const t = (ni * ny + nj) * 2 + turn;
1383
+ if (c < dist[t]) {
1384
+ dist[t] = c;
1385
+ prev[t] = s;
1386
+ heap.push(c, t);
1387
+ }
1388
+ }
1389
+ }
1390
+ if (!done[want]) return null;
1391
+ const points = [];
1392
+ for (let s = want; s >= 0; s = prev[s]) {
1393
+ const node = Math.floor(s / 2);
1394
+ points.push({ x: xs[Math.floor(node / ny)], y: ys[node % ny] });
1395
+ }
1396
+ return points.reverse();
1397
+ }
1398
+ var Frontier = class {
1399
+ constructor() {
1400
+ __publicField(this, "heap", []);
1401
+ __publicField(this, "next", 0);
1402
+ }
1403
+ less(i, j) {
1404
+ const a = this.heap[i], b = this.heap[j];
1405
+ return a[0] < b[0] || a[0] === b[0] && a[1] < b[1];
1406
+ }
1407
+ push(cost, state) {
1408
+ this.heap.push([cost, this.next++, state]);
1409
+ for (let i = this.heap.length - 1; i > 0; ) {
1410
+ const up = i - 1 >> 1;
1411
+ if (!this.less(i, up)) break;
1412
+ [this.heap[i], this.heap[up]] = [this.heap[up], this.heap[i]];
1413
+ i = up;
1414
+ }
1415
+ }
1416
+ pop() {
1417
+ if (this.heap.length === 0) return null;
1418
+ const top = this.heap[0], last = this.heap.pop();
1419
+ if (this.heap.length > 0) {
1420
+ this.heap[0] = last;
1421
+ for (let i = 0; ; ) {
1422
+ const l = 2 * i + 1, r = l + 1;
1423
+ let m = i;
1424
+ if (l < this.heap.length && this.less(l, m)) m = l;
1425
+ if (r < this.heap.length && this.less(r, m)) m = r;
1426
+ if (m === i) break;
1427
+ [this.heap[i], this.heap[m]] = [this.heap[m], this.heap[i]];
1428
+ i = m;
1429
+ }
1430
+ }
1431
+ return [top[0], top[2]];
1432
+ }
1433
+ };
1434
+
1164
1435
  // src/connector.ts
1165
1436
  function portPoint(shape, port) {
1166
1437
  const r = frame(shape);
@@ -1304,7 +1575,7 @@ function routeThrough(p, c) {
1304
1575
  }
1305
1576
  }
1306
1577
  }
1307
- function route(e, index) {
1578
+ function route2(e, index) {
1308
1579
  const c = e.connector;
1309
1580
  if (!c) return { kind: "polyline", points: [] };
1310
1581
  const [a, b] = endpoints(e, index);
@@ -1320,16 +1591,19 @@ function route(e, index) {
1320
1591
  case "elbow": {
1321
1592
  const startV = vertical(c.start.port) ?? !horizontal;
1322
1593
  const endV = vertical(c.end.port) ?? !horizontal;
1594
+ let plain;
1323
1595
  if (startV && endV) {
1324
1596
  const my = a.y + dy / 2;
1325
- return { kind: "polyline", points: [a, { x: a.x, y: my }, { x: b.x, y: my }, b] };
1326
- }
1327
- if (!startV && !endV) {
1597
+ plain = [a, { x: a.x, y: my }, { x: b.x, y: my }, b];
1598
+ } else if (!startV && !endV) {
1328
1599
  const mx = a.x + dx / 2;
1329
- return { kind: "polyline", points: [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b] };
1330
- }
1331
- if (startV) return { kind: "polyline", points: [a, { x: a.x, y: b.y }, b] };
1332
- return { kind: "polyline", points: [a, { x: b.x, y: a.y }, b] };
1600
+ plain = [a, { x: mx, y: a.y }, { x: mx, y: b.y }, b];
1601
+ } else if (startV) plain = [a, { x: a.x, y: b.y }, b];
1602
+ else plain = [a, { x: b.x, y: a.y }, b];
1603
+ if (c.avoid !== true) return { kind: "polyline", points: plain };
1604
+ const ends = new Set([c.start.elementID, c.end.elementID].filter((x) => x !== null));
1605
+ const routed = avoiding(plain, a, b, startV, endV, index.values(), ends);
1606
+ return { kind: "polyline", points: routed === plain ? plain : simplified(routed) };
1333
1607
  }
1334
1608
  }
1335
1609
  }
@@ -1340,7 +1614,7 @@ function arrowhead(tip, from, size) {
1340
1614
  return [wing(spread), wing(-spread)];
1341
1615
  }
1342
1616
  function connectorBounds(e, index) {
1343
- const pts = routePoints(route(e, index));
1617
+ const pts = routePoints(route2(e, index));
1344
1618
  if (pts.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
1345
1619
  let x0 = pts[0].x, y0 = pts[0].y, x1 = x0, y1 = y0;
1346
1620
  for (const p of pts) {
@@ -1352,7 +1626,7 @@ function connectorBounds(e, index) {
1352
1626
  return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
1353
1627
  }
1354
1628
  function midpoint(e, index) {
1355
- const pts = routePoints(route(e, index));
1629
+ const pts = routePoints(route2(e, index));
1356
1630
  if (pts.length <= 1) return pts[0] ?? { x: 0, y: 0 };
1357
1631
  const lengths = [];
1358
1632
  let total = 0;
@@ -1372,9 +1646,17 @@ function midpoint(e, index) {
1372
1646
  }
1373
1647
  return pts[0];
1374
1648
  }
1649
+ var indexes = /* @__PURE__ */ new WeakMap();
1650
+ function isIndexOf(page, index) {
1651
+ return indexes.get(page.elements)?.index === index;
1652
+ }
1375
1653
  function indexOf(page) {
1654
+ const els = page.elements;
1655
+ const hit = indexes.get(els);
1656
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.index;
1376
1657
  const m = /* @__PURE__ */ new Map();
1377
- for (const e of page.elements) if (!m.has(e.id)) m.set(e.id, e);
1658
+ for (const e of els) if (!m.has(e.id)) m.set(e.id, e);
1659
+ indexes.set(els, { members: [...els], index: m });
1378
1660
  return m;
1379
1661
  }
1380
1662
 
@@ -1457,43 +1739,112 @@ function packetID(flow, k) {
1457
1739
  return flow.slice(0, 24) + (n < 0 ? n + 2 ** 48 : n).toString(16).toUpperCase().padStart(12, "0");
1458
1740
  }
1459
1741
  var routeCost = (cost) => cost === null || !Number.isFinite(cost) ? 1 : Math.min(Math.max(cost, 0), 1e6);
1460
- function flowTree(from, frame2, offline = /* @__PURE__ */ new Set()) {
1461
- const shapes2 = new Set(frame2.elements.filter((e) => e.kind !== "connector" && !offline.has(e.id)).map((e) => e.id));
1462
- if (!shapes2.has(from)) return null;
1463
- const next = /* @__PURE__ */ new Map();
1464
- const add2 = (a, edge) => {
1465
- const list = next.get(a);
1466
- if (list) list.push(edge);
1467
- else next.set(a, [edge]);
1468
- };
1469
- for (const e of frame2.elements) {
1742
+ var graphs = /* @__PURE__ */ new WeakMap();
1743
+ var networks = /* @__PURE__ */ new Map();
1744
+ function graphOf(frame2) {
1745
+ const els = frame2.elements;
1746
+ const hit = graphs.get(els);
1747
+ if (hit && hit.members.length === els.length && hit.members.every((e, i) => e === els[i])) return hit.graph;
1748
+ const shapes2 = new Set(els.filter((e) => e.kind !== "connector").map((e) => e.id));
1749
+ const lines = [];
1750
+ for (const e of els) {
1470
1751
  const c = e.connector;
1471
- const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1472
- if (!c || offline.has(e.id) || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1473
- const cost = routeCost(c.cost);
1474
- add2(a, { to: b, via: e.id, back: false, cost });
1475
- if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1752
+ if (c) lines.push([e.id, c.start.elementID, c.end.elementID, String(routeCost(c.cost)), c.startHead !== "none" ? "both" : "one"]);
1753
+ }
1754
+ const key = JSON.stringify([[...shapes2], lines]);
1755
+ let graph = networks.get(key);
1756
+ if (!graph) {
1757
+ const next = /* @__PURE__ */ new Map();
1758
+ const add2 = (a, edge) => {
1759
+ const list = next.get(a);
1760
+ if (list) list.push(edge);
1761
+ else next.set(a, [edge]);
1762
+ };
1763
+ for (const e of els) {
1764
+ const c = e.connector;
1765
+ const a = c?.start.elementID ?? null, b = c?.end.elementID ?? null;
1766
+ if (!c || a === null || b === null || a === b || !shapes2.has(a) || !shapes2.has(b)) continue;
1767
+ const cost = routeCost(c.cost);
1768
+ add2(a, { to: b, via: e.id, back: false, cost });
1769
+ if (c.startHead !== "none") add2(b, { to: a, via: e.id, back: true, cost });
1770
+ }
1771
+ graph = { shapes: shapes2, next, trees: /* @__PURE__ */ new Map() };
1772
+ if (networks.size >= 16) networks.clear();
1773
+ networks.set(key, graph);
1774
+ }
1775
+ graphs.set(els, { members: [...els], graph });
1776
+ return graph;
1777
+ }
1778
+ var Frontier2 = class {
1779
+ constructor() {
1780
+ __publicField(this, "heap", []);
1781
+ }
1782
+ get size() {
1783
+ return this.heap.length;
1784
+ }
1785
+ less(i, j) {
1786
+ const a = this.heap[i], b = this.heap[j];
1787
+ return a.dist < b.dist || a.dist === b.dist && a.order < b.order;
1788
+ }
1789
+ swap(i, j) {
1790
+ [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
1791
+ }
1792
+ push(id, dist, order) {
1793
+ this.heap.push({ id, dist, order });
1794
+ for (let i = this.heap.length - 1; i > 0; ) {
1795
+ const up = i - 1 >> 1;
1796
+ if (!this.less(i, up)) break;
1797
+ this.swap(i, up);
1798
+ i = up;
1799
+ }
1800
+ }
1801
+ pop() {
1802
+ const top = this.heap[0], last = this.heap.pop();
1803
+ if (this.heap.length > 0) {
1804
+ this.heap[0] = last;
1805
+ for (let i = 0; ; ) {
1806
+ const l = 2 * i + 1, r = l + 1;
1807
+ let m = i;
1808
+ if (l < this.heap.length && this.less(l, m)) m = l;
1809
+ if (r < this.heap.length && this.less(r, m)) m = r;
1810
+ if (m === i) break;
1811
+ this.swap(i, m);
1812
+ i = m;
1813
+ }
1814
+ }
1815
+ return top;
1476
1816
  }
1817
+ };
1818
+ function flowTree(from, frame2, offline = /* @__PURE__ */ new Set(), until) {
1819
+ const graph = graphOf(frame2);
1820
+ const key = JSON.stringify([from, until ?? null, [...offline].sort()]);
1821
+ if (graph.trees.has(key)) return graph.trees.get(key);
1822
+ const tree = grow(graph, from, offline, until);
1823
+ if (graph.trees.size >= 4096) graph.trees.clear();
1824
+ graph.trees.set(key, tree);
1825
+ return tree;
1826
+ }
1827
+ function grow({ shapes: shapes2, next }, from, offline, until) {
1828
+ if (!shapes2.has(from) || offline.has(from)) return null;
1477
1829
  const dist = /* @__PURE__ */ new Map([[from, 0]]), order = /* @__PURE__ */ new Map([[from, 0]]);
1478
1830
  let counter = 1;
1479
1831
  const tree = new FlowTree(from);
1480
- const open = [from];
1481
- while (open.length > 0) {
1482
- let best = 0;
1483
- for (let i = 1; i < open.length; i++) {
1484
- const da = dist.get(open[i]), db = dist.get(open[best]);
1485
- if (da < db || da === db && order.get(open[i]) < order.get(open[best])) best = i;
1486
- }
1487
- const n = open.splice(best, 1)[0];
1832
+ const open = new Frontier2();
1833
+ open.push(from, 0, 0);
1834
+ while (open.size > 0) {
1835
+ const { id: n, order: o } = open.pop();
1836
+ if (tree.settled.has(n) || order.get(n) !== o) continue;
1488
1837
  tree.settled.add(n);
1838
+ if (n === until && n !== from) break;
1489
1839
  for (const edge of next.get(n) ?? []) {
1840
+ if (offline.has(edge.via) || offline.has(edge.to)) continue;
1490
1841
  if (tree.settled.has(edge.to)) continue;
1491
1842
  const d = dist.get(n) + edge.cost;
1492
1843
  const old = dist.get(edge.to);
1493
1844
  if (old !== void 0 && old <= d) continue;
1494
- if (old === void 0) open.push(edge.to);
1495
1845
  dist.set(edge.to, d);
1496
- order.set(edge.to, counter++);
1846
+ order.set(edge.to, counter);
1847
+ open.push(edge.to, d, counter++);
1497
1848
  tree.came.set(edge.to, { from: n, via: edge.via, back: edge.back });
1498
1849
  }
1499
1850
  }
@@ -1573,9 +1924,18 @@ function delayOf(e) {
1573
1924
  var queueKey = (shape, flow) => shape + "|" + flow;
1574
1925
  function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set(), sinceChange = null, carried = {}) {
1575
1926
  const since = Math.max(0, Math.min(sinceChange ?? t, 1e6));
1576
- const byID = /* @__PURE__ */ new Map();
1577
- for (const e of frame2.elements) if (!byID.has(e.id)) byID.set(e.id, e);
1927
+ const byID = indexOf(frame2);
1928
+ const traced = /* @__PURE__ */ new Map();
1929
+ const traceOf = (c) => {
1930
+ let pts = traced.get(c.id);
1931
+ if (!pts) {
1932
+ pts = tracing(c, frame2, byID).points;
1933
+ traced.set(c.id, pts);
1934
+ }
1935
+ return pts;
1936
+ };
1578
1937
  const metrics2 = emptyMetrics();
1938
+ const routes = /* @__PURE__ */ new Map();
1579
1939
  for (const e of frame2.elements) {
1580
1940
  if (e.kind === "connector" && !e.connector) continue;
1581
1941
  metrics2.elements.set(e.id, { throughput: 0, load: null, delay: e.kind !== "connector" ? delayOf(e) : 0, backlog: 0, inFlight: 0, depth: 0 });
@@ -1587,7 +1947,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1587
1947
  if (c !== null && Number.isFinite(c)) room.set(e.id, Math.max(c, 0));
1588
1948
  }
1589
1949
  const full = () => new Set([...room].filter(([, v]) => v <= TINY).map(([id]) => id));
1590
- const route2 = (tree, goal) => {
1950
+ const route3 = (tree, goal) => {
1591
1951
  const hops = [], shapes2 = [];
1592
1952
  let at = goal;
1593
1953
  for (let step = tree.came.get(at); step; step = tree.came.get(at)) {
@@ -1599,26 +1959,26 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1599
1959
  return { hops: hops.reverse(), shapes: shapes2.reverse() };
1600
1960
  };
1601
1961
  const way = (hops, shapes2) => {
1602
- const points = [], inside = [], stretches = [];
1962
+ const points = [], inside2 = [], stretches = [];
1603
1963
  let length = 0;
1604
1964
  hops.forEach(([id, back], k) => {
1605
1965
  const c = byID.get(id);
1606
1966
  if (!c) return;
1607
- const line2 = back ? [...tracing(c, frame2).points].reverse() : tracing(c, frame2).points;
1967
+ const line2 = back ? [...traceOf(c)].reverse() : traceOf(c);
1608
1968
  if (line2.length === 0) return;
1609
1969
  const last = points[points.length - 1];
1610
1970
  if (last) {
1611
1971
  const gap = Math.hypot(line2[0].x - last.x, line2[0].y - last.y);
1612
1972
  const shape = byID.get(shapes2[k]);
1613
- inside.push([length, length + gap, shape ? delayOf(shape) : 0]);
1973
+ inside2.push([length, length + gap, shape ? delayOf(shape) : 0]);
1614
1974
  length += gap;
1615
1975
  }
1616
- const start = length;
1976
+ const start2 = length;
1617
1977
  for (let q = 1; q < line2.length; q++) length += Math.hypot(line2[q].x - line2[q - 1].x, line2[q].y - line2[q - 1].y);
1618
- stretches.push([id, start, length]);
1978
+ stretches.push([id, start2, length]);
1619
1979
  points.push(...line2);
1620
1980
  });
1621
- return length > 0 ? { line: new Polyline(points), length, inside, stretches } : null;
1981
+ return length > 0 ? { line: new Polyline(points), length, inside: inside2, stretches } : null;
1622
1982
  };
1623
1983
  const along = (w, elapsedAt, speed) => {
1624
1984
  let elapsed = elapsedAt, d = 0;
@@ -1650,25 +2010,31 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1650
2010
  const packets = [];
1651
2011
  const waitingBy = /* @__PURE__ */ new Map();
1652
2012
  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);
2013
+ const bases = /* @__PURE__ */ new Map();
2014
+ const baseTree = (from) => {
2015
+ if (!bases.has(from)) bases.set(from, flowTree(from, frame2, offline));
2016
+ return bases.get(from);
2017
+ };
1653
2018
  for (const [i, flow] of list.entries()) {
1654
2019
  if (flow.when !== null && values !== null && !holds(flow.when, values)) continue;
1655
2020
  const source = byID.get(flow.from);
1656
- const base2 = source ? flowTree(flow.from, frame2, offline) : null;
2021
+ const base2 = source ? baseTree(flow.from) : null;
2022
+ routes.set(flow.id, []);
1657
2023
  if (!source || !base2) continue;
1658
2024
  const volume = amountIn(flow, values);
1659
2025
  const fm = { volume, placed: 0, backlog: 0, backup: 0, transit: null };
1660
2026
  const placements = [];
1661
- const waiting = [];
2027
+ const waiting2 = [];
1662
2028
  const goals = base2.goals(flow.to, flow.spread === true);
1663
2029
  const shares = goals.length > 1 ? goals.map((g) => [[g], volume / goals.length]) : [[flow.to, volume]];
1664
2030
  for (const [candidates, share] of shares) {
1665
2031
  if (!(share > TINY)) continue;
1666
2032
  let left = share;
1667
2033
  for (let n = 0; n < 32 && left > TINY; n++) {
1668
- const tree = flowTree(flow.from, frame2, union2(offline, full()));
2034
+ const tree = flowTree(flow.from, frame2, union2(offline, full()), candidates[0]);
1669
2035
  const goal2 = tree?.goals(candidates, false)[0];
1670
2036
  if (!tree || goal2 === void 0) break;
1671
- const r = route2(tree, goal2);
2037
+ const r = route3(tree, goal2);
1672
2038
  const ids = [...r.shapes, ...r.hops.map(([id]) => id)];
1673
2039
  const rooms = ids.filter((id) => room.has(id)).map((id) => room.get(id));
1674
2040
  const fits = Math.min(left, rooms.length > 0 ? Math.min(...rooms) : Infinity);
@@ -1681,7 +2047,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1681
2047
  let at = flow.from;
1682
2048
  const goal = base2.goals(candidates, false)[0];
1683
2049
  if (goal !== void 0) {
1684
- const r = route2(base2, goal), done = full();
2050
+ const r = route3(base2, goal), done = full();
1685
2051
  const sequence = [[r.shapes[0], r.shapes[0]]];
1686
2052
  r.hops.forEach(([id], k) => {
1687
2053
  sequence.push([id, r.shapes[k]], [r.shapes[k + 1], r.shapes[k]]);
@@ -1705,18 +2071,25 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1705
2071
  placements.push({ hops, shapes: shapes2, goal: at, amount: left, waits: true });
1706
2072
  }
1707
2073
  }
1708
- const existing = waiting.find(([s]) => s === at);
2074
+ const existing = waiting2.find(([s]) => s === at);
1709
2075
  if (existing) existing[1] += left;
1710
- else waiting.push([at, left]);
1711
- }
1712
- waitingBy.set(flow.id, waiting);
2076
+ else waiting2.push([at, left]);
2077
+ }
2078
+ waitingBy.set(flow.id, waiting2);
2079
+ routes.set(flow.id, placements.map((p) => ({
2080
+ goal: p.goal,
2081
+ hops: p.hops,
2082
+ amount: p.amount,
2083
+ waits: p.waits === true,
2084
+ cost: p.hops.reduce((s, [id]) => s + routeCost(byID.get(id)?.connector?.cost ?? null), 0)
2085
+ })));
1713
2086
  for (const p of placements) {
1714
2087
  for (const id of [...p.shapes, ...p.hops.map(([h]) => h)]) bump(id, (m) => {
1715
2088
  m.throughput += p.amount;
1716
2089
  });
1717
2090
  if (!p.waits) fm.placed += p.amount;
1718
2091
  }
1719
- for (const [at, a] of waiting) {
2092
+ for (const [at, a] of waiting2) {
1720
2093
  bump(at, (m) => {
1721
2094
  m.backlog += a;
1722
2095
  });
@@ -1761,10 +2134,12 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1761
2134
  metrics2.flows.set(flow.id, fm);
1762
2135
  }
1763
2136
  const piles = /* @__PURE__ */ new Map();
2137
+ const carriedKeys = Object.keys(carried).filter((k) => carried[k] > TINY);
1764
2138
  for (const flow of list) {
1765
- const waiting = waitingBy.get(flow.id) ?? [];
1766
- const shapes2 = waiting.map(([s]) => s);
1767
- for (const e of frame2.elements) {
2139
+ const waiting2 = waitingBy.get(flow.id) ?? [];
2140
+ const shapes2 = waiting2.map(([s]) => s);
2141
+ const suffix = "|" + flow.id;
2142
+ if (carriedKeys.some((k) => k.endsWith(suffix))) for (const e of frame2.elements) {
1768
2143
  const c = carried[queueKey(e.id, flow.id)];
1769
2144
  if (c !== void 0 && c > TINY && !shapes2.includes(e.id)) shapes2.push(e.id);
1770
2145
  }
@@ -1778,7 +2153,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1778
2153
  const shape = byID.get(at);
1779
2154
  if (!shape) return;
1780
2155
  const layer = byID.get(flow.from)?.layerID ?? shape.layerID;
1781
- const backlog = waiting.find(([s]) => s === at)?.[1] ?? 0;
2156
+ const backlog = waiting2.find(([s]) => s === at)?.[1] ?? 0;
1782
2157
  const before = Math.max(0, carried[queueKey(at, flow.id)] ?? 0);
1783
2158
  let depth = before + backlog * since;
1784
2159
  if (backlog <= TINY && before > TINY) {
@@ -1787,10 +2162,10 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1787
2162
  let total = 0;
1788
2163
  for (let n2 = 0; n2 < 32; n2++) {
1789
2164
  const blocked = new Set([...trial].filter(([, v]) => v <= TINY).map(([id]) => id));
1790
- const tree = flowTree(at, frame2, union2(offline, blocked));
2165
+ const tree = flowTree(at, frame2, union2(offline, blocked), flow.to[0]);
1791
2166
  const goal = tree?.goals(flow.to, false)[0];
1792
2167
  if (!tree || goal === void 0) break;
1793
- const r = route2(tree, goal);
2168
+ const r = route3(tree, goal);
1794
2169
  const ids = [...r.shapes.slice(1), ...r.hops.map(([id]) => id)];
1795
2170
  const rooms = ids.filter((id) => trial.has(id)).map((id) => trial.get(id));
1796
2171
  const spare = rooms.length > 0 ? Math.min(...rooms) : Infinity;
@@ -1845,7 +2220,7 @@ function flowFrame(flows, frame2, t, values, offline = /* @__PURE__ */ new Set()
1845
2220
  if (c === null || !Number.isFinite(c) || !m) continue;
1846
2221
  m.load = c > 0 ? m.throughput / c : 1;
1847
2222
  }
1848
- return { packets, metrics: metrics2 };
2223
+ return { packets, metrics: metrics2, routes };
1849
2224
  }
1850
2225
  var BLANK = {
1851
2226
  seed: 1,
@@ -1900,21 +2275,21 @@ function metricLookup(m, frame2) {
1900
2275
  return id === null ? null : metricValue(m, term.metric, id);
1901
2276
  };
1902
2277
  }
1903
- function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2278
+ function finishPlan(elements, metrics2, values, frame2, since = Infinity, carried = {}) {
1904
2279
  const lookup = metricLookup(metrics2, frame2);
1905
- const out = [], readings = {};
1906
- for (let e of elements) {
2280
+ const plan2 = [], readings = {};
2281
+ for (const e of elements) {
2282
+ const applied = [];
1907
2283
  if (values !== null && e.overrides) {
1908
- for (const o of e.overrides) {
1909
- const own = e.id;
1910
- if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, own))) e = applyOverride(looksOnly(o), e);
1911
- }
2284
+ e.overrides.forEach((o, k) => {
2285
+ if (mentionsMetric(o.when) && holds(o.when, values, (m) => lookup(m, e.id))) applied.push(k);
2286
+ });
1912
2287
  }
1913
- if (!e.gauge || values === null) {
1914
- out.push(e);
2288
+ const g = values === null ? null : e.gauge;
2289
+ if (!g) {
2290
+ plan2.push({ applied, gauge: null });
1915
2291
  continue;
1916
2292
  }
1917
- const g = e.gauge;
1918
2293
  let shown = metricValue(metrics2, g.metric, g.of);
1919
2294
  const then = carried[gaugeKey(e.id)];
1920
2295
  if (shown !== null && then !== void 0 && g.metric !== "depth" && g.metric !== "inFlight" && since < GAUGE_EASE) {
@@ -1922,22 +2297,33 @@ function finishing(elements, metrics2, values, frame2, flows, since = Infinity,
1922
2297
  shown = then + (shown - then) * smooth;
1923
2298
  }
1924
2299
  if (shown !== null) readings[gaugeKey(e.id)] = shown;
1925
- out.push(...gauge(e, g, shown, frame2, flows));
2300
+ plan2.push({ applied, gauge: { value: shown } });
1926
2301
  }
2302
+ return { plan: plan2, readings };
2303
+ }
2304
+ function finishing(elements, metrics2, values, frame2, flows, since = Infinity, carried = {}) {
2305
+ const { plan: plan2, readings } = finishPlan(elements, metrics2, values, frame2, since, carried);
2306
+ const out = [];
2307
+ elements.forEach((e, i) => {
2308
+ for (const k of plan2[i].applied) e = applyOverride(looksOnly(e.overrides[k]), e);
2309
+ const g = plan2[i].gauge;
2310
+ if (g && e.gauge) out.push(...gauge(e, e.gauge, g.value, frame2, flows));
2311
+ else out.push(e);
2312
+ });
1927
2313
  return { elements: out, readings };
1928
2314
  }
1929
2315
  var gaugeKey = (id) => "gauge|" + id;
1930
2316
  var GAUGE_EASE = 1;
1931
- var hex = (h) => {
2317
+ var hex2 = (h) => {
1932
2318
  const v = parseInt(h.slice(1), 16);
1933
2319
  return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255, a: 1 };
1934
2320
  };
1935
- var GAUGE_GREEN = hex("#16a34a");
1936
- var GAUGE_AMBER = hex("#d97706");
1937
- var GAUGE_RED = hex("#dc2626");
1938
- var GAUGE_BLUE = hex("#2f6fdf");
1939
- var GAUGE_TRACK = hex("#e5e7eb");
1940
- var GAUGE_GRAY = hex("#9ca3af");
2321
+ var GAUGE_GREEN = hex2("#16a34a");
2322
+ var GAUGE_AMBER = hex2("#d97706");
2323
+ var GAUGE_RED = hex2("#dc2626");
2324
+ var GAUGE_BLUE = hex2("#2f6fdf");
2325
+ var GAUGE_TRACK = hex2("#e5e7eb");
2326
+ var GAUGE_GRAY = hex2("#9ca3af");
1941
2327
  function formatted(metric, v) {
1942
2328
  if (v === null || !Number.isFinite(v)) return "\u2013";
1943
2329
  switch (metric) {
@@ -2069,7 +2455,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2069
2455
  const bow = offset(rng, length / 60, roughness) * bowing * gain;
2070
2456
  const dx = b.x - a.x, dy = b.y - a.y;
2071
2457
  const nx = -dy / length, ny = dx / length;
2072
- const start = preserveEnds ? a : (() => {
2458
+ const start2 = preserveEnds ? a : (() => {
2073
2459
  const x = a.x + j();
2074
2460
  return { x, y: a.y + j() };
2075
2461
  })();
@@ -2081,7 +2467,7 @@ function line(a, b, rng, roughness, bowing, preserveEnds, into) {
2081
2467
  const c1y = a.y + dy * mid * 0.66 + ny * bow + j() * 0.5;
2082
2468
  const c2x = a.x + dx * (0.5 + mid * 0.5) + nx * bow + j() * 0.5;
2083
2469
  const c2y = a.y + dy * (0.5 + mid * 0.5) + ny * bow + j() * 0.5;
2084
- into.moveTo(start);
2470
+ into.moveTo(start2);
2085
2471
  into.curveTo(end, { x: c1x, y: c1y }, { x: c2x, y: c2y });
2086
2472
  }
2087
2473
  }
@@ -2223,17 +2609,17 @@ function clusterStrokes(c, angle2, size) {
2223
2609
  });
2224
2610
  }
2225
2611
  function contains(contours, p) {
2226
- let inside = false;
2612
+ let inside2 = false;
2227
2613
  for (const poly of contours) {
2228
2614
  if (poly.length < 3) continue;
2229
2615
  let k = poly.length - 1;
2230
2616
  for (let i = 0; i < poly.length; i++) {
2231
2617
  const a = poly[i], b = poly[k];
2232
- 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;
2618
+ 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;
2233
2619
  k = i;
2234
2620
  }
2235
2621
  }
2236
- return inside;
2622
+ return inside2;
2237
2623
  }
2238
2624
  function distance(p, contours) {
2239
2625
  let best = Infinity;
@@ -2342,10 +2728,10 @@ var Cache = class {
2342
2728
  __publicField(this, "limit");
2343
2729
  this.limit = limit;
2344
2730
  }
2345
- get(key, make) {
2731
+ get(key, make2) {
2346
2732
  const hit = this.map.get(key);
2347
2733
  if (hit !== void 0) return hit;
2348
- const v = make();
2734
+ const v = make2();
2349
2735
  if (this.map.size >= this.limit) this.map.clear();
2350
2736
  this.map.set(key, v);
2351
2737
  return v;
@@ -2510,7 +2896,7 @@ function connectorParts(e, index, page) {
2510
2896
  const c = e.connector;
2511
2897
  if (!c) return [new Path(), new Path()];
2512
2898
  const [a, b] = endpoints(e, index);
2513
- const r = route(e, index);
2899
+ const r = route2(e, index);
2514
2900
  const hops = c.hops === true && page ? hopCrossings(e, r, page, index) : [];
2515
2901
  const key = [
2516
2902
  e.seed,
@@ -2569,26 +2955,59 @@ function subCubic(p, t0, t1) {
2569
2955
  const left = split(p, t1)[0];
2570
2956
  return t1 > 0 ? split(left, t0 / t1)[1] : [p[0], p[0], p[0], p[0]];
2571
2957
  }
2958
+ function boxOf(pts) {
2959
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
2960
+ for (const p of pts) {
2961
+ x0 = Math.min(x0, p.x);
2962
+ y0 = Math.min(y0, p.y);
2963
+ x1 = Math.max(x1, p.x);
2964
+ y1 = Math.max(y1, p.y);
2965
+ }
2966
+ return { x0, y0, x1, y1 };
2967
+ }
2968
+ function touching(a, b) {
2969
+ const eps = 1e-6;
2970
+ return a.x0 <= b.x1 + eps && b.x0 <= a.x1 + eps && a.y0 <= b.y1 + eps && b.y0 <= a.y1 + eps;
2971
+ }
2972
+ var hopFrames = /* @__PURE__ */ new WeakMap();
2973
+ function linesBeneath(e, page, index) {
2974
+ let f = hopFrames.get(index);
2975
+ if (!f || f.layers !== page.layers || !isIndexOf(page, index)) {
2976
+ const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2977
+ const first = /* @__PURE__ */ new Map(), under = [];
2978
+ for (const o of orderedElements(page)) {
2979
+ if (!first.has(o.id)) first.set(o.id, under.length);
2980
+ if (o.kind === "connector" && visible.has(o.layerID)) under.push(o);
2981
+ }
2982
+ f = { layers: page.layers, first, under, lines: [] };
2983
+ if (isIndexOf(page, index)) hopFrames.set(index, f);
2984
+ }
2985
+ const count = f.first.get(e.id) ?? f.under.length;
2986
+ while (f.lines.length < count) {
2987
+ const points = routePoints(route2(f.under[f.lines.length], index));
2988
+ f.lines.push({ points, box: boxOf(points) });
2989
+ }
2990
+ return { lines: f.lines, count };
2991
+ }
2572
2992
  function hopCrossings(e, r, page, index) {
2573
2993
  const segments = hopSamples(r);
2574
2994
  if (segments.length === 0) return [];
2575
- const visible = new Set(page.layers.filter((l) => l.visible).map((l) => l.id));
2576
- const beneath = [];
2577
- for (const o of orderedElements(page)) {
2578
- if (o.id === e.id) break;
2579
- if (o.kind !== "connector" || !visible.has(o.layerID)) continue;
2580
- beneath.push(routePoints(route(o, index)));
2581
- }
2995
+ const { lines, count } = linesBeneath(e, page, index);
2582
2996
  const rad = hopRadius(e.style.strokeWidth);
2583
2997
  const out = [];
2584
2998
  for (const samples of segments) {
2585
2999
  const along = runningLengths(samples);
2586
3000
  const length = along[along.length - 1];
2587
3001
  const found = [];
3002
+ const near = [];
3003
+ const whole = boxOf(samples);
3004
+ for (let n = 0; n < count; n++) if (touching(whole, lines[n].box)) near.push(lines[n]);
2588
3005
  for (let i = 0; i < samples.length - 1; i++) {
2589
3006
  const p = samples[i], q = samples[i + 1];
2590
3007
  const dx = q.x - p.x, dy = q.y - p.y;
2591
- for (const line2 of beneath) {
3008
+ const box = boxOf([p, q]);
3009
+ for (const { points: line2, box: lineBox } of near) {
3010
+ if (!touching(box, lineBox)) continue;
2592
3011
  for (let k = 0; k < line2.length - 1; k++) {
2593
3012
  const a = line2[k], b = line2[k + 1];
2594
3013
  const ex = b.x - a.x, ey = b.y - a.y;
@@ -2868,8 +3287,8 @@ function followSpan(track, t) {
2868
3287
  if (track.keys.some((k) => k.t >= span.end && k.t <= t && (k.x !== void 0 || k.y !== void 0))) return null;
2869
3288
  return f;
2870
3289
  }
2871
- function tracing(e, page) {
2872
- if (e.kind === "connector") return new Polyline(routePoints(route(e, indexOf(page))));
3290
+ function tracing(e, page, index = indexOf(page)) {
3291
+ if (e.kind === "connector") return new Polyline(routePoints(route2(e, index)));
2873
3292
  const clean = cloneElement(e);
2874
3293
  clean.style.roughness = 0;
2875
3294
  const pts = outline(clean).contours()[0] ?? [];
@@ -2879,9 +3298,10 @@ function following(track, e, t, page) {
2879
3298
  const f = followSpan(track, t);
2880
3299
  if (!f) return e;
2881
3300
  t = localTime(track, t);
2882
- const pathElement = page.elements.find((x) => x.id === f.path);
3301
+ const index = indexOf(page);
3302
+ const pathElement = index.get(f.path);
2883
3303
  if (!pathElement || pathElement.id === e.id) return e;
2884
- const line2 = tracing(pathElement, page);
3304
+ const line2 = tracing(pathElement, page, index);
2885
3305
  const span = f.end - f.start;
2886
3306
  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);
2887
3307
  if (u > 1) u = rem(u, 1) === 0 ? 1 : rem(u, 1);
@@ -3136,17 +3556,30 @@ function symbolPage(s) {
3136
3556
  function hasInstances(page) {
3137
3557
  return page.elements.some((e) => e.kind === "symbol");
3138
3558
  }
3139
- function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map()) {
3559
+ var Expansions = class {
3560
+ constructor(page, t, posing) {
3561
+ __publicField(this, "page");
3562
+ __publicField(this, "t");
3563
+ __publicField(this, "posing");
3564
+ /** For each instance: its time override, the budget it found and left, and what it made. */
3565
+ __publicField(this, "made", /* @__PURE__ */ new Map());
3566
+ this.page = page;
3567
+ this.t = t;
3568
+ this.posing = posing;
3569
+ }
3570
+ };
3571
+ function expandingSymbols(page, t, posing = true, times = /* @__PURE__ */ new Map(), memo2) {
3140
3572
  if (!hasInstances(page)) return page;
3141
3573
  const budget = { left: MAX_ELEMENTS };
3142
- return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times) };
3574
+ const m = memo2 && memo2.page === page && memo2.t === t && memo2.posing === posing ? memo2 : void 0;
3575
+ return { ...page, elements: expand(page.elements, page, t, symbolTable(page), [], posing, budget, times, m) };
3143
3576
  }
3144
3577
  function symbolTable(page) {
3145
3578
  const table = /* @__PURE__ */ new Map();
3146
3579
  for (const s of page.symbols ?? []) if (!table.has(s.id)) table.set(s.id, s);
3147
3580
  return table;
3148
3581
  }
3149
- function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map()) {
3582
+ function expand(elements, page, t, table, stack, posing, budget, times = /* @__PURE__ */ new Map(), memo2) {
3150
3583
  const instances = new Set(elements.filter((e) => e.kind === "symbol").map((e) => e.id));
3151
3584
  if (instances.size === 0) {
3152
3585
  budget.left -= elements.length;
@@ -3171,6 +3604,14 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3171
3604
  budget.left -= 1;
3172
3605
  continue;
3173
3606
  }
3607
+ const override0 = stack.length === 0 ? times.get(e.id) : void 0;
3608
+ const was = memo2?.made.get(e);
3609
+ if (was && was.override === override0 && was.after > 0 && budget.left - (was.before - was.after) > 0) {
3610
+ budget.left -= was.before - was.after;
3611
+ for (const x of was.out) out.push(x);
3612
+ continue;
3613
+ }
3614
+ const before = budget.left, from = out.length;
3174
3615
  budget.left -= 1;
3175
3616
  const id = e.symbol;
3176
3617
  const symbol = id === null ? void 0 : table.get(id);
@@ -3186,6 +3627,7 @@ function expand(elements, page, t, table, stack, posing, budget, times = /* @__P
3186
3627
  layers: [{ id: e.layerID, name: "", visible: true, locked: false }]
3187
3628
  };
3188
3629
  for (const x of expand(drawn.elements, drawn, u, table, [...stack, id], posing, budget)) out.push(placed(x, e, symbol));
3630
+ if (memo2 && stack.length === 0 && !was) memo2.made.set(e, { override: override0, before, after: budget.left, out: out.slice(from) });
3189
3631
  }
3190
3632
  return out;
3191
3633
  }
@@ -3238,8 +3680,8 @@ function mix2(x) {
3238
3680
  return z ^ z >> 31n;
3239
3681
  }
3240
3682
  function halves(id) {
3241
- const hex2 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3242
- return [BigInt(`0x${hex2.slice(0, 16)}`), BigInt(`0x${hex2.slice(16)}`)];
3683
+ const hex3 = id.replace(/-/g, "").padStart(32, "0").slice(-32);
3684
+ return [BigInt(`0x${hex3.slice(0, 16)}`), BigInt(`0x${hex3.slice(16)}`)];
3243
3685
  }
3244
3686
  function derivedID(instance, inner) {
3245
3687
  const [a, b] = halves(instance), [c, d] = halves(inner);
@@ -3453,13 +3895,13 @@ function blocks(lines) {
3453
3895
  flush();
3454
3896
  return out;
3455
3897
  }
3456
- function parseList(lines, start) {
3457
- const first = reItem.exec(lines[start]);
3898
+ function parseList(lines, start2) {
3899
+ const first = reItem.exec(lines[start2]);
3458
3900
  const indent = first[1].length;
3459
3901
  const bullet = /^\d/.test(first[2]) ? null : first[2];
3460
3902
  const ordered = bullet === null ? parseInt(first[2], 10) : null;
3461
3903
  const items = [];
3462
- let i = start;
3904
+ let i = start2;
3463
3905
  while (i < lines.length) {
3464
3906
  const m = reItem.exec(lines[i]);
3465
3907
  if (!m || m[1].length !== indent || bullet === null !== /^\d/.test(m[2]) || bullet !== null && m[2] !== bullet) break;
@@ -3567,12 +4009,23 @@ function inline(src, style2 = {}) {
3567
4009
  i += m[0].length;
3568
4010
  continue;
3569
4011
  }
3570
- if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4012
+ if (m = /^(\*\*\*|___)(?=\S)([\s\S]*?\S)\1(?!\*|_)/.exec(rest)) {
3571
4013
  emit();
3572
- out.push(...inline(m[2], { ...style2, bold: true }));
4014
+ out.push(...inline(m[2], { ...style2, bold: true, italic: true }));
3573
4015
  i += m[0].length;
3574
4016
  continue;
3575
4017
  }
4018
+ if (m = /^(\*\*|__)(?=\S)([\s\S]*?\S)\1/.exec(rest)) {
4019
+ let body = m[2], length = m[0].length;
4020
+ if (m[1] === "**" && rest[length] === "*" && (body.replace(/\*\*/g, "").match(/\*/g) ?? []).length % 2 === 1) {
4021
+ body += "*";
4022
+ length++;
4023
+ }
4024
+ emit();
4025
+ out.push(...inline(body, { ...style2, bold: true }));
4026
+ i += length;
4027
+ continue;
4028
+ }
3576
4029
  if ((m = /^(\*|_)(?=\S)([\s\S]*?\S)\1(?!\1)/.exec(rest)) && (m[1] === "*" || !/\w/.test(src[i - 1] ?? ""))) {
3577
4030
  emit();
3578
4031
  out.push(...inline(m[2], { ...style2, italic: true }));
@@ -3788,10 +4241,10 @@ function layoutLines(paras, width, height, fontSize, fam) {
3788
4241
  let k = 0;
3789
4242
  let firstLine = true;
3790
4243
  while (k < chars.length || firstLine && chars.length === 0) {
3791
- const start = firstLine ? p.firstIndent : p.indent;
4244
+ const start2 = firstLine ? p.firstIndent : p.indent;
3792
4245
  const maxX2 = width;
3793
4246
  const glyphs = [];
3794
- let x = start, lastSpace = -1, end = k;
4247
+ let x = start2, lastSpace = -1, end = k;
3795
4248
  for (; end < chars.length; end++) {
3796
4249
  const { ch, run } = chars[end];
3797
4250
  const r = runs[run];
@@ -3840,7 +4293,7 @@ function layoutLines(paras, width, height, fontSize, fam) {
3840
4293
  }
3841
4294
  if (glyphs.length === 0 && end === k && k < chars.length) {
3842
4295
  const { ch, run } = chars[k];
3843
- glyphs.push({ ch, run, x: start, w: charWidth(ch, runs[run], fam) });
4296
+ glyphs.push({ ch, run, x: start2, w: charWidth(ch, runs[run], fam) });
3844
4297
  end = k + 1;
3845
4298
  }
3846
4299
  let asc = 0, desc = 0, lead = 0;
@@ -3866,13 +4319,38 @@ function layoutLines(paras, width, height, fontSize, fam) {
3866
4319
  });
3867
4320
  return lines;
3868
4321
  }
4322
+ var Memo = class {
4323
+ constructor(limit) {
4324
+ __publicField(this, "map", /* @__PURE__ */ new Map());
4325
+ __publicField(this, "limit");
4326
+ this.limit = limit;
4327
+ }
4328
+ get(key, make2) {
4329
+ const hit = this.map.get(key);
4330
+ if (hit !== void 0) return hit;
4331
+ const v = make2();
4332
+ if (this.map.size >= this.limit) this.map.clear();
4333
+ this.map.set(key, v);
4334
+ return v;
4335
+ }
4336
+ };
4337
+ var layouts = new Memo(500);
4338
+ var markups = new Memo(500);
3869
4339
  function noteMarkup(source, e) {
4340
+ const st = e.style, ink = st.stroke;
4341
+ 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]);
4342
+ return markups.get(key, () => computeNoteMarkup(source, e));
4343
+ }
4344
+ function computeNoteMarkup(source, e) {
3870
4345
  const rect2 = textRect({ x: e.x, y: e.y, width: e.width, height: e.height });
3871
4346
  if (!(rect2.width > 1 && rect2.height > 1)) return "";
3872
4347
  const fam = familyMetrics(e.style.font);
3873
- const b = new Builder(e.style.fontSize, rect2.width, fam);
3874
- for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
3875
- const lines = layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4348
+ const layoutKey = JSON.stringify([...[rect2.width, rect2.height, e.style.fontSize].map(String), e.style.font ?? "", source]);
4349
+ const lines = layouts.get(layoutKey, () => {
4350
+ const b = new Builder(e.style.fontSize, rect2.width, fam);
4351
+ for (const block of parseBlocks(source)) b.block(block, { indent: 0, marker: null, quote: null, listDepth: 0 });
4352
+ return layoutLines(b.paras, rect2.width, rect2.height, e.style.fontSize, fam);
4353
+ });
3876
4354
  const ink = color(e.style.stroke);
3877
4355
  let s = "";
3878
4356
  const marks = [];
@@ -3931,20 +4409,6 @@ function sameStyle(a, b) {
3931
4409
  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;
3932
4410
  }
3933
4411
 
3934
- // src/theme.ts
3935
- var rgb = (r, g, b) => ({ r, g, b, a: 1 });
3936
- var themes = [
3937
- { id: "paper", name: "Paper", background: rgb(0.984, 0.98, 0.965), ink: rgb(0.12, 0.12, 0.13) },
3938
- { id: "white", name: "White", background: rgb(1, 1, 1), ink: rgb(0.12, 0.12, 0.13) },
3939
- { id: "sepia", name: "Sepia", background: rgb(0.953, 0.914, 0.824), ink: rgb(0.23, 0.18, 0.13) },
3940
- { id: "blueprint", name: "Blueprint", background: rgb(0.106, 0.227, 0.361), ink: rgb(0.91, 0.945, 0.984) },
3941
- { id: "chalkboard", name: "Chalkboard", background: rgb(0.169, 0.227, 0.2), ink: rgb(0.945, 0.945, 0.918) },
3942
- { id: "midnight", name: "Midnight", background: rgb(0.082, 0.086, 0.102), ink: rgb(0.925, 0.925, 0.94) }
3943
- ];
3944
- function themeNamed(id) {
3945
- return themes.find((t) => t.id === id) ?? null;
3946
- }
3947
-
3948
4412
  // src/svg.ts
3949
4413
  function bounds(page, padding = 24) {
3950
4414
  if (hasInstances(page)) return bounds(expandingSymbols(page, 0), padding);
@@ -3961,6 +4425,26 @@ function bounds(page, padding = 24) {
3961
4425
  return inset(r, -padding, -padding);
3962
4426
  }
3963
4427
  function plan(page) {
4428
+ const hit = plans.get(page);
4429
+ if (hit && unchanged(page, hit.fields)) return hit.plan;
4430
+ const made = makePlan(page);
4431
+ plans.set(page, { fields: { ...page }, plan: made });
4432
+ return made;
4433
+ }
4434
+ var plans = /* @__PURE__ */ new WeakMap();
4435
+ function hasPlan(page) {
4436
+ const hit = plans.get(page);
4437
+ return !!hit && unchanged(page, hit.fields);
4438
+ }
4439
+ function rememberPlan(page, made) {
4440
+ plans.set(page, { fields: { ...page }, plan: made });
4441
+ }
4442
+ function unchanged(page, fields) {
4443
+ const a = page, b = fields;
4444
+ const keys = Object.keys(a);
4445
+ return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
4446
+ }
4447
+ function makePlan(page) {
3964
4448
  const tl = page.timeline ?? { fps: 12, duration: 0, boil: null, settle: null, poster: null, tracks: [], camera: null, markers: null };
3965
4449
  const times = frameTimes(tl);
3966
4450
  const first = tl.camera?.find((k) => k.width > 1 && k.height > 1);
@@ -3988,9 +4472,9 @@ function svgOpen(b, background, paint) {
3988
4472
  }
3989
4473
  return out;
3990
4474
  }
3991
- function svgParts(page, background, fixed, links = false) {
3992
- if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links);
3993
- const theme = themeNamed(page.theme);
4475
+ function svgParts(page, background, fixed, links = false, only) {
4476
+ if (hasInstances(page)) return svgParts(expandingSymbols(page, 0), background, fixed, links, only);
4477
+ const theme = pageTheme(page);
3994
4478
  const bg = background ? theme?.background ?? background : null;
3995
4479
  const ink = theme?.ink ?? black;
3996
4480
  const d = { ...page, elements: page.elements.map((e) => resolvingInk(e, ink)) };
@@ -4000,11 +4484,11 @@ function svgParts(page, background, fixed, links = false) {
4000
4484
  const index = indexOf(d);
4001
4485
  const ordered = orderedElements(d);
4002
4486
  const floors = ordered.filter((e) => visible.has(e.layerID) && isFloor(e));
4003
- 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"/>
4487
+ 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"/>
4004
4488
  `).join("");
4005
4489
  if (rockMarkup) parts.push({ key: "rock", markup: rockMarkup });
4006
4490
  for (const e of ordered) {
4007
- if (!visible.has(e.layerID)) continue;
4491
+ if (!visible.has(e.layerID) || only && !only(e.id)) continue;
4008
4492
  const alpha = Math.min(Math.max(e.style.opacity ?? 1, 0), 1);
4009
4493
  if (alpha <= 0) continue;
4010
4494
  let body = svgElement(e, index, bg, d);
@@ -4026,7 +4510,7 @@ ${body}</a>
4026
4510
  }
4027
4511
  function svg(page, background = { r: 1, g: 1, b: 1, a: 1 }, fixed) {
4028
4512
  const { bounds: b, parts } = svgParts(page, background, fixed, true);
4029
- return svgOpen(b, themeNamed(page.theme)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4513
+ return svgOpen(b, pageTheme(page)?.background ?? background, background !== null) + parts.map((p) => p.markup).join("") + "</svg>\n";
4030
4514
  }
4031
4515
  function labelRect(e, index) {
4032
4516
  const m = midpoint(e, index);
@@ -4120,7 +4604,7 @@ function svgElement(e, index, labelBackground, page) {
4120
4604
  }
4121
4605
  s += `<path d="${outline(e).data()}" fill="none" ${strokeAttrs}/>
4122
4606
  `;
4123
- if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" ? pathLabelBox(e) : frame(e), e, true);
4607
+ if (text) s += e.kind === "note" ? noteMarkup(text, e) : textElement(text, e.kind === "path" || e.kind === "rectangle" ? pathLabelBox(e) : frame(e), e, true);
4124
4608
  return s;
4125
4609
  }
4126
4610
  default:
@@ -4152,6 +4636,95 @@ function textElement(text, f, e, centered) {
4152
4636
  return s + "</text>\n";
4153
4637
  }
4154
4638
 
4639
+ // src/planner.ts
4640
+ var WORKER_WORK = 3e4;
4641
+ function planWork(page) {
4642
+ const tl = page.timeline;
4643
+ if (!tl || tl.camera?.some((k) => k.width > 1 && k.height > 1)) return 0;
4644
+ const elements = page.elements.length + (page.symbols ?? []).reduce((n, s) => n + s.elements.length, 0);
4645
+ return frameTimes(tl).length * elements;
4646
+ }
4647
+ function workerMaker() {
4648
+ if (typeof Worker === "undefined" || typeof __DOTTIE_WORKER__ === "undefined") return null;
4649
+ if (__DOTTIE_WORKER__ === "self" && typeof dottiePlayerScript === "function") {
4650
+ const source = `(${dottiePlayerScript.toString()})();
4651
+ `;
4652
+ return () => {
4653
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
4654
+ try {
4655
+ return new Worker(url);
4656
+ } finally {
4657
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
4658
+ }
4659
+ };
4660
+ }
4661
+ if (__DOTTIE_WORKER__ === "sibling") {
4662
+ const here = import.meta.url;
4663
+ if (!here) return null;
4664
+ return () => new Worker(new URL("./dottie-player.min.js", here));
4665
+ }
4666
+ return null;
4667
+ }
4668
+ var make;
4669
+ var worker = null;
4670
+ var failed = false;
4671
+ var nextID = 1;
4672
+ var waiting = /* @__PURE__ */ new Map();
4673
+ function plansInWorker(page) {
4674
+ if (failed || hasPlan(page) || planWork(page) < WORKER_WORK) return false;
4675
+ if (make === void 0) make = workerMaker();
4676
+ return make !== null;
4677
+ }
4678
+ function planInWorker(page) {
4679
+ return new Promise((resolve2) => {
4680
+ const done = (p) => {
4681
+ if (p) rememberPlan(page, p);
4682
+ resolve2();
4683
+ };
4684
+ let w;
4685
+ try {
4686
+ w = worker ?? start();
4687
+ } catch {
4688
+ failed = true;
4689
+ done(null);
4690
+ return;
4691
+ }
4692
+ const id = nextID++;
4693
+ waiting.set(id, { page, done });
4694
+ try {
4695
+ w.postMessage({ dottiePlan: id, page });
4696
+ } catch {
4697
+ waiting.delete(id);
4698
+ done(null);
4699
+ }
4700
+ });
4701
+ }
4702
+ function start() {
4703
+ const w = make();
4704
+ w.onmessage = (ev) => {
4705
+ const { dottiePlan: id, plan: p } = ev.data ?? {};
4706
+ const job = id === void 0 ? void 0 : waiting.get(id);
4707
+ if (!job) return;
4708
+ waiting.delete(id);
4709
+ job.done(p ?? null);
4710
+ if (waiting.size === 0 && worker === w) {
4711
+ w.terminate();
4712
+ worker = null;
4713
+ }
4714
+ };
4715
+ w.onerror = (ev) => {
4716
+ ev.preventDefault?.();
4717
+ failed = true;
4718
+ if (worker === w) worker = null;
4719
+ w.terminate();
4720
+ const jobs = [...waiting.values()];
4721
+ waiting.clear();
4722
+ for (const job of jobs) job.done(null);
4723
+ };
4724
+ worker = w;
4725
+ return w;
4726
+ }
4727
+
4155
4728
  // src/player.ts
4156
4729
  var WHITE = { r: 1, g: 1, b: 1, a: 1 };
4157
4730
  function pageIndex(file, spec) {
@@ -4221,7 +4794,7 @@ var Playback = class {
4221
4794
  __publicField(this, "order", []);
4222
4795
  __publicField(this, "frame", -1);
4223
4796
  __publicField(this, "onFrame", null);
4224
- this.page = options.clean ? withRoughness(page, 0) : page;
4797
+ this.page = options.clean ? cleanCopy(page) : page;
4225
4798
  addFonts(usedFonts(this.page));
4226
4799
  this.plan = plan(this.page);
4227
4800
  this.clicks = clickTargets(page, options.file);
@@ -4259,7 +4832,7 @@ var Playback = class {
4259
4832
  this.sceneNames = (page.timeline?.markers ?? []).map((m) => m.name);
4260
4833
  this.sceneLoops = (page.timeline?.markers ?? []).map((m) => m.loop === true);
4261
4834
  this.state = { ...page.variables ?? {} };
4262
- const theme = themeNamed(page.theme);
4835
+ const theme = pageTheme(page);
4263
4836
  this.background = theme?.background ?? WHITE;
4264
4837
  this.ink = theme?.ink ?? { r: 0.12, g: 0.12, b: 0.13, a: 1 };
4265
4838
  const ns = "http://www.w3.org/2000/svg";
@@ -4298,7 +4871,7 @@ var Playback = class {
4298
4871
  }
4299
4872
  /** Whether a copy is still playing through after a click. */
4300
4873
  replaying(now = performance.now()) {
4301
- for (const [id, start] of this.replayStarts) if (now - start < (this.replays.get(id) ?? 0) * 1e3) return true;
4874
+ for (const [id, start2] of this.replayStarts) if (now - start2 < (this.replays.get(id) ?? 0) * 1e3) return true;
4302
4875
  return false;
4303
4876
  }
4304
4877
  /** Draws the last moment drawn again, for copies replaying while the page stands still. */
@@ -4311,17 +4884,24 @@ var Playback = class {
4311
4884
  const r = this.plan.rects[Math.min(i, this.plan.rects.length - 1)];
4312
4885
  const now = typeof performance === "undefined" ? 0 : performance.now();
4313
4886
  const times = /* @__PURE__ */ new Map();
4314
- for (const [id, start] of this.replayStarts) times.set(id, Math.min((now - start) / 1e3, this.replays.get(id) ?? 0));
4887
+ for (const [id, start2] of this.replayStarts) times.set(id, Math.min((now - start2) / 1e3, this.replays.get(id) ?? 0));
4315
4888
  const tc = clampTime(t);
4316
4889
  const pose = posed(this.page, tc, this.state, this.sinceChange(now), this.carried);
4317
- const frame2 = expandingSymbols(pose, tc, true, times);
4890
+ const made = new Expansions(pose, tc, true);
4891
+ const frame2 = expandingSymbols(pose, tc, true, times, made);
4318
4892
  const { parts } = svgParts(frame2, this.background, r);
4319
4893
  const owned = (m, parts2) => {
4320
4894
  for (const p of parts2) if (this.owner.has(p.key)) m.set(p.key, p.markup);
4321
4895
  return m;
4322
4896
  };
4323
- 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) ?? "")));
4324
- 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)));
4897
+ const state = (st) => st.size === 0 ? /* @__PURE__ */ new Map() : owned(/* @__PURE__ */ new Map(), svgParts(
4898
+ expandingSymbols(pose, tc, true, new Map([...times, ...st]), made),
4899
+ this.background,
4900
+ r,
4901
+ false,
4902
+ (key) => st.has(this.owner.get(key) ?? "")
4903
+ ).parts);
4904
+ 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);
4325
4905
  const over = state(this.overTimes), down = state(this.downTimes), hit = state(this.hitTimes);
4326
4906
  const normal = owned(/* @__PURE__ */ new Map(), parts);
4327
4907
  const merged = [];
@@ -4450,6 +5030,14 @@ ${h}</g>
4450
5030
  return { frame: next, stop: entered >= 0 && !this.sceneLoops[entered] };
4451
5031
  }
4452
5032
  };
5033
+ var cleanCopies = /* @__PURE__ */ new WeakMap();
5034
+ function cleanCopy(page) {
5035
+ const hit = cleanCopies.get(page);
5036
+ if (hit && unchanged(page, hit.fields)) return hit.page;
5037
+ const clean = withRoughness(page, 0);
5038
+ cleanCopies.set(page, { fields: { ...page }, page: clean });
5039
+ return clean;
5040
+ }
4453
5041
  var ElementBase = typeof HTMLElement === "undefined" ? class {
4454
5042
  } : HTMLElement;
4455
5043
  var DottiePlayer = class extends ElementBase {
@@ -4472,6 +5060,10 @@ var DottiePlayer = class extends ElementBase {
4472
5060
  __publicField(this, "raf", 0);
4473
5061
  __publicField(this, "replayRaf", 0);
4474
5062
  __publicField(this, "loadToken", 0);
5063
+ /** Counts mounts, so a plan that comes back from a worker after a newer one is dropped. */
5064
+ __publicField(this, "mountToken", 0);
5065
+ /** The time a still shows; null while the drawing is animated. */
5066
+ __publicField(this, "stillTime", null);
4475
5067
  __publicField(this, "visible", true);
4476
5068
  __publicField(this, "observer", null);
4477
5069
  this.root = this.attachShadow({ mode: "open" });
@@ -4548,9 +5140,19 @@ var DottiePlayer = class extends ElementBase {
4548
5140
  text = inline2.textContent ?? "";
4549
5141
  }
4550
5142
  if (token !== this.loadToken) return;
4551
- this.setFile(parseDottie(text));
5143
+ this.showFile(parseDottie(text));
4552
5144
  } catch (err) {
4553
5145
  if (token !== this.loadToken) return;
5146
+ this.setPlaying(false);
5147
+ cancelAnimationFrame(this.raf);
5148
+ cancelAnimationFrame(this.replayRaf);
5149
+ this.raf = 0;
5150
+ this.replayRaf = 0;
5151
+ this.playback = null;
5152
+ this.file = null;
5153
+ this.mountToken++;
5154
+ this.stillTime = null;
5155
+ this.bar.hidden = true;
4554
5156
  this.stage.innerHTML = "";
4555
5157
  const box = document.createElement("div");
4556
5158
  box.className = "error";
@@ -4561,15 +5163,35 @@ var DottiePlayer = class extends ElementBase {
4561
5163
  }
4562
5164
  /** Shows an already-parsed file. */
4563
5165
  setFile(file) {
5166
+ this.loadToken++;
5167
+ this.showFile(file);
5168
+ }
5169
+ showFile(file) {
4564
5170
  this.file = file;
4565
5171
  for (const w of file.warnings) console.warn(`dottie-player: ${w}`);
4566
- this.mount();
4567
- this.dispatchEvent(new CustomEvent("load", { detail: { file } }));
5172
+ this.mount(() => this.dispatchEvent(new CustomEvent("load", { detail: { file } })));
4568
5173
  }
4569
- mount() {
5174
+ /** Shows the file's page; then `mounted`. A big animation is planned in a worker first (planner.ts), and what was
5175
+ * showing stays until it is ready; a newer mount, file or failed load in the meantime wins. */
5176
+ mount(mounted) {
4570
5177
  const file = this.file;
4571
5178
  const page = file.pages[pageIndex(file, this.getAttribute("page"))];
4572
- const p = new Playback(page, this.stage, { clean: this.hasAttribute("clean"), file });
5179
+ const clean = this.hasAttribute("clean");
5180
+ const token = ++this.mountToken;
5181
+ const shown = clean ? cleanCopy(page) : page;
5182
+ if (!plansInWorker(shown)) {
5183
+ this.mountPage(file, page, clean);
5184
+ mounted?.();
5185
+ return;
5186
+ }
5187
+ void planInWorker(shown).then(() => {
5188
+ if (token !== this.mountToken || this.file !== file) return;
5189
+ this.mountPage(file, page, clean);
5190
+ mounted?.();
5191
+ });
5192
+ }
5193
+ mountPage(file, page, clean) {
5194
+ const p = new Playback(page, this.stage, { clean, file });
4573
5195
  p.still = this.hasAttribute("at");
4574
5196
  this.playback = p;
4575
5197
  this.style.setProperty("--dottie-page", color(p.background));
@@ -4592,10 +5214,14 @@ var DottiePlayer = class extends ElementBase {
4592
5214
  this.dispatchEvent(new CustomEvent("frame", { detail: { frame: i, time: p.plan.times[i] } }));
4593
5215
  };
4594
5216
  if (!animated) {
4595
- p.drawAt(at !== null ? Number(at) || 0 : posterTime(page), at !== null ? nearest(p, Number(at) || 0) : p.posterFrame);
5217
+ const i = at !== null ? nearest(p, Number(at) || 0) : p.posterFrame;
5218
+ this.stillTime = at !== null ? Number(at) || 0 : posterTime(page);
5219
+ p.drawAt(this.stillTime, i);
5220
+ p.frame = i;
4596
5221
  this.setPlaying(false);
4597
5222
  return;
4598
5223
  }
5224
+ this.stillTime = null;
4599
5225
  p.show(0);
4600
5226
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4601
5227
  if (reduced) {
@@ -4609,8 +5235,10 @@ var DottiePlayer = class extends ElementBase {
4609
5235
  get paused() {
4610
5236
  return !this.playing;
4611
5237
  }
5238
+ /** The moment shown: a still's own time (its `at`, or the poster), else the current frame's. */
4612
5239
  get currentTime() {
4613
5240
  const p = this.playback;
5241
+ if (p && this.stillTime !== null) return this.stillTime;
4614
5242
  return p && p.frame >= 0 ? p.plan.times[p.frame] : 0;
4615
5243
  }
4616
5244
  set currentTime(t) {
@@ -4858,7 +5486,7 @@ function inspectFrame(page, t) {
4858
5486
  const [a, b] = endpoints(e, index);
4859
5487
  d.start = pt(a);
4860
5488
  d.end = pt(b);
4861
- d.route = routePoints(route(e, index)).map(pt);
5489
+ d.route = routePoints(route2(e, index)).map(pt);
4862
5490
  }
4863
5491
  return d;
4864
5492
  });
@@ -4897,7 +5525,7 @@ function render(source, options = {}) {
4897
5525
  function isFile(v) {
4898
5526
  return typeof v === "object" && v !== null && Array.isArray(v.pages) && Array.isArray(v.warnings);
4899
5527
  }
4900
- var VERSION = "0.8.0";
5528
+ var VERSION = "0.10.0";
4901
5529
  export {
4902
5530
  BOIL_DRAWINGS,
4903
5531
  BUNDLED_FONTS,
@@ -4933,6 +5561,8 @@ export {
4933
5561
  luminance,
4934
5562
  orderedElements,
4935
5563
  pageIndex,
5564
+ pageTheme,
5565
+ paletteColor,
4936
5566
  parseDottie,
4937
5567
  plan,
4938
5568
  posterTime,