@weasel-js/svg 1.9.4 → 1.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/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { identity, multiply, invert } from '@weasel-js/geom';
2
- import { boundsOfPath, transformPath, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, poseFrameOf, poseLinearOf, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize } from '@weasel-js/core';
1
+ import { identity, multiply, invert, fillsOpenSubpaths, splitByClosure, splitSubpaths, rotateAboutPoint, hasOpenSubpath } from '@weasel-js/geom';
2
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, isPatternPaint, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
5
5
  // src/types.ts
@@ -210,13 +210,13 @@ function isNonUniform(m) {
210
210
  return Math.abs(a * c + b * d) > 1e-4 * n * n || Math.abs(Math.hypot(a, b) - Math.hypot(c, d)) > 1e-4 * n;
211
211
  }
212
212
  function fillThroughMatrix(fill, m) {
213
- if (!("units" in fill) || fill.units !== "world") return fill;
213
+ if (!("units" in fill) || fill.units !== "world" && fill.units !== "local") return fill;
214
214
  const at = (p) => applyMatrix(m, p.x, p.y);
215
- if (fill.fill === "linear-gradient") return { ...fill, from: at(fill.from), to: at(fill.to) };
216
- if (fill.fill === "radial-gradient") {
215
+ if (fill.kind === "linear-gradient") return { ...fill, from: at(fill.from), to: at(fill.to) };
216
+ if (fill.kind === "radial-gradient") {
217
217
  return { ...fill, center: at(fill.center), radius: fill.radius * uniformScaleOf(m) };
218
218
  }
219
- if (fill.fill === "conic-gradient") return { ...fill, center: at(fill.center) };
219
+ if (fill.kind === "conic-gradient") return { ...fill, center: at(fill.center) };
220
220
  return fill;
221
221
  }
222
222
  function paintThroughMatrix(paint, m) {
@@ -635,6 +635,106 @@ function evaluateMediaQuery(list, env) {
635
635
  }
636
636
  });
637
637
  }
638
+
639
+ // src/container.ts
640
+ function queryContainerOf(declared, media) {
641
+ const [shortNames, shortType] = (declared("container") ?? "").split("/").map((part) => part.trim());
642
+ const type = (declared("container-type") ?? shortType ?? "").trim().toLowerCase();
643
+ if (type !== "size" && type !== "inline-size") return null;
644
+ const names = (declared("container-name") ?? shortNames ?? "").trim().split(/\s+/).filter((n) => n !== "" && n.toLowerCase() !== "none");
645
+ return { names, type, width: media.width, height: media.height };
646
+ }
647
+ var NAME = /^(-?[a-zA-Z_\u0080-￿][\w\u0080-￿-]*)(?![\w(-])\s*/;
648
+ var BLOCK_AXIS = /\b(?:height|block-size|aspect-ratio|orientation)\b/;
649
+ var NOT_A_SIZE_FEATURE = /\b(?:device-|resolution|color|monochrome|prefers-|update|hover|pointer)/;
650
+ function containerQueryHolds(prelude, container) {
651
+ if (!container) return false;
652
+ let condition2 = prelude.trim();
653
+ const named = NAME.exec(condition2);
654
+ if (named && named[1].toLowerCase() !== "not") {
655
+ if (!container.names.includes(named[1])) return false;
656
+ condition2 = condition2.slice(named[0].length);
657
+ }
658
+ condition2 = condition2.toLowerCase();
659
+ if (NOT_A_SIZE_FEATURE.test(condition2)) return false;
660
+ if (container.type === "inline-size" && BLOCK_AXIS.test(condition2)) return false;
661
+ const physical = condition2.replace(/\binline-size\b/g, "width").replace(/\bblock-size\b/g, "height");
662
+ return evaluateMediaQuery(physical, {
663
+ ...DEFAULT_MEDIA_ENVIRONMENT,
664
+ width: container.width,
665
+ height: container.height
666
+ });
667
+ }
668
+
669
+ // src/specificity.ts
670
+ function identEnd(s, i) {
671
+ while (i < s.length) {
672
+ const ch = s[i];
673
+ if (ch === "\\") i += 2;
674
+ else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
675
+ else break;
676
+ }
677
+ return i;
678
+ }
679
+ var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
680
+ var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
681
+ function compareSpecificity(a, b) {
682
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
683
+ }
684
+ function maxSpecificity(list) {
685
+ let best = [0, 0, 0];
686
+ for (const sel of splitTopLevel(list, ",")) {
687
+ const s = specificity(sel);
688
+ if (compareSpecificity(s, best) > 0) best = s;
689
+ }
690
+ return best;
691
+ }
692
+ function specificity(selector) {
693
+ let a = 0, b = 0, c = 0;
694
+ const s = selector;
695
+ let i = 0;
696
+ while (i < s.length) {
697
+ const ch = s[i];
698
+ if (ch === "#") {
699
+ a++;
700
+ i = identEnd(s, i + 1);
701
+ } else if (ch === ".") {
702
+ b++;
703
+ i = identEnd(s, i + 1);
704
+ } else if (ch === "[") {
705
+ b++;
706
+ while (i < s.length && s[i] !== "]") {
707
+ i = s[i] === '"' || s[i] === "'" ? skipString(s, i) : i + 1;
708
+ }
709
+ i++;
710
+ } else if (ch === ":") {
711
+ const element = s[i + 1] === ":";
712
+ const nameStart = i + (element ? 2 : 1);
713
+ const nameEnd = identEnd(s, nameStart);
714
+ const name = s.slice(nameStart, nameEnd).toLowerCase();
715
+ let arg = null;
716
+ i = nameEnd;
717
+ if (s[i] === "(") {
718
+ const end = parenEnd(s, i);
719
+ arg = s.slice(i + 1, end - 1);
720
+ i = end;
721
+ }
722
+ if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++;
723
+ else if (name === "where") ; else if (MAX_ARG_PSEUDOS.has(name) && arg != null) {
724
+ const m = maxSpecificity(arg);
725
+ a += m[0];
726
+ b += m[1];
727
+ c += m[2];
728
+ } else b++;
729
+ } else if (/[a-zA-Z_\\-]/.test(ch) || ch.charCodeAt(0) > 127) {
730
+ c++;
731
+ i = identEnd(s, i);
732
+ } else {
733
+ i++;
734
+ }
735
+ }
736
+ return [a, b, c];
737
+ }
638
738
  function parsePaintAttr(raw) {
639
739
  if (raw == null) return null;
640
740
  const s = raw.trim();
@@ -888,74 +988,6 @@ function evaluateSupports(condition2, opts = {}) {
888
988
 
889
989
  // src/cascade.ts
890
990
  var EMPTY_STYLE = {};
891
- function identEnd(s, i) {
892
- while (i < s.length) {
893
- const ch = s[i];
894
- if (ch === "\\") i += 2;
895
- else if (/[\w-]/.test(ch) || ch.charCodeAt(0) > 127) i++;
896
- else break;
897
- }
898
- return i;
899
- }
900
- var LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set(["before", "after", "first-line", "first-letter"]);
901
- var MAX_ARG_PSEUDOS = /* @__PURE__ */ new Set(["not", "is", "matches", "-webkit-any", "has"]);
902
- function compareSpecificity(a, b) {
903
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
904
- }
905
- function maxSpecificity(list) {
906
- let best = [0, 0, 0];
907
- for (const sel of splitTopLevel(list, ",")) {
908
- const s = specificity(sel);
909
- if (compareSpecificity(s, best) > 0) best = s;
910
- }
911
- return best;
912
- }
913
- function specificity(selector) {
914
- let a = 0, b = 0, c = 0;
915
- const s = selector;
916
- let i = 0;
917
- while (i < s.length) {
918
- const ch = s[i];
919
- if (ch === "#") {
920
- a++;
921
- i = identEnd(s, i + 1);
922
- } else if (ch === ".") {
923
- b++;
924
- i = identEnd(s, i + 1);
925
- } else if (ch === "[") {
926
- b++;
927
- while (i < s.length && s[i] !== "]") {
928
- i = s[i] === '"' || s[i] === "'" ? skipString(s, i) : i + 1;
929
- }
930
- i++;
931
- } else if (ch === ":") {
932
- const element = s[i + 1] === ":";
933
- const nameStart = i + (element ? 2 : 1);
934
- const nameEnd = identEnd(s, nameStart);
935
- const name = s.slice(nameStart, nameEnd).toLowerCase();
936
- let arg = null;
937
- i = nameEnd;
938
- if (s[i] === "(") {
939
- const end = parenEnd(s, i);
940
- arg = s.slice(i + 1, end - 1);
941
- i = end;
942
- }
943
- if (element || LEGACY_PSEUDO_ELEMENTS.has(name)) c++;
944
- else if (name === "where") ; else if (MAX_ARG_PSEUDOS.has(name) && arg != null) {
945
- const m = maxSpecificity(arg);
946
- a += m[0];
947
- b += m[1];
948
- c += m[2];
949
- } else b++;
950
- } else if (/[a-zA-Z_\\-]/.test(ch) || ch.charCodeAt(0) > 127) {
951
- c++;
952
- i = identEnd(s, i);
953
- } else {
954
- i++;
955
- }
956
- }
957
- return [a, b, c];
958
- }
959
991
  var CascadeLayers = class {
960
992
  children = /* @__PURE__ */ new Map();
961
993
  anonymous = 0;
@@ -1044,6 +1076,11 @@ function parseRuleList(src, from, to, layer, ctx, rules, state) {
1044
1076
  if (names && names.length <= 1) {
1045
1077
  const inner = names.length ? state.layers.declare(layer, names[0]) : state.layers.declareAnonymous(layer);
1046
1078
  parseRuleList(src, i + 1, end - 1, inner, ctx, rules, state);
1079
+ } else if (name === "container") {
1080
+ const outer = state.containers;
1081
+ state.containers = [...outer ?? [], at[2]];
1082
+ parseRuleList(src, i + 1, end - 1, layer, ctx, rules, state);
1083
+ state.containers = outer;
1047
1084
  } else if (conditionHolds(name, at[2], ctx)) {
1048
1085
  parseRuleList(src, i + 1, end - 1, layer, ctx, rules, state);
1049
1086
  }
@@ -1052,7 +1089,15 @@ function parseRuleList(src, from, to, layer, ctx, rules, state) {
1052
1089
  for (const raw of splitTopLevel(prelude, ",")) {
1053
1090
  const selector = raw.trim();
1054
1091
  if (selector) {
1055
- rules.push({ selector, declarations, specificity: specificity(selector), order: state.order++, layer });
1092
+ const { containers } = state;
1093
+ rules.push({
1094
+ selector,
1095
+ declarations,
1096
+ specificity: specificity(selector),
1097
+ order: state.order++,
1098
+ layer,
1099
+ ...containers ? { containers } : {}
1100
+ });
1056
1101
  }
1057
1102
  }
1058
1103
  }
@@ -1072,15 +1117,23 @@ function appliesAsCss(style, ctx) {
1072
1117
  }
1073
1118
  var sheetCache = /* @__PURE__ */ new WeakMap();
1074
1119
  function bindStylesheets(doc, media, onWarn) {
1075
- sheetCache.set(doc, collectRules(doc, media, onWarn));
1120
+ sheetCache.set(doc, { rules: collectRules(doc, media, onWarn), media });
1076
1121
  }
1077
- function documentRules(doc) {
1122
+ function documentSheet(doc) {
1078
1123
  const cached = sheetCache.get(doc);
1079
1124
  if (cached) return cached;
1080
1125
  const root = doc.documentElement;
1081
- const rules = collectRules(doc, root ? mediaEnvironmentFor(root) : DEFAULT_MEDIA_ENVIRONMENT);
1082
- sheetCache.set(doc, rules);
1083
- return rules;
1126
+ const media = root ? mediaEnvironmentFor(root) : DEFAULT_MEDIA_ENVIRONMENT;
1127
+ const sheet = { rules: collectRules(doc, media), media };
1128
+ sheetCache.set(doc, sheet);
1129
+ return sheet;
1130
+ }
1131
+ function containersHold(el, containers, media) {
1132
+ const root = el.ownerDocument?.documentElement;
1133
+ if (!root || el === root) return false;
1134
+ const declared = authorDeclarations(root);
1135
+ const container = queryContainerOf((prop) => declared.get(prop), media);
1136
+ return containers.every((prelude) => containerQueryHolds(prelude, container));
1084
1137
  }
1085
1138
  function collectRules(doc, media, onWarn) {
1086
1139
  const ctx = { media, onWarn, selector: (s) => isValidSelector(doc, s) };
@@ -1118,8 +1171,8 @@ var elementCache = /* @__PURE__ */ new WeakMap();
1118
1171
  function authorDeclarations(el) {
1119
1172
  const cached = elementCache.get(el);
1120
1173
  if (cached) return cached;
1121
- const rules = el.ownerDocument ? documentRules(el.ownerDocument) : [];
1122
- const matched = rules.filter((r) => matches(el, r.selector));
1174
+ const sheet = el.ownerDocument && documentSheet(el.ownerDocument);
1175
+ const matched = (sheet ? sheet.rules : []).filter((r) => matches(el, r.selector) && (!r.containers || containersHold(el, r.containers, sheet.media)));
1123
1176
  const inline = parseDeclarations(el.getAttribute("style") ?? "");
1124
1177
  const out = /* @__PURE__ */ new Map();
1125
1178
  const importantOrder = [...matched].sort((x, y) => y.rank - x.rank || compareCascade(x, y));
@@ -1242,15 +1295,12 @@ function collectElementsByTag(root, tags) {
1242
1295
  }
1243
1296
 
1244
1297
  // src/patterns.ts
1245
- function patternSpecOf(paint) {
1246
- return "tile" in paint.pattern ? paint.pattern : null;
1247
- }
1248
1298
  function patternXml(id, paint, onWarn) {
1249
- const spec = patternSpecOf(paint);
1250
- if (!spec) {
1251
- onWarn?.("pattern fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
1299
+ if (!isTilePaint(paint)) {
1300
+ onWarn?.("texture fill carries a TextureHandle, which has no vector form \u2014 omitted from <defs>");
1252
1301
  return "";
1253
1302
  }
1303
+ const spec = tileSpecOf(paint);
1254
1304
  const geometry = tileGeometry(spec);
1255
1305
  const body = geometry.shapes.map(shapeXml).join("");
1256
1306
  const origin = paint.origin ?? { x: 0, y: 0 };
@@ -1318,7 +1368,7 @@ function readPattern(el, onWarn) {
1318
1368
  onWarn?.(`<pattern id="${el.getAttribute("id")}"> has malformed data-weasel-tile \u2014 dropped`);
1319
1369
  return null;
1320
1370
  }
1321
- if (typeof spec?.tile !== "string" || typeof spec?.color !== "string") {
1371
+ if (!isTileKind(spec?.kind) || typeof spec?.color !== "string") {
1322
1372
  onWarn?.(`<pattern id="${el.getAttribute("id")}"> data-weasel-tile is not a tile spec \u2014 dropped`);
1323
1373
  return null;
1324
1374
  }
@@ -1326,7 +1376,7 @@ function readPattern(el, onWarn) {
1326
1376
  const y = parseFloat(el.getAttribute("y") ?? "0");
1327
1377
  const m = parseTransform(el.getAttribute("patternTransform"), onWarn);
1328
1378
  const origin = applyMatrix(m, x, y);
1329
- const paint = { fill: "pattern", pattern: spec };
1379
+ const paint = { ...spec };
1330
1380
  if (origin.x !== 0 || origin.y !== 0) paint.origin = origin;
1331
1381
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1332
1382
  return paint;
@@ -1369,10 +1419,10 @@ function serializePathD(path) {
1369
1419
  var WEASEL_NS = "urn:weasel-js:svg";
1370
1420
  var WEASEL_NS_PREFIX = "wzl";
1371
1421
  function nativeSvgKind(kind) {
1372
- return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "pattern";
1422
+ return kind === "solid" || kind === "linear-gradient" || kind === "radial-gradient" || kind === "texture" || isTileKind(kind);
1373
1423
  }
1374
1424
  function hasNativeSvgForm(paint) {
1375
- return nativeSvgKind(paint.fill ?? "solid");
1425
+ return nativeSvgKind(paint.kind ?? "solid");
1376
1426
  }
1377
1427
  var INTERPOLATE_SPACES = /* @__PURE__ */ new Set(["rgb", "oklab", "oklch"]);
1378
1428
  function nativeSvgSpace(space) {
@@ -1520,7 +1570,7 @@ function readLinearGradient(el, elements, onWarn) {
1520
1570
  const units = readGradientUnits(el, elements);
1521
1571
  const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1522
1572
  return {
1523
- fill: "linear-gradient",
1573
+ kind: "linear-gradient",
1524
1574
  from: { x: at("x1", "x", 0), y: at("y1", "y", 0) },
1525
1575
  to: { x: at("x2", "x", 1), y: at("y2", "y", 0) },
1526
1576
  stops: inheritedStops(el, elements, onWarn),
@@ -1534,7 +1584,7 @@ function readRadialGradient(el, elements, onWarn) {
1534
1584
  const units = readGradientUnits(el, elements);
1535
1585
  const at = (name, axis, fraction) => coord(el, elements, name, axis, fraction, units, onWarn);
1536
1586
  return {
1537
- fill: "radial-gradient",
1587
+ kind: "radial-gradient",
1538
1588
  center: { x: at("cx", "x", 0.5), y: at("cy", "y", 0.5) },
1539
1589
  radius: at("r", "other", 0.5),
1540
1590
  stops: inheritedStops(el, elements, onWarn),
@@ -1545,7 +1595,7 @@ function readRadialGradient(el, elements, onWarn) {
1545
1595
  function readConicGradient(el, elements, onWarn) {
1546
1596
  warnGradientTransform(el, elements, onWarn);
1547
1597
  return {
1548
- fill: "conic-gradient",
1598
+ kind: "conic-gradient",
1549
1599
  center: {
1550
1600
  x: num(inheritedAttr(el, elements, "cx"), 0.5),
1551
1601
  y: num(inheritedAttr(el, elements, "cy"), 0.5)
@@ -1576,7 +1626,7 @@ function readMeshGradient(el, onWarn) {
1576
1626
  if (patches.length === 0) return null;
1577
1627
  const space = el.getAttribute("interpolate") ?? el.getAttributeNS(WEASEL_NS, "interpolate");
1578
1628
  return {
1579
- fill: "mesh-gradient",
1629
+ kind: "mesh-gradient",
1580
1630
  patches,
1581
1631
  units: el.getAttribute("gradientUnits") === "objectBoundingBox" ? "bounds" : "world",
1582
1632
  ...space === "oklab" || space === "oklch" ? { interpolate: space } : {}
@@ -1600,7 +1650,7 @@ var PaintServerRegistry = class {
1600
1650
  register(paint) {
1601
1651
  const existing = this.byPaint.get(paint);
1602
1652
  if (existing) return existing;
1603
- const id = paint.fill === "pattern" ? `pat${this.counter++}` : `grad${this.counter++}`;
1653
+ const id = isPatternPaint(paint) ? `pat${this.counter++}` : `grad${this.counter++}`;
1604
1654
  this.byPaint.set(paint, id);
1605
1655
  this.order.push(paint);
1606
1656
  return id;
@@ -1619,7 +1669,7 @@ var PaintServerRegistry = class {
1619
1669
  }
1620
1670
  /** The paint kind of each registered paint, once, in first-use order. */
1621
1671
  paintKinds() {
1622
- return [...new Set(this.order.map((paint) => paint.fill ?? "solid"))];
1672
+ return [...new Set(this.order.map((paint) => paint.kind ?? "solid"))];
1623
1673
  }
1624
1674
  /** Whether any registered paint serializes outside SVG's own vocabulary, so
1625
1675
  * the root has to declare {@link WEASEL_NS}. */
@@ -1682,7 +1732,7 @@ function writesInset(entry) {
1682
1732
  function defaultMarkerXml(id, entry, onWarn, key, size) {
1683
1733
  const name = key ?? id;
1684
1734
  const unit = size === void 0 ? 1 : resolveMarkerSize({ key: name, size }, 1);
1685
- const path = entry.path({ size: unit, stroke: { paint: { fill: "solid", color: "#000" } } });
1735
+ const path = entry.path({ size: unit, stroke: { paint: { kind: "solid", color: "#000" } } });
1686
1736
  const d = serializePathD(path);
1687
1737
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1688
1738
  const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
@@ -1694,43 +1744,43 @@ function defaultMarkerXml(id, entry, onWarn, key, size) {
1694
1744
  function markerPaintAttrs(attr, paint, id, onWarn) {
1695
1745
  if (paint === "none") return `${attr}="none"`;
1696
1746
  if (paint === void 0 || paint === "line") return `${attr}="context-stroke"`;
1697
- if ("color" in paint && (paint.fill === void 0 || paint.fill === "solid")) {
1747
+ if ("color" in paint && (paint.kind === void 0 || paint.kind === "solid")) {
1698
1748
  return paint.opacity != null && paint.opacity !== 1 ? `${attr}="${paint.color}" ${attr}-opacity="${trimNumber(paint.opacity)}"` : `${attr}="${paint.color}"`;
1699
1749
  }
1700
1750
  onWarn?.(`marker "${id}" ${attr} is a paint server, which a <marker> def cannot reference here; written as the line's paint`);
1701
1751
  return `${attr}="context-stroke"`;
1702
1752
  }
1703
1753
  function paintServerXml(id, paint, onWarn) {
1704
- if (paint.fill === "pattern") return patternXml(id, paint, onWarn);
1754
+ if (isPatternPaint(paint)) return patternXml(id, paint, onWarn);
1705
1755
  const builtin = gradientXml(id, paint);
1706
1756
  if (builtin) return builtin;
1707
- const kind = getPaintKind(paint.fill);
1757
+ const kind = getPaintKind(paint.kind);
1708
1758
  const custom = kind?.toSvg?.(id, paint);
1709
1759
  if (custom) return custom;
1710
- onWarn?.(kind ? `${paint.fill} fill has no vector form \u2014 omitted from <defs>` : `${paint.fill} fill is not registered \u2014 omitted from <defs>; a kind loaded on demand needs \`await warmSvg(nodes)\` before a synchronous export`);
1760
+ onWarn?.(kind ? `${paint.kind} fill has no vector form \u2014 omitted from <defs>` : `${paint.kind} fill is not registered \u2014 omitted from <defs>; a kind loaded on demand needs \`await warmSvg(nodes)\` before a synchronous export`);
1711
1761
  return "";
1712
1762
  }
1713
1763
  function gradientUnitsAttr(units) {
1714
1764
  return units === "bounds" ? "objectBoundingBox" : "userSpaceOnUse";
1715
1765
  }
1716
1766
  function gradientXml(id, paint) {
1717
- if (paint.fill === "linear-gradient") {
1767
+ if (paint.kind === "linear-gradient") {
1718
1768
  const stops = paint.stops.map(stopXml).join("");
1719
1769
  return `<linearGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" x1="${trimNumber(paint.from.x)}" y1="${trimNumber(paint.from.y)}" x2="${trimNumber(paint.to.x)}" y2="${trimNumber(paint.to.y)}"${interpolateAttr(paint.interpolate)}>${stops}</linearGradient>`;
1720
1770
  }
1721
- if (paint.fill === "conic-gradient") {
1771
+ if (paint.kind === "conic-gradient") {
1722
1772
  const stops = paint.stops.map(stopXml).join("");
1723
1773
  const P = WEASEL_NS_PREFIX;
1724
1774
  return `<${P}:conicGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" angle="${trimNumber(paint.angle)}"${interpolateAttr(paint.interpolate)}>${stops}</${P}:conicGradient>`;
1725
1775
  }
1726
- if (paint.fill === "radial-gradient") {
1776
+ if (paint.kind === "radial-gradient") {
1727
1777
  const stops = paint.stops.map(stopXml).join("");
1728
1778
  return `<radialGradient id="${id}" gradientUnits="${gradientUnitsAttr(paint.units)}" cx="${trimNumber(paint.center.x)}" cy="${trimNumber(paint.center.y)}" r="${trimNumber(paint.radius)}"${interpolateAttr(paint.interpolate)}>${stops}</radialGradient>`;
1729
1779
  }
1730
1780
  return "";
1731
1781
  }
1732
1782
  function paintFallback(paint) {
1733
- const color = getPaintKind(paint.fill)?.colorOf(paint);
1783
+ const color = getPaintKind(paint.kind)?.colorOf(paint);
1734
1784
  if (!color) return "none";
1735
1785
  const packed = /^#([0-9a-f]{6})[0-9a-f]{2}$/i.exec(color);
1736
1786
  return packed ? `#${packed[1].toLowerCase()}` : color;
@@ -1816,8 +1866,6 @@ function polygonToPath(points, m = IDENTITY_MATRIX) {
1816
1866
  b.close();
1817
1867
  return transformPath(b.build(), m);
1818
1868
  }
1819
-
1820
- // src/lowerLeaf.ts
1821
1869
  var SUPPORTED_LEAF_TAGS = /* @__PURE__ */ new Set([
1822
1870
  "rect",
1823
1871
  "circle",
@@ -1849,7 +1897,7 @@ function lowerLeaf(el, tag, m, onWarn) {
1849
1897
  if (tag === "circle") return circleToPath(x("cx"), y("cy"), r("r"), m);
1850
1898
  if (tag === "ellipse") return ellipseToPath(x("cx"), y("cy"), x("rx"), y("ry"), m);
1851
1899
  if (tag === "line") return lineToPath(x("x1"), y("y1"), x("x2"), y("y2"), m);
1852
- if (tag === "polyline") return polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1900
+ if (tag === "polyline") return svgFilled(polylineToPath(parsePoints(el.getAttribute("points") ?? ""), m));
1853
1901
  if (tag === "polygon") return polygonToPath(parsePoints(el.getAttribute("points") ?? ""), m);
1854
1902
  if (tag === "path") {
1855
1903
  const d = el.getAttribute("d") ?? "";
@@ -1863,10 +1911,13 @@ function lowerLeaf(el, tag, m, onWarn) {
1863
1911
  const rect = polygonToRectIfAxisAligned(transformed);
1864
1912
  if (rect) return rect;
1865
1913
  }
1866
- return transformed;
1914
+ return svgFilled(transformed);
1867
1915
  }
1868
1916
  return null;
1869
1917
  }
1918
+ function svgFilled(path) {
1919
+ return hasOpenSubpath(path) ? { ...path, openFill: "close" } : path;
1920
+ }
1870
1921
  function polygonToRectIfAxisAligned(p) {
1871
1922
  const cmds = p.commands;
1872
1923
  if (cmds.length !== 5) return null;
@@ -2031,7 +2082,7 @@ function coreStroke(stroke) {
2031
2082
  if (stroke.paint.kind === "gradient") {
2032
2083
  paint = stroke.paint.paint;
2033
2084
  } else if (stroke.paint.kind === "solid") {
2034
- paint = stroke.paint.opacity != null ? { fill: "solid", color: stroke.paint.color, opacity: stroke.paint.opacity } : { fill: "solid", color: stroke.paint.color };
2085
+ paint = stroke.paint.opacity != null ? { kind: "solid", color: stroke.paint.color, opacity: stroke.paint.opacity } : { kind: "solid", color: stroke.paint.color };
2035
2086
  } else {
2036
2087
  return void 0;
2037
2088
  }
@@ -2187,19 +2238,48 @@ function anchorOffset(style, width2, text) {
2187
2238
  }
2188
2239
 
2189
2240
  // src/tspanLines.ts
2241
+ var DEFAULT_LINE_HEIGHT = 1.2;
2190
2242
  var LineTracker = class {
2191
- y;
2192
- constructor(text) {
2243
+ constructor(text, onWarn) {
2244
+ this.onWarn = onWarn;
2193
2245
  this.y = text.getAttribute("y");
2246
+ this.at = readLength(text, "y", "y", 0, onWarn);
2194
2247
  }
2195
- /** Whether `tspan` opens a new line. Call once per tspan, in order. */
2196
- startsLine(tspan) {
2248
+ onWarn;
2249
+ y;
2250
+ /** Where the current line sits, in user units. */
2251
+ at;
2252
+ pitches = [];
2253
+ /** Whether `tspan` opens a new line. Call once per tspan, in order.
2254
+ * `afterText` is whether any text precedes it: the first line's own
2255
+ * placement is not a pitch. */
2256
+ startsLine(tspan, afterText) {
2197
2257
  const y = tspan.getAttribute("y");
2198
2258
  const movedY = y != null && y !== this.y;
2199
2259
  if (y != null) this.y = y;
2260
+ const from = this.at;
2261
+ const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2262
+ this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2200
2263
  if (!tspan.hasAttribute("x")) return false;
2201
- const dy = parseFloat(tspan.getAttribute("dy") ?? "");
2202
- return movedY || Number.isFinite(dy) && dy !== 0;
2264
+ const starts = movedY || dy !== 0;
2265
+ if (starts && afterText && this.at > from) this.pitches.push(this.at - from);
2266
+ return starts;
2267
+ }
2268
+ /**
2269
+ * The line height the lines were pitched at, as a multiple of `fontSize`:
2270
+ * `undefined` for a single line, or for lines at the default pitch. The
2271
+ * model holds one line height, so uneven pitches give their mean, which
2272
+ * keeps the block's height.
2273
+ */
2274
+ lineHeight(fontSize) {
2275
+ const { pitches } = this;
2276
+ if (pitches.length === 0 || !(fontSize > 0)) return void 0;
2277
+ const mean = pitches.reduce((sum, p) => sum + p, 0) / pitches.length;
2278
+ if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
2279
+ this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
2280
+ }
2281
+ const lineHeight = mean / fontSize;
2282
+ return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2203
2283
  }
2204
2284
  };
2205
2285
  function warnUnreadPositions(el, onWarn) {
@@ -2229,6 +2309,13 @@ function joinLines(lines) {
2229
2309
  return out;
2230
2310
  }
2231
2311
 
2312
+ // src/fillPaint.ts
2313
+ function mapFillPaint(fill, f) {
2314
+ if (!fill.paint) return fill;
2315
+ const paint = f(fill.paint);
2316
+ return paint === fill.paint ? fill : { ...fill, paint };
2317
+ }
2318
+
2232
2319
  // src/readText.ts
2233
2320
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2234
2321
  function preservesSpace(el) {
@@ -2298,12 +2385,12 @@ function placeTextNode(node, m, onWarn) {
2298
2385
  if (node.runs) node.runs = scaleRuns(node.runs, scale);
2299
2386
  if (node.stroke) node.stroke = scaleStroke(node.stroke, scale);
2300
2387
  }
2301
- if (node.fill) node.fill = fillThroughMatrix(node.fill, inBox);
2388
+ if (node.fill) node.fill = mapFillPaint(node.fill, (p) => fillThroughMatrix(p, inBox));
2302
2389
  if (node.stroke?.paint) {
2303
2390
  node.stroke = { ...node.stroke, paint: fillThroughMatrix(node.stroke.paint, inBox) };
2304
2391
  }
2305
2392
  if (node.runs) {
2306
- node.runs = node.runs.map((r) => r.fill ? { ...r, fill: fillThroughMatrix(r.fill, inBox) } : r);
2393
+ node.runs = node.runs.map((r) => r.fill ? { ...r, fill: mapFillPaint(r.fill, (p) => fillThroughMatrix(p, inBox)) } : r);
2307
2394
  }
2308
2395
  }
2309
2396
  function parseTextElement(el, ctm, style, gradients, onWarn) {
@@ -2320,13 +2407,12 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2320
2407
  const textStyle = readTextStyle(leafStyle, onWarn);
2321
2408
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2322
2409
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2323
- const lineHeight = textStyle.lineHeight ?? 1.2;
2324
2410
  const dominantBaseline = el.getAttribute("dominant-baseline");
2325
2411
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2326
2412
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2327
2413
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2328
2414
  const runLines = [[]];
2329
- const tracker = new LineTracker(el);
2415
+ const tracker = new LineTracker(el, onWarn);
2330
2416
  const push = (run) => {
2331
2417
  runLines[runLines.length - 1].push(run);
2332
2418
  };
@@ -2345,13 +2431,17 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2345
2431
  continue;
2346
2432
  }
2347
2433
  warnUnreadPositions(sp, onWarn);
2348
- if (tracker.startsLine(sp)) runLines.push([]);
2434
+ const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2435
+ if (tracker.startsLine(sp, afterText)) runLines.push([]);
2349
2436
  push(readTspanRun(sp, gradients, leafStyle, onWarn));
2350
2437
  }
2351
2438
  }
2352
2439
  if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2353
2440
  const runs = joinLines(runLines);
2354
2441
  const plain = runs.map((r) => r.text).join("");
2442
+ const pitched = tracker.lineHeight(fontSize);
2443
+ if (pitched !== void 0) textStyle.lineHeight = pitched;
2444
+ const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2355
2445
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
2356
2446
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
2357
2447
  const width2 = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
@@ -2419,11 +2509,11 @@ function readTspanRun(el, gradients, style, onWarn) {
2419
2509
  if (parsed?.kind === "none") {
2420
2510
  run.fill = null;
2421
2511
  } else if (parsed?.kind === "solid") {
2422
- run.fill = { fill: "solid", color: parsed.color };
2512
+ run.fill = { paint: { kind: "solid", color: parsed.color } };
2423
2513
  } else if (parsed?.kind === "ref") {
2424
2514
  const paint = gradients.get(parsed.id);
2425
- if (paint) run.fill = paint;
2426
- else if (parsed.fallback) run.fill = { fill: "solid", color: parsed.fallback.color };
2515
+ if (paint) run.fill = { paint };
2516
+ else if (parsed.fallback) run.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2427
2517
  }
2428
2518
  }
2429
2519
  const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
@@ -2465,12 +2555,12 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
2465
2555
  if (fillRaw) {
2466
2556
  const parsed = readProperty("fill", fillRaw);
2467
2557
  if (parsed?.kind === "solid") {
2468
- out.fill = { fill: "solid", color: parsed.color };
2558
+ out.fill = { paint: { kind: "solid", color: parsed.color } };
2469
2559
  } else if (parsed?.kind === "ref") {
2470
2560
  const paint = gradients.get(parsed.id);
2471
- if (paint) out.fill = paint;
2561
+ if (paint) out.fill = { paint };
2472
2562
  else if (parsed.fallback) {
2473
- out.fill = { fill: "solid", color: parsed.fallback.color };
2563
+ out.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2474
2564
  }
2475
2565
  } else if (parsed?.kind === "none") {
2476
2566
  out.fill = null;
@@ -2726,7 +2816,7 @@ function parseSvg(svg, opts = {}) {
2726
2816
  for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
2727
2817
  const rootStyle = deriveStyle(EMPTY_STYLE, root);
2728
2818
  const clips = collectClipPaths(root, onWarn);
2729
- const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
2819
+ const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix, opts.keepHidden === true);
2730
2820
  const markers = ingestMarkers(root, nodes, gradients, onWarn);
2731
2821
  const result = { nodes, warnings };
2732
2822
  if (markers.length > 0) result.markers = markers;
@@ -2827,26 +2917,25 @@ function collectElementMeta(el, uriToPrefix) {
2827
2917
  }
2828
2918
  return Object.keys(meta).length > 0 ? meta : void 0;
2829
2919
  }
2830
- function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2920
+ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2831
2921
  const out = [];
2832
2922
  for (let i = 0; i < parent.children.length; i++) {
2833
2923
  const el = parent.children[i];
2834
2924
  const ns = el.namespaceURI;
2835
2925
  if (ns && ns !== "http://www.w3.org/2000/svg") continue;
2836
- const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix);
2837
- if (node) {
2838
- if (Array.isArray(node)) out.push(...node);
2839
- else out.push(node);
2840
- }
2926
+ const hidden = isHiddenElement(el, style);
2927
+ if (hidden && !keepHidden) continue;
2928
+ const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
2929
+ if (!node) continue;
2930
+ const parsed = Array.isArray(node) ? node : [node];
2931
+ if (hidden) for (const n of parsed) n.hidden = true;
2932
+ out.push(...parsed);
2841
2933
  }
2842
2934
  return out;
2843
2935
  }
2844
- function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2936
+ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2845
2937
  const tag = el.tagName.toLowerCase();
2846
2938
  if (IGNORED_TAGS.has(tag)) return null;
2847
- if (ownValue(el, "display") === "none") return null;
2848
- const containers = tag === "g" || SUPPORTED_GROUP_TAGS.has(tag);
2849
- if (!containers && isHidden(deriveStyle(style, el))) return null;
2850
2939
  if (tag === "g" && el.hasAttribute("data-weasel-image")) {
2851
2940
  const image = parseCroppedImage(el, ctm, onWarn);
2852
2941
  if (image) {
@@ -2863,7 +2952,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2863
2952
  const local = parseTransform(el.getAttribute("transform"), onWarn);
2864
2953
  const childCtm = multiply(ctm, local);
2865
2954
  const childStyle = deriveStyle(style, el);
2866
- const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
2955
+ const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2867
2956
  const opacity2 = ownValue(el, "opacity");
2868
2957
  const group = { kind: "group", children };
2869
2958
  const clip = clipFor(el, childCtm, clips, onWarn);
@@ -2881,7 +2970,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2881
2970
  if (el.hasAttribute("viewBox")) {
2882
2971
  onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
2883
2972
  }
2884
- return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
2973
+ return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2885
2974
  }
2886
2975
  if (tag === "text") {
2887
2976
  const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
@@ -2949,56 +3038,16 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2949
3038
  if (meta) node.meta = meta;
2950
3039
  return node;
2951
3040
  }
2952
- function isHidden(style) {
2953
- const v = styleValue(style, "visibility");
3041
+ function isHiddenElement(el, style) {
3042
+ const tag = el.tagName.toLowerCase();
3043
+ if (IGNORED_TAGS.has(tag)) return false;
3044
+ if (ownValue(el, "display") === "none") return true;
3045
+ if (SUPPORTED_GROUP_TAGS.has(tag)) return false;
3046
+ const v = styleValue(deriveStyle(style, el), "visibility");
2954
3047
  return v === "hidden" || v === "collapse";
2955
3048
  }
2956
- function serializeSvg(nodes, opts = {}) {
2957
- const registry = new PaintServerRegistry();
2958
- registerPaintServers(nodes, registry);
2959
- const bounds = opts.viewBox ?? computeBounds(nodes);
2960
- const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
2961
- const namespaces = opts.namespaces ?? {};
2962
- const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
2963
- if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
2964
- rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
2965
- }
2966
- for (const [prefix, uri] of Object.entries(namespaces)) {
2967
- rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
2968
- }
2969
- rootAttrs.push(`viewBox="${vb}"`);
2970
- if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
2971
- if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
2972
- if (opts.documentMeta) {
2973
- for (const prefix of Object.keys(namespaces)) {
2974
- const bucket = opts.documentMeta[prefix];
2975
- if (!bucket?.attrs) continue;
2976
- for (const [name, value] of Object.entries(bucket.attrs)) {
2977
- rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
2978
- }
2979
- }
2980
- }
2981
- let docMetaXml = "";
2982
- if (opts.documentMeta) {
2983
- for (const prefix of Object.keys(namespaces)) {
2984
- const bucket = opts.documentMeta[prefix];
2985
- if (!bucket?.elements) continue;
2986
- for (const [localName2, list] of Object.entries(bucket.elements)) {
2987
- for (const el of list) docMetaXml += namespacedElementXml(prefix, localName2, el);
2988
- }
2989
- }
2990
- }
2991
- const defsXml = registry.toDefsXml(opts.onWarn);
2992
- const said = /* @__PURE__ */ new Set();
2993
- const warn = (m) => {
2994
- if (!opts.onWarn || said.has(m)) return;
2995
- said.add(m);
2996
- opts.onWarn(m);
2997
- };
2998
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
2999
- const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3000
- return `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3001
- }
3049
+
3050
+ // src/metaXml.ts
3002
3051
  function namespacedElementXml(prefix, localName2, el) {
3003
3052
  const attrs = [];
3004
3053
  for (const [name, value] of Object.entries(el.attrs)) {
@@ -3039,6 +3088,158 @@ function metaElementsXml(meta, namespaces) {
3039
3088
  }
3040
3089
  return out;
3041
3090
  }
3091
+ function computeBounds(nodes) {
3092
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3093
+ const visit = (n, ctm) => {
3094
+ if (n.hidden) return;
3095
+ if (n.kind === "group") {
3096
+ const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
3097
+ n.children.forEach((c) => visit(c, childCtm));
3098
+ return;
3099
+ }
3100
+ const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
3101
+ const frame = n.kind === "path" ? null : poseFrameOf(n);
3102
+ const local = frame ? multiply(ctm, frame) : n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
3103
+ for (const [x, y] of corners(b)) {
3104
+ const px = local[0] * x + local[2] * y + local[4];
3105
+ const py = local[1] * x + local[3] * y + local[5];
3106
+ if (px < minX) minX = px;
3107
+ if (py < minY) minY = py;
3108
+ if (px > maxX) maxX = px;
3109
+ if (py > maxY) maxY = py;
3110
+ }
3111
+ };
3112
+ nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
3113
+ if (!Number.isFinite(minX)) {
3114
+ return { x: 0, y: 0, width: 0, height: 0 };
3115
+ }
3116
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
3117
+ }
3118
+ function corners(b) {
3119
+ return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
3120
+ }
3121
+ function rotateAboutCenter(ctm, b, angle) {
3122
+ const cx = (b.minX + b.maxX) / 2;
3123
+ const cy = (b.minY + b.maxY) / 2;
3124
+ const c = Math.cos(angle);
3125
+ const s = Math.sin(angle);
3126
+ return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
3127
+ }
3128
+ function pathBounds(path) {
3129
+ const b = boundsOfPath(path);
3130
+ return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3131
+ }
3132
+
3133
+ // src/openSubpathFill.ts
3134
+ var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
3135
+ function withoutEndMarkers(stroke) {
3136
+ const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
3137
+ return rest;
3138
+ }
3139
+ function withOpenSubpathsUnfilled(node) {
3140
+ const path = node.path;
3141
+ if (path.kind !== "polygon" || node.fill.kind === "none" || fillsOpenSubpaths(path)) return node;
3142
+ const { closed, open } = splitByClosure(path);
3143
+ if (!open) return node;
3144
+ const { fill, stroke } = node;
3145
+ const opens = capsEnds(stroke) ? splitSubpaths(open) : [open];
3146
+ if (!closed && opens.length === 1) return { ...node, fill: { kind: "none" } };
3147
+ const piece = (part, paint, line = stroke) => ({ kind: "path", path: part, fill: paint, ...line ? { stroke: line } : {} });
3148
+ const group = {
3149
+ kind: "group",
3150
+ children: [
3151
+ ...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
3152
+ ...opens.map((part) => piece(part, { kind: "none" }))
3153
+ ]
3154
+ };
3155
+ if (node.rotation != null && node.rotation !== 0) {
3156
+ const b = pathBounds(path);
3157
+ group.transform = rotateAboutPoint((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2, node.rotation);
3158
+ }
3159
+ if (node.opacity != null) group.opacity = node.opacity;
3160
+ if (node.hidden) group.hidden = true;
3161
+ if (node.meta) group.meta = node.meta;
3162
+ return group;
3163
+ }
3164
+
3165
+ // src/prettyXml.ts
3166
+ var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
3167
+ var isTag = (t) => t !== void 0 && t.startsWith("<");
3168
+ var isClose = (t) => t.startsWith("</");
3169
+ var isSelfClosing = (t) => t.endsWith("/>");
3170
+ var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
3171
+ function matchingClose(tokens, open) {
3172
+ let depth = 0;
3173
+ for (let i = open; i < tokens.length; i++) {
3174
+ const t = tokens[i];
3175
+ if (!isTag(t) || isSelfClosing(t)) continue;
3176
+ depth += isClose(t) ? -1 : 1;
3177
+ if (depth === 0) return i;
3178
+ }
3179
+ return tokens.length - 1;
3180
+ }
3181
+ function prettyXml(xml) {
3182
+ const tokens = xml.match(TOKEN2) ?? [];
3183
+ const lines = [];
3184
+ let depth = 0;
3185
+ for (let i = 0; i < tokens.length; i++) {
3186
+ const t = tokens[i];
3187
+ if (!isTag(t) || isSelfClosing(t)) {
3188
+ lines.push(" ".repeat(depth) + t);
3189
+ } else if (isClose(t)) {
3190
+ depth--;
3191
+ lines.push(" ".repeat(depth) + t);
3192
+ } else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
3193
+ const close2 = matchingClose(tokens, i);
3194
+ lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
3195
+ i = close2;
3196
+ } else {
3197
+ lines.push(" ".repeat(depth) + t);
3198
+ depth++;
3199
+ }
3200
+ }
3201
+ return lines.join("\n");
3202
+ }
3203
+
3204
+ // src/serialize.ts
3205
+ function serializeSvg(nodes, opts = {}) {
3206
+ const registry = new PaintServerRegistry();
3207
+ registerPaintServers(nodes, registry);
3208
+ const bounds = opts.viewBox ?? computeBounds(nodes);
3209
+ const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3210
+ const namespaces = opts.namespaces ?? {};
3211
+ const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3212
+ if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3213
+ rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3214
+ }
3215
+ for (const [prefix, uri] of Object.entries(namespaces)) {
3216
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
3217
+ }
3218
+ rootAttrs.push(`viewBox="${vb}"`);
3219
+ if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
3220
+ if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
3221
+ if (opts.documentMeta) {
3222
+ for (const prefix of Object.keys(namespaces)) {
3223
+ const bucket = opts.documentMeta[prefix];
3224
+ if (!bucket?.attrs) continue;
3225
+ for (const [name, value] of Object.entries(bucket.attrs)) {
3226
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
3227
+ }
3228
+ }
3229
+ }
3230
+ const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
3231
+ const defsXml = registry.toDefsXml(opts.onWarn);
3232
+ const said = /* @__PURE__ */ new Set();
3233
+ const warn = (m) => {
3234
+ if (!opts.onWarn || said.has(m)) return;
3235
+ said.add(m);
3236
+ opts.onWarn(m);
3237
+ };
3238
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3239
+ const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3240
+ const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3241
+ return opts.pretty ? prettyXml(xml) : xml;
3242
+ }
3042
3243
  function registerPaintServers(nodes, registry) {
3043
3244
  for (const n of nodes) {
3044
3245
  if (n.kind === "group") {
@@ -3049,18 +3250,18 @@ function registerPaintServers(nodes, registry) {
3049
3250
  if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
3050
3251
  registerMarkers(n.stroke, registry);
3051
3252
  } else if (n.kind === "text") {
3052
- registerTextPaint(n.fill, registry);
3253
+ registerTextPaint(n.fill?.paint, registry);
3053
3254
  registerTextPaint(n.stroke?.paint, registry);
3054
3255
  registerMarkers(n.stroke, registry);
3055
3256
  for (const run of n.runs ?? []) {
3056
- registerTextPaint(run.fill, registry);
3257
+ registerTextPaint(run.fill?.paint, registry);
3057
3258
  registerTextPaint(run.stroke?.paint, registry);
3058
3259
  }
3059
3260
  }
3060
3261
  }
3061
3262
  }
3062
3263
  function registerTextPaint(paint, registry) {
3063
- if (paint && !("color" in paint)) registry.register(paint);
3264
+ if (paint && !isSolidPaint(paint)) registry.register(paint);
3064
3265
  }
3065
3266
  function registerMarkers(stroke, registry) {
3066
3267
  if (!stroke) return;
@@ -3069,10 +3270,15 @@ function registerMarkers(stroke, registry) {
3069
3270
  }
3070
3271
  }
3071
3272
  function nodeXml(node, registry, namespaces, warn) {
3273
+ const xml = kindXml(node, registry, namespaces, warn);
3274
+ return node.hidden ? xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`) : xml;
3275
+ }
3276
+ function kindXml(node, registry, namespaces, warn) {
3072
3277
  if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
3073
3278
  if (node.kind === "text") return textXml(node, registry, namespaces, warn);
3074
3279
  if (node.kind === "image") return imageXml(node, namespaces);
3075
- return pathXml(node, registry, namespaces, warn);
3280
+ const drawn = withOpenSubpathsUnfilled(node);
3281
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3076
3282
  }
3077
3283
  function warnStrokeAlign(align, warn) {
3078
3284
  if (align && align !== "center") {
@@ -3171,7 +3377,7 @@ function coreStrokeAttrs(stroke, registry, warn) {
3171
3377
  const width2 = stroke.width ?? 1;
3172
3378
  if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
3173
3379
  warnStrokeAlign(stroke.align, warn);
3174
- const attrs = [`stroke="${"color" in paint ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3380
+ const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3175
3381
  attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
3176
3382
  attrs.push(...strokeWidthAttrs(width2));
3177
3383
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -3223,42 +3429,6 @@ function strokeAttrsFor(stroke, registry, warn) {
3223
3429
  }
3224
3430
  return attrs;
3225
3431
  }
3226
- function computeBounds(nodes) {
3227
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3228
- const visit = (n, ctm) => {
3229
- if (n.kind === "group") {
3230
- const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
3231
- n.children.forEach((c) => visit(c, childCtm));
3232
- return;
3233
- }
3234
- const b = n.kind === "text" || n.kind === "image" ? { minX: n.x, minY: n.y, maxX: n.x + n.width, maxY: n.y + n.height } : pathBounds(n.path);
3235
- const frame = n.kind === "path" ? null : poseFrameOf(n);
3236
- const local = frame ? multiply(ctm, frame) : n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
3237
- for (const [x, y] of corners(b)) {
3238
- const px = local[0] * x + local[2] * y + local[4];
3239
- const py = local[1] * x + local[3] * y + local[5];
3240
- if (px < minX) minX = px;
3241
- if (py < minY) minY = py;
3242
- if (px > maxX) maxX = px;
3243
- if (py > maxY) maxY = py;
3244
- }
3245
- };
3246
- nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
3247
- if (!Number.isFinite(minX)) {
3248
- return { x: 0, y: 0, width: 0, height: 0 };
3249
- }
3250
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
3251
- }
3252
- function corners(b) {
3253
- return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
3254
- }
3255
- function rotateAboutCenter(ctm, b, angle) {
3256
- const cx = (b.minX + b.maxX) / 2;
3257
- const cy = (b.minY + b.maxY) / 2;
3258
- const c = Math.cos(angle);
3259
- const s = Math.sin(angle);
3260
- return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
3261
- }
3262
3432
  function textXml(node, registry, namespaces, warn) {
3263
3433
  const attrs = [
3264
3434
  `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
@@ -3302,11 +3472,13 @@ function textXml(node, registry, namespaces, warn) {
3302
3472
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
3303
3473
  warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
3304
3474
  }
3305
- if (node.fill === null) {
3306
- attrs.push('fill="none"');
3307
- } else if (node.fill) {
3308
- attrs.push(`fill="${"color" in node.fill ? escapeAttr(node.fill.color) : registry.ref(node.fill)}"`);
3309
- attrs.push(...opacityAttr("fill-opacity", node.fill.opacity));
3475
+ if (node.fill !== void 0) {
3476
+ const paint = node.fill?.paint;
3477
+ if (!paint) attrs.push('fill="none"');
3478
+ else {
3479
+ attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
3480
+ attrs.push(...opacityAttr("fill-opacity", paint.opacity));
3481
+ }
3310
3482
  }
3311
3483
  for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
3312
3484
  if (node.opacity != null && node.opacity !== 1) {
@@ -3346,13 +3518,11 @@ function runXml(run, style, registry, warn) {
3346
3518
  }
3347
3519
  const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3348
3520
  if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
3349
- if (run.fill === null) attrs.push('fill="none"');
3350
- else if (run.fill) {
3351
- if ("color" in run.fill) {
3352
- attrs.push(`fill="${escapeAttr(run.fill.color)}"`);
3353
- } else {
3354
- attrs.push(`fill="${registry.ref(run.fill)}"`);
3355
- }
3521
+ if (run.fill !== void 0) {
3522
+ const paint = run.fill?.paint;
3523
+ if (!paint) attrs.push('fill="none"');
3524
+ else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
3525
+ else attrs.push(`fill="${registry.ref(paint)}"`);
3356
3526
  }
3357
3527
  for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3358
3528
  if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
@@ -3366,10 +3536,6 @@ function textDecorationValue(underline, strikethrough, overline) {
3366
3536
  if (overline) tokens.push("overline");
3367
3537
  return tokens.length > 0 ? tokens.join(" ") : null;
3368
3538
  }
3369
- function pathBounds(path) {
3370
- const b = boundsOfPath(path);
3371
- return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3372
- }
3373
3539
  function svgNeeds(nodes) {
3374
3540
  const registry = new PaintServerRegistry();
3375
3541
  registerPaintServers(nodes, registry);
@@ -3395,6 +3561,22 @@ function warmSvg(nodes) {
3395
3561
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
3396
3562
  ]).then(() => void 0);
3397
3563
  }
3564
+
3565
+ // src/fadePaint.ts
3566
+ function fadeFill(fill, k) {
3567
+ return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
3568
+ }
3569
+ function fadeStroke(stroke, k) {
3570
+ return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
3571
+ }
3572
+ function fadeRun(run, k) {
3573
+ if (k === 1 || !run.fill && !run.stroke) return run;
3574
+ return {
3575
+ ...run,
3576
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fadeFill(p, k)) } : {},
3577
+ ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
3578
+ };
3579
+ }
3398
3580
  var CASCADE_OFFSET_PX = 24;
3399
3581
  var VIEWPORT_FIT = 0.9;
3400
3582
  function readFileText(file) {
@@ -3413,20 +3595,6 @@ function fillDataFromSvg(paint, box) {
3413
3595
  }
3414
3596
  return fillInBoxFrame(paint.paint, box);
3415
3597
  }
3416
- function fadeFill(fill, k) {
3417
- return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
3418
- }
3419
- function fadeStroke(stroke, k) {
3420
- return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
3421
- }
3422
- function fadeRun(run, k) {
3423
- if (k === 1 || !run.fill && !run.stroke) return run;
3424
- return {
3425
- ...run,
3426
- ...run.fill ? { fill: fadeFill(run.fill, k) } : {},
3427
- ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
3428
- };
3429
- }
3430
3598
  function strokeDataFromSvg(stroke, box) {
3431
3599
  if (!stroke || stroke.paint.kind === "none") return void 0;
3432
3600
  const paint = fillDataFromSvg(stroke.paint, box);
@@ -3448,7 +3616,7 @@ function fillInBoxFrame(fill, box) {
3448
3616
  return units === "world" ? fillToBoundsFrame(fill, box) : fill;
3449
3617
  }
3450
3618
  function runInBoxFrame(run, box) {
3451
- const fill = run.fill ? fillInBoxFrame(run.fill, box) : run.fill;
3619
+ const fill = run.fill ? mapFillPaint(run.fill, (p) => fillInBoxFrame(p, box)) : run.fill;
3452
3620
  const stroke = run.stroke ? strokeInBoxFrame(run.stroke, box) : run.stroke;
3453
3621
  if (fill === run.fill && stroke === run.stroke) return run;
3454
3622
  return { ...run, ...fill ? { fill } : {}, ...stroke ? { stroke } : {} };
@@ -3468,7 +3636,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3468
3636
  const id = nextId(source);
3469
3637
  const out = options.leaf ? options.leaf(id, { pose, data }, source) : data;
3470
3638
  if (out === null) return null;
3471
- drafts.push({ kind: "leaf", id, parentId, pose, data: out });
3639
+ drafts.push({ kind: "leaf", id, parentId, pose, data: out, ...source.hidden ? { hidden: true } : {} });
3472
3640
  return pose;
3473
3641
  };
3474
3642
  const visit = (n, parentId, inherited2) => {
@@ -3478,7 +3646,9 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3478
3646
  kind: "container",
3479
3647
  id: nextId(n),
3480
3648
  parentId,
3481
- pose: { x: 0, y: 0, width: 0, height: 0 }
3649
+ pose: { x: 0, y: 0, width: 0, height: 0 },
3650
+ data: n.clip ? { path: n.clip, fill: null } : {},
3651
+ ...n.hidden ? { hidden: true } : {}
3482
3652
  };
3483
3653
  drafts.push(draft);
3484
3654
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
@@ -3486,7 +3656,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3486
3656
  drafts.splice(drafts.indexOf(draft), 1);
3487
3657
  return null;
3488
3658
  }
3489
- draft.pose = acc;
3659
+ draft.pose = n.clip ? boxOfPath(n.clip) : acc;
3490
3660
  return draft.pose;
3491
3661
  }
3492
3662
  if (n.kind === "text") {
@@ -3512,7 +3682,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3512
3682
  ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
3513
3683
  // `!== undefined`, not a truthiness test: `null` is the document
3514
3684
  // saying `fill="none"`, and absent takes the painter's default.
3515
- ...fill2 !== void 0 ? { fill: fill2 === null ? null : fadeFill(fillInBoxFrame(fill2, box), k) } : {},
3685
+ ...fill2 !== void 0 ? { fill: fill2 && mapFillPaint(fill2, (p) => fadeFill(fillInBoxFrame(p, box), k)) } : {},
3516
3686
  ...n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
3517
3687
  });
3518
3688
  }
@@ -3530,20 +3700,24 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3530
3700
  }
3531
3701
  });
3532
3702
  }
3533
- const b = n.path.kind === "rect" ? { x: n.path.x, y: n.path.y, width: n.path.width, height: n.path.height } : boundsOfPath(n.path);
3703
+ const b = boxOfPath(n.path);
3534
3704
  const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
3535
3705
  if (n.rotation) pose.rotation = n.rotation;
3536
3706
  const fill = fillDataFromSvg(n.fill, b);
3537
3707
  const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
3538
3708
  return pushLeaf(n, parentId, pose, {
3539
3709
  path: n.path,
3540
- ...fill !== void 0 ? { fill: fill && fadeFill(fill, k) } : {},
3710
+ ...fill !== void 0 ? { fill: fill && { paint: fadeFill(fill, k) } } : {},
3541
3711
  ...strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
3542
3712
  });
3543
3713
  };
3544
3714
  for (const n of nodes) visit(n, null, 1);
3545
3715
  return drafts;
3546
3716
  }
3717
+ function boxOfPath(path) {
3718
+ const b = boundsOfPath(path);
3719
+ return { x: b.x, y: b.y, width: b.width, height: b.height };
3720
+ }
3547
3721
  function isNodeList(input) {
3548
3722
  return Array.isArray(input);
3549
3723
  }
@@ -3575,7 +3749,7 @@ async function unpackSvgFiles(files, ctx) {
3575
3749
  const wrapperId = ctx.scene.mintId();
3576
3750
  const union2 = unionAABB(roots.map((d) => d.pose));
3577
3751
  drafts = [
3578
- { kind: "container", id: wrapperId, parentId: null, pose: union2 },
3752
+ { kind: "container", id: wrapperId, parentId: null, pose: union2, data: {} },
3579
3753
  ...drafts.map((d) => d.parentId === null ? { ...d, parentId: wrapperId } : d)
3580
3754
  ];
3581
3755
  }
@@ -3607,7 +3781,7 @@ async function unpackSvgFiles(files, ctx) {
3607
3781
  kind: d.kind,
3608
3782
  layer,
3609
3783
  pose: place(d.pose),
3610
- data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : {},
3784
+ data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3611
3785
  parent: d.parentId
3612
3786
  },
3613
3787
  label: "Insert SVG"
@@ -3622,7 +3796,7 @@ async function unpackSvgFiles(files, ctx) {
3622
3796
  }
3623
3797
  function svgPaintFromKit(fill, box) {
3624
3798
  if (fill === null) return { kind: "none" };
3625
- if (fill.fill === void 0 || fill.fill === "solid") {
3799
+ if (fill.kind === void 0 || fill.kind === "solid") {
3626
3800
  return { kind: "solid", color: fill.color, ...fill.opacity != null ? { opacity: fill.opacity } : {} };
3627
3801
  }
3628
3802
  return { kind: "gradient", paint: fillInPoseFrame(fill, box) };
@@ -3670,7 +3844,7 @@ function runInPoseFrame(run, box) {
3670
3844
  if (!run.fill && !run.stroke) return run;
3671
3845
  return {
3672
3846
  ...run,
3673
- ...run.fill ? { fill: fillInPoseFrame(run.fill, box) } : {},
3847
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fillInPoseFrame(p, box)) } : {},
3674
3848
  ...run.stroke ? { stroke: strokeInPoseFrame(run.stroke, box) } : {}
3675
3849
  };
3676
3850
  }
@@ -3688,7 +3862,7 @@ function svgLeafFromKit(data, pose) {
3688
3862
  if (data.style) node2.style = data.style;
3689
3863
  if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
3690
3864
  if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
3691
- if (data.fill !== void 0) node2.fill = data.fill && fillInPoseFrame(data.fill, pose);
3865
+ if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
3692
3866
  if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3693
3867
  if (pose.rotation) node2.rotation = pose.rotation;
3694
3868
  if (pose.transform) node2.transform = pose.transform;
@@ -3700,10 +3874,15 @@ function svgLeafFromKit(data, pose) {
3700
3874
  const node = {
3701
3875
  kind: "path",
3702
3876
  path: pathInPoseFrame(data.path, pose),
3703
- fill: svgPaintFromKit(fill, pose)
3877
+ fill: svgPaintFromKit(fill?.paint ?? null, pose)
3704
3878
  };
3705
3879
  if (stroke) node.stroke = stroke;
3706
- if (pose.rotation) node.rotation = pose.rotation;
3880
+ const frame = poseLinearOf(pose) && poseFrameOf(pose);
3881
+ if (frame) {
3882
+ node.path = pathThroughPoseFrame(node.path, pose);
3883
+ node.fill = paintThroughMatrix(node.fill, frame);
3884
+ if (stroke) node.stroke = strokeThroughMatrix(stroke, frame);
3885
+ } else if (pose.rotation) node.rotation = pose.rotation;
3707
3886
  return node;
3708
3887
  }
3709
3888
  function svgNodesFromKit(tree, options = {}) {
@@ -3719,6 +3898,8 @@ function svgNodesFromKit(tree, options = {}) {
3719
3898
  if (child) children.push(child);
3720
3899
  }
3721
3900
  const g = { kind: "group", children };
3901
+ const clip = node.data?.path;
3902
+ if (clip) g.clip = pathThroughPoseFrame(pathInPoseFrame(clip, node.pose), node.pose);
3722
3903
  return group ? group(id, g) : g;
3723
3904
  }
3724
3905
  return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);