svg-path-simplify 0.0.1 → 0.0.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 (42) hide show
  1. package/README.md +28 -1
  2. package/dist/svg-path-simplify.esm.js +4040 -0
  3. package/dist/svg-path-simplify.esm.min.js +1 -0
  4. package/dist/svg-path-simplify.js +4065 -0
  5. package/dist/svg-path-simplify.min.js +1 -0
  6. package/dist/svg-path-simplify.node.js +4062 -0
  7. package/dist/svg-path-simplify.node.min.js +1 -0
  8. package/index.html +222 -0
  9. package/package.json +2 -2
  10. package/src/constants.js +4 -0
  11. package/src/index.js +18 -3
  12. package/src/pathData_simplify_cubic.js +324 -0
  13. package/src/pathData_simplify_cubic_arr.js +50 -0
  14. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  15. package/src/pathSimplify-main.js +294 -0
  16. package/src/svgii/...parse.js +402 -0
  17. package/src/svgii/geometry.js +1096 -0
  18. package/src/svgii/geometry_area.js +265 -0
  19. package/src/svgii/geometry_bbox.js +223 -0
  20. package/src/svgii/pathData_analyze.js +896 -0
  21. package/src/svgii/pathData_convert.js +1180 -0
  22. package/src/svgii/pathData_parse.js +487 -0
  23. package/src/svgii/pathData_remove_collinear.js +85 -0
  24. package/src/svgii/pathData_remove_zerolength.js +28 -0
  25. package/src/svgii/pathData_reorder.js +204 -0
  26. package/src/svgii/pathData_reverse.js +124 -0
  27. package/src/svgii/pathData_scale.js +42 -0
  28. package/src/svgii/pathData_split.js +449 -0
  29. package/src/svgii/pathData_stringify.js +146 -0
  30. package/src/svgii/pathData_toPolygon.js +92 -0
  31. package/src/svgii/pathdata_cleanup.js +363 -0
  32. package/src/svgii/poly_analyze.js +172 -0
  33. package/src/svgii/poly_to_pathdata.js +185 -0
  34. package/src/svgii/rounding.js +154 -0
  35. package/src/svgii/simplify.js +248 -0
  36. package/src/svgii/simplify_bezier.js +470 -0
  37. package/src/svgii/simplify_linetos.js +93 -0
  38. package/src/svgii/simplify_polygon.js +135 -0
  39. package/src/svgii/stringify.js +103 -0
  40. package/src/svgii/svg_cleanup.js +80 -0
  41. package/src/svgii/visualize.js +317 -0
  42. package/LICENSE +0 -21
@@ -0,0 +1,896 @@
1
+ import { splitSubpaths } from './pathData_split.js';
2
+ import { getAngle, bezierhasExtreme, getPathDataVertices, svgArcToCenterParam, getSquareDistance, commandIsFlat, getDistAv } from "./geometry.js";
3
+ import { getPolygonArea, getPathArea } from './geometry_area.js';
4
+ import { getPolyBBox } from './geometry_bbox.js';
5
+ import { renderPoint, renderPath } from "./visualize.js";
6
+
7
+
8
+
9
+ /**
10
+ * analyze path data for
11
+ * decimal detection
12
+ * sub paths
13
+ * directions
14
+ * crucial geometry properties
15
+ */
16
+
17
+
18
+ export function addDimensionData(pathData) {
19
+
20
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
21
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
22
+ let p;
23
+
24
+ pathData[0].dimA = 0;
25
+ let len = pathData.length
26
+
27
+ for (let c = 2; len && c <= len; c++) {
28
+
29
+ let com = pathData[c - 1];
30
+ let { type, values } = com;
31
+ let valsL = values.slice(-2);
32
+
33
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
34
+
35
+ // update M for Z starting points
36
+ if (type === 'M') {
37
+ M = p;
38
+ }
39
+ else if (type.toLowerCase() === 'z') {
40
+ p = M;
41
+ }
42
+
43
+ let dimA = getDistAv(p0, p);
44
+ com.dimA = dimA;
45
+ com.p0 = p0
46
+ com.p = p
47
+
48
+
49
+ if(type==='C' || type==='Q') com.cp1 = {x:values[0], y:values[1]}
50
+ if(type==='C' ) com.cp2 = {x:values[2], y:values[3]}
51
+
52
+ p0=p
53
+ }
54
+
55
+
56
+ console.log('!!!pathData', pathData);
57
+ return pathData
58
+ }
59
+
60
+
61
+ export function analyzePathData(pathData = []) {
62
+
63
+ let pathDataPlus = [];
64
+
65
+ let pathPoly = getPathDataVertices(pathData);
66
+ let bb = getPolyBBox(pathPoly)
67
+ let { left, right, top, bottom, width, height } = bb;
68
+
69
+ // initial starting point coordinates
70
+ let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
71
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
72
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
73
+ let p;
74
+
75
+ // init starting point data
76
+ pathData[0].idx = 0;
77
+ pathData[0].p0 = M;
78
+ pathData[0].p = M;
79
+ pathData[0].lineto = false;
80
+ pathData[0].corner = false;
81
+ pathData[0].extreme = false;
82
+ pathData[0].directionChange = false;
83
+ pathData[0].closePath = false;
84
+ pathData[0].dimA = 0;
85
+
86
+
87
+ // add first M command
88
+ let pathDataProps = [pathData[0]];
89
+ let area0 = 0;
90
+ let len = pathData.length;
91
+
92
+ for (let c = 2; len && c <= len; c++) {
93
+
94
+ let com = pathData[c - 1];
95
+ let { type, values } = com;
96
+ let valsL = values.slice(-2);
97
+
98
+ /**
99
+ * get command points for
100
+ * flatness checks:
101
+ * this way we can skip certain tests
102
+ */
103
+ let commandPts = [p0];
104
+ let isFlat = false;
105
+
106
+ // init properties
107
+ com.idx = c - 1;
108
+ com.lineto = false;
109
+ com.corner = false;
110
+ com.extreme = false;
111
+ com.directionChange = false;
112
+ com.closePath = false;
113
+ com.dimA = 0;
114
+
115
+
116
+ /**
117
+ * define angle threshold for
118
+ * corner detection
119
+ */
120
+ let angleThreshold = 0.05
121
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
122
+
123
+
124
+ // update M for Z starting points
125
+ if (type === 'M') {
126
+ M = p;
127
+ p0 = p
128
+ }
129
+ else if (type.toLowerCase() === 'z') {
130
+ p = M;
131
+ }
132
+
133
+ // add on-path points
134
+ com.p0 = p0;
135
+ com.p = p;
136
+
137
+ let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
138
+
139
+ //let dimA = (width + height) / 2;
140
+ let dimA = getDistAv(p0, p);
141
+ com.dimA = dimA;
142
+ //com.a = dimA;
143
+
144
+
145
+
146
+ /**
147
+ * explicit and implicit linetos
148
+ * - introduced by Z
149
+ */
150
+ if (type === 'L') com.lineto = true;
151
+
152
+ if (type === 'Z') {
153
+ com.closePath = true;
154
+ // if Z introduces an implicit lineto with a length
155
+ if (M.x !== M0.x && M.y !== M0.y) {
156
+ com.lineto = true;
157
+ }
158
+ }
159
+
160
+ // if bezier
161
+ if (type === 'Q' || type === 'C') {
162
+ cp1 = { x: values[0], y: values[1] }
163
+ cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
164
+ com.cp1 = cp1;
165
+ if (cp2) com.cp2 = cp2;
166
+ }
167
+
168
+
169
+ /**
170
+ * check command flatness
171
+ * we leave it to the bezier simplifier
172
+ * to convert flat beziers to linetos
173
+ * otherwise we may strip rather flat starting segments
174
+ * preventing a better simplification
175
+ */
176
+
177
+ if (values.length > 2) {
178
+ if (type === 'Q' || type === 'C') commandPts.push(cp1);
179
+ if (type === 'C') commandPts.push(cp2);
180
+ commandPts.push(p);
181
+
182
+ let commandFlatness = commandIsFlat(commandPts);
183
+ isFlat = commandFlatness.flat;
184
+ com.flat = isFlat;
185
+
186
+ if (isFlat) {
187
+ com.extreme = false;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * is extreme relative to bounding box
193
+ * in case elements are rotated we can't rely on 90degree angles
194
+ * so we interpret maximum x/y on-path points as well as extremes
195
+ * but we ignore linetos to allow chunk compilation
196
+ */
197
+ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
198
+ com.extreme = true;
199
+ }
200
+
201
+
202
+ //next command
203
+ let comN = pathData[c] ? pathData[c] : null;
204
+ let comNValsL = comN ? comN.values.slice(-2) : null;
205
+ typeN = comN ? comN.type : null;
206
+
207
+
208
+ // get bezier control points
209
+ if (comN && (comN.type === 'Q' || comN.type === 'C')) {
210
+ pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
211
+
212
+ cp1N = { x: comN.values[0], y: comN.values[1] }
213
+ cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
214
+ }
215
+
216
+
217
+ /**
218
+ * Detect direction change points
219
+ * this will prevent distortions when simplifying
220
+ * e.g in the "spine" of an "S" glyph
221
+ */
222
+ area1 = getPolygonArea(commandPts)
223
+ let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
224
+ // update area
225
+ area0 = area1
226
+
227
+ if (signChange) {
228
+ //renderPoint(svg1, p0, 'orange', '1%', '0.75')
229
+ com.directionChange = true;
230
+ }
231
+
232
+
233
+ /**
234
+ * check extremes or corners
235
+ * for adjacent curves by
236
+ * control point angles
237
+ */
238
+ if ((type === 'Q' || type === 'C')) {
239
+
240
+ if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
241
+
242
+ // check extremes
243
+ let cpts = commandPts.slice(1);
244
+
245
+ let w = pN ? Math.abs(pN.x - p0.x) : 0
246
+ let h = pN ? Math.abs(pN.y - p0.y) : 0
247
+ let thresh = (w + h) / 2 * 0.1;
248
+ let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
249
+
250
+ let flatness2 = commandIsFlat(pts1, thresh)
251
+ let isFlat2 = flatness2.flat;
252
+
253
+ /**
254
+ * if current and next cubic are flat
255
+ * we don't flag them as extremes to allow simplification
256
+ */
257
+ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
258
+
259
+ //let bezierExtreme = bezierhasExtreme(p0, cpts, angleThreshold);
260
+
261
+ //console.log(isFlat, isFlat2, cpts, hasExtremes, 'com.extreme', com.extreme, 'commandPts', commandPts);
262
+
263
+ if (hasExtremes) {
264
+ com.extreme = true
265
+ }
266
+
267
+ // check corners
268
+ else {
269
+
270
+ let cpts1 = cp2 ? [cp2, p] : [cp1, p];
271
+ let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
272
+
273
+ let angCom1 = getAngle(...cpts1, true)
274
+ let angCom2 = getAngle(...cpts2, true)
275
+ let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
276
+
277
+
278
+ let cpDist1 = getSquareDistance(...cpts1)
279
+ let cpDist2 = getSquareDistance(...cpts2)
280
+
281
+ let cornerThreshold = 10
282
+ let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
283
+
284
+ if (isCorner) {
285
+ com.corner = true;
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+
292
+ pathDataProps.push(com)
293
+ p0 = p;
294
+
295
+ }
296
+
297
+
298
+ let dimA = (width + height) / 2
299
+ //pathDataPlus.push({ pathData: pathDataProps, bb: bb, dimA: dimA })
300
+ pathDataPlus = { pathData: pathDataProps, bb: bb, dimA: dimA }
301
+
302
+ //console.log('pathDataPlus', pathDataPlus);
303
+ return pathDataPlus
304
+
305
+ }
306
+
307
+
308
+
309
+
310
+ export function analyzePathData2(pathData = [], debug = true) {
311
+
312
+ // clone
313
+ pathData = JSON.parse(JSON.stringify(pathData));
314
+
315
+ // split to sub paths
316
+ let pathDataSubArr = splitSubpaths(pathData)
317
+
318
+ // collect more verbose data
319
+ let pathDataPlus = [];
320
+
321
+ // log
322
+ let simplyfy_debug_log = [];
323
+
324
+ /**
325
+ * analyze sub paths
326
+ * add simplified bbox (based on on-path-points)
327
+ * get area
328
+ */
329
+ pathDataSubArr.forEach(pathData => {
330
+
331
+ let pathPoly = getPathDataVertices(pathData);
332
+ //let pathDataArea = getPathArea(pathData);
333
+ let bb = getPolyBBox(pathPoly)
334
+ let { left, right, top, bottom, width, height } = bb;
335
+
336
+ // initial starting point coordinates
337
+ let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
338
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
339
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
340
+ let p;
341
+
342
+ // init starting point data
343
+ pathData[0].p0 = M;
344
+ pathData[0].p = M;
345
+ pathData[0].lineto = false;
346
+ pathData[0].corner = false;
347
+ pathData[0].extreme = false;
348
+ pathData[0].directionChange = false;
349
+ pathData[0].closePath = false;
350
+
351
+
352
+ // add first M command
353
+ let pathDataProps = [pathData[0]];
354
+ let area0 = 0;
355
+ let len = pathData.length;
356
+
357
+ for (let c = 2; len && c <= len; c++) {
358
+
359
+ let com = pathData[c - 1];
360
+ let { type, values } = com;
361
+ let valsL = values.slice(-2);
362
+
363
+ /**
364
+ * get command points for
365
+ * flatness checks:
366
+ * this way we can skip certain tests
367
+ */
368
+ let commandPts = [p0];
369
+ let isFlat = false;
370
+
371
+ // init properties
372
+ com.lineto = false;
373
+ com.corner = false;
374
+ com.extreme = false;
375
+ com.directionChange = false;
376
+ com.closePath = false;
377
+
378
+ /**
379
+ * define angle threshold for
380
+ * corner detection
381
+ */
382
+ let angleThreshold = 0.05
383
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
384
+
385
+
386
+ // update M for Z starting points
387
+ if (type === 'M') {
388
+ M = p;
389
+ p0 = p
390
+ //p0 = p
391
+ }
392
+ else if (type.toLowerCase() === 'z') {
393
+ //p0 = M;
394
+ p = M;
395
+ }
396
+
397
+ // add on-path points
398
+ com.p0 = p0;
399
+ com.p = p;
400
+
401
+ let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
402
+
403
+ /**
404
+ * explicit and implicit linetos
405
+ * - introduced by Z
406
+ */
407
+ if (type === 'L') com.lineto = true;
408
+
409
+ if (type === 'Z') {
410
+ com.closePath = true;
411
+ // if Z introduces an implicit lineto with a length
412
+ if (M.x !== M0.x && M.y !== M0.y) {
413
+ com.lineto = true;
414
+ }
415
+ }
416
+
417
+ // if bezier
418
+ if (type === 'Q' || type === 'C') {
419
+ cp1 = { x: values[0], y: values[1] }
420
+ cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
421
+ com.cp1 = cp1;
422
+ if (cp2) com.cp2 = cp2;
423
+ }
424
+
425
+ /**
426
+ * check command flatness
427
+ * we leave it to the bezier simplifier
428
+ * to convert flat beziers to linetos
429
+ * otherwise we may strip rather flat starting segments
430
+ * preventing a better simplification
431
+ */
432
+
433
+ if (values.length > 2) {
434
+ if (type === 'Q' || type === 'C') commandPts.push(cp1);
435
+ if (type === 'C') commandPts.push(cp2);
436
+ commandPts.push(p);
437
+
438
+ let commandFlatness = commandIsFlat(commandPts);
439
+ isFlat = commandFlatness.flat;
440
+ com.flat = isFlat;
441
+
442
+ if (isFlat) {
443
+ com.extreme = false;
444
+ /*
445
+ pathDataProps.push(com)
446
+ p0 = p;
447
+ continue;
448
+ */
449
+ }
450
+ }
451
+
452
+
453
+ /**
454
+ * is extreme relative to bounding box
455
+ * in case elements are rotated we can't rely on 90degree angles
456
+ * so we interpret maximum x/y on-path points as well as extremes
457
+ * but we ignore linetos to allow chunk compilation
458
+ */
459
+ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
460
+ com.extreme = true;
461
+ }
462
+
463
+
464
+ // add to average
465
+ //let squareDist = getSquareDistance(p0, p)
466
+ //com.size = squareDist;
467
+
468
+ let dimA = (width + height) / 2;
469
+ com.dimA = dimA;
470
+ //console.log('decimals', decimals, size);
471
+
472
+ //next command
473
+ let comN = pathData[c] ? pathData[c] : null;
474
+ let comNValsL = comN ? comN.values.slice(-2) : null;
475
+ typeN = comN ? comN.type : null;
476
+
477
+
478
+ // get bezier control points
479
+ if (comN) {
480
+ pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
481
+
482
+ if (comN.type === 'Q' || comN.type === 'C') {
483
+ cp1N = { x: comN.values[0], y: comN.values[1] }
484
+ cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
485
+ }
486
+ }
487
+
488
+
489
+ /**
490
+ * Detect direction change points
491
+ * this will prevent distortions when simplifying
492
+ * e.g in the "spine" of an "S" glyph
493
+ */
494
+ area1 = getPolygonArea(commandPts)
495
+ let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
496
+ // update area
497
+ area0 = area1
498
+
499
+ if (signChange) {
500
+ //renderPoint(svg1, p0, 'orange', '1%', '0.75')
501
+ com.directionChange = true;
502
+ }
503
+
504
+
505
+ /**
506
+ * check extremes or corners for adjacent curves by control point angles
507
+ */
508
+ if ((type === 'Q' || type === 'C')) {
509
+
510
+ if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
511
+
512
+ // check extremes
513
+ let cpts = commandPts.slice(1);
514
+
515
+ let w = Math.abs(pN.x - p0.x)
516
+ let h = Math.abs(pN.y - p0.y)
517
+ let thresh = (w + h) / 2 * 0.1;
518
+ let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
519
+
520
+ let flatness2 = commandIsFlat(pts1, thresh)
521
+ let isFlat2 = flatness2.flat;
522
+
523
+ //console.log('isFlat2', isFlat2, isFlat);
524
+
525
+ /**
526
+ * if current and next cubic are flat
527
+ * we don't flag them as extremes to allow simplification
528
+ */
529
+ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
530
+
531
+ if (hasExtremes) {
532
+ com.extreme = true
533
+ }
534
+
535
+ // check corners
536
+ else {
537
+
538
+ let cpts1 = cp2 ? [cp2, p] : [cp1, p];
539
+ let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
540
+
541
+ let angCom1 = getAngle(...cpts1, true)
542
+ let angCom2 = getAngle(...cpts2, true)
543
+ let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
544
+
545
+
546
+ let cpDist1 = getSquareDistance(...cpts1)
547
+ let cpDist2 = getSquareDistance(...cpts2)
548
+
549
+ let cornerThreshold = 10
550
+ let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
551
+
552
+ if (isCorner) {
553
+ com.corner = true;
554
+ }
555
+ }
556
+ }
557
+ }
558
+
559
+
560
+
561
+ //let debug = false
562
+ //debug = true
563
+ if (debug) {
564
+ if (com.signChange) {
565
+ renderPoint(markers, p0, 'orange', '1.5%', '0.75')
566
+ }
567
+ if (com.extreme) {
568
+ renderPoint(markers, p, 'cyan', '1%', '0.75')
569
+ }
570
+ if (com.corner) {
571
+ renderPoint(markers, p, 'magenta')
572
+ }
573
+ }
574
+
575
+ pathDataProps.push(com)
576
+ p0 = p;
577
+
578
+ }
579
+
580
+
581
+ //decimalsAV = Array.from(decimalsAV)
582
+ //decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
583
+ //console.log('decimalsAV', decimalsAV);
584
+ //pathDataProps[0].decimals = decimalsAV
585
+
586
+ //decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
587
+ let dimA = (width + height) / 2
588
+ pathDataPlus.push({ pathData: pathDataProps, bb: bb, dimA: dimA })
589
+
590
+
591
+ if (simplyfy_debug_log.length) {
592
+ console.log(simplyfy_debug_log);
593
+ }
594
+
595
+ })
596
+
597
+
598
+
599
+ return pathDataPlus
600
+
601
+ }
602
+
603
+
604
+
605
+ export function analyzePathData__(pathData = [], debug = true) {
606
+
607
+ // clone
608
+ pathData = JSON.parse(JSON.stringify(pathData));
609
+
610
+ // split to sub paths
611
+ let pathDataSubArr = splitSubpaths(pathData)
612
+
613
+ // collect more verbose data
614
+ let pathDataPlus = [];
615
+
616
+ // log
617
+ let simplyfy_debug_log = [];
618
+
619
+ /**
620
+ * analyze sub paths
621
+ * add simplified bbox (based on on-path-points)
622
+ * get area
623
+ */
624
+ pathDataSubArr.forEach(pathData => {
625
+
626
+ let pathDataArea = getPathArea(pathData);
627
+ let pathPoly = getPathDataVertices(pathData);
628
+ let bb = getPolyBBox(pathPoly)
629
+ let { left, right, top, bottom, width, height } = bb;
630
+
631
+ // initial starting point coordinates
632
+ let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
633
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
634
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
635
+ let p;
636
+
637
+ // init starting point data
638
+ pathData[0].p0 = M;
639
+ pathData[0].p = M;
640
+ pathData[0].lineto = false;
641
+ pathData[0].corner = false;
642
+ pathData[0].extreme = false;
643
+ pathData[0].directionChange = false;
644
+ pathData[0].closePath = false;
645
+
646
+
647
+ // add first M command
648
+ let pathDataProps = [pathData[0]];
649
+ let area0 = 0;
650
+ let len = pathData.length;
651
+
652
+ for (let c = 2; len && c <= len; c++) {
653
+
654
+ let com = pathData[c - 1];
655
+ let { type, values } = com;
656
+ let valsL = values.slice(-2);
657
+
658
+ /**
659
+ * get command points for
660
+ * flatness checks:
661
+ * this way we can skip certain tests
662
+ */
663
+ let commandPts = [p0];
664
+ let isFlat = false;
665
+
666
+ // init properties
667
+ com.lineto = false;
668
+ com.corner = false;
669
+ com.extreme = false;
670
+ com.directionChange = false;
671
+ com.closePath = false;
672
+
673
+ /**
674
+ * define angle threshold for
675
+ * corner detection
676
+ */
677
+ let angleThreshold = 0.05
678
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
679
+
680
+
681
+ // update M for Z starting points
682
+ if (type === 'M') {
683
+ M = p;
684
+ p0 = p
685
+ //p0 = p
686
+ }
687
+ else if (type.toLowerCase() === 'z') {
688
+ //p0 = M;
689
+ p = M;
690
+ }
691
+
692
+ // add on-path points
693
+ com.p0 = p0;
694
+ com.p = p;
695
+
696
+ let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
697
+
698
+ /**
699
+ * explicit and implicit linetos
700
+ * - introduced by Z
701
+ */
702
+ if (type === 'L') com.lineto = true;
703
+
704
+ if (type === 'Z') {
705
+ com.closePath = true;
706
+ // if Z introduces an implicit lineto with a length
707
+ if (M.x !== M0.x && M.y !== M0.y) {
708
+ com.lineto = true;
709
+ }
710
+ }
711
+
712
+ // if bezier
713
+ if (type === 'Q' || type === 'C') {
714
+ cp1 = { x: values[0], y: values[1] }
715
+ cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
716
+ com.cp1 = cp1;
717
+ if (cp2) com.cp2 = cp2;
718
+ }
719
+
720
+ /**
721
+ * check command flatness
722
+ * we leave it to the bezier simplifier
723
+ * to convert flat beziers to linetos
724
+ * otherwise we may strip rather flat starting segments
725
+ * preventing a better simplification
726
+ */
727
+
728
+ if (values.length > 2) {
729
+ if (type === 'Q' || type === 'C') commandPts.push(cp1);
730
+ if (type === 'C') commandPts.push(cp2);
731
+ commandPts.push(p);
732
+
733
+ let commandFlatness = commandIsFlat(commandPts);
734
+ isFlat = commandFlatness.flat;
735
+ com.flat = isFlat;
736
+
737
+ if (isFlat) {
738
+ com.extreme = false;
739
+ /*
740
+ pathDataProps.push(com)
741
+ p0 = p;
742
+ continue;
743
+ */
744
+ }
745
+ }
746
+
747
+
748
+ /**
749
+ * is extreme relative to bounding box
750
+ * in case elements are rotated we can't rely on 90degree angles
751
+ * so we interpret maximum x/y on-path points as well as extremes
752
+ * but we ignore linetos to allow chunk compilation
753
+ */
754
+ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
755
+ com.extreme = true;
756
+ }
757
+
758
+
759
+ // add to average
760
+ //let squareDist = getSquareDistance(p0, p)
761
+ //com.size = squareDist;
762
+
763
+ let dimA = (width + height) / 2;
764
+ com.dimA = dimA;
765
+ //console.log('decimals', decimals, size);
766
+
767
+ //next command
768
+ let comN = pathData[c] ? pathData[c] : null;
769
+ let comNValsL = comN ? comN.values.slice(-2) : null;
770
+ typeN = comN ? comN.type : null;
771
+
772
+
773
+ // get bezier control points
774
+ if (comN) {
775
+ pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
776
+
777
+ if (comN.type === 'Q' || comN.type === 'C') {
778
+ cp1N = { x: comN.values[0], y: comN.values[1] }
779
+ cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
780
+ }
781
+ }
782
+
783
+
784
+ /**
785
+ * Detect direction change points
786
+ * this will prevent distortions when simplifying
787
+ * e.g in the "spine" of an "S" glyph
788
+ */
789
+ area1 = getPolygonArea(commandPts)
790
+ let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
791
+ // update area
792
+ area0 = area1
793
+
794
+ if (signChange) {
795
+ //renderPoint(svg1, p0, 'orange', '1%', '0.75')
796
+ com.directionChange = true;
797
+ }
798
+
799
+
800
+ /**
801
+ * check extremes or corners for adjacent curves by control point angles
802
+ */
803
+ if ((type === 'Q' || type === 'C')) {
804
+
805
+ if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
806
+
807
+ // check extremes
808
+ let cpts = commandPts.slice(1);
809
+
810
+ let w = Math.abs(pN.x - p0.x)
811
+ let h = Math.abs(pN.y - p0.y)
812
+ let thresh = (w + h) / 2 * 0.1;
813
+ let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
814
+
815
+ let flatness2 = commandIsFlat(pts1, thresh)
816
+ let isFlat2 = flatness2.flat;
817
+
818
+ //console.log('isFlat2', isFlat2, isFlat);
819
+
820
+ /**
821
+ * if current and next cubic are flat
822
+ * we don't flag them as extremes to allow simplification
823
+ */
824
+ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
825
+
826
+ if (hasExtremes) {
827
+ com.extreme = true
828
+ }
829
+
830
+ // check corners
831
+ else {
832
+
833
+ let cpts1 = cp2 ? [cp2, p] : [cp1, p];
834
+ let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
835
+
836
+ let angCom1 = getAngle(...cpts1, true)
837
+ let angCom2 = getAngle(...cpts2, true)
838
+ let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI
839
+
840
+
841
+ let cpDist1 = getSquareDistance(...cpts1)
842
+ let cpDist2 = getSquareDistance(...cpts2)
843
+
844
+ let cornerThreshold = 10
845
+ let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2
846
+
847
+ if (isCorner) {
848
+ com.corner = true;
849
+ }
850
+ }
851
+ }
852
+ }
853
+
854
+
855
+
856
+ let debug = false
857
+ //debug = true
858
+ if (debug) {
859
+ if (com.signChange) {
860
+ renderPoint(svg1, p0, 'orange', '1.5%', '0.75')
861
+ }
862
+ if (com.extreme) {
863
+ renderPoint(svg1, p, 'cyan', '1%', '0.75')
864
+ }
865
+ if (com.corner) {
866
+ renderPoint(svg1, p, 'magenta')
867
+ }
868
+ }
869
+
870
+ pathDataProps.push(com)
871
+ p0 = p;
872
+
873
+ }
874
+
875
+
876
+ //decimalsAV = Array.from(decimalsAV)
877
+ //decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length);
878
+ //console.log('decimalsAV', decimalsAV);
879
+ //pathDataProps[0].decimals = decimalsAV
880
+
881
+ //decimalsAV = Math.floor(decimalsAV/decimalsAV.length);
882
+ let dimA = (width + height) / 2
883
+ pathDataPlus.push({ pathData: pathDataProps, bb: bb, area: pathDataArea, dimA: dimA })
884
+
885
+
886
+ if (simplyfy_debug_log.length) {
887
+ console.log(simplyfy_debug_log);
888
+ }
889
+
890
+ })
891
+
892
+
893
+
894
+ return pathDataPlus
895
+
896
+ }