@emasoft/svg-matrix 1.1.0 → 1.2.0
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 +7 -6
- package/bin/svgm.js +109 -40
- package/dist/svg-matrix.min.js +7 -7
- package/dist/svg-toolbox.min.js +148 -228
- package/dist/svgm.min.js +152 -232
- package/dist/version.json +5 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -41
- package/scripts/test-postinstall.js +18 -16
- package/scripts/version-sync.js +78 -60
- package/src/animation-optimization.js +190 -98
- package/src/animation-references.js +11 -3
- package/src/arc-length.js +23 -20
- package/src/bezier-analysis.js +9 -13
- package/src/bezier-intersections.js +18 -4
- package/src/browser-verify.js +35 -8
- package/src/clip-path-resolver.js +285 -114
- package/src/convert-path-data.js +20 -8
- package/src/css-specificity.js +33 -9
- package/src/douglas-peucker.js +272 -141
- package/src/geometry-to-path.js +79 -22
- package/src/gjk-collision.js +287 -126
- package/src/index.js +56 -21
- package/src/inkscape-support.js +122 -101
- package/src/logger.js +43 -27
- package/src/marker-resolver.js +201 -121
- package/src/mask-resolver.js +231 -98
- package/src/matrix.js +9 -5
- package/src/mesh-gradient.js +22 -14
- package/src/off-canvas-detection.js +53 -17
- package/src/path-optimization.js +356 -171
- package/src/path-simplification.js +671 -256
- package/src/pattern-resolver.js +1 -3
- package/src/polygon-clip.js +396 -78
- package/src/svg-boolean-ops.js +90 -23
- package/src/svg-collections.js +1546 -667
- package/src/svg-flatten.js +152 -38
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +5 -1
- package/src/svg-rendering-context.js +3 -1
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-toolbox.js +99 -457
- package/src/svg-validation-data.js +513 -345
- package/src/svg2-polyfills.js +156 -93
- package/src/svgm-lib.js +8 -4
- package/src/transform-optimization.js +168 -51
- package/src/transforms2d.js +73 -40
- package/src/transforms3d.js +34 -27
- package/src/use-symbol-resolver.js +175 -76
- package/src/vector.js +80 -44
- package/src/vendor/inkscape-hatch-polyfill.js +143 -108
- package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
- package/src/vendor/inkscape-mesh-polyfill.js +953 -766
- package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
- package/src/verification.js +3 -4
|
@@ -155,7 +155,9 @@ export function scaleMatrix(sx, sy) {
|
|
|
155
155
|
*/
|
|
156
156
|
export function matrixMaxDifference(m1, m2) {
|
|
157
157
|
if (!m1 || !m2) {
|
|
158
|
-
throw new Error(
|
|
158
|
+
throw new Error(
|
|
159
|
+
"matrixMaxDifference: both m1 and m2 parameters are required",
|
|
160
|
+
);
|
|
159
161
|
}
|
|
160
162
|
if (!m1.rows || !m1.cols || !m1.data) {
|
|
161
163
|
throw new Error("matrixMaxDifference: m1 must be a valid Matrix object");
|
|
@@ -164,7 +166,9 @@ export function matrixMaxDifference(m1, m2) {
|
|
|
164
166
|
throw new Error("matrixMaxDifference: m2 must be a valid Matrix object");
|
|
165
167
|
}
|
|
166
168
|
if (m1.rows !== m2.rows || m1.cols !== m2.cols) {
|
|
167
|
-
throw new Error(
|
|
169
|
+
throw new Error(
|
|
170
|
+
`matrixMaxDifference: matrix dimensions must match (m1: ${m1.rows}x${m1.cols}, m2: ${m2.rows}x${m2.cols})`,
|
|
171
|
+
);
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
let maxDiff = D(0);
|
|
@@ -221,12 +225,24 @@ export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
|
|
|
221
225
|
*/
|
|
222
226
|
export function mergeTranslations(t1, t2) {
|
|
223
227
|
if (!t1 || !t2) {
|
|
224
|
-
throw new Error(
|
|
228
|
+
throw new Error(
|
|
229
|
+
"mergeTranslations: both t1 and t2 parameters are required",
|
|
230
|
+
);
|
|
225
231
|
}
|
|
226
|
-
if (
|
|
232
|
+
if (
|
|
233
|
+
t1.tx === null ||
|
|
234
|
+
t1.tx === undefined ||
|
|
235
|
+
t1.ty === null ||
|
|
236
|
+
t1.ty === undefined
|
|
237
|
+
) {
|
|
227
238
|
throw new Error("mergeTranslations: t1 must have tx and ty properties");
|
|
228
239
|
}
|
|
229
|
-
if (
|
|
240
|
+
if (
|
|
241
|
+
t2.tx === null ||
|
|
242
|
+
t2.tx === undefined ||
|
|
243
|
+
t2.ty === null ||
|
|
244
|
+
t2.ty === undefined
|
|
245
|
+
) {
|
|
230
246
|
throw new Error("mergeTranslations: t2 must have tx and ty properties");
|
|
231
247
|
}
|
|
232
248
|
|
|
@@ -343,10 +359,20 @@ export function mergeScales(s1, s2) {
|
|
|
343
359
|
if (!s1 || !s2) {
|
|
344
360
|
throw new Error("mergeScales: both s1 and s2 parameters are required");
|
|
345
361
|
}
|
|
346
|
-
if (
|
|
362
|
+
if (
|
|
363
|
+
s1.sx === null ||
|
|
364
|
+
s1.sx === undefined ||
|
|
365
|
+
s1.sy === null ||
|
|
366
|
+
s1.sy === undefined
|
|
367
|
+
) {
|
|
347
368
|
throw new Error("mergeScales: s1 must have sx and sy properties");
|
|
348
369
|
}
|
|
349
|
-
if (
|
|
370
|
+
if (
|
|
371
|
+
s2.sx === null ||
|
|
372
|
+
s2.sx === undefined ||
|
|
373
|
+
s2.sy === null ||
|
|
374
|
+
s2.sy === undefined
|
|
375
|
+
) {
|
|
350
376
|
throw new Error("mergeScales: s2 must have sx and sy properties");
|
|
351
377
|
}
|
|
352
378
|
|
|
@@ -409,7 +435,9 @@ export function matrixToTranslate(matrix) {
|
|
|
409
435
|
throw new Error("matrixToTranslate: matrix must be a valid Matrix object");
|
|
410
436
|
}
|
|
411
437
|
if (matrix.rows !== 3 || matrix.cols !== 3) {
|
|
412
|
-
throw new Error(
|
|
438
|
+
throw new Error(
|
|
439
|
+
`matrixToTranslate: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`,
|
|
440
|
+
);
|
|
413
441
|
}
|
|
414
442
|
|
|
415
443
|
const data = matrix.data;
|
|
@@ -488,7 +516,9 @@ export function matrixToRotate(matrix) {
|
|
|
488
516
|
throw new Error("matrixToRotate: matrix must be a valid Matrix object");
|
|
489
517
|
}
|
|
490
518
|
if (matrix.rows !== 3 || matrix.cols !== 3) {
|
|
491
|
-
throw new Error(
|
|
519
|
+
throw new Error(
|
|
520
|
+
`matrixToRotate: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`,
|
|
521
|
+
);
|
|
492
522
|
}
|
|
493
523
|
|
|
494
524
|
const data = matrix.data;
|
|
@@ -589,7 +619,9 @@ export function matrixToScale(matrix) {
|
|
|
589
619
|
throw new Error("matrixToScale: matrix must be a valid Matrix object");
|
|
590
620
|
}
|
|
591
621
|
if (matrix.rows !== 3 || matrix.cols !== 3) {
|
|
592
|
-
throw new Error(
|
|
622
|
+
throw new Error(
|
|
623
|
+
`matrixToScale: matrix must be 3x3 (got ${matrix.rows}x${matrix.cols})`,
|
|
624
|
+
);
|
|
593
625
|
}
|
|
594
626
|
|
|
595
627
|
const data = matrix.data;
|
|
@@ -682,7 +714,9 @@ export function matrixToScale(matrix) {
|
|
|
682
714
|
*/
|
|
683
715
|
export function removeIdentityTransforms(transforms) {
|
|
684
716
|
if (!transforms) {
|
|
685
|
-
throw new Error(
|
|
717
|
+
throw new Error(
|
|
718
|
+
"removeIdentityTransforms: transforms parameter is required",
|
|
719
|
+
);
|
|
686
720
|
}
|
|
687
721
|
if (!Array.isArray(transforms)) {
|
|
688
722
|
throw new Error("removeIdentityTransforms: transforms must be an array");
|
|
@@ -693,17 +727,21 @@ export function removeIdentityTransforms(transforms) {
|
|
|
693
727
|
|
|
694
728
|
const filtered = transforms.filter((t) => {
|
|
695
729
|
// Validate transform object structure
|
|
696
|
-
if (!t || typeof t !==
|
|
730
|
+
if (!t || typeof t !== "object") {
|
|
697
731
|
return true; // Keep malformed transforms for debugging
|
|
698
732
|
}
|
|
699
|
-
if (!t.type || !t.params || typeof t.params !==
|
|
733
|
+
if (!t.type || !t.params || typeof t.params !== "object") {
|
|
700
734
|
return true; // Keep malformed transforms for debugging
|
|
701
735
|
}
|
|
702
736
|
|
|
703
737
|
switch (t.type) {
|
|
704
738
|
case "translate": {
|
|
705
|
-
if (
|
|
706
|
-
|
|
739
|
+
if (
|
|
740
|
+
t.params.tx === null ||
|
|
741
|
+
t.params.tx === undefined ||
|
|
742
|
+
t.params.ty === null ||
|
|
743
|
+
t.params.ty === undefined
|
|
744
|
+
) {
|
|
707
745
|
return true; // Keep transforms with missing params for debugging
|
|
708
746
|
}
|
|
709
747
|
const tx = D(t.params.tx);
|
|
@@ -725,8 +763,12 @@ export function removeIdentityTransforms(transforms) {
|
|
|
725
763
|
}
|
|
726
764
|
|
|
727
765
|
case "scale": {
|
|
728
|
-
if (
|
|
729
|
-
|
|
766
|
+
if (
|
|
767
|
+
t.params.sx === null ||
|
|
768
|
+
t.params.sx === undefined ||
|
|
769
|
+
t.params.sy === null ||
|
|
770
|
+
t.params.sy === undefined
|
|
771
|
+
) {
|
|
730
772
|
return true; // Keep transforms with missing params for debugging
|
|
731
773
|
}
|
|
732
774
|
const sx = D(t.params.sx);
|
|
@@ -881,15 +923,25 @@ export function optimizeTransformList(transforms) {
|
|
|
881
923
|
let originalMatrix = identityMatrix();
|
|
882
924
|
for (const t of transforms) {
|
|
883
925
|
// Validate transform object structure
|
|
884
|
-
if (
|
|
926
|
+
if (
|
|
927
|
+
!t ||
|
|
928
|
+
typeof t !== "object" ||
|
|
929
|
+
!t.type ||
|
|
930
|
+
!t.params ||
|
|
931
|
+
typeof t.params !== "object"
|
|
932
|
+
) {
|
|
885
933
|
continue; // Skip malformed transforms
|
|
886
934
|
}
|
|
887
935
|
|
|
888
936
|
let m = null; // Initialize m to null to catch missing assignments
|
|
889
937
|
switch (t.type) {
|
|
890
938
|
case "translate":
|
|
891
|
-
if (
|
|
892
|
-
|
|
939
|
+
if (
|
|
940
|
+
t.params.tx === null ||
|
|
941
|
+
t.params.tx === undefined ||
|
|
942
|
+
t.params.ty === null ||
|
|
943
|
+
t.params.ty === undefined
|
|
944
|
+
) {
|
|
893
945
|
continue; // Skip transforms with missing params
|
|
894
946
|
}
|
|
895
947
|
m = translationMatrix(t.params.tx, t.params.ty);
|
|
@@ -898,8 +950,12 @@ export function optimizeTransformList(transforms) {
|
|
|
898
950
|
if (t.params.angle === null || t.params.angle === undefined) {
|
|
899
951
|
continue; // Skip transforms with missing angle
|
|
900
952
|
}
|
|
901
|
-
if (
|
|
902
|
-
|
|
953
|
+
if (
|
|
954
|
+
t.params.cx !== undefined &&
|
|
955
|
+
t.params.cx !== null &&
|
|
956
|
+
t.params.cy !== undefined &&
|
|
957
|
+
t.params.cy !== null
|
|
958
|
+
) {
|
|
903
959
|
m = rotationMatrixAroundPoint(
|
|
904
960
|
t.params.angle,
|
|
905
961
|
t.params.cx,
|
|
@@ -910,8 +966,12 @@ export function optimizeTransformList(transforms) {
|
|
|
910
966
|
}
|
|
911
967
|
break;
|
|
912
968
|
case "scale":
|
|
913
|
-
if (
|
|
914
|
-
|
|
969
|
+
if (
|
|
970
|
+
t.params.sx === null ||
|
|
971
|
+
t.params.sx === undefined ||
|
|
972
|
+
t.params.sy === null ||
|
|
973
|
+
t.params.sy === undefined
|
|
974
|
+
) {
|
|
915
975
|
continue; // Skip transforms with missing params
|
|
916
976
|
}
|
|
917
977
|
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
@@ -944,8 +1004,14 @@ export function optimizeTransformList(transforms) {
|
|
|
944
1004
|
const next = optimized[i + 1];
|
|
945
1005
|
|
|
946
1006
|
// Validate transform objects before processing
|
|
947
|
-
if (
|
|
948
|
-
|
|
1007
|
+
if (
|
|
1008
|
+
!current ||
|
|
1009
|
+
!current.type ||
|
|
1010
|
+
!current.params ||
|
|
1011
|
+
!next ||
|
|
1012
|
+
!next.type ||
|
|
1013
|
+
!next.params
|
|
1014
|
+
) {
|
|
949
1015
|
i++;
|
|
950
1016
|
continue;
|
|
951
1017
|
}
|
|
@@ -963,18 +1029,36 @@ export function optimizeTransformList(transforms) {
|
|
|
963
1029
|
}
|
|
964
1030
|
} else if (current.type === "rotate" && next.type === "rotate") {
|
|
965
1031
|
// Only merge if both are around origin: cx and cy are undefined/null OR both are ≈0
|
|
966
|
-
const currentCx =
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1032
|
+
const currentCx =
|
|
1033
|
+
current.params.cx !== undefined && current.params.cx !== null
|
|
1034
|
+
? D(current.params.cx)
|
|
1035
|
+
: null;
|
|
1036
|
+
const currentCy =
|
|
1037
|
+
current.params.cy !== undefined && current.params.cy !== null
|
|
1038
|
+
? D(current.params.cy)
|
|
1039
|
+
: null;
|
|
1040
|
+
const nextCx =
|
|
1041
|
+
next.params.cx !== undefined && next.params.cx !== null
|
|
1042
|
+
? D(next.params.cx)
|
|
1043
|
+
: null;
|
|
1044
|
+
const nextCy =
|
|
1045
|
+
next.params.cy !== undefined && next.params.cy !== null
|
|
1046
|
+
? D(next.params.cy)
|
|
1047
|
+
: null;
|
|
970
1048
|
|
|
971
1049
|
// Check if rotation is effectively around origin
|
|
972
|
-
const currentIsOrigin =
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1050
|
+
const currentIsOrigin =
|
|
1051
|
+
(currentCx === null && currentCy === null) ||
|
|
1052
|
+
(currentCx !== null &&
|
|
1053
|
+
currentCy !== null &&
|
|
1054
|
+
currentCx.abs().lessThan(EPSILON) &&
|
|
1055
|
+
currentCy.abs().lessThan(EPSILON));
|
|
1056
|
+
const nextIsOrigin =
|
|
1057
|
+
(nextCx === null && nextCy === null) ||
|
|
1058
|
+
(nextCx !== null &&
|
|
1059
|
+
nextCy !== null &&
|
|
1060
|
+
nextCx.abs().lessThan(EPSILON) &&
|
|
1061
|
+
nextCy.abs().lessThan(EPSILON));
|
|
978
1062
|
|
|
979
1063
|
if (currentIsOrigin && nextIsOrigin) {
|
|
980
1064
|
const result = mergeRotations(current.params, next.params);
|
|
@@ -1012,9 +1096,17 @@ export function optimizeTransformList(transforms) {
|
|
|
1012
1096
|
const t3 = optimized[i + 2];
|
|
1013
1097
|
|
|
1014
1098
|
// Validate transform objects before processing
|
|
1015
|
-
if (
|
|
1016
|
-
|
|
1017
|
-
|
|
1099
|
+
if (
|
|
1100
|
+
!t1 ||
|
|
1101
|
+
!t1.type ||
|
|
1102
|
+
!t1.params ||
|
|
1103
|
+
!t2 ||
|
|
1104
|
+
!t2.type ||
|
|
1105
|
+
!t2.params ||
|
|
1106
|
+
!t3 ||
|
|
1107
|
+
!t3.type ||
|
|
1108
|
+
!t3.params
|
|
1109
|
+
) {
|
|
1018
1110
|
i++;
|
|
1019
1111
|
continue;
|
|
1020
1112
|
}
|
|
@@ -1025,11 +1117,18 @@ export function optimizeTransformList(transforms) {
|
|
|
1025
1117
|
t3.type === "translate"
|
|
1026
1118
|
) {
|
|
1027
1119
|
// Validate required parameters exist
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1120
|
+
if (
|
|
1121
|
+
t1.params.tx === null ||
|
|
1122
|
+
t1.params.tx === undefined ||
|
|
1123
|
+
t1.params.ty === null ||
|
|
1124
|
+
t1.params.ty === undefined ||
|
|
1125
|
+
t2.params.angle === null ||
|
|
1126
|
+
t2.params.angle === undefined ||
|
|
1127
|
+
t3.params.tx === null ||
|
|
1128
|
+
t3.params.tx === undefined ||
|
|
1129
|
+
t3.params.ty === null ||
|
|
1130
|
+
t3.params.ty === undefined
|
|
1131
|
+
) {
|
|
1033
1132
|
i++;
|
|
1034
1133
|
continue;
|
|
1035
1134
|
}
|
|
@@ -1114,15 +1213,25 @@ export function optimizeTransformList(transforms) {
|
|
|
1114
1213
|
let optimizedMatrix = identityMatrix();
|
|
1115
1214
|
for (const t of final) {
|
|
1116
1215
|
// Validate transform object structure
|
|
1117
|
-
if (
|
|
1216
|
+
if (
|
|
1217
|
+
!t ||
|
|
1218
|
+
typeof t !== "object" ||
|
|
1219
|
+
!t.type ||
|
|
1220
|
+
!t.params ||
|
|
1221
|
+
typeof t.params !== "object"
|
|
1222
|
+
) {
|
|
1118
1223
|
continue; // Skip malformed transforms
|
|
1119
1224
|
}
|
|
1120
1225
|
|
|
1121
1226
|
let m = null; // Initialize m to null to catch missing assignments
|
|
1122
1227
|
switch (t.type) {
|
|
1123
1228
|
case "translate":
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1229
|
+
if (
|
|
1230
|
+
t.params.tx === null ||
|
|
1231
|
+
t.params.tx === undefined ||
|
|
1232
|
+
t.params.ty === null ||
|
|
1233
|
+
t.params.ty === undefined
|
|
1234
|
+
) {
|
|
1126
1235
|
continue; // Skip transforms with missing params
|
|
1127
1236
|
}
|
|
1128
1237
|
m = translationMatrix(t.params.tx, t.params.ty);
|
|
@@ -1131,8 +1240,12 @@ export function optimizeTransformList(transforms) {
|
|
|
1131
1240
|
if (t.params.angle === null || t.params.angle === undefined) {
|
|
1132
1241
|
continue; // Skip transforms with missing angle
|
|
1133
1242
|
}
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1243
|
+
if (
|
|
1244
|
+
t.params.cx !== undefined &&
|
|
1245
|
+
t.params.cx !== null &&
|
|
1246
|
+
t.params.cy !== undefined &&
|
|
1247
|
+
t.params.cy !== null
|
|
1248
|
+
) {
|
|
1136
1249
|
m = rotationMatrixAroundPoint(
|
|
1137
1250
|
t.params.angle,
|
|
1138
1251
|
t.params.cx,
|
|
@@ -1143,8 +1256,12 @@ export function optimizeTransformList(transforms) {
|
|
|
1143
1256
|
}
|
|
1144
1257
|
break;
|
|
1145
1258
|
case "scale":
|
|
1146
|
-
if (
|
|
1147
|
-
|
|
1259
|
+
if (
|
|
1260
|
+
t.params.sx === null ||
|
|
1261
|
+
t.params.sx === undefined ||
|
|
1262
|
+
t.params.sy === null ||
|
|
1263
|
+
t.params.sy === undefined
|
|
1264
|
+
) {
|
|
1148
1265
|
continue; // Skip transforms with missing params
|
|
1149
1266
|
}
|
|
1150
1267
|
m = scaleMatrix(t.params.sx, t.params.sy);
|
package/src/transforms2d.js
CHANGED
|
@@ -35,16 +35,22 @@ function validateNumeric(value, name) {
|
|
|
35
35
|
if (value === undefined || value === null) {
|
|
36
36
|
throw new Error(`${name} is required`);
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
typeof value !== "number" &&
|
|
40
|
+
typeof value !== "string" &&
|
|
41
|
+
!(value instanceof Decimal)
|
|
42
|
+
) {
|
|
39
43
|
throw new Error(`${name} must be a number, string, or Decimal`);
|
|
40
44
|
}
|
|
41
45
|
// Check for NaN and Infinity in numeric values
|
|
42
|
-
if (typeof value ===
|
|
46
|
+
if (typeof value === "number" && !isFinite(value)) {
|
|
43
47
|
throw new Error(`${name} must be a finite number (got ${value})`);
|
|
44
48
|
}
|
|
45
49
|
// Check for Infinity in Decimal instances
|
|
46
50
|
if (value instanceof Decimal && !value.isFinite()) {
|
|
47
|
-
throw new Error(
|
|
51
|
+
throw new Error(
|
|
52
|
+
`${name} must be a finite Decimal (got ${value.toString()})`,
|
|
53
|
+
);
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -138,8 +144,8 @@ function validateNumeric(value, name) {
|
|
|
138
144
|
* // This rotates 45° AFTER translating 10 units right
|
|
139
145
|
*/
|
|
140
146
|
export function translation(tx, ty) {
|
|
141
|
-
validateNumeric(tx,
|
|
142
|
-
validateNumeric(ty,
|
|
147
|
+
validateNumeric(tx, "tx");
|
|
148
|
+
validateNumeric(ty, "ty");
|
|
143
149
|
return Matrix.from([
|
|
144
150
|
[new Decimal(1), new Decimal(0), D(tx)],
|
|
145
151
|
[new Decimal(0), new Decimal(1), D(ty)],
|
|
@@ -195,16 +201,16 @@ export function translation(tx, ty) {
|
|
|
195
201
|
* // This scales by 2× around point (100, 50) instead of origin
|
|
196
202
|
*/
|
|
197
203
|
export function scale(sx, sy = null) {
|
|
198
|
-
validateNumeric(sx,
|
|
204
|
+
validateNumeric(sx, "sx");
|
|
199
205
|
if (sy !== null) {
|
|
200
|
-
validateNumeric(sy,
|
|
206
|
+
validateNumeric(sy, "sy");
|
|
201
207
|
}
|
|
202
208
|
const syValue = sy === null ? sx : sy;
|
|
203
209
|
// Check for zero scale factors which create singular matrices
|
|
204
210
|
const sxD = D(sx);
|
|
205
211
|
const syD = D(syValue);
|
|
206
212
|
if (sxD.isZero() || syD.isZero()) {
|
|
207
|
-
throw new Error(
|
|
213
|
+
throw new Error("Scale factors cannot be zero (creates singular matrix)");
|
|
208
214
|
}
|
|
209
215
|
return Matrix.from([
|
|
210
216
|
[sxD, new Decimal(0), new Decimal(0)],
|
|
@@ -266,21 +272,25 @@ export function scale(sx, sy = null) {
|
|
|
266
272
|
* // Rotates 90° around point (100, 100)
|
|
267
273
|
*/
|
|
268
274
|
export function rotate(theta) {
|
|
269
|
-
validateNumeric(theta,
|
|
275
|
+
validateNumeric(theta, "theta");
|
|
270
276
|
const t = D(theta);
|
|
271
277
|
// Normalize angle to [-π, π] to prevent NaN from Math.cos/sin with very large angles
|
|
272
278
|
const tNum = t.toNumber();
|
|
273
279
|
const pi = Math.PI;
|
|
274
|
-
const normalizedAngle = tNum -
|
|
280
|
+
const normalizedAngle = tNum - 2 * pi * Math.floor((tNum + pi) / (2 * pi));
|
|
275
281
|
// Check if normalization produced valid number
|
|
276
282
|
if (!isFinite(normalizedAngle)) {
|
|
277
|
-
throw new Error(
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Angle normalization failed for theta=${theta} (too large or invalid)`,
|
|
285
|
+
);
|
|
278
286
|
}
|
|
279
287
|
const c = new Decimal(Math.cos(normalizedAngle));
|
|
280
288
|
const s = new Decimal(Math.sin(normalizedAngle));
|
|
281
289
|
// Validate trig results are finite
|
|
282
290
|
if (!c.isFinite() || !s.isFinite()) {
|
|
283
|
-
throw new Error(
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Trigonometric computation failed for theta=${theta} (produced non-finite values)`,
|
|
293
|
+
);
|
|
284
294
|
}
|
|
285
295
|
return Matrix.from([
|
|
286
296
|
[c, s.negated(), new Decimal(0)],
|
|
@@ -335,9 +345,9 @@ export function rotate(theta) {
|
|
|
335
345
|
* // Result: point moves 30° counterclockwise around the center
|
|
336
346
|
*/
|
|
337
347
|
export function rotateAroundPoint(theta, px, py) {
|
|
338
|
-
validateNumeric(theta,
|
|
339
|
-
validateNumeric(px,
|
|
340
|
-
validateNumeric(py,
|
|
348
|
+
validateNumeric(theta, "theta");
|
|
349
|
+
validateNumeric(px, "px");
|
|
350
|
+
validateNumeric(py, "py");
|
|
341
351
|
const pxD = D(px);
|
|
342
352
|
const pyD = D(py);
|
|
343
353
|
return translation(pxD, pyD)
|
|
@@ -400,14 +410,16 @@ export function rotateAroundPoint(theta, px, py) {
|
|
|
400
410
|
* // Both coordinates affect each other, creating complex shearing
|
|
401
411
|
*/
|
|
402
412
|
export function skew(ax, ay) {
|
|
403
|
-
validateNumeric(ax,
|
|
404
|
-
validateNumeric(ay,
|
|
413
|
+
validateNumeric(ax, "ax");
|
|
414
|
+
validateNumeric(ay, "ay");
|
|
405
415
|
const axD = D(ax);
|
|
406
416
|
const ayD = D(ay);
|
|
407
417
|
// Check determinant: det = 1 - ax*ay. If det <= 0, matrix is singular or inverts orientation
|
|
408
418
|
const det = new Decimal(1).minus(axD.mul(ayD));
|
|
409
419
|
if (det.lessThanOrEqualTo(0)) {
|
|
410
|
-
throw new Error(
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Skew parameters create singular or orientation-inverting matrix (ax*ay = ${axD.mul(ayD).toString()}, must be < 1)`,
|
|
422
|
+
);
|
|
411
423
|
}
|
|
412
424
|
return Matrix.from([
|
|
413
425
|
[new Decimal(1), axD, new Decimal(0)],
|
|
@@ -477,20 +489,22 @@ export function skew(ax, ay) {
|
|
|
477
489
|
* // Result: x = 10, y = 10 (Y compressed to half)
|
|
478
490
|
*/
|
|
479
491
|
export function stretchAlongAxis(ux, uy, k) {
|
|
480
|
-
validateNumeric(ux,
|
|
481
|
-
validateNumeric(uy,
|
|
482
|
-
validateNumeric(k,
|
|
492
|
+
validateNumeric(ux, "ux");
|
|
493
|
+
validateNumeric(uy, "uy");
|
|
494
|
+
validateNumeric(k, "k");
|
|
483
495
|
const uxD = D(ux);
|
|
484
496
|
const uyD = D(uy);
|
|
485
497
|
const kD = D(k);
|
|
486
498
|
// Check if k is zero which creates singular matrix
|
|
487
499
|
if (kD.isZero()) {
|
|
488
|
-
throw new Error(
|
|
500
|
+
throw new Error(
|
|
501
|
+
"Stretch factor k cannot be zero (creates singular matrix)",
|
|
502
|
+
);
|
|
489
503
|
}
|
|
490
504
|
// Validate axis vector is not zero
|
|
491
505
|
const normSquared = uxD.mul(uxD).plus(uyD.mul(uyD));
|
|
492
506
|
if (normSquared.isZero()) {
|
|
493
|
-
throw new Error(
|
|
507
|
+
throw new Error("Axis vector (ux, uy) cannot be zero");
|
|
494
508
|
}
|
|
495
509
|
// Validate axis vector is normalized (required for correct stretch factor)
|
|
496
510
|
const normDiff = normSquared.minus(1).abs();
|
|
@@ -498,8 +512,8 @@ export function stretchAlongAxis(ux, uy, k) {
|
|
|
498
512
|
if (normDiff.greaterThan(tolerance)) {
|
|
499
513
|
throw new Error(
|
|
500
514
|
`Axis vector (ux, uy) must be normalized (||u|| = 1). ` +
|
|
501
|
-
|
|
502
|
-
|
|
515
|
+
`Current magnitude squared: ${normSquared.toString()}, expected: 1. ` +
|
|
516
|
+
`Use normalized vector: (${uxD.div(normSquared.sqrt()).toString()}, ${uyD.div(normSquared.sqrt()).toString()})`,
|
|
503
517
|
);
|
|
504
518
|
}
|
|
505
519
|
const one = new Decimal(1);
|
|
@@ -573,33 +587,52 @@ export function stretchAlongAxis(ux, uy, k) {
|
|
|
573
587
|
* // Efficiently reuses the same transformation matrix
|
|
574
588
|
*/
|
|
575
589
|
export function applyTransform(M, x, y) {
|
|
576
|
-
if (!M || typeof M.mul !==
|
|
577
|
-
throw new Error(
|
|
590
|
+
if (!M || typeof M.mul !== "function") {
|
|
591
|
+
throw new Error("applyTransform: first argument must be a Matrix");
|
|
578
592
|
}
|
|
579
593
|
// Check matrix dimensions
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
594
|
+
if (
|
|
595
|
+
!M.data ||
|
|
596
|
+
!Array.isArray(M.data) ||
|
|
597
|
+
M.data.length !== 3 ||
|
|
598
|
+
!M.data[0] ||
|
|
599
|
+
M.data[0].length !== 3 ||
|
|
600
|
+
!M.data[1] ||
|
|
601
|
+
M.data[1].length !== 3 ||
|
|
602
|
+
!M.data[2] ||
|
|
603
|
+
M.data[2].length !== 3
|
|
604
|
+
) {
|
|
605
|
+
throw new Error("applyTransform: matrix must be 3x3");
|
|
585
606
|
}
|
|
586
|
-
validateNumeric(x,
|
|
587
|
-
validateNumeric(y,
|
|
607
|
+
validateNumeric(x, "x");
|
|
608
|
+
validateNumeric(y, "y");
|
|
588
609
|
const P = Matrix.from([[D(x)], [D(y)], [new Decimal(1)]]);
|
|
589
610
|
const R = M.mul(P);
|
|
590
611
|
// Validate result matrix structure
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
612
|
+
if (
|
|
613
|
+
!R ||
|
|
614
|
+
!R.data ||
|
|
615
|
+
!Array.isArray(R.data) ||
|
|
616
|
+
R.data.length !== 3 ||
|
|
617
|
+
!R.data[0] ||
|
|
618
|
+
!R.data[0][0] ||
|
|
619
|
+
!R.data[1] ||
|
|
620
|
+
!R.data[1][0] ||
|
|
621
|
+
!R.data[2] ||
|
|
622
|
+
!R.data[2][0]
|
|
623
|
+
) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
"applyTransform: matrix multiplication produced invalid result",
|
|
626
|
+
);
|
|
596
627
|
}
|
|
597
628
|
const rx = R.data[0][0],
|
|
598
629
|
ry = R.data[1][0],
|
|
599
630
|
rw = R.data[2][0];
|
|
600
631
|
// Check for zero division in perspective division
|
|
601
632
|
if (rw.isZero()) {
|
|
602
|
-
throw new Error(
|
|
633
|
+
throw new Error(
|
|
634
|
+
"applyTransform: perspective division by zero (invalid transformation matrix)",
|
|
635
|
+
);
|
|
603
636
|
}
|
|
604
637
|
// Perspective division (for affine transforms, rw is always 1)
|
|
605
638
|
return [rx.div(rw), ry.div(rw)];
|