From 0f48ad4157db967e46f490a314b98d9514833de4 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 3 Aug 2026 14:35:48 +0800 Subject: [PATCH] WIP: add wait_for_temp_on_wipe_tower feature --- resources/profiles/Custom.json | 2 +- .../machine/fdm_toolchanger_common.json | 1 + src/libslic3r/GCode.cpp | 15 ++- src/libslic3r/GCode.hpp | 4 +- src/libslic3r/GCode/WipeTower2.cpp | 96 ++++++++++++-- src/libslic3r/GCode/WipeTower2.hpp | 9 +- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/Print.cpp | 1 + src/libslic3r/PrintConfig.cpp | 11 ++ src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/Tab.cpp | 2 + tests/fff_print/test_multifilament.cpp | 118 ++++++++++++++++++ 12 files changed, 244 insertions(+), 18 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 0429742c88..7eba013111 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.02", + "version": "02.04.00.03", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_toolchanger_common.json b/resources/profiles/Custom/machine/fdm_toolchanger_common.json index 7ef8b5207c..55f1b2cadb 100644 --- a/resources/profiles/Custom/machine/fdm_toolchanger_common.json +++ b/resources/profiles/Custom/machine/fdm_toolchanger_common.json @@ -6,6 +6,7 @@ "instantiation": "false", "gcode_flavor": "klipper", "single_extruder_multi_material": "0", + "wait_for_temp_on_wipe_tower": "1", "default_filament_profile": [ "Generic PLA @MyToolChanger" ], diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d88f75955b..0f06a3d2b6 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -412,10 +412,12 @@ static std::vector get_path_of_change_filament(const Print& print) return gcode; } - std::string OozePrevention::post_toolchange(GCode& gcodegen) + std::string OozePrevention::post_toolchange(GCode& gcodegen, bool wait) { + // With wait_for_temp_on_wipe_tower the blocking M109 is emitted by the wipe tower + // generator once the head is back over the tower; here only the target is restored. return (gcodegen.config().standby_temperature_delta.value != 0) ? - gcodegen.writer().set_temperature(this->_get_temp(gcodegen), true, gcodegen.writer().filament()->id()) : + gcodegen.writer().set_temperature(this->_get_temp(gcodegen), wait, gcodegen.writer().filament()->id()) : std::string(); } @@ -1554,7 +1556,8 @@ static std::vector get_path_of_change_filament(const Print& print) interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id); toolchange_temp_override = interface_temp; } - toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z + toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override, + gcodegen.config().wait_for_temp_on_wipe_tower.value); // TODO: toolchange_z vs print_z if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) { // The tool changed in place (multi-tool printer without ramming), so the // tower entry is the tcr's own positioning move — a straight line across @@ -1705,7 +1708,7 @@ static std::vector get_path_of_change_filament(const Print& print) std::string trimmed = line; trimmed.erase(0, trimmed.find_first_not_of(" \t")); bool skip_line = false; - if (boost::starts_with(trimmed, "M109")) { + if (boost::starts_with(trimmed, "M109") && trimmed.find(WipeTower2::wait_for_temp_tag()) == std::string::npos) { bool matches_extruder = true; if (trimmed.find('T') != std::string::npos) matches_extruder = trimmed.find(t_token) != std::string::npos; @@ -8936,7 +8939,7 @@ void GCode::update_placeholder_parser_with_variant_params() } } -std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override) +std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override, bool defer_temp_wait) { int new_extruder_id = get_extruder_id(new_filament_id); if (!m_writer.need_toolchange(new_filament_id)) @@ -9348,7 +9351,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo } // Set the new extruder to the operating temperature. if (m_ooze_prevention.enable) - gcode += m_ooze_prevention.post_toolchange(*this); + gcode += m_ooze_prevention.post_toolchange(*this, !defer_temp_wait); if (m_config.enable_pressure_advance.get_at(new_filament_id)) { gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id)); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index acd5acb0a2..64df280e1e 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -50,7 +50,7 @@ public: OozePrevention() : enable(false) {} std::string pre_toolchange(GCode &gcodegen); - std::string post_toolchange(GCode &gcodegen); + std::string post_toolchange(GCode &gcodegen, bool wait = true); private: int _get_temp(const GCode &gcodegen) const; @@ -262,7 +262,7 @@ public: std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone); // extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract. std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); } - std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1); + std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false); bool is_BBL_Printer(); WipeTowerType wipe_tower_type(); diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index 51ab155dd9..e799263576 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -412,6 +412,7 @@ public: const Vec2f& pos() const { return m_current_pos; } const Vec2f start_pos_rotated() const { return m_start_pos; } const Vec2f pos_rotated() const { return this->rotate(m_current_pos); } + const Vec2f rotated(const Vec2f &pt) const { return this->rotate(pt); } float elapsed_time() const { return m_elapsed_time; } float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; } @@ -623,10 +624,13 @@ public: } // Set extruder temperature, don't wait by default. - WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false) + WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false, const std::string& comment = std::string()) { m_gcode += "G4 S0\n"; // to flush planner queue - m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature) + "\n"; + m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature); + if (!comment.empty()) + m_gcode += " " + comment; + m_gcode += "\n"; return *this; } @@ -1034,7 +1038,8 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau m_wall_type((int)config.wipe_tower_wall_type), m_use_gap_wall(use_gap_wall(config)), m_enable_tower_interface_features(config.enable_tower_interface_features.value), - m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value) + m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value), + m_wait_for_temp_on_wipe_tower(config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value) { // Read absolute value of first layer speed, if given as percentage, // it is taken over following default. Speeds from config are not @@ -1066,6 +1071,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau // Calculate where the priming lines should be - very naive test not detecting parallelograms etc. const std::vector& bed_points = config.printable_area.values; BoundingBoxf bb(bed_points); + m_bed_bbox = bb; m_bed_width = float(bb.size().x()); m_bed_shape = (bed_points.size() == 4 ? RectangularBed : CircularBed); @@ -1235,7 +1241,7 @@ std::vector WipeTower2::prime( unsigned int tool = tools[idx_tool]; m_left_to_right = true; - toolchange_Change(writer, tool, m_filpar[tool].material); // Select the tool, set a speed override for soluble and flex materials. + toolchange_Change(writer, tool, m_filpar[tool].material, m_filpar[tool].first_layer_temperature, false); // Select the tool, set a speed override for soluble and flex materials. toolchange_Load(writer, cleaning_box); // Prime the tool. if (idx_tool + 1 == tools.size()) { // Last tool should not be unloaded, but it should be wiped enough to become of a pure color. @@ -1354,13 +1360,19 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, (is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature), new_tool_temp); - toolchange_Change(writer, tool, m_filpar[tool].material); // Change the tool, set a speed override for soluble and flex materials. + // Wait-at-tower target: the interface temp when an interface boost applies on this layer, + // otherwise the print temp (nozzle_temperature == 0 means "use the first layer temp"). + int wait_for_temp = interface_layer && m_filpar[tool].interface_print_temperature > 0 ? + m_filpar[tool].interface_print_temperature : + (is_first_layer() || m_filpar[tool].temperature == 0 ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature); + toolchange_Change(writer, tool, m_filpar[tool].material, wait_for_temp, true); // Change the tool, set a speed override for soluble and flex materials. toolchange_Load(writer, cleaning_box); writer.travel(writer.x(), writer.y()-m_perimeter_width); // cooling and loading were done a bit down the road int base_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature; if (interface_layer) { int interface_temp = m_filpar[tool].interface_print_temperature; - if (interface_temp > 0 && interface_temp != base_temp) + // With wait-for-temp-on-wipe-tower the toolchange already blocked for the interface temp. + if (interface_temp > 0 && interface_temp != base_temp && !m_wait_for_temp_on_wipe_tower) writer.set_extruder_temp(interface_temp, true); if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp) writer.set_extruder_temp(base_temp, false); @@ -1670,7 +1682,9 @@ void WipeTower2::toolchange_Unload( void WipeTower2::toolchange_Change( WipeTowerWriter2 &writer, const size_t new_tool, - const std::string& new_material) + const std::string& new_material, + const int wait_for_temp, + const bool wait_beside_tower) { // Ask the writer about how much of the old filament we consumed: if (m_current_tool < m_used_filament_length.size()) @@ -1684,6 +1698,70 @@ void WipeTower2::toolchange_Change( if (m_is_mk4mmu3) writer.switch_filament_monitoring(true); + const bool wait_for_temp_here = m_wait_for_temp_on_wipe_tower && wait_for_temp > 0; + + // The Tn above was issued without a blocking temperature wait (OozePrevention::post_toolchange + // only restores the target); block here, before the deretraction below, which must not extrude + // on a cold nozzle. Like the Bambu H2C, park beside the tower for the heat-up so drool lands + // next to it instead of on its top surface — nearest x side first, the other side as fallback, + // in place if both would leave the bed. Raw pre-rotated moves (see the repositioning move + // below) keep the writer's tracked position at the tower entry. The tag keeps the + // interface-temp deduplication pass in append_tcr2 from stripping the M109. + if (wait_for_temp_here && wait_beside_tower) { + // The rib wall and the stabilization cone bulge past the nominal width rectangle + // (widest near the bottom), and the first-layer brim is printed around the wall later + // in the layer — clear the widest of them, not just the rectangle, so the park point + // and its drool stay off the tower. + float min_x = 0.f, max_x = m_wipe_tower_width; + if (m_wall_type == (int)wtwRib) { + WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width); + const BoundingBox rib_bbox = get_extents(generate_rib_polygon(wt_box)); // the fillet stays within this bbox + min_x = std::min(min_x, unscaled(rib_bbox.min.x())); + max_x = std::max(max_x, unscaled(rib_bbox.max.x())); + } else if (m_wall_type == (int)wtwCone) { + const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth, + m_wipe_tower_cone_angle).second; + const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z; + const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z); + const double w = m_layer_info->depth + m_perimeter_width; + if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall + const float bulge = float(std::sqrt(r * r - 0.25 * w * w) / support_scale); + min_x = std::min(min_x, m_wipe_tower_width / 2.f - bulge); + max_x = std::max(max_x, m_wipe_tower_width / 2.f + bulge); + } + } + if (is_first_layer()) { + const float brim = m_wipe_tower_brim_width < 0.f ? WipeTower::get_auto_brim_by_height(m_wipe_tower_height) : + m_wipe_tower_brim_width; + min_x -= brim; + max_x += brim; + } + constexpr float gap = 2.f; + const float near_x = writer.x() < m_wipe_tower_width / 2.f ? min_x - gap : max_x + gap; + const float far_x = near_x < m_wipe_tower_width / 2.f ? max_x + gap : min_x - gap; + const float a = float(m_wipe_tower_rotation_angle * M_PI / 180.); + const float c = std::cos(a), s = std::sin(a); + for (float side_x : { near_x, far_x }) { + const Vec2f stop = writer.rotated(Vec2f(side_x, writer.y())); + const Vec2f wt = stop + m_rib_offset; + const Vec2f bed_pt(c * wt.x() - s * wt.y() + m_wipe_tower_pos.x(), + s * wt.x() + c * wt.y() + m_wipe_tower_pos.y()); + bool on_bed = false; + if (m_bed_shape == RectangularBed) + on_bed = m_bed_bbox.contains(bed_pt.cast()); + else if (m_bed_shape == CircularBed) + on_bed = (bed_pt.cast() - m_bed_bbox.center()).norm() <= m_bed_width / 2.; + if (!on_bed) + continue; + writer.feedrate(m_travel_speed * 60.f) + .append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(stop.x()) + + " Y" + Slic3r::float_to_string_decimal_point(stop.y()) + + never_skip_tag() + "\n"); + break; + } + writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag()); + } + // Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc) // gcode could have left the extruder somewhere, we cannot just start extruding. We should also inform the // postprocessor that we absolutely want to have this in the gcode, even if it thought it is the same as before. @@ -1694,6 +1772,10 @@ void WipeTower2::toolchange_Change( + never_skip_tag() + "\n" ); + // Priming has no tower to park beside — wait right at the priming line instead. + if (wait_for_temp_here && !wait_beside_tower) + writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag()); + writer.append("[deretraction_from_wipe_tower_generator]"); // The toolchange Tn command will be inserted later, only in case that the user does diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 3efa884202..fc5687cedf 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -22,6 +22,9 @@ class WipeTower2 { public: static const std::string never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; } + // Marks the wait-for-temp-on-wipe-tower M109 so the interface-temp deduplication pass + // in WipeTowerIntegration::append_tcr2 does not strip it. + static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; } static std::pair get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg); static std::vector> extract_wipe_volumes(const PrintConfig& config); @@ -227,6 +230,7 @@ private: size_t m_first_layer_idx = size_t(-1); bool m_enable_tower_interface_features = false; bool m_enable_tower_interface_cooldown_during_tower = false; + bool m_wait_for_temp_on_wipe_tower = false; bool m_prev_layer_had_interface = false; bool m_current_layer_has_interface = false; @@ -263,6 +267,7 @@ private: } m_bed_shape; float m_bed_width; // width of the bed bounding box Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds) + BoundingBoxf m_bed_bbox; // bounding box of the printable area float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill. float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter. @@ -385,7 +390,9 @@ private: void toolchange_Change( WipeTowerWriter2 &writer, const size_t new_tool, - const std::string& new_material); + const std::string& new_material, + const int wait_for_temp, + const bool wait_beside_tower); void toolchange_Load( WipeTowerWriter2 &writer, diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 781c412cac..72736f7d6b 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1422,7 +1422,7 @@ static std::vector s_Preset_printer_options { "use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes", "grab_length", "support_object_skip_flush", "physical_extruder_map", "cooling_tube_retraction", - "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", + "cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", "wait_for_temp_on_wipe_tower", "z_offset", "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", "bed_temperature_formula", "nozzle_flush_dataset", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 371f1e68ea..bffdc1e645 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -382,6 +382,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "wiping_volumes_extruders" || opt_key == "enable_filament_ramming" || opt_key == "tool_change_on_wipe_tower" + || opt_key == "wait_for_temp_on_wipe_tower" || opt_key == "purge_in_prime_tower" || opt_key == "z_offset" || opt_key == "support_multi_bed_types" diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index e77864775e..ca22ffe486 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6542,6 +6542,17 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wait_for_temp_on_wipe_tower", coBool); + def->label = L("Wait for temperature on wipe tower"); + def->tooltip = L("Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe " + "tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on " + "the tower instead of the model, and the travel overlaps with the heating. " + "Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. " + "The firmware or tool change macro must not wait for the temperature itself. " + "When disabled, the temperature wait is issued right after the tool change command."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_no_sparse_layers", coBool); def->label = L("No sparse layers (beta)"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 67841c6404..c51e45b96f 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1658,6 +1658,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, purge_in_prime_tower)) ((ConfigOptionBool, enable_filament_ramming)) ((ConfigOptionBool, tool_change_on_wipe_tower)) + ((ConfigOptionBool, wait_for_temp_on_wipe_tower)) ((ConfigOptionBool, support_multi_bed_types)) ((ConfigOptionBool, use_3mf)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 51db482390..557a73bf34 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5582,6 +5582,7 @@ if (is_marlin_flavor) optgroup->append_single_option_line("purge_in_prime_tower", "printer_multimaterial_wipe_tower#purge-in-prime-tower"); optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming"); optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower"); + optgroup->append_single_option_line("wait_for_temp_on_wipe_tower", "printer_multimaterial_wipe_tower#wait-for-temperature-on-wipe-tower"); optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); @@ -6095,6 +6096,7 @@ void TabPrinter::toggle_options() // so the option is irrelevant there. const size_t extruders_count = m_config->option("nozzle_diameter")->size(); toggle_option("tool_change_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); + toggle_option("wait_for_temp_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1); } wxString extruder_number; long val = 1; diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index f33a8e1e61..694a00097f 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -5,8 +5,10 @@ #include "test_helpers.hpp" #include +#include #include #include +#include using namespace Slic3r; using namespace Slic3r::Test; @@ -86,6 +88,122 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]") CHECK(tools_for_role(gcode, "infill") == std::set{ 0 }); // infill not overridden: stays on F1 } +// With wait_for_temp_on_wipe_tower the blocking M109 moves from right after the Tn command to +// a stop point parked beside the wipe tower (heat-up drool falls next to the tower, not onto +// its top): tagged with _WAIT_FOR_TEMP_ON_WIPE_TOWER, after the toolchange and before the +// repositioning move and the first extrusion of the purge, while the post-toolchange restore +// demotes to a non-blocking M104. Ordering and the off-tower stop are the contract here. +TEST_CASE("Toolchange temperature wait moves to the wipe tower when enabled", "[MultiFilament]") +{ + const bool wait_on_tower = GENERATE(false, true); + DYNAMIC_SECTION("wait_for_temp_on_wipe_tower " << (wait_on_tower ? 1 : 0)) { + const std::string gcode = slice_with_object_overrides( + { cube(20), cube(20) }, + multifilament_config(2, { + { "nozzle_diameter", "0.4,0.4" }, + { "printer_extruder_id", "1,2" }, + { "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" }, + { "extruder_printable_height", "0,0" }, + { "single_extruder_multi_material", 0 }, + { "enable_prime_tower", 1 }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", "50" }, + { "wipe_tower_y", "50" }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "wait_for_temp_on_wipe_tower", wait_on_tower ? 1 : 0 }, + }), + // One filament per object -> a toolchange on every layer. Assigned at the object + // level: the used-filament count that gates the prime tower is derived from + // object/volume configs on the harness's single apply (region filament ids such + // as sparse_infill_filament_id are not counted there and the tower would be + // silently disabled). + { { { "extruder", 1 } }, { { "extruder", 2 } } }); + + // Split into lines and scan the "; CP TOOLCHANGE START".."; CP TOOLCHANGE END" blocks. + std::vector lines; + for (size_t pos = 0; pos < gcode.size();) { + size_t eol = gcode.find('\n', pos); + if (eol == std::string::npos) + eol = gcode.size(); + lines.emplace_back(gcode.substr(pos, eol - pos)); + pos = eol + 1; + } + const auto is_tool_line = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char)l[1]); }; + const auto is_m109_line = [](const std::string& l) { return l.rfind("M109", 0) == 0; }; + const auto is_tagged_wait = [](const std::string& l) { return l.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") != std::string::npos; }; + const auto is_extruding = [](const std::string& l) { + if (l.rfind("G1 ", 0) != 0) + return false; + const size_t e = l.find(" E"); + return e != std::string::npos && l.find_first_of("XY") != std::string::npos && l[e + 2] != '-'; + }; + + int checked_blocks = 0; + for (size_t i = 0; i < lines.size(); ++i) { + if (lines[i].find("; CP TOOLCHANGE START") == std::string::npos) + continue; + size_t block_end = i; + while (block_end < lines.size() && lines[block_end].find("; CP TOOLCHANGE END") == std::string::npos) + ++block_end; + size_t tool_line = block_end; + for (size_t j = i; j < block_end; ++j) + if (is_tool_line(lines[j])) { tool_line = j; break; } + if (tool_line == block_end) + continue; // final unload block, no toolchange + ++checked_blocks; + + size_t tagged_wait = block_end, untagged_m109 = block_end, first_extrusion = block_end; + for (size_t j = tool_line + 1; j < block_end; ++j) { + if (is_m109_line(lines[j]) && tagged_wait == block_end && is_tagged_wait(lines[j])) + tagged_wait = j; + if (is_m109_line(lines[j]) && untagged_m109 == block_end && !is_tagged_wait(lines[j])) + untagged_m109 = j; + if (first_extrusion == block_end && is_extruding(lines[j])) + first_extrusion = j; + } + INFO("toolchange block at line " << i + 1); + if (wait_on_tower) { + // The only blocking wait is the tagged one, parked beside the tower before the purge. + REQUIRE(tagged_wait < block_end); + CHECK(untagged_m109 == block_end); + REQUIRE(first_extrusion < block_end); + CHECK(tagged_wait < first_extrusion); + // The travel preceding the wait parks outside the tower footprint. The tower + // auto-sizes, so derive its extent from the purge extrusions of this block. + size_t stop_line = block_end; + for (size_t j = tagged_wait; j-- > tool_line;) + if (lines[j].rfind("G1 ", 0) == 0 && lines[j].find('X') != std::string::npos) { stop_line = j; break; } + REQUIRE(stop_line < block_end); + const double stop_x = std::stod(lines[stop_line].substr(lines[stop_line].find('X') + 1)); + double purge_min_x = std::numeric_limits::max(), purge_max_x = std::numeric_limits::lowest(); + for (size_t j = tagged_wait; j < block_end; ++j) { + const size_t x_pos = lines[j].find('X'); + if (!is_extruding(lines[j]) || x_pos == std::string::npos) + continue; + const double x = std::stod(lines[j].substr(x_pos + 1)); + purge_min_x = std::min(purge_min_x, x); + purge_max_x = std::max(purge_max_x, x); + } + REQUIRE(purge_min_x <= purge_max_x); + INFO("stop travel: " << lines[stop_line] << " purge x range: " << purge_min_x << ".." << purge_max_x); + const bool beside_tower = stop_x < purge_min_x - 0.5 || stop_x > purge_max_x + 0.5; + CHECK(beside_tower); + } else { + // Stock behavior: the blocking wait follows the toolchange command directly. + REQUIRE(untagged_m109 < block_end); + CHECK(tagged_wait == block_end); + if (first_extrusion < block_end) + CHECK(untagged_m109 < first_extrusion); + } + i = block_end; + } + REQUIRE(checked_blocks > 0); + if (!wait_on_tower) + CHECK(gcode.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos); + } +} + // max_layer_height can be shorter than the extruder count (normalization sizes it to the // filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering // indexed it per-nozzle and read past the end. Shortened directly here to isolate that read;