Merge lp:~inkscape+alexander/inkscape/offset-cubic-improvement into lp:~inkscape.dev/inkscape/trunk
- offset-cubic-improvement
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 15291 |
Proposed branch: | lp:~inkscape+alexander/inkscape/offset-cubic-improvement |
Merge into: | lp:~inkscape.dev/inkscape/trunk |
Diff against target: |
392 lines (+196/-49) 2 files modified
src/helper/geom-pathstroke.cpp (+181/-47) src/helper/geom-pathstroke.h (+15/-2) |
To merge this branch: | bzr merge lp:~inkscape+alexander/inkscape/offset-cubic-improvement |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jabiertxof | code + ux | Approve | |
Review via email: mp+311898@code.launchpad.net |
Commit message
Improve precision of offset_cubic
Description of the change
I changed the behaviour of offset_cubic / half_outline:
1. Tolerance (convergence criterion) now depends on width to prevent problems with very thin lines where the old code would calculate a rough approximation.
See attachment for an example.
2. The old code does some constant adjustment to the length of the bezier handles and subdivides if the tolerance is exceeded at (t=0.5).
The new code does iterative adjustment of the length, evaluates the error at t = {0.1, 0.2, ..., 0.9} and subdivides at the point where the error is highest (if neccessary).
Alexander Brock (brock-alexander) wrote : | # |
Jabiertxof (jabiertxof) wrote : | # |
Hi Alexander. Could you try with this file?
https:/
Seems a bit slow and also has a point to (0,0) on the climb.
Jabiertxof (jabiertxof) wrote : | # |
As far I find the problem is the 0 width segment in the closing point of the mountain
m 101.00553,6.8925054 5.26676,5.3490096 c 0.49943,0.510326 0.49249,1.499268 0.21305,1.783664 l -2.61505,-2.091842 -0.51429,3.09763 -2.18499,-1.153436 -3.498951,2.210752 -1.15839,-4.660317 -1.87979,3.252214 h -2.87368 c -1.171269,0 -1.309008,-1.486387 -0.24475,-2.55064 1.85897,-2.006622 3.99243,-4.0518896 5.15181,-5.2370346 1.16533,-1.191091 3.19573,-1.1574 4.338271,0 z
Jabiertxof (jabiertxof) wrote : | # |
Maybe you can add code to ignore it?
Jabiertxof (jabiertxof) wrote : | # |
Ignore all degenerate curves
Jabiertxof (jabiertxof) wrote : | # |
Geom::Curve:
Alexander Brock (brock-alexander) wrote : | # |
I added experimental code to ignore degenerate curves, but that changed nothing.
I found the place in the code where the "L 0 0 z" is created:
content of temp:
M 15.835720153852229 8.14728407690592 C 15.835735083689977 8.14729920020721 15.835750013370388 8.147314323706892 15.835764942893457 8.147329447404967
content of temp2:
M 15.835674601501454 8.147237813173517 L 15.835720153852229 8.14728407690592
Tangents: (-0.70252347404
content of temp after the call to outline_join:
M 15.835720153852229 8.14728407690592 C 15.835735083689977 8.14729920020721 15.835750013370388 8.147314323706892 15.835764942893457 8.147329447404967 L 0 0
I tried to figure out what exactly is happening in outline_join and now my head hurts. Since the problem has nothing to do with the offset_cubic improvements, can we merge this?
Jabiertxof (jabiertxof) wrote : | # |
Hi Alexander, I just fix the problem as I try to tell in IRC. Also comment some cool improvements copied from IRC:
The Fix:
https:/
you need to apply to current trun geom-patstroke.cpp
The comments:
Just to know: is posibol remove overlaps when the offset in a path time value <= 0? Or is too complex. I could do with libarot in mayority cases but for example in the same SVG of climb when line join has a hight value, inside line cross outside line-
If this kind of work is done... simplify at ~80% the offset LPE, realy geom-pathstroke can do all work without boolops and without Livarot.
One cuestion about new tolerance.
¿Can it have a default value with similar process time than previous version? a deefault value in half_outline and in outline so you can use this parent and grandparent functions with tolerance
or with default value.
- 15284. By Alexander Brock <email address hidden>
-
Fix instability caused by degenerate paths (patch by jabiertxof)
- 15285. By Alexander Brock <email address hidden>
-
Make tolerance a parameter on every level
- 15286. By Alexander Brock <email address hidden>
-
Handle negative width correctly in tolerance calculation
Alexander Brock (brock-alexander) wrote : | # |
I made the tolerance a parameter of outline and half_outline. If not set (or set to some value <= 0) it is automatically calculated as 5% of the width parameter.
Default tolerance is 5% on my machine and runtime is 6 times slower than the old code in my benchmarks but it's not noticable on my machine in a debug build in the gui.
It creates a smaller number of segments than the old code and the accuracy is much better for small values of width, therefore I propose it for merging.
Jabiertxof (jabiertxof) wrote : | # |
Thanks for working on this.
Preview Diff
1 | === modified file 'src/helper/geom-pathstroke.cpp' |
2 | --- src/helper/geom-pathstroke.cpp 2016-08-03 13:29:38 +0000 |
3 | +++ src/helper/geom-pathstroke.cpp 2016-11-29 16:21:40 +0000 |
4 | @@ -1,6 +1,7 @@ |
5 | /* Authors: |
6 | * Liam P. White |
7 | * Tavmjong Bah |
8 | + * Alexander Brock |
9 | * |
10 | * Copyright (C) 2014-2015 Authors |
11 | * |
12 | @@ -746,33 +747,36 @@ |
13 | len = l; |
14 | } |
15 | |
16 | -void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels) |
17 | -{ |
18 | +double _offset_cubic_stable_sub( |
19 | + Geom::CubicBezier const& bez, |
20 | + Geom::CubicBezier& c, |
21 | + const Geom::Point& start_normal, |
22 | + const Geom::Point& end_normal, |
23 | + const Geom::Point& start_new, |
24 | + const Geom::Point& end_new, |
25 | + const double start_rad, |
26 | + const double end_rad, |
27 | + const double start_len, |
28 | + const double end_len, |
29 | + const double width, |
30 | + const double width_correction) { |
31 | using Geom::X; |
32 | using Geom::Y; |
33 | |
34 | - Geom::Point start_pos = bez.initialPoint(); |
35 | - Geom::Point end_pos = bez.finalPoint(); |
36 | - |
37 | - Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0)); |
38 | - Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.)); |
39 | - |
40 | - // offset the start and end control points out by the width |
41 | - Geom::Point start_new = start_pos + start_normal*width; |
42 | - Geom::Point end_new = end_pos + end_normal*width; |
43 | - |
44 | - // -------- |
45 | - double start_rad, end_rad; |
46 | - double start_len, end_len; // tangent lengths |
47 | - get_cubic_data(bez, 0, start_len, start_rad); |
48 | - get_cubic_data(bez, 1, end_len, end_rad); |
49 | - |
50 | double start_off = 1, end_off = 1; |
51 | // correction of the lengths of the tangent to the offset |
52 | if (!Geom::are_near(start_rad, 0)) |
53 | - start_off += width / start_rad; |
54 | + start_off += (width + width_correction) / start_rad; |
55 | if (!Geom::are_near(end_rad, 0)) |
56 | - end_off += width / end_rad; |
57 | + end_off += (width + width_correction) / end_rad; |
58 | + |
59 | + // We don't change the direction of the control points |
60 | + if (start_off < 0) { |
61 | + start_off = 0; |
62 | + } |
63 | + if (end_off < 0) { |
64 | + end_off = 0; |
65 | + } |
66 | start_off *= start_len; |
67 | end_off *= end_len; |
68 | // -------- |
69 | @@ -783,23 +787,137 @@ |
70 | mid2_new = Geom::Point(end_new[X] - mid2_new[X]/3., end_new[Y] - mid2_new[Y]/3.); |
71 | |
72 | // create the estimate curve |
73 | - Geom::CubicBezier c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new); |
74 | + c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new); |
75 | + |
76 | + // check the tolerance for our estimate to be a parallel curve |
77 | + |
78 | + double worst_residual = 0; |
79 | + for (size_t ii = 3; ii <= 7; ii+=2) { |
80 | + const double t = static_cast<double>(ii) / 10; |
81 | + const Geom::Point req = bez.pointAt(t); |
82 | + const Geom::Point chk = c.pointAt(c.nearestTime(req)); |
83 | + const double current_residual = (chk-req).length() - std::abs(width); |
84 | + if (std::abs(current_residual) > std::abs(worst_residual)) { |
85 | + worst_residual = current_residual; |
86 | + } |
87 | + } |
88 | + return worst_residual; |
89 | +} |
90 | + |
91 | +void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels) |
92 | +{ |
93 | + using Geom::X; |
94 | + using Geom::Y; |
95 | + |
96 | + const Geom::Point start_pos = bez.initialPoint(); |
97 | + const Geom::Point end_pos = bez.finalPoint(); |
98 | + |
99 | + const Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0)); |
100 | + const Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.)); |
101 | + |
102 | + // offset the start and end control points out by the width |
103 | + const Geom::Point start_new = start_pos + start_normal*width; |
104 | + const Geom::Point end_new = end_pos + end_normal*width; |
105 | + |
106 | + // -------- |
107 | + double start_rad, end_rad; |
108 | + double start_len, end_len; // tangent lengths |
109 | + get_cubic_data(bez, 0, start_len, start_rad); |
110 | + get_cubic_data(bez, 1, end_len, end_rad); |
111 | + |
112 | + |
113 | + Geom::CubicBezier c; |
114 | + |
115 | + double best_width_correction = 0; |
116 | + double best_residual = _offset_cubic_stable_sub( |
117 | + bez, c, |
118 | + start_normal, end_normal, |
119 | + start_new, end_new, |
120 | + start_rad, end_rad, |
121 | + start_len, end_len, |
122 | + width, best_width_correction); |
123 | + double stepsize = std::abs(width)/2; |
124 | + bool seen_success = false; |
125 | + double stepsize_threshold = 0; |
126 | + // std::cout << "Residual from " << best_residual << " "; |
127 | + size_t ii = 0; |
128 | + for (; ii < 100 && stepsize > stepsize_threshold; ++ii) { |
129 | + bool success = false; |
130 | + const double width_correction = best_width_correction - (best_residual > 0 ? 1 : -1) * stepsize; |
131 | + Geom::CubicBezier current_curve; |
132 | + const double residual = _offset_cubic_stable_sub( |
133 | + bez, current_curve, |
134 | + start_normal, end_normal, |
135 | + start_new, end_new, |
136 | + start_rad, end_rad, |
137 | + start_len, end_len, |
138 | + width, width_correction); |
139 | + if (std::abs(residual) < std::abs(best_residual)) { |
140 | + best_residual = residual; |
141 | + best_width_correction = width_correction; |
142 | + c = current_curve; |
143 | + success = true; |
144 | + if (std::abs(best_residual) < tol/4) { |
145 | + break; |
146 | + } |
147 | + } |
148 | + |
149 | + if (success) { |
150 | + if (!seen_success) { |
151 | + seen_success = true; |
152 | + //std::cout << "Stepsize factor: " << std::abs(width) / stepsize << std::endl; |
153 | + stepsize_threshold = stepsize / 1000; |
154 | + } |
155 | + } |
156 | + else { |
157 | + stepsize /= 2; |
158 | + } |
159 | + if (std::abs(best_width_correction) >= std::abs(width)/2) { |
160 | + //break; // Seems to prevent some numerical instabilities, not clear if useful |
161 | + } |
162 | + } |
163 | |
164 | // reached maximum recursive depth |
165 | // don't bother with any more correction |
166 | if (levels == 0) { |
167 | - p.append(c); |
168 | + try { |
169 | + p.append(c); |
170 | + } |
171 | + catch (...) { |
172 | + if ((p.finalPoint() - c.initialPoint()).length() < 1e-6) { |
173 | + c.setInitial(p.finalPoint()); |
174 | + } |
175 | + else { |
176 | + auto line = Geom::LineSegment(p.finalPoint(), c.initialPoint()); |
177 | + p.append(line); |
178 | + } |
179 | + p.append(c); |
180 | + } |
181 | + |
182 | return; |
183 | } |
184 | |
185 | - // check the tolerance for our estimate to be a parallel curve |
186 | - Geom::Point chk = c.pointAt(.5); |
187 | - Geom::Point req = bez.pointAt(.5) + Geom::rot90(bez.unitTangentAt(.5))*width; // required accuracy |
188 | - |
189 | - Geom::Point const diff = req - chk; |
190 | - double const err = Geom::dot(diff, diff); |
191 | - |
192 | - if (err < tol) { |
193 | + // We find the point on our new curve (c) for which the distance between |
194 | + // (c) and (bez) differs the most from the desired distance (width). |
195 | + double worst_err = std::abs(best_residual); |
196 | + double worst_time = .5; |
197 | + for (size_t ii = 1; ii <= 9; ++ii) { |
198 | + const double t = static_cast<double>(ii) / 10; |
199 | + const Geom::Point req = bez.pointAt(t); |
200 | + // We use the exact solution with nearestTime because it is numerically |
201 | + // much more stable than simply assuming that the point on (c) closest |
202 | + // to bez.pointAt(t) is given by c.pointAt(t) |
203 | + const Geom::Point chk = c.pointAt(c.nearestTime(req)); |
204 | + |
205 | + Geom::Point const diff = req - chk; |
206 | + const double err = std::abs(diff.length() - std::abs(width)); |
207 | + if (err > worst_err) { |
208 | + worst_err = err; |
209 | + worst_time = t; |
210 | + } |
211 | + } |
212 | + |
213 | + if (worst_err < tol) { |
214 | if (Geom::are_near(start_new, p.finalPoint())) { |
215 | p.setFinal(start_new); // if it isn't near, we throw |
216 | } |
217 | @@ -809,7 +927,7 @@ |
218 | return; |
219 | } else { |
220 | // split the curve in two |
221 | - std::pair<Geom::CubicBezier, Geom::CubicBezier> s = bez.subdivide(.5); |
222 | + std::pair<Geom::CubicBezier, Geom::CubicBezier> s = bez.subdivide(worst_time); |
223 | offset_cubic(p, s.first, width, tol, levels - 1); |
224 | offset_cubic(p, s.second, width, tol, levels - 1); |
225 | } |
226 | @@ -827,9 +945,8 @@ |
227 | offset_cubic(p, cub, width, tol, levels); |
228 | } |
229 | |
230 | -void offset_curve(Geom::Path& res, Geom::Curve const* current, double width) |
231 | +void offset_curve(Geom::Path& res, Geom::Curve const* current, double width, double tolerance) |
232 | { |
233 | - double const tolerance = 0.0025; |
234 | size_t levels = 8; |
235 | |
236 | if (current->isDegenerate()) return; // don't do anything |
237 | @@ -855,14 +972,14 @@ |
238 | default: { |
239 | Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), tolerance); |
240 | for (size_t i = 0; i < sbasis_path.size(); ++i) |
241 | - offset_curve(res, &sbasis_path[i], width); |
242 | + offset_curve(res, &sbasis_path[i], width, tolerance); |
243 | break; |
244 | } |
245 | } |
246 | } else { |
247 | Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), 0.1); |
248 | for (size_t i = 0; i < sbasis_path.size(); ++i) |
249 | - offset_curve(res, &sbasis_path[i], width); |
250 | + offset_curve(res, &sbasis_path[i], width, tolerance); |
251 | } |
252 | } |
253 | |
254 | @@ -902,13 +1019,19 @@ |
255 | |
256 | namespace Inkscape { |
257 | |
258 | -Geom::PathVector outline(Geom::Path const& input, double width, double miter, LineJoinType join, LineCapType butt) |
259 | +Geom::PathVector outline( |
260 | + Geom::Path const& input, |
261 | + double width, |
262 | + double miter, |
263 | + LineJoinType join, |
264 | + LineCapType butt, |
265 | + double tolerance) |
266 | { |
267 | if (input.size() == 0) return Geom::PathVector(); // nope, don't even try |
268 | |
269 | Geom::PathBuilder res; |
270 | - Geom::Path with_dir = half_outline(input, width/2., miter, join); |
271 | - Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join); |
272 | + Geom::Path with_dir = half_outline(input, width/2., miter, join, tolerance); |
273 | + Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join, tolerance); |
274 | |
275 | res.moveTo(with_dir[0].initialPoint()); |
276 | res.append(with_dir); |
277 | @@ -947,8 +1070,21 @@ |
278 | return res.peek(); |
279 | } |
280 | |
281 | -Geom::Path half_outline(Geom::Path const& input, double width, double miter, LineJoinType join) |
282 | +Geom::Path half_outline( |
283 | + Geom::Path const& input, |
284 | + double width, |
285 | + double miter, |
286 | + LineJoinType join, |
287 | + double tolerance) |
288 | { |
289 | + if (tolerance <= 0) { |
290 | + if (std::abs(width) > 0) { |
291 | + tolerance = 5.0 * (std::abs(width)/100); |
292 | + } |
293 | + else { |
294 | + tolerance = 1e-4; |
295 | + } |
296 | + } |
297 | Geom::Path res; |
298 | if (input.size() == 0) return res; |
299 | |
300 | @@ -963,12 +1099,13 @@ |
301 | res.start(start); |
302 | |
303 | // Do two curves at a time for efficiency, since the join function needs to know the outgoing curve as well |
304 | - const size_t k = (input.back_closed().isDegenerate() && input.closed()) |
305 | - ?input.size_default()-1:input.size_default(); |
306 | + const Geom::Curve &closingline = input.back_closed(); |
307 | + const size_t k = (are_near(closingline.initialPoint(), closingline.finalPoint()) && input.closed() ) |
308 | + ?input.size_open():input.size_default(); |
309 | for (size_t u = 0; u < k; u += 2) { |
310 | temp.clear(); |
311 | |
312 | - offset_curve(temp, &input[u], width); |
313 | + offset_curve(temp, &input[u], width, tolerance); |
314 | |
315 | // on the first run through, there isn't a join |
316 | if (u == 0) { |
317 | @@ -981,12 +1118,11 @@ |
318 | // odd number of paths |
319 | if (u < k - 1) { |
320 | temp.clear(); |
321 | - offset_curve(temp, &input[u+1], width); |
322 | + offset_curve(temp, &input[u+1], width, tolerance); |
323 | tangents(tang, input[u], input[u+1]); |
324 | outline_join(res, temp, tang[0], tang[1], width, miter, join); |
325 | } |
326 | } |
327 | - |
328 | if (input.closed()) { |
329 | Geom::Curve const &c1 = res.back(); |
330 | Geom::Curve const &c2 = res.front(); |
331 | @@ -998,7 +1134,6 @@ |
332 | outline_join(temp, temp2, tang[0], tang[1], width, miter, join); |
333 | res.erase(res.begin()); |
334 | res.erase_last(); |
335 | - // |
336 | res.append(temp); |
337 | res.close(); |
338 | } |
339 | @@ -1010,9 +1145,8 @@ |
340 | { |
341 | if (res.size() == 0 || temp.size() == 0) |
342 | return; |
343 | - |
344 | Geom::Curve const& outgoing = temp.front(); |
345 | - if (Geom::are_near(res.finalPoint(), outgoing.initialPoint())) { |
346 | + if (Geom::are_near(res.finalPoint(), outgoing.initialPoint(), 0.01)) { |
347 | // if the points are /that/ close, just ignore this one |
348 | res.setFinal(temp.initialPoint()); |
349 | res.append(temp); |
350 | |
351 | === modified file 'src/helper/geom-pathstroke.h' |
352 | --- src/helper/geom-pathstroke.h 2016-02-15 09:52:41 +0000 |
353 | +++ src/helper/geom-pathstroke.h 2016-11-29 16:21:40 +0000 |
354 | @@ -42,12 +42,19 @@ |
355 | * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. |
356 | * @param[in] join Line join type used during offset. Member of LineJoinType enum. |
357 | * @param[in] cap Line cap type used during stroking. Member of LineCapType enum. |
358 | + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. |
359 | * |
360 | * @return Stroked path. |
361 | * If the input path is closed, the resultant vector will contain two paths. |
362 | * Otherwise, there should be only one in the output. |
363 | */ |
364 | -Geom::PathVector outline(Geom::Path const& input, double width, double miter, LineJoinType join = JOIN_BEVEL, LineCapType cap = BUTT_FLAT); |
365 | +Geom::PathVector outline( |
366 | + Geom::Path const& input, |
367 | + double width, |
368 | + double miter, |
369 | + LineJoinType join = JOIN_BEVEL, |
370 | + LineCapType cap = BUTT_FLAT, |
371 | + double tolerance = -1); |
372 | |
373 | /** |
374 | * Offset the input path by @a width. |
375 | @@ -57,10 +64,16 @@ |
376 | * @param[in] width Amount to offset. |
377 | * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. |
378 | * @param[in] join Line join type used during offset. Member of LineJoinType enum. |
379 | + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. |
380 | * |
381 | * @return Offsetted output. |
382 | */ |
383 | -Geom::Path half_outline(Geom::Path const& input, double width, double miter, LineJoinType join = JOIN_BEVEL); |
384 | +Geom::Path half_outline( |
385 | + Geom::Path const& input, |
386 | + double width, |
387 | + double miter, |
388 | + LineJoinType join = JOIN_BEVEL, |
389 | + double tolerance = -1); |
390 | |
391 | /** |
392 | * Builds a join on the provided path. |
Comparison of old and new behaviour:
http:// imgur.com/ a/9ysxL
Path effect is "Join Type" with parameter "Line width" = 0.1