@weasel-js/svg 1.4.2 → 1.4.4

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
@@ -72,7 +72,12 @@ type SvgPaint = {
72
72
  /** Stroke description: a paint plus structural line parameters. */
73
73
  interface SvgStroke {
74
74
  paint: SvgPaint;
75
- width: number;
75
+ /** World units, or `{ px }` for a width that holds its rendered thickness
76
+ * however the document is scaled — SVG's `vector-effect="non-scaling-stroke"`,
77
+ * and the same unit system as the kit's `Stroke.width`. */
78
+ width: number | {
79
+ px: number;
80
+ };
76
81
  opacity?: number;
77
82
  /** `stroke-linecap`. Default per SVG spec is `'butt'`. */
78
83
  cap?: 'butt' | 'round' | 'square';
@@ -166,8 +171,11 @@ interface SvgTextNode {
166
171
  * in `data.fill` / `data.stroke`. Not `SvgPaint`, which is the path
167
172
  * nodes' shape — a text paint reaches the renderer through the runs, and
168
173
  * `StyledRun.fill` / `.stroke` override these per range.
174
+ *
175
+ * `null` is SVG's `fill="none"`: outline-only text, painted by its stroke
176
+ * alone. Absent takes the default black.
169
177
  */
170
- fill?: FillStyle;
178
+ fill?: FillStyle | null;
171
179
  stroke?: Stroke;
172
180
  /** Element-level opacity (`opacity="..."`), 0..1. */
173
181
  opacity?: number;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, getMarker, SCRIPT_METRICS, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q, resolveStrokeWidth, solid, fillToBoundsFrame, resolveTextStyle, getPaintKind } from '@weasel-js/core';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, getMarker, SCRIPT_METRICS, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, PATH_C, PATH_Q, solid, fillToBoundsFrame, resolveTextStyle, getPaintKind } from '@weasel-js/core';
2
2
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
3
3
 
4
4
  // src/parse.ts
@@ -879,7 +879,7 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
879
879
  }
880
880
  const leafStyle = deriveStyle(style, el);
881
881
  const fill = readPaint(leafStyle, "fill", "#000000", gradients, onWarn);
882
- const stroke = readStroke(leafStyle, gradients, onWarn);
882
+ const stroke = readStroke(leafStyle, gradients, onWarn, isNonScalingStroke(el));
883
883
  const opacity = readOpacityAttr(el, "opacity");
884
884
  const fillRuleRaw = leafStyle["fill-rule"] ?? null;
885
885
  if (fillRuleRaw === "evenodd" && path.kind === "polygon") {
@@ -988,14 +988,14 @@ function readPaint(style, attr, defaultColor, gradients, onWarn) {
988
988
  if (a != null) out.opacity = a;
989
989
  return out;
990
990
  }
991
- function readStroke(style, gradients, onWarn) {
991
+ function readStroke(style, gradients, onWarn, nonScaling = false) {
992
992
  const inheritedStroke = style["stroke"] ?? null;
993
993
  const inheritedWidth = style["stroke-width"] ?? null;
994
994
  if (inheritedStroke == null && inheritedWidth == null) return void 0;
995
995
  const paint = readPaint(style, "stroke", "#000000", gradients, onWarn);
996
996
  if (paint.kind === "none") return void 0;
997
- const width = inheritedWidth != null ? parseFloat(inheritedWidth) : 1;
998
- const stroke = { paint, width };
997
+ const n = inheritedWidth != null ? parseFloat(inheritedWidth) : 1;
998
+ const stroke = { paint, width: nonScaling ? { px: n } : n };
999
999
  const opacityRaw = style["stroke-opacity"] ?? null;
1000
1000
  if (opacityRaw != null) {
1001
1001
  const a = clamp01(parseFloat(opacityRaw));
@@ -1062,6 +1062,9 @@ var STROKE_KEYS = [
1062
1062
  "marker-mid",
1063
1063
  "marker-end"
1064
1064
  ];
1065
+ function isNonScalingStroke(el) {
1066
+ return ownProp(el, "vector-effect") === "non-scaling-stroke";
1067
+ }
1065
1068
  function ownStrokeStyle(el) {
1066
1069
  const out = {};
1067
1070
  for (const k of STROKE_KEYS) {
@@ -1249,7 +1252,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1249
1252
  const ay = m[1] * rawX + m[3] * rawY + m[5];
1250
1253
  const leafStyle = deriveStyle(style, el);
1251
1254
  const textStyle = readTextStyle(leafStyle, onWarn);
1252
- const textPaint = readTextPaint(leafStyle, gradients, onWarn);
1255
+ const textPaint = readTextPaint(leafStyle, gradients, onWarn, isNonScalingStroke(el));
1253
1256
  const fontSize = textStyle.fontSize ?? 16;
1254
1257
  const lineHeight = textStyle.lineHeight ?? 1.2;
1255
1258
  const dominantBaseline = el.getAttribute("dominant-baseline");
@@ -1297,7 +1300,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1297
1300
  );
1298
1301
  if (hasStyling) node.runs = runs;
1299
1302
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
1300
- if (textPaint.fill != null) node.fill = textPaint.fill;
1303
+ if (textPaint.fill !== void 0) node.fill = textPaint.fill;
1301
1304
  if (textPaint.stroke != null) node.stroke = textPaint.stroke;
1302
1305
  if (opacity != null) node.opacity = opacity;
1303
1306
  if (!isIdentity(localTransform)) {
@@ -1357,7 +1360,7 @@ function readTspanRun(el, gradients, style, onWarn) {
1357
1360
  if (paint) run.fill = paint;
1358
1361
  }
1359
1362
  }
1360
- const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn));
1363
+ const stroke = coreStroke(readStroke(ownStrokeStyle(el), gradients, onWarn, isNonScalingStroke(el)));
1361
1364
  if (stroke) run.stroke = stroke;
1362
1365
  return run;
1363
1366
  }
@@ -1394,7 +1397,7 @@ function readTextStyle(style, onWarn) {
1394
1397
  if (decoration.overline) out.overline = true;
1395
1398
  return out;
1396
1399
  }
1397
- function readTextPaint(style, gradients, onWarn) {
1400
+ function readTextPaint(style, gradients, onWarn, nonScalingStroke = false) {
1398
1401
  const out = {};
1399
1402
  const fillRaw = resolveCurrentColor(style["fill"] ?? null, style);
1400
1403
  if (fillRaw) {
@@ -1404,9 +1407,11 @@ function readTextPaint(style, gradients, onWarn) {
1404
1407
  } else if (parsed?.kind === "ref") {
1405
1408
  const paint = gradients.get(parsed.id);
1406
1409
  if (paint) out.fill = paint;
1410
+ } else if (parsed?.kind === "none") {
1411
+ out.fill = null;
1407
1412
  }
1408
1413
  }
1409
- const stroke = coreStroke(readStroke(style, gradients, onWarn));
1414
+ const stroke = coreStroke(readStroke(style, gradients, onWarn, nonScalingStroke));
1410
1415
  if (stroke) out.stroke = stroke;
1411
1416
  return out;
1412
1417
  }
@@ -1606,20 +1611,25 @@ function paintAttrs(paint, name, registry, includeOpacity = true) {
1606
1611
  const id = registry.register(paint.paint);
1607
1612
  return [`${name}="url(#${id})"`];
1608
1613
  }
1614
+ function strokeWidthAttrs(width) {
1615
+ if (typeof width === "number") return [`stroke-width="${trimNumber(width)}"`];
1616
+ return [`stroke-width="${trimNumber(width.px)}"`, 'vector-effect="non-scaling-stroke"'];
1617
+ }
1609
1618
  function coreStrokeAttrs(stroke, registry) {
1610
- if (!stroke) return [];
1611
- const width = resolveStrokeWidth(stroke.width ?? 1, 1);
1612
- if (!(width > 0)) return [];
1619
+ const paint = stroke?.paint;
1620
+ if (!stroke || !paint) return [];
1621
+ const width = stroke.width ?? 1;
1622
+ if (!((typeof width === "object" ? width.px : width) > 0)) return [];
1613
1623
  const attrs = [];
1614
- if ("color" in stroke.paint) {
1615
- attrs.push(`stroke="${stroke.paint.color}"`);
1616
- if (stroke.paint.opacity != null && stroke.paint.opacity !== 1) {
1617
- attrs.push(`stroke-opacity="${trimNumber(stroke.paint.opacity)}"`);
1624
+ if ("color" in paint) {
1625
+ attrs.push(`stroke="${paint.color}"`);
1626
+ if (paint.opacity != null && paint.opacity !== 1) {
1627
+ attrs.push(`stroke-opacity="${trimNumber(paint.opacity)}"`);
1618
1628
  }
1619
1629
  } else {
1620
- attrs.push(`stroke="url(#${registry.register(stroke.paint)})"`);
1630
+ attrs.push(`stroke="url(#${registry.register(paint)})"`);
1621
1631
  }
1622
- attrs.push(`stroke-width="${trimNumber(width)}"`);
1632
+ attrs.push(...strokeWidthAttrs(width));
1623
1633
  if (stroke.cap) attrs.push(`stroke-linecap="${stroke.cap}"`);
1624
1634
  if (stroke.join) attrs.push(`stroke-linejoin="${stroke.join}"`);
1625
1635
  if (stroke.dash && stroke.dash.length > 0) {
@@ -1642,7 +1652,7 @@ function coreStrokeAttrs(stroke, registry) {
1642
1652
  }
1643
1653
  function strokeAttrsFor(stroke, registry) {
1644
1654
  const attrs = paintAttrs(stroke.paint, "stroke", registry, false);
1645
- attrs.push(`stroke-width="${trimNumber(stroke.width)}"`);
1655
+ attrs.push(...strokeWidthAttrs(stroke.width));
1646
1656
  const opacity = stroke.opacity ?? (stroke.paint.kind === "solid" ? stroke.paint.opacity : void 0);
1647
1657
  if (opacity != null && opacity !== 1) {
1648
1658
  attrs.push(`stroke-opacity="${trimNumber(opacity)}"`);
@@ -1735,7 +1745,9 @@ function textXml(node, registry, namespaces) {
1735
1745
  }
1736
1746
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
1737
1747
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
1738
- if (node.fill) {
1748
+ if (node.fill === null) {
1749
+ attrs.push('fill="none"');
1750
+ } else if (node.fill) {
1739
1751
  if ("color" in node.fill) {
1740
1752
  attrs.push(`fill="${node.fill.color}"`);
1741
1753
  if (node.fill.opacity != null && node.fill.opacity !== 1) {
@@ -1855,6 +1867,21 @@ function strokeDataFromSvg(stroke, box) {
1855
1867
  ...stroke.markerEnd !== void 0 ? { markerEnd: stroke.markerEnd } : {}
1856
1868
  };
1857
1869
  }
1870
+ function fillInBoxFrame(fill, box) {
1871
+ const units = "units" in fill ? fill.units : void 0;
1872
+ return units === "world" ? fillToBoundsFrame(fill, box) : fill;
1873
+ }
1874
+ function runInBoxFrame(run, box) {
1875
+ const fill = run.fill ? fillInBoxFrame(run.fill, box) : run.fill;
1876
+ const stroke = run.stroke ? strokeInBoxFrame(run.stroke, box) : run.stroke;
1877
+ if (fill === run.fill && stroke === run.stroke) return run;
1878
+ return { ...run, ...fill ? { fill } : {}, ...stroke ? { stroke } : {} };
1879
+ }
1880
+ function strokeInBoxFrame(stroke, box) {
1881
+ if (!stroke.paint) return stroke;
1882
+ const paint = fillInBoxFrame(stroke.paint, box);
1883
+ return paint === stroke.paint ? stroke : { ...stroke, paint };
1884
+ }
1858
1885
  function svgNodesToKitDrafts(nodes, nextId) {
1859
1886
  const drafts = [];
1860
1887
  const visit = (n, parentId) => {
@@ -1886,12 +1913,26 @@ function svgNodesToKitDrafts(nodes, nextId) {
1886
1913
  height: n.height
1887
1914
  };
1888
1915
  if (n.rotation) pose2.rotation = n.rotation;
1916
+ const box = {
1917
+ x: pose2.x,
1918
+ y: pose2.y,
1919
+ width: pose2.width,
1920
+ height: pose2.height
1921
+ };
1889
1922
  drafts.push({
1890
1923
  kind: "leaf",
1891
1924
  id: nextId(),
1892
1925
  parentId,
1893
1926
  pose: pose2,
1894
- data: { text: n.text, ...n.style ? { style: n.style } : {} }
1927
+ data: {
1928
+ text: n.text,
1929
+ ...n.style ? { style: n.style } : {},
1930
+ ...n.runs ? { runs: n.runs.map((r) => runInBoxFrame(r, box)) } : {},
1931
+ // `!== undefined`, not a truthiness test: `null` is the document
1932
+ // saying `fill="none"`, and absent takes the painter's default.
1933
+ ...n.fill !== void 0 ? { fill: n.fill === null ? null : fillInBoxFrame(n.fill, box) } : {},
1934
+ ...n.stroke !== void 0 ? { stroke: strokeInBoxFrame(n.stroke, box) } : {}
1935
+ }
1895
1936
  });
1896
1937
  return pose2;
1897
1938
  }
@@ -1943,8 +1984,12 @@ function textBoxWidth(n) {
1943
1984
  function scaleTextData(data, scale) {
1944
1985
  if (scale === 1 || typeof data.text !== "string") return data;
1945
1986
  const style = data.style ?? {};
1987
+ const runs = data.runs;
1946
1988
  return {
1947
1989
  ...data,
1990
+ ...runs ? {
1991
+ runs: runs.map((r) => r.fontSize !== void 0 ? { ...r, fontSize: r.fontSize * scale } : r)
1992
+ } : {},
1948
1993
  style: { ...style, fontSize: resolveTextStyle(style).fontSize * scale }
1949
1994
  };
1950
1995
  }