diff options
Diffstat (limited to 'src/live_effects/lpe-taperstroke.cpp')
-rw-r--r-- | src/live_effects/lpe-taperstroke.cpp | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/src/live_effects/lpe-taperstroke.cpp b/src/live_effects/lpe-taperstroke.cpp new file mode 100644 index 0000000..b7d970c --- /dev/null +++ b/src/live_effects/lpe-taperstroke.cpp @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Taper Stroke path effect, provided as an alternative to Power Strokes + * for otherwise constant-width paths. + * + * Authors: + * Liam P White + * + * Copyright (C) 2014-2020 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-taperstroke.h" +#include "live_effects/fill-conversion.h" + +#include <2geom/circle.h> +#include <2geom/sbasis-to-bezier.h> + +#include "style.h" + +#include "display/curve.h" +#include "helper/geom.h" +#include "helper/geom-nodetype.h" +#include "helper/geom-pathstroke.h" +#include "object/sp-shape.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +template<typename T> +inline bool withinRange(T value, T low, T high) { + return (value > low && value < high); +} + +namespace Inkscape { +namespace LivePathEffect { + +namespace TpS { + class KnotHolderEntityAttachBegin : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachBegin(LPETaperStroke * effect, size_t index) + : LPEKnotHolderEntity(effect) + , _effect(effect) + , _index(index) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + Geom::Point knot_get() const override; + bool valid_index(unsigned int index) const { + return (_effect->attach_start._vector.size() > index); + }; + private: + size_t _index; + LPETaperStroke * _effect; + }; + + class KnotHolderEntityAttachEnd : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachEnd(LPETaperStroke * effect, size_t index) + : LPEKnotHolderEntity(effect) + , _effect(effect) + , _index(index) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + Geom::Point knot_get() const override; + bool valid_index(unsigned int index) const { + return (_effect->attach_end._vector.size() > index); + }; + private: + size_t _index; + LPETaperStroke * _effect; + }; +} // TpS + +static const Util::EnumData<unsigned> JoinType[] = { + // clang-format off + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated"), "extrapolated"}, + // clang-format on +}; + +enum TaperShape { + TAPER_CENTER, + TAPER_RIGHT, + TAPER_LEFT, + LAST_SHAPE +}; + +static const Util::EnumData<unsigned> TaperShapeType[] = { + {TAPER_CENTER, N_("Center"), "center"}, + {TAPER_LEFT, N_("Left"), "left"}, + {TAPER_RIGHT, N_("Right"), "right"}, +}; + +static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinType, sizeof (JoinType)/sizeof(*JoinType)); +static const Util::EnumDataConverter<unsigned> TaperShapeTypeConverter(TaperShapeType, sizeof (TaperShapeType)/sizeof(*TaperShapeType)); + +LPETaperStroke::LPETaperStroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + subpath(_("Select subpath:"), _("Select the subpath you want to modify"), "subpath", &wr, this, 1.), + line_width(_("Stroke width:"), _("The (non-tapered) width of the path"), "stroke_width", &wr, this, 1.), + 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), + start_smoothing(_("Start smoothing:"), _("Amount of smoothing to apply to the start taper"), "start_smoothing", &wr, this, 0.5), + end_smoothing(_("End smoothing:"), _("Amount of smoothing to apply to the end taper"), "end_smoothing", &wr, this, 0.5), + join_type(_("Join type:"), _("Join type for non-smooth nodes"), "jointype", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE), + start_shape(_("Start direction:"), _("Direction of the taper at the path start"), "start_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER), + end_shape(_("End direction:"), _("Direction of the taper at the path end"), "end_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER), + miter_limit(_("Miter limit:"), _("Limit for miter joins"), "miter_limit", &wr, this, 100.) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + attach_start.param_set_digits(3); + attach_end.param_set_digits(3); + subpath.param_set_range(1, 1); + subpath.param_set_increments(1, 1); + subpath.param_set_digits(0); + + registerParameter(&line_width); + registerParameter(&subpath); + registerParameter(&attach_start); + registerParameter(&attach_end); + registerParameter(&start_smoothing); + registerParameter(&end_smoothing); + registerParameter(&join_type); + registerParameter(&start_shape); + registerParameter(&end_shape); + registerParameter(&miter_limit); +} + +LPETaperStroke::~LPETaperStroke() = default; + +// from LPEPowerStroke -- sets fill if stroke color because we will +// be converting to a fill to make the new join. + +void LPETaperStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke && !sp_lpe_item->unoptimized()) { + line_width.param_transform_multiply(postmul, false); + } +} + +void LPETaperStroke::doOnApply(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto item = cast<SPShape>(lpeitem_mutable); + + if (!item) { + printf("WARNING: It only makes sense to apply Taper stroke to paths (not groups).\n"); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; + + lpe_shape_convert_stroke_and_fill(item); + + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)"stroke_width"; + + bool valid = prefs->getEntry(pref_path).isValid(); + + if (!valid) { + line_width.param_set_value(width); + } + + line_width.write_to_SVG(); +} + +void LPETaperStroke::doOnRemove(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto shape = cast<SPShape>(lpeitem_mutable); + if (shape) { + lpe_shape_revert_stroke_and_fill(shape, line_width); + } +} + +using Geom::Piecewise; +using Geom::D2; +using Geom::SBasis; +// leave Geom::Path + +static Geom::Path return_at_first_cusp(Geom::Path const & path_in, double /*smooth_tolerance*/ = 0.05) +{ + Geom::Path temp; + + for (unsigned i = 0; i < path_in.size(); i++) { + temp.append(path_in[i]); + if (path_in.size() > i+1) { + if (Geom::get_nodetype(path_in[i], path_in[i + 1]) != Geom::NODE_SMOOTH ) { + break; + } + } + } + + return temp; +} + +Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > pwd2_in, Geom::Path pattern, double width); + +// actual effect + +Geom::PathVector LPETaperStroke::doEffect_path(Geom::PathVector const& path_in) +{ + return pathv_out; +} + +/** + * @return Always returns a PathVector with three elements. + * + * The positions of the effect knots are accessed to determine + * where exactly the input path should be split. + */ +Geom::PathVector LPETaperStroke::doEffect_simplePath(Geom::Path const & path, size_t index, double start, double end) +{ + Geom::Coord endTime = path.size() - end; + + Geom::Path p1 = path.portion(0., start); + Geom::Path p2 = path.portion(start, endTime); + Geom::Path p3 = path.portion(endTime, path.size()); + + Geom::PathVector out; + out.push_back(p1); + out.push_back(p2); + out.push_back(p3); + + return out; +} + + +/** + * Most of the below function is verbatim from Pattern Along Path. However, it needed a little + * tweaking to get it to work right in this case. Also, large portions of the effect have been + * stripped out as I deemed them unnecessary for the relative simplicity of this effect. + */ +Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > 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<D2<SBasis> > output; + std::vector<Piecewise<D2<SBasis> > > pre_output; + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pattern.toPwSb()); + Piecewise<SBasis> x0 = Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y0 = Piecewise<SBasis>(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + if (pattBndsX && pattBndsY) { + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double noffset = 0; + double toffset = 0; + // Prevent more than 90% overlap... + + y0+=noffset; + + std::vector<Piecewise<D2<SBasis> > > paths_in; + paths_in = split_at_discontinuities(pwd2_in); + + for (auto path_i : paths_in) { + Piecewise<SBasis> x = x0; + Piecewise<SBasis> y = y0; + Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.1)); + + int nbCopies = 0; + double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + nbCopies = 1; + + 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<nbCopies; i++) { + if (false) { + Piecewise<D2<SBasis> > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); + std::vector<Piecewise<D2<SBasis> > > 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; + } + } + return output; + } else { + return pwd2_in; + } +} + +void +LPETaperStroke::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(pathvector_before_effect); + size_t sicepv = pathv.size(); + bool write = false; + if (previous_size != sicepv) { + subpath.param_set_range(1, sicepv); + subpath.param_readSVGValue("1"); + if (!is_load) { + attach_start._vector.clear(); + attach_end._vector.clear(); + start_smoothing._vector.clear(); + end_smoothing._vector.clear(); + start_shape._vector.clear(); + end_shape._vector.clear(); + } + previous_size = sicepv; + } + if (!attach_start._vector.size()) { + for (auto path : pathvector_before_effect) { + attach_start._vector.push_back(0); + attach_end._vector.push_back(0); + start_smoothing._vector.push_back(0); + end_smoothing._vector.push_back(0); + start_shape._vector.emplace_back("center"); + end_shape._vector.emplace_back("center"); + } + attach_start.param_set_default(); + attach_end.param_set_default(); + start_smoothing.param_set_default(); + end_smoothing.param_set_default(); + start_shape.param_set_default(); + end_shape.param_set_default(); + write = true; + } + if (prev_subpath != subpath) { + attach_start.param_setActive(subpath - 1); + attach_end.param_setActive(subpath - 1); + start_smoothing.param_setActive(subpath - 1); + end_smoothing.param_setActive(subpath - 1); + start_shape.param_setActive(subpath - 1); + end_shape.param_setActive(subpath - 1); + prev_subpath = subpath; + refresh_widgets = true; + write = true; + } + std::vector<double> attach_startv; + for (auto & doub : attach_start.data()) { + attach_startv.push_back(doub); + } + std::vector<double> attach_endv; + for (auto & doub : attach_end.data()) { + attach_endv.push_back(doub); + } + std::vector<double> start_smoothingv; + for (auto & doub : start_smoothing.data()) { + start_smoothingv.push_back(doub); + } + std::vector<double> end_smoothingv; + for (auto & doub : end_smoothing.data()) { + end_smoothingv.push_back(doub); + } + if (write) { + start_smoothing.param_set_and_write_new_value(start_smoothingv); + end_smoothing.param_set_and_write_new_value(end_smoothingv); + attach_start.param_set_and_write_new_value(attach_startv); + attach_end.param_set_and_write_new_value(attach_endv); + } + pathv_out.clear(); + if (pathvector_before_effect.empty()) { + return; + } + + size_t index = 0; + start_attach_point.clear(); + end_attach_point.clear(); + for (auto path : pathv) { + Geom::Path first_cusp = return_at_first_cusp(path); + Geom::Path last_cusp = return_at_first_cusp(path.reversed()); + + bool zeroStart = false; // [distance from start taper knot -> start of path] == 0 + bool zeroEnd = false; // [distance from end taper knot -> end of path] == 0 + bool metInMiddle = false; // knots are touching + + // 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.size(); + if (size == first_cusp.size()) { + // check to see if the knots were dragged over each other + // if so, reset the end offset, but still allow the start offset. + if ( attach_startv[index] >= (size - attach_endv[index]) ) { + attach_endv[index] = ( size - attach_startv[index] ); + metInMiddle = true; + } + } + + if (attach_startv[index] == size - attach_endv[index]) { + metInMiddle = true; + } + if (attach_endv[index] == size - attach_startv[index]) { + metInMiddle = true; + } + + // don't let it be integer (TODO this is stupid!) + { + if (double(unsigned(attach_startv[index])) == attach_startv[index]) { + attach_startv[index] = (attach_startv[index] - 0.00001); + } + if (double(unsigned(attach_endv[index])) == attach_endv[index]) { + attach_endv[index] = (attach_endv[index] - 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_startv[index] >= allowed_start) { + attach_startv[index] = ((double)allowed_start - 0.00001); + } + if ((unsigned)attach_endv[index] >= allowed_end) { + attach_endv[index] = ((double)allowed_end - 0.00001); + } + } + + // don't let it be zero (this is stupid too!) + if (attach_startv[index] < 0.0000001 || withinRange(double(attach_startv[index]), 0.00000001, 0.000001)) { + attach_startv[index] = ( 0.0000001 ); + zeroStart = true; + } + if (attach_endv[index] < 0.0000001 || withinRange(double(attach_endv[index]), 0.00000001, 0.000001)) { + attach_endv[index] = ( 0.0000001 ); + zeroEnd = true; + } + + // Path::operator () means get point at time t + start_attach_point.push_back(first_cusp(attach_startv[index])); + end_attach_point.push_back(last_cusp(attach_endv[index])); + Geom::PathVector pathv_tmp; + + // the following function just splits it up into three pieces. + pathv_tmp = doEffect_simplePath(path, index, attach_startv[index], attach_endv[index]); + + // now for the actual tapering. the stretch_along method (stolen from PaP) is used to accomplish this + + Geom::Path real_path; + Geom::PathVector pat_vec; + Piecewise<D2<SBasis> > pwd2; + Geom::Path throwaway_path; + + if (!zeroStart) { + // Construct the pattern + std::stringstream pat_str; + pat_str.imbue(std::locale::classic()); + switch (TaperShapeTypeConverter.get_id_from_key(start_shape._vector[index])) { + case TAPER_RIGHT: + pat_str << "M 1,0 Q " << 1 - (double)start_smoothingv[index] << ",0 0,1 L 1,1"; + break; + case TAPER_LEFT: + pat_str << "M 1,0 L 0,0 Q " << 1 - (double)start_smoothingv[index] << ",1 1,1"; + break; + default: + pat_str << "M 1,0 C " << 1 - (double)start_smoothingv[index] << ",0 0,0.5 0,0.5 0,0.5 " << 1 - (double)start_smoothingv[index] << ",1 1,1"; + break; + } + + pat_vec = sp_svg_read_pathv(pat_str.str().c_str()); + pwd2.concat(stretch_along(pathv_tmp[0].toPwSb(), pat_vec[0], fabs(line_width))); + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + + real_path.append(throwaway_path); + } + + // if this condition happens to evaluate false, i.e. there was no space for a path to be drawn, it is simply skipped. + // although this seems obvious, it can probably lead to bugs. + if (!metInMiddle) { + // append the outside outline of the path (goes with the direction of the path) + throwaway_path = half_outline(pathv_tmp[1], fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value())); + if (!zeroStart && real_path.size() >= 1 && throwaway_path.size() >= 1) { + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint())) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + } + real_path.append(throwaway_path); + } + + if (!zeroEnd) { + // append the ending taper + std::stringstream pat_str_1; + pat_str_1.imbue(std::locale::classic()); + + switch (TaperShapeTypeConverter.get_id_from_key(end_shape._vector[index])) { + case TAPER_RIGHT: + pat_str_1 << "M 0,1 L 1,1 Q " << (double)end_smoothingv[index] << ",0 0,0"; + break; + case TAPER_LEFT: + pat_str_1 << "M 0,1 Q " << (double)end_smoothingv[index] << ",1 1,0 L 0,0"; + break; + default: + pat_str_1 << "M 0,1 C " << (double)end_smoothingv[index] << ",1 1,0.5 1,0.5 1,0.5 " << (double)end_smoothingv[index] << ",0 0,0"; + break; + } + + pat_vec = sp_svg_read_pathv(pat_str_1.str().c_str()); + + pwd2 = Piecewise<D2<SBasis> >(); + pwd2.concat(stretch_along(pathv_tmp[2].toPwSb(), pat_vec[0], fabs(line_width))); + + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!metInMiddle) { + // append the inside outline of the path (against direction) + throwaway_path = half_outline(pathv_tmp[1].reversed(), fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value())); + + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!Geom::are_near(real_path.finalPoint(), real_path.initialPoint())) { + real_path.appendNew<Geom::LineSegment>(real_path.initialPoint()); + } else { + real_path.setFinal(real_path.initialPoint()); + } + real_path.close(); + + pathv_out.push_back(real_path); + index++; + } + /* start_smoothing.param_set_and_write_new_value(start_smoothingv); + end_smoothing.param_set_and_write_new_value(end_smoothingv); + attach_start.param_set_and_write_new_value(attach_startv); + attach_end.param_set_and_write_new_value(attach_endv); */ + start_smoothingv.clear(); + end_smoothingv.clear(); + attach_startv.clear(); + attach_endv.clear(); +} + +void LPETaperStroke::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + for (size_t i = 0 ; i < attach_start._vector.size(); i++) { + KnotHolderEntity *e = new TpS::KnotHolderEntityAttachBegin(this, i); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeBegin", + _("<b>Start point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction")); + knotholder->add(e); + + KnotHolderEntity *f = new TpS::KnotHolderEntityAttachEnd(this, i); + f->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeEnd", + _("<b>End point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction")); + knotholder->add(f); + } +} + +namespace TpS { + +void KnotHolderEntityAttachBegin::knot_set(Geom::Point const &p, Geom::Point const&/*origin*/, guint state) +{ + using namespace Geom; + + if (!valid_index(_index) || _effect->start_attach_point.size() <= _index) { + return; + } + + Geom::Point const s = snap_knot_position(p, state); + + if (!is<SPShape>(_effect->sp_lpe_item)) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + if (!cast_unsafe<SPShape>(_effect->sp_lpe_item)->curve()) { + // oops + return; + } + // in case you are wondering, the above are simply sanity checks. we never want to actually + // use that object. + + Geom::PathVector pathv = _effect->pathvector_before_effect; + Piecewise<D2<SBasis> > pwd2; + Geom::Path p_in = return_at_first_cusp(pathv[_index]); + pwd2.concat(p_in.toPwSb()); + + double t0 = nearest_time(s, pwd2); + _effect->attach_start._vector[_index] = t0; + _effect->attach_start.write_to_SVG(); +} + +void KnotHolderEntityAttachEnd::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + using namespace Geom; + + if (!valid_index(_index) || _effect->end_attach_point.size() <= _index) { + return; + } + + Geom::Point const s = snap_knot_position(p, state); + + if (!is<SPShape>(_effect->sp_lpe_item)) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + if (!cast_unsafe<SPShape>(_effect->sp_lpe_item)->curve()) { + // oops + return; + } + Geom::PathVector pathv = _effect->pathvector_before_effect; + Geom::Path p_in = return_at_first_cusp(pathv[_index].reversed()); + Piecewise<D2<SBasis>> pwd2 = p_in.toPwSb(); + + double t0 = nearest_time(s, pwd2); + _effect->attach_end._vector[_index] = t0; + _effect->attach_end.write_to_SVG(); +} + +void KnotHolderEntityAttachBegin::knot_click(guint state) +{ + using namespace Geom; + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + if (!valid_index(_index) || _effect->start_attach_point.size() <= _index) { + return; + } + + _effect->start_shape._vector[_index] = TaperShapeTypeConverter.get_key((TaperShapeTypeConverter.get_id_from_key(_effect->start_shape._vector[_index]) + 1) % LAST_SHAPE); + _effect->start_shape.write_to_SVG(); +} + +void KnotHolderEntityAttachEnd::knot_click(guint state) +{ + using namespace Geom; + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + if (!valid_index(_index) || _effect->end_attach_point.size() <= _index) { + return; + } + + _effect->end_shape._vector[_index] = TaperShapeTypeConverter.get_key((TaperShapeTypeConverter.get_id_from_key(_effect->end_shape._vector[_index]) + 1) % LAST_SHAPE); + _effect->end_shape.write_to_SVG(); +} + +Geom::Point KnotHolderEntityAttachBegin::knot_get() const +{ + if (!valid_index(_index)) { + return Geom::Point(); + } + if (_effect && _effect->start_attach_point.size() > _index) { + return _effect->start_attach_point[_index]; + } + return Geom::Point(); +} + +Geom::Point KnotHolderEntityAttachEnd::knot_get() const +{ + if (!valid_index(_index)) { + return Geom::Point(); + } + if (_effect && _effect->end_attach_point.size() > _index) { + return _effect->end_attach_point[_index]; + } + return Geom::Point(); +} + +} // namespace TpS +} // 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 : |