diff --git a/.gitignore b/.gitignore index 100b5bd7257804cd7da84baf28ac3fad7abbfcf1..99808bb1b5b5092f39a1e0fe8f9d6da0c6a36362 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,8 @@ tags # YCM code completer .ycm_* + +# Ignore build and install files +install-prefix/ +bin/ +lib/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e1cd54d54db39245dcdcadb4916229bc74123b28..2e9d4762dbbca364a800af77a3df524d81706503 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -100,7 +100,6 @@ inkscape:linux: paths: - build/ - appimage:linux: stage: build timeout: 3h @@ -212,6 +211,7 @@ inkscape:windows: #tests, always run after building test:linux: stage: test + timeout: 3h rules: - *do_not_run_for_schedules - *run_otherwise diff --git a/share/ui/toolbar-rect.ui b/share/ui/toolbar-rect.ui index 69902e957e4c76dc99e4dfe34e9171b2c5c6c9f9..9bea068b08ca35ff87cc65d23698f8df95ebff0e 100644 --- a/share/ui/toolbar-rect.ui +++ b/share/ui/toolbar-rect.ui @@ -52,6 +52,20 @@ + + + center + True + True + False + Lock width and height proportions + + + object-unlocked + + + + Height of rectangle @@ -96,6 +110,20 @@ + + + center + True + True + False + Lock Rx and Ry proportions + + + object-unlocked + + + + Vertical radius of rounded corners diff --git a/src/attributes.cpp b/src/attributes.cpp index 54cd18c4284084c43b10a3e99a3ead6e15b6f960..99525fe6226419cd2b12f53a018282791843d554 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -598,6 +598,12 @@ static SPStyleProp const props[] = { // named view settings {SPAttr::INKSCAPE_ORIGIN_CORRECTION, "origin-correction"}, {SPAttr::INKSCAPE_Y_AXIS_DOWN, "y-axis-down"}, + + // rectangle lock aspect ratio state + {SPAttr::INKSCAPE_LOCK_WH, "inkscape:lock-wh"}, + {SPAttr::INKSCAPE_ASPECT_RATIO_WH, "inkscape:aspect-ratio-wh"}, + {SPAttr::INKSCAPE_LOCK_RXY, "inkscape:lock-rxy"}, + {SPAttr::INKSCAPE_ASPECT_RATIO_RXY, "inkscape:aspect-ratio-rxy"}, }; #define n_attrs (sizeof(props) / sizeof(props[0])) diff --git a/src/attributes.h b/src/attributes.h index 85eb0b86177a057a3d97ad1c387e7dbd2c9c1794..05b56ec4f9e934ed7fc7fc65e65e1baa431906c9 100644 --- a/src/attributes.h +++ b/src/attributes.h @@ -600,8 +600,16 @@ enum class SPAttr { INKSCAPE_ORIGIN_CORRECTION, INKSCAPE_Y_AXIS_DOWN, + + // rectangle lock aspect ratio state + INKSCAPE_LOCK_WH, + INKSCAPE_ASPECT_RATIO_WH, + INKSCAPE_LOCK_RXY, + INKSCAPE_ASPECT_RATIO_RXY, + // sentinel - SPAttr_SIZE + SPAttr_SIZE, + }; /** diff --git a/src/object/sp-rect.cpp b/src/object/sp-rect.cpp index def363af991feaf73f1f7495b848126e657be7b1..c6dfca44780966008a8572e753f3659ac9e43e89 100644 --- a/src/object/sp-rect.cpp +++ b/src/object/sp-rect.cpp @@ -32,7 +32,7 @@ //#define OBJECT_TRACE SPRect::SPRect() : SPShape() - ,type(SP_GENERIC_RECT_UNDEFINED) + ,type(SP_GENERIC_RECT_UNDEFINED), lock_wh(false), lock_rxy(false) { } @@ -67,6 +67,12 @@ void SPRect::build(SPDocument* doc, Inkscape::XML::Node* repr) { this->readAttr(SPAttr::RX); this->readAttr(SPAttr::RY); + // Read custom attributes for lock state and aspect ratio + this->readAttr(SPAttr::INKSCAPE_LOCK_WH); + this->readAttr(SPAttr::INKSCAPE_ASPECT_RATIO_WH); + this->readAttr(SPAttr::INKSCAPE_LOCK_RXY); + this->readAttr(SPAttr::INKSCAPE_ASPECT_RATIO_RXY); + #ifdef OBJECT_TRACE objectTrace( "SPRect::build", false ); #endif @@ -132,7 +138,24 @@ void SPRect::set(SPAttr key, gchar const *value) { this->ry.update( em, ex, h ); this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); break; - + case SPAttr::INKSCAPE_LOCK_WH: + lock_wh.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_ASPECT_RATIO_WH: + aspect_ratio_wh = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_wh <= 0) aspect_ratio_wh = 1.0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_LOCK_RXY: + lock_rxy.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SPAttr::INKSCAPE_ASPECT_RATIO_RXY: + aspect_ratio_rxy = value ? g_ascii_strtod(value, nullptr) : 1.0; + if (aspect_ratio_rxy <= 0) aspect_ratio_rxy = 1.0; + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; default: SPShape::set(key, value); break; @@ -223,6 +246,23 @@ Inkscape::XML::Node * SPRect::write(Inkscape::XML::Document *xml_doc, Inkscape:: repr->setAttributeSvgLength("x", this->x); repr->setAttributeSvgLength("y", this->y); + + // Write custom attributes + if (lock_wh) { + repr->setAttribute("inkscape:lock-wh", lock_wh ? "true" : "false"); + repr->setAttributeSvgDouble("inkscape:aspect-ratio-wh", this->aspect_ratio_wh); + } else { + repr->removeAttribute("inkscape:lock-wh"); + repr->removeAttribute("inkscape:aspect-ratio-wh"); + } + if (lock_rxy) { + repr->setAttribute("inkscape:lock-rxy", lock_rxy ? "true" : "false"); + repr->setAttributeSvgDouble("inkscape:aspect-ratio-rxy", this->aspect_ratio_rxy); + } else { + repr->removeAttribute("inkscape:lock-rxy"); + repr->removeAttribute("inkscape:aspect-ratio-rxy"); + } + // write d= if (type == SP_GENERIC_PATH) { set_rect_path_attribute(repr); // include set_shape() @@ -647,6 +687,38 @@ void SPRect::convert_to_guides() const { sp_guide_pt_pairs_to_guides(this->document, pts); } +void SPRect::setLockWh(bool lock) { + lock_wh = lock; +} + +bool SPRect::getLockWh() const { + return lock_wh; +} + +void SPRect::setLockRxy(bool lock) { + lock_rxy = lock; +} + +bool SPRect::getLockRxy() const { + return lock_rxy; +} + +void SPRect::setAspectRatioWh(double ratio) { + aspect_ratio_wh = ratio; +} + +double SPRect::getAspectRatioWh() const { + return aspect_ratio_wh; +} + +void SPRect::setAspectRatioRxy(double ratio) { + aspect_ratio_rxy = ratio; +} + +double SPRect::getAspectRatioRxy() const { + return aspect_ratio_rxy; +} + /* Local Variables: mode:c++ diff --git a/src/object/sp-rect.h b/src/object/sp-rect.h index f157e81e10cd8f873f3da83f180187fa88ad91fd..d3e4b2f7639c59fc00795ab057080a09e28b9c6a 100644 --- a/src/object/sp-rect.h +++ b/src/object/sp-rect.h @@ -18,6 +18,7 @@ #include <2geom/forward.h> #include "svg/svg-length.h" +#include "svg/svg-bool.h" #include "sp-shape.h" enum GenericRectType { @@ -54,6 +55,20 @@ public: double getVisibleHeight() const; void setVisibleHeight(double ry); + bool getLockWh() const; + void setLockWh(bool lock); + + bool getLockRxy() const; + void setLockRxy(bool lock); + + double getAspectRatioWh() const; + void setAspectRatioWh(double ratio); + + double getAspectRatioRxy() const; + void setAspectRatioRxy(double ratio); + + + void compensateRxRy(Geom::Affine xform); void build(SPDocument* doc, Inkscape::XML::Node* repr) override; @@ -79,6 +94,10 @@ public: SVGLength height; SVGLength rx; SVGLength ry; + SVGBool lock_wh{false}; // Default false, unset + SVGBool lock_rxy{false}; // Default false, unset + double aspect_ratio_wh=0.0; + double aspect_ratio_rxy=0.0; private: static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp index 77cb2133336e791af341290bf22b512b6361f51b..9b6b6595d556e25dcd04c6b82ba657598cfb939e 100644 --- a/src/ui/shape-editor-knotholders.cpp +++ b/src/ui/shape-editor-knotholders.cpp @@ -245,7 +245,15 @@ RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*orig if (state & GDK_CONTROL_MASK) { gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; rect->rx = rect->ry = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, temp); - } else { + }else if(rect->getLockRxy()&& rect->ry._set) + { + double new_rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0); + rect->rx = new_rx; + rect->ry = new_rx * rect->getAspectRatioRxy(); + rect->ry = MIN(rect->ry.computed, rect->height.computed / 2.0); + + } + else { rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0); } @@ -295,7 +303,14 @@ RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*orig // resulting in a perfect circle (and not an ellipse) gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; rect->rx = rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, temp); - } else { + }else if(rect->getLockRxy()&& rect->rx._set) + { + double new_ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, rect->height.computed / 2.0); + rect->ry = new_ry; + rect->rx = new_ry / rect->getAspectRatioRxy(); + rect->rx = MIN(rect->rx.computed, rect->width.computed / 2.0); + } + else { if (!rect->rx._set || rect->rx.computed == 0) { rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, @@ -357,6 +372,8 @@ RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &or g_assert(rect != nullptr); Geom::Point s = p; + double old_width = rect->width.computed; + double old_height = rect->height.computed; if (state & GDK_CONTROL_MASK) { // original width/height when drag started @@ -412,7 +429,29 @@ RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &or } - } else { + } else if (rect->getLockWh()){ + + s = snap_knot_position(p, state); + double new_width = MAX(s[Geom::X] - rect->x.computed, 0); + double new_height = MAX(s[Geom::Y] - rect->y.computed, 0); + + + double aspect = rect->getAspectRatioWh(); // height / width + bool width_changed = fabs(new_width - old_width) > 1e-6; + bool height_changed = fabs(new_height - old_height) > 1e-6; + + if (width_changed && !height_changed) { + new_height = new_width * aspect; + } else if (height_changed && !width_changed) { + new_width = new_height / aspect; + } else if (width_changed && height_changed) { + new_height = new_width * aspect; // Prioritize width for corner drag + } + + rect->width = new_width; + rect->height = new_height; + } + else { // move freely s = snap_knot_position(p, state); rect->width = MAX(s[Geom::X] - rect->x.computed, 0); diff --git a/src/ui/toolbar/rect-toolbar.cpp b/src/ui/toolbar/rect-toolbar.cpp index 4887e6078d8eb2aa2a96a2590862ef0577ccadbc..872cb1965c441c4ffe9940efb88add9717c18d23 100644 --- a/src/ui/toolbar/rect-toolbar.cpp +++ b/src/ui/toolbar/rect-toolbar.cpp @@ -27,12 +27,16 @@ #include "rect-toolbar.h" +#include + #include #include #include -#include +#include #include #include +#include +#include #include "desktop.h" #include "document-undo.h" @@ -47,6 +51,7 @@ #include "ui/widget/spinbutton.h" #include "ui/widget/unit-tracker.h" #include "widgets/widget-sizes.h" +#include "preferences.h" using Inkscape::UI::Widget::UnitTracker; using Inkscape::DocumentUndo; @@ -85,12 +90,19 @@ RectToolbar::RectToolbar(Glib::RefPtr const &builder) , _height_item{UI::get_derived_widget(builder, "_height_item", "height", &SPRect::getVisibleHeight, &SPRect::setVisibleHeight)} , _rx_item{UI::get_derived_widget(builder, "_rx_item", "rx", &SPRect::getVisibleRx, &SPRect::setVisibleRx)} , _ry_item{UI::get_derived_widget(builder, "_ry_item", "ry", &SPRect::getVisibleRy, &SPRect::setVisibleRy)} + , _lock_wh_button{get_widget(builder, "_lock_wh_button")} + , _lock_rxy_button{get_widget(builder, "_lock_rxy_button")} { + auto prefs = Inkscape::Preferences::get(); auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); get_widget(builder, "unit_menu_box").append(*unit_menu); _not_rounded.signal_clicked().connect([this] { _setDefaults(); }); + // Configure the lock buttons + _lock_wh_button.signal_toggled().connect([this] { toggle_lock_wh(); }); + _lock_rxy_button.signal_toggled().connect([this] { toggle_lock_rxy(); }); + for (auto sb : _getDerivedSpinButtons()) { auto const adj = sb->get_adjustment(); auto const path = Glib::ustring{"/tools/shapes/rect/"} + sb->name; @@ -213,20 +225,52 @@ void RectToolbar::_valueChanged(DerivedSpinButton &btn) auto guard = _blocker.block(); auto const adj = btn.get_adjustment(); + double value = Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px"); + // save the new value to preferences if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { auto const path = Glib::ustring{"/tools/shapes/rect/"} + btn.name; - Preferences::get()->setDouble(path, Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px")); + Preferences::get()->setDouble(path, value); } bool modified = false; for (auto item : _desktop->getSelection()->items()) { if (auto rect = cast(item)) { - if (adj->get_value() != 0) { - (rect->*btn.setter)(Quantity::convert(adj->get_value(), _tracker->getActiveUnit(), "px")); + double new_value = value; + double paired_value = 0.0; + + if (&btn == &_width_item && rect->getLockWh()) { + paired_value = new_value * rect->getAspectRatioWh(); + _height_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleHeight(paired_value); + } else if (&btn == &_height_item && rect->getLockWh()) { + paired_value = new_value / rect->getAspectRatioWh(); + _width_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleWidth(paired_value); + } else if (&btn == &_rx_item && rect->getLockRxy()) { + paired_value = new_value * rect->getAspectRatioRxy(); + _ry_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleRy(paired_value); + } else if (&btn == &_ry_item && rect->getLockRxy()) { + paired_value = new_value / rect->getAspectRatioRxy(); + _rx_item.get_adjustment()->set_value(Quantity::convert(paired_value, "px", _tracker->getActiveUnit())); + rect->setVisibleRx(paired_value); + } + + // Update the primary dimension + if (new_value != 0) { + (rect->*btn.setter)(new_value); } else { rect->removeAttribute(btn.name); } + + // Update aspect ratio after change + double w = rect->getVisibleWidth(); + double h = rect->getVisibleHeight(); + rect->setAspectRatioWh(h / (w != 0 ? w : 1.0)); + double rx = rect->getVisibleRx(); + double ry = rect->getVisibleRy(); + rect->setAspectRatioRxy(ry / (rx != 0 ? rx : 1.0)); modified = true; } } @@ -275,6 +319,10 @@ void RectToolbar::_selectionChanged(Selection *selection) if (_single) { _attachRepr(repr, rect); + _lock_wh_button.set_active(rect->getLockWh()); + _lock_rxy_button.set_active(rect->getLockRxy()); + _aspect_ratio_wh = rect->getAspectRatioWh(); + _aspect_ratio_rxy = rect->getAspectRatioRxy(); _queueUpdate(); } @@ -340,6 +388,29 @@ void RectToolbar::_update() _sensitivize(); } +void RectToolbar::toggle_lock_wh() { + bool active = _lock_wh_button.get_active(); + if (_single && _rect) { + _rect->setLockWh(active); + _lock_wh_button.set_image_from_icon_name(active ? "object-locked" : "object-unlocked"); + double w = _rect->getVisibleWidth(); + double h = _rect->getVisibleHeight(); + _rect->setAspectRatioWh(h / (w != 0 ? w : 1.0)); // Update aspect ratio when toggling + DocumentUndo::done(_desktop->getDocument(), _("Toggle rectangle lock"), INKSCAPE_ICON("draw-rectangle")); + } +} + +void RectToolbar::toggle_lock_rxy() { + bool active = _lock_rxy_button.get_active(); + if (_single && _rect) { + _rect->setLockRxy(active); + _lock_rxy_button.set_image_from_icon_name(active ? "object-locked" : "object-unlocked"); + double rx = _rect->getVisibleRx(); + double ry = _rect->getVisibleRy(); + _rect->setAspectRatioRxy(ry / (rx != 0 ? rx : 1.0)); + DocumentUndo::done(_desktop->getDocument(), _("Toggle rectangle lock"), INKSCAPE_ICON("draw-rectangle")); + } +} } // namespace Inkscape::UI::Toolbar /* diff --git a/src/ui/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h index 14d7d1f414c9c16eb669d3b0c00e98e75aa442e5..1296342cdabcbd8d99997319fdc33765e7996b3a 100644 --- a/src/ui/toolbar/rect-toolbar.h +++ b/src/ui/toolbar/rect-toolbar.h @@ -29,8 +29,9 @@ */ #include - +#include #include "toolbar.h" +#include "preferences.h" #include "ui/operation-blocker.h" #include "xml/node-observer.h" @@ -39,6 +40,7 @@ class Builder; class Button; class Label; class Adjustment; +class ToggleButton; } // namespace Gtk class SPRect; @@ -68,6 +70,8 @@ public: void setDesktop(SPDesktop *desktop) override; void setActiveUnit(Util::Unit const *unit) override; + bool getLockWH() const { return _lock_wh; } + bool getLockRXY() const { return _lock_rxy; } private: RectToolbar(Glib::RefPtr const &builder); @@ -85,6 +89,16 @@ private: auto _getDerivedSpinButtons() const { return std::to_array({&_rx_item, &_ry_item, &_width_item, &_height_item}); } void _valueChanged(DerivedSpinButton &btn); + // Add lock button and state + Gtk::ToggleButton &_lock_wh_button; // New toggle button for locking width/height + Gtk::ToggleButton &_lock_rxy_button; // New toggle button for locking rx/ry + bool _lock_wh = false; + bool _lock_rxy = false; + double _aspect_ratio_wh=0.0; + double _aspect_ratio_rxy=0.0; + void toggle_lock_wh(); + void toggle_lock_rxy(); + XML::Node *_repr = nullptr; SPRect *_rect = nullptr; void _attachRepr(XML::Node *repr, SPRect *rect); diff --git a/testfiles/CMakeLists.txt b/testfiles/CMakeLists.txt index 0f2591485378bb1448569c3252c5712f9b94d279..f8283f629b67a223caf679ef37f85daf73c190cd 100644 --- a/testfiles/CMakeLists.txt +++ b/testfiles/CMakeLists.txt @@ -142,6 +142,7 @@ foreach(test_source ${TEST_SOURCES}) add_executable(${testname} src/${test_source}.cpp) target_include_directories(${testname} SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS}) target_link_libraries(${testname} cpp_test_static_library 2Geom::2geom) + set_target_properties(${testname} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") add_test(NAME ${testname} COMMAND ${testname}) set_tests_properties(${testname} PROPERTIES ENVIRONMENT "${INKSCAPE_TEST_PROFILE_DIR_ENV}/${testname};${CMAKE_CTEST_ENV}") add_dependencies(tests ${testname})