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
=== modified file 'src/helper/geom-pathstroke.cpp'
--- src/helper/geom-pathstroke.cpp 2016-08-03 13:29:38 +0000
+++ src/helper/geom-pathstroke.cpp 2016-11-29 16:21:40 +0000
@@ -1,6 +1,7 @@
1/* Authors:1/* Authors:
2 * Liam P. White2 * Liam P. White
3 * Tavmjong Bah3 * Tavmjong Bah
4 * Alexander Brock
4 *5 *
5 * Copyright (C) 2014-2015 Authors6 * Copyright (C) 2014-2015 Authors
6 *7 *
@@ -746,33 +747,36 @@
746 len = l;747 len = l;
747}748}
748749
749void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels)750double _offset_cubic_stable_sub(
750{751 Geom::CubicBezier const& bez,
752 Geom::CubicBezier& c,
753 const Geom::Point& start_normal,
754 const Geom::Point& end_normal,
755 const Geom::Point& start_new,
756 const Geom::Point& end_new,
757 const double start_rad,
758 const double end_rad,
759 const double start_len,
760 const double end_len,
761 const double width,
762 const double width_correction) {
751 using Geom::X;763 using Geom::X;
752 using Geom::Y;764 using Geom::Y;
753765
754 Geom::Point start_pos = bez.initialPoint();
755 Geom::Point end_pos = bez.finalPoint();
756
757 Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0));
758 Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.));
759
760 // offset the start and end control points out by the width
761 Geom::Point start_new = start_pos + start_normal*width;
762 Geom::Point end_new = end_pos + end_normal*width;
763
764 // --------
765 double start_rad, end_rad;
766 double start_len, end_len; // tangent lengths
767 get_cubic_data(bez, 0, start_len, start_rad);
768 get_cubic_data(bez, 1, end_len, end_rad);
769
770 double start_off = 1, end_off = 1;766 double start_off = 1, end_off = 1;
771 // correction of the lengths of the tangent to the offset767 // correction of the lengths of the tangent to the offset
772 if (!Geom::are_near(start_rad, 0))768 if (!Geom::are_near(start_rad, 0))
773 start_off += width / start_rad;769 start_off += (width + width_correction) / start_rad;
774 if (!Geom::are_near(end_rad, 0))770 if (!Geom::are_near(end_rad, 0))
775 end_off += width / end_rad;771 end_off += (width + width_correction) / end_rad;
772
773 // We don't change the direction of the control points
774 if (start_off < 0) {
775 start_off = 0;
776 }
777 if (end_off < 0) {
778 end_off = 0;
779 }
776 start_off *= start_len;780 start_off *= start_len;
777 end_off *= end_len;781 end_off *= end_len;
778 // --------782 // --------
@@ -783,23 +787,137 @@
783 mid2_new = Geom::Point(end_new[X] - mid2_new[X]/3., end_new[Y] - mid2_new[Y]/3.);787 mid2_new = Geom::Point(end_new[X] - mid2_new[X]/3., end_new[Y] - mid2_new[Y]/3.);
784788
785 // create the estimate curve789 // create the estimate curve
786 Geom::CubicBezier c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new);790 c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new);
791
792 // check the tolerance for our estimate to be a parallel curve
793
794 double worst_residual = 0;
795 for (size_t ii = 3; ii <= 7; ii+=2) {
796 const double t = static_cast<double>(ii) / 10;
797 const Geom::Point req = bez.pointAt(t);
798 const Geom::Point chk = c.pointAt(c.nearestTime(req));
799 const double current_residual = (chk-req).length() - std::abs(width);
800 if (std::abs(current_residual) > std::abs(worst_residual)) {
801 worst_residual = current_residual;
802 }
803 }
804 return worst_residual;
805}
806
807void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels)
808{
809 using Geom::X;
810 using Geom::Y;
811
812 const Geom::Point start_pos = bez.initialPoint();
813 const Geom::Point end_pos = bez.finalPoint();
814
815 const Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0));
816 const Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.));
817
818 // offset the start and end control points out by the width
819 const Geom::Point start_new = start_pos + start_normal*width;
820 const Geom::Point end_new = end_pos + end_normal*width;
821
822 // --------
823 double start_rad, end_rad;
824 double start_len, end_len; // tangent lengths
825 get_cubic_data(bez, 0, start_len, start_rad);
826 get_cubic_data(bez, 1, end_len, end_rad);
827
828
829 Geom::CubicBezier c;
830
831 double best_width_correction = 0;
832 double best_residual = _offset_cubic_stable_sub(
833 bez, c,
834 start_normal, end_normal,
835 start_new, end_new,
836 start_rad, end_rad,
837 start_len, end_len,
838 width, best_width_correction);
839 double stepsize = std::abs(width)/2;
840 bool seen_success = false;
841 double stepsize_threshold = 0;
842 // std::cout << "Residual from " << best_residual << " ";
843 size_t ii = 0;
844 for (; ii < 100 && stepsize > stepsize_threshold; ++ii) {
845 bool success = false;
846 const double width_correction = best_width_correction - (best_residual > 0 ? 1 : -1) * stepsize;
847 Geom::CubicBezier current_curve;
848 const double residual = _offset_cubic_stable_sub(
849 bez, current_curve,
850 start_normal, end_normal,
851 start_new, end_new,
852 start_rad, end_rad,
853 start_len, end_len,
854 width, width_correction);
855 if (std::abs(residual) < std::abs(best_residual)) {
856 best_residual = residual;
857 best_width_correction = width_correction;
858 c = current_curve;
859 success = true;
860 if (std::abs(best_residual) < tol/4) {
861 break;
862 }
863 }
864
865 if (success) {
866 if (!seen_success) {
867 seen_success = true;
868 //std::cout << "Stepsize factor: " << std::abs(width) / stepsize << std::endl;
869 stepsize_threshold = stepsize / 1000;
870 }
871 }
872 else {
873 stepsize /= 2;
874 }
875 if (std::abs(best_width_correction) >= std::abs(width)/2) {
876 //break; // Seems to prevent some numerical instabilities, not clear if useful
877 }
878 }
787879
788 // reached maximum recursive depth880 // reached maximum recursive depth
789 // don't bother with any more correction881 // don't bother with any more correction
790 if (levels == 0) {882 if (levels == 0) {
791 p.append(c);883 try {
884 p.append(c);
885 }
886 catch (...) {
887 if ((p.finalPoint() - c.initialPoint()).length() < 1e-6) {
888 c.setInitial(p.finalPoint());
889 }
890 else {
891 auto line = Geom::LineSegment(p.finalPoint(), c.initialPoint());
892 p.append(line);
893 }
894 p.append(c);
895 }
896
792 return;897 return;
793 }898 }
794899
795 // check the tolerance for our estimate to be a parallel curve900 // We find the point on our new curve (c) for which the distance between
796 Geom::Point chk = c.pointAt(.5);901 // (c) and (bez) differs the most from the desired distance (width).
797 Geom::Point req = bez.pointAt(.5) + Geom::rot90(bez.unitTangentAt(.5))*width; // required accuracy902 double worst_err = std::abs(best_residual);
798903 double worst_time = .5;
799 Geom::Point const diff = req - chk;904 for (size_t ii = 1; ii <= 9; ++ii) {
800 double const err = Geom::dot(diff, diff);905 const double t = static_cast<double>(ii) / 10;
801906 const Geom::Point req = bez.pointAt(t);
802 if (err < tol) {907 // We use the exact solution with nearestTime because it is numerically
908 // much more stable than simply assuming that the point on (c) closest
909 // to bez.pointAt(t) is given by c.pointAt(t)
910 const Geom::Point chk = c.pointAt(c.nearestTime(req));
911
912 Geom::Point const diff = req - chk;
913 const double err = std::abs(diff.length() - std::abs(width));
914 if (err > worst_err) {
915 worst_err = err;
916 worst_time = t;
917 }
918 }
919
920 if (worst_err < tol) {
803 if (Geom::are_near(start_new, p.finalPoint())) {921 if (Geom::are_near(start_new, p.finalPoint())) {
804 p.setFinal(start_new); // if it isn't near, we throw922 p.setFinal(start_new); // if it isn't near, we throw
805 }923 }
@@ -809,7 +927,7 @@
809 return;927 return;
810 } else {928 } else {
811 // split the curve in two929 // split the curve in two
812 std::pair<Geom::CubicBezier, Geom::CubicBezier> s = bez.subdivide(.5);930 std::pair<Geom::CubicBezier, Geom::CubicBezier> s = bez.subdivide(worst_time);
813 offset_cubic(p, s.first, width, tol, levels - 1);931 offset_cubic(p, s.first, width, tol, levels - 1);
814 offset_cubic(p, s.second, width, tol, levels - 1);932 offset_cubic(p, s.second, width, tol, levels - 1);
815 }933 }
@@ -827,9 +945,8 @@
827 offset_cubic(p, cub, width, tol, levels);945 offset_cubic(p, cub, width, tol, levels);
828}946}
829947
830void offset_curve(Geom::Path& res, Geom::Curve const* current, double width)948void offset_curve(Geom::Path& res, Geom::Curve const* current, double width, double tolerance)
831{949{
832 double const tolerance = 0.0025;
833 size_t levels = 8;950 size_t levels = 8;
834951
835 if (current->isDegenerate()) return; // don't do anything952 if (current->isDegenerate()) return; // don't do anything
@@ -855,14 +972,14 @@
855 default: {972 default: {
856 Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), tolerance);973 Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), tolerance);
857 for (size_t i = 0; i < sbasis_path.size(); ++i)974 for (size_t i = 0; i < sbasis_path.size(); ++i)
858 offset_curve(res, &sbasis_path[i], width);975 offset_curve(res, &sbasis_path[i], width, tolerance);
859 break;976 break;
860 }977 }
861 }978 }
862 } else {979 } else {
863 Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), 0.1);980 Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), 0.1);
864 for (size_t i = 0; i < sbasis_path.size(); ++i)981 for (size_t i = 0; i < sbasis_path.size(); ++i)
865 offset_curve(res, &sbasis_path[i], width);982 offset_curve(res, &sbasis_path[i], width, tolerance);
866 }983 }
867}984}
868985
@@ -902,13 +1019,19 @@
9021019
903namespace Inkscape {1020namespace Inkscape {
9041021
905Geom::PathVector outline(Geom::Path const& input, double width, double miter, LineJoinType join, LineCapType butt)1022Geom::PathVector outline(
1023 Geom::Path const& input,
1024 double width,
1025 double miter,
1026 LineJoinType join,
1027 LineCapType butt,
1028 double tolerance)
906{1029{
907 if (input.size() == 0) return Geom::PathVector(); // nope, don't even try1030 if (input.size() == 0) return Geom::PathVector(); // nope, don't even try
9081031
909 Geom::PathBuilder res;1032 Geom::PathBuilder res;
910 Geom::Path with_dir = half_outline(input, width/2., miter, join);1033 Geom::Path with_dir = half_outline(input, width/2., miter, join, tolerance);
911 Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join);1034 Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join, tolerance);
9121035
913 res.moveTo(with_dir[0].initialPoint());1036 res.moveTo(with_dir[0].initialPoint());
914 res.append(with_dir);1037 res.append(with_dir);
@@ -947,8 +1070,21 @@
947 return res.peek();1070 return res.peek();
948}1071}
9491072
950Geom::Path half_outline(Geom::Path const& input, double width, double miter, LineJoinType join)1073Geom::Path half_outline(
1074 Geom::Path const& input,
1075 double width,
1076 double miter,
1077 LineJoinType join,
1078 double tolerance)
951{1079{
1080 if (tolerance <= 0) {
1081 if (std::abs(width) > 0) {
1082 tolerance = 5.0 * (std::abs(width)/100);
1083 }
1084 else {
1085 tolerance = 1e-4;
1086 }
1087 }
952 Geom::Path res;1088 Geom::Path res;
953 if (input.size() == 0) return res;1089 if (input.size() == 0) return res;
9541090
@@ -963,12 +1099,13 @@
963 res.start(start);1099 res.start(start);
9641100
965 // Do two curves at a time for efficiency, since the join function needs to know the outgoing curve as well1101 // Do two curves at a time for efficiency, since the join function needs to know the outgoing curve as well
966 const size_t k = (input.back_closed().isDegenerate() && input.closed())1102 const Geom::Curve &closingline = input.back_closed();
967 ?input.size_default()-1:input.size_default();1103 const size_t k = (are_near(closingline.initialPoint(), closingline.finalPoint()) && input.closed() )
1104 ?input.size_open():input.size_default();
968 for (size_t u = 0; u < k; u += 2) {1105 for (size_t u = 0; u < k; u += 2) {
969 temp.clear();1106 temp.clear();
9701107
971 offset_curve(temp, &input[u], width);1108 offset_curve(temp, &input[u], width, tolerance);
9721109
973 // on the first run through, there isn't a join1110 // on the first run through, there isn't a join
974 if (u == 0) {1111 if (u == 0) {
@@ -981,12 +1118,11 @@
981 // odd number of paths1118 // odd number of paths
982 if (u < k - 1) {1119 if (u < k - 1) {
983 temp.clear();1120 temp.clear();
984 offset_curve(temp, &input[u+1], width);1121 offset_curve(temp, &input[u+1], width, tolerance);
985 tangents(tang, input[u], input[u+1]);1122 tangents(tang, input[u], input[u+1]);
986 outline_join(res, temp, tang[0], tang[1], width, miter, join);1123 outline_join(res, temp, tang[0], tang[1], width, miter, join);
987 }1124 }
988 }1125 }
989
990 if (input.closed()) {1126 if (input.closed()) {
991 Geom::Curve const &c1 = res.back();1127 Geom::Curve const &c1 = res.back();
992 Geom::Curve const &c2 = res.front();1128 Geom::Curve const &c2 = res.front();
@@ -998,7 +1134,6 @@
998 outline_join(temp, temp2, tang[0], tang[1], width, miter, join);1134 outline_join(temp, temp2, tang[0], tang[1], width, miter, join);
999 res.erase(res.begin());1135 res.erase(res.begin());
1000 res.erase_last();1136 res.erase_last();
1001 //
1002 res.append(temp);1137 res.append(temp);
1003 res.close();1138 res.close();
1004 }1139 }
@@ -1010,9 +1145,8 @@
1010{1145{
1011 if (res.size() == 0 || temp.size() == 0)1146 if (res.size() == 0 || temp.size() == 0)
1012 return;1147 return;
1013
1014 Geom::Curve const& outgoing = temp.front();1148 Geom::Curve const& outgoing = temp.front();
1015 if (Geom::are_near(res.finalPoint(), outgoing.initialPoint())) {1149 if (Geom::are_near(res.finalPoint(), outgoing.initialPoint(), 0.01)) {
1016 // if the points are /that/ close, just ignore this one1150 // if the points are /that/ close, just ignore this one
1017 res.setFinal(temp.initialPoint());1151 res.setFinal(temp.initialPoint());
1018 res.append(temp);1152 res.append(temp);
10191153
=== modified file 'src/helper/geom-pathstroke.h'
--- src/helper/geom-pathstroke.h 2016-02-15 09:52:41 +0000
+++ src/helper/geom-pathstroke.h 2016-11-29 16:21:40 +0000
@@ -42,12 +42,19 @@
42 * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE.42 * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE.
43 * @param[in] join Line join type used during offset. Member of LineJoinType enum.43 * @param[in] join Line join type used during offset. Member of LineJoinType enum.
44 * @param[in] cap Line cap type used during stroking. Member of LineCapType enum.44 * @param[in] cap Line cap type used during stroking. Member of LineCapType enum.
45 * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width.
45 *46 *
46 * @return Stroked path.47 * @return Stroked path.
47 * If the input path is closed, the resultant vector will contain two paths.48 * If the input path is closed, the resultant vector will contain two paths.
48 * Otherwise, there should be only one in the output.49 * Otherwise, there should be only one in the output.
49 */50 */
50Geom::PathVector outline(Geom::Path const& input, double width, double miter, LineJoinType join = JOIN_BEVEL, LineCapType cap = BUTT_FLAT);51Geom::PathVector outline(
52 Geom::Path const& input,
53 double width,
54 double miter,
55 LineJoinType join = JOIN_BEVEL,
56 LineCapType cap = BUTT_FLAT,
57 double tolerance = -1);
5158
52/**59/**
53 * Offset the input path by @a width.60 * Offset the input path by @a width.
@@ -57,10 +64,16 @@
57 * @param[in] width Amount to offset.64 * @param[in] width Amount to offset.
58 * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE.65 * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE.
59 * @param[in] join Line join type used during offset. Member of LineJoinType enum.66 * @param[in] join Line join type used during offset. Member of LineJoinType enum.
67 * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width.
60 *68 *
61 * @return Offsetted output.69 * @return Offsetted output.
62 */70 */
63Geom::Path half_outline(Geom::Path const& input, double width, double miter, LineJoinType join = JOIN_BEVEL);71Geom::Path half_outline(
72 Geom::Path const& input,
73 double width,
74 double miter,
75 LineJoinType join = JOIN_BEVEL,
76 double tolerance = -1);
6477
65/**78/**
66 * Builds a join on the provided path.79 * Builds a join on the provided path.