From 494835c5bab0f3effa3e40f9f6edaa345b05d01b Mon Sep 17 00:00:00 2001 From: Rad Date: Tue, 10 Feb 2026 19:40:22 +0100 Subject: [PATCH] Implement advanced mixed filament features: Add gradient settings for mixed filaments, including height-weighted cadence and custom filament definitions. Enhance MixedFilamentManager to support custom rows and apply gradient settings during filament generation. Update PrintConfig and GUI to accommodate new configuration options for mixed filament management. --- CHANGELOG_v0.3.md | 41 ++ src/libslic3r/GCode/ToolOrdering.cpp | 52 ++- src/libslic3r/GCode/ToolOrdering.hpp | 7 +- src/libslic3r/MixedFilament.cpp | 328 ++++++++++++++- src/libslic3r/MixedFilament.hpp | 43 +- src/libslic3r/PresetBundle.cpp | 66 +++ src/libslic3r/Print.cpp | 6 + src/libslic3r/PrintApply.cpp | 60 ++- src/libslic3r/PrintConfig.cpp | 71 +++- src/libslic3r/PrintConfig.hpp | 6 + src/libslic3r/PrintObject.cpp | 316 +++++++++++++- src/slic3r/GUI/OptionsGroup.cpp | 303 +++++++++++++- src/slic3r/GUI/Plater.cpp | 603 +++++++++++++++++++++++---- src/slic3r/GUI/Tab.cpp | 31 +- 14 files changed, 1796 insertions(+), 137 deletions(-) create mode 100644 CHANGELOG_v0.3.md diff --git a/CHANGELOG_v0.3.md b/CHANGELOG_v0.3.md new file mode 100644 index 0000000000..e7aab8f07d --- /dev/null +++ b/CHANGELOG_v0.3.md @@ -0,0 +1,41 @@ +## ⚠️ EXPERIMENTAL BUILD - NOT TESTED ⚠️ + +Based on Snapmaker Orca v2.2.4 + +This is an alpha release of Full Spectrum with mixed-color filament support. + +**THIS HAS NOT BEEN TESTED ON ACTUAL HARDWARE!** + +### What's New +- **Mixed Filaments UI**: Renamed the feature from Dithering to Mixed Filaments and improved row labeling. +- **Auto + Custom Mixed Filaments**: Auto-generated mixed rows from physical filaments plus manual custom rows via `+`. +- **Gradient Selector**: Added visual gradient picking for custom mixed rows to control A/B blend ratio. +- **Cadence Controls**: Added Layer cycle cadence, Height-weighted cadence toggle, and Advanced dithering mode. +- **Mixed Height Bounds**: Added lower/upper mixed filament height bounds and cycle controls in Process -> Others. +- **Z Step Controls**: Added dithering Z step size and painted-zone behavior options. + +### Changelog +- Added mixed filament dependency cleanup when a physical filament is removed. +- Added Process -> Others controls for mixed filament cadence and height behavior. +- Fixed multiple startup/UI crashes related to mixed filament rows and Others-tab interactions. +- Fixed gradient selector state handling and crash on color change. +- Improved sidebar behavior with compact mixed-row spacing and scroll handling for many mixed rows. +- Improved mixed preview colors with weighted RYB pigment-style blending and normalization. +- Added invalidation propagation so mixed-setting changes correctly require re-slicing. +- Updated mixed-setting tooltips with experimental disclaimers and a wiki-availability note. + +### Installation +1. Download `Snapmaker_Orca.exe` (or the full package) +2. Extract to a folder +3. Run the executable + +### ⚠️ Warning +- Use at your own risk +- May produce incorrect G-code +- Seeking testers with U1 printers + +### Known Issues +- Untested on real hardware +- Mixed filament behavior may require per-material calibration +- Advanced dithering is highly experimental and may not match normal dithering color output +- Height-weighted cadence operates at the layer plane level, not independent per-color subregions in the same XY plane diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 889c3258fe..5dd4f1c578 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -31,6 +31,8 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_ size_t num_physical, unsigned int filament_id_1based, int layer_index, + float layer_print_z, + float layer_height, float layer_height_a, float layer_height_b, float base_layer_height) @@ -38,15 +40,17 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_ if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical))) return filament_id_1based; - if (layer_height_a > 0.f || layer_height_b > 0.f) { + const size_t idx = static_cast(filament_id_1based - num_physical - 1); + const auto &mixed = mixed_mgr->mixed_filaments(); + const bool is_custom_mixed = idx < mixed.size() && mixed[idx].custom; + + if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) { const float safe_base = std::max(0.01f, base_layer_height); const int ratio_a = std::max(1, int(std::lround((layer_height_a > 0.f ? layer_height_a : safe_base) / safe_base))); const int ratio_b = std::max(1, int(std::lround((layer_height_b > 0.f ? layer_height_b : safe_base) / safe_base))); const int cycle = ratio_a + ratio_b; if (cycle > 0) { - const size_t idx = static_cast(filament_id_1based - num_physical - 1); - const auto &mixed = mixed_mgr->mixed_filaments(); if (idx < mixed.size()) { const int pos = ((layer_index % cycle) + cycle) % cycle; return pos < ratio_a ? mixed[idx].component_a : mixed[idx].component_b; @@ -54,7 +58,7 @@ unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_ } } - return mixed_mgr->resolve(filament_id_1based, num_physical, layer_index); + return mixed_mgr->resolve(filament_id_1based, num_physical, layer_index, layer_print_z, layer_height); } } // namespace @@ -188,6 +192,8 @@ unsigned int LayerTools::resolve_mixed_1based(unsigned int filament_id) const num_physical, filament_id, this->layer_index, + float(this->print_z), + float(this->layer_height), mixed_layer_height_a, mixed_layer_height_b, mixed_base_layer_height); @@ -571,11 +577,18 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto // Collect the support extruders. for (auto support_layer : object.support_layers()) { LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z); + layer_tools.layer_height = support_layer->height; ExtrusionRole role = support_layer->support_fills.role(); bool has_support = role == erMixed || role == erSupportMaterial || role == erSupportTransition; bool has_interface = role == erMixed || role == erSupportMaterialInterface; - unsigned int extruder_support = resolve_mixed(object.config().support_filament.value, layer_tools.layer_index); - unsigned int extruder_interface = resolve_mixed(object.config().support_interface_filament.value, layer_tools.layer_index); + unsigned int extruder_support = resolve_mixed(object.config().support_filament.value, + layer_tools.layer_index, + float(support_layer->print_z), + float(support_layer->height)); + unsigned int extruder_interface = resolve_mixed(object.config().support_interface_filament.value, + layer_tools.layer_index, + float(support_layer->print_z), + float(support_layer->height)); if (has_support) layer_tools.extruders.push_back(extruder_support); if (has_interface) @@ -600,7 +613,8 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto for (auto layer : object.layers()) { LayerTools &layer_tools = this->tools_for_layer(layer->print_z); // Store the sequential layer index and mixed-filament context for resolution. - layer_tools.layer_index = layerCount; + layer_tools.layer_index = layerCount; + layer_tools.layer_height = layer->height; // Override extruder with the next for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override) @@ -625,7 +639,7 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto if (something_nonoverriddable){ unsigned int wall_ext = (extruder_override == 0) ? region.config().wall_filament.value : extruder_override; - wall_ext = resolve_mixed(wall_ext, layerCount); + wall_ext = resolve_mixed(wall_ext, layerCount, float(layer->print_z), float(layer->height)); layer_tools.extruders.emplace_back(wall_ext); if (layerCount == 0) { firstLayerExtruders.emplace_back(wall_ext); @@ -656,11 +670,20 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto if (something_nonoverriddable || !m_print_config_ptr) { if (extruder_override == 0) { if (has_solid_infill) - layer_tools.extruders.emplace_back(resolve_mixed(region.config().solid_infill_filament, layerCount)); + layer_tools.extruders.emplace_back(resolve_mixed(region.config().solid_infill_filament, + layerCount, + float(layer->print_z), + float(layer->height))); if (has_infill) - layer_tools.extruders.emplace_back(resolve_mixed(region.config().sparse_infill_filament, layerCount)); + layer_tools.extruders.emplace_back(resolve_mixed(region.config().sparse_infill_filament, + layerCount, + float(layer->print_z), + float(layer->height))); } else if (has_solid_infill || has_infill) - layer_tools.extruders.emplace_back(resolve_mixed(extruder_override, layerCount)); + layer_tools.extruders.emplace_back(resolve_mixed(extruder_override, + layerCount, + float(layer->print_z), + float(layer->height))); } if (has_solid_infill || has_infill) layer_tools.has_object = true; @@ -1513,12 +1536,17 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject } // Resolve a 1-based filament ID through the mixed-filament manager. -unsigned int ToolOrdering::resolve_mixed(unsigned int filament_id_1based, int layer_index) const +unsigned int ToolOrdering::resolve_mixed(unsigned int filament_id_1based, + int layer_index, + float layer_print_z, + float layer_height) const { return resolve_mixed_with_layer_heights(m_mixed_mgr, m_num_physical, filament_id_1based, layer_index, + layer_print_z, + layer_height, m_mixed_layer_height_a, m_mixed_layer_height_b, m_mixed_base_layer_height); diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 7b457f635b..10a0772551 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -122,6 +122,8 @@ public: unsigned int extruder_override = 0; // Sequential layer index (0-based), used by mixed-filament resolution. int layer_index = 0; + // Actual layer height for this print_z where available. + coordf_t layer_height = 0.; // Should a skirt be printed at this layer? // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed. bool has_skirt = false; @@ -220,7 +222,10 @@ private: // Resolve a 1-based filament ID through the mixed-filament manager. // Returns the resolved physical extruder (1-based). If the ID is not a // mixed filament or no manager is set, returns the input unchanged. - unsigned int resolve_mixed(unsigned int filament_id_1based, int layer_index) const; + unsigned int resolve_mixed(unsigned int filament_id_1based, + int layer_index, + float layer_print_z = 0.f, + float layer_height = 0.f) const; std::vector m_layer_tools; // First printing extruder, including the multi-material priming sequence. diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 51124c9c38..d8cd912120 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Slic3r { @@ -146,6 +147,148 @@ static std::string rgb_to_hex(const RGB &c) return std::string(buf); } +static int clamp_int(int v, int lo, int hi) +{ + return std::max(lo, std::min(hi, v)); +} + +static int safe_ratio_from_height(float h, float unit) +{ + if (unit <= 1e-6f) + return 1; + return std::max(0, int(std::lround(h / unit))); +} + +static void compute_gradient_heights(const MixedFilament &mf, float lower_bound, float upper_bound, float &h_a, float &h_b) +{ + const int mix_b = clamp_int(mf.mix_b_percent, 0, 100); + const float pct_b = float(mix_b) / 100.f; + const float pct_a = 1.f - pct_b; + const float lo = std::max(0.01f, lower_bound); + const float hi = std::max(lo, upper_bound); + + h_a = lo + pct_a * (hi - lo); + h_b = lo + pct_b * (hi - lo); +} + +static void normalize_ratio_pair(int &a, int &b) +{ + a = std::max(0, a); + b = std::max(0, b); + if (a == 0 && b == 0) { + a = 1; + return; + } + if (a > 0 && b > 0) { + const int g = std::gcd(a, b); + if (g > 1) { + a /= g; + b /= g; + } + } +} + +static void compute_gradient_ratios(MixedFilament &mf, int gradient_mode, float lower_bound, float upper_bound, int cycle_layers) +{ + if (gradient_mode == 1) { + // Height-weighted mode: + // map blend to [lower, upper], then convert relative heights to an integer cadence. + float h_a = 0.f; + float h_b = 0.f; + compute_gradient_heights(mf, lower_bound, upper_bound, h_a, h_b); + // Use lower-bound as quantization unit so this mode differs clearly from layer-cycle mode. + const float unit = std::max(0.01f, std::min(h_a, h_b)); + mf.ratio_a = std::max(1, safe_ratio_from_height(h_a, unit)); + mf.ratio_b = std::max(1, safe_ratio_from_height(h_b, unit)); + } else { + // Layer-cycle mode: + // distribute an integer cycle directly by blend percentages. + const int mix_b = clamp_int(mf.mix_b_percent, 0, 100); + const float pct_b = float(mix_b) / 100.f; + const int cycle = std::max(2, cycle_layers); + mf.ratio_b = clamp_int(int(std::lround(pct_b * cycle)), 0, cycle); + mf.ratio_a = cycle - mf.ratio_b; + } + + normalize_ratio_pair(mf.ratio_a, mf.ratio_b); +} + +static int safe_mod(int x, int m) +{ + if (m <= 0) + return 0; + int r = x % m; + return (r < 0) ? (r + m) : r; +} + +static int dithering_phase_step(int cycle) +{ + if (cycle <= 1) + return 0; + int step = cycle / 2 + 1; + while (std::gcd(step, cycle) != 1) + ++step; + return step % cycle; +} + +static bool use_component_b_advanced_dither(int layer_index, int ratio_a, int ratio_b) +{ + ratio_a = std::max(0, ratio_a); + ratio_b = std::max(0, ratio_b); + + const int cycle = ratio_a + ratio_b; + if (cycle <= 0 || ratio_b <= 0) + return false; + if (ratio_a <= 0) + return true; + + // Base ordered pattern: as evenly distributed as possible for ratio_b/cycle. + const int pos = safe_mod(layer_index, cycle); + const int cycle_idx = (layer_index - pos) / cycle; + + // Rotate each cycle to avoid visible long-period vertical striping. + const int phase = safe_mod(cycle_idx * dithering_phase_step(cycle), cycle); + const int p = safe_mod(pos + phase, cycle); + + const int b_before = (p * ratio_b) / cycle; + const int b_after = ((p + 1) * ratio_b) / cycle; + return b_after > b_before; +} + +static bool parse_row_definition(const std::string &row, + unsigned int &a, + unsigned int &b, + bool &enabled, + bool &custom, + int &mix_b_percent) +{ + std::vector values; + std::stringstream ss(row); + std::string token; + while (std::getline(ss, token, ',')) { + if (token.empty()) + return false; + try { + values.push_back(std::stoi(token)); + } catch (...) { + return false; + } + } + + if (values.size() != 4 && values.size() != 5) + return false; + + if (values[0] <= 0 || values[1] <= 0) + return false; + + a = unsigned(values[0]); + b = unsigned(values[1]); + enabled = (values[2] != 0); + custom = (values.size() == 5) ? (values[3] != 0) : true; + mix_b_percent = clamp_int(values.size() == 5 ? values[4] : values[3], 0, 100); + return true; +} + // --------------------------------------------------------------------------- // MixedFilamentManager // --------------------------------------------------------------------------- @@ -153,7 +296,7 @@ static std::string rgb_to_hex(const RGB &c) void MixedFilamentManager::auto_generate(const std::vector &filament_colours) { // Keep a copy of the old list so we can preserve user-modified ratios and - // enabled flags. + // enabled flags and custom rows. std::vector old = std::move(m_mixed); m_mixed.clear(); @@ -161,6 +304,16 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen if (n < 2) return; + std::vector custom_rows; + custom_rows.reserve(old.size()); + for (const MixedFilament &prev : old) { + if (!prev.custom) + continue; + if (prev.component_a == 0 || prev.component_b == 0 || prev.component_a > n || prev.component_b > n || prev.component_a == prev.component_b) + continue; + custom_rows.push_back(prev); + } + // Generate all C(N,2) pairwise combinations. for (size_t i = 0; i < n; ++i) { for (size_t j = i + 1; j < n; ++j) { @@ -169,25 +322,27 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen mf.component_b = static_cast(j + 1); mf.ratio_a = 1; mf.ratio_b = 1; + mf.mix_b_percent = 50; mf.enabled = true; + mf.custom = false; // Try to preserve previous settings. for (const auto &prev : old) { - if (prev.component_a == mf.component_a && + if (!prev.custom && + prev.component_a == mf.component_a && prev.component_b == mf.component_b) { - mf.ratio_a = prev.ratio_a; - mf.ratio_b = prev.ratio_b; mf.enabled = prev.enabled; break; } } - - mf.display_color = blend_color(filament_colours[i], - filament_colours[j], - mf.ratio_a, mf.ratio_b); m_mixed.push_back(mf); } } + + for (MixedFilament &mf : custom_rows) + m_mixed.push_back(std::move(mf)); + + refresh_display_colors(filament_colours); } void MixedFilamentManager::remove_physical_filament(unsigned int deleted_filament_id) @@ -211,9 +366,127 @@ void MixedFilamentManager::remove_physical_filament(unsigned int deleted_filamen m_mixed = std::move(filtered); } +void MixedFilamentManager::add_custom_filament(unsigned int component_a, + unsigned int component_b, + int mix_b_percent, + const std::vector &filament_colours) +{ + const size_t n = filament_colours.size(); + if (n < 2) + return; + + component_a = std::max(1, std::min(component_a, unsigned(n))); + component_b = std::max(1, std::min(component_b, unsigned(n))); + if (component_a == component_b) { + component_b = (component_a == 1) ? 2 : 1; + } + + MixedFilament mf; + mf.component_a = component_a; + mf.component_b = component_b; + mf.mix_b_percent = clamp_int(mix_b_percent, 0, 100); + mf.ratio_a = 1; + mf.ratio_b = 1; + mf.enabled = true; + mf.custom = true; + m_mixed.push_back(std::move(mf)); + refresh_display_colors(filament_colours); +} + +void MixedFilamentManager::clear_custom_entries() +{ + m_mixed.erase(std::remove_if(m_mixed.begin(), m_mixed.end(), [](const MixedFilament &mf) { return mf.custom; }), m_mixed.end()); +} + +void MixedFilamentManager::apply_gradient_settings(int gradient_mode, + float lower_bound, + float upper_bound, + int cycle_layers, + bool advanced_dithering) +{ + m_gradient_mode = (gradient_mode != 0) ? 1 : 0; + m_height_lower_bound = std::max(0.01f, lower_bound); + m_height_upper_bound = std::max(m_height_lower_bound, upper_bound); + m_cycle_layers = std::max(2, cycle_layers); + m_advanced_dithering = advanced_dithering; + + for (MixedFilament &mf : m_mixed) { + if (!mf.custom) { + mf.ratio_a = 1; + mf.ratio_b = 1; + continue; + } + compute_gradient_ratios(mf, m_gradient_mode, m_height_lower_bound, m_height_upper_bound, m_cycle_layers); + } +} + +std::string MixedFilamentManager::serialize_custom_entries() const +{ + std::ostringstream ss; + bool first = true; + for (const MixedFilament &mf : m_mixed) { + if (!first) + ss << ';'; + first = false; + ss << mf.component_a << ',' + << mf.component_b << ',' + << (mf.enabled ? 1 : 0) << ',' + << (mf.custom ? 1 : 0) << ',' + << clamp_int(mf.mix_b_percent, 0, 100); + } + return ss.str(); +} + +void MixedFilamentManager::load_custom_entries(const std::string &serialized, const std::vector &filament_colours) +{ + const size_t n = filament_colours.size(); + if (serialized.empty() || n < 2) + return; + + std::stringstream all(serialized); + std::string row; + while (std::getline(all, row, ';')) { + if (row.empty()) + continue; + unsigned int a = 0; + unsigned int b = 0; + bool enabled = true; + bool custom = true; + int mix = 50; + if (!parse_row_definition(row, a, b, enabled, custom, mix)) + continue; + if (a == 0 || b == 0 || a > n || b > n || a == b) + continue; + + if (!custom) { + auto it_auto = std::find_if(m_mixed.begin(), m_mixed.end(), [a, b](const MixedFilament &mf) { + return !mf.custom && mf.component_a == a && mf.component_b == b; + }); + if (it_auto != m_mixed.end()) { + it_auto->enabled = enabled; + it_auto->mix_b_percent = mix; + continue; + } + } + + MixedFilament mf; + mf.component_a = a; + mf.component_b = b; + mf.mix_b_percent = mix; + mf.ratio_a = 1; + mf.ratio_b = 1; + mf.enabled = enabled; + mf.custom = custom; + m_mixed.push_back(std::move(mf)); + } + refresh_display_colors(filament_colours); +} + unsigned int MixedFilamentManager::resolve(unsigned int filament_id, size_t num_physical, - int layer_index) const + int layer_index, + float layer_print_z, + float layer_height) const { if (!is_mixed(filament_id, num_physical)) return filament_id; @@ -223,10 +496,30 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, return 1; // fallback to first extruder const MixedFilament &mf = m_mixed[idx]; + + // Height-weighted cadence for custom rows uses Z-height windows rather + // than integer layer counts. + if (m_gradient_mode == 1 && mf.custom) { + float h_a = 0.f; + float h_b = 0.f; + compute_gradient_heights(mf, m_height_lower_bound, m_height_upper_bound, h_a, h_b); + const float cycle_h = std::max(0.01f, h_a + h_b); + const float z_anchor = (layer_height > 1e-6f) + ? std::max(0.f, layer_print_z - 0.5f * layer_height) + : std::max(0.f, layer_print_z); + float phase = std::fmod(z_anchor, cycle_h); + if (phase < 0.f) + phase += cycle_h; + return (phase < h_a) ? mf.component_a : mf.component_b; + } + const int cycle = mf.ratio_a + mf.ratio_b; if (cycle <= 0) return mf.component_a; + if (m_gradient_mode == 0 && m_advanced_dithering && mf.custom) + return use_component_b_advanced_dither(layer_index, mf.ratio_a, mf.ratio_b) ? mf.component_b : mf.component_a; + const int pos = ((layer_index % cycle) + cycle) % cycle; // safe modulo for negatives return (pos < mf.ratio_a) ? mf.component_a : mf.component_b; } @@ -265,6 +558,23 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a, return rgb_to_hex(to_rgb8(rgb_out)); } +void MixedFilamentManager::refresh_display_colors(const std::vector &filament_colours) +{ + for (MixedFilament &mf : m_mixed) { + if (mf.component_a == 0 || mf.component_b == 0 || + mf.component_a > filament_colours.size() || mf.component_b > filament_colours.size()) { + mf.display_color = "#26A69A"; + continue; + } + const int ratio_a = std::max(0, 100 - clamp_int(mf.mix_b_percent, 0, 100)); + const int ratio_b = clamp_int(mf.mix_b_percent, 0, 100); + mf.display_color = blend_color( + filament_colours[mf.component_a - 1], + filament_colours[mf.component_b - 1], + ratio_a, ratio_b); + } +} + size_t MixedFilamentManager::enabled_count() const { size_t count = 0; diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 3fe2e4f37a..979ac5de76 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -23,9 +23,15 @@ struct MixedFilament int ratio_a = 1; int ratio_b = 1; + // Blend percentage of component B in [0..100]. + int mix_b_percent = 50; + // Whether this mixed filament is enabled (available for assignment). bool enabled = true; + // True when this row was user-created (custom) instead of auto-generated. + bool custom = false; + // Computed display colour as "#RRGGBB". std::string display_color; @@ -35,7 +41,9 @@ struct MixedFilament component_b == rhs.component_b && ratio_a == rhs.ratio_a && ratio_b == rhs.ratio_b && - enabled == rhs.enabled; + mix_b_percent == rhs.mix_b_percent && + enabled == rhs.enabled && + custom == rhs.custom; } bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); } }; @@ -67,6 +75,24 @@ public: // Remaining component IDs are shifted down to stay aligned with physical IDs. void remove_physical_filament(unsigned int deleted_filament_id); + // Add a custom mixed filament. + void add_custom_filament(unsigned int component_a, unsigned int component_b, int mix_b_percent, const std::vector &filament_colours); + + // Remove all custom rows, keep auto-generated ones. + void clear_custom_entries(); + + // Recompute cadence ratios from gradient settings. + // gradient_mode: 0 = Layer cycle weighted, 1 = Height weighted. + void apply_gradient_settings(int gradient_mode, + float lower_bound, + float upper_bound, + int cycle_layers, + bool advanced_dithering = false); + + // Persist only custom rows. + std::string serialize_custom_entries() const; + void load_custom_entries(const std::string &serialized, const std::vector &filament_colours); + // ---- Queries -------------------------------------------------------- // True when `filament_id` (1-based) refers to a mixed filament. @@ -76,9 +102,13 @@ public: } // Resolve a mixed filament ID to a physical extruder (1-based) for the - // given `layer_index`. Returns `filament_id` unchanged when it is not a + // given layer context. Returns `filament_id` unchanged when it is not a // mixed filament. - unsigned int resolve(unsigned int filament_id, size_t num_physical, int layer_index) const; + unsigned int resolve(unsigned int filament_id, + size_t num_physical, + int layer_index, + float layer_print_z = 0.f, + float layer_height = 0.f) const; // Compute a display colour by blending in RYB pigment space. static std::string blend_color(const std::string &color_a, @@ -105,7 +135,14 @@ private: return static_cast(filament_id - num_physical - 1); } + void refresh_display_colors(const std::vector &filament_colours); + std::vector m_mixed; + int m_gradient_mode = 0; + float m_height_lower_bound = 0.04f; + float m_height_upper_bound = 0.16f; + int m_cycle_layers = 4; + bool m_advanced_dithering = false; }; } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 09301959b0..19b1349bff 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3274,8 +3274,74 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam { ConfigOptionStrings *color_opt = this->project_config.option("filament_colour"); if (color_opt) { + DynamicPrintConfig &print_cfg = this->prints.get_edited_preset().config; + auto get_mixed_int = [this, &print_cfg](const std::string &key, int fallback) { + if (print_cfg.has(key)) + return print_cfg.opt_int(key); + return this->project_config.has(key) ? this->project_config.opt_int(key) : fallback; + }; + auto get_mixed_bool = [this, &print_cfg](const std::string &key, bool fallback) { + if (const ConfigOptionBool *opt = print_cfg.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg.option(key)) + return opt->value != 0; + if (const ConfigOptionBool *opt = this->project_config.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = this->project_config.option(key)) + return opt->value != 0; + return fallback; + }; + auto get_mixed_mode = [this, &print_cfg](bool fallback) { + if (const ConfigOptionBool *opt = print_cfg.option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg.option("mixed_filament_gradient_mode")) + return opt->value != 0; + if (const ConfigOptionBool *opt = this->project_config.option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = this->project_config.option("mixed_filament_gradient_mode")) + return opt->value != 0; + return fallback; + }; + auto get_mixed_float = [this, &print_cfg](const std::string &key, float fallback) { + if (print_cfg.has(key)) + return float(print_cfg.opt_float(key)); + return this->project_config.has(key) ? float(this->project_config.opt_float(key)) : fallback; + }; + auto get_mixed_string = [this, &print_cfg](const std::string &key) { + if (print_cfg.has(key)) + return print_cfg.opt_string(key); + return this->project_config.has(key) ? this->project_config.opt_string(key) : std::string(); + }; + auto set_mixed_string = [this, &print_cfg](const std::string &key, const std::string &value) { + if (ConfigOptionString *opt = print_cfg.option(key)) + opt->value = value; + else + print_cfg.set_key_value(key, new ConfigOptionString(value)); + if (ConfigOptionString *opt = this->project_config.option(key)) + opt->value = value; + else + this->project_config.set_key_value(key, new ConfigOptionString(value)); + }; + color_opt->values.resize(num_filaments, "#26A69A"); this->mixed_filaments.auto_generate(color_opt->values); + + int gradient_mode = get_mixed_mode(false) ? 1 : 0; + float lower_bound = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); + float upper_bound = get_mixed_float("mixed_filament_height_upper_bound", 0.16f); + int cycle_layers = get_mixed_int("mixed_filament_cycle_layers", 4); + bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); + gradient_mode = std::clamp(gradient_mode, 0, 1); + lower_bound = std::max(0.01f, lower_bound); + upper_bound = std::max(lower_bound, upper_bound); + cycle_layers = std::max(2, cycle_layers); + + this->mixed_filaments.clear_custom_entries(); + this->mixed_filaments.load_custom_entries(get_mixed_string("mixed_filament_definitions"), color_opt->values); + this->mixed_filaments.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, cycle_layers, advanced_dithering); + + const std::string serialized = this->mixed_filaments.serialize_custom_entries(); + set_mixed_string("mixed_filament_definitions", serialized); } } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 01016d8ad5..18fc13d378 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -247,6 +247,12 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "precise_z_height" || opt_key == "dithering_z_step_size" || opt_key == "dithering_step_painted_zones_only" + || opt_key == "mixed_filament_gradient_mode" + || opt_key == "mixed_filament_height_lower_bound" + || opt_key == "mixed_filament_height_upper_bound" + || opt_key == "mixed_filament_cycle_layers" + || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_definitions" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index fa619c8a09..e1dc8257d6 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1095,10 +1095,28 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // Ensure newly introduced dithering keys are present so in-session updates are detected. new_full_config.option("dithering_z_step_size", true); new_full_config.option("dithering_step_painted_zones_only", true); + new_full_config.option("mixed_filament_gradient_mode", true); + new_full_config.option("mixed_filament_height_lower_bound", true); + new_full_config.option("mixed_filament_height_upper_bound", true); + new_full_config.option("mixed_filament_cycle_layers", true); + new_full_config.option("mixed_filament_advanced_dithering", true); + new_full_config.option("mixed_filament_definitions", true); m_config.option("dithering_z_step_size", true); m_config.option("dithering_step_painted_zones_only", true); + m_config.option("mixed_filament_gradient_mode", true); + m_config.option("mixed_filament_height_lower_bound", true); + m_config.option("mixed_filament_height_upper_bound", true); + m_config.option("mixed_filament_cycle_layers", true); + m_config.option("mixed_filament_advanced_dithering", true); + m_config.option("mixed_filament_definitions", true); m_default_object_config.option("dithering_z_step_size", true); m_default_object_config.option("dithering_step_painted_zones_only", true); + m_default_object_config.option("mixed_filament_gradient_mode", true); + m_default_object_config.option("mixed_filament_height_lower_bound", true); + m_default_object_config.option("mixed_filament_height_upper_bound", true); + m_default_object_config.option("mixed_filament_cycle_layers", true); + m_default_object_config.option("mixed_filament_advanced_dithering", true); + m_default_object_config.option("mixed_filament_definitions", true); // BBS int used_filaments = this->extruders(true).size(); @@ -1196,10 +1214,50 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } } - // Regenerate mixed (virtual) filaments from physical filament colours only. + int mixed_gradient_mode = 0; + float mixed_height_lower = 0.04f; + float mixed_height_upper = 0.16f; + int mixed_cycle_layers = 4; + bool mixed_advanced_dither = false; + std::string mixed_custom_definitions; + if (new_full_config.has("mixed_filament_gradient_mode")) { + if (const ConfigOptionBool *opt = new_full_config.option("mixed_filament_gradient_mode")) + mixed_gradient_mode = opt->value ? 1 : 0; + else + mixed_gradient_mode = new_full_config.opt_int("mixed_filament_gradient_mode"); + } + if (new_full_config.has("mixed_filament_height_lower_bound")) + mixed_height_lower = float(new_full_config.opt_float("mixed_filament_height_lower_bound")); + if (new_full_config.has("mixed_filament_height_upper_bound")) + mixed_height_upper = float(new_full_config.opt_float("mixed_filament_height_upper_bound")); + if (new_full_config.has("mixed_filament_cycle_layers")) + mixed_cycle_layers = new_full_config.opt_int("mixed_filament_cycle_layers"); + if (new_full_config.has("mixed_filament_advanced_dithering")) { + if (const ConfigOptionBool *opt = new_full_config.option("mixed_filament_advanced_dithering")) + mixed_advanced_dither = opt->value; + else + mixed_advanced_dither = (new_full_config.opt_int("mixed_filament_advanced_dithering") != 0); + } + if (new_full_config.has("mixed_filament_definitions")) + mixed_custom_definitions = new_full_config.opt_string("mixed_filament_definitions"); + + mixed_gradient_mode = std::clamp(mixed_gradient_mode, 0, 1); + mixed_height_lower = std::max(0.01f, mixed_height_lower); + mixed_height_upper = std::max(mixed_height_lower, mixed_height_upper); + mixed_cycle_layers = std::max(2, mixed_cycle_layers); + + // Regenerate mixed (virtual) filaments from physical filament colours and + // re-apply user custom mixed definitions. std::vector physical_filament_colors = m_config.filament_colour.values; physical_filament_colors.resize(num_extruders, "#26A69A"); + m_mixed_filament_mgr.clear_custom_entries(); m_mixed_filament_mgr.auto_generate(physical_filament_colors); + m_mixed_filament_mgr.load_custom_entries(mixed_custom_definitions, physical_filament_colors); + m_mixed_filament_mgr.apply_gradient_settings(mixed_gradient_mode, + mixed_height_lower, + mixed_height_upper, + mixed_cycle_layers, + mixed_advanced_dither); // Total filaments = physical extruders + enabled mixed (virtual) filaments. // Used for extruder ID clamping so that virtual IDs are accepted. size_t num_total_filaments = m_mixed_filament_mgr.total_filaments(num_extruders); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 9dd9c750b0..9918194379 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4125,7 +4125,8 @@ void PrintConfigDef::init_fff_params() def->label = L("Dithering cadence height A"); def->category = L("Others"); def->tooltip = L("Layer height contribution of component A for dithering virtual filaments. " - "Set to 0 to use normal 1-layer A / 1-layer B alternation."); + "Set to 0 to use normal 1-layer A / 1-layer B alternation.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); def->sidetext = "mm"; def->min = 0.; def->mode = comAdvanced; @@ -4135,17 +4136,78 @@ void PrintConfigDef::init_fff_params() def->label = L("Dithering cadence height B"); def->category = L("Others"); def->tooltip = L("Layer height contribution of component B for dithering virtual filaments. " - "Set to 0 to use normal 1-layer A / 1-layer B alternation."); + "Set to 0 to use normal 1-layer A / 1-layer B alternation.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); def->sidetext = "mm"; def->min = 0.; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); + def = this->add("mixed_filament_gradient_mode", coBool); + def->label = L("Height-weighted cadence"); + def->category = L("Others"); + def->tooltip = L("Enable height-weighted cadence for mixed filaments. " + "Limitation: only one height-weighted mixed color should be present at a given Z plane, " + "because independent per-color layer heights are not supported and the resulting layer height applies to the whole plane. " + "When disabled, layer-cycle cadence is used.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("mixed_filament_height_lower_bound", coFloat); + def->label = L("Mixed filament lower height bound"); + def->category = L("Others"); + def->tooltip = L("Lower bound used by the height-weighted mixed filament gradient mode.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->sidetext = "mm"; + def->min = 0.01; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.04)); + + def = this->add("mixed_filament_height_upper_bound", coFloat); + def->label = L("Mixed filament upper height bound"); + def->category = L("Others"); + def->tooltip = L("Upper bound used by the height-weighted mixed filament gradient mode.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->sidetext = "mm"; + def->min = 0.01; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.16)); + + def = this->add("mixed_filament_cycle_layers", coInt); + def->label = L("Mixed filament layer cycle"); + def->category = L("Others"); + def->tooltip = L("Number of layers in one alternation cycle for layer-cycle mixed filament mode.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->min = 2; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionInt(4)); + + def = this->add("mixed_filament_advanced_dithering", coBool); + def->label = L("Advanced dithering"); + def->category = L("Others"); + def->tooltip = L("Distribute mixed filament layer-cycle cadence using an advanced ordered dithering pattern " + "instead of a simple contiguous A-then-B run. This can reduce visible striping for some hues.\n\n" + "This is an even more experimental mode and the perceived color may differ from normal dithering " + "for the same filament pair and ratio.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("mixed_filament_definitions", coString); + def->label = L("Mixed filament custom definitions"); + def->tooltip = L("Serialized custom mixed filament rows.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); + def->gui_flags = "serialized"; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString("")); + def = this->add("dithering_z_step_size", coFloat); def->label = L("Dithering Z step size"); def->category = L("Others"); def->tooltip = L("Layer height used in Z zones painted with dithering (mixed virtual filaments). " - "Set to 0 to keep normal layer height in those zones."); + "Set to 0 to keep normal layer height in those zones.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); def->sidetext = "mm"; def->min = 0.; def->mode = comAdvanced; @@ -4155,7 +4217,8 @@ void PrintConfigDef::init_fff_params() def->label = L("Use step size in painted zones only"); def->category = L("Others"); def->tooltip = L("When enabled, dithering Z step size is applied only where mixed filament is painted. " - "Unpainted zones keep their original layer height."); + "Unpainted zones keep their original layer height.\n\n" + "Detailed mixed filament setting explanations will be published once the project wiki is available."); def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(true)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 7fb49aa982..d59f8329dd 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1353,6 +1353,12 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionStrings, post_process)) ((ConfigOptionFloat, mixed_color_layer_height_a)) ((ConfigOptionFloat, mixed_color_layer_height_b)) + ((ConfigOptionBool, mixed_filament_gradient_mode)) + ((ConfigOptionFloat, mixed_filament_height_lower_bound)) + ((ConfigOptionFloat, mixed_filament_height_upper_bound)) + ((ConfigOptionInt, mixed_filament_cycle_layers)) + ((ConfigOptionBool, mixed_filament_advanced_dithering)) + ((ConfigOptionString, mixed_filament_definitions)) ((ConfigOptionFloat, dithering_z_step_size)) ((ConfigOptionBool, dithering_step_painted_zones_only)) ((ConfigOptionString, printer_model)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 308f1b5545..f83b699598 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -964,6 +964,17 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "interlocking_depth" || opt_key == "interlocking_boundary_avoidance" || opt_key == "interlocking_beam_width") { + steps.emplace_back(posSlice); + } else if ( + opt_key == "mixed_filament_gradient_mode" + || opt_key == "mixed_filament_height_lower_bound" + || opt_key == "mixed_filament_height_upper_bound" + || opt_key == "mixed_filament_cycle_layers" + || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_definitions") { + // Mixed filament gradient controls affect layer cadence and virtual + // tool distribution, so force a re-slice prompt like other + // layer-structure settings. steps.emplace_back(posSlice); } else if ( opt_key == "elefant_foot_compensation" @@ -3341,6 +3352,18 @@ struct LayerHeightRangeOverride { coordf_t height { 0.f }; }; +struct MixedStateZRanges { + size_t state_id { 0 }; + std::vector ranges; +}; + +struct MixedStateCadence { + size_t state_id { 0 }; + coordf_t height_a { 0.f }; + coordf_t height_b { 0.f }; + std::vector ranges; +}; + static void sort_and_merge_layer_ranges(std::vector &ranges) { if (ranges.empty()) @@ -3368,11 +3391,8 @@ static void sort_and_merge_layer_ranges(std::vector &range static std::vector collect_mixed_painted_z_ranges(const PrintObject &print_object, coordf_t object_height) { std::vector mixed_ranges; - if (object_height <= EPSILON) - return mixed_ranges; - const Print *print = print_object.print(); - if (print == nullptr) + if (object_height <= EPSILON || print == nullptr) return mixed_ranges; const size_t num_physical = print->config().filament_colour.size(); @@ -3381,6 +3401,7 @@ static std::vector collect_mixed_painted_z_ranges(const Pr return mixed_ranges; const size_t max_state = std::min(num_total, size_t(EnforcerBlockerType::ExtruderMax)); + std::vector> per_state(max_state + 1); const Transform3d object_to_print = print_object.trafo_centered(); for (const ModelVolume *mv : print_object.model_object()->volumes) { @@ -3401,6 +3422,7 @@ static std::vector collect_mixed_painted_z_ranges(const Pr if (facets.indices.empty() || facets.vertices.empty()) continue; + auto &state_ranges = per_state[state_idx]; for (const auto &face : facets.indices) { double tri_z_min = DBL_MAX; double tri_z_max = -DBL_MAX; @@ -3424,15 +3446,98 @@ static std::vector collect_mixed_painted_z_ranges(const Pr hi = std::min(object_height, center + thin_band * 0.5f); } if (lo + EPSILON < hi) - mixed_ranges.emplace_back(lo, hi); + state_ranges.emplace_back(lo, hi); } } } + for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) { + auto &state_ranges = per_state[state_idx]; + if (state_ranges.empty()) + continue; + sort_and_merge_layer_ranges(state_ranges); + mixed_ranges.insert(mixed_ranges.end(), state_ranges.begin(), state_ranges.end()); + } + sort_and_merge_layer_ranges(mixed_ranges); return mixed_ranges; } +static std::vector collect_mixed_painted_z_ranges_by_state(const PrintObject &print_object, coordf_t object_height) +{ + std::vector out; + const Print *print = print_object.print(); + if (object_height <= EPSILON || print == nullptr) + return out; + + const size_t num_physical = print->config().filament_colour.size(); + const size_t num_total = print->mixed_filament_manager().total_filaments(num_physical); + if (num_total <= num_physical) + return out; + + const size_t max_state = std::min(num_total, size_t(EnforcerBlockerType::ExtruderMax)); + std::vector> per_state(max_state + 1); + const Transform3d object_to_print = print_object.trafo_centered(); + + for (const ModelVolume *mv : print_object.model_object()->volumes) { + if (mv == nullptr || !mv->is_model_part() || mv->mmu_segmentation_facets.empty()) + continue; + + const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states; + if (used_states.empty()) + continue; + + const Transform3d volume_to_print = object_to_print * mv->get_matrix(); + constexpr coordf_t thin_band = 0.01f; + for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) { + if (state_idx >= used_states.size() || !used_states[state_idx]) + continue; + + const auto facets = mv->mmu_segmentation_facets.get_facets_strict(*mv, static_cast(state_idx)); + if (facets.indices.empty() || facets.vertices.empty()) + continue; + + auto &state_ranges = per_state[state_idx]; + for (const auto &face : facets.indices) { + double tri_z_min = DBL_MAX; + double tri_z_max = -DBL_MAX; + for (int i = 0; i < 3; ++i) { + const size_t vertex_idx = size_t(face[i]); + if (vertex_idx >= facets.vertices.size()) + continue; + const Vec3d p = volume_to_print * facets.vertices[vertex_idx].cast(); + tri_z_min = std::min(tri_z_min, p.z()); + tri_z_max = std::max(tri_z_max, p.z()); + } + + if (tri_z_min == DBL_MAX || tri_z_max == -DBL_MAX) + continue; + + coordf_t lo = std::max(0.f, coordf_t(tri_z_min)); + coordf_t hi = std::min(object_height, coordf_t(tri_z_max)); + if (hi <= lo + EPSILON) { + const coordf_t center = std::max(0.f, std::min(object_height, lo)); + lo = std::max(0.f, center - thin_band * 0.5f); + hi = std::min(object_height, center + thin_band * 0.5f); + } + if (lo + EPSILON < hi) + state_ranges.emplace_back(lo, hi); + } + } + } + + out.reserve(max_state > num_physical ? max_state - num_physical : 0); + for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) { + auto &state_ranges = per_state[state_idx]; + if (state_ranges.empty()) + continue; + sort_and_merge_layer_ranges(state_ranges); + if (!state_ranges.empty()) + out.push_back({ state_idx, std::move(state_ranges) }); + } + return out; +} + static std::vector base_layer_height_overrides(const t_layer_config_ranges &ranges, coordf_t object_height) { std::vector out; @@ -3550,6 +3655,167 @@ static t_layer_config_ranges layer_ranges_with_dithering(const t_layer_config_ra return out; } +static bool mixed_state_heights(const MixedFilamentManager &mixed_mgr, + size_t num_physical, + size_t state_id, + coordf_t lower_bound, + coordf_t upper_bound, + coordf_t &height_a, + coordf_t &height_b) +{ + if (state_id <= num_physical) + return false; + + const size_t idx = state_id - num_physical - 1; + const auto &mixed = mixed_mgr.mixed_filaments(); + if (idx >= mixed.size()) + return false; + + const int mix_b = std::clamp(mixed[idx].mix_b_percent, 0, 100); + const coordf_t pct_b = coordf_t(mix_b) / coordf_t(100.f); + const coordf_t pct_a = coordf_t(1.f) - pct_b; + const coordf_t lo = std::max(0.01f, lower_bound); + const coordf_t hi = std::max(lo, upper_bound); + + height_a = std::max(0.01f, lo + pct_a * (hi - lo)); + height_b = std::max(0.01f, lo + pct_b * (hi - lo)); + return true; +} + +static coordf_t mixed_state_height_at_z(const MixedStateCadence &state, coordf_t z) +{ + const coordf_t cycle = std::max(0.01f, state.height_a + state.height_b); + coordf_t phase = std::fmod(std::max(0.f, z), cycle); + if (phase < 0.f) + phase += cycle; + return (phase < state.height_a) ? state.height_a : state.height_b; +} + +static void append_state_cadence_boundaries(std::vector &boundaries, + const t_layer_height_range &range, + coordf_t height_a, + coordf_t height_b) +{ + const coordf_t cycle = height_a + height_b; + if (cycle <= EPSILON || range.second <= range.first + EPSILON) + return; + + const int k_start = int(std::floor(range.first / cycle)) - 1; + coordf_t boundary = coordf_t(k_start) * cycle; + size_t guard = 0; + while (boundary <= range.second + cycle + EPSILON && guard++ < 200000) { + if (boundary > range.first + EPSILON && boundary < range.second - EPSILON) + boundaries.emplace_back(boundary); + const coordf_t split = boundary + height_a; + if (split > range.first + EPSILON && split < range.second - EPSILON) + boundaries.emplace_back(split); + boundary += cycle; + } +} + +static t_layer_config_ranges layer_ranges_with_height_weighted_mixed( + const t_layer_config_ranges &base_ranges_map, + coordf_t object_height, + coordf_t default_layer_height, + const std::vector &mixed_state_ranges, + const MixedFilamentManager &mixed_mgr, + size_t num_physical, + coordf_t lower_bound, + coordf_t upper_bound) +{ + if (object_height <= EPSILON || mixed_state_ranges.empty()) + return base_ranges_map; + + const std::vector base_ranges = base_layer_height_overrides(base_ranges_map, object_height); + + std::vector states; + states.reserve(mixed_state_ranges.size()); + for (const MixedStateZRanges &state_ranges : mixed_state_ranges) { + coordf_t height_a = 0.f; + coordf_t height_b = 0.f; + if (!mixed_state_heights(mixed_mgr, num_physical, state_ranges.state_id, lower_bound, upper_bound, height_a, height_b)) + continue; + states.push_back({ state_ranges.state_id, height_a, height_b, state_ranges.ranges }); + } + if (states.empty()) + return base_ranges_map; + + std::vector boundaries; + boundaries.reserve(2 + base_ranges.size() * 2 + states.size() * 64); + boundaries.emplace_back(0.f); + boundaries.emplace_back(object_height); + for (const LayerHeightRangeOverride &range : base_ranges) { + boundaries.emplace_back(range.lo); + boundaries.emplace_back(range.hi); + } + for (const MixedStateCadence &state : states) { + for (const t_layer_height_range &range : state.ranges) { + boundaries.emplace_back(range.first); + boundaries.emplace_back(range.second); + append_state_cadence_boundaries(boundaries, range, state.height_a, state.height_b); + } + } + + std::sort(boundaries.begin(), boundaries.end()); + boundaries.erase(std::unique(boundaries.begin(), boundaries.end(), [](coordf_t a, coordf_t b) { + return std::abs(a - b) <= EPSILON; + }), + boundaries.end()); + + std::vector merged_ranges; + merged_ranges.reserve(boundaries.size()); + for (size_t i = 1; i < boundaries.size(); ++i) { + const coordf_t lo = boundaries[i - 1]; + const coordf_t hi = boundaries[i]; + if (hi <= lo + EPSILON) + continue; + + const coordf_t z_mid = 0.5f * (lo + hi); + coordf_t target_height = default_layer_height; + coordf_t base_height = 0.f; + const bool has_base_override = get_override_height(base_ranges, z_mid, base_height); + if (has_base_override) + target_height = base_height; + + bool has_mixed = false; + coordf_t mixed_height = target_height; + for (const MixedStateCadence &state : states) { + if (!contains_z(state.ranges, z_mid)) + continue; + const coordf_t state_height = mixed_state_height_at_z(state, z_mid); + if (!has_mixed) { + mixed_height = state_height; + has_mixed = true; + } else { + mixed_height = std::min(mixed_height, state_height); + } + } + if (has_mixed) + target_height = mixed_height; + + if (std::abs(target_height - default_layer_height) <= EPSILON) + continue; + + if (!merged_ranges.empty() && + std::abs(merged_ranges.back().height - target_height) <= EPSILON && + std::abs(merged_ranges.back().hi - lo) <= EPSILON) { + merged_ranges.back().hi = hi; + } else { + merged_ranges.push_back({ lo, hi, target_height }); + } + } + + t_layer_config_ranges out; + for (const LayerHeightRangeOverride &range : merged_ranges) { + if (range.hi <= range.lo + EPSILON) + continue; + ModelConfig cfg; + cfg.set_key_value("layer_height", new ConfigOptionFloat(range.height)); + out.emplace(t_layer_height_range(range.lo, range.hi), std::move(cfg)); + } + return out; +} + } // namespace bool PrintObject::update_layer_height_profile(const ModelObject &model_object, @@ -3560,9 +3826,45 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_ bool updated = false; const t_layer_config_ranges *ranges_to_use = &model_object.layer_config_ranges; + t_layer_config_ranges mixed_gradient_ranges; t_layer_config_ranges dithering_ranges; if (print_object != nullptr && print_object->print() != nullptr) { const DynamicPrintConfig &full_cfg = print_object->print()->full_print_config(); + const PrintConfig &print_cfg = print_object->print()->config(); + + bool height_weighted_mode = print_cfg.mixed_filament_gradient_mode.value; + if (full_cfg.has("mixed_filament_gradient_mode")) { + if (const ConfigOptionBool *opt = full_cfg.option("mixed_filament_gradient_mode")) + height_weighted_mode = opt->value; + else if (const ConfigOptionInt *opt = full_cfg.option("mixed_filament_gradient_mode")) + height_weighted_mode = (opt->value != 0); + } + + coordf_t mixed_lower = coordf_t(print_cfg.mixed_filament_height_lower_bound.value); + coordf_t mixed_upper = coordf_t(print_cfg.mixed_filament_height_upper_bound.value); + if (full_cfg.has("mixed_filament_height_lower_bound")) + mixed_lower = coordf_t(full_cfg.opt_float("mixed_filament_height_lower_bound")); + if (full_cfg.has("mixed_filament_height_upper_bound")) + mixed_upper = coordf_t(full_cfg.opt_float("mixed_filament_height_upper_bound")); + mixed_lower = std::max(0.01f, mixed_lower); + mixed_upper = std::max(mixed_lower, mixed_upper); + + if (height_weighted_mode) { + const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height(); + const auto mixed_states = collect_mixed_painted_z_ranges_by_state(*print_object, object_height); + if (!mixed_states.empty()) { + mixed_gradient_ranges = layer_ranges_with_height_weighted_mixed(*ranges_to_use, + object_height, + slicing_parameters.layer_height, + mixed_states, + print_object->print()->mixed_filament_manager(), + print_cfg.filament_colour.size(), + mixed_lower, + mixed_upper); + ranges_to_use = &mixed_gradient_ranges; + } + } + coordf_t dithering_step = coordf_t(print_object->print()->config().dithering_z_step_size.value); bool painted_zones_only = print_object->print()->config().dithering_step_painted_zones_only.value; if (full_cfg.has("dithering_z_step_size")) @@ -3570,7 +3872,7 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_ if (full_cfg.has("dithering_step_painted_zones_only")) painted_zones_only = full_cfg.opt_bool("dithering_step_painted_zones_only"); - if (dithering_step > EPSILON) { + if (!height_weighted_mode && dithering_step > EPSILON) { const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height(); std::vector mixed_ranges; if (painted_zones_only) @@ -3579,7 +3881,7 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_ mixed_ranges.emplace_back(0.f, object_height); if (!mixed_ranges.empty()) { - dithering_ranges = layer_ranges_with_dithering(model_object.layer_config_ranges, + dithering_ranges = layer_ranges_with_dithering(*ranges_to_use, object_height, slicing_parameters.layer_height, mixed_ranges, diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index 5330ccf40d..8ef4751dd2 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -909,6 +909,9 @@ boost::any ConfigOptionsGroup::config_value(const std::string& opt_key, int opt_ if (opt_key == "bed_type") return boost::any((int)BedType::btPC); + if (m_config == nullptr || m_config->def() == nullptr) + return boost::any(); + if (deserialize) { // Want to edit a vector value(currently only multi - strings) in a single edit box. // Aggregate the strings the old way. @@ -931,9 +934,31 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config boost::any ret; wxString text_value = wxString(""); const ConfigOptionDef* opt = config.def()->get(opt_key); + if (opt == nullptr) + return ret; if (opt->nullable) { + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + switch (opt->type) + { + case coPercents: + case coFloats: + ret = _(L("N/A")); + break; + case coBools: + ret = static_cast(false); + break; + case coInts: + case coEnums: + ret = 0; + break; + default: + break; + } + return ret; + } + switch (opt->type) { case coPercents: @@ -964,6 +989,16 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config switch (opt->type) { case coFloatOrPercent:{ + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults != nullptr) { + text_value = double_to_string(defaults->value); + if (defaults->percent) + text_value += "%"; + ret = text_value; + } + break; + } const auto &value = *config.option(opt_key); text_value = double_to_string(value.value); @@ -974,7 +1009,11 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config break; } case coPercent:{ - double val = config.option(opt_key)->value; + double val = 0.0; + if (config.has(opt_key) && config.option(opt_key) != nullptr) + val = config.option(opt_key)->value; + else if (opt->default_value) + val = opt->default_value->getFloat(); ret = double_to_string(val);// += "%"; } break; @@ -1008,9 +1047,35 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config } break; case coString: - ret = from_u8(config.opt_string(opt_key)); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = from_u8(config.opt_string(opt_key)); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? from_u8(defaults->value) : wxString(""); + } else + ret = wxString(""); break; case coStrings: + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults == nullptr || defaults->values.empty()) { + ret = text_value; + break; + } + if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { + ret = defaults->values; + break; + } + if (opt->gui_flags == "serialized") { + for (const auto &el : defaults->values) + text_value += from_u8(el) + ";"; + ret = text_value; + } else { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = from_u8(defaults->values[safe_idx]); + } + break; + } if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { ret = config.option(opt_key)->values; break; @@ -1050,13 +1115,30 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config } } else { ret = false; - } - break; + } + break; case coInt: - ret = config.opt_int(opt_key); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : 0; + } else + ret = 0; break; case coInts: - ret = config.opt_int(opt_key, idx); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key, idx); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } else { + ret = 0; + } + } else + ret = 0; break; case coEnum: if (!config.has("first_layer_sequence_choice") && opt_key == "first_layer_sequence_choice") { @@ -1075,16 +1157,50 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = global_cfg.option("curr_bed_type")->getInt(); break; } - ret = config.option(opt_key)->getInt(); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.option(opt_key)->getInt(); + else + ret = opt->default_value ? opt->default_value->getInt() : 0; break; // BBS case coEnums: - ret = config.opt_int(opt_key, idx); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key, idx); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } else { + ret = 0; + } + } else + ret = 0; break; case coPoint: - ret = config.option(opt_key)->value; + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.option(opt_key)->value; + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : Vec2d::Zero(); + } else + ret = Vec2d::Zero(); break; case coPoints: + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults == nullptr || defaults->values.empty()) { + ret = std::vector(); + break; + } + if (opt_key == "printable_area" || opt_key == "bed_exclude_area") + ret = get_thumbnails_string(defaults->values); + else { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } + break; + } if (opt_key == "printable_area") ret = get_thumbnails_string(config.option(opt_key)->values); else if (opt_key == "bed_exclude_area") @@ -1106,9 +1222,31 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi boost::any ret; const ConfigOptionDef* opt = config.def()->get(opt_key); + if (opt == nullptr) + return ret; if (opt->nullable) { + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + switch (opt->type) + { + case coPercents: + case coFloats: + ret = ConfigOptionFloatsNullable::nil_value(); + break; + case coBools: + ret = static_cast(false); + break; + case coInts: + case coEnums: + ret = 0; + break; + default: + break; + } + return ret; + } + switch (opt->type) { case coPercents: @@ -1136,6 +1274,16 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi switch (opt->type) { case coFloatOrPercent:{ + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults != nullptr) { + wxString text_value = double_to_string(defaults->value); + if (defaults->percent) + text_value += "%"; + ret = into_u8(text_value); + } + break; + } const auto &value = *config.option(opt_key); wxString text_value = double_to_string(value.value); @@ -1146,24 +1294,71 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi break; } case coPercent:{ - double val = config.option(opt_key)->value; + double val = 0.0; + if (config.has(opt_key) && config.option(opt_key) != nullptr) + val = config.option(opt_key)->value; + else if (opt->default_value) + val = opt->default_value->getFloat(); ret = val; } break; case coPercents: case coFloats: case coFloat:{ - double val = opt->type == coFloats ? - config.opt_float(opt_key, idx) : - opt->type == coFloat ? config.opt_float(opt_key) : - config.option(opt_key)->get_at(idx); + double val = 0.0; + if (config.has(opt_key) && config.option(opt_key) != nullptr) { + val = opt->type == coFloats ? + config.opt_float(opt_key, idx) : + opt->type == coFloat ? config.opt_float(opt_key) : + config.option(opt_key)->get_at(idx); + } else if (opt->default_value) { + if (opt->type == coFloats) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + val = defaults->values[safe_idx]; + } + } else if (opt->type == coFloat) { + val = opt->default_value->getFloat(); + } else { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + val = defaults->values[safe_idx]; + } + } + } ret = val; } break; case coString: - ret = config.opt_string(opt_key); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_string(opt_key); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : std::string(); + } else + ret = std::string(); break; case coStrings: + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults == nullptr || defaults->values.empty()) { + ret = std::string(); + break; + } + if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { + ret = defaults->values; + break; + } + if (opt->gui_flags == "serialized") + ret = defaults->values; + else { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } + break; + } if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { ret = config.option(opt_key)->values; break; @@ -1200,21 +1395,72 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi ret = static_cast(false); break; case coInt: - ret = config.opt_int(opt_key); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : 0; + } else + ret = 0; break; case coInts: - ret = config.opt_int(opt_key, idx); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key, idx); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } else { + ret = 0; + } + } else + ret = 0; break; case coEnum: - ret = config.option(opt_key)->getInt(); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.option(opt_key)->getInt(); + else + ret = opt->default_value ? opt->default_value->getInt() : 0; break; case coEnums: - ret = config.opt_int(opt_key, idx); + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.opt_int(opt_key, idx); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } else { + ret = 0; + } + } else + ret = 0; break; case coPoint: - ret = config.option(opt_key)->value; + if (config.has(opt_key) && config.option(opt_key) != nullptr) + ret = config.option(opt_key)->value; + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : Vec2d::Zero(); + } else + ret = Vec2d::Zero(); break; case coPoints: + if (!config.has(opt_key) || config.option(opt_key) == nullptr) { + const auto *defaults = opt->default_value ? dynamic_cast(opt->default_value.get()) : nullptr; + if (defaults == nullptr || defaults->values.empty()) { + ret = std::vector(); + break; + } + if (opt_key == "printable_area" || opt_key == "bed_exclude_area") + ret = get_thumbnails_string(defaults->values); + else { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } + break; + } if (opt_key == "printable_area") ret = get_thumbnails_string(config.option(opt_key)->values); else if (opt_key == "bed_exclude_area") @@ -1263,6 +1509,8 @@ std::pair ConfigOptionsGroup::get_custom_ctrl_with_blinki void ConfigOptionsGroup::change_opt_value(const t_config_option_key& opt_key, const boost::any& value, int opt_index /*= 0*/) { + if (m_config == nullptr || m_config->def() == nullptr || m_config->def()->get(opt_key) == nullptr) + return; Slic3r::GUI::change_opt_value(const_cast(*m_config), opt_key, value, opt_index); if (m_modelconfig) m_modelconfig->touch(); @@ -1271,6 +1519,11 @@ void ConfigOptionsGroup::change_opt_value(const t_config_option_key& opt_key, co // BBS void ExtruderOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const boost::any& value) { + if (m_config == nullptr || m_config->def() == nullptr) { + OptionsGroup::on_change_OG(opt_id, value); + return; + } + if (!m_opt_map.empty()) { auto it = m_opt_map.find(opt_id); @@ -1282,8 +1535,18 @@ void ExtruderOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const auto itOption = it->second; const std::string& opt_key = itOption.first; + if (!m_config->has(opt_key)) { + this->change_opt_value(opt_key, value, std::max(0, itOption.second)); + OptionsGroup::on_change_OG(opt_id, value); + return; + } auto opt = m_config->option(opt_key); + if (opt == nullptr) { + this->change_opt_value(opt_key, value, std::max(0, itOption.second)); + OptionsGroup::on_change_OG(opt_id, value); + return; + } const ConfigOptionVectorBase* opt_vec = dynamic_cast(opt); if (opt_vec != nullptr) { for (int opt_index = 0; opt_index < opt_vec->size(); opt_index++) { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 2121b88da1..6a1e8e073b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -35,8 +35,12 @@ #include #include #include +#include +#include #include #include +#include +#include #ifdef _WIN32 #include #include @@ -950,11 +954,6 @@ Sidebar::Sidebar(Plater *parent) Choice::register_dynamic_list("wipe_tower_filament", &dynamic_filament_list); p->scrolled = new wxPanel(this); - // p->scrolled->SetScrollbars(0, 100, 1, 2); // ys_DELETE_after_testing. pixelsPerUnitY = 100 - // but this cause the bad layout of the sidebar, when all infoboxes appear. - // As a result we can see the empty block at the bottom of the sidebar - // But if we set this value to 5, layout will be better - //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); @@ -1429,14 +1428,14 @@ Sidebar::Sidebar(Plater *parent) scrolled_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND, 0); } - // --- Mixed Colours Panel --- + // --- Mixed Filaments Panel --- { p->m_panel_mixed_filaments = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); p->m_panel_mixed_filaments->SetBackgroundColour(wxColour(255, 255, 255)); p->m_sizer_mixed_filaments = new wxBoxSizer(wxVERTICAL); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); // Title - auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Dithering")); + auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Mixed Filaments")); mixed_title->SetFont(Label::Head_14); p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16)); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); @@ -2187,28 +2186,326 @@ void Sidebar::on_filaments_change(size_t num_filaments) update_mixed_filament_panel(); } +namespace { +wxColour parse_mixed_color(const std::string &value) +{ + wxColour color(value); + if (!color.IsOk()) + color = wxColour("#26A69A"); + return color; +} + +class MixedGradientSelector : public wxPanel +{ +public: + MixedGradientSelector(wxWindow *parent, const wxColour &left, const wxColour &right, int value_percent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + , m_left(left) + , m_right(right) + , m_value(std::clamp(value_percent, 0, 100)) + { + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetMinSize(wxSize(FromDIP(96), FromDIP(14))); + Bind(wxEVT_PAINT, &MixedGradientSelector::on_paint, this); + Bind(wxEVT_LEFT_DOWN, &MixedGradientSelector::on_left_down, this); + Bind(wxEVT_LEFT_UP, &MixedGradientSelector::on_left_up, this); + Bind(wxEVT_MOTION, &MixedGradientSelector::on_mouse_move, this); + Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedGradientSelector::on_capture_lost, this); + } + + ~MixedGradientSelector() override + { + if (HasCapture()) + ReleaseMouse(); + } + + int value() const { return m_value; } + + void set_colors(const wxColour &left, const wxColour &right) + { + m_left = left; + m_right = right; + Refresh(); + } + +private: + wxRect gradient_rect() const + { + const int margin_x = FromDIP(2); + const int margin_y = FromDIP(1); + const wxSize sz = GetClientSize(); + return wxRect(margin_x, margin_y, std::max(1, sz.GetWidth() - margin_x * 2), std::max(1, sz.GetHeight() - margin_y * 2)); + } + + int value_from_x(int x) const + { + const wxRect rect = gradient_rect(); + const int min_x = rect.GetLeft(); + const int max_x = rect.GetLeft() + rect.GetWidth(); + const int clamped_x = std::clamp(x, min_x, max_x); + return ((clamped_x - min_x) * 100 + rect.GetWidth() / 2) / rect.GetWidth(); + } + + void update_from_x(int x, bool notify) + { + const int new_value = value_from_x(x); + m_value = new_value; + Refresh(); + + if (notify) { + wxCommandEvent evt(wxEVT_SLIDER, GetId()); + evt.SetInt(m_value); + evt.SetEventObject(this); + ProcessWindowEvent(evt); + } + } + + void on_paint(wxPaintEvent &) + { + wxAutoBufferedPaintDC dc(this); + dc.SetBackground(wxBrush(GetBackgroundColour())); + dc.Clear(); + + const wxRect rect = gradient_rect(); + dc.GradientFillLinear(rect, m_left, m_right, wxEAST); + dc.SetPen(wxPen(wxColour(170, 170, 170), 1)); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(rect); + + int marker_x = rect.GetLeft() + (rect.GetWidth() * m_value + 50) / 100; + marker_x = std::clamp(marker_x, rect.GetLeft(), rect.GetRight()); + dc.SetPen(wxPen(wxColour(255, 255, 255), 3)); + dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom()); + dc.SetPen(wxPen(wxColour(33, 33, 33), 1)); + dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom()); + } + + void on_left_down(wxMouseEvent &evt) + { + if (!HasCapture()) + CaptureMouse(); + m_dragging = true; + update_from_x(evt.GetX(), false); + } + + void on_left_up(wxMouseEvent &evt) + { + if (m_dragging) + update_from_x(evt.GetX(), true); + m_dragging = false; + if (HasCapture()) + ReleaseMouse(); + } + + void on_mouse_move(wxMouseEvent &evt) + { + if (m_dragging && evt.LeftIsDown()) + update_from_x(evt.GetX(), false); + } + + void on_capture_lost(wxMouseCaptureLostEvent &) + { + m_dragging = false; + } + +private: + wxColour m_left; + wxColour m_right; + int m_value {50}; + bool m_dragging {false}; +}; +} // namespace + void Sidebar::update_mixed_filament_panel() { if (!p->m_panel_mixed_filaments || !p->m_sizer_mixed_filaments) return; + int prev_rows_view_y = 0; + for (wxWindow *child : p->m_panel_mixed_filaments->GetChildren()) { + if (auto *scrolled = dynamic_cast(child)) { + int tmp_x = 0; + scrolled->GetViewStart(&tmp_x, &prev_rows_view_y); + break; + } + } + auto *preset_bundle = wxGetApp().preset_bundle; if (!preset_bundle) return; + DynamicPrintConfig *print_cfg = &preset_bundle->prints.get_edited_preset().config; - const auto &mixed_mgr = preset_bundle->mixed_filaments; - const auto &mixed = mixed_mgr.mixed_filaments(); + const size_t num_physical = p->combos_filament.size(); + ConfigOptionStrings *color_opt = preset_bundle->project_config.option("filament_colour"); + std::vector physical_colors = color_opt ? color_opt->values : std::vector(); + physical_colors.resize(num_physical, "#26A69A"); + + auto get_mixed_int = [preset_bundle, print_cfg](const std::string &key, int fallback) { + if (print_cfg && print_cfg->has(key)) + return print_cfg->opt_int(key); + return preset_bundle->project_config.has(key) ? preset_bundle->project_config.opt_int(key) : fallback; + }; + auto get_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool fallback) { + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option(key)) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option(key)) + return opt->value != 0; + } + if (const ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + return opt->value != 0; + return fallback; + }; + auto get_mixed_mode = [preset_bundle, print_cfg](bool fallback) { + if (print_cfg) { + if (const ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + return opt->value != 0; + } + if (const ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value; + if (const ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + return opt->value != 0; + return fallback; + }; + auto get_mixed_float = [preset_bundle, print_cfg](const std::string &key, float fallback) { + if (print_cfg && print_cfg->has(key)) + return float(print_cfg->opt_float(key)); + return preset_bundle->project_config.has(key) ? float(preset_bundle->project_config.opt_float(key)) : fallback; + }; + auto get_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &fallback = std::string()) { + if (print_cfg && print_cfg->has(key)) + return print_cfg->opt_string(key); + return preset_bundle->project_config.has(key) ? preset_bundle->project_config.opt_string(key) : fallback; + }; + auto set_mixed_int = [preset_bundle, print_cfg](const std::string &key, int value) { + if (print_cfg) { + if (ConfigOptionInt *opt = print_cfg->option(key)) + opt->value = value; + else + print_cfg->set_key_value(key, new ConfigOptionInt(value)); + } + if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionInt(value)); + }; + auto set_mixed_float = [preset_bundle, print_cfg](const std::string &key, float value) { + if (print_cfg) { + if (ConfigOptionFloat *opt = print_cfg->option(key)) + opt->value = value; + else + print_cfg->set_key_value(key, new ConfigOptionFloat(value)); + } + if (ConfigOptionFloat *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionFloat(value)); + }; + auto set_mixed_string = [preset_bundle, print_cfg](const std::string &key, const std::string &value) { + if (print_cfg) { + if (ConfigOptionString *opt = print_cfg->option(key)) + opt->value = value; + else + print_cfg->set_key_value(key, new ConfigOptionString(value)); + } + if (ConfigOptionString *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); + }; + auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = print_cfg->option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + else + print_cfg->set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option("mixed_filament_gradient_mode")) + opt->value = enabled ? 1 : 0; + else + preset_bundle->project_config.set_key_value("mixed_filament_gradient_mode", new ConfigOptionBool(enabled)); + }; + auto notify_mixed_change = [print_cfg]() { + if (!print_cfg) + return; + if (auto *print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT)) + print_tab->update_dirty(); + if (wxGetApp().mainframe) + wxGetApp().mainframe->on_config_changed(print_cfg); + }; + + const bool height_weighted_mode = get_mixed_mode(false); + int gradient_mode = height_weighted_mode ? 1 : 0; + float lower_bound = std::max(0.01f, get_mixed_float("mixed_filament_height_lower_bound", 0.04f)); + float upper_bound = std::max(lower_bound, get_mixed_float("mixed_filament_height_upper_bound", 0.16f)); + int cycle_layers = std::max(2, get_mixed_int("mixed_filament_cycle_layers", 4)); + bool advanced_dithering = get_mixed_bool("mixed_filament_advanced_dithering", false); + const std::string mixed_definitions = get_mixed_string("mixed_filament_definitions"); + + auto &mixed_mgr = preset_bundle->mixed_filaments; + mixed_mgr.auto_generate(physical_colors); + mixed_mgr.clear_custom_entries(); + mixed_mgr.load_custom_entries(mixed_definitions, physical_colors); + mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, cycle_layers, advanced_dithering); + + set_mixed_mode(height_weighted_mode); + set_mixed_float("mixed_filament_height_lower_bound", lower_bound); + set_mixed_float("mixed_filament_height_upper_bound", upper_bound); + set_mixed_int("mixed_filament_cycle_layers", cycle_layers); + set_mixed_string("mixed_filament_definitions", mixed_mgr.serialize_custom_entries()); + + auto &mixed = mixed_mgr.mixed_filaments(); - // Clear old entries safely. Using Clear(true) avoids manual child-window - // destruction loops with deferred wxWindow::Destroy(). p->m_sizer_mixed_filaments->Clear(true); - // Recreate header. - p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); - auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Dithering")); + // Header with title and add button. + auto *header_row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY); + header_row->SetBackgroundColour(wxColour(255, 255, 255)); + auto *header_sizer = new wxBoxSizer(wxHORIZONTAL); + auto *mixed_title = new wxStaticText(header_row, wxID_ANY, _L("Mixed Filaments")); mixed_title->SetFont(Label::Head_14); - p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16)); - p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); + header_sizer->Add(mixed_title, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + auto *add_btn = new wxButton(header_row, wxID_ANY, "+", wxDefaultPosition, wxSize(FromDIP(24), FromDIP(22))); + add_btn->SetToolTip(_L("Add custom mixed filament")); + add_btn->Enable(num_physical >= 2); + add_btn->Bind(wxEVT_BUTTON, [this, preset_bundle, physical_colors, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](wxCommandEvent &) { + if (physical_colors.size() < 2) + return; + auto &mgr = preset_bundle->mixed_filaments; + mgr.add_custom_filament(1, 2, 50, physical_colors); + + int mode = get_mixed_mode(false) ? 1 : 0; + float lo = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); + float hi = get_mixed_float("mixed_filament_height_upper_bound", 0.16f); + int cycle = get_mixed_int("mixed_filament_cycle_layers", 4); + bool advanced = get_mixed_bool("mixed_filament_advanced_dithering", false); + mode = std::clamp(mode, 0, 1); + lo = std::max(0.01f, lo); + hi = std::max(lo, hi); + cycle = std::max(2, cycle); + mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); + + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + notify_mixed_change(); + + this->CallAfter([this]() { + update_dynamic_filament_list(); + update_mixed_filament_panel(); + }); + }); + header_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + header_row->SetSizer(header_sizer); + p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); + p->m_sizer_mixed_filaments->Add(header_row, 0, wxEXPAND); + p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); if (mixed.empty()) { p->m_panel_mixed_filaments->Hide(); @@ -2216,81 +2513,237 @@ void Sidebar::update_mixed_filament_panel() return; } - int mixed_id = 0; - for (const auto &mf : mixed) { - // Row panel - auto *row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY); + // Global gradient settings row. + auto *settings_row = new wxPanel(p->m_panel_mixed_filaments, wxID_ANY); + settings_row->SetBackgroundColour(wxColour(255, 255, 255)); + auto *settings_sizer = new wxBoxSizer(wxHORIZONTAL); + settings_sizer->AddSpacer(FromDIP(4)); + + settings_sizer->Add(new wxStaticText(settings_row, wxID_ANY, _L("Lower")), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *lower_spin = new wxSpinCtrlDouble(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(70), -1), wxSP_ARROW_KEYS, 0.01, 10.0, lower_bound, 0.01); + settings_sizer->Add(lower_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + + settings_sizer->Add(new wxStaticText(settings_row, wxID_ANY, _L("Upper")), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *upper_spin = new wxSpinCtrlDouble(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(70), -1), wxSP_ARROW_KEYS, 0.01, 10.0, upper_bound, 0.01); + settings_sizer->Add(upper_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(3)); + + settings_sizer->Add(new wxStaticText(settings_row, wxID_ANY, _L("Cycle")), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + auto *cycle_spin = new wxSpinCtrl(settings_row, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxSize(FromDIP(56), -1), wxSP_ARROW_KEYS, 2, 32, cycle_layers); + settings_sizer->Add(cycle_spin, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(3)); + settings_row->SetSizer(settings_sizer); + p->m_sizer_mixed_filaments->Add(settings_row, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(4)); + p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); + + auto *rows_scroller = new wxScrolledWindow(p->m_panel_mixed_filaments, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxTAB_TRAVERSAL); + rows_scroller->SetScrollRate(0, FromDIP(8)); + rows_scroller->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + rows_scroller->SetBackgroundColour(wxColour(255, 255, 255)); + auto *rows_sizer = new wxBoxSizer(wxVERTICAL); + rows_scroller->SetSizer(rows_sizer); + + auto apply_settings = [this, preset_bundle, get_mixed_mode, get_mixed_bool, lower_spin, upper_spin, cycle_spin, set_mixed_int, set_mixed_float, set_mixed_string, notify_mixed_change]() { + const int mode = get_mixed_mode(false) ? 1 : 0; + const bool advanced = get_mixed_bool("mixed_filament_advanced_dithering", false); + float lo = std::max(0.01f, float(lower_spin->GetValue())); + float hi = std::max(lo, float(upper_spin->GetValue())); + int cycle = std::max(2, cycle_spin->GetValue()); + if (std::abs(float(upper_spin->GetValue()) - hi) > 1e-6f) + upper_spin->SetValue(hi); + + set_mixed_float("mixed_filament_height_lower_bound", lo); + set_mixed_float("mixed_filament_height_upper_bound", hi); + set_mixed_int("mixed_filament_cycle_layers", cycle); + + auto &mgr = preset_bundle->mixed_filaments; + mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + notify_mixed_change(); + + this->CallAfter([this]() { + update_dynamic_filament_list(); + update_mixed_filament_panel(); + }); + }; + lower_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); + upper_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_settings](wxSpinDoubleEvent &) { apply_settings(); }); + cycle_spin->Bind(wxEVT_SPINCTRL, [apply_settings](wxSpinEvent &) { apply_settings(); }); + lower_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); + upper_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); + cycle_spin->Bind(wxEVT_TEXT, [apply_settings](wxCommandEvent &) { apply_settings(); }); + lower_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); + upper_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); + cycle_spin->Bind(wxEVT_TEXT_ENTER, [apply_settings](wxCommandEvent &) { apply_settings(); }); + lower_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); + upper_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); + cycle_spin->Bind(wxEVT_KILL_FOCUS, [apply_settings](wxFocusEvent &evt) { apply_settings(); evt.Skip(); }); + + for (size_t mixed_id = 0; mixed_id < mixed.size(); ++mixed_id) { + MixedFilament &mf = mixed[mixed_id]; + auto *row = new wxPanel(rows_scroller, wxID_ANY); row->SetBackgroundColour(wxColour(255, 255, 255)); auto *row_sizer = new wxBoxSizer(wxHORIZONTAL); - // Colour swatch + auto *content_sizer = new wxBoxSizer(wxVERTICAL); + auto *title_row = new wxBoxSizer(wxHORIZONTAL); + wxColour swatch_color(mf.display_color); - auto *swatch = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(16), FromDIP(16))); + auto *swatch = new wxPanel(row, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(14), FromDIP(14))); swatch->SetBackgroundColour(swatch_color); - swatch->SetMinSize(wxSize(FromDIP(16), FromDIP(16))); - row_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + swatch->SetMinSize(wxSize(FromDIP(14), FromDIP(14))); + title_row->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - // Label: "Filament 1 + Filament 2" - wxString label_str = wxString::Format("Filament %u + Filament %u", - (unsigned)mf.component_a, (unsigned)mf.component_b); - auto *label = new wxStaticText(row, wxID_ANY, label_str); - row_sizer->Add(label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + const int virtual_filament_id = int(num_physical + mixed_id + 1); + auto *name_label = new wxStaticText(row, wxID_ANY, wxString::Format("Mixed Filament %d", virtual_filament_id)); + title_row->Add(name_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); - // Ratio display - wxString ratio_str = wxString::Format("%d:%d", mf.ratio_a, mf.ratio_b); - auto *ratio_label = new wxStaticText(row, wxID_ANY, ratio_str); - row_sizer->Add(ratio_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + if (!mf.custom) { + auto *pair_label = new wxStaticText(row, wxID_ANY, wxString::Format("(Filament %u + Filament %u)", + unsigned(mf.component_a), unsigned(mf.component_b))); + title_row->Add(pair_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + } + content_sizer->Add(title_row, 0, wxEXPAND); - // Ratio spin controls - auto *spin_a = new wxSpinCtrl(row, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize(FromDIP(45), -1), wxSP_ARROW_KEYS, 1, 10, mf.ratio_a); - auto *spin_b = new wxSpinCtrl(row, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxSize(FromDIP(45), -1), wxSP_ARROW_KEYS, 1, 10, mf.ratio_b); - row_sizer->Add(spin_a, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); - auto *colon = new wxStaticText(row, wxID_ANY, ":"); - row_sizer->Add(colon, 0, wxALIGN_CENTER_VERTICAL); - row_sizer->Add(spin_b, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(2)); + if (mf.custom) { + wxArrayString filament_choices; + for (size_t i = 0; i < num_physical; ++i) + filament_choices.Add(wxString::Format("Filament %d", int(i + 1))); + + const int component_a = std::clamp(int(mf.component_a), 1, int(num_physical)); + const int component_b = std::clamp(int(mf.component_b), 1, int(num_physical)); + + auto *choice_a = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + auto *choice_b = new wxChoice(row, wxID_ANY, wxDefaultPosition, wxDefaultSize, filament_choices); + choice_a->SetSelection(component_a - 1); + choice_b->SetSelection(component_b - 1); + + auto *picker_row = new wxBoxSizer(wxHORIZONTAL); + picker_row->Add(choice_a, 1, wxALIGN_CENTER_VERTICAL); + picker_row->Add(new wxStaticText(row, wxID_ANY, "+"), 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6)); + picker_row->Add(choice_b, 1, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(picker_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + wxColour color_a = parse_mixed_color(physical_colors[size_t(component_a - 1)]); + wxColour color_b = parse_mixed_color(physical_colors[size_t(component_b - 1)]); + auto *blend_selector = new MixedGradientSelector(row, color_a, color_b, std::clamp(mf.mix_b_percent, 0, 100)); + auto *blend_row = new wxBoxSizer(wxHORIZONTAL); + blend_row->Add(blend_selector, 1, wxEXPAND); + content_sizer->Add(blend_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + auto *blend_label = new wxStaticText(row, wxID_ANY, wxString::Format("%d%%/%d%%", + 100 - std::clamp(mf.mix_b_percent, 0, 100), + std::clamp(mf.mix_b_percent, 0, 100))); + auto *ratio_row = new wxBoxSizer(wxHORIZONTAL); + ratio_row->AddStretchSpacer(1); + ratio_row->Add(blend_label, 0, wxALIGN_CENTER_VERTICAL); + content_sizer->Add(ratio_row, 0, wxEXPAND | wxLEFT | wxTOP, FromDIP(8)); + + auto apply_custom_row = [this, preset_bundle, mixed_id, choice_a, choice_b, blend_selector, blend_label, swatch, num_physical, get_mixed_mode, get_mixed_int, get_mixed_float, get_mixed_bool, set_mixed_string, notify_mixed_change](bool refresh_panel) { + if (num_physical < 1) + return; + + auto &mgr = preset_bundle->mixed_filaments; + auto &mfs = mgr.mixed_filaments(); + if (mixed_id >= mfs.size()) + return; + + int a = std::clamp(choice_a->GetSelection() + 1, 1, int(num_physical)); + int b = std::clamp(choice_b->GetSelection() + 1, 1, int(num_physical)); + if (a == b && num_physical > 1) { + b = (a == int(num_physical)) ? 1 : a + 1; + choice_b->SetSelection(b - 1); + } + + MixedFilament &cur = mfs[mixed_id]; + cur.component_a = unsigned(a); + cur.component_b = unsigned(b); + cur.mix_b_percent = std::clamp(blend_selector->value(), 0, 100); + cur.custom = true; + + ConfigOptionStrings *co = preset_bundle->project_config.option("filament_colour"); + std::vector colors = co ? co->values : std::vector(); + colors.resize(num_physical, "#26A69A"); + + wxColour color_a_wx = parse_mixed_color(colors[size_t(a - 1)]); + wxColour color_b_wx = parse_mixed_color(colors[size_t(b - 1)]); + blend_selector->set_colors(color_a_wx, color_b_wx); + + cur.display_color = MixedFilamentManager::blend_color(colors[size_t(a - 1)], colors[size_t(b - 1)], 100 - cur.mix_b_percent, cur.mix_b_percent); + blend_label->SetLabel(wxString::Format("%d%%/%d%%", 100 - cur.mix_b_percent, cur.mix_b_percent)); + swatch->SetBackgroundColour(wxColour(cur.display_color)); + swatch->Refresh(); + + int mode = get_mixed_mode(false) ? 1 : 0; + float lo = get_mixed_float("mixed_filament_height_lower_bound", 0.04f); + float hi = get_mixed_float("mixed_filament_height_upper_bound", 0.16f); + int cycle = get_mixed_int("mixed_filament_cycle_layers", 4); + bool advanced = get_mixed_bool("mixed_filament_advanced_dithering", false); + mode = std::clamp(mode, 0, 1); + lo = std::max(0.01f, lo); + hi = std::max(lo, hi); + cycle = std::max(2, cycle); + mgr.apply_gradient_settings(mode, lo, hi, cycle, advanced); + + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + notify_mixed_change(); + + if (refresh_panel) { + this->CallAfter([this]() { + update_dynamic_filament_list(); + update_mixed_filament_panel(); + }); + } else { + this->CallAfter([this]() { update_dynamic_filament_list(); }); + } + }; + + choice_a->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + choice_b->Bind(wxEVT_CHOICE, [apply_custom_row](wxCommandEvent &) { apply_custom_row(true); }); + blend_selector->Bind(wxEVT_SLIDER, [apply_custom_row](wxCommandEvent &) { apply_custom_row(false); }); + } + + row_sizer->Add(content_sizer, 1, wxEXPAND); - // Enable checkbox auto *chk = new wxCheckBox(row, wxID_ANY, wxEmptyString); chk->SetValue(mf.enabled); - row_sizer->Add(chk, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(8)); - - // Bind events to update the MixedFilamentManager when ratio/enabled changes. - int capturedIdx = mixed_id; - auto *capturedSpinA = spin_a; - auto *capturedSpinB = spin_b; - auto *capturedChk = chk; - - auto update_lambda = [capturedIdx, capturedSpinA, capturedSpinB, capturedChk](wxCommandEvent &) { - auto &mgr = wxGetApp().preset_bundle->mixed_filaments; + row_sizer->Add(chk, 0, wxALIGN_TOP | wxLEFT | wxRIGHT | wxTOP, FromDIP(2)); + chk->Bind(wxEVT_CHECKBOX, [this, preset_bundle, mixed_id, chk, set_mixed_string, notify_mixed_change](wxCommandEvent &) { + auto &mgr = preset_bundle->mixed_filaments; auto &mfs = mgr.mixed_filaments(); - if (capturedIdx < (int)mfs.size()) { - mfs[capturedIdx].ratio_a = capturedSpinA->GetValue(); - mfs[capturedIdx].ratio_b = capturedSpinB->GetValue(); - mfs[capturedIdx].enabled = capturedChk->GetValue(); - // Recompute display color. - ConfigOptionStrings *co = wxGetApp().preset_bundle->project_config.option("filament_colour"); - if (co && mfs[capturedIdx].component_a <= co->values.size() && mfs[capturedIdx].component_b <= co->values.size()) { - mfs[capturedIdx].display_color = Slic3r::MixedFilamentManager::blend_color( - co->values[mfs[capturedIdx].component_a - 1], - co->values[mfs[capturedIdx].component_b - 1], - mfs[capturedIdx].ratio_a, mfs[capturedIdx].ratio_b); - } - } - }; - spin_a->Bind(wxEVT_SPINCTRL, update_lambda); - spin_b->Bind(wxEVT_SPINCTRL, update_lambda); - chk->Bind(wxEVT_CHECKBOX, update_lambda); + if (mixed_id < mfs.size()) + mfs[mixed_id].enabled = chk->GetValue(); + + set_mixed_string("mixed_filament_definitions", mgr.serialize_custom_entries()); + notify_mixed_change(); + + this->CallAfter([this]() { + update_dynamic_filament_list(); + update_mixed_filament_panel(); + }); + }); row->SetSizer(row_sizer); - p->m_sizer_mixed_filaments->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8)); - p->m_sizer_mixed_filaments->AddSpacer(FromDIP(2)); - - ++mixed_id; + rows_sizer->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(4)); + rows_sizer->AddSpacer(FromDIP(1)); } - p->m_sizer_mixed_filaments->AddSpacer(FromDIP(8)); + rows_sizer->AddSpacer(FromDIP(4)); + rows_scroller->Layout(); + rows_scroller->FitInside(); + + const int min_h = FromDIP(72); + const int max_h = FromDIP(260); + const int content_h = std::max(0, rows_scroller->GetVirtualSize().GetHeight()); + const int desired_h = std::clamp(content_h, min_h, max_h); + rows_scroller->SetMinSize(wxSize(-1, desired_h)); + rows_scroller->SetMaxSize(wxSize(-1, desired_h)); + if (prev_rows_view_y > 0) + rows_scroller->Scroll(0, prev_rows_view_y); + + p->m_sizer_mixed_filaments->Add(rows_scroller, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(0)); + p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); p->m_panel_mixed_filaments->Show(); p->m_panel_mixed_filaments->Layout(); Layout(); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9a8224beb9..6625e06af3 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1397,6 +1397,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if (wxGetApp().plater() == nullptr) { return; } + if (m_config == nullptr) { + return; + } if (opt_key == "compatible_prints") this->compatible_widget_reload(m_compatible_prints); @@ -1775,6 +1778,21 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) return; } + // Keep Mixed Filaments global settings in sync with project_config. In + // full_fff_config(), project_config is applied last and would otherwise + // override the edited print preset value from the Others panel. + if (m_type == Preset::TYPE_PRINT && + (opt_key == "mixed_filament_gradient_mode" || + opt_key == "mixed_filament_height_lower_bound" || + opt_key == "mixed_filament_height_upper_bound" || + opt_key == "mixed_filament_cycle_layers" || + opt_key == "mixed_filament_advanced_dithering" || + opt_key == "mixed_filament_definitions")) { + DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; + if (const ConfigOption *opt = m_config->option(opt_key)) + project_cfg.set_key_value(opt_key, opt->clone()); + } + update(); if(m_active_page) m_active_page->update_visibility(m_mode, true); @@ -2489,11 +2507,14 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); // Use default (no icon) here to avoid runtime bitmap load failures. - optgroup = page->new_optgroup(L("Dithering")); - optgroup->append_single_option_line("mixed_color_layer_height_a", "others_settings_mixed_colors#layer-height-a"); - optgroup->append_single_option_line("mixed_color_layer_height_b", "others_settings_mixed_colors#layer-height-b"); - optgroup->append_single_option_line("dithering_z_step_size", "others_settings_mixed_colors#z-step-size"); - optgroup->append_single_option_line("dithering_step_painted_zones_only", "others_settings_mixed_colors#painted-zones-only"); + optgroup = page->new_optgroup(L("Mixed Filaments")); + optgroup->append_single_option_line("mixed_filament_gradient_mode"); + optgroup->append_single_option_line("mixed_filament_height_lower_bound"); + optgroup->append_single_option_line("mixed_filament_height_upper_bound"); + optgroup->append_single_option_line("mixed_filament_cycle_layers"); + optgroup->append_single_option_line("mixed_filament_advanced_dithering"); + optgroup->append_single_option_line("dithering_z_step_size"); + optgroup->append_single_option_line("dithering_step_painted_zones_only"); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin");