feat(slicing): enforce filament flow-type restrictions in auto grouping

Filament grouping already consumed per-filament forbidden nozzle volume
types, but every call site passed an empty map, so a variant-restricted
filament (e.g. one limited to "Direct Drive TPU High Flow") could be
auto-grouped onto an incompatible nozzle flow type on multi-variant
printers.

- add convert_to_nvt_type() to parse extruder variant strings
- add Print::get_filament_unprintable_flow(): forbidden volume types =
  printer extruder variants minus the filament's declared variants;
  filaments declaring no variants stay unrestricted
- feed the map into grouping at the by-object path (Print.cpp) and all
  six mapping/planning sites in reorder_extruders_for_minimum_flush_volume
- unit-test the string parser

Non-restricted configurations produce an empty map, so existing
printers' grouping and g-code are unchanged.
This commit is contained in:
SoftFever
2026-07-10 15:10:46 +08:00
parent 9e8ac03476
commit b2eeed2622
7 changed files with 121 additions and 7 deletions

View File

@@ -1944,6 +1944,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
std::vector<std::set<int>>geometric_unprintables = m_print->get_geometric_unprintable_filaments();
std::vector<std::set<int>>physical_unprintables = m_print->get_physical_unprintable_filaments(used_filaments);
auto filament_unprintable_volumes = m_print->get_filament_unprintable_flow(used_filaments);
filament_maps = m_print->get_filament_maps();
map_mode = m_print->get_filament_map_mode();
@@ -2012,7 +2013,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
// per-layer maps in a selector (4-arg create) result — which sets support_dynamic_nozzle_map and
// lights the GCode per-layer hotend/nozzle placeholders. filament_sequences is produced here, so
// the static reorder below is skipped for this branch.
auto grouping_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, {}, FilamentMapMode::fmmAutoForFlush,
auto grouping_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, filament_unprintable_volumes, FilamentMapMode::fmmAutoForFlush,
m_initial_nozzle_status.get_nozzle_filament_map());
// The time estimator's per-extruder print times are global and do not apply to per-range
// grouping.
@@ -2020,7 +2021,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
m_nozzle_status = m_initial_nozzle_status;
auto dynamic_plan_res = plan_filament_mapping_and_order_by_combo_ranges(m_print, grouping_context, get_custom_seq, FilamentMapMode::fmmAutoForFlush,
physical_unprintables, geometric_unprintables, {}, &m_nozzle_status);
physical_unprintables, geometric_unprintables, filament_unprintable_volumes, &m_nozzle_status);
std::vector<std::vector<int>> nozzle_map_per_layer;
for (auto& res : dynamic_plan_res) {
@@ -2042,7 +2043,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
// only check and map in sequence mode, in by object mode, we check the map in print.cpp
else if (print_config->print_sequence != PrintSequence::ByObject || m_print->objects().size() == 1) {
grouping_result = ToolOrdering::get_recommended_filament_maps(layer_filaments, m_print, map_mode, physical_unprintables, geometric_unprintables);
grouping_result = ToolOrdering::get_recommended_filament_maps(layer_filaments, m_print, map_mode, physical_unprintables, geometric_unprintables, filament_unprintable_volumes);
std::vector<int> derived_maps = grouping_result.get_extruder_map(false); // 1-based extruder map
if (map_mode < FilamentMapMode::fmmManual) {
@@ -2145,12 +2146,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
// is_dynamic_group_reorder() implies filament_map_mode == fmmAutoForFlush, contradicting
// this map_mode != fmmAutoForFlush guard, so this sub-branch is unreachable under the
// current predicate. It is provably inert.
auto best_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, {}, FilamentMapMode::fmmAutoForFlush,
auto best_context = build_filament_group_context(m_print, layer_filaments, physical_unprintables, geometric_unprintables, filament_unprintable_volumes, FilamentMapMode::fmmAutoForFlush,
m_initial_nozzle_status.get_nozzle_filament_map());
best_context.speed_info.group_with_time = false;
MultiNozzleUtils::NozzleStatusRecorder best_nozzle_status = m_initial_nozzle_status;
auto best_plan = plan_filament_mapping_and_order_by_combo_ranges(m_print, best_context, get_custom_seq, FilamentMapMode::fmmAutoForFlush,
physical_unprintables, geometric_unprintables, {}, &best_nozzle_status);
physical_unprintables, geometric_unprintables, filament_unprintable_volumes, &best_nozzle_status);
std::vector<std::vector<int>> best_nozzle_map_per_layer;
for (auto& res : best_plan) {
best_sequences.emplace_back(cast<unsigned int>(res.fil_order));
@@ -2164,7 +2165,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
// Best-for-flush grouping (nozzle-aware result). The extruder-level map fed to the flush
// reorder is derived exactly as before, so best_sequences (and the flush weight) are
// identical; only flush_filament_change_count is now charged per physical nozzle.
auto best_group_result = get_recommended_filament_maps(layer_filaments, m_print, fmmAutoForFlush, physical_unprintables, geometric_unprintables);
auto best_group_result = get_recommended_filament_maps(layer_filaments, m_print, fmmAutoForFlush, physical_unprintables, geometric_unprintables, filament_unprintable_volumes);
std::vector<int> best_maps = best_group_result.get_extruder_map();
reorder_filaments_for_minimum_flush_volume(
filament_lists,

View File

@@ -24,6 +24,7 @@
#include <algorithm>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <unordered_set>
#include <sstream>
#include <boost/filesystem/path.hpp>
@@ -2504,13 +2505,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments);
std::vector<int>filament_maps = this->get_filament_maps();
auto map_mode = get_filament_map_mode();
// get recommended filament map
if (map_mode < FilamentMapMode::fmmManual) {
// Grouping returns a nozzle-aware result; the 1-based extruder map
// for the by-object path is derived from it.
auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables);
auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables, filament_unprintable_volumes);
auto derived_maps = grouping_result.get_extruder_map(false);
if (!derived_maps.empty()) {
filament_maps = derived_maps;
@@ -3280,6 +3282,41 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
return physical_unprintables;
}
std::map<int, std::set<NozzleVolumeType>> Print::get_filament_unprintable_flow(const std::vector<unsigned int> &used_filaments) const
{
std::map<int, std::set<NozzleVolumeType>> ret;
std::vector<std::string> extruder_variant_list = m_config.printer_extruder_variant.values;
// A filament that declares no extruder variants carries no flow restriction.
const ConfigOptionStrings *filament_variant_opt = m_ori_full_print_config.option<ConfigOptionStrings>("filament_extruder_variant");
if (filament_variant_opt == nullptr)
return ret;
std::vector<std::string> filament_variant_list = filament_variant_opt->values;
std::vector<int> filament_self_index;
if (!m_ori_full_print_config.has("filament_self_index"))
filament_self_index.resize(filament_variant_list.size(), 1);
else
filament_self_index = m_ori_full_print_config.option<ConfigOptionInts>("filament_self_index")->values;
std::unordered_set<int> used_fils_set(used_filaments.begin(), used_filaments.end());
std::unordered_map<int, std::set<NozzleVolumeType>> filament_variant_map;
for(int i = 0; i < filament_variant_list.size(); ++i){
NozzleVolumeType volume = convert_to_nvt_type(filament_variant_list[i]);
if(volume != nvtHybrid) filament_variant_map[filament_self_index[i]].insert(volume);
}
for (auto iter : filament_variant_map) {
int fil_idx = iter.first - 1;
if (used_fils_set.find(fil_idx) == used_fils_set.end()) continue;
const std::set<NozzleVolumeType> &volumes = iter.second;
for (int exd_idx = 0; exd_idx < extruder_variant_list.size(); ++exd_idx) {
auto exd_volume = convert_to_nvt_type(extruder_variant_list[exd_idx]);
assert(exd_volume != nvtHybrid);
if (volumes.find(exd_volume) == volumes.end() && exd_volume != nvtHybrid) ret[fil_idx].insert(exd_volume);
}
}
return ret;
}
std::vector<double> Print::get_extruder_printable_height() const
{

View File

@@ -1057,6 +1057,18 @@ public:
*/
std::vector<std::set<int>> get_physical_unprintable_filaments(const std::vector<unsigned int>& used_filaments) const;
/**
* @brief Determines the forbidden nozzle volume types for each used filament
*
* A filament may declare the extruder variants it supports. Every volume type offered by the
* printer's extruders that the filament does not support is forbidden for that filament.
* Hybrid volumes are ignored on both sides, and filaments declaring no variants are unrestricted.
*
* @param used_filaments Totally used filaments when slicing
* @return A map from used filament index to the set of nozzle volume types it cannot print on
*/
std::map<int, std::set<NozzleVolumeType>> get_filament_unprintable_flow(const std::vector<unsigned int> &used_filaments) const;
std::vector<double> get_extruder_printable_height() const;
std::vector<Polygons> get_extruder_printable_polygons() const;
std::vector<Polygons> get_extruder_unprintable_polygons() const;

View File

@@ -689,6 +689,35 @@ std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std
return extruder_ams_count_str;
}
// Maps a full extruder variant string such as "Direct Drive High Flow" to its
// NozzleVolumeType: the extruder-type prefix is stripped, the remainder is trimmed
// and looked up as a volume-type name. Returns nvtHybrid when the string cannot
// be parsed.
NozzleVolumeType convert_to_nvt_type(const std::string &variant_str) {
const auto &ext_types = ConfigOptionEnum<ExtruderType>::get_enum_names();
auto trim = [](std::string &s) {
s.erase(s.find_last_not_of(" \t\r\n") + 1);
s.erase(0, s.find_first_not_of(" \t\r\n"));
};
for (auto ext_type : ext_types) {
size_t pos = variant_str.find(ext_type);
if (pos == std::string::npos)
continue;
std::string result = variant_str;
result.erase(pos, ext_type.size());
trim(result);
auto iter = s_keys_map_NozzleVolumeType.find(result);
if (iter != s_keys_map_NozzleVolumeType.end())
return NozzleVolumeType(iter->second);
}
return nvtHybrid;
}
// Parses the extruder_nozzle_stats option: one "|"-separated token list per
// extruder, each token
// "<volume_type_name>#<count>". Empty string => no per-type stats for that extruder.

View File

@@ -568,6 +568,9 @@ extern bool is_filament_extruder_override_key(const std::string &opt_key);
extern std::vector<std::map<int, int>> get_extruder_ams_count(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std::map<int, int>> &extruder_ams_count);
// maps a full extruder variant string (e.g. "Direct Drive High Flow") to its NozzleVolumeType; nvtHybrid if unparsable
extern NozzleVolumeType convert_to_nvt_type(const std::string& variant_str);
// for parse extruder_nozzle_stats (per-extruder physical nozzle inventory by volume type)
extern std::vector<std::map<NozzleVolumeType, int>> get_extruder_nozzle_stats(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_nozzle_stats_to_string(const std::vector<std::map<NozzleVolumeType, int>> &extruder_nozzle_stats);

View File

@@ -21,6 +21,7 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_nozzle_volume_type.cpp
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp

View File

@@ -0,0 +1,31 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
TEST_CASE("convert_to_nvt_type maps extruder variant strings to nozzle volume types", "[Config]")
{
SECTION("Direct Drive variants") {
REQUIRE(convert_to_nvt_type("Direct Drive Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Direct Drive High Flow") == nvtHighFlow);
REQUIRE(convert_to_nvt_type("Direct Drive TPU High Flow") == nvtTPUHighFlow);
}
SECTION("Bowden variants") {
REQUIRE(convert_to_nvt_type("Bowden Standard") == nvtStandard);
REQUIRE(convert_to_nvt_type("Bowden High Flow") == nvtHighFlow);
}
SECTION("Unparsable strings fall back to hybrid") {
REQUIRE(convert_to_nvt_type("Unknown Extruder") == nvtHybrid);
REQUIRE(convert_to_nvt_type("") == nvtHybrid);
REQUIRE(convert_to_nvt_type("High Flow") == nvtHybrid);
REQUIRE(convert_to_nvt_type("Direct Drive") == nvtHybrid);
}
SECTION("Whitespace around the volume-type remainder is trimmed") {
REQUIRE(convert_to_nvt_type("Direct Drive High Flow ") == nvtHighFlow);
REQUIRE(convert_to_nvt_type(" Bowden Standard") == nvtStandard);
}
}