From abbd420f2ace49ac1873ee69605bcae27065a58a Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sun, 12 Jul 2026 11:15:58 +0800 Subject: [PATCH] feat(engine): write per-variant filament values back for selector regroups When the per-layer filament selector (enable_filament_dynamic_map) migrates a filament across nozzle variants (e.g. Standard -> High Flow), the config write-back only stored the derived extruder map; every per-variant filament value (retraction, nozzle temperature, flow, flush...) kept the numbers resolved from the pre-slice static mapping. Now both dynamic write-back sites (the by-layer branch and the sequential stitch) branch on the result's dynamic support. Migrating results run a mixed-filament expansion that regathers every filament_options_with_variant key from the pristine per-variant superset, giving a migrating filament one config slot per (extruder type x nozzle volume type) it lands on - filament_self_index, filament_extruder_variant, and all value arrays grow in lockstep - and recompute the retract overrides with per-slot machine indices so a nil slot falls back to its own variant's machine value. Non-migrating dynamic results take the merged three-map write-back so re-applies reproduce from the written maps. Unrouted filaments resolve from the result's own default map, so slot resolution never depends on filament_map round-tripping through the plate config. Print::apply reproduces the identical expansion from the persisted group result (shared dedupe helper, expansion function, and slot indices on both sides): the expanded keys sit in the psWipeTower / psGCodeExport invalidate lists, so without the reproduction every re-apply after a selector slice would diff non-empty and permanently invalidate. cal_non_support_filaments now resolves the extruder per layer from the published result for dynamic groupings. filament_map_2 keeps its apply-time static derivation; nothing on the dynamic path reads it (the per-slot machine indices key the override merge), and per-(extruder x volume-type) machine limits in the g-code processor remain a documented follow-up. Every change is gated behind is_dynamic_group_reorder() or a persisted result with dynamic support; no profile sets the flag, so the static fleet's instruction stream is unchanged (20/20 pinned-slice byte gate identical, incl. the sequential repro sliced twice, deterministic). Tests: expansion unit coverage (migrating slots, unrouted fallback via the default map, mis-sized volume map ignored, nullable retract keys in lockstep, slot machine index layout), an end-to-end stub-driven write-back asserting expanded slots, per-layer config-index resolution, the override merge incl. the nil-slot variant fallback, and re-apply stability, plus a real selector slice staying valid across re-apply. Suites green (libslic3r 48987/168, fff_print 633/60). --- src/libslic3r/GCode/ToolOrdering.cpp | 35 +-- src/libslic3r/Print.cpp | 134 +++++++++++- src/libslic3r/Print.hpp | 16 ++ src/libslic3r/PrintApply.cpp | 38 +++- src/libslic3r/PrintConfig.cpp | 139 ++++++++++++ src/libslic3r/PrintConfig.hpp | 29 +++ .../test_toolordering_nozzle_group.cpp | 204 +++++++++++++++++- 7 files changed, 569 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 0faa7b29b9..f37025d4e7 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1063,12 +1063,24 @@ bool ToolOrdering::cal_non_support_filaments(const PrintConfig &config, int find_count = 0; int find_first_filaments_count = 0; bool has_non_support = has_non_support_filament(config); - for (const LayerTools &layer_tool : m_layer_tools) { - for (const unsigned int &filament : layer_tool.extruders) { + // The selector can move a filament between extruders per layer; resolve the extruder from + // the published result then, so the first filament attributed to an extruder is one it + // actually prints there. Static results keep the cross-layer filament_map arithmetic. + const bool use_dynamic_map = m_nozzle_group_result.is_support_dynamic_nozzle_map() && m_nozzle_group_result.get_layer_count() > 0; + auto extruder_for_filament = [&](unsigned int filament, size_t layer_idx) -> int { + if (use_dynamic_map) + return m_nozzle_group_result.get_extruder_id(static_cast(filament), static_cast(layer_idx)); + return config.filament_map.values[filament] - 1; + }; + for (size_t layer_idx = 0; layer_idx < m_layer_tools.size(); ++layer_idx) { + for (const unsigned int &filament : m_layer_tools[layer_idx].extruders) { //check first filament - if (!config.filament_map.values.empty() && initial_filaments[config.filament_map.values[filament] - 1] == -1) { - initial_filaments[config.filament_map.values[filament] - 1] = filament; - find_first_filaments_count++; + if (!config.filament_map.values.empty()) { + const int extruder_id = extruder_for_filament(filament, layer_idx); + if (extruder_id >= 0 && extruder_id < static_cast(initial_filaments.size()) && initial_filaments[extruder_id] == -1) { + initial_filaments[extruder_id] = filament; + find_first_filaments_count++; + } } if (has_non_support) { @@ -1083,8 +1095,9 @@ bool ToolOrdering::cal_non_support_filaments(const PrintConfig &config, if (config.filament_map.values.empty()) return true; - if (initial_non_support_filaments[config.filament_map.values[filament] - 1] == -1) { - initial_non_support_filaments[config.filament_map.values[filament] - 1] = filament; + const int extruder_id = extruder_for_filament(filament, layer_idx); + if (extruder_id >= 0 && extruder_id < static_cast(initial_non_support_filaments.size()) && initial_non_support_filaments[extruder_id] == -1) { + initial_non_support_filaments[extruder_id] = filament; find_count++; } @@ -2080,17 +2093,15 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first auto result = MultiNozzleUtils::LayeredNozzleGroupResult::create(nozzle_map_per_layer, grouping_context.nozzle_info.nozzle_list, used_filaments, filament_sequences); grouping_result = result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult(); - // Derive the extruder-level map for the stats path + config write-back. - // Orca: the dynamic (per-layer) result carries no single volume/nozzle map, so only the - // extruder map is written back here; the full per-nozzle config write-back - // (a dedicated dynamic-map path) is a follow-up behind the dev flag. + // Derive the extruder-level map for the stats path; the write-back resolves the + // per-variant slots from the full grouping result itself. std::vector derived_maps = grouping_result.get_extruder_map(false); // 1-based if (!derived_maps.empty()) { filament_maps = derived_maps; // A sequential per-object plan must not write its own map: the objects' plans are // stitched print-wide afterwards and written back once from there. if (not_sequential) - m_print->update_filament_maps_to_config(filament_maps); + m_print->update_to_config_by_nozzle_group_result(grouping_result); } std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; }); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 7373623d7c..6820ea8804 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2603,12 +2603,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) stitched_layer_filaments, used_filaments, physical_unprintables, geometric_unprintables, filament_unprintable_volumes); this->set_nozzle_group_result(std::make_shared(stitched)); - // Orca: the dynamic (per-layer) result carries no single volume/nozzle map, so only - // the extruder map is written back (matching the by-layer selector branch); the full - // per-nozzle config write-back is a follow-up behind the dev flag. - std::vector derived_maps = stitched.get_extruder_map(false); // 1-based - if (!derived_maps.empty()) - update_filament_maps_to_config(derived_maps); + update_to_config_by_nozzle_group_result(stitched); } } else { @@ -3370,6 +3365,133 @@ void Print::update_filament_maps_to_config(std::vector f_maps, std::vector< m_has_auto_filament_map_result = true; } +bool Print::collect_filament_variant_uses(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result, + const DynamicPrintConfig& config, + std::unordered_map>& uses) const +{ + auto opt_filament_type = config.option("filament_type"); + auto opt_extruder_type = dynamic_cast(config.option("extruder_type")); + if (!opt_filament_type || !opt_extruder_type) + return false; + + const size_t filament_count = opt_filament_type->values.size(); + const size_t extruder_count = opt_extruder_type->values.size(); + auto add_use = [&](std::set &variant_set, const MultiNozzleUtils::NozzleInfo &nozzle) { + // Orca: a persisted result can outlive a printer swap; never index the extruder + // arrays with a stale nozzle record. + if (nozzle.extruder_id < 0 || static_cast(nozzle.extruder_id) >= extruder_count) + return; + FilamentVariantUse use; + use.extruder_type = static_cast(opt_extruder_type->get_at(nozzle.extruder_id)); + use.nozzle_volume_type = nozzle.volume_type; + use.extruder_id = nozzle.extruder_id; + variant_set.insert(use); + }; + for (size_t f_index = 0; f_index < filament_count; ++f_index) { + std::set variant_set; + for (const MultiNozzleUtils::NozzleInfo &nozzle : group_result.get_nozzles_for_filament(static_cast(f_index))) + add_use(variant_set, nozzle); + // A filament the plan never routes (not printed) still needs a deterministic slot: take + // its default-map assignment from the result itself, so both the slice-time write-back + // and the apply-time reproduction resolve the same slot even when the surrounding + // filament_map has not round-tripped through the plate config in between. + if (variant_set.empty()) { + if (auto default_nozzle = group_result.get_nozzle_for_filament(static_cast(f_index), -1); default_nozzle.has_value()) + add_use(variant_set, *default_nozzle); + } + // Filaments still without a variant stay absent from the map: the slot rebuild then + // resolves them from their static filament_map / filament_volume_map assignment. + if (!variant_set.empty()) + uses[static_cast(f_index)] = std::vector(variant_set.begin(), variant_set.end()); + } + return true; +} + +void Print::update_to_config_by_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result) +{ + std::vector derived_maps = group_result.get_extruder_map(false); // 1-based + if (derived_maps.empty()) + return; + + if (!group_result.is_support_dynamic_nozzle_map()) { + // No filament actually migrated between nozzles, so the plan reduces to a single + // grouping: write all maps like the static paths do, and the next apply re-derives + // identical slots from the written maps. + std::vector base_filament_map = m_config.filament_map.values; + if (base_filament_map.size() != derived_maps.size()) + base_filament_map.assign(derived_maps.size(), 1); + std::vector base_volume_map = m_config.filament_volume_map.values; + if (base_volume_map.size() != derived_maps.size()) + base_volume_map.assign(derived_maps.size(), (int)nvtStandard); + const std::vector used_filaments = group_result.get_used_filaments(); + update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments), + FilamentGroupUtils::update_used_filament_values(base_volume_map, group_result.get_volume_map(), used_filaments), + group_result.get_nozzle_map()); + return; + } + + // Orca: keep the coarse per-filament extruder map published even though the per-layer truth + // lives in the grouping result: the pre-export consumers, the plate read-back after slicing + // and the preview panel all key on filament_map. The write is direct — the full map + // write-back's single-slot rebuild would undo the per-variant expansion below. + m_ori_full_print_config.option("filament_map", true)->values = derived_maps; + m_config.filament_map.values = derived_maps; + + std::unordered_map> filament_variant_uses; + if (!collect_filament_variant_uses(group_result, m_ori_full_print_config, filament_variant_uses)) { + // Degenerate config (no filament/extruder typing): fall back to the single-slot + // write-back so the maps and overrides stay coherent. + update_filament_maps_to_config(derived_maps); + return; + } + + int extruder_count = 1, extruder_volume_type_count = 1; + m_ori_full_print_config.support_different_extruders(extruder_count); + std::vector> nozzle_volume_types; + extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); + + // Note: filament_map_2 keeps its apply-time (static) derivation here; the per-slot machine + // indices below key the override merge instead, so nothing on this path reads it. Its other + // consumers are the three-map write-back (which recomputes it) and the diagnostic copy in + // the g-code header; per-(extruder x volume-type) machine limits in the g-code processor + // remain a follow-up (see GCodeProcessor::get_axis_max_feedrate). + m_full_print_config = m_ori_full_print_config; + std::set filament_keys = filament_options_with_variant; + filament_keys.insert("filament_self_index"); + std::vector slot_machine_indices; + m_full_print_config.update_filament_config_values_for_multiple_extruders(m_full_print_config, filament_variant_uses, + extruder_count, extruder_volume_type_count, + filament_keys, "filament_self_index", "filament_extruder_variant", + &slot_machine_indices); + + const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); + const std::string filament_prefix = "filament_"; + t_config_option_keys print_diff; + DynamicPrintConfig filament_overrides; + for (auto& opt_key: extruder_retract_keys) + { + const ConfigOption *opt_new_filament = m_full_print_config.option(filament_prefix + opt_key); + const ConfigOption *opt_new_machine = m_full_print_config.option(opt_key); + const ConfigOption *opt_old_machine = m_config.option(opt_key); + + if (opt_new_filament) + compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, slot_machine_indices); + } + + { + 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); + } + if (!print_diff.empty()) { + m_placeholder_parser.apply_config(filament_overrides); + m_config.apply(filament_overrides); + } + + update_filament_self_index_cache(); + m_has_auto_filament_map_result = true; +} + void Print::apply_config_for_render(const DynamicConfig &config) { m_config.apply(config); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 00460dbcd8..bd5c25f478 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1006,6 +1006,13 @@ public: const ToolOrdering& tool_ordering() const { return m_tool_ordering; } void update_filament_maps_to_config(std::vector f_maps, std::vector f_volume_maps = std::vector{}, std::vector f_nozzle_maps = std::vector{}); + // Write-back for a selector (per-layer planned) grouping result. When a filament actually + // migrates between nozzle variants, rebuilds the per-slot filament arrays so it holds one + // slot per variant and recomputes the extruder retract overrides against the expanded + // slots — update_filament_maps_to_config's single-slot rebuild cannot represent a + // migration. A result without migration reduces to a single grouping and takes the + // three-map write-back like the static paths. + void update_to_config_by_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result); void apply_config_for_render(const DynamicConfig &config); // 1 based group ids @@ -1242,6 +1249,15 @@ private: void _make_wipe_tower(); void finalize_first_layer_convex_hull(); void update_filament_self_index_cache(); + // Deduplicates, per filament, the (extruder type x volume type) variants the grouping + // result routes it through; filaments the plan never routes get their default-map + // assignment so the slot resolution never depends on the (mutable) filament_map. config + // must carry extruder_type; returns false when it does not. Both the slice-time write-back + // and the apply-time reproduction call this with m_ori_full_print_config so the two + // expansions resolve identical slots. + bool collect_filament_variant_uses(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result, + const DynamicPrintConfig& config, + std::unordered_map>& uses) const; // Islands of objects and their supports extruded at the 1st layer. Polygons first_layer_islands() const; diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index aeed4a7769..0956cfe154 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -224,7 +224,11 @@ static t_config_option_keys print_config_diffs( const DynamicPrintConfig &new_full_config, DynamicPrintConfig &filament_overrides, int plate_index, - std::vector& filament_maps) + std::vector& filament_maps, + // Per-slot machine indices when the filament arrays hold the per-variant expansion of a + // selector result (one slot per variant a filament migrates through); the per-filament + // map cannot index the expanded override arrays. Null on the single-slot path. + const std::vector* dynamic_override_indices = nullptr) { const std::vector &extruder_retract_keys = print_config_def.extruder_retract_keys(); const std::string filament_prefix = "filament_"; @@ -240,9 +244,14 @@ static t_config_option_keys print_config_diffs( const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr; if (opt_new_filament != nullptr) { - std::vector filament_map_indices(filament_maps.size(), 0); - for (int i = 0; i < filament_maps.size(); i++) - filament_map_indices[i] = filament_maps[i] - 1; + std::vector filament_map_indices; + if (dynamic_override_indices) + filament_map_indices = *dynamic_override_indices; + else { + filament_map_indices.assign(filament_maps.size(), 0); + for (int i = 0; i < filament_maps.size(); i++) + filament_map_indices[i] = filament_maps[i] - 1; + } compute_filament_override_value(opt_key, opt_old, opt_new, opt_new_filament, new_full_config, print_diff, filament_overrides, filament_map_indices); } else if (*opt_new != *opt_old) { //BBS: add plate_index logic for wipe_tower_x/wipe_tower_y @@ -1171,6 +1180,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ std::vector> nozzle_volume_types; int extruder_count = 1, extruder_volume_type_count = 1; bool different_extruder = false; + // Filled only when the filament arrays are rebuilt from a persisted selector result below; + // print_config_diffs then keys the retract overrides per expanded slot. + std::vector dynamic_slot_indices; different_extruder = new_full_config.support_different_extruders(extruder_count); extruder_volume_type_count = new_full_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); @@ -1189,7 +1201,20 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ std::set filament_keys = filament_options_with_variant; filament_keys.insert("filament_self_index"); - if ((extruder_count > 1) || different_extruder) + // A persisted selector result with an actual migration means the last slice rebuilt the + // per-slot filament arrays from it (one slot per variant a filament prints through). + // Reproduce that exact expansion here so an unchanged config diffs empty — the expanded + // keys invalidate the wipe tower / g-code export, and the placeholder parser aliases + // the full config — instead of trimming back to one slot per filament. + auto group_result = std::dynamic_pointer_cast(this->get_nozzle_group_result()); + std::unordered_map> filament_variant_uses; + if (group_result && group_result->is_support_dynamic_nozzle_map() + && collect_filament_variant_uses(*group_result, m_ori_full_print_config, filament_variant_uses)) + new_full_config.update_filament_config_values_for_multiple_extruders(m_ori_full_print_config, filament_variant_uses, + extruder_count, extruder_volume_type_count, filament_keys, + "filament_self_index", "filament_extruder_variant", + &dynamic_slot_indices); + else if ((extruder_count > 1) || different_extruder) 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"); } @@ -1210,7 +1235,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ // Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles. DynamicPrintConfig filament_overrides; //BBS: add plate index - t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps); + t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps, + dynamic_slot_indices.empty() ? nullptr : &dynamic_slot_indices); // Orca: filament_map_2 is engine-derived state, never a user input: the rebuild below // recomputes it from filament_map/filament_volume_map/the variant slots on every apply // (all of which are diffed and invalidation-listed on their own), and the grouping diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b495738e32..2f4cd09801 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10759,6 +10759,145 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen } } +// Regathers a vector option's values through per-slot source indices (one input index per +// output slot). Out-of-range indices keep the first value, matching get_at's fallback. +template +static void gather_option_values(const std::string &key, OptType *opt, const std::vector &slot_param_indices) +{ + if (!opt || opt->values.empty()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key; + return; + } + std::vector new_values; + new_values.reserve(slot_param_indices.size()); + for (int idx : slot_param_indices) { + if (idx < 0 || static_cast(idx) >= opt->values.size()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx; + new_values.emplace_back(opt->values.front()); + } + else + new_values.emplace_back(opt->values[idx]); + } + opt->values = std::move(new_values); +} + +void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config, + const std::unordered_map>& filament_variant_uses, + int extruder_count, int extruder_nozzle_volume_count, + std::set& key_set, std::string id_name, std::string variant_name, + std::vector* slot_machine_indices) +{ + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count; + + auto opt_filament_map = printer_config.option("filament_map"); + if (!opt_filament_map) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: filament_map option not found, skipping")%__LINE__; + return; + } + std::vector filament_maps = opt_filament_map->values; + size_t filament_count = filament_maps.size(); + auto opt_extruder_type = dynamic_cast(printer_config.option("extruder_type")); + auto opt_nozzle_volume_type = dynamic_cast(printer_config.option("nozzle_volume_type")); + if (!opt_extruder_type || !opt_nozzle_volume_type) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: extruder_type or nozzle_volume_type option not found, skipping")%__LINE__; + return; + } + + auto opt_filament_volume_maps = dynamic_cast(printer_config.option("filament_volume_map")); + std::vector filament_volume_maps; + // Orca: same sizing guard as the single-slot rebuild above — honour the per-filament volume + // map only when a producer sized it to the filament count. + 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 slot_param_indices; + slot_param_indices.reserve(filament_count * 2); + if (slot_machine_indices) { + slot_machine_indices->clear(); + slot_machine_indices->reserve(filament_count * 2); + } + + auto resolve_slot = [&](int f_index, ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type, int extruder_id_1based) { + int param_index = get_index_for_extruder(f_index + 1, id_name, extruder_type, nozzle_volume_type, variant_name); + if (param_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%") + %__LINE__ %s_keys_names_ExtruderType[extruder_type] % s_keys_names_NozzleVolumeType[nozzle_volume_type] % (f_index+1) %extruder_id_1based; + assert(false); + //for some updates happens in a invalid state(caused by popup window) + //we need to avoid crash + param_index = 0; + if (opt_ids) { + for (int i = 0; i < opt_ids->values.size(); i++) + if (opt_ids->values[i] == (f_index + 1)) { + param_index = i; + break; + } + } + } + slot_param_indices.push_back(param_index); + if (slot_machine_indices) { + // Orca: key each output slot to the machine slot of its own variant (the same + // derivation the per-filament slot map uses), so the retract-override nil fallback + // stays aligned when a filament occupies more than one slot. + int machine_index = printer_config.get_index_for_extruder(extruder_id_1based, "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); + slot_machine_indices->push_back(std::max(machine_index, 0)); + } + }; + + for (int f_index = 0; f_index < static_cast(filament_count); f_index++) { + auto uses_it = filament_variant_uses.find(f_index); + if (uses_it != filament_variant_uses.end() && !uses_it->second.empty()) { + for (const FilamentVariantUse &use : uses_it->second) + resolve_slot(f_index, use.extruder_type, use.nozzle_volume_type, use.extruder_id + 1); + } + else { + ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(filament_maps[f_index] - 1)); + NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(filament_maps[f_index] - 1)); + if ((extruder_nozzle_volume_count > extruder_count || nozzle_volume_type == nvtHybrid) && !filament_volume_maps.empty()) + nozzle_volume_type = (NozzleVolumeType)(filament_volume_maps[f_index]); + resolve_slot(f_index, extruder_type, nozzle_volume_type, filament_maps[f_index]); + } + } + + const ConfigDef *config_def = this->def(); + if (!config_def) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: can not find config define")%__LINE__; + return; + } + bool has_id_key = !id_name.empty() && key_set.count(id_name) > 0; + for (auto &key : key_set) { + if (has_id_key && key == id_name) + continue; + const ConfigOptionDef *optdef = config_def->get(key); + if (!optdef) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key; + continue; + } + switch (optdef->type) { + case coStrings: gather_option_values(key, this->option(key), slot_param_indices); break; + case coInts: gather_option_values(key, this->option(key), slot_param_indices); break; + case coFloats: gather_option_values(key, this->option(key), slot_param_indices); break; + case coPercents: gather_option_values(key, this->option(key), slot_param_indices); break; + case coFloatsOrPercents: gather_option_values(key, this->option(key), slot_param_indices); break; + case coBools: gather_option_values(key, this->option(key), slot_param_indices); break; + case coEnums: gather_option_values(key, this->option(key), slot_param_indices); break; + default: + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key; + break; + } + } + // Orca: also require a non-empty id list; get_at on an empty vector is undefined. + if (has_id_key && opt_ids && !opt_ids->values.empty()) { + // remap the id list itself last: the slot resolution above still needs the original ids + std::vector new_values; + new_values.reserve(slot_param_indices.size()); + for (int idx : slot_param_indices) + new_values.push_back(opt_ids->get_at(idx)); + const_cast(opt_ids)->values = std::move(new_values); + } +} + namespace { // Options in printer_options_with_variant_2 are stored as (normal,silent) pairs per printer variant. // Some legacy presets/projects carry a variant list but still store only one pair; normalize to avoid crashes. diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ab5e5797a5..2249360ea5 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -683,6 +683,23 @@ class StaticPrintConfig; // Minimum object distance for arrangement, based on printer technology. double min_object_distance(const ConfigBase &cfg); +// One (extruder type x nozzle volume type) parameter variant a filament prints through, plus a +// representative physical extruder observed using it. Ordering (and set-dedup identity) covers +// the variant pair only, so the same variant reached through two extruders keeps one config slot. +struct FilamentVariantUse +{ + ExtruderType extruder_type{etDirectDrive}; + NozzleVolumeType nozzle_volume_type{nvtStandard}; + int extruder_id{0}; // 0-based, first extruder seen using this variant + + bool operator<(const FilamentVariantUse &other) const + { + if (extruder_type != other.extruder_type) + return extruder_type < other.extruder_type; + return nozzle_volume_type < other.nozzle_volume_type; + } +}; + // Slic3r dynamic configuration, used to override the configuration // per object, per modification volume or per printing material. // The dynamic configuration is also used to store user modifications of the print global parameters, @@ -748,6 +765,18 @@ public: std::vector update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector>& nv_types, std::set& key_set, std::string id_name, std::string variant_name, unsigned int stride = 1, unsigned int extruder_id = 0, NozzleVolumeType filament_nvt = nvtStandard); void update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set& key_set, std::string id_name, std::string variant_name); + // Rebuilds the per-slot filament arrays from a per-layer grouping outcome: a filament that + // prints through several (extruder x nozzle volume type) variants keeps one slot per variant + // (unlike the single-slot rebuild above), so layer-aware consumers can resolve the slot the + // current layer actually prints with. Filaments absent from filament_variant_uses keep a + // single slot resolved from filament_map / filament_volume_map. When slot_machine_indices is + // non-null it receives one machine-variant slot index per output slot (the nil-value fallback + // keying for the extruder retract overrides; a per-filament map cannot index expanded arrays). + void update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config, + const std::unordered_map>& filament_variant_uses, + int extruder_count, int extruder_nozzle_volume_count, + std::set& key_set, std::string id_name, std::string variant_name, + std::vector* slot_machine_indices = nullptr); void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set& different_keys, std::string extruder_id_name, std::string extruder_variant_name, std::set& key_set1, std::set& key_set2); diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index 2caab39c7c..0a0befde3f 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -605,8 +606,8 @@ TEST_CASE("Sequential selector prints publish a stitched result and cache the pl { // By-object + smart filament assign: the by-object branch of Print::process must plan each // object with nozzle-status threading, cache the plans for the g-code export, stitch them - // into the published print-wide result, and write the derived extruder map back once - // (per-object orderings must not churn the config). + // into the published print-wide result, and write the grouping result back to the config + // once (per-object orderings must not churn the config). DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.option("nozzle_diameter", true)->values = {0.4, 0.4}; config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"}; @@ -655,6 +656,10 @@ TEST_CASE("Sequential selector prints publish a stitched result and cache the pl REQUIRE(result->get_layer_count() > 0); // The write-back mirrors the stitched result's extruder map. REQUIRE(print.config().filament_map.values == result->get_extruder_map(false)); + // The per-slot filament arrays stay label-consistent whether or not the stitched plan + // actually migrated a filament (one slot per filament, plus one per extra variant). + REQUIRE(print.config().filament_extruder_variant.values.size() == print.config().filament_self_index.values.size()); + REQUIRE(print.config().filament_self_index.values.size() >= print.config().filament_map.values.size()); // Export must consume the cached plans and produce g-code without throwing. boost::filesystem::path gcode_path = boost::filesystem::temp_directory_path() / "orca_seq_dynamic_publish_test.gcode"; @@ -662,3 +667,198 @@ TEST_CASE("Sequential selector prints publish a stitched result and cache the pl REQUIRE(boost::filesystem::exists(gcode_path)); boost::filesystem::remove(gcode_path); } + +TEST_CASE("Per-variant expansion gives migrating filaments one slot per variant", "[PrintConfig][H2C][Dynamic]") +{ + // The selector write-back rebuilds the filament arrays from the grouping result: a filament + // that prints through several (extruder x volume type) variants keeps one slot per variant, + // and every key grows in lockstep with the self-index / variant labels. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // Extruder 1 Standard, extruder 2 Hybrid (Standard + High Flow): 3 nozzle slots, 2 extruders. + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + config.option("print_extruder_id", true)->values = {1, 1, 2, 2}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + // Two filaments with superset arrays: one column per (filament x variant). + config.option("filament_map", true)->values = {1, 2}; + config.option("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtHighFlow}; + config.option("filament_self_index", true)->values = {1, 1, 2, 2}; + config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + config.option("nozzle_temperature", true)->values = {220, 230, 240, 250}; + + std::set key_set = {"filament_self_index", "filament_extruder_variant", "nozzle_temperature"}; + + auto make_use = [](ExtruderType et, NozzleVolumeType nvt, int extruder_id) { + FilamentVariantUse use; + use.extruder_type = et; + use.nozzle_volume_type = nvt; + use.extruder_id = extruder_id; + return use; + }; + + SECTION("a migrating filament expands, machine slots track each output slot") { + std::unordered_map> uses; + uses[0] = {make_use(etDirectDrive, nvtStandard, 0), make_use(etDirectDrive, nvtHighFlow, 1)}; + uses[1] = {make_use(etDirectDrive, nvtHighFlow, 1)}; + std::vector slot_machine_indices; + config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set, + "filament_self_index", "filament_extruder_variant", + &slot_machine_indices); + REQUIRE(config.option("filament_self_index")->values == std::vector{1, 1, 2}); + REQUIRE(config.option("filament_extruder_variant")->values == + std::vector({"Direct Drive Standard", "Direct Drive High Flow", "Direct Drive High Flow"})); + REQUIRE(config.option("nozzle_temperature")->values == std::vector{220, 230, 250}); + // Slot 0 backs onto extruder 1 Standard; slots 1-2 onto extruder 2 High Flow. + REQUIRE(slot_machine_indices == std::vector{0, 3, 3}); + } + + SECTION("filaments absent from the uses fall back to their static assignment") { + std::unordered_map> uses; + uses[0] = {make_use(etDirectDrive, nvtStandard, 0)}; + // Filament 1 unrouted: filament_map -> extruder 2 (Hybrid) -> volume map -> High Flow. + config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set, + "filament_self_index", "filament_extruder_variant"); + REQUIRE(config.option("filament_self_index")->values == std::vector{1, 2}); + REQUIRE(config.option("nozzle_temperature")->values == std::vector{220, 250}); + } + + SECTION("a mis-sized filament_volume_map is ignored") { + config.option("filament_volume_map", true)->values = {(int) nvtHighFlow}; + std::unordered_map> uses; + uses[0] = {make_use(etDirectDrive, nvtStandard, 0)}; + // Unrouted filament 1 keeps the extruder's own typing (Hybrid folds to Standard). + config.update_filament_config_values_for_multiple_extruders(config, uses, 2, 3, key_set, + "filament_self_index", "filament_extruder_variant"); + REQUIRE(config.option("nozzle_temperature")->values == std::vector{220, 240}); + } +} + +TEST_CASE("Selector write-back expands migrating filaments and survives re-apply", "[Print][H2C][Dynamic]") +{ + // A filament the per-layer plan moves between nozzle variants must end up with one config + // slot per variant (so per-layer temperatures/retractions resolve correctly), the extruder + // retract overrides must key each slot to its own variant's machine value, and an unchanged + // re-apply must reproduce the expansion instead of trimming it back to one slot per + // filament — a trim-back would diff the freshly written values and invalidate the result. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.option("nozzle_diameter", true)->values = {0.4, 0.4}; + config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"}; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + config.option("print_extruder_id", true)->values = {1, 1, 2, 2}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + // Three filaments: 0 -> extruder 1 (Std), 1 -> extruder 2 (Std), 2 -> extruder 2, migrating + // Standard -> High Flow between layers. Superset arrays: one column per (filament x variant). + // filament_type must be sized to the filament count: the variant-use collection (like the + // full-config producers) keys the per-filament loop on it. + config.option("filament_type", true)->values = {"PLA", "PLA", "PLA"}; + config.option("filament_diameter", true)->values = {1.75, 1.75, 1.75}; + config.option("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"}; + config.option("filament_map", true)->values = {1, 2, 2}; + config.option("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtStandard}; + config.option("filament_self_index", true)->values = {1, 1, 2, 2, 3, 3}; + config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + config.option("nozzle_temperature", true)->values = {200, 210, 220, 230, 240, 250}; + // The migrating filament's Standard column is nil, so the override merge must fall back to + // the machine value of the Standard slot (not the High Flow one). + config.option("filament_retraction_length", true)->values = + {0.5, 0.5, 0.6, 0.6, ConfigOptionFloatsNullable::nil_value(), 1.2}; + config.option("retraction_length", true)->values = {0.8, 0.9, 1.0, 1.1}; + + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); + + Print print; + print.apply(model, config); + + // Stub grouping result: nozzles as in the resolver test; filament 2 prints on the Standard + // nozzle at layer 0 and on the High Flow nozzle at layer 1. + std::vector nozzle_list; + { + NozzleInfo n; + n.diameter = "0.4"; + n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzle_list.push_back(n); + n.volume_type = nvtStandard; n.extruder_id = 1; n.group_id = 1; nozzle_list.push_back(n); + n.volume_type = nvtHighFlow; n.extruder_id = 1; n.group_id = 2; nozzle_list.push_back(n); + } + std::vector> layer_maps = {{0, 1, 1}, {0, 1, 2}}; + std::vector> layer_seqs = {{0, 1, 2}, {0, 1, 2}}; + auto group = LayeredNozzleGroupResult::create(layer_maps, nozzle_list, {0, 1, 2}, layer_seqs); + REQUIRE(group.has_value()); + REQUIRE(group->is_support_dynamic_nozzle_map()); + print.set_nozzle_group_result(std::make_shared(*group)); + + print.update_to_config_by_nozzle_group_result(*group); + + // Filament 2 holds two slots (Standard + High Flow), everything in lockstep. + REQUIRE(print.config().filament_map.values == group->get_extruder_map(false)); + REQUIRE(print.config().filament_self_index.values == std::vector{1, 2, 3, 3}); + REQUIRE(print.config().nozzle_temperature.values == std::vector{200, 220, 240, 250}); + // The layer-aware resolver picks the slot matching each layer's variant. + REQUIRE(print.get_filament_config_indx(2, 0) == 2); + REQUIRE(print.get_filament_config_indx(2, 1) == 3); + // Retract overrides: non-nil slots take the filament value; the nil Standard slot of the + // migrating filament falls back to its own variant's machine value. + const auto &machine_retract = print.full_print_config().option("retraction_length")->values; + int f2_std_machine_slot = print.full_print_config().get_index_for_extruder(2, "print_extruder_id", etDirectDrive, nvtStandard, + "print_extruder_variant"); + REQUIRE(f2_std_machine_slot >= 0); + const std::vector merged_retract = print.config().retraction_length.values; + REQUIRE(merged_retract.size() == 4); + REQUIRE_THAT(merged_retract[0], Catch::Matchers::WithinAbs(0.5, 1e-9)); + REQUIRE_THAT(merged_retract[1], Catch::Matchers::WithinAbs(0.6, 1e-9)); + REQUIRE_THAT(merged_retract[2], Catch::Matchers::WithinAbs(machine_retract[f2_std_machine_slot], 1e-9)); + REQUIRE_THAT(merged_retract[3], Catch::Matchers::WithinAbs(1.2, 1e-9)); + + // Re-apply the unchanged config: the persisted result must reproduce the exact expansion. + auto status = print.apply(model, config); + REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); + REQUIRE(print.config().filament_self_index.values == std::vector{1, 2, 3, 3}); + REQUIRE(print.config().nozzle_temperature.values == std::vector{200, 220, 240, 250}); + REQUIRE(print.config().retraction_length.values == merged_retract); +} + +TEST_CASE("Selector slicing keeps the result valid across re-apply", "[Print][H2C][Dynamic]") +{ + // The dynamic counterpart of the static re-apply test above: a full process() run through + // the selector branch (whatever grouping it settles on) must leave the config in a state + // the next apply reproduces without invalidating the freshly sliced result. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.option("nozzle_diameter", true)->values = {0.4, 0.4}; + config.option("extruder_nozzle_stats", true)->values = {"Standard#1", "Standard#1|High Flow#2"}; + config.option("extruder_type", true)->values = {etDirectDrive, etDirectDrive}; + config.option("nozzle_volume_type", true)->values = {nvtStandard, nvtHybrid}; + config.option("extruder_variant_list", true)->values = {"Direct Drive Standard,Direct Drive High Flow", + "Direct Drive Standard,Direct Drive High Flow"}; + config.option("print_extruder_id", true)->values = {1, 1, 2, 2}; + config.option("print_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow", + "Direct Drive Standard", "Direct Drive High Flow"}; + config.option("filament_diameter", true)->values = {1.75, 1.75, 1.75}; + config.option("filament_colour", true)->values = {"#FF0000", "#00FF00", "#0000FF"}; + config.option("filament_map", true)->values = {1, 2, 2}; + config.option("filament_volume_map", true)->values = {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow}; + config.set_key_value("enable_filament_dynamic_map", new ConfigOptionBool(true)); + config.option>("filament_map_mode", true)->value = FilamentMapMode::fmmAutoForFlush; + + Model model; + ModelObject *object = model.add_object("cube", "", make_cube(20, 20, 20)); + object->add_instance()->set_offset(Vec3d(100., 100., 0.)); + + Print print; + print.apply(model, config); + print.process(); + REQUIRE(print.is_step_done(psSlicingFinished)); + + auto status = print.apply(model, config); + REQUIRE(status != PrintBase::APPLY_STATUS_INVALIDATED); + REQUIRE(print.is_step_done(psSlicingFinished)); +}