From 1b5b8fce5420f1da3d9d057259cbff30bb950170 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 22 Aug 2026 13:15:09 +0800 Subject: [PATCH] Port mixed filament dialog fixes from BambuStudio --- src/slic3r/GUI/ColorDecomposeDialog.cpp | 24 +++- src/slic3r/GUI/GradientCurveEditor.cpp | 40 ++++-- src/slic3r/GUI/GradientCurveEditor.hpp | 5 + src/slic3r/GUI/MixedFilamentDialog.cpp | 154 ++++++++++++------------ src/slic3r/GUI/MixedFilamentDialog.hpp | 6 + 5 files changed, 137 insertions(+), 92 deletions(-) diff --git a/src/slic3r/GUI/ColorDecomposeDialog.cpp b/src/slic3r/GUI/ColorDecomposeDialog.cpp index 3c2fbe4e45..c52d4f4380 100644 --- a/src/slic3r/GUI/ColorDecomposeDialog.cpp +++ b/src/slic3r/GUI/ColorDecomposeDialog.cpp @@ -196,8 +196,10 @@ ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent, build_ui(); wxGetApp().UpdateDlgDarkUI(this); // Restore target swatch after dark mode color remapping - if (m_target_swatch) + if (m_target_swatch) { m_target_swatch->SetBackgroundColour(m_target_color); + m_target_swatch->Refresh(); + } update_card_visibility(); Fit(); @@ -322,6 +324,26 @@ static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size)); panel->SetBackgroundColour(color); panel->SetMinSize(wxSize(size, size)); + panel->SetBackgroundStyle(wxBG_STYLE_PAINT); + panel->Bind(wxEVT_PAINT, [panel](wxPaintEvent&) { + wxAutoBufferedPaintDC dc(panel); + wxSize sz = panel->GetClientSize(); + wxColour c = panel->GetBackgroundColour(); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(c)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + // Mirror sidebar (FilamentBitmapUtils::create_single_filament_bitmap): + // gray border for near-white in light mode so white swatches stay + // visible on a white background; light border for near-black in dark mode. + const bool light_mode = !wxGetApp().dark_mode(); + if ((light_mode && c.Red() > 224 && c.Green() > 224 && c.Blue() > 224) || + (!light_mode && c.Red() < 45 && c.Green() < 45 && c.Blue() < 45)) { + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(light_mode ? wxColour(130, 130, 128) : wxColour(207, 207, 207), + 1, wxPENSTYLE_SOLID)); + dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); + } + }); return panel; } diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp index b3d3436465..782961aba4 100644 --- a/src/slic3r/GUI/GradientCurveEditor.cpp +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -183,13 +183,17 @@ wxRect GradientCurveEditor::plot_rect() const return wxRect(x, y, side, side); } -wxPoint GradientCurveEditor::data_to_px(double x, double y) const +wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const { const wxRect r = plot_rect(); - const int px = r.x + static_cast(std::lround(x * r.width)); // y axis is inverted: y=1 should sit at the top. - const int py = r.y + static_cast(std::lround((1.0 - y) * r.height)); - return wxPoint(px, py); + return wxPoint2DDouble(r.x + x * r.width, r.y + (1.0 - y) * r.height); +} + +wxPoint GradientCurveEditor::data_to_px(double x, double y) const +{ + const wxPoint2DDouble p = data_to_px_f(x, y); + return wxPoint(static_cast(std::lround(p.m_x)), static_cast(std::lround(p.m_y))); } void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const @@ -321,6 +325,9 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) // Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered // DC is the actual back buffer that gets blitted to the window. wxGCDC dc(raw_dc); + // The curve and its anchors are drawn straight on the graphics context so their + // coordinates stay sub-pixel accurate (see data_to_px_f). + wxGraphicsContext* gc = dc.GetGraphicsContext(); const wxRect rc = plot_rect(); if (rc.width <= 0 || rc.height <= 0) @@ -416,7 +423,7 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) dc.SetTextForeground(label_muted); dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); - if (m_points.size() < 2) + if (m_points.size() < 2 || !gc) return; auto color_for_curve = [&](int curve_idx) -> wxColour { @@ -428,22 +435,26 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) return c; }; - auto build_polyline = [&](int curve_idx) -> std::vector { + auto build_polyline = [&](int curve_idx) -> std::vector { const int samples = std::max(128, rc.width * 2); - std::vector poly; + std::vector poly; poly.reserve(samples + 1); for (int s = 0; s <= samples; ++s) { const double x = double(s) / samples; const double y0 = sample_curve_y(x); const double vy = to_visual_y(curve_idx, y0); - poly.push_back(data_to_px(x, vy)); + poly.push_back(data_to_px_f(x, vy)); } return poly; }; - auto draw_polyline = [&](const std::vector& poly, const wxColour& col, int stroke_dip) { + // Only the geometry goes through the graphics context: dc.DrawLines() takes integer + // wxPoint and would quantize the curve back to whole pixels. The pen is still set on + // the dc, which forwards it to this same context while keeping the dc's own cached + // state in sync, so later dc drawing does not inherit the curve's pen. + auto draw_polyline = [&](const std::vector& poly, const wxColour& col, int stroke_dip) { dc.SetPen(wxPen(col, FromDIP(stroke_dip))); - dc.DrawLines(static_cast(poly.size()), poly.data()); + gc->StrokeLines(poly.size(), poly.data()); }; // Outline only when the curve color is perceptually close to the background; otherwise @@ -472,13 +483,16 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) draw_one(m_selected_curve, kStrokeSelected); // Control points (selected curve only): hollow circle with axis-color border, theme-aware fill. - const int r = FromDIP(kPointRadius); + // Drawn on the graphics context with a sub-pixel center so the ring stays centered on the + // curve instead of drifting up to half a pixel off it; pen and brush go through the dc for + // the same reason as in draw_polyline above. + const double r = FromDIP(kPointRadius); dc.SetPen(wxPen(axis_color, 1)); dc.SetBrush(wxBrush(point_fill)); for (size_t i = 0; i < m_points.size(); ++i) { const double vy = to_visual_y(m_selected_curve, m_points[i].y); - const wxPoint p = data_to_px(m_points[i].x, vy); - dc.DrawCircle(p.x, p.y, r); + const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy); + gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); } } diff --git a/src/slic3r/GUI/GradientCurveEditor.hpp b/src/slic3r/GUI/GradientCurveEditor.hpp index 8412db3df2..f9858cab11 100644 --- a/src/slic3r/GUI/GradientCurveEditor.hpp +++ b/src/slic3r/GUI/GradientCurveEditor.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "libslic3r/FilamentMixer.hpp" @@ -74,6 +75,10 @@ private: // Coordinate mapping between data (x, y in [0,1]) and pixels in plot area. wxRect plot_rect() const; + // Sub-pixel accurate mapping, used for drawing: rounding the curve vertices to whole + // pixels leaves a staircase that anti-aliasing cannot smooth out, and the step is + // twice as coarse on 2x (Retina) displays. + wxPoint2DDouble data_to_px_f(double x, double y) const; wxPoint data_to_px(double x, double y) const; void px_to_data(int px, int py, double& x, double& y) const; // Anchor hit test for the currently-selected curve (uses translated visual y). diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index 241f051905..84eb9ca0b9 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -258,6 +258,46 @@ wxBitmap MixedFilamentDialog::make_swatch_bitmap(size_t idx) }); } +void MixedFilamentDialog::apply_uniform_label_width(wxStaticText* lbl) +{ + // A material row places the combo right after the label, so the combo x follows the label + // width and the rows drift apart with fonts that render digits at different advances (which + // is what macOS does). Reserve the width of the widest row label on every row instead. + // The label itself is used as the measuring device on purpose: SetMinSize overrides the + // control's own best size rather than being merged with it, and on macOS the native cell is + // wider than the plain text extent, so a wxDC-measured width would clip the text. + const wxString text = lbl->GetLabel(); + int w = 0; + for (int i = 1; i <= MAX_COMPONENTS; ++i) { + lbl->SetLabel(wxString::Format(_L("Filament %d"), i)); + lbl->InvalidateBestSize(); + w = std::max(w, lbl->GetBestSize().x); + } + lbl->SetLabel(text); + lbl->InvalidateBestSize(); + lbl->SetMinSize(wxSize(w, -1)); +} + +void MixedFilamentDialog::append_material_row() +{ + auto* row = new wxBoxSizer(wxHORIZONTAL); + auto* lbl = new wxStaticText(this, wxID_ANY, + wxString::Format(_L("Filament %d"), (int)(m_combo_filaments.size() + 1))); + lbl->SetFont(::Label::Body_12); + apply_uniform_label_width(lbl); + row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + + auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); + combo->SetKeepDropArrow(true); + combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); + row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); + + m_combo_filaments.push_back(combo); + m_combo_to_physical.push_back({}); + m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); +} + void MixedFilamentDialog::reset_manual_ratio_state() { m_ratio_manual_order.clear(); @@ -444,13 +484,18 @@ void MixedFilamentDialog::start_ratio_editor(size_t idx, wxWindow* anchor, const m_ratio_editor->SetBackgroundColour(bg); m_ratio_editor->SetForegroundColour(fg); // Default wxTextCtrl best width (~140px) is too wide for the sizer to - // shrink, which would push the "%" suffix out of the panel. Cap the - // editor's min width to the digits only (ratios are always two digits). + // shrink, which would push the "%" suffix out of the panel. Size the + // editor for the *widest* three digits rather than the largest accepted + // value: SetMaxLength above lets anything up to "888" be typed, and the + // macOS system font renders digits at different advances, so "100" is + // narrower than what the user can actually enter. GetSizeFromTextSize() + // then adds the platform's own text field margins; on macOS those margins + // are what clipped the digits. { wxClientDC mdc(m_ratio_editor); mdc.SetFont(::Label::Body_10); - int digits_w = mdc.GetTextExtent(wxT("88")).GetWidth(); - m_ratio_editor->SetMinSize(wxSize(digits_w + FromDIP(2), -1)); + int digits_w = mdc.GetTextExtent(wxT("888")).GetWidth(); + m_ratio_editor->SetMinSize(m_ratio_editor->GetSizeFromTextSize(digits_w)); } auto* pct_label = new wxStaticText(m_ratio_editor_panel, wxID_ANY, wxT("%")); @@ -494,11 +539,21 @@ void MixedFilamentDialog::start_ratio_editor(size_t idx, wxWindow* anchor, const wxPoint pos = anchor->GetPosition() + anchor_rect.GetTopLeft(); // Match the editor to the label (hover box) size so the inline editor and - // the hover state look identical. A small floor keeps the "%" suffix from - // being squeezed out on very narrow labels. + // the hover state look identical, but never go below what the digits and + // the "%" suffix need: the sizer takes any missing width out of the + // stretchable editor, which would clip the value. + wxSize needed = m_ratio_editor_panel->ClientToWindowSize( + m_ratio_editor_panel->GetSizer()->CalcMin()); wxSize size = anchor->GetSize(); - size.SetWidth(std::max(size.GetWidth(), FromDIP(30))); - size.SetHeight(std::max(size.GetHeight(), FromDIP(18))); + size.SetWidth(std::max(size.GetWidth(), needed.GetWidth())); + size.SetHeight(std::max(size.GetHeight(), needed.GetHeight())); + // An editor wider than the label must still stay inside its parent, or the + // corner labels of the triangle picker would have it clipped at the edge. + if (wxWindow* editor_parent = m_ratio_editor_panel->GetParent()) { + wxSize avail = editor_parent->GetClientSize(); + pos.x = std::clamp(pos.x, 0, std::max(0, avail.GetWidth() - size.GetWidth())); + pos.y = std::clamp(pos.y, 0, std::max(0, avail.GetHeight() - size.GetHeight())); + } m_ratio_editor_panel->SetSize(wxRect(pos, size)); m_ratio_editor_panel->Layout(); m_ratio_editor->SetValue(wxString::Format(wxT("%d"), ratio(idx))); @@ -769,23 +824,8 @@ wxBoxSizer* MixedFilamentDialog::create_material_selection() m_combo_filaments.clear(); m_combo_to_physical.clear(); - for (size_t i = 0; i < m_result.components.size(); ++i) { - auto* row = new wxBoxSizer(wxHORIZONTAL); - wxString lbl_text = wxString::Format(_L("Filament %d"), (int)(i + 1)); - auto* lbl = new wxStaticText(this, wxID_ANY, lbl_text); - lbl->SetFont(::Label::Body_12); - row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - - auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); - combo->SetKeepDropArrow(true); - combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); - row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); - - m_combo_filaments.push_back(combo); - m_combo_to_physical.push_back({}); - m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); - } + for (size_t i = 0; i < m_result.components.size(); ++i) + append_material_row(); sizer->Add(m_material_rows_sizer, 0, wxEXPAND); @@ -1455,26 +1495,6 @@ void MixedFilamentDialog::on_ratio_changed(int new_ratio_a) void MixedFilamentDialog::on_gradient_toggled() { - bool checked = m_chk_gradient->GetValue(); - - if (checked) { - auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - if (!print_config.opt_bool("enable_mixed_color_sublayer")) { - wxMessageDialog dlg(this, - _L("Gradient effect requires 'Mixed color sublayer' to be enabled. Enable it now?"), - _L("Mixed Color Sublayer"), - wxYES_NO | wxICON_QUESTION); - if (dlg.ShowModal() == wxID_YES) { - DynamicPrintConfig new_conf; - new_conf.set_key_value("enable_mixed_color_sublayer", new ConfigOptionBool(true)); - wxGetApp().get_tab(Preset::TYPE_PRINT)->load_config(new_conf); - } else { - m_chk_gradient->SetValue(false); - return; - } - } - } - m_result.gradient_enabled = m_chk_gradient->GetValue(); if (m_ratio_sizer) @@ -1572,21 +1592,7 @@ void MixedFilamentDialog::on_add_material() } reset_manual_ratio_state(); - auto* row = new wxBoxSizer(wxHORIZONTAL); - wxString lbl_text = wxString::Format(_L("Filament %d"), (int)(n + 1)); - auto* lbl = new wxStaticText(this, wxID_ANY, lbl_text); - lbl->SetFont(::Label::Body_12); - row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - - auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); - combo->SetKeepDropArrow(true); - combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); - row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); - - m_combo_filaments.push_back(combo); - m_combo_to_physical.push_back({}); - m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); + append_material_row(); rebuild_all_combos(); refresh_curve_editor_colors(); @@ -1670,24 +1676,8 @@ void MixedFilamentDialog::on_recommendation_clicked_triple(unsigned int a, unsig // Ensure we have exactly 3 combo rows if (num_components() < 3) { // Need to add a 3rd combo row - while (m_combo_filaments.size() < 3) { - size_t idx = m_combo_filaments.size(); - auto* row = new wxBoxSizer(wxHORIZONTAL); - wxString lbl_text = wxString::Format(_L("Filament %d"), (int)(idx + 1)); - auto* lbl = new wxStaticText(this, wxID_ANY, lbl_text); - lbl->SetFont(::Label::Body_12); - row->Add(lbl, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); - - auto* combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize(FromDIP(166), FromDIP(24)), 0, nullptr, wxCB_READONLY); - combo->SetKeepDropArrow(true); - combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_filament_changed(); }); - row->Add(combo, 1, wxALIGN_CENTER_VERTICAL); - - m_combo_filaments.push_back(combo); - m_combo_to_physical.push_back({}); - m_material_rows_sizer->Add(row, 0, wxEXPAND | wxTOP, FromDIP(9)); - } + while (m_combo_filaments.size() < 3) + append_material_row(); } else if (num_components() > 3) { while (m_material_rows_sizer->GetItemCount() > 3) { auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); @@ -1813,7 +1803,11 @@ void MixedFilamentDialog::update_ok_button_state() parts += wxString::Format(_L("Slot %s (%s)"), slots, wxString::FromUTF8(it->first)); } m_type_mismatch_msg = parts + " " + _L("cannot be mixed. Please select the same filament type."); + } else { + m_type_mismatch_msg.clear(); } + } else { + m_type_mismatch_msg.clear(); } bool has_unselected = false; @@ -1839,6 +1833,10 @@ void MixedFilamentDialog::update_ok_button_state() if (m_warning_panel) { m_warning_panel->Show(has_type_mismatch); + // Force a repaint: when the panel is already visible and only the + // mismatch text changes (e.g. PETG -> ABS), Show()/Layout() do not + // generate a paint event, so paint_warning_panel keeps the stale text. + m_warning_panel->Refresh(); Layout(); } } diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp index a1deaa2022..a1af146897 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.hpp +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -100,6 +100,12 @@ private: wxBitmap make_swatch_bitmap(size_t idx); + // Reserves the same width on every material row label so the combo boxes line up. + static void apply_uniform_label_width(wxStaticText* lbl); + // Appends one "Filament N" label + combo row to m_material_rows_sizer. N follows the + // number of rows already there, so callers must not renumber anything themselves. + void append_material_row(); + // Helpers for component/ratio access size_t num_components() const { return m_result.components.size(); } unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; }