// SPDX-License-Identifier: GPL-2.0-or-later /** * @brief Arranges Objects into a Circle/Ellipse */ /* Authors: * Declara Denis * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include #include <2geom/transforms.h> #include "desktop.h" #include "document-undo.h" #include "document.h" #include "inkscape.h" #include "preferences.h" #include "object/sp-ellipse.h" #include "object/sp-item-transform.h" #include "ui/dialog/polar-arrange-tab.h" #include "ui/dialog/tile.h" #include "ui/icon-names.h" namespace Inkscape { namespace UI { namespace Dialog { PolarArrangeTab::PolarArrangeTab(ArrangeDialog *parent_) : parent(parent_), parametersTable(), centerY("", C_("Polar arrange tab", "Y coordinate of the center"), UNIT_TYPE_LINEAR), centerX("", C_("Polar arrange tab", "X coordinate of the center"), centerY), radiusY("", C_("Polar arrange tab", "Y coordinate of the radius"), UNIT_TYPE_LINEAR), radiusX("", C_("Polar arrange tab", "X coordinate of the radius"), radiusY), angleY("", C_("Polar arrange tab", "Ending angle"), UNIT_TYPE_RADIAL), angleX("", C_("Polar arrange tab", "Starting angle"), angleY) { anchorPointLabel.set_text(C_("Polar arrange tab", "Anchor point:")); anchorPointLabel.set_halign(Gtk::ALIGN_START); pack_start(anchorPointLabel, false, false); anchorBoundingBoxRadio.set_label(C_("Polar arrange tab", "Objects' bounding boxes:")); anchorRadioGroup = anchorBoundingBoxRadio.get_group(); anchorBoundingBoxRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); pack_start(anchorBoundingBoxRadio, false, false); pack_start(anchorSelector, false, false); anchorObjectPivotRadio.set_label(C_("Polar arrange tab", "Objects' rotational centers")); anchorObjectPivotRadio.set_group(anchorRadioGroup); anchorObjectPivotRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); pack_start(anchorObjectPivotRadio, false, false); arrangeOnLabel.set_text(C_("Polar arrange tab", "Arrange on:")); arrangeOnLabel.set_halign(Gtk::ALIGN_START); pack_start(arrangeOnLabel, false, false); arrangeOnFirstCircleRadio.set_label(C_("Polar arrange tab", "First selected circle/ellipse/arc")); arrangeRadioGroup = arrangeOnFirstCircleRadio.get_group(); arrangeOnFirstCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); pack_start(arrangeOnFirstCircleRadio, false, false); arrangeOnLastCircleRadio.set_label(C_("Polar arrange tab", "Last selected circle/ellipse/arc")); arrangeOnLastCircleRadio.set_group(arrangeRadioGroup); arrangeOnLastCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); pack_start(arrangeOnLastCircleRadio, false, false); arrangeOnParametersRadio.set_label(C_("Polar arrange tab", "Parameterized:")); arrangeOnParametersRadio.set_group(arrangeRadioGroup); arrangeOnParametersRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); pack_start(arrangeOnParametersRadio, false, false); centerLabel.set_text(C_("Polar arrange tab", "Center X/Y:")); parametersTable.attach(centerLabel, 0, 0, 1, 1); centerX.setDigits(2); centerX.setIncrements(0.2, 0); centerX.setRange(-10000, 10000); centerX.setValue(0, "px"); centerY.setDigits(2); centerY.setIncrements(0.2, 0); centerY.setRange(-10000, 10000); centerY.setValue(0, "px"); parametersTable.attach(centerX, 1, 0, 1, 1); parametersTable.attach(centerY, 2, 0, 1, 1); radiusLabel.set_text(C_("Polar arrange tab", "Radius X/Y:")); parametersTable.attach(radiusLabel, 0, 1, 1, 1); radiusX.setDigits(2); radiusX.setIncrements(0.2, 0); radiusX.setRange(0.001, 10000); radiusX.setValue(100, "px"); radiusY.setDigits(2); radiusY.setIncrements(0.2, 0); radiusY.setRange(0.001, 10000); radiusY.setValue(100, "px"); parametersTable.attach(radiusX, 1, 1, 1, 1); parametersTable.attach(radiusY, 2, 1, 1, 1); angleLabel.set_text(_("Angle X/Y:")); parametersTable.attach(angleLabel, 0, 2, 1, 1); angleX.setDigits(2); angleX.setIncrements(0.2, 0); angleX.setRange(-10000, 10000); angleX.setValue(0, "°"); angleY.setDigits(2); angleY.setIncrements(0.2, 0); angleY.setRange(-10000, 10000); angleY.setValue(180, "°"); parametersTable.attach(angleX, 1, 2, 1, 1); parametersTable.attach(angleY, 2, 2, 1, 1); parametersTable.set_row_spacing(4); parametersTable.set_column_spacing(4); pack_start(parametersTable, false, false); rotateObjectsCheckBox.set_label(_("Rotate objects")); rotateObjectsCheckBox.set_active(true); pack_start(rotateObjectsCheckBox, false, false); centerX.set_sensitive(false); centerY.set_sensitive(false); angleX.set_sensitive(false); angleY.set_sensitive(false); radiusX.set_sensitive(false); radiusY.set_sensitive(false); set_border_width(4); parametersTable.show_all(); parametersTable.set_no_show_all(); parametersTable.hide(); } /** * This function rotates an item around a given point by a given amount * @param item item to rotate * @param center center of the rotation to perform * @param rotation amount to rotate the object by */ static void rotateAround(SPItem *item, Geom::Point center, Geom::Rotate const &rotation) { Geom::Translate const s(center); Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); // Save old center center = item->getCenter(); item->set_i2d_affine(item->i2dt_affine() * affine); item->doWriteTransform(item->transform); if(item->isCenterSet()) { item->setCenter(center * affine); item->updateRepr(); } } /** * Calculates the angle at which to put an object given the total amount * of objects, the index of the objects as well as the arc start and end * points * @param arcBegin angle at which the arc begins * @param arcEnd angle at which the arc ends * @param count number of objects in the selection * @param n index of the object in the selection */ static float calcAngle(float arcBegin, float arcEnd, int count, int n) { float arcLength = arcEnd - arcBegin; float delta = std::abs(std::abs(arcLength) - 2*M_PI); if(delta > 0.01) count--; // If not a complete circle, put an object also at the extremes of the arc; float angle = n / (float)count; // Normalize for arcLength: angle = angle * arcLength; angle += arcBegin; return angle; } /** * Calculates the point at which an object needs to be, given the center of the ellipse, * it's radius (x and y), as well as the angle */ static Geom::Point calcPoint(float cx, float cy, float rx, float ry, float angle) { return Geom::Point(cx + cos(angle) * rx, cy + sin(angle) * ry); } /** * Returns the selected anchor point in desktop coordinates. If anchor * is 0 to 8, then a bounding box point has been chosen. If it is 9 however * the rotational center is chosen. */ static Geom::Point getAnchorPoint(int anchor, SPItem *item) { Geom::Point source; Geom::OptRect bbox = item->documentVisualBounds(); switch(anchor) { case 0: // Top - Left case 3: // Middle - Left case 6: // Bottom - Left source[0] = bbox->min()[Geom::X]; break; case 1: // Top - Middle case 4: // Middle - Middle case 7: // Bottom - Middle source[0] = (bbox->min()[Geom::X] + bbox->max()[Geom::X]) / 2.0f; break; case 2: // Top - Right case 5: // Middle - Right case 8: // Bottom - Right source[0] = bbox->max()[Geom::X]; break; }; switch(anchor) { case 0: // Top - Left case 1: // Top - Middle case 2: // Top - Right source[1] = bbox->min()[Geom::Y]; break; case 3: // Middle - Left case 4: // Middle - Middle case 5: // Middle - Right source[1] = (bbox->min()[Geom::Y] + bbox->max()[Geom::Y]) / 2.0f; break; case 6: // Bottom - Left case 7: // Bottom - Middle case 8: // Bottom - Right source[1] = bbox->max()[Geom::Y]; break; }; // If using center if(anchor == 9) source = item->getCenter(); else { source *= item->document->doc2dt(); } return source; } /** * Moves an SPItem to a given location, the location is based on the given anchor point. * @param anchor 0 to 8 are the various bounding box points like follows: * 0 1 2 * 3 4 5 * 6 7 8 * Anchor mode 9 is the rotational center of the object * @param item Item to move * @param p point at which to move the object */ static void moveToPoint(int anchor, SPItem *item, Geom::Point p) { item->move_rel(Geom::Translate(p - getAnchorPoint(anchor, item))); } void PolarArrangeTab::arrange() { Inkscape::Selection *selection = parent->getDesktop()->getSelection(); const std::vector tmp(selection->items().begin(), selection->items().end()); SPGenericEllipse *referenceEllipse = nullptr; // Last ellipse in selection bool arrangeOnEllipse = !arrangeOnParametersRadio.get_active(); bool arrangeOnFirstEllipse = arrangeOnEllipse && arrangeOnFirstCircleRadio.get_active(); float yaxisdir = parent->getDesktop()->yaxisdir(); int count = 0; for(auto item : tmp) { if(arrangeOnEllipse) { if(!arrangeOnFirstEllipse) { if(SP_IS_GENERICELLIPSE(item)) referenceEllipse = SP_GENERICELLIPSE(item); } else { if(SP_IS_GENERICELLIPSE(item) && referenceEllipse == nullptr) referenceEllipse = SP_GENERICELLIPSE(item); } } ++count; } float cx, cy; // Center of the ellipse float rx, ry; // Radiuses of the ellipse in x and y direction float arcBeg, arcEnd; // begin and end angles for arcs Geom::Affine transformation; // Any additional transformation to apply to the objects if(arrangeOnEllipse) { if(referenceEllipse == nullptr) { Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true); dialog.run(); return; } else { cx = referenceEllipse->cx.value; cy = referenceEllipse->cy.value; rx = referenceEllipse->rx.value; ry = referenceEllipse->ry.value; arcBeg = referenceEllipse->start; arcEnd = referenceEllipse->end; transformation = referenceEllipse->i2dt_affine(); // We decrement the count by 1 as we are not going to lay // out the reference ellipse --count; } } else { // Read options from UI cx = centerX.getValue("px"); cy = centerY.getValue("px"); rx = radiusX.getValue("px"); ry = radiusY.getValue("px"); arcBeg = angleX.getValue("rad"); arcEnd = angleY.getValue("rad") * yaxisdir; transformation.setIdentity(); referenceEllipse = nullptr; } int anchor = 9; if(anchorBoundingBoxRadio.get_active()) { anchor = anchorSelector.getHorizontalAlignment() + anchorSelector.getVerticalAlignment() * 3; } Geom::Point realCenter = Geom::Point(cx, cy) * transformation; int i = 0; for(auto item : tmp) { // Ignore the reference ellipse if any if(item != referenceEllipse) { float angle = calcAngle(arcBeg, arcEnd, count, i); Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation; moveToPoint(anchor, item, newLocation); if(rotateObjectsCheckBox.get_active()) { // Calculate the angle by which to rotate each object angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y())); rotateAround(item, newLocation, Geom::Rotate(angle)); } ++i; } } DocumentUndo::done(parent->getDesktop()->getDocument(), _("Arrange on ellipse"), INKSCAPE_ICON("dialog-align-and-distribute")); } void PolarArrangeTab::updateSelection() { } void PolarArrangeTab::on_arrange_radio_changed() { bool arrangeParametric = arrangeOnParametersRadio.get_active(); centerX.set_sensitive(arrangeParametric); centerY.set_sensitive(arrangeParametric); angleX.set_sensitive(arrangeParametric); angleY.set_sensitive(arrangeParametric); radiusX.set_sensitive(arrangeParametric); radiusY.set_sensitive(arrangeParametric); parametersTable.set_visible(arrangeParametric); } void PolarArrangeTab::on_anchor_radio_changed() { bool anchorBoundingBox = anchorBoundingBoxRadio.get_active(); anchorSelector.set_sensitive(anchorBoundingBox); } } //namespace Dialog } //namespace UI } //namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :