Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
eraser-tool.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Eraser drawing mode
4 *
5 * Authors:
6 * Mitsuru Oka <oka326@parkcity.ne.jp>
7 * Lauris Kaplinski <lauris@kaplinski.com>
8 * bulia byak <buliabyak@users.sf.net>
9 * MenTaLguY <mental@rydia.net>
10 * Jon A. Cruz <jon@joncruz.org>
11 * Abhishek Sharma
12 * Rafael Siejakowski <rs@rs-math.net>
13 *
14 * The original dynadraw code:
15 * Paul Haeberli <paul@sgi.com>
16 *
17 * Copyright (C) 1998 The Free Software Foundation
18 * Copyright (C) 1999-2005 authors
19 * Copyright (C) 2001-2002 Ximian, Inc.
20 * Copyright (C) 2005-2007 bulia byak
21 * Copyright (C) 2006 MenTaLguY
22 * Copyright (C) 2008 Jon A. Cruz
23 *
24 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
25 */
26
27#define noERASER_VERBOSE
28
29#include "eraser-tool.h"
30
31#include <cmath>
32#include <cstring>
33#include <string>
34#include <numeric>
35#include <gtk/gtk.h>
36#include <gdk/gdkkeysyms.h>
37#include <glibmm/i18n.h>
38#include <2geom/bezier-utils.h>
39#include <2geom/pathvector.h>
40
41#include "context-fns.h"
42#include "desktop-events.h"
43#include "desktop-style.h"
44#include "desktop.h"
45#include "document-undo.h"
46#include "document.h"
47#include "layer-manager.h"
48#include "message-context.h"
49#include "message-stack.h"
50#include "path-chemistry.h"
51#include "preferences.h"
52#include "rubberband.h"
53#include "selection-chemistry.h"
54#include "selection.h"
55#include "style.h"
56#include "display/curve.h"
58#include "object/sp-clippath.h"
59#include "object/sp-image.h"
61#include "object/sp-path.h"
62#include "object/sp-rect.h"
63#include "object/sp-shape.h"
64#include "object/sp-use.h"
65#include "ui/icon-names.h"
67#include "svg/svg.h"
68
70
71namespace Inkscape::UI::Tools {
72
74 : DynamicBase(desktop, "/tools/eraser", "eraser.svg")
75 , _break_apart{"/tools/eraser/break_apart", false}
76 , _mode_int{"/tools/eraser/mode", 1} // Cut mode is default
77{
78 currentshape = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
79 currentshape->set_stroke(0x0);
81
82 /* fixme: Cannot we cascade it to root more clearly? */
83 currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop));
84
85 sp_event_context_read(this, "mass");
86 sp_event_context_read(this, "wiggle");
87 sp_event_context_read(this, "angle");
88 sp_event_context_read(this, "width");
89 sp_event_context_read(this, "thinning");
90 sp_event_context_read(this, "tremor");
91 sp_event_context_read(this, "flatness");
92 sp_event_context_read(this, "tracebackground");
93 sp_event_context_read(this, "usepressure");
94 sp_event_context_read(this, "usetilt");
95 sp_event_context_read(this, "abs_width");
96 sp_event_context_read(this, "cap_rounding");
97
98 is_drawing = false;
99 //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem
100
101 _mode_int.min = 0;
102 _mode_int.max = 2;
103 _updateMode();
104 _mode_int.action = [this]() { _updateMode(); };
105
107}
108
109EraserTool::~EraserTool() = default;
110
113{
114 int const mode_idx = _mode_int;
115 // Note: the integer indices must agree with those in EraserToolbar::_modeAsInt()
116 if (mode_idx == 0) {
118 } else if (mode_idx == 1) {
120 } else if (mode_idx == 2) {
122 } else {
123 g_printerr("Error: invalid mode setting \"%d\" for Eraser tool!", mode_idx);
125 }
126}
127
128inline double square(double const x)
129{
130 return x * x;
131}
132
134{
136 vel = Geom::Point(0, 0);
137 vel_max = 0;
138 acc = Geom::Point(0, 0);
139 ang = Geom::Point(0, 0);
140 del = Geom::Point(0, 0);
141}
142
144{
145 if (ext.pressure) {
146 pressure = std::clamp(*ext.pressure, min_pressure, max_pressure);
147 } else {
149 }
150
151 if (ext.xtilt) {
152 xtilt = std::clamp(*ext.xtilt, min_tilt, max_tilt);
153 } else {
155 }
156
157 if (ext.ytilt) {
158 ytilt = std::clamp(*ext.ytilt, min_tilt, max_tilt);
159 } else {
161 }
162}
163
165{
166 /* Calculate force and acceleration */
168 Geom::Point force = n - cur;
169
170 // If force is below the absolute threshold `epsilon`,
171 // or we haven't yet reached `vel_start` (i.e. at the beginning of stroke)
172 // _and_ the force is below the (higher) `epsilon_start` threshold,
173 // discard this move.
174 // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen,
175 // especially bothersome at the start of the stroke where we don't yet have the inertia to
176 // smooth them out.
177 if (Geom::L2(force) < epsilon || (vel_max < vel_start && Geom::L2(force) < epsilon_start)) {
178 return false;
179 }
180
181 // Calculate mass
182 double const m = std::lerp(1.0, 160.0, mass);
183 acc = force / m;
184 vel += acc; // Calculate new velocity
185 double const speed = Geom::L2(vel);
186
187 if (speed > vel_max) {
188 vel_max = speed;
189 } else if (speed < epsilon) {
190 return false; // return early if movement is insignificant
191 }
192
193 /* Calculate angle of eraser tool */
194 double angle_fixed{0.0};
195 if (usetilt) {
196 // 1a. calculate nib angle from input device tilt:
197 Geom::Point normal{ytilt, xtilt};
198 if (!Geom::is_zero(normal)) {
199 angle_fixed = Geom::atan2(normal);
200 }
201 } else {
202 // 1b. fixed angle (absolutely flat nib):
203 angle_fixed = angle * M_PI / 180.0; // convert to radians
204 }
205 if (flatness < 0.0) {
206 // flips direction. Useful when usetilt is true
207 // allows simulating both pen/charcoal and broad-nibbed pen
208 angle_fixed *= -1;
209 }
210
211 // 2. Angle perpendicular to vel (absolutely non-flat nib):
212 double angle_dynamic = Geom::atan2(Geom::rot90(vel));
213 // flip angle_dynamic to force it to be in the same half-circle as angle_fixed
214 bool flipped = false;
215 if (fabs(angle_dynamic - angle_fixed) > M_PI_2) {
216 angle_dynamic += M_PI;
217 flipped = true;
218 }
219 // normalize angle_dynamic
220 if (angle_dynamic > M_PI) {
221 angle_dynamic -= 2 * M_PI;
222 }
223 if (angle_dynamic < -M_PI) {
224 angle_dynamic += 2 * M_PI;
225 }
226
227 // 3. Average them using flatness parameter:
228 // find the flatness-weighted bisector angle, unflip if angle_dynamic was flipped
229 // FIXME: when `vel` is oscillating around the fixed angle, the new_ang flips back and forth.
230 // How to avoid this?
231 double new_ang = std::lerp(angle_dynamic, angle_fixed, fabs(flatness)) - (flipped ? M_PI : 0);
232
233 // Try to detect a sudden flip when the new angle differs too much from the previous for the
234 // current velocity; in that case discard this move
235 double angle_delta = Geom::L2(Geom::Point(cos(new_ang), sin(new_ang)) - ang);
236 if (angle_delta / speed > 4000) {
237 return false;
238 }
239
240 // convert to point
241 ang = Geom::Point(cos(new_ang), sin(new_ang));
242
243 /* Apply drag */
244 double const d = std::lerp(0.0, 0.5, square(drag));
245 vel *= 1.0 - d;
246
247 /* Update position */
248 last = cur;
249 cur += vel;
250
251 return true;
252}
253
255{
256 g_assert(npoints >= 0 && npoints < SAMPLING_SIZE);
257
258 // How much velocity thins strokestyle
259 double const vel_thinning = std::lerp(0, 160, vel_thin);
260
261 // Influence of pressure on thickness
262 double const pressure_thick = (usepressure ? pressure : 1.0);
263
264 // get the real brush point, not the same as pointer (affected by mass drag)
266
267 double const trace_thick = 1;
268 double const speed = Geom::L2(vel);
269 double effective_width = (pressure_thick * trace_thick - vel_thinning * speed) * width;
270
271 double tremble_left = 0, tremble_right = 0;
272 if (tremor > 0) {
273 // obtain two normally distributed random variables, using polar Box-Muller transform
274 double y1, y2;
275 _generateNormalDist2(y1, y2);
276
277 // deflect both left and right edges randomly and independently, so that:
278 // (1) tremor=1 corresponds to sigma=1, decreasing tremor narrows the bell curve;
279 // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths;
280 // (3) deflection somewhat depends on speed, to prevent fast strokes looking
281 // comparatively smooth and slow ones excessively jittery
282 double const width_coefficient = 0.15 + 0.8 * effective_width;
283 double const speed_coefficient = 0.35 + 14 * speed;
284 double const total_coefficient = tremor * width_coefficient * speed_coefficient;
285
286 tremble_left = y1 * total_coefficient;
287 tremble_right = y2 * total_coefficient;
288 }
289
290 double const min_width = 0.02 * width;
291 if (effective_width < min_width) {
292 effective_width = min_width;
293 }
294
295 double dezoomify_factor = 0.05 * 1000;
296 if (!abs_width) {
297 dezoomify_factor /= _desktop->current_zoom();
298 }
299
300 Geom::Point del_left = dezoomify_factor * (effective_width + tremble_left) * ang;
301 Geom::Point del_right = dezoomify_factor * (effective_width + tremble_right) * ang;
302
303 point1[npoints] = brush + del_left;
304 point2[npoints] = brush - del_right;
305
306 if (nowidth) {
308 }
309 del = Geom::middle_point(del_left, del_right);
310
311 npoints++;
312}
313
314void EraserTool::_generateNormalDist2(double &r1, double &r2)
315{
316 // obtain two normally distributed random variables, using polar Box-Muller transform
317 double x1, x2, w;
318 do {
319 x1 = 2.0 * g_random_double_range(0, 1) - 1.0;
320 x2 = 2.0 * g_random_double_range(0, 1) - 1.0;
321 w = square(x1) + square(x2);
322 } while (w >= 1.0);
323 w = sqrt(-2.0 * log(w) / w);
324 r1 = x1 * w;
325 r2 = x2 * w;
326}
327
329{
330 dragging = false;
331 is_drawing = false;
333
334 segments.clear();
335
336 /* reset accumulated curve */
339 repr = nullptr;
340}
341
343{
344 bool ret = false;
345
346 inspect_event(event,
347 [&] (ButtonPressEvent const &event) {
348 if (event.num_press == 1 && event.button == 1) {
349 if (!have_viable_layer(_desktop, defaultMessageContext())) {
350 return;
351 ret = true;
352 }
353
354 auto const button_w = event.pos;
355 auto const button_dt = _desktop->w2d(button_w);
356
357 _reset(button_dt);
358 _extinput(event.extinput);
359 _apply(button_dt);
361
362 repr = nullptr;
363
365 auto rubberband = Inkscape::Rubberband::get(_desktop);
366 rubberband->setMode(Rubberband::Mode::TOUCHPATH);
367 rubberband->setHandle(RUBBERBAND_TOUCHPATH_ERASER);
368 rubberband->start(_desktop, button_dt);
369 }
370 /* initialize first point */
371 npoints = 0;
372
374 is_drawing = true;
375 ret = true;
376 }
377 },
378
379 [&] (MotionEvent const &event) {
380 auto const motion_w = event.pos;
381 auto const motion_dt = _desktop->w2d(motion_w);
382 _extinput(event.extinput);
383
384 message_context->clear();
385
386 if (is_drawing && (event.modifiers & GDK_BUTTON1_MASK)) {
387 dragging = true;
388
389 message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke"));
390
391 if (!_apply(motion_dt)) {
392 ret = true;
393 return;
394 }
395
396 if (cur != last) {
397 _brush();
398 g_assert(npoints > 0);
399 _fitAndSplit(false);
400 }
401
402 ret = true;
403 }
405 accumulated.reset();
406 Inkscape::Rubberband::get(_desktop)->move(motion_dt);
407 }
408 },
409
410 [&] (ButtonReleaseEvent const &event) {
411 if (event.button != 1) {
412 return;
413 }
414
415 auto const motion_w = event.pos;
416 auto const motion_dt = _desktop->w2d(motion_w);
417
418 ungrabCanvasEvents();
419
420 is_drawing = false;
421
422 if (dragging) {
423 dragging = false;
424
425 _apply(motion_dt);
426 segments.clear();
427
428 // Create eraser stroke shape
429 _fitAndSplit(true);
430 _accumulate();
431
432 // Perform the actual erase operation
433 auto document = _desktop->getDocument();
434 if (_doWork()) {
435 DocumentUndo::done(document, _("Draw eraser stroke"), INKSCAPE_ICON("draw-eraser"));
436 } else {
437 DocumentUndo::cancel(document);
438 }
439
440 /* reset accumulated curve */
441 accumulated.reset();
442
443 _clearCurrent();
444 repr = nullptr;
445
446 message_context->clear();
447 ret = true;
448 }
449
451 auto r = Inkscape::Rubberband::get(_desktop);
452 if (r->isStarted()) {
453 r->stop();
454 }
455 }
456 },
457
458 [&] (KeyPressEvent const &event) {
459 ret = _handleKeypress(event);
460 },
461
462 [&] (KeyReleaseEvent const &event) {
463 switch (get_latin_keyval(event)) {
464 case GDK_KEY_Control_L:
465 case GDK_KEY_Control_R:
466 message_context->clear();
467 break;
468
469 default:
470 break;
471 }
472 },
473
474 [&] (CanvasEvent const &event) {}
475 );
476
477 return ret || DynamicBase::root_handler(event);
478}
479
481bool EraserTool::_handleKeypress(KeyPressEvent const &key)
482{
483 bool ret = false;
484 bool just_ctrl = (key.modifiers & GDK_CONTROL_MASK) // Ctrl key is down
485 && !(key.modifiers & (GDK_ALT_MASK | GDK_SHIFT_MASK)); // but not Alt or Shift
486
487 bool just_alt = (key.modifiers & GDK_ALT_MASK) // Alt is down
488 && !(key.modifiers & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)); // but not Ctrl or Shift
489
490 switch (get_latin_keyval(key)) {
491 case GDK_KEY_Right:
492 case GDK_KEY_KP_Right:
493 if (!just_ctrl) {
494 width += 0.01;
495 if (width > 1.0) {
496 width = 1.0;
497 }
498 // Alt+X sets focus to this spinbutton as well
499 _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
500 ret = true;
501 }
502 break;
503
504 case GDK_KEY_Left:
505 case GDK_KEY_KP_Left:
506 if (!just_ctrl) {
507 width -= 0.01;
508 if (width < 0.01) {
509 width = 0.01;
510 }
511 _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
512 ret = true;
513 }
514 break;
515
516 case GDK_KEY_Home:
517 case GDK_KEY_KP_Home:
518 width = 0.01;
519 _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
520 ret = true;
521 break;
522
523 case GDK_KEY_End:
524 case GDK_KEY_KP_End:
525 width = 1.0;
526 _desktop->setToolboxAdjustmentValue("eraser-width", width * 100);
527 ret = true;
528 break;
529
530 case GDK_KEY_x:
531 case GDK_KEY_X:
532 if (just_alt) {
533 _desktop->setToolboxFocusTo("eraser-width");
534 ret = true;
535 }
536 break;
537
538 case GDK_KEY_Escape:
539 if (mode == EraserToolMode::DELETE) {
540 Inkscape::Rubberband::get(_desktop)->stop();
541 }
542 if (is_drawing) {
543 // if drawing, cancel, otherwise pass it up for deselecting
544 _cancel();
545 ret = true;
546 }
547 break;
548
549 case GDK_KEY_z:
550 case GDK_KEY_Z:
551 if (just_ctrl && is_drawing) { // Ctrl+Z pressed while drawing
552 _cancel();
553 ret = true;
554 } // if not drawing, pass it up for undo
555 break;
556
557 default:
558 break;
559 }
560 return ret;
561}
562
566SPItem *EraserTool::_insertAcidIntoDocument(SPDocument *document)
567{
568 auto *top_layer = _desktop->layerManager().currentRoot();
569 auto *eraser_item = cast<SPItem>(top_layer->appendChildRepr(repr));
571 eraser_item->updateRepr();
572 Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
573 pathv *= eraser_item->i2doc_affine().inverse();
574 repr->setAttribute("d", sp_svg_write_path(pathv));
575 return cast<SPItem>(document->getObjectByRepr(repr));
576}
577
578void EraserTool::_clearCurrent()
579{
580 // reset bpath
581 currentshape->set_bpath(nullptr);
582
583 // reset curve
584 currentcurve.reset();
585 cal1.reset();
586 cal2.reset();
587
588 // reset points
589 npoints = 0;
590}
591
596bool EraserTool::_doWork()
597{
598 if (accumulated.is_empty()) {
599 if (repr) {
600 sp_repr_unparent(repr);
601 repr = nullptr;
602 }
603 return false;
604 }
605
606 SPDocument *document = _desktop->getDocument();
607 if (!repr) {
608 // Create eraser repr
609 Inkscape::XML::Document *xml_doc = document->getReprDoc();
610 Inkscape::XML::Node *eraser_repr = xml_doc->createElement("svg:path");
611
612 sp_desktop_apply_style_tool(_desktop, eraser_repr, "/tools/eraser", false);
613 repr = eraser_repr;
614 }
615 if (!repr) {
616 return false;
617 }
618
619 Selection *selection = _desktop->getSelection();
620 if (!selection) {
621 return false;
622 }
623 bool was_selection = !selection->isEmpty();
624
625 // Find items to work on as well as items that will be needed to restore the selection afterwards.
626 _survivers.clear();
627 _clearStatusBar();
628
629 std::vector<EraseTarget> to_erase = _findItemsToErase();
630
631 bool work_done = false;
632 if (!to_erase.empty()) {
633 selection->clear();
634 work_done = _performEraseOperation(to_erase, true);
635 if (was_selection && !_survivers.empty()) {
636 selection->add(_survivers.begin(), _survivers.end());
637 }
638 }
639 // Clean up the eraser stroke repr:
640 sp_repr_unparent(repr);
641 repr = nullptr;
642 _acid = nullptr;
643 return work_done;
644}
645
652bool EraserTool::_cutErase(EraseTarget target, bool store_survivers)
653{
654 // If the item is a clone, we check if the original is cuttable before unlinking it
655 if (auto use = cast<SPUse>(target.item)) {
656 auto original = use->trueOriginal();
657 if (_uncuttableItemType(original)) {
658 if (store_survivers && target.was_selected) {
659 _survivers.push_back(target.item);
660 }
661 return false;
662 } else if (auto *group = cast<SPGroup>(original)) {
663 return _probeUnlinkCutClonedGroup(target, use, group, store_survivers);
664 }
665 // A simple clone of a cuttable item: unlink and erase it.
666 target.item = use->unlink();
667 if (target.was_selected && store_survivers) { // Reselect the freshly unlinked item
668 _survivers.push_back(target.item);
669 }
670 }
671 return _booleanErase(target, store_survivers);
672}
673
688bool EraserTool::_probeUnlinkCutClonedGroup(EraseTarget &original_target, SPUse *clone, SPGroup *cloned_group,
689 bool store_survivers)
690{
691 std::vector<EraseTarget> children;
692 children.reserve(cloned_group->getItemCount());
693
694 for (auto *child : cloned_group->childList(false)) {
695 children.emplace_back(cast<SPItem>(child), false);
696 }
697 auto const filtered_children = _filterCutEraseables(children, true);
698
699 // We must now check if any of the eraseable items in the original group, after transforming
700 // to the coordinates of the clone, actually intersect the eraser stroke.
701 Geom::Affine parent_inverse_transform;
702 if (auto *parent_item = cast<SPItem>(cloned_group->parent)) {
703 parent_inverse_transform = parent_item->i2doc_affine().inverse();
704 }
705 auto const relative_transform = parent_inverse_transform * clone->i2doc_affine();
706 auto const eraser_bounds = _acid->documentExactBounds();
707 if (!eraser_bounds) {
708 return false;
709 }
710 auto const eraser_in_group_coordinates = *eraser_bounds * relative_transform.inverse();
711 bool found_collision = false;
712 for (auto const &orig_child : filtered_children) {
713 if (orig_child.item->collidesWith(eraser_in_group_coordinates)) {
714 found_collision = true;
715 break;
716 }
717 }
718 if (found_collision) {
719 auto *unlinked = cast<SPGroup>(clone->unlink());
720 if (!unlinked) {
721 return false;
722 }
723 std::vector<EraseTarget> unlinked_children;
724 unlinked_children.reserve(filtered_children.size());
725
726 for (auto *child : unlinked->childList(false)) {
727 unlinked_children.emplace_back(cast<SPItem>(child), false);
728 }
729 auto overlapping = _filterCutEraseables(_filterByCollision(unlinked_children, _acid));
730
731 // If the clone was selected, the newly unlinked group should stay selected
732 if (original_target.was_selected && store_survivers) {
733 _survivers.push_back(unlinked);
734 }
735
736 return _performEraseOperation(overlapping, false);
737 } else {
738 if (original_target.was_selected && store_survivers) {
739 _survivers.push_back(original_target.item); // If the clone was selected, it should stay so
740 }
741 if (filtered_children.size() < children.size()) {
742 auto non_eraseable_touched = [&](EraseTarget const &t) -> bool {
743 if (!t.item || !_uncuttableItemType(t.item)) {
744 return false;
745 }
746 return t.item->collidesWith(eraser_in_group_coordinates);
747 };
748 if (std::any_of(children.begin(), children.end(), non_eraseable_touched)) {
749 _setStatusBarMessage(_("Some objects could not be cut."));
750 }
751 }
752 return false;
753 }
754}
755
757EraserTool::Error EraserTool::_uncuttableItemType(SPItem *item)
758{
759 if (!item) {
760 return NON_EXISTENT;
761 } else if (is<SPImage>(item)) {
762 return RASTER_IMAGE;
763 } else if (_isStraightSegment(item)) {
764 return NO_AREA_PATH;
765 } else {
766 return ALL_GOOD;
767 }
768}
769
776bool EraserTool::_booleanErase(EraseTarget target, bool store_survivers)
777{
778 if (!target.item) {
779 return false;
780 }
781 XML::Document *xml_doc = _desktop->doc()->getReprDoc();
782 XML::Node *duplicate_stroke = repr->duplicate(xml_doc);
783 repr->parent()->appendChild(duplicate_stroke);
784 Glib::ustring duplicate_id = duplicate_stroke->attribute("id");
785 GC::release(duplicate_stroke); // parent takes over
786 ObjectSet operands(_desktop);
787 operands.set(duplicate_stroke);
788 if (!nowidth) {
789 operands.pathUnion(true, true);
790 }
791 operands.add(target.item);
792 _handleStrokeStyle(target.item);
793 operands.removeLPESRecursive(true);
794
795
796 if (nowidth) {
797 operands.pathCut(true, true);
798 } else {
799 operands.pathDiff(true, true);
800 }
801 if (auto *spill = _desktop->doc()->getObjectById(duplicate_id)) {
802 operands.remove(spill);
803 spill->deleteObject(false);
804 return false;
805 }
806 if (!_break_apart) {
807 operands.combine(true, true);
808 } else if (!nowidth) {
809 operands.breakApart(true, false, true);
810 }
811 if (store_survivers && target.was_selected) {
812 _survivers.insert(_survivers.end(), operands.items().begin(), operands.items().end());
813 }
814 return true;
815}
816
825bool EraserTool::_performEraseOperation(std::vector<EraseTarget> const &items_to_erase, bool store_survivers)
826{
827 if (mode == EraserToolMode::CUT) {
828 bool erased_something = false;
829 for (auto const &target : items_to_erase) {
830 erased_something = _cutErase(target, store_survivers) || erased_something;
831 }
832 return erased_something;
833 } else if (mode == EraserToolMode::CLIP) {
834 if (nowidth) {
835 return false;
836 }
837 for (auto const &target : items_to_erase) {
838 _clipErase(target.item);
839 }
840 return true;
841 } else { // mode == EraserToolMode::DELETE
842 for (auto const &target : items_to_erase) {
843 if (target.item) {
844 target.item->deleteObject(true);
845 }
846 }
847 return true;
848 }
849}
850
852void EraserTool::_handleStrokeStyle(SPItem *item) const
853{
854 auto *style = item->style;
855 if (style && style->fill_rule.value == SP_WIND_RULE_EVENODD) {
857 sp_repr_css_set_property(css, "fill-rule", "evenodd");
858 sp_desktop_set_style(_desktop, css);
860 css = nullptr;
861 }
862}
863
865void EraserTool::_setStatusBarMessage(char *message)
866{
867 MessageId id = _desktop->messageStack()->flash(WARNING_MESSAGE, message);
868 _our_messages.push_back(id);
869}
870
872void EraserTool::_clearStatusBar()
873{
874 if (!_our_messages.empty()) {
875 auto ms = _desktop->messageStack();
876 for (MessageId id : _our_messages) {
877 ms->cancel(id);
878 }
879 _our_messages.clear();
880 }
881}
882
884void EraserTool::_clipErase(SPItem *item) const
885{
886 Inkscape::ObjectSet w_selection(_desktop);
888 Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc();
889 Inkscape::XML::Node *dup = repr->duplicate(xml_doc);
890 repr->parent()->appendChild(dup);
891 Inkscape::GC::release(dup); // parent takes over
892 w_selection.set(dup);
893 w_selection.pathUnion(true);
894 bool delete_old_clip_path = false;
895 SPClipPath *clip_path = item->getClipObject();
896 if (clip_path) {
897 std::vector<SPItem *> selected;
898 selected.push_back(cast<SPItem>(clip_path->firstChild()));
899 std::vector<Inkscape::XML::Node *> to_select;
900 std::vector<SPItem *> items(selected);
901 sp_item_list_to_curves(items, selected, to_select);
902 Inkscape::XML::Node *clip_data = cast<SPItem>(clip_path->firstChild())->getRepr();
903 if (!clip_data && !to_select.empty()) {
904 clip_data = *(to_select.begin());
905 }
906 if (clip_data) {
907 Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc);
908 if (dup_clip) {
909 auto dup_clip_obj = cast<SPItem>(item->parent->appendChildRepr(dup_clip));
910 Inkscape::GC::release(dup_clip);
911 if (dup_clip_obj) {
912 dup_clip_obj->transform *= item->getRelativeTransform(cast<SPItem>(item->parent));
913 dup_clip_obj->updateRepr();
914 delete_old_clip_path = true;
915 w_selection.raiseToTop(true);
916 w_selection.add(dup_clip);
917 w_selection.pathDiff(true, true);
918 }
919 }
920 }
921 } else {
922 Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect");
923 sp_desktop_apply_style_tool(_desktop, rect_repr, "/tools/eraser", false);
924 auto rect = cast<SPRect>(item->parent->appendChildRepr(rect_repr));
925 Inkscape::GC::release(rect_repr);
926 rect->setPosition(bbox->left(), bbox->top(), bbox->width(), bbox->height());
927 rect->transform = cast<SPItem>(rect->parent)->i2doc_affine().inverse();
928
929 rect->updateRepr();
930 rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
931 w_selection.raiseToTop(true);
932 w_selection.add(rect);
933 w_selection.pathDiff(true, true);
934 }
935 w_selection.raiseToTop(true);
936 w_selection.add(item);
937 w_selection.setMask(true, false, true);
938 if (delete_old_clip_path) {
939 clip_path->deleteObject(true);
940 }
941}
942
945bool EraserTool::_isStraightSegment(SPItem *path)
946{
947 auto as_path = cast<SPPath>(path);
948 if (!as_path) {
949 return false;
950 }
951
952 auto const &curve = as_path->curve();
953 if (!curve) {
954 return false;
955 }
956 auto const &pathvector = curve->get_pathvector();
957
958 // Check if all segments are straight and collinear
959 for (auto const &path : pathvector) {
960 Geom::Point initial_tangent = path.front().unitTangentAt(0.0);
961 for (auto const &segment : path) {
962 if (!segment.isLineSegment()) {
963 return false;
964 } else {
965 Geom::Point dir = segment.unitTangentAt(0.0);
966 if (!Geom::are_near(dir, initial_tangent) && !Geom::are_near(-dir, initial_tangent)) {
967 return false;
968 }
969 }
970 }
971 }
972 return true;
973}
974
975void EraserTool::_addCap(SPCurve &curve, Geom::Point const &pre, Geom::Point const &from, Geom::Point const &to,
976 Geom::Point const &post, double rounding)
977{
978 Geom::Point vel = rounding * Geom::rot90(to - from) / M_SQRT2;
979 double mag = Geom::L2(vel);
980
981 Geom::Point v_in = from - pre;
982 double mag_in = Geom::L2(v_in);
983
984 if (mag_in > epsilon) {
985 v_in = mag * v_in / mag_in;
986 } else {
987 v_in = Geom::Point(0, 0);
988 }
989
990 Geom::Point v_out = to - post;
991 double mag_out = Geom::L2(v_out);
992
993 if (mag_out > epsilon) {
994 v_out = mag * v_out / mag_out;
995 } else {
996 v_out = Geom::Point(0, 0);
997 }
998
999 if (Geom::L2(v_in) > epsilon || Geom::L2(v_out) > epsilon) {
1000 curve.curveto(from + v_in, to + v_out, to);
1001 }
1002}
1003
1004void EraserTool::_accumulate()
1005{
1006 // construct a crude outline of the eraser's path.
1007 // this desperately needs to be rewritten to use the path outliner...
1008 if (!cal1.get_segment_count() || !cal2.get_segment_count()) {
1009 return;
1010 }
1011
1012 auto rev_cal2 = cal2.reversed();
1013
1014 g_assert(!cal1.first_path()->closed());
1015 g_assert(!rev_cal2.first_path()->closed());
1016
1017 Geom::BezierCurve const *dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>(cal1.first_segment());
1018 Geom::BezierCurve const *rev_cal2_firstseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2.first_segment());
1019 Geom::BezierCurve const *dc_cal1_lastseg = dynamic_cast<Geom::BezierCurve const *>(cal1.last_segment());
1020 Geom::BezierCurve const *rev_cal2_lastseg = dynamic_cast<Geom::BezierCurve const *>(rev_cal2.last_segment());
1021
1022 g_assert(dc_cal1_firstseg);
1023 g_assert(rev_cal2_firstseg);
1024 g_assert(dc_cal1_lastseg);
1025 g_assert(rev_cal2_lastseg);
1026
1027 accumulated.append(cal1);
1028 if (!nowidth) {
1029 _addCap(accumulated,
1030 dc_cal1_lastseg->finalPoint() - dc_cal1_lastseg->unitTangentAt(1),
1031 dc_cal1_lastseg->finalPoint(),
1032 rev_cal2_firstseg->initialPoint(),
1033 rev_cal2_firstseg->initialPoint() + rev_cal2_firstseg->unitTangentAt(0),
1034 cap_rounding);
1035
1036 accumulated.append(rev_cal2, true);
1037
1038 _addCap(accumulated,
1039 rev_cal2_lastseg->finalPoint() - rev_cal2_lastseg->unitTangentAt(1),
1040 rev_cal2_lastseg->finalPoint(),
1041 dc_cal1_firstseg->initialPoint(),
1042 dc_cal1_firstseg->initialPoint() + dc_cal1_firstseg->unitTangentAt(0),
1043 cap_rounding);
1044
1045 accumulated.closepath();
1046 }
1047 cal1.reset();
1048 cal2.reset();
1049}
1050
1059std::vector<EraseTarget> EraserTool::_filterCutEraseables(std::vector<EraseTarget> const &items, bool silent)
1060{
1061 std::vector<EraseTarget> result;
1062 result.reserve(items.size());
1063
1064 for (auto &target : items) {
1065 if (Error e = _uncuttableItemType(target.item)) {
1066 if (!silent) {
1067 if (e & RASTER_IMAGE) {
1068 _setStatusBarMessage(_("Cannot cut out from a bitmap, use <b>Clip</b> mode "
1069 "instead."));
1070 } else if (e & NO_AREA_PATH) {
1071 _setStatusBarMessage(_("Cannot cut out from a path with zero area, use "
1072 "<b>Clip</b> mode instead."));
1073 }
1074 }
1075 } else {
1076 result.push_back(target);
1077 }
1078 }
1079 return result;
1080}
1081
1088std::vector<EraseTarget> EraserTool::_filterByCollision(std::vector<EraseTarget> const &items, SPItem *with) const
1089{
1090 std::vector<EraseTarget> result;
1091 if (!with) {
1092 return result;
1093 }
1094 result.reserve(items.size());
1095
1096 if (auto const collision_shape = with->documentExactBounds()) {
1097 for (auto const &target : items) {
1098 if (target.item && target.item->collidesWith(*collision_shape)) {
1099 result.push_back(target);
1100 }
1101 }
1102 }
1103 return result;
1104}
1105
1114std::vector<EraseTarget> EraserTool::_findItemsToErase()
1115{
1116 std::vector<EraseTarget> result;
1117
1118 auto *document = _desktop->getDocument();
1119 auto *selection = _desktop->getSelection();
1120 if (!document || !selection) {
1121 return result;
1122 }
1123
1124 if (mode == EraserToolMode::DELETE) {
1125 // In DELETE mode, the classification is based on having been touched by the mouse cursor:
1126 // * result should contain touched items;
1127 // * _survivers should contain selected but untouched items.
1128 auto *r = Rubberband::get(_desktop);
1129 std::vector<SPItem *> touched = document->getItemsAtPoints(_desktop->dkey, r->getPoints());
1130 if (selection->isEmpty()) {
1131 for (auto *item : touched) {
1132 result.emplace_back(item, false);
1133 }
1134 } else {
1135 for (auto *item : selection->items()) {
1136 if (std::find(touched.begin(), touched.end(), item) == touched.end()) {
1137 _survivers.push_back(item);
1138 } else {
1139 result.emplace_back(item, true);
1140 }
1141 }
1142 }
1143 } else {
1144 // In the other modes, we start with a crude filtering step based on bounding boxes
1145 _acid = _insertAcidIntoDocument(document);
1146 if (!_acid) {
1147 return result;
1148 }
1149 Geom::OptRect eraser_bbox = _acid->documentVisualBounds();
1150 if (!eraser_bbox) {
1151 return result;
1152 }
1153 std::vector<SPItem *> candidates = document->getItemsPartiallyInBox(_desktop->dkey, *eraser_bbox,
1154 false, false, false, true);
1155 std::vector<EraseTarget> allowed;
1156 allowed.reserve(candidates.size());
1157
1158 // If selection is empty, we're allowed to erase all items except the eraser stroke itself.
1159 if (selection->isEmpty()) {
1160 for (auto *candidate : candidates) {
1161 if (candidate != _acid) {
1162 allowed.emplace_back(candidate, false);
1163 }
1164 }
1165 } // How we handle non-empty selection further depends on the mode.
1166
1167 if (mode == EraserToolMode::CUT) {
1168 // In CUT mode, we must unpack groups, since the boolean difference/cut operation
1169 // doesn't make sense for a group.
1170 for (auto *selected : selection->items()) {
1171 bool included_for_erase = false;
1172 for (auto *candidate : candidates) {
1173 if (selected == candidate || selected->isAncestorOf(candidate)) {
1174 allowed.emplace_back(candidate, selection->includes(candidate));
1175 included_for_erase = (candidate == selected) || included_for_erase;
1176 }
1177 }
1178 if (!included_for_erase) {
1179 _survivers.push_back(selected);
1180 }
1181 }
1182 // The filtering is based on a precise collision detection procedure:
1183 // * result will contain all eraseable items that overlap with the eraser stroke;
1184 // * _survivers will contain all selected items that were rejected during this filtering.
1185 auto overlapping = _filterByCollision(allowed, _acid);
1186 auto valid = _filterCutEraseables(overlapping); // Sets status bar messages
1187
1188 for (auto const &element : allowed) {
1189 if (element.item && element.was_selected &&
1190 std::find(valid.begin(), valid.end(), element) == valid.end())
1191 {
1192 _survivers.push_back(element.item);
1193 }
1194 }
1195 result.insert(result.end(), valid.begin(), valid.end());
1196
1197 } else if (mode == EraserToolMode::CLIP) {
1198 // In CLIP mode, we don't check descendants, because clip can be set to an entire group.
1199 auto const all_selected = selection->items();
1200 for (auto *item : all_selected) {
1201 allowed.emplace_back(item, true);
1202 }
1203
1204 // The classification is also based on the precise collision detection:
1205 // * result will contain all items that overlap with the eraser stroke;
1206 // * _survivers will contain all selected items, since CLIP mode is always non-destructive.
1207 auto overlapping = _filterByCollision(allowed, _acid);
1208 result.insert(result.end(), overlapping.begin(), overlapping.end());
1209 _survivers.insert(_survivers.end(), all_selected.begin(), all_selected.end());
1210 }
1211 }
1212 return result;
1213}
1214
1215void EraserTool::_fitAndSplit(bool releasing)
1216{
1217 double const tolerance_sq = square(_desktop->w2d().descrim() * tolerance);
1218 nowidth = (width == 0); // setting width is managed by the base class
1219
1220#ifdef ERASER_VERBOSE
1221 g_print("[F&S:R=%c]", releasing ? 'T' : 'F');
1222#endif
1223 if (npoints >= SAMPLING_SIZE || npoints <= 0) {
1224 return; // just clicked
1225 }
1226
1227 if (npoints == SAMPLING_SIZE - 1 || releasing) {
1228 _completeBezier(tolerance_sq, releasing);
1229
1230#ifdef ERASER_VERBOSE
1231 g_print("[%d]Yup\n", npoints);
1232#endif
1233 if (!releasing) {
1234 _fitDrawLastPoint();
1235 }
1236
1237 // Copy last point
1238 point1[0] = point1[npoints - 1];
1239 point2[0] = point2[npoints - 1];
1240 npoints = 1;
1241 } else {
1242 _drawTemporaryBox();
1243 }
1244}
1245
1246void EraserTool::_completeBezier(double tolerance_sq, bool releasing)
1247{
1248 /* Current eraser */
1249 if (cal1.is_empty() || cal2.is_empty()) {
1250 /* dc->npoints > 0 */
1251 cal1.reset();
1252 cal2.reset();
1253
1254 cal1.moveto(point1[0]);
1255 cal2.moveto(point2[0]);
1256 }
1257#ifdef ERASER_VERBOSE
1258 g_print("[F&S:#] npoints:%d, releasing:%s\n", npoints, releasing ? "TRUE" : "FALSE");
1259#endif
1260
1261 unsigned const bezier_size = 4;
1262 unsigned const max_beziers = 8;
1263 size_t const bezier_max_length = bezier_size * max_beziers;
1264
1265 Geom::Point b1[bezier_max_length];
1266 gint const nb1 = Geom::bezier_fit_cubic_r(b1, point1, npoints, tolerance_sq, max_beziers);
1267 g_assert(nb1 * bezier_size <= gint(G_N_ELEMENTS(b1)));
1268
1269 Geom::Point b2[bezier_max_length];
1270 gint const nb2 = Geom::bezier_fit_cubic_r(b2, point2, npoints, tolerance_sq, max_beziers);
1271 g_assert(nb2 * bezier_size <= gint(G_N_ELEMENTS(b2)));
1272
1273 if (nb1 == -1 || nb2 == -1) {
1274 _failedBezierFallback(); // TODO: do we ever need this?
1275 return;
1276 }
1277
1278 /* Fit and draw and reset state */
1279#ifdef ERASER_VERBOSE
1280 g_print("nb1:%d nb2:%d\n", nb1, nb2);
1281#endif
1282
1283 /* CanvasShape */
1284 if (!releasing) {
1285 currentcurve.reset();
1286 currentcurve.moveto(b1[0]);
1287
1288 for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
1289 currentcurve.curveto(bp1[1], bp1[2], bp1[3]);
1290 }
1291
1292 currentcurve.lineto(b2[bezier_size * (nb2 - 1) + 3]);
1293
1294 for (Geom::Point *bp2 = b2 + bezier_size * (nb2 - 1); bp2 >= b2; bp2 -= bezier_size) {
1295 currentcurve.curveto(bp2[2], bp2[1], bp2[0]);
1296 }
1297
1298 // FIXME: segments is always NULL at this point??
1299 if (segments.empty()) { // first segment
1300 _addCap(currentcurve, b2[1], b2[0], b1[0], b1[1], cap_rounding);
1301 }
1302
1303 currentcurve.closepath();
1304 currentshape->set_bpath(&currentcurve, true);
1305 }
1306
1307 /* Current eraser */
1308 for (Geom::Point *bp1 = b1; bp1 < b1 + bezier_size * nb1; bp1 += bezier_size) {
1309 cal1.curveto(bp1[1], bp1[2], bp1[3]);
1310 }
1311
1312 for (Geom::Point *bp2 = b2; bp2 < b2 + bezier_size * nb2; bp2 += bezier_size) {
1313 cal2.curveto(bp2[1], bp2[2], bp2[3]);
1314 }
1315}
1316
1317void EraserTool::_failedBezierFallback()
1318{
1319 /* fixme: ??? */
1320#ifdef ERASER_VERBOSE
1321 g_print("[_failedBezierFallback] - failed to fit cubic.\n");
1322#endif
1323 _drawTemporaryBox();
1324
1325 for (gint i = 1; i < npoints; i++) {
1326 cal1.lineto(point1[i]);
1327 }
1328
1329 for (gint i = 1; i < npoints; i++) {
1330 cal2.lineto(point2[i]);
1331 }
1332}
1333
1334void EraserTool::_fitDrawLastPoint()
1335{
1336 g_assert(!currentcurve.is_empty());
1337
1338 auto fillColor = sp_desktop_get_color_tool(_desktop, "/tools/eraser", true);
1339 double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/eraser");
1340 double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/eraser", true);
1341
1342 // TODO This removes color space information from the color
1343 auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true);
1344 cbp->set_fill(fillColor ? fillColor->toRGBA(opacity * fillOpacity) : 0x0, trace_wind_rule);
1345 cbp->set_stroke(0x0);
1346
1347 /* fixme: Cannot we cascade it to root more clearly? */
1348 cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop));
1349 segments.emplace_back(cbp);
1350
1351 if (mode == EraserToolMode::DELETE) {
1352 cbp->set_visible(false);
1353 currentshape->set_visible(false);
1354 }
1355}
1356
1357void EraserTool::_drawTemporaryBox()
1358{
1359 currentcurve.reset();
1360
1361 currentcurve.moveto(point1[npoints - 1]);
1362
1363 for (gint i = npoints - 2; i >= 0; i--) {
1364 currentcurve.lineto(point1[i]);
1365 }
1366
1367 for (gint i = 0; i < npoints; i++) {
1368 currentcurve.lineto(point2[i]);
1369 }
1370
1371 if (npoints >= 2) {
1372 _addCap(currentcurve,
1373 point2[npoints - 2], point2[npoints - 1],
1374 point1[npoints - 1], point1[npoints - 2], cap_rounding);
1375 }
1376
1377 currentcurve.closepath();
1378 currentshape->set_bpath(&currentcurve, true);
1379}
1380
1381} // namespace Inkscape::UI::Tools
1382
1383/*
1384 Local Variables:
1385 mode:c++
1386 c-file-style:"stroustrup"
1387 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1388 indent-tabs-mode:nil
1389 fill-column:99
1390 End:
1391*/
1392// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Bezier fitting algorithms.
3x3 matrix representing an affine transformation.
Definition affine.h:70
Affine inverse() const
Compute the inverse matrix.
Definition affine.cpp:388
Two-dimensional Bezier curve of arbitrary order.
Point finalPoint() const override
Retrieve the end of the curve.
Point initialPoint() const override
Retrieve the start of the curve.
virtual Point unitTangentAt(Coord t, unsigned n=3) const
Compute a vector tangent to the curve.
Definition curve.cpp:201
Axis-aligned rectangle that can be empty.
Definition rect.h:203
Sequence of subpaths.
Definition pathvector.h:122
Two-dimensional point that doubles as a vector.
Definition point.h:66
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
static void cancel(SPDocument *document)
bool remove(SPObject *object)
Removes an item from the set of selected objects.
void breakApart(bool skip_undo=false, bool overlapping=true, bool silent=false)
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
void pathUnion(bool skip_undo=false, bool silent=false)
void removeLPESRecursive(bool keep_paths)
void pathCut(bool skip_undo=false, bool silent=false)
bool add(SPObject *object, bool nosignal=false)
Add an SPObject to the set of selected objects.
void raiseToTop(bool skip_undo=false)
void pathDiff(bool skip_undo=false, bool silent=false)
void clear()
Unselects all selected objects.
bool isEmpty()
Returns true if no items are selected.
void set(SPObject *object, bool persist_selection_context=false)
Set the selection to a single specific object.
void combine(bool skip_undo=false, bool silent=false)
void setMask(bool apply_clip_path, bool apply_to_layer, bool remove_original)
Creates a mask or clipPath from selection.
std::function< void()> action
The action to perform when the value changes, if any.
static Rubberband * get(SPDesktop *desktop)
void move(Geom::Point const &p)
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
void add(XML::Node *repr)
Add an XML node's SPObject to the set of selected objects.
Definition selection.h:107
Geom::Point getViewPoint(Geom::Point const &n) const
Geom::Point getNormalizedPoint(Geom::Point const &v) const
bool abs_width
uses absolute width independent of zoom
Geom::Point point1[SAMPLING_SIZE]
left edge points for this segment
SPCurve accumulated
accumulated shape which ultimately goes in svg:path
CanvasItemPtr< CanvasItemBpath > currentshape
canvas item for red "leading" segment
std::vector< CanvasItemPtr< CanvasItemBpath > > segments
canvas items for "committed" segments
int npoints
number of edge points for this segment
Geom::Point point2[SAMPLING_SIZE]
right edge points for this segment
static constexpr double epsilon
Definition eraser-tool.h:76
bool root_handler(CanvasEvent const &event) final
static constexpr double default_pressure
Definition eraser-tool.h:86
EraserTool(SPDesktop *desktop)
static constexpr double max_tilt
Definition eraser-tool.h:89
bool _apply(Geom::Point const &p)
static constexpr double max_pressure
Definition eraser-tool.h:85
static constexpr double default_tilt
Definition eraser-tool.h:90
static constexpr uint32_t trace_color_rgba
Definition eraser-tool.h:71
static constexpr SPWindRule trace_wind_rule
Definition eraser-tool.h:72
static constexpr double min_pressure
Definition eraser-tool.h:84
static void _generateNormalDist2(double &r1, double &r2)
static constexpr double min_tilt
Definition eraser-tool.h:88
void _updateMode()
Reads the current Eraser mode from Preferences and sets mode accordingly.
void _extinput(ExtendedInput const &ext)
static constexpr double epsilon_start
Definition eraser-tool.h:77
static constexpr double vel_start
Definition eraser-tool.h:78
void ungrabCanvasEvents()
Ungrab events from the Canvas Catchall.
void grabCanvasEvents(EventMask mask=EventType::KEY_PRESS|EventType::BUTTON_RELEASE|EventType::MOTION|EventType::BUTTON_PRESS)
Grab events from the Canvas Catchall.
bool dragging
are we dragging?
Definition tool-base.h:146
virtual bool root_handler(CanvasEvent const &event)
void enableSelectionCue(bool enable=true)
Enables/disables the ToolBase's SelCue.
Interface for refcounted XML nodes.
Definition node.h:80
virtual Node * duplicate(Document *doc) const =0
Create a duplicate of this node.
virtual char const * attribute(char const *key) const =0
Get the string representation of a node's attribute.
Wrapper around a Geom::PathVector object.
Definition curve.h:26
void reset()
Set curve to empty curve.
Definition curve.cpp:118
To do: update description of desktop.
Definition desktop.h:149
double current_zoom() const
Definition desktop.h:335
Inkscape::CanvasItemGroup * getCanvasSketch() const
Definition desktop.h:201
Geom::Affine const & w2d() const
Transformation from window to desktop coordinates (zoom/rotate).
Definition desktop.h:416
Typed SVG document implementation.
Definition document.h:103
Inkscape::XML::Document * getReprDoc()
Our Inkscape::XML::Document.
Definition document.h:213
SPObject * getObjectByRepr(Inkscape::XML::Node *repr) const
int getItemCount() const
Base class for visual SVG elements.
Definition sp-item.h:109
virtual std::optional< Geom::PathVector > documentExactBounds() const
Get an exact geometric shape representing the visual bounds of the item in the document coordinates.
Definition sp-item.cpp:1042
Geom::OptRect documentVisualBounds() const
Get item's visual bbox in document coordinate system.
Definition sp-item.cpp:1025
Geom::Affine getRelativeTransform(SPObject const *obj) const
Definition sp-item.cpp:1819
SPClipPath * getClipObject() const
Definition sp-item.cpp:102
std::vector< SPObject * > childList(bool add_ref, Action action=ActionGeneral)
Retrieves the children as a std vector object, optionally ref'ing the children in the process,...
SPObject * firstChild()
Definition sp-object.h:315
SPStyle * style
Represents the style properties, whether from presentation attributes, the style attribute,...
Definition sp-object.h:248
SPObject * parent
Definition sp-object.h:189
void deleteObject(bool propagate, bool propagate_descendants)
Deletes an object, unparenting it from its parent.
SPObject * appendChildRepr(Inkscape::XML::Node *repr)
Append repr as child of this object.
Definition sp-use.h:25
const double w
Definition conic-4.cpp:19
std::shared_ptr< Css const > css
Css & result
bool sp_desktop_root_handler(Inkscape::CanvasEvent const &event, SPDesktop *desktop)
void sp_desktop_apply_style_tool(SPDesktop *desktop, Inkscape::XML::Node *repr, Glib::ustring const &tool_path, bool with_text)
Apply the desktop's current style or the tool style to repr.
double sp_desktop_get_master_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool *has_opacity)
std::optional< Color > sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill)
double sp_desktop_get_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill)
void sp_desktop_set_style(SPDesktop *desktop, SPCSSAttr *css, bool change, bool write_current, bool switch_style)
Apply style on selection on desktop.
Editable view implementation.
TODO: insert short description here.
constexpr int SAMPLING_SIZE
Macro for icon names used in Inkscape.
SPItem * item
std::string original
Interface for locally managing a current status message.
Raw stack of active status messages.
double atan2(Point const &p)
bool is_zero(Point const &p)
int bezier_fit_cubic_r(Point bezier[], Point const data[], int len, double error, unsigned max_beziers)
Fit a multi-segment Bezier curve to a set of digitized points, with possible weedout of identical poi...
SBasis L2(D2< SBasis > const &a, unsigned k)
Definition d2-sbasis.cpp:42
bool are_near(Affine const &a1, Affine const &a2, Coord eps=EPSILON)
D2< T > rot90(D2< T > const &a)
Definition d2.h:397
Point middle_point(LineSegment const &_segment)
static R & release(R &r)
Decrements the reference count of a anchored object.
double square(double const x)
static constexpr auto DEFAULT_ERASER_MODE
Definition eraser-tool.h:43
unsigned get_latin_keyval(GtkEventControllerKey const *const controller, unsigned const keyval, unsigned const keycode, GdkModifierType const state, unsigned *consumed_modifiers)
Return the keyval corresponding to the event controller key in Latin group.
void sp_event_context_read(ToolBase *tool, char const *key)
Calls virtual set() function of ToolBase.
void inspect_event(E &&event, Fs... funcs)
Perform pattern-matching on a CanvasEvent.
std::uint_least32_t MessageId
An integer ID which identifies a displayed message in a particular Inkscape::MessageStack.
Definition message.h:39
@ RUBBERBAND_TOUCHPATH_ERASER
@ NORMAL_MESSAGE
Definition message.h:26
@ WARNING_MESSAGE
Definition message.h:28
static cairo_user_data_key_t key
int mode
bool sp_item_list_to_curves(const std::vector< SPItem * > &items, std::vector< SPItem * > &selected, std::vector< Inkscape::XML::Node * > &to_select, bool skip_all_lpeitems)
PathVector - a sequence of subpaths.
Singleton class to access the preferences file in a convenient way.
Piecewise< SBasis > log(Interval in)
Definition pw-funcs.cpp:37
Ocnode * child[8]
Definition quantize.cpp:33
SPCSSAttr * sp_repr_css_attr_new()
Creates an empty SPCSSAttr (a class for manipulating CSS style properties).
Definition repr-css.cpp:67
void sp_repr_css_attr_unref(SPCSSAttr *css)
Unreferences an SPCSSAttr (will be garbage collected if no references remain).
Definition repr-css.cpp:76
void sp_repr_css_set_property(SPCSSAttr *css, gchar const *name, gchar const *value)
Set a style property to a new value (e.g.
Definition repr-css.cpp:191
void sp_repr_unparent(Inkscape::XML::Node *repr)
Remove repr from children of its parent node.
Definition repr.h:107
GList * items
SVG <image> implementation.
A mouse button (left/right/middle) is pressed.
Abstract base class for events.
Extended input data associated to events generated by graphics tablets.
std::optional< double > ytilt
std::optional< double > xtilt
std::optional< double > pressure
A key has been pressed.
Movement of the mouse pointer.
Represents an item to erase.
Definition eraser-tool.h:47
bool was_selected
Whether the item was part of selection.
Definition eraser-tool.h:49
SPItem * item
Pointer to the item to be erased.
Definition eraser-tool.h:48
Interface for XML documents.
Definition document.h:43
virtual Node * createElement(char const *name)=0
Definition curve.h:24
@ SP_WIND_RULE_EVENODD
Definition style-enums.h:26
SPStyle - a style object for SPItem objects.
static void sp_svg_write_path(Inkscape::SVG::PathString &str, Geom::Path const &p, bool normalize=false)
Definition svg-path.cpp:109
SPDesktop * desktop
static double square(double const x)
double width