diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index a6c4355910..c2ccc4db26 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -39,7 +39,13 @@ on: - '[0-9].[0-9][0-9]*' jobs: + check_actor: + uses: ultimaker/cura-workflows/.github/workflows/check-actor.yml@main + secrets: inherit + conan-package: + needs: [check_actor] + if: needs.check_actor.outputs.proceed == 'true' uses: ultimaker/cura-workflows/.github/workflows/conan-package.yml@main with: platform_wasm: true diff --git a/include/ExtruderPlan.h b/include/ExtruderPlan.h index f0158408a6..d3ae678530 100644 --- a/include/ExtruderPlan.h +++ b/include/ExtruderPlan.h @@ -37,6 +37,7 @@ class ExtruderPlan friend class LayerPlanBuffer; friend class LayerPlan; #ifdef BUILD_TESTS + friend class LayerPlanTest; friend class ExtruderPlanPathsParameterizedTest; FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationZeroIsUncompensated); FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationFull); diff --git a/include/LayerPlan.h b/include/LayerPlan.h index 4419c7f37d..33e47153a6 100644 --- a/include/LayerPlan.h +++ b/include/LayerPlan.h @@ -60,6 +60,7 @@ class LayerPlan : public NoCopy friend class LayerPlanBuffer; #ifdef BUILD_TESTS friend class AddTravelTest; + friend class LayerPlanTest; friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; friend class AntiOozeAmountsTest; FRIEND_TEST(AntiOozeAmountsTest, ComputeAntiOozeAmounts); @@ -151,6 +152,12 @@ class LayerPlan : public NoCopy bool first_travel_destination_is_inside_; //!< Whether the destination of the first planned travel move is inside a layer part std::optional> first_extrusion_acc_jerk_; //!< The acceleration and jerk rates of the first extruded move (if this layer is not empty). std::optional> next_layer_acc_jerk_; //!< If there is a next layer, the first acceleration and jerk it starts with. + struct NextLayerHopReturn + { + Velocity fastest_speed; + coord_t z; + }; + std::optional next_layer_hop_return_; //!< Conservative timing for a cool-lift hop that ends on the next layer. bool was_inside_; //!< Whether the last planned (extrusion) move was inside a layer part bool is_inside_; //!< Whether the destination of the next planned travel move is inside a layer part Shape comb_boundary_minimum_; //!< The minimum boundary within which to comb, or to move into when performing a retraction. diff --git a/include/gcode_export/gcodeExport.h b/include/gcode_export/gcodeExport.h index c17ffa8f93..4eb8f7ca08 100644 --- a/include/gcode_export/gcodeExport.h +++ b/include/gcode_export/gcodeExport.h @@ -45,6 +45,7 @@ class GCodeExport : public NoCopy friend class GCodeExportTest; friend class GriffinHeaderTest; friend class AntiOozeAmountsTest; + friend class LayerPlanTest; FRIEND_TEST(GCodeExportTest, CommentEmpty); FRIEND_TEST(GCodeExportTest, CommentSimple); FRIEND_TEST(GCodeExportTest, CommentMultiLine); diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index cedb79c927..784c8cd7c4 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -36,6 +36,7 @@ #include "plugins/slots.h" #include "raft.h" // getTotalExtraLayers #include "range/v3/view/chunk_by.hpp" +#include "settings/types/Duration.h" #include "settings/types/Ratio.h" #include "sliceDataStorage.h" #include "utils/Simplify.h" @@ -4047,12 +4048,39 @@ void LayerPlan::writeGCode(GCodeExport& gcode) const RetractionAndWipeConfig& actual_retraction_config = current_mesh ? current_mesh->retraction_wipe_config : storage_.retraction_wipe_config_per_extruder[gcode.getExtruderNr()]; gcode.writeRetraction(actual_retraction_config.retraction_config); + Duration cool_lift_travel_time{ 0.0 }; if (extruder_plan_idx == extruder_plans_.size() - 1 || ! extruder.settings_.get("machine_extruder_end_pos_abs")) { // only do the z-hop if it's the last extruder plan; otherwise it's already at the switching bay area // or do it anyway when we switch extruder in-place - gcode.writeZhopStart(MM2INT(3.0)); + constexpr coord_t hop_height = MM2INT(3.0); + gcode.writeZhopStart(hop_height); + const Velocity hop_speed = extruder.settings_.get("speed_z_hop"); + if (hop_speed > 0.0) + { + Velocity fastest_return_speed = hop_speed; + const auto& extruders = Application::getInstance().current_slice_->scene.extruders; + for (size_t next_idx = extruder_plan_idx + 1; next_idx < extruder_plans_.size(); ++next_idx) + { + fastest_return_speed = std::max(fastest_return_speed, extruders[extruder_plans_[next_idx].extruder_nr_].settings_.get("speed_z_hop")); + } + + coord_t return_distance = hop_height; + if (next_layer_hop_return_) + { + fastest_return_speed = std::max(fastest_return_speed, next_layer_hop_return_->fastest_speed); + return_distance = std::max(0, hop_height - std::max(0, next_layer_hop_return_->z - z_)); + } + + // A later plan may end the hop after switching extruders. Credit only the + // return time guaranteed by its distance and the fastest possible hop speed. + cool_lift_travel_time = Duration(INT2MM(hop_height) / static_cast(hop_speed) + INT2MM(return_distance) / static_cast(fastest_return_speed)); + } + } + const Duration remaining_dwell = Duration(extruder_plan.extra_time_) - cool_lift_travel_time; + if (remaining_dwell > 0.0) + { + gcode.writeDelay(remaining_dwell); } - gcode.writeDelay(extruder_plan.extra_time_); } extruder_plan.handleAllRemainingInserts(gcode); diff --git a/src/LayerPlanBuffer.cpp b/src/LayerPlanBuffer.cpp index 5bf6bb57b9..f3794d43b4 100644 --- a/src/LayerPlanBuffer.cpp +++ b/src/LayerPlanBuffer.cpp @@ -3,6 +3,8 @@ #include "LayerPlanBuffer.h" +#include + #include #include @@ -124,6 +126,21 @@ void LayerPlanBuffer::addConnectingTravelMove(LayerPlan* prev_layer, const Layer return; } + const auto& extruders = Application::getInstance().current_slice_->scene.extruders; + Velocity fastest_hop_speed{ 0.0 }; + for (const ExtruderPlan& plan : newest_layer->extruder_plans_) + { + fastest_hop_speed = std::max(fastest_hop_speed, extruders[plan.extruder_nr_].settings_.get("speed_z_hop")); + } + const size_t first_extruder = newest_layer->extruder_plans_.front().extruder_nr_; + const WipeScriptConfig& wipe_config = newest_layer->storage_.retraction_wipe_config_per_extruder[first_extruder].wipe_config; + // A layer-start wipe can replace the pending hop at its own speed. + if (wipe_config.clean_between_layers && wipe_config.hop_enable) + { + fastest_hop_speed = std::max(fastest_hop_speed, wipe_config.hop_speed); + } + prev_layer->next_layer_hop_return_ = LayerPlan::NextLayerHopReturn{ fastest_hop_speed, newest_layer->z_ }; + Point2LL first_location_new_layer = new_layer_destination_state->first; assert(newest_layer->extruder_plans_.front().paths_[0].points.size() == 1); diff --git a/tests/LayerPlanTest.cpp b/tests/LayerPlanTest.cpp index 8b381a15b7..49090aa000 100644 --- a/tests/LayerPlanTest.cpp +++ b/tests/LayerPlanTest.cpp @@ -4,14 +4,19 @@ #include "LayerPlan.h" //The code under test. #include +#include #include #include "Application.h" //To provide settings for the layer plan. #include "RetractionConfig.h" //To provide retraction settings. #include "Slice.h" //To provide settings for the layer plan. +#include "arcus/MockCommunication.h" //To prevent calls to any missing Communication class. +#include "gcode_export/GCodePart.h" //To read the written g-code parts. +#include "gcode_export/gcodeExport.h" //To capture the dwell written for a small layer. #include "pathPlanning/Comb.h" //To create a combing path around the layer plan. #include "pathPlanning/NozzleTempInsert.h" //To provide nozzle temperature commands. +#include "settings/types/Duration.h" #include "sliceDataStorage.h" //To provide slice data as input for the planning stage. #include "utils/Coord_t.h" @@ -231,6 +236,52 @@ class LayerPlanTest : public testing::Test { delete storage; } + + /*! + * Write one extruder plan whose only cooling action is cool_lift_head. + * \param extra_time Pause still required after slowing down, in seconds. + * \param hop_speed_mm_per_s Commanded Z hop speed. + * \param next_hop_speed_mm_per_s Fastest possible return speed on the next layer, if known. + * \return G-code produced for that plan. + */ + std::string writeCoolLiftHead(const double extra_time, const char* hop_speed_mm_per_s, const double next_hop_speed_mm_per_s = 0.0) + { + settings->add("flow_rate_max_extrusion_offset", "0"); + settings->add("flow_rate_extrusion_offset_factor", "0"); + settings->add("build_volume_fan_nr", "0"); + settings->add("acceleration_enabled", "false"); + settings->add("acceleration_travel_enabled", "false"); + settings->add("jerk_enabled", "false"); + settings->add("jerk_travel_enabled", "false"); + settings->add("coasting_enable", "false"); + settings->add("retract_at_layer_change", "false"); + settings->add("cool_lift_head", "true"); + settings->add("machine_extruder_end_pos_abs", "false"); + settings->add("speed_z_hop", hop_speed_mm_per_s); + + storage->retraction_wipe_config_per_extruder[0].wipe_config.clean_between_layers = false; + storage->retraction_wipe_config_per_extruder[0].retraction_config.distance = 0; + layer_plan.extruder_plans_[0].paths_.clear(); + layer_plan.extruder_plans_[0].extra_time_ = extra_time; + if (next_hop_speed_mm_per_s > 0.0) + { + layer_plan.next_layer_hop_return_ = LayerPlan::NextLayerHopReturn{ Velocity(next_hop_speed_mm_per_s), layer_plan.z_ + MM2INT(0.1) }; + } + + Application::getInstance().communication_ = std::make_shared(); + + GCodeExport gcode; + gcode.setZ(layer_plan.z_); + gcode.writeTravel(gcode.getPositionXY(), settings->get("speed_z_hop")); + layer_plan.writeGCode(gcode); + + std::stringstream written; + for (const std::shared_ptr& part : gcode.gcode_parts_) + { + written << part->str(); + } + return written.str(); + } }; // Test all combinations of these settings in parameterised tests. @@ -672,6 +723,47 @@ TEST_F(OverhangSpeedTest, SpeedFactorSplitAtOverhangBoundary) EXPECT_TRUE(found_reduced_speed) << "Segment crossing into overhang should have reduced speed"; } +TEST_F(LayerPlanTest, CoolLiftHeadDwellSubtractsRoundTripHopTime) +{ + // 3 mm up and 3 mm down at 10 mm/s is 0.6 s, so a 5 s pause dwells 4.4 s. + const std::string gcode = writeCoolLiftHead(5.0, "10"); + const Duration hop_time(2.0 * INT2MM(MM2INT(3.0)) / 10.0); + const int dwell_ms = static_cast((Duration(5.0) - hop_time) * 1000.0); + + const size_t layer_z = gcode.find(" Z10\n"); + ASSERT_NE(layer_z, std::string::npos) << gcode; + EXPECT_NE(gcode.find("G1 Z13\n", layer_z), std::string::npos) << gcode; + EXPECT_NE(gcode.find("G4 P" + std::to_string(dwell_ms)), std::string::npos) << gcode; + EXPECT_EQ(gcode.find("G4 P5000"), std::string::npos) << gcode; + EXPECT_LT(dwell_ms, 5000); + EXPECT_GT(dwell_ms, 4000); +} + +TEST_F(LayerPlanTest, CoolLiftHeadSkipsDwellWhenHopCoversExtraTime) +{ + // Round trip at 10 mm/s is 0.6 s, which already covers a 0.4 s pause. + const std::string gcode = writeCoolLiftHead(0.4, "10"); + + const size_t layer_z = gcode.find(" Z10\n"); + ASSERT_NE(layer_z, std::string::npos) << gcode; + EXPECT_NE(gcode.find("G1 Z13\n", layer_z), std::string::npos) << gcode; + EXPECT_EQ(gcode.find("G4 "), std::string::npos) << gcode; +} + +TEST_F(LayerPlanTest, CoolLiftHeadAccountsForFasterNextExtruderReturn) +{ + // The next layer is 0.1 mm higher and its extruder can return the hop at 100 mm/s. + // Only 0.3 s of ascent and at least 0.029 s of descent can count toward the pause. + const std::string gcode = writeCoolLiftHead(0.5, "10", 100.0); + const size_t dwell_pos = gcode.find("G4 P"); + + const size_t layer_z = gcode.find(" Z10\n"); + ASSERT_NE(layer_z, std::string::npos) << gcode; + EXPECT_NE(gcode.find("G1 Z13\n", layer_z), std::string::npos) << gcode; + ASSERT_NE(dwell_pos, std::string::npos) << gcode; + EXPECT_NEAR(std::stoi(gcode.substr(dwell_pos + 4)), 171, 1) << gcode; +} + TEST(NozzleTempInsertTest, SortNozzleTempInsterts) { std::vector nozzle_temp_inserts{