diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 525a396bea..ba6230c2ad 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -78,6 +78,7 @@ static bool point_in_poly(const Vec2d& q, const std::vector& poly); static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, const Vec3d& v0, const Vec3d& v1, const Vec3d& v2, double& t); static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b); +static double ray_axis_proj(const Linef3& ray, const Vec3d& anchor, const Vec3d& dir); // Project a world-space point to canvas screen pixels (device px, GL viewport units; // origin top-left after the GL y-flip). Mirrors GLCanvas3D's world->screen pattern: @@ -5819,19 +5820,22 @@ bool DesignSketchTool::hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEven axis = best; return true; } -// Skew-line closest point of the mouse ray to the axis line through the ORIGINAL centroid -// -> signed offset along that axis (no clamp; a body can move either way). -void DesignSketchTool::drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis) +// Record how far along the arrow it was grabbed (NaN while the camera looks down the axis; the +// first drag sample that projects stands in), so the body's centre does not snap to the grab. +void DesignSketchTool::grab_move_arrow(int axis, const Linef3& ray) { - const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); - const Vec3d ro = r.a, rd = r.b - r.a; - const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() }; - const Vec3d e = axes[axis]; - const Vec3d w0 = m_mv_base - ro; - const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); - const double denom = a * c - b * b; - if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave offset as-is - m_mv_offset[axis] = (b * ee - c * dd) / denom; + m_mv_drag = axis; + m_mv_grab_along = ray_axis_proj(ray, m_mv_base + m_mv_offset, Vec3d::Unit(axis)); +} + +// Slide the body along the grabbed axis by the cursor's travel (no clamp; it can move either way). +void DesignSketchTool::drag_move_arrow(const Linef3& ray) +{ + if (m_mv_drag < 0 || m_mv_drag > 2) return; + const double proj = ray_axis_proj(ray, m_mv_base + m_mv_offset, Vec3d::Unit(m_mv_drag)); + if (std::isnan(proj)) return; // camera ∥ axis: leave offset as-is + if (std::isnan(m_mv_grab_along)) { m_mv_grab_along = proj; return; } + m_mv_offset[m_mv_drag] += proj - m_mv_grab_along; if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform()); } @@ -6201,21 +6205,10 @@ int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEven return best; } -// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along -// dir. NaN when the camera is ~parallel to the axis (no meaningful projection). double DesignSketchTool::hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec3d& anchor, const Vec3d& dir) const { - const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY())); - const Vec3d ro = r.a, rd = r.b - r.a; - const Vec3d e = dir; - const Vec3d w0 = anchor - ro; - const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); - const double denom = a * c - b * b; - // Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in - // top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through. - if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan(""); - return (b * ee - c * dd) / denom; + return ray_axis_proj(canvas.mouse_ray(Point(evt.GetX(), evt.GetY())), anchor, dir); } void DesignSketchTool::start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which) @@ -10179,6 +10172,21 @@ static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a return (pr - ps).norm(); } +// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along +// dir. NaN when the camera is ~parallel to the axis (no meaningful projection). +static double ray_axis_proj(const Linef3& ray, const Vec3d& anchor, const Vec3d& dir) +{ + const Vec3d ro = ray.a, rd = ray.vector(); + const Vec3d e = dir; + const Vec3d w0 = anchor - ro; + const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0); + const double denom = a * c - b * b; + // Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in + // top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through. + if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan(""); + return (b * ee - c * dd) / denom; +} + // Screen-plane distance from p to a sketch entity, for click picking in Constrain // mode. Circles/arcs measure distance to the ring; points to their position. static double entity_pick_dist(const Vec2d& p, const SketchEntity& e) @@ -10687,7 +10695,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) // exits move mode. A LeftDown that misses the arrows falls through to solid re-pick. if (m_mv_active) { if (m_mv_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) { - if (m_mv_drag < 3) drag_move_arrow(canvas, evt, m_mv_drag); + if (m_mv_drag < 3) drag_move_arrow(canvas.mouse_ray(Point(evt.GetX(), evt.GetY()))); else drag_move_arc(canvas, evt, m_mv_drag - 3); return true; } @@ -10705,7 +10713,8 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) if (evt.LeftDown()) { int axis = -1; if (hit_test_move_arrow(canvas, evt, axis)) { // translate arrows win over rings - m_mv_drag = axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY(); + grab_move_arrow(axis, canvas.mouse_ray(Point(evt.GetX(), evt.GetY()))); + m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY(); return true; } if (hit_test_move_arc(canvas, evt, axis)) { diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 8d575241e9..88c003bc36 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -3,6 +3,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Line.hpp" #include "libslic3r/CAD/SketchEngine.hpp" #include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking #include @@ -260,6 +261,9 @@ public: void clear_move_gizmo(); bool moving_body() const { return m_mv_active; } int move_body_index() const { return m_mv_body; } + // Press, then drag, translate arrow `axis` (0..2 = X/Y/Z) with the mouse ray under the cursor. + void grab_move_arrow(int axis, const Linef3& ray); + void drag_move_arrow(const Linef3& ray); std::function on_body_move_changed; // Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id). std::function on_solid_selection_changed; @@ -1432,6 +1436,7 @@ private: int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none double m_mv_radius{0.0}; // body bounding-sphere radius (mm); 0 = unknown int m_mv_press_x{0}, m_mv_press_y{0}; + double m_mv_grab_along{0.0}; // arrow grab point's distance from the anchor; NaN = unknown Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis void render_move_gizmo(); @@ -1439,7 +1444,6 @@ private: double move_gizmo_arm(const Camera& cam) const; bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const; - void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis); void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis); bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const; void open_move_editor(int axis); diff --git a/tests/slic3rutils/test_design_sketch_tool.cpp b/tests/slic3rutils/test_design_sketch_tool.cpp index 3446d813d2..685f661fc3 100644 --- a/tests/slic3rutils/test_design_sketch_tool.cpp +++ b/tests/slic3rutils/test_design_sketch_tool.cpp @@ -28,6 +28,7 @@ #include "libslic3r/BoundingBox.hpp" #include "libslic3r/CAD/SketchEngine.hpp" +#include "libslic3r/Line.hpp" #include "libslic3r/Point.hpp" #include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/CAD/DesignSketchTool.hpp" @@ -305,3 +306,41 @@ TEST_CASE("Cutting the base planes along each other keeps every plane whole", "[ for (double a : covered) CHECK_THAT(a, WithinRel(4. * half * half, 1e-9)); } + +TEST_CASE("A move arrow drag moves the body by the cursor's travel, wherever the arrow is grabbed", "[DesignSketchTool]") +{ + DesignSketchTool tool; + Transform3d moved = Transform3d::Identity(); + tool.on_body_move_changed = [&moved](int, const Transform3d& xform) { moved = xform; }; + tool.set_move_gizmo(0, Vec3d::Zero(), Transform3d::Identity(), 10.); + // Looking straight down onto the X arrow, the cursor over x = `x` on it. + const auto ray_at = [](double x) { return Linef3(Vec3d(x, 0., 100.), Vec3d(x, 0., 0.)); }; + + // Grabbed 12 mm out from the body centre: a 1 mm move moves the body 1 mm, not 13. + tool.grab_move_arrow(0, ray_at(12.)); + tool.drag_move_arrow(ray_at(13.)); + CHECK_THAT(moved.translation().x(), WithinAbs(1., 1e-9)); + tool.drag_move_arrow(ray_at(17.)); + CHECK_THAT(moved.translation().x(), WithinAbs(5., 1e-9)); + + // The next drag carries on from where the body was left: grabbed 3 mm past it, moved 2 mm. + tool.grab_move_arrow(0, ray_at(8.)); + tool.drag_move_arrow(ray_at(10.)); + CHECK_THAT(moved.translation().x(), WithinAbs(7., 1e-9)); +} + +TEST_CASE("A move arrow pressed while looking down its axis does not jump on the first move", "[DesignSketchTool]") +{ + DesignSketchTool tool; + Transform3d moved = Transform3d::Identity(); + tool.on_body_move_changed = [&moved](int, const Transform3d& xform) { moved = xform; }; + tool.set_move_gizmo(0, Vec3d::Zero(), Transform3d::Identity(), 10.); + const auto ray_at = [](double x) { return Linef3(Vec3d(x, 0., 100.), Vec3d(x, 0., 0.)); }; + + // The press projects nowhere on the X axis, so the first move only finds where it was grabbed. + tool.grab_move_arrow(0, Linef3(Vec3d(100., 0., 0.), Vec3d::Zero())); + tool.drag_move_arrow(ray_at(13.)); + CHECK_THAT(moved.translation().x(), WithinAbs(0., 1e-9)); + tool.drag_move_arrow(ray_at(15.)); + CHECK_THAT(moved.translation().x(), WithinAbs(2., 1e-9)); +}