@weasel-js/svg 1.10.0 → 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, fillsOpenSubpaths, splitByClosure, splitSubpaths, rotateAboutPoint, hasOpenSubpath } from '@weasel-js/geom';
2
- import { isTileKind, boundsOfPath, transformPath, isSolidPaint, resolveScreenLength, scriptMetricsFor, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, createInsertOp, fillInPoseFrame, DEFAULT_SHAPE_FILL, pathInPoseFrame, poseLinearOf, poseFrameOf, pathThroughPoseFrame, DEFAULT_TEXT_STYLE, pathFromD, isPatternPaint, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, resolveAlign, paragraphDirection, resolveRunFace, resolveTextStyle, parseColor, rgbaToHex, layoutRuns, resolveRuns, PathBuilder, getPaintKind, resolveMarkerSize, isTilePaint, tileSpecOf } from '@weasel-js/core';
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
@@ -1548,9 +1548,9 @@ function warnUnmodeled(el, elements, onWarn) {
1548
1548
  break;
1549
1549
  }
1550
1550
  }
1551
- const spread = inheritedAttr(el, elements, "spreadMethod");
1552
- if (spread === "reflect" || spread === "repeat") {
1553
- 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`);
1554
1554
  }
1555
1555
  }
1556
1556
  function coord(el, elements, name, axis, fraction, units, onWarn) {
@@ -2238,6 +2238,31 @@ function anchorOffset(style, width2, text) {
2238
2238
  }
2239
2239
 
2240
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
+ }
2241
2266
  var DEFAULT_LINE_HEIGHT = 1.2;
2242
2267
  var LineTracker = class {
2243
2268
  constructor(text, onWarn) {
@@ -2250,9 +2275,9 @@ var LineTracker = class {
2250
2275
  /** Where the current line sits, in user units. */
2251
2276
  at;
2252
2277
  pitches = [];
2253
- /** Whether `tspan` opens a new line. Call once per tspan, in order.
2254
- * `afterText` is whether any text precedes it: the first line's own
2255
- * placement is not a pitch. */
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. */
2256
2281
  startsLine(tspan, afterText) {
2257
2282
  const y = tspan.getAttribute("y");
2258
2283
  const movedY = y != null && y !== this.y;
@@ -2260,10 +2285,11 @@ var LineTracker = class {
2260
2285
  const from = this.at;
2261
2286
  const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2262
2287
  this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2263
- if (!tspan.hasAttribute("x")) return false;
2264
- const starts = movedY || dy !== 0;
2265
- if (starts && afterText && this.at > from) this.pitches.push(this.at - from);
2266
- return starts;
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;
2267
2293
  }
2268
2294
  /**
2269
2295
  * The line height the lines were pitched at, as a multiple of `fontSize`:
@@ -2278,7 +2304,7 @@ var LineTracker = class {
2278
2304
  if (pitches.some((p) => Math.abs(p - mean) > 1e-3 * fontSize)) {
2279
2305
  this.onWarn("<text> lines are pitched unevenly; they take the mean pitch as one line height");
2280
2306
  }
2281
- const lineHeight = mean / fontSize;
2307
+ const lineHeight = Math.round(mean / fontSize * 1e6) / 1e6;
2282
2308
  return Math.abs(lineHeight - DEFAULT_LINE_HEIGHT) < 1e-6 ? void 0 : lineHeight;
2283
2309
  }
2284
2310
  };
@@ -2288,35 +2314,88 @@ function warnUnreadPositions(el, onWarn) {
2288
2314
  const raw = el.getAttribute(name)?.trim() ?? "";
2289
2315
  if (/[\s,]/.test(raw)) onWarn(`<${tag} ${name}="${raw}"> positions each glyph; only the first value is read`);
2290
2316
  }
2291
- for (const name of ["dx", "rotate"]) {
2317
+ for (const name of tag === "tspan" ? ["rotate"] : ["dx", "rotate"]) {
2292
2318
  const raw = el.getAttribute(name)?.trim() ?? "";
2293
2319
  if (raw.split(/[\s,]+/).some((v) => parseFloat(v) !== 0 && v !== "")) {
2294
2320
  onWarn(`<${tag} ${name}="${raw}"> is not read; the glyphs keep their own advance and angle`);
2295
2321
  }
2296
2322
  }
2297
2323
  }
2298
- 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) {
2299
2331
  const out = [];
2300
- let breaks = 0;
2332
+ let breaks = leading;
2301
2333
  for (const line of lines) {
2302
2334
  if (line.length > 0) {
2303
- if (breaks > 0) line[0] = { ...line[0], text: "\n".repeat(breaks) + line[0].text };
2304
- 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);
2305
2344
  breaks = 0;
2306
2345
  }
2307
2346
  if (out.length > 0) breaks++;
2308
2347
  }
2309
2348
  return out;
2310
2349
  }
2311
-
2312
- // src/fillPaint.ts
2313
- function mapFillPaint(fill, f) {
2314
- if (!fill.paint) return fill;
2315
- const paint = f(fill.paint);
2316
- return paint === fill.paint ? fill : { ...fill, paint };
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;
2317
2396
  }
2318
2397
 
2319
- // src/readText.ts
2398
+ // src/textWhitespace.ts
2320
2399
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2321
2400
  function preservesSpace(el) {
2322
2401
  for (let e = el; e; e = e.parentElement) {
@@ -2353,6 +2432,15 @@ function collapseRunWhitespace(runs) {
2353
2432
  if (runs[i].text === "") runs.splice(i, 1);
2354
2433
  }
2355
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
2356
2444
  var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
2357
2445
  function laidOutWidth(runs, style, plain) {
2358
2446
  const resolved = resolveTextStyle(style);
@@ -2411,33 +2499,8 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2411
2499
  const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2412
2500
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2413
2501
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2414
- const runLines = [[]];
2415
2502
  const tracker = new LineTracker(el, onWarn);
2416
- const push = (run) => {
2417
- runLines[runLines.length - 1].push(run);
2418
- };
2419
- for (let i = 0; i < el.childNodes.length; i++) {
2420
- const child = el.childNodes[i];
2421
- if (child.nodeType === 3) {
2422
- const t = child.textContent ?? "";
2423
- if (!t) continue;
2424
- push({ text: t, ...inheritedShift });
2425
- } else if (child.nodeType === 1) {
2426
- const sp = child;
2427
- if (sp.tagName.toLowerCase() !== "tspan") {
2428
- onWarn(`<text> child <${sp.tagName}> not supported; flattening text content`);
2429
- const t = sp.textContent ?? "";
2430
- if (t) push({ text: t, ...inheritedShift });
2431
- continue;
2432
- }
2433
- warnUnreadPositions(sp, onWarn);
2434
- const afterText = runLines.some((line) => line.some((r) => r.text.trim() !== ""));
2435
- if (tracker.startsLine(sp, afterText)) runLines.push([]);
2436
- push(readTspanRun(sp, gradients, leafStyle, onWarn));
2437
- }
2438
- }
2439
- if (!preservesSpace(el)) for (const line of runLines) collapseRunWhitespace(line);
2440
- const runs = joinLines(runLines);
2503
+ const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
2441
2504
  const plain = runs.map((r) => r.text).join("");
2442
2505
  const pitched = tracker.lineHeight(fontSize);
2443
2506
  if (pitched !== void 0) textStyle.lineHeight = pitched;
@@ -2450,7 +2513,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2450
2513
  const opacity = ownValue(el, "opacity");
2451
2514
  const node = {
2452
2515
  kind: "text",
2453
- x: ax - anchorOffset(textStyle, width2, plain),
2516
+ x: ax + lead - anchorOffset(textStyle, width2, plain),
2454
2517
  y: topY,
2455
2518
  width: width2,
2456
2519
  height: height2,
@@ -2476,6 +2539,38 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2476
2539
  placeTextNode(node, multiply(ctm, localTransform), onWarn);
2477
2540
  return node;
2478
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
+ }
2479
2574
  function readTspanRun(el, gradients, style, onWarn) {
2480
2575
  const text = el.textContent ?? "";
2481
2576
  const run = { text };
@@ -3132,25 +3227,45 @@ function pathBounds(path) {
3132
3227
 
3133
3228
  // src/openSubpathFill.ts
3134
3229
  var capsEnds = (stroke) => stroke?.markerStart !== void 0 || stroke?.markerEnd !== void 0;
3230
+ var marked = (stroke) => capsEnds(stroke) || stroke?.markerMid !== void 0;
3135
3231
  function withoutEndMarkers(stroke) {
3136
3232
  const { markerStart: _start, markerEnd: _end, ...rest } = stroke;
3137
3233
  return rest;
3138
3234
  }
3139
- function withOpenSubpathsUnfilled(node) {
3140
- const path = node.path;
3141
- if (path.kind !== "polygon" || node.fill.kind === "none" || fillsOpenSubpaths(path)) return node;
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;
3142
3256
  const { closed, open } = splitByClosure(path);
3143
- if (!open) return node;
3144
- const { fill, stroke } = node;
3145
- const opens = capsEnds(stroke) ? splitSubpaths(open) : [open];
3146
- if (!closed && opens.length === 1) return { ...node, fill: { kind: "none" } };
3147
- const piece = (part, paint, line = stroke) => ({ kind: "path", path: part, fill: paint, ...line ? { stroke: line } : {} });
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
+ }
3148
3266
  const group = {
3149
3267
  kind: "group",
3150
- children: [
3151
- ...closed ? [piece(closed, fill, stroke && withoutEndMarkers(stroke))] : [],
3152
- ...opens.map((part) => piece(part, { kind: "none" }))
3153
- ]
3268
+ children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
3154
3269
  };
3155
3270
  if (node.rotation != null && node.rotation !== 0) {
3156
3271
  const b = pathBounds(path);
@@ -3161,6 +3276,200 @@ function withOpenSubpathsUnfilled(node) {
3161
3276
  if (node.meta) group.meta = node.meta;
3162
3277
  return group;
3163
3278
  }
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)];
3283
+ }
3284
+ function warnStrokeAlign(align, warn) {
3285
+ if (align && align !== "center") {
3286
+ warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
3287
+ }
3288
+ }
3289
+ function paintAttrs(paint, name, registry, includeOpacity = true) {
3290
+ if (paint.kind === "none") return [`${name}="none"`];
3291
+ const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
3292
+ if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
3293
+ return out;
3294
+ }
3295
+ function svgPaintOpacity(paint) {
3296
+ if (paint.kind === "solid") return paint.opacity;
3297
+ return paint.kind === "gradient" ? paint.paint.opacity : void 0;
3298
+ }
3299
+ function opacityAttr(name, v) {
3300
+ return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
3301
+ }
3302
+ function strokeWidthAttrs(width2) {
3303
+ if (typeof width2 === "number") return [`stroke-width="${trimNumber(width2)}"`];
3304
+ return [`stroke-width="${trimNumber(width2.px)}"`, 'vector-effect="non-scaling-stroke"'];
3305
+ }
3306
+ function coreStrokeAttrs(stroke, registry, warn) {
3307
+ const paint = stroke?.paint;
3308
+ if (!stroke || !paint) return [];
3309
+ const width2 = stroke.width ?? 1;
3310
+ if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
3311
+ warnStrokeAlign(stroke.align, warn);
3312
+ const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3313
+ attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
3314
+ attrs.push(...strokeWidthAttrs(width2));
3315
+ if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
3316
+ if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
3317
+ if (stroke.dash && stroke.dash.length > 0) {
3318
+ attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
3319
+ }
3320
+ if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
3321
+ for (const [field, attr] of [
3322
+ ["markerStart", "marker-start"],
3323
+ ["markerMid", "marker-mid"],
3324
+ ["markerEnd", "marker-end"]
3325
+ ]) {
3326
+ const ref = stroke[field];
3327
+ if (ref === void 0) continue;
3328
+ const id = registry.markerId(ref);
3329
+ if (id === void 0) continue;
3330
+ attrs.push(`${attr}="url(#${id})"`);
3331
+ }
3332
+ return attrs;
3333
+ }
3334
+ function strokeAttrsFor(stroke, registry, warn) {
3335
+ warnStrokeAlign(stroke.align, warn);
3336
+ const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
3337
+ attrs.push(...strokeWidthAttrs(stroke.width));
3338
+ attrs.push(...opacityAttr("stroke-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint)));
3339
+ if (stroke.cap) {
3340
+ attrs.push(`stroke-linecap="${stroke.cap}"`);
3341
+ }
3342
+ if (stroke.join) {
3343
+ attrs.push(`stroke-linejoin="${stroke.join}"`);
3344
+ }
3345
+ if (stroke.dash && stroke.dash.length > 0) {
3346
+ attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
3347
+ }
3348
+ if (stroke.miterLimit != null) {
3349
+ attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
3350
+ }
3351
+ for (const [field, attr] of [
3352
+ ["markerStart", "marker-start"],
3353
+ ["markerMid", "marker-mid"],
3354
+ ["markerEnd", "marker-end"]
3355
+ ]) {
3356
+ const ref = stroke[field];
3357
+ if (ref === void 0) continue;
3358
+ const id = registry.markerId(ref);
3359
+ if (id === void 0) continue;
3360
+ attrs.push(`${attr}="url(#${id})"`);
3361
+ }
3362
+ return attrs;
3363
+ }
3364
+ function textXml(node, registry, namespaces, warn) {
3365
+ const x = node.x + anchorOffset(node.style, node.width, node.text);
3366
+ const attrs = [
3367
+ `x="${trimNumber(x)}"`,
3368
+ `y="${trimNumber(node.y)}"`,
3369
+ `dominant-baseline="text-before-edge"`,
3370
+ // SVG's default whitespace handling would collapse indentation and runs
3371
+ // of spaces away on re-import.
3372
+ `xml:space="preserve"`,
3373
+ `data-weasel-width="${trimNumber(node.width)}"`,
3374
+ `data-weasel-height="${trimNumber(node.height)}"`
3375
+ ];
3376
+ const style = node.style;
3377
+ if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
3378
+ if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
3379
+ if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
3380
+ if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
3381
+ if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
3382
+ const direction = svgDirection(style, node.text);
3383
+ if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
3384
+ if (style?.align != null || direction === "rtl") {
3385
+ 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";
3387
+ if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
3388
+ if (align === "justify") attrs.push('data-weasel-align="justify"');
3389
+ }
3390
+ if (style?.letterSpacing != null && style.letterSpacing !== 0) {
3391
+ attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
3392
+ }
3393
+ const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
3394
+ if (decoration) attrs.push(`text-decoration="${decoration}"`);
3395
+ if (style?.script) attrs.push(`baseline-shift="${style.script}"`);
3396
+ if (style?.textTransform && style.textTransform !== "none") {
3397
+ attrs.push(`style="text-transform:${style.textTransform}"`);
3398
+ }
3399
+ if (style?.wrap) {
3400
+ 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)");
3402
+ }
3403
+ if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
3404
+ if (node.verticalAlign && node.verticalAlign !== "top") {
3405
+ 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)`);
3407
+ }
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
+ }
3415
+ }
3416
+ for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
3417
+ if (node.opacity != null && node.opacity !== 1) {
3418
+ attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3419
+ }
3420
+ const frame = boxTransformAttr(node);
3421
+ if (frame) attrs.push(frame);
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);
3425
+ const metaAttrs = metaAttrsXml(node.meta, namespaces);
3426
+ const metaEls = metaElementsXml(node.meta, namespaces);
3427
+ return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
3428
+ }
3429
+ function scriptSizeFace(run, style) {
3430
+ if (run.script == null || run.baselineShift == null) return void 0;
3431
+ const face = resolveRunFace(run, resolveTextStyle(style));
3432
+ return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3433
+ }
3434
+ function runAttrs(run, style, registry, warn) {
3435
+ const attrs = [];
3436
+ if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
3437
+ else if (run.bold) attrs.push(`font-weight="700"`);
3438
+ if (run.italic) attrs.push(`font-style="italic"`);
3439
+ if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
3440
+ if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
3441
+ const face = scriptSizeFace(run, style);
3442
+ const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
3443
+ const scale = run.fontScale ?? presetScale;
3444
+ if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
3445
+ else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
3446
+ if (run.baselineShift != null) {
3447
+ attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
3448
+ } else if (run.script) {
3449
+ attrs.push(`baseline-shift="${run.script}"`);
3450
+ }
3451
+ if (run.letterSpacing != null) {
3452
+ attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
3453
+ }
3454
+ const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3455
+ if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
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)}"`);
3461
+ }
3462
+ for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3463
+ if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
3464
+ return attrs;
3465
+ }
3466
+ function textDecorationValue(underline, strikethrough, overline) {
3467
+ const tokens = [];
3468
+ if (underline) tokens.push("underline");
3469
+ if (strikethrough) tokens.push("line-through");
3470
+ if (overline) tokens.push("overline");
3471
+ return tokens.length > 0 ? tokens.join(" ") : null;
3472
+ }
3164
3473
 
3165
3474
  // src/prettyXml.ts
3166
3475
  var TOKEN2 = /<(?:[^>"]|"[^"]*")*>|[^<]+/g;
@@ -3277,14 +3586,9 @@ function kindXml(node, registry, namespaces, warn) {
3277
3586
  if (node.kind === "group") return groupXml(node, registry, namespaces, warn);
3278
3587
  if (node.kind === "text") return textXml(node, registry, namespaces, warn);
3279
3588
  if (node.kind === "image") return imageXml(node, namespaces);
3280
- const drawn = withOpenSubpathsUnfilled(node);
3589
+ const drawn = pathAsSvgDraws(node);
3281
3590
  return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3282
3591
  }
3283
- function warnStrokeAlign(align, warn) {
3284
- if (align && align !== "center") {
3285
- warn(`a stroke aligned "${align}" of its edge is written centered: SVG has no stroke alignment`);
3286
- }
3287
- }
3288
3592
  function imageXml(node, namespaces) {
3289
3593
  const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
3290
3594
  const outer = [];
@@ -3354,188 +3658,6 @@ function pathXml(node, registry, namespaces, warn) {
3354
3658
  }
3355
3659
  return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3356
3660
  }
3357
- function paintAttrs(paint, name, registry, includeOpacity = true) {
3358
- if (paint.kind === "none") return [`${name}="none"`];
3359
- const out = [`${name}="${paint.kind === "solid" ? escapeAttr(paint.color) : registry.ref(paint.paint)}"`];
3360
- if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
3361
- return out;
3362
- }
3363
- function svgPaintOpacity(paint) {
3364
- if (paint.kind === "solid") return paint.opacity;
3365
- return paint.kind === "gradient" ? paint.paint.opacity : void 0;
3366
- }
3367
- function opacityAttr(name, v) {
3368
- return v != null && v !== 1 ? [`${name}="${trimNumber(v)}"`] : [];
3369
- }
3370
- function strokeWidthAttrs(width2) {
3371
- if (typeof width2 === "number") return [`stroke-width="${trimNumber(width2)}"`];
3372
- return [`stroke-width="${trimNumber(width2.px)}"`, 'vector-effect="non-scaling-stroke"'];
3373
- }
3374
- function coreStrokeAttrs(stroke, registry, warn) {
3375
- const paint = stroke?.paint;
3376
- if (!stroke || !paint) return [];
3377
- const width2 = stroke.width ?? 1;
3378
- if (!((typeof width2 === "object" ? width2.px : width2) > 0)) return [];
3379
- warnStrokeAlign(stroke.align, warn);
3380
- const attrs = [`stroke="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`];
3381
- attrs.push(...opacityAttr("stroke-opacity", paint.opacity));
3382
- attrs.push(...strokeWidthAttrs(width2));
3383
- if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
3384
- if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
3385
- if (stroke.dash && stroke.dash.length > 0) {
3386
- attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
3387
- }
3388
- if (stroke.miterLimit != null) attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
3389
- for (const [field, attr] of [
3390
- ["markerStart", "marker-start"],
3391
- ["markerMid", "marker-mid"],
3392
- ["markerEnd", "marker-end"]
3393
- ]) {
3394
- const ref = stroke[field];
3395
- if (ref === void 0) continue;
3396
- const id = registry.markerId(ref);
3397
- if (id === void 0) continue;
3398
- attrs.push(`${attr}="url(#${id})"`);
3399
- }
3400
- return attrs;
3401
- }
3402
- function strokeAttrsFor(stroke, registry, warn) {
3403
- warnStrokeAlign(stroke.align, warn);
3404
- const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
3405
- attrs.push(...strokeWidthAttrs(stroke.width));
3406
- attrs.push(...opacityAttr("stroke-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint)));
3407
- if (stroke.cap) {
3408
- attrs.push(`stroke-linecap="${stroke.cap}"`);
3409
- }
3410
- if (stroke.join) {
3411
- attrs.push(`stroke-linejoin="${stroke.join}"`);
3412
- }
3413
- if (stroke.dash && stroke.dash.length > 0) {
3414
- attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
3415
- }
3416
- if (stroke.miterLimit != null) {
3417
- attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
3418
- }
3419
- for (const [field, attr] of [
3420
- ["markerStart", "marker-start"],
3421
- ["markerMid", "marker-mid"],
3422
- ["markerEnd", "marker-end"]
3423
- ]) {
3424
- const ref = stroke[field];
3425
- if (ref === void 0) continue;
3426
- const id = registry.markerId(ref);
3427
- if (id === void 0) continue;
3428
- attrs.push(`${attr}="url(#${id})"`);
3429
- }
3430
- return attrs;
3431
- }
3432
- function textXml(node, registry, namespaces, warn) {
3433
- const attrs = [
3434
- `x="${trimNumber(node.x + anchorOffset(node.style, node.width, node.text))}"`,
3435
- `y="${trimNumber(node.y)}"`,
3436
- `dominant-baseline="text-before-edge"`,
3437
- // The runs model stores real line breaks; SVG's default whitespace
3438
- // handling would collapse them (and any indentation) away on re-import.
3439
- `xml:space="preserve"`,
3440
- `data-weasel-width="${trimNumber(node.width)}"`,
3441
- `data-weasel-height="${trimNumber(node.height)}"`
3442
- ];
3443
- const style = node.style;
3444
- if (style?.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(style.fontSize, 1))}"`);
3445
- if (style?.fontFamily) attrs.push(`font-family="${escapeAttr(style.fontFamily)}"`);
3446
- if (style?.fontWeight != null) attrs.push(`font-weight="${escapeAttr(String(style.fontWeight))}"`);
3447
- if (style?.fontStyle && style.fontStyle !== "normal") attrs.push(`font-style="${style.fontStyle}"`);
3448
- if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
3449
- const direction = svgDirection(style, node.text);
3450
- if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
3451
- if (style?.align != null || direction === "rtl") {
3452
- const align = style?.align ?? "left";
3453
- const anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3454
- if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
3455
- if (align === "justify") attrs.push('data-weasel-align="justify"');
3456
- }
3457
- if (style?.letterSpacing != null && style.letterSpacing !== 0) {
3458
- attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(style.letterSpacing, 1))}"`);
3459
- }
3460
- const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
3461
- if (decoration) attrs.push(`text-decoration="${decoration}"`);
3462
- if (style?.script) attrs.push(`baseline-shift="${style.script}"`);
3463
- if (style?.textTransform && style.textTransform !== "none") {
3464
- attrs.push(`style="text-transform:${style.textTransform}"`);
3465
- }
3466
- if (style?.wrap) {
3467
- attrs.push('data-weasel-wrap="true"');
3468
- 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)");
3469
- }
3470
- if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
3471
- if (node.verticalAlign && node.verticalAlign !== "top") {
3472
- attrs.push(`data-weasel-vertical-align="${node.verticalAlign}"`);
3473
- warn(`text aligned to the ${node.verticalAlign} of its box is written top-aligned: SVG text has no box, so other readers draw it at the top (weasel reads the alignment back)`);
3474
- }
3475
- if (node.fill !== void 0) {
3476
- const paint = node.fill?.paint;
3477
- if (!paint) attrs.push('fill="none"');
3478
- else {
3479
- attrs.push(`fill="${isSolidPaint(paint) ? escapeAttr(paint.color) : registry.ref(paint)}"`);
3480
- attrs.push(...opacityAttr("fill-opacity", paint.opacity));
3481
- }
3482
- }
3483
- for (const a of coreStrokeAttrs(node.stroke, registry, warn)) attrs.push(a);
3484
- if (node.opacity != null && node.opacity !== 1) {
3485
- attrs.push(`opacity="${trimNumber(node.opacity)}"`);
3486
- }
3487
- const frame = boxTransformAttr(node);
3488
- if (frame) attrs.push(frame);
3489
- const body = node.runs && node.runs.length > 0 ? node.runs.map((r) => runXml(r, style, registry, warn)).join("") : escapeText(node.text);
3490
- const metaAttrs = metaAttrsXml(node.meta, namespaces);
3491
- const metaEls = metaElementsXml(node.meta, namespaces);
3492
- return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
3493
- }
3494
- function scriptSizeFace(run, style) {
3495
- if (run.script == null || run.baselineShift == null) return void 0;
3496
- const face = resolveRunFace(run, resolveTextStyle(style));
3497
- return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3498
- }
3499
- function runXml(run, style, registry, warn) {
3500
- const attrs = [];
3501
- if (run.fontWeight != null) attrs.push(`font-weight="${run.fontWeight}"`);
3502
- else if (run.bold) attrs.push(`font-weight="700"`);
3503
- if (run.italic) attrs.push(`font-style="italic"`);
3504
- if (run.fontVariantCaps) attrs.push(`font-variant="${run.fontVariantCaps}"`);
3505
- if (run.fontFamily) attrs.push(`font-family="${escapeAttr(run.fontFamily)}"`);
3506
- const face = scriptSizeFace(run, style);
3507
- const presetScale = face && run.script ? scriptMetricsFor(face.family, face.weight, face.style)[run.script].size : void 0;
3508
- const scale = run.fontScale ?? presetScale;
3509
- if (run.fontSize != null) attrs.push(`font-size="${trimNumber(resolveScreenLength(run.fontSize, 1))}"`);
3510
- else if (scale != null) attrs.push(`font-size="${trimNumber(scale * 100)}%"`);
3511
- if (run.baselineShift != null) {
3512
- attrs.push(`baseline-shift="${trimNumber(run.baselineShift * 100)}%"`);
3513
- } else if (run.script) {
3514
- attrs.push(`baseline-shift="${run.script}"`);
3515
- }
3516
- if (run.letterSpacing != null) {
3517
- attrs.push(`letter-spacing="${trimNumber(resolveScreenLength(run.letterSpacing, 1))}"`);
3518
- }
3519
- const runDecoration = textDecorationValue(run.underline, run.strikethrough, run.overline);
3520
- if (runDecoration) attrs.push(`text-decoration="${runDecoration}"`);
3521
- if (run.fill !== void 0) {
3522
- const paint = run.fill?.paint;
3523
- if (!paint) attrs.push('fill="none"');
3524
- else if (isSolidPaint(paint)) attrs.push(`fill="${escapeAttr(paint.color)}"`);
3525
- else attrs.push(`fill="${registry.ref(paint)}"`);
3526
- }
3527
- for (const a of coreStrokeAttrs(run.stroke, registry, warn)) attrs.push(a);
3528
- if (run.textTransform) attrs.push(`style="text-transform:${run.textTransform}"`);
3529
- const head = attrs.length > 0 ? `<tspan ${attrs.join(" ")}>` : "<tspan>";
3530
- return `${head}${escapeText(run.text)}</tspan>`;
3531
- }
3532
- function textDecorationValue(underline, strikethrough, overline) {
3533
- const tokens = [];
3534
- if (underline) tokens.push("underline");
3535
- if (strikethrough) tokens.push("line-through");
3536
- if (overline) tokens.push("overline");
3537
- return tokens.length > 0 ? tokens.join(" ") : null;
3538
- }
3539
3661
  function svgNeeds(nodes) {
3540
3662
  const registry = new PaintServerRegistry();
3541
3663
  registerPaintServers(nodes, registry);
@@ -3892,14 +4014,25 @@ function svgNodesFromKit(tree, options = {}) {
3892
4014
  const node = tree.get(id);
3893
4015
  if (!node) return null;
3894
4016
  if (node.kind === "container") {
3895
- const children = [];
3896
- for (const c of tree.childrenOf(id)) {
3897
- const child = emit(c);
3898
- 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
+ };
3899
4035
  }
3900
- const g = { kind: "group", children };
3901
- const clip = node.data?.path;
3902
- if (clip) g.clip = pathThroughPoseFrame(pathInPoseFrame(clip, node.pose), node.pose);
3903
4036
  return group ? group(id, g) : g;
3904
4037
  }
3905
4038
  return leaf ? leaf(id, node) : svgLeafFromKit(node.data, node.pose);