@weasel-js/svg 1.4.4 → 1.5.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
@@ -130,16 +130,6 @@ interface SvgGroupNode {
130
130
  /** Opaque per-element bag for declared namespaces. See `NamespaceMeta`. */
131
131
  meta?: NamespaceMeta;
132
132
  }
133
- /**
134
- * Width given to an external `<text>` that carries no `data-weasel-width`:
135
- * large enough that line wrapping never fires, since SVG text flows from a
136
- * baseline and the source says nothing about a box.
137
- *
138
- * It is a wrap sentinel, not a measurement. Anything that treats a text
139
- * node's width as geometry — a union AABB, a fit-clamp — has to recognize it
140
- * and substitute an estimate, or one text node swamps the whole file.
141
- */
142
- declare const UNBOUNDED_TEXT_WIDTH = 99999;
143
133
  /**
144
134
  * Text leaf node. Mirrors the kit's `TextPose` shape — a bounding box plus
145
135
  * text content, optional rich-text `runs` (per-range styling), and an
@@ -150,9 +140,12 @@ declare const UNBOUNDED_TEXT_WIDTH = 99999;
150
140
  * `dominant-baseline="text-before-edge"` so `y` is the top of the text
151
141
  * box, plus `data-weasel-width` / `data-weasel-height` so weasel's
152
142
  * explicit box dimensions round-trip losslessly. External SVG text
153
- * (lacking the data-* attrs) imports with estimated dimensions —
154
- * {@link UNBOUNDED_TEXT_WIDTH} and
143
+ * (lacking the data-* attrs) imports with the width the kit's text layout
144
+ * measures — an em-based estimate when no registered font can — and
155
145
  * `height = fontSize * lineHeight * (lineCount || 1)`.
146
+ *
147
+ * `x` here is always the box's left edge. In the SVG it is the
148
+ * `text-anchor` point, and the parser and serializer convert between them.
156
149
  */
157
150
  interface SvgTextNode {
158
151
  kind: 'text';
@@ -401,4 +394,4 @@ declare function svgNodesToKitDrafts(nodes: readonly SvgNode[], nextId: () => st
401
394
  */
402
395
  declare function unpackSvgFiles(files: File[], ctx: IngestCtx): Promise<void>;
403
396
 
404
- export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
397
+ export { IDENTITY_MATRIX, type Matrix, type NamespaceMeta, type NamespacedElement, type ParseOptions, type ParseResult, type SerializeOptions, type SvgDraftBounds, type SvgGroupNode, type SvgImageNode, type SvgNode, type SvgPaint, type SvgPathNode, type SvgSceneDraft, type SvgStroke, type SvgTextNode, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
package/dist/index.js CHANGED
@@ -1,13 +1,11 @@
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';
1
+ import { pathFromD, PATH_M, PATH_L, PATH_Z, boundsOfPath, getMarker, SCRIPT_METRICS, dwarn, createInsertOp, PathBuilder, parseColor, rgbaToHex, resolveTextStyle, layoutRuns, resolveRuns, resolveAlign, PATH_C, PATH_Q, solid, fillToBoundsFrame, getPaintKind } from '@weasel-js/core';
2
+ import { invert } from '@weasel-js/geom';
2
3
  import { tileGeometry } from '@weasel-js/core/patterns-builtin';
3
4
 
4
5
  // src/parse.ts
5
6
 
6
7
  // src/types.ts
7
8
  var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
8
- var UNBOUNDED_TEXT_WIDTH = 99999;
9
-
10
- // src/transform.ts
11
9
  function multiply(a, b) {
12
10
  return [
13
11
  a[0] * b[0] + a[2] * b[1],
@@ -78,15 +76,11 @@ function parseTransform(attr, onWarn) {
78
76
  }
79
77
  return result;
80
78
  }
81
- function invertMatrix(m) {
82
- const det = m[0] * m[3] - m[1] * m[2];
83
- if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
84
- const a = m[3] / det, b = -m[1] / det, c = -m[2] / det, d = m[0] / det;
85
- return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
86
- }
87
79
  function rebaseTransform(ctm, local) {
88
- const inv = invertMatrix(ctm);
89
- return inv ? multiply(multiply(ctm, local), inv) : local;
80
+ const inv = invert([...ctm]);
81
+ if (!inv) return null;
82
+ const [a, b, c, d, e, f] = inv;
83
+ return multiply(multiply(ctm, local), [a, b, c, d, e, f]);
90
84
  }
91
85
  function decomposeRotation(m, cx, cy, eps = 1e-4) {
92
86
  const [a, b, c, d, e, f] = m;
@@ -236,6 +230,10 @@ function polygonToPath(points, m = IDENTITY_MATRIX) {
236
230
  b.close();
237
231
  return transformPath(b.build(), m);
238
232
  }
233
+ function anchorOffset(style, width) {
234
+ const edge = resolveAlign(style?.align ?? "left", style?.direction ?? "ltr");
235
+ return edge === "center" ? width / 2 : edge === "right" ? width : 0;
236
+ }
239
237
  function parsePaintAttr(raw) {
240
238
  if (raw == null) return null;
241
239
  const s = raw.trim();
@@ -866,8 +864,10 @@ function parseElement(el, ctm, style, gradients, onWarn, uriToPrefix) {
866
864
  const bounds = pathAabb(path);
867
865
  const cx = bounds.x + bounds.width / 2;
868
866
  const cy = bounds.y + bounds.height / 2;
869
- const angle = decomposeRotation(worldLocal, cx, cy);
870
- if (angle != null) {
867
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
868
+ if (!worldLocal) {
869
+ onWarn("leaf transform sits under a singular parent transform; dropped");
870
+ } else if (angle != null) {
871
871
  rotation = angle;
872
872
  } else {
873
873
  path = transformPath(path, worldLocal);
@@ -1187,8 +1187,10 @@ function parseImageElement(el, ctm, onWarn) {
1187
1187
  const worldLocal = rebaseTransform(ctm, localTransform);
1188
1188
  const cx = node.x + node.width / 2;
1189
1189
  const cy = node.y + node.height / 2;
1190
- const angle = decomposeRotation(worldLocal, cx, cy);
1191
- if (angle != null) {
1190
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1191
+ if (!worldLocal) {
1192
+ onWarn("<image> transform sits under a singular parent transform; dropped");
1193
+ } else if (angle != null) {
1192
1194
  node.rotation = angle;
1193
1195
  } else {
1194
1196
  const rotComp = rotationComponent(worldLocal);
@@ -1238,6 +1240,19 @@ function collapseRunWhitespace(runs) {
1238
1240
  if (runs[i].text === "") runs.splice(i, 1);
1239
1241
  }
1240
1242
  }
1243
+ var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
1244
+ function laidOutWidth(runs, style, plain) {
1245
+ const resolved = resolveTextStyle(style);
1246
+ const { bounds } = layoutRuns(resolveRuns(runs.length > 0 ? runs : [{ text: plain }], resolved), {
1247
+ maxWidth: Infinity,
1248
+ lineHeight: resolved.lineHeight,
1249
+ align: resolved.align,
1250
+ direction: resolved.direction
1251
+ });
1252
+ if (bounds.width > 0) return bounds.width;
1253
+ const longest = plain.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
1254
+ return longest * resolved.fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
1255
+ }
1241
1256
  function parseTextElement(el, ctm, style, gradients, onWarn) {
1242
1257
  const num2 = (raw, fallback) => {
1243
1258
  if (raw == null) return fallback;
@@ -1283,13 +1298,13 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1283
1298
  const plain = runs.map((r) => r.text).join("");
1284
1299
  const dataW = num2(el.getAttribute("data-weasel-width"), NaN);
1285
1300
  const dataH = num2(el.getAttribute("data-weasel-height"), NaN);
1286
- const width = Number.isFinite(dataW) ? dataW : UNBOUNDED_TEXT_WIDTH;
1301
+ const width = Number.isFinite(dataW) ? dataW : laidOutWidth(runs, textStyle, plain);
1287
1302
  const lines = (plain.match(/\n/g)?.length ?? 0) + 1;
1288
1303
  const height = Number.isFinite(dataH) ? dataH : fontSize * lineHeight * lines;
1289
1304
  const opacity = readOpacityAttr(el, "opacity");
1290
1305
  const node = {
1291
1306
  kind: "text",
1292
- x: ax,
1307
+ x: ax - anchorOffset(textStyle, width),
1293
1308
  y: topY,
1294
1309
  width,
1295
1310
  height,
@@ -1299,6 +1314,7 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1299
1314
  (r) => Object.entries(r).some(([k, v]) => k !== "text" && v !== void 0)
1300
1315
  );
1301
1316
  if (hasStyling) node.runs = runs;
1317
+ if (el.getAttribute("data-weasel-wrap") === "true") textStyle.wrap = true;
1302
1318
  if (Object.keys(textStyle).length > 0) node.style = textStyle;
1303
1319
  if (textPaint.fill !== void 0) node.fill = textPaint.fill;
1304
1320
  if (textPaint.stroke != null) node.stroke = textPaint.stroke;
@@ -1307,8 +1323,10 @@ function parseTextElement(el, ctm, style, gradients, onWarn) {
1307
1323
  const worldLocal = rebaseTransform(ctm, localTransform);
1308
1324
  const cx = node.x + node.width / 2;
1309
1325
  const cy = node.y + node.height / 2;
1310
- const angle = decomposeRotation(worldLocal, cx, cy);
1311
- if (angle != null) {
1326
+ const angle = worldLocal && decomposeRotation(worldLocal, cx, cy);
1327
+ if (!worldLocal) {
1328
+ onWarn("<text> transform sits under a singular parent transform; dropped");
1329
+ } else if (angle != null) {
1312
1330
  node.rotation = angle;
1313
1331
  } else {
1314
1332
  const rotComp = rotationComponent(worldLocal);
@@ -1719,7 +1737,7 @@ function rotateAboutCenter(ctm, b, angle) {
1719
1737
  }
1720
1738
  function textXml(node, registry, namespaces) {
1721
1739
  const attrs = [
1722
- `x="${trimNumber(node.x)}"`,
1740
+ `x="${trimNumber(node.x + anchorOffset(node.style, node.width))}"`,
1723
1741
  `y="${trimNumber(node.y)}"`,
1724
1742
  `dominant-baseline="text-before-edge"`,
1725
1743
  // The runs model stores real line breaks; SVG's default whitespace
@@ -1745,6 +1763,7 @@ function textXml(node, registry, namespaces) {
1745
1763
  }
1746
1764
  const decoration = textDecorationValue(style?.underline, style?.strikethrough, style?.overline);
1747
1765
  if (decoration) attrs.push(`text-decoration="${decoration}"`);
1766
+ if (style?.wrap) attrs.push('data-weasel-wrap="true"');
1748
1767
  if (node.fill === null) {
1749
1768
  attrs.push('fill="none"');
1750
1769
  } else if (node.fill) {
@@ -1909,7 +1928,7 @@ function svgNodesToKitDrafts(nodes, nextId) {
1909
1928
  const pose2 = {
1910
1929
  x: n.x,
1911
1930
  y: n.y,
1912
- width: textBoxWidth(n),
1931
+ width: n.width,
1913
1932
  height: n.height
1914
1933
  };
1915
1934
  if (n.rotation) pose2.rotation = n.rotation;
@@ -1974,13 +1993,6 @@ function svgNodesToKitDrafts(nodes, nextId) {
1974
1993
  for (const n of nodes) visit(n, null);
1975
1994
  return drafts;
1976
1995
  }
1977
- var ESTIMATED_GLYPH_ADVANCE_EM = 0.6;
1978
- function textBoxWidth(n) {
1979
- if (n.width !== UNBOUNDED_TEXT_WIDTH) return n.width;
1980
- const fontSize = resolveTextStyle(n.style).fontSize;
1981
- const longest = n.text.split("\n").reduce((max, line) => Math.max(max, line.length), 0);
1982
- return longest * fontSize * ESTIMATED_GLYPH_ADVANCE_EM;
1983
- }
1984
1996
  function scaleTextData(data, scale) {
1985
1997
  if (scale === 1 || typeof data.text !== "string") return data;
1986
1998
  const style = data.style ?? {};
@@ -2064,6 +2076,6 @@ async function unpackSvgFiles(files, ctx) {
2064
2076
  }
2065
2077
  }
2066
2078
 
2067
- export { IDENTITY_MATRIX, UNBOUNDED_TEXT_WIDTH, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2079
+ export { IDENTITY_MATRIX, parseSvg, serializeSvg, strokeDataFromSvg, svgNodesToKitDrafts, tilePreviewCssUrl, tilePreviewSvg, unpackSvgFiles };
2068
2080
  //# sourceMappingURL=index.js.map
2069
2081
  //# sourceMappingURL=index.js.map