@emasoft/svg-matrix 1.0.28 → 1.0.30
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 +985 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +723 -180
- 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 +18 -7
- 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 +22 -18
- 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 +16381 -3370
- package/src/svg2-polyfills.js +93 -224
- 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
package/src/path-analysis.js
CHANGED
|
@@ -11,49 +11,51 @@
|
|
|
11
11
|
* @version 1.0.0
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import Decimal from
|
|
14
|
+
import Decimal from "decimal.js";
|
|
15
15
|
import {
|
|
16
16
|
bezierPoint,
|
|
17
17
|
bezierDerivative,
|
|
18
18
|
bezierTangent,
|
|
19
|
-
bezierBoundingBox
|
|
20
|
-
} from
|
|
21
|
-
import { arcLength } from
|
|
19
|
+
bezierBoundingBox,
|
|
20
|
+
} from "./bezier-analysis.js";
|
|
21
|
+
import { arcLength } from "./arc-length.js";
|
|
22
22
|
|
|
23
23
|
Decimal.set({ precision: 80 });
|
|
24
24
|
|
|
25
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
26
|
-
const PI = new Decimal(
|
|
25
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
26
|
+
const PI = new Decimal(
|
|
27
|
+
"3.1415926535897932384626433832795028841971693993751058209749445923078164062862090",
|
|
28
|
+
);
|
|
27
29
|
|
|
28
30
|
// ============================================================================
|
|
29
31
|
// NUMERICAL CONSTANTS (documented magic numbers)
|
|
30
32
|
// ============================================================================
|
|
31
33
|
|
|
32
34
|
/** Tolerance for boundary detection in point-in-path - very small to catch points on curve */
|
|
33
|
-
const BOUNDARY_TOLERANCE = new Decimal(
|
|
35
|
+
const BOUNDARY_TOLERANCE = new Decimal("1e-20");
|
|
34
36
|
|
|
35
37
|
/** Default tolerance for path closed/continuous checks - detects microscopic gaps */
|
|
36
|
-
const DEFAULT_CONTINUITY_TOLERANCE =
|
|
38
|
+
const DEFAULT_CONTINUITY_TOLERANCE = "1e-20";
|
|
37
39
|
|
|
38
40
|
/** Default tolerance for path smoothness (tangent angle) checks - allows tiny angle differences */
|
|
39
|
-
const DEFAULT_SMOOTHNESS_TOLERANCE =
|
|
41
|
+
const DEFAULT_SMOOTHNESS_TOLERANCE = "1e-10";
|
|
40
42
|
|
|
41
43
|
/** Tolerance for centroid-based direction calculations - avoids division by near-zero */
|
|
42
|
-
const CENTROID_ZERO_THRESHOLD = new Decimal(
|
|
44
|
+
const CENTROID_ZERO_THRESHOLD = new Decimal("1e-30");
|
|
43
45
|
|
|
44
46
|
/** Small epsilon for neighbor point testing - small offset for nearby point checks */
|
|
45
|
-
const NEIGHBOR_TEST_EPSILON = new Decimal(
|
|
47
|
+
const NEIGHBOR_TEST_EPSILON = new Decimal("1e-10");
|
|
46
48
|
|
|
47
49
|
/** Threshold for considering tangents anti-parallel (180-degree turn) - dot product ~ -1 */
|
|
48
|
-
const ANTI_PARALLEL_THRESHOLD = new Decimal(
|
|
50
|
+
const ANTI_PARALLEL_THRESHOLD = new Decimal("-0.99");
|
|
49
51
|
|
|
50
52
|
/** Tolerance for Newton-Raphson singular Jacobian detection - avoids division by zero */
|
|
51
|
-
const JACOBIAN_SINGULARITY_THRESHOLD = new Decimal(
|
|
53
|
+
const JACOBIAN_SINGULARITY_THRESHOLD = new Decimal("1e-60");
|
|
52
54
|
|
|
53
55
|
/** Numerical precision tolerance for farthest point verification.
|
|
54
56
|
* WHY: Accounts for floating-point rounding in distance comparisons, not sampling error.
|
|
55
57
|
* The found distance should be >= max sampled within this numerical tolerance. */
|
|
56
|
-
const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal(
|
|
58
|
+
const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal("1e-10");
|
|
57
59
|
|
|
58
60
|
// ============================================================================
|
|
59
61
|
// AREA CALCULATION (GREEN'S THEOREM)
|
|
@@ -88,7 +90,7 @@ const FARTHEST_POINT_NUMERICAL_TOLERANCE = new Decimal('1e-10');
|
|
|
88
90
|
export function pathArea(segments, options = {}) {
|
|
89
91
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
90
92
|
if (!segments || !Array.isArray(segments)) {
|
|
91
|
-
throw new Error(
|
|
93
|
+
throw new Error("pathArea: segments must be an array");
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
const { samples = 50 } = options;
|
|
@@ -118,11 +120,9 @@ export function pathArea(segments, options = {}) {
|
|
|
118
120
|
|
|
119
121
|
// Green: (1/2) * integral of (x*dy - y*dx)
|
|
120
122
|
area = area.plus(lineIntegralXdY.minus(lineIntegralYdX).div(2));
|
|
121
|
-
|
|
122
123
|
} else if (n === 2 || n === 3) {
|
|
123
124
|
// Quadratic or Cubic: use exact polynomial integration
|
|
124
125
|
area = area.plus(bezierAreaContribution(points));
|
|
125
|
-
|
|
126
126
|
} else {
|
|
127
127
|
// Higher degree: numerical integration
|
|
128
128
|
area = area.plus(numericalAreaContribution(points, samples));
|
|
@@ -157,23 +157,48 @@ function bezierAreaContribution(points) {
|
|
|
157
157
|
// Integral of x(t)*y'(t) from 0 to 1
|
|
158
158
|
// This expands to a polynomial integral that can be computed exactly
|
|
159
159
|
// After expansion and integration:
|
|
160
|
-
const integral_x_dy = x0
|
|
160
|
+
const integral_x_dy = x0
|
|
161
|
+
.times(y1.minus(y0))
|
|
161
162
|
.plus(x0.times(y2.minus(y1.times(2)).plus(y0)).div(2))
|
|
162
163
|
.plus(x1.times(2).minus(x0.times(2)).times(y1.minus(y0)).div(2))
|
|
163
|
-
.plus(
|
|
164
|
+
.plus(
|
|
165
|
+
x1
|
|
166
|
+
.times(2)
|
|
167
|
+
.minus(x0.times(2))
|
|
168
|
+
.times(y2.minus(y1.times(2)).plus(y0))
|
|
169
|
+
.div(3),
|
|
170
|
+
)
|
|
164
171
|
.plus(x2.minus(x1.times(2)).plus(x0).times(y1.minus(y0)).div(3))
|
|
165
|
-
.plus(
|
|
172
|
+
.plus(
|
|
173
|
+
x2
|
|
174
|
+
.minus(x1.times(2))
|
|
175
|
+
.plus(x0)
|
|
176
|
+
.times(y2.minus(y1.times(2)).plus(y0))
|
|
177
|
+
.div(4),
|
|
178
|
+
);
|
|
166
179
|
|
|
167
180
|
// Similarly for integral of y(t)*x'(t)
|
|
168
|
-
const integral_y_dx = y0
|
|
181
|
+
const integral_y_dx = y0
|
|
182
|
+
.times(x1.minus(x0))
|
|
169
183
|
.plus(y0.times(x2.minus(x1.times(2)).plus(x0)).div(2))
|
|
170
184
|
.plus(y1.times(2).minus(y0.times(2)).times(x1.minus(x0)).div(2))
|
|
171
|
-
.plus(
|
|
185
|
+
.plus(
|
|
186
|
+
y1
|
|
187
|
+
.times(2)
|
|
188
|
+
.minus(y0.times(2))
|
|
189
|
+
.times(x2.minus(x1.times(2)).plus(x0))
|
|
190
|
+
.div(3),
|
|
191
|
+
)
|
|
172
192
|
.plus(y2.minus(y1.times(2)).plus(y0).times(x1.minus(x0)).div(3))
|
|
173
|
-
.plus(
|
|
193
|
+
.plus(
|
|
194
|
+
y2
|
|
195
|
+
.minus(y1.times(2))
|
|
196
|
+
.plus(y0)
|
|
197
|
+
.times(x2.minus(x1.times(2)).plus(x0))
|
|
198
|
+
.div(4),
|
|
199
|
+
);
|
|
174
200
|
|
|
175
201
|
return integral_x_dy.minus(integral_y_dx).div(2);
|
|
176
|
-
|
|
177
202
|
} else if (n === 3) {
|
|
178
203
|
// Cubic Bezier - use numerical integration
|
|
179
204
|
// WHY: The exact polynomial integration for cubic Bezier area is complex
|
|
@@ -201,7 +226,7 @@ function numericalAreaContribution(points, samples) {
|
|
|
201
226
|
|
|
202
227
|
for (let i = 0; i <= samples; i++) {
|
|
203
228
|
const t = h.times(i);
|
|
204
|
-
const weight = i === 0 || i === samples ? D(1) :
|
|
229
|
+
const weight = i === 0 || i === samples ? D(1) : i % 2 === 0 ? D(2) : D(4);
|
|
205
230
|
|
|
206
231
|
const [x, y] = bezierPoint(points, t);
|
|
207
232
|
const [dx, dy] = bezierDerivative(points, t, 1);
|
|
@@ -226,7 +251,7 @@ function numericalAreaContribution(points, samples) {
|
|
|
226
251
|
export function pathAbsoluteArea(segments, options = {}) {
|
|
227
252
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
228
253
|
if (!segments || !Array.isArray(segments)) {
|
|
229
|
-
throw new Error(
|
|
254
|
+
throw new Error("pathAbsoluteArea: segments must be an array");
|
|
230
255
|
}
|
|
231
256
|
|
|
232
257
|
return pathArea(segments, options).abs();
|
|
@@ -254,13 +279,13 @@ export function pathAbsoluteArea(segments, options = {}) {
|
|
|
254
279
|
export function closestPointOnPath(segments, point, options = {}) {
|
|
255
280
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
256
281
|
if (!segments || !Array.isArray(segments) || segments.length === 0) {
|
|
257
|
-
throw new Error(
|
|
282
|
+
throw new Error("closestPointOnPath: segments must be a non-empty array");
|
|
258
283
|
}
|
|
259
284
|
if (!point || !Array.isArray(point) || point.length < 2) {
|
|
260
|
-
throw new Error(
|
|
285
|
+
throw new Error("closestPointOnPath: point must be an array [x, y]");
|
|
261
286
|
}
|
|
262
287
|
|
|
263
|
-
const { samples = 50, maxIterations = 30, tolerance =
|
|
288
|
+
const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
|
|
264
289
|
|
|
265
290
|
const px = D(point[0]);
|
|
266
291
|
const py = D(point[1]);
|
|
@@ -303,8 +328,12 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
303
328
|
const diffY = y.minus(py);
|
|
304
329
|
|
|
305
330
|
const fPrime = diffX.times(dx).plus(diffY.times(dy)).times(2);
|
|
306
|
-
const fDoublePrime = dx
|
|
307
|
-
.
|
|
331
|
+
const fDoublePrime = dx
|
|
332
|
+
.pow(2)
|
|
333
|
+
.plus(dy.pow(2))
|
|
334
|
+
.plus(diffX.times(d2x))
|
|
335
|
+
.plus(diffY.times(d2y))
|
|
336
|
+
.times(2);
|
|
308
337
|
|
|
309
338
|
// WHY: Use named constant instead of magic number for clarity
|
|
310
339
|
if (fDoublePrime.abs().lt(JACOBIAN_SINGULARITY_THRESHOLD)) break;
|
|
@@ -325,9 +354,9 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
325
354
|
// WHY: Newton refinement finds local minima within a segment, but segment
|
|
326
355
|
// endpoints might be closer than any interior critical point
|
|
327
356
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
328
|
-
const
|
|
357
|
+
const segPts = segments[segIdx];
|
|
329
358
|
for (const tVal of [D(0), D(1)]) {
|
|
330
|
-
const [x, y] = bezierPoint(
|
|
359
|
+
const [x, y] = bezierPoint(segPts, tVal);
|
|
331
360
|
const dist = px.minus(x).pow(2).plus(py.minus(y).pow(2));
|
|
332
361
|
if (dist.lt(bestDist)) {
|
|
333
362
|
bestDist = dist;
|
|
@@ -339,13 +368,17 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
339
368
|
|
|
340
369
|
// Final result
|
|
341
370
|
const [finalX, finalY] = bezierPoint(segments[bestSegment], bestT);
|
|
342
|
-
const finalDist = px
|
|
371
|
+
const finalDist = px
|
|
372
|
+
.minus(finalX)
|
|
373
|
+
.pow(2)
|
|
374
|
+
.plus(py.minus(finalY).pow(2))
|
|
375
|
+
.sqrt();
|
|
343
376
|
|
|
344
377
|
return {
|
|
345
378
|
point: [finalX, finalY],
|
|
346
379
|
distance: finalDist,
|
|
347
380
|
segmentIndex: bestSegment,
|
|
348
|
-
t: bestT
|
|
381
|
+
t: bestT,
|
|
349
382
|
};
|
|
350
383
|
}
|
|
351
384
|
|
|
@@ -360,13 +393,13 @@ export function closestPointOnPath(segments, point, options = {}) {
|
|
|
360
393
|
export function farthestPointOnPath(segments, point, options = {}) {
|
|
361
394
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
362
395
|
if (!segments || !Array.isArray(segments) || segments.length === 0) {
|
|
363
|
-
throw new Error(
|
|
396
|
+
throw new Error("farthestPointOnPath: segments must be a non-empty array");
|
|
364
397
|
}
|
|
365
398
|
if (!point || !Array.isArray(point) || point.length < 2) {
|
|
366
|
-
throw new Error(
|
|
399
|
+
throw new Error("farthestPointOnPath: point must be an array [x, y]");
|
|
367
400
|
}
|
|
368
401
|
|
|
369
|
-
const { samples = 50, maxIterations = 30, tolerance =
|
|
402
|
+
const { samples = 50, maxIterations = 30, tolerance = "1e-30" } = options;
|
|
370
403
|
|
|
371
404
|
const px = D(point[0]);
|
|
372
405
|
const py = D(point[1]);
|
|
@@ -406,8 +439,12 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
406
439
|
|
|
407
440
|
// For maximum: f'(t) = 0, f''(t) < 0
|
|
408
441
|
const fPrime = diffX.times(dx).plus(diffY.times(dy)).times(2);
|
|
409
|
-
const fDoublePrime = dx
|
|
410
|
-
.
|
|
442
|
+
const fDoublePrime = dx
|
|
443
|
+
.pow(2)
|
|
444
|
+
.plus(dy.pow(2))
|
|
445
|
+
.plus(diffX.times(d2x))
|
|
446
|
+
.plus(diffY.times(d2y))
|
|
447
|
+
.times(2);
|
|
411
448
|
|
|
412
449
|
// WHY: Use named constant instead of magic number for clarity
|
|
413
450
|
if (fDoublePrime.abs().lt(JACOBIAN_SINGULARITY_THRESHOLD)) break;
|
|
@@ -426,9 +463,9 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
426
463
|
|
|
427
464
|
// Also check endpoints
|
|
428
465
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
429
|
-
const
|
|
466
|
+
const segPts = segments[segIdx];
|
|
430
467
|
for (const t of [D(0), D(1)]) {
|
|
431
|
-
const [x, y] = bezierPoint(
|
|
468
|
+
const [x, y] = bezierPoint(segPts, t);
|
|
432
469
|
const dist = px.minus(x).pow(2).plus(py.minus(y).pow(2));
|
|
433
470
|
if (dist.gt(bestDist)) {
|
|
434
471
|
bestDist = dist;
|
|
@@ -439,13 +476,17 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
439
476
|
}
|
|
440
477
|
|
|
441
478
|
const [finalX, finalY] = bezierPoint(segments[bestSegment], bestT);
|
|
442
|
-
const finalDist = px
|
|
479
|
+
const finalDist = px
|
|
480
|
+
.minus(finalX)
|
|
481
|
+
.pow(2)
|
|
482
|
+
.plus(py.minus(finalY).pow(2))
|
|
483
|
+
.sqrt();
|
|
443
484
|
|
|
444
485
|
return {
|
|
445
486
|
point: [finalX, finalY],
|
|
446
487
|
distance: finalDist,
|
|
447
488
|
segmentIndex: bestSegment,
|
|
448
|
-
t: bestT
|
|
489
|
+
t: bestT,
|
|
449
490
|
};
|
|
450
491
|
}
|
|
451
492
|
|
|
@@ -468,10 +509,10 @@ export function farthestPointOnPath(segments, point, options = {}) {
|
|
|
468
509
|
export function pointInPath(segments, point, options = {}) {
|
|
469
510
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
470
511
|
if (!segments || !Array.isArray(segments) || segments.length === 0) {
|
|
471
|
-
throw new Error(
|
|
512
|
+
throw new Error("pointInPath: segments must be a non-empty array");
|
|
472
513
|
}
|
|
473
514
|
if (!point || !Array.isArray(point) || point.length < 2) {
|
|
474
|
-
throw new Error(
|
|
515
|
+
throw new Error("pointInPath: point must be an array [x, y]");
|
|
475
516
|
}
|
|
476
517
|
|
|
477
518
|
const { samples = 100 } = options;
|
|
@@ -533,7 +574,7 @@ export function pointInPath(segments, point, options = {}) {
|
|
|
533
574
|
return {
|
|
534
575
|
inside: windingNumber !== 0,
|
|
535
576
|
windingNumber,
|
|
536
|
-
onBoundary: false
|
|
577
|
+
onBoundary: false,
|
|
537
578
|
};
|
|
538
579
|
}
|
|
539
580
|
|
|
@@ -548,10 +589,13 @@ export function pointInPath(segments, point, options = {}) {
|
|
|
548
589
|
* @param {string} [tolerance='1e-20'] - Distance tolerance
|
|
549
590
|
* @returns {boolean}
|
|
550
591
|
*/
|
|
551
|
-
export function isPathClosed(
|
|
592
|
+
export function isPathClosed(
|
|
593
|
+
segments,
|
|
594
|
+
tolerance = DEFAULT_CONTINUITY_TOLERANCE,
|
|
595
|
+
) {
|
|
552
596
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
553
597
|
if (!segments || !Array.isArray(segments)) {
|
|
554
|
-
throw new Error(
|
|
598
|
+
throw new Error("isPathClosed: segments must be an array");
|
|
555
599
|
}
|
|
556
600
|
if (segments.length === 0) return false;
|
|
557
601
|
|
|
@@ -560,7 +604,10 @@ export function isPathClosed(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE)
|
|
|
560
604
|
const lastSeg = segments[segments.length - 1];
|
|
561
605
|
|
|
562
606
|
const [x0, y0] = [D(firstSeg[0][0]), D(firstSeg[0][1])];
|
|
563
|
-
const [xn, yn] = [
|
|
607
|
+
const [xn, yn] = [
|
|
608
|
+
D(lastSeg[lastSeg.length - 1][0]),
|
|
609
|
+
D(lastSeg[lastSeg.length - 1][1]),
|
|
610
|
+
];
|
|
564
611
|
|
|
565
612
|
const dist = x0.minus(xn).pow(2).plus(y0.minus(yn).pow(2)).sqrt();
|
|
566
613
|
return dist.lt(tol);
|
|
@@ -573,10 +620,13 @@ export function isPathClosed(segments, tolerance = DEFAULT_CONTINUITY_TOLERANCE)
|
|
|
573
620
|
* @param {string} [tolerance='1e-20'] - Distance tolerance
|
|
574
621
|
* @returns {{continuous: boolean, gaps: Array}}
|
|
575
622
|
*/
|
|
576
|
-
export function isPathContinuous(
|
|
623
|
+
export function isPathContinuous(
|
|
624
|
+
segments,
|
|
625
|
+
tolerance = DEFAULT_CONTINUITY_TOLERANCE,
|
|
626
|
+
) {
|
|
577
627
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
578
628
|
if (!segments || !Array.isArray(segments)) {
|
|
579
|
-
throw new Error(
|
|
629
|
+
throw new Error("isPathContinuous: segments must be an array");
|
|
580
630
|
}
|
|
581
631
|
if (segments.length <= 1) return { continuous: true, gaps: [] };
|
|
582
632
|
|
|
@@ -597,14 +647,14 @@ export function isPathContinuous(segments, tolerance = DEFAULT_CONTINUITY_TOLERA
|
|
|
597
647
|
segmentIndex: i,
|
|
598
648
|
gap: dist,
|
|
599
649
|
from: [x1, y1],
|
|
600
|
-
to: [x2, y2]
|
|
650
|
+
to: [x2, y2],
|
|
601
651
|
});
|
|
602
652
|
}
|
|
603
653
|
}
|
|
604
654
|
|
|
605
655
|
return {
|
|
606
656
|
continuous: gaps.length === 0,
|
|
607
|
-
gaps
|
|
657
|
+
gaps,
|
|
608
658
|
};
|
|
609
659
|
}
|
|
610
660
|
|
|
@@ -615,10 +665,13 @@ export function isPathContinuous(segments, tolerance = DEFAULT_CONTINUITY_TOLERA
|
|
|
615
665
|
* @param {string} [tolerance='1e-10'] - Tangent angle tolerance (radians)
|
|
616
666
|
* @returns {{smooth: boolean, kinks: Array}}
|
|
617
667
|
*/
|
|
618
|
-
export function isPathSmooth(
|
|
668
|
+
export function isPathSmooth(
|
|
669
|
+
segments,
|
|
670
|
+
tolerance = DEFAULT_SMOOTHNESS_TOLERANCE,
|
|
671
|
+
) {
|
|
619
672
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
620
673
|
if (!segments || !Array.isArray(segments)) {
|
|
621
|
-
throw new Error(
|
|
674
|
+
throw new Error("isPathSmooth: segments must be an array");
|
|
622
675
|
}
|
|
623
676
|
if (segments.length <= 1) return { smooth: true, kinks: [] };
|
|
624
677
|
|
|
@@ -653,14 +706,14 @@ export function isPathSmooth(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE)
|
|
|
653
706
|
segmentIndex: i,
|
|
654
707
|
angle: Decimal.atan2(cross, dot).abs(),
|
|
655
708
|
tangent1: [tx1, ty1],
|
|
656
|
-
tangent2: [tx2, ty2]
|
|
709
|
+
tangent2: [tx2, ty2],
|
|
657
710
|
});
|
|
658
711
|
}
|
|
659
712
|
}
|
|
660
713
|
|
|
661
714
|
return {
|
|
662
715
|
smooth: kinks.length === 0,
|
|
663
|
-
kinks
|
|
716
|
+
kinks,
|
|
664
717
|
};
|
|
665
718
|
}
|
|
666
719
|
|
|
@@ -674,18 +727,18 @@ export function isPathSmooth(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE)
|
|
|
674
727
|
export function findKinks(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE) {
|
|
675
728
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
676
729
|
if (!segments || !Array.isArray(segments)) {
|
|
677
|
-
throw new Error(
|
|
730
|
+
throw new Error("findKinks: segments must be an array");
|
|
678
731
|
}
|
|
679
732
|
|
|
680
733
|
const { kinks } = isPathSmooth(segments, tolerance);
|
|
681
734
|
|
|
682
735
|
// Convert to path parameter
|
|
683
|
-
return kinks.map((k,
|
|
736
|
+
return kinks.map((k, _i) => ({
|
|
684
737
|
segmentIndex: k.segmentIndex,
|
|
685
738
|
globalT: k.segmentIndex + 1, // At junction between segments
|
|
686
739
|
angle: k.angle,
|
|
687
740
|
angleRadians: k.angle,
|
|
688
|
-
angleDegrees: k.angle.times(180).div(PI)
|
|
741
|
+
angleDegrees: k.angle.times(180).div(PI),
|
|
689
742
|
}));
|
|
690
743
|
}
|
|
691
744
|
|
|
@@ -702,7 +755,7 @@ export function findKinks(segments, tolerance = DEFAULT_SMOOTHNESS_TOLERANCE) {
|
|
|
702
755
|
export function pathBoundingBox(segments) {
|
|
703
756
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
704
757
|
if (!segments || !Array.isArray(segments)) {
|
|
705
|
-
throw new Error(
|
|
758
|
+
throw new Error("pathBoundingBox: segments must be an array");
|
|
706
759
|
}
|
|
707
760
|
if (segments.length === 0) {
|
|
708
761
|
return { xmin: D(0), xmax: D(0), ymin: D(0), ymax: D(0) };
|
|
@@ -735,21 +788,35 @@ export function boundingBoxesOverlap(bbox1, bbox2) {
|
|
|
735
788
|
// INPUT VALIDATION
|
|
736
789
|
// WHY: Prevent cryptic errors from undefined/null bounding boxes
|
|
737
790
|
if (!bbox1 || !bbox2) {
|
|
738
|
-
throw new Error(
|
|
791
|
+
throw new Error("boundingBoxesOverlap: both bounding boxes are required");
|
|
739
792
|
}
|
|
740
|
-
if (
|
|
741
|
-
|
|
742
|
-
|
|
793
|
+
if (
|
|
794
|
+
bbox1.xmin === undefined ||
|
|
795
|
+
bbox1.xmax === undefined ||
|
|
796
|
+
bbox1.ymin === undefined ||
|
|
797
|
+
bbox1.ymax === undefined
|
|
798
|
+
) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
"boundingBoxesOverlap: bbox1 must have xmin, xmax, ymin, ymax",
|
|
801
|
+
);
|
|
743
802
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
803
|
+
if (
|
|
804
|
+
bbox2.xmin === undefined ||
|
|
805
|
+
bbox2.xmax === undefined ||
|
|
806
|
+
bbox2.ymin === undefined ||
|
|
807
|
+
bbox2.ymax === undefined
|
|
808
|
+
) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
"boundingBoxesOverlap: bbox2 must have xmin, xmax, ymin, ymax",
|
|
811
|
+
);
|
|
747
812
|
}
|
|
748
813
|
|
|
749
|
-
return !(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
814
|
+
return !(
|
|
815
|
+
bbox1.xmax.lt(bbox2.xmin) ||
|
|
816
|
+
bbox1.xmin.gt(bbox2.xmax) ||
|
|
817
|
+
bbox1.ymax.lt(bbox2.ymin) ||
|
|
818
|
+
bbox1.ymin.gt(bbox2.ymax)
|
|
819
|
+
);
|
|
753
820
|
}
|
|
754
821
|
|
|
755
822
|
// ============================================================================
|
|
@@ -766,7 +833,7 @@ export function boundingBoxesOverlap(bbox1, bbox2) {
|
|
|
766
833
|
export function pathLength(segments, options = {}) {
|
|
767
834
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
768
835
|
if (!segments || !Array.isArray(segments)) {
|
|
769
|
-
throw new Error(
|
|
836
|
+
throw new Error("pathLength: segments must be an array");
|
|
770
837
|
}
|
|
771
838
|
|
|
772
839
|
let total = D(0);
|
|
@@ -791,10 +858,10 @@ export function pathLength(segments, options = {}) {
|
|
|
791
858
|
* @param {number|string|Decimal} [tolerance='1e-5'] - Relative error tolerance
|
|
792
859
|
* @returns {{valid: boolean, greenArea: Decimal, shoelaceArea: Decimal, relativeError: Decimal}}
|
|
793
860
|
*/
|
|
794
|
-
export function verifyPathArea(segments, samples = 100, tolerance =
|
|
861
|
+
export function verifyPathArea(segments, samples = 100, tolerance = "1e-5") {
|
|
795
862
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
796
863
|
if (!segments || !Array.isArray(segments)) {
|
|
797
|
-
throw new Error(
|
|
864
|
+
throw new Error("verifyPathArea: segments must be an array");
|
|
798
865
|
}
|
|
799
866
|
|
|
800
867
|
const tol = D(tolerance);
|
|
@@ -829,7 +896,7 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
|
|
|
829
896
|
|
|
830
897
|
let relativeError;
|
|
831
898
|
// WHY: Use named constant to avoid division by near-zero values
|
|
832
|
-
const AREA_ZERO_THRESHOLD = new Decimal(
|
|
899
|
+
const AREA_ZERO_THRESHOLD = new Decimal("1e-30");
|
|
833
900
|
if (absGreen.gt(AREA_ZERO_THRESHOLD)) {
|
|
834
901
|
relativeError = absGreen.minus(absShoelace).abs().div(absGreen);
|
|
835
902
|
} else {
|
|
@@ -841,7 +908,7 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
|
|
|
841
908
|
greenArea,
|
|
842
909
|
shoelaceArea,
|
|
843
910
|
relativeError,
|
|
844
|
-
sameSign: greenArea.isNegative() === shoelaceArea.isNegative()
|
|
911
|
+
sameSign: greenArea.isNegative() === shoelaceArea.isNegative(),
|
|
845
912
|
};
|
|
846
913
|
}
|
|
847
914
|
|
|
@@ -854,13 +921,13 @@ export function verifyPathArea(segments, samples = 100, tolerance = '1e-5') {
|
|
|
854
921
|
* @param {number|string|Decimal} [tolerance='1e-10'] - Perpendicularity tolerance
|
|
855
922
|
* @returns {{valid: boolean, closestPoint: Object, dotProduct: Decimal, isEndpoint: boolean}}
|
|
856
923
|
*/
|
|
857
|
-
export function verifyClosestPoint(segments, queryPoint, tolerance =
|
|
924
|
+
export function verifyClosestPoint(segments, queryPoint, tolerance = "1e-10") {
|
|
858
925
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
859
926
|
if (!segments || !Array.isArray(segments)) {
|
|
860
|
-
throw new Error(
|
|
927
|
+
throw new Error("verifyClosestPoint: segments must be an array");
|
|
861
928
|
}
|
|
862
929
|
if (!queryPoint || !Array.isArray(queryPoint) || queryPoint.length < 2) {
|
|
863
|
-
throw new Error(
|
|
930
|
+
throw new Error("verifyClosestPoint: queryPoint must be an array [x, y]");
|
|
864
931
|
}
|
|
865
932
|
|
|
866
933
|
const tol = D(tolerance);
|
|
@@ -885,8 +952,9 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
|
|
|
885
952
|
|
|
886
953
|
// WHY: Check if at endpoint (where perpendicularity may not hold)
|
|
887
954
|
// Use a small threshold to determine if t is effectively 0 or 1
|
|
888
|
-
const ENDPOINT_THRESHOLD = new Decimal(
|
|
889
|
-
const isEndpoint =
|
|
955
|
+
const ENDPOINT_THRESHOLD = new Decimal("1e-10");
|
|
956
|
+
const isEndpoint =
|
|
957
|
+
t.lt(ENDPOINT_THRESHOLD) || t.gt(D(1).minus(ENDPOINT_THRESHOLD));
|
|
890
958
|
|
|
891
959
|
return {
|
|
892
960
|
valid: dotProduct.abs().lte(tol) || isEndpoint,
|
|
@@ -894,7 +962,7 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
|
|
|
894
962
|
dotProduct,
|
|
895
963
|
isEndpoint,
|
|
896
964
|
vectorToQuery: [vx, vy],
|
|
897
|
-
tangent: [tx, ty]
|
|
965
|
+
tangent: [tx, ty],
|
|
898
966
|
};
|
|
899
967
|
}
|
|
900
968
|
|
|
@@ -910,10 +978,10 @@ export function verifyClosestPoint(segments, queryPoint, tolerance = '1e-10') {
|
|
|
910
978
|
export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
|
|
911
979
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
912
980
|
if (!segments || !Array.isArray(segments)) {
|
|
913
|
-
throw new Error(
|
|
981
|
+
throw new Error("verifyFarthestPoint: segments must be an array");
|
|
914
982
|
}
|
|
915
983
|
if (!queryPoint || !Array.isArray(queryPoint) || queryPoint.length < 2) {
|
|
916
|
-
throw new Error(
|
|
984
|
+
throw new Error("verifyFarthestPoint: queryPoint must be an array [x, y]");
|
|
917
985
|
}
|
|
918
986
|
|
|
919
987
|
const qx = D(queryPoint[0]);
|
|
@@ -942,13 +1010,15 @@ export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
|
|
|
942
1010
|
// This defeats the purpose of verification - we want to ensure the found point is actually the farthest
|
|
943
1011
|
// Instead, we check that foundDistance is at least as large as maxSampledDistance
|
|
944
1012
|
// with a small tolerance for numerical precision (not sampling error, but floating point rounding)
|
|
945
|
-
const valid = foundDistance.gte(
|
|
1013
|
+
const valid = foundDistance.gte(
|
|
1014
|
+
maxSampledDistance.minus(FARTHEST_POINT_NUMERICAL_TOLERANCE),
|
|
1015
|
+
);
|
|
946
1016
|
|
|
947
1017
|
return {
|
|
948
1018
|
valid,
|
|
949
1019
|
farthestPoint: result,
|
|
950
1020
|
maxSampledDistance,
|
|
951
|
-
foundDistance
|
|
1021
|
+
foundDistance,
|
|
952
1022
|
};
|
|
953
1023
|
}
|
|
954
1024
|
|
|
@@ -964,10 +1034,10 @@ export function verifyFarthestPoint(segments, queryPoint, samples = 200) {
|
|
|
964
1034
|
export function verifyPointInPath(segments, testPoint) {
|
|
965
1035
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
966
1036
|
if (!segments || !Array.isArray(segments)) {
|
|
967
|
-
throw new Error(
|
|
1037
|
+
throw new Error("verifyPointInPath: segments must be an array");
|
|
968
1038
|
}
|
|
969
1039
|
if (!testPoint || !Array.isArray(testPoint) || testPoint.length < 2) {
|
|
970
|
-
throw new Error(
|
|
1040
|
+
throw new Error("verifyPointInPath: testPoint must be an array [x, y]");
|
|
971
1041
|
}
|
|
972
1042
|
|
|
973
1043
|
const result = pointInPath(segments, testPoint);
|
|
@@ -1009,10 +1079,16 @@ export function verifyPointInPath(segments, testPoint) {
|
|
|
1009
1079
|
const unitDy = dy.div(len).times(epsilon);
|
|
1010
1080
|
|
|
1011
1081
|
// Test point slightly toward centroid
|
|
1012
|
-
const towardCentroid = pointInPath(segments, [
|
|
1082
|
+
const towardCentroid = pointInPath(segments, [
|
|
1083
|
+
px.plus(unitDx),
|
|
1084
|
+
py.plus(unitDy),
|
|
1085
|
+
]);
|
|
1013
1086
|
|
|
1014
1087
|
// Test point slightly away from centroid
|
|
1015
|
-
const awayFromCentroid = pointInPath(segments, [
|
|
1088
|
+
const awayFromCentroid = pointInPath(segments, [
|
|
1089
|
+
px.minus(unitDx),
|
|
1090
|
+
py.minus(unitDy),
|
|
1091
|
+
]);
|
|
1016
1092
|
|
|
1017
1093
|
// If inside, moving toward centroid should stay inside
|
|
1018
1094
|
// If outside, moving toward centroid should stay outside or become inside (not suddenly outside)
|
|
@@ -1030,7 +1106,7 @@ export function verifyPointInPath(segments, testPoint) {
|
|
|
1030
1106
|
result,
|
|
1031
1107
|
consistentWithNeighbors,
|
|
1032
1108
|
towardCentroid,
|
|
1033
|
-
awayFromCentroid
|
|
1109
|
+
awayFromCentroid,
|
|
1034
1110
|
};
|
|
1035
1111
|
}
|
|
1036
1112
|
|
|
@@ -1044,14 +1120,14 @@ export function verifyPointInPath(segments, testPoint) {
|
|
|
1044
1120
|
export function verifyPathBoundingBox(segments, samples = 100) {
|
|
1045
1121
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
1046
1122
|
if (!segments || !Array.isArray(segments)) {
|
|
1047
|
-
throw new Error(
|
|
1123
|
+
throw new Error("verifyPathBoundingBox: segments must be an array");
|
|
1048
1124
|
}
|
|
1049
1125
|
|
|
1050
1126
|
const bbox = pathBoundingBox(segments);
|
|
1051
1127
|
const errors = [];
|
|
1052
1128
|
let allInside = true;
|
|
1053
1129
|
|
|
1054
|
-
const tolerance = new Decimal(
|
|
1130
|
+
const tolerance = new Decimal("1e-40");
|
|
1055
1131
|
|
|
1056
1132
|
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
1057
1133
|
const pts = segments[segIdx];
|
|
@@ -1061,12 +1137,16 @@ export function verifyPathBoundingBox(segments, samples = 100) {
|
|
|
1061
1137
|
const [x, y] = bezierPoint(pts, t);
|
|
1062
1138
|
|
|
1063
1139
|
if (x.lt(bbox.xmin.minus(tolerance)) || x.gt(bbox.xmax.plus(tolerance))) {
|
|
1064
|
-
errors.push(
|
|
1140
|
+
errors.push(
|
|
1141
|
+
`Segment ${segIdx}, t=${t}: x=${x} outside [${bbox.xmin}, ${bbox.xmax}]`,
|
|
1142
|
+
);
|
|
1065
1143
|
allInside = false;
|
|
1066
1144
|
}
|
|
1067
1145
|
|
|
1068
1146
|
if (y.lt(bbox.ymin.minus(tolerance)) || y.gt(bbox.ymax.plus(tolerance))) {
|
|
1069
|
-
errors.push(
|
|
1147
|
+
errors.push(
|
|
1148
|
+
`Segment ${segIdx}, t=${t}: y=${y} outside [${bbox.ymin}, ${bbox.ymax}]`,
|
|
1149
|
+
);
|
|
1070
1150
|
allInside = false;
|
|
1071
1151
|
}
|
|
1072
1152
|
}
|
|
@@ -1076,7 +1156,7 @@ export function verifyPathBoundingBox(segments, samples = 100) {
|
|
|
1076
1156
|
valid: errors.length === 0,
|
|
1077
1157
|
bbox,
|
|
1078
1158
|
allInside,
|
|
1079
|
-
errors
|
|
1159
|
+
errors,
|
|
1080
1160
|
};
|
|
1081
1161
|
}
|
|
1082
1162
|
|
|
@@ -1089,7 +1169,7 @@ export function verifyPathBoundingBox(segments, samples = 100) {
|
|
|
1089
1169
|
export function verifyPathContinuity(segments) {
|
|
1090
1170
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
1091
1171
|
if (!segments || !Array.isArray(segments)) {
|
|
1092
|
-
throw new Error(
|
|
1172
|
+
throw new Error("verifyPathContinuity: segments must be an array");
|
|
1093
1173
|
}
|
|
1094
1174
|
|
|
1095
1175
|
const { continuous, gaps } = isPathContinuous(segments);
|
|
@@ -1114,7 +1194,7 @@ export function verifyPathContinuity(segments) {
|
|
|
1114
1194
|
valid: allValid,
|
|
1115
1195
|
continuous,
|
|
1116
1196
|
gaps,
|
|
1117
|
-
maxGap
|
|
1197
|
+
maxGap,
|
|
1118
1198
|
};
|
|
1119
1199
|
}
|
|
1120
1200
|
|
|
@@ -1128,7 +1208,7 @@ export function verifyPathContinuity(segments) {
|
|
|
1128
1208
|
export function verifyPathLength(segments) {
|
|
1129
1209
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
1130
1210
|
if (!segments || !Array.isArray(segments)) {
|
|
1131
|
-
throw new Error(
|
|
1211
|
+
throw new Error("verifyPathLength: segments must be an array");
|
|
1132
1212
|
}
|
|
1133
1213
|
|
|
1134
1214
|
const totalArcLength = pathLength(segments);
|
|
@@ -1147,7 +1227,7 @@ export function verifyPathLength(segments) {
|
|
|
1147
1227
|
valid: totalArcLength.gte(chordSum),
|
|
1148
1228
|
arcLength: totalArcLength,
|
|
1149
1229
|
chordSum,
|
|
1150
|
-
ratio // Should be >= 1
|
|
1230
|
+
ratio, // Should be >= 1
|
|
1151
1231
|
};
|
|
1152
1232
|
}
|
|
1153
1233
|
|
|
@@ -1158,10 +1238,10 @@ export function verifyPathLength(segments) {
|
|
|
1158
1238
|
* @param {Object} [options] - Options
|
|
1159
1239
|
* @returns {{valid: boolean, results: Object}}
|
|
1160
1240
|
*/
|
|
1161
|
-
export function verifyAllPathFunctions(segments,
|
|
1241
|
+
export function verifyAllPathFunctions(segments, _options = {}) {
|
|
1162
1242
|
// WHY: Validate input to prevent undefined behavior and provide clear error messages
|
|
1163
1243
|
if (!segments || !Array.isArray(segments)) {
|
|
1164
|
-
throw new Error(
|
|
1244
|
+
throw new Error("verifyAllPathFunctions: segments must be an array");
|
|
1165
1245
|
}
|
|
1166
1246
|
|
|
1167
1247
|
const results = {};
|
|
@@ -1192,11 +1272,11 @@ export function verifyAllPathFunctions(segments, options = {}) {
|
|
|
1192
1272
|
results.pointInPath = verifyPointInPath(segments, [centerX, centerY]);
|
|
1193
1273
|
}
|
|
1194
1274
|
|
|
1195
|
-
const allValid = Object.values(results).every(r => r.valid);
|
|
1275
|
+
const allValid = Object.values(results).every((r) => r.valid);
|
|
1196
1276
|
|
|
1197
1277
|
return {
|
|
1198
1278
|
valid: allValid,
|
|
1199
|
-
results
|
|
1279
|
+
results,
|
|
1200
1280
|
};
|
|
1201
1281
|
}
|
|
1202
1282
|
|
|
@@ -1237,5 +1317,5 @@ export default {
|
|
|
1237
1317
|
verifyPathBoundingBox,
|
|
1238
1318
|
verifyPathContinuity,
|
|
1239
1319
|
verifyPathLength,
|
|
1240
|
-
verifyAllPathFunctions
|
|
1320
|
+
verifyAllPathFunctions,
|
|
1241
1321
|
};
|