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,1180 @@
1
+
2
+ /*
3
+ import { getPathDataVertices, getPointOnEllipse, pointAtT, checkLineIntersection, getDistance, interpolate, getAngle } from './geometry.js';
4
+
5
+ import { splitSubpaths } from "./convert_segments";
6
+
7
+
8
+ import { getPolygonArea, getPathArea, getRelativeAreaDiff } from './geometry_area.js';
9
+ import { splitSubpaths } from './pathData_split.js';
10
+ import { getPolyBBox} from './geometry_bbox.js';
11
+ import { renderPoint, renderPath } from "./visualize";
12
+ */
13
+
14
+
15
+ import { checkLineIntersection, getAngle, getDistance, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint } from './geometry';
16
+ import { getPathArea, getPolygonArea, getRelativeAreaDiff } from './geometry_area';
17
+ import { roundPathData } from './rounding';
18
+ import { renderPoint } from './visualize';
19
+
20
+ export function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
21
+
22
+ // test if cubic can be simplified to quadratic
23
+ let cp1X = interpolate(p0, cp1, 1.5)
24
+ let cp2X = interpolate(p, cp2, 1.5)
25
+
26
+ let dist0 = getDistAv(p0, p)
27
+ let threshold = dist0 * 0.01;
28
+ let dist1 = getDistAv(cp1X, cp2X)
29
+
30
+ let cp1_Q = null;
31
+ let type = 'C'
32
+ let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
33
+
34
+ if (dist1 < threshold) {
35
+ cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
36
+ if (cp1_Q) {
37
+ //renderPoint(markers, cp1_Q )
38
+ type = 'Q'
39
+ values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
40
+ }
41
+ }
42
+
43
+ return { type, values }
44
+
45
+ }
46
+
47
+
48
+ export function convertPathData(pathData, {
49
+ toShorthands = true,
50
+ toRelative = true,
51
+ decimals = 3
52
+ } = {}) {
53
+
54
+ //if(decimals>-1 && decimals<2) pathData = roundPathData(pathData, decimals);
55
+ if (toShorthands) pathData = pathDataToShorthands(pathData);
56
+
57
+ // pre round - before relative conversion to minimize distortions
58
+ pathData = roundPathData(pathData, decimals);
59
+ if (toRelative) pathData = pathDataToRelative(pathData);
60
+ if (decimals > -1) pathData = roundPathData(pathData, decimals);
61
+ return pathData
62
+ }
63
+
64
+
65
+ /**
66
+ * convert cubic circle approximations
67
+ * to more compact arcs
68
+ */
69
+
70
+ export function pathDataArcsToCubics(pathData, {
71
+ arcAccuracy = 1
72
+ } = {}) {
73
+
74
+ let pathDataCubic = [pathData[0]];
75
+ for (let i = 1, len = pathData.length; i < len; i++) {
76
+
77
+ let com = pathData[i];
78
+ let comPrev = pathData[i - 1];
79
+ let valuesPrev = comPrev.values;
80
+ let valuesPrevL = valuesPrev.length;
81
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
82
+
83
+ //convert arcs to cubics
84
+ if (com.type === 'A') {
85
+ // add all C commands instead of Arc
86
+ let cubicArcs = arcToBezier(p0, com.values, arcAccuracy);
87
+ cubicArcs.forEach((cubicArc) => {
88
+ pathDataCubic.push(cubicArc);
89
+ });
90
+ }
91
+
92
+ else {
93
+ // add command
94
+ pathDataCubic.push(com)
95
+ }
96
+ }
97
+
98
+ return pathDataCubic
99
+
100
+ }
101
+
102
+
103
+ export function pathDataQuadraticToCubic(pathData) {
104
+
105
+ let pathDataQuadratic = [pathData[0]];
106
+ for (let i = 1, len = pathData.length; i < len; i++) {
107
+
108
+ let com = pathData[i];
109
+ let comPrev = pathData[i - 1];
110
+ let valuesPrev = comPrev.values;
111
+ let valuesPrevL = valuesPrev.length;
112
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
113
+
114
+ //convert quadratic to cubics
115
+ if (com.type === 'Q') {
116
+ pathDataQuadratic.push(quadratic2Cubic(p0, com.values))
117
+ }
118
+
119
+ else {
120
+ // add command
121
+ pathDataQuadratic.push(com)
122
+ }
123
+ }
124
+
125
+ return pathDataQuadratic
126
+ }
127
+
128
+
129
+
130
+ /**
131
+ * convert quadratic commands to cubic
132
+ */
133
+ export function quadratic2Cubic(p0, values) {
134
+ if (Array.isArray(p0)) {
135
+ p0 = {
136
+ x: p0[0],
137
+ y: p0[1]
138
+ }
139
+ }
140
+ let cp1 = {
141
+ x: p0.x + 2 / 3 * (values[0] - p0.x),
142
+ y: p0.y + 2 / 3 * (values[1] - p0.y)
143
+ }
144
+ let cp2 = {
145
+ x: values[2] + 2 / 3 * (values[0] - values[2]),
146
+ y: values[3] + 2 / 3 * (values[1] - values[3])
147
+ }
148
+ return ({ type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, values[2], values[3]] });
149
+ }
150
+
151
+
152
+ /**
153
+ * convert pathData to
154
+ * This is just a port of Dmitry Baranovskiy's
155
+ * pathToRelative/Absolute methods used in snap.svg
156
+ * https://github.com/adobe-webplatform/Snap.svg/
157
+ */
158
+
159
+
160
+ export function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decimals = -1) {
161
+ if (decimals >= 0) {
162
+ pathData[0].values = pathData[0].values.map(val => +val.toFixed(decimals));
163
+ }
164
+
165
+ let M = pathData[0].values;
166
+ let x = M[0],
167
+ y = M[1],
168
+ mx = x,
169
+ my = y;
170
+
171
+ for (let i = 1, len = pathData.length; i < len; i++) {
172
+ let com = pathData[i];
173
+ let { type, values } = com;
174
+ let newType = toRelative ? type.toLowerCase() : type.toUpperCase();
175
+
176
+ if (type !== newType) {
177
+ type = newType;
178
+ com.type = type;
179
+
180
+ switch (type) {
181
+ case "a":
182
+ case "A":
183
+ values[5] = toRelative ? values[5] - x : values[5] + x;
184
+ values[6] = toRelative ? values[6] - y : values[6] + y;
185
+ break;
186
+ case "v":
187
+ case "V":
188
+ values[0] = toRelative ? values[0] - y : values[0] + y;
189
+ break;
190
+ case "h":
191
+ case "H":
192
+ values[0] = toRelative ? values[0] - x : values[0] + x;
193
+ break;
194
+ case "m":
195
+ case "M":
196
+ if (toRelative) {
197
+ values[0] -= x;
198
+ values[1] -= y;
199
+ } else {
200
+ values[0] += x;
201
+ values[1] += y;
202
+ }
203
+ mx = toRelative ? values[0] + x : values[0];
204
+ my = toRelative ? values[1] + y : values[1];
205
+ break;
206
+ default:
207
+ if (values.length) {
208
+ for (let v = 0; v < values.length; v++) {
209
+ values[v] = toRelative
210
+ ? values[v] - (v % 2 ? y : x)
211
+ : values[v] + (v % 2 ? y : x);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ let vLen = values.length;
218
+ switch (type) {
219
+ case "z":
220
+ case "Z":
221
+ x = mx;
222
+ y = my;
223
+ break;
224
+ case "h":
225
+ case "H":
226
+ x = toRelative ? x + values[0] : values[0];
227
+ break;
228
+ case "v":
229
+ case "V":
230
+ y = toRelative ? y + values[0] : values[0];
231
+ break;
232
+ case "m":
233
+ case "M":
234
+ mx = values[vLen - 2] + (toRelative ? x : 0);
235
+ my = values[vLen - 1] + (toRelative ? y : 0);
236
+ default:
237
+ x = values[vLen - 2] + (toRelative ? x : 0);
238
+ y = values[vLen - 1] + (toRelative ? y : 0);
239
+ }
240
+
241
+ if (decimals >= 0) {
242
+ com.values = com.values.map(val => +val.toFixed(decimals));
243
+ }
244
+ }
245
+ return pathData;
246
+ }
247
+
248
+
249
+ export function pathDataToRelative(pathData, decimals = -1) {
250
+ return pathDataToAbsoluteOrRelative(pathData, true, decimals)
251
+ }
252
+
253
+ export function pathDataToAbsolute(pathData, decimals = -1) {
254
+ return pathDataToAbsoluteOrRelative(pathData, false, decimals)
255
+ }
256
+
257
+
258
+ /**
259
+ * decompose/convert shorthands to "longhand" commands:
260
+ * H, V, S, T => L, L, C, Q
261
+ * reversed method: pathDataToShorthands()
262
+ */
263
+
264
+ export function pathDataToLonghands(pathData, decimals = -1, test = true) {
265
+
266
+ // analyze pathdata – if you're sure your data is already absolute skip it via test=false
267
+ let hasRel = false;
268
+
269
+ if (test) {
270
+ let commandTokens = pathData.map(com => { return com.type }).join('')
271
+ let hasShorthands = /[hstv]/gi.test(commandTokens);
272
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
273
+ //console.log('test', hasRel, hasShorthands);
274
+
275
+ if (!hasShorthands) {
276
+ return pathData;
277
+ }
278
+ }
279
+
280
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
281
+
282
+ let pathDataLonghand = [];
283
+ let comPrev = {
284
+ type: "M",
285
+ values: pathData[0].values
286
+ };
287
+ pathDataLonghand.push(comPrev);
288
+
289
+ for (let i = 1, len = pathData.length; i < len; i++) {
290
+ let com = pathData[i];
291
+ let { type, values } = com;
292
+ let valuesL = values.length;
293
+ let valuesPrev = comPrev.values;
294
+ let valuesPrevL = valuesPrev.length;
295
+ let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
296
+ let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
297
+ let [prevX, prevY] = [
298
+ valuesPrev[valuesPrevL - 2],
299
+ valuesPrev[valuesPrevL - 1]
300
+ ];
301
+ switch (type) {
302
+ case "H":
303
+ comPrev = {
304
+ type: "L",
305
+ values: [values[0], prevY]
306
+ };
307
+ break;
308
+ case "V":
309
+ comPrev = {
310
+ type: "L",
311
+ values: [prevX, values[0]]
312
+ };
313
+ break;
314
+ case "T":
315
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
316
+ [prevX, prevY] = [
317
+ valuesPrev[valuesPrevL - 2],
318
+ valuesPrev[valuesPrevL - 1]
319
+ ];
320
+ // new control point
321
+ cpN1X = prevX + (prevX - cp1X);
322
+ cpN1Y = prevY + (prevY - cp1Y);
323
+ comPrev = {
324
+ type: "Q",
325
+ values: [cpN1X, cpN1Y, x, y]
326
+ };
327
+ break;
328
+ case "S":
329
+
330
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
331
+ [prevX, prevY] = [
332
+ valuesPrev[valuesPrevL - 2],
333
+ valuesPrev[valuesPrevL - 1]
334
+ ];
335
+
336
+ [cp2X, cp2Y] =
337
+ valuesPrevL > 2 && comPrev.type !== 'A' ?
338
+ [valuesPrev[2], valuesPrev[3]] :
339
+ [prevX, prevY];
340
+
341
+ // new control points
342
+ cpN1X = 2 * prevX - cp2X;
343
+ cpN1Y = 2 * prevY - cp2Y;
344
+ cpN2X = values[0];
345
+ cpN2Y = values[1];
346
+ comPrev = {
347
+ type: "C",
348
+ values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
349
+ };
350
+
351
+ break;
352
+ default:
353
+ comPrev = {
354
+ type: type,
355
+ values: values
356
+ };
357
+ }
358
+ // round final longhand values
359
+ if (decimals > -1) {
360
+ comPrev.values = comPrev.values.map(val => { return +val.toFixed(decimals) })
361
+ }
362
+
363
+ pathDataLonghand.push(comPrev);
364
+ }
365
+ return pathDataLonghand;
366
+ }
367
+
368
+ /**
369
+ * apply shorthand commands if possible
370
+ * L, L, C, Q => H, V, S, T
371
+ * reversed method: pathDataToLonghands()
372
+ */
373
+ export function pathDataToShorthands(pathData, decimals = -1, test = true) {
374
+
375
+ //pathData = JSON.parse(JSON.stringify(pathData))
376
+ //console.log('has dec', pathData);
377
+
378
+ /**
379
+ * analyze pathdata – if you're sure your data is already absolute skip it via test=false
380
+ */
381
+ let hasRel
382
+ if (test) {
383
+ let commandTokens = pathData.map(com => { return com.type }).join('')
384
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
385
+ }
386
+
387
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
388
+
389
+ let comShort = {
390
+ type: "M",
391
+ values: pathData[0].values
392
+ };
393
+
394
+ if (pathData[0].decimals) {
395
+ //console.log('has dec');
396
+ comShort.decimals = pathData[0].decimals
397
+ }
398
+
399
+ let pathDataShorts = [comShort];
400
+
401
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
402
+ let p;
403
+ let tolerance = 0.01
404
+
405
+ for (let i = 1, len = pathData.length; i < len; i++) {
406
+
407
+ let com = pathData[i];
408
+ let { type, values } = com;
409
+ let valuesLast = values.slice(-2);
410
+
411
+ // previoius command
412
+ let comPrev = pathData[i - 1];
413
+ let typePrev = comPrev.type
414
+
415
+ //last on-path point
416
+ p = { x: valuesLast[0], y: valuesLast[1] };
417
+
418
+ // first bezier control point for S/T shorthand tests
419
+ let cp1 = { x: values[0], y: values[1] };
420
+
421
+
422
+ //calculate threshold based on command dimensions
423
+ let w = Math.abs(p.x - p0.x)
424
+ let h = Math.abs(p.y - p0.y)
425
+ let thresh = (w + h) / 2 * tolerance
426
+
427
+ let diffX, diffY, diff, cp1_reflected;
428
+
429
+
430
+ switch (type) {
431
+ case "L":
432
+
433
+ if (h === 0 || (h < thresh && w > thresh)) {
434
+ //console.log('is H');
435
+ comShort = {
436
+ type: "H",
437
+ values: [values[0]]
438
+ };
439
+ }
440
+
441
+ // V
442
+ else if (w === 0 || (h > thresh && w < thresh)) {
443
+ //console.log('is V', w, h);
444
+ comShort = {
445
+ type: "V",
446
+ values: [values[1]]
447
+ };
448
+ } else {
449
+ //console.log('not', type, h, w, thresh, com);
450
+ comShort = com;
451
+ }
452
+
453
+ break;
454
+
455
+ case "Q":
456
+
457
+ // skip test
458
+ if (typePrev !== 'Q') {
459
+ //console.log('skip T:', type, typePrev);
460
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
461
+ pathDataShorts.push(com);
462
+ continue;
463
+ }
464
+
465
+ let cp1_prev = { x: comPrev.values[0], y: comPrev.values[1] };
466
+ // reflected Q control points
467
+ cp1_reflected = { x: (2 * p0.x - cp1_prev.x), y: (2 * p0.y - cp1_prev.y) };
468
+
469
+ //let thresh = (diffX+diffY)/2
470
+ diffX = Math.abs(cp1.x - cp1_reflected.x)
471
+ diffY = Math.abs(cp1.y - cp1_reflected.y)
472
+ diff = (diffX + diffY) / 2
473
+
474
+ if (diff < thresh) {
475
+ //console.log('is T', diff, thresh);
476
+ comShort = {
477
+ type: "T",
478
+ values: [p.x, p.y]
479
+ };
480
+ } else {
481
+ comShort = com;
482
+ }
483
+
484
+ break;
485
+ case "C":
486
+
487
+ let cp2 = { x: values[2], y: values[3] };
488
+
489
+ if (typePrev !== 'C') {
490
+ //console.log('skip S', typePrev);
491
+ pathDataShorts.push(com);
492
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
493
+ continue;
494
+ }
495
+
496
+ let cp2_prev = { x: comPrev.values[2], y: comPrev.values[3] };
497
+
498
+ // reflected C control points
499
+ cp1_reflected = { x: (2 * p0.x - cp2_prev.x), y: (2 * p0.y - cp2_prev.y) };
500
+
501
+ //let thresh = (diffX+diffY)/2
502
+ diffX = Math.abs(cp1.x - cp1_reflected.x)
503
+ diffY = Math.abs(cp1.y - cp1_reflected.y)
504
+ diff = (diffX + diffY) / 2
505
+
506
+
507
+ if (diff < thresh) {
508
+ //console.log('is S');
509
+ comShort = {
510
+ type: "S",
511
+ values: [cp2.x, cp2.y, p.x, p.y]
512
+ };
513
+ } else {
514
+ comShort = com;
515
+ }
516
+ break;
517
+ default:
518
+ comShort = {
519
+ type: type,
520
+ values: values
521
+ };
522
+ }
523
+
524
+
525
+ // add decimal info
526
+ if (com.decimals || com.decimals === 0) {
527
+ comShort.decimals = com.decimals
528
+ }
529
+
530
+
531
+ // round final values
532
+ if (decimals > -1) {
533
+ comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) })
534
+ }
535
+
536
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
537
+ pathDataShorts.push(comShort);
538
+ }
539
+ return pathDataShorts;
540
+ }
541
+
542
+
543
+
544
+ /**
545
+ * based on puzrin's
546
+ * fontello/cubic2quad
547
+ * https://github.com/fontello/cubic2quad/blob/master/test/cubic2quad.js
548
+ */
549
+
550
+ export function pathDataToQuadratic(pathData, precision = 0.1) {
551
+ pathData = pathDataToLonghands(pathData)
552
+ let newPathData = [pathData[0]];
553
+ for (let i = 1, len = pathData.length; i < len; i++) {
554
+ let comPrev = pathData[i - 1];
555
+ let com = pathData[i];
556
+ let [type, values] = [com.type, com.values];
557
+ let [typePrev, valuesPrev] = [comPrev.type, comPrev.values];
558
+ let valuesPrevL = valuesPrev.length;
559
+ let [xPrev, yPrev] = [
560
+ valuesPrev[valuesPrevL - 2],
561
+ valuesPrev[valuesPrevL - 1]
562
+ ];
563
+
564
+ // convert C to Q
565
+ if (type == "C") {
566
+
567
+ let quadCommands = cubicToQuad(
568
+ xPrev,
569
+ yPrev,
570
+ values[0],
571
+ values[1],
572
+ values[2],
573
+ values[3],
574
+ values[4],
575
+ values[5],
576
+ precision
577
+ );
578
+
579
+ quadCommands.forEach(comQ => {
580
+ newPathData.push(comQ)
581
+ })
582
+
583
+
584
+ } else {
585
+ newPathData.push(com);
586
+ }
587
+ }
588
+ return newPathData;
589
+ }
590
+
591
+
592
+
593
+ /**
594
+ * convert arctocommands to cubic bezier
595
+ * based on puzrin's a2c.js
596
+ * https://github.com/fontello/svgpath/blob/master/lib/a2c.js
597
+ * returns pathData array
598
+ */
599
+
600
+ export function arcToBezier(p0, values, splitSegments = 1) {
601
+ const TAU = Math.PI * 2;
602
+ let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
603
+
604
+ if (rx === 0 || ry === 0) {
605
+ return []
606
+ }
607
+
608
+ let phi = rotation ? rotation * TAU / 360 : 0;
609
+ let sinphi = phi ? Math.sin(phi) : 0
610
+ let cosphi = phi ? Math.cos(phi) : 1
611
+ let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2
612
+ let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2
613
+
614
+ if (pxp === 0 && pyp === 0) {
615
+ return []
616
+ }
617
+ rx = Math.abs(rx)
618
+ ry = Math.abs(ry)
619
+ let lambda =
620
+ pxp * pxp / (rx * rx) +
621
+ pyp * pyp / (ry * ry)
622
+ if (lambda > 1) {
623
+ let lambdaRt = Math.sqrt(lambda);
624
+ rx *= lambdaRt
625
+ ry *= lambdaRt
626
+ }
627
+
628
+ /**
629
+ * parametrize arc to
630
+ * get center point start and end angles
631
+ */
632
+ let rxsq = rx * rx,
633
+ rysq = rx === ry ? rxsq : ry * ry
634
+
635
+ let pxpsq = pxp * pxp,
636
+ pypsq = pyp * pyp
637
+ let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)
638
+
639
+ if (radicant <= 0) {
640
+ radicant = 0
641
+ } else {
642
+ radicant /= (rxsq * pypsq) + (rysq * pxpsq)
643
+ radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1)
644
+ }
645
+
646
+ let centerxp = radicant ? radicant * rx / ry * pyp : 0
647
+ let centeryp = radicant ? radicant * -ry / rx * pxp : 0
648
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2
649
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2
650
+
651
+ let vx1 = (pxp - centerxp) / rx
652
+ let vy1 = (pyp - centeryp) / ry
653
+ let vx2 = (-pxp - centerxp) / rx
654
+ let vy2 = (-pyp - centeryp) / ry
655
+
656
+ // get start and end angle
657
+ const vectorAngle = (ux, uy, vx, vy) => {
658
+ let dot = +(ux * vx + uy * vy).toFixed(9)
659
+ if (dot === 1 || dot === -1) {
660
+ return dot === 1 ? 0 : Math.PI
661
+ }
662
+ dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot)
663
+ let sign = (ux * vy - uy * vx < 0) ? -1 : 1
664
+ return sign * Math.acos(dot);
665
+ }
666
+
667
+ let ang1 = vectorAngle(1, 0, vx1, vy1),
668
+ ang2 = vectorAngle(vx1, vy1, vx2, vy2)
669
+
670
+ if (sweepFlag === 0 && ang2 > 0) {
671
+ ang2 -= Math.PI * 2
672
+ }
673
+ else if (sweepFlag === 1 && ang2 < 0) {
674
+ ang2 += Math.PI * 2
675
+ }
676
+
677
+
678
+ //ratio must be at least 1
679
+ let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1
680
+
681
+
682
+ // increase segments for more accureate length calculations
683
+ let segments = ratio * splitSegments;
684
+ ang2 /= segments
685
+ let pathDataArc = [];
686
+
687
+
688
+ // If 90 degree circular arc, use a constant
689
+ // https://pomax.github.io/bezierinfo/#circles_cubic
690
+ // k=0.551784777779014
691
+ const angle90 = 1.5707963267948966;
692
+ const k = 0.551785
693
+ let a = ang2 === angle90 ? k :
694
+ (
695
+ ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
696
+ );
697
+
698
+ let cos2 = ang2 ? Math.cos(ang2) : 1;
699
+ let sin2 = ang2 ? Math.sin(ang2) : 0;
700
+ let type = 'C'
701
+
702
+ const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
703
+ let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
704
+ let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
705
+ let x2 = Math.cos(ang1 + ang2);
706
+ let y2 = Math.sin(ang1 + ang2);
707
+
708
+ return [
709
+ { x: x1 - y1 * a, y: y1 + x1 * a },
710
+ { x: x2 + y2 * a, y: y2 - x2 * a },
711
+ { x: x2, y: y2 }
712
+ ];
713
+ }
714
+
715
+ for (let i = 0; i < segments; i++) {
716
+ let com = { type: type, values: [] }
717
+ let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);
718
+
719
+ curve.forEach((pt) => {
720
+ let x = pt.x * rx
721
+ let y = pt.y * ry
722
+ com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery)
723
+ })
724
+ pathDataArc.push(com);
725
+ ang1 += ang2
726
+ }
727
+
728
+ return pathDataArc;
729
+ }
730
+
731
+
732
+ /**
733
+ * add readable command point data
734
+ * to pathData command objects
735
+ */
736
+ export function pathDataToVerbose(pathData) {
737
+
738
+ let pathDataOriginal = JSON.parse(JSON.stringify(pathData))
739
+
740
+ // normalize
741
+ pathData = pathDataToLonghands(pathDataToAbsolute(pathData));
742
+
743
+ let pathDataVerbose = [];
744
+ let pathDataL = pathData.length;
745
+ let closed = pathData[pathDataL - 1].type.toLowerCase() === 'z' ? true : false;
746
+
747
+ pathData.forEach((com, i) => {
748
+ let {
749
+ type,
750
+ values
751
+ } = com;
752
+
753
+ let comO = pathDataOriginal[i];
754
+ let typeO = comO.type;
755
+ let valuesO = comO.values;
756
+
757
+ let typeLc = typeO.toLowerCase();
758
+ let valuesL = values.length;
759
+ let isRel = typeO === typeO.toLowerCase();
760
+
761
+ let comPrev = pathData[i - 1] ? pathData[i - 1] : false;
762
+ let comPrevValues = comPrev ? comPrev.values : [];
763
+ let comPrevValuesL = comPrevValues.length;
764
+
765
+
766
+ let p0 = {
767
+ x: comPrevValues[comPrevValuesL - 2],
768
+ y: comPrevValues[comPrevValuesL - 1]
769
+ }
770
+
771
+ let p = valuesL ? {
772
+ x: values[valuesL - 2],
773
+ y: values[valuesL - 1]
774
+ } : (i === pathData.length - 1 && closed ? pathData[0].values : false);
775
+
776
+ let comObj = {
777
+ type: typeO,
778
+ values: valuesO,
779
+ valuesAbsolute: values,
780
+ pFinal: p,
781
+ isRelative: isRel
782
+ }
783
+ if (comPrevValuesL) {
784
+ comObj.pPrev = p0
785
+ }
786
+ switch (typeLc) {
787
+ case 'q':
788
+ comObj.cp1 = {
789
+ x: values[valuesL - 4],
790
+ y: values[valuesL - 3]
791
+ }
792
+ break;
793
+ case 'c':
794
+ comObj.cp1 = {
795
+ x: values[valuesL - 6],
796
+ y: values[valuesL - 5]
797
+ }
798
+ comObj.cp2 = {
799
+ x: values[valuesL - 4],
800
+ y: values[valuesL - 3]
801
+ }
802
+ break;
803
+ case 'a':
804
+
805
+ // parametrized arc rx and ry values
806
+ let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
807
+
808
+ comObj.rx = arcData.rx
809
+ comObj.ry = arcData.ry
810
+ comObj.xAxisRotation = values[2]
811
+ comObj.largeArcFlag = values[3]
812
+ comObj.sweepFlag = values[4]
813
+ comObj.startAngle = arcData.startAngle
814
+ comObj.endAngle = arcData.endAngle
815
+ comObj.deltaAngle = arcData.deltaAngle
816
+ break;
817
+ }
818
+ pathDataVerbose.push(comObj);
819
+ });
820
+ return pathDataVerbose;
821
+ }
822
+
823
+ /**
824
+ * convert pathData nested array notation
825
+ * as used in snap and other libraries
826
+ */
827
+ export function convertArrayPathData(pathDataArray) {
828
+ let pathData = [];
829
+ pathDataArray.forEach(com => {
830
+ let type = com.shift();
831
+ pathData.push({
832
+ type: type,
833
+ values: com
834
+ })
835
+ })
836
+ return pathData;
837
+ }
838
+
839
+
840
+
841
+
842
+
843
+
844
+ /**
845
+ * cubics to arcs
846
+ */
847
+
848
+ export function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
849
+
850
+ //console.log(p0, cp1, cp2, p, segArea );
851
+ let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
852
+ //let pathDataChunk = [{ type: 'M', values: [p0.x, p0.y] }, com];
853
+
854
+ let arcSegArea = 0, isArc = false
855
+
856
+ // check angles
857
+ let angle1 = getAngle(p0, cp1, true);
858
+ let angle2 = getAngle(p, cp2, true);
859
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
860
+
861
+
862
+ let angleDiff = Math.abs((deltaAngle % 180) - 90);
863
+ let isRightAngle = angleDiff < 3;
864
+
865
+
866
+ /*
867
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
868
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
869
+ //renderPoint(markers, cp1_r )
870
+
871
+ // assumed centroid
872
+ let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
873
+
874
+ let dist0 = getSquareDistance(p0, p)
875
+ let dist1 = getSquareDistance(p0, ptC)
876
+ let dist2 = getSquareDistance(p, ptC)
877
+
878
+ // let mid point
879
+ let ptM = pointAtT([p0, cp1, cp2, p], 0.5)
880
+ //let dist3 = getSquareDistance(ptM, ptC)
881
+ let diff1 = Math.abs(dist1 - dist2)
882
+
883
+
884
+ if (diff1 <= dist0 * 0.01) {
885
+
886
+ let r = Math.sqrt((dist1 + dist2) / 2)
887
+ //r = Math.sqrt((dist1 + dist2 + dist3) / 3)
888
+ //r = Math.sqrt( Math.min(dist1, dist2) )
889
+ //r=0.25
890
+ //console.log('diff1', diff1, r);
891
+
892
+ let arcArea = getPolygonArea([p0, cp1, cp2, p])
893
+ let sweep = arcArea < 0 ? 0 : 1;
894
+
895
+ // new arc command
896
+ let comArc = { type: 'A', values: [r, r, 0, 0, sweep, p.x, p.y] };
897
+ //renderPoint(markers, ptC)
898
+ //renderPoint(markers, ptM)
899
+ isArc = true;
900
+
901
+ return { com: comArc, isArc, area: arcSegArea }
902
+
903
+ }
904
+ */
905
+
906
+
907
+ if (isRightAngle) {
908
+ // point between cps
909
+
910
+
911
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
912
+
913
+ if (pI) {
914
+
915
+ let r1 = getDistance(p0, pI);
916
+ let r2 = getDistance(p, pI);
917
+
918
+ let rMax = +Math.max(r1, r2).toFixed(8);
919
+ let rMin = +Math.min(r1, r2).toFixed(8);
920
+
921
+ let rx = rMin
922
+ let ry = rMax
923
+
924
+ let arcArea = getPolygonArea([p0, cp1, cp2, p])
925
+ let sweep = arcArea < 0 ? 0 : 1;
926
+
927
+ let w = Math.abs(p.x - p0.x);
928
+ let h = Math.abs(p.y - p0.y);
929
+ let landscape = w > h;
930
+
931
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
932
+
933
+ if (circular) {
934
+ //rx = (rx+ry)/2
935
+ rx = rMax
936
+ ry = rx;
937
+ }
938
+
939
+ if (landscape) {
940
+ //console.log('landscape', w, h);
941
+ rx = rMax
942
+ ry = rMin
943
+ }
944
+
945
+
946
+ // get original cubic area
947
+ let comO = [
948
+ { type: 'M', values: [p0.x, p0.y] },
949
+ { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
950
+ ];
951
+
952
+ let comArea = getPathArea(comO);
953
+
954
+ // new arc command
955
+ let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
956
+
957
+ // calculate arc seg area
958
+ arcSegArea = (Math.PI * (rx * ry)) / 4
959
+
960
+ // subtract polygon between start, end and center point
961
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]))
962
+
963
+ let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
964
+
965
+ if (areaDiff < tolerance) {
966
+ isArc = true;
967
+ com = comArc;
968
+ }
969
+
970
+ }
971
+ }
972
+
973
+ return { com: com, isArc, area: arcSegArea }
974
+
975
+ }
976
+
977
+ /**
978
+ * combine adjacent arcs
979
+ */
980
+
981
+ export function combineArcs(pathData) {
982
+
983
+ let arcSeq = [[]]
984
+ let ind = 0
985
+ let arcIndices = [[]];
986
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
987
+
988
+ for (let i = 0, len = pathData.length; i < len; i++) {
989
+ let com = pathData[i];
990
+ let { type, values } = com;
991
+
992
+ if (type === 'A') {
993
+
994
+ let comPrev = pathData[i - 1];
995
+
996
+ /**
997
+ * previous p0 values might not be correct
998
+ * anymore due to cubic simplification
999
+ */
1000
+ let valsL = comPrev.values.slice(-2);
1001
+ p0 = { x: valsL[0], y: valsL[1] };
1002
+
1003
+ let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
1004
+
1005
+ // check if arc is circular
1006
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
1007
+
1008
+
1009
+ //add p0
1010
+ p = { x: values[5], y: values[6] }
1011
+ com.p0 = p0;
1012
+ com.p = p;
1013
+ com.circular = circular;
1014
+
1015
+ let comNext = pathData[i + 1];
1016
+
1017
+ //add first
1018
+ if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
1019
+ arcSeq[ind].push(com)
1020
+ arcIndices[ind].push(i)
1021
+ }
1022
+
1023
+ if (comNext && comNext.type === 'A') {
1024
+ let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
1025
+ let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0
1026
+ let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0
1027
+ //let diff = (diffRx + diffRy) / 2
1028
+ //let circular2 = (100 / rx1 * Math.abs(rx1 - ry1)) < 5;
1029
+
1030
+ p = { x: comNext.values[5], y: comNext.values[6] }
1031
+ comNext.p0 = p0;
1032
+ comNext.p = p;
1033
+
1034
+ // add if radii are almost same
1035
+ if (diffRx < 5 && diffRy < 5) {
1036
+ //console.log(rx, rx1, ry, ry1, 'diff:',diff, 'circular', circular, circular2);
1037
+ arcSeq[ind].push(comNext)
1038
+ arcIndices[ind].push(i + 1)
1039
+ } else {
1040
+
1041
+
1042
+ // start new segment
1043
+ arcSeq.push([])
1044
+ arcIndices.push([])
1045
+ ind++
1046
+
1047
+ }
1048
+ }
1049
+
1050
+ else {
1051
+ //arcSeq[ind].push(com)
1052
+ //arcIndices[ind].push(i - 1)
1053
+ arcSeq.push([])
1054
+ arcIndices.push([])
1055
+ ind++
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ if (!arcIndices.length) return pathData;
1061
+
1062
+ arcSeq = arcSeq.filter(item => item.length)
1063
+ arcIndices = arcIndices.filter(item => item.length)
1064
+ //console.log('combine arcs:', arcSeq, arcIndices);
1065
+
1066
+
1067
+ // Process in reverse to avoid index shifting
1068
+ for (let i = arcSeq.length - 1; i >= 0; i--) {
1069
+ const seq = arcSeq[i];
1070
+ const start = arcIndices[i][0];
1071
+ const len = seq.length;
1072
+
1073
+ // Average radii to prevent distortions
1074
+ let rxA = 0, ryA = 0;
1075
+ seq.forEach(({ values }) => {
1076
+ const [rx, ry] = values;
1077
+ rxA += rx;
1078
+ ryA += ry;
1079
+ });
1080
+ rxA /= len;
1081
+ ryA /= len;
1082
+
1083
+ // Correct near-circular arcs
1084
+ //console.log('seq', seq);
1085
+
1086
+ //let rDiff = 100 / rxA * Math.abs(rxA - ryA);
1087
+ //let circular = rDiff < 5;
1088
+
1089
+ // check if arc is circular
1090
+ let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
1091
+
1092
+
1093
+ if (circular) {
1094
+ // average radii
1095
+ rxA = (rxA + ryA) / 2;
1096
+ ryA = rxA;
1097
+ }
1098
+
1099
+ let comPrev = pathData[start - 1]
1100
+ let comPrevVals = comPrev.values.slice(-2)
1101
+ let M = { type: 'M', values: [comPrevVals[0], comPrevVals[1]] }
1102
+
1103
+
1104
+ if (len === 4) {
1105
+ //console.log('4 arcs');
1106
+
1107
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
1108
+ let [, , , , , x2, y2] = seq[3].values;
1109
+
1110
+ let xDiff = Math.abs(x2 - x1);
1111
+ let yDiff = Math.abs(y2 - y1);
1112
+ let horizontal = xDiff > yDiff;
1113
+
1114
+ if (circular) {
1115
+ let adjustY = !horizontal ? rxA * 2 : 0;
1116
+ //x1 = M.values[0];
1117
+ //y1 = M.values[1] + adjustY;
1118
+ //x2 = M.values[0];
1119
+ //y2 = M.values[1];
1120
+
1121
+ // simplify radii
1122
+ rxA = 1;
1123
+ ryA = 1;
1124
+ }
1125
+
1126
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
1127
+ let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1128
+
1129
+ // This now correctly replaces the original 4 arc commands with 2
1130
+ pathData.splice(start, len, com1, com2);
1131
+ //console.log(com1, com2);
1132
+ }
1133
+
1134
+ else if (len === 3) {
1135
+ //console.log('3 arcs');
1136
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
1137
+ let [rx2, ry2, , , , x2, y2] = seq[2].values;
1138
+
1139
+ // must be large arc
1140
+ largeArc = 1;
1141
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1142
+
1143
+ // replace
1144
+ pathData.splice(start, len, com1);
1145
+
1146
+ }
1147
+
1148
+
1149
+ else if (len === 2) {
1150
+ //console.log('2 arcs');
1151
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
1152
+ let [rx2, ry2, , , , x2, y2] = seq[1].values;
1153
+
1154
+ // if circular or non-elliptic xAxisRotation has no effect
1155
+ if (circular) {
1156
+ rxA = 1;
1157
+ ryA = 1;
1158
+ xAxisRotation = 0;
1159
+ }
1160
+
1161
+ // check if arc is already ideal
1162
+ let { p0, p } = seq[0];
1163
+ let [p0_1, p_1] = [seq[1].p0, seq[1].p];
1164
+
1165
+ if (p0.x !== p_1.x || p0.y !== p_1.y) {
1166
+
1167
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1168
+
1169
+ // replace
1170
+ pathData.splice(start, len, com1);
1171
+ }
1172
+ }
1173
+
1174
+ else {
1175
+ //console.log('single arc');
1176
+ }
1177
+ }
1178
+
1179
+ return pathData
1180
+ }