@emasoft/svg-matrix 1.3.1 → 1.3.4
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 +14 -1
- package/dist/svg-matrix.global.min.js +3 -3
- package/dist/svg-matrix.min.js +3 -3
- package/dist/svg-toolbox.global.min.js +19 -19
- package/dist/svg-toolbox.min.js +19 -19
- package/dist/svgm.min.js +14 -14
- package/dist/version.json +12 -12
- package/package.json +2 -2
- package/scripts/version-sync.js +12 -6
- package/src/animation-optimization.js +3 -2
- package/src/bezier-analysis.js +2 -1
- package/src/bezier-intersections.js +53 -18
- package/src/convert-path-data.js +2 -3
- package/src/flatten-pipeline.js +2 -0
- package/src/font-manager.js +2 -2
- package/src/index.js +2 -2
- package/src/inkscape-support.js +2 -1
- package/src/marker-resolver.js +41 -18
- package/src/mesh-gradient.js +10 -1
- package/src/off-canvas-detection.js +18 -8
- package/src/path-simplification.js +3 -3
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svgm-lib.js +2 -2
- package/src/transform-decomposition.js +9 -1
- package/src/transforms3d.js +4 -15
package/dist/version.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.3.
|
|
3
|
-
"buildTime": "2026-01-
|
|
2
|
+
"version": "1.3.4",
|
|
3
|
+
"buildTime": "2026-01-25T12:44:06.936Z",
|
|
4
4
|
"bundles": {
|
|
5
5
|
"esm": [
|
|
6
6
|
{
|
|
7
7
|
"name": "svg-matrix.min.js",
|
|
8
|
-
"size":
|
|
9
|
-
"gzipSize":
|
|
8
|
+
"size": 56104,
|
|
9
|
+
"gzipSize": 19413,
|
|
10
10
|
"description": "ESM bundle for bundlers/Node.js (includes decimal.js)"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "svg-toolbox.min.js",
|
|
14
|
-
"size":
|
|
15
|
-
"gzipSize":
|
|
14
|
+
"size": 577298,
|
|
15
|
+
"gzipSize": 152361,
|
|
16
16
|
"description": "ESM bundle for bundlers/Node.js (includes decimal.js)"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"name": "svgm.min.js",
|
|
20
|
-
"size":
|
|
21
|
-
"gzipSize":
|
|
20
|
+
"size": 602600,
|
|
21
|
+
"gzipSize": 159489,
|
|
22
22
|
"description": "ESM bundle for bundlers/Node.js (includes decimal.js)"
|
|
23
23
|
}
|
|
24
24
|
],
|
|
25
25
|
"iife": [
|
|
26
26
|
{
|
|
27
27
|
"name": "svg-matrix.global.min.js",
|
|
28
|
-
"size":
|
|
29
|
-
"gzipSize":
|
|
28
|
+
"size": 56487,
|
|
29
|
+
"gzipSize": 19638,
|
|
30
30
|
"description": "IIFE bundle for browsers via <script> (includes decimal.js)"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"name": "svg-toolbox.global.min.js",
|
|
34
|
-
"size":
|
|
35
|
-
"gzipSize":
|
|
34
|
+
"size": 577427,
|
|
35
|
+
"gzipSize": 151728,
|
|
36
36
|
"description": "IIFE bundle for browsers via <script> (includes decimal.js)"
|
|
37
37
|
},
|
|
38
38
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emasoft/svg-matrix",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "Arbitrary-precision matrix, vector and affine transformation library for JavaScript using decimal.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"version:sync": "node scripts/version-sync.js",
|
|
25
25
|
"version:check": "node scripts/version-sync.js --check",
|
|
26
26
|
"preversion": "npm run version:sync",
|
|
27
|
-
"version": "node scripts/version-sync.js && git add src/index.js package-lock.json",
|
|
27
|
+
"version": "node scripts/version-sync.js && git add src/index.js package-lock.json src/svg-matrix-lib.js src/svg-toolbox-lib.js src/svgm-lib.js",
|
|
28
28
|
"prepublishOnly": "npm run build && npm run version:check && npm test",
|
|
29
29
|
"postinstall": "node scripts/postinstall.js || true"
|
|
30
30
|
},
|
package/scripts/version-sync.js
CHANGED
|
@@ -172,18 +172,24 @@ function updateLockfileVersion(version) {
|
|
|
172
172
|
const filePath = join(ROOT, "package-lock.json");
|
|
173
173
|
try {
|
|
174
174
|
const lock = JSON.parse(readFileSync(filePath, "utf8"));
|
|
175
|
-
|
|
175
|
+
|
|
176
|
+
// WHY: Compare version values directly instead of stringified JSON
|
|
177
|
+
// The old approach compared compact JSON vs pretty-printed JSON which always differed
|
|
178
|
+
const rootVersionMatch = lock.version === version;
|
|
179
|
+
const packageVersionMatch =
|
|
180
|
+
!lock.packages?.[""] || lock.packages[""].version === version;
|
|
181
|
+
|
|
182
|
+
if (rootVersionMatch && packageVersionMatch) {
|
|
183
|
+
return false; // No update needed
|
|
184
|
+
}
|
|
176
185
|
|
|
177
186
|
lock.version = version;
|
|
178
187
|
if (lock.packages && lock.packages[""]) {
|
|
179
188
|
lock.packages[""].version = version;
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
writeFileSync(filePath, updated, "utf8");
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
191
|
+
writeFileSync(filePath, JSON.stringify(lock, null, 2) + "\n", "utf8");
|
|
192
|
+
return true;
|
|
187
193
|
} catch {
|
|
188
194
|
// Lockfile might not exist
|
|
189
195
|
}
|
|
@@ -76,9 +76,10 @@ export function formatSplineValue(value, precision = 3) {
|
|
|
76
76
|
// Convert to string
|
|
77
77
|
let str = rounded.toString();
|
|
78
78
|
|
|
79
|
-
// Remove trailing zeros after decimal point
|
|
79
|
+
// Remove trailing zeros after decimal point (two-step to avoid "10.0" -> "1" bug)
|
|
80
80
|
if (str.includes(".")) {
|
|
81
|
-
str = str.replace(
|
|
81
|
+
str = str.replace(/0+$/, ""); // First remove trailing zeros
|
|
82
|
+
str = str.replace(/\.$/, ""); // Then remove orphan decimal point
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
// Remove leading zero for values between -1 and 1 (exclusive)
|
package/src/bezier-analysis.js
CHANGED
|
@@ -1589,7 +1589,8 @@ export function verifyCurvature(points, t, tolerance = "1e-10") {
|
|
|
1589
1589
|
|
|
1590
1590
|
const t1 = Decimal.max(D(0), tD.minus(h));
|
|
1591
1591
|
const t2 = Decimal.min(D(1), tD.plus(h));
|
|
1592
|
-
|
|
1592
|
+
// NOTE: Actual step size differs at boundaries, but curvature comparison
|
|
1593
|
+
// uses arc length directly rather than parameter difference
|
|
1593
1594
|
|
|
1594
1595
|
const tan1 = bezierTangent(points, t1);
|
|
1595
1596
|
const tan2 = bezierTangent(points, t2);
|
|
@@ -40,6 +40,23 @@ const SINGULARITY_THRESHOLD = "1e-50"; // Below this, Jacobian considered singul
|
|
|
40
40
|
const INTERSECTION_VERIFY_FACTOR = 100; // Multiplier for intersection verification
|
|
41
41
|
const DEDUP_TOLERANCE_FACTOR = 1000; // Multiplier for duplicate detection
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Default tolerance for intersection detection.
|
|
45
|
+
* WHY: 1e-10 provides high precision while being practical for real-world use.
|
|
46
|
+
* This is much more lenient than the theoretical 1e-30 but still far more precise
|
|
47
|
+
* than float64 operations (~1e-15).
|
|
48
|
+
* @type {string}
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_INTERSECTION_TOLERANCE = "1e-10";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tolerance for svgpathtools-compatible behavior.
|
|
54
|
+
* WHY: svgpathtools uses float64 (15 digits) and typically finds intersections
|
|
55
|
+
* with ~4e-7 distance error. This tolerance ensures compatible results.
|
|
56
|
+
* @type {string}
|
|
57
|
+
*/
|
|
58
|
+
export const SVGPATHTOOLS_COMPATIBLE_TOLERANCE = "1e-6";
|
|
59
|
+
|
|
43
60
|
/** Maximum Newton iterations for intersection refinement */
|
|
44
61
|
const MAX_NEWTON_ITERATIONS = 30;
|
|
45
62
|
|
|
@@ -382,14 +399,18 @@ function refineBezierLineRoot(bezier, line, t0, t1, tol) {
|
|
|
382
399
|
* @param {Array} bezier1 - First Bezier control points [[x,y], ...]
|
|
383
400
|
* @param {Array} bezier2 - Second Bezier control points [[x,y], ...]
|
|
384
401
|
* @param {Object} [options] - Options
|
|
385
|
-
* @param {string} [options.tolerance=
|
|
402
|
+
* @param {string} [options.tolerance=DEFAULT_INTERSECTION_TOLERANCE] - Intersection tolerance.
|
|
403
|
+
* Use DEFAULT_INTERSECTION_TOLERANCE (1e-10) for high precision,
|
|
404
|
+
* SVGPATHTOOLS_COMPATIBLE_TOLERANCE (1e-6) for svgpathtools-compatible results.
|
|
386
405
|
* @param {number} [options.maxDepth=50] - Maximum recursion depth
|
|
387
406
|
* @returns {Array} Intersections [{t1, t2, point, error}]
|
|
388
407
|
*/
|
|
389
408
|
export function bezierBezierIntersection(bezier1, bezier2, options = {}) {
|
|
390
|
-
// WHY: Use named
|
|
391
|
-
const {
|
|
392
|
-
|
|
409
|
+
// WHY: Use named constants as defaults for clarity and easy configuration
|
|
410
|
+
const {
|
|
411
|
+
tolerance = DEFAULT_INTERSECTION_TOLERANCE,
|
|
412
|
+
maxDepth = MAX_INTERSECTION_RECURSION_DEPTH,
|
|
413
|
+
} = options;
|
|
393
414
|
|
|
394
415
|
// Input validation
|
|
395
416
|
if (!bezier1 || !Array.isArray(bezier1) || bezier1.length < 2) {
|
|
@@ -573,21 +594,24 @@ function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
|
573
594
|
break;
|
|
574
595
|
}
|
|
575
596
|
|
|
576
|
-
// Solve: J * [dt1, dt2]^T = -[fx, fy]^T
|
|
577
|
-
//
|
|
578
|
-
//
|
|
597
|
+
// Solve: J * [dt1, dt2]^T = -[fx, fy]^T using Cramer's rule
|
|
598
|
+
// The Jacobian J = [[dx1, -dx2], [dy1, -dy2]], det(J) = dx2*dy1 - dx1*dy2
|
|
599
|
+
// For J^-1 * [-fx, -fy]^T:
|
|
600
|
+
// dt1 = (dy2*fx - dx2*fy) / det
|
|
601
|
+
// dt2 = (dy1*fx - dx1*fy) / det
|
|
602
|
+
// WHY: This is the correct Cramer's rule solution for the 2x2 linear system.
|
|
579
603
|
|
|
580
|
-
const dt1 = dy2.
|
|
581
|
-
const dt2 =
|
|
604
|
+
const dt1 = dy2.times(fx).minus(dx2.times(fy)).div(det);
|
|
605
|
+
const dt2 = dy1.times(fx).minus(dx1.times(fy)).div(det);
|
|
582
606
|
|
|
583
607
|
currentT1 = currentT1.plus(dt1);
|
|
584
608
|
currentT2 = currentT2.plus(dt2);
|
|
585
609
|
|
|
586
610
|
// Check for convergence by step size
|
|
587
611
|
if (dt1.abs().lt(tol) && dt2.abs().lt(tol)) {
|
|
588
|
-
// BUGFIX: Compute fresh error value
|
|
589
|
-
// WHY:
|
|
590
|
-
//
|
|
612
|
+
// BUGFIX: Compute fresh error value and verify it's within tolerance
|
|
613
|
+
// WHY: Step size convergence doesn't guarantee point accuracy.
|
|
614
|
+
// We must verify the final error meets the tolerance requirement.
|
|
591
615
|
const [finalX, finalY] = bezierPoint(bez1, currentT1);
|
|
592
616
|
const [finalX2, finalY2] = bezierPoint(bez2, currentT2);
|
|
593
617
|
const finalError = D(finalX)
|
|
@@ -595,12 +619,19 @@ function refineIntersection(bez1, bez2, t1, t2, tol) {
|
|
|
595
619
|
.pow(2)
|
|
596
620
|
.plus(D(finalY).minus(D(finalY2)).pow(2))
|
|
597
621
|
.sqrt();
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
622
|
+
|
|
623
|
+
// WHY: Only return if final error is within tolerance.
|
|
624
|
+
// This prevents returning false positives where Newton converged
|
|
625
|
+
// in parameter space but not in point distance space.
|
|
626
|
+
if (finalError.lt(tol)) {
|
|
627
|
+
return {
|
|
628
|
+
t1: currentT1,
|
|
629
|
+
t2: currentT2,
|
|
630
|
+
point: [finalX, finalY],
|
|
631
|
+
error: finalError,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
// Step size converged but error too large - continue iterating
|
|
604
635
|
}
|
|
605
636
|
}
|
|
606
637
|
|
|
@@ -1953,6 +1984,10 @@ export function verifyAllIntersectionFunctions(tolerance = "1e-30") {
|
|
|
1953
1984
|
// ============================================================================
|
|
1954
1985
|
|
|
1955
1986
|
export default {
|
|
1987
|
+
// Tolerance constants for configurable precision
|
|
1988
|
+
DEFAULT_INTERSECTION_TOLERANCE,
|
|
1989
|
+
SVGPATHTOOLS_COMPATIBLE_TOLERANCE,
|
|
1990
|
+
|
|
1956
1991
|
// Line-line
|
|
1957
1992
|
lineLineIntersection,
|
|
1958
1993
|
|
package/src/convert-path-data.js
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* @module convert-path-data
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Decimal.set({ precision: 80 });
|
|
15
|
+
// NOTE: This module uses native JavaScript Number/Math for all calculations.
|
|
16
|
+
// For arbitrary-precision needs, see the main svg-matrix library.
|
|
18
17
|
|
|
19
18
|
// SVG path command parameters count
|
|
20
19
|
const COMMAND_PARAMS = {
|
package/src/flatten-pipeline.js
CHANGED
|
@@ -1659,6 +1659,8 @@ function getElementBBox(el) {
|
|
|
1659
1659
|
|
|
1660
1660
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1661
1661
|
} catch {
|
|
1662
|
+
// Error is intentionally swallowed - invalid path data or polygon conversion
|
|
1663
|
+
// failures are expected for some edge cases and should return null
|
|
1662
1664
|
return null;
|
|
1663
1665
|
}
|
|
1664
1666
|
}
|
package/src/font-manager.js
CHANGED
|
@@ -511,8 +511,8 @@ export async function convertToWoff2(fontPath, options = {}) {
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
try {
|
|
514
|
-
// Use fonttools
|
|
515
|
-
execFileSync("fonttools", ["ttLib.woff2", "compress", fontPath,
|
|
514
|
+
// Use fonttools woff2 compress - output is positional argument, not -o flag
|
|
515
|
+
execFileSync("fonttools", ["ttLib.woff2", "compress", fontPath, outputPath], {
|
|
516
516
|
stdio: "pipe",
|
|
517
517
|
timeout: 60000,
|
|
518
518
|
});
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* SVG path conversion, and 2D/3D affine transformations using Decimal.js.
|
|
6
6
|
*
|
|
7
7
|
* @module @emasoft/svg-matrix
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.4
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -135,7 +135,7 @@ Decimal.set({ precision: 80 });
|
|
|
135
135
|
* Library version
|
|
136
136
|
* @constant {string}
|
|
137
137
|
*/
|
|
138
|
-
export const VERSION = "1.3.
|
|
138
|
+
export const VERSION = "1.3.4";
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Default precision for path output (decimal places)
|
package/src/inkscape-support.js
CHANGED
|
@@ -528,11 +528,12 @@ export function cloneElement(element) {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
// Create proper SVGElement with serialize() method
|
|
531
|
+
// Note: SVGElement constructor requires textContent to be a string, not null
|
|
531
532
|
const clone = new SVGElement(
|
|
532
533
|
element.tagName,
|
|
533
534
|
attrs,
|
|
534
535
|
clonedChildren,
|
|
535
|
-
element.textContent ||
|
|
536
|
+
element.textContent || "",
|
|
536
537
|
);
|
|
537
538
|
|
|
538
539
|
return clone;
|
package/src/marker-resolver.js
CHANGED
|
@@ -378,6 +378,12 @@ export function getMarkerTransform(
|
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
// Step 4: Apply viewBox transformation if present
|
|
381
|
+
// BUG FIX: When viewBox has non-zero origin, the refX/refY are in viewBox coordinates.
|
|
382
|
+
// We must convert them to viewport coordinates before applying the ref translation.
|
|
383
|
+
// Transform chain: T_position * R * T(-ref_viewport) * S * T(-viewBox.origin)
|
|
384
|
+
let hasViewBox = false;
|
|
385
|
+
let viewBoxScaleFactor = 1;
|
|
386
|
+
|
|
381
387
|
if (viewBox) {
|
|
382
388
|
// Calculate scale factors to fit viewBox into marker viewport
|
|
383
389
|
const vbWidth = viewBox.width;
|
|
@@ -397,30 +403,47 @@ export function getMarkerTransform(
|
|
|
397
403
|
// Validate scale factors are finite
|
|
398
404
|
if (isFinite(scaleFactorX) && isFinite(scaleFactorY)) {
|
|
399
405
|
// For now, use uniform scaling (can be enhanced with full preserveAspectRatio parsing)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
scaleX *= scaleFactor;
|
|
403
|
-
scaleY *= scaleFactor;
|
|
406
|
+
viewBoxScaleFactor = Math.min(scaleFactorX, scaleFactorY);
|
|
407
|
+
hasViewBox = true;
|
|
404
408
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
-viewBox.x,
|
|
408
|
-
-viewBox.y,
|
|
409
|
-
);
|
|
410
|
-
transform = transform.mul(viewBoxTranslate);
|
|
409
|
+
scaleX *= viewBoxScaleFactor;
|
|
410
|
+
scaleY *= viewBoxScaleFactor;
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
415
|
+
if (hasViewBox) {
|
|
416
|
+
// When viewBox is present, refX/refY are in viewBox coordinates.
|
|
417
|
+
// Convert to viewport (marker) coordinates by:
|
|
418
|
+
// 1. Subtract viewBox origin to get position relative to viewBox
|
|
419
|
+
// 2. Scale by viewBox scale factor to get position in marker viewport
|
|
420
|
+
const refViewportX = (refX - viewBox.x) * viewBoxScaleFactor;
|
|
421
|
+
const refViewportY = (refY - viewBox.y) * viewBoxScaleFactor;
|
|
422
|
+
|
|
423
|
+
// Apply ref translation in viewport coordinates (BEFORE scaling in the chain)
|
|
424
|
+
const refTranslate = Transforms2D.translation(-refViewportX, -refViewportY);
|
|
425
|
+
transform = transform.mul(refTranslate);
|
|
426
|
+
|
|
427
|
+
// Apply combined scaling
|
|
428
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
429
|
+
const scale = Transforms2D.scale(scaleX, scaleY);
|
|
430
|
+
transform = transform.mul(scale);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Apply viewBox origin translation (AFTER scaling, so it scales correctly)
|
|
434
|
+
const viewBoxTranslate = Transforms2D.translation(-viewBox.x, -viewBox.y);
|
|
435
|
+
transform = transform.mul(viewBoxTranslate);
|
|
436
|
+
} else {
|
|
437
|
+
// No viewBox: apply scaling then ref translation in original coordinates
|
|
438
|
+
if (scaleX !== 1 || scaleY !== 1) {
|
|
439
|
+
const scale = Transforms2D.scale(scaleX, scaleY);
|
|
440
|
+
transform = transform.mul(scale);
|
|
441
|
+
}
|
|
420
442
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
443
|
+
// Step 5: Translate by -refX, -refY
|
|
444
|
+
const refTranslate = Transforms2D.translation(-refX, -refY);
|
|
445
|
+
transform = transform.mul(refTranslate);
|
|
446
|
+
}
|
|
424
447
|
|
|
425
448
|
return transform;
|
|
426
449
|
}
|
package/src/mesh-gradient.js
CHANGED
|
@@ -161,17 +161,26 @@ export function parseColor(colorStr, opacity = 1) {
|
|
|
161
161
|
return color(r, g, b, alphaClamped * 255 * opacity);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// Handle hex colors
|
|
164
|
+
// Handle hex colors: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (CSS Color Level 4)
|
|
165
165
|
const hexMatch = colorStr.match(/^#([0-9a-f]{3,8})$/i);
|
|
166
166
|
if (hexMatch) {
|
|
167
167
|
const hex = hexMatch[1];
|
|
168
168
|
if (hex.length === 3) {
|
|
169
|
+
// #RGB format - expand each digit to two (e.g., #F00 -> #FF0000)
|
|
169
170
|
return color(
|
|
170
171
|
parseInt(hex[0] + hex[0], 16),
|
|
171
172
|
parseInt(hex[1] + hex[1], 16),
|
|
172
173
|
parseInt(hex[2] + hex[2], 16),
|
|
173
174
|
255 * opacity,
|
|
174
175
|
);
|
|
176
|
+
} else if (hex.length === 4) {
|
|
177
|
+
// #RGBA format (CSS4) - expand each digit to two (e.g., #F00A -> #FF0000AA)
|
|
178
|
+
return color(
|
|
179
|
+
parseInt(hex[0] + hex[0], 16),
|
|
180
|
+
parseInt(hex[1] + hex[1], 16),
|
|
181
|
+
parseInt(hex[2] + hex[2], 16),
|
|
182
|
+
parseInt(hex[3] + hex[3], 16) * opacity,
|
|
183
|
+
);
|
|
175
184
|
} else if (hex.length === 6) {
|
|
176
185
|
return color(
|
|
177
186
|
parseInt(hex.slice(0, 2), 16),
|
|
@@ -1564,8 +1564,10 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1564
1564
|
continue;
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1567
|
+
// BUG FIX: Handle relative coordinates (lowercase 'm')
|
|
1568
|
+
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
1569
|
+
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
1570
|
+
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
1569
1571
|
|
|
1570
1572
|
// Only add move if point is inside bounds
|
|
1571
1573
|
if (pointInBBox({ x, y }, bounds)) {
|
|
@@ -1587,8 +1589,10 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1587
1589
|
continue;
|
|
1588
1590
|
}
|
|
1589
1591
|
|
|
1590
|
-
|
|
1591
|
-
const
|
|
1592
|
+
// BUG FIX: Handle relative coordinates (lowercase 'l')
|
|
1593
|
+
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
1594
|
+
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
1595
|
+
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
1592
1596
|
|
|
1593
1597
|
// Clip line segment
|
|
1594
1598
|
const clipped = clipLine(
|
|
@@ -1629,7 +1633,9 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1629
1633
|
continue;
|
|
1630
1634
|
}
|
|
1631
1635
|
|
|
1632
|
-
|
|
1636
|
+
// BUG FIX: Handle relative coordinates (lowercase 'h')
|
|
1637
|
+
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
1638
|
+
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
1633
1639
|
const clipped = clipLine(
|
|
1634
1640
|
{ x: currentX, y: currentY },
|
|
1635
1641
|
{ x, y: currentY },
|
|
@@ -1665,7 +1671,9 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1665
1671
|
continue;
|
|
1666
1672
|
}
|
|
1667
1673
|
|
|
1668
|
-
|
|
1674
|
+
// BUG FIX: Handle relative coordinates (lowercase 'v')
|
|
1675
|
+
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
1676
|
+
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
1669
1677
|
const clipped = clipLine(
|
|
1670
1678
|
{ x: currentX, y: currentY },
|
|
1671
1679
|
{ x: currentX, y },
|
|
@@ -1708,8 +1716,10 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1708
1716
|
// BUG FIX: Sample the curve to better approximate clipping
|
|
1709
1717
|
// A full implementation would clip curves exactly, but sampling provides
|
|
1710
1718
|
// a reasonable approximation (WHY: curves can extend outside bounds even if endpoints are inside)
|
|
1711
|
-
|
|
1712
|
-
const
|
|
1719
|
+
// BUG FIX: Handle relative coordinates (lowercase 'c', 's', 'q', 't', 'a')
|
|
1720
|
+
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
1721
|
+
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
1722
|
+
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
1713
1723
|
|
|
1714
1724
|
// Sample 10 points along the curve path from current to end
|
|
1715
1725
|
const samples = 10;
|
|
@@ -1361,11 +1361,11 @@ export function removeZeroLengthSegments(pathData, tolerance = EPSILON) {
|
|
|
1361
1361
|
currentY = D(0);
|
|
1362
1362
|
let startX = D(0),
|
|
1363
1363
|
startY = D(0);
|
|
1364
|
-
// Track previous control points for S and T commands (
|
|
1364
|
+
// Track previous control points for S and T commands (smooth Bezier continuity)
|
|
1365
1365
|
let prevCp2X = null,
|
|
1366
|
-
prevCp2Y = null; // For S command (cubic)
|
|
1366
|
+
prevCp2Y = null; // For S command (cubic) - tracks 2nd control point of previous C/S
|
|
1367
1367
|
let prevCpX = null,
|
|
1368
|
-
prevCpY = null; // For T command (quadratic)
|
|
1368
|
+
prevCpY = null; // For T command (quadratic) - tracks control point of previous Q/T
|
|
1369
1369
|
|
|
1370
1370
|
for (let idx = 0; idx < pathData.length; idx++) {
|
|
1371
1371
|
const item = pathData[idx];
|
package/src/svg-matrix-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Works in both Node.js and browser environments.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-matrix-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.4
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -32,7 +32,7 @@ Decimal.set({ precision: 80 });
|
|
|
32
32
|
/**
|
|
33
33
|
* Library version
|
|
34
34
|
*/
|
|
35
|
-
export const VERSION = "1.3.
|
|
35
|
+
export const VERSION = "1.3.4";
|
|
36
36
|
|
|
37
37
|
// Export core classes
|
|
38
38
|
export { Decimal, Matrix, Vector };
|
package/src/svg-toolbox-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provides 69+ operations for cleaning, optimizing, and transforming SVG files.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-toolbox-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.4
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -34,7 +34,7 @@ import * as SVGToolboxModule from "./svg-toolbox.js";
|
|
|
34
34
|
/**
|
|
35
35
|
* Library version
|
|
36
36
|
*/
|
|
37
|
-
export const VERSION = "1.3.
|
|
37
|
+
export const VERSION = "1.3.4";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Default export for browser global (window.SVGToolbox)
|
package/src/svgm-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* comprehensive SVG manipulation (SVGToolbox). Works in Node.js and browser.
|
|
6
6
|
*
|
|
7
7
|
* @module svgm-lib
|
|
8
|
-
* @version 1.3.
|
|
8
|
+
* @version 1.3.4
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -49,7 +49,7 @@ Decimal.set({ precision: 80 });
|
|
|
49
49
|
/**
|
|
50
50
|
* Library version
|
|
51
51
|
*/
|
|
52
|
-
export const VERSION = "1.3.
|
|
52
|
+
export const VERSION = "1.3.4";
|
|
53
53
|
|
|
54
54
|
// Export math classes
|
|
55
55
|
export { Decimal, Matrix, Vector };
|
|
@@ -294,7 +294,15 @@ export function decomposeMatrix(matrix) {
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// Calculate rotation angle from first column
|
|
297
|
-
|
|
297
|
+
// BUG FIX: When scaleX is negative (reflection), we must compute rotation from negated
|
|
298
|
+
// column vector to separate the reflection component from the rotation component.
|
|
299
|
+
// Without this fix, X-flip matrix(-1,0,0,1,0,0) gives rotation=π instead of 0.
|
|
300
|
+
let rotation;
|
|
301
|
+
if (scaleX.lessThan(0)) {
|
|
302
|
+
rotation = Decimal.atan2(b.neg(), a.neg());
|
|
303
|
+
} else {
|
|
304
|
+
rotation = Decimal.atan2(b, a);
|
|
305
|
+
}
|
|
298
306
|
|
|
299
307
|
// Calculate skew
|
|
300
308
|
// After removing rotation and scale, we get the skew
|
package/src/transforms3d.js
CHANGED
|
@@ -63,17 +63,9 @@ function normalizeAngle(theta) {
|
|
|
63
63
|
|
|
64
64
|
// For large angles, normalize to [-2π, 2π] to reduce precision loss
|
|
65
65
|
// Using modulo with 2π (tau) to find equivalent angle in standard range
|
|
66
|
+
// Note: modulo already guarantees result in (-2π, 2π) exclusive
|
|
66
67
|
const TWO_PI = 6.283185307179586;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Ensure result is in [-2π, 2π] range
|
|
70
|
-
if (normalized > TWO_PI) {
|
|
71
|
-
normalized -= TWO_PI;
|
|
72
|
-
} else if (normalized < -TWO_PI) {
|
|
73
|
-
normalized += TWO_PI;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return normalized;
|
|
68
|
+
return tNum % TWO_PI;
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
/**
|
|
@@ -229,11 +221,8 @@ export function scale(sx, sy = null, sz = null) {
|
|
|
229
221
|
const syD = D(syValue);
|
|
230
222
|
const szD = D(szValue);
|
|
231
223
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
// Zero scale is mathematically valid but creates non-invertible matrix
|
|
235
|
-
// We allow it but document the behavior in JSDoc above
|
|
236
|
-
}
|
|
224
|
+
// Note: Zero scale factors create singular (non-invertible) matrices
|
|
225
|
+
// This is mathematically valid but cannot be reversed - documented in JSDoc above
|
|
237
226
|
|
|
238
227
|
return Matrix.from([
|
|
239
228
|
[sxD, new Decimal(0), new Decimal(0), new Decimal(0)],
|