mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
BBL Port Color Mix Base
This commit is contained in:
@@ -0,0 +1,943 @@
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcbuffer.h>
|
||||
#include "wx/graphics.h"
|
||||
|
||||
#include "I18N.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "format.hpp"
|
||||
#include "Widgets/ComboBox.hpp"
|
||||
#include "Widgets/DropDown.hpp"
|
||||
#include "Widgets/Button.hpp"
|
||||
#include "Widgets/CheckBox.hpp"
|
||||
#include "Widgets/Label.hpp"
|
||||
#include "wxExtensions.hpp"
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
static const wxColour COLOR_BRAND("#00AE42");
|
||||
static const wxColour COLOR_BORDER_NORMAL("#EEEEEE");
|
||||
static const wxColour COLOR_BG_CARD("#F8F8F8");
|
||||
static const wxColour COLOR_LABEL_GREY("#ACACAC");
|
||||
static const wxColour COLOR_TEXT_DARK("#262E30");
|
||||
static const wxColour COLOR_DIVIDER("#EEEEEE");
|
||||
|
||||
// Standard CMYW base colors
|
||||
static const wxColour CMYW_CYAN(0, 255, 255);
|
||||
static const wxColour CMYW_MAGENTA(255, 0, 255);
|
||||
static const wxColour CMYW_YELLOW(255, 255, 0);
|
||||
static const wxColour CMYW_WHITE(255, 255, 255);
|
||||
|
||||
// Standard RYBW base colors
|
||||
static const wxColour RYBW_RED(255, 0, 0);
|
||||
static const wxColour RYBW_YELLOW(255, 255, 0);
|
||||
static const wxColour RYBW_BLUE(0, 0, 255);
|
||||
static const wxColour RYBW_WHITE(255, 255, 255);
|
||||
|
||||
static size_t mode_index(DecomposeMode mode)
|
||||
{
|
||||
return static_cast<size_t>(mode);
|
||||
}
|
||||
|
||||
static ColorDecomposeRgb wx_colour_to_recipe_rgb(const wxColour& color)
|
||||
{
|
||||
return {
|
||||
static_cast<unsigned char>(color.Red()),
|
||||
static_cast<unsigned char>(color.Green()),
|
||||
static_cast<unsigned char>(color.Blue())
|
||||
};
|
||||
}
|
||||
|
||||
static wxColour hex_to_wx_colour(const std::string& hex, const wxColour& fallback)
|
||||
{
|
||||
wxColour color(hex);
|
||||
return color.IsOk() ? color : fallback;
|
||||
}
|
||||
|
||||
static bool same_rgb(const wxColour& lhs, const wxColour& rhs)
|
||||
{
|
||||
return lhs.Red() == rhs.Red() && lhs.Green() == rhs.Green() && lhs.Blue() == rhs.Blue();
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_from_key(const std::string& key)
|
||||
{
|
||||
if (key == "Cyan") return DecomposeBaseColor::Cyan;
|
||||
if (key == "Magenta") return DecomposeBaseColor::Magenta;
|
||||
if (key == "Yellow") return DecomposeBaseColor::Yellow;
|
||||
if (key == "White") return DecomposeBaseColor::White;
|
||||
if (key == "Red") return DecomposeBaseColor::Red;
|
||||
if (key == "Green") return DecomposeBaseColor::Green;
|
||||
if (key == "Blue") return DecomposeBaseColor::Blue;
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static wxColour pure_color_for_base(DecomposeBaseColor base)
|
||||
{
|
||||
switch (base) {
|
||||
case DecomposeBaseColor::Cyan: return CMYW_CYAN;
|
||||
case DecomposeBaseColor::Magenta: return CMYW_MAGENTA;
|
||||
case DecomposeBaseColor::Yellow: return CMYW_YELLOW;
|
||||
case DecomposeBaseColor::White: return CMYW_WHITE;
|
||||
case DecomposeBaseColor::Red: return RYBW_RED;
|
||||
case DecomposeBaseColor::Blue: return RYBW_BLUE;
|
||||
default: return *wxBLACK;
|
||||
}
|
||||
}
|
||||
|
||||
static DecomposeBaseColor standard_base_color_for(DecomposeMode mode, const wxColour& color)
|
||||
{
|
||||
if (mode == DecomposeMode::CMYW) {
|
||||
if (same_rgb(color, CMYW_CYAN)) return DecomposeBaseColor::Cyan;
|
||||
if (same_rgb(color, CMYW_MAGENTA)) return DecomposeBaseColor::Magenta;
|
||||
if (same_rgb(color, CMYW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, CMYW_WHITE)) return DecomposeBaseColor::White;
|
||||
} else if (mode == DecomposeMode::RYBW) {
|
||||
if (same_rgb(color, RYBW_RED)) return DecomposeBaseColor::Red;
|
||||
if (same_rgb(color, RYBW_YELLOW)) return DecomposeBaseColor::Yellow;
|
||||
if (same_rgb(color, RYBW_BLUE)) return DecomposeBaseColor::Blue;
|
||||
if (same_rgb(color, RYBW_WHITE)) return DecomposeBaseColor::White;
|
||||
}
|
||||
return DecomposeBaseColor::None;
|
||||
}
|
||||
|
||||
static ColorDecomposeResult to_dialog_result(const ColorDecomposeRecipeResult& recipe,
|
||||
const wxColour& fallback)
|
||||
{
|
||||
ColorDecomposeResult result;
|
||||
result.mode = recipe.mode;
|
||||
result.matched_color = hex_to_wx_colour(recipe.matched_color_hex, fallback);
|
||||
for (const auto& comp_recipe : recipe.components) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = hex_to_wx_colour(comp_recipe.color_hex, fallback);
|
||||
comp.ratio = comp_recipe.ratio;
|
||||
comp.filament_index = static_cast<int>(comp_recipe.filament_index);
|
||||
comp.base_color = standard_base_color_from_key(comp_recipe.base_color);
|
||||
if (comp.base_color == DecomposeBaseColor::None)
|
||||
comp.base_color = standard_base_color_for(recipe.mode, comp.colour);
|
||||
result.components.push_back(comp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static wxPanel* create_h_divider(wxWindow* parent, int fixed_width = -1)
|
||||
{
|
||||
const int h = parent->FromDIP(1);
|
||||
int w = fixed_width > 0 ? fixed_width : -1;
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(w, h));
|
||||
panel->SetMinSize(wxSize(w, h));
|
||||
if (fixed_width > 0)
|
||||
panel->SetMaxSize(wxSize(fixed_width, h));
|
||||
panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_DIVIDER));
|
||||
return panel;
|
||||
}
|
||||
|
||||
static wxStaticText* create_mode_group_label(wxWindow* parent, const wxString& text)
|
||||
{
|
||||
auto* label = new wxStaticText(parent, wxID_ANY, text);
|
||||
label->SetFont(Label::Body_11);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_LABEL_GREY));
|
||||
return label;
|
||||
}
|
||||
|
||||
static void match_parent_bg(wxWindow* w, const wxColour& bg)
|
||||
{
|
||||
w->SetBackgroundColour(bg);
|
||||
}
|
||||
|
||||
static bool material_type_matches(const std::string& a, const std::string& b)
|
||||
{
|
||||
if (a.empty() || b.empty())
|
||||
return false;
|
||||
return a == b || a == b + " Basic" || b == a + " Basic";
|
||||
}
|
||||
|
||||
|
||||
ColorDecomposeDialog::ColorDecomposeDialog(wxWindow* parent,
|
||||
int filament_idx,
|
||||
const wxColour& target_color,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& filament_names,
|
||||
const std::vector<std::string>& filament_types,
|
||||
size_t current_filament_count,
|
||||
size_t max_filament_count,
|
||||
std::vector<size_t> physical_config_indices)
|
||||
: DPIDialog(parent, wxID_ANY, _L("Decompose Color"), wxDefaultPosition,
|
||||
wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_filament_idx(filament_idx)
|
||||
, m_target_color(target_color)
|
||||
, m_physical_colors(physical_colors)
|
||||
, m_filament_names(filament_names)
|
||||
, m_filament_types(filament_types)
|
||||
, m_current_filament_count(current_filament_count)
|
||||
, m_max_filament_count(max_filament_count)
|
||||
, m_physical_config_indices(std::move(physical_config_indices))
|
||||
{
|
||||
for (const auto& t : m_filament_types) {
|
||||
if (std::find(m_project_types.begin(), m_project_types.end(), t) == m_project_types.end())
|
||||
m_project_types.push_back(t);
|
||||
}
|
||||
|
||||
if (m_filament_idx >= 0 && static_cast<size_t>(m_filament_idx) < m_filament_types.size())
|
||||
m_preferred_type = m_filament_types[m_filament_idx];
|
||||
else if (!m_project_types.empty())
|
||||
m_preferred_type = m_project_types.front();
|
||||
|
||||
build_ui();
|
||||
wxGetApp().UpdateDlgDarkUI(this);
|
||||
// Restore target swatch after dark mode color remapping
|
||||
if (m_target_swatch)
|
||||
m_target_swatch->SetBackgroundColour(m_target_color);
|
||||
|
||||
update_card_visibility();
|
||||
Fit();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
{
|
||||
(void)suggested_rect;
|
||||
Fit();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::build_ui()
|
||||
{
|
||||
SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
|
||||
auto* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
const int selector_side_margin = FromDIP(26);
|
||||
const int selector_top_gap = FromDIP(22);
|
||||
const int content_side_margin = FromDIP(30);
|
||||
const int target_section_top_gap = FromDIP(18);
|
||||
|
||||
main_sizer->AddSpacer(selector_top_gap);
|
||||
main_sizer->Add(create_filament_selector(), 0, wxEXPAND | wxLEFT | wxRIGHT, selector_side_margin);
|
||||
main_sizer->AddSpacer(target_section_top_gap);
|
||||
main_sizer->Add(create_target_color_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_h_divider(this), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_mode_selection_section(), 0, wxEXPAND | wxLEFT | wxRIGHT, content_side_margin);
|
||||
main_sizer->AddSpacer(FromDIP(16));
|
||||
main_sizer->Add(create_button_panel(), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, content_side_margin);
|
||||
|
||||
SetSizer(main_sizer);
|
||||
SetMinSize(wxSize(FromDIP(477), FromDIP(380)));
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_filament_selector()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_type_combo = new ComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
|
||||
wxSize(-1, FromDIP(36)), 0, nullptr, wxCB_READONLY);
|
||||
m_type_combo->SetFont(Label::Body_13);
|
||||
|
||||
m_combo_item_types.clear();
|
||||
int default_sel = -1;
|
||||
|
||||
// --- Group 1: Project filament list (deduplicated by type) ---
|
||||
m_type_combo->Append(_L("Project Filament List"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
std::set<std::string> seen_types;
|
||||
for (size_t i = 0; i < m_filament_names.size(); ++i) {
|
||||
const std::string& type = (i < m_filament_types.size()) ? m_filament_types[i] : "PLA";
|
||||
if (!seen_types.insert(type).second)
|
||||
continue;
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(m_filament_names[i]));
|
||||
m_combo_item_types.push_back(type);
|
||||
if (type == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
// --- Group 2: Standard mode material recommendations ---
|
||||
static const char* kStandardTypes[] = {
|
||||
kDecomposePlaBasicType
|
||||
};
|
||||
|
||||
m_type_combo->Append(_L("Standard Mode Recommendations"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
|
||||
m_combo_item_types.push_back(std::string());
|
||||
|
||||
for (size_t s = 0; s < sizeof(kStandardTypes) / sizeof(kStandardTypes[0]); ++s) {
|
||||
// Always show standard recommendations, even if the same type already
|
||||
// appears in the project filament list above.
|
||||
const std::string label = std::string(kDecomposeBambuPresetPrefix) + kStandardTypes[s];
|
||||
int idx = m_type_combo->Append(wxString::FromUTF8(label));
|
||||
m_combo_item_types.push_back(kStandardTypes[s]);
|
||||
if (kStandardTypes[s] == m_preferred_type && default_sel < 0)
|
||||
default_sel = idx;
|
||||
}
|
||||
|
||||
if (default_sel < 0) {
|
||||
for (int i = 0; i < static_cast<int>(m_combo_item_types.size()); ++i) {
|
||||
if (!m_combo_item_types[i].empty()) {
|
||||
default_sel = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (default_sel >= 0) {
|
||||
m_type_combo->SetSelection(default_sel);
|
||||
if (!m_combo_item_types[default_sel].empty())
|
||||
m_preferred_type = m_combo_item_types[default_sel];
|
||||
}
|
||||
|
||||
m_type_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& evt) {
|
||||
evt.StopPropagation();
|
||||
int sel = m_type_combo->GetSelection();
|
||||
if (sel >= 0 && static_cast<size_t>(sel) < m_combo_item_types.size()
|
||||
&& !m_combo_item_types[sel].empty()) {
|
||||
m_preferred_type = m_combo_item_types[sel];
|
||||
}
|
||||
update_card_visibility();
|
||||
compute_decomposition();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
});
|
||||
|
||||
sizer->Add(m_type_combo, 1, wxEXPAND);
|
||||
return sizer;
|
||||
}
|
||||
|
||||
static wxPanel* create_color_swatch(wxWindow* parent, const wxColour& color, int size)
|
||||
{
|
||||
auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(size, size));
|
||||
panel->SetBackgroundColour(color);
|
||||
panel->SetMinSize(wxSize(size, size));
|
||||
return panel;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_target_color_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto* label = new wxStaticText(this, wxID_ANY, _L("Target Color"));
|
||||
label->SetFont(Label::Head_14);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(19));
|
||||
|
||||
m_target_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_target_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_target_rgb_text->SetFont(Label::Body_13);
|
||||
m_target_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_target_rgb_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
auto* arrow_text = new wxStaticText(this, wxID_ANY, wxString::FromUTF8("\xe2\x86\x92"));
|
||||
arrow_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(arrow_text, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_swatch = create_color_swatch(this, m_target_color, FromDIP(28));
|
||||
sizer->Add(m_matched_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(12));
|
||||
|
||||
m_matched_rgb_text = new wxStaticText(this, wxID_ANY,
|
||||
wxString::Format("RGB: %d, %d, %d", m_target_color.Red(), m_target_color.Green(), m_target_color.Blue()));
|
||||
m_matched_rgb_text->SetFont(Label::Head_13);
|
||||
m_matched_rgb_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(m_matched_rgb_text, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxPanel* ColorDecomposeDialog::create_mode_card(wxWindow* parent, DecomposeMode mode,
|
||||
const wxString& title)
|
||||
{
|
||||
const int pad = FromDIP(12);
|
||||
|
||||
auto* card = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
||||
card->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
|
||||
auto* card_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* title_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* title_label = new wxStaticText(card, wxID_ANY, title);
|
||||
title_label->SetFont(Label::Body_14);
|
||||
title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#909090")));
|
||||
match_parent_bg(title_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
title_sizer->Add(title_label, 1, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
auto* chk = new ::CheckBox(card);
|
||||
chk->SetValue(mode == m_selected_mode);
|
||||
match_parent_bg(chk, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
switch (mode) {
|
||||
case DecomposeMode::MaterialList: m_chk_material_list = chk; break;
|
||||
case DecomposeMode::CMYW: m_chk_cmyw = chk; break;
|
||||
case DecomposeMode::RYBW: m_chk_rybw = chk; break;
|
||||
}
|
||||
chk->Bind(wxEVT_TOGGLEBUTTON, [this, mode](wxCommandEvent& e) {
|
||||
select_mode(mode);
|
||||
e.Skip(); // let CheckBox::update() re-sync its bitmap to GetValue()
|
||||
});
|
||||
title_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL);
|
||||
|
||||
card_sizer->Add(title_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, pad);
|
||||
|
||||
card_sizer->Add(create_h_divider(card), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(8));
|
||||
|
||||
auto* colors_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
card_sizer->Add(colors_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
||||
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
controls.card = card;
|
||||
controls.components_sizer = colors_sizer;
|
||||
|
||||
card->SetSizer(card_sizer);
|
||||
card->SetMinSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(FromDIP(128), FromDIP(111)));
|
||||
|
||||
card->Bind(wxEVT_PAINT, [this, card, mode](wxPaintEvent&) {
|
||||
wxBufferedPaintDC dc(card);
|
||||
wxSize sz = card->GetClientSize();
|
||||
dc.SetBackground(wxBrush(StateColor::darkModeColorFor(*wxWHITE)));
|
||||
dc.Clear();
|
||||
|
||||
bool selected = (m_selected_mode == mode);
|
||||
wxColour border_col = selected
|
||||
? StateColor::darkModeColorFor(COLOR_BRAND)
|
||||
: StateColor::darkModeColorFor(COLOR_BORDER_NORMAL);
|
||||
const int border_width = FromDIP(selected ? 2 : 1);
|
||||
const double inset = border_width / 2.0;
|
||||
std::unique_ptr<wxGraphicsContext> gc(wxGraphicsContext::Create(dc));
|
||||
if (gc) {
|
||||
gc->SetPen(wxPen(border_col, border_width));
|
||||
gc->SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
gc->DrawRoundedRectangle(inset, inset, sz.x - 2 * inset, sz.y - 2 * inset, FromDIP(8));
|
||||
} else {
|
||||
const int fallback_inset = (border_width + 1) / 2;
|
||||
dc.SetPen(wxPen(border_col, border_width));
|
||||
dc.SetBrush(wxBrush(StateColor::darkModeColorFor(COLOR_BG_CARD)));
|
||||
dc.DrawRoundedRectangle(fallback_inset, fallback_inset, sz.x - 2 * fallback_inset, sz.y - 2 * fallback_inset, FromDIP(8));
|
||||
}
|
||||
});
|
||||
|
||||
std::function<void(wxWindow*)> bind_click;
|
||||
bind_click = [this, mode, chk, &bind_click](wxWindow* w) {
|
||||
if (w == chk || dynamic_cast<::CheckBox*>(w))
|
||||
return;
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
for (auto* child : w->GetChildren())
|
||||
bind_click(child);
|
||||
};
|
||||
bind_click(card);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_mode_selection_section()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
|
||||
auto* section_label = new wxStaticText(this, wxID_ANY, _L("Select Color Decomposition"));
|
||||
section_label->SetFont(Label::Head_14);
|
||||
section_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
sizer->Add(section_label, 0, wxBOTTOM, FromDIP(4));
|
||||
|
||||
auto* modes_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// --- Arbitrary mode column (wrapped in a panel so the whole column hides together) ---
|
||||
m_arb_column_panel = new wxPanel(this, wxID_ANY);
|
||||
m_arb_column_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* arb_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* arb_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
arb_header_sizer->Add(create_mode_group_label(m_arb_column_panel, _L("Arbitrary Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
arb_header_sizer->Add(create_h_divider(m_arb_column_panel, FromDIP(88)), 0, wxALIGN_CENTER_VERTICAL);
|
||||
arb_col->Add(arb_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
m_card_material_list = create_mode_card(m_arb_column_panel, DecomposeMode::MaterialList,
|
||||
_L("Material List"));
|
||||
arb_col->Add(m_card_material_list, 0, wxEXPAND);
|
||||
}
|
||||
m_arb_column_panel->SetSizer(arb_col);
|
||||
modes_sizer->Add(m_arb_column_panel, 0, wxEXPAND | wxRIGHT, FromDIP(16));
|
||||
|
||||
// --- Standard mode column ---
|
||||
auto* std_col = new wxBoxSizer(wxVERTICAL);
|
||||
{
|
||||
auto* std_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
std_header_sizer->Add(create_mode_group_label(this, _L("Standard Mode")),
|
||||
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5));
|
||||
std_header_sizer->Add(create_h_divider(this), 1, wxALIGN_CENTER_VERTICAL);
|
||||
std_col->Add(std_header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
||||
|
||||
auto* cards_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
m_card_cmyw = create_mode_card(this, DecomposeMode::CMYW, "CMYW");
|
||||
cards_sizer->Add(m_card_cmyw, 0, wxRIGHT, FromDIP(12));
|
||||
|
||||
m_card_rybw = create_mode_card(this, DecomposeMode::RYBW, "RYBW");
|
||||
cards_sizer->Add(m_card_rybw, 0);
|
||||
|
||||
std_col->Add(cards_sizer, 0, wxEXPAND);
|
||||
}
|
||||
modes_sizer->Add(std_col, 0, wxEXPAND);
|
||||
|
||||
sizer->Add(modes_sizer, 0, wxEXPAND);
|
||||
|
||||
m_no_card_hint = new wxStaticText(this, wxID_ANY,
|
||||
_L("At least two filaments of the same material type are required for decomposition"));
|
||||
m_no_card_hint->SetFont(Label::Body_13);
|
||||
m_no_card_hint->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#909090")));
|
||||
m_no_card_hint->Wrap(FromDIP(400));
|
||||
m_no_card_hint->Hide();
|
||||
sizer->Add(m_no_card_hint, 0, wxTOP, FromDIP(8));
|
||||
|
||||
m_limit_warning_panel = new wxPanel(this, wxID_ANY);
|
||||
m_limit_warning_panel->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
||||
auto* warning_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto* warn_bmp = new wxStaticBitmap(m_limit_warning_panel, wxID_ANY,
|
||||
create_scaled_bitmap("obj_warning", m_limit_warning_panel, 16),
|
||||
wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16)));
|
||||
m_limit_warning_text = new wxStaticText(m_limit_warning_panel, wxID_ANY, wxEmptyString);
|
||||
m_limit_warning_text->SetFont(Label::Body_13);
|
||||
m_limit_warning_text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D32F2F")));
|
||||
m_limit_warning_text->Wrap(FromDIP(400));
|
||||
warning_sizer->Add(warn_bmp, 0, wxALIGN_TOP | wxRIGHT, FromDIP(6));
|
||||
warning_sizer->Add(m_limit_warning_text, 1, wxEXPAND);
|
||||
m_limit_warning_panel->SetSizer(warning_sizer);
|
||||
m_limit_warning_panel->Hide();
|
||||
sizer->Add(m_limit_warning_panel, 0, wxEXPAND | wxTOP, FromDIP(8));
|
||||
|
||||
return sizer;
|
||||
}
|
||||
|
||||
wxBoxSizer* ColorDecomposeDialog::create_button_panel()
|
||||
{
|
||||
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
sizer->AddStretchSpacer();
|
||||
|
||||
m_btn_cancel = new Button(this, _L("Cancel"));
|
||||
m_btn_cancel->SetBackgroundColor(StateColor::darkModeColorFor(*wxWHITE));
|
||||
m_btn_cancel->SetBorderColor(StateColor::darkModeColorFor(wxColour("#CECECE")));
|
||||
m_btn_cancel->SetTextColor(StateColor::darkModeColorFor(wxColour("#262E30")));
|
||||
m_btn_cancel->SetMinSize(wxSize(FromDIP(55), FromDIP(24)));
|
||||
m_btn_cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
|
||||
|
||||
m_btn_ok = new Button(this, _L("OK"));
|
||||
m_btn_ok->SetBackgroundColor(StateColor(
|
||||
std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled),
|
||||
std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal)));
|
||||
m_btn_ok->SetBorderColor(StateColor(
|
||||
std::make_pair(wxColour("#C2C2C2"), (int) StateColor::Disabled),
|
||||
std::make_pair(wxColour("#00AE42"), (int) StateColor::Normal)));
|
||||
m_btn_ok->SetTextColor(StateColor(
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Disabled),
|
||||
std::make_pair(*wxWHITE, (int) StateColor::Normal)));
|
||||
m_btn_ok->SetMinSize(wxSize(FromDIP(55), FromDIP(24)));
|
||||
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 ColorDecomposeDialog::select_mode(DecomposeMode mode)
|
||||
{
|
||||
m_selected_mode = mode;
|
||||
m_result = m_mode_results[mode_index(mode)];
|
||||
update_card_styles();
|
||||
update_matched_color_display();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_styles()
|
||||
{
|
||||
if (m_card_material_list) m_card_material_list->Refresh();
|
||||
if (m_card_cmyw) m_card_cmyw->Refresh();
|
||||
if (m_card_rybw) m_card_rybw->Refresh();
|
||||
|
||||
if (m_chk_material_list)
|
||||
m_chk_material_list->SetValue(m_selected_mode == DecomposeMode::MaterialList);
|
||||
if (m_chk_cmyw)
|
||||
m_chk_cmyw->SetValue(m_selected_mode == DecomposeMode::CMYW);
|
||||
if (m_chk_rybw)
|
||||
m_chk_rybw->SetValue(m_selected_mode == DecomposeMode::RYBW);
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_card_visibility()
|
||||
{
|
||||
// Count physical filaments of the same type (excluding the source filament)
|
||||
int same_type_count = 0;
|
||||
for (size_t i = 0; i < m_filament_types.size(); ++i) {
|
||||
if (static_cast<int>(i) == m_filament_idx)
|
||||
continue;
|
||||
if (material_type_matches(m_filament_types[i], m_preferred_type))
|
||||
++same_type_count;
|
||||
}
|
||||
|
||||
bool show_arb = (same_type_count >= 2);
|
||||
bool show_cmyw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
bool show_rybw = (m_preferred_type == kDecomposePlaBasicType);
|
||||
|
||||
if (m_arb_column_panel) m_arb_column_panel->Show(show_arb);
|
||||
if (m_card_material_list) m_card_material_list->Show(show_arb);
|
||||
if (m_card_cmyw) m_card_cmyw->Show(show_cmyw);
|
||||
if (m_card_rybw) m_card_rybw->Show(show_rybw);
|
||||
|
||||
bool any_visible = show_arb || show_cmyw || show_rybw;
|
||||
if (m_no_card_hint)
|
||||
m_no_card_hint->Show(!any_visible);
|
||||
|
||||
// Auto-select a visible mode when current selection becomes hidden
|
||||
if (any_visible) {
|
||||
bool cur_visible = false;
|
||||
if (m_selected_mode == DecomposeMode::MaterialList && show_arb) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::CMYW && show_cmyw) cur_visible = true;
|
||||
if (m_selected_mode == DecomposeMode::RYBW && show_rybw) cur_visible = true;
|
||||
if (!cur_visible) {
|
||||
if (show_arb) select_mode(DecomposeMode::MaterialList);
|
||||
else if (show_cmyw) select_mode(DecomposeMode::CMYW);
|
||||
else select_mode(DecomposeMode::RYBW);
|
||||
}
|
||||
}
|
||||
|
||||
Layout();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_filament_limit_warning()
|
||||
{
|
||||
if (!m_limit_warning_panel || !m_limit_warning_text)
|
||||
return;
|
||||
|
||||
size_t missing_new = 0;
|
||||
if (m_missing_calculator) {
|
||||
missing_new = m_missing_calculator(m_result);
|
||||
} else {
|
||||
const size_t source_physical_idx = m_filament_idx >= 0 ? static_cast<size_t>(m_filament_idx) : size_t(-1);
|
||||
const std::vector<size_t>* indices =
|
||||
m_physical_config_indices.empty() ? nullptr : &m_physical_config_indices;
|
||||
missing_new = count_decompose_new_physical_filaments(
|
||||
m_result, m_physical_colors, m_filament_types, source_physical_idx, indices);
|
||||
}
|
||||
// A result with fewer than 2 components (e.g. target color is already a
|
||||
// standard base color shown as "100%") creates no mixed filament and no new
|
||||
// physical filament, so it can never exceed the limit.
|
||||
const bool creates_mixed = m_result.components.size() >= 2;
|
||||
// +1 for the mixed filament slot that will be created after decomposition.
|
||||
const size_t needed = m_current_filament_count + missing_new + 1;
|
||||
const bool blocked = creates_mixed && needed > m_max_filament_count;
|
||||
|
||||
const bool was_shown = m_limit_warning_panel->IsShown();
|
||||
|
||||
if (!blocked) {
|
||||
if (was_shown) {
|
||||
m_limit_warning_panel->Hide();
|
||||
Layout();
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
wxString mode_name;
|
||||
switch (m_selected_mode) {
|
||||
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
|
||||
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
|
||||
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
|
||||
}
|
||||
|
||||
const wxString warning_text = format_wxstr(
|
||||
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
|
||||
m_max_filament_count, mode_name);
|
||||
|
||||
// Show first so the panel is laid out and the text control gets its real
|
||||
// width, then wrap to that width so the paragraph fills the content area.
|
||||
m_limit_warning_panel->Show();
|
||||
Layout();
|
||||
const int avail = m_limit_warning_text->GetClientSize().x;
|
||||
m_limit_warning_text->SetLabel(warning_text);
|
||||
if (avail > FromDIP(50))
|
||||
m_limit_warning_text->Wrap(avail);
|
||||
|
||||
Layout();
|
||||
// Only resize/recenter when the warning panel actually toggled from hidden
|
||||
// to shown. While already visible, switching modes must not re-Fit/recenter
|
||||
// the dialog, which would make it jump on every card switch.
|
||||
if (!was_shown) {
|
||||
Fit();
|
||||
CenterOnParent();
|
||||
}
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn)
|
||||
{
|
||||
m_missing_calculator = std::move(fn);
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_ok_button_state()
|
||||
{
|
||||
if (!m_btn_ok) return;
|
||||
update_filament_limit_warning();
|
||||
bool any_card_visible = (m_card_material_list && m_card_material_list->IsShown())
|
||||
|| (m_card_cmyw && m_card_cmyw->IsShown())
|
||||
|| (m_card_rybw && m_card_rybw->IsShown());
|
||||
const bool blocked = m_limit_warning_panel && m_limit_warning_panel->IsShown();
|
||||
m_btn_ok->Enable(any_card_visible && !blocked);
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_content(DecomposeMode mode)
|
||||
{
|
||||
auto& controls = m_mode_cards[mode_index(mode)];
|
||||
auto* sizer = controls.components_sizer;
|
||||
auto* card = controls.card;
|
||||
if (!sizer || !card)
|
||||
return;
|
||||
|
||||
sizer->Clear(true);
|
||||
const auto& components = m_mode_results[mode_index(mode)].components;
|
||||
const size_t count = components.size();
|
||||
if (count == 0) {
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const int swatch_sz = FromDIP(24);
|
||||
const int plus_gap = FromDIP(24);
|
||||
const wxFont& ratio_font = Label::Body_13;
|
||||
auto bind_select = [this, mode](wxWindow* w) {
|
||||
w->Bind(wxEVT_LEFT_UP, [this, mode](wxMouseEvent&) {
|
||||
select_mode(mode);
|
||||
});
|
||||
w->SetCursor(wxCursor(wxCURSOR_HAND));
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
auto* col = new wxBoxSizer(wxVERTICAL);
|
||||
auto* swatch = create_color_swatch(card, components[i].colour, swatch_sz);
|
||||
bind_select(swatch);
|
||||
col->Add(swatch, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
auto* ratio_text = new wxStaticText(card, wxID_ANY, wxString::Format("%d%%", components[i].ratio));
|
||||
ratio_text->SetFont(ratio_font);
|
||||
ratio_text->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(ratio_text, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(ratio_text);
|
||||
col->Add(ratio_text, 0, wxALIGN_CENTER_HORIZONTAL | wxTOP, FromDIP(4));
|
||||
sizer->Add(col, 0, wxALIGN_TOP);
|
||||
|
||||
if (i + 1 < count) {
|
||||
sizer->AddStretchSpacer();
|
||||
auto* plus_panel = new wxPanel(card, wxID_ANY, wxDefaultPosition, wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMinSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetMaxSize(wxSize(plus_gap, swatch_sz));
|
||||
plus_panel->SetBackgroundColour(StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
auto* plus_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
auto* plus_label = new wxStaticText(plus_panel, wxID_ANY, "+");
|
||||
plus_label->SetFont(Label::Body_13);
|
||||
plus_label->SetForegroundColour(StateColor::darkModeColorFor(COLOR_TEXT_DARK));
|
||||
match_parent_bg(plus_label, StateColor::darkModeColorFor(COLOR_BG_CARD));
|
||||
bind_select(plus_panel);
|
||||
bind_select(plus_label);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_sizer->Add(plus_label, 0, wxALIGN_CENTER_HORIZONTAL);
|
||||
plus_sizer->AddStretchSpacer();
|
||||
plus_panel->SetSizer(plus_sizer);
|
||||
sizer->Add(plus_panel, 0, wxALIGN_TOP);
|
||||
sizer->AddStretchSpacer();
|
||||
}
|
||||
}
|
||||
|
||||
const int card_width = FromDIP(128 + (count > 2 ? static_cast<int>(count - 2) * 31 : 0));
|
||||
card->SetMinSize(wxSize(card_width, FromDIP(111)));
|
||||
card->SetMaxSize(wxSize(card_width, FromDIP(111)));
|
||||
|
||||
card->Layout();
|
||||
card->Refresh();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_mode_card_contents()
|
||||
{
|
||||
update_mode_card_content(DecomposeMode::MaterialList);
|
||||
update_mode_card_content(DecomposeMode::CMYW);
|
||||
update_mode_card_content(DecomposeMode::RYBW);
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::update_matched_color_display()
|
||||
{
|
||||
if (!m_result.matched_color.IsOk())
|
||||
m_result.matched_color = m_target_color;
|
||||
|
||||
if (m_matched_swatch) {
|
||||
m_matched_swatch->SetBackgroundColour(m_result.matched_color);
|
||||
m_matched_swatch->Refresh();
|
||||
}
|
||||
if (m_matched_rgb_text) {
|
||||
m_matched_rgb_text->SetLabel(wxString::Format("RGB: %d, %d, %d",
|
||||
m_result.matched_color.Red(), m_result.matched_color.Green(), m_result.matched_color.Blue()));
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorDecomposeDialog::try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const
|
||||
{
|
||||
// Gate by preferred type, matching card visibility: CMYW and RYBW only for PLA Basic.
|
||||
if (mode == DecomposeMode::CMYW || mode == DecomposeMode::RYBW) {
|
||||
if (m_preferred_type != kDecomposePlaBasicType)
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
static const DecomposeBaseColor cmyw_bases[] = {
|
||||
DecomposeBaseColor::Cyan, DecomposeBaseColor::Magenta,
|
||||
DecomposeBaseColor::Yellow, DecomposeBaseColor::White
|
||||
};
|
||||
static const DecomposeBaseColor rybw_bases[] = {
|
||||
DecomposeBaseColor::Red, DecomposeBaseColor::Yellow,
|
||||
DecomposeBaseColor::Blue, DecomposeBaseColor::White
|
||||
};
|
||||
const DecomposeBaseColor* bases = (mode == DecomposeMode::CMYW) ? cmyw_bases : rybw_bases;
|
||||
const size_t base_count = (mode == DecomposeMode::CMYW)
|
||||
? sizeof(cmyw_bases) / sizeof(cmyw_bases[0])
|
||||
: sizeof(rybw_bases) / sizeof(rybw_bases[0]);
|
||||
|
||||
const std::string target_hex = decompose_normalize_color_hex(
|
||||
m_target_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
|
||||
for (size_t i = 0; i < base_count; ++i) {
|
||||
const DecomposeBaseColor base = bases[i];
|
||||
DecomposeOfficialComponent official =
|
||||
lookup_decompose_official_component(m_preferred_type, base, pure_color_for_base(base));
|
||||
if (decompose_normalize_color_hex(official.color_hex) != target_hex)
|
||||
continue;
|
||||
|
||||
out = ColorDecomposeResult{};
|
||||
out.mode = mode;
|
||||
out.matched_color = hex_to_wx_colour(official.color_hex, m_target_color);
|
||||
DecomposeComponent comp;
|
||||
comp.colour = out.matched_color;
|
||||
comp.ratio = 100;
|
||||
comp.filament_index = -1;
|
||||
comp.base_color = base;
|
||||
out.components.push_back(comp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ColorDecomposeDialog::compute_decomposition()
|
||||
{
|
||||
auto fallback_result = [this](DecomposeMode mode, const std::vector<DecomposeComponent>& components) {
|
||||
ColorDecomposeResult result;
|
||||
result.mode = mode;
|
||||
result.components = components;
|
||||
int total = 0;
|
||||
double r = 0.0, g = 0.0, b = 0.0;
|
||||
for (const auto& comp : result.components)
|
||||
total += comp.ratio;
|
||||
if (total <= 0)
|
||||
total = 100;
|
||||
for (const auto& comp : result.components) {
|
||||
const double w = static_cast<double>(comp.ratio) / total;
|
||||
r += comp.colour.Red() * w;
|
||||
g += comp.colour.Green() * w;
|
||||
b += comp.colour.Blue() * w;
|
||||
}
|
||||
result.matched_color = result.components.empty()
|
||||
? m_target_color
|
||||
: wxColour(static_cast<unsigned char>(std::clamp(r, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(g, 0.0, 255.0)),
|
||||
static_cast<unsigned char>(std::clamp(b, 0.0, 255.0)));
|
||||
return result;
|
||||
};
|
||||
|
||||
std::vector<ColorDecomposePhysicalFilament> physical_filaments;
|
||||
physical_filaments.reserve(m_physical_colors.size());
|
||||
for (size_t i = 0; i < m_physical_colors.size(); ++i) {
|
||||
if (m_filament_idx >= 0 && i == static_cast<size_t>(m_filament_idx))
|
||||
continue;
|
||||
ColorDecomposePhysicalFilament filament;
|
||||
filament.color_hex = m_physical_colors[i];
|
||||
filament.name = i < m_filament_names.size() ? m_filament_names[i] : "";
|
||||
filament.type = i < m_filament_types.size() ? m_filament_types[i] : "";
|
||||
filament.filament_index = static_cast<unsigned int>(i + 1);
|
||||
physical_filaments.push_back(std::move(filament));
|
||||
}
|
||||
|
||||
const ColorDecomposeRgb target_rgb = wx_colour_to_recipe_rgb(m_target_color);
|
||||
|
||||
auto material_recipe = recommend_from_physical_filaments(target_rgb, physical_filaments, m_preferred_type);
|
||||
if (material_recipe.valid) {
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
to_dialog_result(material_recipe, m_target_color);
|
||||
} else {
|
||||
std::vector<DecomposeComponent> components;
|
||||
for (size_t i = 0; i < std::min<size_t>(2, physical_filaments.size()); ++i) {
|
||||
DecomposeComponent comp;
|
||||
comp.colour = wxColour(physical_filaments[i].color_hex);
|
||||
comp.ratio = 50;
|
||||
comp.filament_index = static_cast<int>(physical_filaments[i].filament_index);
|
||||
components.push_back(comp);
|
||||
}
|
||||
if (components.empty()) {
|
||||
components.push_back({m_target_color, 100, -1});
|
||||
} else if (components.size() == 1) {
|
||||
components.front().ratio = 100;
|
||||
}
|
||||
m_mode_results[mode_index(DecomposeMode::MaterialList)] =
|
||||
fallback_result(DecomposeMode::MaterialList, components);
|
||||
}
|
||||
|
||||
ColorDecomposeResult single_base;
|
||||
if (try_build_single_base_result(DecomposeMode::CMYW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = single_base;
|
||||
} else {
|
||||
auto cmyw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::CMYW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::CMYW)] = cmyw_recipe.valid
|
||||
? to_dialog_result(cmyw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::CMYW, {
|
||||
{CMYW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{CMYW_CYAN, 50, -1, DecomposeBaseColor::Cyan}
|
||||
});
|
||||
}
|
||||
|
||||
if (try_build_single_base_result(DecomposeMode::RYBW, single_base)) {
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = single_base;
|
||||
} else {
|
||||
auto rybw_recipe = lookup_standard_recipe(target_rgb, ColorDecomposeRecipeMode::RYBW, m_preferred_type);
|
||||
m_mode_results[mode_index(DecomposeMode::RYBW)] = rybw_recipe.valid
|
||||
? to_dialog_result(rybw_recipe, m_target_color)
|
||||
: fallback_result(DecomposeMode::RYBW, {
|
||||
{RYBW_YELLOW, 50, -1, DecomposeBaseColor::Yellow},
|
||||
{RYBW_BLUE, 50, -1, DecomposeBaseColor::Blue}
|
||||
});
|
||||
}
|
||||
|
||||
m_result = m_mode_results[mode_index(m_selected_mode)];
|
||||
update_mode_card_contents();
|
||||
update_ok_button_state();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,152 @@
|
||||
#ifndef slic3r_ColorDecomposeDialog_hpp_
|
||||
#define slic3r_ColorDecomposeDialog_hpp_
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/statbmp.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
||||
|
||||
class Button;
|
||||
class CheckBox;
|
||||
class ComboBox;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
using DecomposeMode = ColorDecomposeRecipeMode;
|
||||
|
||||
enum class DecomposeBaseColor {
|
||||
None,
|
||||
Cyan,
|
||||
Magenta,
|
||||
Yellow,
|
||||
White,
|
||||
Red,
|
||||
Green,
|
||||
Blue
|
||||
};
|
||||
|
||||
struct DecomposeComponent {
|
||||
wxColour colour;
|
||||
int ratio{50}; // percentage
|
||||
int filament_index{-1}; // 1-based physical filament index, -1 if standard base color
|
||||
DecomposeBaseColor base_color{DecomposeBaseColor::None};
|
||||
};
|
||||
|
||||
struct ColorDecomposeResult {
|
||||
DecomposeMode mode{DecomposeMode::MaterialList};
|
||||
wxColour matched_color;
|
||||
std::vector<DecomposeComponent> components;
|
||||
};
|
||||
|
||||
class ColorDecomposeDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
ColorDecomposeDialog(wxWindow* parent,
|
||||
int filament_idx,
|
||||
const wxColour& target_color,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& filament_names,
|
||||
const std::vector<std::string>& filament_types,
|
||||
size_t current_filament_count = 0,
|
||||
size_t max_filament_count = 32,
|
||||
std::vector<size_t> physical_config_indices = {});
|
||||
|
||||
ColorDecomposeResult get_result() const { return m_result; }
|
||||
|
||||
// Override the "new physical filaments" count used by the filament-limit
|
||||
// warning. The Texture import path supplies its own calculator so the
|
||||
// pre-check shares the exact reuse rule as its write-back (existing +
|
||||
// virtual physical filaments), instead of the project-config based default
|
||||
// that cannot see not-yet-committed virtual base colors.
|
||||
void set_missing_physical_calculator(std::function<size_t(const ColorDecomposeResult&)> fn);
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
wxBoxSizer* create_filament_selector();
|
||||
wxBoxSizer* create_target_color_section();
|
||||
wxBoxSizer* create_mode_selection_section();
|
||||
wxPanel* create_mode_card(wxWindow* parent, DecomposeMode mode, const wxString& title);
|
||||
wxBoxSizer* create_button_panel();
|
||||
|
||||
void select_mode(DecomposeMode mode);
|
||||
void update_card_styles();
|
||||
void update_card_visibility();
|
||||
void update_mode_card_content(DecomposeMode mode);
|
||||
void update_mode_card_contents();
|
||||
void update_matched_color_display();
|
||||
void update_ok_button_state();
|
||||
void update_filament_limit_warning();
|
||||
|
||||
void compute_decomposition();
|
||||
|
||||
// When the target color is exactly one of the standard base colors for the
|
||||
// preferred type, the standard card should show that base at 100% instead of
|
||||
// a mix. PLA Basic covers CMYW and RYBW.
|
||||
bool try_build_single_base_result(DecomposeMode mode, ColorDecomposeResult& out) const;
|
||||
|
||||
struct ModeCardControls {
|
||||
wxPanel* card{nullptr};
|
||||
wxBoxSizer* components_sizer{nullptr};
|
||||
};
|
||||
|
||||
ColorDecomposeResult m_result;
|
||||
std::array<ColorDecomposeResult, 3> m_mode_results;
|
||||
std::array<ModeCardControls, 3> m_mode_cards;
|
||||
int m_filament_idx{-1};
|
||||
wxColour m_target_color;
|
||||
std::vector<std::string> m_physical_colors;
|
||||
std::vector<std::string> m_filament_names;
|
||||
std::vector<std::string> m_filament_types;
|
||||
std::vector<std::string> m_project_types;
|
||||
std::string m_preferred_type;
|
||||
// Dropdown selectable item index -> material type string
|
||||
std::vector<std::string> m_combo_item_types;
|
||||
size_t m_current_filament_count{0};
|
||||
size_t m_max_filament_count{32};
|
||||
std::vector<size_t> m_physical_config_indices;
|
||||
std::function<size_t(const ColorDecomposeResult&)> m_missing_calculator;
|
||||
|
||||
// UI controls
|
||||
ComboBox* m_type_combo{nullptr};
|
||||
wxPanel* m_target_swatch{nullptr};
|
||||
wxStaticText* m_target_rgb_text{nullptr};
|
||||
wxPanel* m_matched_swatch{nullptr};
|
||||
wxStaticText* m_matched_rgb_text{nullptr};
|
||||
|
||||
// Mode cards
|
||||
wxPanel* m_card_material_list{nullptr};
|
||||
wxPanel* m_card_cmyw{nullptr};
|
||||
wxPanel* m_card_rybw{nullptr};
|
||||
wxPanel* m_arb_column_panel{nullptr};
|
||||
CheckBox* m_chk_material_list{nullptr};
|
||||
CheckBox* m_chk_cmyw{nullptr};
|
||||
CheckBox* m_chk_rybw{nullptr};
|
||||
DecomposeMode m_selected_mode{DecomposeMode::MaterialList};
|
||||
|
||||
// Hint shown when no mode card is visible
|
||||
wxStaticText* m_no_card_hint{nullptr};
|
||||
|
||||
// Warning shown when decomposition would exceed filament limit
|
||||
wxPanel* m_limit_warning_panel{nullptr};
|
||||
wxStaticText* m_limit_warning_text{nullptr};
|
||||
|
||||
Button* m_btn_ok{nullptr};
|
||||
Button* m_btn_cancel{nullptr};
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_ColorDecomposeDialog_hpp_
|
||||
@@ -0,0 +1,386 @@
|
||||
#include "ColorDecomposeSupport.hpp"
|
||||
#include "MixedFilamentDialog.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
std::string decompose_normalize_color_hex(std::string color)
|
||||
{
|
||||
if (color.size() >= 7)
|
||||
color = color.substr(0, 7);
|
||||
std::transform(color.begin(), color.end(), color.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::toupper(c));
|
||||
});
|
||||
return color;
|
||||
}
|
||||
|
||||
const char* decompose_base_color_en(DecomposeBaseColor color)
|
||||
{
|
||||
switch (color) {
|
||||
case DecomposeBaseColor::Cyan: return "Cyan";
|
||||
case DecomposeBaseColor::Magenta: return "Magenta";
|
||||
case DecomposeBaseColor::Yellow: return "Yellow";
|
||||
case DecomposeBaseColor::White: return "White";
|
||||
case DecomposeBaseColor::Red: return "Red";
|
||||
case DecomposeBaseColor::Green: return "Green";
|
||||
case DecomposeBaseColor::Blue: return "Blue";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
wxString decompose_base_color_display(DecomposeBaseColor color)
|
||||
{
|
||||
switch (color) {
|
||||
case DecomposeBaseColor::Cyan: return _L("Cyan");
|
||||
case DecomposeBaseColor::Magenta: return _L("Magenta");
|
||||
case DecomposeBaseColor::Yellow: return _L("Yellow");
|
||||
case DecomposeBaseColor::White: return _L("White");
|
||||
case DecomposeBaseColor::Red: return _L("Red");
|
||||
case DecomposeBaseColor::Green: return _L("Green");
|
||||
case DecomposeBaseColor::Blue: return _L("Blue");
|
||||
default: return wxString();
|
||||
}
|
||||
}
|
||||
|
||||
std::string decompose_basic_type_from_source(size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_types)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
|
||||
if (source_config_idx < filament_id_opt->values.size()) {
|
||||
const std::string& filament_id = filament_id_opt->values[source_config_idx];
|
||||
if (filament_id == kDecomposePetgFilamentId)
|
||||
return kDecomposePetgBasicType;
|
||||
if (filament_id == kDecomposePlaFilamentId)
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
}
|
||||
|
||||
if (source_physical_idx < physical_types.size()) {
|
||||
const std::string& type = physical_types[source_physical_idx];
|
||||
if (type == kDecomposePetgShortType || type == kDecomposePetgBasicType)
|
||||
return kDecomposePetgBasicType;
|
||||
if (type == kDecomposePlaShortType || type == kDecomposePlaBasicType)
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
return kDecomposePlaBasicType;
|
||||
}
|
||||
|
||||
std::string decompose_basic_filament_id(const std::string& basic_type)
|
||||
{
|
||||
if (basic_type == kDecomposePetgBasicType)
|
||||
return kDecomposePetgFilamentId;
|
||||
return kDecomposePlaFilamentId;
|
||||
}
|
||||
|
||||
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
if (!component.filament_id.empty()) {
|
||||
if (auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id")) {
|
||||
while (filament_id_opt->values.size() <= config_idx)
|
||||
filament_id_opt->values.push_back("");
|
||||
filament_id_opt->values[config_idx] = component.filament_id;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType :
|
||||
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : "";
|
||||
if (!type.empty()) {
|
||||
if (auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type")) {
|
||||
while (type_opt->values.size() <= config_idx)
|
||||
type_opt->values.push_back("");
|
||||
type_opt->values[config_idx] = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DecomposeOfficialComponent lookup_decompose_official_component(
|
||||
const std::string& basic_type,
|
||||
DecomposeBaseColor base_color,
|
||||
const wxColour& fallback)
|
||||
{
|
||||
DecomposeOfficialComponent result;
|
||||
result.base_color = base_color;
|
||||
result.color_hex = decompose_normalize_color_hex(fallback.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
||||
result.filament_id = decompose_basic_filament_id(basic_type);
|
||||
|
||||
const char* color_name = decompose_base_color_en(base_color);
|
||||
if (color_name[0] == '\0')
|
||||
return result;
|
||||
|
||||
// Some materials name a standard base color differently in the color-code
|
||||
// table. PETG Basic's RYBW blue base is "Reflex Blue" (deep blue, B00,
|
||||
// #001489), not "Blue". Match by an ordered list of exact English names so
|
||||
// "Navy Blue" (B01, #0086D6) is never picked up by mistake.
|
||||
std::vector<std::string> candidate_names;
|
||||
candidate_names.emplace_back(color_name);
|
||||
if (base_color == DecomposeBaseColor::Blue && basic_type == kDecomposePetgBasicType)
|
||||
candidate_names.emplace_back("Reflex Blue");
|
||||
|
||||
std::ifstream ifs(resources_dir() + "/profiles/BBL/filament/filaments_color_codes.json");
|
||||
if (!ifs)
|
||||
return result;
|
||||
|
||||
json root = json::parse(ifs, nullptr, false);
|
||||
if (root.is_discarded() || !root.contains("data") || !root["data"].is_array())
|
||||
return result;
|
||||
|
||||
for (const std::string& candidate : candidate_names) {
|
||||
for (const auto& item : root["data"]) {
|
||||
if (!item.is_object() || item.value("fila_type", "") != basic_type)
|
||||
continue;
|
||||
if (!item.contains("fila_color_name"))
|
||||
continue;
|
||||
const auto& names = item["fila_color_name"];
|
||||
if (!names.is_object() || names.value("en", "") != candidate)
|
||||
continue;
|
||||
if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty())
|
||||
result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get<std::string>());
|
||||
result.filament_id = item.value("fila_id", result.filament_id);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type)
|
||||
{
|
||||
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
if (source_config_idx < preset_bundle.filament_presets.size()) {
|
||||
const std::string& source_name = preset_bundle.filament_presets[source_config_idx];
|
||||
if (source_name.find(std::string(kDecomposeBambuPresetPrefix) + basic_type) != std::string::npos)
|
||||
return source_name;
|
||||
}
|
||||
|
||||
const std::string prefix = std::string(kDecomposeBambuPresetPrefix) + basic_type + " @BBL ";
|
||||
for (const std::string& preset_name : preset_bundle.filament_presets) {
|
||||
if (preset_name.find(prefix) == 0)
|
||||
return preset_name;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string official_basic_type_from_preset_name(const std::string& preset_name)
|
||||
{
|
||||
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePlaBasicType) != std::string::npos)
|
||||
return kDecomposePlaBasicType;
|
||||
if (preset_name.find(std::string(kDecomposeBambuPresetPrefix) + kDecomposePetgBasicType) != std::string::npos)
|
||||
return kDecomposePetgBasicType;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string filament_type_for_color_decompose(Preset* preset)
|
||||
{
|
||||
if (!preset)
|
||||
return kDecomposePlaShortType;
|
||||
|
||||
std::string display_type;
|
||||
std::string ft = preset->config.get_filament_type(display_type);
|
||||
const std::string basic = official_basic_type_from_preset_name(preset->name);
|
||||
if (!basic.empty())
|
||||
ft = basic;
|
||||
if (ft.empty())
|
||||
ft = kDecomposePlaShortType;
|
||||
return ft;
|
||||
}
|
||||
|
||||
int find_existing_decompose_component(
|
||||
const DecomposeOfficialComponent& component,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
size_t source_config_idx)
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* filament_id_opt = project_config.option<ConfigOptionStrings>("filament_id");
|
||||
auto* type_opt = project_config.option<ConfigOptionStrings>("filament_type");
|
||||
const PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
|
||||
const size_t num_physical = physical_colors.size();
|
||||
const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType :
|
||||
component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : "";
|
||||
const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType :
|
||||
expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : "";
|
||||
const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type;
|
||||
for (size_t i = 0; i < num_physical && i < physical_config_indices.size(); ++i) {
|
||||
const size_t config_idx = physical_config_indices[i];
|
||||
const std::string slot_color = decompose_normalize_color_hex(physical_colors[i]);
|
||||
const std::string slot_filament_id = (filament_id_opt && config_idx < filament_id_opt->values.size()) ? filament_id_opt->values[config_idx] : "";
|
||||
const std::string slot_type = (type_opt && config_idx < type_opt->values.size()) ? type_opt->values[config_idx] : "";
|
||||
const std::string preset_name = config_idx < preset_bundle.filament_presets.size() ? preset_bundle.filament_presets[config_idx] : "";
|
||||
if (config_idx == source_config_idx) {
|
||||
continue;
|
||||
}
|
||||
if (slot_color != component.color_hex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!component.filament_id.empty() && slot_filament_id == component.filament_id) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
if (!expected_basic_type.empty() && (slot_type == expected_basic_type || slot_type == expected_short_type)) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
if (!expected_preset_part.empty() && preset_name.find(expected_preset_part) != std::string::npos) {
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
|
||||
const bool has_material_hint = !slot_filament_id.empty() || !slot_type.empty() || !preset_name.empty();
|
||||
if (!expected_basic_type.empty() && has_material_hint)
|
||||
continue;
|
||||
|
||||
return static_cast<int>(config_idx + 1);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool prepare_decompose_mixed_result(
|
||||
const ColorDecomposeResult& result,
|
||||
size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
MixedFilamentResult& out_result,
|
||||
std::vector<DecomposeMissingComponent>& missing)
|
||||
{
|
||||
out_result = {};
|
||||
missing.clear();
|
||||
if (result.components.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool standard_mode = result.mode == DecomposeMode::CMYW || result.mode == DecomposeMode::RYBW;
|
||||
std::string basic_type;
|
||||
std::string preset_name;
|
||||
if (standard_mode) {
|
||||
basic_type = decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
|
||||
preset_name = find_decompose_standard_preset_name(source_config_idx, basic_type);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < result.components.size(); ++i) {
|
||||
const DecomposeComponent& comp = result.components[i];
|
||||
out_result.ratios.push_back(comp.ratio);
|
||||
if (!standard_mode) {
|
||||
if (comp.filament_index <= 0) {
|
||||
return false;
|
||||
}
|
||||
const size_t physical_idx = static_cast<size_t>(comp.filament_index - 1);
|
||||
if (physical_idx >= physical_config_indices.size()) {
|
||||
return false;
|
||||
}
|
||||
out_result.components.push_back(static_cast<unsigned int>(physical_config_indices[physical_idx] + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (comp.base_color == DecomposeBaseColor::None) {
|
||||
return false;
|
||||
}
|
||||
DecomposeOfficialComponent official_component =
|
||||
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
|
||||
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
|
||||
physical_config_indices, source_config_idx);
|
||||
if (existing_idx > 0) {
|
||||
out_result.components.push_back(static_cast<unsigned int>(existing_idx));
|
||||
continue;
|
||||
}
|
||||
|
||||
DecomposeMissingComponent missing_comp;
|
||||
missing_comp.component_idx = out_result.components.size();
|
||||
missing_comp.official_component = official_component;
|
||||
missing_comp.preset_name = preset_name;
|
||||
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
|
||||
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
|
||||
missing.push_back(std::move(missing_comp));
|
||||
out_result.components.push_back(0);
|
||||
}
|
||||
|
||||
const bool ok = out_result.components.size() == out_result.ratios.size() && out_result.components.size() >= 2;
|
||||
return ok;
|
||||
}
|
||||
|
||||
size_t count_decompose_new_physical_filaments(
|
||||
const ColorDecomposeResult& result,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<size_t>* physical_config_indices)
|
||||
{
|
||||
if (result.mode != DecomposeMode::CMYW && result.mode != DecomposeMode::RYBW)
|
||||
return 0;
|
||||
|
||||
std::vector<size_t> fallback_indices;
|
||||
const std::vector<size_t>* indices = physical_config_indices;
|
||||
if (!indices) {
|
||||
fallback_indices.resize(physical_colors.size());
|
||||
for (size_t i = 0; i < fallback_indices.size(); ++i)
|
||||
fallback_indices[i] = i;
|
||||
indices = &fallback_indices;
|
||||
}
|
||||
|
||||
size_t source_config_idx = size_t(-1);
|
||||
if (source_physical_idx < indices->size())
|
||||
source_config_idx = (*indices)[source_physical_idx];
|
||||
|
||||
const std::string basic_type =
|
||||
decompose_basic_type_from_source(source_config_idx, source_physical_idx, physical_types);
|
||||
|
||||
size_t missing_count = 0;
|
||||
for (const DecomposeComponent& comp : result.components) {
|
||||
if (comp.base_color == DecomposeBaseColor::None)
|
||||
continue;
|
||||
DecomposeOfficialComponent official_component =
|
||||
lookup_decompose_official_component(basic_type, comp.base_color, comp.colour);
|
||||
int existing_idx = find_existing_decompose_component(official_component, physical_colors,
|
||||
*indices, source_config_idx);
|
||||
if (existing_idx <= 0)
|
||||
++missing_count;
|
||||
}
|
||||
return missing_count;
|
||||
}
|
||||
|
||||
bool confirm_create_decompose_missing_components(wxWindow* parent, const std::vector<DecomposeMissingComponent>& missing)
|
||||
{
|
||||
if (missing.empty())
|
||||
return true;
|
||||
|
||||
static const char* config_key = "not_show_color_decompose_missing_component_tip";
|
||||
if (wxGetApp().app_config->get(config_key) == "1") {
|
||||
return true;
|
||||
}
|
||||
|
||||
wxString missing_text;
|
||||
for (size_t i = 0; i < missing.size(); ++i) {
|
||||
if (i > 0)
|
||||
missing_text += _L(", ");
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = _L("The current filament list does not contain ") + missing_text +
|
||||
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
dlg.show_dsa_button();
|
||||
int res = dlg.ShowModal();
|
||||
if (res == wxID_OK && dlg.get_checkbox_state())
|
||||
wxGetApp().app_config->set(config_key, "1");
|
||||
return res == wxID_OK;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,104 @@
|
||||
#ifndef slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
#define slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <wx/string.h>
|
||||
#include <wx/colour.h>
|
||||
#include "ColorDecomposeDialog.hpp"
|
||||
|
||||
class wxWindow;
|
||||
|
||||
namespace Slic3r {
|
||||
class Preset;
|
||||
namespace GUI {
|
||||
|
||||
// ---- Constants ----
|
||||
|
||||
inline constexpr const char* kDecomposePlaBasicType = "PLA Basic";
|
||||
inline constexpr const char* kDecomposePetgBasicType = "PETG Basic";
|
||||
inline constexpr const char* kDecomposePlaShortType = "PLA";
|
||||
inline constexpr const char* kDecomposePetgShortType = "PETG";
|
||||
inline constexpr const char* kDecomposePlaFilamentId = "GFA00";
|
||||
inline constexpr const char* kDecomposePetgFilamentId = "GFG00";
|
||||
inline constexpr const char* kDecomposeBambuPresetPrefix = "Bambu ";
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
struct DecomposeOfficialComponent {
|
||||
DecomposeBaseColor base_color{DecomposeBaseColor::None};
|
||||
std::string color_hex;
|
||||
std::string filament_id;
|
||||
};
|
||||
|
||||
struct DecomposeMissingComponent {
|
||||
size_t component_idx{0};
|
||||
DecomposeOfficialComponent official_component;
|
||||
std::string preset_name;
|
||||
wxString display_name;
|
||||
};
|
||||
|
||||
struct MixedFilamentResult;
|
||||
|
||||
// ---- Functions ----
|
||||
|
||||
std::string decompose_normalize_color_hex(std::string color);
|
||||
|
||||
const char* decompose_base_color_en(DecomposeBaseColor color);
|
||||
|
||||
wxString decompose_base_color_display(DecomposeBaseColor color);
|
||||
|
||||
std::string decompose_basic_type_from_source(size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_types);
|
||||
|
||||
std::string decompose_basic_filament_id(const std::string& basic_type);
|
||||
|
||||
void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component);
|
||||
|
||||
DecomposeOfficialComponent lookup_decompose_official_component(
|
||||
const std::string& basic_type,
|
||||
DecomposeBaseColor base_color,
|
||||
const wxColour& fallback);
|
||||
|
||||
std::string find_decompose_standard_preset_name(size_t source_config_idx, const std::string& basic_type);
|
||||
|
||||
// Returns "PLA Basic" / "PETG Basic" when preset_name names an official Bambu
|
||||
// basic filament, else an empty string.
|
||||
std::string official_basic_type_from_preset_name(const std::string& preset_name);
|
||||
|
||||
// Resolve display type for color-decompose: official Bambu Basic overrides
|
||||
// get_filament_type when preset name matches; empty/missing -> "PLA".
|
||||
std::string filament_type_for_color_decompose(Preset* preset);
|
||||
|
||||
int find_existing_decompose_component(
|
||||
const DecomposeOfficialComponent& component,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
size_t source_config_idx);
|
||||
|
||||
bool prepare_decompose_mixed_result(
|
||||
const ColorDecomposeResult& result,
|
||||
size_t source_config_idx,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
const std::vector<size_t>& physical_config_indices,
|
||||
MixedFilamentResult& out_result,
|
||||
std::vector<DecomposeMissingComponent>& missing);
|
||||
|
||||
// For standard modes: how many base colors are not reusable from physical list.
|
||||
// MaterialList returns 0. When physical_config_indices is null, indices are 0..n-1.
|
||||
size_t count_decompose_new_physical_filaments(
|
||||
const ColorDecomposeResult& result,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_types,
|
||||
size_t source_physical_idx,
|
||||
const std::vector<size_t>* physical_config_indices);
|
||||
|
||||
bool confirm_create_decompose_missing_components(wxWindow* parent,
|
||||
const std::vector<DecomposeMissingComponent>& missing);
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_ColorDecomposeSupport_hpp_
|
||||
@@ -577,17 +577,30 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
|
||||
}
|
||||
|
||||
// BBS
|
||||
static const char* keys[] = { "support_filament", "support_interface_filament"};
|
||||
// A per-role filament override must name a real, physical filament. Out-of-range values are
|
||||
// stale; a mixed-color slot is virtual and cannot be driven directly by a role override, so
|
||||
// both are reset to 0 ("inherit the object's filament"). The object's own extruder assignment
|
||||
// is what legitimately carries a mixed slot. Orca splits BBS's wall/solid_infill roles into
|
||||
// six keys, so all of them are checked here.
|
||||
static const char* keys[] = { "support_filament", "support_interface_filament",
|
||||
"outer_wall_filament_id", "inner_wall_filament_id",
|
||||
"sparse_infill_filament_id", "internal_solid_filament_id",
|
||||
"top_surface_filament_id", "bottom_surface_filament_id" };
|
||||
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
||||
std::string key = std::string(keys[i]);
|
||||
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
|
||||
if (opt != nullptr) {
|
||||
if (opt->getInt() > filament_cnt) {
|
||||
int val = opt->getInt();
|
||||
bool out_of_range = val > filament_cnt;
|
||||
bool is_mixed = (val > 0 && val <= filament_cnt &&
|
||||
wxGetApp().preset_bundle->is_mixed_filament(val - 1));
|
||||
if (out_of_range || is_mixed) {
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
int new_value = 0;
|
||||
if (conf_temp != nullptr && conf_temp->has(key)) {
|
||||
new_value = conf_temp->opt_int(key);
|
||||
if (out_of_range) {
|
||||
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
|
||||
if (conf_temp != nullptr && conf_temp->has(key))
|
||||
new_value = conf_temp->opt_int(key);
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
apply(config, &new_conf);
|
||||
@@ -595,6 +608,37 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-layer splitting divides each layer by the mix ratio; an adaptive layer profile makes
|
||||
// those sub-layer heights vary per layer, which degrades the blend. Warn once per enable.
|
||||
{
|
||||
static bool s_mixed_sublayer_warned = false;
|
||||
bool sublayer_on = config->opt_bool("enable_mixed_color_sublayer");
|
||||
if (sublayer_on && !s_mixed_sublayer_warned &&
|
||||
wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") {
|
||||
bool has_variable_layer = false;
|
||||
for (const auto* obj : wxGetApp().model().objects) {
|
||||
if (obj->layer_height_profile.get().size() > 4) {
|
||||
has_variable_layer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (has_variable_layer) {
|
||||
MessageDialog dialog(m_msg_dlg_parent,
|
||||
_L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."),
|
||||
"", wxICON_WARNING | wxOK);
|
||||
dialog.show_dsa_button();
|
||||
is_msg_dlg_already_exist = true;
|
||||
dialog.ShowModal();
|
||||
is_msg_dlg_already_exist = false;
|
||||
if (dialog.get_checkbox_state())
|
||||
wxGetApp().app_config->set("no_warn_mixed_sublayer_variable_layer", "1");
|
||||
s_mixed_sublayer_warned = true;
|
||||
}
|
||||
}
|
||||
if (!sublayer_on)
|
||||
s_mixed_sublayer_warned = false;
|
||||
}
|
||||
|
||||
if (config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None &&
|
||||
config->get_abs_value("seam_slope_start_height") >= layer_height) {
|
||||
const wxString msg_text = _(L("seam_slope_start_height need to be smaller than layer_height.\nReset to 0."));
|
||||
|
||||
@@ -155,6 +155,11 @@ std::string& get_filament_mixture_warning_text(){
|
||||
return filament_mixture_warning_text;
|
||||
}
|
||||
|
||||
std::string& get_single_extruder_mixed_filament_warning_text(){
|
||||
static std::string single_extruder_mixed_filament_warning_text;
|
||||
return single_extruder_mixed_filament_warning_text;
|
||||
}
|
||||
|
||||
|
||||
static std::string format_number(float value)
|
||||
{
|
||||
@@ -2984,6 +2989,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
bool mix_pla_and_petg = cur_plate->check_mixture_of_pla_and_petg(full_config_temp);
|
||||
_set_warning_notification(EWarning::MixUsePLAAndPETG, !mix_pla_and_petg);
|
||||
|
||||
bool single_extruder_mixed_risk = cur_plate->check_single_extruder_mixed_filament_risk(full_config_temp, get_single_extruder_mixed_filament_warning_text());
|
||||
_set_warning_notification(EWarning::SingleExtruderMixedFilament, single_extruder_mixed_risk);
|
||||
|
||||
bool filament_nozzle_compatible = cur_plate->check_compatible_of_nozzle_and_filament(full_config_temp, wxGetApp().preset_bundle->filament_presets, get_nozzle_filament_incompatible_text());
|
||||
_set_warning_notification(EWarning::NozzleFilamentIncompatible, !filament_nozzle_compatible);
|
||||
|
||||
@@ -3010,6 +3018,7 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
|
||||
_set_warning_notification(EWarning::TPUPrintableError, false);
|
||||
_set_warning_notification(EWarning::FilamentPrintableError, false);
|
||||
_set_warning_notification(EWarning::MixUsePLAAndPETG, false);
|
||||
_set_warning_notification(EWarning::SingleExtruderMixedFilament, false);
|
||||
_set_warning_notification(EWarning::PrimeTowerOutside, false);
|
||||
_set_warning_notification(EWarning::MultiExtruderPrintableError,false);
|
||||
_set_warning_notification(EWarning::MultiExtruderHeightOutside,false);
|
||||
@@ -10570,6 +10579,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
|
||||
case EWarning::MixUsePLAAndPETG:
|
||||
text = _u8L("PLA and PETG filaments detected in the mixture. Adjust parameters according to the Wiki to ensure print quality.");
|
||||
break;
|
||||
case EWarning::SingleExtruderMixedFilament:
|
||||
text = get_single_extruder_mixed_filament_warning_text();
|
||||
break;
|
||||
case EWarning::PrimeTowerOutside:
|
||||
text = _u8L("The prime tower extends beyond the plate boundary.");
|
||||
break;
|
||||
|
||||
@@ -391,6 +391,7 @@ class GLCanvas3D
|
||||
PrimeTowerOutside,
|
||||
NozzleFilamentIncompatible,
|
||||
MixtureFilamentIncompatible,
|
||||
SingleExtruderMixedFilament,
|
||||
FlushingVolumeZero
|
||||
};
|
||||
|
||||
|
||||
@@ -731,6 +731,10 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
|
||||
continue;
|
||||
|
||||
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
|
||||
// A volume may be assigned to a mixed-color slot, whose index can sit past the
|
||||
// physical colour list; fall back to the first colour rather than reading OOB.
|
||||
if (extruder_idx >= (int)m_extruders_colors.size())
|
||||
extruder_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
@@ -753,6 +757,9 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
|
||||
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
|
||||
int extruder_idx = m_volumes_extruder_idxs[i];
|
||||
int extruder_color_idx = std::max(0, extruder_idx - 1);
|
||||
// As above: a mixed-color slot can index past the physical colour list.
|
||||
if (extruder_color_idx >= (int)m_extruders_colors.size())
|
||||
extruder_color_idx = 0;
|
||||
std::vector<ColorRGBA> ebt_colors;
|
||||
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
|
||||
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
#include "GradientCurveEditor.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "GuiColor.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "Widgets/StateColor.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <wx/dcbuffer.h>
|
||||
#include <wx/dcclient.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/settings.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
|
||||
|
||||
namespace {
|
||||
// Layout (Figma "Property 1=Default", 214.06 x 179.63 px reference).
|
||||
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
|
||||
constexpr double kPlotLeftRatio = 0.0316;
|
||||
constexpr double kPlotRightRatio = 0.6766;
|
||||
constexpr double kPlotTopRatio = 0.1529;
|
||||
constexpr double kPlotBottomRatio = 0.8474;
|
||||
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
|
||||
|
||||
// Hit / stroke (DIP).
|
||||
constexpr int kHitRadius = 6;
|
||||
constexpr int kCurveHitRadius = 5;
|
||||
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
|
||||
constexpr int kStrokeUnselected = 2;
|
||||
constexpr int kStrokeSelected = 4;
|
||||
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
|
||||
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
|
||||
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
|
||||
|
||||
// Light-mode design tokens from Figma. Resolved through StateColor::darkModeColorFor()
|
||||
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
|
||||
// #818183, #262E30 -> #EFEFF0, *wxWHITE -> #2D2D31). Don't read these directly in paint;
|
||||
// always go through the resolved locals declared at the top of on_paint().
|
||||
const wxColour kGridColor (238, 238, 238); // #EEEEEE grey 300
|
||||
const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
|
||||
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
|
||||
|
||||
// LAB (DeltaE76) threshold for "curve color is too close to the background". Below this
|
||||
// we paint a subtle axis-color outline so the curve doesn't visually vanish; above this
|
||||
// we draw the curve plain. ~15 is "perceptible but still close", looser than the strict
|
||||
// 5.0 used by FlushPredict::is_similar_color but loose enough that a pastel pink on white
|
||||
// or a charcoal on #2B2B2B still triggers an outline.
|
||||
constexpr float kBgSimilarThreshold = 15.0f;
|
||||
constexpr int kOutlineExtraDip = 2;
|
||||
} // namespace
|
||||
|
||||
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
|
||||
const wxColour& color_low,
|
||||
const wxColour& color_high)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
|
||||
, m_color_low(color_low)
|
||||
, m_color_high(color_high)
|
||||
{
|
||||
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
||||
SetBackgroundColour(wxGetApp().get_window_default_clr());
|
||||
// Wide enough so the X-axis "Material Ratio" label fits past the arrow tip without overlap.
|
||||
// 260 (was 240): adds room for the "Material Ratio" label that gets shifted right by the
|
||||
// longer axis arrow; the hosting MixedFilamentDialog grows to 470 DIP to accommodate.
|
||||
SetMinSize(FromDIP(wxSize(260, 200)));
|
||||
|
||||
reset_to_linear(0.10, 0.90);
|
||||
|
||||
Bind(wxEVT_PAINT, &GradientCurveEditor::on_paint, this);
|
||||
Bind(wxEVT_LEFT_DOWN, &GradientCurveEditor::on_left_down, this);
|
||||
Bind(wxEVT_LEFT_UP, &GradientCurveEditor::on_left_up, this);
|
||||
Bind(wxEVT_RIGHT_DOWN, &GradientCurveEditor::on_right_down, this);
|
||||
Bind(wxEVT_MOTION, &GradientCurveEditor::on_motion, this);
|
||||
Bind(wxEVT_LEAVE_WINDOW,&GradientCurveEditor::on_leave, this);
|
||||
Bind(wxEVT_SIZE, &GradientCurveEditor::on_size, this);
|
||||
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
|
||||
m_drag_mode = DragMode::None;
|
||||
m_drag_idx = -1;
|
||||
m_dragged_moved = false;
|
||||
});
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_points(const PointList& pts)
|
||||
{
|
||||
m_points = pts;
|
||||
normalize_points();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_colors(const wxColour& color_low, const wxColour& color_high)
|
||||
{
|
||||
m_color_low = color_low;
|
||||
m_color_high = color_high;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::set_selected_curve(int curve_idx)
|
||||
{
|
||||
const int new_sel = (curve_idx == 0) ? 0 : 1;
|
||||
if (m_selected_curve == new_sel) return;
|
||||
m_selected_curve = new_sel;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::reset_to_linear(double y0, double y1)
|
||||
{
|
||||
auto clamp_y = [](double v) {
|
||||
return std::max(kGradientMinRatio, std::min(kGradientMaxRatio, v));
|
||||
};
|
||||
m_points.clear();
|
||||
GradientAnchor a0; a0.x = 0.0; a0.y = clamp_y(y0);
|
||||
GradientAnchor a1; a1.x = 1.0; a1.y = clamp_y(y1);
|
||||
m_points.push_back(a0);
|
||||
m_points.push_back(a1);
|
||||
m_selected_curve = 0;
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::reverse()
|
||||
{
|
||||
// Mirror y around 0.5. Tangents are slopes dy/dx so they flip sign to keep the
|
||||
// local shape consistent across the mirror; NaN tangents remain "use PCHIP default".
|
||||
for (auto& p : m_points) {
|
||||
p.y = 1.0 - p.y;
|
||||
if (std::isfinite(p.m_in)) p.m_in = -p.m_in;
|
||||
if (std::isfinite(p.m_out)) p.m_out = -p.m_out;
|
||||
}
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::normalize_points()
|
||||
{
|
||||
if (m_points.empty()) {
|
||||
GradientAnchor a0; a0.x = 0.0; a0.y = kGradientMinRatio;
|
||||
GradientAnchor a1; a1.x = 1.0; a1.y = kGradientMaxRatio;
|
||||
m_points.push_back(a0);
|
||||
m_points.push_back(a1);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& p : m_points) {
|
||||
p.x = std::max(0.0, std::min(1.0, p.x));
|
||||
p.y = std::max(kGradientMinRatio, std::min(kGradientMaxRatio, p.y));
|
||||
}
|
||||
std::sort(m_points.begin(), m_points.end(),
|
||||
[](const GradientAnchor& a, const GradientAnchor& b) {
|
||||
return a.x < b.x;
|
||||
});
|
||||
|
||||
if (m_points.size() < 2) {
|
||||
GradientAnchor tail; tail.x = 1.0; tail.y = m_points.front().y;
|
||||
m_points.push_back(tail);
|
||||
}
|
||||
|
||||
m_points.front().x = 0.0;
|
||||
m_points.back().x = 1.0;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::emit_changed()
|
||||
{
|
||||
wxCommandEvent evt(wxEVT_GRADIENT_CURVE_CHANGED, GetId());
|
||||
evt.SetEventObject(this);
|
||||
ProcessWindowEvent(evt);
|
||||
}
|
||||
|
||||
wxRect GradientCurveEditor::plot_rect() const
|
||||
{
|
||||
const wxSize sz = GetClientSize();
|
||||
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
|
||||
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
|
||||
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
|
||||
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
|
||||
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
|
||||
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
|
||||
const int side = std::max(1, std::min(x2 - x, y2 - y));
|
||||
return wxRect(x, y, side, side);
|
||||
}
|
||||
|
||||
wxPoint GradientCurveEditor::data_to_px(double x, double y) const
|
||||
{
|
||||
const wxRect r = plot_rect();
|
||||
const int px = r.x + static_cast<int>(std::lround(x * r.width));
|
||||
// y axis is inverted: y=1 should sit at the top.
|
||||
const int py = r.y + static_cast<int>(std::lround((1.0 - y) * r.height));
|
||||
return wxPoint(px, py);
|
||||
}
|
||||
|
||||
void GradientCurveEditor::px_to_data(int px, int py, double& x, double& y) const
|
||||
{
|
||||
const wxRect r = plot_rect();
|
||||
const double w = std::max(1, r.width);
|
||||
const double h = std::max(1, r.height);
|
||||
x = std::max(0.0, std::min(1.0, (px - r.x) / w));
|
||||
y = std::max(0.0, std::min(1.0, 1.0 - (py - r.y) / h));
|
||||
}
|
||||
|
||||
double GradientCurveEditor::sample_curve_y(double x) const
|
||||
{
|
||||
GradientCurve gc;
|
||||
gc.points = m_points;
|
||||
return sample_gradient_curve(gc, x);
|
||||
}
|
||||
|
||||
int GradientCurveEditor::hit_test(int px, int py) const
|
||||
{
|
||||
const int tol = FromDIP(kHitRadius);
|
||||
int best_idx = -1;
|
||||
int best_d2 = tol * tol;
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
// Anchor visual y is curve-specific: component 1's anchor sits at (x, 1 - stored_y).
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
const wxPoint p = data_to_px(m_points[i].x, vy);
|
||||
const int dx = px - p.x;
|
||||
const int dy = py - p.y;
|
||||
const int d2 = dx * dx + dy * dy;
|
||||
if (d2 <= best_d2) {
|
||||
best_idx = static_cast<int>(i);
|
||||
best_d2 = d2;
|
||||
}
|
||||
}
|
||||
return best_idx;
|
||||
}
|
||||
|
||||
int GradientCurveEditor::hit_test_curve(int px, int py, int* seg_out) const
|
||||
{
|
||||
if (seg_out) *seg_out = -1;
|
||||
if (m_points.size() < 2) return -1;
|
||||
const int tol = FromDIP(kCurveHitRadius);
|
||||
const int tol2 = tol * tol;
|
||||
|
||||
auto dist2_to_seg = [&](int ax, int ay, int bx, int by) -> int {
|
||||
const double dx = bx - ax;
|
||||
const double dy = by - ay;
|
||||
const double l2 = dx * dx + dy * dy;
|
||||
if (l2 == 0.0) {
|
||||
const double ddx = px - ax;
|
||||
const double ddy = py - ay;
|
||||
return static_cast<int>(ddx * ddx + ddy * ddy);
|
||||
}
|
||||
double t = ((px - ax) * dx + (py - ay) * dy) / l2;
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
const double ex = ax + t * dx;
|
||||
const double ey = ay + t * dy;
|
||||
const double ddx = px - ex;
|
||||
const double ddy = py - ey;
|
||||
return static_cast<int>(ddx * ddx + ddy * ddy);
|
||||
};
|
||||
|
||||
// Hit-test against the same dense Hermite polyline that on_paint draws, so the
|
||||
// clickable line follows the visual curve exactly (no offset on the bent parts).
|
||||
// When a hit is found, also report the index of the left anchor of the data-space
|
||||
// segment that covers cursor x; needed by the segment-bend interaction.
|
||||
const wxRect rc = plot_rect();
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
auto seg_for_x = [&](double cursor_x) -> int {
|
||||
for (size_t i = 1; i < m_points.size(); ++i) {
|
||||
if (cursor_x <= m_points[i].x)
|
||||
return static_cast<int>(i - 1);
|
||||
}
|
||||
return static_cast<int>(m_points.size() - 2);
|
||||
};
|
||||
|
||||
auto curve_hit = [&](int curve_idx) -> bool {
|
||||
wxPoint prev;
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
const wxPoint cur = data_to_px(x, vy);
|
||||
if (s > 0 && dist2_to_seg(prev.x, prev.y, cur.x, cur.y) <= tol2)
|
||||
return true;
|
||||
prev = cur;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Prefer the selected curve so overlapping segments don't unintentionally steal focus.
|
||||
if (curve_hit(m_selected_curve)) {
|
||||
if (seg_out) {
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(px, py, nx, dummy);
|
||||
*seg_out = seg_for_x(nx);
|
||||
}
|
||||
return m_selected_curve;
|
||||
}
|
||||
const int other = 1 - m_selected_curve;
|
||||
if (curve_hit(other)) {
|
||||
if (seg_out) {
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(px, py, nx, dummy);
|
||||
*seg_out = seg_for_x(nx);
|
||||
}
|
||||
return other;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
|
||||
{
|
||||
// Resolve theme colors every paint so dark-mode toggles (no re-construction) take
|
||||
// effect without an explicit listener. Window bg is read from GUI_App, not
|
||||
// GetBackgroundColour(), since the latter is snapshotted at construction time.
|
||||
const wxColour bg = wxGetApp().get_window_default_clr();
|
||||
const wxColour grid_color = StateColor::darkModeColorFor(kGridColor);
|
||||
const wxColour axis_color = StateColor::darkModeColorFor(kAxisColor);
|
||||
const wxColour label_muted = StateColor::darkModeColorFor(kLabelMuted);
|
||||
const wxColour label_strong = StateColor::darkModeColorFor(kLabelStrong);
|
||||
const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE);
|
||||
|
||||
wxAutoBufferedPaintDC raw_dc(this);
|
||||
raw_dc.SetBackground(wxBrush(bg));
|
||||
raw_dc.Clear();
|
||||
|
||||
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
|
||||
// DC is the actual back buffer that gets blitted to the window.
|
||||
wxGCDC dc(raw_dc);
|
||||
|
||||
const wxRect rc = plot_rect();
|
||||
if (rc.width <= 0 || rc.height <= 0)
|
||||
return;
|
||||
|
||||
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
|
||||
dc.SetPen(wxPen(grid_color, 1));
|
||||
for (int i = 0; i <= kGridDivisions; ++i) {
|
||||
const int x = rc.x + rc.width * i / kGridDivisions;
|
||||
const int y = rc.y + rc.height * i / kGridDivisions;
|
||||
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
|
||||
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
|
||||
}
|
||||
|
||||
// Set the label font first so text width measurements drive arrow / label placement.
|
||||
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
|
||||
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
|
||||
dc.SetFont(label_font);
|
||||
|
||||
const wxString axis_y_title = _L("Material Ratio");
|
||||
const wxString axis_x_title = _L("Model Height");
|
||||
const wxString pct_text = wxT("100%");
|
||||
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
|
||||
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
|
||||
|
||||
wxFont strong_font = label_font;
|
||||
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
|
||||
dc.SetFont(strong_font);
|
||||
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
|
||||
dc.SetFont(label_font);
|
||||
|
||||
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
|
||||
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
|
||||
const int arrow_half = FromDIP(kAxisArrowHalf);
|
||||
const int arrow_len = FromDIP(kAxisArrowLen);
|
||||
const wxSize sz = GetClientSize();
|
||||
dc.SetPen(wxPen(axis_color, kStrokeAxis));
|
||||
dc.SetBrush(wxBrush(axis_color));
|
||||
|
||||
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
|
||||
const int y_axis_x = rc.x;
|
||||
const int y_title_pct_gap = FromDIP(1);
|
||||
const int y_title_bottom_pad = FromDIP(2);
|
||||
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
|
||||
const int y_arrow_tip_y = y_title_y;
|
||||
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
|
||||
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(y_axis_x, y_arrow_tip_y),
|
||||
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
|
||||
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
|
||||
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
|
||||
const int x_axis_y = rc.y + rc.height;
|
||||
const int x_label_gap = FromDIP(4);
|
||||
const int x_edge_pad = FromDIP(6);
|
||||
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
|
||||
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
|
||||
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
|
||||
std::min(x_arrow_ideal, x_arrow_max));
|
||||
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
|
||||
const int x_title_x = x_arrow_tip_x + x_label_gap;
|
||||
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
|
||||
{
|
||||
wxPoint tri[3] = {
|
||||
wxPoint(x_arrow_tip_x, x_axis_y),
|
||||
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
|
||||
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
|
||||
};
|
||||
dc.DrawPolygon(3, tri);
|
||||
}
|
||||
|
||||
// Labels.
|
||||
// "Model Height" and "100%" share the same left x; the gap is larger than the
|
||||
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
|
||||
const int label_left_x = y_axis_x + FromDIP(10);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_y_title, label_left_x, y_title_y);
|
||||
|
||||
dc.SetFont(strong_font);
|
||||
dc.SetTextForeground(label_strong);
|
||||
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
|
||||
|
||||
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
|
||||
// X-axis arrow tip (placement was already clamped above to leave room).
|
||||
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
|
||||
dc.SetFont(label_font);
|
||||
dc.SetTextForeground(label_muted);
|
||||
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
|
||||
|
||||
if (m_points.size() < 2)
|
||||
return;
|
||||
|
||||
auto color_for_curve = [&](int curve_idx) -> wxColour {
|
||||
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
|
||||
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
|
||||
// Lift alpha so the curve stays visible while still hinting at transparency.
|
||||
if (c.Alpha() == 0)
|
||||
c.Set(c.Red(), c.Green(), c.Blue(), 150);
|
||||
return c;
|
||||
};
|
||||
|
||||
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint> {
|
||||
const int samples = std::max(128, rc.width * 2);
|
||||
std::vector<wxPoint> poly;
|
||||
poly.reserve(samples + 1);
|
||||
for (int s = 0; s <= samples; ++s) {
|
||||
const double x = double(s) / samples;
|
||||
const double y0 = sample_curve_y(x);
|
||||
const double vy = to_visual_y(curve_idx, y0);
|
||||
poly.push_back(data_to_px(x, vy));
|
||||
}
|
||||
return poly;
|
||||
};
|
||||
|
||||
auto draw_polyline = [&](const std::vector<wxPoint>& poly, const wxColour& col, int stroke_dip) {
|
||||
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
|
||||
dc.DrawLines(static_cast<int>(poly.size()), poly.data());
|
||||
};
|
||||
|
||||
// Outline only when the curve color is perceptually close to the background; otherwise
|
||||
// the plain filament color reads fine and the extra stroke would look heavy.
|
||||
// Outline tone is intentionally softer than axis_color so it disambiguates the curve
|
||||
// from the bg without competing with the structural axis/grid: light mode uses a pale
|
||||
// grey, dark mode uses a slightly-above-bg grey (gDarkColors has no entry for these).
|
||||
const wxColour outline_color = wxGetApp().dark_mode()
|
||||
? wxColour(90, 90, 94) // > bg #2B2B2B, < axis #818183
|
||||
: wxColour(200, 200, 200); // > grid #EEEEEE, < axis #6B6B6B
|
||||
auto needs_outline = [&](const wxColour& c) {
|
||||
return calc_color_distance(c, bg) < kBgSimilarThreshold;
|
||||
};
|
||||
|
||||
auto draw_one = [&](int curve_idx, int stroke_dip) {
|
||||
const auto poly = build_polyline(curve_idx);
|
||||
const wxColour col = color_for_curve(curve_idx);
|
||||
if (needs_outline(col))
|
||||
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
|
||||
draw_polyline(poly, col, stroke_dip);
|
||||
};
|
||||
|
||||
// Draw unselected first so the selected curve sits on top.
|
||||
const int other = 1 - m_selected_curve;
|
||||
draw_one(other, kStrokeUnselected);
|
||||
draw_one(m_selected_curve, kStrokeSelected);
|
||||
|
||||
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
|
||||
const int r = FromDIP(kPointRadius);
|
||||
dc.SetPen(wxPen(axis_color, 1));
|
||||
dc.SetBrush(wxBrush(point_fill));
|
||||
for (size_t i = 0; i < m_points.size(); ++i) {
|
||||
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
|
||||
const wxPoint p = data_to_px(m_points[i].x, vy);
|
||||
dc.DrawCircle(p.x, p.y, r);
|
||||
}
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
|
||||
{
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
m_dragged_moved = false;
|
||||
|
||||
// 1) Anchor on the selected curve takes precedence over everything else.
|
||||
// Dragging an anchor resets its tangent overrides so the surrounding curve
|
||||
// returns to PCHIP-default shape (matches user expectation that pulling an
|
||||
// anchor "straightens out" the local mess).
|
||||
const int idx = hit_test(pos.x, pos.y);
|
||||
if (idx >= 0) {
|
||||
m_drag_mode = DragMode::Anchor;
|
||||
m_drag_idx = idx;
|
||||
// Only emit a change event when clearing the tangents actually mutates
|
||||
// the curve. A plain click on an already-default anchor must not trigger
|
||||
// re-slicing through the changed-event listener.
|
||||
const bool had_tangent = std::isfinite(m_points[idx].m_in)
|
||||
|| std::isfinite(m_points[idx].m_out);
|
||||
m_points[idx].m_in = std::numeric_limits<double>::quiet_NaN();
|
||||
m_points[idx].m_out = std::numeric_limits<double>::quiet_NaN();
|
||||
if (!HasCapture())
|
||||
CaptureMouse();
|
||||
Refresh();
|
||||
if (had_tangent)
|
||||
emit_changed();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Line-body hit. Determine which curve and which segment.
|
||||
int seg = -1;
|
||||
const int curve_hit = hit_test_curve(pos.x, pos.y, &seg);
|
||||
if (curve_hit < 0) {
|
||||
m_drag_mode = DragMode::None;
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Non-selected curve hit -> switch selection only, no drag arming.
|
||||
if (curve_hit != m_selected_curve) {
|
||||
m_selected_curve = curve_hit;
|
||||
m_drag_mode = DragMode::None;
|
||||
Refresh();
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Selected curve line body hit -> insert a new anchor at cursor x (snapped
|
||||
// to the current smooth curve so the initial click is visually invisible)
|
||||
// and immediately enter Anchor drag mode. PS Curves style: the drag-bend
|
||||
// interaction has no separate "bend without anchor" mode; pressing and
|
||||
// dragging on the line is equivalent to clicking to add then dragging the
|
||||
// fresh anchor. Trades the previous (failed) "no anchor on drag" promise
|
||||
// for genuine cursor tracking, since a single cubic between two existing
|
||||
// anchors mathematically cannot put its peak under an off-center cursor.
|
||||
double nx = 0, dummy = 0;
|
||||
px_to_data(pos.x, pos.y, nx, dummy);
|
||||
if (nx <= 0.0 || nx >= 1.0 || seg < 0) {
|
||||
m_drag_mode = DragMode::None;
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
GradientAnchor a;
|
||||
a.x = nx;
|
||||
a.y = sample_curve_y(nx);
|
||||
const size_t insert_idx = static_cast<size_t>(seg) + 1;
|
||||
m_points.insert(m_points.begin() + insert_idx, a);
|
||||
|
||||
m_drag_mode = DragMode::Anchor;
|
||||
m_drag_idx = static_cast<int>(insert_idx);
|
||||
if (!HasCapture())
|
||||
CaptureMouse();
|
||||
Refresh();
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_left_up(wxMouseEvent& evt)
|
||||
{
|
||||
if (HasCapture())
|
||||
ReleaseMouse();
|
||||
|
||||
// Anchor mode (either an existing anchor or one freshly inserted by on_left_down)
|
||||
// already fired emit_changed on mouse_down; only fire again here if the user
|
||||
// actually dragged so the slicer doesn't re-run on a pure click.
|
||||
if (m_drag_mode == DragMode::Anchor && m_dragged_moved)
|
||||
emit_changed();
|
||||
|
||||
m_drag_mode = DragMode::None;
|
||||
m_drag_idx = -1;
|
||||
m_dragged_moved = false;
|
||||
(void)evt;
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_right_down(wxMouseEvent& evt)
|
||||
{
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
const int idx = hit_test(pos.x, pos.y);
|
||||
if (idx > 0 && static_cast<size_t>(idx) + 1 < m_points.size()) {
|
||||
// Interior anchor on the selected curve -> delete it. Endpoints stay locked.
|
||||
m_points.erase(m_points.begin() + idx);
|
||||
Refresh();
|
||||
emit_changed();
|
||||
return;
|
||||
}
|
||||
// Right-click on the non-selected curve switches selection (never deletes).
|
||||
const int curve_hit = hit_test_curve(pos.x, pos.y);
|
||||
if (curve_hit >= 0 && curve_hit != m_selected_curve) {
|
||||
m_selected_curve = curve_hit;
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_motion(wxMouseEvent& evt)
|
||||
{
|
||||
if (!evt.LeftIsDown() || m_drag_mode != DragMode::Anchor) {
|
||||
evt.Skip();
|
||||
return;
|
||||
}
|
||||
if (static_cast<size_t>(m_drag_idx) >= m_points.size())
|
||||
return;
|
||||
|
||||
const wxPoint pos = evt.GetPosition();
|
||||
double nx = 0, vy = 0;
|
||||
px_to_data(pos.x, pos.y, nx, vy);
|
||||
|
||||
auto& p = m_points[m_drag_idx];
|
||||
const bool is_first = (m_drag_idx == 0);
|
||||
const bool is_last = (static_cast<size_t>(m_drag_idx) + 1 == m_points.size());
|
||||
|
||||
// Endpoints stay locked at x=0 / x=1; interior anchors clamp into
|
||||
// (left_neighbor.x, right_neighbor.x) so they can't cross or coincide.
|
||||
if (!is_first && !is_last) {
|
||||
const double xl = m_points[m_drag_idx - 1].x;
|
||||
const double xr = m_points[m_drag_idx + 1].x;
|
||||
const double eps = 1e-4;
|
||||
nx = std::max(xl + eps, std::min(xr - eps, nx));
|
||||
p.x = nx;
|
||||
}
|
||||
// y is constrained to the reserved blend band so neither component ever
|
||||
// reaches 0% / 100%, matching the sampler's clamp.
|
||||
p.y = std::max(kGradientMinRatio,
|
||||
std::min(kGradientMaxRatio, to_stored_y(m_selected_curve, vy)));
|
||||
m_dragged_moved = true;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_leave(wxMouseEvent& evt)
|
||||
{
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
void GradientCurveEditor::on_size(wxSizeEvent& evt)
|
||||
{
|
||||
Refresh();
|
||||
evt.Skip();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,115 @@
|
||||
#ifndef slic3r_GradientCurveEditor_hpp_
|
||||
#define slic3r_GradientCurveEditor_hpp_
|
||||
|
||||
#include <vector>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// Photoshop-style curve editor for "Z progress -> first-component ratio" mapping.
|
||||
// Curve evaluation uses cubic Hermite with PCHIP defaults plus optional per-anchor
|
||||
// tangent overrides (m_in / m_out, NaN = use PCHIP default). The same evaluator
|
||||
// (FilamentMixer::sample_gradient_curve) is shared with the slicing backend so what
|
||||
// the editor renders matches the G-code output 1:1.
|
||||
//
|
||||
// Interaction model (PS Curves style):
|
||||
// - Click or press-and-drag on the line body inserts a new anchor at the cursor x
|
||||
// (snapped to the current smooth curve, NaN tangents) and starts dragging it.
|
||||
// A pure click leaves an anchor sitting exactly on the previous curve shape; a
|
||||
// drag moves the new anchor freely so the bump follows the cursor 1:1.
|
||||
// - Dragging an existing anchor moves (x, y) and clears its m_in / m_out so the
|
||||
// local curve returns to the PCHIP default shape around it.
|
||||
// - Right-click on an interior anchor deletes it; endpoints stay locked.
|
||||
class GradientCurveEditor : public wxPanel
|
||||
{
|
||||
public:
|
||||
using PointList = std::vector<GradientAnchor>;
|
||||
|
||||
GradientCurveEditor(wxWindow* parent,
|
||||
const wxColour& color_low = wxColour(217, 217, 217),
|
||||
const wxColour& color_high = wxColour(217, 217, 217));
|
||||
|
||||
// Replace the entire point list. The widget enforces x in [0,1], y in [0,1],
|
||||
// sorts by x, and clamps the first / last x to 0 / 1. Tangent overrides are
|
||||
// preserved as-is (NaN entries continue to use PCHIP defaults).
|
||||
void set_points(const PointList& pts);
|
||||
const PointList& get_points() const { return m_points; }
|
||||
|
||||
void set_colors(const wxColour& color_low, const wxColour& color_high);
|
||||
|
||||
// Which curve currently responds to drag / add / delete and is drawn with the thick stroke.
|
||||
// 0 = first component (color_low), 1 = second component (color_high). Storage layer is
|
||||
// unaffected: m_points always represents component 0's ratio.
|
||||
void set_selected_curve(int curve_idx);
|
||||
int get_selected_curve() const { return m_selected_curve; }
|
||||
|
||||
// Reset to a two-point linear curve from y0 at t=0 to y1 at t=1.
|
||||
// Clears all tangent overrides.
|
||||
void reset_to_linear(double y0, double y1);
|
||||
// Flip the curve top to bottom (all y -> 1 - y; tangents negated to mirror shape).
|
||||
void reverse();
|
||||
|
||||
private:
|
||||
enum class DragMode {
|
||||
None, // nothing armed
|
||||
Anchor, // dragging an anchor (either existing or just inserted from a line hit)
|
||||
};
|
||||
|
||||
void normalize_points();
|
||||
void emit_changed();
|
||||
|
||||
void on_paint(wxPaintEvent& evt);
|
||||
void on_left_down(wxMouseEvent& evt);
|
||||
void on_left_up(wxMouseEvent& evt);
|
||||
void on_right_down(wxMouseEvent& evt);
|
||||
void on_motion(wxMouseEvent& evt);
|
||||
void on_leave(wxMouseEvent& evt);
|
||||
void on_size(wxSizeEvent& evt);
|
||||
|
||||
// Coordinate mapping between data (x, y in [0,1]) and pixels in plot area.
|
||||
wxRect plot_rect() const;
|
||||
wxPoint data_to_px(double x, double y) const;
|
||||
void px_to_data(int px, int py, double& x, double& y) const;
|
||||
// Anchor hit test for the currently-selected curve (uses translated visual y).
|
||||
int hit_test(int px, int py) const; // returns point index or -1
|
||||
// Line-body hit test across both curves. Returns 0/1 for which curve was hit, -1 if none.
|
||||
// Prefers the selected curve when both are within threshold. seg_out (when non-null)
|
||||
// receives the left-anchor index of the segment that was hit on the returned curve;
|
||||
// on_left_down uses it to know where in m_points to insert a freshly-added anchor.
|
||||
int hit_test_curve(int px, int py, int* seg_out = nullptr) const;
|
||||
|
||||
// Sample the curve in stored space (component 0) at x.
|
||||
double sample_curve_y(double x) const;
|
||||
|
||||
// Symmetric translation between visual y (what the user sees / clicks) and stored y
|
||||
// (component 0's ratio in m_points).
|
||||
static double to_stored_y(int curve_idx, double visual_y) {
|
||||
return (curve_idx == 0) ? visual_y : (1.0 - visual_y);
|
||||
}
|
||||
static double to_visual_y(int curve_idx, double stored_y) {
|
||||
return (curve_idx == 0) ? stored_y : (1.0 - stored_y);
|
||||
}
|
||||
|
||||
PointList m_points;
|
||||
wxColour m_color_low;
|
||||
wxColour m_color_high;
|
||||
|
||||
int m_selected_curve = 0;
|
||||
DragMode m_drag_mode = DragMode::None;
|
||||
int m_drag_idx = -1; // valid when m_drag_mode == Anchor
|
||||
bool m_dragged_moved = false;
|
||||
};
|
||||
|
||||
// Custom event raised when the curve is edited (drag / add / remove / reset / reverse).
|
||||
wxDECLARE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GradientCurveEditor_hpp_
|
||||
@@ -2330,6 +2330,11 @@ bool MainFrame::get_enable_slice_status()
|
||||
}
|
||||
}
|
||||
|
||||
// A mixed filament whose components were deleted, or whose components disagree in type,
|
||||
// cannot be resolved at slicing time. Block the slice until the user fixes it.
|
||||
if (enable && m_plater->sidebar().has_broken_mixed_filament())
|
||||
enable = false;
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
|
||||
return enable;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
#ifndef slic3r_MixedFilamentDialog_hpp_
|
||||
#define slic3r_MixedFilamentDialog_hpp_
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/panel.h>
|
||||
#include <wx/tglbtn.h>
|
||||
#include <wx/stattext.h>
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
|
||||
class Button;
|
||||
class CheckBox;
|
||||
class ComboBox;
|
||||
class wxMouseEvent;
|
||||
class wxScrolledWindow;
|
||||
class wxTextCtrl;
|
||||
class wxWrapSizer;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class GradientCurveEditor;
|
||||
class RatioLabelPanel;
|
||||
|
||||
struct MixedFilamentResult {
|
||||
std::vector<unsigned int> components; // 1-based physical filament indices
|
||||
std::vector<int> ratios; // percentages, sum = 100
|
||||
bool gradient_enabled = false;
|
||||
int gradient_direction = 0; // 0 = A→B, 1 = B→A (only for 2-color)
|
||||
bool per_part_gradient = false; // valid only when gradient_enabled == true
|
||||
// Optional Photoshop-style custom curve overriding the linear A→B gradient.
|
||||
// Empty -> use linear (gradient_direction). Non-empty -> cubic Hermite over [0,1]^2
|
||||
// with optional per-anchor tangent overrides (see GradientAnchor).
|
||||
std::vector<GradientAnchor> gradient_curve;
|
||||
};
|
||||
|
||||
class MixedFilamentDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
MixedFilamentDialog(wxWindow* parent,
|
||||
const std::vector<std::string>& physical_colors,
|
||||
const std::vector<std::string>& physical_names,
|
||||
const std::vector<std::string>& physical_types = {});
|
||||
|
||||
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 = {});
|
||||
|
||||
MixedFilamentResult get_result() const { return m_result; }
|
||||
|
||||
protected:
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
wxBoxSizer* create_preview_panel();
|
||||
wxBoxSizer* create_material_selection();
|
||||
wxBoxSizer* create_ratio_slider();
|
||||
wxBoxSizer* create_triangle_picker();
|
||||
wxBoxSizer* create_gradient_section();
|
||||
wxBoxSizer* create_recommendation_grid();
|
||||
wxBoxSizer* create_button_panel();
|
||||
|
||||
void on_filament_changed();
|
||||
void on_ratio_changed(int new_ratio_a);
|
||||
void on_gradient_toggled();
|
||||
void on_gradient_direction_changed();
|
||||
void on_gradient_curve_changed();
|
||||
void on_per_part_gradient_toggled();
|
||||
void on_add_material();
|
||||
void on_remove_material();
|
||||
void on_recommendation_clicked(unsigned int comp_a, unsigned int comp_b);
|
||||
void on_recommendation_clicked_triple(unsigned int a, unsigned int b, unsigned int c);
|
||||
void apply_manual_ratio(size_t idx, int value);
|
||||
void apply_dragged_triangle_ratio(int r0, int r1, int r2);
|
||||
void reset_manual_ratio_state();
|
||||
void refresh_ratio_labels();
|
||||
void sync_triangle_weights_from_ratios();
|
||||
void start_ratio_editor(size_t idx, wxWindow* anchor, const wxRect& anchor_rect);
|
||||
void commit_ratio_editor(bool apply);
|
||||
void commit_ratio_editor_from_background(wxMouseEvent& e);
|
||||
void update_preview();
|
||||
void update_ok_button_state();
|
||||
void update_gradient_direction_items();
|
||||
void update_component_count_ui();
|
||||
// Picks dialog (width, height) based on current state so the gradient curve
|
||||
// editor and the recommendation list stay visible at the same time.
|
||||
wxSize compute_dialog_size() const;
|
||||
void rebuild_all_combos();
|
||||
void rebuild_recommendation_items();
|
||||
void refresh_curve_editor_colors();
|
||||
void paint_warning_panel(wxPaintEvent& evt);
|
||||
|
||||
wxBitmap make_swatch_bitmap(size_t idx);
|
||||
|
||||
// Helpers for component/ratio access
|
||||
size_t num_components() const { return m_result.components.size(); }
|
||||
unsigned int comp(size_t i) const { return (i < m_result.components.size()) ? m_result.components[i] : 1; }
|
||||
int ratio(size_t i) const { return (i < m_result.ratios.size()) ? m_result.ratios[i] : 0; }
|
||||
wxColour comp_colour(size_t i) const;
|
||||
|
||||
MixedFilamentResult m_result;
|
||||
bool m_edit_mode{false};
|
||||
std::vector<std::string> m_physical_colors;
|
||||
std::vector<std::string> m_physical_names;
|
||||
std::vector<std::string> m_physical_types;
|
||||
wxString m_type_mismatch_msg;
|
||||
|
||||
// Combo item index -> 1-based physical filament index (per combo)
|
||||
std::vector<std::vector<unsigned int>> m_combo_to_physical;
|
||||
|
||||
// UI controls
|
||||
wxPanel* m_preview_canvas{nullptr};
|
||||
wxPanel* m_summary_panel{nullptr};
|
||||
std::vector<ComboBox*> m_combo_filaments;
|
||||
wxBoxSizer* m_material_rows_sizer{nullptr};
|
||||
wxPanel* m_ratio_bar{nullptr};
|
||||
wxPanel* m_triangle_panel{nullptr};
|
||||
RatioLabelPanel* m_label_ratio_a{nullptr};
|
||||
RatioLabelPanel* m_label_ratio_b{nullptr};
|
||||
wxPanel* m_ratio_editor_panel{nullptr};
|
||||
wxTextCtrl* m_ratio_editor{nullptr};
|
||||
CheckBox* m_chk_gradient{nullptr};
|
||||
wxStaticText* m_label_gradient{nullptr};
|
||||
ComboBox* m_combo_gradient_dir{nullptr};
|
||||
wxBoxSizer* m_gradient_sizer{nullptr};
|
||||
GradientCurveEditor* m_curve_editor{nullptr};
|
||||
wxBoxSizer* m_curve_sizer{nullptr};
|
||||
CheckBox* m_chk_per_part_gradient{nullptr};
|
||||
wxStaticText* m_label_per_part_gradient{nullptr};
|
||||
wxBoxSizer* m_per_part_gradient_sizer{nullptr};
|
||||
Button* m_btn_add_material{nullptr};
|
||||
Button* m_btn_remove_material{nullptr};
|
||||
Button* m_btn_ok{nullptr};
|
||||
Button* m_btn_cancel{nullptr};
|
||||
wxBoxSizer* m_warning_sizer{nullptr};
|
||||
wxPanel* m_warning_panel{nullptr};
|
||||
|
||||
wxBoxSizer* m_ratio_sizer{nullptr};
|
||||
wxBoxSizer* m_triangle_sizer{nullptr};
|
||||
wxBoxSizer* m_right_sizer{nullptr};
|
||||
|
||||
wxScrolledWindow* m_recommendation_scroll{nullptr};
|
||||
wxWrapSizer* m_recommendation_grid{nullptr};
|
||||
|
||||
// Cached preview bitmaps (loaded once at construction)
|
||||
wxBitmap m_preview_bmp_two;
|
||||
wxBitmap m_preview_bmp_three;
|
||||
|
||||
// Drag state
|
||||
bool m_dragging{false};
|
||||
std::vector<size_t> m_ratio_manual_order;
|
||||
size_t m_ratio_editor_idx{0};
|
||||
bool m_ratio_editor_committing{false};
|
||||
wxWindow* m_ratio_editor_anchor{nullptr};
|
||||
// Triangle picker drag point (barycentric weights)
|
||||
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
|
||||
|
||||
// Cached triangle color bitmap (invalidated when colors or size change)
|
||||
wxBitmap m_tri_cache_bmp;
|
||||
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
|
||||
wxSize m_tri_cache_size;
|
||||
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_MixedFilamentDialog_hpp_
|
||||
@@ -162,6 +162,8 @@ enum class NotificationType
|
||||
//BBL: plugin install hint
|
||||
BBLPluginInstallHint,
|
||||
BBLFlushingVolumeZero,
|
||||
// A mixed-color filament references a deleted component, or its components disagree in type.
|
||||
BBLMixedFilamentBroken,
|
||||
BBLPluginUpdateAvailable,
|
||||
BBLPreviewOnlyMode,
|
||||
BBLPrinterConfigUpdateAvailable,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include <future>
|
||||
#include <glad/gl.h>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -1675,6 +1676,25 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1836,6 +1856,24 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto* is_mixed_opt = full_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = full_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1889,6 +1927,25 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
|
||||
std::sort(plate_extruders.begin(), plate_extruders.end());
|
||||
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
|
||||
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
|
||||
|
||||
// Expand mixed filament slots to their physical components. A mixed slot is virtual and
|
||||
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
|
||||
// physical filaments it resolves to instead.
|
||||
{
|
||||
auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
|
||||
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
|
||||
std::vector<unsigned int> ext_0based;
|
||||
for (int e : plate_extruders)
|
||||
if (e >= 1) ext_0based.push_back((unsigned int)(e - 1));
|
||||
auto expanded = expand_mixed_filaments(ext_0based, is_mixed_opt->values, comp_strs_opt->values);
|
||||
plate_extruders.clear();
|
||||
for (unsigned int e : expanded)
|
||||
plate_extruders.push_back((int)(e + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return plate_extruders;
|
||||
}
|
||||
|
||||
@@ -1990,6 +2047,55 @@ bool PartPlate::check_tpu_printable_status(const DynamicPrintConfig & config, co
|
||||
return true;
|
||||
}
|
||||
|
||||
// A mixed-color filament alternates between its components constantly. On a single-nozzle
|
||||
// printer every one of those switches is a full filament change plus a purge, so warn the
|
||||
// user before they commit to it. Printers with more than one nozzle can keep the components
|
||||
// loaded simultaneously and are not affected.
|
||||
//
|
||||
// BBS additionally excludes its H2C/H2D/X2D models by name; those are multi-nozzle machines
|
||||
// already ruled out by the nozzle_diameter test above, so the name check is dropped here
|
||||
// rather than carried over as a Bambu-specific special case.
|
||||
bool PartPlate::check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const
|
||||
{
|
||||
warning_text.clear();
|
||||
|
||||
auto *nozzle_diameter_opt = config.option<ConfigOptionFloatsNullable>("nozzle_diameter");
|
||||
if (!nozzle_diameter_opt || nozzle_diameter_opt->values.size() > 1)
|
||||
return false;
|
||||
|
||||
auto *is_mixed_opt = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
|
||||
if (!is_mixed_opt || !has_any_mixed_filament(is_mixed_opt->values))
|
||||
return false;
|
||||
|
||||
auto is_mixed_slot = [&](int extruder_1based) {
|
||||
size_t idx = (size_t)(extruder_1based - 1);
|
||||
return idx < is_mixed_opt->values.size() && is_mixed_opt->values[idx];
|
||||
};
|
||||
|
||||
const std::string mixed_warn_msg = _u8L("Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, "
|
||||
"which may significantly increase waste and the risk of nozzle / waste-chute clogging.");
|
||||
|
||||
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); ++obj_idx) {
|
||||
if (!contain_instance_totally(obj_idx, 0))
|
||||
continue;
|
||||
ModelObject *mo = m_model->objects[obj_idx];
|
||||
int obj_ext = mo->config.has("extruder") ? mo->config.extruder() : 1;
|
||||
if (is_mixed_slot(obj_ext)) {
|
||||
warning_text = mixed_warn_msg;
|
||||
return true;
|
||||
}
|
||||
for (ModelVolume *mv : mo->volumes) {
|
||||
int vol_ext = mv->config.has("extruder") ? mv->config.extruder() : obj_ext;
|
||||
if (is_mixed_slot(vol_ext)) {
|
||||
warning_text = mixed_warn_msg;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PartPlate::check_mixture_of_pla_and_petg(const DynamicPrintConfig &config)
|
||||
{
|
||||
bool has_pla = false;
|
||||
|
||||
@@ -354,6 +354,9 @@ public:
|
||||
bool check_filament_printable(const DynamicPrintConfig & config, wxString& error_message);
|
||||
bool check_tpu_printable_status(const DynamicPrintConfig & config, const std::vector<int> &tpu_filaments);
|
||||
bool check_mixture_of_pla_and_petg(const DynamicPrintConfig & config);
|
||||
// Warns when a mixed-color filament is used on a single-nozzle printer, where every
|
||||
// component switch costs a full filament change and purge.
|
||||
bool check_single_extruder_mixed_filament_risk(const DynamicPrintConfig &config, std::string &warning_text) const;
|
||||
bool check_mixture_filament_compatible(const DynamicPrintConfig& config, std::string &error_msg);
|
||||
bool check_compatible_of_nozzle_and_filament(const DynamicPrintConfig & config, const std::vector<std::string>& filament_presets, std::string& error_msg);
|
||||
|
||||
|
||||
+1764
-8
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,10 @@ using t_optgroups = std::vector <std::shared_ptr<ConfigOptionsGroup>>;
|
||||
class Plater;
|
||||
enum class ActionButtonType : int;
|
||||
|
||||
// Sentinel filament id meaning "use the slot the sidebar context menu was opened on"
|
||||
// (Sidebar::priv::m_menu_filament_id) rather than an explicit index.
|
||||
inline constexpr int kSidebarContextMenuFilamentId = -2;
|
||||
|
||||
#define EVT_PUBLISHING_START 1
|
||||
#define EVT_PUBLISHING_STOP 2
|
||||
|
||||
@@ -188,7 +192,7 @@ public:
|
||||
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default
|
||||
void change_filament(size_t from_id, size_t to_id); // 0 base
|
||||
void edit_filament();
|
||||
void add_custom_filament(wxColour new_col);
|
||||
void add_custom_filament(wxColour new_col, const std::string& preset_name = std::string(), bool skip_preset_validation = false);
|
||||
bool is_new_project_in_gcode3mf();
|
||||
// BBS
|
||||
void on_bed_type_change(BedType bed_type);
|
||||
@@ -262,6 +266,20 @@ public:
|
||||
std::vector<PlaterPresetComboBox*>& combos_filament();
|
||||
void clear_combos_filament_badge();
|
||||
void udpate_combos_filament_badge();
|
||||
|
||||
// Mixed-color filament sidebar section
|
||||
void add_mixed_filament();
|
||||
void edit_mixed_filament(size_t idx);
|
||||
void delete_mixed_filament_at(size_t idx);
|
||||
void decompose_filament_color(int filament_idx);
|
||||
void recalc_filament_scroll_sizes();
|
||||
void update_mixed_filament_list();
|
||||
bool has_broken_mixed_filament() const;
|
||||
bool has_broken_mixed_filament(const PartPlate* plate) const;
|
||||
void collect_physical_filament_info(std::vector<std::string>& color_strs,
|
||||
std::vector<std::string>& names,
|
||||
std::vector<std::string>& types,
|
||||
std::vector<size_t>* config_indices = nullptr);
|
||||
Search::OptionsSearcher& get_searcher();
|
||||
std::string& get_search_line();
|
||||
void update_printer_thumbnail();
|
||||
@@ -313,6 +331,11 @@ public:
|
||||
const SLAPrint& sla_print() const;
|
||||
SLAPrint& sla_print();
|
||||
|
||||
// Helper: returns config indices where filament_is_mixed == true
|
||||
std::vector<size_t> mixed_filament_config_indices() const;
|
||||
// Helper: returns config indices where filament_is_mixed == false
|
||||
std::vector<size_t> physical_filament_config_indices() const;
|
||||
|
||||
int new_project(bool skip_confirm = false, bool silent = false, const wxString& project_name = wxString());
|
||||
// BBS: save & backup
|
||||
void load_project(wxString const & filename = "", wxString const & originfile = "-");
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PresetHints.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
@@ -3497,6 +3498,21 @@ void TabPrintModel::activate_selected_page(std::function<void()> throw_if_cancel
|
||||
f->set_value(boost::any(), false);
|
||||
}
|
||||
}
|
||||
if (m_type == Preset::TYPE_PLATE)
|
||||
static_cast<TabPrintPlate *>(this)->update_mixed_filament_seq_state();
|
||||
}
|
||||
|
||||
// A mixed-color slot resolves to a different physical filament per layer, so a
|
||||
// user-defined filament print order cannot be honoured while one exists.
|
||||
void TabPrintPlate::update_mixed_filament_seq_state()
|
||||
{
|
||||
if (!m_active_page) return;
|
||||
auto &proj_cfg = m_preset_bundle->project_config;
|
||||
auto *opt = proj_cfg.option<ConfigOptionBools>("filament_is_mixed");
|
||||
bool has_mixed = opt && has_any_mixed_filament(opt->values);
|
||||
|
||||
toggle_option("first_layer_sequence_choice", !has_mixed);
|
||||
toggle_option("other_layers_sequence_choice", !has_mixed);
|
||||
}
|
||||
|
||||
void TabPrintModel::on_value_change(const std::string& opt_id, const boost::any& value)
|
||||
|
||||
@@ -545,6 +545,8 @@ public:
|
||||
void build() override;
|
||||
void reset_model_config() override;
|
||||
int show_spiral_mode_settings_dialog(bool is_object_config) { return m_config_manipulation.show_spiral_mode_settings_dialog(is_object_config); }
|
||||
// Disables the user-defined filament print order while a mixed-color filament exists.
|
||||
void update_mixed_filament_seq_state();
|
||||
|
||||
protected:
|
||||
virtual void on_value_change(const std::string& opt_key, const boost::any& value) override;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
#pragma once
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "Widgets/ProgressDialog.hpp"
|
||||
#include "libslic3r/TexturePainting.hpp"
|
||||
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/stattext.h>
|
||||
#include "Widgets/PopupWindow.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include "Widgets/SpinInput.hpp"
|
||||
#include <wx/checkbox.h>
|
||||
#include <wx/button.h>
|
||||
#include "Widgets/Button.hpp"
|
||||
#include <wx/glcanvas.h>
|
||||
#include <wx/event.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
class GreenSlider;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent);
|
||||
|
||||
enum class TextureImportState {
|
||||
Idle,
|
||||
Computing,
|
||||
Ready,
|
||||
Error
|
||||
};
|
||||
|
||||
enum class TextureAutoMixMode {
|
||||
CMYW,
|
||||
RYBW
|
||||
};
|
||||
|
||||
enum class TextureFilamentKind {
|
||||
ExistingPhysical,
|
||||
ExistingMixed,
|
||||
NewPhysical,
|
||||
NewMixed
|
||||
};
|
||||
|
||||
struct TextureFilamentEntry {
|
||||
TextureFilamentKind kind{TextureFilamentKind::ExistingPhysical};
|
||||
int dialog_index{-1};
|
||||
size_t project_config_index{size_t(-1)};
|
||||
std::string color_hex;
|
||||
std::string name;
|
||||
std::string type;
|
||||
std::string preset_name;
|
||||
std::vector<unsigned int> mixed_components;
|
||||
std::vector<int> mixed_ratios;
|
||||
};
|
||||
|
||||
struct TextureNewMixedFilament {
|
||||
int dialog_index{-1};
|
||||
std::string color_hex;
|
||||
std::vector<int> component_dialog_indices;
|
||||
std::vector<int> ratios;
|
||||
};
|
||||
|
||||
struct FilamentMappingRow {
|
||||
int cluster_id = -1;
|
||||
std::array<std::size_t, 3> source_color = {0, 0, 0};
|
||||
std::string source_hex;
|
||||
int target_filament_idx = 0;
|
||||
wxPanel* source_panel = nullptr;
|
||||
wxPanel* target_panel = nullptr;
|
||||
};
|
||||
|
||||
class FilamentSelectPopup;
|
||||
class AutoMixSelectPopup;
|
||||
// Lightweight 3D preview panel using wxGLCanvas.
|
||||
// Renders: original textured, multi-color, or filament-mapped.
|
||||
class TexturePreviewCanvas : public wxGLCanvas
|
||||
{
|
||||
public:
|
||||
enum class RenderMode { Original, MultiColor, FilamentMap };
|
||||
|
||||
TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs);
|
||||
~TexturePreviewCanvas();
|
||||
|
||||
void set_mesh_data(
|
||||
const std::vector<std::array<float, 3>>& vertices,
|
||||
const std::vector<std::array<int, 3>>& indices);
|
||||
|
||||
void set_texture_data(
|
||||
const std::vector<std::array<float, 2>>& uvs,
|
||||
const unsigned char* tex_data, int tex_w, int tex_h, int tex_channels);
|
||||
|
||||
void set_texture_render_data(
|
||||
const std::vector<std::vector<unsigned char>>& tex_pixels_rgb,
|
||||
const std::vector<int>& tex_widths,
|
||||
const std::vector<int>& tex_heights,
|
||||
const std::vector<std::array<std::array<float,2>, 3>>& face_uvs,
|
||||
const std::vector<int>& face_tex_ids);
|
||||
|
||||
void set_painted_mesh_data(
|
||||
const std::vector<std::array<float, 3>>& vertices,
|
||||
const std::vector<std::array<int, 3>>& indices);
|
||||
void set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
|
||||
void set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors);
|
||||
void set_filament_color_map(const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map);
|
||||
|
||||
void set_render_mode(RenderMode mode);
|
||||
RenderMode get_render_mode() const { return m_mode; }
|
||||
void set_computing_overlay(bool show);
|
||||
void reset_view();
|
||||
|
||||
private:
|
||||
void on_paint(wxPaintEvent& evt);
|
||||
void on_size(wxSizeEvent& evt);
|
||||
void on_mouse(wxMouseEvent& evt);
|
||||
void ensure_gl_ready();
|
||||
void render();
|
||||
void render_mesh();
|
||||
void render_textured_original();
|
||||
void render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size);
|
||||
void upload_reset_icon_textures();
|
||||
unsigned int upload_reset_icon_texture(const std::string& icon_name);
|
||||
wxRect reset_overlay_rect() const;
|
||||
bool handle_reset_overlay_mouse(wxMouseEvent& evt);
|
||||
void upload_textures();
|
||||
void compute_smooth_normals();
|
||||
void update_bounding_box();
|
||||
|
||||
wxGLContext* m_context = nullptr;
|
||||
bool m_gl_initialized = false;
|
||||
RenderMode m_mode = RenderMode::Original;
|
||||
|
||||
float m_zoom = 1.0f;
|
||||
float m_rot_x = -30.0f;
|
||||
float m_rot_y = 30.0f;
|
||||
float m_pan_x = 0.0f;
|
||||
float m_pan_y = 0.0f;
|
||||
wxPoint m_last_mouse_pos;
|
||||
enum class DragMode { None, Rotate, Pan };
|
||||
DragMode m_drag_mode = DragMode::None;
|
||||
|
||||
std::vector<std::array<float, 3>> m_vertices;
|
||||
std::vector<std::array<int, 3>> m_indices;
|
||||
std::vector<std::array<float, 2>> m_uvs;
|
||||
std::vector<std::array<float, 3>> m_painted_vertices;
|
||||
std::vector<std::array<int, 3>> m_painted_indices;
|
||||
std::vector<std::array<float, 3>> m_face_colors_rgb;
|
||||
std::vector<std::array<float, 3>> m_original_face_colors_rgb;
|
||||
std::vector<std::array<float, 3>> m_filament_colors_rgb;
|
||||
std::map<std::array<std::size_t, 3>, std::array<float, 3>> m_color_map;
|
||||
|
||||
unsigned int m_tex_id = 0;
|
||||
int m_tex_w = 0;
|
||||
int m_tex_h = 0;
|
||||
int m_tex_channels = 3;
|
||||
bool m_tex_dirty = false;
|
||||
std::vector<unsigned char> m_tex_data;
|
||||
|
||||
std::vector<unsigned int> m_gl_tex_ids;
|
||||
std::vector<std::vector<unsigned char>> m_tex_pixels_rgb;
|
||||
std::vector<int> m_tex_widths;
|
||||
std::vector<int> m_tex_heights;
|
||||
std::vector<std::array<std::array<float,2>, 3>> m_face_uvs;
|
||||
std::vector<int> m_face_tex_ids;
|
||||
bool m_multi_tex_dirty = false;
|
||||
|
||||
std::vector<std::array<float, 3>> m_vertex_normals;
|
||||
|
||||
std::array<float, 3> m_center = {0, 0, 0};
|
||||
float m_radius = 1.0f;
|
||||
|
||||
unsigned int m_reset_icon_tex = 0;
|
||||
unsigned int m_reset_icon_hover_tex = 0;
|
||||
unsigned int m_reset_icon_dark_tex = 0;
|
||||
unsigned int m_reset_icon_dark_hover_tex = 0;
|
||||
bool m_reset_overlay_hovered = false;
|
||||
bool m_reset_overlay_pressed = false;
|
||||
};
|
||||
|
||||
|
||||
class TextureImportDialog : public DPIDialog
|
||||
{
|
||||
public:
|
||||
TextureImportDialog(wxWindow* parent,
|
||||
const Slic3r::TexturedMesh& textured_mesh,
|
||||
const std::vector<TextureFilamentEntry>& filament_entries,
|
||||
std::function<bool()> initial_cancel_callback = {},
|
||||
std::function<bool(int)> initial_progress_callback = {});
|
||||
~TextureImportDialog();
|
||||
|
||||
int ShowModal() override;
|
||||
void on_dpi_changed(const wxRect& suggested_rect) override;
|
||||
|
||||
Slic3r::PaintedMesh get_painted_mesh() const;
|
||||
std::vector<Slic3r::FilamentMatch> get_matches() const;
|
||||
bool was_skipped() const { return m_skipped; }
|
||||
bool fallback_to_geometry_only() const { return m_fallback_to_geometry_only; }
|
||||
// Colors of virtual filaments that need to be created after dialog confirmation.
|
||||
// Index i corresponds to filament index (m_existing_filament_count + i).
|
||||
const std::vector<std::array<float, 4>>& get_new_filament_colors() const { return m_new_filament_colors; }
|
||||
const std::vector<std::string>& get_new_filament_preset_names() const { return m_new_filament_preset_names; }
|
||||
const std::vector<TextureNewMixedFilament>& get_new_mixed_filaments() const { return m_new_mixed_filaments; }
|
||||
const std::vector<TextureFilamentEntry>& get_filament_entries() const { return m_filament_entries; }
|
||||
size_t get_existing_filament_count() const { return m_existing_filament_count; }
|
||||
|
||||
private:
|
||||
void build_ui();
|
||||
void build_preview_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_params_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_mapping_panel(wxWindow* parent, wxSizer* sizer);
|
||||
void build_bottom_buttons(wxSizer* sizer);
|
||||
|
||||
void set_state(TextureImportState new_state);
|
||||
void update_ui_for_state();
|
||||
|
||||
void start_computation(bool auto_color = false, bool initial = false);
|
||||
void cancel_computation();
|
||||
void on_computation_complete(wxCommandEvent& evt);
|
||||
void on_computation_progress(wxCommandEvent& evt);
|
||||
void on_computation_error(wxCommandEvent& evt);
|
||||
void on_mesh_repair_decision_required(wxCommandEvent& evt);
|
||||
|
||||
void rebuild_mapping_rows();
|
||||
void do_auto_match();
|
||||
// Reorder m_current_matches into a canonical, predictable order (ascending
|
||||
// filament_index, with unmapped entries pushed to the end). Used right
|
||||
// after the initial computation so the first view the user sees has a
|
||||
// stable, intuitive layout.
|
||||
void sort_current_matches_by_filament_index();
|
||||
// Reorder m_current_matches so they appear in the same order as
|
||||
// `previous_matches` (keyed by cluster_index). Entries whose cluster_index
|
||||
// was not present before are appended at the end, preserving their current
|
||||
// relative order. Used when the user toggles auto-merge so the rows do not
|
||||
// visually jump around. Assumes each cluster_index appears at most once in
|
||||
// both vectors (this invariant is currently guaranteed by do_auto_match,
|
||||
// which produces one match per cluster).
|
||||
void restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches);
|
||||
std::vector<Slic3r::FilamentMatch> build_matches_from_rows() const;
|
||||
void update_filament_color_map();
|
||||
void show_filament_popup(size_t row_index);
|
||||
void dismiss_filament_popup();
|
||||
void dismiss_filament_popup_on_wheel(wxMouseEvent& evt);
|
||||
void show_auto_mix_popup();
|
||||
void dismiss_auto_mix_popup();
|
||||
void set_auto_mix_mode(TextureAutoMixMode mode);
|
||||
void apply_auto_standard_mix(TextureAutoMixMode mode);
|
||||
void reset_auto_mix();
|
||||
void update_auto_mix_reset_visibility();
|
||||
bool add_decomposed_mixed_filament(size_t row_index);
|
||||
int add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex,
|
||||
const std::string& preset_name = std::string());
|
||||
int add_virtual_mixed_filament(const std::string& color_hex,
|
||||
const std::vector<int>& component_dialog_indices,
|
||||
const std::vector<int>& ratios);
|
||||
size_t max_filament_count() const;
|
||||
bool can_add_virtual_filament() const;
|
||||
// Recomputes m_drop_warning_label visibility from m_filaments_dropped and
|
||||
// m_state. Safe to call whether or not the label has been created yet.
|
||||
// Visibility reflects ONLY the result of the most recent do_auto_match():
|
||||
// if the latest match did not drop any cluster, the label is hidden even
|
||||
// if a previous match had dropped (no historical accumulation).
|
||||
void update_drop_warning_visibility();
|
||||
void compact_used_virtual_filaments();
|
||||
int find_closest_filament_index(const std::array<std::size_t, 3>& color) const;
|
||||
|
||||
void on_color_preset_clicked(wxCommandEvent& evt);
|
||||
void on_color_slider_changed(wxCommandEvent& evt);
|
||||
void on_color_spin_changed(wxCommandEvent& evt);
|
||||
void on_color_spin_text_changed(wxCommandEvent& evt);
|
||||
void on_smooth_slider_changed(wxCommandEvent& evt);
|
||||
void on_smooth_spin_changed(wxCommandEvent& evt);
|
||||
void on_smooth_spin_text_changed(wxCommandEvent& evt);
|
||||
void on_apply_clicked(wxCommandEvent& evt);
|
||||
void on_auto_merge_toggled(wxCommandEvent& evt);
|
||||
void highlight_view_button(int view_index);
|
||||
void on_skip_clicked(wxCommandEvent& evt);
|
||||
void on_ok_clicked(wxCommandEvent& evt);
|
||||
|
||||
void set_color_count_value(int value, bool update_spin);
|
||||
void set_smooth_value(int value, bool update_spin);
|
||||
void preview_spin_text_value(SpinInput* spin, GreenSlider* slider, int& param,
|
||||
int min_value, int max_value, const wxString& text,
|
||||
std::function<void()> on_value_changed = {});
|
||||
void update_color_count_preset_buttons();
|
||||
|
||||
bool has_valid_result() const;
|
||||
bool is_params_dirty() const;
|
||||
void update_confirm_button_state();
|
||||
|
||||
Slic3r::TexturedMesh m_textured_mesh;
|
||||
std::vector<std::string> m_filament_color_strs; // existing + virtual
|
||||
std::vector<std::string> m_filament_names; // existing + virtual
|
||||
std::vector<std::array<float, 4>> m_filament_colors_rgba; // existing + virtual
|
||||
std::vector<TextureFilamentEntry> m_filament_entries; // aligned with m_filament_colors_rgba
|
||||
size_t m_existing_filament_count = 0;
|
||||
std::vector<std::array<float, 4>> m_new_filament_colors; // only virtual (to be created)
|
||||
std::vector<std::string> m_new_filament_preset_names; // only virtual, aligned with m_new_filament_colors
|
||||
std::vector<TextureNewMixedFilament> m_new_mixed_filaments;
|
||||
std::string m_default_virtual_filament_preset_name;
|
||||
|
||||
TextureImportState m_state = TextureImportState::Idle;
|
||||
bool m_skipped = false;
|
||||
bool m_fallback_to_geometry_only = false;
|
||||
// True iff *the most recent* do_auto_match() ran into the global filament
|
||||
// limit and had to drop one or more clusters. Reset to false on every
|
||||
// do_auto_match() entry so it never accumulates across runs: a run that
|
||||
// does not drop anything must observe false here, regardless of whether
|
||||
// previous runs dropped. Drives the inline orange warning above the
|
||||
// bottom buttons; never affects the mapping itself.
|
||||
bool m_filaments_dropped = false;
|
||||
bool m_auto_merge_enabled = true;
|
||||
TextureAutoMixMode m_auto_mix_mode = TextureAutoMixMode::CMYW;
|
||||
int m_auto_mix_font_point_size = 10;
|
||||
|
||||
Slic3r::PaintedMesh m_painted;
|
||||
std::vector<Slic3r::FilamentMatch> m_current_matches;
|
||||
|
||||
std::unique_ptr<std::thread> m_worker;
|
||||
std::atomic<bool> m_cancel_flag{false};
|
||||
std::mutex m_result_mutex;
|
||||
Slic3r::PaintedMesh m_pending_result;
|
||||
std::function<bool()> m_initial_cancel_callback;
|
||||
std::function<bool(int)> m_initial_progress_callback;
|
||||
bool m_current_computation_initial = false;
|
||||
bool m_initial_computation_pending = false;
|
||||
bool m_initial_computation_cancelled = false;
|
||||
bool m_initial_computation_failed = false;
|
||||
bool m_initial_tooltips_set = false;
|
||||
bool m_current_computation_auto_color = false;
|
||||
Slic3r::TexturePaintingSettings::MeshRepairDecision m_mesh_repair_decision =
|
||||
Slic3r::TexturePaintingSettings::MeshRepairDecision::Ask;
|
||||
|
||||
Button* m_btn_color_4 = nullptr;
|
||||
Button* m_btn_color_8 = nullptr;
|
||||
Button* m_btn_color_16 = nullptr;
|
||||
Button* m_btn_color_auto = nullptr;
|
||||
GreenSlider* m_color_slider = nullptr;
|
||||
SpinInput* m_color_spin = nullptr;
|
||||
GreenSlider* m_smooth_slider = nullptr;
|
||||
SpinInput* m_smooth_spin = nullptr;
|
||||
Button* m_btn_apply = nullptr;
|
||||
|
||||
wxCheckBox* m_auto_merge_cb = nullptr;
|
||||
Button* m_btn_auto_mix = nullptr;
|
||||
Button* m_btn_mix_reset = nullptr;
|
||||
bool m_auto_mix_applied = false;
|
||||
AutoMixSelectPopup* m_auto_mix_popup = nullptr;
|
||||
wxScrolledWindow* m_mapping_scroll = nullptr;
|
||||
wxBoxSizer* m_mapping_sizer = nullptr;
|
||||
std::vector<FilamentMappingRow> m_mapping_rows;
|
||||
FilamentSelectPopup* m_filament_popup = nullptr;
|
||||
int m_filament_popup_row = -1;
|
||||
int m_skip_next_filament_popup_row = -1;
|
||||
|
||||
TexturePreviewCanvas* m_preview_canvas = nullptr;
|
||||
wxPanel* m_tab_panel = nullptr;
|
||||
Button* m_btn_view_original = nullptr;
|
||||
Button* m_btn_view_multicolor = nullptr;
|
||||
|
||||
ProgressDialog* m_progress_dlg = nullptr;
|
||||
|
||||
Button* m_btn_skip = nullptr;
|
||||
Button* m_btn_ok = nullptr;
|
||||
wxStaticText* m_drop_warning_label = nullptr;
|
||||
|
||||
int m_param_color_count = 4;
|
||||
int m_param_smooth = 5;
|
||||
|
||||
int m_applied_color_count = -1;
|
||||
int m_applied_smooth = -1;
|
||||
wxStaticText* m_hint_label = nullptr;
|
||||
|
||||
static const int ID_COLOR_4 = wxID_HIGHEST + 200;
|
||||
static const int ID_COLOR_8 = wxID_HIGHEST + 201;
|
||||
static const int ID_COLOR_16 = wxID_HIGHEST + 202;
|
||||
static const int ID_COLOR_AUTO = wxID_HIGHEST + 203;
|
||||
static const int ID_BTN_APPLY = wxID_HIGHEST + 204;
|
||||
static const int ID_BTN_SKIP = wxID_HIGHEST + 205;
|
||||
static const int ID_VIEW_ORIGINAL = wxID_HIGHEST + 206;
|
||||
static const int ID_VIEW_MULTICOLOR = wxID_HIGHEST + 207;
|
||||
|
||||
wxDECLARE_EVENT_TABLE();
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -87,10 +87,18 @@ void ComboBox::SetSelection(int n)
|
||||
return;
|
||||
drop.SetSelection(n);
|
||||
SetLabel(drop.GetValue());
|
||||
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
|
||||
SetIcon(items[drop.selection].icon_textctrl);
|
||||
else
|
||||
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
|
||||
if (m_keep_drop_arrow) {
|
||||
SetIcon("drop_down");
|
||||
SetIcon_1(items[drop.selection].icon_textctrl);
|
||||
} else {
|
||||
SetIcon(items[drop.selection].icon_textctrl);
|
||||
}
|
||||
} else {
|
||||
SetIcon("drop_down");
|
||||
if (m_keep_drop_arrow)
|
||||
SetIcon_1(wxNullBitmap);
|
||||
}
|
||||
|
||||
if (drop.selection >= 0) {
|
||||
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
|
||||
@@ -120,10 +128,18 @@ void ComboBox::SetValue(const wxString &value)
|
||||
{
|
||||
drop.SetValue(value);
|
||||
SetLabel(value);
|
||||
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk())
|
||||
SetIcon(items[drop.selection].icon_textctrl);
|
||||
else
|
||||
if (drop.selection >= 0 && drop.iconSize.y > 0 && items[drop.selection].icon_textctrl.IsOk()) {
|
||||
if (m_keep_drop_arrow) {
|
||||
SetIcon("drop_down");
|
||||
SetIcon_1(items[drop.selection].icon_textctrl);
|
||||
} else {
|
||||
SetIcon(items[drop.selection].icon_textctrl);
|
||||
}
|
||||
} else {
|
||||
SetIcon("drop_down");
|
||||
if (m_keep_drop_arrow)
|
||||
SetIcon_1(wxNullBitmap);
|
||||
}
|
||||
|
||||
if (drop.selection >= 0) {
|
||||
SetStaticTips(items[drop.selection].text_static_tips, wxNullBitmap);
|
||||
|
||||
@@ -16,6 +16,7 @@ class ComboBox : public wxWindowWithItems<TextInput, wxItemContainer>
|
||||
bool drop_down = false;
|
||||
bool text_off = false;
|
||||
bool is_replace_text_to_image = false;
|
||||
bool m_keep_drop_arrow = false; // When true, item icon goes to icon_1, keeping drop_down arrow
|
||||
wxString replace_text;
|
||||
wxString image_for_text;
|
||||
|
||||
@@ -31,6 +32,11 @@ public:
|
||||
|
||||
DropDown & GetDropDown() { return drop; }
|
||||
|
||||
// When true, item icon is shown as icon_1 (secondary), preserving drop_down arrow.
|
||||
// Note: item bitmaps are set via raw wxBitmap (not ScalableBitmap), so they won't
|
||||
// auto-rescale on DPI change. Caller should recreate items after DPI change.
|
||||
void SetKeepDropArrow(bool keep) { m_keep_drop_arrow = keep; }
|
||||
|
||||
virtual bool SetFont(wxFont const & font) override;
|
||||
|
||||
public:
|
||||
|
||||
@@ -360,6 +360,9 @@ void DropDown::render(wxDC &dc)
|
||||
for (int i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
int states2 = states;
|
||||
// Dimmed items stay selectable but render greyed out (used by the mixed-filament
|
||||
// dialog to show components that are already consumed by another mix).
|
||||
bool is_dimmed = (item.style & DD_ITEM_STYLE_DIMMED) != 0;
|
||||
if ((item.style & DD_ITEM_STYLE_DISABLED) != 0)
|
||||
states2 &= ~StateColor::Enabled;
|
||||
// Skip by group
|
||||
@@ -427,7 +430,7 @@ void DropDown::render(wxDC &dc)
|
||||
}
|
||||
pt.y += (rcContent.height - textSize.y) / 2;
|
||||
dc.SetFont(GetFont());
|
||||
dc.SetTextForeground(text_color.colorForStates(states2));
|
||||
dc.SetTextForeground(is_dimmed ? wxColour(0xCE, 0xCE, 0xCE) : text_color.colorForStates(states2));
|
||||
dc.DrawText(text, pt);
|
||||
if (group.IsEmpty() && !item.group_key.IsEmpty()) {
|
||||
auto szBmp = arrow_bitmap.GetBmpSize();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#define DD_ITEM_STYLE_SPLIT_ITEM 0x0001 // ----text----, text with horizontal line arounds
|
||||
#define DD_ITEM_STYLE_DISABLED 0x0002 // ----text----, text with horizontal line arounds
|
||||
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||
|
||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "../GUI_Utils.hpp"
|
||||
#endif
|
||||
|
||||
wxDEFINE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(SpinInput, StaticBox)
|
||||
|
||||
EVT_KEY_DOWN(SpinInput::keyPressed)
|
||||
@@ -74,6 +76,7 @@ void SpinInput::Create(wxWindow *parent,
|
||||
state_handler.attach_child(text_ctrl);
|
||||
text_ctrl->Bind(wxEVT_KILL_FOCUS, &SpinInput::onTextLostFocus, this);
|
||||
text_ctrl->Bind(wxEVT_TEXT_ENTER, &SpinInput::onTextEnter, this);
|
||||
text_ctrl->Bind(wxEVT_TEXT, &SpinInput::onTextChanged, this);
|
||||
text_ctrl->Bind(wxEVT_KEY_DOWN, &SpinInput::keyPressed, this);
|
||||
text_ctrl->Bind(wxEVT_RIGHT_DOWN, [this](auto &e) {}); // disable context menu
|
||||
button_inc = createButton(true);
|
||||
@@ -300,6 +303,19 @@ void SpinInput::onTextEnter(wxCommandEvent &event)
|
||||
ProcessEventLocally(event);
|
||||
}
|
||||
|
||||
void SpinInput::onTextChanged(wxCommandEvent &event)
|
||||
{
|
||||
long value;
|
||||
if (text_ctrl->GetValue().ToLong(&value)) {
|
||||
wxCommandEvent e(EVT_SPINCTRL_TEXT, GetId());
|
||||
e.SetEventObject(this);
|
||||
e.SetInt((int) value);
|
||||
e.SetString(text_ctrl->GetValue());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
}
|
||||
event.Skip();
|
||||
}
|
||||
|
||||
void SpinInput::mouseWheelMoved(wxMouseEvent &event)
|
||||
{
|
||||
auto delta = event.GetWheelRotation() < 0 ? 1 : -1;
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
class Button;
|
||||
|
||||
// Fired on every keystroke that leaves a parseable integer in the field, so callers can
|
||||
// react live rather than only on commit (wxEVT_SPINCTRL) or Enter. Ported from BambuStudio.
|
||||
wxDECLARE_EVENT(EVT_SPINCTRL_TEXT, wxCommandEvent);
|
||||
|
||||
class SpinInput : public wxNavigationEnabled<StaticBox>
|
||||
{
|
||||
wxSize labelSize;
|
||||
@@ -98,6 +102,7 @@ private:
|
||||
void keyPressed(wxKeyEvent& event);
|
||||
void onTimer(wxTimerEvent &evnet);
|
||||
void onTextLostFocus(wxEvent &event);
|
||||
void onTextChanged(wxCommandEvent &event);
|
||||
void onTextEnter(wxCommandEvent &event);
|
||||
|
||||
void sendSpinEvent();
|
||||
|
||||
@@ -139,6 +139,15 @@ void TextInput::SetIcon_1(const wxString &icon) {
|
||||
Rescale();
|
||||
}
|
||||
|
||||
// Set icon_1 from a raw bitmap. Note: won't auto-rescale on DPI change
|
||||
// since ScalableBitmap::name() will be empty. Caller should re-set after DPI change.
|
||||
void TextInput::SetIcon_1(const wxBitmap &icon) {
|
||||
this->icon_1 = ScalableBitmap();
|
||||
if (icon.IsOk())
|
||||
this->icon_1.bmp() = icon;
|
||||
Rescale();
|
||||
}
|
||||
|
||||
void TextInput::SetLabelColor(StateColor const &color)
|
||||
{
|
||||
label_color = color;
|
||||
|
||||
@@ -54,6 +54,7 @@ public:
|
||||
void SetIcon(const wxString & icon);
|
||||
|
||||
void SetIcon_1(const wxString &icon);
|
||||
void SetIcon_1(const wxBitmap &icon);
|
||||
|
||||
void SetLabelColor(StateColor const &color);
|
||||
|
||||
|
||||
@@ -256,6 +256,41 @@ static std::vector<float> MatrixFlatten(const WipingDialog::VolumeMatrix& matrix
|
||||
return vec;
|
||||
}
|
||||
|
||||
// Mixed-color slots are virtual: they are never loaded into a tray and so have no flushing
|
||||
// volumes of their own. The dialog therefore shows only the physical filaments, which means
|
||||
// converting between the full config matrix (indexed by config slot) and a dense physical
|
||||
// sub-matrix (indexed by row/column in the table).
|
||||
static std::vector<double> extract_physical_sub_matrix(
|
||||
const std::vector<double>& full_matrix, size_t full_n,
|
||||
const std::vector<size_t>& indices)
|
||||
{
|
||||
size_t p = indices.size();
|
||||
std::vector<double> sub(p * p, 0.0);
|
||||
if (full_matrix.size() < full_n * full_n)
|
||||
return sub;
|
||||
for (size_t pi = 0; pi < p; ++pi)
|
||||
for (size_t pj = 0; pj < p; ++pj)
|
||||
sub[pi * p + pj] = full_matrix[indices[pi] * full_n + indices[pj]];
|
||||
return sub;
|
||||
}
|
||||
|
||||
// Write the edited physical sub-matrix back into a copy of the full matrix, leaving the
|
||||
// entries that belong to mixed slots untouched.
|
||||
static std::vector<double> expand_physical_to_full_matrix(
|
||||
const std::vector<double>& sub_matrix,
|
||||
const std::vector<size_t>& indices, size_t full_n,
|
||||
const std::vector<double>& original_matrix)
|
||||
{
|
||||
std::vector<double> full = original_matrix;
|
||||
if (full.size() < full_n * full_n)
|
||||
return full;
|
||||
size_t p = indices.size();
|
||||
for (size_t pi = 0; pi < p; ++pi)
|
||||
for (size_t pj = 0; pj < p; ++pj)
|
||||
full[indices[pi] * full_n + indices[pj]] = sub_matrix[pi * p + pj];
|
||||
return full;
|
||||
}
|
||||
|
||||
wxString WipingDialog::BuildTableObjStr()
|
||||
{
|
||||
auto full_config = wxGetApp().preset_bundle->full_config();
|
||||
@@ -265,9 +300,22 @@ wxString WipingDialog::BuildTableObjStr()
|
||||
auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
|
||||
auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values;
|
||||
|
||||
// Restrict the table to physical filaments; mixed slots have no flushing volumes.
|
||||
m_physical_indices = wxGetApp().preset_bundle->physical_filament_config_indices();
|
||||
const size_t full_n = filament_colors.size();
|
||||
{
|
||||
std::vector<std::string> physical_colors;
|
||||
physical_colors.reserve(m_physical_indices.size());
|
||||
for (size_t i : m_physical_indices)
|
||||
if (i < filament_colors.size())
|
||||
physical_colors.push_back(filament_colors[i]);
|
||||
filament_colors = std::move(physical_colors);
|
||||
}
|
||||
|
||||
std::vector<std::vector<double>> flush_matrixs;
|
||||
for (int idx = 0; idx < nozzle_num; ++idx) {
|
||||
flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
|
||||
auto fm = get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num);
|
||||
flush_matrixs.emplace_back(extract_physical_sub_matrix(fm, full_n, m_physical_indices));
|
||||
}
|
||||
flush_multiplier.resize(nozzle_num, 1);
|
||||
|
||||
@@ -372,7 +420,7 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
|
||||
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
this->SetSizer(main_sizer);
|
||||
this->SetBackgroundColour(*wxWHITE);
|
||||
auto filament_count = wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
|
||||
auto filament_count = wxGetApp().preset_bundle->physical_filament_config_indices().size();
|
||||
|
||||
// Estimate table scroll area size based on filament count
|
||||
// Each table cell is ~60x25 DIP, plus headers and borders
|
||||
@@ -592,11 +640,29 @@ void WipingDialog::StoreFlushData(int extruder_num, const std::vector<std::vecto
|
||||
m_raw_matrixs = flush_volume_vecs;
|
||||
}
|
||||
|
||||
// The table edits a physical-only sub-matrix; GetFlattenMatrix has to hand back a full-size
|
||||
// matrix so the config layout stays indexed by config slot. Mixed-slot entries keep whatever
|
||||
// the config already held.
|
||||
std::vector<double> WipingDialog::ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const
|
||||
{
|
||||
const auto& project_config = wxGetApp().preset_bundle->project_config;
|
||||
const size_t full_n = project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
|
||||
if (m_physical_indices.size() == full_n)
|
||||
return sub_matrix; // no mixed slots: sub-matrix already is the full matrix
|
||||
|
||||
auto raw = project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
|
||||
int nozzle_num = (int)wxGetApp().preset_bundle->project_config.option<ConfigOptionFloats>("flush_multiplier")->values.size();
|
||||
if (nozzle_num < 1) nozzle_num = 1;
|
||||
auto original = get_flush_volumes_matrix(raw, nozzle_idx, nozzle_num);
|
||||
return expand_physical_to_full_matrix(sub_matrix, m_physical_indices, full_n, original);
|
||||
}
|
||||
|
||||
std::vector<double> WipingDialog::GetFlattenMatrix()const
|
||||
{
|
||||
std::vector<double> ret;
|
||||
for (auto& matrix : m_raw_matrixs) {
|
||||
ret.insert(ret.end(), matrix.begin(), matrix.end());
|
||||
for (size_t idx = 0; idx < m_raw_matrixs.size(); ++idx) {
|
||||
auto full = ExpandToFullMatrix(m_raw_matrixs[idx], (int)idx);
|
||||
ret.insert(ret.end(), full.begin(), full.end());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -58,12 +58,16 @@ private:
|
||||
wxString BuildTableObjStr();
|
||||
wxString BuildTextObjStr(bool multi_language = true);
|
||||
void StoreFlushData(int extruder_num, const std::vector<std::vector<double>>& flush_volume_vecs, const std::vector<double>& flush_multipliers);
|
||||
// Maps the physical-only matrix shown in the table back onto the full config-indexed matrix.
|
||||
std::vector<double> ExpandToFullMatrix(const std::vector<double>& sub_matrix, int nozzle_idx) const;
|
||||
|
||||
wxWebView* m_webview;
|
||||
int m_max_flush_volume;
|
||||
|
||||
VolumeMatrix m_raw_matrixs;
|
||||
std::vector<double> m_flush_multipliers;
|
||||
// Config indices of the physical (non-mixed) filaments, in table order.
|
||||
std::vector<size_t> m_physical_indices;
|
||||
bool m_submit_flag{ false };
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user