mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Feature: Wait for the toolchange temperature on the wipe tower (#15144)
# Description <!-- > Please provide a summary of the changes made in this PR. Include details such as: > * What issue does this PR address or fix? > * What new features or enhancements does this PR introduce? > * Are there any breaking changes or dependencies that need to be considered? --> Adds **Wait for temperature on wipe tower**, a printer option for multi-extruder machines using a Type 2 wipe tower. With it on, the new tool is picked up without a blocking temperature wait; the printer travels to the wipe tower and waits there right before purging, parked beside the tower so the ooze from the heat-up lands next to it rather than on the model. The incoming filament's target is raised ahead of the tool change, so the heat-up overlaps both the change itself and the travel to the tower. The benefit is less oozing and less dead time. The tool no longer sits at full print temperature while it waits to be picked up or right after it undocks — it heats on the move and only reaches temperature once it is over the tower, so there is far less hot-and-idle time, and what does ooze ends up beside the tower. This matters most on tool changer printers with long docking and attaching cycles, such as Tapchanger and StealthChanger machines, where that wait is otherwise pure stall time spent dripping. The firmware or tool change macro must not wait for the temperature itself. The option is off by default and only shown for multi-extruder printers on a Type 2 wipe tower, and it is enabled by default for the generic toolchanger profile. # Screenshots/Recordings/Graphs <!-- > Please attach relevant screenshots to showcase the UI changes. > Please attach images that can help explain the changes. --> ## Tests <!-- > Please describe the tests that you have conducted to verify the changes made in this PR. --> New Catch2 cases in `tests/fff_print/test_multifilament.cpp`: the wait moves to the tower when enabled, priming pre-heats to the first layer temperature, the park side is regenerated when the tower is moved or rotated, and a regression test pinning the unchanged (option-off) toolchange temperature commands against a recorded trace (`tests/data/wipe_tower_temperature_trace_main.txt`). <!-- > A guide for users on how to download the artifacts from this PR. --> [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Custom Printer",
|
"name": "Custom Printer",
|
||||||
"version": "02.04.00.02",
|
"version": "02.04.00.03",
|
||||||
"force_update": "0",
|
"force_update": "0",
|
||||||
"description": "My configurations",
|
"description": "My configurations",
|
||||||
"machine_model_list": [
|
"machine_model_list": [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"instantiation": "false",
|
"instantiation": "false",
|
||||||
"gcode_flavor": "klipper",
|
"gcode_flavor": "klipper",
|
||||||
"single_extruder_multi_material": "0",
|
"single_extruder_multi_material": "0",
|
||||||
|
"wait_for_temp_on_wipe_tower": "1",
|
||||||
"default_filament_profile": [
|
"default_filament_profile": [
|
||||||
"Generic PLA @MyToolChanger"
|
"Generic PLA @MyToolChanger"
|
||||||
],
|
],
|
||||||
|
|||||||
+26
-5
@@ -1554,7 +1554,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id);
|
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id);
|
||||||
toolchange_temp_override = interface_temp;
|
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,
|
||||||
|
WipeTower2::wait_for_temp_enabled(gcodegen.m_config)); // TODO: toolchange_z vs print_z
|
||||||
if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) {
|
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
|
// 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
|
// tower entry is the tcr's own positioning move — a straight line across
|
||||||
@@ -1705,7 +1706,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
|||||||
std::string trimmed = line;
|
std::string trimmed = line;
|
||||||
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
|
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
|
||||||
bool skip_line = false;
|
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;
|
bool matches_extruder = true;
|
||||||
if (trimmed.find('T') != std::string::npos)
|
if (trimmed.find('T') != std::string::npos)
|
||||||
matches_extruder = trimmed.find(t_token) != std::string::npos;
|
matches_extruder = trimmed.find(t_token) != std::string::npos;
|
||||||
@@ -8939,7 +8940,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);
|
int new_extruder_id = get_extruder_id(new_filament_id);
|
||||||
if (!m_writer.need_toolchange(new_filament_id))
|
if (!m_writer.need_toolchange(new_filament_id))
|
||||||
@@ -9046,6 +9047,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
|||||||
if (toolchange_temp_override > 0)
|
if (toolchange_temp_override > 0)
|
||||||
new_filament_temp = toolchange_temp_override;
|
new_filament_temp = toolchange_temp_override;
|
||||||
|
|
||||||
|
// With wait_for_temp_on_wipe_tower the blocking M109 is deferred to the wipe tower, so raise
|
||||||
|
// the incoming filament's target here — ahead of the tool change rather than after it — and
|
||||||
|
// let the heat-up overlap the change itself as well as the travel to the tower. The command
|
||||||
|
// always carries an explicit tool index (the option is off for single extruder MM, so the
|
||||||
|
// writer emits one), leaving the outgoing filament that pre_toolchange just dropped to its
|
||||||
|
// standby temperature alone. nozzle_temperature == 0 means "use the first layer temperature".
|
||||||
|
if (defer_temp_wait) {
|
||||||
|
// Target what the tower will wait on. It waits on the first layer temperature not only on
|
||||||
|
// the first layer but also while priming, which runs before any layer is set: there
|
||||||
|
// on_first_layer() is false and print_z is the initial layer height, so neither test above
|
||||||
|
// catches it. nozzle_temperature == 0 means "use the first layer temperature" as well.
|
||||||
|
int preheat_temp = new_filament_temp;
|
||||||
|
if (toolchange_temp_override <= 0 && (m_layer == nullptr || preheat_temp <= 0))
|
||||||
|
preheat_temp = m_config.nozzle_temperature_initial_layer.get_at(new_fi);
|
||||||
|
if (preheat_temp > 0)
|
||||||
|
gcode += m_writer.set_temperature(preheat_temp, false, new_filament_id);
|
||||||
|
}
|
||||||
|
|
||||||
Vec3d nozzle_pos = m_writer.get_position();
|
Vec3d nozzle_pos = m_writer.get_position();
|
||||||
float old_retract_length, old_retract_length_toolchange, wipe_volume;
|
float old_retract_length, old_retract_length_toolchange, wipe_volume;
|
||||||
int old_filament_temp, old_filament_e_feedrate;
|
int old_filament_temp, old_filament_e_feedrate;
|
||||||
@@ -9349,8 +9368,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
|||||||
}
|
}
|
||||||
check_add_eol(gcode);
|
check_add_eol(gcode);
|
||||||
}
|
}
|
||||||
// Set the new extruder to the operating temperature.
|
// Set the new extruder to the operating temperature. With defer_temp_wait the target was
|
||||||
if (m_ooze_prevention.enable)
|
// already raised before the tool change and the blocking wait belongs to the wipe tower
|
||||||
|
// generator, so there is nothing left to restore here.
|
||||||
|
if (m_ooze_prevention.enable && !defer_temp_wait)
|
||||||
gcode += m_ooze_prevention.post_toolchange(*this);
|
gcode += m_ooze_prevention.post_toolchange(*this);
|
||||||
|
|
||||||
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
||||||
|
|||||||
@@ -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);
|
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.
|
// 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 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();
|
bool is_BBL_Printer();
|
||||||
WipeTowerType wipe_tower_type();
|
WipeTowerType wipe_tower_type();
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ public:
|
|||||||
const Vec2f& pos() const { return m_current_pos; }
|
const Vec2f& pos() const { return m_current_pos; }
|
||||||
const Vec2f start_pos_rotated() const { return m_start_pos; }
|
const Vec2f start_pos_rotated() const { return m_start_pos; }
|
||||||
const Vec2f pos_rotated() const { return this->rotate(m_current_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 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; }
|
float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; }
|
||||||
|
|
||||||
@@ -624,10 +625,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set extruder temperature, don't wait by default.
|
// 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())
|
||||||
{
|
{
|
||||||
flush_planner_queue();
|
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;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,6 +1010,13 @@ bool WipeTower2::use_gap_wall(const PrintConfig& config)
|
|||||||
return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone;
|
return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WipeTower2::wait_for_temp_enabled(const PrintConfig& config)
|
||||||
|
{
|
||||||
|
// SEMM runs its own unload/load temperature sequence; the GUI hides the option
|
||||||
|
// there but a profile may still carry it set.
|
||||||
|
return config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value;
|
||||||
|
}
|
||||||
|
|
||||||
WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) :
|
WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) :
|
||||||
m_semm(config.single_extruder_multi_material.value),
|
m_semm(config.single_extruder_multi_material.value),
|
||||||
m_enable_filament_ramming(config.enable_filament_ramming.value),
|
m_enable_filament_ramming(config.enable_filament_ramming.value),
|
||||||
@@ -1035,7 +1046,8 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
|||||||
m_wall_type((int)config.wipe_tower_wall_type),
|
m_wall_type((int)config.wipe_tower_wall_type),
|
||||||
m_use_gap_wall(use_gap_wall(config)),
|
m_use_gap_wall(use_gap_wall(config)),
|
||||||
m_enable_tower_interface_features(config.enable_tower_interface_features.value),
|
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(wait_for_temp_enabled(config))
|
||||||
{
|
{
|
||||||
// Read absolute value of first layer speed, if given as percentage,
|
// Read absolute value of first layer speed, if given as percentage,
|
||||||
// it is taken over following default. Speeds from config are not
|
// it is taken over following default. Speeds from config are not
|
||||||
@@ -1085,6 +1097,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
|||||||
m_bed_bottom_left = m_bed_shape == RectangularBed
|
m_bed_bottom_left = m_bed_shape == RectangularBed
|
||||||
? Vec2f(bed_points.front().x(), bed_points.front().y())
|
? Vec2f(bed_points.front().x(), bed_points.front().y())
|
||||||
: Vec2f::Zero();
|
: Vec2f::Zero();
|
||||||
|
m_bed_polygon = Polygon::new_scale(bed_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1236,7 +1249,7 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime(
|
|||||||
|
|
||||||
unsigned int tool = tools[idx_tool];
|
unsigned int tool = tools[idx_tool];
|
||||||
m_left_to_right = true;
|
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.
|
toolchange_Load(writer, cleaning_box); // Prime the tool.
|
||||||
if (idx_tool + 1 == tools.size()) {
|
if (idx_tool + 1 == tools.size()) {
|
||||||
// Last tool should not be unloaded, but it should be wiped enough to become of a pure color.
|
// Last tool should not be unloaded, but it should be wiped enough to become of a pure color.
|
||||||
@@ -1355,13 +1368,19 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
|
|||||||
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material,
|
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),
|
(is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature),
|
||||||
new_tool_temp);
|
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);
|
toolchange_Load(writer, cleaning_box);
|
||||||
writer.travel(writer.x(), writer.y()-m_perimeter_width); // cooling and loading were done a bit down the road
|
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;
|
int base_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature;
|
||||||
if (interface_layer) {
|
if (interface_layer) {
|
||||||
int interface_temp = m_filpar[tool].interface_print_temperature;
|
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);
|
writer.set_extruder_temp(interface_temp, true);
|
||||||
if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp)
|
if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp)
|
||||||
writer.set_extruder_temp(base_temp, false);
|
writer.set_extruder_temp(base_temp, false);
|
||||||
@@ -1671,7 +1690,9 @@ void WipeTower2::toolchange_Unload(
|
|||||||
void WipeTower2::toolchange_Change(
|
void WipeTower2::toolchange_Change(
|
||||||
WipeTowerWriter2 &writer,
|
WipeTowerWriter2 &writer,
|
||||||
const size_t new_tool,
|
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:
|
// Ask the writer about how much of the old filament we consumed:
|
||||||
if (m_current_tool < m_used_filament_length.size())
|
if (m_current_tool < m_used_filament_length.size())
|
||||||
@@ -1685,6 +1706,90 @@ void WipeTower2::toolchange_Change(
|
|||||||
if (m_is_mk4mmu3)
|
if (m_is_mk4mmu3)
|
||||||
writer.switch_filament_monitoring(true);
|
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 (GCode::set_extruder only raises
|
||||||
|
// the target, ahead of the Tn); 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, then that side clamped
|
||||||
|
// toward the bed edge, then the far side, in place if all 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<float>(rib_bbox.min.x()));
|
||||||
|
max_x = std::max(max_x, unscaled<float>(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;
|
||||||
|
constexpr float min_gap = 0.5f;
|
||||||
|
const bool on_left = writer.x() < m_wipe_tower_width / 2.f;
|
||||||
|
const float near_x = on_left ? min_x - gap : max_x + gap;
|
||||||
|
const float far_x = on_left ? max_x + gap : min_x - gap;
|
||||||
|
const Eigen::Rotation2Df to_bed(float(Geometry::deg2rad(m_wipe_tower_rotation_angle)));
|
||||||
|
auto park_pt_on_bed = [this, &writer, to_bed](float side_x) {
|
||||||
|
const Vec2f bed_pt = to_bed * (writer.rotated(Vec2f(side_x, writer.y())) + m_rib_offset) + m_wipe_tower_pos;
|
||||||
|
return m_bed_polygon.contains(Point::new_scale(bed_pt.x(), bed_pt.y()));
|
||||||
|
};
|
||||||
|
float park_x = near_x;
|
||||||
|
bool have_park = park_pt_on_bed(near_x);
|
||||||
|
if (!have_park) {
|
||||||
|
// The ideal near point hangs off the bed: pull it back to the bed edge as long
|
||||||
|
// as that still clears the tower envelope by min_gap (the BBL tower clamps its
|
||||||
|
// stop_pos against the bed the same way in append_tcr). Bisection, because with
|
||||||
|
// tower rotation and non-rectangular beds the bed edge is not axis-aligned.
|
||||||
|
const float limit_x = on_left ? min_x - min_gap : max_x + min_gap;
|
||||||
|
if (park_pt_on_bed(limit_x)) {
|
||||||
|
float on = limit_x, off = near_x;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
const float mid = 0.5f * (on + off);
|
||||||
|
if (park_pt_on_bed(mid))
|
||||||
|
on = mid;
|
||||||
|
else
|
||||||
|
off = mid;
|
||||||
|
}
|
||||||
|
park_x = on;
|
||||||
|
have_park = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!have_park && park_pt_on_bed(far_x)) {
|
||||||
|
park_x = far_x;
|
||||||
|
have_park = true;
|
||||||
|
}
|
||||||
|
if (have_park) {
|
||||||
|
const Vec2f stop = writer.rotated(Vec2f(park_x, writer.y()));
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
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)
|
// 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
|
// 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.
|
// postprocessor that we absolutely want to have this in the gcode, even if it thought it is the same as before.
|
||||||
@@ -1695,6 +1800,10 @@ void WipeTower2::toolchange_Change(
|
|||||||
+ never_skip_tag() + "\n"
|
+ 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]");
|
writer.append("[deretraction_from_wipe_tower_generator]");
|
||||||
|
|
||||||
// The toolchange Tn command will be inserted later, only in case that the user does
|
// The toolchange Tn command will be inserted later, only in case that the user does
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class WipeTower2
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static const std::string never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; }
|
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<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
|
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
|
||||||
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config);
|
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config);
|
||||||
|
|
||||||
@@ -38,6 +41,11 @@ public:
|
|||||||
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
|
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
|
||||||
static bool use_gap_wall(const PrintConfig& config);
|
static bool use_gap_wall(const PrintConfig& config);
|
||||||
|
|
||||||
|
// Whether the blocking toolchange temperature wait moves onto the wipe tower.
|
||||||
|
// Shared with the defer flag in GCode.cpp append_tcr2 so the deferral and the
|
||||||
|
// tower's tagged M109 can never disagree.
|
||||||
|
static bool wait_for_temp_enabled(const PrintConfig& config);
|
||||||
|
|
||||||
// x -- x coordinates of wipe tower in mm ( left bottom corner )
|
// x -- x coordinates of wipe tower in mm ( left bottom corner )
|
||||||
// y -- y coordinates of wipe tower in mm ( left bottom corner )
|
// y -- y coordinates of wipe tower in mm ( left bottom corner )
|
||||||
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
|
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
|
||||||
@@ -227,6 +235,7 @@ private:
|
|||||||
size_t m_first_layer_idx = size_t(-1);
|
size_t m_first_layer_idx = size_t(-1);
|
||||||
bool m_enable_tower_interface_features = false;
|
bool m_enable_tower_interface_features = false;
|
||||||
bool m_enable_tower_interface_cooldown_during_tower = 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_prev_layer_had_interface = false;
|
||||||
bool m_current_layer_has_interface = false;
|
bool m_current_layer_has_interface = false;
|
||||||
|
|
||||||
@@ -263,6 +272,7 @@ private:
|
|||||||
} m_bed_shape;
|
} m_bed_shape;
|
||||||
float m_bed_width; // width of the bed bounding box
|
float m_bed_width; // width of the bed bounding box
|
||||||
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
|
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
|
||||||
|
Polygon m_bed_polygon; // printable_area contour (scaled)
|
||||||
|
|
||||||
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
|
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.
|
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
|
||||||
@@ -385,7 +395,9 @@ private:
|
|||||||
void toolchange_Change(
|
void toolchange_Change(
|
||||||
WipeTowerWriter2 &writer,
|
WipeTowerWriter2 &writer,
|
||||||
const size_t new_tool,
|
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(
|
void toolchange_Load(
|
||||||
WipeTowerWriter2 &writer,
|
WipeTowerWriter2 &writer,
|
||||||
|
|||||||
@@ -1423,7 +1423,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
|||||||
"use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes",
|
"use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes",
|
||||||
"grab_length", "support_object_skip_flush", "physical_extruder_map",
|
"grab_length", "support_object_skip_flush", "physical_extruder_map",
|
||||||
"cooling_tube_retraction",
|
"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",
|
"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",
|
"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",
|
"bed_temperature_formula", "nozzle_flush_dataset",
|
||||||
|
|||||||
@@ -282,6 +282,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
|||||||
|| opt_key == "wipe_tower_x"
|
|| opt_key == "wipe_tower_x"
|
||||||
|| opt_key == "wipe_tower_y"
|
|| opt_key == "wipe_tower_y"
|
||||||
|| opt_key == "wipe_tower_rotation_angle") {
|
|| opt_key == "wipe_tower_rotation_angle") {
|
||||||
|
// The tower gcode itself is position-independent (position and rotation are applied
|
||||||
|
// at export), except that the wait_for_temp_on_wipe_tower park bakes a bed-relative
|
||||||
|
// side choice into it (WipeTower2::toolchange_Change) — regenerate it when the tower
|
||||||
|
// moves. Gating on the old config is safe: both inputs of wait_for_temp_enabled
|
||||||
|
// invalidate psWipeTower themselves when they are part of the same diff.
|
||||||
|
if ((opt_key == "wipe_tower_x" || opt_key == "wipe_tower_y" || opt_key == "wipe_tower_rotation_angle")
|
||||||
|
&& WipeTower2::wait_for_temp_enabled(m_config))
|
||||||
|
steps.emplace_back(psWipeTower);
|
||||||
steps.emplace_back(psSkirtBrim);
|
steps.emplace_back(psSkirtBrim);
|
||||||
} else if (
|
} else if (
|
||||||
opt_key == "slicing_pipeline_plugin"
|
opt_key == "slicing_pipeline_plugin"
|
||||||
@@ -382,6 +390,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
|||||||
|| opt_key == "wiping_volumes_extruders"
|
|| opt_key == "wiping_volumes_extruders"
|
||||||
|| opt_key == "enable_filament_ramming"
|
|| opt_key == "enable_filament_ramming"
|
||||||
|| opt_key == "tool_change_on_wipe_tower"
|
|| opt_key == "tool_change_on_wipe_tower"
|
||||||
|
|| opt_key == "wait_for_temp_on_wipe_tower"
|
||||||
|| opt_key == "purge_in_prime_tower"
|
|| opt_key == "purge_in_prime_tower"
|
||||||
|| opt_key == "z_offset"
|
|| opt_key == "z_offset"
|
||||||
|| opt_key == "support_multi_bed_types"
|
|| opt_key == "support_multi_bed_types"
|
||||||
|
|||||||
@@ -6570,6 +6570,17 @@ void PrintConfigDef::init_fff_params()
|
|||||||
def->mode = comAdvanced;
|
def->mode = comAdvanced;
|
||||||
def->set_default_value(new ConfigOptionBool(false));
|
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 = this->add("wipe_tower_no_sparse_layers", coBool);
|
||||||
def->label = L("No sparse layers (beta)");
|
def->label = L("No sparse layers (beta)");
|
||||||
|
|||||||
@@ -1660,6 +1660,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
|||||||
((ConfigOptionBool, purge_in_prime_tower))
|
((ConfigOptionBool, purge_in_prime_tower))
|
||||||
((ConfigOptionBool, enable_filament_ramming))
|
((ConfigOptionBool, enable_filament_ramming))
|
||||||
((ConfigOptionBool, tool_change_on_wipe_tower))
|
((ConfigOptionBool, tool_change_on_wipe_tower))
|
||||||
|
((ConfigOptionBool, wait_for_temp_on_wipe_tower))
|
||||||
((ConfigOptionBool, support_multi_bed_types))
|
((ConfigOptionBool, support_multi_bed_types))
|
||||||
((ConfigOptionBool, use_3mf))
|
((ConfigOptionBool, use_3mf))
|
||||||
|
|
||||||
|
|||||||
@@ -5627,6 +5627,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("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("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("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");
|
optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings");
|
||||||
@@ -6160,6 +6161,7 @@ void TabPrinter::toggle_options()
|
|||||||
// so the option is irrelevant there.
|
// so the option is irrelevant there.
|
||||||
const size_t extruders_count = m_config->option<ConfigOptionFloats>("nozzle_diameter")->size();
|
const size_t extruders_count = m_config->option<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||||
toggle_option("tool_change_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1);
|
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;
|
wxString extruder_number;
|
||||||
long val = 1;
|
long val = 1;
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Temperature and tool-change commands of a wait_for_temp_on_wipe_tower-off slice,
|
||||||
|
# captured from the main branch at a10d9e77cf. Regeneration is described
|
||||||
|
# at the test that reads this file: "Toolchange temperature commands are unchanged
|
||||||
|
# when the wipe tower wait is off" in tests/fff_print/test_multifilament.cpp.
|
||||||
|
M104 S215 T0 ; set nozzle temperature
|
||||||
|
M104 S215 T1 ; set nozzle temperature
|
||||||
|
; CP PRIMING START
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S215 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
M104 S175 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S215 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP PRIMING END
|
||||||
|
M104 S215 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S175 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S215 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; set nozzle temperature
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.0s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.4s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 31s lead 30.9s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 31s lead 30.7s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 31s lead 30.6s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.3s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 31s lead 30.6s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.0s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.2s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 31s lead 30.7s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.4s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.4s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T0 ; preheat T0 time: 30s lead 30.0s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T1 ; set nozzle temperature ;cooldown
|
||||||
|
T0 ; change extruder
|
||||||
|
M109 S240 T0 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S240 T1 ; preheat T1 time: 30s lead 30.0s
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
M104 S200 T0 ; set nozzle temperature ;cooldown
|
||||||
|
T1 ; change extruder
|
||||||
|
M109 S240 T1 ; set nozzle temperature and wait for it to be reached
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
; CP TOOLCHANGE START
|
||||||
|
; CP TOOLCHANGE END
|
||||||
|
M104 S0 ; turn off temperature
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
#include <catch2/catch_all.hpp>
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
#include "libslic3r/GCodeReader.hpp"
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
using namespace Slic3r::Test;
|
using namespace Slic3r::Test;
|
||||||
@@ -27,6 +39,156 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
|
|||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
|
||||||
|
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
|
||||||
|
static std::vector<double> wait_park_xs(const std::string& gcode)
|
||||||
|
{
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::istringstream stream(gcode);
|
||||||
|
for (std::string line; std::getline(stream, line);)
|
||||||
|
lines.emplace_back(std::move(line));
|
||||||
|
std::vector<double> xs;
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (lines[i].rfind("M109", 0) != 0 || lines[i].find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos)
|
||||||
|
continue;
|
||||||
|
for (size_t j = i; j-- > 0;) {
|
||||||
|
if (lines[j].rfind("G1 ", 0) != 0)
|
||||||
|
continue;
|
||||||
|
const size_t x_pos = lines[j].find('X');
|
||||||
|
if (x_pos == std::string::npos)
|
||||||
|
continue;
|
||||||
|
xs.push_back(std::stod(lines[j].substr(x_pos + 1)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimated print time at each 1-based line of an exported G-code file, from a second
|
||||||
|
// GCodeProcessor pass over it. MoveVertex::time is the duration of one move and gcode_id is the
|
||||||
|
// line it came from (already rebased past the M73 insertions), so the running sum before the first
|
||||||
|
// move of a line is the elapsed time at that line. The file carries its own config footer, so
|
||||||
|
// process_file configures the processor -- including the shared s_IsBBLPrinter static that other
|
||||||
|
// tests in this binary mutate -- from the settings the export itself used.
|
||||||
|
static std::vector<double> elapsed_time_by_line(const std::string& gcode)
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile temp_gcode(".gcode");
|
||||||
|
{
|
||||||
|
std::ofstream os(temp_gcode.string());
|
||||||
|
os << gcode;
|
||||||
|
}
|
||||||
|
GCodeProcessor processor;
|
||||||
|
processor.process_file(temp_gcode.string());
|
||||||
|
|
||||||
|
constexpr size_t NORMAL = size_t(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||||
|
const size_t n_lines = size_t(std::count(gcode.begin(), gcode.end(), '\n')) + 2;
|
||||||
|
std::vector<double> elapsed(n_lines, 0.);
|
||||||
|
double running = 0.;
|
||||||
|
size_t next = 0;
|
||||||
|
for (const auto& move : processor.get_result().moves) {
|
||||||
|
const size_t id = std::min<size_t>(move.gcode_id, n_lines - 1);
|
||||||
|
while (next <= id)
|
||||||
|
elapsed[next++] = running;
|
||||||
|
running += move.time[NORMAL];
|
||||||
|
}
|
||||||
|
while (next < n_lines)
|
||||||
|
elapsed[next++] = running;
|
||||||
|
return elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The temperature-relevant projection of `gcode`: every M104/M109/Tn line, plus the toolchange and
|
||||||
|
// priming markers that anchor them, in order. A preheat -- an M104 the GCodeProcessor backtrace
|
||||||
|
// inserts mid-object, outside any block, naming a tool other than the one currently loaded -- also
|
||||||
|
// carries "lead <n>s", the estimated time from there to the tool change it heats for, which is the
|
||||||
|
// property preheat_time controls. No other temperature command gets one: for an M104 retargeting
|
||||||
|
// the active tool (the first-layer-to-other-layers bump) or one inside a block, the distance to the
|
||||||
|
// next Tn is a layer time or a handful of moves and says nothing about preheat_time. Everything
|
||||||
|
// else is dropped, so the trace does not move when travel, tower geometry or line numbering do.
|
||||||
|
static std::vector<std::string> temperature_trace(const std::string& gcode)
|
||||||
|
{
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::istringstream stream(gcode);
|
||||||
|
for (std::string line; std::getline(stream, line);) {
|
||||||
|
line.erase(0, line.find_first_not_of(" \t"));
|
||||||
|
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||||
|
line.pop_back();
|
||||||
|
lines.emplace_back(std::move(line));
|
||||||
|
}
|
||||||
|
const std::vector<double> elapsed = elapsed_time_by_line(gcode);
|
||||||
|
|
||||||
|
const auto is_tool = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char) l[1]); };
|
||||||
|
const auto is_temp = [](const std::string& l) { return l.rfind("M104", 0) == 0 || l.rfind("M109", 0) == 0; };
|
||||||
|
const auto marker = [](const std::string& l) -> const char* {
|
||||||
|
for (const char* m : { "; CP TOOLCHANGE START", "; CP TOOLCHANGE END", "; CP PRIMING START", "; CP PRIMING END" })
|
||||||
|
if (l.find(m) != std::string::npos)
|
||||||
|
return m;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tool a "T<n>" line, or the "T<n>" argument of an M104, names -- or -1 when it names none.
|
||||||
|
const auto tool_of = [&is_tool](const std::string& l) -> int {
|
||||||
|
size_t t = std::string::npos; // index of the 'T'
|
||||||
|
if (is_tool(l))
|
||||||
|
t = 0;
|
||||||
|
else if (l.rfind("M104", 0) == 0 && l.find(" T") != std::string::npos)
|
||||||
|
t = l.find(" T") + 1;
|
||||||
|
if (t == std::string::npos || t + 1 >= l.size() || !std::isdigit((unsigned char) l[t + 1]))
|
||||||
|
return -1;
|
||||||
|
return std::stoi(l.substr(t + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::string> trace;
|
||||||
|
bool in_block = false;
|
||||||
|
int current_tool = -1;
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (const char* m = marker(lines[i])) {
|
||||||
|
in_block = std::string(m).find("START") != std::string::npos;
|
||||||
|
trace.emplace_back(m); // the marker alone: some carry a trailing tool id, some do not
|
||||||
|
} else if (is_tool(lines[i]) || is_temp(lines[i])) {
|
||||||
|
std::string entry = lines[i];
|
||||||
|
const int named = tool_of(lines[i]);
|
||||||
|
if (!in_block && lines[i].rfind("M104", 0) == 0 && current_tool != -1 && named != -1 && named != current_tool) {
|
||||||
|
size_t tn = i;
|
||||||
|
while (tn < lines.size() && !is_tool(lines[tn]))
|
||||||
|
++tn;
|
||||||
|
if (tn < lines.size()) {
|
||||||
|
char lead[32];
|
||||||
|
std::snprintf(lead, sizeof(lead), "\tlead %.1fs", elapsed[tn + 1] - elapsed[i + 1]);
|
||||||
|
entry += lead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_tool(lines[i]))
|
||||||
|
current_tool = named;
|
||||||
|
trace.emplace_back(std::move(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits a trace entry into its command text and the lead time appended after a tab, if any.
|
||||||
|
static std::pair<std::string, std::optional<double>> split_lead(const std::string& entry)
|
||||||
|
{
|
||||||
|
const size_t tab = entry.find('\t');
|
||||||
|
if (tab == std::string::npos)
|
||||||
|
return { entry, std::nullopt };
|
||||||
|
const std::string tail = entry.substr(tab + 1); // "lead 30.2s"
|
||||||
|
return { entry.substr(0, tab), std::stod(tail.substr(tail.find(' ') + 1)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same command, and a lead time within half a second. The lead is an estimate summed over every
|
||||||
|
// move before it, so it drifts slightly with unrelated changes to travel or tower geometry; half a
|
||||||
|
// second is far below the tens of seconds a preheat leaving its backtrace position would shift it.
|
||||||
|
static bool trace_entries_match(const std::string& a, const std::string& b)
|
||||||
|
{
|
||||||
|
const auto x = split_lead(a);
|
||||||
|
const auto y = split_lead(b);
|
||||||
|
if (x.first != y.first)
|
||||||
|
return false;
|
||||||
|
if (x.second.has_value() != y.second.has_value())
|
||||||
|
return false;
|
||||||
|
return !x.second.has_value() || std::abs(*x.second - *y.second) <= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
// Tool index = filament id - 1; brim and skirt follow the wall filament.
|
// Tool index = filament id - 1; brim and skirt follow the wall filament.
|
||||||
TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]")
|
TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]")
|
||||||
{
|
{
|
||||||
@@ -86,6 +248,389 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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. The restore that used to block there
|
||||||
|
// demotes to a non-blocking M104 and moves ahead of the Tn, so the incoming tool heats up over
|
||||||
|
// the change itself. 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 },
|
||||||
|
// The post-processor's own preheat pass also inserts an M104 for the incoming
|
||||||
|
// filament ahead of the Tn; switch it off so the temperature commands under test
|
||||||
|
// are the only ones in the toolchange block.
|
||||||
|
{ "preheat_time", 0 },
|
||||||
|
{ "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<std::string> lines;
|
||||||
|
std::istringstream gcode_stream(gcode);
|
||||||
|
for (std::string line; std::getline(gcode_stream, line);)
|
||||||
|
lines.emplace_back(std::move(line));
|
||||||
|
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; };
|
||||||
|
// A non-blocking set-temperature naming one specific tool, e.g. "M104 S255 T1".
|
||||||
|
const auto is_m104_for_tool = [](const std::string& l, int tool) {
|
||||||
|
if (l.rfind("M104", 0) != 0)
|
||||||
|
return false;
|
||||||
|
const std::string token = " T" + std::to_string(tool);
|
||||||
|
const size_t at = l.find(token);
|
||||||
|
return at != std::string::npos && !std::isdigit((unsigned char)l[at + token.size()]);
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Where the incoming tool's target temperature is raised, relative to its Tn.
|
||||||
|
const int new_tool = std::stoi(lines[tool_line].substr(1));
|
||||||
|
size_t preheat = tool_line, restore = block_end;
|
||||||
|
for (size_t j = i; j < tool_line; ++j)
|
||||||
|
if (is_m104_for_tool(lines[j], new_tool)) { preheat = j; break; }
|
||||||
|
for (size_t j = tool_line + 1; j < block_end; ++j)
|
||||||
|
if (is_m104_for_tool(lines[j], new_tool)) { restore = j; break; }
|
||||||
|
|
||||||
|
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);
|
||||||
|
// The target is raised ahead of the toolchange, so the incoming tool heats up
|
||||||
|
// while it is picked up, and nothing sets it again afterwards.
|
||||||
|
CHECK(preheat < tool_line);
|
||||||
|
CHECK(restore == 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<double>::max(), purge_max_x = std::numeric_limits<double>::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, and
|
||||||
|
// nothing raises the incoming tool's target before it.
|
||||||
|
REQUIRE(untagged_m109 < block_end);
|
||||||
|
CHECK(tagged_wait == block_end);
|
||||||
|
CHECK(preheat == tool_line);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priming runs before the first layer is set up, so set_extruder sees no layer at all: its
|
||||||
|
// on_first_layer() test is false and print_z is the initial layer height rather than 0. The
|
||||||
|
// tower nonetheless blocks on the first layer temperature there, so the pre-heat raised ahead
|
||||||
|
// of each priming Tn has to name that same temperature — pre-heating to the "other layers"
|
||||||
|
// value instead leaves the tagged M109 asking the firmware to cool back down before the
|
||||||
|
// priming lines are extruded.
|
||||||
|
TEST_CASE("Wipe tower priming pre-heats to the first layer temperature", "[MultiFilament]")
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
{ "single_extruder_multi_material_priming", 1 },
|
||||||
|
{ "enable_prime_tower", 1 },
|
||||||
|
{ "prime_tower_width", 35 },
|
||||||
|
{ "wipe_tower_x", "50" },
|
||||||
|
{ "wipe_tower_y", "50" },
|
||||||
|
{ "preheat_time", 0 }, // see the wait test above
|
||||||
|
// Distinct enough that picking the wrong one is unambiguous.
|
||||||
|
{ "nozzle_temperature_initial_layer", "215,215" },
|
||||||
|
{ "nozzle_temperature", "240,240" },
|
||||||
|
{ "wait_for_temp_on_wipe_tower", 1 },
|
||||||
|
}),
|
||||||
|
{ { { "extruder", 1 } }, { { "extruder", 2 } } });
|
||||||
|
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::istringstream gcode_stream(gcode);
|
||||||
|
for (std::string line; std::getline(gcode_stream, line);)
|
||||||
|
lines.emplace_back(std::move(line));
|
||||||
|
// Temperature of an M104/M109, or -1 when the line is neither.
|
||||||
|
const auto temp_of = [](const std::string& l) {
|
||||||
|
if (l.rfind("M104", 0) != 0 && l.rfind("M109", 0) != 0)
|
||||||
|
return -1;
|
||||||
|
const size_t s = l.find('S');
|
||||||
|
return s == std::string::npos ? -1 : std::stoi(l.substr(s + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t start = lines.size(), end = lines.size();
|
||||||
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
|
if (start == lines.size() && lines[i].find("; CP PRIMING START") != std::string::npos)
|
||||||
|
start = i;
|
||||||
|
else if (start < lines.size() && lines[i].find("; CP PRIMING END") != std::string::npos) {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(start < end);
|
||||||
|
|
||||||
|
int checked_waits = 0;
|
||||||
|
for (size_t i = start; i < end; ++i) {
|
||||||
|
if (lines[i].find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos)
|
||||||
|
continue;
|
||||||
|
++checked_waits;
|
||||||
|
INFO("priming wait at line " << i + 1 << ": " << lines[i]);
|
||||||
|
CHECK(temp_of(lines[i]) == 215); // the tower waits on the first layer temperature
|
||||||
|
// The most recent set-temperature before it is the pre-heat, and must agree with it.
|
||||||
|
int preheat = -1;
|
||||||
|
for (size_t j = i; j-- > start;)
|
||||||
|
if ((preheat = temp_of(lines[j])) != -1)
|
||||||
|
break;
|
||||||
|
CHECK(preheat == 215);
|
||||||
|
}
|
||||||
|
REQUIRE(checked_waits > 0); // the feature under test is active
|
||||||
|
}
|
||||||
|
|
||||||
|
// The temperature-wait park picks its side of the tower by testing bed containment with the
|
||||||
|
// tower position at psWipeTower generation time, while WipeTowerIntegration shifts the cached
|
||||||
|
// moves by the CURRENT position at export. Moving the tower normally invalidates only
|
||||||
|
// psSkirtBrim (tower gcode is position-independent), but the park makes it bed-relative, so a
|
||||||
|
// GUI-style move-and-reslice on the same Print must regenerate the tower — otherwise the stale
|
||||||
|
// park prints outside the bed. Contract: every tagged wait parks inside the printable area.
|
||||||
|
TEST_CASE("Wipe tower temperature-wait park is regenerated when the tower moves", "[MultiFilament]")
|
||||||
|
{
|
||||||
|
// Two objects, one filament each: a toolchange (and a tagged wait) on every layer, like
|
||||||
|
// the wait test above — but on a single-extruder machine profile: the synthetic
|
||||||
|
// dual-extruder keys would drag in the extruder-variant expansion, which is not
|
||||||
|
// idempotent on the default machine profile and would pollute the re-apply diff below.
|
||||||
|
// Rectangle wall and no brim keep the tower-local footprint inside [0, 35], so the park
|
||||||
|
// sits at the generator's 2mm side gap: local -2 or 37.
|
||||||
|
DynamicPrintConfig config = multifilament_config(2, {
|
||||||
|
{ "single_extruder_multi_material", 0 },
|
||||||
|
{ "enable_prime_tower", 1 },
|
||||||
|
{ "prime_tower_width", 35 },
|
||||||
|
{ "wipe_tower_wall_type", "rectangle" }, // the default rib bulges past the width
|
||||||
|
{ "prime_tower_brim_width", 0 }, // the default 3 widens the first-layer envelope
|
||||||
|
{ "printable_area", "0x0,200x0,200x200,0x200" },
|
||||||
|
{ "wipe_tower_x", "0" },
|
||||||
|
{ "wipe_tower_y", "50" },
|
||||||
|
{ "ooze_prevention", 1 },
|
||||||
|
{ "standby_temperature_delta", -40 },
|
||||||
|
{ "wait_for_temp_on_wipe_tower", 1 },
|
||||||
|
});
|
||||||
|
// init_print force-sets this on its own copy; set it here too so the re-apply below
|
||||||
|
// diffs in wipe_tower_x ONLY — the exact GUI increment under test.
|
||||||
|
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{
|
||||||
|
{ { "extruder", 1 } }, { { "extruder", 2 } } }; // object-level, see the wait test above
|
||||||
|
init_print(std::vector<TriangleMesh>{ cube(20), cube(20) }, print, model, config, &overrides);
|
||||||
|
|
||||||
|
const std::string at_edge = gcode(print);
|
||||||
|
const std::vector<double> at_edge_parks = wait_park_xs(at_edge);
|
||||||
|
REQUIRE(!at_edge_parks.empty()); // the feature under test is active
|
||||||
|
for (double x : at_edge_parks) {
|
||||||
|
INFO("wait park X " << x << " with the tower at x=0 on a 200mm bed");
|
||||||
|
CHECK(x >= -0.05);
|
||||||
|
CHECK(x <= 200.05);
|
||||||
|
}
|
||||||
|
REQUIRE(print.is_step_done(psWipeTower));
|
||||||
|
|
||||||
|
// Move the tower to the right bed edge (164 + 35 = 199 keeps the body printable) and
|
||||||
|
// re-apply on the SAME Print, as the GUI does. Base the re-apply on the print's own
|
||||||
|
// resolved config so the diff is wipe_tower_x alone — re-applying the caller's config
|
||||||
|
// would also diff the apply-time extruder normalization write-backs, and those keys
|
||||||
|
// regenerate the tower for the wrong reason. The cached right-side park would export
|
||||||
|
// at 164 + 37 = 201, off the bed; regeneration clamps the park against the bed edge.
|
||||||
|
// Assemble the moved config exactly the way init_print assembled the first one — the
|
||||||
|
// apply-time normalization is only idempotent when both applies start from the same
|
||||||
|
// derivation, and any stray diff key would regenerate the tower for the wrong reason.
|
||||||
|
config.set_deserialize_strict({ { "wipe_tower_x", "164" } });
|
||||||
|
DynamicPrintConfig moved_config = DynamicPrintConfig::full_print_config();
|
||||||
|
moved_config.apply(config);
|
||||||
|
moved_config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||||
|
print.apply(model, moved_config);
|
||||||
|
CHECK_FALSE(print.is_step_done(psWipeTower)); // the move must re-generate the tower
|
||||||
|
|
||||||
|
const std::string moved = gcode(print);
|
||||||
|
const std::vector<double> moved_parks = wait_park_xs(moved);
|
||||||
|
REQUIRE(!moved_parks.empty()); // the waits must survive the re-slice
|
||||||
|
for (double x : moved_parks) {
|
||||||
|
INFO("wait park X " << x << " with the tower at x=164 on a 200mm bed");
|
||||||
|
CHECK(x >= -0.05);
|
||||||
|
CHECK(x <= 200.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The flag-off half of the three tests above. Every site wait_for_temp_on_wipe_tower touches is
|
||||||
|
// guarded -- set_extruder's pre-toolchange preheat block and its post_toolchange skip,
|
||||||
|
// toolchange_Change's park, the interface-temp guard in WipeTower2::tool_change, and append_tcr2's
|
||||||
|
// tagged-M109 filter -- so with the option off the feature has to be inert and temperature emission
|
||||||
|
// has to stay exactly as it was before the option existed. That is pinned against a trace captured
|
||||||
|
// from main rather than against expectations written from the current code, which would be
|
||||||
|
// re-derived from the very code they are meant to guard.
|
||||||
|
//
|
||||||
|
// Note what main emits here, since it is easy to misread as a missing wait: with preheat_time set,
|
||||||
|
// the toolchange carries no blocking M109 at all. GCodeProcessor's backtrace moves the heat-up to
|
||||||
|
// an M104 preheat_time seconds earlier and demotes the in-place command, which is the entire point
|
||||||
|
// of preheating. The lead times below are what pin that placement.
|
||||||
|
TEST_CASE("Toolchange temperature commands are unchanged when the wipe tower wait is off", "[MultiFilament][Regression]")
|
||||||
|
{
|
||||||
|
// 20x20x5 cubes at the default 0.2mm layer height are 25 layers, one filament each, so there is
|
||||||
|
// a toolchange -- and a preheat ahead of it -- on every layer.
|
||||||
|
const std::string gcode = slice_with_object_overrides(
|
||||||
|
{ make_cube(20., 20., 5.), make_cube(20., 20., 5.) },
|
||||||
|
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 },
|
||||||
|
{ "single_extruder_multi_material_priming", 1 }, // reaches toolchange_Change's priming path
|
||||||
|
{ "enable_prime_tower", 1 },
|
||||||
|
{ "prime_tower_width", 35 },
|
||||||
|
{ "wipe_tower_x", "50" },
|
||||||
|
{ "wipe_tower_y", "50" },
|
||||||
|
// GCodeProcessor::apply_config enables the preheat backtrace on
|
||||||
|
// ooze_prevention && preheat_time > 0 && !SEMM && filaments > 1. That is what puts an
|
||||||
|
// M104 preheat_time seconds ahead of every Tn, and it also gives set_extruder's
|
||||||
|
// standby/restore pair, which the option demotes and moves when it is on.
|
||||||
|
{ "ooze_prevention", 1 },
|
||||||
|
{ "standby_temperature_delta", -40 },
|
||||||
|
{ "preheat_time", 30 },
|
||||||
|
{ "preheat_steps", 1 },
|
||||||
|
// enable_tower_interface_features is deliberately left off: the interface temperature
|
||||||
|
// is observable only through a change_filament_gcode template that reads
|
||||||
|
// new_filament_temp, since append_tcr2 strips the tower's own M109 for it, and the
|
||||||
|
// default template here has none. The option's interface-temp guard is covered by the
|
||||||
|
// enabled-path tests above instead.
|
||||||
|
//
|
||||||
|
// Distinct enough that a wrong pick between the two is unambiguous in the trace.
|
||||||
|
{ "nozzle_temperature_initial_layer", "215,215" },
|
||||||
|
{ "nozzle_temperature", "240,240" },
|
||||||
|
{ "wait_for_temp_on_wipe_tower", 0 },
|
||||||
|
}),
|
||||||
|
// Object-level, so the used-filament count that gates the prime tower is derived from it.
|
||||||
|
{ { { "extruder", 1 } }, { { "extruder", 2 } } });
|
||||||
|
|
||||||
|
const std::vector<std::string> trace = temperature_trace(gcode);
|
||||||
|
REQUIRE(trace.size() > 1);
|
||||||
|
CHECK(gcode.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos);
|
||||||
|
|
||||||
|
const std::string golden_path = std::string(TEST_DATA_DIR PATH_SEPARATOR "wipe_tower_temperature_trace_main.txt");
|
||||||
|
|
||||||
|
// Regenerate by appending this test and its helpers to the same file on main (dropping the
|
||||||
|
// wait_for_temp_on_wipe_tower key, which main's config does not know), rebuilding
|
||||||
|
// fff_print_tests there, running it with ORCA_UPDATE_WIPE_TOWER_TEMP_TRACE=1, copying the file
|
||||||
|
// it writes back here, and filling in the commit it was captured from.
|
||||||
|
if (std::getenv("ORCA_UPDATE_WIPE_TOWER_TEMP_TRACE") != nullptr) {
|
||||||
|
std::ofstream out(golden_path);
|
||||||
|
REQUIRE(out.good());
|
||||||
|
out << "# Temperature and tool-change commands of a wait_for_temp_on_wipe_tower-off slice,\n"
|
||||||
|
"# captured from the main branch at <fill in the commit>. Regeneration is described\n"
|
||||||
|
"# at the test that reads this file: \"Toolchange temperature commands are unchanged\n"
|
||||||
|
"# when the wipe tower wait is off\" in tests/fff_print/test_multifilament.cpp.\n";
|
||||||
|
for (const std::string& entry : trace)
|
||||||
|
out << entry << "\n";
|
||||||
|
WARN("Rewrote " << golden_path << " from this run; it no longer reflects main.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> golden;
|
||||||
|
{
|
||||||
|
std::ifstream in(golden_path);
|
||||||
|
INFO("reading " << golden_path);
|
||||||
|
REQUIRE(in.good());
|
||||||
|
for (std::string line; std::getline(in, line);) {
|
||||||
|
if (!line.empty() && line.back() == '\r')
|
||||||
|
line.pop_back();
|
||||||
|
if (!line.empty() && line[0] != '#')
|
||||||
|
golden.push_back(std::move(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REQUIRE(!golden.empty());
|
||||||
|
|
||||||
|
const size_t common = std::min(trace.size(), golden.size());
|
||||||
|
for (size_t i = 0; i < common; ++i) {
|
||||||
|
if (trace_entries_match(trace[i], golden[i]))
|
||||||
|
continue;
|
||||||
|
// Report the first difference only: past it the two are misaligned and every later entry
|
||||||
|
// would be reported as a difference too.
|
||||||
|
INFO("first difference at trace entry " << i + 1);
|
||||||
|
INFO(" main: " << golden[i]);
|
||||||
|
INFO(" branch: " << trace[i]);
|
||||||
|
FAIL("temperature emission differs from main with wait_for_temp_on_wipe_tower off");
|
||||||
|
}
|
||||||
|
CHECK(trace.size() == golden.size());
|
||||||
|
}
|
||||||
|
|
||||||
// max_layer_height can be shorter than the extruder count (normalization sizes it to the
|
// 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
|
// 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;
|
// indexed it per-nozzle and read past the end. Shortened directly here to isolate that read;
|
||||||
@@ -104,3 +649,4 @@ TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height",
|
|||||||
init_and_process_print({ cube(20) }, print, config);
|
init_and_process_print({ cube(20) }, print, config);
|
||||||
REQUIRE_FALSE(print.objects().front()->layers().empty());
|
REQUIRE_FALSE(print.objects().front()->layers().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user