diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ec7ef83a3d..b79a386324 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1033,6 +1033,15 @@ static std::vector get_path_of_change_filament(const Print& print) return buf; } + // FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode. + // Only written when FanMover runs, the same gate as its construction in GCode::process_layers. + static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode) + { + if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0) + return gcode; + return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n"; + } + std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const { if (new_filament_id != -1 && new_filament_id != tcr.new_tool) @@ -1384,6 +1393,9 @@ static std::vector get_path_of_change_filament(const Print& print) toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config); check_add_eol(toolchange_gcode_str); + // Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does, + // so FanMover doesn't split a G1 inside this custom gcode either. + toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str); //BBS { @@ -10895,7 +10907,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config); check_add_eol(toolchange_gcode_parsed); - gcode += toolchange_gcode_parsed; + // toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool). + gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed); //BBS { diff --git a/src/libslic3r/GCode/FanMover.cpp b/src/libslic3r/GCode/FanMover.cpp index d303cbbac6..d49ca8b114 100644 --- a/src/libslic3r/GCode/FanMover.cpp +++ b/src/libslic3r/GCode/FanMover.cpp @@ -445,6 +445,11 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6); current_role = ExtrusionEntity::string_to_role(extrusion_string); } + if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) { + // Internal markers: read here, then dropped from the output. + m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0; + time = -1; + } if (line.raw().size() > 16) { if (line.raw().rfind("; custom gcode", 0) != std::string::npos) { if (line.raw().rfind("; custom gcode end", 0) != std::string::npos) diff --git a/tests/fff_print/test_helpers.cpp b/tests/fff_print/test_helpers.cpp index c7302d4c4b..daa8132d5d 100644 --- a/tests/fff_print/test_helpers.cpp +++ b/tests/fff_print/test_helpers.cpp @@ -223,7 +223,10 @@ DynamicPrintConfig multifilament_config(unsigned int filaments, std::initializer // These are read by filament id during export but absent from filament_option_keys(), // so set_num_filaments leaves them size 1; size them too, else out-of-range access. const auto &defaults = FullPrintConfig::defaults(); - for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode" }) + for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode", "filament_change_length", "filament_change_length_nc", + "filament_ramming_travel_time", "filament_ramming_travel_time_nc", + "filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc", + "nozzle_temperature", "nozzle_temperature_initial_layer" }) static_cast(config.option(key, true))->resize(filaments, defaults.option(key)); // flush_volumes_matrix must be sized filaments*filaments or export rejects it. diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index e1f70571b8..54a98d2268 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -315,6 +315,79 @@ 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 } +// FanMover's guard against splitting a G1 inside custom gcode keys off the ";_FAN_MOVER_SKIP_START/END" +// markers GCode::set_extruder() must emit around change_filament_gcode's output. Direct +// toolchanges and Type2's wipe-tower toolchange (append_tcr2() calls set_extruder()) both go +// through it; Type1's wipe-tower toolchange (WipeTowerIntegration::append_tcr(), append_tcr2's only +// sibling caller of change_filament_gcode) needs the same bracketing of its own. +TEST_CASE("Toolchange gcode is bracketed against FanMover splitting its moves", "[MultiFilament]") +{ + const std::string custom_gcode = "; fan full\nM106 P1 S255\nM400 S3\nG1 X77 F5000\nG1 X91 F3000\n"; + + // "direct": no prime tower, GCode::set_extruder() runs the toolchange. By-object sequencing + // with a per-object filament override is what makes one happen at all without a tower. + // "type1": a Type1 prime tower routes the toolchange through WipeTowerIntegration::append_tcr() + // instead (append_tcr2, Type2's equivalent, itself calls set_extruder() - so Type2 needs no + // separate case here). + const std::string path = GENERATE(as{}, "direct", "type1"); + CAPTURE(path); + + std::string gcode_str; + if (path == "direct") { + const std::vector> per_object_overrides = { + {}, { { "outer_wall_filament_id", "2" }, { "inner_wall_filament_id", "2" } } + }; + DynamicPrintConfig config = multifilament_config(2, { + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + { "print_sequence", "by object" }, + { "change_filament_gcode", custom_gcode }, + { "fan_speedup_time", 0.5 }, + { "fan_kickstart", 0.1 }, + }); + gcode_str = slice_with_object_overrides({ cube(20), cube(20) }, config, per_object_overrides); + } else { + const std::vector> per_object_overrides = { + { { "extruder", "1" } }, { { "extruder", "2" } } + }; + DynamicPrintConfig config = multifilament_config(2, { + { "change_filament_gcode", custom_gcode }, + { "fan_speedup_time", 0.5 }, + { "fan_kickstart", 0.1 }, + { "enable_prime_tower", true }, + { "wipe_tower_type", "type1" }, + { "prime_tower_width", 35 }, + { "wipe_tower_x", 120 }, + { "wipe_tower_y", 120 }, + { "printable_area", "0x0,200x0,200x200,0x200" }, + { "skirt_loops", 0 }, + { "brim_type", "no_brim" }, + }); + gcode_str = slice_with_object_overrides({ cube(20), cube(20) }, config, per_object_overrides); + } + + // The resolved-settings config footer echoes "change_filament_gcode = ..." with its value + // escaped, so drop it before counting or its own copy of "G1 X77 F5000" is miscounted. + const size_t footer_start = gcode_str.find("; CONFIG_BLOCK_START"); + REQUIRE(footer_start != std::string::npos); + gcode_str.erase(footer_start); + + auto count_occurrences = [](const std::string &haystack, const std::string &needle) { + size_t count = 0, pos = 0; + while ((pos = haystack.find(needle, pos)) != std::string::npos) { + ++count; + pos += needle.size(); + } + return count; + }; + const size_t total_waypoints = count_occurrences(gcode_str, "G1 X77 F5000"); + const size_t bracketed = count_occurrences(gcode_str, "M400 S3\nG1 X77 F5000"); + REQUIRE(total_waypoints > 0); + CHECK(bracketed == total_waypoints); + // FanMover drops its own markers; only the start-of-print toolchange, which it never processes, keeps them. + CHECK(count_occurrences(gcode_str, ";_FAN_MOVER_SKIP_START") <= 1); +} + // 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