@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
package/src/arc-length.js
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* - 10^65x better precision than float64 implementations
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import Decimal from
|
|
17
|
-
import { bezierDerivative, bezierPoint } from
|
|
16
|
+
import Decimal from "decimal.js";
|
|
17
|
+
import { bezierDerivative, bezierPoint } from "./bezier-analysis.js";
|
|
18
18
|
|
|
19
19
|
// Ensure high precision
|
|
20
20
|
Decimal.set({ precision: 80 });
|
|
21
21
|
|
|
22
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
22
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
23
23
|
|
|
24
24
|
// ============================================================================
|
|
25
25
|
// GAUSS-LEGENDRE QUADRATURE NODES AND WEIGHTS
|
|
@@ -33,47 +33,47 @@ const GAUSS_LEGENDRE = {
|
|
|
33
33
|
// 5-point rule (sufficient for most cases)
|
|
34
34
|
5: {
|
|
35
35
|
nodes: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
"-0.90617984593866399279762687829939296512565191076",
|
|
37
|
+
"-0.53846931010568309103631442070020880496728660690",
|
|
38
|
+
"0",
|
|
39
|
+
"0.53846931010568309103631442070020880496728660690",
|
|
40
|
+
"0.90617984593866399279762687829939296512565191076",
|
|
41
41
|
],
|
|
42
42
|
weights: [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
]
|
|
43
|
+
"0.23692688505618908751426404071991736264326000221",
|
|
44
|
+
"0.47862867049936646804129151483563819291229555035",
|
|
45
|
+
"0.56888888888888888888888888888888888888888888889",
|
|
46
|
+
"0.47862867049936646804129151483563819291229555035",
|
|
47
|
+
"0.23692688505618908751426404071991736264326000221",
|
|
48
|
+
],
|
|
49
49
|
},
|
|
50
50
|
// 10-point rule (for higher accuracy)
|
|
51
51
|
10: {
|
|
52
52
|
nodes: [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
"-0.97390652851717172007796401208445205342826994669",
|
|
54
|
+
"-0.86506336668898451073209668842349304852754301497",
|
|
55
|
+
"-0.67940956829902440623432736511487357576929471183",
|
|
56
|
+
"-0.43339539412924719079926594316578416220007183765",
|
|
57
|
+
"-0.14887433898163121088482600112971998461756485942",
|
|
58
|
+
"0.14887433898163121088482600112971998461756485942",
|
|
59
|
+
"0.43339539412924719079926594316578416220007183765",
|
|
60
|
+
"0.67940956829902440623432736511487357576929471183",
|
|
61
|
+
"0.86506336668898451073209668842349304852754301497",
|
|
62
|
+
"0.97390652851717172007796401208445205342826994669",
|
|
63
63
|
],
|
|
64
64
|
weights: [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]
|
|
76
|
-
}
|
|
65
|
+
"0.06667134430868813759356880989333179285786483432",
|
|
66
|
+
"0.14945134915058059314577633965769733240255644326",
|
|
67
|
+
"0.21908636251598204399553493422816219682140867715",
|
|
68
|
+
"0.26926671930999635509122692156946935285975993846",
|
|
69
|
+
"0.29552422471475287017389299465133832942104671702",
|
|
70
|
+
"0.29552422471475287017389299465133832942104671702",
|
|
71
|
+
"0.26926671930999635509122692156946935285975993846",
|
|
72
|
+
"0.21908636251598204399553493422816219682140867715",
|
|
73
|
+
"0.14945134915058059314577633965769733240255644326",
|
|
74
|
+
"0.06667134430868813759356880989333179285786483432",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// ============================================================================
|
|
@@ -86,7 +86,7 @@ const GAUSS_LEGENDRE = {
|
|
|
86
86
|
* the curve derivative is essentially zero. At such points, Newton's method
|
|
87
87
|
* would divide by near-zero, causing instability. We switch to bisection instead.
|
|
88
88
|
*/
|
|
89
|
-
const NEAR_ZERO_SPEED_THRESHOLD = new Decimal(
|
|
89
|
+
const NEAR_ZERO_SPEED_THRESHOLD = new Decimal("1e-60");
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Default tolerance for arc length computation.
|
|
@@ -94,7 +94,7 @@ const NEAR_ZERO_SPEED_THRESHOLD = new Decimal('1e-60');
|
|
|
94
94
|
* The value 1e-30 provides extremely high precision (suitable for arbitrary
|
|
95
95
|
* precision arithmetic) while still converging in reasonable time.
|
|
96
96
|
*/
|
|
97
|
-
const DEFAULT_ARC_LENGTH_TOLERANCE =
|
|
97
|
+
const DEFAULT_ARC_LENGTH_TOLERANCE = "1e-30";
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Subdivision convergence threshold.
|
|
@@ -102,7 +102,7 @@ const DEFAULT_ARC_LENGTH_TOLERANCE = '1e-30';
|
|
|
102
102
|
* by comparing 5-point and 10-point Gauss-Legendre results. When results
|
|
103
103
|
* differ by less than this, we accept the higher-order result.
|
|
104
104
|
*/
|
|
105
|
-
const
|
|
105
|
+
const _SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal("1e-15");
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Tolerance for table roundtrip verification.
|
|
@@ -110,14 +110,14 @@ const SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal('1e-15');
|
|
|
110
110
|
* produces consistent results. This tolerance accounts for interpolation error
|
|
111
111
|
* in table-based lookups.
|
|
112
112
|
*/
|
|
113
|
-
const TABLE_ROUNDTRIP_TOLERANCE = new Decimal(
|
|
113
|
+
const TABLE_ROUNDTRIP_TOLERANCE = new Decimal("1e-20");
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* Maximum relative error for subdivision comparison verification.
|
|
117
117
|
* WHY: When comparing adaptive quadrature vs subdivision methods, this tolerance
|
|
118
118
|
* accounts for the inherent approximation in chord-based subdivision.
|
|
119
119
|
*/
|
|
120
|
-
const SUBDIVISION_COMPARISON_TOLERANCE =
|
|
120
|
+
const SUBDIVISION_COMPARISON_TOLERANCE = "1e-20";
|
|
121
121
|
|
|
122
122
|
// ============================================================================
|
|
123
123
|
// ARC LENGTH COMPUTATION
|
|
@@ -148,13 +148,15 @@ export function arcLength(points, t0 = 0, t1 = 1, options = {}) {
|
|
|
148
148
|
// at least 2 control points to define a curve. Catching this early prevents
|
|
149
149
|
// cryptic errors deep in the computation.
|
|
150
150
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
151
|
-
throw new Error(
|
|
151
|
+
throw new Error(
|
|
152
|
+
"arcLength: points must be an array with at least 2 control points",
|
|
153
|
+
);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
const {
|
|
155
157
|
tolerance = DEFAULT_ARC_LENGTH_TOLERANCE,
|
|
156
158
|
maxDepth = 50,
|
|
157
|
-
minDepth = 3
|
|
159
|
+
minDepth = 3,
|
|
158
160
|
} = options;
|
|
159
161
|
|
|
160
162
|
const t0D = D(t0);
|
|
@@ -172,13 +174,13 @@ export function arcLength(points, t0 = 0, t1 = 1, options = {}) {
|
|
|
172
174
|
|
|
173
175
|
// Use adaptive quadrature
|
|
174
176
|
return adaptiveQuadrature(
|
|
175
|
-
t => speedAtT(points, t),
|
|
177
|
+
(t) => speedAtT(points, t),
|
|
176
178
|
t0D,
|
|
177
179
|
t1D,
|
|
178
180
|
tol,
|
|
179
181
|
maxDepth,
|
|
180
182
|
minDepth,
|
|
181
|
-
0
|
|
183
|
+
0,
|
|
182
184
|
);
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -188,8 +190,8 @@ export function arcLength(points, t0 = 0, t1 = 1, options = {}) {
|
|
|
188
190
|
* WHY: Speed is the magnitude of the velocity vector (first derivative).
|
|
189
191
|
* This is the integrand for arc length: L = integral of |B'(t)| dt.
|
|
190
192
|
*
|
|
191
|
-
* @param {Array} points - Control points
|
|
192
|
-
* @param {Decimal} t - Parameter
|
|
193
|
+
* @param {Array} points - Control points [[x,y], ...]
|
|
194
|
+
* @param {Decimal} t - Parameter in [0, 1]
|
|
193
195
|
* @returns {Decimal} Speed (magnitude of derivative)
|
|
194
196
|
*/
|
|
195
197
|
function speedAtT(points, t) {
|
|
@@ -211,14 +213,14 @@ function speedAtT(points, t) {
|
|
|
211
213
|
* Subdivides intervals where the integrand varies significantly,
|
|
212
214
|
* ensuring accuracy while minimizing computation.
|
|
213
215
|
*
|
|
214
|
-
* @param {Function} f - Function to integrate
|
|
216
|
+
* @param {Function} f - Function to integrate (takes Decimal, returns Decimal)
|
|
215
217
|
* @param {Decimal} a - Start of interval
|
|
216
218
|
* @param {Decimal} b - End of interval
|
|
217
219
|
* @param {Decimal} tol - Error tolerance
|
|
218
220
|
* @param {number} maxDepth - Maximum recursion depth
|
|
219
221
|
* @param {number} minDepth - Minimum recursion depth
|
|
220
|
-
* @param {number} depth - Current depth
|
|
221
|
-
* @returns {Decimal} Integral
|
|
222
|
+
* @param {number} depth - Current recursion depth
|
|
223
|
+
* @returns {Decimal} Integral approximation
|
|
222
224
|
*/
|
|
223
225
|
function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
224
226
|
// Compute integral using 5-point and 10-point rules
|
|
@@ -236,8 +238,24 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
|
236
238
|
const mid = a.plus(b).div(2);
|
|
237
239
|
const halfTol = tol.div(2);
|
|
238
240
|
|
|
239
|
-
const leftIntegral = adaptiveQuadrature(
|
|
240
|
-
|
|
241
|
+
const leftIntegral = adaptiveQuadrature(
|
|
242
|
+
f,
|
|
243
|
+
a,
|
|
244
|
+
mid,
|
|
245
|
+
halfTol,
|
|
246
|
+
maxDepth,
|
|
247
|
+
minDepth,
|
|
248
|
+
depth + 1,
|
|
249
|
+
);
|
|
250
|
+
const rightIntegral = adaptiveQuadrature(
|
|
251
|
+
f,
|
|
252
|
+
mid,
|
|
253
|
+
b,
|
|
254
|
+
halfTol,
|
|
255
|
+
maxDepth,
|
|
256
|
+
minDepth,
|
|
257
|
+
depth + 1,
|
|
258
|
+
);
|
|
241
259
|
|
|
242
260
|
return leftIntegral.plus(rightIntegral);
|
|
243
261
|
}
|
|
@@ -248,10 +266,10 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
|
248
266
|
* Transforms integral from [a,b] to [-1,1] and applies formula:
|
|
249
267
|
* integral ≈ (b-a)/2 * sum of (weight[i] * f(transformed_node[i]))
|
|
250
268
|
*
|
|
251
|
-
* @param {Function} f - Function to integrate
|
|
269
|
+
* @param {Function} f - Function to integrate (takes Decimal, returns Decimal)
|
|
252
270
|
* @param {Decimal} a - Start of interval
|
|
253
271
|
* @param {Decimal} b - End of interval
|
|
254
|
-
* @param {number} order - Number of points (5 or 10)
|
|
272
|
+
* @param {number} order - Number of quadrature points (5 or 10)
|
|
255
273
|
* @returns {Decimal} Integral approximation
|
|
256
274
|
*/
|
|
257
275
|
function gaussLegendre(f, a, b, order) {
|
|
@@ -306,14 +324,16 @@ export function inverseArcLength(points, targetLength, options = {}) {
|
|
|
306
324
|
// WHY: inverseArcLength calls arcLength internally, which requires valid points.
|
|
307
325
|
// Catching this early provides clearer error messages to users.
|
|
308
326
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
309
|
-
throw new Error(
|
|
327
|
+
throw new Error(
|
|
328
|
+
"inverseArcLength: points must be an array with at least 2 control points",
|
|
329
|
+
);
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
const {
|
|
313
333
|
tolerance = DEFAULT_ARC_LENGTH_TOLERANCE,
|
|
314
334
|
maxIterations = 100,
|
|
315
335
|
lengthTolerance = DEFAULT_ARC_LENGTH_TOLERANCE,
|
|
316
|
-
initialT
|
|
336
|
+
initialT,
|
|
317
337
|
} = options;
|
|
318
338
|
|
|
319
339
|
const target = D(targetLength);
|
|
@@ -325,7 +345,7 @@ export function inverseArcLength(points, targetLength, options = {}) {
|
|
|
325
345
|
// a magnitude). Accepting negative values would be nonsensical and lead to
|
|
326
346
|
// incorrect results or infinite loops in Newton's method.
|
|
327
347
|
if (target.lt(0)) {
|
|
328
|
-
throw new Error(
|
|
348
|
+
throw new Error("inverseArcLength: targetLength must be non-negative");
|
|
329
349
|
}
|
|
330
350
|
|
|
331
351
|
// Handle edge case: zero length
|
|
@@ -403,7 +423,7 @@ export function inverseArcLength(points, targetLength, options = {}) {
|
|
|
403
423
|
t,
|
|
404
424
|
length: finalLength,
|
|
405
425
|
iterations,
|
|
406
|
-
converged
|
|
426
|
+
converged,
|
|
407
427
|
};
|
|
408
428
|
}
|
|
409
429
|
|
|
@@ -423,10 +443,10 @@ export function pathArcLength(segments, options = {}) {
|
|
|
423
443
|
// WHY: We need to iterate over segments and call arcLength on each.
|
|
424
444
|
// Catching invalid input early prevents cryptic errors in the loop.
|
|
425
445
|
if (!segments || !Array.isArray(segments)) {
|
|
426
|
-
throw new Error(
|
|
446
|
+
throw new Error("pathArcLength: segments must be an array");
|
|
427
447
|
}
|
|
428
448
|
if (segments.length === 0) {
|
|
429
|
-
throw new Error(
|
|
449
|
+
throw new Error("pathArcLength: segments array must not be empty");
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
let total = D(0);
|
|
@@ -451,10 +471,10 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
451
471
|
// INPUT VALIDATION: Ensure segments is a valid array
|
|
452
472
|
// WHY: We need to iterate over segments to find which one contains the target length.
|
|
453
473
|
if (!segments || !Array.isArray(segments)) {
|
|
454
|
-
throw new Error(
|
|
474
|
+
throw new Error("pathInverseArcLength: segments must be an array");
|
|
455
475
|
}
|
|
456
476
|
if (segments.length === 0) {
|
|
457
|
-
throw new Error(
|
|
477
|
+
throw new Error("pathInverseArcLength: segments array must not be empty");
|
|
458
478
|
}
|
|
459
479
|
|
|
460
480
|
const target = D(targetLength);
|
|
@@ -462,7 +482,7 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
462
482
|
// FAIL FAST: Negative arc length is invalid
|
|
463
483
|
// WHY: Same reason as inverseArcLength - arc length is non-negative by definition.
|
|
464
484
|
if (target.lt(0)) {
|
|
465
|
-
throw new Error(
|
|
485
|
+
throw new Error("pathInverseArcLength: targetLength must be non-negative");
|
|
466
486
|
}
|
|
467
487
|
|
|
468
488
|
// EDGE CASE: Zero target length
|
|
@@ -471,7 +491,7 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
471
491
|
return {
|
|
472
492
|
segmentIndex: 0,
|
|
473
493
|
t: D(0),
|
|
474
|
-
totalLength: D(0)
|
|
494
|
+
totalLength: D(0),
|
|
475
495
|
};
|
|
476
496
|
}
|
|
477
497
|
|
|
@@ -489,7 +509,7 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
489
509
|
return {
|
|
490
510
|
segmentIndex: i,
|
|
491
511
|
t,
|
|
492
|
-
totalLength: accumulated.plus(arcLength(segments[i], 0, t, options))
|
|
512
|
+
totalLength: accumulated.plus(arcLength(segments[i], 0, t, options)),
|
|
493
513
|
};
|
|
494
514
|
}
|
|
495
515
|
|
|
@@ -500,7 +520,7 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
500
520
|
return {
|
|
501
521
|
segmentIndex: segments.length - 1,
|
|
502
522
|
t: D(1),
|
|
503
|
-
totalLength: accumulated
|
|
523
|
+
totalLength: accumulated,
|
|
504
524
|
};
|
|
505
525
|
}
|
|
506
526
|
|
|
@@ -522,10 +542,14 @@ export function pathInverseArcLength(segments, targetLength, options = {}) {
|
|
|
522
542
|
export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
523
543
|
// Input validation
|
|
524
544
|
if (!points || points.length < 2) {
|
|
525
|
-
throw new Error(
|
|
545
|
+
throw new Error(
|
|
546
|
+
"createArcLengthTable: points must have at least 2 control points",
|
|
547
|
+
);
|
|
526
548
|
}
|
|
527
549
|
if (samples < 2) {
|
|
528
|
-
throw new Error(
|
|
550
|
+
throw new Error(
|
|
551
|
+
"createArcLengthTable: samples must be at least 2 (for binary search to work)",
|
|
552
|
+
);
|
|
529
553
|
}
|
|
530
554
|
|
|
531
555
|
const table = [];
|
|
@@ -601,7 +625,7 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
|
601
625
|
// Use approxT as starting point for Newton
|
|
602
626
|
const { t } = inverseArcLength(points, s, { ...opts, initialT: approxT });
|
|
603
627
|
return t;
|
|
604
|
-
}
|
|
628
|
+
},
|
|
605
629
|
};
|
|
606
630
|
}
|
|
607
631
|
|
|
@@ -623,17 +647,23 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
623
647
|
// WHY: This function needs to access points[0], points[length-1], and iterate
|
|
624
648
|
// over points to compute polygon length. Invalid input would cause errors.
|
|
625
649
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
626
|
-
throw new Error(
|
|
650
|
+
throw new Error(
|
|
651
|
+
"verifyArcLength: points must be an array with at least 2 control points",
|
|
652
|
+
);
|
|
627
653
|
}
|
|
628
654
|
|
|
629
655
|
const errors = [];
|
|
630
656
|
|
|
631
657
|
// Compute arc length if not provided
|
|
632
|
-
const length =
|
|
658
|
+
const length =
|
|
659
|
+
computedLength !== null ? D(computedLength) : arcLength(points);
|
|
633
660
|
|
|
634
661
|
// Chord length (straight line from start to end)
|
|
635
662
|
const [x0, y0] = [D(points[0][0]), D(points[0][1])];
|
|
636
|
-
const [xn, yn] = [
|
|
663
|
+
const [xn, yn] = [
|
|
664
|
+
D(points[points.length - 1][0]),
|
|
665
|
+
D(points[points.length - 1][1]),
|
|
666
|
+
];
|
|
637
667
|
const chordLength = xn.minus(x0).pow(2).plus(yn.minus(y0).pow(2)).sqrt();
|
|
638
668
|
|
|
639
669
|
// Control polygon length
|
|
@@ -641,7 +671,9 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
641
671
|
for (let i = 0; i < points.length - 1; i++) {
|
|
642
672
|
const [x1, y1] = [D(points[i][0]), D(points[i][1])];
|
|
643
673
|
const [x2, y2] = [D(points[i + 1][0]), D(points[i + 1][1])];
|
|
644
|
-
polygonLength = polygonLength.plus(
|
|
674
|
+
polygonLength = polygonLength.plus(
|
|
675
|
+
x2.minus(x1).pow(2).plus(y2.minus(y1).pow(2)).sqrt(),
|
|
676
|
+
);
|
|
645
677
|
}
|
|
646
678
|
|
|
647
679
|
// Check bounds
|
|
@@ -658,7 +690,7 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
658
690
|
polygonLength,
|
|
659
691
|
arcLength: length,
|
|
660
692
|
ratio: chordLength.gt(0) ? length.div(chordLength) : D(1),
|
|
661
|
-
errors
|
|
693
|
+
errors,
|
|
662
694
|
};
|
|
663
695
|
}
|
|
664
696
|
|
|
@@ -671,12 +703,18 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
671
703
|
* @param {number|string|Decimal} [tolerance='1e-25'] - Maximum error
|
|
672
704
|
* @returns {{valid: boolean, targetLength: Decimal, foundT: Decimal, verifiedLength: Decimal, error: Decimal}}
|
|
673
705
|
*/
|
|
674
|
-
export function verifyInverseArcLength(
|
|
706
|
+
export function verifyInverseArcLength(
|
|
707
|
+
points,
|
|
708
|
+
targetLength,
|
|
709
|
+
tolerance = "1e-25",
|
|
710
|
+
) {
|
|
675
711
|
// INPUT VALIDATION: Ensure points array is valid
|
|
676
712
|
// WHY: This function calls inverseArcLength and arcLength, both of which require
|
|
677
713
|
// valid points. We validate early for clearer error messages.
|
|
678
714
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
679
|
-
throw new Error(
|
|
715
|
+
throw new Error(
|
|
716
|
+
"verifyInverseArcLength: points must be an array with at least 2 control points",
|
|
717
|
+
);
|
|
680
718
|
}
|
|
681
719
|
|
|
682
720
|
const target = D(targetLength);
|
|
@@ -684,7 +722,9 @@ export function verifyInverseArcLength(points, targetLength, tolerance = '1e-25'
|
|
|
684
722
|
// FAIL FAST: Validate targetLength is non-negative
|
|
685
723
|
// WHY: Negative arc lengths are mathematically invalid. This prevents nonsensical tests.
|
|
686
724
|
if (target.lt(0)) {
|
|
687
|
-
throw new Error(
|
|
725
|
+
throw new Error(
|
|
726
|
+
"verifyInverseArcLength: targetLength must be non-negative",
|
|
727
|
+
);
|
|
688
728
|
}
|
|
689
729
|
|
|
690
730
|
const tol = D(tolerance);
|
|
@@ -704,7 +744,7 @@ export function verifyInverseArcLength(points, targetLength, tolerance = '1e-25'
|
|
|
704
744
|
foundT,
|
|
705
745
|
verifiedLength,
|
|
706
746
|
error,
|
|
707
|
-
converged
|
|
747
|
+
converged,
|
|
708
748
|
};
|
|
709
749
|
}
|
|
710
750
|
|
|
@@ -717,12 +757,18 @@ export function verifyInverseArcLength(points, targetLength, tolerance = '1e-25'
|
|
|
717
757
|
* @param {number|string|Decimal} [tolerance='1e-20'] - Maximum difference
|
|
718
758
|
* @returns {{valid: boolean, quadratureLength: Decimal, subdivisionLength: Decimal, difference: Decimal}}
|
|
719
759
|
*/
|
|
720
|
-
export function verifyArcLengthBySubdivision(
|
|
760
|
+
export function verifyArcLengthBySubdivision(
|
|
761
|
+
points,
|
|
762
|
+
subdivisions = 16,
|
|
763
|
+
tolerance = SUBDIVISION_COMPARISON_TOLERANCE,
|
|
764
|
+
) {
|
|
721
765
|
// INPUT VALIDATION: Ensure points array is valid
|
|
722
766
|
// WHY: This function calls arcLength and bezierPoint, both of which require
|
|
723
767
|
// valid control points. Early validation provides better error messages.
|
|
724
768
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
725
|
-
throw new Error(
|
|
769
|
+
throw new Error(
|
|
770
|
+
"verifyArcLengthBySubdivision: points must be an array with at least 2 control points",
|
|
771
|
+
);
|
|
726
772
|
}
|
|
727
773
|
|
|
728
774
|
const tol = D(tolerance);
|
|
@@ -740,7 +786,9 @@ export function verifyArcLengthBySubdivision(points, subdivisions = 16, toleranc
|
|
|
740
786
|
|
|
741
787
|
const dx = D(currPoint[0]).minus(D(prevPoint[0]));
|
|
742
788
|
const dy = D(currPoint[1]).minus(D(prevPoint[1]));
|
|
743
|
-
subdivisionLength = subdivisionLength.plus(
|
|
789
|
+
subdivisionLength = subdivisionLength.plus(
|
|
790
|
+
dx.pow(2).plus(dy.pow(2)).sqrt(),
|
|
791
|
+
);
|
|
744
792
|
|
|
745
793
|
prevPoint = currPoint;
|
|
746
794
|
}
|
|
@@ -754,7 +802,7 @@ export function verifyArcLengthBySubdivision(points, subdivisions = 16, toleranc
|
|
|
754
802
|
quadratureLength,
|
|
755
803
|
subdivisionLength,
|
|
756
804
|
difference,
|
|
757
|
-
underestimate: quadratureLength.gt(subdivisionLength)
|
|
805
|
+
underestimate: quadratureLength.gt(subdivisionLength),
|
|
758
806
|
};
|
|
759
807
|
}
|
|
760
808
|
|
|
@@ -766,19 +814,25 @@ export function verifyArcLengthBySubdivision(points, subdivisions = 16, toleranc
|
|
|
766
814
|
* @param {number|string|Decimal} [tolerance='1e-30'] - Maximum error
|
|
767
815
|
* @returns {{valid: boolean, totalLength: Decimal, leftLength: Decimal, rightLength: Decimal, sum: Decimal, error: Decimal}}
|
|
768
816
|
*/
|
|
769
|
-
export function verifyArcLengthAdditivity(
|
|
817
|
+
export function verifyArcLengthAdditivity(
|
|
818
|
+
points,
|
|
819
|
+
t,
|
|
820
|
+
tolerance = DEFAULT_ARC_LENGTH_TOLERANCE,
|
|
821
|
+
) {
|
|
770
822
|
// INPUT VALIDATION: Ensure points array is valid
|
|
771
823
|
// WHY: This function calls arcLength multiple times with the same points array.
|
|
772
824
|
// Validating once here is more efficient than letting each call validate.
|
|
773
825
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
774
|
-
throw new Error(
|
|
826
|
+
throw new Error(
|
|
827
|
+
"verifyArcLengthAdditivity: points must be an array with at least 2 control points",
|
|
828
|
+
);
|
|
775
829
|
}
|
|
776
830
|
|
|
777
831
|
const tD = D(t);
|
|
778
832
|
// PARAMETER VALIDATION: t must be in [0, 1] for additivity to make sense
|
|
779
833
|
// WHY: Arc length additivity L(0,t) + L(t,1) = L(0,1) only holds for t in [0,1]
|
|
780
834
|
if (tD.lt(0) || tD.gt(1)) {
|
|
781
|
-
throw new Error(
|
|
835
|
+
throw new Error("verifyArcLengthAdditivity: t must be in range [0, 1]");
|
|
782
836
|
}
|
|
783
837
|
|
|
784
838
|
const tol = D(tolerance);
|
|
@@ -796,7 +850,7 @@ export function verifyArcLengthAdditivity(points, t, tolerance = DEFAULT_ARC_LEN
|
|
|
796
850
|
leftLength,
|
|
797
851
|
rightLength,
|
|
798
852
|
sum,
|
|
799
|
-
error
|
|
853
|
+
error,
|
|
800
854
|
};
|
|
801
855
|
}
|
|
802
856
|
|
|
@@ -812,7 +866,9 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
812
866
|
// WHY: This function calls createArcLengthTable and arcLength, both requiring
|
|
813
867
|
// valid points. Early validation provides better diagnostics.
|
|
814
868
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
815
|
-
throw new Error(
|
|
869
|
+
throw new Error(
|
|
870
|
+
"verifyArcLengthTable: points must be an array with at least 2 control points",
|
|
871
|
+
);
|
|
816
872
|
}
|
|
817
873
|
|
|
818
874
|
const errors = [];
|
|
@@ -840,7 +896,9 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
840
896
|
// Verify table boundaries
|
|
841
897
|
const firstEntry = table.table[0];
|
|
842
898
|
if (!firstEntry.t.isZero() || !firstEntry.length.isZero()) {
|
|
843
|
-
errors.push(
|
|
899
|
+
errors.push(
|
|
900
|
+
`First entry should be t=0, length=0, got t=${firstEntry.t}, length=${firstEntry.length}`,
|
|
901
|
+
);
|
|
844
902
|
}
|
|
845
903
|
|
|
846
904
|
const lastEntry = table.table[table.table.length - 1];
|
|
@@ -853,7 +911,9 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
853
911
|
const tableTotalDiff = table.totalLength.minus(directLength).abs();
|
|
854
912
|
// WHY: Use TABLE_ROUNDTRIP_TOLERANCE to account for accumulated segment errors
|
|
855
913
|
if (tableTotalDiff.gt(TABLE_ROUNDTRIP_TOLERANCE)) {
|
|
856
|
-
errors.push(
|
|
914
|
+
errors.push(
|
|
915
|
+
`Table total length ${table.totalLength} differs from direct computation ${directLength}`,
|
|
916
|
+
);
|
|
857
917
|
}
|
|
858
918
|
|
|
859
919
|
// Verify getT roundtrip for a few values
|
|
@@ -864,7 +924,9 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
864
924
|
const roundtripError = recoveredLength.minus(targetLength).abs();
|
|
865
925
|
|
|
866
926
|
if (roundtripError.gt(table.totalLength.div(samples).times(2))) {
|
|
867
|
-
errors.push(
|
|
927
|
+
errors.push(
|
|
928
|
+
`getT roundtrip error too large at ${fraction}: ${roundtripError}`,
|
|
929
|
+
);
|
|
868
930
|
}
|
|
869
931
|
}
|
|
870
932
|
|
|
@@ -874,7 +936,7 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
874
936
|
isMonotonic,
|
|
875
937
|
maxGap,
|
|
876
938
|
tableSize: table.table.length,
|
|
877
|
-
totalLength: table.totalLength
|
|
939
|
+
totalLength: table.totalLength,
|
|
878
940
|
};
|
|
879
941
|
}
|
|
880
942
|
|
|
@@ -885,12 +947,14 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
885
947
|
* @param {Object} [options] - Options
|
|
886
948
|
* @returns {{valid: boolean, results: Object}}
|
|
887
949
|
*/
|
|
888
|
-
export function verifyAllArcLengthFunctions(points,
|
|
950
|
+
export function verifyAllArcLengthFunctions(points, _options = {}) {
|
|
889
951
|
// INPUT VALIDATION: Ensure points array is valid
|
|
890
952
|
// WHY: This function orchestrates multiple verification functions, all of which
|
|
891
953
|
// require valid points. Validating once at the top prevents redundant checks.
|
|
892
954
|
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
893
|
-
throw new Error(
|
|
955
|
+
throw new Error(
|
|
956
|
+
"verifyAllArcLengthFunctions: points must be an array with at least 2 control points",
|
|
957
|
+
);
|
|
894
958
|
}
|
|
895
959
|
|
|
896
960
|
const results = {};
|
|
@@ -911,11 +975,11 @@ export function verifyAllArcLengthFunctions(points, options = {}) {
|
|
|
911
975
|
// 5. Verify table
|
|
912
976
|
results.table = verifyArcLengthTable(points, 20);
|
|
913
977
|
|
|
914
|
-
const allValid = Object.values(results).every(r => r.valid);
|
|
978
|
+
const allValid = Object.values(results).every((r) => r.valid);
|
|
915
979
|
|
|
916
980
|
return {
|
|
917
981
|
valid: allValid,
|
|
918
|
-
results
|
|
982
|
+
results,
|
|
919
983
|
};
|
|
920
984
|
}
|
|
921
985
|
|
|
@@ -936,5 +1000,5 @@ export default {
|
|
|
936
1000
|
verifyArcLengthBySubdivision,
|
|
937
1001
|
verifyArcLengthAdditivity,
|
|
938
1002
|
verifyArcLengthTable,
|
|
939
|
-
verifyAllArcLengthFunctions
|
|
1003
|
+
verifyAllArcLengthFunctions,
|
|
940
1004
|
};
|