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-06-01 09:24:54 -03:00
committed by Ian Bassi
parent 0e0e34c8b4
commit c0dfe50bc5
10 changed files with 1516 additions and 184 deletions
+63 -51
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)
@@ -1042,8 +1081,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;
@@ -1059,24 +1099,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") + ")";
@@ -1084,11 +1116,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);
}
}
@@ -2213,8 +2247,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;
@@ -2229,12 +2263,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];
@@ -2242,47 +2274,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"));
+6
View File
@@ -11471,6 +11471,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)
+75
View File
@@ -1646,6 +1646,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;
@@ -1961,10 +2004,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() {