@weasel-js/svg 1.9.4 → 1.10.1

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, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
2
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, 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) {
@@ -1498,9 +1548,9 @@ function warnUnmodeled(el, elements, onWarn) {
1498
1548
  break;
1499
1549
  }
1500
1550
  }
1501
- const spread = inheritedAttr(el, elements, "spreadMethod");
1502
- if (spread === "reflect" || spread === "repeat") {
1503
- onWarn?.(`spreadMethod="${spread}" is not modeled; the gradient pads`);
1551
+ const spread2 = inheritedAttr(el, elements, "spreadMethod");
1552
+ if (spread2 === "reflect" || spread2 === "repeat") {
1553
+ onWarn?.(`spreadMethod="${spread2}" is not modeled; the gradient pads`);
1504
1554
  }
1505
1555
  }
1506
1556
  function coord(el, elements, name, axis, fraction, units, onWarn) {
@@ -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,74 @@ function anchorOffset(style, width2, text) {
2187
2238
  }
2188
2239
 
2189
2240
  // src/tspanLines.ts
2241
+ var LINES_ATTR = "data-weasel-lines";
2242
+ var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
2243
+ function linesXml(pieces, x, pitch) {
2244
+ let out = "";
2245
+ let pending = 0;
2246
+ let wrote = false;
2247
+ const tspan = (attrs, text) => `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
2248
+ const lineAttrs = () => {
2249
+ if (pending === 0) return [];
2250
+ const attrs = [`x="${trimNumber(x)}"`, `dy="${trimNumber(pending * pitch)}"`];
2251
+ if (pending > 1 || !wrote) attrs.push(`${LINES_ATTR}="${pending}"`);
2252
+ pending = 0;
2253
+ return attrs;
2254
+ };
2255
+ for (const piece of pieces) {
2256
+ piece.text.split(/[\n\u000B\u000C]/).forEach((line, i) => {
2257
+ if (i > 0) pending++;
2258
+ if (line === "") return;
2259
+ const attrs = [...piece.attrs ?? [], ...lineAttrs()];
2260
+ out += piece.attrs === null && attrs.length === 0 ? escapeText(line) : tspan(attrs, line);
2261
+ wrote = true;
2262
+ });
2263
+ }
2264
+ return pending > 0 ? out + tspan(lineAttrs(), "") : out;
2265
+ }
2266
+ var DEFAULT_LINE_HEIGHT = 1.2;
2190
2267
  var LineTracker = class {
2191
- y;
2192
- constructor(text) {
2268
+ constructor(text, onWarn) {
2269
+ this.onWarn = onWarn;
2193
2270
  this.y = text.getAttribute("y");
2271
+ this.at = readLength(text, "y", "y", 0, onWarn);
2194
2272
  }
2195
- /** Whether `tspan` opens a new line. Call once per tspan, in order. */
2196
- startsLine(tspan) {
2273
+ onWarn;
2274
+ y;
2275
+ /** Where the current line sits, in user units. */
2276
+ at;
2277
+ pitches = [];
2278
+ /** How many lines down `tspan` opens a new line, or 0 when it stays on the
2279
+ * current one. Call once per tspan, in order. `afterText` is whether any
2280
+ * text precedes it: the first line's own placement is not a pitch. */
2281
+ startsLine(tspan, afterText) {
2197
2282
  const y = tspan.getAttribute("y");
2198
2283
  const movedY = y != null && y !== this.y;
2199
2284
  if (y != null) this.y = y;
2200
- if (!tspan.hasAttribute("x")) return false;
2201
- const dy = parseFloat(tspan.getAttribute("dy") ?? "");
2202
- return movedY || Number.isFinite(dy) && dy !== 0;
2285
+ const from = this.at;
2286
+ const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2287
+ this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2288
+ if (!tspan.hasAttribute("x") || !(movedY || dy !== 0)) return 0;
2289
+ const stated = Number(tspan.getAttribute(LINES_ATTR));
2290
+ const lines = Number.isInteger(stated) && stated > 0 ? stated : 1;
2291
+ if (afterText && this.at > from) this.pitches.push((this.at - from) / lines);
2292
+ return lines;
2293
+ }
2294
+ /**
2295
+ * The line height the lines were pitched at, as a multiple of `fontSize`:
2296
+ * `undefined` for a single line, or for lines at the default pitch. The
2297
+ * model holds one line height, so uneven pitches give their mean, which
2298
+ * keeps the block's height.
2299
+ */
2300
+ lineHeight(fontSize) {
2301
+ const { pitches } = this;
2302
+ if (pitches.length === 0 || !(fontSize > 0)) return void 0;
2303
+ const mean = pitches.reduce((sum, p) => sum + p, 0) / pitches.length;
2304
+ if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
2305
+ this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
2306
+ }
2307
+ const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
2308
+ return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2203
2309
  }
2204
2310
  };
2205
2311
  function warnUnreadPositions(el, onWarn) {
@@ -2208,28 +2314,88 @@ function warnUnreadPositions(el, onWarn) {
2208
2314
  const raw = el.getAttribute(name)?.trim() ?? "";
2209
2315
  if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2210
2316
  }
2211
- for (const name of ["dx", "rotate"]) {
2317
+ for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
2212
2318
  const raw = el.getAttribute(name)?.trim() ?? "";
2213
2319
  if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2214
2320
  onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
2215
2321
  }
2216
2322
  }
2217
2323
  }
2218
- function joinLines(lines) {
2324
+ function sameStyle(a, b) {
2325
+ const style = (run) => JSON.stringify(
2326
+ Object.entries(run).filter(([k, v]) => k !== "text" && v !== void 0).sort(([j], [k]) => j < k ? -1 : 1)
2327
+ );
2328
+ return style(a) === style(b);
2329
+ }
2330
+ function joinLines(lines, leading = 0) {
2219
2331
  const out = [];
2220
- let breaks = 0;
2332
+ let breaks = leading;
2221
2333
  for (const line of lines) {
2222
2334
  if (line.length > 0) {
2223
- if (breaks > 0) line[0] = { ...line[0], text: "\n".repeat(breaks) + line[0].text };
2224
- out.push(...line);
2335
+ const [first, ...rest] = line;
2336
+ const head = breaks > 0 ? { ...first, text: "\n".repeat(breaks) + first.text } : first;
2337
+ const last = out[out.length - 1];
2338
+ if (breaks > 0 && last && sameStyle(last, head)) {
2339
+ out[out.length - 1] = { ...last, text: last.text + head.text };
2340
+ } else {
2341
+ out.push(head);
2342
+ }
2343
+ out.push(...rest);
2225
2344
  breaks = 0;
2226
2345
  }
2227
2346
  if (out.length > 0) breaks++;
2228
2347
  }
2229
2348
  return out;
2230
2349
  }
2350
+ var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
2351
+ function readDx(el, onWarn) {
2352
+ const raw = el.getAttribute("dx")?.trim();
2353
+ if (!raw) return null;
2354
+ const values = raw.split(/[\s,]+/).map((v) => resolveLength(v, el, "dx", "x", 0, onWarn));
2355
+ return values.some((v) => v !== 0) ? values : null;
2356
+ }
2357
+ var spacingOf = (run, inherited2) => resolveScreenLength(run.letterSpacing ?? inherited2 ?? 0, 1);
2358
+ function spread(run, extra, inherited2) {
2359
+ const base = spacingOf(run, inherited2);
2360
+ const out = [];
2361
+ let last;
2362
+ Array.from(run.text).forEach((glyph, i) => {
2363
+ const more = extra[i] ?? 0;
2364
+ if (out.length > 0 && more === last) out[out.length - 1].text += glyph;
2365
+ else out.push(more === 0 ? { ...run, text: glyph } : { ...run, text: glyph, letterSpacing: base + more });
2366
+ last = more;
2367
+ });
2368
+ return out;
2369
+ }
2370
+ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2371
+ const line = lines[lines.length - 1];
2372
+ const dx = readDx(tspan, onWarn);
2373
+ if (!dx) {
2374
+ line.push(run);
2375
+ return 0;
2376
+ }
2377
+ const raw = tspan.getAttribute("dx");
2378
+ if (COLLAPSIBLE.test(run.text)) {
2379
+ onWarn(`<tspan dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
2380
+ line.push(run);
2381
+ return 0;
2382
+ }
2383
+ let lead = 0;
2384
+ let before = line.length - 1;
2385
+ while (before >= 0 && line[before].text === "") before--;
2386
+ if (dx[0] !== 0 && before >= 0) {
2387
+ const glyphs = Array.from(line[before].text);
2388
+ line.splice(before, 1, ...spread(line[before], [...glyphs.slice(1).map(() => 0), dx[0]], inherited2));
2389
+ } else if (dx[0] !== 0 && lines.length === 1) {
2390
+ lead = dx[0];
2391
+ } else if (dx[0] !== 0) {
2392
+ onWarn(`<tspan dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
2393
+ }
2394
+ line.push(...spread(run, dx.slice(1), inherited2));
2395
+ return lead;
2396
+ }
2231
2397
 
2232
- // src/readText.ts
2398
+ // src/textWhitespace.ts
2233
2399
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2234
2400
  function preservesSpace(el) {
2235
2401
  for (let e = el; e; e = e.parentElement) {
@@ -2266,6 +2432,15 @@ function collapseRunWhitespace(runs) {
2266
2432
  if (runs[i].text === "") runs.splice(i, 1);
2267
2433
  }
2268
2434
  }
2435
+
2436
+ // src/fillPaint.ts
2437
+ function mapFillPaint(fill, f) {
2438
+ if (!fill.paint) return fill;
2439
+ const paint = f(fill.paint);
2440
+ return paint === fill.paint ? fill : { ...fill, paint };
2441
+ }
2442
+
2443
+ // src/readText.ts
2269
2444
  var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
2270
2445
  function laidOutWidth(runs, style, plain) {
2271
2446
  const resolved = resolveTextStyle(style);
@@ -2298,12 +2473,12 @@ function placeTextNode(node, m, onWarn) {
2298
2473
  if (node.runs) node.runs = scaleRuns(node.runs, scale);
2299
2474
  if (node.stroke) node.stroke = scaleStroke(node.stroke, scale);
2300
2475
  }
2301
- if (node.fill) node.fill = fillThroughMatrix(node.fill, inBox);
2476
+ if (node.fill) node.fill = mapFillPaint(node.fill, (p) => fillThroughMatrix(p, inBox));
2302
2477
  if (node.stroke?.paint) {
2303
2478
  node.stroke = { ...node.stroke, paint: fillThroughMatrix(node.stroke.paint, inBox) };
2304
2479
  }
2305
2480
  if (node.runs) {
2306
- node.runs = node.runs.map((r) => r.fill ? { ...r, fill: fillThroughMatrix(r.fill, inBox) } : r);
2481
+ node.runs = node.runs.map((r) => r.fill ? { ...r, fill: mapFillPaint(r.fill, (p) => fillThroughMatrix(p, inBox)) } : r);
2307
2482
  }
2308
2483
  }
2309
2484
  function parseTextElement(el, ctm, style, gradients, onWarn) {
@@ -2320,38 +2495,16 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2320
2495
  const textStyle = readTextStyle(leafStyle, onWarn);
2321
2496
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2322
2497
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2323
- const lineHeight = textStyle.lineHeight ?? 1.2;
2324
2498
  const dominantBaseline = el.getAttribute("dominant-baseline");
2325
2499
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2326
2500
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2327
2501
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2328
- const runLines = [[]];
2329
- const tracker = new LineTracker(el);
2330
- const push = (run) => {
2331
- runLines[runLines.length - 1].push(run);
2332
- };
2333
- for (let i = 0; i < el.childNodes.length; i++) {
2334
- const child = el.childNodes[i];
2335
- if (child.nodeType === 3) {
2336
- const t = child.textContent ?? "";
2337
- if (!t) continue;
2338
- push({ text: t, ...inheritedShift });
2339
- } else if (child.nodeType === 1) {
2340
- const sp = child;
2341
- if (sp.tagName.toLowerCase() !== "tspan") {
2342
- onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2343
- const t = sp.textContent ?? "";
2344
- if (t) push({ text: t, ...inheritedShift });
2345
- continue;
2346
- }
2347
- warnUnreadPositions(sp, onWarn);
2348
- if (tracker.startsLine(sp)) runLines.push([]);
2349
- push(readTspanRun(sp, gradients, leafStyle, onWarn));
2350
- }
2351
- }
2352
- if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2353
- const runs = joinLines(runLines);
2502
+ const tracker = new LineTracker(el, onWarn);
2503
+ const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
2354
2504
  const plain = runs.map((r) => r.text).join("");
2505
+ const pitched = tracker.lineHeight(fontSize);
2506
+ if (pitched !== void 0) textStyle.lineHeight = pitched;
2507
+ const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2355
2508
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
2356
2509
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
2357
2510
  const width2 = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
@@ -2360,7 +2513,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2360
2513
  const opacity = ownValue(el, "opacity");
2361
2514
  const node = {
2362
2515
  kind: "text",
2363
- x: ax - anchorOffset(textStyle, width2, plain),
2516
+ x: ax + lead - anchorOffset(textStyle, width2, plain),
2364
2517
  y: topY,
2365
2518
  width: width2,
2366
2519
  height: height2,
@@ -2386,6 +2539,38 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2386
2539
  placeTextNode(node, multiply(ctm, localTransform), onWarn);
2387
2540
  return node;
2388
2541
  }
2542
+ function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafStyle, onWarn) {
2543
+ const runLines = [[]];
2544
+ let leading = 0;
2545
+ let lead = 0;
2546
+ const push = (run) => {
2547
+ runLines[runLines.length - 1].push(run);
2548
+ };
2549
+ for (let i = 0; i < el.childNodes.length; i++) {
2550
+ const child = el.childNodes[i];
2551
+ if (child.nodeType === 3) {
2552
+ const t = child.textContent ?? "";
2553
+ if (!t) continue;
2554
+ push({ text: t, ...inheritedShift });
2555
+ } else if (child.nodeType === 1) {
2556
+ const sp = child;
2557
+ if (sp.tagName.toLowerCase() !== "tspan") {
2558
+ onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2559
+ const t = sp.textContent ?? "";
2560
+ if (t) push({ text: t, ...inheritedShift });
2561
+ continue;
2562
+ }
2563
+ warnUnreadPositions(sp, onWarn);
2564
+ const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2565
+ const lines = tracker.startsLine(sp, afterText);
2566
+ if (runLines.length === 1 && runLines[0].length === 0 && statesLines(sp)) leading = lines;
2567
+ else for (let n = 0; n < lines; n++) runLines.push([]);
2568
+ lead += placeTspanRun(sp, readTspanRun(sp, gradients, leafStyle, onWarn), runLines, letterSpacing, onWarn);
2569
+ }
2570
+ }
2571
+ if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2572
+ return { runs: joinLines(runLines, leading), lead };
2573
+ }
2389
2574
  function readTspanRun(el, gradients, style, onWarn) {
2390
2575
  const text = el.textContent ?? "";
2391
2576
  const run = { text };
@@ -2419,11 +2604,11 @@ function readTspanRun(el, gradients, style, onWarn) {
2419
2604
  if (parsed?.kind === "none") {
2420
2605
  run.fill = null;
2421
2606
  } else if (parsed?.kind === "solid") {
2422
- run.fill = { fill: "solid", color: parsed.color };
2607
+ run.fill = { paint: { kind: "solid", color: parsed.color } };
2423
2608
  } else if (parsed?.kind === "ref") {
2424
2609
  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 };
2610
+ if (paint) run.fill = { paint };
2611
+ else if (parsed.fallback) run.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2427
2612
  }
2428
2613
  }
2429
2614
  const stroke = coreStroke(readStroke(ownStrokeStyle(el, tspanStyle), gradients, onWarn, isNonScalingStroke(el)));
@@ -2465,12 +2650,12 @@ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
2465
2650
  if (fillRaw) {
2466
2651
  const parsed = readProperty("fill", fillRaw);
2467
2652
  if (parsed?.kind === "solid") {
2468
- out.fill = { fill: "solid", color: parsed.color };
2653
+ out.fill = { paint: { kind: "solid", color: parsed.color } };
2469
2654
  } else if (parsed?.kind === "ref") {
2470
2655
  const paint = gradients.get(parsed.id);
2471
- if (paint) out.fill = paint;
2656
+ if (paint) out.fill = { paint };
2472
2657
  else if (parsed.fallback) {
2473
- out.fill = { fill: "solid", color: parsed.fallback.color };
2658
+ out.fill = { paint: { kind: "solid", color: parsed.fallback.color } };
2474
2659
  }
2475
2660
  } else if (parsed?.kind === "none") {
2476
2661
  out.fill = null;
@@ -2726,7 +2911,7 @@ function parseSvg(svg, opts = {}) {
2726
2911
  for (const [id, paint] of collectPatterns(root, onWarn)) gradients.set(id, paint);
2727
2912
  const rootStyle = deriveStyle(EMPTY_STYLE, root);
2728
2913
  const clips = collectClipPaths(root, onWarn);
2729
- const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix);
2914
+ const nodes = parseChildren(root, IDENTITY_MATRIX, rootStyle, gradients, clips, onWarn, uriToPrefix, opts.keepHidden === true);
2730
2915
  const markers = ingestMarkers(root, nodes, gradients, onWarn);
2731
2916
  const result = { nodes, warnings };
2732
2917
  if (markers.length > 0) result.markers = markers;
@@ -2827,26 +3012,25 @@ function collectElementMeta(el, uriToPrefix) {
2827
3012
  }
2828
3013
  return Object.keys(meta).length > 0 ? meta : void 0;
2829
3014
  }
2830
- function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix) {
3015
+ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2831
3016
  const out = [];
2832
3017
  for (let i = 0; i < parent.children.length; i++) {
2833
3018
  const el = parent.children[i];
2834
3019
  const ns = el.namespaceURI;
2835
3020
  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
- }
3021
+ const hidden = isHiddenElement(el, style);
3022
+ if (hidden && !keepHidden) continue;
3023
+ const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
3024
+ if (!node) continue;
3025
+ const parsed = Array.isArray(node) ? node : [node];
3026
+ if (hidden) for (const n of parsed) n.hidden = true;
3027
+ out.push(...parsed);
2841
3028
  }
2842
3029
  return out;
2843
3030
  }
2844
- function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
3031
+ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden) {
2845
3032
  const tag = el.tagName.toLowerCase();
2846
3033
  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
3034
  if (tag === "g" && el.hasAttribute("data-weasel-image")) {
2851
3035
  const image = parseCroppedImage(el, ctm, onWarn);
2852
3036
  if (image) {
@@ -2863,7 +3047,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2863
3047
  const local = parseTransform(el.getAttribute("transform"), onWarn);
2864
3048
  const childCtm = multiply(ctm, local);
2865
3049
  const childStyle = deriveStyle(style, el);
2866
- const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix);
3050
+ const children = parseChildren(el, childCtm, childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2867
3051
  const opacity2 = ownValue(el, "opacity");
2868
3052
  const group = { kind: "group", children };
2869
3053
  const clip = clipFor(el, childCtm, clips, onWarn);
@@ -2881,7 +3065,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2881
3065
  if (el.hasAttribute("viewBox")) {
2882
3066
  onWarn("nested <svg> viewBox is not modeled; its children are not rescaled");
2883
3067
  }
2884
- return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix);
3068
+ return parseChildren(el, multiply(ctm, offset), childStyle, gradients, clips, onWarn, uriToPrefix, keepHidden);
2885
3069
  }
2886
3070
  if (tag === "text") {
2887
3071
  const textNode = parseTextElement(el, ctm, style, gradients, onWarn);
@@ -2949,69 +3133,29 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix) {
2949
3133
  if (meta) node.meta = meta;
2950
3134
  return node;
2951
3135
  }
2952
- function isHidden(style) {
2953
- const v = styleValue(style, "visibility");
3136
+ function isHiddenElement(el, style) {
3137
+ const tag = el.tagName.toLowerCase();
3138
+ if (IGNORED_TAGS.has(tag)) return false;
3139
+ if (ownValue(el, "display") === "none") return true;
3140
+ if (SUPPORTED_GROUP_TAGS.has(tag)) return false;
3141
+ const v = styleValue(deriveStyle(style, el), "visibility");
2954
3142
  return v === "hidden" || v === "collapse";
2955
3143
  }
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)}"`);
3144
+
3145
+ // src/metaXml.ts
3146
+ function namespacedElementXml(prefix, localName2, el) {
3147
+ const attrs = [];
3148
+ for (const [name, value] of Object.entries(el.attrs)) {
3149
+ attrs.push(`${name}="${escapeAttr(value)}"`);
2968
3150
  }
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
- }
3151
+ const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
3152
+ let body = "";
3153
+ if (el.children) {
3154
+ for (const [childName, list] of Object.entries(el.children)) {
3155
+ for (const child of list) body += namespacedElementXml(prefix, childName, child);
2979
3156
  }
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
- }
3002
- function namespacedElementXml(prefix, localName2, el) {
3003
- const attrs = [];
3004
- for (const [name, value] of Object.entries(el.attrs)) {
3005
- attrs.push(`${name}="${escapeAttr(value)}"`);
3006
- }
3007
- const head = attrs.length > 0 ? `<${prefix}:${localName2} ${attrs.join(" ")}>` : `<${prefix}:${localName2}>`;
3008
- let body = "";
3009
- if (el.children) {
3010
- for (const [childName, list] of Object.entries(el.children)) {
3011
- for (const child of list) body += namespacedElementXml(prefix, childName, child);
3012
- }
3013
- } else if (el.text != null) {
3014
- body = escapeText(el.text);
3157
+ } else if (el.text != null) {
3158
+ body = escapeText(el.text);
3015
3159
  }
3016
3160
  return `${head}${body}</${prefix}:${localName2}>`;
3017
3161
  }
@@ -3039,115 +3183,109 @@ function metaElementsXml(meta, namespaces) {
3039
3183
  }
3040
3184
  return out;
3041
3185
  }
3042
- function registerPaintServers(nodes, registry) {
3043
- for (const n of nodes) {
3186
+ function computeBounds(nodes) {
3187
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3188
+ const visit = (n, ctm) => {
3189
+ if (n.hidden) return;
3044
3190
  if (n.kind === "group") {
3045
- if (n.clip) registry.clipId(n.clip);
3046
- registerPaintServers(n.children, registry);
3047
- } else if (n.kind === "path") {
3048
- if (n.fill.kind === "gradient") registry.register(n.fill.paint);
3049
- if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
3050
- registerMarkers(n.stroke, registry);
3051
- } else if (n.kind === "text") {
3052
- registerTextPaint(n.fill, registry);
3053
- registerTextPaint(n.stroke?.paint, registry);
3054
- registerMarkers(n.stroke, registry);
3055
- for (const run of n.runs ?? []) {
3056
- registerTextPaint(run.fill, registry);
3057
- registerTextPaint(run.stroke?.paint, registry);
3058
- }
3191
+ const childCtm = n.transform ? multiply(ctm, n.transform) : ctm;
3192
+ n.children.forEach((c) => visit(c, childCtm));
3193
+ return;
3059
3194
  }
3195
+ 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);
3196
+ const frame = n.kind === "path" ? null : poseFrameOf(n);
3197
+ const local = frame ? multiply(ctm, frame) : n.rotation ? rotateAboutCenter(ctm, b, n.rotation) : ctm;
3198
+ for (const [x, y] of corners(b)) {
3199
+ const px = local[0] * x + local[2] * y + local[4];
3200
+ const py = local[1] * x + local[3] * y + local[5];
3201
+ if (px < minX) minX = px;
3202
+ if (py < minY) minY = py;
3203
+ if (px > maxX) maxX = px;
3204
+ if (py > maxY) maxY = py;
3205
+ }
3206
+ };
3207
+ nodes.forEach((n) => visit(n, IDENTITY_MATRIX));
3208
+ if (!Number.isFinite(minX)) {
3209
+ return { x: 0, y: 0, width: 0, height: 0 };
3060
3210
  }
3211
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
3061
3212
  }
3062
- function registerTextPaint(paint, registry) {
3063
- if (paint && !("color" in paint)) registry.register(paint);
3213
+ function corners(b) {
3214
+ return [[b.minX, b.minY], [b.maxX, b.minY], [b.maxX, b.maxY], [b.minX, b.maxY]];
3064
3215
  }
3065
- function registerMarkers(stroke, registry) {
3066
- if (!stroke) return;
3067
- for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
3068
- if (ref !== void 0) registry.markerId(ref);
3216
+ function rotateAboutCenter(ctm, b, angle) {
3217
+ const cx = (b.minX + b.maxX) / 2;
3218
+ const cy = (b.minY + b.maxY) / 2;
3219
+ const c = Math.cos(angle);
3220
+ const s = Math.sin(angle);
3221
+ return multiply(ctm, [c, s, -s, c, cx - cx * c + cy * s, cy - cx * s - cy * c]);
3222
+ }
3223
+ function pathBounds(path) {
3224
+ const b = boundsOfPath(path);
3225
+ return { minX: b.x, minY: b.y, maxX: b.x + b.width, maxY: b.y + b.height };
3226
+ }
3227
+
3228
+ // src/openSubpathFill.ts
3229
+ var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
3230
+ var marked = (stroke) => capsEnds(stroke) || stroke?.markerMid !== void 0;
3231
+ function withoutEndMarkers(stroke) {
3232
+ const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
3233
+ return rest;
3234
+ }
3235
+ var closedStroke = (stroke) => stroke && capsEnds(stroke) ? withoutEndMarkers(stroke) : stroke;
3236
+ var isClosed = (part) => splitByClosure(part).open === null;
3237
+ function samePath(a, b) {
3238
+ if (a === b) return true;
3239
+ return a.commands.length === b.commands.length && a.coords.length === b.coords.length && a.commands.every((c, i) => c === b.commands[i]) && a.coords.every((v, i) => v === b.coords[i]);
3240
+ }
3241
+ function strokeParts(path, stroke, filled, closed, open) {
3242
+ const none = { kind: "none" };
3243
+ const subs = marked(stroke) ? splitSubpaths(path) : [];
3244
+ if (subs.length > 1) {
3245
+ return subs.map((sub) => ({ path: sub, fill: none, stroke: isClosed(sub) ? closedStroke(stroke) : stroke }));
3246
+ }
3247
+ if (closed && open && filled === closed) {
3248
+ return [{ path: closed, fill: none, stroke: closedStroke(stroke) }, { path: open, fill: none, stroke }];
3249
+ }
3250
+ return [{ path, fill: none, stroke: open ? stroke : closedStroke(stroke) }];
3251
+ }
3252
+ function pathAsSvgDraws(node) {
3253
+ const { path, fill, stroke } = node;
3254
+ if (path.kind !== "polygon") return capsEnds(stroke) ? { ...node, stroke: closedStroke(stroke) } : node;
3255
+ if (path.commands.length === 0) return node;
3256
+ const { closed, open } = splitByClosure(path);
3257
+ const filled = fill.kind === "none" ? null : fillsOpenSubpaths(path) ? path : closed;
3258
+ const parts = strokeParts(path, stroke, filled, closed, open);
3259
+ const pieces = filled ? withFill(parts, filled, fill) : parts;
3260
+ if (pieces.length === 1) {
3261
+ const only = pieces[0];
3262
+ if (only.fill === fill && only.stroke === stroke) return node;
3263
+ const { stroke: _stroke, ...rest } = node;
3264
+ return { ...rest, fill: only.fill, ...only.stroke ? { stroke: only.stroke } : {} };
3265
+ }
3266
+ const group = {
3267
+ kind: "group",
3268
+ children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
3269
+ };
3270
+ if (node.rotation != null && node.rotation !== 0) {
3271
+ const b = pathBounds(path);
3272
+ group.transform = rotateAboutPoint((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2, node.rotation);
3069
3273
  }
3274
+ if (node.opacity != null) group.opacity = node.opacity;
3275
+ if (node.hidden) group.hidden = true;
3276
+ if (node.meta) group.meta = node.meta;
3277
+ return group;
3070
3278
  }
3071
- function nodeXml(node, registry, namespaces, warn) {
3072
- if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
3073
- if (node.kind === "text") return textXml(node, registry, namespaces, warn);
3074
- if (node.kind === "image") return imageXml(node, namespaces);
3075
- return pathXml(node, registry, namespaces, warn);
3279
+ function withFill(parts, filled, fill) {
3280
+ const host = parts.findIndex((p) => samePath(p.path, filled));
3281
+ if (host < 0) return [{ path: filled, fill, stroke: void 0 }, ...parts];
3282
+ return [{ ...parts[host], fill }, ...parts.filter((_, i) => i !== host)];
3076
3283
  }
3077
3284
  function warnStrokeAlign(align, warn) {
3078
3285
  if (align && align !== "center") {
3079
3286
  warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
3080
3287
  }
3081
3288
  }
3082
- function imageXml(node, namespaces) {
3083
- const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
3084
- const outer = [];
3085
- if (node.opacity != null && node.opacity !== 1) {
3086
- outer.push(`opacity="${trimNumber(node.opacity)}"`);
3087
- }
3088
- const frame = boxTransformAttr(node);
3089
- if (frame) outer.push(frame);
3090
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3091
- const metaEls = metaElementsXml(node.meta, namespaces);
3092
- const href = `href="${escapeAttr(node.href)}"`;
3093
- if (!node.source && !node.flipX && !node.flipY) {
3094
- const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
3095
- if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
3096
- return `<image ${attrs}${metaAttrs}/>`;
3097
- }
3098
- const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
3099
- const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
3100
- const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
3101
- const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3102
- return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
3103
- }
3104
- function groupXml(node, registry, namespaces, warn) {
3105
- const attrs = [];
3106
- if (node.transform) {
3107
- const m = formatMatrix(node.transform);
3108
- if (m) attrs.push(`transform="${m}"`);
3109
- }
3110
- if (node.clip) {
3111
- attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
3112
- }
3113
- if (node.opacity != null && node.opacity !== 1) {
3114
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3115
- }
3116
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3117
- const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3118
- const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
3119
- return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3120
- }
3121
- function pathXml(node, registry, namespaces, warn) {
3122
- const attrs = [`d="${serializePathD(node.path)}"`];
3123
- const fillAttrs = paintAttrs(node.fill, "fill", registry);
3124
- for (const a of fillAttrs) attrs.push(a);
3125
- if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3126
- attrs.push(`fill-rule="evenodd"`);
3127
- }
3128
- if (node.stroke) {
3129
- const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
3130
- for (const a of strokeAttrs) attrs.push(a);
3131
- } else {
3132
- attrs.push('stroke="none"');
3133
- }
3134
- if (node.opacity != null && node.opacity !== 1) {
3135
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3136
- }
3137
- if (node.rotation != null && node.rotation !== 0) {
3138
- const b = pathBounds(node.path);
3139
- const cx = b.minX + (b.maxX - b.minX) / 2;
3140
- const cy = b.minY + (b.maxY - b.minY) / 2;
3141
- const deg = node.rotation * 180 / Math.PI;
3142
- attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
3143
- }
3144
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3145
- const metaEls = metaElementsXml(node.meta, namespaces);
3146
- if (metaEls) {
3147
- return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
3148
- }
3149
- return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3150
- }
3151
3289
  function paintAttrs(paint, name, registry, includeOpacity = true) {
3152
3290
  if (paint.kind === "none") return [`${name}="none"`];
3153
3291
  const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
@@ -3171,7 +3309,7 @@ function coreStrokeAttrs(stroke, registry, warn) {
3171
3309
  const width2 = stroke.width ?? 1;
3172
3310
  if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
3173
3311
  warnStrokeAlign(stroke.align, warn);
3174
- const attrs = [`stroke="${"color" in paint ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3312
+ const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3175
3313
  attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
3176
3314
  attrs.push(...strokeWidthAttrs(width2));
3177
3315
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
@@ -3223,49 +3361,14 @@ function strokeAttrsFor(stroke, registry, warn) {
3223
3361
  }
3224
3362
  return attrs;
3225
3363
  }
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
3364
  function textXml(node, registry, namespaces, warn) {
3365
+ const x = node.x + anchorOffset(node.style, node.width, node.text);
3263
3366
  const attrs = [
3264
- `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
3367
+ `x="${trimNumber(x)}"`,
3265
3368
  `y="${trimNumber(node.y)}"`,
3266
3369
  `dominant-baseline="text-before-edge"`,
3267
- // The runs model stores real line breaks; SVG's default whitespace
3268
- // handling would collapse them (and any indentation) away on re-import.
3370
+ // SVG's default whitespace handling would collapse indentation and runs
3371
+ // of spaces away on re-import.
3269
3372
  `xml:space="preserve"`,
3270
3373
  `data-weasel-width="${trimNumber(node.width)}"`,
3271
3374
  `data-weasel-height="${trimNumber(node.height)}"`
@@ -3302,11 +3405,13 @@ function textXml(node, registry, namespaces, warn) {
3302
3405
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
3303
3406
  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
3407
  }
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));
3408
+ if (node.fill !== void 0) {
3409
+ const paint = node.fill?.paint;
3410
+ if (!paint) attrs.push('fill="none"');
3411
+ else {
3412
+ attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
3413
+ attrs.push(...opacityAttr("fill-opacity", paint.opacity));
3414
+ }
3310
3415
  }
3311
3416
  for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
3312
3417
  if (node.opacity != null && node.opacity !== 1) {
@@ -3314,7 +3419,9 @@ function textXml(node, registry, namespaces, warn) {
3314
3419
  }
3315
3420
  const frame = boxTransformAttr(node);
3316
3421
  if (frame) attrs.push(frame);
3317
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
3422
+ const pieces = node.runs && node.runs.length > 0 ? node.runs.map((r) => ({ text: r.text, attrs: runAttrs(r, style, registry, warn) })) : [{ text: node.text, attrs: null }];
3423
+ const resolved = resolveTextStyle(style);
3424
+ const body = linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3318
3425
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3319
3426
  const metaEls = metaElementsXml(node.meta, namespaces);
3320
3427
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -3324,7 +3431,7 @@ function scriptSizeFace(run, style) {
3324
3431
  const face = resolveRunFace(run, resolveTextStyle(style));
3325
3432
  return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3326
3433
  }
3327
- function runXml(run, style, registry, warn) {
3434
+ function runAttrs(run, style, registry, warn) {
3328
3435
  const attrs = [];
3329
3436
  if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
3330
3437
  else if (run.bold) attrs.push(`font-weight="700"`);
@@ -3346,18 +3453,15 @@ function runXml(run, style, registry, warn) {
3346
3453
  }
3347
3454
  const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3348
3455
  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
- }
3456
+ if (run.fill !== void 0) {
3457
+ const paint = run.fill?.paint;
3458
+ if (!paint) attrs.push('fill="none"');
3459
+ else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
3460
+ else attrs.push(`fill="${registry.ref(paint)}"`);
3356
3461
  }
3357
3462
  for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3358
3463
  if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
3359
- const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
3360
- return `${head}${escapeText(run.text)}</tspan>`;
3464
+ return attrs;
3361
3465
  }
3362
3466
  function textDecorationValue(underline, strikethrough, overline) {
3363
3467
  const tokens = [];
@@ -3366,9 +3470,193 @@ function textDecorationValue(underline, strikethrough, overline) {
3366
3470
  if (overline) tokens.push("overline");
3367
3471
  return tokens.length > 0 ? tokens.join(" ") : null;
3368
3472
  }
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 };
3473
+
3474
+ // src/prettyXml.ts
3475
+ var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
3476
+ var isTag = (t) => t !== void 0 && t.startsWith("<");
3477
+ var isClose = (t) => t.startsWith("</");
3478
+ var isSelfClosing = (t) => t.endsWith("/>");
3479
+ var tagName = (t) => /^<\/?([^\s/>]+)/.exec(t)[1];
3480
+ function matchingClose(tokens, open) {
3481
+ let depth = 0;
3482
+ for (let i = open; i < tokens.length; i++) {
3483
+ const t = tokens[i];
3484
+ if (!isTag(t) || isSelfClosing(t)) continue;
3485
+ depth += isClose(t) ? -1 : 1;
3486
+ if (depth === 0) return i;
3487
+ }
3488
+ return tokens.length - 1;
3489
+ }
3490
+ function prettyXml(xml) {
3491
+ const tokens = xml.match(TOKEN2) ?? [];
3492
+ const lines = [];
3493
+ let depth = 0;
3494
+ for (let i = 0; i < tokens.length; i++) {
3495
+ const t = tokens[i];
3496
+ if (!isTag(t) || isSelfClosing(t)) {
3497
+ lines.push(" ".repeat(depth) + t);
3498
+ } else if (isClose(t)) {
3499
+ depth--;
3500
+ lines.push(" ".repeat(depth) + t);
3501
+ } else if (tagName(t) === "text" || !isTag(tokens[i + 1])) {
3502
+ const close2 = matchingClose(tokens, i);
3503
+ lines.push(" ".repeat(depth) + tokens.slice(i, close2 + 1).join(""));
3504
+ i = close2;
3505
+ } else {
3506
+ lines.push(" ".repeat(depth) + t);
3507
+ depth++;
3508
+ }
3509
+ }
3510
+ return lines.join("\n");
3511
+ }
3512
+
3513
+ // src/serialize.ts
3514
+ function serializeSvg(nodes, opts = {}) {
3515
+ const registry = new PaintServerRegistry();
3516
+ registerPaintServers(nodes, registry);
3517
+ const bounds = opts.viewBox ?? computeBounds(nodes);
3518
+ const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3519
+ const namespaces = opts.namespaces ?? {};
3520
+ const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3521
+ if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3522
+ rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3523
+ }
3524
+ for (const [prefix, uri] of Object.entries(namespaces)) {
3525
+ rootAttrs.push(`xmlns:${prefix}="${escapeAttr(uri)}"`);
3526
+ }
3527
+ rootAttrs.push(`viewBox="${vb}"`);
3528
+ if (opts.width != null) rootAttrs.push(`width="${trimNumber(opts.width)}"`);
3529
+ if (opts.height != null) rootAttrs.push(`height="${trimNumber(opts.height)}"`);
3530
+ if (opts.documentMeta) {
3531
+ for (const prefix of Object.keys(namespaces)) {
3532
+ const bucket = opts.documentMeta[prefix];
3533
+ if (!bucket?.attrs) continue;
3534
+ for (const [name, value] of Object.entries(bucket.attrs)) {
3535
+ rootAttrs.push(`${prefix}:${name}="${escapeAttr(value)}"`);
3536
+ }
3537
+ }
3538
+ }
3539
+ const docMetaXml = metaElementsXml(opts.documentMeta, namespaces);
3540
+ const defsXml = registry.toDefsXml(opts.onWarn);
3541
+ const said = /* @__PURE__ */ new Set();
3542
+ const warn = (m) => {
3543
+ if (!opts.onWarn || said.has(m)) return;
3544
+ said.add(m);
3545
+ opts.onWarn(m);
3546
+ };
3547
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3548
+ const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3549
+ const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3550
+ return opts.pretty ? prettyXml(xml) : xml;
3551
+ }
3552
+ function registerPaintServers(nodes, registry) {
3553
+ for (const n of nodes) {
3554
+ if (n.kind === "group") {
3555
+ if (n.clip) registry.clipId(n.clip);
3556
+ registerPaintServers(n.children, registry);
3557
+ } else if (n.kind === "path") {
3558
+ if (n.fill.kind === "gradient") registry.register(n.fill.paint);
3559
+ if (n.stroke && n.stroke.paint.kind === "gradient") registry.register(n.stroke.paint.paint);
3560
+ registerMarkers(n.stroke, registry);
3561
+ } else if (n.kind === "text") {
3562
+ registerTextPaint(n.fill?.paint, registry);
3563
+ registerTextPaint(n.stroke?.paint, registry);
3564
+ registerMarkers(n.stroke, registry);
3565
+ for (const run of n.runs ?? []) {
3566
+ registerTextPaint(run.fill?.paint, registry);
3567
+ registerTextPaint(run.stroke?.paint, registry);
3568
+ }
3569
+ }
3570
+ }
3571
+ }
3572
+ function registerTextPaint(paint, registry) {
3573
+ if (paint && !isSolidPaint(paint)) registry.register(paint);
3574
+ }
3575
+ function registerMarkers(stroke, registry) {
3576
+ if (!stroke) return;
3577
+ for (const ref of [stroke.markerStart, stroke.markerMid, stroke.markerEnd]) {
3578
+ if (ref !== void 0) registry.markerId(ref);
3579
+ }
3580
+ }
3581
+ function nodeXml(node, registry, namespaces, warn) {
3582
+ const xml = kindXml(node, registry, namespaces, warn);
3583
+ return node.hidden ? xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`) : xml;
3584
+ }
3585
+ function kindXml(node, registry, namespaces, warn) {
3586
+ if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
3587
+ if (node.kind === "text") return textXml(node, registry, namespaces, warn);
3588
+ if (node.kind === "image") return imageXml(node, namespaces);
3589
+ const drawn = pathAsSvgDraws(node);
3590
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3591
+ }
3592
+ function imageXml(node, namespaces) {
3593
+ const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
3594
+ const outer = [];
3595
+ if (node.opacity != null && node.opacity !== 1) {
3596
+ outer.push(`opacity="${trimNumber(node.opacity)}"`);
3597
+ }
3598
+ const frame = boxTransformAttr(node);
3599
+ if (frame) outer.push(frame);
3600
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3601
+ const metaEls = metaElementsXml(node.meta, namespaces);
3602
+ const href = `href="${escapeAttr(node.href)}"`;
3603
+ if (!node.source && !node.flipX && !node.flipY) {
3604
+ const attrs = [href, box, 'preserveAspectRatio="none"', ...outer].join(" ");
3605
+ if (metaEls) return `<image ${attrs}${metaAttrs}>${metaEls}</image>`;
3606
+ return `<image ${attrs}${metaAttrs}/>`;
3607
+ }
3608
+ const src = node.source ?? { x: 0, y: 0, width: 1, height: 1 };
3609
+ const viewBox = [src.x, src.y, src.width, src.height].map(trimNumber).join(" ");
3610
+ const flip = node.flipX || node.flipY ? ` transform="matrix(${node.flipX ? -1 : 1} 0 0 ${node.flipY ? -1 : 1} ${trimNumber(node.flipX ? 2 * src.x + src.width : 0)} ${trimNumber(node.flipY ? 2 * src.y + src.height : 0)})"` : "";
3611
+ const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3612
+ return `${head}${metaAttrs}><svg ${box} viewBox="${viewBox}" preserveAspectRatio="none"><image ${href} x="0" y="0" width="1" height="1" preserveAspectRatio="none"${flip}/></svg>${metaEls}</g>`;
3613
+ }
3614
+ function groupXml(node, registry, namespaces, warn) {
3615
+ const attrs = [];
3616
+ if (node.transform) {
3617
+ const m = formatMatrix(node.transform);
3618
+ if (m) attrs.push(`transform="${m}"`);
3619
+ }
3620
+ if (node.clip) {
3621
+ attrs.push(`clip-path="url(#${registry.clipId(node.clip)})"`);
3622
+ }
3623
+ if (node.opacity != null && node.opacity !== 1) {
3624
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3625
+ }
3626
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3627
+ const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3628
+ const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
3629
+ return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3630
+ }
3631
+ function pathXml(node, registry, namespaces, warn) {
3632
+ const attrs = [`d="${serializePathD(node.path)}"`];
3633
+ const fillAttrs = paintAttrs(node.fill, "fill", registry);
3634
+ for (const a of fillAttrs) attrs.push(a);
3635
+ if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3636
+ attrs.push(`fill-rule="evenodd"`);
3637
+ }
3638
+ if (node.stroke) {
3639
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
3640
+ for (const a of strokeAttrs) attrs.push(a);
3641
+ } else {
3642
+ attrs.push('stroke="none"');
3643
+ }
3644
+ if (node.opacity != null && node.opacity !== 1) {
3645
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3646
+ }
3647
+ if (node.rotation != null && node.rotation !== 0) {
3648
+ const b = pathBounds(node.path);
3649
+ const cx = b.minX + (b.maxX - b.minX) / 2;
3650
+ const cy = b.minY + (b.maxY - b.minY) / 2;
3651
+ const deg = node.rotation * 180 / Math.PI;
3652
+ attrs.push(`transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`);
3653
+ }
3654
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3655
+ const metaEls = metaElementsXml(node.meta, namespaces);
3656
+ if (metaEls) {
3657
+ return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
3658
+ }
3659
+ return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3372
3660
  }
3373
3661
  function svgNeeds(nodes) {
3374
3662
  const registry = new PaintServerRegistry();
@@ -3395,6 +3683,22 @@ function warmSvg(nodes) {
3395
3683
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
3396
3684
  ]).then(() => void 0);
3397
3685
  }
3686
+
3687
+ // src/fadePaint.ts
3688
+ function fadeFill(fill, k) {
3689
+ return k === 1 ? fill : { ...fill, opacity: (fill.opacity ?? 1) * k };
3690
+ }
3691
+ function fadeStroke(stroke, k) {
3692
+ return k === 1 || !stroke.paint ? stroke : { ...stroke, paint: fadeFill(stroke.paint, k) };
3693
+ }
3694
+ function fadeRun(run, k) {
3695
+ if (k === 1 || !run.fill && !run.stroke) return run;
3696
+ return {
3697
+ ...run,
3698
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fadeFill(p, k)) } : {},
3699
+ ...run.stroke ? { stroke: fadeStroke(run.stroke, k) } : {}
3700
+ };
3701
+ }
3398
3702
  var CASCADE_OFFSET_PX = 24;
3399
3703
  var VIEWPORT_FIT = 0.9;
3400
3704
  function readFileText(file) {
@@ -3413,20 +3717,6 @@ function fillDataFromSvg(paint, box) {
3413
3717
  }
3414
3718
  return fillInBoxFrame(paint.paint, box);
3415
3719
  }
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
3720
  function strokeDataFromSvg(stroke, box) {
3431
3721
  if (!stroke || stroke.paint.kind === "none") return void 0;
3432
3722
  const paint = fillDataFromSvg(stroke.paint, box);
@@ -3448,7 +3738,7 @@ function fillInBoxFrame(fill, box) {
3448
3738
  return units === "world" ? fillToBoundsFrame(fill, box) : fill;
3449
3739
  }
3450
3740
  function runInBoxFrame(run, box) {
3451
- const fill = run.fill ? fillInBoxFrame(run.fill, box) : run.fill;
3741
+ const fill = run.fill ? mapFillPaint(run.fill, (p) => fillInBoxFrame(p, box)) : run.fill;
3452
3742
  const stroke = run.stroke ? strokeInBoxFrame(run.stroke, box) : run.stroke;
3453
3743
  if (fill === run.fill && stroke === run.stroke) return run;
3454
3744
  return { ...run, ...fill ? { fill } : {}, ...stroke ? { stroke } : {} };
@@ -3468,7 +3758,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3468
3758
  const id = nextId(source);
3469
3759
  const out = options.leaf ? options.leaf(id, { pose, data }, source) : data;
3470
3760
  if (out === null) return null;
3471
- drafts.push({ kind: "leaf", id, parentId, pose, data: out });
3761
+ drafts.push({ kind: "leaf", id, parentId, pose, data: out, ...source.hidden ? { hidden: true } : {} });
3472
3762
  return pose;
3473
3763
  };
3474
3764
  const visit = (n, parentId, inherited2) => {
@@ -3478,7 +3768,9 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3478
3768
  kind: "container",
3479
3769
  id: nextId(n),
3480
3770
  parentId,
3481
- pose: { x: 0, y: 0, width: 0, height: 0 }
3771
+ pose: { x: 0, y: 0, width: 0, height: 0 },
3772
+ data: n.clip ? { path: n.clip, fill: null } : {},
3773
+ ...n.hidden ? { hidden: true } : {}
3482
3774
  };
3483
3775
  drafts.push(draft);
3484
3776
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
@@ -3486,7 +3778,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3486
3778
  drafts.splice(drafts.indexOf(draft), 1);
3487
3779
  return null;
3488
3780
  }
3489
- draft.pose = acc;
3781
+ draft.pose = n.clip ? boxOfPath(n.clip) : acc;
3490
3782
  return draft.pose;
3491
3783
  }
3492
3784
  if (n.kind === "text") {
@@ -3512,7 +3804,7 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3512
3804
  ...n.runs ? { runs: n.runs.map((r) => fadeRun(runInBoxFrame(r, box), k)) } : {},
3513
3805
  // `!== undefined`, not a truthiness test: `null` is the document
3514
3806
  // saying `fill="none"`, and absent takes the painter's default.
3515
- ...fill2 !== void 0 ? { fill: fill2 === null ? null : fadeFill(fillInBoxFrame(fill2, box), k) } : {},
3807
+ ...fill2 !== void 0 ? { fill: fill2 && mapFillPaint(fill2, (p) => fadeFill(fillInBoxFrame(p, box), k)) } : {},
3516
3808
  ...n.stroke !== void 0 ? { stroke: fadeStroke(strokeInBoxFrame(n.stroke, box), k) } : {}
3517
3809
  });
3518
3810
  }
@@ -3530,20 +3822,24 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3530
3822
  }
3531
3823
  });
3532
3824
  }
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);
3825
+ const b = boxOfPath(n.path);
3534
3826
  const pose = { x: b.x, y: b.y, width: b.width, height: b.height };
3535
3827
  if (n.rotation) pose.rotation = n.rotation;
3536
3828
  const fill = fillDataFromSvg(n.fill, b);
3537
3829
  const strokeFromSvg = strokeDataFromSvg(n.stroke, b);
3538
3830
  return pushLeaf(n, parentId, pose, {
3539
3831
  path: n.path,
3540
- ...fill !== void 0 ? { fill: fill && fadeFill(fill, k) } : {},
3832
+ ...fill !== void 0 ? { fill: fill && { paint: fadeFill(fill, k) } } : {},
3541
3833
  ...strokeFromSvg !== void 0 ? { stroke: fadeStroke(strokeFromSvg, k) } : {}
3542
3834
  });
3543
3835
  };
3544
3836
  for (const n of nodes) visit(n, null, 1);
3545
3837
  return drafts;
3546
3838
  }
3839
+ function boxOfPath(path) {
3840
+ const b = boundsOfPath(path);
3841
+ return { x: b.x, y: b.y, width: b.width, height: b.height };
3842
+ }
3547
3843
  function isNodeList(input) {
3548
3844
  return Array.isArray(input);
3549
3845
  }
@@ -3575,7 +3871,7 @@ async function unpackSvgFiles(files, ctx) {
3575
3871
  const wrapperId = ctx.scene.mintId();
3576
3872
  const union2 = unionAABB(roots.map((d) => d.pose));
3577
3873
  drafts = [
3578
- { kind: "container", id: wrapperId, parentId: null, pose: union2 },
3874
+ { kind: "container", id: wrapperId, parentId: null, pose: union2, data: {} },
3579
3875
  ...drafts.map((d) => d.parentId === null ? { ...d, parentId: wrapperId } : d)
3580
3876
  ];
3581
3877
  }
@@ -3607,7 +3903,7 @@ async function unpackSvgFiles(files, ctx) {
3607
3903
  kind: d.kind,
3608
3904
  layer,
3609
3905
  pose: place(d.pose),
3610
- data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : {},
3906
+ data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3611
3907
  parent: d.parentId
3612
3908
  },
3613
3909
  label: "Insert SVG"
@@ -3622,7 +3918,7 @@ async function unpackSvgFiles(files, ctx) {
3622
3918
  }
3623
3919
  function svgPaintFromKit(fill, box) {
3624
3920
  if (fill === null) return { kind: "none" };
3625
- if (fill.fill === void 0 || fill.fill === "solid") {
3921
+ if (fill.kind === void 0 || fill.kind === "solid") {
3626
3922
  return { kind: "solid", color: fill.color, ...fill.opacity != null ? { opacity: fill.opacity } : {} };
3627
3923
  }
3628
3924
  return { kind: "gradient", paint: fillInPoseFrame(fill, box) };
@@ -3670,7 +3966,7 @@ function runInPoseFrame(run, box) {
3670
3966
  if (!run.fill && !run.stroke) return run;
3671
3967
  return {
3672
3968
  ...run,
3673
- ...run.fill ? { fill: fillInPoseFrame(run.fill, box) } : {},
3969
+ ...run.fill ? { fill: mapFillPaint(run.fill, (p) => fillInPoseFrame(p, box)) } : {},
3674
3970
  ...run.stroke ? { stroke: strokeInPoseFrame(run.stroke, box) } : {}
3675
3971
  };
3676
3972
  }
@@ -3688,7 +3984,7 @@ function svgLeafFromKit(data, pose) {
3688
3984
  if (data.style) node2.style = data.style;
3689
3985
  if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
3690
3986
  if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
3691
- if (data.fill !== void 0) node2.fill = data.fill && fillInPoseFrame(data.fill, pose);
3987
+ if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
3692
3988
  if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3693
3989
  if (pose.rotation) node2.rotation = pose.rotation;
3694
3990
  if (pose.transform) node2.transform = pose.transform;
@@ -3700,10 +3996,15 @@ function svgLeafFromKit(data, pose) {
3700
3996
  const node = {
3701
3997
  kind: "path",
3702
3998
  path: pathInPoseFrame(data.path, pose),
3703
- fill: svgPaintFromKit(fill, pose)
3999
+ fill: svgPaintFromKit(fill?.paint ?? null, pose)
3704
4000
  };
3705
4001
  if (stroke) node.stroke = stroke;
3706
- if (pose.rotation) node.rotation = pose.rotation;
4002
+ const frame = poseLinearOf(pose) && poseFrameOf(pose);
4003
+ if (frame) {
4004
+ node.path = pathThroughPoseFrame(node.path, pose);
4005
+ node.fill = paintThroughMatrix(node.fill, frame);
4006
+ if (stroke) node.stroke = strokeThroughMatrix(stroke, frame);
4007
+ } else if (pose.rotation) node.rotation = pose.rotation;
3707
4008
  return node;
3708
4009
  }
3709
4010
  function svgNodesFromKit(tree, options = {}) {
@@ -3713,12 +4014,25 @@ function svgNodesFromKit(tree, options = {}) {
3713
4014
  const node = tree.get(id);
3714
4015
  if (!node) return null;
3715
4016
  if (node.kind === "container") {
3716
- const children = [];
3717
- for (const c of tree.childrenOf(id)) {
3718
- const child = emit(c);
3719
- if (child) children.push(child);
4017
+ const emitChildren = () => {
4018
+ const children = [];
4019
+ for (const c of tree.childrenOf(id)) {
4020
+ const child = emit(c);
4021
+ if (child) children.push(child);
4022
+ }
4023
+ return children;
4024
+ };
4025
+ const path = node.data?.path;
4026
+ const clip = path ? pathThroughPoseFrame(pathInPoseFrame(path, node.pose), node.pose) : void 0;
4027
+ const copies = node.copiesFromPose?.(node.pose, node) ?? [];
4028
+ const content = () => ({ kind: "group", children: emitChildren(), ...clip ? { clip } : {} });
4029
+ let g = content();
4030
+ if (copies.length > 0) {
4031
+ g = {
4032
+ kind: "group",
4033
+ children: [g, ...copies.map((transform) => ({ kind: "group", transform, children: [content()] }))]
4034
+ };
3720
4035
  }
3721
- const g = { kind: "group", children };
3722
4036
  return group ? group(id, g) : g;
3723
4037
  }
3724
4038
  return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);