From 21b46044d04560c488dd56c08d420551fbb2216e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 11 Jul 2026 15:00:33 +0800 Subject: [PATCH] feat(gcode): resolve per-filament variant slots per layer - the g-code writer tracks the current layer id and resolves FILAMENT_CONFIG/NOZZLE_CONFIG (plus every non-macro variant lookup, toolchange placeholder scalars, and the change-filament flush overrides) through Print's per-filament, per-layer config-index resolvers instead of the filament->extruder collapse - update_layer_related_config refreshes the per-layer extruder/volume/nozzle maps in the writer config; update_placeholder_parser_with_variant_params remaps the filament-variant arrays into filament-id space for custom g-code (Orca's flush placeholder computation moves inside it) - the engine's concrete per-filament volume assignment now merges into the config write-back (the temporary hold from the producer commit is lifted together with these consumers), and the background process reads the computed volume map back to the plate - append_full_config dumps the resolved filament_map_2 slots - update_used_filament_values gains a bounds guard - tests: per-filament Hybrid slot resolution + null-result fallback Result: on a Hybrid extruder, each filament's features slice with its assigned sub-nozzle's variant values (speeds, volumetric limits, retraction). Verified on a 4-filament H2C Hybrid project: outer walls split into three feedrate populations (30/50/200 mm/s), toolpath geometry byte-identical, deterministic across repeated slices. All 18 non-Hybrid reference fixtures stay byte-identical except the filament_map_2 header value now showing the real slot. Auto grouping ties (multiple zero-flush perfect matchings) may pick a different filament-to-nozzle isolation than other slicers; verified co-optimal. --- src/libslic3r/FilamentGroupUtils.cpp | 4 + src/libslic3r/GCode.cpp | 297 +++++++++++++----- src/libslic3r/GCode.hpp | 12 + src/libslic3r/GCode/GCodeProcessor.cpp | 10 +- src/libslic3r/GCode/ToolOrdering.cpp | 9 +- src/libslic3r/Print.cpp | 6 +- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 12 +- .../test_toolordering_nozzle_group.cpp | 69 ++++ 8 files changed, 312 insertions(+), 107 deletions(-) diff --git a/src/libslic3r/FilamentGroupUtils.cpp b/src/libslic3r/FilamentGroupUtils.cpp index 13c9603e0a..c0cd747025 100644 --- a/src/libslic3r/FilamentGroupUtils.cpp +++ b/src/libslic3r/FilamentGroupUtils.cpp @@ -331,6 +331,10 @@ namespace FilamentGroupUtils { std::vector res = old_values; for (size_t i = 0; i < used_filaments.size(); ++i) { + // Orca: guard against filament ids beyond the map sizes (possible with + // mis-normalized per-filament arrays from CLI inputs); skip instead of UB. + if (used_filaments[i] >= res.size() || used_filaments[i] >= new_values.size()) + continue; res[used_filaments[i]] = new_values[used_filaments[i]]; } return res; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index b18d26ffc2..2870be7875 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -457,7 +457,9 @@ static std::vector get_path_of_change_filament(const Print& print) if(retraction_length_remaining <=EPSILON) return {retractionBeforeWipe,0.f}; // Calculate wipe speed - double wipe_speed = config.role_based_wipe_speed ? writer.get_current_speed() / 60.0 : config.get_abs_value("wipe_speed", gcodegen.config().travel_speed.get_at(gcodegen.cur_extruder_index())); + // Orca: resolve the travel_speed slot via the Print-side per-layer resolver; the writer's + // per-layer synced config would yield the same index. + double wipe_speed = config.role_based_wipe_speed ? writer.get_current_speed() / 60.0 : config.get_abs_value("wipe_speed", gcodegen.config().travel_speed.get_at(gcodegen.get_nozzle_config_index(gcodegen.writer().filament()->id()))); wipe_speed = std::max(wipe_speed, 10.0); // Process wipe path & calculate wipe path length @@ -549,7 +551,9 @@ static std::vector get_path_of_change_filament(const Print& print) /* Reduce feedrate a bit; travel speed is often too high to move on existing material. Too fast = ripping of existing material; too slow = short wipe path, thus more blob. */ - double _wipe_speed = gcodegen.config().get_abs_value("wipe_speed", gcodegen.config().travel_speed.get_at(gcodegen.cur_extruder_index()));// gcodegen.writer().config.travel_speed.value * 0.8; + // Orca: resolve the travel_speed slot via the Print-side per-layer resolver; the writer's + // per-layer synced config would yield the same index. + double _wipe_speed = gcodegen.config().get_abs_value("wipe_speed", gcodegen.config().travel_speed.get_at(gcodegen.get_nozzle_config_index(gcodegen.writer().filament()->id())));// gcodegen.writer().config.travel_speed.value * 0.8; if(gcodegen.config().role_based_wipe_speed) _wipe_speed = gcodegen.writer().get_current_speed() / 60.0; if(_wipe_speed < 10) @@ -1089,25 +1093,30 @@ static std::vector get_path_of_change_filament(const Print& print) config.set_key_value("travel_point_3_x", new ConfigOptionFloat(float(travel_point_3.x()))); config.set_key_value("travel_point_3_y", new ConfigOptionFloat(float(travel_point_3.y()))); - auto flush_v_speed = m_print_config->filament_flush_volumetric_speed.values; - // Fast purge mode uses filament_flush_temp_fast; Default is inert. - auto flush_temps = (m_print_config->prime_volume_mode == PrimeVolumeMode::pvmFast ? m_print_config->filament_flush_temp_fast : m_print_config->filament_flush_temp).values; - auto filament_cooling_before_tower = m_print_config->filament_cooling_before_tower.values; - for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { - if (flush_v_speed[idx] == 0) - flush_v_speed[idx] = m_print_config->filament_max_volumetric_speed.get_at(idx); + { + size_t num_filaments = m_print_config->filament_type.values.size(); + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + bool use_fast_flush = m_print_config->prime_volume_mode == PrimeVolumeMode::pvmFast; + std::vector flush_v_speed(num_filaments); + std::vector flush_temps(num_filaments); + std::vector filament_cooling_before_tower(num_filaments); + for (size_t idx = 0; idx < num_filaments; ++idx) { + size_t fi = gcodegen.get_filament_config_index(idx); + flush_v_speed[idx] = m_print_config->filament_flush_volumetric_speed.get_at(fi); + if (flush_v_speed[idx] == 0) + flush_v_speed[idx] = m_print_config->filament_max_volumetric_speed.get_at(fi); + flush_temps[idx] = use_fast_flush ? m_print_config->filament_flush_temp_fast.get_at(fi) + : m_print_config->filament_flush_temp.get_at(fi); + if (flush_temps[idx] == 0) + flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(idx); + filament_cooling_before_tower[idx] = m_print_config->filament_cooling_before_tower.get_at(fi); + } + if (tcr.is_contact || gcodegen.m_layer_index == 0) + std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0); + config.set_key_value("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); + config.set_key_value("flush_temperatures", new ConfigOptionInts(flush_temps)); + config.set_key_value("filament_cooling_before_tower", new ConfigOptionFloats(filament_cooling_before_tower)); } - for (size_t idx = 0; idx < flush_temps.size(); ++idx) { - if (flush_temps[idx] == 0) - flush_temps[idx] = m_print_config->nozzle_temperature_range_high.get_at(idx); - } - if (filament_cooling_before_tower.size() < m_print_config->filament_type.values.size()) - filament_cooling_before_tower.resize(m_print_config->filament_type.values.size(), m_print_config->filament_cooling_before_tower.get_at(0)); - if (tcr.is_contact || gcodegen.m_layer_index == 0) - std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0); - config.set_key_value("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); - config.set_key_value("flush_temperatures", new ConfigOptionInts(flush_temps)); - config.set_key_value("filament_cooling_before_tower", new ConfigOptionFloats(filament_cooling_before_tower)); config.set_key_value("flush_length", new ConfigOptionFloat(purge_length)); config.set_key_value("wipe_avoid_perimeter", new ConfigOptionBool(is_used_travel_avoid_perimeter)); config.set_key_value("wipe_avoid_pos_x", new ConfigOptionFloat(wipe_avoid_pos_x)); @@ -1238,10 +1247,13 @@ static std::vector get_path_of_change_filament(const Print& print) check_add_eol(toolchange_unretract_str); gcodegen.placeholder_parser().set("current_extruder", new_filament_id); - gcodegen.placeholder_parser().set("retraction_distance_when_cut", gcodegen.m_config.retraction_distances_when_cut.get_at(new_filament_id)); - gcodegen.placeholder_parser().set("long_retraction_when_cut", gcodegen.m_config.long_retractions_when_cut.get_at(new_filament_id)); - gcodegen.placeholder_parser().set("retraction_distance_when_ec", gcodegen.m_config.retraction_distances_when_ec.get_at(new_filament_id)); - gcodegen.placeholder_parser().set("long_retraction_when_ec", gcodegen.m_config.long_retractions_when_ec.get_at(new_filament_id)); + { + size_t fi = gcodegen.get_filament_config_index(new_filament_id); + gcodegen.placeholder_parser().set("retraction_distance_when_cut", gcodegen.m_config.retraction_distances_when_cut.get_at(fi)); + gcodegen.placeholder_parser().set("long_retraction_when_cut", gcodegen.m_config.long_retractions_when_cut.get_at(fi)); + gcodegen.placeholder_parser().set("retraction_distance_when_ec", gcodegen.m_config.retraction_distances_when_ec.get_at(fi)); + gcodegen.placeholder_parser().set("long_retraction_when_ec", gcodegen.m_config.long_retractions_when_ec.get_at(fi)); + } // Process the start filament gcode. std::string start_filament_gcode_str; @@ -1791,8 +1803,8 @@ static std::vector get_path_of_change_filament(const Print& print) const std::vector ColorPrintColors::Colors = { "#C0392B", "#E67E22", "#F1C40F", "#27AE60", "#1ABC9C", "#2980B9", "#9B59B6" }; #define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->extruder_id()) -#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(m_writer.filament()->id()) -#define NOZZLE_CONFIG(OPT) m_config.OPT.get_at(cur_extruder_index()) +#define FILAMENT_CONFIG(OPT) m_config.OPT.get_at(get_filament_config_index(m_writer.filament()->id())) +#define NOZZLE_CONFIG(OPT) m_config.OPT.get_at(get_nozzle_config_index(m_writer.filament()->id())) void GCode::PlaceholderParserIntegration::reset() { @@ -2676,6 +2688,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_print = &print; m_timelapse_pos_picker.init(&print,m_writer.get_xy_offset().cast()); + // init as filament map + update_layer_related_config(0); // modifies m_silent_time_estimator_enabled DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled); @@ -3086,15 +3100,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("initial_nozzle_id", first_nozzle_id_for_gcode_placeholder(group_result, (int) initial_extruder_id, extruder_id)); this->placeholder_parser().set("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); this->placeholder_parser().set("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); - //Orca: set the key for compatibilty - this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(initial_extruder_id)); - this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(initial_extruder_id)); - this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(initial_extruder_id)); - this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(initial_extruder_id)); + //Orca: set the key for compatibilty, scalar values for the initial extruder (variant-aware) + { + size_t fi = get_filament_config_index(initial_extruder_id); + this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(fi)); + this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(fi)); + this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(fi)); + this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(fi)); + } this->placeholder_parser().set("temperature", new ConfigOptionInts(print.config().nozzle_temperature)); - - this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(m_config.retraction_distances_when_cut)); + // retraction_distances_when_cut (array), flush_volumetric_speeds, flush_temperatures and + // filament_cooling_before_tower are set by update_placeholder_parser_with_variant_params() + // with variant-aware remapping. this->placeholder_parser().set("long_retractions_when_cut",new ConfigOptionBools(m_config.long_retractions_when_cut)); this->placeholder_parser().set("retraction_distances_when_ec", new ConfigOptionFloatsNullable(m_config.retraction_distances_when_ec)); this->placeholder_parser().set("long_retractions_when_ec",new ConfigOptionBoolsNullable(m_config.long_retractions_when_ec)); @@ -3104,20 +3122,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("close_additional_fan_first_x_layers", new ConfigOptionInts(m_config.close_additional_fan_first_x_layers)); this->placeholder_parser().set("additional_fan_full_speed_layer", new ConfigOptionInts(m_config.additional_fan_full_speed_layer)); - auto flush_v_speed = m_config.filament_flush_volumetric_speed.values; - // Fast purge mode uses filament_flush_temp_fast; Default is inert. - auto flush_temps = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast ? m_config.filament_flush_temp_fast : m_config.filament_flush_temp).values; - for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { - if (flush_v_speed[idx] == 0) - flush_v_speed[idx] = m_config.filament_max_volumetric_speed.get_at(idx); - } - for (size_t idx = 0; idx < flush_temps.size(); ++idx) { - if (flush_temps[idx] == 0) - flush_temps[idx] = m_config.nozzle_temperature_range_high.get_at(idx); - } - this->placeholder_parser().set("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); - this->placeholder_parser().set("flush_temperatures", new ConfigOptionInts(flush_temps)); - this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloatsNullable(m_config.filament_cooling_before_tower)); //Set variable for total layer count so it can be used in custom gcode. this->placeholder_parser().set("total_layer_count", m_layer_count); // Useful for sequential prints. @@ -3369,6 +3373,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + // Sync variant-mapped params into placeholder_parser before processing start gcode + update_placeholder_parser_with_variant_params(); + std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); if (print.config().gcode_flavor != gcfKlipper) { // Set bed temperature if the start G-code does not contain any bed temp control G-codes. @@ -3963,6 +3970,25 @@ size_t GCode::get_extruder_id(unsigned int filament_id) const return 0; } +size_t GCode::get_filament_config_index(int filament_id) const +{ + if (m_print) { + return m_print->get_filament_config_indx(filament_id, m_cur_layer_idx); + } + // Orca: without a Print the filament-indexed arrays are unexpanded, so the + // filament id itself is the only meaningful column. + return filament_id; +} + +size_t GCode::get_nozzle_config_index(int filament_id) const +{ + if (m_print) { + return m_print->get_nozzle_config_index(filament_id, m_cur_layer_idx); + } + // Orca: same reasoning; degenerate to the filament's extruder column. + return get_extruder_id(filament_id); +} + // Process all layers of all objects (non-sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. @@ -5193,6 +5219,7 @@ LayerResult GCode::process_layer( else if (support_layer != nullptr) layer_ptr = support_layer; const Layer& layer = *layer_ptr; + m_cur_layer_idx = layer.id(); LayerResult result { {}, layer.id(), false, last_layer }; if (layer_tools.extruders.empty()) // Nothing to extrude. @@ -5288,6 +5315,8 @@ LayerResult GCode::process_layer( // BBS: don't use lazy_raise when enable spiral vase gcode += this->change_layer(print_z); // this will increase m_layer_index + update_layer_related_config(m_layer_index); + update_placeholder_parser_with_variant_params(); m_layer = &layer; m_object_layer_over_raft = false; @@ -5501,7 +5530,7 @@ LayerResult GCode::process_layer( if (layer_to_print.object_layer) { const auto& regions = layer_to_print.object_layer->regions(); const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) { - return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(cur_extruder_index()); + return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id())); }); if (enable_overhang_speed) { m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object, @@ -6269,6 +6298,11 @@ void GCode::append_full_config(const Print &print, std::string &str) throw Slic3r::SlicingError(_(L("Flush volumes matrix do not match to the correct size!"))); } } + // filament_map_2 (the per-filament (extruder x volume-type) slot map) is computed during apply / + // write-back and lives in the applied print config only; the pre-expansion snapshot this dump is + // built from still carries the registered default. Copy the real values so the header line is + // diagnostic (nothing parses it back). + cfg.option("filament_map_2", true)->values = print.config().filament_map_2.values; // Sorted list of config keys, which shall not be stored into the G-code. Initializer list. static const std::set banned_keys( { // filament_extruder_compatibility is a device-side (blacklist) compatibility hint read from the @@ -7116,12 +7150,12 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } else if (this->object_layer_over_raft() && m_config.first_layer_acceleration_over_raft.value > 0) { acceleration = m_config.first_layer_acceleration_over_raft.value; #endif - } else if (m_config.get_abs_value_at("bridge_acceleration", cur_extruder_index()) > 0 && is_bridge(path.role())) { - acceleration = m_config.get_abs_value_at("bridge_acceleration", cur_extruder_index()); - } else if (m_config.get_abs_value_at("sparse_infill_acceleration", cur_extruder_index()) > 0 && (path.role() == erInternalInfill)) { - acceleration = m_config.get_abs_value_at("sparse_infill_acceleration", cur_extruder_index()); - } else if (m_config.get_abs_value_at("internal_solid_infill_acceleration", cur_extruder_index()) > 0 && (path.role() == erSolidInfill)) { - acceleration = m_config.get_abs_value_at("internal_solid_infill_acceleration", cur_extruder_index()); + } else if (m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && is_bridge(path.role())) { + acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + } else if (m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erInternalInfill)) { + acceleration = m_config.get_abs_value_at("sparse_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + } else if (m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())) > 0 && (path.role() == erSolidInfill)) { + acceleration = m_config.get_abs_value_at("internal_solid_infill_acceleration", get_nozzle_config_index(m_writer.filament()->id())); } else if (NOZZLE_CONFIG(outer_wall_acceleration) > 0 && is_external_perimeter(path.role())) { acceleration = NOZZLE_CONFIG(outer_wall_acceleration); } else if (NOZZLE_CONFIG(inner_wall_acceleration) > 0 && is_internal_perimeter(path.role())) { @@ -7222,7 +7256,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, } } else if(path.role() == erInternalBridgeInfill) { - speed = m_config.get_abs_value_at("internal_bridge_speed", cur_extruder_index()); + speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer.filament()->id())); } else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) { speed = NOZZLE_CONFIG(bridge_speed); } else if (path.role() == erInternalInfill) { @@ -8098,8 +8132,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string unsigned int acceleration_to_set = 0; if (this->on_first_layer()) { - unsigned int initial_layer_travel_acceleration = m_config.get_abs_value_at("initial_layer_travel_acceleration", cur_extruder_index()); - double initial_layer_travel_jerk = m_config.get_abs_value_at("initial_layer_travel_jerk", cur_extruder_index()); + unsigned int initial_layer_travel_acceleration = m_config.get_abs_value_at("initial_layer_travel_acceleration", get_nozzle_config_index(m_writer.filament()->id())); + double initial_layer_travel_jerk = m_config.get_abs_value_at("initial_layer_travel_jerk", get_nozzle_config_index(m_writer.filament()->id())); if (NOZZLE_CONFIG(default_acceleration) > 0 && initial_layer_travel_acceleration > 0) { acceleration_to_set = (unsigned int) floor(initial_layer_travel_acceleration + 0.5); @@ -8112,7 +8146,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string if (NOZZLE_CONFIG(default_acceleration) > 0) { if (role == erOverhangPerimeter && is_short_travel) { - const double bridge_acceleration = m_config.get_abs_value_at("bridge_acceleration", cur_extruder_index()); + const double bridge_acceleration = m_config.get_abs_value_at("bridge_acceleration", get_nozzle_config_index(m_writer.filament()->id())); if (bridge_acceleration > 0) acceleration_to_set = (unsigned int) floor(bridge_acceleration + 0.5); @@ -8437,6 +8471,87 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li return gcode; } +void GCode::update_layer_related_config(int layer_id){ + auto group_result = m_print->get_layered_nozzle_group_result(); + // Orca: sequential (by-object) prints never publish a Print-level layered group result on + // this branch; the statically applied config maps stay authoritative there. + if(!group_result) + return; + + auto extruder_map = group_result->get_extruder_map(false,layer_id); + auto volume_map = group_result->get_volume_map(layer_id); + auto nozzle_map = group_result->get_nozzle_map(layer_id); + + m_config.filament_map.values = extruder_map; + m_config.filament_volume_map.values = volume_map; + m_config.filament_nozzle_map.values = nozzle_map; + + m_writer.config.filament_map.values = extruder_map; + m_writer.config.filament_volume_map.values = volume_map; + m_writer.config.filament_nozzle_map.values = nozzle_map; + +} + +void GCode::update_placeholder_parser_with_variant_params() +{ + if (!m_print) + return; + + size_t num_filaments = m_config.filament_type.values.size(); + if (num_filaments == 0) + return; + + // Helpers: remap config arrays from variant index space to filament_id index space. + // After remapping, gcode templates can use param[filament_id] directly. + auto remap_floats_by_filament = [&](const auto &src) { + std::vector dst(num_filaments); + for (size_t i = 0; i < num_filaments; ++i) + dst[i] = src.get_at(get_filament_config_index(i)); + return dst; + }; + auto remap_ints_by_filament = [&](const auto &src) { + std::vector dst(num_filaments); + for (size_t i = 0; i < num_filaments; ++i) + dst[i] = src.get_at(get_filament_config_index(i)); + return dst; + }; + + // --- filament_options_with_variant: gcode indexes by filament_id --- + this->placeholder_parser().set("filament_max_volumetric_speed", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_max_volumetric_speed))); + this->placeholder_parser().set("filament_pre_cooling_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature))); + this->placeholder_parser().set("filament_pre_cooling_temperature_nc", new ConfigOptionInts(remap_ints_by_filament(m_config.filament_pre_cooling_temperature_nc))); + this->placeholder_parser().set("filament_cooling_before_tower", new ConfigOptionFloats(remap_floats_by_filament(m_config.filament_cooling_before_tower))); + this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer))); + this->placeholder_parser().set("nozzle_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature))); + // first_layer_temperature is a legacy alias of nozzle_temperature_initial_layer + this->placeholder_parser().set("first_layer_temperature", new ConfigOptionInts(remap_ints_by_filament(m_config.nozzle_temperature_initial_layer))); + + // --- printer_options_with_variant_1: in m_config these are already merged as filament-indexed --- + this->placeholder_parser().set("retraction_distances_when_cut", new ConfigOptionFloats(remap_floats_by_filament(m_config.retraction_distances_when_cut))); + // hotend_cooling_rate / hotend_heating_rate: gcode uses [filament_map[x]-1] (extruder_id), no remap needed + + // --- filament_map: per-layer dynamic, sync from m_config to placeholder_parser --- + this->placeholder_parser().set("filament_map", new ConfigOptionInts(m_config.filament_map)); + + // --- flush_volumetric_speeds / flush_temperatures: derived with fallback --- + { + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + bool use_fast_flush = m_config.prime_volume_mode == PrimeVolumeMode::pvmFast; + auto flush_v_speed = remap_floats_by_filament(m_config.filament_flush_volumetric_speed); + auto filament_max_v = remap_floats_by_filament(m_config.filament_max_volumetric_speed); + auto flush_temps = remap_ints_by_filament(use_fast_flush ? m_config.filament_flush_temp_fast + : m_config.filament_flush_temp); + for (size_t i = 0; i < num_filaments; ++i) { + if (flush_v_speed[i] == 0) + flush_v_speed[i] = filament_max_v[i]; + if (flush_temps[i] == 0) + flush_temps[i] = m_config.nozzle_temperature_range_high.get_at(i); + } + this->placeholder_parser().set("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); + this->placeholder_parser().set("flush_temperatures", new ConfigOptionInts(flush_temps)); + } +} + std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) { int new_extruder_id = get_extruder_id(new_filament_id); @@ -8446,8 +8561,11 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo // if we are running a single-extruder setup, just set the extruder and return nothing if (!m_writer.multiple_extruders) { this->placeholder_parser().set("current_extruder", new_filament_id); - this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); - this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); + { + size_t fi = get_filament_config_index(new_filament_id); + this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(fi)); + this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(fi)); + } std::string gcode; // Append the filament start G-code. @@ -8459,9 +8577,12 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo config.set_key_value("layer_z", new ConfigOptionFloat(this->writer().get_position().z() - m_config.z_offset.value)); config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z)); config.set_key_value("filament_extruder_id", new ConfigOptionInt(int(new_filament_id))); - config.set_key_value("retraction_distance_when_cut", - new ConfigOptionFloat(m_config.retraction_distances_when_cut.get_at(new_filament_id))); - config.set_key_value("long_retraction_when_cut", new ConfigOptionBool(m_config.long_retractions_when_cut.get_at(new_filament_id))); + { + size_t fi = get_filament_config_index(new_filament_id); + config.set_key_value("retraction_distance_when_cut", + new ConfigOptionFloat(m_config.retraction_distances_when_cut.get_at(fi))); + config.set_key_value("long_retraction_when_cut", new ConfigOptionBool(m_config.long_retractions_when_cut.get_at(fi))); + } gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config); check_add_eol(gcode); @@ -8664,24 +8785,29 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo dyn_config.set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(first_layer_temps)); } - auto flush_v_speed = m_print->config().filament_flush_volumetric_speed.values; - // Fast purge mode uses filament_flush_temp_fast; Default is inert. - auto flush_temps = (m_print->config().prime_volume_mode == PrimeVolumeMode::pvmFast ? m_print->config().filament_flush_temp_fast : m_print->config().filament_flush_temp).values; - auto filament_cooling_before_tower = m_print->config().filament_cooling_before_tower.values; - for (size_t idx = 0; idx < flush_v_speed.size(); ++idx) { - if (flush_v_speed[idx] == 0) - flush_v_speed[idx] = m_print->config().filament_max_volumetric_speed.get_at(idx); + { + size_t num_filaments = m_print->config().filament_type.values.size(); + // Fast purge mode uses filament_flush_temp_fast; Default is inert. + bool use_fast_flush = m_print->config().prime_volume_mode == PrimeVolumeMode::pvmFast; + std::vector flush_v_speed(num_filaments); + std::vector flush_temps(num_filaments); + std::vector filament_cooling_before_tower(num_filaments); + for (size_t idx = 0; idx < num_filaments; ++idx) { + size_t fi = get_filament_config_index(idx); + flush_v_speed[idx] = m_print->config().filament_flush_volumetric_speed.get_at(fi); + if (flush_v_speed[idx] == 0) + flush_v_speed[idx] = m_print->config().filament_max_volumetric_speed.get_at(fi); + flush_temps[idx] = use_fast_flush ? m_print->config().filament_flush_temp_fast.get_at(fi) + : m_print->config().filament_flush_temp.get_at(fi); + if (flush_temps[idx] == 0) + flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(idx); + filament_cooling_before_tower[idx] = m_print->config().filament_cooling_before_tower.get_at(fi); + } + std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0); + dyn_config.set_key_value("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); + dyn_config.set_key_value("flush_temperatures", new ConfigOptionInts(flush_temps)); + dyn_config.set_key_value("filament_cooling_before_tower", new ConfigOptionFloats(filament_cooling_before_tower)); } - for (size_t idx = 0; idx < flush_temps.size(); ++idx) { - if (flush_temps[idx] == 0) - flush_temps[idx] = m_print->config().nozzle_temperature_range_high.get_at(idx); - } - if (filament_cooling_before_tower.size() < m_print->config().filament_type.values.size()) - filament_cooling_before_tower.resize(m_print->config().filament_type.values.size(), m_print->config().filament_cooling_before_tower.get_at(0)); - std::fill(filament_cooling_before_tower.begin(), filament_cooling_before_tower.end(), 0); - dyn_config.set_key_value("flush_volumetric_speeds", new ConfigOptionFloats(flush_v_speed)); - dyn_config.set_key_value("flush_temperatures", new ConfigOptionInts(flush_temps)); - dyn_config.set_key_value("filament_cooling_before_tower", new ConfigOptionFloats(filament_cooling_before_tower)); dyn_config.set_key_value("flush_length", new ConfigOptionFloat(wipe_length)); int flush_count = std::min(g_max_flush_count, (int)std::round(wipe_volume / g_purge_volume_one_time)); @@ -8756,10 +8882,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo this->placeholder_parser().set("current_extruder", new_filament_id); this->placeholder_parser().set("current_hotend", hotend_id_for_gcode_placeholder(m_config, group_result, (int) new_filament_id, new_extruder_id, m_layer_index)); - this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(new_filament_id)); - this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(new_filament_id)); - this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(new_filament_id)); - this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(new_filament_id)); + { + size_t fi = get_filament_config_index(new_filament_id); + this->placeholder_parser().set("retraction_distance_when_cut", m_config.retraction_distances_when_cut.get_at(fi)); + this->placeholder_parser().set("long_retraction_when_cut", m_config.long_retractions_when_cut.get_at(fi)); + this->placeholder_parser().set("retraction_distance_when_ec", m_config.retraction_distances_when_ec.get_at(fi)); + this->placeholder_parser().set("long_retraction_when_ec", m_config.long_retractions_when_ec.get_at(fi)); + } // Append the filament start G-code. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 9acff7c82c..0129e61fac 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -395,6 +395,12 @@ private: void check_placeholder_parser_failed(); size_t cur_extruder_index() const; size_t get_extruder_id(unsigned int filament_id) const; + // Per-filament config-slot resolvers for the current layer (m_cur_layer_idx): the filament + // resolver keys filament-indexed arrays, the nozzle resolver keys (extruder x volume-type) + // slot arrays. Both degenerate to filament_id / extruder index on single-volume printers. + size_t get_filament_config_index(int filament_id) const; + size_t get_nozzle_config_index(int filament_id) const; + void update_placeholder_parser_with_variant_params(); void set_last_pos(const Point &pos) { m_last_pos = Point3(pos, 0); m_last_pos_defined = true; } void set_last_pos(const Point3 &pos) { m_last_pos = pos; m_last_pos_defined = true; } @@ -701,12 +707,18 @@ private: int m_start_gcode_filament = -1; std::string m_filament_instances_code; + // Object layer id of the layer being generated; keys the per-filament config-slot + // resolvers. Distinct from m_layer_index (an export progress counter starting at -1). + size_t m_cur_layer_idx{0}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS int get_bed_temperature(const int extruder_id, const bool is_first_layer, const BedType bed_type) const; int get_highest_bed_temperature(const bool is_first_layer,const Print &print) const; + void update_layer_related_config(int layer_id); + double calc_max_volumetric_speed(const double layer_height, const double line_width, const std::string co_str); std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1); bool _needSAFC(const ExtrusionPath &path); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 22ed145ca5..7358e0852b 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -6940,10 +6940,12 @@ float GCodeProcessor::minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMod } // Machine limit arrays hold 2 values: [0]=Normal, [1]=Stealth. Index by mode only. -// BambuStudio used extruder_id*2+mode to support per-nozzle limits, but OrcaSlicer -// never ported that system (filament_map_2 / get_config_idx_for_filament), so the -// extruder_id offset was always wrong: uninitialized extruder (255) or extruder > 0 -// would overshoot the array and fall back to values.back() (stealth limits). +// Orca: per-(extruder x volume-type) machine limits are deliberately not resolved here even +// though the slicing side now materializes filament_map_2 and the per-filament volume map +// (an extruder_id*2+mode style offset would need filament_map_2 in the processor-side config +// plus a re-audit of every get_option_value(..., mode) call). This only affects +// time-estimation fidelity: limits are mode-indexed for ALL multi-extruder printers alike, so +// a Hybrid extruder degrades no further than existing dual-extruder machines. Follow-up. float GCodeProcessor::get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const { switch (axis) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index bf0c86242d..3cdde4f32c 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -2093,15 +2093,8 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first 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, + FilamentGroupUtils::update_used_filament_values(base_volume_map, grouping_result.get_volume_map(), used_filaments), grouping_result.get_nozzle_map()); } 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 b2b5ae45cf..117ae02428 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2535,12 +2535,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) 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, + FilamentGroupUtils::update_used_filament_values(base_volume_map, grouping_result.get_volume_map(), used_filaments), grouping_result.get_nozzle_map()); } } diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index 5553a20607..23b647303e 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -231,12 +231,12 @@ 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. + // The engine's concrete per-filament volume assignment is merged into the print + // config by the ToolOrdering write-back; reading it back keeps the plate config the + // next apply overlays equal to the written-back state (no diff, no re-invalidation) + // and persists the auto result (always concrete Std/HF, never the Hybrid seed). + std::vector f_volume_maps = m_fff_print->get_filament_volume_maps(); + m_current_plate->set_filament_volume_maps(f_volume_maps); } 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 diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index ae5c5f7a9b..fbc05d811f 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -4,6 +4,9 @@ #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r/TriangleMesh.hpp" #include #include @@ -391,3 +394,69 @@ TEST_CASE("update_used_filament_values merges only used filaments", "[ToolOrderi // No used filaments => the config baseline is returned untouched. REQUIRE(FilamentGroupUtils::update_used_filament_values(old_values, new_values, {}) == old_values); } + +TEST_CASE("Print config-index resolvers pick per-filament Hybrid slots", "[Print][H2C]") +{ + // A 2-extruder printer whose second extruder is Hybrid (Standard + High Flow nozzles). + // The preset-style variant columns carry one column per (extruder x volume type); apply() + // expands them to the 3-slot layout [e1-Std, e2-Std, e2-HF]. + 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("outer_wall_speed", true)->values = {30., 200., 50., 500.}; + + // Three filaments: 0 -> extruder 1 (Std), 1 -> extruder 2 (Std), 2 -> extruder 2 (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}; + + Model model; + model.add_object("cube", "", make_cube(20, 20, 20))->add_instance(); + + Print print; + print.apply(model, config); + + // Stub grouping result mirroring the maps above: one nozzle per (extruder, volume type). + 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 used_filaments = {0, 1, 2}; + auto group = LayeredNozzleGroupResult::create(std::vector{0, 1, 2}, nozzle_list, used_filaments); + REQUIRE(group.has_value()); + print.set_nozzle_group_result(std::make_shared(*group)); + + // The write-back re-expands the config and refreshes the resolver caches. + print.update_filament_maps_to_config({1, 2, 2}, {(int) nvtStandard, (int) nvtStandard, (int) nvtHighFlow}, {0, 1, 2}); + + // The expansion must have produced the 3-slot layout the resolvers index into. + const auto ®ion_config = print.default_region_config(); + REQUIRE(region_config.print_extruder_variant.values == + std::vector({"Direct Drive Standard", "Direct Drive Standard", "Direct Drive High Flow"})); + REQUIRE(region_config.print_extruder_id.values == std::vector({1, 2, 2})); + + SECTION("each filament resolves to its own (extruder x volume type) slot") { + REQUIRE(print.get_nozzle_config_index(0, 0) == 0); // extruder 1, Standard + REQUIRE(print.get_nozzle_config_index(1, 0) == 1); // extruder 2, Standard + REQUIRE(print.get_nozzle_config_index(2, 0) == 2); // extruder 2, High Flow + } + + SECTION("without a group result the resolver falls back to the filament's extruder slot") { + print.set_nozzle_group_result(nullptr); + REQUIRE(print.get_nozzle_config_index(0, 0) == 0); + REQUIRE(print.get_nozzle_config_index(1, 0) == 1); + REQUIRE(print.get_nozzle_config_index(2, 0) == 1); // extruder slot, not the High Flow slot + } +}