From cf97b7aba8da73c56f9dfb376ed102426bd96532 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sat, 25 Jul 2026 14:55:34 +0200 Subject: [PATCH] =?UTF-8?q?Design=20tab:=20Mate=20tool=20+=20interference?= =?UTF-8?q?=20report=20=E2=80=94=20the=2016=20tools=20are=20now=20reachabl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mate completes the set: every CadDocument feature type now has a card. It is the one that needed CoordSys wired first, since a mate connector IS a CoordSys feature and cs_a/cs_b are feature indices into the recipe. The card states what each kind constrains rather than just naming it, because the five kinds are not distinguishable from their labels — the useful fact is which DOF each PRESERVES: Fastened fixes all six, Planar leaves in-plane sliding, Revolute leaves spin, Slider leaves axial travel, Cylindrical leaves both. The offset/angle spins retitle per kind, since offset is a plane distance for Planar and a position along the axis for the three joint kinds. Confirm is blocked, with the reason in the status line, when fewer than two CoordSys features exist or when A and B are the same one: a mate with cs_a == cs_b is meaningless and a dangling index recomputes to nothing useful. check_interference() gets a button in the feature-tree header behind a rule, not a tool card — it adds no feature, so it must not checkpoint(), recompute(), or touch the undo stack, and a card would imply it does. Results go to the status line as count + worst volume, with the per-pair list in a message box, named as the parts tree names them. Fixes on top of the generated wiring: - the button's sizer adds sat after the closing brace of the block declaring trow, so trow was out of scope ("'trow' was not declared in this scope"); - the CoordSys client data was typed const void*, which Append rejects; - the report labelled bodies 0-based while all 28 other body labels in this panel (and the parts tree) are 1-based, so it would have called the tree's "Body 2" an interference on "Body 1" — and it was the only unlocalised label. Compiles clean (0 errors); kernel suite unchanged at 139 cases / 1960 assertions. Still to come: the M6 variables panel. The GUI has not yet been exercised on a display. Co-Authored-By: Claude Opus 5 (1M context) --- src/slic3r/GUI/DesignPanel.cpp | 268 +++++++++++++++++++++++++++++++++ src/slic3r/GUI/DesignPanel.hpp | 19 ++- 2 files changed, 286 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index 3b673934c9..9229958bca 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -29,6 +29,7 @@ #include #include #include // wxWindowDisabler, wxMilliSleep +#include // wxMessageBox #include #include @@ -721,6 +722,10 @@ DesignPanel::DesignPanel(wxWindow* parent) populate_plane_choices(m_helix_plane); open_tool(Tool::Helix); }, 0}, + {"design_plane", _L("Mate"), _L("Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"), + [this] { + open_tool(Tool::Mate); + }, 0}, }); auto* b_boolean = icon_btn("design_boolean", _L("Boolean (combine bodies)")); @@ -2413,6 +2418,90 @@ DesignPanel::DesignPanel(wxWindow* parent) } cards->Add(m_box_coordsys, 0, wxEXPAND); + // --- Mate (assembly: align two CoordSys features) --- + m_box_mate = new wxBoxSizer(wxVERTICAL); + m_box_mate->Add(card_header(m_cards, "design_plane", _L("Mate"), m_hdr_mate), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_mate->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8); + { + auto* mform = two_col_form(); + + m_mate_kind = make_combo(m_cards); + m_mate_kind->Append(_L("Fastened — 6 DOF locked, B driven onto A exactly")); + m_mate_kind->Append(_L("Planar — z aligned, offset distance; free in-plane slide")); + m_mate_kind->Append(_L("Revolute — axes collinear; free rotation about z")); + m_mate_kind->Append(_L("Slider — orientation locked; free axial slide")); + m_mate_kind->Append(_L("Cylindrical — axes collinear; free spin + axial slide")); + m_mate_kind->SetSelection(0); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Kind")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(m_mate_kind, 0, wxEXPAND); + + // Populate the CoordSys pickers on open; show "A (fixed)" and "B (moves)" combos. + m_mate_cs_a = make_combo(m_cards); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("CS A (fixed)")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(m_mate_cs_a, 0, wxEXPAND); + + m_mate_cs_b = make_combo(m_cards); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("CS B (moves)")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(m_mate_cs_b, 0, wxEXPAND); + + m_offset_label = new wxStaticText(m_cards, wxID_ANY, _L("Offset")); + m_mate_offset = make_spin(m_cards, 0.0, -100000.0, 100000.0); + mform->Add(m_offset_label, 0, wxALIGN_CENTER_VERTICAL); + mform->Add(spin_frame(m_mate_offset), 0, wxEXPAND); + + m_angle_label = new wxStaticText(m_cards, wxID_ANY, _L("Angle \u00b0")); + m_mate_angle = make_spin(m_cards, 0.0, -360.0, 360.0); + mform->Add(m_angle_label, 0, wxALIGN_CENTER_VERTICAL); + mform->Add(spin_frame(m_mate_angle), 0, wxEXPAND); + + m_mate_flip = new CheckBox(m_cards); + auto* frow = new wxBoxSizer(wxHORIZONTAL); + frow->Add(new wxStaticText(m_cards, wxID_ANY, _L("Flip (oppose z axes)")), 0, wxALIGN_CENTER_VERTICAL); + frow->AddStretchSpacer(); + frow->Add(m_mate_flip, 0, wxALIGN_CENTER_VERTICAL); + mform->Add(0, 0); + mform->Add(frow, 0, wxEXPAND); + + // Retitle offset/angle labels when the kind changes. + m_mate_kind->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { + const int kind = m_mate_kind->GetSelection(); + switch (kind) { + case 0: // Fastened + m_offset_label->SetLabel(_L("Offset")); + m_angle_label->SetLabel(_L("Angle \u00b0")); + break; + case 1: // Planar + m_offset_label->SetLabel(_L("Plane distance")); + m_angle_label->SetLabel(_L("Angle \u00b0")); + break; + case 2: // Revolute + m_offset_label->SetLabel(_L("Axial position")); + m_angle_label->SetLabel(_L("Angle \u00b0")); + break; + case 3: // Slider + m_offset_label->SetLabel(_L("Axial position")); + m_angle_label->SetLabel(_L("Angle \u00b0")); + break; + case 4: // Cylindrical + m_offset_label->SetLabel(_L("Axial position")); + m_angle_label->SetLabel(_L("Angle \u00b0")); + break; + } + m_cards->Layout(); m_form->Layout(); m_form->FitInside(); + refresh_preview(); + e.Skip(); + }); + + m_box_mate->Add(mform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12); + } + cards->Add(m_box_mate, 0, wxEXPAND); + // Refresh the confirm state whenever the mate inputs change. + m_mate_cs_a->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); }); + m_mate_cs_b->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); }); + m_mate_offset->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); }); + m_mate_angle->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); }); + m_mate_flip->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent&) { refresh_preview(); }); + // --- Docked value-entry card (Onshape Button->Dialog->Confirm for dimensions) --- m_box_value = new wxBoxSizer(wxVERTICAL); { @@ -2556,6 +2645,8 @@ DesignPanel::DesignPanel(wxWindow* parent) up->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(-1); }); auto* down = edit_btn("design_movedown", _L("Move down")); down->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_move_feature(+1); }); + m_btn_interfere = edit_btn("color_palette", _L("Check interference — find overlapping bodies")); + m_btn_interfere->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_check_interference(); }); const int gap = FromDIP(SidebarProps::ElementSpacing()); trow->Add(edit, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); trow->Add(move, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); @@ -2563,6 +2654,12 @@ DesignPanel::DesignPanel(wxWindow* parent) trow->Add(del, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); trow->Add(up, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); trow->Add(down, 0, wxALIGN_CENTER_VERTICAL); + // Interference check sits after a rule: it reports, it does not edit the recipe. + trow->AddSpacer(8); + trow->Add(new wxStaticLine(m_tree_box, wxID_ANY, wxDefaultPosition, wxSize(1, 22), wxLI_VERTICAL), + 0, wxALIGN_CENTER_VERTICAL); + trow->AddSpacer(8); + trow->Add(m_btn_interfere, 0, wxALIGN_CENTER_VERTICAL); // trow IS the header sizer — already added to root above the tree. } @@ -4278,6 +4375,79 @@ void DesignPanel::on_add_helix() refresh_tree(); } +void DesignPanel::on_add_mate() +{ + const int sel_a = m_mate_cs_a->GetSelection(); + const int sel_b = m_mate_cs_b->GetSelection(); + if (sel_a == wxNOT_FOUND || sel_b == wxNOT_FOUND) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Mate needs two CoordSys features — create them first")); + m_status->Refresh(); + return; + } + 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))); + if (cs_a == cs_b) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Mate: CS A and CS B must be different CoordSys features")); + m_status->Refresh(); + return; + } + m_feature_counter++; + int idx = m_doc.add_mate(m_mate_kind->GetSelection(), cs_a, cs_b, + m_mate_offset->GetValue(), m_mate_angle->GetValue(), + m_mate_flip->GetValue(), + "Mate" + std::to_string(m_feature_counter)); + if (idx < 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Mate rejected")); + m_status->Refresh(); + return; + } + if (!recompute_guarded(_L("Rebuilding model…"))) + m_status->SetLabel(_L("Recompute error: ") + wxString::FromUTF8(m_doc.error)); + else + set_status_ok(); + refresh_tree(); +} + +void DesignPanel::on_check_interference() +{ + if (m_doc.bodies.size() < 2) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("No interference — need at least two solid bodies to check")); + m_status->Refresh(); + return; + } + const auto pairs = m_doc.check_interference(); + if (pairs.empty()) { + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(_L("No interference found")); + m_status->Refresh(); + return; + } + double worst = 0; + for (const auto& p : pairs) + if (p.volume > worst) worst = p.volume; + m_status->SetForegroundColour(wxNullColour); + m_status->SetLabel(wxString::Format(_L("%zu interference pairs, worst %.2f mm³"), + pairs.size(), worst)); + m_status->Refresh(); + wxString msg = _L("Interference pairs:\n\n"); + for (const auto& p : pairs) { + // 1-based, like every other body label in this panel and in the parts tree: + // reporting "Body 1" for what the tree calls "Body 2" is worse than no name. + wxString na = wxString::Format(_L("Body %d"), p.body_a + 1); + wxString nb = wxString::Format(_L("Body %d"), p.body_b + 1); + if (p.body_a >= 0 && p.body_a < int(m_doc.bodies.size()) && !m_doc.bodies[p.body_a].name.empty()) + na = wxString::FromUTF8(m_doc.bodies[p.body_a].name); + if (p.body_b >= 0 && p.body_b < int(m_doc.bodies.size()) && !m_doc.bodies[p.body_b].name.empty()) + nb = wxString::FromUTF8(m_doc.bodies[p.body_b].name); + msg += wxString::Format("%s <-> %s: %.4f mm³\n", na, nb, p.volume); + } + wxMessageBox(msg, _L("Interference"), wxOK, this); +} + void DesignPanel::populate_sheet_body_choices(ComboBox* c) const { if (!c) return; @@ -4635,6 +4805,7 @@ int DesignPanel::tree_icon_for(CadFeatureType t) case CadFeatureType::Project: return 0; // sketch-family icon (produces sketch) case CadFeatureType::DeleteFace: return 5; // dressup-family icon case CadFeatureType::Helix: return 4; // thread-family icon (curve) + case CadFeatureType::Mate: return 2; // dressup-family icon (assembly) } return 0; } @@ -6778,6 +6949,14 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f) m_helix_left_handed->SetValue(f.helix_left_handed); m_helix_taper->SetValue(f.helix_taper_deg); break; + case CadFeatureType::Mate: + m_mate_kind->SetSelection(f.mate_kind); + // CoordSys pickers are repopulated by open_tool(Mate); pre-selection + // is done there too via the stored feature index. + m_mate_offset->SetValue(f.mate_offset); + m_mate_angle->SetValue(f.mate_angle); + m_mate_flip->SetValue(f.mate_flip); + break; default: break; } } @@ -6961,6 +7140,28 @@ void DesignPanel::on_edit_feature() load_feature_into_dialog(f); open_tool(Tool::Helix); break; + case CadFeatureType::Mate: + m_edit_index = sel; + m_mate_kind->SetSelection(f.mate_kind); + m_mate_offset->SetValue(f.mate_offset); + m_mate_angle->SetValue(f.mate_angle); + m_mate_flip->SetValue(f.mate_flip); + open_tool(Tool::Mate); // populates CS pickers; pre-selects from current selection + // Now set the re-edit cs_a/cs_b after populating (override the open_tool default) + { + int pre_a = wxNOT_FOUND, pre_b = wxNOT_FOUND; + for (unsigned i = 0; i < m_mate_cs_a->GetCount(); ++i) { + const auto* cd = m_mate_cs_a->GetClientData(i); + if (cd && int(reinterpret_cast(cd)) == f.mate_cs_a) pre_a = int(i); + } + for (unsigned i = 0; i < m_mate_cs_b->GetCount(); ++i) { + const auto* cd = m_mate_cs_b->GetClientData(i); + if (cd && int(reinterpret_cast(cd)) == f.mate_cs_b) pre_b = int(i); + } + if (pre_a != wxNOT_FOUND) m_mate_cs_a->SetSelection(pre_a); + if (pre_b != wxNOT_FOUND) m_mate_cs_b->SetSelection(pre_b); + } + break; default: // Import / Cut have no parametric edit dialog yet (follow-up // snaporca-nu9). Don't silently swallow the Edit click — tell the user. @@ -7329,6 +7530,18 @@ CadFeature DesignPanel::build_candidate(Tool t) const f.helix_taper_deg = m_helix_taper->GetValue(); break; } + case Tool::Mate: { + f.type = CadFeatureType::Mate; + f.mate_kind = m_mate_kind->GetSelection(); + f.mate_cs_a = m_mate_cs_a->GetSelection() != wxNOT_FOUND + ? int(reinterpret_cast(m_mate_cs_a->GetClientData(m_mate_cs_a->GetSelection()))) : -1; + f.mate_cs_b = m_mate_cs_b->GetSelection() != wxNOT_FOUND + ? int(reinterpret_cast(m_mate_cs_b->GetClientData(m_mate_cs_b->GetSelection()))) : -1; + f.mate_offset = m_mate_offset->GetValue(); + f.mate_angle = m_mate_angle->GetValue(); + f.mate_flip = m_mate_flip->GetValue(); + break; + } case Tool::Insert: // imported art is committed by add_imported_sketch, not build_candidate case Tool::None: break; @@ -7712,6 +7925,30 @@ void DesignPanel::refresh_preview() return; } + 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(); + 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_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Mate needs at least two CoordSys features")); + } else if (same) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Mate: CS A and CS B must be different")); + } else { + m_status->SetForegroundColour(wxColour(120, 210, 120)); + m_status->SetLabel(_L("Mate ready")); + } + for (wxButton* b : m_confirm_btns) if (b) b->Enable(ok); + m_status->Refresh(); + return; + } + // Trim m_body_xform to the LIVE committed bodies before building the ghost. A move // writes a per-body display transform keyed by index; if a moved body is later deleted // or consumed, its stale transform must not survive and get re-applied to whatever new @@ -7901,8 +8138,36 @@ void DesignPanel::open_tool(Tool t) s->Show(m_box_project, t == Tool::Project, true); s->Show(m_box_delete_face, t == Tool::DeleteFace, true); s->Show(m_box_helix, t == Tool::Helix, true); + s->Show(m_box_mate, t == Tool::Mate, true); s->Show(m_box_insert, t == Tool::Insert, true); + if (t == Tool::Mate) { + // Populate both CoordSys pickers with every CoordSys feature; store the real + // feature index in client data. Keep prior selection when re-editing. + int keep_a = -1, keep_b = -1; + if (m_mate_cs_a->GetCount() > 0 && m_mate_cs_a->GetSelection() != wxNOT_FOUND) { + const auto* cl = m_mate_cs_a->GetClientData(m_mate_cs_a->GetSelection()); + if (cl) keep_a = int(reinterpret_cast(cl)); + } + if (m_mate_cs_b->GetCount() > 0 && m_mate_cs_b->GetSelection() != wxNOT_FOUND) { + const auto* cl = m_mate_cs_b->GetClientData(m_mate_cs_b->GetSelection()); + if (cl) keep_b = int(reinterpret_cast(cl)); + } + m_mate_cs_a->Clear(); + m_mate_cs_b->Clear(); + for (int i = 0; i < int(m_doc.features.size()); ++i) { + if (m_doc.features[i].type != CadFeatureType::CoordSys) continue; + const wxString nm = wxString::FromUTF8(m_doc.features[i].name); + void* const cd = reinterpret_cast(intptr_t(i)); + const int pos_a = m_mate_cs_a->Append(nm, wxNullBitmap, cd); + const int pos_b = m_mate_cs_b->Append(nm, wxNullBitmap, cd); + if (i == keep_a) m_mate_cs_a->SetSelection(pos_a); + if (i == keep_b) m_mate_cs_b->SetSelection(pos_b); + } + if (keep_a < 0 && m_mate_cs_a->GetCount() > 0) m_mate_cs_a->SetSelection(0); + if (keep_b < 0 && m_mate_cs_b->GetCount() > 0) m_mate_cs_b->SetSelection(0); + } + if (t == Tool::Revolve && m_revolve_sketch_ref >= 0 && m_revolve_sketch_ref < int(m_doc.features.size())) m_revolve_sketch_label->SetLabel(_L("Sketch: ") + @@ -8038,6 +8303,7 @@ void DesignPanel::open_tool(Tool t) case Tool::Project: m_hdr_project->SetLabel(title(_L("Project"))); break; case Tool::DeleteFace: m_hdr_delete_face->SetLabel(title(_L("Delete Face"))); break; case Tool::Helix: m_hdr_helix->SetLabel(title(_L("Helix"))); break; + case Tool::Mate: m_hdr_mate->SetLabel(title(_L("Mate"))); break; case Tool::Insert: break; // header set by open_insert_card() case Tool::None: break; } @@ -8083,6 +8349,7 @@ void DesignPanel::close_tool() s->Show(m_box_project, false, true); s->Show(m_box_delete_face, false, true); s->Show(m_box_helix, false, true); + s->Show(m_box_mate, false, true); s->Show(m_box_insert, false, true); m_viewport->clear_preview(); m_viewport->clear_extrude_gizmo(); @@ -8137,6 +8404,7 @@ void DesignPanel::confirm_tool() case Tool::Cut: on_add_cut(); break; case Tool::Axis: on_add_axis(); break; case Tool::CoordSys: on_add_coordsys(); break; + case Tool::Mate: on_add_mate(); break; case Tool::SurfaceExtrude: on_add_surface_extrude(); break; case Tool::SurfaceRevolve: on_add_surface_revolve(); break; case Tool::SurfaceLoft: on_add_surface_loft(); break; diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index 3cfe639a1f..6cd26a5ee7 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -51,7 +51,7 @@ public: void mcp_after_change() { after_tree_edit(true); } // refresh tree + viewport + status private: - enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix }; + enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate }; // Plane tool: which datum reference the next solid pick fills (declared early so the // method decls + card lambdas below can name it). enum class PlanePick { None, FaceA, FaceB, EdgeA, EdgeB }; @@ -112,6 +112,8 @@ private: void on_add_project(); void on_add_delete_face(); void on_add_helix(); + void on_add_mate(); + void on_check_interference(); // Fill m_bool_target / m_bool_tool / m_cut_target. as_of_feature < 0 = current bodies (add); // >= 0 = the bodies as they existed just before that feature index (Boolean re-edit, so a // consumed tool body still appears and its saved selection round-trips). @@ -280,6 +282,7 @@ private: wxSizer* m_box_project{nullptr}; wxSizer* m_box_delete_face{nullptr}; wxSizer* m_box_helix{nullptr}; + wxSizer* m_box_mate{nullptr}; wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm // Move-body gizmo runs through the unified action bar too: Confirm keeps the placement, @@ -324,6 +327,7 @@ private: wxStaticText* m_hdr_project{nullptr}; wxStaticText* m_hdr_delete_face{nullptr}; wxStaticText* m_hdr_helix{nullptr}; + wxStaticText* m_hdr_mate{nullptr}; wxStaticText* m_hdr_insert{nullptr}; wxScrolledWindow* m_form{nullptr}; @@ -478,6 +482,19 @@ private: CheckBox* m_helix_left_handed{nullptr}; wxSpinCtrlDouble* m_helix_taper{nullptr}; + // Mate (assembly) controls + ComboBox* m_mate_kind{nullptr}; + ComboBox* m_mate_cs_a{nullptr}; + ComboBox* m_mate_cs_b{nullptr}; + wxSpinCtrlDouble* m_mate_offset{nullptr}; + wxSpinCtrlDouble* m_mate_angle{nullptr}; + CheckBox* m_mate_flip{nullptr}; + wxStaticText* m_offset_label{nullptr}; + wxStaticText* m_angle_label{nullptr}; + + // Feature-tree button + ScalableButton* m_btn_interfere{nullptr}; + // Pattern controls (replicate the target body: linear or circular). ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular wxSpinCtrlDouble* m_pattern_count{nullptr}; // total instances incl. seed