mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 07:11:10 +00:00
Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI scroll areas. Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm. This makes Preferences and other dialogs scroll consistently across Windows DPI settings.
4362 lines
170 KiB
C++
4362 lines
170 KiB
C++
#include <glad/gl.h>
|
|
#include "OpenGLManager.hpp"
|
|
|
|
#include "TextureImportDialog.hpp"
|
|
#include "I18N.hpp"
|
|
#include "GUI_App.hpp"
|
|
#include "MsgDialog.hpp"
|
|
#include "ColorDecomposeDialog.hpp"
|
|
#include "ColorDecomposeSupport.hpp"
|
|
#include "Widgets/StateColor.hpp"
|
|
#include "Widgets/StaticLine.hpp"
|
|
#include "libslic3r/ColorDecomposeRecipe.hpp"
|
|
#include "libslic3r/MeshBoolean.hpp"
|
|
#include "libslic3r/TriangleSelector.hpp"
|
|
|
|
#include <wx/button.h>
|
|
#include <wx/colour.h>
|
|
#include <wx/colordlg.h>
|
|
#include <wx/dcclient.h>
|
|
#include <wx/dcbuffer.h>
|
|
#include <wx/display.h>
|
|
#include <wx/evtloop.h>
|
|
#include <wx/statline.h>
|
|
#include <wx/scrolwin.h>
|
|
#include <wx/msgdlg.h>
|
|
#include <wx/utils.h>
|
|
#include <wx/valtext.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <set>
|
|
#include <sstream>
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE = "PLA Basic";
|
|
static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE = "PLA";
|
|
static constexpr const char* DEFAULT_VIRTUAL_FILAMENT_NAME = "Bambu PLA Basic";
|
|
|
|
static bool is_dark() { return Slic3r::GUI::wxGetApp().dark_mode(); }
|
|
|
|
static wxColour texture_import_gray9000()
|
|
{
|
|
return wxColour(38, 46, 48);
|
|
}
|
|
|
|
static wxColour texture_import_text_colour()
|
|
{
|
|
return StateColor::darkModeColorFor(texture_import_gray9000());
|
|
}
|
|
|
|
// StaticLine::SetLineColour stores the raw key and resolves it itself when it paints, so those
|
|
// sinks take SEPARATOR_COLOUR_KEY directly; only raw wx sinks need the resolved form below.
|
|
static constexpr const char* SEPARATOR_COLOUR_KEY = "#CECECE";
|
|
|
|
static wxColour texture_import_separator_colour()
|
|
{
|
|
return StateColor::darkModeColorFor(wxColour(SEPARATOR_COLOUR_KEY));
|
|
}
|
|
|
|
// Orca's confirm palette, applied here rather than through Button::SetStyle because these buttons
|
|
// keep custom pill geometry that SetStyle resets. The Disabled entries are load-bearing: without
|
|
// one, StateColor::colorForStates falls through to the Normal entry and a disabled button paints
|
|
// as a live accent button.
|
|
static void apply_accent_button_colours(Button* btn)
|
|
{
|
|
btn->SetBackgroundColor(StateColor(
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Disabled),
|
|
std::pair<wxColour, int>(wxColour(0, 137, 123), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour(38, 166, 154), StateColor::Hovered),
|
|
std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Normal)));
|
|
btn->SetBorderColor(StateColor(
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Disabled),
|
|
std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Normal)));
|
|
btn->SetTextColor(StateColor(
|
|
std::pair<wxColour, int>(wxColour("#6B6B6A"), StateColor::Disabled),
|
|
std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Normal)));
|
|
}
|
|
|
|
// The same button while the parameters behind it are dirty: still clickable, but reading as
|
|
// "what you see is not what this button would apply".
|
|
static void apply_muted_button_colours(Button* btn)
|
|
{
|
|
btn->SetBackgroundColor(wxColour("#CECECE"));
|
|
btn->SetBorderColor(wxColour("#CECECE"));
|
|
btn->SetTextColor(wxColour("#6B6B6A"));
|
|
}
|
|
|
|
static wxFont texture_import_section_title_font(wxWindow* win)
|
|
{
|
|
wxFont font = win ? win->GetFont() : wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
|
|
font.MakeBold();
|
|
return font;
|
|
}
|
|
|
|
static wxSize gl_viewport_size(wxWindow* win, const wxSize& logical_size)
|
|
{
|
|
wxSize viewport_size = logical_size;
|
|
#ifdef __APPLE__
|
|
const double scale = win ? win->GetContentScaleFactor() : 1.0;
|
|
if (scale > 0.0) {
|
|
viewport_size.x = std::max(1, (int)std::round(viewport_size.x * scale));
|
|
viewport_size.y = std::max(1, (int)std::round(viewport_size.y * scale));
|
|
}
|
|
#else
|
|
(void)win;
|
|
#endif
|
|
return viewport_size;
|
|
}
|
|
|
|
class ScopedInteractiveBusyCursorSuspender
|
|
{
|
|
public:
|
|
ScopedInteractiveBusyCursorSuspender()
|
|
{
|
|
#if defined(__WXMSW__) || defined(__APPLE__)
|
|
while (wxIsBusy()) {
|
|
wxEndBusyCursor();
|
|
++m_suspended_count;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
~ScopedInteractiveBusyCursorSuspender()
|
|
{
|
|
#if defined(__WXMSW__) || defined(__APPLE__)
|
|
for (int i = 0; i < m_suspended_count; ++i)
|
|
wxBeginBusyCursor();
|
|
#endif
|
|
}
|
|
|
|
private:
|
|
#if defined(__WXMSW__) || defined(__APPLE__)
|
|
int m_suspended_count = 0;
|
|
#endif
|
|
};
|
|
|
|
static bool needs_filament_swatch_border(const wxColour& colour)
|
|
{
|
|
if (is_dark())
|
|
return colour.Red() < 45 && colour.Green() < 45 && colour.Blue() < 45;
|
|
return colour.Red() > 224 && colour.Green() > 224 && colour.Blue() > 224;
|
|
}
|
|
|
|
static wxColour filament_swatch_border_colour()
|
|
{
|
|
return is_dark() ? wxColour(207, 207, 207) : wxColour(130, 130, 128);
|
|
}
|
|
|
|
static void draw_filament_swatch_border(wxDC& dc, const wxColour& colour,
|
|
int x, int y, int w, int h, int radius = 0)
|
|
{
|
|
if (!needs_filament_swatch_border(colour))
|
|
return;
|
|
|
|
dc.SetPen(wxPen(filament_swatch_border_colour(), 1));
|
|
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
|
if (radius > 0)
|
|
dc.DrawRoundedRectangle(x, y, w, h, radius);
|
|
else
|
|
dc.DrawRectangle(x, y, w, h);
|
|
}
|
|
|
|
static void draw_filament_swatch_ellipse_border(wxDC& dc, const wxColour& colour,
|
|
int x, int y, int w, int h)
|
|
{
|
|
if (!needs_filament_swatch_border(colour))
|
|
return;
|
|
|
|
dc.SetPen(wxPen(filament_swatch_border_colour(), 1));
|
|
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
|
dc.DrawEllipse(x, y, w, h);
|
|
}
|
|
|
|
static wxString ellipsize_text(wxDC& dc, wxString text, int max_width)
|
|
{
|
|
if (max_width <= 0)
|
|
return wxEmptyString;
|
|
if (dc.GetTextExtent(text).x <= max_width)
|
|
return text;
|
|
|
|
const wxString ellipsis = "...";
|
|
while (!text.empty() && dc.GetTextExtent(text + ellipsis).x > max_width)
|
|
text.RemoveLast();
|
|
if (text.empty() && dc.GetTextExtent(ellipsis).x > max_width)
|
|
return wxString();
|
|
return text + ellipsis;
|
|
}
|
|
|
|
// ============================================================
|
|
// AccentSlider — thin track + accent-coloured triangle thumb
|
|
// ============================================================
|
|
|
|
class AccentSlider : public wxPanel {
|
|
public:
|
|
AccentSlider(wxWindow* parent, int value, int minVal, int maxVal,
|
|
const wxPoint& pos = wxDefaultPosition,
|
|
const wxSize& size = wxDefaultSize);
|
|
~AccentSlider() override;
|
|
int GetValue() const;
|
|
void SetValue(int val);
|
|
bool Enable(bool enable = true) override;
|
|
private:
|
|
void OnPaint(wxPaintEvent&);
|
|
void OnMouse(wxMouseEvent&);
|
|
int xFromValue() const;
|
|
int valueFromX(int x) const;
|
|
int m_value, m_min, m_max;
|
|
bool m_dragging = false;
|
|
};
|
|
|
|
AccentSlider::AccentSlider(wxWindow* parent, int value, int minVal, int maxVal,
|
|
const wxPoint& pos, const wxSize& size)
|
|
: wxPanel(parent, wxID_ANY, pos, size.IsFullySpecified() ? size : wxSize(-1, parent->FromDIP(24)),
|
|
wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE)
|
|
, m_value(std::clamp(value, minVal, maxVal)), m_min(minVal), m_max(maxVal)
|
|
{
|
|
SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
SetMinSize(wxSize(-1, FromDIP(24)));
|
|
|
|
Bind(wxEVT_PAINT, &AccentSlider::OnPaint, this);
|
|
Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) {
|
|
evt.Skip();
|
|
Refresh();
|
|
});
|
|
Bind(wxEVT_LEFT_DOWN, &AccentSlider::OnMouse, this);
|
|
Bind(wxEVT_LEFT_UP, &AccentSlider::OnMouse, this);
|
|
Bind(wxEVT_MOTION, &AccentSlider::OnMouse, this);
|
|
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) { m_dragging = false; });
|
|
}
|
|
|
|
AccentSlider::~AccentSlider()
|
|
{
|
|
// See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it
|
|
// still holds the capture wedges mouse input for the whole application.
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
}
|
|
|
|
int AccentSlider::GetValue() const { return m_value; }
|
|
|
|
void AccentSlider::SetValue(int val)
|
|
{
|
|
val = std::clamp(val, m_min, m_max);
|
|
if (val != m_value) { m_value = val; Refresh(); }
|
|
}
|
|
|
|
bool AccentSlider::Enable(bool enable)
|
|
{
|
|
bool ok = wxPanel::Enable(enable);
|
|
Refresh();
|
|
return ok;
|
|
}
|
|
|
|
int AccentSlider::xFromValue() const
|
|
{
|
|
wxSize sz = GetClientSize();
|
|
int margin = FromDIP(6);
|
|
int track_w = sz.x - 2 * margin;
|
|
if (m_max <= m_min || track_w <= 0) return margin;
|
|
return margin + (m_value - m_min) * track_w / (m_max - m_min);
|
|
}
|
|
|
|
int AccentSlider::valueFromX(int x) const
|
|
{
|
|
wxSize sz = GetClientSize();
|
|
int margin = FromDIP(6);
|
|
int track_w = sz.x - 2 * margin;
|
|
if (track_w <= 0 || m_max <= m_min) return m_min;
|
|
int val = m_min + (x - margin) * (m_max - m_min) / track_w;
|
|
return std::clamp(val, m_min, m_max);
|
|
}
|
|
|
|
void AccentSlider::OnPaint(wxPaintEvent&)
|
|
{
|
|
wxAutoBufferedPaintDC dc(this);
|
|
wxSize sz = GetClientSize();
|
|
|
|
dc.SetBackground(wxBrush(GetParent()->GetBackgroundColour()));
|
|
dc.Clear();
|
|
|
|
int margin = FromDIP(6);
|
|
int track_y = sz.y / 2;
|
|
int ts = FromDIP(8);
|
|
int pen_w = FromDIP(2);
|
|
|
|
wxColour accent_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#009688") : wxColour("#ACACAC"));
|
|
wxColour track_clr = StateColor::darkModeColorFor(IsEnabled() ? wxColour("#CECECE") : wxColour("#DFDFDF"));
|
|
|
|
int tx = xFromValue();
|
|
|
|
dc.SetPen(wxPen(accent_clr, pen_w));
|
|
dc.DrawLine(margin, track_y, tx, track_y);
|
|
|
|
dc.SetPen(wxPen(track_clr, pen_w));
|
|
dc.DrawLine(tx, track_y, sz.x - margin, track_y);
|
|
|
|
wxPoint tri[3] = {
|
|
{tx, track_y + FromDIP(1)},
|
|
{tx - ts / 2, track_y + FromDIP(1) + ts},
|
|
{tx + ts / 2, track_y + FromDIP(1) + ts}
|
|
};
|
|
dc.SetBrush(wxBrush(accent_clr));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawPolygon(3, tri);
|
|
}
|
|
|
|
void AccentSlider::OnMouse(wxMouseEvent& evt)
|
|
{
|
|
if (!IsEnabled()) return;
|
|
|
|
auto update = [&](int x) {
|
|
int nv = valueFromX(x);
|
|
if (nv != m_value) {
|
|
m_value = nv;
|
|
Refresh();
|
|
wxCommandEvent e(wxEVT_SLIDER, GetId());
|
|
e.SetEventObject(this);
|
|
ProcessWindowEvent(e);
|
|
}
|
|
};
|
|
|
|
if (evt.LeftDown()) {
|
|
m_dragging = true;
|
|
if (!HasCapture()) CaptureMouse();
|
|
update(evt.GetX());
|
|
} else if (evt.LeftUp()) {
|
|
m_dragging = false;
|
|
if (HasCapture()) ReleaseMouse();
|
|
} else if (evt.Dragging() && m_dragging) {
|
|
update(evt.GetX());
|
|
}
|
|
}
|
|
|
|
namespace Slic3r { namespace GUI {
|
|
|
|
wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_DONE, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_PROGRESS, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_TEXTURE_COMPUTE_ERROR, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_TEXTURE_MESH_REPAIR_DECISION, wxCommandEvent);
|
|
|
|
static std::array<float, 4> parse_color_string(const std::string& hex)
|
|
{
|
|
std::array<float, 4> c = {1.f, 1.f, 1.f, 1.f};
|
|
if (hex.size() >= 7 && hex[0] == '#') {
|
|
unsigned long val = std::strtoul(hex.c_str() + 1, nullptr, 16);
|
|
c[0] = ((val >> 16) & 0xFF) / 255.f;
|
|
c[1] = ((val >> 8) & 0xFF) / 255.f;
|
|
c[2] = ((val ) & 0xFF) / 255.f;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
static wxString rgb_to_hex(const std::array<std::size_t, 3>& c)
|
|
{
|
|
return wxString::Format("#%02X%02X%02X",
|
|
(unsigned)c[0], (unsigned)c[1], (unsigned)c[2]);
|
|
}
|
|
|
|
static wxString filament_name_to_wx_string(const std::string& name)
|
|
{
|
|
wxString utf8_name = wxString::FromUTF8(name.c_str());
|
|
if (!utf8_name.empty() || name.empty())
|
|
return utf8_name;
|
|
return wxString(name);
|
|
}
|
|
|
|
static std::string texture_normalize_color_hex(std::string hex)
|
|
{
|
|
if (hex.empty())
|
|
return "#808080";
|
|
if (hex.front() != '#')
|
|
hex = "#" + hex;
|
|
return decompose_normalize_color_hex(std::move(hex));
|
|
}
|
|
|
|
static std::string texture_rgba_to_hex(const std::array<float, 4>& rgba)
|
|
{
|
|
return wxString::Format("#%02X%02X%02X",
|
|
(unsigned char)std::clamp(rgba[0] * 255.f, 0.f, 255.f),
|
|
(unsigned char)std::clamp(rgba[1] * 255.f, 0.f, 255.f),
|
|
(unsigned char)std::clamp(rgba[2] * 255.f, 0.f, 255.f)).ToStdString();
|
|
}
|
|
|
|
static bool texture_entry_is_physical(TextureFilamentKind kind)
|
|
{
|
|
return kind == TextureFilamentKind::ExistingPhysical || kind == TextureFilamentKind::NewPhysical;
|
|
}
|
|
|
|
static bool texture_entry_is_mixed(TextureFilamentKind kind)
|
|
{
|
|
return kind == TextureFilamentKind::ExistingMixed || kind == TextureFilamentKind::NewMixed;
|
|
}
|
|
|
|
static bool texture_entry_is_pla_basic(const TextureFilamentEntry& entry)
|
|
{
|
|
return entry.type == DEFAULT_VIRTUAL_FILAMENT_SHORT_TYPE || entry.type == DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE ||
|
|
entry.name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos ||
|
|
entry.preset_name.find(DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE) != std::string::npos;
|
|
}
|
|
|
|
static bool texture_entry_official_basic(const TextureFilamentEntry& entry)
|
|
{
|
|
if (!texture_entry_is_physical(entry.kind))
|
|
return false;
|
|
// NewPhysical entries are created by add_virtual_filament with a fixed Bambu Basic name.
|
|
if (entry.kind == TextureFilamentKind::NewPhysical)
|
|
return !official_basic_type_from_preset_name(entry.name).empty();
|
|
// ExistingPhysical: resolve the filament preset name from project_config_index.
|
|
auto& pb = *wxGetApp().preset_bundle;
|
|
const size_t cfg = entry.project_config_index;
|
|
if (cfg < pb.filament_presets.size())
|
|
return !official_basic_type_from_preset_name(pb.filament_presets[cfg]).empty();
|
|
return false;
|
|
}
|
|
|
|
static Slic3r::ColorDecomposeRecipeMode texture_recipe_mode(TextureAutoMixMode mode)
|
|
{
|
|
return mode == TextureAutoMixMode::CMYW ? Slic3r::ColorDecomposeRecipeMode::CMYW :
|
|
Slic3r::ColorDecomposeRecipeMode::RYBW;
|
|
}
|
|
|
|
static bool starts_with_preset_name(const std::string& name, const char* prefix)
|
|
{
|
|
const size_t prefix_len = std::strlen(prefix);
|
|
return name.size() >= prefix_len && name.compare(0, prefix_len, prefix) == 0;
|
|
}
|
|
|
|
static std::string resolve_default_virtual_filament_preset_name()
|
|
{
|
|
auto* preset_bundle = wxGetApp().preset_bundle;
|
|
if (!preset_bundle)
|
|
return {};
|
|
|
|
auto valid_preset_name = [preset_bundle](const std::string& name) -> bool {
|
|
return !name.empty() && preset_bundle->filaments.find_preset(name, false) != nullptr;
|
|
};
|
|
|
|
const auto* default_profiles = preset_bundle->printers.get_selected_preset()
|
|
.config.option<ConfigOptionStrings>("default_filament_profile");
|
|
if (default_profiles) {
|
|
for (const std::string& name : default_profiles->values) {
|
|
if (starts_with_preset_name(name, DEFAULT_VIRTUAL_FILAMENT_NAME) && valid_preset_name(name))
|
|
return name;
|
|
}
|
|
}
|
|
|
|
for (const Preset& preset : preset_bundle->filaments.get_presets()) {
|
|
if (preset.is_system && preset.is_visible && preset.is_compatible &&
|
|
starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) {
|
|
return preset.name;
|
|
}
|
|
}
|
|
|
|
for (const Preset& preset : preset_bundle->filaments.get_presets()) {
|
|
if (preset.is_visible && preset.is_compatible &&
|
|
starts_with_preset_name(preset.name, DEFAULT_VIRTUAL_FILAMENT_NAME)) {
|
|
return preset.name;
|
|
}
|
|
}
|
|
|
|
std::string selected = preset_bundle->filaments.get_selected_preset_name();
|
|
return valid_preset_name(selected) ? selected : std::string();
|
|
}
|
|
|
|
static wxString auto_mix_mode_label(TextureAutoMixMode mode)
|
|
{
|
|
return mode == TextureAutoMixMode::CMYW ? _L("One-click CMYW auto-mix") :
|
|
_L("One-click RYBW auto-mix");
|
|
}
|
|
|
|
static wxPoint constrained_dialog_position(wxWindow* anchor, const wxSize& dialog_size)
|
|
{
|
|
if (!anchor)
|
|
return wxDefaultPosition;
|
|
|
|
wxSize size = dialog_size;
|
|
if (size.x <= 0 || size.y <= 0)
|
|
size = wxSize(anchor->FromDIP(450), anchor->FromDIP(350));
|
|
|
|
wxPoint pos = anchor->ClientToScreen(wxPoint(0, anchor->GetSize().y));
|
|
wxRect display_rect;
|
|
int display_idx = wxDisplay::GetFromPoint(pos);
|
|
if (display_idx != wxNOT_FOUND)
|
|
display_rect = wxDisplay(display_idx).GetClientArea();
|
|
else
|
|
display_rect = wxDisplay().GetClientArea();
|
|
|
|
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
|
std::max(display_rect.GetLeft(), display_rect.GetRight() - size.x));
|
|
pos.y = std::clamp(pos.y, display_rect.GetTop(),
|
|
std::max(display_rect.GetTop(), display_rect.GetBottom() - size.y));
|
|
return pos;
|
|
}
|
|
|
|
// ============================================================
|
|
// FilamentSelectPopup
|
|
// ============================================================
|
|
|
|
class FilamentSelectPopup : public PopupWindow
|
|
{
|
|
public:
|
|
FilamentSelectPopup(wxWindow* parent,
|
|
const std::vector<TextureFilamentEntry>& entries,
|
|
const std::vector<std::array<float, 4>>& colors_rgba,
|
|
const std::vector<std::string>& names,
|
|
size_t existing_count,
|
|
int popup_width,
|
|
wxWindow* dialog_anchor,
|
|
std::function<void(int)> on_select,
|
|
std::function<void(wxColour)> on_add_filament,
|
|
std::function<void()> on_decompose_color,
|
|
std::function<bool()> can_add_filament,
|
|
std::function<void(bool)> on_close,
|
|
std::vector<int> display_numbers)
|
|
: PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS)
|
|
, m_entries(entries)
|
|
, m_colors_rgba(colors_rgba)
|
|
, m_names(names)
|
|
, m_dialog_anchor(dialog_anchor)
|
|
, m_on_select(std::move(on_select))
|
|
, m_on_add_filament(std::move(on_add_filament))
|
|
, m_on_decompose_color(std::move(on_decompose_color))
|
|
, m_can_add_filament(std::move(can_add_filament))
|
|
, m_on_close(std::move(on_close))
|
|
, m_display_numbers(std::move(display_numbers))
|
|
{
|
|
wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
SetBackgroundColour(pop_bg);
|
|
|
|
m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
|
|
m_content->SetBackgroundColour(pop_bg);
|
|
m_content->SetScrollRate(0, FromDIP(20));
|
|
auto* outer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
const int pop_w = std::max(FromDIP(213), popup_width);
|
|
const int row_h = FromDIP(32);
|
|
const int pad = FromDIP(8);
|
|
const int max_visible_rows = 10;
|
|
const wxColour header_clr = StateColor::darkModeColorFor(wxColour("#ACACAC"));
|
|
|
|
auto add_section_header = [&](const wxString& label) {
|
|
auto* hdr = new wxStaticText(m_content, wxID_ANY, label);
|
|
wxFont hf = hdr->GetFont();
|
|
hf.SetPointSize(9);
|
|
hdr->SetFont(hf);
|
|
hdr->SetForegroundColour(header_clr);
|
|
outer->Add(hdr, 0, wxLEFT | wxRIGHT | wxTOP, pad);
|
|
auto* line = new StaticLine(m_content);
|
|
line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY));
|
|
outer->Add(line, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
|
};
|
|
|
|
auto add_section = [&](const wxString& label, TextureFilamentKind kind) {
|
|
bool has_any = false;
|
|
for (const auto& entry : m_entries) {
|
|
if (entry.kind == kind) {
|
|
has_any = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!has_any)
|
|
return;
|
|
add_section_header(label);
|
|
for (const auto& entry : m_entries) {
|
|
if (entry.kind != kind)
|
|
continue;
|
|
wxPanel* row = texture_entry_is_mixed(entry.kind) ? create_mixed_item_row(entry, row_h)
|
|
: create_item_row((size_t)entry.dialog_index, row_h);
|
|
outer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, pad);
|
|
}
|
|
};
|
|
|
|
// Section order matches compute_display_numbers() so the visible IDs
|
|
// ascend monotonically (ExistingPhysical -> NewPhysical -> ExistingMixed
|
|
// -> NewMixed) instead of jumping (e.g. 1,2 -> 7 -> 3,4,5,6 -> 8,9,10).
|
|
add_section(_L("Project Physical Filaments"), TextureFilamentKind::ExistingPhysical);
|
|
add_section(_L("New Physical Filaments"), TextureFilamentKind::NewPhysical);
|
|
add_section(_L("Project Mixed Filaments"), TextureFilamentKind::ExistingMixed);
|
|
add_section(_L("New Mixed Filaments"), TextureFilamentKind::NewMixed);
|
|
|
|
auto* decompose_label = new wxStaticText(this, wxID_ANY, _L("Decompose Color"));
|
|
auto* add_label = new wxStaticText(this, wxID_ANY, _L("+ Add Material"));
|
|
wxFont af = add_label->GetFont();
|
|
af.SetPointSize(10);
|
|
add_label->SetFont(af);
|
|
decompose_label->SetFont(af);
|
|
const bool add_enabled = !m_can_add_filament || m_can_add_filament();
|
|
const wxColour action_clr = StateColor::darkModeColorFor(wxColour("#009688"));
|
|
add_label->SetForegroundColour(add_enabled ? action_clr : header_clr);
|
|
decompose_label->SetForegroundColour(add_enabled ? action_clr : header_clr);
|
|
add_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW));
|
|
decompose_label->SetCursor(wxCursor(add_enabled ? wxCURSOR_HAND : wxCURSOR_ARROW));
|
|
if (!add_enabled)
|
|
add_label->SetToolTip(wxString::Format(
|
|
_L("The project supports up to %d filaments. Extra filaments will be discarded."),
|
|
(int)EnforcerBlockerType::ExtruderMax));
|
|
decompose_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) {
|
|
if (m_can_add_filament && !m_can_add_filament())
|
|
return;
|
|
auto on_decompose_color = m_on_decompose_color;
|
|
m_closing_from_action = true;
|
|
Dismiss();
|
|
if (on_decompose_color)
|
|
on_decompose_color();
|
|
});
|
|
add_label->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) {
|
|
if (m_can_add_filament && !m_can_add_filament()) {
|
|
return;
|
|
}
|
|
auto on_add_filament = m_on_add_filament;
|
|
wxWindow* popup_parent = GetParent();
|
|
wxWindow* color_anchor = m_dialog_anchor ? m_dialog_anchor : popup_parent;
|
|
m_closing_from_action = true;
|
|
Dismiss();
|
|
wxColourData cd;
|
|
cd.SetChooseFull(true);
|
|
wxColourDialog dlg(popup_parent, &cd);
|
|
auto move_color_dialog = [&dlg, color_anchor]() {
|
|
dlg.Move(constrained_dialog_position(color_anchor, dlg.GetBestSize()));
|
|
};
|
|
dlg.Bind(wxEVT_SHOW, [move_color_dialog](wxShowEvent& e) mutable {
|
|
e.Skip();
|
|
if (e.IsShown())
|
|
move_color_dialog();
|
|
});
|
|
move_color_dialog();
|
|
if (dlg.ShowModal() == wxID_OK) {
|
|
wxColour clr = dlg.GetColourData().GetColour();
|
|
if (on_add_filament) on_add_filament(clr);
|
|
}
|
|
});
|
|
|
|
m_content->SetSizer(outer);
|
|
m_content->FitInside();
|
|
|
|
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
|
|
int list_h = outer->GetMinSize().y;
|
|
if (m_colors_rgba.size() > max_visible_rows)
|
|
list_h -= ((int)m_colors_rgba.size() - max_visible_rows) * row_h;
|
|
m_content->SetMinSize(wxSize(pop_w, list_h));
|
|
m_content->SetMaxSize(wxSize(pop_w, list_h));
|
|
top_sizer->Add(m_content, 0, wxEXPAND);
|
|
|
|
top_sizer->AddSpacer(FromDIP(4));
|
|
auto* sep_line = new StaticLine(this);
|
|
sep_line->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY));
|
|
top_sizer->Add(sep_line, 0, wxEXPAND | wxLEFT | wxRIGHT, pad);
|
|
top_sizer->Add(decompose_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
|
auto* sep_line2 = new StaticLine(this);
|
|
sep_line2->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY));
|
|
top_sizer->Add(sep_line2, 0, wxEXPAND | wxLEFT | wxRIGHT, pad);
|
|
top_sizer->Add(add_label, 0, wxALIGN_CENTER_HORIZONTAL | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, pad);
|
|
SetSizerAndFit(top_sizer);
|
|
|
|
SetSize(pop_w, top_sizer->GetMinSize().y);
|
|
}
|
|
|
|
private:
|
|
void OnDismiss() override
|
|
{
|
|
restore_cursor_state();
|
|
if (m_on_close) m_on_close(m_closing_from_action);
|
|
m_closing_from_action = false;
|
|
wxPopupTransientWindow::OnDismiss();
|
|
schedule_destroy();
|
|
}
|
|
|
|
void restore_cursor_state()
|
|
{
|
|
SetCursor(wxNullCursor);
|
|
if (m_content)
|
|
m_content->SetCursor(wxNullCursor);
|
|
if (m_dialog_anchor)
|
|
m_dialog_anchor->SetCursor(wxCursor(wxCURSOR_HAND));
|
|
wxSetCursor(wxNullCursor);
|
|
}
|
|
|
|
void schedule_destroy()
|
|
{
|
|
if (m_destroy_scheduled)
|
|
return;
|
|
m_destroy_scheduled = true;
|
|
CallAfter([this]() { Destroy(); });
|
|
}
|
|
|
|
wxPanel* create_item_row(size_t idx, int row_h)
|
|
{
|
|
wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4"));
|
|
wxColour name_fg = texture_import_text_colour();
|
|
|
|
wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h));
|
|
row->SetBackgroundColour(row_bg);
|
|
row->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
row->SetCursor(wxCursor(wxCURSOR_HAND));
|
|
|
|
const int sq = row->FromDIP(24);
|
|
const int sq_r = row->FromDIP(2);
|
|
const int sq_x = row->FromDIP(4);
|
|
const int gap1 = row->FromDIP(8);
|
|
|
|
wxColour fil_clr = idx < m_colors_rgba.size()
|
|
? wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f),
|
|
(unsigned char)(m_colors_rgba[idx][1] * 255.f),
|
|
(unsigned char)(m_colors_rgba[idx][2] * 255.f))
|
|
: wxColour(128, 128, 128);
|
|
|
|
wxString name_str = (idx < m_names.size()) ? filament_name_to_wx_string(m_names[idx])
|
|
: wxString::Format("Filament %d", display_number((int)idx));
|
|
row->SetToolTip(name_str);
|
|
|
|
row->Bind(wxEVT_PAINT, [this, idx, sq, sq_r, sq_x, gap1, fil_clr, name_str, row_bg, hover_bg, name_fg](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
|
|
bool hovered = (m_hover_idx == (int)idx);
|
|
dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
int sq_y = (sz.y - sq) / 2;
|
|
wxColour paint_clr = fil_clr;
|
|
if (idx < m_colors_rgba.size()) {
|
|
paint_clr = wxColour((unsigned char)(m_colors_rgba[idx][0] * 255.f),
|
|
(unsigned char)(m_colors_rgba[idx][1] * 255.f),
|
|
(unsigned char)(m_colors_rgba[idx][2] * 255.f));
|
|
}
|
|
dc.SetBrush(wxBrush(paint_clr));
|
|
dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r);
|
|
draw_filament_swatch_border(dc, paint_clr, sq_x, sq_y, sq, sq, sq_r);
|
|
|
|
{
|
|
wxFont nf = p->GetFont();
|
|
nf.SetPointSize(9);
|
|
dc.SetFont(nf);
|
|
dc.SetTextForeground(paint_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000());
|
|
wxString ns = wxString::Format("%d", display_number((int)idx));
|
|
wxSize tsz = dc.GetTextExtent(ns);
|
|
dc.DrawText(ns, sq_x + (sq - tsz.x) / 2, sq_y + (sq - tsz.y) / 2);
|
|
}
|
|
|
|
// Material name
|
|
{
|
|
wxFont mf = p->GetFont();
|
|
mf.SetPointSize(10);
|
|
dc.SetFont(mf);
|
|
dc.SetTextForeground(name_fg);
|
|
int tx = sq_x + sq + gap1;
|
|
wxString display = ellipsize_text(dc, name_str, sz.x - tx - p->FromDIP(4));
|
|
wxSize tsz = dc.GetTextExtent(display);
|
|
if (!display.empty())
|
|
dc.DrawText(display, tx, (sz.y - tsz.y) / 2);
|
|
}
|
|
});
|
|
|
|
row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) {
|
|
if (m_hover_idx != (int)idx) {
|
|
m_hover_idx = (int)idx;
|
|
m_content->Refresh();
|
|
}
|
|
evt.Skip();
|
|
});
|
|
row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) {
|
|
if (m_hover_idx != -1) {
|
|
m_hover_idx = -1;
|
|
m_content->Refresh();
|
|
}
|
|
evt.Skip();
|
|
});
|
|
|
|
row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) {
|
|
if (m_on_select) m_on_select((int)idx);
|
|
m_closing_from_action = true;
|
|
Dismiss();
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
wxPanel* create_mixed_item_row(const TextureFilamentEntry& entry, int row_h)
|
|
{
|
|
wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4"));
|
|
wxColour name_fg = texture_import_text_colour();
|
|
const int idx = entry.dialog_index;
|
|
|
|
wxPanel* row = new wxPanel(m_content, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h));
|
|
row->SetBackgroundColour(row_bg);
|
|
row->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
row->SetCursor(wxCursor(wxCURSOR_HAND));
|
|
row->SetToolTip(entry.name.empty() ? wxString::Format("Filament %d", display_number(idx)) : filament_name_to_wx_string(entry.name));
|
|
|
|
row->Bind(wxEVT_PAINT, [this, entry, idx, row_bg, hover_bg, name_fg](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
const bool hovered = (m_hover_idx == idx);
|
|
dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
wxFont font = p->GetFont();
|
|
font.SetPointSize(9);
|
|
dc.SetFont(font);
|
|
int x = p->FromDIP(2);
|
|
const int sw = p->FromDIP(22);
|
|
const int sw_r = p->FromDIP(2);
|
|
const int y = (sz.y - sw) / 2;
|
|
|
|
for (size_t ci = 0; ci < entry.mixed_components.size() && ci < entry.mixed_ratios.size(); ++ci) {
|
|
if (ci > 0) {
|
|
dc.SetTextForeground(name_fg);
|
|
wxString plus = "+";
|
|
wxSize psz = dc.GetTextExtent(plus);
|
|
dc.DrawText(plus, x, (sz.y - psz.y) / 2);
|
|
x += psz.x + p->FromDIP(4);
|
|
}
|
|
|
|
const unsigned int comp_id = entry.mixed_components[ci];
|
|
const int comp_dialog_idx = comp_id >= 1 ? (int)comp_id - 1 : -1;
|
|
wxColour comp_clr("#D9D9D9");
|
|
if (comp_dialog_idx >= 0 && comp_dialog_idx < (int)m_colors_rgba.size()) {
|
|
const auto& c = m_colors_rgba[comp_dialog_idx];
|
|
comp_clr = wxColour((unsigned char)(c[0] * 255.f),
|
|
(unsigned char)(c[1] * 255.f),
|
|
(unsigned char)(c[2] * 255.f));
|
|
}
|
|
|
|
dc.SetBrush(wxBrush(comp_clr));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRoundedRectangle(x, y, sw, sw, sw_r);
|
|
draw_filament_swatch_border(dc, comp_clr, x, y, sw, sw, sw_r);
|
|
|
|
wxString num = wxString::Format("%d", display_number(comp_dialog_idx));
|
|
wxSize nsz = dc.GetTextExtent(num);
|
|
dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000());
|
|
dc.DrawText(num, x + (sw - nsz.x) / 2, y + (sw - nsz.y) / 2);
|
|
x += sw + p->FromDIP(4);
|
|
|
|
dc.SetTextForeground(name_fg);
|
|
wxString pct = wxString::Format("%d%%", entry.mixed_ratios[ci]);
|
|
wxSize psz = dc.GetTextExtent(pct);
|
|
dc.DrawText(pct, x, (sz.y - psz.y) / 2);
|
|
x += psz.x + p->FromDIP(4);
|
|
}
|
|
});
|
|
|
|
row->Bind(wxEVT_MOTION, [this, idx](wxMouseEvent& evt) {
|
|
if (m_hover_idx != idx) {
|
|
m_hover_idx = idx;
|
|
m_content->Refresh();
|
|
}
|
|
evt.Skip();
|
|
});
|
|
row->Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent& evt) {
|
|
if (m_hover_idx != -1) {
|
|
m_hover_idx = -1;
|
|
m_content->Refresh();
|
|
}
|
|
evt.Skip();
|
|
});
|
|
row->Bind(wxEVT_LEFT_DOWN, [this, idx](wxMouseEvent&) {
|
|
if (m_on_select) m_on_select(idx);
|
|
m_closing_from_action = true;
|
|
Dismiss();
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
wxScrolledWindow* m_content = nullptr;
|
|
std::vector<TextureFilamentEntry> m_entries;
|
|
std::vector<std::array<float, 4>> m_colors_rgba;
|
|
std::vector<std::string> m_names;
|
|
wxWindow* m_dialog_anchor = nullptr;
|
|
std::function<void(int)> m_on_select;
|
|
std::function<void(wxColour)> m_on_add_filament;
|
|
std::function<void()> m_on_decompose_color;
|
|
std::function<bool()> m_can_add_filament;
|
|
std::function<void(bool)> m_on_close;
|
|
// 1-based display number per dialog_index, mirroring the post-apply
|
|
// sidebar ordering (ExistingPhysical, NewPhysical, ExistingMixed, NewMixed).
|
|
std::vector<int> m_display_numbers;
|
|
int m_hover_idx = -1;
|
|
bool m_closing_from_action = false;
|
|
bool m_destroy_scheduled = false;
|
|
|
|
// Returns the display number for a dialog_index, falling back to idx + 1
|
|
// when no mapping is available (e.g. index out of range).
|
|
int display_number(int idx) const {
|
|
return (idx >= 0 && idx < (int)m_display_numbers.size() && m_display_numbers[idx] > 0)
|
|
? m_display_numbers[idx] : idx + 1;
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// AutoMixSelectPopup
|
|
// ============================================================
|
|
|
|
class AutoMixSelectPopup : public PopupWindow
|
|
{
|
|
public:
|
|
AutoMixSelectPopup(wxWindow* parent,
|
|
TextureAutoMixMode current_mode,
|
|
int popup_width,
|
|
int font_point_size,
|
|
std::function<void(TextureAutoMixMode)> on_select,
|
|
std::function<void()> on_close)
|
|
: PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS)
|
|
, m_current_mode(current_mode)
|
|
, m_font_point_size(font_point_size)
|
|
, m_on_select(std::move(on_select))
|
|
, m_on_close(std::move(on_close))
|
|
{
|
|
wxColour pop_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
SetBackgroundColour(pop_bg);
|
|
|
|
auto* content = new wxPanel(this, wxID_ANY);
|
|
content->SetBackgroundColour(pop_bg);
|
|
content->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
|
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
|
const int row_h = FromDIP(36);
|
|
const int pop_w = std::max(FromDIP(216), popup_width);
|
|
sizer->Add(create_item_row(content, TextureAutoMixMode::CMYW, row_h), 0, wxEXPAND);
|
|
sizer->Add(create_item_row(content, TextureAutoMixMode::RYBW, row_h), 0, wxEXPAND);
|
|
content->SetSizer(sizer);
|
|
content->SetMinSize(wxSize(pop_w, row_h * 2));
|
|
|
|
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
|
|
top_sizer->Add(content, 0, wxEXPAND | wxALL, FromDIP(4));
|
|
SetSizerAndFit(top_sizer);
|
|
SetSize(pop_w, top_sizer->GetMinSize().y);
|
|
}
|
|
|
|
private:
|
|
void OnDismiss() override
|
|
{
|
|
if (m_on_close)
|
|
m_on_close();
|
|
wxPopupTransientWindow::OnDismiss();
|
|
CallAfter([this]() { Destroy(); });
|
|
}
|
|
|
|
wxPanel* create_item_row(wxWindow* parent, TextureAutoMixMode mode, int row_h)
|
|
{
|
|
wxColour row_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
wxColour hover_bg = StateColor::darkModeColorFor(wxColour("#F4F4F4"));
|
|
wxColour text_fg = texture_import_text_colour();
|
|
wxColour accent = StateColor::darkModeColorFor(wxColour("#009688"));
|
|
|
|
wxPanel* row = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h),
|
|
wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE);
|
|
row->SetBackgroundColour(row_bg);
|
|
row->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
row->SetCursor(wxCursor(wxCURSOR_HAND));
|
|
|
|
const int row_idx = mode == TextureAutoMixMode::CMYW ? 0 : 1;
|
|
row->Bind(wxEVT_PAINT, [this, row_bg, hover_bg, text_fg, accent, mode, row_idx](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
const bool hovered = (m_hover_idx == row_idx);
|
|
const bool selected = (m_current_mode == mode);
|
|
|
|
dc.SetBrush(wxBrush(hovered ? hover_bg : row_bg));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
wxFont font = p->GetFont();
|
|
font.SetPointSize(m_font_point_size);
|
|
dc.SetFont(font);
|
|
dc.SetTextForeground(text_fg);
|
|
wxString label = auto_mix_mode_label(mode);
|
|
wxSize tsz = dc.GetTextExtent(label);
|
|
dc.DrawText(label, p->FromDIP(12), (sz.y - tsz.y) / 2);
|
|
|
|
if (selected) {
|
|
wxFont check_font = p->GetFont();
|
|
check_font.SetPointSize(12);
|
|
check_font.MakeBold();
|
|
dc.SetFont(check_font);
|
|
dc.SetTextForeground(accent);
|
|
wxString check = wxString::FromUTF8("✓");
|
|
wxSize csz = dc.GetTextExtent(check);
|
|
dc.DrawText(check, sz.x - p->FromDIP(16) - csz.x, (sz.y - csz.y) / 2);
|
|
}
|
|
});
|
|
|
|
row->Bind(wxEVT_MOTION, [this, row, row_idx](wxMouseEvent& evt) {
|
|
if (m_hover_idx != row_idx) {
|
|
m_hover_idx = row_idx;
|
|
row->Refresh();
|
|
}
|
|
evt.Skip();
|
|
});
|
|
row->Bind(wxEVT_LEAVE_WINDOW, [this, row](wxMouseEvent& evt) {
|
|
m_hover_idx = -1;
|
|
row->Refresh();
|
|
evt.Skip();
|
|
});
|
|
row->Bind(wxEVT_LEFT_DOWN, [this, mode](wxMouseEvent&) {
|
|
if (m_on_select)
|
|
m_on_select(mode);
|
|
Dismiss();
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
TextureAutoMixMode m_current_mode;
|
|
int m_font_point_size = 10;
|
|
int m_hover_idx = -1;
|
|
std::function<void(TextureAutoMixMode)> m_on_select;
|
|
std::function<void()> m_on_close;
|
|
};
|
|
|
|
// ============================================================
|
|
// TexturePreviewCanvas
|
|
// ============================================================
|
|
|
|
TexturePreviewCanvas::TexturePreviewCanvas(wxWindow* parent, const wxGLAttributes& attrs)
|
|
: wxGLCanvas(parent, attrs, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE)
|
|
{
|
|
m_context = new wxGLContext(this);
|
|
|
|
Bind(wxEVT_PAINT, &TexturePreviewCanvas::on_paint, this);
|
|
Bind(wxEVT_SIZE, &TexturePreviewCanvas::on_size, this);
|
|
Bind(wxEVT_MOUSEWHEEL, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_LEFT_DOWN, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_LEFT_UP, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_RIGHT_DOWN, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_RIGHT_UP, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_MIDDLE_DOWN, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_MIDDLE_UP, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_MOTION, &TexturePreviewCanvas::on_mouse, this);
|
|
Bind(wxEVT_MOUSE_CAPTURE_LOST, [this](wxMouseCaptureLostEvent&) {
|
|
m_drag_mode = DragMode::None;
|
|
m_reset_overlay_pressed = false;
|
|
});
|
|
Bind(wxEVT_LEAVE_WINDOW, &TexturePreviewCanvas::on_mouse, this);
|
|
}
|
|
|
|
TexturePreviewCanvas::~TexturePreviewCanvas()
|
|
{
|
|
// See MixedFilamentDialog::~MixedFilamentDialog: a widget destroyed while it
|
|
// still holds the capture wedges mouse input for the whole application.
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
|
|
if (m_context) {
|
|
SetCurrent(*m_context);
|
|
if (m_tex_id)
|
|
glDeleteTextures(1, &m_tex_id);
|
|
for (unsigned int id : m_gl_tex_ids)
|
|
if (id) glDeleteTextures(1, &id);
|
|
for (unsigned int id : {m_reset_icon_tex, m_reset_icon_hover_tex,
|
|
m_reset_icon_dark_tex, m_reset_icon_dark_hover_tex})
|
|
if (id) glDeleteTextures(1, &id);
|
|
delete m_context;
|
|
}
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_mesh_data(
|
|
const std::vector<std::array<float, 3>>& vertices,
|
|
const std::vector<std::array<int, 3>>& indices)
|
|
{
|
|
m_vertices = vertices;
|
|
m_indices = indices;
|
|
update_bounding_box();
|
|
compute_smooth_normals();
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::compute_smooth_normals()
|
|
{
|
|
m_vertex_normals.clear();
|
|
if (m_vertices.empty() || m_indices.empty()) return;
|
|
|
|
m_vertex_normals.resize(m_vertices.size(), {0.f, 0.f, 0.f});
|
|
|
|
for (const auto& face : m_indices) {
|
|
int i0 = face[0], i1 = face[1], i2 = face[2];
|
|
if (i0 < 0 || i0 >= (int)m_vertices.size() ||
|
|
i1 < 0 || i1 >= (int)m_vertices.size() ||
|
|
i2 < 0 || i2 >= (int)m_vertices.size())
|
|
continue;
|
|
|
|
const auto& v0 = m_vertices[i0];
|
|
const auto& v1 = m_vertices[i1];
|
|
const auto& v2 = m_vertices[i2];
|
|
|
|
float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]);
|
|
float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]);
|
|
float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]);
|
|
|
|
m_vertex_normals[i0][0] += nx; m_vertex_normals[i0][1] += ny; m_vertex_normals[i0][2] += nz;
|
|
m_vertex_normals[i1][0] += nx; m_vertex_normals[i1][1] += ny; m_vertex_normals[i1][2] += nz;
|
|
m_vertex_normals[i2][0] += nx; m_vertex_normals[i2][1] += ny; m_vertex_normals[i2][2] += nz;
|
|
}
|
|
|
|
for (auto& n : m_vertex_normals) {
|
|
float len = std::sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]);
|
|
if (len > 1e-8f) { n[0] /= len; n[1] /= len; n[2] /= len; }
|
|
}
|
|
}
|
|
|
|
void TexturePreviewCanvas::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)
|
|
{
|
|
m_uvs = uvs;
|
|
m_tex_w = tex_w;
|
|
m_tex_h = tex_h;
|
|
m_tex_channels = tex_channels;
|
|
m_tex_dirty = true;
|
|
|
|
size_t sz = (size_t)tex_w * tex_h * tex_channels;
|
|
m_tex_data.assign(tex_data, tex_data + sz);
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::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)
|
|
{
|
|
m_tex_pixels_rgb = tex_pixels_rgb;
|
|
m_tex_widths = tex_widths;
|
|
m_tex_heights = tex_heights;
|
|
m_face_uvs = face_uvs;
|
|
m_face_tex_ids = face_tex_ids;
|
|
m_multi_tex_dirty = true;
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::upload_textures()
|
|
{
|
|
if (!m_multi_tex_dirty) return;
|
|
m_multi_tex_dirty = false;
|
|
|
|
for (unsigned int id : m_gl_tex_ids)
|
|
if (id) glDeleteTextures(1, &id);
|
|
m_gl_tex_ids.clear();
|
|
|
|
m_gl_tex_ids.resize(m_tex_pixels_rgb.size(), 0);
|
|
for (size_t i = 0; i < m_tex_pixels_rgb.size(); ++i) {
|
|
if (m_tex_pixels_rgb[i].empty() || m_tex_widths[i] <= 0 || m_tex_heights[i] <= 0)
|
|
continue;
|
|
GLuint tex_id = 0;
|
|
glGenTextures(1, &tex_id);
|
|
glBindTexture(GL_TEXTURE_2D, tex_id);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_tex_widths[i], m_tex_heights[i],
|
|
0, GL_RGB, GL_UNSIGNED_BYTE, m_tex_pixels_rgb[i].data());
|
|
m_gl_tex_ids[i] = tex_id;
|
|
}
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_painted_mesh_data(
|
|
const std::vector<std::array<float, 3>>& vertices,
|
|
const std::vector<std::array<int, 3>>& indices)
|
|
{
|
|
m_painted_vertices = vertices;
|
|
m_painted_indices = indices;
|
|
Refresh();
|
|
}
|
|
|
|
static void convert_face_colors(const std::vector<std::array<std::size_t, 3>>& src,
|
|
std::vector<std::array<float, 3>>& dst)
|
|
{
|
|
dst.resize(src.size());
|
|
for (size_t i = 0; i < src.size(); ++i)
|
|
dst[i] = { src[i][0] / 255.f, src[i][1] / 255.f, src[i][2] / 255.f };
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors)
|
|
{
|
|
convert_face_colors(face_colors, m_face_colors_rgb);
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_original_face_colors(const std::vector<std::array<std::size_t, 3>>& face_colors)
|
|
{
|
|
convert_face_colors(face_colors, m_original_face_colors_rgb);
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_filament_color_map(
|
|
const std::map<std::array<std::size_t, 3>, std::array<float, 3>>& color_map)
|
|
{
|
|
m_color_map = color_map;
|
|
m_filament_colors_rgb.resize(m_face_colors_rgb.size());
|
|
for (size_t i = 0; i < m_face_colors_rgb.size(); ++i) {
|
|
std::array<std::size_t, 3> key = {
|
|
(std::size_t)(m_face_colors_rgb[i][0] * 255.f + 0.5f),
|
|
(std::size_t)(m_face_colors_rgb[i][1] * 255.f + 0.5f),
|
|
(std::size_t)(m_face_colors_rgb[i][2] * 255.f + 0.5f)
|
|
};
|
|
auto it = color_map.find(key);
|
|
if (it != color_map.end())
|
|
m_filament_colors_rgb[i] = it->second;
|
|
else
|
|
m_filament_colors_rgb[i] = m_face_colors_rgb[i];
|
|
}
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_render_mode(RenderMode mode)
|
|
{
|
|
if (m_mode != mode) {
|
|
m_mode = mode;
|
|
Refresh();
|
|
}
|
|
}
|
|
|
|
void TexturePreviewCanvas::set_computing_overlay(bool /*show*/)
|
|
{
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::reset_view()
|
|
{
|
|
m_zoom = 1.0f;
|
|
m_rot_x = -30.0f;
|
|
m_rot_y = 30.0f;
|
|
m_pan_x = 0.0f;
|
|
m_pan_y = 0.0f;
|
|
Refresh();
|
|
}
|
|
|
|
wxRect TexturePreviewCanvas::reset_overlay_rect() const
|
|
{
|
|
wxSize sz = GetClientSize();
|
|
const int button_size = FromDIP(40);
|
|
const int margin = FromDIP(20);
|
|
return wxRect(
|
|
std::max(margin, sz.x - button_size - margin),
|
|
std::max(margin, sz.y - button_size - margin),
|
|
button_size,
|
|
button_size);
|
|
}
|
|
|
|
unsigned int TexturePreviewCanvas::upload_reset_icon_texture(const std::string& icon_name)
|
|
{
|
|
wxBitmap bmp = create_scaled_bitmap(icon_name, this, 40);
|
|
if (!bmp.IsOk())
|
|
return 0;
|
|
|
|
wxImage image = bmp.ConvertToImage();
|
|
if (!image.IsOk())
|
|
return 0;
|
|
|
|
const int w = image.GetWidth();
|
|
const int h = image.GetHeight();
|
|
const unsigned char* rgb = image.GetData();
|
|
const unsigned char* alpha = image.HasAlpha() ? image.GetAlpha() : nullptr;
|
|
if (!rgb || w <= 0 || h <= 0)
|
|
return 0;
|
|
|
|
std::vector<unsigned char> rgba((size_t)w * h * 4);
|
|
for (int i = 0; i < w * h; ++i) {
|
|
rgba[(size_t)i * 4 + 0] = rgb[i * 3 + 0];
|
|
rgba[(size_t)i * 4 + 1] = rgb[i * 3 + 1];
|
|
rgba[(size_t)i * 4 + 2] = rgb[i * 3 + 2];
|
|
rgba[(size_t)i * 4 + 3] = alpha ? alpha[i] : 255;
|
|
}
|
|
|
|
GLuint tex_id = 0;
|
|
glGenTextures(1, &tex_id);
|
|
glBindTexture(GL_TEXTURE_2D, tex_id);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
return tex_id;
|
|
}
|
|
|
|
void TexturePreviewCanvas::upload_reset_icon_textures()
|
|
{
|
|
if (m_reset_icon_tex && m_reset_icon_hover_tex && m_reset_icon_dark_tex && m_reset_icon_dark_hover_tex)
|
|
return;
|
|
|
|
if (!m_reset_icon_tex)
|
|
m_reset_icon_tex = upload_reset_icon_texture("canvas_zoom");
|
|
if (!m_reset_icon_hover_tex)
|
|
m_reset_icon_hover_tex = upload_reset_icon_texture("canvas_zoom_hover");
|
|
if (!m_reset_icon_dark_tex)
|
|
m_reset_icon_dark_tex = upload_reset_icon_texture("canvas_zoom_dark");
|
|
if (!m_reset_icon_dark_hover_tex)
|
|
m_reset_icon_dark_hover_tex = upload_reset_icon_texture("canvas_zoom_dark_hover");
|
|
}
|
|
|
|
bool TexturePreviewCanvas::handle_reset_overlay_mouse(wxMouseEvent& evt)
|
|
{
|
|
if (evt.Leaving()) {
|
|
if (m_reset_overlay_pressed) {
|
|
m_reset_overlay_hovered = false;
|
|
m_reset_overlay_pressed = false;
|
|
SetCursor(wxCursor(wxCURSOR_ARROW));
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
Refresh();
|
|
return true;
|
|
}
|
|
if (m_reset_overlay_hovered) {
|
|
m_reset_overlay_hovered = false;
|
|
SetCursor(wxCursor(wxCURSOR_ARROW));
|
|
Refresh();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const bool over = reset_overlay_rect().Contains(evt.GetPosition());
|
|
if (over != m_reset_overlay_hovered) {
|
|
m_reset_overlay_hovered = over;
|
|
SetCursor(wxCursor(over ? wxCURSOR_HAND : wxCURSOR_ARROW));
|
|
Refresh();
|
|
}
|
|
|
|
if (m_drag_mode != DragMode::None && !m_reset_overlay_pressed)
|
|
return false;
|
|
|
|
if (evt.LeftDown() && over) {
|
|
m_reset_overlay_pressed = true;
|
|
if (!HasCapture())
|
|
CaptureMouse();
|
|
Refresh();
|
|
return true;
|
|
}
|
|
|
|
if (evt.LeftUp() && m_reset_overlay_pressed) {
|
|
const bool activate = over;
|
|
m_reset_overlay_pressed = false;
|
|
if (HasCapture())
|
|
ReleaseMouse();
|
|
if (activate)
|
|
reset_view();
|
|
else
|
|
Refresh();
|
|
return true;
|
|
}
|
|
|
|
return over;
|
|
}
|
|
|
|
void TexturePreviewCanvas::update_bounding_box()
|
|
{
|
|
if (m_vertices.empty()) return;
|
|
std::array<float, 3> mn = m_vertices[0], mx = m_vertices[0];
|
|
for (const auto& v : m_vertices) {
|
|
for (int i = 0; i < 3; ++i) {
|
|
mn[i] = std::min(mn[i], v[i]);
|
|
mx[i] = std::max(mx[i], v[i]);
|
|
}
|
|
}
|
|
m_center = { (mn[0]+mx[0])/2, (mn[1]+mx[1])/2, (mn[2]+mx[2])/2 };
|
|
float dx = mx[0]-mn[0], dy = mx[1]-mn[1], dz = mx[2]-mn[2];
|
|
m_radius = std::sqrt(dx*dx + dy*dy + dz*dz) / 2.0f;
|
|
if (m_radius < 1e-6f) m_radius = 1.0f;
|
|
}
|
|
|
|
void TexturePreviewCanvas::ensure_gl_ready()
|
|
{
|
|
if (m_gl_initialized) return;
|
|
|
|
// BBS loads the GL entry points here with GLEW; Orca loads them centrally in
|
|
// OpenGLManager, so only check that this has already happened (glad leaves unresolved
|
|
// entry points null) and drain any stale error state.
|
|
if (glGetString == nullptr) {
|
|
BOOST_LOG_TRIVIAL(error) << "TexturePreviewCanvas: OpenGL functions are not loaded yet";
|
|
return;
|
|
}
|
|
while (glGetError() != GL_NO_ERROR) {}
|
|
|
|
m_gl_initialized = true;
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glEnable(GL_LIGHTING);
|
|
glEnable(GL_LIGHT0);
|
|
glEnable(GL_COLOR_MATERIAL);
|
|
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
|
|
|
|
GLfloat light_pos[] = { 0.5f, 1.0f, 1.0f, 0.0f };
|
|
GLfloat light_ambient[] = { 0.3f, 0.3f, 0.3f, 1.0f };
|
|
GLfloat light_diffuse[] = { 0.8f, 0.8f, 0.8f, 1.0f };
|
|
glLightfv(GL_LIGHT0, GL_POSITION, light_pos);
|
|
glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
|
|
glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);
|
|
}
|
|
|
|
void TexturePreviewCanvas::on_paint(wxPaintEvent&)
|
|
{
|
|
wxPaintDC dc(this);
|
|
if (!m_context) return;
|
|
SetCurrent(*m_context);
|
|
ensure_gl_ready();
|
|
render();
|
|
SwapBuffers();
|
|
}
|
|
|
|
void TexturePreviewCanvas::on_size(wxSizeEvent&)
|
|
{
|
|
Refresh();
|
|
}
|
|
|
|
void TexturePreviewCanvas::on_mouse(wxMouseEvent& evt)
|
|
{
|
|
if (handle_reset_overlay_mouse(evt))
|
|
return;
|
|
|
|
if (evt.LeftDown()) {
|
|
m_drag_mode = DragMode::Rotate;
|
|
m_last_mouse_pos = evt.GetPosition();
|
|
if (!HasCapture()) CaptureMouse();
|
|
}
|
|
else if (evt.LeftUp()) {
|
|
if (m_drag_mode == DragMode::Rotate) {
|
|
m_drag_mode = DragMode::None;
|
|
if (HasCapture()) ReleaseMouse();
|
|
}
|
|
}
|
|
else if (evt.RightDown()) {
|
|
m_drag_mode = DragMode::Pan;
|
|
m_last_mouse_pos = evt.GetPosition();
|
|
if (!HasCapture()) CaptureMouse();
|
|
}
|
|
else if (evt.MiddleDown()) {
|
|
m_drag_mode = DragMode::Pan;
|
|
m_last_mouse_pos = evt.GetPosition();
|
|
if (!HasCapture()) CaptureMouse();
|
|
}
|
|
else if (evt.RightUp() || evt.MiddleUp()) {
|
|
if (m_drag_mode == DragMode::Pan) {
|
|
m_drag_mode = DragMode::None;
|
|
if (HasCapture()) ReleaseMouse();
|
|
}
|
|
}
|
|
else if (evt.Dragging() && m_drag_mode != DragMode::None) {
|
|
wxPoint pos = evt.GetPosition();
|
|
float dx = (float)(pos.x - m_last_mouse_pos.x);
|
|
float dy = (float)(pos.y - m_last_mouse_pos.y);
|
|
|
|
if (m_drag_mode == DragMode::Rotate) {
|
|
m_rot_y += dx * 0.5f;
|
|
m_rot_x += dy * 0.5f;
|
|
m_rot_x = std::max(-89.0f, std::min(89.0f, m_rot_x));
|
|
} else if (m_drag_mode == DragMode::Pan) {
|
|
wxSize sz = GetClientSize();
|
|
if (sz.x > 0)
|
|
m_pan_x += dx / (float)sz.x * m_radius * 2.0f / m_zoom;
|
|
if (sz.y > 0)
|
|
m_pan_y -= dy / (float)sz.y * m_radius * 2.0f / m_zoom;
|
|
}
|
|
|
|
m_last_mouse_pos = pos;
|
|
Refresh();
|
|
}
|
|
else if (evt.GetWheelRotation() != 0) {
|
|
float delta = evt.GetWheelRotation() > 0 ? 1.1f : 0.9f;
|
|
m_zoom *= delta;
|
|
m_zoom = std::max(0.1f, std::min(20.0f, m_zoom));
|
|
Refresh();
|
|
}
|
|
}
|
|
|
|
void TexturePreviewCanvas::render()
|
|
{
|
|
wxSize sz = GetClientSize();
|
|
if (sz.x <= 0 || sz.y <= 0) return;
|
|
|
|
wxSize viewport_sz = gl_viewport_size(this, sz);
|
|
glViewport(0, 0, viewport_sz.x, viewport_sz.y);
|
|
// Same palette key as the preview container, so canvas and frame cannot drift apart.
|
|
const wxColour clear_clr = StateColor::darkModeColorFor(wxColour("#EEEEEE"));
|
|
glClearColor(clear_clr.Red() / 255.f, clear_clr.Green() / 255.f, clear_clr.Blue() / 255.f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
glMatrixMode(GL_PROJECTION);
|
|
glLoadIdentity();
|
|
float aspect = (float)viewport_sz.x / (float)viewport_sz.y;
|
|
float dist = m_radius * 3.0f / m_zoom;
|
|
float near_plane = dist * 0.01f;
|
|
float far_plane = dist * 10.0f;
|
|
float fov_rad = 45.0f * static_cast<float>(M_PI) / 180.0f;
|
|
float f = 1.0f / std::tan(fov_rad / 2.0f);
|
|
float proj[16] = {};
|
|
proj[0] = f / aspect;
|
|
proj[5] = f;
|
|
proj[10] = (far_plane + near_plane) / (near_plane - far_plane);
|
|
proj[11] = -1.0f;
|
|
proj[14] = (2.0f * far_plane * near_plane) / (near_plane - far_plane);
|
|
glMultMatrixf(proj);
|
|
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glLoadIdentity();
|
|
glTranslatef(0.0f, 0.0f, -dist);
|
|
glTranslatef(m_pan_x, m_pan_y, 0.0f);
|
|
glRotatef(m_rot_x, 1.0f, 0.0f, 0.0f);
|
|
glRotatef(m_rot_y, 0.0f, 1.0f, 0.0f);
|
|
glTranslatef(-m_center[0], -m_center[1], -m_center[2]);
|
|
|
|
render_mesh();
|
|
render_reset_overlay(sz, viewport_sz);
|
|
}
|
|
|
|
void TexturePreviewCanvas::render_reset_overlay(const wxSize& logical_size, const wxSize& viewport_size)
|
|
{
|
|
if (logical_size.x <= 0 || logical_size.y <= 0 || viewport_size.x <= 0 || viewport_size.y <= 0)
|
|
return;
|
|
|
|
upload_reset_icon_textures();
|
|
|
|
const unsigned int tex_id = is_dark()
|
|
? (m_reset_overlay_hovered ? m_reset_icon_dark_hover_tex : m_reset_icon_dark_tex)
|
|
: (m_reset_overlay_hovered ? m_reset_icon_hover_tex : m_reset_icon_tex);
|
|
if (!tex_id)
|
|
return;
|
|
|
|
wxRect rc = reset_overlay_rect();
|
|
const float sx = (float)viewport_size.x / (float)logical_size.x;
|
|
const float sy = (float)viewport_size.y / (float)logical_size.y;
|
|
const float x0 = rc.GetLeft() * sx;
|
|
const float y0 = rc.GetTop() * sy;
|
|
const float x1 = (rc.GetLeft() + rc.GetWidth()) * sx;
|
|
const float y1 = (rc.GetTop() + rc.GetHeight()) * sy;
|
|
const float alpha = m_reset_overlay_hovered ? 1.0f : 0.78f;
|
|
|
|
glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_TEXTURE_BIT | GL_DEPTH_BUFFER_BIT);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_LIGHTING);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
glEnable(GL_TEXTURE_2D);
|
|
glBindTexture(GL_TEXTURE_2D, tex_id);
|
|
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
|
|
|
|
glMatrixMode(GL_PROJECTION);
|
|
glPushMatrix();
|
|
glLoadIdentity();
|
|
glOrtho(0.0, viewport_size.x, viewport_size.y, 0.0, -1.0, 1.0);
|
|
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glPushMatrix();
|
|
glLoadIdentity();
|
|
|
|
glColor4f(1.0f, 1.0f, 1.0f, alpha);
|
|
glBegin(GL_QUADS);
|
|
glTexCoord2f(0.0f, 0.0f); glVertex2f(x0, y0);
|
|
glTexCoord2f(1.0f, 0.0f); glVertex2f(x1, y0);
|
|
glTexCoord2f(1.0f, 1.0f); glVertex2f(x1, y1);
|
|
glTexCoord2f(0.0f, 1.0f); glVertex2f(x0, y1);
|
|
glEnd();
|
|
|
|
glPopMatrix();
|
|
glMatrixMode(GL_PROJECTION);
|
|
glPopMatrix();
|
|
glMatrixMode(GL_MODELVIEW);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
glPopAttrib();
|
|
}
|
|
|
|
void TexturePreviewCanvas::render_textured_original()
|
|
{
|
|
if (m_vertices.empty() || m_indices.empty()) return;
|
|
if (m_face_uvs.empty() || m_face_tex_ids.empty()) return;
|
|
if (m_face_uvs.size() != m_indices.size()) return;
|
|
|
|
upload_textures();
|
|
|
|
const bool has_smooth = (m_vertex_normals.size() == m_vertices.size());
|
|
|
|
// Group faces by texture id for batch rendering
|
|
std::map<int, std::vector<size_t>> tex_groups;
|
|
for (size_t fi = 0; fi < m_indices.size(); ++fi) {
|
|
int tid = (fi < m_face_tex_ids.size()) ? m_face_tex_ids[fi] : -1;
|
|
tex_groups[tid].push_back(fi);
|
|
}
|
|
|
|
glEnable(GL_LIGHTING);
|
|
glColor3f(1.0f, 1.0f, 1.0f);
|
|
|
|
for (const auto& [tid, face_list] : tex_groups) {
|
|
bool tex_bound = false;
|
|
if (tid >= 0 && tid < (int)m_gl_tex_ids.size() && m_gl_tex_ids[tid] != 0) {
|
|
glEnable(GL_TEXTURE_2D);
|
|
glBindTexture(GL_TEXTURE_2D, m_gl_tex_ids[tid]);
|
|
tex_bound = true;
|
|
} else {
|
|
glDisable(GL_TEXTURE_2D);
|
|
}
|
|
|
|
glBegin(GL_TRIANGLES);
|
|
for (size_t fi : face_list) {
|
|
const auto& face = m_indices[fi];
|
|
const auto& uvs = m_face_uvs[fi];
|
|
|
|
if (!tex_bound) {
|
|
if (fi < m_original_face_colors_rgb.size())
|
|
glColor3fv(m_original_face_colors_rgb[fi].data());
|
|
else
|
|
glColor3f(0.7f, 0.7f, 0.7f);
|
|
}
|
|
|
|
for (int vi = 0; vi < 3; ++vi) {
|
|
int idx = face[vi];
|
|
if (idx < 0 || idx >= (int)m_vertices.size()) continue;
|
|
|
|
if (has_smooth) {
|
|
glNormal3fv(m_vertex_normals[idx].data());
|
|
} else if (vi == 0) {
|
|
const auto& v0 = m_vertices[face[0]];
|
|
const auto& v1 = m_vertices[face[1]];
|
|
const auto& v2 = m_vertices[face[2]];
|
|
float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]);
|
|
float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]);
|
|
float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]);
|
|
float len = std::sqrt(nx*nx + ny*ny + nz*nz);
|
|
if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; }
|
|
glNormal3f(nx, ny, nz);
|
|
}
|
|
|
|
if (tex_bound)
|
|
glTexCoord2fv(uvs[vi].data());
|
|
glVertex3fv(m_vertices[idx].data());
|
|
}
|
|
}
|
|
glEnd();
|
|
}
|
|
|
|
glDisable(GL_TEXTURE_2D);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
}
|
|
|
|
void TexturePreviewCanvas::render_mesh()
|
|
{
|
|
if (m_vertices.empty() || m_indices.empty()) return;
|
|
|
|
// Original mode with texture data: use proper texture mapping
|
|
if (m_mode == RenderMode::Original && !m_face_uvs.empty()) {
|
|
render_textured_original();
|
|
return;
|
|
}
|
|
|
|
// For Multi-Color / FilamentMap, use the painted (remeshed) geometry if available;
|
|
// the face color arrays match the painted mesh, not the original mesh.
|
|
const bool use_painted = (m_mode != RenderMode::Original)
|
|
&& !m_painted_vertices.empty()
|
|
&& !m_painted_indices.empty();
|
|
|
|
const auto& verts = use_painted ? m_painted_vertices : m_vertices;
|
|
const auto& faces = use_painted ? m_painted_indices : m_indices;
|
|
|
|
const std::vector<std::array<float, 3>>* colors_ptr = nullptr;
|
|
if (m_mode == RenderMode::Original && !m_original_face_colors_rgb.empty()
|
|
&& m_original_face_colors_rgb.size() == m_indices.size()) {
|
|
colors_ptr = &m_original_face_colors_rgb;
|
|
} else if (m_mode == RenderMode::FilamentMap && !m_filament_colors_rgb.empty()
|
|
&& m_filament_colors_rgb.size() == faces.size()) {
|
|
colors_ptr = &m_filament_colors_rgb;
|
|
} else if (!m_face_colors_rgb.empty() && m_face_colors_rgb.size() == faces.size()) {
|
|
colors_ptr = &m_face_colors_rgb;
|
|
}
|
|
|
|
// Use smooth normals for the original mesh when available
|
|
const bool has_smooth = !use_painted
|
|
&& (m_vertex_normals.size() == m_vertices.size());
|
|
|
|
glDisable(GL_TEXTURE_2D);
|
|
glEnable(GL_LIGHTING);
|
|
|
|
glBegin(GL_TRIANGLES);
|
|
for (size_t fi = 0; fi < faces.size(); ++fi) {
|
|
if (colors_ptr)
|
|
glColor3fv((*colors_ptr)[fi].data());
|
|
else
|
|
glColor3f(0.7f, 0.7f, 0.7f);
|
|
|
|
const auto& face = faces[fi];
|
|
for (int vi = 0; vi < 3; ++vi) {
|
|
int idx = face[vi];
|
|
if (idx < 0 || idx >= (int)verts.size()) continue;
|
|
|
|
if (has_smooth && idx < (int)m_vertex_normals.size()) {
|
|
glNormal3fv(m_vertex_normals[idx].data());
|
|
} else if (vi == 0) {
|
|
const auto& v0 = verts[face[0]];
|
|
const auto& v1 = verts[face[1]];
|
|
const auto& v2 = verts[face[2]];
|
|
float nx = (v1[1]-v0[1])*(v2[2]-v0[2]) - (v1[2]-v0[2])*(v2[1]-v0[1]);
|
|
float ny = (v1[2]-v0[2])*(v2[0]-v0[0]) - (v1[0]-v0[0])*(v2[2]-v0[2]);
|
|
float nz = (v1[0]-v0[0])*(v2[1]-v0[1]) - (v1[1]-v0[1])*(v2[0]-v0[0]);
|
|
float len = std::sqrt(nx*nx + ny*ny + nz*nz);
|
|
if (len > 1e-8f) { nx /= len; ny /= len; nz /= len; }
|
|
glNormal3f(nx, ny, nz);
|
|
}
|
|
|
|
glVertex3fv(verts[idx].data());
|
|
}
|
|
}
|
|
glEnd();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// TextureImportDialog
|
|
// ============================================================
|
|
|
|
wxBEGIN_EVENT_TABLE(TextureImportDialog, DPIDialog)
|
|
EVT_BUTTON(TextureImportDialog::ID_COLOR_4, TextureImportDialog::on_color_preset_clicked)
|
|
EVT_BUTTON(TextureImportDialog::ID_COLOR_8, TextureImportDialog::on_color_preset_clicked)
|
|
EVT_BUTTON(TextureImportDialog::ID_COLOR_16, TextureImportDialog::on_color_preset_clicked)
|
|
EVT_BUTTON(TextureImportDialog::ID_COLOR_AUTO, TextureImportDialog::on_color_preset_clicked)
|
|
EVT_BUTTON(TextureImportDialog::ID_BTN_APPLY, TextureImportDialog::on_apply_clicked)
|
|
EVT_BUTTON(TextureImportDialog::ID_BTN_SKIP, TextureImportDialog::on_skip_clicked)
|
|
EVT_BUTTON(wxID_OK, TextureImportDialog::on_ok_clicked)
|
|
wxEND_EVENT_TABLE()
|
|
|
|
TextureImportDialog::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)
|
|
: DPIDialog(parent, wxID_ANY, _L("Import Model"),
|
|
wxDefaultPosition, wxDefaultSize,
|
|
(wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) & ~(wxMINIMIZE_BOX | wxMAXIMIZE_BOX))
|
|
, m_textured_mesh(textured_mesh)
|
|
, m_filament_entries(filament_entries)
|
|
, m_initial_cancel_callback(std::move(initial_cancel_callback))
|
|
, m_initial_progress_callback(std::move(initial_progress_callback))
|
|
{
|
|
SetSize(wxSize(FromDIP(960), FromDIP(640)));
|
|
|
|
m_filament_colors_rgba.reserve(m_filament_entries.size());
|
|
m_filament_color_strs.reserve(m_filament_entries.size());
|
|
m_filament_names.reserve(m_filament_entries.size());
|
|
for (size_t i = 0; i < m_filament_entries.size(); ++i) {
|
|
auto& entry = m_filament_entries[i];
|
|
entry.dialog_index = (int)i;
|
|
entry.color_hex = texture_normalize_color_hex(entry.color_hex);
|
|
if (entry.name.empty())
|
|
entry.name = "Filament " + std::to_string(i + 1);
|
|
m_filament_color_strs.push_back(entry.color_hex);
|
|
m_filament_names.push_back(entry.name);
|
|
m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex));
|
|
}
|
|
|
|
m_existing_filament_count = m_filament_colors_rgba.size();
|
|
m_default_virtual_filament_preset_name = resolve_default_virtual_filament_preset_name();
|
|
|
|
Bind(EVT_TEXTURE_COMPUTE_DONE, &TextureImportDialog::on_computation_complete, this);
|
|
Bind(EVT_TEXTURE_COMPUTE_PROGRESS, &TextureImportDialog::on_computation_progress, this);
|
|
Bind(EVT_TEXTURE_COMPUTE_ERROR, &TextureImportDialog::on_computation_error, this);
|
|
Bind(EVT_TEXTURE_MESH_REPAIR_DECISION, &TextureImportDialog::on_mesh_repair_decision_required, this);
|
|
|
|
build_ui();
|
|
SetMinSize(wxSize(FromDIP(800), FromDIP(500)));
|
|
CenterOnParent();
|
|
wxGetApp().UpdateDlgDarkUI(this);
|
|
|
|
m_preview_canvas->set_mesh_data(m_textured_mesh.vertices, m_textured_mesh.indices);
|
|
|
|
// Pre-computed face colors (OBJ vertex colors / MTL face colors):
|
|
// use them directly as the Original preview, skip texture decode.
|
|
if (!m_textured_mesh.precomputed_face_colors.empty()) {
|
|
m_preview_canvas->set_original_face_colors(m_textured_mesh.precomputed_face_colors);
|
|
} else if (!m_textured_mesh.textures.empty()) {
|
|
std::vector<std::vector<unsigned char>> tex_pixels_rgb;
|
|
std::vector<int> tex_widths, tex_heights;
|
|
tex_pixels_rgb.reserve(m_textured_mesh.textures.size());
|
|
tex_widths.reserve(m_textured_mesh.textures.size());
|
|
tex_heights.reserve(m_textured_mesh.textures.size());
|
|
|
|
for (const auto& ti : m_textured_mesh.textures) {
|
|
std::vector<unsigned char> bgr_pixels;
|
|
int w = 0, h = 0;
|
|
if (Slic3r::decode_texture_to_pixels(ti, bgr_pixels, w, h) && !bgr_pixels.empty()) {
|
|
// Convert BGR to RGB for OpenGL
|
|
for (size_t p = 0; p < bgr_pixels.size(); p += 3)
|
|
std::swap(bgr_pixels[p], bgr_pixels[p + 2]);
|
|
tex_pixels_rgb.push_back(std::move(bgr_pixels));
|
|
} else {
|
|
tex_pixels_rgb.push_back({});
|
|
}
|
|
tex_widths.push_back(w);
|
|
tex_heights.push_back(h);
|
|
}
|
|
|
|
const size_t nf = m_textured_mesh.indices.size();
|
|
const bool has_mapping = !m_textured_mesh.material_texture_map.empty();
|
|
|
|
// Build per-face UV array
|
|
std::vector<std::array<std::array<float,2>, 3>> face_uvs(nf);
|
|
for (size_t fi = 0; fi < nf; ++fi) {
|
|
if (m_textured_mesh.has_face_uvs()) {
|
|
const auto& ui = m_textured_mesh.uv_indices[fi];
|
|
for (int vi = 0; vi < 3; ++vi) {
|
|
int idx = ui[vi];
|
|
if (idx >= 0 && static_cast<size_t>(idx) < m_textured_mesh.uv_coords.size())
|
|
face_uvs[fi][vi] = m_textured_mesh.uv_coords[idx];
|
|
else
|
|
face_uvs[fi][vi] = {0.f, 0.f};
|
|
}
|
|
} else if (!m_textured_mesh.uvs.empty()) {
|
|
const auto& face = m_textured_mesh.indices[fi];
|
|
for (int vi = 0; vi < 3; ++vi) {
|
|
int idx = face[vi];
|
|
if (idx >= 0 && static_cast<size_t>(idx) < m_textured_mesh.uvs.size())
|
|
face_uvs[fi][vi] = m_textured_mesh.uvs[idx];
|
|
else
|
|
face_uvs[fi][vi] = {0.f, 0.f};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build per-face texture index
|
|
std::vector<int> face_tex_ids(nf, 0);
|
|
for (size_t fi = 0; fi < nf; ++fi) {
|
|
int mat_idx = (fi < m_textured_mesh.material_ids.size())
|
|
? m_textured_mesh.material_ids[fi] : -1;
|
|
if (has_mapping && mat_idx >= 0
|
|
&& static_cast<size_t>(mat_idx) < m_textured_mesh.material_texture_map.size())
|
|
face_tex_ids[fi] = m_textured_mesh.material_texture_map[mat_idx];
|
|
else if (!tex_pixels_rgb.empty())
|
|
face_tex_ids[fi] = 0;
|
|
else
|
|
face_tex_ids[fi] = -1;
|
|
}
|
|
|
|
m_preview_canvas->set_texture_render_data(
|
|
tex_pixels_rgb, tex_widths, tex_heights, face_uvs, face_tex_ids);
|
|
|
|
// Still sample per-face colors as fallback
|
|
std::vector<std::array<std::size_t, 3>> orig_colors;
|
|
if (Slic3r::sample_original_face_colors(m_textured_mesh, orig_colors))
|
|
m_preview_canvas->set_original_face_colors(orig_colors);
|
|
}
|
|
|
|
set_state(TextureImportState::Idle);
|
|
}
|
|
|
|
TextureImportDialog::~TextureImportDialog()
|
|
{
|
|
dismiss_auto_mix_popup();
|
|
dismiss_filament_popup();
|
|
m_cancel_flag = true;
|
|
if (m_worker && m_worker->joinable())
|
|
m_worker->join();
|
|
}
|
|
|
|
int TextureImportDialog::ShowModal()
|
|
{
|
|
if (m_state == TextureImportState::Idle && m_painted.face_colors.empty()) {
|
|
start_computation(true, true);
|
|
|
|
while (m_initial_computation_pending) {
|
|
if (auto* event_loop = wxEventLoopBase::GetActive())
|
|
event_loop->Yield();
|
|
else
|
|
wxYield();
|
|
if (m_progress_dlg && m_progress_dlg->WasCancelled())
|
|
m_cancel_flag = true;
|
|
if (m_initial_cancel_callback && m_initial_cancel_callback())
|
|
m_cancel_flag = true;
|
|
wxMilliSleep(10);
|
|
}
|
|
|
|
if (m_worker && m_worker->joinable())
|
|
m_worker->join();
|
|
m_worker.reset();
|
|
|
|
if (m_initial_computation_cancelled || m_initial_computation_failed)
|
|
return wxID_CANCEL;
|
|
}
|
|
|
|
ScopedInteractiveBusyCursorSuspender busy_cursor_suspender;
|
|
return DPIDialog::ShowModal();
|
|
}
|
|
|
|
void TextureImportDialog::build_ui()
|
|
{
|
|
const wxColour dialog_bg = StateColor::darkModeColorFor(*wxWHITE);
|
|
SetBackgroundColour(dialog_bg);
|
|
SetForegroundColour(StateColor::darkModeColorFor(wxColour("#323A3D")));
|
|
|
|
wxBoxSizer* root_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
auto line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
|
|
line_top->SetBackgroundColour(texture_import_separator_colour());
|
|
root_sizer->Add(line_top, 0, wxEXPAND);
|
|
|
|
wxBoxSizer* main_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
|
|
wxBoxSizer* left_sizer = new wxBoxSizer(wxVERTICAL);
|
|
build_preview_panel(this, left_sizer);
|
|
main_sizer->Add(left_sizer, 3, wxEXPAND | wxALL, FromDIP(8));
|
|
|
|
wxBoxSizer* right_sizer = new wxBoxSizer(wxVERTICAL);
|
|
build_params_panel(this, right_sizer);
|
|
build_mapping_panel(this, right_sizer);
|
|
build_bottom_buttons(right_sizer);
|
|
main_sizer->Add(right_sizer, 2, wxEXPAND | wxALL, FromDIP(8));
|
|
|
|
root_sizer->Add(main_sizer, 1, wxEXPAND);
|
|
|
|
SetSizer(root_sizer);
|
|
Layout();
|
|
Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
|
|
#ifdef __WXMSW__
|
|
wxPanel* size_grip_cover = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
|
size_grip_cover->SetBackgroundColour(dialog_bg);
|
|
size_grip_cover->SetBackgroundStyle(wxBG_STYLE_COLOUR);
|
|
|
|
auto update_size_grip_cover = [this, size_grip_cover]() {
|
|
const int cover_size = FromDIP(20);
|
|
wxSize client_size = GetClientSize();
|
|
size_grip_cover->SetSize(client_size.x - cover_size, client_size.y - cover_size, cover_size, cover_size);
|
|
size_grip_cover->Raise();
|
|
};
|
|
update_size_grip_cover();
|
|
|
|
Bind(wxEVT_SIZE, [update_size_grip_cover](wxSizeEvent& e) {
|
|
e.Skip();
|
|
update_size_grip_cover();
|
|
});
|
|
#endif
|
|
}
|
|
|
|
void TextureImportDialog::build_preview_panel(wxWindow* parent, wxSizer* sizer)
|
|
{
|
|
wxColour preview_bg = StateColor::darkModeColorFor(wxColour("#EEEEEE"));
|
|
wxColour preview_bd = texture_import_separator_colour();
|
|
|
|
wxPanel* preview_container = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
|
|
preview_container->SetBackgroundColour(preview_bg);
|
|
preview_container->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
preview_container->Bind(wxEVT_PAINT, [preview_bg, preview_bd](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour()));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
dc.SetBrush(wxBrush(preview_bg));
|
|
dc.SetPen(wxPen(preview_bd, 1));
|
|
dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, 4);
|
|
});
|
|
|
|
wxBoxSizer* container_sizer = new wxBoxSizer(wxVERTICAL);
|
|
|
|
wxGLAttributes canvas_attrs;
|
|
canvas_attrs.PlatformDefaults().RGBA().DoubleBuffer().Depth(24).EndList();
|
|
m_preview_canvas = new TexturePreviewCanvas(preview_container, canvas_attrs);
|
|
container_sizer->Add(m_preview_canvas, 1, wxEXPAND | wxALL, FromDIP(1));
|
|
|
|
preview_container->SetSizer(container_sizer);
|
|
sizer->Add(preview_container, 1, wxEXPAND);
|
|
|
|
m_tab_panel = new wxPanel(preview_container, wxID_ANY);
|
|
m_tab_panel->SetBackgroundColour(preview_bg);
|
|
|
|
m_btn_view_original = new Button(m_tab_panel, _L("Original"));
|
|
m_btn_view_original->SetId(ID_VIEW_ORIGINAL);
|
|
m_btn_view_multicolor = new Button(m_tab_panel, _L("Multi-Color"));
|
|
m_btn_view_multicolor->SetId(ID_VIEW_MULTICOLOR);
|
|
|
|
const int view_button_height = FromDIP(27);
|
|
m_btn_view_original->SetCornerRadius(view_button_height / 2);
|
|
m_btn_view_original->SetMinSize(wxSize(FromDIP(57), view_button_height));
|
|
m_btn_view_original->SetFont(m_btn_view_original->GetFont().Bold());
|
|
m_btn_view_original->SetToolTip(_L("Your input texture model"));
|
|
m_btn_view_multicolor->SetCornerRadius(view_button_height / 2);
|
|
m_btn_view_multicolor->SetMinSize(wxSize(FromDIP(57), view_button_height));
|
|
m_btn_view_multicolor->SetFont(m_btn_view_multicolor->GetFont().Bold());
|
|
m_btn_view_multicolor->SetToolTip(_L("Processed multi-color model"));
|
|
|
|
wxBoxSizer* tab_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
tab_sizer->Add(m_btn_view_original, 0, wxRIGHT, FromDIP(2));
|
|
tab_sizer->Add(m_btn_view_multicolor, 0);
|
|
m_tab_panel->SetSizer(tab_sizer);
|
|
m_tab_panel->Fit();
|
|
|
|
m_btn_view_multicolor->Hide();
|
|
|
|
auto preview_original = [this](wxMouseEvent& e) {
|
|
if (m_preview_canvas) {
|
|
m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::Original);
|
|
highlight_view_button(0);
|
|
}
|
|
e.Skip();
|
|
};
|
|
auto preview_multicolor = [this](wxMouseEvent& e) {
|
|
if (m_preview_canvas) {
|
|
m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::MultiColor);
|
|
highlight_view_button(1);
|
|
}
|
|
e.Skip();
|
|
};
|
|
auto restore_filament_if_outside = [this](wxMouseEvent& e) {
|
|
if (m_preview_canvas && m_tab_panel) {
|
|
wxWindow* event_window = wxDynamicCast(e.GetEventObject(), wxWindow);
|
|
wxPoint screen_pos = event_window ? event_window->ClientToScreen(e.GetPosition()) : wxGetMousePosition();
|
|
wxPoint panel_pos = m_tab_panel->ScreenToClient(screen_pos);
|
|
if (!m_tab_panel->GetClientRect().Contains(panel_pos)) {
|
|
const bool mapping_ready = (m_state == TextureImportState::Ready);
|
|
m_preview_canvas->set_render_mode(mapping_ready ? TexturePreviewCanvas::RenderMode::FilamentMap :
|
|
TexturePreviewCanvas::RenderMode::Original);
|
|
highlight_view_button(-1);
|
|
}
|
|
}
|
|
e.Skip();
|
|
};
|
|
|
|
m_btn_view_original->Bind(wxEVT_ENTER_WINDOW, preview_original);
|
|
m_btn_view_multicolor->Bind(wxEVT_ENTER_WINDOW, preview_multicolor);
|
|
m_btn_view_original->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside);
|
|
m_btn_view_multicolor->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside);
|
|
m_tab_panel->Bind(wxEVT_LEAVE_WINDOW, restore_filament_if_outside);
|
|
|
|
auto update_preview_overlay_buttons = [this]() {
|
|
if (m_tab_panel) {
|
|
m_tab_panel->Fit();
|
|
m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8)));
|
|
m_tab_panel->Raise();
|
|
}
|
|
};
|
|
|
|
preview_container->Bind(wxEVT_SIZE, [update_preview_overlay_buttons](wxSizeEvent& e) {
|
|
e.Skip();
|
|
update_preview_overlay_buttons();
|
|
});
|
|
update_preview_overlay_buttons();
|
|
|
|
highlight_view_button(-1);
|
|
}
|
|
|
|
void TextureImportDialog::build_params_panel(wxWindow* parent, wxSizer* sizer)
|
|
{
|
|
wxColour label_fg = StateColor::darkModeColorFor(wxColour("#323A3D"));
|
|
|
|
wxBoxSizer* color_header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
wxStaticText* lbl_colors = new wxStaticText(parent, wxID_ANY, _L("Color Count"));
|
|
lbl_colors->SetForegroundColour(label_fg);
|
|
lbl_colors->SetFont(lbl_colors->GetFont().Bold());
|
|
color_header_sizer->Add(lbl_colors, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
|
|
|
|
m_btn_color_4 = new Button(parent, "4");
|
|
m_btn_color_4->SetId(ID_COLOR_4);
|
|
m_btn_color_8 = new Button(parent, "8");
|
|
m_btn_color_8->SetId(ID_COLOR_8);
|
|
m_btn_color_16 = new Button(parent, "16");
|
|
m_btn_color_16->SetId(ID_COLOR_16);
|
|
m_btn_color_auto = new Button(parent, _L("Auto"));
|
|
m_btn_color_auto->SetId(ID_COLOR_AUTO);
|
|
|
|
{
|
|
StateColor preset_bg(
|
|
std::pair<wxColour, int>(wxColour(0, 137, 123), StateColor::Pressed | StateColor::Checked),
|
|
std::pair<wxColour, int>(wxColour(38, 166, 154), StateColor::Hovered | StateColor::Checked),
|
|
std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Checked),
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(*wxWHITE, StateColor::Normal));
|
|
StateColor preset_bd(
|
|
std::pair<wxColour, int>(wxColour(0, 150, 136), StateColor::Checked),
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Normal));
|
|
StateColor preset_text(
|
|
std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Checked),
|
|
std::pair<wxColour, int>(wxColour("#323A3D"), StateColor::Normal));
|
|
|
|
for (auto* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) {
|
|
btn->SetCornerRadius(FromDIP(12));
|
|
btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28)));
|
|
btn->SetBackgroundColor(preset_bg);
|
|
btn->SetBorderColor(preset_bd);
|
|
btn->SetTextColor(preset_text);
|
|
}
|
|
}
|
|
|
|
update_color_count_preset_buttons();
|
|
|
|
color_header_sizer->Add(m_btn_color_4, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2));
|
|
color_header_sizer->Add(m_btn_color_8, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2));
|
|
color_header_sizer->Add(m_btn_color_16, 0, wxALIGN_CENTER_VERTICAL);
|
|
sizer->Add(color_header_sizer, 0, wxBOTTOM, FromDIP(4));
|
|
|
|
wxBoxSizer* color_slider_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
m_color_slider = new AccentSlider(parent, m_param_color_count, 1, (int)max_filament_count());
|
|
m_color_spin = new SpinInput(parent, wxString::Format("%d", m_param_color_count),
|
|
wxEmptyString, wxDefaultPosition,
|
|
wxSize(FromDIP(60), FromDIP(28)),
|
|
wxTE_PROCESS_ENTER, 1, (int)max_filament_count(), m_param_color_count);
|
|
|
|
m_color_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_color_slider_changed, this);
|
|
m_color_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_color_spin_changed, this);
|
|
m_color_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_color_spin_text_changed, this);
|
|
|
|
color_slider_sizer->Add(m_color_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
|
color_slider_sizer->Add(m_color_spin, 0, wxALIGN_CENTER_VERTICAL);
|
|
sizer->Add(color_slider_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
|
|
|
wxStaticText* lbl_smooth = new wxStaticText(parent, wxID_ANY, _L("Smooth Level"));
|
|
lbl_smooth->SetForegroundColour(label_fg);
|
|
lbl_smooth->SetFont(lbl_smooth->GetFont().Bold());
|
|
sizer->Add(lbl_smooth, 0, wxBOTTOM, FromDIP(4));
|
|
|
|
wxBoxSizer* smooth_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
m_smooth_slider = new AccentSlider(parent, m_param_smooth, 0, 10);
|
|
m_smooth_spin = new SpinInput(parent, wxString::Format("%d", m_param_smooth),
|
|
wxEmptyString, wxDefaultPosition,
|
|
wxSize(FromDIP(60), FromDIP(28)),
|
|
wxTE_PROCESS_ENTER, 0, 10, m_param_smooth);
|
|
|
|
m_smooth_slider->Bind(wxEVT_SLIDER, &TextureImportDialog::on_smooth_slider_changed, this);
|
|
m_smooth_spin->Bind(wxEVT_SPINCTRL, &TextureImportDialog::on_smooth_spin_changed, this);
|
|
m_smooth_spin->Bind(EVT_SPINCTRL_TEXT, &TextureImportDialog::on_smooth_spin_text_changed, this);
|
|
|
|
smooth_sizer->Add(m_smooth_slider, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
|
smooth_sizer->Add(m_smooth_spin, 0, wxALIGN_CENTER_VERTICAL);
|
|
sizer->Add(smooth_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
|
|
|
|
m_btn_apply = new Button(parent, _L("Apply"));
|
|
m_btn_apply->SetId(ID_BTN_APPLY);
|
|
|
|
{
|
|
StateColor btn_bg_white(
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(*wxWHITE, StateColor::Normal));
|
|
const wxColour btn_bd_accent = wxColour(0, 150, 136);
|
|
const wxColour btn_text_accent = wxColour(0, 150, 136);
|
|
|
|
m_btn_color_auto->SetCornerRadius(FromDIP(12));
|
|
m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
m_btn_color_auto->SetBackgroundColor(btn_bg_white);
|
|
m_btn_color_auto->SetBorderColor(btn_bd_accent);
|
|
m_btn_color_auto->SetTextColor(btn_text_accent);
|
|
|
|
m_btn_apply->SetCornerRadius(FromDIP(12));
|
|
m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
m_btn_apply->SetBackgroundColor(btn_bg_white);
|
|
m_btn_apply->SetBorderColor(btn_bd_accent);
|
|
m_btn_apply->SetTextColor(btn_text_accent);
|
|
}
|
|
|
|
// Defer attaching the Auto/Apply tooltips until the dialog has actually
|
|
// been shown. On macOS, AppKit creates NSTrackingArea and dispatches a
|
|
// synthetic mouseEntered: as soon as the window first becomes visible,
|
|
// which would otherwise pop the native tooltip without the user actually
|
|
// hovering when the cursor happens to land on these buttons as the dialog
|
|
// appears.
|
|
Bind(wxEVT_SHOW, [this](wxShowEvent& e) {
|
|
e.Skip();
|
|
if (!e.IsShown() || m_initial_tooltips_set)
|
|
return;
|
|
m_initial_tooltips_set = true;
|
|
CallAfter([this]() {
|
|
if (m_btn_color_auto)
|
|
m_btn_color_auto->SetToolTip(_L("Automatically determine the optimal color count only and recompute filament mapping"));
|
|
if (m_btn_apply)
|
|
m_btn_apply->SetToolTip(_L("Convert texture to painting using the specified color count and smooth level"));
|
|
});
|
|
});
|
|
|
|
wxBoxSizer* apply_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
apply_sizer->Add(m_btn_color_auto, 0, wxRIGHT, FromDIP(4));
|
|
apply_sizer->Add(m_btn_apply, 0);
|
|
sizer->Add(apply_sizer, 0, wxALIGN_RIGHT | wxBOTTOM, FromDIP(8));
|
|
|
|
m_hint_label = new wxStaticText(parent, wxID_ANY,
|
|
_L("Reminder: parameters changed, click Apply to take effect"));
|
|
m_hint_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00")));
|
|
m_hint_label->SetFont(texture_import_section_title_font(parent));
|
|
m_hint_label->Hide();
|
|
sizer->Add(m_hint_label, 0, wxBOTTOM, FromDIP(4));
|
|
|
|
auto* mapping_separator = new StaticLine(parent);
|
|
mapping_separator->SetLineColour(wxColour(SEPARATOR_COLOUR_KEY));
|
|
sizer->Add(mapping_separator, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
|
}
|
|
|
|
void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer)
|
|
{
|
|
wxColour secondary_fg = StateColor::darkModeColorFor(wxColour("#6B6B6B"));
|
|
|
|
wxBoxSizer* header_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
|
|
wxStaticText* lbl_mapping = new wxStaticText(parent, wxID_ANY, _L("Filament Mapping"));
|
|
lbl_mapping->SetForegroundColour(secondary_fg);
|
|
lbl_mapping->SetFont(texture_import_section_title_font(parent));
|
|
m_auto_mix_font_point_size = lbl_mapping->GetFont().GetPointSize();
|
|
header_sizer->Add(lbl_mapping, 0, wxALIGN_CENTER_VERTICAL);
|
|
|
|
m_btn_mix_reset = new Button(parent, "", "revert_btn", wxBORDER_NONE, 16);
|
|
m_btn_mix_reset->SetCanFocus(false);
|
|
m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2)));
|
|
{
|
|
StateColor reset_bg(
|
|
std::pair<wxColour, int>(wxColour("#F4F4F4"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour("#F8F8F8"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(*wxWHITE, StateColor::Normal));
|
|
m_btn_mix_reset->SetBackgroundColor(reset_bg);
|
|
m_btn_mix_reset->SetBorderColor(StateColor());
|
|
}
|
|
m_btn_mix_reset->SetToolTip(_L("Reset filament mapping to the state before one-click mixing"));
|
|
m_btn_mix_reset->Bind(wxEVT_BUTTON, [this](wxCommandEvent& evt) {
|
|
reset_auto_mix();
|
|
evt.Skip();
|
|
});
|
|
m_btn_mix_reset->Hide();
|
|
header_sizer->Add(m_btn_mix_reset, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6));
|
|
|
|
header_sizer->AddStretchSpacer();
|
|
|
|
m_btn_auto_mix = new Button(parent, auto_mix_mode_label(m_auto_mix_mode));
|
|
{
|
|
wxFont btn_font = m_btn_auto_mix->GetFont();
|
|
btn_font.SetPointSize(m_auto_mix_font_point_size);
|
|
m_btn_auto_mix->SetFont(btn_font);
|
|
}
|
|
m_btn_auto_mix->SetCornerRadius(FromDIP(14));
|
|
m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28)));
|
|
{
|
|
StateColor btn_bg(
|
|
std::pair<wxColour, int>(wxColour("#F4F4F4"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour("#F8F8F8"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(*wxWHITE, StateColor::Normal));
|
|
const wxColour btn_bd = wxColour("#CECECE");
|
|
const wxColour btn_text = texture_import_gray9000();
|
|
m_btn_auto_mix->SetBackgroundColor(btn_bg);
|
|
m_btn_auto_mix->SetBorderColor(btn_bd);
|
|
m_btn_auto_mix->SetTextColor(btn_text);
|
|
}
|
|
m_btn_auto_mix->SetToolTip(_L("Choose the one-click auto-mix mode for texture color import"));
|
|
m_btn_auto_mix->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& evt) {
|
|
show_auto_mix_popup();
|
|
evt.Skip();
|
|
});
|
|
m_btn_auto_mix->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) {
|
|
show_auto_mix_popup();
|
|
evt.Skip();
|
|
});
|
|
header_sizer->Add(m_btn_auto_mix, 0, wxALIGN_CENTER_VERTICAL);
|
|
|
|
sizer->Add(header_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
|
|
|
|
wxBoxSizer* merge_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
m_auto_merge_cb = new wxCheckBox(parent, wxID_ANY, _L("Auto-merge same filament"));
|
|
m_auto_merge_cb->SetToolTip(_L("Automatically merge identical filaments into existing filaments in the project"));
|
|
m_auto_merge_cb->SetForegroundColour(secondary_fg);
|
|
m_auto_merge_cb->SetValue(true);
|
|
m_auto_merge_cb->Bind(wxEVT_CHECKBOX, &TextureImportDialog::on_auto_merge_toggled, this);
|
|
merge_sizer->Add(m_auto_merge_cb, 0, wxALIGN_CENTER_VERTICAL);
|
|
|
|
sizer->Add(merge_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(8));
|
|
|
|
m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition,
|
|
wxSize(-1, FromDIP(300)));
|
|
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
|
|
m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
|
|
m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
|
|
m_mapping_sizer = new wxBoxSizer(wxVERTICAL);
|
|
m_mapping_scroll->SetSizer(m_mapping_sizer);
|
|
|
|
sizer->Add(m_mapping_scroll, 1, wxEXPAND | wxBOTTOM, FromDIP(8));
|
|
}
|
|
|
|
void TextureImportDialog::build_bottom_buttons(wxSizer* sizer)
|
|
{
|
|
m_drop_warning_label = new wxStaticText(this, wxID_ANY,
|
|
wxString::Format(
|
|
_L("The project supports up to %d filaments. Extra filaments will be discarded."),
|
|
(int)max_filament_count()));
|
|
m_drop_warning_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#FF6F00")));
|
|
m_drop_warning_label->SetFont(texture_import_section_title_font(this));
|
|
m_drop_warning_label->Hide();
|
|
sizer->Add(m_drop_warning_label, 0, wxALIGN_LEFT | wxBOTTOM, FromDIP(4));
|
|
|
|
wxBoxSizer* btn_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
m_btn_skip = new Button(this, _L("Skip Matching"));
|
|
m_btn_skip->SetId(ID_BTN_SKIP);
|
|
m_btn_skip->SetToolTip(_L("Skip filament mapping and import as a single-color model"));
|
|
m_btn_skip->SetCornerRadius(FromDIP(20));
|
|
m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40)));
|
|
{
|
|
StateColor skip_bg(
|
|
std::pair<wxColour, int>(wxColour("#CECECE"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(wxColour("#EEEEEE"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(*wxWHITE, StateColor::Normal));
|
|
const wxColour skip_bd = wxColour("#CECECE");
|
|
const wxColour skip_text = wxColour("#6B6B6A");
|
|
m_btn_skip->SetBackgroundColor(skip_bg);
|
|
m_btn_skip->SetBorderColor(skip_bd);
|
|
m_btn_skip->SetTextColor(skip_text);
|
|
}
|
|
|
|
m_btn_ok = new Button(this, _L("Confirm"));
|
|
m_btn_ok->SetId(wxID_OK);
|
|
m_btn_ok->SetCornerRadius(FromDIP(20));
|
|
m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40)));
|
|
apply_accent_button_colours(m_btn_ok);
|
|
|
|
btn_sizer->AddStretchSpacer();
|
|
btn_sizer->Add(m_btn_skip, 0, wxRIGHT, FromDIP(16));
|
|
btn_sizer->Add(m_btn_ok, 0);
|
|
|
|
sizer->Add(btn_sizer, 0, wxEXPAND | wxTOP, FromDIP(8));
|
|
}
|
|
|
|
// ---- State machine ----
|
|
|
|
void TextureImportDialog::set_state(TextureImportState new_state)
|
|
{
|
|
m_state = new_state;
|
|
update_ui_for_state();
|
|
}
|
|
|
|
void TextureImportDialog::update_ui_for_state()
|
|
{
|
|
bool computing = (m_state == TextureImportState::Computing);
|
|
bool ready = (m_state == TextureImportState::Ready);
|
|
bool idle = (m_state == TextureImportState::Idle);
|
|
bool valid = has_valid_result();
|
|
|
|
m_color_slider->Enable(!computing);
|
|
m_color_spin->Enable(!computing);
|
|
m_smooth_slider->Enable(!computing);
|
|
m_smooth_spin->Enable(!computing);
|
|
m_btn_apply->Enable(!computing);
|
|
m_btn_color_4->Enable(!computing);
|
|
m_btn_color_8->Enable(!computing);
|
|
m_btn_color_16->Enable(!computing);
|
|
m_btn_color_auto->Enable(!computing);
|
|
if (m_btn_auto_mix)
|
|
m_btn_auto_mix->Enable(!computing);
|
|
if (m_btn_mix_reset)
|
|
m_btn_mix_reset->Enable(!computing);
|
|
if (computing)
|
|
dismiss_auto_mix_popup();
|
|
|
|
m_btn_ok->Enable(ready && valid);
|
|
m_btn_skip->Enable(ready || idle);
|
|
|
|
m_auto_merge_cb->Enable(!computing);
|
|
|
|
m_preview_canvas->set_computing_overlay(computing);
|
|
|
|
if (ready && valid)
|
|
style_confirm_button(is_params_dirty());
|
|
else if (m_hint_label)
|
|
m_hint_label->Hide();
|
|
|
|
m_btn_ok->Refresh();
|
|
Layout();
|
|
}
|
|
|
|
// ---- Async computation ----
|
|
|
|
void TextureImportDialog::start_computation(bool auto_color, bool initial)
|
|
{
|
|
cancel_computation();
|
|
|
|
m_cancel_flag = false;
|
|
m_current_computation_initial = initial;
|
|
m_current_computation_auto_color = auto_color;
|
|
if (initial) {
|
|
m_initial_computation_pending = true;
|
|
m_initial_computation_cancelled = false;
|
|
m_initial_computation_failed = false;
|
|
}
|
|
set_state(TextureImportState::Computing);
|
|
|
|
bool silent_initial = initial && static_cast<bool>(m_initial_cancel_callback);
|
|
if (!silent_initial) {
|
|
m_progress_dlg = new ProgressDialog(
|
|
_L("Processing"), _L("Computing texture colors..."),
|
|
100, initial ? GetParent() : this, wxPD_APP_MODAL | wxPD_CAN_ABORT | wxPD_AUTO_HIDE);
|
|
}
|
|
|
|
Slic3r::TexturePaintingSettings settings;
|
|
settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count;
|
|
settings.smooth_weight = m_param_smooth / 10.0;
|
|
settings.mesh_repair_decision = m_mesh_repair_decision;
|
|
// BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows
|
|
// builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works
|
|
// on all three platforms, so use that instead.
|
|
settings.mesh_repair_callback = [](const indexed_triangle_set& mesh,
|
|
indexed_triangle_set& repaired_mesh,
|
|
std::function<void(const char*, unsigned)> progress_callback,
|
|
std::function<bool()> cancel_callback,
|
|
std::string* error_message) -> bool {
|
|
if (cancel_callback && cancel_callback())
|
|
return false;
|
|
if (progress_callback)
|
|
progress_callback(_u8L("Repairing mesh").c_str(), 0);
|
|
|
|
TriangleMesh tm(mesh);
|
|
if (!MeshBoolean::cgal::repair(tm, nullptr, error_message))
|
|
return false;
|
|
|
|
if (cancel_callback && cancel_callback())
|
|
return false;
|
|
repaired_mesh = tm.its;
|
|
if (progress_callback)
|
|
progress_callback(_u8L("Repairing mesh").c_str(), 100);
|
|
return true;
|
|
};
|
|
|
|
Slic3r::TexturedMesh mesh_copy = m_textured_mesh;
|
|
wxEvtHandler* handler = this;
|
|
|
|
m_worker = std::make_unique<std::thread>([this, settings, mesh_copy, handler]() {
|
|
Slic3r::PaintedMesh result;
|
|
|
|
auto progress_cb = [handler](int percent, const char*) {
|
|
auto* evt = new wxCommandEvent(EVT_TEXTURE_COMPUTE_PROGRESS);
|
|
evt->SetInt(percent);
|
|
wxQueueEvent(handler, evt);
|
|
};
|
|
|
|
auto cancel_cb = [this]() -> bool {
|
|
return m_cancel_flag.load();
|
|
};
|
|
|
|
auto worker_settings = settings;
|
|
bool mesh_repair_decision_required = false;
|
|
worker_settings.mesh_repair_decision_required = &mesh_repair_decision_required;
|
|
bool ok;
|
|
if (!mesh_copy.precomputed_face_colors.empty()) {
|
|
ok = Slic3r::face_colors_to_painting(
|
|
mesh_copy, result, worker_settings, progress_cb, cancel_cb);
|
|
} else {
|
|
ok = Slic3r::texture_to_painting(mesh_copy, result, worker_settings, progress_cb, cancel_cb);
|
|
}
|
|
|
|
if (m_cancel_flag.load()) {
|
|
wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR));
|
|
return;
|
|
}
|
|
|
|
if (!ok && mesh_repair_decision_required) {
|
|
wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_MESH_REPAIR_DECISION));
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_result_mutex);
|
|
m_pending_result = std::move(result);
|
|
}
|
|
|
|
if (ok) {
|
|
wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_DONE));
|
|
} else {
|
|
wxQueueEvent(handler, new wxCommandEvent(EVT_TEXTURE_COMPUTE_ERROR));
|
|
}
|
|
});
|
|
}
|
|
|
|
void TextureImportDialog::cancel_computation()
|
|
{
|
|
m_cancel_flag = true;
|
|
if (m_worker && m_worker->joinable())
|
|
m_worker->join();
|
|
m_worker.reset();
|
|
|
|
if (m_progress_dlg) {
|
|
m_progress_dlg->Destroy();
|
|
m_progress_dlg = nullptr;
|
|
}
|
|
|
|
if (m_current_computation_initial) {
|
|
m_initial_computation_cancelled = true;
|
|
m_initial_computation_pending = false;
|
|
m_current_computation_initial = false;
|
|
}
|
|
}
|
|
|
|
void TextureImportDialog::on_computation_progress(wxCommandEvent& evt)
|
|
{
|
|
if (m_progress_dlg) {
|
|
if (!m_progress_dlg->Update(evt.GetInt()))
|
|
m_cancel_flag = true;
|
|
} else if (m_current_computation_initial && m_initial_progress_callback) {
|
|
if (!m_initial_progress_callback(evt.GetInt()))
|
|
m_cancel_flag = true;
|
|
}
|
|
}
|
|
|
|
void TextureImportDialog::on_computation_complete(wxCommandEvent&)
|
|
{
|
|
bool initial = m_current_computation_initial;
|
|
|
|
if (m_progress_dlg) {
|
|
m_progress_dlg->Destroy();
|
|
m_progress_dlg = nullptr;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_result_mutex);
|
|
m_painted = std::move(m_pending_result);
|
|
}
|
|
|
|
int actual_colors = (int)m_painted.cluster_colors.size();
|
|
if (actual_colors >= 2 && actual_colors <= (int)max_filament_count()) {
|
|
set_color_count_value(actual_colors, true);
|
|
}
|
|
|
|
m_preview_canvas->set_painted_mesh_data(m_painted.vertices, m_painted.indices);
|
|
m_preview_canvas->set_face_colors(m_painted.face_colors);
|
|
|
|
// A fresh texture computation replaces m_painted, so virtual filaments from
|
|
// the previous computation must not consume capacity when deciding whether
|
|
// this run drops extra colors. Rebuild virtual filaments from this result.
|
|
m_current_matches.clear();
|
|
if (m_filament_colors_rgba.size() > m_existing_filament_count)
|
|
m_filament_colors_rgba.resize(m_existing_filament_count);
|
|
if (m_filament_color_strs.size() > m_existing_filament_count)
|
|
m_filament_color_strs.resize(m_existing_filament_count);
|
|
if (m_filament_names.size() > m_existing_filament_count)
|
|
m_filament_names.resize(m_existing_filament_count);
|
|
if (m_filament_entries.size() > m_existing_filament_count)
|
|
m_filament_entries.resize(m_existing_filament_count);
|
|
while (m_filament_entries.size() < m_existing_filament_count) {
|
|
TextureFilamentEntry entry;
|
|
entry.kind = TextureFilamentKind::ExistingPhysical;
|
|
entry.dialog_index = (int)m_filament_entries.size();
|
|
entry.project_config_index = m_filament_entries.size();
|
|
m_filament_entries.push_back(entry);
|
|
}
|
|
for (size_t i = 0; i < m_filament_entries.size(); ++i) {
|
|
m_filament_entries[i].dialog_index = (int)i;
|
|
m_filament_entries[i].color_hex = i < m_filament_color_strs.size() ?
|
|
texture_normalize_color_hex(m_filament_color_strs[i]) : "#808080";
|
|
m_filament_entries[i].name = i < m_filament_names.size() ?
|
|
m_filament_names[i] : "Filament " + std::to_string(i + 1);
|
|
}
|
|
m_new_filament_colors.clear();
|
|
m_new_filament_preset_names.clear();
|
|
m_new_mixed_filaments.clear();
|
|
|
|
do_auto_match();
|
|
compact_used_virtual_filaments();
|
|
sort_current_matches_by_filament_index();
|
|
update_filament_color_map();
|
|
rebuild_mapping_rows();
|
|
|
|
m_applied_color_count = m_param_color_count;
|
|
m_applied_smooth = m_param_smooth;
|
|
|
|
set_state(TextureImportState::Ready);
|
|
update_drop_warning_visibility();
|
|
update_auto_mix_reset_visibility();
|
|
|
|
m_btn_view_multicolor->Show();
|
|
if (m_tab_panel) {
|
|
m_tab_panel->GetSizer()->Layout();
|
|
m_tab_panel->Fit();
|
|
m_tab_panel->SetPosition(wxPoint(FromDIP(8), FromDIP(8)));
|
|
}
|
|
GetSizer()->Layout();
|
|
|
|
m_preview_canvas->set_render_mode(TexturePreviewCanvas::RenderMode::FilamentMap);
|
|
highlight_view_button(-1);
|
|
|
|
if (initial) {
|
|
m_initial_computation_pending = false;
|
|
m_current_computation_initial = false;
|
|
}
|
|
}
|
|
|
|
void TextureImportDialog::on_computation_error(wxCommandEvent&)
|
|
{
|
|
bool initial = m_current_computation_initial;
|
|
|
|
if (m_progress_dlg) {
|
|
m_progress_dlg->Destroy();
|
|
m_progress_dlg = nullptr;
|
|
}
|
|
|
|
if (m_cancel_flag.load()) {
|
|
if (initial) {
|
|
m_initial_computation_cancelled = true;
|
|
m_initial_computation_pending = false;
|
|
m_current_computation_initial = false;
|
|
return;
|
|
}
|
|
if (has_valid_result()) {
|
|
if (m_applied_color_count >= 0) {
|
|
m_param_color_count = m_applied_color_count;
|
|
m_color_slider->SetValue(m_param_color_count);
|
|
m_color_spin->SetValue(m_param_color_count);
|
|
update_color_count_preset_buttons();
|
|
}
|
|
if (m_applied_smooth >= 0) {
|
|
m_param_smooth = m_applied_smooth;
|
|
m_smooth_slider->SetValue(m_param_smooth);
|
|
m_smooth_spin->SetValue(m_param_smooth);
|
|
}
|
|
set_state(TextureImportState::Ready);
|
|
return;
|
|
}
|
|
set_state(TextureImportState::Idle);
|
|
return;
|
|
}
|
|
|
|
if (initial) {
|
|
m_initial_computation_failed = true;
|
|
m_initial_computation_pending = false;
|
|
m_current_computation_initial = false;
|
|
m_fallback_to_geometry_only = true;
|
|
return;
|
|
}
|
|
|
|
set_state(TextureImportState::Error);
|
|
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
|
|
_L("Computation failed. Please adjust parameters and retry."),
|
|
_L("Error"), wxOK | wxICON_ERROR);
|
|
dlg.ShowModal();
|
|
}
|
|
|
|
void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
|
|
{
|
|
bool initial = m_current_computation_initial;
|
|
bool auto_color = m_current_computation_auto_color;
|
|
|
|
if (m_progress_dlg) {
|
|
m_progress_dlg->Destroy();
|
|
m_progress_dlg = nullptr;
|
|
}
|
|
|
|
#ifdef HAS_WIN10SDK
|
|
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
|
|
_L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."),
|
|
_L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT);
|
|
dlg.SetButtonLabel(wxID_YES, _L("Import without repair"));
|
|
dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true);
|
|
// "Repair and import" is the recommended action here, so the accent moves off the default YES
|
|
// button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling
|
|
// with the same type swaps only the palette and leaves the geometry alone.
|
|
if (auto* yes_btn = dynamic_cast<Button*>(dlg.FindWindow(wxID_YES))) {
|
|
yes_btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
|
|
yes_btn->SetMinSize(wxSize(FromDIP(180), FromDIP(24)));
|
|
}
|
|
if (auto* no_btn = dynamic_cast<Button*>(dlg.FindWindow(wxID_NO))) {
|
|
no_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
|
|
no_btn->SetMinSize(wxSize(FromDIP(160), FromDIP(24)));
|
|
}
|
|
dlg.Layout();
|
|
dlg.Fit();
|
|
dlg.CenterOnParent();
|
|
int ret = dlg.ShowModal();
|
|
m_mesh_repair_decision = (ret == wxID_NO)
|
|
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
|
|
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
|
|
#else
|
|
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
|
|
_L("Please note that the mesh has non-manifold geometry or open boundaries."),
|
|
_L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT);
|
|
dlg.SetButtonLabel(wxID_OK, _L("Continue"), true);
|
|
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
|
|
int ret = dlg.ShowModal();
|
|
if (ret != wxID_OK) {
|
|
m_cancel_flag = true;
|
|
if (initial) {
|
|
m_initial_computation_cancelled = true;
|
|
m_initial_computation_pending = false;
|
|
m_current_computation_initial = false;
|
|
} else if (has_valid_result()) {
|
|
set_state(TextureImportState::Ready);
|
|
} else {
|
|
set_state(TextureImportState::Idle);
|
|
}
|
|
return;
|
|
}
|
|
m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
|
|
#endif
|
|
|
|
start_computation(auto_color, initial);
|
|
}
|
|
|
|
// ---- Mapping ----
|
|
|
|
void TextureImportDialog::update_filament_color_map()
|
|
{
|
|
std::map<std::array<std::size_t, 3>, std::array<float, 3>> color_map;
|
|
for (const auto& m : m_current_matches) {
|
|
if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) {
|
|
color_map[m.cluster_color] = {
|
|
m_filament_colors_rgba[m.filament_index][0],
|
|
m_filament_colors_rgba[m.filament_index][1],
|
|
m_filament_colors_rgba[m.filament_index][2]
|
|
};
|
|
}
|
|
}
|
|
m_preview_canvas->set_filament_color_map(color_map);
|
|
}
|
|
|
|
// Canonical ordering used on the very first display after a computation:
|
|
// sort ascending by filament_index, and push unmapped (filament_index < 0)
|
|
// entries to the end. This gives the user a stable, predictable mapping
|
|
// layout regardless of the cluster discovery order.
|
|
void TextureImportDialog::sort_current_matches_by_filament_index()
|
|
{
|
|
std::stable_sort(m_current_matches.begin(), m_current_matches.end(),
|
|
[](const auto& lhs, const auto& rhs) {
|
|
const bool lhs_valid = lhs.filament_index >= 0;
|
|
const bool rhs_valid = rhs.filament_index >= 0;
|
|
|
|
if (lhs_valid != rhs_valid)
|
|
return lhs_valid;
|
|
if (!lhs_valid)
|
|
return false;
|
|
|
|
return lhs.filament_index < rhs.filament_index;
|
|
});
|
|
}
|
|
|
|
// Preserve the row order the user is currently looking at across a
|
|
// re-computation (e.g. when auto-merge is toggled). We key on cluster_index
|
|
// because it survives compact_used_virtual_filaments() and filament-index
|
|
// renumbering, whereas filament_index does not.
|
|
//
|
|
// Behaviour:
|
|
// * Entries whose cluster_index appeared in `previous_matches` keep their
|
|
// previous relative order.
|
|
// * Entries whose cluster_index is new (not in `previous_matches`) are
|
|
// appended at the end, in their current relative order.
|
|
//
|
|
// Assumption: each cluster_index appears at most once in both vectors. This
|
|
// is currently guaranteed by do_auto_match(), which emits exactly one match
|
|
// per cluster. If that invariant ever changes, the std::map::emplace below
|
|
// silently keeps only the first occurrence and the order will be wrong.
|
|
void TextureImportDialog::restore_current_match_order(const std::vector<Slic3r::FilamentMatch>& previous_matches)
|
|
{
|
|
if (previous_matches.empty() || m_current_matches.size() < 2)
|
|
return;
|
|
|
|
std::map<int, size_t> previous_order_by_cluster;
|
|
for (size_t i = 0; i < previous_matches.size(); ++i) {
|
|
if (previous_matches[i].cluster_index >= 0)
|
|
previous_order_by_cluster.emplace(previous_matches[i].cluster_index, i);
|
|
}
|
|
|
|
std::stable_sort(m_current_matches.begin(), m_current_matches.end(),
|
|
[&previous_order_by_cluster](const auto& lhs, const auto& rhs) {
|
|
const auto lhs_it = previous_order_by_cluster.find(lhs.cluster_index);
|
|
const auto rhs_it = previous_order_by_cluster.find(rhs.cluster_index);
|
|
const bool lhs_known = lhs_it != previous_order_by_cluster.end();
|
|
const bool rhs_known = rhs_it != previous_order_by_cluster.end();
|
|
|
|
if (lhs_known != rhs_known)
|
|
return lhs_known;
|
|
if (!lhs_known)
|
|
return false;
|
|
|
|
return lhs_it->second < rhs_it->second;
|
|
});
|
|
}
|
|
|
|
size_t TextureImportDialog::max_filament_count() const
|
|
{
|
|
return static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
|
|
}
|
|
|
|
bool TextureImportDialog::can_add_virtual_filament() const
|
|
{
|
|
return m_filament_colors_rgba.size() < max_filament_count();
|
|
}
|
|
|
|
int TextureImportDialog::find_closest_filament_index(const std::array<std::size_t, 3>& color) const
|
|
{
|
|
int best_idx = -1;
|
|
double best_delta = std::numeric_limits<double>::max();
|
|
const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count());
|
|
for (size_t i = 0; i < filament_count; ++i) {
|
|
const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]);
|
|
if (delta < best_delta) {
|
|
best_delta = delta;
|
|
best_idx = (int)i;
|
|
}
|
|
}
|
|
return best_idx;
|
|
}
|
|
|
|
int TextureImportDialog::add_virtual_filament(const std::array<float, 4>& rgba, const std::string& hex,
|
|
const std::string& preset_name)
|
|
{
|
|
if (m_filament_color_strs.size() != m_filament_colors_rgba.size() ||
|
|
m_filament_names.size() != m_filament_colors_rgba.size() ||
|
|
m_filament_entries.size() != m_filament_colors_rgba.size()) {
|
|
return -1;
|
|
}
|
|
if (!can_add_virtual_filament()) {
|
|
// Mark that this do_auto_match() run hit the filament cap and had to
|
|
// drop at least one cluster. The mapping itself still falls back via
|
|
// find_closest_filament_index() below; this flag only drives the
|
|
// inline orange warning above the bottom buttons.
|
|
// Note: only the false -> true transition happens here; the flag is
|
|
// cleared exclusively at the entry of do_auto_match() so it always
|
|
// reflects the most recent match, never an accumulated history.
|
|
m_filaments_dropped = true;
|
|
return -1;
|
|
}
|
|
|
|
const int new_idx = (int)m_filament_colors_rgba.size();
|
|
m_filament_colors_rgba.push_back(rgba);
|
|
m_filament_color_strs.push_back(hex);
|
|
m_filament_names.push_back(DEFAULT_VIRTUAL_FILAMENT_NAME);
|
|
TextureFilamentEntry entry;
|
|
entry.kind = TextureFilamentKind::NewPhysical;
|
|
entry.dialog_index = new_idx;
|
|
entry.project_config_index = size_t(-1);
|
|
entry.color_hex = texture_normalize_color_hex(hex);
|
|
entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME;
|
|
entry.preset_name = preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name;
|
|
m_filament_entries.push_back(entry);
|
|
m_new_filament_colors.push_back(rgba);
|
|
m_new_filament_preset_names.push_back(preset_name.empty() ? m_default_virtual_filament_preset_name : preset_name);
|
|
return new_idx;
|
|
}
|
|
|
|
int TextureImportDialog::add_virtual_mixed_filament(const std::string& color_hex,
|
|
const std::vector<int>& component_dialog_indices,
|
|
const std::vector<int>& ratios)
|
|
{
|
|
if (m_filament_color_strs.size() != m_filament_colors_rgba.size() ||
|
|
m_filament_names.size() != m_filament_colors_rgba.size() ||
|
|
m_filament_entries.size() != m_filament_colors_rgba.size()) {
|
|
return -1;
|
|
}
|
|
if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size())
|
|
return -1;
|
|
for (int idx : component_dialog_indices) {
|
|
if (idx < 0 || idx >= (int)m_filament_entries.size() ||
|
|
!texture_entry_is_physical(m_filament_entries[idx].kind)) {
|
|
return -1;
|
|
}
|
|
}
|
|
if (!can_add_virtual_filament()) {
|
|
m_filaments_dropped = true;
|
|
return -1;
|
|
}
|
|
|
|
const int new_idx = (int)m_filament_colors_rgba.size();
|
|
TextureFilamentEntry entry;
|
|
entry.kind = TextureFilamentKind::NewMixed;
|
|
entry.dialog_index = new_idx;
|
|
entry.project_config_index = size_t(-1);
|
|
entry.color_hex = texture_normalize_color_hex(color_hex);
|
|
entry.name = DEFAULT_VIRTUAL_FILAMENT_NAME;
|
|
entry.mixed_ratios = ratios;
|
|
for (int idx : component_dialog_indices)
|
|
entry.mixed_components.push_back((unsigned int)(idx + 1));
|
|
|
|
TextureNewMixedFilament mixed;
|
|
mixed.dialog_index = entry.dialog_index;
|
|
mixed.color_hex = entry.color_hex;
|
|
mixed.component_dialog_indices = component_dialog_indices;
|
|
mixed.ratios = ratios;
|
|
|
|
m_filament_entries.push_back(entry);
|
|
m_filament_color_strs.push_back(entry.color_hex);
|
|
m_filament_names.push_back(entry.name);
|
|
m_filament_colors_rgba.push_back(parse_color_string(entry.color_hex));
|
|
m_new_mixed_filaments.push_back(mixed);
|
|
return entry.dialog_index;
|
|
}
|
|
|
|
void TextureImportDialog::compact_used_virtual_filaments()
|
|
{
|
|
if (m_current_matches.empty())
|
|
return;
|
|
|
|
const std::vector<std::array<float, 4>> old_colors = m_filament_colors_rgba;
|
|
const std::vector<std::string> old_color_strs = m_filament_color_strs;
|
|
const std::vector<std::string> old_names = m_filament_names;
|
|
const std::vector<TextureFilamentEntry> old_entries = m_filament_entries;
|
|
|
|
auto old_new_mixed_has_valid_components = [&old_entries, &old_colors](const TextureFilamentEntry& entry) {
|
|
if (entry.kind != TextureFilamentKind::NewMixed)
|
|
return true;
|
|
if (entry.mixed_components.size() < 2 || entry.mixed_components.size() != entry.mixed_ratios.size())
|
|
return false;
|
|
for (unsigned int comp : entry.mixed_components) {
|
|
int comp_idx = comp >= 1 ? (int)comp - 1 : -1;
|
|
if (comp_idx < 0 || comp_idx >= (int)old_entries.size() || comp_idx >= (int)old_colors.size() ||
|
|
!texture_entry_is_physical(old_entries[comp_idx].kind)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
std::set<int> used_virtual_indices;
|
|
for (const auto& m : m_current_matches) {
|
|
if (m.filament_index >= (int)m_existing_filament_count &&
|
|
m.filament_index < (int)old_colors.size()) {
|
|
if (m.filament_index < (int)old_entries.size() &&
|
|
old_entries[m.filament_index].kind == TextureFilamentKind::NewMixed &&
|
|
!old_new_mixed_has_valid_components(old_entries[m.filament_index])) {
|
|
continue;
|
|
}
|
|
used_virtual_indices.insert(m.filament_index);
|
|
}
|
|
}
|
|
bool added_dependency = true;
|
|
while (added_dependency) {
|
|
added_dependency = false;
|
|
std::vector<int> current_used(used_virtual_indices.begin(), used_virtual_indices.end());
|
|
for (int used_idx : current_used) {
|
|
if (used_idx < 0 || used_idx >= (int)old_entries.size() ||
|
|
old_entries[used_idx].kind != TextureFilamentKind::NewMixed ||
|
|
!old_new_mixed_has_valid_components(old_entries[used_idx]))
|
|
continue;
|
|
for (unsigned int comp : old_entries[used_idx].mixed_components) {
|
|
int comp_idx = comp >= 1 ? (int)comp - 1 : -1;
|
|
if (comp_idx >= (int)m_existing_filament_count && comp_idx < (int)old_entries.size() &&
|
|
used_virtual_indices.insert(comp_idx).second) {
|
|
added_dependency = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<std::array<float, 4>> compact_colors;
|
|
std::vector<std::string> compact_color_strs;
|
|
std::vector<std::string> compact_names;
|
|
std::vector<TextureFilamentEntry> compact_entries;
|
|
compact_colors.reserve(m_existing_filament_count + used_virtual_indices.size());
|
|
compact_color_strs.reserve(m_existing_filament_count + used_virtual_indices.size());
|
|
compact_names.reserve(m_existing_filament_count + used_virtual_indices.size());
|
|
compact_entries.reserve(m_existing_filament_count + used_virtual_indices.size());
|
|
|
|
const size_t existing_count = std::min(m_existing_filament_count, old_colors.size());
|
|
for (size_t i = 0; i < existing_count; ++i) {
|
|
compact_colors.push_back(old_colors[i]);
|
|
compact_color_strs.push_back(i < old_color_strs.size() ? old_color_strs[i] : "");
|
|
compact_names.push_back(i < old_names.size() ? old_names[i] : "Filament " + std::to_string(i + 1));
|
|
TextureFilamentEntry entry = i < old_entries.size() ? old_entries[i] : TextureFilamentEntry{};
|
|
entry.dialog_index = (int)i;
|
|
entry.color_hex = texture_normalize_color_hex(compact_color_strs.back());
|
|
entry.name = compact_names.back();
|
|
compact_entries.push_back(entry);
|
|
}
|
|
|
|
std::map<int, int> old_to_new;
|
|
std::vector<std::array<float, 4>> compact_new_colors;
|
|
std::vector<std::string> compact_new_preset_names;
|
|
compact_new_colors.reserve(used_virtual_indices.size());
|
|
compact_new_preset_names.reserve(used_virtual_indices.size());
|
|
|
|
for (int old_idx : used_virtual_indices) {
|
|
old_to_new[old_idx] = (int)compact_colors.size();
|
|
compact_colors.push_back(old_colors[old_idx]);
|
|
compact_color_strs.push_back(old_idx < (int)old_color_strs.size() ? old_color_strs[old_idx] : "");
|
|
compact_names.push_back(old_idx < (int)old_names.size() ? old_names[old_idx] : DEFAULT_VIRTUAL_FILAMENT_NAME);
|
|
TextureFilamentEntry entry = old_idx < (int)old_entries.size() ? old_entries[old_idx] : TextureFilamentEntry{};
|
|
entry.dialog_index = (int)compact_entries.size();
|
|
entry.color_hex = texture_normalize_color_hex(compact_color_strs.back());
|
|
entry.name = compact_names.back();
|
|
if (entry.kind == TextureFilamentKind::NewPhysical) {
|
|
compact_new_colors.push_back(old_colors[old_idx]);
|
|
compact_new_preset_names.push_back(entry.preset_name.empty() ? m_default_virtual_filament_preset_name : entry.preset_name);
|
|
}
|
|
compact_entries.push_back(entry);
|
|
}
|
|
|
|
m_filament_colors_rgba = std::move(compact_colors);
|
|
m_filament_color_strs = std::move(compact_color_strs);
|
|
m_filament_names = std::move(compact_names);
|
|
m_filament_entries = std::move(compact_entries);
|
|
m_new_filament_colors = std::move(compact_new_colors);
|
|
m_new_filament_preset_names = std::move(compact_new_preset_names);
|
|
m_new_mixed_filaments.clear();
|
|
std::set<int> invalid_compacted_mixed_indices;
|
|
for (auto& entry : m_filament_entries) {
|
|
if (entry.kind != TextureFilamentKind::NewMixed)
|
|
continue;
|
|
TextureNewMixedFilament mixed;
|
|
mixed.dialog_index = entry.dialog_index;
|
|
mixed.color_hex = entry.color_hex;
|
|
mixed.ratios = entry.mixed_ratios;
|
|
mixed.component_dialog_indices.reserve(entry.mixed_components.size());
|
|
bool valid_components = entry.mixed_components.size() >= 2 &&
|
|
entry.mixed_components.size() == entry.mixed_ratios.size();
|
|
for (unsigned int comp : entry.mixed_components) {
|
|
int old_comp_idx = comp >= 1 ? (int)comp - 1 : -1;
|
|
if (old_comp_idx < 0) {
|
|
valid_components = false;
|
|
break;
|
|
}
|
|
auto remap_it = old_to_new.find(old_comp_idx);
|
|
int new_comp_idx = remap_it != old_to_new.end() ? remap_it->second : old_comp_idx;
|
|
if (new_comp_idx < 0 || new_comp_idx >= (int)m_filament_entries.size() ||
|
|
!texture_entry_is_physical(m_filament_entries[new_comp_idx].kind)) {
|
|
valid_components = false;
|
|
break;
|
|
}
|
|
mixed.component_dialog_indices.push_back(new_comp_idx);
|
|
}
|
|
if (!valid_components) {
|
|
invalid_compacted_mixed_indices.insert(entry.dialog_index);
|
|
continue;
|
|
}
|
|
entry.mixed_components.clear();
|
|
for (int comp_idx : mixed.component_dialog_indices)
|
|
entry.mixed_components.push_back((unsigned int)(comp_idx + 1));
|
|
m_new_mixed_filaments.push_back(mixed);
|
|
}
|
|
|
|
auto find_closest_physical_filament_index = [this](const std::array<std::size_t, 3>& color) {
|
|
int best_idx = -1;
|
|
double best_delta = std::numeric_limits<double>::max();
|
|
const size_t filament_count = std::min(m_filament_colors_rgba.size(), max_filament_count());
|
|
for (size_t i = 0; i < filament_count && i < m_filament_entries.size(); ++i) {
|
|
if (!texture_entry_is_physical(m_filament_entries[i].kind))
|
|
continue;
|
|
const double delta = Slic3r::compute_delta_e(color, m_filament_colors_rgba[i]);
|
|
if (delta < best_delta) {
|
|
best_delta = delta;
|
|
best_idx = (int)i;
|
|
}
|
|
}
|
|
return best_idx;
|
|
};
|
|
|
|
for (auto& m : m_current_matches) {
|
|
auto it = old_to_new.find(m.filament_index);
|
|
if (it != old_to_new.end()) {
|
|
m.filament_index = it->second;
|
|
} else if (m.filament_index >= (int)m_existing_filament_count) {
|
|
m.filament_index = find_closest_filament_index(m.cluster_color);
|
|
}
|
|
if (invalid_compacted_mixed_indices.count(m.filament_index) > 0) {
|
|
int fallback_idx = find_closest_physical_filament_index(m.cluster_color);
|
|
m.filament_index = fallback_idx >= 0 ? fallback_idx : find_closest_filament_index(m.cluster_color);
|
|
}
|
|
|
|
if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) {
|
|
m.filament_color = m_filament_colors_rgba[m.filament_index];
|
|
m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color);
|
|
if (m.filament_index >= (int)m_existing_filament_count)
|
|
m.delta_e = 0.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<int> TextureImportDialog::compute_display_numbers() const
|
|
{
|
|
// Assigns each entry a 1-based display number in the order the sidebar will
|
|
// show after apply: ExistingPhysical, NewPhysical, ExistingMixed, NewMixed.
|
|
// This keeps the dialog's visible IDs in sync with the post-apply sidebar,
|
|
// instead of the raw dialog_index (which interleaves physicals and mixeds
|
|
// by processing order and causes e.g. CMYW to show 4,5,6,8 instead of 3,4,5,6).
|
|
// MUST mirror ordering in apply_textured_mesh_import_result (Plater.cpp:9896):
|
|
// - ExistingPhysical keeps its project_config_index
|
|
// - NewPhysical is inserted at existing_physical_count + new_order
|
|
// - ExistingMixed shifts to project_config_index + new_physical_count
|
|
// - NewMixed is appended after all existing mixeds
|
|
std::vector<int> result(m_filament_entries.size(), 0);
|
|
int next = 1;
|
|
|
|
auto assign_group = [&](TextureFilamentKind kind, bool by_project_config_index) {
|
|
if (by_project_config_index) {
|
|
std::vector<const TextureFilamentEntry*> group;
|
|
for (const auto& e : m_filament_entries)
|
|
if (e.kind == kind)
|
|
group.push_back(&e);
|
|
std::sort(group.begin(), group.end(),
|
|
[](const TextureFilamentEntry* a, const TextureFilamentEntry* b) {
|
|
return a->project_config_index < b->project_config_index;
|
|
});
|
|
for (const auto* e : group) {
|
|
if (e->dialog_index >= 0 && e->dialog_index < (int)result.size())
|
|
result[e->dialog_index] = next;
|
|
++next;
|
|
}
|
|
} else {
|
|
for (const auto& e : m_filament_entries) {
|
|
if (e.kind != kind)
|
|
continue;
|
|
if (e.dialog_index >= 0 && e.dialog_index < (int)result.size())
|
|
result[e.dialog_index] = next;
|
|
++next;
|
|
}
|
|
}
|
|
};
|
|
|
|
assign_group(TextureFilamentKind::ExistingPhysical, true);
|
|
assign_group(TextureFilamentKind::NewPhysical, false);
|
|
assign_group(TextureFilamentKind::ExistingMixed, true);
|
|
assign_group(TextureFilamentKind::NewMixed, false);
|
|
return result;
|
|
}
|
|
|
|
void TextureImportDialog::dismiss_filament_popup()
|
|
{
|
|
if (!m_filament_popup) {
|
|
m_filament_popup_row = -1;
|
|
return;
|
|
}
|
|
|
|
FilamentSelectPopup* popup = m_filament_popup;
|
|
m_filament_popup = nullptr;
|
|
m_filament_popup_row = -1;
|
|
if (popup->IsShown())
|
|
popup->Dismiss();
|
|
else
|
|
popup->Destroy();
|
|
}
|
|
|
|
void TextureImportDialog::show_auto_mix_popup()
|
|
{
|
|
if (!m_btn_auto_mix || !m_btn_auto_mix->IsEnabled())
|
|
return;
|
|
|
|
if (m_auto_mix_popup && m_auto_mix_popup->IsShown())
|
|
return;
|
|
dismiss_auto_mix_popup();
|
|
|
|
auto on_select = [this](TextureAutoMixMode mode) {
|
|
set_auto_mix_mode(mode);
|
|
};
|
|
auto on_close = [this]() {
|
|
m_auto_mix_popup = nullptr;
|
|
};
|
|
|
|
auto* popup = new AutoMixSelectPopup(this, m_auto_mix_mode, m_btn_auto_mix->GetSize().x,
|
|
m_auto_mix_font_point_size,
|
|
on_select, on_close);
|
|
wxPoint pos = m_btn_auto_mix->ClientToScreen(wxPoint(0, m_btn_auto_mix->GetSize().y));
|
|
wxRect display_rect;
|
|
int display_idx = wxDisplay::GetFromPoint(pos);
|
|
if (display_idx != wxNOT_FOUND)
|
|
display_rect = wxDisplay(display_idx).GetClientArea();
|
|
else
|
|
display_rect = wxDisplay().GetClientArea();
|
|
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
|
std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x));
|
|
popup->Position(pos, wxSize(0, 0));
|
|
popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) {
|
|
e.Skip();
|
|
if (m_auto_mix_popup == popup)
|
|
m_auto_mix_popup = nullptr;
|
|
});
|
|
m_auto_mix_popup = popup;
|
|
popup->Popup();
|
|
}
|
|
|
|
void TextureImportDialog::dismiss_auto_mix_popup()
|
|
{
|
|
if (!m_auto_mix_popup)
|
|
return;
|
|
|
|
AutoMixSelectPopup* popup = m_auto_mix_popup;
|
|
m_auto_mix_popup = nullptr;
|
|
if (popup->IsShown())
|
|
popup->Dismiss();
|
|
else
|
|
popup->Destroy();
|
|
}
|
|
|
|
void TextureImportDialog::set_auto_mix_mode(TextureAutoMixMode mode)
|
|
{
|
|
m_auto_mix_mode = mode;
|
|
if (m_btn_auto_mix) {
|
|
m_btn_auto_mix->SetLabel(auto_mix_mode_label(mode));
|
|
m_btn_auto_mix->Refresh();
|
|
}
|
|
apply_auto_standard_mix(mode);
|
|
}
|
|
|
|
void TextureImportDialog::apply_auto_standard_mix(TextureAutoMixMode mode)
|
|
{
|
|
if (m_mapping_rows.empty())
|
|
return;
|
|
m_filaments_dropped = false;
|
|
|
|
auto find_or_add_base_physical = [this](const std::string& color_hex) -> int {
|
|
const std::string normalized = texture_normalize_color_hex(color_hex);
|
|
for (const auto& entry : m_filament_entries) {
|
|
if (!texture_entry_is_physical(entry.kind))
|
|
continue;
|
|
if (texture_normalize_color_hex(entry.color_hex) != normalized)
|
|
continue;
|
|
if (texture_entry_is_pla_basic(entry))
|
|
return entry.dialog_index;
|
|
}
|
|
|
|
std::array<float, 4> rgba = parse_color_string(normalized);
|
|
int idx = add_virtual_filament(rgba, normalized, m_default_virtual_filament_preset_name);
|
|
if (idx >= 0 && idx < (int)m_filament_entries.size()) {
|
|
m_filament_entries[idx].type = DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE;
|
|
m_filament_entries[idx].name = DEFAULT_VIRTUAL_FILAMENT_NAME;
|
|
}
|
|
return idx;
|
|
};
|
|
|
|
auto find_existing_mixed = [this](const std::vector<int>& component_indices, const std::vector<int>& ratios) -> int {
|
|
for (const auto& entry : m_filament_entries) {
|
|
if (!texture_entry_is_mixed(entry.kind) || entry.mixed_components.size() != component_indices.size() ||
|
|
entry.mixed_ratios.size() != ratios.size())
|
|
continue;
|
|
bool same = true;
|
|
for (size_t i = 0; i < component_indices.size(); ++i) {
|
|
if (entry.mixed_components[i] != (unsigned int)(component_indices[i] + 1) ||
|
|
entry.mixed_ratios[i] != ratios[i]) {
|
|
same = false;
|
|
break;
|
|
}
|
|
}
|
|
if (same)
|
|
return entry.dialog_index;
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
bool changed = false;
|
|
const auto recipe_mode = texture_recipe_mode(mode);
|
|
for (size_t row_index = 0; row_index < m_mapping_rows.size(); ++row_index) {
|
|
Slic3r::ColorDecomposeRgb target_rgb;
|
|
if (!Slic3r::color_decompose_hex_to_rgb(m_mapping_rows[row_index].source_hex, target_rgb))
|
|
continue;
|
|
|
|
auto recipe = Slic3r::lookup_standard_recipe(target_rgb, recipe_mode, DEFAULT_VIRTUAL_FILAMENT_BASIC_TYPE);
|
|
if (!recipe.valid || recipe.components.size() < 2)
|
|
continue;
|
|
|
|
std::vector<int> component_dialog_indices;
|
|
std::vector<int> ratios;
|
|
for (const auto& comp : recipe.components) {
|
|
int component_idx = find_or_add_base_physical(comp.color_hex);
|
|
if (component_idx < 0) {
|
|
component_dialog_indices.clear();
|
|
break;
|
|
}
|
|
component_dialog_indices.push_back(component_idx);
|
|
ratios.push_back(comp.ratio);
|
|
}
|
|
if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size())
|
|
continue;
|
|
|
|
int mixed_idx = find_existing_mixed(component_dialog_indices, ratios);
|
|
if (mixed_idx < 0)
|
|
mixed_idx = add_virtual_mixed_filament(recipe.matched_color_hex, component_dialog_indices, ratios);
|
|
if (mixed_idx < 0)
|
|
continue;
|
|
|
|
m_mapping_rows[row_index].target_filament_idx = mixed_idx;
|
|
if (row_index < m_current_matches.size()) {
|
|
m_current_matches[row_index].filament_index = mixed_idx;
|
|
m_current_matches[row_index].filament_color = m_filament_colors_rgba[mixed_idx];
|
|
m_current_matches[row_index].delta_e = Slic3r::compute_delta_e(
|
|
m_current_matches[row_index].cluster_color, m_current_matches[row_index].filament_color);
|
|
if (mixed_idx >= (int)m_existing_filament_count)
|
|
m_current_matches[row_index].delta_e = 0.0;
|
|
}
|
|
changed = true;
|
|
}
|
|
|
|
if (!changed)
|
|
return;
|
|
|
|
m_auto_mix_applied = true;
|
|
compact_used_virtual_filaments();
|
|
update_filament_color_map();
|
|
rebuild_mapping_rows();
|
|
update_drop_warning_visibility();
|
|
update_auto_mix_reset_visibility();
|
|
}
|
|
|
|
void TextureImportDialog::reset_auto_mix()
|
|
{
|
|
if (m_state != TextureImportState::Ready || !m_auto_mix_applied)
|
|
return;
|
|
|
|
dismiss_auto_mix_popup();
|
|
|
|
// Clear mixed filament references so the compact inside do_auto_match()
|
|
// removes them (and their exclusively-owned base physicals) from the
|
|
// filament arrays, giving the baseline matching a clean starting state.
|
|
for (auto& m : m_current_matches) {
|
|
if (m.filament_index >= 0 && m.filament_index < (int)m_filament_entries.size() &&
|
|
texture_entry_is_mixed(m_filament_entries[m.filament_index].kind)) {
|
|
m.filament_index = -1;
|
|
}
|
|
}
|
|
|
|
// Re-run the baseline auto-match (same flow as the auto-merge toggle) so the
|
|
// mapping reverts to the pre-mix state: every colour matches an existing
|
|
// physical filament or a virtual physical filament, with no mixed filaments.
|
|
const auto previous_matches = m_current_matches;
|
|
do_auto_match();
|
|
restore_current_match_order(previous_matches);
|
|
compact_used_virtual_filaments();
|
|
update_filament_color_map();
|
|
rebuild_mapping_rows();
|
|
update_drop_warning_visibility();
|
|
update_auto_mix_reset_visibility();
|
|
}
|
|
|
|
void TextureImportDialog::update_auto_mix_reset_visibility()
|
|
{
|
|
if (!m_btn_mix_reset)
|
|
return;
|
|
if (m_btn_mix_reset->Show(m_auto_mix_applied)) {
|
|
if (wxWindow* parent = m_btn_mix_reset->GetParent())
|
|
parent->Layout();
|
|
}
|
|
}
|
|
|
|
bool TextureImportDialog::add_decomposed_mixed_filament(size_t row_index)
|
|
{
|
|
if (row_index >= m_mapping_rows.size())
|
|
return false;
|
|
|
|
std::vector<std::string> physical_colors;
|
|
std::vector<std::string> physical_names;
|
|
std::vector<std::string> physical_types;
|
|
std::vector<int> physical_dialog_indices;
|
|
std::vector<size_t> physical_config_indices;
|
|
auto& preset_bundle = *wxGetApp().preset_bundle;
|
|
for (const auto& entry : m_filament_entries) {
|
|
if (entry.kind != TextureFilamentKind::ExistingPhysical)
|
|
continue;
|
|
physical_colors.push_back(entry.color_hex);
|
|
physical_names.push_back(entry.name);
|
|
const size_t cfg_idx = entry.project_config_index;
|
|
Preset* preset = nullptr;
|
|
if (cfg_idx < preset_bundle.filament_presets.size())
|
|
preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]);
|
|
physical_types.push_back(filament_type_for_color_decompose(preset));
|
|
physical_dialog_indices.push_back(entry.dialog_index);
|
|
physical_config_indices.push_back(cfg_idx);
|
|
}
|
|
if (physical_colors.empty())
|
|
return false;
|
|
|
|
wxColour target(m_mapping_rows[row_index].source_hex);
|
|
ColorDecomposeDialog dlg(this, -1, target, physical_colors, physical_names, physical_types,
|
|
m_filament_entries.size(), max_filament_count(),
|
|
std::move(physical_config_indices));
|
|
// Count "new physical filaments" with the exact reuse rule of the write-back
|
|
// loop below: a base color is only new if no existing OR virtual official
|
|
// Bambu Basic filament already carries that color. This keeps the dialog's
|
|
// filament-limit pre-check consistent with what add_decomposed_mixed_filament
|
|
// will actually create, so already-present virtual base colors are not
|
|
// double counted (which previously could wrongly disable OK).
|
|
dlg.set_missing_physical_calculator([this](const ColorDecomposeResult& result) -> size_t {
|
|
size_t missing = 0;
|
|
for (const DecomposeComponent& comp : result.components) {
|
|
if (comp.filament_index > 0)
|
|
continue; // reuses a physical slot passed to the dialog, no new filament
|
|
const std::string comp_hex = texture_normalize_color_hex(
|
|
comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
|
bool found = false;
|
|
for (const auto& entry : m_filament_entries) {
|
|
if (!texture_entry_is_physical(entry.kind))
|
|
continue;
|
|
if (texture_normalize_color_hex(entry.color_hex) != comp_hex)
|
|
continue;
|
|
if (texture_entry_official_basic(entry)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
++missing;
|
|
}
|
|
return missing;
|
|
});
|
|
if (dlg.ShowModal() != wxID_OK)
|
|
return false;
|
|
|
|
ColorDecomposeResult result = dlg.get_result();
|
|
std::vector<int> component_dialog_indices;
|
|
std::vector<int> ratios;
|
|
for (const DecomposeComponent& comp : result.components) {
|
|
ratios.push_back(comp.ratio);
|
|
if (comp.filament_index > 0) {
|
|
const size_t physical_idx = (size_t)(comp.filament_index - 1);
|
|
if (physical_idx >= physical_dialog_indices.size())
|
|
return false;
|
|
component_dialog_indices.push_back(physical_dialog_indices[physical_idx]);
|
|
continue;
|
|
}
|
|
|
|
const std::string comp_hex = texture_normalize_color_hex(comp.colour.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
|
int existing_idx = -1;
|
|
for (const auto& entry : m_filament_entries) {
|
|
if (!texture_entry_is_physical(entry.kind))
|
|
continue;
|
|
if (texture_normalize_color_hex(entry.color_hex) != comp_hex)
|
|
continue;
|
|
if (texture_entry_official_basic(entry)) {
|
|
existing_idx = entry.dialog_index;
|
|
break;
|
|
}
|
|
}
|
|
if (existing_idx < 0) {
|
|
std::array<float, 4> rgba = parse_color_string(comp_hex);
|
|
existing_idx = add_virtual_filament(rgba, comp_hex);
|
|
if (existing_idx < 0)
|
|
return false;
|
|
}
|
|
component_dialog_indices.push_back(existing_idx);
|
|
}
|
|
|
|
if (component_dialog_indices.size() < 2 || component_dialog_indices.size() != ratios.size())
|
|
return false;
|
|
|
|
const std::string mixed_hex = texture_normalize_color_hex(
|
|
result.matched_color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
|
|
int mixed_idx = add_virtual_mixed_filament(mixed_hex, component_dialog_indices, ratios);
|
|
if (mixed_idx < 0)
|
|
return false;
|
|
|
|
m_mapping_rows[row_index].target_filament_idx = mixed_idx;
|
|
if (row_index < m_current_matches.size())
|
|
m_current_matches[row_index].filament_index = mixed_idx;
|
|
rebuild_mapping_rows();
|
|
update_filament_color_map();
|
|
return true;
|
|
}
|
|
|
|
void TextureImportDialog::dismiss_filament_popup_on_wheel(wxMouseEvent& evt)
|
|
{
|
|
dismiss_filament_popup();
|
|
dismiss_auto_mix_popup();
|
|
evt.Skip();
|
|
}
|
|
|
|
void TextureImportDialog::show_filament_popup(size_t row_index)
|
|
{
|
|
if (row_index >= m_mapping_rows.size()) return;
|
|
|
|
if (m_skip_next_filament_popup_row == (int)row_index) {
|
|
m_skip_next_filament_popup_row = -1;
|
|
return;
|
|
}
|
|
|
|
if (m_filament_popup && m_filament_popup->IsShown()) {
|
|
if (m_filament_popup_row == (int)row_index) {
|
|
dismiss_filament_popup();
|
|
return;
|
|
}
|
|
dismiss_filament_popup();
|
|
}
|
|
|
|
const auto display_numbers = compute_display_numbers();
|
|
auto display_number = [display_numbers](int idx) -> int {
|
|
return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0)
|
|
? display_numbers[idx] : idx + 1;
|
|
};
|
|
|
|
auto on_select = [this, row_index, display_number](int idx) {
|
|
if (row_index >= m_mapping_rows.size()) return;
|
|
m_mapping_rows[row_index].target_filament_idx = idx;
|
|
if (row_index < m_current_matches.size())
|
|
m_current_matches[row_index].filament_index = idx;
|
|
if (m_mapping_rows[row_index].target_panel) {
|
|
wxString label = (idx >= 0 && idx < (int)m_filament_names.size())
|
|
? filament_name_to_wx_string(m_filament_names[idx])
|
|
: wxString::Format("Filament %d", display_number(idx));
|
|
m_mapping_rows[row_index].target_panel->SetToolTip(label);
|
|
m_mapping_rows[row_index].target_panel->Refresh();
|
|
}
|
|
update_filament_color_map();
|
|
};
|
|
|
|
auto on_add_filament = [this, row_index](wxColour clr) {
|
|
std::array<float, 4> rgba = {clr.Red() / 255.f, clr.Green() / 255.f,
|
|
clr.Blue() / 255.f, 1.0f};
|
|
std::string hex = wxString::Format("#%02X%02X%02X",
|
|
clr.Red(), clr.Green(), clr.Blue()).ToStdString();
|
|
int new_idx = add_virtual_filament(rgba, hex);
|
|
if (new_idx < 0)
|
|
return;
|
|
|
|
if (row_index < m_mapping_rows.size()) {
|
|
m_mapping_rows[row_index].target_filament_idx = new_idx;
|
|
if (row_index < m_current_matches.size())
|
|
m_current_matches[row_index].filament_index = new_idx;
|
|
}
|
|
rebuild_mapping_rows();
|
|
update_filament_color_map();
|
|
};
|
|
|
|
auto on_decompose_color = [this, row_index]() {
|
|
CallAfter([this, row_index]() {
|
|
add_decomposed_mixed_filament(row_index);
|
|
});
|
|
};
|
|
|
|
wxPanel* tp = m_mapping_rows[row_index].target_panel;
|
|
if (!tp) return;
|
|
|
|
auto on_close = [this, row_index](bool closed_by_action) {
|
|
if (m_filament_popup_row == (int)row_index) {
|
|
m_filament_popup = nullptr;
|
|
m_filament_popup_row = -1;
|
|
}
|
|
if (!closed_by_action) {
|
|
m_skip_next_filament_popup_row = (int)row_index;
|
|
CallAfter([this, row_index]() {
|
|
if (m_skip_next_filament_popup_row == (int)row_index)
|
|
m_skip_next_filament_popup_row = -1;
|
|
});
|
|
}
|
|
};
|
|
|
|
auto* popup = new FilamentSelectPopup(
|
|
this, m_filament_entries, m_filament_colors_rgba, m_filament_names,
|
|
m_existing_filament_count, tp->GetSize().x, tp, on_select, on_add_filament,
|
|
on_decompose_color,
|
|
[this]() { return can_add_virtual_filament(); },
|
|
on_close,
|
|
display_numbers);
|
|
|
|
wxPoint pos = tp->ClientToScreen(wxPoint(0, tp->GetSize().y));
|
|
wxRect display_rect;
|
|
int display_idx = wxDisplay::GetFromPoint(pos);
|
|
if (display_idx != wxNOT_FOUND)
|
|
display_rect = wxDisplay(display_idx).GetClientArea();
|
|
else
|
|
display_rect = wxDisplay().GetClientArea();
|
|
pos.x = std::clamp(pos.x, display_rect.GetLeft(),
|
|
std::max(display_rect.GetLeft(), display_rect.GetRight() - popup->GetSize().x));
|
|
popup->Position(pos, wxSize(0, 0));
|
|
popup->Bind(wxEVT_DESTROY, [this, popup](wxWindowDestroyEvent& e) {
|
|
e.Skip();
|
|
if (m_filament_popup == popup) {
|
|
m_filament_popup = nullptr;
|
|
m_filament_popup_row = -1;
|
|
}
|
|
});
|
|
m_filament_popup = popup;
|
|
m_filament_popup_row = (int)row_index;
|
|
popup->Popup();
|
|
}
|
|
|
|
void TextureImportDialog::do_auto_match()
|
|
{
|
|
if (m_painted.cluster_colors.empty()) return;
|
|
|
|
// do_auto_match() always rebuilds the baseline mapping without any mixed
|
|
// filaments, so it is the common entry for every "revert one-click mix"
|
|
// path. Clear the applied flag here; callers refresh the reset button.
|
|
m_auto_mix_applied = false;
|
|
|
|
// Reset the "filaments were dropped" flag at the start of every run, so it
|
|
// strictly reflects what happens during *this* match (no historical
|
|
// accumulation). add_virtual_filament() will flip it back to true if and
|
|
// only if it hits the global filament cap below.
|
|
m_filaments_dropped = false;
|
|
|
|
// Drop any virtual filaments left over from previous match runs that the
|
|
// current m_current_matches no longer references. Without this, the
|
|
// residual virtual filaments inflate m_filament_colors_rgba.size() at the
|
|
// entry of this match, which can make add_virtual_filament() fail (and
|
|
// wrongly flip m_filaments_dropped to true) even when the *real* count
|
|
// of needed virtual filaments for this run is well below the global cap.
|
|
// This is purely a state cleanup; it does not change any mapping rule.
|
|
compact_used_virtual_filaments();
|
|
|
|
const auto previous_matches = m_current_matches;
|
|
|
|
std::map<std::array<std::size_t, 3>, int> previous_virtual_by_cluster;
|
|
for (const auto& match : previous_matches) {
|
|
if (match.filament_index >= (int)m_existing_filament_count &&
|
|
match.filament_index < (int)m_filament_entries.size() &&
|
|
texture_entry_is_physical(m_filament_entries[match.filament_index].kind)) {
|
|
previous_virtual_by_cluster[match.cluster_color] = match.filament_index;
|
|
}
|
|
}
|
|
|
|
auto find_virtual_filament_by_color = [this](const std::array<std::size_t, 3>& color) -> int {
|
|
std::string hex = rgb_to_hex(color).ToStdString();
|
|
for (size_t i = m_existing_filament_count; i < m_filament_color_strs.size(); ++i) {
|
|
if (m_filament_color_strs[i] == hex &&
|
|
i < m_filament_entries.size() && texture_entry_is_physical(m_filament_entries[i].kind))
|
|
return (int)i;
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
auto get_or_add_virtual_filament = [this, &previous_virtual_by_cluster, &find_virtual_filament_by_color](
|
|
const std::array<std::size_t, 3>& color) -> int {
|
|
auto previous_it = previous_virtual_by_cluster.find(color);
|
|
if (previous_it != previous_virtual_by_cluster.end() &&
|
|
previous_it->second >= (int)m_existing_filament_count &&
|
|
previous_it->second < (int)m_filament_colors_rgba.size()) {
|
|
return previous_it->second;
|
|
}
|
|
|
|
int existing_idx = find_virtual_filament_by_color(color);
|
|
if (existing_idx >= 0)
|
|
return existing_idx;
|
|
|
|
std::array<float, 4> rgba = {
|
|
color[0] / 255.f,
|
|
color[1] / 255.f,
|
|
color[2] / 255.f,
|
|
1.f
|
|
};
|
|
return add_virtual_filament(rgba, rgb_to_hex(color).ToStdString());
|
|
};
|
|
|
|
if (m_auto_merge_cb && m_auto_merge_cb->GetValue()) {
|
|
// Match clusters to closest existing filaments
|
|
std::vector<std::string> names;
|
|
for (size_t i = 0; i < m_existing_filament_count; ++i)
|
|
names.push_back(m_filament_names.size() > i ? m_filament_names[i] : "Filament " + std::to_string(i + 1));
|
|
|
|
std::vector<std::array<float, 4>> existing_filament_colors(
|
|
m_filament_colors_rgba.begin(),
|
|
m_filament_colors_rgba.begin() + std::min(m_existing_filament_count, m_filament_colors_rgba.size()));
|
|
|
|
m_current_matches = Slic3r::match_clusters_to_filaments(
|
|
m_painted.cluster_colors, existing_filament_colors, names);
|
|
|
|
// For clusters with poor match (CIEDE2000 ΔE > 5), create virtual filaments.
|
|
constexpr double NEW_FILAMENT_THRESHOLD = 5.0;
|
|
std::map<std::array<std::size_t, 3>, int> virtual_color_index;
|
|
|
|
for (auto& m : m_current_matches) {
|
|
if (m.delta_e <= NEW_FILAMENT_THRESHOLD)
|
|
continue;
|
|
|
|
auto it = virtual_color_index.find(m.cluster_color);
|
|
if (it != virtual_color_index.end()) {
|
|
m.filament_index = it->second;
|
|
} else {
|
|
int new_idx = get_or_add_virtual_filament(m.cluster_color);
|
|
if (new_idx >= 0)
|
|
virtual_color_index[m.cluster_color] = new_idx;
|
|
m.filament_index = new_idx >= 0 ? new_idx : find_closest_filament_index(m.cluster_color);
|
|
}
|
|
if (m.filament_index >= 0 && m.filament_index < (int)m_filament_colors_rgba.size()) {
|
|
m.filament_color = m_filament_colors_rgba[m.filament_index];
|
|
m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color);
|
|
if (m.filament_index >= (int)m_existing_filament_count)
|
|
m.delta_e = 0.0;
|
|
}
|
|
}
|
|
} else {
|
|
// Keep all virtual filaments in this dialog; unused ones are pruned only on OK.
|
|
m_current_matches.clear();
|
|
std::map<std::array<std::size_t, 3>, int> virtual_map;
|
|
|
|
for (size_t i = 0; i < m_painted.cluster_colors.size(); ++i) {
|
|
const auto& cc = m_painted.cluster_colors[i];
|
|
Slic3r::FilamentMatch fm;
|
|
fm.cluster_index = (int)i;
|
|
fm.cluster_color = cc;
|
|
|
|
auto it = virtual_map.find(cc);
|
|
if (it != virtual_map.end()) {
|
|
fm.filament_index = it->second;
|
|
} else {
|
|
int idx = get_or_add_virtual_filament(cc);
|
|
if (idx >= 0)
|
|
virtual_map[cc] = idx;
|
|
fm.filament_index = idx >= 0 ? idx : find_closest_filament_index(cc);
|
|
}
|
|
if (fm.filament_index >= 0 && fm.filament_index < (int)m_filament_colors_rgba.size()) {
|
|
fm.filament_color = m_filament_colors_rgba[fm.filament_index];
|
|
fm.delta_e = Slic3r::compute_delta_e(fm.cluster_color, fm.filament_color);
|
|
if (fm.filament_index >= (int)m_existing_filament_count)
|
|
fm.delta_e = 0.0;
|
|
}
|
|
m_current_matches.push_back(fm);
|
|
}
|
|
}
|
|
|
|
update_filament_color_map();
|
|
}
|
|
|
|
void TextureImportDialog::rebuild_mapping_rows()
|
|
{
|
|
m_mapping_scroll->Freeze();
|
|
m_mapping_sizer->Clear(true);
|
|
m_mapping_rows.clear();
|
|
|
|
if (m_current_matches.empty()) {
|
|
m_mapping_scroll->FitInside();
|
|
m_mapping_scroll->Thaw();
|
|
return;
|
|
}
|
|
|
|
auto get_target_wxcolor = [this](int idx) -> wxColour {
|
|
if (idx >= 0 && idx < (int)m_filament_colors_rgba.size()) {
|
|
const auto& c = m_filament_colors_rgba[idx];
|
|
return wxColour((unsigned char)(c[0] * 255.f),
|
|
(unsigned char)(c[1] * 255.f),
|
|
(unsigned char)(c[2] * 255.f));
|
|
}
|
|
return wxColour(128, 128, 128);
|
|
};
|
|
|
|
const auto display_numbers = compute_display_numbers();
|
|
auto display_number = [display_numbers](int idx) -> int {
|
|
return (idx >= 0 && idx < (int)display_numbers.size() && display_numbers[idx] > 0)
|
|
? display_numbers[idx] : idx + 1;
|
|
};
|
|
|
|
auto get_filament_label = [this, display_number](int idx) -> wxString {
|
|
if (idx >= 0 && idx < (int)m_filament_names.size())
|
|
return filament_name_to_wx_string(m_filament_names[idx]);
|
|
return wxString::Format("Filament %d", display_number(idx));
|
|
};
|
|
|
|
const wxColour dash_clr = StateColor::darkModeColorFor(wxColour("#ACACAC"));
|
|
const wxColour hex_fg = texture_import_text_colour();
|
|
const wxColour card_bg = StateColor::darkModeColorFor(wxColour("#E8E8E8"));
|
|
const wxColour card_bd = StateColor::darkModeColorFor(wxColour("#DBDBDB"));
|
|
const wxColour name_fg = texture_import_text_colour();
|
|
const wxColour chev_clr = StateColor::darkModeColorFor(wxColour("#6B6B6A"));
|
|
|
|
m_mapping_rows.resize(m_current_matches.size());
|
|
for (size_t ci = 0; ci < m_current_matches.size(); ++ci) {
|
|
auto& row = m_mapping_rows[ci];
|
|
row.cluster_id = m_current_matches[ci].cluster_index;
|
|
row.source_color = m_current_matches[ci].cluster_color;
|
|
row.source_hex = rgb_to_hex(row.source_color).ToStdString();
|
|
row.target_filament_idx = m_current_matches[ci].filament_index;
|
|
|
|
wxColour src_wx_color(
|
|
(unsigned char)row.source_color[0],
|
|
(unsigned char)row.source_color[1],
|
|
(unsigned char)row.source_color[2]);
|
|
|
|
// --- Row container ---
|
|
wxPanel* row_panel = new wxPanel(m_mapping_scroll, wxID_ANY);
|
|
row_panel->SetBackgroundColour(m_mapping_scroll->GetBackgroundColour());
|
|
row_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
wxBoxSizer* row_sizer = new wxBoxSizer(wxHORIZONTAL);
|
|
|
|
// --- Source card (dashed border, circle + hex) ---
|
|
const int src_w = FromDIP(138);
|
|
const int target_min_w = FromDIP(239);
|
|
const int row_h = FromDIP(44);
|
|
row.source_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(src_w, row_h),
|
|
wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE);
|
|
row.source_panel->SetMinSize(wxSize(src_w, row_h));
|
|
row.source_panel->SetMaxSize(wxSize(src_w, row_h));
|
|
row.source_panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
|
|
row.source_panel->Bind(wxEVT_PAINT, [this, ci, src_wx_color, dash_clr, hex_fg](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
|
|
dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour()));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
wxPen dash_pen(dash_clr, 1, wxPENSTYLE_SHORT_DASH);
|
|
dc.SetPen(dash_pen);
|
|
dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour()));
|
|
int r = p->FromDIP(8);
|
|
dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r);
|
|
|
|
// Color circle 24px
|
|
int cd = p->FromDIP(24);
|
|
int cx = p->FromDIP(10);
|
|
int cy = (sz.y - cd) / 2;
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetBrush(wxBrush(src_wx_color));
|
|
dc.DrawEllipse(cx, cy, cd, cd);
|
|
draw_filament_swatch_ellipse_border(dc, src_wx_color, cx, cy, cd, cd);
|
|
|
|
if (ci < m_mapping_rows.size()) {
|
|
wxFont hex_font = p->GetFont();
|
|
hex_font.SetPointSize(9);
|
|
dc.SetFont(hex_font);
|
|
dc.SetTextForeground(hex_fg);
|
|
wxString hex_str = wxString::Format("# %s", m_mapping_rows[ci].source_hex.substr(1));
|
|
wxSize tsz = dc.GetTextExtent(hex_str);
|
|
dc.DrawText(hex_str, cx + cd + p->FromDIP(6), (sz.y - tsz.y) / 2);
|
|
}
|
|
});
|
|
row.source_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) {
|
|
e.Skip();
|
|
static_cast<wxWindow*>(e.GetEventObject())->Refresh();
|
|
});
|
|
row.source_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
|
|
row_sizer->Add(row.source_panel, 0, wxEXPAND);
|
|
|
|
// --- Arrow panel (dashed arrow) ---
|
|
const int arrow_w = FromDIP(24);
|
|
wxPanel* arrow_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(arrow_w, row_h));
|
|
arrow_panel->SetMinSize(wxSize(arrow_w, row_h));
|
|
arrow_panel->SetMaxSize(wxSize(arrow_w, row_h));
|
|
arrow_panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
arrow_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
arrow_panel->Bind(wxEVT_PAINT, [dash_clr](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
|
|
dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour()));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
int mid_y = sz.y / 2;
|
|
int margin = p->FromDIP(2);
|
|
int arrow_tip = sz.x - margin;
|
|
int arrow_start = margin;
|
|
|
|
wxPen dash_pen(dash_clr, p->FromDIP(1), wxPENSTYLE_SHORT_DASH);
|
|
dc.SetPen(dash_pen);
|
|
dc.DrawLine(arrow_start, mid_y, arrow_tip - p->FromDIP(4), mid_y);
|
|
|
|
int ah = p->FromDIP(4);
|
|
wxPoint tri[3] = {
|
|
{arrow_tip, mid_y},
|
|
{arrow_tip - ah, mid_y - ah / 2},
|
|
{arrow_tip - ah, mid_y + ah / 2}
|
|
};
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetBrush(wxBrush(dash_clr));
|
|
dc.DrawPolygon(3, tri);
|
|
});
|
|
|
|
row_sizer->Add(arrow_panel, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
|
|
|
|
// --- Target card (numbered square + material name + chevron) ---
|
|
row.target_panel = new wxPanel(row_panel, wxID_ANY, wxDefaultPosition, wxSize(-1, row_h),
|
|
wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE);
|
|
row.target_panel->SetMinSize(wxSize(target_min_w, row_h));
|
|
row.target_panel->SetToolTip(get_filament_label(row.target_filament_idx));
|
|
row.target_panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
|
|
row.target_panel->SetCursor(wxCursor(wxCURSOR_HAND));
|
|
|
|
row.target_panel->Bind(wxEVT_PAINT, [this, ci, get_target_wxcolor, get_filament_label,
|
|
display_number, card_bg, card_bd, name_fg, chev_clr](wxPaintEvent& e) {
|
|
auto* p = static_cast<wxPanel*>(e.GetEventObject());
|
|
wxAutoBufferedPaintDC dc(p);
|
|
wxSize sz = p->GetClientSize();
|
|
|
|
dc.SetBrush(wxBrush(p->GetParent()->GetBackgroundColour()));
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.DrawRectangle(0, 0, sz.x, sz.y);
|
|
|
|
if (ci >= m_mapping_rows.size()) return;
|
|
int fil_idx = m_mapping_rows[ci].target_filament_idx;
|
|
|
|
int r = p->FromDIP(8);
|
|
dc.SetBrush(wxBrush(card_bg));
|
|
dc.SetPen(wxPen(card_bd, 1));
|
|
dc.DrawRoundedRectangle(0, 0, sz.x, sz.y, r);
|
|
|
|
if (fil_idx >= 0 && fil_idx < (int)m_filament_entries.size() &&
|
|
texture_entry_is_mixed(m_filament_entries[fil_idx].kind)) {
|
|
const TextureFilamentEntry& entry = m_filament_entries[fil_idx];
|
|
wxFont mixed_font = p->GetFont();
|
|
mixed_font.SetPointSize(10);
|
|
dc.SetFont(mixed_font);
|
|
|
|
int x = p->FromDIP(10);
|
|
const int sw = p->FromDIP(28);
|
|
const int sw_r = p->FromDIP(6);
|
|
const int sw_y = (sz.y - sw) / 2;
|
|
for (size_t mi = 0; mi < entry.mixed_components.size() && mi < entry.mixed_ratios.size(); ++mi) {
|
|
if (mi > 0) {
|
|
dc.SetTextForeground(name_fg);
|
|
wxString plus = "+";
|
|
wxSize psz = dc.GetTextExtent(plus);
|
|
dc.DrawText(plus, x, (sz.y - psz.y) / 2);
|
|
x += psz.x + p->FromDIP(4);
|
|
}
|
|
|
|
const unsigned int comp_id = entry.mixed_components[mi];
|
|
const int comp_idx = comp_id >= 1 ? (int)comp_id - 1 : -1;
|
|
wxColour comp_clr("#D9D9D9");
|
|
if (comp_idx >= 0 && comp_idx < (int)m_filament_colors_rgba.size()) {
|
|
const auto& c = m_filament_colors_rgba[comp_idx];
|
|
comp_clr = wxColour((unsigned char)(c[0] * 255.f),
|
|
(unsigned char)(c[1] * 255.f),
|
|
(unsigned char)(c[2] * 255.f));
|
|
}
|
|
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetBrush(wxBrush(comp_clr));
|
|
dc.DrawRoundedRectangle(x, sw_y, sw, sw, sw_r);
|
|
draw_filament_swatch_border(dc, comp_clr, x, sw_y, sw, sw, sw_r);
|
|
|
|
wxString num_str = wxString::Format("%d", display_number(comp_idx));
|
|
wxSize nsz = dc.GetTextExtent(num_str);
|
|
dc.SetTextForeground(comp_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000());
|
|
dc.DrawText(num_str, x + (sw - nsz.x) / 2, sw_y + (sw - nsz.y) / 2);
|
|
x += sw + p->FromDIP(5);
|
|
|
|
dc.SetTextForeground(name_fg);
|
|
wxString pct = wxString::Format("%d%%", entry.mixed_ratios[mi]);
|
|
wxSize pct_sz = dc.GetTextExtent(pct);
|
|
dc.DrawText(pct, x, (sz.y - pct_sz.y) / 2);
|
|
x += pct_sz.x + p->FromDIP(5);
|
|
if (x > sz.x - p->FromDIP(34))
|
|
break;
|
|
}
|
|
|
|
int chev_cx = sz.x - p->FromDIP(14);
|
|
int chev_cy = sz.y / 2;
|
|
int hw = p->FromDIP(3);
|
|
int hh = p->FromDIP(2);
|
|
dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1));
|
|
dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh);
|
|
dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh);
|
|
return;
|
|
}
|
|
|
|
// Numbered color square 32x32, rounded 6px
|
|
int sq = p->FromDIP(32);
|
|
int sq_x = p->FromDIP(6);
|
|
int sq_y = (sz.y - sq) / 2;
|
|
int sq_r = p->FromDIP(6);
|
|
wxColour fil_clr = get_target_wxcolor(fil_idx);
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetBrush(wxBrush(fil_clr));
|
|
dc.DrawRoundedRectangle(sq_x, sq_y, sq, sq, sq_r);
|
|
draw_filament_swatch_border(dc, fil_clr, sq_x, sq_y, sq, sq, sq_r);
|
|
|
|
{
|
|
wxFont num_font = p->GetFont();
|
|
num_font.SetPointSize(10);
|
|
dc.SetFont(num_font);
|
|
dc.SetTextForeground(fil_clr.GetLuminance() < 0.6 ? *wxWHITE : texture_import_gray9000());
|
|
wxString num_str = wxString::Format("%d", display_number(fil_idx));
|
|
wxSize nsz = dc.GetTextExtent(num_str);
|
|
dc.DrawText(num_str, sq_x + (sq - nsz.x) / 2, sq_y + (sq - nsz.y) / 2);
|
|
}
|
|
|
|
// Material name
|
|
{
|
|
wxFont name_font = p->GetFont();
|
|
name_font.SetPointSize(9);
|
|
dc.SetFont(name_font);
|
|
dc.SetTextForeground(name_fg);
|
|
wxString name_str = get_filament_label(fil_idx);
|
|
int text_x = sq_x + sq + p->FromDIP(8);
|
|
int max_text_w = sz.x - text_x - p->FromDIP(24);
|
|
if (max_text_w > 0) {
|
|
name_str = ellipsize_text(dc, name_str, max_text_w);
|
|
wxSize tsz = dc.GetTextExtent(name_str);
|
|
dc.DrawText(name_str, text_x, (sz.y - tsz.y) / 2);
|
|
}
|
|
}
|
|
|
|
// Dropdown chevron at right edge
|
|
{
|
|
int chev_cx = sz.x - p->FromDIP(14);
|
|
int chev_cy = sz.y / 2;
|
|
int hw = p->FromDIP(3);
|
|
int hh = p->FromDIP(2);
|
|
dc.SetPen(wxPen(chev_clr, p->FromDIP(1) > 0 ? p->FromDIP(1) : 1));
|
|
dc.DrawLine(chev_cx - hw, chev_cy - hh, chev_cx, chev_cy + hh);
|
|
dc.DrawLine(chev_cx, chev_cy + hh, chev_cx + hw, chev_cy - hh);
|
|
}
|
|
});
|
|
|
|
row.target_panel->Bind(wxEVT_LEFT_DOWN, [this, ci](wxMouseEvent&) {
|
|
show_filament_popup(ci);
|
|
});
|
|
row.target_panel->Bind(wxEVT_SIZE, [](wxSizeEvent& e) {
|
|
e.Skip();
|
|
static_cast<wxWindow*>(e.GetEventObject())->Refresh();
|
|
});
|
|
row.target_panel->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
|
|
|
|
row_sizer->Add(row.target_panel, 1, wxEXPAND);
|
|
|
|
row_panel->SetSizer(row_sizer);
|
|
m_mapping_sizer->Add(row_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(12));
|
|
}
|
|
|
|
m_mapping_scroll->FitInside();
|
|
m_mapping_scroll->Layout();
|
|
m_mapping_scroll->Thaw();
|
|
}
|
|
|
|
std::vector<Slic3r::FilamentMatch> TextureImportDialog::build_matches_from_rows() const
|
|
{
|
|
std::vector<Slic3r::FilamentMatch> matches(m_mapping_rows.size());
|
|
for (size_t i = 0; i < m_mapping_rows.size(); ++i) {
|
|
auto& m = matches[i];
|
|
m.cluster_index = m_mapping_rows[i].cluster_id;
|
|
m.cluster_color = m_mapping_rows[i].source_color;
|
|
|
|
int sel = m_mapping_rows[i].target_filament_idx;
|
|
if (sel >= 0 && sel < (int)m_filament_colors_rgba.size()) {
|
|
m.filament_index = sel;
|
|
m.filament_color = m_filament_colors_rgba[sel];
|
|
m.delta_e = Slic3r::compute_delta_e(m.cluster_color, m.filament_color);
|
|
}
|
|
}
|
|
return matches;
|
|
}
|
|
|
|
// ---- Event handlers ----
|
|
|
|
void TextureImportDialog::update_color_count_preset_buttons()
|
|
{
|
|
if (m_btn_color_4) m_btn_color_4->SetValue(m_param_color_count == 4);
|
|
if (m_btn_color_8) m_btn_color_8->SetValue(m_param_color_count == 8);
|
|
if (m_btn_color_16) m_btn_color_16->SetValue(m_param_color_count == 16);
|
|
}
|
|
|
|
void TextureImportDialog::set_color_count_value(int value, bool update_spin)
|
|
{
|
|
m_param_color_count = std::clamp(value, 1, (int)max_filament_count());
|
|
m_color_slider->SetValue(m_param_color_count);
|
|
if (update_spin)
|
|
m_color_spin->SetValue(m_param_color_count);
|
|
update_color_count_preset_buttons();
|
|
update_confirm_button_state();
|
|
}
|
|
|
|
void TextureImportDialog::set_smooth_value(int value, bool update_spin)
|
|
{
|
|
m_param_smooth = std::clamp(value, 0, 10);
|
|
m_smooth_slider->SetValue(m_param_smooth);
|
|
if (update_spin)
|
|
m_smooth_spin->SetValue(m_param_smooth);
|
|
update_confirm_button_state();
|
|
}
|
|
|
|
void TextureImportDialog::preview_spin_text_value(SpinInput* spin, AccentSlider* slider, int& param,
|
|
int min_value, int max_value, const wxString& text,
|
|
std::function<void()> on_value_changed)
|
|
{
|
|
long value;
|
|
if (!text.ToLong(&value))
|
|
return;
|
|
|
|
wxTextCtrl* tc = spin->GetTextCtrl();
|
|
long parsed = value;
|
|
value = std::clamp((int)parsed, min_value, max_value);
|
|
|
|
wxString normalized = text;
|
|
if (parsed > max_value || (text.length() > 1 && text[0] == '0'))
|
|
normalized = wxString::Format("%ld", value);
|
|
|
|
if (normalized != text) {
|
|
long pos = tc->GetInsertionPoint();
|
|
tc->ChangeValue(normalized);
|
|
if (parsed > max_value)
|
|
tc->SetInsertionPointEnd();
|
|
else
|
|
tc->SetInsertionPoint(std::min<long>(normalized.length(), std::max(0L, pos - 1)));
|
|
}
|
|
|
|
param = (int)value;
|
|
slider->SetValue(param);
|
|
if (on_value_changed)
|
|
on_value_changed();
|
|
update_confirm_button_state();
|
|
}
|
|
|
|
void TextureImportDialog::on_color_preset_clicked(wxCommandEvent& evt)
|
|
{
|
|
int id = evt.GetId();
|
|
int color_count = m_param_color_count;
|
|
if (id == ID_COLOR_4) { color_count = 4; }
|
|
if (id == ID_COLOR_8) { color_count = 8; }
|
|
if (id == ID_COLOR_16) { color_count = 16; }
|
|
|
|
if (id == ID_COLOR_AUTO) {
|
|
start_computation(true);
|
|
return;
|
|
}
|
|
|
|
set_color_count_value(color_count, true);
|
|
}
|
|
|
|
void TextureImportDialog::on_color_slider_changed(wxCommandEvent&)
|
|
{
|
|
set_color_count_value(m_color_slider->GetValue(), true);
|
|
}
|
|
|
|
void TextureImportDialog::on_color_spin_changed(wxCommandEvent&)
|
|
{
|
|
set_color_count_value(m_color_spin->GetValue(), true);
|
|
}
|
|
|
|
void TextureImportDialog::on_color_spin_text_changed(wxCommandEvent& evt)
|
|
{
|
|
preview_spin_text_value(m_color_spin, m_color_slider, m_param_color_count,
|
|
1, (int)max_filament_count(), evt.GetString(),
|
|
[this]() { update_color_count_preset_buttons(); });
|
|
}
|
|
|
|
void TextureImportDialog::on_smooth_slider_changed(wxCommandEvent&)
|
|
{
|
|
set_smooth_value(m_smooth_slider->GetValue(), true);
|
|
}
|
|
|
|
void TextureImportDialog::on_smooth_spin_changed(wxCommandEvent&)
|
|
{
|
|
set_smooth_value(m_smooth_spin->GetValue(), true);
|
|
}
|
|
|
|
void TextureImportDialog::on_smooth_spin_text_changed(wxCommandEvent& evt)
|
|
{
|
|
preview_spin_text_value(m_smooth_spin, m_smooth_slider, m_param_smooth,
|
|
0, 10, evt.GetString());
|
|
}
|
|
|
|
void TextureImportDialog::on_apply_clicked(wxCommandEvent&)
|
|
{
|
|
start_computation();
|
|
}
|
|
|
|
void TextureImportDialog::on_auto_merge_toggled(wxCommandEvent&)
|
|
{
|
|
bool auto_merge_enabled = !m_auto_merge_cb || m_auto_merge_cb->GetValue();
|
|
m_auto_merge_enabled = auto_merge_enabled;
|
|
|
|
if (m_state == TextureImportState::Ready) {
|
|
const auto previous_matches = m_current_matches;
|
|
do_auto_match();
|
|
restore_current_match_order(previous_matches);
|
|
compact_used_virtual_filaments();
|
|
update_filament_color_map();
|
|
rebuild_mapping_rows();
|
|
update_drop_warning_visibility();
|
|
update_auto_mix_reset_visibility();
|
|
}
|
|
}
|
|
|
|
void TextureImportDialog::highlight_view_button(int view_index)
|
|
{
|
|
Button* btns[] = { m_btn_view_original, m_btn_view_multicolor };
|
|
|
|
// The inactive pill lies on m_tab_panel, which is preview_bg (#EEEEEE -> #4C4C55), and has to
|
|
// read as raised above that strip in both themes — so its fill steps away from the strip in
|
|
// opposite directions. gDarkColors pairs one light tone with one dark tone and cannot express
|
|
// an inversion, so the two are picked here the way filament_swatch_border_colour() does.
|
|
const bool dark_pill = is_dark();
|
|
StateColor inactive_bg(
|
|
std::pair<wxColour, int>(dark_pill ? wxColour(0x5C, 0x5C, 0x64) : wxColour("#F4F4F4"), StateColor::Pressed),
|
|
std::pair<wxColour, int>(dark_pill ? wxColour(0x66, 0x66, 0x6E) : wxColour("#F8F8F8"), StateColor::Hovered),
|
|
std::pair<wxColour, int>(dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE, StateColor::Normal));
|
|
const wxColour inactive_bd = dark_pill ? wxColour(0x54, 0x54, 0x5B) : *wxWHITE;
|
|
const wxColour inactive_text = wxColour("#6B6B6A");
|
|
|
|
for (int i = 0; i < 2; ++i) {
|
|
if (!btns[i]) continue;
|
|
if (i == view_index) {
|
|
apply_accent_button_colours(btns[i]);
|
|
} else {
|
|
btns[i]->SetBackgroundColor(inactive_bg);
|
|
btns[i]->SetBorderColor(inactive_bd);
|
|
btns[i]->SetTextColor(inactive_text);
|
|
}
|
|
btns[i]->Refresh();
|
|
}
|
|
}
|
|
|
|
void TextureImportDialog::on_skip_clicked(wxCommandEvent&)
|
|
{
|
|
m_skipped = true;
|
|
m_new_filament_colors.clear();
|
|
m_new_filament_preset_names.clear();
|
|
m_new_mixed_filaments.clear();
|
|
m_current_matches.clear();
|
|
cancel_computation();
|
|
EndModal(wxID_CANCEL);
|
|
}
|
|
|
|
bool TextureImportDialog::has_valid_result() const
|
|
{
|
|
if (m_painted.face_colors.empty() || m_current_matches.empty() || m_mapping_rows.empty())
|
|
return false;
|
|
|
|
if (m_mapping_rows.size() != m_current_matches.size())
|
|
return false;
|
|
|
|
const int filament_count = (int)std::min(m_filament_colors_rgba.size(), max_filament_count());
|
|
for (const auto& row : m_mapping_rows) {
|
|
if (row.target_filament_idx < 0 || row.target_filament_idx >= filament_count)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TextureImportDialog::is_params_dirty() const
|
|
{
|
|
if (m_applied_color_count < 0)
|
|
return false;
|
|
return m_param_color_count != m_applied_color_count
|
|
|| m_param_smooth != m_applied_smooth;
|
|
}
|
|
|
|
void TextureImportDialog::update_drop_warning_visibility()
|
|
{
|
|
if (!m_drop_warning_label) return;
|
|
// Show only when the most recent do_auto_match() ran into the filament
|
|
// cap AND we are in the Ready state. The flag is reset at every
|
|
// do_auto_match() entry, so any "clean" re-run automatically hides the
|
|
// warning even if a previous run had dropped clusters.
|
|
const bool show = (m_state == TextureImportState::Ready) && m_filaments_dropped;
|
|
if (m_drop_warning_label->IsShown() == show) return;
|
|
m_drop_warning_label->Show(show);
|
|
Layout();
|
|
}
|
|
|
|
void TextureImportDialog::update_confirm_button_state()
|
|
{
|
|
if (m_state != TextureImportState::Ready)
|
|
return;
|
|
|
|
if (!has_valid_result()) {
|
|
m_btn_ok->Enable(false);
|
|
if (m_hint_label) m_hint_label->Hide();
|
|
m_btn_ok->Refresh();
|
|
Layout();
|
|
return;
|
|
}
|
|
|
|
m_btn_ok->Enable(true);
|
|
style_confirm_button(is_params_dirty());
|
|
|
|
m_btn_ok->Refresh();
|
|
Layout();
|
|
}
|
|
|
|
// Both state updaters land here: the Confirm button reads as accent only while it would apply
|
|
// exactly what the preview shows.
|
|
void TextureImportDialog::style_confirm_button(bool dirty)
|
|
{
|
|
if (dirty) {
|
|
apply_muted_button_colours(m_btn_ok);
|
|
m_btn_ok->SetToolTip(_L("Reminder: parameters changed, click Apply to take effect"));
|
|
} else {
|
|
apply_accent_button_colours(m_btn_ok);
|
|
m_btn_ok->UnsetToolTip();
|
|
}
|
|
if (m_hint_label)
|
|
m_hint_label->Show(dirty);
|
|
}
|
|
|
|
void TextureImportDialog::on_ok_clicked(wxCommandEvent&)
|
|
{
|
|
if (m_state != TextureImportState::Ready || !has_valid_result() || is_params_dirty())
|
|
return;
|
|
|
|
m_current_matches = build_matches_from_rows();
|
|
if (m_current_matches.empty())
|
|
return;
|
|
|
|
compact_used_virtual_filaments();
|
|
|
|
EndModal(wxID_OK);
|
|
}
|
|
|
|
// ---- Result accessors ----
|
|
|
|
Slic3r::PaintedMesh TextureImportDialog::get_painted_mesh() const
|
|
{
|
|
return m_painted;
|
|
}
|
|
|
|
std::vector<Slic3r::FilamentMatch> TextureImportDialog::get_matches() const
|
|
{
|
|
if (!m_current_matches.empty())
|
|
return m_current_matches;
|
|
return build_matches_from_rows();
|
|
}
|
|
|
|
void TextureImportDialog::on_dpi_changed(const wxRect&)
|
|
{
|
|
// All control sizes below are baked into persistent properties (min size,
|
|
// corner radius, fixed wxSize) using FromDIP() at build time. The base
|
|
// DPIAware::rescale() only rescales fonts; it does not recompute these
|
|
// stored pixel values. Re-apply them here so the layout stays consistent
|
|
// when the dialog is dragged to a screen with a different DPI.
|
|
SetMinSize(wxSize(FromDIP(800), FromDIP(500)));
|
|
|
|
const int view_button_height = FromDIP(27);
|
|
for (Button* btn : {m_btn_view_original, m_btn_view_multicolor}) {
|
|
if (btn) {
|
|
btn->SetCornerRadius(view_button_height / 2);
|
|
btn->SetMinSize(wxSize(FromDIP(57), view_button_height));
|
|
}
|
|
}
|
|
|
|
for (Button* btn : {m_btn_color_4, m_btn_color_8, m_btn_color_16}) {
|
|
if (btn) {
|
|
btn->SetCornerRadius(FromDIP(12));
|
|
btn->SetMinSize(wxSize(FromDIP(28), FromDIP(28)));
|
|
}
|
|
}
|
|
|
|
if (m_btn_color_auto) {
|
|
m_btn_color_auto->SetCornerRadius(FromDIP(12));
|
|
m_btn_color_auto->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
}
|
|
if (m_btn_apply) {
|
|
m_btn_apply->SetCornerRadius(FromDIP(12));
|
|
m_btn_apply->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
}
|
|
if (m_btn_auto_mix) {
|
|
m_btn_auto_mix->SetCornerRadius(FromDIP(14));
|
|
m_btn_auto_mix->SetMinSize(wxSize(FromDIP(178), FromDIP(28)));
|
|
}
|
|
if (m_btn_mix_reset)
|
|
m_btn_mix_reset->SetPaddingSize(wxSize(FromDIP(2), FromDIP(2)));
|
|
|
|
if (m_color_spin)
|
|
m_color_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
if (m_smooth_spin)
|
|
m_smooth_spin->SetMinSize(wxSize(FromDIP(60), FromDIP(28)));
|
|
|
|
if (m_mapping_scroll) {
|
|
m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300)));
|
|
m_mapping_scroll->SetScrollRate(0, FromDIP(20));
|
|
}
|
|
|
|
if (m_btn_skip) {
|
|
m_btn_skip->SetCornerRadius(FromDIP(20));
|
|
m_btn_skip->SetMinSize(wxSize(FromDIP(136), FromDIP(40)));
|
|
}
|
|
if (m_btn_ok) {
|
|
m_btn_ok->SetCornerRadius(FromDIP(20));
|
|
m_btn_ok->SetMinSize(wxSize(FromDIP(156), FromDIP(40)));
|
|
}
|
|
|
|
// Mapping rows store their panel sizes (source/target/arrow/row height)
|
|
// as fixed FromDIP min/max sizes, so rebuild them to pick up the new DPI.
|
|
rebuild_mapping_rows();
|
|
|
|
if (wxSizer* sizer = GetSizer())
|
|
sizer->Layout();
|
|
Layout();
|
|
Refresh();
|
|
wxGetApp().UpdateDlgDarkUI(this);
|
|
}
|
|
|
|
}} // namespace Slic3r::GUI
|