@emasoft/svg-matrix 1.0.19 → 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.
- package/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -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
|
+
};
|