mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-24 18:37:39 +00:00
Port mixed filament dialog fixes from BambuStudio
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int>(std::lround(x * r.width));
|
||||
// y axis is inverted: y=1 should sit at the top.
|
||||
const int py = r.y + static_cast<int>(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<int>(std::lround(p.m_x)), static_cast<int>(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<wxPoint> {
|
||||
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
std::vector<wxPoint> poly;
|
||||
std::vector<wxPoint2DDouble> 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<wxPoint>& 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<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
|
||||
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
|
||||
dc.DrawLines(static_cast<int>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wx/colour.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/geometry.h>
|
||||
#include <wx/panel.h>
|
||||
|
||||
#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).
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user