diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 93621648ff..b13273d696 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -5037,10 +5037,10 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; - curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, - static_cast(delta_pos[Y]) * inv_distance, - static_cast(delta_pos[Z]) * inv_distance, - static_cast(delta_pos[E]) * inv_distance); + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -5415,10 +5415,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line) if (!is_extrusion_only_move(delta_pos)) curr.enter_direction = curr.enter_direction / norm; curr.exit_direction = curr.enter_direction; - curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]) * inv_distance, - static_cast(delta_pos[Y]) * inv_distance, - static_cast(delta_pos[Z]) * inv_distance, - static_cast(delta_pos[E]) * inv_distance); + curr.jd_unit_vec = Vec4f(static_cast(delta_pos[X]), + static_cast(delta_pos[Y]), + static_cast(delta_pos[Z]), + static_cast(delta_pos[E])).normalized(); TimeBlock block; block.move_type = type; @@ -7245,6 +7245,11 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const return 0.0f; // starts from rest, the planner raises this on the reverse pass // -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin(). + // Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance + // instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter + // than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0) + // and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree + // that the corner is planned by its geometry, and normalizing matches them to within 1e-5. float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); if (junction_cos_theta > 0.999999f) return 0.0f; // the path doubles back, the machine has to stop diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index e968986695..505f7c06a0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -637,9 +637,8 @@ class Print; //For line move, there are same. For arc move, there are different. Vec3f enter_direction; Vec3f exit_direction; - // Orca: move direction over all four axes, scaled by 1 / block.distance. Used by - // calc_vmax_junction_deviation(), which needs E to see extrusion-rate changes - // between collinear moves the way Marlin and Klipper do. + // Orca: move direction over all four axes, unit length. Used by + // calc_vmax_junction_deviation(); see there for why E is normalized in. Vec4f jd_unit_vec; void reset(); diff --git a/tests/fff_print/test_gcode_timing.cpp b/tests/fff_print/test_gcode_timing.cpp index 9802bcc8f7..8c08fc4f03 100644 --- a/tests/fff_print/test_gcode_timing.cpp +++ b/tests/fff_print/test_gcode_timing.cpp @@ -468,22 +468,27 @@ FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, constexpr double junction_x = 60.0; constexpr double junction_y = 60.0; -// Two 40mm travels meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. +// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`. // 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests -// produce. Travels (no E) keep the junction vector purely geometric, as the formulas below assume. -std::string corner_gcode(double turn_deg, double orientation_deg) +// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric +// as the formulas below assume. +std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0) { const double len = 40.0; const double a_in = orientation_deg * M_PI / 180.0; const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0; + std::ostringstream extrude; + if (e_per_mm > 0.0) + extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm; std::ostringstream os; os << std::fixed << std::setprecision(4) << "M83\n" << "G1 Z0.2 F1200\n" << "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n" - << "G1 X" << junction_x << " Y" << junction_y << " F9000\n" - << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " F9000\n"; + << "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n" + << "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) + << extrude.str() << " F9000\n"; return os.str(); } @@ -492,7 +497,7 @@ std::string corner_gcode(double turn_deg, double orientation_deg) double corner_speed(const GCodeProcessorResult& r) { for (const auto& mv : r.moves) - if (mv.type == EMoveType::Travel && + if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) && std::abs(mv.position.x() - junction_x) < 1e-3 && std::abs(mv.position.y() - junction_y) < 1e-3) return mv.actual_feedrate; @@ -500,11 +505,11 @@ double corner_speed(const GCodeProcessorResult& r) } double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation, - double turn_deg, double orientation_deg = 0.0) + double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0) { GCodeProcessor proc; run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation), - corner_gcode(turn_deg, orientation_deg).c_str()); + corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str()); return corner_speed(proc.get_result()); } @@ -582,3 +587,30 @@ TEST_CASE("Junction deviation is only used where the firmware actually plans wit Catch::Matchers::WithinRel(without, 1e-4)); } } + +TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it", + "[GCodeTiming][JunctionDeviation]") +{ + // The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the + // E term makes the two paths look more parallel than they are and the corner comes out too fast, + // the more so the higher the flow. Marlin normalizes over XYZE on any extruding move + // (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether + // (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone. + const double scv = 5.0; + const double turn = 6.0; + const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn); + REQUIRE(geometric > 0.0); + + // 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one. + // Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9. + for (double e_per_mm : {0.029, 0.1}) + REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm), + Catch::Matchers::WithinRel(geometric, 0.02)); + + SECTION("and the same holds on Marlin 2") { + const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn); + REQUIRE(marlin > 0.0); + REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029), + Catch::Matchers::WithinRel(marlin, 0.02)); + } +}