svg-path-simplify 0.1.3 → 0.2.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 (43) hide show
  1. package/README.md +10 -0
  2. package/dist/svg-path-simplify.esm.js +3905 -1533
  3. package/dist/svg-path-simplify.esm.min.js +13 -1
  4. package/dist/svg-path-simplify.js +3923 -1551
  5. package/dist/svg-path-simplify.min.js +13 -1
  6. package/dist/svg-path-simplify.min.js.gz +0 -0
  7. package/index.html +61 -31
  8. package/package.json +3 -5
  9. package/src/constants.js +3 -0
  10. package/src/index-node.js +0 -1
  11. package/src/index.js +26 -0
  12. package/src/pathData_simplify_cubic.js +74 -31
  13. package/src/pathData_simplify_cubicsToArcs.js +566 -0
  14. package/src/pathData_simplify_harmonize_cpts.js +170 -0
  15. package/src/pathData_simplify_revertToquadratics.js +21 -0
  16. package/src/pathSimplify-main.js +253 -86
  17. package/src/poly-fit-curve-schneider.js +570 -0
  18. package/src/simplify_poly_RDP.js +146 -0
  19. package/src/simplify_poly_radial_distance.js +100 -0
  20. package/src/svg_getViewbox.js +1 -1
  21. package/src/svgii/geometry.js +389 -63
  22. package/src/svgii/geometry_area.js +2 -1
  23. package/src/svgii/pathData_analyze.js +259 -212
  24. package/src/svgii/pathData_convert.js +91 -663
  25. package/src/svgii/pathData_fromPoly.js +12 -0
  26. package/src/svgii/pathData_parse.js +90 -89
  27. package/src/svgii/pathData_parse_els.js +3 -0
  28. package/src/svgii/pathData_parse_fontello.js +449 -0
  29. package/src/svgii/pathData_remove_collinear.js +44 -37
  30. package/src/svgii/pathData_reorder.js +2 -1
  31. package/src/svgii/pathData_simplify_redraw.js +343 -0
  32. package/src/svgii/pathData_simplify_refineCorners.js +18 -9
  33. package/src/svgii/pathData_simplify_refineExtremes.js +19 -78
  34. package/src/svgii/pathData_split.js +42 -45
  35. package/src/svgii/pathData_toPolygon.js +130 -4
  36. package/src/svgii/poly_analyze.js +470 -14
  37. package/src/svgii/poly_to_pathdata.js +224 -19
  38. package/src/svgii/rounding.js +55 -112
  39. package/src/svgii/svg_cleanup.js +13 -1
  40. package/src/svgii/visualize.js +8 -3
  41. package/{debug.cjs → tests/debug.cjs} +3 -0
  42. /package/{test.js → tests/test.js} +0 -0
  43. /package/{testSVG.js → tests/testSVG.js} +0 -0
@@ -3,8 +3,6 @@
3
3
  import { getPathDataVertices, getPointOnEllipse, pointAtT, checkLineIntersection, getDistance, interpolate, getAngle } from './geometry.js';
4
4
 
5
5
  import { splitSubpaths } from "./convert_segments";
6
-
7
-
8
6
  import { getPolygonArea, getPathArea, getRelativeAreaDiff } from './geometry_area.js';
9
7
  import { splitSubpaths } from './pathData_split.js';
10
8
  import { getPolyBBox} from './geometry_bbox.js';
@@ -12,7 +10,7 @@ import { renderPoint, renderPath } from "./visualize";
12
10
  */
13
11
 
14
12
 
15
- import { checkLineIntersection, getAngle, getDeltaAngle, getDistance, getDistAv, getSquareDistance, interpolate, pointAtT, rotatePoint, toParametricAngle } from './geometry';
13
+ import { checkLineIntersection, getAngle, getDeltaAngle, getDistance, getDistAv, getDistManhattan, getSquareDistance, interpolate, pointAtT, rotatePoint, toParametricAngle } from './geometry';
16
14
  import { getPathArea, getPolygonArea, getRelativeAreaDiff } from './geometry_area';
17
15
  import { pathDataToD } from './pathData_stringify';
18
16
  import { roundPathData } from './rounding';
@@ -53,15 +51,49 @@ export function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
53
51
 
54
52
  export function convertPathData(pathData, {
55
53
  toShorthands = true,
54
+ toLonghands = false,
56
55
  toRelative = true,
57
- decimals = 3
56
+ toAbsolute = false,
57
+ decimals = 3,
58
+ arcToCubic = false,
59
+ quadraticToCubic = false,
60
+
61
+ // assume we need full normalization
62
+ hasRelatives = true,
63
+ hasShorthands = true,
64
+ hasQuadratics = true,
65
+ hasArcs = true,
66
+ testTypes = false
67
+
68
+
58
69
  } = {}) {
59
70
 
71
+ // pathdata properties - test= true adds a manual test
72
+ if (testTypes) {
73
+ //console.log('test for conversions');
74
+ let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
75
+ hasRelatives = /[lcqamts]/gi.test(commands);
76
+ hasQuadratics = /[qt]/gi.test(commands);
77
+ hasArcs = /[a]/gi.test(commands);
78
+ hasShorthands = /[vhst]/gi.test(commands);
79
+ isPoly = /[mlz]/gi.test(commands);
80
+ }
81
+
82
+
83
+ // some params exclude each other
84
+ toRelative = toAbsolute ? false : toRelative;
85
+ toShorthands = toLonghands ? false : toShorthands
86
+
60
87
 
61
88
  //console.log(toShorthands, toRelative, decimals);
89
+ if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
90
+ if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData);
62
91
 
63
92
  //if(decimals>-1 && decimals<2) pathData = roundPathData(pathData, decimals);
64
93
  if (toShorthands) pathData = pathDataToShorthands(pathData);
94
+ if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
95
+
96
+ if (toAbsolute) pathData = pathDataToAbsolute(pathData);
65
97
 
66
98
  // pre round - before relative conversion to minimize distortions
67
99
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
@@ -174,37 +206,36 @@ export function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decim
174
206
  pathData[0].values = pathData[0].values.map(val => +val.toFixed(decimals));
175
207
  }
176
208
 
209
+ let len = pathData.length;
177
210
  let M = pathData[0].values;
178
211
  let x = M[0],
179
212
  y = M[1],
180
213
  mx = x,
181
214
  my = y;
182
215
 
183
- for (let i = 1, len = pathData.length; i < len; i++) {
216
+ for (let i = 1; i < len; i++) {
184
217
  let com = pathData[i];
185
218
  let { type, values } = com;
186
- let newType = toRelative ? type.toLowerCase() : type.toUpperCase();
219
+ let vLen = values.length;
220
+ let typeRel = type.toLowerCase();
221
+ let typeAbs =type.toUpperCase();
222
+ let typeNew = toRelative ? typeRel : typeAbs;
187
223
 
188
- if (type !== newType) {
189
- type = newType;
190
- com.type = type;
224
+ if (type !== typeNew) {
225
+ com.type = typeNew;
191
226
 
192
- switch (type) {
227
+ switch (typeRel) {
193
228
  case "a":
194
- case "A":
195
229
  values[5] = toRelative ? values[5] - x : values[5] + x;
196
230
  values[6] = toRelative ? values[6] - y : values[6] + y;
197
231
  break;
198
232
  case "v":
199
- case "V":
200
233
  values[0] = toRelative ? values[0] - y : values[0] + y;
201
234
  break;
202
235
  case "h":
203
- case "H":
204
236
  values[0] = toRelative ? values[0] - x : values[0] + x;
205
237
  break;
206
238
  case "m":
207
- case "M":
208
239
  if (toRelative) {
209
240
  values[0] -= x;
210
241
  values[1] -= y;
@@ -226,23 +257,18 @@ export function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decim
226
257
  }
227
258
  }
228
259
 
229
- let vLen = values.length;
230
- switch (type) {
260
+ switch (typeRel) {
231
261
  case "z":
232
- case "Z":
233
262
  x = mx;
234
263
  y = my;
235
264
  break;
236
265
  case "h":
237
- case "H":
238
266
  x = toRelative ? x + values[0] : values[0];
239
267
  break;
240
268
  case "v":
241
- case "V":
242
269
  y = toRelative ? y + values[0] : values[0];
243
270
  break;
244
271
  case "m":
245
- case "M":
246
272
  mx = values[vLen - 2] + (toRelative ? x : 0);
247
273
  my = values[vLen - 1] + (toRelative ? y : 0);
248
274
  default:
@@ -386,7 +412,6 @@ export function pathDataToShorthands(pathData, decimals = -1, test = false) {
386
412
 
387
413
  //pathData = JSON.parse(JSON.stringify(pathData))
388
414
  //console.log('has dec', pathData);
389
-
390
415
  /**
391
416
  * analyze pathdata – if you're sure your data is already absolute skip it via test=false
392
417
  */
@@ -401,47 +426,67 @@ export function pathDataToShorthands(pathData, decimals = -1, test = false) {
401
426
  let len = pathData.length
402
427
  let pathDataShorts = new Array(len);
403
428
 
404
- let comShort = {
405
- type: "M",
406
- values: pathData[0].values
407
- };
408
-
429
+ let comShort = pathData[0]
409
430
  pathDataShorts[0] = comShort;
410
431
 
411
432
  let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
412
- let p;
413
- let tolerance = 0.01
433
+ let p = p0;
414
434
 
415
435
  for (let i = 1; i < len; i++) {
416
436
 
417
437
  let com = pathData[i];
438
+ comShort = com;
418
439
  let { type, values } = com;
419
440
  let valuesLen = values.length;
420
441
  let valuesLast = [values[valuesLen - 2], values[valuesLen - 1]];
421
442
 
422
- // previoius command
443
+ // previous command
423
444
  let comPrev = pathData[i - 1];
424
- let typePrev = comPrev.type
425
445
 
426
446
  //last on-path point
427
447
  p = { x: valuesLast[0], y: valuesLast[1] };
428
448
 
449
+ // deltas for h or v
450
+ let dx = Math.abs(p.x - p0.x)
451
+ let dy = Math.abs(p.y - p0.y)
452
+ let maxDist = getDistManhattan(p0, p) * 0.01
453
+
454
+
429
455
  // first bezier control point for S/T shorthand tests
430
- let cp1 = { x: values[0], y: values[1] };
456
+ let isShort = false, isHorizontal = false, isVertical = false;
431
457
 
458
+ if ((type === 'C' && comPrev.type === 'C') || (type === 'Q' && comPrev.type === 'Q')) {
459
+ let cpPrev = comPrev.type === 'C' ? { x: comPrev.values[2], y: comPrev.values[3] } : { x: comPrev.values[0], y: comPrev.values[1] };
460
+ let cpFirst = { x: values[0], y: values[1] };
432
461
 
433
- //calculate threshold based on command dimensions
434
- let w = Math.abs(p.x - p0.x)
435
- let h = Math.abs(p.y - p0.y)
436
- let thresh = (w + h) / 2 * tolerance
462
+ let dx1 = (p0.x - cpPrev.x)
463
+ let dy1 = (p0.y - cpPrev.y)
437
464
 
438
- let diffX, diffY, diff, cp1_reflected;
465
+ //adjust maxDist
466
+ maxDist = getDistManhattan(cpPrev, cpFirst) * 0.05
467
+
468
+ // reflected cp
469
+ let cpR = { x: cpPrev.x + dx1 * 2, y: cpPrev.y + dy1 * 2 }
470
+ let distCp = getDistManhattan(cpR, cpFirst)
471
+
472
+ isShort = distCp < maxDist;
473
+
474
+ }
475
+
476
+ else if (type === 'L') {
477
+ isHorizontal = dy === 0 || dy < maxDist;
478
+ isVertical = dx === 0 || dx < maxDist;
479
+ isShort = isVertical || isHorizontal;
480
+
481
+ if (isShort) {
482
+ //renderPoint(markers, p, 'magenta')
483
+ }
484
+ }
439
485
 
440
486
 
441
487
  switch (type) {
442
488
  case "L":
443
-
444
- if (h === 0 || (h < thresh && w > thresh)) {
489
+ if (isHorizontal) {
445
490
  //console.log('is H');
446
491
  comShort = {
447
492
  type: "H",
@@ -450,83 +495,34 @@ export function pathDataToShorthands(pathData, decimals = -1, test = false) {
450
495
  }
451
496
 
452
497
  // V
453
- else if (w === 0 || (h > thresh && w < thresh)) {
498
+ if (isVertical) {
454
499
  //console.log('is V', w, h);
455
500
  comShort = {
456
501
  type: "V",
457
502
  values: [values[1]]
458
503
  };
459
- } else {
460
- //console.log('not', type, h, w, thresh, com);
461
- comShort = com;
462
- }
463
-
504
+ }
464
505
  break;
465
506
 
466
507
  case "Q":
467
508
 
468
- // skip test
469
- if (typePrev !== 'Q') {
470
- //console.log('skip T:', type, typePrev);
471
- p0 = { x: valuesLast[0], y: valuesLast[1] };
472
- //pathDataShorts.push(com);
473
- pathDataShorts[i] = com;
474
- continue;
475
- }
476
-
477
- let cp1_prev = { x: comPrev.values[0], y: comPrev.values[1] };
478
- // reflected Q control points
479
- cp1_reflected = { x: (2 * p0.x - cp1_prev.x), y: (2 * p0.y - cp1_prev.y) };
480
-
481
- //let thresh = (diffX+diffY)/2
482
- diffX = Math.abs(cp1.x - cp1_reflected.x)
483
- diffY = Math.abs(cp1.y - cp1_reflected.y)
484
- diff = (diffX + diffY) / 2
485
-
486
- if (diff < thresh) {
487
- //console.log('is T', diff, thresh);
509
+ if (isShort) {
510
+ //comShort = com;
488
511
  comShort = {
489
512
  type: "T",
490
513
  values: [p.x, p.y]
491
514
  };
492
- } else {
493
- comShort = com;
494
515
  }
495
516
 
496
517
  break;
497
518
  case "C":
498
-
499
- let cp2 = { x: values[2], y: values[3] };
500
-
501
- if (typePrev !== 'C') {
502
- //console.log('skip S', typePrev);
503
- //pathDataShorts.push(com);
504
- pathDataShorts[i] = com;
505
-
506
- p0 = { x: valuesLast[0], y: valuesLast[1] };
507
- continue;
508
- }
509
-
510
- let cp2_prev = { x: comPrev.values[2], y: comPrev.values[3] };
511
-
512
- // reflected C control points
513
- cp1_reflected = { x: (2 * p0.x - cp2_prev.x), y: (2 * p0.y - cp2_prev.y) };
514
-
515
- //let thresh = (diffX+diffY)/2
516
- diffX = Math.abs(cp1.x - cp1_reflected.x)
517
- diffY = Math.abs(cp1.y - cp1_reflected.y)
518
- diff = (diffX + diffY) / 2
519
-
520
-
521
- if (diff < thresh) {
519
+ if (isShort) {
522
520
  //console.log('is S');
523
521
  comShort = {
524
522
  type: "S",
525
- values: [cp2.x, cp2.y, p.x, p.y]
523
+ values: [values[2], values[3], p.x, p.y]
526
524
  };
527
- } else {
528
- comShort = com;
529
- }
525
+ }
530
526
  break;
531
527
  default:
532
528
  comShort = {
@@ -535,19 +531,8 @@ export function pathDataToShorthands(pathData, decimals = -1, test = false) {
535
531
  };
536
532
  }
537
533
 
538
- // add decimal info
539
- if (com.decimals || com.decimals === 0) {
540
- comShort.decimals = com.decimals
541
- }
542
-
543
- // round final values
544
- if (decimals > -1) {
545
- comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) })
546
- }
547
-
548
- p0 = { x: valuesLast[0], y: valuesLast[1] };
534
+ p0 = p;
549
535
  pathDataShorts[i] = comShort;
550
- //pathDataShorts.push(comShort);
551
536
  }
552
537
 
553
538
  //console.log('pathDataShorts', pathDataShorts);
@@ -968,560 +953,3 @@ export function convertArrayPathData(pathDataArray) {
968
953
  return pathData;
969
954
  }
970
955
 
971
-
972
-
973
-
974
-
975
-
976
- /**
977
- * cubics to arcs
978
- */
979
-
980
-
981
- export function combineCubicsToArcs(pathData = [], {
982
- threshold = 0,
983
- } = {}) {
984
-
985
- let l = pathData.length;
986
- let pathDataN = [pathData[0]];
987
-
988
- for (let i = 1; i < l; i++) {
989
- let com = pathData[i];
990
- let { type, cp1 = null, cp2 = null, p0, p } = com;
991
- let comP = pathData[i - 1];
992
- let comN = pathData[i + 1] ? pathData[i + 1] : null;
993
- let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
994
-
995
- if (type === 'C' && comN && comN.type === 'C') {
996
-
997
- let thresh = getDistAv(p0, p) * 0.02;
998
- //thresh = getDistAv(p0, p) * 10000;
999
-
1000
- let dx1 = Math.abs(p0.x - cp1.x)
1001
- let dy1 = Math.abs(p0.y - cp1.y)
1002
-
1003
- let isHorizontal1 = dy1 < thresh;
1004
- let isVertical1 = dx1 < thresh;
1005
-
1006
-
1007
- let dx2 = Math.abs(comN.p0.x - comN.cp1.x)
1008
- let dy2 = Math.abs(comN.p0.y - comN.cp1.y)
1009
-
1010
- let isHorizontal2 = dy2 < thresh;
1011
- let isVertical2 = dx2 < thresh;
1012
-
1013
- //console.log(isHorizontal1, isVertical1);
1014
-
1015
- // check angles
1016
- let angleDiff1 = (isHorizontal1 || isVertical1) ? 0 : Infinity;
1017
- let angleDiff2 = (isHorizontal2 || isVertical2) ? 0 : Infinity;
1018
-
1019
- if (!isHorizontal1 && !isVertical1) {
1020
- //console.log('get angles', isHorizontal1, isVertical1);
1021
- let angle1 = getAngle(p0, cp1, true);
1022
- let angle2 = getAngle(p, cp2, true);
1023
- let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
1024
- angleDiff1 = Math.abs((deltaAngle % 180) - 90);
1025
- }
1026
-
1027
- if (!isHorizontal2 && !isVertical2) {
1028
- //console.log('get angles', isHorizontal1, isVertical1);
1029
- let angle1 = getAngle(p0, cp1, true);
1030
- let angle2 = getAngle(p, cp2, true);
1031
- let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
1032
- angleDiff2 = Math.abs((deltaAngle % 180) - 90);
1033
- }
1034
-
1035
-
1036
- let isRightAngle1 = angleDiff1 < 3;
1037
- let isRightAngle2 = angleDiff2 < 3;
1038
-
1039
- let centroids = [];
1040
- let poly = [];
1041
- let rArr = []
1042
- let largeArc = 0;
1043
-
1044
- // final on path point
1045
- let p_a = p
1046
-
1047
- // 2 possible candidates - test radius
1048
- if (isRightAngle1 && isRightAngle2) {
1049
- //renderPoint(markers, com.p)
1050
-
1051
- let pI = checkLineIntersection(p0, cp1, p, cp2, false);
1052
- let r1 = getDistance(p0, pI);
1053
- let r2 = getDistance(p, pI);
1054
- let rDiff1 = Math.abs(r1 - r2)
1055
- //let r = r1
1056
-
1057
- rArr.push(r1, r2)
1058
-
1059
- poly.push(p0, p)
1060
- p_a = p
1061
-
1062
-
1063
- // 2 commands can be combined – similar radii
1064
- if (rDiff1 < thresh) {
1065
-
1066
- //renderPoint(markers, com.p)
1067
-
1068
- // add to polygon for sweep
1069
- poly.push(comN.p)
1070
-
1071
- // update final point
1072
- p_a = comN.p
1073
-
1074
- // approximate/average final center point for final radius
1075
- let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
1076
- let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
1077
-
1078
- let cp1_r2 = rotatePoint(comN.cp1, comN.p0.x, comN.p0.y, (Math.PI * -0.5))
1079
- let cp2_r2 = rotatePoint(comN.cp2, comN.p.x, comN.p.y, (Math.PI * 0.5))
1080
-
1081
- // assumed centroid
1082
- let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
1083
- let ptC2 = checkLineIntersection(comN.p0, cp1_r2, comN.p, cp2_r2, false)
1084
- let distC = ptC && ptC2 ? getDistAv(ptC, ptC2) : Infinity
1085
-
1086
-
1087
- // 2 commands can definitely be combined
1088
- if (distC < thresh) {
1089
- //renderPoint(markers, ptC, 'cyan', '1.2%', '0.5')
1090
- //renderPoint(markers, ptC2, 'magenta', '0.5%', '0.5')
1091
-
1092
- // add to centroid array
1093
- centroids.push(ptC, ptC2)
1094
-
1095
- }
1096
-
1097
-
1098
- if (comN2 && comN2.type === 'C') {
1099
-
1100
- let cp1_r3 = rotatePoint(comN2.cp1, comN2.p0.x, comN2.p0.y, (Math.PI * -0.5))
1101
- let cp2_r3 = rotatePoint(comN2.cp2, comN2.p.x, comN2.p.y, (Math.PI * 0.5))
1102
- let ptC3 = checkLineIntersection(comN2.p0, cp1_r3, comN2.p, cp2_r3, false)
1103
-
1104
- let distC2 = ptC && ptC3 ? getDistAv(ptC, ptC3) : Infinity
1105
-
1106
- // can be combined with 3rd command
1107
- if (distC2 < thresh) {
1108
- //renderPoint(markers, ptC3, 'green', '2%', '0.3')
1109
-
1110
- let r3 = getDistance(ptC3, comN2.p)
1111
- rArr.push(r3)
1112
-
1113
- // update final point
1114
- p_a = comN2.p
1115
- poly.push(p, comN2.p)
1116
-
1117
- largeArc = 1;
1118
-
1119
- }
1120
- }
1121
- //console.log(rDiff1, r, r1, r2);
1122
-
1123
- } else {
1124
- pathDataN.push(com)
1125
- continue
1126
- }
1127
-
1128
- }
1129
-
1130
-
1131
- // create new arc command
1132
- if (poly.length > 1) {
1133
-
1134
- // get average radius
1135
- //rArr = rArr.sort()
1136
- let rA = Math.max(...rArr)
1137
- rA = rArr[0]
1138
-
1139
- let centroidA;
1140
- let xArr = centroids.map(pt => pt.x)
1141
- let yArr = centroids.map(pt => pt.y)
1142
-
1143
- centroidA = {
1144
- x: (xArr.reduce((a, b) => a + b, 0)) / centroids.length,
1145
- y: (yArr.reduce((a, b) => a + b, 0)) / centroids.length
1146
- }
1147
-
1148
- //console.log(xArr, centroidA);
1149
-
1150
- //rA = getDistance(p0, centroids[0])
1151
-
1152
- rA = getDistance(p0, centroidA)
1153
- let rA2 = getDistance(p, centroidA)
1154
- //rA = (rA+rA2) /2
1155
- //rA = Math.min(rA,rA2)
1156
-
1157
- // rA = ((Math.min(...rArr) * 2 + Math.max(...rArr)) ) / 3
1158
- //console.log(rArr, rA);
1159
-
1160
- let area = getPolygonArea(poly, false)
1161
- let sweep = area < 0 ? 0 : 1;
1162
-
1163
- let comA = { type: 'A', values: [rA, rA, 0, largeArc, sweep, p_a.x, p_a.y], p0, p: p_a }
1164
-
1165
- console.log('comA', comA);
1166
-
1167
- pathDataN.push(comA)
1168
-
1169
- i += rArr.length - 1;
1170
- //i++
1171
- continue
1172
- /*
1173
- */
1174
-
1175
- }
1176
-
1177
-
1178
-
1179
-
1180
- // test angles
1181
-
1182
- }
1183
-
1184
- pathDataN.push(com)
1185
- }
1186
-
1187
- let d = pathDataToD(pathDataN)
1188
- console.log(d);
1189
-
1190
- console.log('pathDataN', pathDataN);
1191
- return pathDataN
1192
-
1193
- }
1194
-
1195
- export function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
1196
-
1197
- //console.log(p0, cp1, cp2, p, segArea );
1198
- let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
1199
- //let pathDataChunk = [{ type: 'M', values: [p0.x, p0.y] }, com];
1200
-
1201
- let arcSegArea = 0, isArc = false
1202
-
1203
- // check angles
1204
- let angle1 = getAngle(p0, cp1, true);
1205
- let angle2 = getAngle(p, cp2, true);
1206
- let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
1207
-
1208
-
1209
- let angleDiff = Math.abs((deltaAngle % 180) - 90);
1210
- let isRightAngle = angleDiff < 3;
1211
-
1212
-
1213
- /*
1214
- let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
1215
- let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
1216
- //renderPoint(markers, cp1_r )
1217
-
1218
- // assumed centroid
1219
- let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
1220
-
1221
- let dist0 = getSquareDistance(p0, p)
1222
- let dist1 = getSquareDistance(p0, ptC)
1223
- let dist2 = getSquareDistance(p, ptC)
1224
-
1225
- // let mid point
1226
- let ptM = pointAtT([p0, cp1, cp2, p], 0.5)
1227
- //let dist3 = getSquareDistance(ptM, ptC)
1228
- let diff1 = Math.abs(dist1 - dist2)
1229
-
1230
-
1231
- if (diff1 <= dist0 * 0.01) {
1232
-
1233
- let r = Math.sqrt((dist1 + dist2) / 2)
1234
- //r = Math.sqrt((dist1 + dist2 + dist3) / 3)
1235
- //r = Math.sqrt( Math.min(dist1, dist2) )
1236
- //r=0.25
1237
- //console.log('diff1', diff1, r);
1238
-
1239
- let arcArea = getPolygonArea([p0, cp1, cp2, p])
1240
- let sweep = arcArea < 0 ? 0 : 1;
1241
-
1242
- // new arc command
1243
- let comArc = { type: 'A', values: [r, r, 0, 0, sweep, p.x, p.y] };
1244
- //renderPoint(markers, ptC)
1245
- //renderPoint(markers, ptM)
1246
- isArc = true;
1247
-
1248
- return { com: comArc, isArc, area: arcSegArea }
1249
-
1250
- }
1251
- */
1252
-
1253
-
1254
- if (isRightAngle) {
1255
- // point between cps
1256
-
1257
-
1258
- let pI = checkLineIntersection(p0, cp1, p, cp2, false);
1259
-
1260
- if (pI) {
1261
-
1262
- let r1 = getDistance(p0, pI);
1263
- let r2 = getDistance(p, pI);
1264
-
1265
- let rMax = +Math.max(r1, r2).toFixed(8);
1266
- let rMin = +Math.min(r1, r2).toFixed(8);
1267
-
1268
- let rx = rMin
1269
- let ry = rMax
1270
-
1271
- let arcArea = getPolygonArea([p0, cp1, cp2, p])
1272
- let sweep = arcArea < 0 ? 0 : 1;
1273
-
1274
- let w = Math.abs(p.x - p0.x);
1275
- let h = Math.abs(p.y - p0.y);
1276
- let landscape = w > h;
1277
-
1278
- let circular = (100 / rx * Math.abs(rx - ry)) < 5;
1279
-
1280
- if (circular) {
1281
- //rx = (rx+ry)/2
1282
- rx = rMax
1283
- ry = rx;
1284
- }
1285
-
1286
- if (landscape) {
1287
- //console.log('landscape', w, h);
1288
- rx = rMax
1289
- ry = rMin
1290
- }
1291
-
1292
-
1293
- // get original cubic area
1294
- let comO = [
1295
- { type: 'M', values: [p0.x, p0.y] },
1296
- { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
1297
- ];
1298
-
1299
- let comArea = getPathArea(comO);
1300
-
1301
- // new arc command
1302
- let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
1303
-
1304
- // calculate arc seg area
1305
- arcSegArea = (Math.PI * (rx * ry)) / 4
1306
-
1307
- // subtract polygon between start, end and center point
1308
- arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]))
1309
-
1310
- let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
1311
-
1312
- if (areaDiff < tolerance) {
1313
- isArc = true;
1314
- com = comArc;
1315
- }
1316
-
1317
- }
1318
- }
1319
-
1320
- return { com: com, isArc, area: arcSegArea }
1321
-
1322
- }
1323
-
1324
- /**
1325
- * combine adjacent arcs
1326
- */
1327
-
1328
- export function combineArcs(pathData) {
1329
-
1330
- let arcSeq = [[]]
1331
- let ind = 0
1332
- let arcIndices = [[]];
1333
- let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
1334
-
1335
- for (let i = 0, len = pathData.length; i < len; i++) {
1336
- let com = pathData[i];
1337
- let { type, values } = com;
1338
-
1339
- if (type === 'A') {
1340
-
1341
- let comPrev = pathData[i - 1];
1342
-
1343
- /**
1344
- * previous p0 values might not be correct
1345
- * anymore due to cubic simplification
1346
- */
1347
- let valsL = comPrev.values.slice(-2);
1348
- p0 = { x: valsL[0], y: valsL[1] };
1349
-
1350
- let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
1351
-
1352
- // check if arc is circular
1353
- let circular = (100 / rx * Math.abs(rx - ry)) < 5;
1354
-
1355
-
1356
- //add p0
1357
- p = { x: values[5], y: values[6] }
1358
- com.p0 = p0;
1359
- com.p = p;
1360
- com.circular = circular;
1361
-
1362
- let comNext = pathData[i + 1];
1363
-
1364
- //add first
1365
- if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
1366
- arcSeq[ind].push(com)
1367
- arcIndices[ind].push(i)
1368
- }
1369
-
1370
- if (comNext && comNext.type === 'A') {
1371
- let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
1372
- let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0
1373
- let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0
1374
- //let diff = (diffRx + diffRy) / 2
1375
- //let circular2 = (100 / rx1 * Math.abs(rx1 - ry1)) < 5;
1376
-
1377
- p = { x: comNext.values[5], y: comNext.values[6] }
1378
- comNext.p0 = p0;
1379
- comNext.p = p;
1380
-
1381
- // add if radii are almost same
1382
- if (diffRx < 5 && diffRy < 5) {
1383
- //console.log(rx, rx1, ry, ry1, 'diff:',diff, 'circular', circular, circular2);
1384
- arcSeq[ind].push(comNext)
1385
- arcIndices[ind].push(i + 1)
1386
- } else {
1387
-
1388
-
1389
- // start new segment
1390
- arcSeq.push([])
1391
- arcIndices.push([])
1392
- ind++
1393
-
1394
- }
1395
- }
1396
-
1397
- else {
1398
- //arcSeq[ind].push(com)
1399
- //arcIndices[ind].push(i - 1)
1400
- arcSeq.push([])
1401
- arcIndices.push([])
1402
- ind++
1403
- }
1404
- }
1405
- }
1406
-
1407
- if (!arcIndices.length) return pathData;
1408
-
1409
- arcSeq = arcSeq.filter(item => item.length)
1410
- arcIndices = arcIndices.filter(item => item.length)
1411
- //console.log('combine arcs:', arcSeq, arcIndices);
1412
-
1413
-
1414
- // Process in reverse to avoid index shifting
1415
- for (let i = arcSeq.length - 1; i >= 0; i--) {
1416
- const seq = arcSeq[i];
1417
- const start = arcIndices[i][0];
1418
- const len = seq.length;
1419
-
1420
- // Average radii to prevent distortions
1421
- let rxA = 0, ryA = 0;
1422
- seq.forEach(({ values }) => {
1423
- const [rx, ry] = values;
1424
- rxA += rx;
1425
- ryA += ry;
1426
- });
1427
- rxA /= len;
1428
- ryA /= len;
1429
-
1430
- // Correct near-circular arcs
1431
- //console.log('seq', seq);
1432
-
1433
- //let rDiff = 100 / rxA * Math.abs(rxA - ryA);
1434
- //let circular = rDiff < 5;
1435
-
1436
- // check if arc is circular
1437
- let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
1438
-
1439
-
1440
- if (circular) {
1441
- // average radii
1442
- rxA = (rxA + ryA) / 2;
1443
- ryA = rxA;
1444
- }
1445
-
1446
- let comPrev = pathData[start - 1]
1447
- let comPrevVals = comPrev.values.slice(-2)
1448
- let M = { type: 'M', values: [comPrevVals[0], comPrevVals[1]] }
1449
-
1450
-
1451
- if (len === 4) {
1452
- //console.log('4 arcs');
1453
-
1454
- let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
1455
- let [, , , , , x2, y2] = seq[3].values;
1456
-
1457
- let xDiff = Math.abs(x2 - x1);
1458
- let yDiff = Math.abs(y2 - y1);
1459
- let horizontal = xDiff > yDiff;
1460
-
1461
- if (circular) {
1462
- let adjustY = !horizontal ? rxA * 2 : 0;
1463
- //x1 = M.values[0];
1464
- //y1 = M.values[1] + adjustY;
1465
- //x2 = M.values[0];
1466
- //y2 = M.values[1];
1467
-
1468
- // simplify radii
1469
- rxA = 1;
1470
- ryA = 1;
1471
- }
1472
-
1473
- let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
1474
- let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1475
-
1476
- // This now correctly replaces the original 4 arc commands with 2
1477
- pathData.splice(start, len, com1, com2);
1478
- //console.log(com1, com2);
1479
- }
1480
-
1481
- else if (len === 3) {
1482
- //console.log('3 arcs');
1483
- let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
1484
- let [rx2, ry2, , , , x2, y2] = seq[2].values;
1485
-
1486
- // must be large arc
1487
- largeArc = 1;
1488
- let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1489
-
1490
- // replace
1491
- pathData.splice(start, len, com1);
1492
-
1493
- }
1494
-
1495
-
1496
- else if (len === 2) {
1497
- //console.log('2 arcs');
1498
- let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
1499
- let [rx2, ry2, , , , x2, y2] = seq[1].values;
1500
-
1501
- // if circular or non-elliptic xAxisRotation has no effect
1502
- if (circular) {
1503
- rxA = 1;
1504
- ryA = 1;
1505
- xAxisRotation = 0;
1506
- }
1507
-
1508
- // check if arc is already ideal
1509
- let { p0, p } = seq[0];
1510
- let [p0_1, p_1] = [seq[1].p0, seq[1].p];
1511
-
1512
- if (p0.x !== p_1.x || p0.y !== p_1.y) {
1513
-
1514
- let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
1515
-
1516
- // replace
1517
- pathData.splice(start, len, com1);
1518
- }
1519
- }
1520
-
1521
- else {
1522
- //console.log('single arc');
1523
- }
1524
- }
1525
-
1526
- return pathData
1527
- }