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 1310 additions and 21 deletions

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())

View File

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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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<ExPolygons> 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<TreeSupportData> alloc_tree_support_preview_cache();
void clear_tree_support_preview_cache() { m_tree_support_preview_cache.reset(); }
const std::vector<LocalZInterval>& local_z_intervals() const { return m_local_z_intervals; }
const std::vector<SubLayerPlan>& local_z_sublayer_plan() const { return m_local_z_sublayer_plan; }
void set_local_z_plan(std::vector<LocalZInterval> intervals, std::vector<SubLayerPlan> 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<LocalZInterval> m_local_z_intervals;
std::vector<SubLayerPlan> m_local_z_sublayer_plan;
// BBS
std::shared_ptr<TreeSupportData> m_tree_support_preview_cache;

View File

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

View File

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

View File

@@ -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))

View File

@@ -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<ConfigOptionBool>("dithering_local_z_mode"))
local_z_mode = opt->value;
else if (const ConfigOptionInt *opt = full_cfg.option<ConfigOptionInt>("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<t_layer_height_range> mixed_ranges;
if (painted_zones_only)

View File

@@ -1,5 +1,12 @@
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <tbb/parallel_for.h>
#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<typename ThrowOnCancel>
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<ConfigOptionBool>(key))
return opt->value;
if (const ConfigOptionInt *opt = full_cfg.option<ConfigOptionInt>(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<ConfigOptionFloat>(key))
return coordf_t(opt->value);
return coordf_t(full_cfg.opt_float(key));
}
static bool fit_pass_heights_to_interval(std::vector<double> &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<double> build_uniform_local_z_pass_heights(double base_height, double lo, double hi)
{
std::vector<double> out;
if (base_height <= EPSILON)
return out;
size_t min_passes = size_t(std::max<double>(1.0, std::ceil((base_height - EPSILON) / hi)));
size_t max_passes = size_t(std::max<double>(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<double>(1.0, std::llround(base_height / std::max<double>(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<double>(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<double>(0.01, lower_bound);
const double hi = std::max<double>(lo, upper_bound);
h_a = lo + pct_a * (hi - lo);
h_b = lo + pct_b * (hi - lo);
}
static std::vector<double> 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<double>(0.01, lower_bound);
const double hi = std::max<double>(lo, upper_bound);
if (base_height < 2.0 * lo - EPSILON)
return { base_height };
const double cycle_h = std::max<double>(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<double>(2.0, std::ceil((base_height - EPSILON) / hi)));
if ((min_passes % 2) != 0)
++min_passes;
size_t max_passes = size_t(std::max<double>(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<double> 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<double> 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<double>(0.01, lower_bound);
const double hi = std::max<double>(lo, upper_bound);
std::vector<double> 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<double> 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<LocalZInterval> &intervals = print_object.local_z_intervals();
const std::vector<SubLayerPlan> &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<const char *, 10> 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<std::pair<ExPolygons, SVG::ExPolygonAttributes>> 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<typename ThrowOnCancel>
static void build_local_z_plan(PrintObject &print_object, const std::vector<std::vector<ExPolygons>> &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("<unknown>");
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<coordf_t>(0.01f, mixed_lower);
mixed_upper = std::max<coordf_t>(mixed_lower, mixed_upper);
preferred_a = std::max<coordf_t>(0.f, preferred_a);
preferred_b = std::max<coordf_t>(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<LocalZInterval> intervals;
std::vector<SubLayerPlan> 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<float>(5. * EPSILON), scaled<float>(5. * EPSILON));
base_masks = union_ex(filtered);
}
}
std::vector<double> 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<double>(pass_height_nominal, interval.z_hi - z_cursor);
const double z_next = std::min<double>(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<typename ThrowOnCancel>
static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<std::vector<ExPolygons>> segmentation, ThrowOnCancel throw_on_cancel)
{
// Returns MM segmentation based on painting in MM segmentation gizmo
std::vector<std::vector<ExPolygons>> segmentation = multi_material_segmentation_by_painting(print_object, throw_on_cancel);
assert(segmentation.size() == print_object.layer_count());
tbb::parallel_for(
tbb::blocked_range<size_t>(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<std::vector<ExPolygons>> 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?

View File

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