@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
|
@@ -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
|
-
//
|
|
81
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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) {
|