@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
@@ -63,8 +63,15 @@ export function identityMatrix() {
63
63
  * @param {number|string|Decimal} tx - X translation
64
64
  * @param {number|string|Decimal} ty - Y translation
65
65
  * @returns {Matrix} 3x3 translation matrix
66
+ * @throws {Error} If tx or ty is null or undefined
66
67
  */
67
68
  export function translationMatrix(tx, ty) {
69
+ if (tx === null || tx === undefined) {
70
+ throw new Error("translationMatrix: tx parameter is required");
71
+ }
72
+ if (ty === null || ty === undefined) {
73
+ throw new Error("translationMatrix: ty parameter is required");
74
+ }
68
75
  return Matrix.from([
69
76
  [1, 0, D(tx)],
70
77
  [0, 1, D(ty)],
@@ -76,8 +83,12 @@ export function translationMatrix(tx, ty) {
76
83
  * Create a 2D rotation matrix.
77
84
  * @param {number|string|Decimal} angle - Rotation angle in radians
78
85
  * @returns {Matrix} 3x3 rotation matrix
86
+ * @throws {Error} If angle is null or undefined
79
87
  */
80
88
  export function rotationMatrix(angle) {
89
+ if (angle === null || angle === undefined) {
90
+ throw new Error("rotationMatrix: angle parameter is required");
91
+ }
81
92
  const theta = D(angle);
82
93
  const cos = Decimal.cos(theta);
83
94
  const sin = Decimal.sin(theta);
@@ -94,8 +105,18 @@ export function rotationMatrix(angle) {
94
105
  * @param {number|string|Decimal} cx - X coordinate of rotation center
95
106
  * @param {number|string|Decimal} cy - Y coordinate of rotation center
96
107
  * @returns {Matrix} 3x3 rotation matrix around point (cx, cy)
108
+ * @throws {Error} If angle, cx, or cy is null or undefined
97
109
  */
98
110
  export function rotationMatrixAroundPoint(angle, cx, cy) {
111
+ if (angle === null || angle === undefined) {
112
+ throw new Error("rotationMatrixAroundPoint: angle parameter is required");
113
+ }
114
+ if (cx === null || cx === undefined) {
115
+ throw new Error("rotationMatrixAroundPoint: cx parameter is required");
116
+ }
117
+ if (cy === null || cy === undefined) {
118
+ throw new Error("rotationMatrixAroundPoint: cy parameter is required");
119
+ }
99
120
  const cxD = D(cx);
100
121
  const cyD = D(cy);
101
122
  const T1 = translationMatrix(cxD.neg(), cyD.neg());
@@ -109,8 +130,15 @@ export function rotationMatrixAroundPoint(angle, cx, cy) {
109
130
  * @param {number|string|Decimal} sx - X scale factor
110
131
  * @param {number|string|Decimal} sy - Y scale factor
111
132
  * @returns {Matrix} 3x3 scale matrix
133
+ * @throws {Error} If sx or sy is null or undefined
112
134
  */
113
135
  export function scaleMatrix(sx, sy) {
136
+ if (sx === null || sx === undefined) {
137
+ throw new Error("scaleMatrix: sx parameter is required");
138
+ }
139
+ if (sy === null || sy === undefined) {
140
+ throw new Error("scaleMatrix: sy parameter is required");
141
+ }
114
142
  return Matrix.from([
115
143
  [D(sx), 0, 0],
116
144
  [0, D(sy), 0],
@@ -123,8 +151,22 @@ export function scaleMatrix(sx, sy) {
123
151
  * @param {Matrix} m1 - First matrix
124
152
  * @param {Matrix} m2 - Second matrix
125
153
  * @returns {Decimal} Maximum absolute difference
154
+ * @throws {Error} If m1 or m2 is null, undefined, or dimensions don't match
126
155
  */
127
156
  export function matrixMaxDifference(m1, m2) {
157
+ if (!m1 || !m2) {
158
+ throw new Error("matrixMaxDifference: both m1 and m2 parameters are required");
159
+ }
160
+ if (!m1.rows || !m1.cols || !m1.data) {
161
+ throw new Error("matrixMaxDifference: m1 must be a valid Matrix object");
162
+ }
163
+ if (!m2.rows || !m2.cols || !m2.data) {
164
+ throw new Error("matrixMaxDifference: m2 must be a valid Matrix object");
165
+ }
166
+ if (m1.rows !== m2.rows || m1.cols !== m2.cols) {
167
+ throw new Error(`matrixMaxDifference: matrix dimensions must match (m1: ${m1.rows}x${m1.cols}, m2: ${m2.rows}x${m2.cols})`);
168
+ }
169
+
128
170
  let maxDiff = D(0);
129
171
 
130
172
  for (let i = 0; i < m1.rows; i++) {
@@ -170,6 +212,7 @@ export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
170
212
  * verified: boolean,
171
213
  * maxError: Decimal
172
214
  * }} Merged translation with verification result
215
+ * @throws {Error} If t1 or t2 is null/undefined or missing required properties
173
216
  *
174
217
  * @example
175
218
  * // Merge translate(5, 10) and translate(3, -2)
@@ -177,6 +220,16 @@ export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
177
220
  * // Result: {tx: 8, ty: 8, verified: true}
178
221
  */
179
222
  export function mergeTranslations(t1, t2) {
223
+ if (!t1 || !t2) {
224
+ throw new Error("mergeTranslations: both t1 and t2 parameters are required");
225
+ }
226
+ if (t1.tx === null || t1.tx === undefined || t1.ty === null || t1.ty === undefined) {
227
+ throw new Error("mergeTranslations: t1 must have tx and ty properties");
228
+ }
229
+ if (t2.tx === null || t2.tx === undefined || t2.ty === null || t2.ty === undefined) {
230
+ throw new Error("mergeTranslations: t2 must have tx and ty properties");
231
+ }
232
+
180
233
  // Calculate merged translation: sum of components
181
234
  const tx = D(t1.tx).plus(D(t2.tx));
182
235
  const ty = D(t1.ty).plus(D(t2.ty));
@@ -216,6 +269,7 @@ export function mergeTranslations(t1, t2) {
216
269
  * verified: boolean,
217
270
  * maxError: Decimal
218
271
  * }} Merged rotation with verification result
272
+ * @throws {Error} If r1 or r2 is null/undefined or missing angle property
219
273
  *
220
274
  * @example
221
275
  * // Merge rotate(π/4) and rotate(π/4)
@@ -223,6 +277,16 @@ export function mergeTranslations(t1, t2) {
223
277
  * // Result: {angle: π/2, verified: true}
224
278
  */
225
279
  export function mergeRotations(r1, r2) {
280
+ if (!r1 || !r2) {
281
+ throw new Error("mergeRotations: both r1 and r2 parameters are required");
282
+ }
283
+ if (r1.angle === null || r1.angle === undefined) {
284
+ throw new Error("mergeRotations: r1 must have angle property");
285
+ }
286
+ if (r2.angle === null || r2.angle === undefined) {
287
+ throw new Error("mergeRotations: r2 must have angle property");
288
+ }
289
+
226
290
  // Calculate merged rotation: sum of angles
227
291
  const angle = D(r1.angle).plus(D(r2.angle));
228
292
 
@@ -268,6 +332,7 @@ export function mergeRotations(r1, r2) {
268
332
  * verified: boolean,
269
333
  * maxError: Decimal
270
334
  * }} Merged scale with verification result
335
+ * @throws {Error} If s1 or s2 is null/undefined or missing required properties
271
336
  *
272
337
  * @example
273
338
  * // Merge scale(2, 3) and scale(1.5, 0.5)
@@ -275,6 +340,16 @@ export function mergeRotations(r1, r2) {
275
340
  * // Result: {sx: 3, sy: 1.5, verified: true}
276
341
  */
277
342
  export function mergeScales(s1, s2) {
343
+ if (!s1 || !s2) {
344
+ throw new Error("mergeScales: both s1 and s2 parameters are required");
345
+ }
346
+ if (s1.sx === null || s1.sx === undefined || s1.sy === null || s1.sy === undefined) {
347
+ throw new Error("mergeScales: s1 must have sx and sy properties");
348
+ }
349
+ if (s2.sx === null || s2.sx === undefined || s2.sy === null || s2.sy === undefined) {
350
+ throw new Error("mergeScales: s2 must have sx and sy properties");
351
+ }
352
+
278
353
  // Calculate merged scale: product of components
279
354
  const sx = D(s1.sx).mul(D(s2.sx));
280
355
  const sy = D(s1.sy).mul(D(s2.sy));
@@ -318,6 +393,7 @@ export function mergeScales(s1, s2) {
318
393
  * verified: boolean,
319
394
  * maxError: Decimal
320
395
  * }} Translation parameters if matrix is pure translation
396
+ * @throws {Error} If matrix is null, undefined, or not 3x3
321
397
  *
322
398
  * @example
323
399
  * // Check if a matrix is a pure translation
@@ -326,6 +402,16 @@ export function mergeScales(s1, s2) {
326
402
  * // Result: {isTranslation: true, tx: 5, ty: 10, verified: true}
327
403
  */
328
404
  export function matrixToTranslate(matrix) {
405
+ if (!matrix) {
406
+ throw new Error("matrixToTranslate: matrix parameter is required");
407
+ }
408
+ if (!matrix.data || !matrix.rows || !matrix.cols) {
409
+ throw new Error("matrixToTranslate: matrix must be a valid Matrix object");
410
+ }
411
+ if (matrix.rows !== 3 || matrix.cols !== 3) {
412
+ throw new Error(`matrixToTranslate: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`);
413
+ }
414
+
329
415
  const data = matrix.data;
330
416
 
331
417
  // Check if linear part is identity
@@ -386,6 +472,7 @@ export function matrixToTranslate(matrix) {
386
472
  * verified: boolean,
387
473
  * maxError: Decimal
388
474
  * }} Rotation angle if matrix is pure rotation
475
+ * @throws {Error} If matrix is null, undefined, or not 3x3
389
476
  *
390
477
  * @example
391
478
  * // Check if a matrix is a pure rotation
@@ -394,6 +481,16 @@ export function matrixToTranslate(matrix) {
394
481
  * // Result: {isRotation: true, angle: π/4, verified: true}
395
482
  */
396
483
  export function matrixToRotate(matrix) {
484
+ if (!matrix) {
485
+ throw new Error("matrixToRotate: matrix parameter is required");
486
+ }
487
+ if (!matrix.data || !matrix.rows || !matrix.cols) {
488
+ throw new Error("matrixToRotate: matrix must be a valid Matrix object");
489
+ }
490
+ if (matrix.rows !== 3 || matrix.cols !== 3) {
491
+ throw new Error(`matrixToRotate: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`);
492
+ }
493
+
397
494
  const data = matrix.data;
398
495
 
399
496
  // Extract components
@@ -476,6 +573,7 @@ export function matrixToRotate(matrix) {
476
573
  * verified: boolean,
477
574
  * maxError: Decimal
478
575
  * }} Scale factors if matrix is pure scale
576
+ * @throws {Error} If matrix is null, undefined, or not 3x3
479
577
  *
480
578
  * @example
481
579
  * // Check if a matrix is a pure scale
@@ -484,6 +582,16 @@ export function matrixToRotate(matrix) {
484
582
  * // Result: {isScale: true, sx: 2, sy: 2, isUniform: true, verified: true}
485
583
  */
486
584
  export function matrixToScale(matrix) {
585
+ if (!matrix) {
586
+ throw new Error("matrixToScale: matrix parameter is required");
587
+ }
588
+ if (!matrix.data || !matrix.rows || !matrix.cols) {
589
+ throw new Error("matrixToScale: matrix must be a valid Matrix object");
590
+ }
591
+ if (matrix.rows !== 3 || matrix.cols !== 3) {
592
+ throw new Error(`matrixToScale: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`);
593
+ }
594
+
487
595
  const data = matrix.data;
488
596
 
489
597
  // Extract components
@@ -559,6 +667,7 @@ export function matrixToScale(matrix) {
559
667
  * transforms: Array<{type: string, params: Object}>,
560
668
  * removedCount: number
561
669
  * }} Filtered transform list
670
+ * @throws {Error} If transforms is null, undefined, or not an array
562
671
  *
563
672
  * @example
564
673
  * // Remove identity transforms
@@ -572,18 +681,40 @@ export function matrixToScale(matrix) {
572
681
  * // Result: {transforms: [{type: 'translate', params: {tx: 5, ty: 10}}], removedCount: 3}
573
682
  */
574
683
  export function removeIdentityTransforms(transforms) {
684
+ if (!transforms) {
685
+ throw new Error("removeIdentityTransforms: transforms parameter is required");
686
+ }
687
+ if (!Array.isArray(transforms)) {
688
+ throw new Error("removeIdentityTransforms: transforms must be an array");
689
+ }
690
+
575
691
  const PI = Decimal.acos(-1);
576
692
  const TWO_PI = PI.mul(2);
577
693
 
578
694
  const filtered = transforms.filter((t) => {
695
+ // Validate transform object structure
696
+ if (!t || typeof t !== 'object') {
697
+ return true; // Keep malformed transforms for debugging
698
+ }
699
+ if (!t.type || !t.params || typeof t.params !== 'object') {
700
+ return true; // Keep malformed transforms for debugging
701
+ }
702
+
579
703
  switch (t.type) {
580
704
  case "translate": {
705
+ if (t.params.tx === null || t.params.tx === undefined ||
706
+ t.params.ty === null || t.params.ty === undefined) {
707
+ return true; // Keep transforms with missing params for debugging
708
+ }
581
709
  const tx = D(t.params.tx);
582
710
  const ty = D(t.params.ty);
583
711
  return !tx.abs().lessThan(EPSILON) || !ty.abs().lessThan(EPSILON);
584
712
  }
585
713
 
586
714
  case "rotate": {
715
+ if (t.params.angle === null || t.params.angle === undefined) {
716
+ return true; // Keep transforms with missing params for debugging
717
+ }
587
718
  const angle = D(t.params.angle);
588
719
  // Normalize angle to [0, 2π)
589
720
  const normalized = angle.mod(TWO_PI);
@@ -591,6 +722,10 @@ export function removeIdentityTransforms(transforms) {
591
722
  }
592
723
 
593
724
  case "scale": {
725
+ if (t.params.sx === null || t.params.sx === undefined ||
726
+ t.params.sy === null || t.params.sy === undefined) {
727
+ return true; // Keep transforms with missing params for debugging
728
+ }
594
729
  const sx = D(t.params.sx);
595
730
  const sy = D(t.params.sy);
596
731
  return (
@@ -600,6 +735,9 @@ export function removeIdentityTransforms(transforms) {
600
735
  }
601
736
 
602
737
  case "matrix": {
738
+ if (!t.params.matrix) {
739
+ return true; // Keep transforms with missing matrix for debugging
740
+ }
603
741
  // Check if matrix is identity
604
742
  const m = t.params.matrix;
605
743
  const identity = identityMatrix();
@@ -643,6 +781,7 @@ export function removeIdentityTransforms(transforms) {
643
781
  * verified: boolean,
644
782
  * maxError: Decimal
645
783
  * }} Shorthand rotation parameters with verification
784
+ * @throws {Error} If any parameter is null or undefined
646
785
  *
647
786
  * @example
648
787
  * // Convert translate-rotate-translate to rotate around point
@@ -650,6 +789,22 @@ export function removeIdentityTransforms(transforms) {
650
789
  * // Result: {angle: π/4, cx: 100, cy: 50, verified: true}
651
790
  */
652
791
  export function shortRotate(translateX, translateY, angle, centerX, centerY) {
792
+ if (translateX === null || translateX === undefined) {
793
+ throw new Error("shortRotate: translateX parameter is required");
794
+ }
795
+ if (translateY === null || translateY === undefined) {
796
+ throw new Error("shortRotate: translateY parameter is required");
797
+ }
798
+ if (angle === null || angle === undefined) {
799
+ throw new Error("shortRotate: angle parameter is required");
800
+ }
801
+ if (centerX === null || centerX === undefined) {
802
+ throw new Error("shortRotate: centerX parameter is required");
803
+ }
804
+ if (centerY === null || centerY === undefined) {
805
+ throw new Error("shortRotate: centerY parameter is required");
806
+ }
807
+
653
808
  const txD = D(translateX);
654
809
  const tyD = D(translateY);
655
810
  const angleD = D(angle);
@@ -697,6 +852,7 @@ export function shortRotate(translateX, translateY, angle, centerX, centerY) {
697
852
  * verified: boolean,
698
853
  * maxError: Decimal
699
854
  * }} Optimized transform list with verification
855
+ * @throws {Error} If transforms is null, undefined, or not an array
700
856
  *
701
857
  * @example
702
858
  * // Optimize a transform list
@@ -711,16 +867,36 @@ export function shortRotate(translateX, translateY, angle, centerX, centerY) {
711
867
  * // Result: optimized list with merged translations and scales, identity rotation removed
712
868
  */
713
869
  export function optimizeTransformList(transforms) {
870
+ if (!transforms) {
871
+ throw new Error("optimizeTransformList: transforms parameter is required");
872
+ }
873
+ if (!Array.isArray(transforms)) {
874
+ throw new Error("optimizeTransformList: transforms must be an array");
875
+ }
876
+
714
877
  // Calculate original combined matrix for verification
715
878
  let originalMatrix = identityMatrix();
716
879
  for (const t of transforms) {
717
- let m;
880
+ // Validate transform object structure
881
+ if (!t || typeof t !== 'object' || !t.type || !t.params || typeof t.params !== 'object') {
882
+ continue; // Skip malformed transforms
883
+ }
884
+
885
+ let m = null; // Initialize m to null to catch missing assignments
718
886
  switch (t.type) {
719
887
  case "translate":
888
+ if (t.params.tx === null || t.params.tx === undefined ||
889
+ t.params.ty === null || t.params.ty === undefined) {
890
+ continue; // Skip transforms with missing params
891
+ }
720
892
  m = translationMatrix(t.params.tx, t.params.ty);
721
893
  break;
722
894
  case "rotate":
723
- if (t.params.cx !== undefined && t.params.cy !== undefined) {
895
+ if (t.params.angle === null || t.params.angle === undefined) {
896
+ continue; // Skip transforms with missing angle
897
+ }
898
+ if (t.params.cx !== undefined && t.params.cx !== null &&
899
+ t.params.cy !== undefined && t.params.cy !== null) {
724
900
  m = rotationMatrixAroundPoint(
725
901
  t.params.angle,
726
902
  t.params.cx,
@@ -731,15 +907,26 @@ export function optimizeTransformList(transforms) {
731
907
  }
732
908
  break;
733
909
  case "scale":
910
+ if (t.params.sx === null || t.params.sx === undefined ||
911
+ t.params.sy === null || t.params.sy === undefined) {
912
+ continue; // Skip transforms with missing params
913
+ }
734
914
  m = scaleMatrix(t.params.sx, t.params.sy);
735
915
  break;
736
916
  case "matrix":
917
+ if (!t.params.matrix) {
918
+ continue; // Skip transforms with missing matrix
919
+ }
737
920
  m = t.params.matrix;
738
921
  break;
739
922
  default:
923
+ // Skip unknown transform types, but don't try to multiply null matrix
740
924
  continue;
741
925
  }
742
- originalMatrix = originalMatrix.mul(m);
926
+ // Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
927
+ if (m !== null) {
928
+ originalMatrix = originalMatrix.mul(m);
929
+ }
743
930
  }
744
931
 
745
932
  // Step 1: Remove identity transforms
@@ -753,6 +940,13 @@ export function optimizeTransformList(transforms) {
753
940
  const current = optimized[i];
754
941
  const next = optimized[i + 1];
755
942
 
943
+ // Validate transform objects before processing
944
+ if (!current || !current.type || !current.params ||
945
+ !next || !next.type || !next.params) {
946
+ i++;
947
+ continue;
948
+ }
949
+
756
950
  // Try to merge
757
951
  let merged = null;
758
952
 
@@ -765,13 +959,13 @@ export function optimizeTransformList(transforms) {
765
959
  };
766
960
  }
767
961
  } else if (current.type === "rotate" && next.type === "rotate") {
768
- // Only merge if both are around origin
769
- if (
770
- !current.params.cx &&
771
- !current.params.cy &&
772
- !next.params.cx &&
773
- !next.params.cy
774
- ) {
962
+ // Only merge if both are around origin (cx and cy must be undefined or null, not 0)
963
+ const currentHasCenter = (current.params.cx !== undefined && current.params.cx !== null) ||
964
+ (current.params.cy !== undefined && current.params.cy !== null);
965
+ const nextHasCenter = (next.params.cx !== undefined && next.params.cx !== null) ||
966
+ (next.params.cy !== undefined && next.params.cy !== null);
967
+
968
+ if (!currentHasCenter && !nextHasCenter) {
775
969
  const result = mergeRotations(current.params, next.params);
776
970
  if (result.verified) {
777
971
  merged = {
@@ -806,11 +1000,29 @@ export function optimizeTransformList(transforms) {
806
1000
  const t2 = optimized[i + 1];
807
1001
  const t3 = optimized[i + 2];
808
1002
 
1003
+ // Validate transform objects before processing
1004
+ if (!t1 || !t1.type || !t1.params ||
1005
+ !t2 || !t2.type || !t2.params ||
1006
+ !t3 || !t3.type || !t3.params) {
1007
+ i++;
1008
+ continue;
1009
+ }
1010
+
809
1011
  if (
810
1012
  t1.type === "translate" &&
811
1013
  t2.type === "rotate" &&
812
1014
  t3.type === "translate"
813
1015
  ) {
1016
+ // Validate required parameters exist
1017
+ if (t1.params.tx === null || t1.params.tx === undefined ||
1018
+ t1.params.ty === null || t1.params.ty === undefined ||
1019
+ t2.params.angle === null || t2.params.angle === undefined ||
1020
+ t3.params.tx === null || t3.params.tx === undefined ||
1021
+ t3.params.ty === null || t3.params.ty === undefined) {
1022
+ i++;
1023
+ continue;
1024
+ }
1025
+
814
1026
  // Check if t3 is inverse of t1
815
1027
  const tx1 = D(t1.params.tx);
816
1028
  const ty1 = D(t1.params.ty);
@@ -841,7 +1053,17 @@ export function optimizeTransformList(transforms) {
841
1053
  // Step 4: Convert matrices to simpler forms
842
1054
  for (let j = 0; j < optimized.length; j++) {
843
1055
  const t = optimized[j];
1056
+
1057
+ // Validate transform object before processing
1058
+ if (!t || !t.type || !t.params) {
1059
+ continue;
1060
+ }
1061
+
844
1062
  if (t.type === "matrix") {
1063
+ if (!t.params.matrix) {
1064
+ continue; // Skip if matrix is missing
1065
+ }
1066
+
845
1067
  const m = t.params.matrix;
846
1068
 
847
1069
  // Try to convert to simpler forms
@@ -880,13 +1102,26 @@ export function optimizeTransformList(transforms) {
880
1102
  // Calculate optimized combined matrix for verification
881
1103
  let optimizedMatrix = identityMatrix();
882
1104
  for (const t of final) {
883
- let m;
1105
+ // Validate transform object structure
1106
+ if (!t || typeof t !== 'object' || !t.type || !t.params || typeof t.params !== 'object') {
1107
+ continue; // Skip malformed transforms
1108
+ }
1109
+
1110
+ let m = null; // Initialize m to null to catch missing assignments
884
1111
  switch (t.type) {
885
1112
  case "translate":
1113
+ if (t.params.tx === null || t.params.tx === undefined ||
1114
+ t.params.ty === null || t.params.ty === undefined) {
1115
+ continue; // Skip transforms with missing params
1116
+ }
886
1117
  m = translationMatrix(t.params.tx, t.params.ty);
887
1118
  break;
888
1119
  case "rotate":
889
- if (t.params.cx !== undefined && t.params.cy !== undefined) {
1120
+ if (t.params.angle === null || t.params.angle === undefined) {
1121
+ continue; // Skip transforms with missing angle
1122
+ }
1123
+ if (t.params.cx !== undefined && t.params.cx !== null &&
1124
+ t.params.cy !== undefined && t.params.cy !== null) {
890
1125
  m = rotationMatrixAroundPoint(
891
1126
  t.params.angle,
892
1127
  t.params.cx,
@@ -897,15 +1132,26 @@ export function optimizeTransformList(transforms) {
897
1132
  }
898
1133
  break;
899
1134
  case "scale":
1135
+ if (t.params.sx === null || t.params.sx === undefined ||
1136
+ t.params.sy === null || t.params.sy === undefined) {
1137
+ continue; // Skip transforms with missing params
1138
+ }
900
1139
  m = scaleMatrix(t.params.sx, t.params.sy);
901
1140
  break;
902
1141
  case "matrix":
1142
+ if (!t.params.matrix) {
1143
+ continue; // Skip transforms with missing matrix
1144
+ }
903
1145
  m = t.params.matrix;
904
1146
  break;
905
1147
  default:
1148
+ // Skip unknown transform types, but don't try to multiply null matrix
906
1149
  continue;
907
1150
  }
908
- optimizedMatrix = optimizedMatrix.mul(m);
1151
+ // Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
1152
+ if (m !== null) {
1153
+ optimizedMatrix = optimizedMatrix.mul(m);
1154
+ }
909
1155
  }
910
1156
 
911
1157
  // VERIFICATION: Combined matrices must be equal
@@ -2,11 +2,28 @@ import Decimal from "decimal.js";
2
2
  import { Matrix } from "./matrix.js";
3
3
 
4
4
  /**
5
- * Helper to convert any numeric input to Decimal.
5
+ * Helper to convert any numeric input to Decimal with validation.
6
6
  * @param {number|string|Decimal} x - The value to convert
7
7
  * @returns {Decimal} The Decimal representation
8
+ * @throws {Error} If the value is invalid or infinite
8
9
  */
9
- const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
10
+ const D = (x) => {
11
+ if (x instanceof Decimal) {
12
+ if (!x.isFinite()) {
13
+ throw new Error(`Value must be finite (got ${x.toString()})`);
14
+ }
15
+ return x;
16
+ }
17
+ try {
18
+ const result = new Decimal(x);
19
+ if (!result.isFinite()) {
20
+ throw new Error(`Value must be finite (got ${x})`);
21
+ }
22
+ return result;
23
+ } catch (error) {
24
+ throw new Error(`Invalid numeric value: "${x}" (${error.message})`);
25
+ }
26
+ };
10
27
 
11
28
  /**
12
29
  * Validates that a value can be converted to Decimal.
@@ -21,6 +38,14 @@ function validateNumeric(value, name) {
21
38
  if (typeof value !== 'number' && typeof value !== 'string' && !(value instanceof Decimal)) {
22
39
  throw new Error(`${name} must be a number, string, or Decimal`);
23
40
  }
41
+ // Check for NaN and Infinity in numeric values
42
+ if (typeof value === 'number' && !isFinite(value)) {
43
+ throw new Error(`${name} must be a finite number (got ${value})`);
44
+ }
45
+ // Check for Infinity in Decimal instances
46
+ if (value instanceof Decimal && !value.isFinite()) {
47
+ throw new Error(`${name} must be a finite Decimal (got ${value.toString()})`);
48
+ }
24
49
  }
25
50
 
26
51
  /**
@@ -175,9 +200,15 @@ export function scale(sx, sy = null) {
175
200
  validateNumeric(sy, 'sy');
176
201
  }
177
202
  const syValue = sy === null ? sx : sy;
203
+ // Check for zero scale factors which create singular matrices
204
+ const sxD = D(sx);
205
+ const syD = D(syValue);
206
+ if (sxD.isZero() || syD.isZero()) {
207
+ throw new Error('Scale factors cannot be zero (creates singular matrix)');
208
+ }
178
209
  return Matrix.from([
179
- [D(sx), new Decimal(0), new Decimal(0)],
180
- [new Decimal(0), D(syValue), new Decimal(0)],
210
+ [sxD, new Decimal(0), new Decimal(0)],
211
+ [new Decimal(0), syD, new Decimal(0)],
181
212
  [new Decimal(0), new Decimal(0), new Decimal(1)],
182
213
  ]);
183
214
  }
@@ -292,6 +323,9 @@ export function rotate(theta) {
292
323
  * // Result: point moves 30° counterclockwise around the center
293
324
  */
294
325
  export function rotateAroundPoint(theta, px, py) {
326
+ validateNumeric(theta, 'theta');
327
+ validateNumeric(px, 'px');
328
+ validateNumeric(py, 'py');
295
329
  const pxD = D(px);
296
330
  const pyD = D(py);
297
331
  return translation(pxD, pyD)
@@ -354,9 +388,18 @@ export function rotateAroundPoint(theta, px, py) {
354
388
  * // Both coordinates affect each other, creating complex shearing
355
389
  */
356
390
  export function skew(ax, ay) {
391
+ validateNumeric(ax, 'ax');
392
+ validateNumeric(ay, 'ay');
393
+ const axD = D(ax);
394
+ const ayD = D(ay);
395
+ // Check determinant: det = 1 - ax*ay. If det <= 0, matrix is singular or inverts orientation
396
+ const det = new Decimal(1).minus(axD.mul(ayD));
397
+ if (det.lessThanOrEqualTo(0)) {
398
+ throw new Error(`Skew parameters create singular or orientation-inverting matrix (ax*ay = ${axD.mul(ayD).toString()}, must be < 1)`);
399
+ }
357
400
  return Matrix.from([
358
- [new Decimal(1), D(ax), new Decimal(0)],
359
- [D(ay), new Decimal(1), new Decimal(0)],
401
+ [new Decimal(1), axD, new Decimal(0)],
402
+ [ayD, new Decimal(1), new Decimal(0)],
360
403
  [new Decimal(0), new Decimal(0), new Decimal(1)],
361
404
  ]);
362
405
  }
@@ -421,9 +464,21 @@ export function skew(ax, ay) {
421
464
  * // Result: x = 10, y = 10 (Y compressed to half)
422
465
  */
423
466
  export function stretchAlongAxis(ux, uy, k) {
424
- const uxD = D(ux),
425
- uyD = D(uy),
426
- kD = D(k);
467
+ validateNumeric(ux, 'ux');
468
+ validateNumeric(uy, 'uy');
469
+ validateNumeric(k, 'k');
470
+ const uxD = D(ux);
471
+ const uyD = D(uy);
472
+ const kD = D(k);
473
+ // Check if k is zero which creates singular matrix
474
+ if (kD.isZero()) {
475
+ throw new Error('Stretch factor k cannot be zero (creates singular matrix)');
476
+ }
477
+ // Warn if axis vector is not normalized (optional but recommended)
478
+ const normSquared = uxD.mul(uxD).plus(uyD.mul(uyD));
479
+ if (normSquared.isZero()) {
480
+ throw new Error('Axis vector (ux, uy) cannot be zero');
481
+ }
427
482
  const one = new Decimal(1);
428
483
  const factor = kD.minus(one);
429
484
  const m00 = one.plus(factor.mul(uxD.mul(uxD)));
@@ -498,13 +553,31 @@ export function applyTransform(M, x, y) {
498
553
  if (!M || typeof M.mul !== 'function') {
499
554
  throw new Error('applyTransform: first argument must be a Matrix');
500
555
  }
556
+ // Check matrix dimensions
557
+ if (!M.data || !Array.isArray(M.data) || M.data.length !== 3 ||
558
+ !M.data[0] || M.data[0].length !== 3 ||
559
+ !M.data[1] || M.data[1].length !== 3 ||
560
+ !M.data[2] || M.data[2].length !== 3) {
561
+ throw new Error('applyTransform: matrix must be 3x3');
562
+ }
501
563
  validateNumeric(x, 'x');
502
564
  validateNumeric(y, 'y');
503
565
  const P = Matrix.from([[D(x)], [D(y)], [new Decimal(1)]]);
504
566
  const R = M.mul(P);
567
+ // Validate result matrix structure
568
+ if (!R || !R.data || !Array.isArray(R.data) || R.data.length !== 3 ||
569
+ !R.data[0] || !R.data[0][0] ||
570
+ !R.data[1] || !R.data[1][0] ||
571
+ !R.data[2] || !R.data[2][0]) {
572
+ throw new Error('applyTransform: matrix multiplication produced invalid result');
573
+ }
505
574
  const rx = R.data[0][0],
506
575
  ry = R.data[1][0],
507
576
  rw = R.data[2][0];
577
+ // Check for zero division in perspective division
578
+ if (rw.isZero()) {
579
+ throw new Error('applyTransform: perspective division by zero (invalid transformation matrix)');
580
+ }
508
581
  // Perspective division (for affine transforms, rw is always 1)
509
582
  return [rx.div(rw), ry.div(rw)];
510
583
  }