svg-path-simplify 0.0.7 → 0.0.9

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 (38) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +1250 -562
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +4756 -4068
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +1250 -562
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +89 -29
  9. package/package.json +5 -3
  10. package/src/detect_input.js +17 -10
  11. package/src/dom-polyfill.js +29 -0
  12. package/src/dom-polyfill_back.js +22 -0
  13. package/src/index.js +10 -1
  14. package/src/pathData_simplify_cubic.js +114 -143
  15. package/src/pathData_simplify_cubic_extrapolate.js +64 -35
  16. package/src/pathSimplify-main.js +113 -165
  17. package/src/svgii/geometry.js +8 -155
  18. package/src/svgii/geometry_flatness.js +94 -0
  19. package/src/svgii/pathData_analyze.js +15 -596
  20. package/src/svgii/pathData_convert.js +26 -17
  21. package/src/svgii/pathData_interpolate.js +65 -0
  22. package/src/svgii/pathData_parse.js +25 -9
  23. package/src/svgii/pathData_parse_els.js +245 -0
  24. package/src/svgii/pathData_remove_collinear.js +33 -28
  25. package/src/svgii/pathData_remove_orphaned.js +21 -0
  26. package/src/svgii/pathData_remove_zerolength.js +17 -3
  27. package/src/svgii/pathData_reorder.js +9 -3
  28. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  29. package/src/svgii/pathData_simplify_refineExtremes.js +208 -0
  30. package/src/svgii/pathData_split.js +43 -15
  31. package/src/svgii/pathData_stringify.js +3 -12
  32. package/src/svgii/rounding.js +35 -27
  33. package/src/svgii/svg_cleanup.js +4 -1
  34. package/testSVG.js +39 -0
  35. package/src/pathData_simplify_cubic_arr.js +0 -50
  36. package/src/svgii/simplify.js +0 -248
  37. package/src/svgii/simplify_bezier.js +0 -470
  38. package/src/svgii/simplify_linetos.js +0 -93
@@ -5,88 +5,134 @@ import { renderPoint } from "./svgii/visualize";
5
5
 
6
6
 
7
7
 
8
- function findSplitParameter(com1, com2) {
9
-
10
- let chordLength1 = getDistance(com1.p0, com1.p);
11
- let totalChord = getDistance(com1.p0, com2.p);
12
-
13
- let t = chordLength1 / totalChord;
14
-
15
- return refineParameter(com1, com2, t);
16
-
17
- return t
18
- }
19
-
20
- function refineParameter(com1, com2, tEstimate, maxIterations = 10) {
21
- let t = tEstimate;
22
-
23
- for (let i = 0; i < maxIterations; i++) {
24
- // Calculate error based on Q2 and R1 relationships
25
- const P0 = com1.p0;
26
- const P3 = com2.p;
27
-
28
- // Reconstruct P1 and P2 at current t
29
- const P1 = {
30
- x: (com1.cp1.x - (1 - t) * P0.x) / t,
31
- y: (com1.cp1.y - (1 - t) * P0.y) / t
32
- };
33
-
34
- const P2 = {
35
- x: (com2.cp2.x - t * P3.x) / (1 - t),
36
- y: (com2.cp2.y - t * P3.y) / (1 - t)
37
- };
8
+ export function simplifyPathDataCubic(pathData, {
9
+ keepExtremes = true,
10
+ keepInflections = true,
11
+ keepCorners = true,
12
+ extrapolateDominant = true,
13
+ tolerance = 1,
14
+ } = {}) {
15
+
16
+ let pathDataN = [pathData[0]];
17
+ let l = pathData.length;
18
+
19
+ for (let i = 2; l && i <= l; i++) {
20
+ let com = pathData[i - 1];
21
+ let comN = i < l ? pathData[i] : null;
22
+ let typeN = comN?.type || null;
23
+ //let isCornerN = comN?.corner || null;
24
+ //let isExtremeN = comN?.extreme || null;
25
+ let isDirChange = com?.directionChange || null;
26
+ let isDirChangeN = comN?.directionChange || null;
27
+
28
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
29
+
30
+
31
+ // next is also cubic
32
+ if (type === 'C' && typeN === 'C') {
33
+
34
+ // cannot be combined as crossing extremes or corners
35
+ if (
36
+ (keepInflections && isDirChangeN) ||
37
+ (keepCorners && corner) ||
38
+ (!isDirChange && keepExtremes && extreme)
39
+ ) {
40
+ //renderPoint(markers, p, 'red', '1%')
41
+ pathDataN.push(com)
42
+ }
38
43
 
39
- // Calculate what Q2 and R1 should be
40
- const Q1 = interpolate(P0, P1, t);
41
- const P1P2_mid = interpolate(P1, P2, t);
42
- const Q2_calc = interpolate(Q1, P1P2_mid, t);
44
+ // try simplification
45
+ else {
46
+ //renderPoint(markers, p, 'magenta', '1%')
47
+ let combined = combineCubicPairs(com, comN, {tolerance})
48
+ let error = 0;
49
+
50
+ // combining successful! try next segment
51
+ if (combined.length === 1) {
52
+ com = combined[0]
53
+ let offset = 1;
54
+
55
+ // add cumulative error to prevent distortions
56
+ error += com.error;
57
+ //console.log('!error', error);
58
+
59
+ // find next candidates
60
+ //offset<2 &&
61
+ for (let n = i + 1; error < tolerance && n < l; n++) {
62
+ let comN = pathData[n]
63
+ if (comN.type !== 'C' ||
64
+ (
65
+ (keepInflections && comN.directionChange) ||
66
+ (keepCorners && com.corner) ||
67
+ (keepExtremes && com.extreme)
68
+ )
69
+ ) {
70
+ break
71
+ }
72
+
73
+ let combined = combineCubicPairs(com, comN, {tolerance})
74
+ if (combined.length === 1) {
75
+ // add cumulative error to prevent distortions
76
+ //console.log('combined', combined);
77
+ error += combined[0].error * 0.5;
78
+ //error += combined[0].error * 1;
79
+ offset++
80
+ }
81
+ com = combined[0]
82
+ }
83
+
84
+ //com.opt = true
85
+ pathDataN.push(com)
86
+
87
+ if (i < l) {
88
+ i += offset
89
+ }
90
+
91
+ } else {
92
+ pathDataN.push(com)
93
+ }
94
+ }
43
95
 
44
- const P2P3_mid = interpolate(P2, P3, t);
45
- const R1_calc = interpolate(P1P2_mid, P2P3_mid, t);
96
+ } // end of bezier command
46
97
 
47
- // Calculate errors
48
- const errorQ2 = getDistance(Q2_calc, com1.cp2);
49
- const errorR1 = getDistance(R1_calc, com2.cp1);
50
- const totalError = errorQ2 + errorR1;
51
98
 
52
- if (totalError < 1e-9) {
53
- break;
99
+ // other commands
100
+ else {
101
+ pathDataN.push(com)
54
102
  }
55
103
 
56
- // Simple adjustment - in practice you'd use proper Newton step
57
- t = t * 0.5 + 0.5 * (errorQ2 < errorR1 ? t + 0.01 : t - 0.01);
58
- t = Math.max(0.001, Math.min(0.999, t));
59
- }
104
+ } // end command loop
60
105
 
61
- return t;
106
+ return pathDataN
62
107
  }
63
108
 
64
109
 
65
110
 
66
- export function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
111
+
112
+ export function combineCubicPairs(com1, com2, {
113
+ tolerance = 1
114
+ } = {}) {
67
115
 
68
116
  let commands = [com1, com2];
69
- let t = findSplitT(com1, com2);
70
117
 
71
- //threshold = 0.01
118
+ // assume 2 segments are result of a segment split
119
+ let t = findSplitT(com1, com2);
72
120
 
73
121
  let distAv1 = getDistAv(com1.p0, com1.p);
74
122
  let distAv2 = getDistAv(com2.p0, com2.p);
75
- let distMin = Math.min(distAv1, distAv2)
76
- //let distMax = Math.max(distAv1, distAv2)
123
+ let distMin = Math.max(0, Math.min(distAv1, distAv2))
77
124
 
78
- let distScale = 0.05
125
+
126
+ let distScale = 0.06
79
127
  let maxDist = distMin * distScale * tolerance
80
- //tolerance = distMax * threshold
81
128
 
82
- let comS = getExtrapolatedCommand(com1, com2, t, t)
129
+ // get hypothetical combined command
130
+ let comS = getExtrapolatedCommand(com1, com2, t)
83
131
 
84
- // test on path point against original
132
+ // test new point-at-t against original mid segment starting point
85
133
  let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t)
86
134
 
87
135
 
88
- //let tolerance = Math.min(Math.max(com1.dimA, com2.dimA), Math.min(com1.dimA, com2.dimA)) * 0.05
89
-
90
136
  let dist0 = getDistAv(com1.p, pt)
91
137
  let dist1 = 0, dist2 = 0;
92
138
  let close = dist0 < maxDist;
@@ -97,14 +143,6 @@ export function combineCubicPairs(com1, com2, extrapolateDominant = false, toler
97
143
 
98
144
 
99
145
 
100
- /*
101
- if (com2.directionChange) {
102
- //renderPoint(markers, com2.p0)
103
- }
104
- */
105
-
106
- //console.log('tolerance', tolerance, close, dist0);
107
-
108
146
  if (close) {
109
147
 
110
148
  /**
@@ -159,44 +197,10 @@ export function combineCubicPairs(com1, com2, extrapolateDominant = false, toler
159
197
  } // end 1st try
160
198
 
161
199
 
162
-
163
- /*
164
- if (extrapolateDominant && com2.extreme) {
165
- renderPoint(markers, com2.p)
166
- //renderPoint(markers, com1.p, 'cyan')
167
- //extrapolateDominant = false;
168
- }
169
- */
170
-
171
-
172
-
173
-
174
- // try extrapolated dominant curve
175
- //&& !com2.extreme
176
- // && !com1.extreme
177
- if (extrapolateDominant && !success ) {
178
-
179
- let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
180
-
181
- //console.log('???combinedEx', combinedEx);
182
-
183
- if(combinedEx.length===1){
184
- success = true
185
- comS = combinedEx[0]
186
- error = comS.error
187
-
188
- //console.log('!!!combinedEx', combinedEx);
189
- }
190
-
191
-
192
- }
193
200
 
194
201
  // add meta
195
202
  if (success) {
196
203
 
197
- //comS.dimA = (Math.abs(comS.p0.x - comS.p.x) + Math.abs(comS.p0.y - comS.p.y)) / 2
198
-
199
-
200
204
  // correct to exact start and end points
201
205
  comS.p0 = com1.p0
202
206
  comS.p = com2.p
@@ -215,8 +219,6 @@ export function combineCubicPairs(com1, com2, extrapolateDominant = false, toler
215
219
  //comS.p0 = com1.p0;
216
220
  commands = [comS];
217
221
 
218
- //console.log('commands combined', commands);
219
-
220
222
  }
221
223
 
222
224
 
@@ -239,28 +241,23 @@ export function getError(com, area = 0, threshold = 0) {
239
241
  }
240
242
 
241
243
 
242
- export function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
244
+ export function getExtrapolatedCommand(com1, com2, t = 0) {
243
245
 
244
246
  let { p0, cp1 } = com1;
245
247
  let { p, cp2 } = com2;
246
248
 
247
249
  // extrapolate control points
248
- let cp1_S = {
249
- x: (cp1.x - (1 - t1) * p0.x) / t1,
250
- y: (cp1.y - (1 - t1) * p0.y) / t1
250
+ cp1 = {
251
+ x: (cp1.x - (1 - t) * p0.x) / t,
252
+ y: (cp1.y - (1 - t) * p0.y) / t
251
253
  };
252
254
 
253
-
254
- let cp2_S = {
255
- x: (cp2.x - t2 * p.x) / (1 - t2),
256
- y: (cp2.y - t2 * p.y) / (1 - t2)
255
+ cp2 = {
256
+ x: (cp2.x - t * p.x) / (1 - t),
257
+ y: (cp2.y - t * p.y) / (1 - t)
257
258
  };
258
259
 
259
- let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
260
- //let pt = pointAtT([p0, cp1_S, cp2_S, p], (t1 + t2) / 2);
261
-
262
- return comS
263
-
260
+ return { p0, cp1, cp2, p };
264
261
  }
265
262
 
266
263
 
@@ -281,40 +278,14 @@ export function getBezierCommandArea(commands = [com1, com2], absolute = true) {
281
278
  }
282
279
 
283
280
 
284
- function findSplitT(com1, com2) {
285
-
286
- //let selfIntersecting = false
287
-
288
- // control tangent intersection
289
- let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false)
290
-
291
- // intersection 2nd cp1 tangent and global tangent intersection
292
- let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false)
293
-
294
-
295
- let len1 = getDistance(pt1, com2.p)
296
- let len2 = getDistance(ptI, com2.p)
297
-
298
- //let t = !t3 ? 1-len2/len1 : (t3+len2/len1)*0.5
299
- let t = 1 - len2 / len1
300
-
301
-
302
- // check self intersections
303
- //let ptI2 = checkLineIntersection(com1.cp1, com2.cp2, com1.p0, com2.p, true)
304
- //let hasInfliction = ptI2!==null
305
-
281
+ export function findSplitT(com1, com2) {
306
282
 
307
283
  let len3 = getDistance(com1.cp2, com1.p)
308
284
  let len4 = getDistance(com1.cp2, com2.cp1)
309
285
 
310
- //let t5 = 1-Math.min(len7, len8)/len9
311
- t = Math.min(len3) / len4
312
-
313
- //console.log('???selfIntersecting:', t, hasInfliction, ptI2)
286
+ let t = Math.min(len3) / len4
314
287
 
315
288
  return t
316
-
317
-
318
289
  }
319
290
 
320
291
 
@@ -3,7 +3,7 @@ import { getPathArea } from "./svgii/geometry_area";
3
3
  import { pathDataToD } from "./svgii/pathData_stringify";
4
4
  import { renderPath, renderPoint } from "./svgii/visualize";
5
5
 
6
- export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
6
+ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
7
7
 
8
8
  // cubic Bézier derivative
9
9
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -21,18 +21,14 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
21
21
  };
22
22
  }
23
23
 
24
-
25
-
26
24
  // if combining fails return original commands
27
25
  let commands = [com1, com2]
28
26
 
29
27
  // detect dominant
30
- let dist1 = getSquareDistance(com1.p0, com1.p)
31
- let dist2 = getSquareDistance(com2.p0, com2.p)
32
- let reverse = dist1 > dist2;
28
+ let dist1 = getDistAv(com1.p0, com1.p)
29
+ let dist2 = getDistAv(com2.p0, com2.p)
33
30
 
34
- //let ang1 = getAngle(com1.p0, com1.cp1)
35
- //let ang2 = getAngle(com2.p, com1.cp2)
31
+ let reverse = dist1 > dist2;
36
32
 
37
33
  // backup original commands
38
34
  let com1_o = JSON.parse(JSON.stringify(com1))
@@ -42,6 +38,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
42
38
 
43
39
  if (!ptI) {
44
40
  //renderPoint(markers, com1.p, 'purple')
41
+ //console.log('nope');
45
42
  return commands
46
43
  }
47
44
 
@@ -95,12 +92,10 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
95
92
  let r = sub(P, com1.p0);
96
93
 
97
94
  //let t0_2 = t0 - dot(r, dP) / dot(dP, dP);
98
-
99
- t0 -= dot(r, dP) / dot(dP, dP);
100
95
 
96
+ t0 -= dot(r, dP) / dot(dP, dP);
101
97
 
102
98
  // construct merged cubic over [t0, 1]
103
-
104
99
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
105
100
  let Q3 = com2.p;
106
101
 
@@ -117,6 +112,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
117
112
  cp1: Q1,
118
113
  cp2: Q2,
119
114
  p: Q3,
115
+ t0
120
116
  };
121
117
 
122
118
 
@@ -126,39 +122,46 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
126
122
  cp1: Q2,
127
123
  cp2: Q1,
128
124
  p: Q0,
125
+ t0
129
126
  }
130
127
  }
131
128
 
132
129
 
133
- let tMid = (1 - t0)*0.5 ;
130
+ let tMid = (1 - t0) * 0.5;
131
+ //tMid = (1 +t0) * 0.5;
132
+ let tSplit = t0 - 1;
134
133
  //tMid = 0.5;
135
- //console.log('t0', t0, tMid);
134
+
135
+ //console.log(1 - t0);
136
+
136
137
 
137
138
  let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true)
138
139
  let seg1_cp2 = ptM.cpts[2]
139
140
  //let seg2_cp1 = ptM.cpts[3]
140
141
 
141
-
142
142
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false)
143
143
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false)
144
144
 
145
145
 
146
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333)
147
- let cp2_2 = interpolate(result.p, ptI_2, 1.333)
146
+ //let tscale =(1 + t0)
147
+ //console.log('tscale', tscale);
148
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333 )
149
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333 )
148
150
 
149
151
  // test self intersections and exit
150
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true )
151
- if(cp_intersection){
152
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true)
153
+ if (cp_intersection) {
152
154
  //renderPoint(markers, cp_intersection )
153
155
  return commands;
154
156
  }
155
157
 
158
+ if (debug) renderPoint(markers, ptM, 'purple')
156
159
 
157
160
  result.cp1 = cp1_2
158
161
  result.cp2 = cp2_2
159
162
 
160
- // check distances
161
163
 
164
+ // check distances between original starting point and extrapolated
162
165
  let dist3 = getDistAv(com1_o.p0, result.p0)
163
166
  let dist4 = getDistAv(com2_o.p, result.p)
164
167
  let dist5 = (dist3 + dist4)
@@ -170,13 +173,43 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
170
173
  result.corner = com2_o.corner
171
174
  result.dimA = com2_o.dimA
172
175
  result.directionChange = com2_o.directionChange
176
+ result.type = 'C'
173
177
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y]
174
178
 
175
179
 
176
-
177
- // check if completely off
180
+ // extrapolated starting point is not completely off
178
181
  if (dist5 < maxDist) {
179
182
 
183
+ /*
184
+ let tTotal = 1 + Math.abs(t0);
185
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
186
+ //tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
187
+ //console.log('t0', t0, tMid, 'tSplit', tSplit);
188
+
189
+ let pO = pointAtT([com2_o.p0, com2_o.cp1, com2_o.cp2, com2_o.p], t0);
190
+ */
191
+
192
+ // split t to meet original mid segment start point
193
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
194
+
195
+ let tTotal = 1 + Math.abs(t0);
196
+ tSplit = reverse ? 1 + t0 : Math.abs(t0) / tTotal;
197
+
198
+
199
+ //console.log('t0', t0, tMid, 'tSplit', tSplit);
200
+
201
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
202
+ let distSplit = getDistAv(ptSplit, com1.p)
203
+ //console.log('distS', distS, maxDist );
204
+
205
+ // not close enough - exit
206
+ if (distSplit > maxDist * tolerance ) {
207
+ //renderPoint(markers, ptSplit, 'cyan', '1%')
208
+ //renderPoint(markers, com1.p, 'red', '0.5%')
209
+ return commands;
210
+ }
211
+
212
+
180
213
  // compare combined with original area
181
214
  let pathData0 = [
182
215
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -193,28 +226,24 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
193
226
  let areaN = getPathArea(pathDataN)
194
227
  let areaDiff = Math.abs(areaN / area0 - 1)
195
228
 
196
- result.error = areaDiff * 10 * tolerance;
229
+ result.error = areaDiff * 5 * tolerance;
197
230
  //result.error = areaDiff + dist5;
198
231
 
199
- let d = pathDataToD(pathDataN)
232
+ //debug=true;
200
233
 
201
- // success
202
- if (areaDiff < 0.01) {
203
- commands = [result];
204
- //renderPath(markers, d, 'orange')
205
- //console.log('areaDiff', areaDiff);
206
-
207
- } else {
208
- // renderPath(markers, d, 'red')
209
- // console.log('areaDiff', areaDiff);
234
+ if (debug) {
235
+ let d = pathDataToD(pathDataN)
236
+ renderPath(markers, d, 'orange')
210
237
  }
211
238
 
212
-
213
- //renderPath(markers, d, 'orange')
239
+ // success!!!
240
+ if (areaDiff < 0.05 * tolerance) {
241
+ commands = [result];
242
+ //console.log('areaDiff', areaDiff);
243
+ }
214
244
  }
215
245
 
216
246
 
217
-
218
247
  //console.log(commands);
219
248
 
220
249
  return commands