// 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 "live_effects/parameter/enum.h" #include "live_effects/lpe-offset.h" #include "display/curve.h" #include "inkscape.h" #include "helper/geom.h" #include "helper/geom-pathstroke.h" #include <2geom/sbasis-to-bezier.h> #include <2geom/piecewise.h> #include <2geom/path-intersection.h> #include <2geom/intersection-graph.h> #include <2geom/elliptical-arc.h> #include <2geom/angle.h> #include <2geom/curve.h> #include "object/sp-shape.h" #include "knot-holder-entity.h" #include "knotholder.h" #include "util/units.h" #include "knot.h" #include #include "splivarot.h" #include "livarot/Path.h" #include "livarot/Shape.h" #include "svg/svg.h" #include <2geom/elliptical-arc.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) {} void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; Geom::Point knot_get() const override; private: }; } // OfS static const Util::EnumData JoinTypeData[] = { {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"}, }; 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, true), 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(4); 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(); } LPEOffset::~LPEOffset() = default; typedef FillRule FillRuleFlatten; static void sp_flatten(Geom::PathVector &pathvector, FillRuleFlatten fillkind) { Path *orig = new Path; orig->LoadPathVector(pathvector); Shape *theShape = new Shape; Shape *theRes = new Shape; orig->ConvertWithBackData (1.0); orig->Fill (theShape, 0); theRes->ConvertToShape (theShape, FillRule(fillkind)); Path *originaux[1]; originaux[0] = orig; Path *res = new Path; theRes->ConvertToForme (res, 1, originaux, true); delete theShape; delete theRes; char *res_d = res->svg_dump_path (); delete res; delete orig; pathvector = sp_svg_read_pathv(res_d); } Geom::Point LPEOffset::get_nearest_point(Geom::PathVector pathv, Geom::Point point) const { Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); boost::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; } void LPEOffset::transform_multiply(Geom::Affine const &postmul, bool /*set*/) { offset.param_transform_multiply(postmul, true); offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); } Geom::Point LPEOffset::get_default_point(Geom::PathVector pathv) const { Geom::Point origin = Geom::Point(Geom::infinity(), Geom::infinity()); Geom::OptRect bbox = pathv.boundsFast(); if (bbox) { if (SP_IS_GROUP(sp_lpe_item)) { origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); } else { origin = Geom::Point((*bbox).midpoint()[Geom::X],(*bbox).top()); origin = get_nearest_point(pathv, origin); } } return origin; } double sp_get_distance_point(Geom::PathVector pathv, Geom::Point origin) { boost::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(origin); Geom::Point nearest = origin; if (pathvectortime) { Geom::PathTime pathtime = pathvectortime->asPathTime(); nearest = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); // we are not on simplet case on a parallel offset if (Geom::are_near(pathtime.t, 0) || Geom::are_near(pathtime.t, 1)) { bool start = Geom::are_near(nearest, pathv[(*pathvectortime).path_index].initialPoint()); bool end = Geom::are_near(nearest, pathv[(*pathvectortime).path_index].finalPoint()); bool closed = Geom::are_near(pathv[(*pathvectortime).path_index].initialPoint(), pathv[(*pathvectortime).path_index].finalPoint()); double dist_corner = 0; Geom::Point pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); Geom::Point pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); if (closed && start) { pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); } if (!closed && (start || end)) { if (start) { pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); } } Geom::Line line; line.setPoints(pa, pb); line.setAngle(line.angle() + Geom::rad_from_deg(90)); Geom::Point nearestline = line.pointAt(line.nearestTime(origin)); boost::optional pathvectortime_line = pathv.nearestTime(nearestline); if (pathvectortime_line) { Geom::PathTime pathtime_line = pathvectortime_line->asPathTime(); nearest = pathv[(*pathvectortime_line).path_index].pointAt(pathtime_line.curve_index + pathtime_line.t); dist_corner = Geom::distance(nearestline, nearest); } if (closed || !(start || end)) { if (closed && start) { size_t sizepath = pathv[(*pathvectortime).path_index].size(); pa = pathv[(*pathvectortime).path_index].pointAt(sizepath - 1); pb = pathv[(*pathvectortime).path_index].pointAt(sizepath - 2); } else { pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 2); } line; line.setPoints(pa, pb); line.setAngle(line.angle() + Geom::rad_from_deg(90)); Geom::Point nearestline = line.pointAt(line.nearestTime(origin)); boost::optional pathvectortime_line = pathv.nearestTime(nearestline); if (pathvectortime_line) { Geom::PathTime pathtime_line = pathvectortime_line->asPathTime(); nearest = pathv[(*pathvectortime_line).path_index].pointAt(pathtime_line.curve_index + pathtime_line.t); return std::max(dist_corner, Geom::distance(nearestline, nearest)); } } } } return Geom::distance(origin, nearest); } double LPEOffset::sp_get_offset(Geom::Point origin) { SPGroup * group = dynamic_cast(sp_lpe_item); double ret_offset = 0; if (group) { Geom::Point initial = get_default_point(filled_rule_pathv); ret_offset = Geom::distance(origin, initial); if (origin[Geom::Y] < initial[Geom::Y]) { ret_offset *= -1; } return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale; } int winding_value = filled_rule_pathv.winding(origin); bool inset = false; if (winding_value % 2 != 0) { inset = true; } ret_offset = sp_get_distance_point(filled_rule_pathv, 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) { original_bbox(lpeitem); SPDocument *document = getSPDoc(); if (!document) { return; } if (prev_unit != unit.get_abbreviation()) { offset.param_set_value(Inkscape::Util::Quantity::convert(offset, prev_unit, unit.get_abbreviation())); } prev_unit = unit.get_abbreviation(); SPGroup const *group = dynamic_cast(lpeitem); this->scale = lpeitem->i2doc_affine().descrim(); if (group) { helper_path.clear(); Geom::Point origin = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); Geom::Point endpont = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); endpont[Geom::Y] = endpont[Geom::Y] + Inkscape::Util::Quantity::convert(offset, unit.get_abbreviation(), "px")/ this->scale; Geom::Path hp(origin); hp.appendNew(endpont); helper_path.push_back(hp); } } 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; } /* Geom::Path sp_get_outer(Geom::PathVector pathv) { sp_flatten(pathv, fill_nonZero); Geom::Path out_bounds; Geom::Path out_size; Geom::OptRect bounds; double size = 0; bool get_bounds = false; for (auto path_child:pathv) { Geom::OptRect path_bounds = path_child.boundsFast(); if (path_bounds) { double path_size = (*path_bounds).width() + (*path_bounds).height(); if (path_bounds.contains(bounds)) { bounds = path_bounds; out_bounds = path_child; get_bounds = true; } if (size < path_size) { size = path_size; out_size = path_child; } } } pathv.clear(); if (get_bounds) { return out_bounds; } return out_size; } Geom::PathVector sp_get_inner(Geom::PathVector pathv, Geom::Path outer) { Geom::PathVector out; for (auto path_child:pathv) { if (path_child != outer) { out.push_back(path_child); } } return out; } */ Geom::PathVector LPEOffset::doEffect_path(Geom::PathVector const & path_in) { SPItem * item = SP_ITEM(current_shape); if (!item) { return path_in; } SPCSSAttr *css; const gchar *val; css = sp_repr_css_attr (item->getRepr() , "style"); val = sp_repr_css_property (css, "fill-rule", nullptr); FillRuleFlatten fillrule = fill_nonZero; if (val && strcmp (val, "evenodd") == 0) { fillrule = fill_oddEven; } Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); filled_rule_pathv = original_pathv; sp_flatten(filled_rule_pathv, fillrule); if (offset == 0.0) { return path_in; } Geom::PathVector ret; Geom::PathVector open_ret; Geom::PathVector ret_outline; for (const auto & path_it : filled_rule_pathv) { Geom::Path original = path_it; if (original.empty()) { continue; } int wdg = offset_winding(filled_rule_pathv, original); bool path_inside = wdg % 2 != 0; double gap_size = -0.01; bool closed = original.closed(); double to_offset = Inkscape::Util::Quantity::convert(std::abs(offset), unit.get_abbreviation(), "px") / this->scale; if (to_offset <= 0.01) { return path_in; } Geom::OptRect original_bounds = original.boundsFast(); double original_height = 0; double original_width = 0; if (original_bounds) { original_height = (*original_bounds).height(); original_width = (*original_bounds).width(); } if (path_inside && (offset * 2 > (original_height + original_width) / 2.0)) { continue; } Geom::Path with_dir = half_outline(original, to_offset, (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); Geom::Path against_dir = half_outline(original.reversed(), to_offset, (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); Geom::Path with_dir_gap = half_outline(original, std::abs(to_offset + gap_size), (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); Geom::Path against_dir_gap = half_outline(original.reversed(), std::abs(to_offset + gap_size), (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); bool reversed = false; Geom::OptRect against_dir_bounds = against_dir.boundsFast(); Geom::OptRect with_dir_bounds = with_dir.boundsFast(); double with_dir_height = 0; double against_dir_height = 0; double with_dir_width = 0; double against_dir_width = 0; if (with_dir_bounds) { with_dir_height = (*with_dir_bounds).height(); with_dir_width = (*with_dir_bounds).width(); } if (against_dir_bounds) { against_dir_height = (*against_dir_bounds).height(); against_dir_width = (*against_dir_bounds).width(); } reversed = against_dir_bounds.contains(with_dir_bounds) == false; // We can have a strange result for the bounding box container // Gives a wrong result, in theory, it happens sometimes on expand offset if (offset > 0 && ((original_width < against_dir_width && original_width < with_dir_width) || (original_height < against_dir_height && original_height < with_dir_height))) { Geom::Path with_dir_size = half_outline(original, 2, (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); Geom::Path against_dir_size = half_outline(original.reversed(), 2, (attempt_force_join ? std::numeric_limits::max() : miter_limit), static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); Geom::OptRect against_dir_size_bounds = against_dir_size.boundsFast(); Geom::OptRect with_dir_size_bounds = with_dir_size.boundsFast(); reversed = against_dir_size_bounds.contains(with_dir_size_bounds) == false; } Geom::PathVector tmp; Geom::PathVector outline; Geom::Path big; Geom::Path gap; Geom::Path small; if (offset < 0) { outline.push_back(with_dir); outline.push_back(against_dir); sp_flatten(outline, fill_nonZero); } if (reversed || !closed) { big = with_dir; gap = with_dir_gap; small = against_dir; } else { big = against_dir; gap = against_dir_gap; small = with_dir; } //big = sp_get_outer(big); //gap = sp_get_outer(gap); if (!closed) { tmp.push_back(small); double smalldist = sp_get_distance_point(tmp, offset_pt); tmp.clear(); tmp.push_back(big); double bigdist = sp_get_distance_point(tmp, offset_pt); tmp.clear(); if (bigdist > smalldist) { open_ret.push_back(small); } else { open_ret.push_back(big); } continue; } bool fix_reverse = (original_width + original_height) / 2.0 > to_offset * 2; if (offset < 0) { tmp.push_back(gap); } else { if (path_inside) { if (fix_reverse) { tmp.push_back(small); } } else { tmp.push_back(big); } } ret.insert(ret.end(), tmp.begin(), tmp.end()); if (offset < 0) { ret_outline.insert(ret_outline.end(), outline.begin(), outline.end()); } } if (offset < 0) { sp_flatten(ret_outline, fill_nonZero); if (!ret_outline.empty() && !ret.empty()) { ret = sp_pathvector_boolop(ret_outline, ret, bool_op_diff, fill_nonZero, fill_oddEven); } } sp_flatten(ret, fill_nonZero); ret.insert(ret.end(), open_ret.begin(), open_ret.end()); if (offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { offset_pt = get_default_point(ret); } return ret; } void LPEOffset::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { _knot_entity = new OfS::KnotHolderEntityOffsetPoint(this); _knot_entity->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Offset point"), SP_KNOT_SHAPE_CIRCLE); knotholder->add(_knot_entity); } namespace OfS { void KnotHolderEntityOffsetPoint::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) { using namespace Geom; SPGroup * group = dynamic_cast(item); LPEOffset* lpe = dynamic_cast(_effect); Geom::Point s = snap_knot_position(p, state); if (group) { s[Geom::X] = lpe->boundingbox_X.min(); } double offset = lpe->sp_get_offset(s); lpe->offset_pt = s; lpe->offset.param_set_value(offset); if (lpe->update_on_knot_move) { sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, false); } } Geom::Point KnotHolderEntityOffsetPoint::knot_get() const { SPGroup * group = dynamic_cast(item); LPEOffset * lpe = dynamic_cast (_effect); if (!lpe->update_on_knot_move) { return lpe->offset_pt; } Geom::Point nearest = lpe->offset_pt; if (lpe->offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { if (group) { nearest = Geom::Point(lpe->boundingbox_X.min(), lpe->boundingbox_Y.min()); } else { Geom::PathVector out = SP_SHAPE(item)->getCurve(true)->get_pathvector(); nearest = lpe->get_default_point(out); boost::optional pathvectortime = out.nearestTime(nearest); if (pathvectortime) { Geom::PathTime pathtime = pathvectortime->asPathTime(); nearest = out[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); } } } return nearest; } } // 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 :