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