// SPDX-License-Identifier: GPL-2.0-or-later /** \file * LPE implementation */ /* * Authors: * Maximilian Albert * Jabiertxo Arraiza * * Copyright (C) Johan Engelen 2007 * Copyright (C) Maximilian Albert 2008 * Copyright (C) Jabierto Arraiza 2015 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "lpe-offset.h" #include <2geom/path-intersection.h> #include <2geom/piecewise.h> #include <2geom/svg-path-parser.h> #include "inkscape.h" #include "style.h" #include "display/curve.h" #include "helper/geom-pathstroke.h" #include "helper/geom.h" #include "live_effects/parameter/enum.h" #include "object/sp-shape.h" #include "path/path-boolop.h" #include "path/path-util.h" #include "svg/svg.h" #include "ui/knot/knot-holder.h" #include "ui/knot/knot-holder-entity.h" #include "util/units.h" // TODO due to internal breakage in glibmm headers, this must be last: #include namespace Inkscape { namespace LivePathEffect { namespace OfS { class KnotHolderEntityOffsetPoint : public LPEKnotHolderEntity { public: KnotHolderEntityOffsetPoint(LPEOffset * effect) : LPEKnotHolderEntity(effect) {} ~KnotHolderEntityOffsetPoint() override { LPEOffset *lpe = dynamic_cast(_effect); if (lpe) { lpe->_knot_entity = nullptr; } } void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; Geom::Point knot_get() const override; private: }; } // OfS static const Util::EnumData JoinTypeData[] = { // clang-format off {JOIN_BEVEL, N_("Beveled"), "bevel"}, {JOIN_ROUND, N_("Rounded"), "round"}, {JOIN_MITER, N_("Miter"), "miter"}, {JOIN_MITER_CLIP, N_("Miter Clip"), "miter-clip"}, {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"}, {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"}, {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"}, {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"}, // clang-format on }; static const Util::EnumDataConverter JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData)); LPEOffset::LPEOffset(LivePathEffectObject *lpeobject) : Effect(lpeobject), unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), offset(_("Offset:"), _("Offset"), "offset", &wr, this, 0.0), linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", JoinTypeConverter, &wr, this, JOIN_MITER), miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 4.0), attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, false), update_on_knot_move(_("Live update"), _("Update while moving handle"), "update_on_knot_move", &wr, this, true) { show_orig_path = true; registerParameter(&linejoin_type); registerParameter(&unit); registerParameter(&offset); registerParameter(&miter_limit); registerParameter(&attempt_force_join); registerParameter(&update_on_knot_move); offset.param_set_increments(0.1, 0.1); offset.param_set_digits(6); offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); _knot_entity = nullptr; _provides_knotholder_entities = true; apply_to_clippath_and_mask = true; prev_unit = unit.get_abbreviation(); liveknot = false; fillrule = fill_nonZero; } LPEOffset::~LPEOffset() { modified_connection.disconnect(); }; bool LPEOffset::doOnOpen(SPLPEItem const *lpeitem) { bool fixed = false; if (!is_load || is_applied) { return fixed; } legacytest_livarotonly = false; Glib::ustring version = lpeversion.param_getSVGValue(); if (version < "1.2") { if (!SP_ACTIVE_DESKTOP) { legacytest_livarotonly = true; } lpeversion.param_setValue("1.2", true); fixed = true; } return fixed; } void LPEOffset::doOnApply(SPLPEItem const* lpeitem) { lpeversion.param_setValue("1.2", true); } void LPEOffset::modified(SPObject *obj, guint flags) { if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { // Get the used fillrule SPCSSAttr *css; const gchar *val; css = sp_repr_css_attr (sp_lpe_item->getRepr() , "style"); val = sp_repr_css_property (css, "fill-rule", nullptr); FillRuleFlatten fillrule_chan = fill_nonZero; if (val && strcmp (val, "evenodd") == 0) { fillrule_chan = fill_oddEven; } if (fillrule != fillrule_chan) { sp_lpe_item_update_patheffect (sp_lpe_item, true, true); } } } Geom::Point get_nearest_point(Geom::PathVector pathv, Geom::Point point) { Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); std::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(point); if (pathvectortime) { Geom::PathTime pathtime = pathvectortime->asPathTime(); res = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); } return res; } /* double get_separation(Geom::PathVector pathv, Geom::Point point, Geom::Point point_b) { Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); std::optional pathvectortime = pathv.nearestTime(point); std::optional pathvectortime_b = pathv.nearestTime(point_b); if (pathvectortime && pathvectortime_b) { Geom::PathTime pathtime = pathvectortime->asPathTime(); Geom::PathTime pathtime_b = pathvectortime_b->asPathTime(); if ((*pathvectortime).path_index == (*pathvectortime_b).path_index) { return std::abs((pathtime.curve_index + pathtime.t) - (pathtime_b.curve_index + pathtime_b.t)); } } return -1; } */ void LPEOffset::transform_multiply(Geom::Affine const &postmul, bool /*set*/) { refresh_widgets = true; if (!postmul.isTranslation()) { Geom::Affine current_affine = sp_item_transform_repr(sp_lpe_item); offset.param_transform_multiply(postmul * current_affine.inverse(), true); } offset_pt *= postmul; } Geom::Point LPEOffset::get_default_point(Geom::PathVector pathv) { Geom::Point origin = Geom::Point(Geom::infinity(), Geom::infinity()); Geom::OptRect bbox = pathv.boundsFast(); if (bbox) { origin = Geom::Point((*bbox).midpoint()[Geom::X], (*bbox).top()); origin = get_nearest_point(pathv, origin); } return origin; } double LPEOffset::sp_get_offset(Geom::Point origin) { double ret_offset = 0; int winding_value = mix_pathv_all.winding(origin); bool inset = false; if (winding_value % 2 != 0) { inset = true; } ret_offset = Geom::distance(origin, get_nearest_point(mix_pathv_all, origin)); if (inset) { ret_offset *= -1; } return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale; } void LPEOffset::addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) { hp_vec.push_back(helper_path); } void LPEOffset::doBeforeEffect (SPLPEItem const* lpeitem) { SPObject *obj = dynamic_cast(sp_lpe_item); if (is_load && obj) { modified_connection = obj->connectModified(sigc::mem_fun(*this, &LPEOffset::modified)); } original_bbox(lpeitem); SPGroup *group = dynamic_cast(sp_lpe_item); if (group) { mix_pathv_all.clear(); } this->scale = lpeitem->i2doc_affine().descrim(); if (!is_load && prev_unit != unit.get_abbreviation()) { offset.param_set_value(Inkscape::Util::Quantity::convert(offset, prev_unit, unit.get_abbreviation())); } prev_unit = unit.get_abbreviation(); } int offset_winding(Geom::PathVector pathvector, Geom::Path path) { int wind = 0; Geom::Point p = path.initialPoint(); for (auto i:pathvector) { if (i == path) continue; if (!i.boundsFast().contains(p)) continue; wind += i.winding(p); } return wind; } void LPEOffset::doAfterEffect(SPLPEItem const * /*lpeitem*/, SPCurve *curve) { if (offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { if (_knot_entity) { _knot_entity->knot_get(); } } if (is_load) { offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); } if (_knot_entity && sp_lpe_item && !liveknot) { Geom::PathVector out; // we don do this on groups, editing is joining ito so no need to update knot SPShape *shape = dynamic_cast(sp_lpe_item); if (shape) { out = SP_SHAPE(sp_lpe_item)->curve()->get_pathvector(); offset_pt = get_nearest_point(out, offset_pt); _knot_entity->knot_get(); } } } // TODO: find a way to not remove wanted self intersections // previously are some failed attempts /* // Taken from Knot LPE duple code static Geom::Path::size_type size_nondegenerate(Geom::Path const &path) { Geom::Path::size_type retval = path.size_default(); const Geom::Curve &closingline = path.back_closed(); // the closing line segment is always of type // Geom::LineSegment. if (are_near(closingline.initialPoint(), closingline.finalPoint())) { // closingline.isDegenerate() did not work, because it only checks for // *exact* zero length, which goes wrong for relative coordinates and // rounding errors... // the closing line segment has zero-length. So stop before that one! retval = path.size_open(); } return retval; } gint get_nearest_corner(Geom::OptRect bbox, Geom::Point point) { if (bbox) { double distance_a = Geom::distance(point, (*bbox).corner(0)); double distance_b = Geom::distance(point, (*bbox).corner(1)); double distance_c = Geom::distance(point, (*bbox).corner(2)); double distance_d = Geom::distance(point, (*bbox).corner(3)); std::vector distances{distance_a, distance_b, distance_c, distance_d}; std::vector::iterator mindistance = std::min_element(distances.begin(), distances.end()); return std::distance(distances.begin(), mindistance); } return -1; } // This way not work with good selfintersections on consecutive curves // and when there is nodes nearest to points // I try different methods to cleanup without luky // if anyone is interested there is a way to explore // if in original path the section into the 2 nearest point dont have // self intersections we can suppose this is a intersection to remove // it works very well but in good selfintersections work only in one offset direction bool consecutiveCurves(Geom::Path pathin, Geom::Point p) { Geom::Coord mindist = std::numeric_limits::max(); size_t pos; for (size_t i = 0; i < pathin.size_default(); ++i) { Geom::Curve const &c = pathin.at(i); double dist = Geom::distance(p, c.boundsFast()); if (dist >= mindist) { continue; } Geom::Coord t = c.nearestTime(p); Geom::Coord d = Geom::distance(c.pointAt(t), p); if (d < mindist) { pos = i; mindist = d; } } size_t pos_b; mindist = std::numeric_limits::max(); for (size_t i = 0; i < pathin.size_default(); ++i) { Geom::Curve const &c = pathin.at(i); double dist = Geom::distance(p, c.boundsFast()); if (dist >= mindist || pos == i) { continue; } Geom::Coord t = c.nearestTime(p); Geom::Coord d = Geom::distance(c.pointAt(t), p); if (d < mindist) { pos_b = i; mindist = d; } } if (Geom::are_near(Geom::distance(pos,pos_b), 1)) { return true; } return false; } Geom::Path removeIntersects(Geom::Path pathin, Geom::Path pathorig, size_t skipcross) { Geom::Path out; Geom::Crossings crossings = Geom::self_crossings(pathin); size_t counter = 0; for (auto cross : crossings) { if (!Geom::are_near(cross.ta, cross.tb, 0.01)) { size_t sizepath = size_nondegenerate(pathin); double ta = cross.ta > cross.tb ? cross.tb : cross.ta; double tb = cross.ta > cross.tb ? cross.ta : cross.tb; if (!pathin.closed()) { counter++; if (skipcross >= counter) { continue; } bool removeint = consecutiveCurves(pathorig, pathin.pointAt(ta)); Geom::Path p0 = pathin; if (removeint) { Geom::Path p1 = pathin.portion(ta, tb); if ((*p1.boundsFast()).maxExtent() > 0.01) { p0 = pathin.portion(0, ta); if (!Geom::are_near(tb, sizepath, 0.01)) { Geom::Path p2 = pathin.portion(tb, sizepath); p0.setFinal(p2.initialPoint()); p0.append(p2); } } else { skipcross++; } } else { skipcross++; } out = removeIntersects(p0, pathorig, skipcross); return out; } } } return pathin; } */ Geom::Path removeIntersects(Geom::Path pathin) { // I have a pending ping to moazin for 1.2 to fix open paths offeset self intesections (Jabiertxof) // For 1.1 I comment the code because simply slow a lot or crash sometimes and never work really well /* Geom::Path out; Geom::Crossings crossings = Geom::self_crossings(pathin); static size_t maxiter = 0; if (!maxiter) { maxiter = crossings.size(); } for (auto cross : crossings) { maxiter--; if (!maxiter) { return pathin; } if (!Geom::are_near(cross.ta, cross.tb, 0.01)) { size_t sizepath = size_nondegenerate(pathin); double ta = cross.ta > cross.tb ? cross.tb : cross.ta; double tb = cross.ta > cross.tb ? cross.ta : cross.tb; if (!pathin.closed()) { Geom::Path p0 = pathin; Geom::Path p1 = pathin.portion(ta, tb); p0 = pathin.portion(0, ta); if (!Geom::are_near(tb, sizepath, 0.01)) { Geom::Path p2 = pathin.portion(tb, sizepath); p0.setFinal(p2.initialPoint()); p0.append(p2); } out = removeIntersects(p0); return out; } } } */ return pathin; } static Geom::PathVector sp_simplify_pathvector(Geom::PathVector original_pathv, double threshold) { Path* pathliv = Path_for_pathvector(original_pathv); pathliv->ConvertEvenLines(threshold); pathliv->Simplify(threshold); return Geom::parse_svg_path(pathliv->svg_dump_path()); } Geom::PathVector LPEOffset::doEffect_path(Geom::PathVector const & path_in) { Geom::PathVector ret_closed; Geom::PathVector ret_open; SPItem *item = current_shape; SPDocument *document = getSPDoc(); if (!item || !document) { return path_in; } // Get the used fillrule SPCSSAttr *css; const gchar *val; css = sp_repr_css_attr (item->getRepr() , "style"); val = sp_repr_css_property (css, "fill-rule", nullptr); fillrule = fill_nonZero; if (val && strcmp (val, "evenodd") == 0) { fillrule = fill_oddEven; } double tolerance = -1; if (liveknot) { tolerance = 3; } // Get the doc units offset double to_offset = Inkscape::Util::Quantity::convert(offset, unit.get_abbreviation(), "px") / this->scale; Geom::PathVector open_pathv; Geom::PathVector closed_pathv; Geom::PathVector mix_pathv; Geom::PathVector mix_pathv_workon; Geom::PathVector orig_pathv = pathv_to_linear_and_cubic_beziers(path_in); helper_path = orig_pathv; // Store separated open/closed paths Geom::PathVector splitter; for (auto &i : orig_pathv) { // this improve offset in near closed paths if (Geom::are_near(i.initialPoint(), i.finalPoint())) { i.close(true); } if (i.closed()) { closed_pathv.push_back(i); } else { open_pathv.push_back(i); } } sp_flatten(closed_pathv, fillrule); // we flatten using original fill rule mix_pathv = open_pathv; for (auto path : closed_pathv) { mix_pathv.push_back(path); } SPGroup *group = dynamic_cast(sp_lpe_item); // Calculate the original pathvector used outside this function // to calculate the offset if (group) { mix_pathv_all.insert(mix_pathv_all.begin(), mix_pathv.begin(), mix_pathv.end()); } else { mix_pathv_all = mix_pathv; } if (to_offset < 0) { Geom::OptRect bbox = mix_pathv.boundsFast(); if (bbox) { (*bbox).expandBy(to_offset / 2.0); if ((*bbox).hasZeroArea()) { Geom::PathVector empty; return empty; } } } for (auto pathin : closed_pathv) { // Geom::OptRect bbox = pathin.boundsFast(); // if (pbbox && (*pbbox).minExtent() > to_offset) { mix_pathv_workon.push_back(pathin); //} } mix_pathv_workon.insert(mix_pathv_workon.begin(), open_pathv.begin(), open_pathv.end()); if (offset == 0.0) { if (is_load && offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { offset_pt = get_default_point(path_in); if (_knot_entity) { _knot_entity->knot_get(); } } return path_in; } Geom::OptRect bbox = closed_pathv.boundsFast(); double bboxsize = 0; if (bbox) { bboxsize = (*bbox).maxExtent(); } LineJoinType join = static_cast(linejoin_type.get_value()); Geom::PathVector ret_closed_tmp; if (to_offset > 0) { for (auto &i : mix_pathv_workon) { Geom::Path tmp = half_outline( i, to_offset, (attempt_force_join ? std::numeric_limits::max() : miter_limit), join, tolerance); if (tmp.closed()) { Geom::OptRect pbbox = tmp.boundsFast(); if (pbbox && (*pbbox).minExtent() > to_offset) { ret_closed_tmp.push_back(tmp); } } else { Geom::Path tmp_b = half_outline(i.reversed(), to_offset, (attempt_force_join ? std::numeric_limits::max() : miter_limit), join, tolerance); Geom::PathVector switch_pv_a(tmp); Geom::PathVector switch_pv_b(tmp_b); double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt)); double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt)); if (distance_b < distance_a) { ret_open.push_back(removeIntersects(tmp)); } else { ret_open.push_back(removeIntersects(tmp_b)); } } } sp_flatten(ret_closed_tmp, fill_nonZero); for (auto path : ret_closed_tmp) { ret_closed.push_back(path); } } else if (to_offset < 0) { for (auto &i : mix_pathv_workon) { double gap = 0.01; if (legacytest_livarotonly) { gap = 0; } Geom::Path tmp = half_outline(i.reversed(), std::abs(to_offset + gap), (attempt_force_join ? std::numeric_limits::max() : miter_limit), join, tolerance); // not remember why i instead tmp, afete 1.1 release we can switch to tmp to tests if (i.closed()) { Geom::PathVector out(tmp); for (auto path : out) { ret_closed.push_back(path); } } else { Geom::Path tmp_b = half_outline(i, std::abs(to_offset), (attempt_force_join ? std::numeric_limits::max() : miter_limit), join, tolerance); Geom::PathVector switch_pv_a(tmp); Geom::PathVector switch_pv_b(tmp_b); double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt)); double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt)); if (distance_b < distance_a) { ret_open.push_back(removeIntersects(tmp)); } else { ret_open.push_back(removeIntersects(tmp_b)); } } } if (!ret_closed.empty()) { Geom::PathVector outline; for (const auto &i : mix_pathv_workon) { if (i.closed()) { Geom::PathVector tmp = Inkscape::outline(i, std::abs(to_offset * 2), 4.0, join, static_cast(BUTT_FLAT), tolerance); outline.insert(outline.begin(), tmp.begin(), tmp.end()); } } sp_flatten(outline, fill_nonZero); double size = Geom::L2(Geom::bounds_fast(ret_closed)->dimensions()); size /= sp_lpe_item->i2doc_affine().descrim(); ret_closed = sp_pathvector_boolop(outline, ret_closed, bool_op_diff, fill_nonZero, fill_nonZero, legacytest_livarotonly); if (!liveknot && legacytest_livarotonly) { ret_closed = sp_simplify_pathvector(ret_closed, 0.0003 * size); } } } ret_closed.insert(ret_closed.begin(), ret_open.begin(), ret_open.end()); return ret_closed; } void LPEOffset::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { _knot_entity = new OfS::KnotHolderEntityOffsetPoint(this); _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPEOffset", _("Offset point")); _knot_entity->knot->setMode(Inkscape::CANVAS_ITEM_CTRL_MODE_COLOR); _knot_entity->knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); _knot_entity->knot->setFill(0xFF6600FF, 0x4BA1C7FF, 0xCF1410FF, 0xFF6600FF); _knot_entity->knot->setStroke(0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF); _knot_entity->knot->updateCtrl(); offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); knotholder->add(_knot_entity); } namespace OfS { void KnotHolderEntityOffsetPoint::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) { using namespace Geom; LPEOffset* lpe = dynamic_cast(_effect); Geom::Point s = snap_knot_position(p, state); double offset = lpe->sp_get_offset(s); lpe->offset_pt = s; if (lpe->update_on_knot_move) { lpe->liveknot = true; lpe->offset.param_set_value(offset); sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, false); } else { lpe->liveknot = false; } } void KnotHolderEntityOffsetPoint::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) { LPEOffset *lpe = dynamic_cast(_effect); lpe->refresh_widgets = true; lpe->liveknot = false; using namespace Geom; Geom::Point s = lpe->offset_pt; double offset = lpe->sp_get_offset(s); lpe->offset.param_set_value(offset); sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); } Geom::Point KnotHolderEntityOffsetPoint::knot_get() const { LPEOffset *lpe = dynamic_cast(_effect); if (!lpe) { return Geom::Point(); } if (!lpe->update_on_knot_move) { return lpe->offset_pt; } Geom::Point nearest = lpe->offset_pt; if (nearest == Geom::Point(Geom::infinity(), Geom::infinity())) { Geom::PathVector out; SPGroup *group = dynamic_cast(item); SPShape *shape = dynamic_cast(item); if (group) { std::vector item_list = sp_item_group_item_list(group); for (auto child : item_list) { SPShape *subchild = dynamic_cast(child); if (subchild) { Geom::PathVector tmp = subchild->curve()->get_pathvector(); out.insert(out.begin(), tmp.begin(), tmp.end()); sp_flatten(out, fill_oddEven); } } } else if (shape) { SPCurve const *c = shape->curve(); if (c) { out = c->get_pathvector(); } } if (!out.empty()) { nearest = lpe->get_default_point(out); } } lpe->offset_pt = nearest; return lpe->offset_pt; } } // namespace OfS } //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 :