mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +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:
+14
-1
@@ -1033,6 +1033,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
return buf;
|
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
|
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)
|
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
|
||||||
@@ -1384,6 +1393,9 @@ static std::vector<Vec2d> 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);
|
toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config);
|
||||||
|
|
||||||
check_add_eol(toolchange_gcode_str);
|
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
|
//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);
|
toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config);
|
||||||
check_add_eol(toolchange_gcode_parsed);
|
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
|
//BBS
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
|
||||||
current_role = ExtrusionEntity::string_to_role(extrusion_string);
|
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().size() > 16) {
|
||||||
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
|
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
|
||||||
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
|
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
|
||||||
|
|||||||
@@ -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(),
|
// 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.
|
// so set_num_filaments leaves them size 1; size them too, else out-of-range access.
|
||||||
const auto &defaults = FullPrintConfig::defaults();
|
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));
|
static_cast<ConfigOptionVectorBase *>(config.option(key, true))->resize(filaments, defaults.option(key));
|
||||||
|
|
||||||
// flush_volumes_matrix must be sized filaments*filaments or export rejects it.
|
// 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
|
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
|
// 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
|
// 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
|
// its top): tagged with _WAIT_FOR_TEMP_ON_WIPE_TOWER, after the toolchange and before the
|
||||||
|
|||||||
Reference in New Issue
Block a user