@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
package/src/path-analysis.js
CHANGED
|
@@ -92,12 +92,26 @@ export function pathArea(segments, options = {}) {
|
|
|
92
92
|
if (!segments || !Array.isArray(segments)) {
|
|
93
93
|
throw new Error("pathArea: segments must be an array");
|
|
94
94
|
}
|
|
95
|
+
if (segments.length === 0) {
|
|
96
|
+
return D(0);
|
|
97
|
+
}
|
|
95
98
|
|
|
96
99
|
const { samples = 50 } = options;
|
|
100
|
+
// WHY: Validate samples to prevent division by zero and infinite loops
|
|
101
|
+
if (!Number.isFinite(samples) || samples <= 0) {
|
|
102
|
+
throw new Error("pathArea: samples must be a positive number");
|
|
103
|
+
}
|
|
97
104
|
|
|
98
105
|
let area = D(0);
|
|
99
106
|
|
|
100
|
-
for (
|
|
107
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
108
|
+
const points = segments[segIdx];
|
|
109
|
+
// WHY: Validate each segment to prevent undefined behavior
|
|
110
|
+
if (!Array.isArray(points) || points.length < 2) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`pathArea: segment ${segIdx} must be an array with at least 2 control points`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
101
115
|
const n = points.length - 1; // Degree
|
|
102
116
|
|
|
103
117
|
if (n === 1) {
|
|
@@ -139,6 +153,12 @@ export function pathArea(segments, options = {}) {
|
|
|
139
153
|
* @returns {Decimal} Area contribution
|
|
140
154
|
*/
|
|
141
155
|
function bezierAreaContribution(points) {
|
|
156
|
+
// WHY: Validate input to prevent undefined behavior with invalid control points
|
|
157
|
+
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"bezierAreaContribution: points must be an array with at least 2 elements",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
142
162
|
const n = points.length - 1;
|
|
143
163
|
|
|
144
164
|
// Convert to Decimal
|
|
@@ -218,6 +238,18 @@ function bezierAreaContribution(points) {
|
|
|
218
238
|
* @returns {Decimal} Area contribution
|
|
219
239
|
*/
|
|
220
240
|
function numericalAreaContribution(points, samples) {
|
|
241
|
+
// WHY: Validate input to prevent division by zero and invalid control points
|
|
242
|
+
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"numericalAreaContribution: points must be an array with at least 2 elements",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (!Number.isFinite(samples) || samples <= 0) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"numericalAreaContribution: samples must be a positive number",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
221
253
|
// Use composite Simpson's rule
|
|
222
254
|
let integral_x_dy = D(0);
|
|
223
255
|
let integral_y_dx = D(0);
|
|
@@ -286,6 +318,15 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
286
318
|
}
|
|
287
319
|
|
|
288
320
|
const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
|
|
321
|
+
// WHY: Validate numeric parameters to prevent division by zero and infinite loops
|
|
322
|
+
if (!Number.isFinite(samples) || samples <= 0) {
|
|
323
|
+
throw new Error("closestPointOnPath: samples must be a positive number");
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isFinite(maxIterations) || maxIterations < 0) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"closestPointOnPath: maxIterations must be a non-negative number",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
289
330
|
|
|
290
331
|
const px = D(point[0]);
|
|
291
332
|
const py = D(point[1]);
|
|
@@ -298,6 +339,12 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
298
339
|
// Coarse sampling
|
|
299
340
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
300
341
|
const pts = segments[segIdx];
|
|
342
|
+
// WHY: Validate each segment before processing
|
|
343
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`closestPointOnPath: segment ${segIdx} must be an array with at least 2 control points`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
301
348
|
|
|
302
349
|
for (let i = 0; i <= samples; i++) {
|
|
303
350
|
const t = D(i).div(samples);
|
|
@@ -400,6 +447,15 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
400
447
|
}
|
|
401
448
|
|
|
402
449
|
const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
|
|
450
|
+
// WHY: Validate numeric parameters to prevent division by zero and infinite loops
|
|
451
|
+
if (!Number.isFinite(samples) || samples <= 0) {
|
|
452
|
+
throw new Error("farthestPointOnPath: samples must be a positive number");
|
|
453
|
+
}
|
|
454
|
+
if (!Number.isFinite(maxIterations) || maxIterations < 0) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"farthestPointOnPath: maxIterations must be a non-negative number",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
403
459
|
|
|
404
460
|
const px = D(point[0]);
|
|
405
461
|
const py = D(point[1]);
|
|
@@ -412,6 +468,12 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
412
468
|
// Coarse sampling
|
|
413
469
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
414
470
|
const pts = segments[segIdx];
|
|
471
|
+
// WHY: Validate each segment before processing
|
|
472
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`farthestPointOnPath: segment ${segIdx} must be an array with at least 2 control points`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
415
477
|
|
|
416
478
|
for (let i = 0; i <= samples; i++) {
|
|
417
479
|
const t = D(i).div(samples);
|
|
@@ -516,6 +578,10 @@ export function pointInPath(segments, point, options = {}) {
|
|
|
516
578
|
}
|
|
517
579
|
|
|
518
580
|
const { samples = 100 } = options;
|
|
581
|
+
// WHY: Validate samples to prevent division by zero and infinite loops
|
|
582
|
+
if (!Number.isFinite(samples) || samples <= 0) {
|
|
583
|
+
throw new Error("pointInPath: samples must be a positive number");
|
|
584
|
+
}
|
|
519
585
|
|
|
520
586
|
const px = D(point[0]);
|
|
521
587
|
const py = D(point[1]);
|
|
@@ -524,7 +590,15 @@ export function pointInPath(segments, point, options = {}) {
|
|
|
524
590
|
// WHY: Use named constant instead of magic number for clarity and maintainability
|
|
525
591
|
const boundaryTolerance = BOUNDARY_TOLERANCE;
|
|
526
592
|
|
|
527
|
-
for (
|
|
593
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
594
|
+
const pts = segments[segIdx];
|
|
595
|
+
// WHY: Validate each segment before processing
|
|
596
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`pointInPath: segment ${segIdx} must be an array with at least 2 control points`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
528
602
|
// Sample the segment and count crossings
|
|
529
603
|
let prevX = null;
|
|
530
604
|
let prevY = null;
|
|
@@ -551,16 +625,20 @@ export function pointInPath(segments, point, options = {}) {
|
|
|
551
625
|
// Check if segment crosses the ray's y-level
|
|
552
626
|
if ((y1.lte(py) && y2.gt(py)) || (y1.gt(py) && y2.lte(py))) {
|
|
553
627
|
// Find x at intersection
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
628
|
+
const yDiff = y2.minus(y1);
|
|
629
|
+
// WHY: Check for division by zero (horizontal segment at ray level)
|
|
630
|
+
if (yDiff.abs().gt(JACOBIAN_SINGULARITY_THRESHOLD)) {
|
|
631
|
+
const fraction = py.minus(y1).div(yDiff);
|
|
632
|
+
const xIntersect = x1.plus(x2.minus(x1).times(fraction));
|
|
633
|
+
|
|
634
|
+
// Count if intersection is to the right of point
|
|
635
|
+
if (xIntersect.gt(px)) {
|
|
636
|
+
// Determine winding direction
|
|
637
|
+
if (y2.gt(y1)) {
|
|
638
|
+
windingNumber++;
|
|
639
|
+
} else {
|
|
640
|
+
windingNumber--;
|
|
641
|
+
}
|
|
564
642
|
}
|
|
565
643
|
}
|
|
566
644
|
}
|
|
@@ -603,6 +681,24 @@ export function isPathClosed(
|
|
|
603
681
|
const firstSeg = segments[0];
|
|
604
682
|
const lastSeg = segments[segments.length - 1];
|
|
605
683
|
|
|
684
|
+
// WHY: Validate segment endpoints exist before accessing them
|
|
685
|
+
if (
|
|
686
|
+
!Array.isArray(firstSeg) ||
|
|
687
|
+
firstSeg.length === 0 ||
|
|
688
|
+
!Array.isArray(firstSeg[0]) ||
|
|
689
|
+
firstSeg[0].length < 2
|
|
690
|
+
) {
|
|
691
|
+
throw new Error("isPathClosed: first segment has invalid structure");
|
|
692
|
+
}
|
|
693
|
+
if (
|
|
694
|
+
!Array.isArray(lastSeg) ||
|
|
695
|
+
lastSeg.length === 0 ||
|
|
696
|
+
!Array.isArray(lastSeg[lastSeg.length - 1]) ||
|
|
697
|
+
lastSeg[lastSeg.length - 1].length < 2
|
|
698
|
+
) {
|
|
699
|
+
throw new Error("isPathClosed: last segment has invalid structure");
|
|
700
|
+
}
|
|
701
|
+
|
|
606
702
|
const [x0, y0] = [D(firstSeg[0][0]), D(firstSeg[0][1])];
|
|
607
703
|
const [xn, yn] = [
|
|
608
704
|
D(lastSeg[lastSeg.length - 1][0]),
|
|
@@ -637,6 +733,26 @@ export function isPathContinuous(
|
|
|
637
733
|
const seg1 = segments[i];
|
|
638
734
|
const seg2 = segments[i + 1];
|
|
639
735
|
|
|
736
|
+
// WHY: Validate segment structure before accessing endpoints
|
|
737
|
+
if (
|
|
738
|
+
!Array.isArray(seg1) ||
|
|
739
|
+
seg1.length === 0 ||
|
|
740
|
+
!Array.isArray(seg1[seg1.length - 1]) ||
|
|
741
|
+
seg1[seg1.length - 1].length < 2
|
|
742
|
+
) {
|
|
743
|
+
throw new Error(`isPathContinuous: segment ${i} has invalid structure`);
|
|
744
|
+
}
|
|
745
|
+
if (
|
|
746
|
+
!Array.isArray(seg2) ||
|
|
747
|
+
seg2.length === 0 ||
|
|
748
|
+
!Array.isArray(seg2[0]) ||
|
|
749
|
+
seg2[0].length < 2
|
|
750
|
+
) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
`isPathContinuous: segment ${i + 1} has invalid structure`,
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
640
756
|
const [x1, y1] = [D(seg1[seg1.length - 1][0]), D(seg1[seg1.length - 1][1])];
|
|
641
757
|
const [x2, y2] = [D(seg2[0][0]), D(seg2[0][1])];
|
|
642
758
|
|
|
@@ -766,7 +882,14 @@ export function pathBoundingBox(segments) {
|
|
|
766
882
|
let ymin = new Decimal(Infinity);
|
|
767
883
|
let ymax = new Decimal(-Infinity);
|
|
768
884
|
|
|
769
|
-
for (
|
|
885
|
+
for (let i = 0; i < segments.length; i++) {
|
|
886
|
+
const pts = segments[i];
|
|
887
|
+
// WHY: Validate each segment before processing
|
|
888
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
889
|
+
throw new Error(
|
|
890
|
+
`pathBoundingBox: segment ${i} must be an array with at least 2 control points`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
770
893
|
const bbox = bezierBoundingBox(pts);
|
|
771
894
|
xmin = Decimal.min(xmin, bbox.xmin);
|
|
772
895
|
xmax = Decimal.max(xmax, bbox.xmax);
|
|
@@ -838,7 +961,14 @@ export function pathLength(segments, options = {}) {
|
|
|
838
961
|
|
|
839
962
|
let total = D(0);
|
|
840
963
|
|
|
841
|
-
for (
|
|
964
|
+
for (let i = 0; i < segments.length; i++) {
|
|
965
|
+
const pts = segments[i];
|
|
966
|
+
// WHY: Validate each segment before processing
|
|
967
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
`pathLength: segment ${i} must be an array with at least 2 control points`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
842
972
|
total = total.plus(arcLength(pts, 0, 1, options));
|
|
843
973
|
}
|
|
844
974
|
|
|
@@ -871,7 +1001,14 @@ export function verifyPathArea(segments, samples = 100, tolerance = "1e-5") {
|
|
|
871
1001
|
|
|
872
1002
|
// Method 2: Shoelace formula on sampled polygon
|
|
873
1003
|
const polygon = [];
|
|
874
|
-
for (
|
|
1004
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1005
|
+
const pts = segments[segIdx];
|
|
1006
|
+
// WHY: Validate each segment before processing
|
|
1007
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
1008
|
+
throw new Error(
|
|
1009
|
+
`verifyPathArea: segment ${segIdx} must be an array with at least 2 control points`,
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
875
1012
|
for (let i = 0; i <= samples; i++) {
|
|
876
1013
|
const t = D(i).div(samples);
|
|
877
1014
|
const [x, y] = bezierPoint(pts, t);
|
|
@@ -993,7 +1130,14 @@ export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
|
|
|
993
1130
|
// Sample all segments to find maximum distance
|
|
994
1131
|
let maxSampledDistance = D(0);
|
|
995
1132
|
|
|
996
|
-
for (
|
|
1133
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1134
|
+
const pts = segments[segIdx];
|
|
1135
|
+
// WHY: Validate each segment before processing
|
|
1136
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`verifyFarthestPoint: segment ${segIdx} must be an array with at least 2 control points`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
997
1141
|
for (let i = 0; i <= samples; i++) {
|
|
998
1142
|
const t = D(i).div(samples);
|
|
999
1143
|
const [x, y] = bezierPoint(pts, t);
|
|
@@ -1051,13 +1195,25 @@ export function verifyPointInPath(segments, testPoint) {
|
|
|
1051
1195
|
let sumY = D(0);
|
|
1052
1196
|
let count = 0;
|
|
1053
1197
|
|
|
1054
|
-
for (
|
|
1198
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1199
|
+
const pts = segments[i];
|
|
1200
|
+
// WHY: Validate each segment before processing
|
|
1201
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`verifyPointInPath: segment ${i} must be an array with at least 2 control points`,
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1055
1206
|
const [x, y] = bezierPoint(pts, 0.5);
|
|
1056
1207
|
sumX = sumX.plus(x);
|
|
1057
1208
|
sumY = sumY.plus(y);
|
|
1058
1209
|
count++;
|
|
1059
1210
|
}
|
|
1060
1211
|
|
|
1212
|
+
// WHY: Prevent division by zero if no segments
|
|
1213
|
+
if (count === 0) {
|
|
1214
|
+
return { valid: true, result, consistentWithNeighbors: true };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1061
1217
|
const centroidX = sumX.div(count);
|
|
1062
1218
|
const centroidY = sumY.div(count);
|
|
1063
1219
|
|
|
@@ -1131,6 +1287,12 @@ export function verifyPathBoundingBox(segments, samples = 100) {
|
|
|
1131
1287
|
|
|
1132
1288
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1133
1289
|
const pts = segments[segIdx];
|
|
1290
|
+
// WHY: Validate each segment before processing
|
|
1291
|
+
if (!Array.isArray(pts) || pts.length < 2) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`verifyPathBoundingBox: segment ${segIdx} must be an array with at least 2 control points`,
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1134
1296
|
|
|
1135
1297
|
for (let i = 0; i <= samples; i++) {
|
|
1136
1298
|
const t = D(i).div(samples);
|
|
@@ -1214,7 +1376,19 @@ export function verifyPathLength(segments) {
|
|
|
1214
1376
|
const totalArcLength = pathLength(segments);
|
|
1215
1377
|
|
|
1216
1378
|
let chordSum = D(0);
|
|
1217
|
-
for (
|
|
1379
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1380
|
+
const pts = segments[i];
|
|
1381
|
+
// WHY: Validate segment endpoints exist before accessing them
|
|
1382
|
+
if (
|
|
1383
|
+
!Array.isArray(pts) ||
|
|
1384
|
+
pts.length === 0 ||
|
|
1385
|
+
!Array.isArray(pts[0]) ||
|
|
1386
|
+
pts[0].length < 2 ||
|
|
1387
|
+
!Array.isArray(pts[pts.length - 1]) ||
|
|
1388
|
+
pts[pts.length - 1].length < 2
|
|
1389
|
+
) {
|
|
1390
|
+
throw new Error(`verifyPathLength: segment ${i} has invalid structure`);
|
|
1391
|
+
}
|
|
1218
1392
|
const [x0, y0] = [D(pts[0][0]), D(pts[0][1])];
|
|
1219
1393
|
const [xn, yn] = [D(pts[pts.length - 1][0]), D(pts[pts.length - 1][1])];
|
|
1220
1394
|
const chord = xn.minus(x0).pow(2).plus(yn.minus(y0).pow(2)).sqrt();
|