@emasoft/svg-matrix 1.2.1 → 1.3.1

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/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * SVG path conversion, and 2D/3D affine transformations using Decimal.js.
6
6
  *
7
7
  * @module @emasoft/svg-matrix
8
- * @version 1.2.1
8
+ * @version 1.3.1
9
9
  * @license MIT
10
10
  *
11
11
  * @example
@@ -135,7 +135,7 @@ Decimal.set({ precision: 80 });
135
135
  * Library version
136
136
  * @constant {string}
137
137
  */
138
- export const VERSION = '1.2.1';
138
+ export const VERSION = "1.3.1";
139
139
 
140
140
  /**
141
141
  * Default precision for path output (decimal places)
@@ -215,8 +215,8 @@ export function getNodeTypes(element) {
215
215
  const nodeTypes = element.getAttribute("sodipodi:nodetypes");
216
216
  if (!nodeTypes) return null;
217
217
 
218
- // Validate format: should only contain c, s, z, a characters
219
- if (!/^[csza]+$/i.test(nodeTypes)) return null;
218
+ // Validate format: should only contain c, s, z, a characters (case-sensitive)
219
+ if (!/^[csza]+$/.test(nodeTypes)) return null;
220
220
 
221
221
  return nodeTypes;
222
222
  }
@@ -44,8 +44,6 @@
44
44
  */
45
45
 
46
46
  import Decimal from "decimal.js";
47
- import { Matrix as _Matrix } from "./matrix.js";
48
- import * as _Transforms2D from "./transforms2d.js";
49
47
  import * as PolygonClip from "./polygon-clip.js";
50
48
  import * as ClipPathResolver from "./clip-path-resolver.js";
51
49
  import * as MeshGradient from "./mesh-gradient.js";
@@ -1721,10 +1719,15 @@ export function clipWithMeshGradientShape(
1721
1719
  }
1722
1720
 
1723
1721
  // Union all patch polygons to get the complete mesh shape
1724
- let meshShape = meshPolygons[0].polygon;
1725
- if (!meshShape || meshShape.length < 3) {
1722
+ // Validate first polygon before use
1723
+ if (
1724
+ !meshPolygons[0] ||
1725
+ !meshPolygons[0].polygon ||
1726
+ meshPolygons[0].polygon.length < 3
1727
+ ) {
1726
1728
  return [];
1727
1729
  }
1730
+ let meshShape = meshPolygons[0].polygon;
1728
1731
 
1729
1732
  for (let i = 1; i < meshPolygons.length; i++) {
1730
1733
  if (
@@ -1817,10 +1820,15 @@ export function meshGradientToClipPath(meshData, options = {}) {
1817
1820
  }
1818
1821
 
1819
1822
  // Union all patches into one shape
1820
- let result = meshPolygons[0].polygon;
1821
- if (!result || result.length < 3) {
1823
+ // Validate first polygon before use
1824
+ if (
1825
+ !meshPolygons[0] ||
1826
+ !meshPolygons[0].polygon ||
1827
+ meshPolygons[0].polygon.length < 3
1828
+ ) {
1822
1829
  return [];
1823
1830
  }
1831
+ let result = meshPolygons[0].polygon;
1824
1832
 
1825
1833
  for (let i = 1; i < meshPolygons.length; i++) {
1826
1834
  if (!meshPolygons[i] || !meshPolygons[i].polygon) continue; // Skip invalid polygons
package/src/matrix.js CHANGED
@@ -96,7 +96,7 @@ export class Matrix {
96
96
  throw new Error("size must be a positive integer");
97
97
  const out = Array.from({ length: n }, (_, i) =>
98
98
  Array.from({ length: n }, (_, j) =>
99
- i === j ? new Decimal(1) : new Decimal(0),
99
+ (i === j ? new Decimal(1) : new Decimal(0)),
100
100
  ),
101
101
  );
102
102
  return new Matrix(out);
@@ -400,11 +400,11 @@ export class Matrix {
400
400
  // Create augmented matrix [A | I]
401
401
  const aug = Array.from({ length: n }, (_, i) =>
402
402
  Array.from({ length: 2 * n }, (_, j) =>
403
- j < n
403
+ (j < n
404
404
  ? new Decimal(this.data[i][j])
405
405
  : j - n === i
406
406
  ? new Decimal(1)
407
- : new Decimal(0),
407
+ : new Decimal(0)),
408
408
  ),
409
409
  );
410
410
  // Gauss-Jordan elimination
@@ -8,8 +8,6 @@
8
8
  */
9
9
 
10
10
  import Decimal from "decimal.js";
11
- import { Matrix as _Matrix } from "./matrix.js";
12
- import * as _Transforms2D from "./transforms2d.js";
13
11
  import * as PolygonClip from "./polygon-clip.js";
14
12
 
15
13
  Decimal.set({ precision: 80 });
@@ -501,6 +499,49 @@ export function splitBezier(curve) {
501
499
  ];
502
500
  }
503
501
 
502
+ /**
503
+ * Sample a cubic Bezier curve at regular intervals to generate an array of points.
504
+ *
505
+ * Evaluates the curve at `samples` evenly-spaced t values from 0 to 1,
506
+ * producing a polyline approximation of the curve. This is useful for
507
+ * converting bezier curves to polygon representations for clipping operations.
508
+ *
509
+ * @param {Array<{x: Decimal, y: Decimal}>} curve - Array of 4 control points [p0, p1, p2, p3]
510
+ * @param {number} [samples=20] - Number of sample points to generate (minimum 2)
511
+ * @returns {Array<{x: Decimal, y: Decimal}>} Array of points sampled along the curve
512
+ *
513
+ * @example
514
+ * // Sample a curve with 10 points
515
+ * const curve = [point(0,0), point(100,200), point(200,200), point(300,0)];
516
+ * const points = sampleBezierCurve(curve, 10);
517
+ * // Returns: 10 points evenly distributed along the curve from t=0 to t=1
518
+ *
519
+ * @example
520
+ * // High-resolution sampling for smooth approximation
521
+ * const points = sampleBezierCurve(curve, 50);
522
+ * // Returns: 50 points for a smoother polyline representation
523
+ */
524
+ export function sampleBezierCurve(curve, samples = 20) {
525
+ if (!Array.isArray(curve))
526
+ throw new Error("sampleBezierCurve: curve must be an array");
527
+ if (curve.length !== 4)
528
+ throw new Error(
529
+ `sampleBezierCurve: curve must have exactly 4 points, got ${curve.length}`,
530
+ );
531
+ if (typeof samples !== "number" || samples < 2)
532
+ throw new Error("sampleBezierCurve: samples must be a number >= 2");
533
+
534
+ const [p0, p1, p2, p3] = curve;
535
+ const points = [];
536
+
537
+ for (let i = 0; i < samples; i++) {
538
+ const t = i / (samples - 1); // t goes from 0 to 1 inclusive
539
+ points.push(evalCubicBezier(p0, p1, p2, p3, t));
540
+ }
541
+
542
+ return points;
543
+ }
544
+
504
545
  // ============================================================================
505
546
  // Coons Patch Evaluation
506
547
  // ============================================================================
@@ -795,7 +795,7 @@ export function pathBoundingBox(pathCommands) {
795
795
  }
796
796
  break;
797
797
 
798
- case "A": // Arc (proper extrema calculation)
798
+ case "A": // Arc (conservative bounding box using extrema bounds)
799
799
  {
800
800
  // Validate required properties (WHY: prevent accessing undefined properties)
801
801
  if (
@@ -825,30 +825,22 @@ export function pathBoundingBox(pathCommands) {
825
825
  maxY = Decimal.max(maxY, currentY, y);
826
826
  samplePoints.push({ x: currentX, y: currentY }, { x, y });
827
827
  } else {
828
- // For proper arc bounding box, include start, end, and potential extrema
829
- // Start and end points
830
- minX = Decimal.min(minX, currentX, x);
831
- minY = Decimal.min(minY, currentY, y);
832
- maxX = Decimal.max(maxX, currentX, x);
833
- maxY = Decimal.max(maxY, currentY, y);
834
-
835
- // For a complete solution, we'd need to:
836
- // 1. Convert to center parameterization (complex for arbitrary precision)
837
- // 2. Check if extrema angles (0°, 90°, 180°, 270°) fall within arc sweep
838
- // 3. Include those extrema in bbox
839
- // For now, conservatively expand bbox by radii to ensure containment
840
- // This may overestimate slightly but guarantees correctness
841
- const centerX = currentX.plus(x).div(2);
842
- const centerY = currentY.plus(y).div(2);
843
- minX = Decimal.min(minX, centerX.minus(rx));
844
- minY = Decimal.min(minY, centerY.minus(ry));
845
- maxX = Decimal.max(maxX, centerX.plus(rx));
846
- maxY = Decimal.max(maxY, centerY.plus(ry));
847
-
848
- // Sample arc for verification points
828
+ // FIX: Use conservative bounds that guarantee containment (WHY: exact arc center requires complex calculations)
829
+ // The tightest conservative bound without full arc parameterization is to
830
+ // consider all possible extrema within a box of size 2*rx by 2*ry centered at each endpoint
831
+ // This ensures we never underestimate the bbox, though we may slightly overestimate
832
+ minX = Decimal.min(minX, currentX, x, currentX.minus(rx), x.minus(rx));
833
+ minY = Decimal.min(minY, currentY, y, currentY.minus(ry), y.minus(ry));
834
+ maxX = Decimal.max(maxX, currentX, x, currentX.plus(rx), x.plus(rx));
835
+ maxY = Decimal.max(maxY, currentY, y, currentY.plus(ry), y.plus(ry));
836
+
837
+ // Sample arc for verification: use parametric sampling along the arc
838
+ // Note: This is still an approximation, but better than linear interpolation
849
839
  const samples = 20;
850
840
  for (let i = 0; i <= samples; i++) {
851
841
  const t = D(i).div(samples);
842
+ // Simple parametric interpolation (approximation)
843
+ // For exact results, we'd need to convert to center parameterization
852
844
  const px = currentX.plus(x.minus(currentX).mul(t));
853
845
  const py = currentY.plus(y.minus(currentY).mul(t));
854
846
  samplePoints.push({ x: px, y: py });
@@ -19,6 +19,7 @@ import { Matrix } from "./matrix.js";
19
19
  import * as Transforms2D from "./transforms2d.js";
20
20
  import * as PolygonClip from "./polygon-clip.js";
21
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
 
@@ -1229,8 +1230,8 @@ export function parsePatternTransform(transformStr) {
1229
1230
  return Matrix.identity(3);
1230
1231
  }
1231
1232
 
1232
- // Use ClipPathResolver's transform parser
1233
- return ClipPathResolver.parseTransform(transformStr);
1233
+ // Use svg-flatten's transform parser (parseTransformAttribute)
1234
+ return parseTransformAttribute(transformStr);
1234
1235
  }
1235
1236
 
1236
1237
  export default {
@@ -23,15 +23,10 @@ const EPSILON = new Decimal("1e-40");
23
23
 
24
24
  const {
25
25
  point,
26
- pointsEqual: _pointsEqual,
27
26
  cross,
28
- polygonArea: _polygonArea,
29
27
  polygonIntersection,
30
28
  polygonUnion,
31
29
  polygonDifference,
32
- isCounterClockwise: _isCounterClockwise,
33
- ensureCCW: _ensureCCW,
34
- segmentIntersection: _segmentIntersection,
35
30
  } = PolygonClip;
36
31
 
37
32
  // ============================================================================
@@ -459,6 +459,7 @@ export const colorsProps = new Set([
459
459
 
460
460
  /**
461
461
  * Complete list of known SVG elements
462
+ * Includes both canonical names and lowercase variants for case-insensitive matching
462
463
  */
463
464
  export const knownElements = new Set([
464
465
  // Structural elements
@@ -487,8 +488,13 @@ export const knownElements = new Set([
487
488
  "glyphRef",
488
489
  // Gradient elements
489
490
  "linearGradient",
491
+ "lineargradient", // lowercase variant for case-insensitive matching
490
492
  "radialGradient",
493
+ "radialgradient", // lowercase variant for case-insensitive matching
491
494
  "meshGradient",
495
+ "meshgradient", // lowercase variant for case-insensitive matching
496
+ "meshrow", // SVG 2.0 mesh gradient row
497
+ "meshpatch", // SVG 2.0 mesh gradient patch
492
498
  "stop",
493
499
  // Container elements
494
500
  "a",
@@ -496,6 +502,7 @@ export const knownElements = new Set([
496
502
  "mask",
497
503
  "pattern",
498
504
  "clipPath",
505
+ "clippath", // lowercase variant for case-insensitive matching
499
506
  "switch",
500
507
  // Filter elements
501
508
  "filter",
@@ -531,8 +538,11 @@ export const knownElements = new Set([
531
538
  // Animation elements
532
539
  "animate",
533
540
  "animateColor",
541
+ "animatecolor", // lowercase variant for case-insensitive matching
534
542
  "animateMotion",
543
+ "animatemotion", // lowercase variant for case-insensitive matching
535
544
  "animateTransform",
545
+ "animatetransform", // lowercase variant for case-insensitive matching
536
546
  "set",
537
547
  "mpath",
538
548
  // Other elements
@@ -542,6 +552,7 @@ export const knownElements = new Set([
542
552
  "style",
543
553
  "script",
544
554
  "solidColor",
555
+ "solidcolor", // lowercase variant for case-insensitive matching
545
556
  "hatch",
546
557
  "hatchpath",
547
558
  ]);
@@ -5,7 +5,7 @@
5
5
  * Works in both Node.js and browser environments.
6
6
  *
7
7
  * @module svg-matrix-lib
8
- * @version 1.2.1
8
+ * @version 1.3.1
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -32,7 +32,7 @@ Decimal.set({ precision: 80 });
32
32
  /**
33
33
  * Library version
34
34
  */
35
- export const VERSION = "1.2.1";
35
+ export const VERSION = "1.3.1";
36
36
 
37
37
  // Export core classes
38
38
  export { Decimal, Matrix, Vector };
@@ -56,7 +56,8 @@ export const scale3D = Transforms3D.scale;
56
56
  export const applyTransform3D = Transforms3D.applyTransform;
57
57
 
58
58
  /**
59
- * Default export for browser global (window.SVGMatrix)
59
+ * Default export for browser global (window.SVGMatrixLib)
60
+ * Note: Named SVGMatrixLib to avoid conflict with native browser SVGMatrix
60
61
  */
61
62
  const SVGMatrix = {
62
63
  VERSION,
package/src/svg-parser.js CHANGED
@@ -7,10 +7,6 @@
7
7
  * @module svg-parser
8
8
  */
9
9
 
10
- import Decimal from "decimal.js";
11
-
12
- Decimal.set({ precision: 80 });
13
-
14
10
  /**
15
11
  * Recursively set ownerDocument on an element and all its descendants.
16
12
  * @param {SVGElement} el - Element to set ownerDocument on
@@ -666,30 +662,6 @@ export class SVGElement {
666
662
  // INTERNAL PARSING FUNCTIONS
667
663
  // ============================================================================
668
664
 
669
- /**
670
- * Check if whitespace should be preserved based on xml:space attribute.
671
- * BUG FIX 2: Helper function to check xml:space="preserve" on element or ancestors
672
- * @private
673
- * @param {SVGElement} element - Element to check for xml:space attribute
674
- * @returns {boolean} True if whitespace should be preserved, false otherwise
675
- */
676
- function _shouldPreserveWhitespace(element) {
677
- // Validation: Ensure element is not null/undefined
678
- if (!element) {
679
- throw new Error(
680
- "_shouldPreserveWhitespace: element cannot be null or undefined",
681
- );
682
- }
683
- let current = element;
684
- while (current) {
685
- const xmlSpace = current.getAttribute("xml:space");
686
- if (xmlSpace === "preserve") return true;
687
- if (xmlSpace === "default") return false;
688
- current = current.parentNode;
689
- }
690
- return false;
691
- }
692
-
693
665
  /**
694
666
  * Parse a single element from SVG string.
695
667
  * @private
@@ -31,10 +31,8 @@ import Decimal from "decimal.js";
31
31
  import {
32
32
  FillRule,
33
33
  pointInPolygonWithRule,
34
- offsetPolygon as _offsetPolygon,
35
34
  strokeToFilledPolygon,
36
35
  } from "./svg-boolean-ops.js";
37
- import * as _PolygonClip from "./polygon-clip.js";
38
36
 
39
37
  Decimal.set({ precision: 80 });
40
38
 
@@ -353,9 +351,9 @@ export class SVGRenderingContext {
353
351
  extent = halfWidth.times(this.strokeMiterlimit);
354
352
  }
355
353
 
356
- // Square linecaps extend by half stroke width beyond endpoints - Why: total extent is stroke radius + cap extension = strokeWidth
354
+ // Square linecaps extend strokeWidth/2 beyond endpoints - Why: total extent is base strokeWidth/2 + cap extension strokeWidth/2 = strokeWidth
357
355
  if (this.strokeLinecap === "square") {
358
- const capExtent = halfWidth.times(2); // strokeWidth/2 + strokeWidth/2
356
+ const capExtent = halfWidth.times(2); // Total extent: strokeWidth (base halfWidth + cap halfWidth)
359
357
  if (capExtent.gt(extent)) {
360
358
  extent = capExtent;
361
359
  }
@@ -5,7 +5,7 @@
5
5
  * Provides 69+ operations for cleaning, optimizing, and transforming SVG files.
6
6
  *
7
7
  * @module svg-toolbox-lib
8
- * @version 1.2.1
8
+ * @version 1.3.1
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -34,7 +34,7 @@ import * as SVGToolboxModule from "./svg-toolbox.js";
34
34
  /**
35
35
  * Library version
36
36
  */
37
- export const VERSION = "1.2.1";
37
+ export const VERSION = "1.3.1";
38
38
 
39
39
  /**
40
40
  * Default export for browser global (window.SVGToolbox)
@@ -606,7 +606,7 @@ export const VALID_CHILDREN = {
606
606
  "set",
607
607
  "title",
608
608
  ],
609
- font: ["desc", "glyph", "hkern", "metadata", "title", "vkern"],
609
+ font: ["desc", "font-face", "glyph", "hkern", "metadata", "missing-glyph", "title", "vkern"],
610
610
  "font-face": ["desc", "font-face-src", "metadata", "title"],
611
611
  "font-face-src": ["font-face-name", "font-face-uri"],
612
612
  "font-face-uri": ["font-face-format"],
package/src/svgm-lib.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * comprehensive SVG manipulation (SVGToolbox). Works in Node.js and browser.
6
6
  *
7
7
  * @module svgm-lib
8
- * @version 1.2.1
8
+ * @version 1.3.1
9
9
  * @license MIT
10
10
  *
11
11
  * @example Browser usage:
@@ -49,7 +49,7 @@ Decimal.set({ precision: 80 });
49
49
  /**
50
50
  * Library version
51
51
  */
52
- export const VERSION = "1.2.1";
52
+ export const VERSION = "1.3.1";
53
53
 
54
54
  // Export math classes
55
55
  export { Decimal, Matrix, Vector };