@weasel-js/svg 1.15.1 → 1.16.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Affine } from '@weasel-js/geom';
2
2
  export { pathToD as serializePathD } from '@weasel-js/geom';
3
- import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, BooleanShapeOp, StyledRun, TextStyle, TextVerticalAlign, Fill, Stroke, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, IngestCtx, Inherit, ImageNodeData, NodeClip } from '@weasel-js/core';
3
+ import { Path, Paint, ScreenLength, StrokeAlign, MarkerRef, VertexWidth, StrokeNib, Stroke, BooleanShapeOp, StyledRun, TextStyle, TextVerticalAlign, Fill, PoseLinear, MarkerEntry, TextPoseLayout, RenderNeeds, TilePatternSpec, IngestCtx, Inherit, ImageNodeData, NodeClip } from '@weasel-js/core';
4
4
 
5
5
  /**
6
6
  * Media Queries 4 evaluation against a fixed environment.
@@ -133,6 +133,12 @@ interface SvgStroke {
133
133
  * more takes `width`. */
134
134
  vertexWidths?: VertexWidth[];
135
135
  nib?: StrokeNib;
136
+ /** The kit stroke's `trim`, measured along the path as written. SVG has no
137
+ * trim, so the serializer strokes only the span it draws; `parseSvg` never sets it. */
138
+ trim?: Stroke['trim'];
139
+ /** `stroke-dashoffset`: where along its pattern the dash starts. Written for
140
+ * a trimmed run that starts mid-pattern; `parseSvg` never sets it. */
141
+ dashOffset?: number;
136
142
  }
137
143
  /**
138
144
  * Which of a node's paints its own element sets none of, so that it takes the
@@ -463,7 +469,7 @@ declare function nativeSvgSpace(space: string | null | undefined): boolean;
463
469
  * Produce an SVG document string. The root `viewBox` is `opts.viewBox` if
464
470
  * supplied, otherwise a tight bounding box around every leaf.
465
471
  */
466
- declare function serializeSvg(nodes: SvgNode[], opts?: SerializeOptions): string;
472
+ declare function serializeSvg(given: SvgNode[], opts?: SerializeOptions): string;
467
473
 
468
474
  /**
469
475
  * Escaping for the XML this package writes by hand.
@@ -562,13 +568,7 @@ declare function evaluateSupports(condition: string, opts?: SupportsOptions): bo
562
568
  */
563
569
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
564
570
 
565
- /**
566
- * The kit-to-SVG direction: scene leaves back to `SvgNode`s, the inverse of
567
- * `svgNodesToKitDrafts`. Each leaf is written as the built-in painter that
568
- * draws it would draw it — the pose baked into the geometry, box-relative
569
- * paints resolved into that box — so `serializeSvg` over the result shows what
570
- * the canvas shows.
571
- */
571
+ /** A kit paint and stroke as the `SvgPaint` and `SvgStroke` an element is written with. */
572
572
 
573
573
  /** A leaf's box: where it is drawn, and the frame its box-relative paints
574
574
  * resolve against. */
@@ -582,6 +582,22 @@ interface SvgKitPose {
582
582
  * so a path leaf's is baked into its geometry with its rotation. */
583
583
  transform?: PoseLinear;
584
584
  }
585
+ /** A kit paint as an `SvgPaint`. `null` is `fill="none"`; a box-relative
586
+ * gradient or pattern is resolved into `box`, since the geometry it paints
587
+ * is written already placed there. */
588
+ declare function svgPaintFromKit(fill: Paint | null, box: SvgKitPose): SvgPaint;
589
+ /** A kit `Stroke` as an `SvgStroke` — the inverse of `strokeDataFromSvg`.
590
+ * One with no paint or no width draws nothing and is written as no stroke. */
591
+ declare function svgStrokeFromKit(raw: Stroke | null | undefined, box: SvgKitPose): SvgStroke | undefined;
592
+
593
+ /**
594
+ * The kit-to-SVG direction: scene leaves back to `SvgNode`s, the inverse of
595
+ * `svgNodesToKitDrafts`. Each leaf is written as the built-in painter that
596
+ * draws it would draw it — the pose baked into the geometry, box-relative
597
+ * paints resolved into that box — so `serializeSvg` over the result shows what
598
+ * the canvas shows.
599
+ */
600
+
585
601
  /** The leaf data the kit's path, text and image painters read — what
586
602
  * `svgNodesToKitDrafts` writes. */
587
603
  interface SvgKitLeafData {
@@ -601,13 +617,7 @@ interface SvgKitLeafData {
601
617
  overflow?: 'visible' | 'clip';
602
618
  image?: ImageNodeData['image'];
603
619
  }
604
- /** A kit paint as an `SvgPaint`. `null` is `fill="none"`; a box-relative
605
- * gradient or pattern is resolved into `box`, since the geometry it paints
606
- * is written already placed there. */
607
- declare function svgPaintFromKit(fill: Paint | null, box: SvgKitPose): SvgPaint;
608
- /** A kit `Stroke` as an `SvgStroke` — the inverse of `strokeDataFromSvg`.
609
- * One with no paint or no width draws nothing and is written as no stroke. */
610
- declare function svgStrokeFromKit(raw: Stroke | null | undefined, box: SvgKitPose): SvgStroke | undefined;
620
+
611
621
  /** Write a `kit:image` leaf as an `SvgImageNode` — the inverse of the image
612
622
  * arm of `svgNodesToKitDrafts`. The pose is the image's box. */
613
623
  declare function svgImageFromKit(image: ImageNodeData['image'], pose: SvgKitPose): SvgImageNode;
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
- import { identity, multiply, pathToD, invert, splitByClosure, fillsOpenSubpaths, rotateAboutPoint, hasOpenSubpath, worldCoords, splitSubpaths, joinPaths } from '@weasel-js/geom';
1
+ import { identity, RAD_TO_DEG, multiply, pathToD, clamp01, DEG_TO_RAD, invert, splitByClosure, fillsOpenSubpaths, pathLengthTable, lengthAtCurve, hasOpenSubpath, worldCoords, boundsOfPath as boundsOfPath$1, splitSubpaths, rotateAboutPoint, applyToPoint, joinPaths } from '@weasel-js/geom';
2
2
  export { pathToD as serializePathD } from '@weasel-js/geom';
3
- import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, localizeWorldPoses, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, definesFrame, INHERIT, clipOutline, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, halfLeadingFor, numericWeight, strokeToPath, booleanGroupFields, toRuns, resolveRunFace, derivedDepOf, documentOf, booleanOpOf, DEFAULT_TEXT_FILL, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isImagePatternPaint, isTilePaint, tileSpecOf } from '@weasel-js/core';
3
+ import { isTileKind, boundsOfPath, transformPath, isSolidPaint, warmFonts, warmPaintKinds, isPaintKindKnown, solid, fillToBoundsFrame, getMarker, registerMarker, dwarn, unionAABB, localizeWorldPoses, createInsertOp, fillInPoseFrame, resolveStroke, pathThroughPoseFrame, resolveFill, pathInPoseFrame, poseLinearOf, poseFrameOf, asPolygonPath, definesFrame, INHERIT, clipOutline, resolveScreenLength, pathFromD, isPatternPaint, resolveTextStyle, halfLeadingFor, numericWeight, strokeToPath, booleanGroupFields, toRuns, resolveRunFace, derivedDepOf, documentOf, booleanOpOf, DEFAULT_TEXT_FILL, parseColor, rgbaToHex, layoutRuns, resolveRuns, resolveAlign, PathBuilder, PATH_M, PATH_L, PATH_Z, scaleVertexWidth, getPaintKind, resolveMarkerSize, paragraphDirection, scriptMetricsFor, vertexHalfWidths, isImagePatternPaint, isTilePaint, tileSpecOf } from '@weasel-js/core';
4
4
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
5
+ import { trimSpan, trimmedPath, trimmedRuns } from '@weasel-js/stroke';
5
6
 
6
7
  // src/types.ts
7
8
  var IDENTITY_MATRIX = identity();
8
9
  function applyMatrix(m, x, y) {
9
- return { x: m[0] * x + m[2] * y + m[4], y: m[1] * x + m[3] * y + m[5] };
10
+ const [px, py] = applyToPoint(m, x, y);
11
+ return { x: px, y: py };
10
12
  }
11
13
  function isIdentity(m, eps = 1e-9) {
12
14
  return Math.abs(m[0] - 1) < eps && Math.abs(m[1]) < eps && Math.abs(m[2]) < eps && Math.abs(m[3] - 1) < eps && Math.abs(m[4]) < eps && Math.abs(m[5]) < eps;
@@ -42,7 +44,7 @@ function parseTransform(attr, onWarn) {
42
44
  const sy = nums.length >= 2 ? nums[1] : sx;
43
45
  local = [sx, 0, 0, sy, 0, 0];
44
46
  } else if (name === "rotate") {
45
- const ang = (nums[0] ?? 0) * Math.PI / 180;
47
+ const ang = (nums[0] ?? 0) * DEG_TO_RAD;
46
48
  const c = Math.cos(ang);
47
49
  const s = Math.sin(ang);
48
50
  const rot = [c, s, -s, c, 0, 0];
@@ -54,7 +56,7 @@ function parseTransform(attr, onWarn) {
54
56
  local = rot;
55
57
  }
56
58
  } else if (name === "skewx" || name === "skewy") {
57
- const ang = (nums[0] ?? 0) * Math.PI / 180;
59
+ const ang = (nums[0] ?? 0) * DEG_TO_RAD;
58
60
  const t = Math.tan(ang);
59
61
  local = name === "skewx" ? [1, 0, t, 1, 0, 0] : [1, t, 0, 1, 0, 0];
60
62
  } else {
@@ -773,7 +775,6 @@ function dimension(v) {
773
775
  const m = DIMENSION2.exec(v);
774
776
  return m ? { n: Number(m[1]), unit: m[2].toLowerCase() } : void 0;
775
777
  }
776
- var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
777
778
  function pxPer(unit, fontSize) {
778
779
  if (unit === "") return 1;
779
780
  const ems = emsPer(unit);
@@ -1377,7 +1378,7 @@ function boxTransformAttr(node) {
1377
1378
  if (!node.rotation) return null;
1378
1379
  const cx = node.x + node.width / 2;
1379
1380
  const cy = node.y + node.height / 2;
1380
- const deg = node.rotation * 180 / Math.PI;
1381
+ const deg = node.rotation * RAD_TO_DEG;
1381
1382
  return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
1382
1383
  }
1383
1384
 
@@ -1439,8 +1440,6 @@ function toggle(node, key) {
1439
1440
  if (node[key]) delete node[key];
1440
1441
  else node[key] = true;
1441
1442
  }
1442
-
1443
- // src/patterns.ts
1444
1443
  function patternXml(id, paint, onWarn) {
1445
1444
  if (isImagePatternPaint(paint)) return imagePatternXml(id, paint, onWarn);
1446
1445
  if (!isTilePaint(paint)) {
@@ -1500,7 +1499,7 @@ function shapeXml(shape) {
1500
1499
  case "circle":
1501
1500
  return `<circle cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" r="${trimNumber(shape.r)}" fill="${escapeAttr(shape.color)}"/>`;
1502
1501
  case "ellipse": {
1503
- const deg = shape.rotation * 180 / Math.PI;
1502
+ const deg = shape.rotation * RAD_TO_DEG;
1504
1503
  const transform = deg === 0 ? "" : ` transform="rotate(${trimNumber(deg)} ${trimNumber(shape.cx)} ${trimNumber(shape.cy)})"`;
1505
1504
  return `<ellipse cx="${trimNumber(shape.cx)}" cy="${trimNumber(shape.cy)}" rx="${trimNumber(shape.rx)}" ry="${trimNumber(shape.ry)}" fill="${escapeAttr(shape.color)}"${transform}/>`;
1506
1505
  }
@@ -1566,8 +1565,6 @@ function readPlacement(el, onWarn) {
1566
1565
  if (!isIdentity([m[0], m[1], m[2], m[3], 0, 0])) placement.transform = [m[0], m[1], m[2], m[3]];
1567
1566
  return placement;
1568
1567
  }
1569
-
1570
- // src/gradients.ts
1571
1568
  var WEASEL_NS = "urn:weasel-js:svg";
1572
1569
  var WEASEL_NS_PREFIX = "wzl";
1573
1570
  function nativeSvgKind(kind) {
@@ -1888,7 +1885,7 @@ function defaultMarkerXml(id, entry, onWarn, key, size) {
1888
1885
  const d = pathToD(path);
1889
1886
  const fill = markerPaintAttrs("fill", entry.fill, id, onWarn);
1890
1887
  const outline = entry.outline ? ` ${markerPaintAttrs("stroke", entry.outline.paint, id, onWarn)} stroke-width="${trimNumber(entry.outline.width * unit)}" stroke-linecap="round" stroke-linejoin="round"` : "";
1891
- const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * 180 / Math.PI) : "auto-start-reverse";
1888
+ const orient = typeof entry.orient === "number" ? trimNumber(entry.orient * RAD_TO_DEG) : "auto-start-reverse";
1892
1889
  const units = size === void 0 ? 'markerUnits="strokeWidth" markerWidth="8" markerHeight="8"' : `markerUnits="userSpaceOnUse" markerWidth="${trimNumber(8 * unit)}" markerHeight="${trimNumber(8 * unit)}" ${WEASEL_NS_PREFIX}:key="${escapeAttr(name)}" ${WEASEL_NS_PREFIX}:size="${typeof size === "number" ? trimNumber(size) : `${trimNumber(size.px)}px`}"`;
1893
1890
  const inset = writesInset(entry) ? ` ${WEASEL_NS_PREFIX}:inset="${trimNumber(entry.inset)}"` : "";
1894
1891
  return `<marker id="${id}" ${units}${inset} refX="0" refY="0" orient="${orient}" overflow="visible"><path d="${d}" ${fill}${outline}/></marker>`;
@@ -2295,7 +2292,8 @@ function strokeOutlineXml(path, stroke, fillAttrs, transform) {
2295
2292
  ...stroke.align ? { align: stroke.align } : {},
2296
2293
  ...stroke.markerStart ? { markerStart: stroke.markerStart } : {},
2297
2294
  ...stroke.markerMid ? { markerMid: stroke.markerMid } : {},
2298
- ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {}
2295
+ ...stroke.markerEnd ? { markerEnd: stroke.markerEnd } : {},
2296
+ ...stroke.trim ? { trim: stroke.trim } : {}
2299
2297
  };
2300
2298
  const outline = strokeToPath(path, kit);
2301
2299
  const attrs = [`d="${pathToD(outline)}"`, ...fillAttrs, 'stroke="none"'];
@@ -3277,7 +3275,7 @@ function markerOrient(raw) {
3277
3275
  if (!m) return { orient: 0, reverseAtStart: false };
3278
3276
  const n = parseFloat(m[1]);
3279
3277
  const unit = (m[2] ?? "deg").toLowerCase();
3280
- const rad = unit === "rad" ? n : unit === "grad" ? n * Math.PI / 200 : unit === "turn" ? n * 2 * Math.PI : n * Math.PI / 180;
3278
+ const rad = unit === "rad" ? n : unit === "grad" ? n * Math.PI / 200 : unit === "turn" ? n * 2 * Math.PI : n * DEG_TO_RAD;
3281
3279
  return { orient: rad, reverseAtStart: false };
3282
3280
  }
3283
3281
  function markerPaint(style, attr, gradients, onWarn) {
@@ -3824,12 +3822,15 @@ function pathAsSvgDraws(node) {
3824
3822
  const { stroke: _stroke, ...rest } = node;
3825
3823
  return { ...rest, fill: only.fill, ...only.stroke ? { stroke: only.stroke } : {} };
3826
3824
  }
3827
- const group = {
3828
- kind: "group",
3829
- children: pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
3830
- };
3825
+ return drawsAsGroup(
3826
+ node,
3827
+ pieces.map((p) => ({ kind: "path", path: p.path, fill: p.fill, ...p.stroke ? { stroke: p.stroke } : {} }))
3828
+ );
3829
+ }
3830
+ function drawsAsGroup(node, children) {
3831
+ const group = { kind: "group", children };
3831
3832
  if (node.rotation != null && node.rotation !== 0) {
3832
- const b = pathBounds(path);
3833
+ const b = pathBounds(node.path);
3833
3834
  group.transform = rotateAboutPoint((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2, node.rotation);
3834
3835
  }
3835
3836
  if (node.opacity != null) group.opacity = node.opacity;
@@ -3911,6 +3912,7 @@ function strokeAttrsFor(stroke, registry, warn) {
3911
3912
  }
3912
3913
  if (stroke.dash && stroke.dash.length > 0) {
3913
3914
  attrs.push(`stroke-dasharray="${stroke.dash.map(trimNumber).join(" ")}"`);
3915
+ if (stroke.dashOffset) attrs.push(`stroke-dashoffset="${trimNumber(stroke.dashOffset)}"`);
3914
3916
  }
3915
3917
  if (stroke.miterLimit != null) {
3916
3918
  attrs.push(`stroke-miterlimit="${trimNumber(stroke.miterLimit)}"`);
@@ -3983,6 +3985,44 @@ function fitLeafXml(xml, inherits, painted) {
3983
3985
  return `<${name}${attrs}${resets.map((n) => ` ${n}="${INITIAL[n]}"`).join("")}${close2}>`;
3984
3986
  });
3985
3987
  }
3988
+ var NONE = { kind: "none" };
3989
+ function withTrimDrawn(node) {
3990
+ const { stroke, inherits: _inherits, ...rest } = node;
3991
+ if (!stroke || stroke.trim === void 0 || hasVariableWidth(stroke)) return node;
3992
+ const { trim, ...own2 } = stroke;
3993
+ const bare = own2.paint.kind === "gradient" ? { ...own2, paint: { ...own2.paint, paint: fillInPoseFrame(own2.paint.paint, boundsOfPath$1(node.path)) } } : own2;
3994
+ const cut = trimmedPath(node.path, trim);
3995
+ const children = [
3996
+ ...node.fill.kind === "none" ? [] : [{ kind: "path", path: node.path, fill: node.fill }],
3997
+ ...cut ? strokeRuns(cut, bare) : []
3998
+ ];
3999
+ if (children.length === 0) return null;
4000
+ if (children.length === 1 && !node.rotation) return { ...rest, ...children[0] };
4001
+ return drawsAsGroup(node, children);
4002
+ }
4003
+ function withTrimsDrawn(nodes) {
4004
+ const out = [];
4005
+ for (const n of nodes) {
4006
+ const drawn = n.kind === "path" ? withTrimDrawn(n) : n.kind === "group" ? { ...n, children: withTrimsDrawn(n.children) } : n;
4007
+ if (drawn) out.push(drawn);
4008
+ }
4009
+ return out;
4010
+ }
4011
+ function strokeRuns(cut, stroke) {
4012
+ const dashed = (stroke.dash?.length ?? 0) > 0;
4013
+ return trimmedRuns(cut, dashed, true).map(({ path, dashPhase, fromClosed }) => {
4014
+ const s = fromClosed ? withoutEndMarkers(stroke) : stroke;
4015
+ return { kind: "path", path, fill: NONE, stroke: dashed && dashPhase > 0 ? { ...s, dashOffset: dashPhase } : s };
4016
+ });
4017
+ }
4018
+ var NOTHING = { start: 1, end: 0 };
4019
+ function strokeTrimOnPath(stroke, local, drawn) {
4020
+ if (stroke.trim === void 0) return stroke;
4021
+ const span = trimSpan(local, stroke.trim);
4022
+ if (!span) return { ...stroke, trim: NOTHING };
4023
+ const table = pathLengthTable(drawn);
4024
+ return { ...stroke, trim: { start: { length: lengthAtCurve(table, span.from) }, end: { length: lengthAtCurve(table, span.to) } } };
4025
+ }
3986
4026
  function textXml(node, registry, namespaces, warn, given) {
3987
4027
  const layout = given && layoutMeasured(given, node.text) ? given : null;
3988
4028
  const resolved = resolveTextStyle(node.style);
@@ -4145,13 +4185,12 @@ function prettyXml(xml) {
4145
4185
  }
4146
4186
  return lines.join("\n");
4147
4187
  }
4148
-
4149
- // src/serialize.ts
4150
4188
  var NOTHING_PAINTED = /* @__PURE__ */ new Set();
4151
- function serializeSvg(nodes, opts = {}) {
4189
+ function serializeSvg(given, opts = {}) {
4152
4190
  const registry = new PaintServerRegistry();
4191
+ const nodes = withTrimsDrawn(given);
4153
4192
  registerPaintServers(nodes, registry);
4154
- const bounds = opts.viewBox ?? computeBounds(nodes);
4193
+ const bounds = opts.viewBox ?? computeBounds(given);
4155
4194
  const vb = `${trimNumber(bounds.x)} ${trimNumber(bounds.y)} ${trimNumber(bounds.width)} ${trimNumber(bounds.height)}`;
4156
4195
  const namespaces = opts.namespaces ?? {};
4157
4196
  const rootAttrs = [`xmlns="http://www.w3.org/2000/svg"`];
@@ -4304,7 +4343,7 @@ function rotationAttr(node) {
4304
4343
  const b = pathBounds(node.path);
4305
4344
  const cx = b.minX + (b.maxX - b.minX) / 2;
4306
4345
  const cy = b.minY + (b.maxY - b.minY) / 2;
4307
- const deg = node.rotation * 180 / Math.PI;
4346
+ const deg = node.rotation * RAD_TO_DEG;
4308
4347
  return `transform="rotate(${trimNumber(deg)} ${trimNumber(cx)} ${trimNumber(cy)})"`;
4309
4348
  }
4310
4349
  function svgNeeds(nodes, opts = {}) {
@@ -4393,7 +4432,8 @@ function strokeDataFromSvg(stroke, box) {
4393
4432
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
4394
4433
  ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
4395
4434
  ...stroke.vertexWidths !== void 0 ? { vertexWidths: stroke.vertexWidths } : {},
4396
- ...stroke.nib !== void 0 ? { nib: stroke.nib } : {}
4435
+ ...stroke.nib !== void 0 ? { nib: stroke.nib } : {},
4436
+ ...stroke.trim !== void 0 ? { trim: stroke.trim } : {}
4397
4437
  };
4398
4438
  }
4399
4439
  function fillInBoxFrame(fill, box) {
@@ -4699,9 +4739,12 @@ function svgStrokeFromKit(raw, box) {
4699
4739
  ...stroke.markerMid !== void 0 ? { markerMid: stroke.markerMid } : {},
4700
4740
  ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {},
4701
4741
  ...stroke.vertexWidths?.length ? { vertexWidths: stroke.vertexWidths } : {},
4702
- ...stroke.nib ? { nib: stroke.nib } : {}
4742
+ ...stroke.nib ? { nib: stroke.nib } : {},
4743
+ ...stroke.trim !== void 0 ? { trim: stroke.trim } : {}
4703
4744
  };
4704
4745
  }
4746
+
4747
+ // src/fromKit.ts
4705
4748
  function svgImageFromKit(image, pose) {
4706
4749
  const node = {
4707
4750
  kind: "image",
@@ -4750,9 +4793,10 @@ function svgLeafFromKit(data, pose) {
4750
4793
  if (stroke) node.stroke = stroke;
4751
4794
  const frame = poseLinearOf(pose) && poseFrameOf(pose);
4752
4795
  if (frame) {
4753
- node.path = pathThroughPoseFrame(node.path, pose);
4796
+ const local = node.path;
4797
+ node.path = pathThroughPoseFrame(stroke?.trim ? asPolygonPath(local) : local, pose);
4754
4798
  node.fill = paintThroughMatrix(node.fill, frame);
4755
- if (stroke) node.stroke = strokeThroughMatrix(stroke, frame);
4799
+ if (stroke) node.stroke = strokeTrimOnPath(strokeThroughMatrix(stroke, frame), local, node.path);
4756
4800
  } else if (pose.rotation) node.rotation = pose.rotation;
4757
4801
  return node;
4758
4802
  }