diff --git a/src/slic3r/GUI/DesignCanvas.cpp b/src/slic3r/GUI/DesignCanvas.cpp index f0e58d12cf..68683896f6 100644 --- a/src/slic3r/GUI/DesignCanvas.cpp +++ b/src/slic3r/GUI/DesignCanvas.cpp @@ -12,6 +12,8 @@ #include "MeshUtils.hpp" // ClippingPlane (section view) #include "libslic3r/Config.hpp" #include +#include +#include #include #include @@ -43,6 +45,12 @@ DesignCanvas::DesignCanvas(wxWindow* parent) m_canvas->set_process(wxGetApp().plater()->get_background_process()); m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D); + // CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit + // moves to middle-drag and pan to right-drag. Design is a different modality from + // Prepare/Preview and every CAD the user already knows maps the mouse this way; the other + // tabs are untouched. + m_canvas->set_cad_navigation(true); + m_canvas->enable_picking(false); // viewport face/edge picking is custom (TODO) m_canvas->enable_moving(false); m_canvas->enable_gizmos(false); @@ -798,8 +806,18 @@ void DesignCanvas::set_on_context_menu(std::function cb) // Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event. // It only consumes when it actually opens the offer; every other right-click still falls // through to the polyline-chain end and the move gizmo, which were there first. + // Right-drag pans. Without remembering where the press landed, every pan ended by popping + // the offer over wherever the camera stopped — the menu appearing as the reward for moving + // the view. The offer is the release of a STATIONARY right-click, at the same 8 px budget + // the left-click pick uses. + m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) { + m_ctx_press = e.GetPosition(); + e.Skip(); // the canvas still needs the press to seed the pan + }); m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) { - if (m_on_context_menu && !is_sketching() && !inline_busy()) { + const wxPoint d = e.GetPosition() - m_ctx_press; + if (m_on_context_menu && !is_sketching() && !inline_busy() + && std::max(std::abs(d.x), std::abs(d.y)) <= 8) { m_on_context_menu(m_canvas_widget->ClientToScreen(e.GetPosition())); return; // consumed } diff --git a/src/slic3r/GUI/DesignCanvas.hpp b/src/slic3r/GUI/DesignCanvas.hpp index c3250bc577..0762678343 100644 --- a/src/slic3r/GUI/DesignCanvas.hpp +++ b/src/slic3r/GUI/DesignCanvas.hpp @@ -269,6 +269,7 @@ private: std::function m_on_context_menu; bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set + wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG pans, it must not offer Bed3D m_bed; Model m_model; diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index ba5d516150..f142059ee1 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -3133,19 +3133,10 @@ DesignPanel::DesignPanel(wxWindow* parent) // Say what got picked. Without this the ONLY feedback is the viewport highlight, so a // pick that registers but draws faintly is indistinguishable from one that never // happened — which is precisely how this failure was reported and why it resisted - // diagnosis. Cards that show their own labels still do. - if (m_status != nullptr) { - m_status->SetForegroundColour(wxNullColour); - if (level <= 0) - m_status->SetLabel(_L("Selection cleared")); - else if (level == 1) - m_status->SetLabel(wxString::Format(_L("Body %d selected"), body + 1)); - else if (level == 2) - m_status->SetLabel(wxString::Format(_L("Body %d, face %d"), body + 1, face)); - else - m_status->SetLabel(wxString::Format(_L("Body %d, edge %d selected"), body + 1, edge)); - m_status->Refresh(); - } + // diagnosis. Cards that show their own labels still do. The label itself is written + // ONCE, at the end of this handler — a second writer here only ever produced text that + // the later one overwrote before a frame was drawn, and reading it as the live string + // is how a vertex pick came to be "fixed" in a branch that never reaches the screen. // If the Fillet/Chamfer card is open, re-anchor (or drop) the radius arrow on the new pick // and rebuild the ghost — once an edge is picked the preview-only mode hides the base body. if (m_active == Tool::Dressup) { sync_dressup_target(); update_fillet_gizmo(); refresh_preview(); } diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index 4d05e6a263..bb214f6151 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -2785,8 +2785,59 @@ static void dp_pick_trace(const char* fmt, ...) std::fflush(stderr); } -// LeftDown on the solid cycles whole->face->edge. Returns true if the click hit the solid -// (consumed); false otherwise so the caller can try committed-sketch loop picking. +// Resolve a swept rubber band into a whole-body selection. +// +// Sample points are the display mesh's triangle vertices plus each triangle's centroid. That +// mesh is already in world coordinates — the very points the ray pick tests — so no per-body +// transform is needed here. A body counts as swept when any of its samples lands inside the +// rectangle (crossing semantics: touching selects, which is the forgiving reading of a sweep), +// and the body with the most samples inside wins because the selection callback downstream +// carries exactly one body. +// +// ponytail: crossing over a triangle sample set. A rectangle small enough to sit entirely +// inside one flat triangle selects nothing — drag a bigger one, or click. Real multi-body +// selection (and the homogeneous-set rule that goes with it) is snaporca-9xw. +void DesignSketchTool::pick_bodies_in_rectangle() +{ + if (m_solid_mesh == nullptr || m_solid_tri_body == nullptr || m_solid_bodies == nullptr) + return; + const indexed_triangle_set& its = m_solid_mesh->its; + std::vector pts; + std::vector owner; + pts.reserve(its.indices.size() * 4); + owner.reserve(its.indices.size() * 4); + for (size_t i = 0; i < its.indices.size(); ++i) { + const int b = (i < m_solid_tri_body->size()) ? (*m_solid_tri_body)[i] : -1; + if (!body_pickable(b)) continue; // hidden bodies aren't swept either + const auto& idx = its.indices[i]; + Vec3d c = Vec3d::Zero(); + for (int k = 0; k < 3; ++k) { + const Vec3d p = its.vertices[idx(k)].cast(); + pts.push_back(p); owner.push_back(b); c += p; + } + pts.push_back(c / 3.0); owner.push_back(b); + } + + std::vector hits(m_solid_bodies->size(), 0); + if (!pts.empty()) + for (unsigned int i : m_rubber.contains(pts)) + if (i < owner.size() && owner[i] >= 0 && owner[i] < int(hits.size())) + ++hits[owner[i]]; + + int best = -1, best_n = 0; + for (int b = 0; b < int(hits.size()); ++b) + if (hits[b] > best_n) { best_n = hits[b]; best = b; } + + if (best < 0) clear_solid_selection(); // swept empty space -> drop the selection + else select_body(best); + dp_pick_trace("rubber band -> body=%d (%d samples)", best, best_n); + if (on_solid_selection_changed) + on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge); +} + +// A click on the solid takes the smallest thing under the cursor (vertex/edge/face). Returns +// true if the click hit the solid (consumed); false otherwise so the caller can try +// committed-sketch loop picking. Whole bodies are taken by the rubber band, not by clicking. bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt) { if (m_solid_bodies == nullptr || m_solid_mesh == nullptr) { @@ -2822,7 +2873,8 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent // ---- one click, one deterministic result --------------------------------------------- // NO CYCLE. A click selects the SMALLEST thing under the cursor: the edge if the pointer is - // within tolerance of one, otherwise the face. A DOUBLE-click takes the whole body. + // within tolerance of one, otherwise the face. The WHOLE body is taken by a left-drag + // rubber band (pick_bodies_in_rectangle) — a different gesture for a different scale. // // What this replaces: click 1 = whole body, click 2 = face, click 3 = nearest edge, click 4 // = back to whole. That made "click a face" a two-click gesture and "click an edge" a @@ -6737,6 +6789,9 @@ void DesignSketchTool::render(GLCanvas3D& canvas) return; } render_view_helpers(); // origin planes / world axes — drawn whenever their toggle is on + m_rubber.render(canvas); // left-drag rubber band (no-op unless one is being swept). Drawn + // here, ahead of every early return below, so a band over an empty + // plate is still visible. if (m_active && m_mode != Mode::Constrain && m_entities.empty() && m_points.empty() && m_display_sketches.empty()) { if (on_readout) on_readout(std::string()); @@ -7769,6 +7824,32 @@ bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas) return true; } } + // Left-drag rubber band -> whole body. Past the click budget the press becomes a sweep: + // the rectangle is anchored at the ORIGINAL press point (not at the frame where the + // threshold was crossed, which would lose the first few pixels) and the events are + // consumed from here on. Left-drag no longer orbits in this canvas — DesignCanvas puts + // orbit on middle-drag and pan on right-drag, the CAD convention — so nothing downstream + // is being starved of a gesture it used to own. + if (evt.Dragging() && evt.LeftIsDown() && m_pick_pending) { + if (!m_rubber.is_dragging()) { + if (std::max(std::abs(evt.GetX() - m_pick_press_x), + std::abs(evt.GetY() - m_pick_press_y)) <= 8) + return false; // still inside the click budget + m_rubber.start_dragging(Vec2d(m_pick_press_x, m_pick_press_y), + GLSelectionRectangle::Select); + } + m_rubber.dragging(Vec2d(evt.GetX(), evt.GetY())); + return true; + } + // Any event with the left button up ends a band — not just LeftUp. A release that lands + // outside the canvas never sends us one, and a band left running would then paint a + // rectangle that follows the cursor with no button held. + if (m_rubber.is_dragging() && !evt.LeftIsDown()) { + m_pick_pending = false; + if (evt.LeftUp()) pick_bodies_in_rectangle(); // a release elsewhere selects nothing + m_rubber.stop_dragging(); + return evt.LeftUp(); + } // Click vs drag. Consuming the LeftDown here killed camera orbit/pan the moment a // solid was on screen: Orca starts a rotate drag on the press, so swallowing it meant // the canvas never began one. (A SpaceMouse kept working — it never goes through diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index 0ec808abb2..d4c7fde6d9 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -7,6 +7,7 @@ #include "libslic3r/SketchInference.hpp" #include "libslic3r/SketchSolver.hpp" #include "GLModel.hpp" +#include "GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies #include #include #include @@ -906,7 +907,12 @@ private: int m_sel_edge{-1}; std::vector m_sel_edge_pts; Vec3d m_sel_vertex_pt{Vec3d::Zero()}; // world point of a picked vertex - bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // cycle + notify + bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // pick + notify + // Left-drag rubber band: sweep a rectangle over the plate to take a whole body. Orbit + // moves to middle-drag in this canvas (DesignCanvas::set_cad_navigation) so the left + // button is free for it, which is the CAD convention (Onshape/SolidWorks). + GLSelectionRectangle m_rubber; + void pick_bodies_in_rectangle(); // resolve the swept rectangle -> whole-body selection void render_solid_highlight(); void render_datum_planes(); // translucent rectangles for datum/reference planes void render_view_helpers(); // world origin planes + axis triad (P / A toggles) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bbff72cf38..bd32467e50 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4981,6 +4981,8 @@ bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map m_old_size{ 0, 0 }; bool m_is_touchpad_navigation{ false }; + // CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves + // to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere + // else, so Prepare/Preview keep the mouse the user already learned. + bool m_cad_navigation{ false }; // Screen is only refreshed from the OnIdle handler if it is dirty. bool m_dirty; @@ -1077,6 +1081,7 @@ public: bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map& mappings) const; bool is_camera_rotate(const wxMouseEvent& evt, const std::map& mappings) const; bool is_camera_pan(const wxMouseEvent& evt, const std::map& mappings) const; + void set_cad_navigation(bool b) { m_cad_navigation = b; } Size get_canvas_size() const; Vec2d get_local_mouse_position() const;