diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index d37ac20a45..d478a42b88 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -18749,3 +18749,9 @@ msgstr "" msgid "Both" msgstr "" + +msgid "" +"The printer has no nozzle matching the slicing file (%s). Please install a " +"matching nozzle on the printer, or set the corresponding printer preset " +"while slicing." +msgstr "" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ec0185d3fe..e72edfe8f5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -638,7 +638,8 @@ std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolume //extruder_type = etDirectDrive; return variant_string; } - if (nozzle_volume_type > nvtMaxNozzleVolumeType) { + auto nozzle_volume_types = get_valid_nozzle_volume_type(); + if (nozzle_volume_types.count(nozzle_volume_type) == 0) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", unsupported NozzleVolumeType=%1%")%nozzle_volume_type; //extruder_type = etDirectDrive; return variant_string; @@ -9679,6 +9680,9 @@ int DynamicPrintConfig::get_index_for_extruder(int extruder_or_filament_id, std: if (variant_opt != nullptr) { int v_size = variant_opt->values.size(); const bool has_complete_id_map = id_opt && int(id_opt->values.size()) >= v_size; + // nvtHybrid not supported in presets, switch to nvtStandard to match the preset values + if (nozzle_volume_type == nvtHybrid) + nozzle_volume_type = nvtStandard; std::string extruder_variant = get_extruder_variant_string(extruder_type, nozzle_volume_type); for (int index = 0; index < v_size; index++) { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9d091a7e01..64fcea68d0 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -452,7 +452,9 @@ enum ExtruderType { enum NozzleVolumeType { nvtStandard = 0, nvtHighFlow, - nvtHybrid, // match-any / mixed sentinel; hidden from user selectors, kept out of the shipping slicer + nvtHybrid, // extruder holds a mix of Standard and High Flow sub-nozzles; selectable only for extruders + // 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 // Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable. nvtMaxNozzleVolumeType = nvtTPUHighFlow @@ -481,7 +483,8 @@ static std::set get_valid_nozzle_volume_type() { std::set type; for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) { auto t = static_cast(i); - // Hybrid is a "match-any / mixed" sentinel, never a user-selectable physical type. + // Hybrid is not a physical nozzle variant: presets never define it, so it must not + // produce a variant string. if (t == nvtHybrid) continue; type.insert(t); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index de9e80afc8..6535d457e7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1592,9 +1592,10 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj if (!nozzle_option->extruder_nozzle_stats.count(extruder_id)) { nozzle_count = 0; - // Reset the concrete volume types (Standard/High Flow); Hybrid stays a hidden match-any - // sentinel and is left alone. TPU High Flow is a concrete variant shipped on 0.4/0.6 - // nozzles, so reset it too, but only there (mirrors the High-Flow-skip-for-0.2 guard). + // Reset the concrete volume types (Standard/High Flow); Hybrid is not a physical + // nozzle type and never appears in the stats. TPU High Flow is a concrete variant + // shipped on 0.4/0.6 nozzles, so reset it too, but only there (mirrors the + // High-Flow-skip-for-0.2 guard). std::vector reset_types{nvtStandard, nvtHighFlow}; if (extruder_supports_tpu_high_flow(preset_bundle, extruder_id)) reset_types.push_back(nvtTPUHighFlow); @@ -1816,27 +1817,11 @@ bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_man std::optional select_type; if (nozzle_option && nozzle_option->extruder_nozzle_stats.count(index)) { const auto &stats = nozzle_option->extruder_nozzle_stats[index]; - if (stats.size() > 1) { - // Orca: for a mixed extruder, keep Hybrid out of the (still-deferred) nozzle-centric slicing - // pipeline, so collapse to the dominant concrete volume type (tie -> Standard). - // TPU High Flow is shipped on 0.4/0.6 nozzles, so only fold it in there (mirrors the - // High-Flow-skip-for-0.2 guard). Uses the device-reported diameter, same predicate as the preset path. - int std_cnt = 0, hf_cnt = 0, tpu_hf_cnt = 0; - for (const auto &kv : stats) { - if (kv.first == nvtStandard) std_cnt = kv.second; - else if (kv.first == nvtHighFlow) hf_cnt = kv.second; - else if (kv.first == nvtTPUHighFlow) tpu_hf_cnt = kv.second; - } - if (!nozzle_diameter_supports_tpu_high_flow(nozzle_diameters[extruder_id])) - tpu_hf_cnt = 0; - select_type = nvtStandard; - if (hf_cnt > std_cnt && hf_cnt >= tpu_hf_cnt) - select_type = nvtHighFlow; - else if (tpu_hf_cnt > std_cnt && tpu_hf_cnt > hf_cnt) - select_type = nvtTPUHighFlow; - } else { + if (stats.size() > 1) + // The extruder holds nozzles of several flow types: select the mixed-flow mode. + select_type = NozzleVolumeType::nvtHybrid; + else select_type = stats.begin()->first; - } } if (obj->is_nozzle_flow_type_supported()) { if (obj->GetExtderSystem()->GetNozzleFlowType(index) == NozzleFlowType::NONE_FLOWTYPE) { @@ -3292,8 +3277,12 @@ void Sidebar::update_presets(Preset::Type preset_type) 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) { - if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) /*|| - extruder_max_nozzle_count->values[index] > 1 && nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid*/) { // TODO: Orca: Support hybrid + // get_at falls back to the first entry when a profile defines no per-extruder value, + // so extruders without an explicit sub-nozzle count never offer Hybrid. A nullable-int + // nil is INT_MAX (> 1) and would otherwise falsely pass the gate, so exclude it too. + 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" || is_skip_high_flow_printer(printer_model))) continue; diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index ec4f3a7cab..ff169a86ae 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -1401,7 +1401,9 @@ bool SelectMachineDialog::is_nozzle_type_match(DevExtderSystem data, wxString& e //check nozzle used auto used_filaments = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_used_filaments(); // 1 based auto filament_maps = wxGetApp().plater()->get_partplate_list().get_curr_plate()->get_real_filament_maps(project_config); // 1 based - std::map used_extruders_flow; + // flow type and diameter of every nozzle the sliced plate needs, keyed by logical extruder; + // a hybrid extruder contributes one entry per used sub-nozzle flow + std::multimap> used_extruders_flow; std::vector used_extruders; // 0 based for (auto f : used_filaments) { int filament_extruder = filament_maps[f - 1] - 1; @@ -1411,60 +1413,78 @@ bool SelectMachineDialog::is_nozzle_type_match(DevExtderSystem data, wxString& e std::sort(used_extruders.begin(), used_extruders.end()); auto nozzle_volume_type_opt = dynamic_cast(wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")); + auto nozzle_diameter_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter"); for (auto i = 0; i < used_extruders.size(); i++) { if (nozzle_volume_type_opt) { NozzleVolumeType nozzle_volume_type = (NozzleVolumeType) (nozzle_volume_type_opt->get_at(used_extruders[i])); - if (nozzle_volume_type == NozzleVolumeType::nvtStandard) { used_extruders_flow[used_extruders[i]] = "Standard";} - else if (nozzle_volume_type == NozzleVolumeType::nvtTPUHighFlow) { used_extruders_flow[used_extruders[i]] = "TPU High Flow";} - else {used_extruders_flow[used_extruders[i]] = "High Flow";} + float preset_diameter = nozzle_diameter_opt ? (float) nozzle_diameter_opt->get_at(used_extruders[i]) : -1.0f; + if (nozzle_volume_type == NozzleVolumeType::nvtHybrid) { + // A hybrid extruder prints with a mix of nozzle flows: collect the flow of each + // physically used nozzle instead of forcing a single one. + auto nozzle_group_res = DevUtilBackend::GetNozzleGroupResult(m_plater); + if (nozzle_group_res) { + for (const auto &nozzle_info : nozzle_group_res->get_used_nozzles_in_extruder(used_extruders[i])) { + float diameter = preset_diameter; + try { diameter = std::stof(nozzle_info.diameter); } catch (const std::exception &) {} + used_extruders_flow.insert({used_extruders[i], {DevNozzle::ToNozzleFlowType(nozzle_info.volume_type), diameter}}); + } + } else { + // A by-object plate with several objects produces no plate-level nozzle grouping, + // so the used flows are unknown; skip the check for this extruder rather than + // blocking the print. + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": no nozzle group result, flow check skipped for extruder " << used_extruders[i]; + } + } else { + used_extruders_flow.insert({used_extruders[i], {DevNozzle::ToNozzleFlowType(nozzle_volume_type), preset_diameter}}); + } } } vector map_extruders = {1, 0}; - - // The default two extruders are left, right, but the order of the extruders on the machine is right, left. - std::vector flow_type_of_machine; - for (const auto& it : data.GetExtruders()) - { - if (it.GetNozzleFlowType() == NozzleFlowType::H_FLOW) - { - flow_type_of_machine.push_back(L("High Flow")); - } - else if (it.GetNozzleFlowType() == NozzleFlowType::S_FLOW) - { - flow_type_of_machine.push_back(L("Standard")); - } - else if (it.GetNozzleFlowType() == NozzleFlowType::U_FLOW) - { - flow_type_of_machine.push_back(L("TPU High Flow")); - } - } + const DevNozzleSystem* nozzle_sys = data.Owner() ? data.Owner()->GetNozzleSystem() : nullptr; + const bool has_nozzle_rack = nozzle_sys && nozzle_sys->GetNozzleRack() && nozzle_sys->GetNozzleRack()->IsSupported(); //Only when all preset nozzle types and machine nozzle types are exactly the same, return true. - for (std::map::iterator it = used_extruders_flow.begin(); it!= used_extruders_flow.end(); it++) { + for (auto it = used_extruders_flow.begin(); it != used_extruders_flow.end(); it++) { + // The default two extruders are left, right, but the order of the extruders on the machine is right, left. int target_machine_nozzle_id = map_extruders[it->first]; + NozzleFlowType used_flow = it->second.first; - if (target_machine_nozzle_id < flow_type_of_machine.size()) { - if (flow_type_of_machine[target_machine_nozzle_id] != used_extruders_flow[it->first]) { - - wxString pos; - if (target_machine_nozzle_id == DEPUTY_EXTRUDER_ID) - { - pos = _L("left nozzle"); - } - else if(target_machine_nozzle_id == MAIN_EXTRUDER_ID) - { - pos = _L("right nozzle"); - } - - error_message = wxString::Format(_L("The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). " - "Please make sure the nozzle installed matches with settings in printer, " - "then set the corresponding printer preset while slicing."), pos, - _L(flow_type_of_machine[target_machine_nozzle_id]), - _L(used_extruders_flow[it->first])); + // A nozzle-rack extruder swaps nozzles during the print, so the mounted nozzle is irrelevant: + // the extruder inventory (mounted nozzle + rack) must hold a nozzle of every needed flow instead. + if (has_nozzle_rack && target_machine_nozzle_id == MAIN_EXTRUDER_ID) { + if (nozzle_sys->CollectNozzles(target_machine_nozzle_id, used_flow, it->second.second).empty()) { + error_message = wxString::Format(_L("The printer has no nozzle matching the slicing file (%s). " + "Please install a matching nozzle on the printer, " + "or set the corresponding printer preset while slicing."), + DevNozzle::GetNozzleFlowTypeStr(used_flow)); return false; } + continue; + } + + NozzleFlowType machine_flow = data.GetNozzleFlowType(target_machine_nozzle_id); + if (machine_flow == NozzleFlowType::NONE_FLOWTYPE) + continue; // the mounted nozzle is unknown, nothing to compare against + + if (machine_flow != used_flow) { + wxString pos; + if (target_machine_nozzle_id == DEPUTY_EXTRUDER_ID) + { + pos = _L("left nozzle"); + } + else if(target_machine_nozzle_id == MAIN_EXTRUDER_ID) + { + pos = _L("right nozzle"); + } + + error_message = wxString::Format(_L("The nozzle flow setting of %s(%s) doesn't match with the slicing file(%s). " + "Please make sure the nozzle installed matches with settings in printer, " + "then set the corresponding printer preset while slicing."), pos, + DevNozzle::GetNozzleFlowTypeStr(machine_flow), + DevNozzle::GetNozzleFlowTypeStr(used_flow)); + return false; } } return true; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 595fc3182e..a6ff7fba97 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -616,26 +616,25 @@ void Tab::parse_extruder_selection(int selection, int &extruder_id, NozzleVolume for (int i = 0; i < extruder_nums; ++i) { NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); - // TODO: Orca: Support hybrid - //if (volume_type == NozzleVolumeType::nvtHybrid) { - // if (selection == current_index) { - // extruder_id = i; - // nozzle_type = NozzleVolumeType::nvtStandard; - // return; - // } else if (selection == current_index + 1) { - // extruder_id = i; - // nozzle_type = NozzleVolumeType::nvtHighFlow; - // return; - // } - // current_index += 2; - //} else { + if (volume_type == NozzleVolumeType::nvtHybrid) { + if (selection == current_index) { + extruder_id = i; + nozzle_type = NozzleVolumeType::nvtStandard; + return; + } else if (selection == current_index + 1) { + extruder_id = i; + nozzle_type = NozzleVolumeType::nvtHighFlow; + return; + } + current_index += 2; + } else { if (selection == current_index) { extruder_id = i; nozzle_type = volume_type; return; } current_index += 1; - //} + } } extruder_id = 0; @@ -651,17 +650,16 @@ int Tab::calculate_selection_index_for_extruder(int extruder_id, NozzleVolumeTyp for (int i = 0; i < extruder_nums; ++i) { if (i == extruder_id) { - // TODO: Orca: Support hybrid NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); - /*if (volume_type == NozzleVolumeType::nvtHybrid) { + if (volume_type == NozzleVolumeType::nvtHybrid) { return nozzle_type == NozzleVolumeType::nvtHighFlow ? index + 1 : index; - } else*/ { + } else { return index; } } NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); - index += /*(volume_type == NozzleVolumeType::nvtHybrid) ? 2 :*/ 1; + index += (volume_type == NozzleVolumeType::nvtHybrid) ? 2 : 1; } return 0; @@ -7557,8 +7555,14 @@ void TabPrinter::set_extruder_volume_type(int extruder_id, NozzleVolumeType type //save to app config if (!m_base_preset_name.empty()) { - ConfigOptionEnumsGeneric* nozzle_volume_type_option = m_preset_bundle->project_config.option("nozzle_volume_type"); - std::string nozzle_volume_type_str = nozzle_volume_type_option->serialize(); + // do not save hybrid flow status to config: it depends on the nozzles currently installed + // on the connected printer, so it must not be restored in a later session + ConfigOptionEnumsGeneric nozzle_volume_type_option = *m_preset_bundle->project_config.option("nozzle_volume_type"); + for (size_t i = 0; i < nozzle_volume_type_option.values.size(); i++) { + if (nozzle_volume_type_option.values[i] == (int) nvtHybrid) + nozzle_volume_type_option.values[i] = (int) nvtStandard; + } + std::string nozzle_volume_type_str = nozzle_volume_type_option.serialize(); wxGetApp().app_config->save_nozzle_volume_types_to_config(m_base_preset_name, nozzle_volume_type_str); } @@ -7888,12 +7892,11 @@ std::vector Tab::generate_extruder_options() wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name( pt, ext_id, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true)); NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]); - - // TODO: Orca: Support hybrid - /*if (volume_type == NozzleVolumeType::nvtHybrid) { + + 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"))); - } else*/ { + } else { wxString volume_name = get_nozzle_volume_type_name(volume_type); options.push_back(wxString::Format(_L("%s: %s"), extruder_name, volume_name)); } @@ -7942,36 +7945,35 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id) if (left_nozzle == right_nozzle) { return true; } - - // TODO: Orca: Support hybrid - //if (left_nozzle != NozzleVolumeType::nvtHybrid && right_nozzle != NozzleVolumeType::nvtHybrid) { - // return false; - //} - //if (left_nozzle == NozzleVolumeType::nvtHybrid && right_nozzle == NozzleVolumeType::nvtHybrid) { - // return true; - //} + if (left_nozzle != NozzleVolumeType::nvtHybrid && right_nozzle != NozzleVolumeType::nvtHybrid) { + return false; + } - //// Hybrid rules - //auto current_nozzle = get_actual_nozzle_volume_type(extruder_id); - //if (left_nozzle != NozzleVolumeType::nvtHybrid) { - // if (extruder_id == 0) { - // return true; - // } - // if (extruder_id == 1 && current_nozzle == left_nozzle) { - // return true; - // } - // return false; - //} - //if (right_nozzle != NozzleVolumeType::nvtHybrid) { - // if (extruder_id == 1) { - // return true; - // } - // if (extruder_id == 0 && current_nozzle == right_nozzle) { - // return true; - // } - // return false; - //} + if (left_nozzle == NozzleVolumeType::nvtHybrid && right_nozzle == NozzleVolumeType::nvtHybrid) { + return true; + } + + // Hybrid rules + auto current_nozzle = get_actual_nozzle_volume_type(extruder_id); + if (left_nozzle != NozzleVolumeType::nvtHybrid) { + if (extruder_id == 0) { + return true; + } + if (extruder_id == 1 && current_nozzle == left_nozzle) { + return true; + } + return false; + } + if (right_nozzle != NozzleVolumeType::nvtHybrid) { + if (extruder_id == 1) { + return true; + } + if (extruder_id == 0 && current_nozzle == right_nozzle) { + return true; + } + return false; + } return false; }