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.

This commit is contained in:
Rad
2026-02-11 03:15:16 +01:00
parent 494835c5ba
commit 112e2e5ef5
11 changed files with 1309 additions and 20 deletions
+564 -10
View File
@@ -3293,6 +3293,153 @@ inline GCode::ObjectByExtruder& object_by_extruder(std::map<unsigned int, std::v
return objects_by_extruder[object_idx];
}
static inline void apply_local_z_flow_height_override(ExtrusionPath& path, const double flow_height_override)
{
if (flow_height_override <= EPSILON)
return;
if (path.height > 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<double>::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<const ExtrusionPath*>(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<const ExtrusionMultiPath*>(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<const ExtrusionLoop*>(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<const ExtrusionPath*>(entity)) {
lines.emplace_back(path->polyline);
} else if (const auto* multipath = dynamic_cast<const ExtrusionMultiPath*>(entity)) {
for (const ExtrusionPath& p : multipath->paths)
lines.emplace_back(p.polyline);
} else if (const auto* loop = dynamic_cast<const ExtrusionLoop*>(entity)) {
for (const ExtrusionPath& p : loop->paths)
lines.emplace_back(p.polyline);
}
}
return lines;
}
static std::unique_ptr<ExtrusionEntityCollection> 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<ExtrusionEntityCollection>(source);
}
auto out = std::make_unique<ExtrusionEntityCollection>();
out->no_sort = source.no_sort;
ExtrusionEntityCollection flattened = source.flatten(false);
for (const ExtrusionEntity* entity : flattened.entities) {
if (const auto* path = dynamic_cast<const ExtrusionPath*>(entity)) {
append_clipped_path(*path, include_masks, exclude_masks, flow_height_override, *out);
} else if (const auto* multipath = dynamic_cast<const ExtrusionMultiPath*>(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<const ExtrusionLoop*>(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<GCode::ObjectByExtruder::Island>& object_islands_by_extruder(
std::map<unsigned int, std::vector<GCode::ObjectByExtruder>>& 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<unsigned int, std::vector<ObjectByExtruder>> by_extruder;
bool is_anything_overridden = const_cast<LayerTools&>(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<ExPolygons> compensated_masks_by_extruder;
std::map<unsigned int, std::vector<ObjectByExtruder>> 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<LocalZPassBucket> 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<LocalZLayerContext> local_z_layer_contexts;
std::vector<std::unique_ptr<ExtrusionEntityCollection>> 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<ObjectByExtruder::Island>& 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<LayerTools&>(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<ObjectByExtruder::Island>& islands = object_islands_by_extruder(by_extruder, extruder,
&layer_to_print - layers.data(),
layers.size(), n_slices + 1);
std::vector<ObjectByExtruder::Island>& 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<LocalZPassRef> 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<ObjectByExtruder>& 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<InstanceToPrint> 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<const PrintObject*, Point> 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())