// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Clone tiling dialog */ /* Authors: * bulia byak * Johan Engelen * Jon A. Cruz * Abhishek Sharma * Romain de Bossoreille * * Copyright (C) 2004-2011 Authors * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "clonetiler.h" #include #include #include #include #include #include #include #include #include #include <2geom/transforms.h> #include "desktop.h" #include "document-undo.h" #include "document.h" #include "filter-chemistry.h" #include "inkscape.h" #include "message-stack.h" #include "display/cairo-utils.h" #include "display/drawing-context.h" #include "display/drawing.h" #include "ui/icon-loader.h" #include "object/algorithms/unclump.h" #include "object/sp-item.h" #include "object/sp-namedview.h" #include "object/sp-root.h" #include "object/sp-use.h" #include "ui/icon-names.h" #include "ui/widget/spinbutton.h" #include "ui/widget/unit-menu.h" #include "svg/svg-color.h" #include "svg/svg.h" using Inkscape::DocumentUndo; using Inkscape::Util::unit_table; namespace Inkscape { namespace UI { namespace Widget { /** * Simple extension of Gtk::CheckButton, which adds a flag * to indicate whether the box should be unticked when reset */ class CheckButtonInternal : public Gtk::CheckButton { private: bool _uncheckable = false; public: CheckButtonInternal() = default; CheckButtonInternal(const Glib::ustring &label) : Gtk::CheckButton(label) {} void set_uncheckable(const bool val = true) { _uncheckable = val; } bool get_uncheckable() const { return _uncheckable; } }; } namespace Dialog { #define SB_MARGIN 1 #define VB_MARGIN 4 static Glib::ustring const prefs_path = "/dialogs/clonetiler/"; static Inkscape::Drawing *trace_drawing = nullptr; static unsigned trace_visionkey; static gdouble trace_zoom; static SPDocument *trace_doc = nullptr; CloneTiler::CloneTiler() : DialogBase("/dialogs/clonetiler/", "CloneTiler") , table_row_labels(nullptr) { set_spacing(0); { auto prefs = Inkscape::Preferences::get(); auto mainbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); mainbox->set_homogeneous(false); mainbox->set_border_width(6); pack_start(*mainbox, true, true, 0); nb = Gtk::manage(new Gtk::Notebook()); mainbox->pack_start(*nb, false, false, 0); // Symmetry { auto vb = new_tab(nb, _("_Symmetry")); /* TRANSLATORS: For the following 17 symmetry groups, see * http://www.bib.ulb.ac.be/coursmath/doc/17.htm (visual examples); * http://www.clarku.edu/~djoyce/wallpaper/seventeen.html (English vocabulary); or * http://membres.lycos.fr/villemingerard/Geometri/Sym1D.htm (French vocabulary). */ struct SymGroups { gint group; Glib::ustring label; } const sym_groups[] = { // TRANSLATORS: "translation" means "shift" / "displacement" here. {TILE_P1, _("P1: simple translation")}, {TILE_P2, _("P2: 180° rotation")}, {TILE_PM, _("PM: reflection")}, // TRANSLATORS: "glide reflection" is a reflection and a translation combined. // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html {TILE_PG, _("PG: glide reflection")}, {TILE_CM, _("CM: reflection + glide reflection")}, {TILE_PMM, _("PMM: reflection + reflection")}, {TILE_PMG, _("PMG: reflection + 180° rotation")}, {TILE_PGG, _("PGG: glide reflection + 180° rotation")}, {TILE_CMM, _("CMM: reflection + reflection + 180° rotation")}, {TILE_P4, _("P4: 90° rotation")}, {TILE_P4M, _("P4M: 90° rotation + 45° reflection")}, {TILE_P4G, _("P4G: 90° rotation + 90° reflection")}, {TILE_P3, _("P3: 120° rotation")}, {TILE_P31M, _("P31M: reflection + 120° rotation, dense")}, {TILE_P3M1, _("P3M1: reflection + 120° rotation, sparse")}, {TILE_P6, _("P6: 60° rotation")}, {TILE_P6M, _("P6M: reflection + 60° rotation")}, }; gint current = prefs->getInt(prefs_path + "symmetrygroup", 0); // Add a new combo box widget with the list of symmetry groups to the vbox auto combo = Gtk::manage(new Gtk::ComboBoxText()); combo->set_tooltip_text(_("Select one of the 17 symmetry groups for the tiling")); // Hack to add markup support auto cell_list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo->gobj())); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo->gobj()), GTK_CELL_RENDERER(cell_list->data), "markup", 0, nullptr); for (const auto & sg : sym_groups) { // Add the description of the symgroup to a new row combo->append(sg.label); } vb->pack_start(*combo, false, false, SB_MARGIN); combo->set_active(current); combo->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::symgroup_changed), combo)); } table_row_labels = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); // Shift { auto vb = new_tab(nb, _("S_hift")); auto table = table_x_y_rand (3); vb->pack_start(*table, false, false, 0); // X { auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount // xgettext:no-c-format l->set_markup(_("Shift X:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 2, 1); } { auto l = spinbox ( // xgettext:no-c-format _("Horizontal shift per row (in % of tile width)"), "shiftx_per_j", -10000, 10000, "%"); table_attach (table, l, 0, 2, 2); } { auto l = spinbox ( // xgettext:no-c-format _("Horizontal shift per column (in % of tile width)"), "shiftx_per_i", -10000, 10000, "%"); table_attach (table, l, 0, 2, 3); } { auto l = spinbox (_("Randomize the horizontal shift by this percentage"), "shiftx_rand", 0, 1000, "%"); table_attach (table, l, 0, 2, 4); } // Y { auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount // xgettext:no-c-format l->set_markup(_("Shift Y:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 3, 1); } { auto l = spinbox ( // xgettext:no-c-format _("Vertical shift per row (in % of tile height)"), "shifty_per_j", -10000, 10000, "%"); table_attach (table, l, 0, 3, 2); } { auto l = spinbox ( // xgettext:no-c-format _("Vertical shift per column (in % of tile height)"), "shifty_per_i", -10000, 10000, "%"); table_attach (table, l, 0, 3, 3); } { auto l = spinbox ( _("Randomize the vertical shift by this percentage"), "shifty_rand", 0, 1000, "%"); table_attach (table, l, 0, 3, 4); } // Exponent { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Exponent:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 4, 1); } { auto l = spinbox ( _("Whether rows are spaced evenly (1), converge (<1) or diverge (>1)"), "shifty_exp", 0, 10, "", true); table_attach (table, l, 0, 4, 2); } { auto l = spinbox ( _("Whether columns are spaced evenly (1), converge (<1) or diverge (>1)"), "shiftx_exp", 0, 10, "", true); table_attach (table, l, 0, 4, 3); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Alternate" is a verb here l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 5, 1); } { auto l = checkbox (_("Alternate the sign of shifts for each row"), "shifty_alternate"); table_attach (table, l, 0, 5, 2); } { auto l = checkbox (_("Alternate the sign of shifts for each column"), "shiftx_alternate"); table_attach (table, l, 0, 5, 3); } { // Cumulate auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Cumulate" is a verb here l->set_markup(_("Cumulate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 6, 1); } { auto l = checkbox (_("Cumulate the shifts for each row"), "shifty_cumulate"); table_attach (table, l, 0, 6, 2); } { auto l = checkbox (_("Cumulate the shifts for each column"), "shiftx_cumulate"); table_attach (table, l, 0, 6, 3); } { // Exclude tile width and height in shift auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Cumulate" is a verb here l->set_markup(_("Exclude tile:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 7, 1); } { auto l = checkbox (_("Exclude tile height in shift"), "shifty_excludeh"); table_attach (table, l, 0, 7, 2); } { auto l = checkbox (_("Exclude tile width in shift"), "shiftx_excludew"); table_attach (table, l, 0, 7, 3); } } // Scale { auto vb = new_tab(nb, _("Sc_ale")); auto table = table_x_y_rand(2); vb->pack_start(*table, false, false, 0); // X { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Scale X:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 2, 1); } { auto l = spinbox ( // xgettext:no-c-format _("Horizontal scale per row (in % of tile width)"), "scalex_per_j", -100, 1000, "%"); table_attach (table, l, 0, 2, 2); } { auto l = spinbox ( // xgettext:no-c-format _("Horizontal scale per column (in % of tile width)"), "scalex_per_i", -100, 1000, "%"); table_attach (table, l, 0, 2, 3); } { auto l = spinbox (_("Randomize the horizontal scale by this percentage"), "scalex_rand", 0, 1000, "%"); table_attach (table, l, 0, 2, 4); } // Y { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Scale Y:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 3, 1); } { auto l = spinbox ( // xgettext:no-c-format _("Vertical scale per row (in % of tile height)"), "scaley_per_j", -100, 1000, "%"); table_attach (table, l, 0, 3, 2); } { auto l = spinbox ( // xgettext:no-c-format _("Vertical scale per column (in % of tile height)"), "scaley_per_i", -100, 1000, "%"); table_attach (table, l, 0, 3, 3); } { auto l = spinbox (_("Randomize the vertical scale by this percentage"), "scaley_rand", 0, 1000, "%"); table_attach (table, l, 0, 3, 4); } // Exponent { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Exponent:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 4, 1); } { auto l = spinbox (_("Whether row scaling is uniform (1), converge (<1) or diverge (>1)"), "scaley_exp", 0, 10, "", true); table_attach (table, l, 0, 4, 2); } { auto l = spinbox (_("Whether column scaling is uniform (1), converge (<1) or diverge (>1)"), "scalex_exp", 0, 10, "", true); table_attach (table, l, 0, 4, 3); } // Logarithmic (as in logarithmic spiral) { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Base:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 5, 1); } { auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scaley_log", 0, 10, "", false); table_attach (table, l, 0, 5, 2); } { auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scalex_log", 0, 10, "", false); table_attach (table, l, 0, 5, 3); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Alternate" is a verb here l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 6, 1); } { auto l = checkbox (_("Alternate the sign of scales for each row"), "scaley_alternate"); table_attach (table, l, 0, 6, 2); } { auto l = checkbox (_("Alternate the sign of scales for each column"), "scalex_alternate"); table_attach (table, l, 0, 6, 3); } { // Cumulate auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Cumulate" is a verb here l->set_markup(_("Cumulate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 7, 1); } { auto l = checkbox (_("Cumulate the scales for each row"), "scaley_cumulate"); table_attach (table, l, 0, 7, 2); } { auto l = checkbox (_("Cumulate the scales for each column"), "scalex_cumulate"); table_attach (table, l, 0, 7, 3); } } // Rotation { auto vb = new_tab(nb, _("_Rotation")); auto table = table_x_y_rand (1); vb->pack_start(*table, false, false, 0); // Angle { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Angle:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 2, 1); } { auto l = spinbox ( // xgettext:no-c-format _("Rotate tiles by this angle for each row"), "rotate_per_j", -180, 180, "°"); table_attach (table, l, 0, 2, 2); } { auto l = spinbox ( // xgettext:no-c-format _("Rotate tiles by this angle for each column"), "rotate_per_i", -180, 180, "°"); table_attach (table, l, 0, 2, 3); } { auto l = spinbox (_("Randomize the rotation angle by this percentage"), "rotate_rand", 0, 100, "%"); table_attach (table, l, 0, 2, 4); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Alternate" is a verb here l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 3, 1); } { auto l = checkbox (_("Alternate the rotation direction for each row"), "rotate_alternatej"); table_attach (table, l, 0, 3, 2); } { auto l = checkbox (_("Alternate the rotation direction for each column"), "rotate_alternatei"); table_attach (table, l, 0, 3, 3); } { // Cumulate auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Cumulate" is a verb here l->set_markup(_("Cumulate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 4, 1); } { auto l = checkbox (_("Cumulate the rotation for each row"), "rotate_cumulatej"); table_attach (table, l, 0, 4, 2); } { auto l = checkbox (_("Cumulate the rotation for each column"), "rotate_cumulatei"); table_attach (table, l, 0, 4, 3); } } // Blur and opacity { auto vb = new_tab(nb, _("_Blur & opacity")); auto table = table_x_y_rand(1); vb->pack_start(*table, false, false, 0); // Blur { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Blur:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 2, 1); } { auto l = spinbox (_("Blur tiles by this percentage for each row"), "blur_per_j", 0, 100, "%"); table_attach (table, l, 0, 2, 2); } { auto l = spinbox (_("Blur tiles by this percentage for each column"), "blur_per_i", 0, 100, "%"); table_attach (table, l, 0, 2, 3); } { auto l = spinbox (_("Randomize the tile blur by this percentage"), "blur_rand", 0, 100, "%"); table_attach (table, l, 0, 2, 4); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Alternate" is a verb here l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 3, 1); } { auto l = checkbox (_("Alternate the sign of blur change for each row"), "blur_alternatej"); table_attach (table, l, 0, 3, 2); } { auto l = checkbox (_("Alternate the sign of blur change for each column"), "blur_alternatei"); table_attach (table, l, 0, 3, 3); } // Dissolve { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Opacity:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 4, 1); } { auto l = spinbox (_("Decrease tile opacity by this percentage for each row"), "opacity_per_j", 0, 100, "%"); table_attach (table, l, 0, 4, 2); } { auto l = spinbox (_("Decrease tile opacity by this percentage for each column"), "opacity_per_i", 0, 100, "%"); table_attach (table, l, 0, 4, 3); } { auto l = spinbox (_("Randomize the tile opacity by this percentage"), "opacity_rand", 0, 100, "%"); table_attach (table, l, 0, 4, 4); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); // TRANSLATORS: "Alternate" is a verb here l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 5, 1); } { auto l = checkbox (_("Alternate the sign of opacity change for each row"), "opacity_alternatej"); table_attach (table, l, 0, 5, 2); } { auto l = checkbox (_("Alternate the sign of opacity change for each column"), "opacity_alternatei"); table_attach (table, l, 0, 5, 3); } } // Color { auto vb = new_tab(nb, _("Co_lor")); { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); hb->set_homogeneous(false); auto l = Gtk::manage(new Gtk::Label(_("Initial color: "))); hb->pack_start(*l, false, false, 0); guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff); color_picker = new Inkscape::UI::Widget::ColorPicker (*new Glib::ustring(_("Initial color of tiled clones")), *new Glib::ustring(_("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)")), rgba, false); color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed)); hb->pack_start(*color_picker, false, false, 0); vb->pack_start(*hb, false, false, 0); } auto table = table_x_y_rand(3); vb->pack_start(*table, false, false, 0); // Hue { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("H:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 2, 1); } { auto l = spinbox (_("Change the tile hue by this percentage for each row"), "hue_per_j", -100, 100, "%"); table_attach (table, l, 0, 2, 2); } { auto l = spinbox (_("Change the tile hue by this percentage for each column"), "hue_per_i", -100, 100, "%"); table_attach (table, l, 0, 2, 3); } { auto l = spinbox (_("Randomize the tile hue by this percentage"), "hue_rand", 0, 100, "%"); table_attach (table, l, 0, 2, 4); } // Saturation { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("S:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 3, 1); } { auto l = spinbox (_("Change the color saturation by this percentage for each row"), "saturation_per_j", -100, 100, "%"); table_attach (table, l, 0, 3, 2); } { auto l = spinbox (_("Change the color saturation by this percentage for each column"), "saturation_per_i", -100, 100, "%"); table_attach (table, l, 0, 3, 3); } { auto l = spinbox (_("Randomize the color saturation by this percentage"), "saturation_rand", 0, 100, "%"); table_attach (table, l, 0, 3, 4); } // Lightness { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("L:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 4, 1); } { auto l = spinbox (_("Change the color lightness by this percentage for each row"), "lightness_per_j", -100, 100, "%"); table_attach (table, l, 0, 4, 2); } { auto l = spinbox (_("Change the color lightness by this percentage for each column"), "lightness_per_i", -100, 100, "%"); table_attach (table, l, 0, 4, 3); } { auto l = spinbox (_("Randomize the color lightness by this percentage"), "lightness_rand", 0, 100, "%"); table_attach (table, l, 0, 4, 4); } { // alternates auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Alternate:")); l->set_xalign(0.0); table_row_labels->add_widget(*l); table_attach (table, l, 1, 5, 1); } { auto l = checkbox (_("Alternate the sign of color changes for each row"), "color_alternatej"); table_attach (table, l, 0, 5, 2); } { auto l = checkbox (_("Alternate the sign of color changes for each column"), "color_alternatei"); table_attach (table, l, 0, 5, 3); } } // Trace { auto vb = new_tab(nb, _("_Trace")); { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); hb->set_border_width(4); hb->set_homogeneous(false); vb->pack_start(*hb, false, false, 0); _b = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Trace the drawing under the clones/sprayed items"))); _b->set_uncheckable(); bool old = prefs->getBool(prefs_path + "dotrace"); _b->set_active(old); _b->set_tooltip_text(_("For each clone/sprayed item, pick a value from the drawing in its location and apply it")); hb->pack_start(*_b, false, false, 0); _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled)); } { auto vvb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0)); vvb->set_homogeneous(false); vb->pack_start(*vvb, false, false, 0); _dotrace = vvb; { auto frame = Gtk::manage(new Gtk::Frame(_("1. Pick from the drawing:"))); frame->set_shadow_type(Gtk::SHADOW_NONE); vvb->pack_start(*frame, false, false, 0); auto table = Gtk::manage(new Gtk::Grid()); table->set_row_spacing(4); table->set_column_spacing(6); table->set_border_width(4); frame->add(*table); Gtk::RadioButtonGroup rb_group; { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Color"))); radio->set_tooltip_text(_("Pick the visible color and opacity")); table_attach(table, radio, 0.0, 1, 1); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_COLOR)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_COLOR); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Opacity"))); radio->set_tooltip_text(_("Pick the total accumulated opacity")); table_attach (table, radio, 0.0, 2, 1); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_OPACITY)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_OPACITY); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("R"))); radio->set_tooltip_text(_("Pick the Red component of the color")); table_attach (table, radio, 0.0, 1, 2); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_R)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_R); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("G"))); radio->set_tooltip_text(_("Pick the Green component of the color")); table_attach (table, radio, 0.0, 2, 2); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_G)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_G); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("B"))); radio->set_tooltip_text(_("Pick the Blue component of the color")); table_attach (table, radio, 0.0, 3, 2); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_B)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_B); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color hue", "H"))); radio->set_tooltip_text(_("Pick the hue of the color")); table_attach (table, radio, 0.0, 1, 3); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_H)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_H); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color saturation", "S"))); radio->set_tooltip_text(_("Pick the saturation of the color")); table_attach (table, radio, 0.0, 2, 3); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_S)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_S); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color lightness", "L"))); radio->set_tooltip_text(_("Pick the lightness of the color")); table_attach (table, radio, 0.0, 3, 3); radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_L)); radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_L); } } { auto frame = Gtk::manage(new Gtk::Frame(_("2. Tweak the picked value:"))); frame->set_shadow_type(Gtk::SHADOW_NONE); vvb->pack_start(*frame, false, false, VB_MARGIN); auto table = Gtk::manage(new Gtk::Grid()); table->set_row_spacing(4); table->set_column_spacing(6); table->set_border_width(4); frame->add(*table); { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Gamma-correct:")); table_attach (table, l, 1.0, 1, 1); } { auto l = spinbox (_("Shift the mid-range of the picked value upwards (>0) or downwards (<0)"), "gamma_picked", -10, 10, ""); table_attach (table, l, 0.0, 1, 2); } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Randomize:")); table_attach (table, l, 1.0, 1, 3); } { auto l = spinbox (_("Randomize the picked value by this percentage"), "rand_picked", 0, 100, "%"); table_attach (table, l, 0.0, 1, 4); } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Invert:")); table_attach (table, l, 1.0, 2, 1); } { auto l = checkbox (_("Invert the picked value"), "invert_picked"); table_attach (table, l, 0.0, 2, 2); } } { auto frame = Gtk::manage(new Gtk::Frame(_("3. Apply the value to the clones':"))); frame->set_shadow_type(Gtk::SHADOW_NONE); vvb->pack_start(*frame, false, false, 0); auto table = Gtk::manage(new Gtk::Grid()); table->set_row_spacing(4); table->set_column_spacing(6); table->set_border_width(4); frame->add(*table); { auto b = Gtk::manage(new Gtk::CheckButton(_("Presence"))); bool old = prefs->getBool(prefs_path + "pick_to_presence", true); b->set_active(old); b->set_tooltip_text(_("Each clone is created with the probability determined by the picked value in that point")); table_attach (table, b, 0.0, 1, 1); b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_presence")); } { auto b = Gtk::manage(new Gtk::CheckButton(_("Size"))); bool old = prefs->getBool(prefs_path + "pick_to_size"); b->set_active(old); b->set_tooltip_text(_("Each clone's size is determined by the picked value in that point")); table_attach (table, b, 0.0, 2, 1); b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_size")); } { auto b = Gtk::manage(new Gtk::CheckButton(_("Color"))); bool old = prefs->getBool(prefs_path + "pick_to_color", false); b->set_active(old); b->set_tooltip_text(_("Each clone is painted by the picked color (the original must have unset fill or stroke)")); table_attach (table, b, 0.0, 1, 2); b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_color")); } { auto b = Gtk::manage(new Gtk::CheckButton(_("Opacity"))); bool old = prefs->getBool(prefs_path + "pick_to_opacity", false); b->set_active(old); b->set_tooltip_text(_("Each clone's opacity is determined by the picked value in that point")); table_attach (table, b, 0.0, 2, 2); b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_opacity")); } } vvb->set_sensitive(prefs->getBool(prefs_path + "dotrace")); } } { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); hb->set_homogeneous(false); mainbox->pack_start(*hb, false, false, 0); auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Apply to tiled clones:")); hb->pack_start(*l, false, false, 0); } // Rows/columns, width/height { auto table = Gtk::manage(new Gtk::Grid()); table->set_row_spacing(4); table->set_column_spacing(6); table->set_border_width(VB_MARGIN); mainbox->pack_start(*table, false, false, 0); { { auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); int value = prefs->getInt(prefs_path + "jmax", 2); a->set_value (value); auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); sb->set_tooltip_text (_("How many rows in the tiling")); sb->set_width_chars (7); sb->set_name("row"); table_attach(table, sb, 0.0f, 1, 2); _rowscols.push_back(sb); a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "jmax")); } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup("×"); table_attach(table, l, 0.0f, 1, 3); _rowscols.push_back(l); } { auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); int value = prefs->getInt(prefs_path + "imax", 2); a->set_value (value); auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); sb->set_tooltip_text (_("How many columns in the tiling")); sb->set_width_chars (7); table_attach(table, sb, 0.0f, 1, 4); _rowscols.push_back(sb); a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax")); } } { // unitmenu unit_menu = new Inkscape::UI::Widget::UnitMenu(); unit_menu->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); unit_menu->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); unitChangedConn = unit_menu->signal_changed().connect(sigc::mem_fun(*this, &CloneTiler::unit_changed)); { // Width spinbutton fill_width = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); double value = prefs->getDouble(prefs_path + "fillwidth", 50.0); Inkscape::Util::Unit const *unit = unit_menu->getUnit(); gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); fill_width->set_value (units); auto e = new Inkscape::UI::Widget::SpinButton(fill_width, 1.0, 2); e->set_tooltip_text (_("Width of the rectangle to be filled")); e->set_width_chars (7); e->set_digits (4); table_attach(table, e, 0.0f, 2, 2); _widthheight.push_back(e); fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed)); } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup("×"); table_attach(table, l, 0.0f, 2, 3); _widthheight.push_back(l); } { // Height spinbutton fill_height = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); double value = prefs->getDouble(prefs_path + "fillheight", 50.0); Inkscape::Util::Unit const *unit = unit_menu->getUnit(); gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); fill_height->set_value (units); auto e = new Inkscape::UI::Widget::SpinButton(fill_height, 1.0, 2); e->set_tooltip_text (_("Height of the rectangle to be filled")); e->set_width_chars (7); e->set_digits (4); table_attach(table, e, 0.0f, 2, 4); _widthheight.push_back(e); fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed)); } table_attach(table, unit_menu, 0.0f, 2, 5); _widthheight.push_back(unit_menu); } // Switch Gtk::RadioButtonGroup rb_group; { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Rows, columns: "))); radio->set_tooltip_text(_("Create the specified number of rows and columns")); table_attach(table, radio, 0.0, 1, 1); if (!prefs->getBool(prefs_path + "fillrect")) { radio->set_active(true); switch_to_create(); } radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create)); } { auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Width, height: "))); radio->set_tooltip_text(_("Fill the specified width and height with the tiling")); table_attach(table, radio, 0.0, 2, 1); if (prefs->getBool(prefs_path + "fillrect")) { radio->set_active(true); switch_to_fill(); } radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill)); } } // Use saved pos { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); mainbox->pack_start(*hb, false, false, 0); _cb_keep_bbox = Gtk::manage(new UI::Widget::CheckButtonInternal(_("Use saved size and position of the tile"))); auto keepbbox = prefs->getBool(prefs_path + "keepbbox", true); _cb_keep_bbox->set_active(keepbbox); _cb_keep_bbox->set_tooltip_text(_("Pretend that the size and position of the tile are the same " "as the last time you tiled it (if any), instead of using the " "current size")); hb->pack_start(*_cb_keep_bbox, false, false, 0); _cb_keep_bbox->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::keep_bbox_toggled)); } // Statusbar { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); hb->set_homogeneous(false); mainbox->pack_end(*hb, false, false, 0); auto l = Gtk::manage(new Gtk::Label("")); _status = l; hb->pack_start(*l, false, false, 0); } // Buttons { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); hb->set_homogeneous(false); mainbox->pack_start(*hb, false, false, 0); { auto b = Gtk::manage(new Gtk::Button()); auto l = Gtk::manage(new Gtk::Label("")); l->set_markup_with_mnemonic(_(" _Create ")); b->add(*l); b->set_tooltip_text(_("Create and tile the clones of the selection")); b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::apply)); hb->pack_end(*b, false, false, 0); } { // buttons which are enabled only when there are tiled clones auto sb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); sb->set_homogeneous(false); hb->pack_end(*sb, false, false, 0); _buttons_on_tiles = sb; { // TRANSLATORS: if a group of objects are "clumped" together, then they // are unevenly spread in the given amount of space - as shown in the // diagrams on the left in the following screenshot: // http://www.inkscape.org/screenshots/gallery/inkscape-0.42-CVS-tiles-unclump.png // So unclumping is the process of spreading a number of objects out more evenly. auto b = Gtk::manage(new Gtk::Button(_(" _Unclump "), true)); b->set_tooltip_text(_("Spread out clones to reduce clumping; can be applied repeatedly")); b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::unclump)); sb->pack_end(*b, false, false, 0); } { auto b = Gtk::manage(new Gtk::Button(_(" Re_move "), true)); b->set_tooltip_text(_("Remove existing tiled clones of the selected object (siblings only)")); b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::on_remove_button_clicked)); sb->pack_end(*b, false, false, 0); } // connect to global selection changed signal (so we can change desktops) and // external_change (so we're not fooled by undo) selectChangedConn = INKSCAPE.signal_selection_changed.connect(sigc::mem_fun(*this, &CloneTiler::change_selection)); externChangedConn = INKSCAPE.signal_external_change.connect(sigc::mem_fun(*this, &CloneTiler::external_change)); // update now change_selection(SP_ACTIVE_DESKTOP->getSelection()); } { auto b = Gtk::manage(new Gtk::Button(_(" R_eset "), true)); // TRANSLATORS: "change" is a noun here b->set_tooltip_text(_("Reset all shifts, scales, rotates, opacity and color changes in the dialog to zero")); b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::reset)); hb->pack_start(*b, false, false, 0); } } mainbox->show_all(); } show_all(); } CloneTiler::~CloneTiler () { selectChangedConn.disconnect(); externChangedConn.disconnect(); color_changed_connection.disconnect(); } void CloneTiler::on_picker_color_changed(guint rgba) { static bool is_updating = false; if (is_updating || !SP_ACTIVE_DESKTOP) return; is_updating = true; gchar c[32]; sp_svg_write_color(c, sizeof(c), rgba); Inkscape::Preferences *prefs = Inkscape::Preferences::get(); prefs->setString(prefs_path + "initial_color", c); is_updating = false; } void CloneTiler::change_selection(Inkscape::Selection *selection) { if (selection->isEmpty()) { _buttons_on_tiles->set_sensitive(false); _status->set_markup(_("Nothing selected.")); return; } if (boost::distance(selection->items()) > 1) { _buttons_on_tiles->set_sensitive(false); _status->set_markup(_("More than one object selected.")); return; } guint n = number_of_clones(selection->singleItem()); if (n > 0) { _buttons_on_tiles->set_sensitive(true); gchar *sta = g_strdup_printf (_("Object has %d tiled clones."), n); _status->set_markup(sta); g_free (sta); } else { _buttons_on_tiles->set_sensitive(false); _status->set_markup(_("Object has no tiled clones.")); } } void CloneTiler::external_change() { change_selection(SP_ACTIVE_DESKTOP->getSelection()); } Geom::Affine CloneTiler::get_transform( // symmetry group int type, // row, column int i, int j, // center, width, height of the tile double cx, double cy, double w, double h, // values from the dialog: // Shift double shiftx_per_i, double shifty_per_i, double shiftx_per_j, double shifty_per_j, double shiftx_rand, double shifty_rand, double shiftx_exp, double shifty_exp, int shiftx_alternate, int shifty_alternate, int shiftx_cumulate, int shifty_cumulate, int shiftx_excludew, int shifty_excludeh, // Scale double scalex_per_i, double scaley_per_i, double scalex_per_j, double scaley_per_j, double scalex_rand, double scaley_rand, double scalex_exp, double scaley_exp, double scalex_log, double scaley_log, int scalex_alternate, int scaley_alternate, int scalex_cumulate, int scaley_cumulate, // Rotation double rotate_per_i, double rotate_per_j, double rotate_rand, int rotate_alternatei, int rotate_alternatej, int rotate_cumulatei, int rotate_cumulatej ) { // Shift (in units of tile width or height) ------------- double delta_shifti = 0.0; double delta_shiftj = 0.0; if( shiftx_alternate ) { delta_shifti = (double)(i%2); } else { if( shiftx_cumulate ) { // Should the delta shifts be cumulative (i.e. 1, 1+2, 1+2+3, ...) delta_shifti = (double)(i*i); } else { delta_shifti = (double)i; } } if( shifty_alternate ) { delta_shiftj = (double)(j%2); } else { if( shifty_cumulate ) { delta_shiftj = (double)(j*j); } else { delta_shiftj = (double)j; } } // Random shift, only calculate if non-zero. double delta_shiftx_rand = 0.0; double delta_shifty_rand = 0.0; if( shiftx_rand != 0.0 ) delta_shiftx_rand = shiftx_rand * g_random_double_range (-1, 1); if( shifty_rand != 0.0 ) delta_shifty_rand = shifty_rand * g_random_double_range (-1, 1); // Delta shift (units of tile width/height) double di = shiftx_per_i * delta_shifti + shiftx_per_j * delta_shiftj + delta_shiftx_rand; double dj = shifty_per_i * delta_shifti + shifty_per_j * delta_shiftj + delta_shifty_rand; // Shift in actual x and y, used below double dx = w * di; double dy = h * dj; double shifti = di; double shiftj = dj; // Include tile width and height in shift if required if( !shiftx_excludew ) shifti += i; if( !shifty_excludeh ) shiftj += j; // Add exponential shift if necessary double shifti_sign = (shifti > 0.0) ? 1.0 : -1.0; shifti = shifti_sign * pow(fabs(shifti), shiftx_exp); double shiftj_sign = (shiftj > 0.0) ? 1.0 : -1.0; shiftj = shiftj_sign * pow(fabs(shiftj), shifty_exp); // Final shift Geom::Affine rect_translate (Geom::Translate (w * shifti, h * shiftj)); // Rotation (in degrees) ------------ double delta_rotationi = 0.0; double delta_rotationj = 0.0; if( rotate_alternatei ) { delta_rotationi = (double)(i%2); } else { if( rotate_cumulatei ) { delta_rotationi = (double)(i*i + i)/2.0; } else { delta_rotationi = (double)i; } } if( rotate_alternatej ) { delta_rotationj = (double)(j%2); } else { if( rotate_cumulatej ) { delta_rotationj = (double)(j*j + j)/2.0; } else { delta_rotationj = (double)j; } } double delta_rotate_rand = 0.0; if( rotate_rand != 0.0 ) delta_rotate_rand = rotate_rand * 180.0 * g_random_double_range (-1, 1); double dr = rotate_per_i * delta_rotationi + rotate_per_j * delta_rotationj + delta_rotate_rand; // Scale (times the original) ----------- double delta_scalei = 0.0; double delta_scalej = 0.0; if( scalex_alternate ) { delta_scalei = (double)(i%2); } else { if( scalex_cumulate ) { // Should the delta scales be cumulative (i.e. 1, 1+2, 1+2+3, ...) delta_scalei = (double)(i*i + i)/2.0; } else { delta_scalei = (double)i; } } if( scaley_alternate ) { delta_scalej = (double)(j%2); } else { if( scaley_cumulate ) { delta_scalej = (double)(j*j + j)/2.0; } else { delta_scalej = (double)j; } } // Random scale, only calculate if non-zero. double delta_scalex_rand = 0.0; double delta_scaley_rand = 0.0; if( scalex_rand != 0.0 ) delta_scalex_rand = scalex_rand * g_random_double_range (-1, 1); if( scaley_rand != 0.0 ) delta_scaley_rand = scaley_rand * g_random_double_range (-1, 1); // But if random factors are same, scale x and y proportionally if( scalex_rand == scaley_rand ) delta_scalex_rand = delta_scaley_rand; // Total delta scale double scalex = 1.0 + scalex_per_i * delta_scalei + scalex_per_j * delta_scalej + delta_scalex_rand; double scaley = 1.0 + scaley_per_i * delta_scalei + scaley_per_j * delta_scalej + delta_scaley_rand; if( scalex < 0.0 ) scalex = 0.0; if( scaley < 0.0 ) scaley = 0.0; // Add exponential scale if necessary if ( scalex_exp != 1.0 ) scalex = pow( scalex, scalex_exp ); if ( scaley_exp != 1.0 ) scaley = pow( scaley, scaley_exp ); // Add logarithmic factor if necessary if ( scalex_log > 0.0 ) scalex = pow( scalex_log, scalex - 1.0 ); if ( scaley_log > 0.0 ) scaley = pow( scaley_log, scaley - 1.0 ); // Alternative using rotation angle //if ( scalex_log != 1.0 ) scalex *= pow( scalex_log, M_PI*dr/180 ); //if ( scaley_log != 1.0 ) scaley *= pow( scaley_log, M_PI*dr/180 ); // Calculate transformation matrices, translating back to "center of tile" (rotation center) before transforming Geom::Affine drot_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI*dr/180) * Geom::Translate(cx, cy); Geom::Affine dscale_c = Geom::Translate(-cx, -cy) * Geom::Scale (scalex, scaley) * Geom::Translate(cx, cy); Geom::Affine d_s_r = dscale_c * drot_c; Geom::Affine rotate_180_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI) * Geom::Translate(cx, cy); Geom::Affine rotate_90_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/2) * Geom::Translate(cx, cy); Geom::Affine rotate_m90_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/2) * Geom::Translate(cx, cy); Geom::Affine rotate_120_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-2*M_PI/3) * Geom::Translate(cx, cy); Geom::Affine rotate_m120_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( 2*M_PI/3) * Geom::Translate(cx, cy); Geom::Affine rotate_60_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/3) * Geom::Translate(cx, cy); Geom::Affine rotate_m60_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/3) * Geom::Translate(cx, cy); Geom::Affine flip_x = Geom::Translate(-cx, -cy) * Geom::Scale (-1, 1) * Geom::Translate(cx, cy); Geom::Affine flip_y = Geom::Translate(-cx, -cy) * Geom::Scale (1, -1) * Geom::Translate(cx, cy); // Create tile with required symmetry const double cos60 = cos(M_PI/3); const double sin60 = sin(M_PI/3); const double cos30 = cos(M_PI/6); const double sin30 = sin(M_PI/6); switch (type) { case TILE_P1: return d_s_r * rect_translate; break; case TILE_P2: if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * rotate_180_c * rect_translate; } break; case TILE_PM: if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } break; case TILE_PG: if (j % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } break; case TILE_CM: if ((i + j) % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } break; case TILE_PMM: if (j % 2 == 0) { if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } } else { if (i % 2 == 0) { return d_s_r * flip_y * rect_translate; } else { return d_s_r * flip_x * flip_y * rect_translate; } } break; case TILE_PMG: if (j % 2 == 0) { if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * rotate_180_c * rect_translate; } } else { if (i % 2 == 0) { return d_s_r * flip_y * rect_translate; } else { return d_s_r * rotate_180_c * flip_y * rect_translate; } } break; case TILE_PGG: if (j % 2 == 0) { if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_y * rect_translate; } } else { if (i % 2 == 0) { return d_s_r * rotate_180_c * rect_translate; } else { return d_s_r * rotate_180_c * flip_y * rect_translate; } } break; case TILE_CMM: if (j % 4 == 0) { if (i % 2 == 0) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } } else if (j % 4 == 1) { if (i % 2 == 0) { return d_s_r * flip_y * rect_translate; } else { return d_s_r * flip_x * flip_y * rect_translate; } } else if (j % 4 == 2) { if (i % 2 == 1) { return d_s_r * rect_translate; } else { return d_s_r * flip_x * rect_translate; } } else { if (i % 2 == 1) { return d_s_r * flip_y * rect_translate; } else { return d_s_r * flip_x * flip_y * rect_translate; } } break; case TILE_P4: { Geom::Affine ori (Geom::Translate ((w + h) * pow((i/2), shiftx_exp) + dx, (h + w) * pow((j/2), shifty_exp) + dy)); Geom::Affine dia1 (Geom::Translate (w/2 + h/2, -h/2 + w/2)); Geom::Affine dia2 (Geom::Translate (-w/2 + h/2, h/2 + w/2)); if (j % 2 == 0) { if (i % 2 == 0) { return d_s_r * ori; } else { return d_s_r * rotate_m90_c * dia1 * ori; } } else { if (i % 2 == 0) { return d_s_r * rotate_90_c * dia2 * ori; } else { return d_s_r * rotate_180_c * dia1 * dia2 * ori; } } } break; case TILE_P4M: { double max = MAX(w, h); Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow((j/2), shifty_exp) + dy)); Geom::Affine dia1 (Geom::Translate ( w/2 - h/2, h/2 - w/2)); Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 - h/2)); if (j % 2 == 0) { if (i % 4 == 0) { return d_s_r * ori; } else if (i % 4 == 1) { return d_s_r * flip_y * rotate_m90_c * dia1 * ori; } else if (i % 4 == 2) { return d_s_r * rotate_m90_c * dia1 * Geom::Translate (h, 0) * ori; } else if (i % 4 == 3) { return d_s_r * flip_x * Geom::Translate (w, 0) * ori; } } else { if (i % 4 == 0) { return d_s_r * flip_y * Geom::Translate(0, h) * ori; } else if (i % 4 == 1) { return d_s_r * rotate_90_c * dia2 * Geom::Translate(0, h) * ori; } else if (i % 4 == 2) { return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate(h, 0) * Geom::Translate(0, h) * ori; } else if (i % 4 == 3) { return d_s_r * flip_y * flip_x * Geom::Translate(w, 0) * Geom::Translate(0, h) * ori; } } } break; case TILE_P4G: { double max = MAX(w, h); Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow(j, shifty_exp) + dy)); Geom::Affine dia1 (Geom::Translate ( w/2 + h/2, h/2 - w/2)); Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 + h/2)); if (((i/4) + j) % 2 == 0) { if (i % 4 == 0) { return d_s_r * ori; } else if (i % 4 == 1) { return d_s_r * rotate_m90_c * dia1 * ori; } else if (i % 4 == 2) { return d_s_r * rotate_90_c * dia2 * ori; } else if (i % 4 == 3) { return d_s_r * rotate_180_c * dia1 * dia2 * ori; } } else { if (i % 4 == 0) { return d_s_r * flip_y * Geom::Translate (0, h) * ori; } else if (i % 4 == 1) { return d_s_r * flip_y * rotate_m90_c * dia1 * Geom::Translate (-h, 0) * ori; } else if (i % 4 == 2) { return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate (h, 0) * ori; } else if (i % 4 == 3) { return d_s_r * flip_x * Geom::Translate (w, 0) * ori; } } } break; case TILE_P3: { double width; double height; Geom::Affine dia1; Geom::Affine dia2; if (w > h) { width = w + w * cos60; height = 2 * w * sin60; dia1 = Geom::Affine (Geom::Translate (w/2 + w/2 * cos60, -(w/2 * sin60))); dia2 = dia1 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60))); } else { width = h * cos (M_PI/6); height = h; dia1 = Geom::Affine (Geom::Translate (h/2 * cos30, -(h/2 * sin30))); dia2 = dia1 * Geom::Affine (Geom::Translate (0, h/2)); } Geom::Affine ori (Geom::Translate (width * pow((2*(i/3) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); if (i % 3 == 0) { return d_s_r * ori; } else if (i % 3 == 1) { return d_s_r * rotate_m120_c * dia1 * ori; } else if (i % 3 == 2) { return d_s_r * rotate_120_c * dia2 * ori; } } break; case TILE_P31M: { Geom::Affine ori; Geom::Affine dia1; Geom::Affine dia2; Geom::Affine dia3; Geom::Affine dia4; if (w > h) { ori = Geom::Affine(Geom::Translate (w * pow((i/6) + 0.5*(j%2), shiftx_exp) + dx, (w * cos30) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); } else { ori = Geom::Affine (Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (2*h - h * sin30) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); } if (i % 6 == 0) { return d_s_r * ori; } else if (i % 6 == 1) { return d_s_r * flip_y * rotate_m120_c * dia1 * ori; } else if (i % 6 == 2) { return d_s_r * rotate_m120_c * dia2 * ori; } else if (i % 6 == 3) { return d_s_r * flip_y * rotate_120_c * dia3 * ori; } else if (i % 6 == 4) { return d_s_r * rotate_120_c * dia4 * ori; } else if (i % 6 == 5) { return d_s_r * flip_y * Geom::Translate(0, h) * ori; } } break; case TILE_P3M1: { double width; double height; Geom::Affine dia1; Geom::Affine dia2; Geom::Affine dia3; Geom::Affine dia4; if (w > h) { width = w + w * cos60; height = 2 * w * sin60; dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); } else { width = 2 * h * cos (M_PI/6); height = 2 * h; dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); } Geom::Affine ori (Geom::Translate (width * pow((2*(i/6) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); if (i % 6 == 0) { return d_s_r * ori; } else if (i % 6 == 1) { return d_s_r * flip_y * rotate_m120_c * dia1 * ori; } else if (i % 6 == 2) { return d_s_r * rotate_m120_c * dia2 * ori; } else if (i % 6 == 3) { return d_s_r * flip_y * rotate_120_c * dia3 * ori; } else if (i % 6 == 4) { return d_s_r * rotate_120_c * dia4 * ori; } else if (i % 6 == 5) { return d_s_r * flip_y * Geom::Translate(0, h) * ori; } } break; case TILE_P6: { Geom::Affine ori; Geom::Affine dia1; Geom::Affine dia2; Geom::Affine dia3; Geom::Affine dia4; Geom::Affine dia5; if (w > h) { ori = Geom::Affine(Geom::Translate (w * pow((2*(i/6) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60)); dia2 = dia1 * Geom::Affine (Geom::Translate (w/2, 0)); dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, w/2 * sin60)); dia4 = dia3 * Geom::Affine (Geom::Translate (-w/2 * cos60, w/2 * sin60)); dia5 = dia4 * Geom::Affine (Geom::Translate (-w/2, 0)); } else { ori = Geom::Affine(Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (h + h * sin30) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); dia2 = dia1 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); dia4 = dia3 * dia1.inverse(); dia5 = dia3 * dia2.inverse(); } if (i % 6 == 0) { return d_s_r * ori; } else if (i % 6 == 1) { return d_s_r * rotate_m60_c * dia1 * ori; } else if (i % 6 == 2) { return d_s_r * rotate_m120_c * dia2 * ori; } else if (i % 6 == 3) { return d_s_r * rotate_180_c * dia3 * ori; } else if (i % 6 == 4) { return d_s_r * rotate_120_c * dia4 * ori; } else if (i % 6 == 5) { return d_s_r * rotate_60_c * dia5 * ori; } } break; case TILE_P6M: { Geom::Affine ori; Geom::Affine dia1, dia2, dia3, dia4, dia5, dia6, dia7, dia8, dia9, dia10; if (w > h) { ori = Geom::Affine(Geom::Translate (w * pow((2*(i/12) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (w/2, h/2) * Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, h/2 * sin30)); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (-h/2 * cos30, h/2 * sin30) * Geom::Translate (w * cos60, 0) * Geom::Translate (-h/2 * cos30, -h/2 * sin30)); dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia5 = dia4 * Geom::Affine (Geom::Translate (-h/2 * cos30, -h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60) * Geom::Translate (w/2, -h/2)); dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); dia7 = dia6 * dia1.inverse(); dia8 = dia6 * dia2.inverse(); dia9 = dia6 * dia3.inverse(); dia10 = dia6 * dia4.inverse(); } else { ori = Geom::Affine(Geom::Translate (4*h * cos30 * pow((i/12 + 0.5*(j%2)), shiftx_exp) + dx, (2*h + 2*h * sin30) * pow(j, shifty_exp) + dy)); dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); dia3 = dia2 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h * cos30, 0) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); dia5 = dia4 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); dia7 = dia6 * dia1.inverse(); dia8 = dia6 * dia2.inverse(); dia9 = dia6 * dia3.inverse(); dia10 = dia6 * dia4.inverse(); } if (i % 12 == 0) { return d_s_r * ori; } else if (i % 12 == 1) { return d_s_r * flip_y * rotate_m60_c * dia1 * ori; } else if (i % 12 == 2) { return d_s_r * rotate_m60_c * dia2 * ori; } else if (i % 12 == 3) { return d_s_r * flip_y * rotate_m120_c * dia3 * ori; } else if (i % 12 == 4) { return d_s_r * rotate_m120_c * dia4 * ori; } else if (i % 12 == 5) { return d_s_r * flip_x * dia5 * ori; } else if (i % 12 == 6) { return d_s_r * flip_x * flip_y * dia6 * ori; } else if (i % 12 == 7) { return d_s_r * flip_y * rotate_120_c * dia7 * ori; } else if (i % 12 == 8) { return d_s_r * rotate_120_c * dia8 * ori; } else if (i % 12 == 9) { return d_s_r * flip_y * rotate_60_c * dia9 * ori; } else if (i % 12 == 10) { return d_s_r * rotate_60_c * dia10 * ori; } else if (i % 12 == 11) { return d_s_r * flip_y * Geom::Translate (0, h) * ori; } } break; default: break; } return Geom::identity(); } bool CloneTiler::is_a_clone_of(SPObject *tile, SPObject *obj) { bool result = false; char *id_href = nullptr; if (obj) { Inkscape::XML::Node *obj_repr = obj->getRepr(); id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); } if (dynamic_cast(tile) && tile->getRepr()->attribute("xlink:href") && (!id_href || !strcmp(id_href, tile->getRepr()->attribute("xlink:href"))) && tile->getRepr()->attribute("inkscape:tiled-clone-of") && (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of")))) { result = true; } else { result = false; } if (id_href) { g_free(id_href); id_href = nullptr; } return result; } void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from) { if (!trace_drawing) return; for (auto& o: from->children) { SPItem *item = dynamic_cast(&o); if (item && is_a_clone_of(&o, nullptr)) { item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too! } trace_hide_tiled_clones_recursively (&o); } } void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original) { trace_drawing = new Inkscape::Drawing(); /* Create ArenaItem and set transform */ trace_visionkey = SPItem::display_key_new(1); trace_doc = doc; trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY)); // hide the (current) original and any tiled clones, we only want to pick the background original->invoke_hide(trace_visionkey); trace_hide_tiled_clones_recursively(trace_doc->getRoot()); trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); trace_doc->ensureUpToDate(); trace_zoom = zoom; } guint32 CloneTiler::trace_pick(Geom::Rect box) { if (!trace_drawing) { return 0; } trace_drawing->root()->setTransform(Geom::Scale(trace_zoom)); trace_drawing->update(); /* Item integer bbox in points */ Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards(); /* Find visible area */ cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); Inkscape::DrawingContext dc(s, ibox.min()); /* Render */ trace_drawing->render(dc, ibox); double R = 0, G = 0, B = 0, A = 0; ink_cairo_surface_average_color(s, R, G, B, A); cairo_surface_destroy(s); return SP_RGBA32_F_COMPOSE (R, G, B, A); } void CloneTiler::trace_finish() { if (trace_doc) { trace_doc->getRoot()->invoke_hide(trace_visionkey); delete trace_drawing; trace_doc = nullptr; trace_drawing = nullptr; } } void CloneTiler::unclump() { auto selection = getSelection(); if (!selection) return; // check if something is selected if (selection->isEmpty() || boost::distance(selection->items()) > 1) { getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select one object whose tiled clones to unclump.")); return; } auto obj = selection->singleItem(); auto parent = obj->parent; std::vector to_unclump; // not including the original for (auto& child: parent->children) { if (is_a_clone_of (&child, obj)) { to_unclump.push_back((SPItem*)&child); } } getDocument()->ensureUpToDate(); reverse(to_unclump.begin(),to_unclump.end()); ::unclump (to_unclump); DocumentUndo::done(getDocument(), _("Unclump tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); } guint CloneTiler::number_of_clones(SPObject *obj) { SPObject *parent = obj->parent; guint n = 0; for (auto& child: parent->children) { if (is_a_clone_of (&child, obj)) { n ++; } } return n; } void CloneTiler::remove(bool do_undo/* = true*/) { auto selection = getSelection(); if (!selection) return; // check if something is selected if (selection->isEmpty() || boost::distance(selection->items()) > 1) { getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select one object whose tiled clones to remove.")); return; } SPObject *obj = selection->singleItem(); SPObject *parent = obj->parent; // remove old tiling std::vector to_delete; for (auto& child: parent->children) { if (is_a_clone_of (&child, obj)) { to_delete.push_back(&child); } } for (auto obj:to_delete) { g_assert(obj != nullptr); obj->deleteObject(); } change_selection (selection); if (do_undo) { DocumentUndo::done(getDocument(), _("Delete tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); } } Geom::Rect CloneTiler::transform_rect(Geom::Rect const &r, Geom::Affine const &m) { using Geom::X; using Geom::Y; Geom::Point const p1 = r.corner(1) * m; Geom::Point const p2 = r.corner(2) * m; Geom::Point const p3 = r.corner(3) * m; Geom::Point const p4 = r.corner(4) * m; return Geom::Rect( Geom::Point( std::min(std::min(p1[X], p2[X]), std::min(p3[X], p4[X])), std::min(std::min(p1[Y], p2[Y]), std::min(p3[Y], p4[Y]))), Geom::Point( std::max(std::max(p1[X], p2[X]), std::max(p3[X], p4[X])), std::max(std::max(p1[Y], p2[Y]), std::max(p3[Y], p4[Y])))); } /** Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same probability of being displaced. */ double CloneTiler::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... } void CloneTiler::apply() { auto desktop = getDesktop(); auto selection = getSelection(); if (!selection) return; // check if something is selected if (selection->isEmpty()) { desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an object to clone.")); return; } // Check if more than one object is selected. if (boost::distance(selection->items()) > 1) { desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("If you want to clone several objects, group them and clone the group.")); return; } // set "busy" cursor desktop->setWaitingCursor(); // set statusbar text _status->set_markup(_("Creating tiled clones...")); _status->queue_draw(); SPObject *obj = selection->singleItem(); if (!obj) { // Should never happen (empty selection checked above). std::cerr << "CloneTiler::clonetile_apply(): No object in single item selection!!!" << std::endl; return; } Inkscape::XML::Node *obj_repr = obj->getRepr(); const char *id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); SPObject *parent = obj->parent; remove(false); Geom::Scale scale = getDocument()->getDocumentScale().inverse(); double scale_units = scale[Geom::X]; // Use just x direction.... Inkscape::Preferences *prefs = Inkscape::Preferences::get(); double shiftx_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_i", 0, -10000, 10000); double shifty_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_i", 0, -10000, 10000); double shiftx_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_j", 0, -10000, 10000); double shifty_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_j", 0, -10000, 10000); double shiftx_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_rand", 0, 0, 1000); double shifty_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_rand", 0, 0, 1000); double shiftx_exp = prefs->getDoubleLimited(prefs_path + "shiftx_exp", 1, 0, 10); double shifty_exp = prefs->getDoubleLimited(prefs_path + "shifty_exp", 1, 0, 10); bool shiftx_alternate = prefs->getBool(prefs_path + "shiftx_alternate"); bool shifty_alternate = prefs->getBool(prefs_path + "shifty_alternate"); bool shiftx_cumulate = prefs->getBool(prefs_path + "shiftx_cumulate"); bool shifty_cumulate = prefs->getBool(prefs_path + "shifty_cumulate"); bool shiftx_excludew = prefs->getBool(prefs_path + "shiftx_excludew"); bool shifty_excludeh = prefs->getBool(prefs_path + "shifty_excludeh"); double scalex_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_i", 0, -100, 1000); double scaley_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_i", 0, -100, 1000); double scalex_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_j", 0, -100, 1000); double scaley_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_j", 0, -100, 1000); double scalex_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_rand", 0, 0, 1000); double scaley_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_rand", 0, 0, 1000); double scalex_exp = prefs->getDoubleLimited(prefs_path + "scalex_exp", 1, 0, 10); double scaley_exp = prefs->getDoubleLimited(prefs_path + "scaley_exp", 1, 0, 10); double scalex_log = prefs->getDoubleLimited(prefs_path + "scalex_log", 0, 0, 10); double scaley_log = prefs->getDoubleLimited(prefs_path + "scaley_log", 0, 0, 10); bool scalex_alternate = prefs->getBool(prefs_path + "scalex_alternate"); bool scaley_alternate = prefs->getBool(prefs_path + "scaley_alternate"); bool scalex_cumulate = prefs->getBool(prefs_path + "scalex_cumulate"); bool scaley_cumulate = prefs->getBool(prefs_path + "scaley_cumulate"); double rotate_per_i = prefs->getDoubleLimited(prefs_path + "rotate_per_i", 0, -180, 180); double rotate_per_j = prefs->getDoubleLimited(prefs_path + "rotate_per_j", 0, -180, 180); double rotate_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "rotate_rand", 0, 0, 100); bool rotate_alternatei = prefs->getBool(prefs_path + "rotate_alternatei"); bool rotate_alternatej = prefs->getBool(prefs_path + "rotate_alternatej"); bool rotate_cumulatei = prefs->getBool(prefs_path + "rotate_cumulatei"); bool rotate_cumulatej = prefs->getBool(prefs_path + "rotate_cumulatej"); double blur_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_i", 0, 0, 100); double blur_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_j", 0, 0, 100); bool blur_alternatei = prefs->getBool(prefs_path + "blur_alternatei"); bool blur_alternatej = prefs->getBool(prefs_path + "blur_alternatej"); double blur_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_rand", 0, 0, 100); double opacity_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_i", 0, 0, 100); double opacity_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_j", 0, 0, 100); bool opacity_alternatei = prefs->getBool(prefs_path + "opacity_alternatei"); bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej"); double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100); Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color"); double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100); double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100); double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100); double saturation_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_j", 0, -100, 100); double saturation_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_i", 0, -100, 100); double saturation_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_rand", 0, 0, 100); double lightness_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_j", 0, -100, 100); double lightness_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_i", 0, -100, 100); double lightness_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_rand", 0, 0, 100); bool color_alternatej = prefs->getBool(prefs_path + "color_alternatej"); bool color_alternatei = prefs->getBool(prefs_path + "color_alternatei"); int type = prefs->getInt(prefs_path + "symmetrygroup", 0); bool keepbbox = prefs->getBool(prefs_path + "keepbbox", true); int imax = prefs->getInt(prefs_path + "imax", 2); int jmax = prefs->getInt(prefs_path + "jmax", 2); bool fillrect = prefs->getBool(prefs_path + "fillrect"); double fillwidth = scale_units*prefs->getDoubleLimited(prefs_path + "fillwidth", 50, 0, 1e6); double fillheight = scale_units*prefs->getDoubleLimited(prefs_path + "fillheight", 50, 0, 1e6); bool dotrace = prefs->getBool(prefs_path + "dotrace"); int pick = prefs->getInt(prefs_path + "pick"); bool pick_to_presence = prefs->getBool(prefs_path + "pick_to_presence"); bool pick_to_size = prefs->getBool(prefs_path + "pick_to_size"); bool pick_to_color = prefs->getBool(prefs_path + "pick_to_color"); bool pick_to_opacity = prefs->getBool(prefs_path + "pick_to_opacity"); double rand_picked = 0.01 * prefs->getDoubleLimited(prefs_path + "rand_picked", 0, 0, 100); bool invert_picked = prefs->getBool(prefs_path + "invert_picked"); double gamma_picked = prefs->getDoubleLimited(prefs_path + "gamma_picked", 0, -10, 10); SPItem *item = dynamic_cast(obj); if (dotrace) { trace_setup(getDocument(), 1.0, item); } Geom::Point center; double w = 0; double h = 0; double x0 = 0; double y0 = 0; if (keepbbox && obj_repr->attribute("inkscape:tile-w") && obj_repr->attribute("inkscape:tile-h") && obj_repr->attribute("inkscape:tile-x0") && obj_repr->attribute("inkscape:tile-y0") && obj_repr->attribute("inkscape:tile-cx") && obj_repr->attribute("inkscape:tile-cy")) { double cx = obj_repr->getAttributeDouble("inkscape:tile-cx", 0); double cy = obj_repr->getAttributeDouble("inkscape:tile-cy", 0); center = Geom::Point (cx, cy); w = obj_repr->getAttributeDouble("inkscape:tile-w", w); h = obj_repr->getAttributeDouble("inkscape:tile-h", h); x0 = obj_repr->getAttributeDouble("inkscape:tile-x0", x0); y0 = obj_repr->getAttributeDouble("inkscape:tile-y0", y0); } else { bool prefs_bbox = prefs->getBool("/tools/bounding_box", false); SPItem::BBoxType bbox_type = ( !prefs_bbox ? SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX ); Geom::OptRect r = item->documentBounds(bbox_type); if (r) { w = scale_units*r->dimensions()[Geom::X]; h = scale_units*r->dimensions()[Geom::Y]; x0 = scale_units*r->min()[Geom::X]; y0 = scale_units*r->min()[Geom::Y]; center = scale_units*desktop->dt2doc(item->getCenter()); obj_repr->setAttributeSvgDouble("inkscape:tile-cx", center[Geom::X]); obj_repr->setAttributeSvgDouble("inkscape:tile-cy", center[Geom::Y]); obj_repr->setAttributeSvgDouble("inkscape:tile-w", w); obj_repr->setAttributeSvgDouble("inkscape:tile-h", h); obj_repr->setAttributeSvgDouble("inkscape:tile-x0", x0); obj_repr->setAttributeSvgDouble("inkscape:tile-y0", y0); } else { center = Geom::Point(0, 0); w = h = 0; x0 = y0 = 0; } } Geom::Point cur(0, 0); Geom::Rect bbox_original (Geom::Point (x0, y0), Geom::Point (x0 + w, y0 + h)); double perimeter_original = (w + h)/4; // The integers i and j are reserved for tile column and row. // The doubles x and y are used for coordinates for (int i = 0; fillrect? (fabs(cur[Geom::X]) < fillwidth && i < 200) // prevent "freezing" with too large fillrect, arbitrarily limit rows : (i < imax); i ++) { for (int j = 0; fillrect? (fabs(cur[Geom::Y]) < fillheight && j < 200) // prevent "freezing" with too large fillrect, arbitrarily limit cols : (j < jmax); j ++) { // Note: We create a clone at 0,0 too, right over the original, in case our clones are colored // Get transform from symmetry, shift, scale, rotation Geom::Affine orig_t = get_transform (type, i, j, center[Geom::X], center[Geom::Y], w, h, shiftx_per_i, shifty_per_i, shiftx_per_j, shifty_per_j, shiftx_rand, shifty_rand, shiftx_exp, shifty_exp, shiftx_alternate, shifty_alternate, shiftx_cumulate, shifty_cumulate, shiftx_excludew, shifty_excludeh, scalex_per_i, scaley_per_i, scalex_per_j, scaley_per_j, scalex_rand, scaley_rand, scalex_exp, scaley_exp, scalex_log, scaley_log, scalex_alternate, scaley_alternate, scalex_cumulate, scaley_cumulate, rotate_per_i, rotate_per_j, rotate_rand, rotate_alternatei, rotate_alternatej, rotate_cumulatei, rotate_cumulatej ); Geom::Affine parent_transform = (((SPItem*)item->parent)->i2doc_affine())*(item->document->getRoot()->c2p.inverse()); Geom::Affine t = parent_transform*orig_t*parent_transform.inverse(); cur = center * t - center; if (fillrect) { if ((cur[Geom::X] > fillwidth) || (cur[Geom::Y] > fillheight)) { // off limits continue; } } gchar color_string[32]; *color_string = 0; // Color tab if (!initial_color.empty()) { guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff); float hsl[3]; SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); double eff_i = (color_alternatei? (i%2) : (i)); double eff_j = (color_alternatej? (j%2) : (j)); hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1); double notused; hsl[0] = modf( hsl[0], ¬used ); // Restrict to 0-1 hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1); hsl[1] = CLAMP (hsl[1], 0, 1); hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1); hsl[2] = CLAMP (hsl[2], 0, 1); float rgb[3]; SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]); sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); } // Blur double blur = 0.0; { int eff_i = (blur_alternatei? (i%2) : (i)); int eff_j = (blur_alternatej? (j%2) : (j)); blur = (blur_per_i * eff_i + blur_per_j * eff_j + blur_rand * g_random_double_range (-1, 1)); blur = CLAMP (blur, 0, 1); } // Opacity double opacity = 1.0; { int eff_i = (opacity_alternatei? (i%2) : (i)); int eff_j = (opacity_alternatej? (j%2) : (j)); opacity = 1 - (opacity_per_i * eff_i + opacity_per_j * eff_j + opacity_rand * g_random_double_range (-1, 1)); opacity = CLAMP (opacity, 0, 1); } // Trace tab if (dotrace) { Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units)); guint32 rgba = trace_pick (bbox_t); 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); 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 (r, power); g = pow (g, power); b = pow (b, 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_presence) { if (g_random_double_range (0, 1) > val) { continue; // skip! } } if (pick_to_size) { t = parent_transform * Geom::Translate(-center[Geom::X], -center[Geom::Y]) * Geom::Scale (val, val) * Geom::Translate(center[Geom::X], center[Geom::Y]) * parent_transform.inverse() * t; } if (pick_to_opacity) { opacity *= val; } if (pick_to_color) { sp_svg_write_color(color_string, sizeof(color_string), rgba); } } if (opacity < 1e-6) { // invisibly transparent, skip continue; } if (fabs(t[0]) + fabs (t[1]) + fabs(t[2]) + fabs(t[3]) < 1e-6) { // too small, skip continue; } // Create the clone Inkscape::XML::Node *clone = obj_repr->document()->createElement("svg:use"); clone->setAttribute("x", "0"); clone->setAttribute("y", "0"); clone->setAttribute("inkscape:tiled-clone-of", id_href); clone->setAttribute("xlink:href", id_href); Geom::Point new_center; bool center_set = false; if (obj_repr->attribute("inkscape:transform-center-x") || obj_repr->attribute("inkscape:transform-center-y")) { new_center = scale_units*desktop->dt2doc(item->getCenter()) * orig_t; center_set = true; } clone->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(t)); if (opacity < 1.0) { clone->setAttributeCssDouble("opacity", opacity); } if (*color_string) { clone->setAttribute("fill", color_string); clone->setAttribute("stroke", color_string); } // add the new clone to the top of the original's parent parent->getRepr()->appendChild(clone); if (blur > 0.0) { SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); SPItem *item = dynamic_cast(clone_object); double radius = blur * perimeter_original * t.descrim(); // this is necessary for all newly added clones to have correct bboxes, // otherwise filters won't work: desktop->getDocument()->ensureUpToDate(); SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim()); constructed->update_filter_region(item); sp_style_set_property_url (clone_object, "filter", constructed, false); } if (center_set) { SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); SPItem *item = dynamic_cast(clone_object); if (clone_object && item) { clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); item->setCenter(desktop->doc2dt(new_center)); clone_object->updateRepr(); } } Inkscape::GC::release(clone); } cur[Geom::Y] = 0; } if (dotrace) { trace_finish (); } change_selection(selection); desktop->clearWaitingCursor(); DocumentUndo::done(getDocument(), _("Create tiled clones"), INKSCAPE_ICON("dialog-tile-clones")); } Gtk::Box * CloneTiler::new_tab(Gtk::Notebook *nb, const gchar *label) { auto l = Gtk::manage(new Gtk::Label(label, true)); auto vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, VB_MARGIN)); vb->set_homogeneous(false); vb->set_border_width(VB_MARGIN); nb->append_page(*vb, *l); return vb; } void CloneTiler::checkbox_toggled(Gtk::ToggleButton *tb, const Glib::ustring &attr) { auto prefs = Inkscape::Preferences::get(); prefs->setBool(prefs_path + attr, tb->get_active()); } Gtk::Widget * CloneTiler::checkbox(const char *tip, const Glib::ustring &attr) { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); auto b = Gtk::manage(new UI::Widget::CheckButtonInternal()); b->set_tooltip_text(tip); auto const prefs = Inkscape::Preferences::get(); auto const value = prefs->getBool(prefs_path + attr); b->set_active(value); hb->pack_start(*b, false, true); b->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::checkbox_toggled), b, attr)); b->set_uncheckable(); return hb; } void CloneTiler::value_changed(Glib::RefPtr &adj, Glib::ustring const &pref) { auto prefs = Inkscape::Preferences::get(); prefs->setDouble(prefs_path + pref, adj->get_value()); } Gtk::Widget * CloneTiler::spinbox(const char *tip, const Glib::ustring &attr, double lower, double upper, const gchar *suffix, bool exponent/* = false*/) { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); { // Parameters for adjustment auto const initial_value = (exponent ? 1.0 : 0.0); auto const step_increment = (exponent ? 0.01 : 0.1); auto const page_increment = (exponent ? 0.05 : 0.4); auto a = Gtk::Adjustment::create(initial_value, lower, upper, step_increment, page_increment); auto const climb_rate = (exponent ? 0.01 : 0.1); auto const digits = (exponent ? 2 : 1); auto sb = new Inkscape::UI::Widget::SpinButton(a, climb_rate, digits); sb->set_tooltip_text (tip); sb->set_width_chars (5); sb->set_digits(3); hb->pack_start(*sb, false, false, SB_MARGIN); auto prefs = Inkscape::Preferences::get(); auto value = prefs->getDoubleLimited(prefs_path + attr, exponent? 1.0 : 0.0, lower, upper); a->set_value (value); a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::value_changed), a, attr)); if (exponent) { sb->set_oneable(); } else { sb->set_zeroable(); } } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(suffix); hb->pack_start(*l); } return hb; } void CloneTiler::symgroup_changed(Gtk::ComboBox *cb) { auto prefs = Inkscape::Preferences::get(); auto group_new = cb->get_active_row_number(); prefs->setInt(prefs_path + "symmetrygroup", group_new); } void CloneTiler::xy_changed(Glib::RefPtr &adj, Glib::ustring const &pref) { auto prefs = Inkscape::Preferences::get(); prefs->setInt(prefs_path + pref, (int) floor(adj->get_value() + 0.5)); } void CloneTiler::keep_bbox_toggled() { auto prefs = Inkscape::Preferences::get(); prefs->setBool(prefs_path + "keepbbox", _cb_keep_bbox->get_active()); } void CloneTiler::pick_to(Gtk::ToggleButton *tb, Glib::ustring const &pref) { auto prefs = Inkscape::Preferences::get(); prefs->setBool(prefs_path + pref, tb->get_active()); } void CloneTiler::reset_recursive(Gtk::Widget *w) { if (w) { auto sb = dynamic_cast(w); auto tb = dynamic_cast(w); { if (sb && sb->get_zeroable()) { // spinbutton auto a = sb->get_adjustment(); a->set_value(0); } } { if (sb && sb->get_oneable()) { // spinbutton auto a = sb->get_adjustment(); a->set_value(1); } } { if (tb && tb->get_uncheckable()) { // checkbox tb->set_active(false); } } } auto container = dynamic_cast(w); if (container) { auto c = container->get_children(); for (auto i : c) { reset_recursive(i); } } } void CloneTiler::reset() { reset_recursive(this); } void CloneTiler::table_attach(Gtk::Grid *table, Gtk::Widget *widget, float align, int row, int col) { widget->set_halign(Gtk::ALIGN_FILL); widget->set_valign(Gtk::ALIGN_CENTER); table->attach(*widget, col, row, 1, 1); } Gtk::Grid * CloneTiler::table_x_y_rand(int values) { auto table = Gtk::manage(new Gtk::Grid()); table->set_row_spacing(6); table->set_column_spacing(8); table->set_border_width(VB_MARGIN); { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); hb->set_homogeneous(false); auto i = Glib::wrap(sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU)); hb->pack_start(*i, false, false, 2); auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Per row:")); hb->pack_start(*l, false, false, 2); table_attach(table, hb, 0, 1, 2); } { auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); hb->set_homogeneous(false); auto i = Glib::wrap(sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU)); hb->pack_start(*i, false, false, 2); auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Per column:")); hb->pack_start(*l, false, false, 2); table_attach(table, hb, 0, 1, 3); } { auto l = Gtk::manage(new Gtk::Label("")); l->set_markup(_("Randomize:")); table_attach(table, l, 0, 1, 4); } return table; } void CloneTiler::pick_switched(PickType v) { auto prefs = Inkscape::Preferences::get(); prefs->setInt(prefs_path + "pick", v); } void CloneTiler::switch_to_create() { for (auto w : _rowscols) { w->set_sensitive(true); } for (auto w : _widthheight) { w->set_sensitive(false); } auto prefs = Inkscape::Preferences::get(); prefs->setBool(prefs_path + "fillrect", false); } void CloneTiler::switch_to_fill() { for (auto w : _rowscols) { w->set_sensitive(false); } for (auto w : _widthheight) { w->set_sensitive(true); } auto prefs = Inkscape::Preferences::get(); prefs->setBool(prefs_path + "fillrect", true); } void CloneTiler::fill_width_changed() { auto const raw_dist = fill_width->get_value(); auto const unit = unit_menu->getUnit(); auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); auto prefs = Inkscape::Preferences::get(); prefs->setDouble(prefs_path + "fillwidth", pixels); } void CloneTiler::fill_height_changed() { auto const raw_dist = fill_height->get_value(); auto const unit = unit_menu->getUnit(); auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); auto prefs = Inkscape::Preferences::get(); prefs->setDouble(prefs_path + "fillheight", pixels); } void CloneTiler::unit_changed() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); gdouble width_pixels = prefs->getDouble(prefs_path + "fillwidth"); gdouble height_pixels = prefs->getDouble(prefs_path + "fillheight"); Inkscape::Util::Unit const *unit = unit_menu->getUnit(); gdouble width_value = Inkscape::Util::Quantity::convert(width_pixels, "px", unit); gdouble height_value = Inkscape::Util::Quantity::convert(height_pixels, "px", unit); fill_width->set_value(width_value); fill_height->set_value(height_value); } void CloneTiler::do_pick_toggled() { auto prefs = Inkscape::Preferences::get(); auto active = _b->get_active(); prefs->setBool(prefs_path + "dotrace", active); if (_dotrace) { _dotrace->set_sensitive(active); } } void CloneTiler::show_page_trace() { nb->set_current_page(6); _b->set_active(false); } } } } /* 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 :