@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.
- package/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
package/src/transforms2d.js
CHANGED
|
@@ -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) =>
|
|
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
|
-
[
|
|
180
|
-
[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),
|
|
359
|
-
[
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
}
|