@emasoft/svg-matrix 1.0.27 → 1.0.29
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 +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
* @version 1.0.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import Decimal from
|
|
18
|
+
import Decimal from "decimal.js";
|
|
19
19
|
import {
|
|
20
20
|
bezierPoint,
|
|
21
21
|
bezierBoundingBox,
|
|
22
22
|
bezierSplit,
|
|
23
23
|
bezierCrop,
|
|
24
|
-
bezierDerivative
|
|
25
|
-
} from
|
|
24
|
+
bezierDerivative,
|
|
25
|
+
} from "./bezier-analysis.js";
|
|
26
26
|
|
|
27
27
|
Decimal.set({ precision: 80 });
|
|
28
28
|
|
|
29
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
29
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
30
30
|
|
|
31
31
|
// ============================================================================
|
|
32
32
|
// LINE-LINE INTERSECTION
|
|
@@ -35,10 +35,10 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
|
35
35
|
// Numerical thresholds (documented magic numbers)
|
|
36
36
|
// WHY: Centralizing magic numbers as constants improves maintainability and makes
|
|
37
37
|
// the code self-documenting. These thresholds were tuned for 80-digit precision arithmetic.
|
|
38
|
-
const PARALLEL_THRESHOLD =
|
|
39
|
-
const SINGULARITY_THRESHOLD =
|
|
40
|
-
const INTERSECTION_VERIFY_FACTOR = 100;
|
|
41
|
-
const DEDUP_TOLERANCE_FACTOR = 1000;
|
|
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
42
|
|
|
43
43
|
/** Maximum Newton iterations for intersection refinement */
|
|
44
44
|
const MAX_NEWTON_ITERATIONS = 30;
|
|
@@ -47,7 +47,7 @@ const MAX_NEWTON_ITERATIONS = 30;
|
|
|
47
47
|
const MAX_INTERSECTION_RECURSION_DEPTH = 50;
|
|
48
48
|
|
|
49
49
|
/** Minimum parameter separation for self-intersection detection */
|
|
50
|
-
const DEFAULT_MIN_SEPARATION =
|
|
50
|
+
const DEFAULT_MIN_SEPARATION = "0.01";
|
|
51
51
|
|
|
52
52
|
/** Maximum bisection iterations for bezier-line refinement */
|
|
53
53
|
const MAX_BISECTION_ITERATIONS = 100;
|
|
@@ -65,10 +65,10 @@ const MAX_BISECTION_ITERATIONS = 100;
|
|
|
65
65
|
export function lineLineIntersection(line1, line2) {
|
|
66
66
|
// Input validation
|
|
67
67
|
if (!line1 || !Array.isArray(line1) || line1.length !== 2) {
|
|
68
|
-
throw new Error(
|
|
68
|
+
throw new Error("lineLineIntersection: line1 must be an array of 2 points");
|
|
69
69
|
}
|
|
70
70
|
if (!line2 || !Array.isArray(line2) || line2.length !== 2) {
|
|
71
|
-
throw new Error(
|
|
71
|
+
throw new Error("lineLineIntersection: line2 must be an array of 2 points");
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
const [x1, y1] = [D(line1[0][0]), D(line1[0][1])];
|
|
@@ -102,11 +102,13 @@ export function lineLineIntersection(line1, line2) {
|
|
|
102
102
|
const px = x1.plus(dx1.times(t1));
|
|
103
103
|
const py = y1.plus(dy1.times(t1));
|
|
104
104
|
|
|
105
|
-
return [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
t1,
|
|
108
|
+
t2,
|
|
109
|
+
point: [px, py],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
return [];
|
|
@@ -130,15 +132,19 @@ export function lineLineIntersection(line1, line2) {
|
|
|
130
132
|
* @returns {Array} Intersections [{t1 (bezier), t2 (line), point}]
|
|
131
133
|
*/
|
|
132
134
|
export function bezierLineIntersection(bezier, line, options = {}) {
|
|
133
|
-
const { tolerance =
|
|
135
|
+
const { tolerance = "1e-30", samplesPerDegree = 20 } = options;
|
|
134
136
|
const tol = D(tolerance);
|
|
135
137
|
|
|
136
138
|
// Input validation
|
|
137
139
|
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
138
|
-
throw new Error(
|
|
140
|
+
throw new Error(
|
|
141
|
+
"bezierLineIntersection: bezier must have at least 2 control points",
|
|
142
|
+
);
|
|
139
143
|
}
|
|
140
144
|
if (!line || !Array.isArray(line) || line.length !== 2) {
|
|
141
|
-
throw new Error(
|
|
145
|
+
throw new Error(
|
|
146
|
+
"bezierLineIntersection: line must be an array of 2 points",
|
|
147
|
+
);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
const [lx0, ly0] = [D(line[0][0]), D(line[0][1])];
|
|
@@ -169,7 +175,7 @@ export function bezierLineIntersection(bezier, line, options = {}) {
|
|
|
169
175
|
|
|
170
176
|
// Distance from line (signed)
|
|
171
177
|
const dist = by.minus(ly0).times(dlx).minus(bx.minus(lx0).times(dly));
|
|
172
|
-
const sign = dist.isNegative() ? -1 :
|
|
178
|
+
const sign = dist.isNegative() ? -1 : dist.isZero() ? 0 : 1;
|
|
173
179
|
|
|
174
180
|
if (sign === 0) {
|
|
175
181
|
// Exactly on line
|
|
@@ -218,6 +224,12 @@ export function bezierLineIntersection(bezier, line, options = {}) {
|
|
|
218
224
|
|
|
219
225
|
/**
|
|
220
226
|
* Refine Bezier-line intersection using bisection.
|
|
227
|
+
* @param {Array} bezier - Bezier control points
|
|
228
|
+
* @param {Array} line - Line segment [[x0,y0], [x1,y1]]
|
|
229
|
+
* @param {Decimal} t0 - Start of bracket interval
|
|
230
|
+
* @param {Decimal} t1 - End of bracket interval
|
|
231
|
+
* @param {Decimal} tol - Tolerance for convergence
|
|
232
|
+
* @returns {Decimal} Refined parameter value
|
|
221
233
|
*/
|
|
222
234
|
function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
223
235
|
const [lx0, ly0] = [D(line[0][0]), D(line[0][1])];
|
|
@@ -228,13 +240,13 @@ function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
|
228
240
|
let lo = D(t0);
|
|
229
241
|
let hi = D(t1);
|
|
230
242
|
|
|
231
|
-
const evalDist = t => {
|
|
243
|
+
const evalDist = (t) => {
|
|
232
244
|
const [bx, by] = bezierPoint(bezier, t);
|
|
233
245
|
return by.minus(ly0).times(dlx).minus(bx.minus(lx0).times(dly));
|
|
234
246
|
};
|
|
235
247
|
|
|
236
248
|
let fLo = evalDist(lo);
|
|
237
|
-
let
|
|
249
|
+
let _fHi = evalDist(hi);
|
|
238
250
|
|
|
239
251
|
// WHY: Use named constant instead of magic number for clarity and maintainability
|
|
240
252
|
for (let i = 0; i < MAX_BISECTION_ITERATIONS; i++) {
|
|
@@ -245,12 +257,15 @@ function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
|
245
257
|
return mid;
|
|
246
258
|
}
|
|
247
259
|
|
|
248
|
-
if (
|
|
260
|
+
if (
|
|
261
|
+
(fLo.isNegative() && fMid.isNegative()) ||
|
|
262
|
+
(fLo.isPositive() && fMid.isPositive())
|
|
263
|
+
) {
|
|
249
264
|
lo = mid;
|
|
250
265
|
fLo = fMid;
|
|
251
266
|
} else {
|
|
252
267
|
hi = mid;
|
|
253
|
-
|
|
268
|
+
_fHi = fMid;
|
|
254
269
|
}
|
|
255
270
|
}
|
|
256
271
|
|
|
@@ -278,17 +293,19 @@ function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
|
278
293
|
*/
|
|
279
294
|
export function bezierBezierIntersection(bezier1, bezier2, options = {}) {
|
|
280
295
|
// WHY: Use named constant as default instead of hardcoded 50 for clarity
|
|
281
|
-
const {
|
|
282
|
-
|
|
283
|
-
maxDepth = MAX_INTERSECTION_RECURSION_DEPTH
|
|
284
|
-
} = options;
|
|
296
|
+
const { tolerance = "1e-30", maxDepth = MAX_INTERSECTION_RECURSION_DEPTH } =
|
|
297
|
+
options;
|
|
285
298
|
|
|
286
299
|
// Input validation
|
|
287
300
|
if (!bezier1 || !Array.isArray(bezier1) || bezier1.length < 2) {
|
|
288
|
-
throw new Error(
|
|
301
|
+
throw new Error(
|
|
302
|
+
"bezierBezierIntersection: bezier1 must have at least 2 control points",
|
|
303
|
+
);
|
|
289
304
|
}
|
|
290
305
|
if (!bezier2 || !Array.isArray(bezier2) || bezier2.length < 2) {
|
|
291
|
-
throw new Error(
|
|
306
|
+
throw new Error(
|
|
307
|
+
"bezierBezierIntersection: bezier2 must have at least 2 control points",
|
|
308
|
+
);
|
|
292
309
|
}
|
|
293
310
|
|
|
294
311
|
const tol = D(tolerance);
|
|
@@ -388,7 +405,7 @@ function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
|
388
405
|
t1: currentT1,
|
|
389
406
|
t2: currentT2,
|
|
390
407
|
point: [x1, y1],
|
|
391
|
-
error
|
|
408
|
+
error,
|
|
392
409
|
};
|
|
393
410
|
}
|
|
394
411
|
|
|
@@ -422,13 +439,16 @@ function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
|
422
439
|
// so it may not reflect the final accuracy. We need to recompute error for the converged parameters.
|
|
423
440
|
const [finalX, finalY] = bezierPoint(bez1, currentT1);
|
|
424
441
|
const [finalX2, finalY2] = bezierPoint(bez2, currentT2);
|
|
425
|
-
const finalError = D(finalX)
|
|
426
|
-
.
|
|
442
|
+
const finalError = D(finalX)
|
|
443
|
+
.minus(D(finalX2))
|
|
444
|
+
.pow(2)
|
|
445
|
+
.plus(D(finalY).minus(D(finalY2)).pow(2))
|
|
446
|
+
.sqrt();
|
|
427
447
|
return {
|
|
428
448
|
t1: currentT1,
|
|
429
449
|
t2: currentT2,
|
|
430
450
|
point: [finalX, finalY],
|
|
431
|
-
error: finalError
|
|
451
|
+
error: finalError,
|
|
432
452
|
};
|
|
433
453
|
}
|
|
434
454
|
}
|
|
@@ -438,6 +458,9 @@ function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
|
438
458
|
|
|
439
459
|
/**
|
|
440
460
|
* Check if two bounding boxes overlap.
|
|
461
|
+
* @param {Object} bbox1 - First bounding box with {xmin, xmax, ymin, ymax}
|
|
462
|
+
* @param {Object} bbox2 - Second bounding box with {xmin, xmax, ymin, ymax}
|
|
463
|
+
* @returns {boolean} True if bounding boxes overlap
|
|
441
464
|
*/
|
|
442
465
|
function bboxOverlap(bbox1, bbox2) {
|
|
443
466
|
// INPUT VALIDATION
|
|
@@ -446,14 +469,19 @@ function bboxOverlap(bbox1, bbox2) {
|
|
|
446
469
|
return false; // No overlap if either bbox is missing
|
|
447
470
|
}
|
|
448
471
|
|
|
449
|
-
return !(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
return !(
|
|
473
|
+
bbox1.xmax.lt(bbox2.xmin) ||
|
|
474
|
+
bbox1.xmin.gt(bbox2.xmax) ||
|
|
475
|
+
bbox1.ymax.lt(bbox2.ymin) ||
|
|
476
|
+
bbox1.ymin.gt(bbox2.ymax)
|
|
477
|
+
);
|
|
453
478
|
}
|
|
454
479
|
|
|
455
480
|
/**
|
|
456
481
|
* Remove duplicate intersections.
|
|
482
|
+
* @param {Array} intersections - Array of intersection objects with t1, t2 parameters
|
|
483
|
+
* @param {Decimal} tol - Tolerance for considering intersections duplicate
|
|
484
|
+
* @returns {Array} Array of unique intersections
|
|
457
485
|
*/
|
|
458
486
|
function deduplicateIntersections(intersections, tol) {
|
|
459
487
|
const result = [];
|
|
@@ -465,7 +493,10 @@ function deduplicateIntersections(intersections, tol) {
|
|
|
465
493
|
const dt1 = isect.t1.minus(existing.t1).abs();
|
|
466
494
|
const dt2 = isect.t2.minus(existing.t2).abs();
|
|
467
495
|
|
|
468
|
-
if (
|
|
496
|
+
if (
|
|
497
|
+
dt1.lt(tol.times(DEDUP_TOLERANCE_FACTOR)) &&
|
|
498
|
+
dt2.lt(tol.times(DEDUP_TOLERANCE_FACTOR))
|
|
499
|
+
) {
|
|
469
500
|
isDuplicate = true;
|
|
470
501
|
break;
|
|
471
502
|
}
|
|
@@ -498,13 +529,19 @@ function deduplicateIntersections(intersections, tol) {
|
|
|
498
529
|
*/
|
|
499
530
|
export function bezierSelfIntersection(bezier, options = {}) {
|
|
500
531
|
// WHY: Use named constants as defaults instead of hardcoded values for clarity
|
|
501
|
-
const {
|
|
532
|
+
const {
|
|
533
|
+
tolerance = "1e-30",
|
|
534
|
+
minSeparation = DEFAULT_MIN_SEPARATION,
|
|
535
|
+
maxDepth = 30,
|
|
536
|
+
} = options;
|
|
502
537
|
const tol = D(tolerance);
|
|
503
538
|
const minSep = D(minSeparation);
|
|
504
539
|
|
|
505
540
|
// Input validation
|
|
506
541
|
if (!bezier || bezier.length < 2) {
|
|
507
|
-
throw new Error(
|
|
542
|
+
throw new Error(
|
|
543
|
+
"bezierSelfIntersection: bezier must have at least 2 control points",
|
|
544
|
+
);
|
|
508
545
|
}
|
|
509
546
|
|
|
510
547
|
// Self-intersections only possible for cubic and higher
|
|
@@ -537,7 +574,10 @@ export function bezierSelfIntersection(bezier, options = {}) {
|
|
|
537
574
|
const rightPts = bezierCrop(bezier, tmid, tmax);
|
|
538
575
|
|
|
539
576
|
// Find intersections between left and right portions
|
|
540
|
-
const isects = bezierBezierIntersection(leftPts, rightPts, {
|
|
577
|
+
const isects = bezierBezierIntersection(leftPts, rightPts, {
|
|
578
|
+
tolerance,
|
|
579
|
+
maxDepth: maxDepth - depth,
|
|
580
|
+
});
|
|
541
581
|
|
|
542
582
|
for (const isect of isects) {
|
|
543
583
|
// Map from cropped parameter space [0,1] back to original range
|
|
@@ -552,7 +592,7 @@ export function bezierSelfIntersection(bezier, options = {}) {
|
|
|
552
592
|
results.push({
|
|
553
593
|
t1: Decimal.min(origT1, origT2),
|
|
554
594
|
t2: Decimal.max(origT1, origT2),
|
|
555
|
-
point: isect.point
|
|
595
|
+
point: isect.point,
|
|
556
596
|
});
|
|
557
597
|
}
|
|
558
598
|
}
|
|
@@ -569,7 +609,7 @@ export function bezierSelfIntersection(bezier, options = {}) {
|
|
|
569
609
|
// The recursive subdivision can find the same intersection from multiple branches,
|
|
570
610
|
// with slightly different parameter values. Use minSep as the dedup tolerance
|
|
571
611
|
// since intersections closer than minSep in parameter space are considered the same.
|
|
572
|
-
const dedupTol = minSep.div(10);
|
|
612
|
+
const dedupTol = minSep.div(10); // Use 1/10 of minSeparation for deduplication
|
|
573
613
|
const deduped = deduplicateIntersections(results, dedupTol);
|
|
574
614
|
|
|
575
615
|
// WHY: After finding rough intersections via subdivision on cropped curves,
|
|
@@ -577,7 +617,13 @@ export function bezierSelfIntersection(bezier, options = {}) {
|
|
|
577
617
|
// full precision because we're optimizing directly on the original parameters.
|
|
578
618
|
const refined = [];
|
|
579
619
|
for (const isect of deduped) {
|
|
580
|
-
const refinedIsect = refineSelfIntersection(
|
|
620
|
+
const refinedIsect = refineSelfIntersection(
|
|
621
|
+
bezier,
|
|
622
|
+
isect.t1,
|
|
623
|
+
isect.t2,
|
|
624
|
+
tol,
|
|
625
|
+
minSep,
|
|
626
|
+
);
|
|
581
627
|
if (refinedIsect) {
|
|
582
628
|
refined.push(refinedIsect);
|
|
583
629
|
} else {
|
|
@@ -614,8 +660,14 @@ function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
|
614
660
|
const mid = t1.plus(t2).div(2);
|
|
615
661
|
t1 = mid.minus(minSep.div(2));
|
|
616
662
|
t2 = mid.plus(minSep.div(2));
|
|
617
|
-
if (t1.lt(0)) {
|
|
618
|
-
|
|
663
|
+
if (t1.lt(0)) {
|
|
664
|
+
t1 = D(0);
|
|
665
|
+
t2 = minSep;
|
|
666
|
+
}
|
|
667
|
+
if (t2.gt(1)) {
|
|
668
|
+
t2 = D(1);
|
|
669
|
+
t1 = D(1).minus(minSep);
|
|
670
|
+
}
|
|
619
671
|
}
|
|
620
672
|
|
|
621
673
|
// Evaluate curve at both parameters
|
|
@@ -632,8 +684,8 @@ function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
|
632
684
|
return {
|
|
633
685
|
t1: Decimal.min(t1, t2),
|
|
634
686
|
t2: Decimal.max(t1, t2),
|
|
635
|
-
point: [x1.plus(x2).div(2), y1.plus(y2).div(2)],
|
|
636
|
-
error
|
|
687
|
+
point: [x1.plus(x2).div(2), y1.plus(y2).div(2)], // Average of both points
|
|
688
|
+
error,
|
|
637
689
|
};
|
|
638
690
|
}
|
|
639
691
|
|
|
@@ -649,9 +701,9 @@ function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
|
649
701
|
// Singular Jacobian - curves are nearly parallel at these points
|
|
650
702
|
// Try bisection step instead
|
|
651
703
|
if (fx.isNegative()) {
|
|
652
|
-
t1 = t1.plus(D(
|
|
704
|
+
t1 = t1.plus(D("0.0001"));
|
|
653
705
|
} else {
|
|
654
|
-
t2 = t2.minus(D(
|
|
706
|
+
t2 = t2.minus(D("0.0001"));
|
|
655
707
|
}
|
|
656
708
|
continue;
|
|
657
709
|
}
|
|
@@ -662,8 +714,8 @@ function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
|
662
714
|
// J^{-1} = (1/det) * [[-dy2, dx2], [-dy1, dx1]]
|
|
663
715
|
// J^{-1} * f = (1/det) * [-dy2*fx + dx2*fy, -dy1*fx + dx1*fy]
|
|
664
716
|
// Newton update: t_new = t - J^{-1}*f
|
|
665
|
-
const dt1 = dx2.times(fy).minus(dy2.times(fx)).div(det);
|
|
666
|
-
const dt2 = dx1.times(fy).minus(dy1.times(fx)).div(det);
|
|
717
|
+
const dt1 = dx2.times(fy).minus(dy2.times(fx)).div(det); // -dy2*fx + dx2*fy
|
|
718
|
+
const dt2 = dx1.times(fy).minus(dy1.times(fx)).div(det); // -dy1*fx + dx1*fy
|
|
667
719
|
|
|
668
720
|
t1 = t1.minus(dt1);
|
|
669
721
|
t2 = t2.minus(dt2);
|
|
@@ -673,19 +725,22 @@ function refineSelfIntersection(bezier, t1Init, t2Init, tol, minSep) {
|
|
|
673
725
|
// Recompute final error
|
|
674
726
|
const [finalX1, finalY1] = bezierPoint(bezier, t1);
|
|
675
727
|
const [finalX2, finalY2] = bezierPoint(bezier, t2);
|
|
676
|
-
const finalError = finalX1
|
|
677
|
-
.
|
|
728
|
+
const finalError = finalX1
|
|
729
|
+
.minus(finalX2)
|
|
730
|
+
.pow(2)
|
|
731
|
+
.plus(finalY1.minus(finalY2).pow(2))
|
|
732
|
+
.sqrt();
|
|
678
733
|
|
|
679
734
|
return {
|
|
680
735
|
t1: Decimal.min(t1, t2),
|
|
681
736
|
t2: Decimal.max(t1, t2),
|
|
682
737
|
point: [finalX1.plus(finalX2).div(2), finalY1.plus(finalY2).div(2)],
|
|
683
|
-
error: finalError
|
|
738
|
+
error: finalError,
|
|
684
739
|
};
|
|
685
740
|
}
|
|
686
741
|
}
|
|
687
742
|
|
|
688
|
-
return null;
|
|
743
|
+
return null; // Failed to converge
|
|
689
744
|
}
|
|
690
745
|
|
|
691
746
|
// ============================================================================
|
|
@@ -704,10 +759,10 @@ export function pathPathIntersection(path1, path2, options = {}) {
|
|
|
704
759
|
// INPUT VALIDATION
|
|
705
760
|
// WHY: Prevent cryptic errors from undefined/null paths. Fail fast with clear messages.
|
|
706
761
|
if (!path1 || !Array.isArray(path1)) {
|
|
707
|
-
throw new Error(
|
|
762
|
+
throw new Error("pathPathIntersection: path1 must be an array");
|
|
708
763
|
}
|
|
709
764
|
if (!path2 || !Array.isArray(path2)) {
|
|
710
|
-
throw new Error(
|
|
765
|
+
throw new Error("pathPathIntersection: path2 must be an array");
|
|
711
766
|
}
|
|
712
767
|
|
|
713
768
|
// Handle empty paths gracefully
|
|
@@ -728,7 +783,7 @@ export function pathPathIntersection(path1, path2, options = {}) {
|
|
|
728
783
|
segment2: j,
|
|
729
784
|
t1: isect.t1,
|
|
730
785
|
t2: isect.t2,
|
|
731
|
-
point: isect.point
|
|
786
|
+
point: isect.point,
|
|
732
787
|
});
|
|
733
788
|
}
|
|
734
789
|
}
|
|
@@ -748,7 +803,7 @@ export function pathSelfIntersection(path, options = {}) {
|
|
|
748
803
|
// INPUT VALIDATION
|
|
749
804
|
// WHY: Prevent cryptic errors from undefined/null path. Fail fast with clear messages.
|
|
750
805
|
if (!path || !Array.isArray(path)) {
|
|
751
|
-
throw new Error(
|
|
806
|
+
throw new Error("pathSelfIntersection: path must be an array");
|
|
752
807
|
}
|
|
753
808
|
|
|
754
809
|
// Handle empty or single-segment paths
|
|
@@ -768,7 +823,7 @@ export function pathSelfIntersection(path, options = {}) {
|
|
|
768
823
|
segment2: i,
|
|
769
824
|
t1: isect.t1,
|
|
770
825
|
t2: isect.t2,
|
|
771
|
-
point: isect.point
|
|
826
|
+
point: isect.point,
|
|
772
827
|
});
|
|
773
828
|
}
|
|
774
829
|
}
|
|
@@ -779,7 +834,7 @@ export function pathSelfIntersection(path, options = {}) {
|
|
|
779
834
|
// WHY: j starts at i+2, so segments i and j are never adjacent (which would be i and i+1)
|
|
780
835
|
// However, for closed paths, first (i=0) and last (j=path.length-1) segments ARE adjacent
|
|
781
836
|
// because they share the start/end vertex. Skip this pair.
|
|
782
|
-
const isClosedPathAdjacent =
|
|
837
|
+
const isClosedPathAdjacent = i === 0 && j === path.length - 1;
|
|
783
838
|
if (isClosedPathAdjacent) continue;
|
|
784
839
|
|
|
785
840
|
const isects = bezierBezierIntersection(path[i], path[j], options);
|
|
@@ -790,7 +845,7 @@ export function pathSelfIntersection(path, options = {}) {
|
|
|
790
845
|
segment2: j,
|
|
791
846
|
t1: isect.t1,
|
|
792
847
|
t2: isect.t2,
|
|
793
|
-
point: isect.point
|
|
848
|
+
point: isect.point,
|
|
794
849
|
});
|
|
795
850
|
}
|
|
796
851
|
}
|
|
@@ -812,17 +867,26 @@ export function pathSelfIntersection(path, options = {}) {
|
|
|
812
867
|
* @param {string} [tolerance='1e-20'] - Verification tolerance
|
|
813
868
|
* @returns {{valid: boolean, distance: Decimal}}
|
|
814
869
|
*/
|
|
815
|
-
export function verifyIntersection(
|
|
870
|
+
export function verifyIntersection(
|
|
871
|
+
bez1,
|
|
872
|
+
bez2,
|
|
873
|
+
intersection,
|
|
874
|
+
tolerance = "1e-20",
|
|
875
|
+
) {
|
|
816
876
|
// INPUT VALIDATION
|
|
817
877
|
// WHY: Ensure all required data is present before computation. Prevents undefined errors.
|
|
818
878
|
if (!bez1 || !Array.isArray(bez1) || bez1.length < 2) {
|
|
819
|
-
throw new Error(
|
|
879
|
+
throw new Error(
|
|
880
|
+
"verifyIntersection: bez1 must have at least 2 control points",
|
|
881
|
+
);
|
|
820
882
|
}
|
|
821
883
|
if (!bez2 || !Array.isArray(bez2) || bez2.length < 2) {
|
|
822
|
-
throw new Error(
|
|
884
|
+
throw new Error(
|
|
885
|
+
"verifyIntersection: bez2 must have at least 2 control points",
|
|
886
|
+
);
|
|
823
887
|
}
|
|
824
888
|
if (!intersection) {
|
|
825
|
-
throw new Error(
|
|
889
|
+
throw new Error("verifyIntersection: intersection object is required");
|
|
826
890
|
}
|
|
827
891
|
|
|
828
892
|
const tol = D(tolerance);
|
|
@@ -836,7 +900,7 @@ export function verifyIntersection(bez1, bez2, intersection, tolerance = '1e-20'
|
|
|
836
900
|
valid: distance.lt(tol),
|
|
837
901
|
distance,
|
|
838
902
|
point1: [x1, y1],
|
|
839
|
-
point2: [x2, y2]
|
|
903
|
+
point2: [x2, y2],
|
|
840
904
|
};
|
|
841
905
|
}
|
|
842
906
|
|
|
@@ -852,23 +916,34 @@ export function verifyIntersection(bez1, bez2, intersection, tolerance = '1e-20'
|
|
|
852
916
|
* @param {string} [tolerance='1e-40'] - Verification tolerance
|
|
853
917
|
* @returns {{valid: boolean, parametricError1: Decimal, parametricError2: Decimal, algebraicError: Decimal, crossProductError: Decimal}}
|
|
854
918
|
*/
|
|
855
|
-
export function verifyLineLineIntersection(
|
|
919
|
+
export function verifyLineLineIntersection(
|
|
920
|
+
line1,
|
|
921
|
+
line2,
|
|
922
|
+
intersection,
|
|
923
|
+
tolerance = "1e-40",
|
|
924
|
+
) {
|
|
856
925
|
// INPUT VALIDATION
|
|
857
926
|
// WHY: Verify all required inputs before processing. Fail fast with clear error messages.
|
|
858
927
|
if (!line1 || !Array.isArray(line1) || line1.length !== 2) {
|
|
859
|
-
throw new Error(
|
|
928
|
+
throw new Error(
|
|
929
|
+
"verifyLineLineIntersection: line1 must be an array of 2 points",
|
|
930
|
+
);
|
|
860
931
|
}
|
|
861
932
|
if (!line2 || !Array.isArray(line2) || line2.length !== 2) {
|
|
862
|
-
throw new Error(
|
|
933
|
+
throw new Error(
|
|
934
|
+
"verifyLineLineIntersection: line2 must be an array of 2 points",
|
|
935
|
+
);
|
|
863
936
|
}
|
|
864
937
|
if (!intersection) {
|
|
865
|
-
throw new Error(
|
|
938
|
+
throw new Error(
|
|
939
|
+
"verifyLineLineIntersection: intersection object is required",
|
|
940
|
+
);
|
|
866
941
|
}
|
|
867
942
|
|
|
868
943
|
const tol = D(tolerance);
|
|
869
944
|
|
|
870
945
|
if (!intersection.t1) {
|
|
871
|
-
return { valid: false, reason:
|
|
946
|
+
return { valid: false, reason: "No intersection provided" };
|
|
872
947
|
}
|
|
873
948
|
|
|
874
949
|
const [x1, y1] = [D(line1[0][0]), D(line1[0][1])];
|
|
@@ -886,17 +961,35 @@ export function verifyLineLineIntersection(line1, line2, intersection, tolerance
|
|
|
886
961
|
const p2x = x3.plus(x4.minus(x3).times(t2));
|
|
887
962
|
const p2y = y3.plus(y4.minus(y3).times(t2));
|
|
888
963
|
|
|
889
|
-
const parametricError1 = px
|
|
890
|
-
|
|
891
|
-
|
|
964
|
+
const parametricError1 = px
|
|
965
|
+
.minus(p1x)
|
|
966
|
+
.pow(2)
|
|
967
|
+
.plus(py.minus(p1y).pow(2))
|
|
968
|
+
.sqrt();
|
|
969
|
+
const parametricError2 = px
|
|
970
|
+
.minus(p2x)
|
|
971
|
+
.pow(2)
|
|
972
|
+
.plus(py.minus(p2y).pow(2))
|
|
973
|
+
.sqrt();
|
|
974
|
+
const pointMismatchError = p1x
|
|
975
|
+
.minus(p2x)
|
|
976
|
+
.pow(2)
|
|
977
|
+
.plus(p1y.minus(p2y).pow(2))
|
|
978
|
+
.sqrt();
|
|
892
979
|
|
|
893
980
|
// 2. Algebraic verification: substitute into line equations
|
|
894
981
|
// Line 1: (y - y1) / (y2 - y1) = (x - x1) / (x2 - x1)
|
|
895
982
|
// Cross-multiply: (y - y1)(x2 - x1) = (x - x1)(y2 - y1)
|
|
896
|
-
const algebraicError1 = py
|
|
897
|
-
.minus(
|
|
898
|
-
|
|
899
|
-
.minus(px.minus(
|
|
983
|
+
const algebraicError1 = py
|
|
984
|
+
.minus(y1)
|
|
985
|
+
.times(x2.minus(x1))
|
|
986
|
+
.minus(px.minus(x1).times(y2.minus(y1)))
|
|
987
|
+
.abs();
|
|
988
|
+
const algebraicError2 = py
|
|
989
|
+
.minus(y3)
|
|
990
|
+
.times(x4.minus(x3))
|
|
991
|
+
.minus(px.minus(x3).times(y4.minus(y3)))
|
|
992
|
+
.abs();
|
|
900
993
|
|
|
901
994
|
// 3. Cross product verification: vectors from endpoints to intersection should be collinear
|
|
902
995
|
const v1x = px.minus(x1);
|
|
@@ -912,8 +1005,13 @@ export function verifyLineLineIntersection(line1, line2, intersection, tolerance
|
|
|
912
1005
|
const crossProduct2 = v3x.times(v4y).minus(v3y.times(v4x)).abs();
|
|
913
1006
|
|
|
914
1007
|
const maxError = Decimal.max(
|
|
915
|
-
parametricError1,
|
|
916
|
-
|
|
1008
|
+
parametricError1,
|
|
1009
|
+
parametricError2,
|
|
1010
|
+
pointMismatchError,
|
|
1011
|
+
algebraicError1,
|
|
1012
|
+
algebraicError2,
|
|
1013
|
+
crossProduct1,
|
|
1014
|
+
crossProduct2,
|
|
917
1015
|
);
|
|
918
1016
|
|
|
919
1017
|
return {
|
|
@@ -925,7 +1023,7 @@ export function verifyLineLineIntersection(line1, line2, intersection, tolerance
|
|
|
925
1023
|
algebraicError2,
|
|
926
1024
|
crossProduct1,
|
|
927
1025
|
crossProduct2,
|
|
928
|
-
maxError
|
|
1026
|
+
maxError,
|
|
929
1027
|
};
|
|
930
1028
|
}
|
|
931
1029
|
|
|
@@ -941,23 +1039,34 @@ export function verifyLineLineIntersection(line1, line2, intersection, tolerance
|
|
|
941
1039
|
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
942
1040
|
* @returns {{valid: boolean, bezierError: Decimal, lineError: Decimal, signedDistance: Decimal}}
|
|
943
1041
|
*/
|
|
944
|
-
export function verifyBezierLineIntersection(
|
|
1042
|
+
export function verifyBezierLineIntersection(
|
|
1043
|
+
bezier,
|
|
1044
|
+
line,
|
|
1045
|
+
intersection,
|
|
1046
|
+
tolerance = "1e-30",
|
|
1047
|
+
) {
|
|
945
1048
|
// INPUT VALIDATION
|
|
946
1049
|
// WHY: Ensure all required inputs are valid before verification. Prevents undefined behavior.
|
|
947
1050
|
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
948
|
-
throw new Error(
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
"verifyBezierLineIntersection: bezier must have at least 2 control points",
|
|
1053
|
+
);
|
|
949
1054
|
}
|
|
950
1055
|
if (!line || !Array.isArray(line) || line.length !== 2) {
|
|
951
|
-
throw new Error(
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
"verifyBezierLineIntersection: line must be an array of 2 points",
|
|
1058
|
+
);
|
|
952
1059
|
}
|
|
953
1060
|
if (!intersection) {
|
|
954
|
-
throw new Error(
|
|
1061
|
+
throw new Error(
|
|
1062
|
+
"verifyBezierLineIntersection: intersection object is required",
|
|
1063
|
+
);
|
|
955
1064
|
}
|
|
956
1065
|
|
|
957
1066
|
const tol = D(tolerance);
|
|
958
1067
|
|
|
959
1068
|
if (intersection.t1 === undefined) {
|
|
960
|
-
return { valid: false, reason:
|
|
1069
|
+
return { valid: false, reason: "No intersection provided" };
|
|
961
1070
|
}
|
|
962
1071
|
|
|
963
1072
|
const t1 = D(intersection.t1);
|
|
@@ -976,18 +1085,37 @@ export function verifyBezierLineIntersection(bezier, line, intersection, toleran
|
|
|
976
1085
|
const dly = ly1.minus(ly0);
|
|
977
1086
|
const expectedLineX = lx0.plus(dlx.times(t2));
|
|
978
1087
|
const expectedLineY = ly0.plus(dly.times(t2));
|
|
979
|
-
const lineError = px
|
|
1088
|
+
const lineError = px
|
|
1089
|
+
.minus(expectedLineX)
|
|
1090
|
+
.pow(2)
|
|
1091
|
+
.plus(py.minus(expectedLineY).pow(2))
|
|
1092
|
+
.sqrt();
|
|
980
1093
|
|
|
981
1094
|
// 3. Signed distance from line
|
|
982
1095
|
// dist = ((y - ly0) * dlx - (x - lx0) * dly) / sqrt(dlx^2 + dly^2)
|
|
983
1096
|
const lineLen = dlx.pow(2).plus(dly.pow(2)).sqrt();
|
|
984
|
-
const signedDistance = lineLen.isZero()
|
|
985
|
-
|
|
1097
|
+
const signedDistance = lineLen.isZero()
|
|
1098
|
+
? D(0)
|
|
1099
|
+
: py
|
|
1100
|
+
.minus(ly0)
|
|
1101
|
+
.times(dlx)
|
|
1102
|
+
.minus(px.minus(lx0).times(dly))
|
|
1103
|
+
.div(lineLen)
|
|
1104
|
+
.abs();
|
|
986
1105
|
|
|
987
1106
|
// 4. Verify bezier point matches line point
|
|
988
|
-
const pointMismatch = bx
|
|
1107
|
+
const pointMismatch = bx
|
|
1108
|
+
.minus(expectedLineX)
|
|
1109
|
+
.pow(2)
|
|
1110
|
+
.plus(by.minus(expectedLineY).pow(2))
|
|
1111
|
+
.sqrt();
|
|
989
1112
|
|
|
990
|
-
const maxError = Decimal.max(
|
|
1113
|
+
const maxError = Decimal.max(
|
|
1114
|
+
bezierError,
|
|
1115
|
+
lineError,
|
|
1116
|
+
signedDistance,
|
|
1117
|
+
pointMismatch,
|
|
1118
|
+
);
|
|
991
1119
|
|
|
992
1120
|
return {
|
|
993
1121
|
valid: maxError.lt(tol),
|
|
@@ -997,7 +1125,7 @@ export function verifyBezierLineIntersection(bezier, line, intersection, toleran
|
|
|
997
1125
|
pointMismatch,
|
|
998
1126
|
bezierPoint: [bx, by],
|
|
999
1127
|
linePoint: [expectedLineX, expectedLineY],
|
|
1000
|
-
maxError
|
|
1128
|
+
maxError,
|
|
1001
1129
|
};
|
|
1002
1130
|
}
|
|
1003
1131
|
|
|
@@ -1013,23 +1141,34 @@ export function verifyBezierLineIntersection(bezier, line, intersection, toleran
|
|
|
1013
1141
|
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1014
1142
|
* @returns {{valid: boolean, distance: Decimal, point1: Array, point2: Array, refinementConverged: boolean}}
|
|
1015
1143
|
*/
|
|
1016
|
-
export function verifyBezierBezierIntersection(
|
|
1144
|
+
export function verifyBezierBezierIntersection(
|
|
1145
|
+
bez1,
|
|
1146
|
+
bez2,
|
|
1147
|
+
intersection,
|
|
1148
|
+
tolerance = "1e-30",
|
|
1149
|
+
) {
|
|
1017
1150
|
// INPUT VALIDATION
|
|
1018
1151
|
// WHY: Validate inputs before verification to prevent unexpected errors from invalid data.
|
|
1019
1152
|
if (!bez1 || !Array.isArray(bez1) || bez1.length < 2) {
|
|
1020
|
-
throw new Error(
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
"verifyBezierBezierIntersection: bez1 must have at least 2 control points",
|
|
1155
|
+
);
|
|
1021
1156
|
}
|
|
1022
1157
|
if (!bez2 || !Array.isArray(bez2) || bez2.length < 2) {
|
|
1023
|
-
throw new Error(
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
"verifyBezierBezierIntersection: bez2 must have at least 2 control points",
|
|
1160
|
+
);
|
|
1024
1161
|
}
|
|
1025
1162
|
if (!intersection) {
|
|
1026
|
-
throw new Error(
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
"verifyBezierBezierIntersection: intersection object is required",
|
|
1165
|
+
);
|
|
1027
1166
|
}
|
|
1028
1167
|
|
|
1029
1168
|
const tol = D(tolerance);
|
|
1030
1169
|
|
|
1031
1170
|
if (intersection.t1 === undefined) {
|
|
1032
|
-
return { valid: false, reason:
|
|
1171
|
+
return { valid: false, reason: "No intersection provided" };
|
|
1033
1172
|
}
|
|
1034
1173
|
|
|
1035
1174
|
const t1 = D(intersection.t1);
|
|
@@ -1054,10 +1193,10 @@ export function verifyBezierBezierIntersection(bez1, bez2, intersection, toleran
|
|
|
1054
1193
|
// If intersection is real, perturbations should converge back
|
|
1055
1194
|
let refinementConverged = true;
|
|
1056
1195
|
const perturbations = [
|
|
1057
|
-
[D(
|
|
1058
|
-
[D(
|
|
1059
|
-
[D(0), D(
|
|
1060
|
-
[D(0), D(
|
|
1196
|
+
[D("0.001"), D(0)],
|
|
1197
|
+
[D("-0.001"), D(0)],
|
|
1198
|
+
[D(0), D("0.001")],
|
|
1199
|
+
[D(0), D("-0.001")],
|
|
1061
1200
|
];
|
|
1062
1201
|
|
|
1063
1202
|
for (const [dt1, dt2] of perturbations) {
|
|
@@ -1092,7 +1231,7 @@ export function verifyBezierBezierIntersection(bez1, bez2, intersection, toleran
|
|
|
1092
1231
|
refinementConverged,
|
|
1093
1232
|
t1InBounds,
|
|
1094
1233
|
t2InBounds,
|
|
1095
|
-
maxError
|
|
1234
|
+
maxError,
|
|
1096
1235
|
};
|
|
1097
1236
|
}
|
|
1098
1237
|
|
|
@@ -1108,21 +1247,28 @@ export function verifyBezierBezierIntersection(bez1, bez2, intersection, toleran
|
|
|
1108
1247
|
* @param {string} [minSeparation='0.01'] - Minimum parameter separation
|
|
1109
1248
|
* @returns {{valid: boolean, distance: Decimal, separation: Decimal}}
|
|
1110
1249
|
*/
|
|
1111
|
-
export function verifySelfIntersection(
|
|
1250
|
+
export function verifySelfIntersection(
|
|
1251
|
+
bezier,
|
|
1252
|
+
intersection,
|
|
1253
|
+
tolerance = "1e-30",
|
|
1254
|
+
minSeparation = "0.01",
|
|
1255
|
+
) {
|
|
1112
1256
|
// INPUT VALIDATION
|
|
1113
1257
|
// WHY: Ensure curve and intersection data are valid before attempting verification.
|
|
1114
1258
|
if (!bezier || !Array.isArray(bezier) || bezier.length < 2) {
|
|
1115
|
-
throw new Error(
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
"verifySelfIntersection: bezier must have at least 2 control points",
|
|
1261
|
+
);
|
|
1116
1262
|
}
|
|
1117
1263
|
if (!intersection) {
|
|
1118
|
-
throw new Error(
|
|
1264
|
+
throw new Error("verifySelfIntersection: intersection object is required");
|
|
1119
1265
|
}
|
|
1120
1266
|
|
|
1121
1267
|
const tol = D(tolerance);
|
|
1122
1268
|
const minSep = D(minSeparation);
|
|
1123
1269
|
|
|
1124
1270
|
if (intersection.t1 === undefined) {
|
|
1125
|
-
return { valid: false, reason:
|
|
1271
|
+
return { valid: false, reason: "No intersection provided" };
|
|
1126
1272
|
}
|
|
1127
1273
|
|
|
1128
1274
|
const t1 = D(intersection.t1);
|
|
@@ -1147,25 +1293,53 @@ export function verifySelfIntersection(bezier, intersection, tolerance = '1e-30'
|
|
|
1147
1293
|
|
|
1148
1294
|
// 5. Verify by sampling nearby - true self-intersection is stable
|
|
1149
1295
|
let stableIntersection = true;
|
|
1150
|
-
const epsilon = D(
|
|
1296
|
+
const epsilon = D("0.0001");
|
|
1151
1297
|
|
|
1152
1298
|
// Sample points slightly before and after each parameter
|
|
1153
|
-
const [xBefore1, yBefore1] = bezierPoint(
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1299
|
+
const [xBefore1, yBefore1] = bezierPoint(
|
|
1300
|
+
bezier,
|
|
1301
|
+
Decimal.max(D(0), t1.minus(epsilon)),
|
|
1302
|
+
);
|
|
1303
|
+
const [xAfter1, yAfter1] = bezierPoint(
|
|
1304
|
+
bezier,
|
|
1305
|
+
Decimal.min(D(1), t1.plus(epsilon)),
|
|
1306
|
+
);
|
|
1307
|
+
const [xBefore2, yBefore2] = bezierPoint(
|
|
1308
|
+
bezier,
|
|
1309
|
+
Decimal.max(D(0), t2.minus(epsilon)),
|
|
1310
|
+
);
|
|
1311
|
+
const [xAfter2, yAfter2] = bezierPoint(
|
|
1312
|
+
bezier,
|
|
1313
|
+
Decimal.min(D(1), t2.plus(epsilon)),
|
|
1314
|
+
);
|
|
1157
1315
|
|
|
1158
1316
|
// The curve portions should cross (distances should increase on both sides)
|
|
1159
|
-
const distBefore = xBefore1
|
|
1160
|
-
|
|
1317
|
+
const distBefore = xBefore1
|
|
1318
|
+
.minus(xBefore2)
|
|
1319
|
+
.pow(2)
|
|
1320
|
+
.plus(yBefore1.minus(yBefore2).pow(2))
|
|
1321
|
+
.sqrt();
|
|
1322
|
+
const distAfter = xAfter1
|
|
1323
|
+
.minus(xAfter2)
|
|
1324
|
+
.pow(2)
|
|
1325
|
+
.plus(yAfter1.minus(yAfter2).pow(2))
|
|
1326
|
+
.sqrt();
|
|
1161
1327
|
|
|
1162
1328
|
// Both neighboring distances should be larger than intersection distance
|
|
1163
|
-
if (
|
|
1329
|
+
if (
|
|
1330
|
+
!distBefore.gt(distance.minus(tol)) ||
|
|
1331
|
+
!distAfter.gt(distance.minus(tol))
|
|
1332
|
+
) {
|
|
1164
1333
|
stableIntersection = false;
|
|
1165
1334
|
}
|
|
1166
1335
|
|
|
1167
1336
|
return {
|
|
1168
|
-
valid:
|
|
1337
|
+
valid:
|
|
1338
|
+
distance.lt(tol) &&
|
|
1339
|
+
sufficientSeparation &&
|
|
1340
|
+
t1InBounds &&
|
|
1341
|
+
t2InBounds &&
|
|
1342
|
+
properlyOrdered,
|
|
1169
1343
|
distance,
|
|
1170
1344
|
separation,
|
|
1171
1345
|
sufficientSeparation,
|
|
@@ -1174,7 +1348,7 @@ export function verifySelfIntersection(bezier, intersection, tolerance = '1e-30'
|
|
|
1174
1348
|
properlyOrdered,
|
|
1175
1349
|
stableIntersection,
|
|
1176
1350
|
point1: [x1, y1],
|
|
1177
|
-
point2: [x2, y2]
|
|
1351
|
+
point2: [x2, y2],
|
|
1178
1352
|
};
|
|
1179
1353
|
}
|
|
1180
1354
|
|
|
@@ -1187,17 +1361,24 @@ export function verifySelfIntersection(bezier, intersection, tolerance = '1e-30'
|
|
|
1187
1361
|
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1188
1362
|
* @returns {{valid: boolean, results: Array, invalidCount: number}}
|
|
1189
1363
|
*/
|
|
1190
|
-
export function verifyPathPathIntersection(
|
|
1364
|
+
export function verifyPathPathIntersection(
|
|
1365
|
+
path1,
|
|
1366
|
+
path2,
|
|
1367
|
+
intersections,
|
|
1368
|
+
tolerance = "1e-30",
|
|
1369
|
+
) {
|
|
1191
1370
|
// INPUT VALIDATION
|
|
1192
1371
|
// WHY: Validate all inputs before processing to ensure meaningful error messages.
|
|
1193
1372
|
if (!path1 || !Array.isArray(path1)) {
|
|
1194
|
-
throw new Error(
|
|
1373
|
+
throw new Error("verifyPathPathIntersection: path1 must be an array");
|
|
1195
1374
|
}
|
|
1196
1375
|
if (!path2 || !Array.isArray(path2)) {
|
|
1197
|
-
throw new Error(
|
|
1376
|
+
throw new Error("verifyPathPathIntersection: path2 must be an array");
|
|
1198
1377
|
}
|
|
1199
1378
|
if (!intersections || !Array.isArray(intersections)) {
|
|
1200
|
-
throw new Error(
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
"verifyPathPathIntersection: intersections must be an array",
|
|
1381
|
+
);
|
|
1201
1382
|
}
|
|
1202
1383
|
|
|
1203
1384
|
const results = [];
|
|
@@ -1208,12 +1389,17 @@ export function verifyPathPathIntersection(path1, path2, intersections, toleranc
|
|
|
1208
1389
|
const seg2 = path2[isect.segment2];
|
|
1209
1390
|
|
|
1210
1391
|
if (!seg1 || !seg2) {
|
|
1211
|
-
results.push({ valid: false, reason:
|
|
1392
|
+
results.push({ valid: false, reason: "Invalid segment index" });
|
|
1212
1393
|
invalidCount++;
|
|
1213
1394
|
continue;
|
|
1214
1395
|
}
|
|
1215
1396
|
|
|
1216
|
-
const verification = verifyBezierBezierIntersection(
|
|
1397
|
+
const verification = verifyBezierBezierIntersection(
|
|
1398
|
+
seg1,
|
|
1399
|
+
seg2,
|
|
1400
|
+
isect,
|
|
1401
|
+
tolerance,
|
|
1402
|
+
);
|
|
1217
1403
|
results.push(verification);
|
|
1218
1404
|
|
|
1219
1405
|
if (!verification.valid) {
|
|
@@ -1225,7 +1411,7 @@ export function verifyPathPathIntersection(path1, path2, intersections, toleranc
|
|
|
1225
1411
|
valid: invalidCount === 0,
|
|
1226
1412
|
results,
|
|
1227
1413
|
invalidCount,
|
|
1228
|
-
totalIntersections: intersections.length
|
|
1414
|
+
totalIntersections: intersections.length,
|
|
1229
1415
|
};
|
|
1230
1416
|
}
|
|
1231
1417
|
|
|
@@ -1236,71 +1422,131 @@ export function verifyPathPathIntersection(path1, path2, intersections, toleranc
|
|
|
1236
1422
|
* @param {string} [tolerance='1e-30'] - Verification tolerance
|
|
1237
1423
|
* @returns {{allPassed: boolean, results: Object}}
|
|
1238
1424
|
*/
|
|
1239
|
-
export function verifyAllIntersectionFunctions(tolerance =
|
|
1425
|
+
export function verifyAllIntersectionFunctions(tolerance = "1e-30") {
|
|
1240
1426
|
const results = {};
|
|
1241
1427
|
let allPassed = true;
|
|
1242
1428
|
|
|
1243
1429
|
// Test 1: Line-line intersection
|
|
1244
|
-
const line1 = [
|
|
1245
|
-
|
|
1430
|
+
const line1 = [
|
|
1431
|
+
[0, 0],
|
|
1432
|
+
[2, 2],
|
|
1433
|
+
];
|
|
1434
|
+
const line2 = [
|
|
1435
|
+
[0, 2],
|
|
1436
|
+
[2, 0],
|
|
1437
|
+
];
|
|
1246
1438
|
const lineIsects = lineLineIntersection(line1, line2);
|
|
1247
1439
|
|
|
1248
1440
|
if (lineIsects.length > 0) {
|
|
1249
|
-
const lineVerify = verifyLineLineIntersection(
|
|
1441
|
+
const lineVerify = verifyLineLineIntersection(
|
|
1442
|
+
line1,
|
|
1443
|
+
line2,
|
|
1444
|
+
lineIsects[0],
|
|
1445
|
+
tolerance,
|
|
1446
|
+
);
|
|
1250
1447
|
results.lineLine = lineVerify;
|
|
1251
1448
|
if (!lineVerify.valid) allPassed = false;
|
|
1252
1449
|
} else {
|
|
1253
1450
|
// WHY: These specific test lines (diagonal from [0,0] to [2,2] and [0,2] to [2,0])
|
|
1254
1451
|
// geometrically MUST intersect at [1,1]. No intersection indicates a bug.
|
|
1255
|
-
results.lineLine = {
|
|
1452
|
+
results.lineLine = {
|
|
1453
|
+
valid: false,
|
|
1454
|
+
reason:
|
|
1455
|
+
"No intersection found for lines that geometrically must intersect at [1,1]",
|
|
1456
|
+
};
|
|
1256
1457
|
allPassed = false;
|
|
1257
1458
|
}
|
|
1258
1459
|
|
|
1259
1460
|
// Test 2: Bezier-line intersection
|
|
1260
|
-
const cubic = [
|
|
1261
|
-
|
|
1461
|
+
const cubic = [
|
|
1462
|
+
[0, 0],
|
|
1463
|
+
[0.5, 2],
|
|
1464
|
+
[1.5, 2],
|
|
1465
|
+
[2, 0],
|
|
1466
|
+
];
|
|
1467
|
+
const horizLine = [
|
|
1468
|
+
[0, 1],
|
|
1469
|
+
[2, 1],
|
|
1470
|
+
];
|
|
1262
1471
|
const bezLineIsects = bezierLineIntersection(cubic, horizLine);
|
|
1263
1472
|
|
|
1264
1473
|
if (bezLineIsects.length > 0) {
|
|
1265
1474
|
let allValid = true;
|
|
1266
1475
|
const verifications = [];
|
|
1267
1476
|
for (const isect of bezLineIsects) {
|
|
1268
|
-
const v = verifyBezierLineIntersection(
|
|
1477
|
+
const v = verifyBezierLineIntersection(
|
|
1478
|
+
cubic,
|
|
1479
|
+
horizLine,
|
|
1480
|
+
isect,
|
|
1481
|
+
tolerance,
|
|
1482
|
+
);
|
|
1269
1483
|
verifications.push(v);
|
|
1270
1484
|
if (!v.valid) allValid = false;
|
|
1271
1485
|
}
|
|
1272
|
-
results.bezierLine = {
|
|
1486
|
+
results.bezierLine = {
|
|
1487
|
+
valid: allValid,
|
|
1488
|
+
intersectionCount: bezLineIsects.length,
|
|
1489
|
+
verifications,
|
|
1490
|
+
};
|
|
1273
1491
|
if (!allValid) allPassed = false;
|
|
1274
1492
|
} else {
|
|
1275
|
-
results.bezierLine = { valid: false, reason:
|
|
1493
|
+
results.bezierLine = { valid: false, reason: "No intersection found" };
|
|
1276
1494
|
allPassed = false;
|
|
1277
1495
|
}
|
|
1278
1496
|
|
|
1279
1497
|
// Test 3: Bezier-bezier intersection
|
|
1280
1498
|
// WHY: These specific curves may or may not intersect depending on their geometry.
|
|
1281
1499
|
// An empty result is valid if the curves don't actually cross. This is not a failure condition.
|
|
1282
|
-
const cubic1 = [
|
|
1283
|
-
|
|
1500
|
+
const cubic1 = [
|
|
1501
|
+
[0, 0],
|
|
1502
|
+
[1, 2],
|
|
1503
|
+
[2, 2],
|
|
1504
|
+
[3, 0],
|
|
1505
|
+
];
|
|
1506
|
+
const cubic2 = [
|
|
1507
|
+
[0, 1],
|
|
1508
|
+
[1, -1],
|
|
1509
|
+
[2, 3],
|
|
1510
|
+
[3, 1],
|
|
1511
|
+
];
|
|
1284
1512
|
const bezBezIsects = bezierBezierIntersection(cubic1, cubic2);
|
|
1285
1513
|
|
|
1286
1514
|
if (bezBezIsects.length > 0) {
|
|
1287
1515
|
let allValid = true;
|
|
1288
1516
|
const verifications = [];
|
|
1289
1517
|
for (const isect of bezBezIsects) {
|
|
1290
|
-
const v = verifyBezierBezierIntersection(
|
|
1518
|
+
const v = verifyBezierBezierIntersection(
|
|
1519
|
+
cubic1,
|
|
1520
|
+
cubic2,
|
|
1521
|
+
isect,
|
|
1522
|
+
tolerance,
|
|
1523
|
+
);
|
|
1291
1524
|
verifications.push(v);
|
|
1292
1525
|
if (!v.valid) allValid = false;
|
|
1293
1526
|
}
|
|
1294
|
-
results.bezierBezier = {
|
|
1527
|
+
results.bezierBezier = {
|
|
1528
|
+
valid: allValid,
|
|
1529
|
+
intersectionCount: bezBezIsects.length,
|
|
1530
|
+
verifications,
|
|
1531
|
+
};
|
|
1295
1532
|
if (!allValid) allPassed = false;
|
|
1296
1533
|
} else {
|
|
1297
1534
|
// WHY: No intersection is not an error - it's a valid result when curves don't cross.
|
|
1298
1535
|
// We mark it as valid since the function is working correctly.
|
|
1299
|
-
results.bezierBezier = {
|
|
1536
|
+
results.bezierBezier = {
|
|
1537
|
+
valid: true,
|
|
1538
|
+
intersectionCount: 0,
|
|
1539
|
+
note: "No intersections (may be geometrically correct)",
|
|
1540
|
+
};
|
|
1300
1541
|
}
|
|
1301
1542
|
|
|
1302
1543
|
// Test 4: Self-intersection (use a loop curve)
|
|
1303
|
-
const loopCurve = [
|
|
1544
|
+
const loopCurve = [
|
|
1545
|
+
[0, 0],
|
|
1546
|
+
[2, 2],
|
|
1547
|
+
[0, 2],
|
|
1548
|
+
[2, 0],
|
|
1549
|
+
]; // Figure-8 shape
|
|
1304
1550
|
const selfIsects = bezierSelfIntersection(loopCurve);
|
|
1305
1551
|
|
|
1306
1552
|
if (selfIsects.length > 0) {
|
|
@@ -1311,11 +1557,19 @@ export function verifyAllIntersectionFunctions(tolerance = '1e-30') {
|
|
|
1311
1557
|
verifications.push(v);
|
|
1312
1558
|
if (!v.valid) allValid = false;
|
|
1313
1559
|
}
|
|
1314
|
-
results.selfIntersection = {
|
|
1560
|
+
results.selfIntersection = {
|
|
1561
|
+
valid: allValid,
|
|
1562
|
+
intersectionCount: selfIsects.length,
|
|
1563
|
+
verifications,
|
|
1564
|
+
};
|
|
1315
1565
|
if (!allValid) allPassed = false;
|
|
1316
1566
|
} else {
|
|
1317
1567
|
// Self-intersection expected for this curve
|
|
1318
|
-
results.selfIntersection = {
|
|
1568
|
+
results.selfIntersection = {
|
|
1569
|
+
valid: true,
|
|
1570
|
+
intersectionCount: 0,
|
|
1571
|
+
note: "No self-intersections found",
|
|
1572
|
+
};
|
|
1319
1573
|
}
|
|
1320
1574
|
|
|
1321
1575
|
// Test 5: Path-path intersection
|
|
@@ -1324,16 +1578,25 @@ export function verifyAllIntersectionFunctions(tolerance = '1e-30') {
|
|
|
1324
1578
|
const pathIsects = pathPathIntersection(path1, path2);
|
|
1325
1579
|
|
|
1326
1580
|
if (pathIsects.length > 0) {
|
|
1327
|
-
const pathVerify = verifyPathPathIntersection(
|
|
1581
|
+
const pathVerify = verifyPathPathIntersection(
|
|
1582
|
+
path1,
|
|
1583
|
+
path2,
|
|
1584
|
+
pathIsects,
|
|
1585
|
+
tolerance,
|
|
1586
|
+
);
|
|
1328
1587
|
results.pathPath = pathVerify;
|
|
1329
1588
|
if (!pathVerify.valid) allPassed = false;
|
|
1330
1589
|
} else {
|
|
1331
|
-
results.pathPath = {
|
|
1590
|
+
results.pathPath = {
|
|
1591
|
+
valid: true,
|
|
1592
|
+
intersectionCount: 0,
|
|
1593
|
+
note: "No path intersections",
|
|
1594
|
+
};
|
|
1332
1595
|
}
|
|
1333
1596
|
|
|
1334
1597
|
return {
|
|
1335
1598
|
allPassed,
|
|
1336
|
-
results
|
|
1599
|
+
results,
|
|
1337
1600
|
};
|
|
1338
1601
|
}
|
|
1339
1602
|
|
|
@@ -1365,5 +1628,5 @@ export default {
|
|
|
1365
1628
|
verifyBezierBezierIntersection,
|
|
1366
1629
|
verifySelfIntersection,
|
|
1367
1630
|
verifyPathPathIntersection,
|
|
1368
|
-
verifyAllIntersectionFunctions
|
|
1631
|
+
verifyAllIntersectionFunctions,
|
|
1369
1632
|
};
|