diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 87849b67da..5db6940ad9 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -192,6 +192,8 @@ set(SLIC3R_SOURCES GCode/GCodeProcessor.hpp GCode/AvoidCrossingPerimeters.cpp GCode/AvoidCrossingPerimeters.hpp + GCode/Travels.cpp + GCode/Travels.hpp GCode.cpp GCode.hpp GCodeReader.cpp diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 55acf9350e..0e18895cdf 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -34,6 +34,7 @@ #include "GCode/Thumbnails.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTowerIntegration.hpp" +#include "GCode/Travels.hpp" #include "Point.hpp" #include "Polygon.hpp" #include "PrintConfig.hpp" @@ -2737,7 +2738,7 @@ std::optional GCodeGenerator::get_helical_layer_change_gcode( const double n_gon_circumference = unscaled(n_gon.length()); const double z_change{print_z - previous_layer_z}; - Points3 helix{GCode::Impl::generate_elevated_travel( + Points3 helix{GCode::Impl::Travels::generate_elevated_travel( n_gon.points, {}, previous_layer_z, @@ -3297,215 +3298,6 @@ std::string GCodeGenerator::_extrude( return gcode; } -Points3 generate_flat_travel(tcb::span xy_path, const float elevation) { - Points3 result; - result.reserve(xy_path.size() - 1); - for (const Point& point : xy_path.subspan(1)) { - result.emplace_back(point.x(), point.y(), scaled(elevation)); - } - return result; -} - -Vec2d place_at_segment(const Vec2d& current_point, const Vec2d& previous_point, const double distance) { - Vec2d direction = (current_point - previous_point).normalized(); - return previous_point + direction * distance; -} - -namespace GCode::Impl { -std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances) { - assert(xy_path.size() >= 2); - std::vector result; - result.reserve(xy_path.size() + sorted_distances.size()); - double total_distance{0}; - result.emplace_back(DistancedPoint{xy_path.front(), 0}); - Point previous_point = result.front().point; - std::size_t offset{0}; - for (const Point& point : xy_path.subspan(1)) { - Vec2d unscaled_point{unscaled(point)}; - Vec2d unscaled_previous_point{unscaled(previous_point)}; - const double current_segment_length = (unscaled_point - unscaled_previous_point).norm(); - for (const double distance_to_add : sorted_distances.subspan(offset)) { - if (distance_to_add <= total_distance + current_segment_length) { - Point to_place = scaled(place_at_segment( - unscaled_point, - unscaled_previous_point, - distance_to_add - total_distance - )); - if (to_place != previous_point && to_place != point) { - result.emplace_back(DistancedPoint{to_place, distance_to_add}); - } - ++offset; - } else { - break; - } - } - total_distance += current_segment_length; - result.emplace_back(DistancedPoint{point, total_distance}); - previous_point = point; - } - return result; -} - -struct ElevatedTravelParams { - double lift_height{}; - double slope_end{}; -}; - -struct ElevatedTravelFormula { - double operator()(double distance_from_start) const { - if (distance_from_start < this->params.slope_end) { - const double lift_percent = distance_from_start / this->params.slope_end; - return lift_percent * this->params.lift_height; - } else { - return this->params.lift_height; - } - } - - ElevatedTravelParams params{}; -}; - -Points3 generate_elevated_travel( - const tcb::span xy_path, - const std::vector& ensure_points_at_distances, - const double initial_elevation, - const std::function& elevation -) { - Points3 result{}; - - std::vector extended_xy_path = slice_xy_path(xy_path, ensure_points_at_distances); - result.reserve(extended_xy_path.size()); - - for (const DistancedPoint& point : extended_xy_path) { - result.emplace_back(point.point.x(), point.point.y(), scaled(initial_elevation + elevation(point.distance_from_start))); - } - - return result; -} - - -std::optional get_first_crossed_line_distance( - tcb::span xy_path, - const AABBTreeLines::LinesDistancer& distancer -) { - assert(!xy_path.empty()); - if (xy_path.empty()) { - return {}; - } - - double traversed_distance = 0; - for (const Line& line : xy_path) { - const Linef unscaled_line = {unscaled(line.a), unscaled(line.b)}; - auto intersections = distancer.intersections_with_line(unscaled_line); - if (!intersections.empty()) { - const Vec2d intersection = intersections.front().first; - const double distance = traversed_distance + (unscaled_line.a - intersection).norm(); - if (distance > EPSILON) { - return distance; - } else if (intersections.size() >= 2) { // Edge case - const Vec2d second_intersection = intersections[1].first; - return traversed_distance + (unscaled_line.a - second_intersection).norm(); - } - } - traversed_distance += (unscaled_line.a - unscaled_line.b).norm(); - } - - return {}; -} - -std::optional get_obstacle_adjusted_slope_end( - const Lines& xy_path, - const std::optional>& previous_layer_distancer -) { - if (!previous_layer_distancer) { - return std::nullopt; - } - std::optional first_obstacle_distance = get_first_crossed_line_distance( - xy_path, *previous_layer_distancer - ); - if (!first_obstacle_distance) { - return std::nullopt; - } - return *first_obstacle_distance; -} - -ElevatedTravelParams get_elevated_traval_params( - const Lines& xy_path, - const FullPrintConfig& config, - const unsigned extruder_id, - const std::optional>& previous_layer_distancer -) { - ElevatedTravelParams elevation_params{}; - if (!config.travel_ramping_lift.get_at(extruder_id)) { - elevation_params.slope_end = 0; - elevation_params.lift_height = config.retract_lift.get_at(extruder_id); - return elevation_params; - } - elevation_params.lift_height = config.travel_max_lift.get_at(extruder_id); - - const double slope_deg = config.travel_slope.get_at(extruder_id); - - if (slope_deg >= 90 || slope_deg <= 0) { - elevation_params.slope_end = 0; - } else { - const double slope_rad = slope_deg * (M_PI / 180); // rad - elevation_params.slope_end = elevation_params.lift_height / std::tan(slope_rad); - } - - std::optional obstacle_adjusted_slope_end{get_obstacle_adjusted_slope_end( - xy_path, - previous_layer_distancer - )}; - - if (obstacle_adjusted_slope_end && obstacle_adjusted_slope_end < elevation_params.slope_end) { - elevation_params.slope_end = *obstacle_adjusted_slope_end; - } - - return elevation_params; -} - -Points3 generate_travel_to_extrusion( - const Polyline& xy_path, - const FullPrintConfig& config, - const unsigned extruder_id, - const double initial_elevation, - const std::optional>& previous_layer_distancer, - const Point& xy_path_coord_origin -) { - const double upper_limit = config.retract_lift_below.get_at(extruder_id); - const double lower_limit = config.retract_lift_above.get_at(extruder_id); - if ( - (lower_limit > 0 && initial_elevation < lower_limit) - || (upper_limit > 0 && initial_elevation > upper_limit) - ) { - return generate_flat_travel(xy_path.points, initial_elevation); - } - - Lines global_xy_path; - for (const Line& line : xy_path.lines()) { - global_xy_path.emplace_back(line.a + xy_path_coord_origin, line.b + xy_path_coord_origin); - } - - ElevatedTravelParams elevation_params{get_elevated_traval_params( - global_xy_path, - config, - extruder_id, - previous_layer_distancer - )}; - - const std::vector ensure_points_at_distances{elevation_params.slope_end}; - - Points3 result{generate_elevated_travel( - xy_path.points, - ensure_points_at_distances, - initial_elevation, - ElevatedTravelFormula{elevation_params} - )}; - - result.emplace_back(xy_path.back().x(), xy_path.back().y(), scaled(initial_elevation)); - return result; -} -} - std::string GCodeGenerator::generate_travel_gcode( const Points3& travel, const std::string& comment @@ -3660,8 +3452,8 @@ std::string GCodeGenerator::travel_to(const Point &point, ExtrusionRole role, st const double initial_elevation = this->m_last_layer_z + this->m_config.z_offset.value; const Points3 travel = ( can_be_flat ? - generate_flat_travel(xy_path.points, initial_elevation) : - GCode::Impl::generate_travel_to_extrusion( + GCode::Impl::Travels::generate_flat_travel(xy_path.points, initial_elevation) : + GCode::Impl::Travels::generate_travel_to_extrusion( xy_path, this->m_config, extruder_id, diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 20c806f8f5..d75a86d443 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -90,65 +90,6 @@ struct LayerResult { }; namespace GCode::Impl { -struct DistancedPoint { - Point point; - double distance_from_start; -}; - -/** - * @brief Takes a path described as a list of points and adds points to it. - * - * @param xy_path A list of points describing a path in xy. - * @param sorted_distances A sorted list of distances along the path. - * @return Sliced path. - * - * The algorithm travels along the path segments and adds points to - * the segments in such a way that the points have specified distances - * from the xy_path start. **Any distances over the xy_path end will - * be simply ignored.** - * - * Example usage - simplified for clarity: - * @code - * std::vector distances{0.5, 1.5}; - * std::vector xy_path{{0, 0}, {1, 0}}; - * // produces - * {{0, 0}, {0, 0.5}, {1, 0}} - * // notice that 1.5 is omitted - * @endcode - */ -std::vector slice_xy_path(tcb::span xy_path, tcb::span sorted_distances); - -/** - * @brief Take xy_path and genrate a travel acording to elevation. - * - * @param xy_path A list of points describing a path in xy. - * @param ensure_points_at_distances See slice_xy_path sorted_distances. - * @param elevation A function taking current distance in mm as input and returning elevation in mm as output. - * - * **Be aweare** that the elevation function operates in mm, while xy_path and returned travel are in - * scaled coordinates. - */ -Points3 generate_elevated_travel( - const tcb::span xy_path, - const std::vector& ensure_points_at_distances, - const double initial_elevation, - const std::function& elevation -); - -/** - * @brief Given a AABB tree over lines find intersection with xy_path closest to the xy_path start. - * - * @param xy_path A path in 2D. - * @param distancer AABB Tree over lines. - * @return Distance to the first intersection if there is one. - * - * **Ignores intersection with xy_path starting point.** - */ -std::optional get_first_crossed_line_distance( - tcb::span xy_path, - const AABBTreeLines::LinesDistancer& distancer -); - /** * Generates a regular polygon - all angles are the same (e.g. typical hexagon). diff --git a/src/libslic3r/GCode/Travels.cpp b/src/libslic3r/GCode/Travels.cpp new file mode 100644 index 0000000000..17f4344742 --- /dev/null +++ b/src/libslic3r/GCode/Travels.cpp @@ -0,0 +1,205 @@ +#include "Travels.hpp" + +namespace Slic3r::GCode::Impl::Travels { + +Points3 generate_flat_travel(tcb::span xy_path, const float elevation) { + Points3 result; + result.reserve(xy_path.size() - 1); + for (const Point &point : xy_path.subspan(1)) { + result.emplace_back(point.x(), point.y(), scaled(elevation)); + } + return result; +} + +Vec2d place_at_segment( + const Vec2d ¤t_point, const Vec2d &previous_point, const double distance +) { + Vec2d direction = (current_point - previous_point).normalized(); + return previous_point + direction * distance; +} + +std::vector slice_xy_path( + tcb::span xy_path, tcb::span sorted_distances +) { + assert(xy_path.size() >= 2); + std::vector result; + result.reserve(xy_path.size() + sorted_distances.size()); + double total_distance{0}; + result.emplace_back(DistancedPoint{xy_path.front(), 0}); + Point previous_point = result.front().point; + std::size_t offset{0}; + for (const Point &point : xy_path.subspan(1)) { + Vec2d unscaled_point{unscaled(point)}; + Vec2d unscaled_previous_point{unscaled(previous_point)}; + const double current_segment_length = (unscaled_point - unscaled_previous_point).norm(); + for (const double distance_to_add : sorted_distances.subspan(offset)) { + if (distance_to_add <= total_distance + current_segment_length) { + Point to_place = scaled(place_at_segment( + unscaled_point, unscaled_previous_point, distance_to_add - total_distance + )); + if (to_place != previous_point && to_place != point) { + result.emplace_back(DistancedPoint{to_place, distance_to_add}); + } + ++offset; + } else { + break; + } + } + total_distance += current_segment_length; + result.emplace_back(DistancedPoint{point, total_distance}); + previous_point = point; + } + return result; +} + +struct ElevatedTravelParams +{ + double lift_height{}; + double slope_end{}; +}; + +struct ElevatedTravelFormula +{ + double operator()(double distance_from_start) const { + if (distance_from_start < this->params.slope_end) { + const double lift_percent = distance_from_start / this->params.slope_end; + return lift_percent * this->params.lift_height; + } else { + return this->params.lift_height; + } + } + + ElevatedTravelParams params{}; +}; + +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector &ensure_points_at_distances, + const double initial_elevation, + const std::function &elevation +) { + Points3 result{}; + + std::vector extended_xy_path = slice_xy_path(xy_path, ensure_points_at_distances); + result.reserve(extended_xy_path.size()); + + for (const DistancedPoint &point : extended_xy_path) { + result.emplace_back( + point.point.x(), point.point.y(), + scaled(initial_elevation + elevation(point.distance_from_start)) + ); + } + + return result; +} + +std::optional get_first_crossed_line_distance( + tcb::span xy_path, const AABBTreeLines::LinesDistancer &distancer +) { + assert(!xy_path.empty()); + if (xy_path.empty()) { + return {}; + } + + double traversed_distance = 0; + for (const Line &line : xy_path) { + const Linef unscaled_line = {unscaled(line.a), unscaled(line.b)}; + auto intersections = distancer.intersections_with_line(unscaled_line); + if (!intersections.empty()) { + const Vec2d intersection = intersections.front().first; + const double distance = traversed_distance + (unscaled_line.a - intersection).norm(); + if (distance > EPSILON) { + return distance; + } else if (intersections.size() >= 2) { // Edge case + const Vec2d second_intersection = intersections[1].first; + return traversed_distance + (unscaled_line.a - second_intersection).norm(); + } + } + traversed_distance += (unscaled_line.a - unscaled_line.b).norm(); + } + + return {}; +} + +std::optional get_obstacle_adjusted_slope_end( + const Lines &xy_path, + const std::optional> &previous_layer_distancer +) { + if (!previous_layer_distancer) { + return std::nullopt; + } + std::optional first_obstacle_distance = + get_first_crossed_line_distance(xy_path, *previous_layer_distancer); + if (!first_obstacle_distance) { + return std::nullopt; + } + return *first_obstacle_distance; +} + +ElevatedTravelParams get_elevated_traval_params( + const Lines &xy_path, + const FullPrintConfig &config, + const unsigned extruder_id, + const std::optional> &previous_layer_distancer +) { + ElevatedTravelParams elevation_params{}; + if (!config.travel_ramping_lift.get_at(extruder_id)) { + elevation_params.slope_end = 0; + elevation_params.lift_height = config.retract_lift.get_at(extruder_id); + return elevation_params; + } + elevation_params.lift_height = config.travel_max_lift.get_at(extruder_id); + + const double slope_deg = config.travel_slope.get_at(extruder_id); + + if (slope_deg >= 90 || slope_deg <= 0) { + elevation_params.slope_end = 0; + } else { + const double slope_rad = slope_deg * (M_PI / 180); // rad + elevation_params.slope_end = elevation_params.lift_height / std::tan(slope_rad); + } + + std::optional obstacle_adjusted_slope_end{ + get_obstacle_adjusted_slope_end(xy_path, previous_layer_distancer)}; + + if (obstacle_adjusted_slope_end && obstacle_adjusted_slope_end < elevation_params.slope_end) { + elevation_params.slope_end = *obstacle_adjusted_slope_end; + } + + return elevation_params; +} + +Points3 generate_travel_to_extrusion( + const Polyline &xy_path, + const FullPrintConfig &config, + const unsigned extruder_id, + const double initial_elevation, + const std::optional> &previous_layer_distancer, + const Point &xy_path_coord_origin +) { + const double upper_limit = config.retract_lift_below.get_at(extruder_id); + const double lower_limit = config.retract_lift_above.get_at(extruder_id); + if ((lower_limit > 0 && initial_elevation < lower_limit) || + (upper_limit > 0 && initial_elevation > upper_limit)) { + return generate_flat_travel(xy_path.points, initial_elevation); + } + + Lines global_xy_path; + for (const Line &line : xy_path.lines()) { + global_xy_path.emplace_back(line.a + xy_path_coord_origin, line.b + xy_path_coord_origin); + } + + ElevatedTravelParams elevation_params{ + get_elevated_traval_params(global_xy_path, config, extruder_id, previous_layer_distancer)}; + + const std::vector ensure_points_at_distances{elevation_params.slope_end}; + + Points3 result{generate_elevated_travel( + xy_path.points, ensure_points_at_distances, initial_elevation, + ElevatedTravelFormula{elevation_params} + )}; + + result.emplace_back(xy_path.back().x(), xy_path.back().y(), scaled(initial_elevation)); + return result; +} +} // namespace Slic3r::GCode::Impl::Travels diff --git a/src/libslic3r/GCode/Travels.hpp b/src/libslic3r/GCode/Travels.hpp new file mode 100644 index 0000000000..bf0e4b99f6 --- /dev/null +++ b/src/libslic3r/GCode/Travels.hpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +#include "libslic3r/Line.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/AABBTreeLines.hpp" +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r::GCode::Impl::Travels { +struct DistancedPoint +{ + Point point; + double distance_from_start; +}; + +/** + * @brief Takes a path described as a list of points and adds points to it. + * + * @param xy_path A list of points describing a path in xy. + * @param sorted_distances A sorted list of distances along the path. + * @return Sliced path. + * + * The algorithm travels along the path segments and adds points to + * the segments in such a way that the points have specified distances + * from the xy_path start. **Any distances over the xy_path end will + * be simply ignored.** + * + * Example usage - simplified for clarity: + * @code + * std::vector distances{0.5, 1.5}; + * std::vector xy_path{{0, 0}, {1, 0}}; + * // produces + * {{0, 0}, {0, 0.5}, {1, 0}} + * // notice that 1.5 is omitted + * @endcode + */ +std::vector slice_xy_path( + tcb::span xy_path, tcb::span sorted_distances +); + +/** + * @brief Simply return the xy_path with z coord set to elevation. + */ +Points3 generate_flat_travel(tcb::span xy_path, const float elevation); + +/** + * @brief Take xy_path and genrate a travel acording to elevation. + * + * @param xy_path A list of points describing a path in xy. + * @param ensure_points_at_distances See slice_xy_path sorted_distances. + * @param elevation A function taking current distance in mm as input and returning elevation in mm + * as output. + * + * **Be aweare** that the elevation function operates in mm, while xy_path and returned travel are + * in scaled coordinates. + */ +Points3 generate_elevated_travel( + const tcb::span xy_path, + const std::vector &ensure_points_at_distances, + const double initial_elevation, + const std::function &elevation +); + +/** + * @brief Given a AABB tree over lines find intersection with xy_path closest to the xy_path start. + * + * @param xy_path A path in 2D. + * @param distancer AABB Tree over lines. + * @return Distance to the first intersection if there is one. + * + * **Ignores intersection with xy_path starting point.** + */ +std::optional get_first_crossed_line_distance( + tcb::span xy_path, const AABBTreeLines::LinesDistancer &distancer +); + +/** + * @brief Extract parameters and decide wheather the travel can be elevated. + * Then generate the whole travel 3D path - elevated if possible. + */ +Points3 generate_travel_to_extrusion( + const Polyline &xy_path, + const FullPrintConfig &config, + const unsigned extruder_id, + const double initial_elevation, + const std::optional> &previous_layer_distancer, + const Point &xy_path_coord_origin +); +} // namespace Slic3r::GCode::Impl::Travels diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 6e9a80ef86..45efe83341 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests test_flow.cpp test_gaps.cpp test_gcode.cpp + test_gcode_travels.cpp test_gcodefindreplace.cpp test_gcodewriter.cpp test_model.cpp diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp index 69acc44901..11dce2e90f 100644 --- a/tests/fff_print/test_gcode.cpp +++ b/tests/fff_print/test_gcode.cpp @@ -22,180 +22,6 @@ SCENARIO("Origin manipulation", "[GCode]") { } } -struct ApproxEqualsPoints : public Catch::MatcherBase { - ApproxEqualsPoints(const Points& expected, unsigned tolerance): expected(expected), tolerance(tolerance) {} - bool match(const Points& points) const override { - if (points.size() != expected.size()) { - return false; - } - for (auto i = 0u; i < points.size(); ++i) { - const Point& point = points[i]; - const Point& expected_point = this->expected[i]; - if ( - std::abs(point.x() - expected_point.x()) > this->tolerance - || std::abs(point.y() - expected_point.y()) > this->tolerance - ) { - return false; - } - } - return true; - } - std::string describe() const override { - std::stringstream ss; - ss << std::endl; - for (const Point& point : expected) { - ss << "(" << point.x() << ", " << point.y() << ")" << std::endl; - } - ss << "With tolerance: " << this->tolerance; - - return "Equals " + ss.str(); - } - -private: - Points expected; - unsigned tolerance; -}; - -Points get_points(const std::vector& result) { - Points result_points; - std::transform( - result.begin(), - result.end(), - std::back_inserter(result_points), - [](const DistancedPoint& point){ - return point.point; - } - ); - return result_points; -} - -std::vector get_distances(const std::vector& result) { - std::vector result_distances; - std::transform( - result.begin(), - result.end(), - std::back_inserter(result_distances), - [](const DistancedPoint& point){ - return point.distance_from_start; - } - ); - return result_distances; -} - -TEST_CASE("Place points at distances - expected use", "[GCode]") { - std::vector line{ - scaled(Vec2f{0, 0}), - scaled(Vec2f{1, 0}), - scaled(Vec2f{2, 1}), - scaled(Vec2f{2, 2}) - }; - std::vector distances{0, 0.2, 0.5, 1 + std::sqrt(2)/2, 1 + std::sqrt(2) + 0.5, 100.0}; - std::vector result = slice_xy_path(line, distances); - - REQUIRE_THAT(get_points(result), ApproxEqualsPoints(Points{ - scaled(Vec2f{0, 0}), - scaled(Vec2f{0.2, 0}), - scaled(Vec2f{0.5, 0}), - scaled(Vec2f{1, 0}), - scaled(Vec2f{1.5, 0.5}), - scaled(Vec2f{2, 1}), - scaled(Vec2f{2, 1.5}), - scaled(Vec2f{2, 2}) - }, 5)); - - REQUIRE_THAT(get_distances(result), Catch::Matchers::Approx(std::vector{ - distances[0], distances[1], distances[2], 1, distances[3], 1 + std::sqrt(2), distances[4], 2 + std::sqrt(2) - })); -} - -TEST_CASE("Place points at distances - edge case", "[GCode]") { - std::vector line{ - scaled(Vec2f{0, 0}), - scaled(Vec2f{1, 0}), - scaled(Vec2f{2, 0}) - }; - std::vector distances{0, 1, 1.5, 2}; - Points result{get_points(slice_xy_path(line, distances))}; - CHECK(result == Points{ - scaled(Vec2f{0, 0}), - scaled(Vec2f{1, 0}), - scaled(Vec2f{1.5, 0}), - scaled(Vec2f{2, 0}) - }); -} - -TEST_CASE("Generate elevated travel", "[GCode]") { - std::vector xy_path{ - scaled(Vec2f{0, 0}), - scaled(Vec2f{1, 0}), - }; - std::vector ensure_points_at_distances{0.2, 0.5}; - Points3 result{generate_elevated_travel(xy_path, ensure_points_at_distances, 2.0, [](double x){return 1 + x;})}; - - CHECK(result == Points3{ - scaled(Vec3f{0, 0, 3.0}), - scaled(Vec3f{0.2, 0, 3.2}), - scaled(Vec3f{0.5, 0, 3.5}), - scaled(Vec3f{1, 0, 4.0}) - }); -} - -TEST_CASE("Get first crossed line distance", "[GCode]") { - // A 2x2 square at 0, 0, with 1x1 square hole in its center. - ExPolygon square_with_hole{ - { - scaled(Vec2f{-1, -1}), - scaled(Vec2f{1, -1}), - scaled(Vec2f{1, 1}), - scaled(Vec2f{-1, 1}) - }, - { - scaled(Vec2f{-0.5, -0.5}), - scaled(Vec2f{0.5, -0.5}), - scaled(Vec2f{0.5, 0.5}), - scaled(Vec2f{-0.5, 0.5}) - } - }; - // A 2x2 square above the previous square at (0, 3). - ExPolygon square_above{ - { - scaled(Vec2f{-1, 2}), - scaled(Vec2f{1, 2}), - scaled(Vec2f{1, 4}), - scaled(Vec2f{-1, 4}) - } - }; - - // Bottom-up travel intersecting the squares. - Lines travel{Polyline{ - scaled(Vec2f{0, -2}), - scaled(Vec2f{0, -0.7}), - scaled(Vec2f{0, 0}), - scaled(Vec2f{0, 1}), - scaled(Vec2f{0, 1.3}), - scaled(Vec2f{0, 2.4}), - scaled(Vec2f{0, 4.5}), - scaled(Vec2f{0, 5}), - }.lines()}; - - std::vector lines; - for (const ExPolygon& polygon : {square_with_hole, square_above}) { - for (const Line& line : polygon.lines()) { - lines.emplace_back(unscale(line.a), unscale(line.b)); - } - } - // Try different cases by skipping lines in the travel. - AABBTreeLines::LinesDistancer distancer{std::move(lines)}; - - CHECK(*get_first_crossed_line_distance(travel, distancer) == Approx(1)); - CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(1), distancer) == Approx(0.2)); - CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(2), distancer) == Approx(0.5)); - CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(3), distancer) == Approx(1.0)); //Edge case - CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(4), distancer) == Approx(0.7)); - CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(5), distancer) == Approx(1.6)); - CHECK_FALSE(get_first_crossed_line_distance(tcb::span{travel}.subspan(6), distancer)); -} - TEST_CASE("Generate regular polygon", "[GCode]") { const unsigned points_count{32}; const Point centroid{scaled(Vec2d{5, -2})}; diff --git a/tests/fff_print/test_gcode_travels.cpp b/tests/fff_print/test_gcode_travels.cpp new file mode 100644 index 0000000000..d8bbf4c0e7 --- /dev/null +++ b/tests/fff_print/test_gcode_travels.cpp @@ -0,0 +1,181 @@ +#include +#include +#include + +using namespace Slic3r; +using namespace Slic3r::GCode::Impl::Travels; + +struct ApproxEqualsPoints : public Catch::MatcherBase { + ApproxEqualsPoints(const Points& expected, unsigned tolerance): expected(expected), tolerance(tolerance) {} + bool match(const Points& points) const override { + if (points.size() != expected.size()) { + return false; + } + for (auto i = 0u; i < points.size(); ++i) { + const Point& point = points[i]; + const Point& expected_point = this->expected[i]; + if ( + std::abs(point.x() - expected_point.x()) > this->tolerance + || std::abs(point.y() - expected_point.y()) > this->tolerance + ) { + return false; + } + } + return true; + } + std::string describe() const override { + std::stringstream ss; + ss << std::endl; + for (const Point& point : expected) { + ss << "(" << point.x() << ", " << point.y() << ")" << std::endl; + } + ss << "With tolerance: " << this->tolerance; + + return "Equals " + ss.str(); + } + +private: + Points expected; + unsigned tolerance; +}; + +Points get_points(const std::vector& result) { + Points result_points; + std::transform( + result.begin(), + result.end(), + std::back_inserter(result_points), + [](const DistancedPoint& point){ + return point.point; + } + ); + return result_points; +} + +std::vector get_distances(const std::vector& result) { + std::vector result_distances; + std::transform( + result.begin(), + result.end(), + std::back_inserter(result_distances), + [](const DistancedPoint& point){ + return point.distance_from_start; + } + ); + return result_distances; +} + +TEST_CASE("Place points at distances - expected use", "[GCode]") { + std::vector line{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{2, 1}), + scaled(Vec2f{2, 2}) + }; + std::vector distances{0, 0.2, 0.5, 1 + std::sqrt(2)/2, 1 + std::sqrt(2) + 0.5, 100.0}; + std::vector result = slice_xy_path(line, distances); + + REQUIRE_THAT(get_points(result), ApproxEqualsPoints(Points{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{0.2, 0}), + scaled(Vec2f{0.5, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{1.5, 0.5}), + scaled(Vec2f{2, 1}), + scaled(Vec2f{2, 1.5}), + scaled(Vec2f{2, 2}) + }, 5)); + + REQUIRE_THAT(get_distances(result), Catch::Matchers::Approx(std::vector{ + distances[0], distances[1], distances[2], 1, distances[3], 1 + std::sqrt(2), distances[4], 2 + std::sqrt(2) + })); +} + +TEST_CASE("Place points at distances - edge case", "[GCode]") { + std::vector line{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{2, 0}) + }; + std::vector distances{0, 1, 1.5, 2}; + Points result{get_points(slice_xy_path(line, distances))}; + CHECK(result == Points{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + scaled(Vec2f{1.5, 0}), + scaled(Vec2f{2, 0}) + }); +} + +TEST_CASE("Generate elevated travel", "[GCode]") { + std::vector xy_path{ + scaled(Vec2f{0, 0}), + scaled(Vec2f{1, 0}), + }; + std::vector ensure_points_at_distances{0.2, 0.5}; + Points3 result{generate_elevated_travel(xy_path, ensure_points_at_distances, 2.0, [](double x){return 1 + x;})}; + + CHECK(result == Points3{ + scaled(Vec3f{0, 0, 3.0}), + scaled(Vec3f{0.2, 0, 3.2}), + scaled(Vec3f{0.5, 0, 3.5}), + scaled(Vec3f{1, 0, 4.0}) + }); +} + +TEST_CASE("Get first crossed line distance", "[GCode]") { + // A 2x2 square at 0, 0, with 1x1 square hole in its center. + ExPolygon square_with_hole{ + { + scaled(Vec2f{-1, -1}), + scaled(Vec2f{1, -1}), + scaled(Vec2f{1, 1}), + scaled(Vec2f{-1, 1}) + }, + { + scaled(Vec2f{-0.5, -0.5}), + scaled(Vec2f{0.5, -0.5}), + scaled(Vec2f{0.5, 0.5}), + scaled(Vec2f{-0.5, 0.5}) + } + }; + // A 2x2 square above the previous square at (0, 3). + ExPolygon square_above{ + { + scaled(Vec2f{-1, 2}), + scaled(Vec2f{1, 2}), + scaled(Vec2f{1, 4}), + scaled(Vec2f{-1, 4}) + } + }; + + // Bottom-up travel intersecting the squares. + Lines travel{Polyline{ + scaled(Vec2f{0, -2}), + scaled(Vec2f{0, -0.7}), + scaled(Vec2f{0, 0}), + scaled(Vec2f{0, 1}), + scaled(Vec2f{0, 1.3}), + scaled(Vec2f{0, 2.4}), + scaled(Vec2f{0, 4.5}), + scaled(Vec2f{0, 5}), + }.lines()}; + + std::vector lines; + for (const ExPolygon& polygon : {square_with_hole, square_above}) { + for (const Line& line : polygon.lines()) { + lines.emplace_back(unscale(line.a), unscale(line.b)); + } + } + // Try different cases by skipping lines in the travel. + AABBTreeLines::LinesDistancer distancer{std::move(lines)}; + + CHECK(*get_first_crossed_line_distance(travel, distancer) == Approx(1)); + CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(1), distancer) == Approx(0.2)); + CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(2), distancer) == Approx(0.5)); + CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(3), distancer) == Approx(1.0)); //Edge case + CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(4), distancer) == Approx(0.7)); + CHECK(*get_first_crossed_line_distance(tcb::span{travel}.subspan(5), distancer) == Approx(1.6)); + CHECK_FALSE(get_first_crossed_line_distance(tcb::span{travel}.subspan(6), distancer)); +} +