diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm
index 016c2e800..951f3bd0f 100644
--- a/lib/Slic3r/GUI/Plater.pm
+++ b/lib/Slic3r/GUI/Plater.pm
@@ -153,16 +153,15 @@ sub new {
     };
     
     # callback to react to gizmo rotate
-    # omitting last three parameters means rotation around Z
-    # otherwise they are the components of the rotation axis vector
     my $on_gizmo_rotate = sub {
+        my ($angle) = @_;
+        $self->rotate(rad2deg($angle), Z, 'absolute');
+    };
+    
+    # callback to react to gizmo flatten
+    my $on_gizmo_flatten = sub {
         my ($angle, $axis_x, $axis_y, $axis_z) = @_;
-        if (!defined $axis_x) {
-            $self->rotate(rad2deg($angle), Z, 'absolute');
-        }
-        else {
-            $self->rotate(rad2deg($angle), undef, 'absolute', $axis_x, $axis_y, $axis_z) if $angle != 0;
-        }
+        $self->rotate(rad2deg($angle), undef, 'absolute', $axis_x, $axis_y, $axis_z) if $angle != 0;
     };
 
     # callback to update object's geometry info while using gizmos
@@ -263,6 +262,7 @@ sub new {
         Slic3r::GUI::_3DScene::register_on_enable_action_buttons_callback($self->{canvas3D}, $enable_action_buttons);
         Slic3r::GUI::_3DScene::register_on_gizmo_scale_uniformly_callback($self->{canvas3D}, $on_gizmo_scale_uniformly);
         Slic3r::GUI::_3DScene::register_on_gizmo_rotate_callback($self->{canvas3D}, $on_gizmo_rotate);
+        Slic3r::GUI::_3DScene::register_on_gizmo_flatten_callback($self->{canvas3D}, $on_gizmo_flatten);
         Slic3r::GUI::_3DScene::register_on_update_geometry_info_callback($self->{canvas3D}, $on_update_geometry_info);
         Slic3r::GUI::_3DScene::register_action_add_callback($self->{canvas3D}, $on_action_add);
         Slic3r::GUI::_3DScene::register_action_delete_callback($self->{canvas3D}, $on_action_delete);
@@ -879,6 +879,15 @@ sub load_files {
             $model->convert_multipart_object(scalar(@$nozzle_dmrs)) if $dialog->ShowModal() == wxID_YES;
         }
         
+        # objects imported from 3mf require a call to center_around_origin to have gizmos working properly and this call
+        # need to be done after looks_like_multipart_object detection
+        if ($input_file =~ /.3[mM][fF]$/)
+        {
+            foreach my $model_object (@{$model->objects}) {
+                $model_object->center_around_origin;  # also aligns object to Z = 0
+            }
+        }
+        
         if ($one_by_one) {
             push @obj_idx, $self->load_model_objects(@{$model->objects});
         } else {
diff --git a/resources/icons/overlay/move_hover.png b/resources/icons/overlay/move_hover.png
new file mode 100644
index 000000000..99dc4cf8d
Binary files /dev/null and b/resources/icons/overlay/move_hover.png differ
diff --git a/resources/icons/overlay/move_off.png b/resources/icons/overlay/move_off.png
new file mode 100644
index 000000000..cd4f130e1
Binary files /dev/null and b/resources/icons/overlay/move_off.png differ
diff --git a/resources/icons/overlay/move_on.png b/resources/icons/overlay/move_on.png
new file mode 100644
index 000000000..7e78e0e6a
Binary files /dev/null and b/resources/icons/overlay/move_on.png differ
diff --git a/xs/src/libslic3r/Format/3mf.cpp b/xs/src/libslic3r/Format/3mf.cpp
index 4dc40bd3c..464cbb8e6 100644
--- a/xs/src/libslic3r/Format/3mf.cpp
+++ b/xs/src/libslic3r/Format/3mf.cpp
@@ -601,8 +601,6 @@ namespace Slic3r {
 
             if (!_generate_volumes(*object.second, obj_geometry->second, *volumes_ptr))
                 return false;
-
-            object.second->center_around_origin();
         }
 
         // fixes the min z of the model if negative
diff --git a/xs/src/libslic3r/GCodeTimeEstimator.cpp b/xs/src/libslic3r/GCodeTimeEstimator.cpp
index f4f6472e5..7471367fe 100644
--- a/xs/src/libslic3r/GCodeTimeEstimator.cpp
+++ b/xs/src/libslic3r/GCodeTimeEstimator.cpp
@@ -301,7 +301,7 @@ namespace Slic3r {
             if (((_mode == Normal) && (gcode_line == Normal_First_M73_Output_Placeholder_Tag)) ||
                 ((_mode == Silent) && (gcode_line == Silent_First_M73_Output_Placeholder_Tag)))
             {
-                sprintf(time_line, time_mask.c_str(), std::to_string(0), _get_time_minutes(_time).c_str());
+                sprintf(time_line, time_mask.c_str(), "0", _get_time_minutes(_time).c_str());
                 gcode_line = time_line;
             }
             else
diff --git a/xs/src/slic3r/GUI/3DScene.cpp b/xs/src/slic3r/GUI/3DScene.cpp
index 57639fed0..ed2f8690d 100644
--- a/xs/src/slic3r/GUI/3DScene.cpp
+++ b/xs/src/slic3r/GUI/3DScene.cpp
@@ -313,6 +313,14 @@ void GLVolume::set_select_group_id(const std::string& select_by)
         select_group_id = composite_id;
 }
 
+void GLVolume::set_drag_group_id(const std::string& drag_by)
+{
+    if (drag_by == "object")
+        drag_group_id = object_idx() * 1000;
+    else if (drag_by == "instance")
+        drag_group_id = object_idx() * 1000 + instance_idx();
+}
+
 const Transform3f& GLVolume::world_matrix() const
 {
     if (m_world_matrix_dirty)
@@ -666,11 +674,7 @@ std::vector<int> GLVolumeCollection::load_object(
             v.indexed_vertex_array.finalize_geometry(use_VBOs);
             v.composite_id = obj_idx * 1000000 + volume_idx * 1000 + instance_idx;
             v.set_select_group_id(select_by);
-            if (drag_by == "object")
-                v.drag_group_id = obj_idx * 1000;
-            else if (drag_by == "instance")
-                v.drag_group_id = obj_idx * 1000 + instance_idx;
-
+            v.set_drag_group_id(drag_by);
             if (!model_volume->modifier)
             {
                 v.set_convex_hull(model_volume->get_convex_hull());
@@ -963,6 +967,15 @@ void GLVolumeCollection::set_select_by(const std::string& select_by)
     }
 }
 
+void GLVolumeCollection::set_drag_by(const std::string& drag_by)
+{
+    for (GLVolume *vol : this->volumes)
+    {
+        if (vol != nullptr)
+            vol->set_drag_group_id(drag_by);
+    }
+}
+
 std::vector<double> GLVolumeCollection::get_current_print_zs(bool active_only) const
 {
     // Collect layer top positions of all volumes.
@@ -2044,6 +2057,11 @@ void _3DScene::register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callb
     s_canvas_mgr.register_on_gizmo_rotate_callback(canvas, callback);
 }
 
+void _3DScene::register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback)
+{
+    s_canvas_mgr.register_on_gizmo_flatten_callback(canvas, callback);
+}
+
 void _3DScene::register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback)
 {
     s_canvas_mgr.register_on_update_geometry_info_callback(canvas, callback);
diff --git a/xs/src/slic3r/GUI/3DScene.hpp b/xs/src/slic3r/GUI/3DScene.hpp
index ec9d1a501..6db58aa16 100644
--- a/xs/src/slic3r/GUI/3DScene.hpp
+++ b/xs/src/slic3r/GUI/3DScene.hpp
@@ -338,6 +338,7 @@ public:
     void set_convex_hull(const TriangleMesh& convex_hull);
 
     void set_select_group_id(const std::string& select_by);
+    void set_drag_group_id(const std::string& drag_by);
 
     int                 object_idx() const { return this->composite_id / 1000000; }
     int                 volume_idx() const { return (this->composite_id / 1000) % 1000; }
@@ -449,6 +450,7 @@ public:
     void update_colors_by_extruder(const DynamicPrintConfig* config);
 
     void set_select_by(const std::string& select_by);
+    void set_drag_by(const std::string& drag_by);
 
     // Returns a vector containing the sorted list of all the print_zs of the volumes contained in this collection
     std::vector<double> get_current_print_zs(bool active_only) const;
@@ -556,6 +558,7 @@ public:
     static void register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback);
     static void register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback);
     static void register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback);
+    static void register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback);
     static void register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback);
 
     static void register_action_add_callback(wxGLCanvas* canvas, void* callback);
diff --git a/xs/src/slic3r/GUI/GLCanvas3D.cpp b/xs/src/slic3r/GUI/GLCanvas3D.cpp
index 161ce89da..fdee693ee 100644
--- a/xs/src/slic3r/GUI/GLCanvas3D.cpp
+++ b/xs/src/slic3r/GUI/GLCanvas3D.cpp
@@ -1123,7 +1123,6 @@ const float GLCanvas3D::Gizmos::OverlayGapY = 5.0f * OverlayTexturesScale;
 GLCanvas3D::Gizmos::Gizmos()
     : m_enabled(false)
     , m_current(Undefined)
-    , m_dragging(false)
 {
 }
 
@@ -1134,13 +1133,35 @@ GLCanvas3D::Gizmos::~Gizmos()
 
 bool GLCanvas3D::Gizmos::init(GLCanvas3D& parent)
 {
-    GLGizmoBase* gizmo = new GLGizmoScale3D(parent);
+    GLGizmoBase* gizmo = new GLGizmoMove3D(parent);
     if (gizmo == nullptr)
         return false;
 
     if (!gizmo->init())
         return false;
 
+    // temporary disable z grabber
+    gizmo->disable_grabber(2);
+
+    m_gizmos.insert(GizmosMap::value_type(Move, gizmo));
+
+    gizmo = new GLGizmoScale3D(parent);
+    if (gizmo == nullptr)
+        return false;
+
+    if (!gizmo->init())
+        return false;
+
+    // temporary disable x grabbers
+    gizmo->disable_grabber(0);
+    gizmo->disable_grabber(1);
+    // temporary disable y grabbers
+    gizmo->disable_grabber(2);
+    gizmo->disable_grabber(3);
+    // temporary disable z grabbers
+    gizmo->disable_grabber(4);
+    gizmo->disable_grabber(5);
+
     m_gizmos.insert(GizmosMap::value_type(Scale, gizmo));
 
     gizmo = new GLGizmoRotate3D(parent);
@@ -1156,6 +1177,10 @@ bool GLCanvas3D::Gizmos::init(GLCanvas3D& parent)
         return false;
     }
 
+    // temporary disable x and y grabbers
+    gizmo->disable_grabber(0);
+    gizmo->disable_grabber(1);
+
     m_gizmos.insert(GizmosMap::value_type(Rotate, gizmo));
 
     gizmo = new GLGizmoFlatten(parent);
@@ -1336,12 +1361,12 @@ bool GLCanvas3D::Gizmos::is_running() const
 
 bool GLCanvas3D::Gizmos::is_dragging() const
 {
-    return m_dragging;
+    GLGizmoBase* curr = _get_current();
+    return (curr != nullptr) ? curr->is_dragging() : false;
 }
 
 void GLCanvas3D::Gizmos::start_dragging(const BoundingBoxf3& box)
 {
-    m_dragging = true;
     GLGizmoBase* curr = _get_current();
     if (curr != nullptr)
         curr->start_dragging(box);
@@ -1349,12 +1374,30 @@ void GLCanvas3D::Gizmos::start_dragging(const BoundingBoxf3& box)
 
 void GLCanvas3D::Gizmos::stop_dragging()
 {
-    m_dragging = false;
     GLGizmoBase* curr = _get_current();
     if (curr != nullptr)
         curr->stop_dragging();
 }
 
+Vec3d GLCanvas3D::Gizmos::get_position() const
+{
+    if (!m_enabled)
+        return Vec3d::Zero();
+
+    GizmosMap::const_iterator it = m_gizmos.find(Move);
+    return (it != m_gizmos.end()) ? reinterpret_cast<GLGizmoMove3D*>(it->second)->get_position() : Vec3d::Zero();
+}
+
+void GLCanvas3D::Gizmos::set_position(const Vec3d& position)
+{
+    if (!m_enabled)
+        return;
+
+    GizmosMap::const_iterator it = m_gizmos.find(Move);
+    if (it != m_gizmos.end())
+        reinterpret_cast<GLGizmoMove3D*>(it->second)->set_position(position);
+}
+
 float GLCanvas3D::Gizmos::get_scale() const
 {
     if (!m_enabled)
@@ -2143,6 +2186,7 @@ void GLCanvas3D::set_select_by(const std::string& value)
 void GLCanvas3D::set_drag_by(const std::string& value)
 {
     m_drag_by = value;
+    m_volumes.set_drag_by(value);
 }
 
 const std::string& GLCanvas3D::get_select_by() const
@@ -2150,6 +2194,11 @@ const std::string& GLCanvas3D::get_select_by() const
     return m_select_by;
 }
 
+const std::string& GLCanvas3D::get_drag_by() const
+{
+    return m_drag_by;
+}
+
 float GLCanvas3D::get_camera_zoom() const
 {
     return m_camera.zoom;
@@ -2326,6 +2375,7 @@ void GLCanvas3D::update_gizmos_data()
             ModelInstance* model_instance = model_object->instances[0];
             if (model_instance != nullptr)
             {
+                m_gizmos.set_position(Vec3d(model_instance->offset(0), model_instance->offset(1), 0.0));
                 m_gizmos.set_scale(model_instance->scaling_factor);
                 m_gizmos.set_angle_z(model_instance->rotation);
                 m_gizmos.set_flattening_data(model_object);
@@ -2334,6 +2384,7 @@ void GLCanvas3D::update_gizmos_data()
     }
     else
     {
+        m_gizmos.set_position(Vec3d::Zero());
         m_gizmos.set_scale(1.0f);
         m_gizmos.set_angle_z(0.0f);
         m_gizmos.set_flattening_data(nullptr);
@@ -2699,6 +2750,12 @@ void GLCanvas3D::register_on_gizmo_rotate_callback(void* callback)
         m_on_gizmo_rotate_callback.register_callback(callback);
 }
 
+void GLCanvas3D::register_on_gizmo_flatten_callback(void* callback)
+{
+    if (callback != nullptr)
+        m_on_gizmo_flatten_callback.register_callback(callback);
+}
+
 void GLCanvas3D::register_on_update_geometry_info_callback(void* callback)
 {
     if (callback != nullptr)
@@ -3016,7 +3073,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
                 if (normal(0) != 0.0 || normal(1) != 0.0 || normal(2) != 0.0) {
                     Vec3d axis = normal(2) > 0.999 ? Vec3d::UnitX() : normal.cross(-Vec3d::UnitZ()).normalized();
                     float angle = acos(clamp(-1.0, 1.0, -normal(2)));
-                    m_on_gizmo_rotate_callback.call(angle, (float)axis(0), (float)axis(1), (float)axis(2));
+                    m_on_gizmo_flatten_callback.call(angle, (float)axis(0), (float)axis(1), (float)axis(2));
                 }
             }
 
@@ -3175,6 +3232,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
 
         switch (m_gizmos.get_current_type())
         {
+        case Gizmos::Move:
+        {
+            // Apply new temporary offset
+            GLVolume* volume = m_volumes.volumes[m_mouse.drag.gizmo_volume_idx];
+            Vec3d offset = m_gizmos.get_position() - volume->get_offset();
+            for (GLVolume* v : volumes)
+            {
+                v->set_offset(v->get_offset() + offset);
+            }
+            update_position_values(volume->get_offset());
+            break;
+        }
         case Gizmos::Scale:
         {
             // Apply new temporary scale factor
@@ -3182,8 +3251,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
             for (GLVolume* v : volumes)
             {
                 v->set_scaling_factor((double)scale_factor);
-                update_scale_values((double)scale_factor);
             }
+            update_scale_values((double)scale_factor);
             break;
         }
         case Gizmos::Rotate:
@@ -3193,8 +3262,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
             for (GLVolume* v : volumes)
             {
                 v->set_rotation((double)angle_z);
-                update_rotation_value((double)angle_z, Z);
             }
+            update_rotation_value((double)angle_z, Z);
             break;
         }
         default:
@@ -3304,6 +3373,27 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
         {
             switch (m_gizmos.get_current_type())
             {
+            case Gizmos::Move:
+            {
+                // get all volumes belonging to the same group, if any
+                std::vector<int> volume_idxs;
+                int vol_id = m_mouse.drag.gizmo_volume_idx;
+                int group_id = m_volumes.volumes[vol_id]->select_group_id;
+                if (group_id == -1)
+                    volume_idxs.push_back(vol_id);
+                else
+                {
+                    for (int i = 0; i < (int)m_volumes.volumes.size(); ++i)
+                    {
+                        if (m_volumes.volumes[i]->select_group_id == group_id)
+                            volume_idxs.push_back(i);
+                    }
+                }
+
+                _on_move(volume_idxs);
+
+                break;
+            }
             case Gizmos::Scale:
             {
                 m_on_gizmo_scale_uniformly_callback.call((double)m_gizmos.get_scale());
@@ -3759,6 +3849,7 @@ void GLCanvas3D::_deregister_callbacks()
     m_on_enable_action_buttons_callback.deregister_callback();
     m_on_gizmo_scale_uniformly_callback.deregister_callback();
     m_on_gizmo_rotate_callback.deregister_callback();
+    m_on_gizmo_flatten_callback.deregister_callback();
     m_on_update_geometry_info_callback.deregister_callback();
 
     m_action_add_callback.deregister_callback();
diff --git a/xs/src/slic3r/GUI/GLCanvas3D.hpp b/xs/src/slic3r/GUI/GLCanvas3D.hpp
index c5b4581fa..c4b46c44d 100644
--- a/xs/src/slic3r/GUI/GLCanvas3D.hpp
+++ b/xs/src/slic3r/GUI/GLCanvas3D.hpp
@@ -105,7 +105,6 @@ class GLCanvas3D
         void reset() { first_volumes.clear(); }
     };
 
-public:
     struct Camera
     {
         enum EType : unsigned char
@@ -336,6 +335,7 @@ public:
         enum EType : unsigned char
         {
             Undefined,
+            Move,
             Scale,
             Rotate,
             Flatten,
@@ -347,7 +347,6 @@ public:
         typedef std::map<EType, GLGizmoBase*> GizmosMap;
         GizmosMap m_gizmos;
         EType m_current;
-        bool m_dragging;
 
     public:
         Gizmos();
@@ -376,6 +375,9 @@ public:
         void start_dragging(const BoundingBoxf3& box);
         void stop_dragging();
 
+        Vec3d get_position() const;
+        void set_position(const Vec3d& position);
+
         float get_scale() const;
         void set_scale(float scale);
 
@@ -438,7 +440,6 @@ public:
         void render(const GLCanvas3D& canvas) const;
     };
 
-private:
     wxGLCanvas* m_canvas;
     wxGLContext* m_context;
     LegendTexture m_legend_texture;
@@ -501,6 +502,7 @@ private:
     PerlCallback m_on_enable_action_buttons_callback;
     PerlCallback m_on_gizmo_scale_uniformly_callback;
     PerlCallback m_on_gizmo_rotate_callback;
+    PerlCallback m_on_gizmo_flatten_callback;
     PerlCallback m_on_update_geometry_info_callback;
 
     PerlCallback m_action_add_callback;
@@ -557,6 +559,7 @@ public:
     void set_drag_by(const std::string& value);
 
     const std::string& get_select_by() const;
+    const std::string& get_drag_by() const;
 
     float get_camera_zoom() const;
 
@@ -623,6 +626,7 @@ public:
     void register_on_enable_action_buttons_callback(void* callback);
     void register_on_gizmo_scale_uniformly_callback(void* callback);
     void register_on_gizmo_rotate_callback(void* callback);
+    void register_on_gizmo_flatten_callback(void* callback);
     void register_on_update_geometry_info_callback(void* callback);
 
     void register_action_add_callback(void* callback);
diff --git a/xs/src/slic3r/GUI/GLCanvas3DManager.cpp b/xs/src/slic3r/GUI/GLCanvas3DManager.cpp
index a09d83b89..3445d4b65 100644
--- a/xs/src/slic3r/GUI/GLCanvas3DManager.cpp
+++ b/xs/src/slic3r/GUI/GLCanvas3DManager.cpp
@@ -700,6 +700,13 @@ void GLCanvas3DManager::register_on_gizmo_rotate_callback(wxGLCanvas* canvas, vo
         it->second->register_on_gizmo_rotate_callback(callback);
 }
 
+void GLCanvas3DManager::register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback)
+{
+    CanvasesMap::iterator it = _get_canvas(canvas);
+    if (it != m_canvases.end())
+        it->second->register_on_gizmo_flatten_callback(callback);
+}
+
 void GLCanvas3DManager::register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback)
 {
     CanvasesMap::iterator it = _get_canvas(canvas);
diff --git a/xs/src/slic3r/GUI/GLCanvas3DManager.hpp b/xs/src/slic3r/GUI/GLCanvas3DManager.hpp
index 1c715a9a3..b808c022e 100644
--- a/xs/src/slic3r/GUI/GLCanvas3DManager.hpp
+++ b/xs/src/slic3r/GUI/GLCanvas3DManager.hpp
@@ -163,6 +163,7 @@ public:
     void register_on_enable_action_buttons_callback(wxGLCanvas* canvas, void* callback);
     void register_on_gizmo_scale_uniformly_callback(wxGLCanvas* canvas, void* callback);
     void register_on_gizmo_rotate_callback(wxGLCanvas* canvas, void* callback);
+    void register_on_gizmo_flatten_callback(wxGLCanvas* canvas, void* callback);
     void register_on_update_geometry_info_callback(wxGLCanvas* canvas, void* callback);
 
     void register_action_add_callback(wxGLCanvas* canvas, void* callback);
diff --git a/xs/src/slic3r/GUI/GLGizmo.cpp b/xs/src/slic3r/GUI/GLGizmo.cpp
index 1d02e1752..9c209035b 100644
--- a/xs/src/slic3r/GUI/GLGizmo.cpp
+++ b/xs/src/slic3r/GUI/GLGizmo.cpp
@@ -20,103 +20,105 @@ static const float AXES_COLOR[3][3] = { { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f, 0.0f
 namespace Slic3r {
 namespace GUI {
 
-    // returns the intersection of the given ray with the plane parallel to plane XY and passing through the given center
-    // coordinates are local to the plane
-    Vec3d intersection_on_plane_xy(const Linef3& ray, const Vec3d& center)
+// returns the intersection of the given ray with the plane parallel to plane XY and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_xy(const Linef3& ray, const Vec3d& center)
+{
+    Transform3d m = Transform3d::Identity();
+    m.translate(-center);
+    Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+    return Vec3d(mouse_pos_2d(0), mouse_pos_2d(1), 0.0);
+}
+
+// returns the intersection of the given ray with the plane parallel to plane XZ and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_xz(const Linef3& ray, const Vec3d& center)
+{
+    Transform3d m = Transform3d::Identity();
+    m.rotate(Eigen::AngleAxisd(-0.5 * (double)PI, Vec3d::UnitX()));
+    m.translate(-center);
+    Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+    return Vec3d(mouse_pos_2d(0), 0.0, mouse_pos_2d(1));
+}
+
+// returns the intersection of the given ray with the plane parallel to plane YZ and passing through the given center
+// coordinates are local to the plane
+Vec3d intersection_on_plane_yz(const Linef3& ray, const Vec3d& center)
+{
+    Transform3d m = Transform3d::Identity();
+    m.rotate(Eigen::AngleAxisd(-0.5f * (double)PI, Vec3d::UnitY()));
+    m.translate(-center);
+    Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
+
+    return Vec3d(0.0, mouse_pos_2d(1), -mouse_pos_2d(0));
+}
+
+// return an index:
+// 0 for plane XY
+// 1 for plane XZ
+// 2 for plane YZ
+// which indicates which plane is best suited for intersecting the given unit vector
+// giving precedence to the plane with the given index
+unsigned int select_best_plane(const Vec3d& unit_vector, unsigned int preferred_plane)
+{
+    unsigned int ret = preferred_plane;
+
+    // 1st checks if the given vector is not parallel to the given preferred plane
+    double dot_to_normal = 0.0;
+    switch (ret)
     {
-        Transform3d m = Transform3d::Identity();
-        m.translate(-center);
-        Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
-        return Vec3d(mouse_pos_2d(0), mouse_pos_2d(1), 0.0);
+    case 0: // plane xy
+    {
+        dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitZ()));
+        break;
+    }
+    case 1: // plane xz
+    {
+        dot_to_normal = std::abs(unit_vector.dot(-Vec3d::UnitY()));
+        break;
+    }
+    case 2: // plane yz
+    {
+        dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitX()));
+        break;
+    }
+    default:
+    {
+        break;
+    }
     }
 
-    // returns the intersection of the given ray with the plane parallel to plane XZ and passing through the given center
-    // coordinates are local to the plane
-    Vec3d intersection_on_plane_xz(const Linef3& ray, const Vec3d& center)
+    // if almost parallel, select the plane whose normal direction is closest to the given vector direction,
+    // otherwise return the given preferred plane index
+    if (dot_to_normal < 0.1)
     {
-        Transform3d m = Transform3d::Identity();
-        m.rotate(Eigen::AngleAxisd(-0.5 * (double)PI, Vec3d::UnitX()));
-        m.translate(-center);
-        Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
-        return Vec3d(mouse_pos_2d(0), 0.0, mouse_pos_2d(1));
+        typedef std::map<double, unsigned int> ProjsMap;
+        ProjsMap projs_map;
+        projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitZ())), 0));  // plane xy
+        projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(-Vec3d::UnitY())), 1)); // plane xz
+        projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitX())), 2));  // plane yz
+        ret = projs_map.rbegin()->second;
     }
 
-    // returns the intersection of the given ray with the plane parallel to plane YZ and passing through the given center
-    // coordinates are local to the plane
-    Vec3d intersection_on_plane_yz(const Linef3& ray, const Vec3d& center)
-    {
-        Transform3d m = Transform3d::Identity();
-        m.rotate(Eigen::AngleAxisd(-0.5f * (double)PI, Vec3d::UnitY()));
-        m.translate(-center);
-        Vec2d mouse_pos_2d = to_2d(transform(ray, m).intersect_plane(0.0));
-
-        return Vec3d(0.0, mouse_pos_2d(1), -mouse_pos_2d(0));
-    }
-
-    // return an index:
-    // 0 for plane XY
-    // 1 for plane XZ
-    // 2 for plane YZ
-    // which indicates which plane is best suited for intersecting the given unit vector
-    // giving precedence to the plane with the given index
-    unsigned int select_best_plane(const Vec3d& unit_vector, unsigned int preferred_plane)
-    {
-        unsigned int ret = preferred_plane;
-
-        // 1st checks if the given vector is not parallel to the given preferred plane
-        double dot_to_normal = 0.0;
-        switch (ret)
-        {
-        case 0: // plane xy
-        {
-            dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitZ()));
-            break;
-        }
-        case 1: // plane xz
-        {
-            dot_to_normal = std::abs(unit_vector.dot(-Vec3d::UnitY()));
-            break;
-        }
-        case 2: // plane yz
-        {
-            dot_to_normal = std::abs(unit_vector.dot(Vec3d::UnitX()));
-            break;
-        }
-        default:
-        {
-            break;
-        }
-        }
-
-        // if almost parallel, select the plane whose normal direction is closest to the given vector direction,
-        // otherwise return the given preferred plane index
-        if (dot_to_normal < 0.1)
-        {
-            typedef std::map<double, unsigned int> ProjsMap;
-            ProjsMap projs_map;
-            projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitZ())), 0));  // plane xy
-            projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(-Vec3d::UnitY())), 1)); // plane xz
-            projs_map.insert(ProjsMap::value_type(std::abs(unit_vector.dot(Vec3d::UnitX())), 2));  // plane yz
-            ret = projs_map.rbegin()->second;
-        }
-
-        return ret;
-    }
+    return ret;
+}
     
-    const float GLGizmoBase::Grabber::HalfSize = 2.0f;
+const float GLGizmoBase::Grabber::SizeFactor = 0.025f;
+const float GLGizmoBase::Grabber::MinHalfSize = 1.5f;
 const float GLGizmoBase::Grabber::DraggingScaleFactor = 1.25f;
 
 GLGizmoBase::Grabber::Grabber()
     : center(Vec3d::Zero())
     , angles(Vec3d::Zero())
     , dragging(false)
+    , enabled(true)
 {
     color[0] = 1.0f;
     color[1] = 1.0f;
     color[2] = 1.0f;
 }
 
-void GLGizmoBase::Grabber::render(bool hover) const
+void GLGizmoBase::Grabber::render(bool hover, const BoundingBoxf3& box) const
 {
     float render_color[3];
     if (hover)
@@ -128,12 +130,15 @@ void GLGizmoBase::Grabber::render(bool hover) const
     else
         ::memcpy((void*)render_color, (const void*)color, 3 * sizeof(float));
 
-    render(render_color, true);
+    render(box, render_color, true);
 }
 
-void GLGizmoBase::Grabber::render(const float* render_color, bool use_lighting) const
+void GLGizmoBase::Grabber::render(const BoundingBoxf3& box, const float* render_color, bool use_lighting) const
 {
-    float half_size = dragging ? HalfSize * DraggingScaleFactor : HalfSize;
+    float max_size = (float)box.max_size();
+    float half_size = dragging ? max_size * SizeFactor * DraggingScaleFactor : max_size * SizeFactor;
+    half_size = std::max(half_size, MinHalfSize);
+
     if (use_lighting)
         ::glEnable(GL_LIGHTING);
 
@@ -234,6 +239,22 @@ void GLGizmoBase::set_highlight_color(const float* color)
         ::memcpy((void*)m_highlight_color, (const void*)color, 3 * sizeof(float));
 }
 
+void GLGizmoBase::enable_grabber(unsigned int id)
+{
+    if ((0 <= id) && (id < (unsigned int)m_grabbers.size()))
+        m_grabbers[id].enabled = true;
+
+    on_enable_grabber(id);
+}
+
+void GLGizmoBase::disable_grabber(unsigned int id)
+{
+    if ((0 <= id) && (id < (unsigned int)m_grabbers.size()))
+        m_grabbers[id].enabled = false;
+
+    on_disable_grabber(id);
+}
+
 void GLGizmoBase::start_dragging(const BoundingBoxf3& box)
 {
     m_dragging = true;
@@ -274,22 +295,26 @@ float GLGizmoBase::picking_color_component(unsigned int id) const
     return (float)color / 255.0f;
 }
 
-void GLGizmoBase::render_grabbers() const
+void GLGizmoBase::render_grabbers(const BoundingBoxf3& box) const
 {
     for (int i = 0; i < (int)m_grabbers.size(); ++i)
     {
-        m_grabbers[i].render(m_hover_id == i);
+        if (m_grabbers[i].enabled)
+            m_grabbers[i].render((m_hover_id == i), box);
     }
 }
 
-void GLGizmoBase::render_grabbers_for_picking() const
+void GLGizmoBase::render_grabbers_for_picking(const BoundingBoxf3& box) const
 {
     for (unsigned int i = 0; i < (unsigned int)m_grabbers.size(); ++i)
     {
-        m_grabbers[i].color[0] = 1.0f;
-        m_grabbers[i].color[1] = 1.0f;
-        m_grabbers[i].color[2] = picking_color_component(i);
-        m_grabbers[i].render_for_picking();
+        if (m_grabbers[i].enabled)
+        {
+            m_grabbers[i].color[0] = 1.0f;
+            m_grabbers[i].color[1] = 1.0f;
+            m_grabbers[i].color[2] = picking_color_component(i);
+            m_grabbers[i].render_for_picking(box);
+        }
     }
 }
 
@@ -386,6 +411,9 @@ void GLGizmoRotate::on_update(const Linef3& mouse_ray)
 
 void GLGizmoRotate::on_render(const BoundingBoxf3& box) const
 {
+    if (!m_grabbers[0].enabled)
+        return;
+
     if (m_dragging)
         set_tooltip(format(m_angle * 180.0f / (float)PI, 4));
     else
@@ -416,7 +444,7 @@ void GLGizmoRotate::on_render(const BoundingBoxf3& box) const
     if (m_hover_id != -1)
         render_angle();
 
-    render_grabber();
+    render_grabber(box);
 
     ::glPopMatrix();
 }
@@ -428,7 +456,7 @@ void GLGizmoRotate::on_render_for_picking(const BoundingBoxf3& box) const
     ::glPushMatrix();
 
     transform_to_local();
-    render_grabbers_for_picking();
+    render_grabbers_for_picking(box);
 
     ::glPopMatrix();
 }
@@ -520,7 +548,7 @@ void GLGizmoRotate::render_angle() const
     ::glEnd();
 }
 
-void GLGizmoRotate::render_grabber() const
+void GLGizmoRotate::render_grabber(const BoundingBoxf3& box) const
 {
     double grabber_radius = (double)(m_radius + GrabberOffset);
     m_grabbers[0].center = Vec3d(::cos(m_angle) * grabber_radius, ::sin(m_angle) * grabber_radius, 0.0);
@@ -534,7 +562,7 @@ void GLGizmoRotate::render_grabber() const
     ::glEnd();
 
     ::memcpy((void*)m_grabbers[0].color, (const void*)m_highlight_color, 3 * sizeof(float));
-    render_grabbers();
+    render_grabbers(box);
 }
 
 void GLGizmoRotate::transform_to_local() const
@@ -665,6 +693,7 @@ void GLGizmoRotate3D::on_render(const BoundingBoxf3& box) const
 }
 
 const float GLGizmoScale3D::Offset = 5.0f;
+const Vec3d GLGizmoScale3D::OffsetVec = (double)GLGizmoScale3D::Offset * Vec3d::Ones();
 
 GLGizmoScale3D::GLGizmoScale3D(GLCanvas3D& parent)
     : GLGizmoBase(parent)
@@ -714,7 +743,7 @@ void GLGizmoScale3D::on_start_dragging(const BoundingBoxf3& box)
     {
         m_starting_drag_position = m_grabbers[m_hover_id].center;
         m_show_starting_box = true;
-        m_starting_box = box;
+        m_starting_box = BoundingBoxf3(box.min - OffsetVec, box.max + OffsetVec);
     }
 }
 
@@ -748,9 +777,7 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
 
     ::glEnable(GL_DEPTH_TEST);
 
-    Vec3d offset_vec = (double)Offset * Vec3d::Ones();
-
-    m_box = BoundingBoxf3(box.min - offset_vec, box.max + offset_vec);
+    m_box = BoundingBoxf3(box.min - OffsetVec, box.max + OffsetVec);
     const Vec3d& center = m_box.center();
 
     // x axis
@@ -789,14 +816,23 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
         ::glColor3fv(m_base_color);
         render_box(m_box);
         // draw connections
-        ::glColor3fv(m_grabbers[0].color);
-        render_grabbers_connection(0, 1);
-        ::glColor3fv(m_grabbers[2].color);
-        render_grabbers_connection(2, 3);
-        ::glColor3fv(m_grabbers[4].color);
-        render_grabbers_connection(4, 5);
+        if (m_grabbers[0].enabled && m_grabbers[1].enabled)
+        {
+            ::glColor3fv(m_grabbers[0].color);
+            render_grabbers_connection(0, 1);
+        }
+        if (m_grabbers[2].enabled && m_grabbers[3].enabled)
+        {
+            ::glColor3fv(m_grabbers[2].color);
+            render_grabbers_connection(2, 3);
+        }
+        if (m_grabbers[4].enabled && m_grabbers[5].enabled)
+        {
+            ::glColor3fv(m_grabbers[4].color);
+            render_grabbers_connection(4, 5);
+        }
         // draw grabbers
-        render_grabbers();
+        render_grabbers(m_box);
     }
     else if ((m_hover_id == 0) || (m_hover_id == 1))
     {
@@ -813,8 +849,8 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
         ::glColor3fv(m_grabbers[0].color);
         render_grabbers_connection(0, 1);
         // draw grabbers
-        m_grabbers[0].render(true);
-        m_grabbers[1].render(true);
+        m_grabbers[0].render(true, m_box);
+        m_grabbers[1].render(true, m_box);
     }
     else if ((m_hover_id == 2) || (m_hover_id == 3))
     {
@@ -831,8 +867,8 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
         ::glColor3fv(m_grabbers[2].color);
         render_grabbers_connection(2, 3);
         // draw grabbers
-        m_grabbers[2].render(true);
-        m_grabbers[3].render(true);
+        m_grabbers[2].render(true, m_box);
+        m_grabbers[3].render(true, m_box);
     }
     else if ((m_hover_id == 4) || (m_hover_id == 5))
     {
@@ -849,8 +885,8 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
         ::glColor3fv(m_grabbers[4].color);
         render_grabbers_connection(4, 5);
         // draw grabbers
-        m_grabbers[4].render(true);
-        m_grabbers[5].render(true);
+        m_grabbers[4].render(true, m_box);
+        m_grabbers[5].render(true, m_box);
     }
     else if (m_hover_id >= 6)
     {
@@ -866,7 +902,7 @@ void GLGizmoScale3D::on_render(const BoundingBoxf3& box) const
         // draw grabbers
         for (int i = 6; i < 10; ++i)
         {
-            m_grabbers[i].render(true);
+            m_grabbers[i].render(true, m_box);
         }
     }
 }
@@ -875,7 +911,7 @@ void GLGizmoScale3D::on_render_for_picking(const BoundingBoxf3& box) const
 {
     ::glDisable(GL_DEPTH_TEST);
 
-    render_grabbers_for_picking();
+    render_grabbers_for_picking(box);
 }
 
 void GLGizmoScale3D::render_box(const BoundingBoxf3& box) const
@@ -989,6 +1025,160 @@ double GLGizmoScale3D::calc_ratio(unsigned int preferred_plane_id, const Linef3&
     return ratio;
 }
 
+const double GLGizmoMove3D::Offset = 10.0;
+
+GLGizmoMove3D::GLGizmoMove3D(GLCanvas3D& parent)
+    : GLGizmoBase(parent)
+    , m_position(Vec3d::Zero())
+    , m_starting_drag_position(Vec3d::Zero())
+    , m_starting_box_center(Vec3d::Zero())
+{
+}
+
+bool GLGizmoMove3D::on_init()
+{
+    std::string path = resources_dir() + "/icons/overlay/";
+
+    std::string filename = path + "move_off.png";
+    if (!m_textures[Off].load_from_file(filename, false))
+        return false;
+
+    filename = path + "move_hover.png";
+    if (!m_textures[Hover].load_from_file(filename, false))
+        return false;
+
+    filename = path + "move_on.png";
+    if (!m_textures[On].load_from_file(filename, false))
+        return false;
+
+    for (int i = 0; i < 3; ++i)
+    {
+        m_grabbers.push_back(Grabber());
+    }
+
+    return true;
+}
+
+void GLGizmoMove3D::on_start_dragging(const BoundingBoxf3& box)
+{
+    if (m_hover_id != -1)
+    {
+        m_starting_drag_position = m_grabbers[m_hover_id].center;
+        m_starting_box_center = box.center();
+    }
+}
+
+void GLGizmoMove3D::on_update(const Linef3& mouse_ray)
+{
+    if (m_hover_id == 0)
+        m_position(0) = 2.0 * m_starting_box_center(0) + calc_displacement(1, mouse_ray) - m_starting_drag_position(0);
+    else if (m_hover_id == 1)
+        m_position(1) = 2.0 * m_starting_box_center(1) + calc_displacement(2, mouse_ray) - m_starting_drag_position(1);
+    else if (m_hover_id == 2)
+        m_position(2) = 2.0 * m_starting_box_center(2) + calc_displacement(1, mouse_ray) - m_starting_drag_position(2);
+}
+
+void GLGizmoMove3D::on_render(const BoundingBoxf3& box) const
+{
+    if (m_grabbers[0].dragging)
+        set_tooltip("X: " + format(m_position(0), 2));
+    else if (m_grabbers[1].dragging)
+        set_tooltip("Y: " + format(m_position(1), 2));
+    else if (m_grabbers[2].dragging)
+        set_tooltip("Z: " + format(m_position(2), 2));
+
+    ::glEnable(GL_DEPTH_TEST);
+
+    const Vec3d& center = box.center();
+
+    // x axis
+    m_grabbers[0].center = Vec3d(box.max(0) + Offset, center(1), center(2));
+    ::memcpy((void*)m_grabbers[0].color, (const void*)&AXES_COLOR[0], 3 * sizeof(float));
+
+    // y axis
+    m_grabbers[1].center = Vec3d(center(0), box.max(1) + Offset, center(2));
+    ::memcpy((void*)m_grabbers[1].color, (const void*)&AXES_COLOR[1], 3 * sizeof(float));
+
+    // z axis
+    m_grabbers[2].center = Vec3d(center(0), center(1), box.max(2) + Offset);
+    ::memcpy((void*)m_grabbers[2].color, (const void*)&AXES_COLOR[2], 3 * sizeof(float));
+
+    ::glLineWidth((m_hover_id != -1) ? 2.0f : 1.5f);
+
+    if (m_hover_id == -1)
+    {
+        // draw axes
+        for (unsigned int i = 0; i < 3; ++i)
+        {
+            if (m_grabbers[i].enabled)
+            {
+                ::glColor3fv(AXES_COLOR[i]);
+                ::glBegin(GL_LINES);
+                ::glVertex3f(center(0), center(1), center(2));
+                ::glVertex3f((GLfloat)m_grabbers[i].center(0), (GLfloat)m_grabbers[i].center(1), (GLfloat)m_grabbers[i].center(2));
+                ::glEnd();
+            }
+        }
+
+        // draw grabbers
+        render_grabbers(box);
+    }
+    else
+    {
+        // draw axis
+        ::glColor3fv(AXES_COLOR[m_hover_id]);
+        ::glBegin(GL_LINES);
+        ::glVertex3f(center(0), center(1), center(2));
+        ::glVertex3f((GLfloat)m_grabbers[m_hover_id].center(0), (GLfloat)m_grabbers[m_hover_id].center(1), (GLfloat)m_grabbers[m_hover_id].center(2));
+        ::glEnd();
+
+        // draw grabber
+        m_grabbers[m_hover_id].render(true, box);
+    }
+}
+
+void GLGizmoMove3D::on_render_for_picking(const BoundingBoxf3& box) const
+{
+    ::glDisable(GL_DEPTH_TEST);
+
+    render_grabbers_for_picking(box);
+}
+
+double GLGizmoMove3D::calc_displacement(unsigned int preferred_plane_id, const Linef3& mouse_ray) const
+{
+    double displacement = 0.0;
+
+    Vec3d starting_vec = m_starting_drag_position - m_starting_box_center;
+    double len_starting_vec = starting_vec.norm();
+    if (len_starting_vec == 0.0)
+        return displacement;
+
+    Vec3d starting_vec_dir = starting_vec.normalized();
+    Vec3d mouse_dir = mouse_ray.unit_vector();
+
+    unsigned int plane_id = select_best_plane(mouse_dir, preferred_plane_id);
+
+    switch (plane_id)
+    {
+    case 0:
+    {
+        displacement = starting_vec_dir.dot(intersection_on_plane_xy(mouse_ray, m_starting_box_center));
+        break;
+    }
+    case 1:
+    {
+        displacement = starting_vec_dir.dot(intersection_on_plane_xz(mouse_ray, m_starting_box_center));
+        break;
+    }
+    case 2:
+    {
+        displacement = starting_vec_dir.dot(intersection_on_plane_yz(mouse_ray, m_starting_box_center));
+        break;
+    }
+    }
+
+    return displacement;
+}
 
 GLGizmoFlatten::GLGizmoFlatten(GLCanvas3D& parent)
     : GLGizmoBase(parent)
diff --git a/xs/src/slic3r/GUI/GLGizmo.hpp b/xs/src/slic3r/GUI/GLGizmo.hpp
index 0599955ed..fc2912d3a 100644
--- a/xs/src/slic3r/GUI/GLGizmo.hpp
+++ b/xs/src/slic3r/GUI/GLGizmo.hpp
@@ -22,21 +22,23 @@ class GLGizmoBase
 protected:
     struct Grabber
     {
-        static const float HalfSize;
+        static const float SizeFactor;
+        static const float MinHalfSize;
         static const float DraggingScaleFactor;
 
         Vec3d center;
         Vec3d angles;
         float color[3];
+        bool enabled;
         bool dragging;
 
         Grabber();
 
-        void render(bool hover) const;
-        void render_for_picking() const { render(color, false); }
+        void render(bool hover, const BoundingBoxf3& box) const;
+        void render_for_picking(const BoundingBoxf3& box) const { render(box, color, false); }
 
     private:
-        void render(const float* render_color, bool use_lighting) const;
+        void render(const BoundingBoxf3& box, const float* render_color, bool use_lighting) const;
         void render_face(float half_size) const;
     };
 
@@ -80,9 +82,12 @@ public:
 
     int get_hover_id() const { return m_hover_id; }
     void set_hover_id(int id);
-
+    
     void set_highlight_color(const float* color);
 
+    void enable_grabber(unsigned int id);
+    void disable_grabber(unsigned int id);
+
     void start_dragging(const BoundingBoxf3& box);
     void stop_dragging();
     bool is_dragging() const { return m_dragging; }
@@ -96,6 +101,8 @@ protected:
     virtual bool on_init() = 0;
     virtual void on_set_state() {}
     virtual void on_set_hover_id() {}
+    virtual void on_enable_grabber(unsigned int id) {}
+    virtual void on_disable_grabber(unsigned int id) {}
     virtual void on_start_dragging(const BoundingBoxf3& box) {}
     virtual void on_stop_dragging() {}
     virtual void on_update(const Linef3& mouse_ray) = 0;
@@ -103,8 +110,8 @@ protected:
     virtual void on_render_for_picking(const BoundingBoxf3& box) const = 0;
 
     float picking_color_component(unsigned int id) const;
-    void render_grabbers() const;
-    void render_grabbers_for_picking() const;
+    void render_grabbers(const BoundingBoxf3& box) const;
+    void render_grabbers_for_picking(const BoundingBoxf3& box) const;
 
     void set_tooltip(const std::string& tooltip) const;
     std::string format(float value, unsigned int decimals) const;
@@ -157,7 +164,7 @@ private:
     void render_snap_radii() const;
     void render_reference_radius() const;
     void render_angle() const;
-    void render_grabber() const;
+    void render_grabber(const BoundingBoxf3& box) const;
 
     void transform_to_local() const;
     // returns the intersection of the mouse ray with the plane perpendicular to the gizmo axis, in local coordinate
@@ -196,6 +203,16 @@ protected:
             m_gizmos[i].set_hover_id((m_hover_id == i) ? 0 : -1);
         }
     }
+    virtual void on_enable_grabber(unsigned int id)
+    {
+        if ((0 <= id) && (id < 3))
+            m_gizmos[id].enable_grabber(0);
+    }
+    virtual void on_disable_grabber(unsigned int id)
+    {
+        if ((0 <= id) && (id < 3))
+            m_gizmos[id].disable_grabber(0);
+    }
     virtual void on_start_dragging(const BoundingBoxf3& box);
     virtual void on_stop_dragging();
     virtual void on_update(const Linef3& mouse_ray)
@@ -218,6 +235,7 @@ protected:
 class GLGizmoScale3D : public GLGizmoBase
 {
     static const float Offset;
+    static const Vec3d OffsetVec;
 
     mutable BoundingBoxf3 m_box;
 
@@ -262,6 +280,31 @@ private:
     double calc_ratio(unsigned int preferred_plane_id, const Linef3& mouse_ray, const Vec3d& center) const;
 };
 
+class GLGizmoMove3D : public GLGizmoBase
+{
+    static const double Offset;
+
+    Vec3d m_position;
+    Vec3d m_starting_drag_position;
+    Vec3d m_starting_box_center;
+
+public:
+    explicit GLGizmoMove3D(GLCanvas3D& parent);
+
+    const Vec3d& get_position() const { return m_position; }
+    void set_position(const Vec3d& position) { m_position = position; }
+
+protected:
+    virtual bool on_init();
+    virtual void on_start_dragging(const BoundingBoxf3& box);
+    virtual void on_update(const Linef3& mouse_ray);
+    virtual void on_render(const BoundingBoxf3& box) const;
+    virtual void on_render_for_picking(const BoundingBoxf3& box) const;
+
+private:
+    double calc_displacement(unsigned int preferred_plane_id, const Linef3& mouse_ray) const;
+};
+
 class GLGizmoFlatten : public GLGizmoBase
 {
 // This gizmo does not use grabbers. The m_hover_id relates to polygon managed by the class itself.
diff --git a/xs/xsp/GUI_3DScene.xsp b/xs/xsp/GUI_3DScene.xsp
index 756f9e547..4c3c5501e 100644
--- a/xs/xsp/GUI_3DScene.xsp
+++ b/xs/xsp/GUI_3DScene.xsp
@@ -651,6 +651,13 @@ register_on_gizmo_rotate_callback(canvas, callback)
     CODE:
         _3DScene::register_on_gizmo_rotate_callback((wxGLCanvas*)wxPli_sv_2_object(aTHX_ canvas, "Wx::GLCanvas"), (void*)callback);
 
+void
+register_on_gizmo_flatten_callback(canvas, callback)
+        SV *canvas;
+        SV *callback;
+    CODE:
+        _3DScene::register_on_gizmo_flatten_callback((wxGLCanvas*)wxPli_sv_2_object(aTHX_ canvas, "Wx::GLCanvas"), (void*)callback);
+
 void
 register_on_update_geometry_info_callback(canvas, callback)
         SV *canvas;