@emasoft/svg-matrix 1.0.18 → 1.0.20

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.
@@ -0,0 +1,1626 @@
1
+ /**
2
+ * @fileoverview Arbitrary-Precision Bezier Curve Analysis
3
+ *
4
+ * Superior implementation of differential geometry operations on Bezier curves
5
+ * using Decimal.js for 80-digit precision (configurable to 10^9 digits).
6
+ *
7
+ * This module provides mathematically exact computations where possible,
8
+ * and controlled-precision numerical methods where necessary.
9
+ *
10
+ * @module bezier-analysis
11
+ * @version 1.0.0
12
+ *
13
+ * Advantages over svgpathtools (float64):
14
+ * - 10^65x better precision (80 vs 15 digits)
15
+ * - Exact polynomial arithmetic
16
+ * - Verified mathematical correctness
17
+ * - No accumulating round-off errors
18
+ * - Handles extreme coordinate ranges
19
+ */
20
+
21
+ import Decimal from 'decimal.js';
22
+
23
+ // Ensure high precision is set
24
+ Decimal.set({ precision: 80 });
25
+
26
+ /**
27
+ * Convert any numeric input to Decimal
28
+ * @param {number|string|Decimal} x - Value to convert
29
+ * @returns {Decimal}
30
+ */
31
+ const D = x => (x instanceof Decimal ? x : new Decimal(x));
32
+
33
+ /**
34
+ * Validate that a value is a finite number (not NaN or Infinity).
35
+ * WHY: Prevents invalid calculations from propagating through the system.
36
+ * Non-finite values indicate numerical errors that should be caught early.
37
+ * @param {Decimal} val - Value to check
38
+ * @param {string} context - Function name for error message
39
+ * @throws {Error} If value is not finite
40
+ */
41
+ function assertFinite(val, context) {
42
+ if (!val.isFinite()) {
43
+ throw new Error(`${context}: encountered non-finite value ${val}`);
44
+ }
45
+ }
46
+
47
+ // ============================================================================
48
+ // NUMERICAL CONSTANTS (documented magic numbers)
49
+ // ============================================================================
50
+ // WHY: Magic numbers scattered throughout code make maintenance difficult.
51
+ // Named constants improve readability and allow easy adjustment of precision thresholds.
52
+
53
+ /** Threshold below which derivative magnitude is considered zero (cusp detection).
54
+ * WHY: Prevents division by zero in tangent/normal calculations at cusps. */
55
+ const DERIVATIVE_ZERO_THRESHOLD = new Decimal('1e-50');
56
+
57
+ /** Threshold for curvature denominator to detect cusps.
58
+ * WHY: Curvature formula has (x'^2 + y'^2)^(3/2) in denominator; this threshold
59
+ * prevents division by near-zero values that would produce spurious infinities. */
60
+ const CURVATURE_SINGULARITY_THRESHOLD = new Decimal('1e-100');
61
+
62
+ /** Threshold for finite difference step size.
63
+ * WHY: Used in numerical derivative approximations. Balance between truncation error
64
+ * (too large) and cancellation error (too small). */
65
+ const FINITE_DIFFERENCE_STEP = new Decimal('1e-8');
66
+
67
+ /** Newton-Raphson convergence threshold.
68
+ * WHY: Iteration stops when change is below this threshold, indicating convergence. */
69
+ const NEWTON_CONVERGENCE_THRESHOLD = new Decimal('1e-40');
70
+
71
+ /** Near-zero threshold for general comparisons.
72
+ * WHY: Used throughout for detecting effectively zero values in high-precision arithmetic. */
73
+ const NEAR_ZERO_THRESHOLD = new Decimal('1e-60');
74
+
75
+ /** Threshold for degenerate quadratic equations.
76
+ * WHY: When 'a' coefficient is below this relative to other coefficients,
77
+ * equation degenerates to linear case, avoiding division by near-zero. */
78
+ const QUADRATIC_DEGENERATE_THRESHOLD = new Decimal('1e-70');
79
+
80
+ /** Subdivision convergence threshold for root finding.
81
+ * WHY: When interval becomes smaller than this, subdivision has converged to a root. */
82
+ const SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal('1e-15');
83
+
84
+ /** Threshold for arc length comparison in curvature verification.
85
+ * WHY: Arc lengths below this are too small for reliable finite difference approximation. */
86
+ const ARC_LENGTH_THRESHOLD = new Decimal('1e-50');
87
+
88
+ /** Relative error threshold for curvature comparison.
89
+ * WHY: Curvature verification uses relative error; this threshold balances precision vs noise. */
90
+ const CURVATURE_RELATIVE_ERROR_THRESHOLD = new Decimal('1e-10');
91
+
92
+ /** Finite difference step for derivative verification (higher order).
93
+ * WHY: Smaller step than general finite difference for more accurate verification. */
94
+ const DERIVATIVE_VERIFICATION_STEP = new Decimal('1e-10');
95
+
96
+ /** Threshold for magnitude comparison in derivative verification.
97
+ * WHY: Used to determine if derivative magnitude is large enough for relative error. */
98
+ const DERIVATIVE_MAGNITUDE_THRESHOLD = new Decimal('1e-20');
99
+
100
+ /**
101
+ * 2D Point represented as [Decimal, Decimal]
102
+ * @typedef {[Decimal, Decimal]} Point2D
103
+ */
104
+
105
+ /**
106
+ * Bezier control points array
107
+ * @typedef {Point2D[]} BezierPoints
108
+ */
109
+
110
+ // ============================================================================
111
+ // BEZIER CURVE EVALUATION
112
+ // ============================================================================
113
+
114
+ /**
115
+ * Evaluate a Bezier curve at parameter t using de Casteljau's algorithm.
116
+ *
117
+ * de Casteljau is numerically stable and works for any degree.
118
+ * Complexity: O(n^2) where n is the number of control points.
119
+ *
120
+ * @param {BezierPoints} points - Control points [[x0,y0], [x1,y1], ...]
121
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
122
+ * @returns {Point2D} Point on curve at parameter t
123
+ *
124
+ * @example
125
+ * // Cubic Bezier
126
+ * const p = [[0,0], [100,200], [200,200], [300,0]];
127
+ * const [x, y] = bezierPoint(p, 0.5);
128
+ */
129
+ export function bezierPoint(points, t) {
130
+ // INPUT VALIDATION: Ensure points array is valid
131
+ // WHY: Empty or invalid arrays would cause crashes in the de Casteljau iteration
132
+ if (!points || !Array.isArray(points) || points.length < 2) {
133
+ throw new Error('bezierPoint: points must be an array with at least 2 control points');
134
+ }
135
+
136
+ const tD = D(t);
137
+ // PARAMETER VALIDATION: Warn if t is outside [0,1] but still compute
138
+ // WHY: Values slightly outside [0,1] may occur in numerical algorithms
139
+ // and should still produce valid extrapolations, but large deviations
140
+ // indicate bugs in calling code
141
+ if (tD.lt(-0.01) || tD.gt(1.01)) {
142
+ console.warn(`bezierPoint: t=${tD} is significantly outside [0,1]`);
143
+ }
144
+
145
+ const oneMinusT = D(1).minus(tD);
146
+
147
+ // Convert all points to Decimal
148
+ let pts = points.map(([x, y]) => [D(x), D(y)]);
149
+
150
+ // de Casteljau's algorithm: recursively interpolate
151
+ while (pts.length > 1) {
152
+ const newPts = [];
153
+ for (let i = 0; i < pts.length - 1; i++) {
154
+ const x = pts[i][0].times(oneMinusT).plus(pts[i + 1][0].times(tD));
155
+ const y = pts[i][1].times(oneMinusT).plus(pts[i + 1][1].times(tD));
156
+ newPts.push([x, y]);
157
+ }
158
+ pts = newPts;
159
+ }
160
+
161
+ return pts[0];
162
+ }
163
+
164
+ /**
165
+ * Evaluate Bezier using Horner's rule (optimized for cubics).
166
+ *
167
+ * For cubic: B(t) = P0 + t(c1 + t(c2 + t*c3))
168
+ * where c1, c2, c3 are derived from control points.
169
+ *
170
+ * This is faster than de Casteljau but equivalent.
171
+ *
172
+ * @param {BezierPoints} points - Control points (2-4 points)
173
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
174
+ * @returns {Point2D} Point on curve
175
+ */
176
+ export function bezierPointHorner(points, t) {
177
+ // INPUT VALIDATION: Ensure points array is valid
178
+ // WHY: Horner's rule requires at least 2 points; invalid arrays cause index errors
179
+ if (!points || !Array.isArray(points) || points.length < 2) {
180
+ throw new Error('bezierPointHorner: points must be an array with at least 2 control points');
181
+ }
182
+
183
+ const tD = D(t);
184
+ const n = points.length - 1; // Degree
185
+
186
+ if (n === 1) {
187
+ // Line: P0 + t(P1 - P0)
188
+ const [x0, y0] = [D(points[0][0]), D(points[0][1])];
189
+ const [x1, y1] = [D(points[1][0]), D(points[1][1])];
190
+ return [
191
+ x0.plus(tD.times(x1.minus(x0))),
192
+ y0.plus(tD.times(y1.minus(y0)))
193
+ ];
194
+ }
195
+
196
+ if (n === 2) {
197
+ // Quadratic: P0 + t(2(P1-P0) + t(P0 - 2P1 + P2))
198
+ const [x0, y0] = [D(points[0][0]), D(points[0][1])];
199
+ const [x1, y1] = [D(points[1][0]), D(points[1][1])];
200
+ const [x2, y2] = [D(points[2][0]), D(points[2][1])];
201
+
202
+ const c1x = x1.minus(x0).times(2);
203
+ const c1y = y1.minus(y0).times(2);
204
+ const c2x = x0.minus(x1.times(2)).plus(x2);
205
+ const c2y = y0.minus(y1.times(2)).plus(y2);
206
+
207
+ return [
208
+ x0.plus(tD.times(c1x.plus(tD.times(c2x)))),
209
+ y0.plus(tD.times(c1y.plus(tD.times(c2y))))
210
+ ];
211
+ }
212
+
213
+ if (n === 3) {
214
+ // Cubic: P0 + t(c1 + t(c2 + t*c3))
215
+ const [x0, y0] = [D(points[0][0]), D(points[0][1])];
216
+ const [x1, y1] = [D(points[1][0]), D(points[1][1])];
217
+ const [x2, y2] = [D(points[2][0]), D(points[2][1])];
218
+ const [x3, y3] = [D(points[3][0]), D(points[3][1])];
219
+
220
+ // c1 = 3(P1 - P0)
221
+ const c1x = x1.minus(x0).times(3);
222
+ const c1y = y1.minus(y0).times(3);
223
+
224
+ // c2 = 3(P0 - 2P1 + P2)
225
+ const c2x = x0.minus(x1.times(2)).plus(x2).times(3);
226
+ const c2y = y0.minus(y1.times(2)).plus(y2).times(3);
227
+
228
+ // c3 = -P0 + 3P1 - 3P2 + P3
229
+ const c3x = x0.neg().plus(x1.times(3)).minus(x2.times(3)).plus(x3);
230
+ const c3y = y0.neg().plus(y1.times(3)).minus(y2.times(3)).plus(y3);
231
+
232
+ return [
233
+ x0.plus(tD.times(c1x.plus(tD.times(c2x.plus(tD.times(c3x)))))),
234
+ y0.plus(tD.times(c1y.plus(tD.times(c2y.plus(tD.times(c3y))))))
235
+ ];
236
+ }
237
+
238
+ // For higher degrees, fall back to de Casteljau
239
+ return bezierPoint(points, t);
240
+ }
241
+
242
+ // ============================================================================
243
+ // DERIVATIVES
244
+ // ============================================================================
245
+
246
+ /**
247
+ * Compute the nth derivative of a Bezier curve at parameter t.
248
+ *
249
+ * The derivative of a degree-n Bezier is a degree-(n-1) Bezier
250
+ * with control points: n * (P[i+1] - P[i])
251
+ *
252
+ * @param {BezierPoints} points - Control points
253
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
254
+ * @param {number} [n=1] - Derivative order (1 = first derivative, 2 = second, etc.)
255
+ * @returns {Point2D} Derivative vector at t
256
+ *
257
+ * @example
258
+ * const velocity = bezierDerivative(cubicPoints, 0.5, 1); // First derivative
259
+ * const acceleration = bezierDerivative(cubicPoints, 0.5, 2); // Second derivative
260
+ */
261
+ export function bezierDerivative(points, t, n = 1) {
262
+ // INPUT VALIDATION: Ensure points array is valid
263
+ // WHY: Derivative computation requires iterating over control points
264
+ if (!points || !Array.isArray(points) || points.length < 2) {
265
+ throw new Error('bezierDerivative: points must be an array with at least 2 control points');
266
+ }
267
+
268
+ if (n === 0) {
269
+ return bezierPoint(points, t);
270
+ }
271
+
272
+ const degree = points.length - 1;
273
+
274
+ if (n > degree) {
275
+ // Derivative of order > degree is zero
276
+ return [D(0), D(0)];
277
+ }
278
+
279
+ // Compute derivative control points
280
+ let derivPoints = points.map(([x, y]) => [D(x), D(y)]);
281
+
282
+ for (let d = 0; d < n; d++) {
283
+ const currentDegree = derivPoints.length - 1;
284
+ const newPoints = [];
285
+
286
+ for (let i = 0; i < currentDegree; i++) {
287
+ const dx = derivPoints[i + 1][0].minus(derivPoints[i][0]).times(currentDegree);
288
+ const dy = derivPoints[i + 1][1].minus(derivPoints[i][1]).times(currentDegree);
289
+ newPoints.push([dx, dy]);
290
+ }
291
+
292
+ derivPoints = newPoints;
293
+ }
294
+
295
+ // Evaluate the derivative Bezier at t
296
+ if (derivPoints.length === 1) {
297
+ return derivPoints[0];
298
+ }
299
+
300
+ return bezierPoint(derivPoints, t);
301
+ }
302
+
303
+ /**
304
+ * Get the derivative control points (hodograph) of a Bezier curve.
305
+ *
306
+ * Useful for repeated derivative evaluations at different t values.
307
+ *
308
+ * @param {BezierPoints} points - Original control points
309
+ * @returns {BezierPoints} Derivative control points (one fewer point)
310
+ */
311
+ export function bezierDerivativePoints(points) {
312
+ // INPUT VALIDATION: Ensure points array is valid
313
+ // WHY: Need at least 2 points to compute derivative control points
314
+ if (!points || !Array.isArray(points) || points.length < 2) {
315
+ throw new Error('bezierDerivativePoints: points must be an array with at least 2 control points');
316
+ }
317
+
318
+ const n = points.length - 1;
319
+ const result = [];
320
+
321
+ for (let i = 0; i < n; i++) {
322
+ const dx = D(points[i + 1][0]).minus(D(points[i][0])).times(n);
323
+ const dy = D(points[i + 1][1]).minus(D(points[i][1])).times(n);
324
+ result.push([dx, dy]);
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ // ============================================================================
331
+ // TANGENT AND NORMAL VECTORS
332
+ // ============================================================================
333
+
334
+ /**
335
+ * Compute the unit tangent vector at parameter t.
336
+ *
337
+ * The tangent is the normalized first derivative.
338
+ * Handles the edge case where derivative is zero (cusps) by using
339
+ * higher-order derivatives or returning [1, 0] as fallback.
340
+ *
341
+ * @param {BezierPoints} points - Control points
342
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
343
+ * @returns {Point2D} Unit tangent vector
344
+ */
345
+ export function bezierTangent(points, t) {
346
+ // INPUT VALIDATION: Ensure points array is valid
347
+ // WHY: Tangent calculation requires derivative computation which needs valid points
348
+ if (!points || !Array.isArray(points) || points.length < 2) {
349
+ throw new Error('bezierTangent: points must be an array with at least 2 control points');
350
+ }
351
+
352
+ const [dx, dy] = bezierDerivative(points, t, 1);
353
+
354
+ // Compute magnitude
355
+ const mag = dx.times(dx).plus(dy.times(dy)).sqrt();
356
+
357
+ // Handle zero derivative (cusp or degenerate case)
358
+ // WHY: Use named constant for clarity and consistency across codebase
359
+ if (mag.isZero() || mag.lt(DERIVATIVE_ZERO_THRESHOLD)) {
360
+ // Try second derivative
361
+ const [d2x, d2y] = bezierDerivative(points, t, 2);
362
+ const mag2 = d2x.times(d2x).plus(d2y.times(d2y)).sqrt();
363
+
364
+ if (mag2.isZero() || mag2.lt(DERIVATIVE_ZERO_THRESHOLD)) {
365
+ // Fallback to direction from start to end
366
+ const [x0, y0] = [D(points[0][0]), D(points[0][1])];
367
+ const [xn, yn] = [D(points[points.length - 1][0]), D(points[points.length - 1][1])];
368
+ const ddx = xn.minus(x0);
369
+ const ddy = yn.minus(y0);
370
+ const magFallback = ddx.times(ddx).plus(ddy.times(ddy)).sqrt();
371
+
372
+ if (magFallback.isZero()) {
373
+ return [D(1), D(0)]; // Degenerate curve, return arbitrary direction
374
+ }
375
+ return [ddx.div(magFallback), ddy.div(magFallback)];
376
+ }
377
+
378
+ return [d2x.div(mag2), d2y.div(mag2)];
379
+ }
380
+
381
+ return [dx.div(mag), dy.div(mag)];
382
+ }
383
+
384
+ /**
385
+ * Compute the unit normal vector at parameter t.
386
+ *
387
+ * The normal is perpendicular to the tangent, using the right-hand rule:
388
+ * normal = rotate tangent by 90 degrees counter-clockwise.
389
+ *
390
+ * @param {BezierPoints} points - Control points
391
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
392
+ * @returns {Point2D} Unit normal vector
393
+ */
394
+ export function bezierNormal(points, t) {
395
+ // INPUT VALIDATION: Ensure points array is valid
396
+ // WHY: Normal is computed from tangent which requires valid points
397
+ if (!points || !Array.isArray(points) || points.length < 2) {
398
+ throw new Error('bezierNormal: points must be an array with at least 2 control points');
399
+ }
400
+
401
+ const [tx, ty] = bezierTangent(points, t);
402
+
403
+ // Rotate 90 degrees counter-clockwise: (x, y) -> (-y, x)
404
+ return [ty.neg(), tx];
405
+ }
406
+
407
+ // ============================================================================
408
+ // CURVATURE
409
+ // ============================================================================
410
+
411
+ /**
412
+ * Compute the curvature at parameter t.
413
+ *
414
+ * Curvature formula: k = (x'y'' - y'x'') / (x'^2 + y'^2)^(3/2)
415
+ *
416
+ * Positive curvature = curve bends left (counter-clockwise)
417
+ * Negative curvature = curve bends right (clockwise)
418
+ * Zero curvature = straight line
419
+ *
420
+ * @param {BezierPoints} points - Control points
421
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
422
+ * @returns {Decimal} Signed curvature
423
+ */
424
+ export function bezierCurvature(points, t) {
425
+ // INPUT VALIDATION: Ensure points array is valid
426
+ // WHY: Curvature requires first and second derivatives which need valid points
427
+ if (!points || !Array.isArray(points) || points.length < 2) {
428
+ throw new Error('bezierCurvature: points must be an array with at least 2 control points');
429
+ }
430
+
431
+ const [dx, dy] = bezierDerivative(points, t, 1);
432
+ const [d2x, d2y] = bezierDerivative(points, t, 2);
433
+
434
+ // Numerator: x'y'' - y'x''
435
+ const numerator = dx.times(d2y).minus(dy.times(d2x));
436
+
437
+ // Denominator: (x'^2 + y'^2)^(3/2)
438
+ const speedSquared = dx.times(dx).plus(dy.times(dy));
439
+
440
+ // WHY: Use named constant for curvature singularity detection
441
+ if (speedSquared.isZero() || speedSquared.lt(CURVATURE_SINGULARITY_THRESHOLD)) {
442
+ // At a cusp, curvature is undefined (infinity)
443
+ return new Decimal(Infinity);
444
+ }
445
+
446
+ const denominator = speedSquared.sqrt().pow(3);
447
+
448
+ return numerator.div(denominator);
449
+ }
450
+
451
+ /**
452
+ * Compute the radius of curvature at parameter t.
453
+ *
454
+ * Radius = 1 / |curvature|
455
+ *
456
+ * @param {BezierPoints} points - Control points
457
+ * @param {number|string|Decimal} t - Parameter in [0, 1]
458
+ * @returns {Decimal} Radius of curvature (positive, or Infinity for straight segments)
459
+ */
460
+ export function bezierRadiusOfCurvature(points, t) {
461
+ // INPUT VALIDATION: Ensure points array is valid
462
+ // WHY: Radius computation requires curvature which needs valid points
463
+ if (!points || !Array.isArray(points) || points.length < 2) {
464
+ throw new Error('bezierRadiusOfCurvature: points must be an array with at least 2 control points');
465
+ }
466
+
467
+ const k = bezierCurvature(points, t);
468
+
469
+ if (k.isZero()) {
470
+ return new Decimal(Infinity);
471
+ }
472
+
473
+ return D(1).div(k.abs());
474
+ }
475
+
476
+ // ============================================================================
477
+ // DE CASTELJAU SPLITTING
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Split a Bezier curve at parameter t using de Casteljau's algorithm.
482
+ *
483
+ * Returns two Bezier curves that together form the original curve.
484
+ * This is mathematically exact (no approximation).
485
+ *
486
+ * @param {BezierPoints} points - Control points
487
+ * @param {number|string|Decimal} t - Split parameter in [0, 1]
488
+ * @returns {{left: BezierPoints, right: BezierPoints}} Two Bezier curves
489
+ *
490
+ * @example
491
+ * const { left, right } = bezierSplit(cubicPoints, 0.5);
492
+ * // left covers t in [0, 0.5], right covers t in [0.5, 1]
493
+ */
494
+ export function bezierSplit(points, t) {
495
+ // INPUT VALIDATION: Ensure points array is valid
496
+ // WHY: de Casteljau algorithm requires iterating over control points
497
+ if (!points || !Array.isArray(points) || points.length < 2) {
498
+ throw new Error('bezierSplit: points must be an array with at least 2 control points');
499
+ }
500
+
501
+ const tD = D(t);
502
+ // PARAMETER VALIDATION: Warn if t is outside [0,1] but still compute
503
+ // WHY: Values slightly outside [0,1] may occur in numerical algorithms
504
+ // and should still produce valid extrapolations, but large deviations
505
+ // indicate bugs in calling code
506
+ if (tD.lt(-0.01) || tD.gt(1.01)) {
507
+ console.warn(`bezierSplit: t=${tD} is significantly outside [0,1]`);
508
+ }
509
+
510
+ const oneMinusT = D(1).minus(tD);
511
+
512
+ // Convert to Decimal
513
+ let pts = points.map(([x, y]) => [D(x), D(y)]);
514
+
515
+ const left = [pts[0]]; // First point of left curve
516
+ const right = [];
517
+
518
+ // de Casteljau iterations, saving the edges
519
+ while (pts.length > 1) {
520
+ right.unshift(pts[pts.length - 1]); // Last point goes to right curve
521
+
522
+ const newPts = [];
523
+ for (let i = 0; i < pts.length - 1; i++) {
524
+ const x = pts[i][0].times(oneMinusT).plus(pts[i + 1][0].times(tD));
525
+ const y = pts[i][1].times(oneMinusT).plus(pts[i + 1][1].times(tD));
526
+ newPts.push([x, y]);
527
+ }
528
+
529
+ if (newPts.length > 0) {
530
+ left.push(newPts[0]); // First point of each level goes to left
531
+ }
532
+
533
+ pts = newPts;
534
+ }
535
+
536
+ // The final point is shared by both curves
537
+ if (pts.length === 1) {
538
+ right.unshift(pts[0]);
539
+ }
540
+
541
+ return { left, right };
542
+ }
543
+
544
+ /**
545
+ * Split a Bezier curve at t = 0.5 (optimized).
546
+ *
547
+ * This is a common operation and can be slightly optimized.
548
+ *
549
+ * @param {BezierPoints} points - Control points
550
+ * @returns {{left: BezierPoints, right: BezierPoints}} Two Bezier curves
551
+ */
552
+ export function bezierHalve(points) {
553
+ // INPUT VALIDATION: Ensure points array is valid
554
+ // WHY: bezierHalve delegates to bezierSplit which needs valid points
555
+ if (!points || !Array.isArray(points) || points.length < 2) {
556
+ throw new Error('bezierHalve: points must be an array with at least 2 control points');
557
+ }
558
+
559
+ return bezierSplit(points, 0.5);
560
+ }
561
+
562
+ /**
563
+ * Extract a portion of a Bezier curve between t0 and t1.
564
+ *
565
+ * Uses two splits: first at t0, then adjust and split at (t1-t0)/(1-t0).
566
+ *
567
+ * @param {BezierPoints} points - Control points
568
+ * @param {number|string|Decimal} t0 - Start parameter
569
+ * @param {number|string|Decimal} t1 - End parameter
570
+ * @returns {BezierPoints} Control points for the cropped curve
571
+ */
572
+ export function bezierCrop(points, t0, t1) {
573
+ // INPUT VALIDATION: Ensure points array is valid
574
+ // WHY: bezierCrop uses bezierSplit which requires valid points
575
+ if (!points || !Array.isArray(points) || points.length < 2) {
576
+ throw new Error('bezierCrop: points must be an array with at least 2 control points');
577
+ }
578
+
579
+ const t0D = D(t0);
580
+ const t1D = D(t1);
581
+
582
+ if (t0D.gte(t1D)) {
583
+ throw new Error('bezierCrop: t0 must be less than t1');
584
+ }
585
+
586
+ // PARAMETER BOUNDS: Ensure t0 and t1 are within valid range [0, 1]
587
+ // WHY: Parameters outside [0,1] don't correspond to points on the curve segment
588
+ if (t0D.lt(0) || t0D.gt(1)) {
589
+ throw new Error('bezierCrop: t0 must be in range [0, 1]');
590
+ }
591
+ if (t1D.lt(0) || t1D.gt(1)) {
592
+ throw new Error('bezierCrop: t1 must be in range [0, 1]');
593
+ }
594
+
595
+ // First split at t0, take the right portion
596
+ const { right: afterT0 } = bezierSplit(points, t0);
597
+
598
+ // Adjust t1 to the new parameter space: (t1 - t0) / (1 - t0)
599
+ const adjustedT1 = t1D.minus(t0D).div(D(1).minus(t0D));
600
+
601
+ // Split at adjusted t1, take the left portion
602
+ const { left: cropped } = bezierSplit(afterT0, adjustedT1);
603
+
604
+ return cropped;
605
+ }
606
+
607
+ // ============================================================================
608
+ // BOUNDING BOX
609
+ // ============================================================================
610
+
611
+ /**
612
+ * Compute the axis-aligned bounding box of a Bezier curve.
613
+ *
614
+ * Finds exact bounds by:
615
+ * 1. Computing derivative polynomial
616
+ * 2. Finding roots (critical points where derivative = 0)
617
+ * 3. Evaluating curve at t=0, t=1, and all critical points
618
+ * 4. Taking min/max of all evaluated points
619
+ *
620
+ * @param {BezierPoints} points - Control points
621
+ * @returns {{xmin: Decimal, xmax: Decimal, ymin: Decimal, ymax: Decimal}}
622
+ */
623
+ export function bezierBoundingBox(points) {
624
+ // INPUT VALIDATION: Ensure points array is valid
625
+ // WHY: Bounding box computation requires accessing control points and computing derivatives
626
+ if (!points || !Array.isArray(points) || points.length < 2) {
627
+ throw new Error('bezierBoundingBox: points must be an array with at least 2 control points');
628
+ }
629
+
630
+ const n = points.length;
631
+
632
+ // Start with endpoints
633
+ const [x0, y0] = [D(points[0][0]), D(points[0][1])];
634
+ const [xn, yn] = [D(points[n - 1][0]), D(points[n - 1][1])];
635
+
636
+ let xmin = Decimal.min(x0, xn);
637
+ let xmax = Decimal.max(x0, xn);
638
+ let ymin = Decimal.min(y0, yn);
639
+ let ymax = Decimal.max(y0, yn);
640
+
641
+ if (n <= 2) {
642
+ // Line segment: endpoints are sufficient
643
+ return { xmin, xmax, ymin, ymax };
644
+ }
645
+
646
+ // Get derivative control points
647
+ const derivPts = bezierDerivativePoints(points);
648
+
649
+ // Find critical points (where derivative = 0) for x and y separately
650
+ const criticalTs = findBezierRoots1D(derivPts, 'x')
651
+ .concat(findBezierRoots1D(derivPts, 'y'))
652
+ .filter(t => t.gt(0) && t.lt(1));
653
+
654
+ // Evaluate at critical points
655
+ for (const t of criticalTs) {
656
+ const [x, y] = bezierPoint(points, t);
657
+ xmin = Decimal.min(xmin, x);
658
+ xmax = Decimal.max(xmax, x);
659
+ ymin = Decimal.min(ymin, y);
660
+ ymax = Decimal.max(ymax, y);
661
+ }
662
+
663
+ return { xmin, xmax, ymin, ymax };
664
+ }
665
+
666
+ /**
667
+ * Find roots of a 1D Bezier curve (where either x or y component = 0).
668
+ *
669
+ * Uses subdivision method for robustness.
670
+ *
671
+ * @param {BezierPoints} points - Control points of derivative
672
+ * @param {'x'|'y'} component - Which component to find roots for
673
+ * @returns {Decimal[]} Array of t values where component = 0
674
+ */
675
+ function findBezierRoots1D(points, component) {
676
+ // INPUT VALIDATION
677
+ if (!points || !Array.isArray(points) || points.length === 0) {
678
+ return []; // No roots possible for empty input
679
+ }
680
+
681
+ const idx = component === 'x' ? 0 : 1;
682
+ const roots = [];
683
+
684
+ // Extract 1D control points
685
+ const coeffs = points.map(p => D(p[idx]));
686
+
687
+ // For quadratic (2 points) and cubic (3 points), use analytical solutions
688
+ if (coeffs.length === 2) {
689
+ // Linear: a + t(b - a) = 0 => t = -a / (b - a)
690
+ const a = coeffs[0];
691
+ const b = coeffs[1];
692
+ const denom = b.minus(a);
693
+
694
+ if (!denom.isZero()) {
695
+ const t = a.neg().div(denom);
696
+ if (t.gt(0) && t.lt(1)) {
697
+ roots.push(t);
698
+ }
699
+ }
700
+ } else if (coeffs.length === 3) {
701
+ // Quadratic derivative from cubic Bezier
702
+ // B(t) = (1-t)^2 * P0 + 2(1-t)t * P1 + t^2 * P2
703
+ // Expanding: a*t^2 + b*t + c where
704
+ // a = P0 - 2P1 + P2
705
+ // b = 2(P1 - P0)
706
+ // c = P0
707
+ const P0 = coeffs[0];
708
+ const P1 = coeffs[1];
709
+ const P2 = coeffs[2];
710
+
711
+ const a = P0.minus(P1.times(2)).plus(P2);
712
+ const b = P1.minus(P0).times(2);
713
+ const c = P0;
714
+
715
+ const quadRoots = solveQuadratic(a, b, c);
716
+ for (const t of quadRoots) {
717
+ if (t.gt(0) && t.lt(1)) {
718
+ roots.push(t);
719
+ }
720
+ }
721
+ } else {
722
+ // Higher degree: use subdivision
723
+ const subdivisionRoots = findRootsBySubdivision(coeffs, D(0), D(1), 50);
724
+ roots.push(...subdivisionRoots.filter(t => t.gt(0) && t.lt(1)));
725
+ }
726
+
727
+ return roots;
728
+ }
729
+
730
+ /**
731
+ * Solve quadratic equation ax^2 + bx + c = 0 with arbitrary precision.
732
+ * Uses numerically stable formula to avoid catastrophic cancellation.
733
+ *
734
+ * WHY: Standard quadratic formula can lose precision when b^2 >> 4ac due to
735
+ * subtracting nearly equal numbers. This implementation uses the numerically
736
+ * stable formula that avoids that cancellation.
737
+ *
738
+ * @param {Decimal} a - Quadratic coefficient
739
+ * @param {Decimal} b - Linear coefficient
740
+ * @param {Decimal} c - Constant
741
+ * @returns {Decimal[]} Real roots
742
+ */
743
+ function solveQuadratic(a, b, c) {
744
+ // NUMERICAL STABILITY: Use threshold relative to coefficient magnitudes
745
+ // to determine if 'a' is effectively zero (degenerate to linear equation)
746
+ // WHY: Absolute thresholds fail when coefficients are scaled; relative threshold adapts
747
+ const coeffMag = Decimal.max(a.abs(), b.abs(), c.abs());
748
+
749
+ if (coeffMag.gt(0) && a.abs().div(coeffMag).lt(QUADRATIC_DEGENERATE_THRESHOLD)) {
750
+ // Linear equation: bx + c = 0
751
+ if (b.isZero()) return [];
752
+ return [c.neg().div(b)];
753
+ }
754
+
755
+ if (a.isZero()) {
756
+ if (b.isZero()) return [];
757
+ return [c.neg().div(b)];
758
+ }
759
+
760
+ const discriminant = b.times(b).minus(a.times(c).times(4));
761
+
762
+ if (discriminant.lt(0)) {
763
+ return []; // No real roots
764
+ }
765
+
766
+ if (discriminant.isZero()) {
767
+ return [b.neg().div(a.times(2))];
768
+ }
769
+
770
+ const sqrtD = discriminant.sqrt();
771
+ const twoA = a.times(2);
772
+
773
+ // NUMERICAL STABILITY: Use Vieta's formula to compute the second root
774
+ // when catastrophic cancellation would occur in the standard formula.
775
+ // When b and sqrt(D) have similar magnitudes and the same sign,
776
+ // -b + sqrt(D) or -b - sqrt(D) can lose precision.
777
+ //
778
+ // Solution: Compute the larger root directly, use Vieta's for the other
779
+ // x1 * x2 = c/a, so x2 = (c/a) / x1
780
+
781
+ let root1, root2;
782
+ if (b.isNegative()) {
783
+ // -b is positive, so -b + sqrt(D) is well-conditioned
784
+ root1 = b.neg().plus(sqrtD).div(twoA);
785
+ // Use Vieta's formula: x1 * x2 = c/a
786
+ root2 = c.div(a).div(root1);
787
+ } else {
788
+ // -b is negative or zero, so -b - sqrt(D) is well-conditioned
789
+ root1 = b.neg().minus(sqrtD).div(twoA);
790
+ // Use Vieta's formula: x1 * x2 = c/a
791
+ root2 = c.div(a).div(root1);
792
+ }
793
+
794
+ return [root1, root2];
795
+ }
796
+
797
+ /**
798
+ * Find roots of a 1D Bezier using subdivision (for higher degrees).
799
+ *
800
+ * @param {Decimal[]} coeffs - 1D control values
801
+ * @param {Decimal} t0 - Start of interval
802
+ * @param {Decimal} t1 - End of interval
803
+ * @param {number} maxDepth - Maximum recursion depth
804
+ * @returns {Decimal[]} Roots in interval
805
+ */
806
+ function findRootsBySubdivision(coeffs, t0, t1, maxDepth) {
807
+ // Check if interval might contain a root (sign change in convex hull)
808
+ const signs = coeffs.map(c => c.isNegative() ? -1 : (c.isZero() ? 0 : 1));
809
+ const minSign = Math.min(...signs);
810
+ const maxSign = Math.max(...signs);
811
+
812
+ if (minSign > 0 || maxSign < 0) {
813
+ // All same sign, no root in this interval
814
+ return [];
815
+ }
816
+
817
+ // WHY: Use named constant for subdivision convergence check
818
+ if (maxDepth <= 0 || t1.minus(t0).lt(SUBDIVISION_CONVERGENCE_THRESHOLD)) {
819
+ // Converged, return midpoint
820
+ return [t0.plus(t1).div(2)];
821
+ }
822
+
823
+ // Subdivide at midpoint
824
+ const tMid = t0.plus(t1).div(2);
825
+
826
+ // Compute subdivided control points using de Casteljau
827
+ const { left, right } = subdivideBezier1D(coeffs);
828
+
829
+ const leftRoots = findRootsBySubdivision(left, t0, tMid, maxDepth - 1);
830
+ const rightRoots = findRootsBySubdivision(right, tMid, t1, maxDepth - 1);
831
+
832
+ return leftRoots.concat(rightRoots);
833
+ }
834
+
835
+ /**
836
+ * Subdivide 1D Bezier at t=0.5.
837
+ */
838
+ function subdivideBezier1D(coeffs) {
839
+ const half = D(0.5);
840
+ let pts = coeffs.map(c => D(c));
841
+
842
+ const left = [pts[0]];
843
+ const right = [];
844
+
845
+ while (pts.length > 1) {
846
+ right.unshift(pts[pts.length - 1]);
847
+ const newPts = [];
848
+ for (let i = 0; i < pts.length - 1; i++) {
849
+ newPts.push(pts[i].plus(pts[i + 1]).times(half));
850
+ }
851
+ if (newPts.length > 0) {
852
+ left.push(newPts[0]);
853
+ }
854
+ pts = newPts;
855
+ }
856
+
857
+ if (pts.length === 1) {
858
+ right.unshift(pts[0]);
859
+ }
860
+
861
+ return { left, right };
862
+ }
863
+
864
+ // ============================================================================
865
+ // POLYNOMIAL CONVERSION
866
+ // ============================================================================
867
+
868
+ /**
869
+ * Convert Bezier control points to polynomial coefficients.
870
+ *
871
+ * For cubic Bezier: B(t) = c0 + c1*t + c2*t^2 + c3*t^3
872
+ *
873
+ * @param {BezierPoints} points - Control points
874
+ * @returns {{x: Decimal[], y: Decimal[]}} Polynomial coefficients (constant first)
875
+ */
876
+ export function bezierToPolynomial(points) {
877
+ // INPUT VALIDATION: Ensure points array is valid
878
+ // WHY: Polynomial conversion requires accessing control points by index
879
+ if (!points || !Array.isArray(points) || points.length < 2) {
880
+ throw new Error('bezierToPolynomial: points must be an array with at least 2 control points');
881
+ }
882
+
883
+ const n = points.length - 1;
884
+ const xCoeffs = [];
885
+ const yCoeffs = [];
886
+
887
+ // Convert points to Decimal
888
+ const P = points.map(([x, y]) => [D(x), D(y)]);
889
+
890
+ if (n === 1) {
891
+ // Line: P0 + t(P1 - P0)
892
+ xCoeffs.push(P[0][0]);
893
+ xCoeffs.push(P[1][0].minus(P[0][0]));
894
+ yCoeffs.push(P[0][1]);
895
+ yCoeffs.push(P[1][1].minus(P[0][1]));
896
+ } else if (n === 2) {
897
+ // Quadratic: P0 + 2t(P1-P0) + t^2(P0 - 2P1 + P2)
898
+ xCoeffs.push(P[0][0]);
899
+ xCoeffs.push(P[1][0].minus(P[0][0]).times(2));
900
+ xCoeffs.push(P[0][0].minus(P[1][0].times(2)).plus(P[2][0]));
901
+
902
+ yCoeffs.push(P[0][1]);
903
+ yCoeffs.push(P[1][1].minus(P[0][1]).times(2));
904
+ yCoeffs.push(P[0][1].minus(P[1][1].times(2)).plus(P[2][1]));
905
+ } else if (n === 3) {
906
+ // Cubic
907
+ xCoeffs.push(P[0][0]);
908
+ xCoeffs.push(P[1][0].minus(P[0][0]).times(3));
909
+ xCoeffs.push(P[0][0].minus(P[1][0].times(2)).plus(P[2][0]).times(3));
910
+ xCoeffs.push(P[0][0].neg().plus(P[1][0].times(3)).minus(P[2][0].times(3)).plus(P[3][0]));
911
+
912
+ yCoeffs.push(P[0][1]);
913
+ yCoeffs.push(P[1][1].minus(P[0][1]).times(3));
914
+ yCoeffs.push(P[0][1].minus(P[1][1].times(2)).plus(P[2][1]).times(3));
915
+ yCoeffs.push(P[0][1].neg().plus(P[1][1].times(3)).minus(P[2][1].times(3)).plus(P[3][1]));
916
+ } else {
917
+ throw new Error(`Polynomial conversion for degree ${n} not implemented`);
918
+ }
919
+
920
+ return { x: xCoeffs, y: yCoeffs };
921
+ }
922
+
923
+ /**
924
+ * Convert polynomial coefficients back to Bezier control points.
925
+ *
926
+ * @param {Decimal[]} xCoeffs - X polynomial coefficients (constant first)
927
+ * @param {Decimal[]} yCoeffs - Y polynomial coefficients
928
+ * @returns {BezierPoints} Control points
929
+ */
930
+ export function polynomialToBezier(xCoeffs, yCoeffs) {
931
+ // INPUT VALIDATION
932
+ if (!xCoeffs || !Array.isArray(xCoeffs) || xCoeffs.length < 2) {
933
+ throw new Error('polynomialToBezier: xCoeffs must be an array with at least 2 coefficients');
934
+ }
935
+ if (!yCoeffs || !Array.isArray(yCoeffs) || yCoeffs.length < 2) {
936
+ throw new Error('polynomialToBezier: yCoeffs must be an array with at least 2 coefficients');
937
+ }
938
+ if (xCoeffs.length !== yCoeffs.length) {
939
+ throw new Error('polynomialToBezier: xCoeffs and yCoeffs must have the same length');
940
+ }
941
+
942
+ const n = xCoeffs.length - 1;
943
+
944
+ if (n === 1) {
945
+ return [
946
+ [xCoeffs[0], yCoeffs[0]],
947
+ [xCoeffs[0].plus(xCoeffs[1]), yCoeffs[0].plus(yCoeffs[1])]
948
+ ];
949
+ }
950
+
951
+ if (n === 2) {
952
+ const x0 = xCoeffs[0];
953
+ const x1 = xCoeffs[0].plus(xCoeffs[1].div(2));
954
+ const x2 = xCoeffs[0].plus(xCoeffs[1]).plus(xCoeffs[2]);
955
+
956
+ const y0 = yCoeffs[0];
957
+ const y1 = yCoeffs[0].plus(yCoeffs[1].div(2));
958
+ const y2 = yCoeffs[0].plus(yCoeffs[1]).plus(yCoeffs[2]);
959
+
960
+ return [[x0, y0], [x1, y1], [x2, y2]];
961
+ }
962
+
963
+ if (n === 3) {
964
+ const x0 = xCoeffs[0];
965
+ const x1 = xCoeffs[0].plus(xCoeffs[1].div(3));
966
+ const x2 = xCoeffs[0].plus(xCoeffs[1].times(2).div(3)).plus(xCoeffs[2].div(3));
967
+ const x3 = xCoeffs[0].plus(xCoeffs[1]).plus(xCoeffs[2]).plus(xCoeffs[3]);
968
+
969
+ const y0 = yCoeffs[0];
970
+ const y1 = yCoeffs[0].plus(yCoeffs[1].div(3));
971
+ const y2 = yCoeffs[0].plus(yCoeffs[1].times(2).div(3)).plus(yCoeffs[2].div(3));
972
+ const y3 = yCoeffs[0].plus(yCoeffs[1]).plus(yCoeffs[2]).plus(yCoeffs[3]);
973
+
974
+ return [[x0, y0], [x1, y1], [x2, y2], [x3, y3]];
975
+ }
976
+
977
+ throw new Error(`Bezier conversion for degree ${n} not implemented`);
978
+ }
979
+
980
+ // ============================================================================
981
+ // VERIFICATION UTILITIES (INVERSE OPERATIONS)
982
+ // ============================================================================
983
+
984
+ /**
985
+ * Verify bezierPoint by comparing de Casteljau with Horner evaluation.
986
+ * Mathematical verification: two different algorithms must produce same result.
987
+ *
988
+ * @param {BezierPoints} points - Control points
989
+ * @param {number|string|Decimal} t - Parameter
990
+ * @param {number|string|Decimal} [tolerance='1e-60'] - Maximum difference
991
+ * @returns {{valid: boolean, deCasteljau: Point2D, horner: Point2D, difference: Decimal}}
992
+ */
993
+ export function verifyBezierPoint(points, t, tolerance = '1e-60') {
994
+ // INPUT VALIDATION: Ensure points array is valid
995
+ // WHY: Verification functions need valid input to produce meaningful results
996
+ if (!points || !Array.isArray(points) || points.length < 2) {
997
+ throw new Error('verifyBezierPoint: points must be an array with at least 2 control points');
998
+ }
999
+
1000
+ const tol = D(tolerance);
1001
+ const deCasteljau = bezierPoint(points, t);
1002
+ const horner = bezierPointHorner(points, t);
1003
+
1004
+ const diffX = D(deCasteljau[0]).minus(D(horner[0])).abs();
1005
+ const diffY = D(deCasteljau[1]).minus(D(horner[1])).abs();
1006
+ const maxDiff = Decimal.max(diffX, diffY);
1007
+
1008
+ return {
1009
+ valid: maxDiff.lte(tol),
1010
+ deCasteljau,
1011
+ horner,
1012
+ difference: maxDiff
1013
+ };
1014
+ }
1015
+
1016
+ /**
1017
+ * Verify bezierSplit by checking:
1018
+ * 1. Left curve at t=1 equals split point
1019
+ * 2. Right curve at t=0 equals split point
1020
+ * 3. Evaluating original at t equals split point
1021
+ * 4. Left curve maps [0,1] to original [0, splitT]
1022
+ * 5. Right curve maps [0,1] to original [splitT, 1]
1023
+ *
1024
+ * @param {BezierPoints} points - Original control points
1025
+ * @param {number|string|Decimal} splitT - Split parameter
1026
+ * @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
1027
+ * @returns {{valid: boolean, errors: string[], splitPoint: Point2D, leftEnd: Point2D, rightStart: Point2D}}
1028
+ */
1029
+ export function verifyBezierSplit(points, splitT, tolerance = '1e-50') {
1030
+ // INPUT VALIDATION: Ensure points array and split parameter are valid
1031
+ // WHY: Split verification requires valid curve and parameter
1032
+ if (!points || !Array.isArray(points) || points.length < 2) {
1033
+ throw new Error('verifyBezierSplit: points must be an array with at least 2 control points');
1034
+ }
1035
+
1036
+ const tol = D(tolerance);
1037
+ const errors = [];
1038
+
1039
+ const { left, right } = bezierSplit(points, splitT);
1040
+ const splitPoint = bezierPoint(points, splitT);
1041
+
1042
+ // Check 1: Left curve ends at split point
1043
+ const leftEnd = bezierPoint(left, 1);
1044
+ const leftDiff = D(leftEnd[0]).minus(D(splitPoint[0])).abs()
1045
+ .plus(D(leftEnd[1]).minus(D(splitPoint[1])).abs());
1046
+ if (leftDiff.gt(tol)) {
1047
+ errors.push(`Left curve end differs from split point by ${leftDiff}`);
1048
+ }
1049
+
1050
+ // Check 2: Right curve starts at split point
1051
+ const rightStart = bezierPoint(right, 0);
1052
+ const rightDiff = D(rightStart[0]).minus(D(splitPoint[0])).abs()
1053
+ .plus(D(rightStart[1]).minus(D(splitPoint[1])).abs());
1054
+ if (rightDiff.gt(tol)) {
1055
+ errors.push(`Right curve start differs from split point by ${rightDiff}`);
1056
+ }
1057
+
1058
+ // Check 3: Sample points on both halves match original
1059
+ const tD = D(splitT);
1060
+ for (const testT of [0.25, 0.5, 0.75]) {
1061
+ // Test left half: t in [0, splitT] maps to leftT in [0, 1]
1062
+ const origT = D(testT).times(tD);
1063
+ const origPt = bezierPoint(points, origT);
1064
+ const leftPt = bezierPoint(left, testT);
1065
+ const leftTestDiff = D(origPt[0]).minus(D(leftPt[0])).abs()
1066
+ .plus(D(origPt[1]).minus(D(leftPt[1])).abs());
1067
+ if (leftTestDiff.gt(tol)) {
1068
+ errors.push(`Left half at t=${testT} differs by ${leftTestDiff}`);
1069
+ }
1070
+
1071
+ // Test right half: t in [splitT, 1] maps to rightT in [0, 1]
1072
+ const origT2 = tD.plus(D(testT).times(D(1).minus(tD)));
1073
+ const origPt2 = bezierPoint(points, origT2);
1074
+ const rightPt = bezierPoint(right, testT);
1075
+ const rightTestDiff = D(origPt2[0]).minus(D(rightPt[0])).abs()
1076
+ .plus(D(origPt2[1]).minus(D(rightPt[1])).abs());
1077
+ if (rightTestDiff.gt(tol)) {
1078
+ errors.push(`Right half at t=${testT} differs by ${rightTestDiff}`);
1079
+ }
1080
+ }
1081
+
1082
+ return {
1083
+ valid: errors.length === 0,
1084
+ errors,
1085
+ splitPoint,
1086
+ leftEnd,
1087
+ rightStart
1088
+ };
1089
+ }
1090
+
1091
+ /**
1092
+ * Verify bezierCrop by checking endpoints match expected positions.
1093
+ *
1094
+ * @param {BezierPoints} points - Original control points
1095
+ * @param {number|string|Decimal} t0 - Start parameter
1096
+ * @param {number|string|Decimal} t1 - End parameter
1097
+ * @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
1098
+ * @returns {{valid: boolean, errors: string[], expectedStart: Point2D, actualStart: Point2D, expectedEnd: Point2D, actualEnd: Point2D}}
1099
+ */
1100
+ export function verifyBezierCrop(points, t0, t1, tolerance = '1e-50') {
1101
+ // INPUT VALIDATION: Ensure points array and parameters are valid
1102
+ // WHY: Crop verification requires valid curve and parameter range
1103
+ if (!points || !Array.isArray(points) || points.length < 2) {
1104
+ throw new Error('verifyBezierCrop: points must be an array with at least 2 control points');
1105
+ }
1106
+
1107
+ const tol = D(tolerance);
1108
+ const errors = [];
1109
+
1110
+ const cropped = bezierCrop(points, t0, t1);
1111
+
1112
+ // Expected: cropped curve starts at original's t0 and ends at original's t1
1113
+ const expectedStart = bezierPoint(points, t0);
1114
+ const expectedEnd = bezierPoint(points, t1);
1115
+
1116
+ const actualStart = bezierPoint(cropped, 0);
1117
+ const actualEnd = bezierPoint(cropped, 1);
1118
+
1119
+ const startDiff = D(expectedStart[0]).minus(D(actualStart[0])).abs()
1120
+ .plus(D(expectedStart[1]).minus(D(actualStart[1])).abs());
1121
+ if (startDiff.gt(tol)) {
1122
+ errors.push(`Cropped start differs by ${startDiff}`);
1123
+ }
1124
+
1125
+ const endDiff = D(expectedEnd[0]).minus(D(actualEnd[0])).abs()
1126
+ .plus(D(expectedEnd[1]).minus(D(actualEnd[1])).abs());
1127
+ if (endDiff.gt(tol)) {
1128
+ errors.push(`Cropped end differs by ${endDiff}`);
1129
+ }
1130
+
1131
+ // Verify midpoint
1132
+ const midT = D(t0).plus(D(t1)).div(2);
1133
+ const expectedMid = bezierPoint(points, midT);
1134
+ const actualMid = bezierPoint(cropped, 0.5);
1135
+ const midDiff = D(expectedMid[0]).minus(D(actualMid[0])).abs()
1136
+ .plus(D(expectedMid[1]).minus(D(actualMid[1])).abs());
1137
+ if (midDiff.gt(tol)) {
1138
+ errors.push(`Cropped midpoint differs by ${midDiff}`);
1139
+ }
1140
+
1141
+ return {
1142
+ valid: errors.length === 0,
1143
+ errors,
1144
+ expectedStart,
1145
+ actualStart,
1146
+ expectedEnd,
1147
+ actualEnd
1148
+ };
1149
+ }
1150
+
1151
+ /**
1152
+ * Verify polynomial conversion by roundtrip: bezier -> polynomial -> bezier.
1153
+ *
1154
+ * @param {BezierPoints} points - Control points
1155
+ * @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
1156
+ * @returns {{valid: boolean, maxError: Decimal, originalPoints: BezierPoints, reconstructedPoints: BezierPoints}}
1157
+ */
1158
+ export function verifyPolynomialConversion(points, tolerance = '1e-50') {
1159
+ // INPUT VALIDATION: Ensure points array is valid
1160
+ // WHY: Polynomial conversion verification requires valid control points
1161
+ if (!points || !Array.isArray(points) || points.length < 2) {
1162
+ throw new Error('verifyPolynomialConversion: points must be an array with at least 2 control points');
1163
+ }
1164
+
1165
+ const tol = D(tolerance);
1166
+
1167
+ const { x: xCoeffs, y: yCoeffs } = bezierToPolynomial(points);
1168
+ const reconstructed = polynomialToBezier(xCoeffs, yCoeffs);
1169
+
1170
+ let maxError = D(0);
1171
+
1172
+ // Compare each control point
1173
+ for (let i = 0; i < points.length; i++) {
1174
+ const diffX = D(points[i][0]).minus(D(reconstructed[i][0])).abs();
1175
+ const diffY = D(points[i][1]).minus(D(reconstructed[i][1])).abs();
1176
+ maxError = Decimal.max(maxError, diffX, diffY);
1177
+ }
1178
+
1179
+ // Also verify by sampling the curves
1180
+ for (const t of [0, 0.25, 0.5, 0.75, 1]) {
1181
+ const orig = bezierPoint(points, t);
1182
+ const recon = bezierPoint(reconstructed, t);
1183
+ const diffX = D(orig[0]).minus(D(recon[0])).abs();
1184
+ const diffY = D(orig[1]).minus(D(recon[1])).abs();
1185
+ maxError = Decimal.max(maxError, diffX, diffY);
1186
+ }
1187
+
1188
+ return {
1189
+ valid: maxError.lte(tol),
1190
+ maxError,
1191
+ originalPoints: points,
1192
+ reconstructedPoints: reconstructed
1193
+ };
1194
+ }
1195
+
1196
+ /**
1197
+ * Verify tangent and normal vectors are correct:
1198
+ * 1. Tangent is a unit vector
1199
+ * 2. Normal is a unit vector
1200
+ * 3. Tangent and normal are perpendicular (dot product = 0)
1201
+ * 4. Tangent direction matches derivative direction
1202
+ *
1203
+ * @param {BezierPoints} points - Control points
1204
+ * @param {number|string|Decimal} t - Parameter
1205
+ * @param {number|string|Decimal} [tolerance='1e-50'] - Maximum error
1206
+ * @returns {{valid: boolean, errors: string[], tangent: Point2D, normal: Point2D, tangentMagnitude: Decimal, normalMagnitude: Decimal, dotProduct: Decimal}}
1207
+ */
1208
+ export function verifyTangentNormal(points, t, tolerance = '1e-50') {
1209
+ // INPUT VALIDATION: Ensure points array and parameter are valid
1210
+ // WHY: Tangent/normal verification requires valid curve and parameter
1211
+ if (!points || !Array.isArray(points) || points.length < 2) {
1212
+ throw new Error('verifyTangentNormal: points must be an array with at least 2 control points');
1213
+ }
1214
+
1215
+ const tol = D(tolerance);
1216
+ const errors = [];
1217
+
1218
+ const tangent = bezierTangent(points, t);
1219
+ const normal = bezierNormal(points, t);
1220
+ const deriv = bezierDerivative(points, t, 1);
1221
+
1222
+ const [tx, ty] = [D(tangent[0]), D(tangent[1])];
1223
+ const [nx, ny] = [D(normal[0]), D(normal[1])];
1224
+ const [dx, dy] = [D(deriv[0]), D(deriv[1])];
1225
+
1226
+ // Check tangent is unit vector
1227
+ const tangentMag = tx.pow(2).plus(ty.pow(2)).sqrt();
1228
+ if (tangentMag.minus(1).abs().gt(tol)) {
1229
+ errors.push(`Tangent magnitude ${tangentMag} != 1`);
1230
+ }
1231
+
1232
+ // Check normal is unit vector
1233
+ const normalMag = nx.pow(2).plus(ny.pow(2)).sqrt();
1234
+ if (normalMag.minus(1).abs().gt(tol)) {
1235
+ errors.push(`Normal magnitude ${normalMag} != 1`);
1236
+ }
1237
+
1238
+ // Check perpendicularity
1239
+ const dotProduct = tx.times(nx).plus(ty.times(ny));
1240
+ if (dotProduct.abs().gt(tol)) {
1241
+ errors.push(`Tangent and normal not perpendicular, dot product = ${dotProduct}`);
1242
+ }
1243
+
1244
+ // Check tangent aligns with derivative direction
1245
+ const derivMag = dx.pow(2).plus(dy.pow(2)).sqrt();
1246
+ if (derivMag.gt(tol)) {
1247
+ const normalizedDx = dx.div(derivMag);
1248
+ const normalizedDy = dy.div(derivMag);
1249
+ const alignDiff = tx.minus(normalizedDx).abs().plus(ty.minus(normalizedDy).abs());
1250
+ if (alignDiff.gt(tol)) {
1251
+ errors.push(`Tangent doesn't align with derivative direction`);
1252
+ }
1253
+ }
1254
+
1255
+ return {
1256
+ valid: errors.length === 0,
1257
+ errors,
1258
+ tangent,
1259
+ normal,
1260
+ tangentMagnitude: tangentMag,
1261
+ normalMagnitude: normalMag,
1262
+ dotProduct
1263
+ };
1264
+ }
1265
+
1266
+ /**
1267
+ * Verify curvature calculation by comparing with finite difference approximation.
1268
+ * Also verifies radius of curvature = 1/|curvature|.
1269
+ *
1270
+ * @param {BezierPoints} points - Control points
1271
+ * @param {number|string|Decimal} t - Parameter
1272
+ * @param {number|string|Decimal} [tolerance='1e-10'] - Maximum relative error
1273
+ * @returns {{valid: boolean, errors: string[], analyticCurvature: Decimal, finiteDiffCurvature: Decimal, radiusVerified: boolean}}
1274
+ */
1275
+ export function verifyCurvature(points, t, tolerance = '1e-10') {
1276
+ // INPUT VALIDATION: Ensure points array and parameter are valid
1277
+ // WHY: Curvature verification requires valid curve and parameter
1278
+ if (!points || !Array.isArray(points) || points.length < 2) {
1279
+ throw new Error('verifyCurvature: points must be an array with at least 2 control points');
1280
+ }
1281
+
1282
+ const tol = D(tolerance);
1283
+ const errors = [];
1284
+ const tD = D(t);
1285
+
1286
+ const analyticCurvature = bezierCurvature(points, t);
1287
+ const radius = bezierRadiusOfCurvature(points, t);
1288
+
1289
+ // Finite difference approximation using tangent angle change
1290
+ // WHY: Use named constant for finite difference step size
1291
+ const h = FINITE_DIFFERENCE_STEP;
1292
+
1293
+ const t1 = Decimal.max(D(0), tD.minus(h));
1294
+ const t2 = Decimal.min(D(1), tD.plus(h));
1295
+ const actualH = t2.minus(t1);
1296
+
1297
+ const tan1 = bezierTangent(points, t1);
1298
+ const tan2 = bezierTangent(points, t2);
1299
+
1300
+ // Angle change
1301
+ const angle1 = Decimal.atan2(D(tan1[1]), D(tan1[0]));
1302
+ const angle2 = Decimal.atan2(D(tan2[1]), D(tan2[0]));
1303
+ let angleChange = angle2.minus(angle1);
1304
+
1305
+ // Normalize angle change to [-pi, pi]
1306
+ const PI = Decimal.acos(-1);
1307
+ while (angleChange.gt(PI)) angleChange = angleChange.minus(PI.times(2));
1308
+ while (angleChange.lt(PI.neg())) angleChange = angleChange.plus(PI.times(2));
1309
+
1310
+ // Arc length over interval
1311
+ const pt1 = bezierPoint(points, t1);
1312
+ const pt2 = bezierPoint(points, t2);
1313
+ const arcLen = D(pt2[0]).minus(D(pt1[0])).pow(2)
1314
+ .plus(D(pt2[1]).minus(D(pt1[1])).pow(2)).sqrt();
1315
+
1316
+ let finiteDiffCurvature;
1317
+ // WHY: Use named constant for arc length threshold in curvature verification
1318
+ if (arcLen.gt(ARC_LENGTH_THRESHOLD)) {
1319
+ finiteDiffCurvature = angleChange.div(arcLen);
1320
+ } else {
1321
+ finiteDiffCurvature = D(0);
1322
+ }
1323
+
1324
+ // Compare (use relative error for large curvatures)
1325
+ if (!analyticCurvature.isFinite() || analyticCurvature.abs().gt(1e10)) {
1326
+ // Skip comparison for extreme curvatures (cusps)
1327
+ } else if (analyticCurvature.abs().gt(CURVATURE_RELATIVE_ERROR_THRESHOLD)) {
1328
+ // WHY: Use named constant for curvature magnitude threshold
1329
+ const relError = analyticCurvature.minus(finiteDiffCurvature).abs().div(analyticCurvature.abs());
1330
+ if (relError.gt(tol)) {
1331
+ errors.push(`Curvature relative error ${relError} exceeds tolerance`);
1332
+ }
1333
+ }
1334
+
1335
+ // Verify radius = 1/|curvature|
1336
+ let radiusVerified = true;
1337
+ if (analyticCurvature.isZero()) {
1338
+ radiusVerified = !radius.isFinite() || radius.gt(1e50);
1339
+ } else if (analyticCurvature.abs().lt(1e-50)) {
1340
+ radiusVerified = radius.gt(1e40);
1341
+ } else {
1342
+ const expectedRadius = D(1).div(analyticCurvature.abs());
1343
+ const radiusDiff = radius.minus(expectedRadius).abs();
1344
+ if (radiusDiff.gt(tol.times(expectedRadius))) {
1345
+ errors.push(`Radius ${radius} != 1/|curvature| = ${expectedRadius}`);
1346
+ radiusVerified = false;
1347
+ }
1348
+ }
1349
+
1350
+ return {
1351
+ valid: errors.length === 0,
1352
+ errors,
1353
+ analyticCurvature,
1354
+ finiteDiffCurvature,
1355
+ radiusVerified
1356
+ };
1357
+ }
1358
+
1359
+ /**
1360
+ * Verify bounding box contains all curve points and is minimal.
1361
+ *
1362
+ * @param {BezierPoints} points - Control points
1363
+ * @param {number} [samples=100] - Number of sample points
1364
+ * @param {number|string|Decimal} [tolerance='1e-40'] - Maximum error
1365
+ * @returns {{valid: boolean, errors: string[], bbox: Object, allPointsInside: boolean, criticalPointsOnEdge: boolean}}
1366
+ */
1367
+ export function verifyBoundingBox(points, samples = 100, tolerance = '1e-40') {
1368
+ // INPUT VALIDATION: Ensure points array is valid
1369
+ // WHY: Bounding box verification requires valid control points
1370
+ if (!points || !Array.isArray(points) || points.length < 2) {
1371
+ throw new Error('verifyBoundingBox: points must be an array with at least 2 control points');
1372
+ }
1373
+
1374
+ const tol = D(tolerance);
1375
+ const errors = [];
1376
+
1377
+ const bbox = bezierBoundingBox(points);
1378
+ let allPointsInside = true;
1379
+ let criticalPointsOnEdge = true;
1380
+
1381
+ // Check all sampled points are inside bounding box
1382
+ for (let i = 0; i <= samples; i++) {
1383
+ const t = D(i).div(samples);
1384
+ const [x, y] = bezierPoint(points, t);
1385
+
1386
+ if (D(x).lt(bbox.xmin.minus(tol)) || D(x).gt(bbox.xmax.plus(tol))) {
1387
+ errors.push(`Point at t=${t} x=${x} outside x bounds [${bbox.xmin}, ${bbox.xmax}]`);
1388
+ allPointsInside = false;
1389
+ }
1390
+ if (D(y).lt(bbox.ymin.minus(tol)) || D(y).gt(bbox.ymax.plus(tol))) {
1391
+ errors.push(`Point at t=${t} y=${y} outside y bounds [${bbox.ymin}, ${bbox.ymax}]`);
1392
+ allPointsInside = false;
1393
+ }
1394
+ }
1395
+
1396
+ // Verify bounding box edges are achieved by some point
1397
+ let xminAchieved = false, xmaxAchieved = false, yminAchieved = false, ymaxAchieved = false;
1398
+
1399
+ for (let i = 0; i <= samples; i++) {
1400
+ const t = D(i).div(samples);
1401
+ const [x, y] = bezierPoint(points, t);
1402
+
1403
+ if (D(x).minus(bbox.xmin).abs().lt(tol)) xminAchieved = true;
1404
+ if (D(x).minus(bbox.xmax).abs().lt(tol)) xmaxAchieved = true;
1405
+ if (D(y).minus(bbox.ymin).abs().lt(tol)) yminAchieved = true;
1406
+ if (D(y).minus(bbox.ymax).abs().lt(tol)) ymaxAchieved = true;
1407
+ }
1408
+
1409
+ if (!xminAchieved || !xmaxAchieved || !yminAchieved || !ymaxAchieved) {
1410
+ criticalPointsOnEdge = false;
1411
+ if (!xminAchieved) errors.push('xmin not achieved by any curve point');
1412
+ if (!xmaxAchieved) errors.push('xmax not achieved by any curve point');
1413
+ if (!yminAchieved) errors.push('ymin not achieved by any curve point');
1414
+ if (!ymaxAchieved) errors.push('ymax not achieved by any curve point');
1415
+ }
1416
+
1417
+ return {
1418
+ valid: errors.length === 0,
1419
+ errors,
1420
+ bbox,
1421
+ allPointsInside,
1422
+ criticalPointsOnEdge
1423
+ };
1424
+ }
1425
+
1426
+ /**
1427
+ * Verify derivative by comparing with finite difference approximation.
1428
+ *
1429
+ * @param {BezierPoints} points - Control points
1430
+ * @param {number|string|Decimal} t - Parameter
1431
+ * @param {number} [order=1] - Derivative order
1432
+ * @param {number|string|Decimal} [tolerance='1e-8'] - Maximum relative error
1433
+ * @returns {{valid: boolean, analytic: Point2D, finiteDiff: Point2D, relativeError: Decimal}}
1434
+ */
1435
+ export function verifyDerivative(points, t, order = 1, tolerance = '1e-8') {
1436
+ // INPUT VALIDATION: Ensure points array, parameter, and order are valid
1437
+ // WHY: Derivative verification requires valid inputs for meaningful results
1438
+ if (!points || !Array.isArray(points) || points.length < 2) {
1439
+ throw new Error('verifyDerivative: points must be an array with at least 2 control points');
1440
+ }
1441
+
1442
+ const tol = D(tolerance);
1443
+ const tD = D(t);
1444
+
1445
+ const analytic = bezierDerivative(points, t, order);
1446
+
1447
+ // Finite difference (central difference for better accuracy)
1448
+ // WHY: Use named constant for derivative verification step size
1449
+ const h = DERIVATIVE_VERIFICATION_STEP;
1450
+ let finiteDiff;
1451
+
1452
+ if (order === 1) {
1453
+ const t1 = Decimal.max(D(0), tD.minus(h));
1454
+ const t2 = Decimal.min(D(1), tD.plus(h));
1455
+ const pt1 = bezierPoint(points, t1);
1456
+ const pt2 = bezierPoint(points, t2);
1457
+ const dt = t2.minus(t1);
1458
+ finiteDiff = [
1459
+ D(pt2[0]).minus(D(pt1[0])).div(dt),
1460
+ D(pt2[1]).minus(D(pt1[1])).div(dt)
1461
+ ];
1462
+ } else if (order === 2) {
1463
+ const t0 = tD;
1464
+ const t1 = Decimal.max(D(0), tD.minus(h));
1465
+ const t2 = Decimal.min(D(1), tD.plus(h));
1466
+ const pt0 = bezierPoint(points, t0);
1467
+ const pt1 = bezierPoint(points, t1);
1468
+ const pt2 = bezierPoint(points, t2);
1469
+ // Second derivative: (f(t+h) - 2f(t) + f(t-h)) / h^2
1470
+ const h2 = h.pow(2);
1471
+ finiteDiff = [
1472
+ D(pt2[0]).minus(D(pt0[0]).times(2)).plus(D(pt1[0])).div(h2),
1473
+ D(pt2[1]).minus(D(pt0[1]).times(2)).plus(D(pt1[1])).div(h2)
1474
+ ];
1475
+ } else {
1476
+ // Higher orders: not implemented in finite difference approximation
1477
+ // WHY: Higher order finite differences require more sample points and
1478
+ // have increasing numerical instability. For verification purposes,
1479
+ // we note this limitation.
1480
+ console.warn(`verifyDerivative: finite difference for order ${order} not implemented`);
1481
+ finiteDiff = analytic; // Use analytic as fallback (always "passes")
1482
+ }
1483
+
1484
+ const magAnalytic = D(analytic[0]).pow(2).plus(D(analytic[1]).pow(2)).sqrt();
1485
+ const diffX = D(analytic[0]).minus(D(finiteDiff[0])).abs();
1486
+ const diffY = D(analytic[1]).minus(D(finiteDiff[1])).abs();
1487
+
1488
+ let relativeError;
1489
+ // WHY: Use named constant for derivative magnitude threshold
1490
+ if (magAnalytic.gt(DERIVATIVE_MAGNITUDE_THRESHOLD)) {
1491
+ relativeError = diffX.plus(diffY).div(magAnalytic);
1492
+ } else {
1493
+ relativeError = diffX.plus(diffY);
1494
+ }
1495
+
1496
+ return {
1497
+ valid: relativeError.lte(tol),
1498
+ analytic,
1499
+ finiteDiff,
1500
+ relativeError
1501
+ };
1502
+ }
1503
+
1504
+ /**
1505
+ * Verify that a point lies on a Bezier curve within tolerance.
1506
+ *
1507
+ * @param {BezierPoints} points - Control points
1508
+ * @param {Point2D} testPoint - Point to verify
1509
+ * @param {number|string|Decimal} [tolerance='1e-30'] - Maximum distance
1510
+ * @returns {{valid: boolean, t: Decimal|null, distance: Decimal}}
1511
+ */
1512
+ export function verifyPointOnCurve(points, testPoint, tolerance = '1e-30') {
1513
+ // INPUT VALIDATION: Ensure points array and test point are valid
1514
+ // WHY: Point-on-curve verification requires valid curve and test point
1515
+ if (!points || !Array.isArray(points) || points.length < 2) {
1516
+ throw new Error('verifyPointOnCurve: points must be an array with at least 2 control points');
1517
+ }
1518
+ if (!testPoint || !Array.isArray(testPoint) || testPoint.length < 2) {
1519
+ throw new Error('verifyPointOnCurve: testPoint must be a valid 2D point [x, y]');
1520
+ }
1521
+
1522
+ const [px, py] = [D(testPoint[0]), D(testPoint[1])];
1523
+ const tol = D(tolerance);
1524
+
1525
+ // Sample the curve and find closest point
1526
+ let bestT = D(0);
1527
+ let bestDist = new Decimal(Infinity);
1528
+
1529
+ // Initial sampling
1530
+ for (let i = 0; i <= 100; i++) {
1531
+ const t = D(i).div(100);
1532
+ const [x, y] = bezierPoint(points, t);
1533
+ const dist = px.minus(x).pow(2).plus(py.minus(y).pow(2)).sqrt();
1534
+
1535
+ if (dist.lt(bestDist)) {
1536
+ bestDist = dist;
1537
+ bestT = t;
1538
+ }
1539
+ }
1540
+
1541
+ // Refine using Newton's method
1542
+ for (let iter = 0; iter < 20; iter++) {
1543
+ const [x, y] = bezierPoint(points, bestT);
1544
+ const [dx, dy] = bezierDerivative(points, bestT, 1);
1545
+
1546
+ // Distance squared: f(t) = (x(t) - px)^2 + (y(t) - py)^2
1547
+ // Derivative: f'(t) = 2(x(t) - px)*x'(t) + 2(y(t) - py)*y'(t)
1548
+
1549
+ const diffX = x.minus(px);
1550
+ const diffY = y.minus(py);
1551
+ const fPrime = diffX.times(dx).plus(diffY.times(dy)).times(2);
1552
+
1553
+ // WHY: Use named constant for zero-derivative check in Newton iteration
1554
+ if (fPrime.abs().lt(NEAR_ZERO_THRESHOLD)) break;
1555
+
1556
+ // f''(t) for Newton's method
1557
+ const [d2x, d2y] = bezierDerivative(points, bestT, 2);
1558
+ const fDoublePrime = dx.pow(2).plus(dy.pow(2)).plus(diffX.times(d2x)).plus(diffY.times(d2y)).times(2);
1559
+
1560
+ // WHY: Use named constant for zero second derivative check
1561
+ if (fDoublePrime.abs().lt(NEAR_ZERO_THRESHOLD)) break;
1562
+
1563
+ const delta = fPrime.div(fDoublePrime);
1564
+ bestT = bestT.minus(delta);
1565
+
1566
+ // Clamp to [0, 1]
1567
+ if (bestT.lt(0)) bestT = D(0);
1568
+ if (bestT.gt(1)) bestT = D(1);
1569
+
1570
+ // WHY: Use named constant for Newton-Raphson convergence check
1571
+ if (delta.abs().lt(NEWTON_CONVERGENCE_THRESHOLD)) break;
1572
+ }
1573
+
1574
+ // Final distance check
1575
+ const [finalX, finalY] = bezierPoint(points, bestT);
1576
+ const finalDist = px.minus(finalX).pow(2).plus(py.minus(finalY).pow(2)).sqrt();
1577
+
1578
+ return {
1579
+ valid: finalDist.lte(tol),
1580
+ t: finalDist.lte(tol) ? bestT : null,
1581
+ distance: finalDist
1582
+ };
1583
+ }
1584
+
1585
+ // ============================================================================
1586
+ // EXPORTS
1587
+ // ============================================================================
1588
+
1589
+ export default {
1590
+ // Evaluation
1591
+ bezierPoint,
1592
+ bezierPointHorner,
1593
+
1594
+ // Derivatives
1595
+ bezierDerivative,
1596
+ bezierDerivativePoints,
1597
+
1598
+ // Differential geometry
1599
+ bezierTangent,
1600
+ bezierNormal,
1601
+ bezierCurvature,
1602
+ bezierRadiusOfCurvature,
1603
+
1604
+ // Subdivision
1605
+ bezierSplit,
1606
+ bezierHalve,
1607
+ bezierCrop,
1608
+
1609
+ // Bounding box
1610
+ bezierBoundingBox,
1611
+
1612
+ // Polynomial conversion
1613
+ bezierToPolynomial,
1614
+ polynomialToBezier,
1615
+
1616
+ // Verification (inverse operations)
1617
+ verifyBezierPoint,
1618
+ verifyBezierSplit,
1619
+ verifyBezierCrop,
1620
+ verifyPolynomialConversion,
1621
+ verifyTangentNormal,
1622
+ verifyCurvature,
1623
+ verifyBoundingBox,
1624
+ verifyDerivative,
1625
+ verifyPointOnCurve
1626
+ };