svg-path-simplify 0.1.2 → 0.2.1

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 (46) hide show
  1. package/README.md +10 -0
  2. package/dist/svg-path-simplify.esm.js +3935 -1441
  3. package/dist/svg-path-simplify.esm.min.js +13 -1
  4. package/dist/svg-path-simplify.js +3953 -1459
  5. package/dist/svg-path-simplify.min.js +13 -1
  6. package/dist/svg-path-simplify.min.js.gz +0 -0
  7. package/dist/svg-path-simplify.poly.cjs +0 -1
  8. package/index.html +69 -31
  9. package/package.json +5 -9
  10. package/src/constants.js +3 -0
  11. package/src/index-node.js +0 -1
  12. package/src/index-poly.js +0 -1
  13. package/src/index.js +26 -0
  14. package/src/pathData_simplify_cubic.js +75 -46
  15. package/src/pathData_simplify_cubicsToArcs.js +566 -0
  16. package/src/pathData_simplify_harmonize_cpts.js +170 -0
  17. package/src/pathData_simplify_revertToquadratics.js +21 -0
  18. package/src/pathSimplify-main.js +274 -61
  19. package/src/poly-fit-curve-schneider.js +570 -0
  20. package/src/simplify_poly_RDP.js +146 -0
  21. package/src/simplify_poly_radial_distance.js +100 -0
  22. package/src/svg_getViewbox.js +28 -15
  23. package/src/svgii/geometry.js +389 -63
  24. package/src/svgii/geometry_area.js +2 -1
  25. package/src/svgii/pathData_analyze.js +259 -212
  26. package/src/svgii/pathData_convert.js +91 -663
  27. package/src/svgii/pathData_fromPoly.js +12 -0
  28. package/src/svgii/pathData_parse.js +90 -89
  29. package/src/svgii/pathData_parse_els.js +3 -0
  30. package/src/svgii/pathData_parse_fontello.js +449 -0
  31. package/src/svgii/pathData_remove_collinear.js +44 -37
  32. package/src/svgii/pathData_reorder.js +2 -1
  33. package/src/svgii/pathData_simplify_redraw.js +343 -0
  34. package/src/svgii/pathData_simplify_refineCorners.js +18 -9
  35. package/src/svgii/pathData_simplify_refineExtremes.js +19 -78
  36. package/src/svgii/pathData_split.js +42 -45
  37. package/src/svgii/pathData_toPolygon.js +130 -4
  38. package/src/svgii/pathData_transform_scale.js +51 -0
  39. package/src/svgii/poly_analyze.js +470 -14
  40. package/src/svgii/poly_to_pathdata.js +224 -19
  41. package/src/svgii/rounding.js +55 -112
  42. package/src/svgii/svg_cleanup.js +13 -1
  43. package/src/svgii/visualize.js +8 -3
  44. package/{debug.cjs → tests/debug.cjs} +3 -0
  45. package/{testSVG.js → tests/testSVG.js} +1 -1
  46. /package/{test.js → tests/test.js} +0 -0
@@ -0,0 +1,566 @@
1
+ import { checkLineIntersection, getAngle, getDistManhattan, getDistance, rotatePoint } from "./svgii/geometry";
2
+ import { getPathArea, getPolygonArea, getRelativeAreaDiff } from "./svgii/geometry_area";
3
+ //import { cubicCommandToArc } from "./svgii/pathData_convert";
4
+
5
+ export function pathDataCubicsToArc(pathData, { areaThreshold = 2.5 } = {}) {
6
+
7
+ for (let c = 0, l = pathData.length; c < l; c++) {
8
+ let com = pathData[c]
9
+ let comN = pathData[c + 1] || null
10
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
11
+
12
+ if (type === 'C' && comN && comN.type === 'C') {
13
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, areaThreshold)
14
+ let comAN = cubicCommandToArc(comN.p0, comN.cp1, comN.cp2, comN.p, areaThreshold)
15
+
16
+
17
+ if (comA.isArc && comAN.isArc) {
18
+
19
+ let dist = getDistManhattan(p0, comN.p);
20
+ let maxDist = dist * 0.01;
21
+ let dx = Math.abs(comN.p.x - p0.x)
22
+ let dy = Math.abs(comN.p.y - p0.y)
23
+
24
+ let horizontal = dy < maxDist && dx > maxDist;
25
+ let vertical = dx < maxDist && dy > maxDist;
26
+ //console.log(comA, comAN, horizontal, vertical);
27
+
28
+ let { rx, ry } = comA;
29
+ let area = getPolygonArea([p0, p, comN.p])
30
+ let sweep = area < 0 ? 0 : 1;
31
+
32
+ if (vertical || horizontal) {
33
+ rx = Math.min(rx, comAN.rx)
34
+ ry = Math.min(ry, comAN.ry)
35
+ pathData[c] = null;
36
+ pathData[c + 1].type = 'A';
37
+ pathData[c + 1].values = [rx, ry, 0, 0, sweep, comN.p.x, comN.p.y];
38
+ continue
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ pathData = pathData.filter(Boolean)
45
+
46
+ return pathData
47
+
48
+ }
49
+
50
+
51
+
52
+ export function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
53
+
54
+ //console.log(p0, cp1, cp2, p, segArea );
55
+ let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
56
+ //let pathDataChunk = [{ type: 'M', values: [p0.x, p0.y] }, com];
57
+
58
+ let arcSegArea = 0, isArc = false
59
+
60
+ // check angles
61
+ let angle1 = getAngle(p0, cp1, true);
62
+ let angle2 = getAngle(p, cp2, true);
63
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
64
+
65
+
66
+ let angleDiff = Math.abs((deltaAngle % 180) - 90);
67
+ let isRightAngle = angleDiff < 3;
68
+
69
+
70
+ let rx = 0
71
+ let ry = 0
72
+ let ptC;
73
+
74
+ if (isRightAngle) {
75
+ // point between cps
76
+
77
+
78
+ // center point
79
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
80
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
81
+
82
+ // assumed centroid
83
+ ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
84
+ //renderPoint(markers, ptC, 'red', '1%', '0.5' )
85
+
86
+
87
+
88
+
89
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
90
+
91
+ if (pI) {
92
+
93
+ let r1 = getDistance(p0, pI);
94
+ let r2 = getDistance(p, pI);
95
+
96
+ let rMax = +Math.max(r1, r2).toFixed(8);
97
+ let rMin = +Math.min(r1, r2).toFixed(8);
98
+
99
+ rx = rMin
100
+ ry = rMax
101
+
102
+ let arcArea = getPolygonArea([p0, cp1, cp2, p])
103
+ let sweep = arcArea < 0 ? 0 : 1;
104
+
105
+ let w = Math.abs(p.x - p0.x);
106
+ let h = Math.abs(p.y - p0.y);
107
+ let landscape = w > h;
108
+
109
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
110
+
111
+ if (circular) {
112
+ //rx = (rx+ry)/2
113
+ rx = rMax
114
+ ry = rx;
115
+ }
116
+
117
+ if (landscape) {
118
+ //console.log('landscape', w, h);
119
+ rx = rMax
120
+ ry = rMin
121
+ }
122
+
123
+
124
+ // get original cubic area
125
+ let comO = [
126
+ { type: 'M', values: [p0.x, p0.y] },
127
+ { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
128
+ ];
129
+
130
+ let comArea = getPathArea(comO);
131
+
132
+ // new arc command
133
+ let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
134
+
135
+ // calculate arc seg area
136
+ arcSegArea = (Math.PI * (rx * ry)) / 4
137
+
138
+ // subtract polygon between start, end and center point
139
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]))
140
+
141
+ let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
142
+
143
+ if (areaDiff < tolerance) {
144
+ isArc = true;
145
+ com = comArc;
146
+ }
147
+
148
+ }
149
+ }
150
+
151
+ return { com: com, isArc, area: arcSegArea, rx, ry, centroid: ptC }
152
+
153
+ }
154
+
155
+
156
+
157
+ /*
158
+
159
+
160
+ // combine adjacent arcs
161
+
162
+ export function combineArcs(pathData) {
163
+
164
+ let arcSeq = [[]]
165
+ let ind = 0
166
+ let arcIndices = [[]];
167
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
168
+
169
+ for (let i = 0, len = pathData.length; i < len; i++) {
170
+ let com = pathData[i];
171
+ let { type, values } = com;
172
+
173
+ if (type === 'A') {
174
+
175
+ let comPrev = pathData[i - 1];
176
+
177
+ // previous p0 values might not be correct anymore due to cubic simplification
178
+ let valsL = comPrev.values.slice(-2);
179
+ p0 = { x: valsL[0], y: valsL[1] };
180
+
181
+ let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
182
+
183
+ // check if arc is circular
184
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
185
+
186
+
187
+ //add p0
188
+ p = { x: values[5], y: values[6] }
189
+ com.p0 = p0;
190
+ com.p = p;
191
+ com.circular = circular;
192
+
193
+ let comNext = pathData[i + 1];
194
+
195
+ //add first
196
+ if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
197
+ arcSeq[ind].push(com)
198
+ arcIndices[ind].push(i)
199
+ }
200
+
201
+ if (comNext && comNext.type === 'A') {
202
+ let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
203
+ let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0
204
+ let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0
205
+ //let diff = (diffRx + diffRy) / 2
206
+ //let circular2 = (100 / rx1 * Math.abs(rx1 - ry1)) < 5;
207
+
208
+ p = { x: comNext.values[5], y: comNext.values[6] }
209
+ comNext.p0 = p0;
210
+ comNext.p = p;
211
+
212
+ // add if radii are almost same
213
+ if (diffRx < 5 && diffRy < 5) {
214
+ //console.log(rx, rx1, ry, ry1, 'diff:',diff, 'circular', circular, circular2);
215
+ arcSeq[ind].push(comNext)
216
+ arcIndices[ind].push(i + 1)
217
+ } else {
218
+
219
+ // start new segment
220
+ arcSeq.push([])
221
+ arcIndices.push([])
222
+ ind++
223
+
224
+ }
225
+ }
226
+
227
+ else {
228
+ //arcSeq[ind].push(com)
229
+ //arcIndices[ind].push(i - 1)
230
+ arcSeq.push([])
231
+ arcIndices.push([])
232
+ ind++
233
+ }
234
+ }
235
+ }
236
+
237
+ if (!arcIndices.length) return pathData;
238
+
239
+ arcSeq = arcSeq.filter(item => item.length)
240
+ arcIndices = arcIndices.filter(item => item.length)
241
+ //console.log('combine arcs:', arcSeq, arcIndices);
242
+
243
+
244
+ // Process in reverse to avoid index shifting
245
+ for (let i = arcSeq.length - 1; i >= 0; i--) {
246
+ const seq = arcSeq[i];
247
+ const start = arcIndices[i][0];
248
+ const len = seq.length;
249
+
250
+ // Average radii to prevent distortions
251
+ let rxA = 0, ryA = 0;
252
+ seq.forEach(({ values }) => {
253
+ const [rx, ry] = values;
254
+ rxA += rx;
255
+ ryA += ry;
256
+ });
257
+ rxA /= len;
258
+ ryA /= len;
259
+
260
+ // Correct near-circular arcs
261
+ //console.log('seq', seq);
262
+
263
+ //let rDiff = 100 / rxA * Math.abs(rxA - ryA);
264
+ //let circular = rDiff < 5;
265
+
266
+ // check if arc is circular
267
+ let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
268
+
269
+
270
+ if (circular) {
271
+ // average radii
272
+ rxA = (rxA + ryA) / 2;
273
+ ryA = rxA;
274
+ }
275
+
276
+ let comPrev = pathData[start - 1]
277
+ let comPrevVals = comPrev.values.slice(-2)
278
+ let M = { type: 'M', values: [comPrevVals[0], comPrevVals[1]] }
279
+
280
+
281
+ if (len === 4) {
282
+ //console.log('4 arcs');
283
+
284
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
285
+ let [, , , , , x2, y2] = seq[3].values;
286
+
287
+ let xDiff = Math.abs(x2 - x1);
288
+ let yDiff = Math.abs(y2 - y1);
289
+ let horizontal = xDiff > yDiff;
290
+
291
+ if (circular) {
292
+ let adjustY = !horizontal ? rxA * 2 : 0;
293
+
294
+ // simplify radii
295
+ rxA = 1;
296
+ ryA = 1;
297
+ }
298
+
299
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
300
+ let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
301
+
302
+ // This now correctly replaces the original 4 arc commands with 2
303
+ pathData.splice(start, len, com1, com2);
304
+ //console.log(com1, com2);
305
+ }
306
+
307
+ else if (len === 3) {
308
+ //console.log('3 arcs');
309
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
310
+ let [rx2, ry2, , , , x2, y2] = seq[2].values;
311
+
312
+ // must be large arc
313
+ largeArc = 1;
314
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
315
+
316
+ // replace
317
+ pathData.splice(start, len, com1);
318
+
319
+ }
320
+
321
+
322
+ else if (len === 2) {
323
+ //console.log('2 arcs');
324
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
325
+ let [rx2, ry2, , , , x2, y2] = seq[1].values;
326
+
327
+ // if circular or non-elliptic xAxisRotation has no effect
328
+ if (circular) {
329
+ rxA = 1;
330
+ ryA = 1;
331
+ xAxisRotation = 0;
332
+ }
333
+
334
+ // check if arc is already ideal
335
+ let { p0, p } = seq[0];
336
+ let [p0_1, p_1] = [seq[1].p0, seq[1].p];
337
+
338
+ if (p0.x !== p_1.x || p0.y !== p_1.y) {
339
+
340
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
341
+
342
+ // replace
343
+ pathData.splice(start, len, com1);
344
+ }
345
+ }
346
+
347
+ else {
348
+ //console.log('single arc');
349
+ }
350
+ }
351
+
352
+ return pathData
353
+ }
354
+
355
+
356
+
357
+ //cubics to arcs old
358
+
359
+ export function combineCubicsToArcs(pathData = [], {
360
+ threshold = 0,
361
+ } = {}) {
362
+
363
+ let l = pathData.length;
364
+ let pathDataN = [pathData[0]];
365
+
366
+ for (let i = 1; i < l; i++) {
367
+ let com = pathData[i];
368
+ let { type, cp1 = null, cp2 = null, p0, p } = com;
369
+ let comP = pathData[i - 1];
370
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
371
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
372
+
373
+ if (type === 'C' && comN && comN.type === 'C') {
374
+
375
+ let thresh = getDistAv(p0, p) * 0.02;
376
+ //thresh = getDistAv(p0, p) * 10000;
377
+
378
+ let dx1 = Math.abs(p0.x - cp1.x)
379
+ let dy1 = Math.abs(p0.y - cp1.y)
380
+
381
+ let isHorizontal1 = dy1 < thresh;
382
+ let isVertical1 = dx1 < thresh;
383
+
384
+
385
+ let dx2 = Math.abs(comN.p0.x - comN.cp1.x)
386
+ let dy2 = Math.abs(comN.p0.y - comN.cp1.y)
387
+
388
+ let isHorizontal2 = dy2 < thresh;
389
+ let isVertical2 = dx2 < thresh;
390
+
391
+ //console.log(isHorizontal1, isVertical1);
392
+
393
+ // check angles
394
+ let angleDiff1 = (isHorizontal1 || isVertical1) ? 0 : Infinity;
395
+ let angleDiff2 = (isHorizontal2 || isVertical2) ? 0 : Infinity;
396
+
397
+ if (!isHorizontal1 && !isVertical1) {
398
+ //console.log('get angles', isHorizontal1, isVertical1);
399
+ let angle1 = getAngle(p0, cp1, true);
400
+ let angle2 = getAngle(p, cp2, true);
401
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
402
+ angleDiff1 = Math.abs((deltaAngle % 180) - 90);
403
+ }
404
+
405
+ if (!isHorizontal2 && !isVertical2) {
406
+ //console.log('get angles', isHorizontal1, isVertical1);
407
+ let angle1 = getAngle(p0, cp1, true);
408
+ let angle2 = getAngle(p, cp2, true);
409
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
410
+ angleDiff2 = Math.abs((deltaAngle % 180) - 90);
411
+ }
412
+
413
+
414
+ let isRightAngle1 = angleDiff1 < 3;
415
+ let isRightAngle2 = angleDiff2 < 3;
416
+
417
+ let centroids = [];
418
+ let poly = [];
419
+ let rArr = []
420
+ let largeArc = 0;
421
+
422
+ // final on path point
423
+ let p_a = p
424
+
425
+ // 2 possible candidates - test radius
426
+ if (isRightAngle1 && isRightAngle2) {
427
+ //renderPoint(markers, com.p)
428
+
429
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
430
+ let r1 = getDistance(p0, pI);
431
+ let r2 = getDistance(p, pI);
432
+ let rDiff1 = Math.abs(r1 - r2)
433
+ //let r = r1
434
+
435
+ rArr.push(r1, r2)
436
+
437
+ poly.push(p0, p)
438
+ p_a = p
439
+
440
+
441
+ // 2 commands can be combined – similar radii
442
+ if (rDiff1 < thresh) {
443
+
444
+ //renderPoint(markers, com.p)
445
+
446
+ // add to polygon for sweep
447
+ poly.push(comN.p)
448
+
449
+ // update final point
450
+ p_a = comN.p
451
+
452
+ // approximate/average final center point for final radius
453
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
454
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
455
+
456
+ let cp1_r2 = rotatePoint(comN.cp1, comN.p0.x, comN.p0.y, (Math.PI * -0.5))
457
+ let cp2_r2 = rotatePoint(comN.cp2, comN.p.x, comN.p.y, (Math.PI * 0.5))
458
+
459
+ // assumed centroid
460
+ let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
461
+ let ptC2 = checkLineIntersection(comN.p0, cp1_r2, comN.p, cp2_r2, false)
462
+ let distC = ptC && ptC2 ? getDistAv(ptC, ptC2) : Infinity
463
+
464
+
465
+ // 2 commands can definitely be combined
466
+ if (distC < thresh) {
467
+ //renderPoint(markers, ptC, 'cyan', '1.2%', '0.5')
468
+ //renderPoint(markers, ptC2, 'magenta', '0.5%', '0.5')
469
+
470
+ // add to centroid array
471
+ centroids.push(ptC, ptC2)
472
+
473
+ }
474
+
475
+
476
+ if (comN2 && comN2.type === 'C') {
477
+
478
+ let cp1_r3 = rotatePoint(comN2.cp1, comN2.p0.x, comN2.p0.y, (Math.PI * -0.5))
479
+ let cp2_r3 = rotatePoint(comN2.cp2, comN2.p.x, comN2.p.y, (Math.PI * 0.5))
480
+ let ptC3 = checkLineIntersection(comN2.p0, cp1_r3, comN2.p, cp2_r3, false)
481
+
482
+ let distC2 = ptC && ptC3 ? getDistAv(ptC, ptC3) : Infinity
483
+
484
+ // can be combined with 3rd command
485
+ if (distC2 < thresh) {
486
+ //renderPoint(markers, ptC3, 'green', '2%', '0.3')
487
+
488
+ let r3 = getDistance(ptC3, comN2.p)
489
+ rArr.push(r3)
490
+
491
+ // update final point
492
+ p_a = comN2.p
493
+ poly.push(p, comN2.p)
494
+
495
+ largeArc = 1;
496
+
497
+ }
498
+ }
499
+ //console.log(rDiff1, r, r1, r2);
500
+
501
+ } else {
502
+ pathDataN.push(com)
503
+ continue
504
+ }
505
+
506
+ }
507
+
508
+
509
+ // create new arc command
510
+ if (poly.length > 1) {
511
+
512
+ // get average radius
513
+ //rArr = rArr.sort()
514
+ let rA = Math.max(...rArr)
515
+ rA = rArr[0]
516
+
517
+ let centroidA;
518
+ let xArr = centroids.map(pt => pt.x)
519
+ let yArr = centroids.map(pt => pt.y)
520
+
521
+ centroidA = {
522
+ x: (xArr.reduce((a, b) => a + b, 0)) / centroids.length,
523
+ y: (yArr.reduce((a, b) => a + b, 0)) / centroids.length
524
+ }
525
+
526
+ //console.log(xArr, centroidA);
527
+
528
+ //rA = getDistance(p0, centroids[0])
529
+
530
+ rA = getDistance(p0, centroidA)
531
+ let rA2 = getDistance(p, centroidA)
532
+ //rA = (rA+rA2) /2
533
+ //rA = Math.min(rA,rA2)
534
+
535
+ // rA = ((Math.min(...rArr) * 2 + Math.max(...rArr)) ) / 3
536
+ //console.log(rArr, rA);
537
+
538
+ let area = getPolygonArea(poly, false)
539
+ let sweep = area < 0 ? 0 : 1;
540
+
541
+ let comA = { type: 'A', values: [rA, rA, 0, largeArc, sweep, p_a.x, p_a.y], p0, p: p_a }
542
+
543
+ console.log('comA', comA);
544
+
545
+ pathDataN.push(comA)
546
+
547
+ i += rArr.length - 1;
548
+ //i++
549
+ continue
550
+
551
+ }
552
+
553
+ // test angles
554
+ }
555
+
556
+ pathDataN.push(com)
557
+ }
558
+
559
+ let d = pathDataToD(pathDataN)
560
+ console.log(d);
561
+
562
+ console.log('pathDataN', pathDataN);
563
+ return pathDataN
564
+
565
+ }
566
+ */