@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.
Files changed (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. package/src/verification.js +392 -1
@@ -41,6 +41,99 @@ import {
41
41
  formatNumber,
42
42
  } from "./convert-path-data.js";
43
43
 
44
+ // ============================================================================
45
+ // INPUT VALIDATION HELPERS
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Validate path string input - fail fast on invalid input
50
+ * @param {string} d - Path d attribute to validate
51
+ * @throws {TypeError} If d is not a valid string
52
+ */
53
+ function validatePathString(d) {
54
+ if (typeof d !== "string") {
55
+ throw new TypeError("Path d must be a string");
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Validate numeric parameter - fail fast on invalid numbers
61
+ * Reserved for future use - prefixed with underscore to indicate internal/unused status
62
+ * @param {number} value - Numeric value to validate
63
+ * @param {string} name - Parameter name for error message
64
+ * @throws {TypeError} If value is not a finite number
65
+ */
66
+ function _validateNumber(value, name) {
67
+ if (typeof value !== "number" || !Number.isFinite(value)) {
68
+ throw new TypeError(`${name} must be a finite number, got ${value}`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate precision parameter - fail fast on invalid precision
74
+ * @param {number} precision - Precision value to validate
75
+ * @throws {TypeError} If precision is not a valid non-negative finite number
76
+ */
77
+ function validatePrecision(precision) {
78
+ if (
79
+ typeof precision !== "number" ||
80
+ !Number.isFinite(precision) ||
81
+ precision < 0
82
+ ) {
83
+ throw new TypeError(
84
+ `Precision must be a finite non-negative number, got ${precision}`,
85
+ );
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Validate tolerance parameter - fail fast on invalid tolerance
91
+ * @param {number} tolerance - Tolerance value to validate
92
+ * @throws {TypeError} If tolerance is not a valid non-negative finite number
93
+ */
94
+ function validateTolerance(tolerance) {
95
+ if (
96
+ typeof tolerance !== "number" ||
97
+ !Number.isFinite(tolerance) ||
98
+ tolerance < 0
99
+ ) {
100
+ throw new TypeError(
101
+ `Tolerance must be a finite non-negative number, got ${tolerance}`,
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Validate command object structure - fail fast on invalid commands
108
+ * @param {object} cmd - Command object to validate
109
+ * @param {number} minArgs - Minimum required args length (optional)
110
+ * @throws {TypeError} If command structure is invalid
111
+ */
112
+ function validateCommand(cmd, minArgs = 0) {
113
+ if (!cmd || typeof cmd !== "object") {
114
+ throw new TypeError("Command must be an object");
115
+ }
116
+ if (typeof cmd.command !== "string") {
117
+ throw new TypeError("Command must have a string 'command' property");
118
+ }
119
+ if (!Array.isArray(cmd.args)) {
120
+ throw new TypeError("Command must have an array 'args' property");
121
+ }
122
+ if (cmd.args.length < minArgs) {
123
+ throw new TypeError(
124
+ `Command ${cmd.command} requires at least ${minArgs} args, got ${cmd.args.length}`,
125
+ );
126
+ }
127
+ // Validate all args are finite numbers
128
+ for (let i = 0; i < cmd.args.length; i++) {
129
+ if (typeof cmd.args[i] !== "number" || !Number.isFinite(cmd.args[i])) {
130
+ throw new TypeError(
131
+ `Command ${cmd.command} arg[${i}] must be a finite number, got ${cmd.args[i]}`,
132
+ );
133
+ }
134
+ }
135
+ }
136
+
44
137
  // ============================================================================
45
138
  // PLUGIN: removeLeadingZero
46
139
  // Removes leading zeros from decimal numbers (0.5 -> .5)
@@ -54,16 +147,27 @@ import {
54
147
  * @returns {string} Optimized path
55
148
  */
56
149
  export function removeLeadingZero(d, precision = 3) {
150
+ // Validate input parameters - fail fast if invalid
151
+ validatePathString(d);
152
+ validatePrecision(precision);
153
+
57
154
  const commands = parsePath(d);
58
155
  if (commands.length === 0) return d;
59
156
 
157
+ // Validate all commands have proper structure
158
+ commands.forEach((cmd) => validateCommand(cmd));
159
+
60
160
  // Format numbers with leading zero removal
61
161
  const formatted = commands.map((cmd) => ({
62
162
  command: cmd.command,
63
163
  args: cmd.args.map((n) => {
64
164
  const str = formatNumber(n, precision);
65
- // formatNumber already handles leading zero removal
66
- return parseFloat(str);
165
+ const parsed = parseFloat(str);
166
+ // Ensure the parsed value is valid - fail fast if corrupted
167
+ if (!Number.isFinite(parsed)) {
168
+ throw new Error(`Invalid numeric value encountered: ${str}`);
169
+ }
170
+ return parsed;
67
171
  }),
68
172
  }));
69
173
 
@@ -83,9 +187,16 @@ export function removeLeadingZero(d, precision = 3) {
83
187
  * @returns {string} Optimized path
84
188
  */
85
189
  export function negativeExtraSpace(d, precision = 3) {
190
+ // Validate input parameters - fail fast if invalid
191
+ validatePathString(d);
192
+ validatePrecision(precision);
193
+
86
194
  const commands = parsePath(d);
87
195
  if (commands.length === 0) return d;
88
196
 
197
+ // Validate command structures
198
+ commands.forEach((cmd) => validateCommand(cmd));
199
+
89
200
  // serializePath already handles this optimization
90
201
  return serializePath(commands, precision);
91
202
  }
@@ -102,9 +213,16 @@ export function negativeExtraSpace(d, precision = 3) {
102
213
  * @returns {string} Path with relative commands
103
214
  */
104
215
  export function convertToRelative(d, precision = 3) {
216
+ // Validate input parameters - fail fast if invalid
217
+ validatePathString(d);
218
+ validatePrecision(precision);
219
+
105
220
  const commands = parsePath(d);
106
221
  if (commands.length === 0) return d;
107
222
 
223
+ // Validate command structures
224
+ commands.forEach((cmd) => validateCommand(cmd));
225
+
108
226
  let cx = 0,
109
227
  cy = 0;
110
228
  let startX = 0,
@@ -153,6 +271,8 @@ export function convertToRelative(d, precision = 3) {
153
271
  cx = startX;
154
272
  cy = startY;
155
273
  break;
274
+ default:
275
+ break;
156
276
  }
157
277
  }
158
278
 
@@ -171,9 +291,16 @@ export function convertToRelative(d, precision = 3) {
171
291
  * @returns {string} Path with absolute commands
172
292
  */
173
293
  export function convertToAbsolute(d, precision = 3) {
294
+ // Validate input parameters - fail fast if invalid
295
+ validatePathString(d);
296
+ validatePrecision(precision);
297
+
174
298
  const commands = parsePath(d);
175
299
  if (commands.length === 0) return d;
176
300
 
301
+ // Validate command structures
302
+ commands.forEach((cmd) => validateCommand(cmd));
303
+
177
304
  let cx = 0,
178
305
  cy = 0;
179
306
  let startX = 0,
@@ -221,6 +348,8 @@ export function convertToAbsolute(d, precision = 3) {
221
348
  cx = startX;
222
349
  cy = startY;
223
350
  break;
351
+ default:
352
+ break;
224
353
  }
225
354
  }
226
355
 
@@ -241,9 +370,17 @@ export function convertToAbsolute(d, precision = 3) {
241
370
  * @returns {string} Optimized path
242
371
  */
243
372
  export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
373
+ // Validate input parameters - fail fast if invalid
374
+ validatePathString(d);
375
+ validateTolerance(tolerance);
376
+ validatePrecision(precision);
377
+
244
378
  const commands = parsePath(d);
245
379
  if (commands.length === 0) return d;
246
380
 
381
+ // Validate command structures
382
+ commands.forEach((cmd) => validateCommand(cmd));
383
+
247
384
  let cx = 0,
248
385
  cy = 0;
249
386
  let startX = 0,
@@ -310,6 +447,8 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
310
447
  cx = startX;
311
448
  cy = startY;
312
449
  break;
450
+ default:
451
+ break;
313
452
  }
314
453
  }
315
454
 
@@ -330,9 +469,17 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
330
469
  * @returns {string} Optimized path
331
470
  */
332
471
  export function convertToZ(d, tolerance = 1e-6, precision = 3) {
472
+ // Validate input parameters - fail fast if invalid
473
+ validatePathString(d);
474
+ validateTolerance(tolerance);
475
+ validatePrecision(precision);
476
+
333
477
  const commands = parsePath(d);
334
478
  if (commands.length === 0) return d;
335
479
 
480
+ // Validate command structures
481
+ commands.forEach((cmd) => validateCommand(cmd));
482
+
336
483
  let cx = 0,
337
484
  cy = 0;
338
485
  let startX = 0,
@@ -397,6 +544,8 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
397
544
  cx = startX;
398
545
  cy = startY;
399
546
  break;
547
+ default:
548
+ break;
400
549
  }
401
550
  }
402
551
 
@@ -407,6 +556,36 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
407
556
  // BEZIER CURVE UTILITIES - Proper mathematical evaluation and distance
408
557
  // ============================================================================
409
558
 
559
+ /**
560
+ * Validate that all coordinate parameters are finite numbers
561
+ * @param {...number} coords - Coordinate values to validate
562
+ * @throws {TypeError} If any coordinate is not a finite number
563
+ */
564
+ function validateCoordinates(...coords) {
565
+ for (let i = 0; i < coords.length; i++) {
566
+ if (typeof coords[i] !== "number" || !Number.isFinite(coords[i])) {
567
+ throw new TypeError(
568
+ `Coordinate at position ${i} must be a finite number, got ${coords[i]}`,
569
+ );
570
+ }
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Validate t parameter is in valid range [0, 1]
576
+ * @param {number} t - Parameter value to validate
577
+ * @throws {TypeError} If t is not a valid number
578
+ * @throws {RangeError} If t is outside [0, 1] range
579
+ */
580
+ function validateT(t) {
581
+ if (typeof t !== "number" || !Number.isFinite(t)) {
582
+ throw new TypeError(`Parameter t must be a finite number, got ${t}`);
583
+ }
584
+ if (t < 0 || t > 1) {
585
+ throw new RangeError(`Parameter t must be in range [0, 1], got ${t}`);
586
+ }
587
+ }
588
+
410
589
  /**
411
590
  * Evaluate a cubic Bezier curve at parameter t.
412
591
  * B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
@@ -422,6 +601,10 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
422
601
  * @returns {{x: number, y: number}} Point on curve
423
602
  */
424
603
  function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
604
+ // Validate all inputs - fail fast on invalid data
605
+ validateT(t);
606
+ validateCoordinates(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
607
+
425
608
  const mt = 1 - t;
426
609
  const mt2 = mt * mt;
427
610
  const mt3 = mt2 * mt;
@@ -448,6 +631,10 @@ function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
448
631
  * @returns {{x: number, y: number}} First derivative vector
449
632
  */
450
633
  function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
634
+ // Validate all inputs - fail fast on invalid data
635
+ validateT(t);
636
+ validateCoordinates(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
637
+
451
638
  const mt = 1 - t;
452
639
  const mt2 = mt * mt;
453
640
  const t2 = t * t;
@@ -472,6 +659,10 @@ function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
472
659
  * @returns {{x: number, y: number}} Second derivative vector
473
660
  */
474
661
  function cubicBezierDeriv2(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
662
+ // Validate all inputs - fail fast on invalid data
663
+ validateT(t);
664
+ validateCoordinates(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
665
+
475
666
  const mt = 1 - t;
476
667
  return {
477
668
  x: 6 * mt * (p2x - 2 * p1x + p0x) + 6 * t * (p3x - 2 * p2x + p1x),
@@ -508,9 +699,14 @@ function _closestTOnCubicBezier(
508
699
  p3y,
509
700
  tInit = 0.5,
510
701
  ) {
702
+ // Validate all coordinate inputs - fail fast on invalid data
703
+ validateCoordinates(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
704
+ // Note: tInit is clamped below so no need to strictly validate it
705
+
511
706
  let t = Math.max(0, Math.min(1, tInit));
512
707
 
513
708
  for (let iter = 0; iter < 10; iter++) {
709
+ // Note: t is always clamped to [0,1] so cubicBezierPoint validation won't fail
514
710
  const Q = cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
515
711
  const Q1 = cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
516
712
  const Q2 = cubicBezierDeriv2(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y);
@@ -523,6 +719,7 @@ function _closestTOnCubicBezier(
523
719
  // f'(t) = B'(t)·B'(t) + (B(t)-p)·B''(t)
524
720
  const fp = Q1.x * Q1.x + Q1.y * Q1.y + diffX * Q2.x + diffY * Q2.y;
525
721
 
722
+ // Division by zero check - fail fast if derivative is zero
526
723
  if (Math.abs(fp) < 1e-12) break;
527
724
 
528
725
  const tNext = Math.max(0, Math.min(1, t - f / fp));
@@ -547,10 +744,14 @@ function _closestTOnCubicBezier(
547
744
  * @returns {number} Distance from point to line segment
548
745
  */
549
746
  function pointToLineDistance(px, py, x0, y0, x1, y1) {
747
+ // Validate all coordinate inputs - fail fast on invalid data
748
+ validateCoordinates(px, py, x0, y0, x1, y1);
749
+
550
750
  const dx = x1 - x0;
551
751
  const dy = y1 - y0;
552
752
  const lengthSq = dx * dx + dy * dy;
553
753
 
754
+ // Handle degenerate case where line segment is a point
554
755
  if (lengthSq < 1e-10) {
555
756
  return Math.sqrt((px - x0) ** 2 + (py - y0) ** 2);
556
757
  }
@@ -579,6 +780,9 @@ function pointToLineDistance(px, py, x0, y0, x1, y1) {
579
780
  * @returns {number} Maximum distance from any curve point to the line
580
781
  */
581
782
  function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
783
+ // Validate all coordinate inputs - fail fast on invalid data
784
+ validateCoordinates(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y);
785
+
582
786
  let maxErr = 0;
583
787
 
584
788
  // Sample at regular t intervals and find closest point on line
@@ -650,9 +854,17 @@ function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance) {
650
854
  * @returns {string} Optimized path
651
855
  */
652
856
  export function straightCurves(d, tolerance = 0.5, precision = 3) {
857
+ // Validate input parameters - fail fast if invalid
858
+ validatePathString(d);
859
+ validateTolerance(tolerance);
860
+ validatePrecision(precision);
861
+
653
862
  const commands = parsePath(d);
654
863
  if (commands.length === 0) return d;
655
864
 
865
+ // Validate command structures
866
+ commands.forEach((cmd) => validateCommand(cmd));
867
+
656
868
  let cx = 0,
657
869
  cy = 0;
658
870
  let startX = 0,
@@ -719,6 +931,8 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
719
931
  cx = startX;
720
932
  cy = startY;
721
933
  break;
934
+ default:
935
+ break;
722
936
  }
723
937
  }
724
938
 
@@ -739,9 +953,16 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
739
953
  * @returns {string} Optimized path
740
954
  */
741
955
  export function collapseRepeated(d, precision = 3) {
956
+ // Validate input parameters - fail fast if invalid
957
+ validatePathString(d);
958
+ validatePrecision(precision);
959
+
742
960
  const commands = parsePath(d);
743
961
  if (commands.length === 0) return d;
744
962
 
963
+ // Validate command structures
964
+ commands.forEach((cmd) => validateCommand(cmd));
965
+
745
966
  // serializePath already collapses repeated commands
746
967
  return serializePath(commands, precision);
747
968
  }
@@ -759,9 +980,21 @@ export function collapseRepeated(d, precision = 3) {
759
980
  * @returns {string} Path with rounded numbers
760
981
  */
761
982
  export function floatPrecision(d, precision = 3) {
983
+ // Validate input parameters - fail fast if invalid
984
+ validatePathString(d);
985
+ validatePrecision(precision);
986
+
987
+ // Ensure precision is reasonable to avoid overflow
988
+ if (precision > 15) {
989
+ throw new RangeError(`Precision ${precision} is too large (max 15)`);
990
+ }
991
+
762
992
  const commands = parsePath(d);
763
993
  if (commands.length === 0) return d;
764
994
 
995
+ // Validate command structures
996
+ commands.forEach((cmd) => validateCommand(cmd));
997
+
765
998
  const factor = Math.pow(10, precision);
766
999
  const rounded = commands.map((cmd) => ({
767
1000
  command: cmd.command,
@@ -785,9 +1018,17 @@ export function floatPrecision(d, precision = 3) {
785
1018
  * @returns {string} Optimized path
786
1019
  */
787
1020
  export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
1021
+ // Validate input parameters - fail fast if invalid
1022
+ validatePathString(d);
1023
+ validateTolerance(tolerance);
1024
+ validatePrecision(precision);
1025
+
788
1026
  const commands = parsePath(d);
789
1027
  if (commands.length === 0) return d;
790
1028
 
1029
+ // Validate command structures
1030
+ commands.forEach((cmd) => validateCommand(cmd));
1031
+
791
1032
  let cx = 0,
792
1033
  cy = 0;
793
1034
  let startX = 0,
@@ -832,6 +1073,8 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
832
1073
  keep = false;
833
1074
  }
834
1075
  break;
1076
+ default:
1077
+ break;
835
1078
  }
836
1079
 
837
1080
  if (keep) {
@@ -873,6 +1116,8 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
873
1116
  cx = startX;
874
1117
  cy = startY;
875
1118
  break;
1119
+ default:
1120
+ break;
876
1121
  }
877
1122
  }
878
1123
  }
@@ -898,6 +1143,10 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
898
1143
  * @returns {{x: number, y: number}} Point on curve
899
1144
  */
900
1145
  function quadraticBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y) {
1146
+ // Validate all inputs - fail fast on invalid data
1147
+ validateT(t);
1148
+ validateCoordinates(p0x, p0y, p1x, p1y, p2x, p2y);
1149
+
901
1150
  const mt = 1 - t;
902
1151
  const mt2 = mt * mt;
903
1152
  const t2 = t * t;
@@ -934,6 +1183,9 @@ function maxErrorCubicToQuadratic(
934
1183
  qx,
935
1184
  qy,
936
1185
  ) {
1186
+ // Validate all coordinate inputs - fail fast on invalid data
1187
+ validateCoordinates(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y, qx, qy);
1188
+
937
1189
  let maxErr = 0;
938
1190
 
939
1191
  // Dense sampling including midpoints
@@ -990,6 +1242,10 @@ function cubicToQuadraticControlPoint(
990
1242
  y3,
991
1243
  tolerance,
992
1244
  ) {
1245
+ // Validate all coordinate inputs - fail fast on invalid data
1246
+ validateCoordinates(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3);
1247
+ validateTolerance(tolerance);
1248
+
993
1249
  // Calculate the best-fit quadratic control point
994
1250
  // For a cubic to be exactly representable as quadratic:
995
1251
  // Q = (3*(P1 + P2) - P0 - P3) / 4
@@ -1026,9 +1282,17 @@ function cubicToQuadraticControlPoint(
1026
1282
  * @returns {string} Optimized path
1027
1283
  */
1028
1284
  export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
1285
+ // Validate input parameters - fail fast if invalid
1286
+ validatePathString(d);
1287
+ validateTolerance(tolerance);
1288
+ validatePrecision(precision);
1289
+
1029
1290
  const commands = parsePath(d);
1030
1291
  if (commands.length === 0) return d;
1031
1292
 
1293
+ // Validate command structures
1294
+ commands.forEach((cmd) => validateCommand(cmd));
1295
+
1032
1296
  let cx = 0,
1033
1297
  cy = 0;
1034
1298
  let startX = 0,
@@ -1108,6 +1372,8 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
1108
1372
  cx = startX;
1109
1373
  cy = startY;
1110
1374
  break;
1375
+ default:
1376
+ break;
1111
1377
  }
1112
1378
  }
1113
1379
 
@@ -1128,9 +1394,17 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
1128
1394
  * @returns {string} Optimized path
1129
1395
  */
1130
1396
  export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
1397
+ // Validate input parameters - fail fast if invalid
1398
+ validatePathString(d);
1399
+ validateTolerance(tolerance);
1400
+ validatePrecision(precision);
1401
+
1131
1402
  const commands = parsePath(d);
1132
1403
  if (commands.length === 0) return d;
1133
1404
 
1405
+ // Validate command structures
1406
+ commands.forEach((cmd) => validateCommand(cmd));
1407
+
1134
1408
  let cx = 0,
1135
1409
  cy = 0;
1136
1410
  let startX = 0,
@@ -1168,9 +1442,15 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
1168
1442
  lastQcpX = abs.args[0];
1169
1443
  lastQcpY = abs.args[1];
1170
1444
  } else if (abs.command === "T") {
1171
- // T uses reflected control point
1172
- lastQcpX = 2 * cx - lastQcpX;
1173
- lastQcpY = 2 * cy - lastQcpY;
1445
+ // T uses reflected control point - only valid if previous was Q or T
1446
+ if (lastQcpX !== null) {
1447
+ lastQcpX = 2 * cx - lastQcpX;
1448
+ lastQcpY = 2 * cy - lastQcpY;
1449
+ } else {
1450
+ // T after non-Q command - control point is current position (no reflection)
1451
+ lastQcpX = cx;
1452
+ lastQcpY = cy;
1453
+ }
1174
1454
  } else {
1175
1455
  lastQcpX = null;
1176
1456
  lastQcpY = null;
@@ -1212,6 +1492,8 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
1212
1492
  cx = startX;
1213
1493
  cy = startY;
1214
1494
  break;
1495
+ default:
1496
+ break;
1215
1497
  }
1216
1498
  }
1217
1499
 
@@ -1232,9 +1514,17 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
1232
1514
  * @returns {string} Optimized path
1233
1515
  */
1234
1516
  export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
1517
+ // Validate input parameters - fail fast if invalid
1518
+ validatePathString(d);
1519
+ validateTolerance(tolerance);
1520
+ validatePrecision(precision);
1521
+
1235
1522
  const commands = parsePath(d);
1236
1523
  if (commands.length === 0) return d;
1237
1524
 
1525
+ // Validate command structures
1526
+ commands.forEach((cmd) => validateCommand(cmd));
1527
+
1238
1528
  let cx = 0,
1239
1529
  cy = 0;
1240
1530
  let startX = 0,
@@ -1284,6 +1574,7 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
1284
1574
  lastCcp2Y = abs.args[3];
1285
1575
  } else if (abs.command === "S") {
1286
1576
  // S uses its control point as the second cubic control point
1577
+ // This is always valid since S command provides explicit control point
1287
1578
  lastCcp2X = abs.args[0];
1288
1579
  lastCcp2Y = abs.args[1];
1289
1580
  } else {
@@ -1327,6 +1618,8 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
1327
1618
  cx = startX;
1328
1619
  cy = startY;
1329
1620
  break;
1621
+ default:
1622
+ break;
1330
1623
  }
1331
1624
  }
1332
1625
 
@@ -1347,12 +1640,23 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
1347
1640
  * @returns {string} Optimized path
1348
1641
  */
1349
1642
  export function arcShorthands(d, precision = 3) {
1643
+ // Validate input parameters - fail fast if invalid
1644
+ validatePathString(d);
1645
+ validatePrecision(precision);
1646
+
1350
1647
  const commands = parsePath(d);
1351
1648
  if (commands.length === 0) return d;
1352
1649
 
1650
+ // Validate command structures
1651
+ commands.forEach((cmd) => validateCommand(cmd));
1652
+
1353
1653
  const result = commands.map((cmd) => {
1354
1654
  if (cmd.command === "A" || cmd.command === "a") {
1355
1655
  const args = [...cmd.args];
1656
+ // Ensure args array has correct length for arc command
1657
+ if (args.length !== 7) {
1658
+ throw new Error(`Arc command requires 7 arguments, got ${args.length}`);
1659
+ }
1356
1660
  // Normalize rotation angle to 0-360
1357
1661
  args[2] = ((args[2] % 360) + 360) % 360;
1358
1662
  // If rx == ry, rotation doesn't matter, set to 0