svg-path-simplify 0.1.3 → 0.2.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 (43) hide show
  1. package/README.md +10 -0
  2. package/dist/svg-path-simplify.esm.js +3905 -1533
  3. package/dist/svg-path-simplify.esm.min.js +13 -1
  4. package/dist/svg-path-simplify.js +3923 -1551
  5. package/dist/svg-path-simplify.min.js +13 -1
  6. package/dist/svg-path-simplify.min.js.gz +0 -0
  7. package/index.html +61 -31
  8. package/package.json +3 -5
  9. package/src/constants.js +3 -0
  10. package/src/index-node.js +0 -1
  11. package/src/index.js +26 -0
  12. package/src/pathData_simplify_cubic.js +74 -31
  13. package/src/pathData_simplify_cubicsToArcs.js +566 -0
  14. package/src/pathData_simplify_harmonize_cpts.js +170 -0
  15. package/src/pathData_simplify_revertToquadratics.js +21 -0
  16. package/src/pathSimplify-main.js +253 -86
  17. package/src/poly-fit-curve-schneider.js +570 -0
  18. package/src/simplify_poly_RDP.js +146 -0
  19. package/src/simplify_poly_radial_distance.js +100 -0
  20. package/src/svg_getViewbox.js +1 -1
  21. package/src/svgii/geometry.js +389 -63
  22. package/src/svgii/geometry_area.js +2 -1
  23. package/src/svgii/pathData_analyze.js +259 -212
  24. package/src/svgii/pathData_convert.js +91 -663
  25. package/src/svgii/pathData_fromPoly.js +12 -0
  26. package/src/svgii/pathData_parse.js +90 -89
  27. package/src/svgii/pathData_parse_els.js +3 -0
  28. package/src/svgii/pathData_parse_fontello.js +449 -0
  29. package/src/svgii/pathData_remove_collinear.js +44 -37
  30. package/src/svgii/pathData_reorder.js +2 -1
  31. package/src/svgii/pathData_simplify_redraw.js +343 -0
  32. package/src/svgii/pathData_simplify_refineCorners.js +18 -9
  33. package/src/svgii/pathData_simplify_refineExtremes.js +19 -78
  34. package/src/svgii/pathData_split.js +42 -45
  35. package/src/svgii/pathData_toPolygon.js +130 -4
  36. package/src/svgii/poly_analyze.js +470 -14
  37. package/src/svgii/poly_to_pathdata.js +224 -19
  38. package/src/svgii/rounding.js +55 -112
  39. package/src/svgii/svg_cleanup.js +13 -1
  40. package/src/svgii/visualize.js +8 -3
  41. package/{debug.cjs → tests/debug.cjs} +3 -0
  42. /package/{test.js → tests/test.js} +0 -0
  43. /package/{testSVG.js → tests/testSVG.js} +0 -0
@@ -0,0 +1,449 @@
1
+
2
+ const paramCounts = { a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0 };
3
+
4
+ // report errors for debugging
5
+ const addError = (state, errorCode) => {
6
+ let value = state.path[state.index]
7
+ let com = errorCode !== 6 ? state.path[state.segmentStart] : value;
8
+ let typeInfo = errorCode != 0 ? `type: »${com}«` : ``
9
+
10
+ console.log(state);
11
+
12
+ if (state.debug) {
13
+
14
+ state.log.push(`Command #${state.lastIndex} | ${state.index}) ${typeInfo} value: »${value}«: ${errors[errorCode]}`);
15
+
16
+ if (errorCode === 0) {
17
+ state.path = 'M' + state.path
18
+ state.max++
19
+ state.index--
20
+ }
21
+
22
+ else if (errorCode === 5) {
23
+ /*
24
+ let idx = state.index;
25
+ state.path = state.path.slice(0, idx + 2) + state.path.slice(idx + 3)
26
+ state.max--
27
+ console.log(state.path);
28
+ //state.index-=1
29
+ //state.index+=1
30
+
31
+ //state.index++;
32
+ //finalizeSegment(state)
33
+ */
34
+
35
+ // skip to next segment
36
+ state.index = getNextCommandIndex(state)
37
+ //state.lastIndex++
38
+ state.skipped++
39
+
40
+ return
41
+
42
+ }
43
+
44
+ // missing param
45
+ else if (errorCode === 2) {
46
+
47
+ // skip to next segment
48
+ state.index = getNextCommandIndex(state)
49
+ state.lastIndex++
50
+ //state.skipped++
51
+ return
52
+
53
+ }
54
+
55
+ state.index++;
56
+
57
+ } else {
58
+ state.err.push(`Command #${state.result.length} | ${state.index}) ${typeInfo} value: »${value}«: ${errors[errorCode]}`);
59
+ }
60
+
61
+ }
62
+
63
+ const errors = [
64
+ 'Paths must start with `M` or `m` command',
65
+ 'Arc largeArc or sweep flag can only be 1 or 0',
66
+ 'Missing param',
67
+ 'Not a number – param should start with 0..9 or `.`',
68
+ 'Trailing zeroes are not permitted',
69
+ 'Invalid float exponent',
70
+ 'Invalid command'
71
+ ];
72
+
73
+ const SPECIAL_SPACES = [
74
+ 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
75
+ 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
76
+ ];
77
+
78
+
79
+ function isSpaceOrComma(ch) {
80
+ return (ch === 0x20) || (ch === 0x002C) || // White spaces or comma
81
+ (ch === 0x0A) || (ch === 0x0D) || // nl cr
82
+ (ch === 0x2028) || (ch === 0x2029) || // Line terminators
83
+ (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
84
+ (ch > 5759 && SPECIAL_SPACES.indexOf(ch) >= 0);
85
+ //(ch >= 0x1680 && SPECIAL_SPACES.indexOf(ch) >= 0);
86
+ }
87
+
88
+
89
+ const COMMAND_LOOKUP = new Uint8Array(128);
90
+ 'achlmqstvz'.split('').forEach(ch => {
91
+ const code = ch.charCodeAt(0);
92
+ COMMAND_LOOKUP[code] = 1;
93
+ COMMAND_LOOKUP[code - 32] = 1; // uppercase
94
+ });
95
+
96
+
97
+ function isCommand(code) {
98
+ //return code < 128 && COMMAND_LOOKUP[code] === 1;
99
+ return code > 64 && COMMAND_LOOKUP[code];
100
+ }
101
+
102
+ function isDigit(code) {
103
+ // 0..9
104
+ return (code > 47 && code < 58);
105
+ }
106
+
107
+ function isDigitStart(code) {
108
+ return (code > 47 && code < 58) || /* 0..9 */
109
+ code === 0x2E || /* . */
110
+ code === 0x2D || /* - */
111
+ code === 0x2B /* + */
112
+ }
113
+
114
+
115
+ function skipSpaces(state) {
116
+ while (state.index < state.max && isSpaceOrComma(state.path.charCodeAt(state.index))) {
117
+ state.index++;
118
+ }
119
+ }
120
+
121
+
122
+ function scanArcFlag(state) {
123
+ let ch = state.path.charCodeAt(state.index);
124
+
125
+ // zero
126
+ if (ch === 0x30) {
127
+ state.param = 0;
128
+ state.index++;
129
+ return;
130
+ }
131
+
132
+ // one
133
+ if (ch === 0x31) {
134
+ state.param = 1;
135
+ state.index++;
136
+ return;
137
+ }
138
+
139
+ addError(state, 1)
140
+ //state.err.push(`${state.index}: ${errors[1]}`);
141
+ }
142
+
143
+ // collect command data
144
+ function State(path, debug = true) {
145
+ this.index = 0;
146
+ this.path = path;
147
+ this.max = path.length;
148
+ this.result = [];
149
+ this.param = 0;
150
+ this.err = [];
151
+ this.log = [];
152
+ this.segmentStart = 0;
153
+ this.data = [];
154
+ this.lastType = '';
155
+ this.allTypes = new Set([])
156
+ this.debug = debug
157
+ this.skipped=0
158
+ this.lastIndex=0
159
+ }
160
+
161
+
162
+ /**
163
+ * Scan command
164
+ */
165
+ function scanSegment(state) {
166
+ let max = state.max;
167
+ let cmdCode = state.path.charCodeAt(state.index);
168
+ let is_arc = (cmdCode === 0x61 || cmdCode === 0x41);
169
+
170
+ // Error 6:not valid command
171
+ if (!isCommand(cmdCode)) {
172
+ addError(state, 6)
173
+ return;
174
+ }
175
+
176
+ let cmd = state.path[state.index];
177
+ let cmdLC = cmd.toLowerCase();
178
+ let needParams = paramCounts[cmdLC];
179
+
180
+ state.segmentStart = state.index;
181
+ state.data = [];
182
+
183
+ state.index++;
184
+ skipSpaces(state);
185
+
186
+ // Z : close path
187
+ if (!needParams) {
188
+ finalizeSegment(state);
189
+ return;
190
+ }
191
+
192
+
193
+ while (state.index < max) {
194
+
195
+ for (let i = 0; i < needParams; i++) {
196
+
197
+ // Error 2: missing param
198
+ if (state.index < max && isCommand(state.path.charCodeAt(state.index))) {
199
+ addError(state, 2)
200
+ return;
201
+ }
202
+
203
+ // is Arc command
204
+ if (is_arc && (i === 3 || i === 4)) scanArcFlag(state);
205
+ else scanParam(state);
206
+
207
+ if (state.err.length) {
208
+ //state.index++
209
+ //continue
210
+ return;
211
+ }
212
+
213
+ state.data.push(state.param);
214
+ skipSpaces(state);
215
+
216
+ }
217
+
218
+ if (state.index >= max) break;
219
+
220
+ // If next is not number start → segment done
221
+ if (!isDigitStart(state.path.charCodeAt(state.index))) break;
222
+ }
223
+
224
+ finalizeSegment(state);
225
+ }
226
+
227
+
228
+
229
+ /**
230
+ * Scan parameters in command
231
+ */
232
+
233
+
234
+ function scanParam(state) {
235
+ let start = state.index,
236
+ index = start,
237
+ max = state.max,
238
+ hasCeiling = false,
239
+ hasDecimal = false,
240
+ hasDot = false
241
+
242
+ let ch = state.path.charCodeAt(index);
243
+
244
+ // Error 2: Missing param
245
+ if (state.index >= max) {
246
+ addError(state, 2)
247
+ return;
248
+ }
249
+
250
+ // Plus/Minus
251
+ if (ch === 0x2B || ch === 0x2D) {
252
+ index++;
253
+ ch = (index < max) ? state.path.charCodeAt(index) : 0;
254
+ }
255
+
256
+
257
+ // not number or '.' dot separator: 3. Not a number – param should start with 0..9 or `.`
258
+ if (!isDigit(ch) && ch !== 0x2E) {
259
+ addError(state, 3)
260
+ return;
261
+ }
262
+
263
+ // not '.' floating point separator
264
+ if (ch !== 0x2E) {
265
+
266
+ // is zero
267
+ let zeroFirst = (ch === 0x30);
268
+ index++;
269
+
270
+ ch = (index < max) ? state.path.charCodeAt(index) : 0;
271
+
272
+ if (zeroFirst && index < max) {
273
+ // decimal number starts with '0' such as '09' is illegal.
274
+ if (ch && isDigit(ch)) {
275
+ addError(state, 4)
276
+ return;
277
+ }
278
+ }
279
+
280
+ while (index < max && isDigit(state.path.charCodeAt(index))) {
281
+ index++;
282
+ hasCeiling = true;
283
+ }
284
+ ch = (index < max) ? state.path.charCodeAt(index) : 0;
285
+ }
286
+
287
+ // '.' separator
288
+ if (ch === 0x2E) {
289
+ hasDot = true;
290
+ index++;
291
+ while (isDigit(state.path.charCodeAt(index))) {
292
+ index++;
293
+ hasDecimal = true;
294
+ }
295
+ ch = (index < max) ? state.path.charCodeAt(index) : 0;
296
+ }
297
+
298
+ // scientific notation 'e/E'
299
+ if (ch === 0x65 || ch === 0x45) {
300
+
301
+ // 5. Invalid float exponent
302
+ if (hasDot && !hasCeiling && !hasDecimal) {
303
+ addError(state, 5)
304
+
305
+ //finalizeSegment(state)
306
+ // skip to next segment
307
+ //state.index = getNextCommandIndex(state)-1
308
+
309
+ return
310
+ //if(!debug) return
311
+ }
312
+
313
+ index++;
314
+
315
+ ch = (index < max) ? state.path.charCodeAt(index) : 0;
316
+ // plus or minus
317
+ if (ch === 0x2B || ch === 0x2D) {
318
+ index++;
319
+ }
320
+ if (index < max && isDigit(state.path.charCodeAt(index))) {
321
+ while (index < max && isDigit(state.path.charCodeAt(index))) {
322
+ index++;
323
+ }
324
+ } else {
325
+ // 5. Invalid float exponent
326
+ addError(state, 5)
327
+ return;
328
+ }
329
+ }
330
+
331
+ state.index = index;
332
+ state.param = +state.path.slice(start, index);
333
+ }
334
+
335
+
336
+
337
+
338
+ /**
339
+ * Process duplicated commands (without comand name)
340
+ * This logic is shamelessly borrowed from Raphael
341
+ * https://github.com/DmitryBaranovskiy/raphael/
342
+ *
343
+ * !!! removed ROM Catmull command
344
+ */
345
+ function finalizeSegment(state) {
346
+
347
+ let cmd = state.path[state.segmentStart];
348
+ let cmdLC = cmd.toLowerCase();
349
+ let params = state.data;
350
+ let lastType = state.lastType
351
+ state.allTypes.add(cmd)
352
+
353
+ // Z close path
354
+ if (cmdLC === 'z') {
355
+ //console.log('!!!is Z');
356
+ state.result.push({ type: 'Z', values: [] });
357
+ state.lastType = 'z'
358
+ state.lastIndex++
359
+ return
360
+ }
361
+
362
+ // implicit linetos introduced by M/m commands
363
+ if (cmdLC === 'm' && params.length > 2) {
364
+ state.result.push({ type: cmd, values: [params[0], params[1]] });
365
+ state.lastIndex++
366
+ params = params.slice(2);
367
+ cmdLC = 'l';
368
+ cmd = (cmd === 'm') ? 'l' : 'L';
369
+ }
370
+
371
+ let maxParams = paramCounts[cmdLC]
372
+
373
+ // prepend implicit m after Z for better subpath detection
374
+ if (lastType === 'z' && cmdLC !== 'z') {
375
+ state.result.push({ type: 'm', values: [0, 0] });
376
+ state.lastIndex++
377
+ }
378
+
379
+ state.lastType = cmdLC
380
+
381
+ // create new commands of same type
382
+ while (params.length >= maxParams) {
383
+ state.result.push({ type: cmd, values: params.splice(0, maxParams) });
384
+ state.lastIndex++
385
+
386
+ if (!maxParams) {
387
+ break;
388
+ }
389
+ }
390
+
391
+ }
392
+
393
+ function getNextCommandIndex(state) {
394
+ let i = state.index;
395
+ while (i < state.max) {
396
+ let ch = state.path.charCodeAt(i);
397
+ if (isCommand(ch)) break
398
+ i++
399
+ }
400
+ return i
401
+ }
402
+
403
+
404
+ export function parsePathDataFontello(pathDataString, debug = true) {
405
+
406
+ pathDataString = pathDataString.trim()
407
+ let state = new State(pathDataString, debug);
408
+ let max = state.max;
409
+
410
+
411
+ // Error 0: missing M command: 0. Paths must start with `M` or `m` command
412
+ if (pathDataString[0] !== 'M' && pathDataString[0] !== 'm') {
413
+ addError(state, 0)
414
+ }
415
+
416
+ while (state.index < max && (debug || !state.err.length)) {
417
+ scanSegment(state);
418
+ }
419
+
420
+ // force absolute M for 1st sub path - facilitates concatenation
421
+ if (state.result.length) {
422
+ state.result[0].type = 'M';
423
+ }
424
+
425
+ /**
426
+ * check if absolute/relative or
427
+ * shorthands are present
428
+ * to specify if normalization is required
429
+ */
430
+ //check types relative arcs or quadratics
431
+ let allTypestypes = Array.from(state.allTypes).join('');
432
+
433
+ let pathDataObj = {
434
+ errors: state.err,
435
+ log: state.log,
436
+ pathData: state.result,
437
+ hasRelatives: /[lcqamts]/g.test(allTypestypes),
438
+ hasShorthands: /[vhst]/gi.test(allTypestypes),
439
+ hasArcs: /[a]/gi.test(allTypestypes),
440
+ hasQuadratics: /[qt]/gi.test(allTypestypes),
441
+ isPolygon: /[cqats]/gi.test(allTypestypes) ? false : true,
442
+ }
443
+
444
+
445
+ console.log(pathDataObj.log);
446
+ console.log(pathDataObj.errors);
447
+ console.log(pathDataObj.pathData);
448
+ return pathDataObj
449
+ };
@@ -1,13 +1,13 @@
1
- import { getDistAv, getSquareDistance } from "./geometry.js";
1
+ import { getDeltaAngle, getDistAv, getDistManhattan, getSquareDistance } from "./geometry.js";
2
2
  import { getPolygonArea } from "./geometry_area.js";
3
3
  import { checkBezierFlatness, commandIsFlat } from "./geometry_flatness.js";
4
- import { renderPoint } from "./visualize.js";
4
+ import { renderPoint, renderPoly } from "./visualize.js";
5
5
 
6
6
  export function pathDataRemoveColinear(pathData, {
7
- tolerance = 1,
7
+ tolerance = 1,
8
8
  //toleranceCubics = null,
9
9
  flatBezierToLinetos = true
10
- }={}) {
10
+ } = {}) {
11
11
 
12
12
  //toleranceCubics = !toleranceCubics ? tolerance : toleranceCubics;
13
13
  let pathDataN = [pathData[0]];
@@ -30,32 +30,60 @@ export function pathDataRemoveColinear(pathData, {
30
30
  p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
31
31
 
32
32
 
33
+ /*
33
34
  let area = p1 ? getPolygonArea([p0, p, p1], true) : Infinity
34
-
35
-
36
- //let distSquare0 = getSquareDistance(p0, p)
37
- //let distSquare1 = getSquareDistance(p, p1)
38
35
  let distSquare = getSquareDistance(p0, p1)
39
- //distSquare = (distSquare0+distSquare1) * 0.5
40
-
41
36
  let distMax = distSquare ? distSquare / 333 * tolerance : 0
37
+ */
42
38
 
43
- let isFlat = area < distMax;
39
+ //let isFlat = area < distMax;
40
+ let isFlat = false;
44
41
  let isFlatBez = false;
45
42
 
46
43
 
44
+ // flatness by cross product
45
+ let dx0 = Math.abs(p1.x - p0.x)
46
+ let dy0 = Math.abs(p1.y - p0.y)
47
+
48
+ let dx1 = Math.abs(p.x - p0.x)
49
+ let dy1 = Math.abs(p.y - p0.y)
50
+
51
+ let dx2 = Math.abs(p1.x - p.x)
52
+ let dy2 = Math.abs(p1.y - p.y)
53
+
54
+ // zero length segments are flat
55
+ let isZeroLength = (!dy1 && !dx1) || (!dy2 && !dx2)
56
+ if (isZeroLength) isFlat = true;
57
+
58
+ // check cross products for colinearity
59
+ if (!isFlat) {
60
+
61
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
62
+ //let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
63
+ //let crossDiff = Math.abs(cross0-cross1)
64
+ //let cross = Math.max(cross0, cross1)
65
+ let thresh = (dx0 + dy0) * 0.1
66
+
67
+ //!cross0 ||
68
+ if ( cross0 < thresh) {
69
+ //renderPoint(markers, p)
70
+ isFlat = true
71
+ }
72
+ }
73
+
74
+
47
75
  if (!flatBezierToLinetos && type === 'C') isFlat = false;
48
76
 
49
77
  // convert flat beziers to linetos
50
78
  if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
51
79
 
52
80
  let cpts = type === 'C' ?
53
- [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
54
- (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
81
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
82
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
55
83
 
56
- isFlatBez = commandIsFlat([p0, ...cpts, p],{tolerance});
84
+ isFlatBez = commandIsFlat([p0, ...cpts, p], { tolerance });
57
85
 
58
- if (isFlatBez && c < l - 1 ) {
86
+ if (isFlatBez && c < l - 1) {
59
87
  type = "L"
60
88
  com.type = "L"
61
89
  com.values = valsL
@@ -66,23 +94,7 @@ export function pathDataRemoveColinear(pathData, {
66
94
 
67
95
  // colinear – exclude arcs (as always =) as semicircles won't have an area
68
96
  //&& comN.type==='L'
69
- if ( isFlat && c < l - 1 && comN.type!=='A' && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
70
-
71
- /*
72
- console.log(area, distMax );
73
- //if(comN.type!=='L' ){}
74
-
75
- if(p0.x === p.x && p0.y === p.y){
76
-
77
- }
78
-
79
- renderPoint(markers, p0, 'blue', '1.5%', '1')
80
- renderPoint(markers, p, 'red', '1%', '1')
81
- renderPoint(markers, p1, 'cyan', '0.5%', '1')
82
- */
83
-
84
- //renderPoint(markers, p, 'blue', '1%', '1')
85
-
97
+ if (isFlat && c < l - 1 && comN.type !== 'A' && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
86
98
 
87
99
  continue;
88
100
  }
@@ -93,11 +105,6 @@ export function pathDataRemoveColinear(pathData, {
93
105
 
94
106
  if (type === 'M') {
95
107
  M = p
96
- p0 = M
97
- }
98
-
99
- else if (type === 'Z') {
100
- p0 = M;
101
108
  }
102
109
 
103
110
  // proceed and add command
@@ -38,7 +38,7 @@ export function pathDataToTopLeft(pathData) {
38
38
 
39
39
  // reorder to top left most
40
40
  //|| a.x - b.x
41
- indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) );
41
+ indices = indices.sort((a, b) => +a.y.toFixed(8) - +b.y.toFixed(8) || a.x-b.x );
42
42
  newIndex = indices[0].index
43
43
 
44
44
  return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
@@ -233,6 +233,7 @@ export function shiftSvgStartingPoint(pathData, offset) {
233
233
  */
234
234
 
235
235
  export function addClosePathLineto(pathData) {
236
+
236
237
  let pathDataL = pathData.length;
237
238
  let closed = pathData[pathDataL - 1].type.toLowerCase() === "z" ? true : false;
238
239