svg-path-simplify 0.0.1 → 0.0.2

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 (42) hide show
  1. package/README.md +28 -1
  2. package/dist/svg-path-simplify.esm.js +4040 -0
  3. package/dist/svg-path-simplify.esm.min.js +1 -0
  4. package/dist/svg-path-simplify.js +4065 -0
  5. package/dist/svg-path-simplify.min.js +1 -0
  6. package/dist/svg-path-simplify.node.js +4062 -0
  7. package/dist/svg-path-simplify.node.min.js +1 -0
  8. package/index.html +222 -0
  9. package/package.json +2 -2
  10. package/src/constants.js +4 -0
  11. package/src/index.js +18 -3
  12. package/src/pathData_simplify_cubic.js +324 -0
  13. package/src/pathData_simplify_cubic_arr.js +50 -0
  14. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  15. package/src/pathSimplify-main.js +294 -0
  16. package/src/svgii/...parse.js +402 -0
  17. package/src/svgii/geometry.js +1096 -0
  18. package/src/svgii/geometry_area.js +265 -0
  19. package/src/svgii/geometry_bbox.js +223 -0
  20. package/src/svgii/pathData_analyze.js +896 -0
  21. package/src/svgii/pathData_convert.js +1180 -0
  22. package/src/svgii/pathData_parse.js +487 -0
  23. package/src/svgii/pathData_remove_collinear.js +85 -0
  24. package/src/svgii/pathData_remove_zerolength.js +28 -0
  25. package/src/svgii/pathData_reorder.js +204 -0
  26. package/src/svgii/pathData_reverse.js +124 -0
  27. package/src/svgii/pathData_scale.js +42 -0
  28. package/src/svgii/pathData_split.js +449 -0
  29. package/src/svgii/pathData_stringify.js +146 -0
  30. package/src/svgii/pathData_toPolygon.js +92 -0
  31. package/src/svgii/pathdata_cleanup.js +363 -0
  32. package/src/svgii/poly_analyze.js +172 -0
  33. package/src/svgii/poly_to_pathdata.js +185 -0
  34. package/src/svgii/rounding.js +154 -0
  35. package/src/svgii/simplify.js +248 -0
  36. package/src/svgii/simplify_bezier.js +470 -0
  37. package/src/svgii/simplify_linetos.js +93 -0
  38. package/src/svgii/simplify_polygon.js +135 -0
  39. package/src/svgii/stringify.js +103 -0
  40. package/src/svgii/svg_cleanup.js +80 -0
  41. package/src/svgii/visualize.js +317 -0
  42. package/LICENSE +0 -21
@@ -0,0 +1,4065 @@
1
+ (function (exports) {
2
+ 'use strict';
3
+
4
+ function renderPoint(
5
+ svg,
6
+ coords,
7
+ fill = "red",
8
+ r = "1%",
9
+ opacity = "1",
10
+ title = '',
11
+ render = true,
12
+ id = "",
13
+ className = ""
14
+ ) {
15
+ if (Array.isArray(coords)) {
16
+ coords = {
17
+ x: coords[0],
18
+ y: coords[1]
19
+ };
20
+ }
21
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
22
+ <title>${title}</title></circle>`;
23
+
24
+ if (render) {
25
+ svg.insertAdjacentHTML("beforeend", marker);
26
+ } else {
27
+ return marker;
28
+ }
29
+ }
30
+
31
+ /*
32
+ import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
33
+ log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
34
+ */
35
+
36
+ const {
37
+ abs: abs$1, acos: acos$1, asin: asin$1, atan: atan$1, atan2: atan2$1, ceil: ceil$1, cos: cos$1, exp: exp$1, floor: floor$1,
38
+ log: log$1, max: max$1, min: min$1, pow: pow$1, random: random$1, round: round$1, sin: sin$1, sqrt: sqrt$1, tan: tan$1, PI: PI$1
39
+ } = Math;
40
+
41
+ // get angle helper
42
+ function getAngle(p1, p2, normalize = false) {
43
+ let angle = atan2$1(p2.y - p1.y, p2.x - p1.x);
44
+ // normalize negative angles
45
+ if (normalize && angle < 0) angle += Math.PI * 2;
46
+ return angle
47
+ }
48
+
49
+ /**
50
+ * based on: Justin C. Round's
51
+ * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
52
+ */
53
+
54
+ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
55
+ // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
56
+ let denominator, a, b, numerator1, numerator2;
57
+ let intersectionPoint = {};
58
+
59
+ try {
60
+ denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
61
+ if (denominator == 0) {
62
+ return false;
63
+ }
64
+
65
+ } catch {
66
+ console.log('!catch', p1, p2, 'p3:', p3, p4);
67
+ }
68
+
69
+ a = p1.y - p3.y;
70
+ b = p1.x - p3.x;
71
+ numerator1 = ((p4.x - p3.x) * a) - ((p4.y - p3.y) * b);
72
+ numerator2 = ((p2.x - p1.x) * a) - ((p2.y - p1.y) * b);
73
+
74
+ a = numerator1 / denominator;
75
+ b = numerator2 / denominator;
76
+
77
+ // if we cast these lines infinitely in both directions, they intersect here:
78
+ intersectionPoint = {
79
+ x: p1.x + (a * (p2.x - p1.x)),
80
+ y: p1.y + (a * (p2.y - p1.y))
81
+ };
82
+
83
+ // console.log('intersectionPoint', intersectionPoint, p1, p2);
84
+
85
+ let intersection = false;
86
+ // if line1 is a segment and line2 is infinite, they intersect if:
87
+ if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
88
+ intersection = true;
89
+
90
+ }
91
+
92
+ if (exact && !intersection) {
93
+
94
+ return false;
95
+ }
96
+
97
+ // if line1 and line2 are segments, they intersect if both of the above are true
98
+
99
+ return intersectionPoint;
100
+ }
101
+
102
+ /**
103
+ * get distance between 2 points
104
+ * pythagorean theorem
105
+ */
106
+ function getDistance(p1, p2) {
107
+ return sqrt$1(
108
+ (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
109
+ );
110
+ }
111
+
112
+ function getSquareDistance(p1, p2) {
113
+ return (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
114
+ }
115
+
116
+ /**
117
+ * Linear interpolation (LERP) helper
118
+ */
119
+ function interpolate(p1, p2, t, getTangent = false) {
120
+
121
+ let pt = {
122
+ x: (p2.x - p1.x) * t + p1.x,
123
+ y: (p2.y - p1.y) * t + p1.y,
124
+ };
125
+
126
+ if (getTangent) {
127
+ pt.angle = getAngle(p1, p2);
128
+
129
+ // normalize negative angles
130
+ if (pt.angle < 0) pt.angle += PI$1 * 2;
131
+ }
132
+
133
+ return pt
134
+ }
135
+
136
+ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false) {
137
+
138
+ const getPointAtBezierT = (pts, t, getTangent = false) => {
139
+
140
+ let isCubic = pts.length === 4;
141
+ let p0 = pts[0];
142
+ let cp1 = pts[1];
143
+ let cp2 = isCubic ? pts[2] : pts[1];
144
+ let p = pts[pts.length - 1];
145
+ let pt = { x: 0, y: 0 };
146
+
147
+ if (getTangent || getCpts) {
148
+ let m0, m1, m2, m3, m4;
149
+ let shortCp1 = p0.x === cp1.x && p0.y === cp1.y;
150
+ let shortCp2 = p.x === cp2.x && p.y === cp2.y;
151
+
152
+ if (t === 0 && !shortCp1) {
153
+ pt.x = p0.x;
154
+ pt.y = p0.y;
155
+ pt.angle = getAngle(p0, cp1);
156
+ }
157
+
158
+ else if (t === 1 && !shortCp2) {
159
+ pt.x = p.x;
160
+ pt.y = p.y;
161
+ pt.angle = getAngle(cp2, p);
162
+ }
163
+
164
+ else {
165
+ // adjust if cps are on start or end point
166
+ if (shortCp1) t += 0.0000001;
167
+ if (shortCp2) t -= 0.0000001;
168
+
169
+ m0 = interpolate(p0, cp1, t);
170
+ if (isCubic) {
171
+ m1 = interpolate(cp1, cp2, t);
172
+ m2 = interpolate(cp2, p, t);
173
+ m3 = interpolate(m0, m1, t);
174
+ m4 = interpolate(m1, m2, t);
175
+ pt = interpolate(m3, m4, t);
176
+
177
+ // add angles
178
+ pt.angle = getAngle(m3, m4);
179
+
180
+ // add control points
181
+ if (getCpts) pt.cpts = [m1, m2, m3, m4];
182
+ } else {
183
+ m1 = interpolate(p0, cp1, t);
184
+ m2 = interpolate(cp1, p, t);
185
+ pt = interpolate(m1, m2, t);
186
+ pt.angle = getAngle(m1, m2);
187
+
188
+ // add control points
189
+ if (getCpts) pt.cpts = [m1, m2];
190
+ }
191
+ }
192
+
193
+ }
194
+ // take simplified calculations without tangent angles
195
+ else {
196
+ let t1 = 1 - t;
197
+
198
+ // cubic beziers
199
+ if (isCubic) {
200
+ pt = {
201
+ x:
202
+ t1 ** 3 * p0.x +
203
+ 3 * t1 ** 2 * t * cp1.x +
204
+ 3 * t1 * t ** 2 * cp2.x +
205
+ t ** 3 * p.x,
206
+ y:
207
+ t1 ** 3 * p0.y +
208
+ 3 * t1 ** 2 * t * cp1.y +
209
+ 3 * t1 * t ** 2 * cp2.y +
210
+ t ** 3 * p.y,
211
+ };
212
+
213
+ }
214
+ // quadratic beziers
215
+ else {
216
+ pt = {
217
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
218
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
219
+ };
220
+ }
221
+
222
+ }
223
+
224
+ return pt
225
+
226
+ };
227
+
228
+ let pt;
229
+ if (pts.length > 2) {
230
+ pt = getPointAtBezierT(pts, t, getTangent);
231
+ }
232
+
233
+ else {
234
+ pt = interpolate(pts[0], pts[1], t, getTangent);
235
+ }
236
+
237
+ // normalize negative angles
238
+ if (getTangent && pt.angle < 0) pt.angle += PI$1 * 2;
239
+
240
+ return pt
241
+ }
242
+
243
+ /**
244
+ * get vertices from path command final on-path points
245
+ */
246
+ function getPathDataVertices(pathData) {
247
+ let polyPoints = [];
248
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
249
+
250
+ pathData.forEach((com) => {
251
+ let { type, values } = com;
252
+ // get final on path point from last 2 values
253
+ if (values.length) {
254
+ let pt = values.length > 1 ? { x: values[values.length - 2], y: values[values.length - 1] }
255
+ : (type === 'V' ? { x: p0.x, y: values[0] } : { x: values[0], y: p0.y });
256
+ polyPoints.push(pt);
257
+ p0 = pt;
258
+ }
259
+ });
260
+ return polyPoints;
261
+ }
262
+
263
+ /**
264
+ * based on @cuixiping;
265
+ * https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
266
+ */
267
+ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
268
+
269
+ // helper for angle calculation
270
+ const getAngle = (cx, cy, x, y) => {
271
+ return atan2$1(y - cy, x - cx);
272
+ };
273
+
274
+ // make sure rx, ry are positive
275
+ rx = abs$1(rx);
276
+ ry = abs$1(ry);
277
+
278
+ // create data object
279
+ let arcData = {
280
+ cx: 0,
281
+ cy: 0,
282
+ // rx/ry values may be deceptive in arc commands
283
+ rx: rx,
284
+ ry: ry,
285
+ startAngle: 0,
286
+ endAngle: 0,
287
+ deltaAngle: 0,
288
+ clockwise: sweep,
289
+ // copy explicit arc properties
290
+ xAxisRotation,
291
+ largeArc,
292
+ sweep
293
+ };
294
+
295
+ if (rx == 0 || ry == 0) {
296
+ // invalid arguments
297
+ throw Error("rx and ry can not be 0");
298
+ }
299
+
300
+ let shortcut = true;
301
+
302
+ if (rx === ry && shortcut) {
303
+
304
+ // test semicircles
305
+ let diffX = Math.abs(x2 - x1);
306
+ let diffY = Math.abs(y2 - y1);
307
+ let r = diffX;
308
+
309
+ let xMin = Math.min(x1, x2),
310
+ yMin = Math.min(y1, y2),
311
+ PIHalf = Math.PI * 0.5;
312
+
313
+ // semi circles
314
+ if (diffX === 0 && diffY || diffY === 0 && diffX) {
315
+
316
+ r = diffX === 0 && diffY ? diffY / 2 : diffX / 2;
317
+ arcData.rx = r;
318
+ arcData.ry = r;
319
+
320
+ // verical
321
+ if (diffX === 0 && diffY) {
322
+ arcData.cx = x1;
323
+ arcData.cy = yMin + diffY / 2;
324
+ arcData.startAngle = y1 > y2 ? PIHalf : -PIHalf;
325
+ arcData.endAngle = y1 > y2 ? -PIHalf : PIHalf;
326
+ arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
327
+
328
+ }
329
+ // horizontal
330
+ else if (diffY === 0 && diffX) {
331
+ arcData.cx = xMin + diffX / 2;
332
+ arcData.cy = y1;
333
+ arcData.startAngle = x1 > x2 ? Math.PI : 0;
334
+ arcData.endAngle = x1 > x2 ? -Math.PI : Math.PI;
335
+ arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
336
+ }
337
+
338
+ return arcData;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * if rx===ry x-axis rotation is ignored
344
+ * otherwise convert degrees to radians
345
+ */
346
+ let phi = rx === ry ? 0 : (xAxisRotation * PI$1) / 180;
347
+ let cx, cy;
348
+
349
+ let s_phi = !phi ? 0 : sin$1(phi);
350
+ let c_phi = !phi ? 1 : cos$1(phi);
351
+
352
+ let hd_x = (x1 - x2) / 2;
353
+ let hd_y = (y1 - y2) / 2;
354
+ let hs_x = (x1 + x2) / 2;
355
+ let hs_y = (y1 + y2) / 2;
356
+
357
+ // F6.5.1
358
+ let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
359
+ let y1_ = !phi ? hd_y : c_phi * hd_y - s_phi * hd_x;
360
+
361
+ // F.6.6 Correction of out-of-range radii
362
+ // Step 3: Ensure radii are large enough
363
+ let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
364
+ if (lambda > 1) {
365
+ rx = rx * sqrt$1(lambda);
366
+ ry = ry * sqrt$1(lambda);
367
+
368
+ // save real rx/ry
369
+ arcData.rx = rx;
370
+ arcData.ry = ry;
371
+ }
372
+
373
+ let rxry = rx * ry;
374
+ let rxy1_ = rx * y1_;
375
+ let ryx1_ = ry * x1_;
376
+ let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
377
+ if (!sum_of_sq) {
378
+
379
+ throw Error("start point can not be same as end point");
380
+ }
381
+ let coe = sqrt$1(abs$1((rxry * rxry - sum_of_sq) / sum_of_sq));
382
+ if (largeArc == sweep) {
383
+ coe = -coe;
384
+ }
385
+
386
+ // F6.5.2
387
+ let cx_ = (coe * rxy1_) / ry;
388
+ let cy_ = (-coe * ryx1_) / rx;
389
+
390
+ /** F6.5.3
391
+ * center point of ellipse
392
+ */
393
+ cx = !phi ? hs_x + cx_ : c_phi * cx_ - s_phi * cy_ + hs_x;
394
+ cy = !phi ? hs_y + cy_ : s_phi * cx_ + c_phi * cy_ + hs_y;
395
+ arcData.cy = cy;
396
+ arcData.cx = cx;
397
+
398
+ /** F6.5.5
399
+ * calculate angles between center point and
400
+ * commands starting and final on path point
401
+ */
402
+ let startAngle = getAngle(cx, cy, x1, y1);
403
+ let endAngle = getAngle(cx, cy, x2, y2);
404
+
405
+ // adjust end angle
406
+ if (!sweep && endAngle > startAngle) {
407
+
408
+ endAngle -= Math.PI * 2;
409
+ }
410
+
411
+ if (sweep && startAngle > endAngle) {
412
+
413
+ endAngle = endAngle <= 0 ? endAngle + Math.PI * 2 : endAngle;
414
+ }
415
+
416
+ let deltaAngle = endAngle - startAngle;
417
+ arcData.startAngle = startAngle;
418
+ arcData.endAngle = endAngle;
419
+ arcData.deltaAngle = deltaAngle;
420
+
421
+ return arcData;
422
+ }
423
+
424
+ function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
425
+
426
+ // Convert degrees to radians
427
+ angle = degrees ? (angle * PI$1) / 180 : angle;
428
+ ellipseRotation = degrees ? (ellipseRotation * PI$1) / 180 : ellipseRotation;
429
+ // reset rotation for circles or 360 degree
430
+ ellipseRotation = rx !== ry ? (ellipseRotation !== PI$1 * 2 ? ellipseRotation : 0) : 0;
431
+
432
+ // is ellipse
433
+ if (parametricAngle && rx !== ry) {
434
+ // adjust angle for ellipse rotation
435
+ angle = ellipseRotation ? angle - ellipseRotation : angle;
436
+ // Get the parametric angle for the ellipse
437
+ let angleParametric = atan$1(tan$1(angle) * (rx / ry));
438
+ // Ensure the parametric angle is in the correct quadrant
439
+ angle = cos$1(angle) < 0 ? angleParametric + PI$1 : angleParametric;
440
+ }
441
+
442
+ // Calculate the point on the ellipse without rotation
443
+ let x = cx + rx * cos$1(angle),
444
+ y = cy + ry * sin$1(angle);
445
+ let pt = {
446
+ x: x,
447
+ y: y
448
+ };
449
+
450
+ if (ellipseRotation) {
451
+ pt.x = cx + (x - cx) * cos$1(ellipseRotation) - (y - cy) * sin$1(ellipseRotation);
452
+ pt.y = cy + (x - cx) * sin$1(ellipseRotation) + (y - cy) * cos$1(ellipseRotation);
453
+ }
454
+ return pt
455
+ }
456
+
457
+ function bezierhasExtreme(p0, cpts = [], angleThreshold = 0.05) {
458
+ let isCubic = cpts.length === 3 ? true : false;
459
+ let cp1 = cpts[0] || null;
460
+ let cp2 = isCubic ? cpts[1] : null;
461
+ let p = isCubic ? cpts[2] : cpts[1];
462
+ let PIquarter = Math.PI * 0.5;
463
+
464
+ let extCp1 = false,
465
+ extCp2 = false;
466
+
467
+ let ang1 = cp1 ? getAngle(p, cp1, true) : null;
468
+
469
+ extCp1 = Math.abs((ang1 % PIquarter)) < angleThreshold || Math.abs((ang1 % PIquarter) - PIquarter) < angleThreshold;
470
+
471
+ if (isCubic) {
472
+ let ang2 = cp2 ? getAngle(cp2, p, true) : 0;
473
+ extCp2 = Math.abs((ang2 % PIquarter)) <= angleThreshold ||
474
+ Math.abs((ang2 % PIquarter) - PIquarter) <= angleThreshold;
475
+ }
476
+ return (extCp1 || extCp2)
477
+ }
478
+
479
+ function getBezierExtremeT(pts) {
480
+ let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
481
+ return tArr;
482
+ }
483
+
484
+ // cubic bezier.
485
+ function cubicBezierExtremeT(p0, cp1, cp2, p) {
486
+ let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
487
+
488
+ /**
489
+ * if control points are within
490
+ * bounding box of start and end point
491
+ * we cant't have extremes
492
+ */
493
+ let top = Math.min(p0.y, p.y);
494
+ let left = Math.min(p0.x, p.x);
495
+ let right = Math.max(p0.x, p.x);
496
+ let bottom = Math.max(p0.y, p.y);
497
+
498
+ if (
499
+ cp1.y >= top && cp1.y <= bottom &&
500
+ cp2.y >= top && cp2.y <= bottom &&
501
+ cp1.x >= left && cp1.x <= right &&
502
+ cp2.x >= left && cp2.x <= right
503
+ ) {
504
+ return []
505
+ }
506
+
507
+ let tArr = [],
508
+ a, b, c, t, t1, t2, b2ac, sqrt_b2ac;
509
+ for (let i = 0; i < 2; ++i) {
510
+ if (i == 0) {
511
+ b = 6 * x0 - 12 * x1 + 6 * x2;
512
+ a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
513
+ c = 3 * x1 - 3 * x0;
514
+ } else {
515
+ b = 6 * y0 - 12 * y1 + 6 * y2;
516
+ a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
517
+ c = 3 * y1 - 3 * y0;
518
+ }
519
+ if (Math.abs(a) < 1e-12) {
520
+ if (Math.abs(b) < 1e-12) {
521
+ continue;
522
+ }
523
+ t = -c / b;
524
+ if (0 < t && t < 1) {
525
+ tArr.push(t);
526
+ }
527
+ continue;
528
+ }
529
+ b2ac = b * b - 4 * c * a;
530
+ if (b2ac < 0) {
531
+ if (Math.abs(b2ac) < 1e-12) {
532
+ t = -b / (2 * a);
533
+ if (0 < t && t < 1) {
534
+ tArr.push(t);
535
+ }
536
+ }
537
+ continue;
538
+ }
539
+ sqrt_b2ac = Math.sqrt(b2ac);
540
+ t1 = (-b + sqrt_b2ac) / (2 * a);
541
+ if (0 < t1 && t1 < 1) {
542
+ tArr.push(t1);
543
+ }
544
+ t2 = (-b - sqrt_b2ac) / (2 * a);
545
+ if (0 < t2 && t2 < 1) {
546
+ tArr.push(t2);
547
+ }
548
+ }
549
+
550
+ let j = tArr.length;
551
+ while (j--) {
552
+ t = tArr[j];
553
+ }
554
+ return tArr;
555
+ }
556
+
557
+ function quadraticBezierExtremeT(p0, cp1, p) {
558
+ /**
559
+ * if control points are within
560
+ * bounding box of start and end point
561
+ * we cant't have extremes
562
+ */
563
+ let top = Math.min(p0.y, p.y);
564
+ let left = Math.min(p0.x, p.x);
565
+ let right = Math.max(p0.x, p.x);
566
+ let bottom = Math.max(p0.y, p.y);
567
+ let a, b, t;
568
+
569
+ if (
570
+ cp1.y >= top && cp1.y <= bottom &&
571
+ cp1.x >= left && cp1.x <= right
572
+ ) {
573
+ return []
574
+ }
575
+
576
+ let [x0, y0, x1, y1, x2, y2] = [p0.x, p0.y, cp1.x, cp1.y, p.x, p.y];
577
+ let extemeT = [];
578
+
579
+ for (let i = 0; i < 2; ++i) {
580
+ a = i == 0 ? x0 - 2 * x1 + x2 : y0 - 2 * y1 + y2;
581
+ b = i == 0 ? -2 * x0 + 2 * x1 : -2 * y0 + 2 * y1;
582
+ if (Math.abs(a) > 1e-12) {
583
+ t = -b / (2 * a);
584
+ if (t > 0 && t < 1) {
585
+ extemeT.push(t);
586
+ }
587
+ }
588
+ }
589
+ return extemeT
590
+ }
591
+
592
+ function commandIsFlat(points, tolerance = 0.025) {
593
+
594
+ let p0 = points[0];
595
+ let p = points[points.length - 1];
596
+
597
+ let xArr = points.map(pt => { return pt.x });
598
+ let yArr = points.map(pt => { return pt.y });
599
+
600
+ let xMin = Math.min(...xArr);
601
+ let xMax = Math.max(...xArr);
602
+ let yMin = Math.min(...yArr);
603
+ let yMax = Math.max(...yArr);
604
+ let w = xMax - xMin;
605
+ let h = yMax - yMin;
606
+
607
+ if (points.length < 3 || (w === 0 || h === 0)) {
608
+ return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
609
+ }
610
+
611
+ let squareDist = getSquareDistance(p0, p);
612
+ let squareDist1 = getSquareDistance(p0, points[0]);
613
+ let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
614
+ let squareDistAvg = (squareDist1 + squareDist2) / 2;
615
+
616
+ tolerance = 0.5;
617
+ let thresh = (w + h) * 0.5 * tolerance;
618
+
619
+ let area = 0;
620
+ for (let i = 0, l = points.length; i < l; i++) {
621
+ let addX = points[i].x;
622
+ let addY = points[i === points.length - 1 ? 0 : i + 1].y;
623
+ let subX = points[i === points.length - 1 ? 0 : i + 1].x;
624
+ let subY = points[i].y;
625
+ area += addX * addY * 0.5 - subX * subY * 0.5;
626
+ }
627
+
628
+ area = +Math.abs(area).toFixed(9);
629
+ let areaThresh = 1000;
630
+
631
+ let ratio = area / (squareDistAvg);
632
+
633
+ let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
634
+
635
+ return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
636
+ }
637
+
638
+ /**
639
+ * sloppy distance calculation
640
+ * based on x/y differences
641
+ */
642
+ function getDistAv(pt1, pt2) {
643
+ let diffX = Math.abs(pt1.x - pt2.x);
644
+ let diffY = Math.abs(pt1.y - pt2.y);
645
+ let diff = (diffX + diffY) / 2;
646
+ return diff;
647
+ }
648
+
649
+ /**
650
+ * split compound paths into
651
+ * sub path data array
652
+ */
653
+ function splitSubpaths(pathData) {
654
+
655
+ let subPathArr = [];
656
+
657
+
658
+ try{
659
+ let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
660
+
661
+ }catch{
662
+ console.log('catch', pathData);
663
+ }
664
+
665
+ let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
666
+
667
+ // no compound path
668
+ if (subPathIndices.length === 1) {
669
+ return [pathData]
670
+ }
671
+ subPathIndices.forEach((index, i) => {
672
+ subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
673
+ });
674
+
675
+ return subPathArr;
676
+ }
677
+
678
+ /**
679
+ * calculate split command points
680
+ * for single t value
681
+ */
682
+ function splitCommand(points, t) {
683
+
684
+ let seg1 = [];
685
+ let seg2 = [];
686
+
687
+ let p0 = points[0];
688
+ let cp1 = points[1];
689
+ let cp2 = points[points.length - 2];
690
+ let p = points[points.length - 1];
691
+ let m0,m1,m2,m3,m4, p2;
692
+
693
+ // cubic
694
+ if (points.length === 4) {
695
+ m0 = pointAtT([p0, cp1], t);
696
+ m1 = pointAtT([cp1, cp2], t);
697
+ m2 = pointAtT([cp2, p], t);
698
+ m3 = pointAtT([m0, m1], t);
699
+ m4 = pointAtT([m1, m2], t);
700
+
701
+ // split end point
702
+ p2 = pointAtT([m3, m4], t);
703
+
704
+ // 1. segment
705
+ seg1.push(
706
+ { x: p0.x, y: p0.y },
707
+ { x: m0.x, y: m0.y },
708
+ { x: m3.x, y: m3.y },
709
+ { x: p2.x, y: p2.y },
710
+ );
711
+ // 2. segment
712
+ seg2.push(
713
+ { x: p2.x, y: p2.y },
714
+ { x: m4.x, y: m4.y },
715
+ { x: m2.x, y: m2.y },
716
+ { x: p.x, y: p.y },
717
+ );
718
+ }
719
+
720
+ // quadratic
721
+ else if (points.length === 3) {
722
+ m1 = pointAtT([p0, cp1], t);
723
+ m2 = pointAtT([cp1, p], t);
724
+ p2 = pointAtT([m1, m2], t);
725
+
726
+ // 1. segment
727
+ seg1.push(
728
+ { x: p0.x, y: p0.y },
729
+ { x: m1.x, y: m1.y },
730
+ { x: p2.x, y: p2.y },
731
+ );
732
+
733
+ // 1. segment
734
+ seg2.push(
735
+ { x: p2.x, y: p2.y },
736
+ { x: m2.x, y: m2.y },
737
+ { x: p.x, y: p.y },
738
+ );
739
+ }
740
+
741
+ // lineto
742
+ else if (points.length === 2) {
743
+ m1 = pointAtT([p0, p], t);
744
+
745
+ // 1. segment
746
+ seg1.push(
747
+ { x: p0.x, y: p0.y },
748
+ { x: m1.x, y: m1.y },
749
+ );
750
+
751
+ // 1. segment
752
+ seg2.push(
753
+ { x: m1.x, y: m1.y },
754
+ { x: p.x, y: p.y },
755
+ );
756
+ }
757
+ return [seg1, seg2];
758
+ }
759
+
760
+ /**
761
+ * calculate command extremes
762
+ */
763
+
764
+ function addExtemesToCommand(p0, values, tMin=0, tMax=1) {
765
+
766
+ let pathDataNew = [];
767
+
768
+ let type = values.length === 6 ? 'C' : 'Q';
769
+ let cp1 = { x: values[0], y: values[1] };
770
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
771
+ let p = { x: values[4], y: values[5] };
772
+
773
+ // get inner bbox
774
+ let xMax = Math.max(p.x, p0.x);
775
+ let xMin = Math.min(p.x, p0.x);
776
+ let yMax = Math.max(p.y, p0.y);
777
+ let yMin = Math.min(p.y, p0.y);
778
+
779
+ let extremeCount = 0;
780
+
781
+ if (
782
+ cp1.x < xMin ||
783
+ cp1.x > xMax ||
784
+ cp1.y < yMin ||
785
+ cp1.y > yMax ||
786
+ cp2.x < xMin ||
787
+ cp2.x > xMax ||
788
+ cp2.y < yMin ||
789
+ cp2.y > yMax
790
+
791
+ ) {
792
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
793
+ let tArr = getBezierExtremeT(pts).sort();
794
+
795
+ // avoid t split too close to start or end
796
+ tArr = tArr.filter(t=>t>tMin && t<tMax);
797
+
798
+ if(tArr.length){
799
+ let commandsSplit = splitCommandAtTValues(p0, values, tArr);
800
+ pathDataNew.push(...commandsSplit);
801
+ extremeCount += commandsSplit.length;
802
+ }else {
803
+
804
+ pathDataNew.push({ type: type, values: values });
805
+ }
806
+
807
+ }
808
+ // no extremes
809
+ else {
810
+ pathDataNew.push({ type: type, values: values });
811
+ }
812
+
813
+ return { pathData: pathDataNew, count: extremeCount };
814
+
815
+ }
816
+
817
+ function addExtremePoints(pathData, tMin=0, tMax=1) {
818
+ let pathDataNew = [pathData[0]];
819
+ // previous on path point
820
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
821
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
822
+ let len = pathData.length;
823
+
824
+ for (let c = 1; len && c < len; c++) {
825
+ let com = pathData[c];
826
+
827
+ let { type, values } = com;
828
+ let valsL = values.slice(-2);
829
+ ({ x: valsL[0], y: valsL[1] });
830
+
831
+ if (type !== 'C' && type !== 'Q') {
832
+ pathDataNew.push(com);
833
+ }
834
+
835
+ else {
836
+ // add extremes
837
+ if (type === 'C' || type === 'Q') {
838
+ let comExt = addExtemesToCommand(p0, values, tMin, tMax).pathData;
839
+
840
+ pathDataNew.push(...comExt );
841
+ }
842
+ }
843
+
844
+ p0 = { x: valsL[0], y: valsL[1] };
845
+
846
+ if (type.toLowerCase() === "z") {
847
+ p0 = M;
848
+ } else if (type === "M") {
849
+ M = { x: valsL[0], y: valsL[1] };
850
+ }
851
+ }
852
+
853
+ return pathDataNew;
854
+ }
855
+
856
+ /**
857
+ * split commands multiple times
858
+ * based on command points
859
+ * and t array
860
+ */
861
+ function splitCommandAtTValues(p0, values, tArr, returnCommand = true) {
862
+ let segmentPoints = [];
863
+
864
+ if (!tArr.length) {
865
+ return false
866
+ }
867
+
868
+ let valuesL = values.length;
869
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
870
+ let cp1, cp2, points;
871
+
872
+ if (values.length === 2) {
873
+ points = [p0, p];
874
+ }
875
+ else if (values.length === 4) {
876
+ cp1 = { x: values[0], y: values[1] };
877
+ points = [p0, cp1, p];
878
+ }
879
+ else if (values.length === 6) {
880
+ cp1 = { x: values[0], y: values[1] };
881
+ cp2 = { x: values[2], y: values[3] };
882
+ points = [p0, cp1, cp2, p];
883
+ }
884
+
885
+ if (tArr.length) {
886
+ // single t
887
+ if (tArr.length === 1) {
888
+ let segs = splitCommand(points, tArr[0]);
889
+ let points1 = segs[0];
890
+ let points2 = segs[1];
891
+ segmentPoints.push(points1, points2);
892
+
893
+ } else {
894
+
895
+ // 1st segment
896
+ let t1 = tArr[0];
897
+ let seg0 = splitCommand(points, t1);
898
+ let points0 = seg0[0];
899
+ segmentPoints.push(points0);
900
+ points = seg0[1];
901
+
902
+ for (let i = 1; i < tArr.length; i++) {
903
+ t1 = tArr[i - 1];
904
+ let t2 = tArr[i];
905
+
906
+ // new t value for 2nd segment
907
+ let t2_1 = (t2 - t1) / (1 - t1);
908
+ let segs2 = splitCommand(points, t2_1);
909
+ segmentPoints.push(segs2[0]);
910
+
911
+ if (i === tArr.length - 1) {
912
+ segmentPoints.push(segs2[segs2.length - 1]);
913
+ }
914
+ // take 2nd segment for next splitting
915
+ points = segs2[1];
916
+ }
917
+ }
918
+ }
919
+
920
+ if (returnCommand) {
921
+
922
+ let pathData = [];
923
+ let com, values;
924
+
925
+ segmentPoints.forEach(seg => {
926
+ com = { type: '', values: [] };
927
+ seg.shift();
928
+ values = seg.map(val => { return Object.values(val) }).flat();
929
+ com.values = values;
930
+
931
+ // cubic
932
+ if (seg.length === 3) {
933
+ com.type = 'C';
934
+ }
935
+
936
+ // quadratic
937
+ else if (seg.length === 2) {
938
+ com.type = 'Q';
939
+ }
940
+
941
+ // lineto
942
+ else if (seg.length === 1) {
943
+ com.type = 'L';
944
+ }
945
+ pathData.push(com);
946
+ });
947
+ return pathData;
948
+ }
949
+
950
+ return segmentPoints;
951
+ }
952
+
953
+ /**
954
+ * calculate polygon bbox
955
+ */
956
+ function getPolyBBox(vertices, decimals = -1) {
957
+ let xArr = vertices.map(pt => pt.x);
958
+ let yArr = vertices.map(pt => pt.y);
959
+ let left = Math.min(...xArr);
960
+ let right = Math.max(...xArr);
961
+ let top = Math.min(...yArr);
962
+ let bottom = Math.max(...yArr);
963
+ let bb = {
964
+ x: left,
965
+ left: left,
966
+ right: right,
967
+ y: top,
968
+ top: top,
969
+ bottom: bottom,
970
+ width: right - left,
971
+ height: bottom - top
972
+ };
973
+
974
+ // round
975
+
976
+ if (decimals > -1) {
977
+ for (let prop in bb) {
978
+ bb[prop] = +bb[prop].toFixed(decimals);
979
+ }
980
+ }
981
+
982
+ return bb;
983
+ }
984
+
985
+ function getSubPathBBoxes(subPaths) {
986
+ let bboxArr = [];
987
+ subPaths.forEach((pathData) => {
988
+
989
+ let bb = getPathDataBBox_sloppy(pathData);
990
+ bboxArr.push(bb);
991
+ });
992
+
993
+ return bboxArr;
994
+ }
995
+
996
+ function checkBBoxIntersections(bb, bb1) {
997
+ let [x, y, width, height, right, bottom] = [
998
+ bb.x,
999
+ bb.y,
1000
+ bb.width,
1001
+ bb.height,
1002
+ bb.x + bb.width,
1003
+ bb.y + bb.height
1004
+ ];
1005
+ let [x1, y1, width1, height1, right1, bottom1] = [
1006
+ bb1.x,
1007
+ bb1.y,
1008
+ bb1.width,
1009
+ bb1.height,
1010
+ bb1.x + bb1.width,
1011
+ bb1.y + bb1.height
1012
+ ];
1013
+ let intersects = false;
1014
+ if (width * height != width1 * height1) {
1015
+ if (width * height > width1 * height1) {
1016
+ if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
1017
+ intersects = true;
1018
+ }
1019
+ }
1020
+ }
1021
+ return intersects;
1022
+ }
1023
+
1024
+ /**
1025
+ * sloppy path bbox aaproximation
1026
+ */
1027
+
1028
+ function getPathDataBBox_sloppy(pathData) {
1029
+ let pts = getPathDataPoly(pathData);
1030
+ let bb = getPolyBBox(pts);
1031
+ return bb;
1032
+ }
1033
+
1034
+ /**
1035
+ * get path data poly
1036
+ * including command points
1037
+ * handy for faster/sloppy bbox approximations
1038
+ */
1039
+
1040
+ function getPathDataPoly(pathData) {
1041
+
1042
+ let poly = [];
1043
+ for (let i = 0; i < pathData.length; i++) {
1044
+ let com = pathData[i];
1045
+ let prev = i > 0 ? pathData[i - 1] : pathData[i];
1046
+ let { type, values } = com;
1047
+ let p0 = { x: prev.values[prev.values.length - 2], y: prev.values[prev.values.length - 1] };
1048
+ let p = values.length ? { x: values[values.length - 2], y: values[values.length - 1] } : '';
1049
+ let cp1 = values.length ? { x: values[0], y: values[1] } : '';
1050
+
1051
+ switch (type) {
1052
+
1053
+ // convert to cubic to get polygon
1054
+ case 'A':
1055
+
1056
+ if (typeof arcToBezier !== 'function') {
1057
+
1058
+ // get real radii
1059
+ let rx = getDistance(p0, p) / 2;
1060
+ let ptMid = interpolate(p0, p, 0.5);
1061
+
1062
+ let pt1 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, 0);
1063
+ let pt2 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, Math.PI);
1064
+ poly.push(pt1, pt2, p);
1065
+
1066
+ break;
1067
+ }
1068
+ let cubic = arcToBezier(p0, values);
1069
+ cubic.forEach(com => {
1070
+ let vals = com.values;
1071
+ let cp1 = { x: vals[0], y: vals[1] };
1072
+ let cp2 = { x: vals[2], y: vals[3] };
1073
+ let p = { x: vals[4], y: vals[5] };
1074
+ poly.push(cp1, cp2, p);
1075
+ });
1076
+ break;
1077
+
1078
+ case 'C':
1079
+ let cp2 = { x: values[2], y: values[3] };
1080
+ poly.push(cp1, cp2);
1081
+ break;
1082
+ case 'Q':
1083
+ poly.push(cp1);
1084
+ break;
1085
+ }
1086
+
1087
+ // M and L commands
1088
+ if (type.toLowerCase() !== 'z') {
1089
+ poly.push(p);
1090
+ }
1091
+ }
1092
+
1093
+ return poly;
1094
+ }
1095
+
1096
+ /**
1097
+ * get pathdata area
1098
+ */
1099
+
1100
+ function getPathArea(pathData, decimals = 9) {
1101
+ let totalArea = 0;
1102
+ let polyPoints = [];
1103
+
1104
+ let subPathsData = splitSubpaths(pathData);
1105
+ let isCompoundPath = subPathsData.length > 1 ? true : false;
1106
+ let counterShapes = [];
1107
+
1108
+ // check intersections for compund paths
1109
+ if (isCompoundPath) {
1110
+ let bboxArr = getSubPathBBoxes(subPathsData);
1111
+
1112
+ bboxArr.forEach(function (bb, b) {
1113
+
1114
+ for (let i = 0; i < bboxArr.length; i++) {
1115
+ let bb2 = bboxArr[i];
1116
+ if (bb != bb2) {
1117
+ let intersects = checkBBoxIntersections(bb, bb2);
1118
+ if (intersects) {
1119
+ counterShapes.push(i);
1120
+ }
1121
+ }
1122
+ }
1123
+ });
1124
+ }
1125
+
1126
+ subPathsData.forEach((pathData, d) => {
1127
+
1128
+ polyPoints = [];
1129
+ let comArea = 0;
1130
+ let pathArea = 0;
1131
+ let multiplier = 1;
1132
+ let pts = [];
1133
+
1134
+ pathData.forEach(function (com, i) {
1135
+ let [type, values] = [com.type, com.values];
1136
+ let valuesL = values.length;
1137
+
1138
+ if (values.length) {
1139
+ let prevC = i > 0 ? pathData[i - 1] : pathData[0];
1140
+ let prevCVals = prevC.values;
1141
+ let prevCValsL = prevCVals.length;
1142
+ let p0 = { x: prevCVals[prevCValsL - 2], y: prevCVals[prevCValsL - 1] };
1143
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1144
+
1145
+ // C commands
1146
+ if (type === 'C' || type === 'Q') {
1147
+ let cp1 = { x: values[0], y: values[1] };
1148
+ pts = type === 'C' ? [p0, cp1, { x: values[2], y: values[3] }, p] : [p0, cp1, p];
1149
+ let areaBez = Math.abs(getBezierArea(pts));
1150
+ comArea += areaBez;
1151
+
1152
+ polyPoints.push(p0, p);
1153
+ }
1154
+
1155
+ // A commands
1156
+ else if (type === 'A') {
1157
+ let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], com.values[3], com.values[4], p.x, p.y);
1158
+ let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle } = arcData;
1159
+
1160
+ let arcArea = Math.abs(getEllipseArea(rx, ry, startAngle, endAngle));
1161
+
1162
+ // subtract remaining polygon between p0, center and p
1163
+ let polyArea = Math.abs(getPolygonArea([p0, { x: cx, y: cy }, p]));
1164
+ arcArea -= polyArea;
1165
+
1166
+ polyPoints.push(p0, p);
1167
+ comArea += arcArea;
1168
+ }
1169
+
1170
+ // L commands
1171
+ else {
1172
+ polyPoints.push(p0, p);
1173
+ }
1174
+ }
1175
+ });
1176
+
1177
+ let areaPoly = getPolygonArea(polyPoints);
1178
+
1179
+ if (counterShapes.indexOf(d) !== -1) {
1180
+ multiplier = -1;
1181
+ }
1182
+
1183
+ if (
1184
+ (areaPoly < 0 && comArea < 0)
1185
+ ) {
1186
+ // are negative
1187
+ pathArea = (Math.abs(comArea) - Math.abs(areaPoly)) * multiplier;
1188
+
1189
+ } else {
1190
+ pathArea = (Math.abs(comArea) + Math.abs(areaPoly)) * multiplier;
1191
+ }
1192
+
1193
+ totalArea += pathArea;
1194
+ });
1195
+
1196
+ return totalArea;
1197
+ }
1198
+
1199
+ /**
1200
+ * get ellipse area
1201
+ * skips to circle calculation if rx===ry
1202
+ */
1203
+
1204
+ function getEllipseArea(rx, ry, startAngle, endAngle) {
1205
+ const totalArea = Math.PI * rx * ry;
1206
+ let angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
1207
+ // If circle, use simple circular formula
1208
+ if (rx === ry) return totalArea * (angleDiff / (2 * Math.PI));
1209
+
1210
+ // Convert absolute angles to parametric angles
1211
+ const absoluteToParametric = (phi)=>{
1212
+ return Math.atan2(rx * Math.sin(phi), ry * Math.cos(phi));
1213
+ };
1214
+ startAngle = absoluteToParametric(startAngle);
1215
+ endAngle = absoluteToParametric(endAngle);
1216
+ angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
1217
+ return totalArea * (angleDiff / (2 * Math.PI));
1218
+ }
1219
+
1220
+ /**
1221
+ * compare areas
1222
+ * for thresholds
1223
+ * returns a percentage value
1224
+ */
1225
+
1226
+ function getRelativeAreaDiff(area0, area1) {
1227
+ let diff = Math.abs(area0 - area1);
1228
+ return Math.abs(100 - (100 / area0 * (area0 + diff)))
1229
+ }
1230
+
1231
+ /**
1232
+ * get bezier area
1233
+ */
1234
+ function getBezierArea(pts, absolute=false) {
1235
+
1236
+ let [p0, cp1, cp2, p] = [pts[0], pts[1], pts[2], pts[pts.length - 1]];
1237
+ let area;
1238
+
1239
+ if (pts.length < 3) return 0;
1240
+
1241
+ // quadratic beziers
1242
+ if (pts.length === 3) {
1243
+ cp1 = {
1244
+ x: pts[0].x * 1 / 3 + pts[1].x * 2 / 3,
1245
+ y: pts[0].y * 1 / 3 + pts[1].y * 2 / 3
1246
+ };
1247
+
1248
+ cp2 = {
1249
+ x: pts[2].x * 1 / 3 + pts[1].x * 2 / 3,
1250
+ y: pts[2].y * 1 / 3 + pts[1].y * 2 / 3
1251
+ };
1252
+ }
1253
+
1254
+ area = ((p0.x * (-2 * cp1.y - cp2.y + 3 * p.y) +
1255
+ cp1.x * (2 * p0.y - cp2.y - p.y) +
1256
+ cp2.x * (p0.y + cp1.y - 2 * p.y) +
1257
+ p.x * (-3 * p0.y + cp1.y + 2 * cp2.y)) *
1258
+ 3) / 20;
1259
+
1260
+ return absolute ? Math.abs(area) : area;
1261
+ }
1262
+
1263
+ function getPolygonArea(points, absolute=false) {
1264
+ let area = 0;
1265
+ for (let i = 0, len = points.length; len && i < len; i++) {
1266
+ let addX = points[i].x;
1267
+ let addY = points[i === points.length - 1 ? 0 : i + 1].y;
1268
+ let subX = points[i === points.length - 1 ? 0 : i + 1].x;
1269
+ let subY = points[i].y;
1270
+ area += addX * addY * 0.5 - subX * subY * 0.5;
1271
+ }
1272
+ if(absolute) area=Math.abs(area);
1273
+ return area;
1274
+ }
1275
+
1276
+ /**
1277
+ * serialize pathData array to
1278
+ * d attribute string
1279
+ */
1280
+
1281
+ function pathDataToD(pathData, optimize = 0) {
1282
+
1283
+ optimize = parseFloat(optimize);
1284
+
1285
+ let beautify = optimize > 1;
1286
+ let minify = beautify || optimize ? false : true;
1287
+
1288
+ // Convert first "M" to "m" if followed by "l" (when minified)
1289
+ if (pathData[1].type === "l" && minify) {
1290
+ pathData[0].type = "m";
1291
+ }
1292
+
1293
+ let d = '';
1294
+ let suff = beautify ? `\n` : ' ';
1295
+
1296
+ if (minify) {
1297
+ d = `${pathData[0].type} ${pathData[0].values.join(" ")}`;
1298
+ } else {
1299
+ d = `${pathData[0].type} ${pathData[0].values.join(" ")}${suff}`;
1300
+ }
1301
+
1302
+ for (let i = 1, len = pathData.length; i < len; i++) {
1303
+ let com0 = pathData[i - 1];
1304
+ let com = pathData[i];
1305
+ let { type, values } = com;
1306
+
1307
+ // Minify Arc commands (A/a) – actually sucks!
1308
+ if (minify && (type === 'A' || type === 'a')) {
1309
+ values = [
1310
+ values[0], values[1], values[2],
1311
+ `${values[3]}${values[4]}${values[5]}`,
1312
+ values[6]
1313
+ ];
1314
+ }
1315
+
1316
+ // Omit type for repeated commands
1317
+ type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1318
+ ? " "
1319
+ : (
1320
+ (com0.type === "m" && com.type === "l") ||
1321
+ (com0.type === "M" && com.type === "l") ||
1322
+ (com0.type === "M" && com.type === "L")
1323
+ ) && minify
1324
+ ? " "
1325
+ : com.type;
1326
+
1327
+ // concatenate subsequent floating point values
1328
+ if (minify) {
1329
+
1330
+ let valsString = '';
1331
+ let prevWasFloat = false;
1332
+
1333
+ for (let v = 0, l = values.length; v < l; v++) {
1334
+ let val = values[v];
1335
+ let valStr = val.toString();
1336
+ let isFloat = valStr.includes('.');
1337
+ let isSmallFloat = isFloat && Math.abs(val) < 1;
1338
+
1339
+ // Remove leading zero from small floats *only* if the previous was also a float
1340
+ if (isSmallFloat && prevWasFloat) {
1341
+ valStr = valStr.replace(/^0\./, '.');
1342
+ }
1343
+
1344
+ // Add space unless this is the first value OR previous was a small float
1345
+ if (v > 0 && !(prevWasFloat && isSmallFloat)) {
1346
+ valsString += ' ';
1347
+ }
1348
+
1349
+ valsString += valStr;
1350
+
1351
+ prevWasFloat = isSmallFloat;
1352
+ }
1353
+
1354
+ d += `${type}${valsString}`;
1355
+
1356
+ }
1357
+ // regular non-minified output
1358
+ else {
1359
+ d += `${type} ${values.join(' ')}${suff}`;
1360
+ }
1361
+ }
1362
+
1363
+ if (minify) {
1364
+ d = d
1365
+ .replace(/ 0\./g, " .") // Space before small decimals
1366
+ .replace(/ -/g, "-") // Remove space before negatives
1367
+ .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
1368
+ .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
1369
+ }
1370
+
1371
+ return d;
1372
+ }
1373
+
1374
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1375
+
1376
+ // cubic Bézier derivative
1377
+ const cubicDerivative = (p0, p1, p2, p3, t) => {
1378
+ let mt = 1 - t;
1379
+
1380
+ return {
1381
+ x:
1382
+ 3 * mt * mt * (p1.x - p0.x) +
1383
+ 6 * mt * t * (p2.x - p1.x) +
1384
+ 3 * t * t * (p3.x - p2.x),
1385
+ y:
1386
+ 3 * mt * mt * (p1.y - p0.y) +
1387
+ 6 * mt * t * (p2.y - p1.y) +
1388
+ 3 * t * t * (p3.y - p2.y)
1389
+ };
1390
+ };
1391
+
1392
+ // if combining fails return original commands
1393
+ let commands = [com1, com2];
1394
+
1395
+ // detect dominant
1396
+ let dist1 = getSquareDistance(com1.p0, com1.p);
1397
+ let dist2 = getSquareDistance(com2.p0, com2.p);
1398
+ let reverse = dist1 > dist2;
1399
+
1400
+ // backup original commands
1401
+ let com1_o = JSON.parse(JSON.stringify(com1));
1402
+ let com2_o = JSON.parse(JSON.stringify(com2));
1403
+
1404
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false);
1405
+
1406
+ if (!ptI) {
1407
+
1408
+ return commands
1409
+ }
1410
+
1411
+ if (reverse) {
1412
+ let com2_R = {
1413
+ p0: { x: com1.p.x, y: com1.p.y },
1414
+ cp1: { x: com1.cp2.x, y: com1.cp2.y },
1415
+ cp2: { x: com1.cp1.x, y: com1.cp1.y },
1416
+ p: { x: com1.p0.x, y: com1.p0.y },
1417
+ };
1418
+
1419
+ let com1_R = {
1420
+ p0: { x: com2.p.x, y: com2.p.y },
1421
+ cp1: { x: com2.cp2.x, y: com2.cp2.y },
1422
+ cp2: { x: com2.cp1.x, y: com2.cp1.y },
1423
+ p: { x: com2.p0.x, y: com2.p0.y },
1424
+ };
1425
+
1426
+ com1 = com1_R;
1427
+ com2 = com2_R;
1428
+ }
1429
+
1430
+ let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
1431
+ let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
1432
+ let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
1433
+ let dot = (a, b) => a.x * b.x + a.y * b.y;
1434
+
1435
+ // estimate extrapolation parameter t0
1436
+
1437
+ let B0 = com2.p0;
1438
+ let D0 = cubicDerivative(
1439
+ com2.p0,
1440
+ com2.cp1,
1441
+ com2.cp2,
1442
+ com2.p,
1443
+ 0
1444
+ );
1445
+
1446
+ let v = sub(com1.p0, B0);
1447
+
1448
+ // first-order projection onto tangent
1449
+ let t0 = dot(v, D0) / dot(D0, D0);
1450
+
1451
+ // refine with one Newton iteration (optional but cheap)
1452
+ let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1453
+ let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1454
+ let r = sub(P, com1.p0);
1455
+
1456
+ t0 -= dot(r, dP) / dot(dP, dP);
1457
+
1458
+ // construct merged cubic over [t0, 1]
1459
+
1460
+ let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1461
+ let Q3 = com2.p;
1462
+
1463
+ let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1464
+ let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
1465
+
1466
+ let scale = 1 - t0;
1467
+
1468
+ let Q1 = add(Q0, mul(d0, scale / 3));
1469
+ let Q2 = sub(Q3, mul(d1, scale / 3));
1470
+
1471
+ let result = {
1472
+ p0: Q0,
1473
+ cp1: Q1,
1474
+ cp2: Q2,
1475
+ p: Q3,
1476
+ };
1477
+
1478
+ if (reverse) {
1479
+ result = {
1480
+ p0: Q3,
1481
+ cp1: Q2,
1482
+ cp2: Q1,
1483
+ p: Q0,
1484
+ };
1485
+ }
1486
+
1487
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true);
1488
+ let seg1_cp2 = ptM.cpts[2];
1489
+
1490
+ let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
1491
+ let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
1492
+
1493
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333);
1494
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1495
+
1496
+ // test self intersections and exit
1497
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1498
+ if(cp_intersection){
1499
+
1500
+ return commands;
1501
+ }
1502
+
1503
+ result.cp1 = cp1_2;
1504
+ result.cp2 = cp2_2;
1505
+
1506
+ // check distances
1507
+
1508
+ let dist3 = getDistAv(com1_o.p0, result.p0);
1509
+ let dist4 = getDistAv(com2_o.p, result.p);
1510
+ let dist5 = (dist3 + dist4);
1511
+
1512
+ // use original points
1513
+ result.p0 = com1_o.p0;
1514
+ result.p = com2_o.p;
1515
+ result.extreme = com2_o.extreme;
1516
+ result.corner = com2_o.corner;
1517
+ result.dimA = com2_o.dimA;
1518
+ result.directionChange = com2_o.directionChange;
1519
+ result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1520
+
1521
+ // check if completely off
1522
+ if (dist5 < maxDist) {
1523
+
1524
+ // compare combined with original area
1525
+ let pathData0 = [
1526
+ { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
1527
+ { type: 'C', values: [com1_o.cp1.x, com1_o.cp1.y, com1_o.cp2.x, com1_o.cp2.y, com1_o.p.x, com1_o.p.y] },
1528
+ { type: 'C', values: [com2_o.cp1.x, com2_o.cp1.y, com2_o.cp2.x, com2_o.cp2.y, com2_o.p.x, com2_o.p.y] },
1529
+ ];
1530
+
1531
+ let area0 = getPathArea(pathData0);
1532
+ let pathDataN = [
1533
+ { type: 'M', values: [result.p0.x, result.p0.y] },
1534
+ { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
1535
+ ];
1536
+
1537
+ let areaN = getPathArea(pathDataN);
1538
+ let areaDiff = Math.abs(areaN / area0 - 1);
1539
+
1540
+ result.error = areaDiff * 10 * tolerance;
1541
+
1542
+ pathDataToD(pathDataN);
1543
+
1544
+ // success
1545
+ if (areaDiff < 0.01) {
1546
+ commands = [result];
1547
+
1548
+ }
1549
+
1550
+ }
1551
+
1552
+ return commands
1553
+
1554
+ }
1555
+
1556
+ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
1557
+
1558
+ let commands = [com1, com2];
1559
+ let t = findSplitT(com1, com2);
1560
+
1561
+ let distAv1 = getDistAv(com1.p0, com1.p);
1562
+ let distAv2 = getDistAv(com2.p0, com2.p);
1563
+ let distMin = Math.min(distAv1, distAv2);
1564
+
1565
+ let distScale = 0.05;
1566
+ let maxDist = distMin * distScale * tolerance;
1567
+
1568
+ let comS = getExtrapolatedCommand(com1, com2, t, t);
1569
+
1570
+ // test on path point against original
1571
+ let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1572
+
1573
+ let dist0 = getDistAv(com1.p, pt);
1574
+ let dist1 = 0, dist2 = 0;
1575
+ let close = dist0 < maxDist;
1576
+ let success = false;
1577
+
1578
+ // collect error data
1579
+ let error = dist0;
1580
+
1581
+ /*
1582
+ if (com2.directionChange) {
1583
+
1584
+ }
1585
+ */
1586
+
1587
+ if (close) {
1588
+
1589
+ /**
1590
+ * check additional points
1591
+ * to prevent distortions
1592
+ */
1593
+
1594
+ // 2nd segment mid
1595
+ let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1596
+
1597
+ // simplified path
1598
+ let t3 = (1 + t) * 0.5;
1599
+ let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1600
+ dist1 = getDistAv(pt_2, ptS_2);
1601
+
1602
+ error += dist1;
1603
+
1604
+ // quit - paths not congruent
1605
+
1606
+ if (dist1 < maxDist) {
1607
+
1608
+ // 1st segment mid
1609
+ let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1610
+
1611
+ let t2 = t * 0.5;
1612
+ let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1613
+ dist2 = getDistAv(pt_1, ptS_1);
1614
+
1615
+ /*
1616
+ if(dist1>tolerance){
1617
+ renderPoint(markers, pt_1, 'blue')
1618
+ renderPoint(markers, ptS_1, 'orange', '0.5%')
1619
+ }
1620
+ */
1621
+
1622
+ // quit - paths not congruent
1623
+ if (dist1 + dist2 < maxDist) success = true;
1624
+
1625
+ // collect error data
1626
+ error += dist2;
1627
+
1628
+ }
1629
+
1630
+ } // end 1st try
1631
+
1632
+
1633
+ /*
1634
+ if (extrapolateDominant && com2.extreme) {
1635
+ renderPoint(markers, com2.p)
1636
+
1637
+ }
1638
+ */
1639
+
1640
+
1641
+
1642
+ // try extrapolated dominant curve
1643
+
1644
+ // && !com1.extreme
1645
+ if (extrapolateDominant && !success ) {
1646
+
1647
+ let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
1648
+
1649
+ if(combinedEx.length===1){
1650
+ success = true;
1651
+ comS = combinedEx[0];
1652
+ error = comS.error;
1653
+
1654
+ }
1655
+
1656
+
1657
+ }
1658
+
1659
+ // add meta
1660
+ if (success) {
1661
+
1662
+
1663
+ // correct to exact start and end points
1664
+ comS.p0 = com1.p0;
1665
+ comS.p = com2.p;
1666
+
1667
+ comS.dimA = getDistAv(comS.p0, comS.p);
1668
+ comS.type = 'C';
1669
+ comS.extreme = com2.extreme;
1670
+ comS.directionChange = com2.directionChange;
1671
+ comS.corner = com2.corner;
1672
+
1673
+ comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
1674
+
1675
+ // relative error
1676
+ comS.error = error / maxDist;
1677
+
1678
+ commands = [comS];
1679
+
1680
+ }
1681
+
1682
+ return commands;
1683
+ }
1684
+
1685
+ function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1686
+
1687
+ let { p0, cp1 } = com1;
1688
+ let { p, cp2 } = com2;
1689
+
1690
+ // extrapolate control points
1691
+ let cp1_S = {
1692
+ x: (cp1.x - (1 - t1) * p0.x) / t1,
1693
+ y: (cp1.y - (1 - t1) * p0.y) / t1
1694
+ };
1695
+
1696
+ let cp2_S = {
1697
+ x: (cp2.x - t2 * p.x) / (1 - t2),
1698
+ y: (cp2.y - t2 * p.y) / (1 - t2)
1699
+ };
1700
+
1701
+ let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
1702
+
1703
+ return comS
1704
+
1705
+ }
1706
+
1707
+ function findSplitT(com1, com2) {
1708
+
1709
+ // control tangent intersection
1710
+ let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1711
+
1712
+ // intersection 2nd cp1 tangent and global tangent intersection
1713
+ let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
1714
+
1715
+ let len1 = getDistance(pt1, com2.p);
1716
+ let len2 = getDistance(ptI, com2.p);
1717
+
1718
+ let t = 1 - len2 / len1;
1719
+
1720
+ // check self intersections
1721
+
1722
+ let len3 = getDistance(com1.cp2, com1.p);
1723
+ let len4 = getDistance(com1.cp2, com2.cp1);
1724
+
1725
+ t = Math.min(len3) / len4;
1726
+
1727
+ return t
1728
+
1729
+ }
1730
+
1731
+ function analyzePathData(pathData = []) {
1732
+
1733
+ let pathDataPlus = [];
1734
+
1735
+ let pathPoly = getPathDataVertices(pathData);
1736
+ let bb = getPolyBBox(pathPoly);
1737
+ let { left, right, top, bottom, width, height } = bb;
1738
+
1739
+ // initial starting point coordinates
1740
+ let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
1741
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
1742
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
1743
+ let p;
1744
+
1745
+ // init starting point data
1746
+ pathData[0].idx = 0;
1747
+ pathData[0].p0 = M;
1748
+ pathData[0].p = M;
1749
+ pathData[0].lineto = false;
1750
+ pathData[0].corner = false;
1751
+ pathData[0].extreme = false;
1752
+ pathData[0].directionChange = false;
1753
+ pathData[0].closePath = false;
1754
+ pathData[0].dimA = 0;
1755
+
1756
+ // add first M command
1757
+ let pathDataProps = [pathData[0]];
1758
+ let area0 = 0;
1759
+ let len = pathData.length;
1760
+
1761
+ for (let c = 2; len && c <= len; c++) {
1762
+
1763
+ let com = pathData[c - 1];
1764
+ let { type, values } = com;
1765
+ let valsL = values.slice(-2);
1766
+
1767
+ /**
1768
+ * get command points for
1769
+ * flatness checks:
1770
+ * this way we can skip certain tests
1771
+ */
1772
+ let commandPts = [p0];
1773
+ let isFlat = false;
1774
+
1775
+ // init properties
1776
+ com.idx = c - 1;
1777
+ com.lineto = false;
1778
+ com.corner = false;
1779
+ com.extreme = false;
1780
+ com.directionChange = false;
1781
+ com.closePath = false;
1782
+ com.dimA = 0;
1783
+
1784
+ /**
1785
+ * define angle threshold for
1786
+ * corner detection
1787
+ */
1788
+ let angleThreshold = 0.05;
1789
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
1790
+
1791
+ // update M for Z starting points
1792
+ if (type === 'M') {
1793
+ M = p;
1794
+ p0 = p;
1795
+ }
1796
+ else if (type.toLowerCase() === 'z') {
1797
+ p = M;
1798
+ }
1799
+
1800
+ // add on-path points
1801
+ com.p0 = p0;
1802
+ com.p = p;
1803
+
1804
+ let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
1805
+
1806
+ let dimA = getDistAv(p0, p);
1807
+ com.dimA = dimA;
1808
+
1809
+ /**
1810
+ * explicit and implicit linetos
1811
+ * - introduced by Z
1812
+ */
1813
+ if (type === 'L') com.lineto = true;
1814
+
1815
+ if (type === 'Z') {
1816
+ com.closePath = true;
1817
+ // if Z introduces an implicit lineto with a length
1818
+ if (M.x !== M0.x && M.y !== M0.y) {
1819
+ com.lineto = true;
1820
+ }
1821
+ }
1822
+
1823
+ // if bezier
1824
+ if (type === 'Q' || type === 'C') {
1825
+ cp1 = { x: values[0], y: values[1] };
1826
+ cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
1827
+ com.cp1 = cp1;
1828
+ if (cp2) com.cp2 = cp2;
1829
+ }
1830
+
1831
+ /**
1832
+ * check command flatness
1833
+ * we leave it to the bezier simplifier
1834
+ * to convert flat beziers to linetos
1835
+ * otherwise we may strip rather flat starting segments
1836
+ * preventing a better simplification
1837
+ */
1838
+
1839
+ if (values.length > 2) {
1840
+ if (type === 'Q' || type === 'C') commandPts.push(cp1);
1841
+ if (type === 'C') commandPts.push(cp2);
1842
+ commandPts.push(p);
1843
+
1844
+ let commandFlatness = commandIsFlat(commandPts);
1845
+ isFlat = commandFlatness.flat;
1846
+ com.flat = isFlat;
1847
+
1848
+ if (isFlat) {
1849
+ com.extreme = false;
1850
+ }
1851
+ }
1852
+
1853
+ /**
1854
+ * is extreme relative to bounding box
1855
+ * in case elements are rotated we can't rely on 90degree angles
1856
+ * so we interpret maximum x/y on-path points as well as extremes
1857
+ * but we ignore linetos to allow chunk compilation
1858
+ */
1859
+ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1860
+ com.extreme = true;
1861
+ }
1862
+
1863
+ let comN = pathData[c] ? pathData[c] : null;
1864
+ let comNValsL = comN ? comN.values.slice(-2) : null;
1865
+ typeN = comN ? comN.type : null;
1866
+
1867
+ // get bezier control points
1868
+ if (comN && (comN.type === 'Q' || comN.type === 'C')) {
1869
+ pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1870
+
1871
+ cp1N = { x: comN.values[0], y: comN.values[1] };
1872
+ cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1873
+ }
1874
+
1875
+ /**
1876
+ * Detect direction change points
1877
+ * this will prevent distortions when simplifying
1878
+ * e.g in the "spine" of an "S" glyph
1879
+ */
1880
+ area1 = getPolygonArea(commandPts);
1881
+ let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
1882
+ // update area
1883
+ area0 = area1;
1884
+
1885
+ if (signChange) {
1886
+
1887
+ com.directionChange = true;
1888
+ }
1889
+
1890
+ /**
1891
+ * check extremes or corners
1892
+ * for adjacent curves by
1893
+ * control point angles
1894
+ */
1895
+ if ((type === 'Q' || type === 'C')) {
1896
+
1897
+ if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
1898
+
1899
+ // check extremes
1900
+ let cpts = commandPts.slice(1);
1901
+
1902
+ let w = pN ? Math.abs(pN.x - p0.x) : 0;
1903
+ let h = pN ? Math.abs(pN.y - p0.y) : 0;
1904
+ let thresh = (w + h) / 2 * 0.1;
1905
+ let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1906
+
1907
+ let flatness2 = commandIsFlat(pts1, thresh);
1908
+ let isFlat2 = flatness2.flat;
1909
+
1910
+ /**
1911
+ * if current and next cubic are flat
1912
+ * we don't flag them as extremes to allow simplification
1913
+ */
1914
+ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1915
+
1916
+ if (hasExtremes) {
1917
+ com.extreme = true;
1918
+ }
1919
+
1920
+ // check corners
1921
+ else {
1922
+
1923
+ let cpts1 = cp2 ? [cp2, p] : [cp1, p];
1924
+ let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
1925
+
1926
+ let angCom1 = getAngle(...cpts1, true);
1927
+ let angCom2 = getAngle(...cpts2, true);
1928
+ let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI;
1929
+
1930
+ let cpDist1 = getSquareDistance(...cpts1);
1931
+ let cpDist2 = getSquareDistance(...cpts2);
1932
+
1933
+ let cornerThreshold = 10;
1934
+ let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2;
1935
+
1936
+ if (isCorner) {
1937
+ com.corner = true;
1938
+ }
1939
+ }
1940
+ }
1941
+ }
1942
+
1943
+ pathDataProps.push(com);
1944
+ p0 = p;
1945
+
1946
+ }
1947
+
1948
+ let dimA = (width + height) / 2;
1949
+
1950
+ pathDataPlus = { pathData: pathDataProps, bb: bb, dimA: dimA };
1951
+
1952
+ return pathDataPlus
1953
+
1954
+ }
1955
+
1956
+ function detectAccuracy(pathData) {
1957
+
1958
+ // Reference first MoveTo command (M)
1959
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
1960
+ let p0 = M;
1961
+ let p = M;
1962
+ pathData[0].decimals = 0;
1963
+ let minDim = Infinity;
1964
+
1965
+ // add average distances
1966
+ for (let i = 0, len = pathData.length; i < len; i++) {
1967
+ let com = pathData[i];
1968
+ let { type, values } = com;
1969
+
1970
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
1971
+ p={x:lastVals[0], y:lastVals[1]};
1972
+
1973
+ // use existing averave dimension value or calculate
1974
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
1975
+
1976
+ if(dimA && dimA<minDim) minDim = dimA;
1977
+
1978
+
1979
+
1980
+ if(type==='M'){
1981
+ M=p;
1982
+ }
1983
+ p0 = p;
1984
+ }
1985
+
1986
+ let decimalsAuto = Math.floor(50 / minDim).toString().length;
1987
+
1988
+ // clamp
1989
+ return Math.min(Math.max(0, decimalsAuto), 8)
1990
+
1991
+ }
1992
+
1993
+ /**
1994
+ * round path data
1995
+ * either by explicit decimal value or
1996
+ * based on suggested accuracy in path data
1997
+ */
1998
+ function roundPathData(pathData, decimals = -1) {
1999
+ // has recommended decimals
2000
+ let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2001
+
2002
+ for(let c=0, len=pathData.length; c<len; c++){
2003
+ let com=pathData[c];
2004
+
2005
+ if (decimals >-1 || hasDecimal) {
2006
+ decimals = hasDecimal ? com.decimals : decimals;
2007
+
2008
+ pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2009
+
2010
+ }
2011
+ } return pathData;
2012
+ }
2013
+
2014
+ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2015
+
2016
+ // test if cubic can be simplified to quadratic
2017
+ let cp1X = interpolate(p0, cp1, 1.5);
2018
+ let cp2X = interpolate(p, cp2, 1.5);
2019
+
2020
+ let dist0 = getDistAv(p0, p);
2021
+ let threshold = dist0 * 0.01;
2022
+ let dist1 = getDistAv(cp1X, cp2X);
2023
+
2024
+ let cp1_Q = null;
2025
+ let type = 'C';
2026
+ let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2027
+
2028
+ if (dist1 < threshold) {
2029
+ cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2030
+ if (cp1_Q) {
2031
+
2032
+ type = 'Q';
2033
+ values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2034
+ }
2035
+ }
2036
+
2037
+ return { type, values }
2038
+
2039
+ }
2040
+
2041
+ function convertPathData(pathData, {
2042
+ toShorthands = true,
2043
+ toRelative = true,
2044
+ decimals = 3
2045
+ } = {}) {
2046
+
2047
+ if (toShorthands) pathData = pathDataToShorthands(pathData);
2048
+
2049
+ // pre round - before relative conversion to minimize distortions
2050
+ pathData = roundPathData(pathData, decimals);
2051
+ if (toRelative) pathData = pathDataToRelative(pathData);
2052
+ if (decimals > -1) pathData = roundPathData(pathData, decimals);
2053
+ return pathData
2054
+ }
2055
+
2056
+ /**
2057
+ * convert cubic circle approximations
2058
+ * to more compact arcs
2059
+ */
2060
+
2061
+ function pathDataArcsToCubics(pathData, {
2062
+ arcAccuracy = 1
2063
+ } = {}) {
2064
+
2065
+ let pathDataCubic = [pathData[0]];
2066
+ for (let i = 1, len = pathData.length; i < len; i++) {
2067
+
2068
+ let com = pathData[i];
2069
+ let comPrev = pathData[i - 1];
2070
+ let valuesPrev = comPrev.values;
2071
+ let valuesPrevL = valuesPrev.length;
2072
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
2073
+
2074
+ if (com.type === 'A') {
2075
+ // add all C commands instead of Arc
2076
+ let cubicArcs = arcToBezier$1(p0, com.values, arcAccuracy);
2077
+ cubicArcs.forEach((cubicArc) => {
2078
+ pathDataCubic.push(cubicArc);
2079
+ });
2080
+ }
2081
+
2082
+ else {
2083
+ // add command
2084
+ pathDataCubic.push(com);
2085
+ }
2086
+ }
2087
+
2088
+ return pathDataCubic
2089
+
2090
+ }
2091
+
2092
+ function pathDataQuadraticToCubic(pathData) {
2093
+
2094
+ let pathDataQuadratic = [pathData[0]];
2095
+ for (let i = 1, len = pathData.length; i < len; i++) {
2096
+
2097
+ let com = pathData[i];
2098
+ let comPrev = pathData[i - 1];
2099
+ let valuesPrev = comPrev.values;
2100
+ let valuesPrevL = valuesPrev.length;
2101
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
2102
+
2103
+ if (com.type === 'Q') {
2104
+ pathDataQuadratic.push(quadratic2Cubic(p0, com.values));
2105
+ }
2106
+
2107
+ else {
2108
+ // add command
2109
+ pathDataQuadratic.push(com);
2110
+ }
2111
+ }
2112
+
2113
+ return pathDataQuadratic
2114
+ }
2115
+
2116
+ /**
2117
+ * convert quadratic commands to cubic
2118
+ */
2119
+ function quadratic2Cubic(p0, values) {
2120
+ if (Array.isArray(p0)) {
2121
+ p0 = {
2122
+ x: p0[0],
2123
+ y: p0[1]
2124
+ };
2125
+ }
2126
+ let cp1 = {
2127
+ x: p0.x + 2 / 3 * (values[0] - p0.x),
2128
+ y: p0.y + 2 / 3 * (values[1] - p0.y)
2129
+ };
2130
+ let cp2 = {
2131
+ x: values[2] + 2 / 3 * (values[0] - values[2]),
2132
+ y: values[3] + 2 / 3 * (values[1] - values[3])
2133
+ };
2134
+ return ({ type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, values[2], values[3]] });
2135
+ }
2136
+
2137
+ /**
2138
+ * convert pathData to
2139
+ * This is just a port of Dmitry Baranovskiy's
2140
+ * pathToRelative/Absolute methods used in snap.svg
2141
+ * https://github.com/adobe-webplatform/Snap.svg/
2142
+ */
2143
+
2144
+ function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decimals = -1) {
2145
+ if (decimals >= 0) {
2146
+ pathData[0].values = pathData[0].values.map(val => +val.toFixed(decimals));
2147
+ }
2148
+
2149
+ let M = pathData[0].values;
2150
+ let x = M[0],
2151
+ y = M[1],
2152
+ mx = x,
2153
+ my = y;
2154
+
2155
+ for (let i = 1, len = pathData.length; i < len; i++) {
2156
+ let com = pathData[i];
2157
+ let { type, values } = com;
2158
+ let newType = toRelative ? type.toLowerCase() : type.toUpperCase();
2159
+
2160
+ if (type !== newType) {
2161
+ type = newType;
2162
+ com.type = type;
2163
+
2164
+ switch (type) {
2165
+ case "a":
2166
+ case "A":
2167
+ values[5] = toRelative ? values[5] - x : values[5] + x;
2168
+ values[6] = toRelative ? values[6] - y : values[6] + y;
2169
+ break;
2170
+ case "v":
2171
+ case "V":
2172
+ values[0] = toRelative ? values[0] - y : values[0] + y;
2173
+ break;
2174
+ case "h":
2175
+ case "H":
2176
+ values[0] = toRelative ? values[0] - x : values[0] + x;
2177
+ break;
2178
+ case "m":
2179
+ case "M":
2180
+ if (toRelative) {
2181
+ values[0] -= x;
2182
+ values[1] -= y;
2183
+ } else {
2184
+ values[0] += x;
2185
+ values[1] += y;
2186
+ }
2187
+ mx = toRelative ? values[0] + x : values[0];
2188
+ my = toRelative ? values[1] + y : values[1];
2189
+ break;
2190
+ default:
2191
+ if (values.length) {
2192
+ for (let v = 0; v < values.length; v++) {
2193
+ values[v] = toRelative
2194
+ ? values[v] - (v % 2 ? y : x)
2195
+ : values[v] + (v % 2 ? y : x);
2196
+ }
2197
+ }
2198
+ }
2199
+ }
2200
+
2201
+ let vLen = values.length;
2202
+ switch (type) {
2203
+ case "z":
2204
+ case "Z":
2205
+ x = mx;
2206
+ y = my;
2207
+ break;
2208
+ case "h":
2209
+ case "H":
2210
+ x = toRelative ? x + values[0] : values[0];
2211
+ break;
2212
+ case "v":
2213
+ case "V":
2214
+ y = toRelative ? y + values[0] : values[0];
2215
+ break;
2216
+ case "m":
2217
+ case "M":
2218
+ mx = values[vLen - 2] + (toRelative ? x : 0);
2219
+ my = values[vLen - 1] + (toRelative ? y : 0);
2220
+ default:
2221
+ x = values[vLen - 2] + (toRelative ? x : 0);
2222
+ y = values[vLen - 1] + (toRelative ? y : 0);
2223
+ }
2224
+
2225
+ if (decimals >= 0) {
2226
+ com.values = com.values.map(val => +val.toFixed(decimals));
2227
+ }
2228
+ }
2229
+ return pathData;
2230
+ }
2231
+
2232
+ function pathDataToRelative(pathData, decimals = -1) {
2233
+ return pathDataToAbsoluteOrRelative(pathData, true, decimals)
2234
+ }
2235
+
2236
+ function pathDataToAbsolute(pathData, decimals = -1) {
2237
+ return pathDataToAbsoluteOrRelative(pathData, false, decimals)
2238
+ }
2239
+
2240
+ /**
2241
+ * decompose/convert shorthands to "longhand" commands:
2242
+ * H, V, S, T => L, L, C, Q
2243
+ * reversed method: pathDataToShorthands()
2244
+ */
2245
+
2246
+ function pathDataToLonghands(pathData, decimals = -1, test = true) {
2247
+
2248
+ // analyze pathdata – if you're sure your data is already absolute skip it via test=false
2249
+ let hasRel = false;
2250
+
2251
+ if (test) {
2252
+ let commandTokens = pathData.map(com => { return com.type }).join('');
2253
+ let hasShorthands = /[hstv]/gi.test(commandTokens);
2254
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
2255
+
2256
+ if (!hasShorthands) {
2257
+ return pathData;
2258
+ }
2259
+ }
2260
+
2261
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2262
+
2263
+ let pathDataLonghand = [];
2264
+ let comPrev = {
2265
+ type: "M",
2266
+ values: pathData[0].values
2267
+ };
2268
+ pathDataLonghand.push(comPrev);
2269
+
2270
+ for (let i = 1, len = pathData.length; i < len; i++) {
2271
+ let com = pathData[i];
2272
+ let { type, values } = com;
2273
+ let valuesL = values.length;
2274
+ let valuesPrev = comPrev.values;
2275
+ let valuesPrevL = valuesPrev.length;
2276
+ let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
2277
+ let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
2278
+ let [prevX, prevY] = [
2279
+ valuesPrev[valuesPrevL - 2],
2280
+ valuesPrev[valuesPrevL - 1]
2281
+ ];
2282
+ switch (type) {
2283
+ case "H":
2284
+ comPrev = {
2285
+ type: "L",
2286
+ values: [values[0], prevY]
2287
+ };
2288
+ break;
2289
+ case "V":
2290
+ comPrev = {
2291
+ type: "L",
2292
+ values: [prevX, values[0]]
2293
+ };
2294
+ break;
2295
+ case "T":
2296
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
2297
+ [prevX, prevY] = [
2298
+ valuesPrev[valuesPrevL - 2],
2299
+ valuesPrev[valuesPrevL - 1]
2300
+ ];
2301
+ // new control point
2302
+ cpN1X = prevX + (prevX - cp1X);
2303
+ cpN1Y = prevY + (prevY - cp1Y);
2304
+ comPrev = {
2305
+ type: "Q",
2306
+ values: [cpN1X, cpN1Y, x, y]
2307
+ };
2308
+ break;
2309
+ case "S":
2310
+
2311
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
2312
+ [prevX, prevY] = [
2313
+ valuesPrev[valuesPrevL - 2],
2314
+ valuesPrev[valuesPrevL - 1]
2315
+ ];
2316
+
2317
+ [cp2X, cp2Y] =
2318
+ valuesPrevL > 2 && comPrev.type !== 'A' ?
2319
+ [valuesPrev[2], valuesPrev[3]] :
2320
+ [prevX, prevY];
2321
+
2322
+ // new control points
2323
+ cpN1X = 2 * prevX - cp2X;
2324
+ cpN1Y = 2 * prevY - cp2Y;
2325
+ cpN2X = values[0];
2326
+ cpN2Y = values[1];
2327
+ comPrev = {
2328
+ type: "C",
2329
+ values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
2330
+ };
2331
+
2332
+ break;
2333
+ default:
2334
+ comPrev = {
2335
+ type: type,
2336
+ values: values
2337
+ };
2338
+ }
2339
+ // round final longhand values
2340
+ if (decimals > -1) {
2341
+ comPrev.values = comPrev.values.map(val => { return +val.toFixed(decimals) });
2342
+ }
2343
+
2344
+ pathDataLonghand.push(comPrev);
2345
+ }
2346
+ return pathDataLonghand;
2347
+ }
2348
+
2349
+ /**
2350
+ * apply shorthand commands if possible
2351
+ * L, L, C, Q => H, V, S, T
2352
+ * reversed method: pathDataToLonghands()
2353
+ */
2354
+ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2355
+
2356
+ /**
2357
+ * analyze pathdata – if you're sure your data is already absolute skip it via test=false
2358
+ */
2359
+ let hasRel;
2360
+ if (test) {
2361
+ let commandTokens = pathData.map(com => { return com.type }).join('');
2362
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
2363
+ }
2364
+
2365
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2366
+
2367
+ let comShort = {
2368
+ type: "M",
2369
+ values: pathData[0].values
2370
+ };
2371
+
2372
+ if (pathData[0].decimals) {
2373
+
2374
+ comShort.decimals = pathData[0].decimals;
2375
+ }
2376
+
2377
+ let pathDataShorts = [comShort];
2378
+
2379
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
2380
+ let p;
2381
+ let tolerance = 0.01;
2382
+
2383
+ for (let i = 1, len = pathData.length; i < len; i++) {
2384
+
2385
+ let com = pathData[i];
2386
+ let { type, values } = com;
2387
+ let valuesLast = values.slice(-2);
2388
+
2389
+ // previoius command
2390
+ let comPrev = pathData[i - 1];
2391
+ let typePrev = comPrev.type;
2392
+
2393
+ p = { x: valuesLast[0], y: valuesLast[1] };
2394
+
2395
+ // first bezier control point for S/T shorthand tests
2396
+ let cp1 = { x: values[0], y: values[1] };
2397
+
2398
+ let w = Math.abs(p.x - p0.x);
2399
+ let h = Math.abs(p.y - p0.y);
2400
+ let thresh = (w + h) / 2 * tolerance;
2401
+
2402
+ let diffX, diffY, diff, cp1_reflected;
2403
+
2404
+ switch (type) {
2405
+ case "L":
2406
+
2407
+ if (h === 0 || (h < thresh && w > thresh)) {
2408
+
2409
+ comShort = {
2410
+ type: "H",
2411
+ values: [values[0]]
2412
+ };
2413
+ }
2414
+
2415
+ // V
2416
+ else if (w === 0 || (h > thresh && w < thresh)) {
2417
+
2418
+ comShort = {
2419
+ type: "V",
2420
+ values: [values[1]]
2421
+ };
2422
+ } else {
2423
+
2424
+ comShort = com;
2425
+ }
2426
+
2427
+ break;
2428
+
2429
+ case "Q":
2430
+
2431
+ // skip test
2432
+ if (typePrev !== 'Q') {
2433
+
2434
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2435
+ pathDataShorts.push(com);
2436
+ continue;
2437
+ }
2438
+
2439
+ let cp1_prev = { x: comPrev.values[0], y: comPrev.values[1] };
2440
+ // reflected Q control points
2441
+ cp1_reflected = { x: (2 * p0.x - cp1_prev.x), y: (2 * p0.y - cp1_prev.y) };
2442
+
2443
+ diffX = Math.abs(cp1.x - cp1_reflected.x);
2444
+ diffY = Math.abs(cp1.y - cp1_reflected.y);
2445
+ diff = (diffX + diffY) / 2;
2446
+
2447
+ if (diff < thresh) {
2448
+
2449
+ comShort = {
2450
+ type: "T",
2451
+ values: [p.x, p.y]
2452
+ };
2453
+ } else {
2454
+ comShort = com;
2455
+ }
2456
+
2457
+ break;
2458
+ case "C":
2459
+
2460
+ let cp2 = { x: values[2], y: values[3] };
2461
+
2462
+ if (typePrev !== 'C') {
2463
+
2464
+ pathDataShorts.push(com);
2465
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2466
+ continue;
2467
+ }
2468
+
2469
+ let cp2_prev = { x: comPrev.values[2], y: comPrev.values[3] };
2470
+
2471
+ // reflected C control points
2472
+ cp1_reflected = { x: (2 * p0.x - cp2_prev.x), y: (2 * p0.y - cp2_prev.y) };
2473
+
2474
+ diffX = Math.abs(cp1.x - cp1_reflected.x);
2475
+ diffY = Math.abs(cp1.y - cp1_reflected.y);
2476
+ diff = (diffX + diffY) / 2;
2477
+
2478
+ if (diff < thresh) {
2479
+
2480
+ comShort = {
2481
+ type: "S",
2482
+ values: [cp2.x, cp2.y, p.x, p.y]
2483
+ };
2484
+ } else {
2485
+ comShort = com;
2486
+ }
2487
+ break;
2488
+ default:
2489
+ comShort = {
2490
+ type: type,
2491
+ values: values
2492
+ };
2493
+ }
2494
+
2495
+ // add decimal info
2496
+ if (com.decimals || com.decimals === 0) {
2497
+ comShort.decimals = com.decimals;
2498
+ }
2499
+
2500
+ // round final values
2501
+ if (decimals > -1) {
2502
+ comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) });
2503
+ }
2504
+
2505
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2506
+ pathDataShorts.push(comShort);
2507
+ }
2508
+ return pathDataShorts;
2509
+ }
2510
+
2511
+ /**
2512
+ * convert arctocommands to cubic bezier
2513
+ * based on puzrin's a2c.js
2514
+ * https://github.com/fontello/svgpath/blob/master/lib/a2c.js
2515
+ * returns pathData array
2516
+ */
2517
+
2518
+ function arcToBezier$1(p0, values, splitSegments = 1) {
2519
+ const TAU = Math.PI * 2;
2520
+ let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
2521
+
2522
+ if (rx === 0 || ry === 0) {
2523
+ return []
2524
+ }
2525
+
2526
+ let phi = rotation ? rotation * TAU / 360 : 0;
2527
+ let sinphi = phi ? Math.sin(phi) : 0;
2528
+ let cosphi = phi ? Math.cos(phi) : 1;
2529
+ let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
2530
+ let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
2531
+
2532
+ if (pxp === 0 && pyp === 0) {
2533
+ return []
2534
+ }
2535
+ rx = Math.abs(rx);
2536
+ ry = Math.abs(ry);
2537
+ let lambda =
2538
+ pxp * pxp / (rx * rx) +
2539
+ pyp * pyp / (ry * ry);
2540
+ if (lambda > 1) {
2541
+ let lambdaRt = Math.sqrt(lambda);
2542
+ rx *= lambdaRt;
2543
+ ry *= lambdaRt;
2544
+ }
2545
+
2546
+ /**
2547
+ * parametrize arc to
2548
+ * get center point start and end angles
2549
+ */
2550
+ let rxsq = rx * rx,
2551
+ rysq = rx === ry ? rxsq : ry * ry;
2552
+
2553
+ let pxpsq = pxp * pxp,
2554
+ pypsq = pyp * pyp;
2555
+ let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq);
2556
+
2557
+ if (radicant <= 0) {
2558
+ radicant = 0;
2559
+ } else {
2560
+ radicant /= (rxsq * pypsq) + (rysq * pxpsq);
2561
+ radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1);
2562
+ }
2563
+
2564
+ let centerxp = radicant ? radicant * rx / ry * pyp : 0;
2565
+ let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
2566
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
2567
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
2568
+
2569
+ let vx1 = (pxp - centerxp) / rx;
2570
+ let vy1 = (pyp - centeryp) / ry;
2571
+ let vx2 = (-pxp - centerxp) / rx;
2572
+ let vy2 = (-pyp - centeryp) / ry;
2573
+
2574
+ // get start and end angle
2575
+ const vectorAngle = (ux, uy, vx, vy) => {
2576
+ let dot = +(ux * vx + uy * vy).toFixed(9);
2577
+ if (dot === 1 || dot === -1) {
2578
+ return dot === 1 ? 0 : Math.PI
2579
+ }
2580
+ dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot);
2581
+ let sign = (ux * vy - uy * vx < 0) ? -1 : 1;
2582
+ return sign * Math.acos(dot);
2583
+ };
2584
+
2585
+ let ang1 = vectorAngle(1, 0, vx1, vy1),
2586
+ ang2 = vectorAngle(vx1, vy1, vx2, vy2);
2587
+
2588
+ if (sweepFlag === 0 && ang2 > 0) {
2589
+ ang2 -= Math.PI * 2;
2590
+ }
2591
+ else if (sweepFlag === 1 && ang2 < 0) {
2592
+ ang2 += Math.PI * 2;
2593
+ }
2594
+
2595
+ let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
2596
+
2597
+ // increase segments for more accureate length calculations
2598
+ let segments = ratio * splitSegments;
2599
+ ang2 /= segments;
2600
+ let pathDataArc = [];
2601
+
2602
+ // If 90 degree circular arc, use a constant
2603
+ // https://pomax.github.io/bezierinfo/#circles_cubic
2604
+ // k=0.551784777779014
2605
+ const angle90 = 1.5707963267948966;
2606
+ const k = 0.551785;
2607
+ let a = ang2 === angle90 ? k :
2608
+ (
2609
+ ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
2610
+ );
2611
+
2612
+ let cos2 = ang2 ? Math.cos(ang2) : 1;
2613
+ let sin2 = ang2 ? Math.sin(ang2) : 0;
2614
+ let type = 'C';
2615
+
2616
+ const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
2617
+ let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
2618
+ let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
2619
+ let x2 = Math.cos(ang1 + ang2);
2620
+ let y2 = Math.sin(ang1 + ang2);
2621
+
2622
+ return [
2623
+ { x: x1 - y1 * a, y: y1 + x1 * a },
2624
+ { x: x2 + y2 * a, y: y2 - x2 * a },
2625
+ { x: x2, y: y2 }
2626
+ ];
2627
+ };
2628
+
2629
+ for (let i = 0; i < segments; i++) {
2630
+ let com = { type: type, values: [] };
2631
+ let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);
2632
+
2633
+ curve.forEach((pt) => {
2634
+ let x = pt.x * rx;
2635
+ let y = pt.y * ry;
2636
+ com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery);
2637
+ });
2638
+ pathDataArc.push(com);
2639
+ ang1 += ang2;
2640
+ }
2641
+
2642
+ return pathDataArc;
2643
+ }
2644
+
2645
+ /**
2646
+ * cubics to arcs
2647
+ */
2648
+
2649
+ function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
2650
+
2651
+ let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
2652
+
2653
+ let arcSegArea = 0, isArc = false;
2654
+
2655
+ // check angles
2656
+ let angle1 = getAngle(p0, cp1, true);
2657
+ let angle2 = getAngle(p, cp2, true);
2658
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
2659
+
2660
+ let angleDiff = Math.abs((deltaAngle % 180) - 90);
2661
+ let isRightAngle = angleDiff < 3;
2662
+
2663
+ /*
2664
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
2665
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
2666
+
2667
+ // assumed centroid
2668
+ let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
2669
+
2670
+ let dist0 = getSquareDistance(p0, p)
2671
+ let dist1 = getSquareDistance(p0, ptC)
2672
+ let dist2 = getSquareDistance(p, ptC)
2673
+
2674
+ // let mid point
2675
+ let ptM = pointAtT([p0, cp1, cp2, p], 0.5)
2676
+
2677
+ let diff1 = Math.abs(dist1 - dist2)
2678
+
2679
+ if (diff1 <= dist0 * 0.01) {
2680
+
2681
+ let r = Math.sqrt((dist1 + dist2) / 2)
2682
+
2683
+ let arcArea = getPolygonArea([p0, cp1, cp2, p])
2684
+ let sweep = arcArea < 0 ? 0 : 1;
2685
+
2686
+ // new arc command
2687
+ let comArc = { type: 'A', values: [r, r, 0, 0, sweep, p.x, p.y] };
2688
+
2689
+ isArc = true;
2690
+
2691
+ return { com: comArc, isArc, area: arcSegArea }
2692
+
2693
+ }
2694
+ */
2695
+
2696
+ if (isRightAngle) {
2697
+ // point between cps
2698
+
2699
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
2700
+
2701
+ if (pI) {
2702
+
2703
+ let r1 = getDistance(p0, pI);
2704
+ let r2 = getDistance(p, pI);
2705
+
2706
+ let rMax = +Math.max(r1, r2).toFixed(8);
2707
+ let rMin = +Math.min(r1, r2).toFixed(8);
2708
+
2709
+ let rx = rMin;
2710
+ let ry = rMax;
2711
+
2712
+ let arcArea = getPolygonArea([p0, cp1, cp2, p]);
2713
+ let sweep = arcArea < 0 ? 0 : 1;
2714
+
2715
+ let w = Math.abs(p.x - p0.x);
2716
+ let h = Math.abs(p.y - p0.y);
2717
+ let landscape = w > h;
2718
+
2719
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
2720
+
2721
+ if (circular) {
2722
+
2723
+ rx = rMax;
2724
+ ry = rx;
2725
+ }
2726
+
2727
+ if (landscape) {
2728
+
2729
+ rx = rMax;
2730
+ ry = rMin;
2731
+ }
2732
+
2733
+ // get original cubic area
2734
+ let comO = [
2735
+ { type: 'M', values: [p0.x, p0.y] },
2736
+ { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
2737
+ ];
2738
+
2739
+ let comArea = getPathArea(comO);
2740
+
2741
+ // new arc command
2742
+ let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
2743
+
2744
+ // calculate arc seg area
2745
+ arcSegArea = (Math.PI * (rx * ry)) / 4;
2746
+
2747
+ // subtract polygon between start, end and center point
2748
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]));
2749
+
2750
+ let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
2751
+
2752
+ if (areaDiff < tolerance) {
2753
+ isArc = true;
2754
+ com = comArc;
2755
+ }
2756
+
2757
+ }
2758
+ }
2759
+
2760
+ return { com: com, isArc, area: arcSegArea }
2761
+
2762
+ }
2763
+
2764
+ /**
2765
+ * combine adjacent arcs
2766
+ */
2767
+
2768
+ function combineArcs(pathData) {
2769
+
2770
+ let arcSeq = [[]];
2771
+ let ind = 0;
2772
+ let arcIndices = [[]];
2773
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
2774
+
2775
+ for (let i = 0, len = pathData.length; i < len; i++) {
2776
+ let com = pathData[i];
2777
+ let { type, values } = com;
2778
+
2779
+ if (type === 'A') {
2780
+
2781
+ let comPrev = pathData[i - 1];
2782
+
2783
+ /**
2784
+ * previous p0 values might not be correct
2785
+ * anymore due to cubic simplification
2786
+ */
2787
+ let valsL = comPrev.values.slice(-2);
2788
+ p0 = { x: valsL[0], y: valsL[1] };
2789
+
2790
+ let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
2791
+
2792
+ // check if arc is circular
2793
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
2794
+
2795
+ p = { x: values[5], y: values[6] };
2796
+ com.p0 = p0;
2797
+ com.p = p;
2798
+ com.circular = circular;
2799
+
2800
+ let comNext = pathData[i + 1];
2801
+
2802
+ if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
2803
+ arcSeq[ind].push(com);
2804
+ arcIndices[ind].push(i);
2805
+ }
2806
+
2807
+ if (comNext && comNext.type === 'A') {
2808
+ let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
2809
+ let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0;
2810
+ let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0;
2811
+
2812
+ p = { x: comNext.values[5], y: comNext.values[6] };
2813
+ comNext.p0 = p0;
2814
+ comNext.p = p;
2815
+
2816
+ // add if radii are almost same
2817
+ if (diffRx < 5 && diffRy < 5) {
2818
+
2819
+ arcSeq[ind].push(comNext);
2820
+ arcIndices[ind].push(i + 1);
2821
+ } else {
2822
+
2823
+ // start new segment
2824
+ arcSeq.push([]);
2825
+ arcIndices.push([]);
2826
+ ind++;
2827
+
2828
+ }
2829
+ }
2830
+
2831
+ else {
2832
+
2833
+ arcSeq.push([]);
2834
+ arcIndices.push([]);
2835
+ ind++;
2836
+ }
2837
+ }
2838
+ }
2839
+
2840
+ if (!arcIndices.length) return pathData;
2841
+
2842
+ arcSeq = arcSeq.filter(item => item.length);
2843
+ arcIndices = arcIndices.filter(item => item.length);
2844
+
2845
+ // Process in reverse to avoid index shifting
2846
+ for (let i = arcSeq.length - 1; i >= 0; i--) {
2847
+ const seq = arcSeq[i];
2848
+ const start = arcIndices[i][0];
2849
+ const len = seq.length;
2850
+
2851
+ // Average radii to prevent distortions
2852
+ let rxA = 0, ryA = 0;
2853
+ seq.forEach(({ values }) => {
2854
+ const [rx, ry] = values;
2855
+ rxA += rx;
2856
+ ryA += ry;
2857
+ });
2858
+ rxA /= len;
2859
+ ryA /= len;
2860
+
2861
+ // Correct near-circular arcs
2862
+
2863
+ // check if arc is circular
2864
+ let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
2865
+
2866
+ if (circular) {
2867
+ // average radii
2868
+ rxA = (rxA + ryA) / 2;
2869
+ ryA = rxA;
2870
+ }
2871
+
2872
+ let comPrev = pathData[start - 1];
2873
+ let comPrevVals = comPrev.values.slice(-2);
2874
+ ({ type: 'M', values: [comPrevVals[0], comPrevVals[1]] });
2875
+
2876
+ if (len === 4) {
2877
+
2878
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
2879
+ let [, , , , , x2, y2] = seq[3].values;
2880
+
2881
+ if (circular) {
2882
+
2883
+ // simplify radii
2884
+ rxA = 1;
2885
+ ryA = 1;
2886
+ }
2887
+
2888
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
2889
+ let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
2890
+
2891
+ // This now correctly replaces the original 4 arc commands with 2
2892
+ pathData.splice(start, len, com1, com2);
2893
+
2894
+ }
2895
+
2896
+ else if (len === 3) {
2897
+
2898
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
2899
+ let [rx2, ry2, , , , x2, y2] = seq[2].values;
2900
+
2901
+ // must be large arc
2902
+ largeArc = 1;
2903
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
2904
+
2905
+ // replace
2906
+ pathData.splice(start, len, com1);
2907
+
2908
+ }
2909
+
2910
+ else if (len === 2) {
2911
+
2912
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
2913
+ let [rx2, ry2, , , , x2, y2] = seq[1].values;
2914
+
2915
+ // if circular or non-elliptic xAxisRotation has no effect
2916
+ if (circular) {
2917
+ rxA = 1;
2918
+ ryA = 1;
2919
+ xAxisRotation = 0;
2920
+ }
2921
+
2922
+ // check if arc is already ideal
2923
+ let { p0, p } = seq[0];
2924
+ let [p0_1, p_1] = [seq[1].p0, seq[1].p];
2925
+
2926
+ if (p0.x !== p_1.x || p0.y !== p_1.y) {
2927
+
2928
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
2929
+
2930
+ // replace
2931
+ pathData.splice(start, len, com1);
2932
+ }
2933
+ }
2934
+
2935
+ else ;
2936
+ }
2937
+
2938
+ return pathData
2939
+ }
2940
+
2941
+ /**
2942
+ * parse normalized
2943
+ */
2944
+
2945
+ function normalizePathData(pathData = [],
2946
+ {
2947
+ toAbsolute = true,
2948
+ toLonghands = true,
2949
+ quadraticToCubic = false,
2950
+ arcToCubic = false,
2951
+ arcAccuracy = 2,
2952
+ } = {},
2953
+
2954
+ {
2955
+ hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
2956
+ } = {}
2957
+ ) {
2958
+
2959
+ // pathdata properties - test= true adds a manual test
2960
+ if (testTypes) {
2961
+
2962
+ let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
2963
+ hasRelatives = /[lcqamts]/gi.test(commands);
2964
+ hasQuadratics = /[qt]/gi.test(commands);
2965
+ hasArcs = /[a]/gi.test(commands);
2966
+ hasShorthands = /[vhst]/gi.test(commands);
2967
+ isPoly = /[mlz]/gi.test(commands);
2968
+ }
2969
+
2970
+ /**
2971
+ * normalize:
2972
+ * convert to all absolute
2973
+ * all longhands
2974
+ */
2975
+
2976
+ if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
2977
+ toLonghands = true;
2978
+ toAbsolute = true;
2979
+ }
2980
+
2981
+ if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
2982
+ if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
2983
+ if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
2984
+ if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
2985
+
2986
+ return pathData;
2987
+
2988
+ }
2989
+
2990
+ function parsePathDataNormalized(d,
2991
+ {
2992
+ // necessary for most calculations
2993
+ toAbsolute = true,
2994
+ toLonghands = true,
2995
+
2996
+ // not necessary unless you need cubics only
2997
+ quadraticToCubic = false,
2998
+
2999
+ // mostly a fallback if arc calculations fail
3000
+ arcToCubic = false,
3001
+ // arc to cubic precision - adds more segments for better precision
3002
+ arcAccuracy = 4,
3003
+ } = {}
3004
+ ) {
3005
+
3006
+ let pathDataObj = parsePathDataString(d);
3007
+ let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
3008
+ let pathData = pathDataObj.pathData;
3009
+
3010
+ // normalize
3011
+ pathData = normalizePathData(pathData,
3012
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
3013
+
3014
+ { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
3015
+ );
3016
+
3017
+ return pathData;
3018
+ }
3019
+
3020
+ const commandSet = new Set([
3021
+ 0x4D, 0x6D, 0x41, 0x61, 0x43, 0x63,
3022
+ 0x4C, 0x6C, 0x51, 0x71, 0x53, 0x73,
3023
+ 0x54, 0x74, 0x48, 0x68, 0x56, 0x76,
3024
+ 0x5A, 0x7A
3025
+ ]);
3026
+
3027
+ const paramCountsArr = new Uint8Array(128);
3028
+ // M starting point
3029
+ paramCountsArr[0x4D] = 2;
3030
+ paramCountsArr[0x6D] = 2;
3031
+
3032
+ // A Arc
3033
+ paramCountsArr[0x41] = 7;
3034
+ paramCountsArr[0x61] = 7;
3035
+
3036
+ // C Cubic Bézier
3037
+ paramCountsArr[0x43] = 6;
3038
+ paramCountsArr[0x63] = 6;
3039
+
3040
+ // L Line To
3041
+ paramCountsArr[0x4C] = 2;
3042
+ paramCountsArr[0x6C] = 2;
3043
+
3044
+ // Q Quadratic Bézier
3045
+ paramCountsArr[0x51] = 4;
3046
+ paramCountsArr[0x71] = 4;
3047
+
3048
+ // S Smooth Cubic Bézier
3049
+ paramCountsArr[0x53] = 4;
3050
+ paramCountsArr[0x73] = 4;
3051
+
3052
+ // T Smooth Quadratic Bézier
3053
+ paramCountsArr[0x54] = 2;
3054
+ paramCountsArr[0x74] = 2;
3055
+
3056
+ // H Horizontal Line
3057
+ paramCountsArr[0x48] = 1;
3058
+ paramCountsArr[0x68] = 1;
3059
+
3060
+ // V Vertical Line
3061
+ paramCountsArr[0x56] = 1;
3062
+ paramCountsArr[0x76] = 1;
3063
+
3064
+ // Z Close Path
3065
+ paramCountsArr[0x5A] = 0;
3066
+ paramCountsArr[0x7A] = 0;
3067
+
3068
+ function parsePathDataString(d, debug = true) {
3069
+ d = d.trim();
3070
+
3071
+ if (d === '') {
3072
+ return {
3073
+ pathData: [],
3074
+ hasRelatives: false,
3075
+ hasShorthands: false,
3076
+ hasQuadratics: false,
3077
+ hasArcs: false
3078
+ }
3079
+ }
3080
+
3081
+ const SPECIAL_SPACES = new Set([
3082
+ 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
3083
+ 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
3084
+ ]);
3085
+
3086
+ const isSpace = (ch) => {
3087
+ return (ch === 0x20) || (ch === 0x002C) || // White spaces or comma
3088
+ (ch === 0x0A) || (ch === 0x0D) || // nl cr
3089
+ (ch === 0x2028) || (ch === 0x2029) || // Line terminators
3090
+ (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
3091
+ (ch >= 0x1680 && SPECIAL_SPACES.has(ch));
3092
+ };
3093
+
3094
+ let i = 0, len = d.length;
3095
+ let lastCommand = "";
3096
+ let pathData = [];
3097
+ let itemCount = -1;
3098
+ let val = '';
3099
+ let wasE = false;
3100
+ let floatCount = 0;
3101
+ let valueIndex = 0;
3102
+ let maxParams = 0;
3103
+ let needsNewSegment = false;
3104
+ let foundCommands = new Set([]);
3105
+
3106
+ // collect errors
3107
+ let log = [];
3108
+ let feedback;
3109
+
3110
+ const addSeg = () => {
3111
+ // Create new segment if needed before adding the minus sign
3112
+ if (needsNewSegment) {
3113
+
3114
+ // sanitize implicit linetos
3115
+ if (lastCommand === 'M') lastCommand = 'L';
3116
+ else if (lastCommand === 'm') lastCommand = 'l';
3117
+
3118
+ pathData.push({ type: lastCommand, values: [] });
3119
+
3120
+ itemCount++;
3121
+ valueIndex = 0;
3122
+ needsNewSegment = false;
3123
+ }
3124
+ };
3125
+
3126
+ const pushVal = (checkFloats = false) => {
3127
+
3128
+ // regular value or float
3129
+ if (!checkFloats ? val !== '' : floatCount > 0) {
3130
+
3131
+ // error: no first command
3132
+ if (debug && itemCount === -1) {
3133
+
3134
+ feedback = 'Pathdata must start with M command';
3135
+ log.push(feedback);
3136
+
3137
+ // add M command to collect subsequent errors
3138
+ lastCommand = 'M';
3139
+ pathData.push({ type: lastCommand, values: [] });
3140
+ maxParams = 2;
3141
+ valueIndex = 0;
3142
+ itemCount++;
3143
+
3144
+ }
3145
+
3146
+ if (lastCommand === 'A' || lastCommand === 'a') {
3147
+ val = sanitizeArc();
3148
+
3149
+ pathData[itemCount].values.push(...val);
3150
+
3151
+ } else {
3152
+ // error: leading zeroes
3153
+ if (debug && val[1] && val[1] !== '.' && val[0] === '0') {
3154
+ feedback = `${itemCount}. command: Leading zeros not valid: ${val}`;
3155
+ log.push(feedback);
3156
+ }
3157
+ pathData[itemCount].values.push(+val);
3158
+ }
3159
+
3160
+ valueIndex++;
3161
+ val = '';
3162
+ floatCount = 0;
3163
+
3164
+ // Mark that a new segment is needed if maxParams is reached
3165
+ needsNewSegment = valueIndex >= maxParams;
3166
+
3167
+ }
3168
+ };
3169
+
3170
+ const sanitizeArc = () => {
3171
+
3172
+ let valLen = val.length;
3173
+ let arcSucks = false;
3174
+
3175
+ // large arc and sweep
3176
+ if (valueIndex === 3 && valLen === 2) {
3177
+
3178
+ val = [+val[0], +val[1]];
3179
+ arcSucks = true;
3180
+ valueIndex++;
3181
+ }
3182
+
3183
+ // sweep and final
3184
+ else if (valueIndex === 4 && valLen > 1) {
3185
+
3186
+ val = [+val[0], +val[1]];
3187
+ arcSucks = true;
3188
+ valueIndex++;
3189
+ }
3190
+
3191
+ // large arc, sweep and final pt combined
3192
+ else if (valueIndex === 3 && valLen >= 3) {
3193
+
3194
+ val = [+val[0], +val[1], +val.substring(2)];
3195
+ arcSucks = true;
3196
+ valueIndex += 2;
3197
+ }
3198
+
3199
+ return !arcSucks ? [+val] : val;
3200
+
3201
+ };
3202
+
3203
+ const validateCommand = () => {
3204
+
3205
+ if (itemCount > 0) {
3206
+ let lastCom = pathData[itemCount];
3207
+ let valLen = lastCom.values.length;
3208
+
3209
+ if ((valLen && valLen < maxParams) || (valLen && valLen > maxParams) || ((lastCommand === 'z' || lastCommand === 'Z') && valLen > 0)) {
3210
+ let diff = maxParams - valLen;
3211
+ feedback = `${itemCount}. command of type "${lastCommand}": ${diff} values too few - ${maxParams} expected`;
3212
+
3213
+ let prevFeedback = log[log.length - 1];
3214
+
3215
+ if (prevFeedback !== feedback) {
3216
+ log.push(feedback);
3217
+ }
3218
+ }
3219
+ }
3220
+ };
3221
+
3222
+ let isE = false;
3223
+ let isMinusorPlus = false;
3224
+ let isDot = false;
3225
+
3226
+ while (i < len) {
3227
+
3228
+ let charCode = d.charCodeAt(i);
3229
+
3230
+ let isDigit = (charCode > 47 && charCode < 58);
3231
+ if (!isDigit) {
3232
+ isE = (charCode === 101 || charCode === 69);
3233
+ isMinusorPlus = (charCode === 45 || charCode === 43);
3234
+ isDot = charCode === 46;
3235
+ }
3236
+
3237
+ /**
3238
+ * number related:
3239
+ * digit, e-notation, dot or -/+ operator
3240
+ */
3241
+
3242
+ if (
3243
+ isDigit ||
3244
+ isMinusorPlus ||
3245
+ isDot ||
3246
+ isE
3247
+ ) {
3248
+
3249
+ // minus or float/dot separated: 0x2D=hyphen; 0x2E=dot
3250
+ if (!wasE && (charCode === 0x2D || charCode === 0x2E)) {
3251
+
3252
+ // checkFloats changes condition for value adding
3253
+ let checkFloats = charCode === 0x2E;
3254
+
3255
+ // new val
3256
+ pushVal(checkFloats);
3257
+
3258
+ // new segment
3259
+ addSeg();
3260
+
3261
+ // concatenated floats
3262
+ if (checkFloats) {
3263
+ floatCount++;
3264
+ }
3265
+ }
3266
+
3267
+ // regular splitting
3268
+ else {
3269
+
3270
+ addSeg();
3271
+ }
3272
+
3273
+ val += d[i];
3274
+
3275
+ // e/scientific notation in value
3276
+ wasE = isE;
3277
+ i++;
3278
+ continue;
3279
+ }
3280
+
3281
+ /**
3282
+ * Separated by white space
3283
+ */
3284
+ if ((charCode < 48 || charCode > 5759) && isSpace(charCode)) {
3285
+
3286
+ // push value
3287
+ pushVal();
3288
+
3289
+ i++;
3290
+ continue;
3291
+ }
3292
+
3293
+ /**
3294
+ * New command introduced by
3295
+ * alphabetic A-Z character
3296
+ */
3297
+ if (charCode > 64) {
3298
+
3299
+ // is valid command
3300
+ let isValid = commandSet.has(charCode);
3301
+
3302
+ if (!isValid) {
3303
+ feedback = `${itemCount}. command "${d[i]}" is not a valid type`;
3304
+ log.push(feedback);
3305
+ i++;
3306
+ continue
3307
+ }
3308
+
3309
+ // command is concatenated without whitespace
3310
+ if (val !== '') {
3311
+ pathData[itemCount].values.push(+val);
3312
+ valueIndex++;
3313
+ val = '';
3314
+ }
3315
+
3316
+ // check if previous command was correctly closed
3317
+ if (debug) validateCommand();
3318
+
3319
+ lastCommand = d[i];
3320
+ maxParams = paramCountsArr[charCode];
3321
+ let isM = lastCommand === 'M' || lastCommand === 'm';
3322
+ let wasClosePath = itemCount > 0 && (pathData[itemCount].type === 'z' || pathData[itemCount].type === 'Z');
3323
+
3324
+ foundCommands.add(lastCommand);
3325
+
3326
+ // add omitted M command after Z
3327
+ if (wasClosePath && !isM) {
3328
+ pathData.push({ type: 'm', values: [0, 0] });
3329
+ itemCount++;
3330
+ }
3331
+
3332
+ // init new command
3333
+ pathData.push({ type: lastCommand, values: [] });
3334
+ itemCount++;
3335
+
3336
+ // reset counters
3337
+ floatCount = 0;
3338
+ valueIndex = 0;
3339
+ needsNewSegment = false;
3340
+
3341
+ i++;
3342
+ continue;
3343
+ }
3344
+
3345
+ // exceptions - prevent infinite loop
3346
+ if (!isDigit) {
3347
+ feedback = `${itemCount}. ${d[i]} is not a valid separarator or token`;
3348
+ log.push(feedback);
3349
+ val = '';
3350
+ }
3351
+
3352
+ i++;
3353
+
3354
+ }
3355
+
3356
+ // final value
3357
+ pushVal();
3358
+ if (debug) validateCommand();
3359
+
3360
+ // return error log
3361
+ if (debug && log.length) {
3362
+ feedback = 'Invalid path data:\n' + log.join('\n');
3363
+ if (debug === 'log') {
3364
+ console.log(feedback);
3365
+ } else {
3366
+ throw new Error(feedback)
3367
+ }
3368
+ }
3369
+
3370
+ pathData[0].type = 'M';
3371
+
3372
+ /**
3373
+ * check if absolute/relative or
3374
+ * shorthands are present
3375
+ * to specify if normalization is required
3376
+ */
3377
+
3378
+ let commands = Array.from(foundCommands).join('');
3379
+ let hasRelatives = /[lcqamts]/g.test(commands);
3380
+ let hasShorthands = /[vhst]/gi.test(commands);
3381
+ let hasArcs = /[a]/gi.test(commands);
3382
+ let hasQuadratics = /[qt]/gi.test(commands);
3383
+
3384
+ return {
3385
+ pathData,
3386
+ hasRelatives,
3387
+ hasShorthands,
3388
+ hasQuadratics,
3389
+ hasArcs
3390
+ }
3391
+
3392
+ }
3393
+
3394
+ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3395
+
3396
+ let pathDataN = [pathData[0]];
3397
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3398
+ let p0 = M;
3399
+ let p = M;
3400
+ pathData[pathData.length - 1].type.toLowerCase() === 'z';
3401
+
3402
+ for (let c = 1, l = pathData.length; c < l; c++) {
3403
+ let comPrev = pathData[c - 1];
3404
+ let com = pathData[c];
3405
+ let comN = pathData[c + 1] || pathData[l - 1];
3406
+ let p1 = comN.type === 'Z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3407
+
3408
+ let { type, values } = com;
3409
+ let valsL = values.slice(-2);
3410
+ p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
3411
+
3412
+ let cpts = type === 'C' ?
3413
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3414
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3415
+
3416
+ let area = getPolygonArea([p0, ...cpts, p, p1], true);
3417
+ let distSquare = getSquareDistance(p0, p);
3418
+ let distMax = distSquare / 500 * tolerance;
3419
+
3420
+ let isFlat = area < distMax;
3421
+
3422
+ if(!flatBezierToLinetos && type==='C') isFlat = false;
3423
+
3424
+ // convert flat beziers to linetos
3425
+ if (flatBezierToLinetos && type === 'C') {
3426
+
3427
+ let areaBez = getPolygonArea([p0, ...cpts, p], true);
3428
+ let isFlatBez = areaBez < distSquare / 1000;
3429
+
3430
+ if (isFlatBez && comPrev.type !== 'C') {
3431
+ com.type = "L";
3432
+ com.values = valsL;
3433
+ }
3434
+
3435
+ }
3436
+
3437
+ // update end point
3438
+ p0 = p;
3439
+
3440
+ // colinear – exclude arcs (as always =) as semicircles won't have an area
3441
+ if (type !== 'A' && isFlat && c < l - 1) {
3442
+ continue;
3443
+ }
3444
+
3445
+ if (type === 'M') {
3446
+ M = p;
3447
+ p0 = M;
3448
+ }
3449
+
3450
+ else if (type === 'Z') {
3451
+ p0 = M;
3452
+ }
3453
+
3454
+ // proceed and add command
3455
+ pathDataN.push(com);
3456
+
3457
+ }
3458
+
3459
+ return pathDataN;
3460
+
3461
+ }
3462
+
3463
+ function removeZeroLengthLinetos(pathData) {
3464
+
3465
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3466
+ let p0 = M;
3467
+ let p = p0;
3468
+
3469
+ let pathDataN = [pathData[0]];
3470
+
3471
+ for (let c = 1, l = pathData.length; c < l; c++) {
3472
+ let com = pathData[c];
3473
+ let { type, values } = com;
3474
+
3475
+ let valsL = values.slice(-2);
3476
+ p = { x: valsL[0], y: valsL[1] };
3477
+
3478
+ // skip lineto
3479
+ if (type === 'L' && p.x === p0.x && p.y === p0.y) {
3480
+ continue
3481
+ }
3482
+
3483
+ pathDataN.push(com);
3484
+ p0 = p;
3485
+ }
3486
+
3487
+ return pathDataN
3488
+
3489
+ }
3490
+
3491
+ function pathDataToTopLeft(pathData, removeFinalLineto = false, reorder = true) {
3492
+
3493
+ let pathDataNew = [];
3494
+ let len = pathData.length;
3495
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3496
+ let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3497
+
3498
+ let linetos = pathData.filter(com => com.type === 'L');
3499
+
3500
+ // check if order is ideal
3501
+ let penultimateCom = pathData[len - 2];
3502
+ let penultimateType = penultimateCom.type;
3503
+ let penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
3504
+
3505
+ // last L command ends at M
3506
+ let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3507
+
3508
+ // if last segment is not closing or a lineto
3509
+ let skipReorder = pathData[1].type!=='L' && (!isClosingCommand || penultimateType==='L' );
3510
+ skipReorder=false;
3511
+
3512
+ // we can't change starting point for non closed paths
3513
+ if (!isClosed ) {
3514
+ return pathData
3515
+ }
3516
+
3517
+ let newIndex = 0;
3518
+
3519
+ if (!skipReorder) {
3520
+
3521
+ let indices = [];
3522
+ for (let i = 0, len = pathData.length; i < len; i++) {
3523
+ let com = pathData[i];
3524
+ let { type, values } = com;
3525
+ if (values.length) {
3526
+ let valsL = values.slice(-2);
3527
+ let prevL = pathData[i - 1] && pathData[i - 1].type === 'L';
3528
+ let nextL = pathData[i + 1] && pathData[i + 1].type === 'L';
3529
+ let prevCom = pathData[i - 1] ? pathData[i - 1].type.toUpperCase() : null;
3530
+ let nextCom = pathData[i + 1] ? pathData[i + 1].type.toUpperCase() : null;
3531
+ let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevL, nextL, prevCom, nextCom };
3532
+ p.index = i;
3533
+ indices.push(p);
3534
+ }
3535
+ }
3536
+
3537
+ // find top most lineto
3538
+
3539
+ if (linetos.length) {
3540
+ let curveAfterLine = indices.filter(com => (com.type !== 'L' && com.type !== 'M') && com.prevCom &&
3541
+ com.prevCom === 'L' || com.prevCom==='M' && penultimateType==='L' ).sort((a, b) => a.y - b.y || a.x-b.x)[0];
3542
+
3543
+ newIndex = curveAfterLine ? curveAfterLine.index - 1 : 0;
3544
+
3545
+ }
3546
+ // use top most command
3547
+ else {
3548
+ indices = indices.sort((a, b) => +a.y.toFixed(1) - +b.y.toFixed(1) || a.x - b.x );
3549
+ newIndex = indices[0].index;
3550
+ }
3551
+
3552
+ // reorder
3553
+ pathData = newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3554
+ }
3555
+
3556
+ len = pathData.length;
3557
+
3558
+ // remove last lineto
3559
+ penultimateCom = pathData[len - 2];
3560
+ penultimateType = penultimateCom.type;
3561
+ penultimateComCoords = penultimateCom.values.slice(-2);
3562
+
3563
+ isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
3564
+
3565
+ if (removeFinalLineto && isClosingCommand) {
3566
+ pathData.splice(len - 2, 1);
3567
+ }
3568
+
3569
+ pathDataNew.push(...pathData);
3570
+
3571
+ return pathDataNew
3572
+ }
3573
+
3574
+ /**
3575
+ * shift starting point
3576
+ */
3577
+ function shiftSvgStartingPoint(pathData, offset) {
3578
+ let pathDataL = pathData.length;
3579
+ let newStartIndex = 0;
3580
+ let lastCommand = pathData[pathDataL - 1]["type"];
3581
+ let isClosed = lastCommand.toLowerCase() === "z";
3582
+
3583
+ if (!isClosed || offset < 1 || pathData.length < 3) {
3584
+ return pathData;
3585
+ }
3586
+
3587
+ let trimRight = isClosed ? 1 : 0;
3588
+
3589
+ // add explicit lineto
3590
+ addClosePathLineto(pathData);
3591
+
3592
+ // M start offset
3593
+ newStartIndex =
3594
+ offset + 1 < pathData.length - 1
3595
+ ? offset + 1
3596
+ : pathData.length - 1 - trimRight;
3597
+
3598
+ // slice array to reorder
3599
+ let pathDataStart = pathData.slice(newStartIndex);
3600
+ let pathDataEnd = pathData.slice(0, newStartIndex);
3601
+
3602
+ // remove original M
3603
+ pathDataEnd.shift();
3604
+ let pathDataEndL = pathDataEnd.length;
3605
+
3606
+ let pathDataEndLastValues, pathDataEndLastXY;
3607
+ pathDataEndLastValues = pathDataEnd[pathDataEndL - 1].values || [];
3608
+ pathDataEndLastXY = [
3609
+ pathDataEndLastValues[pathDataEndLastValues.length - 2],
3610
+ pathDataEndLastValues[pathDataEndLastValues.length - 1]
3611
+ ];
3612
+
3613
+ if (trimRight) {
3614
+ pathDataStart.pop();
3615
+ pathDataEnd.push({
3616
+ type: "Z",
3617
+ values: []
3618
+ });
3619
+ }
3620
+ // prepend new M command and concatenate array chunks
3621
+ pathData = [
3622
+ {
3623
+ type: "M",
3624
+ values: pathDataEndLastXY
3625
+ },
3626
+ ...pathDataStart,
3627
+ ...pathDataEnd,
3628
+ ];
3629
+
3630
+ return pathData;
3631
+ }
3632
+
3633
+ /**
3634
+ * Add closing lineto:
3635
+ * needed for path reversing or adding points
3636
+ */
3637
+
3638
+ function addClosePathLineto(pathData) {
3639
+ let pathDataL = pathData.length;
3640
+ let closed = pathData[pathDataL - 1].type.toLowerCase() === "z" ? true : false;
3641
+
3642
+ let M = pathData[0];
3643
+ let [x0, y0] = [M.values[0], M.values[1]].map(val => { return +val.toFixed(8) });
3644
+ let comLast = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
3645
+ let comLastL = comLast.values.length;
3646
+
3647
+ // last explicit on-path coordinates
3648
+ let [xL, yL] = [comLast.values[comLastL - 2], comLast.values[comLastL - 1]].map(val => { return +val.toFixed(8) });
3649
+
3650
+ if (closed && (x0 != xL || y0 != yL)) {
3651
+
3652
+ pathData.pop();
3653
+ pathData.push(
3654
+ {
3655
+ type: "L",
3656
+ values: [x0, y0]
3657
+ },
3658
+ {
3659
+ type: "Z",
3660
+ values: []
3661
+ }
3662
+ );
3663
+ }
3664
+
3665
+ return pathData;
3666
+ }
3667
+
3668
+ /**
3669
+ * reverse pathdata
3670
+ * make sure all command coordinates are absolute and
3671
+ * shorthands are converted to long notation
3672
+ */
3673
+ function reversePathData(pathData, {
3674
+ arcToCubic = false,
3675
+ quadraticToCubic = false,
3676
+ toClockwise = false,
3677
+ returnD = false
3678
+ } = {}) {
3679
+
3680
+ /**
3681
+ * Add closing lineto:
3682
+ * needed for path reversing or adding points
3683
+ */
3684
+ const addClosePathLineto = (pathData) => {
3685
+ let closed = pathData[pathData.length - 1].type.toLowerCase() === "z";
3686
+ let M = pathData[0];
3687
+ let [x0, y0] = [M.values[0], M.values[1]];
3688
+ let lastCom = closed ? pathData[pathData.length - 2] : pathData[pathData.length - 1];
3689
+ let [xE, yE] = [lastCom.values[lastCom.values.length - 2], lastCom.values[lastCom.values.length - 1]];
3690
+
3691
+ if (closed && (x0 != xE || y0 != yE)) {
3692
+
3693
+ pathData.pop();
3694
+ pathData.push(
3695
+ {
3696
+ type: "L",
3697
+ values: [x0, y0]
3698
+ },
3699
+ {
3700
+ type: "Z",
3701
+ values: []
3702
+ }
3703
+ );
3704
+ }
3705
+ return pathData;
3706
+ };
3707
+
3708
+ // helper to rearrange control points for all command types
3709
+ const reverseControlPoints = (type, values) => {
3710
+ let controlPoints = [];
3711
+ let endPoints = [];
3712
+ if (type !== "A") {
3713
+ for (let p = 0; p < values.length; p += 2) {
3714
+ controlPoints.push([values[p], values[p + 1]]);
3715
+ }
3716
+ endPoints = controlPoints.pop();
3717
+ controlPoints.reverse();
3718
+ }
3719
+ // is arc
3720
+ else {
3721
+
3722
+ let sweep = values[4] == 0 ? 1 : 0;
3723
+ controlPoints = [values[0], values[1], values[2], values[3], sweep];
3724
+ endPoints = [values[5], values[6]];
3725
+ }
3726
+ return { controlPoints, endPoints };
3727
+ };
3728
+
3729
+ // start compiling new path data
3730
+ let pathDataNew = [];
3731
+
3732
+ let closed =
3733
+ pathData[pathData.length - 1].type.toLowerCase() === "z" ? true : false;
3734
+ if (closed) {
3735
+ // add lineto closing space between Z and M
3736
+ pathData = addClosePathLineto(pathData);
3737
+ // remove Z closepath
3738
+ pathData.pop();
3739
+ }
3740
+
3741
+ // define last point as new M if path isn't closed
3742
+ let valuesLast = pathData[pathData.length - 1].values;
3743
+ let valuesLastL = valuesLast.length;
3744
+ let M = closed
3745
+ ? pathData[0]
3746
+ : {
3747
+ type: "M",
3748
+ values: [valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]]
3749
+ };
3750
+ // starting M stays the same – unless the path is not closed
3751
+ pathDataNew.push(M);
3752
+
3753
+ // reverse path data command order for processing
3754
+ pathData.reverse();
3755
+ for (let i = 1; i < pathData.length; i++) {
3756
+ let com = pathData[i];
3757
+ let type = com.type;
3758
+ let values = com.values;
3759
+ let comPrev = pathData[i - 1];
3760
+ let typePrev = comPrev.type;
3761
+ let valuesPrev = comPrev.values;
3762
+
3763
+ // get reversed control points and new end coordinates
3764
+ let controlPointsPrev = reverseControlPoints(typePrev, valuesPrev).controlPoints;
3765
+ let endPoints = reverseControlPoints(type, values).endPoints;
3766
+
3767
+ // create new path data
3768
+ let newValues = [];
3769
+ newValues = [controlPointsPrev, endPoints].flat();
3770
+ pathDataNew.push({
3771
+ type: typePrev,
3772
+ values: newValues.flat()
3773
+ });
3774
+ }
3775
+
3776
+ // add previously removed Z close path
3777
+ if (closed) {
3778
+ pathDataNew.push({
3779
+ type: "z",
3780
+ values: []
3781
+ });
3782
+ }
3783
+
3784
+ return pathDataNew;
3785
+ }
3786
+
3787
+ function svgPathSimplify(d = '', {
3788
+ toAbsolute = true,
3789
+ toRelative = true,
3790
+ toShorthands = true,
3791
+ decimals = 3,
3792
+
3793
+ // not necessary unless you need cubics only
3794
+ quadraticToCubic = true,
3795
+
3796
+ // mostly a fallback if arc calculations fail
3797
+ arcToCubic = false,
3798
+ cubicToArc = false,
3799
+
3800
+ // arc to cubic precision - adds more segments for better precision
3801
+ arcAccuracy = 4,
3802
+ keepExtremes = true,
3803
+ keepCorners = true,
3804
+ keepInflections = true,
3805
+ extrapolateDominant = false,
3806
+ addExtremes = false,
3807
+ optimizeOrder = true,
3808
+ removeColinear = true,
3809
+ simplifyBezier = true,
3810
+ autoAccuracy = true,
3811
+ flatBezierToLinetos = true,
3812
+ revertToQuadratics = true,
3813
+ minifyD = 0,
3814
+ tolerance = 1,
3815
+ reverse = false
3816
+ } = {}) {
3817
+
3818
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
3819
+
3820
+ // create clone for fallback
3821
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
3822
+
3823
+ // count commands for evaluation
3824
+ let comCount = pathDataO.length;
3825
+
3826
+ /**
3827
+ * get sub paths
3828
+ */
3829
+ let subPathArr = splitSubpaths(pathData);
3830
+
3831
+ // cleaned up pathData
3832
+ let pathDataArrN = [];
3833
+
3834
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
3835
+
3836
+ let pathDataSub = subPathArr[i];
3837
+
3838
+ // try simplification in reversed order
3839
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
3840
+
3841
+ // remove zero length linetos
3842
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
3843
+
3844
+ // add extremes
3845
+
3846
+ let tMin = 0, tMax = 1;
3847
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
3848
+
3849
+ // sort to top left
3850
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
3851
+
3852
+ // remove colinear/flat
3853
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
3854
+
3855
+ // analyze pathdata to add info about signicant properties such as extremes, corners
3856
+ let pathDataPlus = analyzePathData(pathDataSub);
3857
+
3858
+ // simplify beziers
3859
+ let { pathData, bb, dimA } = pathDataPlus;
3860
+
3861
+ let pathDataN = pathData;
3862
+
3863
+
3864
+ pathDataN = simplifyBezier ? simplifyPathData(pathDataN, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathDataN;
3865
+
3866
+ // cubic to arcs
3867
+ if(cubicToArc){
3868
+
3869
+ let thresh = 3;
3870
+
3871
+ pathDataN.forEach((com, c) => {
3872
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
3873
+ if (type === 'C') {
3874
+
3875
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
3876
+ if(comA.isArc) pathDataN[c] = comA.com;
3877
+
3878
+ }
3879
+ });
3880
+
3881
+ // combine adjacent cubics
3882
+ pathDataN = combineArcs(pathDataN);
3883
+
3884
+ }
3885
+
3886
+ // simplify to quadratics
3887
+ if (revertToQuadratics) {
3888
+ pathDataN.forEach((com, c) => {
3889
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
3890
+ if (type === 'C') {
3891
+
3892
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
3893
+ if (comQ.type === 'Q') pathDataN[c] = comQ;
3894
+ }
3895
+ });
3896
+ }
3897
+
3898
+ // update
3899
+ pathDataArrN.push(pathDataN);
3900
+ }
3901
+
3902
+ // merge pathdata
3903
+ let pathDataFlat = pathDataArrN.flat();
3904
+
3905
+ /**
3906
+ * detect accuracy
3907
+ */
3908
+ if (autoAccuracy) {
3909
+ decimals = detectAccuracy(pathDataFlat);
3910
+ }
3911
+
3912
+ // compare command count
3913
+ let comCountS = pathDataFlat.length;
3914
+
3915
+ // optimize
3916
+ let pathOptions = {
3917
+ toRelative,
3918
+ toShorthands,
3919
+ decimals,
3920
+ };
3921
+
3922
+ // optimize path data
3923
+ pathData = convertPathData(pathDataFlat, pathOptions);
3924
+ let dOpt = pathDataToD(pathData, minifyD);
3925
+
3926
+ let report = {
3927
+ original: comCount,
3928
+ new: comCountS,
3929
+ saved: comCount - comCountS,
3930
+ decimals,
3931
+ success: comCountS < comCount
3932
+ };
3933
+
3934
+ return { pathData, d: dOpt, report };
3935
+
3936
+ }
3937
+
3938
+ function simplifyPathData(pathData, {
3939
+ keepExtremes = true,
3940
+ keepInflections = true,
3941
+ keepCorners = true,
3942
+ extrapolateDominant = true,
3943
+ tolerance = 1,
3944
+ reverse = false
3945
+ } = {}) {
3946
+
3947
+ let pathDataN = [pathData[0]];
3948
+
3949
+ for (let i = 2, l = pathData.length; l && i <= l; i++) {
3950
+ let com = pathData[i - 1];
3951
+ let comN = i < l ? pathData[i] : null;
3952
+ let typeN = comN?.type || null;
3953
+
3954
+ let isDirChange = com?.directionChange || null;
3955
+ let isDirChangeN = comN?.directionChange || null;
3956
+
3957
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
3958
+
3959
+ // next is also cubic
3960
+ if (type === 'C' && typeN === 'C') {
3961
+
3962
+ // cannot be combined as crossing extremes or corners
3963
+ if (
3964
+ (keepInflections && isDirChangeN) ||
3965
+ (keepCorners && corner) ||
3966
+ (!isDirChange && keepExtremes && extreme)
3967
+ ) {
3968
+
3969
+ pathDataN.push(com);
3970
+ }
3971
+
3972
+ // try simplification
3973
+ else {
3974
+
3975
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
3976
+ let error = 0;
3977
+
3978
+ // combining successful! try next segment
3979
+ if (combined.length === 1) {
3980
+ com = combined[0];
3981
+ let offset = 1;
3982
+ error += com.error;
3983
+
3984
+ // find next candidates
3985
+ for (let n = i + 1; error < tolerance && n < l; n++) {
3986
+ let comN = pathData[n];
3987
+ if (comN.type !== 'C' ||
3988
+ (
3989
+ (keepInflections && comN.directionChange) ||
3990
+ (keepCorners && com.corner) ||
3991
+ (keepExtremes && com.extreme)
3992
+ )
3993
+ ) {
3994
+ break
3995
+ }
3996
+
3997
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
3998
+ if (combined.length === 1) {
3999
+ offset++;
4000
+ }
4001
+ com = combined[0];
4002
+ }
4003
+
4004
+ pathDataN.push(com);
4005
+
4006
+ if (i < l) {
4007
+ i += offset;
4008
+ }
4009
+
4010
+ } else {
4011
+ pathDataN.push(com);
4012
+ }
4013
+ }
4014
+
4015
+ } // end of bezier command
4016
+
4017
+ // other commands
4018
+ else {
4019
+ pathDataN.push(com);
4020
+ }
4021
+
4022
+ } // end command loop
4023
+
4024
+ // reverse back
4025
+ if (reverse) pathDataN = reversePathData(pathDataN);
4026
+
4027
+ return pathDataN
4028
+ }
4029
+
4030
+ const {
4031
+ abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4032
+ log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4033
+ } = Math;
4034
+
4035
+ // just for visual debugging
4036
+
4037
+ // IIFE
4038
+ if (typeof window !== 'undefined') {
4039
+ window.svgPathSimplify = svgPathSimplify;
4040
+ window.renderPoint = renderPoint;
4041
+ }
4042
+
4043
+ exports.PI = PI;
4044
+ exports.abs = abs;
4045
+ exports.acos = acos;
4046
+ exports.asin = asin;
4047
+ exports.atan = atan;
4048
+ exports.atan2 = atan2;
4049
+ exports.ceil = ceil;
4050
+ exports.cos = cos;
4051
+ exports.exp = exp;
4052
+ exports.floor = floor;
4053
+ exports.hypot = hypot;
4054
+ exports.log = log;
4055
+ exports.max = max;
4056
+ exports.min = min;
4057
+ exports.pow = pow;
4058
+ exports.random = random;
4059
+ exports.round = round;
4060
+ exports.sin = sin;
4061
+ exports.sqrt = sqrt;
4062
+ exports.svgPathSimplify = svgPathSimplify;
4063
+ exports.tan = tan;
4064
+
4065
+ })(this["svg-path-simplify"] = this["svg-path-simplify"] || {});