svg-path-simplify 0.0.7 → 0.0.9

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 (38) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +1250 -562
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +4756 -4068
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +1250 -562
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +89 -29
  9. package/package.json +5 -3
  10. package/src/detect_input.js +17 -10
  11. package/src/dom-polyfill.js +29 -0
  12. package/src/dom-polyfill_back.js +22 -0
  13. package/src/index.js +10 -1
  14. package/src/pathData_simplify_cubic.js +114 -143
  15. package/src/pathData_simplify_cubic_extrapolate.js +64 -35
  16. package/src/pathSimplify-main.js +113 -165
  17. package/src/svgii/geometry.js +8 -155
  18. package/src/svgii/geometry_flatness.js +94 -0
  19. package/src/svgii/pathData_analyze.js +15 -596
  20. package/src/svgii/pathData_convert.js +26 -17
  21. package/src/svgii/pathData_interpolate.js +65 -0
  22. package/src/svgii/pathData_parse.js +25 -9
  23. package/src/svgii/pathData_parse_els.js +245 -0
  24. package/src/svgii/pathData_remove_collinear.js +33 -28
  25. package/src/svgii/pathData_remove_orphaned.js +21 -0
  26. package/src/svgii/pathData_remove_zerolength.js +17 -3
  27. package/src/svgii/pathData_reorder.js +9 -3
  28. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  29. package/src/svgii/pathData_simplify_refineExtremes.js +208 -0
  30. package/src/svgii/pathData_split.js +43 -15
  31. package/src/svgii/pathData_stringify.js +3 -12
  32. package/src/svgii/rounding.js +35 -27
  33. package/src/svgii/svg_cleanup.js +4 -1
  34. package/testSVG.js +39 -0
  35. package/src/pathData_simplify_cubic_arr.js +0 -50
  36. package/src/svgii/simplify.js +0 -248
  37. package/src/svgii/simplify_bezier.js +0 -470
  38. package/src/svgii/simplify_linetos.js +0 -93
@@ -1,5 +1,44 @@
1
1
  'use strict';
2
2
 
3
+ function renderPoint(
4
+ svg,
5
+ coords,
6
+ fill = "red",
7
+ r = "1%",
8
+ opacity = "1",
9
+ title = '',
10
+ render = true,
11
+ id = "",
12
+ className = ""
13
+ ) {
14
+ if (Array.isArray(coords)) {
15
+ coords = {
16
+ x: coords[0],
17
+ y: coords[1]
18
+ };
19
+ }
20
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
21
+ <title>${title}</title></circle>`;
22
+
23
+ if (render) {
24
+ svg.insertAdjacentHTML("beforeend", marker);
25
+ } else {
26
+ return marker;
27
+ }
28
+ }
29
+
30
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
31
+
32
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
33
+
34
+ if (render) {
35
+ svg.insertAdjacentHTML("beforeend", path);
36
+ } else {
37
+ return path;
38
+ }
39
+
40
+ }
41
+
3
42
  function detectInputType(input) {
4
43
  let type = 'string';
5
44
  /*
@@ -10,23 +49,29 @@ function detectInputType(input) {
10
49
  if (input instanceof ArrayBuffer) return "buffer";
11
50
  if (input instanceof Blob) return "blob";
12
51
  */
13
- if (Array.isArray(input)) return "array";
52
+ if (Array.isArray(input)) {
53
+ if (input[0]?.type && input[0]?.values
54
+ ) {
55
+ return "pathData";
56
+ }
57
+
58
+ return "array";
59
+ }
14
60
 
15
61
  if (typeof input === "string") {
16
62
  input = input.trim();
17
63
  let isSVG = input.includes('<svg') && input.includes('</svg');
18
64
  let isPathData = input.startsWith('M') || input.startsWith('m');
19
- let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
65
+ let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length));
20
66
 
21
-
22
- if(isSVG) {
23
- type='svgMarkup';
67
+ if (isSVG) {
68
+ type = 'svgMarkup';
24
69
  }
25
- else if(isPathData) {
26
- type='pathDataString';
70
+ else if (isPathData) {
71
+ type = 'pathDataString';
27
72
  }
28
- else if(isPolyString) {
29
- type='polyString';
73
+ else if (isPolyString) {
74
+ type = 'polyString';
30
75
  }
31
76
 
32
77
  else {
@@ -67,19 +112,24 @@ function getAngle(p1, p2, normalize = false) {
67
112
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
68
113
  */
69
114
 
70
- function checkLineIntersection(p1, p2, p3, p4, exact = true) {
115
+ function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
71
116
  // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
72
117
  let denominator, a, b, numerator1, numerator2;
73
118
  let intersectionPoint = {};
74
119
 
120
+ if(!p1 || !p2 || !p3 || !p4){
121
+ if(debug) console.warn('points missing');
122
+ return false
123
+ }
124
+
75
125
  try {
76
126
  denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
77
127
  if (denominator == 0) {
78
128
  return false;
79
129
  }
80
-
81
130
  } catch {
82
- console.log('!catch', p1, p2, 'p3:', p3, p4);
131
+ if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
132
+ return false
83
133
  }
84
134
 
85
135
  a = p1.y - p3.y;
@@ -96,8 +146,6 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
96
146
  y: p1.y + (a * (p2.y - p1.y))
97
147
  };
98
148
 
99
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
100
-
101
149
  let intersection = false;
102
150
  // if line1 is a segment and line2 is infinite, they intersect if:
103
151
  if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
@@ -497,6 +545,110 @@ function getBezierExtremeT(pts) {
497
545
  return tArr;
498
546
  }
499
547
 
548
+ /**
549
+ * based on Nikos M.'s answer
550
+ * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
551
+ * https://stackoverflow.com/questions/87734/#75031511
552
+ * See also: https://github.com/foo123/Geometrize
553
+ */
554
+ function getArcExtemes(p0, values) {
555
+ // compute point on ellipse from angle around ellipse (theta)
556
+ const arc = (theta, cx, cy, rx, ry, alpha) => {
557
+ // theta is angle in radians around arc
558
+ // alpha is angle of rotation of ellipse in radians
559
+ var cos = Math.cos(alpha),
560
+ sin = Math.sin(alpha),
561
+ x = rx * Math.cos(theta),
562
+ y = ry * Math.sin(theta);
563
+
564
+ return {
565
+ x: cx + cos * x - sin * y,
566
+ y: cy + sin * x + cos * y
567
+ };
568
+ };
569
+
570
+ let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
571
+ let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
572
+
573
+ // arc rotation
574
+ let deg = values[2];
575
+
576
+ // final on path point
577
+ let p = { x: values[5], y: values[6] };
578
+
579
+ // collect extreme points – add end point
580
+ let extremes = [p];
581
+
582
+ // rotation to radians
583
+ let alpha = deg * Math.PI / 180;
584
+ let tan = Math.tan(alpha),
585
+ p1, p2, p3, p4, theta;
586
+
587
+ /**
588
+ * find min/max from zeroes of directional derivative along x and y
589
+ * along x axis
590
+ */
591
+ theta = Math.atan2(-ry * tan, rx);
592
+
593
+ let angle1 = theta;
594
+ let angle2 = theta + Math.PI;
595
+ let angle3 = Math.atan2(ry, rx * tan);
596
+ let angle4 = angle3 + Math.PI;
597
+
598
+ // inner bounding box
599
+ let xArr = [p0.x, p.x];
600
+ let yArr = [p0.y, p.y];
601
+ let xMin = Math.min(...xArr);
602
+ let xMax = Math.max(...xArr);
603
+ let yMin = Math.min(...yArr);
604
+ let yMax = Math.max(...yArr);
605
+
606
+ // on path point close after start
607
+ let angleAfterStart = endAngle - deltaAngle * 0.001;
608
+ let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
609
+
610
+ // on path point close before end
611
+ let angleBeforeEnd = endAngle - deltaAngle * 0.999;
612
+ let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
613
+
614
+ /**
615
+ * expected extremes
616
+ * if leaving inner bounding box
617
+ * (between segment start and end point)
618
+ * otherwise exclude elliptic extreme points
619
+ */
620
+
621
+ // right
622
+ if (pP2.x > xMax || pP3.x > xMax) {
623
+ // get point for this theta
624
+ p1 = arc(angle1, cx, cy, rx, ry, alpha);
625
+ extremes.push(p1);
626
+ }
627
+
628
+ // left
629
+ if (pP2.x < xMin || pP3.x < xMin) {
630
+ // get anti-symmetric point
631
+ p2 = arc(angle2, cx, cy, rx, ry, alpha);
632
+ extremes.push(p2);
633
+ }
634
+
635
+ // top
636
+ if (pP2.y < yMin || pP3.y < yMin) {
637
+ // get anti-symmetric point
638
+ p4 = arc(angle4, cx, cy, rx, ry, alpha);
639
+ extremes.push(p4);
640
+ }
641
+
642
+ // bottom
643
+ if (pP2.y > yMax || pP3.y > yMax) {
644
+ // get point for this theta
645
+ p3 = arc(angle3, cx, cy, rx, ry, alpha);
646
+ extremes.push(p3);
647
+ }
648
+
649
+ return extremes;
650
+ }
651
+
500
652
  // cubic bezier.
501
653
  function cubicBezierExtremeT(p0, cp1, cp2, p) {
502
654
  let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
@@ -605,90 +757,6 @@ function quadraticBezierExtremeT(p0, cp1, p) {
605
757
  return extemeT
606
758
  }
607
759
 
608
- function commandIsFlat(points, tolerance = 0.025) {
609
-
610
- let p0 = points[0];
611
- let p = points[points.length - 1];
612
-
613
- let xArr = points.map(pt => { return pt.x });
614
- let yArr = points.map(pt => { return pt.y });
615
-
616
- let xMin = Math.min(...xArr);
617
- let xMax = Math.max(...xArr);
618
- let yMin = Math.min(...yArr);
619
- let yMax = Math.max(...yArr);
620
- let w = xMax - xMin;
621
- let h = yMax - yMin;
622
-
623
- if (points.length < 3 || (w === 0 || h === 0)) {
624
- return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
625
- }
626
-
627
- let squareDist = getSquareDistance(p0, p);
628
- let squareDist1 = getSquareDistance(p0, points[0]);
629
- let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
630
- let squareDistAvg = (squareDist1 + squareDist2) / 2;
631
-
632
- tolerance = 0.5;
633
- let thresh = (w + h) * 0.5 * tolerance;
634
-
635
- let area = 0;
636
- for (let i = 0, l = points.length; i < l; i++) {
637
- let addX = points[i].x;
638
- let addY = points[i === points.length - 1 ? 0 : i + 1].y;
639
- let subX = points[i === points.length - 1 ? 0 : i + 1].x;
640
- let subY = points[i].y;
641
- area += addX * addY * 0.5 - subX * subY * 0.5;
642
- }
643
-
644
- area = +Math.abs(area).toFixed(9);
645
- let areaThresh = 1000;
646
-
647
- let ratio = area / (squareDistAvg);
648
-
649
- let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
650
-
651
- return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
652
- }
653
-
654
- function checkBezierFlatness(p0, cpts, p) {
655
-
656
- let isFlat = false;
657
-
658
- let isCubic = cpts.length===2;
659
-
660
- let cp1 = cpts[0];
661
- let cp2 = isCubic ? cpts[1] : cp1;
662
-
663
- if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
664
-
665
- let dx1 = cp1.x - p0.x;
666
- let dy1 = cp1.y - p0.y;
667
-
668
- let dx2 = p.x - cp2.x;
669
- let dy2 = p.y - cp2.y;
670
-
671
- let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
672
-
673
- if(!cross1) return true
674
-
675
- let dx0 = p.x - p0.x;
676
- let dy0 = p.y - p0.y;
677
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
678
-
679
- if(!cross0) return true
680
-
681
- let rat = (cross0/cross1);
682
-
683
- if (rat<1.1 ) {
684
-
685
- isFlat = true;
686
- }
687
-
688
- return isFlat;
689
-
690
- }
691
-
692
760
  /**
693
761
  * sloppy distance calculation
694
762
  * based on x/y differences
@@ -712,27 +780,23 @@ function getDistAv(pt1, pt2) {
712
780
  * split compound paths into
713
781
  * sub path data array
714
782
  */
715
- function splitSubpaths(pathData) {
716
783
 
784
+ function splitSubpaths(pathData) {
717
785
  let subPathArr = [];
786
+ let current = [pathData[0]];
787
+ let l = pathData.length;
718
788
 
719
-
720
- try{
721
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
789
+ for (let i = 1; i < l; i++) {
790
+ let com = pathData[i];
722
791
 
723
- }catch{
724
- console.log('catch', pathData);
792
+ if (com.type === 'M' || com.type === 'm') {
793
+ subPathArr.push(current);
794
+ current = [];
795
+ }
796
+ current.push(com);
725
797
  }
726
798
 
727
- let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
728
-
729
- // no compound path
730
- if (subPathIndices.length === 1) {
731
- return [pathData]
732
- }
733
- subPathIndices.forEach((index, i) => {
734
- subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
735
- });
799
+ if (current.length) subPathArr.push(current);
736
800
 
737
801
  return subPathArr;
738
802
  }
@@ -1156,6 +1220,73 @@ function getPathDataPoly(pathData) {
1156
1220
  return poly;
1157
1221
  }
1158
1222
 
1223
+ /**
1224
+ * get exact path BBox
1225
+ * calculating extremes for all command types
1226
+ */
1227
+
1228
+ function getPathDataBBox(pathData) {
1229
+
1230
+ // save extreme values
1231
+ let xMin = Infinity;
1232
+ let xMax = -Infinity;
1233
+ let yMin = Infinity;
1234
+ let yMax = -Infinity;
1235
+
1236
+ const setXYmaxMin = (pt) => {
1237
+ if (pt.x < xMin) {
1238
+ xMin = pt.x;
1239
+ }
1240
+ if (pt.x > xMax) {
1241
+ xMax = pt.x;
1242
+ }
1243
+ if (pt.y < yMin) {
1244
+ yMin = pt.y;
1245
+ }
1246
+ if (pt.y > yMax) {
1247
+ yMax = pt.y;
1248
+ }
1249
+ };
1250
+
1251
+ for (let i = 0; i < pathData.length; i++) {
1252
+ let com = pathData[i];
1253
+ let { type, values } = com;
1254
+ let valuesL = values.length;
1255
+ let comPrev = pathData[i - 1] ? pathData[i - 1] : pathData[i];
1256
+ let valuesPrev = comPrev.values;
1257
+ let valuesPrevL = valuesPrev.length;
1258
+
1259
+ if (valuesL) {
1260
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
1261
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1262
+ // add final on path point
1263
+ setXYmaxMin(p);
1264
+
1265
+ if (type === 'C' || type === 'Q') {
1266
+ let cp1 = { x: values[0], y: values[1] };
1267
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
1268
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
1269
+
1270
+ let bezierExtremesT = getBezierExtremeT(pts);
1271
+ bezierExtremesT.forEach(t => {
1272
+ let pt = pointAtT(pts, t);
1273
+ setXYmaxMin(pt);
1274
+ });
1275
+ }
1276
+
1277
+ else if (type === 'A') {
1278
+ let arcExtremes = getArcExtemes(p0, values);
1279
+ arcExtremes.forEach(pt => {
1280
+ setXYmaxMin(pt);
1281
+ });
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
1287
+ return bbox
1288
+ }
1289
+
1159
1290
  /**
1160
1291
  * get pathdata area
1161
1292
  */
@@ -1349,13 +1480,6 @@ function pathDataToD(pathData, optimize = 0) {
1349
1480
  let beautify = optimize > 1;
1350
1481
  let minify = beautify || optimize ? false : true;
1351
1482
 
1352
- // Convert first "M" to "m" if followed by "l" (when minified)
1353
- /*
1354
- if (pathData[1].type === "l" && minify) {
1355
- pathData[0].type = "m";
1356
- }
1357
- */
1358
-
1359
1483
  let d = '';
1360
1484
  let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1361
1485
  let separator_type = !minify ? ' ' : '';
@@ -1377,13 +1501,11 @@ function pathDataToD(pathData, optimize = 0) {
1377
1501
  }
1378
1502
 
1379
1503
  // Omit type for repeated commands
1380
- type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1504
+ type = (minify && com0.type === com.type && com.type.toLowerCase() !== 'm' )
1381
1505
  ? " "
1382
- : (
1383
- (com0.type === "M" && com.type === "L")
1384
- ) && minify
1506
+ : (minify && com0.type === "M" && com.type === "L"
1385
1507
  ? " "
1386
- : com.type;
1508
+ : com.type);
1387
1509
 
1388
1510
  // concatenate subsequent floating point values
1389
1511
  if (minify) {
@@ -1431,7 +1553,7 @@ function pathDataToD(pathData, optimize = 0) {
1431
1553
  return d;
1432
1554
  }
1433
1555
 
1434
- function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1556
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
1435
1557
 
1436
1558
  // cubic Bézier derivative
1437
1559
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -1453,8 +1575,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1453
1575
  let commands = [com1, com2];
1454
1576
 
1455
1577
  // detect dominant
1456
- let dist1 = getSquareDistance(com1.p0, com1.p);
1457
- let dist2 = getSquareDistance(com2.p0, com2.p);
1578
+ let dist1 = getDistAv(com1.p0, com1.p);
1579
+ let dist2 = getDistAv(com2.p0, com2.p);
1580
+
1458
1581
  let reverse = dist1 > dist2;
1459
1582
 
1460
1583
  // backup original commands
@@ -1513,11 +1636,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1513
1636
  let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1514
1637
  let r = sub(P, com1.p0);
1515
1638
 
1516
-
1517
1639
  t0 -= dot(r, dP) / dot(dP, dP);
1518
1640
 
1519
1641
  // construct merged cubic over [t0, 1]
1520
-
1521
1642
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1522
1643
  let Q3 = com2.p;
1523
1644
 
@@ -1534,6 +1655,7 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1534
1655
  cp1: Q1,
1535
1656
  cp2: Q2,
1536
1657
  p: Q3,
1658
+ t0
1537
1659
  };
1538
1660
 
1539
1661
  if (reverse) {
@@ -1542,10 +1664,11 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1542
1664
  cp1: Q2,
1543
1665
  cp2: Q1,
1544
1666
  p: Q0,
1667
+ t0
1545
1668
  };
1546
1669
  }
1547
1670
 
1548
- let tMid = (1 - t0)*0.5 ;
1671
+ let tMid = (1 - t0) * 0.5;
1549
1672
 
1550
1673
  let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
1551
1674
  let seg1_cp2 = ptM.cpts[2];
@@ -1553,21 +1676,22 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1553
1676
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
1554
1677
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
1555
1678
 
1556
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333);
1557
- let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1679
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333 );
1680
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333 );
1558
1681
 
1559
1682
  // test self intersections and exit
1560
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1561
- if(cp_intersection){
1683
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
1684
+ if (cp_intersection) {
1562
1685
 
1563
1686
  return commands;
1564
1687
  }
1565
1688
 
1689
+ if (debug) renderPoint(markers, ptM, 'purple');
1690
+
1566
1691
  result.cp1 = cp1_2;
1567
1692
  result.cp2 = cp2_2;
1568
1693
 
1569
- // check distances
1570
-
1694
+ // check distances between original starting point and extrapolated
1571
1695
  let dist3 = getDistAv(com1_o.p0, result.p0);
1572
1696
  let dist4 = getDistAv(com2_o.p, result.p);
1573
1697
  let dist5 = (dist3 + dist4);
@@ -1579,11 +1703,34 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1579
1703
  result.corner = com2_o.corner;
1580
1704
  result.dimA = com2_o.dimA;
1581
1705
  result.directionChange = com2_o.directionChange;
1706
+ result.type = 'C';
1582
1707
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1583
1708
 
1584
- // check if completely off
1709
+ // extrapolated starting point is not completely off
1585
1710
  if (dist5 < maxDist) {
1586
1711
 
1712
+ /*
1713
+ let tTotal = 1 + Math.abs(t0);
1714
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1715
+
1716
+ let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
1717
+ */
1718
+
1719
+ // split t to meet original mid segment start point
1720
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1721
+
1722
+ let tTotal = 1 + Math.abs(t0);
1723
+ tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
1724
+
1725
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
1726
+ let distSplit = getDistAv(ptSplit, com1.p);
1727
+
1728
+ // not close enough - exit
1729
+ if (distSplit > maxDist * tolerance ) {
1730
+
1731
+ return commands;
1732
+ }
1733
+
1587
1734
  // compare combined with original area
1588
1735
  let pathData0 = [
1589
1736
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -1600,129 +1747,198 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1600
1747
  let areaN = getPathArea(pathDataN);
1601
1748
  let areaDiff = Math.abs(areaN / area0 - 1);
1602
1749
 
1603
- result.error = areaDiff * 10 * tolerance;
1750
+ result.error = areaDiff * 5 * tolerance;
1604
1751
 
1605
- pathDataToD(pathDataN);
1752
+ if (debug) {
1753
+ let d = pathDataToD(pathDataN);
1754
+ renderPath(markers, d, 'orange');
1755
+ }
1606
1756
 
1607
- // success
1608
- if (areaDiff < 0.01) {
1757
+ // success!!!
1758
+ if (areaDiff < 0.05 * tolerance) {
1609
1759
  commands = [result];
1610
1760
 
1611
- }
1612
-
1761
+ }
1613
1762
  }
1614
1763
 
1615
1764
  return commands
1616
1765
 
1617
1766
  }
1618
1767
 
1619
- function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
1768
+ function simplifyPathDataCubic(pathData, {
1769
+ keepExtremes = true,
1770
+ keepInflections = true,
1771
+ keepCorners = true,
1772
+ extrapolateDominant = true,
1773
+ tolerance = 1,
1774
+ } = {}) {
1620
1775
 
1621
- let commands = [com1, com2];
1622
- let t = findSplitT(com1, com2);
1776
+ let pathDataN = [pathData[0]];
1777
+ let l = pathData.length;
1623
1778
 
1624
- let distAv1 = getDistAv(com1.p0, com1.p);
1625
- let distAv2 = getDistAv(com2.p0, com2.p);
1626
- let distMin = Math.min(distAv1, distAv2);
1779
+ for (let i = 2; l && i <= l; i++) {
1780
+ let com = pathData[i - 1];
1781
+ let comN = i < l ? pathData[i] : null;
1782
+ let typeN = comN?.type || null;
1627
1783
 
1628
- let distScale = 0.05;
1629
- let maxDist = distMin * distScale * tolerance;
1784
+ let isDirChange = com?.directionChange || null;
1785
+ let isDirChangeN = comN?.directionChange || null;
1630
1786
 
1631
- let comS = getExtrapolatedCommand(com1, com2, t, t);
1787
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
1632
1788
 
1633
- // test on path point against original
1634
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1789
+ // next is also cubic
1790
+ if (type === 'C' && typeN === 'C') {
1635
1791
 
1636
- let dist0 = getDistAv(com1.p, pt);
1637
- let dist1 = 0, dist2 = 0;
1638
- let close = dist0 < maxDist;
1639
- let success = false;
1792
+ // cannot be combined as crossing extremes or corners
1793
+ if (
1794
+ (keepInflections && isDirChangeN) ||
1795
+ (keepCorners && corner) ||
1796
+ (!isDirChange && keepExtremes && extreme)
1797
+ ) {
1640
1798
 
1641
- // collect error data
1642
- let error = dist0;
1799
+ pathDataN.push(com);
1800
+ }
1643
1801
 
1644
- /*
1645
- if (com2.directionChange) {
1802
+ // try simplification
1803
+ else {
1646
1804
 
1647
- }
1648
- */
1805
+ let combined = combineCubicPairs(com, comN, {tolerance});
1806
+ let error = 0;
1649
1807
 
1650
- if (close) {
1808
+ // combining successful! try next segment
1809
+ if (combined.length === 1) {
1810
+ com = combined[0];
1811
+ let offset = 1;
1651
1812
 
1652
- /**
1653
- * check additional points
1654
- * to prevent distortions
1655
- */
1813
+ // add cumulative error to prevent distortions
1814
+ error += com.error;
1656
1815
 
1657
- // 2nd segment mid
1658
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1816
+ // find next candidates
1659
1817
 
1660
- // simplified path
1661
- let t3 = (1 + t) * 0.5;
1662
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1663
- dist1 = getDistAv(pt_2, ptS_2);
1818
+ for (let n = i + 1; error < tolerance && n < l; n++) {
1819
+ let comN = pathData[n];
1820
+ if (comN.type !== 'C' ||
1821
+ (
1822
+ (keepInflections && comN.directionChange) ||
1823
+ (keepCorners && com.corner) ||
1824
+ (keepExtremes && com.extreme)
1825
+ )
1826
+ ) {
1827
+ break
1828
+ }
1664
1829
 
1665
- error += dist1;
1830
+ let combined = combineCubicPairs(com, comN, {tolerance});
1831
+ if (combined.length === 1) {
1832
+ // add cumulative error to prevent distortions
1666
1833
 
1667
- // quit - paths not congruent
1834
+ error += combined[0].error * 0.5;
1668
1835
 
1669
- if (dist1 < maxDist) {
1836
+ offset++;
1837
+ }
1838
+ com = combined[0];
1839
+ }
1670
1840
 
1671
- // 1st segment mid
1672
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1841
+ pathDataN.push(com);
1673
1842
 
1674
- let t2 = t * 0.5;
1675
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1676
- dist2 = getDistAv(pt_1, ptS_1);
1843
+ if (i < l) {
1844
+ i += offset;
1845
+ }
1677
1846
 
1678
- /*
1679
- if(dist1>tolerance){
1680
- renderPoint(markers, pt_1, 'blue')
1681
- renderPoint(markers, ptS_1, 'orange', '0.5%')
1847
+ } else {
1848
+ pathDataN.push(com);
1849
+ }
1682
1850
  }
1683
- */
1684
-
1685
- // quit - paths not congruent
1686
- if (dist1 + dist2 < maxDist) success = true;
1687
1851
 
1688
- // collect error data
1689
- error += dist2;
1852
+ } // end of bezier command
1690
1853
 
1854
+ // other commands
1855
+ else {
1856
+ pathDataN.push(com);
1691
1857
  }
1692
1858
 
1693
- } // end 1st try
1859
+ } // end command loop
1694
1860
 
1695
-
1696
- /*
1697
- if (extrapolateDominant && com2.extreme) {
1698
- renderPoint(markers, com2.p)
1861
+ return pathDataN
1862
+ }
1699
1863
 
1700
- }
1701
- */
1864
+ function combineCubicPairs(com1, com2, {
1865
+ tolerance = 1
1866
+ } = {}) {
1867
+
1868
+ let commands = [com1, com2];
1869
+
1870
+ // assume 2 segments are result of a segment split
1871
+ let t = findSplitT(com1, com2);
1872
+
1873
+ let distAv1 = getDistAv(com1.p0, com1.p);
1874
+ let distAv2 = getDistAv(com2.p0, com2.p);
1875
+ let distMin = Math.max(0, Math.min(distAv1, distAv2));
1876
+
1877
+ let distScale = 0.06;
1878
+ let maxDist = distMin * distScale * tolerance;
1879
+
1880
+ // get hypothetical combined command
1881
+ let comS = getExtrapolatedCommand(com1, com2, t);
1882
+
1883
+ // test new point-at-t against original mid segment starting point
1884
+ let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1885
+
1886
+ let dist0 = getDistAv(com1.p, pt);
1887
+ let dist1 = 0, dist2 = 0;
1888
+ let close = dist0 < maxDist;
1889
+ let success = false;
1890
+
1891
+ // collect error data
1892
+ let error = dist0;
1893
+
1894
+ if (close) {
1895
+
1896
+ /**
1897
+ * check additional points
1898
+ * to prevent distortions
1899
+ */
1900
+
1901
+ // 2nd segment mid
1902
+ let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1903
+
1904
+ // simplified path
1905
+ let t3 = (1 + t) * 0.5;
1906
+ let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1907
+ dist1 = getDistAv(pt_2, ptS_2);
1908
+
1909
+ error += dist1;
1910
+
1911
+ // quit - paths not congruent
1912
+
1913
+ if (dist1 < maxDist) {
1702
1914
 
1703
-
1915
+ // 1st segment mid
1916
+ let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1704
1917
 
1705
- // try extrapolated dominant curve
1918
+ let t2 = t * 0.5;
1919
+ let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1920
+ dist2 = getDistAv(pt_1, ptS_1);
1706
1921
 
1707
- // && !com1.extreme
1708
- if (extrapolateDominant && !success ) {
1922
+ /*
1923
+ if(dist1>tolerance){
1924
+ renderPoint(markers, pt_1, 'blue')
1925
+ renderPoint(markers, ptS_1, 'orange', '0.5%')
1926
+ }
1927
+ */
1709
1928
 
1710
- let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
1929
+ // quit - paths not congruent
1930
+ if (dist1 + dist2 < maxDist) success = true;
1711
1931
 
1712
- if(combinedEx.length===1){
1713
- success = true;
1714
- comS = combinedEx[0];
1715
- error = comS.error;
1932
+ // collect error data
1933
+ error += dist2;
1716
1934
 
1717
1935
  }
1718
1936
 
1719
-
1720
- }
1937
+ } // end 1st try
1721
1938
 
1722
1939
  // add meta
1723
1940
  if (success) {
1724
1941
 
1725
-
1726
1942
  // correct to exact start and end points
1727
1943
  comS.p0 = com1.p0;
1728
1944
  comS.p = com2.p;
@@ -1745,50 +1961,69 @@ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance =
1745
1961
  return commands;
1746
1962
  }
1747
1963
 
1748
- function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1964
+ function getExtrapolatedCommand(com1, com2, t = 0) {
1749
1965
 
1750
1966
  let { p0, cp1 } = com1;
1751
1967
  let { p, cp2 } = com2;
1752
1968
 
1753
1969
  // extrapolate control points
1754
- let cp1_S = {
1755
- x: (cp1.x - (1 - t1) * p0.x) / t1,
1756
- y: (cp1.y - (1 - t1) * p0.y) / t1
1970
+ cp1 = {
1971
+ x: (cp1.x - (1 - t) * p0.x) / t,
1972
+ y: (cp1.y - (1 - t) * p0.y) / t
1757
1973
  };
1758
1974
 
1759
- let cp2_S = {
1760
- x: (cp2.x - t2 * p.x) / (1 - t2),
1761
- y: (cp2.y - t2 * p.y) / (1 - t2)
1975
+ cp2 = {
1976
+ x: (cp2.x - t * p.x) / (1 - t),
1977
+ y: (cp2.y - t * p.y) / (1 - t)
1762
1978
  };
1763
1979
 
1764
- let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
1980
+ return { p0, cp1, cp2, p };
1981
+ }
1982
+
1983
+ function findSplitT(com1, com2) {
1984
+
1985
+ let len3 = getDistance(com1.cp2, com1.p);
1986
+ let len4 = getDistance(com1.cp2, com2.cp1);
1765
1987
 
1766
- return comS
1988
+ let t = Math.min(len3) / len4;
1767
1989
 
1990
+ return t
1768
1991
  }
1769
1992
 
1770
- function findSplitT(com1, com2) {
1993
+ function commandIsFlat(points, {
1994
+ tolerance = 1,
1995
+ debug=false
1996
+ } = {}) {
1771
1997
 
1772
- // control tangent intersection
1773
- let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1998
+ let isFlat=false;
1999
+ let report = {
2000
+ flat:true,
2001
+ steepness:0
2002
+ };
1774
2003
 
1775
- // intersection 2nd cp1 tangent and global tangent intersection
1776
- let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
2004
+ let p0 = points[0];
2005
+ let p = points[points.length - 1];
1777
2006
 
1778
- let len1 = getDistance(pt1, com2.p);
1779
- let len2 = getDistance(ptI, com2.p);
2007
+ let xSet = new Set([...points.map(pt => +pt.x.toFixed(8))]);
2008
+ let ySet = new Set([...points.map(pt => +pt.y.toFixed(8))]);
1780
2009
 
1781
- let t = 1 - len2 / len1;
2010
+ // must be flat
2011
+ if(xSet.size===1 || ySet.size===1) return !debug ? true : report;
1782
2012
 
1783
- // check self intersections
2013
+ let squareDist = getSquareDistance(p0, p);
2014
+ let threshold = squareDist / 1000 * tolerance;
2015
+ let area = getPolygonArea(points, true);
1784
2016
 
1785
- let len3 = getDistance(com1.cp2, com1.p);
1786
- let len4 = getDistance(com1.cp2, com2.cp1);
2017
+ // flat enough
2018
+ if(area < threshold) isFlat = true;
1787
2019
 
1788
- t = Math.min(len3) / len4;
2020
+ if(debug){
2021
+ report.flat = isFlat;
1789
2022
 
1790
- return t
2023
+ report.steepness = area/squareDist*10;
2024
+ }
1791
2025
 
2026
+ return !debug ? isFlat : report;
1792
2027
  }
1793
2028
 
1794
2029
  function analyzePathData(pathData = []) {
@@ -1833,7 +2068,6 @@ function analyzePathData(pathData = []) {
1833
2068
  * this way we can skip certain tests
1834
2069
  */
1835
2070
  let commandPts = [p0];
1836
- let isFlat = false;
1837
2071
 
1838
2072
  // init properties
1839
2073
  com.idx = c - 1;
@@ -1864,7 +2098,7 @@ function analyzePathData(pathData = []) {
1864
2098
  com.p0 = p0;
1865
2099
  com.p = p;
1866
2100
 
1867
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
2101
+ let cp1, cp2, cp1N, pN, typeN, area1;
1868
2102
 
1869
2103
  let dimA = getDistAv(p0, p);
1870
2104
  com.dimA = dimA;
@@ -1904,13 +2138,18 @@ function analyzePathData(pathData = []) {
1904
2138
  if (type === 'C') commandPts.push(cp2);
1905
2139
  commandPts.push(p);
1906
2140
 
2141
+ /*
2142
+
1907
2143
  let commandFlatness = commandIsFlat(commandPts);
1908
2144
  isFlat = commandFlatness.flat;
1909
2145
  com.flat = isFlat;
1910
2146
 
1911
2147
  if (isFlat) {
1912
2148
  com.extreme = false;
2149
+
1913
2150
  }
2151
+ */
2152
+
1914
2153
  }
1915
2154
 
1916
2155
  /**
@@ -1919,7 +2158,7 @@ function analyzePathData(pathData = []) {
1919
2158
  * so we interpret maximum x/y on-path points as well as extremes
1920
2159
  * but we ignore linetos to allow chunk compilation
1921
2160
  */
1922
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
2161
+ if (type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1923
2162
  com.extreme = true;
1924
2163
  }
1925
2164
 
@@ -1932,7 +2171,7 @@ function analyzePathData(pathData = []) {
1932
2171
  pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1933
2172
 
1934
2173
  cp1N = { x: comN.values[0], y: comN.values[1] };
1935
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
2174
+ comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1936
2175
  }
1937
2176
 
1938
2177
  /**
@@ -1962,19 +2201,15 @@ function analyzePathData(pathData = []) {
1962
2201
  // check extremes
1963
2202
  let cpts = commandPts.slice(1);
1964
2203
 
1965
- let w = pN ? Math.abs(pN.x - p0.x) : 0;
1966
- let h = pN ? Math.abs(pN.y - p0.y) : 0;
1967
- let thresh = (w + h) / 2 * 0.1;
1968
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1969
-
1970
- let flatness2 = commandIsFlat(pts1, thresh);
1971
- let isFlat2 = flatness2.flat;
2204
+ pN ? Math.abs(pN.x - p0.x) : 0;
2205
+ pN ? Math.abs(pN.y - p0.y) : 0;
1972
2206
 
1973
2207
  /**
1974
2208
  * if current and next cubic are flat
1975
2209
  * we don't flag them as extremes to allow simplification
1976
2210
  */
1977
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
2211
+
2212
+ let hasExtremes = (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1978
2213
 
1979
2214
  if (hasExtremes) {
1980
2215
  com.extreme = true;
@@ -2020,11 +2255,11 @@ function detectAccuracy(pathData) {
2020
2255
 
2021
2256
  // Reference first MoveTo command (M)
2022
2257
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
2023
- let p0 = M;
2258
+ let p0 = M;
2024
2259
  let p = M;
2025
2260
  pathData[0].decimals = 0;
2026
2261
 
2027
- let dims = new Set();
2262
+ let dims = [];
2028
2263
 
2029
2264
  // add average distances
2030
2265
  for (let i = 0, len = pathData.length; i < len; i++) {
@@ -2032,28 +2267,33 @@ function detectAccuracy(pathData) {
2032
2267
  let { type, values } = com;
2033
2268
 
2034
2269
  let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
2035
- p={x:lastVals[0], y:lastVals[1]};
2270
+ p = { x: lastVals[0], y: lastVals[1] };
2036
2271
 
2037
2272
  // use existing averave dimension value or calculate
2038
- let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
2273
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type !== 'M' ? +getDistAv(p0, p).toFixed(8) : 0;
2039
2274
 
2040
- if(dimA) dims.add(dimA);
2041
-
2275
+ if (dimA) dims.push(dimA);
2042
2276
 
2043
- if(type==='M'){
2044
- M=p;
2277
+ if (type === 'M') {
2278
+ M = p;
2045
2279
  }
2046
2280
  p0 = p;
2047
2281
  }
2048
2282
 
2049
- let dim_min = Array.from(dims).sort();
2050
- let sliceIdx = Math.ceil(dim_min.length/8);
2051
- dim_min = dim_min.slice(0, sliceIdx );
2283
+ let dim_min = dims.sort();
2052
2284
 
2053
- let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2285
+ /*
2286
+ let minVal = dim_min.length > 15 ?
2287
+ (dim_min[0] + dim_min[2]) / 2 :
2288
+ dim_min[0];
2289
+ */
2290
+
2291
+ let sliceIdx = Math.ceil(dim_min.length / 10);
2292
+ dim_min = dim_min.slice(0, sliceIdx);
2293
+ let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
2054
2294
 
2055
- let threshold = 50;
2056
- let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
2295
+ let threshold = 40;
2296
+ let decimalsAuto = minVal > threshold*1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
2057
2297
 
2058
2298
  // clamp
2059
2299
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -2066,19 +2306,24 @@ function detectAccuracy(pathData) {
2066
2306
  * based on suggested accuracy in path data
2067
2307
  */
2068
2308
  function roundPathData(pathData, decimals = -1) {
2069
- // has recommended decimals
2070
- let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2071
2309
 
2072
- for(let c=0, len=pathData.length; c<len; c++){
2073
- let com=pathData[c];
2310
+ let len = pathData.length;
2311
+
2312
+ for (let c = 0; c < len; c++) {
2313
+
2314
+ let values = pathData[c].values;
2315
+ let valLen = values.length;
2074
2316
 
2075
- if (decimals >-1 || hasDecimal) {
2076
- decimals = hasDecimal ? com.decimals : decimals;
2317
+ if (valLen && (decimals > -1) ) {
2077
2318
 
2078
- pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2319
+ for(let v=0; v<valLen; v++){
2079
2320
 
2321
+ pathData[c].values[v] = +values[v].toFixed(decimals);
2322
+ }
2080
2323
  }
2081
- } return pathData;
2324
+ }
2325
+
2326
+ return pathData;
2082
2327
  }
2083
2328
 
2084
2329
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
@@ -2096,7 +2341,7 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2096
2341
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2097
2342
  let comN = {type, values};
2098
2343
 
2099
- if (dist1 < threshold) {
2344
+ if (dist1 && threshold && dist1 < threshold) {
2100
2345
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2101
2346
  if (cp1_Q) {
2102
2347
 
@@ -2121,9 +2366,10 @@ function convertPathData(pathData, {
2121
2366
  if (toShorthands) pathData = pathDataToShorthands(pathData);
2122
2367
 
2123
2368
  // pre round - before relative conversion to minimize distortions
2124
- pathData = roundPathData(pathData, decimals);
2369
+ if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
2125
2370
  if (toRelative) pathData = pathDataToRelative(pathData);
2126
2371
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
2372
+
2127
2373
  return pathData
2128
2374
  }
2129
2375
 
@@ -2425,7 +2671,7 @@ function pathDataToLonghands(pathData, decimals = -1, test = true) {
2425
2671
  * L, L, C, Q => H, V, S, T
2426
2672
  * reversed method: pathDataToLonghands()
2427
2673
  */
2428
- function pathDataToShorthands(pathData, decimals = -1, test = true) {
2674
+ function pathDataToShorthands(pathData, decimals = -1, test = false) {
2429
2675
 
2430
2676
  /**
2431
2677
  * analyze pathdata – if you're sure your data is already absolute skip it via test=false
@@ -2436,29 +2682,28 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2436
2682
  hasRel = /[astvqmhlc]/g.test(commandTokens);
2437
2683
  }
2438
2684
 
2439
- pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2685
+ pathData = test && hasRel ? pathDataToAbsoluteOrRelative(pathData) : pathData;
2686
+
2687
+ let len = pathData.length;
2688
+ let pathDataShorts = new Array(len);
2440
2689
 
2441
2690
  let comShort = {
2442
2691
  type: "M",
2443
2692
  values: pathData[0].values
2444
2693
  };
2445
2694
 
2446
- if (pathData[0].decimals) {
2447
-
2448
- comShort.decimals = pathData[0].decimals;
2449
- }
2450
-
2451
- let pathDataShorts = [comShort];
2695
+ pathDataShorts[0] = comShort;
2452
2696
 
2453
2697
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
2454
2698
  let p;
2455
2699
  let tolerance = 0.01;
2456
2700
 
2457
- for (let i = 1, len = pathData.length; i < len; i++) {
2701
+ for (let i = 1; i < len; i++) {
2458
2702
 
2459
2703
  let com = pathData[i];
2460
2704
  let { type, values } = com;
2461
- let valuesLast = values.slice(-2);
2705
+ let valuesLen = values.length;
2706
+ let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
2462
2707
 
2463
2708
  // previoius command
2464
2709
  let comPrev = pathData[i - 1];
@@ -2506,7 +2751,8 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2506
2751
  if (typePrev !== 'Q') {
2507
2752
 
2508
2753
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2509
- pathDataShorts.push(com);
2754
+
2755
+ pathDataShorts[i] = com;
2510
2756
  continue;
2511
2757
  }
2512
2758
 
@@ -2535,7 +2781,8 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2535
2781
 
2536
2782
  if (typePrev !== 'C') {
2537
2783
 
2538
- pathDataShorts.push(com);
2784
+ pathDataShorts[i] = com;
2785
+
2539
2786
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2540
2787
  continue;
2541
2788
  }
@@ -2577,8 +2824,10 @@ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2577
2824
  }
2578
2825
 
2579
2826
  p0 = { x: valuesLast[0], y: valuesLast[1] };
2580
- pathDataShorts.push(comShort);
2827
+ pathDataShorts[i] = comShort;
2828
+
2581
2829
  }
2830
+
2582
2831
  return pathDataShorts;
2583
2832
  }
2584
2833
 
@@ -3023,10 +3272,10 @@ function normalizePathData(pathData = [],
3023
3272
  quadraticToCubic = false,
3024
3273
  arcToCubic = false,
3025
3274
  arcAccuracy = 2,
3026
- } = {},
3027
3275
 
3028
- {
3276
+ // assume we need full normalization
3029
3277
  hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3278
+
3030
3279
  } = {}
3031
3280
  ) {
3032
3281
 
@@ -3077,15 +3326,28 @@ function parsePathDataNormalized(d,
3077
3326
  } = {}
3078
3327
  ) {
3079
3328
 
3080
- let pathDataObj = parsePathDataString(d);
3081
- let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
3082
- let pathData = pathDataObj.pathData;
3329
+ // is already array
3330
+ let isArray = Array.isArray(d);
3331
+
3332
+ // normalize native pathData to regular array
3333
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3334
+ /*
3335
+ if (hasConstructor) {
3336
+ d = d.map(com => { return { type: com.type, values: com.values } })
3337
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3338
+ }
3339
+ */
3340
+
3341
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3342
+
3343
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3344
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3083
3345
 
3084
3346
  // normalize
3085
3347
  pathData = normalizePathData(pathData,
3086
- { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
3087
-
3088
- { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
3348
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3349
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3350
+ },
3089
3351
  );
3090
3352
 
3091
3353
  return pathData;
@@ -3437,7 +3699,8 @@ function parsePathDataString(d, debug = true) {
3437
3699
  if (debug === 'log') {
3438
3700
  console.log(feedback);
3439
3701
  } else {
3440
- throw new Error(feedback)
3702
+
3703
+ console.warn(feedback);
3441
3704
  }
3442
3705
  }
3443
3706
 
@@ -3465,18 +3728,266 @@ function parsePathDataString(d, debug = true) {
3465
3728
 
3466
3729
  }
3467
3730
 
3468
- function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3731
+ function stringifyPathData(pathData) {
3732
+ return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3733
+ }
3734
+
3735
+ function shapeElToPath(el) {
3736
+
3737
+ let nodeName = el.nodeName.toLowerCase();
3738
+ if (nodeName === 'path') return el;
3739
+
3740
+ let pathData = getPathDataFromEl(el);
3741
+ let d = pathData.map(com => { return `${com.type} ${com.values} ` }).join(' ');
3742
+ let attributes = [...el.attributes].map(att => att.name);
3743
+
3744
+ let pathN = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3745
+ pathN.setAttribute('d', d);
3746
+
3747
+ let exclude = ['x', 'y', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
3748
+
3749
+ attributes.forEach(att => {
3750
+ if (!exclude.includes(att)) {
3751
+ let val = el.getAttribute(att);
3752
+ pathN.setAttribute(att, val);
3753
+ }
3754
+ });
3755
+
3756
+ return pathN
3757
+
3758
+ }
3759
+
3760
+ // retrieve pathdata from svg geometry elements
3761
+ function getPathDataFromEl(el, stringify = false) {
3762
+
3763
+ let pathData = [];
3764
+ let type = el.nodeName;
3765
+ let atts, attNames, d, x, y, width, height, r, rx, ry, cx, cy, x1, x2, y1, y2;
3766
+
3767
+ // convert relative or absolute units
3768
+ const svgElUnitsToPixel = (el, decimals = 9) => {
3769
+
3770
+ const svg = el.nodeName !== "svg" ? el.closest("svg") : el;
3771
+
3772
+ // convert real life units to pixels
3773
+ const translateUnitToPixel = (value) => {
3774
+
3775
+ if (value === null) {
3776
+ return 0
3777
+ }
3778
+
3779
+ let dpi = 96;
3780
+ let unit = value.match(/([a-z]+)/gi);
3781
+ unit = unit ? unit[0] : "";
3782
+ let val = parseFloat(value);
3783
+ let rat;
3784
+
3785
+ // no unit - already pixes/user unit
3786
+ if (!unit) {
3787
+ return val;
3788
+ }
3789
+
3790
+ switch (unit) {
3791
+ case "in":
3792
+ rat = dpi;
3793
+ break;
3794
+ case "pt":
3795
+ rat = (1 / 72) * 96;
3796
+ break;
3797
+ case "cm":
3798
+ rat = (1 / 2.54) * 96;
3799
+ break;
3800
+ case "mm":
3801
+ rat = ((1 / 2.54) * 96) / 10;
3802
+ break;
3803
+ // just a default approximation
3804
+ case "em":
3805
+ case "rem":
3806
+ rat = 16;
3807
+ break;
3808
+ default:
3809
+ rat = 1;
3810
+ }
3811
+ let valuePx = val * rat;
3812
+ return +valuePx.toFixed(decimals);
3813
+ };
3814
+
3815
+ // svg width and height attributes
3816
+ let width = svg.getAttribute("width");
3817
+ width = width ? translateUnitToPixel(width) : 300;
3818
+ let height = svg.getAttribute("height");
3819
+ height = width ? translateUnitToPixel(height) : 150;
3820
+
3821
+ let vB = svg.getAttribute("viewBox");
3822
+ vB = vB
3823
+ ? vB
3824
+ .replace(/,/g, " ")
3825
+ .split(" ")
3826
+ .filter(Boolean)
3827
+ .map((val) => {
3828
+ return +val;
3829
+ })
3830
+ : [];
3831
+
3832
+ let w = vB.length ? vB[2] : width;
3833
+ let h = vB.length ? vB[3] : height;
3834
+ let scaleX = w / 100;
3835
+ let scaleY = h / 100;
3836
+ let scalRoot = Math.sqrt((Math.pow(scaleX, 2) + Math.pow(scaleY, 2)) / 2);
3837
+
3838
+ let attsH = ["x", "width", "x1", "x2", "rx", "cx", "r"];
3839
+ let attsV = ["y", "height", "y1", "y2", "ry", "cy"];
3840
+
3841
+ let atts = el.getAttributeNames();
3842
+ atts.forEach((att) => {
3843
+ let val = el.getAttribute(att);
3844
+ let valAbs = val;
3845
+ if (attsH.includes(att) || attsV.includes(att)) {
3846
+ let scale = attsH.includes(att) ? scaleX : scaleY;
3847
+ scale = att === "r" && w != h ? scalRoot : scale;
3848
+ let unit = val.match(/([a-z|%]+)/gi);
3849
+ unit = unit ? unit[0] : "";
3850
+ if (val.includes("%")) {
3851
+ valAbs = parseFloat(val) * scale;
3852
+ }
3853
+
3854
+ else {
3855
+ valAbs = translateUnitToPixel(val);
3856
+ }
3857
+ el.setAttribute(att, +valAbs);
3858
+ }
3859
+ });
3860
+ };
3861
+
3862
+ svgElUnitsToPixel(el);
3863
+
3864
+ const getAtts = (attNames) => {
3865
+ atts = {};
3866
+ attNames.forEach(att => {
3867
+ atts[att] = +el.getAttribute(att);
3868
+ });
3869
+ return atts
3870
+ };
3871
+
3872
+ switch (type) {
3873
+ case 'path':
3874
+ d = el.getAttribute("d");
3875
+ pathData = parsePathDataNormalized(d);
3876
+ break;
3877
+
3878
+ case 'rect':
3879
+ attNames = ['x', 'y', 'width', 'height', 'rx', 'ry'];
3880
+ ({ x, y, width, height, rx, ry } = getAtts(attNames));
3881
+
3882
+ if (!rx && !ry) {
3883
+ pathData = [
3884
+ { type: "M", values: [x, y] },
3885
+ { type: "L", values: [x + width, y] },
3886
+ { type: "L", values: [x + width, y + height] },
3887
+ { type: "L", values: [x, y + height] },
3888
+ { type: "Z", values: [] }
3889
+ ];
3890
+ } else {
3891
+
3892
+ if (rx > width / 2) {
3893
+ rx = width / 2;
3894
+ }
3895
+ if (ry > height / 2) {
3896
+ ry = height / 2;
3897
+ }
3898
+ pathData = [
3899
+ { type: "M", values: [x + rx, y] },
3900
+ { type: "L", values: [x + width - rx, y] },
3901
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
3902
+ { type: "L", values: [x + width, y + height - ry] },
3903
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
3904
+ { type: "L", values: [x + rx, y + height] },
3905
+ { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
3906
+ { type: "L", values: [x, y + ry] },
3907
+ { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
3908
+ { type: "Z", values: [] }
3909
+ ];
3910
+ }
3911
+ break;
3912
+
3913
+ case 'circle':
3914
+ case 'ellipse':
3915
+
3916
+ attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
3917
+ ({ cx, cy, r, rx, ry } = getAtts(attNames));
3918
+
3919
+ let isCircle = type === 'circle';
3920
+
3921
+ if (isCircle) {
3922
+ r = r;
3923
+ rx = r;
3924
+ ry = r;
3925
+ } else {
3926
+ rx = rx ? rx : r;
3927
+ ry = ry ? ry : r;
3928
+ }
3929
+
3930
+ // simplified radii for cirecles
3931
+ let rxS = isCircle && r>=1 ? 1 : rx;
3932
+ let ryS = isCircle && r>=1 ? 1 : rx;
3933
+
3934
+ pathData = [
3935
+ { type: "M", values: [cx + rx, cy] },
3936
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx - rx, cy] },
3937
+ { type: "A", values: [rxS, ryS, 0, 1, 1, cx + rx, cy] },
3938
+ ];
3939
+
3940
+ break;
3941
+ case 'line':
3942
+ attNames = ['x1', 'y1', 'x2', 'y2'];
3943
+ ({ x1, y1, x2, y2 } = getAtts(attNames));
3944
+ pathData = [
3945
+ { type: "M", values: [x1, y1] },
3946
+ { type: "L", values: [x2, y2] }
3947
+ ];
3948
+ break;
3949
+ case 'polygon':
3950
+ case 'polyline':
3951
+
3952
+ let points = el.getAttribute('points').replaceAll(',', ' ').split(' ').filter(Boolean);
3953
+
3954
+ for (let i = 0; i < points.length; i += 2) {
3955
+ pathData.push({
3956
+ type: (i === 0 ? "M" : "L"),
3957
+ values: [+points[i], +points[i + 1]]
3958
+ });
3959
+ }
3960
+ if (type === 'polygon') {
3961
+ pathData.push({
3962
+ type: "Z",
3963
+ values: []
3964
+ });
3965
+ }
3966
+ break;
3967
+ }
3968
+
3969
+ return stringify ? stringifyPathData(pathData) : pathData;
3970
+
3971
+ }
3972
+
3973
+ function pathDataRemoveColinear(pathData, {
3974
+ tolerance = 1,
3975
+
3976
+ flatBezierToLinetos = true
3977
+ }={}) {
3469
3978
 
3470
3979
  let pathDataN = [pathData[0]];
3980
+
3471
3981
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3472
3982
  let p0 = M;
3473
3983
  let p = M;
3474
3984
  pathData[pathData.length - 1].type.toLowerCase() === 'z';
3475
3985
 
3476
3986
  for (let c = 1, l = pathData.length; c < l; c++) {
3477
- let comPrev = pathData[c - 1];
3987
+
3478
3988
  let com = pathData[c];
3479
3989
  let comN = pathData[c + 1] || pathData[l - 1];
3990
+
3480
3991
  let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3481
3992
 
3482
3993
  let { type, values } = com;
@@ -3485,11 +3996,9 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3485
3996
 
3486
3997
  let area = getPolygonArea([p0, p, p1], true);
3487
3998
 
3488
- getSquareDistance(p0, p);
3489
- getSquareDistance(p, p1);
3490
3999
  let distSquare = getSquareDistance(p0, p1);
3491
4000
 
3492
- let distMax = distSquare / 200 * tolerance;
4001
+ let distMax = distSquare ? distSquare / 333 * tolerance : 0;
3493
4002
 
3494
4003
  let isFlat = area < distMax;
3495
4004
  let isFlatBez = false;
@@ -3503,30 +4012,38 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3503
4012
  [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3504
4013
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3505
4014
 
3506
- isFlatBez = checkBezierFlatness(p0, cpts, p);
3507
- // console.log();
4015
+ isFlatBez = commandIsFlat([p0, ...cpts, p],{tolerance});
3508
4016
 
3509
- if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
4017
+ if (isFlatBez && c < l - 1 ) {
3510
4018
  type = "L";
3511
4019
  com.type = "L";
3512
4020
  com.values = valsL;
3513
4021
 
3514
4022
  }
3515
-
3516
4023
  }
3517
4024
 
3518
- // update end point
3519
- p0 = p;
3520
-
3521
4025
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3522
4026
 
3523
4027
  if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
3524
-
3525
4028
 
4029
+ /*
4030
+ console.log(area, distMax );
4031
+
4032
+ if(p0.x === p.x && p0.y === p.y){
4033
+
4034
+ }
4035
+
4036
+ renderPoint(markers, p0, 'blue', '1.5%', '1')
4037
+ renderPoint(markers, p, 'red', '1%', '1')
4038
+ renderPoint(markers, p1, 'cyan', '0.5%', '1')
4039
+ */
3526
4040
 
3527
4041
  continue;
3528
4042
  }
3529
4043
 
4044
+ // update end point
4045
+ p0 = p;
4046
+
3530
4047
  if (type === 'M') {
3531
4048
  M = p;
3532
4049
  p0 = M;
@@ -3545,20 +4062,44 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3545
4062
 
3546
4063
  }
3547
4064
 
4065
+ function removeOrphanedM(pathData) {
4066
+
4067
+ let pathDataN = [];
4068
+ for (let i = 0, l = pathData.length; i < l; i++) {
4069
+ let com = pathData[i];
4070
+ if (!com) continue;
4071
+ let { type = null, values = [] } = com;
4072
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4073
+ if ((type === 'M' || type === 'm')) {
4074
+
4075
+ if (!comN || (comN && (comN.type === 'Z' || comN.type === 'z'))) {
4076
+ if(comN) i++;
4077
+ continue
4078
+ }
4079
+ }
4080
+ pathDataN.push(com);
4081
+ }
4082
+
4083
+ return pathDataN;
4084
+
4085
+ }
4086
+
4087
+ /*
3548
4088
  // remove zero-length segments introduced by rounding
3549
- function removeZeroLengthLinetos_post(pathData) {
3550
- let pathDataOpt = [];
4089
+ export function removeZeroLengthLinetos_post(pathData) {
4090
+ let pathDataOpt = []
3551
4091
  pathData.forEach((com, i) => {
3552
4092
  let { type, values } = com;
3553
4093
  if (type === 'l' || type === 'v' || type === 'h') {
3554
- let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
3555
- if (hasLength) pathDataOpt.push(com);
4094
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
4095
+ if (hasLength) pathDataOpt.push(com)
3556
4096
  } else {
3557
- pathDataOpt.push(com);
4097
+ pathDataOpt.push(com)
3558
4098
  }
3559
- });
4099
+ })
3560
4100
  return pathDataOpt
3561
4101
  }
4102
+ */
3562
4103
 
3563
4104
  function removeZeroLengthLinetos(pathData) {
3564
4105
 
@@ -3570,16 +4111,27 @@ function removeZeroLengthLinetos(pathData) {
3570
4111
 
3571
4112
  for (let c = 1, l = pathData.length; c < l; c++) {
3572
4113
  let com = pathData[c];
4114
+ let comPrev = pathData[c-1];
4115
+ let comNext = pathData[c+1] || null;
3573
4116
  let { type, values } = com;
3574
4117
 
3575
- let valsL = values.slice(-2);
3576
- p = { x: valsL[0], y: valsL[1] };
4118
+ // zero length segments are simetimes used in icons for dots
4119
+ let isDot = comPrev.type.toLowerCase() ==='m' && !comNext;
4120
+
4121
+ let valsLen = values.length;
4122
+ p = { x: values[valsLen-2], y: values[valsLen-1] };
3577
4123
 
3578
4124
  // skip lineto
3579
- if (type === 'L' && p.x === p0.x && p.y === p0.y) {
4125
+ if (!isDot && type === 'L' && p.x === p0.x && p.y === p0.y) {
3580
4126
  continue
3581
4127
  }
3582
4128
 
4129
+ // skip minified zero length
4130
+ if (!isDot && (type === 'l' || type === 'v' || type === 'h')) {
4131
+ let noLength = type === 'l' ? (values.join('') === '00') : values[0] === 0;
4132
+ if(noLength) continue
4133
+ }
4134
+
3583
4135
  pathDataN.push(com);
3584
4136
  p0 = p;
3585
4137
  }
@@ -3677,7 +4229,7 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3677
4229
  }
3678
4230
  // use top most command
3679
4231
  else {
3680
- indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x);
4232
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
3681
4233
  newIndex = indices[0].index;
3682
4234
  }
3683
4235
 
@@ -3685,7 +4237,7 @@ function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
3685
4237
  pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3686
4238
  }
3687
4239
 
3688
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(7) };
4240
+ M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
3689
4241
 
3690
4242
  len = pathData.length;
3691
4243
 
@@ -3799,123 +4351,161 @@ function addClosePathLineto(pathData) {
3799
4351
  return pathData;
3800
4352
  }
3801
4353
 
3802
- /**
3803
- * reverse pathdata
3804
- * make sure all command coordinates are absolute and
3805
- * shorthands are converted to long notation
3806
- */
3807
- function reversePathData(pathData, {
3808
- arcToCubic = false,
3809
- quadraticToCubic = false,
3810
- toClockwise = false,
3811
- returnD = false
4354
+ function refineAdjacentExtremes(pathData, {
4355
+ threshold = null, tolerance = 1
3812
4356
  } = {}) {
3813
4357
 
3814
- /**
3815
- * Add closing lineto:
3816
- * needed for path reversing or adding points
3817
- */
3818
- const addClosePathLineto = (pathData) => {
3819
- let closed = pathData[pathData.length - 1].type.toLowerCase() === "z";
3820
- let M = pathData[0];
3821
- let [x0, y0] = [M.values[0], M.values[1]];
3822
- let lastCom = closed ? pathData[pathData.length - 2] : pathData[pathData.length - 1];
3823
- let [xE, yE] = [lastCom.values[lastCom.values.length - 2], lastCom.values[lastCom.values.length - 1]];
3824
-
3825
- if (closed && (x0 != xE || y0 != yE)) {
3826
-
3827
- pathData.pop();
3828
- pathData.push(
3829
- {
3830
- type: "L",
3831
- values: [x0, y0]
3832
- },
3833
- {
3834
- type: "Z",
3835
- values: []
3836
- }
3837
- );
3838
- }
3839
- return pathData;
3840
- };
4358
+ if (!threshold) {
4359
+ let bb = getPathDataBBox(pathData);
4360
+ threshold = (bb.width + bb.height) / 2 * 0.05;
4361
+
4362
+ }
4363
+
4364
+ let l = pathData.length;
4365
+
4366
+ for (let i = 0; i < l; i++) {
4367
+ let com = pathData[i];
4368
+ let { type, values, extreme, corner = false, dimA, p0, p } = com;
4369
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4370
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4371
+
4372
+ // check dist
4373
+ let diff = comN ? getDistAv(p, comN.p) : Infinity;
4374
+ let isCose = diff < threshold;
4375
+
4376
+ let diff2 = comN2 ? getDistAv(comN2.p, comN.p) : Infinity;
4377
+ let isCose2 = diff2 < threshold;
4378
+
4379
+ // next is extreme
4380
+ if (comN && type === 'C' && comN.type === 'C' && extreme && comN2 && comN2.extreme) {
4381
+
4382
+ if (isCose2 || isCose) {
4383
+
4384
+ // extrapolate
4385
+ let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
3841
4386
 
3842
- // helper to rearrange control points for all command types
3843
- const reverseControlPoints = (type, values) => {
3844
- let controlPoints = [];
3845
- let endPoints = [];
3846
- if (type !== "A") {
3847
- for (let p = 0; p < values.length; p += 2) {
3848
- controlPoints.push([values[p], values[p + 1]]);
4387
+ if (comEx.length === 1) {
4388
+
4389
+ pathData[i + 1] = null;
4390
+ comEx = comEx[0];
4391
+
4392
+ pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4393
+ pathData[i + 2].cp1 = comEx.cp1;
4394
+ pathData[i + 2].cp2 = comEx.cp2;
4395
+ pathData[i + 2].p0 = comEx.p0;
4396
+ pathData[i + 2].p = comEx.p;
4397
+ pathData[i + 2].extreme = comEx.extreme;
4398
+
4399
+ i++;
4400
+ continue
4401
+ }
3849
4402
  }
3850
- endPoints = controlPoints.pop();
3851
- controlPoints.reverse();
4403
+
3852
4404
  }
3853
- // is arc
3854
- else {
3855
4405
 
3856
- let sweep = values[4] == 0 ? 1 : 0;
3857
- controlPoints = [values[0], values[1], values[2], values[3], sweep];
3858
- endPoints = [values[5], values[6]];
4406
+ // short after extreme
4407
+
4408
+ if (comN && type === 'C' && comN.type === 'C' && extreme ) {
4409
+
4410
+ if (isCose) {
4411
+
4412
+ let dx1 = (com.cp1.x - comN.p0.x);
4413
+ let dy1 = (com.cp1.y - comN.p0.y);
4414
+
4415
+ let horizontal = Math.abs(dy1) < Math.abs(dx1);
4416
+
4417
+ let pN = comN.p;
4418
+ let ptI;
4419
+ let t = 1;
4420
+
4421
+ let area0 = getPolygonArea([com.p0, com.p , comN.p]);
4422
+ // cpts area
4423
+ let area1 = getPolygonArea([com.p0, com.cp1, com.cp2, com.p]);
4424
+
4425
+ // sign change: is corner => skip
4426
+ if ( (area0<0 && area1>0) || (area0>0 && area1<0)) {
4427
+
4428
+ continue;
4429
+ }
4430
+
4431
+
4432
+ if (comN.extreme) {
4433
+
4434
+ // extend cp2
4435
+ if (horizontal) {
4436
+ t = Math.abs(Math.abs(comN.cp2.x - comN.p.x) / Math.abs(com.cp2.x - com.p.x));
4437
+ t = Math.min(1, t);
4438
+
4439
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4440
+ com.cp2.x = ptI.x;
4441
+
4442
+ }
4443
+ else {
4444
+
4445
+ t = Math.abs(Math.abs(comN.cp2.y - comN.p.y) / Math.abs(com.cp2.y - com.p.y));
4446
+ t = Math.min(1, t);
4447
+
4448
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4449
+ com.cp2.y = ptI.y;
4450
+ }
4451
+
4452
+ pathData[i + 1].values = [com.cp1.x, com.cp1.y, com.cp2.x, com.cp2.y, pN.x, pN.y];
4453
+ pathData[i + 1].cp1 = com.cp1;
4454
+ pathData[i + 1].cp2 = com.cp2;
4455
+ pathData[i + 1].p0 = com.p0;
4456
+ pathData[i + 1].p = pN;
4457
+ pathData[i + 1].extreme = true;
4458
+
4459
+ // nullify 1st
4460
+ pathData[i] = null;
4461
+ continue
4462
+
4463
+ }
4464
+
4465
+ }
3859
4466
  }
3860
- return { controlPoints, endPoints };
3861
- };
3862
4467
 
3863
- // start compiling new path data
3864
- let pathDataNew = [];
4468
+ /*
4469
+ */
3865
4470
 
3866
- let closed =
3867
- pathData[pathData.length - 1].type.toLowerCase() === "z" ? true : false;
3868
- if (closed) {
3869
- // add lineto closing space between Z and M
3870
- pathData = addClosePathLineto(pathData);
3871
- // remove Z closepath
3872
- pathData.pop();
3873
4471
  }
3874
4472
 
3875
- // define last point as new M if path isn't closed
3876
- let valuesLast = pathData[pathData.length - 1].values;
3877
- let valuesLastL = valuesLast.length;
3878
- let M = closed
3879
- ? pathData[0]
3880
- : {
3881
- type: "M",
3882
- values: [valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]]
3883
- };
3884
- // starting M stays the same – unless the path is not closed
3885
- pathDataNew.push(M);
4473
+ // remove commands
4474
+ pathData = pathData.filter(Boolean);
4475
+ l = pathData.length;
3886
4476
 
3887
- // reverse path data command order for processing
3888
- pathData.reverse();
3889
- for (let i = 1; i < pathData.length; i++) {
3890
- let com = pathData[i];
3891
- let type = com.type;
3892
- let values = com.values;
3893
- let comPrev = pathData[i - 1];
3894
- let typePrev = comPrev.type;
3895
- let valuesPrev = comPrev.values;
4477
+ /**
4478
+ * refine closing commands
4479
+ */
3896
4480
 
3897
- // get reversed control points and new end coordinates
3898
- let controlPointsPrev = reverseControlPoints(typePrev, valuesPrev).controlPoints;
3899
- let endPoints = reverseControlPoints(type, values).endPoints;
4481
+ let closed = pathData[l - 1].type.toLowerCase() === 'z';
4482
+ let lastIdx = closed ? l - 2 : l - 1;
4483
+ let lastCom = pathData[lastIdx];
4484
+ let penultimateCom = pathData[lastIdx - 1] || null;
4485
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3900
4486
 
3901
- // create new path data
3902
- let newValues = [];
3903
- newValues = [controlPointsPrev, endPoints].flat();
3904
- pathDataNew.push({
3905
- type: typePrev,
3906
- values: newValues.flat()
3907
- });
3908
- }
4487
+ let dec = 8;
4488
+ let lastVals = lastCom.values.slice(-2);
4489
+ let isClosingTo = +lastVals[0].toFixed(dec) === +M.x.toFixed(dec) && +lastVals[1].toFixed(dec) === +M.y.toFixed(dec);
4490
+ let fistExt = pathData[1].type === 'C' && pathData[1].extreme ? pathData[1] : null;
4491
+
4492
+ let diff = getDistAv(lastCom.p0, lastCom.p);
4493
+ let isCose = diff < threshold;
4494
+
4495
+ if (penultimateCom && penultimateCom.type === 'C' && isCose && isClosingTo && fistExt) {
4496
+
4497
+ let comEx = getCombinedByDominant(penultimateCom, lastCom, threshold, tolerance, false);
4498
+
4499
+ if (comEx.length === 1) {
4500
+ pathData[lastIdx - 1] = comEx[0];
4501
+ pathData[lastIdx] = null;
4502
+ pathData = pathData.filter(Boolean);
4503
+ }
3909
4504
 
3910
- // add previously removed Z close path
3911
- if (closed) {
3912
- pathDataNew.push({
3913
- type: "z",
3914
- values: []
3915
- });
3916
4505
  }
3917
4506
 
3918
- return pathDataNew;
4507
+ return pathData
4508
+
3919
4509
  }
3920
4510
 
3921
4511
  function removeEmptySVGEls(svg) {
@@ -3940,7 +4530,7 @@ function cleanUpSVG(svgMarkup, {
3940
4530
  .querySelector("svg");
3941
4531
 
3942
4532
 
3943
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
4533
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
3944
4534
  removeExcludedAttribues(svg, allowed);
3945
4535
 
3946
4536
  let removeEls = ['metadata', 'script'];
@@ -4011,6 +4601,156 @@ function stringifySVG(svg){
4011
4601
  return markup
4012
4602
  }
4013
4603
 
4604
+ function refineRoundedCorners(pathData, {
4605
+ threshold = 0,
4606
+ tolerance = 1
4607
+ } = {}) {
4608
+
4609
+ let l = pathData.length;
4610
+
4611
+ // add fist command
4612
+ let pathDataN = [pathData[0]];
4613
+
4614
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4615
+ let lastOff = isClosed ? 2 : 1;
4616
+
4617
+ let comLast = pathData[l - lastOff];
4618
+ let lastIsLine = comLast.type === 'L';
4619
+ let lastIsBez = comLast.type === 'C';
4620
+ let firstIsLine = pathData[1].type === 'L';
4621
+ let firstIsBez = pathData[1].type === 'C';
4622
+
4623
+ let normalizeClose = isClosed && firstIsBez;
4624
+
4625
+ // normalize closepath to lineto
4626
+ if (normalizeClose) {
4627
+ pathData[l - 1].values = pathData[0].values;
4628
+ pathData[l - 1].type = 'L';
4629
+ lastIsLine = true;
4630
+ }
4631
+
4632
+ for (let i = 1; i < l; i++) {
4633
+ let com = pathData[i];
4634
+ let { type } = com;
4635
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4636
+
4637
+ // search small cubic segments enclosed by linetos
4638
+ if ((type === 'L' && comN && comN.type === 'C') ||
4639
+ (type === 'C' && comN && comN.type === 'L')
4640
+
4641
+ ) {
4642
+ let comL0 = com;
4643
+ let comL1 = null;
4644
+ let comBez = [];
4645
+ let offset = 0;
4646
+
4647
+ // start to end
4648
+ if (i === 1 && firstIsBez && lastIsLine) {
4649
+ comBez = [pathData[1]];
4650
+ comL0 = pathData[l - 1];
4651
+ comL1 = comN;
4652
+
4653
+ }
4654
+
4655
+ // closing corner to start
4656
+ if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4657
+ comL1 = pathData[1];
4658
+ comBez = [pathData[l - lastOff]];
4659
+
4660
+ }
4661
+
4662
+ for (let j = i + 1; j < l; j++) {
4663
+ let comN = pathData[j] ? pathData[j] : null;
4664
+ let comPrev = pathData[j - 1];
4665
+
4666
+ if (comPrev.type === 'C') {
4667
+ comBez.push(comPrev);
4668
+ }
4669
+
4670
+ if (comN.type === 'L' && comPrev.type === 'C') {
4671
+ comL1 = comN;
4672
+ break;
4673
+ }
4674
+ offset++;
4675
+ }
4676
+
4677
+ if (comL1) {
4678
+
4679
+ // linetos
4680
+ let len1 = getDistAv(comL0.p0, comL0.p);
4681
+ let len2 = getDistAv(comL1.p0, comL1.p);
4682
+
4683
+ // bezier
4684
+
4685
+ let comBezLen = comBez.length;
4686
+ let len3 = getDistAv(comBez[0].p0, comBez[comBezLen - 1].p);
4687
+
4688
+ // check concaveness by area sign change
4689
+ let area1 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4690
+ let area2 = getPolygonArea([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], false);
4691
+
4692
+ let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0);
4693
+
4694
+ if (comBez && !signChange && len3 < threshold && len1 > len3 && len2 > len3) {
4695
+
4696
+ let ptQ = checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false);
4697
+ if (ptQ) {
4698
+
4699
+ /*
4700
+ let dist1 = getDistAv(ptQ, comL0.p)
4701
+ let dist2 = getDistAv(ptQ, comL1.p0)
4702
+ let diff = Math.abs(dist1-dist2)
4703
+ let rat = diff/Math.max(dist1, dist2)
4704
+ console.log('rat', rat);
4705
+ */
4706
+
4707
+ /*
4708
+ // adjust curve start and end to meet original
4709
+ let t = 1
4710
+
4711
+ let p0_2 = pointAtT([ptQ, comL0.p], t)
4712
+
4713
+ comL0.p = p0_2
4714
+ comL0.values = [p0_2.x, p0_2.y]
4715
+
4716
+ let p_2 = pointAtT([ptQ, comL1.p0], t)
4717
+
4718
+ comL1.p0 = p_2
4719
+
4720
+ */
4721
+
4722
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4723
+ comQ.p0 = comL0.p;
4724
+ comQ.cp1 = ptQ;
4725
+ comQ.p = comL1.p0;
4726
+
4727
+ // add quadratic command
4728
+ pathDataN.push(comL0, comQ);
4729
+ i += offset;
4730
+ continue;
4731
+ }
4732
+ }
4733
+ }
4734
+ }
4735
+
4736
+ // skip last lineto
4737
+ if (normalizeClose && i === l - 1 && type === 'L') {
4738
+ continue
4739
+ }
4740
+
4741
+ pathDataN.push(com);
4742
+
4743
+ }
4744
+
4745
+ // revert close path normalization
4746
+ if (normalizeClose) {
4747
+ pathDataN.push({ type: 'Z', values: [] });
4748
+ }
4749
+
4750
+ return pathDataN;
4751
+
4752
+ }
4753
+
4014
4754
  function svgPathSimplify(input = '', {
4015
4755
 
4016
4756
  // return svg markup or object
@@ -4029,15 +4769,20 @@ function svgPathSimplify(input = '', {
4029
4769
 
4030
4770
  simplifyBezier = true,
4031
4771
  optimizeOrder = true,
4772
+ removeZeroLength = true,
4032
4773
  removeColinear = true,
4033
4774
  flatBezierToLinetos = true,
4034
4775
  revertToQuadratics = true,
4035
4776
 
4777
+ refineExtremes = true,
4778
+ refineCorners = false,
4779
+
4036
4780
  keepExtremes = true,
4037
4781
  keepCorners = true,
4038
4782
  extrapolateDominant = true,
4039
4783
  keepInflections = false,
4040
4784
  addExtremes = false,
4785
+ removeOrphanSubpaths = false,
4041
4786
 
4042
4787
  // svg path optimizations
4043
4788
  decimals = 3,
@@ -4051,6 +4796,7 @@ function svgPathSimplify(input = '', {
4051
4796
  mergePaths = false,
4052
4797
  removeHidden = true,
4053
4798
  removeUnused = true,
4799
+ shapesToPaths = true,
4054
4800
 
4055
4801
  } = {}) {
4056
4802
 
@@ -4075,24 +4821,43 @@ function svgPathSimplify(input = '', {
4075
4821
  */
4076
4822
 
4077
4823
  // original size
4078
- svgSize = new Blob([input]).size;
4079
4824
 
4080
- // single path
4825
+ svgSize = input.length;
4826
+
4827
+ // mode:0 – single path
4081
4828
  if (!mode) {
4082
4829
  if (inputType === 'pathDataString') {
4083
4830
  d = input;
4084
4831
  } else if (inputType === 'polyString') {
4085
4832
  d = 'M' + input;
4086
4833
  }
4834
+ else if (inputType === 'pathData') {
4835
+ d = input;
4836
+
4837
+ // stringify to compare lengths
4838
+
4839
+ let dStr = d.map(com=>{return `${com.type} ${com.values.join(' ')}`}).join(' ') ;
4840
+ svgSize = dStr.length;
4841
+
4842
+ }
4843
+
4087
4844
  paths.push({ d, el: null });
4088
4845
  }
4089
- // process svg
4846
+ // mode:1 – process complete svg DOM
4090
4847
  else {
4091
4848
 
4092
4849
  let returnDom = true;
4093
4850
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4094
4851
  );
4095
4852
 
4853
+ if (shapesToPaths) {
4854
+ let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
4855
+ shapes.forEach(shape => {
4856
+ let path = shapeElToPath(shape);
4857
+ shape.replaceWith(path);
4858
+ });
4859
+ }
4860
+
4096
4861
  // collect paths
4097
4862
  let pathEls = svg.querySelectorAll('path');
4098
4863
  pathEls.forEach(path => {
@@ -4102,6 +4867,7 @@ function svgPathSimplify(input = '', {
4102
4867
 
4103
4868
  /**
4104
4869
  * process all paths
4870
+ * try simplifications and removals
4105
4871
  */
4106
4872
 
4107
4873
  // SVG optimization options
@@ -4114,34 +4880,35 @@ function svgPathSimplify(input = '', {
4114
4880
  // combinded path data for SVGs with mergePaths enabled
4115
4881
  let pathData_merged = [];
4116
4882
 
4117
- paths.forEach(path => {
4118
- let { d, el } = path;
4883
+ for (let i = 0, l = paths.length; l && i < l; i++) {
4119
4884
 
4120
- let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4885
+ let path = paths[i];
4886
+ let { d, el } = path;
4121
4887
 
4122
- // create clone for fallback
4123
- let pathData = JSON.parse(JSON.stringify(pathDataO));
4888
+ let pathData = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4124
4889
 
4125
4890
  // count commands for evaluation
4126
- let comCount = pathDataO.length;
4891
+ let comCount = pathData.length;
4892
+
4893
+ if (removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4127
4894
 
4128
4895
  /**
4129
4896
  * get sub paths
4130
4897
  */
4131
4898
  let subPathArr = splitSubpaths(pathData);
4899
+ let lenSub = subPathArr.length;
4132
4900
 
4133
4901
  // cleaned up pathData
4134
- let pathDataArrN = [];
4135
4902
 
4136
- for (let i = 0, l = subPathArr.length; i < l; i++) {
4903
+ // reset array
4904
+ let pathDataFlat = [];
4137
4905
 
4138
- let pathDataSub = subPathArr[i];
4906
+ for (let i = 0; i < lenSub; i++) {
4139
4907
 
4140
- // try simplification in reversed order
4141
- if (reverse) pathDataSub = reversePathData(pathDataSub);
4908
+ let pathDataSub = subPathArr[i];
4142
4909
 
4143
4910
  // remove zero length linetos
4144
- if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4911
+ if (removeColinear || removeZeroLength) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4145
4912
 
4146
4913
  // add extremes
4147
4914
 
@@ -4151,50 +4918,63 @@ function svgPathSimplify(input = '', {
4151
4918
  // sort to top left
4152
4919
  if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4153
4920
 
4154
- // remove colinear/flat
4155
- if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4921
+ // Preprocessing: remove colinear - ignore flat beziers (removed later)
4922
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, { tolerance, flatBezierToLinetos: false });
4156
4923
 
4157
4924
  // analyze pathdata to add info about signicant properties such as extremes, corners
4158
4925
  let pathDataPlus = analyzePathData(pathDataSub);
4159
4926
 
4160
4927
  // simplify beziers
4161
4928
  let { pathData, bb, dimA } = pathDataPlus;
4162
-
4163
4929
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4164
4930
 
4931
+ // refine extremes
4932
+ if (refineExtremes) {
4933
+ let thresholdEx = (bb.width + bb.height) / 2 * 0.05;
4934
+ pathData = refineAdjacentExtremes(pathData, { threshold: thresholdEx, tolerance });
4935
+ }
4936
+
4165
4937
  // cubic to arcs
4166
4938
  if (cubicToArc) {
4167
4939
 
4168
- let thresh = 3;
4940
+ let thresh = 1;
4169
4941
 
4170
- pathData.forEach((com, c) => {
4942
+ for(let c=0, l=pathData.length; c<l; c++){
4943
+ let com = pathData[c];
4171
4944
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4172
4945
  if (type === 'C') {
4173
4946
 
4174
4947
  let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4175
4948
  if (comA.isArc) pathData[c] = comA.com;
4176
-
4177
4949
  }
4178
- });
4950
+ }
4179
4951
 
4180
4952
  // combine adjacent cubics
4181
4953
  pathData = combineArcs(pathData);
4182
4954
 
4183
4955
  }
4184
4956
 
4957
+ // post processing: remove flat beziers
4958
+ if (removeColinear && flatBezierToLinetos) {
4959
+ pathData = pathDataRemoveColinear(pathData, { tolerance, flatBezierToLinetos });
4960
+ }
4961
+
4962
+ // refine corners
4963
+ if(refineCorners){
4964
+ let threshold = (bb.width + bb.height) / 2 * 0.1;
4965
+ pathData = refineRoundedCorners(pathData, { threshold, tolerance });
4966
+
4967
+ }
4968
+
4185
4969
  // simplify to quadratics
4186
4970
  if (revertToQuadratics) {
4187
- pathData.forEach((com, c) => {
4971
+ for(let c=0, l=pathData.length; c<l; c++){
4972
+ let com = pathData[c];
4188
4973
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4189
4974
  if (type === 'C') {
4190
4975
 
4191
4976
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4192
4977
  if (comQ.type === 'Q') {
4193
- /*
4194
- comQ.p0 = com.p0
4195
- comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
4196
- comQ.p = com.p
4197
- */
4198
4978
  comQ.extreme = com.extreme;
4199
4979
  comQ.corner = com.corner;
4200
4980
  comQ.dimA = com.dimA;
@@ -4202,20 +4982,25 @@ function svgPathSimplify(input = '', {
4202
4982
  pathData[c] = comQ;
4203
4983
  }
4204
4984
  }
4205
- });
4985
+ }
4206
4986
  }
4207
4987
 
4208
4988
  // optimize close path
4209
4989
  if (optimizeOrder) pathData = optimizeClosePath(pathData);
4210
4990
 
4211
- // poly
4212
-
4213
4991
  // update
4214
- pathDataArrN.push(pathData);
4992
+ pathDataFlat.push(...pathData);
4993
+
4215
4994
  }
4216
4995
 
4217
4996
  // flatten compound paths
4218
- pathData = pathDataArrN.flat();
4997
+ pathData = pathDataFlat;
4998
+
4999
+ if (autoAccuracy) {
5000
+ decimals = detectAccuracy(pathData);
5001
+ pathOptions.decimals = decimals;
5002
+
5003
+ }
4219
5004
 
4220
5005
  // collect for merged svg paths
4221
5006
  if (el && mergePaths) {
@@ -4224,24 +5009,19 @@ function svgPathSimplify(input = '', {
4224
5009
  // single output
4225
5010
  else {
4226
5011
 
4227
- /**
4228
- * detect accuracy
4229
- */
4230
- if (autoAccuracy) {
4231
- decimals = detectAccuracy(pathData);
4232
- }
4233
-
4234
5012
  // optimize path data
4235
5013
  pathData = convertPathData(pathData, pathOptions);
4236
5014
 
4237
5015
  // remove zero-length segments introduced by rounding
4238
- pathData = removeZeroLengthLinetos_post(pathData);
5016
+ pathData = removeZeroLengthLinetos(pathData);
4239
5017
 
4240
5018
  // compare command count
4241
5019
  let comCountS = pathData.length;
4242
5020
 
4243
5021
  let dOpt = pathDataToD(pathData, minifyD);
4244
- svgSizeOpt = new Blob([dOpt]).size;
5022
+
5023
+ svgSizeOpt = dOpt.length;
5024
+
4245
5025
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4246
5026
 
4247
5027
  path.d = dOpt;
@@ -4257,19 +5037,19 @@ function svgPathSimplify(input = '', {
4257
5037
  // apply new path for svgs
4258
5038
  if (el) el.setAttribute('d', dOpt);
4259
5039
  }
4260
- });
4261
-
5040
+ }
4262
5041
  /**
4263
5042
  * stringify new SVG
4264
5043
  */
4265
5044
  if (mode) {
4266
5045
 
4267
5046
  if (pathData_merged.length) {
5047
+
4268
5048
  // optimize path data
4269
5049
  let pathData = convertPathData(pathData_merged, pathOptions);
4270
5050
 
4271
5051
  // remove zero-length segments introduced by rounding
4272
- pathData = removeZeroLengthLinetos_post(pathData);
5052
+ pathData = removeZeroLengthLinetos(pathData);
4273
5053
 
4274
5054
  let dOpt = pathDataToD(pathData, minifyD);
4275
5055
 
@@ -4287,7 +5067,8 @@ function svgPathSimplify(input = '', {
4287
5067
  }
4288
5068
 
4289
5069
  svg = stringifySVG(svg);
4290
- svgSizeOpt = new Blob([svg]).size;
5070
+
5071
+ svgSizeOpt = svg.length;
4291
5072
 
4292
5073
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4293
5074
 
@@ -4308,109 +5089,16 @@ function svgPathSimplify(input = '', {
4308
5089
 
4309
5090
  }
4310
5091
 
4311
- function simplifyPathDataCubic(pathData, {
4312
- keepExtremes = true,
4313
- keepInflections = true,
4314
- keepCorners = true,
4315
- extrapolateDominant = true,
4316
- tolerance = 1,
4317
- reverse = false
4318
- } = {}) {
4319
-
4320
- let pathDataN = [pathData[0]];
4321
-
4322
- for (let i = 2, l = pathData.length; l && i <= l; i++) {
4323
- let com = pathData[i - 1];
4324
- let comN = i < l ? pathData[i] : null;
4325
- let typeN = comN?.type || null;
4326
-
4327
- let isDirChange = com?.directionChange || null;
4328
- let isDirChangeN = comN?.directionChange || null;
4329
-
4330
- let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
4331
-
4332
- // next is also cubic
4333
- if (type === 'C' && typeN === 'C') {
4334
-
4335
- // cannot be combined as crossing extremes or corners
4336
- if (
4337
- (keepInflections && isDirChangeN) ||
4338
- (keepCorners && corner) ||
4339
- (!isDirChange && keepExtremes && extreme)
4340
- ) {
4341
-
4342
- pathDataN.push(com);
4343
- }
4344
-
4345
- // try simplification
4346
- else {
4347
-
4348
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4349
- let error = 0;
4350
-
4351
- // combining successful! try next segment
4352
- if (combined.length === 1) {
4353
- com = combined[0];
4354
- let offset = 1;
4355
-
4356
- // add cumulative error to prevent distortions
4357
- error += com.error;
4358
-
4359
- // find next candidates
4360
- for (let n = i + 1; error < tolerance && n < l; n++) {
4361
- let comN = pathData[n];
4362
- if (comN.type !== 'C' ||
4363
- (
4364
- (keepInflections && comN.directionChange) ||
4365
- (keepCorners && com.corner) ||
4366
- (keepExtremes && com.extreme)
4367
- )
4368
- ) {
4369
- break
4370
- }
4371
-
4372
- let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4373
- if (combined.length === 1) {
4374
- // add cumulative error to prevent distortions
4375
-
4376
- error += combined[0].error * 0.5;
4377
- offset++;
4378
- }
4379
- com = combined[0];
4380
- }
4381
-
4382
- pathDataN.push(com);
4383
-
4384
- if (i < l) {
4385
- i += offset;
4386
- }
4387
-
4388
- } else {
4389
- pathDataN.push(com);
4390
- }
4391
- }
4392
-
4393
- } // end of bezier command
4394
-
4395
- // other commands
4396
- else {
4397
- pathDataN.push(com);
4398
- }
4399
-
4400
- } // end command loop
4401
-
4402
- // reverse back
4403
- if (reverse) pathDataN = reversePathData(pathDataN);
4404
-
4405
- return pathDataN
4406
- }
4407
-
4408
5092
  const {
4409
5093
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4410
5094
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4411
5095
  } = Math;
4412
5096
 
4413
- // just for visual debugging
5097
+ /*
5098
+ import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5099
+ export {XMLSerializerPoly as XMLSerializerPoly};
5100
+ export {DOMParserPoly as DOMParserPoly};
5101
+ */
4414
5102
 
4415
5103
  // IIFE
4416
5104
  if (typeof window !== 'undefined') {