From 4a5746a6e68a5394fddb596d3ee1781b0890bf25 Mon Sep 17 00:00:00 2001 From: Rad Date: Mon, 13 Apr 2026 21:00:30 +0200 Subject: [PATCH] Add mixed filament bias controls and bump version to 0.9.51 --- .github/workflows/build_all.yml | 3 + README.md | 11 + .../changelogs/CHANGELOG_v0.3.md | 0 .../changelogs/CHANGELOG_v0.7.md | 0 .../changelogs/CHANGELOG_v0.8.md | 0 .../changelogs/CHANGELOG_v0.9.4.md | 0 .../changelogs/CHANGELOG_v0.9.md | 0 .../changelogs/RELEASE_NOTES_v0.9.3.md | 0 .../changelogs/RELEASE_NOTES_v0.9.5.md | 0 .../changelogs/RELEASE_NOTES_v0.92.md | 0 src/libslic3r/Config.cpp | 10 +- src/libslic3r/GCode/GCodeProcessor.cpp | 1 + src/libslic3r/MixedFilament.cpp | 59 +++- src/libslic3r/MixedFilament.hpp | 7 + src/libslic3r/PresetBundle.cpp | 1 + src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintApply.cpp | 3 + src/libslic3r/PrintConfig.cpp | 9 + src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 2 + src/libslic3r/PrintObjectSlice.cpp | 128 ++++++- src/libslic3r/utils.cpp | 3 +- src/slic3r/GUI/GLCanvas3D.cpp | 28 +- src/slic3r/GUI/Plater.cpp | 314 ++++++++++++------ src/slic3r/GUI/Tab.cpp | 8 + tests/libslic3r/test_mixed_filament.cpp | 94 +++++- version.inc | 6 +- 27 files changed, 530 insertions(+), 159 deletions(-) rename CHANGELOG_v0.3.md => doc/changelogs/CHANGELOG_v0.3.md (100%) rename CHANGELOG_v0.7.md => doc/changelogs/CHANGELOG_v0.7.md (100%) rename CHANGELOG_v0.8.md => doc/changelogs/CHANGELOG_v0.8.md (100%) rename CHANGELOG_v0.9.4.md => doc/changelogs/CHANGELOG_v0.9.4.md (100%) rename CHANGELOG_v0.9.md => doc/changelogs/CHANGELOG_v0.9.md (100%) rename RELEASE_NOTES_v0.9.3.md => doc/changelogs/RELEASE_NOTES_v0.9.3.md (100%) rename RELEASE_NOTES_v0.9.5.md => doc/changelogs/RELEASE_NOTES_v0.9.5.md (100%) rename RELEASE_NOTES_v0.92.md => doc/changelogs/RELEASE_NOTES_v0.92.md (100%) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 08a1887f1a..1708928bba 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -11,6 +11,7 @@ on: - 'version.inc' - 'CHANGELOG*.md' - 'RELEASE_NOTES*.md' + - 'doc/changelogs/**' - 'localization/**' - 'resources/**' - ".github/workflows/build_*.yml" @@ -76,6 +77,8 @@ jobs: notes_content="" for notes_file in \ + "doc/changelogs/CHANGELOG_v${version}.md" \ + "doc/changelogs/RELEASE_NOTES_v${version}.md" \ "CHANGELOG_v${version}.md" \ "RELEASE_NOTES_v${version}.md"; do if [ -f "$notes_file" ]; then diff --git a/README.md b/README.md index 5228de59a5..8f0126007f 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Snapmaker Orca FullSpectrum includes support for **virtual mixed-color filaments - Visual preview showing the additive color blend - Enable/disable individual mixed filaments - Per-layer resolution control with customizable ratios +- Optional per-pair Bias control for slightly recessing one component to push the apparent color toward the other - Seamless integration with the existing filament management system ### Using Mixed Filaments @@ -61,6 +62,16 @@ Snapmaker Orca FullSpectrum includes support for **virtual mixed-color filaments 4. Mixed filaments can be assigned to objects just like physical filaments 5. During slicing, the mixed filament resolves to alternating layers of its components +### Bias Control +Snapmaker Orca FullSpectrum also includes an optional **Bias** control for mixed filament pairs. When enabled in **Print Settings -> Others -> Mixed Filaments**, each mixed row gets a compact inline Bias value: + +- **Positive Bias** recesses the second filament in the pair +- **Negative Bias** recesses the first filament in the pair +- This lets you shift the apparent color without changing the nominal layer cadence +- The inline preview updates to show the estimated apparent mix shift + +Example: for a pair like `F1 + F2`, a positive bias makes `F2` sit slightly lower, so `F1` visually dominates more. A negative bias does the opposite and recesses `F1`. + ### Dithering Settings Snapmaker Orca FullSpectrum includes advanced dithering controls to fine-tune the layer alternation behavior for mixed filaments. These settings are found in **Others → Dithering** in the print settings: diff --git a/CHANGELOG_v0.3.md b/doc/changelogs/CHANGELOG_v0.3.md similarity index 100% rename from CHANGELOG_v0.3.md rename to doc/changelogs/CHANGELOG_v0.3.md diff --git a/CHANGELOG_v0.7.md b/doc/changelogs/CHANGELOG_v0.7.md similarity index 100% rename from CHANGELOG_v0.7.md rename to doc/changelogs/CHANGELOG_v0.7.md diff --git a/CHANGELOG_v0.8.md b/doc/changelogs/CHANGELOG_v0.8.md similarity index 100% rename from CHANGELOG_v0.8.md rename to doc/changelogs/CHANGELOG_v0.8.md diff --git a/CHANGELOG_v0.9.4.md b/doc/changelogs/CHANGELOG_v0.9.4.md similarity index 100% rename from CHANGELOG_v0.9.4.md rename to doc/changelogs/CHANGELOG_v0.9.4.md diff --git a/CHANGELOG_v0.9.md b/doc/changelogs/CHANGELOG_v0.9.md similarity index 100% rename from CHANGELOG_v0.9.md rename to doc/changelogs/CHANGELOG_v0.9.md diff --git a/RELEASE_NOTES_v0.9.3.md b/doc/changelogs/RELEASE_NOTES_v0.9.3.md similarity index 100% rename from RELEASE_NOTES_v0.9.3.md rename to doc/changelogs/RELEASE_NOTES_v0.9.3.md diff --git a/RELEASE_NOTES_v0.9.5.md b/doc/changelogs/RELEASE_NOTES_v0.9.5.md similarity index 100% rename from RELEASE_NOTES_v0.9.5.md rename to doc/changelogs/RELEASE_NOTES_v0.9.5.md diff --git a/RELEASE_NOTES_v0.92.md b/doc/changelogs/RELEASE_NOTES_v0.92.md similarity index 100% rename from RELEASE_NOTES_v0.92.md rename to doc/changelogs/RELEASE_NOTES_v0.92.md diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 7c286264e4..48132fd4b3 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -1308,10 +1308,9 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo bool has_delimiters = true; { //BBS - std::string bambuslicer_gcode_header = "; Snapmaker_Orca"; - - std::string Snapmaker_Orca_gcode_header = std::string("; generated by "); - Snapmaker_Orca_gcode_header += SLIC3R_APP_NAME; + std::string bambuslicer_gcode_header = "; Snapmaker_Orca"; + std::string legacy_fs_gcode_header = std::string("; generated by ") + SLIC3R_APP_NAME; + std::string compat_snapmaker_gcode_header = "; generated by Snapmaker Orca"; std::string header; bool header_found = false; @@ -1323,7 +1322,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo line_c = skip_whitespaces(line_c); // BBS if (strncmp(bambuslicer_gcode_header.c_str(), line_c, strlen(bambuslicer_gcode_header.c_str())) == 0 || - strncmp(Snapmaker_Orca_gcode_header.c_str(), line_c, strlen(Snapmaker_Orca_gcode_header.c_str())) == 0) { + strncmp(legacy_fs_gcode_header.c_str(), line_c, strlen(legacy_fs_gcode_header.c_str())) == 0 || + strncmp(compat_snapmaker_gcode_header.c_str(), line_c, strlen(compat_snapmaker_gcode_header.c_str())) == 0) { header_found = true; break; } diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index f770c8579f..a91f17e750 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -619,6 +619,7 @@ const std::vector> GCodeProces //BBS: Snapmaker_Orca is also "bambu". Otherwise the time estimation didn't work. //FIXME: Workaround and should be handled when do removing-bambu { EProducer::Snapmaker_Orca, SLIC3R_APP_NAME }, + { EProducer::Snapmaker_Orca, "generated by Snapmaker Orca" }, { EProducer::Snapmaker_Orca, "generated by Snapmaker_Orca" }, { EProducer::Snapmaker_Orca, "generated by BambuStudio" }, { EProducer::Snapmaker_Orca, "BambuStudio" } diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 7ea2f7d067..34fd0b44e8 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -216,6 +216,18 @@ static float clamp_surface_offset(float v) return std::clamp(v, -2.f, 2.f); } +static float canonical_signed_bias_value(float component_a_surface_offset, float component_b_surface_offset) +{ + const float offset_a = clamp_surface_offset(component_a_surface_offset); + const float offset_b = clamp_surface_offset(component_b_surface_offset); + + if (std::abs(offset_b) > EPSILON) + return offset_b; + if (std::abs(offset_a) > EPSILON) + return (offset_a >= 0.f) ? -std::abs(offset_a) : std::abs(offset_a); + return 0.f; +} + static std::string format_surface_offset_token(float value) { std::ostringstream ss; @@ -1444,10 +1456,11 @@ float MixedFilamentManager::component_surface_offset(unsigned int filament_id, layer_print_z, layer_height, force_height_weighted); - if (resolved == mixed_row->component_a) - return clamp_surface_offset(mixed_row->component_a_surface_offset); - if (resolved == mixed_row->component_b) - return clamp_surface_offset(mixed_row->component_b_surface_offset); + const float signed_bias = canonical_signed_bias_value(mixed_row->component_a_surface_offset, mixed_row->component_b_surface_offset); + if (signed_bias > EPSILON && resolved == mixed_row->component_b) + return signed_bias; + if (signed_bias < -EPSILON && resolved == mixed_row->component_a) + return -signed_bias; return 0.f; } @@ -1591,14 +1604,48 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a, return rgb_to_hex({int(out_r), int(out_g), int(out_b)}); } +float MixedFilamentManager::max_component_surface_offset_mm(float reference_width_mm) +{ + const float safe_reference = std::max(0.05f, std::abs(reference_width_mm)); + return std::clamp(safe_reference, 0.01f, 0.35f); +} + +float MixedFilamentManager::max_pair_bias_mm(float reference_width_mm) +{ + return max_component_surface_offset_mm(reference_width_mm); +} + +std::pair MixedFilamentManager::surface_offset_pair_from_signed_bias(float bias_mm, + float reference_width_mm) +{ + const float clamped_bias = std::clamp(bias_mm, + -max_pair_bias_mm(reference_width_mm), + max_pair_bias_mm(reference_width_mm)); + if (clamped_bias > EPSILON) + return std::make_pair(0.f, clamped_bias); + if (clamped_bias < -EPSILON) + return std::make_pair(-clamped_bias, 0.f); + return std::make_pair(0.f, 0.f); +} + +float MixedFilamentManager::bias_ui_value_from_surface_offsets(float component_a_surface_offset, + float component_b_surface_offset, + float reference_width_mm) +{ + return std::clamp(canonical_signed_bias_value(component_a_surface_offset, component_b_surface_offset), + -max_pair_bias_mm(reference_width_mm), + max_pair_bias_mm(reference_width_mm)); +} + int MixedFilamentManager::apparent_mix_b_percent(int mix_b_percent, float component_a_surface_offset, float component_b_surface_offset, float reference_width_mm) { const float safe_reference = std::max(0.05f, std::abs(reference_width_mm)); - const float shift_pct = 100.f * (clamp_surface_offset(component_a_surface_offset) - - clamp_surface_offset(component_b_surface_offset)) / safe_reference; + const float shift_pct = -100.f * std::clamp(canonical_signed_bias_value(component_a_surface_offset, component_b_surface_offset), + -max_pair_bias_mm(reference_width_mm), + max_pair_bias_mm(reference_width_mm)) / safe_reference; return clamp_int(int(std::lround(float(clamp_int(mix_b_percent, 0, 100)) + shift_pct)), 0, 100); } diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index a62c3492a4..6b4702c877 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -229,6 +229,13 @@ public: static std::string blend_color(const std::string &color_a, const std::string &color_b, int ratio_a, int ratio_b); + static float max_component_surface_offset_mm(float reference_width_mm = 0.4f); + static float max_pair_bias_mm(float reference_width_mm = 0.4f); + static std::pair surface_offset_pair_from_signed_bias(float bias_mm, + float reference_width_mm = 0.4f); + static float bias_ui_value_from_surface_offsets(float component_a_surface_offset, + float component_b_surface_offset, + float reference_width_mm = 0.4f); static int apparent_mix_b_percent(int mix_b_percent, float component_a_surface_offset, float component_b_surface_offset, diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index ce9e266237..970ac73cf3 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -74,6 +74,7 @@ static std::vector s_project_options { "mixed_filament_height_lower_bound", "mixed_filament_height_upper_bound", "mixed_filament_advanced_dithering", + "mixed_filament_component_bias_enabled", "mixed_filament_surface_indentation", "mixed_filament_region_collapse", "mixed_filament_definitions", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b90df40089..e1831f9a5e 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -296,6 +296,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "mixed_filament_height_lower_bound" || opt_key == "mixed_filament_height_upper_bound" || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_component_bias_enabled" || opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_region_collapse" || opt_key == "mixed_filament_definitions" diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index a1befe1f89..d99667932d 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1208,6 +1208,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("mixed_filament_advanced_dithering", true); new_full_config.option("mixed_filament_pointillism_pixel_size", true); new_full_config.option("mixed_filament_pointillism_line_gap", true); + new_full_config.option("mixed_filament_component_bias_enabled", true); new_full_config.option("mixed_filament_surface_indentation", true); new_full_config.option("mixed_filament_region_collapse", true); new_full_config.option("mixed_filament_definitions", true); @@ -1220,6 +1221,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_config.option("mixed_filament_advanced_dithering", true); m_config.option("mixed_filament_pointillism_pixel_size", true); m_config.option("mixed_filament_pointillism_line_gap", true); + m_config.option("mixed_filament_component_bias_enabled", true); m_config.option("mixed_filament_surface_indentation", true); m_config.option("mixed_filament_region_collapse", true); m_config.option("mixed_filament_definitions", true); @@ -1232,6 +1234,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_default_object_config.option("mixed_filament_advanced_dithering", true); m_default_object_config.option("mixed_filament_pointillism_pixel_size", true); m_default_object_config.option("mixed_filament_pointillism_line_gap", true); + m_default_object_config.option("mixed_filament_component_bias_enabled", true); m_default_object_config.option("mixed_filament_surface_indentation", true); m_default_object_config.option("mixed_filament_region_collapse", true); m_default_object_config.option("mixed_filament_definitions", true); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 738c768d2d..de7170d85e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4232,6 +4232,15 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); + def = this->add("mixed_filament_component_bias_enabled", coBool); + def->label = L("Enable mixed filament bias"); + def->category = L("Others"); + def->tooltip = L("Show and apply the per-row mixed filament Bias control.\n\n" + "When enabled, the selected filament in a mixed pair is recessed slightly so the other component becomes more visible.\n\n" + "Bias is ignored for grouped wall patterns, same-layer pointillisme, and Local Z dithering."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("mixed_filament_surface_indentation", coFloat); def->label = L("Selective Expansion contraction"); def->category = L("Others"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 10514744d7..c04d654351 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1362,6 +1362,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, mixed_filament_advanced_dithering)) ((ConfigOptionFloat, mixed_filament_pointillism_pixel_size)) ((ConfigOptionFloat, mixed_filament_pointillism_line_gap)) + ((ConfigOptionBool, mixed_filament_component_bias_enabled)) ((ConfigOptionFloat, mixed_filament_surface_indentation)) ((ConfigOptionBool, mixed_filament_region_collapse)) ((ConfigOptionString, mixed_filament_definitions)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index e4a3d877a9..1b32d1622c 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -950,6 +950,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_step_painted_zones_only" + || opt_key == "mixed_filament_component_bias_enabled" || opt_key == "mixed_filament_region_collapse" || opt_key == "mmu_segmented_region_max_width" || opt_key == "mmu_segmented_region_interlocking_depth" @@ -973,6 +974,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "mixed_filament_height_lower_bound" || opt_key == "mixed_filament_height_upper_bound" || opt_key == "mixed_filament_advanced_dithering" + || opt_key == "mixed_filament_component_bias_enabled" || opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_region_collapse" || opt_key == "mixed_filament_definitions") { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 4571461fce..7953db7311 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -897,6 +897,52 @@ static inline unsigned int segmentation_channel_filament_id(size_t channel_idx) return unsigned(channel_idx); } +static float mixed_filament_reference_nozzle_mm(const MixedFilament &mixed_row, const ConfigOptionFloats &nozzle_diameters) +{ + std::vector samples; + samples.reserve(2); + + auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) { + if (component_id >= 1 && component_id <= nozzle_diameters.size()) + samples.emplace_back(std::max(0.05f, float(nozzle_diameters.get_at(component_id - 1)))); + }; + + append_if_valid(mixed_row.component_a); + append_if_valid(mixed_row.component_b); + + if (samples.empty()) + return 0.4f; + return std::accumulate(samples.begin(), samples.end(), 0.0f) / float(samples.size()); +} + +static coordf_t clamped_mixed_component_surface_offset(const MixedFilamentManager &mixed_mgr, + const PrintConfig &print_cfg, + unsigned int filament_id, + size_t num_physical, + int layer_index, + float layer_print_z, + float layer_height, + bool force_height_weighted = false) +{ + const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(filament_id, num_physical); + if (mixed_row == nullptr) + return 0.f; + + const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset( + filament_id, + num_physical, + layer_index, + layer_print_z, + layer_height, + force_height_weighted)); + if (std::abs(offset_mm) <= EPSILON) + return 0.f; + + const float reference_nozzle = mixed_filament_reference_nozzle_mm(*mixed_row, print_cfg.nozzle_diameter); + const coordf_t limit_mm = coordf_t(MixedFilamentManager::max_component_surface_offset_mm(reference_nozzle)); + return std::clamp(offset_mm, -limit_mm, limit_mm); +} + static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vector> &segmentation) { const Print *print = print_object.print(); @@ -1041,6 +1087,8 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std const DynamicPrintConfig &full_cfg = print->full_print_config(); if (bool_from_full_config(full_cfg, "dithering_local_z_mode", print_cfg.dithering_local_z_mode.value)) return false; + if (!bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value)) + return false; const size_t num_physical = print_cfg.filament_colour.size(); const size_t num_channels = segmentation.front().size(); @@ -1065,7 +1113,6 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std size_t emptied_states = 0; size_t expanded_states = 0; size_t contracted_states = 0; - size_t overlap_clipped_states = 0; for (size_t layer_id = 0; layer_id < segmentation.size(); ++layer_id) { if (segmentation[layer_id].size() != num_channels) @@ -1085,11 +1132,13 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std if (!mixed_mgr.is_mixed(state_id, num_physical)) continue; - const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset(state_id, - num_physical, - int(layer_id), - layer_print_z, - layer_height)); + const coordf_t offset_mm = clamped_mixed_component_surface_offset(mixed_mgr, + print_cfg, + state_id, + num_physical, + int(layer_id), + layer_print_z, + layer_height); if (std::abs(offset_mm) <= EPSILON) continue; @@ -1113,8 +1162,6 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std occupied_other = union_ex(occupied_other); if (!occupied_other.empty()) { ExPolygons clipped = diff_ex(adjusted, occupied_other, ApplySafetyOffset::Yes); - if (std::abs(area(clipped)) + EPSILON < std::abs(area(adjusted))) - ++overlap_clipped_states; adjusted = std::move(clipped); if (!adjusted.empty() && adjusted.size() > 1) adjusted = union_ex(adjusted); @@ -1145,8 +1192,7 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std << " changed_states=" << changed_states << " contracted_states=" << contracted_states << " expanded_states=" << expanded_states - << " emptied_states=" << emptied_states - << " overlap_clipped_states=" << overlap_clipped_states; + << " emptied_states=" << emptied_states; return true; } @@ -2311,6 +2357,8 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object) const DynamicPrintConfig &full_cfg = print->full_print_config(); if (bool_from_full_config(full_cfg, "dithering_local_z_mode", print_cfg.dithering_local_z_mode.value)) return false; + if (!bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value)) + return false; const size_t num_physical = print_cfg.filament_diameter.size(); if (num_physical == 0) @@ -2355,11 +2403,13 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object) if (!mixed_mgr.is_mixed(filament_id, num_physical)) continue; - const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset(filament_id, - num_physical, - int(layer_id), - float(layer.print_z), - float(layer.height))); + const coordf_t offset_mm = clamped_mixed_component_surface_offset(mixed_mgr, + print_cfg, + filament_id, + num_physical, + int(layer_id), + float(layer.print_z), + float(layer.height)); if (std::abs(offset_mm) <= EPSILON) continue; @@ -2367,7 +2417,8 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object) if (delta_scaled <= float(EPSILON)) continue; - ExPolygons adjusted = offset_ex(to_expolygons(layerm->slices.surfaces), offset_mm > 0 ? -delta_scaled : delta_scaled); + const ExPolygons original = to_expolygons(layerm->slices.surfaces); + ExPolygons adjusted = offset_ex(original, offset_mm > 0 ? -delta_scaled : delta_scaled); if (!adjusted.empty() && adjusted.size() > 1) adjusted = union_ex(adjusted); @@ -3255,11 +3306,13 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< const coordf_t base_height = std::max(0.01f, coordf_t(print_object.config().layer_height.value)); const bool collapse_mixed_regions = bool_from_full_config(full_cfg, "mixed_filament_region_collapse", print_cfg.mixed_filament_region_collapse.value); + const bool bias_mode_enabled = + bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value); const MixedFilamentManager &mixed_mgr = print_object.print()->mixed_filament_manager(); tbb::parallel_for( tbb::blocked_range(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))), - [&print_object, &segmentation, &mixed_mgr, num_physical, preferred_a, preferred_b, base_height, collapse_mixed_regions, throw_on_cancel](const tbb::blocked_range &range) { + [&print_object, &segmentation, &mixed_mgr, num_physical, preferred_a, preferred_b, base_height, collapse_mixed_regions, bias_mode_enabled, throw_on_cancel](const tbb::blocked_range &range) { const auto &layer_ranges = print_object.shared_regions()->layer_ranges; double z = print_object.get_layer(int(range.begin()))->slice_z; auto it_layer_range = layer_range_first(layer_ranges, z); @@ -3334,6 +3387,7 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< std::vector missing_target_extruders; ExPolygons default_segmentation = num_channels > 0 ? std::move(segmentation[layer_id][0]) : ExPolygons(); BoundingBox default_bbox; + bool layer_has_component_bias = false; if (!default_segmentation.empty()) { default_bbox = get_extents(default_segmentation); layer_split = true; @@ -3358,11 +3412,37 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< region.bbox = get_extents(region.expolygons); layer_split = true; } + + if (!region.expolygons.empty() && + bias_mode_enabled && + mixed_mgr.is_mixed(channel_id, num_physical) && + std::abs(mixed_mgr.component_surface_offset(channel_id, + num_physical, + int(layer_id), + float(layer.print_z), + float(layer.height))) > EPSILON) + layer_has_component_bias = true; } if (!layer_split) continue; + ExPolygons layer_geometry_mask; + BoundingBox layer_geometry_bbox; + if (layer_has_component_bias) { + if (!default_segmentation.empty()) + append(layer_geometry_mask, default_segmentation); + for (const ByExtruder &segmented : by_extruder) { + if (!segmented.expolygons.empty()) + append(layer_geometry_mask, segmented.expolygons); + } + if (!layer_geometry_mask.empty()) { + if (layer_geometry_mask.size() > 1) + layer_geometry_mask = closing_ex(union_ex(std::move(layer_geometry_mask)), scaled(5. * EPSILON)); + layer_geometry_bbox = get_extents(layer_geometry_mask); + } + } + // Split LayerRegions by by_extruder regions. // layer_range.painted_regions are sorted by extruder ID and parent PrintObject region ID. auto it_painted_region_begin = layer_range.painted_regions.cbegin(); @@ -3399,6 +3479,14 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< it_painted_region_begin = it_first_painted_region; const BoundingBox parent_layer_region_bbox = get_extents(parent_layer_region.slices.surfaces); + const bool clamp_parent_to_geometry = + layer_has_component_bias && + layer_geometry_bbox.defined && + parent_layer_region_bbox.overlap(layer_geometry_bbox); + ExPolygons clamped_parent_expolygons; + if (clamp_parent_to_geometry) + clamped_parent_expolygons = intersection_ex(parent_layer_region.slices.surfaces, layer_geometry_mask); + int self_extruder_id = -1; // 1-based extruder ID ExPolygons explicit_self_expolygons; ExPolygons default_self_expolygons; @@ -3465,7 +3553,11 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector< } // Trim slices of this LayerRegion with all the MM regions. - Polygons mine = to_polygons(parent_layer_region.slices.surfaces); + // Mixed bias can intentionally shrink a painted layer's true silhouette. + // Clamp the parent region to the post-bias segmentation union so the + // vacated area stays empty instead of falling back to the parent tool. + Polygons mine = clamp_parent_to_geometry ? to_polygons(clamped_parent_expolygons) : + to_polygons(parent_layer_region.slices.surfaces); for (size_t extruder_idx = 0; extruder_idx < by_extruder.size(); ++extruder_idx) { const ByExtruder &segmented = by_extruder[extruder_idx]; if (!assigned_extruder[extruder_idx]) diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index 6f1d87bc2e..91d1c493ec 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1173,7 +1173,8 @@ std::string string_printf(const char *format, ...) std::string header_slic3r_generated() { - return std::string(SLIC3R_APP_NAME " " Snapmaker_VERSION); + // Keep generated G-code branded like Snapmaker Orca for printer-side compatibility. + return std::string("Snapmaker Orca ") + Snapmaker_VERSION; } std::string header_gcodeviewer_generated() diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 1e760b8f52..d08ddd2414 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -9364,10 +9364,12 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c for (const LayerRegion* layerm : layer->regions()) { if (layerm->slices.surfaces.empty()) continue; - const PrintRegionConfig& cfg = layerm->region().config(); - if (cfg.wall_filament.value == m_selected_extruder || - cfg.sparse_infill_filament.value == m_selected_extruder || - cfg.solid_infill_filament.value == m_selected_extruder ) { + const int effective_wall_filament = int(layerm->extruder(frPerimeter)); + const int effective_sparse_infill_filament = int(layerm->extruder(frInfill)); + const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill)); + if (effective_wall_filament == m_selected_extruder || + effective_sparse_infill_filament == m_selected_extruder || + effective_solid_infill_filament == m_selected_extruder) { at_least_one_has_correct_extruder = true; break; } @@ -9389,16 +9391,19 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c for (const LayerRegion *layerm : layer->regions()) { if (is_selected_separate_extruder) { - const PrintRegionConfig& cfg = layerm->region().config(); + const int effective_wall_filament = int(layerm->extruder(frPerimeter)); const int effective_sparse_infill_filament = int(layerm->extruder(frInfill)); - if (cfg.wall_filament.value != m_selected_extruder && + const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill)); + if (effective_wall_filament != m_selected_extruder && effective_sparse_infill_filament != m_selected_extruder && - cfg.solid_infill_filament.value != m_selected_extruder) + effective_solid_infill_filament != m_selected_extruder) continue; } - if (ctxt.has_perimeters) + if (ctxt.has_perimeters) { + const int effective_wall_filament = int(layerm->extruder(frPerimeter)); _3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy, - select_geometry(idx_layer, layerm->region().config().wall_filament.value, 0)); + select_geometry(idx_layer, effective_wall_filament, 0)); + } if (ctxt.has_infill) { for (const ExtrusionEntity *ee : layerm->fills.entities) { // fill represents infill extrusions of a single island. @@ -9406,13 +9411,14 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c if (! fill->entities.empty()) { const int effective_sparse_infill_filament = int(layerm->extruder(frInfill)); + const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill)); _3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy, select_geometry(idx_layer, (fill->entities.front()->role() == erSolidInfill && std::abs(layerm->region().config().sparse_infill_density.value - 100.) < EPSILON) ? - int(layerm->extruder(frSolidInfill)) : + effective_solid_infill_filament : (is_solid_infill(fill->entities.front()->role()) ? - layerm->region().config().solid_infill_filament : + effective_solid_infill_filament : effective_sparse_infill_filament), 1)); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e4b9c8461b..999f16fd45 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4583,6 +4583,7 @@ public: const std::vector &nozzle_diameters, const std::vector &palette, const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, OnChangeFn on_change = {}); // Get the updated mixed filament data @@ -4604,6 +4605,7 @@ private: std::vector m_nozzle_diameters; std::vector m_palette; MixedFilamentPreviewSettings m_preview_settings; + bool m_bias_mode_enabled = false; bool m_has_changes = false; wxChoice *m_choice_a = nullptr; @@ -4622,8 +4624,9 @@ private: wxStaticText *m_picker_b_label = nullptr; wxStaticText *m_picker_c_label = nullptr; wxStaticText *m_picker_d_label = nullptr; + wxPanel *m_surface_offset_target_container = nullptr; wxPanel *m_surface_offset_target_swatch = nullptr; - wxChoice *m_surface_offset_target_choice = nullptr; + wxStaticText *m_surface_offset_target_label = nullptr; MixedGradientSelector *m_blend_selector = nullptr; wxStaticText *m_blend_label = nullptr; wxTextCtrl *m_pattern_ctrl = nullptr; @@ -4711,17 +4714,26 @@ private: return m_fallback; } - int vertical_inset_for_extruder(unsigned int extruder_id, int rect_height) const + double max_active_surface_offset_mm() const { - if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || rect_height <= 2) + double max_offset = 0.0; + for (double offset_mm : m_surface_offsets_mm) + max_offset = std::max(max_offset, std::abs(offset_mm)); + return std::max(0.001, max_offset); + } + + int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const + { + if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2) return 0; const double offset_mm = m_surface_offsets_mm[extruder_id]; - if (offset_mm <= 0.0) + if (std::abs(offset_mm) <= EPSILON) return 0; - const double normalized = std::clamp(offset_mm / 2.0, 0.0, 1.0); - return std::clamp(int(std::round(normalized * rect_height * 0.30)), 0, std::max(0, rect_height / 3)); + const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0); + const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1); + return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2)); } void on_paint(wxPaintEvent &) @@ -4747,8 +4759,11 @@ private: dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx]))); const int x = rect.GetLeft() + s * stripe_w; const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w; - const int inset_y = vertical_inset_for_extruder(extruder_id, rect.GetHeight()); - dc.DrawRectangle(x, rect.GetTop() + inset_y, std::max(1, w), std::max(1, rect.GetHeight() - inset_y * 2)); + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); } } else { const int bars = 24; @@ -4763,8 +4778,11 @@ private: dc.SetBrush(wxBrush(color_for_extruder(extruder_id))); const int x = rect.GetLeft() + i * bar_w; const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w; - const int inset_y = vertical_inset_for_extruder(extruder_id, rect.GetHeight()); - dc.DrawRectangle(x, rect.GetTop() + inset_y, std::max(1, w), std::max(1, rect.GetHeight() - inset_y * 2)); + const int inset = slot_inset_for_extruder(extruder_id, w); + wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight()); + draw_rect.Intersect(rect); + if (draw_rect.GetWidth() > 0) + dc.DrawRectangle(draw_rect); } } } @@ -5582,8 +5600,11 @@ int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const Mixe } static bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf, - const MixedFilamentPreviewSettings &preview_settings) + const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled) { + if (!bias_mode_enabled) + return false; if (preview_settings.local_z_mode) return false; if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) @@ -5595,24 +5616,6 @@ static bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf, return mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b; } -static std::pair mixed_filament_single_surface_offset_ui_state(const MixedFilament &mf) -{ - const float offset_a = mf.component_a_surface_offset; - const float offset_b = mf.component_b_surface_offset; - const bool has_a = std::abs(offset_a) > 1e-6f; - const bool has_b = std::abs(offset_b) > 1e-6f; - - if (has_b && (!has_a || std::abs(offset_b) > std::abs(offset_a))) - return { 1, offset_b }; - return { 0, has_a ? offset_a : 0.f }; -} - -static std::pair mixed_filament_single_surface_offset_pair(int target_index, float value) -{ - const float clamped = std::clamp(value, -2.f, 2.f); - return target_index == 1 ? std::make_pair(0.f, clamped) : std::make_pair(clamped, 0.f); -} - static double mixed_filament_reference_nozzle_mm(unsigned int component_a, unsigned int component_b, const std::vector &nozzle_diameters) @@ -5633,12 +5636,37 @@ static double mixed_filament_reference_nozzle_mm(unsigned int comp return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size()); } +static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm)); +} + +static float mixed_filament_single_surface_offset_value(const MixedFilament &mf, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::bias_ui_value_from_surface_offsets( + mf.component_a_surface_offset, + mf.component_b_surface_offset, + float(reference_nozzle_mm)); +} + +static std::pair mixed_filament_single_surface_offset_pair(const MixedFilament &mf, + float value, + const std::vector &nozzle_diameters) +{ + const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); + return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm)); +} + static std::pair mixed_filament_apparent_pair_percentages(const MixedFilament &mf, const MixedFilamentPreviewSettings &preview_settings, - const std::vector &nozzle_diameters) + const std::vector &nozzle_diameters, + bool bias_mode_enabled) { const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); - if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings)) + if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) return { 100 - base_b, base_b }; const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters); @@ -5651,15 +5679,16 @@ static std::pair mixed_filament_apparent_pair_percentages(const MixedF static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf, const MixedFilamentPreviewSettings &preview_settings, - const std::vector &nozzle_diameters) + const std::vector &nozzle_diameters, + bool bias_mode_enabled) { - if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings)) + if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled)) return {}; const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings); const int base_a = 100 - base_b; const auto [apparent_a, apparent_b] = - mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters); + mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled); if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f && (apparent_a != base_a || apparent_b != base_b)) { @@ -5883,6 +5912,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, const std::vector &nozzle_diameters, const std::vector &palette, const MixedFilamentPreviewSettings &preview_settings, + bool bias_mode_enabled, OnChangeFn on_change) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE) , m_mixed_id(mixed_id) @@ -5892,6 +5922,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, , m_nozzle_diameters(nozzle_diameters) , m_palette(palette) , m_preview_settings(preview_settings) + , m_bias_mode_enabled(bias_mode_enabled) , m_selected_weight_state(std::make_shared>()) , m_on_change(on_change) { @@ -5905,6 +5936,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent, void MixedFilamentConfigPanel::build_ui() { const int gap = FromDIP(6); + const int compact_gap = std::max(FromDIP(2), gap / 3); const bool is_dark = wxGetApp().dark_mode(); const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() : (is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255)); @@ -5967,8 +5999,10 @@ void MixedFilamentConfigPanel::build_ui() auto *container_sizer = new wxBoxSizer(wxHORIZONTAL); swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE); swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12))); + swatch_out->SetToolTip(tooltip); label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString); label_out->SetForegroundColour(local_picker_text); + label_out->SetToolTip(tooltip); auto *content_sizer = new wxBoxSizer(wxHORIZONTAL); content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap); @@ -6087,59 +6121,68 @@ void MixedFilamentConfigPanel::build_ui() auto *preview_row = new wxBoxSizer(wxHORIZONTAL); m_mix_preview = new MixedMixPreview(this); m_mix_preview->SetBackgroundColour(panel_bg); - preview_row->Add(m_mix_preview, 3, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, gap); + preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); auto *bias_controls = new wxBoxSizer(wxHORIZONTAL); - const auto initial_surface_offset_state = mixed_filament_single_surface_offset_ui_state(m_mf); + const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters); + const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const wxString bias_tooltip = + _L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n" + "The color chip shows which filament the current value affects.\n\n" + "Grouped wall patterns, same-layer pointillisme, and Local-Z dithering ignore it."); - auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("XY-Bias")); + auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias")); surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20)); - surface_offset_label->SetToolTip( - _L("Additional XY offset applied only on layers where this mixed filament resolves to component A or B.\n\n" - "Positive values contract inward. Negative values expand outward.\n\n" - "Currently this is intended for ordinary layer/height cadence rows. Grouped wall patterns, same-layer pointillisme, and Local-Z dithering ignore it.")); - bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(4), gap / 2)); + surface_offset_label->SetToolTip(bias_tooltip); + bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - auto *target_group = new wxBoxSizer(wxHORIZONTAL); - m_surface_offset_target_swatch = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(10), FromDIP(10)), wxBORDER_SIMPLE); - m_surface_offset_target_swatch->SetMinSize(wxSize(FromDIP(10), FromDIP(10))); - m_surface_offset_target_swatch->SetMaxSize(wxSize(FromDIP(10), FromDIP(10))); - m_surface_offset_target_swatch->SetToolTip(_L("Choose which filament in this pair receives the XY bias.")); - target_group->Add(m_surface_offset_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3)); + create_component_picker(m_surface_offset_target_container, + m_surface_offset_target_swatch, + m_surface_offset_target_label, + bias_tooltip); + if (m_surface_offset_target_container) + m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_swatch) + m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW)); + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW)); + bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); - wxArrayString surface_offset_targets; - surface_offset_targets.Add(wxString::Format("F%d", int(m_mf.component_a))); - surface_offset_targets.Add(wxString::Format("F%d", int(m_mf.component_b))); - m_surface_offset_target_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(62), -1), surface_offset_targets); - m_surface_offset_target_choice->SetSelection(std::clamp(initial_surface_offset_state.first, 0, 1)); - m_surface_offset_target_choice->SetToolTip(_L("Choose which filament in this pair receives the XY bias.")); - target_group->Add(m_surface_offset_target_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); - bias_controls->Add(target_group, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - - m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(64), -1), + m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1), wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER, - -2.0, 2.0, std::clamp(double(initial_surface_offset_state.second), -2.0, 2.0), 0.01); + -initial_bias_limit, initial_bias_limit, + std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001); m_surface_offset_spin->SetDigits(3); - m_surface_offset_spin->SetToolTip( - _L("XY bias for the selected filament on layers where this row resolves to it. Positive values contract inward. Negative values expand outward.")); - bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6)); + m_surface_offset_spin->SetToolTip(bias_tooltip); + bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap); auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm")); surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72)); + surface_offset_units->SetToolTip(bias_tooltip); bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL); - preview_row->Add(bias_controls, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); + if (m_bias_mode_enabled) + preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL); + else { + surface_offset_label->Hide(); + if (m_surface_offset_target_container) + m_surface_offset_target_container->Hide(); + if (m_surface_offset_spin) + m_surface_offset_spin->Hide(); + surface_offset_units->Hide(); + } root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap); - const auto initial_surface_offset_pair = - mixed_filament_single_surface_offset_pair(initial_surface_offset_state.first, initial_surface_offset_state.second); - m_mf.component_a_surface_offset = initial_surface_offset_pair.first; - m_mf.component_b_surface_offset = initial_surface_offset_pair.second; + if (m_bias_mode_enabled) { + const auto initial_surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = initial_surface_offset_pair.first; + m_mf.component_b_surface_offset = initial_surface_offset_pair.second; + } - const bool initial_component_surface_offsets_supported = !pattern_row_mode && + const bool initial_component_surface_offsets_supported = m_bias_mode_enabled && + !pattern_row_mode && row_distribution_mode != int(MixedFilament::SameLayerPointillisme) && !m_preview_settings.local_z_mode; - if (m_surface_offset_target_choice) - m_surface_offset_target_choice->Enable(initial_component_surface_offsets_supported); if (m_surface_offset_spin) m_surface_offset_spin->Enable(initial_component_surface_offsets_supported); @@ -6179,6 +6222,18 @@ void MixedFilamentConfigPanel::build_ui() auto apply_changes = [this]() { m_has_changes = true; + double surface_offset_value = 0.0; + if (m_surface_offset_spin) { + surface_offset_value = m_surface_offset_spin->GetValue(); +#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE) + if (wxTextCtrl *text = m_surface_offset_spin->GetText()) { + double parsed_value = 0.0; + if (text->GetValue().ToDouble(&parsed_value)) + surface_offset_value = parsed_value; + } +#endif + } + int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical)); int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical)); if (a == b && m_num_physical > 1) { @@ -6194,14 +6249,16 @@ void MixedFilamentConfigPanel::build_ui() const bool preserve_same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme); m_mf.component_a = unsigned(a); m_mf.component_b = unsigned(b); - const int surface_offset_target_index = - m_surface_offset_target_choice ? std::clamp(m_surface_offset_target_choice->GetSelection(), 0, 1) : 0; - const float surface_offset_value = - m_surface_offset_spin ? std::clamp(float(m_surface_offset_spin->GetValue()), -2.f, 2.f) : 0.f; - const auto surface_offset_pair = - mixed_filament_single_surface_offset_pair(surface_offset_target_index, surface_offset_value); - m_mf.component_a_surface_offset = surface_offset_pair.first; - m_mf.component_b_surface_offset = surface_offset_pair.second; + if (m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters); + const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit)); + const auto surface_offset_pair = + mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters); + m_mf.component_a_surface_offset = surface_offset_pair.first; + m_mf.component_b_surface_offset = surface_offset_pair.second; + if (m_surface_offset_spin) + m_surface_offset_spin->SetValue(clamped_surface_offset_value); + } m_mf.local_z_max_sublayers = (m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ? std::max(2, m_local_z_limit_spin->GetValue()) : @@ -6290,11 +6347,10 @@ void MixedFilamentConfigPanel::build_ui() m_mf.custom = true; const std::vector selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids); - const bool component_surface_offsets_supported = (m_pattern_ctrl == nullptr) && + const bool component_surface_offsets_supported = m_bias_mode_enabled && + (m_pattern_ctrl == nullptr) && !same_layer_mode && !m_preview_settings.local_z_mode; - if (m_surface_offset_target_choice) - m_surface_offset_target_choice->Enable(component_surface_offsets_supported); if (m_surface_offset_spin) m_surface_offset_spin->Enable(component_surface_offsets_supported); if (preview_sequence.empty()) @@ -6311,11 +6367,11 @@ void MixedFilamentConfigPanel::build_ui() m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state); } - if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings) && + if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && m_mf.component_a >= 1 && m_mf.component_b >= 1 && m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters); + mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); m_mf.display_color = MixedFilamentManager::blend_color( m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_b - 1)], @@ -6344,13 +6400,14 @@ void MixedFilamentConfigPanel::build_ui() } if (m_mix_preview) { - const std::string bias_summary = mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters); + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary; std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = std::max(0.0, double(m_mf.component_a_surface_offset)); - if (m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = std::max(0.0, double(m_mf.component_b_surface_offset)); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); } @@ -6448,8 +6505,6 @@ void MixedFilamentConfigPanel::build_ui() evt.Skip(); }); } - if (m_surface_offset_target_choice) - m_surface_offset_target_choice->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent &) { apply_changes(); }); if (m_surface_offset_spin) { m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); }); m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); }); @@ -6503,6 +6558,7 @@ void MixedFilamentConfigPanel::build_ui() } } + update_component_picker_visuals(); SetSizer(root); Layout(); SetMinSize(wxSize(-1, GetBestSize().GetHeight())); @@ -6557,22 +6613,33 @@ void MixedFilamentConfigPanel::update_component_picker_visuals() update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label); update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label); - if (m_surface_offset_target_choice) { + if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) { const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max(1, m_num_physical))); const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max(1, m_num_physical))); - if (m_surface_offset_target_choice->GetCount() >= 2) { - m_surface_offset_target_choice->SetString(0, wxString::Format("F%d", a_filament)); - m_surface_offset_target_choice->SetString(1, wxString::Format("F%d", b_filament)); + MixedFilament active_pair = m_mf; + active_pair.component_a = unsigned(a_filament); + active_pair.component_b = unsigned(b_filament); + double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters); + + if (m_surface_offset_spin && m_bias_mode_enabled) { + const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters); + m_surface_offset_spin->SetRange(-bias_limit, bias_limit); + signed_bias_value = m_surface_offset_spin->GetValue(); } - const int selected_target = std::clamp(m_surface_offset_target_choice->GetSelection(), 0, 1); - const int color_idx = (selected_target == 1 ? b_filament : a_filament) - 1; + const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament; + const int color_idx = active_filament - 1; const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A"); if (m_surface_offset_target_swatch) { m_surface_offset_target_swatch->SetBackgroundColour(color); m_surface_offset_target_swatch->Refresh(); } - m_surface_offset_target_choice->Refresh(); + if (m_surface_offset_target_label) + m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament)); + if (m_surface_offset_target_container) { + m_surface_offset_target_container->Layout(); + m_surface_offset_target_container->Refresh(); + } } } @@ -6613,11 +6680,11 @@ void MixedFilamentConfigPanel::update_preview() } if (m_mix_preview) { - if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings) && + if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) && m_mf.component_a >= 1 && m_mf.component_b >= 1 && m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) { const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters); + mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); m_mf.display_color = MixedFilamentManager::blend_color( m_physical_colors[size_t(m_mf.component_a - 1)], m_physical_colors[size_t(m_mf.component_b - 1)], @@ -6625,13 +6692,14 @@ void MixedFilamentConfigPanel::update_preview() apparent_pct_b); } - const std::string bias_summary = mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters); + const std::string bias_summary = + mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled); const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary; std::vector preview_surface_offsets(m_palette.size() + 1, 0.0); - if (m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_a] = std::max(0.0, double(m_mf.component_a_surface_offset)); - if (m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) - preview_surface_offsets[m_mf.component_b] = std::max(0.0, double(m_mf.component_b_surface_offset)); + if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset); + if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size()) + preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset); m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color), _L("Preview"), summary.empty() ? wxString() : from_u8(summary)); } @@ -6870,6 +6938,22 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) else preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value)); }; + auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) { + if (print_cfg) { + if (ConfigOptionBool *opt = print_cfg->option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = print_cfg->option(key)) + opt->value = value ? 1 : 0; + else + print_cfg->set_key_value(key, new ConfigOptionBool(value)); + } + if (ConfigOptionBool *opt = preset_bundle->project_config.option(key)) + opt->value = value; + else if (ConfigOptionInt *opt = preset_bundle->project_config.option(key)) + opt->value = value ? 1 : 0; + else + preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value)); + }; auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) { if (print_cfg) { if (ConfigOptionBool *opt = print_cfg->option("mixed_filament_gradient_mode")) @@ -7088,6 +7172,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) if (print_cfg && print_cfg->has("wall_loops")) wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false); + const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f)); float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f)); float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f); @@ -7185,13 +7270,14 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) const bool same_layer_mode = entry.distribution_mode == int(MixedFilament::SameLayerPointillisme); return build_effective_pair_preview_sequence(entry.component_a, entry.component_b, effective_mix_b, same_layer_mode); }; - auto compute_entry_display_color = [num_physical, &physical_colors, &nozzle_diameters, blend_from_sequence, build_entry_preview_sequence, preview_settings](const MixedFilament &entry) { - if (mixed_filament_supports_bias_apparent_color(entry, preview_settings) && + auto compute_entry_display_color = [num_physical, &physical_colors, &nozzle_diameters, blend_from_sequence, build_entry_preview_sequence, + preview_settings, component_bias_enabled](const MixedFilament &entry) { + if (mixed_filament_supports_bias_apparent_color(entry, preview_settings, component_bias_enabled) && entry.component_a >= 1 && entry.component_b >= 1 && entry.component_a <= num_physical && entry.component_b <= num_physical && entry.component_a <= physical_colors.size() && entry.component_b <= physical_colors.size()) { const auto [apparent_pct_a, apparent_pct_b] = - mixed_filament_apparent_pair_percentages(entry, preview_settings, nozzle_diameters); + mixed_filament_apparent_pair_percentages(entry, preview_settings, nozzle_diameters, component_bias_enabled); return MixedFilamentManager::blend_color( physical_colors[entry.component_a - 1], physical_colors[entry.component_b - 1], @@ -7225,11 +7311,21 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering); } + if (component_bias_enabled) { + for (MixedFilament &entry : mixed_mgr.mixed_filaments()) { + const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters); + const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters); + entry.component_a_surface_offset = balanced_pair.first; + entry.component_b_surface_offset = balanced_pair.second; + } + } + // During project load, sidebar may refresh before physical filament combos // finish syncing. Avoid overwriting persisted mixed definitions while the // physical filament set is incomplete. if (num_physical >= 2) { set_mixed_mode(height_weighted_mode); + set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled); set_mixed_float("mixed_filament_height_lower_bound", lower_bound); set_mixed_float("mixed_filament_height_upper_bound", upper_bound); set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a); @@ -7640,7 +7736,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) return row->GetClientRect().Contains(local); }; - auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, preset_bundle, + auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle, editor_host, editor_sizer, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() { if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0) @@ -7652,6 +7748,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) return; auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings, + component_bias_enabled, [this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) { apply_mixed_entry_changes(mixed_id, updated_mf, true); @@ -10360,6 +10457,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ "mixed_filament_advanced_dithering", "mixed_filament_pointillism_pixel_size", "mixed_filament_pointillism_line_gap", + "mixed_filament_component_bias_enabled", "mixed_filament_surface_indentation" }; preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 291e2b4604..c9e0673063 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1793,6 +1793,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) return; } + const bool refresh_mixed_filament_panel = + m_type == Preset::TYPE_PRINT && opt_key == "mixed_filament_component_bias_enabled"; + // 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. @@ -1805,6 +1808,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_pointillism_pixel_size" || opt_key == "mixed_filament_pointillism_line_gap" || + opt_key == "mixed_filament_component_bias_enabled" || opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_region_collapse" || opt_key == "dithering_z_step_size" || @@ -1820,6 +1824,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if(m_active_page) m_active_page->update_visibility(m_mode, true); m_page_view->GetParent()->Layout(); + + if (refresh_mixed_filament_panel && wxGetApp().plater() != nullptr) + wxGetApp().sidebar().update_mixed_filament_panel(false); } void Tab::show_timelapse_warning_dialog() { @@ -2543,6 +2550,7 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("mixed_filament_advanced_dithering"); optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size"); optgroup->append_single_option_line("mixed_filament_pointillism_line_gap"); + optgroup->append_single_option_line("mixed_filament_component_bias_enabled"); optgroup->append_single_option_line("mixed_filament_surface_indentation"); optgroup->append_single_option_line("mixed_filament_region_collapse"); optgroup->append_single_option_line("dithering_z_step_size"); diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index aca4281de4..0bc3d4eee2 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -124,7 +124,7 @@ TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlie bundle.update_multi_material_filament_presets(); auto &mixed = bundle.mixed_filaments.mixed_filaments(); - REQUIRE(mixed.size() >= 10); + REQUIRE(mixed.size() >= 6); const uint64_t stable_id_6 = mixed[1].stable_id; const uint64_t stable_id_7 = mixed[2].stable_id; @@ -137,7 +137,7 @@ TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlie bundle.update_mixed_filament_id_remap(old_mixed, 4, 4); const std::vector remap = bundle.consume_last_filament_id_remap(); - REQUIRE(remap.size() >= 15); + REQUIRE(remap.size() >= 11); CHECK(remap[6] == virtual_id_for_stable_id(mixed, 4, stable_id_6)); CHECK(remap[7] == virtual_id_for_stable_id(mixed, 4, stable_id_7)); CHECK(remap[8] == virtual_id_for_stable_id(mixed, 4, stable_id_8)); @@ -164,7 +164,7 @@ TEST_CASE("Mixed filament grouped manual patterns normalize and round-trip", "[M CHECK(loaded.mixed_filaments().front().mix_b_percent == 13); } -TEST_CASE("Mixed filament component surface offsets round-trip and follow the active layer component", "[MixedFilament]") +TEST_CASE("Mixed filament component surface offsets round-trip and bias the second layer component", "[MixedFilament]") { const std::vector colors = {"#FF0000", "#FFFF00"}; @@ -189,18 +189,98 @@ TEST_CASE("Mixed filament component surface offsets round-trip and follow the ac const MixedFilament &loaded_row = loaded.mixed_filaments().front(); CHECK(loaded_row.component_a_surface_offset == Approx(0.02f)); CHECK(loaded_row.component_b_surface_offset == Approx(-0.01f)); - CHECK(loaded.component_surface_offset(3, 2, 0) == Approx(0.02f)); - CHECK(loaded.component_surface_offset(3, 2, 1) == Approx(-0.01f)); + CHECK(loaded.component_surface_offset(3, 2, 0) == Approx(0.01f)); + CHECK(loaded.component_surface_offset(3, 2, 1) == Approx(0.0f)); } -TEST_CASE("Mixed filament apparent mix percent shifts toward the less-contracted component", "[MixedFilament]") +TEST_CASE("Mixed filament apparent mix percent follows the signed bias target", "[MixedFilament]") { - CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.02f, 0.00f, 0.4f) == 55); + CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, 0.00f, 0.4f) == 50); CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, 0.02f, 0.4f) == 45); + CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.02f, 0.00f, 0.4f) == 55); CHECK(MixedFilamentManager::apparent_mix_b_percent(50, -0.02f, 0.00f, 0.4f) == 45); CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, -0.02f, 0.4f) == 55); } +TEST_CASE("Mixed filament bias helper maps signed bias to a one-sided safe offset pair", "[MixedFilament]") +{ + const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.06f, 0.4f); + CHECK(offset_a == Approx(0.0f)); + CHECK(offset_b == Approx(0.06f)); + + CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(offset_a, offset_b, 0.4f) == Approx(0.06f)); + + CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(0.02f, 0.0f, 0.4f) == Approx(-0.02f)); + CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(-0.02f, 0.0f, 0.4f) == Approx(0.02f)); + + const auto [negative_a, negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.06f, 0.4f); + CHECK(negative_a == Approx(0.06f)); + CHECK(negative_b == Approx(0.0f)); + + const auto [unclamped_a, unclamped_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.30f, 0.4f); + CHECK(unclamped_a == Approx(0.0f)); + CHECK(unclamped_b == Approx(0.30f)); + + const auto [unclamped_negative_a, unclamped_negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.30f, 0.4f); + CHECK(unclamped_negative_a == Approx(0.30f)); + CHECK(unclamped_negative_b == Approx(0.0f)); + + const auto [clamped_a, clamped_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.40f, 0.4f); + CHECK(clamped_a == Approx(0.0f)); + CHECK(clamped_b == Approx(0.35f)); + + const auto [clamped_negative_a, clamped_negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.40f, 0.4f); + CHECK(clamped_negative_a == Approx(0.35f)); + CHECK(clamped_negative_b == Approx(0.0f)); +} + +TEST_CASE("Mixed filament component surface offsets follow the signed bias target across alternating layers", "[MixedFilament]") +{ + const std::vector colors = {"#FF0000", "#FFFF00"}; + + MixedFilamentManager mgr; + mgr.add_custom_filament(1, 2, 50, colors); + REQUIRE(mgr.mixed_filaments().size() == 1); + + MixedFilament &row = mgr.mixed_filaments().front(); + row.manual_pattern.clear(); + row.distribution_mode = int(MixedFilament::Simple); + row.ratio_a = 1; + row.ratio_b = 1; + + { + const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.05f, 0.4f); + row.component_a_surface_offset = offset_a; + row.component_b_surface_offset = offset_b; + + CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.0f)); + CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.05f)); + CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.0f)); + CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.05f)); + } + + { + row.component_a_surface_offset = 0.05f; + row.component_b_surface_offset = 0.0f; + + CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.05f)); + CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.0f)); + CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.05f)); + CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.0f)); + } + + { + const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.05f, 0.4f); + row.component_a_surface_offset = offset_a; + row.component_b_surface_offset = offset_b; + + CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.05f)); + CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.0f)); + CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.05f)); + CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.0f)); + } +} + TEST_CASE("Mixed filament auto generation can be disabled without dropping custom rows", "[MixedFilament]") { const std::vector colors = {"#FF0000", "#00FF00", "#0000FF"}; diff --git a/version.inc b/version.inc index 9b9df5d1a3..9f02f6da02 100644 --- a/version.inc +++ b/version.inc @@ -10,8 +10,8 @@ endif() if(NOT DEFINED BBL_INTERNAL_TESTING) set(BBL_INTERNAL_TESTING "0") endif() -set(Snapmaker_VERSION "0.9.5") -set(FULLSPECTRUM_VERSION "0.9.5") +set(Snapmaker_VERSION "0.9.51") +set(FULLSPECTRUM_VERSION "0.9.51") set(MIN_FIRM_VER "1.0.0") string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" Snapmaker_VERSION_MATCH ${Snapmaker_VERSION}) @@ -19,7 +19,7 @@ set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1}) set(ORCA_VERSION_MINOR ${CMAKE_MATCH_2}) set(ORCA_VERSION_PATCH ${CMAKE_MATCH_3}) -set(SLIC3R_VERSION "01.10.01.50") +set(SLIC3R_VERSION "01.10.01.51") if (NOT DEFINED ORCA_UPDATER_SIG_KEY) set(ORCA_UPDATER_SIG_KEY "" CACHE STRING "Base64url encoded updater signature key")