// SPDX-License-Identifier: GPL-2.0-or-later /* * Spray Tool * * Authors: * Pierre-Antoine MARC * Pierre CACLIN * Aurel-Aimé MARMION * Julien LERAY * Benoît LAVORATA * Vincent MONTAGNE * Pierre BARBRY-BLOT * Steren GIANNINI (steren.giannini@gmail.com) * Jon A. Cruz * Abhishek Sharma * Jabiertxo Arraiza * Adrian Boguszewski * * Copyright (C) 2009 authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include #include #include #include <2geom/circle.h> #include "context-fns.h" #include "desktop-events.h" #include "desktop-style.h" #include "desktop.h" #include "document-undo.h" #include "document.h" #include "filter-chemistry.h" #include "inkscape.h" #include "include/macros.h" #include "message-context.h" #include "path-chemistry.h" #include "selection.h" #include "display/cairo-utils.h" #include "display/curve.h" #include "display/drawing-context.h" #include "display/drawing.h" #include "display/control/canvas-item-bpath.h" #include "display/control/canvas-item-drawing.h" #include "object/box3d.h" #include "object/sp-use.h" #include "object/sp-item-transform.h" #include "svg/svg.h" #include "svg/svg-color.h" #include "ui/icon-names.h" #include "ui/toolbar/spray-toolbar.h" #include "ui/tools/spray-tool.h" #include "ui/widget/canvas.h" using Inkscape::DocumentUndo; #define DDC_RED_RGBA 0xff0000ff #define DYNA_MIN_WIDTH 1.0e-6 // Disabled in 0.91 because of Bug #1274831 (crash, spraying an object // with the mode: spray object in single path) // Please enable again when working on 1.0 #define ENABLE_SPRAY_MODE_SINGLE_PATH namespace Inkscape { namespace UI { namespace Tools { enum { PICK_COLOR, PICK_OPACITY, PICK_R, PICK_G, PICK_B, PICK_H, PICK_S, PICK_L }; /** * This function returns pseudo-random numbers from a normal distribution * @param mu : mean * @param sigma : standard deviation ( > 0 ) */ inline double NormalDistribution(double mu, double sigma) { // use Box Muller's algorithm return mu + sigma * sqrt( -2.0 * log(g_random_double_range(0, 1)) ) * cos( 2.0*M_PI*g_random_double_range(0, 1) ); } /* Method to rotate items */ static void sp_spray_rotate_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Rotate const &rotation) { Geom::Translate const s(c); Geom::Affine affine = s.inverse() * rotation * s; // Rotate item. item->set_i2d_affine(item->i2dt_affine() * affine); // Use each item's own transform writer, consistent with sp_selection_apply_affine() item->doWriteTransform(item->transform); // Restore the center position (it's changed because the bbox center changed) if (item->isCenterSet()) { item->setCenter(c); item->updateRepr(); } } /* Method to scale items */ static void sp_spray_scale_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Scale const &scale) { Geom::Translate const s(c); item->set_i2d_affine(item->i2dt_affine() * s.inverse() * scale * s); item->doWriteTransform(item->transform); } SprayTool::SprayTool(SPDesktop *desktop) : ToolBase(desktop, "/tools/spray", "spray.svg", false) , pressure(TC_DEFAULT_PRESSURE) , dragging(false) , usepressurewidth(false) , usepressurepopulation(false) , usepressurescale(false) , usetilt(false) , usetext(false) , width(0.2) , ratio(0) , tilt(0) , rotation_variation(0) , population(0) , scale_variation(1) , scale(1) , mean(0.2) , standard_deviation(0.2) , distrib(1) , mode(0) , is_drawing(false) , is_dilating(false) , has_dilated(false) , no_overlap(false) , picker(false) , pick_center(true) , pick_inverse_value(false) , pick_fill(false) , pick_stroke(false) , pick_no_overlap(false) , over_transparent(true) , over_no_transparent(true) , offset(0) , pick(0) , do_trace(false) , pick_to_size(false) , pick_to_presence(false) , pick_to_color(false) , pick_to_opacity(false) , invert_picked(false) , gamma_picked(0) , rand_picked(0) { dilate_area = make_canvasitem(desktop->getCanvasControls()); dilate_area->set_stroke(0xff9900ff); dilate_area->set_fill(0x0, SP_WIND_RULE_EVENODD); dilate_area->hide(); this->is_drawing = false; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setBool("/dialogs/clonetiler/dotrace", false); if (prefs->getBool("/tools/spray/selcue")) { this->enableSelectionCue(); } if (prefs->getBool("/tools/spray/gradientdrag")) { this->enableGrDrag(); } desktop->getSelection()->setBackup(); sp_event_context_read(this, "distrib"); sp_event_context_read(this, "width"); sp_event_context_read(this, "ratio"); sp_event_context_read(this, "tilt"); sp_event_context_read(this, "rotation_variation"); sp_event_context_read(this, "scale_variation"); sp_event_context_read(this, "mode"); sp_event_context_read(this, "population"); sp_event_context_read(this, "mean"); sp_event_context_read(this, "standard_deviation"); sp_event_context_read(this, "usepressurewidth"); sp_event_context_read(this, "usepressurepopulation"); sp_event_context_read(this, "usepressurescale"); sp_event_context_read(this, "Scale"); sp_event_context_read(this, "offset"); sp_event_context_read(this, "picker"); sp_event_context_read(this, "pick_center"); sp_event_context_read(this, "pick_inverse_value"); sp_event_context_read(this, "pick_fill"); sp_event_context_read(this, "pick_stroke"); sp_event_context_read(this, "pick_no_overlap"); sp_event_context_read(this, "over_no_transparent"); sp_event_context_read(this, "over_transparent"); sp_event_context_read(this, "no_overlap"); } SprayTool::~SprayTool() { if (!object_set.isEmpty()) { object_set.clear(); } _desktop->getSelection()->restoreBackup(); this->enableGrDrag(false); this->style_set_connection.disconnect(); } void SprayTool::update_cursor(bool /*with_shift*/) { guint num = 0; gchar *sel_message = nullptr; if (!_desktop->getSelection()->isEmpty()) { num = (guint)boost::distance(_desktop->getSelection()->items()); sel_message = g_strdup_printf(ngettext("%i object selected","%i objects selected",num), num); } else { sel_message = g_strdup_printf("%s", _("Nothing selected")); } switch (this->mode) { case SPRAY_MODE_COPY: this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray copies of the initial selection."), sel_message); break; case SPRAY_MODE_CLONE: this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray clones of the initial selection."), sel_message); break; case SPRAY_MODE_SINGLE_PATH: this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray in a single path of the initial selection."), sel_message); break; default: break; } g_free(sel_message); } void SprayTool::setCloneTilerPrefs() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); this->do_trace = prefs->getBool("/dialogs/clonetiler/dotrace", false); this->pick = prefs->getInt("/dialogs/clonetiler/pick"); this->pick_to_size = prefs->getBool("/dialogs/clonetiler/pick_to_size", false); this->pick_to_presence = prefs->getBool("/dialogs/clonetiler/pick_to_presence", false); this->pick_to_color = prefs->getBool("/dialogs/clonetiler/pick_to_color", false); this->pick_to_opacity = prefs->getBool("/dialogs/clonetiler/pick_to_opacity", false); this->rand_picked = 0.01 * prefs->getDoubleLimited("/dialogs/clonetiler/rand_picked", 0, 0, 100); this->invert_picked = prefs->getBool("/dialogs/clonetiler/invert_picked", false); this->gamma_picked = prefs->getDoubleLimited("/dialogs/clonetiler/gamma_picked", 0, -10, 10); } void SprayTool::set(const Inkscape::Preferences::Entry& val) { Glib::ustring path = val.getEntryName(); if (path == "mode") { this->mode = val.getInt(); this->update_cursor(false); } else if (path == "width") { this->width = 0.01 * CLAMP(val.getInt(10), 1, 100); } else if (path == "usepressurewidth") { this->usepressurewidth = val.getBool(); } else if (path == "usepressurepopulation") { this->usepressurepopulation = val.getBool(); } else if (path == "usepressurescale") { this->usepressurescale = val.getBool(); } else if (path == "population") { this->population = 0.01 * CLAMP(val.getInt(10), 1, 100); } else if (path == "rotation_variation") { this->rotation_variation = CLAMP(val.getDouble(0.0), 0, 100.0); } else if (path == "scale_variation") { this->scale_variation = CLAMP(val.getDouble(1.0), 0, 100.0); } else if (path == "standard_deviation") { this->standard_deviation = 0.01 * CLAMP(val.getInt(10), 1, 100); } else if (path == "mean") { this->mean = 0.01 * CLAMP(val.getInt(10), 1, 100); // Not implemented in the toolbar and preferences yet } else if (path == "distribution") { this->distrib = val.getInt(1); } else if (path == "tilt") { this->tilt = CLAMP(val.getDouble(0.1), 0, 1000.0); } else if (path == "ratio") { this->ratio = CLAMP(val.getDouble(), 0.0, 0.9); } else if (path == "offset") { this->offset = val.getDoubleLimited(100.0, 0, 1000.0); } else if (path == "pick_center") { this->pick_center = val.getBool(true); } else if (path == "pick_inverse_value") { this->pick_inverse_value = val.getBool(false); } else if (path == "pick_fill") { this->pick_fill = val.getBool(false); } else if (path == "pick_stroke") { this->pick_stroke = val.getBool(false); } else if (path == "pick_no_overlap") { this->pick_no_overlap = val.getBool(false); } else if (path == "over_no_transparent") { this->over_no_transparent = val.getBool(true); } else if (path == "over_transparent") { this->over_transparent = val.getBool(true); } else if (path == "no_overlap") { this->no_overlap = val.getBool(false); } else if (path == "picker") { this->picker = val.getBool(false); } } static void sp_spray_extinput(SprayTool *tc, GdkEvent *event) { if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &tc->pressure)) { tc->pressure = CLAMP(tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE); } else { tc->pressure = TC_DEFAULT_PRESSURE; } } static double get_width(SprayTool *tc) { double pressure = (tc->usepressurewidth? tc->pressure / TC_DEFAULT_PRESSURE : 1); return pressure * tc->width; } static double get_dilate_radius(SprayTool *tc) { return 250 * get_width(tc)/tc->getDesktop()->current_zoom(); } static double get_path_mean(SprayTool *tc) { return tc->mean; } static double get_path_standard_deviation(SprayTool *tc) { return tc->standard_deviation; } static double get_population(SprayTool *tc) { double pressure = (tc->usepressurepopulation? tc->pressure / TC_DEFAULT_PRESSURE : 1); return pressure * tc->population; } static double get_pressure(SprayTool *tc) { double pressure = tc->pressure / TC_DEFAULT_PRESSURE; return pressure; } static double get_move_mean(SprayTool *tc) { return tc->mean; } static double get_move_standard_deviation(SprayTool *tc) { return tc->standard_deviation; } /** * Method to handle the distribution of the items * @param[out] radius : radius of the position of the sprayed object * @param[out] angle : angle of the position of the sprayed object * @param[in] a : mean * @param[in] s : standard deviation * @param[in] choice : */ static void random_position(double &radius, double &angle, double &a, double &s, int /*choice*/) { // angle is taken from an uniform distribution angle = g_random_double_range(0, M_PI*2.0); // radius is taken from a Normal Distribution double radius_temp =-1; while(!((radius_temp >= 0) && (radius_temp <=1 ))) { radius_temp = NormalDistribution(a, s); } // Because we are in polar coordinates, a special treatment has to be done to the radius. // Otherwise, positions taken from an uniform repartition on radius and angle will not seam to // be uniformily distributed on the disk (more at the center and less at the boundary). // We counter this effect with a 0.5 exponent. This is empiric. radius = pow(radius_temp, 0.5); } static void sp_spray_transform_path(SPItem * item, Geom::Path &path, Geom::Affine affine, Geom::Point center){ path *= i2anc_affine(static_cast(item->parent), nullptr).inverse(); path *= item->transform.inverse(); Geom::Affine dt2p; if (item->parent) { dt2p = static_cast(item->parent)->i2dt_affine().inverse(); } else { dt2p = item->document->dt2doc(); } Geom::Affine i2dt = item->i2dt_affine() * Geom::Translate(center).inverse() * affine * Geom::Translate(center); path *= i2dt * dt2p; path *= i2anc_affine(static_cast(item->parent), nullptr); } /** Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same probability of being displaced. */ double randomize01(double val, double rand) { double base = MIN (val - rand, 1 - 2*rand); if (base < 0) { base = 0; } val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... } static guint32 getPickerData(Geom::IntRect area, SPDesktop *desktop) { Inkscape::CanvasItemDrawing *canvas_item_drawing = desktop->getCanvasDrawing(); Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); // Get average color. double R, G, B, A; drawing->averageColor(area, R, G, B, A); //this can fix the bug #1511998 if confirmed if ( A < 1e-6) { R = 1.0; G = 1.0; B = 1.0; } return SP_RGBA32_F_COMPOSE(R, G, B, A); } static void showHidden(std::vector items_down){ for (auto item_hidden : items_down) { item_hidden->setHidden(false); item_hidden->updateRepr(); } } //todo: maybe move same parameter to preferences static bool fit_item(SPDesktop *desktop, SPItem *item, Geom::OptRect bbox, Geom::Point &move, Geom::Point center, gint mode, double angle, double &_scale, double scale, bool picker, bool pick_center, bool pick_inverse_value, bool pick_fill, bool pick_stroke, bool pick_no_overlap, bool over_no_transparent, bool over_transparent, bool no_overlap, double offset, SPCSSAttr *css, bool trace_scale, int pick, bool do_trace, bool pick_to_size, bool pick_to_presence, bool pick_to_color, bool pick_to_opacity, bool invert_picked, double gamma_picked , double rand_picked) { SPDocument *doc = item->document; double width = bbox->width(); double height = bbox->height(); double offset_width = (offset * width)/100.0 - (width); if(offset_width < 0 ){ offset_width = 0; } double offset_height = (offset * height)/100.0 - (height); if(offset_height < 0 ){ offset_height = 0; } if(picker && pick_to_size && !trace_scale && do_trace){ _scale = 0.1; } Geom::OptRect bbox_procesed = Geom::Rect(Geom::Point(bbox->left() - offset_width, bbox->top() - offset_height),Geom::Point(bbox->right() + offset_width, bbox->bottom() + offset_height)); Geom::Path path; path.start(Geom::Point(bbox_procesed->left(), bbox_procesed->top())); path.appendNew(Geom::Point(bbox_procesed->right(), bbox_procesed->top())); path.appendNew(Geom::Point(bbox_procesed->right(), bbox_procesed->bottom())); path.appendNew(Geom::Point(bbox_procesed->left(), bbox_procesed->bottom())); path.close(true); sp_spray_transform_path(item, path, Geom::Scale(_scale), center); sp_spray_transform_path(item, path, Geom::Scale(scale), center); sp_spray_transform_path(item, path, Geom::Rotate(angle), center); path *= Geom::Translate(move); path *= desktop->doc2dt(); bbox_procesed = path.boundsFast(); double bbox_left_main = bbox_procesed->left(); double bbox_right_main = bbox_procesed->right(); double bbox_top_main = bbox_procesed->top(); double bbox_bottom_main = bbox_procesed->bottom(); double width_transformed = bbox_procesed->width(); double height_transformed = bbox_procesed->height(); Geom::Point mid_point = desktop->d2w(bbox_procesed->midpoint()); Geom::IntRect area = Geom::IntRect::from_xywh(floor(mid_point[Geom::X]), floor(mid_point[Geom::Y]), 1, 1); guint32 rgba = getPickerData(area, desktop); guint32 rgba2 = 0xffffff00; Geom::Rect rect_sprayed(desktop->d2w(Geom::Point(bbox_left_main,bbox_top_main)), desktop->d2w(Geom::Point(bbox_right_main,bbox_bottom_main))); if (!rect_sprayed.hasZeroArea()) { rgba2 = getPickerData(rect_sprayed.roundOutwards(), desktop); } if(pick_no_overlap) { if(rgba != rgba2) { if(mode != SPRAY_MODE_ERASER) { return false; } } } if(!pick_center) { rgba = rgba2; } if(!over_transparent && (SP_RGBA32_A_F(rgba) == 0 || SP_RGBA32_A_F(rgba) < 1e-6)) { if(mode != SPRAY_MODE_ERASER) { return false; } } if(!over_no_transparent && SP_RGBA32_A_F(rgba) > 0) { if(mode != SPRAY_MODE_ERASER) { return false; } } if(offset < 100 ) { offset_width = ((99.0 - offset) * width_transformed)/100.0 - width_transformed; offset_height = ((99.0 - offset) * height_transformed)/100.0 - height_transformed; } else { offset_width = 0; offset_height = 0; } std::vector items_down = desktop->getDocument()->getItemsPartiallyInBox(desktop->dkey, *bbox_procesed); Inkscape::Selection *selection = desktop->getSelection(); if (selection->isEmpty()) { return false; } std::vector const items_selected(selection->items().begin(), selection->items().end()); std::vector items_down_erased; for (std::vector::const_iterator i=items_down.begin(); i!=items_down.end(); ++i) { SPItem *item_down = *i; Geom::OptRect bbox_down = item_down->documentVisualBounds(); double bbox_left = bbox_down->left(); double bbox_top = bbox_down->top(); gchar const * item_down_sharp = g_strdup_printf("#%s", item_down->getId()); items_down_erased.push_back(item_down); for (auto item_selected : items_selected) { gchar const * spray_origin; if(!item_selected->getAttribute("inkscape:spray-origin")){ spray_origin = g_strdup_printf("#%s", item_selected->getId()); } else { spray_origin = item_selected->getAttribute("inkscape:spray-origin"); } if(strcmp(item_down_sharp, spray_origin) == 0 || (item_down->getAttribute("inkscape:spray-origin") && strcmp(item_down->getAttribute("inkscape:spray-origin"),spray_origin) == 0 )) { if(mode == SPRAY_MODE_ERASER) { if(strcmp(item_down_sharp, spray_origin) != 0 && !selection->includes(item_down) ){ item_down->deleteObject(); items_down_erased.pop_back(); break; } } else if(no_overlap) { if(!(offset_width < 0 && offset_height < 0 && std::abs(bbox_left - bbox_left_main) > std::abs(offset_width) && std::abs(bbox_top - bbox_top_main) > std::abs(offset_height))){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } } else if(picker || over_transparent || over_no_transparent) { item_down->setHidden(true); item_down->updateRepr(); } } } } if(mode == SPRAY_MODE_ERASER){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down_erased); } return false; } if(picker || over_transparent || over_no_transparent){ if(!no_overlap){ doc->ensureUpToDate(); rgba = getPickerData(area, desktop); if (!rect_sprayed.hasZeroArea()) { rgba2 = getPickerData(rect_sprayed.roundOutwards(), desktop); } } if(pick_no_overlap){ if(rgba != rgba2){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } } if(!pick_center){ rgba = rgba2; } double opacity = 1.0; gchar color_string[32]; *color_string = 0; float r = SP_RGBA32_R_F(rgba); float g = SP_RGBA32_G_F(rgba); float b = SP_RGBA32_B_F(rgba); float a = SP_RGBA32_A_F(rgba); if(!over_transparent && (a == 0 || a < 1e-6)){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } if(!over_no_transparent && a > 0){ if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } if(picker && do_trace){ float hsl[3]; SPColor::rgb_to_hsl_floatv (hsl, r, g, b); gdouble val = 0; switch (pick) { case PICK_COLOR: val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max break; case PICK_OPACITY: val = a; break; case PICK_R: val = r; break; case PICK_G: val = g; break; case PICK_B: val = b; break; case PICK_H: val = hsl[0]; break; case PICK_S: val = hsl[1]; break; case PICK_L: val = 1 - hsl[2]; break; default: break; } if (rand_picked > 0) { val = randomize01 (val, rand_picked); r = randomize01 (r, rand_picked); g = randomize01 (g, rand_picked); b = randomize01 (b, rand_picked); } if (gamma_picked != 0) { double power; if (gamma_picked > 0) power = 1/(1 + fabs(gamma_picked)); else power = 1 + fabs(gamma_picked); val = pow (val, power); r = pow ((double)r, (double)power); g = pow ((double)g, (double)power); b = pow ((double)b, (double)power); } if (invert_picked) { val = 1 - val; r = 1 - r; g = 1 - g; b = 1 - b; } val = CLAMP (val, 0, 1); r = CLAMP (r, 0, 1); g = CLAMP (g, 0, 1); b = CLAMP (b, 0, 1); // recompose tweaked color rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); if (pick_to_size) { if(!trace_scale){ if(pick_inverse_value) { _scale = 1.0 - val; } else { _scale = val; } if(_scale == 0.0) { if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } if(!fit_item(desktop , item , bbox , move , center , mode , angle , _scale , scale , picker , pick_center , pick_inverse_value , pick_fill , pick_stroke , pick_no_overlap , over_no_transparent , over_transparent , no_overlap , offset , css , true , pick , do_trace , pick_to_size , pick_to_presence , pick_to_color , pick_to_opacity , invert_picked , gamma_picked , rand_picked) ) { if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } } } if (pick_to_opacity) { if(pick_inverse_value) { opacity *= 1.0 - val; } else { opacity *= val; } std::stringstream opacity_str; opacity_str.imbue(std::locale::classic()); opacity_str << opacity; sp_repr_css_set_property(css, "opacity", opacity_str.str().c_str()); } if (pick_to_presence) { if (g_random_double_range (0, 1) > val) { //Hiding the element is a way to retain original //behaviour of tiled clones for presence option. sp_repr_css_set_property(css, "opacity", "0"); } } if (pick_to_color) { sp_svg_write_color(color_string, sizeof(color_string), rgba); if(pick_fill){ sp_repr_css_set_property(css, "fill", color_string); } if(pick_stroke){ sp_repr_css_set_property(css, "stroke", color_string); } } if (opacity < 1e-6) { // invisibly transparent, skip if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } return false; } } if(!do_trace){ if(!pick_center){ rgba = rgba2; } if (pick_inverse_value) { r = 1 - SP_RGBA32_R_F(rgba); g = 1 - SP_RGBA32_G_F(rgba); b = 1 - SP_RGBA32_B_F(rgba); } else { r = SP_RGBA32_R_F(rgba); g = SP_RGBA32_G_F(rgba); b = SP_RGBA32_B_F(rgba); } rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); sp_svg_write_color(color_string, sizeof(color_string), rgba); if(pick_fill){ sp_repr_css_set_property(css, "fill", color_string); } if(pick_stroke){ sp_repr_css_set_property(css, "stroke", color_string); } } if(!no_overlap && (picker || over_transparent || over_no_transparent)){ showHidden(items_down); } } return true; } static bool sp_spray_recursive(SPDesktop *desktop, Inkscape::ObjectSet *set, SPItem *item, SPItem *&single_path_output, Geom::Point p, Geom::Point /*vector*/, gint mode, double radius, double population, double &scale, double scale_variation, bool /*reverse*/, double mean, double standard_deviation, double ratio, double tilt, double rotation_variation, gint _distrib, bool no_overlap, bool picker, bool pick_center, bool pick_inverse_value, bool pick_fill, bool pick_stroke, bool pick_no_overlap, bool over_no_transparent, bool over_transparent, double offset, bool usepressurescale, double pressure, int pick, bool do_trace, bool pick_to_size, bool pick_to_presence, bool pick_to_color, bool pick_to_opacity, bool invert_picked, double gamma_picked , double rand_picked) { bool did = false; { // convert 3D boxes to ordinary groups before spraying their shapes // TODO: ideally the original object is preserved. if (auto box = cast(item)) { desktop->getSelection()->remove(item); set->remove(item); item = box->convert_to_group(); set->add(item); desktop->getSelection()->add(item); } } double _fid = g_random_double_range(0, 1); double angle = g_random_double_range( - rotation_variation / 100.0 * M_PI , rotation_variation / 100.0 * M_PI ); double _scale = g_random_double_range( 1.0 - scale_variation / 100.0, 1.0 + scale_variation / 100.0 ); if(usepressurescale){ _scale = pressure; } double dr; double dp; random_position( dr, dp, mean, standard_deviation, _distrib ); dr=dr*radius; if (mode == SPRAY_MODE_COPY || mode == SPRAY_MODE_ERASER) { Geom::OptRect a = item->documentVisualBounds(); if (a) { if(_fid <= population) { SPDocument *doc = item->document; gchar const * spray_origin; if(!item->getAttribute("inkscape:spray-origin")){ spray_origin = g_strdup_printf("#%s", item->getId()); } else { spray_origin = item->getAttribute("inkscape:spray-origin"); } Geom::Point center = item->getCenter(); Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); SPCSSAttr *css = sp_repr_css_attr_new(); if (mode == SPRAY_MODE_ERASER || pick_no_overlap || no_overlap || picker || !over_transparent || !over_no_transparent) { if(!fit_item(desktop , item , a , move , center , mode , angle , _scale , scale , picker , pick_center , pick_inverse_value , pick_fill , pick_stroke , pick_no_overlap , over_no_transparent , over_transparent , no_overlap , offset , css , false , pick , do_trace , pick_to_size , pick_to_presence , pick_to_color , pick_to_opacity , invert_picked , gamma_picked , rand_picked)){ return false; } } SPItem *item_copied; // Duplicate Inkscape::XML::Document* xml_doc = doc->getReprDoc(); Inkscape::XML::Node *old_repr = item->getRepr(); Inkscape::XML::Node *parent = old_repr->parent(); Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); if(!copy->attribute("inkscape:spray-origin")){ copy->setAttribute("inkscape:spray-origin", spray_origin); } parent->appendChild(copy); SPObject *new_obj = doc->getObjectByRepr(copy); item_copied = cast(new_obj); // Conversion object->item sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(_scale)); sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(scale)); sp_spray_rotate_rel(center,desktop,item_copied, Geom::Rotate(angle)); // Move the cursor p item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); Inkscape::GC::release(copy); if(picker){ sp_desktop_apply_css_recursive(item_copied, css, true); } did = true; } } #ifdef ENABLE_SPRAY_MODE_SINGLE_PATH } else if (mode == SPRAY_MODE_SINGLE_PATH) { if (item) { SPDocument *doc = item->document; Inkscape::XML::Document* xml_doc = doc->getReprDoc(); Inkscape::XML::Node *old_repr = item->getRepr(); Inkscape::XML::Node *parent = old_repr->parent(); Geom::OptRect a = item->documentVisualBounds(); if (a) { if (_fid <= population) { // Rules the population of objects sprayed // Duplicates the parent item Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); gchar const * spray_origin; if(!copy->attribute("inkscape:spray-origin")){ spray_origin = g_strdup_printf("#%s", old_repr->attribute("id")); } else { spray_origin = copy->attribute("inkscape:spray-origin"); } parent->appendChild(copy); SPObject *new_obj = doc->getObjectByRepr(copy); auto item_copied = cast(new_obj); // Move around the cursor Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); Geom::Point center = item->getCenter(); sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); // Union // only works if no groups in selection ObjectSet object_set_tmp = *desktop->getSelection(); object_set_tmp.clear(); object_set_tmp.add(item_copied); object_set_tmp.removeLPESRecursive(true); if (is(object_set_tmp.objects().front())) { object_set_tmp.unlinkRecursive(true); } if (single_path_output) { // Previous result object_set_tmp.add(single_path_output); } object_set_tmp.pathUnion(true); single_path_output = object_set_tmp.items().front(); for (auto item : object_set_tmp.items()) { auto repr = item->getRepr(); repr->setAttribute("inkscape:spray-origin", spray_origin); } object_set_tmp.clear(); Inkscape::GC::release(copy); did = true; } } } #endif } else if (mode == SPRAY_MODE_CLONE) { Geom::OptRect a = item->documentVisualBounds(); if (a) { if(_fid <= population) { SPDocument *doc = item->document; gchar const * spray_origin; if(!item->getAttribute("inkscape:spray-origin")){ spray_origin = g_strdup_printf("#%s", item->getId()); } else { spray_origin = item->getAttribute("inkscape:spray-origin"); } Geom::Point center=item->getCenter(); Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); SPCSSAttr *css = sp_repr_css_attr_new(); if (mode == SPRAY_MODE_ERASER || pick_no_overlap || no_overlap || picker || !over_transparent || !over_no_transparent) { if(!fit_item(desktop , item , a , move , center , mode , angle , _scale , scale , picker , pick_center , pick_inverse_value , pick_fill , pick_stroke , pick_no_overlap , over_no_transparent , over_transparent , no_overlap , offset , css , true , pick , do_trace , pick_to_size , pick_to_presence , pick_to_color , pick_to_opacity , invert_picked , gamma_picked , rand_picked)) { return false; } } SPItem *item_copied; Inkscape::XML::Document* xml_doc = doc->getReprDoc(); Inkscape::XML::Node *old_repr = item->getRepr(); Inkscape::XML::Node *parent = old_repr->parent(); // Creation of the clone Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); // Ad the clone to the list of the parent's children parent->appendChild(clone); // Generates the link between parent and child attributes if(!clone->attribute("inkscape:spray-origin")){ clone->setAttribute("inkscape:spray-origin", spray_origin); } gchar *href_str = g_strdup_printf("#%s", old_repr->attribute("id")); clone->setAttribute("xlink:href", href_str); g_free(href_str); SPObject *clone_object = doc->getObjectByRepr(clone); // Conversion object->item item_copied = cast(clone_object); sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); if(picker){ sp_desktop_apply_css_recursive(item_copied, css, true); } Inkscape::GC::release(clone); did = true; } } } return did; } static bool sp_spray_dilate(SprayTool *tc, Geom::Point /*event_p*/, Geom::Point p, Geom::Point vector, bool reverse) { SPDesktop *desktop = tc->getDesktop(); Inkscape::ObjectSet *set = tc->objectSet(); if (set->isEmpty()) { return false; } bool did = false; double radius = get_dilate_radius(tc); double population = get_population(tc); if (radius == 0 || population == 0) { return false; } double path_mean = get_path_mean(tc); if (radius == 0 || path_mean == 0) { return false; } double path_standard_deviation = get_path_standard_deviation(tc); if (radius == 0 || path_standard_deviation == 0) { return false; } double move_mean = get_move_mean(tc); double move_standard_deviation = get_move_standard_deviation(tc); { std::vector const items(set->items().begin(), set->items().end()); for(auto item : items){ g_assert(item != nullptr); sp_object_ref(item); } for(auto item : items){ g_assert(item != nullptr); if (sp_spray_recursive(desktop , set , item , tc->single_path_output , p, vector , tc->mode , radius , population , tc->scale , tc->scale_variation , reverse , move_mean , move_standard_deviation , tc->ratio , tc->tilt , tc->rotation_variation , tc->distrib , tc->no_overlap , tc->picker , tc->pick_center , tc->pick_inverse_value , tc->pick_fill , tc->pick_stroke , tc->pick_no_overlap , tc->over_no_transparent , tc->over_transparent , tc->offset , tc->usepressurescale , get_pressure(tc) , tc->pick , tc->do_trace , tc->pick_to_size , tc->pick_to_presence , tc->pick_to_color , tc->pick_to_opacity , tc->invert_picked , tc->gamma_picked , tc->rand_picked)) { did = true; } } for(auto item : items){ g_assert(item != nullptr); sp_object_unref(item); } } return did; } static void sp_spray_update_area(SprayTool *tc) { double radius = get_dilate_radius(tc); Geom::Affine const sm ( Geom::Scale(radius/(1-tc->ratio), radius/(1+tc->ratio)) * Geom::Rotate(tc->tilt) * Geom::Translate(tc->getDesktop()->point())); Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin. path *= sm; tc->dilate_area->set_bpath(path); tc->dilate_area->show(); } static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift) { // Select the button mode auto tb = dynamic_cast(tc->getDesktop()->get_toolbar_by_name("SprayToolbar")); if(tb) { tb->set_mode(mode); } else { std::cerr << "Could not access Spray toolbar" << std::endl; } // Need to set explicitly, because the prefs may not have changed by the previous tc->mode = mode; tc->update_cursor(with_shift); } bool SprayTool::root_handler(GdkEvent* event) { gint ret = FALSE; switch (event->type) { case GDK_ENTER_NOTIFY: dilate_area->show(); break; case GDK_LEAVE_NOTIFY: dilate_area->hide(); break; case GDK_BUTTON_PRESS: if (event->button.button == 1) { _desktop->getSelection()->restoreBackup(); if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { return TRUE; } this->setCloneTilerPrefs(); Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(_desktop->w2d(motion_w)); this->last_push = _desktop->dt2doc(motion_dt); sp_spray_extinput(this, event); set_high_motion_precision(); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; object_set = *_desktop->getSelection(); if (mode == SPRAY_MODE_SINGLE_PATH) { this->single_path_output = nullptr; } sp_spray_dilate(this, motion_w, this->last_push, Geom::Point(0,0), MOD__SHIFT(event)); this->has_dilated = 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)); Geom::Point motion_doc(_desktop->dt2doc(motion_dt)); sp_spray_extinput(this, event); // Draw the dilating cursor double radius = get_dilate_radius(this); Geom::Affine const sm (Geom::Scale(radius/(1-this->ratio), radius/(1+this->ratio)) * Geom::Rotate(this->tilt) * Geom::Translate(_desktop->w2d(motion_w))); Geom::PathVector path = Geom::Path(Geom::Circle(0, 0, 1)); // Unit circle centered at origin. path *= sm; this->dilate_area->set_bpath(path); this->dilate_area->show(); guint num = 0; if (!_desktop->getSelection()->isEmpty()) { num = (guint)boost::distance(_desktop->getSelection()->items()); } if (num == 0) { this->message_context->flash(Inkscape::ERROR_MESSAGE, _("Nothing selected! Select objects to spray.")); } // Dilating: if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) { sp_spray_dilate(this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false); //this->last_push = motion_doc; this->has_dilated = true; // It's slow, so prevent clogging up with events gobble_motion_events(GDK_BUTTON1_MASK); return TRUE; } } break; /* Spray with the scroll */ case GDK_SCROLL: { if (event->scroll.state & GDK_BUTTON1_MASK) { double temp ; temp = this->population; this->population = 1.0; _desktop->setToolboxAdjustmentValue("population", this->population * 100); Geom::Point const scroll_w(event->button.x, event->button.y); Geom::Point const scroll_dt = _desktop->point();; switch (event->scroll.direction) { case GDK_SCROLL_DOWN: case GDK_SCROLL_UP: case GDK_SCROLL_SMOOTH: { if (Inkscape::have_viable_layer(_desktop, defaultMessageContext()) == false) { return TRUE; } this->last_push = _desktop->dt2doc(scroll_dt); sp_spray_extinput(this, event); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; if(this->is_dilating) { sp_spray_dilate(this, scroll_w, _desktop->dt2doc(scroll_dt), Geom::Point(0, 0), false); } this->has_dilated = true; this->population = temp; _desktop->setToolboxAdjustmentValue("population", this->population * 100); ret = TRUE; } break; case GDK_SCROLL_RIGHT: {} break; case GDK_SCROLL_LEFT: {} break; } } break; } case GDK_BUTTON_RELEASE: { Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(_desktop->w2d(motion_w)); set_high_motion_precision(false); this->is_drawing = false; if (this->is_dilating && event->button.button == 1) { if (!this->has_dilated) { // If we did not rub, do a light tap this->pressure = 0.03; sp_spray_dilate(this, motion_w, _desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event)); } this->is_dilating = false; this->has_dilated = false; switch (this->mode) { case SPRAY_MODE_COPY: DocumentUndo::done(_desktop->getDocument(), _("Spray with copies"), INKSCAPE_ICON("tool-spray")); break; case SPRAY_MODE_CLONE: DocumentUndo::done(_desktop->getDocument(), _("Spray with clones"), INKSCAPE_ICON("tool-spray")); break; case SPRAY_MODE_SINGLE_PATH: DocumentUndo::done(_desktop->getDocument(), _("Spray in single path"), INKSCAPE_ICON("tool-spray")); break; } } _desktop->getSelection()->clear(); object_set.clear(); break; } case GDK_KEY_PRESS: switch (get_latin_keyval (&event->key)) { case GDK_KEY_j: case GDK_KEY_J: if (MOD__SHIFT_ONLY(event)) { sp_spray_switch_mode(this, SPRAY_MODE_COPY, MOD__SHIFT(event)); ret = TRUE; } break; case GDK_KEY_k: case GDK_KEY_K: if (MOD__SHIFT_ONLY(event)) { sp_spray_switch_mode(this, SPRAY_MODE_CLONE, MOD__SHIFT(event)); ret = TRUE; } break; #ifdef ENABLE_SPRAY_MODE_SINGLE_PATH case GDK_KEY_l: case GDK_KEY_L: if (MOD__SHIFT_ONLY(event)) { sp_spray_switch_mode(this, SPRAY_MODE_SINGLE_PATH, MOD__SHIFT(event)); ret = TRUE; } break; #endif case GDK_KEY_Up: case GDK_KEY_KP_Up: if (!MOD__CTRL_ONLY(event)) { this->population += 0.01; if (this->population > 1.0) { this->population = 1.0; } _desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); ret = TRUE; } break; case GDK_KEY_Down: case GDK_KEY_KP_Down: if (!MOD__CTRL_ONLY(event)) { this->population -= 0.01; if (this->population < 0.0) { this->population = 0.0; } _desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); ret = TRUE; } break; case GDK_KEY_Right: case GDK_KEY_KP_Right: if (!MOD__CTRL_ONLY(event)) { this->width += 0.01; if (this->width > 1.0) { this->width = 1.0; } // The same spinbutton is for alt+x _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); sp_spray_update_area(this); ret = TRUE; } break; case GDK_KEY_Left: case GDK_KEY_KP_Left: if (!MOD__CTRL_ONLY(event)) { this->width -= 0.01; if (this->width < 0.01) { this->width = 0.01; } _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); sp_spray_update_area(this); ret = TRUE; } break; case GDK_KEY_Home: case GDK_KEY_KP_Home: this->width = 0.01; _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); sp_spray_update_area(this); ret = TRUE; break; case GDK_KEY_End: case GDK_KEY_KP_End: this->width = 1.0; _desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); sp_spray_update_area(this); ret = TRUE; break; case GDK_KEY_x: case GDK_KEY_X: if (MOD__ALT_ONLY(event)) { _desktop->setToolboxFocusTo("spray-width"); ret = TRUE; } break; case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: this->update_cursor(true); break; case GDK_KEY_Control_L: case GDK_KEY_Control_R: break; case GDK_KEY_Delete: case GDK_KEY_KP_Delete: case GDK_KEY_BackSpace: ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); break; default: break; } break; case GDK_KEY_RELEASE: { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); switch (get_latin_keyval(&event->key)) { case GDK_KEY_Shift_L: case GDK_KEY_Shift_R: this->update_cursor(false); break; case GDK_KEY_Control_L: case GDK_KEY_Control_R: sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); this->message_context->clear(); break; default: sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); break; } } default: break; } if (!ret) { // if ((SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler) { // ret = (SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler(event_context, event); // } ret = ToolBase::root_handler(event); } return ret; } } } } /* 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 :