svg-path-simplify 0.0.8 → 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 (34) hide show
  1. package/README.md +25 -5
  2. package/dist/svg-path-simplify.esm.js +576 -494
  3. package/dist/svg-path-simplify.esm.min.js +1 -1
  4. package/dist/svg-path-simplify.js +576 -494
  5. package/dist/svg-path-simplify.min.js +1 -1
  6. package/dist/svg-path-simplify.node.js +576 -494
  7. package/dist/svg-path-simplify.node.min.js +1 -1
  8. package/index.html +86 -29
  9. package/package.json +1 -1
  10. package/src/detect_input.js +17 -10
  11. package/src/index.js +3 -0
  12. package/src/pathData_simplify_cubic.js +113 -106
  13. package/src/pathData_simplify_cubic_extrapolate.js +25 -11
  14. package/src/pathSimplify-main.js +89 -182
  15. package/src/svgii/geometry_flatness.js +29 -36
  16. package/src/svgii/pathData_analyze.js +4 -0
  17. package/src/svgii/pathData_convert.js +26 -17
  18. package/src/svgii/pathData_interpolate.js +65 -0
  19. package/src/svgii/pathData_parse.js +25 -9
  20. package/src/svgii/pathData_parse_els.js +18 -12
  21. package/src/svgii/pathData_remove_collinear.js +31 -28
  22. package/src/svgii/pathData_remove_orphaned.js +5 -4
  23. package/src/svgii/pathData_remove_zerolength.js +8 -4
  24. package/src/svgii/pathData_reorder.js +6 -2
  25. package/src/svgii/pathData_simplify_refineCorners.js +160 -0
  26. package/src/svgii/{simplify_refineExtremes.js → pathData_simplify_refineExtremes.js} +78 -43
  27. package/src/svgii/pathData_split.js +42 -15
  28. package/src/svgii/pathData_stringify.js +3 -12
  29. package/src/svgii/rounding.js +16 -14
  30. package/src/svgii/svg_cleanup.js +1 -1
  31. package/src/pathData_simplify_cubic_arr.js +0 -50
  32. package/src/svgii/simplify.js +0 -248
  33. package/src/svgii/simplify_bezier.js +0 -470
  34. 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))
124
+
77
125
 
78
- let distScale = 0.05
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,34 +197,10 @@ export function combineCubicPairs(com1, com2, extrapolateDominant = false, toler
159
197
  } // end 1st try
160
198
 
161
199
 
162
-
163
-
164
- // try extrapolated dominant curve
165
- //&& !com2.extreme
166
- // && !com1.extreme
167
- if (extrapolateDominant && !success ) {
168
-
169
- let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
170
-
171
- //console.log('???combinedEx', combinedEx);
172
-
173
- if(combinedEx.length===1){
174
- success = true
175
- comS = combinedEx[0]
176
- error = comS.error
177
-
178
- //console.log('!!!combinedEx', combinedEx);
179
- }
180
-
181
-
182
- }
183
200
 
184
201
  // add meta
185
202
  if (success) {
186
203
 
187
- //comS.dimA = (Math.abs(comS.p0.x - comS.p.x) + Math.abs(comS.p0.y - comS.p.y)) / 2
188
-
189
-
190
204
  // correct to exact start and end points
191
205
  comS.p0 = com1.p0
192
206
  comS.p = com2.p
@@ -205,8 +219,6 @@ export function combineCubicPairs(com1, com2, extrapolateDominant = false, toler
205
219
  //comS.p0 = com1.p0;
206
220
  commands = [comS];
207
221
 
208
- //console.log('commands combined', commands);
209
-
210
222
  }
211
223
 
212
224
 
@@ -229,28 +241,23 @@ export function getError(com, area = 0, threshold = 0) {
229
241
  }
230
242
 
231
243
 
232
- export function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
244
+ export function getExtrapolatedCommand(com1, com2, t = 0) {
233
245
 
234
246
  let { p0, cp1 } = com1;
235
247
  let { p, cp2 } = com2;
236
248
 
237
249
  // extrapolate control points
238
- let cp1_S = {
239
- x: (cp1.x - (1 - t1) * p0.x) / t1,
240
- 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
241
253
  };
242
254
 
243
-
244
- let cp2_S = {
245
- x: (cp2.x - t2 * p.x) / (1 - t2),
246
- 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)
247
258
  };
248
259
 
249
- let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
250
- //let pt = pointAtT([p0, cp1_S, cp2_S, p], (t1 + t2) / 2);
251
-
252
- return comS
253
-
260
+ return { p0, cp1, cp2, p };
254
261
  }
255
262
 
256
263
 
@@ -271,7 +278,7 @@ export function getBezierCommandArea(commands = [com1, com2], absolute = true) {
271
278
  }
272
279
 
273
280
 
274
- function findSplitT(com1, com2) {
281
+ export function findSplitT(com1, com2) {
275
282
 
276
283
  let len3 = getDistance(com1.cp2, com1.p)
277
284
  let len4 = getDistance(com1.cp2, com2.cp1)
@@ -21,8 +21,6 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
21
21
  };
22
22
  }
23
23
 
24
-
25
-
26
24
  // if combining fails return original commands
27
25
  let commands = [com1, com2]
28
26
 
@@ -30,12 +28,8 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
30
28
  let dist1 = getDistAv(com1.p0, com1.p)
31
29
  let dist2 = getDistAv(com2.p0, com2.p)
32
30
 
33
-
34
31
  let reverse = dist1 > dist2;
35
32
 
36
- //let ang1 = getAngle(com1.p0, com1.cp1)
37
- //let ang2 = getAngle(com2.p, com1.cp2)
38
-
39
33
  // backup original commands
40
34
  let com1_o = JSON.parse(JSON.stringify(com1))
41
35
  let com2_o = JSON.parse(JSON.stringify(com2))
@@ -118,6 +112,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
118
112
  cp1: Q1,
119
113
  cp2: Q2,
120
114
  p: Q3,
115
+ t0
121
116
  };
122
117
 
123
118
 
@@ -127,14 +122,18 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
127
122
  cp1: Q2,
128
123
  cp2: Q1,
129
124
  p: Q0,
125
+ t0
130
126
  }
131
127
  }
132
128
 
133
129
 
134
130
  let tMid = (1 - t0) * 0.5;
131
+ //tMid = (1 +t0) * 0.5;
135
132
  let tSplit = t0 - 1;
136
133
  //tMid = 0.5;
137
134
 
135
+ //console.log(1 - t0);
136
+
138
137
 
139
138
  let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true)
140
139
  let seg1_cp2 = ptM.cpts[2]
@@ -144,10 +143,10 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
144
143
  let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false)
145
144
 
146
145
 
147
-
148
-
149
- let cp1_2 = interpolate(result.p0, ptI_1, 1.333)
150
- 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 )
151
150
 
152
151
  // test self intersections and exit
153
152
  let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true)
@@ -181,8 +180,22 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
181
180
  // extrapolated starting point is not completely off
182
181
  if (dist5 < maxDist) {
183
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
+
184
192
  // split t to meet original mid segment start point
185
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
+
186
199
  //console.log('t0', t0, tMid, 'tSplit', tSplit);
187
200
 
188
201
  let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
@@ -190,7 +203,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
190
203
  //console.log('distS', distS, maxDist );
191
204
 
192
205
  // not close enough - exit
193
- if (distSplit > maxDist * tolerance) {
206
+ if (distSplit > maxDist * tolerance ) {
194
207
  //renderPoint(markers, ptSplit, 'cyan', '1%')
195
208
  //renderPoint(markers, com1.p, 'red', '0.5%')
196
209
  return commands;
@@ -216,6 +229,7 @@ export function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, de
216
229
  result.error = areaDiff * 5 * tolerance;
217
230
  //result.error = areaDiff + dist5;
218
231
 
232
+ //debug=true;
219
233
 
220
234
  if (debug) {
221
235
  let d = pathDataToD(pathDataN)