Update from FS at b3c41fda4.

Slicing
  - align merge_segmented_layers shape with FS apply_mm_segmentation
    (size = num_facets_states, loop from 0, no -1 shift); painted mixed
    regions were previously attributed to filament_id-1 of intent.
    apply_fuzzy_skin_segmentation reads channel 1;
    apply_mixed_surface_indentation uses segmentation_channel_filament_id
  - port apply_mixed_surface_indentation, apply_mixed_component_surface_offsets,
    apply_mixed_region_surface_offsets, apply_surface_emboss_mixed_region_override,
    plus surface_emboss_mixed_* debug subsystem
  - refactor apply_mm_segmentation (by-value MM, bias_mode, surface-type-
    preserving intersection, region normalization, post-MM dump); hoist MM
    segmentation into slice_volumes so mixed apply_* flow can mutate it
  - restore clear_local_z_plan() invalidation hooks
    (PrintObject.cpp:805/1264/1286)

  GCode
  - add LayerTools::preserve_extruder_order, honored by collect_extruders,
    both reorder_extruders overloads, and
    reorder_filaments_for_minimum_flush_volume; helpers
    append_unique_preserve_order / remove_duplicates_preserve_order
  - wire MixedFilamentManager::ordered_perimeter_extruders for grouped
    manual-pattern walls; set preserve_extruder_order when >= 2
  - mixed-aware support: layer_height set for support-only layers,
    ExtrusionRole-based has_support/has_interface with erMixed short-
    circuit, support_filament / support_interface_filament routed through
    resolve_mixed

  Print
  - materialize mixed_filament_pointillism_{pixel_size,line_gap} in
    PrintApply's option-tracking block so in-session edits diff correctly

  GUI
  - Tab::on_value_change: dithering_local_z_mode cascading clears, 17-key
    project_config sync, update_mixed_filament_panel(false) on
    mixed_filament_component_bias_enabled change
  - GUI_Factories: physical_filaments_count, ui_ordered_filament_ids,
    filament_menu_item_name; filaments_count includes enabled mixed
    virtuals; right-click 'Change filament' submenus iterate UI-ordered IDs

  Tests
  - sentinel asserting MultiMaterialSegmentation uses FS-aligned shape
    (39 -> 40)
This commit is contained in:
SoftFever
2026-04-29 19:22:29 +08:00
parent 9c8caf121e
commit 103cf247e5
10 changed files with 1516 additions and 184 deletions

View File

@@ -90,6 +90,12 @@ bool has_grouped_manual_pattern(const MixedFilamentManager *mixed_mgr,
return normalized.find(',') != std::string::npos;
}
void append_unique_preserve_order(std::vector<unsigned int> &dst, unsigned int value)
{
if (std::find(dst.begin(), dst.end(), value) == dst.end())
dst.emplace_back(value);
}
bool internal_solid_infill_uses_sparse_filament(const PrintRegion &region, ExtrusionRole role)
{
return role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON;
@@ -141,6 +147,15 @@ unsigned int grouped_manual_pattern_infill_filament_1based(const LayerTools& la
float(layer_tools.layer_height));
}
void remove_duplicates_preserve_order(std::vector<unsigned int> &values)
{
std::vector<unsigned int> ordered;
ordered.reserve(values.size());
for (unsigned int value : values)
append_unique_preserve_order(ordered, value);
values = std::move(ordered);
}
} // anonymous namespace
// ---------------------------------------------------------------------------
@@ -323,27 +338,29 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector<unsigned int>& too
// Reorder the extruders of first layer
{
LayerTools& lt = m_layer_tools[0];
std::vector<unsigned int> layer0_extruders = lt.extruders;
lt.extruders.clear();
for (unsigned int extruder_id : tool_order_layer0) {
auto iter = std::find(layer0_extruders.begin(), layer0_extruders.end(), extruder_id);
if (iter != layer0_extruders.end()) {
lt.extruders.push_back(extruder_id);
*iter = (unsigned int)-1;
if (!lt.preserve_extruder_order) {
std::vector<unsigned int> layer0_extruders = lt.extruders;
lt.extruders.clear();
for (unsigned int extruder_id : tool_order_layer0) {
auto iter = std::find(layer0_extruders.begin(), layer0_extruders.end(), extruder_id);
if (iter != layer0_extruders.end()) {
lt.extruders.push_back(extruder_id);
*iter = (unsigned int)-1;
}
}
}
for (unsigned int extruder_id : layer0_extruders) {
if (extruder_id == 0)
continue;
for (unsigned int extruder_id : layer0_extruders) {
if (extruder_id == 0)
continue;
if (extruder_id != (unsigned int)-1)
lt.extruders.push_back(extruder_id);
}
if (extruder_id != (unsigned int)-1)
lt.extruders.push_back(extruder_id);
}
// all extruders are zero
if (lt.extruders.empty()) {
lt.extruders.push_back(tool_order_layer0[0]);
// all extruders are zero
if (lt.extruders.empty()) {
lt.extruders.push_back(tool_order_layer0[0]);
}
}
}
@@ -359,6 +376,10 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector<unsigned int>& too
if (lt.extruders.front() == 0)
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
lt.extruders.erase(lt.extruders.begin());
if (lt.preserve_extruder_order) {
last_extruder_id = lt.extruders.back();
continue;
}
// Reorder the extruders to start with the last one.
for (size_t i = 1; i < lt.extruders.size(); ++i)
if (lt.extruders[i] == last_extruder_id) {
@@ -413,6 +434,10 @@ void ToolOrdering::handle_dontcare_extruder(unsigned int last_extruder_id)
if (lt.extruders.front() == 0)
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
lt.extruders.erase(lt.extruders.begin());
if (lt.preserve_extruder_order) {
last_extruder_id = lt.extruders.back();
continue;
}
// Reorder the extruders to start with the last one.
for (size_t i = 1; i < lt.extruders.size(); ++ i)
if (lt.extruders[i] == last_extruder_id) {
@@ -874,19 +899,40 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
if (something_nonoverriddable){
if (extruder_override == 0) {
layer_tools.extruders.emplace_back(layer_tools.wall_filament(region) + 1);
if (layerCount == 0) {
firstLayerExtruders.emplace_back(layer_tools.wall_filament(region) + 1);
const unsigned int configured_wall = (extruder_override == 0)
? region.config().wall_filament.value
: extruder_override;
unsigned int wall_ext = resolve_mixed(configured_wall,
layerCount,
float(layer->print_z),
float(layer->height));
const unsigned int grouped_id =
grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_wall);
if (grouped_id != 0) {
const std::vector<unsigned int> ordered =
m_mixed_mgr->ordered_perimeter_extruders(grouped_id,
m_num_physical,
layerCount,
float(layer->print_z),
float(layer->height));
if (!ordered.empty()) {
if (ordered.size() >= 2)
layer_tools.preserve_extruder_order = true;
for (unsigned int extruder_id : ordered) {
layer_tools.extruders.emplace_back(extruder_id);
if (layerCount == 0 &&
std::find(firstLayerExtruders.begin(), firstLayerExtruders.end(), int(extruder_id)) == firstLayerExtruders.end())
firstLayerExtruders.emplace_back(int(extruder_id));
}
} else {
layer_tools.extruders.emplace_back(wall_ext);
if (layerCount == 0)
firstLayerExtruders.emplace_back(wall_ext);
}
} else {
const unsigned int resolved = resolve_mixed(extruder_override,
layerCount,
float(layer->print_z),
float(layer->height));
layer_tools.extruders.emplace_back(resolved);
layer_tools.extruders.emplace_back(wall_ext);
if (layerCount == 0)
firstLayerExtruders.emplace_back(resolved);
firstLayerExtruders.emplace_back(wall_ext);
}
}
@@ -935,19 +981,24 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// Collect the support extruders.
for (auto support_layer : object.support_layers()) {
LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z);
ExtrusionRole role = support_layer->support_fills.role();
bool has_support = false;
bool has_interface = false;
for (const ExtrusionEntity *ee : support_layer->support_fills.entities) {
ExtrusionRole er = ee->role();
if (er == erSupportMaterial || er == erSupportTransition) has_support = true;
if (er == erSupportMaterialInterface) has_interface = true;
if (has_support && has_interface) break;
}
unsigned int extruder_support = object.config().support_filament.value;
unsigned int extruder_interface = object.config().support_interface_filament.value;
LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z);
layer_tools.layer_height = support_layer->height;
ExtrusionRole role = support_layer->support_fills.role();
bool has_support = role == erMixed || role == erSupportMaterial || role == erSupportTransition;
bool has_interface = role == erMixed || role == erSupportMaterialInterface;
unsigned int extruder_support = resolve_mixed(object.config().support_filament.value,
layer_tools.layer_index,
float(support_layer->print_z),
float(support_layer->height));
unsigned int extruder_interface = resolve_mixed(object.config().support_interface_filament.value,
layer_tools.layer_index,
float(support_layer->print_z),
float(support_layer->height));
if (has_support) {
// BP-only fallback: when support_filament is unset and an interface
// exists, pick the lowest-flush non-soluble body extruder.
if (extruder_support > 0 || !has_interface || extruder_interface == 0 || layer_tools.has_object)
layer_tools.extruders.push_back(extruder_support);
else {
@@ -956,7 +1007,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
std::vector<float> flush_matrix(
cast<float>(get_flush_volumes_matrix(object.print()->config().flush_volumes_matrix.values, 0, object.print()->config().nozzle_diameter.values.size())));
const unsigned int number_of_extruders = (unsigned int) (sqrt(flush_matrix.size()) + EPSILON);
// Extract purging volumes for each extruder pair:
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < number_of_extruders; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders, flush_matrix.begin() + (i + 1) * number_of_extruders));
@@ -983,8 +1033,10 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
for (auto& layer : m_layer_tools) {
// Sort and remove duplicates
sort_remove_duplicates(layer.extruders);
if (layer.preserve_extruder_order)
remove_duplicates_preserve_order(layer.extruders);
else
sort_remove_duplicates(layer.extruders);
// make sure that there are some tools for each object layer (e.g. tall wiping object will result in empty extruders vector)
if (layer.extruders.empty() && layer.has_object)
@@ -1539,6 +1591,9 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
&filament_sequences
);
// TODO(fs-port): for layers with preserve_extruder_order=true the stats
// here reflect the optimized sequence; the guarded writeback below keeps
// the original ordering. UI-only divergence — see line ~1610 below.
auto curr_flush_info = calc_filament_change_info_by_toolorder(print_config, filament_maps, nozzle_flush_mtx, filament_sequences);
if (nozzle_nums <= 1)
m_stats_by_single_extruder = curr_flush_info;
@@ -1580,8 +1635,19 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
}
for (size_t i = 0; i < filament_sequences.size(); ++i)
for (size_t i = 0; i < filament_sequences.size(); ++i) {
// FS preserve_extruder_order guard: keep the layer's existing extruder
// ordering by skipping writeback of the optimized sequence.
//
// Note: BP runs the optimizer (and updates stats) BEFORE this writeback
// because the optimizer is a free function in ToolOrderUtils.cpp without
// LayerTools access. Stats may diverge from g-code reality on
// preserve-order layers. See FS ToolOrdering.cpp:1156-1159 for the
// upstream behavior that guards earlier in the call chain.
if (m_layer_tools[i].preserve_extruder_order)
continue;
m_layer_tools[i].extruders = std::move(filament_sequences[i]);
}
}
// Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed.
void ToolOrdering::mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height)

View File

@@ -157,6 +157,9 @@ public:
bool has_support = false;
// Zero based extruder IDs, ordered to minimize tool switches.
std::vector<unsigned int> extruders;
// When set, downstream reorder passes leave this layer's extruder
// sequence in place (used by grouped manual mixed-filament patterns).
bool preserve_extruder_order = false;
// If per layer extruder switches are inserted by the G-code preview slider, this value contains the new (1 based) extruder, with which the whole object layer is being printed with.
// If not overriden, it is set to 0.
unsigned int extruder_override = 0;

View File

@@ -1829,15 +1829,14 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
{
const size_t num_layers = segmented_regions.size();
std::vector<std::vector<ExPolygons>> segmented_regions_merged(num_layers);
segmented_regions_merged.assign(num_layers, std::vector<ExPolygons>(num_facets_states - 1));
segmented_regions_merged.assign(num_layers, std::vector<ExPolygons>(num_facets_states));
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states);
// Zero is skipped because it is the default color of the volume
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
for (size_t extruder_id = 0; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback();
if (!segmented_regions[layer_idx][extruder_id].empty()) {
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
@@ -1849,16 +1848,16 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
}
}
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
segmented_regions_merged[layer_idx][extruder_id] = std::move(segmented_regions_trimmed);
}
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id].empty();
append(segmented_regions_merged[layer_idx][extruder_id], top_and_bottom_layers[extruder_id][layer_idx]);
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
segmented_regions_merged[layer_idx][extruder_id] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
}
}
}

View File

@@ -1174,6 +1174,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
new_full_config.option("mixed_filament_height_lower_bound", true);
new_full_config.option("mixed_filament_height_upper_bound", true);
new_full_config.option("mixed_filament_advanced_dithering", 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_component_bias_enabled", true);
new_full_config.option("mixed_filament_surface_indentation", true);
new_full_config.option("mixed_filament_region_collapse", true);
@@ -1190,6 +1192,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_config.option("mixed_filament_height_lower_bound", true);
m_config.option("mixed_filament_height_upper_bound", true);
m_config.option("mixed_filament_advanced_dithering", true);
m_config.option("mixed_filament_pointillism_pixel_size", true);
m_config.option("mixed_filament_pointillism_line_gap", true);
m_config.option("mixed_filament_component_bias_enabled", true);
m_config.option("mixed_filament_surface_indentation", true);
m_config.option("mixed_filament_region_collapse", true);
@@ -1206,6 +1210,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.option("mixed_filament_height_lower_bound", true);
m_default_object_config.option("mixed_filament_height_upper_bound", true);
m_default_object_config.option("mixed_filament_advanced_dithering", 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_component_bias_enabled", true);
m_default_object_config.option("mixed_filament_surface_indentation", true);
m_default_object_config.option("mixed_filament_region_collapse", true);
@@ -1374,6 +1380,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
float mixed_height_lower = 0.04f;
float mixed_height_upper = 0.16f;
bool mixed_advanced_dither = false;
float mixed_pointillism_pixel_size = 0.f;
float mixed_pointillism_line_gap = 0.f;
float mixed_surface_indentation = 0.f;
std::string mixed_custom_definitions;
@@ -1393,6 +1401,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
else
mixed_advanced_dither = (new_full_config.opt_int("mixed_filament_advanced_dithering") != 0);
}
if (new_full_config.has("mixed_filament_pointillism_pixel_size"))
mixed_pointillism_pixel_size = float(new_full_config.opt_float("mixed_filament_pointillism_pixel_size"));
if (new_full_config.has("mixed_filament_pointillism_line_gap"))
mixed_pointillism_line_gap = float(new_full_config.opt_float("mixed_filament_pointillism_line_gap"));
if (new_full_config.has("mixed_filament_surface_indentation"))
mixed_surface_indentation = float(new_full_config.opt_float("mixed_filament_surface_indentation"));
if (new_full_config.has("mixed_filament_definitions"))
@@ -1401,6 +1413,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
mixed_gradient_mode = std::clamp(mixed_gradient_mode, 0, 1);
mixed_height_lower = std::max(0.01f, mixed_height_lower);
mixed_height_upper = std::max(mixed_height_lower, mixed_height_upper);
mixed_pointillism_pixel_size = std::max(0.f, mixed_pointillism_pixel_size);
mixed_pointillism_line_gap = std::max(0.f, mixed_pointillism_line_gap);
mixed_surface_indentation = std::clamp(mixed_surface_indentation, -2.f, 2.f);
BOOST_LOG_TRIVIAL(info) << "Print::apply mixed settings"
@@ -1408,6 +1422,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
<< ", lower=" << mixed_height_lower
<< ", upper=" << mixed_height_upper
<< ", advanced_dither=" << (mixed_advanced_dither ? 1 : 0)
<< ", pointillism_pixel_size=" << mixed_pointillism_pixel_size
<< ", pointillism_line_gap=" << mixed_pointillism_line_gap
<< ", surface_indentation=" << mixed_surface_indentation
<< ", custom_definitions_len=" << mixed_custom_definitions.size()
<< ", physical_extruders=" << num_extruders;

View File

@@ -992,6 +992,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;
@@ -1467,6 +1468,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, 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 });
@@ -1488,6 +1490,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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -35,9 +35,48 @@ static PrinterTechnology printer_technology()
return wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology();
}
static int physical_filaments_count()
{
return std::max(wxGetApp().filaments_cnt(), 0);
}
static int filaments_count()
{
return wxGetApp().filaments_cnt();
if (wxGetApp().preset_bundle == nullptr)
return 0;
const int physical = physical_filaments_count();
const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments;
return static_cast<int>(mixed_mgr.total_filaments(size_t(physical)));
}
static std::vector<unsigned int> ui_ordered_filament_ids()
{
if (wxGetApp().plater() == nullptr)
return {};
return wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids();
}
static wxString filament_menu_item_name(const int filament_id_1based, const int display_filament_id_1based)
{
if (filament_id_1based <= 0)
return _L("Default");
if (wxGetApp().preset_bundle == nullptr)
return wxString::Format(_L("Filament %d"), filament_id_1based);
const int physical = physical_filaments_count();
if (filament_id_1based <= physical) {
const size_t preset_idx = size_t(filament_id_1based - 1);
const auto &filament_presets = wxGetApp().preset_bundle->filament_presets;
if (preset_idx < filament_presets.size()) {
auto preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[preset_idx]);
if (preset != nullptr)
return from_u8(preset->label(false));
}
return wxString::Format(_L("Filament %d"), filament_id_1based);
}
return wxString::Format(_L("Mixed Filament %d"), display_filament_id_1based);
}
static bool is_improper_category(const std::string& category, const int filaments_cnt, const bool is_object_settings = true)
@@ -1022,8 +1061,9 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
menu->Destroy(item_id);
}
const int filaments_cnt = filaments_count();
if (filaments_cnt <= 1)
// Use UI-ordered filament IDs (physical first, then enabled mixed in UI order)
const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
if (ordered_filament_ids.size() <= 1)
return;
wxDataViewItemArray sels;
@@ -1039,24 +1079,16 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
if (sels.Count() == 1) {
const ModelConfig& config = obj_list()->get_item_config(sels[0]);
// BBS: set default extruder to 1
initial_extruder = config.has("extruder") ? config.extruder() : 1;
initial_extruder = config.has("extruder") ? config.extruder() : 0;
}
for (int i = 0; i <= filaments_cnt; i++)
for (size_t display_idx = 0; display_idx <= ordered_filament_ids.size(); ++display_idx)
{
bool is_active_extruder = i == initial_extruder;
int icon_idx = i == 0 ? 0 : i - 1;
const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
const bool is_active_extruder = actual_filament_id == initial_extruder;
const int icon_idx = actual_filament_id == 0 ? 0 : actual_filament_id - 1;
wxString item_name = _L("Default");
if (i > 0) {
auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]);
if (preset == nullptr) {
item_name = wxString::Format(_L("Filament %d"), i);
} else {
item_name = from_u8(preset->label(false));
}
}
wxString item_name = filament_menu_item_name(actual_filament_id, int(display_idx));
if (is_active_extruder) {
item_name << " (" + _L("current") + ")";
@@ -1064,11 +1096,13 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
if (icon_idx >= 0 && icon_idx < icons.size()) {
append_menu_item(
extruder_selection_menu, wxID_ANY, item_name, "", [i](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(i); }, *icons[icon_idx], menu,
extruder_selection_menu, wxID_ANY, item_name, "",
[actual_filament_id](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(actual_filament_id); }, *icons[icon_idx], menu,
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
} else {
append_menu_item(
extruder_selection_menu, wxID_ANY, item_name, "", [i](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(i); }, "", menu,
extruder_selection_menu, wxID_ANY, item_name, "",
[actual_filament_id](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(actual_filament_id); }, "", menu,
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
}
}
@@ -2190,8 +2224,8 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
menu->Destroy(item_id);
}
int filaments_cnt = filaments_count();
if (filaments_cnt <= 1)
const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
if (ordered_filament_ids.size() <= 1)
return;
wxDataViewItemArray sels;
@@ -2206,12 +2240,10 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
}
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
if (icons.size() < filaments_cnt) {
BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: icons size %1%, filaments_cnt=%2%")%icons.size()%filaments_cnt;
if (icons.size() < ordered_filament_ids.size()) {
BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: icons size %1%, filaments_cnt=%2%") % icons.size() % ordered_filament_ids.size();
if (icons.size() <= 1)
return;
else
filaments_cnt = icons.size();
}
wxMenu* extruder_selection_menu = new wxMenu();
const wxString& name = sels.Count() == 1 ? names[0] : names[1];
@@ -2219,47 +2251,27 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
int initial_extruder = -1; // negative value for multiple object/part selection
if (sels.Count() == 1) {
const ModelConfig& config = obj_list()->get_item_config(sels[0]);
// BBS
const auto sel_vol = obj_list()->get_selected_model_volume();
if (sel_vol && sel_vol->type() == ModelVolumeType::PARAMETER_MODIFIER)
initial_extruder = config.has("extruder") ? config.extruder() : 0;
else
initial_extruder = config.has("extruder") ? config.extruder() : 1;
initial_extruder = config.has("extruder") ? config.extruder() : 0;
}
// BBS
bool has_modifier = false;
for (auto sel : sels) {
if (obj_list()->GetModel()->GetVolumeType(sel) == ModelVolumeType::PARAMETER_MODIFIER) {
has_modifier = true;
break;
}
}
for (int i = has_modifier ? 0 : 1; i <= filaments_cnt; i++)
for (size_t display_idx = 0; display_idx <= ordered_filament_ids.size(); ++display_idx)
{
// BBS
//bool is_active_extruder = i == initial_extruder;
const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
bool is_active_extruder = false;
wxString item_name = _L("Default");
if (i > 0) {
auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]);
if (preset == nullptr) {
item_name = wxString::Format(_L("Filament %d"), i);
} else {
item_name = from_u8(preset->label(false));
}
}
wxString item_name = filament_menu_item_name(actual_filament_id, int(display_idx));
if (is_active_extruder) {
item_name << " (" + _L("current") + ")";
}
wxBitmap bm = (i == 0 || size_t(i - 1) >= icons.size()) ? wxNullBitmap : *icons[i - 1];
append_menu_item(extruder_selection_menu, wxID_ANY, item_name, "",
[i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, bm, menu,
[actual_filament_id](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(actual_filament_id); },
actual_filament_id == 0 || size_t(actual_filament_id - 1) >= icons.size() ? wxNullBitmap : *icons[size_t(actual_filament_id - 1)], menu,
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
}
menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament"));

View File

@@ -11429,6 +11429,12 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent &event)
if (wxGetApp().app_config->get("auto_calculate_flush") != "disabled") {
sidebar->auto_calc_flushing_volumes(modify_id);
}
// Regenerate mixed filaments and refresh the mixed panel only. Color
// changes do not alter filament IDs, so the full on_filaments_change()
// path is unnecessary and can re-enter UI rebuilds mid-update.
wxGetApp().preset_bundle->update_multi_material_filament_presets();
sidebar->update_mixed_filament_panel();
}
void Plater::priv::install_network_plugin(wxCommandEvent &event)

View File

@@ -1615,6 +1615,49 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
}
}
if (opt_key == "dithering_local_z_mode") {
const bool local_z_enabled = boost::any_cast<bool>(value);
if (local_z_enabled &&
(!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 (!local_z_enabled &&
m_config->has("dithering_local_z_whole_objects") &&
m_config->option("dithering_local_z_whole_objects") != nullptr &&
m_config->opt_bool("dithering_local_z_whole_objects")) {
change_opt_value(*m_config, "dithering_local_z_whole_objects", boost::any(false));
if (m_type == Preset::TYPE_PRINT) {
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
project_cfg.set_key_value("dithering_local_z_whole_objects", new ConfigOptionBool(false));
}
if (Field *field = this->get_field("dithering_local_z_whole_objects"))
field->set_value(boost::any(false), false);
update_dirty();
}
if (!local_z_enabled &&
m_config->has("dithering_local_z_direct_multicolor") &&
m_config->option("dithering_local_z_direct_multicolor") != nullptr &&
m_config->opt_bool("dithering_local_z_direct_multicolor")) {
change_opt_value(*m_config, "dithering_local_z_direct_multicolor", boost::any(false));
if (m_type == Preset::TYPE_PRINT) {
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
project_cfg.set_key_value("dithering_local_z_direct_multicolor", new ConfigOptionBool(false));
}
if (Field *field = this->get_field("dithering_local_z_direct_multicolor"))
field->set_value(boost::any(false), false);
update_dirty();
}
}
// reload scene to update timelapse wipe tower
if (opt_key == "timelapse_type") {
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
@@ -1907,10 +1950,42 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
return;
}
const bool refresh_mixed_filament_panel =
m_type == Preset::TYPE_PRINT && opt_key == "mixed_filament_component_bias_enabled";
// Keep Mixed Filaments global settings in sync with project_config. In
// full_fff_config(), project_config is applied last and would otherwise
// override the edited print preset value from the Others panel.
if (m_type == Preset::TYPE_PRINT &&
(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_advanced_dithering" ||
opt_key == "mixed_filament_pointillism_pixel_size" ||
opt_key == "mixed_filament_pointillism_line_gap" ||
opt_key == "mixed_filament_component_bias_enabled" ||
opt_key == "mixed_filament_surface_indentation" ||
opt_key == "mixed_filament_region_collapse" ||
opt_key == "dithering_z_step_size" ||
opt_key == "dithering_local_z_mode" ||
opt_key == "dithering_local_z_whole_objects" ||
opt_key == "dithering_local_z_direct_multicolor" ||
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))
project_cfg.set_key_value(opt_key, opt->clone());
}
update();
if(m_active_page)
m_active_page->update_visibility(m_mode, true);
m_page_view->GetParent()->Layout();
if (refresh_mixed_filament_panel && wxGetApp().plater() != nullptr)
wxGetApp().sidebar().update_mixed_filament_panel(false);
}
void Tab::show_timelapse_warning_dialog() {

View File

@@ -6,6 +6,9 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/Slicing.hpp"
#include <boost/filesystem.hpp>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
@@ -130,3 +133,57 @@ TEST_CASE("[review-fixes] mixed/dithering option keys exist in PrintConfig",
REQUIRE(print_config_def.get(k) != nullptr);
}
}
TEST_CASE("[review-fixes] clear_local_z_plan called from clear_layers", "[review-fixes][mixed-filament]")
{
// Sentinel: ensure the source contains the 3 invalidation hooks the
// FullSpectrum verification report flagged as missing. This is a
// smoke-test against the source so we can detect future regressions
// without depending on a full slicing-pipeline harness.
namespace fs = boost::filesystem;
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
const fs::path src = repo / "src" / "libslic3r" / "PrintObject.cpp";
REQUIRE(fs::exists(src));
std::ifstream in(src.string());
std::stringstream buf; buf << in.rdbuf();
const std::string body = buf.str();
const auto count_substr = [&](const std::string &needle) {
size_t n = 0, pos = 0;
while ((pos = body.find(needle, pos)) != std::string::npos) { ++n; ++pos; }
return n;
};
// Three FS-mandated call sites: clear_layers, invalidate_step(posSlice),
// invalidate_all_steps. Plus the one already-present site inside
// build_local_z_plan(). PrintObject.cpp itself has 3 (4 once the
// backport is complete).
REQUIRE(count_substr("this->clear_local_z_plan()") >= 3);
}
TEST_CASE("[review-fixes] merge_segmented_layers preserves channel 0", "[review-fixes][mixed-filament]")
{
// Sentinel: ensure merge_segmented_layers uses the FS shape
// (output sized num_facets_states, channel 0 = default), not the
// pre-FS-a11b70e3a shape (output sized num_facets_states - 1,
// channel 0 dropped). The FS-verbatim apply_mm_segmentation in
// PrintObjectSlice.cpp expects channel 0 to be present; mismatching
// shapes silently shifts every painted region's filament_id by -1
// (e.g. paint with mixed slot 4 -> applied as physical filament 3).
namespace fs = boost::filesystem;
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
const fs::path src = repo / "src" / "libslic3r" / "MultiMaterialSegmentation.cpp";
REQUIRE(fs::exists(src));
std::ifstream in(src.string());
std::stringstream buf; buf << in.rdbuf();
const std::string body = buf.str();
// Producer must NOT subtract one from num_facets_states when sizing the output.
REQUIRE(body.find("num_facets_states - 1") == std::string::npos);
// Producer must NOT shift indexing back by one when writing into the output.
REQUIRE(body.find("[extruder_id - 1]") == std::string::npos);
// Loop must include channel 0 (extruder_id starts at 0, not 1).
REQUIRE(body.find("for (size_t extruder_id = 0; extruder_id < num_facets_states") != std::string::npos);
}