@emasoft/svg-matrix 1.0.30 → 1.0.31

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 (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. package/src/verification.js +392 -1
@@ -28,7 +28,14 @@ Decimal.set({ precision: 80 });
28
28
  * @param {number|string|Decimal} x - Value to convert
29
29
  * @returns {Decimal}
30
30
  */
31
- const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
31
+ const D = (x) => {
32
+ // INPUT VALIDATION: Ensure input is not null or undefined
33
+ // WHY: Passing null/undefined to Decimal constructor throws cryptic errors
34
+ if (x == null) {
35
+ throw new Error(`D(): cannot convert null or undefined to Decimal`);
36
+ }
37
+ return x instanceof Decimal ? x : new Decimal(x);
38
+ };
32
39
 
33
40
  /**
34
41
  * Validate that a value is a finite number (not NaN or Infinity).
@@ -39,6 +46,13 @@ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
39
46
  * @throws {Error} If value is not finite
40
47
  */
41
48
  function _assertFinite(val, context) {
49
+ // INPUT VALIDATION: Ensure val is a Decimal instance
50
+ // WHY: Calling .isFinite() on null/undefined/non-Decimal throws errors
51
+ if (!val || !(val instanceof Decimal)) {
52
+ throw new Error(
53
+ `${context}: expected Decimal instance, got ${typeof val}`,
54
+ );
55
+ }
42
56
  if (!val.isFinite()) {
43
57
  throw new Error(`${context}: encountered non-finite value ${val}`);
44
58
  }
@@ -135,6 +149,22 @@ export function bezierPoint(points, t) {
135
149
  );
136
150
  }
137
151
 
152
+ // POINT VALIDATION: Ensure each point is a valid [x, y] array
153
+ // WHY: Invalid point structure causes crashes when accessing indices
154
+ for (let i = 0; i < points.length; i++) {
155
+ if (
156
+ !points[i] ||
157
+ !Array.isArray(points[i]) ||
158
+ points[i].length < 2 ||
159
+ points[i][0] == null ||
160
+ points[i][1] == null
161
+ ) {
162
+ throw new Error(
163
+ `bezierPoint: points[${i}] must be a valid [x, y] array with non-null coordinates`,
164
+ );
165
+ }
166
+ }
167
+
138
168
  const tD = D(t);
139
169
  // PARAMETER VALIDATION: Warn if t is outside [0,1] but still compute
140
170
  // WHY: Values slightly outside [0,1] may occur in numerical algorithms
@@ -184,6 +214,22 @@ export function bezierPointHorner(points, t) {
184
214
  );
185
215
  }
186
216
 
217
+ // POINT VALIDATION: Ensure each point is a valid [x, y] array
218
+ // WHY: Invalid point structure causes crashes when accessing indices
219
+ for (let i = 0; i < points.length; i++) {
220
+ if (
221
+ !points[i] ||
222
+ !Array.isArray(points[i]) ||
223
+ points[i].length < 2 ||
224
+ points[i][0] == null ||
225
+ points[i][1] == null
226
+ ) {
227
+ throw new Error(
228
+ `bezierPointHorner: points[${i}] must be a valid [x, y] array with non-null coordinates`,
229
+ );
230
+ }
231
+ }
232
+
187
233
  const tD = D(t);
188
234
  const n = points.length - 1; // Degree
189
235
 
@@ -268,6 +314,14 @@ export function bezierDerivative(points, t, n = 1) {
268
314
  );
269
315
  }
270
316
 
317
+ // PARAMETER VALIDATION: Ensure n is a non-negative integer
318
+ // WHY: Negative or non-integer derivative orders are not meaningful
319
+ if (typeof n !== "number" || n < 0 || !Number.isInteger(n)) {
320
+ throw new Error(
321
+ `bezierDerivative: n must be a non-negative integer, got ${n}`,
322
+ );
323
+ }
324
+
271
325
  if (n === 0) {
272
326
  return bezierPoint(points, t);
273
327
  }
@@ -625,6 +679,15 @@ export function bezierCrop(points, t0, t1) {
625
679
  throw new Error("bezierCrop: t1 must be in range [0, 1]");
626
680
  }
627
681
 
682
+ // DIVISION BY ZERO PROTECTION: Check if t0 is too close to 1
683
+ // WHY: When t0 approaches 1, the denominator (1 - t0) approaches zero,
684
+ // causing division by zero in the parameter adjustment calculation
685
+ if (D(1).minus(t0D).abs().lt(NEAR_ZERO_THRESHOLD)) {
686
+ throw new Error(
687
+ "bezierCrop: t0 too close to 1, would cause division by zero in parameter adjustment",
688
+ );
689
+ }
690
+
628
691
  // First split at t0, take the right portion
629
692
  const { right: afterT0 } = bezierSplit(points, t0);
630
693
 
@@ -713,6 +776,14 @@ function findBezierRoots1D(points, component) {
713
776
  return []; // No roots possible for empty input
714
777
  }
715
778
 
779
+ // PARAMETER VALIDATION: Ensure component is valid
780
+ // WHY: Invalid component values would cause incorrect index access
781
+ if (component !== "x" && component !== "y") {
782
+ throw new Error(
783
+ `findBezierRoots1D: component must be 'x' or 'y', got '${component}'`,
784
+ );
785
+ }
786
+
716
787
  const idx = component === "x" ? 0 : 1;
717
788
  const roots = [];
718
789
 
@@ -776,37 +847,56 @@ function findBezierRoots1D(points, component) {
776
847
  * @returns {Decimal[]} Real roots
777
848
  */
778
849
  function solveQuadratic(a, b, c) {
850
+ // INPUT VALIDATION: Ensure all coefficients are valid Decimal instances
851
+ // WHY: Invalid inputs cause cryptic errors in arithmetic operations
852
+ if (a == null || b == null || c == null) {
853
+ throw new Error(
854
+ "solveQuadratic: coefficients a, b, c must not be null or undefined",
855
+ );
856
+ }
857
+ // Convert to Decimal if needed (use local variables to avoid param reassignment)
858
+ const aD = a instanceof Decimal ? a : D(a);
859
+ const bD = b instanceof Decimal ? b : D(b);
860
+ const cD = c instanceof Decimal ? c : D(c);
861
+
862
+ // Check for non-finite values
863
+ if (!aD.isFinite() || !bD.isFinite() || !cD.isFinite()) {
864
+ throw new Error(
865
+ "solveQuadratic: coefficients must be finite numbers, got non-finite values",
866
+ );
867
+ }
868
+
779
869
  // NUMERICAL STABILITY: Use threshold relative to coefficient magnitudes
780
870
  // to determine if 'a' is effectively zero (degenerate to linear equation)
781
871
  // WHY: Absolute thresholds fail when coefficients are scaled; relative threshold adapts
782
- const coeffMag = Decimal.max(a.abs(), b.abs(), c.abs());
872
+ const coeffMag = Decimal.max(aD.abs(), bD.abs(), cD.abs());
783
873
 
784
874
  if (
785
875
  coeffMag.gt(0) &&
786
- a.abs().div(coeffMag).lt(QUADRATIC_DEGENERATE_THRESHOLD)
876
+ aD.abs().div(coeffMag).lt(QUADRATIC_DEGENERATE_THRESHOLD)
787
877
  ) {
788
878
  // Linear equation: bx + c = 0
789
- if (b.isZero()) return [];
790
- return [c.neg().div(b)];
879
+ if (bD.isZero()) return [];
880
+ return [cD.neg().div(bD)];
791
881
  }
792
882
 
793
- if (a.isZero()) {
794
- if (b.isZero()) return [];
795
- return [c.neg().div(b)];
883
+ if (aD.isZero()) {
884
+ if (bD.isZero()) return [];
885
+ return [cD.neg().div(bD)];
796
886
  }
797
887
 
798
- const discriminant = b.times(b).minus(a.times(c).times(4));
888
+ const discriminant = bD.times(bD).minus(aD.times(cD).times(4));
799
889
 
800
890
  if (discriminant.lt(0)) {
801
891
  return []; // No real roots
802
892
  }
803
893
 
804
894
  if (discriminant.isZero()) {
805
- return [b.neg().div(a.times(2))];
895
+ return [bD.neg().div(aD.times(2))];
806
896
  }
807
897
 
808
898
  const sqrtD = discriminant.sqrt();
809
- const twoA = a.times(2);
899
+ const twoA = aD.times(2);
810
900
 
811
901
  // NUMERICAL STABILITY: Use Vieta's formula to compute the second root
812
902
  // when catastrophic cancellation would occur in the standard formula.
@@ -817,16 +907,28 @@ function solveQuadratic(a, b, c) {
817
907
  // x1 * x2 = c/a, so x2 = (c/a) / x1
818
908
 
819
909
  let root1, root2;
820
- if (b.isNegative()) {
910
+ if (bD.isNegative()) {
821
911
  // -b is positive, so -b + sqrt(D) is well-conditioned
822
- root1 = b.neg().plus(sqrtD).div(twoA);
912
+ root1 = bD.neg().plus(sqrtD).div(twoA);
823
913
  // Use Vieta's formula: x1 * x2 = c/a
824
- root2 = c.div(a).div(root1);
914
+ // DIVISION BY ZERO PROTECTION: Check if root1 is zero before dividing
915
+ // WHY: When root1 is zero, Vieta's formula degenerates; use direct formula instead
916
+ if (root1.abs().lt(NEAR_ZERO_THRESHOLD)) {
917
+ root2 = bD.neg().minus(sqrtD).div(twoA);
918
+ } else {
919
+ root2 = cD.div(aD).div(root1);
920
+ }
825
921
  } else {
826
922
  // -b is negative or zero, so -b - sqrt(D) is well-conditioned
827
- root1 = b.neg().minus(sqrtD).div(twoA);
923
+ root1 = bD.neg().minus(sqrtD).div(twoA);
828
924
  // Use Vieta's formula: x1 * x2 = c/a
829
- root2 = c.div(a).div(root1);
925
+ // DIVISION BY ZERO PROTECTION: Check if root1 is zero before dividing
926
+ // WHY: When root1 is zero, Vieta's formula degenerates; use direct formula instead
927
+ if (root1.abs().lt(NEAR_ZERO_THRESHOLD)) {
928
+ root2 = bD.neg().plus(sqrtD).div(twoA);
929
+ } else {
930
+ root2 = cD.div(aD).div(root1);
931
+ }
830
932
  }
831
933
 
832
934
  return [root1, root2];
@@ -842,6 +944,45 @@ function solveQuadratic(a, b, c) {
842
944
  * @returns {Decimal[]} Roots in interval
843
945
  */
844
946
  function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
947
+ // INPUT VALIDATION: Ensure coeffs is valid and maxDepth is non-negative
948
+ // WHY: Empty coeffs would cause errors; negative maxDepth could cause infinite recursion
949
+ if (!coeffs || !Array.isArray(coeffs) || coeffs.length === 0) {
950
+ return []; // No roots possible for empty input
951
+ }
952
+ if (typeof maxDepth !== "number" || maxDepth < 0) {
953
+ throw new Error(
954
+ `findRootsBySubdivision: maxDepth must be non-negative number, got ${maxDepth}`,
955
+ );
956
+ }
957
+
958
+ // PARAMETER VALIDATION: Ensure t0 and t1 are valid
959
+ // WHY: Invalid interval bounds cause arithmetic errors
960
+ if (t0 == null || t1 == null) {
961
+ throw new Error(
962
+ "findRootsBySubdivision: t0 and t1 must not be null or undefined",
963
+ );
964
+ }
965
+ const t0D = t0 instanceof Decimal ? t0 : D(t0);
966
+ const t1D = t1 instanceof Decimal ? t1 : D(t1);
967
+ if (!t0D.isFinite() || !t1D.isFinite()) {
968
+ throw new Error(
969
+ "findRootsBySubdivision: t0 and t1 must be finite numbers",
970
+ );
971
+ }
972
+ if (t1D.lte(t0D)) {
973
+ throw new Error("findRootsBySubdivision: t1 must be greater than t0");
974
+ }
975
+
976
+ // COEFFICIENT VALIDATION: Ensure all coefficients are valid Decimals
977
+ // WHY: Sign checking requires .isNegative() and .isZero() methods
978
+ for (let i = 0; i < coeffs.length; i++) {
979
+ if (!coeffs[i] || !(coeffs[i] instanceof Decimal)) {
980
+ throw new Error(
981
+ `findRootsBySubdivision: coeffs[${i}] must be a Decimal instance`,
982
+ );
983
+ }
984
+ }
985
+
845
986
  // Check if interval might contain a root (sign change in convex hull)
846
987
  const signs = coeffs.map((c) => (c.isNegative() ? -1 : c.isZero() ? 0 : 1));
847
988
  const minSign = Math.min(...signs);
@@ -853,19 +994,19 @@ function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
853
994
  }
854
995
 
855
996
  // WHY: Use named constant for subdivision convergence check
856
- if (maxDepth <= 0 || t1.minus(t0).lt(SUBDIVISION_CONVERGENCE_THRESHOLD)) {
997
+ if (maxDepth <= 0 || t1D.minus(t0D).lt(SUBDIVISION_CONVERGENCE_THRESHOLD)) {
857
998
  // Converged, return midpoint
858
- return [t0.plus(t1).div(2)];
999
+ return [t0D.plus(t1D).div(2)];
859
1000
  }
860
1001
 
861
1002
  // Subdivide at midpoint
862
- const tMid = t0.plus(t1).div(2);
1003
+ const tMid = t0D.plus(t1D).div(2);
863
1004
 
864
1005
  // Compute subdivided control points using de Casteljau
865
1006
  const { left, right } = subdivideBezier1D(coeffs);
866
1007
 
867
- const leftRoots = findRootsBySubdivision(left, t0, tMid, maxDepth - 1);
868
- const rightRoots = findRootsBySubdivision(right, tMid, t1, maxDepth - 1);
1008
+ const leftRoots = findRootsBySubdivision(left, t0D, tMid, maxDepth - 1);
1009
+ const rightRoots = findRootsBySubdivision(right, tMid, t1D, maxDepth - 1);
869
1010
 
870
1011
  return leftRoots.concat(rightRoots);
871
1012
  }
@@ -880,6 +1021,14 @@ function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
880
1021
  * @returns {{left: Decimal[], right: Decimal[]}} Two 1D Bezier curves representing left and right halves
881
1022
  */
882
1023
  function subdivideBezier1D(coeffs) {
1024
+ // INPUT VALIDATION: Ensure coeffs is valid and non-empty
1025
+ // WHY: Empty coeffs would cause errors in de Casteljau iteration
1026
+ if (!coeffs || !Array.isArray(coeffs) || coeffs.length === 0) {
1027
+ throw new Error(
1028
+ "subdivideBezier1D: coeffs must be a non-empty array of control values",
1029
+ );
1030
+ }
1031
+
883
1032
  const half = D(0.5);
884
1033
  let pts = coeffs.map((c) => D(c));
885
1034
 
@@ -926,6 +1075,22 @@ export function bezierToPolynomial(points) {
926
1075
  );
927
1076
  }
928
1077
 
1078
+ // POINT VALIDATION: Ensure each point is a valid [x, y] array
1079
+ // WHY: Destructuring and index access fail on invalid point structures
1080
+ for (let i = 0; i < points.length; i++) {
1081
+ if (
1082
+ !points[i] ||
1083
+ !Array.isArray(points[i]) ||
1084
+ points[i].length < 2 ||
1085
+ points[i][0] == null ||
1086
+ points[i][1] == null
1087
+ ) {
1088
+ throw new Error(
1089
+ `bezierToPolynomial: points[${i}] must be a valid [x, y] array with non-null coordinates`,
1090
+ );
1091
+ }
1092
+ }
1093
+
929
1094
  const n = points.length - 1;
930
1095
  const xCoeffs = [];
931
1096
  const yCoeffs = [];
@@ -1003,6 +1168,21 @@ export function polynomialToBezier(xCoeffs, yCoeffs) {
1003
1168
  );
1004
1169
  }
1005
1170
 
1171
+ // COEFFICIENT VALIDATION: Ensure all coefficients are valid (not null/undefined)
1172
+ // WHY: Arithmetic operations fail on null/undefined values
1173
+ for (let i = 0; i < xCoeffs.length; i++) {
1174
+ if (xCoeffs[i] == null) {
1175
+ throw new Error(
1176
+ `polynomialToBezier: xCoeffs[${i}] is null or undefined`,
1177
+ );
1178
+ }
1179
+ if (yCoeffs[i] == null) {
1180
+ throw new Error(
1181
+ `polynomialToBezier: yCoeffs[${i}] is null or undefined`,
1182
+ );
1183
+ }
1184
+ }
1185
+
1006
1186
  const n = xCoeffs.length - 1;
1007
1187
 
1008
1188
  if (n === 1) {
@@ -1487,6 +1667,14 @@ export function verifyBoundingBox(points, samples = 100, tolerance = "1e-40") {
1487
1667
  );
1488
1668
  }
1489
1669
 
1670
+ // PARAMETER VALIDATION: Ensure samples is a positive integer
1671
+ // WHY: Non-positive or non-integer samples would cause loop errors or division by zero
1672
+ if (typeof samples !== "number" || samples < 1 || !Number.isInteger(samples)) {
1673
+ throw new Error(
1674
+ `verifyBoundingBox: samples must be a positive integer, got ${samples}`,
1675
+ );
1676
+ }
1677
+
1490
1678
  const tol = D(tolerance);
1491
1679
  const errors = [];
1492
1680
 
@@ -1564,6 +1752,14 @@ export function verifyDerivative(points, t, order = 1, tolerance = "1e-8") {
1564
1752
  );
1565
1753
  }
1566
1754
 
1755
+ // PARAMETER VALIDATION: Ensure order is a positive integer
1756
+ // WHY: Negative or non-integer orders are not meaningful for derivatives
1757
+ if (typeof order !== "number" || order < 0 || !Number.isInteger(order)) {
1758
+ throw new Error(
1759
+ `verifyDerivative: order must be a non-negative integer, got ${order}`,
1760
+ );
1761
+ }
1762
+
1567
1763
  const tol = D(tolerance);
1568
1764
  const tD = D(t);
1569
1765