@emasoft/svg-matrix 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/svg-matrix.js +7 -6
- package/bin/svgm.js +109 -40
- package/dist/svg-matrix.min.js +7 -7
- package/dist/svg-toolbox.min.js +148 -228
- package/dist/svgm.min.js +152 -232
- package/dist/version.json +5 -5
- package/package.json +1 -1
- package/scripts/postinstall.js +72 -41
- package/scripts/test-postinstall.js +18 -16
- package/scripts/version-sync.js +78 -60
- package/src/animation-optimization.js +190 -98
- package/src/animation-references.js +11 -3
- package/src/arc-length.js +23 -20
- package/src/bezier-analysis.js +9 -13
- package/src/bezier-intersections.js +18 -4
- package/src/browser-verify.js +35 -8
- package/src/clip-path-resolver.js +285 -114
- package/src/convert-path-data.js +20 -8
- package/src/css-specificity.js +33 -9
- package/src/douglas-peucker.js +272 -141
- package/src/geometry-to-path.js +79 -22
- package/src/gjk-collision.js +287 -126
- package/src/index.js +56 -21
- package/src/inkscape-support.js +122 -101
- package/src/logger.js +43 -27
- package/src/marker-resolver.js +201 -121
- package/src/mask-resolver.js +231 -98
- package/src/matrix.js +9 -5
- package/src/mesh-gradient.js +22 -14
- package/src/off-canvas-detection.js +53 -17
- package/src/path-optimization.js +356 -171
- package/src/path-simplification.js +671 -256
- package/src/pattern-resolver.js +1 -3
- package/src/polygon-clip.js +396 -78
- package/src/svg-boolean-ops.js +90 -23
- package/src/svg-collections.js +1546 -667
- package/src/svg-flatten.js +152 -38
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +5 -1
- package/src/svg-rendering-context.js +3 -1
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svg-toolbox.js +99 -457
- package/src/svg-validation-data.js +513 -345
- package/src/svg2-polyfills.js +156 -93
- package/src/svgm-lib.js +8 -4
- package/src/transform-optimization.js +168 -51
- package/src/transforms2d.js +73 -40
- package/src/transforms3d.js +34 -27
- package/src/use-symbol-resolver.js +175 -76
- package/src/vector.js +80 -44
- package/src/vendor/inkscape-hatch-polyfill.js +143 -108
- package/src/vendor/inkscape-hatch-polyfill.min.js +291 -1
- package/src/vendor/inkscape-mesh-polyfill.js +953 -766
- package/src/vendor/inkscape-mesh-polyfill.min.js +896 -1
- package/src/verification.js +3 -4
|
@@ -35,19 +35,19 @@
|
|
|
35
35
|
* @module path-simplification
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import Decimal from
|
|
38
|
+
import Decimal from "decimal.js";
|
|
39
39
|
|
|
40
40
|
// Set high precision for all calculations
|
|
41
41
|
Decimal.set({ precision: 80 });
|
|
42
42
|
|
|
43
43
|
// Helper to convert to Decimal
|
|
44
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
44
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
45
45
|
|
|
46
46
|
// Near-zero threshold for comparisons (much smaller than SVGO's!)
|
|
47
|
-
const EPSILON = new Decimal(
|
|
47
|
+
const EPSILON = new Decimal("1e-40");
|
|
48
48
|
|
|
49
49
|
// Default tolerance for simplification (user-configurable)
|
|
50
|
-
const DEFAULT_TOLERANCE = new Decimal(
|
|
50
|
+
const DEFAULT_TOLERANCE = new Decimal("1e-10");
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Implementation of atan2 using Decimal.js (which doesn't provide it natively).
|
|
@@ -57,12 +57,14 @@ 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)
|
|
61
|
-
|
|
60
|
+
if (y === null || y === undefined)
|
|
61
|
+
throw new Error("decimalAtan2: y parameter is null or undefined");
|
|
62
|
+
if (x === null || x === undefined)
|
|
63
|
+
throw new Error("decimalAtan2: x parameter is null or undefined");
|
|
62
64
|
const yD = D(y);
|
|
63
65
|
const xD = D(x);
|
|
64
|
-
if (!yD.isFinite()) throw new Error(
|
|
65
|
-
if (!xD.isFinite()) throw new Error(
|
|
66
|
+
if (!yD.isFinite()) throw new Error("decimalAtan2: y must be finite");
|
|
67
|
+
if (!xD.isFinite()) throw new Error("decimalAtan2: x must be finite");
|
|
66
68
|
const PI = Decimal.acos(-1);
|
|
67
69
|
|
|
68
70
|
// Check x=0 cases first to avoid division by zero
|
|
@@ -102,12 +104,14 @@ function decimalAtan2(y, x) {
|
|
|
102
104
|
* @returns {{x: Decimal, y: Decimal}} Point object
|
|
103
105
|
*/
|
|
104
106
|
export function point(x, y) {
|
|
105
|
-
if (x === null || x === undefined)
|
|
106
|
-
|
|
107
|
+
if (x === null || x === undefined)
|
|
108
|
+
throw new Error("point: x parameter is null or undefined");
|
|
109
|
+
if (y === null || y === undefined)
|
|
110
|
+
throw new Error("point: y parameter is null or undefined");
|
|
107
111
|
const xD = D(x);
|
|
108
112
|
const yD = D(y);
|
|
109
|
-
if (!xD.isFinite()) throw new Error(
|
|
110
|
-
if (!yD.isFinite()) throw new Error(
|
|
113
|
+
if (!xD.isFinite()) throw new Error("point: x must be finite");
|
|
114
|
+
if (!yD.isFinite()) throw new Error("point: y must be finite");
|
|
111
115
|
return { x: xD, y: yD };
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -119,9 +123,12 @@ export function point(x, y) {
|
|
|
119
123
|
* @returns {Decimal} Squared distance
|
|
120
124
|
*/
|
|
121
125
|
export function distanceSquared(p1, p2) {
|
|
122
|
-
if (!p1 || !p2)
|
|
123
|
-
|
|
124
|
-
if (!(
|
|
126
|
+
if (!p1 || !p2)
|
|
127
|
+
throw new Error("distanceSquared: points cannot be null or undefined");
|
|
128
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
129
|
+
throw new Error("distanceSquared: p1 must have Decimal x and y properties");
|
|
130
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
131
|
+
throw new Error("distanceSquared: p2 must have Decimal x and y properties");
|
|
125
132
|
const dx = p2.x.minus(p1.x);
|
|
126
133
|
const dy = p2.y.minus(p1.y);
|
|
127
134
|
return dx.mul(dx).plus(dy.mul(dy));
|
|
@@ -149,13 +156,26 @@ export function distance(p1, p2) {
|
|
|
149
156
|
* @returns {Decimal} Perpendicular distance
|
|
150
157
|
*/
|
|
151
158
|
export function pointToLineDistance(pt, lineStart, lineEnd) {
|
|
152
|
-
if (!pt || !lineStart || !lineEnd)
|
|
153
|
-
|
|
154
|
-
if (!(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
if (!pt || !lineStart || !lineEnd)
|
|
160
|
+
throw new Error("pointToLineDistance: points cannot be null or undefined");
|
|
161
|
+
if (!(pt.x instanceof Decimal) || !(pt.y instanceof Decimal))
|
|
162
|
+
throw new Error(
|
|
163
|
+
"pointToLineDistance: pt must have Decimal x and y properties",
|
|
164
|
+
);
|
|
165
|
+
if (!(lineStart.x instanceof Decimal) || !(lineStart.y instanceof Decimal))
|
|
166
|
+
throw new Error(
|
|
167
|
+
"pointToLineDistance: lineStart must have Decimal x and y properties",
|
|
168
|
+
);
|
|
169
|
+
if (!(lineEnd.x instanceof Decimal) || !(lineEnd.y instanceof Decimal))
|
|
170
|
+
throw new Error(
|
|
171
|
+
"pointToLineDistance: lineEnd must have Decimal x and y properties",
|
|
172
|
+
);
|
|
173
|
+
const x0 = pt.x,
|
|
174
|
+
y0 = pt.y;
|
|
175
|
+
const x1 = lineStart.x,
|
|
176
|
+
y1 = lineStart.y;
|
|
177
|
+
const x2 = lineEnd.x,
|
|
178
|
+
y2 = lineEnd.y;
|
|
159
179
|
|
|
160
180
|
const dx = x2.minus(x1);
|
|
161
181
|
const dy = y2.minus(y1);
|
|
@@ -168,7 +188,12 @@ export function pointToLineDistance(pt, lineStart, lineEnd) {
|
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
// Numerator: |(y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1|
|
|
171
|
-
const numerator = dy
|
|
191
|
+
const numerator = dy
|
|
192
|
+
.mul(x0)
|
|
193
|
+
.minus(dx.mul(y0))
|
|
194
|
+
.plus(x2.mul(y1))
|
|
195
|
+
.minus(y2.mul(x1))
|
|
196
|
+
.abs();
|
|
172
197
|
|
|
173
198
|
// Denominator: sqrt((y2-y1)² + (x2-x1)²)
|
|
174
199
|
const denominator = lineLengthSq.sqrt();
|
|
@@ -185,10 +210,14 @@ export function pointToLineDistance(pt, lineStart, lineEnd) {
|
|
|
185
210
|
* @returns {Decimal} Cross product (positive = CCW, negative = CW, zero = collinear)
|
|
186
211
|
*/
|
|
187
212
|
export function crossProduct(p1, p2, p3) {
|
|
188
|
-
if (!p1 || !p2 || !p3)
|
|
189
|
-
|
|
190
|
-
if (!(
|
|
191
|
-
|
|
213
|
+
if (!p1 || !p2 || !p3)
|
|
214
|
+
throw new Error("crossProduct: points cannot be null or undefined");
|
|
215
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
216
|
+
throw new Error("crossProduct: p1 must have Decimal x and y properties");
|
|
217
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
218
|
+
throw new Error("crossProduct: p2 must have Decimal x and y properties");
|
|
219
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
220
|
+
throw new Error("crossProduct: p3 must have Decimal x and y properties");
|
|
192
221
|
const v1x = p2.x.minus(p1.x);
|
|
193
222
|
const v1y = p2.y.minus(p1.y);
|
|
194
223
|
const v2x = p3.x.minus(p1.x);
|
|
@@ -212,26 +241,41 @@ export function crossProduct(p1, p2, p3) {
|
|
|
212
241
|
* @returns {{x: Decimal, y: Decimal}} Point on curve
|
|
213
242
|
*/
|
|
214
243
|
export function evaluateCubicBezier(p0, p1, p2, p3, t) {
|
|
215
|
-
if (!p0 || !p1 || !p2 || !p3)
|
|
216
|
-
|
|
217
|
-
if (!(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
245
|
+
throw new Error("evaluateCubicBezier: points cannot be null or undefined");
|
|
246
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
247
|
+
throw new Error(
|
|
248
|
+
"evaluateCubicBezier: p0 must have Decimal x and y properties",
|
|
249
|
+
);
|
|
250
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
251
|
+
throw new Error(
|
|
252
|
+
"evaluateCubicBezier: p1 must have Decimal x and y properties",
|
|
253
|
+
);
|
|
254
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
255
|
+
throw new Error(
|
|
256
|
+
"evaluateCubicBezier: p2 must have Decimal x and y properties",
|
|
257
|
+
);
|
|
258
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
259
|
+
throw new Error(
|
|
260
|
+
"evaluateCubicBezier: p3 must have Decimal x and y properties",
|
|
261
|
+
);
|
|
262
|
+
if (t === null || t === undefined)
|
|
263
|
+
throw new Error("evaluateCubicBezier: t parameter is null or undefined");
|
|
221
264
|
const tD = D(t);
|
|
222
|
-
if (!tD.isFinite()) throw new Error(
|
|
223
|
-
if (tD.lessThan(0) || tD.greaterThan(1))
|
|
265
|
+
if (!tD.isFinite()) throw new Error("evaluateCubicBezier: t must be finite");
|
|
266
|
+
if (tD.lessThan(0) || tD.greaterThan(1))
|
|
267
|
+
throw new Error("evaluateCubicBezier: t must be in range [0, 1]");
|
|
224
268
|
const oneMinusT = D(1).minus(tD);
|
|
225
269
|
|
|
226
270
|
// Bernstein basis polynomials
|
|
227
|
-
const b0 = oneMinusT.pow(3);
|
|
228
|
-
const b1 = D(3).mul(oneMinusT.pow(2)).mul(tD);
|
|
229
|
-
const b2 = D(3).mul(oneMinusT).mul(tD.pow(2));
|
|
230
|
-
const b3 = tD.pow(3);
|
|
271
|
+
const b0 = oneMinusT.pow(3); // (1-t)³
|
|
272
|
+
const b1 = D(3).mul(oneMinusT.pow(2)).mul(tD); // 3(1-t)²t
|
|
273
|
+
const b2 = D(3).mul(oneMinusT).mul(tD.pow(2)); // 3(1-t)t²
|
|
274
|
+
const b3 = tD.pow(3); // t³
|
|
231
275
|
|
|
232
276
|
return {
|
|
233
277
|
x: b0.mul(p0.x).plus(b1.mul(p1.x)).plus(b2.mul(p2.x)).plus(b3.mul(p3.x)),
|
|
234
|
-
y: b0.mul(p0.y).plus(b1.mul(p1.y)).plus(b2.mul(p2.y)).plus(b3.mul(p3.y))
|
|
278
|
+
y: b0.mul(p0.y).plus(b1.mul(p1.y)).plus(b2.mul(p2.y)).plus(b3.mul(p3.y)),
|
|
235
279
|
};
|
|
236
280
|
}
|
|
237
281
|
|
|
@@ -246,24 +290,41 @@ export function evaluateCubicBezier(p0, p1, p2, p3, t) {
|
|
|
246
290
|
* @returns {{x: Decimal, y: Decimal}} Point on curve
|
|
247
291
|
*/
|
|
248
292
|
export function evaluateQuadraticBezier(p0, p1, p2, t) {
|
|
249
|
-
if (!p0 || !p1 || !p2)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
293
|
+
if (!p0 || !p1 || !p2)
|
|
294
|
+
throw new Error(
|
|
295
|
+
"evaluateQuadraticBezier: points cannot be null or undefined",
|
|
296
|
+
);
|
|
297
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
298
|
+
throw new Error(
|
|
299
|
+
"evaluateQuadraticBezier: p0 must have Decimal x and y properties",
|
|
300
|
+
);
|
|
301
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
302
|
+
throw new Error(
|
|
303
|
+
"evaluateQuadraticBezier: p1 must have Decimal x and y properties",
|
|
304
|
+
);
|
|
305
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
306
|
+
throw new Error(
|
|
307
|
+
"evaluateQuadraticBezier: p2 must have Decimal x and y properties",
|
|
308
|
+
);
|
|
309
|
+
if (t === null || t === undefined)
|
|
310
|
+
throw new Error(
|
|
311
|
+
"evaluateQuadraticBezier: t parameter is null or undefined",
|
|
312
|
+
);
|
|
254
313
|
const tD = D(t);
|
|
255
|
-
if (!tD.isFinite())
|
|
256
|
-
|
|
314
|
+
if (!tD.isFinite())
|
|
315
|
+
throw new Error("evaluateQuadraticBezier: t must be finite");
|
|
316
|
+
if (tD.lessThan(0) || tD.greaterThan(1))
|
|
317
|
+
throw new Error("evaluateQuadraticBezier: t must be in range [0, 1]");
|
|
257
318
|
const oneMinusT = D(1).minus(tD);
|
|
258
319
|
|
|
259
320
|
// Bernstein basis polynomials
|
|
260
|
-
const b0 = oneMinusT.pow(2);
|
|
261
|
-
const b1 = D(2).mul(oneMinusT).mul(tD);
|
|
262
|
-
const b2 = tD.pow(2);
|
|
321
|
+
const b0 = oneMinusT.pow(2); // (1-t)²
|
|
322
|
+
const b1 = D(2).mul(oneMinusT).mul(tD); // 2(1-t)t
|
|
323
|
+
const b2 = tD.pow(2); // t²
|
|
263
324
|
|
|
264
325
|
return {
|
|
265
326
|
x: b0.mul(p0.x).plus(b1.mul(p1.x)).plus(b2.mul(p2.x)),
|
|
266
|
-
y: b0.mul(p0.y).plus(b1.mul(p1.y)).plus(b2.mul(p2.y))
|
|
327
|
+
y: b0.mul(p0.y).plus(b1.mul(p1.y)).plus(b2.mul(p2.y)),
|
|
267
328
|
};
|
|
268
329
|
}
|
|
269
330
|
|
|
@@ -277,17 +338,22 @@ export function evaluateQuadraticBezier(p0, p1, p2, t) {
|
|
|
277
338
|
* @returns {{x: Decimal, y: Decimal}} Point on line
|
|
278
339
|
*/
|
|
279
340
|
export function evaluateLine(p0, p1, t) {
|
|
280
|
-
if (!p0 || !p1)
|
|
281
|
-
|
|
282
|
-
if (!(
|
|
283
|
-
|
|
341
|
+
if (!p0 || !p1)
|
|
342
|
+
throw new Error("evaluateLine: points cannot be null or undefined");
|
|
343
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
344
|
+
throw new Error("evaluateLine: p0 must have Decimal x and y properties");
|
|
345
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
346
|
+
throw new Error("evaluateLine: p1 must have Decimal x and y properties");
|
|
347
|
+
if (t === null || t === undefined)
|
|
348
|
+
throw new Error("evaluateLine: t parameter is null or undefined");
|
|
284
349
|
const tD = D(t);
|
|
285
|
-
if (!tD.isFinite()) throw new Error(
|
|
286
|
-
if (tD.lessThan(0) || tD.greaterThan(1))
|
|
350
|
+
if (!tD.isFinite()) throw new Error("evaluateLine: t must be finite");
|
|
351
|
+
if (tD.lessThan(0) || tD.greaterThan(1))
|
|
352
|
+
throw new Error("evaluateLine: t must be in range [0, 1]");
|
|
287
353
|
const oneMinusT = D(1).minus(tD);
|
|
288
354
|
return {
|
|
289
355
|
x: oneMinusT.mul(p0.x).plus(tD.mul(p1.x)),
|
|
290
|
-
y: oneMinusT.mul(p0.y).plus(tD.mul(p1.y))
|
|
356
|
+
y: oneMinusT.mul(p0.y).plus(tD.mul(p1.y)),
|
|
291
357
|
};
|
|
292
358
|
}
|
|
293
359
|
|
|
@@ -311,15 +377,42 @@ export function evaluateLine(p0, p1, t) {
|
|
|
311
377
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
312
378
|
* @returns {{isStraight: boolean, maxDeviation: Decimal, verified: boolean}}
|
|
313
379
|
*/
|
|
314
|
-
export function isCubicBezierStraight(
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
380
|
+
export function isCubicBezierStraight(
|
|
381
|
+
p0,
|
|
382
|
+
p1,
|
|
383
|
+
p2,
|
|
384
|
+
p3,
|
|
385
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
386
|
+
) {
|
|
387
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
388
|
+
throw new Error(
|
|
389
|
+
"isCubicBezierStraight: points cannot be null or undefined",
|
|
390
|
+
);
|
|
391
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
392
|
+
throw new Error(
|
|
393
|
+
"isCubicBezierStraight: p0 must have Decimal x and y properties",
|
|
394
|
+
);
|
|
395
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
396
|
+
throw new Error(
|
|
397
|
+
"isCubicBezierStraight: p1 must have Decimal x and y properties",
|
|
398
|
+
);
|
|
399
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
400
|
+
throw new Error(
|
|
401
|
+
"isCubicBezierStraight: p2 must have Decimal x and y properties",
|
|
402
|
+
);
|
|
403
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
404
|
+
throw new Error(
|
|
405
|
+
"isCubicBezierStraight: p3 must have Decimal x and y properties",
|
|
406
|
+
);
|
|
407
|
+
if (tolerance === null || tolerance === undefined)
|
|
408
|
+
throw new Error(
|
|
409
|
+
"isCubicBezierStraight: tolerance parameter is null or undefined",
|
|
410
|
+
);
|
|
321
411
|
const tol = D(tolerance);
|
|
322
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
412
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
413
|
+
throw new Error(
|
|
414
|
+
"isCubicBezierStraight: tolerance must be a non-negative finite number",
|
|
415
|
+
);
|
|
323
416
|
|
|
324
417
|
// Check if start and end are the same point (degenerate case)
|
|
325
418
|
const chordLength = distance(p0, p3);
|
|
@@ -331,7 +424,7 @@ export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERA
|
|
|
331
424
|
return {
|
|
332
425
|
isStraight: maxDev.lessThan(tol),
|
|
333
426
|
maxDeviation: maxDev,
|
|
334
|
-
verified: true
|
|
427
|
+
verified: true,
|
|
335
428
|
};
|
|
336
429
|
}
|
|
337
430
|
|
|
@@ -345,7 +438,7 @@ export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERA
|
|
|
345
438
|
return {
|
|
346
439
|
isStraight: false,
|
|
347
440
|
maxDeviation: maxControlDeviation,
|
|
348
|
-
verified: true
|
|
441
|
+
verified: true,
|
|
349
442
|
};
|
|
350
443
|
}
|
|
351
444
|
|
|
@@ -366,7 +459,7 @@ export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERA
|
|
|
366
459
|
return {
|
|
367
460
|
isStraight: maxControlDeviation.lessThan(tol) && verified,
|
|
368
461
|
maxDeviation: Decimal.max(maxControlDeviation, maxSampleDeviation),
|
|
369
|
-
verified: true
|
|
462
|
+
verified: true,
|
|
370
463
|
};
|
|
371
464
|
}
|
|
372
465
|
|
|
@@ -379,14 +472,37 @@ export function isCubicBezierStraight(p0, p1, p2, p3, tolerance = DEFAULT_TOLERA
|
|
|
379
472
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
380
473
|
* @returns {{isStraight: boolean, maxDeviation: Decimal, verified: boolean}}
|
|
381
474
|
*/
|
|
382
|
-
export function isQuadraticBezierStraight(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
475
|
+
export function isQuadraticBezierStraight(
|
|
476
|
+
p0,
|
|
477
|
+
p1,
|
|
478
|
+
p2,
|
|
479
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
480
|
+
) {
|
|
481
|
+
if (!p0 || !p1 || !p2)
|
|
482
|
+
throw new Error(
|
|
483
|
+
"isQuadraticBezierStraight: points cannot be null or undefined",
|
|
484
|
+
);
|
|
485
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
486
|
+
throw new Error(
|
|
487
|
+
"isQuadraticBezierStraight: p0 must have Decimal x and y properties",
|
|
488
|
+
);
|
|
489
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
490
|
+
throw new Error(
|
|
491
|
+
"isQuadraticBezierStraight: p1 must have Decimal x and y properties",
|
|
492
|
+
);
|
|
493
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
494
|
+
throw new Error(
|
|
495
|
+
"isQuadraticBezierStraight: p2 must have Decimal x and y properties",
|
|
496
|
+
);
|
|
497
|
+
if (tolerance === null || tolerance === undefined)
|
|
498
|
+
throw new Error(
|
|
499
|
+
"isQuadraticBezierStraight: tolerance parameter is null or undefined",
|
|
500
|
+
);
|
|
388
501
|
const tol = D(tolerance);
|
|
389
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
502
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
503
|
+
throw new Error(
|
|
504
|
+
"isQuadraticBezierStraight: tolerance must be a non-negative finite number",
|
|
505
|
+
);
|
|
390
506
|
|
|
391
507
|
// Check if start and end are the same point (degenerate case)
|
|
392
508
|
const chordLength = distance(p0, p2);
|
|
@@ -395,7 +511,7 @@ export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERA
|
|
|
395
511
|
return {
|
|
396
512
|
isStraight: d1.lessThan(tol),
|
|
397
513
|
maxDeviation: d1,
|
|
398
|
-
verified: true
|
|
514
|
+
verified: true,
|
|
399
515
|
};
|
|
400
516
|
}
|
|
401
517
|
|
|
@@ -407,7 +523,7 @@ export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERA
|
|
|
407
523
|
return {
|
|
408
524
|
isStraight: false,
|
|
409
525
|
maxDeviation: controlDeviation,
|
|
410
|
-
verified: true
|
|
526
|
+
verified: true,
|
|
411
527
|
};
|
|
412
528
|
}
|
|
413
529
|
|
|
@@ -428,7 +544,7 @@ export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERA
|
|
|
428
544
|
return {
|
|
429
545
|
isStraight: controlDeviation.lessThan(tol) && verified,
|
|
430
546
|
maxDeviation: Decimal.max(controlDeviation, maxSampleDeviation),
|
|
431
|
-
verified: true
|
|
547
|
+
verified: true,
|
|
432
548
|
};
|
|
433
549
|
}
|
|
434
550
|
|
|
@@ -443,7 +559,13 @@ export function isQuadraticBezierStraight(p0, p1, p2, tolerance = DEFAULT_TOLERA
|
|
|
443
559
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
444
560
|
* @returns {{start: {x: Decimal, y: Decimal}, end: {x: Decimal, y: Decimal}, maxDeviation: Decimal} | null}
|
|
445
561
|
*/
|
|
446
|
-
export function cubicBezierToLine(
|
|
562
|
+
export function cubicBezierToLine(
|
|
563
|
+
p0,
|
|
564
|
+
p1,
|
|
565
|
+
p2,
|
|
566
|
+
p3,
|
|
567
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
568
|
+
) {
|
|
447
569
|
// Validation is done in isCubicBezierStraight
|
|
448
570
|
const result = isCubicBezierStraight(p0, p1, p2, p3, tolerance);
|
|
449
571
|
if (!result.isStraight || !result.verified) {
|
|
@@ -452,7 +574,7 @@ export function cubicBezierToLine(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
452
574
|
return {
|
|
453
575
|
start: { x: p0.x, y: p0.y },
|
|
454
576
|
end: { x: p3.x, y: p3.y },
|
|
455
|
-
maxDeviation: result.maxDeviation
|
|
577
|
+
maxDeviation: result.maxDeviation,
|
|
456
578
|
};
|
|
457
579
|
}
|
|
458
580
|
|
|
@@ -480,28 +602,55 @@ export function cubicBezierToLine(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
480
602
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
481
603
|
* @returns {{canLower: boolean, quadraticControl: {x: Decimal, y: Decimal} | null, maxDeviation: Decimal, verified: boolean}}
|
|
482
604
|
*/
|
|
483
|
-
export function canLowerCubicToQuadratic(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
605
|
+
export function canLowerCubicToQuadratic(
|
|
606
|
+
p0,
|
|
607
|
+
p1,
|
|
608
|
+
p2,
|
|
609
|
+
p3,
|
|
610
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
611
|
+
) {
|
|
612
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
613
|
+
throw new Error(
|
|
614
|
+
"canLowerCubicToQuadratic: points cannot be null or undefined",
|
|
615
|
+
);
|
|
616
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
617
|
+
throw new Error(
|
|
618
|
+
"canLowerCubicToQuadratic: p0 must have Decimal x and y properties",
|
|
619
|
+
);
|
|
620
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
621
|
+
throw new Error(
|
|
622
|
+
"canLowerCubicToQuadratic: p1 must have Decimal x and y properties",
|
|
623
|
+
);
|
|
624
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
625
|
+
throw new Error(
|
|
626
|
+
"canLowerCubicToQuadratic: p2 must have Decimal x and y properties",
|
|
627
|
+
);
|
|
628
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
629
|
+
throw new Error(
|
|
630
|
+
"canLowerCubicToQuadratic: p3 must have Decimal x and y properties",
|
|
631
|
+
);
|
|
632
|
+
if (tolerance === null || tolerance === undefined)
|
|
633
|
+
throw new Error(
|
|
634
|
+
"canLowerCubicToQuadratic: tolerance parameter is null or undefined",
|
|
635
|
+
);
|
|
490
636
|
const tol = D(tolerance);
|
|
491
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
637
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
638
|
+
throw new Error(
|
|
639
|
+
"canLowerCubicToQuadratic: tolerance must be a non-negative finite number",
|
|
640
|
+
);
|
|
492
641
|
const three = D(3);
|
|
493
642
|
const two = D(2);
|
|
494
643
|
|
|
495
644
|
// Calculate Q1 from P1: Q1 = (3*P1 - P0) / 2
|
|
496
645
|
const q1FromP1 = {
|
|
497
646
|
x: three.mul(p1.x).minus(p0.x).div(two),
|
|
498
|
-
y: three.mul(p1.y).minus(p0.y).div(two)
|
|
647
|
+
y: three.mul(p1.y).minus(p0.y).div(two),
|
|
499
648
|
};
|
|
500
649
|
|
|
501
650
|
// Calculate Q1 from P2: Q1 = (3*P2 - P3) / 2
|
|
502
651
|
const q1FromP2 = {
|
|
503
652
|
x: three.mul(p2.x).minus(p3.x).div(two),
|
|
504
|
-
y: three.mul(p2.y).minus(p3.y).div(two)
|
|
653
|
+
y: three.mul(p2.y).minus(p3.y).div(two),
|
|
505
654
|
};
|
|
506
655
|
|
|
507
656
|
// Check if these are equal within tolerance
|
|
@@ -512,14 +661,14 @@ export function canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOL
|
|
|
512
661
|
canLower: false,
|
|
513
662
|
quadraticControl: null,
|
|
514
663
|
maxDeviation: deviation,
|
|
515
|
-
verified: true
|
|
664
|
+
verified: true,
|
|
516
665
|
};
|
|
517
666
|
}
|
|
518
667
|
|
|
519
668
|
// Use the average as the quadratic control point
|
|
520
669
|
const q1 = {
|
|
521
670
|
x: q1FromP1.x.plus(q1FromP2.x).div(two),
|
|
522
|
-
y: q1FromP1.y.plus(q1FromP2.y).div(two)
|
|
671
|
+
y: q1FromP1.y.plus(q1FromP2.y).div(two),
|
|
523
672
|
};
|
|
524
673
|
|
|
525
674
|
// VERIFICATION: Sample both curves and compare
|
|
@@ -540,7 +689,7 @@ export function canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOL
|
|
|
540
689
|
canLower: verified,
|
|
541
690
|
quadraticControl: verified ? q1 : null,
|
|
542
691
|
maxDeviation: Decimal.max(deviation, maxSampleDeviation),
|
|
543
|
-
verified: true
|
|
692
|
+
verified: true,
|
|
544
693
|
};
|
|
545
694
|
}
|
|
546
695
|
|
|
@@ -555,7 +704,13 @@ export function canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOL
|
|
|
555
704
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
556
705
|
* @returns {{p0: {x: Decimal, y: Decimal}, p1: {x: Decimal, y: Decimal}, p2: {x: Decimal, y: Decimal}, maxDeviation: Decimal} | null}
|
|
557
706
|
*/
|
|
558
|
-
export function cubicToQuadratic(
|
|
707
|
+
export function cubicToQuadratic(
|
|
708
|
+
p0,
|
|
709
|
+
p1,
|
|
710
|
+
p2,
|
|
711
|
+
p3,
|
|
712
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
713
|
+
) {
|
|
559
714
|
// Validation is done in canLowerCubicToQuadratic
|
|
560
715
|
const result = canLowerCubicToQuadratic(p0, p1, p2, p3, tolerance);
|
|
561
716
|
if (!result.canLower || !result.quadraticControl) {
|
|
@@ -565,7 +720,7 @@ export function cubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
565
720
|
p0: { x: p0.x, y: p0.y },
|
|
566
721
|
p1: result.quadraticControl,
|
|
567
722
|
p2: { x: p3.x, y: p3.y },
|
|
568
|
-
maxDeviation: result.maxDeviation
|
|
723
|
+
maxDeviation: result.maxDeviation,
|
|
569
724
|
};
|
|
570
725
|
}
|
|
571
726
|
|
|
@@ -583,9 +738,14 @@ export function cubicToQuadratic(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
583
738
|
* @returns {{center: {x: Decimal, y: Decimal}, radius: Decimal} | null}
|
|
584
739
|
*/
|
|
585
740
|
export function fitCircleToPoints(points) {
|
|
586
|
-
if (!points)
|
|
587
|
-
|
|
588
|
-
|
|
741
|
+
if (!points)
|
|
742
|
+
throw new Error(
|
|
743
|
+
"fitCircleToPoints: points array cannot be null or undefined",
|
|
744
|
+
);
|
|
745
|
+
if (!Array.isArray(points))
|
|
746
|
+
throw new Error("fitCircleToPoints: points must be an array");
|
|
747
|
+
if (points.length === 0)
|
|
748
|
+
throw new Error("fitCircleToPoints: points array cannot be empty");
|
|
589
749
|
if (points.length < 3) {
|
|
590
750
|
return null;
|
|
591
751
|
}
|
|
@@ -593,22 +753,33 @@ export function fitCircleToPoints(points) {
|
|
|
593
753
|
// Validate all points have Decimal x and y properties
|
|
594
754
|
for (let i = 0; i < points.length; i++) {
|
|
595
755
|
const p = points[i];
|
|
596
|
-
if (!p)
|
|
756
|
+
if (!p)
|
|
757
|
+
throw new Error(
|
|
758
|
+
`fitCircleToPoints: point at index ${i} is null or undefined`,
|
|
759
|
+
);
|
|
597
760
|
if (!(p.x instanceof Decimal) || !(p.y instanceof Decimal)) {
|
|
598
|
-
throw new Error(
|
|
761
|
+
throw new Error(
|
|
762
|
+
`fitCircleToPoints: point at index ${i} must have Decimal x and y properties`,
|
|
763
|
+
);
|
|
599
764
|
}
|
|
600
765
|
}
|
|
601
766
|
|
|
602
767
|
const n = D(points.length);
|
|
603
|
-
let sumX = D(0),
|
|
604
|
-
|
|
768
|
+
let sumX = D(0),
|
|
769
|
+
sumY = D(0);
|
|
770
|
+
let sumX2 = D(0),
|
|
771
|
+
sumY2 = D(0);
|
|
605
772
|
let sumXY = D(0);
|
|
606
|
-
let sumX3 = D(0),
|
|
607
|
-
|
|
773
|
+
let sumX3 = D(0),
|
|
774
|
+
sumY3 = D(0);
|
|
775
|
+
let sumX2Y = D(0),
|
|
776
|
+
sumXY2 = D(0);
|
|
608
777
|
|
|
609
778
|
for (const p of points) {
|
|
610
|
-
const x = p.x,
|
|
611
|
-
|
|
779
|
+
const x = p.x,
|
|
780
|
+
y = p.y;
|
|
781
|
+
const x2 = x.mul(x),
|
|
782
|
+
y2 = y.mul(y);
|
|
612
783
|
|
|
613
784
|
sumX = sumX.plus(x);
|
|
614
785
|
sumY = sumY.plus(y);
|
|
@@ -631,8 +802,20 @@ export function fitCircleToPoints(points) {
|
|
|
631
802
|
const A = n.mul(sumX2).minus(sumX.mul(sumX));
|
|
632
803
|
const B = n.mul(sumXY).minus(sumX.mul(sumY));
|
|
633
804
|
const C = n.mul(sumY2).minus(sumY.mul(sumY));
|
|
634
|
-
const DD = D(0.5).mul(
|
|
635
|
-
|
|
805
|
+
const DD = D(0.5).mul(
|
|
806
|
+
n
|
|
807
|
+
.mul(sumX3)
|
|
808
|
+
.plus(n.mul(sumXY2))
|
|
809
|
+
.minus(sumX.mul(sumX2))
|
|
810
|
+
.minus(sumX.mul(sumY2)),
|
|
811
|
+
);
|
|
812
|
+
const E = D(0.5).mul(
|
|
813
|
+
n
|
|
814
|
+
.mul(sumX2Y)
|
|
815
|
+
.plus(n.mul(sumY3))
|
|
816
|
+
.minus(sumY.mul(sumX2))
|
|
817
|
+
.minus(sumY.mul(sumY2)),
|
|
818
|
+
);
|
|
636
819
|
|
|
637
820
|
// Solve: A*a + B*b = D, B*a + C*b = E
|
|
638
821
|
const det = A.mul(C).minus(B.mul(B));
|
|
@@ -670,15 +853,42 @@ export function fitCircleToPoints(points) {
|
|
|
670
853
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
671
854
|
* @returns {{isArc: boolean, circle: {center: {x: Decimal, y: Decimal}, radius: Decimal} | null, maxDeviation: Decimal, verified: boolean}}
|
|
672
855
|
*/
|
|
673
|
-
export function fitCircleToCubicBezier(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
856
|
+
export function fitCircleToCubicBezier(
|
|
857
|
+
p0,
|
|
858
|
+
p1,
|
|
859
|
+
p2,
|
|
860
|
+
p3,
|
|
861
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
862
|
+
) {
|
|
863
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
864
|
+
throw new Error(
|
|
865
|
+
"fitCircleToCubicBezier: points cannot be null or undefined",
|
|
866
|
+
);
|
|
867
|
+
if (!(p0.x instanceof Decimal) || !(p0.y instanceof Decimal))
|
|
868
|
+
throw new Error(
|
|
869
|
+
"fitCircleToCubicBezier: p0 must have Decimal x and y properties",
|
|
870
|
+
);
|
|
871
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
872
|
+
throw new Error(
|
|
873
|
+
"fitCircleToCubicBezier: p1 must have Decimal x and y properties",
|
|
874
|
+
);
|
|
875
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
876
|
+
throw new Error(
|
|
877
|
+
"fitCircleToCubicBezier: p2 must have Decimal x and y properties",
|
|
878
|
+
);
|
|
879
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
880
|
+
throw new Error(
|
|
881
|
+
"fitCircleToCubicBezier: p3 must have Decimal x and y properties",
|
|
882
|
+
);
|
|
883
|
+
if (tolerance === null || tolerance === undefined)
|
|
884
|
+
throw new Error(
|
|
885
|
+
"fitCircleToCubicBezier: tolerance parameter is null or undefined",
|
|
886
|
+
);
|
|
680
887
|
const tol = D(tolerance);
|
|
681
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
888
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
889
|
+
throw new Error(
|
|
890
|
+
"fitCircleToCubicBezier: tolerance must be a non-negative finite number",
|
|
891
|
+
);
|
|
682
892
|
|
|
683
893
|
// Sample points along the curve for fitting
|
|
684
894
|
const sampleCount = 9; // Including endpoints
|
|
@@ -697,7 +907,7 @@ export function fitCircleToCubicBezier(p0, p1, p2, p3, tolerance = DEFAULT_TOLER
|
|
|
697
907
|
isArc: false,
|
|
698
908
|
circle: null,
|
|
699
909
|
maxDeviation: D(Infinity),
|
|
700
|
-
verified: true
|
|
910
|
+
verified: true,
|
|
701
911
|
};
|
|
702
912
|
}
|
|
703
913
|
|
|
@@ -719,7 +929,7 @@ export function fitCircleToCubicBezier(p0, p1, p2, p3, tolerance = DEFAULT_TOLER
|
|
|
719
929
|
isArc,
|
|
720
930
|
circle: isArc ? circle : null,
|
|
721
931
|
maxDeviation,
|
|
722
|
-
verified: true
|
|
932
|
+
verified: true,
|
|
723
933
|
};
|
|
724
934
|
}
|
|
725
935
|
|
|
@@ -734,7 +944,13 @@ export function fitCircleToCubicBezier(p0, p1, p2, p3, tolerance = DEFAULT_TOLER
|
|
|
734
944
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
735
945
|
* @returns {{rx: Decimal, ry: Decimal, rotation: Decimal, largeArc: number, sweep: number, endX: Decimal, endY: Decimal, maxDeviation: Decimal} | null}
|
|
736
946
|
*/
|
|
737
|
-
export function cubicBezierToArc(
|
|
947
|
+
export function cubicBezierToArc(
|
|
948
|
+
p0,
|
|
949
|
+
p1,
|
|
950
|
+
p2,
|
|
951
|
+
p3,
|
|
952
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
953
|
+
) {
|
|
738
954
|
// Validation is done in fitCircleToCubicBezier
|
|
739
955
|
const result = fitCircleToCubicBezier(p0, p1, p2, p3, tolerance);
|
|
740
956
|
|
|
@@ -785,7 +1001,7 @@ export function cubicBezierToArc(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
785
1001
|
sweep,
|
|
786
1002
|
endX: p3.x,
|
|
787
1003
|
endY: p3.y,
|
|
788
|
-
maxDeviation: result.maxDeviation
|
|
1004
|
+
maxDeviation: result.maxDeviation,
|
|
789
1005
|
};
|
|
790
1006
|
}
|
|
791
1007
|
|
|
@@ -804,14 +1020,21 @@ export function cubicBezierToArc(p0, p1, p2, p3, tolerance = DEFAULT_TOLERANCE)
|
|
|
804
1020
|
* @returns {Decimal | null} Sagitta value, or null if chord > diameter
|
|
805
1021
|
*/
|
|
806
1022
|
export function calculateSagitta(radius, chordLength) {
|
|
807
|
-
if (radius === null || radius === undefined)
|
|
808
|
-
|
|
1023
|
+
if (radius === null || radius === undefined)
|
|
1024
|
+
throw new Error("calculateSagitta: radius parameter is null or undefined");
|
|
1025
|
+
if (chordLength === null || chordLength === undefined)
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
"calculateSagitta: chordLength parameter is null or undefined",
|
|
1028
|
+
);
|
|
809
1029
|
const r = D(radius);
|
|
810
1030
|
const c = D(chordLength);
|
|
811
|
-
if (!r.isFinite()) throw new Error(
|
|
812
|
-
if (!c.isFinite())
|
|
813
|
-
|
|
814
|
-
if (
|
|
1031
|
+
if (!r.isFinite()) throw new Error("calculateSagitta: radius must be finite");
|
|
1032
|
+
if (!c.isFinite())
|
|
1033
|
+
throw new Error("calculateSagitta: chordLength must be finite");
|
|
1034
|
+
if (r.lessThan(0))
|
|
1035
|
+
throw new Error("calculateSagitta: radius must be non-negative");
|
|
1036
|
+
if (c.lessThan(0))
|
|
1037
|
+
throw new Error("calculateSagitta: chordLength must be non-negative");
|
|
815
1038
|
const halfChord = c.div(2);
|
|
816
1039
|
|
|
817
1040
|
// Check if chord is valid (must be <= 2*r)
|
|
@@ -843,24 +1066,51 @@ export function calculateSagitta(radius, chordLength) {
|
|
|
843
1066
|
* @param {Decimal} [tolerance=DEFAULT_TOLERANCE] - Maximum allowed deviation
|
|
844
1067
|
* @returns {{isStraight: boolean, sagitta: Decimal | null, maxDeviation: Decimal, verified: boolean}}
|
|
845
1068
|
*/
|
|
846
|
-
export function isArcStraight(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
if (
|
|
857
|
-
|
|
1069
|
+
export function isArcStraight(
|
|
1070
|
+
rx,
|
|
1071
|
+
ry,
|
|
1072
|
+
rotation,
|
|
1073
|
+
largeArc,
|
|
1074
|
+
sweep,
|
|
1075
|
+
start,
|
|
1076
|
+
end,
|
|
1077
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
1078
|
+
) {
|
|
1079
|
+
if (rx === null || rx === undefined)
|
|
1080
|
+
throw new Error("isArcStraight: rx parameter is null or undefined");
|
|
1081
|
+
if (ry === null || ry === undefined)
|
|
1082
|
+
throw new Error("isArcStraight: ry parameter is null or undefined");
|
|
1083
|
+
if (rotation === null || rotation === undefined)
|
|
1084
|
+
throw new Error("isArcStraight: rotation parameter is null or undefined");
|
|
1085
|
+
if (largeArc === null || largeArc === undefined)
|
|
1086
|
+
throw new Error("isArcStraight: largeArc parameter is null or undefined");
|
|
1087
|
+
if (sweep === null || sweep === undefined)
|
|
1088
|
+
throw new Error("isArcStraight: sweep parameter is null or undefined");
|
|
1089
|
+
if (!start || !end)
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
"isArcStraight: start and end points cannot be null or undefined",
|
|
1092
|
+
);
|
|
1093
|
+
if (!(start.x instanceof Decimal) || !(start.y instanceof Decimal))
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
"isArcStraight: start must have Decimal x and y properties",
|
|
1096
|
+
);
|
|
1097
|
+
if (!(end.x instanceof Decimal) || !(end.y instanceof Decimal))
|
|
1098
|
+
throw new Error("isArcStraight: end must have Decimal x and y properties");
|
|
1099
|
+
if (tolerance === null || tolerance === undefined)
|
|
1100
|
+
throw new Error("isArcStraight: tolerance parameter is null or undefined");
|
|
1101
|
+
if (largeArc !== 0 && largeArc !== 1)
|
|
1102
|
+
throw new Error("isArcStraight: largeArc must be 0 or 1");
|
|
1103
|
+
if (sweep !== 0 && sweep !== 1)
|
|
1104
|
+
throw new Error("isArcStraight: sweep must be 0 or 1");
|
|
858
1105
|
const tol = D(tolerance);
|
|
859
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
1106
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
"isArcStraight: tolerance must be a non-negative finite number",
|
|
1109
|
+
);
|
|
860
1110
|
const rxD = D(rx);
|
|
861
1111
|
const ryD = D(ry);
|
|
862
|
-
if (!rxD.isFinite()) throw new Error(
|
|
863
|
-
if (!ryD.isFinite()) throw new Error(
|
|
1112
|
+
if (!rxD.isFinite()) throw new Error("isArcStraight: rx must be finite");
|
|
1113
|
+
if (!ryD.isFinite()) throw new Error("isArcStraight: ry must be finite");
|
|
864
1114
|
|
|
865
1115
|
// Check for zero or near-zero radii
|
|
866
1116
|
if (rxD.abs().lessThan(EPSILON) || ryD.abs().lessThan(EPSILON)) {
|
|
@@ -868,7 +1118,7 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
|
|
|
868
1118
|
isStraight: true,
|
|
869
1119
|
sagitta: D(0),
|
|
870
1120
|
maxDeviation: D(0),
|
|
871
|
-
verified: true
|
|
1121
|
+
verified: true,
|
|
872
1122
|
};
|
|
873
1123
|
}
|
|
874
1124
|
|
|
@@ -885,7 +1135,7 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
|
|
|
885
1135
|
isStraight: false,
|
|
886
1136
|
sagitta: null,
|
|
887
1137
|
maxDeviation: rxD, // Max deviation is at least the radius
|
|
888
|
-
verified: true
|
|
1138
|
+
verified: true,
|
|
889
1139
|
};
|
|
890
1140
|
}
|
|
891
1141
|
|
|
@@ -896,7 +1146,7 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
|
|
|
896
1146
|
isStraight: effectiveSagitta.lessThan(tol),
|
|
897
1147
|
sagitta: effectiveSagitta,
|
|
898
1148
|
maxDeviation: effectiveSagitta,
|
|
899
|
-
verified: true
|
|
1149
|
+
verified: true,
|
|
900
1150
|
};
|
|
901
1151
|
}
|
|
902
1152
|
|
|
@@ -906,7 +1156,7 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
|
|
|
906
1156
|
isStraight: false,
|
|
907
1157
|
sagitta: null,
|
|
908
1158
|
maxDeviation: Decimal.max(rxD, ryD),
|
|
909
|
-
verified: false
|
|
1159
|
+
verified: false,
|
|
910
1160
|
};
|
|
911
1161
|
}
|
|
912
1162
|
|
|
@@ -924,13 +1174,21 @@ export function isArcStraight(rx, ry, rotation, largeArc, sweep, start, end, tol
|
|
|
924
1174
|
* @returns {boolean} True if collinear
|
|
925
1175
|
*/
|
|
926
1176
|
export function areCollinear(p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
|
|
927
|
-
if (!p1 || !p2 || !p3)
|
|
928
|
-
|
|
929
|
-
if (!(
|
|
930
|
-
|
|
931
|
-
if (
|
|
1177
|
+
if (!p1 || !p2 || !p3)
|
|
1178
|
+
throw new Error("areCollinear: points cannot be null or undefined");
|
|
1179
|
+
if (!(p1.x instanceof Decimal) || !(p1.y instanceof Decimal))
|
|
1180
|
+
throw new Error("areCollinear: p1 must have Decimal x and y properties");
|
|
1181
|
+
if (!(p2.x instanceof Decimal) || !(p2.y instanceof Decimal))
|
|
1182
|
+
throw new Error("areCollinear: p2 must have Decimal x and y properties");
|
|
1183
|
+
if (!(p3.x instanceof Decimal) || !(p3.y instanceof Decimal))
|
|
1184
|
+
throw new Error("areCollinear: p3 must have Decimal x and y properties");
|
|
1185
|
+
if (tolerance === null || tolerance === undefined)
|
|
1186
|
+
throw new Error("areCollinear: tolerance parameter is null or undefined");
|
|
932
1187
|
const tol = D(tolerance);
|
|
933
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
1188
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
"areCollinear: tolerance must be a non-negative finite number",
|
|
1191
|
+
);
|
|
934
1192
|
|
|
935
1193
|
// Check using cross product (area of triangle)
|
|
936
1194
|
const cross = crossProduct(p1, p2, p3).abs();
|
|
@@ -954,17 +1212,30 @@ export function areCollinear(p1, p2, p3, tolerance = DEFAULT_TOLERANCE) {
|
|
|
954
1212
|
* @returns {{points: Array<{x: Decimal, y: Decimal}>, mergeCount: number, verified: boolean}}
|
|
955
1213
|
*/
|
|
956
1214
|
export function mergeCollinearSegments(points, tolerance = DEFAULT_TOLERANCE) {
|
|
957
|
-
if (!points)
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1215
|
+
if (!points)
|
|
1216
|
+
throw new Error(
|
|
1217
|
+
"mergeCollinearSegments: points array cannot be null or undefined",
|
|
1218
|
+
);
|
|
1219
|
+
if (!Array.isArray(points))
|
|
1220
|
+
throw new Error("mergeCollinearSegments: points must be an array");
|
|
1221
|
+
if (points.length === 0)
|
|
1222
|
+
throw new Error("mergeCollinearSegments: points array cannot be empty");
|
|
1223
|
+
if (tolerance === null || tolerance === undefined)
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
"mergeCollinearSegments: tolerance parameter is null or undefined",
|
|
1226
|
+
);
|
|
961
1227
|
|
|
962
1228
|
// Validate all points have Decimal x and y properties
|
|
963
1229
|
for (let i = 0; i < points.length; i++) {
|
|
964
1230
|
const p = points[i];
|
|
965
|
-
if (!p)
|
|
1231
|
+
if (!p)
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`mergeCollinearSegments: point at index ${i} is null or undefined`,
|
|
1234
|
+
);
|
|
966
1235
|
if (!(p.x instanceof Decimal) || !(p.y instanceof Decimal)) {
|
|
967
|
-
throw new Error(
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`mergeCollinearSegments: point at index ${i} must have Decimal x and y properties`,
|
|
1238
|
+
);
|
|
968
1239
|
}
|
|
969
1240
|
}
|
|
970
1241
|
|
|
@@ -973,7 +1244,10 @@ export function mergeCollinearSegments(points, tolerance = DEFAULT_TOLERANCE) {
|
|
|
973
1244
|
}
|
|
974
1245
|
|
|
975
1246
|
const tol = D(tolerance);
|
|
976
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
1247
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
"mergeCollinearSegments: tolerance must be a non-negative finite number",
|
|
1250
|
+
);
|
|
977
1251
|
const result = [points[0]];
|
|
978
1252
|
let mergeCount = 0;
|
|
979
1253
|
|
|
@@ -1035,12 +1309,25 @@ export function mergeCollinearSegments(points, tolerance = DEFAULT_TOLERANCE) {
|
|
|
1035
1309
|
* @returns {boolean} True if zero-length
|
|
1036
1310
|
*/
|
|
1037
1311
|
export function isZeroLengthSegment(start, end, tolerance = EPSILON) {
|
|
1038
|
-
if (!start || !end)
|
|
1039
|
-
|
|
1040
|
-
if (!(
|
|
1041
|
-
|
|
1312
|
+
if (!start || !end)
|
|
1313
|
+
throw new Error("isZeroLengthSegment: points cannot be null or undefined");
|
|
1314
|
+
if (!(start.x instanceof Decimal) || !(start.y instanceof Decimal))
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
"isZeroLengthSegment: start must have Decimal x and y properties",
|
|
1317
|
+
);
|
|
1318
|
+
if (!(end.x instanceof Decimal) || !(end.y instanceof Decimal))
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
"isZeroLengthSegment: end must have Decimal x and y properties",
|
|
1321
|
+
);
|
|
1322
|
+
if (tolerance === null || tolerance === undefined)
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
"isZeroLengthSegment: tolerance parameter is null or undefined",
|
|
1325
|
+
);
|
|
1042
1326
|
const tol = D(tolerance);
|
|
1043
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
1327
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
"isZeroLengthSegment: tolerance must be a non-negative finite number",
|
|
1330
|
+
);
|
|
1044
1331
|
return distance(start, end).lessThan(tol);
|
|
1045
1332
|
}
|
|
1046
1333
|
|
|
@@ -1052,49 +1339,86 @@ export function isZeroLengthSegment(start, end, tolerance = EPSILON) {
|
|
|
1052
1339
|
* @returns {{pathData: Array<{command: string, args: Array<Decimal>}>, removeCount: number, verified: boolean}}
|
|
1053
1340
|
*/
|
|
1054
1341
|
export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
1055
|
-
if (!pathData)
|
|
1056
|
-
|
|
1057
|
-
|
|
1342
|
+
if (!pathData)
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
"removeZeroLengthSegments: pathData cannot be null or undefined",
|
|
1345
|
+
);
|
|
1346
|
+
if (!Array.isArray(pathData))
|
|
1347
|
+
throw new Error("removeZeroLengthSegments: pathData must be an array");
|
|
1348
|
+
if (tolerance === null || tolerance === undefined)
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
"removeZeroLengthSegments: tolerance parameter is null or undefined",
|
|
1351
|
+
);
|
|
1058
1352
|
|
|
1059
1353
|
const tol = D(tolerance);
|
|
1060
|
-
if (!tol.isFinite() || tol.lessThan(0))
|
|
1354
|
+
if (!tol.isFinite() || tol.lessThan(0))
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
"removeZeroLengthSegments: tolerance must be a non-negative finite number",
|
|
1357
|
+
);
|
|
1061
1358
|
const result = [];
|
|
1062
1359
|
let removeCount = 0;
|
|
1063
|
-
let currentX = D(0),
|
|
1064
|
-
|
|
1360
|
+
let currentX = D(0),
|
|
1361
|
+
currentY = D(0);
|
|
1362
|
+
let startX = D(0),
|
|
1363
|
+
startY = D(0);
|
|
1065
1364
|
// Track previous control points for S and T commands (reserved for future S/T command handling)
|
|
1066
|
-
let prevCp2X = null,
|
|
1067
|
-
|
|
1365
|
+
let prevCp2X = null,
|
|
1366
|
+
prevCp2Y = null; // For S command (cubic)
|
|
1367
|
+
let prevCpX = null,
|
|
1368
|
+
prevCpY = null; // For T command (quadratic)
|
|
1068
1369
|
|
|
1069
1370
|
for (let idx = 0; idx < pathData.length; idx++) {
|
|
1070
1371
|
const item = pathData[idx];
|
|
1071
|
-
if (!item)
|
|
1072
|
-
|
|
1073
|
-
|
|
1372
|
+
if (!item)
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`removeZeroLengthSegments: item at index ${idx} is null or undefined`,
|
|
1375
|
+
);
|
|
1376
|
+
if (typeof item.command !== "string")
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`removeZeroLengthSegments: item at index ${idx} must have a string command property`,
|
|
1379
|
+
);
|
|
1380
|
+
if (!Array.isArray(item.args))
|
|
1381
|
+
throw new Error(
|
|
1382
|
+
`removeZeroLengthSegments: item at index ${idx} must have an args array property`,
|
|
1383
|
+
);
|
|
1074
1384
|
|
|
1075
1385
|
const { command, args } = item;
|
|
1076
1386
|
let keep = true;
|
|
1077
1387
|
|
|
1078
1388
|
switch (command.toUpperCase()) {
|
|
1079
|
-
case
|
|
1389
|
+
case "M":
|
|
1080
1390
|
// Update current position (absolute M) or move relative (lowercase m)
|
|
1081
|
-
if (args.length < 2)
|
|
1082
|
-
|
|
1083
|
-
|
|
1391
|
+
if (args.length < 2)
|
|
1392
|
+
throw new Error(
|
|
1393
|
+
`removeZeroLengthSegments: M command at index ${idx} requires 2 args, got ${args.length}`,
|
|
1394
|
+
);
|
|
1395
|
+
currentX = command === "M" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1396
|
+
currentY = command === "M" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1084
1397
|
// CRITICAL: Update subpath start for EVERY M command (BUG 3 FIX)
|
|
1085
1398
|
startX = currentX;
|
|
1086
1399
|
startY = currentY;
|
|
1087
1400
|
// Reset previous control points on new subpath
|
|
1088
|
-
prevCp2X = null;
|
|
1089
|
-
|
|
1401
|
+
prevCp2X = null;
|
|
1402
|
+
prevCp2Y = null;
|
|
1403
|
+
prevCpX = null;
|
|
1404
|
+
prevCpY = null;
|
|
1090
1405
|
break;
|
|
1091
1406
|
|
|
1092
|
-
case
|
|
1407
|
+
case "L": {
|
|
1093
1408
|
// Line to: x y (2 args)
|
|
1094
|
-
if (args.length < 2)
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1409
|
+
if (args.length < 2)
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`removeZeroLengthSegments: L command at index ${idx} requires 2 args, got ${args.length}`,
|
|
1412
|
+
);
|
|
1413
|
+
const endX = command === "L" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1414
|
+
const endY = command === "L" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1415
|
+
if (
|
|
1416
|
+
isZeroLengthSegment(
|
|
1417
|
+
{ x: currentX, y: currentY },
|
|
1418
|
+
{ x: endX, y: endY },
|
|
1419
|
+
tol,
|
|
1420
|
+
)
|
|
1421
|
+
) {
|
|
1098
1422
|
keep = false;
|
|
1099
1423
|
removeCount++;
|
|
1100
1424
|
}
|
|
@@ -1102,17 +1426,22 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1102
1426
|
currentX = endX;
|
|
1103
1427
|
currentY = endY;
|
|
1104
1428
|
// Reset previous control points (not a curve command)
|
|
1105
|
-
prevCp2X = null;
|
|
1106
|
-
|
|
1429
|
+
prevCp2X = null;
|
|
1430
|
+
prevCp2Y = null;
|
|
1431
|
+
prevCpX = null;
|
|
1432
|
+
prevCpY = null;
|
|
1107
1433
|
break;
|
|
1108
1434
|
}
|
|
1109
1435
|
|
|
1110
|
-
case
|
|
1436
|
+
case "T": {
|
|
1111
1437
|
// Smooth quadratic Bezier: x y (2 args)
|
|
1112
1438
|
// Control point is reflected from previous Q/T, or current position if none
|
|
1113
|
-
if (args.length < 2)
|
|
1114
|
-
|
|
1115
|
-
|
|
1439
|
+
if (args.length < 2)
|
|
1440
|
+
throw new Error(
|
|
1441
|
+
`removeZeroLengthSegments: T command at index ${idx} requires 2 args, got ${args.length}`,
|
|
1442
|
+
);
|
|
1443
|
+
const endX = command === "T" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1444
|
+
const endY = command === "T" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1116
1445
|
// Calculate implicit control point
|
|
1117
1446
|
let cpX, cpY;
|
|
1118
1447
|
if (prevCpX !== null && prevCpY !== null) {
|
|
@@ -1125,8 +1454,18 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1125
1454
|
cpY = currentY;
|
|
1126
1455
|
}
|
|
1127
1456
|
// Check if ALL points (start, implicit CP, end) are at same location
|
|
1128
|
-
if (
|
|
1129
|
-
|
|
1457
|
+
if (
|
|
1458
|
+
isZeroLengthSegment(
|
|
1459
|
+
{ x: currentX, y: currentY },
|
|
1460
|
+
{ x: endX, y: endY },
|
|
1461
|
+
tol,
|
|
1462
|
+
) &&
|
|
1463
|
+
isZeroLengthSegment(
|
|
1464
|
+
{ x: currentX, y: currentY },
|
|
1465
|
+
{ x: cpX, y: cpY },
|
|
1466
|
+
tol,
|
|
1467
|
+
)
|
|
1468
|
+
) {
|
|
1130
1469
|
keep = false;
|
|
1131
1470
|
removeCount++;
|
|
1132
1471
|
}
|
|
@@ -1136,13 +1475,17 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1136
1475
|
prevCpX = cpX;
|
|
1137
1476
|
prevCpY = cpY;
|
|
1138
1477
|
// T doesn't affect cubic control points
|
|
1139
|
-
prevCp2X = null;
|
|
1478
|
+
prevCp2X = null;
|
|
1479
|
+
prevCp2Y = null;
|
|
1140
1480
|
break;
|
|
1141
1481
|
}
|
|
1142
1482
|
|
|
1143
|
-
case
|
|
1144
|
-
if (args.length < 1)
|
|
1145
|
-
|
|
1483
|
+
case "H": {
|
|
1484
|
+
if (args.length < 1)
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
`removeZeroLengthSegments: H command at index ${idx} requires 1 arg, got ${args.length}`,
|
|
1487
|
+
);
|
|
1488
|
+
const endX = command === "H" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1146
1489
|
if (endX.minus(currentX).abs().lessThan(tol)) {
|
|
1147
1490
|
keep = false;
|
|
1148
1491
|
removeCount++;
|
|
@@ -1150,14 +1493,19 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1150
1493
|
// CRITICAL: Always update position, even when removing segment (consistency with L command)
|
|
1151
1494
|
currentX = endX;
|
|
1152
1495
|
// Reset previous control points (not a curve command)
|
|
1153
|
-
prevCp2X = null;
|
|
1154
|
-
|
|
1496
|
+
prevCp2X = null;
|
|
1497
|
+
prevCp2Y = null;
|
|
1498
|
+
prevCpX = null;
|
|
1499
|
+
prevCpY = null;
|
|
1155
1500
|
break;
|
|
1156
1501
|
}
|
|
1157
1502
|
|
|
1158
|
-
case
|
|
1159
|
-
if (args.length < 1)
|
|
1160
|
-
|
|
1503
|
+
case "V": {
|
|
1504
|
+
if (args.length < 1)
|
|
1505
|
+
throw new Error(
|
|
1506
|
+
`removeZeroLengthSegments: V command at index ${idx} requires 1 arg, got ${args.length}`,
|
|
1507
|
+
);
|
|
1508
|
+
const endY = command === "V" ? D(args[0]) : currentY.plus(D(args[0]));
|
|
1161
1509
|
if (endY.minus(currentY).abs().lessThan(tol)) {
|
|
1162
1510
|
keep = false;
|
|
1163
1511
|
removeCount++;
|
|
@@ -1165,25 +1513,42 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1165
1513
|
// CRITICAL: Always update position, even when removing segment (consistency with L command)
|
|
1166
1514
|
currentY = endY;
|
|
1167
1515
|
// Reset previous control points (not a curve command)
|
|
1168
|
-
prevCp2X = null;
|
|
1169
|
-
|
|
1516
|
+
prevCp2X = null;
|
|
1517
|
+
prevCp2Y = null;
|
|
1518
|
+
prevCpX = null;
|
|
1519
|
+
prevCpY = null;
|
|
1170
1520
|
break;
|
|
1171
1521
|
}
|
|
1172
1522
|
|
|
1173
|
-
case
|
|
1174
|
-
if (args.length < 6)
|
|
1175
|
-
|
|
1176
|
-
|
|
1523
|
+
case "C": {
|
|
1524
|
+
if (args.length < 6)
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
`removeZeroLengthSegments: C command at index ${idx} requires 6 args, got ${args.length}`,
|
|
1527
|
+
);
|
|
1528
|
+
const endX = command === "C" ? D(args[4]) : currentX.plus(D(args[4]));
|
|
1529
|
+
const endY = command === "C" ? D(args[5]) : currentY.plus(D(args[5]));
|
|
1177
1530
|
// For curves, also check if all control points are at the same location
|
|
1178
|
-
const cp1X = command ===
|
|
1179
|
-
const cp1Y = command ===
|
|
1180
|
-
const cp2X = command ===
|
|
1181
|
-
const cp2Y = command ===
|
|
1531
|
+
const cp1X = command === "C" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1532
|
+
const cp1Y = command === "C" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1533
|
+
const cp2X = command === "C" ? D(args[2]) : currentX.plus(D(args[2]));
|
|
1534
|
+
const cp2Y = command === "C" ? D(args[3]) : currentY.plus(D(args[3]));
|
|
1182
1535
|
|
|
1183
1536
|
const allSame =
|
|
1184
|
-
isZeroLengthSegment(
|
|
1185
|
-
|
|
1186
|
-
|
|
1537
|
+
isZeroLengthSegment(
|
|
1538
|
+
{ x: currentX, y: currentY },
|
|
1539
|
+
{ x: endX, y: endY },
|
|
1540
|
+
tol,
|
|
1541
|
+
) &&
|
|
1542
|
+
isZeroLengthSegment(
|
|
1543
|
+
{ x: currentX, y: currentY },
|
|
1544
|
+
{ x: cp1X, y: cp1Y },
|
|
1545
|
+
tol,
|
|
1546
|
+
) &&
|
|
1547
|
+
isZeroLengthSegment(
|
|
1548
|
+
{ x: currentX, y: currentY },
|
|
1549
|
+
{ x: cp2X, y: cp2Y },
|
|
1550
|
+
tol,
|
|
1551
|
+
);
|
|
1187
1552
|
|
|
1188
1553
|
if (allSame) {
|
|
1189
1554
|
keep = false;
|
|
@@ -1195,19 +1560,33 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1195
1560
|
prevCp2X = cp2X;
|
|
1196
1561
|
prevCp2Y = cp2Y;
|
|
1197
1562
|
// C doesn't affect quadratic control points
|
|
1198
|
-
prevCpX = null;
|
|
1563
|
+
prevCpX = null;
|
|
1564
|
+
prevCpY = null;
|
|
1199
1565
|
break;
|
|
1200
1566
|
}
|
|
1201
1567
|
|
|
1202
|
-
case
|
|
1568
|
+
case "Q": {
|
|
1203
1569
|
// Quadratic Bezier: x1 y1 x y (4 args)
|
|
1204
|
-
if (args.length < 4)
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1570
|
+
if (args.length < 4)
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
`removeZeroLengthSegments: Q command at index ${idx} requires 4 args, got ${args.length}`,
|
|
1573
|
+
);
|
|
1574
|
+
const endX = command === "Q" ? D(args[2]) : currentX.plus(D(args[2]));
|
|
1575
|
+
const endY = command === "Q" ? D(args[3]) : currentY.plus(D(args[3]));
|
|
1576
|
+
const cpX = command === "Q" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1577
|
+
const cpY = command === "Q" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1578
|
+
if (
|
|
1579
|
+
isZeroLengthSegment(
|
|
1580
|
+
{ x: currentX, y: currentY },
|
|
1581
|
+
{ x: endX, y: endY },
|
|
1582
|
+
tol,
|
|
1583
|
+
) &&
|
|
1584
|
+
isZeroLengthSegment(
|
|
1585
|
+
{ x: currentX, y: currentY },
|
|
1586
|
+
{ x: cpX, y: cpY },
|
|
1587
|
+
tol,
|
|
1588
|
+
)
|
|
1589
|
+
) {
|
|
1211
1590
|
keep = false;
|
|
1212
1591
|
removeCount++;
|
|
1213
1592
|
}
|
|
@@ -1217,18 +1596,22 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1217
1596
|
prevCpX = cpX;
|
|
1218
1597
|
prevCpY = cpY;
|
|
1219
1598
|
// Q doesn't affect cubic control points
|
|
1220
|
-
prevCp2X = null;
|
|
1599
|
+
prevCp2X = null;
|
|
1600
|
+
prevCp2Y = null;
|
|
1221
1601
|
break;
|
|
1222
1602
|
}
|
|
1223
1603
|
|
|
1224
|
-
case
|
|
1604
|
+
case "S": {
|
|
1225
1605
|
// Smooth cubic Bezier: x2 y2 x y (4 args)
|
|
1226
1606
|
// First control point is reflected from previous C/S, or current position if none
|
|
1227
|
-
if (args.length < 4)
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1607
|
+
if (args.length < 4)
|
|
1608
|
+
throw new Error(
|
|
1609
|
+
`removeZeroLengthSegments: S command at index ${idx} requires 4 args, got ${args.length}`,
|
|
1610
|
+
);
|
|
1611
|
+
const endX = command === "S" ? D(args[2]) : currentX.plus(D(args[2]));
|
|
1612
|
+
const endY = command === "S" ? D(args[3]) : currentY.plus(D(args[3]));
|
|
1613
|
+
const cp2X = command === "S" ? D(args[0]) : currentX.plus(D(args[0]));
|
|
1614
|
+
const cp2Y = command === "S" ? D(args[1]) : currentY.plus(D(args[1]));
|
|
1232
1615
|
// Calculate implicit first control point
|
|
1233
1616
|
let cp1X, cp1Y;
|
|
1234
1617
|
if (prevCp2X !== null && prevCp2Y !== null) {
|
|
@@ -1241,9 +1624,23 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1241
1624
|
cp1Y = currentY;
|
|
1242
1625
|
}
|
|
1243
1626
|
// Check if ALL points (start, implicit CP1, CP2, end) are at same location
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1627
|
+
if (
|
|
1628
|
+
isZeroLengthSegment(
|
|
1629
|
+
{ x: currentX, y: currentY },
|
|
1630
|
+
{ x: endX, y: endY },
|
|
1631
|
+
tol,
|
|
1632
|
+
) &&
|
|
1633
|
+
isZeroLengthSegment(
|
|
1634
|
+
{ x: currentX, y: currentY },
|
|
1635
|
+
{ x: cp1X, y: cp1Y },
|
|
1636
|
+
tol,
|
|
1637
|
+
) &&
|
|
1638
|
+
isZeroLengthSegment(
|
|
1639
|
+
{ x: currentX, y: currentY },
|
|
1640
|
+
{ x: cp2X, y: cp2Y },
|
|
1641
|
+
tol,
|
|
1642
|
+
)
|
|
1643
|
+
) {
|
|
1247
1644
|
keep = false;
|
|
1248
1645
|
removeCount++;
|
|
1249
1646
|
}
|
|
@@ -1253,15 +1650,25 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1253
1650
|
prevCp2X = cp2X;
|
|
1254
1651
|
prevCp2Y = cp2Y;
|
|
1255
1652
|
// S doesn't affect quadratic control points
|
|
1256
|
-
prevCpX = null;
|
|
1653
|
+
prevCpX = null;
|
|
1654
|
+
prevCpY = null;
|
|
1257
1655
|
break;
|
|
1258
1656
|
}
|
|
1259
1657
|
|
|
1260
|
-
case
|
|
1261
|
-
if (args.length < 7)
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1658
|
+
case "A": {
|
|
1659
|
+
if (args.length < 7)
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
`removeZeroLengthSegments: A command at index ${idx} requires 7 args, got ${args.length}`,
|
|
1662
|
+
);
|
|
1663
|
+
const endX = command === "A" ? D(args[5]) : currentX.plus(D(args[5]));
|
|
1664
|
+
const endY = command === "A" ? D(args[6]) : currentY.plus(D(args[6]));
|
|
1665
|
+
if (
|
|
1666
|
+
isZeroLengthSegment(
|
|
1667
|
+
{ x: currentX, y: currentY },
|
|
1668
|
+
{ x: endX, y: endY },
|
|
1669
|
+
tol,
|
|
1670
|
+
)
|
|
1671
|
+
) {
|
|
1265
1672
|
keep = false;
|
|
1266
1673
|
removeCount++;
|
|
1267
1674
|
}
|
|
@@ -1269,26 +1676,38 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1269
1676
|
currentX = endX;
|
|
1270
1677
|
currentY = endY;
|
|
1271
1678
|
// Reset previous control points (not a curve command)
|
|
1272
|
-
prevCp2X = null;
|
|
1273
|
-
|
|
1679
|
+
prevCp2X = null;
|
|
1680
|
+
prevCp2Y = null;
|
|
1681
|
+
prevCpX = null;
|
|
1682
|
+
prevCpY = null;
|
|
1274
1683
|
break;
|
|
1275
1684
|
}
|
|
1276
1685
|
|
|
1277
|
-
case
|
|
1686
|
+
case "Z":
|
|
1278
1687
|
// Z command goes back to start - check if already there
|
|
1279
|
-
if (
|
|
1688
|
+
if (
|
|
1689
|
+
isZeroLengthSegment(
|
|
1690
|
+
{ x: currentX, y: currentY },
|
|
1691
|
+
{ x: startX, y: startY },
|
|
1692
|
+
tol,
|
|
1693
|
+
)
|
|
1694
|
+
) {
|
|
1280
1695
|
// Still keep Z for path closure, but note it's zero-length
|
|
1281
1696
|
}
|
|
1282
1697
|
currentX = startX;
|
|
1283
1698
|
currentY = startY;
|
|
1284
1699
|
// Reset previous control points (new subpath after closure)
|
|
1285
|
-
prevCp2X = null;
|
|
1286
|
-
|
|
1700
|
+
prevCp2X = null;
|
|
1701
|
+
prevCp2Y = null;
|
|
1702
|
+
prevCpX = null;
|
|
1703
|
+
prevCpY = null;
|
|
1287
1704
|
break;
|
|
1288
1705
|
default:
|
|
1289
1706
|
// Unknown commands don't affect control point tracking
|
|
1290
|
-
prevCp2X = null;
|
|
1291
|
-
|
|
1707
|
+
prevCp2X = null;
|
|
1708
|
+
prevCp2Y = null;
|
|
1709
|
+
prevCpX = null;
|
|
1710
|
+
prevCpY = null;
|
|
1292
1711
|
break;
|
|
1293
1712
|
}
|
|
1294
1713
|
|
|
@@ -1300,7 +1719,7 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1300
1719
|
return {
|
|
1301
1720
|
pathData: result,
|
|
1302
1721
|
removeCount,
|
|
1303
|
-
verified: true
|
|
1722
|
+
verified: true,
|
|
1304
1723
|
};
|
|
1305
1724
|
}
|
|
1306
1725
|
|
|
@@ -1308,11 +1727,7 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1308
1727
|
// Exports
|
|
1309
1728
|
// ============================================================================
|
|
1310
1729
|
|
|
1311
|
-
export {
|
|
1312
|
-
EPSILON,
|
|
1313
|
-
DEFAULT_TOLERANCE,
|
|
1314
|
-
D
|
|
1315
|
-
};
|
|
1730
|
+
export { EPSILON, DEFAULT_TOLERANCE, D };
|
|
1316
1731
|
|
|
1317
1732
|
export default {
|
|
1318
1733
|
// Point utilities
|
|
@@ -1355,5 +1770,5 @@ export default {
|
|
|
1355
1770
|
|
|
1356
1771
|
// Constants
|
|
1357
1772
|
EPSILON,
|
|
1358
|
-
DEFAULT_TOLERANCE
|
|
1773
|
+
DEFAULT_TOLERANCE,
|
|
1359
1774
|
};
|