summaryrefslogtreecommitdiffstats
path: root/src/ui/tools/eraser-tool.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/tools/eraser-tool.cpp')
-rw-r--r--src/ui/tools/eraser-tool.cpp1229
1 files changed, 1229 insertions, 0 deletions
diff --git a/src/ui/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp
new file mode 100644
index 0000000..5b9cbab
--- /dev/null
+++ b/src/ui/tools/eraser-tool.cpp
@@ -0,0 +1,1229 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Eraser drawing mode
+ *
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ * MenTaLguY <mental@rydia.net>
+ * Jon A. Cruz <jon@joncruz.org>
+ * Abhishek Sharma
+ * Rafael Siejakowski <rs@rs-math.net>
+ *
+ * The original dynadraw code:
+ * Paul Haeberli <paul@sgi.com>
+ *
+ * Copyright (C) 1998 The Free Software Foundation
+ * Copyright (C) 1999-2005 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * Copyright (C) 2005-2007 bulia byak
+ * Copyright (C) 2006 MenTaLguY
+ * Copyright (C) 2008 Jon A. Cruz
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noERASER_VERBOSE
+
+#include "eraser-tool.h"
+
+#include <string>
+#include <cstring>
+#include <numeric>
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <glibmm/i18n.h>
+
+#include <2geom/bezier-utils.h>
+#include <2geom/pathvector.h>
+
+#include "context-fns.h"
+#include "desktop-events.h"
+#include "desktop-style.h"
+#include "desktop.h"
+#include "document-undo.h"
+#include "document.h"
+#include "layer-manager.h"
+#include "message-context.h"
+#include "message-stack.h"
+#include "path-chemistry.h"
+#include "rubberband.h"
+#include "selection-chemistry.h"
+#include "selection.h"
+
+#include "display/curve.h"
+#include "display/control/canvas-item-bpath.h"
+
+#include "include/macros.h"
+
+#include "object/sp-clippath.h"
+#include "object/sp-image.h"
+#include "object/sp-item-group.h"
+#include "object/sp-path.h"
+#include "object/sp-rect.h"
+#include "object/sp-root.h"
+#include "object/sp-shape.h"
+#include "object/sp-text.h"
+#include "object/sp-use.h"
+
+#include "ui/icon-names.h"
+
+#include "svg/svg.h"
+
+
+using Inkscape::DocumentUndo;
+
+namespace Inkscape {
+namespace UI {
+namespace Tools {
+
+extern EraserToolMode const DEFAULT_ERASER_MODE = EraserToolMode::CUT;
+
+EraserTool::EraserTool(SPDesktop *desktop)
+ : DynamicBase(desktop, "/tools/eraser", "eraser.svg")
+{
+ accumulated.reset(new SPCurve());
+ currentcurve.reset(new SPCurve());
+
+ cal1.reset(new SPCurve());
+ cal2.reset(new SPCurve());
+
+ currentshape = new Inkscape::CanvasItemBpath(desktop->getCanvasSketch());
+ currentshape->set_stroke(0x0);
+ currentshape->set_fill(trace_color_rgba, trace_wind_rule);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop));
+
+ sp_event_context_read(this, "mass");
+ sp_event_context_read(this, "wiggle");
+ sp_event_context_read(this, "angle");
+ sp_event_context_read(this, "width");
+ sp_event_context_read(this, "thinning");
+ sp_event_context_read(this, "tremor");
+ sp_event_context_read(this, "flatness");
+ sp_event_context_read(this, "tracebackground");
+ sp_event_context_read(this, "usepressure");
+ sp_event_context_read(this, "usetilt");
+ sp_event_context_read(this, "abs_width");
+ sp_event_context_read(this, "cap_rounding");
+
+ is_drawing = false;
+ //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (prefs->getBool("/tools/eraser/selcue", false) != 0) {
+ enableSelectionCue();
+ }
+ _updateMode();
+
+ // TODO temp force:
+ enableSelectionCue();
+}
+
+EraserTool::~EraserTool()
+{
+ delete currentshape;
+ currentshape = nullptr;
+}
+
+/** Reads the current Eraser mode from Preferences and sets `mode` accordingly. */
+void EraserTool::_updateMode()
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if (!prefs) {
+ return;
+ }
+
+ int mode_idx = prefs->getInt("/tools/eraser/mode", 1); // Cut mode is default
+
+ // Note: the integer indices must agree with those in EraserToolbar::_modeAsInt()
+ if (mode_idx == 0) {
+ mode = EraserToolMode::DELETE;
+ } else if (mode_idx == 1) {
+ mode = EraserToolMode::CUT;
+ } else if (mode_idx == 2) {
+ mode = EraserToolMode::CLIP;
+ } else {
+ g_printerr("Error: invalid mode setting \"%d\" for Eraser tool!", mode_idx);
+ mode = DEFAULT_ERASER_MODE;
+ }
+}
+
+// TODO: After switch to C++20, replace this with std::lerp
+inline double flerp(double const f0, double const f1, double const p)
+{
+ return f0 + (f1 - f0) * p;
+}
+
+inline double square(double const x)
+{
+ return x * x;
+}
+
+void EraserTool::_reset(Geom::Point p)
+{
+ last = cur = getNormalizedPoint(p);
+ vel = Geom::Point(0, 0);
+ vel_max = 0;
+ acc = Geom::Point(0, 0);
+ ang = Geom::Point(0, 0);
+ del = Geom::Point(0, 0);
+}
+
+void EraserTool::_extinput(GdkEvent *event)
+{
+ if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure)) {
+ pressure = CLAMP(pressure, min_pressure, max_pressure);
+ } else {
+ pressure = default_pressure;
+ }
+
+ if (gdk_event_get_axis(event, GDK_AXIS_XTILT, &xtilt)) {
+ xtilt = CLAMP(xtilt, min_tilt, max_tilt);
+ } else {
+ xtilt = default_tilt;
+ }
+
+ if (gdk_event_get_axis(event, GDK_AXIS_YTILT, &ytilt)) {
+ ytilt = CLAMP(ytilt, min_tilt, max_tilt);
+ } else {
+ ytilt = default_tilt;
+ }
+}
+
+bool EraserTool::_apply(Geom::Point p)
+{
+ /* Calculate force and acceleration */
+ Geom::Point n = getNormalizedPoint(p);
+ Geom::Point force = n - cur;
+
+ // If force is below the absolute threshold `epsilon`,
+ // or we haven't yet reached `vel_start` (i.e. at the beginning of stroke)
+ // _and_ the force is below the (higher) `epsilon_start` threshold,
+ // discard this move.
+ // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen,
+ // especially bothersome at the start of the stroke where we don't yet have the inertia to
+ // smooth them out.
+ if (Geom::L2(force) < epsilon || (vel_max < vel_start && Geom::L2(force) < epsilon_start)) {
+ return false;
+ }
+
+ // Calculate mass
+ double const m = flerp(1.0, 160.0, mass);
+ acc = force / m;
+ vel += acc; // Calculate new velocity
+ double const speed = Geom::L2(vel);
+
+ if (speed > vel_max) {
+ vel_max = speed;
+ } else if (speed < epsilon) {
+ return false; // return early if movement is insignificant
+ }
+
+ /* Calculate angle of eraser tool */
+ double angle_fixed{0.0};
+ if (usetilt) {
+ // 1a. calculate nib angle from input device tilt:
+ Geom::Point normal{ytilt, xtilt};
+ if (!Geom::is_zero(normal)) {
+ angle_fixed = Geom::atan2(normal);
+ }
+ } else {
+ // 1b. fixed angle (absolutely flat nib):
+ angle_fixed = angle * M_PI / 180.0; // convert to radians
+ }
+ if (flatness < 0.0) {
+ // flips direction. Useful when usetilt is true
+ // allows simulating both pen/charcoal and broad-nibbed pen
+ angle_fixed *= -1;
+ }
+
+ // 2. Angle perpendicular to vel (absolutely non-flat nib):
+ double angle_dynamic = Geom::atan2(Geom::rot90(vel));
+ // flip angle_dynamic to force it to be in the same half-circle as angle_fixed
+ bool flipped = false;
+ if (fabs(angle_dynamic - angle_fixed) > M_PI_2) {
+ angle_dynamic += M_PI;
+ flipped = true;
+ }
+ // normalize angle_dynamic
+ if (angle_dynamic > M_PI) {
+ angle_dynamic -= 2 * M_PI;
+ }
+ if (angle_dynamic < -M_PI) {
+ angle_dynamic += 2 * M_PI;
+ }
+
+ // 3. Average them using flatness parameter:
+ // find the flatness-weighted bisector angle, unflip if angle_dynamic was flipped
+ // FIXME: when `vel` is oscillating around the fixed angle, the new_ang flips back and forth.
+ // How to avoid this?
+ double new_ang = flerp(angle_dynamic, angle_fixed, fabs(flatness)) - (flipped ? M_PI : 0);
+
+ // Try to detect a sudden flip when the new angle differs too much from the previous for the
+ // current velocity; in that case discard this move
+ double angle_delta = Geom::L2(Geom::Point(cos(new_ang), sin(new_ang)) - ang);
+ if (angle_delta / speed > 4000) {
+ return false;
+ }
+
+ // convert to point
+ ang = Geom::Point(cos(new_ang), sin(new_ang));
+
+ /* Apply drag */
+ double const d = flerp(0.0, 0.5, square(drag));
+ vel *= 1.0 - d;
+
+ /* Update position */
+ last = cur;
+ cur += vel;
+
+ return true;
+}
+
+void EraserTool::_brush()
+{
+ g_assert(npoints >= 0 && npoints < SAMPLING_SIZE);
+
+ // How much velocity thins strokestyle
+ double const vel_thinning = flerp(0, 160, vel_thin);
+
+ // Influence of pressure on thickness
+ double const pressure_thick = (usepressure ? pressure : 1.0);
+
+ // get the real brush point, not the same as pointer (affected by mass drag)
+ Geom::Point brush = getViewPoint(cur);
+
+ double const trace_thick = 1;
+ double const speed = Geom::L2(vel);
+ double effective_width = (pressure_thick * trace_thick - vel_thinning * speed) * width;
+
+ double tremble_left = 0, tremble_right = 0;
+ if (tremor > 0) {
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double y1, y2;
+ _generateNormalDist2(y1, y2);
+
+ // deflect both left and right edges randomly and independently, so that:
+ // (1) tremor=1 corresponds to sigma=1, decreasing tremor narrows the bell curve;
+ // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths;
+ // (3) deflection somewhat depends on speed, to prevent fast strokes looking
+ // comparatively smooth and slow ones excessively jittery
+ double const width_coefficient = 0.15 + 0.8 * effective_width;
+ double const speed_coefficient = 0.35 + 14 * speed;
+ double const total_coefficient = tremor * width_coefficient * speed_coefficient;
+
+ tremble_left = y1 * total_coefficient;
+ tremble_right = y2 * total_coefficient;
+ }
+
+ double const min_width = 0.02 * width;
+ if (effective_width < min_width) {
+ effective_width = min_width;
+ }
+
+ double dezoomify_factor = 0.05 * 1000;
+ if (!abs_width) {
+ dezoomify_factor /= _desktop->current_zoom();
+ }
+
+ Geom::Point del_left = dezoomify_factor * (effective_width + tremble_left) * ang;
+ Geom::Point del_right = dezoomify_factor * (effective_width + tremble_right) * ang;
+
+ point1[npoints] = brush + del_left;
+ point2[npoints] = brush - del_right;
+
+ if (nowidth) {
+ point1[npoints] = Geom::middle_point(point1[npoints], point2[npoints]);
+ }
+ del = Geom::middle_point(del_left, del_right);
+
+ npoints++;
+}
+
+void EraserTool::_generateNormalDist2(double &r1, double &r2)
+{
+ // obtain two normally distributed random variables, using polar Box-Muller transform
+ double x1, x2, w;
+ do {
+ x1 = 2.0 * g_random_double_range(0, 1) - 1.0;
+ x2 = 2.0 * g_random_double_range(0, 1) - 1.0;
+ w = square(x1) + square(x2);
+ } while (w >= 1.0);
+ w = sqrt(-2.0 * log(w) / w);
+ r1 = x1 * w;
+ r2 = x2 * w;
+}
+
+void EraserTool::_cancel()
+{
+ dragging = false;
+ is_drawing = false;
+ ungrabCanvasEvents();
+
+ _removeTemporarySegments();
+
+ /* reset accumulated curve */
+ accumulated->reset();
+ _clearCurrent();
+ repr = nullptr;
+}
+
+/** Removes all temporary line segments */
+void EraserTool::_removeTemporarySegments()
+{
+ for (auto segment : segments) {
+ delete segment;
+ }
+ segments.clear();
+}
+
+bool EraserTool::root_handler(GdkEvent* event)
+{
+ bool ret = false;
+ _updateMode();
+ switch (event->type) {
+ case GDK_BUTTON_PRESS:
+ if (event->button.button == 1) {
+ if (!Inkscape::have_viable_layer(_desktop, defaultMessageContext())) {
+ return true;
+ }
+
+ Geom::Point const button_w(event->button.x, event->button.y);
+ Geom::Point const button_dt(_desktop->w2d(button_w));
+
+ _reset(button_dt);
+ _extinput(event);
+ _apply(button_dt);
+ accumulated->reset();
+
+ repr = nullptr;
+
+ if (mode == EraserToolMode::DELETE) {
+ auto rubberband = Inkscape::Rubberband::get(_desktop);
+ rubberband->start(_desktop, button_dt);
+ rubberband->setMode(RUBBERBAND_MODE_TOUCHPATH);
+ }
+ /* initialize first point */
+ npoints = 0;
+
+ grabCanvasEvents();
+ is_drawing = true;
+ ret = true;
+ }
+ break;
+
+ case GDK_MOTION_NOTIFY: {
+ Geom::Point const motion_w(event->motion.x, event->motion.y);
+ Geom::Point motion_dt(_desktop->w2d(motion_w));
+ _extinput(event);
+
+ message_context->clear();
+
+ if (is_drawing && (event->motion.state & GDK_BUTTON1_MASK)) {
+ dragging = true;
+
+ message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke"));
+
+ if (!_apply(motion_dt)) {
+ ret = true;
+ break;
+ }
+
+ if (cur != last) {
+ _brush();
+ g_assert(npoints > 0);
+ _fitAndSplit(false);
+ }
+
+ ret = true;
+ }
+ if (mode == EraserToolMode::DELETE) {
+ accumulated->reset();
+ Inkscape::Rubberband::get(_desktop)->move(motion_dt);
+ }
+ break;
+ }
+ case GDK_BUTTON_RELEASE: {
+ if (event->button.button != 1) {
+ break;
+ }
+
+ Geom::Point const motion_w(event->button.x, event->button.y);
+ Geom::Point const motion_dt(_desktop->w2d(motion_w));
+
+ ungrabCanvasEvents();
+
+ is_drawing = false;
+
+ if (dragging) {
+ dragging = false;
+
+ _apply(motion_dt);
+ _removeTemporarySegments();
+
+ /* Create object */
+ _fitAndSplit(true);
+ _accumulate();
+ _setToAccumulated(); // performs document_done
+
+ /* reset accumulated curve */
+ accumulated->reset();
+
+ _clearCurrent();
+ repr = nullptr;
+
+ message_context->clear();
+ ret = true;
+ }
+
+ if (mode == EraserToolMode::DELETE) {
+ auto r = Inkscape::Rubberband::get(_desktop);
+ if (r->is_started()) {
+ r->stop();
+ }
+ }
+
+ break;
+ }
+ case GDK_KEY_PRESS:
+ ret = _handleKeypress(&event->key);
+ break;
+
+ case GDK_KEY_RELEASE:
+ switch (get_latin_keyval(&event->key)) {
+ case GDK_KEY_Control_L:
+ case GDK_KEY_Control_R:
+ message_context->clear();
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (!ret) {
+ ret = DynamicBase::root_handler(event);
+ }
+ return ret;
+}
+
+/** Analyses and handles a key press event, returns true if processed, false if not. */
+bool EraserTool::_handleKeypress(const GdkEventKey *key)
+{
+ bool ret = false;
+ bool just_ctrl = (key->state & GDK_CONTROL_MASK) // Ctrl key is down
+ && !(key->state & (GDK_MOD1_MASK | GDK_SHIFT_MASK)); // but not Alt or Shift
+
+ bool just_alt = (key->state & GDK_MOD1_MASK) // Alt is down
+ && !(key->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); // but not Ctrl or Shift
+
+ switch (get_latin_keyval(key)) {
+ case GDK_KEY_Right:
+ case GDK_KEY_KP_Right:
+ if (!just_ctrl) {
+ width += 0.01;
+ if (width > 1.0) {
+ width = 1.0;
+ }
+ // Alt+X sets focus to this spinbutton as well
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Left:
+ case GDK_KEY_KP_Left:
+ if (!just_ctrl) {
+ width -= 0.01;
+ if (width < 0.01) {
+ width = 0.01;
+ }
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Home:
+ case GDK_KEY_KP_Home:
+ width = 0.01;
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ break;
+
+ case GDK_KEY_End:
+ case GDK_KEY_KP_End:
+ width = 1.0;
+ _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
+ ret = true;
+ break;
+
+ case GDK_KEY_x:
+ case GDK_KEY_X:
+ if (just_alt) {
+ _desktop->setToolboxFocusTo("eraser-width");
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_Escape:
+ if (mode == EraserToolMode::DELETE) {
+ Inkscape::Rubberband::get(_desktop)->stop();
+ }
+ if (is_drawing) {
+ // if drawing, cancel, otherwise pass it up for deselecting
+ _cancel();
+ ret = true;
+ }
+ break;
+
+ case GDK_KEY_z:
+ case GDK_KEY_Z:
+ if (just_ctrl && is_drawing) { // Ctrl+Z pressed while drawing
+ _cancel();
+ ret = true;
+ } // if not drawing, pass it up for undo
+ break;
+
+ default:
+ break;
+ }
+ return ret;
+}
+
+void EraserTool::_clearCurrent()
+{
+ // reset bpath
+ currentshape->set_bpath(nullptr);
+
+ // reset curve
+ currentcurve->reset();
+ cal1->reset();
+ cal2->reset();
+
+ // reset points
+ npoints = 0;
+}
+
+void EraserTool::_setToAccumulated()
+{
+ bool work_done = false;
+ SPDocument *document = _desktop->doc();
+
+ if (!accumulated->is_empty()) {
+ if (!repr) {
+ /* Create object */
+ Inkscape::XML::Document *xml_doc = document->getReprDoc();
+ Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path");
+
+ /* Set style */
+ sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false);
+
+ repr = eraser_repr;
+ }
+ SPObject *top_layer = _desktop->layerManager().currentRoot();
+ SPItem *item_repr = SP_ITEM(top_layer->appendChildRepr(repr));
+ Inkscape::GC::release(repr);
+ item_repr->updateRepr();
+ Geom::PathVector pathv = accumulated->get_pathvector() * _desktop->dt2doc();
+ pathv *= item_repr->i2doc_affine().inverse();
+ repr->setAttribute("d", sp_svg_write_path(pathv));
+ Geom::OptRect eraser_bbox;
+ if (repr) {
+ bool was_selection = false;
+ Inkscape::Selection *selection = _desktop->getSelection();
+ _updateMode();
+ SPItem *acid = SP_ITEM(document->getObjectByRepr(repr));
+ eraser_bbox = acid->documentVisualBounds();
+ std::vector<SPItem *> remaining_items;
+ std::vector<SPItem *> to_work_on;
+ if (selection->isEmpty()) {
+ if (mode == EraserToolMode::CUT || mode == EraserToolMode::CLIP) {
+ to_work_on = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox,
+ false, false, false, true);
+ } else {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+ to_work_on = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ }
+ to_work_on.erase(std::remove(to_work_on.begin(), to_work_on.end(), acid), to_work_on.end());
+ } else {
+ if (mode == EraserToolMode::DELETE) {
+ Inkscape::Rubberband *r = Inkscape::Rubberband::get(_desktop);
+ auto selected_items = selection->items();
+ std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
+ for (auto i : selected_items) {
+ if (std::find(touched.begin(), touched.end(), i) == touched.end()) {
+ remaining_items.push_back(i);
+ } else {
+ to_work_on.push_back(i);
+ }
+ }
+ } else {
+ to_work_on.insert(to_work_on.end(), selection->items().begin(), selection->items().end());
+ }
+ was_selection = true;
+ }
+
+ if (to_work_on.empty()) {
+ _clearStatusBar();
+ } else {
+ selection->clear();
+ if (mode == EraserToolMode::CUT) {
+ Error status = ALL_GOOD;
+ for (auto item : to_work_on) {
+ Error retval = _cutErase(item, eraser_bbox, remaining_items);
+ if (retval == ALL_GOOD) {
+ work_done = true;
+ } else {
+ status |= retval;
+ }
+ }
+
+ status &= ~(NOT_IN_BOUNDS | NON_EXISTENT); // Clear flags not handled at the moment
+ if (status == ALL_GOOD) {
+ _clearStatusBar();
+ } else { // Something went wrong during the cut operation
+ if (status & RASTER_IMAGE) {
+ _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode "
+ "instead."));
+ } else if (status & ERROR_GROUP) {
+ _setStatusBarMessage(_("Cannot cut out from a group, ungroup the objects "
+ "first."));
+ } else if (status & NO_AREA_PATH) {
+ _setStatusBarMessage(_("Cannot cut out from a path with zero area, use "
+ "<b>Clip</b> mode instead."));
+ }
+ }
+ } else if (mode == EraserToolMode::CLIP) {
+ if (!nowidth) {
+ for (auto item : to_work_on) {
+ _clipErase(item, item_repr->parent, eraser_bbox);
+ }
+ if (was_selection) {
+ remaining_items = to_work_on;
+ }
+ work_done = true;
+ }
+ _clearStatusBar();
+ } else if (mode == EraserToolMode::DELETE) {
+ for (auto item : to_work_on) {
+ item->deleteObject(true);
+ }
+ work_done = true;
+ _clearStatusBar();
+ }
+
+ if (was_selection && !remaining_items.empty()) {
+ selection->add(remaining_items.begin(), remaining_items.end());
+ }
+ }
+ // Remove the eraser stroke itself:
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+ } else if (repr) {
+ sp_repr_unparent(repr);
+ repr = nullptr;
+ }
+
+ if (work_done) {
+ DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser"));
+ } else {
+ DocumentUndo::cancel(document);
+ }
+}
+
+/**
+ * @brief Erases from a shape by cutting
+ * @param item - the item to be erased
+ * @param eraser_bbox - bounding box of the eraser stroke
+ * @param survivers - items that survived the erase operation will be added to this vector
+ * @return type of error encountered
+ */
+EraserTool::Error EraserTool::_cutErase(SPItem* item, Geom::OptRect const &eraser_bbox,
+ std::vector<SPItem *> &survivers)
+{
+ // If the item cannot be cut, preserve it
+ if (Error error = EraserTool::_uncuttableItemType(item)) {
+ survivers.push_back(item);
+ return error;
+ }
+
+ Geom::OptRect bbox = item->documentVisualBounds();
+ if (!bbox || !bbox->intersects(eraser_bbox)) {
+ survivers.push_back(item);
+ return NOT_IN_BOUNDS;
+ }
+
+ // If the item is a clone, we check if the original is cuttable before unlinking it
+ if (SPUse *use = dynamic_cast<SPUse *>(item)) {
+ int depth = use->cloneDepth();
+ if (depth < 0) {
+ survivers.push_back(item);
+ return NON_EXISTENT;
+ }
+ // We recurse into the chain of uses until we reach the original item
+ SPItem *original_item = item;
+ for (int i = 0; i < depth; ++i) {
+ SPUse *intermediate_clone = dynamic_cast<SPUse *>(original_item);
+ original_item = intermediate_clone->get_original();
+ }
+ if (Error error = EraserTool::_uncuttableItemType(original_item)) {
+ survivers.push_back(item);
+ return error;
+ }
+ item = use->unlink();
+ }
+
+ _booleanErase(item, survivers);
+ return ALL_GOOD;
+}
+
+/** Returns error flags for items that cannot be meaningfully erased in CUT mode */
+EraserTool::Error EraserTool::_uncuttableItemType(SPItem *item)
+{
+ if (!item) {
+ return NON_EXISTENT;
+ } else if (dynamic_cast<SPGroup *>(item)) {
+ return ERROR_GROUP; // TODO: handle groups in the future
+ } else if (dynamic_cast<SPImage *>(item)) {
+ return RASTER_IMAGE;
+ } else if (_isStraightSegment(item)) {
+ return NO_AREA_PATH;
+ } else {
+ return ALL_GOOD;
+ }
+}
+
+/**
+ * @brief Performs a boolean difference or cut operation which implements the CUT mode operation
+ * @param erasee - the item to be erased
+ * @param survivers - items that survived the erase operation will be added to this vector
+ */
+void EraserTool::_booleanErase(SPItem *erasee, std::vector<SPItem *> &survivers) const
+{
+ XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ XML::Node *duplicate_stroke = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(duplicate_stroke);
+ GC::release(duplicate_stroke); // parent takes over
+ ObjectSet operands(_desktop);
+ operands.set(duplicate_stroke);
+ if (!nowidth) {
+ operands.pathUnion(true, true);
+ }
+ operands.add(erasee);
+ operands.removeLPESRecursive(true);
+
+ _handleStrokeStyle(erasee);
+
+ if (nowidth) {
+ operands.pathCut(true, true);
+ } else {
+ operands.pathDiff(true, true);
+ }
+
+ auto *prefs = Preferences::get();
+ bool break_apart = prefs->getBool("/tools/eraser/break_apart", false);
+ if (!break_apart) {
+ operands.combine(true, true);
+ } else if (!nowidth) {
+ operands.breakApart(true, false, true);
+ }
+ survivers.insert(survivers.end(), operands.items().begin(), operands.items().end());
+}
+
+/** Handles the "evenodd" stroke style */
+void EraserTool::_handleStrokeStyle(SPItem *item) const
+{
+ if (item->style->fill_rule.value == SP_WIND_RULE_EVENODD) {
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_set_property(css, "fill-rule", "evenodd");
+ sp_desktop_set_style(_desktop, css);
+ sp_repr_css_attr_unref(css);
+ css = nullptr;
+ }
+}
+
+/** Sets an error message in the status bar */
+void EraserTool::_setStatusBarMessage(char *message)
+{
+ MessageId id = _desktop->messageStack()->flash(WARNING_MESSAGE, message);
+ _our_messages.push_back(id);
+}
+
+/** Clears all of messages sent by us to the status bar */
+void EraserTool::_clearStatusBar()
+{
+ if (!_our_messages.empty()) {
+ auto ms = _desktop->messageStack();
+ for (MessageId id : _our_messages) {
+ ms->cancel(id);
+ }
+ _our_messages.clear();
+ }
+}
+
+/** Clips through an item */
+void EraserTool::_clipErase(SPItem *item, SPObject *parent, Geom::OptRect &eraser_box)
+{
+ Inkscape::ObjectSet w_selection(_desktop);
+ Geom::OptRect bbox = item->documentVisualBounds();
+ Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc();
+ Inkscape::XML::Node *dup = repr->duplicate(xml_doc);
+ repr->parent()->appendChild(dup);
+ Inkscape::GC::release(dup); // parent takes over
+ w_selection.set(dup);
+ w_selection.pathUnion(true);
+ bool delete_old_clip_path = false;
+ if (bbox && bbox->intersects(*eraser_box)) {
+ SPClipPath *clip_path = item->getClipObject();
+ if (clip_path) {
+ std::vector<SPItem *> selected;
+ selected.push_back(SP_ITEM(clip_path->firstChild()));
+ std::vector<Inkscape::XML::Node *> to_select;
+ std::vector<SPItem *> items(selected);
+ sp_item_list_to_curves(items, selected, to_select);
+ Inkscape::XML::Node *clip_data = SP_ITEM(clip_path->firstChild())->getRepr();
+ if (!clip_data && !to_select.empty()) {
+ clip_data = *(to_select.begin());
+ }
+ if (clip_data) {
+ Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc);
+ if (dup_clip) {
+ SPItem *dup_clip_obj = SP_ITEM(parent->appendChildRepr(dup_clip));
+ Inkscape::GC::release(dup_clip);
+ if (dup_clip_obj) {
+ dup_clip_obj->transform *= item->getRelativeTransform(SP_ITEM(parent));
+ dup_clip_obj->updateRepr();
+ delete_old_clip_path = true;
+ w_selection.raiseToTop(true);
+ w_selection.add(dup_clip);
+ w_selection.pathDiff(true, true);
+ }
+ }
+ }
+ } else {
+ Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
+ sp_desktop_apply_style_tool(_desktop, rect_repr, "/tools/eraser", false);
+ SPRect *rect = SP_RECT(parent->appendChildRepr(rect_repr));
+ Inkscape::GC::release(rect_repr);
+ rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height());
+ rect->transform = SP_ITEM(rect->parent)->i2doc_affine().inverse();
+
+ rect->updateRepr();
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ w_selection.raiseToTop(true);
+ w_selection.add(rect);
+ w_selection.pathDiff(true, true);
+ }
+ w_selection.raiseToTop(true);
+ w_selection.add(item);
+ w_selection.setMask(true, false, true);
+ if (delete_old_clip_path) {
+ clip_path->deleteObject(true);
+ }
+ } else {
+ SPItem *erase_clip = w_selection.singleItem();
+ if (erase_clip) {
+ erase_clip->deleteObject(true);
+ }
+ }
+}
+
+/** Detects whether the given path is a straight line segment which encloses no area
+ or consists of several such segments */
+bool EraserTool::_isStraightSegment(SPItem *path)
+{
+ SPPath *as_path = dynamic_cast<SPPath *>(path);
+ if (!as_path) {
+ return false;
+ }
+
+ auto const &curve = as_path->curve();
+ if (!curve) {
+ return false;
+ }
+ auto const &pathvector = curve->get_pathvector();
+
+ // Check if all segments are straight and collinear
+ for (auto const &path : pathvector) {
+ Geom::Point initial_tangent = path.front().unitTangentAt(0.0);
+ for (auto const &segment : path) {
+ if (!segment.isLineSegment()) {
+ return false;
+ } else {
+ Geom::Point dir = segment.unitTangentAt(0.0);
+ if (!Geom::are_near(dir, initial_tangent) && !Geom::are_near(-dir, initial_tangent)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+void EraserTool::_addCap(SPCurve &curve, Geom::Point const &pre, Geom::Point const &from, Geom::Point const &to,
+ Geom::Point const &post, double rounding)
+{
+ Geom::Point vel = rounding * Geom::rot90(to - from) / M_SQRT2;
+ double mag = Geom::L2(vel);
+
+ Geom::Point v_in = from - pre;
+ double mag_in = Geom::L2(v_in);
+
+ if (mag_in > epsilon) {
+ v_in = mag * v_in / mag_in;
+ } else {
+ v_in = Geom::Point(0, 0);
+ }
+
+ Geom::Point v_out = to - post;
+ double mag_out = Geom::L2(v_out);
+
+ if (mag_out > epsilon) {
+ v_out = mag * v_out / mag_out;
+ } else {
+ v_out = Geom::Point(0, 0);
+ }
+
+ if (Geom::L2(v_in) > epsilon || Geom::L2(v_out) > epsilon) {
+ curve.curveto(from + v_in, to + v_out, to);
+ }
+}
+
+void EraserTool::_accumulate()
+{
+ // construct a crude outline of the eraser's path.
+ // this desperately needs to be rewritten to use the path outliner...
+ if (!cal1->get_segment_count() || !cal2->get_segment_count()) {
+ return;
+ }
+
+ auto rev_cal2 = cal2->create_reverse();
+
+ g_assert(!cal1->first_path()->closed());
+ g_assert(!rev_cal2->first_path()->closed());
+
+ Geom::BezierCurve const *dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>(cal1->first_segment());
+ Geom::BezierCurve const *rev_cal2_firstseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2->first_segment());
+ Geom::BezierCurve const *dc_cal1_lastseg = dynamic_cast<Geom::BezierCurve const *>(cal1->last_segment());
+ Geom::BezierCurve const *rev_cal2_lastseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2->last_segment());
+
+ g_assert(dc_cal1_firstseg);
+ g_assert(rev_cal2_firstseg);
+ g_assert(dc_cal1_lastseg);
+ g_assert(rev_cal2_lastseg);
+
+ accumulated->append(*cal1);
+ if (!nowidth) {
+ _addCap(*accumulated,
+ dc_cal1_lastseg->finalPoint() - dc_cal1_lastseg->unitTangentAt(1),
+ dc_cal1_lastseg->finalPoint(),
+ rev_cal2_firstseg->initialPoint(),
+ rev_cal2_firstseg->initialPoint() + rev_cal2_firstseg->unitTangentAt(0),
+ cap_rounding);
+
+ accumulated->append(*rev_cal2, true);
+
+ _addCap(*accumulated,
+ rev_cal2_lastseg->finalPoint() - rev_cal2_lastseg->unitTangentAt(1),
+ rev_cal2_lastseg->finalPoint(),
+ dc_cal1_firstseg->initialPoint(),
+ dc_cal1_firstseg->initialPoint() + dc_cal1_firstseg->unitTangentAt(0),
+ cap_rounding);
+
+ accumulated->closepath();
+ }
+ cal1->reset();
+ cal2->reset();
+}
+
+void EraserTool::_fitAndSplit(bool releasing)
+{
+ double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance);
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ nowidth = (prefs->getDouble("/tools/eraser/width", 1) == 0);
+
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:R=%c]", releasing ? 'T' : 'F');
+#endif
+ if (npoints >= SAMPLING_SIZE || npoints <= 0) {
+ return; // just clicked
+ }
+
+ if (npoints == SAMPLING_SIZE - 1 || releasing) {
+ _completeBezier(tolerance_sq, releasing);
+
+#ifdef ERASER_VERBOSE
+ g_print("[%d]Yup\n", npoints);
+#endif
+ if (!releasing) {
+ _fitDrawLastPoint();
+ }
+
+ // Copy last point
+ point1[0] = point1[npoints - 1];
+ point2[0] = point2[npoints - 1];
+ npoints = 1;
+ } else {
+ _drawTemporaryBox();
+ }
+}
+
+void EraserTool::_completeBezier(double tolerance_sq, bool releasing)
+{
+ /* Current eraser */
+ if (cal1->is_empty() || cal2->is_empty()) {
+ /* dc->npoints > 0 */
+ cal1->reset();
+ cal2->reset();
+
+ cal1->moveto(point1[0]);
+ cal2->moveto(point2[0]);
+ }
+#ifdef ERASER_VERBOSE
+ g_print("[F&S:#] npoints:%d, releasing:%s\n", npoints, releasing ? "TRUE" : "FALSE");
+#endif
+
+ unsigned const bezier_size = 4;
+ unsigned const max_beziers = 8;
+ size_t const bezier_max_length = bezier_size * max_beziers;
+
+ Geom::Point b1[bezier_max_length];
+ gint const nb1 = Geom::bezier_fit_cubic_r(b1, point1, npoints, tolerance_sq, max_beziers);
+ g_assert(nb1 * bezier_size <= gint(G_N_ELEMENTS(b1)));
+
+ Geom::Point b2[bezier_max_length];
+ gint const nb2 = Geom::bezier_fit_cubic_r(b2, point2, npoints, tolerance_sq, max_beziers);
+ g_assert(nb2 * bezier_size <= gint(G_N_ELEMENTS(b2)));
+
+ if (nb1 == -1 || nb2 == -1) {
+ _failedBezierFallback(); // TODO: do we ever need this?
+ return;
+ }
+
+ /* Fit and draw and reset state */
+#ifdef ERASER_VERBOSE
+ g_print("nb1:%d nb2:%d\n", nb1, nb2);
+#endif
+
+ /* CanvasShape */
+ if (!releasing) {
+ currentcurve->reset();
+ currentcurve->moveto(b1[0]);
+
+ for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
+ currentcurve->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ currentcurve->lineto(b2[bezier_size * (nb2 - 1) + 3]);
+
+ for (Geom::Point *bp2 = b2 + bezier_size * (nb2 - 1); bp2 >= b2; bp2 -= bezier_size) {
+ currentcurve->curveto(bp2[2], bp2[1], bp2[0]);
+ }
+
+ // FIXME: segments is always NULL at this point??
+ if (segments.empty()) { // first segment
+ _addCap(*currentcurve, b2[1], b2[0], b1[0], b1[1], cap_rounding);
+ }
+
+ currentcurve->closepath();
+ currentshape->set_bpath(currentcurve.get(), true);
+ }
+
+ /* Current eraser */
+ for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
+ cal1->curveto(bp1[1], bp1[2], bp1[3]);
+ }
+
+ for (Geom::Point *bp2 = b2; bp2 < b2 + bezier_size * nb2; bp2 += bezier_size) {
+ cal2->curveto(bp2[1], bp2[2], bp2[3]);
+ }
+}
+
+void EraserTool::_failedBezierFallback()
+{
+ /* fixme: ??? */
+#ifdef ERASER_VERBOSE
+ g_print("[_failedBezierFallback] - failed to fit cubic.\n");
+#endif
+ _drawTemporaryBox();
+
+ for (gint i = 1; i < npoints; i++) {
+ cal1->lineto(point1[i]);
+ }
+
+ for (gint i = 1; i < npoints; i++) {
+ cal2->lineto(point2[i]);
+ }
+}
+
+void EraserTool::_fitDrawLastPoint()
+{
+ g_assert(!currentcurve->is_empty());
+
+ guint32 fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true);
+ double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/eraser");
+ double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/eraser", true);
+
+ guint fill = (fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity * fillOpacity);
+
+ auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get(), true);
+ cbp->set_fill(fill, trace_wind_rule);
+ cbp->set_stroke(0x0);
+
+ /* fixme: Cannot we cascade it to root more clearly? */
+ cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop));
+ segments.push_back(cbp);
+
+ if (mode == EraserToolMode::DELETE) {
+ cbp->hide();
+ currentshape->hide();
+ }
+}
+
+void EraserTool::_drawTemporaryBox()
+{
+ currentcurve->reset();
+
+ currentcurve->moveto(point1[npoints - 1]);
+
+ for (gint i = npoints - 2; i >= 0; i--) {
+ currentcurve->lineto(point1[i]);
+ }
+
+ for (gint i = 0; i < npoints; i++) {
+ currentcurve->lineto(point2[i]);
+ }
+
+ if (npoints >= 2) {
+ _addCap(*currentcurve,
+ point2[npoints - 2], point2[npoints - 1],
+ point1[npoints - 1], point1[npoints - 2], cap_rounding);
+ }
+
+ currentcurve->closepath();
+ currentshape->set_bpath(currentcurve.get(), true);
+}
+
+} // namespace Tools
+} // namespace UI
+} // 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:fileencoding=utf-8:textwidth=99 :