svg-path-simplify 0.4.2 → 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 (61) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -4
  3. package/dist/svg-path-simplify.esm.js +3593 -1279
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +3594 -1278
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +1017 -538
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/docs/privacy-webapp.md +24 -0
  11. package/index.html +331 -152
  12. package/package.json +1 -1
  13. package/src/constants.js +4 -0
  14. package/src/css_parse.js +317 -0
  15. package/src/detect_input.js +76 -28
  16. package/src/index.js +8 -0
  17. package/src/pathData_simplify_cubic.js +26 -16
  18. package/src/pathData_simplify_harmonize_cpts.js +77 -1
  19. package/src/pathData_simplify_revertToquadratics.js +0 -1
  20. package/src/pathSimplify-main.js +304 -276
  21. package/src/pathSimplify-only-pathdata.js +7 -2
  22. package/src/pathSimplify-presets.js +254 -0
  23. package/src/poly-fit-curve-schneider.js +14 -7
  24. package/src/simplify_poly_RC.js +102 -0
  25. package/src/simplify_poly_RDP.js +109 -1
  26. package/src/simplify_poly_radial_distance.js +3 -3
  27. package/src/string_helpers.js +130 -4
  28. package/src/svg-getAttributes.js +4 -2
  29. package/src/svgii/convert_units.js +1 -1
  30. package/src/svgii/geometry.js +322 -5
  31. package/src/svgii/geometry_bbox_element.js +1 -1
  32. package/src/svgii/geometry_deduceRadius.js +116 -27
  33. package/src/svgii/geometry_length.js +253 -0
  34. package/src/svgii/pathData_analyze.js +18 -0
  35. package/src/svgii/pathData_convert.js +193 -89
  36. package/src/svgii/pathData_fix_directions.js +12 -14
  37. package/src/svgii/pathData_fromPoly.js +3 -3
  38. package/src/svgii/pathData_getLength.js +86 -0
  39. package/src/svgii/pathData_parse.js +2 -0
  40. package/src/svgii/pathData_parse_els.js +66 -68
  41. package/src/svgii/pathData_reorder.js +122 -16
  42. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  43. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  44. package/src/svgii/pathData_split_to_groups.js +168 -0
  45. package/src/svgii/pathData_stringify.js +26 -64
  46. package/src/svgii/pathData_toPolygon.js +3 -4
  47. package/src/svgii/poly_analyze.js +61 -0
  48. package/src/svgii/poly_normalize.js +11 -2
  49. package/src/svgii/poly_to_pathdata.js +85 -24
  50. package/src/svgii/rounding.js +80 -78
  51. package/src/svgii/svg_cleanup.js +421 -619
  52. package/src/svgii/svg_cleanup_convertPathLength.js +39 -0
  53. package/src/svgii/svg_cleanup_general_svg_atts.js +97 -0
  54. package/src/svgii/svg_cleanup_normalize_transforms.js +83 -0
  55. package/src/svgii/svg_cleanup_remove_els_and_atts.js +77 -0
  56. package/src/svgii/svg_cleanup_ungroup.js +36 -0
  57. package/src/svgii/svg_el_parse_style_props.js +72 -47
  58. package/src/svgii/svg_getElementLength.js +67 -0
  59. package/src/svgii/svg_validate.js +220 -0
  60. package/tests/testSVG.js +14 -1
  61. package/src/svgii/pathData_refine_round.js +0 -222
@@ -1,78 +1,327 @@
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
- if(input[0].length===2){
228
+ if (input[0].length === 2) {
17
229
 
18
- return 'polyArray'
230
+ result.inputType = 'polyArray';
19
231
  }
20
232
 
21
- else if (Array.isArray(input[0][0]) && input[0][0].length === 2 ) {
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
- else if (input[0].x!==undefined && input[0].y!==undefined) {
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") {
48
260
  input = input.trim();
49
261
  let isSVG = input.includes('<svg') && input.includes('</svg');
262
+ let isSymbol = input.startsWith('<symbol') && input.includes('</symbol');
50
263
  let isPathData = input.startsWith('M') || input.startsWith('m');
51
264
  let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length - 1, input.length));
265
+ let isJson = isNumberJson(input);
52
266
 
53
267
  if (isSVG) {
54
- 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
+
282
+ }
283
+
284
+ else if (isJson) {
285
+ result.inputType = 'json';
286
+ }
287
+
288
+ else if (isSymbol) {
289
+ result.inputType = 'symbol';
55
290
  }
56
291
  else if (isPathData) {
57
- type = 'pathDataString';
292
+ result.inputType = 'pathDataString';
58
293
  }
59
294
  else if (isPolyString) {
60
- type = 'polyString';
295
+ result.inputType = 'polyString';
61
296
  }
62
297
 
63
298
  else {
64
299
  let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
65
300
  let dataUrl = input.startsWith('data:image');
66
- type = url || dataUrl ? "url" : "string";
301
+ result.inputType = url || dataUrl ? "url" : "string";
67
302
  }
68
303
 
69
- return type
304
+ return result
70
305
  }
71
306
 
72
- type = typeof input;
73
- let constructor = input.constructor.name;
307
+ result.inputType = (input.constructor.name || typeof input ).toLowerCase();
308
+
309
+ return result;
310
+ }
311
+
312
+ function isNumberJson(str) {
313
+
314
+ str = str.trim();
315
+
316
+ let hasNumber = /\d/.test(str);
317
+ let hasInvalid = /[abcdfghijklmnopqrstuvwz]/gi.test(str);
318
+ if (!hasNumber || hasInvalid) return false
319
+
320
+ // is JSON like
321
+ let isJson = str.startsWith('[') && str.endsWith(']');
74
322
 
75
- return (constructor || type).toLowerCase();
323
+ return isJson
324
+
76
325
  }
77
326
 
78
327
  function renderPoint(
@@ -102,18 +351,6 @@ function renderPoint(
102
351
  }
103
352
  }
104
353
 
105
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
106
-
107
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
108
-
109
- if (render) {
110
- svg.insertAdjacentHTML("beforeend", path);
111
- } else {
112
- return path;
113
- }
114
-
115
- }
116
-
117
354
  /*
118
355
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
119
356
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -330,6 +567,7 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
330
567
  let t1 = 1 - t;
331
568
 
332
569
  // cubic beziers
570
+ /*
333
571
  if (isCubic) {
334
572
  pt = {
335
573
  x:
@@ -345,11 +583,29 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
345
583
  };
346
584
 
347
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
+
348
604
  // quadratic beziers
349
605
  else {
350
606
  pt = {
351
- x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
352
- 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,
353
609
  };
354
610
  }
355
611
 
@@ -384,7 +640,36 @@ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false, returnArray
384
640
  /**
385
641
  * get vertices from path command final on-path points
386
642
  */
387
- function getPathDataVertices(pathData) {
643
+
644
+ function getPathDataVertices(pathData=[], includeCpts = false, decimals = -1) {
645
+ let polyPoints = [];
646
+
647
+ pathData.forEach((com) => {
648
+ let { type, values } = com;
649
+
650
+ // get final on path point from last 2 values
651
+ if (values.length) {
652
+
653
+ // round
654
+ if (decimals > -1) values = values.map(val => +val.toFixed(decimals));
655
+
656
+ if (includeCpts) {
657
+
658
+ for (let i = 1; i < values.length; i += 2) {
659
+ polyPoints.push({ x: values[i - 1], y: values[i] });
660
+ }
661
+
662
+ } else {
663
+ polyPoints.push({ x: values[values.length - 2], y: values[values.length - 1] });
664
+ }
665
+
666
+ }
667
+ });
668
+ return polyPoints;
669
+ }
670
+
671
+ /*
672
+ export function getPathDataVertices(pathData) {
388
673
  let polyPoints = [];
389
674
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
390
675
 
@@ -399,22 +684,30 @@ function getPathDataVertices(pathData) {
399
684
  }
400
685
  });
401
686
  return polyPoints;
402
- }
687
+ };
688
+ */
403
689
 
404
690
  /**
405
691
  * based on @cuixiping;
406
692
  * https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
407
693
  */
408
- function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
694
+
695
+ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2, normalize = true
696
+ ) {
409
697
 
410
698
  // helper for angle calculation
411
- const getAngle = (cx, cy, x, y) => {
412
- return atan2(y - cy, x - cx);
699
+ const getAngle = (cx, cy, x, y, normalize = true) => {
700
+ let angle = Math.atan2(y - cy, x - cx);
701
+ if (normalize && angle < 0) angle += Math.PI * 2;
702
+ return angle
413
703
  };
414
704
 
415
705
  // make sure rx, ry are positive
416
- rx = abs(rx);
417
- ry = abs(ry);
706
+ rx = Math.abs(rx);
707
+ ry = Math.abs(ry);
708
+
709
+ // normalize xAxis rotation
710
+ xAxisRotation = rx === ry ? 0 : (xAxisRotation < 0 && normalize ? xAxisRotation + 360 : xAxisRotation);
418
711
 
419
712
  // create data object
420
713
  let arcData = {
@@ -438,53 +731,11 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
438
731
  throw Error("rx and ry can not be 0");
439
732
  }
440
733
 
441
- let shortcut = true;
442
-
443
- if (rx === ry && shortcut) {
444
-
445
- // test semicircles
446
- let diffX = Math.abs(x2 - x1);
447
- let diffY = Math.abs(y2 - y1);
448
- let r = diffX;
449
-
450
- let xMin = Math.min(x1, x2),
451
- yMin = Math.min(y1, y2),
452
- PIHalf = Math.PI * 0.5;
453
-
454
- // semi circles
455
- if (diffX === 0 && diffY || diffY === 0 && diffX) {
456
-
457
- r = diffX === 0 && diffY ? diffY / 2 : diffX / 2;
458
- arcData.rx = r;
459
- arcData.ry = r;
460
-
461
- // verical
462
- if (diffX === 0 && diffY) {
463
- arcData.cx = x1;
464
- arcData.cy = yMin + diffY / 2;
465
- arcData.startAngle = y1 > y2 ? PIHalf : -PIHalf;
466
- arcData.endAngle = y1 > y2 ? -PIHalf : PIHalf;
467
- arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
468
-
469
- }
470
- // horizontal
471
- else if (diffY === 0 && diffX) {
472
- arcData.cx = xMin + diffX / 2;
473
- arcData.cy = y1;
474
- arcData.startAngle = x1 > x2 ? Math.PI : 0;
475
- arcData.endAngle = x1 > x2 ? -Math.PI : Math.PI;
476
- arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
477
- }
478
-
479
- return arcData;
480
- }
481
- }
482
-
483
734
  /**
484
735
  * if rx===ry x-axis rotation is ignored
485
736
  * otherwise convert degrees to radians
486
737
  */
487
- let phi = rx === ry ? 0 : (xAxisRotation * PI) / 180;
738
+ let phi = rx === ry ? 0 : xAxisRotation * deg2rad;
488
739
  let cx, cy;
489
740
 
490
741
  let s_phi = !phi ? 0 : Math.sin(phi);
@@ -503,8 +754,9 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
503
754
  // Step 3: Ensure radii are large enough
504
755
  let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
505
756
  if (lambda > 1) {
506
- rx = rx * Math.sqrt(lambda);
507
- ry = ry * Math.sqrt(lambda);
757
+ let lambdaRoot = Math.sqrt(lambda);
758
+ rx = rx * lambdaRoot;
759
+ ry = ry * lambdaRoot;
508
760
 
509
761
  // save real rx/ry
510
762
  arcData.rx = rx;
@@ -520,7 +772,7 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
520
772
  throw Error("start point can not be same as end point");
521
773
  }
522
774
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
523
- if (largeArc == sweep) {
775
+ if (largeArc === sweep) {
524
776
  coe = -coe;
525
777
  }
526
778
 
@@ -540,24 +792,33 @@ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2,
540
792
  * calculate angles between center point and
541
793
  * commands starting and final on path point
542
794
  */
543
- let startAngle = getAngle(cx, cy, x1, y1);
544
- let endAngle = getAngle(cx, cy, x2, y2);
795
+ let startAngle = getAngle(cx, cy, x1, y1, normalize);
796
+ let endAngle = getAngle(cx, cy, x2, y2, normalize);
545
797
 
546
798
  // adjust end angle
547
- if (!sweep && endAngle > startAngle) {
548
-
549
- endAngle -= Math.PI * 2;
550
- }
551
-
552
- if (sweep && startAngle > endAngle) {
553
799
 
554
- endAngle = endAngle <= 0 ? endAngle + Math.PI * 2 : endAngle;
800
+ // Adjust angles based on sweep direction
801
+ if (sweep) {
802
+ // Clockwise
803
+ if (endAngle < startAngle) {
804
+ endAngle += Math.PI * 2;
805
+ }
806
+ } else {
807
+ // Counterclockwise
808
+ if (endAngle > startAngle) {
809
+ endAngle -= Math.PI * 2;
810
+ }
555
811
  }
556
812
 
557
813
  let deltaAngle = endAngle - startAngle;
814
+
815
+ // The rest of your code remains the same
558
816
  arcData.startAngle = startAngle;
817
+ arcData.startAngle_deg = startAngle * rad2Deg;
559
818
  arcData.endAngle = endAngle;
819
+ arcData.endAngle_deg = endAngle * rad2Deg;
560
820
  arcData.deltaAngle = deltaAngle;
821
+ arcData.deltaAngle_deg = deltaAngle * rad2Deg;
561
822
 
562
823
  return arcData;
563
824
  }
@@ -660,12 +921,6 @@ function getBezierExtremeT(pts, { addExtremes = true, addSemiExtremes = false }
660
921
  return tArr;
661
922
  }
662
923
 
663
- /**
664
- * based on Nikos M.'s answer
665
- * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
666
- * https://stackoverflow.com/questions/87734/#75031511
667
- * See also: https://github.com/foo123/Geometrize
668
- */
669
924
  function getArcExtemes(p0, values) {
670
925
  // compute point on ellipse from angle around ellipse (theta)
671
926
  const arc = (theta, cx, cy, rx, ry, alpha) => {
@@ -1063,7 +1318,7 @@ function getDistance(p1, p2, isArray = false) {
1063
1318
  let dx = isArray ? p2[0] - p1[0] : (p2.x - p1.x);
1064
1319
  let dy = isArray ? p2[1] - p1[1] : (p2.y - p1.y);
1065
1320
 
1066
- return sqrt(dx * dx + dy * dy);
1321
+ return Math.sqrt(dx * dx + dy * dy);
1067
1322
  }
1068
1323
 
1069
1324
  function getSquareDistance(p1, p2) {
@@ -1391,6 +1646,36 @@ function splitCommandAtTValues(p0, values, tArr, returnCommand = true) {
1391
1646
  return segmentPoints;
1392
1647
  }
1393
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
+
1394
1679
  function detectAccuracy(pathData) {
1395
1680
  let dims = [];
1396
1681
 
@@ -1412,7 +1697,7 @@ function detectAccuracy(pathData) {
1412
1697
 
1413
1698
  let dim_min = dims.sort();
1414
1699
 
1415
- let sliceIdx = Math.ceil(dim_min.length / 8);
1700
+ let sliceIdx = Math.ceil(dim_min.length / 6);
1416
1701
  dim_min = dim_min.slice(0, sliceIdx);
1417
1702
  let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
1418
1703
 
@@ -1424,38 +1709,29 @@ function detectAccuracy(pathData) {
1424
1709
 
1425
1710
  }
1426
1711
 
1427
- function roundTo(num = 0, decimals = 3) {
1428
- if (!decimals) return Math.round(num);
1429
- let factor = 10 ** decimals;
1430
- return Math.round(num * factor) / factor;
1431
- }
1432
-
1433
1712
  /**
1434
- * round path data
1435
- * either by explicit decimal value or
1436
- * based on suggested accuracy in path data
1713
+ * rounding helper
1714
+ * allows for quantized rounding
1715
+ * e.g 0.5 decimals s
1437
1716
  */
1438
- function roundPathData(pathData, decimalsGlobal = -1) {
1439
-
1440
- if (decimalsGlobal < 0) return pathData;
1441
-
1442
- let len = pathData.length;
1443
-
1444
- 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);
1445
1721
 
1446
- for (let c = 0; c < len; c++) {
1447
- let com = pathData[c];
1448
- let {values} = com;
1722
+ // stepped rounding
1723
+ let intPart = Math.floor(decimals);
1449
1724
 
1450
- let valLen = values.length;
1451
- 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;
1452
1728
 
1453
- for (let v = 0; v < valLen; v++) {
1454
- pathData[c].values[v] = roundTo(values[v], decimals);
1455
- }
1729
+ let step = 10 ** -intPart * f;
1730
+ return +(Math.round(num / step) * step).toFixed(8);
1456
1731
  }
1457
1732
 
1458
- return pathData;
1733
+ let factor = 10 ** decimals;
1734
+ return Math.round(num * factor) / factor;
1459
1735
  }
1460
1736
 
1461
1737
  /**
@@ -1843,20 +2119,24 @@ function getPolygonArea(points, absolute=false) {
1843
2119
  * d attribute string
1844
2120
  */
1845
2121
 
1846
- function pathDataToD(pathData, optimize = 0) {
1847
-
1848
- optimize = parseFloat(optimize);
2122
+ function pathDataToD(pathData, mode = 0) {
1849
2123
 
2124
+ mode = parseFloat(mode);
2125
+ /*
2126
+ 0 = max minification
2127
+ 0.5 = safe
2128
+ 1 = verbose
2129
+ 2 = beautify
2130
+ */
1850
2131
  let len = pathData.length;
1851
- let beautify = optimize > 1;
1852
- let minify = beautify || optimize ? false : true;
1853
2132
 
1854
- let d = '';
1855
2133
  let valsString = pathData[0].values.join(" ");
1856
- let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1857
- let separator_type = !minify ? ' ' : '';
2134
+ let separator_command = mode > 1 ? `\n` :
2135
+ ((mode < 1) ? '' : ' ');
2136
+ let separator_type = mode > 0.5 ? ' ' : '';
1858
2137
 
1859
- d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
2138
+ // 1st command
2139
+ let d = `${pathData[0].type}${separator_type}${valsString}${separator_command}`;
1860
2140
 
1861
2141
  for (let i = 1; i < len; i++) {
1862
2142
  let com0 = pathData[i - 1];
@@ -1865,7 +2145,7 @@ function pathDataToD(pathData, optimize = 0) {
1865
2145
  valsString = '';
1866
2146
 
1867
2147
  // Minify Arc commands (A/a) – actually sucks!
1868
- if (minify && (type === 'A' || type === 'a')) {
2148
+ if (!mode && (type === 'A' || type === 'a')) {
1869
2149
  values = [
1870
2150
  values[0], values[1], values[2],
1871
2151
  `${values[3]}${values[4]}${values[5]}`,
@@ -1874,14 +2154,14 @@ function pathDataToD(pathData, optimize = 0) {
1874
2154
  }
1875
2155
 
1876
2156
  // Omit type for repeated commands
1877
- type = (minify && com0.type === com.type && com.type.toLowerCase() !== 'm')
2157
+ type = ((mode < 1) && com0.type === com.type && com.type.toLowerCase() !== 'm')
1878
2158
  ? " "
1879
- : (minify && com0.type === "M" && com.type === "L"
2159
+ : ((mode < 1) && com0.type === "M" && com.type === "L"
1880
2160
  ? " "
1881
2161
  : com.type);
1882
2162
 
1883
2163
  // concatenate subsequent floating point values
1884
- if (minify) {
2164
+ if (!mode) {
1885
2165
 
1886
2166
  let prevWasFloat = false;
1887
2167
 
@@ -1905,22 +2185,23 @@ function pathDataToD(pathData, optimize = 0) {
1905
2185
  prevWasFloat = isSmallFloat;
1906
2186
  }
1907
2187
 
1908
- d += `${type}${separator_type}${valsString}${separator_command}`;
1909
-
1910
2188
  }
1911
2189
  // regular non-minified output
1912
2190
  else {
1913
- d += `${type}${separator_type}${values.join(' ')}${separator_command}`;
2191
+ valsString = values.join(' ');
1914
2192
  }
2193
+
2194
+ if(i===len-1) separator_command='';
2195
+ d += `${type}${separator_type}${valsString}${separator_command}`;
1915
2196
  }
1916
2197
 
1917
- if (minify) {
2198
+ if (mode < 1) {
1918
2199
  d = d
1919
2200
  .replace(/[A-Za-z]0(?=\.)/g, m => m[0])
1920
2201
  .replace(/ 0\./g, " .") // Space before small decimals
1921
2202
  .replace(/ -/g, "-") // Remove space before negatives
1922
2203
  .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
1923
- .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
2204
+ .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
1924
2205
  }
1925
2206
 
1926
2207
  return d;
@@ -2260,9 +2541,9 @@ function combineCubicPairs(com1, com2, {
2260
2541
  let comS = getExtrapolatedCommand(com1, com2, t);
2261
2542
 
2262
2543
  // test new point-at-t against original mid segment starting point
2263
- 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);
2264
2545
 
2265
- let dist0 = getDistManhattan(com1.p, pt);
2546
+ let dist0 = getDistManhattan(com1.p, ptI);
2266
2547
  let dist1 = 0, dist2 = 0;
2267
2548
  let close = dist0 < maxDist;
2268
2549
  let success = false;
@@ -2277,29 +2558,40 @@ function combineCubicPairs(com1, com2, {
2277
2558
  * to prevent distortions
2278
2559
  */
2279
2560
 
2280
- // 2nd segment mid
2281
- 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);
2282
2563
 
2283
- // simplified path
2284
- let t3 = (1 + t) * 0.5;
2285
- let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
2286
- 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);
2287
2568
 
2288
2569
  error += dist1;
2289
2570
 
2290
2571
  if (dist1 < maxDist) {
2291
2572
 
2292
- // 1st segment mid
2293
- 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);
2294
2575
 
2295
- let t2 = t * 0.5;
2296
- let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
2297
- 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);
2298
2580
 
2299
2581
  error += dist2;
2300
2582
 
2301
2583
  if (error < maxDist) success = true;
2302
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
+
2303
2595
  }
2304
2596
 
2305
2597
  } // end 1st try
@@ -2464,6 +2756,7 @@ function analyzePathData(pathData = [], {
2464
2756
  let com = pathData[c - 1];
2465
2757
  let { type, values, p0, p, cp1 = null, cp2 = null, squareDist = 0, cptArea = 0, dimA = 0 } = com;
2466
2758
 
2759
+ let comPrev = pathData[c-2];
2467
2760
  let comN = pathData[c] || null;
2468
2761
 
2469
2762
  // init properties
@@ -2482,6 +2775,7 @@ function analyzePathData(pathData = [], {
2482
2775
 
2483
2776
  // bezier types
2484
2777
  let isBezier = type === 'Q' || type === 'C';
2778
+ let isArc = type === 'A';
2485
2779
  let isBezierN = comN && (comN.type === 'Q' || comN.type === 'C');
2486
2780
 
2487
2781
  /**
@@ -2528,6 +2822,22 @@ function analyzePathData(pathData = [], {
2528
2822
  }
2529
2823
  }
2530
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
+
2531
2841
  if (hasExtremes) com.extreme = true;
2532
2842
 
2533
2843
  // Corners and semi extremes
@@ -2797,6 +3107,7 @@ const sanitizeArc = (val='', valueIndex=0) => {
2797
3107
  };
2798
3108
 
2799
3109
  function parsePathDataString(d, debug = true, limit=0) {
3110
+ if(!d) return []
2800
3111
  d = d.trim();
2801
3112
 
2802
3113
  if(limit) console.log('!!!limit', limit);
@@ -3080,50 +3391,10 @@ function parsePathDataString(d, debug = true, limit=0) {
3080
3391
 
3081
3392
  }
3082
3393
 
3083
- function parsePathDataNormalized(d,
3084
- {
3085
- // necessary for most calculations
3086
- toAbsolute = true,
3087
- toLonghands = true,
3088
-
3089
- // not necessary unless you need cubics only
3090
- quadraticToCubic = false,
3091
-
3092
- // mostly a fallback if arc calculations fail
3093
- arcToCubic = false,
3094
- // arc to cubic precision - adds more segments for better precision
3095
- arcAccuracy = 4,
3096
- } = {}
3097
- ) {
3098
-
3099
- // is already array
3100
- let isArray = Array.isArray(d);
3101
-
3102
- // normalize native pathData to regular array
3103
- let hasConstructor = isArray && typeof d[0] === 'object' && typeof d[0].constructor === 'function';
3104
- /*
3105
- if (hasConstructor) {
3106
- d = d.map(com => { return { type: com.type, values: com.values } })
3107
- console.log('hasConstructor', hasConstructor, (typeof d[0].constructor), d);
3108
- }
3109
- */
3110
-
3111
- let pathDataObj = isArray ? d : parsePathDataString(d);
3112
-
3113
- let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3114
- let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3115
-
3116
- // normalize
3117
- pathData = normalizePathData(pathData,
3118
- {
3119
- toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3120
- hasRelatives, hasShorthands, hasQuadratics, hasArcs
3121
- },
3122
- );
3123
-
3124
- return pathData;
3125
- }
3126
-
3394
+ /**
3395
+ * wrapper function for
3396
+ * all path data conversion
3397
+ */
3127
3398
  function convertPathData(pathData, {
3128
3399
  toShorthands = true,
3129
3400
  toLonghands = false,
@@ -3139,6 +3410,7 @@ function convertPathData(pathData, {
3139
3410
  hasShorthands = true,
3140
3411
  hasQuadratics = true,
3141
3412
  hasArcs = true,
3413
+ isPoly = false,
3142
3414
  optimizeArcs = true,
3143
3415
  testTypes = false
3144
3416
 
@@ -3159,6 +3431,7 @@ function convertPathData(pathData, {
3159
3431
 
3160
3432
  // some params exclude each other
3161
3433
  toRelative = toAbsolute ? false : toRelative;
3434
+
3162
3435
  toShorthands = toLonghands ? false : toShorthands;
3163
3436
 
3164
3437
  if (toAbsolute) pathData = pathDataToAbsolute(pathData);
@@ -3173,22 +3446,24 @@ function convertPathData(pathData, {
3173
3446
 
3174
3447
  if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3175
3448
 
3176
- if(toMixed) toRelative = true;
3449
+ if (toMixed) toRelative = true;
3177
3450
 
3178
3451
  // pre round - before relative conversion to minimize distortions
3179
3452
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
3180
3453
 
3181
3454
  // clone absolute pathdata
3182
- if(toMixed){
3455
+ if (toMixed) {
3183
3456
  pathDataAbs = JSON.parse(JSON.stringify(pathData));
3184
3457
  }
3185
3458
 
3186
3459
  if (toRelative) pathData = pathDataToRelative(pathData);
3460
+
3461
+ // final rounding
3187
3462
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
3188
3463
 
3189
3464
  // choose most compact commands: relative or absolute
3190
- if(toMixed){
3191
- for(let i=0; i<pathData.length; i++){
3465
+ if (toMixed) {
3466
+ for (let i = 0; i < pathData.length; i++) {
3192
3467
  let com = pathData[i];
3193
3468
  let comA = pathDataAbs[i];
3194
3469
  // compare Lengths
@@ -3198,7 +3473,7 @@ function convertPathData(pathData, {
3198
3473
  let lenR = comStr.length;
3199
3474
  let lenA = comStrA.length;
3200
3475
 
3201
- if(lenA<lenR){
3476
+ if (lenA < lenR) {
3202
3477
 
3203
3478
  pathData[i] = pathDataAbs[i];
3204
3479
  }
@@ -3208,81 +3483,147 @@ function convertPathData(pathData, {
3208
3483
  return pathData
3209
3484
  }
3210
3485
 
3211
- /**
3212
- *
3213
- * @param {*} pathData
3214
- * @returns
3215
- */
3216
-
3217
- function optimizeArcPathData(pathData = []) {
3218
-
3219
- let remove =[];
3486
+ function parsePathDataNormalized(d,
3487
+ {
3488
+ // necessary for most calculations
3489
+ toAbsolute = true,
3490
+ toLonghands = true,
3220
3491
 
3221
- pathData.forEach((com, i) => {
3222
- let { type, values } = com;
3223
- if (type === 'A') {
3224
- let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3225
- let comPrev = pathData[i - 1];
3226
- let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3227
- let M = { x: x0, y: y0 };
3228
- let p = { x, y };
3492
+ // not necessary unless you need cubics only
3493
+ quadraticToCubic = false,
3229
3494
 
3230
- if(rx===0 || ry===0){
3231
- pathData[i]= null;
3232
- remove.push(i);
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
+ ) {
3233
3501
 
3234
- }
3502
+ // is already array
3503
+ let isArray = Array.isArray(d);
3235
3504
 
3236
- // rx and ry are large enough
3237
- if (rx >= 1 && (x === x0 || y === y0)) {
3238
- let diff = Math.abs(rx - ry) / rx;
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
+ */
3239
3513
 
3240
- // rx~==ry
3241
- if (diff < 0.01) {
3514
+ let pathDataObj = isArray ? d : parsePathDataString(d);
3242
3515
 
3243
- // test radius against mid point
3244
- let pMid = interpolate(M, p, 0.5);
3245
- let distM = getDistance(pMid, M);
3246
- let rDiff = Math.abs(distM - rx) / rx;
3516
+ let { hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true } = pathDataObj;
3517
+ let pathData = hasConstructor ? pathDataObj : pathDataObj.pathData;
3247
3518
 
3248
- // half distance between mid and start point should be ~ equal
3249
- if(rDiff<0.01){
3250
- pathData[i].values[0] = 1;
3251
- pathData[i].values[1] = 1;
3252
- pathData[i].values[2] = 0;
3253
- }
3254
- }
3255
- }
3256
- }
3257
- });
3519
+ // normalize
3520
+ pathData = normalizePathData(pathData,
3521
+ {
3522
+ toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy,
3523
+ hasRelatives, hasShorthands, hasQuadratics, hasArcs
3524
+ },
3525
+ );
3258
3526
 
3259
- if(remove.length) pathData = pathData.filter(Boolean);
3260
3527
  return pathData;
3261
3528
  }
3262
3529
 
3263
3530
  /**
3264
- * parse normalized
3531
+ *
3532
+ * @param {*} pathData
3533
+ * @returns
3265
3534
  */
3266
3535
 
3267
- function normalizePathData(pathData = [],
3268
- {
3269
- toAbsolute = true,
3270
- toLonghands = true,
3271
- quadraticToCubic = false,
3272
- arcToCubic = false,
3273
- arcAccuracy = 2,
3536
+ function optimizeArcPathData(pathData = []) {
3537
+ let l = pathData.length;
3538
+ let pathDataN = [];
3274
3539
 
3275
- // assume we need full normalization
3276
- hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3540
+ for (let i = 0; i < l; i++) {
3541
+ let com = pathData[i];
3542
+ let { type, values } = com;
3277
3543
 
3278
- } = {}
3279
- ) {
3544
+ if (type !== 'A') {
3545
+ pathDataN.push(com);
3546
+ continue
3547
+ }
3280
3548
 
3281
- return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
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 };
3554
+
3555
+ if (rx === 0 || ry === 0) {
3556
+ pathData[i] = null;
3557
+ }
3558
+
3559
+ // test for elliptic
3560
+ let rat = rx / ry;
3561
+ let error = rx !== ry ? Math.abs(1 - rat) : 0;
3562
+
3563
+ if (error > 0.01) {
3564
+
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);
3596
+ }
3597
+
3598
+ com.values[0] = rx;
3599
+ com.values[1] = rx;
3600
+ pathDataN.push(com);
3601
+ continue
3602
+
3603
+ }
3604
+
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;
3282
3620
  }
3283
3621
 
3284
- /*
3285
- export function normalizePathData(pathData = [],
3622
+ /**
3623
+ * parse normalized
3624
+ */
3625
+
3626
+ function normalizePathData(pathData = [],
3286
3627
  {
3287
3628
  toAbsolute = true,
3288
3629
  toLonghands = true,
@@ -3296,31 +3637,8 @@ export function normalizePathData(pathData = [],
3296
3637
  } = {}
3297
3638
  ) {
3298
3639
 
3299
- // pathdata properties - test= true adds a manual test
3300
- if (testTypes) {
3301
-
3302
- let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
3303
- hasRelatives = /[lcqamts]/gi.test(commands);
3304
- hasQuadratics = /[qt]/gi.test(commands);
3305
- hasArcs = /[a]/gi.test(commands);
3306
- hasShorthands = /[vhst]/gi.test(commands);
3307
- isPoly = /[mlz]/gi.test(commands);
3308
- }
3309
-
3310
- if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
3311
- toLonghands = true
3312
- toAbsolute = true
3313
- }
3314
-
3315
- if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
3316
- if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
3317
- if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
3318
- if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3319
-
3320
- return pathData;
3321
-
3640
+ return convertPathData(pathData, { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy, hasRelatives, hasShorthands, hasQuadratics, hasArcs, testTypes, decimals: -1 })
3322
3641
  }
3323
- */
3324
3642
 
3325
3643
  function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}, tolerance = 1) {
3326
3644
 
@@ -4201,7 +4519,7 @@ function pathDataToTopLeft(pathData) {
4201
4519
  let { type, values } = com;
4202
4520
  let valsLen = values.length;
4203
4521
  if (valsLen) {
4204
- 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 };
4205
4523
  p.index = i;
4206
4524
  indices.push(p);
4207
4525
  }
@@ -4209,113 +4527,111 @@ function pathDataToTopLeft(pathData) {
4209
4527
 
4210
4528
  // reorder to top left most
4211
4529
 
4212
- 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);
4213
4531
  newIndex = indices[0].index;
4214
4532
 
4215
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4533
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4216
4534
  }
4217
4535
 
4218
- function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
4536
+ function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
4219
4537
 
4220
- let pathDataNew = [];
4538
+ let pathDataN = pathData;
4221
4539
  let l = pathData.length;
4222
4540
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4223
4541
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4224
4542
 
4225
- let linetos = pathData.filter(com => com.type === 'L');
4226
-
4227
- // check if order is ideal
4228
- let idxPenultimate = isClosed ? l-2 : l-1;
4543
+ let hasLinetos = false;
4229
4544
 
4545
+ // check if path is closed by explicit lineto
4546
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
4230
4547
  let penultimateCom = pathData[idxPenultimate];
4231
4548
  let penultimateType = penultimateCom.type;
4232
4549
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4233
4550
 
4234
4551
  // last L command ends at M
4235
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4552
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4553
+ let lastIsLine = penultimateType === 'L';
4236
4554
 
4237
- // add closepath Z to enable order optimizations
4238
- if(!isClosed && autoClose && isClosingCommand){
4239
-
4240
- /*
4241
- // adjust final coords
4242
- let valsLast = pathData[idxPenultimate].values
4243
- let valsLastLen = valsLast.length;
4244
- pathData[idxPenultimate].values[valsLastLen-2] = M.x
4245
- pathData[idxPenultimate].values[valsLastLen-1] = M.y
4246
- */
4247
-
4248
- pathData.push({type:'Z', values:[]});
4249
- isClosed = true;
4250
- l++;
4251
- }
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;
4252
4560
 
4253
- // if last segment is not closing or a lineto
4254
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
4255
- skipReorder = false;
4561
+ if(type==='L') hasLinetos = true;
4256
4562
 
4257
- // we can't change starting point for non closed paths
4258
- if (!isClosed) {
4259
- return pathData
4260
- }
4563
+ // exclude Z
4564
+ if (values.length) {
4565
+ values.slice(-2);
4261
4566
 
4262
- let newIndex = 0;
4567
+ let x = Math.min(p0.x, p.x);
4568
+ let y = Math.min(p0.y, p.y);
4263
4569
 
4264
- if (!skipReorder) {
4570
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate];
4571
+ let prevType = prevCom.type;
4265
4572
 
4266
- let indices = [];
4267
- for (let i = 0; i < l; i++) {
4268
- let com = pathData[i];
4269
- let { type, values } = com;
4270
- if (values.length) {
4271
- let valsL = values.slice(-2);
4272
- let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
4273
- let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
4274
- let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
4275
- let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
4276
- let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
4277
- p.index = i;
4278
- indices.push(p);
4279
- }
4573
+ let item = { type: type, x, y, index: 0, prevType };
4574
+ item.index = i;
4575
+ indices.push(item);
4280
4576
  }
4281
4577
 
4282
- // find top most lineto
4578
+ }
4579
+
4580
+ let xMin = Infinity;
4581
+ let yMin = Infinity;
4582
+ let idx_top = null;
4583
+ let len = indices.length;
4283
4584
 
4284
- if (linetos.length) {
4285
- let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
4286
- com.prevCom === 'L' || com.prevCom === 'M' && penultimateType === 'L').sort((a, b) => a.y - b.y || a.x - b.x)[0];
4585
+ for (let i = 0; i < len; i++) {
4586
+ let com = indices[i];
4587
+ let { type, index, x, y, prevType } = com;
4287
4588
 
4288
- newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
4589
+ if (hasLinetos && prevType === 'L') {
4590
+ if (x < xMin && y < yMin) {
4591
+ idx_top = index-1;
4592
+ }
4289
4593
 
4290
- }
4291
- // use top most command
4292
- else {
4293
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
4294
- newIndex = indices[0].index;
4295
- }
4594
+ if (y < yMin) {
4595
+ yMin = y;
4596
+ }
4296
4597
 
4297
- // reorder
4298
- pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4598
+ if (x < xMin) {
4599
+ xMin = x;
4600
+ }
4601
+ }
4299
4602
  }
4300
4603
 
4301
- 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);
4302
4607
 
4303
- 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) };
4304
4611
 
4305
- // remove last lineto
4306
- penultimateCom = pathData[l - 2];
4307
- penultimateType = penultimateCom.type;
4308
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
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';
4617
+
4618
+ // last L command ends at M
4619
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4620
+
4621
+ }
4309
4622
 
4310
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4623
+ // remove unnecessary closing lineto
4624
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
4625
+ pathDataN.splice(l - 2, 1);
4626
+ }
4311
4627
 
4312
- if (removeFinalLineto && isClosingCommand) {
4313
- pathData.splice(l - 2, 1);
4628
+ // add close path
4629
+ if (autoClose && !isClosed && hasClosingCommand) {
4630
+ pathDataN.push({ type: 'Z', values: [] });
4314
4631
  }
4315
4632
 
4316
- pathDataNew.push(...pathData);
4633
+ return pathDataN
4317
4634
 
4318
- return pathDataNew
4319
4635
  }
4320
4636
 
4321
4637
  /**
@@ -4525,8 +4841,130 @@ function refineAdjacentExtremes(pathData, {
4525
4841
 
4526
4842
  }
4527
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
+
4528
4965
  function refineRoundedCorners(pathData, {
4529
4966
  threshold = 0,
4967
+ simplifyQuadraticCorners = false,
4530
4968
  tolerance = 1
4531
4969
  } = {}) {
4532
4970
 
@@ -4551,6 +4989,9 @@ function refineRoundedCorners(pathData, {
4551
4989
  let firstIsLine = pathData[1].type === 'L';
4552
4990
  let firstIsBez = pathData[1].type === 'C';
4553
4991
 
4992
+ // in case we have simplified a corner connecting to the start
4993
+ let M_adj = null;
4994
+
4554
4995
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
4555
4996
 
4556
4997
  // normalize closepath to lineto
@@ -4590,15 +5031,17 @@ function refineRoundedCorners(pathData, {
4590
5031
  // closing corner to start
4591
5032
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4592
5033
  comL1 = pathData[1];
5034
+
4593
5035
  comBez = [pathData[l - lastOff]];
4594
5036
 
4595
5037
  }
4596
5038
 
5039
+ // collect enclosed bezier segments
4597
5040
  for (let j = i + 1; j < l; j++) {
4598
5041
  let comN = pathData[j] ? pathData[j] : null;
4599
5042
  let comPrev = pathData[j - 1];
4600
5043
 
4601
- if (comPrev.type === 'C') {
5044
+ if (comPrev.type === 'C' && j > 2) {
4602
5045
  comBez.push(comPrev);
4603
5046
  }
4604
5047
 
@@ -4629,39 +5072,67 @@ function refineRoundedCorners(pathData, {
4629
5072
  let bezThresh = len3 * 0.5 * tolerance;
4630
5073
  let isSmall = bezThresh < len1 && bezThresh < len2;
4631
5074
 
5075
+ /*
5076
+ */
5077
+
4632
5078
  if (comBez.length && !signChange && isSmall) {
4633
5079
 
4634
- 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
+
4635
5117
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null;
4636
5118
 
4637
- if (!ptQ) {
5119
+ // exit: is rather flat or has no intersection
5120
+
5121
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
4638
5122
  pathDataN.push(com);
4639
5123
  continue
4640
5124
  }
4641
5125
 
4642
- // check sign change
5126
+ // check sign change - exit if present
4643
5127
  if (ptQ) {
4644
5128
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
4645
5129
  let area0_abs = Math.abs(area0);
4646
5130
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
4647
5131
  let area1_abs = Math.abs(area1);
4648
5132
  let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs;
4649
-
4650
- /*
4651
- renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
4652
- renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
4653
- renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
4654
- renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
4655
- if(!area0) {
4656
- pathDataN.push(com);
4657
- continue
4658
- }
4659
- */
4660
-
4661
5133
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
4662
5134
 
4663
5135
  if (!ptQ || signChange || areaDiff > 0.5) {
4664
-
4665
5136
  pathDataN.push(com);
4666
5137
  continue
4667
5138
  }
@@ -4676,24 +5147,67 @@ function refineRoundedCorners(pathData, {
4676
5147
 
4677
5148
  // not in tolerance – return original command
4678
5149
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
4679
-
4680
5150
  pathDataN.push(com);
4681
5151
  continue;
4682
5152
 
4683
- } else {
5153
+ }
4684
5154
 
4685
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4686
- comQ.p0 = comL0.p;
4687
- comQ.cp1 = ptQ;
4688
- comQ.p = comL1.p0;
5155
+ // return simplified quadratic Bézier command
5156
+ let p_Q = comL1.p0;
4689
5157
 
4690
- // add quadratic command
4691
- pathDataN.push(comL0, comQ);
4692
- 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;
4693
5197
 
4694
- continue;
4695
5198
  }
4696
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
+
4697
5211
  }
4698
5212
  }
4699
5213
  }
@@ -4707,6 +5221,12 @@ function refineRoundedCorners(pathData, {
4707
5221
 
4708
5222
  }
4709
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
+
4710
5230
  // revert close path normalization
4711
5231
  if (normalizeClose || (isClosed && pathDataN[pathDataN.length - 1].type !== 'Z')) {
4712
5232
  pathDataN.push({ type: 'Z', values: [] });
@@ -4716,51 +5236,101 @@ function refineRoundedCorners(pathData, {
4716
5236
 
4717
5237
  }
4718
5238
 
4719
- function getArcFromPoly(pts) {
4720
- if (pts.length < 3) return false
5239
+ function refineClosingCommand(pathData = [], {
5240
+ threshold = 0,
5241
+ } = {}) {
4721
5242
 
4722
- // Pick 3 well-spaced points
4723
- let p1 = pts[0];
4724
- let p2 = pts[Math.floor(pts.length / 2)];
4725
- 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);
4726
5249
 
4727
- let x1 = p1.x, y1 = p1.y;
4728
- let x2 = p2.x, y2 = p2.y;
4729
- 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);
4730
5253
 
4731
- let a = x1 - x2;
4732
- let b = y1 - y2;
4733
- let c = x1 - x3;
4734
- let d = y1 - y3;
5254
+ // adjust last coordinates for better reordering
5255
+ if (dist && dist < threshold) {
4735
5256
 
4736
- let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4737
- 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;
4738
5261
 
4739
- let det = a * d - b * c;
5262
+ // adjust cpts
5263
+ let comFirst = pathData[1];
4740
5264
 
4741
- if (Math.abs(det) < 1e-10) {
4742
- console.warn("Points are collinear or numerically unstable");
4743
- 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
+ }
4744
5291
  }
4745
5292
 
4746
- // find center of arc
4747
- let cx = (d * e - b * f) / det;
4748
- let cy = (-c * e + a * f) / det;
4749
- let centroid = { x: cx, y: cy };
5293
+ return pathData;
4750
5294
 
4751
- // Radius (use start point)
4752
- let r = getDistance(centroid, p1);
5295
+ }
4753
5296
 
4754
- let angleData = getDeltaAngle(centroid, p1, p3);
4755
- let {deltaAngle, startAngle, endAngle} = angleData;
5297
+ function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
4756
5298
 
4757
- return {
4758
- centroid,
4759
- r,
4760
- startAngle,
4761
- endAngle,
4762
- deltaAngle
4763
- };
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
4764
5334
  }
4765
5335
 
4766
5336
  function refineRoundSegments(pathData, {
@@ -4779,9 +5349,6 @@ function refineRoundSegments(pathData, {
4779
5349
  // add fist command
4780
5350
  let pathDataN = [pathData[0]];
4781
5351
 
4782
- // just for debugging
4783
- let pathDataTest = [];
4784
-
4785
5352
  for (let i = 1; i < l; i++) {
4786
5353
  let com = pathData[i];
4787
5354
  let { type } = com;
@@ -4808,11 +5375,12 @@ function refineRoundSegments(pathData, {
4808
5375
 
4809
5376
  // 2. line-line-bezier-line-line
4810
5377
  if (
5378
+ comN2 && comN3 &&
4811
5379
  comP.type === 'L' &&
4812
5380
  type === 'L' &&
4813
5381
  comBez &&
4814
5382
  comN2.type === 'L' &&
4815
- comN3 && (comN3.type === 'L' || comN3.type === 'Z')
5383
+ (comN3.type === 'L' || comN3.type === 'Z')
4816
5384
  ) {
4817
5385
 
4818
5386
  L1 = [com.p0, com.p];
@@ -4839,10 +5407,10 @@ function refineRoundSegments(pathData, {
4839
5407
  }
4840
5408
 
4841
5409
  // 1. line-bezier-bezier-line
4842
- else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
5410
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
4843
5411
 
4844
5412
  // 1.2 next is cubic next is lineto
4845
- if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
5413
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
4846
5414
 
4847
5415
  combine = true;
4848
5416
 
@@ -4901,16 +5469,19 @@ function refineRoundSegments(pathData, {
4901
5469
  }
4902
5470
  );
4903
5471
 
4904
- if(bezierCommands.length === 1){
5472
+ if (bezierCommands.length === 1) {
4905
5473
 
4906
5474
  // prefer more compact quadratic - otherwise arcs
4907
5475
  let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
4908
5476
 
4909
5477
  if (comBezier.type === 'Q') {
4910
5478
  toCubic = true;
5479
+ }else {
5480
+ comBezier = bezierCommands[0];
4911
5481
  }
4912
5482
 
4913
5483
  com = comBezier;
5484
+
4914
5485
  }
4915
5486
 
4916
5487
  // prefer arcs if 2 cubics are required
@@ -4930,25 +5501,28 @@ function refineRoundSegments(pathData, {
4930
5501
 
4931
5502
  // test rendering
4932
5503
 
5504
+ /*
4933
5505
  if (debug) {
4934
5506
  // arcs
4935
5507
  if (!toCubic) {
4936
5508
  pathDataTest = [
4937
5509
  { type: 'M', values: [p0_S.x, p0_S.y] },
4938
5510
  { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
4939
- ];
5511
+ ]
4940
5512
  }
4941
5513
  // cubics
4942
5514
  else {
4943
5515
  pathDataTest = [
4944
5516
  { type: 'M', values: [p0_S.x, p0_S.y] },
4945
5517
  ...bezierCommands
4946
- ];
5518
+ ]
5519
+
4947
5520
  }
4948
5521
 
4949
5522
  let d = pathDataToD(pathDataTest);
4950
- renderPath(markers, d, 'orange', '0.5%', '0.5');
5523
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
4951
5524
  }
5525
+ */
4952
5526
 
4953
5527
  pathDataN.push(com);
4954
5528
  i++;
@@ -4965,104 +5539,6 @@ function refineRoundSegments(pathData, {
4965
5539
  return pathDataN;
4966
5540
  }
4967
5541
 
4968
- function refineClosingCommand(pathData = [], {
4969
- threshold = 0,
4970
- } = {}) {
4971
-
4972
- let l = pathData.length;
4973
- let comLast = pathData[l - 1];
4974
- let isClosed = comLast.type.toLowerCase() === 'z';
4975
- let idxPenultimate = isClosed ? l - 2 : l - 1;
4976
- let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
4977
- let valsPen = comPenultimate.values.slice(-2);
4978
-
4979
- let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
4980
- let pPen = { x: valsPen[0], y: valsPen[1] };
4981
- let dist = getDistAv(M, pPen);
4982
-
4983
- // adjust last coordinates for better reordering
4984
- if (dist && dist < threshold) {
4985
-
4986
- let valsLast = pathData[idxPenultimate].values;
4987
- let valsLastLen = valsLast.length;
4988
- pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
4989
- pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
4990
-
4991
- // adjust cpts
4992
- let comFirst = pathData[1];
4993
-
4994
- if (comFirst.type === 'C' && comPenultimate.type === 'C') {
4995
- let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
4996
- let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
4997
-
4998
- let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
4999
- let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
5000
-
5001
- let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5002
- let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5003
-
5004
- let ver = dx2 < threshold && dx3 < threshold && dy1;
5005
- let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5006
-
5007
- if (dx1 && dx1 < threshold && ver) {
5008
-
5009
- pathData[1].values[0] = M.x;
5010
- pathData[idxPenultimate].values[2] = M.x;
5011
- }
5012
-
5013
- if (dy1 && dy1 < threshold && hor) {
5014
-
5015
- pathData[1].values[1] = M.y;
5016
- pathData[idxPenultimate].values[3] = M.y;
5017
- }
5018
-
5019
- }
5020
- }
5021
-
5022
- return pathData;
5023
-
5024
- }
5025
-
5026
- function pathDataRevertCubicToQuadratic(pathData, tolerance=1) {
5027
-
5028
- for (let c = 1, l = pathData.length; c < l; c++) {
5029
- let com = pathData[c];
5030
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5031
- if (type === 'C') {
5032
-
5033
- let comQ = revertCubicQuadratic(p0, cp1, cp2, p, tolerance);
5034
- if (comQ.type === 'Q') {
5035
- comQ.extreme = com.extreme;
5036
- comQ.corner = com.corner;
5037
- comQ.dimA = com.dimA;
5038
- comQ.squareDist = com.squareDist;
5039
- pathData[c] = comQ;
5040
- }
5041
- }
5042
- }
5043
- return pathData
5044
- }
5045
-
5046
- function pathDataLineToCubic(pathData) {
5047
-
5048
- for (let c = 1, l = pathData.length; c < l; c++) {
5049
- let com = pathData[c];
5050
- let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
5051
- if (type === 'L') {
5052
-
5053
- let cp1 = interpolate(p0, p, 0.333);
5054
- let cp2 = interpolate(p, p0, 0.333);
5055
-
5056
- pathData[c].type = 'C';
5057
- pathData[c].values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
5058
- pathData[c].cp1 = cp1;
5059
- pathData[c].cp2 = cp2;
5060
-
5061
- }
5062
- }
5063
- return pathData
5064
- }
5065
-
5066
5542
  function simplifyPathData(input = '', {
5067
5543
 
5068
5544
  toAbsolute = true,
@@ -5120,7 +5596,10 @@ function simplifyPathData(input = '', {
5120
5596
  let yArr = [];
5121
5597
 
5122
5598
  // mode:0 – single path
5123
- let inputType = detectInputType(input);
5599
+
5600
+ let inputDetection = detectInputType(input);
5601
+ let {inputType, log} = inputDetection;
5602
+
5124
5603
  if (inputType === 'pathDataString') {
5125
5604
  d = input;
5126
5605
  } else if (inputType === 'polyString') {