@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/arc-length.js
CHANGED
|
@@ -101,7 +101,9 @@ const DEFAULT_ARC_LENGTH_TOLERANCE = "1e-30";
|
|
|
101
101
|
* WHY: Used in adaptive quadrature to determine if subdivision has converged
|
|
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
|
+
* NOTE: Currently unused - kept for potential future enhancement.
|
|
104
105
|
*/
|
|
106
|
+
|
|
105
107
|
const _SUBDIVISION_CONVERGENCE_THRESHOLD = new Decimal("1e-15");
|
|
106
108
|
|
|
107
109
|
/**
|
|
@@ -195,6 +197,17 @@ export function arcLength(points, t0 = 0, t1 = 1, options = {}) {
|
|
|
195
197
|
* @returns {Decimal} Speed (magnitude of derivative)
|
|
196
198
|
*/
|
|
197
199
|
function speedAtT(points, t) {
|
|
200
|
+
// INPUT VALIDATION: Ensure points array and t parameter are valid
|
|
201
|
+
// WHY: bezierDerivative will fail cryptically if points is invalid or t is not a valid number
|
|
202
|
+
if (!points || !Array.isArray(points) || points.length < 2) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"speedAtT: points must be an array with at least 2 control points",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (t === null || t === undefined) {
|
|
208
|
+
throw new Error("speedAtT: t parameter is required");
|
|
209
|
+
}
|
|
210
|
+
|
|
198
211
|
const [dx, dy] = bezierDerivative(points, t, 1);
|
|
199
212
|
const speedSquared = dx.times(dx).plus(dy.times(dy));
|
|
200
213
|
|
|
@@ -223,6 +236,50 @@ function speedAtT(points, t) {
|
|
|
223
236
|
* @returns {Decimal} Integral approximation
|
|
224
237
|
*/
|
|
225
238
|
function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
239
|
+
// INPUT VALIDATION: Ensure all parameters are valid
|
|
240
|
+
// WHY: Invalid parameters would cause cryptic errors deep in recursion
|
|
241
|
+
if (typeof f !== "function") {
|
|
242
|
+
throw new Error("adaptiveQuadrature: f must be a function");
|
|
243
|
+
}
|
|
244
|
+
if (!a || !(a instanceof Decimal)) {
|
|
245
|
+
throw new Error("adaptiveQuadrature: a must be a Decimal");
|
|
246
|
+
}
|
|
247
|
+
if (!b || !(b instanceof Decimal)) {
|
|
248
|
+
throw new Error("adaptiveQuadrature: b must be a Decimal");
|
|
249
|
+
}
|
|
250
|
+
if (!tol || !(tol instanceof Decimal) || tol.lte(0)) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"adaptiveQuadrature: tol must be a positive Decimal",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
typeof maxDepth !== "number" ||
|
|
257
|
+
maxDepth < 0 ||
|
|
258
|
+
!Number.isInteger(maxDepth)
|
|
259
|
+
) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"adaptiveQuadrature: maxDepth must be a non-negative integer",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
typeof minDepth !== "number" ||
|
|
266
|
+
minDepth < 0 ||
|
|
267
|
+
!Number.isInteger(minDepth)
|
|
268
|
+
) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
"adaptiveQuadrature: minDepth must be a non-negative integer",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
typeof depth !== "number" ||
|
|
275
|
+
depth < 0 ||
|
|
276
|
+
!Number.isInteger(depth)
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"adaptiveQuadrature: depth must be a non-negative integer",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
226
283
|
// Compute integral using 5-point and 10-point rules
|
|
227
284
|
const I5 = gaussLegendre(f, a, b, 5);
|
|
228
285
|
const I10 = gaussLegendre(f, a, b, 10);
|
|
@@ -273,7 +330,31 @@ function adaptiveQuadrature(f, a, b, tol, maxDepth, minDepth, depth) {
|
|
|
273
330
|
* @returns {Decimal} Integral approximation
|
|
274
331
|
*/
|
|
275
332
|
function gaussLegendre(f, a, b, order) {
|
|
333
|
+
// INPUT VALIDATION: Ensure order is valid
|
|
334
|
+
// WHY: GAUSS_LEGENDRE only has entries for orders 5 and 10. Invalid order would cause undefined access.
|
|
335
|
+
if (order !== 5 && order !== 10) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"gaussLegendre: order must be 5 or 10 (only supported orders)",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
276
341
|
const gl = GAUSS_LEGENDRE[order];
|
|
342
|
+
|
|
343
|
+
// INTERNAL CONSISTENCY CHECK: Verify Gauss-Legendre table has correct structure
|
|
344
|
+
// WHY: Ensures the precomputed tables haven't been corrupted or misconfigured
|
|
345
|
+
if (!gl.nodes || !gl.weights || gl.nodes.length !== order || gl.weights.length !== order) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`gaussLegendre: GAUSS_LEGENDRE[${order}] table is malformed (expected ${order} nodes and weights)`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// DIVISION BY ZERO CHECK: Ensure a != b
|
|
352
|
+
// WHY: If a == b, the integral is zero (no width), and halfWidth would be 0.
|
|
353
|
+
// While multiplying by 0 gives correct result (0), we should handle explicitly.
|
|
354
|
+
if (a.eq(b)) {
|
|
355
|
+
return D(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
277
358
|
const halfWidth = b.minus(a).div(2);
|
|
278
359
|
const center = a.plus(b).div(2);
|
|
279
360
|
|
|
@@ -288,6 +369,22 @@ function gaussLegendre(f, a, b, order) {
|
|
|
288
369
|
|
|
289
370
|
// Evaluate function and add weighted contribution
|
|
290
371
|
const fValue = f(t);
|
|
372
|
+
|
|
373
|
+
// VALIDATION: Ensure function returns a valid Decimal
|
|
374
|
+
// WHY: If f returns null, undefined, NaN, or non-Decimal, arithmetic operations will fail
|
|
375
|
+
if (fValue === null || fValue === undefined) {
|
|
376
|
+
throw new Error("gaussLegendre: integrand function f returned null or undefined");
|
|
377
|
+
}
|
|
378
|
+
if (!(fValue instanceof Decimal)) {
|
|
379
|
+
throw new Error("gaussLegendre: integrand function f must return a Decimal instance");
|
|
380
|
+
}
|
|
381
|
+
if (fValue.isNaN()) {
|
|
382
|
+
throw new Error(`gaussLegendre: integrand function f returned NaN at t=${t}`);
|
|
383
|
+
}
|
|
384
|
+
if (!fValue.isFinite()) {
|
|
385
|
+
throw new Error(`gaussLegendre: integrand function f returned non-finite value at t=${t}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
291
388
|
sum = sum.plus(weight.times(fValue));
|
|
292
389
|
}
|
|
293
390
|
|
|
@@ -336,6 +433,18 @@ export function inverseArcLength(points, targetLength, options = {}) {
|
|
|
336
433
|
initialT,
|
|
337
434
|
} = options;
|
|
338
435
|
|
|
436
|
+
// INPUT VALIDATION: Ensure maxIterations is a positive integer
|
|
437
|
+
// WHY: maxIterations controls the loop; negative or zero values would prevent convergence
|
|
438
|
+
if (
|
|
439
|
+
typeof maxIterations !== "number" ||
|
|
440
|
+
maxIterations <= 0 ||
|
|
441
|
+
!Number.isInteger(maxIterations)
|
|
442
|
+
) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
"inverseArcLength: maxIterations must be a positive integer",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
339
448
|
const target = D(targetLength);
|
|
340
449
|
const tol = D(tolerance);
|
|
341
450
|
const lengthOpts = { tolerance: lengthTolerance };
|
|
@@ -348,6 +457,15 @@ export function inverseArcLength(points, targetLength, options = {}) {
|
|
|
348
457
|
throw new Error("inverseArcLength: targetLength must be non-negative");
|
|
349
458
|
}
|
|
350
459
|
|
|
460
|
+
// EDGE CASE: Check for NaN or Infinity in targetLength
|
|
461
|
+
// WHY: NaN or Infinity would cause Newton's method to diverge or produce nonsensical results
|
|
462
|
+
if (target.isNaN()) {
|
|
463
|
+
throw new Error("inverseArcLength: targetLength cannot be NaN");
|
|
464
|
+
}
|
|
465
|
+
if (!target.isFinite()) {
|
|
466
|
+
throw new Error("inverseArcLength: targetLength must be finite");
|
|
467
|
+
}
|
|
468
|
+
|
|
351
469
|
// Handle edge case: zero length
|
|
352
470
|
if (target.isZero()) {
|
|
353
471
|
return { t: D(0), length: D(0), iterations: 0, converged: true };
|
|
@@ -546,9 +664,15 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
|
546
664
|
"createArcLengthTable: points must have at least 2 control points",
|
|
547
665
|
);
|
|
548
666
|
}
|
|
549
|
-
|
|
667
|
+
// TYPE CHECK: Ensure samples is a valid number
|
|
668
|
+
// WHY: samples is used in arithmetic operations and as a loop bound
|
|
669
|
+
if (
|
|
670
|
+
typeof samples !== "number" ||
|
|
671
|
+
!Number.isInteger(samples) ||
|
|
672
|
+
samples < 2
|
|
673
|
+
) {
|
|
550
674
|
throw new Error(
|
|
551
|
-
"createArcLengthTable: samples must be
|
|
675
|
+
"createArcLengthTable: samples must be an integer >= 2 (for binary search to work)",
|
|
552
676
|
);
|
|
553
677
|
}
|
|
554
678
|
|
|
@@ -580,8 +704,23 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
|
580
704
|
* @returns {Decimal} Approximate t
|
|
581
705
|
*/
|
|
582
706
|
getT(s) {
|
|
707
|
+
// INPUT VALIDATION: Ensure s is provided and valid
|
|
708
|
+
// WHY: Without s, we cannot perform the lookup. Catching this early prevents cryptic errors.
|
|
709
|
+
if (s === null || s === undefined) {
|
|
710
|
+
throw new Error("getT: arc length parameter s is required");
|
|
711
|
+
}
|
|
712
|
+
|
|
583
713
|
const sD = D(s);
|
|
584
714
|
|
|
715
|
+
// EDGE CASE: Check for NaN or Infinity
|
|
716
|
+
// WHY: Invalid arc lengths would cause incorrect lookups
|
|
717
|
+
if (sD.isNaN()) {
|
|
718
|
+
throw new Error("getT: arc length s cannot be NaN");
|
|
719
|
+
}
|
|
720
|
+
if (!sD.isFinite()) {
|
|
721
|
+
throw new Error("getT: arc length s must be finite");
|
|
722
|
+
}
|
|
723
|
+
|
|
585
724
|
if (sD.lte(0)) return D(0);
|
|
586
725
|
if (sD.gte(this.totalLength)) return D(1);
|
|
587
726
|
|
|
@@ -610,7 +749,15 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
|
610
749
|
const t0 = table[lo].t;
|
|
611
750
|
const t1 = table[hi].t;
|
|
612
751
|
|
|
613
|
-
|
|
752
|
+
// DIVISION BY ZERO CHECK: Handle degenerate case where s0 == s1
|
|
753
|
+
// WHY: If two consecutive table entries have the same arc length (e.g., at a cusp),
|
|
754
|
+
// division would fail. In this case, we return the midpoint t value.
|
|
755
|
+
const denominator = s1.minus(s0);
|
|
756
|
+
if (denominator.isZero()) {
|
|
757
|
+
return t0.plus(t1).div(2);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const fraction = sD.minus(s0).div(denominator);
|
|
614
761
|
return t0.plus(t1.minus(t0).times(fraction));
|
|
615
762
|
},
|
|
616
763
|
|
|
@@ -621,6 +768,13 @@ export function createArcLengthTable(points, samples = 100, options = {}) {
|
|
|
621
768
|
* @returns {Decimal} Refined t
|
|
622
769
|
*/
|
|
623
770
|
getTRefined(s, opts = {}) {
|
|
771
|
+
// INPUT VALIDATION: Ensure s is provided and valid
|
|
772
|
+
// WHY: This method calls getT internally which will validate, but we validate
|
|
773
|
+
// here too for consistent error messages at the API boundary.
|
|
774
|
+
if (s === null || s === undefined) {
|
|
775
|
+
throw new Error("getTRefined: arc length parameter s is required");
|
|
776
|
+
}
|
|
777
|
+
|
|
624
778
|
const approxT = this.getT(s);
|
|
625
779
|
// Use approxT as starting point for Newton
|
|
626
780
|
const { t } = inverseArcLength(points, s, { ...opts, initialT: approxT });
|
|
@@ -652,6 +806,19 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
652
806
|
);
|
|
653
807
|
}
|
|
654
808
|
|
|
809
|
+
// ARRAY ELEMENT VALIDATION: Ensure points array elements are valid [x, y] pairs
|
|
810
|
+
// WHY: We access points[i][0] and points[i][1] below. Invalid structure would cause errors.
|
|
811
|
+
if (
|
|
812
|
+
!Array.isArray(points[0]) ||
|
|
813
|
+
points[0].length < 2 ||
|
|
814
|
+
!Array.isArray(points[points.length - 1]) ||
|
|
815
|
+
points[points.length - 1].length < 2
|
|
816
|
+
) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
"verifyArcLength: each point must be an array with at least 2 coordinates [x, y]",
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
655
822
|
const errors = [];
|
|
656
823
|
|
|
657
824
|
// Compute arc length if not provided
|
|
@@ -669,6 +836,19 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
669
836
|
// Control polygon length
|
|
670
837
|
let polygonLength = D(0);
|
|
671
838
|
for (let i = 0; i < points.length - 1; i++) {
|
|
839
|
+
// ARRAY ELEMENT VALIDATION: Check each point in the loop
|
|
840
|
+
// WHY: Ensures all intermediate points are valid before accessing coordinates
|
|
841
|
+
if (!Array.isArray(points[i]) || points[i].length < 2) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`verifyArcLength: point at index ${i} must be an array with at least 2 coordinates [x, y]`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (!Array.isArray(points[i + 1]) || points[i + 1].length < 2) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`verifyArcLength: point at index ${i + 1} must be an array with at least 2 coordinates [x, y]`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
672
852
|
const [x1, y1] = [D(points[i][0]), D(points[i][1])];
|
|
673
853
|
const [x2, y2] = [D(points[i + 1][0]), D(points[i + 1][1])];
|
|
674
854
|
polygonLength = polygonLength.plus(
|
|
@@ -684,12 +864,17 @@ export function verifyArcLength(points, computedLength = null) {
|
|
|
684
864
|
errors.push(`Arc length ${length} > polygon length ${polygonLength}`);
|
|
685
865
|
}
|
|
686
866
|
|
|
867
|
+
// DIVISION BY ZERO CHECK: Handle degenerate case where chord length is zero
|
|
868
|
+
// WHY: If start and end points are identical, chordLength is 0, causing division by zero.
|
|
869
|
+
// In this case, the ratio is not meaningful, so we return 1 (or could be Infinity).
|
|
870
|
+
const ratio = chordLength.gt(0) ? length.div(chordLength) : D(1);
|
|
871
|
+
|
|
687
872
|
return {
|
|
688
873
|
valid: errors.length === 0,
|
|
689
874
|
chordLength,
|
|
690
875
|
polygonLength,
|
|
691
876
|
arcLength: length,
|
|
692
|
-
ratio
|
|
877
|
+
ratio,
|
|
693
878
|
errors,
|
|
694
879
|
};
|
|
695
880
|
}
|
|
@@ -771,6 +956,18 @@ export function verifyArcLengthBySubdivision(
|
|
|
771
956
|
);
|
|
772
957
|
}
|
|
773
958
|
|
|
959
|
+
// INPUT VALIDATION: Ensure subdivisions is a positive integer
|
|
960
|
+
// WHY: subdivisions is used as a divisor and loop bound. Zero or negative would cause division by zero or invalid loops.
|
|
961
|
+
if (
|
|
962
|
+
typeof subdivisions !== "number" ||
|
|
963
|
+
subdivisions <= 0 ||
|
|
964
|
+
!Number.isInteger(subdivisions)
|
|
965
|
+
) {
|
|
966
|
+
throw new Error(
|
|
967
|
+
"verifyArcLengthBySubdivision: subdivisions must be a positive integer",
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
774
971
|
const tol = D(tolerance);
|
|
775
972
|
|
|
776
973
|
// Method 1: Adaptive quadrature
|
|
@@ -871,6 +1068,18 @@ export function verifyArcLengthTable(points, samples = 50) {
|
|
|
871
1068
|
);
|
|
872
1069
|
}
|
|
873
1070
|
|
|
1071
|
+
// INPUT VALIDATION: Ensure samples is a positive integer >= 2
|
|
1072
|
+
// WHY: samples is passed to createArcLengthTable which requires it to be >= 2
|
|
1073
|
+
if (
|
|
1074
|
+
typeof samples !== "number" ||
|
|
1075
|
+
!Number.isInteger(samples) ||
|
|
1076
|
+
samples < 2
|
|
1077
|
+
) {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
"verifyArcLengthTable: samples must be an integer >= 2",
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
874
1083
|
const errors = [];
|
|
875
1084
|
const table = createArcLengthTable(points, samples);
|
|
876
1085
|
|