Merge lp:~inkscape+alexander/inkscape/offset-cubic-improvement into lp:~inkscape.dev/inkscape/trunk

Proposed by Alexander Brock
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
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).

To post a comment you must log in.
Revision history for this message
Alexander Brock (brock-alexander) wrote :

Comparison of old and new behaviour:

http://imgur.com/a/9ysxL

Path effect is "Join Type" with parameter "Line width" = 0.1

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Hi Alexander. Could you try with this file?
https://inkscape.org/en/~jabiertxof/%E2%98%85drawing-to-fix-line-join

Seems a bit slow and also has a point to (0,0) on the climb.

Revision history for this message
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

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Maybe you can add code to ignore it?

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Ignore all degenerate curves

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Geom::Curve::isDegenerate()

15282. By Alexander Brock <email address hidden>

Increase tolerance to match old methods precision and improve speed

15283. By Alexander Brock <email address hidden>

Make tolerance of offset_curve a parameter

Revision history for this message
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:

http://bazaar.launchpad.net/~inkscape+alexander/inkscape/offset-cubic-improvement/view/head:/src/helper/geom-pathstroke.cpp#L1116

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.7025234740496769, -0.7116605710654292), (0.7016071317351655, 0.7125639849854567)

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?

Revision history for this message
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://inkscape.org/en/paste/10473/
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

Revision history for this message
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.

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Thanks for working on this.

review: Approve (code + ux)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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.