svg-path-simplify 0.0.9 → 0.1.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.
@@ -28,9 +28,9 @@
28
28
  }
29
29
  }
30
30
 
31
- function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
31
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', opacity="1", render = true) {
32
32
 
33
- let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
33
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-opacity="${opacity}" /> `;
34
34
 
35
35
  if (render) {
36
36
  svg.insertAdjacentHTML("beforeend", path);
@@ -108,18 +108,62 @@
108
108
  return angle
109
109
  }
110
110
 
111
+ function getDeltaAngle(centerPoint, startPoint, endPoint, largeArc = false) {
112
+
113
+ const normalizeAngle = (angle) => {
114
+ let normalized = angle % (2 * Math.PI);
115
+
116
+ if (normalized > Math.PI) {
117
+ normalized -= 2 * Math.PI;
118
+ } else if (normalized <= -Math.PI) {
119
+ normalized += 2 * Math.PI;
120
+ }
121
+ return normalized;
122
+ };
123
+
124
+ let startAngle = Math.atan2(
125
+ startPoint.y - centerPoint.y,
126
+ startPoint.x - centerPoint.x
127
+ );
128
+
129
+ let endAngle = Math.atan2(
130
+ endPoint.y - centerPoint.y,
131
+ endPoint.x - centerPoint.x
132
+ );
133
+
134
+ // Calculate raw delta angle (difference)
135
+ let deltaAngle = endAngle - startAngle;
136
+
137
+ // Normalize the delta angle to range (-π, π]
138
+ deltaAngle = normalizeAngle(deltaAngle);
139
+
140
+ if (largeArc) deltaAngle = Math.PI*2 - Math.abs(deltaAngle);
141
+
142
+ let phi = 180 / Math.PI;
143
+ let startAngleDeg = startAngle * phi;
144
+ let endAngleDeg = endAngle * phi;
145
+ let deltaAngleDeg = deltaAngle * phi;
146
+
147
+ return {
148
+ startAngle, endAngle, deltaAngle, startAngleDeg,
149
+ endAngleDeg,
150
+ deltaAngleDeg
151
+ };
152
+
153
+ }
154
+
111
155
  /**
112
156
  * based on: Justin C. Round's
113
157
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
114
158
  */
115
159
 
116
- function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
160
+ function checkLineIntersection(p1 = null, p2 = null, p3 = null, p4 = null, exact = true, debug = false) {
117
161
  // 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
118
162
  let denominator, a, b, numerator1, numerator2;
119
163
  let intersectionPoint = {};
120
164
 
121
- if(!p1 || !p2 || !p3 || !p4){
122
- if(debug) console.warn('points missing');
165
+ if (!p1 || !p2 || !p3 || !p4) {
166
+ if (debug) console.warn('points missing');
123
167
  return false
124
168
  }
125
169
 
@@ -129,7 +173,7 @@
129
173
  return false;
130
174
  }
131
175
  } catch {
132
- if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
176
+ if (debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
133
177
  return false
134
178
  }
135
179
 
@@ -486,6 +530,17 @@
486
530
  return arcData;
487
531
  }
488
532
 
533
+ function rotatePoint(pt, cx, cy, rotation = 0, convertToRadians = false) {
534
+ if (!rotation) return pt;
535
+
536
+ rotation = convertToRadians ? (rotation / 180) * Math.PI : rotation;
537
+
538
+ return {
539
+ x: cx + (pt.x - cx) * Math.cos(rotation) - (pt.y - cy) * Math.sin(rotation),
540
+ y: cy + (pt.x - cx) * Math.sin(rotation) + (pt.y - cy) * Math.cos(rotation)
541
+ };
542
+ }
543
+
489
544
  function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
490
545
 
491
546
  // Convert degrees to radians
@@ -519,6 +574,18 @@
519
574
  return pt
520
575
  }
521
576
 
577
+ // to parametric angle helper
578
+ function toParametricAngle(angle, rx, ry) {
579
+
580
+ if (rx === ry || (angle % PI$1 * 0.5 === 0)) return angle;
581
+ let angleP = atan$1(tan$1(angle) * (rx / ry));
582
+
583
+ // Ensure the parametric angle is in the correct quadrant
584
+ angleP = cos$1(angle) < 0 ? angleP + PI$1 : angleP;
585
+
586
+ return angleP
587
+ }
588
+
522
589
  function bezierhasExtreme(p0, cpts = [], angleThreshold = 0.05) {
523
590
  let isCubic = cpts.length === 3 ? true : false;
524
591
  let cp1 = cpts[0] || null;
@@ -2334,13 +2401,13 @@
2334
2401
  let cp2X = interpolate(p, cp2, 1.5);
2335
2402
 
2336
2403
  let dist0 = getDistAv(p0, p);
2337
- let threshold = dist0 * 0.01;
2404
+ let threshold = dist0 * 0.03;
2338
2405
  let dist1 = getDistAv(cp1X, cp2X);
2339
2406
 
2340
2407
  let cp1_Q = null;
2341
2408
  let type = 'C';
2342
2409
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2343
- let comN = {type, values};
2410
+ let comN = { type, values };
2344
2411
 
2345
2412
  if (dist1 && threshold && dist1 < threshold) {
2346
2413
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
@@ -2350,6 +2417,7 @@
2350
2417
  comN.values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2351
2418
  comN.p0 = p0;
2352
2419
  comN.cp1 = cp1_Q;
2420
+ comN.cp2 = null;
2353
2421
  comN.p = p;
2354
2422
  }
2355
2423
  }
@@ -2367,7 +2435,7 @@
2367
2435
  if (toShorthands) pathData = pathDataToShorthands(pathData);
2368
2436
 
2369
2437
  // pre round - before relative conversion to minimize distortions
2370
- if(decimals>-1 && toRelative) pathData = roundPathData(pathData, decimals);
2438
+ if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
2371
2439
  if (toRelative) pathData = pathDataToRelative(pathData);
2372
2440
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
2373
2441
 
@@ -2704,7 +2772,7 @@
2704
2772
  let com = pathData[i];
2705
2773
  let { type, values } = com;
2706
2774
  let valuesLen = values.length;
2707
- let valuesLast = [values[valuesLen-2], values[valuesLen-1]];
2775
+ let valuesLast = [values[valuesLen - 2], values[valuesLen - 1]];
2708
2776
 
2709
2777
  // previoius command
2710
2778
  let comPrev = pathData[i - 1];
@@ -2832,6 +2900,108 @@
2832
2900
  return pathDataShorts;
2833
2901
  }
2834
2902
 
2903
+ /**
2904
+ * Convert a parametrized SVG arc to cubic Beziers
2905
+ * Assumes arc parameters are already resolved
2906
+ */
2907
+ function arcToBezierResolved({
2908
+
2909
+ // start / end points
2910
+ p0 = { x: 0, y: 0 },
2911
+ p = { x: 0, y: 0 },
2912
+
2913
+ // center
2914
+ centroid = { x: 0, y: 0 },
2915
+
2916
+ // radii
2917
+ rx = 0,
2918
+ ry = 0,
2919
+
2920
+ // SVG-style rotation
2921
+ xAxisRotation = 0,
2922
+ radToDegree = false,
2923
+
2924
+ // optional
2925
+ startAngle = null,
2926
+ endAngle = null,
2927
+ deltaAngle = null
2928
+
2929
+ } = {}) {
2930
+
2931
+ if (!rx || !ry) return [];
2932
+
2933
+ // new pathData
2934
+ let pathData = [];
2935
+
2936
+ // maximum delta for cubic approximations: Math.PI / 2 (90deg)
2937
+ const maxSegAngle = 1.5707963267948966;
2938
+
2939
+ // Pomax cubic constant
2940
+ const k = 0.551785;
2941
+
2942
+ // rotation
2943
+ let phi = radToDegree
2944
+ ? xAxisRotation
2945
+ : xAxisRotation * Math.PI / 180;
2946
+
2947
+ let cosphi = Math.cos(phi);
2948
+ let sinphi = Math.sin(phi);
2949
+
2950
+ // derive angles if not provided
2951
+ if (startAngle === null || endAngle === null || deltaAngle === null) {
2952
+ ({ startAngle, endAngle, deltaAngle } = getDeltaAngle(centroid, p0, p));
2953
+ }
2954
+
2955
+ // parametrize for elliptic arcs
2956
+ let startAngleParam = rx !== ry ? toParametricAngle(startAngle, rx, ry) : startAngle;
2957
+
2958
+ let deltaAngleParam = rx !== ry ? toParametricAngle(deltaAngle, rx, ry) : deltaAngle;
2959
+
2960
+ let segments = Math.max(1, Math.ceil(Math.abs(deltaAngleParam) / maxSegAngle));
2961
+ let angStep = deltaAngleParam / segments;
2962
+
2963
+ for (let i = 0; i < segments; i++) {
2964
+
2965
+ const a = Math.abs(angStep) === maxSegAngle ?
2966
+ Math.sign(angStep) * k :
2967
+ (4 / 3) * Math.tan(angStep / 4);
2968
+
2969
+ let cos0 = Math.cos(startAngleParam);
2970
+ let sin0 = Math.sin(startAngleParam);
2971
+ let cos1 = Math.cos(startAngleParam + angStep);
2972
+ let sin1 = Math.sin(startAngleParam + angStep);
2973
+
2974
+ // unit arc → cubic
2975
+ let c1 = { x: cos0 - sin0 * a, y: sin0 + cos0 * a };
2976
+ let c2 = { x: cos1 + sin1 * a, y: sin1 - cos1 * a };
2977
+ let e = { x: cos1, y: sin1 };
2978
+
2979
+ let values = [];
2980
+
2981
+ [c1, c2, e].forEach(pt => {
2982
+ let x = pt.x * rx;
2983
+ let y = pt.y * ry;
2984
+
2985
+ values.push(
2986
+ cosphi * x - sinphi * y + centroid.x,
2987
+ sinphi * x + cosphi * y + centroid.y
2988
+ );
2989
+ });
2990
+
2991
+ pathData.push({
2992
+ type: 'C',
2993
+ values,
2994
+ cp1: { x: values[0], y: values[1] },
2995
+ cp2: { x: values[2], y: values[3] },
2996
+ p: { x: values[4], y: values[5] },
2997
+ });
2998
+
2999
+ startAngleParam += angStep;
3000
+ }
3001
+
3002
+ return pathData;
3003
+ }
3004
+
2835
3005
  /**
2836
3006
  * convert arctocommands to cubic bezier
2837
3007
  * based on puzrin's a2c.js
@@ -2966,10 +3136,6 @@
2966
3136
  return pathDataArc;
2967
3137
  }
2968
3138
 
2969
- /**
2970
- * cubics to arcs
2971
- */
2972
-
2973
3139
  function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
2974
3140
 
2975
3141
  let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
@@ -3995,7 +4161,7 @@
3995
4161
  let valsL = values.slice(-2);
3996
4162
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
3997
4163
 
3998
- let area = getPolygonArea([p0, p, p1], true);
4164
+ let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity;
3999
4165
 
4000
4166
  let distSquare = getSquareDistance(p0, p1);
4001
4167
 
@@ -4025,7 +4191,7 @@
4025
4191
 
4026
4192
  // colinear – exclude arcs (as always =) as semicircles won't have an area
4027
4193
 
4028
- if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
4194
+ if ( isFlat && c < l - 1 && comN.type!=='A' && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
4029
4195
 
4030
4196
  /*
4031
4197
  console.log(area, distMax );
@@ -4173,25 +4339,43 @@
4173
4339
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
4174
4340
  }
4175
4341
 
4176
- function optimizeClosePath(pathData, removeFinalLineto = true, reorder = true) {
4342
+ function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
4177
4343
 
4178
4344
  let pathDataNew = [];
4179
- let len = pathData.length;
4345
+ let l = pathData.length;
4180
4346
  let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4181
- let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
4347
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4182
4348
 
4183
4349
  let linetos = pathData.filter(com => com.type === 'L');
4184
4350
 
4185
4351
  // check if order is ideal
4186
- let penultimateCom = pathData[len - 2];
4352
+ let idxPenultimate = isClosed ? l-2 : l-1;
4353
+
4354
+ let penultimateCom = pathData[idxPenultimate];
4187
4355
  let penultimateType = penultimateCom.type;
4188
4356
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8));
4189
4357
 
4190
4358
  // last L command ends at M
4191
4359
  let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4192
4360
 
4361
+ // add closepath Z to enable order optimizations
4362
+ if(!isClosed && autoClose && isClosingCommand){
4363
+
4364
+ /*
4365
+ // adjust final coords
4366
+ let valsLast = pathData[idxPenultimate].values
4367
+ let valsLastLen = valsLast.length;
4368
+ pathData[idxPenultimate].values[valsLastLen-2] = M.x
4369
+ pathData[idxPenultimate].values[valsLastLen-1] = M.y
4370
+ */
4371
+
4372
+ pathData.push({type:'Z', values:[]});
4373
+ isClosed = true;
4374
+ l++;
4375
+ }
4376
+
4193
4377
  // if last segment is not closing or a lineto
4194
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateType === 'L');
4378
+ let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L');
4195
4379
  skipReorder = false;
4196
4380
 
4197
4381
  // we can't change starting point for non closed paths
@@ -4204,7 +4388,7 @@
4204
4388
  if (!skipReorder) {
4205
4389
 
4206
4390
  let indices = [];
4207
- for (let i = 0, len = pathData.length; i < len; i++) {
4391
+ for (let i = 0; i < l; i++) {
4208
4392
  let com = pathData[i];
4209
4393
  let { type, values } = com;
4210
4394
  if (values.length) {
@@ -4240,17 +4424,17 @@
4240
4424
 
4241
4425
  M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) };
4242
4426
 
4243
- len = pathData.length;
4427
+ l = pathData.length;
4244
4428
 
4245
4429
  // remove last lineto
4246
- penultimateCom = pathData[len - 2];
4430
+ penultimateCom = pathData[l - 2];
4247
4431
  penultimateType = penultimateCom.type;
4248
4432
  penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8));
4249
4433
 
4250
4434
  isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y;
4251
4435
 
4252
4436
  if (removeFinalLineto && isClosingCommand) {
4253
- pathData.splice(len - 2, 1);
4437
+ pathData.splice(l - 2, 1);
4254
4438
  }
4255
4439
 
4256
4440
  pathDataNew.push(...pathData);
@@ -4512,49 +4696,47 @@
4512
4696
  function removeEmptySVGEls(svg) {
4513
4697
  let els = svg.querySelectorAll('g, defs');
4514
4698
  els.forEach(el => {
4515
- if (!el.children.length) el.remove();
4699
+ if (!el.children.length) el.remove();
4516
4700
  });
4517
4701
  }
4518
4702
 
4519
4703
  function cleanUpSVG(svgMarkup, {
4520
- returnDom=false,
4521
- removeHidden=true,
4522
- removeUnused=true,
4523
- }={}) {
4704
+ returnDom = false,
4705
+ removeHidden = true,
4706
+ removeUnused = true,
4707
+ } = {}) {
4524
4708
  svgMarkup = cleanSvgPrologue(svgMarkup);
4525
-
4709
+
4526
4710
  // replace namespaced refs
4527
4711
  svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
4528
-
4712
+
4529
4713
  let svg = new DOMParser()
4530
- .parseFromString(svgMarkup, "text/html")
4531
- .querySelector("svg");
4532
-
4533
-
4534
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
4714
+ .parseFromString(svgMarkup, "text/html")
4715
+ .querySelector("svg");
4716
+
4717
+ let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
4535
4718
  removeExcludedAttribues(svg, allowed);
4536
-
4719
+
4537
4720
  let removeEls = ['metadata', 'script'];
4538
-
4721
+
4539
4722
  let els = svg.querySelectorAll('*');
4540
- els.forEach(el=>{
4541
- let name = el.nodeName;
4723
+ els.forEach(el => {
4724
+ let name = el.nodeName;
4542
4725
  // remove hidden elements
4543
4726
  let style = el.getAttribute('style') || '';
4544
4727
  let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
4545
4728
  let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
4546
- if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
4729
+ if (name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden)) {
4547
4730
  el.remove();
4548
- }else {
4731
+ } else {
4549
4732
  // remove BS elements
4550
4733
  removeNameSpaceAtts(el);
4551
4734
  }
4552
4735
  });
4553
4736
 
4554
- if(returnDom) return svg
4737
+ if (returnDom) return svg
4555
4738
 
4556
4739
  let markup = stringifySVG(svg);
4557
- console.log(markup);
4558
4740
 
4559
4741
  return markup;
4560
4742
  }
@@ -4573,7 +4755,7 @@
4573
4755
  );
4574
4756
  }
4575
4757
 
4576
- function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
4758
+ function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
4577
4759
  let atts = [...el.attributes].map((att) => att.name);
4578
4760
  atts.forEach((att) => {
4579
4761
  if (!allowed.includes(att)) {
@@ -4591,13 +4773,13 @@
4591
4773
  });
4592
4774
  }
4593
4775
 
4594
- function stringifySVG(svg){
4595
- let markup = new XMLSerializer().serializeToString(svg);
4776
+ function stringifySVG(svg) {
4777
+ let markup = new XMLSerializer().serializeToString(svg);
4596
4778
  markup = markup
4597
- .replace(/\t/g, "")
4598
- .replace(/[\n\r|]/g, "\n")
4599
- .replace(/\n\s*\n/g, '\n')
4600
- .replace(/ +/g, ' ');
4779
+ .replace(/\t/g, "")
4780
+ .replace(/[\n\r|]/g, "\n")
4781
+ .replace(/\n\s*\n/g, '\n')
4782
+ .replace(/ +/g, ' ');
4601
4783
 
4602
4784
  return markup
4603
4785
  }
@@ -4607,12 +4789,19 @@
4607
4789
  tolerance = 1
4608
4790
  } = {}) {
4609
4791
 
4792
+ // min size threshold for corners
4793
+ threshold *= tolerance;
4794
+
4610
4795
  let l = pathData.length;
4611
4796
 
4612
4797
  // add fist command
4613
4798
  let pathDataN = [pathData[0]];
4614
4799
 
4615
4800
  let isClosed = pathData[l - 1].type.toLowerCase() === 'z';
4801
+ let zIsLineto = isClosed ?
4802
+ (pathData[l-1].p.x === pathData[0].p0.x && pathData[l-1].p.y === pathData[0].p0.y)
4803
+ : false ;
4804
+
4616
4805
  let lastOff = isClosed ? 2 : 1;
4617
4806
 
4618
4807
  let comLast = pathData[l - lastOff];
@@ -4621,7 +4810,7 @@
4621
4810
  let firstIsLine = pathData[1].type === 'L';
4622
4811
  let firstIsBez = pathData[1].type === 'C';
4623
4812
 
4624
- let normalizeClose = isClosed && firstIsBez;
4813
+ let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
4625
4814
 
4626
4815
  // normalize closepath to lineto
4627
4816
  if (normalizeClose) {
@@ -4638,9 +4827,8 @@
4638
4827
  // search small cubic segments enclosed by linetos
4639
4828
  if ((type === 'L' && comN && comN.type === 'C') ||
4640
4829
  (type === 'C' && comN && comN.type === 'L')
4641
-
4642
4830
  ) {
4643
- let comL0 = com;
4831
+ let comL0 = type==='L' ? com : null;
4644
4832
  let comL1 = null;
4645
4833
  let comBez = [];
4646
4834
  let offset = 0;
@@ -4653,6 +4841,11 @@
4653
4841
 
4654
4842
  }
4655
4843
 
4844
+ if(!comL0) {
4845
+ pathDataN.push(com);
4846
+ continue
4847
+ }
4848
+
4656
4849
  // closing corner to start
4657
4850
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
4658
4851
  comL1 = pathData[1];
@@ -4692,43 +4885,41 @@
4692
4885
 
4693
4886
  let signChange = (area1 < 0 && area2 > 0) || (area1 > 0 && area2 < 0);
4694
4887
 
4695
- if (comBez && !signChange && len3 < threshold && len1 > len3 && len2 > len3) {
4888
+ // exclude mid bezier segments that are larger than surrounding linetos
4889
+ let bezThresh = len3*0.5 * tolerance;
4890
+ let isSmall = bezThresh < len1 && bezThresh < len2 ;
4696
4891
 
4697
- let ptQ = checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false);
4698
- if (ptQ) {
4892
+ if (comBez.length && !signChange && isSmall ) {
4699
4893
 
4700
- /*
4701
- let dist1 = getDistAv(ptQ, comL0.p)
4702
- let dist2 = getDistAv(ptQ, comL1.p0)
4703
- let diff = Math.abs(dist1-dist2)
4704
- let rat = diff/Math.max(dist1, dist2)
4705
- console.log('rat', rat);
4706
- */
4894
+ let isFlatBezier = Math.abs(area2) <= getSquareDistance(comBez[0].p0, comBez[0].p)*0.005;
4895
+ let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p0, comL1.p, false) : null;
4707
4896
 
4708
- /*
4709
- // adjust curve start and end to meet original
4710
- let t = 1
4897
+ if (!isFlatBezier && ptQ) {
4711
4898
 
4712
- let p0_2 = pointAtT([ptQ, comL0.p], t)
4899
+ // final check: mid point proximity
4900
+ let ptM = pointAtT([comL0.p, ptQ, comL1.p0], 0.5);
4713
4901
 
4714
- comL0.p = p0_2
4715
- comL0.values = [p0_2.x, p0_2.y]
4902
+ let ptM_bez = comBez.length===1 ? pointAtT( [comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5 ) : comBez[0].p ;
4716
4903
 
4717
- let p_2 = pointAtT([ptQ, comL1.p0], t)
4904
+ let dist1 = getDistAv(ptM, ptM_bez);
4718
4905
 
4719
- comL1.p0 = p_2
4906
+ // not in tolerance – rturn original command
4907
+ if(dist1>len3){
4720
4908
 
4721
- */
4909
+ pathDataN.push(com);
4910
+ } else {
4722
4911
 
4723
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4724
- comQ.p0 = comL0.p;
4725
- comQ.cp1 = ptQ;
4726
- comQ.p = comL1.p0;
4912
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] };
4913
+ comQ.p0 = comL0.p;
4914
+ comQ.cp1 = ptQ;
4915
+ comQ.p = comL1.p0;
4916
+
4917
+ // add quadratic command
4918
+ pathDataN.push(comL0, comQ);
4919
+ i += offset;
4920
+ continue;
4921
+ }
4727
4922
 
4728
- // add quadratic command
4729
- pathDataN.push(comL0, comQ);
4730
- i += offset;
4731
- continue;
4732
4923
  }
4733
4924
  }
4734
4925
  }
@@ -4744,7 +4935,7 @@
4744
4935
  }
4745
4936
 
4746
4937
  // revert close path normalization
4747
- if (normalizeClose) {
4938
+ if (normalizeClose || (isClosed && pathDataN[pathDataN.length-1].type!=='Z') ) {
4748
4939
  pathDataN.push({ type: 'Z', values: [] });
4749
4940
  }
4750
4941
 
@@ -4752,6 +4943,313 @@
4752
4943
 
4753
4944
  }
4754
4945
 
4946
+ function getArcFromPoly(pts) {
4947
+ if (pts.length < 3) return false
4948
+
4949
+ // Pick 3 well-spaced points
4950
+ let p1 = pts[0];
4951
+ let p2 = pts[Math.floor(pts.length / 2)];
4952
+ let p3 = pts[pts.length - 1];
4953
+
4954
+ let x1 = p1.x, y1 = p1.y;
4955
+ let x2 = p2.x, y2 = p2.y;
4956
+ let x3 = p3.x, y3 = p3.y;
4957
+
4958
+ let a = x1 - x2;
4959
+ let b = y1 - y2;
4960
+ let c = x1 - x3;
4961
+ let d = y1 - y3;
4962
+
4963
+ let e = ((x1 * x1 - x2 * x2) + (y1 * y1 - y2 * y2)) / 2;
4964
+ let f = ((x1 * x1 - x3 * x3) + (y1 * y1 - y3 * y3)) / 2;
4965
+
4966
+ let det = a * d - b * c;
4967
+
4968
+ if (Math.abs(det) < 1e-10) {
4969
+ console.warn("Points are collinear or numerically unstable");
4970
+ return false;
4971
+ }
4972
+
4973
+ // find center of arc
4974
+ let cx = (d * e - b * f) / det;
4975
+ let cy = (-c * e + a * f) / det;
4976
+ let centroid = { x: cx, y: cy };
4977
+
4978
+ // Radius (use start point)
4979
+ let r = getDistance(centroid, p1);
4980
+
4981
+ let angleData = getDeltaAngle(centroid, p1, p3);
4982
+ let {deltaAngle, startAngle, endAngle} = angleData;
4983
+
4984
+ return {
4985
+ centroid,
4986
+ r,
4987
+ startAngle,
4988
+ endAngle,
4989
+ deltaAngle
4990
+ };
4991
+ }
4992
+
4993
+ function refineRoundSegments(pathData, {
4994
+ threshold = 0,
4995
+ tolerance = 1,
4996
+ // take arcs or cubic beziers
4997
+ toCubic = false,
4998
+ debug = false
4999
+ } = {}) {
5000
+
5001
+ // min size threshold for corners
5002
+ threshold *= tolerance;
5003
+
5004
+ let l = pathData.length;
5005
+
5006
+ // add fist command
5007
+ let pathDataN = [pathData[0]];
5008
+
5009
+ // just for debugging
5010
+ let pathDataTest = [];
5011
+
5012
+ for (let i = 1; i < l; i++) {
5013
+ let com = pathData[i];
5014
+ let { type } = com;
5015
+ let comP = pathData[i - 1];
5016
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
5017
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
5018
+ let comN3 = pathData[i + 3] ? pathData[i + 3] : null;
5019
+ let comBez = null;
5020
+
5021
+ if ((com.type === 'C' || com.type === 'Q')) comBez = com;
5022
+ else if (comN && (comN.type === 'C' || comN.type === 'Q')) comBez = comN;
5023
+
5024
+ let cpts = comBez ? (comBez.type === 'C' ? [comBez.p0, comBez.cp1, comBez.cp2, comBez.p] : [comBez.p0, comBez.cp1, comBez.p]) : [];
5025
+
5026
+ let areaBez = 0;
5027
+ let areaLines = 0;
5028
+ let signChange = false;
5029
+ let L1, L2;
5030
+ let combine = false;
5031
+
5032
+ let p0_S, p_S;
5033
+ let poly = [];
5034
+ let pMid;
5035
+
5036
+ // 2. line-line-bezier-line-line
5037
+ if (
5038
+ comP.type === 'L' &&
5039
+ type === 'L' &&
5040
+ comBez &&
5041
+ comN2.type === 'L' &&
5042
+ comN3 && (comN3.type === 'L' || comN3.type === 'Z')
5043
+ ) {
5044
+
5045
+ L1 = [com.p0, com.p];
5046
+ L2 = [comN2.p0, comN2.p];
5047
+ p0_S = com.p0;
5048
+ p_S = comN2.p;
5049
+
5050
+ // don't allow sign changes
5051
+ areaBez = getPolygonArea(cpts, false);
5052
+ areaLines = getPolygonArea([...L1, ...L2], false);
5053
+ signChange = (areaBez < 0 && areaLines > 0) || (areaBez > 0 && areaLines < 0);
5054
+
5055
+ if (!signChange) {
5056
+
5057
+ // mid point of mid bezier
5058
+ pMid = pointAtT(cpts, 0.5);
5059
+
5060
+ // add to poly
5061
+ poly = [p0_S, pMid, p_S];
5062
+
5063
+ combine = true;
5064
+ }
5065
+
5066
+ }
5067
+
5068
+ // 1. line-bezier-bezier-line
5069
+ else if ((type === 'C' || type === 'Q') && comP.type === 'L') {
5070
+
5071
+ // 1.2 next is cubic next is lineto
5072
+ if ((comN.type === 'C' || comN.type === 'Q') && comN2.type === 'L') {
5073
+
5074
+ combine = true;
5075
+
5076
+ L1 = [comP.p0, comP.p];
5077
+ L2 = [comN2.p0, comN2.p];
5078
+ p0_S = comP.p;
5079
+ p_S = comN2.p0;
5080
+
5081
+ // mid point of mid bezier
5082
+ pMid = comBez.p;
5083
+
5084
+ // add to poly
5085
+ poly = [p0_S, comBez.p, p_S];
5086
+
5087
+ }
5088
+ }
5089
+
5090
+ /**
5091
+ * calculate either combined
5092
+ * cubic or arc commands
5093
+ */
5094
+ if (combine) {
5095
+
5096
+ // try to find center of arc
5097
+ let arcProps = getArcFromPoly(poly);
5098
+ if (arcProps) {
5099
+
5100
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
5101
+
5102
+ let xAxisRotation = 0;
5103
+ let sweep = deltaAngle > 0 ? 1 : 0;
5104
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
5105
+
5106
+ let pCM = rotatePoint(p0_S, centroid.x, centroid.y, deltaAngle * 0.5);
5107
+
5108
+ let dist2 = getDistAv(pCM, pMid);
5109
+ let thresh = getDistAv(p0_S, p_S) * 0.05;
5110
+ let bezierCommands;
5111
+
5112
+ // point is close enough
5113
+ if (dist2 < thresh) {
5114
+
5115
+ bezierCommands = arcToBezierResolved(
5116
+ {
5117
+ p0: p0_S,
5118
+ p: p_S,
5119
+ centroid,
5120
+ rx: r,
5121
+ ry: r,
5122
+ xAxisRotation,
5123
+ sweep,
5124
+ largeArc,
5125
+ deltaAngle,
5126
+ startAngle,
5127
+ endAngle
5128
+ }
5129
+ );
5130
+
5131
+ if(bezierCommands.length === 1){
5132
+
5133
+ // prefer more compact quadratic - otherwise arcs
5134
+ let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S);
5135
+
5136
+ if (comBezier.type === 'Q') {
5137
+ toCubic = true;
5138
+ }
5139
+
5140
+ com = comBezier;
5141
+ }
5142
+
5143
+ // prefer arcs if 2 cubics are required
5144
+ if (bezierCommands.length > 1) toCubic = false;
5145
+
5146
+ // return elliptic arc commands
5147
+ if (!toCubic) {
5148
+ // rewrite simplified command
5149
+ com.type = 'A';
5150
+ com.values = [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y];
5151
+ }
5152
+
5153
+ com.p0 = p0_S;
5154
+ com.p = p_S;
5155
+ com.extreme = false;
5156
+ com.corner = false;
5157
+
5158
+ // test rendering
5159
+
5160
+ if (debug) {
5161
+ // arcs
5162
+ if (!toCubic) {
5163
+ pathDataTest = [
5164
+ { type: 'M', values: [p0_S.x, p0_S.y] },
5165
+ { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
5166
+ ];
5167
+ }
5168
+ // cubics
5169
+ else {
5170
+ pathDataTest = [
5171
+ { type: 'M', values: [p0_S.x, p0_S.y] },
5172
+ ...bezierCommands
5173
+ ];
5174
+ }
5175
+
5176
+ let d = pathDataToD(pathDataTest);
5177
+ renderPath(markers, d, 'orange', '0.5%', '0.5');
5178
+ }
5179
+
5180
+ pathDataN.push(com);
5181
+ i++;
5182
+ continue
5183
+
5184
+ }
5185
+ }
5186
+ }
5187
+
5188
+ // pass through
5189
+ pathDataN.push(com);
5190
+ }
5191
+
5192
+ return pathDataN;
5193
+ }
5194
+
5195
+ function refineClosingCommand(pathData = [], {
5196
+ threshold = 0,
5197
+ } = {}) {
5198
+
5199
+ let l = pathData.length;
5200
+ let comLast = pathData[l - 1];
5201
+ let isClosed = comLast.type.toLowerCase() === 'z';
5202
+ let idxPenultimate = isClosed ? l - 2 : l - 1;
5203
+ let comPenultimate = isClosed ? pathData[idxPenultimate] : pathData[idxPenultimate];
5204
+ let valsPen = comPenultimate.values.slice(-2);
5205
+
5206
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
5207
+ let pPen = { x: valsPen[0], y: valsPen[1] };
5208
+ let dist = getDistAv(M, pPen);
5209
+
5210
+ // adjust last coordinates for better reordering
5211
+ if (dist && dist < threshold) {
5212
+
5213
+ let valsLast = pathData[idxPenultimate].values;
5214
+ let valsLastLen = valsLast.length;
5215
+ pathData[idxPenultimate].values[valsLastLen - 2] = M.x;
5216
+ pathData[idxPenultimate].values[valsLastLen - 1] = M.y;
5217
+
5218
+ // adjust cpts
5219
+ let comFirst = pathData[1];
5220
+
5221
+ if (comFirst.type === 'C' && comPenultimate.type === 'C') {
5222
+ let dx1 = Math.abs(comFirst.values[0] - comPenultimate.values[2]);
5223
+ let dy1 = Math.abs(comFirst.values[1] - comPenultimate.values[3]);
5224
+
5225
+ let dx2 = Math.abs(pathData[1].values[0] - comFirst.values[0]);
5226
+ let dy2 = Math.abs(pathData[1].values[1] - comFirst.values[1]);
5227
+
5228
+ let dx3 = Math.abs(pathData[1].values[0] - comPenultimate.values[2]);
5229
+ let dy3 = Math.abs(pathData[1].values[1] - comPenultimate.values[3]);
5230
+
5231
+ let ver = dx2 < threshold && dx3 < threshold && dy1;
5232
+ let hor = (dy2 < threshold && dy3 < threshold) && dx1;
5233
+
5234
+ if (dx1 && dx1 < threshold && ver) {
5235
+
5236
+ pathData[1].values[0] = M.x;
5237
+ pathData[idxPenultimate].values[2] = M.x;
5238
+ }
5239
+
5240
+ if (dy1 && dy1 < threshold && hor) {
5241
+
5242
+ pathData[1].values[1] = M.y;
5243
+ pathData[idxPenultimate].values[3] = M.y;
5244
+ }
5245
+
5246
+ }
5247
+ }
5248
+
5249
+ return pathData;
5250
+
5251
+ }
5252
+
4755
5253
  function svgPathSimplify(input = '', {
4756
5254
 
4757
5255
  // return svg markup or object
@@ -4770,13 +5268,15 @@
4770
5268
 
4771
5269
  simplifyBezier = true,
4772
5270
  optimizeOrder = true,
5271
+ autoClose = true,
4773
5272
  removeZeroLength = true,
5273
+ refineClosing = true,
4774
5274
  removeColinear = true,
4775
5275
  flatBezierToLinetos = true,
4776
5276
  revertToQuadratics = true,
4777
5277
 
4778
5278
  refineExtremes = true,
4779
- refineCorners = false,
5279
+ simplifyCorners = false,
4780
5280
 
4781
5281
  keepExtremes = true,
4782
5282
  keepCorners = true,
@@ -4785,6 +5285,8 @@
4785
5285
  addExtremes = false,
4786
5286
  removeOrphanSubpaths = false,
4787
5287
 
5288
+ simplifyRound = false,
5289
+
4788
5290
  // svg path optimizations
4789
5291
  decimals = 3,
4790
5292
  autoAccuracy = true,
@@ -4837,7 +5339,7 @@
4837
5339
 
4838
5340
  // stringify to compare lengths
4839
5341
 
4840
- let dStr = d.map(com=>{return `${com.type} ${com.values.join(' ')}`}).join(' ') ;
5342
+ let dStr = d.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
4841
5343
  svgSize = dStr.length;
4842
5344
 
4843
5345
  }
@@ -4902,8 +5404,9 @@
4902
5404
  // cleaned up pathData
4903
5405
 
4904
5406
  // reset array
4905
- let pathDataFlat = [];
5407
+ let pathDataPlusArr = [];
4906
5408
 
5409
+ // loop sub paths
4907
5410
  for (let i = 0; i < lenSub; i++) {
4908
5411
 
4909
5412
  let pathDataSub = subPathArr[i];
@@ -4927,6 +5430,9 @@
4927
5430
 
4928
5431
  // simplify beziers
4929
5432
  let { pathData, bb, dimA } = pathDataPlus;
5433
+
5434
+ if (refineClosing) pathData = refineClosingCommand(pathData, { threshold: dimA * 0.001 });
5435
+
4930
5436
  pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4931
5437
 
4932
5438
  // refine extremes
@@ -4940,7 +5446,7 @@
4940
5446
 
4941
5447
  let thresh = 1;
4942
5448
 
4943
- for(let c=0, l=pathData.length; c<l; c++){
5449
+ for (let c = 0, l = pathData.length; c < l; c++) {
4944
5450
  let com = pathData[c];
4945
5451
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4946
5452
  if (type === 'C') {
@@ -4953,6 +5459,9 @@
4953
5459
  // combine adjacent cubics
4954
5460
  pathData = combineArcs(pathData);
4955
5461
 
5462
+ /*
5463
+ */
5464
+
4956
5465
  }
4957
5466
 
4958
5467
  // post processing: remove flat beziers
@@ -4961,15 +5470,18 @@
4961
5470
  }
4962
5471
 
4963
5472
  // refine corners
4964
- if(refineCorners){
5473
+ if (simplifyCorners) {
5474
+
4965
5475
  let threshold = (bb.width + bb.height) / 2 * 0.1;
4966
5476
  pathData = refineRoundedCorners(pathData, { threshold, tolerance });
4967
-
4968
5477
  }
4969
5478
 
5479
+ // refine round segment sequences
5480
+ if (simplifyRound) pathData = refineRoundSegments(pathData);
5481
+
4970
5482
  // simplify to quadratics
4971
5483
  if (revertToQuadratics) {
4972
- for(let c=0, l=pathData.length; c<l; c++){
5484
+ for (let c = 0, l = pathData.length; c < l; c++) {
4973
5485
  let com = pathData[c];
4974
5486
  let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4975
5487
  if (type === 'C') {
@@ -4987,15 +5499,22 @@
4987
5499
  }
4988
5500
 
4989
5501
  // optimize close path
4990
- if (optimizeOrder) pathData = optimizeClosePath(pathData);
5502
+ if (optimizeOrder) pathData = optimizeClosePath(pathData, { autoClose });
4991
5503
 
4992
5504
  // update
4993
- pathDataFlat.push(...pathData);
5505
+
5506
+ pathDataPlusArr.push({ pathData, bb });
4994
5507
 
4995
5508
  }
4996
5509
 
5510
+ // sort to top left
5511
+ if (optimizeOrder) pathDataPlusArr = pathDataPlusArr.sort((a, b) => a.bb.y - b.bb.y || a.bb.x - b.bb.x);
5512
+
4997
5513
  // flatten compound paths
4998
- pathData = pathDataFlat;
5514
+ pathData = [];
5515
+ pathDataPlusArr.forEach(sub => {
5516
+ pathData.push(...sub.pathData);
5517
+ });
4999
5518
 
5000
5519
  if (autoAccuracy) {
5001
5520
  decimals = detectAccuracy(pathData);
@@ -5095,11 +5614,7 @@
5095
5614
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
5096
5615
  } = Math;
5097
5616
 
5098
- /*
5099
- import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5100
- export {XMLSerializerPoly as XMLSerializerPoly};
5101
- export {DOMParserPoly as DOMParserPoly};
5102
- */
5617
+ // just for visual debugging
5103
5618
 
5104
5619
  // IIFE
5105
5620
  if (typeof window !== 'undefined') {