@emasoft/svg-matrix 1.0.19 → 1.0.21
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 +9 -3
- 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,1369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Arbitrary-Precision Bezier Intersection Detection
|
|
3
|
+
*
|
|
4
|
+
* Robust intersection detection between Bezier curves using:
|
|
5
|
+
* - Bezier clipping for fast convergence
|
|
6
|
+
* - Subdivision for robustness
|
|
7
|
+
* - Newton-Raphson refinement for precision
|
|
8
|
+
*
|
|
9
|
+
* Superior to svgpathtools:
|
|
10
|
+
* - 80-digit precision vs 15-digit
|
|
11
|
+
* - Handles near-tangent intersections
|
|
12
|
+
* - No exponential blowup
|
|
13
|
+
*
|
|
14
|
+
* @module bezier-intersections
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import Decimal from 'decimal.js';
|
|
19
|
+
import {
|
|
20
|
+
bezierPoint,
|
|
21
|
+
bezierBoundingBox,
|
|
22
|
+
bezierSplit,
|
|
23
|
+
bezierCrop,
|
|
24
|
+
bezierDerivative
|
|
25
|
+
} from './bezier-analysis.js';
|
|
26
|
+
|
|
27
|
+
Decimal.set({ precision: 80 });
|
|
28
|
+
|
|
29
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// LINE-LINE INTERSECTION
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
// Numerical thresholds (documented magic numbers)
|
|
36
|
+
// WHY: Centralizing magic numbers as constants improves maintainability and makes
|
|
37
|
+
// the code self-documenting. These thresholds were tuned for 80-digit precision arithmetic.
|
|
38
|
+
const PARALLEL_THRESHOLD = '1e-60'; // Below this, lines considered parallel
|
|
39
|
+
const SINGULARITY_THRESHOLD = '1e-50'; // Below this, Jacobian considered singular
|
|
40
|
+
const INTERSECTION_VERIFY_FACTOR = 100; // Multiplier for intersection verification
|
|
41
|
+
const DEDUP_TOLERANCE_FACTOR = 1000; // Multiplier for duplicate detection
|
|
42
|
+
|
|
43
|
+
/** Maximum Newton iterations for intersection refinement */
|
|
44
|
+
const MAX_NEWTON_ITERATIONS = 30;
|
|
45
|
+
|
|
46
|
+
/** Maximum recursion depth for bezier-bezier intersection */
|
|
47
|
+
const MAX_INTERSECTION_RECURSION_DEPTH = 50;
|
|
48
|
+
|
|
49
|
+
/** Minimum parameter separation for self-intersection detection */
|
|
50
|
+
const DEFAULT_MIN_SEPARATION = '0.01';
|
|
51
|
+
|
|
52
|
+
/** Maximum bisection iterations for bezier-line refinement */
|
|
53
|
+
const MAX_BISECTION_ITERATIONS = 100;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find intersection between two line segments.
|
|
57
|
+
*
|
|
58
|
+
* Uses Cramer's rule for exact solution with the standard parametric
|
|
59
|
+
* line intersection formula.
|
|
60
|
+
*
|
|
61
|
+
* @param {Array} line1 - First line [[x0,y0], [x1,y1]]
|
|
62
|
+
* @param {Array} line2 - Second line [[x0,y0], [x1,y1]]
|
|
63
|
+
* @returns {Array} Intersection [{t1, t2, point}] or empty array if no intersection
|
|
64
|
+
*/
|
|
65
|
+
export function lineLineIntersection(line1, line2) {
|
|
66
|
+
// Input validation
|
|
67
|
+
if (!line1 || !Array.isArray(line1) || line1.length !== 2) {
|
|
68
|
+
throw new Error('lineLineIntersection: line1 must be an array of 2 points');
|
|
69
|
+
}
|
|
70
|
+
if (!line2 || !Array.isArray(line2) || line2.length !== 2) {
|
|
71
|
+
throw new Error('lineLineIntersection: line2 must be an array of 2 points');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [x1, y1] = [D(line1[0][0]), D(line1[0][1])];
|
|
75
|
+
const [x2, y2] = [D(line1[1][0]), D(line1[1][1])];
|
|
76
|
+
const [x3, y3] = [D(line2[0][0]), D(line2[0][1])];
|
|
77
|
+
const [x4, y4] = [D(line2[1][0]), D(line2[1][1])];
|
|
78
|
+
|
|
79
|
+
// Direction vectors
|
|
80
|
+
const dx1 = x2.minus(x1);
|
|
81
|
+
const dy1 = y2.minus(y1);
|
|
82
|
+
const dx2 = x4.minus(x3);
|
|
83
|
+
const dy2 = y4.minus(y3);
|
|
84
|
+
|
|
85
|
+
// Determinant (cross product of direction vectors)
|
|
86
|
+
const denom = dx1.times(dy2).minus(dy1.times(dx2));
|
|
87
|
+
|
|
88
|
+
if (denom.abs().lt(new Decimal(PARALLEL_THRESHOLD))) {
|
|
89
|
+
// Lines are parallel or nearly parallel
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Solve for t1 and t2
|
|
94
|
+
const dx13 = x1.minus(x3);
|
|
95
|
+
const dy13 = y1.minus(y3);
|
|
96
|
+
|
|
97
|
+
const t1 = dx2.times(dy13).minus(dy2.times(dx13)).div(denom).neg();
|
|
98
|
+
const t2 = dx1.times(dy13).minus(dy1.times(dx13)).div(denom).neg();
|
|
99
|
+
|
|
100
|
+
// Check if intersection is within both segments
|
|
101
|
+
if (t1.gte(0) && t1.lte(1) && t2.gte(0) && t2.lte(1)) {
|
|
102
|
+
const px = x1.plus(dx1.times(t1));
|
|
103
|
+
const py = y1.plus(dy1.times(t1));
|
|
104
|
+
|
|
105
|
+
return [{
|
|
106
|
+
t1,
|
|
107
|
+
t2,
|
|
108
|
+
point: [px, py]
|
|
109
|
+
}];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// BEZIER-LINE INTERSECTION
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find intersections between a Bezier curve and a line segment.
|
|
121
|
+
*
|
|
122
|
+
* Uses algebraic approach: substitute line equation into Bezier polynomial,
|
|
123
|
+
* then find sign changes by sampling and refine with bisection.
|
|
124
|
+
*
|
|
125
|
+
* @param {Array} bezier - Bezier control points [[x,y], ...]
|
|
126
|
+
* @param {Array} line - Line segment [[x0,y0], [x1,y1]]
|
|
127
|
+
* @param {Object} [options] - Options
|
|
128
|
+
* @param {string} [options.tolerance='1e-30'] - Root tolerance
|
|
129
|
+
* @param {number} [options.samplesPerDegree=20] - Samples per curve degree
|
|
130
|
+
* @returns {Array} Intersections [{t1 (bezier), t2 (line), point}]
|
|
131
|
+
*/
|
|
132
|
+
export function bezierLineIntersection(bezier, line, options = {}) {
|
|
133
|
+
const { tolerance = '1e-30', samplesPerDegree = 20 } = options;
|
|
134
|
+
const tol = D(tolerance);
|
|
135
|
+
|
|
136
|
+
// Input validation
|
|
137
|
+
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
138
|
+
throw new Error('bezierLineIntersection: bezier must have at least 2 control points');
|
|
139
|
+
}
|
|
140
|
+
if (!line || !Array.isArray(line) || line.length !== 2) {
|
|
141
|
+
throw new Error('bezierLineIntersection: line must be an array of 2 points');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const [lx0, ly0] = [D(line[0][0]), D(line[0][1])];
|
|
145
|
+
const [lx1, ly1] = [D(line[1][0]), D(line[1][1])];
|
|
146
|
+
|
|
147
|
+
// Line equation: (y - ly0) * (lx1 - lx0) - (x - lx0) * (ly1 - ly0) = 0
|
|
148
|
+
// Substitute Bezier curve (x(t), y(t)) and find roots
|
|
149
|
+
|
|
150
|
+
const dlx = lx1.minus(lx0);
|
|
151
|
+
const dly = ly1.minus(ly0);
|
|
152
|
+
|
|
153
|
+
// Handle degenerate line
|
|
154
|
+
if (dlx.abs().lt(tol) && dly.abs().lt(tol)) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Sample Bezier and find sign changes
|
|
159
|
+
const n = bezier.length - 1;
|
|
160
|
+
const samples = n * samplesPerDegree;
|
|
161
|
+
const candidates = [];
|
|
162
|
+
|
|
163
|
+
let prevSign = null;
|
|
164
|
+
let prevT = null;
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i <= samples; i++) {
|
|
167
|
+
const t = D(i).div(samples);
|
|
168
|
+
const [bx, by] = bezierPoint(bezier, t);
|
|
169
|
+
|
|
170
|
+
// Distance from line (signed)
|
|
171
|
+
const dist = by.minus(ly0).times(dlx).minus(bx.minus(lx0).times(dly));
|
|
172
|
+
const sign = dist.isNegative() ? -1 : (dist.isZero() ? 0 : 1);
|
|
173
|
+
|
|
174
|
+
if (sign === 0) {
|
|
175
|
+
// Exactly on line
|
|
176
|
+
candidates.push(t);
|
|
177
|
+
} else if (prevSign !== null && prevSign !== sign && prevSign !== 0) {
|
|
178
|
+
// Sign change - refine with bisection
|
|
179
|
+
const root = refineBezierLineRoot(bezier, line, prevT, t, tol);
|
|
180
|
+
if (root !== null) {
|
|
181
|
+
candidates.push(root);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
prevSign = sign;
|
|
186
|
+
prevT = t;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate and compute t2 for each candidate
|
|
190
|
+
const results = [];
|
|
191
|
+
|
|
192
|
+
for (const t1 of candidates) {
|
|
193
|
+
const [bx, by] = bezierPoint(bezier, t1);
|
|
194
|
+
|
|
195
|
+
// Find t2 (parameter on line)
|
|
196
|
+
let t2;
|
|
197
|
+
if (dlx.abs().gt(dly.abs())) {
|
|
198
|
+
t2 = bx.minus(lx0).div(dlx);
|
|
199
|
+
} else {
|
|
200
|
+
t2 = by.minus(ly0).div(dly);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if t2 is within [0, 1]
|
|
204
|
+
if (t2.gte(0) && t2.lte(1)) {
|
|
205
|
+
// Verify intersection
|
|
206
|
+
const [lineX, lineY] = [lx0.plus(dlx.times(t2)), ly0.plus(dly.times(t2))];
|
|
207
|
+
const dist = bx.minus(lineX).pow(2).plus(by.minus(lineY).pow(2)).sqrt();
|
|
208
|
+
|
|
209
|
+
if (dist.lt(tol.times(INTERSECTION_VERIFY_FACTOR))) {
|
|
210
|
+
results.push({ t1, t2, point: [bx, by] });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Remove duplicates
|
|
216
|
+
return deduplicateIntersections(results, tol);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Refine Bezier-line intersection using bisection.
|
|
221
|
+
*/
|
|
222
|
+
function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
223
|
+
const [lx0, ly0] = [D(line[0][0]), D(line[0][1])];
|
|
224
|
+
const [lx1, ly1] = [D(line[1][0]), D(line[1][1])];
|
|
225
|
+
const dlx = lx1.minus(lx0);
|
|
226
|
+
const dly = ly1.minus(ly0);
|
|
227
|
+
|
|
228
|
+
let lo = D(t0);
|
|
229
|
+
let hi = D(t1);
|
|
230
|
+
|
|
231
|
+
const evalDist = t => {
|
|
232
|
+
const [bx, by] = bezierPoint(bezier, t);
|
|
233
|
+
return by.minus(ly0).times(dlx).minus(bx.minus(lx0).times(dly));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
let fLo = evalDist(lo);
|
|
237
|
+
let fHi = evalDist(hi);
|
|
238
|
+
|
|
239
|
+
// WHY: Use named constant instead of magic number for clarity and maintainability
|
|
240
|
+
for (let i = 0; i < MAX_BISECTION_ITERATIONS; i++) {
|
|
241
|
+
const mid = lo.plus(hi).div(2);
|
|
242
|
+
const fMid = evalDist(mid);
|
|
243
|
+
|
|
244
|
+
if (fMid.abs().lt(tol) || hi.minus(lo).lt(tol)) {
|
|
245
|
+
return mid;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if ((fLo.isNegative() && fMid.isNegative()) || (fLo.isPositive() && fMid.isPositive())) {
|
|
249
|
+
lo = mid;
|
|
250
|
+
fLo = fMid;
|
|
251
|
+
} else {
|
|
252
|
+
hi = mid;
|
|
253
|
+
fHi = fMid;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return lo.plus(hi).div(2);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// BEZIER-BEZIER INTERSECTION
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Find all intersections between two Bezier curves.
|
|
266
|
+
*
|
|
267
|
+
* Algorithm:
|
|
268
|
+
* 1. Bounding box rejection test
|
|
269
|
+
* 2. Recursive subdivision until parameter ranges converge
|
|
270
|
+
* 3. Newton-Raphson refinement for final precision
|
|
271
|
+
*
|
|
272
|
+
* @param {Array} bezier1 - First Bezier control points [[x,y], ...]
|
|
273
|
+
* @param {Array} bezier2 - Second Bezier control points [[x,y], ...]
|
|
274
|
+
* @param {Object} [options] - Options
|
|
275
|
+
* @param {string} [options.tolerance='1e-30'] - Intersection tolerance
|
|
276
|
+
* @param {number} [options.maxDepth=50] - Maximum recursion depth
|
|
277
|
+
* @returns {Array} Intersections [{t1, t2, point, error}]
|
|
278
|
+
*/
|
|
279
|
+
export function bezierBezierIntersection(bezier1, bezier2, options = {}) {
|
|
280
|
+
// WHY: Use named constant as default instead of hardcoded 50 for clarity
|
|
281
|
+
const {
|
|
282
|
+
tolerance = '1e-30',
|
|
283
|
+
maxDepth = MAX_INTERSECTION_RECURSION_DEPTH
|
|
284
|
+
} = options;
|
|
285
|
+
|
|
286
|
+
// Input validation
|
|
287
|
+
if (!bezier1 || !Array.isArray(bezier1) || bezier1.length < 2) {
|
|
288
|
+
throw new Error('bezierBezierIntersection: bezier1 must have at least 2 control points');
|
|
289
|
+
}
|
|
290
|
+
if (!bezier2 || !Array.isArray(bezier2) || bezier2.length < 2) {
|
|
291
|
+
throw new Error('bezierBezierIntersection: bezier2 must have at least 2 control points');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const tol = D(tolerance);
|
|
295
|
+
const results = [];
|
|
296
|
+
|
|
297
|
+
// Recursive intersection finder
|
|
298
|
+
function findIntersections(pts1, pts2, t1min, t1max, t2min, t2max, depth) {
|
|
299
|
+
// Bounding box rejection
|
|
300
|
+
const bbox1 = bezierBoundingBox(pts1);
|
|
301
|
+
const bbox2 = bezierBoundingBox(pts2);
|
|
302
|
+
|
|
303
|
+
if (!bboxOverlap(bbox1, bbox2)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for convergence
|
|
308
|
+
const t1range = t1max.minus(t1min);
|
|
309
|
+
const t2range = t2max.minus(t2min);
|
|
310
|
+
|
|
311
|
+
if (t1range.lt(tol) && t2range.lt(tol)) {
|
|
312
|
+
// Converged - refine with Newton
|
|
313
|
+
const t1mid = t1min.plus(t1max).div(2);
|
|
314
|
+
const t2mid = t2min.plus(t2max).div(2);
|
|
315
|
+
|
|
316
|
+
const refined = refineIntersection(bezier1, bezier2, t1mid, t2mid, tol);
|
|
317
|
+
if (refined) {
|
|
318
|
+
results.push(refined);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check recursion depth
|
|
324
|
+
if (depth >= maxDepth) {
|
|
325
|
+
const t1mid = t1min.plus(t1max).div(2);
|
|
326
|
+
const t2mid = t2min.plus(t2max).div(2);
|
|
327
|
+
const [x, y] = bezierPoint(bezier1, t1mid);
|
|
328
|
+
results.push({ t1: t1mid, t2: t2mid, point: [x, y] });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Subdivision: split the larger curve
|
|
333
|
+
if (t1range.gt(t2range)) {
|
|
334
|
+
const { left, right } = bezierSplit(pts1, 0.5);
|
|
335
|
+
const t1mid = t1min.plus(t1max).div(2);
|
|
336
|
+
|
|
337
|
+
findIntersections(left, pts2, t1min, t1mid, t2min, t2max, depth + 1);
|
|
338
|
+
findIntersections(right, pts2, t1mid, t1max, t2min, t2max, depth + 1);
|
|
339
|
+
} else {
|
|
340
|
+
const { left, right } = bezierSplit(pts2, 0.5);
|
|
341
|
+
const t2mid = t2min.plus(t2max).div(2);
|
|
342
|
+
|
|
343
|
+
findIntersections(pts1, left, t1min, t1max, t2min, t2mid, depth + 1);
|
|
344
|
+
findIntersections(pts1, right, t1min, t1max, t2mid, t2max, depth + 1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
findIntersections(bezier1, bezier2, D(0), D(1), D(0), D(1), 0);
|
|
349
|
+
|
|
350
|
+
// Remove duplicates and validate
|
|
351
|
+
return deduplicateIntersections(results, tol);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Refine intersection using Newton-Raphson method.
|
|
356
|
+
*
|
|
357
|
+
* Solves: B1(t1) - B2(t2) = 0
|
|
358
|
+
*
|
|
359
|
+
* @param {Array} bez1 - First Bezier
|
|
360
|
+
* @param {Array} bez2 - Second Bezier
|
|
361
|
+
* @param {Decimal} t1 - Initial t1 guess
|
|
362
|
+
* @param {Decimal} t2 - Initial t2 guess
|
|
363
|
+
* @param {Decimal} tol - Tolerance
|
|
364
|
+
* @returns {Object|null} Refined intersection or null
|
|
365
|
+
*/
|
|
366
|
+
function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
367
|
+
let currentT1 = D(t1);
|
|
368
|
+
let currentT2 = D(t2);
|
|
369
|
+
|
|
370
|
+
// WHY: Use named constant instead of hardcoded 30 for clarity and maintainability
|
|
371
|
+
for (let iter = 0; iter < MAX_NEWTON_ITERATIONS; iter++) {
|
|
372
|
+
// Clamp to [0, 1]
|
|
373
|
+
if (currentT1.lt(0)) currentT1 = D(0);
|
|
374
|
+
if (currentT1.gt(1)) currentT1 = D(1);
|
|
375
|
+
if (currentT2.lt(0)) currentT2 = D(0);
|
|
376
|
+
if (currentT2.gt(1)) currentT2 = D(1);
|
|
377
|
+
|
|
378
|
+
const [x1, y1] = bezierPoint(bez1, currentT1);
|
|
379
|
+
const [x2, y2] = bezierPoint(bez2, currentT2);
|
|
380
|
+
|
|
381
|
+
const fx = x1.minus(x2);
|
|
382
|
+
const fy = y1.minus(y2);
|
|
383
|
+
|
|
384
|
+
// Check convergence
|
|
385
|
+
const error = fx.pow(2).plus(fy.pow(2)).sqrt();
|
|
386
|
+
if (error.lt(tol)) {
|
|
387
|
+
return {
|
|
388
|
+
t1: currentT1,
|
|
389
|
+
t2: currentT2,
|
|
390
|
+
point: [x1, y1],
|
|
391
|
+
error
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Jacobian
|
|
396
|
+
const [dx1, dy1] = bezierDerivative(bez1, currentT1, 1);
|
|
397
|
+
const [dx2, dy2] = bezierDerivative(bez2, currentT2, 1);
|
|
398
|
+
|
|
399
|
+
// J = [[dx1, -dx2], [dy1, -dy2]]
|
|
400
|
+
// det(J) = dx1*(-dy2) - (-dx2)*dy1 = -dx1*dy2 + dx2*dy1
|
|
401
|
+
const det = dx2.times(dy1).minus(dx1.times(dy2));
|
|
402
|
+
|
|
403
|
+
if (det.abs().lt(new Decimal(SINGULARITY_THRESHOLD))) {
|
|
404
|
+
// Singular Jacobian - curves are nearly parallel
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Solve: J * [dt1, dt2]^T = -[fx, fy]^T
|
|
409
|
+
// dt1 = (-(-dy2)*(-fx) - (-dx2)*(-fy)) / det = (-dy2*fx + dx2*fy) / det
|
|
410
|
+
// dt2 = (dx1*(-fy) - dy1*(-fx)) / det = (-dx1*fy + dy1*fx) / det
|
|
411
|
+
|
|
412
|
+
const dt1 = dy2.neg().times(fx).plus(dx2.times(fy)).div(det);
|
|
413
|
+
const dt2 = dx1.neg().times(fy).plus(dy1.times(fx)).div(det);
|
|
414
|
+
|
|
415
|
+
currentT1 = currentT1.plus(dt1);
|
|
416
|
+
currentT2 = currentT2.plus(dt2);
|
|
417
|
+
|
|
418
|
+
// Check for convergence by step size
|
|
419
|
+
if (dt1.abs().lt(tol) && dt2.abs().lt(tol)) {
|
|
420
|
+
// BUGFIX: Compute fresh error value instead of using stale one from previous iteration
|
|
421
|
+
// WHY: The `error` variable computed above (line 368) is from before the parameter update,
|
|
422
|
+
// so it may not reflect the final accuracy. We need to recompute error for the converged parameters.
|
|
423
|
+
const [finalX, finalY] = bezierPoint(bez1, currentT1);
|
|
424
|
+
const [finalX2, finalY2] = bezierPoint(bez2, currentT2);
|
|
425
|
+
const finalError = D(finalX).minus(D(finalX2)).pow(2)
|
|
426
|
+
.plus(D(finalY).minus(D(finalY2)).pow(2)).sqrt();
|
|
427
|
+
return {
|
|
428
|
+
t1: currentT1,
|
|
429
|
+
t2: currentT2,
|
|
430
|
+
point: [finalX, finalY],
|
|
431
|
+
error: finalError
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Check if two bounding boxes overlap.
|
|
441
|
+
*/
|
|
442
|
+
function bboxOverlap(bbox1, bbox2) {
|
|
443
|
+
// INPUT VALIDATION
|
|
444
|
+
// WHY: Prevent cryptic errors from undefined bounding boxes
|
|
445
|
+
if (!bbox1 || !bbox2) {
|
|
446
|
+
return false; // No overlap if either bbox is missing
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return !(bbox1.xmax.lt(bbox2.xmin) ||
|
|
450
|
+
bbox1.xmin.gt(bbox2.xmax) ||
|
|
451
|
+
bbox1.ymax.lt(bbox2.ymin) ||
|
|
452
|
+
bbox1.ymin.gt(bbox2.ymax));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Remove duplicate intersections.
|
|
457
|
+
*/
|
|
458
|
+
function deduplicateIntersections(intersections, tol) {
|
|
459
|
+
const result = [];
|
|
460
|
+
|
|
461
|
+
for (const isect of intersections) {
|
|
462
|
+
let isDuplicate = false;
|
|
463
|
+
|
|
464
|
+
for (const existing of result) {
|
|
465
|
+
const dt1 = isect.t1.minus(existing.t1).abs();
|
|
466
|
+
const dt2 = isect.t2.minus(existing.t2).abs();
|
|
467
|
+
|
|
468
|
+
if (dt1.lt(tol.times(DEDUP_TOLERANCE_FACTOR)) && dt2.lt(tol.times(DEDUP_TOLERANCE_FACTOR))) {
|
|
469
|
+
isDuplicate = true;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!isDuplicate) {
|
|
475
|
+
result.push(isect);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// SELF-INTERSECTION
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Find self-intersections of a Bezier curve.
|
|
488
|
+
*
|
|
489
|
+
* Uses recursive subdivision to find where different parts of the curve
|
|
490
|
+
* intersect themselves. Only meaningful for cubic and higher degree curves.
|
|
491
|
+
*
|
|
492
|
+
* @param {Array} bezier - Control points [[x,y], ...]
|
|
493
|
+
* @param {Object} [options] - Options
|
|
494
|
+
* @param {string} [options.tolerance='1e-30'] - Intersection tolerance
|
|
495
|
+
* @param {string} [options.minSeparation='0.01'] - Minimum parameter separation
|
|
496
|
+
* @param {number} [options.maxDepth=30] - Maximum recursion depth
|
|
497
|
+
* @returns {Array} Self-intersections [{t1, t2, point}] where t1 < t2
|
|
498
|
+
*/
|
|
499
|
+
export function bezierSelfIntersection(bezier, options = {}) {
|
|
500
|
+
// WHY: Use named constants as defaults instead of hardcoded values for clarity
|
|
501
|
+
const { tolerance = '1e-30', minSeparation = DEFAULT_MIN_SEPARATION, maxDepth = 30 } = options;
|
|
502
|
+
const tol = D(tolerance);
|
|
503
|
+
const minSep = D(minSeparation);
|
|
504
|
+
|
|
505
|
+
// Input validation
|
|
506
|
+
if (!bezier || bezier.length < 2) {
|
|
507
|
+
throw new Error('bezierSelfIntersection: bezier must have at least 2 control points');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Self-intersections only possible for cubic and higher
|
|
511
|
+
if (bezier.length < 4) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const results = [];
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Recursive helper to find self-intersections in a parameter range.
|
|
519
|
+
* @param {Decimal} tmin - Start of parameter range
|
|
520
|
+
* @param {Decimal} tmax - End of parameter range
|
|
521
|
+
* @param {number} depth - Current recursion depth
|
|
522
|
+
*/
|
|
523
|
+
function findSelfIntersections(tmin, tmax, depth) {
|
|
524
|
+
const range = tmax.minus(tmin);
|
|
525
|
+
|
|
526
|
+
// Stop if range is too small or max depth reached
|
|
527
|
+
if (range.lt(minSep) || depth > maxDepth) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const tmid = tmin.plus(tmax).div(2);
|
|
532
|
+
|
|
533
|
+
// Check for intersection between left and right halves
|
|
534
|
+
// (only if they're separated enough in parameter space)
|
|
535
|
+
if (range.gt(minSep.times(2))) {
|
|
536
|
+
const leftPts = bezierCrop(bezier, tmin, tmid);
|
|
537
|
+
const rightPts = bezierCrop(bezier, tmid, tmax);
|
|
538
|
+
|
|
539
|
+
// Find intersections between left and right portions
|
|
540
|
+
const isects = bezierBezierIntersection(leftPts, rightPts, { tolerance, maxDepth: maxDepth - depth });
|
|
541
|
+
|
|
542
|
+
for (const isect of isects) {
|
|
543
|
+
// Map from cropped parameter space [0,1] back to original range
|
|
544
|
+
// Left half: t_orig = tmin + t_local * (tmid - tmin)
|
|
545
|
+
// Right half: t_orig = tmid + t_local * (tmax - tmid)
|
|
546
|
+
const halfRange = range.div(2);
|
|
547
|
+
const origT1 = tmin.plus(isect.t1.times(halfRange));
|
|
548
|
+
const origT2 = tmid.plus(isect.t2.times(halfRange));
|
|
549
|
+
|
|
550
|
+
// Ensure t1 < t2 and they're sufficiently separated
|
|
551
|
+
if (origT2.minus(origT1).abs().gt(minSep)) {
|
|
552
|
+
results.push({
|
|
553
|
+
t1: Decimal.min(origT1, origT2),
|
|
554
|
+
t2: Decimal.max(origT1, origT2),
|
|
555
|
+
point: isect.point
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Recurse into both halves
|
|
562
|
+
findSelfIntersections(tmin, tmid, depth + 1);
|
|
563
|
+
findSelfIntersections(tmid, tmax, depth + 1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
findSelfIntersections(D(0), D(1), 0);
|
|
567
|
+
|
|
568
|
+
// WHY: Self-intersection deduplication needs a more practical tolerance.
|
|
569
|
+
// The recursive subdivision can find the same intersection from multiple branches,
|
|
570
|
+
// with slightly different parameter values. Use minSep as the dedup tolerance
|
|
571
|
+
// since intersections closer than minSep in parameter space are considered the same.
|
|
572
|
+
const dedupTol = minSep.div(10); // Use 1/10 of minSeparation for deduplication
|
|
573
|
+
const deduped = deduplicateIntersections(results, dedupTol);
|
|
574
|
+
|
|
575
|
+
// WHY: After finding rough intersections via subdivision on cropped curves,
|
|
576
|
+
// refine each one using Newton-Raphson on the ORIGINAL curve. This achieves
|
|
577
|
+
// full precision because we're optimizing directly on the original parameters.
|
|
578
|
+
const refined = [];
|
|
579
|
+
for (const isect of deduped) {
|
|
580
|
+
const refinedIsect = refineSelfIntersection(bezier, isect.t1, isect.t2, tol, minSep);
|
|
581
|
+
if (refinedIsect) {
|
|
582
|
+
refined.push(refinedIsect);
|
|
583
|
+
} else {
|
|
584
|
+
// Keep original if refinement fails
|
|
585
|
+
refined.push(isect);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return refined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Refine a self-intersection using Newton-Raphson directly on the original curve.
|
|
594
|
+
*
|
|
595
|
+
* For self-intersection, we solve: B(t1) = B(t2) with t1 < t2
|
|
596
|
+
*
|
|
597
|
+
* @param {Array} bezier - Original curve control points
|
|
598
|
+
* @param {Decimal} t1Init - Initial t1 guess
|
|
599
|
+
* @param {Decimal} t2Init - Initial t2 guess
|
|
600
|
+
* @param {Decimal} tol - Convergence tolerance
|
|
601
|
+
* @param {Decimal} minSep - Minimum separation between t1 and t2
|
|
602
|
+
* @returns {Object|null} Refined intersection or null if failed
|
|
603
|
+
*/
|
|
604
|
+
function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
605
|
+
let t1 = D(t1Init);
|
|
606
|
+
let t2 = D(t2Init);
|
|
607
|
+
|
|
608
|
+
for (let iter = 0; iter < MAX_NEWTON_ITERATIONS; iter++) {
|
|
609
|
+
// Clamp to valid range while maintaining separation
|
|
610
|
+
if (t1.lt(0)) t1 = D(0);
|
|
611
|
+
if (t2.gt(1)) t2 = D(1);
|
|
612
|
+
if (t2.minus(t1).lt(minSep)) {
|
|
613
|
+
// Maintain minimum separation
|
|
614
|
+
const mid = t1.plus(t2).div(2);
|
|
615
|
+
t1 = mid.minus(minSep.div(2));
|
|
616
|
+
t2 = mid.plus(minSep.div(2));
|
|
617
|
+
if (t1.lt(0)) { t1 = D(0); t2 = minSep; }
|
|
618
|
+
if (t2.gt(1)) { t2 = D(1); t1 = D(1).minus(minSep); }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Evaluate curve at both parameters
|
|
622
|
+
const [x1, y1] = bezierPoint(bezier, t1);
|
|
623
|
+
const [x2, y2] = bezierPoint(bezier, t2);
|
|
624
|
+
|
|
625
|
+
// Residual: B(t1) - B(t2) = 0
|
|
626
|
+
const fx = x1.minus(x2);
|
|
627
|
+
const fy = y1.minus(y2);
|
|
628
|
+
|
|
629
|
+
// Check convergence
|
|
630
|
+
const error = fx.pow(2).plus(fy.pow(2)).sqrt();
|
|
631
|
+
if (error.lt(tol)) {
|
|
632
|
+
return {
|
|
633
|
+
t1: Decimal.min(t1, t2),
|
|
634
|
+
t2: Decimal.max(t1, t2),
|
|
635
|
+
point: [x1.plus(x2).div(2), y1.plus(y2).div(2)], // Average of both points
|
|
636
|
+
error
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Jacobian: d(B(t1) - B(t2))/d[t1, t2] = [B'(t1), -B'(t2)]
|
|
641
|
+
const [dx1, dy1] = bezierDerivative(bezier, t1, 1);
|
|
642
|
+
const [dx2, dy2] = bezierDerivative(bezier, t2, 1);
|
|
643
|
+
|
|
644
|
+
// J = [[dx1, -dx2], [dy1, -dy2]]
|
|
645
|
+
// det(J) = dx1*(-dy2) - (-dx2)*dy1 = -dx1*dy2 + dx2*dy1
|
|
646
|
+
const det = dx2.times(dy1).minus(dx1.times(dy2));
|
|
647
|
+
|
|
648
|
+
if (det.abs().lt(new Decimal(SINGULARITY_THRESHOLD))) {
|
|
649
|
+
// Singular Jacobian - curves are nearly parallel at these points
|
|
650
|
+
// Try bisection step instead
|
|
651
|
+
if (fx.isNegative()) {
|
|
652
|
+
t1 = t1.plus(D('0.0001'));
|
|
653
|
+
} else {
|
|
654
|
+
t2 = t2.minus(D('0.0001'));
|
|
655
|
+
}
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Solve Newton step: [dt1, dt2]^T = J^{-1} * [fx, fy]^T
|
|
660
|
+
// J = [[dx1, -dx2], [dy1, -dy2]]
|
|
661
|
+
// For 2x2 [[a,b],[c,d]], inverse = (1/det)*[[d,-b],[-c,a]]
|
|
662
|
+
// J^{-1} = (1/det) * [[-dy2, dx2], [-dy1, dx1]]
|
|
663
|
+
// J^{-1} * f = (1/det) * [-dy2*fx + dx2*fy, -dy1*fx + dx1*fy]
|
|
664
|
+
// Newton update: t_new = t - J^{-1}*f
|
|
665
|
+
const dt1 = dx2.times(fy).minus(dy2.times(fx)).div(det); // -dy2*fx + dx2*fy
|
|
666
|
+
const dt2 = dx1.times(fy).minus(dy1.times(fx)).div(det); // -dy1*fx + dx1*fy
|
|
667
|
+
|
|
668
|
+
t1 = t1.minus(dt1);
|
|
669
|
+
t2 = t2.minus(dt2);
|
|
670
|
+
|
|
671
|
+
// Check step size convergence
|
|
672
|
+
if (dt1.abs().lt(tol) && dt2.abs().lt(tol)) {
|
|
673
|
+
// Recompute final error
|
|
674
|
+
const [finalX1, finalY1] = bezierPoint(bezier, t1);
|
|
675
|
+
const [finalX2, finalY2] = bezierPoint(bezier, t2);
|
|
676
|
+
const finalError = finalX1.minus(finalX2).pow(2)
|
|
677
|
+
.plus(finalY1.minus(finalY2).pow(2)).sqrt();
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
t1: Decimal.min(t1, t2),
|
|
681
|
+
t2: Decimal.max(t1, t2),
|
|
682
|
+
point: [finalX1.plus(finalX2).div(2), finalY1.plus(finalY2).div(2)],
|
|
683
|
+
error: finalError
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return null; // Failed to converge
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// PATH INTERSECTION
|
|
693
|
+
// ============================================================================
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Find all intersections between two paths.
|
|
697
|
+
*
|
|
698
|
+
* @param {Array} path1 - Array of Bezier segments
|
|
699
|
+
* @param {Array} path2 - Array of Bezier segments
|
|
700
|
+
* @param {Object} [options] - Options
|
|
701
|
+
* @returns {Array} Intersections with segment indices
|
|
702
|
+
*/
|
|
703
|
+
export function pathPathIntersection(path1, path2, options = {}) {
|
|
704
|
+
// INPUT VALIDATION
|
|
705
|
+
// WHY: Prevent cryptic errors from undefined/null paths. Fail fast with clear messages.
|
|
706
|
+
if (!path1 || !Array.isArray(path1)) {
|
|
707
|
+
throw new Error('pathPathIntersection: path1 must be an array');
|
|
708
|
+
}
|
|
709
|
+
if (!path2 || !Array.isArray(path2)) {
|
|
710
|
+
throw new Error('pathPathIntersection: path2 must be an array');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Handle empty paths gracefully
|
|
714
|
+
// WHY: Empty paths have no intersections by definition
|
|
715
|
+
if (path1.length === 0 || path2.length === 0) {
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const results = [];
|
|
720
|
+
|
|
721
|
+
for (let i = 0; i < path1.length; i++) {
|
|
722
|
+
for (let j = 0; j < path2.length; j++) {
|
|
723
|
+
const isects = bezierBezierIntersection(path1[i], path2[j], options);
|
|
724
|
+
|
|
725
|
+
for (const isect of isects) {
|
|
726
|
+
results.push({
|
|
727
|
+
segment1: i,
|
|
728
|
+
segment2: j,
|
|
729
|
+
t1: isect.t1,
|
|
730
|
+
t2: isect.t2,
|
|
731
|
+
point: isect.point
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return results;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Find self-intersections of a path (all segments against each other).
|
|
742
|
+
*
|
|
743
|
+
* @param {Array} path - Array of Bezier segments
|
|
744
|
+
* @param {Object} [options] - Options
|
|
745
|
+
* @returns {Array} Self-intersections with segment indices
|
|
746
|
+
*/
|
|
747
|
+
export function pathSelfIntersection(path, options = {}) {
|
|
748
|
+
// INPUT VALIDATION
|
|
749
|
+
// WHY: Prevent cryptic errors from undefined/null path. Fail fast with clear messages.
|
|
750
|
+
if (!path || !Array.isArray(path)) {
|
|
751
|
+
throw new Error('pathSelfIntersection: path must be an array');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Handle empty or single-segment paths
|
|
755
|
+
// WHY: Single segment path can only have self-intersections within that segment
|
|
756
|
+
if (path.length === 0) {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const results = [];
|
|
761
|
+
|
|
762
|
+
// Check each segment for self-intersection
|
|
763
|
+
for (let i = 0; i < path.length; i++) {
|
|
764
|
+
const selfIsects = bezierSelfIntersection(path[i], options);
|
|
765
|
+
for (const isect of selfIsects) {
|
|
766
|
+
results.push({
|
|
767
|
+
segment1: i,
|
|
768
|
+
segment2: i,
|
|
769
|
+
t1: isect.t1,
|
|
770
|
+
t2: isect.t2,
|
|
771
|
+
point: isect.point
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Check pairs of non-adjacent segments
|
|
777
|
+
for (let i = 0; i < path.length; i++) {
|
|
778
|
+
for (let j = i + 2; j < path.length; j++) {
|
|
779
|
+
// WHY: j starts at i+2, so segments i and j are never adjacent (which would be i and i+1)
|
|
780
|
+
// However, for closed paths, first (i=0) and last (j=path.length-1) segments ARE adjacent
|
|
781
|
+
// because they share the start/end vertex. Skip this pair.
|
|
782
|
+
const isClosedPathAdjacent = (i === 0 && j === path.length - 1);
|
|
783
|
+
if (isClosedPathAdjacent) continue;
|
|
784
|
+
|
|
785
|
+
const isects = bezierBezierIntersection(path[i], path[j], options);
|
|
786
|
+
|
|
787
|
+
for (const isect of isects) {
|
|
788
|
+
results.push({
|
|
789
|
+
segment1: i,
|
|
790
|
+
segment2: j,
|
|
791
|
+
t1: isect.t1,
|
|
792
|
+
t2: isect.t2,
|
|
793
|
+
point: isect.point
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return results;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// VERIFICATION (INVERSE OPERATIONS)
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Verify an intersection is correct by checking point lies on both curves.
|
|
808
|
+
*
|
|
809
|
+
* @param {Array} bez1 - First Bezier
|
|
810
|
+
* @param {Array} bez2 - Second Bezier
|
|
811
|
+
* @param {Object} intersection - Intersection to verify
|
|
812
|
+
* @param {string} [tolerance='1e-20'] - Verification tolerance
|
|
813
|
+
* @returns {{valid: boolean, distance: Decimal}}
|
|
814
|
+
*/
|
|
815
|
+
export function verifyIntersection(bez1, bez2, intersection, tolerance = '1e-20') {
|
|
816
|
+
// INPUT VALIDATION
|
|
817
|
+
// WHY: Ensure all required data is present before computation. Prevents undefined errors.
|
|
818
|
+
if (!bez1 || !Array.isArray(bez1) || bez1.length < 2) {
|
|
819
|
+
throw new Error('verifyIntersection: bez1 must have at least 2 control points');
|
|
820
|
+
}
|
|
821
|
+
if (!bez2 || !Array.isArray(bez2) || bez2.length < 2) {
|
|
822
|
+
throw new Error('verifyIntersection: bez2 must have at least 2 control points');
|
|
823
|
+
}
|
|
824
|
+
if (!intersection) {
|
|
825
|
+
throw new Error('verifyIntersection: intersection object is required');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const tol = D(tolerance);
|
|
829
|
+
|
|
830
|
+
const [x1, y1] = bezierPoint(bez1, intersection.t1);
|
|
831
|
+
const [x2, y2] = bezierPoint(bez2, intersection.t2);
|
|
832
|
+
|
|
833
|
+
const distance = x1.minus(x2).pow(2).plus(y1.minus(y2).pow(2)).sqrt();
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
valid: distance.lt(tol),
|
|
837
|
+
distance,
|
|
838
|
+
point1: [x1, y1],
|
|
839
|
+
point2: [x2, y2]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Verify line-line intersection by checking:
|
|
845
|
+
* 1. Point lies on both lines (parametric check)
|
|
846
|
+
* 2. Point satisfies both line equations (algebraic check)
|
|
847
|
+
* 3. Cross product verification
|
|
848
|
+
*
|
|
849
|
+
* @param {Array} line1 - First line [[x0,y0], [x1,y1]]
|
|
850
|
+
* @param {Array} line2 - Second line [[x0,y0], [x1,y1]]
|
|
851
|
+
* @param {Object} intersection - Intersection result
|
|
852
|
+
* @param {string} [tolerance='1e-40'] - Verification tolerance
|
|
853
|
+
* @returns {{valid: boolean, parametricError1: Decimal, parametricError2: Decimal, algebraicError: Decimal, crossProductError: Decimal}}
|
|
854
|
+
*/
|
|
855
|
+
export function verifyLineLineIntersection(line1, line2, intersection, tolerance = '1e-40') {
|
|
856
|
+
// INPUT VALIDATION
|
|
857
|
+
// WHY: Verify all required inputs before processing. Fail fast with clear error messages.
|
|
858
|
+
if (!line1 || !Array.isArray(line1) || line1.length !== 2) {
|
|
859
|
+
throw new Error('verifyLineLineIntersection: line1 must be an array of 2 points');
|
|
860
|
+
}
|
|
861
|
+
if (!line2 || !Array.isArray(line2) || line2.length !== 2) {
|
|
862
|
+
throw new Error('verifyLineLineIntersection: line2 must be an array of 2 points');
|
|
863
|
+
}
|
|
864
|
+
if (!intersection) {
|
|
865
|
+
throw new Error('verifyLineLineIntersection: intersection object is required');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const tol = D(tolerance);
|
|
869
|
+
|
|
870
|
+
if (!intersection.t1) {
|
|
871
|
+
return { valid: false, reason: 'No intersection provided' };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const [x1, y1] = [D(line1[0][0]), D(line1[0][1])];
|
|
875
|
+
const [x2, y2] = [D(line1[1][0]), D(line1[1][1])];
|
|
876
|
+
const [x3, y3] = [D(line2[0][0]), D(line2[0][1])];
|
|
877
|
+
const [x4, y4] = [D(line2[1][0]), D(line2[1][1])];
|
|
878
|
+
|
|
879
|
+
const t1 = D(intersection.t1);
|
|
880
|
+
const t2 = D(intersection.t2);
|
|
881
|
+
const [px, py] = [D(intersection.point[0]), D(intersection.point[1])];
|
|
882
|
+
|
|
883
|
+
// 1. Parametric verification: compute point from both lines
|
|
884
|
+
const p1x = x1.plus(x2.minus(x1).times(t1));
|
|
885
|
+
const p1y = y1.plus(y2.minus(y1).times(t1));
|
|
886
|
+
const p2x = x3.plus(x4.minus(x3).times(t2));
|
|
887
|
+
const p2y = y3.plus(y4.minus(y3).times(t2));
|
|
888
|
+
|
|
889
|
+
const parametricError1 = px.minus(p1x).pow(2).plus(py.minus(p1y).pow(2)).sqrt();
|
|
890
|
+
const parametricError2 = px.minus(p2x).pow(2).plus(py.minus(p2y).pow(2)).sqrt();
|
|
891
|
+
const pointMismatchError = p1x.minus(p2x).pow(2).plus(p1y.minus(p2y).pow(2)).sqrt();
|
|
892
|
+
|
|
893
|
+
// 2. Algebraic verification: substitute into line equations
|
|
894
|
+
// Line 1: (y - y1) / (y2 - y1) = (x - x1) / (x2 - x1)
|
|
895
|
+
// Cross-multiply: (y - y1)(x2 - x1) = (x - x1)(y2 - y1)
|
|
896
|
+
const algebraicError1 = py.minus(y1).times(x2.minus(x1))
|
|
897
|
+
.minus(px.minus(x1).times(y2.minus(y1))).abs();
|
|
898
|
+
const algebraicError2 = py.minus(y3).times(x4.minus(x3))
|
|
899
|
+
.minus(px.minus(x3).times(y4.minus(y3))).abs();
|
|
900
|
+
|
|
901
|
+
// 3. Cross product verification: vectors from endpoints to intersection should be collinear
|
|
902
|
+
const v1x = px.minus(x1);
|
|
903
|
+
const v1y = py.minus(y1);
|
|
904
|
+
const v2x = x2.minus(x1);
|
|
905
|
+
const v2y = y2.minus(y1);
|
|
906
|
+
const crossProduct1 = v1x.times(v2y).minus(v1y.times(v2x)).abs();
|
|
907
|
+
|
|
908
|
+
const v3x = px.minus(x3);
|
|
909
|
+
const v3y = py.minus(y3);
|
|
910
|
+
const v4x = x4.minus(x3);
|
|
911
|
+
const v4y = y4.minus(y3);
|
|
912
|
+
const crossProduct2 = v3x.times(v4y).minus(v3y.times(v4x)).abs();
|
|
913
|
+
|
|
914
|
+
const maxError = Decimal.max(
|
|
915
|
+
parametricError1, parametricError2, pointMismatchError,
|
|
916
|
+
algebraicError1, algebraicError2, crossProduct1, crossProduct2
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
valid: maxError.lt(tol),
|
|
921
|
+
parametricError1,
|
|
922
|
+
parametricError2,
|
|
923
|
+
pointMismatchError,
|
|
924
|
+
algebraicError1,
|
|
925
|
+
algebraicError2,
|
|
926
|
+
crossProduct1,
|
|
927
|
+
crossProduct2,
|
|
928
|
+
maxError
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Verify bezier-line intersection by checking:
|
|
934
|
+
* 1. Point lies on the Bezier curve (evaluate at t1)
|
|
935
|
+
* 2. Point lies on the line (evaluate at t2)
|
|
936
|
+
* 3. Signed distance from line is zero
|
|
937
|
+
*
|
|
938
|
+
* @param {Array} bezier - Bezier control points
|
|
939
|
+
* @param {Array} line - Line segment [[x0,y0], [x1,y1]]
|
|
940
|
+
* @param {Object} intersection - Intersection result
|
|
941
|
+
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
942
|
+
* @returns {{valid: boolean, bezierError: Decimal, lineError: Decimal, signedDistance: Decimal}}
|
|
943
|
+
*/
|
|
944
|
+
export function verifyBezierLineIntersection(bezier, line, intersection, tolerance = '1e-30') {
|
|
945
|
+
// INPUT VALIDATION
|
|
946
|
+
// WHY: Ensure all required inputs are valid before verification. Prevents undefined behavior.
|
|
947
|
+
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
948
|
+
throw new Error('verifyBezierLineIntersection: bezier must have at least 2 control points');
|
|
949
|
+
}
|
|
950
|
+
if (!line || !Array.isArray(line) || line.length !== 2) {
|
|
951
|
+
throw new Error('verifyBezierLineIntersection: line must be an array of 2 points');
|
|
952
|
+
}
|
|
953
|
+
if (!intersection) {
|
|
954
|
+
throw new Error('verifyBezierLineIntersection: intersection object is required');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const tol = D(tolerance);
|
|
958
|
+
|
|
959
|
+
if (intersection.t1 === undefined) {
|
|
960
|
+
return { valid: false, reason: 'No intersection provided' };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const t1 = D(intersection.t1);
|
|
964
|
+
const t2 = D(intersection.t2);
|
|
965
|
+
const [px, py] = [D(intersection.point[0]), D(intersection.point[1])];
|
|
966
|
+
|
|
967
|
+
const [lx0, ly0] = [D(line[0][0]), D(line[0][1])];
|
|
968
|
+
const [lx1, ly1] = [D(line[1][0]), D(line[1][1])];
|
|
969
|
+
|
|
970
|
+
// 1. Verify point on Bezier
|
|
971
|
+
const [bx, by] = bezierPoint(bezier, t1);
|
|
972
|
+
const bezierError = px.minus(bx).pow(2).plus(py.minus(by).pow(2)).sqrt();
|
|
973
|
+
|
|
974
|
+
// 2. Verify point on line (parametric)
|
|
975
|
+
const dlx = lx1.minus(lx0);
|
|
976
|
+
const dly = ly1.minus(ly0);
|
|
977
|
+
const expectedLineX = lx0.plus(dlx.times(t2));
|
|
978
|
+
const expectedLineY = ly0.plus(dly.times(t2));
|
|
979
|
+
const lineError = px.minus(expectedLineX).pow(2).plus(py.minus(expectedLineY).pow(2)).sqrt();
|
|
980
|
+
|
|
981
|
+
// 3. Signed distance from line
|
|
982
|
+
// dist = ((y - ly0) * dlx - (x - lx0) * dly) / sqrt(dlx^2 + dly^2)
|
|
983
|
+
const lineLen = dlx.pow(2).plus(dly.pow(2)).sqrt();
|
|
984
|
+
const signedDistance = lineLen.isZero() ? D(0) :
|
|
985
|
+
py.minus(ly0).times(dlx).minus(px.minus(lx0).times(dly)).div(lineLen).abs();
|
|
986
|
+
|
|
987
|
+
// 4. Verify bezier point matches line point
|
|
988
|
+
const pointMismatch = bx.minus(expectedLineX).pow(2).plus(by.minus(expectedLineY).pow(2)).sqrt();
|
|
989
|
+
|
|
990
|
+
const maxError = Decimal.max(bezierError, lineError, signedDistance, pointMismatch);
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
valid: maxError.lt(tol),
|
|
994
|
+
bezierError,
|
|
995
|
+
lineError,
|
|
996
|
+
signedDistance,
|
|
997
|
+
pointMismatch,
|
|
998
|
+
bezierPoint: [bx, by],
|
|
999
|
+
linePoint: [expectedLineX, expectedLineY],
|
|
1000
|
+
maxError
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Verify bezier-bezier intersection by checking:
|
|
1006
|
+
* 1. Point lies on both curves
|
|
1007
|
+
* 2. Distance between points on both curves is minimal
|
|
1008
|
+
* 3. Newton refinement converges (inverse check)
|
|
1009
|
+
*
|
|
1010
|
+
* @param {Array} bez1 - First Bezier
|
|
1011
|
+
* @param {Array} bez2 - Second Bezier
|
|
1012
|
+
* @param {Object} intersection - Intersection result
|
|
1013
|
+
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1014
|
+
* @returns {{valid: boolean, distance: Decimal, point1: Array, point2: Array, refinementConverged: boolean}}
|
|
1015
|
+
*/
|
|
1016
|
+
export function verifyBezierBezierIntersection(bez1, bez2, intersection, tolerance = '1e-30') {
|
|
1017
|
+
// INPUT VALIDATION
|
|
1018
|
+
// WHY: Validate inputs before verification to prevent unexpected errors from invalid data.
|
|
1019
|
+
if (!bez1 || !Array.isArray(bez1) || bez1.length < 2) {
|
|
1020
|
+
throw new Error('verifyBezierBezierIntersection: bez1 must have at least 2 control points');
|
|
1021
|
+
}
|
|
1022
|
+
if (!bez2 || !Array.isArray(bez2) || bez2.length < 2) {
|
|
1023
|
+
throw new Error('verifyBezierBezierIntersection: bez2 must have at least 2 control points');
|
|
1024
|
+
}
|
|
1025
|
+
if (!intersection) {
|
|
1026
|
+
throw new Error('verifyBezierBezierIntersection: intersection object is required');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const tol = D(tolerance);
|
|
1030
|
+
|
|
1031
|
+
if (intersection.t1 === undefined) {
|
|
1032
|
+
return { valid: false, reason: 'No intersection provided' };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const t1 = D(intersection.t1);
|
|
1036
|
+
const t2 = D(intersection.t2);
|
|
1037
|
+
|
|
1038
|
+
// 1. Evaluate both curves at their parameter values
|
|
1039
|
+
const [x1, y1] = bezierPoint(bez1, t1);
|
|
1040
|
+
const [x2, y2] = bezierPoint(bez2, t2);
|
|
1041
|
+
|
|
1042
|
+
const distance = x1.minus(x2).pow(2).plus(y1.minus(y2).pow(2)).sqrt();
|
|
1043
|
+
|
|
1044
|
+
// 2. Check reported point matches computed points
|
|
1045
|
+
let reportedPointError = D(0);
|
|
1046
|
+
if (intersection.point) {
|
|
1047
|
+
const [px, py] = [D(intersection.point[0]), D(intersection.point[1])];
|
|
1048
|
+
const err1 = px.minus(x1).pow(2).plus(py.minus(y1).pow(2)).sqrt();
|
|
1049
|
+
const err2 = px.minus(x2).pow(2).plus(py.minus(y2).pow(2)).sqrt();
|
|
1050
|
+
reportedPointError = Decimal.max(err1, err2);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 3. Verify by attempting Newton refinement from nearby starting points
|
|
1054
|
+
// If intersection is real, perturbations should converge back
|
|
1055
|
+
let refinementConverged = true;
|
|
1056
|
+
const perturbations = [
|
|
1057
|
+
[D('0.001'), D(0)],
|
|
1058
|
+
[D('-0.001'), D(0)],
|
|
1059
|
+
[D(0), D('0.001')],
|
|
1060
|
+
[D(0), D('-0.001')]
|
|
1061
|
+
];
|
|
1062
|
+
|
|
1063
|
+
for (const [dt1, dt2] of perturbations) {
|
|
1064
|
+
const newT1 = Decimal.max(D(0), Decimal.min(D(1), t1.plus(dt1)));
|
|
1065
|
+
const newT2 = Decimal.max(D(0), Decimal.min(D(1), t2.plus(dt2)));
|
|
1066
|
+
|
|
1067
|
+
// Simple gradient descent check
|
|
1068
|
+
const [nx1, ny1] = bezierPoint(bez1, newT1);
|
|
1069
|
+
const [nx2, ny2] = bezierPoint(bez2, newT2);
|
|
1070
|
+
const newDist = nx1.minus(nx2).pow(2).plus(ny1.minus(ny2).pow(2)).sqrt();
|
|
1071
|
+
|
|
1072
|
+
// Perturbed point should have larger or similar distance
|
|
1073
|
+
// (intersection is local minimum)
|
|
1074
|
+
if (newDist.lt(distance.minus(tol.times(10)))) {
|
|
1075
|
+
// Found a better point - intersection might not be optimal
|
|
1076
|
+
refinementConverged = false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// 4. Parameter bounds check
|
|
1081
|
+
const t1InBounds = t1.gte(0) && t1.lte(1);
|
|
1082
|
+
const t2InBounds = t2.gte(0) && t2.lte(1);
|
|
1083
|
+
|
|
1084
|
+
const maxError = Decimal.max(distance, reportedPointError);
|
|
1085
|
+
|
|
1086
|
+
return {
|
|
1087
|
+
valid: maxError.lt(tol) && t1InBounds && t2InBounds,
|
|
1088
|
+
distance,
|
|
1089
|
+
reportedPointError,
|
|
1090
|
+
point1: [x1, y1],
|
|
1091
|
+
point2: [x2, y2],
|
|
1092
|
+
refinementConverged,
|
|
1093
|
+
t1InBounds,
|
|
1094
|
+
t2InBounds,
|
|
1095
|
+
maxError
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Verify self-intersection by checking:
|
|
1101
|
+
* 1. Both parameters map to the same point
|
|
1102
|
+
* 2. Parameters are sufficiently separated (not just same point twice)
|
|
1103
|
+
* 3. Intersection is geometrically valid
|
|
1104
|
+
*
|
|
1105
|
+
* @param {Array} bezier - Bezier control points
|
|
1106
|
+
* @param {Object} intersection - Self-intersection result
|
|
1107
|
+
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1108
|
+
* @param {string} [minSeparation='0.01'] - Minimum parameter separation
|
|
1109
|
+
* @returns {{valid: boolean, distance: Decimal, separation: Decimal}}
|
|
1110
|
+
*/
|
|
1111
|
+
export function verifySelfIntersection(bezier, intersection, tolerance = '1e-30', minSeparation = '0.01') {
|
|
1112
|
+
// INPUT VALIDATION
|
|
1113
|
+
// WHY: Ensure curve and intersection data are valid before attempting verification.
|
|
1114
|
+
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
1115
|
+
throw new Error('verifySelfIntersection: bezier must have at least 2 control points');
|
|
1116
|
+
}
|
|
1117
|
+
if (!intersection) {
|
|
1118
|
+
throw new Error('verifySelfIntersection: intersection object is required');
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const tol = D(tolerance);
|
|
1122
|
+
const minSep = D(minSeparation);
|
|
1123
|
+
|
|
1124
|
+
if (intersection.t1 === undefined) {
|
|
1125
|
+
return { valid: false, reason: 'No intersection provided' };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const t1 = D(intersection.t1);
|
|
1129
|
+
const t2 = D(intersection.t2);
|
|
1130
|
+
|
|
1131
|
+
// 1. Evaluate curve at both parameters
|
|
1132
|
+
const [x1, y1] = bezierPoint(bezier, t1);
|
|
1133
|
+
const [x2, y2] = bezierPoint(bezier, t2);
|
|
1134
|
+
|
|
1135
|
+
const distance = x1.minus(x2).pow(2).plus(y1.minus(y2).pow(2)).sqrt();
|
|
1136
|
+
|
|
1137
|
+
// 2. Check parameter separation
|
|
1138
|
+
const separation = t2.minus(t1).abs();
|
|
1139
|
+
const sufficientSeparation = separation.gte(minSep);
|
|
1140
|
+
|
|
1141
|
+
// 3. Check both parameters are in valid range
|
|
1142
|
+
const t1InBounds = t1.gte(0) && t1.lte(1);
|
|
1143
|
+
const t2InBounds = t2.gte(0) && t2.lte(1);
|
|
1144
|
+
|
|
1145
|
+
// 4. Verify ordering (t1 < t2 by convention)
|
|
1146
|
+
const properlyOrdered = t1.lt(t2);
|
|
1147
|
+
|
|
1148
|
+
// 5. Verify by sampling nearby - true self-intersection is stable
|
|
1149
|
+
let stableIntersection = true;
|
|
1150
|
+
const epsilon = D('0.0001');
|
|
1151
|
+
|
|
1152
|
+
// Sample points slightly before and after each parameter
|
|
1153
|
+
const [xBefore1, yBefore1] = bezierPoint(bezier, Decimal.max(D(0), t1.minus(epsilon)));
|
|
1154
|
+
const [xAfter1, yAfter1] = bezierPoint(bezier, Decimal.min(D(1), t1.plus(epsilon)));
|
|
1155
|
+
const [xBefore2, yBefore2] = bezierPoint(bezier, Decimal.max(D(0), t2.minus(epsilon)));
|
|
1156
|
+
const [xAfter2, yAfter2] = bezierPoint(bezier, Decimal.min(D(1), t2.plus(epsilon)));
|
|
1157
|
+
|
|
1158
|
+
// The curve portions should cross (distances should increase on both sides)
|
|
1159
|
+
const distBefore = xBefore1.minus(xBefore2).pow(2).plus(yBefore1.minus(yBefore2).pow(2)).sqrt();
|
|
1160
|
+
const distAfter = xAfter1.minus(xAfter2).pow(2).plus(yAfter1.minus(yAfter2).pow(2)).sqrt();
|
|
1161
|
+
|
|
1162
|
+
// Both neighboring distances should be larger than intersection distance
|
|
1163
|
+
if (!distBefore.gt(distance.minus(tol)) || !distAfter.gt(distance.minus(tol))) {
|
|
1164
|
+
stableIntersection = false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return {
|
|
1168
|
+
valid: distance.lt(tol) && sufficientSeparation && t1InBounds && t2InBounds && properlyOrdered,
|
|
1169
|
+
distance,
|
|
1170
|
+
separation,
|
|
1171
|
+
sufficientSeparation,
|
|
1172
|
+
t1InBounds,
|
|
1173
|
+
t2InBounds,
|
|
1174
|
+
properlyOrdered,
|
|
1175
|
+
stableIntersection,
|
|
1176
|
+
point1: [x1, y1],
|
|
1177
|
+
point2: [x2, y2]
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Verify path-path intersection results.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {Array} path1 - First path (array of Bezier segments)
|
|
1185
|
+
* @param {Array} path2 - Second path (array of Bezier segments)
|
|
1186
|
+
* @param {Array} intersections - Array of intersection results
|
|
1187
|
+
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1188
|
+
* @returns {{valid: boolean, results: Array, invalidCount: number}}
|
|
1189
|
+
*/
|
|
1190
|
+
export function verifyPathPathIntersection(path1, path2, intersections, tolerance = '1e-30') {
|
|
1191
|
+
// INPUT VALIDATION
|
|
1192
|
+
// WHY: Validate all inputs before processing to ensure meaningful error messages.
|
|
1193
|
+
if (!path1 || !Array.isArray(path1)) {
|
|
1194
|
+
throw new Error('verifyPathPathIntersection: path1 must be an array');
|
|
1195
|
+
}
|
|
1196
|
+
if (!path2 || !Array.isArray(path2)) {
|
|
1197
|
+
throw new Error('verifyPathPathIntersection: path2 must be an array');
|
|
1198
|
+
}
|
|
1199
|
+
if (!intersections || !Array.isArray(intersections)) {
|
|
1200
|
+
throw new Error('verifyPathPathIntersection: intersections must be an array');
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const results = [];
|
|
1204
|
+
let invalidCount = 0;
|
|
1205
|
+
|
|
1206
|
+
for (const isect of intersections) {
|
|
1207
|
+
const seg1 = path1[isect.segment1];
|
|
1208
|
+
const seg2 = path2[isect.segment2];
|
|
1209
|
+
|
|
1210
|
+
if (!seg1 || !seg2) {
|
|
1211
|
+
results.push({ valid: false, reason: 'Invalid segment index' });
|
|
1212
|
+
invalidCount++;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const verification = verifyBezierBezierIntersection(seg1, seg2, isect, tolerance);
|
|
1217
|
+
results.push(verification);
|
|
1218
|
+
|
|
1219
|
+
if (!verification.valid) {
|
|
1220
|
+
invalidCount++;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
valid: invalidCount === 0,
|
|
1226
|
+
results,
|
|
1227
|
+
invalidCount,
|
|
1228
|
+
totalIntersections: intersections.length
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Comprehensive verification for all intersection functions.
|
|
1234
|
+
* Tests all types with sample curves and validates results.
|
|
1235
|
+
*
|
|
1236
|
+
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1237
|
+
* @returns {{allPassed: boolean, results: Object}}
|
|
1238
|
+
*/
|
|
1239
|
+
export function verifyAllIntersectionFunctions(tolerance = '1e-30') {
|
|
1240
|
+
const results = {};
|
|
1241
|
+
let allPassed = true;
|
|
1242
|
+
|
|
1243
|
+
// Test 1: Line-line intersection
|
|
1244
|
+
const line1 = [[0, 0], [2, 2]];
|
|
1245
|
+
const line2 = [[0, 2], [2, 0]];
|
|
1246
|
+
const lineIsects = lineLineIntersection(line1, line2);
|
|
1247
|
+
|
|
1248
|
+
if (lineIsects.length > 0) {
|
|
1249
|
+
const lineVerify = verifyLineLineIntersection(line1, line2, lineIsects[0], tolerance);
|
|
1250
|
+
results.lineLine = lineVerify;
|
|
1251
|
+
if (!lineVerify.valid) allPassed = false;
|
|
1252
|
+
} else {
|
|
1253
|
+
// WHY: These specific test lines (diagonal from [0,0] to [2,2] and [0,2] to [2,0])
|
|
1254
|
+
// geometrically MUST intersect at [1,1]. No intersection indicates a bug.
|
|
1255
|
+
results.lineLine = { valid: false, reason: 'No intersection found for lines that geometrically must intersect at [1,1]' };
|
|
1256
|
+
allPassed = false;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Test 2: Bezier-line intersection
|
|
1260
|
+
const cubic = [[0, 0], [0.5, 2], [1.5, 2], [2, 0]];
|
|
1261
|
+
const horizLine = [[0, 1], [2, 1]];
|
|
1262
|
+
const bezLineIsects = bezierLineIntersection(cubic, horizLine);
|
|
1263
|
+
|
|
1264
|
+
if (bezLineIsects.length > 0) {
|
|
1265
|
+
let allValid = true;
|
|
1266
|
+
const verifications = [];
|
|
1267
|
+
for (const isect of bezLineIsects) {
|
|
1268
|
+
const v = verifyBezierLineIntersection(cubic, horizLine, isect, tolerance);
|
|
1269
|
+
verifications.push(v);
|
|
1270
|
+
if (!v.valid) allValid = false;
|
|
1271
|
+
}
|
|
1272
|
+
results.bezierLine = { valid: allValid, intersectionCount: bezLineIsects.length, verifications };
|
|
1273
|
+
if (!allValid) allPassed = false;
|
|
1274
|
+
} else {
|
|
1275
|
+
results.bezierLine = { valid: false, reason: 'No intersection found' };
|
|
1276
|
+
allPassed = false;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Test 3: Bezier-bezier intersection
|
|
1280
|
+
// WHY: These specific curves may or may not intersect depending on their geometry.
|
|
1281
|
+
// An empty result is valid if the curves don't actually cross. This is not a failure condition.
|
|
1282
|
+
const cubic1 = [[0, 0], [1, 2], [2, 2], [3, 0]];
|
|
1283
|
+
const cubic2 = [[0, 1], [1, -1], [2, 3], [3, 1]];
|
|
1284
|
+
const bezBezIsects = bezierBezierIntersection(cubic1, cubic2);
|
|
1285
|
+
|
|
1286
|
+
if (bezBezIsects.length > 0) {
|
|
1287
|
+
let allValid = true;
|
|
1288
|
+
const verifications = [];
|
|
1289
|
+
for (const isect of bezBezIsects) {
|
|
1290
|
+
const v = verifyBezierBezierIntersection(cubic1, cubic2, isect, tolerance);
|
|
1291
|
+
verifications.push(v);
|
|
1292
|
+
if (!v.valid) allValid = false;
|
|
1293
|
+
}
|
|
1294
|
+
results.bezierBezier = { valid: allValid, intersectionCount: bezBezIsects.length, verifications };
|
|
1295
|
+
if (!allValid) allPassed = false;
|
|
1296
|
+
} else {
|
|
1297
|
+
// WHY: No intersection is not an error - it's a valid result when curves don't cross.
|
|
1298
|
+
// We mark it as valid since the function is working correctly.
|
|
1299
|
+
results.bezierBezier = { valid: true, intersectionCount: 0, note: 'No intersections (may be geometrically correct)' };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Test 4: Self-intersection (use a loop curve)
|
|
1303
|
+
const loopCurve = [[0, 0], [2, 2], [0, 2], [2, 0]]; // Figure-8 shape
|
|
1304
|
+
const selfIsects = bezierSelfIntersection(loopCurve);
|
|
1305
|
+
|
|
1306
|
+
if (selfIsects.length > 0) {
|
|
1307
|
+
let allValid = true;
|
|
1308
|
+
const verifications = [];
|
|
1309
|
+
for (const isect of selfIsects) {
|
|
1310
|
+
const v = verifySelfIntersection(loopCurve, isect, tolerance);
|
|
1311
|
+
verifications.push(v);
|
|
1312
|
+
if (!v.valid) allValid = false;
|
|
1313
|
+
}
|
|
1314
|
+
results.selfIntersection = { valid: allValid, intersectionCount: selfIsects.length, verifications };
|
|
1315
|
+
if (!allValid) allPassed = false;
|
|
1316
|
+
} else {
|
|
1317
|
+
// Self-intersection expected for this curve
|
|
1318
|
+
results.selfIntersection = { valid: true, intersectionCount: 0, note: 'No self-intersections found' };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Test 5: Path-path intersection
|
|
1322
|
+
const path1 = [cubic1];
|
|
1323
|
+
const path2 = [cubic2];
|
|
1324
|
+
const pathIsects = pathPathIntersection(path1, path2);
|
|
1325
|
+
|
|
1326
|
+
if (pathIsects.length > 0) {
|
|
1327
|
+
const pathVerify = verifyPathPathIntersection(path1, path2, pathIsects, tolerance);
|
|
1328
|
+
results.pathPath = pathVerify;
|
|
1329
|
+
if (!pathVerify.valid) allPassed = false;
|
|
1330
|
+
} else {
|
|
1331
|
+
results.pathPath = { valid: true, intersectionCount: 0, note: 'No path intersections' };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
allPassed,
|
|
1336
|
+
results
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// ============================================================================
|
|
1341
|
+
// EXPORTS
|
|
1342
|
+
// ============================================================================
|
|
1343
|
+
|
|
1344
|
+
export default {
|
|
1345
|
+
// Line-line
|
|
1346
|
+
lineLineIntersection,
|
|
1347
|
+
|
|
1348
|
+
// Bezier-line
|
|
1349
|
+
bezierLineIntersection,
|
|
1350
|
+
|
|
1351
|
+
// Bezier-Bezier
|
|
1352
|
+
bezierBezierIntersection,
|
|
1353
|
+
|
|
1354
|
+
// Self-intersection
|
|
1355
|
+
bezierSelfIntersection,
|
|
1356
|
+
|
|
1357
|
+
// Path intersections
|
|
1358
|
+
pathPathIntersection,
|
|
1359
|
+
pathSelfIntersection,
|
|
1360
|
+
|
|
1361
|
+
// Verification (inverse operations)
|
|
1362
|
+
verifyIntersection,
|
|
1363
|
+
verifyLineLineIntersection,
|
|
1364
|
+
verifyBezierLineIntersection,
|
|
1365
|
+
verifyBezierBezierIntersection,
|
|
1366
|
+
verifySelfIntersection,
|
|
1367
|
+
verifyPathPathIntersection,
|
|
1368
|
+
verifyAllIntersectionFunctions
|
|
1369
|
+
};
|