mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-01 14:27:00 +00:00
1949 lines
75 KiB
C++
1949 lines
75 KiB
C++
#include "MixedFilamentDialog.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <set>
|
|
#include <wx/image.h>
|
|
#include <wx/sizer.h>
|
|
#include <wx/dcclient.h>
|
|
#include <wx/dcbuffer.h>
|
|
#include <wx/dcgraph.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/textctrl.h>
|
|
#include <wx/wrapsizer.h>
|
|
#include <wx/tokenzr.h>
|
|
|
|
#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<float>(1.0 - ratio_a),
|
|
&r, &g, &bl);
|
|
return wxColour(r, g, bl);
|
|
}
|
|
|
|
|
|
// ---- Constructors ----
|
|
|
|
MixedFilamentDialog::MixedFilamentDialog(wxWindow* parent,
|
|
const std::vector<std::string>& physical_colors,
|
|
const std::vector<std::string>& physical_names,
|
|
const std::vector<std::string>& 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<std::string>& physical_colors,
|
|
const std::vector<std::string>& physical_names,
|
|
const std::vector<std::string>& 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<void(wxDC& dc)>& 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<size_t> 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<int>(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<wxColour> cols;
|
|
std::vector<double> 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<TriPoint, TriPoint, TriPoint> {
|
|
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<float>(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<float>(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<std::string, std::vector<size_t>> 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<unsigned int> others_selected;
|
|
std::set<std::string> 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<std::string, std::vector<unsigned int>> 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
|