mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 10:51:12 +00:00
Bracket a toolchange's own gcode against FanMover splitting its moves (#15904)
FanMover could split a move inside change_filament_gcode to place a delayed fan command. It computed the split point from the last position the G-code set, but on Bambu printers the firmware moves the head during the T command, so the inserted waypoint sent the head to a spot the template never visits (#15789). Both toolchange paths, set_extruder() and the Type1 append_tcr(), now wrap the template output in ;_FAN_MOVER_SKIP_START/END tags when fan speed-up or kick-start is on. FanMover keeps fan commands inside them where they are and drops the tags from the output. multifilament_config() also sizes the per-filament options that the wipe tower reads by index.
This commit is contained in:
@@ -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<ConfigOptionVectorBase *>(config.option(key, true))->resize(filaments, defaults.option(key));
|
||||
|
||||
// flush_volumes_matrix must be sized filaments*filaments or export rejects it.
|
||||
|
||||
@@ -315,6 +315,79 @@ TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
|
||||
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 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<std::string>{}, "direct", "type1");
|
||||
CAPTURE(path);
|
||||
|
||||
std::string gcode_str;
|
||||
if (path == "direct") {
|
||||
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> 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<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> 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
|
||||
|
||||
Reference in New Issue
Block a user