#include "MixedFilamentDialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "libslic3r/Utils.hpp" #include "libslic3r/FilamentMixer.hpp" #include "I18N.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "GradientCurveEditor.hpp" #include "FilamentBitmapUtils.hpp" #include "wxExtensions.hpp" #include "Widgets/Button.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/ComboBox.hpp" #include "Widgets/DropDown.hpp" #include "Widgets/Label.hpp" namespace Slic3r { namespace GUI { static constexpr int MAX_COMPONENTS = 3; static constexpr int MIN_COMPONENT_RATIO = 10; // Section headings and the placeholder text share one muted tone; light key, resolved at each use. static const wxColour COLOR_LABEL_MUTED("#6B6A6A"); // Lightweight self-painting label used for both dual-color and triple-color // ratio percentage display. Hover shows a rounded-rect background; click // fires wxEVT_LEFT_DOWN which the owning dialog binds to start_ratio_editor. class RatioLabelPanel : public wxPanel { public: RatioLabelPanel(wxWindow* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) { SetBackgroundStyle(wxBG_STYLE_PAINT); SetCursor(wxCursor(wxCURSOR_HAND)); SetToolTip(_L("Click to edit ratio")); SetFont(::Label::Body_10); Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { m_hovered = true; Refresh(); e.Skip(); }); Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& e) { m_hovered = false; Refresh(); e.Skip(); }); Bind(wxEVT_PAINT, &RatioLabelPanel::on_paint, this); } void SetLabel(const wxString& text) override { if (m_text == text) return; m_text = text; update_best_size(); Refresh(); } wxString GetLabel() const override { return m_text; } private: void update_best_size() { wxClientDC dc(this); dc.SetFont(GetFont()); wxSize ts = dc.GetTextExtent(m_text); int pad_x = FromDIP(4), pad_y = FromDIP(3); SetMinSize(wxSize(ts.GetWidth() + pad_x * 2, ts.GetHeight() + pad_y * 2)); InvalidateBestSize(); } void on_paint(wxPaintEvent&) { wxBufferedPaintDC dc(this); wxSize sz = GetClientSize(); wxColour parent_bg = GetParent() ? GetParent()->GetBackgroundColour() : StateColor::darkModeColorFor(*wxWHITE); dc.SetBrush(wxBrush(parent_bg)); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); if (m_hovered) { dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(3)); } dc.SetFont(GetFont()); dc.SetTextForeground(m_hovered ? StateColor::darkModeColorFor(wxColour("#009688")) : StateColor::darkModeColorFor(wxColour("#262E30"))); wxSize ts = dc.GetTextExtent(m_text); int x = (sz.GetWidth() - ts.GetWidth()) / 2; int y = (sz.GetHeight() - ts.GetHeight()) / 2; dc.DrawText(m_text, x, y); } wxString m_text; bool m_hovered{false}; }; static wxColour blend_colors(const wxColour& a, const wxColour& b, double ratio_a) { unsigned char r, g, bl; Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(), b.Red(), b.Green(), b.Blue(), static_cast(1.0 - ratio_a), &r, &g, &bl); return wxColour(r, g, bl); } // ---- Constructors ---- MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, const std::vector& physical_colors, const std::vector& physical_names, const std::vector& physical_types) : DPIDialog(parent, wxID_ANY, _L("Add Mixed Filament"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) , m_edit_mode(false) , m_physical_colors(physical_colors) , m_physical_names(physical_names) , m_physical_types(physical_types) { m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; m_result.ratios = {50, 50}; build_ui(); wxGetApp().UpdateDlgDarkUI(this); } MixedFilamentDialog::~MixedFilamentDialog() { // Backstop: a child must never be destroyed while it still holds the mouse // capture. wxWidgets only asserts about this (compiled out in release), and // the macOS port never unwinds its capture stack, so the stale entry would // make wxNSWindow::sendEvent swallow every mouse event in the application. if (m_ratio_bar && m_ratio_bar->HasCapture()) m_ratio_bar->ReleaseMouse(); if (m_triangle_panel && m_triangle_panel->HasCapture()) m_triangle_panel->ReleaseMouse(); } MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent, const MixedFilamentResult& existing, const std::vector& physical_colors, const std::vector& physical_names, const std::vector& physical_types) : DPIDialog(parent, wxID_ANY, _L("Edit Mixed Filament"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) , m_result(existing) , m_edit_mode(true) , m_physical_colors(physical_colors) , m_physical_names(physical_names) , m_physical_types(physical_types) { if (m_result.components.size() < 2) { m_result.components = {1, (physical_colors.size() >= 2) ? 2u : 1u}; m_result.ratios = {50, 50}; } if (m_result.ratios.size() >= 3) { int sum = 0; for (int r : m_result.ratios) sum += r; if (sum > 0) { m_tri_wx = (double)m_result.ratios[0] / sum; m_tri_wy = (double)m_result.ratios[1] / sum; m_tri_wz = (double)m_result.ratios[2] / sum; } } build_ui(); wxGetApp().UpdateDlgDarkUI(this); } void MixedFilamentDialog::on_dpi_changed(const wxRect&) { int h = (num_components() >= 3) ? FromDIP(680) : FromDIP(580); SetSize(FromDIP(439), h); Refresh(); } wxColour MixedFilamentDialog::comp_colour(size_t i) const { unsigned int c = comp(i); if (c >= 1 && c <= m_physical_colors.size()) return wxColour(m_physical_colors[c - 1]); return wxColour("#D9D9D9"); } static wxBitmap make_alpha_bitmap(int w, int h, const std::function& draw_fn) { wxBitmap bmp(w, h); wxMemoryDC memdc; #ifdef __WXOSX__ bmp.UseAlpha(); memdc.SelectObject(bmp); #else { wxImage img(w, h); img.InitAlpha(); memset(img.GetAlpha(), 0, w * h); bmp = wxBitmap(std::move(img)); } memdc.SelectObject(bmp); #endif { #ifdef __WXMSW__ wxGCDC dc(memdc); #else wxDC& dc = memdc; #endif draw_fn(dc); } memdc.SelectObject(wxNullBitmap); return bmp; } wxBitmap MixedFilamentDialog::make_swatch_bitmap(size_t idx) { int swatch_sz = FromDIP(20); int pad_left = FromDIP(2); int pad_right = FromDIP(6); int bmp_w = pad_left + swatch_sz + pad_right; int bmp_h = swatch_sz; // Reuse the sidebar clr_picker swatch (get_extruder_color_icon) so the // checkerboard (transparent.svg tiling), border and label style match the // sidebar exactly, instead of a self-drawn rounded rect / programmatic grid. std::string color_hex = "#D9D9D9"; if (idx < m_physical_colors.size()) color_hex = m_physical_colors[idx]; std::string label = std::to_string(idx + 1); wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { if (icon && icon->IsOk()) dc.DrawBitmap(*icon, pad_left, 0); }); } 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(); if (m_ratio_editor_panel) m_ratio_editor_panel->Hide(); // Restore any label hidden by an in-flight editor so it can never be left // permanently invisible if the editor is dismissed without a commit. if (m_ratio_editor_anchor) { m_ratio_editor_anchor->Show(); m_ratio_editor_anchor = nullptr; } } void MixedFilamentDialog::refresh_ratio_labels() { if (m_label_ratio_a) m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); if (m_label_ratio_b) m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); if (m_ratio_sizer) m_ratio_sizer->Layout(); if (m_triangle_panel) m_triangle_panel->Refresh(); } void MixedFilamentDialog::sync_triangle_weights_from_ratios() { if (m_result.ratios.size() < 3) return; int sum = 0; for (int r : m_result.ratios) sum += r; if (sum <= 0) return; m_tri_wx = (double)m_result.ratios[0] / sum; m_tri_wy = (double)m_result.ratios[1] / sum; m_tri_wz = (double)m_result.ratios[2] / sum; } void MixedFilamentDialog::apply_manual_ratio(size_t idx, int value) { const size_t n = num_components(); if (idx >= n) return; if (m_result.ratios.size() != n) m_result.ratios.assign(n, n > 0 ? 100 / (int)n : 0); bool manual_stale = false; for (size_t o : m_ratio_manual_order) { if (o >= n) { manual_stale = true; break; } } if (manual_stale) reset_manual_ratio_state(); int max_value = (int)(100 - (n - 1) * MIN_COMPONENT_RATIO); value = std::clamp(value, MIN_COMPONENT_RATIO, std::max(MIN_COMPONENT_RATIO, max_value)); if (n == 2) { if (idx == 0) { m_result.ratios[0] = value; m_result.ratios[1] = 100 - value; } else { m_result.ratios[1] = value; m_result.ratios[0] = 100 - value; } m_result.ratios[0] = std::clamp(m_result.ratios[0], MIN_COMPONENT_RATIO, 100 - MIN_COMPONENT_RATIO); m_result.ratios[1] = 100 - m_result.ratios[0]; } else if (n >= 3) { m_result.ratios[idx] = value; int remaining = 100 - value; std::vector others; int others_sum = 0; for (size_t i = 0; i < n; ++i) { if (i == idx) continue; others.push_back(i); others_sum += m_result.ratios[i]; } if (!others.empty()) { if (others_sum > 0) { int assigned = 0; for (size_t k = 0; k < others.size(); ++k) { int nv = (int)((double)remaining * m_result.ratios[others[k]] / others_sum + 0.5); nv = std::max(nv, MIN_COMPONENT_RATIO); m_result.ratios[others[k]] = nv; assigned += nv; } while (assigned != remaining) { if (assigned > remaining) { int pick = -1; for (size_t k = 0; k < others.size(); ++k) if (m_result.ratios[others[k]] > MIN_COMPONENT_RATIO && (pick < 0 || m_result.ratios[others[k]] > m_result.ratios[others[pick]])) pick = (int)k; if (pick < 0) break; --m_result.ratios[others[pick]]; --assigned; } else { int pick = 0; for (size_t k = 1; k < others.size(); ++k) if (m_result.ratios[others[k]] > m_result.ratios[others[pick]]) pick = (int)k; ++m_result.ratios[others[pick]]; ++assigned; } } } else { int base = remaining / (int)others.size(); for (size_t k = 0; k < others.size(); ++k) m_result.ratios[others[k]] = base; m_result.ratios[others.back()] += remaining - base * (int)others.size(); } } } refresh_ratio_labels(); sync_triangle_weights_from_ratios(); update_preview(); } void MixedFilamentDialog::apply_dragged_triangle_ratio(int r0, int r1, int r2) { if (m_result.ratios.size() < 3) return; int ratios[3] = { std::clamp(r0, MIN_COMPONENT_RATIO, 100), std::clamp(r1, MIN_COMPONENT_RATIO, 100), std::clamp(r2, MIN_COMPONENT_RATIO, 100) }; int sum = ratios[0] + ratios[1] + ratios[2]; while (sum > 100) { int idx = 0; for (int i = 1; i < 3; ++i) { if (ratios[i] > ratios[idx]) idx = i; } if (ratios[idx] <= MIN_COMPONENT_RATIO) break; --ratios[idx]; --sum; } while (sum < 100) { int idx = 0; for (int i = 1; i < 3; ++i) { if (ratios[i] < ratios[idx]) idx = i; } ++ratios[idx]; ++sum; } m_result.ratios[0] = ratios[0]; m_result.ratios[1] = ratios[1]; m_result.ratios[2] = ratios[2]; sync_triangle_weights_from_ratios(); reset_manual_ratio_state(); update_preview(); } void MixedFilamentDialog::start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect) { if (!anchor || idx >= m_result.ratios.size()) return; if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) commit_ratio_editor(true); if (!m_ratio_editor_panel) { wxColour bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); wxColour fg = StateColor::darkModeColorFor(wxColour("#262E30")); m_ratio_editor_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE); m_ratio_editor_panel->SetBackgroundColour(bg); auto* hsizer = new wxBoxSizer(wxHORIZONTAL); m_ratio_editor = new wxTextCtrl(m_ratio_editor_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_NONE); m_ratio_editor->SetFont(::Label::Body_10); m_ratio_editor->SetMaxLength(3); 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. 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("888")).GetWidth(); m_ratio_editor->SetMinSize(m_ratio_editor->GetSizeFromTextSize(digits_w)); } auto* pct_label = new wxStaticText(m_ratio_editor_panel, wxID_ANY, wxT("%")); pct_label->SetFont(::Label::Body_10); pct_label->SetForegroundColour(fg); pct_label->SetBackgroundColour(bg); pct_label->SetMinSize(pct_label->GetBestSize()); hsizer->Add(m_ratio_editor, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); hsizer->Add(pct_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); m_ratio_editor_panel->SetSizer(hsizer); m_ratio_editor_panel->Hide(); m_ratio_editor->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { commit_ratio_editor(true); }); m_ratio_editor->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent& e) { commit_ratio_editor(true); e.Skip(); }); m_ratio_editor->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { if (e.GetKeyCode() == WXK_ESCAPE) commit_ratio_editor(false); else e.Skip(); }); } m_ratio_editor_idx = idx; // Keep the editor in the same window hierarchy as the clicked label so the // z-order is reliable and the editor fully covers the anchor (dual-color // labels live on the dialog, triple-color labels live on the triangle // panel). wxWindow* target_parent = anchor->GetParent(); if (target_parent && m_ratio_editor_panel->GetParent() != target_parent) m_ratio_editor_panel->Reparent(target_parent); // Hide the label being edited to avoid its (hover-state) text leaking out // next to the editor; restored on commit. m_ratio_editor_anchor = anchor; anchor->Hide(); 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, 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(), 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))); m_ratio_editor_panel->Show(); m_ratio_editor_panel->Raise(); m_ratio_editor->SetFocus(); m_ratio_editor->SelectAll(); m_ratio_editor_panel->Refresh(); Update(); } void MixedFilamentDialog::commit_ratio_editor(bool apply) { if (!m_ratio_editor_panel || !m_ratio_editor_panel->IsShown() || m_ratio_editor_committing) return; m_ratio_editor_committing = true; // Restore the hidden anchor before applying the ratio, so any sizer layout // triggered by refresh_ratio_labels() accounts for the visible label. m_ratio_editor_panel->Hide(); if (m_ratio_editor_anchor) { m_ratio_editor_anchor->Show(); m_ratio_editor_anchor = nullptr; } if (apply) { wxString value = m_ratio_editor->GetValue(); value.Trim(true); value.Trim(false); if (value.EndsWith(wxT("%"))) value.RemoveLast(); long parsed = 0; if (value.ToLong(&parsed)) apply_manual_ratio(m_ratio_editor_idx, (int)parsed); else refresh_ratio_labels(); } m_ratio_editor_committing = false; } void MixedFilamentDialog::commit_ratio_editor_from_background(wxMouseEvent& e) { if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) { wxPoint mouse_in_panel = m_ratio_editor_panel->ScreenToClient(wxGetMousePosition()); if (!m_ratio_editor_panel->GetClientRect().Contains(mouse_in_panel)) commit_ratio_editor(true); } e.Skip(); } // ---- UI Construction ---- void MixedFilamentDialog::build_ui() { SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); Bind(wxEVT_LEFT_DOWN, &MixedFilamentDialog::commit_ratio_editor_from_background, this); SetSize(FromDIP(439), FromDIP(580)); auto* main_sizer = new wxBoxSizer(wxVERTICAL); auto* top_sizer = new wxBoxSizer(wxHORIZONTAL); top_sizer->Add(create_preview_panel(), 0, wxALL, FromDIP(20)); m_right_sizer = new wxBoxSizer(wxVERTICAL); m_right_sizer->Add(create_material_selection(), 0, wxEXPAND); m_right_sizer->Add(create_gradient_section(), 0, wxEXPAND | wxTOP, FromDIP(7)); m_ratio_sizer = create_ratio_slider(); m_right_sizer->Add(m_ratio_sizer, 0, wxEXPAND | wxTOP, FromDIP(7)); m_triangle_sizer = create_triangle_picker(); m_right_sizer->Add(m_triangle_sizer, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(7)); top_sizer->Add(m_right_sizer, 1, wxTOP | wxRIGHT | wxBOTTOM, FromDIP(20)); main_sizer->Add(top_sizer, 0, wxEXPAND); main_sizer->Add(create_recommendation_grid(), 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(25)); // Warning panel: red bordered box with exclamation icon + text m_warning_sizer = new wxBoxSizer(wxVERTICAL); m_warning_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(48))); m_warning_panel->SetMinSize(wxSize(-1, FromDIP(48))); m_warning_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); m_warning_panel->Bind(wxEVT_PAINT, &MixedFilamentDialog::paint_warning_panel, this); m_warning_sizer->Add(m_warning_panel, 0, wxEXPAND); main_sizer->Add(m_warning_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(25)); m_warning_panel->Hide(); main_sizer->Add(create_button_panel(), 0, wxALIGN_RIGHT | wxALL, FromDIP(20)); SetSizer(main_sizer); rebuild_all_combos(); update_component_count_ui(); update_preview(); update_ok_button_state(); Layout(); CentreOnParent(); } wxBoxSizer* MixedFilamentDialog::create_preview_panel() { auto* sizer = new wxBoxSizer(wxVERTICAL); m_preview_canvas = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(129), FromDIP(129))); m_preview_canvas->SetBackgroundStyle(wxBG_STYLE_PAINT); m_preview_canvas->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { wxBufferedPaintDC dc(m_preview_canvas); wxSize sz = m_preview_canvas->GetClientSize(); size_t n = num_components(); dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); if (n == 0) return; int swatch_sz = FromDIP(80); int x0 = (sz.GetWidth() - swatch_sz) / 2; int y0 = (sz.GetHeight() - swatch_sz) / 2; double radius = FromDIP(6); if (m_result.gradient_enabled && n == 2) { Slic3r::GradientCurve curve; if (!m_result.gradient_curve.empty()) { curve.points = m_result.gradient_curve; } else { double yStart = (m_result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; double yEnd = (m_result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; curve.points = {{0.0, yStart, NAN, NAN}, {1.0, yEnd, NAN, NAN}}; } // Same sampler the sidebar, extruder icons and paint gizmo swatches use, so this // preview and every swatch drawn for the filament agree on what it looks like. auto ramp = sample_gradient_ramp(comp_colour(0), comp_colour(1), curve, std::max(80, swatch_sz)); fill_gradient_ramp_rect(dc, wxRect(x0, y0, swatch_sz, swatch_sz), ramp); // Mask corners: overdraw a thick background-colored rounded rect frame // so the inner edge forms the desired rounded corners. // Known limitation: this assumes the panel background equals // darkModeColorFor(white). wxGraphicsContext::Clip(path) is not // available in our wxWidgets build (only Clip(wxRegion) exists). int r = static_cast(radius); wxColour bg = StateColor::darkModeColorFor(*wxWHITE); dc.SetBrush(*wxTRANSPARENT_BRUSH); dc.SetPen(wxPen(bg, r * 2)); dc.DrawRoundedRectangle(x0 - r, y0 - r, swatch_sz + r * 2, swatch_sz + r * 2, radius * 2); } else { std::vector cols; std::vector weights; for (size_t i = 0; i < n; ++i) { cols.push_back(comp_colour(i)); weights.push_back(ratio(i) / 100.0); } wxColour mixed = blend_n_colors(cols, weights); dc.SetBrush(wxBrush(mixed)); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRoundedRectangle(x0, y0, swatch_sz, swatch_sz, radius); } }); sizer->Add(m_preview_canvas, 0, wxALIGN_CENTER); auto* label = new wxStaticText(this, wxID_ANY, _L("Effect Preview")); label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); label->SetFont(::Label::Body_13); sizer->Add(label, 0, wxALIGN_CENTER | wxTOP, FromDIP(4)); return sizer; } wxBoxSizer* MixedFilamentDialog::create_material_selection() { auto* sizer = new wxBoxSizer(wxVERTICAL); // Summary panel — draws N components dynamically m_summary_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(234), FromDIP(40))); m_summary_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); m_summary_panel->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { wxBufferedPaintDC dc(m_summary_panel); wxSize sz = m_summary_panel->GetClientSize(); wxColour sum_bg = StateColor::darkModeColorFor(wxColour("#F8F8F8")); wxColour sum_text = StateColor::darkModeColorFor(wxColour("#262E30")); dc.SetBrush(wxBrush(sum_bg)); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); int swatch_sz = FromDIP(20); int y_center = (sz.GetHeight() - swatch_sz) / 2; int x = FromDIP(13); dc.SetFont(::Label::Body_13); auto draw_summary_swatch = [&](size_t comp_idx) { unsigned int c = comp(comp_idx); std::string color_hex = "#D9D9D9"; if (c >= 1 && c <= m_physical_colors.size()) color_hex = m_physical_colors[c - 1]; std::string label = std::to_string(c); wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); if (icon && icon->IsOk()) dc.DrawBitmap(*icon, x, y_center); x += swatch_sz + FromDIP(4); }; if (m_result.gradient_enabled && num_components() == 2) { size_t idx_a = (m_result.gradient_direction == 0) ? 0 : 1; size_t idx_b = 1 - idx_a; draw_summary_swatch(idx_a); dc.SetTextForeground(sum_text); wxString arrow = wxT("\u2192"); wxSize arrow_sz = dc.GetTextExtent(arrow); dc.DrawText(arrow, x, y_center + (swatch_sz - arrow_sz.GetHeight()) / 2); x += arrow_sz.GetWidth() + FromDIP(4); draw_summary_swatch(idx_b); } else { for (size_t i = 0; i < num_components(); ++i) { if (i > 0) { dc.SetTextForeground(sum_text); wxString plus = wxT("+"); wxSize plus_sz = dc.GetTextExtent(plus); dc.DrawText(plus, x, y_center + (swatch_sz - plus_sz.GetHeight()) / 2); x += plus_sz.GetWidth() + FromDIP(4); } draw_summary_swatch(i); dc.SetTextForeground(sum_text); wxString pct = wxString::Format(wxT("%d%%"), ratio(i)); wxSize pct_sz = dc.GetTextExtent(pct); dc.DrawText(pct, x, y_center + (swatch_sz - pct_sz.GetHeight()) / 2); x += pct_sz.GetWidth() + FromDIP(4); } } }); sizer->Add(m_summary_panel, 0, wxEXPAND); auto* sel_label = new wxStaticText(this, wxID_ANY, _L("Select Mixed Materials")); sel_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); sel_label->SetFont(::Label::Body_12); sizer->Add(sel_label, 0, wxTOP, FromDIP(6)); m_material_rows_sizer = new wxBoxSizer(wxVERTICAL); m_combo_filaments.clear(); m_combo_to_physical.clear(); for (size_t i = 0; i < m_result.components.size(); ++i) append_material_row(); sizer->Add(m_material_rows_sizer, 0, wxEXPAND); auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); m_btn_add_material = new Button(this, _L("+ Add Material")); m_btn_add_material->SetBackgroundColor(wxColour("#F8F8F8")); m_btn_add_material->SetBorderColor(wxColour("#EEEEEE")); // The disabled tone rides on the StateColor so Enable() alone repaints it, the way m_btn_ok does. m_btn_add_material->SetTextColor(StateColor( std::make_pair(wxColour("#ACACAC"), (int) StateColor::Disabled), std::make_pair(wxColour("#262E30"), (int) StateColor::Normal))); m_btn_add_material->SetMinSize(wxSize(-1, FromDIP(24))); m_btn_add_material->SetCursor(wxCursor(wxCURSOR_HAND)); m_btn_add_material->EnableTooltipEvenDisabled(); m_btn_add_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_material(); }); btn_sizer->Add(m_btn_add_material, 1, wxRIGHT, FromDIP(6)); m_btn_remove_material = new Button(this, _L("- Delete Material")); m_btn_remove_material->SetBackgroundColor(wxColour("#F8F8F8")); m_btn_remove_material->SetBorderColor(wxColour("#EEEEEE")); m_btn_remove_material->SetTextColor(wxColour("#262E30")); m_btn_remove_material->SetMinSize(wxSize(-1, FromDIP(24))); m_btn_remove_material->SetCursor(wxCursor(wxCURSOR_HAND)); m_btn_remove_material->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_remove_material(); }); m_btn_remove_material->Hide(); btn_sizer->Add(m_btn_remove_material, 1, 0, 0); sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(9)); return sizer; } wxBoxSizer* MixedFilamentDialog::create_ratio_slider() { auto* sizer = new wxBoxSizer(wxVERTICAL); auto* ratio_label = new wxStaticText(this, wxID_ANY, _L("Ratio")); ratio_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); ratio_label->SetFont(::Label::Body_12); sizer->Add(ratio_label, 0, wxBOTTOM, FromDIP(4)); m_ratio_bar = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(27))); m_ratio_bar->SetMinSize(wxSize(-1, FromDIP(27))); m_ratio_bar->SetBackgroundStyle(wxBG_STYLE_PAINT); m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { wxBufferedPaintDC dc(m_ratio_bar); wxSize sz = m_ratio_bar->GetClientSize(); wxColour col_a = comp_colour(0), col_b = comp_colour(1); for (int x = 0; x < sz.GetWidth(); ++x) { double t = (double)x / sz.GetWidth(); wxColour c = blend_colors(col_a, col_b, 1.0 - t); dc.SetPen(wxPen(c)); dc.DrawLine(x, 0, x, sz.GetHeight()); } int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth()); // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over // blended filament colour, so it has to keep its contrast against data rather than chrome. dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4))); dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); dc.SetPen(wxPen(*wxWHITE, FromDIP(2))); dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); }); m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) commit_ratio_editor(true); m_ratio_dragging = true; if (!m_ratio_bar->HasCapture()) m_ratio_bar->CaptureMouse(); int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); }); m_ratio_bar->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) { if (!m_ratio_dragging) return; int new_ratio = 100 - (int)(e.GetX() * 100.0 / m_ratio_bar->GetClientSize().GetWidth() + 0.5); on_ratio_changed(std::max(MIN_COMPONENT_RATIO, std::min(100 - MIN_COMPONENT_RATIO, new_ratio))); }); // Key the release off the capture itself, not off the drag flag: the two can fall out of // sync (a lost capture clears the flag on its own), and a capture that outlives the widget // wedges mouse input for the whole application. m_ratio_bar->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { m_ratio_dragging = false; if (m_ratio_bar->HasCapture()) m_ratio_bar->ReleaseMouse(); }); m_ratio_bar->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_ratio_dragging = false; }); sizer->Add(m_ratio_bar, 0, wxEXPAND); auto* pct_sizer = new wxBoxSizer(wxHORIZONTAL); m_label_ratio_a = new RatioLabelPanel(this); m_label_ratio_a->SetLabel(wxString::Format(wxT("%d%%"), ratio(0))); m_label_ratio_b = new RatioLabelPanel(this); m_label_ratio_b->SetLabel(wxString::Format(wxT("%d%%"), ratio(1))); auto bind_ratio_click = [this](RatioLabelPanel* label, size_t idx) { label->Bind(wxEVT_LEFT_DOWN, [this, label, idx](wxMouseEvent&) { wxRect rect(wxPoint(0, 0), label->GetClientSize()); start_ratio_editor(idx, label, rect); }); }; bind_ratio_click(m_label_ratio_a, 0); bind_ratio_click(m_label_ratio_b, 1); pct_sizer->Add(m_label_ratio_a, 0); pct_sizer->AddStretchSpacer(1); pct_sizer->Add(m_label_ratio_b, 0); sizer->Add(pct_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); return sizer; } // ---- Triangle (ternary) ratio picker ---- // Barycentric coordinate utilities struct TriPoint { double x, y; }; static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c) { return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y); } static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) { double total = tri_signed_area2(v0, v1, v2); if (std::abs(total) < 1e-9) return false; double s0 = tri_signed_area2(p, v1, v2) / total; double s1 = tri_signed_area2(v0, p, v2) / total; double s2 = 1.0 - s0 - s1; return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001; } static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2, double& w0, double& w1, double& w2) { double total = std::abs(tri_signed_area2(v0, v1, v2)); if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; } w0 = std::abs(tri_signed_area2(p, v1, v2)) / total; w1 = std::abs(tri_signed_area2(v0, p, v2)) / total; w2 = 1.0 - w0 - w1; w0 = std::clamp(w0, 0.0, 1.0); w1 = std::clamp(w1, 0.0, 1.0); w2 = std::clamp(w2, 0.0, 1.0); double s = w0 + w1 + w2; if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } } static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2) { double w0, w1, w2; tri_barycentric(p, v0, v1, v2, w0, w1, w2); w0 = std::clamp(w0, 0.0, 1.0); w1 = std::clamp(w1, 0.0, 1.0); w2 = std::clamp(w2, 0.0, 1.0); double s = w0 + w1 + w2; if (s > 0) { w0 /= s; w1 /= s; w2 /= s; } return {w0 * v0.x + w1 * v1.x + w2 * v2.x, w0 * v0.y + w1 * v1.y + w2 * v2.y}; } wxBoxSizer* MixedFilamentDialog::create_triangle_picker() { auto* sizer = new wxBoxSizer(wxVERTICAL); int panel_w = FromDIP(160); int panel_h = FromDIP(160); m_triangle_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(panel_w, panel_h)); m_triangle_panel->SetMinSize(wxSize(panel_w, panel_h)); m_triangle_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_triangle_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); auto get_vertices = [this]() -> std::tuple { wxSize sz = m_triangle_panel->GetClientSize(); double pw = sz.GetWidth(), ph = sz.GetHeight(); double margin = FromDIP(20); double avail = std::min(pw, ph) - 2 * margin; double side = avail; double tri_h = side * std::sqrt(3.0) / 2.0; double cx = pw / 2.0; double top_y = (ph - tri_h) / 2.0; double bot_y = top_y + tri_h; TriPoint v0 = {cx, top_y}; // top TriPoint v1 = {cx - side / 2.0, bot_y}; // bottom-left TriPoint v2 = {cx + side / 2.0, bot_y}; // bottom-right return {v0, v1, v2}; }; m_triangle_panel->Bind(wxEVT_PAINT, [this, get_vertices](wxPaintEvent&) { wxBufferedPaintDC dc(m_triangle_panel); wxSize sz = m_triangle_panel->GetClientSize(); auto [v0, v1, v2] = get_vertices(); wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); dc.SetBrush(wxBrush(tri_bg)); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); const bool cache_valid = m_tri_cache_bmp.IsOk() && m_tri_cache_size == sz && m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2; if (!cache_valid) { int min_y = (int)std::min({v0.y, v1.y, v2.y}); int max_y = (int)std::max({v0.y, v1.y, v2.y}); int min_x = (int)std::min({v0.x, v1.x, v2.x}); int max_x = (int)std::max({v0.x, v1.x, v2.x}); m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24); wxMemoryDC mdc(m_tri_cache_bmp); mdc.SetBrush(wxBrush(tri_bg)); mdc.SetPen(*wxTRANSPARENT_PEN); mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); for (int py = min_y; py <= max_y; ++py) { for (int px = min_x; px <= max_x; ++px) { TriPoint p = {(double)px, (double)py}; if (!tri_contains(p, v0, v1, v2)) continue; double w0, w1, w2; tri_barycentric(p, v0, v1, v2, w0, w1, w2); unsigned char mr, mg, mb; if (w0 + w1 > 1e-6) { float t01 = static_cast(w1 / (w0 + w1)); Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, &mb); float t2 = static_cast(w2); Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), t2, &mr, &mg, &mb); } else { mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); } mdc.SetPen(wxPen(wxColour(mr, mg, mb))); mdc.DrawPoint(px, py); } } mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); mdc.SetBrush(*wxTRANSPARENT_BRUSH); wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}}; mdc.DrawPolygon(3, pts); mdc.SelectObject(wxNullBitmap); m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2; m_tri_cache_size = sz; } dc.DrawBitmap(m_tri_cache_bmp, 0, 0); // Drag handle (always redrawn on top of cached bitmap) double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x; double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y; int handle_r = FromDIP(5); dc.SetBrush(*wxWHITE_BRUSH); dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2))); dc.DrawCircle((int)hx, (int)hy, handle_r); if (m_result.ratios.size() >= 3) { dc.SetFont(::Label::Body_10); wxSize ts0 = dc.GetTextExtent(wxString::Format(wxT("%d%%"), m_result.ratios[0])); int top_label_y = std::max(0, (int)(v0.y - ts0.GetHeight() - FromDIP(4))); dc.SetFont(::Label::Body_12); dc.SetTextForeground(StateColor::darkModeColorFor(COLOR_LABEL_MUTED)); dc.DrawText(_L("Ratio"), FromDIP(2), top_label_y); // Position the real RatioLabelPanel children for (int i = 0; i < 3 && i < (int)m_triangle_ratio_labels.size(); ++i) { if (!m_triangle_ratio_labels[i]) continue; m_triangle_ratio_labels[i]->SetLabel( wxString::Format(wxT("%d%%"), m_result.ratios[i])); wxSize lsz = m_triangle_ratio_labels[i]->GetMinSize(); int lx = 0, ly = 0; if (i == 0) { lx = (int)(v0.x - lsz.GetWidth() / 2); ly = top_label_y; } else if (i == 1) { lx = (int)(v1.x - lsz.GetWidth() / 2); ly = (int)(v1.y + FromDIP(3)); } else { lx = (int)(v2.x - lsz.GetWidth() / 2); ly = (int)(v2.y + FromDIP(3)); } m_triangle_ratio_labels[i]->SetSize(lx, ly, lsz.GetWidth(), lsz.GetHeight()); } } }); auto handle_mouse = [this, get_vertices](wxMouseEvent& e, bool is_down) { auto [v0, v1, v2] = get_vertices(); TriPoint p = {(double)e.GetX(), (double)e.GetY()}; if (is_down) { // Only start dragging when the press lands inside the triangle; // clicks outside the triangle must not change the mix ratio. if (!tri_contains(p, v0, v1, v2)) return; m_tri_dragging = true; if (!m_triangle_panel->HasCapture()) m_triangle_panel->CaptureMouse(); } if (!m_tri_dragging) return; TriPoint clamped = tri_clamp(p, v0, v1, v2); tri_barycentric(clamped, v0, v1, v2, m_tri_wx, m_tri_wy, m_tri_wz); int r0 = (int)(m_tri_wx * 100 + 0.5); int r1 = (int)(m_tri_wy * 100 + 0.5); int r2 = 100 - r0 - r1; r0 = std::clamp(r0, 0, 100); r1 = std::clamp(r1, 0, 100); r2 = std::clamp(r2, 0, 100); apply_dragged_triangle_ratio(r0, r1, r2); }; // Create 3 RatioLabelPanel children on the triangle panel m_triangle_ratio_labels.fill(nullptr); for (int i = 0; i < 3; ++i) { auto* lbl = new RatioLabelPanel(m_triangle_panel); lbl->SetLabel(wxString::Format(wxT("%d%%"), (i < (int)m_result.ratios.size()) ? m_result.ratios[i] : 33)); size_t idx = (size_t)i; lbl->Bind(wxEVT_LEFT_DOWN, [this, lbl, idx](wxMouseEvent&) { wxRect rect(wxPoint(0, 0), lbl->GetClientSize()); start_ratio_editor(idx, lbl, rect); }); m_triangle_ratio_labels[i] = lbl; } m_triangle_panel->Bind(wxEVT_LEFT_DOWN, [this, handle_mouse](wxMouseEvent& e) { if (m_ratio_editor_panel && m_ratio_editor_panel->IsShown()) commit_ratio_editor(true); handle_mouse(e, true); }); m_triangle_panel->Bind(wxEVT_MOTION, [this, handle_mouse](wxMouseEvent& e) { if (m_tri_dragging) handle_mouse(e, false); }); m_triangle_panel->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) { m_tri_dragging = false; if (m_triangle_panel->HasCapture()) m_triangle_panel->ReleaseMouse(); }); m_triangle_panel->Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_tri_dragging = false; }); sizer->Add(m_triangle_panel, 0); return sizer; } wxBoxSizer* MixedFilamentDialog::create_gradient_section() { m_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); m_chk_gradient = new ::CheckBox(this); m_chk_gradient->SetValue(m_result.gradient_enabled); m_chk_gradient->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); on_gradient_toggled(); }); m_gradient_sizer->Add(m_chk_gradient, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); m_label_gradient = new wxStaticText(this, wxID_ANY, _L("Gradient Effect")); m_label_gradient->SetFont(::Label::Body_13); m_gradient_sizer->Add(m_label_gradient, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); m_combo_gradient_dir = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(152), FromDIP(24)), 0, nullptr, wxCB_READONLY); m_combo_gradient_dir->SetKeepDropArrow(true); update_gradient_direction_items(); m_combo_gradient_dir->SetSelection(m_result.gradient_direction); m_combo_gradient_dir->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { on_gradient_direction_changed(); }); m_combo_gradient_dir->Show(m_result.gradient_enabled); m_gradient_sizer->Add(m_combo_gradient_dir, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); auto* outer = new wxBoxSizer(wxVERTICAL); outer->Add(m_gradient_sizer, 0, wxEXPAND); // Custom curve editor: visible only when gradient is on and exactly 2 components are mixed. m_curve_sizer = new wxBoxSizer(wxVERTICAL); m_curve_editor = new GradientCurveEditor(this, comp_colour(0), comp_colour(1)); if (!m_result.gradient_curve.empty()) m_curve_editor->set_points(m_result.gradient_curve); else m_curve_editor->reset_to_linear((m_result.gradient_direction == 0) ? 0.9 : 0.1, (m_result.gradient_direction == 0) ? 0.1 : 0.9); m_curve_editor->Bind(wxEVT_GRADIENT_CURVE_CHANGED, [this](wxCommandEvent&) { on_gradient_curve_changed(); }); m_curve_sizer->Add(m_curve_editor, 0, wxEXPAND | wxTOP, FromDIP(4)); outer->Add(m_curve_sizer, 0, wxEXPAND | wxTOP, FromDIP(6)); const bool curve_visible = m_result.gradient_enabled && num_components() == 2; m_curve_sizer->ShowItems(curve_visible); // Per-part gradient toggle sits BELOW the curve editor. m_per_part_gradient_sizer = new wxBoxSizer(wxHORIZONTAL); m_chk_per_part_gradient = new ::CheckBox(this); m_chk_per_part_gradient->SetValue(m_result.per_part_gradient); m_chk_per_part_gradient->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); on_per_part_gradient_toggled(); }); m_per_part_gradient_sizer->Add(m_chk_per_part_gradient, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, FromDIP(4)); m_label_per_part_gradient = new wxStaticText(this, wxID_ANY, _L("Enable per-part gradient effect")); m_label_per_part_gradient->SetFont(::Label::Body_13); m_per_part_gradient_sizer->Add(m_label_per_part_gradient, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); outer->Add(m_per_part_gradient_sizer, 0, wxEXPAND | wxTOP, FromDIP(2)); m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); return outer; } wxBoxSizer* MixedFilamentDialog::create_recommendation_grid() { auto* outer = new wxBoxSizer(wxVERTICAL); auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); auto* rec_label = new wxStaticText(this, wxID_ANY, _L("Mixing Recommendations")); rec_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#ACACAC"))); rec_label->SetFont(::Label::Body_10); title_sizer->Add(rec_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); auto* rec_line = new wxPanel(this, wxID_ANY); rec_line->SetMinSize(wxSize(-1, 1)); rec_line->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#EEEEEE"))); title_sizer->Add(rec_line, 1, wxALIGN_CENTER_VERTICAL); outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); m_recommendation_scroll->SetScrollRate(0, 5); m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); auto* scroll_inner_sizer = new wxBoxSizer(wxVERTICAL); scroll_inner_sizer->Add(m_recommendation_grid, 1, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); m_recommendation_scroll->SetSizer(scroll_inner_sizer); rebuild_recommendation_items(); outer->Add(m_recommendation_scroll, 1, wxEXPAND | wxTOP, FromDIP(4)); return outer; } void MixedFilamentDialog::rebuild_recommendation_items() { if (!m_recommendation_scroll || !m_recommendation_grid) return; static constexpr int MAX_RECOMMENDATIONS = 100; m_recommendation_scroll->Freeze(); m_recommendation_grid->Clear(true); size_t n = m_physical_colors.size(); int count = 0; // Group physical filaments by type (only same-type combos are recommended) std::map> type_groups; for (size_t i = 0; i < n; ++i) { std::string t = (i < m_physical_types.size()) ? m_physical_types[i] : "PLA"; // Skip support filaments (type ends with "-S") if (t.size() >= 2 && t.compare(t.size() - 2, 2, "-S") == 0) continue; type_groups[t].push_back(i); } if (num_components() >= 3) { // Three-color: C(g,3) x 3 variants per same-type group for (auto& [type, indices] : type_groups) { if (count >= MAX_RECOMMENDATIONS) break; size_t g = indices.size(); for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { for (size_t ci = bi + 1; ci < g && count < MAX_RECOMMENDATIONS; ++ci) { size_t idx[3] = {indices[ai], indices[bi], indices[ci]}; // 3 variants: each filament takes the 50% role in turn for (int dominant = 0; dominant < 3 && count < MAX_RECOMMENDATIONS; ++dominant) { size_t i0 = idx[(dominant + 1) % 3]; // 25% size_t i1 = idx[(dominant + 2) % 3]; // 25% size_t i2 = idx[dominant]; // 50% wxColour ca(m_physical_colors[i0]); wxColour cb(m_physical_colors[i1]); wxColour cc(m_physical_colors[i2]); wxColour mixed = blend_n_colors({ca, cb, cc}, {0.25, 0.25, 0.50}); auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); item->SetBackgroundColour(mixed); item->SetCursor(wxCursor(wxCURSOR_HAND)); unsigned int ca_1 = (unsigned int)(i0 + 1); unsigned int cb_1 = (unsigned int)(i1 + 1); unsigned int cc_1 = (unsigned int)(i2 + 1); item->Bind(wxEVT_LEFT_UP, [this, ca_1, cb_1, cc_1](wxMouseEvent&) { on_recommendation_clicked_triple(ca_1, cb_1, cc_1); }); item->SetToolTip(wxString::Format(wxT("%s + %s + %s"), wxString::FromUTF8(m_physical_names[i0]), wxString::FromUTF8(m_physical_names[i1]), wxString::FromUTF8(m_physical_names[i2]))); m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); ++count; } } } } } } else { // Two-color: C(g,2) per same-type group for (auto& [type, indices] : type_groups) { if (count >= MAX_RECOMMENDATIONS) break; size_t g = indices.size(); for (size_t ai = 0; ai < g && count < MAX_RECOMMENDATIONS; ++ai) { for (size_t bi = ai + 1; bi < g && count < MAX_RECOMMENDATIONS; ++bi) { size_t i = indices[ai]; size_t j = indices[bi]; wxColour ca(m_physical_colors[i]); wxColour cb(m_physical_colors[j]); wxColour mixed = blend_colors(ca, cb, 0.5); auto* item = new wxPanel(m_recommendation_scroll, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20))); item->SetBackgroundColour(mixed); item->SetCursor(wxCursor(wxCURSOR_HAND)); unsigned int comp_a = (unsigned int)(i + 1); unsigned int comp_b = (unsigned int)(j + 1); item->Bind(wxEVT_LEFT_UP, [this, comp_a, comp_b](wxMouseEvent&) { on_recommendation_clicked(comp_a, comp_b); }); item->SetToolTip(wxString::Format(wxT("%s + %s"), wxString::FromUTF8(m_physical_names[i]), wxString::FromUTF8(m_physical_names[j]))); m_recommendation_grid->Add(item, 0, wxRIGHT | wxBOTTOM, FromDIP(6)); ++count; } } } } m_recommendation_scroll->SetScrollbars(0, FromDIP(20), 0, 1); m_recommendation_scroll->FitInside(); m_recommendation_scroll->Layout(); m_recommendation_scroll->Thaw(); } wxBoxSizer* MixedFilamentDialog::create_button_panel() { auto* sizer = new wxBoxSizer(wxHORIZONTAL); m_btn_cancel = new Button(this, _L("Cancel")); m_btn_cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice); m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); m_btn_ok = new Button(this, _L("OK")); m_btn_ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); m_btn_ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); }); sizer->Add(m_btn_cancel, 0, wxRIGHT, FromDIP(12)); sizer->Add(m_btn_ok, 0); return sizer; } void MixedFilamentDialog::rebuild_all_combos() { m_combo_to_physical.resize(m_combo_filaments.size()); for (size_t i = 0; i < m_combo_filaments.size(); ++i) { std::set others_selected; std::set others_types; for (size_t k = 0; k < m_result.components.size(); ++k) { if (k == i) continue; unsigned int phys = m_result.components[k]; others_selected.insert(phys); if (phys >= 1 && phys <= m_physical_types.size()) others_types.insert(m_physical_types[phys - 1]); } auto* combo = m_combo_filaments[i]; combo->Clear(); m_combo_to_physical[i].clear(); int restore_sel = -1; unsigned int cur_phys = (i < m_result.components.size()) ? m_result.components[i] : 0; if (cur_phys == 0) { combo->Append(_L("-- Select --")); m_combo_to_physical[i].push_back(0); restore_sel = 0; } for (size_t j = 0; j < m_physical_names.size(); ++j) { unsigned int phys_1based = (unsigned int)(j + 1); if (others_selected.count(phys_1based)) continue; int style = 0; if (!others_types.empty() && !m_physical_types.empty()) { std::string this_type = (j < m_physical_types.size()) ? m_physical_types[j] : "PLA"; if (others_types.find(this_type) == others_types.end()) style = DD_ITEM_STYLE_DIMMED; } int idx = combo->Append(wxString::FromUTF8(m_physical_names[j]), make_swatch_bitmap(j), style); m_combo_to_physical[i].push_back(phys_1based); if (phys_1based == cur_phys) restore_sel = idx; } if (restore_sel >= 0) combo->SetSelection(restore_sel); else if (combo->GetCount() > 0) combo->SetSelection(0); } } void MixedFilamentDialog::refresh_curve_editor_colors() { if (m_curve_editor) m_curve_editor->set_colors(comp_colour(0), comp_colour(1)); } // ---- Event Handlers ---- void MixedFilamentDialog::on_filament_changed() { for (size_t i = 0; i < m_combo_filaments.size() && i < m_result.components.size(); ++i) { int sel = m_combo_filaments[i]->GetSelection(); if (sel >= 0 && i < m_combo_to_physical.size() && sel < (int)m_combo_to_physical[i].size()) m_result.components[i] = m_combo_to_physical[i][sel]; } refresh_curve_editor_colors(); rebuild_all_combos(); update_gradient_direction_items(); update_preview(); update_ok_button_state(); } void MixedFilamentDialog::on_ratio_changed(int new_ratio_a) { if (m_result.ratios.size() < 2) return; m_result.ratios[0] = new_ratio_a; m_result.ratios[1] = 100 - new_ratio_a; reset_manual_ratio_state(); refresh_ratio_labels(); update_preview(); } void MixedFilamentDialog::on_gradient_toggled() { m_result.gradient_enabled = m_chk_gradient->GetValue(); if (m_ratio_sizer) m_ratio_sizer->ShowItems(!m_result.gradient_enabled && num_components() == 2); if (m_combo_gradient_dir) m_combo_gradient_dir->Show(m_result.gradient_enabled); if (m_per_part_gradient_sizer) m_per_part_gradient_sizer->ShowItems(m_result.gradient_enabled); if (m_curve_sizer) m_curve_sizer->ShowItems(m_result.gradient_enabled && num_components() == 2); if (!m_result.gradient_enabled) { m_result.per_part_gradient = false; if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); } // Toggling the curve editor changes the right column height (and width when // turning gradient on), so the dialog must follow or the recommendation list // gets squeezed off-screen. Same trick as 2-color -> 3-color switching. const wxSize new_size = compute_dialog_size(); if (GetSize() != new_size) { const wxRect old_rect = GetRect(); const wxPoint center(old_rect.x + old_rect.width / 2, old_rect.y + old_rect.height / 2); SetSize(new_size); SetPosition(wxPoint(center.x - new_size.x / 2, center.y - new_size.y / 2)); } Layout(); Refresh(); } void MixedFilamentDialog::on_gradient_direction_changed() { if (!m_combo_gradient_dir) return; m_result.gradient_direction = m_combo_gradient_dir->GetSelection(); // Mirror the user's custom curve around y=0.5 instead of resetting it, so // shape work (added anchors, bent segments) survives a direction toggle. // reverse() flips y and tangent signs consistently; default two-point // linear curves end up matching the new direction exactly (0.9->0.1 <-> 0.1->0.9). if (m_curve_editor) { m_curve_editor->reverse(); m_result.gradient_curve = m_curve_editor->get_points(); } update_preview(); } void MixedFilamentDialog::on_gradient_curve_changed() { if (m_curve_editor) m_result.gradient_curve = m_curve_editor->get_points(); update_preview(); } void MixedFilamentDialog::on_per_part_gradient_toggled() { if (m_chk_per_part_gradient) m_result.per_part_gradient = m_chk_per_part_gradient->GetValue(); } void MixedFilamentDialog::on_add_material() { size_t n = num_components(); if (n >= (size_t)MAX_COMPONENTS) return; unsigned int new_comp = 0; for (size_t j = 0; j < m_physical_names.size(); ++j) { unsigned int candidate = (unsigned int)(j + 1); bool used = false; for (auto c : m_result.components) if (c == candidate) { used = true; break; } if (!used) { new_comp = candidate; break; } } if (new_comp == 0) return; m_result.components.push_back(new_comp); int each = 100 / (int)(n + 1); m_result.ratios.clear(); int assigned = 0; for (size_t i = 0; i < n; ++i) { m_result.ratios.push_back(each); assigned += each; } m_result.ratios.push_back(100 - assigned); if (m_result.ratios.size() >= 3) { int sum = 0; for (int r : m_result.ratios) sum += r; if (sum > 0) { m_tri_wx = (double)m_result.ratios[0] / sum; m_tri_wy = (double)m_result.ratios[1] / sum; m_tri_wz = (double)m_result.ratios[2] / sum; } } reset_manual_ratio_state(); append_material_row(); rebuild_all_combos(); refresh_curve_editor_colors(); update_component_count_ui(); update_preview(); update_ok_button_state(); rebuild_recommendation_items(); Layout(); Refresh(); } void MixedFilamentDialog::on_remove_material() { if (num_components() <= 2) return; m_result.components.resize(2); m_result.ratios = {50, 50}; m_tri_wx = 0.5; m_tri_wy = 0.5; m_tri_wz = 0.0; reset_manual_ratio_state(); refresh_ratio_labels(); if (m_material_rows_sizer && m_material_rows_sizer->GetItemCount() > 2) { auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); if (sizer_item && sizer_item->GetSizer()) sizer_item->GetSizer()->Clear(true); m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); } if (m_combo_filaments.size() > 2) m_combo_filaments.pop_back(); if (m_combo_to_physical.size() > 2) m_combo_to_physical.pop_back(); rebuild_all_combos(); refresh_curve_editor_colors(); update_component_count_ui(); update_preview(); update_ok_button_state(); rebuild_recommendation_items(); Layout(); Refresh(); } void MixedFilamentDialog::on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b) { while (m_material_rows_sizer->GetItemCount() > 2) { auto* sizer_item = m_material_rows_sizer->GetItem(m_material_rows_sizer->GetItemCount() - 1); if (sizer_item && sizer_item->GetSizer()) sizer_item->GetSizer()->Clear(true); m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); } while (m_combo_filaments.size() > 2) m_combo_filaments.pop_back(); while (m_combo_to_physical.size() > 2) m_combo_to_physical.pop_back(); m_result.components = {comp_a, comp_b}; m_result.ratios = {50, 50}; reset_manual_ratio_state(); refresh_ratio_labels(); rebuild_all_combos(); refresh_curve_editor_colors(); update_gradient_direction_items(); update_component_count_ui(); update_preview(); update_ok_button_state(); Layout(); Refresh(); } void MixedFilamentDialog::on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c) { // Ensure we have exactly 3 combo rows if (num_components() < 3) { // Need to add a 3rd combo row 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); if (sizer_item && sizer_item->GetSizer()) sizer_item->GetSizer()->Clear(true); m_material_rows_sizer->Remove(m_material_rows_sizer->GetItemCount() - 1); } while (m_combo_filaments.size() > 3) m_combo_filaments.pop_back(); while (m_combo_to_physical.size() > 3) m_combo_to_physical.pop_back(); } m_result.components = {a, b, c}; m_result.ratios = {25, 25, 50}; m_tri_wx = 0.25; m_tri_wy = 0.25; m_tri_wz = 0.50; reset_manual_ratio_state(); rebuild_all_combos(); refresh_curve_editor_colors(); update_gradient_direction_items(); update_component_count_ui(); update_preview(); update_ok_button_state(); Layout(); Refresh(); } void MixedFilamentDialog::update_preview() { if (m_preview_canvas) m_preview_canvas->Refresh(); if (m_summary_panel) m_summary_panel->Refresh(); if (m_ratio_bar) m_ratio_bar->Refresh(); if (m_triangle_panel) m_triangle_panel->Refresh(); } void MixedFilamentDialog::paint_warning_panel(wxPaintEvent&) { wxBufferedPaintDC dc(m_warning_panel); wxSize sz = m_warning_panel->GetClientSize(); dc.SetBrush(wxBrush(StateColor::darkModeColorFor(*wxWHITE))); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#F8F8F8")))); dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#D01B1B")), 1)); dc.DrawRoundedRectangle(0, 0, sz.GetWidth(), sz.GetHeight(), FromDIP(4)); int x = FromDIP(10); int cy = sz.GetHeight() / 2; int icon_r = FromDIP(7); dc.SetBrush(wxBrush(StateColor::darkModeColorFor(wxColour("#D01B1B")))); dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawCircle(x + icon_r, cy, icon_r); dc.SetFont(::Label::Body_10); dc.SetTextForeground(*wxWHITE); wxSize ex = dc.GetTextExtent(wxT("!")); dc.DrawText(wxT("!"), x + icon_r - ex.GetWidth() / 2, cy - ex.GetHeight() / 2); x += icon_r * 2 + FromDIP(6); if (m_type_mismatch_msg.empty()) return; dc.SetFont(::Label::Body_12); dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#D01B1B"))); wxString msg = m_type_mismatch_msg; int avail_w = sz.GetWidth() - x - FromDIP(10); wxSize ts = dc.GetTextExtent(msg); if (ts.GetWidth() <= avail_w) { dc.DrawText(msg, x, cy - ts.GetHeight() / 2); } else { wxArrayString lines; wxString cur_line; wxArrayString words; wxStringTokenizer tkz(msg, wxT(" "), wxTOKEN_RET_EMPTY_ALL); while (tkz.HasMoreTokens()) words.Add(tkz.GetNextToken()); if (words.empty()) words.Add(msg); for (size_t w = 0; w < words.size(); ++w) { wxString test = cur_line.empty() ? words[w] : cur_line + wxT(" ") + words[w]; if (dc.GetTextExtent(test).GetWidth() > avail_w && !cur_line.empty()) { lines.Add(cur_line); cur_line = words[w]; } else { cur_line = test; } } if (!cur_line.empty()) lines.Add(cur_line); if (lines.empty()) lines.Add(msg); int line_h = dc.GetTextExtent(wxT("Mg")).GetHeight(); int total_h = (int)lines.size() * line_h; int y0 = (sz.GetHeight() - total_h) / 2; for (size_t l = 0; l < lines.size(); ++l) dc.DrawText(lines[l], x, y0 + (int)l * line_h); } } void MixedFilamentDialog::update_ok_button_state() { if (!m_btn_ok) return; bool has_type_mismatch = false; if (!m_physical_types.empty() && m_result.components.size() >= 2) { std::map> type_groups; for (size_t i = 0; i < m_result.components.size(); ++i) { unsigned int phys = m_result.components[i]; if (phys < 1 || phys > m_physical_types.size()) continue; type_groups[m_physical_types[phys - 1]].push_back(phys); } has_type_mismatch = type_groups.size() > 1; if (has_type_mismatch) { wxString parts; for (auto it = type_groups.begin(); it != type_groups.end(); ++it) { if (!parts.empty()) parts += _L(" and "); wxString slots; for (size_t j = 0; j < it->second.size(); ++j) { if (!slots.empty()) slots += ", "; slots += std::to_string(it->second[j]); } 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; for (unsigned int c : m_result.components) { if (c == 0) { has_unselected = true; break; } } bool can_confirm = !has_type_mismatch && !has_unselected; // Enable() alone repaints the button: its StateColor carries the disabled grey. m_btn_ok->Enable(can_confirm); if (has_unselected) m_btn_ok->SetToolTip(_L("Please select a filament for all components")); else if (has_type_mismatch) m_btn_ok->SetToolTip(_L("Cannot mix different filament types")); else m_btn_ok->SetToolTip(wxEmptyString); 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(); } } void MixedFilamentDialog::update_gradient_direction_items() { if (!m_combo_gradient_dir) return; int prev_sel = m_combo_gradient_dir->GetSelection(); m_combo_gradient_dir->Clear(); if (num_components() < 2) return; auto make_direction_bitmap = [this](size_t idx_from, size_t idx_to) -> wxBitmap { int swatch_sz = FromDIP(20); int arrow_w = FromDIP(16); int gap = FromDIP(4); int bmp_w = swatch_sz + gap + arrow_w + gap + swatch_sz; int bmp_h = swatch_sz; wxColour dir_text = StateColor::darkModeColorFor(wxColour("#262E30")); return make_alpha_bitmap(bmp_w, bmp_h, [&](wxDC& dc) { dc.SetFont(::Label::Body_13); auto draw_swatch = [&](int x, size_t idx) { std::string color_hex = "#D9D9D9"; if (idx < m_physical_colors.size()) color_hex = m_physical_colors[idx]; std::string label = std::to_string(idx + 1); wxBitmap* icon = get_extruder_color_icon(color_hex, label, swatch_sz, swatch_sz); if (icon && icon->IsOk()) dc.DrawBitmap(*icon, x, 0); }; int x = 0; draw_swatch(x, idx_from); x += swatch_sz + gap; dc.SetTextForeground(dir_text); wxString arrow = wxT("\u2192"); wxSize arrow_sz = dc.GetTextExtent(arrow); dc.DrawText(arrow, x + (arrow_w - arrow_sz.GetWidth()) / 2, (bmp_h - arrow_sz.GetHeight()) / 2); x += arrow_w + gap; draw_swatch(x, idx_to); }); }; size_t idx_a = (comp(0) >= 1) ? comp(0) - 1 : 0; size_t idx_b = (comp(1) >= 1) ? comp(1) - 1 : 1; m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_a, idx_b)); m_combo_gradient_dir->Append(wxT(" "), make_direction_bitmap(idx_b, idx_a)); if (prev_sel >= 0 && prev_sel < (int)m_combo_gradient_dir->GetCount()) m_combo_gradient_dir->SetSelection(prev_sel); else m_combo_gradient_dir->SetSelection(0); } wxSize MixedFilamentDialog::compute_dialog_size() const { const bool is_three = (num_components() >= 3); const bool curve_visible = !is_three && m_result.gradient_enabled; int w = FromDIP(439); int h = FromDIP(580); if (is_three) { h = FromDIP(680); } else if (curve_visible) { // Wider so the gradient editor can show "Material Ratio" intact; // +40 over the 3-color height to fit the curve editor while keeping the // recommendation list visible (it can still scroll if needed). w = FromDIP(470); h = FromDIP(720); } return wxSize(w, h); } void MixedFilamentDialog::update_component_count_ui() { bool is_two = (num_components() == 2); bool is_three = (num_components() >= 3); // Toggle ratio slider vs triangle picker if (m_ratio_sizer) m_ratio_sizer->ShowItems(is_two && !m_result.gradient_enabled); if (m_triangle_sizer) m_triangle_sizer->ShowItems(is_three); // 3-color: hide gradient entirely, force off if (m_gradient_sizer) { bool show_gradient = is_two; m_chk_gradient->Show(show_gradient); if (m_label_gradient) m_label_gradient->Show(show_gradient); m_combo_gradient_dir->Show(show_gradient && m_result.gradient_enabled); if (m_per_part_gradient_sizer) m_per_part_gradient_sizer->ShowItems(show_gradient && m_result.gradient_enabled); if (m_curve_sizer) m_curve_sizer->ShowItems(show_gradient && m_result.gradient_enabled); } if (is_three) { m_result.gradient_enabled = false; if (m_chk_gradient) m_chk_gradient->SetValue(false); m_result.per_part_gradient = false; if (m_chk_per_part_gradient) m_chk_per_part_gradient->SetValue(false); } if (m_btn_add_material) { bool can_add = (num_components() < (size_t)MAX_COMPONENTS && m_physical_colors.size() > num_components()); m_btn_add_material->Enable(can_add); m_btn_add_material->SetToolTip(can_add ? wxString() : (is_three ? _L("Maximum 3 materials for mixing") : _L("Maximum number of components reached"))); } if (m_btn_remove_material) { m_btn_remove_material->Show(is_three); m_btn_remove_material->Enable(is_three); m_btn_remove_material->SetToolTip(is_three ? _L("Remove the third material") : wxString()); } const wxSize new_size = compute_dialog_size(); const wxRect old_rect = GetRect(); const wxPoint center(old_rect.x + old_rect.width / 2, old_rect.y + old_rect.height / 2); SetSize(new_size); SetPosition(wxPoint(center.x - new_size.x / 2, center.y - new_size.y / 2)); Layout(); } } // namespace GUI } // namespace Slic3r