@weasel-js/svg 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
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';
2
+ import { resolveCorners, PATH_M, PATH_L, PATH_C, PATH_Q, PATH_Z, 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, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
4
4
 
5
5
  // src/types.ts
@@ -177,9 +177,11 @@ function scaleLength(v, scale) {
177
177
  return typeof v === "number" ? v * scale : v;
178
178
  }
179
179
  function scaleStroke(stroke, scale) {
180
- const { width: width2 } = stroke;
181
- if (scale === 1 || typeof width2 === "object") return stroke;
182
- const out = { ...stroke };
180
+ const { width: width2, vertexWidths } = stroke;
181
+ if (scale === 1) return stroke;
182
+ const scaledVertices = vertexWidths ? { vertexWidths: vertexWidths.map((n) => scaleVertexWidth(n, scale)) } : {};
183
+ if (typeof width2 === "object") return vertexWidths ? { ...stroke, ...scaledVertices } : stroke;
184
+ const out = { ...stroke, ...scaledVertices };
183
185
  if (width2 !== void 0) out.width = width2 * scale;
184
186
  if (out.dash) out.dash = out.dash.map((n) => n * scale);
185
187
  return out;
@@ -1381,7 +1383,8 @@ function readPattern(el, onWarn) {
1381
1383
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) paint.transform = [m[0], m[1], m[2], m[3]];
1382
1384
  return paint;
1383
1385
  }
1384
- function serializePathD(path) {
1386
+ function serializePathD(given) {
1387
+ const path = resolveCorners(given);
1385
1388
  if (path.kind === "rect") {
1386
1389
  const { x, y, width: width2, height: height2 } = path;
1387
1390
  return `M${trimNumber(x)} ${trimNumber(y)}h${trimNumber(width2)}v${trimNumber(height2)}h${trimNumber(-width2)}Z`;
@@ -2096,6 +2099,97 @@ function coreStroke(stroke) {
2096
2099
  if (stroke.markerEnd) out.markerEnd = stroke.markerEnd;
2097
2100
  return out;
2098
2101
  }
2102
+ var VERTEX_WIDTHS = "vertex-widths";
2103
+ var NIB = "nib";
2104
+ var WIDTH = "stroke-width";
2105
+ var ROLE = "role";
2106
+ var OUTLINE = "stroke-outline";
2107
+ var GROUP = "stroke-group";
2108
+ var MARKER_ATTR = /^(marker-(?:start|mid|end))=/;
2109
+ var UNSET = "-";
2110
+ var SIDES = "/";
2111
+ function attrOf(el, name) {
2112
+ return el.getAttributeNS(WEASEL_NS, name) ?? el.getAttribute(`${WEASEL_NS_PREFIX}:${name}`);
2113
+ }
2114
+ function hasVariableWidth(stroke) {
2115
+ return (stroke?.vertexWidths?.length ?? 0) > 0 || stroke?.nib !== void 0;
2116
+ }
2117
+ function writesVariableWidth(nodes) {
2118
+ return nodes.some((n) => n.kind === "group" ? writesVariableWidth(n.children) : n.kind === "path" && hasVariableWidth(n.stroke));
2119
+ }
2120
+ function widthToken(entry) {
2121
+ const { left, right } = vertexHalfWidths(entry, NaN);
2122
+ const side = (v) => Number.isNaN(v) ? UNSET : trimNumber(v);
2123
+ if (typeof entry === "number") return Number.isNaN(left) ? UNSET : trimNumber(left + right);
2124
+ return Number.isNaN(left) && Number.isNaN(right) ? UNSET : `${side(left)}${SIDES}${side(right)}`;
2125
+ }
2126
+ function widthOfToken(token) {
2127
+ const side = (v) => v === UNSET ? NaN : Number(v);
2128
+ if (!token.includes(SIDES)) return side(token);
2129
+ const [left, right] = token.split(SIDES);
2130
+ return { left: side(left), right: side(right) };
2131
+ }
2132
+ function variableWidthAttrs(stroke, strokeAttrs) {
2133
+ const width2 = typeof stroke.width === "number" ? stroke.width : stroke.width.px;
2134
+ const out = [
2135
+ ...strokeAttrs.map((a) => a.startsWith("stroke-width=") ? 'stroke-width="0"' : MARKER_ATTR.test(a) ? `${WEASEL_NS_PREFIX}:${a}` : a),
2136
+ `${WEASEL_NS_PREFIX}:${WIDTH}="${trimNumber(width2)}"`
2137
+ ];
2138
+ if (stroke.vertexWidths?.length) {
2139
+ const list = stroke.vertexWidths.map(widthToken).join(" ");
2140
+ out.push(`${WEASEL_NS_PREFIX}:${VERTEX_WIDTHS}="${list}"`);
2141
+ }
2142
+ if (stroke.nib) {
2143
+ out.push(`${WEASEL_NS_PREFIX}:${NIB}="${String(stroke.nib.angle)} ${String(stroke.nib.ratio)}"`);
2144
+ }
2145
+ return out;
2146
+ }
2147
+ function strokeOutlineXml(path, stroke, fillAttrs, transform) {
2148
+ const kit = {
2149
+ width: stroke.width,
2150
+ ...stroke.vertexWidths ? { vertexWidths: stroke.vertexWidths } : {},
2151
+ ...stroke.nib ? { nib: stroke.nib } : {},
2152
+ ...stroke.cap ? { cap: stroke.cap } : {},
2153
+ ...stroke.join ? { join: stroke.join } : {},
2154
+ ...stroke.dash ? { dash: stroke.dash } : {},
2155
+ ...stroke.miterLimit != null ? { miterLimit: stroke.miterLimit } : {},
2156
+ ...stroke.align ? { align: stroke.align } : {},
2157
+ ...stroke.markerStart ? { markerStart: stroke.markerStart } : {},
2158
+ ...stroke.markerMid ? { markerMid: stroke.markerMid } : {},
2159
+ ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
2160
+ };
2161
+ const outline = strokeToPath(path, kit);
2162
+ const attrs = [`d="${serializePathD(outline)}"`, ...fillAttrs, 'stroke="none"'];
2163
+ if (transform) attrs.push(transform);
2164
+ attrs.push(`${WEASEL_NS_PREFIX}:${ROLE}="${OUTLINE}"`);
2165
+ return `<path ${attrs.join(" ")}/>`;
2166
+ }
2167
+ function strokeGroupXml(opacity, inner) {
2168
+ return `<g ${opacity} ${WEASEL_NS_PREFIX}:${ROLE}="${GROUP}">${inner}</g>`;
2169
+ }
2170
+ function unwrapStrokeGroup(el, node) {
2171
+ if (attrOf(el, ROLE) !== GROUP || !node || Array.isArray(node) || node.kind !== "group") return node;
2172
+ const [only] = node.children;
2173
+ if (node.children.length !== 1 || only.kind !== "path") return node;
2174
+ return node.opacity != null ? { ...only, opacity: node.opacity * (only.opacity ?? 1) } : only;
2175
+ }
2176
+ function isStrokeOutline(el) {
2177
+ return attrOf(el, ROLE) === OUTLINE;
2178
+ }
2179
+ function withVariableWidth(el, stroke) {
2180
+ const width2 = parseFloat(attrOf(el, WIDTH) ?? "");
2181
+ if (!stroke || !Number.isFinite(width2)) return stroke;
2182
+ const out = { ...stroke, width: typeof stroke.width === "number" ? width2 : { px: width2 } };
2183
+ const list = attrOf(el, VERTEX_WIDTHS);
2184
+ if (list) out.vertexWidths = list.trim().split(/\s+/).map(widthOfToken);
2185
+ const [angle, ratio] = (attrOf(el, NIB) ?? "").trim().split(/\s+/).map(Number);
2186
+ if (Number.isFinite(angle) && Number.isFinite(ratio)) out.nib = { angle, ratio };
2187
+ for (const [attr, field] of [["marker-start", "markerStart"], ["marker-mid", "markerMid"], ["marker-end", "markerEnd"]]) {
2188
+ const id = /^url\(\s*#([^)\s]+)\s*\)$/.exec(attrOf(el, attr)?.trim() ?? "")?.[1];
2189
+ if (id) out[field] = id;
2190
+ }
2191
+ return out;
2192
+ }
2099
2193
  var near = (v, target) => Math.abs(v - target) <= 1e-9;
2100
2194
  function placeBox(box, m, stretch) {
2101
2195
  const [a, b, c, d] = m;
@@ -2239,6 +2333,7 @@ function anchorOffset(style, width2, text) {
2239
2333
 
2240
2334
  // src/tspanLines.ts
2241
2335
  var LINES_ATTR = "data-weasel-lines";
2336
+ var LINE_HEIGHT_ATTR = "data-weasel-line-height";
2242
2337
  var statesLines = (tspan) => tspan.hasAttribute(LINES_ATTR);
2243
2338
  function linesXml(pieces, x, pitch) {
2244
2339
  let out = "";
@@ -2286,9 +2381,9 @@ var LineTracker = class {
2286
2381
  const dy = readLength(tspan, "dy", "y", 0, this.onWarn);
2287
2382
  this.at = (y != null ? readLength(tspan, "y", "y", from, this.onWarn) : from) + dy;
2288
2383
  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);
2384
+ const stated = Number(tspan.getAttribute(LINES_ATTR) ?? NaN);
2385
+ const lines = Number.isInteger(stated) && stated >= 0 ? stated : 1;
2386
+ if (afterText && lines > 0 && this.at > from) this.pitches.push((this.at - from) / lines);
2292
2387
  return lines;
2293
2388
  }
2294
2389
  /**
@@ -2347,6 +2442,76 @@ function joinLines(lines, leading = 0) {
2347
2442
  }
2348
2443
  return out;
2349
2444
  }
2445
+
2446
+ // src/laidLines.ts
2447
+ var LAID_ATTR = "data-weasel-layout";
2448
+ function layoutMeasured(layout, text) {
2449
+ return layout.laid.bounds.width > 0 || text.trim() === "";
2450
+ }
2451
+ function anchorX(cells, { anchor, rtl }) {
2452
+ const left = Math.min(...cells.map((c) => c.x));
2453
+ const right = Math.max(...cells.map((c) => c.x + c.advance));
2454
+ if (anchor === "middle") return (left + right) / 2;
2455
+ return anchor === "start" === !rtl ? left : right;
2456
+ }
2457
+ function wordStarts(text, start, end) {
2458
+ const out = [start];
2459
+ for (let i = start + 1; i < end; i++) {
2460
+ if (!/\s/.test(text[i]) && /\s/.test(text[i - 1])) out.push(i);
2461
+ }
2462
+ return out;
2463
+ }
2464
+ function rangeXml(pieces, offsets, start, end, lead) {
2465
+ let out = "";
2466
+ let first = true;
2467
+ pieces.forEach((piece, k) => {
2468
+ const from = Math.max(start, offsets[k]);
2469
+ const to = Math.min(end, offsets[k] + piece.text.length);
2470
+ if (from >= to) return;
2471
+ const text = piece.text.slice(from - offsets[k], to - offsets[k]);
2472
+ const attrs = [...piece.attrs ?? [], ...first ? lead : []];
2473
+ first = false;
2474
+ out += piece.attrs === null && attrs.length === 0 ? escapeText(text) : `<tspan${attrs.map((a) => ` ${a}`).join("")}>${escapeText(text)}</tspan>`;
2475
+ });
2476
+ return out;
2477
+ }
2478
+ var HARD_BREAKS = /\r\n|[\n\v\f\r\u0085\u2028\u2029]/g;
2479
+ var breaksIn = (text) => text.match(HARD_BREAKS)?.length ?? 0;
2480
+ function laidLinesXml(pieces, layout, anchor) {
2481
+ const text = pieces.map((p) => p.text).join("");
2482
+ const offsets = [];
2483
+ pieces.reduce((at, p) => {
2484
+ offsets.push(at);
2485
+ return at + p.text.length;
2486
+ }, 0);
2487
+ const px = (x) => `x="${trimNumber(layout.x + x)}"`;
2488
+ const py = (line) => `y="${trimNumber(layout.y + line.baselineY)}"`;
2489
+ let out = "";
2490
+ let wrote = false;
2491
+ let prevEnd = 0;
2492
+ const count = (lines) => {
2493
+ const stated = lines !== 1 || !wrote ? [`${LINES_ATTR}="${lines}"`] : [];
2494
+ wrote = true;
2495
+ return stated;
2496
+ };
2497
+ for (const line of layout.laid.lines) {
2498
+ if (line.cells.length === 0) continue;
2499
+ const start = Math.min(...line.cells.map((c) => c.srcIndex));
2500
+ const stepped = breaksIn(text.slice(prevEnd, start));
2501
+ prevEnd = line.srcEnd;
2502
+ const starts = line.justified ? wordStarts(text, start, line.srcEnd) : [start];
2503
+ starts.forEach((from, k) => {
2504
+ const to = starts[k + 1] ?? line.srcEnd;
2505
+ const cells = line.cells.filter((c) => c.srcIndex >= from && c.srcIndex < to);
2506
+ const lead = [px(anchorX(cells, anchor)), ...k === 0 ? [py(line), ...count(stepped)] : []];
2507
+ out += rangeXml(pieces, offsets, from, to, lead);
2508
+ });
2509
+ }
2510
+ const trailing = breaksIn(text.slice(prevEnd));
2511
+ const last = layout.laid.lines[layout.laid.lines.length - 1];
2512
+ if (trailing > 0 && last) out += `<tspan ${[px(last.x0), py(last), ...count(trailing)].join(" ")}></tspan>`;
2513
+ return out;
2514
+ }
2350
2515
  var COLLAPSIBLE = /^\s|\s$|\s\s|[\n\t\r]/;
2351
2516
  function readDx(el, onWarn) {
2352
2517
  const raw = el.getAttribute("dx")?.trim();
@@ -2455,6 +2620,10 @@ function laidOutWidth(runs, style, plain) {
2455
2620
  const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
2456
2621
  return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
2457
2622
  }
2623
+ function numberAttr(el, name) {
2624
+ const n = parseFloat(el.getAttribute(name) ?? "");
2625
+ return Number.isFinite(n) ? n : void 0;
2626
+ }
2458
2627
  function placeTextNode(node, m, onWarn) {
2459
2628
  const cx = node.x + node.width / 2;
2460
2629
  const cy = node.y + node.height / 2;
@@ -2496,13 +2665,14 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
2496
2665
  const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
2497
2666
  const fontSize = resolveScreenLength(textStyle.fontSize ?? 16, 1);
2498
2667
  const dominantBaseline = el.getAttribute("dominant-baseline");
2499
- const explicitTopAnchor = dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2668
+ const laid = el.hasAttribute(LAID_ATTR);
2669
+ const explicitTopAnchor = laid || dominantBaseline === "text-before-edge" || dominantBaseline === "hanging";
2500
2670
  const topY = explicitTopAnchor ? ay : ay - fontSize;
2501
2671
  const inheritedShift = ownValue(el, "baseline-shift", onWarn) ?? {};
2502
2672
  const tracker = new LineTracker(el, onWarn);
2503
2673
  const { runs, lead } = readRuns(el, tracker, inheritedShift, textStyle.letterSpacing, gradients, leafStyle, onWarn);
2504
2674
  const plain = runs.map((r) => r.text).join("");
2505
- const pitched = tracker.lineHeight(fontSize);
2675
+ const pitched = laid ? numberAttr(el, LINE_HEIGHT_ATTR) : tracker.lineHeight(fontSize);
2506
2676
  if (pitched !== void 0) textStyle.lineHeight = pitched;
2507
2677
  const lineHeight = textStyle.lineHeight ?? DEFAULT_LINE_HEIGHT;
2508
2678
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
@@ -3017,10 +3187,10 @@ function parseChildren(parent, ctm, style, gradients, clips, onWarn, uriToPrefix
3017
3187
  for (let i = 0; i < parent.children.length; i++) {
3018
3188
  const el = parent.children[i];
3019
3189
  const ns = el.namespaceURI;
3020
- if (ns && ns !== "http://www.w3.org/2000/svg") continue;
3190
+ if (ns && ns !== "http://www.w3.org/2000/svg" || isStrokeOutline(el)) continue;
3021
3191
  const hidden = isHiddenElement(el, style);
3022
3192
  if (hidden && !keepHidden) continue;
3023
- const node = parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden);
3193
+ const node = unwrapStrokeGroup(el, parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, keepHidden));
3024
3194
  if (!node) continue;
3025
3195
  const parsed = Array.isArray(node) ? node : [node];
3026
3196
  if (hidden) for (const n of parsed) n.hidden = true;
@@ -3113,7 +3283,7 @@ function parseElement(el, ctm, style, gradients, clips, onWarn, uriToPrefix, kee
3113
3283
  }
3114
3284
  const leafStyle = deriveStyle(style, el);
3115
3285
  const fill = paintThroughMatrix(readPaint2(leafStyle, "fill", "#000000", gradients, onWarn), baked);
3116
- const ownStroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
3286
+ const ownStroke = withVariableWidth(el, readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el)));
3117
3287
  const stroke = ownStroke && strokeThroughMatrix(ownStroke, baked);
3118
3288
  if (stroke && typeof stroke.width === "number" && isNonUniform(baked)) {
3119
3289
  onWarn(`<${tag}> stroke sits under a skew or non-uniform scale; its width takes the uniform part only`);
@@ -3292,6 +3462,12 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
3292
3462
  if (includeOpacity) out.push(...opacityAttr(`${name}-opacity`, svgPaintOpacity(paint)));
3293
3463
  return out;
3294
3464
  }
3465
+ function strokePaintAsFillAttrs(stroke, registry) {
3466
+ return [
3467
+ ...paintAttrs(stroke.paint, "fill", registry, false),
3468
+ ...opacityAttr("fill-opacity", stroke.opacity ?? svgPaintOpacity(stroke.paint))
3469
+ ];
3470
+ }
3295
3471
  function svgPaintOpacity(paint) {
3296
3472
  if (paint.kind === "solid") return paint.opacity;
3297
3473
  return paint.kind === "gradient" ? paint.paint.opacity : void 0;
@@ -3361,12 +3537,13 @@ function strokeAttrsFor(stroke, registry, warn) {
3361
3537
  }
3362
3538
  return attrs;
3363
3539
  }
3364
- function textXml(node, registry, namespaces, warn) {
3540
+ function textXml(node, registry, namespaces, warn, given) {
3541
+ const layout = given && layoutMeasured(given, node.text) ? given : null;
3365
3542
  const x = node.x + anchorOffset(node.style, node.width, node.text);
3366
3543
  const attrs = [
3367
3544
  `x="${trimNumber(x)}"`,
3368
3545
  `y="${trimNumber(node.y)}"`,
3369
- `dominant-baseline="text-before-edge"`,
3546
+ layout ? `${LAID_ATTR}="lines"` : `dominant-baseline="text-before-edge"`,
3370
3547
  // SVG's default whitespace handling would collapse indentation and runs
3371
3548
  // of spaces away on re-import.
3372
3549
  `xml:space="preserve"`,
@@ -3381,9 +3558,10 @@ function textXml(node, registry, namespaces, warn) {
3381
3558
  if (style?.fontVariantCaps === "small-caps") attrs.push('font-variant="small-caps"');
3382
3559
  const direction = svgDirection(style, node.text);
3383
3560
  if (direction === "rtl" || style?.direction === "ltr") attrs.push(`direction="${direction}"`);
3561
+ let anchor = "start";
3384
3562
  if (style?.align != null || direction === "rtl") {
3385
3563
  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";
3564
+ anchor = align === "center" ? "middle" : align === "start" || align === "justify" ? "start" : align === "end" ? "end" : align === "left" === (direction === "ltr") ? "start" : "end";
3387
3565
  if (anchor !== "start") attrs.push(`text-anchor="${anchor}"`);
3388
3566
  if (align === "justify") attrs.push('data-weasel-align="justify"');
3389
3567
  }
@@ -3398,13 +3576,14 @@ function textXml(node, registry, namespaces, warn) {
3398
3576
  }
3399
3577
  if (style?.wrap) {
3400
3578
  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)");
3579
+ 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
3580
  }
3403
3581
  if (style?.tabSize != null) attrs.push(`data-weasel-tab-size="${trimNumber(style.tabSize)}"`);
3404
3582
  if (node.verticalAlign && node.verticalAlign !== "top") {
3405
3583
  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)`);
3584
+ 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
3585
  }
3586
+ if (layout && style?.lineHeight != null) attrs.push(`${LINE_HEIGHT_ATTR}="${trimNumber(style.lineHeight)}"`);
3408
3587
  if (node.fill !== void 0) {
3409
3588
  const paint = node.fill?.paint;
3410
3589
  if (!paint) attrs.push('fill="none"');
@@ -3421,7 +3600,7 @@ function textXml(node, registry, namespaces, warn) {
3421
3600
  if (frame) attrs.push(frame);
3422
3601
  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
3602
  const resolved = resolveTextStyle(style);
3424
- const body = linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3603
+ const body = layout ? laidLinesXml(pieces, layout, { anchor, rtl: direction === "rtl" }) : linesXml(pieces, x, resolved.fontSize * resolved.lineHeight);
3425
3604
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3426
3605
  const metaEls = metaElementsXml(node.meta, namespaces);
3427
3606
  return `<text ${attrs.join(" ")}${metaAttrs}>${body}${metaEls}</text>`;
@@ -3518,7 +3697,7 @@ function serializeSvg(nodes, opts = {}) {
3518
3697
  const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
3519
3698
  const namespaces = opts.namespaces ?? {};
3520
3699
  const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
3521
- if (registry.usesPrivateNamespace() && namespaces[WEASEL_NS_PREFIX] == null) {
3700
+ if ((registry.usesPrivateNamespace() || writesVariableWidth(nodes)) && namespaces[WEASEL_NS_PREFIX] == null) {
3522
3701
  rootAttrs.push(`xmlns:${WEASEL_NS_PREFIX}="${WEASEL_NS}"`);
3523
3702
  }
3524
3703
  for (const [prefix, uri] of Object.entries(namespaces)) {
@@ -3544,7 +3723,7 @@ function serializeSvg(nodes, opts = {}) {
3544
3723
  said.add(m);
3545
3724
  opts.onWarn(m);
3546
3725
  };
3547
- const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn)).join("");
3726
+ const bodyXml = nodes.map((n) => nodeXml(n, registry, namespaces, warn, opts.textLayout)).join("");
3548
3727
  const titleXml = opts.title && opts.title.length > 0 ? `<title>${escapeText(opts.title)}</title>` : "";
3549
3728
  const xml = `<svg ${rootAttrs.join(" ")}>${titleXml}${defsXml}${docMetaXml}${bodyXml}</svg>`;
3550
3729
  return opts.pretty ? prettyXml(xml) : xml;
@@ -3578,16 +3757,17 @@ function registerMarkers(stroke, registry) {
3578
3757
  if (ref !== void 0) registry.markerId(ref);
3579
3758
  }
3580
3759
  }
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;
3760
+ function nodeXml(node, registry, namespaces, warn, layoutOf) {
3761
+ const xml = kindXml(node, registry, namespaces, warn, layoutOf);
3762
+ if (!node.hidden) return xml;
3763
+ return xml.replace(/^<[\w:]+/, (open) => `${open} display="none"`).replace(/<path (?=[^>]*:role="stroke-outline")/, '<path display="none" ');
3584
3764
  }
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);
3765
+ function kindXml(node, registry, namespaces, warn, layoutOf) {
3766
+ if (node.kind === "group") return groupXml(node, registry, namespaces, warn, layoutOf);
3767
+ if (node.kind === "text") return textXml(node, registry, namespaces, warn, layoutOf?.(node));
3588
3768
  if (node.kind === "image") return imageXml(node, namespaces);
3589
3769
  const drawn = pathAsSvgDraws(node);
3590
- return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn) : pathXml(drawn, registry, namespaces, warn);
3770
+ return drawn.kind === "group" ? groupXml(drawn, registry, namespaces, warn, layoutOf) : pathXml(drawn, registry, namespaces, warn);
3591
3771
  }
3592
3772
  function imageXml(node, namespaces) {
3593
3773
  const box = `x="${trimNumber(node.x)}" y="${trimNumber(node.y)}" width="${trimNumber(node.width)}" height="${trimNumber(node.height)}"`;
@@ -3611,7 +3791,7 @@ function imageXml(node, namespaces) {
3611
3791
  const head = ['<g data-weasel-image="true"', ...outer].join(" ");
3612
3792
  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
3793
  }
3614
- function groupXml(node, registry, namespaces, warn) {
3794
+ function groupXml(node, registry, namespaces, warn, layoutOf) {
3615
3795
  const attrs = [];
3616
3796
  if (node.transform) {
3617
3797
  const m = formatMatrix(node.transform);
@@ -3625,7 +3805,7 @@ function groupXml(node, registry, namespaces, warn) {
3625
3805
  }
3626
3806
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3627
3807
  const head = attrs.length > 0 ? `<g ${attrs.join(" ")}${metaAttrs}>` : `<g${metaAttrs}>`;
3628
- const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn)).join("");
3808
+ const body = node.children.map((c) => nodeXml(c, registry, namespaces, warn, layoutOf)).join("");
3629
3809
  return `${head}${body}${metaElementsXml(node.meta, namespaces)}</g>`;
3630
3810
  }
3631
3811
  function pathXml(node, registry, namespaces, warn) {
@@ -3635,40 +3815,46 @@ function pathXml(node, registry, namespaces, warn) {
3635
3815
  if (node.path.kind === "polygon" && node.path.fillRule === "evenodd") {
3636
3816
  attrs.push(`fill-rule="evenodd"`);
3637
3817
  }
3818
+ const varies = hasVariableWidth(node.stroke);
3638
3819
  if (node.stroke) {
3639
- const strokeAttrs = strokeAttrsFor(node.stroke, registry, warn);
3640
- for (const a of strokeAttrs) attrs.push(a);
3820
+ const strokeAttrs = strokeAttrsFor(node.stroke, registry, varies ? () => {
3821
+ } : warn);
3822
+ for (const a of varies ? variableWidthAttrs(node.stroke, strokeAttrs) : strokeAttrs) attrs.push(a);
3641
3823
  } else {
3642
3824
  attrs.push('stroke="none"');
3643
3825
  }
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
- }
3826
+ const opacity = node.opacity != null && node.opacity !== 1 ? `opacity="${trimNumber(node.opacity)}"` : null;
3827
+ if (opacity && !varies) attrs.push(opacity);
3828
+ const transform = rotationAttr(node);
3829
+ if (transform) attrs.push(transform);
3654
3830
  const metaAttrs = metaAttrsXml(node.meta, namespaces);
3655
3831
  const metaEls = metaElementsXml(node.meta, namespaces);
3656
- if (metaEls) {
3657
- return `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>`;
3658
- }
3659
- return `<path ${attrs.join(" ")}${metaAttrs}/>`;
3832
+ const xml = metaEls ? `<path ${attrs.join(" ")}${metaAttrs}>${metaEls}</path>` : `<path ${attrs.join(" ")}${metaAttrs}/>`;
3833
+ if (!varies) return xml;
3834
+ const both = xml + strokeOutlineXml(node.path, node.stroke, strokePaintAsFillAttrs(node.stroke, registry), transform);
3835
+ return opacity ? strokeGroupXml(opacity, both) : both;
3836
+ }
3837
+ function rotationAttr(node) {
3838
+ if (node.rotation == null || node.rotation === 0) return null;
3839
+ const b = pathBounds(node.path);
3840
+ const cx = b.minX + (b.maxX - b.minX) / 2;
3841
+ const cy = b.minY + (b.maxY - b.minY) / 2;
3842
+ const deg = node.rotation * 180 / Math.PI;
3843
+ return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
3660
3844
  }
3661
- function svgNeeds(nodes) {
3845
+ function svgNeeds(nodes, opts = {}) {
3662
3846
  const registry = new PaintServerRegistry();
3663
3847
  registerPaintServers(nodes, registry);
3664
3848
  const fonts = /* @__PURE__ */ new Map();
3849
+ const add = (face) => fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3665
3850
  const walk = (list) => {
3666
3851
  for (const n of list) {
3667
3852
  if (n.kind === "group") walk(n.children);
3668
3853
  else if (n.kind === "text") {
3854
+ if (opts.textLayout) layoutFaces(n).forEach(add);
3669
3855
  for (const run of n.runs ?? []) {
3670
3856
  const face = scriptSizeFace(run, n.style);
3671
- if (face) fonts.set(`${face.family}|${face.weight}|${face.style}`, face);
3857
+ if (face) add(face);
3672
3858
  }
3673
3859
  }
3674
3860
  }
@@ -3676,8 +3862,15 @@ function svgNeeds(nodes) {
3676
3862
  walk(nodes);
3677
3863
  return { fonts: [...fonts.values()], paintKinds: registry.paintKinds() };
3678
3864
  }
3679
- function warmSvg(nodes) {
3680
- const needs = svgNeeds(nodes);
3865
+ function layoutFaces(node) {
3866
+ const style = resolveTextStyle(node.style);
3867
+ return toRuns(node.runs && node.runs.length > 0 ? node.runs : node.text).map((run) => {
3868
+ const face = resolveRunFace(run, style);
3869
+ return { family: face.fontFamily, weight: face.fontWeight, style: face.fontStyle };
3870
+ });
3871
+ }
3872
+ function warmSvg(nodes, opts = {}) {
3873
+ const needs = svgNeeds(nodes, opts);
3681
3874
  return Promise.all([
3682
3875
  warmFonts(needs.fonts),
3683
3876
  warmPaintKinds(needs.paintKinds.filter(isPaintKindKnown))
@@ -3730,7 +3923,9 @@ function strokeDataFromSvg(stroke, box) {
3730
3923
  ...stroke.miterLimit !== void 0 ? { miterLimit: stroke.miterLimit } : {},
3731
3924
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3732
3925
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3733
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
3926
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
3927
+ ...stroke.vertexWidths !== void 0 ? { vertexWidths: stroke.vertexWidths } : {},
3928
+ ...stroke.nib !== void 0 ? { nib: stroke.nib } : {}
3734
3929
  };
3735
3930
  }
3736
3931
  function fillInBoxFrame(fill, box) {
@@ -3773,9 +3968,12 @@ function svgNodesToKitDrafts(input, nextId, options = {}) {
3773
3968
  ...n.hidden ? { hidden: true } : {}
3774
3969
  };
3775
3970
  drafts.push(draft);
3971
+ const at = drafts.length - 1;
3776
3972
  const acc = unionAABB(n.children.map((c) => visit(c, draft.id, k)));
3777
3973
  if (!acc) {
3778
- drafts.splice(drafts.indexOf(draft), 1);
3974
+ const kept = drafts.length > at + 1 || (options.keepEmpty?.(n) ?? false);
3975
+ if (!kept) drafts.splice(at, 1);
3976
+ else if (n.clip) draft.pose = boxOfPath(n.clip);
3779
3977
  return null;
3780
3978
  }
3781
3979
  draft.pose = n.clip ? boxOfPath(n.clip) : acc;
@@ -3859,7 +4057,7 @@ async function unpackSvgFiles(files, ctx) {
3859
4057
  for (const file of files) {
3860
4058
  try {
3861
4059
  const text = await readFileText(file);
3862
- const parsed = parseSvg(text);
4060
+ const parsed = parseSvg(text, { keepHidden: true });
3863
4061
  for (const w of parsed.warnings) dwarn("ingest", `svg "${file.name}":`, w);
3864
4062
  let drafts = svgNodesToKitDrafts(parsed, () => ctx.scene.mintId());
3865
4063
  if (drafts.length === 0) {
@@ -3904,7 +4102,8 @@ async function unpackSvgFiles(files, ctx) {
3904
4102
  layer,
3905
4103
  pose: place(d.pose),
3906
4104
  data: d.kind === "leaf" ? scaleLeafData(d.data, scale) : d.data,
3907
- parent: d.parentId
4105
+ parent: d.parentId,
4106
+ ...d.hidden ? { hidden: true } : {}
3908
4107
  },
3909
4108
  label: "Insert SVG"
3910
4109
  })
@@ -3939,7 +4138,9 @@ function svgStrokeFromKit(stroke, box) {
3939
4138
  ...stroke.align !== void 0 ? { align: stroke.align } : {},
3940
4139
  ...stroke.markerStart !== void 0 ? { markerStart: stroke.markerStart } : {},
3941
4140
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
3942
- ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
4141
+ ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
4142
+ ...stroke.vertexWidths?.length ? { vertexWidths: stroke.vertexWidths } : {},
4143
+ ...stroke.nib ? { nib: stroke.nib } : {}
3943
4144
  };
3944
4145
  }
3945
4146
  function svgImageFromKit(image, pose) {
@@ -3973,22 +4174,12 @@ function runInPoseFrame(run, box) {
3973
4174
  function svgLeafFromKit(data, pose) {
3974
4175
  if (data.image) return svgImageFromKit(data.image, pose);
3975
4176
  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;
4177
+ const text = svgTextFromKit({ ...data, text: data.text }, pose);
4178
+ const boxPath = { kind: "rect", x: pose.x, y: pose.y, width: pose.width, height: pose.height };
4179
+ const clip = data.overflow === "clip" ? { clip: pathThroughPoseFrame(boxPath, pose) } : {};
4180
+ if (!data.background?.paint) return clip.clip ? { kind: "group", children: [text], ...clip } : text;
4181
+ const box = svgLeafFromKit({ path: boxPath, fill: data.background }, pose);
4182
+ return { kind: "group", children: [box, text], ...clip };
3992
4183
  }
3993
4184
  if (!data.path) return null;
3994
4185
  const stroke = svgStrokeFromKit(data.stroke, pose);
@@ -4007,12 +4198,33 @@ function svgLeafFromKit(data, pose) {
4007
4198
  } else if (pose.rotation) node.rotation = pose.rotation;
4008
4199
  return node;
4009
4200
  }
4201
+ function svgTextFromKit(data, pose) {
4202
+ const pad = Math.max(0, Math.min(data.padding ?? 0, pose.width / 2, pose.height / 2));
4203
+ const node = {
4204
+ kind: "text",
4205
+ x: pose.x + pad,
4206
+ y: pose.y + pad,
4207
+ width: pose.width - 2 * pad,
4208
+ height: pose.height - 2 * pad,
4209
+ text: data.text
4210
+ };
4211
+ if (data.style) node.style = data.style;
4212
+ if (data.runs) node.runs = data.runs.map((r) => runInPoseFrame(r, pose));
4213
+ if (data.verticalAlign) node.verticalAlign = data.verticalAlign;
4214
+ if (data.fill !== void 0) node.fill = data.fill && mapFillPaint(data.fill, (p) => fillInPoseFrame(p, pose));
4215
+ if (data.stroke) node.stroke = strokeInPoseFrame(data.stroke, pose);
4216
+ if (pose.rotation) node.rotation = pose.rotation;
4217
+ if (pose.transform) node.transform = pose.transform;
4218
+ return node;
4219
+ }
4010
4220
  function svgNodesFromKit(tree, options = {}) {
4011
4221
  const { include, leaf, group } = options;
4012
4222
  const emit = (id) => {
4013
- if (include && !include(id)) return null;
4014
4223
  const node = tree.get(id);
4015
- if (!node) return null;
4224
+ const out2 = node && (!include || include(id)) ? lower(id, node) : null;
4225
+ return out2 && node?.hidden === true ? { ...out2, hidden: true } : out2;
4226
+ };
4227
+ const lower = (id, node) => {
4016
4228
  if (node.kind === "container") {
4017
4229
  const emitChildren = () => {
4018
4230
  const children = [];
@@ -4045,6 +4257,6 @@ function svgNodesFromKit(tree, options = {}) {
4045
4257
  return out;
4046
4258
  }
4047
4259
 
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 };
4260
+ export { DEFAULT_MEDIA_ENVIRONMENT, IDENTITY_MATRIX, evaluateMediaQuery, evaluateSupports, fillDataFromSvg, nativeSvgKind, nativeSvgSpace, parseSvg, serializePathD, serializeSvg, strokeDataFromSvg, svgImageFromKit, svgLeafFromKit, svgNeeds, svgNodesFromKit, svgNodesToKitDrafts, svgPaintFromKit, svgStrokeFromKit, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles, warmSvg };
4049
4261
  //# sourceMappingURL=index.js.map
4050
4262
  //# sourceMappingURL=index.js.map