@emasoft/svg-matrix 1.0.28 → 1.0.30

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.
Files changed (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -14,16 +14,15 @@
14
14
  * @module use-symbol-resolver
15
15
  */
16
16
 
17
- import Decimal from 'decimal.js';
18
- import { Matrix } from './matrix.js';
19
- import * as Transforms2D from './transforms2d.js';
20
- import * as PolygonClip from './polygon-clip.js';
21
- import * as ClipPathResolver from './clip-path-resolver.js';
17
+ import Decimal from "decimal.js";
18
+ import { Matrix } from "./matrix.js";
19
+ import * as Transforms2D from "./transforms2d.js";
20
+ import * as PolygonClip from "./polygon-clip.js";
21
+ import * as ClipPathResolver from "./clip-path-resolver.js";
22
+ import { parseTransformAttribute } from "./svg-flatten.js";
22
23
 
23
24
  Decimal.set({ precision: 80 });
24
25
 
25
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
26
-
27
26
  /**
28
27
  * Detect circular references when resolving use/symbol references.
29
28
  * Prevents infinite loops when SVG contains circular reference chains like:
@@ -87,19 +86,23 @@ function hasCircularReference(startId, getNextId, maxDepth = 100) {
87
86
  * // }
88
87
  */
89
88
  export function parseUseElement(useElement) {
90
- const href = useElement.getAttribute('href') ||
91
- useElement.getAttribute('xlink:href') || '';
89
+ const href =
90
+ useElement.getAttribute("href") ||
91
+ useElement.getAttribute("xlink:href") ||
92
+ "";
92
93
 
93
94
  return {
94
- href: href.startsWith('#') ? href.slice(1) : href,
95
- x: parseFloat(useElement.getAttribute('x') || '0'),
96
- y: parseFloat(useElement.getAttribute('y') || '0'),
97
- width: useElement.getAttribute('width') ?
98
- parseFloat(useElement.getAttribute('width')) : null,
99
- height: useElement.getAttribute('height') ?
100
- parseFloat(useElement.getAttribute('height')) : null,
101
- transform: useElement.getAttribute('transform') || null,
102
- style: extractStyleAttributes(useElement)
95
+ href: href.startsWith("#") ? href.slice(1) : href,
96
+ x: parseFloat(useElement.getAttribute("x") || "0"),
97
+ y: parseFloat(useElement.getAttribute("y") || "0"),
98
+ width: useElement.getAttribute("width")
99
+ ? parseFloat(useElement.getAttribute("width"))
100
+ : null,
101
+ height: useElement.getAttribute("height")
102
+ ? parseFloat(useElement.getAttribute("height"))
103
+ : null,
104
+ transform: useElement.getAttribute("transform") || null,
105
+ style: extractStyleAttributes(useElement),
103
106
  };
104
107
  }
105
108
 
@@ -144,23 +147,27 @@ export function parseUseElement(useElement) {
144
147
  */
145
148
  export function parseSymbolElement(symbolElement) {
146
149
  const data = {
147
- id: symbolElement.getAttribute('id') || '',
148
- viewBox: symbolElement.getAttribute('viewBox') || null,
149
- preserveAspectRatio: symbolElement.getAttribute('preserveAspectRatio') || 'xMidYMid meet',
150
+ id: symbolElement.getAttribute("id") || "",
151
+ viewBox: symbolElement.getAttribute("viewBox") || null,
152
+ preserveAspectRatio:
153
+ symbolElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
150
154
  children: [],
151
- refX: parseFloat(symbolElement.getAttribute('refX') || '0'),
152
- refY: parseFloat(symbolElement.getAttribute('refY') || '0')
155
+ refX: parseFloat(symbolElement.getAttribute("refX") || "0"),
156
+ refY: parseFloat(symbolElement.getAttribute("refY") || "0"),
153
157
  };
154
158
 
155
159
  // Parse viewBox
156
160
  if (data.viewBox) {
157
- const parts = data.viewBox.trim().split(/[\s,]+/).map(Number);
161
+ const parts = data.viewBox
162
+ .trim()
163
+ .split(/[\s,]+/)
164
+ .map(Number);
158
165
  if (parts.length === 4) {
159
166
  data.viewBoxParsed = {
160
167
  x: parts[0],
161
168
  y: parts[1],
162
169
  width: parts[2],
163
- height: parts[3]
170
+ height: parts[3],
164
171
  };
165
172
  }
166
173
  }
@@ -239,61 +246,66 @@ export function parseChildElement(element) {
239
246
 
240
247
  const data = {
241
248
  type: tagName,
242
- id: element.getAttribute('id') || null,
243
- transform: element.getAttribute('transform') || null,
244
- style: extractStyleAttributes(element)
249
+ id: element.getAttribute("id") || null,
250
+ transform: element.getAttribute("transform") || null,
251
+ style: extractStyleAttributes(element),
245
252
  };
246
253
 
247
254
  switch (tagName) {
248
- case 'rect':
249
- data.x = parseFloat(element.getAttribute('x') || '0');
250
- data.y = parseFloat(element.getAttribute('y') || '0');
251
- data.width = parseFloat(element.getAttribute('width') || '0');
252
- data.height = parseFloat(element.getAttribute('height') || '0');
253
- data.rx = parseFloat(element.getAttribute('rx') || '0');
254
- data.ry = parseFloat(element.getAttribute('ry') || '0');
255
+ case "rect":
256
+ data.x = parseFloat(element.getAttribute("x") || "0");
257
+ data.y = parseFloat(element.getAttribute("y") || "0");
258
+ data.width = parseFloat(element.getAttribute("width") || "0");
259
+ data.height = parseFloat(element.getAttribute("height") || "0");
260
+ data.rx = parseFloat(element.getAttribute("rx") || "0");
261
+ data.ry = parseFloat(element.getAttribute("ry") || "0");
255
262
  break;
256
- case 'circle':
257
- data.cx = parseFloat(element.getAttribute('cx') || '0');
258
- data.cy = parseFloat(element.getAttribute('cy') || '0');
259
- data.r = parseFloat(element.getAttribute('r') || '0');
263
+ case "circle":
264
+ data.cx = parseFloat(element.getAttribute("cx") || "0");
265
+ data.cy = parseFloat(element.getAttribute("cy") || "0");
266
+ data.r = parseFloat(element.getAttribute("r") || "0");
260
267
  break;
261
- case 'ellipse':
262
- data.cx = parseFloat(element.getAttribute('cx') || '0');
263
- data.cy = parseFloat(element.getAttribute('cy') || '0');
264
- data.rx = parseFloat(element.getAttribute('rx') || '0');
265
- data.ry = parseFloat(element.getAttribute('ry') || '0');
268
+ case "ellipse":
269
+ data.cx = parseFloat(element.getAttribute("cx") || "0");
270
+ data.cy = parseFloat(element.getAttribute("cy") || "0");
271
+ data.rx = parseFloat(element.getAttribute("rx") || "0");
272
+ data.ry = parseFloat(element.getAttribute("ry") || "0");
266
273
  break;
267
- case 'path':
268
- data.d = element.getAttribute('d') || '';
274
+ case "path":
275
+ data.d = element.getAttribute("d") || "";
269
276
  break;
270
- case 'polygon':
271
- data.points = element.getAttribute('points') || '';
277
+ case "polygon":
278
+ data.points = element.getAttribute("points") || "";
272
279
  break;
273
- case 'polyline':
274
- data.points = element.getAttribute('points') || '';
280
+ case "polyline":
281
+ data.points = element.getAttribute("points") || "";
275
282
  break;
276
- case 'line':
277
- data.x1 = parseFloat(element.getAttribute('x1') || '0');
278
- data.y1 = parseFloat(element.getAttribute('y1') || '0');
279
- data.x2 = parseFloat(element.getAttribute('x2') || '0');
280
- data.y2 = parseFloat(element.getAttribute('y2') || '0');
283
+ case "line":
284
+ data.x1 = parseFloat(element.getAttribute("x1") || "0");
285
+ data.y1 = parseFloat(element.getAttribute("y1") || "0");
286
+ data.x2 = parseFloat(element.getAttribute("x2") || "0");
287
+ data.y2 = parseFloat(element.getAttribute("y2") || "0");
281
288
  break;
282
- case 'g':
289
+ case "g":
283
290
  data.children = [];
284
291
  for (const child of element.children) {
285
292
  data.children.push(parseChildElement(child));
286
293
  }
287
294
  break;
288
- case 'use':
289
- data.href = (element.getAttribute('href') ||
290
- element.getAttribute('xlink:href') || '').replace('#', '');
291
- data.x = parseFloat(element.getAttribute('x') || '0');
292
- data.y = parseFloat(element.getAttribute('y') || '0');
293
- data.width = element.getAttribute('width') ?
294
- parseFloat(element.getAttribute('width')) : null;
295
- data.height = element.getAttribute('height') ?
296
- parseFloat(element.getAttribute('height')) : null;
295
+ case "use":
296
+ data.href = (
297
+ element.getAttribute("href") ||
298
+ element.getAttribute("xlink:href") ||
299
+ ""
300
+ ).replace("#", "");
301
+ data.x = parseFloat(element.getAttribute("x") || "0");
302
+ data.y = parseFloat(element.getAttribute("y") || "0");
303
+ data.width = element.getAttribute("width")
304
+ ? parseFloat(element.getAttribute("width"))
305
+ : null;
306
+ data.height = element.getAttribute("height")
307
+ ? parseFloat(element.getAttribute("height"))
308
+ : null;
297
309
  break;
298
310
  }
299
311
 
@@ -340,14 +352,14 @@ export function parseChildElement(element) {
340
352
  */
341
353
  export function extractStyleAttributes(element) {
342
354
  return {
343
- fill: element.getAttribute('fill'),
344
- stroke: element.getAttribute('stroke'),
345
- strokeWidth: element.getAttribute('stroke-width'),
346
- opacity: element.getAttribute('opacity'),
347
- fillOpacity: element.getAttribute('fill-opacity'),
348
- strokeOpacity: element.getAttribute('stroke-opacity'),
349
- visibility: element.getAttribute('visibility'),
350
- display: element.getAttribute('display')
355
+ fill: element.getAttribute("fill"),
356
+ stroke: element.getAttribute("stroke"),
357
+ strokeWidth: element.getAttribute("stroke-width"),
358
+ opacity: element.getAttribute("opacity"),
359
+ fillOpacity: element.getAttribute("fill-opacity"),
360
+ strokeOpacity: element.getAttribute("stroke-opacity"),
361
+ visibility: element.getAttribute("visibility"),
362
+ display: element.getAttribute("display"),
351
363
  };
352
364
  }
353
365
 
@@ -398,7 +410,12 @@ export function extractStyleAttributes(element) {
398
410
  * // Result: uniform scale of 2.0 (max of 200/100, 150/100)
399
411
  * // Centered: translate(0, -25) then scale(2, 2) - height extends beyond viewport
400
412
  */
401
- export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, preserveAspectRatio = 'xMidYMid meet') {
413
+ export function calculateViewBoxTransform(
414
+ viewBox,
415
+ targetWidth,
416
+ targetHeight,
417
+ preserveAspectRatio = "xMidYMid meet",
418
+ ) {
402
419
  if (!viewBox || !targetWidth || !targetHeight) {
403
420
  return Matrix.identity(3);
404
421
  }
@@ -414,15 +431,16 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
414
431
 
415
432
  // Parse preserveAspectRatio
416
433
  const parts = preserveAspectRatio.trim().split(/\s+/);
417
- const align = parts[0] || 'xMidYMid';
418
- const meetOrSlice = parts[1] || 'meet';
434
+ const align = parts[0] || "xMidYMid";
435
+ const meetOrSlice = parts[1] || "meet";
419
436
 
420
- if (align === 'none') {
437
+ if (align === "none") {
421
438
  // Non-uniform scaling
422
439
  const scaleX = targetWidth / vbW;
423
440
  const scaleY = targetHeight / vbH;
424
- return Transforms2D.translation(-vbX * scaleX, -vbY * scaleY)
425
- .mul(Transforms2D.scale(scaleX, scaleY));
441
+ return Transforms2D.translation(-vbX * scaleX, -vbY * scaleY).mul(
442
+ Transforms2D.scale(scaleX, scaleY),
443
+ );
426
444
  }
427
445
 
428
446
  // Uniform scaling
@@ -430,7 +448,7 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
430
448
  const scaleY = targetHeight / vbH;
431
449
  let scale;
432
450
 
433
- if (meetOrSlice === 'slice') {
451
+ if (meetOrSlice === "slice") {
434
452
  scale = Math.max(scaleX, scaleY);
435
453
  } else {
436
454
  scale = Math.min(scaleX, scaleY);
@@ -444,21 +462,20 @@ export function calculateViewBoxTransform(viewBox, targetWidth, targetHeight, pr
444
462
  const scaledHeight = vbH * scale;
445
463
 
446
464
  // X alignment
447
- if (align.includes('xMid')) {
465
+ if (align.includes("xMid")) {
448
466
  tx += (targetWidth - scaledWidth) / 2;
449
- } else if (align.includes('xMax')) {
467
+ } else if (align.includes("xMax")) {
450
468
  tx += targetWidth - scaledWidth;
451
469
  }
452
470
 
453
471
  // Y alignment
454
- if (align.includes('YMid')) {
472
+ if (align.includes("YMid")) {
455
473
  ty += (targetHeight - scaledHeight) / 2;
456
- } else if (align.includes('YMax')) {
474
+ } else if (align.includes("YMax")) {
457
475
  ty += targetHeight - scaledHeight;
458
476
  }
459
477
 
460
- return Transforms2D.translation(tx, ty)
461
- .mul(Transforms2D.scale(scale, scale));
478
+ return Transforms2D.translation(tx, ty).mul(Transforms2D.scale(scale, scale));
462
479
  }
463
480
 
464
481
  /**
@@ -557,13 +574,13 @@ export function resolveUse(useData, defs, options = {}) {
557
574
  // Pre-multiply by use element's transform if present (step 1)
558
575
  // This makes useTransform apply FIRST, then translation
559
576
  if (useData.transform) {
560
- const useTransform = ClipPathResolver.parseTransform(useData.transform);
577
+ const useTransform = parseTransformAttribute(useData.transform);
561
578
  transform = transform.mul(useTransform);
562
579
  }
563
580
 
564
581
  // Handle symbol with viewBox (step 3)
565
582
  // ViewBox transform applies LAST (after translation and useTransform)
566
- if (target.type === 'symbol' && target.viewBoxParsed) {
583
+ if (target.type === "symbol" && target.viewBoxParsed) {
567
584
  const width = useData.width || target.viewBoxParsed.width;
568
585
  const height = useData.height || target.viewBoxParsed.height;
569
586
 
@@ -571,7 +588,7 @@ export function resolveUse(useData, defs, options = {}) {
571
588
  target.viewBoxParsed,
572
589
  width,
573
590
  height,
574
- target.preserveAspectRatio
591
+ target.preserveAspectRatio,
575
592
  );
576
593
 
577
594
  // ViewBox transform is applied LAST, so it's the leftmost in multiplication
@@ -583,7 +600,7 @@ export function resolveUse(useData, defs, options = {}) {
583
600
  const children = target.children || [target];
584
601
 
585
602
  for (const child of children) {
586
- if (child.type === 'use') {
603
+ if (child.type === "use") {
587
604
  // Recursive resolution
588
605
  const resolved = resolveUse(child, defs, { maxDepth: maxDepth - 1 });
589
606
  if (resolved) {
@@ -592,7 +609,7 @@ export function resolveUse(useData, defs, options = {}) {
592
609
  } else {
593
610
  resolvedChildren.push({
594
611
  element: child,
595
- transform: Matrix.identity(3)
612
+ transform: Matrix.identity(3),
596
613
  });
597
614
  }
598
615
  }
@@ -601,7 +618,7 @@ export function resolveUse(useData, defs, options = {}) {
601
618
  element: target,
602
619
  transform,
603
620
  children: resolvedChildren,
604
- inheritedStyle: useData.style
621
+ inheritedStyle: useData.style,
605
622
  };
606
623
  }
607
624
 
@@ -670,8 +687,12 @@ export function flattenResolvedUse(resolved, samples = 20) {
670
687
  // Recursive flattening
671
688
  const nested = flattenResolvedUse(child, samples);
672
689
  for (const n of nested) {
673
- n.polygon = n.polygon.map(p => {
674
- const [x, y] = Transforms2D.applyTransform(resolved.transform, p.x, p.y);
690
+ n.polygon = n.polygon.map((p) => {
691
+ const [x, y] = Transforms2D.applyTransform(
692
+ resolved.transform,
693
+ p.x,
694
+ p.y,
695
+ );
675
696
  return { x, y };
676
697
  });
677
698
  results.push(n);
@@ -682,7 +703,7 @@ export function flattenResolvedUse(resolved, samples = 20) {
682
703
  if (polygon.length >= 3) {
683
704
  results.push({
684
705
  polygon,
685
- style: mergeStyles(resolved.inheritedStyle, element.style)
706
+ style: mergeStyles(resolved.inheritedStyle, element.style),
686
707
  });
687
708
  }
688
709
  }
@@ -731,7 +752,7 @@ export function elementToPolygon(element, transform, samples = 20) {
731
752
 
732
753
  // Apply transform
733
754
  if (transform && polygon.length > 0) {
734
- polygon = polygon.map(p => {
755
+ polygon = polygon.map((p) => {
735
756
  const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
736
757
  return { x, y };
737
758
  });
@@ -867,7 +888,7 @@ export function getResolvedBBox(resolved, samples = 20) {
867
888
  x: minX,
868
889
  y: minY,
869
890
  width: maxX - minX,
870
- height: maxY - minY
891
+ height: maxY - minY,
871
892
  };
872
893
  }
873
894
 
@@ -929,7 +950,7 @@ export function clipResolvedUse(resolved, clipPolygon, samples = 20) {
929
950
  if (clippedPoly.length >= 3) {
930
951
  result.push({
931
952
  polygon: clippedPoly,
932
- style
953
+ style,
933
954
  });
934
955
  }
935
956
  }
@@ -993,19 +1014,19 @@ export function resolvedUseToPathData(resolved, samples = 20) {
993
1014
 
994
1015
  for (const { polygon } of polygons) {
995
1016
  if (polygon.length >= 3) {
996
- let d = '';
1017
+ let d = "";
997
1018
  for (let i = 0; i < polygon.length; i++) {
998
1019
  const p = polygon[i];
999
1020
  const x = Number(p.x).toFixed(6);
1000
1021
  const y = Number(p.y).toFixed(6);
1001
1022
  d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
1002
1023
  }
1003
- d += ' Z';
1024
+ d += " Z";
1004
1025
  paths.push(d);
1005
1026
  }
1006
1027
  }
1007
1028
 
1008
- return paths.join(' ');
1029
+ return paths.join(" ");
1009
1030
  }
1010
1031
 
1011
1032
  /**
@@ -1063,15 +1084,15 @@ export function buildDefsMap(svgRoot) {
1063
1084
  const defs = {};
1064
1085
 
1065
1086
  // Find all elements with id
1066
- const elementsWithId = svgRoot.querySelectorAll('[id]');
1087
+ const elementsWithId = svgRoot.querySelectorAll("[id]");
1067
1088
 
1068
1089
  for (const element of elementsWithId) {
1069
- const id = element.getAttribute('id');
1090
+ const id = element.getAttribute("id");
1070
1091
  const tagName = element.tagName.toLowerCase();
1071
1092
 
1072
- if (tagName === 'symbol') {
1093
+ if (tagName === "symbol") {
1073
1094
  defs[id] = parseSymbolElement(element);
1074
- defs[id].type = 'symbol';
1095
+ defs[id].type = "symbol";
1075
1096
  } else {
1076
1097
  defs[id] = parseChildElement(element);
1077
1098
  }
@@ -1146,7 +1167,7 @@ export function buildDefsMap(svgRoot) {
1146
1167
  */
1147
1168
  export function resolveAllUses(svgRoot, options = {}) {
1148
1169
  const defs = buildDefsMap(svgRoot);
1149
- const useElements = svgRoot.querySelectorAll('use');
1170
+ const useElements = svgRoot.querySelectorAll("use");
1150
1171
  const resolved = [];
1151
1172
 
1152
1173
  // Helper to get the next use reference from a definition
@@ -1155,14 +1176,14 @@ export function resolveAllUses(svgRoot, options = {}) {
1155
1176
  if (!target) return null;
1156
1177
 
1157
1178
  // Check if target is itself a use element
1158
- if (target.type === 'use') {
1179
+ if (target.type === "use") {
1159
1180
  return target.href;
1160
1181
  }
1161
1182
 
1162
1183
  // Check if target contains use elements in its children
1163
1184
  if (target.children && target.children.length > 0) {
1164
1185
  for (const child of target.children) {
1165
- if (child.type === 'use') {
1186
+ if (child.type === "use") {
1166
1187
  return child.href;
1167
1188
  }
1168
1189
  }
@@ -1176,7 +1197,9 @@ export function resolveAllUses(svgRoot, options = {}) {
1176
1197
 
1177
1198
  // Check for circular reference before attempting to resolve
1178
1199
  if (hasCircularReference(useData.href, getUseRef)) {
1179
- console.warn(`Circular use reference detected: #${useData.href}, skipping resolution`);
1200
+ console.warn(
1201
+ `Circular use reference detected: #${useData.href}, skipping resolution`,
1202
+ );
1180
1203
  continue;
1181
1204
  }
1182
1205
 
@@ -1185,10 +1208,27 @@ export function resolveAllUses(svgRoot, options = {}) {
1185
1208
  resolved.push({
1186
1209
  original: useEl,
1187
1210
  useData,
1188
- resolved: result
1211
+ resolved: result,
1189
1212
  });
1190
1213
  }
1191
1214
  }
1192
1215
 
1193
1216
  return resolved;
1194
1217
  }
1218
+
1219
+ export default {
1220
+ parseUseElement,
1221
+ parseSymbolElement,
1222
+ parseChildElement,
1223
+ extractStyleAttributes,
1224
+ calculateViewBoxTransform,
1225
+ resolveUse,
1226
+ flattenResolvedUse,
1227
+ elementToPolygon,
1228
+ mergeStyles,
1229
+ getResolvedBBox,
1230
+ clipResolvedUse,
1231
+ resolvedUseToPathData,
1232
+ buildDefsMap,
1233
+ resolveAllUses,
1234
+ };
package/src/vector.js CHANGED
@@ -15,6 +15,10 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
15
15
  * Supports basic operations (add, sub, scale), products (dot, cross, outer),
16
16
  * and geometric operations (norm, normalize, angle, projection).
17
17
  *
18
+ * @class
19
+ * @property {Array<Decimal>} data - Array of Decimal vector components
20
+ * @property {number} length - Number of components in the vector
21
+ *
18
22
  * @example
19
23
  * const v = Vector.from([1, 2, 3]);
20
24
  * const w = Vector.from(['1.5', '2.5', '3.5']);
@@ -83,8 +87,12 @@ export class Vector {
83
87
  * @throws {Error} If other is not a Vector or dimensions mismatch
84
88
  */
85
89
  add(other) {
86
- if (!(other instanceof Vector)) throw new Error('add expects Vector');
87
- if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
90
+ if (!other || !(other instanceof Vector)) {
91
+ throw new Error('add: argument must be a Vector');
92
+ }
93
+ if (this.length !== other.length) {
94
+ throw new Error(`add: dimension mismatch (${this.length} vs ${other.length})`);
95
+ }
88
96
  return new Vector(this.data.map((v, i) => v.plus(other.data[i])));
89
97
  }
90
98
 
@@ -95,8 +103,12 @@ export class Vector {
95
103
  * @throws {Error} If other is not a Vector or dimensions mismatch
96
104
  */
97
105
  sub(other) {
98
- if (!(other instanceof Vector)) throw new Error('sub expects Vector');
99
- if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
106
+ if (!other || !(other instanceof Vector)) {
107
+ throw new Error('sub: argument must be a Vector');
108
+ }
109
+ if (this.length !== other.length) {
110
+ throw new Error(`sub: dimension mismatch (${this.length} vs ${other.length})`);
111
+ }
100
112
  return new Vector(this.data.map((v, i) => v.minus(other.data[i])));
101
113
  }
102
114
 
@@ -125,8 +137,12 @@ export class Vector {
125
137
  * @throws {Error} If other is not a Vector or dimensions mismatch
126
138
  */
127
139
  dot(other) {
128
- if (!(other instanceof Vector)) throw new Error('dot expects Vector');
129
- if (other.length !== this.length) throw new Error('shape mismatch: vectors must have same length');
140
+ if (!other || !(other instanceof Vector)) {
141
+ throw new Error('dot: argument must be a Vector');
142
+ }
143
+ if (this.length !== other.length) {
144
+ throw new Error(`dot: dimension mismatch (${this.length} vs ${other.length})`);
145
+ }
130
146
  return this.data.reduce((acc, v, i) => acc.plus(v.mul(other.data[i])), new Decimal(0));
131
147
  }
132
148
 
@@ -143,7 +159,9 @@ export class Vector {
143
159
  * @throws {Error} If other is not a Vector
144
160
  */
145
161
  outer(other) {
146
- if (!(other instanceof Vector)) throw new Error('outer expects Vector');
162
+ if (!other || !(other instanceof Vector)) {
163
+ throw new Error('outer: argument must be a Vector');
164
+ }
147
165
  const rows = this.length, cols = other.length;
148
166
  const out = Array.from({ length: rows }, (_, i) =>
149
167
  Array.from({ length: cols }, (_, j) => this.data[i].mul(other.data[j]))
@@ -155,10 +173,15 @@ export class Vector {
155
173
  * Cross product for 3D vectors only.
156
174
  * @param {Vector} other - 3D Vector to compute cross product with
157
175
  * @returns {Vector} New 3D Vector perpendicular to both inputs
158
- * @throws {Error} If either vector is not 3D
176
+ * @throws {Error} If other is not a Vector or either vector is not 3D
159
177
  */
160
178
  cross(other) {
161
- if (this.length !== 3 || other.length !== 3) throw new Error('cross product requires 3D vectors');
179
+ if (!other || !(other instanceof Vector)) {
180
+ throw new Error('cross: argument must be a Vector');
181
+ }
182
+ if (this.length !== 3 || other.length !== 3) {
183
+ throw new Error(`cross: requires 3D vectors (got ${this.length}D and ${other.length}D)`);
184
+ }
162
185
  const [a1, a2, a3] = this.data;
163
186
  const [b1, b2, b3] = other.data;
164
187
  return new Vector([
@@ -193,15 +216,23 @@ export class Vector {
193
216
  * Angle between this vector and another (in radians).
194
217
  * @param {Vector} other - Vector to compute angle with
195
218
  * @returns {Decimal} Angle in radians as a Decimal
196
- * @throws {Error} If either vector is zero
219
+ * @throws {Error} If other is not a Vector, dimensions mismatch, or either vector is zero
197
220
  */
198
221
  angleBetween(other) {
222
+ if (!other || !(other instanceof Vector)) {
223
+ throw new Error('angleBetween: argument must be a Vector');
224
+ }
225
+ if (this.length !== other.length) {
226
+ throw new Error(`angleBetween: dimension mismatch (${this.length} vs ${other.length})`);
227
+ }
199
228
  const dotProduct = this.dot(other);
200
229
  const n1 = this.norm();
201
230
  const n2 = other.norm();
202
- if (n1.isZero() || n2.isZero()) throw new Error('Angle with zero vector is undefined');
231
+ if (n1.isZero() || n2.isZero()) {
232
+ throw new Error('angleBetween: angle with zero vector is undefined');
233
+ }
203
234
  // Clamp cosine to [-1, 1] for numerical safety
204
- let cosv = dotProduct.div(n1.mul(n2));
235
+ const cosv = dotProduct.div(n1.mul(n2));
205
236
  const cosNum = cosv.toNumber();
206
237
  const clamped = Math.min(1, Math.max(-1, cosNum));
207
238
  return new Decimal(Math.acos(clamped));
@@ -211,11 +242,19 @@ export class Vector {
211
242
  * Project this vector onto another vector.
212
243
  * @param {Vector} other - Vector to project onto
213
244
  * @returns {Vector} The projection of this onto other
214
- * @throws {Error} If other is zero vector
245
+ * @throws {Error} If other is not a Vector, dimensions mismatch, or other is zero vector
215
246
  */
216
247
  projectOnto(other) {
248
+ if (!other || !(other instanceof Vector)) {
249
+ throw new Error('projectOnto: argument must be a Vector');
250
+ }
251
+ if (this.length !== other.length) {
252
+ throw new Error(`projectOnto: dimension mismatch (${this.length} vs ${other.length})`);
253
+ }
217
254
  const denom = other.dot(other);
218
- if (denom.isZero()) throw new Error('Cannot project onto zero vector');
255
+ if (denom.isZero()) {
256
+ throw new Error('projectOnto: cannot project onto zero vector');
257
+ }
219
258
  const coef = this.dot(other).div(denom);
220
259
  return other.scale(coef);
221
260
  }
@@ -250,8 +289,15 @@ export class Vector {
250
289
  * Check if this vector is orthogonal to another.
251
290
  * @param {Vector} other - Vector to check orthogonality with
252
291
  * @returns {boolean} True if vectors are orthogonal (dot product is zero)
292
+ * @throws {Error} If other is not a Vector or dimensions mismatch
253
293
  */
254
294
  isOrthogonalTo(other) {
295
+ if (!other || !(other instanceof Vector)) {
296
+ throw new Error('isOrthogonalTo: argument must be a Vector');
297
+ }
298
+ if (this.length !== other.length) {
299
+ throw new Error(`isOrthogonalTo: dimension mismatch (${this.length} vs ${other.length})`);
300
+ }
255
301
  return this.dot(other).isZero();
256
302
  }
257
303
 
@@ -259,9 +305,15 @@ export class Vector {
259
305
  * Euclidean distance to another vector.
260
306
  * @param {Vector} other - Vector to compute distance to
261
307
  * @returns {Decimal} The Euclidean distance
262
- * @throws {Error} If dimensions mismatch
308
+ * @throws {Error} If other is not a Vector or dimensions mismatch
263
309
  */
264
310
  distance(other) {
311
+ if (!other || !(other instanceof Vector)) {
312
+ throw new Error('distance: argument must be a Vector');
313
+ }
314
+ if (this.length !== other.length) {
315
+ throw new Error(`distance: dimension mismatch (${this.length} vs ${other.length})`);
316
+ }
265
317
  return this.sub(other).norm();
266
318
  }
267
319