@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
@@ -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 (const points of segments) {
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 (const pts of segments) {
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 fraction = py.minus(y1).div(y2.minus(y1));
555
- const xIntersect = x1.plus(x2.minus(x1).times(fraction));
556
-
557
- // Count if intersection is to the right of point
558
- if (xIntersect.gt(px)) {
559
- // Determine winding direction
560
- if (y2.gt(y1)) {
561
- windingNumber++;
562
- } else {
563
- windingNumber--;
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 (const pts of segments) {
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 (const pts of segments) {
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 (const pts of segments) {
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 (const pts of segments) {
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 (const pts of segments) {
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 (const pts of segments) {
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();