/** * @file * Taper Stroke path effect, provided as an alternative to Power Strokes * for otherwise constant-width paths. * * Authors: * Liam P White * * Copyright (C) 2014 Authors * * Released under GNU GPL, read the file 'COPYING' for more information */ #include "live_effects/lpe-taperstroke.h" #include <2geom/path.h> #include <2geom/shape.h> #include <2geom/path.h> #include <2geom/circle.h> #include <2geom/sbasis-to-bezier.h> #include "pathoutlineprovider.h" #include "display/curve.h" #include "sp-shape.h" #include "style.h" #include "xml/repr.h" #include "sp-paint-server.h" #include "svg/svg-color.h" #include "desktop-style.h" #include "svg/css-ostringstream.h" #include "svg/svg.h" //#include #include "knot-holder-entity.h" #include "knotholder.h" namespace Inkscape { namespace LivePathEffect { namespace TpS { class KnotHolderEntityAttachBegin : public LPEKnotHolderEntity { public: KnotHolderEntityAttachBegin(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); virtual Geom::Point knot_get() const; }; class KnotHolderEntityAttachEnd : public LPEKnotHolderEntity { public: KnotHolderEntityAttachEnd(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); virtual Geom::Point knot_get() const; }; } // TpS static const Util::EnumData JoinType[] = { {LINEJOIN_STRAIGHT, N_("Beveled"), "bevel"}, {LINEJOIN_ROUND, N_("Rounded"), "round"}, {LINEJOIN_REFLECTED, N_("Reflected"), "reflected"}, {LINEJOIN_POINTY, N_("Miter"), "miter"}, {LINEJOIN_EXTRAPOLATED, N_("Extrapolated"), "extrapolated"} }; static const Util::EnumDataConverter JoinTypeConverter(JoinType, sizeof (JoinType)/sizeof(*JoinType)); LPETaperStroke::LPETaperStroke(LivePathEffectObject *lpeobject) : Effect(lpeobject), line_width(_("Stroke width"), _("The (non-tapered) width of the path"), "stroke_width", &wr, this, 3), attach_start(_("Start offset"), _("Taper distance from path start"), "attach_start", &wr, this, 0.2), attach_end(_("End offset"), _("The ending position of the taper"), "end_offset", &wr, this, 0.2), smoothing(_("Taper smoothing"), _("Amount of smoothing to apply to the tapers"), "smoothing", &wr, this, 0.5), join_type(_("Join type"), _("Join type for non-smooth nodes"), "jointype", JoinTypeConverter, &wr, this, LINEJOIN_EXTRAPOLATED), miter_limit(_("Miter limit"), _("Limit for miter joins"), "miter_limit", &wr, this, 30.) { show_orig_path = true; _provides_knotholder_entities = true; attach_start.param_set_digits(3); attach_end.param_set_digits(3); registerParameter( dynamic_cast(&line_width) ); registerParameter( dynamic_cast(&attach_start) ); registerParameter( dynamic_cast(&attach_end) ); registerParameter( dynamic_cast(&smoothing) ); registerParameter( dynamic_cast(&join_type) ); registerParameter( dynamic_cast(&miter_limit) ); } LPETaperStroke::~LPETaperStroke() { } //from LPEPowerStroke -- sets fill if stroke color because we will //be converting to a fill to make the new join. void LPETaperStroke::doOnApply(SPLPEItem const* lpeitem) { if (SP_IS_SHAPE(lpeitem)) { SPLPEItem* item = const_cast(lpeitem); double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; SPCSSAttr *css = sp_repr_css_attr_new (); if (lpeitem->style->stroke.isSet()) { if (lpeitem->style->stroke.isPaintserver()) { SPPaintServer * server = lpeitem->style->getStrokePaintServer(); if (server) { Glib::ustring str; str += "url(#"; str += server->getId(); str += ")"; sp_repr_css_set_property (css, "fill", str.c_str()); } } else if (lpeitem->style->stroke.isColor()) { gchar c[64]; sp_svg_write_color (c, sizeof(c), lpeitem->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->stroke_opacity.value))); sp_repr_css_set_property (css, "fill", c); } else { sp_repr_css_set_property (css, "fill", "none"); } } else { sp_repr_css_unset_property (css, "fill"); } sp_repr_css_set_property(css, "stroke", "none"); sp_desktop_apply_css_recursive(item, css, true); sp_repr_css_attr_unref (css); line_width.param_set_value(width); } else { g_warning("LPE Join Type can only be applied to paths (not groups)."); } } //from LPEPowerStroke -- sets stroke color from existing fill color void LPETaperStroke::doOnRemove(SPLPEItem const* lpeitem) { if (SP_IS_SHAPE(lpeitem)) { //TODO: make it getobjbyrepr instead of const_cast because this can cause //undefined behavior SPLPEItem *item = const_cast(lpeitem); SPCSSAttr *css = sp_repr_css_attr_new (); if (lpeitem->style->fill.isSet()) { if (lpeitem->style->fill.isPaintserver()) { SPPaintServer * server = lpeitem->style->getFillPaintServer(); if (server) { Glib::ustring str; str += "url(#"; str += server->getId(); str += ")"; sp_repr_css_set_property (css, "stroke", str.c_str()); } } else if (lpeitem->style->fill.isColor()) { gchar c[64]; sp_svg_write_color (c, sizeof(c), lpeitem->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->stroke_opacity.value))); sp_repr_css_set_property (css, "stroke", c); } else { sp_repr_css_set_property (css, "stroke", "none"); } } else { sp_repr_css_unset_property (css, "stroke"); } Inkscape::CSSOStringStream os; os << fabs(line_width); sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); sp_repr_css_set_property(css, "fill", "none"); sp_desktop_apply_css_recursive(item, css, true); sp_repr_css_attr_unref (css); item->updateRepr(); } } //actual effect impl here Geom::Path return_at_first_cusp (Geom::Path const & path_in, double smooth_tolerance = 0.05) { Geom::Path path_out = Geom::Path(); for (unsigned i = 0; i < path_in.size(); i++) { path_out.append(path_in[i]); if (path_in.size() == 1) break; //determine order of curve int order = Outline::bezierOrder(&path_in[i]); Geom::Point start_point; Geom::Point cross_point = path_in[i].finalPoint(); Geom::Point end_point; g_assert(path_in[i].finalPoint() == path_in[i+1].initialPoint()); //can you tell that the following expressions have been shaped by //repeated compiler errors? ;) switch (order) { case 3: start_point = (static_cast(&path_in[i]))->operator[] (2); //major league b***f***ing if (are_near(start_point, cross_point, 0.0000001)) { start_point = (static_cast(&path_in[i]))->operator[] (1); } break; case 2: //this never happens start_point = (static_cast(&path_in[i]))->operator[] (1); break; case 1: default: start_point = path_in[i].initialPoint(); } order = Outline::bezierOrder(&path_in[i+1]); switch (order) { case 3: end_point = (static_cast(&path_in[i+1]))->operator[] (1); if (are_near(end_point, cross_point, 0.0000001)) { end_point = (static_cast(&path_in[i+1]))->operator[] (2); } break; case 2: end_point = (static_cast(&path_in[i+1]))->operator[] (1); break; case 1: default: end_point = path_in[i+1].finalPoint(); } //clearly it's collinear if two occupy the same point g_assert(!are_near(start_point, cross_point, 0.0000001)); g_assert(!are_near(cross_point, end_point, 0.0000001)); g_assert(!are_near(start_point, end_point, 0.0000001)); if (!are_collinear(start_point, cross_point, end_point, smooth_tolerance)) break; } return path_out; } Geom::Piecewise > stretch_along(Geom::Piecewise > pwd2_in, Geom::Path pattern, double width); //references to pointers, because magic void subdivideCurve(const Geom::Curve * curve_in, Geom::Coord t, Geom::Curve *& val_first, Geom::Curve *& val_second); Geom::PathVector LPETaperStroke::doEffect_path(Geom::PathVector const& path_in) { Geom::Path first_cusp = return_at_first_cusp(path_in[0]); Geom::Path last_cusp = return_at_first_cusp(path_in[0].reverse()); bool zeroStart = false; bool zeroEnd = false; //there is a pretty good chance that people will try to drag the knots //on top of each other, so block it unsigned size = path_in[0].size(); if (size == first_cusp.size()) { //check to see if the knots were dragged over each other //if so, reset the end offset if ( attach_start >= (size - attach_end) ) { attach_end.param_set_value( size - attach_start ); } } //don't let it be zero if (attach_start <= 0.00000001) { attach_start.param_set_value( 0.00000001 ); zeroStart = true; } if (attach_end <= 0.00000001) { attach_end.param_set_value( 0.00000001 ); zeroEnd = true; } //don't let it be integer if (double(unsigned(attach_start)) == attach_start) { attach_start.param_set_value(attach_start - 0.00001); } if (double(unsigned(attach_end)) == attach_end) { attach_end.param_set_value(attach_end - 0.00001); } unsigned allowed_start = first_cusp.size(); unsigned allowed_end = last_cusp.size(); //don't let the knots be farther than they are allowed to be if ((unsigned)attach_start >= allowed_start) { attach_start.param_set_value((double)allowed_start - 0.00000001); } if ((unsigned)attach_end >= allowed_end) { attach_end.param_set_value((double)allowed_end - 0.00000001); } //remember, Path::operator () means get point at time t start_attach_point = first_cusp(attach_start); end_attach_point = last_cusp(attach_end); Geom::PathVector pathv_out; //the following function just splits it up into three pieces. pathv_out = doEffect_simplePath(path_in); //now for the actual tapering. We use a Pattern Along Path method to get this done. Geom::PathVector real_pathv; Geom::Path real_path; Geom::PathVector pat_vec; Geom::Piecewise > pwd2; Geom::Path throwaway_path; if (!zeroStart) { //Construct the pattern (pat_str stands for pattern string) (yes, this is easier, trust me) std::stringstream pat_str; pat_str << "M 1,0 C " << 1 - (double)smoothing << ",0 0,0.5 0,0.5 0,0.5 " << 1 - (double)smoothing << ",1 1,1"; pat_vec = sp_svg_read_pathv(pat_str.str().c_str()); pwd2.concat(stretch_along(pathv_out[0].toPwSb(), pat_vec[0], -fabs(line_width))); throwaway_path = Geom::path_from_piecewise(pwd2, 0.001)[0]; real_path.append(throwaway_path); } //append the outside outline of the path (with direction) throwaway_path = Outline::PathOutsideOutline(pathv_out[1], -fabs(line_width), static_cast(join_type.get_value()), miter_limit); real_path.append(throwaway_path, Geom::Path::STITCH_DISCONTINUOUS); if (!zeroEnd) { //append the ending taper std::stringstream pat_str_1; pat_str_1 << "M 0,0 0,1 C " << (double)smoothing << ",1 1,0.5 1,0.5 1,0.5 " << double(smoothing) << ",0 0,0"; pat_vec = sp_svg_read_pathv(pat_str_1.str().c_str()); pwd2 = Geom::Piecewise > (); pwd2.concat(stretch_along(pathv_out[2].toPwSb(), pat_vec[0], -fabs(line_width))); throwaway_path = Geom::path_from_piecewise(pwd2, 0.001)[0]; real_path.append(throwaway_path, Geom::Path::STITCH_DISCONTINUOUS); } //append the inside outline of the path (against direction) throwaway_path = Outline::PathOutsideOutline(pathv_out[1].reverse(), -fabs(line_width), static_cast(join_type.get_value()), miter_limit); real_path.append(throwaway_path, Geom::Path::STITCH_DISCONTINUOUS); real_path.appendNew(real_path.initialPoint()); real_path.close(); real_pathv.push_back(real_path); return real_pathv; } //in all cases, this should return a PathVector with three elements. Geom::PathVector LPETaperStroke::doEffect_simplePath(Geom::PathVector const & path_in) { unsigned size = path_in[0].size(); //do subdivision and get out unsigned loc = (unsigned)attach_start; Geom::Curve * curve_start = path_in[0] [loc].duplicate(); std::vector pathv_out; Geom::Path path_out = Geom::Path(); Geom::Path trimmed_start = Geom::Path(); Geom::Path trimmed_end = Geom::Path(); for (unsigned i = 0; i < loc; i++) { trimmed_start.append(path_in[0] [i]); } //this is pretty annoying //previously I wrote a function for this but it wasted a lot of time //so I optimized it back into here. unsigned order = Outline::bezierOrder(curve_start); switch (order) { case 3: { Geom::CubicBezier *cb = static_cast(curve_start); std::pair cb_pair = cb->subdivide((attach_start - loc)); trimmed_start.append(cb_pair.first); curve_start = cb_pair.second.duplicate(); //goes out of scope break; } case 2: { Geom::QuadraticBezier *qb = static_cast(curve_start); std::pair qb_pair = qb->subdivide((attach_start - loc)); trimmed_start.append(qb_pair.first); curve_start = qb_pair.second.duplicate(); break; } case 1: { Geom::BezierCurveN<1> *lb = static_cast * >(curve_start); std::pair, Geom::BezierCurveN<1> > lb_pair = lb->subdivide((attach_start - loc)); trimmed_start.append(lb_pair.first); curve_start = lb_pair.second.duplicate(); break; } } //special case: path is one segment long //special case: what if the two knots occupy the same segment? if ((size == 1) || ( size - unsigned(attach_end) - 1 == loc )) { Geom::Coord t = Geom::nearest_point(end_attach_point, *curve_start); //it is just a dumb segment //we have to do some shifting here because the value changed when we reduced the length //of the previous segment. order = Outline::bezierOrder(curve_start); switch (order) { case 3: { Geom::CubicBezier *cb = static_cast(curve_start); std::pair cb_pair = cb->subdivide(t); trimmed_end.append(cb_pair.second); curve_start = cb_pair.first.duplicate(); break; } case 2: { Geom::QuadraticBezier *qb = static_cast(curve_start); std::pair qb_pair = qb->subdivide(t); trimmed_end.append(qb_pair.second); curve_start = qb_pair.first.duplicate(); break; } case 1: { Geom::BezierCurveN<1> *lb = static_cast * >(curve_start); std::pair, Geom::BezierCurveN<1> > lb_pair = lb->subdivide(t); trimmed_end.append(lb_pair.second); curve_start = lb_pair.first.duplicate(); break; } } for (unsigned j = (size - attach_end) + 1; j < size; j++) { trimmed_end.append(path_in[0] [j]); } path_out.append(*curve_start); pathv_out.push_back(trimmed_start); pathv_out.push_back(path_out); pathv_out.push_back(trimmed_end); return pathv_out; } pathv_out.push_back(trimmed_start); //append almost all of the rest of the path, ignore the curves that the knot is past (we'll get to it in a minute) path_out.append(*curve_start); for (unsigned k = loc + 1; k < (size - unsigned(attach_end)) - 1; k++) { path_out.append(path_in[0] [k]); } //deal with the last segment in a very similar fashion to the first loc = size - attach_end; Geom::Curve * curve_end = path_in[0] [loc].duplicate(); Geom::Coord t = Geom::nearest_point(end_attach_point, *curve_end); order = Outline::bezierOrder(curve_end); switch (order) { case 3: { Geom::CubicBezier *cb = static_cast(curve_end); std::pair cb_pair = cb->subdivide(t); trimmed_end.append(cb_pair.second); curve_end = cb_pair.first.duplicate(); break; } case 2: { Geom::QuadraticBezier *qb = static_cast(curve_end); std::pair qb_pair = qb->subdivide(t); trimmed_end.append(qb_pair.second); curve_end = qb_pair.first.duplicate(); break; } case 1: { Geom::BezierCurveN<1> *lb = static_cast * >(curve_end); std::pair, Geom::BezierCurveN<1> > lb_pair = lb->subdivide(t); trimmed_end.append(lb_pair.second); curve_end = lb_pair.first.duplicate(); break; } } for (unsigned j = (size - attach_end) + 1; j < size; j++) { trimmed_end.append(path_in[0] [j]); } path_out.append(*curve_end); pathv_out.push_back(path_out); pathv_out.push_back(trimmed_end); if (curve_end) delete curve_end; if (curve_start) delete curve_start; return pathv_out; } //most of the below code is verbatim from Pattern Along Path. However, it needed a little //tweaking to get it to work right in this case. Geom::Piecewise > stretch_along(Geom::Piecewise > pwd2_in, Geom::Path pattern, double prop_scale) { using namespace Geom; // Don't allow empty path parameter: if ( pattern.empty() ) { return pwd2_in; } /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ Piecewise > output; std::vector > > pre_output; D2 > patternd2 = make_cuts_independent(pattern.toPwSb()); Piecewise x0 = Piecewise(patternd2[0]); Piecewise y0 = Piecewise(patternd2[1]); OptInterval pattBndsX = bounds_exact(x0); OptInterval pattBndsY = bounds_exact(y0); if (pattBndsX && pattBndsY) { x0 -= pattBndsX->min(); y0 -= pattBndsY->middle(); double xspace = 0; double noffset = 0; double toffset = 0; /*if (prop_units.get_value() && pattBndsY){ xspace *= pattBndsX->extent(); noffset *= pattBndsY->extent(); toffset *= pattBndsX->extent(); }*/ //Prevent more than 90% overlap... if (xspace < -pattBndsX->extent()*.9) { xspace = -pattBndsX->extent()*.9; } y0+=noffset; std::vector > > paths_in; paths_in = split_at_discontinuities(pwd2_in); for (unsigned idx = 0; idx < paths_in.size(); idx++) { Geom::Piecewise > path_i = paths_in[idx]; Piecewise x = x0; Piecewise y = y0; Piecewise > uskeleton = arc_length_parametrization(path_i,2,.1); uskeleton = remove_short_cuts(uskeleton,.01); Piecewise > n = rot90(derivative(uskeleton)); n = force_continuity(remove_short_cuts(n,.1)); int nbCopies = 0; double scaling = 1; nbCopies = 1; scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); double pattWidth = pattBndsX->extent() * scaling; if (scaling != 1.0) { x*=scaling; } if ( false ) { y*=(scaling*prop_scale); } else { if (prop_scale != 1.0) y *= prop_scale; } x += toffset; double offs = 0; for (int i=0; i > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); std::vector > > splited_output_piece = split_at_discontinuities(output_piece); pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() ); } else { output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs)); } offs+=pattWidth; } } /*if (false){ pre_output = fuse_nearby_ends(pre_output, fuse_tolerance); for (unsigned i=0; i(curve_in); std::pair cb_pair = cb->subdivide(t); //trimmed_start.append(cb_pair.first); val_first = cb_pair.first.duplicate(); val_second = cb_pair.second.duplicate(); break; } case 2: { Geom::QuadraticBezier *qb = static_cast(curve_in); std::pair qb_pair = qb->subdivide(t); //trimmed_start.append(qb_pair.first); val_first = qb_pair.first.duplicate(); val_second = qb_pair.second.duplicate(); break; } case 1: { Geom::BezierCurveN<1> *lb = static_cast * >(curve_in); std::pair, Geom::BezierCurveN<1> > lb_pair = lb->subdivide(t); //trimmed_start.append(lb_pair.first); val_first = lb_pair.first.duplicate(); val_second = lb_pair.second.duplicate(); break; } } } void LPETaperStroke::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { { KnotHolderEntity *e = new TpS::KnotHolderEntityAttachBegin(this); e->create( desktop, item, knotholder, Inkscape::CTRL_TYPE_UNKNOWN, _("Start point of the taper"), SP_KNOT_SHAPE_CIRCLE ); knotholder->add(e); } { KnotHolderEntity *e = new TpS::KnotHolderEntityAttachEnd(this); e->create( desktop, item, knotholder, Inkscape::CTRL_TYPE_UNKNOWN, _("End point of the taper"), SP_KNOT_SHAPE_CIRCLE ); knotholder->add(e); } } namespace TpS { void KnotHolderEntityAttachBegin::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) { using namespace Geom; LPETaperStroke* lpe = dynamic_cast(_effect); Geom::Point const s = snap_knot_position(p, state); SPCurve *curve = SP_PATH(item)->get_curve_for_edit(); Geom::PathVector pathv = curve->get_pathvector(); Piecewise > pwd2; Geom::Path p_in = return_at_first_cusp(pathv[0]); pwd2.concat(p_in.toPwSb()); std::vector > > pwd_vec = split_at_discontinuities(pwd2); double t0 = nearest_point(s, pwd_vec[0]); lpe->attach_start.param_set_value(t0); // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); } void KnotHolderEntityAttachEnd::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) { using namespace Geom; LPETaperStroke* lpe = dynamic_cast(_effect); Geom::Point const s = snap_knot_position(p, state); SPCurve *curve = SP_PATH(item)->get_curve_for_edit(); Geom::PathVector pathv = curve->get_pathvector(); Piecewise > pwd2; Geom::Path p_in = return_at_first_cusp(pathv[0].reverse()); pwd2.concat(p_in.toPwSb()); std::vector > > pwd_vec = split_at_discontinuities(pwd2); double t0 = nearest_point(s, pwd_vec[0]); lpe->attach_end.param_set_value(t0); // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); } Geom::Point KnotHolderEntityAttachBegin::knot_get() const { LPETaperStroke const * lpe = dynamic_cast (_effect); return lpe->start_attach_point; } Geom::Point KnotHolderEntityAttachEnd::knot_get() const { LPETaperStroke const * lpe = dynamic_cast (_effect); return lpe->end_attach_point; } } /* ######################## */ } //namespace LivePathEffect } /* namespace Inkscape */ /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :