diff options
Diffstat (limited to 'src/live_effects/lpe-offset.cpp')
-rw-r--r-- | src/live_effects/lpe-offset.cpp | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/src/live_effects/lpe-offset.cpp b/src/live_effects/lpe-offset.cpp new file mode 100644 index 0000000..804c677 --- /dev/null +++ b/src/live_effects/lpe-offset.cpp @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <offset> implementation + */ +/* + * Authors: + * Maximilian Albert + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * Copyright (C) Jabierto Arraiza 2015 <jabier.arraiza@marker.es> + * + * 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 <algorithm> + +#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 <glibmm/i18n.h> + +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<unsigned> 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<unsigned> 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<Geom::PathVectorTime> 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<Geom::PathVectorTime> 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<SPGroup *>(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<Geom::PathVector> &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<SPGroup const *>(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<Geom::LineSegment>(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<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), + static_cast<LineCapType>(BUTT_FLAT)); + Geom::Path against_dir = half_outline(original.reversed(), + to_offset, + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), + static_cast<LineCapType>(BUTT_FLAT)); + Geom::Path with_dir_gap = + half_outline(original, std::abs(to_offset + gap_size), + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), static_cast<LineCapType>(BUTT_FLAT)); + Geom::Path against_dir_gap = + half_outline(original.reversed(), std::abs(to_offset + gap_size), + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), static_cast<LineCapType>(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<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), + static_cast<LineCapType>(BUTT_FLAT)); + Geom::Path against_dir_size = half_outline(original.reversed(), + 2, + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), + static_cast<LineCapType>(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<SPGroup *>(item); + LPEOffset* lpe = dynamic_cast<LPEOffset *>(_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<SPGroup *>(item); + LPEOffset * lpe = dynamic_cast<LPEOffset *> (_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<Geom::PathVectorTime> 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 : |