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