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");