diff options
Diffstat (limited to 'src/live_effects/lpe-powerstroke.cpp')
| -rw-r--r-- | src/live_effects/lpe-powerstroke.cpp | 420 |
1 files changed, 256 insertions, 164 deletions
diff --git a/src/live_effects/lpe-powerstroke.cpp b/src/live_effects/lpe-powerstroke.cpp index 5dc170e84..25cb72b42 100644 --- a/src/live_effects/lpe-powerstroke.cpp +++ b/src/live_effects/lpe-powerstroke.cpp @@ -1,16 +1,17 @@ -#define INKSCAPE_LPE_POWERSTROKE_CPP -/** \file - * @brief PowerStroke LPE implementation. Creates curves with modifiable stroke width. +/** + * @file + * PowerStroke LPE implementation. Creates curves with modifiable stroke width. */ /* Authors: - * Johan Engelen <j.b.c.engelen@utwente.nl> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> * - * Copyright (C) 2010 Authors + * Copyright (C) 2010-2011 Authors * * Released under GNU GPL, read the file 'COPYING' for more information */ #include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-powerstroke-interpolators.h" #include "sp-shape.h" #include "display/curve.h" @@ -20,123 +21,72 @@ #include <2geom/sbasis-geometric.h> #include <2geom/transforms.h> #include <2geom/bezier-utils.h> +#include <2geom/svg-elliptical-arc.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/svg-path.h> -/// @TODO Move this to 2geom -namespace Geom { -namespace Interpolate { - -class Interpolator { -public: - Interpolator() {}; - virtual ~Interpolator() {}; - -// virtual Piecewise<D2<SBasis> > interpolateToPwD2Sb(std::vector<Point> points) = 0; - virtual Path interpolateToPath(std::vector<Point> points) = 0; +namespace Inkscape { +namespace LivePathEffect { -private: - Interpolator(const Interpolator&); - Interpolator& operator=(const Interpolator&); +static const Util::EnumData<unsigned> InterpolatorTypeData[] = { + {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"}, + {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"}, + {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"}, + {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"} }; +static const Util::EnumDataConverter<unsigned> InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData)); -class Linear : public Interpolator { -public: - Linear() {}; - virtual ~Linear() {}; - - virtual Path interpolateToPath(std::vector<Point> points) { - Path path; - path.start( points.at(0) ); - for (unsigned int i = 1 ; i < points.size(); ++i) { - path.appendNew<Geom::LineSegment>(points.at(i)); - } - return path; - }; - -private: - Linear(const Linear&); - Linear& operator=(const Linear&); +enum LineCapType { + LINECAP_BUTT, + LINECAP_SQUARE, + LINECAP_ROUND, + LINECAP_PEAK }; - -// this class is terrible -class CubicBezierFit : public Interpolator { -public: - CubicBezierFit() {}; - virtual ~CubicBezierFit() {}; - - virtual Path interpolateToPath(std::vector<Point> points) { - unsigned int n_points = points.size(); - // worst case gives us 2 segment per point - int max_segs = 8*n_points; - Geom::Point * b = g_new(Geom::Point, max_segs); - Geom::Point * points_array = g_new(Geom::Point, 4*n_points); - for (unsigned i = 0; i < n_points; ++i) { - points_array[i] = points.at(i); - } - - double tolerance_sq = 0; // this value is just a random guess - - int const n_segs = Geom::bezier_fit_cubic_r(b, points_array, n_points, - tolerance_sq, max_segs); - - Geom::Path fit; - if ( n_segs > 0) - { - fit.start(b[0]); - for (int c = 0; c < n_segs; c++) { - fit.appendNew<Geom::CubicBezier>(b[4*c+1], b[4*c+2], b[4*c+3]); - } - } - g_free(b); - g_free(points_array); - return fit; - }; - -private: - CubicBezierFit(const CubicBezierFit&); - CubicBezierFit& operator=(const CubicBezierFit&); +static const Util::EnumData<unsigned> LineCapTypeData[] = { + {LINECAP_BUTT , N_("Butt"), "butt"}, + {LINECAP_SQUARE, N_("Square"), "square"}, + {LINECAP_ROUND , N_("Round"), "round"}, + {LINECAP_PEAK , N_("Peak"), "peak"} }; +static const Util::EnumDataConverter<unsigned> LineCapTypeConverter(LineCapTypeData, sizeof(LineCapTypeData)/sizeof(*LineCapTypeData)); -/// @todo invent name for this class -class CubicBezierJohan : public Interpolator { -public: - CubicBezierJohan() {}; - virtual ~CubicBezierJohan() {}; - - virtual Path interpolateToPath(std::vector<Point> points) { - Path fit; - fit.start(points.at(0)); - for (unsigned int i = 1; i < points.size(); ++i) { - Point p0 = points.at(i-1); - Point p1 = points.at(i); - Point dx = Point(p1[X] - p0[X], 0); - fit.appendNew<CubicBezier>(p0+0.2*dx, p1-0.2*dx, p1); - } - return fit; - }; - -private: - CubicBezierJohan(const CubicBezierJohan&); - CubicBezierJohan& operator=(const CubicBezierJohan&); +enum LineCuspType { + LINECUSP_BEVEL, + LINECUSP_ROUND, + LINECUSP_SHARP }; - -} //namespace Interpolate -} //namespace Geom - -namespace Inkscape { -namespace LivePathEffect { +static const Util::EnumData<unsigned> LineCuspTypeData[] = { + {LINECUSP_BEVEL , N_("Beveled"), "bevel"}, + {LINECUSP_ROUND , N_("Rounded"), "round"}, +// not yet supported {LINECUSP_SHARP , N_("Sharp"), "sharp"} +}; +static const Util::EnumDataConverter<unsigned> LineCuspTypeConverter(LineCuspTypeData, sizeof(LineCuspTypeData)/sizeof(*LineCuspTypeData)); LPEPowerStroke::LPEPowerStroke(LivePathEffectObject *lpeobject) : Effect(lpeobject), offset_points(_("Offset points"), _("Offset points"), "offset_points", &wr, this), - sort_points(_("Sort points"), _("Sort offset points according to their time value along the curve."), "sort_points", &wr, this, true) + sort_points(_("Sort points"), _("Sort offset points according to their time value along the curve."), "sort_points", &wr, this, true), + interpolator_type(_("Interpolator type"), _("Determines which kind of interpolator will be used to interpolate between stroke width along the path."), "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN), + interpolator_beta(_("Smoothness"), _("Sets the smoothness for the CubicBezierJohan interpolator. 0 = linear interpolation, 1 = smooth"), "interpolator_beta", &wr, this, 0.2), + start_linecap_type(_("Start line cap type"), _("Determines the shape of the path's start."), "start_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ROUND), + cusp_linecap_type(_("Cusp line cap type"), _("Determines the shape of the cusps along the path."), "cusp_linecap_type", LineCuspTypeConverter, &wr, this, LINECUSP_ROUND), + end_linecap_type(_("End line cap type"), _("Determines the shape of the path's end."), "end_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ROUND) { show_orig_path = true; /// @todo offset_points are initialized with empty path, is that bug-save? + interpolator_beta.addSlider(true); + interpolator_beta.param_set_range(0.,1.); + registerParameter( dynamic_cast<Parameter *>(&offset_points) ); registerParameter( dynamic_cast<Parameter *>(&sort_points) ); + registerParameter( dynamic_cast<Parameter *>(&interpolator_type) ); + registerParameter( dynamic_cast<Parameter *>(&interpolator_beta) ); + registerParameter( dynamic_cast<Parameter *>(&start_linecap_type) ); + registerParameter( dynamic_cast<Parameter *>(&cusp_linecap_type) ); + registerParameter( dynamic_cast<Parameter *>(&end_linecap_type) ); } LPEPowerStroke::~LPEPowerStroke() @@ -156,95 +106,237 @@ LPEPowerStroke::doOnApply(SPLPEItem *lpeitem) offset_points.param_set_and_write_new_value(points); } +void +LPEPowerStroke::adjustForNewPath(std::vector<Geom::Path> const & path_in) +{ + offset_points.recalculate_controlpoints_for_new_pwd2(path_in[0].toPwSb()); +} + static bool compare_offsets (Geom::Point first, Geom::Point second) { return first[Geom::X] < second[Geom::X]; } - -Geom::Piecewise<Geom::D2<Geom::SBasis> > -LPEPowerStroke::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +// find discontinuities in input path +struct discontinuity_data { + Geom::Point der0; // unit derivative of 'left' side of cusp + Geom::Point der1; // unit derivative of 'right' side of cusp + double width; // intended stroke width at cusp +}; +std::vector<discontinuity_data> find_discontinuities( Geom::Piecewise<Geom::D2<Geom::SBasis> > const & der, + Geom::Piecewise<Geom::SBasis> const & x, + Geom::Piecewise<Geom::SBasis> const & y, + double eps=Geom::EPSILON ) { - using namespace Geom; - - offset_points.set_pwd2(pwd2_in); + std::vector<discontinuity_data> vect; + for(unsigned i = 1; i < der.size(); i++) { + if ( ! are_near(der[i-1].at1(), der[i].at0(), eps) ) { + discontinuity_data data; + data.der0 = der[i-1].at1(); + data.der1 = der[i].at0(); + double t = der.cuts[i]; + std::vector< double > rts = roots (x - t); /// @todo this has multiple solutions for general strokewidth paths (generated by spiro interpolator...), ignore for now + if (!rts.empty()) { + data.width = y(rts.front()); + } else { + data.width = 1; + } + vect.push_back(data); + } + } + return vect; +} - Piecewise<D2<SBasis> > der = unitVector(derivative(pwd2_in)); - Piecewise<D2<SBasis> > n = rot90(der); - offset_points.set_pwd2_normal(n); - // see if we should treat the path as being closed. - bool closed_path = false; - if ( are_near(pwd2_in.firstValue(), pwd2_in.lastValue()) ) { - closed_path = true; +Geom::Path path_from_piecewise_fix_cusps( Geom::Piecewise<Geom::D2<Geom::SBasis> > const & B, + std::vector<discontinuity_data> const & cusps, + LineCuspType cusp_linecap, + double tol=Geom::EPSILON) +{ +/* per definition, each discontinuity should be fixed with a cusp-ending, as defined by cusp_linecap_type +*/ + Geom::PathBuilder pb; + if (B.size() == 0) { + return pb.peek().front(); } - Piecewise<D2<SBasis> > output; - if (!closed_path) { - // perhaps use std::list instead of std::vector? - std::vector<Geom::Point> ts(offset_points.data().size() + 2); - // first and last point coincide with input path (for now at least) - ts.front() = Point(pwd2_in.domain().min(),0); - ts.back() = Point(pwd2_in.domain().max(),0); - for (unsigned int i = 0; i < offset_points.data().size(); ++i) { - ts.at(i+1) = offset_points.data().at(i); - } + unsigned int cusp_i = 0; + Geom::Point start = B[0].at0(); + pb.moveTo(start); + build_from_sbasis(pb, B[0], tol, false); + for (unsigned i=1; i < B.size(); i++) { + if (!are_near(B[i-1].at1(), B[i].at0(), tol) ) + { // discontinuity found, so fix it :-) + discontinuity_data const &cusp = cusps[cusp_i]; + + switch (cusp_linecap) { + case LINECUSP_ROUND: // properly bugged ^_^ + pb.arcTo( abs(cusp.width), abs(cusp.width), + angle_between(cusp.der0, cusp.der1), false, cusp.width < 0, + B[i].at0() ); + break; + case LINECUSP_SHARP: // no clue yet what to do here :) + case LINECUSP_BEVEL: + default: + pb.lineTo(B[i].at0()); + break; + } - if (sort_points) { - sort(ts.begin(), ts.end(), compare_offsets); + cusp_i++; } + build_from_sbasis(pb, B[i], tol, false); + } + pb.finish(); + return pb.peek().front(); +} + - // create stroke path where points (x,y) := (t, offset) - Geom::Interpolate::CubicBezierJohan interpolator; - Path strokepath = interpolator.interpolateToPath(ts); - Path mirroredpath = strokepath.reverse() * Geom::Scale(1,-1); +std::vector<Geom::Path> +LPEPowerStroke::doEffect_path (std::vector<Geom::Path> const & path_in) +{ + using namespace Geom; - strokepath.append(mirroredpath, Geom::Path::STITCH_DISCONTINUOUS); - strokepath.close(); + std::vector<Geom::Path> path_out; + if (path_in.size() == 0) { + return path_out; + } - D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(strokepath.toPwSb()); - Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]); - Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]); + // for now, only regard first subpath and ignore the rest + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in = path_in[0].toPwSb(); - output = compose(pwd2_in,x) + y*compose(n,x); - } else { - // path is closed + Piecewise<D2<SBasis> > der = unitVector(derivative(pwd2_in)); + Piecewise<D2<SBasis> > n = rot90(der); + offset_points.set_pwd2(pwd2_in, n); - // perhaps use std::list instead of std::vector? - std::vector<Geom::Point> ts = offset_points.data(); - if (sort_points) { - sort(ts.begin(), ts.end(), compare_offsets); - } + std::vector<Geom::Point> ts = offset_points.data(); + if (sort_points) { + sort(ts.begin(), ts.end(), compare_offsets); + } + if (path_in[0].closed()) { // add extra points for interpolation between first and last point Point first_point = ts.front(); Point last_point = ts.back(); ts.insert(ts.begin(), last_point - Point(pwd2_in.domain().extent() ,0)); ts.push_back( first_point + Point(pwd2_in.domain().extent() ,0) ); - // create stroke path where points (x,y) := (t, offset) - Geom::Interpolate::CubicBezierJohan interpolator; - Path strokepath = interpolator.interpolateToPath(ts); - - // output 2 separate paths - D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(strokepath.toPwSb()); - Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]); - Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]); - // find time values for which x lies outside path domain - // and only take portion of x and y that lies within those time values - std::vector< double > rtsmin = roots (x - pwd2_in.domain().min()); - std::vector< double > rtsmax = roots (x - pwd2_in.domain().max()); - if ( !rtsmin.empty() && !rtsmax.empty() ) { - x = portion(x, rtsmin.at(0), rtsmax.at(0)); - y = portion(y, rtsmin.at(0), rtsmax.at(0)); + } else { + // first and last point have same distance from path as second and second to last points, respectively. + ts.insert(ts.begin(), Point(pwd2_in.domain().min(), ts.front()[Geom::Y]) ); + ts.push_back( Point(pwd2_in.domain().max(), ts.back()[Geom::Y]) ); + } + // create stroke path where points (x,y) := (t, offset) + Geom::Interpolate::Interpolator *interpolator = Geom::Interpolate::Interpolator::create(static_cast<Geom::Interpolate::InterpolatorType>(interpolator_type.get_value())); + if (Geom::Interpolate::CubicBezierJohan *johan = dynamic_cast<Geom::Interpolate::CubicBezierJohan*>(interpolator)) { + johan->setBeta(interpolator_beta); + } + Geom::Path strokepath = interpolator->interpolateToPath(ts); + delete interpolator; + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(strokepath.toPwSb()); + Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]); + // find time values for which x lies outside path domain + // and only take portion of x and y that lies within those time values + std::vector< double > rtsmin = roots (x - pwd2_in.domain().min()); + std::vector< double > rtsmax = roots (x - pwd2_in.domain().max()); + if ( !rtsmin.empty() && !rtsmax.empty() ) { + x = portion(x, rtsmin.at(0), rtsmax.at(0)); + y = portion(y, rtsmin.at(0), rtsmax.at(0)); + } + + std::vector<discontinuity_data> cusps = find_discontinuities(der, x, y); + LineCuspType cusp_linecap = static_cast<LineCuspType>(cusp_linecap_type.get_value()); + + Piecewise<D2<SBasis> > pwd2_out = compose(pwd2_in,x) + y*compose(n,x); + Piecewise<D2<SBasis> > mirrorpath = reverse(compose(pwd2_in,x) - y*compose(n,x)); + + Geom::Path fixed_path = path_from_piecewise_fix_cusps( pwd2_out, cusps, cusp_linecap, LPE_CONVERSION_TOLERANCE); + Geom::Path fixed_mirrorpath = path_from_piecewise_fix_cusps( mirrorpath, cusps, cusp_linecap, LPE_CONVERSION_TOLERANCE); + + if (path_in[0].closed()) { + fixed_path.close(true); + path_out.push_back(fixed_path); + fixed_mirrorpath.close(true); + path_out.push_back(fixed_mirrorpath); + } else { + // add linecaps... + LineCapType end_linecap = static_cast<LineCapType>(end_linecap_type.get_value()); + LineCapType start_linecap = static_cast<LineCapType>(start_linecap_type.get_value()); + switch (end_linecap) { + case LINECAP_PEAK: + { + Geom::Point end_deriv = der.lastValue(); + double radius = 0.5 * distance(pwd2_out.lastValue(), mirrorpath.firstValue()); + Geom::Point midpoint = 0.5*(pwd2_out.lastValue() + mirrorpath.firstValue()) + radius*end_deriv; + fixed_path.appendNew<LineSegment>(midpoint); + fixed_path.appendNew<LineSegment>(mirrorpath.firstValue()); + break; + } + case LINECAP_SQUARE: + { + Geom::Point end_deriv = der.lastValue(); + double radius = 0.5 * distance(pwd2_out.lastValue(), mirrorpath.firstValue()); + fixed_path.appendNew<LineSegment>( pwd2_out.lastValue() + radius*end_deriv ); + fixed_path.appendNew<LineSegment>( mirrorpath.firstValue() + radius*end_deriv ); + fixed_path.appendNew<LineSegment>( mirrorpath.firstValue() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew<LineSegment>( mirrorpath.firstValue() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius1 = 0.5 * distance(pwd2_out.lastValue(), mirrorpath.firstValue()); + fixed_path.appendNew<SVGEllipticalArc>( radius1, radius1, M_PI/2., false, y.lastValue() < 0, mirrorpath.firstValue() ); + break; + } } - output = compose(pwd2_in,x) + y*compose(n,x); - x = reverse(x); - y = reverse(y); - output.concat(compose(pwd2_in,x) - y*compose(n,x)); + + fixed_path.append(fixed_mirrorpath, Geom::Path::STITCH_DISCONTINUOUS); + + switch (start_linecap) { + case LINECAP_PEAK: + { + Geom::Point start_deriv = der.firstValue(); + double radius = 0.5 * distance(pwd2_out.firstValue(), mirrorpath.lastValue()); + Geom::Point midpoint = 0.5*(mirrorpath.lastValue() + pwd2_out.firstValue()) - radius*start_deriv; + fixed_path.appendNew<LineSegment>( midpoint ); + fixed_path.appendNew<LineSegment>( pwd2_out.firstValue() ); + break; + } + case LINECAP_SQUARE: + { + Geom::Point start_deriv = der.firstValue(); + double radius = 0.5 * distance(pwd2_out.firstValue(), mirrorpath.lastValue()); + fixed_path.appendNew<LineSegment>( mirrorpath.lastValue() - radius*start_deriv ); + fixed_path.appendNew<LineSegment>( pwd2_out.firstValue() - radius*start_deriv ); + fixed_path.appendNew<LineSegment>( pwd2_out.firstValue() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew<LineSegment>( pwd2_out.firstValue() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius2 = 0.5 * distance(pwd2_out.firstValue(), mirrorpath.lastValue()); + fixed_path.appendNew<SVGEllipticalArc>( radius2, radius2, M_PI/2., false, y.firstValue() < 0, pwd2_out.firstValue() ); + break; + } + } + + fixed_path.close(true); + path_out.push_back(fixed_path); } - return output; + return path_out; } + /* ######################## */ } //namespace LivePathEffect |
