@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/geometry-to-path.js
CHANGED
|
@@ -41,7 +41,16 @@ export function getKappa() {
|
|
|
41
41
|
* @returns {Decimal} Control point distance factor (multiply by radius)
|
|
42
42
|
*/
|
|
43
43
|
export function getKappaForArc(thetaRadians) {
|
|
44
|
+
if (thetaRadians == null) {
|
|
45
|
+
throw new Error("getKappaForArc: thetaRadians parameter is required");
|
|
46
|
+
}
|
|
44
47
|
const theta = D(thetaRadians);
|
|
48
|
+
if (!theta.isFinite()) {
|
|
49
|
+
throw new Error("getKappaForArc: thetaRadians must be finite");
|
|
50
|
+
}
|
|
51
|
+
if (theta.isZero()) {
|
|
52
|
+
throw new Error("getKappaForArc: thetaRadians cannot be zero");
|
|
53
|
+
}
|
|
45
54
|
const four = new Decimal(4);
|
|
46
55
|
const three = new Decimal(3);
|
|
47
56
|
// L = (4/3) * tan(theta/4)
|
|
@@ -71,6 +80,19 @@ export function getKappaForArc(thetaRadians) {
|
|
|
71
80
|
* @returns {string} SVG path data
|
|
72
81
|
*/
|
|
73
82
|
export function circleToPathDataHP(cx, cy, r, arcs = 8, precision = 6) {
|
|
83
|
+
if (cx == null || cy == null || r == null) {
|
|
84
|
+
throw new Error("circleToPathDataHP: cx, cy, and r parameters are required");
|
|
85
|
+
}
|
|
86
|
+
const rD = D(r);
|
|
87
|
+
if (!rD.isFinite() || rD.isNegative()) {
|
|
88
|
+
throw new Error("circleToPathDataHP: radius must be finite and non-negative");
|
|
89
|
+
}
|
|
90
|
+
if (!Number.isFinite(arcs) || arcs <= 0) {
|
|
91
|
+
throw new Error("circleToPathDataHP: arcs must be a positive number");
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
94
|
+
throw new Error("circleToPathDataHP: precision must be non-negative");
|
|
95
|
+
}
|
|
74
96
|
return ellipseToPathDataHP(cx, cy, r, r, arcs, precision);
|
|
75
97
|
}
|
|
76
98
|
|
|
@@ -90,6 +112,15 @@ export function circleToPathDataHP(cx, cy, r, arcs = 8, precision = 6) {
|
|
|
90
112
|
* @returns {string} SVG path data
|
|
91
113
|
*/
|
|
92
114
|
export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
115
|
+
if (cx == null || cy == null || rx == null || ry == null) {
|
|
116
|
+
throw new Error("ellipseToPathDataHP: cx, cy, rx, and ry parameters are required");
|
|
117
|
+
}
|
|
118
|
+
if (!Number.isFinite(arcs) || arcs <= 0) {
|
|
119
|
+
throw new Error("ellipseToPathDataHP: arcs must be a positive number");
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
122
|
+
throw new Error("ellipseToPathDataHP: precision must be non-negative");
|
|
123
|
+
}
|
|
93
124
|
// Enforce multiple of 4 for symmetry
|
|
94
125
|
let numArcs = arcs;
|
|
95
126
|
if (numArcs % 4 !== 0) {
|
|
@@ -99,6 +130,12 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
99
130
|
cyD = D(cy),
|
|
100
131
|
rxD = D(rx),
|
|
101
132
|
ryD = D(ry);
|
|
133
|
+
if (!rxD.isFinite() || rxD.isNegative()) {
|
|
134
|
+
throw new Error("ellipseToPathDataHP: rx must be finite and non-negative");
|
|
135
|
+
}
|
|
136
|
+
if (!ryD.isFinite() || ryD.isNegative()) {
|
|
137
|
+
throw new Error("ellipseToPathDataHP: ry must be finite and non-negative");
|
|
138
|
+
}
|
|
102
139
|
const f = (v) => formatNumber(v, precision);
|
|
103
140
|
|
|
104
141
|
// Angle per arc in radians
|
|
@@ -158,8 +195,18 @@ export function ellipseToPathDataHP(cx, cy, rx, ry, arcs = 8, precision = 6) {
|
|
|
158
195
|
* @returns {string} Formatted number string
|
|
159
196
|
*/
|
|
160
197
|
function formatNumber(value, precision = 6) {
|
|
198
|
+
if (value == null) {
|
|
199
|
+
throw new Error("formatNumber: value parameter is required");
|
|
200
|
+
}
|
|
201
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
202
|
+
throw new Error("formatNumber: precision must be non-negative");
|
|
203
|
+
}
|
|
204
|
+
const valueD = D(value);
|
|
205
|
+
if (!valueD.isFinite()) {
|
|
206
|
+
throw new Error("formatNumber: value must be finite");
|
|
207
|
+
}
|
|
161
208
|
// Format with precision then remove trailing zeros for smaller output
|
|
162
|
-
let str =
|
|
209
|
+
let str = valueD.toFixed(precision);
|
|
163
210
|
// Remove trailing zeros after decimal point
|
|
164
211
|
if (str.includes(".")) {
|
|
165
212
|
str = str.replace(/\.?0+$/, "");
|
|
@@ -176,9 +223,18 @@ function formatNumber(value, precision = 6) {
|
|
|
176
223
|
* @returns {string} SVG path data string
|
|
177
224
|
*/
|
|
178
225
|
export function circleToPathData(cx, cy, r, precision = 6) {
|
|
226
|
+
if (cx == null || cy == null || r == null) {
|
|
227
|
+
throw new Error("circleToPathData: cx, cy, and r parameters are required");
|
|
228
|
+
}
|
|
229
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
230
|
+
throw new Error("circleToPathData: precision must be non-negative");
|
|
231
|
+
}
|
|
179
232
|
const cxD = D(cx),
|
|
180
233
|
cyD = D(cy),
|
|
181
234
|
rD = D(r);
|
|
235
|
+
if (!rD.isFinite() || rD.isNegative()) {
|
|
236
|
+
throw new Error("circleToPathData: radius must be finite and non-negative");
|
|
237
|
+
}
|
|
182
238
|
const k = getKappa().mul(rD);
|
|
183
239
|
const x0 = cxD.plus(rD),
|
|
184
240
|
y0 = cyD;
|
|
@@ -218,10 +274,22 @@ export function circleToPathData(cx, cy, r, precision = 6) {
|
|
|
218
274
|
* @returns {string} SVG path data string
|
|
219
275
|
*/
|
|
220
276
|
export function ellipseToPathData(cx, cy, rx, ry, precision = 6) {
|
|
277
|
+
if (cx == null || cy == null || rx == null || ry == null) {
|
|
278
|
+
throw new Error("ellipseToPathData: cx, cy, rx, and ry parameters are required");
|
|
279
|
+
}
|
|
280
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
281
|
+
throw new Error("ellipseToPathData: precision must be non-negative");
|
|
282
|
+
}
|
|
221
283
|
const cxD = D(cx),
|
|
222
284
|
cyD = D(cy),
|
|
223
285
|
rxD = D(rx),
|
|
224
286
|
ryD = D(ry);
|
|
287
|
+
if (!rxD.isFinite() || rxD.isNegative()) {
|
|
288
|
+
throw new Error("ellipseToPathData: rx must be finite and non-negative");
|
|
289
|
+
}
|
|
290
|
+
if (!ryD.isFinite() || ryD.isNegative()) {
|
|
291
|
+
throw new Error("ellipseToPathData: ry must be finite and non-negative");
|
|
292
|
+
}
|
|
225
293
|
const kappa = getKappa(),
|
|
226
294
|
kx = kappa.mul(rxD),
|
|
227
295
|
ky = kappa.mul(ryD);
|
|
@@ -275,10 +343,28 @@ export function rectToPathData(
|
|
|
275
343
|
useArcs = false,
|
|
276
344
|
precision = 6,
|
|
277
345
|
) {
|
|
346
|
+
if (x == null || y == null || width == null || height == null) {
|
|
347
|
+
throw new Error("rectToPathData: x, y, width, and height parameters are required");
|
|
348
|
+
}
|
|
349
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
350
|
+
throw new Error("rectToPathData: precision must be non-negative");
|
|
351
|
+
}
|
|
278
352
|
const xD = D(x),
|
|
279
353
|
yD = D(y),
|
|
280
354
|
wD = D(width),
|
|
281
355
|
hD = D(height);
|
|
356
|
+
if (!xD.isFinite()) {
|
|
357
|
+
throw new Error("rectToPathData: x must be finite");
|
|
358
|
+
}
|
|
359
|
+
if (!yD.isFinite()) {
|
|
360
|
+
throw new Error("rectToPathData: y must be finite");
|
|
361
|
+
}
|
|
362
|
+
if (!wD.isFinite() || wD.isNegative()) {
|
|
363
|
+
throw new Error("rectToPathData: width must be finite and non-negative");
|
|
364
|
+
}
|
|
365
|
+
if (!hD.isFinite() || hD.isNegative()) {
|
|
366
|
+
throw new Error("rectToPathData: height must be finite and non-negative");
|
|
367
|
+
}
|
|
282
368
|
let rxD = D(rx || 0),
|
|
283
369
|
ryD = ry !== null ? D(ry) : rxD;
|
|
284
370
|
const halfW = wD.div(2),
|
|
@@ -322,8 +408,18 @@ export function rectToPathData(
|
|
|
322
408
|
* @returns {string} SVG path data string
|
|
323
409
|
*/
|
|
324
410
|
export function lineToPathData(x1, y1, x2, y2, precision = 6) {
|
|
325
|
-
|
|
326
|
-
|
|
411
|
+
if (x1 == null || y1 == null || x2 == null || y2 == null) {
|
|
412
|
+
throw new Error("lineToPathData: x1, y1, x2, and y2 parameters are required");
|
|
413
|
+
}
|
|
414
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
415
|
+
throw new Error("lineToPathData: precision must be non-negative");
|
|
416
|
+
}
|
|
417
|
+
const x1D = D(x1), y1D = D(y1), x2D = D(x2), y2D = D(y2);
|
|
418
|
+
if (!x1D.isFinite() || !y1D.isFinite() || !x2D.isFinite() || !y2D.isFinite()) {
|
|
419
|
+
throw new Error("lineToPathData: all coordinates must be finite");
|
|
420
|
+
}
|
|
421
|
+
const f = (v) => formatNumber(v, precision);
|
|
422
|
+
return `M${f(x1D)} ${f(y1D)}L${f(x2D)} ${f(y2D)}`;
|
|
327
423
|
}
|
|
328
424
|
|
|
329
425
|
/**
|
|
@@ -335,7 +431,13 @@ function parsePoints(points) {
|
|
|
335
431
|
// Handle null/undefined
|
|
336
432
|
if (points == null) return [];
|
|
337
433
|
// Handle arrays
|
|
338
|
-
if (Array.isArray(points))
|
|
434
|
+
if (Array.isArray(points)) {
|
|
435
|
+
try {
|
|
436
|
+
return points.map(([x, y]) => [D(x), D(y)]);
|
|
437
|
+
} catch (_e) {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
339
441
|
// Handle SVGAnimatedPoints or objects with baseVal
|
|
340
442
|
let pointsValue = points;
|
|
341
443
|
if (typeof pointsValue === "object" && pointsValue.baseVal !== undefined) {
|
|
@@ -349,10 +451,16 @@ function parsePoints(points) {
|
|
|
349
451
|
return [];
|
|
350
452
|
}
|
|
351
453
|
}
|
|
352
|
-
const nums =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
454
|
+
const nums = [];
|
|
455
|
+
const parts = pointsValue.split(/[\s,]+/).filter((s) => s.length > 0);
|
|
456
|
+
for (const s of parts) {
|
|
457
|
+
try {
|
|
458
|
+
nums.push(D(s));
|
|
459
|
+
} catch (_e) {
|
|
460
|
+
// Skip invalid numbers
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
356
464
|
const pairs = [];
|
|
357
465
|
for (let i = 0; i < nums.length; i += 2) {
|
|
358
466
|
if (i + 1 < nums.length) pairs.push([nums[i], nums[i + 1]]);
|
|
@@ -367,6 +475,9 @@ function parsePoints(points) {
|
|
|
367
475
|
* @returns {string} SVG path data string
|
|
368
476
|
*/
|
|
369
477
|
export function polylineToPathData(points, precision = 6) {
|
|
478
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
479
|
+
throw new Error("polylineToPathData: precision must be non-negative");
|
|
480
|
+
}
|
|
370
481
|
const pairs = parsePoints(points);
|
|
371
482
|
if (pairs.length === 0) return "";
|
|
372
483
|
const f = (v) => formatNumber(v, precision);
|
|
@@ -386,6 +497,9 @@ export function polylineToPathData(points, precision = 6) {
|
|
|
386
497
|
* @returns {string} SVG path data string with Z (closepath) command
|
|
387
498
|
*/
|
|
388
499
|
export function polygonToPathData(points, precision = 6) {
|
|
500
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
501
|
+
throw new Error("polygonToPathData: precision must be non-negative");
|
|
502
|
+
}
|
|
389
503
|
const path = polylineToPathData(points, precision);
|
|
390
504
|
return path ? path + " Z" : "";
|
|
391
505
|
}
|
|
@@ -420,6 +534,12 @@ const COMMAND_PARAMS = {
|
|
|
420
534
|
* @returns {Array<Object>} Array of {command, args} objects
|
|
421
535
|
*/
|
|
422
536
|
export function parsePathData(pathData) {
|
|
537
|
+
if (pathData == null) {
|
|
538
|
+
throw new Error("parsePathData: pathData parameter is required");
|
|
539
|
+
}
|
|
540
|
+
if (typeof pathData !== "string") {
|
|
541
|
+
throw new Error("parsePathData: pathData must be a string");
|
|
542
|
+
}
|
|
423
543
|
const commands = [];
|
|
424
544
|
const commandRegex = /([MmLlHhVvCcSsQqTtAaZz])\s*([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
425
545
|
let match;
|
|
@@ -430,10 +550,17 @@ export function parsePathData(pathData) {
|
|
|
430
550
|
// FIX: Use regex to extract numbers, handles implicit negative separators (e.g., "0.8-2.9" -> ["0.8", "-2.9"])
|
|
431
551
|
// Per W3C SVG spec, negative signs can act as delimiters without spaces
|
|
432
552
|
const numRegex = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
433
|
-
const allArgs =
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
553
|
+
const allArgs = [];
|
|
554
|
+
if (argsStr.length > 0) {
|
|
555
|
+
for (const m of argsStr.matchAll(numRegex)) {
|
|
556
|
+
try {
|
|
557
|
+
allArgs.push(D(m[0]));
|
|
558
|
+
} catch (_e) {
|
|
559
|
+
// Skip invalid numbers per SVG error handling
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
437
564
|
|
|
438
565
|
const paramCount = COMMAND_PARAMS[command];
|
|
439
566
|
|
|
@@ -467,8 +594,21 @@ export function parsePathData(pathData) {
|
|
|
467
594
|
* @returns {string} SVG path data string
|
|
468
595
|
*/
|
|
469
596
|
export function pathArrayToString(commands, precision = 6) {
|
|
597
|
+
if (commands == null) {
|
|
598
|
+
throw new Error("pathArrayToString: commands parameter is required");
|
|
599
|
+
}
|
|
600
|
+
if (!Array.isArray(commands)) {
|
|
601
|
+
throw new Error("pathArrayToString: commands must be an array");
|
|
602
|
+
}
|
|
603
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
604
|
+
throw new Error("pathArrayToString: precision must be non-negative");
|
|
605
|
+
}
|
|
470
606
|
return commands
|
|
471
|
-
.map((
|
|
607
|
+
.map((cmd) => {
|
|
608
|
+
if (!cmd || typeof cmd.command !== "string" || !Array.isArray(cmd.args)) {
|
|
609
|
+
throw new Error("pathArrayToString: each command must have 'command' (string) and 'args' (array) properties");
|
|
610
|
+
}
|
|
611
|
+
const { command, args } = cmd;
|
|
472
612
|
const argsStr = args.map((a) => formatNumber(a, precision)).join(" ");
|
|
473
613
|
return argsStr.length > 0 ? `${command} ${argsStr}` : command;
|
|
474
614
|
})
|
|
@@ -481,6 +621,9 @@ export function pathArrayToString(commands, precision = 6) {
|
|
|
481
621
|
* @returns {string} Path data with all absolute commands
|
|
482
622
|
*/
|
|
483
623
|
export function pathToAbsolute(pathData) {
|
|
624
|
+
if (pathData == null) {
|
|
625
|
+
throw new Error("pathToAbsolute: pathData parameter is required");
|
|
626
|
+
}
|
|
484
627
|
const commands = parsePathData(pathData);
|
|
485
628
|
const result = [];
|
|
486
629
|
let currentX = new Decimal(0),
|
|
@@ -495,6 +638,7 @@ export function pathToAbsolute(pathData) {
|
|
|
495
638
|
const isRelative = command === command.toLowerCase();
|
|
496
639
|
const upperCmd = command.toUpperCase();
|
|
497
640
|
if (upperCmd === "M") {
|
|
641
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
498
642
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
499
643
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
500
644
|
currentX = x;
|
|
@@ -504,6 +648,7 @@ export function pathToAbsolute(pathData) {
|
|
|
504
648
|
result.push({ command: "M", args: [x, y] });
|
|
505
649
|
lastCommand = "M";
|
|
506
650
|
} else if (upperCmd === "L") {
|
|
651
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
507
652
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
508
653
|
const y = isRelative ? currentY.plus(args[1]) : args[1];
|
|
509
654
|
currentX = x;
|
|
@@ -511,16 +656,19 @@ export function pathToAbsolute(pathData) {
|
|
|
511
656
|
result.push({ command: "L", args: [x, y] });
|
|
512
657
|
lastCommand = "L";
|
|
513
658
|
} else if (upperCmd === "H") {
|
|
659
|
+
if (args.length < 1) continue; // Skip malformed command
|
|
514
660
|
const x = isRelative ? currentX.plus(args[0]) : args[0];
|
|
515
661
|
currentX = x;
|
|
516
662
|
result.push({ command: "L", args: [x, currentY] });
|
|
517
663
|
lastCommand = "H";
|
|
518
664
|
} else if (upperCmd === "V") {
|
|
665
|
+
if (args.length < 1) continue; // Skip malformed command
|
|
519
666
|
const y = isRelative ? currentY.plus(args[0]) : args[0];
|
|
520
667
|
currentY = y;
|
|
521
668
|
result.push({ command: "L", args: [currentX, y] });
|
|
522
669
|
lastCommand = "V";
|
|
523
670
|
} else if (upperCmd === "C") {
|
|
671
|
+
if (args.length < 6) continue; // Skip malformed command
|
|
524
672
|
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
525
673
|
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
526
674
|
const x2 = isRelative ? currentX.plus(args[2]) : args[2];
|
|
@@ -534,6 +682,7 @@ export function pathToAbsolute(pathData) {
|
|
|
534
682
|
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
535
683
|
lastCommand = "C";
|
|
536
684
|
} else if (upperCmd === "S") {
|
|
685
|
+
if (args.length < 4) continue; // Skip malformed command
|
|
537
686
|
// Smooth cubic Bezier: 4 args (x2, y2, x, y)
|
|
538
687
|
// First control point is reflection of previous second control point
|
|
539
688
|
let x1, y1;
|
|
@@ -555,6 +704,7 @@ export function pathToAbsolute(pathData) {
|
|
|
555
704
|
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
556
705
|
lastCommand = "S";
|
|
557
706
|
} else if (upperCmd === "Q") {
|
|
707
|
+
if (args.length < 4) continue; // Skip malformed command
|
|
558
708
|
// Quadratic Bezier: 4 args (x1, y1, x, y)
|
|
559
709
|
const x1 = isRelative ? currentX.plus(args[0]) : args[0];
|
|
560
710
|
const y1 = isRelative ? currentY.plus(args[1]) : args[1];
|
|
@@ -567,6 +717,7 @@ export function pathToAbsolute(pathData) {
|
|
|
567
717
|
result.push({ command: "Q", args: [x1, y1, x, y] });
|
|
568
718
|
lastCommand = "Q";
|
|
569
719
|
} else if (upperCmd === "T") {
|
|
720
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
570
721
|
// Smooth quadratic Bezier: 2 args (x, y)
|
|
571
722
|
// Control point is reflection of previous control point
|
|
572
723
|
let x1, y1;
|
|
@@ -586,6 +737,7 @@ export function pathToAbsolute(pathData) {
|
|
|
586
737
|
result.push({ command: "Q", args: [x1, y1, x, y] });
|
|
587
738
|
lastCommand = "T";
|
|
588
739
|
} else if (upperCmd === "A") {
|
|
740
|
+
if (args.length < 7) continue; // Skip malformed command
|
|
589
741
|
const x = isRelative ? currentX.plus(args[5]) : args[5];
|
|
590
742
|
const y = isRelative ? currentY.plus(args[6]) : args[6];
|
|
591
743
|
currentX = x;
|
|
@@ -630,6 +782,20 @@ export function transformArcParams(
|
|
|
630
782
|
endY,
|
|
631
783
|
matrix,
|
|
632
784
|
) {
|
|
785
|
+
if (rx == null || ry == null || xAxisRotation == null || largeArc == null || sweep == null || endX == null || endY == null) {
|
|
786
|
+
throw new Error("transformArcParams: all parameters (rx, ry, xAxisRotation, largeArc, sweep, endX, endY) are required");
|
|
787
|
+
}
|
|
788
|
+
if (matrix == null) {
|
|
789
|
+
throw new Error("transformArcParams: matrix parameter is required");
|
|
790
|
+
}
|
|
791
|
+
if (!matrix.data || !Array.isArray(matrix.data) || matrix.data.length !== 3) {
|
|
792
|
+
throw new Error("transformArcParams: matrix must be 3x3");
|
|
793
|
+
}
|
|
794
|
+
if (!Array.isArray(matrix.data[0]) || matrix.data[0].length < 2 ||
|
|
795
|
+
!Array.isArray(matrix.data[1]) || matrix.data[1].length < 2 ||
|
|
796
|
+
!Array.isArray(matrix.data[2]) || matrix.data[2].length < 1) {
|
|
797
|
+
throw new Error("transformArcParams: matrix must have valid 3x3 structure");
|
|
798
|
+
}
|
|
633
799
|
const rxD = D(rx),
|
|
634
800
|
ryD = D(ry),
|
|
635
801
|
rotD = D(xAxisRotation);
|
|
@@ -639,8 +805,12 @@ export function transformArcParams(
|
|
|
639
805
|
// Transform the endpoint
|
|
640
806
|
const endPoint = Matrix.from([[endXD], [endYD], [new Decimal(1)]]);
|
|
641
807
|
const transformedEnd = matrix.mul(endPoint);
|
|
642
|
-
const
|
|
643
|
-
|
|
808
|
+
const w = transformedEnd.data[2][0];
|
|
809
|
+
if (w.isZero()) {
|
|
810
|
+
throw new Error("transformArcParams: division by zero in homogeneous coordinate transformation");
|
|
811
|
+
}
|
|
812
|
+
const newEndX = transformedEnd.data[0][0].div(w);
|
|
813
|
+
const newEndY = transformedEnd.data[1][0].div(w);
|
|
644
814
|
|
|
645
815
|
// Extract the 2x2 linear part of the affine transformation
|
|
646
816
|
const a = matrix.data[0][0],
|
|
@@ -697,30 +867,54 @@ export function transformArcParams(
|
|
|
697
867
|
* @returns {string} Transformed SVG path data
|
|
698
868
|
*/
|
|
699
869
|
export function transformPathData(pathData, matrix, precision = 6) {
|
|
870
|
+
if (pathData == null) {
|
|
871
|
+
throw new Error("transformPathData: pathData parameter is required");
|
|
872
|
+
}
|
|
873
|
+
if (matrix == null) {
|
|
874
|
+
throw new Error("transformPathData: matrix parameter is required");
|
|
875
|
+
}
|
|
876
|
+
if (!matrix.data || !Array.isArray(matrix.data) || matrix.data.length !== 3) {
|
|
877
|
+
throw new Error("transformPathData: matrix must be 3x3");
|
|
878
|
+
}
|
|
879
|
+
if (!Array.isArray(matrix.data[0]) || matrix.data[0].length < 2 ||
|
|
880
|
+
!Array.isArray(matrix.data[1]) || matrix.data[1].length < 2 ||
|
|
881
|
+
!Array.isArray(matrix.data[2]) || matrix.data[2].length < 1) {
|
|
882
|
+
throw new Error("transformPathData: matrix must have valid 3x3 structure");
|
|
883
|
+
}
|
|
884
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
885
|
+
throw new Error("transformPathData: precision must be non-negative");
|
|
886
|
+
}
|
|
700
887
|
const absPath = pathToAbsolute(pathData);
|
|
701
888
|
const commands = parsePathData(absPath);
|
|
702
889
|
const result = [];
|
|
703
890
|
for (const { command, args } of commands) {
|
|
704
891
|
if (command === "M" || command === "L") {
|
|
892
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
705
893
|
const pt = Matrix.from([[args[0]], [args[1]], [new Decimal(1)]]);
|
|
706
894
|
const transformed = matrix.mul(pt);
|
|
707
|
-
const
|
|
708
|
-
|
|
895
|
+
const w = transformed.data[2][0];
|
|
896
|
+
if (w.isZero()) {
|
|
897
|
+
throw new Error("transformPathData: division by zero in homogeneous coordinate transformation");
|
|
898
|
+
}
|
|
899
|
+
const x = transformed.data[0][0].div(w);
|
|
900
|
+
const y = transformed.data[1][0].div(w);
|
|
709
901
|
result.push({ command, args: [x, y] });
|
|
710
902
|
} else if (command === "C") {
|
|
903
|
+
if (args.length < 6) continue; // Skip malformed command
|
|
711
904
|
const transformedArgs = [];
|
|
712
905
|
for (let i = 0; i < 6; i += 2) {
|
|
713
906
|
const pt = Matrix.from([[args[i]], [args[i + 1]], [new Decimal(1)]]);
|
|
714
907
|
const transformed = matrix.mul(pt);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
);
|
|
908
|
+
const w = transformed.data[2][0];
|
|
909
|
+
if (w.isZero()) {
|
|
910
|
+
throw new Error("transformPathData: division by zero in homogeneous coordinate transformation");
|
|
911
|
+
}
|
|
912
|
+
transformedArgs.push(transformed.data[0][0].div(w));
|
|
913
|
+
transformedArgs.push(transformed.data[1][0].div(w));
|
|
721
914
|
}
|
|
722
915
|
result.push({ command, args: transformedArgs });
|
|
723
916
|
} else if (command === "A") {
|
|
917
|
+
if (args.length < 7) continue; // Skip malformed command
|
|
724
918
|
const [newRx, newRy, newRot, newLarge, newSweep, newEndX, newEndY] =
|
|
725
919
|
transformArcParams(
|
|
726
920
|
args[0],
|
|
@@ -754,6 +948,9 @@ export function transformPathData(pathData, matrix, precision = 6) {
|
|
|
754
948
|
* @returns {Array<Decimal>} Cubic Bezier control points [cp1x, cp1y, cp2x, cp2y, x2, y2]
|
|
755
949
|
*/
|
|
756
950
|
function quadraticToCubic(x0, y0, x1, y1, x2, y2) {
|
|
951
|
+
if (x0 == null || y0 == null || x1 == null || y1 == null || x2 == null || y2 == null) {
|
|
952
|
+
throw new Error("quadraticToCubic: all parameters (x0, y0, x1, y1, x2, y2) are required");
|
|
953
|
+
}
|
|
757
954
|
const twoThirds = new Decimal(2).div(3);
|
|
758
955
|
const cp1x = x0.plus(twoThirds.mul(x1.minus(x0)));
|
|
759
956
|
const cp1y = y0.plus(twoThirds.mul(y1.minus(y0)));
|
|
@@ -768,6 +965,9 @@ function quadraticToCubic(x0, y0, x1, y1, x2, y2) {
|
|
|
768
965
|
* @returns {string} Path data with only M, C, and Z commands
|
|
769
966
|
*/
|
|
770
967
|
export function pathToCubics(pathData) {
|
|
968
|
+
if (pathData == null) {
|
|
969
|
+
throw new Error("pathToCubics: pathData parameter is required");
|
|
970
|
+
}
|
|
771
971
|
const absPath = pathToAbsolute(pathData);
|
|
772
972
|
const commands = parsePathData(absPath);
|
|
773
973
|
const result = [];
|
|
@@ -778,11 +978,13 @@ export function pathToCubics(pathData) {
|
|
|
778
978
|
let lastCommand = "";
|
|
779
979
|
for (const { command, args } of commands) {
|
|
780
980
|
if (command === "M") {
|
|
981
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
781
982
|
currentX = args[0];
|
|
782
983
|
currentY = args[1];
|
|
783
984
|
result.push({ command: "M", args: [currentX, currentY] });
|
|
784
985
|
lastCommand = "M";
|
|
785
986
|
} else if (command === "L") {
|
|
987
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
786
988
|
const x = args[0],
|
|
787
989
|
y = args[1];
|
|
788
990
|
result.push({ command: "C", args: [currentX, currentY, x, y, x, y] });
|
|
@@ -790,6 +992,7 @@ export function pathToCubics(pathData) {
|
|
|
790
992
|
currentY = y;
|
|
791
993
|
lastCommand = "L";
|
|
792
994
|
} else if (command === "C") {
|
|
995
|
+
if (args.length < 6) continue; // Skip malformed command
|
|
793
996
|
const [x1, y1, x2, y2, x, y] = args;
|
|
794
997
|
result.push({ command: "C", args: [x1, y1, x2, y2, x, y] });
|
|
795
998
|
lastControlX = x2;
|
|
@@ -798,6 +1001,7 @@ export function pathToCubics(pathData) {
|
|
|
798
1001
|
currentY = y;
|
|
799
1002
|
lastCommand = "C";
|
|
800
1003
|
} else if (command === "S") {
|
|
1004
|
+
if (args.length < 4) continue; // Skip malformed command
|
|
801
1005
|
let x1, y1;
|
|
802
1006
|
if (lastCommand === "C" || lastCommand === "S") {
|
|
803
1007
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
@@ -814,6 +1018,7 @@ export function pathToCubics(pathData) {
|
|
|
814
1018
|
currentY = y;
|
|
815
1019
|
lastCommand = "S";
|
|
816
1020
|
} else if (command === "Q") {
|
|
1021
|
+
if (args.length < 4) continue; // Skip malformed command
|
|
817
1022
|
const [x1, y1, x, y] = args;
|
|
818
1023
|
const cubic = quadraticToCubic(currentX, currentY, x1, y1, x, y);
|
|
819
1024
|
result.push({ command: "C", args: cubic });
|
|
@@ -823,6 +1028,7 @@ export function pathToCubics(pathData) {
|
|
|
823
1028
|
currentY = y;
|
|
824
1029
|
lastCommand = "Q";
|
|
825
1030
|
} else if (command === "T") {
|
|
1031
|
+
if (args.length < 2) continue; // Skip malformed command
|
|
826
1032
|
let x1, y1;
|
|
827
1033
|
if (lastCommand === "Q" || lastCommand === "T") {
|
|
828
1034
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
@@ -857,6 +1063,12 @@ export function pathToCubics(pathData) {
|
|
|
857
1063
|
* @returns {string|null} SVG path data string, or null if element type not supported
|
|
858
1064
|
*/
|
|
859
1065
|
export function convertElementToPath(element, precision = 6) {
|
|
1066
|
+
if (element == null) {
|
|
1067
|
+
throw new Error("convertElementToPath: element parameter is required");
|
|
1068
|
+
}
|
|
1069
|
+
if (!Number.isFinite(precision) || precision < 0) {
|
|
1070
|
+
throw new Error("convertElementToPath: precision must be non-negative");
|
|
1071
|
+
}
|
|
860
1072
|
const getAttr = (name, defaultValue = 0) => {
|
|
861
1073
|
const rawValue = element.getAttribute
|
|
862
1074
|
? element.getAttribute(name)
|
|
@@ -916,8 +1128,21 @@ export function convertElementToPath(element, precision = 6) {
|
|
|
916
1128
|
* @returns {number} Numeric value without units
|
|
917
1129
|
*/
|
|
918
1130
|
function stripUnits(val) {
|
|
1131
|
+
if (val == null) {
|
|
1132
|
+
return 0;
|
|
1133
|
+
}
|
|
919
1134
|
if (typeof val === "string") {
|
|
920
|
-
|
|
1135
|
+
const parsed = parseFloat(val);
|
|
1136
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof val === "number") {
|
|
1139
|
+
return isNaN(val) ? 0 : val;
|
|
1140
|
+
}
|
|
1141
|
+
// Handle Decimal type
|
|
1142
|
+
if (val instanceof Decimal) {
|
|
1143
|
+
return val.toNumber();
|
|
921
1144
|
}
|
|
922
|
-
|
|
1145
|
+
// Fallback: try to convert to number
|
|
1146
|
+
const num = Number(val);
|
|
1147
|
+
return isNaN(num) ? 0 : num;
|
|
923
1148
|
}
|