@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
@@ -57,28 +57,37 @@ const DEFAULT_TOLERANCE = new Decimal('1e-10');
57
57
  * @returns {Decimal} Angle in radians (-π to π)
58
58
  */
59
59
  function decimalAtan2(y, x) {
60
+ if (y === null || y === undefined) throw new Error('decimalAtan2: y parameter is null or undefined');
61
+ if (x === null || x === undefined) throw new Error('decimalAtan2: x parameter is null or undefined');
60
62
  const yD = D(y);
61
63
  const xD = D(x);
64
+ if (!yD.isFinite()) throw new Error('decimalAtan2: y must be finite');
65
+ if (!xD.isFinite()) throw new Error('decimalAtan2: x must be finite');
62
66
  const PI = Decimal.acos(-1);
63
67
 
68
+ // Check x=0 cases first to avoid division by zero
69
+ if (xD.equals(0)) {
70
+ if (yD.greaterThan(0)) {
71
+ // Positive y-axis
72
+ return PI.div(2);
73
+ } else if (yD.lessThan(0)) {
74
+ // Negative y-axis
75
+ return PI.div(2).neg();
76
+ } else {
77
+ // x=0, y=0 - undefined, return 0
78
+ return D(0);
79
+ }
80
+ }
81
+
64
82
  if (xD.greaterThan(0)) {
65
83
  // Quadrant I or IV
66
84
  return Decimal.atan(yD.div(xD));
67
85
  } else if (xD.lessThan(0) && yD.greaterThanOrEqualTo(0)) {
68
86
  // Quadrant II
69
87
  return Decimal.atan(yD.div(xD)).plus(PI);
70
- } else if (xD.lessThan(0) && yD.lessThan(0)) {
71
- // Quadrant III
72
- return Decimal.atan(yD.div(xD)).minus(PI);
73
- } else if (xD.equals(0) && yD.greaterThan(0)) {
74
- // Positive y-axis
75
- return PI.div(2);
76
- } else if (xD.equals(0) && yD.lessThan(0)) {
77
- // Negative y-axis
78
- return PI.div(2).neg();
79
88
  } else {
80
- // x=0, y=0 - undefined, return 0
81
- return D(0);
89
+ // Quadrant III (xD.lessThan(0) && yD.lessThan(0))
90
+ return Decimal.atan(yD.div(xD)).minus(PI);
82
91
  }
83
92
  }
84
93
 
@@ -93,7 +102,13 @@ function decimalAtan2(y, x) {
93
102
  * @returns {{x: Decimal, y: Decimal}} Point object
94
103
  */
95
104
  export function point(x, y) {
96
- return { x: D(x), y: D(y) };
105
+ if (x === null || x === undefined) throw new Error('point: x parameter is null or undefined');
106
+ if (y === null || y === undefined) throw new Error('point: y parameter is null or undefined');
107
+ const xD = D(x);
108
+ const yD = D(y);
109
+ if (!xD.isFinite()) throw new Error('point: x must be finite');
110
+ if (!yD.isFinite()) throw new Error('point: y must be finite');
111
+ return { x: xD, y: yD };
97
112
  }
98
113
 
99
114
  /**
@@ -104,6 +119,9 @@ export function point(x, y) {
104
119
  * @returns {Decimal} Squared distance
105
120
  */
106
121
  export function distanceSquared(p1, p2) {
122
+ if (!p1 || !p2) throw new Error('distanceSquared: points cannot be null or undefined');
123
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('distanceSquared: p1 must have Decimal x and y properties');
124
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('distanceSquared: p2 must have Decimal x and y properties');
107
125
  const dx = p2.x.minus(p1.x);
108
126
  const dy = p2.y.minus(p1.y);
109
127
  return dx.mul(dx).plus(dy.mul(dy));
@@ -116,6 +134,7 @@ export function distanceSquared(p1, p2) {
116
134
  * @returns {Decimal} Distance
117
135
  */
118
136
  export function distance(p1, p2) {
137
+ // Validation is done in distanceSquared
119
138
  return distanceSquared(p1, p2).sqrt();
120
139
  }
121
140
 
@@ -130,6 +149,10 @@ export function distance(p1, p2) {
130
149
  * @returns {Decimal} Perpendicular distance
131
150
  */
132
151
  export function pointToLineDistance(pt, lineStart, lineEnd) {
152
+ if (!pt || !lineStart || !lineEnd) throw new Error('pointToLineDistance: points cannot be null or undefined');
153
+ if (!(pt.x instanceof Decimal) || !(pt.y instanceof Decimal)) throw new Error('pointToLineDistance: pt must have Decimal x and y properties');
154
+ if (!(lineStart.x instanceof Decimal) || !(lineStart.y instanceof Decimal)) throw new Error('pointToLineDistance: lineStart must have Decimal x and y properties');
155
+ if (!(lineEnd.x instanceof Decimal) || !(lineEnd.y instanceof Decimal)) throw new Error('pointToLineDistance: lineEnd must have Decimal x and y properties');
133
156
  const x0 = pt.x, y0 = pt.y;
134
157
  const x1 = lineStart.x, y1 = lineStart.y;
135
158
  const x2 = lineEnd.x, y2 = lineEnd.y;
@@ -162,6 +185,10 @@ export function pointToLineDistance(pt, lineStart, lineEnd) {
162
185
  * @returns {Decimal} Cross product (positive = CCW, negative = CW, zero = collinear)
163
186
  */
164
187
  export function crossProduct(p1, p2, p3) {
188
+ if (!p1 || !p2 || !p3) throw new Error('crossProduct: points cannot be null or undefined');
189
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('crossProduct: p1 must have Decimal x and y properties');
190
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('crossProduct: p2 must have Decimal x and y properties');
191
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('crossProduct: p3 must have Decimal x and y properties');
165
192
  const v1x = p2.x.minus(p1.x);
166
193
  const v1y = p2.y.minus(p1.y);
167
194
  const v2x = p3.x.minus(p1.x);
@@ -185,7 +212,15 @@ export function crossProduct(p1, p2, p3) {
185
212
  * @returns {{x: Decimal, y: Decimal}} Point on curve
186
213
  */
187
214
  export function evaluateCubicBezier(p0, p1, p2, p3, t) {
215
+ if (!p0 || !p1 || !p2 || !p3) throw new Error('evaluateCubicBezier: points cannot be null or undefined');
216
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('evaluateCubicBezier: p0 must have Decimal x and y properties');
217
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('evaluateCubicBezier: p1 must have Decimal x and y properties');
218
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('evaluateCubicBezier: p2 must have Decimal x and y properties');
219
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('evaluateCubicBezier: p3 must have Decimal x and y properties');
220
+ if (t === null || t === undefined) throw new Error('evaluateCubicBezier: t parameter is null or undefined');
188
221
  const tD = D(t);
222
+ if (!tD.isFinite()) throw new Error('evaluateCubicBezier: t must be finite');
223
+ if (tD.lessThan(0) || tD.greaterThan(1)) throw new Error('evaluateCubicBezier: t must be in range [0, 1]');
189
224
  const oneMinusT = D(1).minus(tD);
190
225
 
191
226
  // Bernstein basis polynomials
@@ -211,7 +246,14 @@ export function evaluateCubicBezier(p0, p1, p2, p3, t) {
211
246
  * @returns {{x: Decimal, y: Decimal}} Point on curve
212
247
  */
213
248
  export function evaluateQuadraticBezier(p0, p1, p2, t) {
249
+ if (!p0 || !p1 || !p2) throw new Error('evaluateQuadraticBezier: points cannot be null or undefined');
250
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('evaluateQuadraticBezier: p0 must have Decimal x and y properties');
251
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('evaluateQuadraticBezier: p1 must have Decimal x and y properties');
252
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('evaluateQuadraticBezier: p2 must have Decimal x and y properties');
253
+ if (t === null || t === undefined) throw new Error('evaluateQuadraticBezier: t parameter is null or undefined');
214
254
  const tD = D(t);
255
+ if (!tD.isFinite()) throw new Error('evaluateQuadraticBezier: t must be finite');
256
+ if (tD.lessThan(0) || tD.greaterThan(1)) throw new Error('evaluateQuadraticBezier: t must be in range [0, 1]');
215
257
  const oneMinusT = D(1).minus(tD);
216
258
 
217
259
  // Bernstein basis polynomials
@@ -235,7 +277,13 @@ export function evaluateQuadraticBezier(p0, p1, p2, t) {
235
277
  * @returns {{x: Decimal, y: Decimal}} Point on line
236
278
  */
237
279
  export function evaluateLine(p0, p1, t) {
280
+ if (!p0 || !p1) throw new Error('evaluateLine: points cannot be null or undefined');
281
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('evaluateLine: p0 must have Decimal x and y properties');
282
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('evaluateLine: p1 must have Decimal x and y properties');
283
+ if (t === null || t === undefined) throw new Error('evaluateLine: t parameter is null or undefined');
238
284
  const tD = D(t);
285
+ if (!tD.isFinite()) throw new Error('evaluateLine: t must be finite');
286
+ if (tD.lessThan(0) || tD.greaterThan(1)) throw new Error('evaluateLine: t must be in range [0, 1]');
239
287
  const oneMinusT = D(1).minus(tD);
240
288
  return {
241
289
  x: oneMinusT.mul(p0.x).plus(tD.mul(p1.x)),
@@ -264,7 +312,14 @@ export function evaluateLine(p0, p1, t) {
264
312
  * @returns {{isStraight: boolean, maxDeviation: Decimal, verified: boolean}}
265
313
  */
266
314
  export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
315
+ if (!p0 || !p1 || !p2 || !p3) throw new Error('isCubicBezierStraight: points cannot be null or undefined');
316
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('isCubicBezierStraight: p0 must have Decimal x and y properties');
317
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('isCubicBezierStraight: p1 must have Decimal x and y properties');
318
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('isCubicBezierStraight: p2 must have Decimal x and y properties');
319
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('isCubicBezierStraight: p3 must have Decimal x and y properties');
320
+ if (tolerance === null || tolerance === undefined) throw new Error('isCubicBezierStraight: tolerance parameter is null or undefined');
267
321
  const tol = D(tolerance);
322
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('isCubicBezierStraight: tolerance must be a non-negative finite number');
268
323
 
269
324
  // Check if start and end are the same point (degenerate case)
270
325
  const chordLength = distance(p0, p3);
@@ -325,7 +380,13 @@ export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERA
325
380
  * @returns {{isStraight: boolean, maxDeviation: Decimal, verified: boolean}}
326
381
  */
327
382
  export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERANCE) {
383
+ if (!p0 || !p1 || !p2) throw new Error('isQuadraticBezierStraight: points cannot be null or undefined');
384
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('isQuadraticBezierStraight: p0 must have Decimal x and y properties');
385
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('isQuadraticBezierStraight: p1 must have Decimal x and y properties');
386
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('isQuadraticBezierStraight: p2 must have Decimal x and y properties');
387
+ if (tolerance === null || tolerance === undefined) throw new Error('isQuadraticBezierStraight: tolerance parameter is null or undefined');
328
388
  const tol = D(tolerance);
389
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('isQuadraticBezierStraight: tolerance must be a non-negative finite number');
329
390
 
330
391
  // Check if start and end are the same point (degenerate case)
331
392
  const chordLength = distance(p0, p2);
@@ -383,6 +444,7 @@ export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERA
383
444
  * @returns {{start: {x: Decimal, y: Decimal}, end: {x: Decimal, y: Decimal}, maxDeviation: Decimal} | null}
384
445
  */
385
446
  export function cubicBezierToLine(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
447
+ // Validation is done in isCubicBezierStraight
386
448
  const result = isCubicBezierStraight(p0, p1, p2, p3, tolerance);
387
449
  if (!result.isStraight || !result.verified) {
388
450
  return null;
@@ -419,7 +481,14 @@ export function cubicBezierToLine(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
419
481
  * @returns {{canLower: boolean, quadraticControl: {x: Decimal, y: Decimal} | null, maxDeviation: Decimal, verified: boolean}}
420
482
  */
421
483
  export function canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
484
+ if (!p0 || !p1 || !p2 || !p3) throw new Error('canLowerCubicToQuadratic: points cannot be null or undefined');
485
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('canLowerCubicToQuadratic: p0 must have Decimal x and y properties');
486
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('canLowerCubicToQuadratic: p1 must have Decimal x and y properties');
487
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('canLowerCubicToQuadratic: p2 must have Decimal x and y properties');
488
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('canLowerCubicToQuadratic: p3 must have Decimal x and y properties');
489
+ if (tolerance === null || tolerance === undefined) throw new Error('canLowerCubicToQuadratic: tolerance parameter is null or undefined');
422
490
  const tol = D(tolerance);
491
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('canLowerCubicToQuadratic: tolerance must be a non-negative finite number');
423
492
  const three = D(3);
424
493
  const two = D(2);
425
494
 
@@ -487,6 +556,7 @@ export function canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOL
487
556
  * @returns {{p0: {x: Decimal, y: Decimal}, p1: {x: Decimal, y: Decimal}, p2: {x: Decimal, y: Decimal}, maxDeviation: Decimal} | null}
488
557
  */
489
558
  export function cubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
559
+ // Validation is done in canLowerCubicToQuadratic
490
560
  const result = canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance);
491
561
  if (!result.canLower || !result.quadraticControl) {
492
562
  return null;
@@ -513,10 +583,22 @@ export function cubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
513
583
  * @returns {{center: {x: Decimal, y: Decimal}, radius: Decimal} | null}
514
584
  */
515
585
  export function fitCircleToPoints(points) {
586
+ if (!points) throw new Error('fitCircleToPoints: points array cannot be null or undefined');
587
+ if (!Array.isArray(points)) throw new Error('fitCircleToPoints: points must be an array');
588
+ if (points.length === 0) throw new Error('fitCircleToPoints: points array cannot be empty');
516
589
  if (points.length < 3) {
517
590
  return null;
518
591
  }
519
592
 
593
+ // Validate all points have Decimal x and y properties
594
+ for (let i = 0; i < points.length; i++) {
595
+ const p = points[i];
596
+ if (!p) throw new Error(`fitCircleToPoints: point at index ${i} is null or undefined`);
597
+ if (!(p.x instanceof Decimal) || !(p.y instanceof Decimal)) {
598
+ throw new Error(`fitCircleToPoints: point at index ${i} must have Decimal x and y properties`);
599
+ }
600
+ }
601
+
520
602
  const n = D(points.length);
521
603
  let sumX = D(0), sumY = D(0);
522
604
  let sumX2 = D(0), sumY2 = D(0);
@@ -589,7 +671,14 @@ export function fitCircleToPoints(points) {
589
671
  * @returns {{isArc: boolean, circle: {center: {x: Decimal, y: Decimal}, radius: Decimal} | null, maxDeviation: Decimal, verified: boolean}}
590
672
  */
591
673
  export function fitCircleToCubicBezier(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
674
+ if (!p0 || !p1 || !p2 || !p3) throw new Error('fitCircleToCubicBezier: points cannot be null or undefined');
675
+ if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal)) throw new Error('fitCircleToCubicBezier: p0 must have Decimal x and y properties');
676
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('fitCircleToCubicBezier: p1 must have Decimal x and y properties');
677
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('fitCircleToCubicBezier: p2 must have Decimal x and y properties');
678
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('fitCircleToCubicBezier: p3 must have Decimal x and y properties');
679
+ if (tolerance === null || tolerance === undefined) throw new Error('fitCircleToCubicBezier: tolerance parameter is null or undefined');
592
680
  const tol = D(tolerance);
681
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('fitCircleToCubicBezier: tolerance must be a non-negative finite number');
593
682
 
594
683
  // Sample points along the curve for fitting
595
684
  const sampleCount = 9; // Including endpoints
@@ -646,6 +735,7 @@ export function fitCircleToCubicBezier(p0, p1, p2, p3, tolerance = DEFAULT_TOLER
646
735
  * @returns {{rx: Decimal, ry: Decimal, rotation: Decimal, largeArc: number, sweep: number, endX: Decimal, endY: Decimal, maxDeviation: Decimal} | null}
647
736
  */
648
737
  export function cubicBezierToArc(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
738
+ // Validation is done in fitCircleToCubicBezier
649
739
  const result = fitCircleToCubicBezier(p0, p1, p2, p3, tolerance);
650
740
 
651
741
  if (!result.isArc || !result.circle) {
@@ -714,8 +804,14 @@ export function cubicBezierToArc(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
714
804
  * @returns {Decimal | null} Sagitta value, or null if chord > diameter
715
805
  */
716
806
  export function calculateSagitta(radius, chordLength) {
807
+ if (radius === null || radius === undefined) throw new Error('calculateSagitta: radius parameter is null or undefined');
808
+ if (chordLength === null || chordLength === undefined) throw new Error('calculateSagitta: chordLength parameter is null or undefined');
717
809
  const r = D(radius);
718
810
  const c = D(chordLength);
811
+ if (!r.isFinite()) throw new Error('calculateSagitta: radius must be finite');
812
+ if (!c.isFinite()) throw new Error('calculateSagitta: chordLength must be finite');
813
+ if (r.lessThan(0)) throw new Error('calculateSagitta: radius must be non-negative');
814
+ if (c.lessThan(0)) throw new Error('calculateSagitta: chordLength must be non-negative');
719
815
  const halfChord = c.div(2);
720
816
 
721
817
  // Check if chord is valid (must be <= 2*r)
@@ -748,9 +844,23 @@ export function calculateSagitta(radius, chordLength) {
748
844
  * @returns {{isStraight: boolean, sagitta: Decimal | null, maxDeviation: Decimal, verified: boolean}}
749
845
  */
750
846
  export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tolerance = DEFAULT_TOLERANCE) {
847
+ if (rx === null || rx === undefined) throw new Error('isArcStraight: rx parameter is null or undefined');
848
+ if (ry === null || ry === undefined) throw new Error('isArcStraight: ry parameter is null or undefined');
849
+ if (rotation === null || rotation === undefined) throw new Error('isArcStraight: rotation parameter is null or undefined');
850
+ if (largeArc === null || largeArc === undefined) throw new Error('isArcStraight: largeArc parameter is null or undefined');
851
+ if (sweep === null || sweep === undefined) throw new Error('isArcStraight: sweep parameter is null or undefined');
852
+ if (!start || !end) throw new Error('isArcStraight: start and end points cannot be null or undefined');
853
+ if (!(start.x instanceof Decimal) || !(start.y instanceof Decimal)) throw new Error('isArcStraight: start must have Decimal x and y properties');
854
+ if (!(end.x instanceof Decimal) || !(end.y instanceof Decimal)) throw new Error('isArcStraight: end must have Decimal x and y properties');
855
+ if (tolerance === null || tolerance === undefined) throw new Error('isArcStraight: tolerance parameter is null or undefined');
856
+ if (largeArc !== 0 && largeArc !== 1) throw new Error('isArcStraight: largeArc must be 0 or 1');
857
+ if (sweep !== 0 && sweep !== 1) throw new Error('isArcStraight: sweep must be 0 or 1');
751
858
  const tol = D(tolerance);
859
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('isArcStraight: tolerance must be a non-negative finite number');
752
860
  const rxD = D(rx);
753
861
  const ryD = D(ry);
862
+ if (!rxD.isFinite()) throw new Error('isArcStraight: rx must be finite');
863
+ if (!ryD.isFinite()) throw new Error('isArcStraight: ry must be finite');
754
864
 
755
865
  // Check for zero or near-zero radii
756
866
  if (rxD.abs().lessThan(EPSILON) || ryD.abs().lessThan(EPSILON)) {
@@ -814,7 +924,13 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
814
924
  * @returns {boolean} True if collinear
815
925
  */
816
926
  export function areCollinear(p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
927
+ if (!p1 || !p2 || !p3) throw new Error('areCollinear: points cannot be null or undefined');
928
+ if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal)) throw new Error('areCollinear: p1 must have Decimal x and y properties');
929
+ if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal)) throw new Error('areCollinear: p2 must have Decimal x and y properties');
930
+ if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal)) throw new Error('areCollinear: p3 must have Decimal x and y properties');
931
+ if (tolerance === null || tolerance === undefined) throw new Error('areCollinear: tolerance parameter is null or undefined');
817
932
  const tol = D(tolerance);
933
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('areCollinear: tolerance must be a non-negative finite number');
818
934
 
819
935
  // Check using cross product (area of triangle)
820
936
  const cross = crossProduct(p1, p2, p3).abs();
@@ -838,11 +954,26 @@ export function areCollinear(p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
838
954
  * @returns {{points: Array<{x: Decimal, y: Decimal}>, mergeCount: number, verified: boolean}}
839
955
  */
840
956
  export function mergeCollinearSegments(points, tolerance = DEFAULT_TOLERANCE) {
957
+ if (!points) throw new Error('mergeCollinearSegments: points array cannot be null or undefined');
958
+ if (!Array.isArray(points)) throw new Error('mergeCollinearSegments: points must be an array');
959
+ if (points.length === 0) throw new Error('mergeCollinearSegments: points array cannot be empty');
960
+ if (tolerance === null || tolerance === undefined) throw new Error('mergeCollinearSegments: tolerance parameter is null or undefined');
961
+
962
+ // Validate all points have Decimal x and y properties
963
+ for (let i = 0; i < points.length; i++) {
964
+ const p = points[i];
965
+ if (!p) throw new Error(`mergeCollinearSegments: point at index ${i} is null or undefined`);
966
+ if (!(p.x instanceof Decimal) || !(p.y instanceof Decimal)) {
967
+ throw new Error(`mergeCollinearSegments: point at index ${i} must have Decimal x and y properties`);
968
+ }
969
+ }
970
+
841
971
  if (points.length < 3) {
842
972
  return { points: [...points], mergeCount: 0, verified: true };
843
973
  }
844
974
 
845
975
  const tol = D(tolerance);
976
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('mergeCollinearSegments: tolerance must be a non-negative finite number');
846
977
  const result = [points[0]];
847
978
  let mergeCount = 0;
848
979
 
@@ -904,7 +1035,13 @@ export function mergeCollinearSegments(points, tolerance = DEFAULT_TOLERANCE) {
904
1035
  * @returns {boolean} True if zero-length
905
1036
  */
906
1037
  export function isZeroLengthSegment(start, end, tolerance = EPSILON) {
907
- return distance(start, end).lessThan(D(tolerance));
1038
+ if (!start || !end) throw new Error('isZeroLengthSegment: points cannot be null or undefined');
1039
+ if (!(start.x instanceof Decimal) || !(start.y instanceof Decimal)) throw new Error('isZeroLengthSegment: start must have Decimal x and y properties');
1040
+ if (!(end.x instanceof Decimal) || !(end.y instanceof Decimal)) throw new Error('isZeroLengthSegment: end must have Decimal x and y properties');
1041
+ if (tolerance === null || tolerance === undefined) throw new Error('isZeroLengthSegment: tolerance parameter is null or undefined');
1042
+ const tol = D(tolerance);
1043
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('isZeroLengthSegment: tolerance must be a non-negative finite number');
1044
+ return distance(start, end).lessThan(tol);
908
1045
  }
909
1046
 
910
1047
  /**
@@ -915,19 +1052,30 @@ export function isZeroLengthSegment(start, end, tolerance = EPSILON) {
915
1052
  * @returns {{pathData: Array<{command: string, args: Array<Decimal>}>, removeCount: number, verified: boolean}}
916
1053
  */
917
1054
  export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
1055
+ if (!pathData) throw new Error('removeZeroLengthSegments: pathData cannot be null or undefined');
1056
+ if (!Array.isArray(pathData)) throw new Error('removeZeroLengthSegments: pathData must be an array');
1057
+ if (tolerance === null || tolerance === undefined) throw new Error('removeZeroLengthSegments: tolerance parameter is null or undefined');
1058
+
918
1059
  const tol = D(tolerance);
1060
+ if (!tol.isFinite() || tol.lessThan(0)) throw new Error('removeZeroLengthSegments: tolerance must be a non-negative finite number');
919
1061
  const result = [];
920
1062
  let removeCount = 0;
921
1063
  let currentX = D(0), currentY = D(0);
922
1064
  let startX = D(0), startY = D(0);
923
1065
 
924
- for (const item of pathData) {
1066
+ for (let idx = 0; idx < pathData.length; idx++) {
1067
+ const item = pathData[idx];
1068
+ if (!item) throw new Error(`removeZeroLengthSegments: item at index ${idx} is null or undefined`);
1069
+ if (typeof item.command !== 'string') throw new Error(`removeZeroLengthSegments: item at index ${idx} must have a string command property`);
1070
+ if (!Array.isArray(item.args)) throw new Error(`removeZeroLengthSegments: item at index ${idx} must have an args array property`);
1071
+
925
1072
  const { command, args } = item;
926
1073
  let keep = true;
927
1074
 
928
1075
  switch (command.toUpperCase()) {
929
1076
  case 'M':
930
1077
  // Update current position (absolute M) or move relative (lowercase m)
1078
+ if (args.length < 2) throw new Error(`removeZeroLengthSegments: M command at index ${idx} requires 2 args, got ${args.length}`);
931
1079
  currentX = command === 'M' ? D(args[0]) : currentX.plus(D(args[0]));
932
1080
  currentY = command === 'M' ? D(args[1]) : currentY.plus(D(args[1]));
933
1081
  // CRITICAL: Update subpath start for EVERY M command (BUG 3 FIX)
@@ -937,6 +1085,7 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
937
1085
 
938
1086
  case 'L': {
939
1087
  // Line to: x y (2 args)
1088
+ if (args.length < 2) throw new Error(`removeZeroLengthSegments: L command at index ${idx} requires 2 args, got ${args.length}`);
940
1089
  const endX = command === 'L' ? D(args[0]) : currentX.plus(D(args[0]));
941
1090
  const endY = command === 'L' ? D(args[1]) : currentY.plus(D(args[1]));
942
1091
  if (isZeroLengthSegment({ x: currentX, y: currentY }, { x: endX, y: endY }, tol)) {
@@ -951,6 +1100,7 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
951
1100
 
952
1101
  case 'T': {
953
1102
  // Smooth quadratic Bezier: x y (2 args) - BUG 4 FIX (separated from L)
1103
+ if (args.length < 2) throw new Error(`removeZeroLengthSegments: T command at index ${idx} requires 2 args, got ${args.length}`);
954
1104
  const endX = command === 'T' ? D(args[0]) : currentX.plus(D(args[0]));
955
1105
  const endY = command === 'T' ? D(args[1]) : currentY.plus(D(args[1]));
956
1106
  if (isZeroLengthSegment({ x: currentX, y: currentY }, { x: endX, y: endY }, tol)) {
@@ -964,28 +1114,31 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
964
1114
  }
965
1115
 
966
1116
  case 'H': {
1117
+ if (args.length < 1) throw new Error(`removeZeroLengthSegments: H command at index ${idx} requires 1 arg, got ${args.length}`);
967
1118
  const endX = command === 'H' ? D(args[0]) : currentX.plus(D(args[0]));
968
1119
  if (endX.minus(currentX).abs().lessThan(tol)) {
969
1120
  keep = false;
970
1121
  removeCount++;
971
- } else {
972
- currentX = endX;
973
1122
  }
1123
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1124
+ currentX = endX;
974
1125
  break;
975
1126
  }
976
1127
 
977
1128
  case 'V': {
1129
+ if (args.length < 1) throw new Error(`removeZeroLengthSegments: V command at index ${idx} requires 1 arg, got ${args.length}`);
978
1130
  const endY = command === 'V' ? D(args[0]) : currentY.plus(D(args[0]));
979
1131
  if (endY.minus(currentY).abs().lessThan(tol)) {
980
1132
  keep = false;
981
1133
  removeCount++;
982
- } else {
983
- currentY = endY;
984
1134
  }
1135
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1136
+ currentY = endY;
985
1137
  break;
986
1138
  }
987
1139
 
988
1140
  case 'C': {
1141
+ if (args.length < 6) throw new Error(`removeZeroLengthSegments: C command at index ${idx} requires 6 args, got ${args.length}`);
989
1142
  const endX = command === 'C' ? D(args[4]) : currentX.plus(D(args[4]));
990
1143
  const endY = command === 'C' ? D(args[5]) : currentY.plus(D(args[5]));
991
1144
  // For curves, also check if all control points are at the same location
@@ -1002,15 +1155,16 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
1002
1155
  if (allSame) {
1003
1156
  keep = false;
1004
1157
  removeCount++;
1005
- } else {
1006
- currentX = endX;
1007
- currentY = endY;
1008
1158
  }
1159
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1160
+ currentX = endX;
1161
+ currentY = endY;
1009
1162
  break;
1010
1163
  }
1011
1164
 
1012
1165
  case 'Q': {
1013
1166
  // Quadratic Bezier: x1 y1 x y (4 args)
1167
+ if (args.length < 4) throw new Error(`removeZeroLengthSegments: Q command at index ${idx} requires 4 args, got ${args.length}`);
1014
1168
  const endX = command === 'Q' ? D(args[2]) : currentX.plus(D(args[2]));
1015
1169
  const endY = command === 'Q' ? D(args[3]) : currentY.plus(D(args[3]));
1016
1170
  if (isZeroLengthSegment({ x: currentX, y: currentY }, { x: endX, y: endY }, tol)) {
@@ -1022,15 +1176,15 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
1022
1176
  removeCount++;
1023
1177
  }
1024
1178
  }
1025
- if (keep) {
1026
- currentX = endX;
1027
- currentY = endY;
1028
- }
1179
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1180
+ currentX = endX;
1181
+ currentY = endY;
1029
1182
  break;
1030
1183
  }
1031
1184
 
1032
1185
  case 'S': {
1033
1186
  // Smooth cubic Bezier: x2 y2 x y (4 args) - BUG 4 FIX
1187
+ if (args.length < 4) throw new Error(`removeZeroLengthSegments: S command at index ${idx} requires 4 args, got ${args.length}`);
1034
1188
  const endX = command === 'S' ? D(args[2]) : currentX.plus(D(args[2]));
1035
1189
  const endY = command === 'S' ? D(args[3]) : currentY.plus(D(args[3]));
1036
1190
  if (isZeroLengthSegment({ x: currentX, y: currentY }, { x: endX, y: endY }, tol)) {
@@ -1042,23 +1196,23 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
1042
1196
  removeCount++;
1043
1197
  }
1044
1198
  }
1045
- if (keep) {
1046
- currentX = endX;
1047
- currentY = endY;
1048
- }
1199
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1200
+ currentX = endX;
1201
+ currentY = endY;
1049
1202
  break;
1050
1203
  }
1051
1204
 
1052
1205
  case 'A': {
1206
+ if (args.length < 7) throw new Error(`removeZeroLengthSegments: A command at index ${idx} requires 7 args, got ${args.length}`);
1053
1207
  const endX = command === 'A' ? D(args[5]) : currentX.plus(D(args[5]));
1054
1208
  const endY = command === 'A' ? D(args[6]) : currentY.plus(D(args[6]));
1055
1209
  if (isZeroLengthSegment({ x: currentX, y: currentY }, { x: endX, y: endY }, tol)) {
1056
1210
  keep = false;
1057
1211
  removeCount++;
1058
- } else {
1059
- currentX = endX;
1060
- currentY = endY;
1061
1212
  }
1213
+ // CRITICAL: Always update position, even when removing segment (consistency with L command)
1214
+ currentX = endX;
1215
+ currentY = endY;
1062
1216
  break;
1063
1217
  }
1064
1218
 
@@ -1070,6 +1224,8 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
1070
1224
  currentX = startX;
1071
1225
  currentY = startY;
1072
1226
  break;
1227
+ default:
1228
+ break;
1073
1229
  }
1074
1230
 
1075
1231
  if (keep) {