summaryrefslogtreecommitdiffstats
path: root/src/live_effects/lpe-offset.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/live_effects/lpe-offset.cpp710
1 files changed, 710 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..b351b6d
--- /dev/null
+++ b/src/live_effects/lpe-offset.cpp
@@ -0,0 +1,710 @@
+// 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 "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 <glibmm/i18n.h>
+
+namespace Inkscape {
+namespace LivePathEffect {
+
+namespace OfS {
+ class KnotHolderEntityOffsetPoint : public LPEKnotHolderEntity {
+ public:
+ KnotHolderEntityOffsetPoint(LPEOffset * effect) : LPEKnotHolderEntity(effect) {}
+ ~KnotHolderEntityOffsetPoint() override
+ {
+ LPEOffset *lpe = dynamic_cast<LPEOffset *>(_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<unsigned> 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<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, 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<Geom::PathVectorTime> pathvectortime = pathv.nearestTime(point);
+ std::optional<Geom::PathVectorTime> 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<Geom::PathVector> &hp_vec)
+{
+ hp_vec.push_back(helper_path);
+}
+
+void
+LPEOffset::doBeforeEffect (SPLPEItem const* lpeitem)
+{
+ SPObject *obj = dynamic_cast<SPObject *>(sp_lpe_item);
+ if (is_load && obj) {
+ modified_connection = obj->connectModified(sigc::mem_fun(*this, &LPEOffset::modified));
+ }
+ original_bbox(lpeitem);
+ SPGroup *group = dynamic_cast<SPGroup *>(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<SPShape *>(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<double> distances{distance_a, distance_b, distance_c, distance_d};
+ std::vector<double>::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<Geom::Coord>::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<Geom::Coord>::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<SPGroup *>(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<LineJoinType>(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<double>::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<double>::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<double>::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<double>::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<LineCapType>(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<LPEOffset *>(_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<LPEOffset *>(_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<LPEOffset *>(_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<SPGroup *>(item);
+ SPShape *shape = dynamic_cast<SPShape *>(item);
+ if (group) {
+ std::vector<SPItem *> item_list = sp_item_group_item_list(group);
+ for (auto child : item_list) {
+ SPShape *subchild = dynamic_cast<SPShape *>(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 :