feat(print): add per-filament config-index resolvers and filament_map_2

- Print::get_nozzle_config_index / get_filament_config_indx resolve a
  filament's variant slot per layer from the nozzle group result, with
  hashed index caches; when no group result is published (sequential
  prints), they fall back to the static filament->extruder mapping so
  behavior is unchanged
- filament_map_2 caches each filament's resolved print-variant slot;
  rebuilt in Print::apply after the filament_map diff handling and in
  the filament-map write-back
- filament retract overrides now key by slot indices: apply_override
  fallback indexing flips to 0-based, Print::apply passes
  filament_map/extruder indices, the write-back passes filament_map_2
  (identical resolution while slots equal extruders)
- filament_volume_map/filament_nozzle_map/filament_map_2/
  filament_self_index become PrintConfig static members (required for
  member access); grouping input guards tightened so their registered
  1-element defaults are never mistaken for real per-filament maps
  (single-filament manual mode keeps the mix-marker fallback)
- update_filament_self_index_cache refreshed at every full-config
  assignment
- tests: 0-based apply_override fallback, get_config_index_base
  hit/miss/mixed-type cases

The resolvers are not consumed by the g-code writer yet. Non-Hybrid
g-code is unchanged except the config header, which now serializes the
three new static keys (defaults until the per-filament producer lands);
verified by the 19-fixture byte gate: 3 added header lines per fixture,
zero motion changes.
This commit is contained in:
SoftFever
2026-07-11 02:29:40 +08:00
parent 13cd8d4864
commit 18b9279c26
8 changed files with 334 additions and 37 deletions

View File

@@ -1167,8 +1167,8 @@ static std::vector<MultiNozzleUtils::NozzleInfo> build_default_nozzle_list(const
// Best-effort readers for the multi-nozzle dev config keys. These are registered in the ConfigDef
// but not (yet) static PrintConfig members, so the slicing PrintConfig reads them as inert defaults,
// which keeps the auto grouping path bit-exact (all these degrade to the flush-only, non-switcher,
// hybrid-flow case).
// which keeps the auto grouping path bit-exact (all these degrade to the flush-only, non-switcher
// case).
static bool cfg_bool(const ConfigBase& c, const char* key, bool def)
{
if (auto* o = c.option<ConfigOptionBool>(key)) return o->value;
@@ -1179,11 +1179,6 @@ static double cfg_float(const ConfigBase& c, const char* key, double def)
if (auto* o = c.option<ConfigOptionFloat>(key)) return o->value;
return def;
}
static std::vector<int> cfg_ints(const ConfigBase& c, const char* key)
{
if (auto* o = c.option<ConfigOptionInts>(key)) return o->values;
return {};
}
// Prepare per-extruder flush matrices. The prime_volume_mode==pvmFast branch: Default reads
// flush_multiplier, Fast reads flush_multiplier_fast.
@@ -1336,19 +1331,17 @@ static FilamentGroupContext build_filament_group_context(
context.group_info.ignore_ext_filament = ignore_ext_filament;
context.group_info.has_filament_switcher = has_filament_switcher;
// hybrid flow means no special per-filament nozzle-volume request. In manual mode we honour the
// config's per-filament volume map when it is present and correctly sized; otherwise (the key is
// not a static PrintConfig member) fall back to hybrid so rebuild_nozzle_unprintables
// never indexes out of range.
if (mode == FilamentMapMode::fmmManual) {
auto fvm = cfg_ints(print_config, "filament_volume_map");
if (fvm.size() == filament_nums)
context.group_info.filament_volume_map = std::move(fvm);
else
context.group_info.filament_volume_map = std::vector<int>(filament_nums, (int)(NozzleVolumeType::nvtHybrid));
} else {
// hybrid flow means no special per-filament nozzle-volume request.
// Orca: in manual mode the config's per-filament volume map is honoured only when a producer
// sized it to a multi-filament count (the trust guard used by
// update_values_to_printer_extruders_for_multiple_filaments): the registered default is a
// 1-element vector ({Standard}) that a single-filament count cannot tell apart from a real
// map, and it must not displace the hybrid fallback rebuild_nozzle_unprintables relies on.
if (mode == FilamentMapMode::fmmManual && filament_nums > 1 &&
print_config.filament_volume_map.values.size() == filament_nums)
context.group_info.filament_volume_map = print_config.filament_volume_map.values;
else
context.group_info.filament_volume_map = std::vector<int>(filament_nums, (int)(NozzleVolumeType::nvtHybrid));
}
context.nozzle_info.nozzle_list = build_nozzle_list(nozzle_groups);
context.nozzle_info.extruder_nozzle_list = build_extruder_nozzle_list(context.nozzle_info.nozzle_list);
@@ -1493,7 +1486,17 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::get_recommended_filamen
auto manual_filament_map = print_config.filament_map.values;
std::transform(manual_filament_map.begin(), manual_filament_map.end(), manual_filament_map.begin(), [](int v) { return v - 1; });
float diameter = print_config.nozzle_diameter.values.empty() ? 0.4f : (float)print_config.nozzle_diameter.values.front();
auto nozzle_result = LayeredNozzleGroupResult::create(used_filaments, manual_filament_map, cfg_ints(print_config, "filament_volume_map"), cfg_ints(print_config, "filament_nozzle_map"), get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter);
// Orca: create() indexes the volume/nozzle maps per used filament with no bounds check, so
// pass them only when a producer sized them to a multi-filament count — the registered
// 1-element defaults are indistinguishable by size from a real single-filament map.
// Without valid maps the fully-manual request cannot be honoured; return the empty result,
// the same failure an unsatisfiable create() yields.
std::optional<LayeredNozzleGroupResult> nozzle_result;
if (filament_nums > 1 && print_config.filament_volume_map.values.size() == filament_nums &&
print_config.filament_nozzle_map.values.size() == filament_nums)
nozzle_result = LayeredNozzleGroupResult::create(used_filaments, manual_filament_map, print_config.filament_volume_map.values, print_config.filament_nozzle_map.values, get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter);
if (!nozzle_result)
BOOST_LOG_TRIVIAL(error) << "Failed to build nozzle group result from filament nozzle map!";
return nozzle_result ? *nozzle_result : LayeredNozzleGroupResult();
}
@@ -1611,11 +1614,18 @@ static MultiNozzleUtils::LayeredNozzleGroupResult build_group_result_from_map(
{
using namespace MultiNozzleUtils;
const size_t extruder_nums = print_config.nozzle_diameter.values.size();
const size_t filament_nums = print_config.filament_colour.values.size();
const bool has_multiple_nozzle = std::any_of(print_config.extruder_max_nozzle_count.values.begin(), print_config.extruder_max_nozzle_count.values.end(),
[](int v) { return v > 1; });
if (has_multiple_nozzle) {
// Orca: same trust guard as the manual grouping paths — create() indexes the volume/nozzle
// maps per used filament with no bounds check, and the registered 1-element defaults must not
// be mistaken for a real single-filament map. Unsized maps fall through to the extruder-level
// wrap below.
if (has_multiple_nozzle && filament_nums > 1 &&
print_config.filament_volume_map.values.size() == filament_nums &&
print_config.filament_nozzle_map.values.size() == filament_nums) {
float diameter = print_config.nozzle_diameter.values.empty() ? 0.4f : static_cast<float>(print_config.nozzle_diameter.values.front());
if (auto g = LayeredNozzleGroupResult::create(used_filaments, filament_map_0based, cfg_ints(print_config, "filament_volume_map"), cfg_ints(print_config, "filament_nozzle_map"), get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter))
if (auto g = LayeredNozzleGroupResult::create(used_filaments, filament_map_0based, print_config.filament_volume_map.values, print_config.filament_nozzle_map.values, get_extruder_nozzle_stats(print_config.extruder_nozzle_stats.values), diameter))
return *g;
}
auto nozzle_list = build_default_nozzle_list(print_config, extruder_nums);