From 01c609a08301822caac7ad8c7116937c838e0c9a Mon Sep 17 00:00:00 2001 From: Rad Date: Mon, 20 Apr 2026 04:59:50 +0200 Subject: [PATCH] - Adds optional whole-object Local-Z support for mixed wall regions instead of limiting Local-Z strictly to painted mixed zones. - Adds an experimental direct multicolor Local-Z solver with carry-over error for 3+ color mixed rows, reducing pair-cadence banding. --- doc/changelogs/RELEASE_NOTES_v0.9.8.md | 51 +++ src/common_func/common_func.hpp | 2 +- src/libslic3r/GCode.cpp | 43 +- src/libslic3r/MixedFilament.hpp | 1 + src/libslic3r/PresetBundle.cpp | 2 + src/libslic3r/Print.cpp | 1 + src/libslic3r/Print.hpp | 1 + src/libslic3r/PrintApply.cpp | 3 + src/libslic3r/PrintConfig.cpp | 8 + src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/libslic3r/PrintObjectSlice.cpp | 553 ++++++++++++++++++++++--- src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/GUI_App.cpp | 1 + src/slic3r/GUI/Plater.cpp | 30 ++ src/slic3r/GUI/Tab.cpp | 15 + version.inc | 4 +- 17 files changed, 646 insertions(+), 72 deletions(-) create mode 100644 doc/changelogs/RELEASE_NOTES_v0.9.8.md diff --git a/doc/changelogs/RELEASE_NOTES_v0.9.8.md b/doc/changelogs/RELEASE_NOTES_v0.9.8.md new file mode 100644 index 0000000000..1d4db4518e --- /dev/null +++ b/doc/changelogs/RELEASE_NOTES_v0.9.8.md @@ -0,0 +1,51 @@ +# Snapmaker Orca FullSpectrum v0.9.8 + +Local-Z multicolor improvements, duplicate gap-fill cleanup, and mixed-filament workflow fixes. +Compared to the previous FullSpectrum v0.9.7 release. +Based on Snapmaker Orca v2.3.1, with the bundled print-preprocessing Flutter/web resources kept on the newer 2.3.12 line. + +## Quick Overview + +- Adds optional whole-object Local-Z support for mixed wall regions instead of limiting Local-Z strictly to painted mixed zones. +- Adds an experimental direct multicolor Local-Z solver with carry-over error for 3+ color mixed rows, reducing pair-cadence banding. +- Fixes several Local-Z boundary issues so painted colors, fixed-color islands, and whole-object mixed masks cooperate more cleanly. +- Fixes a duplicated inner-loop gap-fill regression that could overprint narrow collapsed regions. +- Fixes mixed-filament bias-state and print-preprocessing remap issues that required restarting or blocked otherwise valid remaps. + +## More Detailed Overview + +### Local-Z Dithering + +- Restores and relabels the Local-Z lower and upper height bounds in the Others tab so they are clearly presented as Local-Z controls. +- Adds `Apply Local-Z to whole mixed objects` as an experimental opt-in, allowing default mixed wall regions to keep Local-Z active outside painted islands. +- Adds `Use direct multicolor Local-Z solver` as an experimental opt-in for 3+ color rows, using direct component allocation with carry-over error across layers instead of collapsing everything into pair cadence first. +- Keeps the older pair-based Local-Z path as the default for compatibility, while letting the new direct solver be tested independently. +- Fixes several planner/runtime issues around painted zones inside whole-object Local-Z bodies, including lost painted Local-Z treatment, phase resets at zone boundaries, and overlap/regrowth artifacts at fixed-color painted caps. +- Improves Local-Z summary text in the mixed-filament panel so the UI reports when the direct multicolor solver is active instead of describing the older pair-cadence model. + +### Gap Fill And Toolpath Generation + +- Removes the collapsed infill gap-fill fallback that was appending duplicate gap-fill passes in narrow looped regions. +- Resolves the regression where certain capsule-like inner loops could receive a doubled gap-fill contour and get visibly over-squished. +- Keeps the safer older path that avoided the duplicate loop in the reproduced meshes. + +### Mixed-Filament Workflow Fixes + +- Fixes mixed-filament bias controls getting stuck disabled after loading certain `.3mf` projects and then creating a new project without restarting. +- Keeps the mixed-filament project config mirrored back into the sidebar state when presets or projects are reloaded programmatically. +- Fixes print-preprocessing filament remap for the newer web dialog by sending file-side nozzle diameter data expected by the 2.3.12 frontend. +- Reduces the mismatch between automatic tray selection and manual remap eligibility in the preprocessing flow. + +### UI Cleanup + +- Retires the old height-weighted cadence and pointillisme-facing controls from the Others tab while keeping compatibility for older stored configs and projects. +- Keeps the Local-Z-related controls that are still in active use visible and synchronized with the current mixed-filament workflow. + +## Notes + +- `cmake --build build --target Snapmaker_Orca --config Release --parallel 4` succeeded. +- Much of this release was validated through repeated slice-preview and hardware-oriented regression checks for mixed-filament and Local-Z behavior. +- The active build tree in this workspace still had no registered `ctest` cases, so broader automated test reruns were not available from `ctest` here. +- macOS builds from this fork remain unsigned and not notarized. +- Release downloads for this fork are available here: https://github.com/ratdoux/OrcaSlicer-FullSpectrum/releases +- Use at your own risk. diff --git a/src/common_func/common_func.hpp b/src/common_func/common_func.hpp index afa1b1b297..2fca41c26b 100644 --- a/src/common_func/common_func.hpp +++ b/src/common_func/common_func.hpp @@ -7,7 +7,7 @@ #define SLIC3R_APP_KEY "Snapmaker_Orca" #define SLIC3R_VERSION "01.10.01.70" #define Snapmaker_VERSION "2.3.1" -#define FULLSPECTRUM_VERSION "0.9.7" +#define FULLSPECTRUM_VERSION "0.9.8" #define MIN_FIRM_VER "1.0.0" #ifndef GIT_COMMIT_HASH #define GIT_COMMIT_HASH "0000000" // 0000000 means uninitialized diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 93c04acbfb..762048bf29 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4865,6 +4865,7 @@ LayerResult GCode::process_layer(const Print& print, constexpr double LOCAL_Z_BASE_MASK_EXPAND_MM = 0.04; const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM)); const float local_z_base_mask_expand = float(scale_(LOCAL_Z_BASE_MASK_EXPAND_MM)); + const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects"); struct LocalZPassBucket { const SubLayerPlan* plan { nullptr }; @@ -4933,17 +4934,45 @@ LayerResult GCode::process_layer(const Print& print, LocalZPassBucket bucket; bucket.plan = &plan; - bucket.compensated_masks_by_extruder.assign(plan.painted_masks_by_extruder.size(), ExPolygons()); + const size_t plan_mask_slots = std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size()); + bucket.compensated_masks_by_extruder.assign(plan_mask_slots, ExPolygons()); bool pass_has_raw_masks = false; bool pass_has_compensated_masks = false; - for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { - const ExPolygons& raw_masks = plan.painted_masks_by_extruder[extruder_id]; - if (raw_masks.empty()) + ExPolygons fixed_raw_masks_union; + for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) + if (!fixed_masks.empty()) + append(fixed_raw_masks_union, fixed_masks); + if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1) + fixed_raw_masks_union = union_ex(fixed_raw_masks_union); + const ExPolygons fixed_compensated_guard = + fixed_raw_masks_union.empty() ? ExPolygons() : local_z_compensate_masks(fixed_raw_masks_union, local_z_perimeter_mask_expand, true); + + for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) { + const ExPolygons mixed_raw_masks = + extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons(); + const ExPolygons fixed_raw_masks = + extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] : ExPolygons(); + if (mixed_raw_masks.empty() && fixed_raw_masks.empty()) continue; pass_has_raw_masks = true; - raw_mask_polygon_count += raw_masks.size(); - append(raw_mixed_masks_union, raw_masks); - ExPolygons compensated = local_z_compensate_masks(raw_masks, local_z_perimeter_mask_expand, true); + raw_mask_polygon_count += mixed_raw_masks.size() + fixed_raw_masks.size(); + if (!mixed_raw_masks.empty()) + append(raw_mixed_masks_union, mixed_raw_masks); + if (!fixed_raw_masks.empty()) + append(raw_mixed_masks_union, fixed_raw_masks); + + ExPolygons compensated; + if (!mixed_raw_masks.empty()) { + ExPolygons compensated_mixed = local_z_compensate_masks(mixed_raw_masks, local_z_perimeter_mask_expand, true); + if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty()) + compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard); + if (!compensated_mixed.empty()) + append(compensated, compensated_mixed); + } + if (!fixed_raw_masks.empty()) + append(compensated, fixed_raw_masks); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); if (!compensated.empty()) { pass_has_compensated_masks = true; compensated_mask_polygon_count += compensated.size(); diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 83088e5e07..16d678ba41 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -120,6 +120,7 @@ struct MixedFilamentPreviewSettings double preferred_a_height { 0.0 }; double preferred_b_height { 0.0 }; bool local_z_mode { false }; + bool local_z_direct_multicolor { false }; size_t wall_loops { 1 }; }; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 970ac73cf3..4c3162b68c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -82,6 +82,8 @@ static std::vector s_project_options { "mixed_color_layer_height_b", "dithering_z_step_size", "dithering_local_z_mode", + "dithering_local_z_whole_objects", + "dithering_local_z_direct_multicolor", "dithering_step_painted_zones_only", }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 352c22d08f..5a71ea319f 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -292,6 +292,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_whole_objects" + || opt_key == "dithering_local_z_direct_multicolor" || opt_key == "dithering_step_painted_zones_only" || opt_key == "mixed_filament_gradient_mode" || opt_key == "mixed_filament_height_lower_bound" diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 3d0f6483a9..4251885c74 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -77,6 +77,7 @@ struct SubLayerPlan double print_z { 0.0 }; double flow_height { 0.0 }; std::vector painted_masks_by_extruder; + std::vector fixed_painted_masks_by_extruder; ExPolygons base_masks; }; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e9de196f53..2f44fe7374 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1205,6 +1205,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("dithering_z_step_size", true); new_full_config.option("dithering_local_z_mode", true); new_full_config.option("dithering_local_z_whole_objects", true); + new_full_config.option("dithering_local_z_direct_multicolor", 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); @@ -1219,6 +1220,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_config.option("dithering_z_step_size", true); m_config.option("dithering_local_z_mode", true); m_config.option("dithering_local_z_whole_objects", true); + m_config.option("dithering_local_z_direct_multicolor", 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); @@ -1233,6 +1235,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_default_object_config.option("dithering_z_step_size", true); m_default_object_config.option("dithering_local_z_mode", true); m_default_object_config.option("dithering_local_z_whole_objects", true); + m_default_object_config.option("dithering_local_z_direct_multicolor", 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); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 419cd2dbcf..7612e3d8e1 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4300,6 +4300,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("dithering_local_z_direct_multicolor", coBool); + def->label = L("Use direct multicolor Local-Z solver"); + def->category = L("Others"); + def->tooltip = L("Experimental. For mixed rows with 3 or more physical filaments, allocate Local-Z sublayers directly across all components with carry-over error between layers instead of collapsing them into pair cadence.\n\n" + "This can reduce visible banding in multicolor Local-Z blends at the cost of more toolchanges. It is ignored when explicit Local-Z A/B heights are set."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("dithering_step_painted_zones_only", coBool); def->label = L("Use step size in painted zones only"); def->category = L("Others"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index fd724984e3..50256de99d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1369,6 +1369,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, dithering_z_step_size)) ((ConfigOptionBool, dithering_local_z_mode)) ((ConfigOptionBool, dithering_local_z_whole_objects)) + ((ConfigOptionBool, dithering_local_z_direct_multicolor)) ((ConfigOptionBool, dithering_step_painted_zones_only)) ((ConfigOptionString, printer_model)) ((ConfigOptionFloat, resolution)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 58d23f0b20..5f10578a7b 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_local_z_whole_objects" + || opt_key == "dithering_local_z_direct_multicolor" || opt_key == "dithering_step_painted_zones_only" || opt_key == "mixed_filament_component_bias_enabled" || opt_key == "mixed_filament_region_collapse" diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 679923247a..2b7cb91383 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1297,6 +1297,28 @@ static std::vector build_uniform_local_z_pass_heights(double base_height return out; } +static std::vector build_uniform_local_z_pass_heights_exact(double base_height, + double lower_bound, + double upper_bound, + size_t pass_count) +{ + if (base_height <= EPSILON || pass_count == 0) + return {}; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + if (pass_count == 1) + return { base_height }; + + if (double(pass_count) * lo > base_height + EPSILON || double(pass_count) * hi < base_height - EPSILON) + return {}; + + std::vector out(pass_count, base_height / double(pass_count)); + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + return {}; + return out; +} + static inline void compute_local_z_gradient_component_heights(int mix_b_percent, double lower_bound, double upper_bound, double &h_a, double &h_b) { @@ -1891,6 +1913,29 @@ static bool local_z_eligible_mixed_row(const MixedFilament &mf) mf.distribution_mode != int(MixedFilament::SameLayerPointillisme); } +static bool local_z_direct_multicolor_row(const MixedFilament &mf, + size_t num_physical, + std::vector *component_ids = nullptr, + std::vector *component_weights = nullptr) +{ + if (!local_z_eligible_mixed_row(mf)) + return false; + + const std::vector ids = decode_gradient_component_ids(mf, num_physical); + if (ids.size() < 3) + return false; + + if (component_ids != nullptr) + *component_ids = ids; + if (component_weights != nullptr) { + std::vector weights = decode_gradient_component_weights(mf, ids.size()); + if (weights.empty()) + weights.assign(ids.size(), 1); + *component_weights = std::move(weights); + } + return true; +} + struct LocalZActivePair { unsigned int component_a = 0; @@ -1998,6 +2043,170 @@ static std::vector build_local_z_pair_cycle_for_row(const Mixe return out; } +static std::vector build_local_z_direct_multicolor_pass_heights(const MixedFilament &mf, + const std::vector &component_weights, + double base_height, + double lower_bound, + double upper_bound, + size_t component_count) +{ + if (base_height <= EPSILON || component_count == 0) + return {}; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + const size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); + const size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); + if (max_passes == 0) + return { base_height }; + + std::vector positive_weights; + positive_weights.reserve(component_weights.size()); + for (const int weight : component_weights) + if (weight > 0) + positive_weights.emplace_back(weight); + if (positive_weights.empty()) + positive_weights.assign(component_count, 1); + + const int total_weight = std::max(1, std::accumulate(positive_weights.begin(), positive_weights.end(), 0)); + std::vector component_targets; + component_targets.reserve(positive_weights.size()); + size_t ideal_passes = 0; + for (const int weight : positive_weights) { + const double target = base_height * double(weight) / double(total_weight); + component_targets.emplace_back(target); + ideal_passes += size_t(std::max(1.0, std::ceil((target - EPSILON) / hi))); + } + + size_t pass_limit = max_passes; + if (mf.local_z_max_sublayers >= 2) + pass_limit = std::min(pass_limit, size_t(std::max(2, mf.local_z_max_sublayers))); + pass_limit = std::max(pass_limit, min_passes); + + size_t desired_passes = std::clamp(std::max(component_targets.size(), ideal_passes), min_passes, pass_limit); + + std::vector bins = component_targets; + while (bins.size() > desired_passes) { + std::sort(bins.begin(), bins.end()); + const double merged = bins[0] + bins[1]; + bins.erase(bins.begin(), bins.begin() + 2); + bins.emplace_back(merged); + } + + while (bins.size() < desired_passes) { + auto it = std::max_element(bins.begin(), bins.end()); + if (it == bins.end()) + break; + const double value = *it; + if (value < 2.0 * lo - EPSILON) + break; + double first = std::clamp(value * 0.5, lo, value - lo); + double second = value - first; + if (first < lo - EPSILON || second < lo - EPSILON) + break; + *it = first; + bins.emplace_back(second); + } + + if (bins.empty()) + return build_uniform_local_z_pass_heights(base_height, lo, hi, desired_passes); + + std::sort(bins.begin(), bins.end(), std::greater()); + for (double &value : bins) + value = std::clamp(value, lo, hi); + if (fit_pass_heights_to_interval(bins, base_height, lo, hi)) + return bins; + + for (size_t pass_count = desired_passes; pass_count >= min_passes; --pass_count) { + std::vector exact = build_uniform_local_z_pass_heights_exact(base_height, lower_bound, upper_bound, pass_count); + if (!exact.empty()) + return exact; + if (pass_count == min_passes) + break; + } + + return build_uniform_local_z_pass_heights(base_height, lo, hi, desired_passes); +} + +static std::vector build_local_z_direct_multicolor_sequence(const std::vector &component_ids, + const std::vector &component_weights, + const std::vector &pass_heights, + std::vector &carry_error_mm) +{ + if (component_ids.empty() || pass_heights.empty()) + return {}; + + std::vector filtered_ids; + std::vector filtered_weights; + filtered_ids.reserve(component_ids.size()); + filtered_weights.reserve(component_ids.size()); + for (size_t idx = 0; idx < component_ids.size(); ++idx) { + const int weight = idx < component_weights.size() ? std::max(0, component_weights[idx]) : 0; + if (weight <= 0) + continue; + filtered_ids.emplace_back(component_ids[idx]); + filtered_weights.emplace_back(weight); + } + if (filtered_ids.empty()) { + filtered_ids = component_ids; + filtered_weights.assign(component_ids.size(), 1); + } + if (filtered_ids.empty()) + return {}; + + if (carry_error_mm.size() != filtered_ids.size()) + carry_error_mm.assign(filtered_ids.size(), 0.0); + + const double total_height = std::accumulate(pass_heights.begin(), pass_heights.end(), 0.0); + const int total_weight = std::max(1, std::accumulate(filtered_weights.begin(), filtered_weights.end(), 0)); + + std::vector desired_heights(filtered_ids.size(), 0.0); + for (size_t idx = 0; idx < filtered_ids.size(); ++idx) + desired_heights[idx] = total_height * double(filtered_weights[idx]) / double(total_weight) + carry_error_mm[idx]; + + std::vector assigned_heights(filtered_ids.size(), 0.0); + std::vector sequence; + sequence.reserve(pass_heights.size()); + int previous_choice = -1; + + for (const double pass_height : pass_heights) { + size_t best_idx = 0; + double best_score = -std::numeric_limits::infinity(); + double best_need = -std::numeric_limits::infinity(); + for (size_t idx = 0; idx < filtered_ids.size(); ++idx) { + const double remaining_need = desired_heights[idx] - assigned_heights[idx]; + double score = remaining_need; + if (int(idx) == previous_choice) + score -= 0.35 * pass_height; + + if (score > best_score + 1e-9 || + (std::abs(score - best_score) <= 1e-9 && + (remaining_need > best_need + 1e-9 || + (std::abs(remaining_need - best_need) <= 1e-9 && filtered_ids[idx] < filtered_ids[best_idx])))) { + best_idx = idx; + best_score = score; + best_need = remaining_need; + } + } + + assigned_heights[best_idx] += pass_height; + previous_choice = int(best_idx); + sequence.emplace_back(filtered_ids[best_idx]); + } + + for (size_t idx = 0; idx < filtered_ids.size(); ++idx) + carry_error_mm[idx] = desired_heights[idx] - assigned_heights[idx]; + + const double error_sum = std::accumulate(carry_error_mm.begin(), carry_error_mm.end(), 0.0); + if (!carry_error_mm.empty() && std::abs(error_sum) > 1e-9) { + const double correction = error_sum / double(carry_error_mm.size()); + for (double &value : carry_error_mm) + value -= correction; + } + + return sequence; +} + static LocalZActivePair derive_local_z_active_pair(const MixedFilament &mf, const std::vector &pair_cycle, size_t num_physical, @@ -2723,6 +2932,92 @@ static std::vector> local_z_planner_segmentation_with_wh return augmented; } +static std::vector collect_local_z_fixed_state_masks_by_extruder(const std::vector &layer_segmentation, + const size_t num_physical) +{ + std::vector masks_by_extruder(num_physical); + for (size_t channel_idx = 1; channel_idx < layer_segmentation.size(); ++channel_idx) { + const ExPolygons &state_masks = layer_segmentation[channel_idx]; + if (state_masks.empty()) + continue; + + const unsigned int state_id = segmentation_channel_filament_id(channel_idx); + if (state_id == 0 || state_id > num_physical) + continue; + append(masks_by_extruder[state_id - 1], state_masks); + } + for (ExPolygons &masks : masks_by_extruder) + if (masks.size() > 1) + masks = union_ex(masks); + return masks_by_extruder; +} + +static std::vector build_local_z_transition_fixed_masks_for_pass( + const std::vector ¤t_masks_by_extruder, + const std::vector &prev_masks_by_extruder, + const std::vector &next_masks_by_extruder, + const size_t pass_idx, + const size_t pass_count) +{ + if (pass_count <= 1) + return current_masks_by_extruder; + + std::vector pass_masks_by_extruder(current_masks_by_extruder.size()); + const bool is_lowest_pass = pass_idx == 0; + const bool is_highest_pass = pass_idx + 1 >= pass_count; + + for (size_t extruder_idx = 0; extruder_idx < current_masks_by_extruder.size(); ++extruder_idx) { + const ExPolygons ¤t_masks = current_masks_by_extruder[extruder_idx]; + if (current_masks.empty()) + continue; + + const ExPolygons prev_masks = extruder_idx < prev_masks_by_extruder.size() ? prev_masks_by_extruder[extruder_idx] : ExPolygons(); + const ExPolygons next_masks = extruder_idx < next_masks_by_extruder.size() ? next_masks_by_extruder[extruder_idx] : ExPolygons(); + + const ExPolygons current_and_prev = prev_masks.empty() ? ExPolygons() : intersection_ex(current_masks, prev_masks); + const ExPolygons current_and_next = next_masks.empty() ? ExPolygons() : intersection_ex(current_masks, next_masks); + const ExPolygons persistent = + current_and_prev.empty() || current_and_next.empty() ? ExPolygons() : intersection_ex(current_and_prev, current_and_next); + + ExPolygons entering = current_and_next; + if (!entering.empty() && !current_and_prev.empty()) + entering = diff_ex(entering, current_and_prev); + + ExPolygons exiting = current_and_prev; + if (!exiting.empty() && !current_and_next.empty()) + exiting = diff_ex(exiting, current_and_next); + + ExPolygons covered; + if (!persistent.empty()) + append(covered, persistent); + if (!entering.empty()) + append(covered, entering); + if (!exiting.empty()) + append(covered, exiting); + if (covered.size() > 1) + covered = union_ex(covered); + + const ExPolygons isolated = covered.empty() ? current_masks : diff_ex(current_masks, covered); + + ExPolygons assigned; + if (!persistent.empty()) + append(assigned, persistent); + if (is_lowest_pass && !exiting.empty()) + append(assigned, exiting); + if (is_highest_pass) { + if (!entering.empty()) + append(assigned, entering); + if (!isolated.empty()) + append(assigned, isolated); + } + if (assigned.size() > 1) + assigned = union_ex(assigned); + pass_masks_by_extruder[extruder_idx] = std::move(assigned); + } + + return pass_masks_by_extruder; +} + template static void build_local_z_plan(PrintObject &print_object, const std::vector> &segmentation, ThrowOnCancel throw_on_cancel) { @@ -2744,6 +3039,9 @@ static void build_local_z_plan(PrintObject &print_object, const std::vectoroption("dithering_local_z_mode") != nullptr && config->opt_bool("dithering_local_z_mode"); toggle_line("dithering_local_z_whole_objects", local_z_dithering_enabled); + toggle_line("dithering_local_z_direct_multicolor", local_z_dithering_enabled); toggle_line("local_z_wipe_tower_purge_lines", have_prime_tower && !is_BBL_Printer && local_z_dithering_enabled); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 219dc789cc..00bc48cbb2 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -6412,6 +6412,7 @@ void GUI_App::load_current_presets(bool active_preset_combox/*= false*/, bool ch "dithering_z_step_size", "dithering_local_z_mode", "dithering_local_z_whole_objects", + "dithering_local_z_direct_multicolor", "dithering_step_painted_zones_only", "mixed_filament_pointillism_pixel_size", "mixed_filament_pointillism_line_gap", diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5706ef866c..db5c603ba6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5671,6 +5671,10 @@ MixedFilamentDisplayContext build_mixed_filament_display_context(const std::vect if (print_cfg != nullptr && print_cfg->has("wall_loops")) context.preview_settings.wall_loops = std::max(1, size_t(std::max(1, print_cfg->opt_int("wall_loops")))); context.preview_settings.local_z_mode = get_mixed_bool("dithering_local_z_mode", false); + context.preview_settings.local_z_direct_multicolor = + get_mixed_bool("dithering_local_z_direct_multicolor", false) && + context.preview_settings.preferred_a_height <= EPSILON && + context.preview_settings.preferred_b_height <= EPSILON; context.component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false); return context; @@ -5866,6 +5870,26 @@ std::string MixedFilamentConfigPanel::summarize_local_z_breakdown(const MixedFil }; const std::vector ids = decode_gradient_ids(mf.gradient_component_ids); + if (preview_settings.local_z_mode && preview_settings.local_z_direct_multicolor && ids.size() >= 3) { + const std::vector normalized = normalize_gradient_weights(weights, ids.size()); + const size_t effective_sublayers = + mf.local_z_max_sublayers >= 2 ? size_t(std::max(2, mf.local_z_max_sublayers)) : ids.size(); + + std::ostringstream ss; + ss << "Local-Z direct multicolor solver: "; + for (size_t idx = 0; idx < ids.size(); ++idx) { + if (idx > 0) + ss << ", "; + const int pct = idx < normalized.size() ? normalized[idx] : 0; + ss << 'F' << ids[idx] << ' ' << pct << '%'; + } + ss << ".\nCarry-over error is distributed directly across all " << ids.size() + << " components instead of collapsing them into pair cadence."; + if (mf.local_z_max_sublayers >= 2) + ss << "\nEffective Local-Z cap: up to " << effective_sublayers << " sublayers per nominal layer."; + return ss.str(); + } + if (ids.size() >= 4) { const std::vector normalized = normalize_gradient_weights(weights, ids.size()); const std::vector pair_tokens = { 1, 2 }; @@ -7281,6 +7305,10 @@ 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 local_z_direct_multicolor = + get_mixed_bool("dithering_local_z_direct_multicolor", false) && + preferred_local_z_a <= EPSILON && + preferred_local_z_b <= EPSILON; 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)); @@ -7294,6 +7322,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager) preferred_local_z_a, preferred_local_z_b, local_z_mode, + local_z_direct_multicolor, wall_loops }; const MixedFilamentDisplayContext display_context { @@ -10493,6 +10522,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ "dithering_z_step_size", "dithering_local_z_mode", "dithering_local_z_whole_objects", + "dithering_local_z_direct_multicolor", "dithering_step_painted_zones_only" }; 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 f1df96b4bb..d4681cfeec 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1519,6 +1519,19 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) field->set_value(boost::any(false), false); update_dirty(); } + if (!local_z_enabled && + m_config->has("dithering_local_z_direct_multicolor") && + m_config->option("dithering_local_z_direct_multicolor") != nullptr && + m_config->opt_bool("dithering_local_z_direct_multicolor")) { + change_opt_value(*m_config, "dithering_local_z_direct_multicolor", boost::any(false)); + if (m_type == Preset::TYPE_PRINT) { + DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; + project_cfg.set_key_value("dithering_local_z_direct_multicolor", new ConfigOptionBool(false)); + } + if (Field *field = this->get_field("dithering_local_z_direct_multicolor")) + field->set_value(boost::any(false), false); + update_dirty(); + } } @@ -1829,6 +1842,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) opt_key == "dithering_z_step_size" || opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_whole_objects" || + opt_key == "dithering_local_z_direct_multicolor" || opt_key == "dithering_step_painted_zones_only" || opt_key == "mixed_filament_definitions")) { DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; @@ -2574,6 +2588,7 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("dithering_z_step_size"); optgroup->append_single_option_line("dithering_local_z_mode"); optgroup->append_single_option_line("dithering_local_z_whole_objects"); + optgroup->append_single_option_line("dithering_local_z_direct_multicolor"); optgroup->append_single_option_line("dithering_step_painted_zones_only"); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); diff --git a/version.inc b/version.inc index a7ce9ea4e7..f4453ba6e4 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.7") -set(FULLSPECTRUM_VERSION "0.9.7") +set(Snapmaker_VERSION "0.9.8") +set(FULLSPECTRUM_VERSION "0.9.8") set(MIN_FIRM_VER "1.0.0") string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" Snapmaker_VERSION_MATCH ${Snapmaker_VERSION})