svg-path-simplify 0.4.3 → 0.4.5

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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +2 -1
  3. package/dist/svg-path-simplify.esm.js +1670 -509
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1671 -508
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +936 -463
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/index.html +60 -20
  11. package/package.json +1 -1
  12. package/src/constants.js +4 -0
  13. package/src/detect_input.js +47 -29
  14. package/src/index.js +8 -0
  15. package/src/pathData_simplify_cubic.js +46 -18
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +81 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +14 -4
  20. package/src/svg-getAttributes.js +5 -3
  21. package/src/svgii/convert_units.js +1 -1
  22. package/src/svgii/geometry.js +140 -2
  23. package/src/svgii/geometry_bbox_element.js +1 -1
  24. package/src/svgii/geometry_deduceRadius.js +116 -27
  25. package/src/svgii/geometry_length.js +18 -2
  26. package/src/svgii/pathData_analyze.js +18 -0
  27. package/src/svgii/pathData_convert.js +188 -88
  28. package/src/svgii/pathData_fix_directions.js +10 -18
  29. package/src/svgii/pathData_reorder.js +123 -16
  30. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  31. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  32. package/src/svgii/poly_normalize.js +9 -8
  33. package/src/svgii/rounding.js +112 -80
  34. package/src/svgii/svg_cleanup.js +75 -22
  35. package/src/svgii/svg_cleanup_convertPathLength.js +27 -15
  36. package/src/svgii/svg_cleanup_normalize_transforms.js +1 -1
  37. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  38. package/src/svgii/svg_el_parse_style_props.js +13 -10
  39. package/src/svgii/svg_validate.js +220 -0
  40. package/tests/testSVG.js +14 -1
  41. package/src/svgii/pathData_refine_round.js +0 -222
@@ -1,47 +1,259 @@
1
+ const rad2Deg = 180/Math.PI;
2
+ const deg2rad = Math.PI/180;
3
+
4
+ function validateSVG(markup, allowed = {}) {
5
+ allowed = {
6
+ ...{
7
+
8
+ useElsNested: 5000,
9
+ hasScripts: false,
10
+ hasEntity: false,
11
+ fileSizeKB: 10000,
12
+ isSymbolSprite: false,
13
+ isSvgFont: false
14
+ },
15
+ ...allowed
16
+ };
17
+
18
+ let fileReport = analyzeSVG(markup, allowed);
19
+ let isValid = true;
20
+ let log = [];
21
+
22
+ if (!fileReport.hasEls) {
23
+ log.push("no elements");
24
+ isValid = false;
25
+ }
26
+
27
+ if (Object.keys(fileReport).length) {
28
+ if (fileReport.isBillionLaugh === true) {
29
+ log.push(`suspicious: might contain billion laugh attack`);
30
+ isValid = false;
31
+ }
32
+
33
+ for (let key in allowed) {
34
+ let val = allowed[key];
35
+ let valRep = fileReport[key];
36
+ if (typeof val === "number" && valRep > val) {
37
+ log.push(`allowed "${key}" exceeded: ${valRep} / ${val} `);
38
+ isValid = false;
39
+ }
40
+ if (valRep === true && val === false) {
41
+ log.push(`not allowed: "${key}" `);
42
+ isValid = false;
43
+ }
44
+ }
45
+ } else {
46
+ isValid = false;
47
+ }
48
+
49
+ /*
50
+ if (!isValid) {
51
+ log = ["SVG not valid"].concat(log);
52
+
53
+ if (Object.keys(fileReport).length) {
54
+ console.warn(fileReport);
55
+ }
56
+ }
57
+ */
58
+
59
+ return { isValid, log, fileReport };
60
+ }
61
+
62
+ function analyzeSVG(markup, allowed = {}) {
63
+ markup = markup.trim();
64
+ let doc, svg;
65
+ let fileSizeKB = +(markup.length / 1024).toFixed(3);
66
+
67
+ let fileReport = {
68
+ totalEls: 1,
69
+ hasEls: true,
70
+ hasDefs: false,
71
+ geometryEls: [],
72
+ useEls: 0,
73
+ useElsNested: 0,
74
+ nonsensePaths: 0,
75
+ isSuspicious: false,
76
+ isBillionLaugh: false,
77
+ hasScripts: false,
78
+ hasPrologue: false,
79
+ hasEntity: false,
80
+ isPathData:false,
81
+ fileSizeKB,
82
+ hasXmlns: markup.includes("http://www.w3.org/2000/svg"),
83
+ isSymbolSprite: false,
84
+ isSvgFont: markup.includes("<glyph>")
85
+ };
86
+
87
+ let maxNested = allowed.useElsNested ? allowed.useElsNested : 2000;
88
+
89
+ /**
90
+ * analyze nestes use references
91
+ */
92
+ const countUseRefs = (useEls, maxNested = 2000) => {
93
+ let nestedCount = 0;
94
+
95
+ for (let i = 0; i < useEls.length && nestedCount < maxNested; i++) {
96
+ let use = useEls[i];
97
+ let refId = use.getAttribute("xlink:href")
98
+ ? use.getAttribute("xlink:href")
99
+ : use.getAttribute("href");
100
+ refId = refId ? refId.replace("#", "") : "";
101
+
102
+ use.setAttribute("href", "#" + refId);
103
+
104
+ let refEl = svg.getElementById(refId);
105
+ let nestedUse = refEl.querySelectorAll("use");
106
+ let nestedUseLength = nestedUse.length;
107
+ nestedCount += nestedUseLength;
108
+
109
+ // query nested use references
110
+ for (let n = 0; n < nestedUse.length && nestedCount < maxNested; n++) {
111
+ let nested = nestedUse[n];
112
+ let id1 = nested.getAttribute("href").replace("#", "");
113
+ let refEl1 = svg.getElementById(id1);
114
+ let nestedUse1 = refEl1.querySelectorAll("use");
115
+ nestedCount += nestedUse1.length;
116
+ }
117
+ }
118
+ fileReport.useElsNested = nestedCount;
119
+ return nestedCount;
120
+ };
121
+ let hasEntity = /\<\!ENTITY/gi.test(markup);
122
+ let hasScripts = /\<script/gi.test(markup) ? true : false;
123
+ let hasUse = /\<use/gi.test(markup) ? true : false;
124
+ let hasEls = /[\<path|\<polygon|\<polyline|\<rect|\<circle|\<ellipse|\<line|\<text|\<foreignObject]/gi.test(markup);
125
+ let hasDefs = /[\<filter|\<linearGradient|\<radialGradient|\<pattern|\<animate|\<animateMotion|\<animateTransform|\<clipPath|\<mask|\<symbol|\<marker]/gi.test(markup);
126
+
127
+ let isPathData = (markup.startsWith('M') || markup.startsWith('m')) && !/[\<svg|\<\/svg]/gi.test(markup);
128
+ fileReport.isPathData = isPathData;
129
+
130
+ // seems OK
131
+ if (!hasEntity && !hasUse && !hasScripts && (hasEls || hasDefs) && fileSizeKB < allowed.fileSizeKB) {
132
+ fileReport.hasEls = hasEls;
133
+ fileReport.hasDefs = hasDefs;
134
+
135
+ return fileReport
136
+ }
137
+
138
+ // Contains xml entity definition: highly suspicious - stop parsing!
139
+ if (allowed.hasEntity === false && hasEntity) {
140
+ fileReport.hasEntity = true;
141
+
142
+ }
143
+
144
+ /**
145
+ * sanitizing for parsing:
146
+ * remove xml prologue and comments
147
+ */
148
+ markup = markup
149
+ .replace(/\<\?xml.+\?\>|\<\!DOCTYPE.+]\>/g, "")
150
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, "");
151
+
152
+ /**
153
+ * Try to parse svg:
154
+ * invalid svg will return false via "catch"
155
+ */
156
+ try {
157
+
158
+ doc = new DOMParser().parseFromString(markup, "text/html");
159
+ svg = doc.querySelector("svg");
160
+
161
+ // paths containing only a M command
162
+ let nonsensePaths = svg.querySelectorAll('path[d="M0,0"], path[d="M0 0"]').length;
163
+ let useEls = svg.querySelectorAll("use").length;
164
+
165
+ // create analyzing object
166
+ fileReport.totalEls = svg.querySelectorAll("*").length;
167
+ fileReport.geometryEls = svg.querySelectorAll(
168
+ "path, rect, circle, ellipse, polygon, polyline, line"
169
+ ).length;
170
+
171
+ fileReport.hasScripts = hasScripts;
172
+ fileReport.useEls = useEls;
173
+ fileReport.nonsensePaths = nonsensePaths;
174
+ fileReport.isSuspicious = false;
175
+ fileReport.isBillionLaugh = false;
176
+ fileReport.hasXmlns = svg.getAttribute("xmlns")
177
+ ? svg.getAttribute("xmlns") === "http://www.w3.org/2000/svg"
178
+ ? true
179
+ : false
180
+ : false;
181
+ fileReport.isSymbolSprite =
182
+ svg.querySelectorAll("symbol").length &&
183
+ svg.querySelectorAll("use").length === 0
184
+ ? true
185
+ : false;
186
+ fileReport.isSvgFont = svg.querySelectorAll("glyph").length ? true : false;
187
+
188
+ let totalEls = fileReport.totalEls;
189
+ let totalUseEls = fileReport.useEls;
190
+ let usePercentage = (100 / totalEls) * totalUseEls;
191
+
192
+ // if percentage of use elements is higher than 75% - suspicious
193
+ if (usePercentage > 75) {
194
+ fileReport.isSuspicious = true;
195
+
196
+ // check nested use references
197
+ let nestedCount = countUseRefs(svg.querySelectorAll("use"), maxNested);
198
+ if (nestedCount >= maxNested) {
199
+ fileReport.isBillionLaugh = true;
200
+ }
201
+ }
202
+
203
+ return fileReport;
204
+ } catch {
205
+ // svg file has malformed markup
206
+ console.warn("svg could not be parsed");
207
+ return false;
208
+ }
209
+ }
210
+
1
211
  function detectInputType(input) {
2
- let type = 'string';
3
- /*
4
- if (input instanceof HTMLImageElement) return "img";
5
- if (input instanceof SVGElement) return "svg";
6
- if (input instanceof HTMLCanvasElement) return "canvas";
7
- if (input instanceof File) return "file";
8
- if (input instanceof ArrayBuffer) return "buffer";
9
- if (input instanceof Blob) return "blob";
10
- */
212
+ let log = '';
213
+ let isValid = true;
214
+
215
+ let result = {
216
+ inputType:'',
217
+ isValid:true,
218
+ fileReport:{},
219
+ };
220
+
11
221
  if (Array.isArray(input)) {
12
222
 
223
+ result.inputType = "array";
224
+
13
225
  // nested array
14
226
  if (Array.isArray(input[0])) {
15
227
 
16
228
  if (input[0].length === 2) {
17
229
 
18
- return 'polyArray'
230
+ result.inputType = 'polyArray';
19
231
  }
20
232
 
21
233
  else if (Array.isArray(input[0][0]) && input[0][0].length === 2) {
22
234
 
23
- return 'polyComplexArray'
235
+ result.inputType = 'polyComplexArray';
24
236
  }
25
237
  else if (input[0][0].x !== undefined && input[0][0].y !== undefined) {
26
238
 
27
- return 'polyComplexObjectArray'
239
+ result.inputType = 'polyComplexObjectArray';
28
240
  }
241
+
29
242
  }
30
243
 
31
244
  // is point array
32
245
  else if (input[0].x !== undefined && input[0].y !== undefined) {
33
246
 
34
- return 'polyObjectArray'
247
+ result.inputType = 'polyObjectArray';
35
248
  }
36
249
 
37
250
  // path data array
38
251
  else if (input[0]?.type && input[0]?.values
39
252
  ) {
40
- return "pathData"
41
-
253
+ result.inputType = "pathData";
42
254
  }
43
255
 
44
- return "array";
256
+ return result;
45
257
  }
46
258
 
47
259
  if (typeof input === "string") {
@@ -53,36 +265,48 @@ function detectInputType(input) {
53
265
  let isJson = isNumberJson(input);
54
266
 
55
267
  if (isSVG) {
56
- type = 'svgMarkup';
268
+ let validate = validateSVG(input);
269
+ ({isValid, log} = validate) ;
270
+ if(!isValid){
271
+
272
+ result.inputType = 'invalid';
273
+ result.isValid=false,
274
+
275
+ result.log = log;
276
+ }else {
277
+ result.inputType = 'svgMarkup';
278
+ }
279
+
280
+ result.fileReport = validate.fileReport;
281
+
57
282
  }
58
283
 
59
284
  else if (isJson) {
60
- type = 'json';
285
+ result.inputType = 'json';
61
286
  }
62
287
 
63
288
  else if (isSymbol) {
64
- type = 'symbol';
289
+ result.inputType = 'symbol';
65
290
  }
66
291
  else if (isPathData) {
67
- type = 'pathDataString';
292
+ result.inputType = 'pathDataString';
68
293
  }
69
294
  else if (isPolyString) {
70
- type = 'polyString';
295
+ result.inputType = 'polyString';
71
296
  }
72
297
 
73
298
  else {
74
299
  let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
75
300
  let dataUrl = input.startsWith('data:image');
76
- type = url || dataUrl ? "url" : "string";
301
+ result.inputType = url || dataUrl ? "url" : "string";
77
302
  }
78
303
 
79
- return type
304
+ return result
80
305
  }
81
306
 
82
- type = typeof input;
83
- let constructor = input.constructor.name;
307
+ result.inputType = (input.constructor.name || typeof input ).toLowerCase();
84
308
 
85
- return (constructor || type).toLowerCase();
309
+ return result;
86
310
  }
87
311
 
88
312
  function isNumberJson(str) {
@@ -100,9 +324,6 @@ function isNumberJson(str) {
100
324
 
101
325
  }
102
326
 
103
- const rad2Deg = 180/Math.PI;
104
- const deg2rad = Math.PI/180;
105
-
106
327
  function renderPoint(
107
328
  svg,
108
329
  coords,
@@ -130,18 +351,6 @@ function renderPoint(
130
351
  }
131
352
  }
132
353
 
133
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
134
-
135
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
136
-
137
- if (render) {
138
- svg.insertAdjacentHTML("beforeend", path);
139
- } else {
140
- return path;
141
- }
142
-
143
- }
144
-
145
354
  /*
146
355
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
147
356
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -358,6 +567,7 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
358
567
  let t1 = 1 - t;
359
568
 
360
569
  // cubic beziers
570
+ /*
361
571
  if (isCubic) {
362
572
  pt = {
363
573
  x:
@@ -373,11 +583,29 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
373
583
  };
374
584
 
375
585
  }
586
+ */
587
+
588
+ if (isCubic) {
589
+ pt = {
590
+ x:
591
+ t1 * t1 * t1 * p0.x +
592
+ 3 * t1 * t1 * t * cp1.x +
593
+ 3 * t1 * t * t * cp2.x +
594
+ t * t * t * p.x,
595
+ y:
596
+ t1 * t1 * t1 * p0.y +
597
+ 3 * t1 * t1 * t * cp1.y +
598
+ 3 * t1 * t * t * cp2.y +
599
+ t * t * t * p.y,
600
+ };
601
+
602
+ }
603
+
376
604
  // quadratic beziers
377
605
  else {
378
606
  pt = {
379
- x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
380
- y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
607
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t * t * p.x,
608
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t * t * p.y,
381
609
  };
382
610
  }
383
611
 
@@ -693,12 +921,6 @@ function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false }
693
921
  return tArr;
694
922
  }
695
923
 
696
- /**
697
- * based on Nikos M.'s answer
698
- * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
699
- * https://stackoverflow.com/questions/87734/#75031511
700
- * See also: https://github.com/foo123/Geometrize
701
- */
702
924
  function getArcExtemes(p0, values) {
703
925
  // compute point on ellipse from angle around ellipse (theta)
704
926
  const arc = (theta, cx, cy, rx, ry, alpha) => {
@@ -1424,6 +1646,36 @@ function splitCommandAtTValues(p0, values, tArr, returnCommand = true) {
1424
1646
  return segmentPoints;
1425
1647
  }
1426
1648
 
1649
+ /**
1650
+ * round path data
1651
+ * either by explicit decimal value or
1652
+ * based on suggested accuracy in path data
1653
+ */
1654
+ function roundPathData(pathData, decimalsGlobal = -1) {
1655
+
1656
+ if (decimalsGlobal < 0) return pathData;
1657
+
1658
+ let len = pathData.length;
1659
+ let decimals = decimalsGlobal;
1660
+ let decimalsArc = decimals < 3 ? decimals + 2 : decimals;
1661
+
1662
+ for (let c = 0; c < len; c++) {
1663
+ let com = pathData[c];
1664
+ let { type, values } = com;
1665
+ let valLen = values.length;
1666
+ if (!valLen) continue
1667
+
1668
+ let isArc = type.toLowerCase() === 'a';
1669
+
1670
+ for (let v = 0; v < valLen; v++) {
1671
+ // allow higher accuracy for arc radii (... it's always arcs)
1672
+ pathData[c].values[v] = isArc && v < 2 ? roundTo(values[v], decimalsArc) : roundTo(values[v], decimals);
1673
+ }
1674
+ }
1675
+
1676
+ return pathData;
1677
+ }
1678
+
1427
1679
  function detectAccuracy(pathData) {
1428
1680
  let dims = [];
1429
1681
 
@@ -1437,59 +1689,73 @@ function detectAccuracy(pathData) {
1437
1689
 
1438
1690
  dimA = dimA ? dimA : getDistManhattan(p0, p);
1439
1691
 
1440
- if (dimA) dims.push(dimA);
1692
+ if (dimA) dims.push(+dimA.toFixed(8));
1441
1693
 
1442
1694
  }
1443
1695
 
1444
1696
  }
1445
1697
 
1446
- let dim_min = dims.sort();
1698
+ dims = dims.sort();
1699
+ let len = dims.length;
1700
+ let dim_mid = dims[Math.floor(len*0.5)];
1701
+
1702
+ // smallest 25% of values
1703
+ let idx_q = Math.ceil(len*0.25);
1704
+ let dims_min = dims.slice(0, idx_q);
1705
+
1706
+ // average smallest values with mid value
1707
+ let dim_min = ((dims_min.reduce((a, b) => a + b, 0) / idx_q) + dim_mid) * 0.5;
1708
+
1709
+ let threshold = 75;
1710
+ let decimalsAuto = dim_min > threshold * 1.5 ? 0 : Math.floor(threshold / dim_min).toString().length;
1447
1711
 
1448
- let sliceIdx = Math.ceil(dim_min.length / 6);
1712
+ // clamp
1713
+ return Math.min(Math.max(0, decimalsAuto), 8)
1714
+
1715
+ /*
1716
+ let dim_min = dims.sort()
1717
+
1718
+ let dim_mid = dim_min[Math.floor(dim_min.length*0.5)]
1719
+
1720
+ let sliceIdx = Math.ceil(dim_min.length / 4);
1449
1721
  dim_min = dim_min.slice(0, sliceIdx);
1450
1722
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1451
1723
 
1452
- let threshold = 75;
1453
- let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
1724
+ // average with mid value
1725
+ minVal = (minVal+dim_mid)*0.5
1726
+
1727
+ let threshold = 75
1728
+ let decimalsAuto = minVal > threshold * 1.5 ? 0 : Math.floor(threshold / minVal).toString().length
1454
1729
 
1455
1730
  // clamp
1456
1731
  return Math.min(Math.max(0, decimalsAuto), 8)
1732
+ */
1457
1733
 
1458
1734
  }
1459
1735
 
1460
- function roundTo(num = 0, decimals = 3) {
1461
- if(decimals<=-1) return num;
1462
- if (!decimals) return Math.round(num);
1463
- let factor = 10 ** decimals;
1464
- return Math.round(num * factor) / factor;
1465
- }
1466
-
1467
1736
  /**
1468
- * round path data
1469
- * either by explicit decimal value or
1470
- * based on suggested accuracy in path data
1737
+ * rounding helper
1738
+ * allows for quantized rounding
1739
+ * e.g 0.5 decimals s
1471
1740
  */
1472
- function roundPathData(pathData, decimalsGlobal = -1) {
1473
-
1474
- if (decimalsGlobal < 0) return pathData;
1475
-
1476
- let len = pathData.length;
1477
-
1478
- let decimals = decimalsGlobal;
1741
+ function roundTo(num = 0, decimals = 3) {
1742
+ if (decimals < 0) return num;
1743
+ // Normal integer rounding
1744
+ if (!decimals) return Math.round(num);
1479
1745
 
1480
- for (let c = 0; c < len; c++) {
1481
- let com = pathData[c];
1482
- let {values} = com;
1746
+ // stepped rounding
1747
+ let intPart = Math.floor(decimals);
1483
1748
 
1484
- let valLen = values.length;
1485
- if (!valLen) continue
1749
+ if (intPart !== decimals) {
1750
+ let f = +(decimals - intPart).toFixed(2);
1751
+ f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
1486
1752
 
1487
- for (let v = 0; v < valLen; v++) {
1488
- pathData[c].values[v] = roundTo(values[v], decimals);
1489
- }
1753
+ let step = 10 ** -intPart * f;
1754
+ return +(Math.round(num / step) * step).toFixed(8);
1490
1755
  }
1491
1756
 
1492
- return pathData;
1757
+ let factor = 10 ** decimals;
1758
+ return Math.round(num * factor) / factor;
1493
1759
  }
1494
1760
 
1495
1761
  /**
@@ -2220,7 +2486,7 @@ function simplifyPathDataCubic(pathData, {
2220
2486
  error += com.error;
2221
2487
 
2222
2488
  // find next candidates
2223
- for (let n = i + 1; error < tolerance && n < l; n++) {
2489
+ for (let n = i + offset; error < tolerance && n < l; n++) {
2224
2490
  let comN = pathData[n];
2225
2491
 
2226
2492
  if (comN.type !== 'C' ||
@@ -2230,6 +2496,7 @@ function simplifyPathDataCubic(pathData, {
2230
2496
  (keepExtremes && com.extreme)
2231
2497
  )
2232
2498
  ) {
2499
+
2233
2500
  break
2234
2501
  }
2235
2502
 
@@ -2237,6 +2504,7 @@ function simplifyPathDataCubic(pathData, {
2237
2504
 
2238
2505
  // failure - could not be combined - exit loop
2239
2506
  if (combined.length > 1) {
2507
+
2240
2508
  break
2241
2509
  }
2242
2510
 
@@ -2250,6 +2518,7 @@ function simplifyPathDataCubic(pathData, {
2250
2518
 
2251
2519
  // return combined
2252
2520
  com = combined[0];
2521
+
2253
2522
  }
2254
2523
 
2255
2524
  pathDataN.push(com);
@@ -2299,9 +2568,9 @@ function combineCubicPairs(com1, com2, {
2299
2568
  let comS = getExtrapolatedCommand(com1, com2, t);
2300
2569
 
2301
2570
  // test new point-at-t against original mid segment starting point
2302
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
2571
+ let ptI = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
2303
2572
 
2304
- let dist0 = getDistManhattan(com1.p, pt);
2573
+ let dist0 = getDistManhattan(com1.p, ptI);
2305
2574
  let dist1 = 0, dist2 = 0;
2306
2575
  let close = dist0 < maxDist;
2307
2576
  let success = false;
@@ -2316,29 +2585,40 @@ function combineCubicPairs(com1, com2, {
2316
2585
  * to prevent distortions
2317
2586
  */
2318
2587
 
2319
- // 2nd segment mid
2320
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
2588
+ // 1st segment mid
2589
+ let ptM_seg1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
2321
2590
 
2322
- // simplified path
2323
- let t3 = (1 + t) * 0.5;
2324
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
2325
- dist1 = getDistManhattan(pt_2, ptS_2);
2591
+ let t2 = t * 0.5;
2592
+ // combined interpolated mid point
2593
+ let ptI_seg1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
2594
+ dist1 = getDistManhattan(ptM_seg1, ptI_seg1);
2326
2595
 
2327
2596
  error += dist1;
2328
2597
 
2329
2598
  if (dist1 < maxDist) {
2330
2599
 
2331
- // 1st segment mid
2332
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
2600
+ // 2nd segment mid
2601
+ let ptM_seg2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
2333
2602
 
2334
- let t2 = t * 0.5;
2335
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
2336
- dist2 = getDistManhattan(pt_1, ptS_1);
2603
+ // simplified path
2604
+ let t3 = (1 + t) * 0.5;
2605
+ let ptI_seg2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
2606
+ dist2 = getDistManhattan(ptM_seg2, ptI_seg2);
2337
2607
 
2338
2608
  error += dist2;
2339
2609
 
2340
2610
  if (error < maxDist) success = true;
2341
2611
 
2612
+ /*
2613
+ renderPoint(markers, ptM_seg1, 'cyan')
2614
+ renderPoint(markers, pt, 'orange', '1.5%', '1')
2615
+ renderPoint(markers, ptM_seg2, 'orange')
2616
+
2617
+ renderPoint(markers, com1.p, 'green')
2618
+
2619
+ renderPoint(markers, ptI_seg1, 'purple')
2620
+ */
2621
+
2342
2622
  }
2343
2623
 
2344
2624
  } // end 1st try
@@ -2352,11 +2632,19 @@ function combineCubicPairs(com1, com2, {
2352
2632
 
2353
2633
  comS.dimA = getDistManhattan(comS.p0, comS.p);
2354
2634
  comS.type = 'C';
2635
+
2355
2636
  comS.extreme = com2.extreme;
2356
2637
  comS.directionChange = com2.directionChange;
2357
-
2358
2638
  comS.corner = com2.corner;
2359
2639
 
2640
+ if (comS.extreme || comS.corner) ;
2641
+
2642
+ /*
2643
+ comS.extreme = com1.extreme;
2644
+ comS.directionChange = com1.directionChange;
2645
+ comS.corner = com1.corner;
2646
+ */
2647
+
2360
2648
  comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
2361
2649
 
2362
2650
  // relative error
@@ -2503,6 +2791,7 @@ function analyzePathData(pathData = [], {
2503
2791
  let com = pathData[c - 1];
2504
2792
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
2505
2793
 
2794
+ let comPrev = pathData[c-2];
2506
2795
  let comN = pathData[c] || null;
2507
2796
 
2508
2797
  // init properties
@@ -2521,6 +2810,7 @@ function analyzePathData(pathData = [], {
2521
2810
 
2522
2811
  // bezier types
2523
2812
  let isBezier = type === 'Q' || type === 'C';
2813
+ let isArc = type === 'A';
2524
2814
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
2525
2815
 
2526
2816
  /**
@@ -2567,6 +2857,22 @@ function analyzePathData(pathData = [], {
2567
2857
  }
2568
2858
  }
2569
2859
 
2860
+ // check extremes introduce by small arcs
2861
+ else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
2862
+ let distN = comN ? comN.dimA : 0;
2863
+ let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
2864
+ let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
2865
+
2866
+ if(isShort && smallRadius){
2867
+ let bb = getPolyBBox([comPrev.p0, comN.p]);
2868
+ if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
2869
+ hasExtremes = true;
2870
+
2871
+ }
2872
+ }
2873
+
2874
+ }
2875
+
2570
2876
  if (hasExtremes) com.extreme = true;
2571
2877
 
2572
2878
  // Corners and semi extremes
@@ -3120,50 +3426,10 @@ function parsePathDataString(d, debug = true, limit=0) {
3120
3426
 
3121
3427
  }
3122
3428
 
3123
- function parsePathDataNormalized(d,
3124
- {
3125
- // necessary for most calculations
3126
- toAbsolute = true,
3127
- toLonghands = true,
3128
-
3129
- // not necessary unless you need cubics only
3130
- quadraticToCubic = false,
3131
-
3132
- // mostly a fallback if arc calculations fail
3133
- arcToCubic = false,
3134
- // arc to cubic precision - adds more segments for better precision
3135
- arcAccuracy = 4,
3136
- } = {}
3137
- ) {
3138
-
3139
- // is already array
3140
- let isArray = Array.isArray(d);
3141
-
3142
- // normalize native pathData to regular array
3143
- let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3144
- /*
3145
- if (hasConstructor) {
3146
- d = d.map(com => { return { type: com.type, values: com.values } })
3147
- console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3148
- }
3149
- */
3150
-
3151
- let pathDataObj = isArray ? d : parsePathDataString(d);
3152
-
3153
- let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3154
- let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3155
-
3156
- // normalize
3157
- pathData = normalizePathData(pathData,
3158
- {
3159
- toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3160
- hasRelatives, hasShorthands, hasQuadratics, hasArcs
3161
- },
3162
- );
3163
-
3164
- return pathData;
3165
- }
3166
-
3429
+ /**
3430
+ * wrapper function for
3431
+ * all path data conversion
3432
+ */
3167
3433
  function convertPathData(pathData, {
3168
3434
  toShorthands = true,
3169
3435
  toLonghands = false,
@@ -3215,22 +3481,24 @@ function convertPathData(pathData, {
3215
3481
 
3216
3482
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3217
3483
 
3218
- if(toMixed) toRelative = true;
3484
+ if (toMixed) toRelative = true;
3219
3485
 
3220
3486
  // pre round - before relative conversion to minimize distortions
3221
3487
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
3222
3488
 
3223
3489
  // clone absolute pathdata
3224
- if(toMixed){
3490
+ if (toMixed) {
3225
3491
  pathDataAbs = JSON.parse(JSON.stringify(pathData));
3226
3492
  }
3227
3493
 
3228
3494
  if (toRelative) pathData = pathDataToRelative(pathData);
3495
+
3496
+ // final rounding
3229
3497
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
3230
3498
 
3231
3499
  // choose most compact commands: relative or absolute
3232
- if(toMixed){
3233
- for(let i=0; i<pathData.length; i++){
3500
+ if (toMixed) {
3501
+ for (let i = 0; i < pathData.length; i++) {
3234
3502
  let com = pathData[i];
3235
3503
  let comA = pathDataAbs[i];
3236
3504
  // compare Lengths
@@ -3240,7 +3508,7 @@ function convertPathData(pathData, {
3240
3508
  let lenR = comStr.length;
3241
3509
  let lenA = comStrA.length;
3242
3510
 
3243
- if(lenA<lenR){
3511
+ if (lenA < lenR) {
3244
3512
 
3245
3513
  pathData[i] = pathDataAbs[i];
3246
3514
  }
@@ -3250,6 +3518,50 @@ function convertPathData(pathData, {
3250
3518
  return pathData
3251
3519
  }
3252
3520
 
3521
+ function parsePathDataNormalized(d,
3522
+ {
3523
+ // necessary for most calculations
3524
+ toAbsolute = true,
3525
+ toLonghands = true,
3526
+
3527
+ // not necessary unless you need cubics only
3528
+ quadraticToCubic = false,
3529
+
3530
+ // mostly a fallback if arc calculations fail
3531
+ arcToCubic = false,
3532
+ // arc to cubic precision - adds more segments for better precision
3533
+ arcAccuracy = 4,
3534
+ } = {}
3535
+ ) {
3536
+
3537
+ // is already array
3538
+ let isArray = Array.isArray(d);
3539
+
3540
+ // normalize native pathData to regular array
3541
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3542
+ /*
3543
+ if (hasConstructor) {
3544
+ d = d.map(com => { return { type: com.type, values: com.values } })
3545
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3546
+ }
3547
+ */
3548
+
3549
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3550
+
3551
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3552
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3553
+
3554
+ // normalize
3555
+ pathData = normalizePathData(pathData,
3556
+ {
3557
+ toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3558
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3559
+ },
3560
+ );
3561
+
3562
+ return pathData;
3563
+ }
3564
+
3253
3565
  /**
3254
3566
  *
3255
3567
  * @param {*} pathData
@@ -3257,75 +3569,97 @@ function convertPathData(pathData, {
3257
3569
  */
3258
3570
 
3259
3571
  function optimizeArcPathData(pathData = []) {
3572
+ let l = pathData.length;
3573
+ let pathDataN = [];
3260
3574
 
3261
- let remove =[];
3262
-
3263
- pathData.forEach((com, i) => {
3575
+ for (let i = 0; i < l; i++) {
3576
+ let com = pathData[i];
3264
3577
  let { type, values } = com;
3265
- if (type === 'A') {
3266
- let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3267
- let comPrev = pathData[i - 1];
3268
- let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3269
- let M = { x: x0, y: y0 };
3270
- let p = { x, y };
3271
3578
 
3272
- if(rx===0 || ry===0){
3273
- pathData[i]= null;
3274
- remove.push(i);
3579
+ if (type !== 'A') {
3580
+ pathDataN.push(com);
3581
+ continue
3582
+ }
3583
+
3584
+ let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3585
+ let comPrev = pathData[i - 1];
3586
+ let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3587
+ let M = { x: x0, y: y0 };
3588
+ let p = { x, y };
3275
3589
 
3276
- }
3590
+ if (rx === 0 || ry === 0) {
3591
+ pathData[i] = null;
3592
+ }
3277
3593
 
3278
- // rx and ry are large enough
3279
- if (rx >= 1 && (x === x0 || y === y0)) {
3280
- let diff = Math.abs(rx - ry) / rx;
3594
+ // test for elliptic
3595
+ let rat = rx / ry;
3596
+ let error = rx !== ry ? Math.abs(1 - rat) : 0;
3281
3597
 
3282
- // rx~==ry
3283
- if (diff < 0.01) {
3598
+ if (error > 0.01) {
3284
3599
 
3285
- // test radius against mid point
3286
- let pMid = interpolate(M, p, 0.5);
3287
- let distM = getDistance(pMid, M);
3288
- let rDiff = Math.abs(distM - rx) / rx;
3600
+ pathDataN.push(com);
3601
+ continue
3289
3602
 
3290
- // half distance between mid and start point should be ~ equal
3291
- if(rDiff<0.01){
3292
- pathData[i].values[0] = 1;
3293
- pathData[i].values[1] = 1;
3294
- pathData[i].values[2] = 0;
3295
- }
3296
- }
3297
- }
3298
3603
  }
3299
- });
3300
3604
 
3301
- if(remove.length) pathData = pathData.filter(Boolean);
3302
- return pathData;
3303
- }
3605
+ // xAxis rotation is futile for circular arcs - reset
3606
+ com.values[2] = 0;
3304
3607
 
3305
- /**
3306
- * parse normalized
3307
- */
3608
+ /**
3609
+ * test semi circles
3610
+ * rx and ry are large enough
3611
+ */
3308
3612
 
3309
- function normalizePathData(pathData = [],
3310
- {
3311
- toAbsolute = true,
3312
- toLonghands = true,
3313
- quadraticToCubic = false,
3314
- arcToCubic = false,
3315
- arcAccuracy = 2,
3613
+ // 1. horizontal or vertical
3614
+ let thresh = getDistManhattan(M, p) * 0.001;
3615
+ let diffX = Math.abs(x - x0);
3616
+ let diffY = Math.abs(y - y0);
3316
3617
 
3317
- // assume we need full normalization
3318
- hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3618
+ let isHorizontal = diffY < thresh;
3619
+ let isVertical = diffX < thresh;
3319
3620
 
3320
- } = {}
3321
- ) {
3621
+ // minify rx and ry
3622
+ if (isHorizontal || isVertical) {
3322
3623
 
3323
- return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
3324
- }
3624
+ // check if semi circle
3625
+ let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
3325
3626
 
3326
- /*
3327
- export function normalizePathData(pathData = [],
3328
- {
3627
+ // is semicircle we can simplify rx
3628
+ if (!needsTrueR) {
3629
+
3630
+ rx = rx >= 1 ? 1 : (rx > 0.5 ? 0.5 : rx);
3631
+ }
3632
+
3633
+ com.values[0] = rx;
3634
+ com.values[1] = rx;
3635
+ pathDataN.push(com);
3636
+ continue
3637
+
3638
+ }
3639
+
3640
+ // 2. get true radius - if rx ~= diameter/distance we have a semicircle
3641
+ let r = getDistance(M, p) * 0.5;
3642
+ error = rx / r;
3643
+
3644
+ if (error < 0.5) {
3645
+ rx = r >= 1 ? 1 : (r > 0.5 ? 0.5 : r);
3646
+ }
3647
+
3648
+ com.values[0] = rx;
3649
+ com.values[1] = rx;
3650
+ pathDataN.push(com);
3651
+
3652
+ }
3653
+
3654
+ return pathDataN;
3655
+ }
3656
+
3657
+ /**
3658
+ * parse normalized
3659
+ */
3660
+
3661
+ function normalizePathData(pathData = [],
3662
+ {
3329
3663
  toAbsolute = true,
3330
3664
  toLonghands = true,
3331
3665
  quadraticToCubic = false,
@@ -3338,31 +3672,8 @@ export function normalizePathData(pathData = [],
3338
3672
  } = {}
3339
3673
  ) {
3340
3674
 
3341
- // pathdata properties - test= true adds a manual test
3342
- if (testTypes) {
3343
-
3344
- let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
3345
- hasRelatives = /[lcqamts]/gi.test(commands);
3346
- hasQuadratics = /[qt]/gi.test(commands);
3347
- hasArcs = /[a]/gi.test(commands);
3348
- hasShorthands = /[vhst]/gi.test(commands);
3349
- isPoly = /[mlz]/gi.test(commands);
3350
- }
3351
-
3352
- if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
3353
- toLonghands = true
3354
- toAbsolute = true
3355
- }
3356
-
3357
- if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
3358
- if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
3359
- if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
3360
- if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3361
-
3362
- return pathData;
3363
-
3675
+ return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
3364
3676
  }
3365
- */
3366
3677
 
3367
3678
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
3368
3679
 
@@ -4243,7 +4554,8 @@ function pathDataToTopLeft(pathData) {
4243
4554
  let { type, values } = com;
4244
4555
  let valsLen = values.length;
4245
4556
  if (valsLen) {
4246
- let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
4557
+ // we need rounding otherwise sorting may crash due to e notation
4558
+ let p = { type: type, x: +values[valsLen - 2].toFixed(8), y: +values[valsLen - 1].toFixed(8), index: 0 };
4247
4559
  p.index = i;
4248
4560
  indices.push(p);
4249
4561
  }
@@ -4251,113 +4563,111 @@ function pathDataToTopLeft(pathData) {
4251
4563
 
4252
4564
  // reorder to top left most
4253
4565
 
4254
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
4566
+ indices = indices.sort((a, b) => a.y - b.y || a.x - b.x);
4255
4567
  newIndex = indices[0].index;
4256
4568
 
4257
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4569
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4258
4570
  }
4259
4571
 
4260
- function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
4572
+ function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
4261
4573
 
4262
- let pathDataNew = [];
4574
+ let pathDataN = pathData;
4263
4575
  let l = pathData.length;
4264
4576
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4265
4577
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4266
4578
 
4267
- let linetos = pathData.filter(com => com.type === 'L');
4268
-
4269
- // check if order is ideal
4270
- let idxPenultimate = isClosed ? l-2 : l-1;
4579
+ let hasLinetos = false;
4271
4580
 
4581
+ // check if path is closed by explicit lineto
4582
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
4272
4583
  let penultimateCom = pathData[idxPenultimate];
4273
4584
  let penultimateType = penultimateCom.type;
4274
4585
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4275
4586
 
4276
4587
  // last L command ends at M
4277
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4588
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4589
+ let lastIsLine = penultimateType === 'L';
4278
4590
 
4279
- // add closepath Z to enable order optimizations
4280
- if(!isClosed && autoClose && isClosingCommand){
4281
-
4282
- /*
4283
- // adjust final coords
4284
- let valsLast = pathData[idxPenultimate].values
4285
- let valsLastLen = valsLast.length;
4286
- pathData[idxPenultimate].values[valsLastLen-2] = M.x
4287
- pathData[idxPenultimate].values[valsLastLen-1] = M.y
4288
- */
4289
-
4290
- pathData.push({type:'Z', values:[]});
4291
- isClosed = true;
4292
- l++;
4293
- }
4591
+ // create index
4592
+ let indices = [];
4593
+ for (let i = 0; i < l; i++) {
4594
+ let com = pathData[i];
4595
+ let { type, values, p0, p } = com;
4294
4596
 
4295
- // if last segment is not closing or a lineto
4296
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
4297
- skipReorder = false;
4597
+ if(type==='L') hasLinetos = true;
4298
4598
 
4299
- // we can't change starting point for non closed paths
4300
- if (!isClosed) {
4301
- return pathData
4302
- }
4599
+ // exclude Z
4600
+ if (values.length) {
4601
+ values.slice(-2);
4303
4602
 
4304
- let newIndex = 0;
4603
+ let x = Math.min(p0.x, p.x);
4604
+ let y = Math.min(p0.y, p.y);
4305
4605
 
4306
- if (!skipReorder) {
4606
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
4607
+ let prevType = prevCom.type;
4307
4608
 
4308
- let indices = [];
4309
- for (let i = 0; i < l; i++) {
4310
- let com = pathData[i];
4311
- let { type, values } = com;
4312
- if (values.length) {
4313
- let valsL = values.slice(-2);
4314
- let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
4315
- let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
4316
- let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
4317
- let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
4318
- let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
4319
- p.index = i;
4320
- indices.push(p);
4321
- }
4609
+ let item = { type: type, x, y, index: 0, prevType };
4610
+ item.index = i;
4611
+ indices.push(item);
4322
4612
  }
4323
4613
 
4324
- // find top most lineto
4614
+ }
4615
+
4616
+ let xMin = Infinity;
4617
+ let yMin = Infinity;
4618
+ let idx_top = null;
4619
+ let len = indices.length;
4325
4620
 
4326
- if (linetos.length) {
4327
- let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
4328
- com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
4621
+ for (let i = 0; i < len; i++) {
4622
+ let com = indices[i];
4623
+ let { type, index, x, y, prevType } = com;
4329
4624
 
4330
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
4625
+ if (hasLinetos && prevType === 'L') {
4626
+ if (x < xMin && y < yMin) {
4627
+ idx_top = index-1;
4628
+ }
4331
4629
 
4332
- }
4333
- // use top most command
4334
- else {
4335
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
4336
- newIndex = indices[0].index;
4337
- }
4630
+ if (y < yMin) {
4631
+ yMin = y;
4632
+ }
4338
4633
 
4339
- // reorder
4340
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4634
+ if (x < xMin) {
4635
+ xMin = x;
4636
+ }
4637
+ }
4341
4638
  }
4342
4639
 
4343
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4640
+ // shift to better starting point
4641
+ if (idx_top) {
4642
+ pathDataN = shiftSvgStartingPoint(pathDataN, idx_top);
4344
4643
 
4345
- l = pathData.length;
4644
+ // update penultimate - reorder might have added new close paths
4645
+ l = pathDataN.length;
4646
+ M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) };
4647
+
4648
+ idxPenultimate = isClosed ? l - 2 : l - 1;
4649
+ penultimateCom = pathDataN[idxPenultimate];
4650
+ penultimateType = penultimateCom.type;
4651
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4652
+ lastIsLine = penultimateType ==='L';
4653
+
4654
+ // last L command ends at M
4655
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4346
4656
 
4347
- // remove last lineto
4348
- penultimateCom = pathData[l - 2];
4349
- penultimateType = penultimateCom.type;
4350
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
4657
+ }
4351
4658
 
4352
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4659
+ // remove unnecessary closing lineto
4660
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
4661
+ pathDataN.splice(l - 2, 1);
4662
+ }
4353
4663
 
4354
- if (removeFinalLineto && isClosingCommand) {
4355
- pathData.splice(l - 2, 1);
4664
+ // add close path
4665
+ if (autoClose && !isClosed && hasClosingCommand) {
4666
+ pathDataN.push({ type: 'Z', values: [] });
4356
4667
  }
4357
4668
 
4358
- pathDataNew.push(...pathData);
4669
+ return pathDataN
4359
4670
 
4360
- return pathDataNew
4361
4671
  }
4362
4672
 
4363
4673
  /**
@@ -4567,8 +4877,130 @@ function refineAdjacentExtremes(pathData, {
4567
4877
 
4568
4878
  }
4569
4879
 
4880
+ function getArcFromPoly(pts, precise = false) {
4881
+ if (pts.length < 3) return false
4882
+
4883
+ // Pick 3 well-spaced points
4884
+ let len = pts.length;
4885
+ let idx1 = Math.floor(len * 0.333);
4886
+ let idx2 = Math.floor(len * 0.666);
4887
+ let idx3 = Math.floor(len * 0.5);
4888
+
4889
+ let p1 = pts[0];
4890
+ let p2 = pts[idx3];
4891
+ let p3 = pts[len - 1];
4892
+
4893
+ // Radius (use start point)
4894
+ let pts1 = [p1, p2, p3];
4895
+ let centroid = getPolyArcCentroid(pts1);
4896
+
4897
+ let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
4898
+
4899
+ // check if radii are consistent
4900
+ if (precise) {
4901
+
4902
+ /**
4903
+ * check multiple centroids
4904
+ * if the polyline can be expressed as
4905
+ * an arc - all centroids should be close
4906
+ */
4907
+
4908
+ if (len > 3) {
4909
+ let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
4910
+ let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
4911
+
4912
+ if (!centroid1 || !centroid2) return false;
4913
+
4914
+ let dist0 = getDistManhattan(centroid, p2);
4915
+ let dist1 = getDistManhattan(centroid, centroid1);
4916
+ let dist2 = getDistManhattan(centroid, centroid2);
4917
+ let errorCentroid = (dist1 + dist2);
4918
+
4919
+ // centroids diverging too much
4920
+ if (errorCentroid > dist0 * 0.05) {
4921
+
4922
+ return false
4923
+ }
4924
+
4925
+ }
4926
+
4927
+ let rSqMid = getSquareDistance(centroid, p2);
4928
+
4929
+ for (let i = 0; i < len; i++) {
4930
+ let pt = pts[i];
4931
+ let rSq = getSquareDistance(centroid, pt);
4932
+ let error = Math.abs(rSqMid - rSq) / rSqMid;
4933
+
4934
+ if (error > 0.0025) {
4935
+ /*
4936
+ console.log('error', error, len, idx1, idx2, idx3);
4937
+ renderPoint(markers, centroid, 'orange')
4938
+ renderPoint(markers, p1, 'green')
4939
+ renderPoint(markers, p2)
4940
+ renderPoint(markers, p3, 'purple')
4941
+ */
4942
+ return false;
4943
+ }
4944
+ }
4945
+
4946
+ // calculate proper radius
4947
+ r = Math.sqrt(rSqMid);
4948
+ angleData = getDeltaAngle(centroid, p1, p3);
4949
+ ({ deltaAngle, startAngle, endAngle } = angleData);
4950
+
4951
+ } else {
4952
+ r = getDistance(centroid, p1);
4953
+ angleData = getDeltaAngle(centroid, p1, p3);
4954
+ ({ deltaAngle, startAngle, endAngle } = angleData);
4955
+ }
4956
+
4957
+ return {
4958
+ centroid,
4959
+ r,
4960
+ startAngle,
4961
+ endAngle,
4962
+ deltaAngle
4963
+ };
4964
+ }
4965
+
4966
+ function getPolyArcCentroid(pts = []) {
4967
+
4968
+ pts = pts.filter(pt => pt !== undefined);
4969
+ if (pts.length < 3) return false
4970
+
4971
+ let p1 = pts[0];
4972
+ let p2 = pts[Math.floor(pts.length / 2)];
4973
+ let p3 = pts[pts.length - 1];
4974
+
4975
+ let x1 = p1.x, y1 = p1.y;
4976
+ let x2 = p2.x, y2 = p2.y;
4977
+ let x3 = p3.x, y3 = p3.y;
4978
+
4979
+ let a = x1 - x2;
4980
+ let b = y1 - y2;
4981
+ let c = x1 - x3;
4982
+ let d = y1 - y3;
4983
+
4984
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4985
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
4986
+
4987
+ let det = a * d - b * c;
4988
+
4989
+ // colinear points
4990
+ if (Math.abs(det) < 1e-10) {
4991
+ return false;
4992
+ }
4993
+
4994
+ // find center of arc
4995
+ let cx = (d * e - b * f) / det;
4996
+ let cy = (-c * e + a * f) / det;
4997
+ let centroid = { x: cx, y: cy };
4998
+ return centroid
4999
+ }
5000
+
4570
5001
  function refineRoundedCorners(pathData, {
4571
5002
  threshold = 0,
5003
+ simplifyQuadraticCorners = false,
4572
5004
  tolerance = 1
4573
5005
  } = {}) {
4574
5006
 
@@ -4593,6 +5025,9 @@ function refineRoundedCorners(pathData, {
4593
5025
  let firstIsLine = pathData[1].type === 'L';
4594
5026
  let firstIsBez = pathData[1].type === 'C';
4595
5027
 
5028
+ // in case we have simplified a corner connecting to the start
5029
+ let M_adj = null;
5030
+
4596
5031
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
4597
5032
 
4598
5033
  // normalize closepath to lineto
@@ -4632,15 +5067,17 @@ function refineRoundedCorners(pathData, {
4632
5067
  // closing corner to start
4633
5068
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4634
5069
  comL1 = pathData[1];
5070
+
4635
5071
  comBez = [pathData[l - lastOff]];
4636
5072
 
4637
5073
  }
4638
5074
 
5075
+ // collect enclosed bezier segments
4639
5076
  for (let j = i + 1; j < l; j++) {
4640
5077
  let comN = pathData[j] ? pathData[j] : null;
4641
5078
  let comPrev = pathData[j - 1];
4642
5079
 
4643
- if (comPrev.type === 'C') {
5080
+ if (comPrev.type === 'C' && j > 2) {
4644
5081
  comBez.push(comPrev);
4645
5082
  }
4646
5083
 
@@ -4671,39 +5108,67 @@ function refineRoundedCorners(pathData, {
4671
5108
  let bezThresh = len3 * 0.5 * tolerance;
4672
5109
  let isSmall = bezThresh < len1 && bezThresh < len2;
4673
5110
 
5111
+ /*
5112
+ */
5113
+
4674
5114
  if (comBez.length && !signChange && isSmall) {
4675
5115
 
4676
- let isFlatBezier = Math.abs(area2) < getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
5116
+ let isSquare = false;
5117
+
5118
+ if (comBez.length === 1) {
5119
+ let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x);
5120
+ let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y);
5121
+ let diff = (dx - dy);
5122
+ let rat = Math.abs(diff / dx);
5123
+ isSquare = rat < 0.01;
5124
+ }
5125
+
5126
+ let preferArcs = true;
5127
+ preferArcs = false;
5128
+
5129
+ // if rectangular prefer arcs
5130
+ if (preferArcs && isSquare) {
5131
+
5132
+ let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5);
5133
+
5134
+ let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p]);
5135
+ let { r, centroid, deltaAngle } = arcProps;
5136
+
5137
+ let sweep = deltaAngle > 0 ? 1 : 0;
5138
+
5139
+ let largeArc = 0;
5140
+
5141
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] };
5142
+
5143
+ pathDataN.push(comL0, comArc);
5144
+ i += offset;
5145
+ continue
5146
+
5147
+ }
5148
+
5149
+ let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
5150
+ let isFlatBezier = Math.abs(area2) < areaThresh;
5151
+ let isFlatBezier2 = Math.abs(area2) < areaThresh * 10;
5152
+
4677
5153
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
4678
5154
 
4679
- if (!ptQ) {
5155
+ // exit: is rather flat or has no intersection
5156
+
5157
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
4680
5158
  pathDataN.push(com);
4681
5159
  continue
4682
5160
  }
4683
5161
 
4684
- // check sign change
5162
+ // check sign change - exit if present
4685
5163
  if (ptQ) {
4686
5164
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4687
5165
  let area0_abs = Math.abs(area0);
4688
5166
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
4689
5167
  let area1_abs = Math.abs(area1);
4690
5168
  let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs;
4691
-
4692
- /*
4693
- renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
4694
- renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
4695
- renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
4696
- renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
4697
- if(!area0) {
4698
- pathDataN.push(com);
4699
- continue
4700
- }
4701
- */
4702
-
4703
5169
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
4704
5170
 
4705
5171
  if (!ptQ || signChange || areaDiff > 0.5) {
4706
-
4707
5172
  pathDataN.push(com);
4708
5173
  continue
4709
5174
  }
@@ -4718,24 +5183,67 @@ function refineRoundedCorners(pathData, {
4718
5183
 
4719
5184
  // not in tolerance – return original command
4720
5185
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
4721
-
4722
5186
  pathDataN.push(com);
4723
5187
  continue;
4724
5188
 
4725
- } else {
5189
+ }
4726
5190
 
4727
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4728
- comQ.p0 = comL0.p;
4729
- comQ.cp1 = ptQ;
4730
- comQ.p = comL1.p0;
5191
+ // return simplified quadratic Bézier command
5192
+ let p_Q = comL1.p0;
4731
5193
 
4732
- // add quadratic command
4733
- pathDataN.push(comL0, comQ);
4734
- i += offset;
5194
+ // adjust previous end point to better fit the cubic curvature
5195
+ let adjustQ = !simplifyQuadraticCorners;
5196
+
5197
+ if (adjustQ) {
5198
+
5199
+ let t = 0.1666;
5200
+ let p0_adj = interpolate(ptQ, comL0.p, (1 + t));
5201
+ p_Q = interpolate(ptQ, comL1.p0, (1 + t));
5202
+
5203
+ // round for large enough segments
5204
+ let isH = ptQ.y===comL0.p.y;
5205
+ let isV = ptQ.x===comL0.p.x;
5206
+ let isH2 = ptQ.y===comL1.p0.y;
5207
+ let isV2 = ptQ.x===comL1.p0.x;
5208
+
5209
+ if(isSquare && com.dimA>3){
5210
+ let dec = 0.5;
5211
+ if(isH) p0_adj.x = roundTo(p0_adj.x, dec);
5212
+ if(isV) p0_adj.y = roundTo(p0_adj.y, dec);
5213
+ if(isH2) p_Q.x = roundTo(p_Q.x, dec);
5214
+ if(isV2) p_Q.y = roundTo(p_Q.y, dec);
5215
+ }
5216
+
5217
+ /*
5218
+ renderPoint(markers, p0_adj, 'orange')
5219
+ renderPoint(markers, p_Q, 'orange')
5220
+ renderPoint(markers, comL0.p, 'green')
5221
+ renderPoint(markers, comL1.p0, 'magenta')
5222
+ */
5223
+
5224
+ // set new M starting point
5225
+ if (i === l - lastOff - 1) {
5226
+
5227
+ M_adj = p_Q;
5228
+ }
5229
+
5230
+ // adjust previous lineto end point
5231
+ comL0.values = [p0_adj.x, p0_adj.y];
5232
+ comL0.p = p0_adj;
4735
5233
 
4736
- continue;
4737
5234
  }
4738
5235
 
5236
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] };
5237
+ comQ.cp1 = ptQ;
5238
+ comQ.p0 = comL0.p;
5239
+ comQ.p = p_Q;
5240
+
5241
+ // add quadratic command
5242
+ pathDataN.push(comL0, comQ);
5243
+
5244
+ i += offset;
5245
+ continue;
5246
+
4739
5247
  }
4740
5248
  }
4741
5249
  }
@@ -4749,6 +5257,12 @@ function refineRoundedCorners(pathData, {
4749
5257
 
4750
5258
  }
4751
5259
 
5260
+ // correct starting point connecting with last corner rounding
5261
+ if (M_adj) {
5262
+ pathDataN[0].values = [M_adj.x, M_adj.y];
5263
+ pathDataN[0].p0 = M_adj;
5264
+ }
5265
+
4752
5266
  // revert close path normalization
4753
5267
  if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
4754
5268
  pathDataN.push({ type: 'Z', values: [] });
@@ -4758,51 +5272,101 @@ function refineRoundedCorners(pathData, {
4758
5272
 
4759
5273
  }
4760
5274
 
4761
- function getArcFromPoly(pts) {
4762
- if (pts.length < 3) return false
5275
+ function refineClosingCommand(pathData = [], {
5276
+ threshold = 0,
5277
+ } = {}) {
4763
5278
 
4764
- // Pick 3 well-spaced points
4765
- let p1 = pts[0];
4766
- let p2 = pts[Math.floor(pts.length / 2)];
4767
- let p3 = pts[pts.length - 1];
5279
+ let l = pathData.length;
5280
+ let comLast = pathData[l - 1];
5281
+ let isClosed = comLast.type.toLowerCase() === 'z';
5282
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
5283
+ let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
5284
+ let valsPen = comPenultimate.values.slice(-2);
5285
+
5286
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5287
+ let pPen = { x: valsPen[0], y: valsPen[1] };
5288
+ let dist = getDistAv(M, pPen);
5289
+
5290
+ // adjust last coordinates for better reordering
5291
+ if (dist && dist < threshold) {
4768
5292
 
4769
- let x1 = p1.x, y1 = p1.y;
4770
- let x2 = p2.x, y2 = p2.y;
4771
- let x3 = p3.x, y3 = p3.y;
5293
+ let valsLast = pathData[idxPenultimate].values;
5294
+ let valsLastLen = valsLast.length;
5295
+ pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
5296
+ pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
4772
5297
 
4773
- let a = x1 - x2;
4774
- let b = y1 - y2;
4775
- let c = x1 - x3;
4776
- let d = y1 - y3;
5298
+ // adjust cpts
5299
+ let comFirst = pathData[1];
4777
5300
 
4778
- let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4779
- let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
5301
+ if (comFirst.type === 'C' && comPenultimate.type === 'C') {
5302
+ let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
5303
+ let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
4780
5304
 
4781
- let det = a * d - b * c;
5305
+ let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
5306
+ let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
4782
5307
 
4783
- if (Math.abs(det) < 1e-10) {
4784
- console.warn("Points are collinear or numerically unstable");
4785
- return false;
5308
+ let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5309
+ let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5310
+
5311
+ let ver = dx2 < threshold && dx3 < threshold && dy1;
5312
+ let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5313
+
5314
+ if (dx1 && dx1 < threshold && ver) {
5315
+
5316
+ pathData[1].values[0] = M.x;
5317
+ pathData[idxPenultimate].values[2] = M.x;
5318
+ }
5319
+
5320
+ if (dy1 && dy1 < threshold && hor) {
5321
+
5322
+ pathData[1].values[1] = M.y;
5323
+ pathData[idxPenultimate].values[3] = M.y;
5324
+ }
5325
+
5326
+ }
4786
5327
  }
4787
5328
 
4788
- // find center of arc
4789
- let cx = (d * e - b * f) / det;
4790
- let cy = (-c * e + a * f) / det;
4791
- let centroid = { x: cx, y: cy };
5329
+ return pathData;
4792
5330
 
4793
- // Radius (use start point)
4794
- let r = getDistance(centroid, p1);
5331
+ }
4795
5332
 
4796
- let angleData = getDeltaAngle(centroid, p1, p3);
4797
- let {deltaAngle, startAngle, endAngle} = angleData;
5333
+ function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
4798
5334
 
4799
- return {
4800
- centroid,
4801
- r,
4802
- startAngle,
4803
- endAngle,
4804
- deltaAngle
4805
- };
5335
+ for (let c = 1, l = pathData.length; c < l; c++) {
5336
+ let com = pathData[c];
5337
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5338
+ if (type === 'C') {
5339
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
5340
+ if (comQ.type === 'Q') {
5341
+ comQ.extreme = com.extreme;
5342
+ comQ.corner = com.corner;
5343
+ comQ.dimA = com.dimA;
5344
+ comQ.squareDist = com.squareDist;
5345
+ pathData[c] = comQ;
5346
+ }
5347
+ }
5348
+ }
5349
+ return pathData
5350
+ }
5351
+
5352
+ function pathDataLineToCubic(pathData) {
5353
+
5354
+ for (let c = 1, l = pathData.length; c < l; c++) {
5355
+ let com = pathData[c];
5356
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5357
+ if (type === 'L') {
5358
+
5359
+ let cp1 = interpolate(p0, p, 0.333);
5360
+ let cp2 = interpolate(p, p0, 0.333);
5361
+
5362
+ pathData[c].type = 'C';
5363
+ pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
5364
+ pathData[c].cp1 = cp1;
5365
+ pathData[c].cp2 = cp2;
5366
+
5367
+ }
5368
+ }
5369
+ return pathData
4806
5370
  }
4807
5371
 
4808
5372
  function refineRoundSegments(pathData, {
@@ -4821,9 +5385,6 @@ function refineRoundSegments(pathData, {
4821
5385
  // add fist command
4822
5386
  let pathDataN = [pathData[0]];
4823
5387
 
4824
- // just for debugging
4825
- let pathDataTest = [];
4826
-
4827
5388
  for (let i = 1; i < l; i++) {
4828
5389
  let com = pathData[i];
4829
5390
  let { type } = com;
@@ -4850,11 +5411,12 @@ function refineRoundSegments(pathData, {
4850
5411
 
4851
5412
  // 2. line-line-bezier-line-line
4852
5413
  if (
5414
+ comN2 && comN3 &&
4853
5415
  comP.type === 'L' &&
4854
5416
  type === 'L' &&
4855
5417
  comBez &&
4856
5418
  comN2.type === 'L' &&
4857
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
5419
+ (comN3.type === 'L' || comN3.type === 'Z')
4858
5420
  ) {
4859
5421
 
4860
5422
  L1 = [com.p0, com.p];
@@ -4881,10 +5443,10 @@ function refineRoundSegments(pathData, {
4881
5443
  }
4882
5444
 
4883
5445
  // 1. line-bezier-bezier-line
4884
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
5446
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
4885
5447
 
4886
5448
  // 1.2 next is cubic next is lineto
4887
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
5449
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
4888
5450
 
4889
5451
  combine = true;
4890
5452
 
@@ -4943,16 +5505,19 @@ function refineRoundSegments(pathData, {
4943
5505
  }
4944
5506
  );
4945
5507
 
4946
- if(bezierCommands.length === 1){
5508
+ if (bezierCommands.length === 1) {
4947
5509
 
4948
5510
  // prefer more compact quadratic - otherwise arcs
4949
5511
  let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
4950
5512
 
4951
5513
  if (comBezier.type === 'Q') {
4952
5514
  toCubic = true;
5515
+ }else {
5516
+ comBezier = bezierCommands[0];
4953
5517
  }
4954
5518
 
4955
5519
  com = comBezier;
5520
+
4956
5521
  }
4957
5522
 
4958
5523
  // prefer arcs if 2 cubics are required
@@ -4972,25 +5537,28 @@ function refineRoundSegments(pathData, {
4972
5537
 
4973
5538
  // test rendering
4974
5539
 
5540
+ /*
4975
5541
  if (debug) {
4976
5542
  // arcs
4977
5543
  if (!toCubic) {
4978
5544
  pathDataTest = [
4979
5545
  { type: 'M', values: [p0_S.x, p0_S.y] },
4980
5546
  { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
4981
- ];
5547
+ ]
4982
5548
  }
4983
5549
  // cubics
4984
5550
  else {
4985
5551
  pathDataTest = [
4986
5552
  { type: 'M', values: [p0_S.x, p0_S.y] },
4987
5553
  ...bezierCommands
4988
- ];
5554
+ ]
5555
+
4989
5556
  }
4990
5557
 
4991
5558
  let d = pathDataToD(pathDataTest);
4992
- renderPath(markers, d, 'orange', '0.5%', '0.5');
5559
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
4993
5560
  }
5561
+ */
4994
5562
 
4995
5563
  pathDataN.push(com);
4996
5564
  i++;
@@ -5007,104 +5575,6 @@ function refineRoundSegments(pathData, {
5007
5575
  return pathDataN;
5008
5576
  }
5009
5577
 
5010
- function refineClosingCommand(pathData = [], {
5011
- threshold = 0,
5012
- } = {}) {
5013
-
5014
- let l = pathData.length;
5015
- let comLast = pathData[l - 1];
5016
- let isClosed = comLast.type.toLowerCase() === 'z';
5017
- let idxPenultimate = isClosed ? l - 2 : l - 1;
5018
- let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
5019
- let valsPen = comPenultimate.values.slice(-2);
5020
-
5021
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5022
- let pPen = { x: valsPen[0], y: valsPen[1] };
5023
- let dist = getDistAv(M, pPen);
5024
-
5025
- // adjust last coordinates for better reordering
5026
- if (dist && dist < threshold) {
5027
-
5028
- let valsLast = pathData[idxPenultimate].values;
5029
- let valsLastLen = valsLast.length;
5030
- pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
5031
- pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
5032
-
5033
- // adjust cpts
5034
- let comFirst = pathData[1];
5035
-
5036
- if (comFirst.type === 'C' && comPenultimate.type === 'C') {
5037
- let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
5038
- let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
5039
-
5040
- let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
5041
- let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
5042
-
5043
- let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5044
- let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5045
-
5046
- let ver = dx2 < threshold && dx3 < threshold && dy1;
5047
- let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5048
-
5049
- if (dx1 && dx1 < threshold && ver) {
5050
-
5051
- pathData[1].values[0] = M.x;
5052
- pathData[idxPenultimate].values[2] = M.x;
5053
- }
5054
-
5055
- if (dy1 && dy1 < threshold && hor) {
5056
-
5057
- pathData[1].values[1] = M.y;
5058
- pathData[idxPenultimate].values[3] = M.y;
5059
- }
5060
-
5061
- }
5062
- }
5063
-
5064
- return pathData;
5065
-
5066
- }
5067
-
5068
- function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
5069
-
5070
- for (let c = 1, l = pathData.length; c < l; c++) {
5071
- let com = pathData[c];
5072
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5073
- if (type === 'C') {
5074
-
5075
- let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
5076
- if (comQ.type === 'Q') {
5077
- comQ.extreme = com.extreme;
5078
- comQ.corner = com.corner;
5079
- comQ.dimA = com.dimA;
5080
- comQ.squareDist = com.squareDist;
5081
- pathData[c] = comQ;
5082
- }
5083
- }
5084
- }
5085
- return pathData
5086
- }
5087
-
5088
- function pathDataLineToCubic(pathData) {
5089
-
5090
- for (let c = 1, l = pathData.length; c < l; c++) {
5091
- let com = pathData[c];
5092
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5093
- if (type === 'L') {
5094
-
5095
- let cp1 = interpolate(p0, p, 0.333);
5096
- let cp2 = interpolate(p, p0, 0.333);
5097
-
5098
- pathData[c].type = 'C';
5099
- pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
5100
- pathData[c].cp1 = cp1;
5101
- pathData[c].cp2 = cp2;
5102
-
5103
- }
5104
- }
5105
- return pathData
5106
- }
5107
-
5108
5578
  function simplifyPathData(input = '', {
5109
5579
 
5110
5580
  toAbsolute = true,
@@ -5162,7 +5632,10 @@ function simplifyPathData(input = '', {
5162
5632
  let yArr = [];
5163
5633
 
5164
5634
  // mode:0 – single path
5165
- let inputType = detectInputType(input);
5635
+
5636
+ let inputDetection = detectInputType(input);
5637
+ let {inputType, log} = inputDetection;
5638
+
5166
5639
  if (inputType === 'pathDataString') {
5167
5640
  d = input;
5168
5641
  } else if (inputType === 'polyString') {