From 03a704df7c9e746c4af629b977c81e464b7f4596 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 11 Jul 2026 04:38:31 +0800 Subject: [PATCH] feat(engine): wire the per-filament volume-map producer pipeline - Print::update_filament_maps_to_config takes filament/volume/nozzle maps, backfills an empty volume map from extruder types, rebuilds filament_map_2, re-expands the per-filament variant arrays, and recomputes retract overrides keyed by resolved slots - grouping writes its result back in every non-sequential mode; manual multi-nozzle grouping validates the user mapping and raises a translatable error on deviation; the engine's concrete volume assignment is deliberately not merged yet (per-filament arrays are already consumed by filament id, so materializing High Flow now would change motion before the layer-aware resolvers land) - Print::apply treats the three map keys as engine outputs in auto modes (erased from the diff and adopted), compares them against used filaments in manual mode, and keeps the pre-expansion snapshot in sync with the late normalization pass so rebuilt headers reflect the sliced state instead of resurrecting stale values - volume/nozzle maps and extruder_nozzle_stats join the invalidation group of filament_map (wipe tower + skirt/brim) - PresetBundle composes full configs with an optional per-filament volume map (plate map, else defaults derived from each extruder's flow type); project config keeps the map sized across filament count changes - PartPlate stores per-plate volume/nozzle maps; Plater injects them at every slice-composition site (incl. g-code reload and wipe-tower estimation); BackgroundSlicingProcess reads engine results back to the plate in auto modes - per-filament map trust guards relaxed to size-match everywhere now that every producer sizes the map; single-filament explicit flow assignments are honored - tests: grouping volume maps stay concrete, merge semantics of update_used_filament_values, single-filament override honoring Motion g-code is byte-identical fleet-wide including Hybrid projects (19-fixture gate + repro determinism double-slice). Header deltas: the map keys now dump real values, and stale pre-normalization values (e.g. enable_prime_tower on single-used-filament prints) no longer leak into the config block. --- localization/i18n/OrcaSlicer.pot | 3 + src/libslic3r/Format/bbs_3mf.cpp | 6 +- src/libslic3r/GCode/ToolOrdering.cpp | 75 ++++++++++++----- src/libslic3r/PresetBundle.cpp | 81 ++++++++++++++++--- src/libslic3r/PresetBundle.hpp | 11 ++- src/libslic3r/Print.cpp | 71 +++++++++++++--- src/libslic3r/Print.hpp | 2 +- src/libslic3r/PrintApply.cpp | 61 ++++++++++++-- src/libslic3r/PrintConfig.cpp | 24 +++--- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 13 +++ src/slic3r/GUI/PartPlate.cpp | 65 ++++++++++++++- src/slic3r/GUI/PartPlate.hpp | 10 +++ src/slic3r/GUI/Plater.cpp | 24 +++++- .../test_config_variant_expansion.cpp | 10 +-- .../test_toolordering_nozzle_group.cpp | 43 ++++++++++ 15 files changed, 429 insertions(+), 70 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index d478a42b88..16def6d348 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -10467,6 +10467,9 @@ msgstr "" msgid "Grouping error: " msgstr "" +msgid "Group error in manual mode. Please check nozzle count or regroup." +msgstr "" + msgid " can not be placed in the " msgstr "" diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 4ab589d63d..3000adb441 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -4512,8 +4512,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (m_curr_plater){ auto filament_volume_map = get_vector_from_string(value); for (size_t idx = 0; idx < filament_volume_map.size(); ++idx) { - // Only Standard(0)/High Flow(1) are currently supported; clamp any higher - // volume-type (TPU High Flow/Hybrid) back to Standard until they are wired in. + // The map feeds per-filament slot resolution and grouping. Clamp any + // higher volume-type back to Standard(0) on load: Hybrid(2) is only an + // in-memory grouping seed that is never persisted, and TPU High Flow(3) + // is clamped with the same information loss on every load. if (filament_volume_map[idx] > 1) { filament_volume_map[idx] = 0; } diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 1454ced0e7..bf0c86242d 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1332,12 +1332,12 @@ static FilamentGroupContext build_filament_group_context( context.group_info.has_filament_switcher = has_filament_switcher; // 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 && + // Orca: honour the config's per-filament volume map only when it is sized to the filament + // count. The full-config producers (PresetBundle injection, engine write-back) always size + // it; a mis-sized map (stale project value, CLI runs until the per-filament synthesis lands + // there) must not displace the hybrid fallback rebuild_nozzle_unprintables relies on, nor be + // indexed out of bounds. + if (mode == FilamentMapMode::fmmManual && print_config.filament_volume_map.values.size() == filament_nums) context.group_info.filament_volume_map = print_config.filament_volume_map.values; else @@ -1487,12 +1487,12 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::get_recommended_filamen 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(); // 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. + // pass them only when a producer sized them to the filament count (mis-sized maps can + // arrive from stale projects or CLI runs until the per-filament synthesis lands there). // Without valid maps the fully-manual request cannot be honoured; return the empty result, // the same failure an unsatisfiable create() yields. std::optional nozzle_result; - if (filament_nums > 1 && print_config.filament_volume_map.values.size() == filament_nums && + if (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) @@ -1556,7 +1556,21 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::get_recommended_filamen if (has_multiple_nozzle) { auto result_opt = LayeredNozzleGroupResult::create(ret, context.nozzle_info.nozzle_list, used_filaments); - return result_opt ? *result_opt : LayeredNozzleGroupResult(); + if (!result_opt) + return LayeredNozzleGroupResult(); + auto result = *result_opt; + if (mode == FilamentMapMode::fmmManual) { + // Manual grouping must reproduce the user's filament->extruder map exactly; a + // deviation means the requested assignment cannot be satisfied by the nozzle + // inventory, which must surface as a slicing error instead of silently regrouping. + auto result_map = result.get_extruder_map(); + for (auto fid : used_filaments) { + if (result_map[fid] != print_config.filament_map.values[fid] - 1) { + throw Slic3r::RuntimeError(_L("Group error in manual mode. Please check nozzle count or regroup.")); + } + } + } + return result; } } @@ -1617,11 +1631,11 @@ static MultiNozzleUtils::LayeredNozzleGroupResult build_group_result_from_map( 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; }); - // 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 && + // Orca: same sizing guard as the manual grouping paths — create() indexes the volume/nozzle + // maps per used filament with no bounds check, so only maps sized to the filament count are + // trusted (mis-sized maps can arrive from stale projects or CLI runs until the per-filament + // synthesis lands there). Unsized maps fall through to the extruder-level wrap below. + if (has_multiple_nozzle && 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(print_config.nozzle_diameter.values.front()); @@ -2042,8 +2056,10 @@ 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 (0-based) for the stats path + config write-back. The full - // per-nozzle config write-back is not yet wired. + // 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. std::vector derived_maps = grouping_result.get_extruder_map(false); // 1-based if (!derived_maps.empty()) { filament_maps = derived_maps; @@ -2060,11 +2076,34 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first if (derived_maps.empty()) return; filament_maps = derived_maps; - m_print->update_filament_maps_to_config(filament_maps); } else if (!derived_maps.empty()) { // Manual modes: the result mirrors the user's config map; adopt it for consistency. filament_maps = derived_maps; } + // Write the maps back for every mode: used filaments adopt the engine's extruder/nozzle + // choice, unused ones keep their config assignment. In manual modes the extruder map + // mirrors the user's map (a deviation throws in get_recommended_filament_maps). + if (!derived_maps.empty()) { + // Orca: the config maps are the merge base; fall back to a synthesized base when no + // producer sized them to the filament count (CLI runs until the per-filament + // synthesis lands there), where indexing per filament would run out of bounds. + std::vector base_filament_map = print_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 = print_config->filament_volume_map.values; + if (base_volume_map.size() != derived_maps.size()) + base_volume_map.assign(derived_maps.size(), (int)nvtStandard); + // Orca: the engine's concrete per-filament volume assignment (get_volume_map()) is + // deliberately NOT merged into the write-back yet. The write-back re-expands the + // per-filament filament_* arrays from these maps, and those arrays are already + // consumed by filament id, so materializing a High Flow assignment here would alter + // motion g-code before the layer-aware g-code resolvers consume the assignment. + // Until those consumers land, the config keeps its own (GUI-injected or project) + // volume map, size-corrected above. + m_print->update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments), + base_volume_map, + grouping_result.get_nozzle_map()); + } std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value - 1; }); if (m_print->is_BBL_printer()) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 7bf5600f2b..ac31907f6d 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -58,6 +58,9 @@ static std::vector s_project_options { "nozzle_volume_type", "filament_map_mode", "filament_map", + // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament + // slot resolution survives preset switches. + "filament_volume_map", // Filament Track Switch device state: whether the switch is installed and ready, and // whether dynamic per-nozzle filament mapping is active. Persisted with the project and // restored from a saved 3mf; reset to false on load and set true only by live device sync. @@ -79,7 +82,8 @@ DynamicPrintConfig PresetBundle::construct_full_config( const DynamicPrintConfig& project_config, std::vector& in_filament_presets, bool apply_extruder, - std::optional> filament_maps_new) + std::optional> filament_maps_new, + std::optional> filament_volume_maps_new) { DynamicPrintConfig &printer_config = in_printer_preset.config; DynamicPrintConfig &print_config = in_print_preset.config; @@ -94,12 +98,23 @@ DynamicPrintConfig PresetBundle::construct_full_config( size_t num_filaments = in_filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; + std::vector filament_volume_maps(num_filaments, (int)nvtStandard); + + ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) filament_maps = *filament_maps_new; + if (filament_volume_maps_new.has_value()) + filament_volume_maps = *filament_volume_maps_new; + else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) + filament_volume_maps = filament_volume_map_opt->values; + // in some middle state, they may be different if (filament_maps.size() != num_filaments) { filament_maps.resize(num_filaments, 1); } + if (filament_volume_maps.size() != num_filaments) { + filament_volume_maps.resize(num_filaments, nvtStandard); + } auto *extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. @@ -147,7 +162,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( // BBS: update filament config related with variants DynamicPrintConfig filament_config = in_filament_presets[0].config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - 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]); + 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()); compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition()); @@ -171,7 +186,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - 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]); + 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]); } // loop through options and apply them to the resulting config. @@ -246,6 +261,7 @@ DynamicPrintConfig PresetBundle::construct_full_config( out.option("printer_settings_id", true)->value = in_printer_preset.name; out.option("filament_ids", true)->values = filament_ids; out.option("filament_map", true)->values = filament_maps; + out.option("filament_volume_map", true)->values = filament_volume_maps; auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { bool nonempty = false; @@ -2701,6 +2717,9 @@ void PresetBundle::update_selections(AppConfig &config) std::vector filament_maps(filament_colors.size(), 1); project_config.option("filament_map")->values = filament_maps; + std::vector filament_volume_maps(filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); + project_config.option("filament_volume_map")->values = filament_volume_maps; + std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); @@ -2845,6 +2864,9 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p std::vector filament_maps(filament_colors.size(), 1); project_config.option("filament_map")->values = filament_maps; + std::vector filament_volume_maps(filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); + project_config.option("filament_volume_map")->values = filament_volume_maps; + std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); @@ -3021,7 +3043,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); - + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); // Sync filament multi colour @@ -3031,6 +3053,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne } filament_color_type->resize(n); filament_map->values.resize(n, 1); + filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); // BBS set new filament color to new_color @@ -3058,7 +3081,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); - + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); filament_color->resize(n); // Sync filament multi colour @@ -3068,6 +3091,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) } filament_color_type->resize(n); filament_map->values.resize(n, 1); + filament_volume_map->values.resize(n, static_cast(NozzleVolumeType::nvtStandard)); ams_multi_color_filment.resize(n); //BBS set new filament color to new_color @@ -3108,15 +3132,20 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); if (filament_color->values.size() > to_del_flament_id) { filament_color->values.erase(filament_color->values.begin() + to_del_flament_id); if (filament_map->values.size() > to_del_flament_id) { filament_map->values.erase(filament_map->values.begin() + to_del_flament_id); } + if (filament_volume_map->values.size() > to_del_flament_id) { + filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id); + } } else { filament_color->values.resize(to_del_flament_id); filament_map->values.resize(to_del_flament_id, 1); + filament_volume_map->values.resize(to_del_flament_id, static_cast(NozzleVolumeType::nvtStandard)); } // lambda function to erase or resize the container @@ -3339,6 +3368,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_colour"); ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); ConfigOptionInts * filament_map = project_config.option("filament_map"); + ConfigOptionInts * filament_volume_map = project_config.option("filament_volume_map"); if (color_only) { auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { for (int i = 0; i < infos.size(); i++) { @@ -3520,6 +3550,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfilament_presets = exist_filament_presets; filament_map->values.resize(exist_filament_presets.size(), 1); + filament_volume_map->values.resize(exist_filament_presets.size(), static_cast(NozzleVolumeType::nvtStandard)); } else {//overwrite; bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), @@ -3574,12 +3605,14 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfilament_presets = result_presets; ams_multi_color_filment = result_multi_colors; filament_map->values.resize(total, 1); + filament_volume_map->values.resize(total, static_cast(NozzleVolumeType::nvtStandard)); } else { // BBL: existing wholesale replace filament_color->values = ams_filament_colors; filament_color_type->values = ams_filament_color_types; this->filament_presets = ams_filament_presets; filament_map->values.resize(ams_filament_colors.size(), 1); + filament_volume_map->values.resize(ams_filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); } auto& print_config = this->prints.get_edited_preset().config; @@ -3882,10 +3915,26 @@ bool PresetBundle::support_different_extruders() const return supported; } -DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional>filament_maps) const +std::vector PresetBundle::get_default_nozzle_volume_types_for_filaments(std::vector& f_maps) +{ + std::vector result; + int filament_count = f_maps.size(); + result.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); + + auto opt_nozzle_volume_type = dynamic_cast(this->project_config.option("nozzle_volume_type")); + for (int index = 0; index < filament_count; index++) + { + if (opt_nozzle_volume_type && opt_nozzle_volume_type->values.size() > (f_maps[index] - 1)) + result[index] = opt_nozzle_volume_type->values[f_maps[index] - 1]; + } + + return result; +} + +DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional>filament_maps, std::optional> filament_volume_maps) const { return (this->printers.get_edited_preset().printer_technology() == ptFFF) ? - this->full_fff_config(apply_extruder, filament_maps) : + this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) : this->full_sla_config(); } @@ -3944,7 +3993,7 @@ const std::set ignore_settings_list ={ "print_settings_id", "filament_settings_id", "printer_settings_id" }; -DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new) const +DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new, std::optional> filament_volume_maps_new) const { DynamicPrintConfig out; out.apply(FullPrintConfig::defaults()); @@ -3958,8 +4007,17 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio size_t num_filaments = this->filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; + std::vector filament_volume_maps(num_filaments, (int)nvtStandard); + + ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) filament_maps = *filament_maps_new; + if (filament_volume_maps_new.has_value()) { + filament_volume_maps = *filament_volume_maps_new; + out.option("filament_volume_map", true)->values = filament_volume_maps; + } + else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) + filament_volume_maps = filament_volume_map_opt->values; //in some middle state, they may be different if (filament_maps.size() != num_filaments) { filament_maps.resize(num_filaments, 1); @@ -3967,6 +4025,9 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio else { assert(filament_maps.size() == num_filaments); } + if (filament_volume_maps.size() != num_filaments) { + filament_volume_maps.resize(num_filaments, nvtStandard); + } auto* extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. @@ -4023,7 +4084,7 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio //BBS: update filament config related with variants DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - 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]); + 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()); compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); @@ -4117,7 +4178,7 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - 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]); + 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]); } // loop through options and apply them to the resulting config. diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 4bdccd4663..685687975b 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -175,7 +175,8 @@ public: const DynamicPrintConfig &project_config, std::vector &in_filament_presets, bool apply_extruder, - std::optional> filament_maps_new); + std::optional> filament_maps_new, + std::optional> filament_volume_maps_new = std::nullopt); // ORCA: utility function to find the vendor for a given preset name static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type); @@ -383,10 +384,14 @@ public: bool has_defauls_only() const { return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); } - DynamicPrintConfig full_config(bool apply_extruder = true, std::optional>filament_maps = std::nullopt) const; + DynamicPrintConfig full_config(bool apply_extruder = true, std::optional>filament_maps = std::nullopt, std::optional> filament_volume_maps = std::nullopt) const; // full_config() with the some "useless" config removed. DynamicPrintConfig full_config_secure(std::optional>filament_maps = std::nullopt) const; + // Default per-filament nozzle-volume types: each filament inherits the volume type of the + // extruder it maps to (1-based f_maps), Standard when unknown. + std::vector get_default_nozzle_volume_types_for_filaments(std::vector& f_maps); + // Per-extruder flush matrix [extruder_id][from_filament][to_filament] in mm^3, optionally scaled // by the per-extruder flush_multiplier (or flush_multiplier_fast when prime_volume_mode==Fast). // Used by the print-dispatch nozzle-mapping flush-weight estimate. @@ -543,7 +548,7 @@ private: /*ConfigSubstitutions load_config_file_config_bundle( const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ - DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional> filament_maps=std::nullopt) const; + DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional> filament_maps=std::nullopt, std::optional> filament_volume_maps=std::nullopt) const; DynamicPrintConfig full_sla_config() const; // Orca: used for validation only diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 46ca5b76b8..b2b5ae45cf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -334,8 +334,11 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "other_layers_print_sequence_nums" || opt_key == "toolchange_ordering" || opt_key == "extruder_ams_count" + || opt_key == "extruder_nozzle_stats" || opt_key == "filament_map_mode" || opt_key == "filament_map" + || opt_key == "filament_nozzle_map" + || opt_key == "filament_volume_map" || opt_key == "filament_adhesiveness_category" || opt_key == "filament_tower_interface_pre_extrusion_dist" || opt_key == "filament_tower_interface_pre_extrusion_length" @@ -2509,6 +2512,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) std::vectorfilament_maps = this->get_filament_maps(); auto map_mode = get_filament_map_mode(); // get recommended filament map + // Orca: the sequential write-back stays gated to auto modes. In manual modes the + // config maps already carry the user's assignment (the per-object ToolOrdering below + // consumes them directly), so a write-back would only re-store the pre-slice values; + // keeping the gate avoids churning the config on every sequential manual slice. 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. @@ -2516,7 +2523,25 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) auto derived_maps = grouping_result.get_extruder_map(false); if (!derived_maps.empty()) { filament_maps = derived_maps; - update_filament_maps_to_config(filament_maps); + // Write the maps back: used filaments adopt the engine's extruder/nozzle + // choice, unused ones keep their config assignment. + // Orca: the config maps are the merge base; fall back to a synthesized base + // when no producer sized them to the filament count (CLI runs until the + // per-filament synthesis lands there), where indexing per filament would + // run out of bounds. + 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); + // Orca: as on the non-sequential path, the engine's concrete volume + // assignment (get_volume_map()) is deliberately not merged in until the + // layer-aware g-code resolvers consume it; the config keeps its own + // (GUI-injected or project) volume map, size-corrected above. + update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments), + base_volume_map, + grouping_result.get_nozzle_map()); } } // check map valid both in auto and mannual mode @@ -3198,14 +3223,30 @@ void Print::finalize_first_layer_convex_hull() m_first_layer_convex_hull = Geometry::convex_hull(m_first_layer_convex_hull.points); } -void Print::update_filament_maps_to_config(std::vector f_maps) +void Print::update_filament_maps_to_config(std::vector f_maps, std::vector f_volume_maps, std::vector f_nozzle_maps) { - if (m_config.filament_map.values != f_maps) + if ((m_config.filament_map.values != f_maps) || (m_config.filament_volume_map.values != f_volume_maps) || (m_config.filament_nozzle_map.values != f_nozzle_maps)) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament maps changed after pre-slicing."); m_ori_full_print_config.option("filament_map", true)->values = f_maps; m_config.filament_map.values = f_maps; + if (!f_volume_maps.empty()) { + m_ori_full_print_config.option("filament_volume_map", true)->values = f_volume_maps; + m_config.filament_volume_map.values = f_volume_maps; + } + else { + m_ori_full_print_config.option("filament_volume_map", true)->values.resize(f_maps.size(), nvtStandard); + m_config.filament_volume_map.values.resize(f_maps.size(), nvtStandard); + } + + if (!f_nozzle_maps.empty()) { + m_ori_full_print_config.option("filament_nozzle_map", true)->values = f_nozzle_maps; + m_config.filament_nozzle_map.values = f_nozzle_maps; + } + } + + { int extruder_count = 1, extruder_volume_type_count = 1; bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count); std::vector> nozzle_volume_types; @@ -3221,15 +3262,22 @@ void Print::update_filament_maps_to_config(std::vector f_maps) auto opt_extruder_type = dynamic_cast(m_ori_full_print_config.option("extruder_type")); auto opt_nozzle_volume_type = dynamic_cast(m_ori_full_print_config.option("nozzle_volume_type")); // Orca: the loop tolerates configs without the extruder options (unit tests, degenerate - // presets); the volume-map override keeps the sizing guard used everywhere else, and the - // grouping engine does not produce per-filament volume maps yet, so m_config only carries - // a map when the project supplied one. + // presets); the backfill and the override are bounds-checked because the change block above + // is skipped when the maps are unchanged, in which case the stored map may be shorter than + // the filament count. + auto* ori_volume_map = m_ori_full_print_config.option("filament_volume_map", true); for (int index = 0; opt_extruder_type && opt_nozzle_volume_type && index < f_maps.size(); index++) { ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(f_maps[index] - 1)); NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(f_maps[index] - 1)); - if ((extruder_volume_type_count > extruder_count) - && f_maps.size() > 1 && m_config.filament_volume_map.values.size() == f_maps.size()) + if (f_volume_maps.empty()) { + // No per-filament map supplied: backfill from the extruder's own volume type. + if (m_config.filament_volume_map.values.size() > index) + m_config.filament_volume_map.values[index] = nozzle_volume_type; + if (ori_volume_map->values.size() > index) + ori_volume_map->values[index] = nozzle_volume_type; + } + else if ((extruder_volume_type_count > extruder_count) && (m_config.filament_volume_map.values.size() > index)) nozzle_volume_type = (NozzleVolumeType)(m_config.filament_volume_map.values[index]); m_config.filament_map_2.values[index] = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); } @@ -3254,8 +3302,11 @@ void Print::update_filament_maps_to_config(std::vector f_maps) 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); } - t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end()); - m_config.apply_only(m_full_print_config, keys, true); + if ((extruder_count > 1) || support_multi) { + 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); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 100db84079..bcbfe542eb 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1005,7 +1005,7 @@ public: const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const; const ToolOrdering& tool_ordering() const { return m_tool_ordering; } - void update_filament_maps_to_config(std::vector f_maps); + void update_filament_maps_to_config(std::vector f_maps, std::vector f_volume_maps = std::vector{}, std::vector f_nozzle_maps = std::vector{}); void apply_config_for_render(const DynamicConfig &config); // 1 based group ids diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index fa5a128e32..49909ec270 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1212,7 +1212,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ //BBS: process the filament_map related logic std::unordered_set print_diff_set(print_diff.begin(), print_diff.end()); - if (print_diff_set.find("filament_map_mode") == print_diff_set.end()) + if (!print_diff_set.empty() && print_diff_set.find("filament_map_mode") == print_diff_set.end()) { FilamentMapMode map_mode = new_full_config.option>("filament_map_mode", true)->value; if (is_auto_filament_map_mode(map_mode)) { @@ -1224,9 +1224,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ old_opt->set(new_opt); m_config.filament_map = *new_opt; } + if (print_diff_set.find("filament_volume_map") != print_diff_set.end()) { + print_diff_set.erase("filament_volume_map"); + //full_config_diff.erase("filament_volume_map"); + ConfigOptionInts* old_opt = m_full_print_config.option("filament_volume_map", true); + ConfigOptionInts* new_opt = new_full_config.option("filament_volume_map", true); + old_opt->set(new_opt); + m_config.filament_volume_map = *new_opt; + } + if (print_diff_set.find("filament_nozzle_map") != print_diff_set.end()) { + print_diff_set.erase("filament_nozzle_map"); + //full_config_diff.erase("filament_nozzle_map"); + ConfigOptionInts* old_opt = m_full_print_config.option("filament_nozzle_map", true); + ConfigOptionInts* new_opt = new_full_config.option("filament_nozzle_map", true); + old_opt->set(new_opt); + m_config.filament_nozzle_map = *new_opt; + } } else { print_diff_set.erase("extruder_ams_count"); + if (map_mode == fmmManual) { + // filament_nozzle_map is an engine output, not a GUI input, in manual mode + print_diff_set.erase("filament_nozzle_map"); + } std::vector old_filament_map = m_config.filament_map.values; std::vector new_filament_map = new_full_config.option("filament_map", true)->values; @@ -1243,8 +1263,32 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ break; } } - if (same_map) + if (same_map) { print_diff_set.erase("filament_map"); + + // The extruder retract overrides are keyed by the (unchanged) filament map; + // recompute them and drop diffs whose recomputed value matches the current + // config, so a cosmetic reordering of unused filaments does not invalidate. + const auto& retract_keys = print_config_def.extruder_retract_keys(); + const std::string filament_prefix = "filament_"; + std::vector old_f_map_indices(old_filament_map.size(), 0); + for (size_t i = 0; i < old_filament_map.size(); i++) + old_f_map_indices[i] = old_filament_map[i] - 1; + + for (const auto& rk : retract_keys) { + if (print_diff_set.find(rk) == print_diff_set.end()) + continue; + const ConfigOption* opt_old = m_config.option(rk); + const ConfigOption* opt_new_m = new_full_config.option(rk); + const ConfigOption* opt_new_f = new_full_config.option(filament_prefix + rk); + if (opt_old && opt_new_m && opt_new_f) { + std::unique_ptr opt_recomputed(opt_new_m->clone()); + opt_recomputed->apply_override(opt_new_f, old_f_map_indices); + if (*opt_old == *opt_recomputed) + print_diff_set.erase(rk); + } + } + } } } if (print_diff_set.size() != print_diff.size()) @@ -1265,11 +1309,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ { ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(filament_maps[index] - 1)); NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(filament_maps[index] - 1)); - // Orca: the per-filament volume map is only trustworthy when a producer explicitly sized it - // to the filament count; the registered default is a 1-element vector (see + // Orca: honour the per-filament volume map only when a producer sized it to the filament + // count; mis-sized maps (stale project values, CLI runs until the per-filament synthesis + // lands there) must not be indexed per filament (see // update_values_to_printer_extruders_for_multiple_filaments for the same guard). if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps - && filament_maps.size() > 1 && opt_filament_volume_maps->values.size() == filament_maps.size()) + && opt_filament_volume_maps->values.size() == filament_maps.size()) nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]); m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant"); } @@ -1694,6 +1739,12 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ m_default_object_config.apply_only(new_full_config, new_changed_keys, true); // Handle changes to regions config defaults m_default_region_config.apply_only(new_full_config, new_changed_keys, true); + // Orca: keep the pre-expansion snapshot in sync with this late normalization pass. + // The engine map write-back rebuilds m_full_print_config from m_ori_full_print_config + // after slicing; a stale snapshot would resurrect the un-normalized values (e.g. + // enable_prime_tower on a single-filament print) in the dumped config and spuriously + // re-invalidate the g-code on the next apply. + m_ori_full_print_config.apply_only(new_full_config, new_changed_keys, true); m_full_print_config = std::move(new_full_config); update_filament_self_index_cache(); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c684c9e85b..fe48dcc79a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2743,8 +2743,10 @@ void PrintConfigDef::init_fff_params() // Per-filament nozzle-volume-type override (multi-volume/Hybrid). Round-trips through .3mf // plate metadata (filament_volume_maps) and steers per-filament slot resolution when an - // extruder exposes several volume types; inert for existing printers (no producer sizes it - // to the filament count until the write-back pipeline lands). + // extruder exposes several volume types. Producers: the GUI full-config composition injects + // a filament-count-sized map (plate map, else per-extruder defaults) and the project config + // keeps it sized; expansion trusts the map only when its size matches the filament count + // (CLI runs may still carry the 1-element default). def = this->add("filament_volume_map", coInts); def->mode = comDevelop; def->set_default_value(new ConfigOptionInts{(int)(NozzleVolumeType::nvtStandard)}); @@ -2774,8 +2776,10 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionEnum(fmmAutoForFlush)); - // Fully-manual mode (fmmNozzleManual): per-filament -> physical-nozzle assignment. - // Inert until the nozzle-assignment engine consumes it. Internal use only, no translation. + // Per-filament -> physical-nozzle assignment. Written back by the grouping engine after + // slicing (all non-sequential modes) and read back to the plate config; a user input only + // in fully-manual mode (fmmNozzleManual). Dumped in the g-code header for diagnostics. + // Internal use only, no translation. def = this->add("filament_nozzle_map", coInts); def->mode = comDevelop; def->set_default_value(new ConfigOptionInts{1}); @@ -10533,12 +10537,12 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen auto opt_filament_volume_maps = dynamic_cast(printer_config.option("filament_volume_map")); std::vector filament_volume_maps; - // Orca: the per-filament volume map is only trustworthy when a producer explicitly - // sized it to the filament count; the registered default is a 1-element vector and - // stale project values may be mis-sized. A single-filament config cannot be told - // apart from that default by size, so it is ignored too until the pipeline that - // seeds the map lands; anything untrusted must not distort slot resolution. - if (opt_filament_volume_maps && filament_count > 1 && opt_filament_volume_maps->values.size() == filament_count) + // Orca: honour the per-filament volume map only when a producer sized it to the + // filament count. The full-config producers (PresetBundle injection, engine + // write-back) always size it; mis-sized maps (stale project values, CLI runs until + // the per-filament synthesis lands there) must not distort slot resolution nor be + // 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; diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 8c60b136b9..5553a20607 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -231,7 +231,20 @@ void BackgroundSlicingProcess::process_fff() if (m_current_plate->get_real_filament_map_mode(preset_bundle.project_config) < FilamentMapMode::fmmManual) { std::vector f_maps = m_fff_print->get_filament_maps(); m_current_plate->set_filament_maps(f_maps); + // Orca: the volume map is not read back to the plate yet. The engine's concrete + // volume assignment is not written into the print config until the layer-aware + // g-code resolvers consume it (see the ToolOrdering write-back), so the print-side + // value here is just the injected plate-or-default map echoed back; persisting it + // would freeze the per-extruder defaults (including the transient Hybrid seed) into + // the plate and the saved project. Restore together with the write-back merge. } + if (m_current_plate->get_real_filament_map_mode(preset_bundle.project_config) != FilamentMapMode::fmmNozzleManual) { + // The engine-resolved nozzle map is read back for every non-nozzle-manual mode so the + // plate config the next apply overlays matches the engine's written-back state + // (otherwise the full-config diff would invalidate the g-code on every apply). + std::vector f_nozzle_maps = m_fff_print->get_filament_nozzle_maps(); + m_current_plate->set_filament_nozzle_maps(f_nozzle_maps); + } wxCommandEvent evt(m_event_slicing_completed_id); // Post the Slicing Finished message for the G-code viewer to update. // Passing the timestamp diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 8dd3ce4d78..912f5cf84d 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -319,6 +319,18 @@ std::vector PartPlate::get_real_filament_maps(const DynamicConfig& g_config return g_maps; } +std::vector PartPlate::get_real_filament_volume_maps(const DynamicConfig& g_config, bool* use_global_param) const +{ + auto maps = get_filament_volume_maps(); + if (!maps.empty()) { + if (use_global_param) { *use_global_param = false; } + return maps; + } + auto g_maps = g_config.option("filament_volume_map")->values; + if (use_global_param) { *use_global_param = true; } + return g_maps; +} + FilamentMapMode PartPlate::get_real_filament_map_mode(const DynamicConfig& g_config, bool* use_global_param) const { auto mode = get_filament_map_mode(); @@ -3506,8 +3518,16 @@ int PartPlate::load_gcode_from_file(const std::string& filename) int ret = 0; // process gcode + auto& preset_bundle = wxGetApp().preset_bundle; std::vector filament_maps = this->get_filament_maps(); - DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps); + // Inject the plate's volume map (or the per-extruder defaults) exactly like the apply-time + // composition, so the config applied over the loaded slice result matches the next + // background-process apply and does not invalidate the embedded g-code. + std::vector f_volume_maps = this->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps); + } + DynamicPrintConfig full_config = preset_bundle->full_config(false, filament_maps, f_volume_maps); full_config.apply(m_config, true); m_print->apply(*m_model, full_config, false); //BBS: need to apply two times, for after the first apply, the m_print got its object, @@ -3814,6 +3834,40 @@ void PartPlate::clear_filament_map() m_config.erase("filament_map"); } +std::vector PartPlate::get_filament_volume_maps() const +{ + std::string key = "filament_volume_map"; + if (m_config.has(key)) + return m_config.option(key)->values; + + return {}; +} + +void PartPlate::set_filament_volume_maps(const std::vector& f_maps) +{ + m_config.option("filament_volume_map", true)->values = f_maps; +} + +void PartPlate::clear_filament_volume_map() +{ + if (m_config.has("filament_volume_map")) + m_config.erase("filament_volume_map"); +} + +std::vector PartPlate::get_filament_nozzle_maps() const +{ + std::string key = "filament_nozzle_map"; + if (m_config.has(key)) + return m_config.option(key)->values; + + return {}; +} + +void PartPlate::set_filament_nozzle_maps(const std::vector& f_maps) +{ + m_config.option("filament_nozzle_map", true)->values = f_maps; +} + void PartPlate::clear_filament_map_mode() { if (m_config.has("filament_map_mode")) @@ -4247,7 +4301,14 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1); std::vector filament_maps = part_plate->get_real_filament_maps(proj_cfg); - DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps); + // Keep this composition consistent with the apply-time injection (plate volume map, else + // per-extruder defaults); the config below currently only feeds scalar reads, but a + // divergent volume map would silently mis-resolve any future per-filament read here. + std::vector f_volume_maps = part_plate->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps); + } + DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps); const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; float w = dynamic_cast(print_cfg.option("prime_tower_width"))->value; float v = dynamic_cast(full_config.option("prime_volume"))->value; diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index bf8304ffb4..54ca55b072 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -253,6 +253,7 @@ public: PrintSequence get_real_print_seq(bool* plate_same_as_global=nullptr) const; std::vector get_real_filament_maps(const DynamicConfig& g_config, bool* use_global_param = nullptr)const; + std::vector get_real_filament_volume_maps(const DynamicConfig& g_config, bool* use_global_param = nullptr) const; FilamentMapMode get_real_filament_map_mode(const DynamicConfig& g_config,bool * use_global_param = nullptr) const; FilamentMapMode get_filament_map_mode() const; @@ -262,6 +263,15 @@ public: std::vector get_filament_maps() const; void set_filament_maps(const std::vector& f_maps); + // per-filament nozzle-volume choice (NozzleVolumeType values, 0 based filament ids) + std::vector get_filament_volume_maps() const; + void set_filament_volume_maps(const std::vector& f_maps); + void clear_filament_volume_map(); + + // per-filament nozzle-group choice (0 based filament and nozzle ids) + std::vector get_filament_nozzle_maps() const; + void set_filament_nozzle_maps(const std::vector& f_maps); + void clear_filament_map(); void clear_filament_map_mode(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5558e77cbe..fae83e2ef3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8818,7 +8818,11 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool if (preset_bundle->get_printer_extruder_count() > 1) { PartPlate* cur_plate = background_process.get_current_plate(); std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); - invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps)); + std::vector f_volume_maps = cur_plate->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); + } + invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps, f_volume_maps)); background_process.fff_print()->set_extruder_filament_info(get_extruder_filament_info()); } else @@ -18326,7 +18330,11 @@ void Plater::apply_background_progress() Print::ApplyStatus invalidated; if (preset_bundle->get_printer_extruder_count() > 1) { std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); - invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + std::vector f_volume_maps = part_plate->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); + } + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); @@ -18371,7 +18379,11 @@ int Plater::select_plate(int plate_index, bool need_slice) //always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); - invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + std::vector f_volume_maps = part_plate->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); + } + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); @@ -18792,7 +18804,11 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi //always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); - invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps)); + std::vector f_volume_maps = part_plate->get_filament_volume_maps(); + if (f_volume_maps.empty()) { + f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); + } + invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); } else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); diff --git a/tests/libslic3r/test_config_variant_expansion.cpp b/tests/libslic3r/test_config_variant_expansion.cpp index 9cae7a34bc..59f311eae5 100644 --- a/tests/libslic3r/test_config_variant_expansion.cpp +++ b/tests/libslic3r/test_config_variant_expansion.cpp @@ -288,14 +288,15 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe REQUIRE(config.option("filament_self_index")->values == std::vector({1, 2})); } - SECTION("a single-filament map matches the registered default's shape and is ignored") { + SECTION("a single-filament explicit assignment on a Hybrid extruder is honored") { DynamicPrintConfig config = make_hybrid_printer_config(); config.option("filament_self_index", true)->values = {1, 1}; config.option("filament_extruder_variant", true)->values = {"Direct Drive Standard", "Direct Drive High Flow"}; config.option("filament_max_volumetric_speed", true)->values = {12., 20.}; config.option("filament_map", true)->values = {2}; - // sized to the (single) filament count, but indistinguishable from the registered - // 1-element default, so it must not override slot resolution + // sized to the (single) filament count: the producers guarantee sizing, so a + // single-filament map is as trustworthy as any other and the explicit High Flow + // request must win over the Hybrid->Standard fallback config.option("filament_volume_map", true)->values = {nvtHighFlow}; std::vector> nozzle_volume_types; @@ -305,8 +306,7 @@ TEST_CASE("update_values_to_printer_extruders_for_multiple_filaments resolves pe config.update_values_to_printer_extruders_for_multiple_filaments(config, extruder_count, count, filament_keys, "filament_self_index", "filament_extruder_variant"); - // Hybrid resolves as Standard, exactly as if no map were present - REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({12.})); + REQUIRE(config.option("filament_max_volumetric_speed")->values == std::vector({20.})); REQUIRE(config.option("filament_self_index")->values == std::vector({1})); } diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index a6750d64f3..ae5c5f7a9b 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -1,5 +1,6 @@ #include +#include "libslic3r/FilamentGroupUtils.hpp" #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/GCode/ToolOrdering.hpp" @@ -348,3 +349,45 @@ TEST_CASE("NozzleStatusRecorder tracks nozzle/extruder occupancy", "[MultiNozzle // Clearing a nozzle leaves the extruder->nozzle association intact. REQUIRE(rec.get_nozzle_in_extruder(1) == 2); } + +TEST_CASE("Hybrid nozzle stats resolve to concrete volume types", "[ToolOrdering][H2C]") +{ + // Extruder 0 is Standard-only; extruder 1 carries a mixed Standard + High Flow inventory + // (the "Hybrid" flow selection). The write-back pipeline persists get_volume_map(), so the + // result must always carry concrete per-filament volume types, never the Hybrid seed. + auto stats = get_extruder_nozzle_stats({"Standard#1", "Standard#1|High Flow#1"}); + REQUIRE(stats.size() == 2); + REQUIRE(stats[1].size() == 2); + + std::vector used_filaments = {0, 1, 2}; + std::vector filament_map = {0, 1, 1}; // 0-based extruder ids + std::vector volume_requests = {(int) nvtStandard, (int) nvtHighFlow, (int) nvtStandard}; + std::vector nozzle_requests = {0, 1, 2}; // distinct logical nozzles + + auto group = LayeredNozzleGroupResult::create(used_filaments, filament_map, volume_requests, nozzle_requests, stats, 0.4f); + REQUIRE(group.has_value()); + + auto volume_map = group->get_volume_map(); + REQUIRE(volume_map == volume_requests); + for (auto fid : used_filaments) + REQUIRE(volume_map[fid] != (int) nvtHybrid); + + // The Hybrid seed itself matches no physical nozzle: such a request is unsatisfiable. + std::vector hybrid_requests = {(int) nvtStandard, (int) nvtHybrid, (int) nvtStandard}; + REQUIRE_FALSE(LayeredNozzleGroupResult::create(used_filaments, filament_map, hybrid_requests, nozzle_requests, stats, 0.4f).has_value()); +} + +TEST_CASE("update_used_filament_values merges only used filaments", "[ToolOrdering][H2C]") +{ + // The config write-back merges the engine's per-filament values over the config baseline: + // used filaments adopt the engine value, unused filaments keep their config assignment. + std::vector old_values = {1, 1, 2, 1}; + std::vector new_values = {2, 2, 1, 2}; + std::vector used = {0, 2}; + + auto merged = FilamentGroupUtils::update_used_filament_values(old_values, new_values, used); + REQUIRE(merged == std::vector{2, 1, 1, 1}); + + // No used filaments => the config baseline is returned untouched. + REQUIRE(FilamentGroupUtils::update_used_filament_values(old_values, new_values, {}) == old_values); +}