@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
package/src/arc-length.js CHANGED
@@ -101,7 +101,9 @@ const DEFAULT_ARC_LENGTH_TOLERANCE = "1e-30";
101
101
  * WHY: Used in adaptive quadrature to determine if subdivision has converged
102
102
  * by comparing 5-point and 10-point Gauss-Legendre results. When results
103
103
  * differ by less than this, we accept the higher-order result.
104
+ * NOTE: Currently unused - kept for potential future enhancement.
104
105
  */
106
+
105
107
  const _SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal("1e-15");
106
108
 
107
109
  /**
@@ -195,6 +197,17 @@ export function arcLength(points, t0 = 0, t1 = 1, options = {}) {
195
197
  * @returns {Decimal} Speed (magnitude of derivative)
196
198
  */
197
199
  function speedAtT(points, t) {
200
+ // INPUT VALIDATION: Ensure points array and t parameter are valid
201
+ // WHY: bezierDerivative will fail cryptically if points is invalid or t is not a valid number
202
+ if (!points || !Array.isArray(points) || points.length < 2) {
203
+ throw new Error(
204
+ "speedAtT: points must be an array with at least 2 control points",
205
+ );
206
+ }
207
+ if (t === null || t === undefined) {
208
+ throw new Error("speedAtT: t parameter is required");
209
+ }
210
+
198
211
  const [dx, dy] = bezierDerivative(points, t, 1);
199
212
  const speedSquared = dx.times(dx).plus(dy.times(dy));
200
213
 
@@ -223,6 +236,50 @@ function speedAtT(points, t) {
223
236
  * @returns {Decimal} Integral approximation
224
237
  */
225
238
  function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
239
+ // INPUT VALIDATION: Ensure all parameters are valid
240
+ // WHY: Invalid parameters would cause cryptic errors deep in recursion
241
+ if (typeof f !== "function") {
242
+ throw new Error("adaptiveQuadrature: f must be a function");
243
+ }
244
+ if (!a || !(a instanceof Decimal)) {
245
+ throw new Error("adaptiveQuadrature: a must be a Decimal");
246
+ }
247
+ if (!b || !(b instanceof Decimal)) {
248
+ throw new Error("adaptiveQuadrature: b must be a Decimal");
249
+ }
250
+ if (!tol || !(tol instanceof Decimal) || tol.lte(0)) {
251
+ throw new Error(
252
+ "adaptiveQuadrature: tol must be a positive Decimal",
253
+ );
254
+ }
255
+ if (
256
+ typeof maxDepth !== "number" ||
257
+ maxDepth < 0 ||
258
+ !Number.isInteger(maxDepth)
259
+ ) {
260
+ throw new Error(
261
+ "adaptiveQuadrature: maxDepth must be a non-negative integer",
262
+ );
263
+ }
264
+ if (
265
+ typeof minDepth !== "number" ||
266
+ minDepth < 0 ||
267
+ !Number.isInteger(minDepth)
268
+ ) {
269
+ throw new Error(
270
+ "adaptiveQuadrature: minDepth must be a non-negative integer",
271
+ );
272
+ }
273
+ if (
274
+ typeof depth !== "number" ||
275
+ depth < 0 ||
276
+ !Number.isInteger(depth)
277
+ ) {
278
+ throw new Error(
279
+ "adaptiveQuadrature: depth must be a non-negative integer",
280
+ );
281
+ }
282
+
226
283
  // Compute integral using 5-point and 10-point rules
227
284
  const I5 = gaussLegendre(f, a, b, 5);
228
285
  const I10 = gaussLegendre(f, a, b, 10);
@@ -273,7 +330,31 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
273
330
  * @returns {Decimal} Integral approximation
274
331
  */
275
332
  function gaussLegendre(f, a, b, order) {
333
+ // INPUT VALIDATION: Ensure order is valid
334
+ // WHY: GAUSS_LEGENDRE only has entries for orders 5 and 10. Invalid order would cause undefined access.
335
+ if (order !== 5 && order !== 10) {
336
+ throw new Error(
337
+ "gaussLegendre: order must be 5 or 10 (only supported orders)",
338
+ );
339
+ }
340
+
276
341
  const gl = GAUSS_LEGENDRE[order];
342
+
343
+ // INTERNAL CONSISTENCY CHECK: Verify Gauss-Legendre table has correct structure
344
+ // WHY: Ensures the precomputed tables haven't been corrupted or misconfigured
345
+ if (!gl.nodes || !gl.weights || gl.nodes.length !== order || gl.weights.length !== order) {
346
+ throw new Error(
347
+ `gaussLegendre: GAUSS_LEGENDRE[${order}] table is malformed (expected ${order} nodes and weights)`,
348
+ );
349
+ }
350
+
351
+ // DIVISION BY ZERO CHECK: Ensure a != b
352
+ // WHY: If a == b, the integral is zero (no width), and halfWidth would be 0.
353
+ // While multiplying by 0 gives correct result (0), we should handle explicitly.
354
+ if (a.eq(b)) {
355
+ return D(0);
356
+ }
357
+
277
358
  const halfWidth = b.minus(a).div(2);
278
359
  const center = a.plus(b).div(2);
279
360
 
@@ -288,6 +369,22 @@ function gaussLegendre(f, a, b, order) {
288
369
 
289
370
  // Evaluate function and add weighted contribution
290
371
  const fValue = f(t);
372
+
373
+ // VALIDATION: Ensure function returns a valid Decimal
374
+ // WHY: If f returns null, undefined, NaN, or non-Decimal, arithmetic operations will fail
375
+ if (fValue === null || fValue === undefined) {
376
+ throw new Error("gaussLegendre: integrand function f returned null or undefined");
377
+ }
378
+ if (!(fValue instanceof Decimal)) {
379
+ throw new Error("gaussLegendre: integrand function f must return a Decimal instance");
380
+ }
381
+ if (fValue.isNaN()) {
382
+ throw new Error(`gaussLegendre: integrand function f returned NaN at t=${t}`);
383
+ }
384
+ if (!fValue.isFinite()) {
385
+ throw new Error(`gaussLegendre: integrand function f returned non-finite value at t=${t}`);
386
+ }
387
+
291
388
  sum = sum.plus(weight.times(fValue));
292
389
  }
293
390
 
@@ -336,6 +433,18 @@ export function inverseArcLength(points, targetLength, options = {}) {
336
433
  initialT,
337
434
  } = options;
338
435
 
436
+ // INPUT VALIDATION: Ensure maxIterations is a positive integer
437
+ // WHY: maxIterations controls the loop; negative or zero values would prevent convergence
438
+ if (
439
+ typeof maxIterations !== "number" ||
440
+ maxIterations <= 0 ||
441
+ !Number.isInteger(maxIterations)
442
+ ) {
443
+ throw new Error(
444
+ "inverseArcLength: maxIterations must be a positive integer",
445
+ );
446
+ }
447
+
339
448
  const target = D(targetLength);
340
449
  const tol = D(tolerance);
341
450
  const lengthOpts = { tolerance: lengthTolerance };
@@ -348,6 +457,15 @@ export function inverseArcLength(points, targetLength, options = {}) {
348
457
  throw new Error("inverseArcLength: targetLength must be non-negative");
349
458
  }
350
459
 
460
+ // EDGE CASE: Check for NaN or Infinity in targetLength
461
+ // WHY: NaN or Infinity would cause Newton's method to diverge or produce nonsensical results
462
+ if (target.isNaN()) {
463
+ throw new Error("inverseArcLength: targetLength cannot be NaN");
464
+ }
465
+ if (!target.isFinite()) {
466
+ throw new Error("inverseArcLength: targetLength must be finite");
467
+ }
468
+
351
469
  // Handle edge case: zero length
352
470
  if (target.isZero()) {
353
471
  return { t: D(0), length: D(0), iterations: 0, converged: true };
@@ -546,9 +664,15 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
546
664
  "createArcLengthTable: points must have at least 2 control points",
547
665
  );
548
666
  }
549
- if (samples < 2) {
667
+ // TYPE CHECK: Ensure samples is a valid number
668
+ // WHY: samples is used in arithmetic operations and as a loop bound
669
+ if (
670
+ typeof samples !== "number" ||
671
+ !Number.isInteger(samples) ||
672
+ samples < 2
673
+ ) {
550
674
  throw new Error(
551
- "createArcLengthTable: samples must be at least 2 (for binary search to work)",
675
+ "createArcLengthTable: samples must be an integer >= 2 (for binary search to work)",
552
676
  );
553
677
  }
554
678
 
@@ -580,8 +704,23 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
580
704
  * @returns {Decimal} Approximate t
581
705
  */
582
706
  getT(s) {
707
+ // INPUT VALIDATION: Ensure s is provided and valid
708
+ // WHY: Without s, we cannot perform the lookup. Catching this early prevents cryptic errors.
709
+ if (s === null || s === undefined) {
710
+ throw new Error("getT: arc length parameter s is required");
711
+ }
712
+
583
713
  const sD = D(s);
584
714
 
715
+ // EDGE CASE: Check for NaN or Infinity
716
+ // WHY: Invalid arc lengths would cause incorrect lookups
717
+ if (sD.isNaN()) {
718
+ throw new Error("getT: arc length s cannot be NaN");
719
+ }
720
+ if (!sD.isFinite()) {
721
+ throw new Error("getT: arc length s must be finite");
722
+ }
723
+
585
724
  if (sD.lte(0)) return D(0);
586
725
  if (sD.gte(this.totalLength)) return D(1);
587
726
 
@@ -610,7 +749,15 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
610
749
  const t0 = table[lo].t;
611
750
  const t1 = table[hi].t;
612
751
 
613
- const fraction = sD.minus(s0).div(s1.minus(s0));
752
+ // DIVISION BY ZERO CHECK: Handle degenerate case where s0 == s1
753
+ // WHY: If two consecutive table entries have the same arc length (e.g., at a cusp),
754
+ // division would fail. In this case, we return the midpoint t value.
755
+ const denominator = s1.minus(s0);
756
+ if (denominator.isZero()) {
757
+ return t0.plus(t1).div(2);
758
+ }
759
+
760
+ const fraction = sD.minus(s0).div(denominator);
614
761
  return t0.plus(t1.minus(t0).times(fraction));
615
762
  },
616
763
 
@@ -621,6 +768,13 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
621
768
  * @returns {Decimal} Refined t
622
769
  */
623
770
  getTRefined(s, opts = {}) {
771
+ // INPUT VALIDATION: Ensure s is provided and valid
772
+ // WHY: This method calls getT internally which will validate, but we validate
773
+ // here too for consistent error messages at the API boundary.
774
+ if (s === null || s === undefined) {
775
+ throw new Error("getTRefined: arc length parameter s is required");
776
+ }
777
+
624
778
  const approxT = this.getT(s);
625
779
  // Use approxT as starting point for Newton
626
780
  const { t } = inverseArcLength(points, s, { ...opts, initialT: approxT });
@@ -652,6 +806,19 @@ export function verifyArcLength(points, computedLength = null) {
652
806
  );
653
807
  }
654
808
 
809
+ // ARRAY ELEMENT VALIDATION: Ensure points array elements are valid [x, y] pairs
810
+ // WHY: We access points[i][0] and points[i][1] below. Invalid structure would cause errors.
811
+ if (
812
+ !Array.isArray(points[0]) ||
813
+ points[0].length < 2 ||
814
+ !Array.isArray(points[points.length - 1]) ||
815
+ points[points.length - 1].length < 2
816
+ ) {
817
+ throw new Error(
818
+ "verifyArcLength: each point must be an array with at least 2 coordinates [x, y]",
819
+ );
820
+ }
821
+
655
822
  const errors = [];
656
823
 
657
824
  // Compute arc length if not provided
@@ -669,6 +836,19 @@ export function verifyArcLength(points, computedLength = null) {
669
836
  // Control polygon length
670
837
  let polygonLength = D(0);
671
838
  for (let i = 0; i < points.length - 1; i++) {
839
+ // ARRAY ELEMENT VALIDATION: Check each point in the loop
840
+ // WHY: Ensures all intermediate points are valid before accessing coordinates
841
+ if (!Array.isArray(points[i]) || points[i].length < 2) {
842
+ throw new Error(
843
+ `verifyArcLength: point at index ${i} must be an array with at least 2 coordinates [x, y]`,
844
+ );
845
+ }
846
+ if (!Array.isArray(points[i + 1]) || points[i + 1].length < 2) {
847
+ throw new Error(
848
+ `verifyArcLength: point at index ${i + 1} must be an array with at least 2 coordinates [x, y]`,
849
+ );
850
+ }
851
+
672
852
  const [x1, y1] = [D(points[i][0]), D(points[i][1])];
673
853
  const [x2, y2] = [D(points[i + 1][0]), D(points[i + 1][1])];
674
854
  polygonLength = polygonLength.plus(
@@ -684,12 +864,17 @@ export function verifyArcLength(points, computedLength = null) {
684
864
  errors.push(`Arc length ${length} > polygon length ${polygonLength}`);
685
865
  }
686
866
 
867
+ // DIVISION BY ZERO CHECK: Handle degenerate case where chord length is zero
868
+ // WHY: If start and end points are identical, chordLength is 0, causing division by zero.
869
+ // In this case, the ratio is not meaningful, so we return 1 (or could be Infinity).
870
+ const ratio = chordLength.gt(0) ? length.div(chordLength) : D(1);
871
+
687
872
  return {
688
873
  valid: errors.length === 0,
689
874
  chordLength,
690
875
  polygonLength,
691
876
  arcLength: length,
692
- ratio: chordLength.gt(0) ? length.div(chordLength) : D(1),
877
+ ratio,
693
878
  errors,
694
879
  };
695
880
  }
@@ -771,6 +956,18 @@ export function verifyArcLengthBySubdivision(
771
956
  );
772
957
  }
773
958
 
959
+ // INPUT VALIDATION: Ensure subdivisions is a positive integer
960
+ // WHY: subdivisions is used as a divisor and loop bound. Zero or negative would cause division by zero or invalid loops.
961
+ if (
962
+ typeof subdivisions !== "number" ||
963
+ subdivisions <= 0 ||
964
+ !Number.isInteger(subdivisions)
965
+ ) {
966
+ throw new Error(
967
+ "verifyArcLengthBySubdivision: subdivisions must be a positive integer",
968
+ );
969
+ }
970
+
774
971
  const tol = D(tolerance);
775
972
 
776
973
  // Method 1: Adaptive quadrature
@@ -871,6 +1068,18 @@ export function verifyArcLengthTable(points, samples = 50) {
871
1068
  );
872
1069
  }
873
1070
 
1071
+ // INPUT VALIDATION: Ensure samples is a positive integer >= 2
1072
+ // WHY: samples is passed to createArcLengthTable which requires it to be >= 2
1073
+ if (
1074
+ typeof samples !== "number" ||
1075
+ !Number.isInteger(samples) ||
1076
+ samples < 2
1077
+ ) {
1078
+ throw new Error(
1079
+ "verifyArcLengthTable: samples must be an integer >= 2",
1080
+ );
1081
+ }
1082
+
874
1083
  const errors = [];
875
1084
  const table = createArcLengthTable(points, samples);
876
1085