/** @file * Affine transform handles component */ /* Authors: * Krzysztof Kosiński * * Copyright (C) 2009 Authors * Released under GNU GPL, read the file 'COPYING' for more information */ #include #include #include #include #include #include <2geom/transforms.h> #include "desktop.h" #include "desktop-handles.h" #include "display/sodipodi-ctrlrect.h" #include "preferences.h" #include "ui/tool/commit-events.h" #include "ui/tool/control-point.h" #include "ui/tool/event-utils.h" #include "ui/tool/transform-handle-set.h" // FIXME BRAIN DAMAGE WARNING: this is a global variable in select-context.cpp // It should be moved to a header extern GdkPixbuf *handles[]; GType sp_select_context_get_type(); namespace Inkscape { namespace UI { namespace { Gtk::AnchorType corner_to_anchor(unsigned c) { switch (c % 4) { case 0: return Gtk::ANCHOR_NE; case 1: return Gtk::ANCHOR_NW; case 2: return Gtk::ANCHOR_SW; default: return Gtk::ANCHOR_SE; } } Gtk::AnchorType side_to_anchor(unsigned s) { switch (s % 4) { case 0: return Gtk::ANCHOR_N; case 1: return Gtk::ANCHOR_W; case 2: return Gtk::ANCHOR_S; default: return Gtk::ANCHOR_E; } } // TODO move those two functions into a common place double snap_angle(double a) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); double unit_angle = M_PI / snaps; return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI); } double snap_increment_degrees() { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); return 180.0 / snaps; } ControlPoint::ColorSet thandle_cset = { {0x000000ff, 0x000000ff}, {0x00ff6600, 0x000000ff}, {0x00ff6600, 0x000000ff} }; ControlPoint::ColorSet center_cset = { {0x00000000, 0x000000ff}, {0x00000000, 0xff0000b0}, {0x00000000, 0xff0000b0} }; } // anonymous namespace /** Base class for node transform handles to simplify implementation */ class TransformHandle : public ControlPoint { public: TransformHandle(TransformHandleSet &th, Gtk::AnchorType anchor, Glib::RefPtr pb) : ControlPoint(th._desktop, Geom::Point(), anchor, pb, &thandle_cset, th._transform_handle_group) , _th(th) { setVisible(false); } protected: virtual void startTransform() {} virtual void endTransform() {} virtual Geom::Matrix computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0; virtual CommitEvent getCommitEvent() = 0; Geom::Matrix _last_transform; Geom::Point _origin; TransformHandleSet &_th; private: virtual bool grabbed(GdkEventMotion *) { _origin = position(); _last_transform.setIdentity(); startTransform(); _th._setActiveHandle(this); _cset = &invisible_cset; _setState(_state); return false; } virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event) { Geom::Matrix t = computeTransform(new_pos, event); // protect against degeneracies if (t.isSingular()) return; Geom::Matrix incr = _last_transform.inverse() * t; if (incr.isSingular()) return; _th.signal_transform.emit(incr); _last_transform = t; } virtual void ungrabbed(GdkEventButton *) { _th._clearActiveHandle(); _cset = &thandle_cset; _setState(_state); endTransform(); _th.signal_commit.emit(getCommitEvent()); } }; class ScaleHandle : public TransformHandle { public: ScaleHandle(TransformHandleSet &th, Gtk::AnchorType anchor, Glib::RefPtr pb) : TransformHandle(th, anchor, pb) {} protected: virtual Glib::ustring _getTip(unsigned state) { if (state_held_control(state)) { if (state_held_shift(state)) { return C_("Transform handle tip", "Shift+Ctrl: scale uniformly about the rotation center"); } return C_("Transform handle tip", "Ctrl: scale uniformly"); } if (state_held_shift(state)) { if (state_held_alt(state)) { return C_("Transform handle tip", "Shift+Alt: scale using an integer ratio about the rotation center"); } return C_("Transform handle tip", "Shift: scale from the rotation center"); } if (state_held_alt(state)) { return C_("Transform handle tip", "Alt: scale using an integer ratio"); } return C_("Transform handle tip", "Scale handle: drag to scale the selection"); } virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { return format_tip(C_("Transform handle tip", "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100); } virtual bool _hasDragTips() { return true; } static double _last_scale_x, _last_scale_y; }; double ScaleHandle::_last_scale_x = 1.0; double ScaleHandle::_last_scale_y = 1.0; /// Corner scaling handle for node transforms class ScaleCornerHandle : public ScaleHandle { public: ScaleCornerHandle(TransformHandleSet &th, unsigned corner) : ScaleHandle(th, corner_to_anchor(corner), _corner_to_pixbuf(corner)) , _corner(corner) {} protected: virtual void startTransform() { _sc_center = _th.rotationCenter(); _sc_opposite = _th.bounds().corner(_corner + 2); _last_scale_x = _last_scale_y = 1.0; } virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; Geom::Point vold = _origin - scc, vnew = new_pos - scc; // avoid exploding the selection if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0)) return Geom::identity(); double scale[2] = { vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y] }; if (held_alt(*event)) { for (unsigned i = 0; i < 2; ++i) { if (scale[i] >= 1.0) scale[i] = round(scale[i]); else scale[i] = 1.0 / round(1.0 / scale[i]); } } else if (held_control(*event)) { scale[0] = scale[1] = std::min(scale[0], scale[1]); } _last_scale_x = scale[0]; _last_scale_y = scale[1]; Geom::Matrix t = Geom::Translate(-scc) * Geom::Scale(scale[0], scale[1]) * Geom::Translate(scc); return t; } virtual CommitEvent getCommitEvent() { return _last_transform.isUniformScale() ? COMMIT_MOUSE_SCALE_UNIFORM : COMMIT_MOUSE_SCALE; } private: static Glib::RefPtr _corner_to_pixbuf(unsigned c) { sp_select_context_get_type(); switch (c % 2) { case 0: return Glib::wrap(handles[1], true); default: return Glib::wrap(handles[0], true); } } Geom::Point _sc_center; Geom::Point _sc_opposite; unsigned _corner; }; /// Side scaling handle for node transforms class ScaleSideHandle : public ScaleHandle { public: ScaleSideHandle(TransformHandleSet &th, unsigned side) : ScaleHandle(th, side_to_anchor(side), _side_to_pixbuf(side)) , _side(side) {} protected: virtual void startTransform() { _sc_center = _th.rotationCenter(); Geom::Rect b = _th.bounds(); _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); _last_scale_x = _last_scale_y = 1.0; } virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; Geom::Point vs; Geom::Dim2 d1 = static_cast((_side + 1) % 2); Geom::Dim2 d2 = static_cast(_side % 2); // avoid exploding the selection if (Geom::are_near(scc[d1], _origin[d1])) return Geom::identity(); vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1]; if (held_alt(*event)) { if (vs[d1] >= 1.0) vs[d1] = round(vs[d1]); else vs[d1] = 1.0 / round(1.0 / vs[d1]); } vs[d2] = held_control(*event) ? vs[d1] : 1.0; _last_scale_x = vs[Geom::X]; _last_scale_y = vs[Geom::Y]; Geom::Matrix t = Geom::Translate(-scc) * Geom::Scale(vs) * Geom::Translate(scc); return t; } virtual CommitEvent getCommitEvent() { return _last_transform.isUniformScale() ? COMMIT_MOUSE_SCALE_UNIFORM : COMMIT_MOUSE_SCALE; } private: static Glib::RefPtr _side_to_pixbuf(unsigned c) { sp_select_context_get_type(); switch (c % 2) { case 0: return Glib::wrap(handles[3], true); default: return Glib::wrap(handles[2], true); } } Geom::Point _sc_center; Geom::Point _sc_opposite; unsigned _side; }; /// Rotation handle for node transforms class RotateHandle : public TransformHandle { public: RotateHandle(TransformHandleSet &th, unsigned corner) : TransformHandle(th, corner_to_anchor(corner), _corner_to_pixbuf(corner)) , _corner(corner) {} protected: virtual void startTransform() { _rot_center = _th.rotationCenter(); _rot_opposite = _th.bounds().corner(_corner + 2); _last_angle = 0; } virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center; double angle = Geom::angle_between(_origin - rotc, new_pos - rotc); if (held_control(*event)) { angle = snap_angle(angle); } _last_angle = angle; Geom::Matrix t = Geom::Translate(-rotc) * Geom::Rotate(angle) * Geom::Translate(rotc); return t; } virtual CommitEvent getCommitEvent() { return COMMIT_MOUSE_ROTATE; } virtual Glib::ustring _getTip(unsigned state) { if (state_held_shift(state)) { if (state_held_control(state)) { return format_tip(C_("Transform handle tip", "Shift+Ctrl: rotate around the opposite corner and snap " "angle to %f° increments"), snap_increment_degrees()); } return C_("Transform handle tip", "Shift: rotate around the opposite corner"); } if (state_held_control(state)) { return format_tip(C_("Transform handle tip", "Ctrl: snap angle to %f° increments"), snap_increment_degrees()); } return C_("Transform handle tip", "Rotation handle: drag to rotate " "the selection around the rotation center"); } virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { return format_tip(C_("Transform handle tip", "Rotate by %.2f°"), _last_angle * 360.0); } virtual bool _hasDragTips() { return true; } private: static Glib::RefPtr _corner_to_pixbuf(unsigned c) { sp_select_context_get_type(); switch (c % 4) { case 0: return Glib::wrap(handles[10], true); case 1: return Glib::wrap(handles[8], true); case 2: return Glib::wrap(handles[6], true); default: return Glib::wrap(handles[4], true); } } Geom::Point _rot_center; Geom::Point _rot_opposite; unsigned _corner; static double _last_angle; }; double RotateHandle::_last_angle = 0; class SkewHandle : public TransformHandle { public: SkewHandle(TransformHandleSet &th, unsigned side) : TransformHandle(th, side_to_anchor(side), _side_to_pixbuf(side)) , _side(side) {} protected: virtual void startTransform() { _skew_center = _th.rotationCenter(); Geom::Rect b = _th.bounds(); _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); _last_angle = 0; _last_horizontal = _side % 2; } virtual Geom::Matrix computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) { Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite; // d1 and d2 are reversed with respect to ScaleSideHandle Geom::Dim2 d1 = static_cast(_side % 2); Geom::Dim2 d2 = static_cast((_side + 1) % 2); Geom::Point proj, scale(1.0, 1.0); // Skew handles allow scaling up to integer multiples of the original size // in the second direction; prevent explosions // TODO should the scaling part be only active with Alt? if (!Geom::are_near(_origin[d2], scc[d2])) { scale[d2] = (new_pos - scc)[d2] / (_origin - scc)[d2]; } if (scale[d2] < 1.0) { scale[d2] = copysign(1.0, scale[d2]); } else { scale[d2] = floor(scale[d2]); } // Calculate skew angle. The angle is calculated with regards to the point obtained // by projecting the handle position on the relevant side of the bounding box. // This avoids degeneracies when moving the skew angle over the rotation center proj[d1] = new_pos[d1]; proj[d2] = scc[d2] + (_origin[d2] - scc[d2]) * scale[d2]; double angle = 0; if (!Geom::are_near(proj[d2], scc[d2])) angle = Geom::angle_between(_origin - scc, proj - scc); if (held_control(*event)) angle = snap_angle(angle); // skew matrix has the from [[1, k],[0, 1]] for horizontal skew // and [[1,0],[k,1]] for vertical skew. Geom::Matrix skew = Geom::identity(); // correct the sign of the tangent skew[d2 + 1] = (d1 == Geom::X ? -1.0 : 1.0) * tan(angle); _last_angle = angle; Geom::Matrix t = Geom::Translate(-scc) * Geom::Scale(scale) * skew * Geom::Translate(scc); return t; } virtual CommitEvent getCommitEvent() { return _side % 2 ? COMMIT_MOUSE_SKEW_Y : COMMIT_MOUSE_SKEW_X; } virtual Glib::ustring _getTip(unsigned state) { if (state_held_shift(state)) { if (state_held_control(state)) { return format_tip(C_("Transform handle tip", "Shift+Ctrl: skew about the rotation center with snapping " "to %f° increments"), snap_increment_degrees()); } return C_("Transform handle tip", "Shift: skew about the rotation center"); } if (state_held_control(state)) { return format_tip(C_("Transform handle tip", "Ctrl: snap skew angle to %f° increments"), snap_increment_degrees()); } return C_("Transform handle tip", "Skew handle: drag to skew (shear) selection about " "the opposite handle"); } virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) { if (_last_horizontal) { return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"), _last_angle * 360.0); } else { return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"), _last_angle * 360.0); } } virtual bool _hasDragTips() { return true; } private: static Glib::RefPtr _side_to_pixbuf(unsigned s) { sp_select_context_get_type(); switch (s % 4) { case 0: return Glib::wrap(handles[9], true); case 1: return Glib::wrap(handles[7], true); case 2: return Glib::wrap(handles[5], true); default: return Glib::wrap(handles[11], true); } } Geom::Point _skew_center; Geom::Point _skew_opposite; unsigned _side; static bool _last_horizontal; static double _last_angle; }; bool SkewHandle::_last_horizontal = false; double SkewHandle::_last_angle = 0; class RotationCenter : public ControlPoint { public: RotationCenter(TransformHandleSet &th) : ControlPoint(th._desktop, Geom::Point(), Gtk::ANCHOR_CENTER, _get_pixbuf(), ¢er_cset, th._transform_handle_group) , _th(th) { setVisible(false); } protected: virtual Glib::ustring _getTip(unsigned /*state*/) { return C_("Transform handle tip", "Rotation center: drag to change the origin of transforms"); } private: static Glib::RefPtr _get_pixbuf() { sp_select_context_get_type(); return Glib::wrap(handles[12], true); } TransformHandleSet &_th; }; TransformHandleSet::TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group) : Manipulator(d) , _active(0) , _transform_handle_group(th_group) , _mode(MODE_SCALE) , _in_transform(false) , _visible(true) { _trans_outline = static_cast(sp_canvas_item_new(sp_desktop_controls(_desktop), SP_TYPE_CTRLRECT, NULL)); sp_canvas_item_hide(_trans_outline); _trans_outline->setDashed(true); for (unsigned i = 0; i < 4; ++i) { _scale_corners[i] = new ScaleCornerHandle(*this, i); _scale_sides[i] = new ScaleSideHandle(*this, i); _rot_corners[i] = new RotateHandle(*this, i); _skew_sides[i] = new SkewHandle(*this, i); } _center = new RotationCenter(*this); // when transforming, update rotation center position signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform)); } TransformHandleSet::~TransformHandleSet() { for (unsigned i = 0; i < 17; ++i) { delete _handles[i]; } } /** Sets the mode of transform handles (scale or rotate). */ void TransformHandleSet::setMode(Mode m) { _mode = m; _updateVisibility(_visible); } Geom::Rect TransformHandleSet::bounds() { return Geom::Rect(*_scale_corners[0], *_scale_corners[2]); } ControlPoint &TransformHandleSet::rotationCenter() { return *_center; } void TransformHandleSet::setVisible(bool v) { if (_visible != v) { _visible = v; _updateVisibility(_visible); } } void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center) { if (_in_transform) { _trans_outline->setRectangle(r); } else { for (unsigned i = 0; i < 4; ++i) { _scale_corners[i]->move(r.corner(i)); _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); _rot_corners[i]->move(r.corner(i)); _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); } if (!preserve_center) _center->move(r.midpoint()); if (_visible) _updateVisibility(true); } } bool TransformHandleSet::event(GdkEvent*) { return false; } void TransformHandleSet::_emitTransform(Geom::Matrix const &t) { signal_transform.emit(t); _center->transform(t); } void TransformHandleSet::_setActiveHandle(ControlPoint *th) { _active = th; if (_in_transform) throw std::logic_error("Transform initiated when another transform in progress"); _in_transform = true; // hide all handles except the active one _updateVisibility(false); sp_canvas_item_show(_trans_outline); } void TransformHandleSet::_clearActiveHandle() { // This can only be called from handles, so they had to be visible before _setActiveHandle sp_canvas_item_hide(_trans_outline); _active = 0; _in_transform = false; _updateVisibility(_visible); } /** Update the visibility of transformation handles according to settings and the dimensions * of the bounding box. It hides the handles that would have no effect or lead to * discontinuities. Additionally, side handles for which there is no space are not shown. */ void TransformHandleSet::_updateVisibility(bool v) { if (v) { Geom::Rect b = bounds(); Geom::Point handle_size( gdk_pixbuf_get_width(handles[0]) / _desktop->current_zoom(), gdk_pixbuf_get_height(handles[0]) / _desktop->current_zoom()); Geom::Point bp = b.dimensions(); // do not scale when the bounding rectangle has zero width or height bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0); // do not rotate if the bounding rectangle is degenerate bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0); bool show_scale_side[2], show_skew[2]; // show sides if: // a) there is enough space between corner handles, or // b) corner handles are not shown, but side handles make sense // this affects horizontal and vertical scale handles; skew handles never // make sense if rotate handles are not shown for (unsigned i = 0; i < 2; ++i) { Geom::Dim2 d = static_cast(i); Geom::Dim2 otherd = static_cast((i+1)%2); show_scale_side[i] = (_mode == MODE_SCALE); show_scale_side[i] &= (show_scale ? bp[d] >= handle_size[d] : !Geom::are_near(bp[otherd], 0)); show_skew[i] = (show_rotate && bp[d] >= handle_size[d] && !Geom::are_near(bp[otherd], 0)); } for (unsigned i = 0; i < 4; ++i) { _scale_corners[i]->setVisible(show_scale); _rot_corners[i]->setVisible(show_rotate); _scale_sides[i]->setVisible(show_scale_side[i%2]); _skew_sides[i]->setVisible(show_skew[i%2]); } // show rotation center if there is enough space (?) _center->setVisible(show_rotate /*&& bp[Geom::X] > handle_size[Geom::X] && bp[Geom::Y] > handle_size[Geom::Y]*/); } else { for (unsigned i = 0; i < 17; ++i) { if (_handles[i] != _active) _handles[i]->setVisible(false); } } } } // namespace UI } // namespace Inkscape /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :