mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 06:41:10 +00:00
Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI scroll areas. Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm. This makes Preferences and other dialogs scroll consistently across Windows DPI settings.
1831 lines
70 KiB
C++
1831 lines
70 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_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_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();
|
|
draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()),
|
|
comp_colour(0), comp_colour(1), ratio(1) / 100.0);
|
|
});
|
|
|
|
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 ----
|
|
// The barycentric utilities (TriPoint, tri_contains, tri_barycentric, tri_clamp) live in
|
|
// FilamentBitmapUtils so the Publish dialog can mirror this picker read-only.
|
|
|
|
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();
|
|
|
|
// Draw the background, cached barycentric fill, outline and drag-handle marker through the
|
|
// shared picker painter (same geometry the read-only Publish preview uses).
|
|
draw_mixed_triangle_picker(dc, sz,
|
|
{comp_colour(0), comp_colour(1), comp_colour(2)},
|
|
{m_tri_wx, m_tri_wy, m_tri_wz},
|
|
{StateColor::darkModeColorFor(*wxWHITE),
|
|
StateColor::darkModeColorFor(wxColour("#CECECE")),
|
|
StateColor::darkModeColorFor(wxColour("#262E30")),
|
|
StateColor::darkModeColorFor(COLOR_LABEL_MUTED)});
|
|
|
|
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, FromDIP(20));
|
|
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
|