@emasoft/svg-matrix 1.0.28 → 1.0.30

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 (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +985 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +723 -180
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +18 -7
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +22 -18
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16381 -3370
  34. package/src/svg2-polyfills.js +93 -224
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -33,7 +33,13 @@
33
33
  * @module path-data-plugins
34
34
  */
35
35
 
36
- import { parsePath, serializePath, toAbsolute, toRelative, formatNumber } from './convert-path-data.js';
36
+ import {
37
+ parsePath,
38
+ serializePath,
39
+ toAbsolute,
40
+ toRelative,
41
+ formatNumber,
42
+ } from "./convert-path-data.js";
37
43
 
38
44
  // ============================================================================
39
45
  // PLUGIN: removeLeadingZero
@@ -52,13 +58,13 @@ export function removeLeadingZero(d, precision = 3) {
52
58
  if (commands.length === 0) return d;
53
59
 
54
60
  // Format numbers with leading zero removal
55
- const formatted = commands.map(cmd => ({
61
+ const formatted = commands.map((cmd) => ({
56
62
  command: cmd.command,
57
- args: cmd.args.map(n => {
58
- let str = formatNumber(n, precision);
63
+ args: cmd.args.map((n) => {
64
+ const str = formatNumber(n, precision);
59
65
  // formatNumber already handles leading zero removal
60
66
  return parseFloat(str);
61
- })
67
+ }),
62
68
  }));
63
69
 
64
70
  return serializePath(formatted, precision);
@@ -99,8 +105,10 @@ export function convertToRelative(d, precision = 3) {
99
105
  const commands = parsePath(d);
100
106
  if (commands.length === 0) return d;
101
107
 
102
- let cx = 0, cy = 0;
103
- let startX = 0, startY = 0;
108
+ let cx = 0,
109
+ cy = 0;
110
+ let startX = 0,
111
+ startY = 0;
104
112
  const result = [];
105
113
 
106
114
  for (const cmd of commands) {
@@ -111,14 +119,40 @@ export function convertToRelative(d, precision = 3) {
111
119
  // Update position using absolute form
112
120
  const abs = toAbsolute(cmd, cx, cy);
113
121
  switch (abs.command) {
114
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
115
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
116
- case 'H': cx = abs.args[0]; break;
117
- case 'V': cy = abs.args[0]; break;
118
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
119
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
120
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
121
- case 'Z': cx = startX; cy = startY; break;
122
+ case "M":
123
+ cx = abs.args[0];
124
+ cy = abs.args[1];
125
+ startX = cx;
126
+ startY = cy;
127
+ break;
128
+ case "L":
129
+ case "T":
130
+ cx = abs.args[0];
131
+ cy = abs.args[1];
132
+ break;
133
+ case "H":
134
+ cx = abs.args[0];
135
+ break;
136
+ case "V":
137
+ cy = abs.args[0];
138
+ break;
139
+ case "C":
140
+ cx = abs.args[4];
141
+ cy = abs.args[5];
142
+ break;
143
+ case "S":
144
+ case "Q":
145
+ cx = abs.args[2];
146
+ cy = abs.args[3];
147
+ break;
148
+ case "A":
149
+ cx = abs.args[5];
150
+ cy = abs.args[6];
151
+ break;
152
+ case "Z":
153
+ cx = startX;
154
+ cy = startY;
155
+ break;
122
156
  }
123
157
  }
124
158
 
@@ -140,8 +174,10 @@ export function convertToAbsolute(d, precision = 3) {
140
174
  const commands = parsePath(d);
141
175
  if (commands.length === 0) return d;
142
176
 
143
- let cx = 0, cy = 0;
144
- let startX = 0, startY = 0;
177
+ let cx = 0,
178
+ cy = 0;
179
+ let startX = 0,
180
+ startY = 0;
145
181
  const result = [];
146
182
 
147
183
  for (const cmd of commands) {
@@ -151,14 +187,40 @@ export function convertToAbsolute(d, precision = 3) {
151
187
 
152
188
  // Update position
153
189
  switch (abs.command) {
154
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
155
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
156
- case 'H': cx = abs.args[0]; break;
157
- case 'V': cy = abs.args[0]; break;
158
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
159
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
160
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
161
- case 'Z': cx = startX; cy = startY; break;
190
+ case "M":
191
+ cx = abs.args[0];
192
+ cy = abs.args[1];
193
+ startX = cx;
194
+ startY = cy;
195
+ break;
196
+ case "L":
197
+ case "T":
198
+ cx = abs.args[0];
199
+ cy = abs.args[1];
200
+ break;
201
+ case "H":
202
+ cx = abs.args[0];
203
+ break;
204
+ case "V":
205
+ cy = abs.args[0];
206
+ break;
207
+ case "C":
208
+ cx = abs.args[4];
209
+ cy = abs.args[5];
210
+ break;
211
+ case "S":
212
+ case "Q":
213
+ cx = abs.args[2];
214
+ cy = abs.args[3];
215
+ break;
216
+ case "A":
217
+ cx = abs.args[5];
218
+ cy = abs.args[6];
219
+ break;
220
+ case "Z":
221
+ cx = startX;
222
+ cy = startY;
223
+ break;
162
224
  }
163
225
  }
164
226
 
@@ -182,28 +244,30 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
182
244
  const commands = parsePath(d);
183
245
  if (commands.length === 0) return d;
184
246
 
185
- let cx = 0, cy = 0;
186
- let startX = 0, startY = 0;
247
+ let cx = 0,
248
+ cy = 0;
249
+ let startX = 0,
250
+ startY = 0;
187
251
  const result = [];
188
252
 
189
253
  for (const cmd of commands) {
190
254
  let newCmd = cmd;
191
255
 
192
- if (cmd.command === 'L' || cmd.command === 'l') {
193
- const isAbs = cmd.command === 'L';
256
+ if (cmd.command === "L" || cmd.command === "l") {
257
+ const isAbs = cmd.command === "L";
194
258
  const endX = isAbs ? cmd.args[0] : cx + cmd.args[0];
195
259
  const endY = isAbs ? cmd.args[1] : cy + cmd.args[1];
196
260
 
197
261
  if (Math.abs(endY - cy) < tolerance) {
198
262
  // Horizontal line
199
263
  newCmd = isAbs
200
- ? { command: 'H', args: [endX] }
201
- : { command: 'h', args: [endX - cx] };
264
+ ? { command: "H", args: [endX] }
265
+ : { command: "h", args: [endX - cx] };
202
266
  } else if (Math.abs(endX - cx) < tolerance) {
203
267
  // Vertical line
204
268
  newCmd = isAbs
205
- ? { command: 'V', args: [endY] }
206
- : { command: 'v', args: [endY - cy] };
269
+ ? { command: "V", args: [endY] }
270
+ : { command: "v", args: [endY - cy] };
207
271
  }
208
272
  }
209
273
 
@@ -212,14 +276,40 @@ export function lineShorthands(d, tolerance = 1e-6, precision = 3) {
212
276
  // Update position
213
277
  const abs = toAbsolute(newCmd, cx, cy);
214
278
  switch (abs.command) {
215
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
216
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
217
- case 'H': cx = abs.args[0]; break;
218
- case 'V': cy = abs.args[0]; break;
219
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
220
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
221
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
222
- case 'Z': cx = startX; cy = startY; break;
279
+ case "M":
280
+ cx = abs.args[0];
281
+ cy = abs.args[1];
282
+ startX = cx;
283
+ startY = cy;
284
+ break;
285
+ case "L":
286
+ case "T":
287
+ cx = abs.args[0];
288
+ cy = abs.args[1];
289
+ break;
290
+ case "H":
291
+ cx = abs.args[0];
292
+ break;
293
+ case "V":
294
+ cy = abs.args[0];
295
+ break;
296
+ case "C":
297
+ cx = abs.args[4];
298
+ cy = abs.args[5];
299
+ break;
300
+ case "S":
301
+ case "Q":
302
+ cx = abs.args[2];
303
+ cy = abs.args[3];
304
+ break;
305
+ case "A":
306
+ cx = abs.args[5];
307
+ cy = abs.args[6];
308
+ break;
309
+ case "Z":
310
+ cx = startX;
311
+ cy = startY;
312
+ break;
223
313
  }
224
314
  }
225
315
 
@@ -243,8 +333,10 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
243
333
  const commands = parsePath(d);
244
334
  if (commands.length === 0) return d;
245
335
 
246
- let cx = 0, cy = 0;
247
- let startX = 0, startY = 0;
336
+ let cx = 0,
337
+ cy = 0;
338
+ let startX = 0,
339
+ startY = 0;
248
340
  const result = [];
249
341
 
250
342
  for (let i = 0; i < commands.length; i++) {
@@ -252,14 +344,17 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
252
344
  let newCmd = cmd;
253
345
 
254
346
  // Check if this L command goes back to start
255
- if (cmd.command === 'L' || cmd.command === 'l') {
256
- const isAbs = cmd.command === 'L';
347
+ if (cmd.command === "L" || cmd.command === "l") {
348
+ const isAbs = cmd.command === "L";
257
349
  const endX = isAbs ? cmd.args[0] : cx + cmd.args[0];
258
350
  const endY = isAbs ? cmd.args[1] : cy + cmd.args[1];
259
351
 
260
- if (Math.abs(endX - startX) < tolerance && Math.abs(endY - startY) < tolerance) {
352
+ if (
353
+ Math.abs(endX - startX) < tolerance &&
354
+ Math.abs(endY - startY) < tolerance
355
+ ) {
261
356
  // This line closes the path
262
- newCmd = { command: 'z', args: [] };
357
+ newCmd = { command: "z", args: [] };
263
358
  }
264
359
  }
265
360
 
@@ -268,14 +363,40 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
268
363
  // Update position
269
364
  const abs = toAbsolute(newCmd, cx, cy);
270
365
  switch (abs.command) {
271
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
272
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
273
- case 'H': cx = abs.args[0]; break;
274
- case 'V': cy = abs.args[0]; break;
275
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
276
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
277
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
278
- case 'Z': cx = startX; cy = startY; break;
366
+ case "M":
367
+ cx = abs.args[0];
368
+ cy = abs.args[1];
369
+ startX = cx;
370
+ startY = cy;
371
+ break;
372
+ case "L":
373
+ case "T":
374
+ cx = abs.args[0];
375
+ cy = abs.args[1];
376
+ break;
377
+ case "H":
378
+ cx = abs.args[0];
379
+ break;
380
+ case "V":
381
+ cy = abs.args[0];
382
+ break;
383
+ case "C":
384
+ cx = abs.args[4];
385
+ cy = abs.args[5];
386
+ break;
387
+ case "S":
388
+ case "Q":
389
+ cx = abs.args[2];
390
+ cy = abs.args[3];
391
+ break;
392
+ case "A":
393
+ cx = abs.args[5];
394
+ cy = abs.args[6];
395
+ break;
396
+ case "Z":
397
+ cx = startX;
398
+ cy = startY;
399
+ break;
279
400
  }
280
401
  }
281
402
 
@@ -289,6 +410,16 @@ export function convertToZ(d, tolerance = 1e-6, precision = 3) {
289
410
  /**
290
411
  * Evaluate a cubic Bezier curve at parameter t.
291
412
  * B(t) = (1-t)^3*P0 + 3*(1-t)^2*t*P1 + 3*(1-t)*t^2*P2 + t^3*P3
413
+ * @param {number} t - Parameter value (0 to 1)
414
+ * @param {number} p0x - Start point X coordinate
415
+ * @param {number} p0y - Start point Y coordinate
416
+ * @param {number} p1x - First control point X coordinate
417
+ * @param {number} p1y - First control point Y coordinate
418
+ * @param {number} p2x - Second control point X coordinate
419
+ * @param {number} p2y - Second control point Y coordinate
420
+ * @param {number} p3x - End point X coordinate
421
+ * @param {number} p3y - End point Y coordinate
422
+ * @returns {{x: number, y: number}} Point on curve
292
423
  */
293
424
  function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
294
425
  const mt = 1 - t;
@@ -298,13 +429,23 @@ function cubicBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
298
429
  const t3 = t2 * t;
299
430
  return {
300
431
  x: mt3 * p0x + 3 * mt2 * t * p1x + 3 * mt * t2 * p2x + t3 * p3x,
301
- y: mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y
432
+ y: mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y,
302
433
  };
303
434
  }
304
435
 
305
436
  /**
306
437
  * First derivative of cubic Bezier at t.
307
438
  * B'(t) = 3(1-t)^2(P1-P0) + 6(1-t)t(P2-P1) + 3t^2(P3-P2)
439
+ * @param {number} t - Parameter value (0 to 1)
440
+ * @param {number} p0x - Start point X coordinate
441
+ * @param {number} p0y - Start point Y coordinate
442
+ * @param {number} p1x - First control point X coordinate
443
+ * @param {number} p1y - First control point Y coordinate
444
+ * @param {number} p2x - Second control point X coordinate
445
+ * @param {number} p2y - Second control point Y coordinate
446
+ * @param {number} p3x - End point X coordinate
447
+ * @param {number} p3y - End point Y coordinate
448
+ * @returns {{x: number, y: number}} First derivative vector
308
449
  */
309
450
  function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
310
451
  const mt = 1 - t;
@@ -312,27 +453,61 @@ function cubicBezierDeriv1(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
312
453
  const t2 = t * t;
313
454
  return {
314
455
  x: 3 * mt2 * (p1x - p0x) + 6 * mt * t * (p2x - p1x) + 3 * t2 * (p3x - p2x),
315
- y: 3 * mt2 * (p1y - p0y) + 6 * mt * t * (p2y - p1y) + 3 * t2 * (p3y - p2y)
456
+ y: 3 * mt2 * (p1y - p0y) + 6 * mt * t * (p2y - p1y) + 3 * t2 * (p3y - p2y),
316
457
  };
317
458
  }
318
459
 
319
460
  /**
320
461
  * Second derivative of cubic Bezier at t.
321
462
  * B''(t) = 6(1-t)(P2-2P1+P0) + 6t(P3-2P2+P1)
463
+ * @param {number} t - Parameter value (0 to 1)
464
+ * @param {number} p0x - Start point X coordinate
465
+ * @param {number} p0y - Start point Y coordinate
466
+ * @param {number} p1x - First control point X coordinate
467
+ * @param {number} p1y - First control point Y coordinate
468
+ * @param {number} p2x - Second control point X coordinate
469
+ * @param {number} p2y - Second control point Y coordinate
470
+ * @param {number} p3x - End point X coordinate
471
+ * @param {number} p3y - End point Y coordinate
472
+ * @returns {{x: number, y: number}} Second derivative vector
322
473
  */
323
474
  function cubicBezierDeriv2(t, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {
324
475
  const mt = 1 - t;
325
476
  return {
326
477
  x: 6 * mt * (p2x - 2 * p1x + p0x) + 6 * t * (p3x - 2 * p2x + p1x),
327
- y: 6 * mt * (p2y - 2 * p1y + p0y) + 6 * t * (p3y - 2 * p2y + p1y)
478
+ y: 6 * mt * (p2y - 2 * p1y + p0y) + 6 * t * (p3y - 2 * p2y + p1y),
328
479
  };
329
480
  }
330
481
 
331
482
  /**
332
483
  * Find closest t on cubic Bezier to point p using Newton's method.
333
484
  * Minimizes |B(t) - p|^2 by finding root of d/dt |B(t)-p|^2 = 2(B(t)-p)·B'(t) = 0
485
+ * @param {number} px - Query point X coordinate
486
+ * @param {number} py - Query point Y coordinate
487
+ * @param {number} p0x - Bezier start point X
488
+ * @param {number} p0y - Bezier start point Y
489
+ * @param {number} p1x - Bezier first control point X
490
+ * @param {number} p1y - Bezier first control point Y
491
+ * @param {number} p2x - Bezier second control point X
492
+ * @param {number} p2y - Bezier second control point Y
493
+ * @param {number} p3x - Bezier end point X
494
+ * @param {number} p3y - Bezier end point Y
495
+ * @param {number} tInit - Initial t value for Newton iteration
496
+ * @returns {number} Closest parameter t (0 to 1)
334
497
  */
335
- function closestTOnCubicBezier(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, tInit = 0.5) {
498
+ function _closestTOnCubicBezier(
499
+ px,
500
+ py,
501
+ p0x,
502
+ p0y,
503
+ p1x,
504
+ p1y,
505
+ p2x,
506
+ p2y,
507
+ p3x,
508
+ p3y,
509
+ tInit = 0.5,
510
+ ) {
336
511
  let t = Math.max(0, Math.min(1, tInit));
337
512
 
338
513
  for (let iter = 0; iter < 10; iter++) {
@@ -351,7 +526,10 @@ function closestTOnCubicBezier(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t
351
526
  if (Math.abs(fp) < 1e-12) break;
352
527
 
353
528
  const tNext = Math.max(0, Math.min(1, t - f / fp));
354
- if (Math.abs(tNext - t) < 1e-9) { t = tNext; break; }
529
+ if (Math.abs(tNext - t) < 1e-9) {
530
+ t = tNext;
531
+ break;
532
+ }
355
533
  t = tNext;
356
534
  }
357
535
 
@@ -360,6 +538,13 @@ function closestTOnCubicBezier(px, py, p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, t
360
538
 
361
539
  /**
362
540
  * Calculate distance from point to line segment (for straight curve check).
541
+ * @param {number} px - Point X coordinate
542
+ * @param {number} py - Point Y coordinate
543
+ * @param {number} x0 - Line segment start X
544
+ * @param {number} y0 - Line segment start Y
545
+ * @param {number} x1 - Line segment end X
546
+ * @param {number} y1 - Line segment end Y
547
+ * @returns {number} Distance from point to line segment
363
548
  */
364
549
  function pointToLineDistance(px, py, x0, y0, x1, y1) {
365
550
  const dx = x1 - x0;
@@ -370,7 +555,10 @@ function pointToLineDistance(px, py, x0, y0, x1, y1) {
370
555
  return Math.sqrt((px - x0) ** 2 + (py - y0) ** 2);
371
556
  }
372
557
 
373
- const t = Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / lengthSq));
558
+ const t = Math.max(
559
+ 0,
560
+ Math.min(1, ((px - x0) * dx + (py - y0) * dy) / lengthSq),
561
+ );
374
562
  const projX = x0 + t * dx;
375
563
  const projY = y0 + t * dy;
376
564
 
@@ -380,7 +568,14 @@ function pointToLineDistance(px, py, x0, y0, x1, y1) {
380
568
  /**
381
569
  * Compute maximum error between cubic Bezier and a line segment.
382
570
  * Uses Newton's method to find closest points + midpoint checks to catch bulges.
383
- *
571
+ * @param {number} p0x - Bezier start point X
572
+ * @param {number} p0y - Bezier start point Y
573
+ * @param {number} cp1x - First control point X
574
+ * @param {number} cp1y - First control point Y
575
+ * @param {number} cp2x - Second control point X
576
+ * @param {number} cp2y - Second control point Y
577
+ * @param {number} p3x - Bezier end point X
578
+ * @param {number} p3y - Bezier end point Y
384
579
  * @returns {number} Maximum distance from any curve point to the line
385
580
  */
386
581
  function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
@@ -397,7 +592,17 @@ function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
397
592
  // Check midpoints between samples to catch bulges
398
593
  for (let i = 0; i < samples.length - 1; i++) {
399
594
  const tMid = (samples[i] + samples[i + 1]) / 2;
400
- const pt = cubicBezierPoint(tMid, p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y);
595
+ const pt = cubicBezierPoint(
596
+ tMid,
597
+ p0x,
598
+ p0y,
599
+ cp1x,
600
+ cp1y,
601
+ cp2x,
602
+ cp2y,
603
+ p3x,
604
+ p3y,
605
+ );
401
606
  const dist = pointToLineDistance(pt.x, pt.y, p0x, p0y, p3x, p3y);
402
607
  if (dist > maxErr) maxErr = dist;
403
608
  }
@@ -420,6 +625,16 @@ function maxErrorCurveToLine(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y) {
420
625
  /**
421
626
  * Check if a cubic bezier is effectively a straight line.
422
627
  * Uses comprehensive sampling + midpoint checks to find actual max deviation.
628
+ * @param {number} x0 - Bezier start point X
629
+ * @param {number} y0 - Bezier start point Y
630
+ * @param {number} cp1x - First control point X
631
+ * @param {number} cp1y - First control point Y
632
+ * @param {number} cp2x - Second control point X
633
+ * @param {number} cp2y - Second control point Y
634
+ * @param {number} x3 - Bezier end point X
635
+ * @param {number} y3 - Bezier end point Y
636
+ * @param {number} tolerance - Maximum deviation to consider straight
637
+ * @returns {boolean} True if curve is effectively straight
423
638
  */
424
639
  function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance) {
425
640
  const maxError = maxErrorCurveToLine(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3);
@@ -438,15 +653,17 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
438
653
  const commands = parsePath(d);
439
654
  if (commands.length === 0) return d;
440
655
 
441
- let cx = 0, cy = 0;
442
- let startX = 0, startY = 0;
656
+ let cx = 0,
657
+ cy = 0;
658
+ let startX = 0,
659
+ startY = 0;
443
660
  const result = [];
444
661
 
445
662
  for (const cmd of commands) {
446
663
  let newCmd = cmd;
447
664
 
448
- if (cmd.command === 'C' || cmd.command === 'c') {
449
- const isAbs = cmd.command === 'C';
665
+ if (cmd.command === "C" || cmd.command === "c") {
666
+ const isAbs = cmd.command === "C";
450
667
  const cp1x = isAbs ? cmd.args[0] : cx + cmd.args[0];
451
668
  const cp1y = isAbs ? cmd.args[1] : cy + cmd.args[1];
452
669
  const cp2x = isAbs ? cmd.args[2] : cx + cmd.args[2];
@@ -454,10 +671,12 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
454
671
  const endX = isAbs ? cmd.args[4] : cx + cmd.args[4];
455
672
  const endY = isAbs ? cmd.args[5] : cy + cmd.args[5];
456
673
 
457
- if (isCurveStraight(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance)) {
674
+ if (
675
+ isCurveStraight(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance)
676
+ ) {
458
677
  newCmd = isAbs
459
- ? { command: 'L', args: [endX, endY] }
460
- : { command: 'l', args: [endX - cx, endY - cy] };
678
+ ? { command: "L", args: [endX, endY] }
679
+ : { command: "l", args: [endX - cx, endY - cy] };
461
680
  }
462
681
  }
463
682
 
@@ -466,14 +685,40 @@ export function straightCurves(d, tolerance = 0.5, precision = 3) {
466
685
  // Update position
467
686
  const abs = toAbsolute(newCmd, cx, cy);
468
687
  switch (abs.command) {
469
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
470
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
471
- case 'H': cx = abs.args[0]; break;
472
- case 'V': cy = abs.args[0]; break;
473
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
474
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
475
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
476
- case 'Z': cx = startX; cy = startY; break;
688
+ case "M":
689
+ cx = abs.args[0];
690
+ cy = abs.args[1];
691
+ startX = cx;
692
+ startY = cy;
693
+ break;
694
+ case "L":
695
+ case "T":
696
+ cx = abs.args[0];
697
+ cy = abs.args[1];
698
+ break;
699
+ case "H":
700
+ cx = abs.args[0];
701
+ break;
702
+ case "V":
703
+ cy = abs.args[0];
704
+ break;
705
+ case "C":
706
+ cx = abs.args[4];
707
+ cy = abs.args[5];
708
+ break;
709
+ case "S":
710
+ case "Q":
711
+ cx = abs.args[2];
712
+ cy = abs.args[3];
713
+ break;
714
+ case "A":
715
+ cx = abs.args[5];
716
+ cy = abs.args[6];
717
+ break;
718
+ case "Z":
719
+ cx = startX;
720
+ cy = startY;
721
+ break;
477
722
  }
478
723
  }
479
724
 
@@ -518,9 +763,9 @@ export function floatPrecision(d, precision = 3) {
518
763
  if (commands.length === 0) return d;
519
764
 
520
765
  const factor = Math.pow(10, precision);
521
- const rounded = commands.map(cmd => ({
766
+ const rounded = commands.map((cmd) => ({
522
767
  command: cmd.command,
523
- args: cmd.args.map(n => Math.round(n * factor) / factor)
768
+ args: cmd.args.map((n) => Math.round(n * factor) / factor),
524
769
  }));
525
770
 
526
771
  return serializePath(rounded, precision);
@@ -543,8 +788,10 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
543
788
  const commands = parsePath(d);
544
789
  if (commands.length === 0) return d;
545
790
 
546
- let cx = 0, cy = 0;
547
- let startX = 0, startY = 0;
791
+ let cx = 0,
792
+ cy = 0;
793
+ let startX = 0,
794
+ startY = 0;
548
795
  const result = [];
549
796
 
550
797
  for (const cmd of commands) {
@@ -553,30 +800,35 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
553
800
 
554
801
  // Check for zero-length commands
555
802
  switch (abs.command) {
556
- case 'L': case 'T':
557
- if (Math.abs(abs.args[0] - cx) < tolerance &&
558
- Math.abs(abs.args[1] - cy) < tolerance) {
803
+ case "L":
804
+ case "T":
805
+ if (
806
+ Math.abs(abs.args[0] - cx) < tolerance &&
807
+ Math.abs(abs.args[1] - cy) < tolerance
808
+ ) {
559
809
  keep = false;
560
810
  }
561
811
  break;
562
- case 'H':
812
+ case "H":
563
813
  if (Math.abs(abs.args[0] - cx) < tolerance) {
564
814
  keep = false;
565
815
  }
566
816
  break;
567
- case 'V':
817
+ case "V":
568
818
  if (Math.abs(abs.args[0] - cy) < tolerance) {
569
819
  keep = false;
570
820
  }
571
821
  break;
572
- case 'C':
822
+ case "C":
573
823
  // Zero-length curve where all points are the same
574
- if (Math.abs(abs.args[4] - cx) < tolerance &&
575
- Math.abs(abs.args[5] - cy) < tolerance &&
576
- Math.abs(abs.args[0] - cx) < tolerance &&
577
- Math.abs(abs.args[1] - cy) < tolerance &&
578
- Math.abs(abs.args[2] - cx) < tolerance &&
579
- Math.abs(abs.args[3] - cy) < tolerance) {
824
+ if (
825
+ Math.abs(abs.args[4] - cx) < tolerance &&
826
+ Math.abs(abs.args[5] - cy) < tolerance &&
827
+ Math.abs(abs.args[0] - cx) < tolerance &&
828
+ Math.abs(abs.args[1] - cy) < tolerance &&
829
+ Math.abs(abs.args[2] - cx) < tolerance &&
830
+ Math.abs(abs.args[3] - cy) < tolerance
831
+ ) {
580
832
  keep = false;
581
833
  }
582
834
  break;
@@ -587,14 +839,40 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
587
839
 
588
840
  // Update position
589
841
  switch (abs.command) {
590
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
591
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
592
- case 'H': cx = abs.args[0]; break;
593
- case 'V': cy = abs.args[0]; break;
594
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
595
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
596
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
597
- case 'Z': cx = startX; cy = startY; break;
842
+ case "M":
843
+ cx = abs.args[0];
844
+ cy = abs.args[1];
845
+ startX = cx;
846
+ startY = cy;
847
+ break;
848
+ case "L":
849
+ case "T":
850
+ cx = abs.args[0];
851
+ cy = abs.args[1];
852
+ break;
853
+ case "H":
854
+ cx = abs.args[0];
855
+ break;
856
+ case "V":
857
+ cy = abs.args[0];
858
+ break;
859
+ case "C":
860
+ cx = abs.args[4];
861
+ cy = abs.args[5];
862
+ break;
863
+ case "S":
864
+ case "Q":
865
+ cx = abs.args[2];
866
+ cy = abs.args[3];
867
+ break;
868
+ case "A":
869
+ cx = abs.args[5];
870
+ cy = abs.args[6];
871
+ break;
872
+ case "Z":
873
+ cx = startX;
874
+ cy = startY;
875
+ break;
598
876
  }
599
877
  }
600
878
  }
@@ -610,6 +888,14 @@ export function removeUselessCommands(d, tolerance = 1e-6, precision = 3) {
610
888
  /**
611
889
  * Evaluate a quadratic Bezier curve at parameter t.
612
890
  * B(t) = (1-t)^2*P0 + 2*(1-t)*t*P1 + t^2*P2
891
+ * @param {number} t - Parameter value (0 to 1)
892
+ * @param {number} p0x - Start point X coordinate
893
+ * @param {number} p0y - Start point Y coordinate
894
+ * @param {number} p1x - Control point X coordinate
895
+ * @param {number} p1y - Control point Y coordinate
896
+ * @param {number} p2x - End point X coordinate
897
+ * @param {number} p2y - End point Y coordinate
898
+ * @returns {{x: number, y: number}} Point on curve
613
899
  */
614
900
  function quadraticBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y) {
615
901
  const mt = 1 - t;
@@ -617,26 +903,58 @@ function quadraticBezierPoint(t, p0x, p0y, p1x, p1y, p2x, p2y) {
617
903
  const t2 = t * t;
618
904
  return {
619
905
  x: mt2 * p0x + 2 * mt * t * p1x + t2 * p2x,
620
- y: mt2 * p0y + 2 * mt * t * p1y + t2 * p2y
906
+ y: mt2 * p0y + 2 * mt * t * p1y + t2 * p2y,
621
907
  };
622
908
  }
623
909
 
624
910
  /**
625
911
  * Compute maximum error between cubic Bezier and quadratic Bezier.
626
912
  * Samples both curves and checks midpoints to find actual max deviation.
627
- *
913
+ * @param {number} p0x - Bezier start point X
914
+ * @param {number} p0y - Bezier start point Y
915
+ * @param {number} cp1x - Cubic first control point X
916
+ * @param {number} cp1y - Cubic first control point Y
917
+ * @param {number} cp2x - Cubic second control point X
918
+ * @param {number} cp2y - Cubic second control point Y
919
+ * @param {number} p3x - Bezier end point X
920
+ * @param {number} p3y - Bezier end point Y
921
+ * @param {number} qx - Quadratic control point X
922
+ * @param {number} qy - Quadratic control point Y
628
923
  * @returns {number} Maximum distance between corresponding points on both curves
629
924
  */
630
- function maxErrorCubicToQuadratic(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y, qx, qy) {
925
+ function maxErrorCubicToQuadratic(
926
+ p0x,
927
+ p0y,
928
+ cp1x,
929
+ cp1y,
930
+ cp2x,
931
+ cp2y,
932
+ p3x,
933
+ p3y,
934
+ qx,
935
+ qy,
936
+ ) {
631
937
  let maxErr = 0;
632
938
 
633
939
  // Dense sampling including midpoints
634
- const samples = [0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5,
635
- 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95];
940
+ const samples = [
941
+ 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7,
942
+ 0.75, 0.8, 0.85, 0.9, 0.95,
943
+ ];
636
944
 
637
945
  for (const t of samples) {
638
946
  // Point on original cubic
639
- const cubic = cubicBezierPoint(t, p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y);
947
+ const cubic = cubicBezierPoint(
948
+ t,
949
+ p0x,
950
+ p0y,
951
+ cp1x,
952
+ cp1y,
953
+ cp2x,
954
+ cp2y,
955
+ p3x,
956
+ p3y,
957
+ );
640
958
  // Point on proposed quadratic
641
959
  const quad = quadraticBezierPoint(t, p0x, p0y, qx, qy, p3x, p3y);
642
960
 
@@ -650,10 +968,28 @@ function maxErrorCubicToQuadratic(p0x, p0y, cp1x, cp1y, cp2x, cp2y, p3x, p3y, qx
650
968
  /**
651
969
  * Check if a cubic bezier can be approximated by a quadratic.
652
970
  * VERIFIED by comparing actual curve points with comprehensive sampling.
653
- *
971
+ * @param {number} x0 - Bezier start point X
972
+ * @param {number} y0 - Bezier start point Y
973
+ * @param {number} cp1x - Cubic first control point X
974
+ * @param {number} cp1y - Cubic first control point Y
975
+ * @param {number} cp2x - Cubic second control point X
976
+ * @param {number} cp2y - Cubic second control point Y
977
+ * @param {number} x3 - Bezier end point X
978
+ * @param {number} y3 - Bezier end point Y
979
+ * @param {number} tolerance - Maximum deviation for conversion
654
980
  * @returns {{cpx: number, cpy: number} | null} Quadratic control point or null
655
981
  */
656
- function cubicToQuadraticControlPoint(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance) {
982
+ function cubicToQuadraticControlPoint(
983
+ x0,
984
+ y0,
985
+ cp1x,
986
+ cp1y,
987
+ cp2x,
988
+ cp2y,
989
+ x3,
990
+ y3,
991
+ tolerance,
992
+ ) {
657
993
  // Calculate the best-fit quadratic control point
658
994
  // For a cubic to be exactly representable as quadratic:
659
995
  // Q = (3*(P1 + P2) - P0 - P3) / 4
@@ -661,7 +997,18 @@ function cubicToQuadraticControlPoint(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, to
661
997
  const qy = (3 * (cp1y + cp2y) - y0 - y3) / 4;
662
998
 
663
999
  // VERIFY by computing actual max error between the curves
664
- const maxError = maxErrorCubicToQuadratic(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, qx, qy);
1000
+ const maxError = maxErrorCubicToQuadratic(
1001
+ x0,
1002
+ y0,
1003
+ cp1x,
1004
+ cp1y,
1005
+ cp2x,
1006
+ cp2y,
1007
+ x3,
1008
+ y3,
1009
+ qx,
1010
+ qy,
1011
+ );
665
1012
 
666
1013
  if (maxError <= tolerance) {
667
1014
  return { cpx: qx, cpy: qy };
@@ -682,15 +1029,17 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
682
1029
  const commands = parsePath(d);
683
1030
  if (commands.length === 0) return d;
684
1031
 
685
- let cx = 0, cy = 0;
686
- let startX = 0, startY = 0;
1032
+ let cx = 0,
1033
+ cy = 0;
1034
+ let startX = 0,
1035
+ startY = 0;
687
1036
  const result = [];
688
1037
 
689
1038
  for (const cmd of commands) {
690
1039
  let newCmd = cmd;
691
1040
 
692
- if (cmd.command === 'C' || cmd.command === 'c') {
693
- const isAbs = cmd.command === 'C';
1041
+ if (cmd.command === "C" || cmd.command === "c") {
1042
+ const isAbs = cmd.command === "C";
694
1043
  const cp1x = isAbs ? cmd.args[0] : cx + cmd.args[0];
695
1044
  const cp1y = isAbs ? cmd.args[1] : cy + cmd.args[1];
696
1045
  const cp2x = isAbs ? cmd.args[2] : cx + cmd.args[2];
@@ -698,12 +1047,25 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
698
1047
  const endX = isAbs ? cmd.args[4] : cx + cmd.args[4];
699
1048
  const endY = isAbs ? cmd.args[5] : cy + cmd.args[5];
700
1049
 
701
- const quadCP = cubicToQuadraticControlPoint(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance);
1050
+ const quadCP = cubicToQuadraticControlPoint(
1051
+ cx,
1052
+ cy,
1053
+ cp1x,
1054
+ cp1y,
1055
+ cp2x,
1056
+ cp2y,
1057
+ endX,
1058
+ endY,
1059
+ tolerance,
1060
+ );
702
1061
 
703
1062
  if (quadCP) {
704
1063
  newCmd = isAbs
705
- ? { command: 'Q', args: [quadCP.cpx, quadCP.cpy, endX, endY] }
706
- : { command: 'q', args: [quadCP.cpx - cx, quadCP.cpy - cy, endX - cx, endY - cy] };
1064
+ ? { command: "Q", args: [quadCP.cpx, quadCP.cpy, endX, endY] }
1065
+ : {
1066
+ command: "q",
1067
+ args: [quadCP.cpx - cx, quadCP.cpy - cy, endX - cx, endY - cy],
1068
+ };
707
1069
  }
708
1070
  }
709
1071
 
@@ -712,14 +1074,40 @@ export function convertCubicToQuadratic(d, tolerance = 0.5, precision = 3) {
712
1074
  // Update position
713
1075
  const abs = toAbsolute(newCmd, cx, cy);
714
1076
  switch (abs.command) {
715
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
716
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
717
- case 'H': cx = abs.args[0]; break;
718
- case 'V': cy = abs.args[0]; break;
719
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
720
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
721
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
722
- case 'Z': cx = startX; cy = startY; break;
1077
+ case "M":
1078
+ cx = abs.args[0];
1079
+ cy = abs.args[1];
1080
+ startX = cx;
1081
+ startY = cy;
1082
+ break;
1083
+ case "L":
1084
+ case "T":
1085
+ cx = abs.args[0];
1086
+ cy = abs.args[1];
1087
+ break;
1088
+ case "H":
1089
+ cx = abs.args[0];
1090
+ break;
1091
+ case "V":
1092
+ cy = abs.args[0];
1093
+ break;
1094
+ case "C":
1095
+ cx = abs.args[4];
1096
+ cy = abs.args[5];
1097
+ break;
1098
+ case "S":
1099
+ case "Q":
1100
+ cx = abs.args[2];
1101
+ cy = abs.args[3];
1102
+ break;
1103
+ case "A":
1104
+ cx = abs.args[5];
1105
+ cy = abs.args[6];
1106
+ break;
1107
+ case "Z":
1108
+ cx = startX;
1109
+ cy = startY;
1110
+ break;
723
1111
  }
724
1112
  }
725
1113
 
@@ -743,38 +1131,43 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
743
1131
  const commands = parsePath(d);
744
1132
  if (commands.length === 0) return d;
745
1133
 
746
- let cx = 0, cy = 0;
747
- let startX = 0, startY = 0;
748
- let lastQcpX = null, lastQcpY = null;
1134
+ let cx = 0,
1135
+ cy = 0;
1136
+ let startX = 0,
1137
+ startY = 0;
1138
+ let lastQcpX = null,
1139
+ lastQcpY = null;
749
1140
  const result = [];
750
1141
 
751
1142
  for (const cmd of commands) {
752
1143
  let newCmd = cmd;
753
1144
  const abs = toAbsolute(cmd, cx, cy);
754
1145
 
755
- if (abs.command === 'Q' && lastQcpX !== null) {
1146
+ if (abs.command === "Q" && lastQcpX !== null) {
756
1147
  // Calculate reflected control point
757
1148
  const reflectedCpX = 2 * cx - lastQcpX;
758
1149
  const reflectedCpY = 2 * cy - lastQcpY;
759
1150
 
760
1151
  // Check if current control point matches reflection
761
- if (Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
762
- Math.abs(abs.args[1] - reflectedCpY) < tolerance) {
1152
+ if (
1153
+ Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
1154
+ Math.abs(abs.args[1] - reflectedCpY) < tolerance
1155
+ ) {
763
1156
  // Can use T command
764
- const isAbs = cmd.command === 'Q';
1157
+ const isAbs = cmd.command === "Q";
765
1158
  newCmd = isAbs
766
- ? { command: 'T', args: [abs.args[2], abs.args[3]] }
767
- : { command: 't', args: [abs.args[2] - cx, abs.args[3] - cy] };
1159
+ ? { command: "T", args: [abs.args[2], abs.args[3]] }
1160
+ : { command: "t", args: [abs.args[2] - cx, abs.args[3] - cy] };
768
1161
  }
769
1162
  }
770
1163
 
771
1164
  result.push(newCmd);
772
1165
 
773
1166
  // Track control points for reflection
774
- if (abs.command === 'Q') {
1167
+ if (abs.command === "Q") {
775
1168
  lastQcpX = abs.args[0];
776
1169
  lastQcpY = abs.args[1];
777
- } else if (abs.command === 'T') {
1170
+ } else if (abs.command === "T") {
778
1171
  // T uses reflected control point
779
1172
  lastQcpX = 2 * cx - lastQcpX;
780
1173
  lastQcpY = 2 * cy - lastQcpY;
@@ -785,14 +1178,40 @@ export function convertQuadraticToSmooth(d, tolerance = 1e-6, precision = 3) {
785
1178
 
786
1179
  // Update position
787
1180
  switch (abs.command) {
788
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
789
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
790
- case 'H': cx = abs.args[0]; break;
791
- case 'V': cy = abs.args[0]; break;
792
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
793
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
794
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
795
- case 'Z': cx = startX; cy = startY; break;
1181
+ case "M":
1182
+ cx = abs.args[0];
1183
+ cy = abs.args[1];
1184
+ startX = cx;
1185
+ startY = cy;
1186
+ break;
1187
+ case "L":
1188
+ case "T":
1189
+ cx = abs.args[0];
1190
+ cy = abs.args[1];
1191
+ break;
1192
+ case "H":
1193
+ cx = abs.args[0];
1194
+ break;
1195
+ case "V":
1196
+ cy = abs.args[0];
1197
+ break;
1198
+ case "C":
1199
+ cx = abs.args[4];
1200
+ cy = abs.args[5];
1201
+ break;
1202
+ case "S":
1203
+ case "Q":
1204
+ cx = abs.args[2];
1205
+ cy = abs.args[3];
1206
+ break;
1207
+ case "A":
1208
+ cx = abs.args[5];
1209
+ cy = abs.args[6];
1210
+ break;
1211
+ case "Z":
1212
+ cx = startX;
1213
+ cy = startY;
1214
+ break;
796
1215
  }
797
1216
  }
798
1217
 
@@ -816,38 +1235,54 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
816
1235
  const commands = parsePath(d);
817
1236
  if (commands.length === 0) return d;
818
1237
 
819
- let cx = 0, cy = 0;
820
- let startX = 0, startY = 0;
821
- let lastCcp2X = null, lastCcp2Y = null;
1238
+ let cx = 0,
1239
+ cy = 0;
1240
+ let startX = 0,
1241
+ startY = 0;
1242
+ let lastCcp2X = null,
1243
+ lastCcp2Y = null;
822
1244
  const result = [];
823
1245
 
824
1246
  for (const cmd of commands) {
825
1247
  let newCmd = cmd;
826
1248
  const abs = toAbsolute(cmd, cx, cy);
827
1249
 
828
- if (abs.command === 'C' && lastCcp2X !== null) {
1250
+ if (abs.command === "C" && lastCcp2X !== null) {
829
1251
  // Calculate reflected control point
830
1252
  const reflectedCpX = 2 * cx - lastCcp2X;
831
1253
  const reflectedCpY = 2 * cy - lastCcp2Y;
832
1254
 
833
1255
  // Check if first control point matches reflection
834
- if (Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
835
- Math.abs(abs.args[1] - reflectedCpY) < tolerance) {
1256
+ if (
1257
+ Math.abs(abs.args[0] - reflectedCpX) < tolerance &&
1258
+ Math.abs(abs.args[1] - reflectedCpY) < tolerance
1259
+ ) {
836
1260
  // Can use S command
837
- const isAbs = cmd.command === 'C';
1261
+ const isAbs = cmd.command === "C";
838
1262
  newCmd = isAbs
839
- ? { command: 'S', args: [abs.args[2], abs.args[3], abs.args[4], abs.args[5]] }
840
- : { command: 's', args: [abs.args[2] - cx, abs.args[3] - cy, abs.args[4] - cx, abs.args[5] - cy] };
1263
+ ? {
1264
+ command: "S",
1265
+ args: [abs.args[2], abs.args[3], abs.args[4], abs.args[5]],
1266
+ }
1267
+ : {
1268
+ command: "s",
1269
+ args: [
1270
+ abs.args[2] - cx,
1271
+ abs.args[3] - cy,
1272
+ abs.args[4] - cx,
1273
+ abs.args[5] - cy,
1274
+ ],
1275
+ };
841
1276
  }
842
1277
  }
843
1278
 
844
1279
  result.push(newCmd);
845
1280
 
846
1281
  // Track control points for reflection
847
- if (abs.command === 'C') {
1282
+ if (abs.command === "C") {
848
1283
  lastCcp2X = abs.args[2];
849
1284
  lastCcp2Y = abs.args[3];
850
- } else if (abs.command === 'S') {
1285
+ } else if (abs.command === "S") {
851
1286
  // S uses its control point as the second cubic control point
852
1287
  lastCcp2X = abs.args[0];
853
1288
  lastCcp2Y = abs.args[1];
@@ -858,14 +1293,40 @@ export function convertCubicToSmooth(d, tolerance = 1e-6, precision = 3) {
858
1293
 
859
1294
  // Update position
860
1295
  switch (abs.command) {
861
- case 'M': cx = abs.args[0]; cy = abs.args[1]; startX = cx; startY = cy; break;
862
- case 'L': case 'T': cx = abs.args[0]; cy = abs.args[1]; break;
863
- case 'H': cx = abs.args[0]; break;
864
- case 'V': cy = abs.args[0]; break;
865
- case 'C': cx = abs.args[4]; cy = abs.args[5]; break;
866
- case 'S': case 'Q': cx = abs.args[2]; cy = abs.args[3]; break;
867
- case 'A': cx = abs.args[5]; cy = abs.args[6]; break;
868
- case 'Z': cx = startX; cy = startY; break;
1296
+ case "M":
1297
+ cx = abs.args[0];
1298
+ cy = abs.args[1];
1299
+ startX = cx;
1300
+ startY = cy;
1301
+ break;
1302
+ case "L":
1303
+ case "T":
1304
+ cx = abs.args[0];
1305
+ cy = abs.args[1];
1306
+ break;
1307
+ case "H":
1308
+ cx = abs.args[0];
1309
+ break;
1310
+ case "V":
1311
+ cy = abs.args[0];
1312
+ break;
1313
+ case "C":
1314
+ cx = abs.args[4];
1315
+ cy = abs.args[5];
1316
+ break;
1317
+ case "S":
1318
+ case "Q":
1319
+ cx = abs.args[2];
1320
+ cy = abs.args[3];
1321
+ break;
1322
+ case "A":
1323
+ cx = abs.args[5];
1324
+ cy = abs.args[6];
1325
+ break;
1326
+ case "Z":
1327
+ cx = startX;
1328
+ cy = startY;
1329
+ break;
869
1330
  }
870
1331
  }
871
1332
 
@@ -889,8 +1350,8 @@ export function arcShorthands(d, precision = 3) {
889
1350
  const commands = parsePath(d);
890
1351
  if (commands.length === 0) return d;
891
1352
 
892
- const result = commands.map(cmd => {
893
- if (cmd.command === 'A' || cmd.command === 'a') {
1353
+ const result = commands.map((cmd) => {
1354
+ if (cmd.command === "A" || cmd.command === "a") {
894
1355
  const args = [...cmd.args];
895
1356
  // Normalize rotation angle to 0-360
896
1357
  args[2] = ((args[2] % 360) + 360) % 360;
@@ -924,5 +1385,5 @@ export default {
924
1385
  convertCubicToQuadratic,
925
1386
  convertQuadraticToSmooth,
926
1387
  convertCubicToSmooth,
927
- arcShorthands
1388
+ arcShorthands,
928
1389
  };