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
@@ -0,0 +1,420 @@
1
+ import { checkLineIntersection, getAngle, getDistAv, getDistManhattan, getDistance, getPointOnEllipse, getSquareDistance, pointAtT, rotatePoint } from "./geometry";
2
+ import { getPolygonArea } from "./geometry_area";
3
+ import { getArcFromPoly } from "./geometry_deduceRadius";
4
+ import { arcToBezierResolved, convertSmallArcsToLinetos, revertCubicQuadratic } from "./pathData_convert";
5
+ import { pathDataToD } from "./pathData_stringify";
6
+ import { renderPath, renderPoint, renderPoly } from "./visualize";
7
+
8
+
9
+ export function simplifyAdjacentRound(pathData, {
10
+ threshold = 0,
11
+ tolerance = 1,
12
+ // take arcs or cubic beziers
13
+ toCubic = false,
14
+ debug = false
15
+ } = {}) {
16
+
17
+
18
+ // fix small Arcs
19
+ pathData = convertSmallArcsToLinetos(pathData);
20
+ //return pathData
21
+
22
+ // min size threshold for corners
23
+ threshold *= tolerance;
24
+
25
+ let l = pathData.length;
26
+
27
+ // add fist command
28
+ let pathDataN = [pathData[0]]
29
+
30
+
31
+ // find adjacent cubics between extremes
32
+ //console.log(pathData);
33
+
34
+ for (let i = 1; i < l; i++) {
35
+ let comPrev = pathData[i - 1];
36
+ let com = pathData[i];
37
+ let comN = pathData[i + 1] || null;
38
+
39
+ if (!comN) {
40
+ pathDataN.push(com);
41
+ break
42
+ }
43
+
44
+ let { type, extreme = false, p0, p, dimA = 0 } = com;
45
+ // for short segment detection
46
+ let dimAN = comN.dimA;
47
+ let dimA0 = dimA + dimAN;
48
+ let thresh = 0.1
49
+ let extreme0 = extreme
50
+
51
+ // ignore short linetos
52
+ let isShortN = dimAN < dimA0 * thresh;
53
+ //let isFlat =
54
+
55
+
56
+ // adjacent cubic commands - accept short in between linetos
57
+ if ((type === 'C') && (comN.type === 'C' || isShortN)) {
58
+
59
+ //console.log((comN.type !== 'C' && isShortN), comN);
60
+ let candidates = []
61
+
62
+ for (let j = i + 1; j < l; j++) {
63
+ let comN = pathData[j];
64
+ let { type, extreme = false, corner = false, dimA = 0 } = comN;
65
+ let isShort = dimA < dimA0 * thresh;
66
+
67
+ // skip for type change(unless very short), extremes or corners
68
+ /*
69
+ if ( (comN.extreme || comN.corner) ) {
70
+ if(!extreme && !corner) candidates.push(comN)
71
+ break;
72
+ }
73
+ */
74
+
75
+ //|| (type !== 'C' && !isShort && !corner && !extreme)
76
+ if (extreme || corner) {
77
+
78
+ /*
79
+ if (comN.extreme) {
80
+ renderPoint(markers, comN.p, 'cyan')
81
+ }
82
+ else if (comN.corner) {
83
+ renderPoint(markers, comN.p, 'magenta')
84
+ }
85
+ else if (type !== 'C') {
86
+ console.log(type);
87
+ renderPoint(markers, comN.p, 'orange')
88
+ }
89
+ */
90
+
91
+ if ((extreme || corner) && type === 'C') {
92
+ //renderPoint(markers, com.p, 'purple')
93
+ //break
94
+ }
95
+
96
+ //&& comN.type !== 'C'
97
+ if (isShort && comN.type !== 'C') {
98
+ //renderPoint(markers, comN.p, 'purple')
99
+ //candidates.push(comN)
100
+ }
101
+
102
+
103
+ if ((extreme && !corner)) {
104
+ //console.log(comN);
105
+ //if(extreme) renderPoint(markers, comN.p0, 'purple')
106
+ //candidates.push(comN)
107
+ candidates.push(comN)
108
+ }
109
+
110
+ break;
111
+ }
112
+
113
+
114
+ candidates.push(comN)
115
+ }
116
+
117
+ // try to create arc command
118
+ if (candidates.length > 1) {
119
+
120
+ let clen = candidates.length;
121
+ let pts = [com.p0, com.p,];
122
+
123
+ // add interpolated points to prevent wrong arc replacements
124
+ candidates.forEach(c => {
125
+ if (c.type === 'C') {
126
+ let pt = pointAtT([c.p0, c.cp1, c.cp2, c.p], 0.5)
127
+ pts.push(pt)
128
+ }
129
+ pts.push(c.p)
130
+ })
131
+
132
+ //let pts = [com.p0, com.p, ...candidates.map(com => com.p)];
133
+ //console.log('pts', pts);
134
+
135
+ let precise = true
136
+ let arcProps = getArcFromPoly(pts, precise)
137
+
138
+ // could be combined
139
+ if (arcProps) {
140
+ //console.log(arcProps, pts);
141
+
142
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
143
+ let sweep = deltaAngle > 0 ? 1 : 0;
144
+ //let area = getPolygonArea(pts)
145
+ //let sweep = area > 0 ? 1 : 0;
146
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
147
+ largeArc = 0;
148
+ let comLast = candidates[clen - 1]
149
+ let p = comLast.p
150
+
151
+ let comArc = { type: 'A', values: [r, r, 0, largeArc, sweep, p.x, p.y] }
152
+
153
+ //console.log(comArc);
154
+
155
+ comArc.dimA = getDistManhattan(p0, p)
156
+ comArc.p0 = p0
157
+ comArc.p = p
158
+ comArc.error = 0
159
+ comArc.directionChange = comLast.directionChange
160
+ comArc.extreme = comLast.extreme
161
+ comArc.corner = comLast.corner
162
+ pathDataN.push(comArc)
163
+
164
+ i += candidates.length
165
+ continue
166
+
167
+ }
168
+
169
+ // arc radius calculation failed - return original
170
+ else {
171
+ pathDataN.push(com)
172
+ }
173
+ }
174
+
175
+ // could not be simplified – return original command
176
+ else {
177
+ pathDataN.push(com)
178
+ }
179
+
180
+ }
181
+ // all other commands
182
+ else {
183
+ pathDataN.push(com)
184
+ }
185
+ }
186
+
187
+ //console.log(pathDataN);
188
+ return pathDataN
189
+ }
190
+
191
+
192
+ export function refineRoundSegments(pathData, {
193
+ threshold = 0,
194
+ tolerance = 1,
195
+ // take arcs or cubic beziers
196
+ toCubic = false,
197
+ debug = false
198
+ } = {}) {
199
+
200
+
201
+ // min size threshold for corners
202
+ threshold *= tolerance;
203
+
204
+ let l = pathData.length;
205
+
206
+ // add fist command
207
+ let pathDataN = [pathData[0]]
208
+
209
+ // just for debugging
210
+ let pathDataTest = []
211
+
212
+ for (let i = 1; i < l; i++) {
213
+ let com = pathData[i];
214
+ let { type } = com;
215
+ let comP = pathData[i - 1];
216
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
217
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
218
+ let comN3 = pathData[i + 3] ? pathData[i + 3] : null;
219
+ let comBez = null;
220
+
221
+ if ((com.type === 'C' || com.type === 'Q')) comBez = com;
222
+ else if (comN && (comN.type === 'C' || comN.type === 'Q')) comBez = comN;
223
+
224
+
225
+ let cpts = comBez ? (comBez.type === 'C' ? [comBez.p0, comBez.cp1, comBez.cp2, comBez.p] : [comBez.p0, comBez.cp1, comBez.p]) : []
226
+
227
+ let areaBez = 0;
228
+ let areaLines = 0;
229
+ let signChange = false;
230
+ let L1, L2;
231
+ let combine = false
232
+
233
+ let p0_S, p_S;
234
+ let poly = []
235
+ let pMid;
236
+
237
+
238
+ // 2. line-line-bezier-line-line
239
+ if (
240
+ comN2 && comN3 &&
241
+ comP.type === 'L' &&
242
+ type === 'L' &&
243
+ comBez &&
244
+ comN2.type === 'L' &&
245
+ (comN3.type === 'L' || comN3.type === 'Z')
246
+ ) {
247
+
248
+ L1 = [com.p0, com.p];
249
+ L2 = [comN2.p0, comN2.p];
250
+ p0_S = com.p0
251
+ p_S = comN2.p
252
+
253
+ // don't allow sign changes
254
+ areaBez = getPolygonArea(cpts, false)
255
+ areaLines = getPolygonArea([...L1, ...L2], false)
256
+ signChange = (areaBez < 0 && areaLines > 0) || (areaBez > 0 && areaLines < 0)
257
+
258
+ if (!signChange) {
259
+
260
+ // mid point of mid bezier
261
+ pMid = pointAtT(cpts, 0.5)
262
+
263
+ // add to poly
264
+ poly = [p0_S, pMid, p_S]
265
+
266
+ combine = true
267
+ }
268
+
269
+ }
270
+
271
+ // 1. line-bezier-bezier-line
272
+ else if (comN && (type === 'C' || type === 'Q') && comP.type === 'L') {
273
+
274
+ //renderPoint(markers, com.p)
275
+
276
+ // 1.2 next is cubic next is lineto
277
+ if (comN2 && comN2.type === 'L' && (comN.type === 'C' || comN.type === 'Q')) {
278
+
279
+ combine = true
280
+
281
+ L1 = [comP.p0, comP.p];
282
+ L2 = [comN2.p0, comN2.p];
283
+ p0_S = comP.p
284
+ p_S = comN2.p0
285
+
286
+ // mid point of mid bezier
287
+ pMid = comBez.p
288
+
289
+ // add to poly
290
+ poly = [p0_S, comBez.p, p_S]
291
+
292
+
293
+ }
294
+ }
295
+
296
+
297
+ /**
298
+ * calculate either combined
299
+ * cubic or arc commands
300
+ */
301
+ if (combine) {
302
+
303
+
304
+ // try to find center of arc
305
+ let arcProps = getArcFromPoly(poly)
306
+ if (arcProps) {
307
+
308
+ let { centroid, r, deltaAngle, startAngle, endAngle } = arcProps;
309
+
310
+ let xAxisRotation = 0;
311
+ let sweep = deltaAngle > 0 ? 1 : 0;
312
+ let largeArc = Math.abs(deltaAngle) > Math.PI ? 1 : 0;
313
+
314
+ let pCM = rotatePoint(p0_S, centroid.x, centroid.y, deltaAngle * 0.5)
315
+
316
+
317
+ let dist2 = getDistAv(pCM, pMid)
318
+ let thresh = getDistAv(p0_S, p_S) * 0.05
319
+ let bezierCommands;
320
+
321
+ // point is close enough
322
+ if (dist2 < thresh) {
323
+
324
+ //toCubic = false;
325
+
326
+ bezierCommands = arcToBezierResolved(
327
+ {
328
+ p0: p0_S,
329
+ p: p_S,
330
+ centroid,
331
+ rx: r,
332
+ ry: r,
333
+ xAxisRotation,
334
+ sweep,
335
+ largeArc,
336
+ deltaAngle,
337
+ startAngle,
338
+ endAngle
339
+ }
340
+ );
341
+
342
+ if (bezierCommands.length === 1) {
343
+
344
+ // prefer more compact quadratic - otherwise arcs
345
+ let comBezier = revertCubicQuadratic(p0_S, bezierCommands[0].cp1, bezierCommands[0].cp2, p_S)
346
+
347
+ if (comBezier.type === 'Q') {
348
+ toCubic = true
349
+ }else{
350
+ comBezier = bezierCommands[0]
351
+ }
352
+
353
+ com = comBezier
354
+ //console.log('bezierCommands', comBezier);
355
+
356
+ }
357
+
358
+
359
+ // prefer arcs if 2 cubics are required
360
+ if (bezierCommands.length > 1) toCubic = false;
361
+
362
+
363
+ //toCubic = false
364
+
365
+ // return elliptic arc commands
366
+ if (!toCubic) {
367
+ // rewrite simplified command
368
+ com.type = 'A'
369
+ com.values = [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y];
370
+ }
371
+
372
+ //console.log(com);
373
+
374
+ com.p0 = p0_S;
375
+ com.p = p_S;
376
+ com.extreme = false;
377
+ com.corner = false;
378
+
379
+ // test rendering
380
+ //debug=true
381
+
382
+ /*
383
+ if (debug) {
384
+ // arcs
385
+ if (!toCubic) {
386
+ pathDataTest = [
387
+ { type: 'M', values: [p0_S.x, p0_S.y] },
388
+ { type: 'A', values: [r, r, xAxisRotation, largeArc, sweep, p_S.x, p_S.y] },
389
+ ]
390
+ }
391
+ // cubics
392
+ else {
393
+ pathDataTest = [
394
+ { type: 'M', values: [p0_S.x, p0_S.y] },
395
+ ...bezierCommands
396
+ ]
397
+
398
+ }
399
+
400
+ let d = pathDataToD(pathDataTest);
401
+ renderPath(markers, d, 'orange', '0.5%', '0.5')
402
+ }
403
+ */
404
+
405
+ pathDataN.push(com);
406
+ i++
407
+ continue
408
+
409
+ }
410
+ }
411
+ }
412
+
413
+ // pass through
414
+ pathDataN.push(com)
415
+ }
416
+
417
+ //let d= pathDataToD(pathDataN)
418
+ //console.log('!pathDataN', d);
419
+ return pathDataN;
420
+ }
@@ -8,6 +8,42 @@ import { getDistAv, getDistManhattan } from "./geometry";
8
8
 
9
9
 
10
10
 
11
+ /**
12
+ * round path data
13
+ * either by explicit decimal value or
14
+ * based on suggested accuracy in path data
15
+ */
16
+ export function roundPathData(pathData, decimalsGlobal = -1) {
17
+
18
+ if (decimalsGlobal < 0) return pathData;
19
+
20
+ let len = pathData.length;
21
+ let decimals = decimalsGlobal
22
+ let decimalsArc = decimals < 3 ? decimals+2 : decimals
23
+ //decimalsArc = decimals
24
+ //console.log(decimalsArc);
25
+
26
+ for (let c = 0; c < len; c++) {
27
+ let com = pathData[c];
28
+ let { type, values } = com
29
+ let valLen = values.length;
30
+ if (!valLen) continue
31
+
32
+ let isArc = type.toLowerCase() === 'a'
33
+
34
+ for (let v = 0; v < valLen; v++) {
35
+ // allow higher accuracy for arc radii (... it's always arcs)
36
+ pathData[c].values[v] = isArc && v < 2 ? roundTo(values[v], decimalsArc) : roundTo(values[v], decimals);
37
+ }
38
+ };
39
+
40
+ //console.log(pathData);
41
+ return pathData;
42
+ }
43
+
44
+
45
+
46
+
11
47
  export function detectAccuracyPoly(pts) {
12
48
 
13
49
  let minDim = Infinity
@@ -20,7 +56,7 @@ export function detectAccuracyPoly(pts) {
20
56
  let { p0 = null, p = null, dimA = 0 } = pt;
21
57
 
22
58
  // use existing averave dimension value or calculate
23
- if ( p && p0) {
59
+ if (p && p0) {
24
60
  dimA = dimA ? dimA : getDistManhattan(p0, p);
25
61
 
26
62
  if (dimA) dims.push(dimA);
@@ -84,11 +120,37 @@ export function detectAccuracy(pathData) {
84
120
 
85
121
  }
86
122
 
123
+ /**
124
+ * rounding helper
125
+ * allows for quantized rounding
126
+ * e.g 0.5 decimals s
127
+ */
128
+ export function roundTo(num = 0, decimals = 3) {
129
+ if (decimals < 0) return num;
130
+ // Normal integer rounding
131
+ if (!decimals) return Math.round(num);
132
+
133
+ // stepped rounding
134
+ let intPart = Math.floor(decimals);
135
+ //let fracPart = decimals.toString().split('.');
136
+ //fracPart = fracPart[1] ? +fracPart[1] : 0
137
+
138
+ if (intPart !== decimals) {
139
+ let f = +(decimals - intPart).toFixed(2)
140
+ f = f > 0.5 ? (Math.floor((f) / 0.5) * 0.5) : f;
141
+ //console.log('fracPart', f);
142
+ let step = 10 ** -intPart * f;
143
+ return +(Math.round(num / step) * step).toFixed(8);
144
+ }
145
+
146
+ let factor = 10 ** decimals;
147
+ return Math.round(num * factor) / factor;
148
+ }
87
149
 
88
150
 
89
151
 
90
- export function roundTo(num = 0, decimals = 3) {
91
- if(decimals<=-1) return num;
152
+ export function roundTo__(num = 0, decimals = 3) {
153
+ if (decimals <= -1) return num;
92
154
  if (!decimals) return Math.round(num);
93
155
  let factor = 10 ** decimals;
94
156
  return Math.round(num * factor) / factor;
@@ -99,85 +161,24 @@ export function roundTo(num = 0, decimals = 3) {
99
161
  * floating point accuracy
100
162
  * based on numeric value
101
163
  */
102
- export function autoRound(val, integerThresh = 50){
103
- let decimals=8;
104
-
105
- if(val>integerThresh*2){
106
- decimals=0
107
- }
108
- else if(val>integerThresh){
109
- decimals=1
110
- }else{
111
- decimals=Math.ceil(500/val).toString().length
112
- //console.log('decimals small', val, decimals);
113
- }
114
-
115
- //console.log(val, decimals);
116
- let factor = 10 ** decimals;
117
- return Math.round(val * factor) / factor;
118
- }
119
-
120
-
121
-
122
-
123
- /**
124
- * round path data
125
- * either by explicit decimal value or
126
- * based on suggested accuracy in path data
127
- */
128
- export function roundPathData(pathData, decimalsGlobal = -1) {
164
+ export function autoRound(val, integerThresh = 50) {
165
+ let decimals = 8;
129
166
 
130
- if (decimalsGlobal < 0) return pathData;
131
-
132
- let len = pathData.length;
133
- //let decimals = pathData[0].decimals ? pathData[0].decimals+1 : decimalsGlobal
134
- let decimals = decimalsGlobal
135
- //decimals = decimalsGlobal;
136
- //console.log('decimals subpath', decimals, pathData[0].decimals, 'decimalsGlobal', decimalsGlobal);
137
-
138
- for (let c = 0; c < len; c++) {
139
- let com = pathData[c];
140
- let {values} = com
141
- //let values = pathData[c].values
142
- let valLen = values.length;
143
- if (!valLen) continue
144
-
145
- for (let v = 0; v < valLen; v++) {
146
- pathData[c].values[v] = roundTo(values[v], decimals);
147
- }
148
- };
167
+ if (val > integerThresh * 2) {
168
+ decimals = 0
169
+ }
170
+ else if (val > integerThresh) {
171
+ decimals = 1
172
+ } else {
173
+ decimals = Math.ceil(500 / val).toString().length
174
+ //console.log('decimals small', val, decimals);
175
+ }
149
176
 
150
- //console.log(pathData);
151
- return pathData;
177
+ //console.log(val, decimals);
178
+ let factor = 10 ** decimals;
179
+ return Math.round(val * factor) / factor;
152
180
  }
153
181
 
154
182
 
155
- export function roundPathData_(pathData, decimals = -1) {
156
-
157
- if (decimals < 0) return pathData;
158
-
159
- let len = pathData.length;
160
- let c = 0;
161
- while (c < len) {
162
-
163
- //let com = pathData[c];
164
- let values = pathData[c].values
165
- let valLen = values.length;
166
-
167
- // Z commands have no values
168
- if (!valLen) {
169
- c++; continue
170
- }
171
-
172
- let v = 0;
173
- while (v < valLen) {
174
- pathData[c].values[v] = roundTo(values[v], decimals);
175
- v++
176
- }
177
- c++
178
183
 
179
- };
180
184
 
181
- //console.log(pathData);
182
- return pathData;
183
- }