svg-path-simplify 0.4.3 → 0.4.4

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -0
  3. package/dist/svg-path-simplify.esm.js +1610 -495
  4. package/dist/svg-path-simplify.esm.min.js +2 -2
  5. package/dist/svg-path-simplify.js +1611 -494
  6. package/dist/svg-path-simplify.min.js +2 -2
  7. package/dist/svg-path-simplify.pathdata.esm.js +893 -456
  8. package/dist/svg-path-simplify.pathdata.esm.min.js +2 -2
  9. package/dist/svg-path-simplify.poly.cjs +9 -8
  10. package/index.html +58 -17
  11. package/package.json +1 -1
  12. package/src/constants.js +4 -0
  13. package/src/detect_input.js +47 -29
  14. package/src/index.js +8 -0
  15. package/src/pathData_simplify_cubic.js +26 -16
  16. package/src/pathData_simplify_revertToquadratics.js +0 -1
  17. package/src/pathSimplify-main.js +75 -20
  18. package/src/pathSimplify-only-pathdata.js +7 -2
  19. package/src/pathSimplify-presets.js +15 -4
  20. package/src/svg-getAttributes.js +4 -2
  21. package/src/svgii/convert_units.js +1 -1
  22. package/src/svgii/geometry.js +140 -2
  23. package/src/svgii/geometry_bbox_element.js +1 -1
  24. package/src/svgii/geometry_deduceRadius.js +116 -27
  25. package/src/svgii/geometry_length.js +17 -1
  26. package/src/svgii/pathData_analyze.js +18 -0
  27. package/src/svgii/pathData_convert.js +188 -88
  28. package/src/svgii/pathData_fix_directions.js +10 -18
  29. package/src/svgii/pathData_reorder.js +122 -16
  30. package/src/svgii/pathData_simplify_refineCorners.js +130 -35
  31. package/src/svgii/pathData_simplify_refine_round.js +420 -0
  32. package/src/svgii/rounding.js +79 -78
  33. package/src/svgii/svg_cleanup.js +68 -20
  34. package/src/svgii/svg_cleanup_convertPathLength.js +22 -15
  35. package/src/svgii/svg_cleanup_remove_els_and_atts.js +6 -1
  36. package/src/svgii/svg_el_parse_style_props.js +13 -10
  37. package/src/svgii/svg_validate.js +220 -0
  38. package/tests/testSVG.js +14 -1
  39. package/src/svgii/pathData_refine_round.js +0 -222
@@ -14,7 +14,6 @@ export function pathDataToTopLeft(pathData) {
14
14
  let len = pathData.length;
15
15
  let isClosed = pathData[len - 1].type.toLowerCase() === 'z'
16
16
 
17
- //return pathData;
18
17
 
19
18
  // we can't change starting point for non closed paths
20
19
  if (!isClosed) {
@@ -30,7 +29,7 @@ export function pathDataToTopLeft(pathData) {
30
29
  let { type, values } = com;
31
30
  let valsLen = values.length
32
31
  if (valsLen) {
33
- let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0}
32
+ let p = { type: type, x: values[valsLen - 2], y: values[valsLen - 1], index: 0 }
34
33
  p.index = i
35
34
  indices.push(p)
36
35
  }
@@ -38,16 +37,124 @@ export function pathDataToTopLeft(pathData) {
38
37
 
39
38
  // reorder to top left most
40
39
  //|| a.x - b.x
41
- indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
40
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x - b.x);
42
41
  newIndex = indices[0].index
43
42
 
44
- return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
43
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
45
44
  }
46
45
 
47
46
 
47
+ export function optimizeClosePath(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
48
48
 
49
+ let pathDataN = pathData;
50
+ let l = pathData.length;
51
+ let M = { x: +pathData[0].values[0].toFixed(8), y: +pathData[0].values[1].toFixed(8) }
52
+ let isClosed = pathData[l - 1].type.toLowerCase() === 'z'
53
+ //let linetos = pathData.filter(com => com.type === 'L')
54
+ //let hasLinetos = linetos.length;
55
+ let hasLinetos = false
56
+
57
+
58
+ // check if path is closed by explicit lineto
59
+ let idxPenultimate = isClosed ? l - 2 : l - 1
60
+ let penultimateCom = pathData[idxPenultimate];
61
+ let penultimateType = penultimateCom.type;
62
+ let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8))
63
+
64
+ // last L command ends at M
65
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
66
+ let lastIsLine = penultimateType === 'L'
67
+ //console.log(pathData);
68
+
69
+ // create index
70
+ let indices = [];
71
+ for (let i = 0; i < l; i++) {
72
+ let com = pathData[i];
73
+ let { type, values, p0, p } = com;
74
+
75
+ if(type==='L') hasLinetos = true;
76
+
77
+ // exclude Z
78
+ if (values.length) {
79
+ let valsL = values.slice(-2)
80
+
81
+ let x = Math.min(p0.x, p.x)
82
+ let y = Math.min(p0.y, p.y)
83
+
84
+ let prevCom = pathData[i - 1] ? pathData[i - 1] : pathData[idxPenultimate]
85
+ let prevType = prevCom.type
86
+ //let p = { type: type, x: valsL[0], y: valsL[1], dist: 0, index: 0, prevType }
87
+ let item = { type: type, x, y, index: 0, prevType }
88
+ item.index = i
89
+ indices.push(item)
90
+ }
91
+
92
+ }
93
+
94
+ let xMin = Infinity;
95
+ let yMin = Infinity;
96
+ let idx_top = null;
97
+ let len = indices.length
98
+
99
+
100
+ for (let i = 0; i < len; i++) {
101
+ let com = indices[i];
102
+ let { type, index, x, y, prevType } = com;
103
+
104
+ if (hasLinetos && prevType === 'L') {
105
+ if (x < xMin && y < yMin) {
106
+ idx_top = index-1;
107
+ }
108
+
109
+ if (y < yMin) {
110
+ yMin = y
111
+ }
112
+
113
+ if (x < xMin) {
114
+ xMin = x
115
+ }
116
+ }
117
+ }
118
+
119
+
120
+ // shift to better starting point
121
+ if (idx_top) {
122
+ pathDataN = shiftSvgStartingPoint(pathDataN, idx_top)
123
+
124
+ // update penultimate - reorder might have added new close paths
125
+ l = pathDataN.length
126
+ M = { x: +pathDataN[0].values[0].toFixed(8), y: +pathDataN[0].values[1].toFixed(8) }
127
+
128
+ idxPenultimate = isClosed ? l - 2 : l - 1
129
+ penultimateCom = pathDataN[idxPenultimate];
130
+ penultimateType = penultimateCom.type;
131
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8))
132
+ lastIsLine = penultimateType ==='L'
133
+
134
+ // last L command ends at M
135
+ hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
136
+
137
+ }
49
138
 
50
- export function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose = true}={}) {
139
+
140
+ // remove unnecessary closing lineto
141
+ if (removeFinalLineto && hasClosingCommand && lastIsLine) {
142
+ pathDataN.splice(l - 2, 1)
143
+ }
144
+
145
+ // add close path
146
+ if (autoClose && !isClosed && hasClosingCommand) {
147
+ pathDataN.push({ type: 'Z', values: [] })
148
+ }
149
+
150
+ return pathDataN
151
+
152
+ }
153
+
154
+
155
+
156
+
157
+ export function optimizeClosePath__(pathData, { removeFinalLineto = true, autoClose = true } = {}) {
51
158
 
52
159
  let pathDataNew = [];
53
160
  let l = pathData.length;
@@ -58,17 +165,16 @@ export function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose
58
165
 
59
166
 
60
167
  // check if order is ideal
61
- let idxPenultimate = isClosed ? l-2 : l-1
62
-
168
+ let idxPenultimate = isClosed ? l - 2 : l - 1
63
169
  let penultimateCom = pathData[idxPenultimate];
64
170
  let penultimateType = penultimateCom.type;
65
171
  let penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8))
66
172
 
67
173
  // last L command ends at M
68
- let isClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
174
+ let hasClosingCommand = penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
69
175
 
70
176
  // add closepath Z to enable order optimizations
71
- if(!isClosed && autoClose && isClosingCommand){
177
+ if (!isClosed && autoClose && hasClosingCommand) {
72
178
 
73
179
  /*
74
180
  // adjust final coords
@@ -77,14 +183,14 @@ export function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose
77
183
  pathData[idxPenultimate].values[valsLastLen-2] = M.x
78
184
  pathData[idxPenultimate].values[valsLastLen-1] = M.y
79
185
  */
80
-
81
- pathData.push({type:'Z', values:[]})
186
+
187
+ pathData.push({ type: 'Z', values: [] })
82
188
  isClosed = true;
83
189
  l++
84
190
  }
85
191
 
86
192
  // if last segment is not closing or a lineto
87
- let skipReorder = pathData[1].type !== 'L' && (!isClosingCommand || penultimateCom.type === 'L')
193
+ let skipReorder = pathData[1].type !== 'L' && (!hasClosingCommand || penultimateCom.type === 'L')
88
194
  skipReorder = false
89
195
 
90
196
 
@@ -143,13 +249,13 @@ export function optimizeClosePath(pathData, {removeFinalLineto = true, autoClose
143
249
  // remove last lineto
144
250
  penultimateCom = pathData[l - 2];
145
251
  penultimateType = penultimateCom.type;
146
- penultimateComCoords = penultimateCom.values.slice(-2).map(val=>+val.toFixed(8))
252
+ penultimateComCoords = penultimateCom.values.slice(-2).map(val => +val.toFixed(8))
147
253
 
148
- isClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
254
+ hasClosingCommand = penultimateType === 'L' && penultimateComCoords[0] === M.x && penultimateComCoords[1] === M.y
149
255
 
150
- //console.log('penultimateCom', isClosingCommand, penultimateCom.values, M);
256
+ //console.log('penultimateCom', hasClosingCommand, penultimateCom.values, M);
151
257
 
152
- if (removeFinalLineto && isClosingCommand) {
258
+ if (removeFinalLineto && hasClosingCommand) {
153
259
  pathData.splice(l - 2, 1)
154
260
  }
155
261
 
@@ -1,10 +1,14 @@
1
1
  import { checkLineIntersection, getDistManhattan, getSquareDistance, interpolate, pointAtT } from "./geometry";
2
2
  import { getPolygonArea } from "./geometry_area";
3
+ import { getArcFromPoly } from "./geometry_deduceRadius";
3
4
  import { commandIsFlat } from "./geometry_flatness";
5
+ import { pathDataToD } from "./pathData_stringify";
6
+ import { roundTo } from "./rounding";
4
7
  import { renderPoint } from "./visualize";
5
8
 
6
9
  export function refineRoundedCorners(pathData, {
7
10
  threshold = 0,
11
+ simplifyQuadraticCorners = false,
8
12
  tolerance = 1
9
13
  } = {}) {
10
14
 
@@ -30,6 +34,9 @@ export function refineRoundedCorners(pathData, {
30
34
  let firstIsLine = pathData[1].type === 'L';
31
35
  let firstIsBez = pathData[1].type === 'C';
32
36
 
37
+ // in case we have simplified a corner connecting to the start
38
+ let M_adj = null;
39
+
33
40
 
34
41
  let normalizeClose = isClosed && firstIsBez && (lastIsLine || zIsLineto);
35
42
 
@@ -70,15 +77,18 @@ export function refineRoundedCorners(pathData, {
70
77
  // closing corner to start
71
78
  if (isClosed && lastIsBez && firstIsLine && i === l - lastOff - 1) {
72
79
  comL1 = pathData[1]
80
+ //???
73
81
  comBez = [pathData[l - lastOff]]
82
+
74
83
  //renderPoint(markers, com.p)
75
84
  }
76
85
 
86
+ // collect enclosed bezier segments
77
87
  for (let j = i + 1; j < l; j++) {
78
88
  let comN = pathData[j] ? pathData[j] : null;
79
89
  let comPrev = pathData[j - 1];
80
90
 
81
- if (comPrev.type === 'C') {
91
+ if (comPrev.type === 'C' && j > 2) {
82
92
  comBez.push(comPrev)
83
93
  }
84
94
 
@@ -89,9 +99,13 @@ export function refineRoundedCorners(pathData, {
89
99
  offset++
90
100
  }
91
101
 
102
+
103
+ //comBez = comBez.filter(com=> com.values.join(''))
104
+
92
105
  if (comL1) {
93
106
  //console.log('comL1', comL1);
94
107
 
108
+
95
109
  // linetos
96
110
  let len1 = getDistManhattan(comL0.p0, comL0.p)
97
111
  let len2 = getDistManhattan(comL1.p0, comL1.p)
@@ -113,43 +127,77 @@ export function refineRoundedCorners(pathData, {
113
127
  let isSmall = bezThresh < len1 && bezThresh < len2;
114
128
 
115
129
 
130
+ /*
131
+ */
132
+
133
+
116
134
  //len1 > len3 && len2 > len3
117
135
  if (comBez.length && !signChange && isSmall) {
118
136
 
119
- let isFlatBezier = Math.abs(area2) < getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005
137
+
138
+ let isSquare = false;
139
+
140
+ if (comBez.length === 1) {
141
+ let dx = Math.abs(comBez[0].p.x - comBez[0].p0.x)
142
+ let dy = Math.abs(comBez[0].p.y - comBez[0].p0.y)
143
+ let diff = (dx - dy)
144
+ let rat = Math.abs(diff / dx)
145
+ isSquare = rat < 0.01;
146
+ }
147
+
148
+
149
+ let preferArcs = true;
150
+ preferArcs = false;
151
+
152
+
153
+ // if rectangular prefer arcs
154
+ if (preferArcs && isSquare) {
155
+
156
+ let pM = pointAtT([comBez[0].p0, comBez[0].cp1, comBez[0].cp2, comBez[0].p], 0.5)
157
+
158
+ let arcProps = getArcFromPoly([comBez[0].p0, pM, comBez[0].p])
159
+ let { r, centroid, deltaAngle } = arcProps;
160
+
161
+ let sweep = deltaAngle > 0 ? 1 : 0;
162
+ //let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
163
+ let largeArc = 0;
164
+
165
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, comBez[0].p.x, comBez[0].p.y] }
166
+
167
+ pathDataN.push(comL0, comArc);
168
+ i += offset
169
+ continue
170
+
171
+ }
172
+
173
+
174
+
175
+
176
+ let areaThresh = getSquareDistance(comBez[0].p0, comBez[0].p) * 0.005
177
+ let isFlatBezier = Math.abs(area2) < areaThresh;
178
+ let isFlatBezier2 = Math.abs(area2) < areaThresh * 10
179
+
180
+
120
181
  let ptQ = !isFlatBezier ? checkLineIntersection(comL0.p0, comL0.p, comL1.p, comL1.p0, false, true) : null
121
182
 
122
- if (!ptQ) {
183
+
184
+ // exit: is rather flat or has no intersection
185
+ //|| (isFlatBezier && comBez.length === 1)
186
+ if (!ptQ || (isFlatBezier2 && comBez.length === 1)) {
123
187
  pathDataN.push(com);
124
188
  continue
125
189
  }
126
190
 
127
- // check sign change
191
+ // check sign change - exit if present
128
192
  if (ptQ) {
129
193
  let area0 = getPolygonArea([comL0.p0, comL0.p, comL1.p0, comL1.p], false);
130
194
  let area0_abs = Math.abs(area0);
131
195
  let area1 = getPolygonArea([comL0.p0, comL0.p, ptQ, comL1.p0, comL1.p], false);
132
196
  let area1_abs = Math.abs(area1);
133
197
  let areaDiff = Math.abs(area0_abs - area1_abs) / area0_abs
134
- //console.log('areaDiff', areaDiff);
135
-
136
-
137
- /*
138
- renderPoint(markers, comL0.p0, 'green', '0.5%', '0.5')
139
- renderPoint(markers, comL0.p, 'red', '1.5%', '0.5')
140
- renderPoint(markers, comL1.p0, 'blue', '0.5%', '0.5')
141
- renderPoint(markers, comL1.p, 'orange', '0.5%', '0.5')
142
- if(!area0) {
143
- pathDataN.push(com);
144
- continue
145
- }
146
- */
147
-
148
198
  let signChange = area0 < 0 && area1 > 0 || area0 > 0 && area1 < 0;
149
199
 
150
200
  if (!ptQ || signChange || areaDiff > 0.5) {
151
- //console.log(signChange, area0, area1, 'pts', comL0.p0, comL0.p, comL1.p0, comL1.p);
152
- //renderPoint(markers, ptQ, 'cyan', '0.5%', '0.5')
153
201
  pathDataN.push(com);
154
202
  continue
155
203
  }
@@ -168,28 +216,71 @@ export function refineRoundedCorners(pathData, {
168
216
 
169
217
  // not in tolerance – return original command
170
218
  if (bezThresh && dist1 > bezThresh && dist1 > len3 * 0.3) {
171
- //renderPoint(markers, ptM_bez, 'cyan', '0.5%', '0.5')
172
- //renderPoint(markers, ptQ, 'magenta', '0.5%', '0.5')
173
219
  pathDataN.push(com);
174
220
  continue;
175
221
 
176
- } else {
222
+ }
177
223
 
178
- //renderPoint(markers, ptQ, 'magenta', '0.5%', '0.5')
179
- let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, comL1.p0.x, comL1.p0.y] }
180
- comQ.p0 = comL0.p;
181
- comQ.cp1 = ptQ;
182
- comQ.p = comL1.p0;
224
+ // return simplified quadratic Bézier command
225
+ let p_Q = comL1.p0;
183
226
 
184
- // add quadratic command
185
- pathDataN.push(comL0, comQ);
186
- i += offset;
187
- //i++
227
+ // adjust previous end point to better fit the cubic curvature
228
+ let adjustQ = !simplifyQuadraticCorners;
229
+
230
+
231
+ if (adjustQ) {
232
+ //let t = 0.1333
233
+ let t = 0.1666
234
+ let p0_adj = interpolate(ptQ, comL0.p, (1 + t))
235
+ p_Q = interpolate(ptQ, comL1.p0, (1 + t))
236
+
237
+ // round for large enough segments
238
+ let isH = ptQ.y===comL0.p.y
239
+ let isV = ptQ.x===comL0.p.x
240
+ let isH2 = ptQ.y===comL1.p0.y
241
+ let isV2 = ptQ.x===comL1.p0.x
242
+
243
+ if(isSquare && com.dimA>3){
244
+ let dec = 0.5;
245
+ if(isH) p0_adj.x = roundTo(p0_adj.x, dec)
246
+ if(isV) p0_adj.y = roundTo(p0_adj.y, dec)
247
+ if(isH2) p_Q.x = roundTo(p_Q.x, dec)
248
+ if(isV2) p_Q.y = roundTo(p_Q.y, dec)
249
+ }
250
+
251
+
252
+ /*
253
+ renderPoint(markers, p0_adj, 'orange')
254
+ renderPoint(markers, p_Q, 'orange')
255
+ renderPoint(markers, comL0.p, 'green')
256
+ renderPoint(markers, comL1.p0, 'magenta')
257
+ */
258
+
259
+ // set new M starting point
260
+ if (i === l - lastOff - 1) {
261
+ //renderPoint(markers, p0_adj, 'red')
262
+ M_adj = p_Q
263
+ }
264
+
265
+ // adjust previous lineto end point
266
+ comL0.values = [p0_adj.x, p0_adj.y]
267
+ comL0.p = p0_adj;
188
268
 
189
- //offset++
190
- continue;
191
269
  }
192
270
 
271
+ let comQ = { type: 'Q', values: [ptQ.x, ptQ.y, p_Q.x, p_Q.y] }
272
+ comQ.cp1 = ptQ;
273
+ comQ.p0 = comL0.p;
274
+ comQ.p = p_Q;
275
+
276
+ // add quadratic command
277
+ pathDataN.push(comL0, comQ);
278
+
279
+
280
+
281
+ i += offset;
282
+ continue;
283
+
193
284
  }
194
285
  }
195
286
  }
@@ -203,6 +294,11 @@ export function refineRoundedCorners(pathData, {
203
294
 
204
295
  }
205
296
 
297
+ // correct starting point connecting with last corner rounding
298
+ if (M_adj) {
299
+ pathDataN[0].values = [M_adj.x, M_adj.y]
300
+ pathDataN[0].p0 = M_adj;
301
+ }
206
302
 
207
303
 
208
304
  // revert close path normalization
@@ -210,7 +306,6 @@ export function refineRoundedCorners(pathData, {
210
306
  pathDataN.push({ type: 'Z', values: [] })
211
307
  }
212
308
 
213
-
214
309
  //console.log(pathDataN);
215
310
 
216
311
  return pathDataN;