diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/ui/shape-editor-knotholders.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream/1.2.2.tar.xz inkscape-upstream/1.2.2.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.cpp | 2610 |
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 : |