summaryrefslogtreecommitdiffstats
path: root/src/ui/shape-editor-knotholders.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 18:24:48 +0000
commitcca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch)
tree146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/shape-editor-knotholders.cpp
parentInitial commit. (diff)
downloadinkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz
inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/ui/shape-editor-knotholders.cpp2610
1 files changed, 2610 insertions, 0 deletions
diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp
new file mode 100644
index 0000000..a56612f
--- /dev/null
+++ b/src/ui/shape-editor-knotholders.cpp
@@ -0,0 +1,2610 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Node editing extension to objects
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Mitsuru Oka
+ * Maximilian Albert <maximilian.albert@gmail.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+// Declared in shape-editor.cpp.
+
+#include <glibmm/i18n.h>
+
+#include "preferences.h"
+#include "desktop.h"
+#include "document.h"
+#include "style.h"
+
+#include "live_effects/effect.h"
+
+#include "object/box3d.h"
+#include "object/sp-marker.h"
+#include "object/sp-ellipse.h"
+#include "object/sp-flowtext.h"
+#include "object/sp-item.h"
+#include "object/sp-namedview.h"
+#include "object/sp-offset.h"
+#include "object/sp-pattern.h"
+#include "object/sp-rect.h"
+#include "object/sp-spiral.h"
+#include "object/sp-star.h"
+#include "object/sp-text.h"
+#include "object/sp-textpath.h"
+#include "object/sp-tspan.h"
+#include "svg/css-ostringstream.h"
+
+#include "ui/knot/knot-holder.h"
+#include "ui/knot/knot-holder-entity.h"
+
+class RectKnotHolder : public KnotHolder {
+public:
+ RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~RectKnotHolder() override = default;;
+};
+
+class Box3DKnotHolder : public KnotHolder {
+public:
+ Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~Box3DKnotHolder() override = default;;
+};
+
+class MarkerKnotHolder : public KnotHolder {
+public:
+ MarkerKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler, double edit_rotation, int edit_marker_mode);
+ ~MarkerKnotHolder() override = default;;
+};
+
+class ArcKnotHolder : public KnotHolder {
+public:
+ ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~ArcKnotHolder() override = default;;
+};
+
+class StarKnotHolder : public KnotHolder {
+public:
+ StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~StarKnotHolder() override = default;;
+};
+
+class SpiralKnotHolder : public KnotHolder {
+public:
+ SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~SpiralKnotHolder() override = default;;
+};
+
+class OffsetKnotHolder : public KnotHolder {
+public:
+ OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~OffsetKnotHolder() override = default;;
+};
+
+class TextKnotHolder : public KnotHolder {
+public:
+ TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~TextKnotHolder() override = default;;
+};
+
+class FlowtextKnotHolder : public KnotHolder {
+public:
+ FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~FlowtextKnotHolder() override = default;;
+};
+
+class MiscKnotHolder : public KnotHolder {
+public:
+ MiscKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler);
+ ~MiscKnotHolder() override = default;;
+};
+
+namespace {
+
+static KnotHolder *sp_lpe_knot_holder(SPLPEItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knot_holder = new KnotHolder(desktop, item, nullptr);
+
+ Inkscape::LivePathEffect::Effect *effect = item->getCurrentLPE();
+ effect->addHandles(knot_holder, item);
+
+ return knot_holder;
+}
+
+} // namespace
+
+namespace Inkscape {
+namespace UI {
+
+KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop, double edit_rotation = 0.0, int edit_marker_mode = -1)
+{
+ KnotHolder *knotholder = nullptr;
+
+ if (dynamic_cast<SPRect *>(item)) {
+ knotholder = new RectKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPBox3D *>(item)) {
+ knotholder = new Box3DKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPMarker *>(item)) {
+ knotholder = new MarkerKnotHolder(desktop, item, nullptr, edit_rotation, edit_marker_mode);
+ } else if (dynamic_cast<SPGenericEllipse *>(item)) {
+ knotholder = new ArcKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPStar *>(item)) {
+ knotholder = new StarKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPSpiral *>(item)) {
+ knotholder = new SpiralKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPOffset *>(item)) {
+ knotholder = new OffsetKnotHolder(desktop, item, nullptr);
+ } else if (dynamic_cast<SPText *>(item)) {
+ SPText *text = dynamic_cast<SPText *>(item);
+
+ // Do not allow conversion to 'inline-size' wrapped text if on path!
+ // <textPath> might not be first child if <title> or <desc> is present.
+ bool is_on_path = false;
+ for (auto child : text->childList(false)) {
+ if (dynamic_cast<SPTextPath *>(child)) is_on_path = true;
+ }
+ if (!is_on_path) {
+ knotholder = new TextKnotHolder(desktop, item, nullptr);
+ }
+ } else {
+ SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item);
+ if (flowtext && flowtext->has_internal_frame()) {
+ knotholder = new FlowtextKnotHolder(desktop, flowtext->get_frame(nullptr), nullptr);
+ } else if ((item->style->fill.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getFillPaintServer())) ||
+ (item->style->stroke.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getStrokePaintServer()))) {
+ knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_pattern_knotholder();
+ }
+ }
+ if (!knotholder) knotholder = new KnotHolder(desktop, item, nullptr);
+ knotholder->add_filter_knotholder();
+
+ return knotholder;
+}
+
+KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop)
+{
+ KnotHolder *knotholder = nullptr;
+
+ SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item);
+ if (lpe &&
+ lpe->getCurrentLPE() &&
+ lpe->getCurrentLPE()->isVisible() &&
+ lpe->getCurrentLPE()->providesKnotholder()) {
+ knotholder = sp_lpe_knot_holder(lpe, desktop);
+ }
+ return knotholder;
+}
+
+}
+} // namespace Inkscape
+
+/* SPRect */
+
+/* handle for horizontal rounding radius */
+class RectKnotHolderEntityRX : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for vertical rounding radius */
+class RectKnotHolderEntityRY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+/* handle for width/height adjustment */
+class RectKnotHolderEntityWH : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+};
+
+/* handle for x/y adjustment */
+class RectKnotHolderEntityXY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+/* handle for position */
+class RectKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+RectKnotHolderEntityRX::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed - rect->rx.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(-1, 0)), state);
+
+ 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 {
+ rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0);
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding from rectangle */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("ry", rect->getRepr()->attribute("rx"));
+ }
+
+}
+
+Geom::Point
+RectKnotHolderEntityRY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->ry.computed);
+}
+
+void
+RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ //In general we cannot just snap this radius to an arbitrary point, as we have only a single
+ //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap
+ //the radius then we should have a constrained snap. snap_knot_position() is unconstrained
+ Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(0, 1)), state);
+
+ if (state & GDK_CONTROL_MASK) { // When holding control then rx will be kept equal to ry,
+ // 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 {
+ if (!rect->rx._set || rect->rx.computed == 0) {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ MIN(rect->height.computed / 2.0, rect->width.computed / 2.0));
+ } else {
+ rect->ry = CLAMP(s[Geom::Y] - rect->y.computed,
+ 0.0,
+ rect->height.computed / 2.0);
+ }
+ }
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ /* remove rounding */
+ rect->getRepr()->removeAttribute("rx");
+ rect->getRepr()->removeAttribute("ry");
+ } else if (state & GDK_CONTROL_MASK) {
+ /* Ctrl-click sets the vertical rounding to be the same as the horizontal */
+ rect->getRepr()->setAttribute("rx", rect->getRepr()->attribute("ry"));
+ }
+}
+
+#define SGN(x) ((x)>0?1:((x)<0?-1:0))
+
+static void sp_rect_clamp_radii(SPRect *rect)
+{
+ // clamp rounding radii so that they do not exceed width/height
+ if (2 * rect->rx.computed > rect->width.computed) {
+ rect->rx = 0.5 * rect->width.computed;
+ }
+ if (2 * rect->ry.computed > rect->height.computed) {
+ rect->ry = 0.5 * rect->height.computed;
+ }
+}
+
+Geom::Point
+RectKnotHolderEntityWH::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point s = p;
+
+ if (state & GDK_CONTROL_MASK) {
+ // original width/height when drag started
+ gdouble const w_orig = (origin[Geom::X] - rect->x.computed);
+ gdouble const h_orig = (origin[Geom::Y] - rect->y.computed);
+
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ Geom::Point p_handle(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig + minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->width = MAX(w_orig + minx, 0);
+
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 * ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig + miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->height = MAX(h_orig + miny, 0);
+
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ rect->width = MAX(s[Geom::X] - rect->x.computed, 0);
+ rect->height = MAX(s[Geom::Y] - rect->y.computed, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+RectKnotHolderEntityWH::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+Geom::Point
+RectKnotHolderEntityXY::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed, rect->y.computed);
+}
+
+void
+RectKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ // opposite corner (unmoved)
+ gdouble opposite_x = (rect->x.computed + rect->width.computed);
+ gdouble opposite_y = (rect->y.computed + rect->height.computed);
+
+ // original width/height when drag started
+ gdouble w_orig = opposite_x - origin[Geom::X];
+ gdouble h_orig = opposite_y - origin[Geom::Y];
+
+ Geom::Point s = p;
+ Geom::Point p_handle(rect->x.computed, rect->y.computed);
+
+ // mouse displacement since drag started
+ gdouble minx = p[Geom::X] - origin[Geom::X];
+ gdouble miny = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_CONTROL_MASK) {
+ //original ratio
+ gdouble ratio = (w_orig / h_orig);
+
+ if (fabs(minx) > fabs(miny)) {
+ // snap to horizontal or diagonal
+ if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y] + minx / ratio, opposite_y);
+ rect->height = MAX(h_orig - minx / ratio, 0);
+ } else {
+ // closer to the horizontal, change only width, height is h_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state);
+ minx = s[Geom::X] - origin[Geom::X];
+ // Dead assignment: Value stored to 'miny' is never read
+ //miny = s[Geom::Y] - origin[Geom::Y];
+ rect->y = MIN(origin[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig, 0);
+ }
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->width = MAX(w_orig - minx, 0);
+ } else {
+ // snap to vertical or diagonal
+ if (miny != 0 && fabs(minx/miny) > 0.5 *ratio && (SGN(minx) == SGN(miny))) {
+ // closer to the diagonal and in same-sign quarters, change both using ratio
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X] + miny * ratio, opposite_x);
+ rect->width = MAX(w_orig - miny * ratio, 0);
+ } else {
+ // closer to the vertical, change only height, width is w_orig
+ s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state);
+ // Dead assignment: Value stored to 'minx' is never read
+ //minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+ rect->x = MIN(origin[Geom::X], opposite_x);
+ rect->width = MAX(w_orig, 0);
+ }
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ } else {
+ // move freely
+ s = snap_knot_position(p, state);
+ minx = s[Geom::X] - origin[Geom::X];
+ miny = s[Geom::Y] - origin[Geom::Y];
+
+ rect->x = MIN(s[Geom::X], opposite_x);
+ rect->y = MIN(s[Geom::Y], opposite_y);
+ rect->width = MAX(w_orig - minx, 0);
+ rect->height = MAX(h_orig - miny, 0);
+ }
+
+ sp_rect_clamp_radii(rect);
+
+ update_knot();
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+RectKnotHolderEntityCenter::knot_get() const
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + (rect->width.computed / 2.), rect->y.computed + (rect->height.computed / 2.));
+}
+
+void
+RectKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPRect *rect = dynamic_cast<SPRect *>(item);
+ g_assert(rect != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ rect->x = s[Geom::X] - (rect->width.computed / 2.);
+ rect->y = s[Geom::Y] - (rect->height.computed / 2.);
+
+ // No need to call sp_rect_clamp_radii(): width and height haven't changed.
+ // No need to call update_knot(): the knot is set directly by the user.
+
+ rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+RectKnotHolder::RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ RectKnotHolderEntityRX *entity_rx = new RectKnotHolderEntityRX();
+ RectKnotHolderEntityRY *entity_ry = new RectKnotHolderEntityRY();
+ RectKnotHolderEntityWH *entity_wh = new RectKnotHolderEntityWH();
+ RectKnotHolderEntityXY *entity_xy = new RectKnotHolderEntityXY();
+ RectKnotHolderEntityCenter *entity_center = new RectKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Rect:rx",
+ _("Adjust the <b>horizontal rounding</b> radius; with <b>Ctrl</b> "
+ "to make the vertical radius the same"));
+
+ entity_ry->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Rect:ry",
+ _("Adjust the <b>vertical rounding</b> radius; with <b>Ctrl</b> "
+ "to make the horizontal radius the same"));
+
+ entity_wh->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Rect:wh",
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"));
+
+ entity_xy->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Rect:xy",
+ _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> "
+ "to lock ratio or stretch in one dimension only"));
+
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Rect:center",
+ _("Drag to move the rectangle"));
+
+ entity.push_back(entity_rx);
+ entity.push_back(entity_ry);
+ entity.push_back(entity_wh);
+ entity.push_back(entity_xy);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* Box3D (= the new 3D box structure) */
+
+class Box3DKnotHolderEntity : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override = 0;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override = 0;
+
+ Geom::Point knot_get_generic(SPItem *item, unsigned int knot_id) const;
+ void knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &p, unsigned int state);
+};
+
+Geom::Point
+Box3DKnotHolderEntity::knot_get_generic(SPItem *item, unsigned int knot_id) const
+{
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ return box->get_corner_screen(knot_id);
+ } else {
+ return Geom::Point(); // TODO investigate proper fallback
+ }
+}
+
+void
+Box3DKnotHolderEntity::knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &new_pos, unsigned int state)
+{
+ Geom::Point const s = snap_knot_position(new_pos, state);
+
+ g_assert(item != nullptr);
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ Box3D::Axis movement;
+ if ((knot_id < 4) != (state & GDK_SHIFT_MASK)) {
+ movement = Box3D::XY;
+ } else {
+ movement = Box3D::Z;
+ }
+
+ box->set_corner (knot_id, s * i2dt, movement, (state & GDK_CONTROL_MASK));
+ box->set_z_orders();
+ box->position_set();
+}
+
+class Box3DKnotHolderEntity0 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity1 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity2 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity3 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity4 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity5 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity6 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntity7 : public Box3DKnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class Box3DKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+Box3DKnotHolderEntity0::knot_get() const
+{
+ return knot_get_generic(item, 0);
+}
+
+Geom::Point
+Box3DKnotHolderEntity1::knot_get() const
+{
+ return knot_get_generic(item, 1);
+}
+
+Geom::Point
+Box3DKnotHolderEntity2::knot_get() const
+{
+ return knot_get_generic(item, 2);
+}
+
+Geom::Point
+Box3DKnotHolderEntity3::knot_get() const
+{
+ return knot_get_generic(item, 3);
+}
+
+Geom::Point
+Box3DKnotHolderEntity4::knot_get() const
+{
+ return knot_get_generic(item, 4);
+}
+
+Geom::Point
+Box3DKnotHolderEntity5::knot_get() const
+{
+ return knot_get_generic(item, 5);
+}
+
+Geom::Point
+Box3DKnotHolderEntity6::knot_get() const
+{
+ return knot_get_generic(item, 6);
+}
+
+Geom::Point
+Box3DKnotHolderEntity7::knot_get() const
+{
+ return knot_get_generic(item, 7);
+}
+
+Geom::Point
+Box3DKnotHolderEntityCenter::knot_get() const
+{
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ if (box) {
+ return box->get_center_screen();
+ } else {
+ return Geom::Point(); // TODO investigate proper fallback
+ }
+}
+
+void
+Box3DKnotHolderEntity0::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 0, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity1::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 1, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity2::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 2, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity3::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 3, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity4::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 4, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity5::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 5, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity6::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 6, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntity7::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state)
+{
+ knot_set_generic(item, 7, new_pos, state);
+}
+
+void
+Box3DKnotHolderEntityCenter::knot_set(Geom::Point const &new_pos, Geom::Point const &origin, unsigned int state)
+{
+ Geom::Point const s = snap_knot_position(new_pos, state);
+
+ SPBox3D *box = dynamic_cast<SPBox3D *>(item);
+ g_assert(box != nullptr);
+ Geom::Affine const i2dt (item->i2dt_affine ());
+
+ box->set_center(s * i2dt, origin * i2dt, !(state & GDK_SHIFT_MASK) ? Box3D::XY : Box3D::Z,
+ state & GDK_CONTROL_MASK);
+
+ box->set_z_orders();
+ box->position_set();
+}
+
+Box3DKnotHolder::Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ Box3DKnotHolderEntity0 *entity_corner0 = new Box3DKnotHolderEntity0();
+ Box3DKnotHolderEntity1 *entity_corner1 = new Box3DKnotHolderEntity1();
+ Box3DKnotHolderEntity2 *entity_corner2 = new Box3DKnotHolderEntity2();
+ Box3DKnotHolderEntity3 *entity_corner3 = new Box3DKnotHolderEntity3();
+ Box3DKnotHolderEntity4 *entity_corner4 = new Box3DKnotHolderEntity4();
+ Box3DKnotHolderEntity5 *entity_corner5 = new Box3DKnotHolderEntity5();
+ Box3DKnotHolderEntity6 *entity_corner6 = new Box3DKnotHolderEntity6();
+ Box3DKnotHolderEntity7 *entity_corner7 = new Box3DKnotHolderEntity7();
+ Box3DKnotHolderEntityCenter *entity_center = new Box3DKnotHolderEntityCenter();
+
+ entity_corner0->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner0",
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner1->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner1",
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner2->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner2",
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner3->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner3",
+ _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner4->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner4",
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner5->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner5",
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner6->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner6",
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_corner7->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Box3D:corner7",
+ _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; "
+ "with <b>Ctrl</b> to constrain to the directions of edges or diagonals"));
+
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Box3D:center",
+ _("Move the box in perspective"));
+
+ entity.push_back(entity_corner0);
+ entity.push_back(entity_corner1);
+ entity.push_back(entity_corner2);
+ entity.push_back(entity_corner3);
+ entity.push_back(entity_corner4);
+ entity.push_back(entity_corner5);
+ entity.push_back(entity_corner6);
+ entity.push_back(entity_corner7);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPMarker */
+
+// marker x scale = (marker width)/(view box width)
+double
+getMarkerXScale(SPItem* item){
+
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ return ((sp_marker->viewBox.width() != 0) ? sp_marker->markerWidth.computed/sp_marker->viewBox.width() : 1.0);
+}
+
+double
+getMarkerYScale(SPItem* item){
+
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ return ((sp_marker->viewBox.height() != 0) ? sp_marker->markerHeight.computed/sp_marker->viewBox.height() : 1.0);
+}
+
+/*
+- edit_rotation is the tangent angle that is used in orient auto mode.
+- edit_rotation is applied in the edit_transform, it needs to be undone and then the orient.computed can be applied.
+*/
+Geom::Affine
+getMarkerRotation(SPItem* item, double edit_rotation, int edit_marker_mode, bool reverse = false){
+
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Affine rot = Geom::Rotate::from_degrees(0.0);
+
+ if ((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (edit_marker_mode == SP_MARKER_LOC_START)) {
+ rot = Geom::Rotate::from_degrees(180.0);
+ } else if (sp_marker->orient_mode == MARKER_ORIENT_ANGLE) {
+ rot = reverse? Geom::Rotate::from_degrees(edit_rotation - sp_marker->orient.computed) : Geom::Rotate::from_degrees(sp_marker->orient.computed - edit_rotation);
+ }
+
+ return rot;
+}
+
+// used to translate the knots when the marker's minimum bounds are less than zero.
+Geom::Rect
+getMarkerBounds(SPItem* item, SPDesktop *desktop){
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ SPDocument *doc = desktop->getDocument();
+
+ g_assert(sp_marker != nullptr);
+ g_assert(doc != nullptr);
+
+ std::vector<SPObject*> items = sp_marker->childList(false, SPObject::ActionBBox);
+ Geom::OptRect r;
+
+ for (auto *i : items) {
+ SPItem *item = dynamic_cast<SPItem*>(i);
+ r.unionWith(item->desktopVisualBounds());
+ }
+ Geom::Rect bounds(r->min() * doc->dt2doc(), r->max() * doc->dt2doc());
+ return bounds;
+}
+
+/*
+- this knot sets the refX/refY attributes of the marker
+- this knot is actually shown in the center of the shape vs the actual
+refX/refY position to make it more intuitive
+*/
+
+class MarkerKnotHolderEntityReference : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ MarkerKnotHolderEntityReference(double edit_rotation, int edit_marker_mode)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+
+Geom::Point
+MarkerKnotHolderEntityReference::knot_get() const
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ // knot is actually shown at center of marker, not at its reference point
+ return Geom::Point((-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityReference::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Point s = -p;
+ s = s * getMarkerRotation(item, _edit_rotation, _edit_marker_mode, true);
+ sp_marker->refX = (s[Geom::X]/ getMarkerXScale(item)) + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2;
+ sp_marker->refY = (s[Geom::Y]/ getMarkerYScale(item)) + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2;
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+// marker orient section - handles rotation
+
+class MarkerKnotHolderEntityOrient : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ bool originals_set = false;
+
+ // angle that the center of the marker makes with the orient knot
+ double original_center_angle = 0;
+ double original_radius = 0;
+ Geom::Point original_center = Geom::Point(0, 0);
+
+ MarkerKnotHolderEntityOrient(double edit_rotation, int edit_marker_mode)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+
+};
+
+void MarkerKnotHolderEntityOrient::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) {
+ originals_set = false;
+}
+
+Geom::Point
+MarkerKnotHolderEntityOrient::knot_get() const
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + sp_marker->viewBox.width() + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityOrient::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ if(!originals_set) {
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ /*
+ - if the marker is set to auto or auto-start-reverse, set its type to orient
+ - calculate and set the default angle for the orient mode
+ */
+ if (sp_marker->orient_mode != MARKER_ORIENT_ANGLE) {
+ sp_marker->orient = (((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (_edit_marker_mode == SP_MARKER_LOC_START)) ? _edit_rotation + 180.0 : _edit_rotation);
+ sp_marker->orient_mode = MARKER_ORIENT_ANGLE;
+ sp_marker->orient_set = true;
+ }
+
+ /*
+ - the original marker center is used to calculate the angle with mouse
+ - the refX/refY will be changing to adjust for the new rotation to give appearance that it is stationary onCanvas while editing.
+ */
+ original_center = Geom::Point(
+ (-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+
+ original_center_angle = atan2(
+ sp_marker->markerHeight.computed - sp_marker->markerHeight.computed/2,
+ sp_marker->markerWidth.computed - sp_marker->markerWidth.computed/2
+ ) * 180.0/M_PI;
+
+ original_radius = L2(original_center);
+ originals_set = true;
+ }
+
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+void
+MarkerKnotHolderEntityOrient::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ // edit_rotation is the tangest angle to the shapes and needs to be taken into account while setting the orient angle
+ double new_angle = atan2(p[Geom::Y] - original_center[Geom::Y], p[Geom::X] - original_center[Geom::X]) * 180.0/M_PI;
+ new_angle = new_angle + _edit_rotation + original_center_angle;
+
+ double axis_angle = -((atan2(original_center) * 180.0/M_PI) + _edit_rotation);
+
+ sp_marker->orient = new_angle;
+ sp_marker->orient_mode = MARKER_ORIENT_ANGLE;
+ sp_marker->orient_set = true;
+
+ Geom::Point ref = Geom::Point(
+ (-(original_radius * cos(-(axis_angle + sp_marker->orient.computed) * M_PI/180.0))/getMarkerXScale(item)) + getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2,
+ (-(original_radius * sin(-(axis_angle + sp_marker->orient.computed) * M_PI/180.0))/getMarkerYScale(item)) + getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2);
+
+ sp_marker->refX = ref[Geom::X];
+ sp_marker->refY = ref[Geom::Y];
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+// marker has multiple scaling knots at its corners
+
+class MarkerKnotHolderEntityScale : public KnotHolderEntity {
+public:
+ double _edit_rotation = 0.0;
+ int _edit_marker_mode = -1;
+
+ /*
+ - related to the position(+/-) of the scaling knot in reference to the center
+ - makes sure scaling works correctly for derived classes
+ */
+ int _x_Sign = 1;
+ int _y_Sign = 1;
+
+ bool originals_set = false;
+
+ double original_scaleX = 1;
+ double original_scaleY = 1;
+
+ double original_refX = 0;
+ double original_refY = 0;
+
+ double original_width = 0;
+ double original_height = 0;
+
+ MarkerKnotHolderEntityScale(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : _edit_rotation(edit_rotation),
+ _edit_marker_mode(edit_marker_mode),
+ _x_Sign(x_Sign),
+ _y_Sign(y_Sign)
+ {
+ }
+
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ Geom::Point knot_get() const override;
+
+protected:
+ void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state);
+
+};
+
+void
+MarkerKnotHolderEntityScale::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) {
+ originals_set = false;
+}
+
+Geom::Point
+MarkerKnotHolderEntityScale::knot_get() const
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + sp_marker->viewBox.width() + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + sp_marker->viewBox.height() + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+void
+MarkerKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ // keep track of the original values before the knot/mouse position is being moved
+ if(!originals_set) {
+
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ original_scaleX = getMarkerXScale(item);
+ original_scaleY = getMarkerYScale(item);
+
+ original_refX = sp_marker->refX.computed;
+ original_refY = sp_marker->refY.computed;
+
+ original_width = sp_marker->viewBox.width();
+ original_height = sp_marker->viewBox.height();
+
+ originals_set = true;
+ }
+
+ set_internal(p, origin, state);
+ update_knot();
+}
+
+// scaling takes place around center of marker, not its reference point
+void
+MarkerKnotHolderEntityScale::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ Geom::Point adjusted_origin = origin;
+ Geom::Point adjusted_p = p;
+
+ if(sp_marker->orient_mode == MARKER_ORIENT_ANGLE) {
+
+ adjusted_origin = adjusted_origin
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(_edit_rotation - sp_marker->orient.computed);
+
+ adjusted_p = adjusted_p
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(_edit_rotation - sp_marker->orient.computed);
+
+ } else if ((sp_marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) && (_edit_marker_mode == SP_MARKER_LOC_START)) {
+
+ adjusted_origin = adjusted_origin
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(180.0);
+
+ adjusted_p = adjusted_p
+ * Geom::Translate(getMarkerBounds(item, desktop).min())
+ * Geom::Rotate::from_degrees(180.0);
+ }
+
+ // x_Sign and y_Sign are (+/- 1) to set the appropriate sign for derived classes
+ double orig_width = _x_Sign*((original_width * original_scaleX)/2);
+ double orig_height = _y_Sign*((original_height * original_scaleY)/2);
+
+ // x & y displacement between origin and new mouse displacement
+ double dx = adjusted_p[Geom::X] - adjusted_origin[Geom::X];
+ double dy = adjusted_p[Geom::Y] - adjusted_origin[Geom::Y];
+ double adjusted_scaleX = 0.0;
+ double adjusted_scaleY = 0.0;
+
+ adjusted_scaleX = (dx/orig_width) + 1;
+ adjusted_scaleY = (dy/orig_height) + 1;
+
+ // uniform scaling when ctrl+key is pressed
+ if(state & GDK_CONTROL_MASK) {
+ adjusted_scaleX = fabs(adjusted_scaleX);
+ adjusted_scaleY = fabs(adjusted_scaleY);
+
+ // possible areas based on which x/y coord is used to calculate uniform scale
+ double dx_area = (sp_marker->viewBox.width()*adjusted_scaleX) * (sp_marker->viewBox.height()*adjusted_scaleX); // A = W*H
+ double dy_area = (sp_marker->viewBox.width()*adjusted_scaleY) * (sp_marker->viewBox.height()*adjusted_scaleY);
+
+ if (dy_area > dx_area) {
+ adjusted_scaleX = adjusted_scaleY;
+ } else if (dx_area > dy_area) {
+ adjusted_scaleY = adjusted_scaleX;
+ }
+
+ adjusted_scaleX = adjusted_scaleX * original_scaleX;
+ adjusted_scaleY = adjusted_scaleY * original_scaleY;
+
+ sp_marker->markerWidth = sp_marker->viewBox.width() * adjusted_scaleX;
+ sp_marker->markerHeight = sp_marker->viewBox.height() * adjusted_scaleY;
+
+ sp_marker->refX = ((original_refX * original_scaleX)/adjusted_scaleX) - ((getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * (original_scaleX/adjusted_scaleX - 1));
+ sp_marker->refY = ((original_refY * original_scaleY)/adjusted_scaleY) - ((getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * (original_scaleY/adjusted_scaleY - 1));
+ } else {
+
+ adjusted_scaleX = adjusted_scaleX * original_scaleX;
+ adjusted_scaleY = adjusted_scaleY * original_scaleY;
+
+ // make sure the preserveAspectRatio is none when the user wants to use non-uniform scaling
+ if (sp_marker->aspect_align != SP_ASPECT_NONE) {
+ sp_marker->setAttribute("preserveAspectRatio", "none");
+ }
+
+ if(adjusted_scaleX > 0.0 && adjusted_scaleY > 0.0) {
+ sp_marker->markerWidth = sp_marker->viewBox.width() * adjusted_scaleX;
+ sp_marker->markerHeight = sp_marker->viewBox.height() * adjusted_scaleY;
+
+ sp_marker->refX = ((original_refX * original_scaleX)/adjusted_scaleX) - ((getMarkerBounds(item, desktop).min()[Geom::X] + sp_marker->viewBox.width()/2) * (original_scaleX/adjusted_scaleX - 1));
+ sp_marker->refY = ((original_refY * original_scaleY)/adjusted_scaleY) - ((getMarkerBounds(item, desktop).min()[Geom::Y] + sp_marker->viewBox.height()/2) * (original_scaleY/adjusted_scaleY - 1));
+ }
+ }
+
+ sp_marker->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG);
+}
+
+class MarkerKnotHolderEntityScale2 : public MarkerKnotHolderEntityScale {
+public:
+ MarkerKnotHolderEntityScale2(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, x_Sign, y_Sign)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+};
+
+Geom::Point
+MarkerKnotHolderEntityScale2::knot_get() const
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ // this corresponds to the reference point
+ return Geom::Point((-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+
+class MarkerKnotHolderEntityScale3 : public MarkerKnotHolderEntityScale {
+public:
+ MarkerKnotHolderEntityScale3(double edit_rotation, int edit_marker_mode, int x_Sign, int y_Sign)
+ : MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, x_Sign, y_Sign)
+ {
+ }
+
+ Geom::Point knot_get() const override;
+};
+
+Geom::Point
+MarkerKnotHolderEntityScale3::knot_get() const
+{
+ SPMarker *sp_marker = dynamic_cast<SPMarker *>(item);
+ g_assert(sp_marker != nullptr);
+
+ return Geom::Point(
+ (-sp_marker->refX.computed + getMarkerBounds(item, desktop).min()[Geom::X]) * getMarkerXScale(item),
+ (-sp_marker->refY.computed + sp_marker->viewBox.height() + getMarkerBounds(item, desktop).min()[Geom::Y]) * getMarkerYScale(item))
+ * getMarkerRotation(item, _edit_rotation, _edit_marker_mode);
+}
+
+MarkerKnotHolder::MarkerKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler, double edit_rotation, int edit_marker_mode)
+ : KnotHolder(desktop, item, relhandler)
+{
+ MarkerKnotHolderEntityReference *entity_reference = new MarkerKnotHolderEntityReference(edit_rotation, edit_marker_mode);
+ MarkerKnotHolderEntityOrient *entity_orient = new MarkerKnotHolderEntityOrient(edit_rotation, edit_marker_mode);
+
+ MarkerKnotHolderEntityScale *entity_scale = new MarkerKnotHolderEntityScale(edit_rotation, edit_marker_mode, 1, 1);
+ // these two additional knots have the same scaling functionality but also serve as a fill in for the empty corners of the marker bounding box
+ MarkerKnotHolderEntityScale2 *entity_scale2 = new MarkerKnotHolderEntityScale2(edit_rotation, edit_marker_mode, -1, -1);
+ MarkerKnotHolderEntityScale3 *entity_scale3 = new MarkerKnotHolderEntityScale3(edit_rotation, edit_marker_mode, -1, 1);
+
+ entity_reference->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Marker:reference",
+ _("Drag to adjust the refX/refY position of the marker"));
+
+ entity_orient->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Marker:orient",
+ _("Adjust marker orientation through rotation"));
+
+ entity_scale->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity_scale2->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity_scale3->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Marker:scale",
+ _("Adjust the <b>size</b> of the marker"));
+
+ entity.push_back(entity_reference);
+ entity.push_back(entity_orient);
+ entity.push_back(entity_scale);
+ entity.push_back(entity_scale2);
+ entity.push_back(entity_scale3);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPArc */
+
+class ArcKnotHolderEntityStart : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityEnd : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityRX : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityRY : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class ArcKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+/*
+ * return values:
+ * 1 : inside
+ * 0 : on the curves
+ * -1 : outside
+ */
+static gint
+sp_genericellipse_side(SPGenericEllipse *ellipse, Geom::Point const &p)
+{
+ gdouble dx = (p[Geom::X] - ellipse->cx.computed) / ellipse->rx.computed;
+ gdouble dy = (p[Geom::Y] - ellipse->cy.computed) / ellipse->ry.computed;
+
+ gdouble s = dx * dx + dy * dy;
+ // We add a bit of a buffer, so there's a decent chance the user will
+ // be able to adjust the arc without the closed status flipping between
+ // open and closed during micro mouse movements.
+ if (s < 0.75) return 1;
+ if (s > 1.25) return -1;
+ return 0;
+}
+
+void
+ArcKnotHolderEntityStart::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->start - atan2(delta * sc.inverse());
+ arc->start -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->start = std::round(arc->start/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->end -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityStart::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->start);
+}
+
+void
+ArcKnotHolderEntityStart::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(arc != nullptr);
+
+ gint side = sp_genericellipse_side(arc, p);
+ if(side != 0) { arc->setArcType( (side == -1) ?
+ SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE :
+ SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); }
+
+ Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed);
+ Geom::Scale sc(arc->rx.computed, arc->ry.computed);
+
+ double offset = arc->end - atan2(delta * sc.inverse());
+ arc->end -= offset;
+
+ if ((state & GDK_CONTROL_MASK) && snaps) {
+ double snaps_radian = M_PI/snaps;
+ arc->end = std::round(arc->end/snaps_radian) * snaps_radian;
+ }
+ if (state & GDK_SHIFT_MASK) {
+ arc->start -= offset;
+ }
+
+ arc->normalize();
+ arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityEnd::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return ge->getPointAtAngle(ge->end);
+}
+
+
+void
+ArcKnotHolderEntityEnd::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_SHIFT_MASK) {
+ ge->end = ge->start = 0;
+ ge->updateRepr();
+ }
+}
+
+
+void
+ArcKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->rx = fabs( ge->cx.computed - s[Geom::X] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->ry = ge->rx.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRX::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(ge->rx.computed, 0));
+}
+
+void
+ArcKnotHolderEntityRX::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->ry = ge->rx.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->ry = fabs( ge->cy.computed - s[Geom::Y] );
+
+ if ( state & GDK_CONTROL_MASK ) {
+ ge->rx = ge->ry.computed;
+ }
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityRY::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(0, ge->ry.computed));
+}
+
+void
+ArcKnotHolderEntityRY::knot_click(unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+ ge->rx = ge->ry.computed;
+ ge->updateRepr();
+ }
+}
+
+void
+ArcKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ ge->cx = s[Geom::X];
+ ge->cy = s[Geom::Y];
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+ArcKnotHolderEntityCenter::knot_get() const
+{
+ SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item);
+ g_assert(ge != nullptr);
+
+ return Geom::Point(ge->cx.computed, ge->cy.computed);
+}
+
+
+ArcKnotHolder::ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ ArcKnotHolderEntityRX *entity_rx = new ArcKnotHolderEntityRX();
+ ArcKnotHolderEntityRY *entity_ry = new ArcKnotHolderEntityRY();
+ ArcKnotHolderEntityStart *entity_start = new ArcKnotHolderEntityStart();
+ ArcKnotHolderEntityEnd *entity_end = new ArcKnotHolderEntityEnd();
+ ArcKnotHolderEntityCenter *entity_center = new ArcKnotHolderEntityCenter();
+
+ entity_rx->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Arc:rx",
+ _("Adjust ellipse <b>width</b>, with <b>Ctrl</b> to make circle"));
+
+ entity_ry->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Arc:ry",
+ _("Adjust ellipse <b>height</b>, with <b>Ctrl</b> to make circle"));
+
+ entity_start->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Arc:start",
+ _("Position the <b>start point</b> of the arc or segment; with <b>Shift</b> to move "
+ "with <b>end point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the "
+ "ellipse for arc, <b>outside</b> for segment"));
+
+ entity_end->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_ROTATE, "Arc:end",
+ _("Position the <b>end point</b> of the arc or segment; with <b>Shift</b> to move "
+ "with <b>start point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the "
+ "ellipse for arc, <b>outside</b> for segment"));
+
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Arc:center",
+ _("Drag to move the ellipse"));
+
+ entity.push_back(entity_rx);
+ entity.push_back(entity_ry);
+ entity.push_back(entity_start);
+ entity.push_back(entity_end);
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPStar */
+
+class StarKnotHolderEntity1 : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class StarKnotHolderEntity2 : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class StarKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+void
+StarKnotHolderEntity1::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[0];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[0] = L2(d);
+ } else {
+ star->r[0] = L2(d);
+ star->arg[0] = arg1;
+ star->arg[1] += darg1;
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+StarKnotHolderEntity2::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ if (star->flatsided == false) {
+ Geom::Point d = s - star->center;
+
+ double arg1 = atan2(d);
+ double darg1 = arg1 - star->arg[1];
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = darg1/(star->arg[0] - star->arg[1]);
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = fabs(darg1/(star->arg[0] - star->arg[1]));
+ } else if (state & GDK_CONTROL_MASK) {
+ star->r[1] = L2(d);
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ }
+ else {
+ star->r[1] = L2(d);
+ star->arg[1] = atan2(d);
+ }
+ star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ }
+}
+
+void
+StarKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ star->center = snap_knot_position(p, state);
+
+ item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+StarKnotHolderEntity1::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT1, 0);
+
+}
+
+Geom::Point
+StarKnotHolderEntity2::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return sp_star_get_xy(star, SP_STAR_POINT_KNOT2, 0);
+}
+
+Geom::Point
+StarKnotHolderEntityCenter::knot_get() const
+{
+ g_assert(item != nullptr);
+
+ SPStar const *star = dynamic_cast<SPStar const *>(item);
+ g_assert(star != nullptr);
+
+ return star->center;
+}
+
+static void
+sp_star_knot_click(SPItem *item, unsigned int state)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(star != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ star->randomized = 0;
+ star->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ star->rounded = 0;
+ star->updateRepr();
+ } else if (state & GDK_CONTROL_MASK) {
+ star->arg[1] = star->arg[0] + M_PI / star->sides;
+ star->updateRepr();
+ }
+}
+
+void
+StarKnotHolderEntity1::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+void
+StarKnotHolderEntity2::knot_click(unsigned int state)
+{
+ sp_star_knot_click(item, state);
+}
+
+StarKnotHolder::StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SPStar *star = dynamic_cast<SPStar *>(item);
+ g_assert(item != nullptr);
+
+ StarKnotHolderEntity1 *entity1 = new StarKnotHolderEntity1();
+ entity1->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Star:entity1",
+ _("Adjust the <b>tip radius</b> of the star or polygon; "
+ "with <b>Shift</b> to round; with <b>Alt</b> to randomize"));
+
+ entity.push_back(entity1);
+
+ if (star->flatsided == false) {
+ StarKnotHolderEntity2 *entity2 = new StarKnotHolderEntity2();
+ entity2->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Star:entity2",
+ _("Adjust the <b>base radius</b> of the star; with <b>Ctrl</b> to keep star rays "
+ "radial (no skew); with <b>Shift</b> to round; with <b>Alt</b> to randomize"));
+ entity.push_back(entity2);
+ }
+
+ StarKnotHolderEntityCenter *entity_center = new StarKnotHolderEntityCenter();
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Star:center",
+ _("Drag to move the star"));
+ entity.push_back(entity_center);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPSpiral */
+
+class SpiralKnotHolderEntityInner : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+class SpiralKnotHolderEntityOuter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+class SpiralKnotHolderEntityCenter : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+
+/*
+ * set attributes via inner (t=t0) knot point:
+ * [default] increase/decrease inner point
+ * [shift] increase/decrease inner and outer arg synchronizely
+ * [control] constrain inner arg to round per PI/4
+ */
+void
+SpiralKnotHolderEntityInner::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ gdouble moved_y = p[Geom::Y] - origin[Geom::Y];
+
+ if (state & GDK_MOD1_MASK) {
+ // adjust divergence by vertical drag, relative to rad
+ if (spiral->rad > 0) {
+ double exp_delta = 0.1*moved_y/(spiral->rad); // arbitrary multiplier to slow it down
+ spiral->exp += exp_delta;
+ if (spiral->exp < 1e-3)
+ spiral->exp = 1e-3;
+ }
+ } else {
+ // roll/unroll from inside
+ gdouble arg_t0;
+ spiral->getPolar(spiral->t0, nullptr, &arg_t0);
+
+ gdouble arg_tmp = atan2(dy, dx) - arg_t0;
+ gdouble arg_t0_new = arg_tmp - floor((arg_tmp+M_PI)/(2.0*M_PI))*2.0*M_PI + arg_t0;
+ spiral->t0 = (arg_t0_new - spiral->arg) / (2.0*M_PI*spiral->revo);
+
+ /* round inner arg per PI/snaps, if CTRL is pressed */
+ if ( ( state & GDK_CONTROL_MASK )
+ && ( fabs(spiral->revo) > SP_EPSILON_2 )
+ && ( snaps != 0 ) ) {
+ gdouble arg = 2.0*M_PI*spiral->revo*spiral->t0 + spiral->arg;
+ double snaps_radian = M_PI/snaps;
+ spiral->t0 = (std::round(arg/snaps_radian)*snaps_radian - spiral->arg)/(2.0*M_PI*spiral->revo);
+ }
+
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+/*
+ * set attributes via outer (t=1) knot point:
+ * [default] increase/decrease revolution factor
+ * [control] constrain inner arg to round per PI/4
+ */
+void
+SpiralKnotHolderEntityOuter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12);
+
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ gdouble dx = p[Geom::X] - spiral->cx;
+ gdouble dy = p[Geom::Y] - spiral->cy;
+
+ if (state & GDK_SHIFT_MASK) { // rotate without roll/unroll
+ spiral->arg = atan2(dy, dx) - 2.0*M_PI*spiral->revo;
+ if (!(state & GDK_MOD1_MASK)) {
+ // if alt not pressed, change also rad; otherwise it is locked
+ spiral->rad = MAX(hypot(dx, dy), 0.001);
+ }
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ spiral->arg = std::round(spiral->arg/snaps_radian) * snaps_radian;
+ }
+ } else { // roll/unroll
+ // arg of the spiral outer end
+ double arg_1;
+ spiral->getPolar(1, nullptr, &arg_1);
+
+ // its fractional part after the whole turns are subtracted
+ static double _2PI = 2.0 * M_PI;
+ double arg_r = arg_1 - std::round(arg_1/_2PI) * _2PI;
+
+ // arg of the mouse point relative to spiral center
+ double mouse_angle = atan2(dy, dx);
+ if (mouse_angle < 0)
+ mouse_angle += _2PI;
+
+ // snap if ctrl
+ if ( ( state & GDK_CONTROL_MASK ) && snaps ) {
+ double snaps_radian = M_PI/snaps;
+ mouse_angle = std::round(mouse_angle/snaps_radian) * snaps_radian;
+ }
+
+ // by how much we want to rotate the outer point
+ double diff = mouse_angle - arg_r;
+ if (diff > M_PI)
+ diff -= _2PI;
+ else if (diff < -M_PI)
+ diff += _2PI;
+
+ // calculate the new rad;
+ // the value of t corresponding to the angle arg_1 + diff:
+ double t_temp = ((arg_1 + diff) - spiral->arg)/(_2PI*spiral->revo);
+ // the rad at that t:
+ double rad_new = 0;
+ if (t_temp > spiral->t0)
+ spiral->getPolar(t_temp, &rad_new, nullptr);
+
+ // change the revo (converting diff from radians to the number of turns)
+ spiral->revo += diff/(2*M_PI);
+ if (spiral->revo < 1e-3)
+ spiral->revo = 1e-3;
+
+ // if alt not pressed and the values are sane, change the rad
+ if (!(state & GDK_MOD1_MASK) && rad_new > 1e-3 && rad_new/spiral->rad < 2) {
+ // adjust t0 too so that the inner point stays unmoved
+ double r0;
+ spiral->getPolar(spiral->t0, &r0, nullptr);
+ spiral->rad = rad_new;
+ spiral->t0 = pow(r0 / spiral->rad, 1.0/spiral->exp);
+ }
+ if (!std::isfinite(spiral->t0)) spiral->t0 = 0.0;
+ spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999);
+ }
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void
+SpiralKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ spiral->cx = s[Geom::X];
+ spiral->cy = s[Geom::Y];
+
+ spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+Geom::Point
+SpiralKnotHolderEntityInner::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(spiral->t0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityOuter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return spiral->getXY(1.0);
+}
+
+Geom::Point
+SpiralKnotHolderEntityCenter::knot_get() const
+{
+ SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item);
+ g_assert(spiral != nullptr);
+
+ return Geom::Point(spiral->cx, spiral->cy);
+}
+
+void
+SpiralKnotHolderEntityInner::knot_click(unsigned int state)
+{
+ SPSpiral *spiral = dynamic_cast<SPSpiral *>(item);
+ g_assert(spiral != nullptr);
+
+ if (state & GDK_MOD1_MASK) {
+ spiral->exp = 1;
+ spiral->updateRepr();
+ } else if (state & GDK_SHIFT_MASK) {
+ spiral->t0 = 0;
+ spiral->updateRepr();
+ }
+}
+
+SpiralKnotHolder::SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SpiralKnotHolderEntityCenter *entity_center = new SpiralKnotHolderEntityCenter();
+ SpiralKnotHolderEntityInner *entity_inner = new SpiralKnotHolderEntityInner();
+ SpiralKnotHolderEntityOuter *entity_outer = new SpiralKnotHolderEntityOuter();
+
+ // NOTE: entity_central and entity_inner can overlap.
+ //
+ // In that case it would be a problem if the center control point was ON
+ // TOP because it would steal the mouse focus and the user would loose the
+ // ability to access the inner control point using only the mouse.
+ //
+ // However if the inner control point is ON TOP, taking focus, the
+ // situation is a lot better: the user can still move the inner control
+ // point with the mouse to regain access to the center control point.
+ //
+ // So, create entity_inner AFTER entity_center; this ensures that
+ // entity_inner gets rendered ON TOP.
+ entity_center->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_POINT, "Spiral:center",
+ _("Drag to move the spiral"));
+
+ entity_inner->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Spiral:inner",
+ _("Roll/unroll the spiral from <b>inside</b>; with <b>Ctrl</b> to snap angle; "
+ "with <b>Alt</b> to converge/diverge"));
+
+ entity_outer->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Spiral:outer",
+ _("Roll/unroll the spiral from <b>outside</b>; with <b>Ctrl</b> to snap angle; "
+ "with <b>Shift</b> to scale/rotate; with <b>Alt</b> to lock radius"));
+
+ entity.push_back(entity_center);
+ entity.push_back(entity_inner);
+ entity.push_back(entity_outer);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+/* SPOffset */
+
+class OffsetKnotHolderEntity : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+void
+OffsetKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPOffset *offset = dynamic_cast<SPOffset *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point const p_snapped = snap_knot_position(p, state);
+
+ offset->rad = sp_offset_distance_to_original(offset, p_snapped);
+ offset->knot = p_snapped;
+ offset->knotSet = true;
+
+ offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+
+Geom::Point
+OffsetKnotHolderEntity::knot_get() const
+{
+ SPOffset const *offset = dynamic_cast<SPOffset const *>(item);
+ g_assert(offset != nullptr);
+
+ Geom::Point np;
+ sp_offset_top_point(offset,&np);
+ return np;
+}
+
+OffsetKnotHolder::OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ OffsetKnotHolderEntity *entity_offset = new OffsetKnotHolderEntity();
+ entity_offset->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Offset:entity",
+ _("Adjust the <b>offset distance</b>"));
+ entity.push_back(entity_offset);
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+
+/* SPText */
+class TextKnotHolderEntityInlineSize : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_click(unsigned int state) override;
+};
+
+Geom::Point
+TextKnotHolderEntityInlineSize::knot_get() const
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ double inline_size = style->inline_size.computed;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point p(text->attributes.firstXY());
+
+ if (text->has_inline_size()) {
+ // SVG 2 'inline-size'
+
+ // Keep handle at end of text line.
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate (inline_size, 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (inline_size/2.0, 0 );
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-inline_size/2.0, 0 );
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-inline_size, 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, inline_size);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, inline_size/2.0);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -inline_size);
+ }
+ }
+ } else {
+ // Normal single line text.
+ Geom::OptRect bbox = text->geometricBounds(); // Check if this is best.
+ if (bbox) {
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ p *= Geom::Translate ((*bbox).width(), 0);
+ } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate ((*bbox).width()/2, 0);
+ } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (-(*bbox).width()/2, 0);
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ p *= Geom::Translate (-(*bbox).width(), 0);
+ }
+ } else {
+ // vertical
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ p *= Geom::Translate (0, (*bbox).height());
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ p *= Geom::Translate (0, (*bbox).height()/2);
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ p *= Geom::Translate (0, -(*bbox).height());
+ }
+ if (mode == SP_CSS_WRITING_MODE_TB_LR) {
+ p += Geom::Point((*bbox).width(), 0); // Keep on right side
+ }
+ }
+ }
+ }
+
+ return p;
+}
+
+// Conversion from Inkscape SVG 1.1 to SVG 2 'inline-size'.
+void
+TextKnotHolderEntityInlineSize::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ SPStyle* style = text->style;
+ unsigned mode = style->writing_mode.computed;
+ unsigned anchor = style->text_anchor.computed;
+ unsigned direction = style->direction.computed;
+
+ Geom::Point const s = snap_knot_position(p, state);
+ Geom::Point delta = s - text->attributes.firstXY();
+ double size = 0.0;
+ if (mode == SP_CSS_WRITING_MODE_LR_TB ||
+ mode == SP_CSS_WRITING_MODE_RL_TB) {
+ // horizontal
+
+ size = delta[Geom::X];
+ if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) {
+ // Do nothing
+ } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) ||
+ (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) {
+ size = -size;
+ } else if ( anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ } else {
+ std::cerr << "TextKnotHolderEntityInlinSize: Should not be reached!" << std::endl;
+ }
+
+ } else {
+ // vertical
+
+ size = delta[Geom::Y];
+ if (anchor == SP_CSS_TEXT_ANCHOR_START) {
+ // Do nothing
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_END) {
+ size = -size;
+ } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) {
+ size = 2.0 * abs(size);
+ }
+ }
+
+ // Size should never be negative
+ if (size < 0.0) {
+ size = 0.0;
+ }
+
+ // Set 'inline-size'.
+ text->style->inline_size.setDouble(size);
+ text->style->inline_size.set = true;
+
+ // Ensure we respect new lines.
+ text->style->white_space.read("pre");
+ text->style->white_space.set = true;
+
+ // Convert sodipodi:role="line" to '\n'.
+ text->sodipodi_to_newline();
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+// Conversion from SVG 2 'inline-size' to Inkscape's SVG 1.1.
+void
+TextKnotHolderEntityInlineSize::knot_click(unsigned int state)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ if (state & GDK_CONTROL_MASK) {
+
+ text->style->inline_size.clear();
+ text->remove_svg11_fallback(); // Else 'x' and 'y' will be interpreted as absolute positions.
+ text->newline_to_sodipodi(); // Convert '\n' to tspans with sodipodi:role="line".
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+ }
+}
+
+/**
+ * Shape padding editor knot positioned top right corner of first object
+ */
+class TextKnotHolderEntityShapePadding : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+TextKnotHolderEntityShapePadding::knot_get() const
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+ Geom::Point corner {Geom::infinity(), Geom::infinity()};
+
+ if (!text->has_shape_inside()) {
+ return corner;
+ }
+
+ auto shape = text->get_first_shape_dependency();
+ if (!shape) {
+ return corner;
+ }
+
+ Geom::OptRect bounds = shape->geometricBounds();
+ if (bounds) {
+ corner = (*bounds).corner(1);
+ if (text->style->shape_padding.set) {
+ auto padding = text->style->shape_padding.computed;
+ corner *= Geom::Affine(Geom::Translate(-padding, padding));
+ }
+ corner *= shape->transform;
+ }
+ return corner;
+}
+
+void
+TextKnotHolderEntityShapePadding::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ // Text in a shape: rectangle
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+ if (!text->has_shape_inside()) {
+ return;
+ }
+
+ if (auto shape = text->get_first_shape_dependency()) {
+ if (Geom::OptRect optbounds = shape->geometricBounds()) {
+ auto bounds = *optbounds;
+ Geom::Point const point_a = snap_knot_position(p, state);
+ Geom::Point point_b = point_a * shape->transform.inverse();
+
+ double padding = 0.0;
+ if (point_b[Geom::X] - 1 > bounds.midpoint()[Geom::X]) {
+ padding = bounds.corner(1)[Geom::X] - point_b[Geom::X];
+ }
+
+ // Padding can only be a positive value according to the CSS/text-padding spec
+ if (padding >= 0.0) {
+ Inkscape::CSSOStringStream os;
+ os << padding;
+ text->style->shape_padding.read(os.str().c_str());
+
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+ }
+ }
+ }
+}
+
+
+/**
+ * Shape margin editor knot positioned top right corner of each object
+ */
+class TextKnotHolderEntityShapeMargin : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+ void set_shape(SPShape *shape) { linked_shape = shape; }
+ SPShape *linked_shape;
+};
+
+Geom::Point
+TextKnotHolderEntityShapeMargin::knot_get() const
+{
+ Geom::Point corner;
+ if (linked_shape == nullptr) return corner;
+
+ Geom::OptRect bounds = linked_shape->geometricBounds();
+ if (bounds) {
+ corner = (*bounds).corner(1);
+ if (linked_shape->style->shape_margin.set) {
+ auto margin = linked_shape->style->shape_margin.computed;
+ corner *= Geom::Affine(Geom::Translate(margin, -margin));
+ }
+ corner *= linked_shape->transform;
+ }
+ return corner;
+}
+
+void
+TextKnotHolderEntityShapeMargin::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ g_assert(linked_shape != nullptr);
+
+ Geom::OptRect bounds = linked_shape->geometricBounds();
+ if (bounds) {
+ Geom::Point const point_a = snap_knot_position(p, state);
+ Geom::Point point_b = point_a * linked_shape->transform.inverse();
+ auto margin = -((*bounds).corner(1)[Geom::X] - point_b[Geom::X]);
+
+ // Margins can only be `non-negative` according to the CSS/shape-margin spec
+ if (margin >= 0.0) {
+ Inkscape::CSSOStringStream os;
+ os << margin;
+ linked_shape->style->shape_margin.read(os.str().c_str());
+
+ linked_shape->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ linked_shape->updateRepr();
+ }
+ }
+}
+
+
+
+
+class TextKnotHolderEntityShapeInside : public KnotHolderEntity {
+public:
+ Geom::Point knot_get() const override;
+ void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {};
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+TextKnotHolderEntityShapeInside::knot_get() const
+{
+ // SVG 2 'shape-inside'. We only get here if there is a rectangle shape.
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ Geom::Point p {Geom::infinity(), Geom::infinity()};
+ if (text->has_shape_inside()) {
+ Geom::OptRect frame = text->get_frame();
+ if (frame) {
+ p = (*frame).corner(2);
+ } else {
+ std::cerr << "TextKnotHolderEntityShapeInside::knot_get(): no frame!" << std::endl;
+ }
+ }
+ return p;
+}
+
+void
+TextKnotHolderEntityShapeInside::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state)
+{
+ // Text in a shape: rectangle
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ Geom::Point const s = snap_knot_position(p, state);
+
+ Inkscape::XML::Node* rectangle = text->get_first_rectangle();
+ if (!rectangle) {
+ return;
+ }
+ double x = rectangle->getAttributeDouble("x", 0.0);;
+ double y = rectangle->getAttributeDouble("y", 0.0);
+ double width = s[Geom::X] - x;
+ double height = s[Geom::Y] - y;
+ rectangle->setAttributeSvgDouble("width", width);
+ rectangle->setAttributeSvgDouble("height", height);
+ text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ text->updateRepr();
+}
+
+TextKnotHolder::TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ SPText *text = dynamic_cast<SPText *>(item);
+ g_assert(text != nullptr);
+
+ if (text->has_shape_inside()) {
+ // 'shape-inside'
+
+ if (text->get_first_rectangle()) {
+ auto entity_shapeinside = new TextKnotHolderEntityShapeInside();
+ entity_shapeinside->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:shapeinside",
+ _("Adjust the <b>rectangular</b> region of the text."));
+ entity.push_back(entity_shapeinside);
+ }
+
+ if (text->get_first_shape_dependency()) {
+ auto entity_shapepadding = new TextKnotHolderEntityShapePadding();
+ entity_shapepadding->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapepadding",
+ _("Adjust the text <b>shape padding</b>."));
+ entity.push_back(entity_shapepadding);
+ }
+
+
+ // Add knots for shape subtraction margins
+ if (text->style->shape_subtract.set) {
+ for (auto *href : text->style->shape_subtract.hrefs) {
+ auto shape = href->getObject();
+ if (dynamic_cast<SPShape *>(shape)) {
+ auto entity_shapemargin = new TextKnotHolderEntityShapeMargin();
+ entity_shapemargin->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SIZER, "Text:shapemargin",
+ _("Adjust the shape's <b>text margin</b>."));
+ entity_shapemargin->set_shape(shape);
+ entity_shapemargin->update_knot();
+ entity.push_back(entity_shapemargin);
+ }
+ }
+ }
+
+ } else {
+ // 'inline-size' or normal text
+ TextKnotHolderEntityInlineSize *entity_inlinesize = new TextKnotHolderEntityInlineSize();
+
+ entity_inlinesize->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "Text:inlinesize",
+ _("Adjust the <b>inline size</b> (line length) of the text."));
+ entity.push_back(entity_inlinesize);
+ }
+
+ add_pattern_knotholder();
+ add_hatch_knotholder();
+}
+
+
+// TODO: this is derived from RectKnotHolderEntityWH because it used the same static function
+// set_internal as the latter before KnotHolderEntity was C++ified. Check whether this also makes
+// sense logically.
+class FlowtextKnotHolderEntity : public RectKnotHolderEntityWH {
+public:
+ Geom::Point knot_get() const override;
+ void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override;
+};
+
+Geom::Point
+FlowtextKnotHolderEntity::knot_get() const
+{
+ SPRect const *rect = dynamic_cast<SPRect const *>(item);
+ g_assert(rect != nullptr);
+
+ return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed);
+}
+
+void
+FlowtextKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state)
+{
+ set_internal(p, origin, state);
+}
+
+FlowtextKnotHolder::FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) :
+ KnotHolder(desktop, item, relhandler)
+{
+ g_assert(item != nullptr);
+
+ FlowtextKnotHolderEntity *entity_flowtext = new FlowtextKnotHolderEntity();
+ entity_flowtext->create(desktop, item, this, Inkscape::CANVAS_ITEM_CTRL_TYPE_SHAPER, "FlowText:entity",
+ _("Drag to resize the <b>flowed text frame</b>"));
+ entity.push_back(entity_flowtext);
+}
+
+/*
+ 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 :