diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 16def6d348..5ed348ec14 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -18332,6 +18332,16 @@ msgstr "" msgid "Set the physical nozzle count..." msgstr "" +#, possible-c-format, possible-boost-format +msgid "Error: %s extruder has no available %s nozzle, current group result is invalid." +msgstr "" + +msgid "Please adjust your grouping or click " +msgstr "" + +msgid " to set nozzle count" +msgstr "" + msgid "Prime tower is required for nozzle changing. There may be flaws on the model without prime tower. Are you sure you want to disable prime tower?" msgstr "" diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 5d151ca43f..1f3e26e1e8 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -5945,6 +5945,72 @@ int CLI::run(int argc, char **argv) else filament_maps = part_plate->get_real_filament_maps(m_print_config); + // Multi-nozzle printers need the per-filament volume assignment as a grouping + // input in the manual modes: synthesize it from the per-extruder flow types when + // the caller did not provide one (an extruder whose nozzle stats span several + // volume types keeps the per-filament choice), and require explicit maps in + // nozzle-manual mode. + auto max_nozzle_counts_opt = m_print_config.option("extruder_max_nozzle_count"); + // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. + bool support_multi_nozzle = + max_nozzle_counts_opt && + std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(), + [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); }); + if (support_multi_nozzle && (mode == fmmManual || mode == fmmNozzleManual) && (plate_to_slice != 0)) { + // Orca: the grouping result is reconstructed purely from the passed maps in + // nozzle-manual mode, so all of them must be present (there are no separate + // per-nozzle CLI parameters to rebuild them from). + if (mode == FilamentMapMode::fmmNozzleManual && + (!m_extra_config.has("filament_volume_map") || !m_extra_config.has("filament_nozzle_map") || + !m_extra_config.has("filament_map"))) { + BOOST_LOG_TRIVIAL(error) + << boost::format("%1%, can not find filament_volume_map/filament_nozzle_map/filament_map under Nozzle Manual mode") % __LINE__; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, index + 1, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } + + if (mode == fmmManual) { + // Build the volume map when absent: filaments on a single-volume extruder + // print with that extruder's volume; a mixed-volume extruder keeps the + // per-filament choice (default Standard). + std::vector using_nozzle_volume_type = new_nozzle_volume_type; + using_nozzle_volume_type.resize(new_extruder_count, nvtStandard); + if (auto extruder_nozzle_stats_opt = m_print_config.option("extruder_nozzle_stats")) { + auto nozzle_stats = get_extruder_nozzle_stats(extruder_nozzle_stats_opt->values); + for (int e_index = 0; e_index < new_extruder_count && e_index < (int) nozzle_stats.size(); e_index++) { + if (nozzle_stats[e_index].size() > 1) { + using_nozzle_volume_type[e_index] = nvtHybrid; + BOOST_LOG_TRIVIAL(info) << boost::format("%1% : extruder %2%, set nozzle_volume_type to hybrid ") % __LINE__ % (e_index + 1); + } + } + } + std::vector &manual_volume_maps = m_extra_config.option("filament_volume_map", true)->values; + // default to standard flow + manual_volume_maps.resize(filament_count, (int) (nvtStandard)); + for (int f_index = 0; f_index < filament_count && f_index < (int) filament_maps.size(); f_index++) { + int f_extruder_index = filament_maps[f_index] - 1; + if (f_extruder_index >= 0 && f_extruder_index < new_extruder_count && + using_nozzle_volume_type[f_extruder_index] != nvtHybrid) { + manual_volume_maps[f_index] = int(using_nozzle_volume_type[f_extruder_index]); + BOOST_LOG_TRIVIAL(info) << boost::format("%1% : filament %2% extruder %3%, set filament_volume_map to %4% ") % __LINE__ % (f_index + 1) % (f_extruder_index + 1) % manual_volume_maps[f_index]; + } + } + } + + if (m_extra_config.has("filament_volume_map")) { + part_plate->set_filament_volume_maps(m_extra_config.option("filament_volume_map")->values); + } + if (m_extra_config.has("filament_nozzle_map")) { + part_plate->set_filament_nozzle_maps(m_extra_config.option("filament_nozzle_map")->values); + } + } + else if (!support_multi_nozzle && (mode == fmmNozzleManual)) { + BOOST_LOG_TRIVIAL(error) + << boost::format("%1%, Nozzle Manual mode not supported for %2%") % __LINE__ % new_printer_name; + record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, index + 1, cli_errors[CLI_INVALID_PARAMS], sliced_info); + flush_and_exit(CLI_INVALID_PARAMS); + } + for (int index = 0; index < filament_maps.size(); index++) { int filament_extruder = filament_maps[index]; @@ -6183,6 +6249,22 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << "print::process: first time_using_cache is " << time_using_cache << " secs."; } if (printer_technology == ptFFF) { + // Read the engine's final grouping back onto the plate so an exported + // project (--export-3mf / gcode.3mf) carries the concrete maps in its + // plate settings, matching what a GUI slice persists. + // Orca: deliberately gated to multi-extruder printers so single-extruder + // exports keep their plate settings unchanged. + if (new_extruder_count > 1) { + FilamentMapMode current_map_mode = print_fff->config().filament_map_mode.value; + if (is_auto_filament_map_mode(current_map_mode)) { + part_plate->set_filament_maps(print_fff->get_filament_maps()); + part_plate->set_filament_volume_maps(print_fff->get_filament_volume_maps()); + } + if (current_map_mode != FilamentMapMode::fmmNozzleManual) { + part_plate->set_filament_nozzle_maps(print_fff->get_filament_nozzle_maps()); + } + } + std::string conflict_result = print_fff->get_conflict_string(); if (!conflict_result.empty()) { BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": found slicing result conflict!"<< std::endl; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index ac31907f6d..01cbc43bc2 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -61,6 +61,9 @@ static std::vector s_project_options { // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament // slot resolution survives preset switches. "filament_volume_map", + // Per-filament physical-nozzle choice the grouping engine writes back; project-level so a + // saved project round-trips the assignment alongside filament_map/filament_volume_map. + "filament_nozzle_map", // Filament Track Switch device state: whether the switch is installed and ready, and // whether dynamic per-nozzle filament mapping is active. Persisted with the project and // restored from a saved 3mf; reset to false on load and set true only by live device sync. @@ -2717,6 +2720,9 @@ void PresetBundle::update_selections(AppConfig &config) std::vector filament_maps(filament_colors.size(), 1); project_config.option("filament_map")->values = filament_maps; + std::vector filament_nozzle_maps(filament_colors.size(), 0); + project_config.option("filament_nozzle_map")->values = filament_nozzle_maps; + std::vector filament_volume_maps(filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); project_config.option("filament_volume_map")->values = filament_volume_maps; @@ -2864,6 +2870,9 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p std::vector filament_maps(filament_colors.size(), 1); project_config.option("filament_map")->values = filament_maps; + std::vector filament_nozzle_maps(filament_colors.size(), 0); + project_config.option("filament_nozzle_map")->values = filament_nozzle_maps; + std::vector filament_volume_maps(filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); project_config.option("filament_volume_map")->values = filament_volume_maps; @@ -3043,6 +3052,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); @@ -3053,6 +3063,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne } filament_color_type->resize(n); filament_map->values.resize(n, 1); + filament_nozzle_map->values.resize(n, 0); filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); @@ -3081,6 +3092,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); @@ -3091,6 +3103,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) } filament_color_type->resize(n); filament_map->values.resize(n, 1); + filament_nozzle_map->values.resize(n, 0); filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); @@ -3132,12 +3145,16 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); if (filament_color->values.size() > to_del_flament_id) { filament_color->values.erase(filament_color->values.begin() + to_del_flament_id); if (filament_map->values.size() > to_del_flament_id) { filament_map->values.erase(filament_map->values.begin() + to_del_flament_id); } + if (filament_nozzle_map->values.size() > to_del_flament_id) { + filament_nozzle_map->values.erase(filament_nozzle_map->values.begin() + to_del_flament_id); + } if (filament_volume_map->values.size() > to_del_flament_id) { filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id); } @@ -3145,6 +3162,7 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) else { filament_color->values.resize(to_del_flament_id); filament_map->values.resize(to_del_flament_id, 1); + filament_nozzle_map->values.resize(to_del_flament_id, 0); filament_volume_map->values.resize(to_del_flament_id, static_cast(NozzleVolumeType::nvtStandard)); } diff --git a/src/slic3r/GUI/DragDropPanel.cpp b/src/slic3r/GUI/DragDropPanel.cpp index 352880cffb..0c1371c307 100644 --- a/src/slic3r/GUI/DragDropPanel.cpp +++ b/src/slic3r/GUI/DragDropPanel.cpp @@ -1,9 +1,14 @@ #include "DragDropPanel.hpp" +#include "GUI_App.hpp" +#include "I18N.hpp" #include "Widgets/Label.hpp" +#include "Widgets/StateColor.hpp" #include namespace Slic3r { namespace GUI { +wxDEFINE_EVENT(wxEVT_DRAG_DROP_COMPLETED, wxCommandEvent); + struct CustomData { int filament_id; @@ -201,7 +206,7 @@ wxDragResult ColorDropTarget::OnData(wxCoord x, wxCoord y, wxDragResult def) /////////////// ColorDropTarget end //////////////////////// -DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto) +DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto, bool has_title, bool is_sub) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) , m_is_auto(is_auto) { @@ -209,22 +214,31 @@ DragDropPanel::DragDropPanel(wxWindow *parent, const wxString &label, bool is_au m_sizer = new wxBoxSizer(wxVERTICAL); - auto title_panel = new wxPanel(this); - title_panel->SetBackgroundColour(0xEEEEEE); - auto title_sizer = new wxBoxSizer(wxHORIZONTAL); - title_panel->SetSizer(title_sizer); + if (has_title) { + auto title_panel = new wxPanel(this); + title_panel->SetBackgroundColour(is_sub ? wxColour(0xF8F8F8) : wxColour(0xEEEEEE)); + auto title_sizer = new wxBoxSizer(wxHORIZONTAL); + title_panel->SetSizer(title_sizer); - Label* static_text = new Label(this, label); - static_text->SetFont(Label::Head_13); - static_text->SetBackgroundColour(0xEEEEEE); + m_title_label = new Label(this, label); + m_title_label->SetFont(is_sub ? Label::Body_12 : Label::Head_13); + if (is_sub) + m_title_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); + m_title_label->SetBackgroundColour(is_sub ? wxColour(0xF8F8F8) : wxColour(0xEEEEEE)); - title_sizer->Add(static_text, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); + title_sizer->Add(m_title_label, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); - m_sizer->Add(title_panel, 0, wxEXPAND); - m_sizer->AddSpacer(10); + m_sizer->Add(title_panel, 0, wxEXPAND); + m_sizer->AddSpacer(10); + } - m_grid_item_sizer = new wxGridSizer(0, 6, FromDIP(8), FromDIP(8)); // row = 0, col = 3, 10 10 is space - m_sizer->Add(m_grid_item_sizer, 1, wxEXPAND | wxALL, FromDIP(8)); + if (is_sub) { + m_grid_item_sizer = new wxGridSizer(0, 3, FromDIP(6), FromDIP(6)); + m_sizer->Add(m_grid_item_sizer, 0, wxEXPAND); + } else { + m_grid_item_sizer = new wxGridSizer(0, 6, FromDIP(8), FromDIP(8)); // row = 0, col = 3, 10 10 is space + m_sizer->Add(m_grid_item_sizer, 1, wxEXPAND | wxALL, FromDIP(8)); + } // set droptarget auto drop_target = new ColorDropTarget(this); @@ -242,20 +256,35 @@ void DragDropPanel::AddColorBlock(const wxColour &color, const std::string &type m_grid_item_sizer->Add(panel, 0); m_filament_blocks.push_back(panel); if (update_ui) { - m_filament_blocks.front()->Refresh(); // FIX BUG: STUDIO-8467 + Freeze(); + + m_grid_item_sizer->Layout(); + Layout(); + Fit(); GetParent()->GetParent()->Layout(); GetParent()->GetParent()->Fit(); + m_filament_blocks.front()->Refresh(); // FIX BUG: STUDIO-8467 + + Thaw(); + NotifyDragDropCompleted(); } } void DragDropPanel::RemoveColorBlock(ColorPanel *panel, bool update_ui) { - m_sizer->Detach(panel); + m_grid_item_sizer->Detach(panel); panel->Destroy(); m_filament_blocks.erase(std::remove(m_filament_blocks.begin(), m_filament_blocks.end(), panel), m_filament_blocks.end()); if (update_ui) { + Freeze(); + + Layout(); + Fit(); GetParent()->GetParent()->Layout(); GetParent()->GetParent()->Fit(); + + Thaw(); + NotifyDragDropCompleted(); } } @@ -270,6 +299,15 @@ void DragDropPanel::DoDragDrop(ColorPanel *panel, const wxColour &color, const s } } +void DragDropPanel::UpdateLabel(const wxString &label) +{ + if (m_title_label) { + m_title_label->SetLabel(label); + m_title_label->Refresh(); + Layout(); + } +} + std::vector DragDropPanel::GetAllFilaments() const { std::vector filaments; @@ -287,4 +325,327 @@ std::vector DragDropPanel::GetAllFilaments() const return filaments; } +void DragDropPanel::NotifyDragDropCompleted() +{ + wxCommandEvent event(wxEVT_DRAG_DROP_COMPLETED); + event.SetEventObject(this); + + wxWindow *parent = GetParent(); + while (parent) { + auto name = parent->GetName(); + if (name == wxT("FilamentMapManualPanel")) { + parent->GetEventHandler()->ProcessEvent(event); + break; + } + parent = parent->GetParent(); + } +} + +class SeparatedColorDropTarget : public wxDropTarget +{ +public: + SeparatedColorDropTarget(SeparatedDragDropPanel *panel) : wxDropTarget(), m_panel(panel) + { + m_data = new ColorDataObject(); + SetDataObject(m_data); + } + + virtual wxDragResult OnData(wxCoord x, wxCoord y, wxDragResult def) override; + virtual bool OnDrop(wxCoord x, wxCoord y) override { return true; } + +private: + SeparatedDragDropPanel *m_panel; + ColorDataObject *m_data; +}; + +wxDragResult SeparatedColorDropTarget::OnData(wxCoord x, wxCoord y, wxDragResult def) +{ + if (!GetData()) return wxDragNone; + + m_panel->AddColorBlock(m_data->GetColor(), m_data->GetType(), m_data->GetFilament(), false); + return wxDragCopy; +} + +SeparatedDragDropPanel::SeparatedDragDropPanel(wxWindow *parent, const wxString &label, bool use_separation) : wxPanel(parent), m_use_separation(use_separation) +{ + SetBackgroundColour(0xF8F8F8); + + m_main_sizer = new wxBoxSizer(wxVERTICAL); + + auto title_panel = new wxPanel(this); + title_panel->SetBackgroundColour(0xEEEEEE); + auto title_sizer = new wxBoxSizer(wxHORIZONTAL); + title_panel->SetSizer(title_sizer); + + m_label = new Label(title_panel, label); + m_label->SetFont(Label::Head_13); + m_label->SetBackgroundColour(0xEEEEEE); + + title_sizer->Add(m_label, 0, wxALIGN_CENTER | wxALL, FromDIP(5)); + + m_main_sizer->Add(title_panel, 0, wxEXPAND); + m_main_sizer->AddSpacer(10); + + m_content_panel = new wxPanel(this); + m_content_panel->SetBackgroundColour(0xF8F8F8); + m_content_sizer = new wxBoxSizer(wxHORIZONTAL); + m_content_panel->SetSizer(m_content_sizer); + + m_high_flow_panel = new DragDropPanel(m_content_panel, _L("High Flow"), false, true, true); + m_standard_panel = new DragDropPanel(m_content_panel, _L("Standard"), false, true, true); + m_unified_panel = new DragDropPanel(m_content_panel, wxEmptyString, false, false); + + m_high_flow_panel->SetBackgroundColour(0xF8F8F8); + m_standard_panel->SetBackgroundColour(0xF8F8F8); + m_unified_panel->SetBackgroundColour(0xF8F8F8); + m_unified_panel->SetMinSize({FromDIP(260), -1}); + + m_main_sizer->Add(m_content_panel, 1, wxEXPAND); + + UpdateLayout(); + + auto drop_target = new SeparatedColorDropTarget(this); + SetDropTarget(drop_target); + + SetSizer(m_main_sizer); + Layout(); + wxGetApp().UpdateDarkUIWin(this); +} + +void SeparatedDragDropPanel::UpdateLayout() +{ + m_content_sizer->Clear(false); + + if (m_use_separation) { + m_unified_panel->Hide(); + m_high_flow_panel->Show(); + m_standard_panel->Show(); + + wxSize content_size = m_content_panel->GetSize(); + int panel_width = (content_size.GetWidth() - FromDIP(1) - FromDIP(8)) / 2; + if (panel_width > 0) { + m_high_flow_panel->SetMinSize(wxSize(panel_width, -1)); + m_standard_panel->SetMinSize(wxSize(panel_width, -1)); + } + + m_content_sizer->Add(m_high_flow_panel, 1, wxEXPAND | wxLEFT, FromDIP(8)); + + // Create the separator once and re-attach it on relayout; Clear(false) above only + // detaches, so allocating here would leak a window per relayout. + if (!m_separator) { + m_separator = new wxPanel(m_content_panel, wxID_ANY); + m_separator->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(0xCE, 0xCE, 0xCE))); + m_separator->SetMinSize(wxSize(FromDIP(1), -1)); + } + m_separator->Show(); + m_content_sizer->Add(m_separator, 0, wxEXPAND | wxALL, FromDIP(4)); + + m_content_sizer->Add(m_standard_panel, 1, wxEXPAND | wxRIGHT, FromDIP(8)); + } else { + m_high_flow_panel->Hide(); + m_standard_panel->Hide(); + if (m_separator) + m_separator->Hide(); + m_unified_panel->Show(); + + m_content_sizer->Add(m_unified_panel, 1, wxEXPAND); + } + m_content_sizer->Layout(); + Layout(); + Fit(); + if (GetParent()) { + GetParent()->Layout(); + if (GetParent()->GetParent()) { + GetParent()->GetParent()->Layout(); + } + } +} + +void SeparatedDragDropPanel::UpdateLabel(const wxString &label) +{ + if (m_label) { + m_label->SetLabel(label); + m_label->Refresh(); + Layout(); + } +} + +void SeparatedDragDropPanel::SetUseSeparation(bool use_separation) +{ + if (m_use_separation != use_separation) { + m_use_separation = use_separation; + + if (use_separation) { + auto blocks = m_unified_panel->get_filament_blocks(); + for (auto &block : blocks) { + m_standard_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false); + m_unified_panel->RemoveColorBlock(block, false); + } + } else { + auto high_flow_blocks = m_high_flow_panel->get_filament_blocks(); + auto standard_blocks = m_standard_panel->get_filament_blocks(); + + for (auto &block : high_flow_blocks) { + m_unified_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false); + m_high_flow_panel->RemoveColorBlock(block, false); + } + + for (auto &block : standard_blocks) { + m_unified_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false); + m_standard_panel->RemoveColorBlock(block, false); + } + } + + UpdateLayout(); + } +} + +void SeparatedDragDropPanel::AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool is_high_flow, bool update_ui) +{ + if (m_use_separation) { + if (is_high_flow) { + m_high_flow_panel->AddColorBlock(color, type, filament_id, update_ui); + } else { + m_standard_panel->AddColorBlock(color, type, filament_id, update_ui); + } + + if (update_ui) { + CallAfter([this]() { + Layout(); + GetParent()->Layout(); + }); + } + } else { + m_unified_panel->AddColorBlock(color, type, filament_id, update_ui); + } +} + +void SeparatedDragDropPanel::RemoveColorBlock(ColorPanel *panel, bool update_ui) +{ + auto high_flow_blocks = m_high_flow_panel->get_filament_blocks(); + auto standard_blocks = m_standard_panel->get_filament_blocks(); + auto unified_blocks = m_unified_panel->get_filament_blocks(); + + if (std::find(high_flow_blocks.begin(), high_flow_blocks.end(), panel) != high_flow_blocks.end()) { + m_high_flow_panel->RemoveColorBlock(panel, update_ui); + } else if (std::find(standard_blocks.begin(), standard_blocks.end(), panel) != standard_blocks.end()) { + m_standard_panel->RemoveColorBlock(panel, update_ui); + } else if (std::find(unified_blocks.begin(), unified_blocks.end(), panel) != unified_blocks.end()) { + m_unified_panel->RemoveColorBlock(panel, update_ui); + } + + if (update_ui && m_use_separation) { + CallAfter([this]() { + Layout(); + GetParent()->Layout(); + }); + } +} + +std::vector SeparatedDragDropPanel::GetAllFilaments() const +{ + if (m_use_separation) { + auto high_flow = m_high_flow_panel->GetAllFilaments(); + auto standard = m_standard_panel->GetAllFilaments(); + + std::vector result; + result.insert(result.end(), high_flow.begin(), high_flow.end()); + result.insert(result.end(), standard.begin(), standard.end()); + return result; + } else { + return m_unified_panel->GetAllFilaments(); + } +} + +std::vector SeparatedDragDropPanel::GetHighFlowFilaments() const +{ + if (m_use_separation) { + return m_high_flow_panel->GetAllFilaments(); + } + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + const int right_eid = 1; + if (nozzle_volumes->values.size() > right_eid) { + int volume_type = nozzle_volumes->values[right_eid]; + if (volume_type == static_cast(NozzleVolumeType::nvtHighFlow)) { + return m_unified_panel->GetAllFilaments(); + } + } + return {}; +} + +std::vector SeparatedDragDropPanel::GetStandardFilaments() const +{ + if (m_use_separation) { + return m_standard_panel->GetAllFilaments(); + } + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + const int right_eid = 1; + if (nozzle_volumes->values.size() > right_eid) { + int volume_type = nozzle_volumes->values[right_eid]; + if (volume_type == static_cast(NozzleVolumeType::nvtStandard)) { + return m_unified_panel->GetAllFilaments(); + } + } + return {}; +} + +std::vector SeparatedDragDropPanel::GetTPUHighFlowFilaments() const +{ + if (m_use_separation) { + return {}; + } + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + const int right_eid = 1; + if (nozzle_volumes->values.size() > right_eid) { + int volume_type = nozzle_volumes->values[right_eid]; + if (volume_type == static_cast(NozzleVolumeType::nvtTPUHighFlow)) { + return m_unified_panel->GetAllFilaments(); + } + } + return {}; +} + +std::vector SeparatedDragDropPanel::get_filament_blocks() const +{ + if (m_use_separation) { + auto high_flow = m_high_flow_panel->get_filament_blocks(); + auto standard = m_standard_panel->get_filament_blocks(); + + std::vector result; + result.insert(result.end(), high_flow.begin(), high_flow.end()); + result.insert(result.end(), standard.begin(), standard.end()); + return result; + } else { + return m_unified_panel->get_filament_blocks(); + } +} + +std::vector SeparatedDragDropPanel::get_high_flow_blocks() const +{ + return m_high_flow_panel->get_filament_blocks(); +} + +std::vector SeparatedDragDropPanel::get_standard_blocks() const +{ + return m_standard_panel->get_filament_blocks(); +} + +void SeparatedDragDropPanel::ClearAllBlocks() +{ + auto high_flow_blocks = m_high_flow_panel->get_filament_blocks(); + for (auto &block : high_flow_blocks) { + m_high_flow_panel->RemoveColorBlock(block, false); + } + + auto standard_blocks = m_standard_panel->get_filament_blocks(); + for (auto &block : standard_blocks) { + m_standard_panel->RemoveColorBlock(block, false); + } + + auto unified_blocks = m_unified_panel->get_filament_blocks(); + for (auto &block : unified_blocks) { + m_unified_panel->RemoveColorBlock(block, false); + } +} + }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/DragDropPanel.hpp b/src/slic3r/GUI/DragDropPanel.hpp index d988164839..4f2a4bced0 100644 --- a/src/slic3r/GUI/DragDropPanel.hpp +++ b/src/slic3r/GUI/DragDropPanel.hpp @@ -3,6 +3,7 @@ #include "GUI.hpp" #include "GUI_Utils.hpp" +#include "Widgets/Label.hpp" #include #include @@ -13,17 +14,20 @@ namespace Slic3r { namespace GUI { +wxDECLARE_EVENT(wxEVT_DRAG_DROP_COMPLETED, wxCommandEvent); + wxColor Hex2Color(const std::string& str); class ColorPanel; class DragDropPanel : public wxPanel { public: - DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto); + DragDropPanel(wxWindow *parent, const wxString &label, bool is_auto, bool has_title = true, bool is_sub = false); void AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool update_ui = true); void RemoveColorBlock(ColorPanel *panel, bool update_ui = true); void DoDragDrop(ColorPanel *panel, const wxColour &color, const std::string &type, int filament_id); + void UpdateLabel(const wxString &label); std::vector GetAllFilaments() const; @@ -35,9 +39,12 @@ public: private: wxBoxSizer *m_sizer; wxGridSizer *m_grid_item_sizer; + Label *m_title_label = nullptr; bool m_is_auto; std::vector m_filament_blocks; + + void NotifyDragDropCompleted(); private: bool m_is_draging = false; }; @@ -64,6 +71,49 @@ private: int m_filament_id; }; + +// Drop zone for one extruder that can split into two sub-zones (High Flow / Standard) +// when the extruder mixes nozzle volume types (Hybrid flow), and collapses back to a +// single unified zone otherwise. +class SeparatedDragDropPanel : public wxPanel +{ +public: + SeparatedDragDropPanel(wxWindow *parent, const wxString &label, bool use_separation = false); + + void AddColorBlock(const wxColour &color, const std::string &type, int filament_id, bool is_high_flow = false, bool update_ui = true); + void RemoveColorBlock(ColorPanel *panel, bool update_ui = true); + + std::vector GetAllFilaments() const; + std::vector GetHighFlowFilaments() const; + std::vector GetStandardFilaments() const; + std::vector GetTPUHighFlowFilaments() const; + + std::vector get_filament_blocks() const; + std::vector get_high_flow_blocks() const; + std::vector get_standard_blocks() const; + + void SetUseSeparation(bool use_separation); + bool IsUseSeparation() const { return m_use_separation; } + void ClearAllBlocks(); + void UpdateLabel(const wxString &label); + +private: + void UpdateLayout(); + + wxBoxSizer *m_main_sizer; + wxPanel *m_content_panel; + wxBoxSizer *m_content_sizer; + wxPanel *m_separator = nullptr; + Label *m_label; + + DragDropPanel *m_high_flow_panel; + DragDropPanel *m_standard_panel; + + DragDropPanel *m_unified_panel; + + bool m_use_separation; +}; + }} // namespace Slic3r::GUI #endif /* slic3r_DragDropPanel_hpp_ */ diff --git a/src/slic3r/GUI/FilamentMapDialog.cpp b/src/slic3r/GUI/FilamentMapDialog.cpp index 19b48ce5f1..12ca3a2965 100644 --- a/src/slic3r/GUI/FilamentMapDialog.cpp +++ b/src/slic3r/GUI/FilamentMapDialog.cpp @@ -36,6 +36,13 @@ static std::vector get_applied_map(DynamicConfig& proj_config, const Plater return plater_ref->get_global_filament_map(); } +static std::vector get_applied_volume_map(DynamicConfig& proj_config, const Plater* plater_ref, const PartPlate* partplate_ref, const bool sync_plate) +{ + if (sync_plate) + return partplate_ref->get_real_filament_volume_maps(proj_config); + return plater_ref->get_global_filament_volume_map(); +} + extern std::string& get_left_extruder_unprintable_text(); extern std::string& get_right_extruder_unprintable_text(); @@ -123,7 +130,9 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p std::vector filament_types = full_config.option("filament_type")->values; FilamentMapMode applied_mode = get_applied_map_mode(full_config, plater_ref,partplate_ref, sync_plate); std::vector applied_maps = get_applied_map(full_config, plater_ref, partplate_ref, sync_plate); + std::vector applied_volume_maps = get_applied_volume_map(full_config, plater_ref, partplate_ref, sync_plate); applied_maps.resize(filament_colors.size(), 1); + applied_volume_maps.resize(filament_colors.size(), 0); if (!force_pop_up && applied_mode != fmmManual) return true; @@ -141,6 +150,7 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p filament_colors, filament_types, applied_maps, + applied_volume_maps, filament_lists, applied_mode, plater_ref->get_machine_sync_status(), @@ -152,25 +162,32 @@ bool try_pop_up_before_slice(bool is_slice_all, Plater* plater_ref, PartPlate* p if (ret == wxID_OK) { FilamentMapMode new_mode = map_dlg.get_mode(); std::vector new_maps = map_dlg.get_filament_maps(); + std::vector new_volume_maps = map_dlg.get_filament_volume_maps(); if (sync_plate) { if (is_slice_all) { auto plate_list = plater_ref->get_partplate_list().get_plate_list(); for (int i = 0; i < plate_list.size(); ++i) { plate_list[i]->set_filament_map_mode(new_mode); - if(new_mode == fmmManual) + if (new_mode == fmmManual) { plate_list[i]->set_filament_maps(new_maps); + plate_list[i]->set_filament_volume_maps(new_volume_maps); + } } } else { partplate_ref->set_filament_map_mode(new_mode); - if (new_mode == fmmManual) + if (new_mode == fmmManual) { partplate_ref->set_filament_maps(new_maps); + partplate_ref->set_filament_volume_maps(new_volume_maps); + } } } else { plater_ref->set_global_filament_map_mode(new_mode); - if (new_mode == fmmManual) + if (new_mode == fmmManual) { plater_ref->set_global_filament_map(new_maps); + plater_ref->set_global_filament_volume_map(new_volume_maps); + } } plater_ref->update(); // check whether able to slice, if not, return false @@ -186,12 +203,17 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, const std::vector &filament_color, const std::vector &filament_type, const std::vector &filament_map, + const std::vector &filament_volume_map, const std::vector &filaments, const FilamentMapMode mode, bool machine_synced, bool show_default, bool with_checkbox) - : wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE), m_filament_color(filament_color), m_filament_type(filament_type), m_filament_map(filament_map) + : wxDialog(parent, wxID_ANY, _L("Filament grouping"), wxDefaultPosition, wxDefaultSize,wxDEFAULT_DIALOG_STYLE) + , m_filament_color(filament_color) + , m_filament_type(filament_type) + , m_filament_map(filament_map) + , m_filament_volume_map(filament_volume_map) { SetBackgroundColour(*wxWHITE); @@ -244,7 +266,20 @@ FilamentMapDialog::FilamentMapDialog(wxWindow *parent, mode == fmmAutoForMatch && !auto_match_available ? fmmAutoForFlush : mode; - m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map); + m_manual_map_panel = new FilamentMapManualPanel(this, m_filament_color, m_filament_type, filaments, filament_map, filament_volume_map); + // A manual grouping can point filaments at a nozzle volume the extruder does not physically + // carry; the panel's validation timer reports that here so OK is gated on a printable map. + m_manual_map_panel->Bind(wxEVT_INVALID_MANUAL_MAP, [this](wxCommandEvent &event) { + if (m_page_type != PageType::ptManual) { + if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); } + return; + } + if (event.GetInt()) { + if (!m_ok_btn->IsEnabled()) { m_ok_btn->Enable(); } + } else { + if (m_ok_btn->IsEnabled()) { m_ok_btn->Disable(); } + } + }); m_auto_map_panel = new FilamentMapAutoPanel(this, default_auto_mode, auto_match_available); if (show_default) m_default_map_panel = new FilamentMapDefaultPanel(this); @@ -351,16 +386,8 @@ void FilamentMapDialog::on_checkbox(wxCommandEvent &event) void FilamentMapDialog::on_ok(wxCommandEvent &event) { if (m_page_type == PageType::ptManual) { - std::vector left_filaments = m_manual_map_panel->GetLeftFilaments(); - std::vector right_filaments = m_manual_map_panel->GetRightFilaments(); - - for (int i = 0; i < m_filament_map.size(); ++i) { - if (std::find(left_filaments.begin(), left_filaments.end(), i + 1) != left_filaments.end()) { - m_filament_map[i] = 1; - } else if (std::find(right_filaments.begin(), right_filaments.end(), i + 1) != right_filaments.end()) { - m_filament_map[i] = 2; - } - } + m_filament_map = m_manual_map_panel->GetFilamentMaps(); + m_filament_volume_map = m_manual_map_panel->GetFilamentVolumeMaps(); } EndModal(wxID_OK); @@ -398,6 +425,11 @@ void FilamentMapDialog::update_panel_status(PageType page) m_auto_map_panel->Show(); } + // The nozzle-availability gate only constrains manual grouping; every other page must + // leave OK usable even if the manual page had disabled it. + if (page != PageType::ptManual && m_ok_btn && !m_ok_btn->IsEnabled()) + m_ok_btn->Enable(); + if (m_smart_filament) m_smart_filament->Show(get_mode() == fmmAutoForFlush); diff --git a/src/slic3r/GUI/FilamentMapDialog.hpp b/src/slic3r/GUI/FilamentMapDialog.hpp index c0692ec081..fd1b056347 100644 --- a/src/slic3r/GUI/FilamentMapDialog.hpp +++ b/src/slic3r/GUI/FilamentMapDialog.hpp @@ -43,6 +43,7 @@ public: const std::vector& filament_color, const std::vector& filament_type, const std::vector &filament_map, + const std::vector &filament_volume_map, const std::vector &filaments, const FilamentMapMode mode, bool machine_synced, @@ -57,6 +58,12 @@ public: return {}; } + std::vector get_filament_volume_maps() const { + if (m_page_type == PageType::ptManual) + return m_filament_volume_map; + return {}; + } + int ShowModal(); void set_modal_btn_labels(const wxString& left_label, const wxString& right_label); private: @@ -87,6 +94,7 @@ private: private: std::vector m_filament_map; + std::vector m_filament_volume_map; std::vector m_filament_color; std::vector m_filament_type; }; diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index 250851e556..136258bc8e 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -1,5 +1,6 @@ #include "FilamentMapPanel.hpp" #include "GUI_App.hpp" +#include "Plater.hpp" #include "Widgets/MultiNozzleSync.hpp" // manuallySetNozzleCount producer for extruder_nozzle_stats #include #include @@ -19,28 +20,278 @@ static const wxColour BorderDisableColor = wxColour("#EEEEEE"); static const wxColour TextNormalBlackColor = wxColour("#262E30"); static const wxColour TextNormalGreyColor = wxColour("#6B6B6B"); static const wxColour TextDisableColor = wxColour("#CECECE"); +static const wxColour TextErrorColor = wxColour("#E14747"); + +wxDEFINE_EVENT(wxEVT_INVALID_MANUAL_MAP, wxCommandEvent); + +// Orca: whether the edited printer has an extruder that can physically carry several nozzles +// (only such extruders track a per-volume-type nozzle inventory worth validating against). +static bool printer_has_multi_nozzle_extruder() +{ + auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_max_nozzle_count"); + // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. + return max_nozzle_counts_opt && + std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(), + [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); }); +} + +void FilamentMapManualPanel::OnTimer(wxTimerEvent &) +{ + bool valid = true; + int invalid_eid = -1; + NozzleVolumeType invalid_nozzle = NozzleVolumeType::nvtStandard; + auto preset_bundle = wxGetApp().preset_bundle; + auto proj_config = preset_bundle->project_config; + auto nozzle_volume_values = proj_config.option("nozzle_volume_type")->values; + std::vector filament_map = GetFilamentMaps(); + std::vector filament_volume_map = GetFilamentVolumeMaps(); + // Orca: only multi-nozzle extruders carry a meaningful nozzle inventory; validating a plain + // dual-extruder printer against it would flag every grouping whenever the stats are stale. + if (printer_has_multi_nozzle_extruder()) { + for (size_t eid = 0; eid < nozzle_volume_values.size(); ++eid) { + NozzleVolumeType extruder_volume_type = NozzleVolumeType(nozzle_volume_values[eid]); + bool extruder_used = std::find_if(m_filament_list.begin(), m_filament_list.end(), + [&filament_map, eid](int fid) { + return fid - 1 < (int) filament_map.size() && (filament_map[fid - 1] - 1) == (int) eid; + }) != m_filament_list.end(); + + if (!extruder_used) { + continue; + } + + if (extruder_volume_type == nvtHybrid) { + int standard_count = getExtruderNozzleCount(preset_bundle, eid, NozzleVolumeType::nvtStandard); + int highflow_count = getExtruderNozzleCount(preset_bundle, eid, NozzleVolumeType::nvtHighFlow); + + auto has_material_of_type = [this, eid, &filament_map, &filament_volume_map](NozzleVolumeType volume_type) { + return std::find_if(m_filament_list.begin(), m_filament_list.end(), + [eid, &filament_map, &filament_volume_map, volume_type](int fid) { + return fid - 1 < (int) filament_map.size() && (filament_map[fid - 1] - 1) == (int) eid && + fid - 1 < (int) filament_volume_map.size() && + filament_volume_map[fid - 1] == static_cast(volume_type); + }) != m_filament_list.end(); + }; + + bool has_standard = has_material_of_type(NozzleVolumeType::nvtStandard); + bool has_highflow = has_material_of_type(NozzleVolumeType::nvtHighFlow); + + if ((has_standard && standard_count == 0) || + (has_highflow && highflow_count == 0)) { + valid = false; + invalid_eid = eid; + invalid_nozzle = (has_standard && standard_count == 0) ? NozzleVolumeType::nvtStandard : NozzleVolumeType::nvtHighFlow; + break; + } + } else { + int count = getExtruderNozzleCount(preset_bundle, eid, extruder_volume_type); + if (count == 0) { + valid = false; + invalid_eid = eid; + invalid_nozzle = extruder_volume_type; + break; + } + } + } + } + + bool update_ui = m_invalid_id != invalid_eid; + bool send_event = update_ui || m_force_validation; + + m_invalid_id = invalid_eid; + + if (update_ui) { + if (valid) { + m_errors->Hide(); + m_suggestion_panel->Hide(); + } else { + m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."), + invalid_eid == 0 ? _L("Left") : _L("Right"), + invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow"))); + // Re-wrap: wrapping only applies to the label text present when Wrap is called, + // and the label was empty at construction time. + m_errors->Wrap(FromDIP(520)); + m_errors->Show(); + m_suggestion_panel->Show(); + } + m_left_panel->Freeze(); + m_right_panel->Freeze(); + m_tips->Freeze(); + m_description->Freeze(); + Layout(); + Fit(); + this->GetParent()->Layout(); + this->GetParent()->Fit(); + m_left_panel->Thaw(); + m_right_panel->Thaw(); + m_tips->Thaw(); + m_description->Thaw(); + } + + if (send_event) { + wxCommandEvent event(wxEVT_INVALID_MANUAL_MAP); + event.SetInt(valid); + ProcessEvent(event); + m_force_validation = false; + } +} + +void FilamentMapManualPanel::OnSuggestionClicked(wxCommandEvent &event) +{ + wxWindow *current = this; + while (current && !wxDynamicCast(current, wxDialog)) { + current = current->GetParent(); + } + + if (current) { + wxDialog *dlg = wxDynamicCast(current, wxDialog); + if (dlg) { + int invalid_eid = m_invalid_id; + dlg->EndModal(wxID_CANCEL); + + if (invalid_eid >= 0) { + manuallySetNozzleCount(invalid_eid); + } + wxGetApp().plater()->update(); + } + } +} + +std::vector FilamentMapManualPanel::GetFilamentMaps() const +{ + std::vector new_filament_map = m_filament_map; + std::vector left_filaments = this->GetLeftFilaments(); + std::vector right_filaments = this->GetRightFilaments(); + + for (int i = 0; i < (int) new_filament_map.size(); ++i) { + if (std::find(left_filaments.begin(), left_filaments.end(), i + 1) != left_filaments.end()) { + new_filament_map[i] = 1; + } else if (std::find(right_filaments.begin(), right_filaments.end(), i + 1) != right_filaments.end()) { + new_filament_map[i] = 2; + } + } + return new_filament_map; +} + +std::vector FilamentMapManualPanel::GetFilamentVolumeMaps() const +{ + std::vector volume_map(m_filament_map.size(), 0); + + std::vector left_filaments = this->GetLeftFilaments(); + std::vector right_high_flow_filaments = this->GetRightHighFlowFilaments(); + std::vector right_standard_filaments = this->GetRightStandardFilaments(); + std::vector right_tpu_high_flow_filaments = this->GetRightTPUHighFlowFilaments(); + + auto preset_bundle = wxGetApp().preset_bundle; + auto proj_config = preset_bundle->project_config; + auto nozzle_volume_values = proj_config.option("nozzle_volume_type")->values; + + for (int i = 0; i < (int) volume_map.size(); ++i) { + int filament_id = i + 1; + + if (std::find(left_filaments.begin(), left_filaments.end(), filament_id) != left_filaments.end()) { + if (nozzle_volume_values.size() > 0) { + // Orca: never emit the Hybrid marker as a per-filament value; on a Hybrid + // extruder each filament prints with a concrete flow, defaulting to Standard. + volume_map[i] = nozzle_volume_values[0] == static_cast(NozzleVolumeType::nvtHybrid) ? + static_cast(NozzleVolumeType::nvtStandard) : + nozzle_volume_values[0]; + } + } + else if (std::find(right_high_flow_filaments.begin(), right_high_flow_filaments.end(), filament_id) != right_high_flow_filaments.end()) { + volume_map[i] = static_cast(NozzleVolumeType::nvtHighFlow); + } + else if (std::find(right_standard_filaments.begin(), right_standard_filaments.end(), filament_id) != right_standard_filaments.end()) { + volume_map[i] = static_cast(NozzleVolumeType::nvtStandard); + } + else if (std::find(right_tpu_high_flow_filaments.begin(), right_tpu_high_flow_filaments.end(), filament_id) != right_tpu_high_flow_filaments.end()) { + volume_map[i] = static_cast(NozzleVolumeType::nvtTPUHighFlow); + } + } + + return volume_map; +} + +void FilamentMapManualPanel::SyncPanelHeights() +{ + if (!m_left_panel || !m_right_panel) return; + + auto curr_left = m_left_panel->GetMinSize(); + auto curr_right = m_right_panel->GetMinSize(); + + m_left_panel->SetMinSize(wxSize(FromDIP(260), FromDIP(110))); + m_right_panel->SetMinSize(wxSize(FromDIP(260), FromDIP(110))); + + m_left_panel->Layout(); + m_left_panel->Fit(); + m_right_panel->Layout(); + m_right_panel->Fit(); + + wxSize left_best_size = m_left_panel->GetBestSize(); + wxSize right_best_size = m_right_panel->GetBestSize(); + + int max_height = std::max(left_best_size.GetHeight(), right_best_size.GetHeight()); + bool height_changed = curr_left.GetHeight() != max_height || curr_right.GetHeight() != max_height; + if (!height_changed) { + if (curr_left.GetHeight() > 0) + m_left_panel->SetMinSize(curr_left); + if (curr_right.GetHeight() > 0) + m_right_panel->SetMinSize(curr_right); + if (GetParent()) { + GetParent()->Layout(); + GetParent()->Fit(); + } + return; + } + + m_left_panel->SetMinSize(wxSize(FromDIP(260), max_height)); + m_right_panel->SetMinSize(wxSize(FromDIP(260), max_height)); + + Layout(); + Fit(); + + if (GetParent()) { + GetParent()->Layout(); + GetParent()->Fit(); + } +} + +void FilamentMapManualPanel::OnDragDropCompleted(wxCommandEvent &event) +{ + SyncPanelHeights(); + event.Skip(); +} FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *parent, const std::vector &color, const std::vector &type, const std::vector &filament_list, - const std::vector &filament_map) - : wxPanel(parent), m_filament_map(filament_map), m_filament_color(color), m_filament_type(type), m_filament_list(filament_list) + const std::vector &filament_map, + const std::vector &filament_volume_map) + : wxPanel(parent) + , m_filament_map(filament_map) + , m_filament_volume_map(filament_volume_map) + , m_filament_list(filament_list) + , m_filament_color(color) + , m_filament_type(type) { + SetName(wxT("FilamentMapManualPanel")); SetBackgroundColour(BgNormalColor); auto top_sizer = new wxBoxSizer(wxVERTICAL); m_description = new Label(this, _L("We will slice according to this grouping method:")); top_sizer->Add(m_description, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15)); + m_description->Wrap(FromDIP(520)); top_sizer->AddSpacer(FromDIP(8)); auto drag_sizer = new wxBoxSizer(wxHORIZONTAL); m_left_panel = new DragDropPanel(this, _L("Left Nozzle"), false); - m_right_panel = new DragDropPanel(this, _L("Right Nozzle"), false); + m_right_panel = new SeparatedDragDropPanel(this, _L("Right Nozzle"), false); m_switch_btn = new ScalableButton(this, wxID_ANY, "switch_filament_maps"); + UpdateNozzleVolumeType(); + for (size_t idx = 0; idx < m_filament_map.size(); ++idx) { auto iter = std::find(m_filament_list.begin(), m_filament_list.end(), idx + 1); if (iter == m_filament_list.end()) continue; @@ -50,29 +301,55 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p m_left_panel->AddColorBlock(color, type, idx + 1); } else { assert(m_filament_map[idx] == 2); - m_right_panel->AddColorBlock(color, type, idx + 1); + bool is_high_flow = (idx < m_filament_volume_map.size()) && (m_filament_volume_map[idx] == static_cast(NozzleVolumeType::nvtHighFlow)); + m_right_panel->AddColorBlock(color, type, idx + 1, is_high_flow); } } - m_left_panel->SetMinSize({ FromDIP(260),-1 }); - m_right_panel->SetMinSize({ FromDIP(260),-1 }); + m_left_panel->SetMinSize({FromDIP(260), FromDIP(110)}); + m_right_panel->SetMinSize({FromDIP(260), FromDIP(110)}); - drag_sizer->AddStretchSpacer(); drag_sizer->Add(m_left_panel, 1, wxEXPAND); - drag_sizer->AddSpacer(FromDIP(7)); - drag_sizer->Add(m_switch_btn, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(1)); - drag_sizer->AddSpacer(FromDIP(7)); + drag_sizer->Add(m_switch_btn, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(8)); drag_sizer->Add(m_right_panel, 1, wxEXPAND); - drag_sizer->AddStretchSpacer(); top_sizer->Add(drag_sizer, 0, wxEXPAND); m_tips = new Label(this, _L("Tip: You can drag the filaments to reassign them to different nozzles.")); m_tips->SetFont(Label::Body_14); m_tips->SetForegroundColour(TextNormalGreyColor); + m_tips->Wrap(FromDIP(520)); top_sizer->AddSpacer(FromDIP(20)); top_sizer->Add(m_tips, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15)); - m_switch_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSwitchFilament, this); + m_errors = new Label(this, ""); + m_errors->SetFont(Label::Body_13); + m_errors->SetForegroundColour(TextErrorColor); + m_errors->Wrap(FromDIP(520)); + top_sizer->AddSpacer(FromDIP(10)); + top_sizer->Add(m_errors, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15)); + + m_errors->Hide(); + + m_suggestion_panel = new wxPanel(this, wxID_ANY); + m_suggestion_panel->SetBackgroundColour(*wxWHITE); + auto suggestion_sizer = new wxBoxSizer(wxHORIZONTAL); + auto suggestion_text = new Label(m_suggestion_panel, _L("Please adjust your grouping or click ")); + suggestion_text->SetFont(Label::Body_13); + suggestion_text->SetForegroundColour(TextErrorColor); + suggestion_text->SetBackgroundColour(*wxWHITE); + auto suggestion_btn = new ScalableButton(m_suggestion_panel, wxID_ANY, "edit", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 14); + suggestion_btn->SetBackgroundColour(*wxWHITE); + auto suggestion_text2 = new Label(m_suggestion_panel, _L(" to set nozzle count")); + suggestion_text2->SetFont(Label::Body_13); + suggestion_text2->SetForegroundColour(TextErrorColor); + suggestion_text2->SetBackgroundColour(*wxWHITE); + suggestion_sizer->Add(suggestion_text, 0, wxALIGN_CENTER_VERTICAL); + suggestion_sizer->Add(suggestion_btn, 0, wxALIGN_CENTER_VERTICAL); + suggestion_sizer->Add(suggestion_text2, 0, wxALIGN_CENTER_VERTICAL); + m_suggestion_panel->SetSizer(suggestion_sizer); + top_sizer->Add(m_suggestion_panel, 0, wxALIGN_LEFT | wxLEFT, FromDIP(15)); + m_suggestion_panel->Hide(); + suggestion_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSuggestionClicked, this); // Multi-nozzle: give the user a reachable way to declare, per extruder, how many // physical nozzles of each volume type a multi-nozzle extruder carries. This is the @@ -80,11 +357,8 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p // auto-sync is deferred). Gated on the edited printer preset having an extruder with // extruder_max_nozzle_count > 1, so the trigger is not even created for any single-nozzle or // dual-extruder ({1,1}, H2D) printer - zero UI change for every existing profile. - auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_max_nozzle_count"); - // Skip nil entries: a nullable-int nil is INT_MAX (> 1) and would otherwise falsely pass the gate. - if (max_nozzle_counts_opt && - std::any_of(max_nozzle_counts_opt->values.begin(), max_nozzle_counts_opt->values.end(), - [](int v) { return v > 1 && v != ConfigOptionIntsNullable::nil_value(); })) { + if (printer_has_multi_nozzle_extruder()) { + auto *max_nozzle_counts_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("extruder_max_nozzle_count"); auto *set_count_link = new Label(this, _L("Set the physical nozzle count...")); set_count_link->SetFont(Label::Body_14); set_count_link->SetForegroundColour(BorderSelectedColor); @@ -94,26 +368,88 @@ FilamentMapManualPanel::FilamentMapManualPanel(wxWindow *p const std::vector max_counts = max_nozzle_counts_opt->values; set_count_link->Bind(wxEVT_LEFT_DOWN, [max_counts](wxMouseEvent &evt) { for (int extruder_id = 0; extruder_id < (int) max_counts.size(); ++extruder_id) { - if (max_counts[extruder_id] > 1) + if (max_counts[extruder_id] > 1 && max_counts[extruder_id] != ConfigOptionIntsNullable::nil_value()) GUI::manuallySetNozzleCount(extruder_id); } evt.Skip(); }); } + m_timer = new wxTimer(this); + Bind(wxEVT_TIMER, &FilamentMapManualPanel::OnTimer, this); + Bind(wxEVT_DRAG_DROP_COMPLETED, &FilamentMapManualPanel::OnDragDropCompleted, this); + + m_switch_btn->Bind(wxEVT_BUTTON, &FilamentMapManualPanel::OnSwitchFilament, this); + SetSizer(top_sizer); + SetMinSize(wxSize(FromDIP(580), -1)); Layout(); Fit(); GUI::wxGetApp().UpdateDarkUIWin(this); } +void FilamentMapManualPanel::UpdateNozzleVolumeType() +{ + auto check_separation = []() { + auto preset_bundle = wxGetApp().preset_bundle; + auto nozzle_volume_values = preset_bundle->project_config.option("nozzle_volume_type")->values; + if (nozzle_volume_values.size() <= 1) + return false; + + return nozzle_volume_values[1] == static_cast(NozzleVolumeType::nvtHybrid); + }; + bool should_separate = check_separation(); + m_right_panel->SetUseSeparation(should_separate); + + UpdateNozzleCountDisplay(); + + Layout(); + Fit(); +} + +void FilamentMapManualPanel::UpdateNozzleCountDisplay() +{ + auto preset_bundle = wxGetApp().preset_bundle; + + // Orca: nozzle counts are only tracked (and only meaningful) for multi-nozzle extruders; + // plain dual-extruder printers keep their unadorned zone titles. + if (!printer_has_multi_nozzle_extruder()) { + m_left_panel->UpdateLabel(_L("Left Nozzle")); + m_right_panel->UpdateLabel(_L("Right Nozzle")); + return; + } + + // Format the count suffix separately so a translation containing '%' cannot + // corrupt the wxString::Format output. + int left_count = getExtruderNozzleCountTotal(preset_bundle, 0); + wxString left_title = _L("Left Nozzle") + wxString::Format("(%d)", left_count); + m_left_panel->UpdateLabel(left_title); + + if (m_right_panel->IsUseSeparation()) { + int standard_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtStandard); + int highflow_count = getExtruderNozzleCount(preset_bundle, 1, NozzleVolumeType::nvtHighFlow); + wxString right_title = _L("Right Nozzle") + wxString::Format("(Std: %d, HF: %d)", standard_count, highflow_count); + m_right_panel->UpdateLabel(right_title); + } else { + int right_count = getExtruderNozzleCountTotal(preset_bundle, 1); + wxString right_title = _L("Right Nozzle") + wxString::Format("(%d)", right_count); + m_right_panel->UpdateLabel(right_title); + } +} + +FilamentMapManualPanel::~FilamentMapManualPanel() +{ + m_timer->Stop(); + delete m_timer; +} + void FilamentMapManualPanel::OnSwitchFilament(wxCommandEvent &) { auto left_blocks = m_left_panel->get_filament_blocks(); auto right_blocks = m_right_panel->get_filament_blocks(); for (auto &block : left_blocks) { - m_right_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false); + m_right_panel->AddColorBlock(block->GetColor(), block->GetType(), block->GetFilamentId(), false, false); m_left_panel->RemoveColorBlock(block, false); } @@ -123,22 +459,27 @@ void FilamentMapManualPanel::OnSwitchFilament(wxCommandEvent &) } this->GetParent()->Layout(); this->GetParent()->Fit(); + + if (m_right_panel->IsUseSeparation()) { + m_left_panel->Layout(); + m_left_panel->Fit(); + m_right_panel->Layout(); + m_right_panel->Fit(); + SyncPanelHeights(); + } } -void FilamentMapManualPanel::Hide() +bool FilamentMapManualPanel::Show(bool show) { - m_left_panel->Hide(); - m_right_panel->Hide(); - m_switch_btn->Hide(); - wxPanel::Hide(); -} + m_force_validation = show; + if (show) { + m_timer->Start(500); + SyncPanelHeights(); + } else { + m_timer->Stop(); + } -void FilamentMapManualPanel::Show() -{ - m_left_panel->Show(); - m_right_panel->Show(); - m_switch_btn->Show(); - wxPanel::Show(); + return wxPanel::Show(show); } GUI::FilamentMapBtnPanel::FilamentMapBtnPanel(wxWindow *parent, const wxString &label, const wxString &detail, const std::string &icon) : wxPanel(parent) diff --git a/src/slic3r/GUI/FilamentMapPanel.hpp b/src/slic3r/GUI/FilamentMapPanel.hpp index ad5cefa459..a0caf8e78b 100644 --- a/src/slic3r/GUI/FilamentMapPanel.hpp +++ b/src/slic3r/GUI/FilamentMapPanel.hpp @@ -9,32 +9,60 @@ namespace Slic3r { namespace GUI { +// Fired by the manual panel's validation timer: the event int is 1 when the current manual +// grouping is printable with the installed nozzles, 0 when some zone has no matching nozzle. +wxDECLARE_EVENT(wxEVT_INVALID_MANUAL_MAP, wxCommandEvent); + class FilamentMapManualPanel : public wxPanel { public: - FilamentMapManualPanel(wxWindow *parent, const std::vector &color, const std::vector &type, const std::vector &filament_list, const std::vector &filament_map); + FilamentMapManualPanel(wxWindow *parent, + const std::vector &color, + const std::vector &type, + const std::vector &filament_list, + const std::vector &filament_map, + const std::vector &filament_volume_map); + ~FilamentMapManualPanel(); - std::vector GetFilamentMaps() const { return m_filament_map; } + std::vector GetFilamentMaps() const; + std::vector GetFilamentVolumeMaps() const; std::vector GetLeftFilaments() const { return m_left_panel->GetAllFilaments(); } std::vector GetRightFilaments() const { return m_right_panel->GetAllFilaments(); } - void Hide(); - void Show(); + std::vector GetRightHighFlowFilaments() const { return m_right_panel->GetHighFlowFilaments(); } + std::vector GetRightStandardFilaments() const { return m_right_panel->GetStandardFilaments(); } + std::vector GetRightTPUHighFlowFilaments() const { return m_right_panel->GetTPUHighFlowFilaments(); } + void UpdateNozzleVolumeType(); + void UpdateNozzleCountDisplay(); + + bool Show(bool show = true) override; private: - void OnSwitchFilament(wxCommandEvent &); - DragDropPanel *m_left_panel; - DragDropPanel *m_right_panel; + void OnTimer(wxTimerEvent &evt); + void OnSwitchFilament(wxCommandEvent &); + void SyncPanelHeights(); + void OnDragDropCompleted(wxCommandEvent &evt); + void OnSuggestionClicked(wxCommandEvent &event); - Label *m_description; - Label *m_tips; + DragDropPanel *m_left_panel; + SeparatedDragDropPanel *m_right_panel; + + Label *m_description; + Label *m_tips; + Label *m_errors; + wxPanel *m_suggestion_panel; ScalableButton *m_switch_btn; std::vector m_filament_map; + std::vector m_filament_volume_map; std::vector m_filament_list; std::vector m_filament_color; std::vector m_filament_type; + + wxTimer *m_timer; + int m_invalid_id{-1}; + bool m_force_validation{false}; }; class FilamentMapBtnPanel : public wxPanel diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 912f5cf84d..40ea003331 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -3894,6 +3894,16 @@ void PartPlate::set_filament_count(int filament_count) std::vector& filament_maps = m_config.option("filament_map")->values; filament_maps.resize(filament_count, 1); } + + if (m_config.has("filament_nozzle_map")) { + std::vector& filament_nozzle_map = m_config.option("filament_nozzle_map")->values; + filament_nozzle_map.resize(filament_count, 0); + } + + if (m_config.has("filament_volume_map")) { + std::vector& filament_volume_map = m_config.option("filament_volume_map")->values; + filament_volume_map.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); + } } void PartPlate::on_filament_added() @@ -3902,6 +3912,27 @@ void PartPlate::on_filament_added() std::vector& filament_maps = m_config.option("filament_map")->values; filament_maps.push_back(1); } + + if (m_config.has("filament_nozzle_map")) { + std::vector& filament_nozzle_map = m_config.option("filament_nozzle_map")->values; + filament_nozzle_map.push_back(0); + } + + if (m_config.has("filament_volume_map")) { + std::vector& filament_volume_map = m_config.option("filament_volume_map")->values; + // A new filament defaults onto the first extruder, so seed its volume value from + // that extruder's flow type. + int volume_type = static_cast(NozzleVolumeType::nvtStandard); + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + if (nozzle_volumes && !nozzle_volumes->values.empty()) + volume_type = nozzle_volumes->values[0]; + // Orca: never store the Hybrid marker as a per-filament value; on a Hybrid extruder + // each filament still prints with a concrete flow, defaulting to Standard. + if (volume_type == static_cast(NozzleVolumeType::nvtHybrid)) + volume_type = static_cast(NozzleVolumeType::nvtStandard); + + filament_volume_map.push_back(volume_type); + } } void PartPlate::on_filament_deleted(int filament_count, int filament_id) @@ -3914,6 +3945,19 @@ void PartPlate::on_filament_deleted(int filament_count, int filament_id) if (filament_id >= 0 && filament_id < (int) filament_maps.size()) filament_maps.erase(filament_maps.begin() + filament_id); } + + if (m_config.has("filament_nozzle_map")) { + std::vector& filament_nozzle_map = m_config.option("filament_nozzle_map")->values; + if (filament_id >= 0 && filament_id < (int) filament_nozzle_map.size()) + filament_nozzle_map.erase(filament_nozzle_map.begin() + filament_id); + } + + if (m_config.has("filament_volume_map")) { + std::vector& filament_volume_map = m_config.option("filament_volume_map")->values; + if (filament_id >= 0 && filament_id < (int) filament_volume_map.size()) + filament_volume_map.erase(filament_volume_map.begin() + filament_id); + } + update_first_layer_print_sequence_when_delete_filament(filament_id); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fae83e2ef3..46706ec65c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1213,9 +1213,16 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title combo_flow->GetDropDown().SetUseContentWidth(true); combo_flow->Bind(wxEVT_COMBOBOX, [this, index, combo_flow](wxCommandEvent &evt) { auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); - printer_tab->set_extruder_volume_type(index, NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt())))); - if (GUI::wxGetApp().plater()) - GUI::wxGetApp().plater()->update_machine_sync_status(); + NozzleVolumeType volume_type = NozzleVolumeType(intptr_t(combo_flow->GetClientData(evt.GetInt()))); + printer_tab->set_extruder_volume_type(index, volume_type); + auto plater = GUI::wxGetApp().plater(); + if (plater) { + // A new Flow type invalidates the per-filament volume choices stored on the + // plates for this extruder; rewrite them so the next apply/grouping sees the + // selected volume instead of a stale one. + plater->update_filament_volume_map(index, static_cast(volume_type)); + plater->update_machine_sync_status(); + } }); this->combo_flow = combo_flow; @@ -7328,6 +7335,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ filament_map->values.resize(filament_count, 1); } + ConfigOptionInts* filament_nozzle_map = proj_cfg.opt("filament_nozzle_map", true); + if (filament_nozzle_map->size() != filament_count) { + filament_nozzle_map->values.resize(filament_count, 0); + } + + ConfigOptionInts* filament_volume_map = proj_cfg.opt("filament_volume_map", true); + if (filament_volume_map->size() != filament_count) { + filament_volume_map->values.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); + } + // Sync filament multi colour ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); if (filament_multi_color->size() != filament_count) { @@ -17799,12 +17816,24 @@ void Plater::set_global_filament_map(const std::vector& filament_map) project_config.option("filament_map")->values = filament_map; } +void Plater::set_global_filament_volume_map(const std::vector& filament_volume_map) +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + project_config.option("filament_volume_map")->values = filament_volume_map; +} + std::vector Plater::get_global_filament_map() const { auto& project_config = wxGetApp().preset_bundle->project_config; return project_config.option("filament_map")->values; } +std::vector Plater::get_global_filament_volume_map() const +{ + auto& project_config = wxGetApp().preset_bundle->project_config; + return project_config.option("filament_volume_map")->values; +} + FilamentMapMode Plater::get_global_filament_map_mode() const { @@ -18721,13 +18750,17 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); + auto plate_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config plate_filament_maps.resize(filament_colors.size(), 1); + if (plate_filament_volume_maps.size() != filament_colors.size()) + plate_filament_volume_maps.resize(filament_colors.size(), 0); FilamentMapDialog filament_dlg(this, filament_colors, filament_types, plate_filament_maps, + plate_filament_volume_maps, curr_plate->get_extruders(true), plate_filament_map_mode, this->get_machine_sync_status(), @@ -18741,16 +18774,21 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); FilamentMapMode new_map_mode = filament_dlg.get_mode(); + std::vector new_filament_volume_maps = filament_dlg.get_filament_volume_maps(); + std::vector old_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); + if (new_map_mode != old_map_mode) { curr_plate->set_filament_map_mode(new_map_mode); } if (new_map_mode == fmmManual){ curr_plate->set_filament_maps(new_filament_maps); + curr_plate->set_filament_volume_maps(new_filament_volume_maps); } bool need_invalidate = (old_map_mode != new_map_mode || - old_filament_maps != new_filament_maps); + old_filament_maps != new_filament_maps || + old_filament_volume_maps != new_filament_volume_maps); if (need_invalidate) { if (need_slice) { @@ -19216,6 +19254,30 @@ bool Plater::get_machine_sync_status() return p->get_machine_sync_status(); } +void Plater::update_filament_volume_map(int extruder_id, int volume_type) +{ + // Hybrid is a per-extruder mix, not a per-filament volume; reset the affected filaments + // to Standard so the manual grouping dialog starts from a concrete assignment. + int selected_volume_type = volume_type == static_cast(NozzleVolumeType::nvtHybrid) ? static_cast(NozzleVolumeType::nvtStandard) : volume_type; + auto& partplate_list = get_partplate_list(); + for (int idx = 0; idx < partplate_list.get_plate_count(); ++idx) { + auto plate = partplate_list.get_plate(idx); + if (!plate) continue; + auto filament_map = plate->get_filament_maps(); + auto filament_volume_map = plate->get_filament_volume_maps(); + if (filament_map.empty() || filament_volume_map.empty()) continue; + if (filament_volume_map.size() < filament_map.size()) { + filament_volume_map.resize(filament_map.size(), static_cast(NozzleVolumeType::nvtStandard)); + } + for (size_t i = 0; i < filament_map.size(); ++i) { + if (filament_map[i] == extruder_id + 1) { + filament_volume_map[i] = selected_volume_type; + } + } + plate->set_filament_volume_maps(filament_volume_map); + } +} + #if ENABLE_ENVIRONMENT_MAP void Plater::init_environment_texture() { diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 0e4f992935..1730bd2705 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -586,7 +586,9 @@ public: void set_global_filament_map_mode(FilamentMapMode mode); void set_global_filament_map(const std::vector& filament_map); + void set_global_filament_volume_map(const std::vector& filament_volume_map); std::vector get_global_filament_map() const; + std::vector get_global_filament_volume_map() const; FilamentMapMode get_global_filament_map_mode() const; void update_menus(); @@ -748,6 +750,11 @@ public: void update_machine_sync_status(); + // Rewrite every plate's per-filament volume choice for the filaments grouped onto this + // extruder after its Flow type changed (Hybrid resets them to Standard so the user + // re-assigns concrete volumes in the grouping dialog). + void update_filament_volume_map(int extruder_id, int volume_type); + #if ENABLE_ENVIRONMENT_MAP void init_environment_texture(); unsigned int get_environment_texture_id() const; diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index f27beec657..05857c6d0d 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -35,7 +35,7 @@ static const int RightExtruderIdx = 1; // config key (ConfigOptionStrings, one encoded map per extruder) via get_extruder_nozzle_stats / // save_extruder_nozzle_stats_to_string. These helpers read and write that config. -static int extruder_nozzle_count(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type) +int getExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type) { if (!preset_bundle) return 0; @@ -49,7 +49,7 @@ static int extruder_nozzle_count(PresetBundle *preset_bundle, int extruder_id, N return it == stats[extruder_id].end() ? 0 : it->second; } -static int extruder_nozzle_count_total(PresetBundle *preset_bundle, int extruder_id) +int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id) { if (!preset_bundle) return 0; @@ -230,8 +230,8 @@ void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, Nozz int display_count = -1; // hides the badge if (support_multi_nozzle) - display_count = volume_type == nvtHybrid ? extruder_nozzle_count_total(preset_bundle, extruder_id) - : extruder_nozzle_count(preset_bundle, extruder_id, volume_type); + display_count = volume_type == nvtHybrid ? getExtruderNozzleCountTotal(preset_bundle, extruder_id) + : getExtruderNozzleCount(preset_bundle, extruder_id, volume_type); plater->sidebar().set_extruder_nozzle_count(extruder_id, display_count); } @@ -269,7 +269,7 @@ void onNozzleVolumeTypeSwitch(PresetBundle *preset_bundle, int extruder_id, Nozz // Hybrid is a mix, not a type every nozzle shares, so there is nothing to carry over. if (type == nvtHybrid) return; - const int total = extruder_nozzle_count_total(preset_bundle, extruder_id); + const int total = getExtruderNozzleCountTotal(preset_bundle, extruder_id); setExtruderNozzleCount(preset_bundle, extruder_id, type, total, true); } @@ -294,14 +294,14 @@ void manuallySetNozzleCount(int extruder_id) return; const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]); - const int standard_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtStandard); - const int highflow_count = extruder_nozzle_count(preset_bundle, extruder_id, nvtHighFlow); + const int standard_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard); + const int highflow_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow); // Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other // extruder currently has none. bool force_no_zero = volume_type == nvtHybrid; if (nozzle_volume_type_opt->values.size() > 1) - force_no_zero |= extruder_nozzle_count_total(preset_bundle, 1 - extruder_id) == 0; + force_no_zero |= getExtruderNozzleCountTotal(preset_bundle, 1 - extruder_id) == 0; ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, max_nozzle_count->values[extruder_id], force_no_zero); if (dialog.ShowModal() == wxID_OK) { diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index 2ee65ea554..18a5ecbd95 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -219,6 +219,11 @@ std::optional tryPopUpMultiNozzleDialog(MachineObject* obj); // counts are reset first. void setExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType type, int nozzle_count, bool clear_all); +// Read one extruder's nozzle count for a volume type (or its total) from the edited printer preset's +// `extruder_nozzle_stats` config key. Returns 0 when the stats are absent or the extruder is unknown. +int getExtruderNozzleCount(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type); +int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id); + // Refresh the sidebar nozzle-count badge for one extruder: shows the extruder's physical nozzle count on // multi-nozzle printers, hides it (count -1) everywhere else. void updateNozzleCountDisplay(PresetBundle *preset_bundle, int extruder_id, NozzleVolumeType volume_type);