Ported Bambu Studio wipe tower interface features (with improved preheat and cooldown behaviour) - NEW (#12266)

Wipe tower interface features and preheat fixes

Fresh PR branch rebuilt on upstream/main (squash of origin/BBL-studio-wipe-tower-merge) to avoid merge-history issues.
This commit is contained in:
Argo
2026-02-13 15:53:25 +01:00
committed by GitHub
parent 0576aa5c05
commit a80257eb7e
15 changed files with 727 additions and 43 deletions

View File

@@ -880,7 +880,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
config.set_key_value("old_retract_length_toolchange", new ConfigOptionFloat(old_retract_length_toolchange));
config.set_key_value("new_retract_length_toolchange", new ConfigOptionFloat(new_retract_length_toolchange));
config.set_key_value("old_filament_temp", new ConfigOptionInt(old_filament_temp));
int interface_temp = full_config.filament_tower_interface_print_temp.get_at(new_filament_id);
if (interface_temp == -1)
interface_temp = full_config.nozzle_temperature_range_high.get_at(new_filament_id);
if (full_config.enable_tower_interface_features && tcr.is_contact)
new_filament_temp = interface_temp;
config.set_key_value("new_filament_temp", new ConfigOptionInt(new_filament_temp));
if (full_config.enable_tower_interface_features && tcr.is_contact) {
auto temps = full_config.nozzle_temperature.values;
if (new_filament_id >= 0 && new_filament_id < (int)temps.size())
temps[new_filament_id] = interface_temp;
config.set_key_value("temperature", new ConfigOptionInts(temps));
auto first_layer_temps = full_config.nozzle_temperature_initial_layer.values;
if (new_filament_id >= 0 && new_filament_id < (int)first_layer_temps.size())
first_layer_temps[new_filament_id] = interface_temp;
config.set_key_value("first_layer_temperature", new ConfigOptionInts(first_layer_temps));
}
config.set_key_value("x_after_toolchange", new ConfigOptionFloat(tool_change_start_pos(0)));
config.set_key_value("y_after_toolchange", new ConfigOptionFloat(tool_change_start_pos(1)));
config.set_key_value("z_after_toolchange", new ConfigOptionFloat(nozzle_pos(2)));
@@ -910,6 +926,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
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));
config.set_key_value("is_prime_tower_interface", new ConfigOptionBool(tcr.is_contact));
config.set_key_value("filament_tower_interface_purge_volume", new ConfigOptionFloat(full_config.filament_tower_interface_purge_volume.get_at(new_filament_id)));
config.set_key_value("filament_tower_interface_print_temp", new ConfigOptionInt(interface_temp));
int flush_count = std::min(g_max_flush_count, (int) std::round(purge_volume / g_purge_volume_one_time));
float flush_unit = purge_length / flush_count;
@@ -1144,10 +1163,18 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string toolchange_gcode_str;
std::string deretraction_str;
int toolchange_temp_override = -1;
int interface_temp = -1;
if (tcr.priming || (new_extruder_id >= 0 && needs_toolchange)) {
if (is_ramming)
gcodegen.m_wipe.reset_path(); // We don't want wiping on the ramming lines.
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z); // TODO: toolchange_z vs print_z
if (gcodegen.config().enable_tower_interface_features && tcr.is_contact) {
interface_temp = gcodegen.config().filament_tower_interface_print_temp.get_at(new_extruder_id);
if (interface_temp == -1)
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
if (gcodegen.config().enable_prime_tower) {
deretraction_str += gcodegen.writer().travel_to_z(z, "Force restore layer Z", true);
Vec3d position{gcodegen.writer().get_position()};
@@ -1157,6 +1184,144 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
}
}
if (toolchange_temp_override > 0) {
int base_temp = gcodegen.on_first_layer() ? gcodegen.config().nozzle_temperature_initial_layer.get_at(new_extruder_id)
: gcodegen.config().nozzle_temperature.get_at(new_extruder_id);
if (std::abs(tcr.print_z) < EPSILON)
base_temp = gcodegen.config().nozzle_temperature_initial_layer.get_at(new_extruder_id);
const std::string t_token = " T" + std::to_string(new_extruder_id);
std::string out;
out.reserve(toolchange_gcode_str.size());
size_t pos = 0;
while (pos < toolchange_gcode_str.size()) {
size_t line_end = toolchange_gcode_str.find('\n', pos);
if (line_end == std::string::npos)
line_end = toolchange_gcode_str.size();
std::string line = toolchange_gcode_str.substr(pos, line_end - pos);
std::string trimmed = line;
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
bool skip_line = false;
if (boost::starts_with(trimmed, "M109")) {
bool matches_extruder = trimmed.find(t_token) != std::string::npos;
if (!matches_extruder) {
size_t t_pos = trimmed.find('T');
if (t_pos != std::string::npos) {
size_t t_end = trimmed.find_first_not_of("0123456789", t_pos + 1);
const std::string t_val = trimmed.substr(t_pos + 1, t_end == std::string::npos ? std::string::npos : t_end - (t_pos + 1));
if (!t_val.empty()) {
try {
matches_extruder = std::stoi(t_val) == new_extruder_id;
} catch (...) {
matches_extruder = false;
}
}
}
}
if (matches_extruder) {
size_t s_pos = trimmed.find('S');
if (s_pos != std::string::npos) {
size_t s_end = trimmed.find_first_not_of("0123456789", s_pos + 1);
const std::string s_val = trimmed.substr(s_pos + 1, s_end == std::string::npos ? std::string::npos : s_end - (s_pos + 1));
if (!s_val.empty()) {
try {
skip_line = std::stoi(s_val) == base_temp;
} catch (...) {
skip_line = false;
}
}
}
}
}
if (!skip_line) {
out.append(line);
if (line_end < toolchange_gcode_str.size())
out.push_back('\n');
}
pos = line_end + 1;
}
toolchange_gcode_str.swap(out);
}
if (toolchange_temp_override > 0) {
const std::string preheat_token = "preheat T" + std::to_string(new_extruder_id);
const int preheat_temp = interface_temp > 0 ? interface_temp : toolchange_temp_override;
std::string out;
out.reserve(tcr_rotated_gcode.size());
size_t pos = 0;
while (pos < tcr_rotated_gcode.size()) {
size_t line_end = tcr_rotated_gcode.find('\n', pos);
if (line_end == std::string::npos)
line_end = tcr_rotated_gcode.size();
std::string line = tcr_rotated_gcode.substr(pos, line_end - pos);
std::string trimmed = line;
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
const bool is_preheat_line = (trimmed.find(preheat_token) != std::string::npos);
if (is_preheat_line) {
// Preserve early-preheat timing while forcing interface temp for contact toolchanges.
size_t s_pos = trimmed.find('S');
if (s_pos != std::string::npos) {
size_t s_end = trimmed.find_first_not_of("0123456789", s_pos + 1);
trimmed.replace(s_pos + 1,
(s_end == std::string::npos ? trimmed.size() : s_end) - (s_pos + 1),
std::to_string(preheat_temp));
// Reapply left indentation from the original line.
size_t line_prefix = line.find_first_not_of(" \t");
if (line_prefix != std::string::npos)
line = line.substr(0, line_prefix) + trimmed;
else
line = trimmed;
}
}
out.append(line);
if (line_end < tcr_rotated_gcode.size())
out.push_back('\n');
pos = line_end + 1;
}
tcr_rotated_gcode.swap(out);
}
if (toolchange_temp_override > 0 && interface_temp > 0) {
const std::string t_token = " T" + std::to_string(new_extruder_id);
std::string out;
out.reserve(tcr_rotated_gcode.size());
size_t pos = 0;
while (pos < tcr_rotated_gcode.size()) {
size_t line_end = tcr_rotated_gcode.find('\n', pos);
if (line_end == std::string::npos)
line_end = tcr_rotated_gcode.size();
std::string line = tcr_rotated_gcode.substr(pos, line_end - pos);
std::string trimmed = line;
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
bool skip_line = false;
if (boost::starts_with(trimmed, "M109")) {
bool matches_extruder = true;
if (trimmed.find('T') != std::string::npos)
matches_extruder = trimmed.find(t_token) != std::string::npos;
if (matches_extruder) {
size_t s_pos = trimmed.find('S');
if (s_pos != std::string::npos) {
size_t s_end = trimmed.find_first_not_of("0123456789", s_pos + 1);
const std::string s_val = trimmed.substr(s_pos + 1, s_end == std::string::npos ? std::string::npos : s_end - (s_pos + 1));
if (!s_val.empty()) {
try {
skip_line = std::stoi(s_val) == interface_temp;
} catch (...) {
skip_line = false;
}
}
}
}
}
if (!skip_line) {
out.append(line);
if (line_end < tcr_rotated_gcode.size())
out.push_back('\n');
}
pos = line_end + 1;
}
tcr_rotated_gcode.swap(out);
}
// Insert the toolchange and deretraction gcode into the generated gcode.
DynamicConfig config;
@@ -7192,7 +7357,7 @@ std::string GCode::retract(bool toolchange, bool is_last_retraction, LiftType li
return gcode;
}
std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object)
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);
if (!m_writer.need_toolchange(new_filament_id))
@@ -7280,6 +7445,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
// BBS: if print_z == 0 use first layer temperature
if (abs(print_z) < EPSILON)
new_filament_temp = m_config.nozzle_temperature_initial_layer.get_at(new_filament_id);
if (toolchange_temp_override > 0)
new_filament_temp = toolchange_temp_override;
Vec3d nozzle_pos = m_writer.get_position();
float old_retract_length, old_retract_length_toolchange, wipe_volume;
@@ -7372,6 +7539,27 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
dyn_config.set_key_value("travel_point_3_y", new ConfigOptionFloat(float(travel_point_3.y())));
dyn_config.set_key_value("wipe_avoid_perimeter", new ConfigOptionBool(false));
dyn_config.set_key_value("wipe_avoid_pos_x", new ConfigOptionFloat(wipe_avoid_pos_x));
dyn_config.set_key_value("is_prime_tower_interface", new ConfigOptionBool(false));
dyn_config.set_key_value("filament_tower_interface_purge_volume", new ConfigOptionFloat(m_config.filament_tower_interface_purge_volume.get_at(new_filament_id)));
{
int interface_temp = m_config.filament_tower_interface_print_temp.get_at(new_filament_id);
if (interface_temp == -1)
interface_temp = m_config.nozzle_temperature_range_high.get_at(new_filament_id);
dyn_config.set_key_value("filament_tower_interface_print_temp", new ConfigOptionInt(interface_temp));
}
if (toolchange_temp_override > 0) {
auto temps = m_config.nozzle_temperature.values;
if (new_filament_id < temps.size())
temps[new_filament_id] = toolchange_temp_override;
dyn_config.set_key_value("temperature", new ConfigOptionInts(temps));
dyn_config.set_key_value("nozzle_temperature", new ConfigOptionInts(temps));
auto first_layer_temps = m_config.nozzle_temperature_initial_layer.values;
if (new_filament_id < first_layer_temps.size())
first_layer_temps[new_filament_id] = toolchange_temp_override;
dyn_config.set_key_value("first_layer_temperature", new ConfigOptionInts(first_layer_temps));
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;
auto flush_temps =m_print->config().filament_flush_temp.values;
@@ -7472,6 +7660,19 @@ 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)));
if (toolchange_temp_override > 0) {
auto temps = m_config.nozzle_temperature.values;
if (new_filament_id < temps.size())
temps[new_filament_id] = toolchange_temp_override;
config.set_key_value("temperature", new ConfigOptionInts(temps));
config.set_key_value("nozzle_temperature", new ConfigOptionInts(temps));
auto first_layer_temps = m_config.nozzle_temperature_initial_layer.values;
if (new_filament_id < first_layer_temps.size())
first_layer_temps[new_filament_id] = toolchange_temp_override;
config.set_key_value("first_layer_temperature", new ConfigOptionInts(first_layer_temps));
config.set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(first_layer_temps));
}
gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config);
if (add_change_filament_624) {
gcode += "M625\n";