From 840d44b6118383a721ae3207c41a527192081024 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 6 Oct 2026 11:21:18 +0800 Subject: [PATCH] Keep the Design tab's edge lines in step with the bodies after undo, hide and delete --- src/slic3r/GUI/CAD/DesignCanvas.cpp | 3 + src/slic3r/GUI/CAD/DesignCanvas.hpp | 5 +- src/slic3r/GUI/CAD/DesignPanel.cpp | 17 ++++-- src/slic3r/GUI/CAD/DesignSketchTool.cpp | 73 +++++++++++++++++-------- src/slic3r/GUI/CAD/DesignSketchTool.hpp | 17 ++++-- 5 files changed, 81 insertions(+), 34 deletions(-) diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index d2c78ed9d7..09c22ba678 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -414,6 +414,7 @@ void DesignCanvas::set_bodies(const std::vector* body_meshes, if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; } m_body_meshes = body_meshes; + m_sketch_tool.refresh_body_edges(); // of the bodies set_solid_pick() pointed the tool at m_lit_faces = m_sketch_tool.selected_faces(); rebuild_bodies(); reload(!m_first_frame); @@ -423,6 +424,8 @@ void DesignCanvas::clear_mesh() { m_body_meshes = nullptr; m_volumes.clear(); + // No solid, so no edge lines, pick or hover either, as after a rebuild that leaves no body. + m_sketch_tool.set_solid_pick(nullptr, nullptr, nullptr, nullptr); if (!m_model.objects.empty()) { m_model.delete_object((size_t)0); reload(true); diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index a80ab4bef2..f5abb14a95 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -49,10 +49,11 @@ public: // Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour). // `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept - // by address (a stable panel member) and read again whenever the selection changes. + // by address (a stable panel member) and read again whenever the selection changes. The + // bodies' edge lines are refreshed with them (DesignSketchTool::refresh_body_edges). void set_bodies(const std::vector* body_meshes, const std::vector& visible = {}); - void clear_mesh(); + void clear_mesh(); // no bodies: drops their volumes and the tool's edge lines, pick and selection void set_preview_mesh(const TriangleMesh& mesh); void clear_preview(); diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index e7f77cf290..c93ee937d5 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -7896,8 +7896,7 @@ void DesignPanel::set_tree_selection(int row) // The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and // features by index, so replacing or renumbering the feature list (undo/redo, New Design, load, -// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild -// that leaves no body never reaches set_solid_pick. The callers repaint. +// delete, reorder) drops it, the viewport's highlights with it. The callers repaint. void DesignPanel::drop_selection() { if (m_viewport != nullptr) @@ -8093,8 +8092,18 @@ void DesignPanel::on_toggle_visibility() refresh_tree(); // greys the row set_tree_selection(sel); // keep the toggled feature selected if (m_viewport != nullptr) { - if (m_doc.display_mesh.its.indices.empty()) m_viewport->clear_mesh(); - else feed_bodies(); + if (m_doc.display_mesh.its.indices.empty()) { + m_viewport->clear_mesh(); + } else { + // Not set_status_ok(), which would take the gizmo from a Move still open. + feed_bodies(); + // Hiding or showing a feature rebuilds the bodies and can renumber them, so the pick is + // re-pointed and its selection dropped. That also brings picking back for a body shown + // after everything was hidden (clear_mesh() dropped it). + m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh, + &m_doc.display_tri_face, &m_doc.display_tri_body, + &m_body_visible, &m_body_xform); + } } sync_sketch_display(); // skips the hidden sketch + direct-renders set_status(StatusKind::Info, shown ? _L("Feature shown") : _L("Feature hidden")); diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 35992344d7..7049f7b08d 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -3473,18 +3473,19 @@ static double display_edge_tol(const TopoDS_Shape& shape) void DesignSketchTool::refresh_body_edges() { const size_t n = m_solid_bodies != nullptr ? m_solid_bodies->size() : 0; + bool rebuilt = n < m_body_edges_shape.size(); // a body went away m_body_edges.resize(n); - m_body_edges_key.resize(n, nullptr); + m_body_edges_shape.resize(n); m_body_edges_tol.resize(n, 0.0); for (size_t b = 0; b < n; ++b) { - const void* key = body_key(int(b)); - if (key == m_body_edges_key[b] && key != nullptr) - continue; - m_body_edges_key[b] = key; - m_body_edges[b].clear(); - if (key == nullptr) + if (is_current_shape(int(b), m_body_edges_shape[b])) continue; + rebuilt = rebuilt || !m_body_edges_shape[b].IsNull(); // a body sampled before was rebuilt const TopoDS_Shape& shape = (*m_solid_bodies)[b].shape; + m_body_edges_shape[b] = shape; + m_body_edges[b].clear(); + if (shape.IsNull()) + continue; m_body_edges_tol[b] = display_edge_tol(shape); try { m_body_edges[b] = GeometryEngine::display_edges(shape, m_body_edges_tol[b]); @@ -3492,15 +3493,36 @@ void DesignSketchTool::refresh_body_edges() m_body_edges[b].clear(); // an unsampleable edge costs its body the lines, nothing else } } + // A pick expires once the body it was taken on is rebuilt, which renumbers its faces and + // edges. Judged by that body's shape, not by these lines: a recompute that refreshed no lines + // (adding a datum, say) may come before a pick on the bodies it built, and that pick holds. + // The hover is resolved again on the next mouse move, so any rebuild just drops it. + if (m_solid_sel != SolidSel::None && !is_current_shape(m_sel_body, m_sel_shape)) + clear_solid_selection(); + else if (rebuilt) + m_pre = SolidPick{}; + // The face outlines of a rebuilt body let go of its shape, which may be a large import. + const auto stale = [this](const FaceHighlight& h) { return !is_current_shape(h.body, h.shape); }; + if (stale(m_sel_hl)) m_sel_hl = FaceHighlight{}; + if (stale(m_pre_hl)) m_pre_hl = FaceHighlight{}; + m_hl_faces.erase(std::remove_if(m_hl_faces.begin(), m_hl_faces.end(), stale), m_hl_faces.end()); } -// The identity of a body's current shape. Face ids and sampled edges hold while it does. -const void* DesignSketchTool::body_key(int body) const +const TopoDS_Shape* DesignSketchTool::body_shape(int body) const { if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size())) return nullptr; const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape; - return shape.IsNull() ? nullptr : shape.TShape().get(); + return shape.IsNull() ? nullptr : &shape; +} + +// Whether `sampled` is still the body's shape. The caches hold the shape itself rather than its +// address, so the sampled TShape stays alive and no rebuilt body can be allocated in its place. +// IsSame also compares the Location, which the sampled lines are in. +bool DesignSketchTool::is_current_shape(int body, const TopoDS_Shape& sampled) const +{ + const TopoDS_Shape* shape = body_shape(body); + return shape != nullptr && sampled.IsSame(*shape); } // View-facing ribbons along a body's polylines (shape coordinates), `hw` either side of the line @@ -3603,6 +3625,7 @@ void DesignSketchTool::clear_solid_selection() { m_solid_sel = SolidSel::None; m_sel_body = m_sel_face = m_sel_edge = -1; + m_sel_shape = TopoDS_Shape(); m_sel_edge_pts.clear(); m_sel_edges_more.clear(); m_sel_edges_more_pts.clear(); @@ -3630,6 +3653,7 @@ void DesignSketchTool::select_body(int body) return; } m_sel_body = body; + m_sel_shape = (*m_solid_bodies)[body].shape; m_sel_face = m_sel_edge = -1; m_sel_edge_pts.clear(); m_sel_edges_more.clear(); @@ -3641,7 +3665,7 @@ void DesignSketchTool::set_highlight_faces(const std::vector { std::map> by_body; for (const auto& [b, f] : faces) - if (body_key(b) != nullptr) + if (body_shape(b) != nullptr) by_body[b].push_back(f); // Reuse each body's entry, so re-sending the same faces resamples nothing. std::vector next; @@ -3663,13 +3687,16 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body, std::sort(faces.begin(), faces.end()); faces.erase(std::unique(faces.begin(), faces.end()), faces.end()); h.faces = std::move(faces); - h.key = body_key(body); - if (h.key == nullptr || h.faces.empty()) + const TopoDS_Shape* shape = body_shape(body); + if (shape == nullptr) + return h; + h.shape = *shape; + if (h.faces.empty()) return h; // Sampled at the body edges' tolerance; a whole body reuses its body edges. - const TopoDS_Shape& shape = (*m_solid_bodies)[body].shape; - const std::vector all = GeometryEngine::faces_of(shape); - const bool cached = body < int(m_body_edges_key.size()) && m_body_edges_key[body] == h.key; + const std::vector all = GeometryEngine::faces_of(*shape); + const bool cached = body < int(m_body_edges_shape.size()) + && is_current_shape(body, m_body_edges_shape[body]); if (cached && h.faces.size() == all.size() && h.faces.front() == 0 && h.faces.back() == int(all.size()) - 1) { h.edges = m_body_edges[body]; return h; @@ -3681,7 +3708,7 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body, if (f >= 0 && f < int(all.size())) builder.Add(picked, all[f]); try { - h.edges = GeometryEngine::display_edges(picked, cached ? m_body_edges_tol[body] : display_edge_tol(shape)); + h.edges = GeometryEngine::display_edges(picked, cached ? m_body_edges_tol[body] : display_edge_tol(*shape)); } catch (const Standard_Failure&) { h.edges.clear(); // an edge that cannot be sampled costs the outline, never the fill } @@ -3691,12 +3718,13 @@ DesignSketchTool::FaceHighlight DesignSketchTool::make_face_highlight(int body, std::vector> DesignSketchTool::picked_faces() const { std::vector> out; - if (body_key(m_sel_body) == nullptr) + const TopoDS_Shape* shape = body_shape(m_sel_body); + if (shape == nullptr) return out; if (m_solid_sel == SolidSel::Face && m_sel_face >= 0) out.emplace_back(m_sel_body, m_sel_face); else if (m_solid_sel == SolidSel::Whole) - for (int f = 0, n = GeometryEngine::face_count((*m_solid_bodies)[m_sel_body].shape); f < n; ++f) + for (int f = 0, n = GeometryEngine::face_count(*shape); f < n; ++f) out.emplace_back(m_sel_body, f); return out; } @@ -3705,7 +3733,7 @@ std::vector> DesignSketchTool::selected_faces() const { std::vector> out = picked_faces(); for (const FaceHighlight& h : m_hl_faces) - if (h.key != nullptr && h.key == body_key(h.body)) + if (is_current_shape(h.body, h.shape)) for (int f : h.faces) out.emplace_back(h.body, f); std::sort(out.begin(), out.end()); @@ -3718,7 +3746,7 @@ const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(F { std::sort(faces.begin(), faces.end()); faces.erase(std::unique(faces.begin(), faces.end()), faces.end()); - if (cache.body != body || cache.faces != faces || cache.key != body_key(body)) + if (cache.body != body || cache.faces != faces || !is_current_shape(body, cache.shape)) cache = make_face_highlight(body, std::move(faces)); return cache; } @@ -3729,7 +3757,7 @@ const DesignSketchTool::FaceHighlight& DesignSketchTool::cached_face_highlight(F // version: the line alone, thinner and fainter. void DesignSketchTool::render_face_outline(const FaceHighlight& h, bool quiet) { - if (m_body_edges_hidden || h.key == nullptr || h.key != body_key(h.body) || !body_pickable(h.body)) + if (m_body_edges_hidden || !is_current_shape(h.body, h.shape) || !body_pickable(h.body)) return; // hidden, or the body was rebuilt and these face ids are stale const Camera& cam = wxGetApp().plater()->get_camera(); @@ -4006,6 +4034,7 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent m_sel_edges_more_pts.clear(); m_sel_body = p.body; + m_sel_shape = (*m_solid_bodies)[p.body].shape; // resolve_solid_pick checked p.body m_sel_face = p.face; m_sel_edge = p.edge; m_sel_edge_pts = std::move(p.edge_pts); diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 85693dba27..cfcf2e8441 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -4,6 +4,7 @@ #include "libslic3r/Point.hpp" #include "libslic3r/CAD/SketchEngine.hpp" #include "libslic3r/CAD/CadDocument.hpp" // CadBody for per-body solid picking +#include #include "libslic3r/CAD/SketchInference.hpp" #include "slic3r/GUI/GLModel.hpp" #include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies @@ -208,6 +209,9 @@ public: // their edges do not float over the preview. void set_body_edges_hidden(bool h) { m_body_edges_hidden = h; } void clear_solid_selection(); + // Resample the edges of each body whose shape changed and drop those of bodies that are gone. + // Also expires a pick or hover on a rebuilt body. The canvas calls it whenever it gets the bodies. + void refresh_body_edges(); bool has_solid_selection() const { return m_solid_sel != SolidSel::None; } // Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes // edges of the same body): the earlier picks first, the last-clicked edge at the end. @@ -1226,19 +1230,20 @@ private: Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform // The bodies' B-rep edges, drawn as dark lines over the solids so faces and features read // apart. One polyline set per body in its own shape coordinates, resampled only for a body - // whose shape changed (keyed by the TShape), since set_solid_pick runs on every recompute. + // whose shape changed (see is_current_shape). std::vector>> m_body_edges; - std::vector m_body_edges_key; + std::vector m_body_edges_shape; // the shape each set was sampled from std::vector m_body_edges_tol; // the chord tolerance they were sampled at bool m_body_edges_hidden{false}; - void refresh_body_edges(); void render_body_edges(); - const void* body_key(int body) const; // the body's TShape, nullptr when there is none + const TopoDS_Shape* body_shape(int body) const; // nullptr when there is no such body or it has no shape + bool is_current_shape(int body, const TopoDS_Shape& sampled) const; // `sampled` is still the body's shape void append_ribbons(GLModel::Geometry& g, int body, const std::vector>& polylines, const Vec3d& vd, const Vec3d& pull, double hw) const; bool body_pickable(int b) const; // false when the body is explicitly hidden SolidSel m_solid_sel{SolidSel::None}; int m_sel_body{-1}; // which body the face/edge selection is on + TopoDS_Shape m_sel_shape; // that body's shape when picked: the ids index into it int m_sel_face{-1}; int m_sel_edge{-1}; std::vector m_sel_edge_pts; @@ -1283,11 +1288,11 @@ private: void render_solid_sel(SolidSel kind, const std::vector& edge_pts, const Vec3d& vertex_pt, const ColorRGBA& rgb); // A set of selected faces of one body, with their edges sampled once, keyed by the body's - // TShape so a recompute that rebuilt the body retires it. + // shape so a recompute that rebuilt the body retires it. struct FaceHighlight { int body{-1}; std::vector faces; // sorted - const void* key{nullptr}; + TopoDS_Shape shape; std::vector> edges; // in the body's shape coordinates }; FaceHighlight make_face_highlight(int body, std::vector faces) const;