diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index a1a349293f..ba5d516150 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -3117,8 +3117,9 @@ DesignPanel::DesignPanel(wxWindow* parent) if (m_viewport->moving_body()) m_viewport->clear_move_gizmo(); // Remember which body + face/edge so Extrude / dress-up target the RIGHT body. m_sel_solid_body = (level >= 1) ? body : -1; - m_sel_solid_face = (level >= 2) ? face : -1; + m_sel_solid_face = (level == 2) ? face : -1; // 4 = Vertex: a corner is not its face m_sel_solid_edge = (level == 3) ? edge : -1; + m_sel_solid_vertex = (level == 4); // Keep the hit face even at whole-body level: the cycle's first click means "this body", // but the user pointed AT a face and a sketch should be able to use it. snaporca-3a2. m_pick_face_body = (level >= 1) ? body : -1; @@ -3240,7 +3241,8 @@ DesignPanel::DesignPanel(wxWindow* parent) m_status->SetForegroundColour(wxNullColour); const int nb = int(m_doc.bodies.size()); const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString(); - m_status->SetLabel(level == 1 ? bodytag + _L("selected (whole body)") + m_status->SetLabel(level == 4 ? bodytag + _L("vertex selected") + : level == 1 ? bodytag + _L("selected (whole body)") : level == 2 ? bodytag + wxString::Format(_L("face %d selected — Extrude to push/pull it"), face) : level == 3 ? bodytag + wxString::Format(_L("edge %d selected — Fillet/Chamfer to dress it"), edge) : _L("Nothing selected")); @@ -4915,6 +4917,8 @@ int DesignPanel::offer_selection_kind() const return int(OfferSel::SkNone); const int nb = int(m_doc.bodies.size()); + if (m_sel_solid_vertex && m_sel_solid_body >= 0 && m_sel_solid_body < nb) + return int(OfferSel::Vertex); if (m_sel_solid_edge >= 0 && m_sel_solid_body >= 0 && m_sel_solid_body < nb) { const TopoDS_Edge e = GeometryEngine::edge_by_index(m_doc.bodies[m_sel_solid_body].shape, m_sel_solid_edge); diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 19ff2ca0dd..3c345cb57b 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -593,6 +593,7 @@ private: int m_sel_solid_body{-1}; // which body the face/edge selection is on int m_sel_solid_face{-1}; int m_sel_solid_edge{-1}; + bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge) // The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level. // The first click on a solid selects the WHOLE body, but the ray has already resolved which face // it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index 69fbf81bd8..4d05e6a263 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -2836,7 +2836,11 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent // the pointer is a screen object and its tolerance has to be one too. const Camera& cam = wxGetApp().plater()->get_camera(); const wxPoint cursor(evt.GetX(), evt.GetY()); - const double kEdgeTolPx = 8.0; + // Vertex beats edge beats face, and the vertex tolerance is the larger of the two: a corner + // sits ON its edges, so an equal radius would make vertices unreachable — every click near + // one would resolve to the edge it lies on. + const double kVertexTolPx = 11.0; + const double kEdgeTolPx = 8.0; auto seg_px = [](const wxPoint& p, const wxPoint& a, const wxPoint& b) { // 2D point→segment, px const double vx = b.x - a.x, vy = b.y - a.y; @@ -2862,11 +2866,20 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent const TopoDS_Shape& bshape = (*m_solid_bodies)[m_sel_body].shape; const TopoDS_Face face = GeometryEngine::face_by_index(bshape, m_sel_face); double best_ed = 1e30; std::vector ed_pts; TopoDS_Edge ed_edge; bool have_edge = false; + double best_vd = 1e30; Vec3d vtx = Vec3d::Zero(); bool have_vtx = false; if (!face.IsNull()) { for (const TopoDS_Edge& e : GeometryEngine::edges_of_face(face)) { std::vector pts = GeometryEngine::sample_edge_world(e); for (Vec3d& q : pts) q = body_xform_pt(m_sel_body, q); // follow a moved body if (pts.size() < 2) continue; + // The polyline ends ARE the edge's vertices; every corner of the face is the + // end of one of its edges, so this covers them without a separate topology walk. + for (const Vec3d& v : {pts.front(), pts.back()}) { + const wxPoint sp = world_to_screen_px(cam, v); + if (sp.x < 0) continue; + const double d = std::hypot(double(sp.x - cursor.x), double(sp.y - cursor.y)); + if (d < best_vd) { best_vd = d; vtx = v; have_vtx = true; } + } double d = 1e30; for (size_t s = 1; s < pts.size(); ++s) { const wxPoint a = world_to_screen_px(cam, pts[s - 1]); @@ -2877,7 +2890,10 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent if (d < best_ed) { best_ed = d; ed_pts = pts; ed_edge = e; have_edge = true; } } } - if (have_edge && best_ed <= kEdgeTolPx) { + if (have_vtx && best_vd <= kVertexTolPx) { + m_sel_vertex_pt = vtx; + m_solid_sel = SolidSel::Vertex; + } else if (have_edge && best_ed <= kEdgeTolPx) { // Promote the face-relative pick to a STABLE GLOBAL edge id so dress-up ops // (fillet/chamfer) can target this exact edge across recomputes. m_sel_edge = GeometryEngine::edge_index_of(bshape, ed_edge); @@ -2960,6 +2976,28 @@ void DesignSketchTool::render_solid_highlight() m_solid_edge_model.set_color(cyan); m_solid_edge_model.render(); } + } else if (m_solid_sel == SolidSel::Vertex) { + // A camera-facing square at the picked corner, sized in screen terms (the same + // 1/zoom trick the edge ribbon uses) so it stays a constant dot at every zoom. A + // selection you cannot see is not a selection (L5), which is why vertex picking waited + // for this rather than shipping without a highlight. + const Camera& cam = wxGetApp().plater()->get_camera(); + Vec3d right = cam.get_dir_right(), up = cam.get_dir_up(); + const double h = 4.5 / std::max(cam.get_zoom(), 1e-6); // ~4.5 px half-size + right *= h; up *= h; + const Vec3d c = m_sel_vertex_pt; + GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 }; + g.add_vertex((Vec3f)(c - right - up).cast()); + g.add_vertex((Vec3f)(c + right - up).cast()); + g.add_vertex((Vec3f)(c + right + up).cast()); + g.add_vertex((Vec3f)(c - right + up).cast()); + g.add_triangle(0, 1, 2); + g.add_triangle(0, 2, 3); + glsafe(::glDisable(GL_DEPTH_TEST)); + m_solid_vertex_model.reset(); + m_solid_vertex_model.init_from(std::move(g)); + m_solid_vertex_model.set_color(cyan); + m_solid_vertex_model.render(); } } diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index 790b1df075..0ec808abb2 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -119,7 +119,9 @@ public: // Solid topology selection on the committed bodies: clicking a solid cycles // whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With // multiple bodies the pick resolves WHICH body was hit (per-triangle body id). - enum class SolidSel { None, Whole, Face, Edge }; + // Appended, never reordered: DesignPanel maps this to a level int (Whole=1, Face=2, + // Edge=3, Vertex=4) and the offer table keys off it. + enum class SolidSel { None, Whole, Face, Edge, Vertex }; // Point the tool at the current bodies + their concatenated tessellation (non-owning; // pass nullptr to clear). Call after each recompute — selection resets (ids invalidate). // tri_face = per-triangle face id within its body; tri_body = per-triangle body index. @@ -903,6 +905,7 @@ private: int m_sel_face{-1}; 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 void render_solid_highlight(); void render_datum_planes(); // translucent rectangles for datum/reference planes @@ -913,6 +916,7 @@ private: std::vector m_datum_sizes; // per-plane (u,v) full extent; empty -> default GLModel m_solid_face_model; GLModel m_solid_edge_model; + GLModel m_solid_vertex_model; int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none) // Visual Extrude gizmo state (C5b). GUI-only; fed by the panel each refresh_preview.