@weasel-js/svg 1.10.1 → 1.11.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,6 @@
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';
1
+ import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, splitSubpaths } from '@weasel-js/geom';
2
+ export { pathToD as serializePathD } from '@weasel-js/geom';
3
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, pathThroughPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, DEFAULT_TEXT_STYLE, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, strokeToPath, toRuns, resolveRunFace, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
4
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
5
 
5
6
  // src/types.ts
@@ -177,9 +178,11 @@ function scaleLength(v, scale) {
177
178
  return typeof v === "number" ? v * scale : v;
178
179
  }
179
180
  function scaleStroke(stroke, scale) {
180
- const { width: width2 } = stroke;
181
- if (scale === 1 || typeof width2 === "object") return stroke;
182
- const out = { ...stroke };
181
+ const { width: width2, vertexWidths } = stroke;
182
+ if (scale === 1) return stroke;
183
+ const scaledVertices = vertexWidths ? { vertexWidths: vertexWidths.map((n) => scaleVertexWidth(n, scale)) } : {};
184
+ if (typeof width2 === "object") return vertexWidths ? { ...stroke, ...scaledVertices } : stroke;
185
+ const out = { ...stroke, ...scaledVertices };
183
186
  if (width2 !== void 0) out.width = width2 * scale;
184
187
  if (out.dash) out.dash = out.dash.map((n) => n * scale);
185
188
  return out;
@@ -1381,39 +1384,6 @@ function readPattern(el, onWarn) {
1381
1384
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1382
1385
  return paint;
1383
1386
  }
1384
- function serializePathD(path) {
1385
- if (path.kind === "rect") {
1386
- const { x, y, width: width2, height: height2 } = path;
1387
- return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
1388
- }
1389
- const cmds = path.commands;
1390
- const xs = path.coords;
1391
- const out = [];
1392
- let ci = 0;
1393
- for (let i = 0; i < cmds.length; i++) {
1394
- const c = cmds[i];
1395
- if (c === PATH_M) {
1396
- out.push(`M${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1397
- ci += 2;
1398
- } else if (c === PATH_L) {
1399
- out.push(`L${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])}`);
1400
- ci += 2;
1401
- } else if (c === PATH_C) {
1402
- out.push(
1403
- `C${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])} ${trimNumber(xs[ci + 4])} ${trimNumber(xs[ci + 5])}`
1404
- );
1405
- ci += 6;
1406
- } else if (c === PATH_Q) {
1407
- out.push(
1408
- `Q${trimNumber(xs[ci])} ${trimNumber(xs[ci + 1])} ${trimNumber(xs[ci + 2])} ${trimNumber(xs[ci + 3])}`
1409
- );
1410
- ci += 4;
1411
- } else if (c === PATH_Z) {
1412
- out.push("Z");
1413
- }
1414
- }
1415
- return out.join("");
1416
- }
1417
1387
 
1418
1388
  // src/gradients.ts
1419
1389
  var WEASEL_NS = "urn:weasel-js:svg";
@@ -1719,7 +1689,7 @@ var PaintServerRegistry = class {
1719
1689
  }
1720
1690
  for (const path of this.clips) {
1721
1691
  parts.push(
1722
- `<clipPath id="${this.clipIds.get(path)}"><path d="${serializePathD(path)}"/></clipPath>`
1692
+ `<clipPath id="${this.clipIds.get(path)}"><path d="${pathToD(path)}"/></clipPath>`
1723
1693
  );
1724
1694
  }
1725
1695
  parts.push("</defs>");
@@ -1733,7 +1703,7 @@ function defaultMarkerXml(id, entry, onWarn, key, size) {
1733
1703
  const name = key ?? id;
1734
1704
  const unit = size === void 0 ? 1 : resolveMarkerSize({ key: name, size }, 1);
1735
1705
  const path = entry.path({ size: unit, stroke: { paint: { kind: "solid", color: "#000" } } });
1736
- const d = serializePathD(path);
1706
+ const d = pathToD(path);
1737
1707
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1738
1708
  const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
1739
1709
  const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
@@ -2096,6 +2066,97 @@ function coreStroke(stroke) {
2096
2066
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
2097
2067
  return out;
2098
2068
  }
2069
+ var VERTEX_WIDTHS = "vertex-widths";
2070
+ var NIB = "nib";
2071
+ var WIDTH = "stroke-width";
2072
+ var ROLE = "role";
2073
+ var OUTLINE = "stroke-outline";
2074
+ var GROUP = "stroke-group";
2075
+ var MARKER_ATTR = /^(marker-(?:start|mid|end))=/;
2076
+ var UNSET = "-";
2077
+ var SIDES = "/";
2078
+ function attrOf(el, name) {
2079
+ return el.getAttributeNS(WEASEL_NS, name) ?? el.getAttribute(`${WEASEL_NS_PREFIX}:${name}`);
2080
+ }
2081
+ function hasVariableWidth(stroke) {
2082
+ return (stroke?.vertexWidths?.length ?? 0) > 0 || stroke?.nib !== void 0;
2083
+ }
2084
+ function writesVariableWidth(nodes) {
2085
+ return nodes.some((n) => n.kind === "group" ? writesVariableWidth(n.children) : n.kind === "path" && hasVariableWidth(n.stroke));
2086
+ }
2087
+ function widthToken(entry) {
2088
+ const { left, right } = vertexHalfWidths(entry, NaN);
2089
+ const side = (v) => Number.isNaN(v) ? UNSET : trimNumber(v);
2090
+ if (typeof entry === "number") return Number.isNaN(left) ? UNSET : trimNumber(left + right);
2091
+ return Number.isNaN(left) && Number.isNaN(right) ? UNSET : `${side(left)}${SIDES}${side(right)}`;
2092
+ }
2093
+ function widthOfToken(token) {
2094
+ const side = (v) => v === UNSET ? NaN : Number(v);
2095
+ if (!token.includes(SIDES)) return side(token);
2096
+ const [left, right] = token.split(SIDES);
2097
+ return { left: side(left), right: side(right) };
2098
+ }
2099
+ function variableWidthAttrs(stroke, strokeAttrs) {
2100
+ const width2 = typeof stroke.width === "number" ? stroke.width : stroke.width.px;
2101
+ const out = [
2102
+ ...strokeAttrs.map((a) => a.startsWith("stroke-width=") ? 'stroke-width="0"' : MARKER_ATTR.test(a) ? `${WEASEL_NS_PREFIX}:${a}` : a),
2103
+ `${WEASEL_NS_PREFIX}:${WIDTH}="${trimNumber(width2)}"`
2104
+ ];
2105
+ if (stroke.vertexWidths?.length) {
2106
+ const list = stroke.vertexWidths.map(widthToken).join(" ");
2107
+ out.push(`${WEASEL_NS_PREFIX}:${VERTEX_WIDTHS}="${list}"`);
2108
+ }
2109
+ if (stroke.nib) {
2110
+ out.push(`${WEASEL_NS_PREFIX}:${NIB}="${String(stroke.nib.angle)} ${String(stroke.nib.ratio)}"`);
2111
+ }
2112
+ return out;
2113
+ }
2114
+ function strokeOutlineXml(path, stroke, fillAttrs, transform) {
2115
+ const kit = {
2116
+ width: stroke.width,
2117
+ ...stroke.vertexWidths ? { vertexWidths: stroke.vertexWidths } : {},
2118
+ ...stroke.nib ? { nib: stroke.nib } : {},
2119
+ ...stroke.cap ? { cap: stroke.cap } : {},
2120
+ ...stroke.join ? { join: stroke.join } : {},
2121
+ ...stroke.dash ? { dash: stroke.dash } : {},
2122
+ ...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {},
2123
+ ...stroke.align ? { align: stroke.align } : {},
2124
+ ...stroke.markerStart ? { markerStart: stroke.markerStart } : {},
2125
+ ...stroke.markerMid ? { markerMid: stroke.markerMid } : {},
2126
+ ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
2127
+ };
2128
+ const outline = strokeToPath(path, kit);
2129
+ const attrs = [`d="${pathToD(outline)}"`, ...fillAttrs, 'stroke="none"'];
2130
+ if (transform) attrs.push(transform);
2131
+ attrs.push(`${WEASEL_NS_PREFIX}:${ROLE}="${OUTLINE}"`);
2132
+ return `<path ${attrs.join(" ")}/>`;
2133
+ }
2134
+ function strokeGroupXml(opacity, inner) {
2135
+ return `<g ${opacity} ${WEASEL_NS_PREFIX}:${ROLE}="${GROUP}">${inner}</g>`;
2136
+ }
2137
+ function unwrapStrokeGroup(el, node) {
2138
+ if (attrOf(el, ROLE) !== GROUP || !node || Array.isArray(node) || node.kind !== "group") return node;
2139
+ const [only] = node.children;
2140
+ if (node.children.length !== 1 || only.kind !== "path") return node;
2141
+ return node.opacity != null ? { ...only, opacity: node.opacity * (only.opacity ?? 1) } : only;
2142
+ }
2143
+ function isStrokeOutline(el) {
2144
+ return attrOf(el, ROLE) === OUTLINE;
2145
+ }
2146
+ function withVariableWidth(el, stroke) {
2147
+ const width2 = parseFloat(attrOf(el, WIDTH) ?? "");
2148
+ if (!stroke || !Number.isFinite(width2)) return stroke;
2149
+ const out = { ...stroke, width: typeof stroke.width === "number" ? width2 : { px: width2 } };
2150
+ const list = attrOf(el, VERTEX_WIDTHS);
2151
+ if (list) out.vertexWidths = list.trim().split(/\s+/).map(widthOfToken);
2152
+ const [angle, ratio] = (attrOf(el, NIB) ?? "").trim().split(/\s+/).map(Number);
2153
+ if (Number.isFinite(angle) && Number.isFinite(ratio)) out.nib = { angle, ratio };
2154
+ for (const [attr, field] of [["marker-start", "markerStart"], ["marker-mid", "markerMid"], ["marker-end", "markerEnd"]]) {
2155
+ const id = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attrOf(el, attr)?.trim() ?? "")?.[1];
2156
+ if (id) out[field] = id;
2157
+ }
2158
+ return out;
2159
+ }
2099
2160
  var near = (v, target) => Math.abs(v - target) <= 1e-9;
2100
2161
  function placeBox(box, m, stretch) {
2101
2162
  const [a, b, c, d] = m;
@@ -2239,6 +2300,7 @@ function anchorOffset(style, width2, text) {
2239
2300
 
2240
2301
  // src/tspanLines.ts
2241
2302
  var LINES_ATTR = "data-weasel-lines";
2303
+ var LINE_HEIGHT_ATTR = "data-weasel-line-height";
2242
2304
  var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
2243
2305
  function linesXml(pieces, x, pitch) {
2244
2306
  let out = "";
@@ -2286,9 +2348,9 @@ var LineTracker = class {
2286
2348
  const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2287
2349
  this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2288
2350
  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);
2351
+ const stated = Number(tspan.getAttribute(LINES_ATTR) ?? NaN);
2352
+ const lines = Number.isInteger(stated) && stated >= 0 ? stated : 1;
2353
+ if (afterText && lines > 0 && this.at > from) this.pitches.push((this.at - from) / lines);
2292
2354
  return lines;
2293
2355
  }
2294
2356
  /**
@@ -2308,13 +2370,17 @@ var LineTracker = class {
2308
2370
  return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2309
2371
  }
2310
2372
  };
2373
+ var holdsOnlyText = (el) => !Array.from(el.childNodes).some(
2374
+ (child) => child.nodeType === 1
2375
+ /* ELEMENT_NODE */
2376
+ );
2311
2377
  function warnUnreadPositions(el, onWarn) {
2312
2378
  const tag = el.tagName.toLowerCase();
2313
2379
  for (const name of ["x", "y"]) {
2314
2380
  const raw = el.getAttribute(name)?.trim() ?? "";
2315
2381
  if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2316
2382
  }
2317
- for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
2383
+ for (const name of tag === "tspan" || holdsOnlyText(el) ? ["rotate"] : ["dx", "rotate"]) {
2318
2384
  const raw = el.getAttribute(name)?.trim() ?? "";
2319
2385
  if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2320
2386
  onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
@@ -2347,6 +2413,76 @@ function joinLines(lines, leading = 0) {
2347
2413
  }
2348
2414
  return out;
2349
2415
  }
2416
+
2417
+ // src/laidLines.ts
2418
+ var LAID_ATTR = "data-weasel-layout";
2419
+ function layoutMeasured(layout, text) {
2420
+ return layout.laid.bounds.width > 0 || text.trim() === "";
2421
+ }
2422
+ function anchorX(cells, { anchor, rtl }) {
2423
+ const left = Math.min(...cells.map((c) => c.x));
2424
+ const right = Math.max(...cells.map((c) => c.x + c.advance));
2425
+ if (anchor === "middle") return (left + right) / 2;
2426
+ return anchor === "start" === !rtl ? left : right;
2427
+ }
2428
+ function wordStarts(text, start, end) {
2429
+ const out = [start];
2430
+ for (let i = start + 1; i < end; i++) {
2431
+ if (!/\s/.test(text[i]) && /\s/.test(text[i - 1])) out.push(i);
2432
+ }
2433
+ return out;
2434
+ }
2435
+ function rangeXml(pieces, offsets, start, end, lead) {
2436
+ let out = "";
2437
+ let first = true;
2438
+ pieces.forEach((piece, k) => {
2439
+ const from = Math.max(start, offsets[k]);
2440
+ const to = Math.min(end, offsets[k] + piece.text.length);
2441
+ if (from >= to) return;
2442
+ const text = piece.text.slice(from - offsets[k], to - offsets[k]);
2443
+ const attrs = [...piece.attrs ?? [], ...first ? lead : []];
2444
+ first = false;
2445
+ out += piece.attrs === null && attrs.length === 0 ? escapeText(text) : `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
2446
+ });
2447
+ return out;
2448
+ }
2449
+ var HARD_BREAKS = /\r\n|[\n\v\f\r\u0085\u2028\u2029]/g;
2450
+ var breaksIn = (text) => text.match(HARD_BREAKS)?.length ?? 0;
2451
+ function laidLinesXml(pieces, layout, anchor) {
2452
+ const text = pieces.map((p) => p.text).join("");
2453
+ const offsets = [];
2454
+ pieces.reduce((at, p) => {
2455
+ offsets.push(at);
2456
+ return at + p.text.length;
2457
+ }, 0);
2458
+ const px = (x) => `x="${trimNumber(layout.x + x)}"`;
2459
+ const py = (line) => `y="${trimNumber(layout.y + line.baselineY)}"`;
2460
+ let out = "";
2461
+ let wrote = false;
2462
+ let prevEnd = 0;
2463
+ const count = (lines) => {
2464
+ const stated = lines !== 1 || !wrote ? [`${LINES_ATTR}="${lines}"`] : [];
2465
+ wrote = true;
2466
+ return stated;
2467
+ };
2468
+ for (const line of layout.laid.lines) {
2469
+ if (line.cells.length === 0) continue;
2470
+ const start = Math.min(...line.cells.map((c) => c.srcIndex));
2471
+ const stepped = breaksIn(text.slice(prevEnd, start));
2472
+ prevEnd = line.srcEnd;
2473
+ const starts = line.justified ? wordStarts(text, start, line.srcEnd) : [start];
2474
+ starts.forEach((from, k) => {
2475
+ const to = starts[k + 1] ?? line.srcEnd;
2476
+ const cells = line.cells.filter((c) => c.srcIndex >= from && c.srcIndex < to);
2477
+ const lead = [px(anchorX(cells, anchor)), ...k === 0 ? [py(line), ...count(stepped)] : []];
2478
+ out += rangeXml(pieces, offsets, from, to, lead);
2479
+ });
2480
+ }
2481
+ const trailing = breaksIn(text.slice(prevEnd));
2482
+ const last = layout.laid.lines[layout.laid.lines.length - 1];
2483
+ if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
2484
+ return out;
2485
+ }
2350
2486
  var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
2351
2487
  function readDx(el, onWarn) {
2352
2488
  const raw = el.getAttribute("dx")?.trim();
@@ -2375,8 +2511,9 @@ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2375
2511
  return 0;
2376
2512
  }
2377
2513
  const raw = tspan.getAttribute("dx");
2514
+ const tag = tspan.tagName.toLowerCase();
2378
2515
  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`);
2516
+ onWarn(`<${tag} dx="${raw}"> is not read: its text has white space SVG collapses, which moves the glyphs the list names`);
2380
2517
  line.push(run);
2381
2518
  return 0;
2382
2519
  }
@@ -2389,7 +2526,7 @@ function placeTspanRun(tspan, run, lines, inherited2, onWarn) {
2389
2526
  } else if (dx[0] !== 0 && lines.length === 1) {
2390
2527
  lead = dx[0];
2391
2528
  } 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`);
2529
+ onWarn(`<${tag} dx="${raw}"> opens a line; the line starts at the box edge, and the rest of the list is read`);
2393
2530
  }
2394
2531
  line.push(...spread(run, dx.slice(1), inherited2));
2395
2532
  return lead;
@@ -2455,6 +2592,10 @@ function laidOutWidth(runs, style, plain) {
2455
2592
  const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
2456
2593
  return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
2457
2594
  }
2595
+ function numberAttr(el, name) {
2596
+ const n = parseFloat(el.getAttribute(name) ?? "");
2597
+ return Number.isFinite(n) ? n : void 0;
2598
+ }
2458
2599
  function placeTextNode(node, m, onWarn) {
2459
2600
  const cx = node.x + node.width / 2;
2460
2601
  const cy = node.y + node.height / 2;
@@ -2496,13 +2637,14 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2496
2637
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2497
2638
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2498
2639
  const dominantBaseline = el.getAttribute("dominant-baseline");
2499
- const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2640
+ const laid = el.hasAttribute(LAID_ATTR);
2641
+ const explicitTopAnchor = laid || dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2500
2642
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2501
2643
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2502
2644
  const tracker = new LineTracker(el, onWarn);
2503
2645
  const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
2504
2646
  const plain = runs.map((r) => r.text).join("");
2505
- const pitched = tracker.lineHeight(fontSize);
2647
+ const pitched = laid ? numberAttr(el, LINE_HEIGHT_ATTR) : tracker.lineHeight(fontSize);
2506
2648
  if (pitched !== void 0) textStyle.lineHeight = pitched;
2507
2649
  const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2508
2650
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
@@ -2546,7 +2688,10 @@ function readRuns(el, tracker, inheritedShift, letterSpacing, gradients, leafSty
2546
2688
  const push = (run) => {
2547
2689
  runLines[runLines.length - 1].push(run);
2548
2690
  };
2549
- for (let i = 0; i < el.childNodes.length; i++) {
2691
+ if (holdsOnlyText(el) && el.hasAttribute("dx")) {
2692
+ const t = el.textContent ?? "";
2693
+ if (t) lead = placeTspanRun(el, { text: t, ...inheritedShift }, runLines, letterSpacing, onWarn);
2694
+ } else for (let i = 0; i < el.childNodes.length; i++) {
2550
2695
  const child = el.childNodes[i];
2551
2696
  if (child.nodeType === 3) {
2552
2697
  const t = child.textContent ?? "";
@@ -3017,10 +3162,10 @@ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix
3017
3162
  for (let i = 0; i < parent.children.length; i++) {
3018
3163
  const el = parent.children[i];
3019
3164
  const ns = el.namespaceURI;
3020
- if (ns && ns !== "http://www.w3.org/2000/svg") continue;
3165
+ if (ns && ns !== "http://www.w3.org/2000/svg" || isStrokeOutline(el)) continue;
3021
3166
  const hidden = isHiddenElement(el, style);
3022
3167
  if (hidden && !keepHidden) continue;
3023
- const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
3168
+ const node = unwrapStrokeGroup(el, parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden));
3024
3169
  if (!node) continue;
3025
3170
  const parsed = Array.isArray(node) ? node : [node];
3026
3171
  if (hidden) for (const n of parsed) n.hidden = true;
@@ -3113,7 +3258,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, kee
3113
3258
  }
3114
3259
  const leafStyle = deriveStyle(style, el);
3115
3260
  const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
3116
- const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
3261
+ const ownStroke = withVariableWidth(el, readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el)));
3117
3262
  const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
3118
3263
  if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
3119
3264
  onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
@@ -3292,6 +3437,12 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
3292
3437
  if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
3293
3438
  return out;
3294
3439
  }
3440
+ function strokePaintAsFillAttrs(stroke, registry) {
3441
+ return [
3442
+ ...paintAttrs(stroke.paint, "fill", registry, false),
3443
+ ...opacityAttr("fill-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint))
3444
+ ];
3445
+ }
3295
3446
  function svgPaintOpacity(paint) {
3296
3447
  if (paint.kind === "solid") return paint.opacity;
3297
3448
  return paint.kind === "gradient" ? paint.paint.opacity : void 0;
@@ -3361,12 +3512,13 @@ function strokeAttrsFor(stroke, registry, warn) {
3361
3512
  }
3362
3513
  return attrs;
3363
3514
  }
3364
- function textXml(node, registry, namespaces, warn) {
3515
+ function textXml(node, registry, namespaces, warn, given) {
3516
+ const layout = given && layoutMeasured(given, node.text) ? given : null;
3365
3517
  const x = node.x + anchorOffset(node.style, node.width, node.text);
3366
3518
  const attrs = [
3367
3519
  `x="${trimNumber(x)}"`,
3368
3520
  `y="${trimNumber(node.y)}"`,
3369
- `dominant-baseline="text-before-edge"`,
3521
+ layout ? `${LAID_ATTR}="lines"` : `dominant-baseline="text-before-edge"`,
3370
3522
  // SVG's default whitespace handling would collapse indentation and runs
3371
3523
  // of spaces away on re-import.
3372
3524
  `xml:space="preserve"`,
@@ -3381,9 +3533,10 @@ function textXml(node, registry, namespaces, warn) {
3381
3533
  if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
3382
3534
  const direction = svgDirection(style, node.text);
3383
3535
  if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
3536
+ let anchor = "start";
3384
3537
  if (style?.align != null || direction === "rtl") {
3385
3538
  const align = style?.align ?? "left";
3386
- const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3539
+ anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3387
3540
  if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
3388
3541
  if (align === "justify") attrs.push('data-weasel-align="justify"');
3389
3542
  }
@@ -3398,13 +3551,14 @@ function textXml(node, registry, namespaces, warn) {
3398
3551
  }
3399
3552
  if (style?.wrap) {
3400
3553
  attrs.push('data-weasel-wrap="true"');
3401
- warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
3554
+ if (!layout) warn("text that wraps at its box width is written unwrapped: SVG text does not wrap, so other readers draw one line per hard break (weasel reads the wrap back)");
3402
3555
  }
3403
3556
  if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
3404
3557
  if (node.verticalAlign && node.verticalAlign !== "top") {
3405
3558
  attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
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)`);
3559
+ if (!layout) 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)`);
3407
3560
  }
3561
+ if (layout && style?.lineHeight != null) attrs.push(`${LINE_HEIGHT_ATTR}="${trimNumber(style.lineHeight)}"`);
3408
3562
  if (node.fill !== void 0) {
3409
3563
  const paint = node.fill?.paint;
3410
3564
  if (!paint) attrs.push('fill="none"');
@@ -3421,7 +3575,7 @@ function textXml(node, registry, namespaces, warn) {
3421
3575
  if (frame) attrs.push(frame);
3422
3576
  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
3577
  const resolved = resolveTextStyle(style);
3424
- const body = linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3578
+ const body = layout ? laidLinesXml(pieces, layout, { anchor, rtl: direction === "rtl" }) : linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3425
3579
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3426
3580
  const metaEls = metaElementsXml(node.meta, namespaces);
3427
3581
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -3518,7 +3672,7 @@ function serializeSvg(nodes, opts = {}) {
3518
3672
  const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3519
3673
  const namespaces = opts.namespaces ?? {};
3520
3674
  const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3521
- if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3675
+ if ((registry.usesPrivateNamespace() || writesVariableWidth(nodes)) && namespaces[WEASEL_NS_PREFIX] == null) {
3522
3676
  rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3523
3677
  }
3524
3678
  for (const [prefix, uri] of Object.entries(namespaces)) {
@@ -3544,7 +3698,7 @@ function serializeSvg(nodes, opts = {}) {
3544
3698
  said.add(m);
3545
3699
  opts.onWarn(m);
3546
3700
  };
3547
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3701
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn, opts.textLayout)).join("");
3548
3702
  const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3549
3703
  const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3550
3704
  return opts.pretty ? prettyXml(xml) : xml;
@@ -3578,16 +3732,17 @@ function registerMarkers(stroke, registry) {
3578
3732
  if (ref !== void 0) registry.markerId(ref);
3579
3733
  }
3580
3734
  }
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;
3735
+ function nodeXml(node, registry, namespaces, warn, layoutOf) {
3736
+ const xml = kindXml(node, registry, namespaces, warn, layoutOf);
3737
+ if (!node.hidden) return xml;
3738
+ return xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`).replace(/<path (?=[^>]*:role="stroke-outline")/, '<path display="none" ');
3584
3739
  }
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);
3740
+ function kindXml(node, registry, namespaces, warn, layoutOf) {
3741
+ if (node.kind === "group") return groupXml(node, registry, namespaces, warn, layoutOf);
3742
+ if (node.kind === "text") return textXml(node, registry, namespaces, warn, layoutOf?.(node));
3588
3743
  if (node.kind === "image") return imageXml(node, namespaces);
3589
3744
  const drawn = pathAsSvgDraws(node);
3590
- return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3745
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn, layoutOf) : pathXml(drawn, registry, namespaces, warn);
3591
3746
  }
3592
3747
  function imageXml(node, namespaces) {
3593
3748
  const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
@@ -3611,7 +3766,7 @@ function imageXml(node, namespaces) {
3611
3766
  const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3612
3767
  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
3768
  }
3614
- function groupXml(node, registry, namespaces, warn) {
3769
+ function groupXml(node, registry, namespaces, warn, layoutOf) {
3615
3770
  const attrs = [];
3616
3771
  if (node.transform) {
3617
3772
  const m = formatMatrix(node.transform);
@@ -3625,50 +3780,56 @@ function groupXml(node, registry, namespaces, warn) {
3625
3780
  }
3626
3781
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3627
3782
  const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3628
- const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
3783
+ const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn, layoutOf)).join("");
3629
3784
  return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3630
3785
  }
3631
3786
  function pathXml(node, registry, namespaces, warn) {
3632
- const attrs = [`d="${serializePathD(node.path)}"`];
3787
+ const attrs = [`d="${pathToD(node.path)}"`];
3633
3788
  const fillAttrs = paintAttrs(node.fill, "fill", registry);
3634
3789
  for (const a of fillAttrs) attrs.push(a);
3635
3790
  if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3636
3791
  attrs.push(`fill-rule="evenodd"`);
3637
3792
  }
3793
+ const varies = hasVariableWidth(node.stroke);
3638
3794
  if (node.stroke) {
3639
- const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
3640
- for (const a of strokeAttrs) attrs.push(a);
3795
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry, varies ? () => {
3796
+ } : warn);
3797
+ for (const a of varies ? variableWidthAttrs(node.stroke, strokeAttrs) : strokeAttrs) attrs.push(a);
3641
3798
  } else {
3642
3799
  attrs.push('stroke="none"');
3643
3800
  }
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
- }
3801
+ const opacity = node.opacity != null && node.opacity !== 1 ? `opacity="${trimNumber(node.opacity)}"` : null;
3802
+ if (opacity && !varies) attrs.push(opacity);
3803
+ const transform = rotationAttr(node);
3804
+ if (transform) attrs.push(transform);
3654
3805
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3655
3806
  const metaEls = metaElementsXml(node.meta, namespaces);
3656
- if (metaEls) {
3657
- return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
3658
- }
3659
- return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3807
+ const xml = metaEls ? `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>` : `<path ${attrs.join(" ")}${metaAttrs}/>`;
3808
+ if (!varies) return xml;
3809
+ const both = xml + strokeOutlineXml(node.path, node.stroke, strokePaintAsFillAttrs(node.stroke, registry), transform);
3810
+ return opacity ? strokeGroupXml(opacity, both) : both;
3811
+ }
3812
+ function rotationAttr(node) {
3813
+ if (node.rotation == null || node.rotation === 0) return null;
3814
+ const b = pathBounds(node.path);
3815
+ const cx = b.minX + (b.maxX - b.minX) / 2;
3816
+ const cy = b.minY + (b.maxY - b.minY) / 2;
3817
+ const deg = node.rotation * 180 / Math.PI;
3818
+ return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
3660
3819
  }
3661
- function svgNeeds(nodes) {
3820
+ function svgNeeds(nodes, opts = {}) {
3662
3821
  const registry = new PaintServerRegistry();
3663
3822
  registerPaintServers(nodes, registry);
3664
3823
  const fonts = /* @__PURE__ */ new Map();
3824
+ const add = (face) => fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3665
3825
  const walk = (list) => {
3666
3826
  for (const n of list) {
3667
3827
  if (n.kind === "group") walk(n.children);
3668
3828
  else if (n.kind === "text") {
3829
+ if (opts.textLayout) layoutFaces(n).forEach(add);
3669
3830
  for (const run of n.runs ?? []) {
3670
3831
  const face = scriptSizeFace(run, n.style);
3671
- if (face) fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3832
+ if (face) add(face);
3672
3833
  }
3673
3834
  }
3674
3835
  }
@@ -3676,8 +3837,15 @@ function svgNeeds(nodes) {
3676
3837
  walk(nodes);
3677
3838
  return { fonts: [...fonts.values()], paintKinds: registry.paintKinds() };
3678
3839
  }
3679
- function warmSvg(nodes) {
3680
- const needs = svgNeeds(nodes);
3840
+ function layoutFaces(node) {
3841
+ const style = resolveTextStyle(node.style);
3842
+ return toRuns(node.runs && node.runs.length > 0 ? node.runs : node.text).map((run) => {
3843
+ const face = resolveRunFace(run, style);
3844
+ return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3845
+ });
3846
+ }
3847
+ function warmSvg(nodes, opts = {}) {
3848
+ const needs = svgNeeds(nodes, opts);
3681
3849
  return Promise.all([
3682
3850
  warmFonts(needs.fonts),
3683
3851
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
@@ -3730,7 +3898,9 @@ function strokeDataFromSvg(stroke, box) {
3730
3898
  ...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
3731
3899
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3732
3900
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3733
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
3901
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
3902
+ ...stroke.vertexWidths !== void 0 ? { vertexWidths: stroke.vertexWidths } : {},
3903
+ ...stroke.nib !== void 0 ? { nib: stroke.nib } : {}
3734
3904
  };
3735
3905
  }
3736
3906
  function fillInBoxFrame(fill, box) {
@@ -3773,9 +3943,12 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3773
3943
  ...n.hidden ? { hidden: true } : {}
3774
3944
  };
3775
3945
  drafts.push(draft);
3946
+ const at = drafts.length - 1;
3776
3947
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
3777
3948
  if (!acc) {
3778
- drafts.splice(drafts.indexOf(draft), 1);
3949
+ const kept = drafts.length > at + 1 || (options.keepEmpty?.(n) ?? false);
3950
+ if (!kept) drafts.splice(at, 1);
3951
+ else if (n.clip) draft.pose = boxOfPath(n.clip);
3779
3952
  return null;
3780
3953
  }
3781
3954
  draft.pose = n.clip ? boxOfPath(n.clip) : acc;
@@ -3859,7 +4032,7 @@ async function unpackSvgFiles(files, ctx) {
3859
4032
  for (const file of files) {
3860
4033
  try {
3861
4034
  const text = await readFileText(file);
3862
- const parsed = parseSvg(text);
4035
+ const parsed = parseSvg(text, { keepHidden: true });
3863
4036
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
3864
4037
  let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
3865
4038
  if (drafts.length === 0) {
@@ -3904,7 +4077,8 @@ async function unpackSvgFiles(files, ctx) {
3904
4077
  layer,
3905
4078
  pose: place(d.pose),
3906
4079
  data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3907
- parent: d.parentId
4080
+ parent: d.parentId,
4081
+ ...d.hidden ? { hidden: true } : {}
3908
4082
  },
3909
4083
  label: "Insert SVG"
3910
4084
  })
@@ -3939,7 +4113,9 @@ function svgStrokeFromKit(stroke, box) {
3939
4113
  ...stroke.align !== void 0 ? { align: stroke.align } : {},
3940
4114
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3941
4115
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3942
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
4116
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
4117
+ ...stroke.vertexWidths?.length ? { vertexWidths: stroke.vertexWidths } : {},
4118
+ ...stroke.nib ? { nib: stroke.nib } : {}
3943
4119
  };
3944
4120
  }
3945
4121
  function svgImageFromKit(image, pose) {
@@ -3973,22 +4149,12 @@ function runInPoseFrame(run, box) {
3973
4149
  function svgLeafFromKit(data, pose) {
3974
4150
  if (data.image) return svgImageFromKit(data.image, pose);
3975
4151
  if (data.text != null) {
3976
- const node2 = {
3977
- kind: "text",
3978
- x: pose.x,
3979
- y: pose.y,
3980
- width: pose.width,
3981
- height: pose.height,
3982
- text: data.text
3983
- };
3984
- if (data.style) node2.style = data.style;
3985
- if (data.runs) node2.runs = data.runs.map((r) => runInPoseFrame(r, pose));
3986
- if (data.verticalAlign) node2.verticalAlign = data.verticalAlign;
3987
- if (data.fill !== void 0) node2.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
3988
- if (data.stroke) node2.stroke = strokeInPoseFrame(data.stroke, pose);
3989
- if (pose.rotation) node2.rotation = pose.rotation;
3990
- if (pose.transform) node2.transform = pose.transform;
3991
- return node2;
4152
+ const text = svgTextFromKit({ ...data, text: data.text }, pose);
4153
+ const boxPath = { kind: "rect", x: pose.x, y: pose.y, width: pose.width, height: pose.height };
4154
+ const clip = data.overflow === "clip" ? { clip: pathThroughPoseFrame(boxPath, pose) } : {};
4155
+ if (!data.background?.paint) return clip.clip ? { kind: "group", children: [text], ...clip } : text;
4156
+ const box = svgLeafFromKit({ path: boxPath, fill: data.background }, pose);
4157
+ return { kind: "group", children: [box, text], ...clip };
3992
4158
  }
3993
4159
  if (!data.path) return null;
3994
4160
  const stroke = svgStrokeFromKit(data.stroke, pose);
@@ -4007,12 +4173,33 @@ function svgLeafFromKit(data, pose) {
4007
4173
  } else if (pose.rotation) node.rotation = pose.rotation;
4008
4174
  return node;
4009
4175
  }
4176
+ function svgTextFromKit(data, pose) {
4177
+ const pad = Math.max(0, Math.min(data.padding ?? 0, pose.width / 2, pose.height / 2));
4178
+ const node = {
4179
+ kind: "text",
4180
+ x: pose.x + pad,
4181
+ y: pose.y + pad,
4182
+ width: pose.width - 2 * pad,
4183
+ height: pose.height - 2 * pad,
4184
+ text: data.text
4185
+ };
4186
+ if (data.style) node.style = data.style;
4187
+ if (data.runs) node.runs = data.runs.map((r) => runInPoseFrame(r, pose));
4188
+ if (data.verticalAlign) node.verticalAlign = data.verticalAlign;
4189
+ if (data.fill !== void 0) node.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
4190
+ if (data.stroke) node.stroke = strokeInPoseFrame(data.stroke, pose);
4191
+ if (pose.rotation) node.rotation = pose.rotation;
4192
+ if (pose.transform) node.transform = pose.transform;
4193
+ return node;
4194
+ }
4010
4195
  function svgNodesFromKit(tree, options = {}) {
4011
4196
  const { include, leaf, group } = options;
4012
4197
  const emit = (id) => {
4013
- if (include && !include(id)) return null;
4014
4198
  const node = tree.get(id);
4015
- if (!node) return null;
4199
+ const out2 = node && (!include || include(id)) ? lower(id, node) : null;
4200
+ return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
4201
+ };
4202
+ const lower = (id, node) => {
4016
4203
  if (node.kind === "container") {
4017
4204
  const emitChildren = () => {
4018
4205
  const children = [];
@@ -4045,6 +4232,6 @@ function svgNodesFromKit(tree, options = {}) {
4045
4232
  return out;
4046
4233
  }
4047
4234
 
4048
- export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
4235
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, escapeAttr, escapeText, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
4049
4236
  //# sourceMappingURL=index.js.map
4050
4237
  //# sourceMappingURL=index.js.map