diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index c8fe00b17c..b2cc67f970 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -199,7 +199,8 @@ DynamicPrintConfig PresetBundle::construct_full_config( if (num_filaments <= 1) { // BBS: update filament config related with variants DynamicPrintConfig filament_config = in_filament_presets[0].config; - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament())) filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition()); @@ -223,7 +224,8 @@ DynamicPrintConfig PresetBundle::construct_full_config( filament_temp_configs.resize(num_filaments); for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament())) filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } @@ -4664,7 +4666,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio if (num_filaments <= 1) { //BBS: update filament config related with variants DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament())) filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition()); @@ -4758,7 +4761,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio filament_temp_configs.resize(num_filaments); for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); - if (apply_extruder && ((extruder_count > 1) || different_extruder)) + // Orca: a multi-variant filament resolves its variants on a single-variant printer too. + if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament())) filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ed3ac6de88..90a37bb8c6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -3938,6 +3938,9 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< { int extruder_count = 1, extruder_volume_type_count = 1; bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count); + // Orca: resolve the filament variants wherever Print::apply does, a multi-variant filament + // on a single-variant printer included. + const bool expand_filaments = (extruder_count > 1) || support_multi || m_ori_full_print_config.has_multi_variant_filament(); std::vector> nozzle_volume_types; extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); @@ -3978,7 +3981,7 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< m_full_print_config = m_ori_full_print_config; std::set filament_keys = filament_options_with_variant; filament_keys.insert("filament_self_index"); - if ((extruder_count > 1) || support_multi) + if (expand_filaments) m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant"); const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); @@ -3995,7 +3998,7 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, m_config.filament_map_2.values); } - if ((extruder_count > 1) || support_multi) { + if (expand_filaments) { t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end()); keys.push_back("filament_self_index"); m_config.apply_only(m_full_print_config, keys, true); diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 90d60b5917..373288dc2c 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1296,7 +1296,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant", &dynamic_slot_indices); - else if ((extruder_count > 1) || different_extruder) + // Orca: also on a printer with a single extruder variant once a filament defines several + // (e.g. Standard and High Flow), so each filament takes its variant for that extruder. + else if ((extruder_count > 1) || different_extruder || new_full_config.has_multi_variant_filament()) new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant"); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1d994d7874..7d916a6e05 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -626,7 +626,8 @@ static const t_config_enum_values s_keys_map_NozzleVolumeType = { { "Standard", nvtStandard }, { "High Flow", nvtHighFlow }, { "TPU High Flow", nvtTPUHighFlow }, - { "Hybrid", nvtHybrid } + { "Hybrid", nvtHybrid }, + { "E3D High Flow", nvtE3DHighFlow } }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType) @@ -676,9 +677,34 @@ int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_ty for (int index = 0; index < int(variant_list.size()); ++index) { if (extruder_variant == variant_list[index] && variant_ids_1based[index] == variant_id_1based) { return index; } } + // Without this variant, use the id's own first variant (usually Standard), not variant index 0, + // which belongs to the first filament or extruder. + for (int index = 0; index < int(variant_list.size()); ++index) { + if (variant_ids_1based[index] == variant_id_1based) { return index; } + } return 0; } +std::set get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id) +{ + std::set supported_types; + + auto *variant_list = printer_config.option("extruder_variant_list"); + auto *extruder_types = printer_config.option("extruder_type"); + if (!variant_list || !extruder_types || extruder_id < 0 || + extruder_id >= (int) variant_list->values.size() || extruder_id >= (int) extruder_types->values.size()) + return supported_types; + + const ExtruderType extruder_type = ExtruderType(extruder_types->values[extruder_id]); + for (NozzleVolumeType volume_type : get_valid_nozzle_volume_type()) { + // An unsupported extruder type yields an empty name, which would match any list. + const std::string variant = get_extruder_variant_string(extruder_type, volume_type); + if (!variant.empty() && variant_list->values[extruder_id].find(variant) != std::string::npos) + supported_types.insert(volume_type); + } + return supported_types; +} + std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type) { if (nozzle_volume_type > nvtMaxNozzleVolumeType) { @@ -5923,15 +5949,18 @@ void PrintConfigDef::init_fff_params() def->label = "Nozzle Volume Type"; def->tooltip = "Nozzle volume type for extruders."; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - // Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3). + // Listed in display order. A position is not the enum value (E3D High Flow is 5, after the reserved 4), + // so map a position to its NozzleVolumeType through enum_keys_map. def->enum_values.push_back(L("Standard")); def->enum_values.push_back(L("High Flow")); def->enum_values.push_back(L("Hybrid")); def->enum_values.push_back(L("TPU High Flow")); + def->enum_values.push_back(L("E3D High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); def->enum_labels.push_back(L("Hybrid")); def->enum_labels.push_back(L("TPU High Flow")); + def->enum_labels.push_back(L("E3D High Flow")); def->mode = comSimple; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5944,10 +5973,12 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back(L("High Flow")); def->enum_values.push_back(L("Hybrid")); def->enum_values.push_back(L("TPU High Flow")); + def->enum_values.push_back(L("E3D High Flow")); def->enum_labels.push_back(L("Standard")); def->enum_labels.push_back(L("High Flow")); def->enum_labels.push_back(L("Hybrid")); def->enum_labels.push_back(L("TPU High Flow")); + def->enum_labels.push_back(L("E3D High Flow")); def->mode = comDevelop; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -5986,7 +6017,7 @@ void PrintConfigDef::init_fff_params() // Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in // src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens // instead. Kept registered so a project/config carrying it loads without an unknown-option - // substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid). + // substitution warning. Registers the physical types only (no Hybrid). // Internal use only, no translation. def = this->add("extruder_nozzle_volume_type", coEnums); def->label = "Extruder nozzle volume type"; @@ -5995,9 +6026,11 @@ void PrintConfigDef::init_fff_params() def->enum_values.push_back("Standard"); def->enum_values.push_back("High Flow"); def->enum_values.push_back("TPU High Flow"); + def->enum_values.push_back("E3D High Flow"); def->enum_labels.push_back("Standard"); def->enum_labels.push_back("High Flow"); def->enum_labels.push_back("TPU High Flow"); + def->enum_labels.push_back("E3D High Flow"); def->mode = comDevelop; def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard }); @@ -10070,6 +10103,13 @@ bool DynamicPrintConfig::is_using_different_extruders() return ret; } +bool DynamicPrintConfig::has_multi_variant_filament() const +{ + auto variants = dynamic_cast(this->option("filament_extruder_variant")); + auto diameters = dynamic_cast(this->option("filament_diameter")); + return variants && diameters && variants->size() > diameters->size(); +} + bool DynamicPrintConfig::support_different_extruders(int& extruder_count) const { std::set variant_set; @@ -11070,6 +11110,11 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen return; } std::vector filament_maps = opt_filament_map->values; + auto opt_ids = id_name.empty()? nullptr: dynamic_cast(this->option(id_name)); + // Orca: a map shorter than the filament count must not drop the filaments past its end; + // they take the first extruder. + if (opt_ids && !opt_ids->values.empty()) + filament_maps.resize(std::max(filament_maps.size(), *std::max_element(opt_ids->values.begin(), opt_ids->values.end())), 1); size_t filament_count = filament_maps.size(); //apply process settings auto opt_extruder_type = dynamic_cast(printer_config.option("extruder_type")); @@ -11088,7 +11133,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen // indexed out of bounds. if (opt_filament_volume_maps && opt_filament_volume_maps->values.size() == filament_count) filament_volume_maps = opt_filament_volume_maps->values; - auto opt_ids = id_name.empty()? nullptr: dynamic_cast(this->option(id_name)); std::vector variant_index; variant_index.resize(filament_count, -1); @@ -11105,9 +11149,10 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen //variant index variant_index[f_index] = get_index_for_extruder(f_index+1, id_name, extruder_type, nozzle_volume_type, variant_name); if (variant_index[f_index] < 0) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%") + // Orca: a filament need not define every extruder variant (a Direct Drive filament on a + // Bowden printer), so this is not an invalid state: the filament's first variant is used. + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%") %__LINE__ %s_keys_names_ExtruderType[extruder_type] % s_keys_names_NozzleVolumeType[nozzle_volume_type] % (f_index+1) %filament_maps[f_index]; - assert(false); //for some updates happens in a invalid state(caused by popup window) //we need to avoid crash variant_index[f_index] = 0; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 679811d699..4c5e2e8f5f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -30,6 +30,8 @@ namespace Slic3r { +class DynamicPrintConfig; + enum GCodeFlavor : unsigned char { gcfMarlinLegacy, gcfKlipper, @@ -519,8 +521,10 @@ enum NozzleVolumeType { // with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for // preset lookup and never emitted in profile variant strings nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only + // 4 is reserved: E3D High Flow is 5 in BambuStudio's slice_info and device numbering. + nvtE3DHighFlow = 5, // physical variant, E3D high-flow hotend on 0.4/0.6 nozzles // Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable. - nvtMaxNozzleVolumeType = nvtTPUHighFlow + nvtMaxNozzleVolumeType = nvtE3DHighFlow }; enum FilamentMapMode { @@ -558,11 +562,19 @@ static std::set get_valid_nozzle_volume_type() { // Hybrid is not a physical nozzle variant: presets never define it, so it must not // produce a variant string. if (t == nvtHybrid) continue; + // Skip the reserved gap between nvtTPUHighFlow (3) and nvtE3DHighFlow (5). + if (i > nvtTPUHighFlow && i < nvtE3DHighFlow) continue; type.insert(t); } return type; } +// The nozzle volume types the given extruder physically provides, as declared by the printer +// profile's extruder_variant_list. An empty set means the profile could not be read and must be +// treated as "unknown", not as "none". nvtHybrid is never reported: it describes an extruder +// holding a mix of nozzles, not a nozzle the profile can offer. +extern std::set get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id); + std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type); static std::string bed_type_to_gcode_string(const BedType type) @@ -699,8 +711,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS -class DynamicPrintConfig; - // Defines each and every configuration option of Slic3r, including the properties of the GUI dialogs. // Does not store the actual values, but defines default values. class PrintConfigDef : public ConfigDef @@ -821,6 +831,10 @@ public: //BBS bool is_using_different_extruders(); bool support_different_extruders(int& extruder_count) const; + // Whether any filament defines more than one variant (filament_extruder_variant longer than + // filament_diameter). Its variants then have to be resolved even on a printer with a single + // extruder variant, which picks the filament's variant of the same variant string. + bool has_multi_variant_filament() const; // Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by // extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched. // Fills nozzle_volume_types with each extruder's volume types in ascending enum order. diff --git a/src/slic3r/GUI/CaliHistoryDialog.cpp b/src/slic3r/GUI/CaliHistoryDialog.cpp index 7055f441ac..9b85e80c10 100644 --- a/src/slic3r/GUI/CaliHistoryDialog.cpp +++ b/src/slic3r/GUI/CaliHistoryDialog.cpp @@ -818,12 +818,14 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const if (support_nozzle_volume(curr_obj)) { Label *nozzle_name_title = new Label(top_panel, _L("Nozzle")); m_comboBox_nozzle_type = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY); - wxArrayString nozzle_items; + // The labels are in display order, not enum order (E3D High Flow is 5 but the fifth label), so each + // item carries its NozzleVolumeType as client data. const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { nozzle_items.push_back(_L(item)); } + for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i) + m_comboBox_nozzle_type->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap, + (void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i])); } - m_comboBox_nozzle_type->Set(nozzle_items); m_comboBox_nozzle_type->SetSelection(-1); flex_sizer->Add(nozzle_name_title); flex_sizer->Add(m_comboBox_nozzle_type); @@ -891,7 +893,7 @@ int NewCalibrationHistoryDialog::get_nozzle_combo_id_code() const void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event) { - // Mirror the picked hotend's flow onto the (Orca index-based) nozzle-type combo. + // Mirror the picked hotend's flow onto the nozzle-type combo. if (!curr_obj || !m_comboBox_nozzle_id || !m_comboBox_nozzle_type || !curr_obj->GetNozzleSystem()) return; @@ -902,7 +904,9 @@ void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event) DevNozzle nozzle = curr_obj->GetNozzleSystem()->GetNozzleByPosId(pos); if (nozzle.IsNormal()) { NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.GetNozzleFlowType()); - m_comboBox_nozzle_type->SetSelection(static_cast(volume_type)); + for (unsigned int i = 0; i < m_comboBox_nozzle_type->GetCount(); ++i) + if (NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(i))) == volume_type) + m_comboBox_nozzle_type->SetSelection(i); } } @@ -949,7 +953,7 @@ void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event) msg_dlg.ShowModal(); return; } - m_new_result.nozzle_volume_type = NozzleVolumeType(m_comboBox_nozzle_type->GetSelection()); + m_new_result.nozzle_volume_type = NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(m_comboBox_nozzle_type->GetSelection()))); } auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()]; diff --git a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp index 7a83d39dd3..f4160885cb 100644 --- a/src/slic3r/GUI/CalibrationWizardPresetPage.cpp +++ b/src/slic3r/GUI/CalibrationWizardPresetPage.cpp @@ -803,6 +803,29 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent) #define NOZZLE_LIST_DEFAULT 1 float nozzle_diameter_list[NOZZLE_LIST_COUNT] = {0.2, 0.4, 0.6, 0.8 }; +// The nozzle_volume_type labels are in display order, not enum order (E3D High Flow is 5 but the fifth +// label), so each item carries its NozzleVolumeType as client data and is selected by that value. +static void select_nozzle_volume(ComboBox *combo, NozzleVolumeType volume_type) +{ + for (unsigned int i = 0; i < combo->GetCount(); ++i) + if (NozzleVolumeType(intptr_t(combo->GetClientData(i))) == volume_type) { + combo->SetSelection(i); + return; + } +} + +static void fill_nozzle_volume_combo(ComboBox *combo) +{ + combo->Clear(); + const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); + if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { + for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i) + combo->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap, + (void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i])); + } + select_nozzle_volume(combo, NozzleVolumeType::nvtStandard); +} + void CalibrationPresetPage::init_selection_values() { // init nozzle diameter and nozzle volume @@ -813,15 +836,7 @@ void CalibrationPresetPage::init_selection_values() } m_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_comboBox_nozzle_volume); } Preset* cur_printer_preset = get_printer_preset(curr_obj, 0.4); @@ -866,15 +881,7 @@ void CalibrationPresetPage::init_selection_values() } m_left_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_left_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_left_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_left_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_left_comboBox_nozzle_volume); } // right @@ -885,15 +892,7 @@ void CalibrationPresetPage::init_selection_values() } m_right_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT); - m_right_comboBox_nozzle_volume->Clear(); - const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type"); - if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) { - for (auto item : nozzle_volume_type_def->enum_labels) { - m_right_comboBox_nozzle_volume->AppendString(_L(item)); - } - } - - m_right_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard)); + fill_nozzle_volume_combo(m_right_comboBox_nozzle_volume); } } @@ -980,13 +979,13 @@ NozzleVolumeType CalibrationPresetPage::get_nozzle_volume_type(int extruder_id) if (curr_obj) { if (curr_obj->is_multi_extruders()) { if (extruder_id == LEFT_EXTRUDER_ID) { - return NozzleVolumeType(m_left_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_left_comboBox_nozzle_volume->GetClientData(m_left_comboBox_nozzle_volume->GetSelection()))); } else if (extruder_id == RIGHT_EXTRUDER_ID) { - return NozzleVolumeType(m_right_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_right_comboBox_nozzle_volume->GetClientData(m_right_comboBox_nozzle_volume->GetSelection()))); } } else - return NozzleVolumeType(m_comboBox_nozzle_volume->GetSelection()); + return NozzleVolumeType(intptr_t(m_comboBox_nozzle_volume->GetClientData(m_comboBox_nozzle_volume->GetSelection()))); } return NozzleVolumeType::nvtStandard; } @@ -2139,7 +2138,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_left_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); + select_nozzle_volume(m_left_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))); } else { m_left_comboBox_nozzle_volume->SetSelection(0); } @@ -2159,7 +2158,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) } if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) { - m_right_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)))); + select_nozzle_volume(m_right_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))); } else { m_right_comboBox_nozzle_volume->SetSelection(0); } @@ -2197,7 +2196,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj) else { if ((obj->GetExtderSystem()->GetTotalExtderCount() > 0) && (obj->GetExtderSystem()->GetNozzleFlowType(0) != NozzleFlowType::NONE_FLOWTYPE)) { - m_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)))); + select_nozzle_volume(m_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0))); } else { m_comboBox_nozzle_volume->SetSelection(0); } diff --git a/src/slic3r/GUI/CalibrationWizardSavePage.cpp b/src/slic3r/GUI/CalibrationWizardSavePage.cpp index 427f022d1c..fdff760078 100644 --- a/src/slic3r/GUI/CalibrationWizardSavePage.cpp +++ b/src/slic3r/GUI/CalibrationWizardSavePage.cpp @@ -732,6 +732,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector< case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break; case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break; case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break; + case NozzleVolumeType::nvtE3DHighFlow: nozzle_id_str += _L("E3D High Flow"); break; default: break; } nozzle_id_value->SetLabel(nozzle_id_str); diff --git a/src/slic3r/GUI/DeviceCore/DevCalib.cpp b/src/slic3r/GUI/DeviceCore/DevCalib.cpp index ddf71e0c62..6385fa77c6 100644 --- a/src/slic3r/GUI/DeviceCore/DevCalib.cpp +++ b/src/slic3r/GUI/DeviceCore/DevCalib.cpp @@ -36,7 +36,8 @@ static NozzleVolumeType convert_to_nozzle_type(const std::string &str) return NozzleVolumeType::nvtHighFlow; else if (str[1] == 'U') return NozzleVolumeType::nvtTPUHighFlow; - // Orca: no nvtE3DHighFlow in Orca's NozzleVolumeType; map 'B' to Standard + else if (str[1] == 'B') + return NozzleVolumeType::nvtE3DHighFlow; else return NozzleVolumeType::nvtStandard; } diff --git a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp index 578149b9d3..9f1b5311c2 100644 --- a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.cpp @@ -13,7 +13,7 @@ namespace Slic3r { -// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow) ---------------------- +// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) ------ // Device-reported U_FLOW nozzles map to nvtTPUHighFlow so a synced TPU-HF rack activates the H2C // change_filament_gcode TPU-kit branch. nvtHybrid is a slicer-only sentinel with no device // representation, so ToNozzleFlowType(nvtHybrid) falls through to NONE_FLOWTYPE. @@ -24,6 +24,7 @@ NozzleFlowType DevNozzle::ToNozzleFlowType(const NozzleVolumeType& type) case NozzleVolumeType::nvtStandard: return NozzleFlowType::S_FLOW; case NozzleVolumeType::nvtHighFlow: return NozzleFlowType::H_FLOW; case NozzleVolumeType::nvtTPUHighFlow: return NozzleFlowType::U_FLOW; + case NozzleVolumeType::nvtE3DHighFlow: return NozzleFlowType::E_FLOW; default: return NozzleFlowType::NONE_FLOWTYPE; } } @@ -34,6 +35,7 @@ NozzleVolumeType DevNozzle::ToNozzleVolumeType(const NozzleFlowType& type) case NozzleFlowType::S_FLOW: return NozzleVolumeType::nvtStandard; case NozzleFlowType::H_FLOW: return NozzleVolumeType::nvtHighFlow; case NozzleFlowType::U_FLOW: return NozzleVolumeType::nvtTPUHighFlow; + case NozzleFlowType::E_FLOW: return NozzleVolumeType::nvtE3DHighFlow; default: { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "nozzle flow type None convert to nozzle volume type Standard"; return NozzleVolumeType::nvtStandard; @@ -47,6 +49,7 @@ wxString DevNozzle::GetNozzleFlowTypeStr(NozzleFlowType type) case NozzleFlowType::H_FLOW: return _L("High Flow"); case NozzleFlowType::S_FLOW: return _L("Standard"); case NozzleFlowType::U_FLOW: return _L("TPU High Flow"); + case NozzleFlowType::E_FLOW: return _L("E3D High Flow"); default: break; } @@ -61,6 +64,7 @@ std::string DevNozzle::GetNozzleFlowTypeString(NozzleFlowType type) case NozzleFlowType::H_FLOW: return "High Flow"; case NozzleFlowType::S_FLOW: return "Standard"; case NozzleFlowType::U_FLOW: return "TPU High Flow"; + case NozzleFlowType::E_FLOW: return "E3D High Flow"; default: return "Unknown"; } } @@ -73,6 +77,7 @@ std::string DevNozzle::ToNozzleFlowString(const NozzleFlowType& type) case NozzleFlowType::S_FLOW: return "Standard"; case NozzleFlowType::H_FLOW: return "High Flow"; case NozzleFlowType::U_FLOW: return "TPU High Flow"; + case NozzleFlowType::E_FLOW: return "E3D High Flow"; default: return std::string(); } } @@ -408,6 +413,7 @@ static unordered_map _str2_nozzle_flow_type = { {"X", NozzleFlowType::S_FLOW}, {"E", NozzleFlowType::H_FLOW}, // E3D high-flow {"U", NozzleFlowType::U_FLOW}, // TPU 1.75 high-flow -> nvtTPUHighFlow + {"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow }; static unordered_map _str2_nozzle_type = { diff --git a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h index 8603baeaaf..168cb94116 100644 --- a/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h +++ b/src/slic3r/GUI/DeviceCore/DevNozzleSystem.h @@ -12,8 +12,9 @@ // Device flow-type mapping note (nozzle rack): the device U_FLOW value maps to nvtTPUHighFlow // (TPU High Flow), so a device-synced TPU-HF rack resolves to nvtTPUHighFlow and the H2C -// change_filament_gcode TPU-kit branch can activate. nvtHybrid is a slicer-only sentinel with no -// device representation, so it stays out of these device flow-type conversions. +// change_filament_gcode TPU-kit branch can activate. E_FLOW maps to nvtE3DHighFlow. nvtHybrid is a +// slicer-only sentinel with no device representation, so it stays out of these device flow-type +// conversions. // // GetExtruderNozzleInfo (and its ExtruderNozzleInfos aggregate) is intentionally omitted here; // the SelectMachine slicing-vs-installed nozzle comparison collects its per-extruder NozzleDef @@ -36,13 +37,13 @@ namespace Slic3r float m_diameter = 0.0f;// unknown until reported by the printer public: - // flow/volume conversions (Standard / High Flow / TPU High Flow) + // flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) static NozzleFlowType ToNozzleFlowType(const NozzleVolumeType& type); static NozzleVolumeType ToNozzleVolumeType(const NozzleFlowType& type); static wxString GetNozzleFlowTypeStr(NozzleFlowType type); - static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales) - static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload + static std::string GetNozzleFlowTypeString(NozzleFlowType type);// untranslated literal ("High Flow"/"Standard"/"TPU High Flow"/"E3D High Flow") — the filament blacklist JSON matches these raw strings, so it must NOT use the translated GetNozzleFlowTypeStr (would break non-English locales) + static std::string ToNozzleFlowString(const NozzleFlowType& type);// untranslated "Standard"/"High Flow"/"TPU High Flow"/"E3D High Flow" ("" for none) — the raw literal serialized into the get_auto_nozzle_mapping payload static wxString GetNozzleTypeStr(NozzleType type); public: diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 6bd690e9a6..6c09e05f48 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -347,6 +347,10 @@ static wxString _generate_nozzle_id(NozzleVolumeType nozzle_type, const std::str nozzle_id += "H"; break; } + case NozzleVolumeType::nvtE3DHighFlow: { + nozzle_id += "B"; + break; + } default: nozzle_id += "H"; break; @@ -368,6 +372,8 @@ NozzleVolumeType convert_to_nozzle_type(const std::string &str) res = NozzleVolumeType::nvtStandard; else if (str[1] == 'H') res = NozzleVolumeType::nvtHighFlow; + else if (str[1] == 'B') + res = NozzleVolumeType::nvtE3DHighFlow; return res; } diff --git a/src/slic3r/GUI/DragDropPanel.cpp b/src/slic3r/GUI/DragDropPanel.cpp index 0c1371c307..41c31d927b 100644 --- a/src/slic3r/GUI/DragDropPanel.cpp +++ b/src/slic3r/GUI/DragDropPanel.cpp @@ -566,7 +566,8 @@ std::vector SeparatedDragDropPanel::GetHighFlowFilaments() const 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)) { + if (volume_type == static_cast(NozzleVolumeType::nvtHighFlow) || + volume_type == static_cast(NozzleVolumeType::nvtE3DHighFlow)) { return m_unified_panel->GetAllFilaments(); } } diff --git a/src/slic3r/GUI/FilamentMapPanel.cpp b/src/slic3r/GUI/FilamentMapPanel.cpp index 0f3cc7217d..bd7d42333b 100644 --- a/src/slic3r/GUI/FilamentMapPanel.cpp +++ b/src/slic3r/GUI/FilamentMapPanel.cpp @@ -185,6 +185,12 @@ std::vector FilamentMapManualPanel::GetFilamentVolumeMaps() const auto preset_bundle = wxGetApp().preset_bundle; auto proj_config = preset_bundle->project_config; auto nozzle_volume_values = proj_config.option("nozzle_volume_type")->values; + // The high-flow panel of the right extruder also holds an E3D High Flow nozzle; keep that type + // instead of writing plain High Flow, which the extruder does not have. + const int right_extruder_id = 1; + const bool right_e3d_high_flow = nozzle_volume_values.size() > right_extruder_id && + nozzle_volume_values[right_extruder_id] == static_cast(NozzleVolumeType::nvtE3DHighFlow); + const int right_high_flow_volume = static_cast(right_e3d_high_flow ? NozzleVolumeType::nvtE3DHighFlow : NozzleVolumeType::nvtHighFlow); for (int i = 0; i < (int) volume_map.size(); ++i) { int filament_id = i + 1; @@ -199,7 +205,7 @@ std::vector FilamentMapManualPanel::GetFilamentVolumeMaps() const } } 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); + volume_map[i] = right_high_flow_volume; } 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); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 951739e213..3a61a95dfd 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -605,15 +605,50 @@ private: bool m_enabled{false}; }; +// The nozzle rows are set in the small body font their "T1" prefix and diameter readout use, so the +// combos of a row drop from the Body_14 they are created with to match, instead of towering over +// the labels next to them. +static void use_nozzle_row_font(ComboBox *combo) +{ + // A read-only combo draws its value with the font of the TextInput it is built on, and its + // dropped list carries a font of its own. + combo->SetFont(Label::Body_10); + combo->GetDropDown().SetFont(Label::Body_10); + + // A row is as tall as the tags of the variant switch: a line of Body_10 text plus the 3 points of + // padding they put above and below it. TextInput::messureSize() instead sizes a combo as its + // (hidden) text control plus 8, and on macOS that control's best height is a fixed 16 points + // whatever font it carries, so the box would keep the height of the Body_14 it was created with. + // Handing the control the height the row asks for, less those 8 points, keeps messureSize()'s own + // arithmetic landing on the small box however often it re-measures. + const int row_height = combo->GetTextExtent("0.4 mm").y + 2 * combo->FromDIP(3); + auto *text_ctrl = combo->GetTextCtrl(); + text_ctrl->SetFont(Label::Body_10); + text_ctrl->SetInitialSize(wxSize(text_ctrl->GetBestSize().x, row_height - 8)); + + combo->SetMinSize(wxSize(combo->GetMinSize().x, row_height)); + combo->SetSize(wxSize(combo->GetSize().x, row_height)); +} + struct ExtruderGroup : StaticBox { + // One nozzle row: the tool prefix ("T1"…"Tn", shown only when the group lists several + // extruders) plus the nozzle readout and flow combo of a single extruder. Rows are appended on + // demand and never destroyed, so an event handler may capture its row index for the lifetime + // of the group. + struct NozzleRow + { + Label * prefix = nullptr; + Label * diameter_label = nullptr; // read-only nozzle diameter of this extruder + ComboBox * flow = nullptr; + }; + ExtruderGroup(wxWindow * parent, int index, wxString const &title); wxBoxSizer * sizer = nullptr; HoverLabel * hover_label = nullptr; wxStaticText* ams_label{nullptr}; ScalableButton * btn_edit = nullptr; ComboBox * combo_diameter = nullptr; - ComboBox * combo_flow = nullptr; AMSPreview * ams[4] = {nullptr}; wxStaticText *ams_not_installed_msg{nullptr}; ScalableButton * btn_up{nullptr}; @@ -627,6 +662,23 @@ struct ExtruderGroup : StaticBox std::vector ams_1; wxString diameter; + std::vector rows; // rows[0] always exists, one row per extruder when multi + size_t nozzle_row_count = 0; // rows currently shown + wxBoxSizer * row_columns[2] = {}; // row i is dealt into column i % 2 + int group_index = -1; // ctor index, the extruder a single-row group edits + + // Show `count` nozzle rows, one per extruder, each prefixed with T when count > 1. + void SetNozzleRowCount(size_t count); + size_t NozzleRowCount() const { return nozzle_row_count; } + // Nozzle diameter readout of the shown rows, one value per extruder. A listing card cannot edit + // it: the diameter belongs to the machine variant the printer preset selects, not to the extruder. + void SetRowDiameters(const std::vector &diameters); + + NozzleRow create_nozzle_row(); + // Widths the prefix and diameter labels of the visible rows so that the flow combos of a column + // line up. + void update_row_widths(); + void set_ams_count(int n4, int n1) { if (n4 == ams_n4 && n1 == ams_n1) @@ -657,7 +709,12 @@ struct ExtruderGroup : StaticBox btn_up->msw_rescale(); btn_down->msw_rescale(); combo_diameter->Rescale(); - combo_flow->Rescale(); + for (const NozzleRow &row : rows) { + row.prefix->SetFont(Label::Body_10.Bold()); + row.diameter_label->SetFont(Label::Body_10); + row.flow->Rescale(); + } + update_row_widths(); for (int i = 0; i < 4; ++i) ams[i]->msw_rescale(); } @@ -668,6 +725,10 @@ struct ExtruderGroup : StaticBox hover_label->sys_color_changed(); if (btn_edit) btn_edit->SetBackgroundColour(extruder_group_chip_bg()); + for (const NozzleRow &row : rows) { + row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + } Refresh(); } }; @@ -691,11 +752,12 @@ struct Sidebar::priv ScalableButton * btn_edit_printer = nullptr; ScalableButton * btn_connect_printer = nullptr; - // Nozzle diameter + // Printer variant switch, titled "Variant": the combo lists the variants (nozzle sizes) of the + // current machine model and picking one switches the printer preset. StaticBox * panel_nozzle_dia = nullptr; Label * label_nozzle_title= nullptr; ComboBox * combo_nozzle_dia = nullptr; - Label * label_nozzle_type = nullptr; + Label * label_nozzle_type = nullptr; // read-only nozzle material, one-row layouts only // Printer - bed StaticBox * panel_printer_bed = nullptr; @@ -809,6 +871,8 @@ struct Sidebar::priv // otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged. std::optional get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual); bool switch_diameter(bool single); + // Switch to the printer preset offering `diameter`. + bool switch_diameter_to(const wxString &diameter); void update_sync_status(const MachineObject* obj); // Filament Track Switch (H2-family accessory): true only when the connected printer is the @@ -918,11 +982,19 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) int extruder_count = 0; const bool has_flow_variant = cfg.support_different_extruders(extruder_count); - panel_nozzle_dia->Show(!has_flow_variant); - extruder_single_sizer->Show(has_flow_variant); + // ORCA: a non-Bambu printer with several extruders lists one nozzle row per tool; Bambu's own + // multi-extruder printers keep the left/right pair above. + const bool multi_extruder_rows = !preset_bundle.is_bbl_vendor() && extruder_count > 1; + + // The variant box switches the machine variant. The card below lists the nozzles when there + // are flow variants to choose or several extruders to show; otherwise the box says it all. + panel_nozzle_dia->Show(true); + extruder_single_sizer->Show(has_flow_variant || multi_extruder_rows); + single_extruder->SetNozzleRowCount(multi_extruder_rows ? size_t(extruder_count) : 1); } else { panel_nozzle_dia->Show(false); extruder_single_sizer->Show(false); + single_extruder->SetNozzleRowCount(1); } // ORCA ensure printer section is visible after changing printer from printer selection dialog @@ -1319,7 +1391,7 @@ public: }; ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) - : StaticBox(parent) + : StaticBox(parent), group_index(index) { SetFont(Label::Body_10); SetForegroundColour(wxColour("#CECECE")); @@ -1332,29 +1404,13 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title hover_label = new HoverLabel(this, title); hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title - // Nozzle + // Nozzle. Only the dual left/right cards show this combo: it is how they ask which nozzle the + // project should print with. Every other card reads the diameter out as text. auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); this->combo_diameter = combo_diameter; combo_diameter->SetToolTip(_L("Diameter")); - - // Flow - auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - combo_flow->GetDropDown().SetUseContentWidth(true); - combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) { - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); - 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; - combo_flow->SetToolTip(_L("Flow")); + combo_diameter->Show(index >= 0); + use_nozzle_row_font(combo_diameter); // AMS auto ams_panel = new wxPanel(this, wxID_ANY); @@ -1440,10 +1496,6 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title btn_down->Hide(); wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL); - wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); - - hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5)); - hsizer->Add(combo_flow , 1); vsizer->AddSpacer(FromDIP(16)); // spacing for title and control if (index < 0) { @@ -1452,14 +1504,137 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5)); vsizer->AddSpacer(FromDIP(2)); } - vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); + // Nozzle rows. The first one is the only one for a printer whose nozzles are not listed + // individually; a multi-extruder printer grows one row per tool (see SetNozzleRowCount). + // The rows are dealt into two columns, which halves the height of a long tool list; the second + // column empties itself when there is a single row, so a one-row card still spans the width. + auto rows_sizer = new wxBoxSizer(wxHORIZONTAL); + for (wxBoxSizer *&column : row_columns) { + column = new wxBoxSizer(wxVERTICAL); + rows_sizer->Add(column, 1, wxEXPAND); + } + vsizer->Add(rows_sizer, 0, wxEXPAND); + sizer = vsizer; // the floating filament-switch icon positions itself against this card SetSizer(vsizer); + SetNozzleRowCount(1); + Layout(); AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this); } +ExtruderGroup::NozzleRow ExtruderGroup::create_nozzle_row() +{ + const size_t row_index = rows.size(); + NozzleRow row; + + row.prefix = new Label(this, Label::Body_10.Bold()); + row.prefix->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.prefix->SetLabelText(wxString::Format("T%d", int(row_index) + 1)); + row.prefix->Hide(); + + row.diameter_label = new Label(this, Label::Body_10); + row.diameter_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row.diameter_label->Hide(); + + auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + combo_flow->GetDropDown().SetUseContentWidth(true); + combo_flow->Bind(wxEVT_COMBOBOX, [this, row_index, combo_flow](wxCommandEvent &evt) { + // A one-row card edits the extruder it was created for, a multi-row card one extruder per row. + const int index = nozzle_row_count > 1 ? int(row_index) : group_index; + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + 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(); + } + }); + combo_flow->SetToolTip(_L("Flow")); + row.flow = combo_flow; + use_nozzle_row_font(combo_flow); + + auto row_sizer = new wxBoxSizer(wxHORIZONTAL); + row_sizer->Add(row.prefix, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3)); + if (row_index == 0) + row_sizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5)); + row_sizer->Add(row.diameter_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(5)); + row_sizer->Add(row.flow, 1); + // Row-major: T1 T2 on the first line, T3 T4 on the second, and so on. + row_columns[row_index % 2]->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); + + return row; +} + +void ExtruderGroup::update_row_widths() +{ + // Measured from the rows on screen rather than fixed, so no gap is reserved for a "T10" or a + // "0.25 mm" that no row carries while the flow combos of a column still line up. + wxSize prefix_width(0, -1); + wxSize diameter_width(0, -1); + for (const NozzleRow &row : rows) { + if (row.prefix->IsShown()) + prefix_width.x = std::max(prefix_width.x, row.prefix->GetTextExtent(row.prefix->GetLabelText()).x); + if (row.diameter_label->IsShown()) + diameter_width.x = std::max(diameter_width.x, row.diameter_label->GetTextExtent(row.diameter_label->GetLabelText()).x); + } + for (const NozzleRow &row : rows) { + row.prefix->SetMinSize(prefix_width); + row.diameter_label->SetMinSize(diameter_width); + } +} + +void ExtruderGroup::SetRowDiameters(const std::vector &diameters) +{ + for (size_t i = 0; i < std::min(nozzle_row_count, diameters.size()); ++i) { + // Spelled like the calibration dialogs spell a nozzle diameter, "0.4 mm". + const wxString text = from_u8(get_diameter_string(diameters[i])) + " mm"; + rows[i].diameter_label->SetLabelText(text); + rows[i].diameter_label->SetToolTip(text); + } + update_row_widths(); + Layout(); +} + +void ExtruderGroup::SetNozzleRowCount(size_t count) +{ + const size_t row_count = std::min(count, size_t(MAXIMUM_EXTRUDER_NUMBER)); + if (row_count == nozzle_row_count) + return; + nozzle_row_count = row_count; + + // Every row names its tool as soon as there is more than one, so a single-nozzle card keeps the + // plain "Nozzle" look. A row is shown by showing its widgets: a sizer item holding a sub-sizer + // reports itself as shown while any of its windows is, so hiding the sub-sizer alone would + // leave the row in the layout. + for (size_t i = 0; i < row_count; ++i) { + if (i >= rows.size()) + rows.push_back(create_nozzle_row()); + rows[i].prefix->Show(row_count > 1); + rows[i].diameter_label->Show(group_index < 0); // the dual left/right cards show combo_diameter + // The flow combo's own visibility is decided with its choices, see update_extruder_variant. + } + for (size_t i = row_count; i < rows.size(); ++i) { + rows[i].prefix->Hide(); + rows[i].diameter_label->Hide(); + rows[i].flow->Hide(); + } + + // The rows just changed: re-measure them, then let the card and its containers give it the new room. + update_row_widths(); + Layout(); + if (wxWindow *parent = GetParent()) { + parent->Layout(); + if (wxWindow *grand_parent = parent->GetParent()) + grand_parent->Layout(); + } +} + void ExtruderGroup::update_ams() { static AMSinfo info4; @@ -1588,9 +1763,19 @@ bool Sidebar::priv::switch_diameter(bool single) diameter = diameter_left; } } - + + return switch_diameter_to(diameter); +} + +bool Sidebar::priv::switch_diameter_to(const wxString &diameter) +{ // ORCA: Check if the selected diameter matches the current nozzle diameter in the config Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + // The combo lists printer variants, and the variant of a mixed-nozzle machine ("0.4+0.6") is no + // single extruder's diameter, so the preset's own variant answers first. + if (printer_preset.config.opt_string("printer_variant") == diameter.ToStdString()) { + return true; + } auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); if (nozzle_diameter && nozzle_diameter->size() > 0) { auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); @@ -2679,7 +2864,8 @@ Sidebar::Sidebar(Plater *parent) p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); }); - p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT); + // "Variant", not "Nozzle": picking one switches the printer preset (see panel_nozzle_dia). + p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Variant"), LB_PROPAGATE_MOUSE_EVENT); p->label_nozzle_title->SetFont(Label::Body_10); p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); @@ -2688,12 +2874,15 @@ Sidebar::Sidebar(Plater *parent) p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) { - auto evt_combo = (*p->single_extruder).combo_diameter; - evt_combo->SetSelection(e.GetSelection()); - wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId()); - evt.SetEventObject(evt_combo); - evt.SetInt(e.GetSelection()); - wxPostEvent(evt_combo, evt); + // This box is the machine-variant selector, so it switches the printer preset directly + // instead of feeding an extruder card's combo. Deferred, because switching preset + // rebuilds this very combo. + const wxString diameter = p->combo_nozzle_dia->GetValue(); + p->combo_nozzle_dia->CallAfter([this, diameter]() { + p->is_switching_diameter = true; + p->switch_diameter_to(diameter); + p->is_switching_diameter = false; + }); e.Skip(); }); // ORCA paint whole combobox on focus @@ -3673,8 +3862,16 @@ void Sidebar::update_presets(Preset::Type preset_type) auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); auto diameter = printer_preset.config.opt_string("printer_variant"); auto extruder_max_nozzle_count = printer_preset.config.option("extruder_max_nozzle_count"); - auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, int index) { - extruder.combo_flow->Clear(); + auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, size_t row, int index) { + ComboBox *combo_flow = extruder.rows[row].flow; + combo_flow->Clear(); + // A profile may leave the per-extruder lists shorter than the nozzle count (they are + // padded when the printer is loaded, but a hand-written preset need not be). + if (index >= int(extruder_variants->values.size()) || index >= int(extruders->values.size()) || + index >= int(nozzle_volumes->values.size())) { + combo_flow->Hide(); + return; + } auto type = extruders_def->enum_labels[extruders->values[index]]; int select = -1; for (size_t i = 0; i < nozzle_volumes_def->enum_labels.size(); ++i) { @@ -3684,26 +3881,36 @@ void Sidebar::update_presets(Preset::Type preset_type) if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) || (extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid)) { - if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" || + auto cur_volume_type = nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]); + // Defensive: profiles restrict E3D to 0.4 / 0.6; keep it out elsewhere. + if (cur_volume_type == NozzleVolumeType::nvtE3DHighFlow && diameter != "0.4" && diameter != "0.6") + continue; + if (cur_volume_type == NozzleVolumeType::nvtHighFlow && (diameter == "0.2" || is_skip_high_flow_printer(printer_model))) continue; - if (nozzle_volumes->values[index] == i) - select = extruder.combo_flow->GetCount(); - extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)i); + // The client data is the enum value, not the label position: E3D High Flow is 5 + // but sits at position 4, and a position would write an invalid type. + if (nozzle_volumes->values[index] == cur_volume_type) + select = combo_flow->GetCount(); + combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)(intptr_t)cur_volume_type); } } if (select == -1) - select = extruder.combo_flow->GetCount() - 1; - extruder.combo_flow->SetSelection(select); + select = combo_flow->GetCount() - 1; + combo_flow->SetSelection(select); + // No flow variant for this extruder: the diameter keeps the whole row. + combo_flow->Show(combo_flow->GetCount() > 0); }; auto update_extruder_diameter = [&diameters, &nozzle_diameter](int extruder_index,ExtruderGroup & extruder) { extruder.combo_diameter->Clear(); + if (extruder_index >= int(nozzle_diameter->values.size())) + return; int select = -1; // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias - if(diameters[0].empty() && !nozzle_dia.empty()){ + if(!diameters.empty() && diameters[0].empty() && !nozzle_dia.empty()){ diameters[0] = nozzle_dia; } // Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option @@ -3719,14 +3926,15 @@ void Sidebar::update_presets(Preset::Type preset_type) extruder.diameter = nozzle_dia; }; auto image_path = get_cur_select_bed_image(); - if (is_dual_extruder) { + const bool multi_extruder_rows = p->single_extruder->NozzleRowCount() > 1; + if (is_dual_extruder && !multi_extruder_rows) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder); AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder); - update_extruder_variant(*p->left_extruder, 0); - update_extruder_variant(*p->right_extruder, 1); + update_extruder_variant(*p->left_extruder, 0, 0); + update_extruder_variant(*p->right_extruder, 0, 1); //if (!p->is_switching_diameter) { update_extruder_diameter(0, *p->left_extruder); update_extruder_diameter(1, *p->right_extruder); @@ -3734,15 +3942,27 @@ void Sidebar::update_presets(Preset::Type preset_type) p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); } else { AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder); - update_extruder_variant(*p->single_extruder, 0); + for (size_t row = 0; row < p->single_extruder->NozzleRowCount(); ++row) + update_extruder_variant(*p->single_extruder, row, int(row)); //if (!p->is_switching_diameter) update_extruder_diameter(0, *p->single_extruder); + // The card reads the nozzles out, the variant box above is what switches the machine. + p->single_extruder->SetRowDiameters(nozzle_diameter->values); + if (multi_extruder_rows) { + // The hidden left/right cards keep mirroring the first extruders' diameters: the + // device sync path (a connected Bambu printer or a printer agent) still reads them. + update_extruder_diameter(0, *p->left_extruder); + update_extruder_diameter(1, *p->right_extruder); + } // ORCA sync unified nozzle combo box p->combo_nozzle_dia->Clear(); for (size_t i = 0; i < diameters.size(); ++i) p->combo_nozzle_dia->Append(diameters[i], {}); - p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection()); + // Prefer the variant the preset names: a mixed-nozzle machine reads "0.4+0.6", which is + // no single extruder's diameter. + const int variant = p->combo_nozzle_dia->FindString(diameter); + p->combo_nozzle_dia->SetSelection(variant != wxNOT_FOUND ? variant : (*p->single_extruder).combo_diameter->GetSelection()); // ORCA update nozzle type const auto& full_config = wxGetApp().preset_bundle->full_config(); diff --git a/src/slic3r/GUI/PrintOptionsDialog.cpp b/src/slic3r/GUI/PrintOptionsDialog.cpp index d68bd24ccc..f03bcb1d29 100644 --- a/src/slic3r/GUI/PrintOptionsDialog.cpp +++ b/src/slic3r/GUI/PrintOptionsDialog.cpp @@ -1793,6 +1793,7 @@ wxString PrinterPartsDialog::GetString(NozzleFlowType nozzle_flow_type) const { case Slic3r::S_FLOW: return _L("Standard"); case Slic3r::H_FLOW: return _L("High flow"); case Slic3r::U_FLOW: return _L("TPU High flow"); + case Slic3r::E_FLOW: return _L("E3D High Flow"); default: break; } diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index a3b83bd643..04eb2b1612 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -75,6 +75,9 @@ std::string get_nozzle_volume_type_cloud_string(NozzleVolumeType nozzle_volume_t else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { return "tpu_high_flow"; } + else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) { + return "e3d_high_flow"; + } else if (nozzle_volume_type == NozzleVolumeType::nvtHybrid) { // to be supported return "hybrid_flow"; diff --git a/src/slic3r/GUI/SyncAmsInfoDialog.cpp b/src/slic3r/GUI/SyncAmsInfoDialog.cpp index 19b1ddc568..07c75e809a 100644 --- a/src/slic3r/GUI/SyncAmsInfoDialog.cpp +++ b/src/slic3r/GUI/SyncAmsInfoDialog.cpp @@ -1516,6 +1516,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err used_extruders_flow[used_extruders[i]] = "Standard"; } else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { used_extruders_flow[used_extruders[i]] = "TPU High Flow"; + } else if (nozzle_volume_type == NozzleVolumeType::nvtE3DHighFlow) { + used_extruders_flow[used_extruders[i]] = "E3D High Flow"; } else { used_extruders_flow[used_extruders[i]] = "High Flow"; } @@ -1533,6 +1535,8 @@ bool SyncAmsInfoDialog::is_nozzle_type_match(DevExtderSystem data, wxString &err flow_type_of_machine.push_back("Standard"); } else if (it->GetNozzleFlowType() == NozzleFlowType::U_FLOW) { flow_type_of_machine.push_back("TPU High Flow"); + } else if (it->GetNozzleFlowType() == NozzleFlowType::E_FLOW) { + flow_type_of_machine.push_back("E3D High Flow"); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bf348f856f..67e899c40e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -506,7 +506,7 @@ void Tab::create_preset_tab() if (dynamic_cast(this) || dynamic_cast(this)) { m_extruder_switch = new MultiSwitchButton(panel); - m_extruder_switch->SetMaxSize({em_unit(this) * 40, -1}); + m_extruder_switch->SetFitToOptions(); m_extruder_switch->Bind(wxCUSTOMEVT_MULTISWITCH_SELECTION, [this](auto &evt) { evt.Skip(); int selection = evt.GetInt(); @@ -539,7 +539,8 @@ void Tab::create_preset_tab() auto right_sizer = new wxBoxSizer(wxHORIZONTAL); m_variant_sizer->AddStretchSpacer(1); - m_variant_sizer->Add(m_extruder_switch, 0, wxALIGN_CENTER, 0); + // Orca: proportion 1 lets a narrow row squeeze the switch, which then scrolls its buttons. + m_variant_sizer->Add(m_extruder_switch, 1, wxALIGN_CENTER, 0); m_variant_sizer->Add(right_sizer, 1, wxALIGN_CENTER); right_sizer->AddStretchSpacer(1); right_sizer->Add(m_extruder_sync_box, 0, wxALIGN_CENTER | wxRIGHT, m_em_unit); @@ -8172,7 +8173,7 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) // Orca: a non-Bambu dual-nozzle printer has two extruders but a single variant column, so // the nozzle switch and sync button have nothing to act on. Only enable with real variants. - if (extruder_nums == 2 && m_preset_bundle->support_different_extruders()) { + if (extruder_nums >= 2 && m_preset_bundle->support_different_extruders()) { auto options = generate_extruder_options(); m_extruder_switch->SetOptions(options); @@ -8219,6 +8220,23 @@ void Tab::update_extruder_variants(int extruder_id, bool reload) } } +// The variant switch tags are the narrowest place a volume type is named, so they abbreviate it; +// the flow combo boxes and the Printer tab's parameter labels keep the full names. +static wxString short_nozzle_volume_name(const std::string &volume_name) +{ + // Hybrid has no entry on purpose: it is never a variant string, and the switch lists its two + // sub-nozzle types as separate tags (see generate_extruder_options). + if (volume_name == "Standard") + return "SF"; + if (volume_name == "High Flow") + return "HF"; + if (volume_name == "TPU High Flow") + return "TPU HF"; + if (volume_name == "E3D High Flow") + return "E3D HF"; + return from_u8(volume_name); +} + std::vector Tab::generate_extruder_options() { std::vector options; @@ -8259,7 +8277,7 @@ std::vector Tab::generate_extruder_options() nozzle = ""; } } - options.push_back(wxString::Format(_L("%s: %s"), _L(drive), _L(nozzle))); + options.push_back(wxString::Format(_L("%s: %s"), _L(drive), short_nozzle_volume_name(nozzle))); } return options; } @@ -8273,18 +8291,22 @@ std::vector Tab::generate_extruder_options() } std::string pt = m_preset_bundle->printers.get_edited_preset().get_printer_type(m_preset_bundle); + // Orca: the main/deputy toolhead names describe a dual-nozzle printer, where extruder 0 is the + // left (deputy) and extruder 1 the right (main) nozzle. From three extruders on the tools are + // interchangeable, so name them by index instead of repeating one side. for (int i = 0; i < extruder_nums; ++i) { - int ext_id = (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID; - wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name( - pt, ext_id, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true)); + wxString extruder_name = extruder_nums > 2 ? wxString::Format("T%d", i + 1) : + _L(DevPrinterConfigUtil::get_toolhead_display_name( + pt, (i == 0) ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID, + ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true)); NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); if (volume_type == NozzleVolumeType::nvtHybrid) { - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("Standard"))); - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("High Flow"))); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtStandard)))); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, short_nozzle_volume_name(get_nozzle_volume_type_string(NozzleVolumeType::nvtHighFlow)))); } else { - wxString volume_name = get_nozzle_volume_type_name(volume_type); - options.push_back(wxString::Format(_L("%s: %s"), extruder_name, volume_name)); + options.push_back(wxString::Format(_L("%s: %s"), extruder_name, + short_nozzle_volume_name(get_nozzle_volume_type_string(volume_type)))); } } return options; @@ -8315,7 +8337,10 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id) Preset& printer_preset = m_preset_bundle->printers.get_edited_preset(); auto nozzle_volumes = m_preset_bundle->project_config.option("nozzle_volume_type"); auto extruders = printer_preset.config.option("extruder_type"); - if (nozzle_volumes->values.size() < 2 || extruders->values.size() < 2) { + // Orca: every rule below describes the two toolheads of a dual-nozzle printer as left/right. + // A printer with any other extruder count has no single counterpart to copy to, so it gets no + // sync button (a toolchanger would need a target to be chosen). + if (nozzle_volumes->values.size() != 2 || extruders->values.size() != 2) { return false; } @@ -8456,6 +8481,10 @@ void Tab::sync_excluder() ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second, stride); }; int active_index = get_current_active_extruder(); + // The button copies to the other toolhead, so it is only offered when that other one exists; + // without this the `1 - active_index` below would index an extruder that is not there. + if (!get_extruder_sync_enable_state(active_index)) + return; auto active_nozzle = get_actual_nozzle_volume_type(active_index); int from_index = get_index_for_extruder(active_index, active_nozzle); int dest_index = get_index_for_extruder(1 - active_index, active_nozzle); diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp index 316389d65c..42659431f0 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.cpp @@ -70,8 +70,8 @@ int getExtruderNozzleCountTotal(PresetBundle *preset_bundle, int extruder_id) // ---- ManualNozzleCountDialog ---------------------------------------------------------------------------------- -ManualNozzleCountDialog::ManualNozzleCountDialog( - wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero) +ManualNozzleCountDialog::ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count, + int max_nozzle_count, bool force_no_zero, const std::set &supported_types) : DPIDialog(parent, wxID_ANY, _L("Set nozzle count"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX) { SetBackgroundColour(*wxWHITE); @@ -93,43 +93,39 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( for (int i = 0; i <= max_nozzle_count; ++i) nozzle_choices.Add(wxString::Format("%d", i)); - // A Hybrid extruder mixes Standard and High Flow nozzles, so it gets both count choices; the concrete - // types get exactly one. - if (volume_type == nvtStandard || volume_type == nvtHybrid) { - wxBoxSizer *standard_sizer = new wxBoxSizer(wxHORIZONTAL); - auto standard_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard)), wxDefaultPosition, wxSize(FromDIP(100), -1)); - standard_label->SetFont(Label::Body_14); - standard_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); - standard_sizer->Add(standard_label, 0, wxALIGN_CENTER_VERTICAL); - m_standard_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); - std::vector::iterator iter; - for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++) - m_standard_choice->Append(*iter); - m_standard_choice->SetSelection(standard_count); - standard_sizer->Add(m_standard_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - choice_sizer->Add(standard_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); - } - if (volume_type == nvtHighFlow || volume_type == nvtHybrid) { - wxBoxSizer *highflow_sizer = new wxBoxSizer(wxHORIZONTAL); - auto highflow_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow)), wxDefaultPosition, wxSize(FromDIP(100), -1)); - highflow_label->SetFont(Label::Body_14); - highflow_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); - highflow_sizer->Add(highflow_label, 0, wxALIGN_CENTER_VERTICAL); - m_highflow_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); - std::vector::iterator iter; - for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++) - m_highflow_choice->Append(*iter); - m_highflow_choice->SetSelection(highflow_count); - highflow_sizer->Add(m_highflow_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); - choice_sizer->Add(highflow_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); - } + // A Hybrid extruder mixes several nozzle volume types, so it gets a count choice for each type the + // extruder actually has; a concrete type gets exactly its own, which it supports by definition. + auto shows = [volume_type, &supported_types](NozzleVolumeType type) { + return volume_type == type || (volume_type == nvtHybrid && (supported_types.empty() || supported_types.count(type) > 0)); + }; + auto add_count_choice = [this, content, choice_sizer, &nozzle_choices](NozzleVolumeType type, int count) { + wxBoxSizer *row_sizer = new wxBoxSizer(wxHORIZONTAL); + auto label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(type)), wxDefaultPosition, wxSize(FromDIP(100), -1)); + label->SetFont(Label::Body_14); + label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); + row_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL); + auto choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY); + for (const wxString &item : nozzle_choices) + choice->Append(item); + choice->SetSelection(count); + row_sizer->Add(choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5)); + choice_sizer->Add(row_sizer, 0, wxTOP | wxRIGHT, FromDIP(15)); + return choice; + }; + if (shows(nvtStandard)) + m_standard_choice = add_count_choice(nvtStandard, standard_count); + if (shows(nvtHighFlow)) + m_highflow_choice = add_count_choice(nvtHighFlow, highflow_count); + if (shows(nvtE3DHighFlow)) + m_e3d_choice = add_count_choice(nvtE3DHighFlow, e3d_count); m_error_label = new wxStaticText(this, wxID_ANY, ""); m_error_label->SetForegroundColour(wxColour("#E14747")); m_error_label->Hide(); - auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](int standard_count, int highflow_count) { - const int total_count = standard_count + highflow_count; + auto update_nozzle_error = [this, force_no_zero, content, max_nozzle_count](wxCommandEvent &e) { + // The Hybrid count is the sum over every choice on screen, whatever the extruder's own type. + const int total_count = GetNozzleCount(nvtHybrid); if (0 < total_count && total_count <= max_nozzle_count && m_error_label->IsShown()) { m_error_label->Hide(); m_confirm_btn->Enable(); @@ -145,18 +141,12 @@ ManualNozzleCountDialog::ManualNozzleCountDialog( Layout(); Fit(); } + e.Skip(); }; - if (m_standard_choice) - m_standard_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) { - update_nozzle_error(m_standard_choice->GetSelection(), m_highflow_choice ? m_highflow_choice->GetSelection() : 0); - e.Skip(); - }); - if (m_highflow_choice) - m_highflow_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) { - update_nozzle_error(m_standard_choice ? m_standard_choice->GetSelection() : 0, m_highflow_choice->GetSelection()); - e.Skip(); - }); + for (ComboBox *choice : {m_standard_choice, m_highflow_choice, m_e3d_choice}) + if (choice) + choice->Bind(wxEVT_COMBOBOX, update_nozzle_error); content_sizer->Add(nozzle_icon, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(15)); content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL); @@ -181,9 +171,10 @@ int ManualNozzleCountDialog::GetNozzleCount(NozzleVolumeType volume_type) const return m_standard_choice ? m_standard_choice->GetSelection() : 0; if (volume_type == nvtHighFlow) return m_highflow_choice ? m_highflow_choice->GetSelection() : 0; + if (volume_type == nvtE3DHighFlow) + return m_e3d_choice ? m_e3d_choice->GetSelection() : 0; if (volume_type == nvtHybrid) - return (m_standard_choice ? m_standard_choice->GetSelection() : 0) + - (m_highflow_choice ? m_highflow_choice->GetSelection() : 0); + return GetNozzleCount(nvtStandard) + GetNozzleCount(nvtHighFlow) + GetNozzleCount(nvtE3DHighFlow); return 0; } @@ -318,6 +309,7 @@ void manuallySetNozzleCount(int extruder_id) const NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id]); const int standard_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard); const int highflow_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow); + const int e3d_count = getExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow); // Require at least one nozzle for a Hybrid extruder (an empty mix is meaningless) and when the other // extruder currently has none. @@ -325,11 +317,19 @@ void manuallySetNozzleCount(int extruder_id) if (nozzle_volume_type_opt->values.size() > 1) 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); + // The printer profile's variant list is the authority on which volume types an extruder provides. An + // empty set means the profile could not be read, and the dialog then skips filtering instead of hiding + // everything. + const std::set supported_types = + get_extruder_supported_nozzle_volume_types(preset_bundle->printers.get_edited_preset().config, extruder_id); + + ManualNozzleCountDialog dialog(wxGetApp().plater(), volume_type, standard_count, highflow_count, e3d_count, + max_nozzle_count->values[extruder_id], force_no_zero, supported_types); if (dialog.ShowModal() == wxID_OK) { if (volume_type == nvtHybrid) { setExtruderNozzleCount(preset_bundle, extruder_id, nvtStandard, dialog.GetNozzleCount(nvtStandard), true); setExtruderNozzleCount(preset_bundle, extruder_id, nvtHighFlow, dialog.GetNozzleCount(nvtHighFlow), false); + setExtruderNozzleCount(preset_bundle, extruder_id, nvtE3DHighFlow, dialog.GetNozzleCount(nvtE3DHighFlow), false); } else { setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, dialog.GetNozzleCount(volume_type), true); } diff --git a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp index 5a3e9374a7..50bd534c72 100644 --- a/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp +++ b/src/slic3r/GUI/Widgets/MultiNozzleSync.hpp @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -64,7 +65,9 @@ struct NozzleOption class ManualNozzleCountDialog : public DPIDialog { public: - ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int max_nozzle_count, bool force_no_zero); + // supported_types lists the volume types the extruder physically provides; an empty set means "unknown", in which case no filtering is applied. + ManualNozzleCountDialog(wxWindow *parent, NozzleVolumeType volume_type, int standard_count, int highflow_count, int e3d_count, int max_nozzle_count, bool force_no_zero, + const std::set &supported_types); ~ManualNozzleCountDialog() override = default; void on_dpi_changed(const wxRect &suggested_rect) override {} int GetNozzleCount(NozzleVolumeType volume_type) const; @@ -72,6 +75,7 @@ public: private: ComboBox *m_standard_choice{nullptr}; ComboBox *m_highflow_choice{nullptr}; + ComboBox *m_e3d_choice{nullptr}; Button *m_confirm_btn{nullptr}; wxStaticText *m_error_label{nullptr}; }; diff --git a/src/slic3r/GUI/Widgets/SwitchButton.cpp b/src/slic3r/GUI/Widgets/SwitchButton.cpp index c391ddd017..f9fa3405d3 100644 --- a/src/slic3r/GUI/Widgets/SwitchButton.cpp +++ b/src/slic3r/GUI/Widgets/SwitchButton.cpp @@ -4,6 +4,8 @@ #include "StaticBox.hpp" #include "../wxExtensions.hpp" + +#include #include "../GUI_App.hpp" #include "slic3r/GUI/I18N.hpp" #include "../Utils/MacDarkMode.hpp" @@ -620,19 +622,34 @@ MultiSwitchButton::MultiSwitchButton(wxWindow *parent, wxWindowID id, const wxPo std::make_pair(0x6B6B6B, (int) StateColor::NotChecked), std::make_pair(0xFFFFFE, (int) StateColor::Normal))) , m_button_radius(10.0) - , m_button_padding(10, 6) + , m_button_padding(FromDIP(wxSize(11, 3))) { SetCornerRadius(m_button_radius); SetBorderWidth(0); - sizer = new wxBoxSizer(wxHORIZONTAL); - auto *hsizer = new wxBoxSizer(wxVERTICAL); - hsizer->Add(sizer, 1, wxEXPAND); - SetSizer(hsizer); - SetMinSize(wxSize(-1, 20)); + // Orca: a switch can hold more buttons than the layout has room for (a toolchanger lists one per + // tool), so they live in a scrolled area: the caller caps the switch at its natural width and + // this scrolls horizontally instead of clipping the last buttons. + m_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxBORDER_NONE); + m_scroll->SetBackgroundColour(GetBackgroundColour()); + // The buttons are a single row, so only the horizontal bar may ever appear: a vertical one would + // eat into the row's height. + m_scroll->ShowScrollbars(wxSHOW_SB_DEFAULT, wxSHOW_SB_NEVER); + m_scroll->EnableScrolling(true, false); + m_scroll->SetScrollRate(FromDIP(10), 0); + sizer = new wxBoxSizer(wxHORIZONTAL); + m_scroll->SetSizer(sizer); + + auto *hsizer = new wxBoxSizer(wxVERTICAL); + hsizer->Add(m_scroll, 1, wxEXPAND); + SetSizer(hsizer); + SetMinSize(wxSize(-1, options_height())); + + Bind(wxEVT_SIZE, &MultiSwitchButton::on_size, this); Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MultiSwitchButton::button_clicked, this); - SetFont(Label::Body_12); + // The tags name a tool and its volume type only, so they stay compact. + SetFont(Label::Body_10); } MultiSwitchButton::~MultiSwitchButton() @@ -640,10 +657,88 @@ MultiSwitchButton::~MultiSwitchButton() DeleteAllOptions(); } +int MultiSwitchButton::options_height() const +{ + // With no button to measure yet, keep the placeholder height the switch starts with. + return btns.empty() ? FromDIP(20) : btns.front()->GetMinSize().y; +} + +void MultiSwitchButton::update_scroll_range() +{ + // The scrollbar range is measured against the virtual size, so it has to follow the buttons + // whenever their labels or count change. + m_scroll->InvalidateBestSize(); + m_scroll->FitInside(); + + // A scrolled window reports its min size plus a scrollbar as its best size, never the width of + // the buttons it holds, so the layout has to be given that width explicitly. It is also the + // widest this switch wants to be: a row with less room squeezes it below this and it scrolls. + const wxSize content = sizer->CalcMin(); + SetMinSize(wxSize(content.x, options_height() + scrollbar_height(content.x))); + SetMaxSize(m_fit_to_options ? wxSize(content.x, -1) : wxDefaultSize); + InvalidateBestSize(); +} + +int MultiSwitchButton::scrollbar_height(int options_width) const +{ + // The bar is drawn inside the switch, so while the buttons need more width than the row gave us + // the switch has to be taller by the bar's height, or the bar sits on top of the buttons. + const int width = GetClientSize().x; + if (width <= 0 || options_width <= width) + return 0; + + const int bar = wxSystemSettings::GetMetric(wxSYS_HSCROLL_Y, this); + return bar > 0 ? bar : 0; +} + +void MultiSwitchButton::on_size(wxSizeEvent &evt) +{ + evt.Skip(); + + // The row resized, so the buttons may now overflow it (or no longer fit in it) and the room the + // bar needs changed with that. Width does not depend on height, so this settles in one pass. + const int height = options_height() + scrollbar_height(sizer->CalcMin().x); + if (GetMinSize().y != height) { + SetMinSize(wxSize(GetMinSize().x, height)); + // The switch sits in a row of this tab, and the tab in a panel that shares its height + // with the page view, so both have to lay out again for the taller row to get its room. + if (wxWindow *tab = GetParent()) { + tab->Layout(); + if (wxWindow *panel = tab->GetParent()) + panel->Layout(); + } + } +} + +void MultiSwitchButton::scroll_option_into_view(Button *btn) +{ + const int width = m_scroll->GetClientSize().x; + const int view_x = m_scroll->GetViewStartPixels().x; + if (width <= 0) // not laid out yet: there is no view to scroll + return; + + const wxRect rect = btn->GetRect(); + const int right = rect.GetRight() + 1; + int target = view_x; + if (rect.x < view_x) + target = rect.x; + else if (right > view_x + width) + target = right - width; + + if (target == view_x) + return; + + // Scroll() counts scroll units; round up so the whole button ends up inside the view rather + // than a few pixels short of it. + int step = 1; + m_scroll->GetScrollPixelsPerUnit(&step, nullptr); + m_scroll->Scroll((target + step - 1) / step, -1); +} + int MultiSwitchButton::AppendOption(const wxString &option, void *clientData) { Button *btn = new Button(); - btn->Create(this, option, "", wxBORDER_NONE); + btn->Create(m_scroll, option, "", wxBORDER_NONE); btn->SetFont(GetFont()); btn->SetBackgroundColor(m_bg_color); btn->SetTextColor(m_text_color); @@ -654,9 +749,6 @@ int MultiSwitchButton::AppendOption(const wxString &option, void *clientData) btns.push_back(btn); sizer->Add(btn, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL); - wxSize text_size = btn->GetTextExtent(option); - btn->SetMinSize(wxSize(text_size.x + m_button_padding.x * 2 + 6, -1)); - return int(btns.size()) - 1; } @@ -666,6 +758,7 @@ void MultiSwitchButton::SetOptions(const std::vector &options) for (const auto &option : options) AppendOption(option); + update_scroll_range(); Layout(); Refresh(); } @@ -700,6 +793,8 @@ void MultiSwitchButton::SetSelection(int index) sel = index; update_button_styles(); send_selection_event(); + // The selected button may be scrolled out of sight, e.g. when the tab restores the active tool. + scroll_option_into_view(btns[sel]); Refresh(); } @@ -718,6 +813,7 @@ void MultiSwitchButton::SetOptionText(unsigned int index, const wxString &text) if (index >= btns.size()) return; btns[index]->SetLabel(text); + update_scroll_range(); } void *MultiSwitchButton::GetOptionData(unsigned int index) const @@ -769,6 +865,7 @@ void MultiSwitchButton::SetButtonPadding(const wxSize &padding) m_button_padding = padding; for (auto *btn : btns) btn->SetPaddingSize(padding); + update_scroll_range(); Layout(); Refresh(); } @@ -777,6 +874,8 @@ void MultiSwitchButton::Rescale() { for (auto *btn : btns) btn->Rescale(); + // Rescaling can change how the labels measure, and the scrollbar range follows the buttons. + update_scroll_range(); } void MultiSwitchButton::button_clicked(wxCommandEvent &event) diff --git a/src/slic3r/GUI/Widgets/SwitchButton.hpp b/src/slic3r/GUI/Widgets/SwitchButton.hpp index 6e271abfaf..decff25919 100644 --- a/src/slic3r/GUI/Widgets/SwitchButton.hpp +++ b/src/slic3r/GUI/Widgets/SwitchButton.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "Button.hpp" @@ -164,6 +165,10 @@ public: void SetButtonCornerRadius(double radius); void SetButtonPadding(const wxSize &padding); + // Keep the switch exactly as wide as the buttons need instead of letting the layout stretch it. + // A layout with less room than that still squeezes it, and it scrolls its buttons then. + void SetFitToOptions(bool fit = true) { m_fit_to_options = fit; update_scroll_range(); } + void Rescale(); protected: @@ -173,9 +178,20 @@ protected: bool send_selection_event(); private: + // Height of the single button row, measured from the buttons themselves. + int options_height() const; + void update_scroll_range(); + int scrollbar_height(int options_width) const; + void scroll_option_into_view(Button *btn); + void on_size(wxSizeEvent &evt); + std::vector