// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) Johan Engelen 2007 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include <2geom/bezier-to-sbasis.h> #include "live_effects/lpe-patternalongpath.h" #include "live_effects/lpeobject.h" #include "display/curve.h" #include "object/sp-shape.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 /* Theory in e-mail from J.F. Barraud Let B be the skeleton path, and P the pattern (the path to be deformed). P is a map t --> P(t) = ( x(t), y(t) ). B is a map t --> B(t) = ( a(t), b(t) ). The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to U(s) is s itself. We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s). The basic deformation associated to B is then given by: (x,y) --> U(x)+y*N(x) (i.e. we go for distance x along the path, and then for distance y along the normal) Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed first) but I think we can first forget about them. */ namespace Inkscape { namespace LivePathEffect { namespace WPAP { class KnotHolderEntityWidthPatternAlongPath : public LPEKnotHolderEntity { public: KnotHolderEntityWidthPatternAlongPath(LPEPatternAlongPath * effect) : LPEKnotHolderEntity(effect) {} ~KnotHolderEntityWidthPatternAlongPath() override { LPEPatternAlongPath *lpe = dynamic_cast (_effect); lpe->_knot_entity = nullptr; } void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; Geom::Point knot_get() const override; }; } // WPAP static const Util::EnumData PAPCopyTypeData[PAPCT_END] = { // clang-format off {PAPCT_SINGLE, N_("Single"), "single"}, {PAPCT_SINGLE_STRETCHED, N_("Single, stretched"), "single_stretched"}, {PAPCT_REPEATED, N_("Repeated"), "repeated"}, {PAPCT_REPEATED_STRETCHED, N_("Repeated, stretched"), "repeated_stretched"} // clang-format on }; static const Util::EnumDataConverter PAPCopyTypeConverter(PAPCopyTypeData, PAPCT_END); LPEPatternAlongPath::LPEPatternAlongPath(LivePathEffectObject *lpeobject) : Effect(lpeobject), pattern(_("Pattern source:"), _("Path to put along the skeleton path"), "pattern", &wr, this, "M0,0 L1,0"), original_height(0.0), prop_scale(_("_Width:"), _("Width of the pattern"), "prop_scale", &wr, this, 1.0), copytype(_("Pattern copies:"), _("How many pattern copies to place along the skeleton path"), "copytype", PAPCopyTypeConverter, &wr, this, PAPCT_SINGLE_STRETCHED), scale_y_rel(_("Wid_th in units of length"), _("Scale the width of the pattern in units of its length"), "scale_y_rel", &wr, this, false), spacing(_("Spa_cing:"), // xgettext:no-c-format _("Space between copies of the pattern. Negative values allowed, but are limited to -90% of pattern width."), "spacing", &wr, this, 0), normal_offset(_("No_rmal offset:"), "", "normal_offset", &wr, this, 0), tang_offset(_("Tan_gential offset:"), "", "tang_offset", &wr, this, 0), prop_units(_("Offsets in _unit of pattern size"), _("Spacing, tangential and normal offset are expressed as a ratio of width/height"), "prop_units", &wr, this, false), vertical_pattern(_("Pattern is _vertical"), _("Rotate pattern 90 deg before applying"), "vertical_pattern", &wr, this, false), hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false), fuse_tolerance(_("_Fuse nearby ends:"), _("Fuse ends closer than this number. 0 means don't fuse."), "fuse_tolerance", &wr, this, 0) { registerParameter(&pattern); registerParameter(©type); registerParameter(&prop_scale); registerParameter(&scale_y_rel); registerParameter(&spacing); registerParameter(&normal_offset); registerParameter(&tang_offset); registerParameter(&prop_units); registerParameter(&vertical_pattern); registerParameter(&hide_knot); registerParameter(&fuse_tolerance); prop_scale.param_set_digits(3); prop_scale.param_set_increments(0.01, 0.10); _knot_entity = nullptr; _provides_knotholder_entities = true; } LPEPatternAlongPath::~LPEPatternAlongPath() = default; bool LPEPatternAlongPath::doOnOpen(SPLPEItem const *lpeitem) { if (!is_load || is_applied) { return false; } pattern.reload(); return false; } void LPEPatternAlongPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/) { if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { pattern.param_transform_multiply(postmul, false); } } void LPEPatternAlongPath::doBeforeEffect (SPLPEItem const* lpeitem) { // get the pattern bounding box Geom::OptRect bbox = pattern.get_pathvector().boundsFast(); if (bbox) { original_height = (*bbox)[Geom::Y].max() - (*bbox)[Geom::Y].min(); } if (is_load) { pattern.reload(); } if (_knot_entity) { if (hide_knot) { helper_path.clear(); _knot_entity->knot->hide(); } else { _knot_entity->knot->show(); } _knot_entity->update_knot(); } } Geom::Piecewise > LPEPatternAlongPath::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) { using namespace Geom; // Don't allow empty path parameter: if ( pattern.get_pathvector().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; PAPCopyType type = copytype.get_value(); Geom::Affine affine = pattern.get_relative_affine(); D2 > patternd2 = make_cuts_independent(pattern.get_pwd2() * affine); Piecewise x0 = vertical_pattern.get_value() ? Piecewise(patternd2[1]) : Piecewise(patternd2[0]); Piecewise y0 = vertical_pattern.get_value() ? Piecewise(patternd2[0]) : Piecewise(patternd2[1]); OptInterval pattBndsX = bounds_exact(x0); OptInterval pattBndsY = bounds_exact(y0); if (pattBndsX && pattBndsY) { x0 -= pattBndsX->min(); y0 -= pattBndsY->middle(); double xspace = spacing; double noffset = normal_offset; double toffset = tang_offset; if (prop_units.get_value()){ xspace *= pattBndsX->extent(); noffset *= pattBndsY->extent(); toffset *= pattBndsX->extent(); } //Prevent more than 90% overlap... if (xspace < -pattBndsX->extent() * 0.9) { xspace = -pattBndsX->extent() * 0.9; } //TODO: dynamical update of parameter ranges? //if (prop_units.get_value()){ // spacing.param_set_range(-.9, Geom::infinity()); // }else{ // spacing.param_set_range(-pattBndsX.extent()*.9, Geom::infinity()); // } y0 += noffset; std::vector > > paths_in; paths_in = split_at_discontinuities(pwd2_in); for (auto path_i : paths_in){ Piecewise x = x0; Piecewise y = y0; Piecewise > uskeleton = arc_length_parametrization(path_i,2, 0.1); uskeleton = remove_short_cuts(uskeleton, 0.01); Piecewise > n = rot90(derivative(uskeleton)); if (Geom::are_near(pwd2_in[0].at0(),pwd2_in[pwd2_in.size()-1].at1(), 0.01)) { n = force_continuity(remove_short_cuts(n, 0.1), 0.01); } else { n = force_continuity(remove_short_cuts(n, 0.1)); } int nbCopies = 0; double scaling = 1; switch(type) { case PAPCT_REPEATED: nbCopies = static_cast(floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace))); pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); break; case PAPCT_SINGLE: nbCopies = (toffset + pattBndsX->extent() < uskeleton.domain().extent()) ? 1 : 0; break; case PAPCT_SINGLE_STRETCHED: nbCopies = 1; scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); break; case PAPCT_REPEATED_STRETCHED: // if uskeleton is closed: if (are_near(path_i.segs.front().at0(), path_i.segs.back().at1())){ nbCopies = std::max(1, static_cast(std::floor((uskeleton.domain().extent() - toffset)/(pattBndsX->extent()+xspace)))); pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent()); // if not closed: no space at the end }else{ nbCopies = std::max(1, static_cast(std::floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace)))); pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent() - xspace); } break; default: return pwd2_in; }; //Ceil to 6 decimals scaling = ceil(scaling * 1000000) / 1000000; double pattWidth = pattBndsX->extent() * scaling; x *= scaling; if ( scale_y_rel.get_value() ) { y *= prop_scale * scaling; } else { y *= prop_scale; } x += toffset; double offs = 0; for (int i=0; i 0){ Geom::Piecewise > 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 (fuse_tolerance > 0){ pre_output = fuse_nearby_ends(pre_output, fuse_tolerance); for (const auto & i : pre_output){ output.concat(i); } } return output; } else { return pwd2_in; } } void LPEPatternAlongPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) { hp_vec.push_back(helper_path); } void LPEPatternAlongPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { _knot_entity = new WPAP::KnotHolderEntityWidthPatternAlongPath(this); _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PatternAlongPath", _("Change the width")); knotholder->add(_knot_entity); if (hide_knot) { _knot_entity->knot->hide(); _knot_entity->update_knot(); } } namespace WPAP { void KnotHolderEntityWidthPatternAlongPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) { LPEPatternAlongPath *lpe = dynamic_cast (_effect); Geom::Point const s = snap_knot_position(p, state); SPShape const *sp_shape = dynamic_cast(SP_LPE_ITEM(item)); if (sp_shape && lpe->original_height) { auto curve_before = SPCurve::copy(sp_shape->curveForEdit()); if (curve_before) { Geom::Path const *path_in = curve_before->first_path(); Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); Geom::Ray ray(ptA, B); if (cubic) { ray.setPoints(ptA, (*cubic)[1]); } ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse(); Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos); if(nearest_to_ray == 0){ lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0)); } else { lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0)); } } if (!lpe->original_height) { lpe->prop_scale.param_set_value(0); } Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setDouble("/live_effects/skeletal/width", lpe->prop_scale); } sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); } Geom::Point KnotHolderEntityWidthPatternAlongPath::knot_get() const { LPEPatternAlongPath *lpe = dynamic_cast (_effect); SPShape const *sp_shape = dynamic_cast(SP_LPE_ITEM(item)); if (sp_shape) { auto curve_before = SPCurve::copy(sp_shape->curveForEdit()); if (curve_before) { Geom::Path const *path_in = curve_before->first_path(); Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); Geom::Ray ray(ptA, B); if (cubic) { ray.setPoints(ptA, (*cubic)[1]); } ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA; lpe->helper_path.clear(); if (!lpe->hide_knot) { Geom::Path hp(result_point); hp.appendNew(ptA); lpe->helper_path.push_back(hp); hp.clear(); } return result_point; } } return Geom::Point(); } } // namespace WPAP } // 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 :