From 112e2e5ef557bc6bdeaedae81b66eaafeda47b1f Mon Sep 17 00:00:00 2001 From: Rad Date: Wed, 11 Feb 2026 03:15:16 +0100 Subject: [PATCH] Add local Z dithering features: Implement local Z height adjustments and clipping for extrusion paths. Introduce new configuration options for local Z dithering in PrintConfig and GUI, enhancing mixed filament management. Update MixedFilamentManager to support height-weighted cadence and integrate local Z settings into the printing process. --- src/libslic3r/GCode.cpp | 574 +++++++++++++++++++++++- src/libslic3r/MixedFilament.cpp | 10 +- src/libslic3r/MixedFilament.hpp | 3 +- src/libslic3r/Print.cpp | 1 + src/libslic3r/Print.hpp | 40 ++ src/libslic3r/PrintApply.cpp | 3 + src/libslic3r/PrintConfig.cpp | 8 + src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 13 +- src/libslic3r/PrintObjectSlice.cpp | 672 ++++++++++++++++++++++++++++- src/slic3r/GUI/Tab.cpp | 6 + 11 files changed, 1310 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 769095e194..73029b7785 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3293,6 +3293,153 @@ inline GCode::ObjectByExtruder& object_by_extruder(std::map EPSILON) { + const double ratio = flow_height_override / path.height; + path.mm3_per_mm *= ratio; + } + path.height = float(flow_height_override); +} + +static inline void append_clipped_path(const ExtrusionPath& src_path, + const ExPolygons* include_masks, + const ExPolygons* exclude_masks, + const double flow_height_override, + ExtrusionEntityCollection& dst) +{ + Polylines segments{src_path.polyline}; + if (include_masks != nullptr && !include_masks->empty()) + segments = intersection_pl(std::move(segments), *include_masks); + if (exclude_masks != nullptr && !exclude_masks->empty()) + segments = diff_pl(std::move(segments), *exclude_masks); + + for (Polyline& segment : segments) { + if (!segment.is_valid()) + continue; + ExtrusionPath clipped(segment, src_path); + apply_local_z_flow_height_override(clipped, flow_height_override); + dst.append(std::move(clipped)); + } +} + +static inline ExPolygons local_z_compensate_masks(const ExPolygons& src_masks, + const float delta_scaled, + const bool fallback_to_source) +{ + if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON) + return src_masks; + + ExPolygons compensated = offset_ex(src_masks, delta_scaled); + if (!compensated.empty() && compensated.size() > 1) + compensated = union_ex(compensated); + + if (compensated.empty() && fallback_to_source) + return src_masks; + return compensated; +} + +struct LocalZPathHeightStats +{ + size_t count { 0 }; + double min { std::numeric_limits::max() }; + double max { 0.0 }; +}; + +static inline LocalZPathHeightStats collect_local_z_path_height_stats(const ExtrusionEntityCollection& source) +{ + LocalZPathHeightStats stats; + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity* entity : flattened.entities) { + if (const auto* path = dynamic_cast(entity)) { + const double h = path->height; + ++stats.count; + stats.min = std::min(stats.min, h); + stats.max = std::max(stats.max, h); + } else if (const auto* multipath = dynamic_cast(entity)) { + for (const ExtrusionPath& p : multipath->paths) { + const double h = p.height; + ++stats.count; + stats.min = std::min(stats.min, h); + stats.max = std::max(stats.max, h); + } + } else if (const auto* loop = dynamic_cast(entity)) { + for (const ExtrusionPath& p : loop->paths) { + const double h = p.height; + ++stats.count; + stats.min = std::min(stats.min, h); + stats.max = std::max(stats.max, h); + } + } + } + if (stats.count == 0) { + stats.min = 0.; + stats.max = 0.; + } + return stats; +} + +static inline Polylines collect_local_z_polylines(const ExtrusionEntityCollection& source) +{ + Polylines lines; + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity* entity : flattened.entities) { + if (const auto* path = dynamic_cast(entity)) { + lines.emplace_back(path->polyline); + } else if (const auto* multipath = dynamic_cast(entity)) { + for (const ExtrusionPath& p : multipath->paths) + lines.emplace_back(p.polyline); + } else if (const auto* loop = dynamic_cast(entity)) { + for (const ExtrusionPath& p : loop->paths) + lines.emplace_back(p.polyline); + } + } + return lines; +} + +static std::unique_ptr clip_extrusion_collection_for_local_z( + const ExtrusionEntityCollection& source, + const ExPolygons* include_masks, + const ExPolygons* exclude_masks, + const double flow_height_override) +{ + if (source.entities.empty()) + return nullptr; + + if ((include_masks == nullptr || include_masks->empty()) && + (exclude_masks == nullptr || exclude_masks->empty()) && + flow_height_override <= EPSILON) { + return std::make_unique(source); + } + + auto out = std::make_unique(); + out->no_sort = source.no_sort; + + ExtrusionEntityCollection flattened = source.flatten(false); + for (const ExtrusionEntity* entity : flattened.entities) { + if (const auto* path = dynamic_cast(entity)) { + append_clipped_path(*path, include_masks, exclude_masks, flow_height_override, *out); + } else if (const auto* multipath = dynamic_cast(entity)) { + for (const ExtrusionPath& path : multipath->paths) + append_clipped_path(path, include_masks, exclude_masks, flow_height_override, *out); + } else if (const auto* loop = dynamic_cast(entity)) { + for (const ExtrusionPath& path : loop->paths) + append_clipped_path(path, include_masks, exclude_masks, flow_height_override, *out); + } else { + // Fallback for unknown entity subclasses: keep behavior unchanged for now. + if (include_masks == nullptr && exclude_masks == nullptr && flow_height_override <= EPSILON) + out->append(*entity); + } + } + + if (out->entities.empty()) + return nullptr; + + return out; +} + inline std::vector& object_islands_by_extruder( std::map>& by_extruder, unsigned int extruder_id, @@ -3991,7 +4138,189 @@ LayerResult GCode::process_layer(const Print& print, // Group extrusions by an extruder, then by an object, an island and a region. std::map> by_extruder; bool is_anything_overridden = const_cast(layer_tools).wiping_extrusions().is_anything_overridden(); + // Compensate perimeter clipping at mixed-mask boundaries to avoid cracks from exact centerline clipping. + constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10; + // Keep base exclusion smaller than mixed-pass inclusion to guarantee a slight overlap + // instead of a moat at the boundary. + 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)); + + struct LocalZPassBucket { + const SubLayerPlan* plan { nullptr }; + std::vector compensated_masks_by_extruder; + std::map> by_extruder; + }; + struct LocalZLayerContext { + bool enabled { false }; + ExPolygons raw_mixed_masks_union; + ExPolygons mixed_masks_union; + ExPolygons mixed_masks_union_for_base_exclude; + size_t local_clipped_collections { 0 }; + size_t base_clipped_collections { 0 }; + size_t base_clip_leak_warnings { 0 }; + std::vector pass_buckets; + }; + + const bool local_z_perimeter_runtime_supported = !has_wipe_tower && !is_anything_overridden; + bool local_z_phase_b_requested_for_layer = false; + std::vector local_z_layer_contexts; + std::vector> local_z_clipped_collections; + size_t local_z_rejected_context_logs = 0; + if (local_z_perimeter_runtime_supported) { + local_z_layer_contexts.resize(layers.size()); + for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) { + const LayerToPrint& layer_to_print = layers[layer_to_print_idx]; + if (layer_to_print.object_layer == nullptr) + continue; + + const PrintObject* print_object = layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); + if (print_object == nullptr) + continue; + + const size_t layer_id = size_t(layer_to_print.object_layer->id()); + const auto& intervals = print_object->local_z_intervals(); + const auto& plans = print_object->local_z_sublayer_plan(); + if (intervals.empty() || plans.empty()) + continue; + + auto it_interval = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval& interval) { + return interval.layer_id == layer_id; + }); + if (it_interval == intervals.end()) + continue; + if (!it_interval->has_mixed_paint || it_interval->sublayer_count <= 1 || it_interval->first_sublayer_idx >= plans.size()) + continue; + local_z_phase_b_requested_for_layer = true; + + LocalZLayerContext& ctx = local_z_layer_contexts[layer_to_print_idx]; + ctx.enabled = true; + const size_t first_idx = it_interval->first_sublayer_idx; + const size_t end_idx = std::min(plans.size(), first_idx + it_interval->sublayer_count); + size_t split_plan_count = 0; + size_t split_plan_with_raw_masks = 0; + size_t split_plan_with_compensated_masks = 0; + size_t raw_mask_polygon_count = 0; + size_t compensated_mask_polygon_count = 0; + ExPolygons raw_mixed_masks_union; + for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { + const SubLayerPlan& plan = plans[plan_idx]; + if (!plan.split_interval) + continue; + ++split_plan_count; + + LocalZPassBucket bucket; + bucket.plan = &plan; + bucket.compensated_masks_by_extruder.assign(plan.painted_masks_by_extruder.size(), 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()) + 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); + if (!compensated.empty()) { + pass_has_compensated_masks = true; + compensated_mask_polygon_count += compensated.size(); + } + bucket.compensated_masks_by_extruder[extruder_id] = std::move(compensated); + } + if (pass_has_raw_masks) + ++split_plan_with_raw_masks; + if (pass_has_compensated_masks) { + ++split_plan_with_compensated_masks; + ctx.pass_buckets.emplace_back(std::move(bucket)); + + const LocalZPassBucket& appended_bucket = ctx.pass_buckets.back(); + for (const ExPolygons& masks : appended_bucket.compensated_masks_by_extruder) + append(ctx.mixed_masks_union, masks); + } + } + if (!raw_mixed_masks_union.empty() && raw_mixed_masks_union.size() > 1) + raw_mixed_masks_union = union_ex(raw_mixed_masks_union); + ctx.raw_mixed_masks_union = raw_mixed_masks_union; + if (!ctx.mixed_masks_union.empty() && ctx.mixed_masks_union.size() > 1) { + ExPolygons merged_masks = union_ex(ctx.mixed_masks_union); + if (!merged_masks.empty()) + ctx.mixed_masks_union = std::move(merged_masks); + else if (!raw_mixed_masks_union.empty()) + ctx.mixed_masks_union = raw_mixed_masks_union; + } + if (ctx.mixed_masks_union.empty() && !raw_mixed_masks_union.empty()) + ctx.mixed_masks_union = raw_mixed_masks_union; + const ExPolygons &base_exclude_source = !ctx.raw_mixed_masks_union.empty() ? ctx.raw_mixed_masks_union : ctx.mixed_masks_union; + if (!base_exclude_source.empty()) { + ctx.mixed_masks_union_for_base_exclude = + local_z_compensate_masks(base_exclude_source, local_z_base_mask_expand, true); + } + if (ctx.pass_buckets.empty() || ctx.mixed_masks_union.empty()) { + ctx.enabled = false; + if (local_z_rejected_context_logs < 50) { + ++local_z_rejected_context_logs; + BOOST_LOG_TRIVIAL(warning) << "Local-Z context rejected" + << " print_z=" << print_z + << " layer_id=" << layer_id + << " first_idx=" << first_idx + << " end_idx=" << end_idx + << " interval_sublayer_count=" << it_interval->sublayer_count + << " split_plan_count=" << split_plan_count + << " split_plan_with_raw_masks=" << split_plan_with_raw_masks + << " split_plan_with_compensated_masks=" << split_plan_with_compensated_masks + << " raw_mask_polygon_count=" << raw_mask_polygon_count + << " compensated_mask_polygon_count=" << compensated_mask_polygon_count + << " pass_buckets=" << ctx.pass_buckets.size() + << " mixed_mask_count=" << ctx.mixed_masks_union.size(); + } + } + BOOST_LOG_TRIVIAL(debug) << "Local-Z context" + << " print_z=" << print_z + << " layer_id=" << layer_id + << " enabled=" << ctx.enabled + << " split_pass_count=" << ctx.pass_buckets.size() + << " mixed_mask_count=" << ctx.mixed_masks_union.size() + << " base_exclude_mask_count=" << ctx.mixed_masks_union_for_base_exclude.size() + << " perimeter_mask_expand_mm=" << LOCAL_Z_PERIMETER_MASK_EXPAND_MM + << " base_mask_expand_mm=" << LOCAL_Z_BASE_MASK_EXPAND_MM; + } + } else { + for (const LayerToPrint& layer_to_print : layers) { + if (layer_to_print.object_layer == nullptr) + continue; + const PrintObject* print_object = layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object(); + if (print_object == nullptr) + continue; + const size_t layer_id = size_t(layer_to_print.object_layer->id()); + const auto& intervals = print_object->local_z_intervals(); + auto it_interval = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval& interval) { + return interval.layer_id == layer_id; + }); + if (it_interval != intervals.end() && it_interval->has_mixed_paint && it_interval->sublayer_count > 1) { + local_z_phase_b_requested_for_layer = true; + break; + } + } + } + + const bool local_z_perimeter_phase_b_enabled = + local_z_perimeter_runtime_supported && + std::any_of(local_z_layer_contexts.begin(), local_z_layer_contexts.end(), [](const LocalZLayerContext& ctx) { return ctx.enabled; }); + if (local_z_phase_b_requested_for_layer && !local_z_perimeter_runtime_supported) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z phase-b disabled" + << " print_z=" << print_z + << " wipe_tower=" << has_wipe_tower + << " wiping_overrides=" << is_anything_overridden; + gcode += "; local-z perimeter phase-b disabled for this layer (wipe tower or wiping overrides active)\n"; + } else if (local_z_phase_b_requested_for_layer && !local_z_perimeter_phase_b_enabled) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z phase-b requested but no eligible contexts" + << " print_z=" << print_z + << " runtime_supported=" << local_z_perimeter_runtime_supported; + } + for (const LayerToPrint& layer_to_print : layers) { + const size_t layer_to_print_idx = &layer_to_print - layers.data(); if (layer_to_print.support_layer != nullptr) { const SupportLayer& support_layer = *layer_to_print.support_layer; const PrintObject& object = *layer_to_print.original_object; @@ -4074,12 +4403,11 @@ LayerResult GCode::process_layer(const Print& print, bool single_extruder = !has_support || support_extruder == interface_extruder; // Assign an extruder to the base. ObjectByExtruder& obj = object_by_extruder(by_extruder, has_support ? support_extruder : interface_extruder, - &layer_to_print - layers.data(), layers.size()); + layer_to_print_idx, layers.size()); obj.support = &support_layer.support_fills; obj.support_extrusion_role = single_extruder ? erMixed : erSupportMaterial; if (!single_extruder && has_interface) { - ObjectByExtruder& obj_interface = object_by_extruder(by_extruder, interface_extruder, &layer_to_print - layers.data(), - layers.size()); + ObjectByExtruder& obj_interface = object_by_extruder(by_extruder, interface_extruder, layer_to_print_idx, layers.size()); obj_interface.support = &support_layer.support_fills; obj_interface.support_extrusion_role = erSupportMaterialInterface; } @@ -4088,6 +4416,10 @@ LayerResult GCode::process_layer(const Print& print, if (layer_to_print.object_layer != nullptr) { const Layer& layer = *layer_to_print.object_layer; + LocalZLayerContext* local_z_ctx = + (local_z_perimeter_phase_b_enabled && layer_to_print_idx < local_z_layer_contexts.size() && local_z_layer_contexts[layer_to_print_idx].enabled) + ? &local_z_layer_contexts[layer_to_print_idx] + : nullptr; // We now define a strategy for building perimeters and fills. The separation // between regions doesn't matter in terms of printing order, as we follow // another logic instead: @@ -4138,8 +4470,95 @@ LayerResult GCode::process_layer(const Print& print, if (extrusions->entities.empty()) // This shouldn't happen but first_point() would fail. continue; + const ExtrusionEntityCollection* filtered_extrusions = extrusions; + if (entity_type == ObjectByExtruder::Island::Region::PERIMETERS && + local_z_ctx != nullptr && !local_z_ctx->mixed_masks_union.empty()) { + for (LocalZPassBucket& pass_bucket : local_z_ctx->pass_buckets) { + if (pass_bucket.plan == nullptr) + continue; + for (size_t pass_extruder_id = 0; pass_extruder_id < pass_bucket.plan->painted_masks_by_extruder.size(); + ++pass_extruder_id) { + const ExPolygons& pass_masks = pass_bucket.compensated_masks_by_extruder.empty() + ? pass_bucket.plan->painted_masks_by_extruder[pass_extruder_id] + : pass_bucket.compensated_masks_by_extruder[pass_extruder_id]; + if (pass_masks.empty()) + continue; + auto clipped_local = clip_extrusion_collection_for_local_z(*extrusions, &pass_masks, nullptr, pass_bucket.plan->flow_height); + if (!clipped_local) + continue; + + const ExtrusionEntityCollection* clipped_ptr = clipped_local.get(); + const LocalZPathHeightStats local_height_stats = collect_local_z_path_height_stats(*clipped_ptr); + ++local_z_ctx->local_clipped_collections; + if (local_height_stats.count > 0 && + (std::abs(local_height_stats.min - pass_bucket.plan->flow_height) > 1e-3 || + std::abs(local_height_stats.max - pass_bucket.plan->flow_height) > 1e-3)) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z local pass height mismatch" + << " print_z=" << print_z + << " layer_to_print_idx=" << layer_to_print_idx + << " plan_layer_id=" << pass_bucket.plan->layer_id + << " pass_index=" << pass_bucket.plan->pass_index + << " expected_height=" << pass_bucket.plan->flow_height + << " observed_min=" << local_height_stats.min + << " observed_max=" << local_height_stats.max + << " path_count=" << local_height_stats.count; + } + local_z_clipped_collections.emplace_back(std::move(clipped_local)); + std::vector& islands = object_islands_by_extruder( + pass_bucket.by_extruder, unsigned(pass_extruder_id), layer_to_print_idx, layers.size(), n_slices + 1); + + for (size_t i = 0; i <= n_slices; ++i) { + const bool last = i == n_slices; + const size_t island_idx = last ? n_slices : slices_test_order[i]; + if (last || point_inside_surface(island_idx, clipped_ptr->first_point())) { + if (islands[island_idx].by_region.empty()) + islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region()); + islands[island_idx].by_region[region.print_region_id()].append( + ObjectByExtruder::Island::Region::PERIMETERS, clipped_ptr, nullptr); + break; + } + } + } + } + + const ExPolygons* base_exclude_masks = + local_z_ctx->mixed_masks_union_for_base_exclude.empty() ? &local_z_ctx->mixed_masks_union + : &local_z_ctx->mixed_masks_union_for_base_exclude; + auto clipped_base = clip_extrusion_collection_for_local_z(*extrusions, nullptr, base_exclude_masks, 0.); + if (!clipped_base) + continue; + const LocalZPathHeightStats base_height_stats = collect_local_z_path_height_stats(*clipped_base); + ++local_z_ctx->base_clipped_collections; + const ExPolygons &mixed_leak_ref = + !local_z_ctx->raw_mixed_masks_union.empty() ? local_z_ctx->raw_mixed_masks_union : local_z_ctx->mixed_masks_union; + if (local_z_ctx->base_clip_leak_warnings < 3 && !mixed_leak_ref.empty()) { + Polylines clipped_base_lines = collect_local_z_polylines(*clipped_base); + if (!clipped_base_lines.empty()) { + Polylines leaked_segments = intersection_pl(std::move(clipped_base_lines), mixed_leak_ref); + if (!leaked_segments.empty()) { + ++local_z_ctx->base_clip_leak_warnings; + BOOST_LOG_TRIVIAL(warning) << "Local-Z base clip leak" + << " print_z=" << print_z + << " layer_to_print_idx=" << layer_to_print_idx + << " leaked_segment_count=" << leaked_segments.size() + << " warn_index=" << local_z_ctx->base_clip_leak_warnings; + } + } + } + if (base_height_stats.count > 0 && local_z_ctx->base_clipped_collections <= 5) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z base clip heights" + << " print_z=" << print_z + << " layer_to_print_idx=" << layer_to_print_idx + << " observed_min=" << base_height_stats.min + << " observed_max=" << base_height_stats.max + << " path_count=" << base_height_stats.count; + } + filtered_extrusions = clipped_base.get(); + local_z_clipped_collections.emplace_back(std::move(clipped_base)); + } + // This extrusion is part of certain Region, which tells us which extruder should be used for it: - int correct_extruder_id = layer_tools.extruder(*extrusions, region); + int correct_extruder_id = layer_tools.extruder(*filtered_extrusions, region); // Let's recover vector of extruder overrides: const WipingExtrusions::ExtruderPerCopy* entity_overrides = nullptr; @@ -4153,7 +4572,7 @@ LayerResult GCode::process_layer(const Print& print, if (is_anything_overridden) { entity_overrides = const_cast(layer_tools) .wiping_extrusions() - .get_extruder_overrides(extrusions, layer_to_print.original_object, correct_extruder_id, + .get_extruder_overrides(filtered_extrusions, layer_to_print.original_object, correct_extruder_id, layer_to_print.object()->instances().size()); if (entity_overrides == nullptr) { printing_extruders.emplace_back(correct_extruder_id); @@ -4173,19 +4592,18 @@ LayerResult GCode::process_layer(const Print& print, // Now we must add this extrusion into the by_extruder map, once for each extruder that will print it: for (unsigned int extruder : printing_extruders) { - std::vector& islands = object_islands_by_extruder(by_extruder, extruder, - &layer_to_print - layers.data(), - layers.size(), n_slices + 1); + std::vector& islands = + object_islands_by_extruder(by_extruder, extruder, layer_to_print_idx, layers.size(), n_slices + 1); for (size_t i = 0; i <= n_slices; ++i) { bool last = i == n_slices; size_t island_idx = last ? n_slices : slices_test_order[i]; if ( // extrusions->first_point does not fit inside any slice last || // extrusions->first_point fits inside ith slice - point_inside_surface(island_idx, extrusions->first_point())) { + point_inside_surface(island_idx, filtered_extrusions->first_point())) { if (islands[island_idx].by_region.empty()) islands[island_idx].by_region.assign(print.num_print_regions(), ObjectByExtruder::Island::Region()); - islands[island_idx].by_region[region.print_region_id()].append(entity_type, extrusions, + islands[island_idx].by_region[region.print_region_id()].append(entity_type, filtered_extrusions, entity_overrides); break; } @@ -4197,9 +4615,145 @@ LayerResult GCode::process_layer(const Print& print, } } // for objects + if (local_z_perimeter_phase_b_enabled) { + for (size_t layer_to_print_idx = 0; layer_to_print_idx < local_z_layer_contexts.size(); ++layer_to_print_idx) { + const LocalZLayerContext& ctx = local_z_layer_contexts[layer_to_print_idx]; + if (!ctx.enabled) + continue; + BOOST_LOG_TRIVIAL(debug) << "Local-Z clipping summary" + << " print_z=" << print_z + << " layer_to_print_idx=" << layer_to_print_idx + << " local_collections=" << ctx.local_clipped_collections + << " base_collections=" << ctx.base_clipped_collections + << " pass_count=" << ctx.pass_buckets.size(); + } + } + if (m_wipe_tower) m_wipe_tower->set_is_first_print(true); + struct LocalZPassRef { + size_t layer_to_print_idx { 0 }; + LocalZPassBucket* bucket { nullptr }; + }; + + std::vector local_z_pass_refs; + if (local_z_perimeter_phase_b_enabled) { + for (size_t layer_to_print_idx = 0; layer_to_print_idx < local_z_layer_contexts.size(); ++layer_to_print_idx) { + LocalZLayerContext& ctx = local_z_layer_contexts[layer_to_print_idx]; + if (!ctx.enabled) + continue; + for (LocalZPassBucket& bucket : ctx.pass_buckets) { + if (bucket.plan != nullptr && !bucket.by_extruder.empty()) + local_z_pass_refs.push_back(LocalZPassRef{layer_to_print_idx, &bucket}); + } + } + std::sort(local_z_pass_refs.begin(), local_z_pass_refs.end(), [](const LocalZPassRef& lhs, const LocalZPassRef& rhs) { + assert(lhs.bucket != nullptr && rhs.bucket != nullptr); + assert(lhs.bucket->plan != nullptr && rhs.bucket->plan != nullptr); + if (lhs.bucket->plan->print_z != rhs.bucket->plan->print_z) + return lhs.bucket->plan->print_z < rhs.bucket->plan->print_z; + if (lhs.layer_to_print_idx != rhs.layer_to_print_idx) + return lhs.layer_to_print_idx < rhs.layer_to_print_idx; + return lhs.bucket->plan->pass_index < rhs.bucket->plan->pass_index; + }); + } + + if (local_z_perimeter_phase_b_enabled) { + BOOST_LOG_TRIVIAL(info) << "Local-Z phase-b prepared" + << " print_z=" << print_z + << " perimeter_passes=" << local_z_pass_refs.size(); + if (local_z_pass_refs.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z phase-b enabled but produced no perimeter passes" + << " print_z=" << print_z; + } + } + + if (!local_z_pass_refs.empty()) { + BOOST_LOG_TRIVIAL(info) << "Local-Z phase-b emitting" + << " print_z=" << print_z + << " perimeter_passes=" << local_z_pass_refs.size(); + gcode += "; local-z phase-b perimeter passes begin\n"; + for (const LocalZPassRef& pass_ref : local_z_pass_refs) { + assert(pass_ref.bucket != nullptr && pass_ref.bucket->plan != nullptr); + const SubLayerPlan& pass_plan = *pass_ref.bucket->plan; + const double pass_z = pass_plan.print_z + m_config.z_offset.value; + const double saved_nominal_z = m_nominal_z; + const float saved_last_layer_z = m_last_layer_z; + // Ensure all travel/lift logic inside this pass references the micro-pass Z, + // not the base layer nominal Z. + m_nominal_z = pass_z; + m_last_layer_z = float(pass_z); + BOOST_LOG_TRIVIAL(debug) << "Local-Z pass emit" + << " print_z=" << print_z + << " layer_to_print_idx=" << pass_ref.layer_to_print_idx + << " layer_id=" << pass_plan.layer_id + << " pass_index=" << pass_plan.pass_index + << " pass_print_z=" << pass_plan.print_z + << " pass_flow_height=" << pass_plan.flow_height + << " extruder_buckets=" << pass_ref.bucket->by_extruder.size(); + if (std::abs(m_writer.get_position().z() - pass_z) > EPSILON) { + gcode += this->retract(false, false, LiftType::NormalLift); + gcode += m_writer.travel_to_z(pass_z, "Local-Z perimeter pass"); + } + + for (auto& by_extruder_entry : pass_ref.bucket->by_extruder) { + const unsigned int local_extruder_id = by_extruder_entry.first; + std::vector& objects_by_extruder = by_extruder_entry.second; + if (objects_by_extruder.empty()) + continue; + + gcode += this->set_extruder(local_extruder_id, pass_plan.print_z); + if (std::abs(m_writer.get_position().z() - pass_z) > EPSILON) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z pass z restore" + << " print_z=" << print_z + << " layer_id=" << pass_plan.layer_id + << " pass_index=" << pass_plan.pass_index + << " extruder=" << local_extruder_id + << " expected_pass_z=" << pass_z + << " observed_z_after_toolchange=" << m_writer.get_position().z(); + gcode += m_writer.travel_to_z(pass_z, "Local-Z pass z restore"); + } + std::vector instances_to_print = + sort_print_object_instances(objects_by_extruder, layers, ordering, single_object_instance_idx); + + for (InstanceToPrint& instance_to_print : instances_to_print) { + const LayerToPrint& layer_to_print = layers[instance_to_print.layer_id]; + const bool object_layer_over_raft = + layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 && + instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id(); + + m_config.apply(instance_to_print.print_object.config(), true); + m_layer = layer_to_print.layer(); + m_object_layer_over_raft = object_layer_over_raft; + if (m_config.reduce_crossing_wall) + m_avoid_crossing_perimeters.init_layer(*m_layer); + + const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; + std::pair this_object_copy(&instance_to_print.print_object, offset); + if (m_last_obj_copy != this_object_copy) + m_avoid_crossing_perimeters.use_external_mp_once(); + m_last_obj_copy = this_object_copy; + this->set_origin(unscale(offset)); + + for (ObjectByExtruder::Island& island : instance_to_print.object_by_extruder.islands) { + gcode += this->extrude_perimeters(print, island.by_region, first_layer, false); + gcode += this->extrude_perimeters(print, island.by_region, first_layer, true); + } + } + } + m_nominal_z = saved_nominal_z; + m_last_layer_z = saved_last_layer_z; + } + + const double nominal_layer_z = print_z + m_config.z_offset.value; + if (std::abs(m_writer.get_position().z() - nominal_layer_z) > EPSILON) { + gcode += this->retract(false, false, LiftType::NormalLift); + gcode += m_writer.travel_to_z(nominal_layer_z, "Local-Z return to nominal layer"); + } + gcode += "; local-z phase-b perimeter passes end\n"; + } + // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. for (unsigned int extruder_id : layer_tools.extruders) { if (print.config().skirt_type == stCombined && !print.skirt().empty()) diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index d8cd912120..2ed842126e 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -486,7 +486,8 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, size_t num_physical, int layer_index, float layer_print_z, - float layer_height) const + float layer_height, + bool force_height_weighted) const { if (!is_mixed(filament_id, num_physical)) return filament_id; @@ -497,9 +498,10 @@ unsigned int MixedFilamentManager::resolve(unsigned int filament_id, const MixedFilament &mf = m_mixed[idx]; - // Height-weighted cadence for custom rows uses Z-height windows rather - // than integer layer counts. - if (m_gradient_mode == 1 && mf.custom) { + // Height-weighted cadence can be forced by the local-Z planner. The + // regular gradient height mode keeps historical behavior (custom rows). + const bool use_height_weighted = force_height_weighted || (m_gradient_mode == 1 && mf.custom); + if (use_height_weighted) { float h_a = 0.f; float h_b = 0.f; compute_gradient_heights(mf, m_height_lower_bound, m_height_upper_bound, h_a, h_b); diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 979ac5de76..8ac54a6f51 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -108,7 +108,8 @@ public: size_t num_physical, int layer_index, float layer_print_z = 0.f, - float layer_height = 0.f) const; + float layer_height = 0.f, + bool force_height_weighted = false) const; // Compute a display colour by blending in RYB pigment space. static std::string blend_color(const std::string &color_a, diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 18fc13d378..33f21b0586 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -246,6 +246,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "resolution" || opt_key == "precise_z_height" || opt_key == "dithering_z_step_size" + || opt_key == "dithering_local_z_mode" || 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 df789c2596..396c15cbd5 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -54,6 +54,32 @@ struct groupedVolumeSlices ExPolygons slices; }; +// Phase A local-Z dithering planner cache. +struct LocalZInterval +{ + size_t layer_id { 0 }; + double z_lo { 0.0 }; + double z_hi { 0.0 }; + double base_height { 0.0 }; + double sublayer_height { 0.0 }; + bool has_mixed_paint { false }; + size_t first_sublayer_idx { 0 }; + size_t sublayer_count { 0 }; +}; + +struct SubLayerPlan +{ + size_t layer_id { 0 }; + size_t pass_index { 0 }; + bool split_interval { false }; + double z_lo { 0.0 }; + double z_hi { 0.0 }; + double print_z { 0.0 }; + double flow_height { 0.0 }; + std::vector painted_masks_by_extruder; + ExPolygons base_masks; +}; + enum SupportNecessaryType { NoNeedSupp=0, SharpTail, @@ -399,6 +425,18 @@ public: SupportLayer* add_tree_support_layer(int id, coordf_t height, coordf_t print_z, coordf_t slice_z); std::shared_ptr alloc_tree_support_preview_cache(); void clear_tree_support_preview_cache() { m_tree_support_preview_cache.reset(); } + const std::vector& local_z_intervals() const { return m_local_z_intervals; } + const std::vector& local_z_sublayer_plan() const { return m_local_z_sublayer_plan; } + void set_local_z_plan(std::vector intervals, std::vector sublayers) + { + m_local_z_intervals = std::move(intervals); + m_local_z_sublayer_plan = std::move(sublayers); + } + void clear_local_z_plan() + { + m_local_z_intervals.clear(); + m_local_z_sublayer_plan.clear(); + } size_t support_layer_count() const { return m_support_layers.size(); } void clear_support_layers(); @@ -547,6 +585,8 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; SupportLayerPtrs m_support_layers; + std::vector m_local_z_intervals; + std::vector m_local_z_sublayer_plan; // BBS std::shared_ptr m_tree_support_preview_cache; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index e1dc8257d6..bba3848a85 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1094,6 +1094,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("printer_settings_id", true); // Ensure newly introduced dithering keys are present so in-session updates are detected. new_full_config.option("dithering_z_step_size", true); + new_full_config.option("dithering_local_z_mode", 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); @@ -1102,6 +1103,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_definitions", true); m_config.option("dithering_z_step_size", true); + m_config.option("dithering_local_z_mode", 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); @@ -1110,6 +1112,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_config.option("mixed_filament_advanced_dithering", true); m_config.option("mixed_filament_definitions", true); m_default_object_config.option("dithering_z_step_size", true); + m_default_object_config.option("dithering_local_z_mode", 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 9918194379..85a99d193e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4213,6 +4213,14 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.0)); + def = this->add("dithering_local_z_mode", coBool); + def->label = L("Local Z dithering mode"); + def->category = L("Others"); + def->tooltip = L("Experimental local mixed-zone Z mode: split only mixed-painted zones into local Z passes while keeping base regions on the nominal layer cadence when possible.\n\n" + "Current implementation focuses on perimeter validation first and may not yet cover all toolpath types."); + 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 d59f8329dd..ca395d3fb6 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1360,6 +1360,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, mixed_filament_advanced_dithering)) ((ConfigOptionString, mixed_filament_definitions)) ((ConfigOptionFloat, dithering_z_step_size)) + ((ConfigOptionBool, dithering_local_z_mode)) ((ConfigOptionBool, dithering_step_painted_zones_only)) ((ConfigOptionString, printer_model)) ((ConfigOptionFloat, resolution)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index f83b699598..1e8fbbb6c4 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -802,6 +802,7 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data() void PrintObject::clear_layers() { + this->clear_local_z_plan(); if (!m_shared_object) { for (Layer *l : m_layers) delete l; @@ -947,6 +948,7 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "layer_height" || opt_key == "dithering_z_step_size" + || opt_key == "dithering_local_z_mode" || opt_key == "dithering_step_painted_zones_only" || opt_key == "mmu_segmented_region_max_width" || opt_key == "mmu_segmented_region_interlocking_depth" @@ -1250,6 +1252,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; + this->clear_local_z_plan(); } else if (step == posSupportMaterial) { invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); @@ -1271,6 +1274,7 @@ bool PrintObject::invalidate_all_steps() bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps(); // Then reset some of the depending values. m_slicing_params.valid = false; + this->clear_local_z_plan(); return result; } @@ -3866,13 +3870,20 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_ } coordf_t dithering_step = coordf_t(print_object->print()->config().dithering_z_step_size.value); + bool local_z_mode = print_object->print()->config().dithering_local_z_mode.value; bool painted_zones_only = print_object->print()->config().dithering_step_painted_zones_only.value; if (full_cfg.has("dithering_z_step_size")) dithering_step = coordf_t(full_cfg.opt_float("dithering_z_step_size")); + if (full_cfg.has("dithering_local_z_mode")) { + if (const ConfigOptionBool *opt = full_cfg.option("dithering_local_z_mode")) + local_z_mode = opt->value; + else if (const ConfigOptionInt *opt = full_cfg.option("dithering_local_z_mode")) + local_z_mode = (opt->value != 0); + } if (full_cfg.has("dithering_step_painted_zones_only")) painted_zones_only = full_cfg.opt_bool("dithering_step_painted_zones_only"); - if (!height_weighted_mode && dithering_step > EPSILON) { + if (!height_weighted_mode && !local_z_mode && dithering_step > EPSILON) { const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height(); std::vector mixed_ranges; if (painted_zones_only) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 9ab8ad226a..d746bcadc4 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,5 +1,12 @@ #include +#include +#include +#include +#include +#include +#include + #include #include "ClipperUtils.hpp" @@ -8,6 +15,7 @@ #include "Layer.hpp" #include "MultiMaterialSegmentation.hpp" #include "Print.hpp" +#include "SVG.hpp" //BBS #include "ShortestPath.hpp" #include "libslic3r/Feature/Interlocking/InterlockingGenerator.hpp" @@ -842,11 +850,663 @@ void PrintObject::slice() this->set_done(posSlice); } -template -static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCancel throw_on_cancel) +static bool bool_from_full_config(const DynamicPrintConfig &full_cfg, const char *key, bool fallback) +{ + if (!full_cfg.has(key)) + return fallback; + if (const ConfigOptionBool *opt = full_cfg.option(key)) + return opt->value; + if (const ConfigOptionInt *opt = full_cfg.option(key)) + return opt->value != 0; + return fallback; +} + +static coordf_t float_from_full_config(const DynamicPrintConfig &full_cfg, const char *key, coordf_t fallback) +{ + if (!full_cfg.has(key)) + return fallback; + if (const ConfigOptionFloat *opt = full_cfg.option(key)) + return coordf_t(opt->value); + return coordf_t(full_cfg.opt_float(key)); +} + +static bool fit_pass_heights_to_interval(std::vector &passes, double base_height, double lo, double hi) +{ + if (passes.empty() || base_height <= EPSILON) + return false; + + double sum = std::accumulate(passes.begin(), passes.end(), 0.0); + double delta = base_height - sum; + + auto within = [lo, hi](double h) { return h >= lo - EPSILON && h <= hi + EPSILON; }; + if (std::abs(delta) > EPSILON) { + if (within(passes.back() + delta)) { + passes.back() += delta; + delta = 0.0; + } else if (delta > 0.0) { + for (size_t i = passes.size(); i > 0 && delta > EPSILON; --i) { + double &h = passes[i - 1]; + const double room = hi - h; + if (room <= EPSILON) + continue; + const double take = std::min(room, delta); + h += take; + delta -= take; + } + } else { + for (size_t i = passes.size(); i > 0 && delta < -EPSILON; --i) { + double &h = passes[i - 1]; + const double room = h - lo; + if (room <= EPSILON) + continue; + const double take = std::min(room, -delta); + h -= take; + delta += take; + } + } + } + + if (std::abs(delta) > 1e-6) + return false; + return std::all_of(passes.begin(), passes.end(), within); +} + +static std::vector build_uniform_local_z_pass_heights(double base_height, double lo, double hi) +{ + std::vector out; + if (base_height <= EPSILON) + return out; + + size_t min_passes = size_t(std::max(1.0, std::ceil((base_height - EPSILON) / hi))); + size_t max_passes = size_t(std::max(1.0, std::floor((base_height + EPSILON) / lo))); + size_t pass_count = min_passes; + + if (max_passes >= min_passes) { + const double target_step = 0.5 * (lo + hi); + const size_t target_passes = + size_t(std::max(1.0, std::llround(base_height / std::max(target_step, EPSILON)))); + pass_count = std::clamp(target_passes, min_passes, max_passes); + } + + if (pass_count == 1 && base_height >= 2.0 * lo - EPSILON && max_passes >= 2) + pass_count = 2; + + if (pass_count <= 1) { + out.emplace_back(base_height); + return out; + } + + const double uniform_height = base_height / double(pass_count); + out.assign(pass_count, uniform_height); + + // Keep the accumulated numeric error at the very top of the interval. + double accumulated = 0.0; + for (size_t i = 0; i + 1 < out.size(); ++i) + accumulated += out[i]; + out.back() = std::max(EPSILON, base_height - accumulated); + 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) +{ + const int mix_b = std::clamp(mix_b_percent, 0, 100); + const double pct_b = double(mix_b) / 100.0; + const double pct_a = 1.0 - pct_b; + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + h_a = lo + pct_a * (hi - lo); + h_b = lo + pct_b * (hi - lo); +} + +static std::vector build_local_z_alternating_pass_heights(double base_height, + double lower_bound, + double upper_bound, + double gradient_h_a, + double gradient_h_b) +{ + if (base_height <= EPSILON) + return {}; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + if (base_height < 2.0 * lo - EPSILON) + return { base_height }; + + const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); + const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); + const double ratio_b = 1.0 - ratio_a; + + size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); + if ((min_passes % 2) != 0) + ++min_passes; + + size_t max_passes = size_t(std::max(2.0, std::floor((base_height + EPSILON) / lo))); + if ((max_passes % 2) != 0) + --max_passes; + if (max_passes < 2 || min_passes > max_passes) + return build_uniform_local_z_pass_heights(base_height, lo, hi); + + for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { + const size_t pair_count = pass_count / 2; + const double pair_h = base_height / double(pair_count); + const double h_a = pair_h * ratio_a; + const double h_b = pair_h * ratio_b; + + std::vector out; + out.reserve(pass_count); + for (size_t pair_idx = 0; pair_idx < pair_count; ++pair_idx) { + out.emplace_back(h_a); + out.emplace_back(h_b); + } + if (fit_pass_heights_to_interval(out, base_height, lo, hi)) + return out; + } + + return build_uniform_local_z_pass_heights(base_height, lo, hi); +} + +static std::vector build_local_z_pass_heights(double base_height, + double lower_bound, + double upper_bound, + double preferred_a, + double preferred_b) +{ + if (base_height <= EPSILON) + return {}; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + + std::vector cadence_unit; + if (preferred_a > EPSILON) + cadence_unit.push_back(std::clamp(preferred_a, lo, hi)); + if (preferred_b > EPSILON) + cadence_unit.push_back(std::clamp(preferred_b, lo, hi)); + + if (!cadence_unit.empty()) { + std::vector out; + out.reserve(size_t(std::ceil(base_height / lo)) + 2); + + double z_used = 0.0; + size_t idx = 0; + size_t guard = 0; + while (z_used + cadence_unit[idx] < base_height - EPSILON && guard++ < 100000) { + out.push_back(cadence_unit[idx]); + z_used += cadence_unit[idx]; + idx = (idx + 1) % cadence_unit.size(); + } + + const double remainder = base_height - z_used; + if (remainder > EPSILON) + out.push_back(remainder); + + if (fit_pass_heights_to_interval(out, base_height, lo, hi)) + return out; + } + + return build_uniform_local_z_pass_heights(base_height, lo, hi); +} + +static ExPolygons collect_layer_region_slices(const Layer &layer) +{ + ExPolygons out; + for (const LayerRegion *layerm : layer.regions()) + append(out, to_expolygons(layerm->slices.surfaces)); + if (!out.empty()) + out = union_ex(out); + return out; +} + +static void export_local_z_plan_debug(const PrintObject &print_object, coordf_t lower_bound, coordf_t upper_bound) +{ + const std::vector &intervals = print_object.local_z_intervals(); + const std::vector &plans = print_object.local_z_sublayer_plan(); + if (intervals.empty() || plans.empty()) + return; + + const int object_id = int(print_object.id().id); + std::ofstream json(debug_out_path("local-z-plan-obj-%d.json", object_id), std::ios::out | std::ios::trunc); + if (json.good()) { + json << std::fixed << std::setprecision(6); + json << "{\n"; + json << " \"object_id\": " << object_id << ",\n"; + json << " \"mixed_height_lower_bound\": " << lower_bound << ",\n"; + json << " \"mixed_height_upper_bound\": " << upper_bound << ",\n"; + json << " \"interval_count\": " << intervals.size() << ",\n"; + json << " \"sublayer_count\": " << plans.size() << ",\n"; + json << " \"intervals\": [\n"; + for (size_t i = 0; i < intervals.size(); ++i) { + const LocalZInterval &interval = intervals[i]; + json << " {\"layer_id\": " << interval.layer_id + << ", \"z_lo\": " << interval.z_lo + << ", \"z_hi\": " << interval.z_hi + << ", \"base_height\": " << interval.base_height + << ", \"sublayer_height\": " << interval.sublayer_height + << ", \"has_mixed_paint\": " << (interval.has_mixed_paint ? "true" : "false") + << ", \"sublayer_count\": " << interval.sublayer_count << "}"; + if (i + 1 < intervals.size()) + json << ","; + json << "\n"; + } + json << " ],\n"; + json << " \"sublayers\": [\n"; + for (size_t i = 0; i < plans.size(); ++i) { + const SubLayerPlan &plan = plans[i]; + json << " {\"layer_id\": " << plan.layer_id + << ", \"pass_index\": " << plan.pass_index + << ", \"split_interval\": " << (plan.split_interval ? "true" : "false") + << ", \"z_lo\": " << plan.z_lo + << ", \"z_hi\": " << plan.z_hi + << ", \"print_z\": " << plan.print_z + << ", \"flow_height\": " << plan.flow_height + << ", \"base_mask_count\": " << plan.base_masks.size() + << ", \"painted_mask_counts\": ["; + for (size_t eidx = 0; eidx < plan.painted_masks_by_extruder.size(); ++eidx) { + json << plan.painted_masks_by_extruder[eidx].size(); + if (eidx + 1 < plan.painted_masks_by_extruder.size()) + json << ", "; + } + json << "]}"; + if (i + 1 < plans.size()) + json << ","; + json << "\n"; + } + json << " ]\n"; + json << "}\n"; + } + + static const std::array colors { + "#E53935", "#1E88E5", "#43A047", "#FB8C00", "#8E24AA", + "#00897B", "#6D4C41", "#3949AB", "#C0CA33", "#F4511E" + }; + for (const SubLayerPlan &plan : plans) { + bool has_painted = std::any_of(plan.painted_masks_by_extruder.begin(), plan.painted_masks_by_extruder.end(), + [](const ExPolygons &masks) { return !masks.empty(); }); + if (!plan.split_interval && !has_painted) + continue; + if (!has_painted && plan.base_masks.empty()) + continue; + + std::vector> layers; + if (!plan.base_masks.empty()) { + layers.emplace_back(plan.base_masks, SVG::ExPolygonAttributes("base", "#D6D6D6", "#6A6A6A", "#6A6A6A", scale_(0.03), 0.45f)); + } + for (size_t eidx = 0; eidx < plan.painted_masks_by_extruder.size(); ++eidx) { + if (plan.painted_masks_by_extruder[eidx].empty()) + continue; + const char *color = colors[eidx % colors.size()]; + layers.emplace_back(plan.painted_masks_by_extruder[eidx], + SVG::ExPolygonAttributes("E" + std::to_string(eidx + 1), color, color, color, scale_(0.03), 0.55f)); + } + if (!layers.empty()) { + SVG::export_expolygons(debug_out_path("local-z-plan-obj-%d-layer-%d-pass-%d.svg", object_id, int(plan.layer_id), int(plan.pass_index)), layers); + } + } +} + +template +static void build_local_z_plan(PrintObject &print_object, const std::vector> &segmentation, ThrowOnCancel throw_on_cancel) +{ + print_object.clear_local_z_plan(); + + const Print *print = print_object.print(); + const std::string object_name = print_object.model_object() ? print_object.model_object()->name : std::string(""); + if (print == nullptr || print_object.layer_count() == 0 || segmentation.size() != print_object.layer_count()) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z plan skipped: invalid preconditions" + << " object=" << object_name + << " print_ptr=" << (print != nullptr) + << " layer_count=" << print_object.layer_count() + << " segmentation_layers=" << segmentation.size(); + return; + } + + const DynamicPrintConfig &full_cfg = print->full_print_config(); + const PrintConfig &print_cfg = print->config(); + const bool local_z_mode = bool_from_full_config(full_cfg, "dithering_local_z_mode", print_cfg.dithering_local_z_mode.value); + if (!local_z_mode) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z plan skipped: mode disabled" + << " object=" << object_name; + return; + } + + coordf_t mixed_lower = float_from_full_config(full_cfg, "mixed_filament_height_lower_bound", + coordf_t(print_cfg.mixed_filament_height_lower_bound.value)); + coordf_t mixed_upper = float_from_full_config(full_cfg, "mixed_filament_height_upper_bound", + coordf_t(print_cfg.mixed_filament_height_upper_bound.value)); + coordf_t preferred_a = float_from_full_config(full_cfg, "mixed_color_layer_height_a", + coordf_t(print_cfg.mixed_color_layer_height_a.value)); + coordf_t preferred_b = float_from_full_config(full_cfg, "mixed_color_layer_height_b", + coordf_t(print_cfg.mixed_color_layer_height_b.value)); + mixed_lower = std::max(0.01f, mixed_lower); + mixed_upper = std::max(mixed_lower, mixed_upper); + preferred_a = std::max(0.f, preferred_a); + preferred_b = std::max(0.f, preferred_b); + + const size_t num_physical = print_cfg.filament_colour.size(); + if (num_physical == 0) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z plan skipped: no physical filaments" + << " object=" << object_name; + return; + } + + BOOST_LOG_TRIVIAL(debug) << "Local-Z plan start" + << " object=" << object_name + << " layers=" << print_object.layer_count() + << " mixed_lower=" << mixed_lower + << " mixed_upper=" << mixed_upper + << " preferred_a=" << preferred_a + << " preferred_b=" << preferred_b + << " physical_filaments=" << num_physical; + + const MixedFilamentManager &mixed_mgr = print->mixed_filament_manager(); + std::vector intervals; + std::vector plans; + intervals.reserve(print_object.layer_count()); + size_t mixed_intervals = 0; + size_t split_intervals = 0; + size_t non_split_mixed_intervals = 0; + size_t total_generated_sublayer_cnt = 0; + size_t total_mixed_state_layers = 0; + size_t forced_height_resolve_calls = 0; + size_t forced_height_resolve_non_custom_calls = 0; + size_t forced_height_resolve_invalid_target = 0; + size_t split_passes_total = 0; + size_t split_passes_with_painted_masks = 0; + size_t split_intervals_without_painted_masks = 0; + size_t strict_ab_assignments = 0; + size_t alternating_height_intervals = 0; + size_t gradient_lock_mismatch_layers = 0; + size_t gradient_lock_unset_mixed_layers = 0; + size_t locked_gradient_source_layer = size_t(-1); + size_t locked_gradient_mixed_idx = size_t(-1); + double locked_gradient_h_a = 0.0; + double locked_gradient_h_b = 0.0; + bool locked_gradient_valid = false; + const auto &mixed_rows = mixed_mgr.mixed_filaments(); + + int cadence_index = 0; + for (size_t layer_id = 0; layer_id < print_object.layer_count(); ++layer_id) { + throw_on_cancel(); + + const Layer &layer = *print_object.get_layer(int(layer_id)); + LocalZInterval interval; + interval.layer_id = layer_id; + interval.z_lo = layer.print_z - layer.height; + interval.z_hi = layer.print_z; + interval.base_height = layer.height; + interval.sublayer_height = layer.height; + interval.first_sublayer_idx = plans.size(); + + ExPolygons mixed_masks; + size_t mixed_state_count = 0; + size_t dominant_mixed_idx = size_t(-1); + double dominant_mixed_area = -1.0; + double dominant_gradient_h_a = 0.0; + double dominant_gradient_h_b = 0.0; + bool dominant_gradient_valid = false; + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; + const unsigned int state_id = unsigned(channel_idx + 1); + if (mixed_mgr.is_mixed(state_id, num_physical)) { + interval.has_mixed_paint = true; + ++mixed_state_count; + append(mixed_masks, state_masks); + const double mixed_area = std::abs(area(state_masks)); + if (mixed_area > dominant_mixed_area) { + dominant_mixed_area = mixed_area; + dominant_mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1); + } + } + } + if (dominant_mixed_idx < mixed_rows.size()) { + compute_local_z_gradient_component_heights(mixed_rows[dominant_mixed_idx].mix_b_percent, mixed_lower, mixed_upper, + dominant_gradient_h_a, dominant_gradient_h_b); + dominant_gradient_valid = true; + } + if (interval.has_mixed_paint && preferred_a <= EPSILON && preferred_b <= EPSILON) { + if (!locked_gradient_valid && dominant_gradient_valid) { + locked_gradient_valid = true; + locked_gradient_source_layer = layer_id; + locked_gradient_mixed_idx = dominant_mixed_idx; + locked_gradient_h_a = dominant_gradient_h_a; + locked_gradient_h_b = dominant_gradient_h_b; + BOOST_LOG_TRIVIAL(warning) << "Local-Z gradient lock acquired" + << " object=" << object_name + << " layer_id=" << layer_id + << " mixed_idx=" << locked_gradient_mixed_idx + << " h_a=" << locked_gradient_h_a + << " h_b=" << locked_gradient_h_b; + } + if (!locked_gradient_valid) + ++gradient_lock_unset_mixed_layers; + else if (dominant_gradient_valid && dominant_mixed_idx != locked_gradient_mixed_idx) + ++gradient_lock_mismatch_layers; + } + total_mixed_state_layers += mixed_state_count; + if (!mixed_masks.empty()) + mixed_masks = union_ex(mixed_masks); + if (interval.has_mixed_paint) + ++mixed_intervals; + + const ExPolygons layer_masks = collect_layer_region_slices(layer); + ExPolygons base_masks = layer_masks; + if (interval.has_mixed_paint && !base_masks.empty() && !mixed_masks.empty()) { + base_masks = diff_ex(base_masks, mixed_masks); + if (!base_masks.empty()) { + const Polygons filtered = opening(to_polygons(base_masks), scaled(5. * EPSILON), scaled(5. * EPSILON)); + base_masks = union_ex(filtered); + } + } + + std::vector pass_heights; + if (interval.has_mixed_paint) { + // Local-Z mode should emit an A/B/A/B pattern for mixed regions and + // derive relative heights from mixed-filament gradient bounds. + if (preferred_a <= EPSILON && preferred_b <= EPSILON) { + if (locked_gradient_valid) { + pass_heights = build_local_z_alternating_pass_heights(interval.base_height, mixed_lower, mixed_upper, + locked_gradient_h_a, locked_gradient_h_b); + if (pass_heights.size() > 1) + ++alternating_height_intervals; + } else if (dominant_gradient_valid) { + pass_heights = build_local_z_alternating_pass_heights(interval.base_height, mixed_lower, mixed_upper, + dominant_gradient_h_a, dominant_gradient_h_b); + if (pass_heights.size() > 1) + ++alternating_height_intervals; + } else { + pass_heights = build_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper, preferred_a, preferred_b); + } + } else { + pass_heights = build_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper, preferred_a, preferred_b); + } + } + else + pass_heights.emplace_back(interval.base_height); + + const bool split_interval = interval.has_mixed_paint && pass_heights.size() > 1; + if (split_interval) { + ++split_intervals; + double z_cursor = interval.z_lo; + size_t pass_idx = 0; + bool interval_has_split_painted_masks = false; + interval.sublayer_height = *std::min_element(pass_heights.begin(), pass_heights.end()); + for (const double pass_height_nominal : pass_heights) { + if (z_cursor >= interval.z_hi - EPSILON) + break; + const double pass_height = std::min(pass_height_nominal, interval.z_hi - z_cursor); + const double z_next = std::min(interval.z_hi, z_cursor + pass_height); + + SubLayerPlan plan; + plan.layer_id = layer_id; + plan.pass_index = pass_idx; + plan.split_interval = true; + plan.z_lo = z_cursor; + plan.z_hi = z_next; + plan.print_z = z_next; + plan.flow_height = pass_height; + plan.painted_masks_by_extruder.assign(num_physical, ExPolygons()); + ++split_passes_total; + bool pass_has_painted_masks = false; + + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; + + const unsigned int state_id = unsigned(channel_idx + 1); + if (!mixed_mgr.is_mixed(state_id, num_physical)) + continue; + ++forced_height_resolve_calls; + const size_t mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1); + if (mixed_idx >= mixed_rows.size() || !mixed_rows[mixed_idx].custom) + ++forced_height_resolve_non_custom_calls; + unsigned int target_extruder = 0; + if (mixed_idx < mixed_rows.size()) { + const MixedFilament &mf = mixed_rows[mixed_idx]; + if (mf.component_a > 0 && mf.component_a <= num_physical && + mf.component_b > 0 && mf.component_b <= num_physical) { + // Enforce strict per-pass alternation inside split local-Z intervals. + target_extruder = ((pass_idx % 2) == 0) ? mf.component_a : mf.component_b; + ++strict_ab_assignments; + } + } + if (target_extruder == 0) { + target_extruder = mixed_mgr.resolve(state_id, num_physical, cadence_index, float(plan.print_z), float(plan.flow_height), true); + } + if (target_extruder == 0 || target_extruder > num_physical) { + ++forced_height_resolve_invalid_target; + continue; + } + append(plan.painted_masks_by_extruder[target_extruder - 1], state_masks); + pass_has_painted_masks = true; + } + for (ExPolygons &masks : plan.painted_masks_by_extruder) + if (masks.size() > 1) + masks = union_ex(masks); + if (pass_has_painted_masks) { + ++split_passes_with_painted_masks; + interval_has_split_painted_masks = true; + } + + if (z_next >= interval.z_hi - EPSILON) + plan.base_masks = base_masks; + + plans.emplace_back(std::move(plan)); + ++interval.sublayer_count; + ++total_generated_sublayer_cnt; + ++pass_idx; + ++cadence_index; + z_cursor = z_next; + } + if (!interval_has_split_painted_masks) + ++split_intervals_without_painted_masks; + } else { + if (interval.has_mixed_paint) + ++non_split_mixed_intervals; + SubLayerPlan plan; + plan.layer_id = layer_id; + plan.pass_index = 0; + plan.split_interval = false; + plan.z_lo = interval.z_lo; + plan.z_hi = interval.z_hi; + plan.print_z = interval.z_hi; + plan.flow_height = interval.base_height; + plan.base_masks = base_masks; + plan.painted_masks_by_extruder.assign(num_physical, ExPolygons()); + + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; + + const unsigned int state_id = unsigned(channel_idx + 1); + if (!mixed_mgr.is_mixed(state_id, num_physical)) + continue; + ++forced_height_resolve_calls; + const size_t mixed_idx = state_id > num_physical ? size_t(state_id - num_physical - 1) : size_t(-1); + if (mixed_idx >= mixed_rows.size() || !mixed_rows[mixed_idx].custom) + ++forced_height_resolve_non_custom_calls; + const unsigned int target_extruder = + mixed_mgr.resolve(state_id, num_physical, cadence_index, float(plan.print_z), float(plan.flow_height), true); + if (target_extruder == 0 || target_extruder > num_physical) { + ++forced_height_resolve_invalid_target; + continue; + } + append(plan.painted_masks_by_extruder[target_extruder - 1], state_masks); + } + for (ExPolygons &masks : plan.painted_masks_by_extruder) + if (masks.size() > 1) + masks = union_ex(masks); + + plans.emplace_back(std::move(plan)); + interval.sublayer_count = 1; + ++total_generated_sublayer_cnt; + ++cadence_index; + } + + if (interval.has_mixed_paint) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z interval" + << " object=" << object_name + << " layer_id=" << layer_id + << " base_height=" << interval.base_height + << " split=" << split_interval + << " mixed_states=" << mixed_state_count + << " pass_count=" << pass_heights.size() + << " pass_min_height=" + << (pass_heights.empty() ? 0.0 : *std::min_element(pass_heights.begin(), pass_heights.end())) + << " pass_max_height=" + << (pass_heights.empty() ? 0.0 : *std::max_element(pass_heights.begin(), pass_heights.end())) + << " mixed_mask_count=" << mixed_masks.size() + << " base_mask_count=" << base_masks.size(); + } + + intervals.emplace_back(std::move(interval)); + } + + if (!intervals.empty() && !plans.empty()) { + print_object.set_local_z_plan(std::move(intervals), std::move(plans)); + export_local_z_plan_debug(print_object, mixed_lower, mixed_upper); + BOOST_LOG_TRIVIAL(warning) << "Local-Z plan built" + << " object=" << object_name + << " mixed_intervals=" << mixed_intervals + << " split_intervals=" << split_intervals + << " non_split_mixed_intervals=" << non_split_mixed_intervals + << " split_intervals_without_painted_masks=" << split_intervals_without_painted_masks + << " sublayer_passes=" << total_generated_sublayer_cnt + << " split_passes_total=" << split_passes_total + << " split_passes_with_painted_masks=" << split_passes_with_painted_masks + << " alternating_height_intervals=" << alternating_height_intervals + << " strict_ab_assignments=" << strict_ab_assignments + << " mixed_state_layers=" << total_mixed_state_layers + << " forced_height_resolve_calls=" << forced_height_resolve_calls + << " forced_height_resolve_non_custom_calls=" << forced_height_resolve_non_custom_calls + << " forced_height_resolve_invalid_target=" << forced_height_resolve_invalid_target + << " gradient_lock_valid=" << locked_gradient_valid + << " gradient_lock_source_layer=" << locked_gradient_source_layer + << " gradient_lock_mixed_idx=" << locked_gradient_mixed_idx + << " gradient_lock_h_a=" << locked_gradient_h_a + << " gradient_lock_h_b=" << locked_gradient_h_b + << " gradient_lock_mismatch_layers=" << gradient_lock_mismatch_layers + << " gradient_lock_unset_mixed_layers=" << gradient_lock_unset_mixed_layers + << " mixed_lower=" << mixed_lower + << " mixed_upper=" << mixed_upper + << " preferred_a=" << preferred_a + << " preferred_b=" << preferred_b; + } else { + BOOST_LOG_TRIVIAL(warning) << "Local-Z plan empty after build" + << " object=" << object_name + << " intervals=" << intervals.size() + << " plans=" << plans.size() + << " mixed_intervals=" << mixed_intervals; + } +} + +template +static inline void apply_mm_segmentation(PrintObject &print_object, std::vector> segmentation, ThrowOnCancel throw_on_cancel) { - // Returns MM segmentation based on painting in MM segmentation gizmo - std::vector> segmentation = multi_material_segmentation_by_painting(print_object, throw_on_cancel); assert(segmentation.size() == print_object.layer_count()); tbb::parallel_for( tbb::blocked_range(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))), @@ -1195,7 +1855,9 @@ void PrintObject::slice_volumes() } BOOST_LOG_TRIVIAL(debug) << "Slicing volumes - MMU segmentation"; - apply_mm_segmentation(*this, [print]() { print->throw_if_canceled(); }); + std::vector> mm_segmentation = multi_material_segmentation_by_painting(*this, [print]() { print->throw_if_canceled(); }); + build_local_z_plan(*this, mm_segmentation, [print]() { print->throw_if_canceled(); }); + apply_mm_segmentation(*this, std::move(mm_segmentation), [print]() { print->throw_if_canceled(); }); } // Is any ModelVolume fuzzy skin painted? diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6625e06af3..dd9306614a 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1785,8 +1785,13 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) (opt_key == "mixed_filament_gradient_mode" || opt_key == "mixed_filament_height_lower_bound" || opt_key == "mixed_filament_height_upper_bound" || + opt_key == "mixed_color_layer_height_a" || + opt_key == "mixed_color_layer_height_b" || opt_key == "mixed_filament_cycle_layers" || opt_key == "mixed_filament_advanced_dithering" || + opt_key == "dithering_z_step_size" || + opt_key == "dithering_local_z_mode" || + opt_key == "dithering_step_painted_zones_only" || opt_key == "mixed_filament_definitions")) { DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; if (const ConfigOption *opt = m_config->option(opt_key)) @@ -2514,6 +2519,7 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("mixed_filament_cycle_layers"); optgroup->append_single_option_line("mixed_filament_advanced_dithering"); optgroup->append_single_option_line("dithering_z_step_size"); + optgroup->append_single_option_line("dithering_local_z_mode"); optgroup->append_single_option_line("dithering_step_painted_zones_only"); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin");