diff --git a/one_vertex_touch.svg b/one_vertex_touch.svg new file mode 100644 index 0000000000..e716f32a03 --- /dev/null +++ b/one_vertex_touch.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index f1cd98df54..ecca6d415f 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -5591,6 +5591,9 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) // which assembly mates a connector pair admits. Shown only when the document holds at least // two ENABLED CoordSys features: below that the whole section would be one permanently dead // row, which is noise rather than information. + // Set while a hovered mate row is showing its ghost, so the cleanup after PopupMenu can tell + // "I put that there" from "some other tool's preview was already on screen". + bool mate_ghost = false; std::vector cs_features; for (int i = 0; i < int(m_doc.features.size()); ++i) if (m_doc.features[i].type == CadFeatureType::CoordSys && m_doc.features[i].enabled) @@ -5646,9 +5649,31 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) menu.Append(mate_base + 1 + i, label)->Enable(o.viable); } - menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, mate_base](wxCommandEvent& e) { + // Hovering a row shows the RESULT, not a description of it (epic gap G3). preview() moves + // the body on a throwaway copy of the document, so nothing is written until the click — + // the "commit nothing until you choose" behaviour the palette was asked for. + auto drop_ghost = [this, &mate_ghost]() { + if (!mate_ghost) return; + m_viewport->clear_preview(); + m_viewport->set_body_hidden(false); + m_viewport->request_repaint(); + mate_ghost = false; + }; + menu.Bind(wxEVT_MENU_HIGHLIGHT, + [this, cs_a, cs_b, opts, mate_base, &mate_ghost, drop_ghost](wxMenuEvent& e) { + const int i = e.GetMenuId() - (mate_base + 1); + // Off the palette (a verb row, the header, or nothing) — a stale ghost from the row + // you just left is worse than none, so it goes as soon as the cursor does. + if (i < 0 || i >= int(opts.size()) || !opts[i].viable) { drop_ghost(); return; } + std::string err; + mate_ghost = show_mate_ghost(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, err); + m_viewport->request_repaint(); + }); + + menu.Bind(wxEVT_MENU, [this, cs_a, cs_b, opts, mate_base, drop_ghost](wxCommandEvent& e) { const int i = e.GetId() - (mate_base + 1); if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return; + drop_ghost(); // the real bodies are about to become the ghost's pose m_doc.checkpoint(); // undo boundary: committing a mate from the offer int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, "Mate" + std::to_string(++m_feature_counter)); @@ -5682,6 +5707,14 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) run_offer_action(bound[i]->action); }); PopupMenu(&menu, ScreenToClient(screen_pos)); + // PopupMenu is modal, so by here the menu is gone and any command it raised has already run. + // A hover ghost that outlived the menu it belonged to would leave the committed bodies hidden + // behind a preview nothing can dismiss — bind nothing to the close event, just clean up here. + if (mate_ghost) { + m_viewport->clear_preview(); + m_viewport->set_body_hidden(false); + m_viewport->request_repaint(); + } } void DesignPanel::apply_plane_refs(CadFeature& f) const @@ -9355,6 +9388,44 @@ void DesignPanel::update_reference_planes() m_viewport->clear_base_pick(); } +TriangleMesh DesignPanel::ghost_from_bodies(const std::vector& per_body) const +{ + TriangleMesh out; + for (size_t b = 0; b < per_body.size(); ++b) { + TriangleMesh m = per_body[b]; + if (b < m_body_xform.size()) m.transform(m_body_xform[b]); + out.merge(m); + } + return out; +} + +bool DesignPanel::show_mate_ghost(int kind, int cs_a, int cs_b, + double offset, double angle_deg, bool flip, std::string& err) +{ + CadFeature cand; + cand.type = CadFeatureType::Mate; + cand.mate_kind = kind; + cand.mate_cs_a = cs_a; + cand.mate_cs_b = cs_b; + cand.mate_offset = offset; + cand.mate_angle = angle_deg; + cand.mate_flip = flip; + + TriangleMesh mesh; + std::vector pbm; + if (!m_doc.preview(cand, mesh, pbm, err)) { + m_viewport->clear_preview(); + m_viewport->set_body_hidden(false); + return false; + } + m_viewport->set_preview_mesh(ghost_from_bodies(pbm)); + // The ghost is the WHOLE assembly in its post-mate pose, not an added lump, so the committed + // bodies have to go: left visible they would draw the mated body in both places at once and + // z-fight everything else against its own copy. Same reason Dressup and Draft hide them. + m_viewport->set_body_hidden(true); + return true; +} + void DesignPanel::refresh_preview() { if (m_active == Tool::None) { m_viewport->clear_preview(); return; } @@ -9386,23 +9457,44 @@ void DesignPanel::refresh_preview() } if (m_active == Tool::Mate) { - // Mate has no 3D ghost — it is an assembly constraint driven by CoordSys features. - // Confirm must be disabled when <2 CoordSys exist or when A == B. - m_viewport->clear_preview(); + // A mate produces no NEW geometry, but it MOVES a body — and the moved assembly is + // exactly the ghost worth showing. This branch used to clear the preview outright, + // saying a mate has no 3D ghost; the kernel had no such opinion (preview() routes a + // Mate candidate through apply_mate on a throwaway copy), so that one line was the + // whole of gap G3. The card's own gate stays: Confirm is disabled when <2 CoordSys + // exist or when A == B, because neither of those is a kernel error, only an unfinished + // form, and the kernel's message for them would be worse than these. const bool has_two = m_mate_cs_a && m_mate_cs_a->GetCount() >= 2; const int sel_a = has_two ? m_mate_cs_a->GetSelection() : wxNOT_FOUND; const int sel_b = has_two ? m_mate_cs_b->GetSelection() : wxNOT_FOUND; const bool same = has_two && sel_a != wxNOT_FOUND && sel_b != wxNOT_FOUND && sel_a == sel_b; bool ok = has_two && !same; if (!has_two) { + m_viewport->clear_preview(); + m_viewport->set_body_hidden(false); // the ghost replaced them; give them back m_status->SetForegroundColour(wxColour(235, 110, 110)); set_status(_L("Mate needs at least two CoordSys features")); } else if (same) { + m_viewport->clear_preview(); + m_viewport->set_body_hidden(false); m_status->SetForegroundColour(wxColour(235, 110, 110)); set_status(_L("Mate: CS A and CS B must be different")); } else { - m_status->SetForegroundColour(wxColour(120, 210, 120)); - set_status(_L("Mate ready")); + sync_body_xform(); // same reason as the solid path below: drop stale per-body poses + const int cs_a = int(reinterpret_cast(m_mate_cs_a->GetClientData(sel_a))); + const int cs_b = int(reinterpret_cast(m_mate_cs_b->GetClientData(sel_b))); + std::string err; + ok = show_mate_ghost(m_mate_kind ? m_mate_kind->GetSelection() : 0, cs_a, cs_b, + m_mate_offset ? m_mate_offset->GetValue() : 0.0, + m_mate_angle ? m_mate_angle->GetValue() : 0.0, + m_mate_flip && m_mate_flip->GetValue(), err); + if (ok) { + m_status->SetForegroundColour(wxColour(120, 210, 120)); + set_status(_L("Mate ready")); + } else { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + set_status(_L("Invalid: ") + wxString::FromUTF8(err)); + } } for (wxButton* b : m_confirm_btns) if (b) b->Enable(ok); m_status->Refresh(); @@ -9424,15 +9516,7 @@ void DesignPanel::refresh_preview() // The body is displayed through its per-body Move transform (m_body_xform); the ghost is // built from the untransformed kernel, so without this it floats back at the origin once a // body has been moved. Re-merge the per-body ghost meshes with the same transforms applied. - auto ghost_from = [this](const std::vector& pbm) -> TriangleMesh { - TriangleMesh out; - for (size_t b = 0; b < pbm.size(); ++b) { - TriangleMesh m = pbm[b]; - if (b < m_body_xform.size()) m.transform(m_body_xform[b]); - out.merge(m); - } - return out; - }; + auto ghost_from = [this](const std::vector& pbm) { return ghost_from_bodies(pbm); }; const bool editing_single = (m_edit_index >= 0); if (editing_single) { diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 580a1b2eb3..6a8fa7acca 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -241,6 +241,14 @@ private: void set_hole_target_label(int face); void set_thread_target_label(int face, int edge); CadFeature build_candidate(Tool t) const; + // Merge per-body ghost meshes with the per-body display transforms applied. The kernel builds + // a ghost from the untransformed bodies, so without this it floats back at the origin once a + // body has been moved. + TriangleMesh ghost_from_bodies(const std::vector& per_body) const; + // A mate makes no new geometry but it MOVES a body, and the moved assembly is the ghost worth + // showing. Used both by the Mate card and by hovering a row of the offer's mate palette. + bool show_mate_ghost(int kind, int cs_a, int cs_b, + double offset, double angle_deg, bool flip, std::string& err); int resolve_extrude_sketch() const; // Plane pickers: fill a choice with XY/XZ/YZ + the document's datum planes, and // map a choice row back to the actual SketchPlane (rows 0-2 base, 3+ datum).