svg-path-simplify 0.4.3 → 0.4.4

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -0
  3. package/dist/svg-path-simplify.esm.js +1610 -495
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1611 -494
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +893 -456
  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 +58 -17
  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 +26 -16
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +75 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +15 -4
  20. package/src/svg-getAttributes.js +4 -2
  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 +17 -1
  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 +122 -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/rounding.js +79 -78
  33. package/src/svgii/svg_cleanup.js +68 -20
  34. package/src/svgii/svg_cleanup_convertPathLength.js +22 -15
  35. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  36. package/src/svgii/svg_el_parse_style_props.js +13 -10
  37. package/src/svgii/svg_validate.js +220 -0
  38. package/tests/testSVG.js +14 -1
  39. 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
 
@@ -1457,39 +1709,29 @@ function detectAccuracy(pathData) {
1457
1709
 
1458
1710
  }
1459
1711
 
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
1712
  /**
1468
- * round path data
1469
- * either by explicit decimal value or
1470
- * based on suggested accuracy in path data
1713
+ * rounding helper
1714
+ * allows for quantized rounding
1715
+ * e.g 0.5 decimals s
1471
1716
  */
1472
- function roundPathData(pathData, decimalsGlobal = -1) {
1473
-
1474
- if (decimalsGlobal < 0) return pathData;
1475
-
1476
- let len = pathData.length;
1477
-
1478
- let decimals = decimalsGlobal;
1717
+ function roundTo(num = 0, decimals = 3) {
1718
+ if (decimals < 0) return num;
1719
+ // Normal integer rounding
1720
+ if (!decimals) return Math.round(num);
1479
1721
 
1480
- for (let c = 0; c < len; c++) {
1481
- let com = pathData[c];
1482
- let {values} = com;
1722
+ // stepped rounding
1723
+ let intPart = Math.floor(decimals);
1483
1724
 
1484
- let valLen = values.length;
1485
- if (!valLen) continue
1725
+ if (intPart !== decimals) {
1726
+ let f = +(decimals - intPart).toFixed(2);
1727
+ f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
1486
1728
 
1487
- for (let v = 0; v < valLen; v++) {
1488
- pathData[c].values[v] = roundTo(values[v], decimals);
1489
- }
1729
+ let step = 10 ** -intPart * f;
1730
+ return +(Math.round(num / step) * step).toFixed(8);
1490
1731
  }
1491
1732
 
1492
- return pathData;
1733
+ let factor = 10 ** decimals;
1734
+ return Math.round(num * factor) / factor;
1493
1735
  }
1494
1736
 
1495
1737
  /**
@@ -2299,9 +2541,9 @@ function combineCubicPairs(com1, com2, {
2299
2541
  let comS = getExtrapolatedCommand(com1, com2, t);
2300
2542
 
2301
2543
  // test new point-at-t against original mid segment starting point
2302
- let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
2544
+ let ptI = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
2303
2545
 
2304
- let dist0 = getDistManhattan(com1.p, pt);
2546
+ let dist0 = getDistManhattan(com1.p, ptI);
2305
2547
  let dist1 = 0, dist2 = 0;
2306
2548
  let close = dist0 < maxDist;
2307
2549
  let success = false;
@@ -2316,29 +2558,40 @@ function combineCubicPairs(com1, com2, {
2316
2558
  * to prevent distortions
2317
2559
  */
2318
2560
 
2319
- // 2nd segment mid
2320
- let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
2561
+ // 1st segment mid
2562
+ let ptM_seg1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
2321
2563
 
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);
2564
+ let t2 = t * 0.5;
2565
+ // combined interpolated mid point
2566
+ let ptI_seg1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
2567
+ dist1 = getDistManhattan(ptM_seg1, ptI_seg1);
2326
2568
 
2327
2569
  error += dist1;
2328
2570
 
2329
2571
  if (dist1 < maxDist) {
2330
2572
 
2331
- // 1st segment mid
2332
- let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
2573
+ // 2nd segment mid
2574
+ let ptM_seg2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
2333
2575
 
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);
2576
+ // simplified path
2577
+ let t3 = (1 + t) * 0.5;
2578
+ let ptI_seg2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
2579
+ dist2 = getDistManhattan(ptM_seg2, ptI_seg2);
2337
2580
 
2338
2581
  error += dist2;
2339
2582
 
2340
2583
  if (error < maxDist) success = true;
2341
2584
 
2585
+ /*
2586
+ renderPoint(markers, ptM_seg1, 'cyan')
2587
+ renderPoint(markers, pt, 'orange', '1.5%', '1')
2588
+ renderPoint(markers, ptM_seg2, 'orange')
2589
+
2590
+ renderPoint(markers, com1.p, 'green')
2591
+
2592
+ renderPoint(markers, ptI_seg1, 'purple')
2593
+ */
2594
+
2342
2595
  }
2343
2596
 
2344
2597
  } // end 1st try
@@ -2503,6 +2756,7 @@ function analyzePathData(pathData = [], {
2503
2756
  let com = pathData[c - 1];
2504
2757
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
2505
2758
 
2759
+ let comPrev = pathData[c-2];
2506
2760
  let comN = pathData[c] || null;
2507
2761
 
2508
2762
  // init properties
@@ -2521,6 +2775,7 @@ function analyzePathData(pathData = [], {
2521
2775
 
2522
2776
  // bezier types
2523
2777
  let isBezier = type === 'Q' || type === 'C';
2778
+ let isArc = type === 'A';
2524
2779
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
2525
2780
 
2526
2781
  /**
@@ -2567,6 +2822,22 @@ function analyzePathData(pathData = [], {
2567
2822
  }
2568
2823
  }
2569
2824
 
2825
+ // check extremes introduce by small arcs
2826
+ else if(isArc && comN && ((comPrev.type==='C' || comPrev.type==='Q') || (comN.type==='C' || comN.type==='Q')) ){
2827
+ let distN = comN ? comN.dimA : 0;
2828
+ let isShort = com.dimA < (comPrev.dimA + distN) * 0.1;
2829
+ let smallRadius = com.values[0] === com.values[1] && (com.values[0] < 1);
2830
+
2831
+ if(isShort && smallRadius){
2832
+ let bb = getPolyBBox([comPrev.p0, comN.p]);
2833
+ if(p.x>bb.right || p.x<bb.x || p.y<bb.y || p.y>bb.bottom){
2834
+ hasExtremes = true;
2835
+
2836
+ }
2837
+ }
2838
+
2839
+ }
2840
+
2570
2841
  if (hasExtremes) com.extreme = true;
2571
2842
 
2572
2843
  // Corners and semi extremes
@@ -3120,50 +3391,10 @@ function parsePathDataString(d, debug = true, limit=0) {
3120
3391
 
3121
3392
  }
3122
3393
 
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
-
3394
+ /**
3395
+ * wrapper function for
3396
+ * all path data conversion
3397
+ */
3167
3398
  function convertPathData(pathData, {
3168
3399
  toShorthands = true,
3169
3400
  toLonghands = false,
@@ -3215,22 +3446,24 @@ function convertPathData(pathData, {
3215
3446
 
3216
3447
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3217
3448
 
3218
- if(toMixed) toRelative = true;
3449
+ if (toMixed) toRelative = true;
3219
3450
 
3220
3451
  // pre round - before relative conversion to minimize distortions
3221
3452
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
3222
3453
 
3223
3454
  // clone absolute pathdata
3224
- if(toMixed){
3455
+ if (toMixed) {
3225
3456
  pathDataAbs = JSON.parse(JSON.stringify(pathData));
3226
3457
  }
3227
3458
 
3228
3459
  if (toRelative) pathData = pathDataToRelative(pathData);
3460
+
3461
+ // final rounding
3229
3462
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
3230
3463
 
3231
3464
  // choose most compact commands: relative or absolute
3232
- if(toMixed){
3233
- for(let i=0; i<pathData.length; i++){
3465
+ if (toMixed) {
3466
+ for (let i = 0; i < pathData.length; i++) {
3234
3467
  let com = pathData[i];
3235
3468
  let comA = pathDataAbs[i];
3236
3469
  // compare Lengths
@@ -3240,7 +3473,7 @@ function convertPathData(pathData, {
3240
3473
  let lenR = comStr.length;
3241
3474
  let lenA = comStrA.length;
3242
3475
 
3243
- if(lenA<lenR){
3476
+ if (lenA < lenR) {
3244
3477
 
3245
3478
  pathData[i] = pathDataAbs[i];
3246
3479
  }
@@ -3250,6 +3483,50 @@ function convertPathData(pathData, {
3250
3483
  return pathData
3251
3484
  }
3252
3485
 
3486
+ function parsePathDataNormalized(d,
3487
+ {
3488
+ // necessary for most calculations
3489
+ toAbsolute = true,
3490
+ toLonghands = true,
3491
+
3492
+ // not necessary unless you need cubics only
3493
+ quadraticToCubic = false,
3494
+
3495
+ // mostly a fallback if arc calculations fail
3496
+ arcToCubic = false,
3497
+ // arc to cubic precision - adds more segments for better precision
3498
+ arcAccuracy = 4,
3499
+ } = {}
3500
+ ) {
3501
+
3502
+ // is already array
3503
+ let isArray = Array.isArray(d);
3504
+
3505
+ // normalize native pathData to regular array
3506
+ let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3507
+ /*
3508
+ if (hasConstructor) {
3509
+ d = d.map(com => { return { type: com.type, values: com.values } })
3510
+ console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3511
+ }
3512
+ */
3513
+
3514
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3515
+
3516
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3517
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3518
+
3519
+ // normalize
3520
+ pathData = normalizePathData(pathData,
3521
+ {
3522
+ toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3523
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3524
+ },
3525
+ );
3526
+
3527
+ return pathData;
3528
+ }
3529
+
3253
3530
  /**
3254
3531
  *
3255
3532
  * @param {*} pathData
@@ -3257,49 +3534,89 @@ function convertPathData(pathData, {
3257
3534
  */
3258
3535
 
3259
3536
  function optimizeArcPathData(pathData = []) {
3537
+ let l = pathData.length;
3538
+ let pathDataN = [];
3260
3539
 
3261
- let remove =[];
3262
-
3263
- pathData.forEach((com, i) => {
3540
+ for (let i = 0; i < l; i++) {
3541
+ let com = pathData[i];
3264
3542
  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
3543
 
3272
- if(rx===0 || ry===0){
3273
- pathData[i]= null;
3274
- remove.push(i);
3544
+ if (type !== 'A') {
3545
+ pathDataN.push(com);
3546
+ continue
3547
+ }
3275
3548
 
3276
- }
3549
+ let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3550
+ let comPrev = pathData[i - 1];
3551
+ let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3552
+ let M = { x: x0, y: y0 };
3553
+ let p = { x, y };
3277
3554
 
3278
- // rx and ry are large enough
3279
- if (rx >= 1 && (x === x0 || y === y0)) {
3280
- let diff = Math.abs(rx - ry) / rx;
3555
+ if (rx === 0 || ry === 0) {
3556
+ pathData[i] = null;
3557
+ }
3281
3558
 
3282
- // rx~==ry
3283
- if (diff < 0.01) {
3559
+ // test for elliptic
3560
+ let rat = rx / ry;
3561
+ let error = rx !== ry ? Math.abs(1 - rat) : 0;
3284
3562
 
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;
3563
+ if (error > 0.01) {
3289
3564
 
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
- }
3565
+ pathDataN.push(com);
3566
+ continue
3567
+
3568
+ }
3569
+
3570
+ // xAxis rotation is futile for circular arcs - reset
3571
+ com.values[2] = 0;
3572
+
3573
+ /**
3574
+ * test semi circles
3575
+ * rx and ry are large enough
3576
+ */
3577
+
3578
+ // 1. horizontal or vertical
3579
+ let thresh = getDistManhattan(M, p) * 0.001;
3580
+ let diffX = Math.abs(x - x0);
3581
+ let diffY = Math.abs(y - y0);
3582
+
3583
+ let isHorizontal = diffY < thresh;
3584
+ let isVertical = diffX < thresh;
3585
+
3586
+ // minify rx and ry
3587
+ if (isHorizontal || isVertical) {
3588
+
3589
+ // check if semi circle
3590
+ let needsTrueR = isHorizontal ? rx*1.9 > diffX : ry*1.9 > diffY;
3591
+
3592
+ // is semicircle we can simplify rx
3593
+ if (!needsTrueR) {
3594
+
3595
+ rx = rx >= 1 ? 1 : (rx > 0.5 ? 0.5 : rx);
3297
3596
  }
3597
+
3598
+ com.values[0] = rx;
3599
+ com.values[1] = rx;
3600
+ pathDataN.push(com);
3601
+ continue
3602
+
3298
3603
  }
3299
- });
3300
3604
 
3301
- if(remove.length) pathData = pathData.filter(Boolean);
3302
- return pathData;
3605
+ // 2. get true radius - if rx ~= diameter/distance we have a semicircle
3606
+ let r = getDistance(M, p) * 0.5;
3607
+ error = rx / r;
3608
+
3609
+ if (error < 0.5) {
3610
+ rx = r >= 1 ? 1 : (r > 0.5 ? 0.5 : r);
3611
+ }
3612
+
3613
+ com.values[0] = rx;
3614
+ com.values[1] = rx;
3615
+ pathDataN.push(com);
3616
+
3617
+ }
3618
+
3619
+ return pathDataN;
3303
3620
  }
3304
3621
 
3305
3622
  /**
@@ -3323,52 +3640,11 @@ function normalizePathData(pathData = [],
3323
3640
  return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
3324
3641
  }
3325
3642
 
3326
- /*
3327
- export function normalizePathData(pathData = [],
3328
- {
3329
- toAbsolute = true,
3330
- toLonghands = true,
3331
- quadraticToCubic = false,
3332
- arcToCubic = false,
3333
- arcAccuracy = 2,
3334
-
3335
- // assume we need full normalization
3336
- hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3643
+ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
3337
3644
 
3338
- } = {}
3339
- ) {
3340
-
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
-
3364
- }
3365
- */
3366
-
3367
- function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
3368
-
3369
- // test if cubic can be simplified to quadratic
3370
- let cp1X = interpolate(p0, cp1, 1.5);
3371
- let cp2X = interpolate(p, cp2, 1.5);
3645
+ // test if cubic can be simplified to quadratic
3646
+ let cp1X = interpolate(p0, cp1, 1.5);
3647
+ let cp2X = interpolate(p, cp2, 1.5);
3372
3648
 
3373
3649
  let dist0 = getDistManhattan(p0, p);
3374
3650
  let threshold = dist0 * 0.01 * tolerance;
@@ -4243,7 +4519,7 @@ function pathDataToTopLeft(pathData) {
4243
4519
  let { type, values } = com;
4244
4520
  let valsLen = values.length;
4245
4521
  if (valsLen) {
4246
- let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
4522
+ let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 };
4247
4523
  p.index = i;
4248
4524
  indices.push(p);
4249
4525
  }
@@ -4251,113 +4527,111 @@ function pathDataToTopLeft(pathData) {
4251
4527
 
4252
4528
  // reorder to top left most
4253
4529
 
4254
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
4530
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
4255
4531
  newIndex = indices[0].index;
4256
4532
 
4257
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4533
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4258
4534
  }
4259
4535
 
4260
- function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
4536
+ function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
4261
4537
 
4262
- let pathDataNew = [];
4538
+ let pathDataN = pathData;
4263
4539
  let l = pathData.length;
4264
4540
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4265
4541
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4266
4542
 
4267
- let linetos = pathData.filter(com => com.type === 'L');
4268
-
4269
- // check if order is ideal
4270
- let idxPenultimate = isClosed ? l-2 : l-1;
4543
+ let hasLinetos = false;
4271
4544
 
4545
+ // check if path is closed by explicit lineto
4546
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
4272
4547
  let penultimateCom = pathData[idxPenultimate];
4273
4548
  let penultimateType = penultimateCom.type;
4274
4549
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4275
4550
 
4276
4551
  // last L command ends at M
4277
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4278
-
4279
- // add closepath Z to enable order optimizations
4280
- if(!isClosed && autoClose && isClosingCommand){
4552
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4553
+ let lastIsLine = penultimateType === 'L';
4281
4554
 
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
- }
4555
+ // create index
4556
+ let indices = [];
4557
+ for (let i = 0; i < l; i++) {
4558
+ let com = pathData[i];
4559
+ let { type, values, p0, p } = com;
4294
4560
 
4295
- // if last segment is not closing or a lineto
4296
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
4297
- skipReorder = false;
4561
+ if(type==='L') hasLinetos = true;
4298
4562
 
4299
- // we can't change starting point for non closed paths
4300
- if (!isClosed) {
4301
- return pathData
4302
- }
4563
+ // exclude Z
4564
+ if (values.length) {
4565
+ values.slice(-2);
4303
4566
 
4304
- let newIndex = 0;
4567
+ let x = Math.min(p0.x, p.x);
4568
+ let y = Math.min(p0.y, p.y);
4305
4569
 
4306
- if (!skipReorder) {
4570
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
4571
+ let prevType = prevCom.type;
4307
4572
 
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
- }
4573
+ let item = { type: type, x, y, index: 0, prevType };
4574
+ item.index = i;
4575
+ indices.push(item);
4322
4576
  }
4323
4577
 
4324
- // find top most lineto
4578
+ }
4325
4579
 
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];
4580
+ let xMin = Infinity;
4581
+ let yMin = Infinity;
4582
+ let idx_top = null;
4583
+ let len = indices.length;
4329
4584
 
4330
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
4585
+ for (let i = 0; i < len; i++) {
4586
+ let com = indices[i];
4587
+ let { type, index, x, y, prevType } = com;
4331
4588
 
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
- }
4589
+ if (hasLinetos && prevType === 'L') {
4590
+ if (x < xMin && y < yMin) {
4591
+ idx_top = index-1;
4592
+ }
4593
+
4594
+ if (y < yMin) {
4595
+ yMin = y;
4596
+ }
4338
4597
 
4339
- // reorder
4340
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4598
+ if (x < xMin) {
4599
+ xMin = x;
4600
+ }
4601
+ }
4341
4602
  }
4342
4603
 
4343
- M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4604
+ // shift to better starting point
4605
+ if (idx_top) {
4606
+ pathDataN = shiftSvgStartingPoint(pathDataN, idx_top);
4344
4607
 
4345
- l = pathData.length;
4608
+ // update penultimate - reorder might have added new close paths
4609
+ l = pathDataN.length;
4610
+ M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) };
4611
+
4612
+ idxPenultimate = isClosed ? l - 2 : l - 1;
4613
+ penultimateCom = pathDataN[idxPenultimate];
4614
+ penultimateType = penultimateCom.type;
4615
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4616
+ lastIsLine = penultimateType ==='L';
4346
4617
 
4347
- // remove last lineto
4348
- penultimateCom = pathData[l - 2];
4349
- penultimateType = penultimateCom.type;
4350
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
4618
+ // last L command ends at M
4619
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4351
4620
 
4352
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4621
+ }
4353
4622
 
4354
- if (removeFinalLineto && isClosingCommand) {
4355
- pathData.splice(l - 2, 1);
4623
+ // remove unnecessary closing lineto
4624
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
4625
+ pathDataN.splice(l - 2, 1);
4356
4626
  }
4357
4627
 
4358
- pathDataNew.push(...pathData);
4628
+ // add close path
4629
+ if (autoClose && !isClosed && hasClosingCommand) {
4630
+ pathDataN.push({ type: 'Z', values: [] });
4631
+ }
4632
+
4633
+ return pathDataN
4359
4634
 
4360
- return pathDataNew
4361
4635
  }
4362
4636
 
4363
4637
  /**
@@ -4567,8 +4841,130 @@ function refineAdjacentExtremes(pathData, {
4567
4841
 
4568
4842
  }
4569
4843
 
4844
+ function getArcFromPoly(pts, precise = false) {
4845
+ if (pts.length < 3) return false
4846
+
4847
+ // Pick 3 well-spaced points
4848
+ let len = pts.length;
4849
+ let idx1 = Math.floor(len * 0.333);
4850
+ let idx2 = Math.floor(len * 0.666);
4851
+ let idx3 = Math.floor(len * 0.5);
4852
+
4853
+ let p1 = pts[0];
4854
+ let p2 = pts[idx3];
4855
+ let p3 = pts[len - 1];
4856
+
4857
+ // Radius (use start point)
4858
+ let pts1 = [p1, p2, p3];
4859
+ let centroid = getPolyArcCentroid(pts1);
4860
+
4861
+ let r = 0, deltaAngle = 0, startAngle = 0, endAngle = 0, angleData = {};
4862
+
4863
+ // check if radii are consistent
4864
+ if (precise) {
4865
+
4866
+ /**
4867
+ * check multiple centroids
4868
+ * if the polyline can be expressed as
4869
+ * an arc - all centroids should be close
4870
+ */
4871
+
4872
+ if (len > 3) {
4873
+ let centroid1 = getPolyArcCentroid([p1, pts[idx1], p3]);
4874
+ let centroid2 = getPolyArcCentroid([p1, pts[idx2], p3]);
4875
+
4876
+ if (!centroid1 || !centroid2) return false;
4877
+
4878
+ let dist0 = getDistManhattan(centroid, p2);
4879
+ let dist1 = getDistManhattan(centroid, centroid1);
4880
+ let dist2 = getDistManhattan(centroid, centroid2);
4881
+ let errorCentroid = (dist1 + dist2);
4882
+
4883
+ // centroids diverging too much
4884
+ if (errorCentroid > dist0 * 0.05) {
4885
+
4886
+ return false
4887
+ }
4888
+
4889
+ }
4890
+
4891
+ let rSqMid = getSquareDistance(centroid, p2);
4892
+
4893
+ for (let i = 0; i < len; i++) {
4894
+ let pt = pts[i];
4895
+ let rSq = getSquareDistance(centroid, pt);
4896
+ let error = Math.abs(rSqMid - rSq) / rSqMid;
4897
+
4898
+ if (error > 0.0025) {
4899
+ /*
4900
+ console.log('error', error, len, idx1, idx2, idx3);
4901
+ renderPoint(markers, centroid, 'orange')
4902
+ renderPoint(markers, p1, 'green')
4903
+ renderPoint(markers, p2)
4904
+ renderPoint(markers, p3, 'purple')
4905
+ */
4906
+ return false;
4907
+ }
4908
+ }
4909
+
4910
+ // calculate proper radius
4911
+ r = Math.sqrt(rSqMid);
4912
+ angleData = getDeltaAngle(centroid, p1, p3);
4913
+ ({ deltaAngle, startAngle, endAngle } = angleData);
4914
+
4915
+ } else {
4916
+ r = getDistance(centroid, p1);
4917
+ angleData = getDeltaAngle(centroid, p1, p3);
4918
+ ({ deltaAngle, startAngle, endAngle } = angleData);
4919
+ }
4920
+
4921
+ return {
4922
+ centroid,
4923
+ r,
4924
+ startAngle,
4925
+ endAngle,
4926
+ deltaAngle
4927
+ };
4928
+ }
4929
+
4930
+ function getPolyArcCentroid(pts = []) {
4931
+
4932
+ pts = pts.filter(pt => pt !== undefined);
4933
+ if (pts.length < 3) return false
4934
+
4935
+ let p1 = pts[0];
4936
+ let p2 = pts[Math.floor(pts.length / 2)];
4937
+ let p3 = pts[pts.length - 1];
4938
+
4939
+ let x1 = p1.x, y1 = p1.y;
4940
+ let x2 = p2.x, y2 = p2.y;
4941
+ let x3 = p3.x, y3 = p3.y;
4942
+
4943
+ let a = x1 - x2;
4944
+ let b = y1 - y2;
4945
+ let c = x1 - x3;
4946
+ let d = y1 - y3;
4947
+
4948
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4949
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
4950
+
4951
+ let det = a * d - b * c;
4952
+
4953
+ // colinear points
4954
+ if (Math.abs(det) < 1e-10) {
4955
+ return false;
4956
+ }
4957
+
4958
+ // find center of arc
4959
+ let cx = (d * e - b * f) / det;
4960
+ let cy = (-c * e + a * f) / det;
4961
+ let centroid = { x: cx, y: cy };
4962
+ return centroid
4963
+ }
4964
+
4570
4965
  function refineRoundedCorners(pathData, {
4571
4966
  threshold = 0,
4967
+ simplifyQuadraticCorners = false,
4572
4968
  tolerance = 1
4573
4969
  } = {}) {
4574
4970
 
@@ -4593,6 +4989,9 @@ function refineRoundedCorners(pathData, {
4593
4989
  let firstIsLine = pathData[1].type === 'L';
4594
4990
  let firstIsBez = pathData[1].type === 'C';
4595
4991
 
4992
+ // in case we have simplified a corner connecting to the start
4993
+ let M_adj = null;
4994
+
4596
4995
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
4597
4996
 
4598
4997
  // normalize closepath to lineto
@@ -4632,15 +5031,17 @@ function refineRoundedCorners(pathData, {
4632
5031
  // closing corner to start
4633
5032
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4634
5033
  comL1 = pathData[1];
5034
+
4635
5035
  comBez = [pathData[l - lastOff]];
4636
5036
 
4637
5037
  }
4638
5038
 
5039
+ // collect enclosed bezier segments
4639
5040
  for (let j = i + 1; j < l; j++) {
4640
5041
  let comN = pathData[j] ? pathData[j] : null;
4641
5042
  let comPrev = pathData[j - 1];
4642
5043
 
4643
- if (comPrev.type === 'C') {
5044
+ if (comPrev.type === 'C' && j > 2) {
4644
5045
  comBez.push(comPrev);
4645
5046
  }
4646
5047
 
@@ -4671,39 +5072,67 @@ function refineRoundedCorners(pathData, {
4671
5072
  let bezThresh = len3 * 0.5 * tolerance;
4672
5073
  let isSmall = bezThresh < len1 && bezThresh < len2;
4673
5074
 
5075
+ /*
5076
+ */
5077
+
4674
5078
  if (comBez.length && !signChange && isSmall) {
4675
5079
 
4676
- let isFlatBezier = Math.abs(area2) < getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
5080
+ let isSquare = false;
5081
+
5082
+ if (comBez.length === 1) {
5083
+ let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x);
5084
+ let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y);
5085
+ let diff = (dx - dy);
5086
+ let rat = Math.abs(diff / dx);
5087
+ isSquare = rat < 0.01;
5088
+ }
5089
+
5090
+ let preferArcs = true;
5091
+ preferArcs = false;
5092
+
5093
+ // if rectangular prefer arcs
5094
+ if (preferArcs && isSquare) {
5095
+
5096
+ let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5);
5097
+
5098
+ let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p]);
5099
+ let { r, centroid, deltaAngle } = arcProps;
5100
+
5101
+ let sweep = deltaAngle > 0 ? 1 : 0;
5102
+
5103
+ let largeArc = 0;
5104
+
5105
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] };
5106
+
5107
+ pathDataN.push(comL0, comArc);
5108
+ i += offset;
5109
+ continue
5110
+
5111
+ }
5112
+
5113
+ let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005;
5114
+ let isFlatBezier = Math.abs(area2) < areaThresh;
5115
+ let isFlatBezier2 = Math.abs(area2) < areaThresh * 10;
5116
+
4677
5117
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
4678
5118
 
4679
- if (!ptQ) {
5119
+ // exit: is rather flat or has no intersection
5120
+
5121
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
4680
5122
  pathDataN.push(com);
4681
5123
  continue
4682
5124
  }
4683
5125
 
4684
- // check sign change
5126
+ // check sign change - exit if present
4685
5127
  if (ptQ) {
4686
5128
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4687
5129
  let area0_abs = Math.abs(area0);
4688
5130
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
4689
5131
  let area1_abs = Math.abs(area1);
4690
5132
  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
5133
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
4704
5134
 
4705
5135
  if (!ptQ || signChange || areaDiff > 0.5) {
4706
-
4707
5136
  pathDataN.push(com);
4708
5137
  continue
4709
5138
  }
@@ -4718,24 +5147,67 @@ function refineRoundedCorners(pathData, {
4718
5147
 
4719
5148
  // not in tolerance – return original command
4720
5149
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
4721
-
4722
5150
  pathDataN.push(com);
4723
5151
  continue;
4724
5152
 
4725
- } else {
5153
+ }
4726
5154
 
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;
5155
+ // return simplified quadratic Bézier command
5156
+ let p_Q = comL1.p0;
4731
5157
 
4732
- // add quadratic command
4733
- pathDataN.push(comL0, comQ);
4734
- i += offset;
5158
+ // adjust previous end point to better fit the cubic curvature
5159
+ let adjustQ = !simplifyQuadraticCorners;
5160
+
5161
+ if (adjustQ) {
5162
+
5163
+ let t = 0.1666;
5164
+ let p0_adj = interpolate(ptQ, comL0.p, (1 + t));
5165
+ p_Q = interpolate(ptQ, comL1.p0, (1 + t));
5166
+
5167
+ // round for large enough segments
5168
+ let isH = ptQ.y===comL0.p.y;
5169
+ let isV = ptQ.x===comL0.p.x;
5170
+ let isH2 = ptQ.y===comL1.p0.y;
5171
+ let isV2 = ptQ.x===comL1.p0.x;
5172
+
5173
+ if(isSquare && com.dimA>3){
5174
+ let dec = 0.5;
5175
+ if(isH) p0_adj.x = roundTo(p0_adj.x, dec);
5176
+ if(isV) p0_adj.y = roundTo(p0_adj.y, dec);
5177
+ if(isH2) p_Q.x = roundTo(p_Q.x, dec);
5178
+ if(isV2) p_Q.y = roundTo(p_Q.y, dec);
5179
+ }
5180
+
5181
+ /*
5182
+ renderPoint(markers, p0_adj, 'orange')
5183
+ renderPoint(markers, p_Q, 'orange')
5184
+ renderPoint(markers, comL0.p, 'green')
5185
+ renderPoint(markers, comL1.p0, 'magenta')
5186
+ */
5187
+
5188
+ // set new M starting point
5189
+ if (i === l - lastOff - 1) {
5190
+
5191
+ M_adj = p_Q;
5192
+ }
5193
+
5194
+ // adjust previous lineto end point
5195
+ comL0.values = [p0_adj.x, p0_adj.y];
5196
+ comL0.p = p0_adj;
4735
5197
 
4736
- continue;
4737
5198
  }
4738
5199
 
5200
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] };
5201
+ comQ.cp1 = ptQ;
5202
+ comQ.p0 = comL0.p;
5203
+ comQ.p = p_Q;
5204
+
5205
+ // add quadratic command
5206
+ pathDataN.push(comL0, comQ);
5207
+
5208
+ i += offset;
5209
+ continue;
5210
+
4739
5211
  }
4740
5212
  }
4741
5213
  }
@@ -4749,6 +5221,12 @@ function refineRoundedCorners(pathData, {
4749
5221
 
4750
5222
  }
4751
5223
 
5224
+ // correct starting point connecting with last corner rounding
5225
+ if (M_adj) {
5226
+ pathDataN[0].values = [M_adj.x, M_adj.y];
5227
+ pathDataN[0].p0 = M_adj;
5228
+ }
5229
+
4752
5230
  // revert close path normalization
4753
5231
  if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
4754
5232
  pathDataN.push({ type: 'Z', values: [] });
@@ -4758,51 +5236,101 @@ function refineRoundedCorners(pathData, {
4758
5236
 
4759
5237
  }
4760
5238
 
4761
- function getArcFromPoly(pts) {
4762
- if (pts.length < 3) return false
5239
+ function refineClosingCommand(pathData = [], {
5240
+ threshold = 0,
5241
+ } = {}) {
4763
5242
 
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];
5243
+ let l = pathData.length;
5244
+ let comLast = pathData[l - 1];
5245
+ let isClosed = comLast.type.toLowerCase() === 'z';
5246
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
5247
+ let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
5248
+ let valsPen = comPenultimate.values.slice(-2);
4768
5249
 
4769
- let x1 = p1.x, y1 = p1.y;
4770
- let x2 = p2.x, y2 = p2.y;
4771
- let x3 = p3.x, y3 = p3.y;
5250
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5251
+ let pPen = { x: valsPen[0], y: valsPen[1] };
5252
+ let dist = getDistAv(M, pPen);
4772
5253
 
4773
- let a = x1 - x2;
4774
- let b = y1 - y2;
4775
- let c = x1 - x3;
4776
- let d = y1 - y3;
5254
+ // adjust last coordinates for better reordering
5255
+ if (dist && dist < threshold) {
4777
5256
 
4778
- let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4779
- let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
5257
+ let valsLast = pathData[idxPenultimate].values;
5258
+ let valsLastLen = valsLast.length;
5259
+ pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
5260
+ pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
4780
5261
 
4781
- let det = a * d - b * c;
5262
+ // adjust cpts
5263
+ let comFirst = pathData[1];
4782
5264
 
4783
- if (Math.abs(det) < 1e-10) {
4784
- console.warn("Points are collinear or numerically unstable");
4785
- return false;
5265
+ if (comFirst.type === 'C' && comPenultimate.type === 'C') {
5266
+ let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
5267
+ let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
5268
+
5269
+ let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
5270
+ let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
5271
+
5272
+ let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5273
+ let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5274
+
5275
+ let ver = dx2 < threshold && dx3 < threshold && dy1;
5276
+ let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5277
+
5278
+ if (dx1 && dx1 < threshold && ver) {
5279
+
5280
+ pathData[1].values[0] = M.x;
5281
+ pathData[idxPenultimate].values[2] = M.x;
5282
+ }
5283
+
5284
+ if (dy1 && dy1 < threshold && hor) {
5285
+
5286
+ pathData[1].values[1] = M.y;
5287
+ pathData[idxPenultimate].values[3] = M.y;
5288
+ }
5289
+
5290
+ }
4786
5291
  }
4787
5292
 
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 };
5293
+ return pathData;
4792
5294
 
4793
- // Radius (use start point)
4794
- let r = getDistance(centroid, p1);
5295
+ }
4795
5296
 
4796
- let angleData = getDeltaAngle(centroid, p1, p3);
4797
- let {deltaAngle, startAngle, endAngle} = angleData;
5297
+ function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
4798
5298
 
4799
- return {
4800
- centroid,
4801
- r,
4802
- startAngle,
4803
- endAngle,
4804
- deltaAngle
4805
- };
5299
+ for (let c = 1, l = pathData.length; c < l; c++) {
5300
+ let com = pathData[c];
5301
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5302
+ if (type === 'C') {
5303
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
5304
+ if (comQ.type === 'Q') {
5305
+ comQ.extreme = com.extreme;
5306
+ comQ.corner = com.corner;
5307
+ comQ.dimA = com.dimA;
5308
+ comQ.squareDist = com.squareDist;
5309
+ pathData[c] = comQ;
5310
+ }
5311
+ }
5312
+ }
5313
+ return pathData
5314
+ }
5315
+
5316
+ function pathDataLineToCubic(pathData) {
5317
+
5318
+ for (let c = 1, l = pathData.length; c < l; c++) {
5319
+ let com = pathData[c];
5320
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5321
+ if (type === 'L') {
5322
+
5323
+ let cp1 = interpolate(p0, p, 0.333);
5324
+ let cp2 = interpolate(p, p0, 0.333);
5325
+
5326
+ pathData[c].type = 'C';
5327
+ pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
5328
+ pathData[c].cp1 = cp1;
5329
+ pathData[c].cp2 = cp2;
5330
+
5331
+ }
5332
+ }
5333
+ return pathData
4806
5334
  }
4807
5335
 
4808
5336
  function refineRoundSegments(pathData, {
@@ -4821,9 +5349,6 @@ function refineRoundSegments(pathData, {
4821
5349
  // add fist command
4822
5350
  let pathDataN = [pathData[0]];
4823
5351
 
4824
- // just for debugging
4825
- let pathDataTest = [];
4826
-
4827
5352
  for (let i = 1; i < l; i++) {
4828
5353
  let com = pathData[i];
4829
5354
  let { type } = com;
@@ -4850,11 +5375,12 @@ function refineRoundSegments(pathData, {
4850
5375
 
4851
5376
  // 2. line-line-bezier-line-line
4852
5377
  if (
5378
+ comN2 && comN3 &&
4853
5379
  comP.type === 'L' &&
4854
5380
  type === 'L' &&
4855
5381
  comBez &&
4856
5382
  comN2.type === 'L' &&
4857
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
5383
+ (comN3.type === 'L' || comN3.type === 'Z')
4858
5384
  ) {
4859
5385
 
4860
5386
  L1 = [com.p0, com.p];
@@ -4881,10 +5407,10 @@ function refineRoundSegments(pathData, {
4881
5407
  }
4882
5408
 
4883
5409
  // 1. line-bezier-bezier-line
4884
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
5410
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
4885
5411
 
4886
5412
  // 1.2 next is cubic next is lineto
4887
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
5413
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
4888
5414
 
4889
5415
  combine = true;
4890
5416
 
@@ -4943,16 +5469,19 @@ function refineRoundSegments(pathData, {
4943
5469
  }
4944
5470
  );
4945
5471
 
4946
- if(bezierCommands.length === 1){
5472
+ if (bezierCommands.length === 1) {
4947
5473
 
4948
5474
  // prefer more compact quadratic - otherwise arcs
4949
5475
  let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
4950
5476
 
4951
5477
  if (comBezier.type === 'Q') {
4952
5478
  toCubic = true;
5479
+ }else {
5480
+ comBezier = bezierCommands[0];
4953
5481
  }
4954
5482
 
4955
5483
  com = comBezier;
5484
+
4956
5485
  }
4957
5486
 
4958
5487
  // prefer arcs if 2 cubics are required
@@ -4972,25 +5501,28 @@ function refineRoundSegments(pathData, {
4972
5501
 
4973
5502
  // test rendering
4974
5503
 
5504
+ /*
4975
5505
  if (debug) {
4976
5506
  // arcs
4977
5507
  if (!toCubic) {
4978
5508
  pathDataTest = [
4979
5509
  { type: 'M', values: [p0_S.x, p0_S.y] },
4980
5510
  { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
4981
- ];
5511
+ ]
4982
5512
  }
4983
5513
  // cubics
4984
5514
  else {
4985
5515
  pathDataTest = [
4986
5516
  { type: 'M', values: [p0_S.x, p0_S.y] },
4987
5517
  ...bezierCommands
4988
- ];
5518
+ ]
5519
+
4989
5520
  }
4990
5521
 
4991
5522
  let d = pathDataToD(pathDataTest);
4992
- renderPath(markers, d, 'orange', '0.5%', '0.5');
5523
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
4993
5524
  }
5525
+ */
4994
5526
 
4995
5527
  pathDataN.push(com);
4996
5528
  i++;
@@ -5007,104 +5539,6 @@ function refineRoundSegments(pathData, {
5007
5539
  return pathDataN;
5008
5540
  }
5009
5541
 
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
5542
  function simplifyPathData(input = '', {
5109
5543
 
5110
5544
  toAbsolute = true,
@@ -5162,7 +5596,10 @@ function simplifyPathData(input = '', {
5162
5596
  let yArr = [];
5163
5597
 
5164
5598
  // mode:0 – single path
5165
- let inputType = detectInputType(input);
5599
+
5600
+ let inputDetection = detectInputType(input);
5601
+ let {inputType, log} = inputDetection;
5602
+
5166
5603
  if (inputType === 'pathDataString') {
5167
5604
  d = input;
5168
5605
  } else if (inputType === 'polyString') {