From 8cbf5b393b6252a65519ee282e01b218a80c0964 Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Tue, 30 Jun 2026 23:21:04 +0200 Subject: [PATCH] Design: Measure-style dimension labels (+ projection fix), New Design, tree auto-fit, i18n pin, UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sketch dimension labels — now identical to the Prepare/Preview Measure gizmo: - draw_text repurposed to draw_dim_label: white ImGui text in a translucent-white box, mirroring GLGizmoMeasure::render_dimensioning exactly (push_common_window_style sets the text colour, BringWindowToDisplayFront, imgui_internal.h). - ROOT-CAUSE FIX: world_to_screen_px multiplied two Eigen Transform3d objects ((proj * view).matrix()); a projection is not affine so Eigen mangled it -> garbage screen coords, so labels never appeared. Now proj.matrix() * view.matrix() like Measure. (That helper was previously [[maybe_unused]] dead code, never exercised.) - Leaders: offset clear of the sketch line (no longer coincident with the geometry), single point-to-point dimension line + arrows, neutral colour, width 0.6 -> 0.2. - dim_text appends mm/in on linear dims (angles keep the degree sign). New Design + delete: - New "New Design" button wipes the whole document (confirm dialog) — the clear-all the per-row Delete can't give. CadDocument::clear() now also clears bodies + display_body_meshes (it left them stale, so solids lingered after a clear). - on_delete_feature: a Body-row selection now shows a helpful hint (bodies are recomputed results with no directly-removable feature) instead of silently doing nothing. Feature tree: auto-fits its content (refresh_tree clamps height 1..9 rows, scrolls past), instead of a fixed 140px block. i18n (Design tab pinned English, per the UX contract): - Restore the lost #undef _L / #define _L(s) wxString::FromUTF8(s) override atop DesignPanel.cpp; wrap all ~54 dropdown options in _L so the single lever governs them. feature_type_name left untranslated (machine-facing MCP JSON). UX: per-card Value Confirm/Cancel buttons removed — the single ribbon action bar owns value confirm/cancel via an m_value_cont guard in tool_confirm/tool_cancel. "needs a body" status messages unified. Both forks; DesignSketchTool.{cpp,hpp} + CadDocument.cpp byte-identical across forks. Built clean; New Design / feature-delete / rotation / tree auto-fit live-verified on :10. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q --- src/libslic3r/CadDocument.cpp | 2 ++ src/slic3r/GUI/DesignPanel.cpp | 46 ++++++++++++++++++++++++++++- src/slic3r/GUI/DesignPanel.hpp | 1 + src/slic3r/GUI/DesignSketchTool.cpp | 29 +++++++++++++----- src/slic3r/GUI/DesignSketchTool.hpp | 1 + 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/CadDocument.cpp b/src/libslic3r/CadDocument.cpp index 695d838d07..160a127d83 100644 --- a/src/libslic3r/CadDocument.cpp +++ b/src/libslic3r/CadDocument.cpp @@ -884,7 +884,9 @@ void CadDocument::clear() { features.clear(); body = TopoDS_Shape(); + bodies.clear(); // multibody result — must clear too (else solids linger) display_mesh = TriangleMesh{}; + display_body_meshes.clear(); display_tri_face.clear(); error.clear(); // A cleared document is a fresh start with no history. diff --git a/src/slic3r/GUI/DesignPanel.cpp b/src/slic3r/GUI/DesignPanel.cpp index 5180169163..14f577828c 100644 --- a/src/slic3r/GUI/DesignPanel.cpp +++ b/src/slic3r/GUI/DesignPanel.cpp @@ -1410,7 +1410,7 @@ DesignPanel::DesignPanel(wxWindow* parent) root->Add(m_box_constraints, 0, wxEXPAND); root->Add(new wxStaticText(m_form, wxID_ANY, _L("Feature tree")), 0, wxLEFT | wxTOP, 12); - m_tree = new wxTreeCtrl(m_form, wxID_ANY, wxDefaultPosition, wxSize(-1, 140), + m_tree = new wxTreeCtrl(m_form, wxID_ANY, wxDefaultPosition, wxSize(-1, 64), wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxTR_FULL_ROW_HIGHLIGHT | wxBORDER_SIMPLE); if (!dp_dark()) m_tree->SetBackgroundColour(dp_panel_bg()); @@ -1512,6 +1512,10 @@ DesignPanel::DesignPanel(wxWindow* parent) } root->Add(m_dof_status, 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); + auto* new_design = new wxButton(m_form, wxID_ANY, _L("New Design")); + new_design->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_new_design(); }); + root->Add(new_design, 0, wxLEFT | wxRIGHT | wxTOP, 12); + auto* commit = new wxButton(m_form, wxID_ANY, _L("Commit to Plate")); commit->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_commit(); }); root->Add(commit, 0, wxLEFT | wxRIGHT | wxTOP, 12); @@ -2941,6 +2945,17 @@ void DesignPanel::refresh_tree() } if (keep >= 0 && keep < int(m_tree_items.size())) m_tree->SelectItem(m_tree_items[keep]); + + // Size the tree to its content (clamped) so it doesn't waste a fixed-height block when + // there are few features, and scrolls internally past ~9 rows instead of growing forever. + int rows = int(m_tree_items.size()); + if (m_doc.bodies.size() > 1) rows += 1 + int(m_doc.bodies.size()); // "Bodies" header + rows + const int rowH = std::max(m_tree->GetCharHeight() + 8, 20); + const int shown = std::min(std::max(rows, 1), 9); + const wxSize ts(-1, shown * rowH + 8); + m_tree->SetMinSize(ts); + m_tree->SetMaxSize(ts); + if (m_form && m_form->GetSizer()) { m_form->Layout(); m_form->FitInside(); } } int DesignPanel::tree_body_selection() const @@ -3143,8 +3158,37 @@ void DesignPanel::after_tree_edit(bool ok) m_status->Refresh(); } +// Erase the whole document (every feature + body) and start fresh. The single "wipe" the +// feature tree's per-row Delete can't give you — also the way out when a body has no +// removable owning feature. +void DesignPanel::on_new_design() +{ + if (m_doc.features.empty() && m_doc.bodies.empty()) { set_status_ok(); return; } + wxMessageDialog dlg(this, + _L("Erase all features and bodies and start a new design? This cannot be undone."), + _L("New Design"), wxYES_NO | wxICON_EXCLAMATION); + if (dlg.ShowModal() != wxID_YES) return; + tool_cancel(); // leave any active tool / sketch / constrain cleanly + m_doc.clear(); // features + bodies + meshes + history + m_edit_index = -1; + m_move_body = -1; + m_body_xform.clear(); + if (m_viewport) { m_viewport->clear_move_gizmo(); m_viewport->clear_mesh(); } + after_tree_edit(true); // rebuild the (now empty) tree + clear the viewport + update_action_bar(); + set_status_ok(); +} + void DesignPanel::on_delete_feature() { + // A Body row has no directly-removable feature (bodies are recomputed results); guide the + // user to delete the feature that created it, or use New Design to wipe everything. + if (tree_body_selection() >= 0) { + m_status->SetForegroundColour(wxColour(235, 110, 110)); + m_status->SetLabel(_L("Select the FEATURE that created this body (or use New Design)")); + m_status->Refresh(); + return; + } int sel = tree_selection(); if (sel == wxNOT_FOUND) { m_status->SetLabel(_L("Select a feature in the tree first")); diff --git a/src/slic3r/GUI/DesignPanel.hpp b/src/slic3r/GUI/DesignPanel.hpp index a54219b4f5..6508bb1ed7 100644 --- a/src/slic3r/GUI/DesignPanel.hpp +++ b/src/slic3r/GUI/DesignPanel.hpp @@ -111,6 +111,7 @@ private: // Feature-tree editing (Onshape-style): act on the selected tree row. void on_delete_feature(); + void on_new_design(); void on_move_feature(int delta); // -1 = up, +1 = down void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled) diff --git a/src/slic3r/GUI/DesignSketchTool.cpp b/src/slic3r/GUI/DesignSketchTool.cpp index 7004b0fc8b..5a80a26fce 100644 --- a/src/slic3r/GUI/DesignSketchTool.cpp +++ b/src/slic3r/GUI/DesignSketchTool.cpp @@ -5,6 +5,7 @@ #include "Plater.hpp" #include +#include #include "libslic3r/BuildVolume.hpp" #include "Camera.hpp" #include "3DScene.hpp" @@ -39,7 +40,9 @@ static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a // needs the design canvas's own camera/viewport, not the plater's.) [[maybe_unused]] static wxPoint world_to_screen_px(const Camera& cam, const Vec3d& world) { - const Eigen::Matrix4d m = (cam.get_projection_matrix() * cam.get_view_matrix()).matrix(); + // NB: multiply the raw 4x4 matrices, NOT the Transform3d objects — the projection is not + // affine, so Transform*Transform mangles it (Eigen assumes affine) and yields garbage w. + const Eigen::Matrix4d m = cam.get_projection_matrix().matrix() * cam.get_view_matrix().matrix(); const Eigen::Vector4d clip = m * world.homogeneous(); if (std::abs(clip.w()) < 1e-9) return wxPoint(-1, -1); const Vec3d ndc = clip.head<3>() / clip.w(); @@ -4913,15 +4916,21 @@ void DesignSketchTool::draw_dim_label(const std::string& txt, const Vec2d& plane const wxPoint sp = world_to_screen_px(cam, m_plane.to_world(plane_center)); if (sp.x < 0 && sp.y < 0) return; ImGuiWrapper* imgui = wxGetApp().imgui(); + // Identical to the Prepare/Preview Measure gizmo label (GLGizmoMeasure::render_dimensioning): + // push_common_window_style sets the white text colour + font/scale (without it the text is + // invisible); BringWindowToDisplayFront keeps the per-frame label window on top. + ImGuiWrapper::push_common_window_style(m_render_scale); + imgui->set_next_window_pos((float)sp.x, (float)sp.y, ImGuiCond_Always, 0.5f, 0.5f); + imgui->set_next_window_bg_alpha(0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f)); - imgui->set_next_window_pos((float)sp.x, (float)sp.y, ImGuiCond_Always, 0.5f, 0.5f); - imgui->set_next_window_bg_alpha(0.0f); const std::string win = "##sketchdim" + std::to_string(m_dim_label_seq++); imgui->begin(win, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + ImGui::AlignTextToFramePadding(); ImDrawList* dl = ImGui::GetWindowDrawList(); const ImVec2 pos = ImGui::GetCursorScreenPos(); const ImVec2 ts = ImGui::CalcTextSize(txt.c_str()); @@ -4934,6 +4943,7 @@ void DesignSketchTool::draw_dim_label(const std::string& txt, const Vec2d& plane imgui->text(txt); imgui->end(); ImGui::PopStyleVar(3); + ImGuiWrapper::pop_common_window_style(); } void DesignSketchTool::draw_text(GLModel& /*model*/, const std::string& s, const Vec2d& center, @@ -4969,15 +4979,18 @@ bool DesignSketchTool::draw_dim_quote(const DimAnnot& a, double th, const ColorR if (L < 1e-6) return false; const Vec2d u = d / L; const Vec2d nrm(-u.y(), u.x()); - segs.emplace_back(pa, pb); // single point-to-point dimension line + const double side = (a.side != 0.0) ? a.side : 1.0; + const double off = side * std::max(L * 0.18, 8.0); + const Vec2d A2 = pa + nrm * off, B2 = pb + nrm * off; // offset clear of the sketch line + segs.emplace_back(A2, B2); // dimension line (not on the geometry) const double as = std::max(L * 0.04, 2.0); auto arrow = [&](const Vec2d& tip, const Vec2d& dir) { const Vec2d back = tip + dir * as; segs.emplace_back(tip, back + nrm * (as * 0.5)); segs.emplace_back(tip, back - nrm * (as * 0.5)); }; - arrow(pa, u); arrow(pb, -u); - out_label = (pa + pb) * 0.5 + nrm * (th * 0.8); + arrow(A2, u); arrow(B2, -u); + out_label = (A2 + B2) * 0.5 + nrm * (side * (th * 0.7 + 1.5)); } else if (a.kind == DimType::Diameter || a.kind == DimType::Radius) { if (a.ea < 0 || a.ea >= int(m_entities.size())) return false; const SketchEntity& e = m_entities[a.ea]; @@ -5035,7 +5048,7 @@ bool DesignSketchTool::draw_dim_quote(const DimAnnot& a, double th, const ColorR } else { return false; } - draw_strokes(m_highlight_model, segs, 0.6, dimcol); + draw_strokes(m_highlight_model, segs, 0.2, dimcol); draw_text(m_line_model, dim_text(a), out_label, th, dimcol); return true; } @@ -6272,6 +6285,7 @@ void DesignSketchTool::confirm_transform() void DesignSketchTool::render(GLCanvas3D& canvas) { m_dim_label_seq = 0; + m_render_scale = canvas.get_scale(); (void)canvas; if (!has_display()) { if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD @@ -6283,6 +6297,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) return; } + // Draw-then-edit: a creation tool that just committed a new entity/feature (gesture now // idle) gets its result auto-selected — so render_live_quotes below computes its quotes — // and the primary value editor armed (opened after those quotes exist, see service block). diff --git a/src/slic3r/GUI/DesignSketchTool.hpp b/src/slic3r/GUI/DesignSketchTool.hpp index 963c3361be..58dd20ca57 100644 --- a/src/slic3r/GUI/DesignSketchTool.hpp +++ b/src/slic3r/GUI/DesignSketchTool.hpp @@ -819,6 +819,7 @@ private: GLModel m_vertex_model; GLModel m_highlight_model; int m_dim_label_seq{0}; + float m_render_scale{1.0f}; // canvas scale for Measure-style dim labels GLModel m_fill_model; // translucent face fill for closed regions std::vector m_display_sketches; // committed sketches drawn persistently int m_display_pick{-1}; // FEATURE index of the click-selected display sketch (-1 none)