mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-29 21:07:11 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user