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,487 @@
1
+ //import { arcToBezier, quadratic2Cubic } from './convert.js';
2
+ //import { getAngle, bezierhasExtreme, getDistance } from "./geometry";
3
+ import { pathDataToAbsoluteOrRelative, pathDataToLonghands, pathDataArcsToCubics, pathDataQuadraticToCubic } from './pathData_convert.js';
4
+
5
+
6
+
7
+ /**
8
+ * parse normalized
9
+ */
10
+
11
+ export function normalizePathData(pathData = [],
12
+ {
13
+ toAbsolute = true,
14
+ toLonghands = true,
15
+ quadraticToCubic = false,
16
+ arcToCubic = false,
17
+ arcAccuracy = 2,
18
+ } = {},
19
+
20
+ {
21
+ hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
22
+ } = {}
23
+ ) {
24
+
25
+ // pathdata properties - test= true adds a manual test
26
+ if (testTypes) {
27
+ //console.log('test for conversions');
28
+ let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
29
+ hasRelatives = /[lcqamts]/gi.test(commands);
30
+ hasQuadratics = /[qt]/gi.test(commands);
31
+ hasArcs = /[a]/gi.test(commands);
32
+ hasShorthands = /[vhst]/gi.test(commands);
33
+ isPoly = /[mlz]/gi.test(commands);
34
+ }
35
+
36
+
37
+ /**
38
+ * normalize:
39
+ * convert to all absolute
40
+ * all longhands
41
+ */
42
+
43
+ if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
44
+ toLonghands = true
45
+ toAbsolute = true
46
+ }
47
+
48
+ if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
49
+ if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
50
+ if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
51
+ if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
52
+
53
+ return pathData;
54
+
55
+ }
56
+
57
+ export function parsePathDataNormalized(d,
58
+ {
59
+ // necessary for most calculations
60
+ toAbsolute = true,
61
+ toLonghands = true,
62
+
63
+ // not necessary unless you need cubics only
64
+ quadraticToCubic = false,
65
+
66
+ // mostly a fallback if arc calculations fail
67
+ arcToCubic = false,
68
+ // arc to cubic precision - adds more segments for better precision
69
+ arcAccuracy = 4,
70
+ } = {}
71
+ ) {
72
+
73
+
74
+ let pathDataObj = parsePathDataString(d);
75
+ let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
76
+ let pathData = pathDataObj.pathData;
77
+
78
+ // normalize
79
+ pathData = normalizePathData(pathData,
80
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
81
+ //{test:true}
82
+ { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
83
+ )
84
+
85
+ return pathData;
86
+ }
87
+
88
+
89
+ const commandSet = new Set([
90
+ 0x4D, 0x6D, 0x41, 0x61, 0x43, 0x63,
91
+ 0x4C, 0x6C, 0x51, 0x71, 0x53, 0x73,
92
+ 0x54, 0x74, 0x48, 0x68, 0x56, 0x76,
93
+ 0x5A, 0x7A
94
+ ]);
95
+
96
+ const paramCountsArr = new Uint8Array(128);
97
+ // M starting point
98
+ paramCountsArr[0x4D] = 2;
99
+ paramCountsArr[0x6D] = 2;
100
+
101
+ // A Arc
102
+ paramCountsArr[0x41] = 7
103
+ paramCountsArr[0x61] = 7
104
+
105
+ // C Cubic Bézier
106
+ paramCountsArr[0x43] = 6
107
+ paramCountsArr[0x63] = 6
108
+
109
+ // L Line To
110
+ paramCountsArr[0x4C] = 2
111
+ paramCountsArr[0x6C] = 2
112
+
113
+ // Q Quadratic Bézier
114
+ paramCountsArr[0x51] = 4
115
+ paramCountsArr[0x71] = 4
116
+
117
+ // S Smooth Cubic Bézier
118
+ paramCountsArr[0x53] = 4
119
+ paramCountsArr[0x73] = 4
120
+
121
+ // T Smooth Quadratic Bézier
122
+ paramCountsArr[0x54] = 2
123
+ paramCountsArr[0x74] = 2
124
+
125
+ // H Horizontal Line
126
+ paramCountsArr[0x48] = 1
127
+ paramCountsArr[0x68] = 1
128
+
129
+ // V Vertical Line
130
+ paramCountsArr[0x56] = 1
131
+ paramCountsArr[0x76] = 1
132
+
133
+ // Z Close Path
134
+ paramCountsArr[0x5A] = 0
135
+ paramCountsArr[0x7A] = 0
136
+
137
+
138
+
139
+ export function parsePathDataString(d, debug = true) {
140
+ d = d.trim();
141
+
142
+ if (d === '') {
143
+ return {
144
+ pathData: [],
145
+ hasRelatives: false,
146
+ hasShorthands: false,
147
+ hasQuadratics: false,
148
+ hasArcs: false
149
+ }
150
+ }
151
+
152
+ const SPECIAL_SPACES = new Set([
153
+ 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
154
+ 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
155
+ ]);
156
+
157
+
158
+ const isSpace = (ch) => {
159
+ return (ch === 0x20) || (ch === 0x002C) || // White spaces or comma
160
+ (ch === 0x0A) || (ch === 0x0D) || // nl cr
161
+ (ch === 0x2028) || (ch === 0x2029) || // Line terminators
162
+ (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
163
+ (ch >= 0x1680 && SPECIAL_SPACES.has(ch));
164
+ }
165
+
166
+
167
+ let i = 0, len = d.length;
168
+ let lastCommand = "";
169
+ let pathData = [];
170
+ let itemCount = -1;
171
+ let val = '';
172
+ let wasE = false;
173
+ let floatCount = 0;
174
+ let valueIndex = 0;
175
+ let maxParams = 0;
176
+ let needsNewSegment = false;
177
+ let foundCommands = new Set([]);
178
+
179
+
180
+ // collect errors
181
+ let log = [];
182
+ let feedback;
183
+
184
+ const addSeg = () => {
185
+ // Create new segment if needed before adding the minus sign
186
+ if (needsNewSegment) {
187
+
188
+ // sanitize implicit linetos
189
+ if (lastCommand === 'M') lastCommand = 'L';
190
+ else if (lastCommand === 'm') lastCommand = 'l';
191
+
192
+ pathData.push({ type: lastCommand, values: [] });
193
+
194
+ itemCount++;
195
+ valueIndex = 0;
196
+ needsNewSegment = false;
197
+ }
198
+ }
199
+
200
+ const pushVal = (checkFloats = false) => {
201
+
202
+ // regular value or float
203
+ if (!checkFloats ? val !== '' : floatCount > 0) {
204
+
205
+ // error: no first command
206
+ if (debug && itemCount === -1) {
207
+
208
+ feedback = 'Pathdata must start with M command'
209
+ log.push(feedback)
210
+
211
+ // add M command to collect subsequent errors
212
+ lastCommand = 'M'
213
+ pathData.push({ type: lastCommand, values: [] });
214
+ maxParams = 2;
215
+ valueIndex = 0
216
+ itemCount++
217
+
218
+ }
219
+
220
+ if (lastCommand === 'A' || lastCommand === 'a') {
221
+ val = sanitizeArc()
222
+ //console.log('arc', val);
223
+ pathData[itemCount].values.push(...val);
224
+
225
+ } else {
226
+ // error: leading zeroes
227
+ if (debug && val[1] && val[1] !== '.' && val[0] === '0') {
228
+ feedback = `${itemCount}. command: Leading zeros not valid: ${val}`
229
+ log.push(feedback)
230
+ }
231
+ pathData[itemCount].values.push(+val);
232
+ }
233
+
234
+ valueIndex++;
235
+ val = '';
236
+ floatCount = 0;
237
+
238
+ // Mark that a new segment is needed if maxParams is reached
239
+ needsNewSegment = valueIndex >= maxParams;
240
+
241
+ }
242
+ }
243
+
244
+ const sanitizeArc = () => {
245
+
246
+ let valLen = val.length;
247
+ let arcSucks = false;
248
+
249
+ // large arc and sweep
250
+ if (valueIndex === 3 && valLen === 2) {
251
+ //console.log('large arc sweep combined', val, +val[0], +val[1]);
252
+ val = [+val[0], +val[1]];
253
+ arcSucks = true
254
+ valueIndex++
255
+ }
256
+
257
+ // sweep and final
258
+ else if (valueIndex === 4 && valLen > 1) {
259
+ //console.log('sweep and final', val, val[0], val[1]);
260
+ val = [+val[0], +val[1]];
261
+ arcSucks = true
262
+ valueIndex++
263
+ }
264
+
265
+ // large arc, sweep and final pt combined
266
+ else if (valueIndex === 3 && valLen >= 3) {
267
+ //console.log('large arc, sweep and final pt combined', val);
268
+ val = [+val[0], +val[1], +val.substring(2)];
269
+ arcSucks = true
270
+ valueIndex += 2
271
+ }
272
+
273
+ //console.log('val arc', val);
274
+ return !arcSucks ? [+val] : val;
275
+
276
+ }
277
+
278
+ const validateCommand = () => {
279
+
280
+ if (itemCount > 0) {
281
+ let lastCom = pathData[itemCount];
282
+ let valLen = lastCom.values.length;
283
+
284
+ if ((valLen && valLen < maxParams) || (valLen && valLen > maxParams) || ((lastCommand === 'z' || lastCommand === 'Z') && valLen > 0)) {
285
+ let diff = maxParams - valLen;
286
+ feedback = `${itemCount}. command of type "${lastCommand}": ${diff} values too few - ${maxParams} expected`;
287
+
288
+ let prevFeedback = log[log.length - 1];
289
+
290
+ if (prevFeedback !== feedback) {
291
+ log.push(feedback)
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+
298
+ let isE = false;
299
+ let isMinusorPlus = false;
300
+ let isDot = false;
301
+
302
+
303
+ while (i < len) {
304
+
305
+ let charCode = d.charCodeAt(i);
306
+
307
+
308
+ let isDigit = (charCode > 47 && charCode < 58);
309
+ if (!isDigit) {
310
+ isE = (charCode === 101 || charCode === 69);
311
+ isMinusorPlus = (charCode === 45 || charCode === 43);
312
+ isDot = charCode === 46;
313
+ }
314
+
315
+ /**
316
+ * number related:
317
+ * digit, e-notation, dot or -/+ operator
318
+ */
319
+
320
+ if (
321
+ isDigit ||
322
+ isMinusorPlus ||
323
+ isDot ||
324
+ isE
325
+ ) {
326
+
327
+
328
+ // minus or float/dot separated: 0x2D=hyphen; 0x2E=dot
329
+ if (!wasE && (charCode === 0x2D || charCode === 0x2E)) {
330
+
331
+ // checkFloats changes condition for value adding
332
+ let checkFloats = charCode === 0x2E;
333
+
334
+ // new val
335
+ pushVal(checkFloats);
336
+
337
+ // new segment
338
+ addSeg()
339
+
340
+ // concatenated floats
341
+ if (checkFloats) {
342
+ floatCount++;
343
+ }
344
+ }
345
+
346
+
347
+ // regular splitting
348
+ else {
349
+ //console.log('reg', d[i]);
350
+ addSeg()
351
+ }
352
+
353
+ //isNumber
354
+ val += d[i];
355
+
356
+ // e/scientific notation in value
357
+ wasE = isE;
358
+ i++;
359
+ continue;
360
+ }
361
+
362
+
363
+ /**
364
+ * Separated by white space
365
+ */
366
+ if ((charCode < 48 || charCode > 5759) && isSpace(charCode)) {
367
+
368
+ // push value
369
+ pushVal()
370
+
371
+ i++;
372
+ continue;
373
+ }
374
+
375
+
376
+ /**
377
+ * New command introduced by
378
+ * alphabetic A-Z character
379
+ */
380
+ if (charCode > 64) {
381
+
382
+ // is valid command
383
+ let isValid = commandSet.has(charCode);
384
+
385
+ if (!isValid) {
386
+ feedback = `${itemCount}. command "${d[i]}" is not a valid type`;
387
+ log.push(feedback);
388
+ i++
389
+ continue
390
+ }
391
+
392
+
393
+ // command is concatenated without whitespace
394
+ if (val !== '') {
395
+ pathData[itemCount].values.push(+val);
396
+ valueIndex++;
397
+ val = '';
398
+ }
399
+
400
+ // check if previous command was correctly closed
401
+ if (debug) validateCommand()
402
+
403
+
404
+ lastCommand = d[i];
405
+ maxParams = paramCountsArr[charCode];
406
+ let isM = lastCommand === 'M' || lastCommand === 'm'
407
+ let wasClosePath = itemCount > 0 && (pathData[itemCount].type === 'z' || pathData[itemCount].type === 'Z')
408
+
409
+ foundCommands.add(lastCommand);
410
+
411
+ // add omitted M command after Z
412
+ if (wasClosePath && !isM) {
413
+ pathData.push({ type: 'm', values: [0, 0] });
414
+ itemCount++;
415
+ }
416
+
417
+ // init new command
418
+ pathData.push({ type: lastCommand, values: [] });
419
+ itemCount++;
420
+
421
+ // reset counters
422
+ floatCount = 0;
423
+ valueIndex = 0;
424
+ needsNewSegment = false;
425
+
426
+ i++;
427
+ continue;
428
+ }
429
+
430
+
431
+ // exceptions - prevent infinite loop
432
+ if (!isDigit) {
433
+ feedback = `${itemCount}. ${d[i]} is not a valid separarator or token`;
434
+ log.push(feedback);
435
+ val = '';
436
+ }
437
+
438
+ i++;
439
+
440
+ }
441
+
442
+ // final value
443
+ pushVal()
444
+ if (debug) validateCommand()
445
+
446
+
447
+ // return error log
448
+ if (debug && log.length) {
449
+ feedback = 'Invalid path data:\n' + log.join('\n')
450
+ if (debug === 'log') {
451
+ console.log(feedback);
452
+ } else {
453
+ throw new Error(feedback)
454
+ }
455
+ }
456
+
457
+ pathData[0].type = 'M'
458
+
459
+ /**
460
+ * check if absolute/relative or
461
+ * shorthands are present
462
+ * to specify if normalization is required
463
+ */
464
+ //check types relative arcs or quadratics
465
+ let commands = Array.from(foundCommands).join('');
466
+ let hasRelatives = /[lcqamts]/g.test(commands);
467
+ let hasShorthands = /[vhst]/gi.test(commands);
468
+ let hasArcs = /[a]/gi.test(commands);
469
+ let hasQuadratics = /[qt]/gi.test(commands);
470
+
471
+
472
+ return {
473
+ pathData,
474
+ hasRelatives,
475
+ hasShorthands,
476
+ hasQuadratics,
477
+ hasArcs
478
+ }
479
+
480
+ }
481
+
482
+
483
+
484
+ export function stringifyPathData(pathData) {
485
+ return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
486
+ }
487
+
@@ -0,0 +1,85 @@
1
+ import { getSquareDistance } from "./geometry.js";
2
+ import { getPolygonArea } from "./geometry_area.js";
3
+ import { renderPoint } from "./visualize.js";
4
+
5
+ export function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
6
+
7
+ let pathDataN = [pathData[0]];
8
+
9
+
10
+ let lastType = 'L';
11
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] }
12
+ let p0 = M;
13
+ let p = M
14
+ let isClosed = pathData[pathData.length - 1].type.toLowerCase() === 'z'
15
+
16
+ for (let c = 1, l = pathData.length; c < l; c++) {
17
+ let comPrev = pathData[c - 1];
18
+ let com = pathData[c];
19
+ let comN = pathData[c + 1] || pathData[l - 1];
20
+ let p1 = comN.type === 'Z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] }
21
+
22
+ let { type, values } = com;
23
+ let valsL = values.slice(-2)
24
+ p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
25
+
26
+ let cpts = type === 'C' ?
27
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
28
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
29
+
30
+
31
+ let area = getPolygonArea([p0, ...cpts, p, p1], true)
32
+ let distSquare = getSquareDistance(p0, p)
33
+ let distMax = distSquare / 500 * tolerance
34
+
35
+ let isFlat = area < distMax
36
+
37
+ if(!flatBezierToLinetos && type==='C') isFlat = false;
38
+ //let isFlat = flatBezierToLinetos && type === 'C' ? area < distMax : false
39
+
40
+
41
+ // convert flat beziers to linetos
42
+ if (flatBezierToLinetos && type === 'C') {
43
+
44
+ let areaBez = getPolygonArea([p0, ...cpts, p], true)
45
+ let isFlatBez = areaBez < distSquare / 1000
46
+
47
+ if (isFlatBez && comPrev.type !== 'C') {
48
+ com.type = "L"
49
+ com.values = valsL
50
+ }
51
+
52
+ }
53
+
54
+
55
+ // update end point
56
+ p0 = p;
57
+
58
+ // colinear – exclude arcs (as always =) as semicircles won't have an area
59
+ if (type !== 'A' && isFlat && c < l - 1) {
60
+ continue;
61
+ }
62
+
63
+ if (type === 'M') {
64
+ M = p
65
+ p0 = M
66
+ }
67
+
68
+ else if (type === 'Z') {
69
+ p0 = M;
70
+ }
71
+
72
+ // proceed and add command
73
+ pathDataN.push(com)
74
+
75
+ }
76
+
77
+ // add close path
78
+ if (isClosed) {
79
+ //pathDataN.push({ type: 'Z', values: [] })
80
+ }
81
+ //console.log('pathDataN', pathDataN);
82
+
83
+ return pathDataN;
84
+
85
+ }
@@ -0,0 +1,28 @@
1
+ export function removeZeroLengthLinetos(pathData) {
2
+
3
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] }
4
+ let p0 = M
5
+ let p = p0
6
+
7
+ let pathDataN = [pathData[0]]
8
+
9
+ for (let c = 1, l = pathData.length; c < l; c++) {
10
+ let com = pathData[c];
11
+ let { type, values } = com;
12
+
13
+ let valsL = values.slice(-2);
14
+ p = { x: valsL[0], y: valsL[1] };
15
+
16
+ // skip lineto
17
+ if (type === 'L' && p.x === p0.x && p.y === p0.y) {
18
+ continue
19
+ }
20
+
21
+ pathDataN.push(com)
22
+ p0 = p;
23
+ }
24
+
25
+
26
+ return pathDataN
27
+
28
+ }