Collapse same-color mixed filament regions across painted areas

Resolve ordinary mixed-filament painted regions to their effective physical
filament on each layer so adjacent same-color zones can merge into one
continuous area instead of being sliced as separate islands.

Add a global "Collapse same-color mixed regions" option, enabled by default,
and automatically turn it off when Local Z dithering is enabled while still
allowing manual re-enable for experimental use.
This commit is contained in:
Rad
2026-03-26 21:10:15 +01:00
parent a4f0947b49
commit ed0182618e
11 changed files with 158 additions and 11 deletions

View File

@@ -1321,6 +1321,43 @@ unsigned int MixedFilamentManager::resolve_perimeter(unsigned int filament_id,
return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted); return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height, force_height_weighted);
} }
unsigned int MixedFilamentManager::effective_painted_region_filament_id(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z,
float layer_height,
float layer_height_a,
float layer_height_b,
float base_layer_height) const
{
const int mixed_idx = mixed_index_from_filament_id(filament_id, num_physical);
if (mixed_idx < 0)
return filament_id;
const MixedFilament &mf = m_mixed[size_t(mixed_idx)];
if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme))
return filament_id;
const std::string normalized_pattern = normalize_manual_pattern(mf.manual_pattern);
if (normalized_pattern.find(',') != std::string::npos)
return filament_id;
const bool is_custom_mixed = mf.custom;
if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) {
const float safe_base = std::max<float>(0.01f, base_layer_height);
const int ratio_a = std::max(1, int(std::lround((layer_height_a > 0.f ? layer_height_a : safe_base) / safe_base)));
const int ratio_b = std::max(1, int(std::lround((layer_height_b > 0.f ? layer_height_b : safe_base) / safe_base)));
const int cycle = ratio_a + ratio_b;
if (cycle > 0) {
const int pos = ((layer_index % cycle) + cycle) % cycle;
return pos < ratio_a ? mf.component_a : mf.component_b;
}
}
return resolve(filament_id, num_physical, layer_index, layer_print_z, layer_height);
}
std::vector<unsigned int> MixedFilamentManager::ordered_perimeter_extruders(unsigned int filament_id, std::vector<unsigned int> MixedFilamentManager::ordered_perimeter_extruders(unsigned int filament_id,
size_t num_physical, size_t num_physical,
int layer_index, int layer_index,

View File

@@ -179,6 +179,18 @@ public:
float layer_print_z = 0.f, float layer_print_z = 0.f,
float layer_height = 0.f, float layer_height = 0.f,
bool force_height_weighted = false) const; bool force_height_weighted = false) const;
// Resolve the filament ID that should own painted regions on this layer.
// Modes that require virtual identity later in G-code generation keep the
// original mixed ID; ordinary mixed rows collapse to the current physical
// extruder so adjacent same-tool regions can merge.
unsigned int effective_painted_region_filament_id(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
float layer_height_a = 0.f,
float layer_height_b = 0.f,
float base_layer_height = 0.2f) const;
std::vector<unsigned int> ordered_perimeter_extruders(unsigned int filament_id, std::vector<unsigned int> ordered_perimeter_extruders(unsigned int filament_id,
size_t num_physical, size_t num_physical,
int layer_index, int layer_index,

View File

@@ -75,6 +75,7 @@ static std::vector<std::string> s_project_options {
"mixed_filament_height_upper_bound", "mixed_filament_height_upper_bound",
"mixed_filament_advanced_dithering", "mixed_filament_advanced_dithering",
"mixed_filament_surface_indentation", "mixed_filament_surface_indentation",
"mixed_filament_region_collapse",
"mixed_filament_definitions", "mixed_filament_definitions",
"mixed_color_layer_height_a", "mixed_color_layer_height_a",
"mixed_color_layer_height_b", "mixed_color_layer_height_b",

View File

@@ -297,6 +297,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "mixed_filament_height_upper_bound" || opt_key == "mixed_filament_height_upper_bound"
|| opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_advanced_dithering"
|| opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_surface_indentation"
|| opt_key == "mixed_filament_region_collapse"
|| opt_key == "mixed_filament_definitions" || opt_key == "mixed_filament_definitions"
// Spiral Vase forces different kind of slicing than the normal model: // Spiral Vase forces different kind of slicing than the normal model:
// In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer.

View File

@@ -1112,18 +1112,22 @@ static inline void append_unique_painted_extruder(std::vector<unsigned int> &pai
painting_extruders.emplace_back(extruder_id); painting_extruders.emplace_back(extruder_id);
} }
static void append_same_layer_component_extruders(const MixedFilamentManager &mixed_mgr, static void append_mixed_component_extruders(const MixedFilamentManager &mixed_mgr,
unsigned int state_id, unsigned int state_id,
size_t num_physical_extruders, size_t num_physical_extruders,
std::vector<unsigned int> &painting_extruders) std::vector<unsigned int> &painting_extruders)
{ {
if (state_id <= num_physical_extruders) if (state_id <= num_physical_extruders)
return; return;
const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(state_id, num_physical_extruders); const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(state_id, num_physical_extruders);
if (mixed_row == nullptr || !mixed_row->enabled || mixed_row->distribution_mode != int(MixedFilament::SameLayerPointillisme)) if (mixed_row == nullptr || !mixed_row->enabled)
return; return;
// Pre-create painted target regions for every physical filament a mixed row
// may resolve to. apply_mm_segmentation can then collapse ordinary mixed
// channels onto the active physical tool for a layer without losing the
// destination region.
append_unique_painted_extruder(painting_extruders, mixed_row->component_a, num_physical_extruders); append_unique_painted_extruder(painting_extruders, mixed_row->component_a, num_physical_extruders);
append_unique_painted_extruder(painting_extruders, mixed_row->component_b, num_physical_extruders); append_unique_painted_extruder(painting_extruders, mixed_row->component_b, num_physical_extruders);
@@ -1177,6 +1181,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
new_full_config.option("mixed_filament_pointillism_pixel_size", true); new_full_config.option("mixed_filament_pointillism_pixel_size", true);
new_full_config.option("mixed_filament_pointillism_line_gap", true); new_full_config.option("mixed_filament_pointillism_line_gap", true);
new_full_config.option("mixed_filament_surface_indentation", true); new_full_config.option("mixed_filament_surface_indentation", true);
new_full_config.option("mixed_filament_region_collapse", true);
new_full_config.option("mixed_filament_definitions", true); new_full_config.option("mixed_filament_definitions", true);
m_config.option("dithering_z_step_size", true); m_config.option("dithering_z_step_size", true);
m_config.option("dithering_local_z_mode", true); m_config.option("dithering_local_z_mode", true);
@@ -1188,6 +1193,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_config.option("mixed_filament_pointillism_pixel_size", true); m_config.option("mixed_filament_pointillism_pixel_size", true);
m_config.option("mixed_filament_pointillism_line_gap", true); m_config.option("mixed_filament_pointillism_line_gap", true);
m_config.option("mixed_filament_surface_indentation", true); m_config.option("mixed_filament_surface_indentation", true);
m_config.option("mixed_filament_region_collapse", true);
m_config.option("mixed_filament_definitions", true); m_config.option("mixed_filament_definitions", true);
m_default_object_config.option("dithering_z_step_size", 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_local_z_mode", true);
@@ -1199,6 +1205,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.option("mixed_filament_pointillism_pixel_size", true); m_default_object_config.option("mixed_filament_pointillism_pixel_size", true);
m_default_object_config.option("mixed_filament_pointillism_line_gap", true); m_default_object_config.option("mixed_filament_pointillism_line_gap", true);
m_default_object_config.option("mixed_filament_surface_indentation", true); m_default_object_config.option("mixed_filament_surface_indentation", true);
m_default_object_config.option("mixed_filament_region_collapse", true);
m_default_object_config.option("mixed_filament_definitions", true); m_default_object_config.option("mixed_filament_definitions", true);
// BBS // BBS
int used_filaments = this->extruders(true).size(); int used_filaments = this->extruders(true).size();
@@ -1772,10 +1779,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
continue; continue;
if (state_idx <= num_total_filaments) { if (state_idx <= num_total_filaments) {
painting_extruders.emplace_back(static_cast<unsigned int>(state_idx)); painting_extruders.emplace_back(static_cast<unsigned int>(state_idx));
append_same_layer_component_extruders(m_mixed_filament_mgr, append_mixed_component_extruders(m_mixed_filament_mgr,
static_cast<unsigned int>(state_idx), static_cast<unsigned int>(state_idx),
num_extruders, num_extruders,
painting_extruders); painting_extruders);
} else } else
++dropped_painted_states; ++dropped_painted_states;
} }

View File

@@ -4244,6 +4244,15 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0)); def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_region_collapse", coBool);
def->label = L("Collapse same-color mixed regions");
def->category = L("Others");
def->tooltip = L("Merge ordinary mixed-filament painted regions into a single area when they resolve to the same physical filament on a layer.\n\n"
"This improves continuity for adjacent same-color areas. Local Z dithering turns this off automatically when enabled, but you may turn it back on manually.\n\n"
"Experimental with Local Z dithering.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("mixed_filament_definitions", coString); def = this->add("mixed_filament_definitions", coString);
def->label = L("Mixed filament custom definitions"); def->label = L("Mixed filament custom definitions");
def->tooltip = L("Serialized custom mixed filament rows.\n\n" def->tooltip = L("Serialized custom mixed filament rows.\n\n"

View File

@@ -1363,6 +1363,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloat, mixed_filament_pointillism_pixel_size)) ((ConfigOptionFloat, mixed_filament_pointillism_pixel_size))
((ConfigOptionFloat, mixed_filament_pointillism_line_gap)) ((ConfigOptionFloat, mixed_filament_pointillism_line_gap))
((ConfigOptionFloat, mixed_filament_surface_indentation)) ((ConfigOptionFloat, mixed_filament_surface_indentation))
((ConfigOptionBool, mixed_filament_region_collapse))
((ConfigOptionString, mixed_filament_definitions)) ((ConfigOptionString, mixed_filament_definitions))
((ConfigOptionFloat, dithering_z_step_size)) ((ConfigOptionFloat, dithering_z_step_size))
((ConfigOptionBool, dithering_local_z_mode)) ((ConfigOptionBool, dithering_local_z_mode))

View File

@@ -950,6 +950,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "dithering_z_step_size" || opt_key == "dithering_z_step_size"
|| opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_mode"
|| opt_key == "dithering_step_painted_zones_only" || opt_key == "dithering_step_painted_zones_only"
|| opt_key == "mixed_filament_region_collapse"
|| opt_key == "mmu_segmented_region_max_width" || opt_key == "mmu_segmented_region_max_width"
|| opt_key == "mmu_segmented_region_interlocking_depth" || opt_key == "mmu_segmented_region_interlocking_depth"
|| opt_key == "raft_layers" || opt_key == "raft_layers"
@@ -973,6 +974,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "mixed_filament_height_upper_bound" || opt_key == "mixed_filament_height_upper_bound"
|| opt_key == "mixed_filament_advanced_dithering" || opt_key == "mixed_filament_advanced_dithering"
|| opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_surface_indentation"
|| opt_key == "mixed_filament_region_collapse"
|| opt_key == "mixed_filament_definitions") { || opt_key == "mixed_filament_definitions") {
// Mixed filament gradient controls affect layer cadence and virtual // Mixed filament gradient controls affect layer cadence and virtual
// tool distribution, so force a re-slice prompt like other // tool distribution, so force a re-slice prompt like other

View File

@@ -2947,9 +2947,21 @@ template<typename ThrowOnCancel>
static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<std::vector<ExPolygons>> segmentation, ThrowOnCancel throw_on_cancel) static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<std::vector<ExPolygons>> segmentation, ThrowOnCancel throw_on_cancel)
{ {
assert(segmentation.size() == print_object.layer_count()); assert(segmentation.size() == print_object.layer_count());
const PrintConfig &print_cfg = print_object.print()->config();
const DynamicPrintConfig &full_cfg = print_object.print()->full_print_config();
const size_t num_physical = print_cfg.filament_diameter.size();
const 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));
const 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));
const coordf_t base_height = std::max<coordf_t>(0.01f, coordf_t(print_object.config().layer_height.value));
const bool collapse_mixed_regions =
bool_from_full_config(full_cfg, "mixed_filament_region_collapse", print_cfg.mixed_filament_region_collapse.value);
const MixedFilamentManager &mixed_mgr = print_object.print()->mixed_filament_manager();
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))), tbb::blocked_range<size_t>(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))),
[&print_object, &segmentation, throw_on_cancel](const tbb::blocked_range<size_t> &range) { [&print_object, &segmentation, &mixed_mgr, num_physical, preferred_a, preferred_b, base_height, collapse_mixed_regions, throw_on_cancel](const tbb::blocked_range<size_t> &range) {
const auto &layer_ranges = print_object.shared_regions()->layer_ranges; const auto &layer_ranges = print_object.shared_regions()->layer_ranges;
double z = print_object.get_layer(int(range.begin()))->slice_z; double z = print_object.get_layer(int(range.begin()))->slice_z;
auto it_layer_range = layer_range_first(layer_ranges, z); auto it_layer_range = layer_range_first(layer_ranges, z);
@@ -2982,7 +2994,20 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
size_t missing_target_regions = 0; size_t missing_target_regions = 0;
std::vector<int> missing_target_extruders; std::vector<int> missing_target_extruders;
for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) { for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) {
ByExtruder &region = by_extruder[extruder_id]; const unsigned int channel_id = unsigned(extruder_id + 1);
const unsigned int effective_filament_id = collapse_mixed_regions ?
mixed_mgr.effective_painted_region_filament_id(channel_id,
num_physical,
int(layer_id),
float(layer.print_z),
float(layer.height),
float(preferred_a),
float(preferred_b),
float(base_height)) :
channel_id;
const size_t effective_idx =
effective_filament_id >= 1 && effective_filament_id <= num_extruders ? size_t(effective_filament_id - 1) : extruder_id;
ByExtruder &region = by_extruder[effective_idx];
append(region.expolygons, std::move(segmentation[layer_id][extruder_id])); append(region.expolygons, std::move(segmentation[layer_id][extruder_id]));
if (! region.expolygons.empty()) { if (! region.expolygons.empty()) {
region.bbox = get_extents(region.expolygons); region.bbox = get_extents(region.expolygons);

View File

@@ -1491,6 +1491,21 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
update_wiping_button_visibility(); update_wiping_button_visibility();
} }
if (opt_key == "dithering_local_z_mode" &&
boost::any_cast<bool>(value) &&
(!m_config->has("mixed_filament_region_collapse") ||
m_config->option("mixed_filament_region_collapse") == nullptr ||
m_config->opt_bool("mixed_filament_region_collapse"))) {
change_opt_value(*m_config, "mixed_filament_region_collapse", boost::any(false));
if (m_type == Preset::TYPE_PRINT) {
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
project_cfg.set_key_value("mixed_filament_region_collapse", new ConfigOptionBool(false));
}
if (Field *field = this->get_field("mixed_filament_region_collapse"))
field->set_value(boost::any(false), false);
update_dirty();
}
if (opt_key == "single_extruder_multi_material" ){ if (opt_key == "single_extruder_multi_material" ){
const auto bSEMM = m_config->opt_bool("single_extruder_multi_material"); const auto bSEMM = m_config->opt_bool("single_extruder_multi_material");
@@ -1791,6 +1806,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
opt_key == "mixed_filament_pointillism_pixel_size" || opt_key == "mixed_filament_pointillism_pixel_size" ||
opt_key == "mixed_filament_pointillism_line_gap" || opt_key == "mixed_filament_pointillism_line_gap" ||
opt_key == "mixed_filament_surface_indentation" || opt_key == "mixed_filament_surface_indentation" ||
opt_key == "mixed_filament_region_collapse" ||
opt_key == "dithering_z_step_size" || opt_key == "dithering_z_step_size" ||
opt_key == "dithering_local_z_mode" || opt_key == "dithering_local_z_mode" ||
opt_key == "dithering_step_painted_zones_only" || opt_key == "dithering_step_painted_zones_only" ||
@@ -2528,6 +2544,7 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops"
optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size"); optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size");
optgroup->append_single_option_line("mixed_filament_pointillism_line_gap"); optgroup->append_single_option_line("mixed_filament_pointillism_line_gap");
optgroup->append_single_option_line("mixed_filament_surface_indentation"); optgroup->append_single_option_line("mixed_filament_surface_indentation");
optgroup->append_single_option_line("mixed_filament_region_collapse");
optgroup->append_single_option_line("dithering_z_step_size"); optgroup->append_single_option_line("dithering_z_step_size");
optgroup->append_single_option_line("dithering_local_z_mode"); optgroup->append_single_option_line("dithering_local_z_mode");
optgroup->append_single_option_line("dithering_step_painted_zones_only"); optgroup->append_single_option_line("dithering_step_painted_zones_only");

View File

@@ -221,6 +221,41 @@ TEST_CASE("Mixed filament perimeter resolver uses grouped manual patterns by ins
CHECK(ordered_layer1[1] == 1); CHECK(ordered_layer1[1] == 1);
} }
TEST_CASE("Mixed filament painted-region resolver collapses ordinary mixed rows to the active physical extruder", "[MixedFilament]")
{
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
MixedFilamentManager mgr;
mgr.add_custom_filament(1, 2, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
MixedFilament &row = mgr.mixed_filaments().front();
row.ratio_a = 1;
row.ratio_b = 1;
row.manual_pattern.clear();
row.distribution_mode = int(MixedFilament::Simple);
CHECK(mgr.effective_painted_region_filament_id(3, 2, 0) == 1);
CHECK(mgr.effective_painted_region_filament_id(3, 2, 1) == 2);
}
TEST_CASE("Mixed filament painted-region resolver preserves virtual channels for grouped and same-layer modes", "[MixedFilament]")
{
const std::vector<std::string> colors = {"#00FFFF", "#FF00FF"};
MixedFilamentManager mgr;
mgr.add_custom_filament(1, 2, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
MixedFilament &row = mgr.mixed_filaments().front();
row.manual_pattern = MixedFilamentManager::normalize_manual_pattern("12,21");
CHECK(mgr.effective_painted_region_filament_id(3, 2, 0) == 3);
row.manual_pattern.clear();
row.distribution_mode = int(MixedFilament::SameLayerPointillisme);
CHECK(mgr.effective_painted_region_filament_id(3, 2, 0) == 3);
}
TEST_CASE("ExtrusionPath copies preserve inset index", "[MixedFilament]") TEST_CASE("ExtrusionPath copies preserve inset index", "[MixedFilament]")
{ {
ExtrusionPath src(erPerimeter); ExtrusionPath src(erPerimeter);