diff --git a/CMakeLists.txt b/CMakeLists.txt index 85cd47f805..1ccaed37b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ set(engine_SRCS # Except main.cpp. src/multiVolumes.cpp src/path_ordering.cpp src/PathAdapter.cpp + src/PathOrderMonotonic.cpp src/Preheat.cpp src/PrimeTower/PrimeTower.cpp src/PrimeTower/PrimeTowerNormal.cpp @@ -177,6 +178,7 @@ set(engine_SRCS # Except main.cpp. src/geometry/Point3LL.cpp src/geometry/Polygon.cpp src/geometry/Shape.cpp + src/geometry/PointMatrix.cpp src/geometry/PointsSet.cpp src/geometry/SingleShape.cpp src/geometry/PartsView.cpp diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h index eeff8dfd8a..9575f157d8 100644 --- a/include/FffGcodeWriter.h +++ b/include/FffGcodeWriter.h @@ -11,6 +11,7 @@ #include "FanSpeedLayerTime.h" #include "GCodePathConfig.h" #include "LayerPlanBuffer.h" +#include "LinesOrderingMethod.h" #include "gcodeExport.h" #include "utils/LayerVector.h" #include "utils/NoCopy.h" @@ -591,6 +592,7 @@ class FffGcodeWriter : public NoCopy * minimise travel moves (``false``). * \param[out] added_something Whether this function added anything to the layer plan * \param fan_speed fan speed override for this skin area + * \param forced_small_area_width A specific value to be used for small_area_width when generating the infill, or nullopt to use the normal value */ void processSkinPrintFeature( const SliceDataStorage& storage, @@ -603,10 +605,11 @@ class FffGcodeWriter : public NoCopy const AngleDegrees skin_angle, const coord_t skin_overlap, const Ratio skin_density, - const bool monotonic, + const LinesOrderingMethod ordering, const bool is_roofing_flooring, bool& added_something, - double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT) const; + double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT, + std::optional forced_small_area_width = std::nullopt) const; /*! * see if we can avoid printing a lines or zig zag style skin part in multiple segments by moving to diff --git a/include/LayerPlan.h b/include/LayerPlan.h index a4bcbd449f..0eea515f75 100644 --- a/include/LayerPlan.h +++ b/include/LayerPlan.h @@ -785,7 +785,8 @@ class LayerPlan : public NoCopy const coord_t exclude_distance = 0, const coord_t wipe_dist = 0, const Ratio flow_ratio = 1.0_r, - const double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT); + const double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT, + const bool interlaced = false); /*! * Add a spiralized slice of wall that is interpolated in X/Y between \p last_wall and \p wall. diff --git a/include/LinesOrderingMethod.h b/include/LinesOrderingMethod.h new file mode 100644 index 0000000000..f488c5d25e --- /dev/null +++ b/include/LinesOrderingMethod.h @@ -0,0 +1,20 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#ifndef LINES_ORDERING_METHOD_H +#define LINES_ORDERING_METHOD_H + + +namespace cura +{ + +enum class LinesOrderingMethod +{ + Basic, // Lines are ordered by shortest distance + Monotonic, // Lines are ordered so that they will always form a continuous print along a direction + Interlaced, // Similar to monotonic, but with 2 passes so that adjacent lines will not be printed just after each other +}; + +} // namespace cura + +#endif diff --git a/include/PathOrderMonotonic.h b/include/PathOrderMonotonic.h index 5602bda632..b9d5b736f2 100644 --- a/include/PathOrderMonotonic.h +++ b/include/PathOrderMonotonic.h @@ -5,8 +5,7 @@ #define PATHORDERMONOTONIC_H #include //For std::sin() and std::cos(). -#include //To track monotonic sequences. -#include //To track starting points of monotonic sequences. +#include #include "PathOrder.h" #include "geometry/Point2D.h" @@ -45,238 +44,16 @@ class PathOrderMonotonic : public PathOrder using Path = PathOrdering; using PathOrder::coincident_point_distance_; - PathOrderMonotonic(const AngleRadians& monotonic_direction, const coord_t max_adjacent_distance, const Point2LL& start_point) + PathOrderMonotonic(const AngleRadians& monotonic_direction, const coord_t max_adjacent_distance, const Point2LL& start_point, const bool interlaced = false) : PathOrder(start_point) // The monotonic vector needs to rotate clockwise instead of counter-clockwise, the same as how the infill patterns are generated. , monotonic_vector_(-std::cos(monotonic_direction), std::sin(monotonic_direction)) , max_adjacent_distance_(max_adjacent_distance) + , interlaced_(interlaced) { } - void optimize() - { - if (this->paths_.empty()) - { - return; - } - - // Get the vertex data and store it in the paths. - for (Path& path : this->paths_) - { - path.converted_ = &path.getVertexData(); - } - - std::vector reordered; // To store the result in. At the end, we'll std::swap with the real paths. - reordered.reserve(this->paths_.size()); - - // First print all the looping polygons, if there are any. - std::vector polylines; // Also find all polylines and store them in a vector that we can sort in-place without making copies all the time. - this->detectLoops(); // Always filter out loops. We don't specifically want to print those in monotonic order. - for (Path& path : this->paths_) - { - if (path.is_closed_ || path.vertices_->size() <= 1) - { - reordered.push_back(path); - } - else - { - polylines.push_back(&path); - // Assign an invalid starting vertex to indicate we don't know the starting point yet. - polylines.back()->start_vertex_ = polylines.back()->converted_->size(); - } - } - - // Sort the polylines by their projection on the monotonic vector. This helps find adjacent lines quickly. - std::stable_sort( - polylines.begin(), - polylines.end(), - [this](Path* a, Path* b) - { - const double a_start_projection = projectToMonotonicVector(a->converted_->front()); - const double a_end_projection = projectToMonotonicVector(a->converted_->back()); - const double a_projection = std::min(a_start_projection, a_end_projection); // The projection of a path is the endpoint furthest back of the two endpoints. - - const double b_start_projection = projectToMonotonicVector(b->converted_->front()); - const double b_end_projection = projectToMonotonicVector(b->converted_->back()); - const double b_projection = std::min(b_start_projection, b_end_projection); - - return a_projection < b_projection; - }); - // Create a bucket grid to be able to find adjacent lines quickly. - SparsePointGridInclusive line_bucket_grid(MM2INT(2)); // Grid size of 2mm. - for (Path* polyline : polylines) - { - if (! polyline->converted_->empty()) - { - line_bucket_grid.insert(polyline->converted_->front(), polyline); - line_bucket_grid.insert(polyline->converted_->back(), polyline); - } - } - - // Create sequences of line segments that get printed together in a monotonic direction. - // There are several constraints we impose here: - // - Strings of incident polylines are printed in sequence. That is, if their endpoints are incident. - // - The endpoint of the string that is earlier in the monotonic direction will get printed first. - // - The start_vertex of this line will already be set to indicate where to start from. - // - If a line overlaps with another line in the perpendicular direction, and is within max_adjacent_distance (~1 line width) in the monotonic direction, it must be - // printed in monotonic order. - // - The earlier line is marked as being in sequence with the later line. - // - The later line is no longer a starting point, unless there are multiple adjacent lines before it. - // The ``starting_lines`` set indicates possible locations to start from. Each starting line represents one "sequence", which is either a set of adjacent line segments or a - // string of polylines. The ``connections`` map indicates, starting from each starting segment, the sequence of line segments to print in order. Note that for performance - // reasons, the ``connections`` map will sometimes link the end of one segment to the start of the next segment. This link should be ignored. - const Point2D perpendicular = monotonic_vector_.rotated90CCW(); // To project on to detect adjacent lines. - - std::unordered_set connected_lines; // Lines that are reachable from one of the starting lines through its connections. - std::unordered_set starting_lines; // Starting points of a linearly connected segment. - std::unordered_map connections; // For each polyline, which polyline it overlaps with, closest in the projected order. - - for (auto polyline_it = polylines.begin(); polyline_it != polylines.end(); polyline_it++) - { - if (connections.contains(*polyline_it)) // Already connected this one through a polyline. - { - continue; - } - // First find out if this polyline is part of a string of polylines. - std::deque polystring = findPolylineString(*polyline_it, line_bucket_grid); - - // If we're part of a string of polylines, connect up the whole string and mark all of them as being connected. - if (polystring.size() > 1) - { - starting_lines.insert(polystring[0]); - for (size_t i = 0; i < polystring.size() - 1; ++i) // Iterate over every pair of adjacent polylines in the string (so skip the last one)! - { - connections[polystring[i]] = polystring[i + 1]; - connected_lines.insert(polystring[i + 1]); - - // Even though we chain polylines, we still want to find lines that they overlap with. - // The strings of polylines may still have weird shapes which interweave with other strings of polylines or loose lines. - // So when a polyline string comes into contact with other lines, we still want to guarantee their order. - // So here we will look for which lines they come into contact with, and thus mark those as possible starting points, so that they function as a new junction. - const std::vector overlapping_lines = getOverlappingLines(std::find(polylines.begin(), polylines.end(), polystring[i]), perpendicular, polylines); - for (Path* overlapping_line : overlapping_lines) - { - if (std::find(polystring.begin(), polystring.end(), overlapping_line) - == polystring.end()) // Mark all overlapping lines not part of the string as possible starting points. - { - starting_lines.insert(overlapping_line); - starting_lines.insert(polystring[i + 1]); // Also be able to re-start from this point in the string. - } - } - } - } - else // Not a string of polylines, but simply adjacent line segments. - { - if (! connected_lines.contains(*polyline_it)) // Nothing connects to this line yet. - { - starting_lines.insert(*polyline_it); // This is a starting point then. - } - const std::vector overlapping_lines = getOverlappingLines(polyline_it, perpendicular, polylines); - if (overlapping_lines.size() == 1) // If we're not a string of polylines, but adjacent to only one other polyline, create a sequence of polylines. - { - connections[*polyline_it] = overlapping_lines[0]; - if (connected_lines.contains(overlapping_lines[0])) // This line was already connected to. - { - starting_lines.insert(overlapping_lines[0]); // Multiple lines connect to it, so we must be able to start there. - } - else - { - connected_lines.insert(overlapping_lines[0]); - } - } - else // Either 0 (the for loop terminates immediately) or multiple overlapping lines. For multiple lines we need to mark all of them a starting position. - { - for (Path* overlapping_line : overlapping_lines) - { - starting_lines.insert(overlapping_line); - } - } - } - } - - struct LineProjections - { - coord_t min; - coord_t max; - - explicit LineProjections(Path* path, const Point2D& monotonic_vector) - { - // Divide by a precision factor before doing the rounding, so that we are sure that aligned lines will end up in the same bucket - constexpr double precision_factor = 10.0; - const coord_t start_projection = std::llround(projectToVector(path->converted_->front(), monotonic_vector) / precision_factor); - const coord_t end_projection = std::llround(projectToVector(path->converted_->back(), monotonic_vector) / precision_factor); - std::tie(min, max) = std::minmax(start_projection, end_projection); - } - - bool operator<(const LineProjections& other) const - { - return min < other.min || (min == other.min && max < other.max); - } - }; - - // Pre-order lines in a multi-map, so that aligned lines will end up in the same bucket and we can process them in a row - std::multimap pre_ordered_lines; - for (Path* starting_line : starting_lines) - { - pre_ordered_lines.insert(std::make_pair(LineProjections(starting_line, monotonic_vector_), starting_line)); - } - - // Now order the lines, by finding the closest line from the current position in the current bucket (row) - Point2LL current_pos = this->start_point_; - while (! pre_ordered_lines.empty()) - { - const LineProjections first_projection_key = pre_ordered_lines.begin()->first; - auto lines_on_row_range = pre_ordered_lines.equal_range(first_projection_key); - - coord_t closest_distance = std::numeric_limits::max(); - auto closest_next_line_iterator = lines_on_row_range.second; - bool closest_backwards = false; - for (auto iterator = lines_on_row_range.first; iterator != lines_on_row_range.second; ++iterator) - { - const Path* path = iterator->second; - const coord_t dist_start = vSize2(current_pos - path->converted_->front()); - const coord_t dist_end = vSize2(current_pos - path->converted_->back()); - if (dist_start < closest_distance) - { - closest_next_line_iterator = iterator; - closest_distance = dist_start; - closest_backwards = false; - } - if (dist_end < closest_distance) - { - closest_next_line_iterator = iterator; - closest_distance = dist_end; - closest_backwards = true; - } - } - - Path* closest_path = closest_next_line_iterator->second; - setStartVertex(closest_path, closest_backwards); - current_pos = (*closest_path->converted_)[closest_path->converted_->size() - 1 - closest_path->start_vertex_]; // Opposite of the start vertex. - reordered.push_back(*closest_path); - pre_ordered_lines.erase(closest_next_line_iterator); - - // Now add the (adjacent) lines to be processed together with this one - auto connection = connections.find(closest_path); - std::unordered_map checked_connections; // Which connections have already been iterated over - auto checked_connection = checked_connections.find(closest_path); - - while (connection != connections.end() // Stop if the sequence ends - && starting_lines.find(connection->second) == starting_lines.end() // or if we hit another starting point. - && (checked_connection == checked_connections.end() - || checked_connection->second != connection->second)) // or if we have already checked the connection (to avoid falling into a cyclical connection) - { - checked_connections.insert({ connection->first, connection->second }); - Path* line = connection->second; - optimizeClosestStartPoint(line, current_pos); - reordered.push_back(*line); // Plan this line in, to be printed next! - connection = connections.find(line); - checked_connection = checked_connections.find(line); - } - } - - std::swap(reordered, this->paths_); // Store the resulting list in the main paths. - } + void optimize(); protected: /*! @@ -295,6 +72,14 @@ class PathOrderMonotonic : public PathOrder */ coord_t max_adjacent_distance_; + /*! + * Interlaced monotonic will order lines in two passes, so that adjacent lines will never be printed just after each other + */ + bool interlaced_{ false }; + + // Divide by a precision factor before doing the rounding, so that we are sure that aligned lines will end up in the same bucket + static constexpr double precision_factor{ 10.0 }; + /*! * Set the proper start vertex for the given path, taking care that it should be processed either forwards or backwards * @param path The path to be setup @@ -367,91 +152,7 @@ class PathOrderMonotonic : public PathOrder * printed. All paths in this string already have their start_vertex set * correctly. */ - std::deque findPolylineString(Path* polyline, const SparsePointGridInclusive& line_bucket_grid) - { - std::deque result; - if (polyline->converted_->empty()) - { - return result; - } - - // Find the two endpoints of the polyline string, on either side. - result.push_back(polyline); - polyline->start_vertex_ = 0; - Point2LL first_endpoint = polyline->converted_->front(); - Point2LL last_endpoint = polyline->converted_->back(); - std::vector> lines_before = line_bucket_grid.getNearby(first_endpoint, coincident_point_distance_); - auto close_line_before = std::find_if( - lines_before.begin(), - lines_before.end(), - [first_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) - { - return canConnectToPolyline(first_endpoint, found_path); - }); - std::vector> lines_after = line_bucket_grid.getNearby(last_endpoint, coincident_point_distance_); - auto close_line_after = std::find_if( - lines_after.begin(), - lines_after.end(), - [last_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) - { - return canConnectToPolyline(last_endpoint, found_path); - }); - - while (close_line_before != lines_before.end()) - { - Path* first = close_line_before->val; - result.push_front(first); // Store this one in the sequence. It's a good one. - size_t farthest_vertex = getFarthestEndpoint(first, close_line_before->point); // Get to the opposite side. - first->start_vertex_ = farthest_vertex; - first->backwards_ = farthest_vertex != 0; - first_endpoint = (*first->converted_)[farthest_vertex]; - lines_before = line_bucket_grid.getNearby(first_endpoint, coincident_point_distance_); - close_line_before = std::find_if( - lines_before.begin(), - lines_before.end(), - [first_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) - { - return canConnectToPolyline(first_endpoint, found_path); - }); - } - while (close_line_after != lines_after.end()) - { - Path* last = close_line_after->val; - result.push_back(last); - size_t farthest_vertex = getFarthestEndpoint(last, close_line_after->point); // Get to the opposite side. - last->start_vertex_ = (farthest_vertex == 0) ? last->converted_->size() - 1 : 0; - last->backwards_ = farthest_vertex != 0; - last_endpoint = (*last->converted_)[farthest_vertex]; - lines_after = line_bucket_grid.getNearby(last_endpoint, coincident_point_distance_); - close_line_after = std::find_if( - lines_after.begin(), - lines_after.end(), - [last_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) - { - return canConnectToPolyline(last_endpoint, found_path); - }); - } - - // Figure out which of the two endpoints to start with: The one monotonically earliest. - const double first_projection = projectToMonotonicVector(first_endpoint); - const double last_projection = projectToMonotonicVector(last_endpoint); - // If the last endpoint should be printed first (unlikely due to monotonic start, but possible), flip the whole polyline! - if (last_projection < first_projection) - { - std::reverse(result.begin(), result.end()); - for (Path* path : result) // Also reverse their start_vertex. - { - path->start_vertex_ = (path->start_vertex_ == 0) ? path->converted_->size() - 1 : 0; - path->backwards_ = ! path->backwards_; - } - } - - if (result.size() == 1) - { - result[0]->start_vertex_ = result[0]->converted_->size(); // Reset start vertex as "unknown" again if it's not a string of polylines. - } - return result; - } + std::deque findPolylineString(Path* polyline, const SparsePointGridInclusive& line_bucket_grid); /*! * Get the endpoint of the polyline that is farthest away from the given @@ -483,58 +184,13 @@ class PathOrderMonotonic : public PathOrder * \param perpendicular A vector perpendicular to the monotonic vector, pre- * calculated. * \param polylines The sorted list of polylines. + * \param max_adjacent_distance The maximum distance for which to consider segments as adjacent */ - std::vector getOverlappingLines(const typename std::vector::iterator& polyline_it, const Point2D& perpendicular, const std::vector& polylines) - { - // How far this extends in the monotonic direction, to make sure we only go up to max_adjacent_distance in that direction. - const double start_monotonic = projectToMonotonicVector((*polyline_it)->converted_->front()); - const double end_monotonic = projectToMonotonicVector((*polyline_it)->converted_->back()); - const double my_farthest_monotonic = std::max(start_monotonic, end_monotonic); - const double my_closest_monotonic = std::min(start_monotonic, end_monotonic); - const double my_farthest_monotonic_padded = my_farthest_monotonic + max_adjacent_distance_; - const double my_closest_monotonic_padded = my_closest_monotonic - max_adjacent_distance_; - // How far this line reaches in the perpendicular direction -- the range at which the line overlaps other lines. - const double my_start = projectToVector((*polyline_it)->converted_->front(), perpendicular); - const double my_end = projectToVector((*polyline_it)->converted_->back(), perpendicular); - const double my_farthest = std::max(my_start, my_end); - const double my_closest = std::min(my_start, my_end); - const double my_farthest_padded = my_farthest + max_adjacent_distance_; - const double my_closest_padded = my_closest - max_adjacent_distance_; - - std::vector overlapping_lines; - for (auto overlapping_line = polyline_it + 1; overlapping_line != polylines.end(); ++overlapping_line) - { - // Don't go beyond the maximum adjacent distance. - const double start_their_projection = projectToMonotonicVector((*overlapping_line)->converted_->front()); - const double end_their_projection = projectToMonotonicVector((*overlapping_line)->converted_->back()); - const double their_farthest_projection = std::max(start_their_projection, end_their_projection); - const double their_closest_projection = std::min(start_their_projection, end_their_projection); - // Multiply by the length of the vector since we need to compare actual distances here. - if (their_closest_projection > my_farthest_monotonic_padded || my_closest_monotonic_padded > their_farthest_projection) - { - break; // Too far. This line and all subsequent lines are not adjacent anymore, even though they might be side-by-side. - } - - // Does this one overlap? - const double their_start = projectToVector((*overlapping_line)->converted_->front(), perpendicular); - const double their_end = projectToVector((*overlapping_line)->converted_->back(), perpendicular); - const double their_farthest = std::max(their_start, their_end); - const double their_closest = std::min(their_start, their_end); - /*There are 5 possible cases of overlapping: - - We are behind them, partially overlapping. my_start is between their_start and their_end. - - We are in front of them, partially overlapping. my_end is between their_start and their_end. - - We are a smaller line, they completely overlap us. Both my_start and my_end are between their_start and their_end. (Caught with the first 2 conditions already.) - - We are a bigger line, and completely overlap them. Both their_start and their_end are between my_start and my_end. - - Lines are exactly equal. Start and end are the same. (Caught with the previous condition too.)*/ - if ((my_closest_padded >= their_closest && my_closest_padded <= their_farthest) || (my_farthest_padded >= their_closest && my_farthest_padded <= their_farthest) - || (their_closest >= my_closest_padded && their_farthest <= my_farthest_padded)) - { - overlapping_lines.push_back(*overlapping_line); - } - } - - return overlapping_lines; - } + std::vector getOverlappingLines( + const typename std::vector::const_iterator& polyline_it, + const Point2D& perpendicular, + const std::vector& polylines, + const coord_t max_adjacent_distance); private: /*! @@ -554,6 +210,14 @@ class PathOrderMonotonic : public PathOrder return found_path.val->start_vertex_ == found_path.val->converted_->size() // Don't find any line already in the string. && vSize2(found_path.point - nearby_endpoint) < coincident_point_distance_ * coincident_point_distance_; // And only find close lines. } + + /*! + * Order the given lines, according to the given adjacence distance + * @param lines The lines to be ordered + * @param max_adjacent_distance The maximum distance for which to consider segments as adjacent + * @return The lines in monotonic order + */ + std::vector makeOrderedPath(const std::vector& lines, const coord_t max_adjacent_distance); }; } // namespace cura diff --git a/include/bridge.h b/include/bridge.h index ff7b73e22f..3e6d069109 100644 --- a/include/bridge.h +++ b/include/bridge.h @@ -4,20 +4,23 @@ #ifndef BRIDGE_H #define BRIDGE_H +#include + namespace cura { class Shape; -class Settings; +class SliceMeshStorage; class SliceDataStorage; class SupportLayer; +class AngleDegrees; /*! * \brief Computes the angle that lines have to take to bridge a certain shape * best. * * If the area should not be bridged, an angle of -1 is returned. - * \param settings The settings container to get settings from. + * \param mesh The mesh being processed. * \param skin_outline The shape to fill with lines. * \param storage The slice data storage where to find objects that the bridge * could rest on in previous layers. @@ -27,8 +30,8 @@ class SupportLayer; * \param supported_regions Pre-computed regions that the support layer would * support. */ -double bridgeAngle( - const Settings& settings, +std::optional bridgeAngle( + const SliceMeshStorage& mesh, const Shape& skin_outline, const SliceDataStorage& storage, const unsigned layer_nr, diff --git a/include/geometry/Point2D.h b/include/geometry/Point2D.h index c8aa57c8c3..ee9ba55977 100644 --- a/include/geometry/Point2D.h +++ b/include/geometry/Point2D.h @@ -68,6 +68,21 @@ class Point2D return Point2D(x_ * scale, y_ * scale); } + Point2D operator-(const Point2D& other) const + { + return Point2D(x_ - other.x_, y_ - other.y_); + } + + Point2D operator+(const Point2D& other) const + { + return Point2D(x_ + other.x_, y_ + other.y_); + } + + Point2D operator-() const + { + return Point2D(-x_, -y_); + } + static double dot(const Point2D& p0, const Point2D& p1) { return p0.x_ * p1.x_ + p0.y_ * p1.y_; diff --git a/include/geometry/PointMatrix.h b/include/geometry/PointMatrix.h index 1a22a74f06..2ae722222e 100644 --- a/include/geometry/PointMatrix.h +++ b/include/geometry/PointMatrix.h @@ -5,7 +5,6 @@ #define GEOMETRY_POINT_MATRIX_H #include -#include #include "geometry/Point2LL.h" @@ -13,6 +12,9 @@ namespace cura { +class AngleDegrees; +class AngleRadians; + class PointMatrix { public: @@ -20,61 +22,24 @@ class PointMatrix PointMatrix() noexcept = default; - explicit PointMatrix(double rotation) - { - rotation = rotation / 180 * std::numbers::pi; - matrix.at(0) = std::cos(rotation); - matrix.at(1) = -std::sin(rotation); - matrix.at(2) = -matrix.at(1); - matrix.at(3) = matrix.at(0); - } - - explicit PointMatrix(const Point2LL& p) - { - matrix.at(0) = static_cast(p.X); - matrix.at(1) = static_cast(p.Y); - double f = std::sqrt((matrix.at(0) * matrix.at(0)) + (matrix.at(1) * matrix.at(1))); - matrix.at(0) /= f; - matrix.at(1) /= f; - matrix.at(2) = -matrix.at(1); - matrix.at(3) = matrix.at(0); - } - - static PointMatrix scale(double s) - { - PointMatrix ret; - ret.matrix.at(0) = s; - ret.matrix.at(3) = s; - return ret; - } - - [[nodiscard]] Point2LL apply(const Point2LL& p) const - { - const auto x = static_cast(p.X); - const auto y = static_cast(p.Y); - return { std::llrint(x * matrix.at(0) + y * matrix.at(1)), std::llrint(x * matrix.at(2) + y * matrix.at(3)) }; - } + explicit PointMatrix(double rotation); + + explicit PointMatrix(const AngleDegrees& rotation); + + explicit PointMatrix(const AngleRadians& rotation); + + explicit PointMatrix(const Point2LL& p); + + static PointMatrix scale(double s); + + [[nodiscard]] Point2LL apply(const Point2LL& p) const; /*! * \warning only works on a rotation matrix! Output is incorrect for other types of matrix */ - [[nodiscard]] Point2LL unapply(const Point2LL& p) const - { - const auto x = static_cast(p.X); - const auto y = static_cast(p.Y); - return { std::llrint(x * matrix.at(0) + y * matrix.at(2)), std::llrint(x * matrix.at(1) + y * matrix.at(3)) }; - } - - [[nodiscard]] PointMatrix inverse() const - { - PointMatrix ret; - double det = matrix.at(0) * matrix.at(3) - matrix.at(1) * matrix.at(2); - ret.matrix.at(0) = matrix.at(3) / det; - ret.matrix.at(1) = -matrix.at(1) / det; - ret.matrix.at(2) = -matrix.at(2) / det; - ret.matrix.at(3) = matrix.at(0) / det; - return ret; - } + [[nodiscard]] Point2LL unapply(const Point2LL& p) const; + + [[nodiscard]] PointMatrix inverse() const; }; } // namespace cura diff --git a/include/infill.h b/include/infill.h index 273e53ea6e..7e8c17d7c6 100644 --- a/include/infill.h +++ b/include/infill.h @@ -214,6 +214,11 @@ class Infill const SliceMeshStorage* mesh = nullptr, const Shape& prevent_small_exposed_to_air = Shape()); + coord_t getLineDistance() const + { + return line_distance_; + } + /*! * Generate the wall toolpaths of an infill area. It will return the inner contour and set the inner-contour. * This function is called within the generate() function but can also be called stand-alone diff --git a/include/utils/HalfEdgeGraph.h b/include/utils/HalfEdgeGraph.h index 979b14ec76..76d16815e7 100644 --- a/include/utils/HalfEdgeGraph.h +++ b/include/utils/HalfEdgeGraph.h @@ -1,22 +1,19 @@ -//Copyright (c) 2020 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. +// Copyright (c) 2020 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef UTILS_HALF_EDGE_GRAPH_H #define UTILS_HALF_EDGE_GRAPH_H -#include #include - - +#include #include "HalfEdge.h" #include "HalfEdgeNode.h" -#include "SVG.h" namespace cura { - using namespace cura; +using namespace cura; template // types of data contained in nodes and edges class HalfEdgeGraph diff --git a/include/utils/SVG.h b/include/utils/SVG.h index 9eba431834..61b376e721 100644 --- a/include/utils/SVG.h +++ b/include/utils/SVG.h @@ -5,7 +5,9 @@ #define SVG_H #include +#include #include // for file output +#include #include @@ -17,7 +19,7 @@ namespace cura { -class Point3D; +class Point2D; class SVG : NoCopy { @@ -38,31 +40,143 @@ class SVG : NoCopy NONE }; - struct ColorObject + enum class FillRule { - bool is_enum_; - Color color_; - int r_, g_, b_; + None, + NonZero, + EvenOdd + }; + + struct RgbColor + { + int r{ 0 }; + int g{ 0 }; + int b{ 0 }; + + RgbColor() = default; + RgbColor(int red, int green, int blue) + : r(red) + , g(green) + , b(blue) + { + } + }; + + using ColorObject = std::variant; + + struct ElementAttributes + { + ColorObject color{ Color::BLACK }; + + ElementAttributes() = default; + + ElementAttributes(const ColorObject& color) + : color(color) + { + } + + virtual ~ElementAttributes() = default; + + virtual bool isDisplayed() const + { + return (std::holds_alternative(color) && std::get(color) != Color::NONE) || std::holds_alternative(color); + } + }; + + struct SurfaceAttributes : ElementAttributes + { + SurfaceAttributes() = default; - ColorObject(Color color) - : is_enum_(true) - , color_(color) + SurfaceAttributes(const ColorObject& color) + : ElementAttributes(color) { } - ColorObject(int r, int g, int b) - : is_enum_(false) - , r_(r) - , g_(g) - , b_(b) + ~SurfaceAttributes() override = default; + }; + + struct LineAttributes : ElementAttributes + { + double width{ 0.4 }; + std::vector dash_array{}; + + LineAttributes() = default; + + LineAttributes(const ColorObject& color, const double width) + : ElementAttributes(color) + , width(width) + { + } + + LineAttributes(const ColorObject& color) + : ElementAttributes(color) + { + } + + LineAttributes(const double width) + : ElementAttributes() + , width(width) { } - static ColorObject toRgb(const Color color); + ~LineAttributes() override = default; + + bool isDisplayed() const override + { + return ElementAttributes::isDisplayed() && width > 0.0; + } + }; + + struct VerticesAttributes : ElementAttributes + { + double radius{ 0.2 }; + bool write_coords{ false }; + double font_size{ 10 }; + + VerticesAttributes() = default; + + VerticesAttributes(const ColorObject& color, const double radius) + : ElementAttributes(color) + , radius(radius) + { + } + + VerticesAttributes(const ColorObject& color) + : ElementAttributes(color) + { + } + + VerticesAttributes(const double radius) + : ElementAttributes() + , radius(radius) + { + } + + VerticesAttributes(const bool write_coords) + : ElementAttributes() + , write_coords(write_coords) + { + } + + ~VerticesAttributes() override = default; + + bool isDisplayed() const override + { + return ElementAttributes::isDisplayed() && (radius > 0.0 || (write_coords && font_size > 0.0)); + } + }; + + struct VisualAttributes + { + SurfaceAttributes surface{ Color::NONE }; + LineAttributes line{ Color::NONE, 0.0 }; + VerticesAttributes vertices{ Color::NONE, 0.0 }; }; private: - std::string toString(const ColorObject& color) const; + static std::string toString(const ColorObject& color); + static std::string toString(const std::vector& dash_array); + static std::string toString(const FillRule fill_rule); void handleFlush(const bool flush) const; FILE* out_; // the output file @@ -70,20 +184,21 @@ class SVG : NoCopy const Point2LL aabb_size_; const Point2LL canvas_size_; const double scale_; - ColorObject background_; size_t layer_nr_ = 1; bool output_is_html_; public: - SVG(std::string filename, const AABB aabb, const Point2LL canvas_size = Point2LL(1024, 1024), const ColorObject background = Color::NONE); - SVG(std::string filename, const AABB aabb, const double scale, const ColorObject background = Color::NONE); - SVG(std::string filename, const AABB aabb, const double scale, const Point2LL canvas_size, const ColorObject background = Color::NONE); + SVG(const std::string& filename, const AABB& aabb, const Point2LL& canvas_size = Point2LL(1024, 1024), const ColorObject& background = Color::NONE); + SVG(const std::string& filename, const AABB& aabb, const double scale, const ColorObject& background = Color::NONE); + SVG(const std::string& filename, const AABB& aabb, const double scale, const Point2LL& canvas_size, const ColorObject& background = Color::NONE); ~SVG(); static std::string toString(const Color color); + static RgbColor toRgb(const Color color); + /*! * get the scaling factor applied to convert real space to canvas space */ @@ -99,70 +214,32 @@ class SVG : NoCopy /*! * transform a point in real space to canvas space with more precision */ - Point3D transformF(const Point2LL& p) const; + Point2D transformF(const Point2LL& p) const; void writeComment(const std::string& comment) const; - void writeAreas(const Shape& polygons, const ColorObject color = Color::GRAY, const ColorObject outline_color = Color::BLACK, const double stroke_width = 1.0) const; + void write(const Shape& shape, const VisualAttributes& visual_attributes, const bool flush = true) const; - void writeAreas(const Polygon& polygon, const ColorObject color = Color::GRAY, const ColorObject outline_color = Color::BLACK, const double stroke_width = 1.0) const; + template + void write(const LinesSet& lines, const VisualAttributes& visual_attributes, const bool flush = true, const FillRule fill_rule = FillRule::None) const; - void writePoint(const Point2LL& p, const bool write_coords = false, const double size = 5.0, const ColorObject color = Color::BLACK) const; + void write(const OpenPolyline& line, const VisualAttributes& visual_attributes, const bool flush = true) const; - void writePoints(const Polygon& poly, const bool write_coords = false, const double size = 5.0, const ColorObject color = Color::BLACK) const; + void write(const ClosedPolyline& line, const VisualAttributes& visual_attributes, const bool flush = true) const; - void writePoints(const Shape& polygons, const bool write_coords = false, const double size = 5.0, const ColorObject color = Color::BLACK) const; + void write(const Point2LL& start, const Point2LL& end, const VisualAttributes& visual_attributes, const bool flush = true) const; - /*! - * \brief Draws a polyline on the canvas. - * - * The polyline is the set of line segments between each pair of consecutive - * points in the specified vector. - * - * \param polyline A set of points between which line segments must be - * drawn. - * \param color The colour of the line segments. If this is not specified, - * black will be used. - */ - void writeLines(const std::vector& polyline, const ColorObject color = Color::BLACK) const; + void write(const PointsSet& points, const VerticesAttributes& visual_attributes, const bool flush = true) const; - void writeLine(const Polyline& line, const ColorObject color = Color::BLACK, const double stroke_width = 1.0) const; + void write(const Point2LL& point, const VerticesAttributes& visual_attributes, const bool flush = true) const; - void writeLine(const Point2LL& a, const Point2LL& b, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; + void write(const std::string& text, const Point2LL& p, const VerticesAttributes& vertices_attributes, const bool flush = true) const; void writeArrow(const Point2LL& a, const Point2LL& b, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const double head_size = 5.0) const; - void writeLineRGB(const Point2LL& from, const Point2LL& to, const int r = 0, const int g = 0, const int b = 0, const double stroke_width = 1.0) const; - - /*! - * \brief Draws a dashed line on the canvas from point A to point B. - * - * This is useful in the case where multiple lines may overlap each other. - * - * \param a The starting endpoint of the line. - * \param b The ending endpoint of the line. - * \param color The stroke colour of the line. - */ - void writeDashedLine(const Point2LL& a, const Point2LL& b, ColorObject color = Color::BLACK) const; - template void printf(const char* txt, Args&&... args) const; - void writeText(const Point2LL& p, const std::string& txt, const ColorObject color = Color::BLACK, const double font_size = 10.0) const; - - void writePolygons(const Shape& polys, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; - - void writePolygon(const Polygon& poly, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; - - void writePolylines(const Shape& polys, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; - - template - void writePolylines(const LinesSet& lines, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; - - void writePolyline(const Polygon& poly, const ColorObject color = Color::BLACK, const double stroke_width = 1.0) const; - - void writePolyline(const Polyline& poly, const ColorObject color = Color::BLACK, const double stroke_width = 1.0, const bool flush = true) const; - /*! * Draw variable-width paths into the image. * @@ -205,7 +282,7 @@ class SVG : NoCopy * \param stroke_width The width of the grid lines. * \param font_size The size of the font to write the coordinates with. */ - void writeCoordinateGrid(const coord_t grid_size = MM2INT(1), const Color color = Color::BLACK, const double stroke_width = 0.1, const double font_size = 10.0) const; + void writeCoordinateGrid(const coord_t grid_size, const VisualAttributes& visual_attributes, const bool flush = true) const; /*! * Draws the provided Voronoi diagram. @@ -236,6 +313,9 @@ class SVG : NoCopy writeLine(Point(v0->x(), v0->y()), Point(v1->x(), v1->y()), color, stroke_width); } } + +private: + void writePathPoints(const Polyline& line) const; }; template diff --git a/include/utils/SparseLineGrid.h b/include/utils/SparseLineGrid.h index 197767d30b..b0817d425c 100644 --- a/include/utils/SparseLineGrid.h +++ b/include/utils/SparseLineGrid.h @@ -114,15 +114,13 @@ void SGI_THIS::debugHTML(std::string filename) rb.Y = -SparseGrid::cell_size_; } // svg.writePoint(lb, true, 1); - svg.writeLine(lb, lt, SVG::Color::GRAY); - svg.writeLine(lt, rt, SVG::Color::GRAY); - svg.writeLine(rt, rb, SVG::Color::GRAY); - svg.writeLine(rb, lb, SVG::Color::GRAY); + svg.write(lb, lt, { .line = { SVG::Color::GRAY } }); + svg.write(lt, rt, { .line = { SVG::Color::GRAY } }); + svg.write(rt, rb, { .line = { SVG::Color::GRAY } }); + svg.write(rb, lb, { .line = { SVG::Color::GRAY } }); std::pair line = m_locator(cell.second); - svg.writePoint(line.first, true); - svg.writePoint(line.second, true); - svg.writeLine(line.first, line.second, SVG::Color::BLACK); + svg.write(line.first, line.second, { .line = { SVG::Color::BLACK }, .vertices = { true } }); } } diff --git a/include/utils/VoronoiUtils.h b/include/utils/VoronoiUtils.h index 287b6765b1..7853a10081 100644 --- a/include/utils/VoronoiUtils.h +++ b/include/utils/VoronoiUtils.h @@ -10,7 +10,6 @@ #include #include "PolygonsSegmentIndex.h" -#include "SVG.h" namespace cura diff --git a/include/utils/linearAlg2D.h b/include/utils/linearAlg2D.h index 56beab5608..4720dca791 100644 --- a/include/utils/linearAlg2D.h +++ b/include/utils/linearAlg2D.h @@ -4,6 +4,8 @@ #ifndef UTILS_LINEAR_ALG_2D_H #define UTILS_LINEAR_ALG_2D_H +#include + #include "geometry/Point2LL.h" namespace cura @@ -84,6 +86,8 @@ class LinearAlg2D static bool lineLineIntersection(const Point2LL& a, const Point2LL& b, const Point2LL& c, const Point2LL& d, Point2LL& output); + static std::optional lineHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y); + /*! * Find whether a point projected on a line segment would be projected to * - properly on the line : zero returned diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index 1a6b35485e..0e9f3ab721 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -3521,7 +3521,7 @@ void FffGcodeWriter::processRoofingFlooring( const Ratio skin_density = 1.0; const coord_t skin_overlap = 0; // skinfill already expanded over the roofing areas; don't overlap with perimeters - const bool monotonic = mesh.settings.get(settings_names.monotonic); + const LinesOrderingMethod ordering = mesh.settings.get(settings_names.monotonic) ? LinesOrderingMethod::Monotonic : LinesOrderingMethod::Basic; constexpr bool is_roofing_flooring = true; processSkinPrintFeature( storage, @@ -3534,7 +3534,7 @@ void FffGcodeWriter::processRoofingFlooring( roofing_angle, skin_overlap, skin_density, - monotonic, + ordering, is_roofing_flooring, added_something); } @@ -3577,6 +3577,7 @@ void FffGcodeWriter::processTopBottom( const bool bridge_enable_more_layers = bridge_settings_enabled && mesh.settings.get("bridge_enable_more_layers"); const Ratio support_threshold = bridge_settings_enabled ? mesh.settings.get("bridge_skin_support_threshold") : 0.0_r; const size_t bottom_layers = mesh.settings.get("bottom_layers"); + std::optional forced_small_area_width; // if support is enabled, consider the support outlines so we don't generate bridges over support @@ -3602,38 +3603,39 @@ void FffGcodeWriter::processTopBottom( Shape supported_skin_part_regions; - const double angle = bridgeAngle(mesh.settings, skin_part.skin_fill, storage, layer_nr, bridge_layer, support_layer, supported_skin_part_regions); + const std::optional bridge_angle = bridgeAngle(mesh, skin_part.skin_fill, storage, layer_nr, bridge_layer, support_layer, supported_skin_part_regions); - if (angle > -1 || (support_threshold > 0 && (supported_skin_part_regions.area() / (skin_part.skin_fill.area() + 1) < support_threshold))) + if (bridge_angle.has_value() || (support_threshold > 0 && (supported_skin_part_regions.area() / (skin_part.skin_fill.area() + 1) < support_threshold))) { - if (angle > -1) + if (bridge_angle.has_value()) { switch (bridge_layer) { default: case 1: - skin_angle = angle; + skin_angle = bridge_angle.value(); break; case 2: if (bottom_layers > 2) { // orientate second bridge skin at +45 deg to first - skin_angle = angle + 45; + skin_angle = bridge_angle.value() + 45; } else { // orientate second bridge skin at 90 deg to first - skin_angle = angle + 90; + skin_angle = bridge_angle.value() + 90; } break; case 3: // orientate third bridge skin at 135 (same result as -45) deg to first - skin_angle = angle + 135; + skin_angle = bridge_angle.value() + 135; break; } } + forced_small_area_width = 0; pattern = EFillMethod::LINES; // force lines pattern when bridging if (bridge_settings_enabled) { @@ -3703,7 +3705,21 @@ void FffGcodeWriter::processTopBottom( fan_speed = mesh.settings.get("support_supported_skin_fan_speed") * 100.0; } } - const bool monotonic = mesh.settings.get("skin_monotonic"); + + LinesOrderingMethod ordering; + if (is_bridge_skin) + { + ordering = mesh.settings.get("bridge_interlace_lines") ? LinesOrderingMethod::Interlaced : LinesOrderingMethod::Basic; + } + else if (mesh.settings.get("skin_monotonic")) + { + ordering = LinesOrderingMethod::Monotonic; + } + else + { + ordering = LinesOrderingMethod::Basic; + } + constexpr bool is_roofing_flooring = false; processSkinPrintFeature( storage, @@ -3716,10 +3732,11 @@ void FffGcodeWriter::processTopBottom( skin_angle, skin_overlap, skin_density, - monotonic, + ordering, is_roofing_flooring, added_something, - fan_speed); + fan_speed, + forced_small_area_width); } void FffGcodeWriter::processSkinPrintFeature( @@ -3733,10 +3750,11 @@ void FffGcodeWriter::processSkinPrintFeature( const AngleDegrees skin_angle, const coord_t skin_overlap, const Ratio skin_density, - const bool monotonic, + const LinesOrderingMethod ordering, const bool is_roofing_flooring, bool& added_something, - double fan_speed) const + double fan_speed, + std::optional forced_small_area_width) const { Shape skin_polygons; OpenLinesSet skin_lines; @@ -3750,7 +3768,7 @@ void FffGcodeWriter::processSkinPrintFeature( coord_t max_resolution = mesh.settings.get("meshfix_maximum_resolution"); coord_t max_deviation = mesh.settings.get("meshfix_maximum_deviation"); const Point2LL infill_origin; - const bool skip_line_stitching = monotonic; + const bool skip_line_stitching = ordering == LinesOrderingMethod::Monotonic || ordering == LinesOrderingMethod::Interlaced; constexpr bool fill_gaps = true; constexpr bool connected_zigzags = false; constexpr bool use_endpieces = true; @@ -3759,7 +3777,8 @@ void FffGcodeWriter::processSkinPrintFeature( constexpr coord_t pocket_size = 0; const bool small_areas_on_surface = mesh.settings.get("small_skin_on_surface"); const coord_t line_width = config.getLineWidth(); - const coord_t small_area_width = (small_areas_on_surface || ! is_roofing_flooring) ? mesh.settings.get("small_skin_width") : line_width / 4; + const coord_t small_area_width + = forced_small_area_width.value_or((small_areas_on_surface || ! is_roofing_flooring) ? mesh.settings.get("small_skin_width") : line_width / 4); const auto& current_layer = mesh.layers[gcode_layer.getLayerNr()]; const auto& exposed_to_air = current_layer.top_surface.areas.unionPolygons(current_layer.bottom_surface); @@ -3845,14 +3864,15 @@ void FffGcodeWriter::processSkinPrintFeature( gcode_layer.addPolygonsByOptimizer(skin_polygons, config, mesh.settings); } - if (monotonic) + if (ordering == LinesOrderingMethod::Monotonic || ordering == LinesOrderingMethod::Interlaced) { const coord_t exclude_distance = config.getLineWidth() * 0.8; + const bool interlaced = ordering == LinesOrderingMethod::Interlaced; const AngleRadians monotonic_direction = AngleRadians(skin_angle); constexpr Ratio flow = 1.0_r; const coord_t max_adjacent_distance - = config.getLineWidth() + = infill_comp.getLineDistance() * 1.1; // Lines are considered adjacent if they are 1 line width apart, with 10% extra play. The monotonic order is enforced if they are adjacent. if (pattern == EFillMethod::GRID || pattern == EFillMethod::LINES || pattern == EFillMethod::TRIANGLES || pattern == EFillMethod::CUBIC || pattern == EFillMethod::TETRAHEDRAL || pattern == EFillMethod::QUARTER_CUBIC || pattern == EFillMethod::CUBICSUBDIV || pattern == EFillMethod::LIGHTNING) @@ -3867,13 +3887,25 @@ void FffGcodeWriter::processSkinPrintFeature( exclude_distance, mesh.settings.get("infill_wipe_dist"), flow, - fan_speed); + fan_speed, + interlaced); } else { const SpaceFillType space_fill_type = (pattern == EFillMethod::ZIG_ZAG) ? SpaceFillType::PolyLines : SpaceFillType::Lines; constexpr coord_t wipe_dist = 0; - gcode_layer.addLinesMonotonic(area, skin_lines, config, space_fill_type, monotonic_direction, max_adjacent_distance, exclude_distance, wipe_dist, flow, fan_speed); + gcode_layer.addLinesMonotonic( + area, + skin_lines, + config, + space_fill_type, + monotonic_direction, + max_adjacent_distance, + exclude_distance, + wipe_dist, + flow, + fan_speed, + interlaced); } } else diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index ef14fc4e1e..60a129f06a 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -2862,7 +2862,8 @@ void LayerPlan::addLinesMonotonic( const coord_t exclude_distance, const coord_t wipe_dist, const Ratio flow_ratio, - const double fan_speed) + const double fan_speed, + const bool interlaced) { const Shape exclude_areas = area.createTubeShape(exclude_distance, exclude_distance); const coord_t exclude_dist2 = exclude_distance * exclude_distance; @@ -2882,7 +2883,7 @@ void LayerPlan::addLinesMonotonic( }; // Order monotonically, except for line-segments which stay in the excluded areas (read: close to the walls) consecutively. - PathOrderMonotonic order(monotonic_direction, max_adjacent_distance, last_position); + PathOrderMonotonic order(monotonic_direction, max_adjacent_distance, last_position, interlaced); OpenLinesSet left_over; bool last_would_have_been_excluded = false; for (size_t line_idx = 0; line_idx < line_order.paths_.size(); ++line_idx) diff --git a/src/PathOrderMonotonic.cpp b/src/PathOrderMonotonic.cpp new file mode 100644 index 0000000000..9826b1c0a1 --- /dev/null +++ b/src/PathOrderMonotonic.cpp @@ -0,0 +1,436 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "PathOrderMonotonic.h" + +#include //To track monotonic sequences. +#include //To track starting points of monotonic sequences. + + +namespace cura +{ + +template +void PathOrderMonotonic::optimize() +{ + if (this->paths_.empty()) + { + return; + } + + // Get the vertex data and store it in the paths. + for (Path& path : this->paths_) + { + path.converted_ = &path.getVertexData(); + } + + std::vector reordered; // To store the result in. At the end, we'll std::swap with the real paths. + reordered.reserve(this->paths_.size()); + + // First print all the looping polygons, if there are any. + std::vector polylines; // Also find all polylines and store them in a vector that we can sort in-place without making copies all the time. + this->detectLoops(); // Always filter out loops. We don't specifically want to print those in monotonic order. + for (Path& path : this->paths_) + { + if (path.is_closed_ || path.vertices_->size() <= 1) + { + reordered.push_back(path); + } + else + { + polylines.push_back(&path); + // Assign an invalid starting vertex to indicate we don't know the starting point yet. + polylines.back()->start_vertex_ = polylines.back()->converted_->size(); + } + } + + // Sort the polylines by their projection on the monotonic vector. This helps find adjacent lines quickly. + std::stable_sort( + polylines.begin(), + polylines.end(), + [this](Path* a, Path* b) + { + const double a_start_projection = projectToMonotonicVector(a->converted_->front()); + const double a_end_projection = projectToMonotonicVector(a->converted_->back()); + const double a_projection = std::min(a_start_projection, a_end_projection); // The projection of a path is the endpoint furthest back of the two endpoints. + + const double b_start_projection = projectToMonotonicVector(b->converted_->front()); + const double b_end_projection = projectToMonotonicVector(b->converted_->back()); + const double b_projection = std::min(b_start_projection, b_end_projection); + + return a_projection < b_projection; + }); + + if (interlaced_) + { + // Separate the lines in two sets by adjacency + std::vector lines_pass1; + std::vector lines_pass2; + lines_pass1.reserve(polylines.size() / 2); // Rough estimation + lines_pass2.reserve(polylines.size() / 2); + + std::vector* current_pass = nullptr; + double current_projection = 0; + + for (Path* path : polylines) + { + const double path_projection = projectToMonotonicVector(path->converted_->front()); + if (current_pass == nullptr) + { + current_pass = &lines_pass1; + current_projection = path_projection; + } + else if ((path_projection - current_projection) > (max_adjacent_distance_ / 2)) + { + current_pass = current_pass == &lines_pass1 ? &lines_pass2 : &lines_pass1; + current_projection = path_projection; + } + + current_pass->push_back(path); + } + + // Process the 2 sets independently and concatenate them to get the final 2-pass monotonic order + std::vector ordered_lines_pass1 = makeOrderedPath(lines_pass1, max_adjacent_distance_ * 2); + std::vector ordered_lines_pass2 = makeOrderedPath(lines_pass2, max_adjacent_distance_ * 2); + + reordered.insert(reordered.begin(), std::make_move_iterator(ordered_lines_pass1.begin()), std::make_move_iterator(ordered_lines_pass1.end())); + reordered.insert(reordered.begin(), std::make_move_iterator(ordered_lines_pass2.begin()), std::make_move_iterator(ordered_lines_pass2.end())); + } + else + { + std::vector lines_ordered = makeOrderedPath(polylines, max_adjacent_distance_); + reordered.insert(reordered.begin(), std::make_move_iterator(lines_ordered.begin()), std::make_move_iterator(lines_ordered.end())); + } + + std::swap(reordered, this->paths_); // Store the resulting list in the main paths. +} + +template +std::deque::Path*> PathOrderMonotonic::findPolylineString(Path* polyline, const SparsePointGridInclusive& line_bucket_grid) +{ + std::deque result; + if (polyline->converted_->empty()) + { + return result; + } + + // Find the two endpoints of the polyline string, on either side. + result.push_back(polyline); + polyline->start_vertex_ = 0; + Point2LL first_endpoint = polyline->converted_->front(); + Point2LL last_endpoint = polyline->converted_->back(); + std::vector> lines_before = line_bucket_grid.getNearby(first_endpoint, coincident_point_distance_); + auto close_line_before = std::find_if( + lines_before.begin(), + lines_before.end(), + [first_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) + { + return canConnectToPolyline(first_endpoint, found_path); + }); + std::vector> lines_after = line_bucket_grid.getNearby(last_endpoint, coincident_point_distance_); + auto close_line_after = std::find_if( + lines_after.begin(), + lines_after.end(), + [last_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) + { + return canConnectToPolyline(last_endpoint, found_path); + }); + + while (close_line_before != lines_before.end()) + { + Path* first = close_line_before->val; + result.push_front(first); // Store this one in the sequence. It's a good one. + size_t farthest_vertex = getFarthestEndpoint(first, close_line_before->point); // Get to the opposite side. + first->start_vertex_ = farthest_vertex; + first->backwards_ = farthest_vertex != 0; + first_endpoint = (*first->converted_)[farthest_vertex]; + lines_before = line_bucket_grid.getNearby(first_endpoint, coincident_point_distance_); + close_line_before = std::find_if( + lines_before.begin(), + lines_before.end(), + [first_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) + { + return canConnectToPolyline(first_endpoint, found_path); + }); + } + while (close_line_after != lines_after.end()) + { + Path* last = close_line_after->val; + result.push_back(last); + size_t farthest_vertex = getFarthestEndpoint(last, close_line_after->point); // Get to the opposite side. + last->start_vertex_ = (farthest_vertex == 0) ? last->converted_->size() - 1 : 0; + last->backwards_ = farthest_vertex != 0; + last_endpoint = (*last->converted_)[farthest_vertex]; + lines_after = line_bucket_grid.getNearby(last_endpoint, coincident_point_distance_); + close_line_after = std::find_if( + lines_after.begin(), + lines_after.end(), + [last_endpoint](SparsePointGridInclusiveImpl::SparsePointGridInclusiveElem found_path) + { + return canConnectToPolyline(last_endpoint, found_path); + }); + } + + // Figure out which of the two endpoints to start with: The one monotonically earliest. + const double first_projection = projectToMonotonicVector(first_endpoint); + const double last_projection = projectToMonotonicVector(last_endpoint); + // If the last endpoint should be printed first (unlikely due to monotonic start, but possible), flip the whole polyline! + if (last_projection < first_projection) + { + std::reverse(result.begin(), result.end()); + for (Path* path : result) // Also reverse their start_vertex. + { + path->start_vertex_ = (path->start_vertex_ == 0) ? path->converted_->size() - 1 : 0; + path->backwards_ = ! path->backwards_; + } + } + + if (result.size() == 1) + { + result[0]->start_vertex_ = result[0]->converted_->size(); // Reset start vertex as "unknown" again if it's not a string of polylines. + } + return result; +} + +template +std::vector::Path*> PathOrderMonotonic::getOverlappingLines( + const typename std::vector::const_iterator& polyline_it, + const Point2D& perpendicular, + const std::vector& polylines, + const coord_t max_adjacent_distance) +{ + // How far this extends in the monotonic direction, to make sure we only go up to max_adjacent_distance in that direction. + const double start_monotonic = projectToMonotonicVector((*polyline_it)->converted_->front()); + const double end_monotonic = projectToMonotonicVector((*polyline_it)->converted_->back()); + const double my_farthest_monotonic = std::max(start_monotonic, end_monotonic); + const double my_closest_monotonic = std::min(start_monotonic, end_monotonic); + const double my_farthest_monotonic_padded = my_farthest_monotonic + max_adjacent_distance; + const double my_closest_monotonic_padded = my_closest_monotonic - max_adjacent_distance; + // How far this line reaches in the perpendicular direction -- the range at which the line overlaps other lines. + const double my_start = projectToVector((*polyline_it)->converted_->front(), perpendicular); + const double my_end = projectToVector((*polyline_it)->converted_->back(), perpendicular); + const double my_farthest = std::max(my_start, my_end); + const double my_closest = std::min(my_start, my_end); + const double my_farthest_padded = my_farthest + max_adjacent_distance; + const double my_closest_padded = my_closest - max_adjacent_distance; + + std::vector overlapping_lines; + for (auto overlapping_line = polyline_it + 1; overlapping_line != polylines.end(); ++overlapping_line) + { + // Don't go beyond the maximum adjacent distance. + const double start_their_projection = projectToMonotonicVector((*overlapping_line)->converted_->front()); + const double end_their_projection = projectToMonotonicVector((*overlapping_line)->converted_->back()); + const double their_farthest_projection = std::max(start_their_projection, end_their_projection); + const double their_closest_projection = std::min(start_their_projection, end_their_projection); + // Multiply by the length of the vector since we need to compare actual distances here. + if (their_closest_projection > my_farthest_monotonic_padded || my_closest_monotonic_padded > their_farthest_projection) + { + break; // Too far. This line and all subsequent lines are not adjacent anymore, even though they might be side-by-side. + } + + // Does this one overlap? + const double their_start = projectToVector((*overlapping_line)->converted_->front(), perpendicular); + const double their_end = projectToVector((*overlapping_line)->converted_->back(), perpendicular); + const double their_farthest = std::max(their_start, their_end); + const double their_closest = std::min(their_start, their_end); + /*There are 5 possible cases of overlapping: + - We are behind them, partially overlapping. my_start is between their_start and their_end. + - We are in front of them, partially overlapping. my_end is between their_start and their_end. + - We are a smaller line, they completely overlap us. Both my_start and my_end are between their_start and their_end. (Caught with the first 2 conditions already.) + - We are a bigger line, and completely overlap them. Both their_start and their_end are between my_start and my_end. + - Lines are exactly equal. Start and end are the same. (Caught with the previous condition too.)*/ + if ((my_closest_padded >= their_closest && my_closest_padded <= their_farthest) || (my_farthest_padded >= their_closest && my_farthest_padded <= their_farthest) + || (their_closest >= my_closest_padded && their_farthest <= my_farthest_padded)) + { + overlapping_lines.push_back(*overlapping_line); + } + } + + return overlapping_lines; +} + +template +std::vector::Path> PathOrderMonotonic::makeOrderedPath(const std::vector& polylines, const coord_t max_adjacent_distance) +{ + std::vector reordered; // To store the result in. At the end, we'll std::swap with the real paths. + reordered.reserve(polylines.size()); + + // Create a bucket grid to be able to find adjacent lines quickly. + SparsePointGridInclusive line_bucket_grid(MM2INT(2)); // Grid size of 2mm. + for (Path* polyline : polylines) + { + if (! polyline->converted_->empty()) + { + line_bucket_grid.insert(polyline->converted_->front(), polyline); + line_bucket_grid.insert(polyline->converted_->back(), polyline); + } + } + + // Create sequences of line segments that get printed together in a monotonic direction. + // There are several constraints we impose here: + // - Strings of incident polylines are printed in sequence. That is, if their endpoints are incident. + // - The endpoint of the string that is earlier in the monotonic direction will get printed first. + // - The start_vertex of this line will already be set to indicate where to start from. + // - If a line overlaps with another line in the perpendicular direction, and is within max_adjacent_distance (~1 line width) in the monotonic direction, it must be + // printed in monotonic order. + // - The earlier line is marked as being in sequence with the later line. + // - The later line is no longer a starting point, unless there are multiple adjacent lines before it. + // The ``starting_lines`` set indicates possible locations to start from. Each starting line represents one "sequence", which is either a set of adjacent line segments or a + // string of polylines. The ``connections`` map indicates, starting from each starting segment, the sequence of line segments to print in order. Note that for performance + // reasons, the ``connections`` map will sometimes link the end of one segment to the start of the next segment. This link should be ignored. + const Point2D perpendicular = monotonic_vector_.rotated90CCW(); // To project on to detect adjacent lines. + + std::unordered_set connected_lines; // Lines that are reachable from one of the starting lines through its connections. + std::unordered_set starting_lines; // Starting points of a linearly connected segment. + std::unordered_map connections; // For each polyline, which polyline it overlaps with, closest in the projected order. + + for (auto polyline_it = polylines.begin(); polyline_it != polylines.end(); polyline_it++) + { + if (connections.contains(*polyline_it)) // Already connected this one through a polyline. + { + continue; + } + // First find out if this polyline is part of a string of polylines. + std::deque polystring = findPolylineString(*polyline_it, line_bucket_grid); + + // If we're part of a string of polylines, connect up the whole string and mark all of them as being connected. + if (polystring.size() > 1) + { + starting_lines.insert(polystring[0]); + for (size_t i = 0; i < polystring.size() - 1; ++i) // Iterate over every pair of adjacent polylines in the string (so skip the last one)! + { + connections[polystring[i]] = polystring[i + 1]; + connected_lines.insert(polystring[i + 1]); + + // Even though we chain polylines, we still want to find lines that they overlap with. + // The strings of polylines may still have weird shapes which interweave with other strings of polylines or loose lines. + // So when a polyline string comes into contact with other lines, we still want to guarantee their order. + // So here we will look for which lines they come into contact with, and thus mark those as possible starting points, so that they function as a new junction. + const std::vector overlapping_lines + = getOverlappingLines(std::find(polylines.begin(), polylines.end(), polystring[i]), perpendicular, polylines, max_adjacent_distance); + for (Path* overlapping_line : overlapping_lines) + { + if (std::find(polystring.begin(), polystring.end(), overlapping_line) + == polystring.end()) // Mark all overlapping lines not part of the string as possible starting points. + { + starting_lines.insert(overlapping_line); + starting_lines.insert(polystring[i + 1]); // Also be able to re-start from this point in the string. + } + } + } + } + else // Not a string of polylines, but simply adjacent line segments. + { + if (! connected_lines.contains(*polyline_it)) // Nothing connects to this line yet. + { + starting_lines.insert(*polyline_it); // This is a starting point then. + } + const std::vector overlapping_lines = getOverlappingLines(polyline_it, perpendicular, polylines, max_adjacent_distance); + if (overlapping_lines.size() == 1) // If we're not a string of polylines, but adjacent to only one other polyline, create a sequence of polylines. + { + connections[*polyline_it] = overlapping_lines[0]; + if (connected_lines.contains(overlapping_lines[0])) // This line was already connected to. + { + starting_lines.insert(overlapping_lines[0]); // Multiple lines connect to it, so we must be able to start there. + } + else + { + connected_lines.insert(overlapping_lines[0]); + } + } + else // Either 0 (the for loop terminates immediately) or multiple overlapping lines. For multiple lines we need to mark all of them a starting position. + { + for (Path* overlapping_line : overlapping_lines) + { + starting_lines.insert(overlapping_line); + } + } + } + } + + struct LineProjections + { + coord_t min; + coord_t max; + + explicit LineProjections(Path* path, const Point2D& monotonic_vector) + { + const coord_t start_projection = std::llround(projectToVector(path->converted_->front(), monotonic_vector) / precision_factor); + const coord_t end_projection = std::llround(projectToVector(path->converted_->back(), monotonic_vector) / precision_factor); + std::tie(min, max) = std::minmax(start_projection, end_projection); + } + + bool operator<(const LineProjections& other) const + { + return min < other.min || (min == other.min && max < other.max); + } + }; + + // Pre-order lines in a multi-map, so that aligned lines will end up in the same bucket and we can process them in a row + std::multimap pre_ordered_lines; + for (Path* starting_line : starting_lines) + { + pre_ordered_lines.insert(std::make_pair(LineProjections(starting_line, monotonic_vector_), starting_line)); + } + + // Now order the lines, by finding the closest line from the current position in the current bucket (row) + Point2LL current_pos = this->start_point_; + while (! pre_ordered_lines.empty()) + { + const LineProjections first_projection_key = pre_ordered_lines.begin()->first; + auto lines_on_row_range = pre_ordered_lines.equal_range(first_projection_key); + + coord_t closest_distance = std::numeric_limits::max(); + auto closest_next_line_iterator = lines_on_row_range.second; + bool closest_backwards = false; + for (auto iterator = lines_on_row_range.first; iterator != lines_on_row_range.second; ++iterator) + { + const Path* path = iterator->second; + const coord_t dist_start = vSize2(current_pos - path->converted_->front()); + const coord_t dist_end = vSize2(current_pos - path->converted_->back()); + if (dist_start < closest_distance) + { + closest_next_line_iterator = iterator; + closest_distance = dist_start; + closest_backwards = false; + } + if (dist_end < closest_distance) + { + closest_next_line_iterator = iterator; + closest_distance = dist_end; + closest_backwards = true; + } + } + + Path* closest_path = closest_next_line_iterator->second; + setStartVertex(closest_path, closest_backwards); + current_pos = (*closest_path->converted_)[closest_path->converted_->size() - 1 - closest_path->start_vertex_]; // Opposite of the start vertex. + reordered.push_back(*closest_path); + pre_ordered_lines.erase(closest_next_line_iterator); + + // Now add the (adjacent) lines to be processed together with this one + auto connection = connections.find(closest_path); + std::unordered_map checked_connections; // Which connections have already been iterated over + auto checked_connection = checked_connections.find(closest_path); + + while (connection != connections.end() // Stop if the sequence ends + && starting_lines.find(connection->second) == starting_lines.end() // or if we hit another starting point. + && (checked_connection == checked_connections.end() + || checked_connection->second != connection->second)) // or if we have already checked the connection (to avoid falling into a cyclical connection) + { + checked_connections.insert({ connection->first, connection->second }); + Path* line = connection->second; + optimizeClosestStartPoint(line, current_pos); + reordered.push_back(*line); // Plan this line in, to be printed next! + connection = connections.find(line); + checked_connection = checked_connections.find(line); + } + } + + return reordered; +} + +// Template functions instantiations +template void PathOrderMonotonic::optimize(); + +} // namespace cura diff --git a/src/bridge.cpp b/src/bridge.cpp index 55b7b8e8cd..0206f6d6bf 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -3,18 +3,341 @@ #include "bridge.h" -#include "geometry/OpenLinesSet.h" -#include "geometry/OpenPolyline.h" +#include + +#include "geometry/Point2D.h" +#include "geometry/PointMatrix.h" #include "geometry/Polygon.h" +#include "settings/EnumSettings.h" #include "settings/types/Ratio.h" #include "sliceDataStorage.h" #include "utils/AABB.h" +#include "utils/linearAlg2D.h" +#include "utils/math.h" + namespace cura { -double bridgeAngle( - const Settings& settings, +struct TransformedSegment +{ + Point2LL transformed_start; + Point2LL transformed_end; + coord_t min_y; + coord_t max_y; +}; + +struct TransformedShape +{ + std::vector segments; + coord_t min_y{ std::numeric_limits::max() }; + coord_t max_y{ std::numeric_limits::lowest() }; +}; + +/*! + * Calculates all the intersections between a horizontal line and the given transformed shape + * @param line_y The horizontal line Y coordinate + * @param transformed_shape The shape to intersect with + * @return The list of X coordinates of the intersections, unsorted + */ +std::vector shapeLineIntersections(const coord_t line_y, const TransformedShape& transformed_shape) +{ + std::vector intersections; + + for (const TransformedSegment& transformed_segment : transformed_shape.segments) + { + if (transformed_segment.min_y > line_y || transformed_segment.max_y < line_y) + { + // Segment is fully over or under the line, skip + continue; + } + + const std::optional intersection = LinearAlg2D::lineHorizontalLineIntersection(transformed_segment.transformed_start, transformed_segment.transformed_end, line_y); + if (intersection.has_value()) + { + intersections.push_back(intersection.value()); + } + } + + return intersections; +} + +/*! + * Evaluates a potential bridging line to see if it can actually bridge between two supported regions + * @param line_y The Y coordinate of the horizontal line + * @param transformed_skin_area The skin outline, transformed so that the bridging line is horizontal + * @param transformed_supported_area The supported regions, transformed so that the bridging line is horizontal + * @return The score of the line regarding bridging, which can be positive if it is mostly bridging, or negative if it is mostly hanging + * + * The score is based on the following criteria: + * - Properly bridging segments, i.e. between two supported areas, add their length to the score + * - Hanging segments, i.e. supported on one side but not the other (or not at all), subtract their length from the score + * - Segments that lie on a supported area substract part of their length from the score */ +coord_t evaluateBridgeLine(const coord_t line_y, const TransformedShape& transformed_skin_area, const TransformedShape& transformed_supported_area) +{ + // Calculate intersections with skin outline to see which segments should actually be printed + std::vector skin_outline_intersections = shapeLineIntersections(line_y, transformed_skin_area); + if (skin_outline_intersections.size() < 2) + { + // We need to enter the skin at some point to bridge inside + return 0; + } + ranges::stable_sort(skin_outline_intersections); + + // Calculate intersections with supported regions to see which segments are anchored + std::vector supported_regions_intersections = shapeLineIntersections(line_y, transformed_supported_area); + ranges::stable_sort(supported_regions_intersections); + + enum class BridgeStatus + { + Outside, // Segment is outside the skin + Hanging, // Segment has started to extrude over air + Anchored, // Segment has been anchored to a supported area + Supported, // Segment is being extruded over a supported area + }; + + // Loop through intersections with skin and supported regions to see which parts of the line are hanging/bridging/supported + bool inside_skin_area = false; + bool inside_supported_area = false; + coord_t last_position; + coord_t segment_score = 0; + BridgeStatus bridge_status = BridgeStatus::Outside; + while (! skin_outline_intersections.empty() || ! supported_regions_intersections.empty()) + { + // See what is the next intersection: skin, supported or both + bool next_intersection_is_skin_area = false; + bool next_intersection_is_supported_area = false; + if (skin_outline_intersections.empty()) + { + next_intersection_is_supported_area = true; + } + else if (supported_regions_intersections.empty()) + { + next_intersection_is_skin_area = true; + } + else + { + const double next_intersection_skin_area = skin_outline_intersections.front(); + const double next_intersection_supported_area = supported_regions_intersections.front(); + + if (is_null(next_intersection_skin_area - next_intersection_supported_area)) + { + next_intersection_is_skin_area = true; + next_intersection_is_supported_area = true; + } + else if (next_intersection_skin_area <= next_intersection_supported_area) + { + next_intersection_is_skin_area = true; + if (inside_skin_area && inside_supported_area) + { + // When leaving skin, assume also leaving supported. This should always happen naturally, but may not due to rounding errors. + next_intersection_is_supported_area = true; + } + } + else + { + next_intersection_is_supported_area = true; + if (! inside_supported_area && ! inside_skin_area) + { + // When reaching supported, assume also reaching skin. This should always happen naturally, but may not due to rounding errors. + next_intersection_is_skin_area = true; + } + } + } + + // Get new insideness states + bool next_inside_skin_area = inside_skin_area; + bool next_inside_supported_area = inside_supported_area; + coord_t next_intersection; + if (next_intersection_is_skin_area) + { + next_intersection = skin_outline_intersections.front(); + skin_outline_intersections.erase(skin_outline_intersections.begin()); + next_inside_skin_area = ! next_inside_skin_area; + } + if (next_intersection_is_supported_area) + { + next_intersection = supported_regions_intersections.front(); + supported_regions_intersections.erase(supported_regions_intersections.begin()); + next_inside_supported_area = ! next_inside_supported_area; + } + + const bool leaving_skin = next_intersection_is_skin_area && ! next_inside_skin_area; + const bool reaching_supported = next_intersection_is_supported_area && next_inside_supported_area; + double add_segment_score_weight = 0.0; + + switch (bridge_status) + { + case BridgeStatus::Outside: + bridge_status = reaching_supported ? BridgeStatus::Supported : BridgeStatus::Hanging; + break; + + case BridgeStatus::Supported: + bridge_status = leaving_skin ? BridgeStatus::Outside : BridgeStatus::Anchored; + // Negatively account for fully supported lines to avoid lonely line parts over the supported areas + add_segment_score_weight = -0.1; + break; + + case BridgeStatus::Hanging: + add_segment_score_weight = -1.0; + bridge_status = reaching_supported ? BridgeStatus::Supported : BridgeStatus::Outside; + break; + + case BridgeStatus::Anchored: + if (reaching_supported) + { + add_segment_score_weight = 1.0; + bridge_status = BridgeStatus::Supported; + } + else if (leaving_skin) + { + add_segment_score_weight = -1.0; + bridge_status = BridgeStatus::Outside; + } + break; + } + + if (add_segment_score_weight != 0.0) + { + const coord_t segment_length = next_intersection - last_position; + segment_score += std::llrint(segment_length * add_segment_score_weight); + } + + last_position = next_intersection; + inside_skin_area = next_inside_skin_area; + inside_supported_area = next_inside_supported_area; + } + + return segment_score; +} + +/*! + * Pre-transforms a shape according to the given matrix, so that the bridging angle ends up being horizontal + * @param shape The shape to be transformed + * @param matrix The transform matrix + * @return The shape transformed to the bridging orientation + */ +TransformedShape transformShape(const Shape& shape, const PointMatrix& matrix) +{ + TransformedShape transformed_shape; + transformed_shape.segments.reserve(shape.pointCount()); + + for (const Polygon& polygon : shape) + { + for (auto iterator = polygon.beginSegments(); iterator != polygon.endSegments(); ++iterator) + { + TransformedSegment transformed_segment; + transformed_segment.transformed_start = matrix.apply((*iterator).start); + transformed_segment.transformed_end = matrix.apply((*iterator).end); + transformed_segment.min_y = std::min(transformed_segment.transformed_start.Y, transformed_segment.transformed_end.Y); + transformed_segment.max_y = std::max(transformed_segment.transformed_start.Y, transformed_segment.transformed_end.Y); + transformed_shape.segments.push_back(transformed_segment); + transformed_shape.min_y = std::min(transformed_shape.min_y, transformed_segment.min_y); + transformed_shape.max_y = std::max(transformed_shape.max_y, transformed_segment.max_y); + } + } + + return transformed_shape; +} + +/*! + * Evaluate the bridging lines scoring for the given angle + * @param skin_outline The skin outline to be filled + * @param supported_regions The supported regions areas + * @param line_width The bridging line width + * @param angle The current angle to be tested + * @return The global bridging score for this angle */ +coord_t evaluateBridgeLines(const Shape& skin_outline, const Shape& supported_regions, const coord_t line_width, const AngleDegrees& angle) +{ + // Transform the skin outline and supported regions according to the angle to speedup intersections calculations + const PointMatrix matrix(angle); + const TransformedShape transformed_skin_area = transformShape(skin_outline, matrix); + const TransformedShape transformed_supported_area = transformShape(supported_regions, matrix); + + if (transformed_skin_area.min_y >= transformed_skin_area.max_y || transformed_supported_area.min_y >= transformed_supported_area.max_y) + { + return std::numeric_limits::lowest(); + } + + const size_t bridge_lines_count = (transformed_skin_area.max_y - transformed_skin_area.min_y) / line_width; + if (bridge_lines_count == 0) + { + // We cannot fit a single line in this direction, give up + return std::numeric_limits::lowest(); + } + + const coord_t line_min = transformed_skin_area.min_y + line_width * 0.5; + + // Evaluated lines that could be properly bridging + coord_t line_score = 0; + const TransformedShape empty_transformed_shape; + for (size_t i = 0; i < bridge_lines_count; ++i) + { + const coord_t line_y = line_min + i * line_width; + const bool has_supports = line_y >= transformed_supported_area.min_y && line_y <= transformed_supported_area.max_y; + line_score += evaluateBridgeLine(line_y, transformed_skin_area, has_supports ? transformed_supported_area : empty_transformed_shape); + } + + return line_score; +} + +/*! + * Gets the proper angle when bridging over infill + * @param mesh The mesh being processed, which contains the actual infill angles + * @param layer_nr The number of the layer being processed + * @return The angle to be applied to bridging over infill on this layer + */ +AngleDegrees bridgeOverInfillAngle(const SliceMeshStorage& mesh, const unsigned layer_nr) +{ + if (layer_nr == 0) + { + // Obviously there is no infill below + return 0; + } + + const std::vector& infill_angles = mesh.infill_angles; + assert(! infill_angles.empty()); + + const AngleDegrees infill_angle_below = infill_angles[(layer_nr - 1) % infill_angles.size()]; + const auto infill_pattern = mesh.settings.get("infill_pattern"); + + AngleDegrees bridge_angle; + + switch (infill_pattern) + { + case EFillMethod::CROSS: + case EFillMethod::CROSS_3D: + bridge_angle = 22.5; + break; + case EFillMethod::GYROID: + case EFillMethod::CONCENTRIC: + case EFillMethod::LIGHTNING: + case EFillMethod::PLUGIN: + case EFillMethod::NONE: + bridge_angle = 45; + break; + case EFillMethod::CUBICSUBDIV: + bridge_angle = infill_angle_below + 45; + break; + case EFillMethod::TRIANGLES: + case EFillMethod::LINES: + case EFillMethod::TRIHEXAGON: + case EFillMethod::CUBIC: + case EFillMethod::ZIG_ZAG: + bridge_angle = infill_angle_below + 90; + break; + case EFillMethod::QUARTER_CUBIC: + case EFillMethod::TETRAHEDRAL: + case EFillMethod::GRID: + bridge_angle = infill_angle_below; + break; + } + + return bridge_angle; +} + +std::optional bridgeAngle( + const SliceMeshStorage& mesh, const Shape& skin_outline, const SliceDataStorage& storage, const unsigned layer_nr, @@ -22,14 +345,23 @@ double bridgeAngle( const SupportLayer* support_layer, Shape& supported_regions) { - assert(! skin_outline.empty()); + const Settings& settings = mesh.settings; + const bool bridge_settings_enabled = settings.get("bridge_settings_enabled"); + if (! bridge_settings_enabled) + { + return std::nullopt; + } + AABB boundary_box(skin_outline); + const coord_t line_width = settings.get("skin_line_width"); + // To detect if we have a bridge, first calculate the intersection of the current layer with the previous layer. // This gives us the islands that the layer rests on. Shape islands; Shape prev_layer_outline; // we also want the complete outline of the previous layer + Shape prev_layer_infill; const Ratio sparse_infill_max_density = settings.get("bridge_sparse_infill_max_density"); @@ -46,10 +378,13 @@ double bridgeAngle( for (const SliceLayerPart& prev_layer_part : mesh.layers[layer_nr - bridge_layer].parts) { + Shape prev_layer_part_infill = prev_layer_part.getOwnInfillArea(); + prev_layer_infill = prev_layer_infill.unionPolygons(prev_layer_part_infill); + Shape solid_below(prev_layer_part.outline); if (bridge_layer == 1 && part_has_sparse_infill) { - solid_below = solid_below.difference(prev_layer_part.getOwnInfillArea()); + solid_below = solid_below.difference(prev_layer_part_infill); } prev_layer_outline.push_back(solid_below); // not intersected with skin @@ -103,105 +438,39 @@ double bridgeAngle( } } - const bool bridge_settings_enabled = settings.get("bridge_settings_enabled"); - const Ratio support_threshold = bridge_settings_enabled ? settings.get("bridge_skin_support_threshold") : 0.0_r; - - // if the proportion of the skin region that is supported is less than supportThreshold, it's considered a bridge and we - // determine the best angle for the skin lines - the current heuristic is that the skin lines should be parallel to the - // direction of the skin area's longest unsupported edge - if the skin has no unsupported edges, we fall through to the - // original code - - if (support_threshold > 0 && (supported_regions.area() / (skin_outline.area() + 1)) < support_threshold) - { - Shape bb_poly; - bb_poly.push_back(boundary_box.toPolygon()); - - // airBelow is the region below the skin that is not supported, it extends well past the boundary of the skin. - // It needs to be shrunk slightly so that the vertices of the skin polygon that would otherwise fall exactly on - // the air boundary do appear to be supported - - const coord_t bb_max_dim = std::max(boundary_box.max_.X - boundary_box.min_.X, boundary_box.max_.Y - boundary_box.min_.Y); - const Shape air_below(bb_poly.offset(bb_max_dim).difference(prev_layer_outline).offset(-10)); - - OpenLinesSet skin_perimeter_lines; - for (const Polygon& poly : skin_outline) - { - if (! poly.empty()) - { - skin_perimeter_lines.emplace_back(poly.toPseudoOpenPolyline()); - } - } - - OpenLinesSet skin_perimeter_lines_over_air(air_below.intersection(skin_perimeter_lines)); - - if (skin_perimeter_lines_over_air.size()) - { - // one or more edges of the skin region are unsupported, determine the longest - coord_t max_dist2 = 0; - double line_angle = -1; - for (const OpenPolyline& air_line : skin_perimeter_lines_over_air) - { - for (auto iterator = air_line.beginSegments(); iterator != air_line.endSegments(); ++iterator) - { - const Point2LL vector = (*iterator).start - (*iterator).end; - coord_t dist2 = vSize2(vector); - if (dist2 > max_dist2) - { - max_dist2 = dist2; - line_angle = angle(vector); - } - } - } - return line_angle; - } - } - else + const Ratio support_threshold = settings.get("bridge_skin_support_threshold"); + if (support_threshold == 0 || (supported_regions.area() / (skin_outline.area() + 1)) >= support_threshold) { // as the proportion of the skin region that is supported is >= supportThreshold, it's not // considered to be a bridge and the original bridge detection code below is skipped - return -1.0; + return std::nullopt; } - if (islands.size() > 5 || islands.size() < 1) + prev_layer_infill = skin_outline.intersection(prev_layer_infill); + const Ratio infill_ratio = prev_layer_infill.area() / (skin_outline.area() + 1); + if (infill_ratio > 0.5) // In practice, the ratio should always be close to 0 or 1, so 0.5 should be good enough { - return -1.0; + // We are doing bridging over infill, so use the infill angle instead of trying to calculate a proper angle + return bridgeOverInfillAngle(mesh, layer_nr); } - // Next find the 2 largest islands that we rest on. - double area1 = 0; - double area2 = 0; - std::optional idx1; - std::optional idx2; - for (size_t n = 0; n < islands.size(); n++) + struct FitAngle { - // Skip internal holes - if (! islands[n].orientation()) - continue; - double area = std::abs(islands[n].area()); - if (area > area1) - { - if (area1 > area2) - { - area2 = area1; - idx2 = idx1; - } - area1 = area; - idx1 = n; - } - else if (area > area2) + coord_t score; + std::optional angle; + }; + + FitAngle best_angle{ std::numeric_limits::lowest(), std::nullopt }; + for (AngleDegrees angle = 0; angle < 180; angle += 1) + { + const coord_t score = evaluateBridgeLines(skin_outline, supported_regions, line_width, angle); + if (score > best_angle.score) { - area2 = area; - idx2 = n; + best_angle = { score, angle + 90 }; } } - if (! idx1.has_value() || ! idx2.has_value()) - return -1.0; - - Point2LL center1 = islands[idx1.value()].centerOfMass(); - Point2LL center2 = islands[idx2.value()].centerOfMass(); - - return angle(center2 - center1); + return best_angle.angle; } } // namespace cura diff --git a/src/geometry/PointMatrix.cpp b/src/geometry/PointMatrix.cpp new file mode 100644 index 0000000000..257b6a0c72 --- /dev/null +++ b/src/geometry/PointMatrix.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2025 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "geometry/PointMatrix.h" + +#include + +#include "settings/types/Angle.h" + + +namespace cura +{ + +PointMatrix::PointMatrix(double rotation) +{ + rotation = rotation / 180 * std::numbers::pi; + matrix.at(0) = std::cos(rotation); + matrix.at(1) = -std::sin(rotation); + matrix.at(2) = -matrix.at(1); + matrix.at(3) = matrix.at(0); +} + +PointMatrix::PointMatrix(const AngleDegrees& rotation) + : PointMatrix(rotation.value_) +{ +} + +PointMatrix::PointMatrix(const AngleRadians& rotation) + : PointMatrix(AngleDegrees(rotation)) +{ +} + +PointMatrix::PointMatrix(const Point2LL& p) +{ + matrix.at(0) = static_cast(p.X); + matrix.at(1) = static_cast(p.Y); + double f = std::sqrt((matrix.at(0) * matrix.at(0)) + (matrix.at(1) * matrix.at(1))); + matrix.at(0) /= f; + matrix.at(1) /= f; + matrix.at(2) = -matrix.at(1); + matrix.at(3) = matrix.at(0); +} + +PointMatrix PointMatrix::scale(double s) +{ + PointMatrix ret; + ret.matrix.at(0) = s; + ret.matrix.at(3) = s; + return ret; +} + +Point2LL PointMatrix::apply(const Point2LL& p) const +{ + const auto x = static_cast(p.X); + const auto y = static_cast(p.Y); + return { std::llrint(x * matrix.at(0) + y * matrix.at(1)), std::llrint(x * matrix.at(2) + y * matrix.at(3)) }; +} + +Point2LL PointMatrix::unapply(const Point2LL& p) const +{ + const auto x = static_cast(p.X); + const auto y = static_cast(p.Y); + return { std::llrint(x * matrix.at(0) + y * matrix.at(2)), std::llrint(x * matrix.at(1) + y * matrix.at(3)) }; +} + +PointMatrix PointMatrix::inverse() const +{ + PointMatrix ret; + double det = matrix.at(0) * matrix.at(3) - matrix.at(1) * matrix.at(2); + ret.matrix.at(0) = matrix.at(3) / det; + ret.matrix.at(1) = -matrix.at(1) / det; + ret.matrix.at(2) = -matrix.at(2) / det; + ret.matrix.at(3) = matrix.at(0) / det; + return ret; +} + +} // namespace cura diff --git a/src/infill/LightningLayer.cpp b/src/infill/LightningLayer.cpp index 9ecb401c41..49917a5340 100644 --- a/src/infill/LightningLayer.cpp +++ b/src/infill/LightningLayer.cpp @@ -9,7 +9,6 @@ #include "infill/LightningDistanceField.h" #include "infill/LightningTreeNode.h" #include "sliceDataStorage.h" -#include "utils/SVG.h" #include "utils/SparsePointGridInclusive.h" #include "utils/linearAlg2D.h" diff --git a/src/infill/SierpinskiFill.cpp b/src/infill/SierpinskiFill.cpp index 1c7593a2f6..6889497d37 100644 --- a/src/infill/SierpinskiFill.cpp +++ b/src/infill/SierpinskiFill.cpp @@ -620,15 +620,15 @@ double SierpinskiFill::getSubdivisionError(std::list::itera void SierpinskiFill::debugOutput(SVG& svg) { - svg.writePolygon(aabb_.toPolygon(), SVG::Color::RED); + svg.write(aabb_.toPolygon(), { .surface = { SVG::Color::RED } }); // draw triangles for (SierpinskiTriangle* node : sequence_) { SierpinskiTriangle& triangle = *node; - svg.writeLine(triangle.a_, triangle.b_, SVG::Color::GRAY); - svg.writeLine(triangle.a_, triangle.straight_corner_, SVG::Color::GRAY); - svg.writeLine(triangle.b_, triangle.straight_corner_, SVG::Color::GRAY); + svg.write(triangle.a_, triangle.b_, { .line = { SVG::Color::GRAY } }); + svg.write(triangle.a_, triangle.straight_corner_, { .line = { SVG::Color::GRAY } }); + svg.write(triangle.b_, triangle.straight_corner_, { .line = { SVG::Color::GRAY } }); } } diff --git a/src/pathPlanning/Comb.cpp b/src/pathPlanning/Comb.cpp index 03cc8d8134..8caeafdeea 100644 --- a/src/pathPlanning/Comb.cpp +++ b/src/pathPlanning/Comb.cpp @@ -14,7 +14,6 @@ #include "pathPlanning/LinePolygonsCrossings.h" #include "sliceDataStorage.h" #include "utils/PolygonsPointIndex.h" -#include "utils/SVG.h" #include "utils/linearAlg2D.h" namespace cura diff --git a/src/pathPlanning/LinePolygonsCrossings.cpp b/src/pathPlanning/LinePolygonsCrossings.cpp index 70a00d94c6..cc84c16102 100644 --- a/src/pathPlanning/LinePolygonsCrossings.cpp +++ b/src/pathPlanning/LinePolygonsCrossings.cpp @@ -6,7 +6,6 @@ #include #include "sliceDataStorage.h" -#include "utils/SVG.h" namespace cura { diff --git a/src/utils/ListPolyIt.cpp b/src/utils/ListPolyIt.cpp index 244bba0177..e2b3b3f6e5 100644 --- a/src/utils/ListPolyIt.cpp +++ b/src/utils/ListPolyIt.cpp @@ -8,8 +8,8 @@ #include // ostream #include "geometry/Polygon.h" -#include "utils/AABB.h" // for debug output svg html -#include "utils/SVG.h" +#include "geometry/Shape.h" + namespace cura { diff --git a/src/utils/OBJ.cpp b/src/utils/OBJ.cpp index 3333a09e16..339eb345ec 100644 --- a/src/utils/OBJ.cpp +++ b/src/utils/OBJ.cpp @@ -94,8 +94,8 @@ OBJ::~OBJ() { out_material << "newmtl " << materialName(color) << "\n"; - SVG::ColorObject color_rgb = SVG::ColorObject::toRgb(color); - out_material << "Kd " << color_rgb.r_ << color_rgb.g_ << color_rgb.b_ << "\n" + SVG::RgbColor color_rgb = SVG::toRgb(color); + out_material << "Kd " << color_rgb.r << color_rgb.g << color_rgb.b << "\n" << "\n"; } } diff --git a/src/utils/SVG.cpp b/src/utils/SVG.cpp index 129515e82b..605ca7be4e 100644 --- a/src/utils/SVG.cpp +++ b/src/utils/SVG.cpp @@ -5,19 +5,25 @@ #include +#include +#include +#include +#include +#include +#include #include #include "geometry/OpenPolyline.h" +#include "geometry/Point2D.h" #include "geometry/Polygon.h" #include "geometry/SingleShape.h" #include "utils/ExtrusionLine.h" -#include "utils/Point3D.h" namespace cura { -std::string SVG::toString(Color color) +std::string SVG::toString(const Color color) { switch (color) { @@ -42,52 +48,91 @@ std::string SVG::toString(Color color) case SVG::Color::YELLOW: return "yellow"; case SVG::Color::NONE: + case SVG::Color::RAINBOW: // rainbow case should never be reached return "none"; default: return "black"; } } -SVG::ColorObject SVG::ColorObject::toRgb(const Color color) +SVG::RgbColor SVG::toRgb(const Color color) { switch (color) { case SVG::Color::WHITE: - return ColorObject(255, 255, 255); + return RgbColor(255, 255, 255); case SVG::Color::GRAY: - return ColorObject(128, 128, 128); + return RgbColor(128, 128, 128); case SVG::Color::RED: - return ColorObject(255, 0, 0); + return RgbColor(255, 0, 0); case SVG::Color::BLUE: - return ColorObject(0, 0, 255); + return RgbColor(0, 0, 255); case SVG::Color::GREEN: - return ColorObject(0, 255, 0); + return RgbColor(0, 255, 0); case SVG::Color::LIME: - return ColorObject(191, 255, 0); + return RgbColor(191, 255, 0); case SVG::Color::ORANGE: - return ColorObject(255, 165, 0); + return RgbColor(255, 165, 0); case SVG::Color::MAGENTA: - return ColorObject(255, 0, 255); + return RgbColor(255, 0, 255); case SVG::Color::YELLOW: - return ColorObject(255, 255, 0); + return RgbColor(255, 255, 0); case SVG::Color::BLACK: + case SVG::Color::RAINBOW: // rainbow case should never be reached case SVG::Color::NONE: - return ColorObject(0, 0, 0); + return RgbColor(0, 0, 0); } - return ColorObject(0, 0, 0); + return RgbColor{}; } -std::string SVG::toString(const ColorObject& color) const +std::string SVG::toString(const ColorObject& color) { - if (color.is_enum_) - return toString(color.color_); - else + if (std::holds_alternative(color)) { + return toString(std::get(color)); + } + + if (std::holds_alternative(color)) + { + const RgbColor& rgb_color = std::get(color); std::ostringstream ss; - ss << "rgb(" << color.r_ << "," << color.g_ << "," << color.b_ << ")"; + ss << "rgb(" << rgb_color.r << "," << rgb_color.g << "," << rgb_color.b << ")"; return ss.str(); } + + return "none"; +} + +std::string SVG::toString(const std::vector& dash_array) +{ + if (dash_array.size() >= 2) + { + return dash_array + | ranges::views::transform( + [](const int dash_array_part) + { + return std::to_string(dash_array_part); + }) + | ranges::views::join(ranges::views::c_str(",")) | ranges::to(); + } + + return "none"; +} + +std::string SVG::toString(const FillRule fill_rule) +{ + switch (fill_rule) + { + case FillRule::NonZero: + return "nonzero"; + case FillRule::EvenOdd: + return "evenodd"; + case FillRule::None: + break; + } + + return "none"; } void SVG::handleFlush(const bool flush) const @@ -99,7 +144,7 @@ void SVG::handleFlush(const bool flush) const } -SVG::SVG(std::string filename, AABB aabb, Point2LL canvas_size, ColorObject background) +SVG::SVG(const std::string& filename, const AABB& aabb, const Point2LL& canvas_size, const ColorObject& background) : SVG(filename, aabb, std::min( @@ -110,17 +155,16 @@ SVG::SVG(std::string filename, AABB aabb, Point2LL canvas_size, ColorObject back { } -SVG::SVG(std::string filename, AABB aabb, double scale, ColorObject background) +SVG::SVG(const std::string& filename, const AABB& aabb, double scale, const ColorObject& background) : SVG(filename, aabb, scale, (aabb.max_ - aabb.min_) * scale, background) { } -SVG::SVG(std::string filename, AABB aabb, double scale, Point2LL canvas_size, ColorObject background) +SVG::SVG(const std::string& filename, const AABB& aabb, double scale, const Point2LL& canvas_size, const ColorObject& background) : aabb_(aabb) , aabb_size_(aabb.max_ - aabb.min_) , canvas_size_(canvas_size) , scale_(scale) - , background_(background) { output_is_html_ = strcmp(filename.c_str() + strlen(filename.c_str()) - 4, "html") == 0; out_ = fopen(filename.c_str(), "w"); @@ -147,9 +191,10 @@ SVG::SVG(std::string filename, AABB aabb, double scale, Point2LL canvas_size, Co fprintf(out_, " inkscape:label=\"layer%zu\"\n", layer_nr_); fprintf(out_, " id=\"layer%zu\">\n", layer_nr_); - if (! background_.is_enum_ || background_.color_ != Color::NONE) + std::string background_str = toString(background); + if (! background_str.empty()) { - fprintf(out_, "\n", toString(background_).c_str()); + fprintf(out_, "\n", background_str.c_str()); } } @@ -181,12 +226,12 @@ void SVG::nextLayer() Point2LL SVG::transform(const Point2LL& p) const { - return Point2LL(std::llrint(static_cast(p.X - aabb_.min_.X) * scale_), std::llrint(static_cast(p.Y - aabb_.min_.Y) * scale_)); + return Point2LL(std::llrint(static_cast(p.X - aabb_.min_.X) * scale_), std::llrint(static_cast(aabb_.max_.Y - p.Y) * scale_)); } -Point3D SVG::transformF(const Point2LL& p) const +Point2D SVG::transformF(const Point2LL& p) const { - return Point3D(static_cast(p.X - aabb_.min_.X) * scale_, static_cast(p.Y - aabb_.min_.Y) * scale_, 0.0); + return Point2D(static_cast(p.X - aabb_.min_.X) * scale_, static_cast(aabb_.max_.Y - p.Y) * scale_); } void SVG::writeComment(const std::string& comment) const @@ -194,305 +239,170 @@ void SVG::writeComment(const std::string& comment) const fprintf(out_, "\n", comment.c_str()); } -void SVG::writeAreas(const Shape& polygons, const ColorObject color, const ColorObject outline_color, const double stroke_width) const +void SVG::write(const Shape& shape, const VisualAttributes& visual_attributes, const bool flush) const { - std::vector parts = polygons.splitIntoParts(); - for (auto part_it = parts.rbegin(); part_it != parts.rend(); ++part_it) - { - SingleShape& part = *part_it; - for (size_t j = 0; j < part.size(); j++) - { - fprintf(out_, "(fp.x_), static_cast(fp.y_)); - } - if (j == 0) - fprintf(out_, "\" style=\"fill:%s;stroke:%s;stroke-width:%f\" />\n", toString(color).c_str(), toString(outline_color).c_str(), static_cast(stroke_width)); - else - fprintf(out_, "\" style=\"fill:white;stroke:%s;stroke-width:%f\" />\n", toString(outline_color).c_str(), static_cast(stroke_width)); - } - } + write(static_cast>(shape), visual_attributes, flush, FillRule::EvenOdd); } -void SVG::writeAreas(const Polygon& polygon, const ColorObject color, const ColorObject outline_color, const double stroke_width) const +template +void SVG::write(const LinesSet& lines, const VisualAttributes& visual_attributes, const bool flush, const FillRule fill_rule) const { - fprintf( - out_, - "(stroke_width)); // The beginning of the polygon tag. - for (const Point2LL& point : polygon) // Add every point to the list of points. + if (visual_attributes.line.isDisplayed() || visual_attributes.surface.isDisplayed()) { - Point3D transformed = transformF(point); - fprintf(out_, "%f,%f ", static_cast(transformed.x_), static_cast(transformed.y_)); - } - fprintf(out_, "\" />\n"); // The end of the polygon tag. -} + if (std::holds_alternative(visual_attributes.line.color) && std::get(visual_attributes.line.color) == Color::RAINBOW) + { + // Rainbow can't be displayed with a path, we have to draw the segments separately + write(lines, { .surface = visual_attributes.surface, .vertices = visual_attributes.vertices }, false); -void SVG::writePoint(const Point2LL& p, const bool write_coords, const double size, const ColorObject color) const -{ - Point3D pf = transformF(p); - fprintf( - out_, - "\n", - static_cast(pf.x_), - static_cast(pf.y_), - static_cast(size), - toString(color).c_str()); + VisualAttributes rainbow_line_attributes{ .line = visual_attributes.line }; - if (write_coords) - { - fprintf(out_, "%lli,%lli\n", static_cast(pf.x_), static_cast(pf.y_), p.X, p.Y); - } -} - -void SVG::writePoints(const Polygon& poly, const bool write_coords, const double size, const ColorObject color) const -{ - for (const Point2LL& p : poly) - { - writePoint(p, write_coords, size, color); - } -} - -void SVG::writePoints(const Shape& polygons, const bool write_coords, const double size, const ColorObject color) const -{ - for (const Polygon& poly : polygons) - { - writePoints(poly, write_coords, size, color); - } -} + for (const LineType& line : lines) + { + size_t index = 0; + for (auto iterator = line.beginSegments(); iterator != line.endSegments(); ++iterator) + { + RgbColor color; + color.r = index * 255 / line.segmentsCount(); + color.g = (index * 255 * 11 / line.segmentsCount()) % (255 * 2); + if (color.g > 255) + { + color.g = 255 * 2 - color.g; + } + color.b = (index * 255 * 5 / line.segmentsCount()) % (255 * 2); + if (color.b > 255) + { + color.b = 255 * 2 - color.b; + } + + rainbow_line_attributes.line.color = color; + write((*iterator).start, (*iterator).end, rainbow_line_attributes, false); + index++; + } + } + } + else + { + bool first_path = true; + for (const LineType& line : lines) + { + if (first_path) + { + fprintf( + out_, + "& polyline, const ColorObject color) const -{ - if (polyline.size() <= 1) // Need at least 2 points. - { - return; - } + fprintf(out_, "\" />\n"); // Write the end of the tag. + } - Point3D transformed = transformF(polyline[0]); // Element 0 must exist due to the check above. - fprintf( - out_, - "(transformed.x_), - static_cast(transformed.y_)); // Write the start of the path tag and the first endpoint. - for (size_t point = 1; point < polyline.size(); point++) - { - transformed = transformF(polyline[point]); - fprintf(out_, "L%f,%f", static_cast(transformed.x_), static_cast(transformed.y_)); // Write a line segment to the next point. + handleFlush(flush); } - fprintf(out_, "\" />\n"); // Write the end of the tag. -} -void SVG::writeLine(const Polyline& line, const ColorObject color, const double stroke_width) const -{ - for (auto iterator = line.beginSegments(); iterator != line.endSegments(); ++iterator) + for (const LineType& line : lines) { - writeLine((*iterator).start, (*iterator).end, color, stroke_width, false); + write(static_cast(line), visual_attributes.vertices, false); } - handleFlush(true); -} - -void SVG::writeLine(const Point2LL& a, const Point2LL& b, const ColorObject color, const double stroke_width, const bool flush) const -{ - Point3D fa = transformF(a); - Point3D fb = transformF(b); - fprintf( - out_, - "\n", - static_cast(fa.x_), - static_cast(fa.y_), - static_cast(fb.x_), - static_cast(fb.y_), - toString(color).c_str(), - static_cast(stroke_width)); - handleFlush(flush); } -void SVG::writeArrow(const Point2LL& a, const Point2LL& b, const ColorObject color, const double stroke_width, const double head_size) const -{ - Point3D fa = transformF(a); - Point3D fb = transformF(b); - Point3D ab = fb - fa; - Point3D normal = Point3D(ab.y_, -ab.x_, 0.0).normalized(); - Point3D direction = ab.normalized(); - - Point3D tip = fb + normal * head_size - direction * head_size; - Point3D b_base = fb + normal * stroke_width - direction * stroke_width * 2.41f; - Point3D a_base = fa + normal * stroke_width; - fprintf( - out_, - "", - toString(color).c_str(), - static_cast(fa.x_), - static_cast(fa.y_), - static_cast(fb.x_), - static_cast(fb.y_), - static_cast(tip.x_), - static_cast(tip.y_), - static_cast(b_base.x_), - static_cast(b_base.y_), - static_cast(a_base.x_), - static_cast(a_base.y_)); -} +template void SVG::write(const LinesSet& lines, const VisualAttributes& visual_attributes, const bool flush, const FillRule fill_rule) const; +template void SVG::write(const LinesSet& lines, const VisualAttributes& visual_attributes, const bool flush, const FillRule fill_rule) const; +template void SVG::write(const LinesSet& lines, const VisualAttributes& visual_attributes, const bool flush, const FillRule fill_rule) const; -void SVG::writeLineRGB(const Point2LL& from, const Point2LL& to, const int r, const int g, const int b, const double stroke_width) const +void SVG::write(const OpenPolyline& line, const VisualAttributes& visual_attributes, const bool flush) const { - Point3D fa = transformF(from); - Point3D fb = transformF(to); - fprintf( - out_, - "\n", - static_cast(fa.x_), - static_cast(fa.y_), - static_cast(fb.x_), - static_cast(fb.y_), - r, - g, - b, - static_cast(stroke_width)); + write(LinesSet{ line }, visual_attributes, flush); } -void SVG::writeDashedLine(const Point2LL& a, const Point2LL& b, ColorObject color) const +void SVG::write(const ClosedPolyline& line, const VisualAttributes& visual_attributes, const bool flush) const { - Point3D fa = transformF(a); - Point3D fb = transformF(b); - fprintf( - out_, - "\n", - static_cast(fa.x_), - static_cast(fa.y_), - static_cast(fb.x_), - static_cast(fb.y_), - toString(color).c_str()); + write(LinesSet{ line }, visual_attributes, flush); } -void SVG::writeText(const Point2LL& p, const std::string& txt, const ColorObject color, const double font_size) const +void SVG::write(const PointsSet& points, const VerticesAttributes& visual_attributes, const bool flush) const { - Point3D pf = transformF(p); - fprintf( - out_, - "%s\n", - static_cast(pf.x_), - static_cast(pf.y_), - static_cast(font_size), - toString(color).c_str(), - txt.c_str()); -} - -void SVG::writePolygons(const Shape& polys, const ColorObject color, const double stroke_width, const bool flush) const -{ - for (const Polygon& poly : polys) + for (const Point2LL& point : points) { - writePolygon(poly, color, stroke_width, false); + write(point, visual_attributes, false); } handleFlush(flush); } -void SVG::writePolygon(const Polygon& poly, const ColorObject color, const double stroke_width, const bool flush) const +void SVG::write(const Point2LL& point, const VerticesAttributes& visual_attributes, const bool flush) const { - if (poly.size() == 0) - { - return; - } - int size = static_cast(poly.size()); - Point2LL p0 = poly.back(); - int i = 0; - for (const Point2LL& p1 : poly) + if (visual_attributes.isDisplayed()) { - if (color.color_ == Color::RAINBOW) - { - int g = (i * 255 * 11 / size) % (255 * 2); - if (g > 255) - { - g = 255 * 2 - g; - } - int b = (i * 255 * 5 / size) % (255 * 2); - if (b > 255) - { - b = 255 * 2 - b; - } - writeLineRGB(p0, p1, i * 255 / size, g, b, stroke_width); - } - else + Point2D transformed_point = transformF(point); + fprintf( + out_, + "\n", + static_cast(transformed_point.x()), + static_cast(transformed_point.y()), + static_cast(visual_attributes.radius), + toString(visual_attributes.color).c_str()); + + if (visual_attributes.write_coords) { - writeLine(p0, p1, color, stroke_width, false); + write(fmt::format("{},{}", point.X, point.Y), point, visual_attributes, false); } - p0 = p1; - i++; } handleFlush(flush); } - -void SVG::writePolylines(const Shape& polys, const ColorObject color, const double stroke_width, const bool flush) const +void SVG::write(const Point2LL& start, const Point2LL& end, const VisualAttributes& visual_attributes, const bool flush) const { - for (const Polygon& poly : polys) - { - writePolyline(poly, color, stroke_width, false); - } - - handleFlush(flush); + write(OpenPolyline({ start, end }), visual_attributes, flush); } -template -void SVG::writePolylines(const LinesSet& lines, const ColorObject color, const double stroke_width, const bool flush) const +void SVG::writeArrow(const Point2LL& a, const Point2LL& b, const ColorObject color, const double stroke_width, const double head_size) const { - for (const LineType& line : lines) - { - writePolyline(line, color, stroke_width, false); - } - - handleFlush(flush); -} + const Point2D fa = transformF(a); + const Point2D fb = transformF(b); + const Point2D ab = fb - fa; + const Point2D normal = Point2D(ab.y(), -ab.x()).vNormalized().value(); + const Point2D direction = ab.vNormalized().value(); -template void SVG::writePolylines(const LinesSet& lines, const ColorObject color, const double stroke_width, const bool flush) const; -template void SVG::writePolylines(const LinesSet& lines, const ColorObject color, const double stroke_width, const bool flush) const; - -void SVG::writePolyline(const Polygon& poly, const ColorObject color, const double stroke_width) const -{ - if (poly.size() == 0) - { - return; - } - const int size = static_cast(poly.size()); - Point2LL p0 = poly[0]; - int i = 0; - for (size_t p_idx = 1; p_idx < poly.size(); p_idx++) - { - Point2LL p1 = poly[p_idx]; - if (color.color_ == Color::RAINBOW) - { - int g = (i * 255 * 11 / size) % (255 * 2); - if (g > 255) - g = 255 * 2 - g; - int b = (i * 255 * 5 / size) % (255 * 2); - if (b > 255) - b = 255 * 2 - b; - writeLineRGB(p0, p1, i * 255 / size, g, b, stroke_width); - } - else - { - writeLine(p0, p1, color, stroke_width); - } - p0 = p1; - i++; - } + const Point2D tip = fb + normal * head_size - direction * head_size; + const Point2D b_base = fb + normal * stroke_width - direction * stroke_width * 2.41f; + const Point2D a_base = fa + normal * stroke_width; + fprintf( + out_, + "", + toString(color).c_str(), + static_cast(fa.x()), + static_cast(fa.y()), + static_cast(fb.x()), + static_cast(fb.y()), + static_cast(tip.x()), + static_cast(tip.y()), + static_cast(b_base.x()), + static_cast(b_base.y()), + static_cast(a_base.x()), + static_cast(a_base.y())); } -void SVG::writePolyline(const Polyline& poly, const ColorObject color, const double stroke_width, const bool flush) const +void SVG::write(const std::string& text, const Point2LL& p, const VerticesAttributes& vertices_attributes, const bool flush) const { - for (auto iterator = poly.beginSegments(); iterator != poly.endSegments(); ++iterator) - { - writeLine((*iterator).start, (*iterator).end, color, stroke_width, false); - } + const Point2D transformed_point = transformF(p); + fprintf( + out_, + "%s\n", + transformed_point.x(), + transformed_point.y(), + vertices_attributes.font_size, + toString(vertices_attributes.color).c_str(), + text.c_str()); handleFlush(flush); } @@ -529,25 +439,25 @@ void SVG::writeLine(const ExtrusionLine& line, const ColorObject color, const do const Point2LL direction_vector = end_vertex.p_ - start_vertex.p_; const Point2LL direction_left = turn90CCW(direction_vector); const Point2LL direction_right = -direction_left; // Opposite of left. - const Point3D start_left + const Point2D start_left = transformF(start_vertex.p_ + normal(direction_left, std::llrint(std::max(minimum_line_width, static_cast(start_vertex.w_) * width_factor)))); - const Point3D start_right + const Point2D start_right = transformF(start_vertex.p_ + normal(direction_right, std::llrint(std::max(minimum_line_width, static_cast(start_vertex.w_) * width_factor)))); - const Point3D end_left = transformF(end_vertex.p_ + normal(direction_left, std::llrint(std::max(minimum_line_width, static_cast(end_vertex.w_) * width_factor)))); - const Point3D end_right = transformF(end_vertex.p_ + normal(direction_right, std::llrint(std::max(minimum_line_width, static_cast(end_vertex.w_) * width_factor)))); + const Point2D end_left = transformF(end_vertex.p_ + normal(direction_left, std::llrint(std::max(minimum_line_width, static_cast(end_vertex.w_) * width_factor)))); + const Point2D end_right = transformF(end_vertex.p_ + normal(direction_right, std::llrint(std::max(minimum_line_width, static_cast(end_vertex.w_) * width_factor)))); fprintf( out_, "\n", toString(color).c_str(), - static_cast(start_left.x_), - static_cast(start_left.y_), - static_cast(start_right.x_), - static_cast(start_right.y_), - static_cast(end_right.x_), - static_cast(end_right.y_), - static_cast(end_left.x_), - static_cast(end_left.y_)); + static_cast(start_left.x()), + static_cast(start_left.y()), + static_cast(start_right.x()), + static_cast(start_right.y()), + static_cast(end_right.x()), + static_cast(end_right.y()), + static_cast(end_left.x()), + static_cast(end_left.y())); start_vertex = end_vertex; // For the next line segment. } @@ -558,7 +468,7 @@ void SVG::writeLine(const ExtrusionLine& line, const ColorObject color, const do } } -void SVG::writeCoordinateGrid(const coord_t grid_size, const Color color, const double stroke_width, const double font_size) const +void SVG::writeCoordinateGrid(const coord_t grid_size, const VisualAttributes& visual_attributes, const bool flush) const { constexpr double dist_from_edge = 0.05; // As fraction of image width or height. const coord_t min_x = aabb_.min_.X - (aabb_.min_.X % grid_size); @@ -566,17 +476,44 @@ void SVG::writeCoordinateGrid(const coord_t grid_size, const Color color, const for (coord_t x = min_x; x < aabb_.max_.X; x += grid_size) { - writeLine(Point2LL(x, aabb_.min_.Y), Point2LL(x, aabb_.max_.Y), color, stroke_width); + write(Point2LL(x, aabb_.min_.Y), Point2LL(x, aabb_.max_.Y), visual_attributes); std::stringstream ss; ss << INT2MM(x); - writeText(Point2LL(x, std::llrint(static_cast(aabb_.min_.Y) + static_cast(aabb_.max_.Y - aabb_.min_.Y) * dist_from_edge)), ss.str(), color, font_size); + write( + ss.str(), + Point2LL(x, std::llrint(static_cast(aabb_.min_.Y) + static_cast(aabb_.max_.Y - aabb_.min_.Y) * dist_from_edge)), + visual_attributes.vertices, + false); } for (coord_t y = min_y; y < aabb_.max_.Y; y += grid_size) { - writeLine(Point2LL(aabb_.min_.X, y), Point2LL(aabb_.max_.Y, y), color, stroke_width); + write(Point2LL(aabb_.min_.X, y), Point2LL(aabb_.max_.Y, y), visual_attributes); std::stringstream ss; ss << INT2MM(y); - writeText(Point2LL(std::llrint(static_cast(aabb_.min_.X) + static_cast(aabb_.max_.X - aabb_.min_.X) * dist_from_edge), y), ss.str(), color, font_size); + write( + ss.str(), + Point2LL(std::llrint(static_cast(aabb_.min_.X) + static_cast(aabb_.max_.X - aabb_.min_.X) * dist_from_edge), y), + visual_attributes.vertices, + false); + } + + handleFlush(flush); +} + +void SVG::writePathPoints(const Polyline& line) const +{ + bool first_segment = true; + for (auto iterator = line.beginSegments(); iterator != line.endSegments(); ++iterator) + { + if (first_segment) + { + const Point2D transformed_start = transformF((*iterator).start); + fprintf(out_, "M%f %f ", transformed_start.x(), transformed_start.y()); + first_segment = false; + } + + const Point2D transformed_end = transformF((*iterator).end); + fprintf(out_, "L%f,%f ", static_cast(transformed_end.x()), static_cast(transformed_end.y())); } } diff --git a/src/utils/ToolpathVisualizer.cpp b/src/utils/ToolpathVisualizer.cpp index 84675f992f..54e6a4c340 100644 --- a/src/utils/ToolpathVisualizer.cpp +++ b/src/utils/ToolpathVisualizer.cpp @@ -12,7 +12,7 @@ namespace cura void ToolpathVisualizer::outline(const Shape& input) { - svg_.writeAreas(input, SVG::Color::GRAY, SVG::Color::NONE, 2); + svg_.write(input, { .surface = { SVG::Color::GRAY } }); svg_.nextLayer(); } @@ -30,11 +30,10 @@ void ToolpathVisualizer::toolpaths(const std::vector& all_segm polys.push_back(covered); } int c = 255 - 200 * (w - .25); - SVG::ColorObject clr(c, c, c); + SVG::RgbColor clr(c, c, c); polys = polys.execute(ClipperLib::pftNonZero); polys = PolygonUtils::connect(polys); - for (const Polygon& connected : polys) - svg_.writeAreas(connected, clr, SVG::Color::NONE); + svg_.write(polys, { .surface = { clr } }); if (! rounded_visualization) break; } @@ -44,14 +43,14 @@ void ToolpathVisualizer::toolpaths(const std::vector& all_segm void ToolpathVisualizer::underfill(const Shape& underfills) { - svg_.writeAreas(underfills, SVG::ColorObject(0, 128, 255), SVG::Color::NONE); + svg_.write(underfills, { .surface = { SVG::RgbColor(0, 128, 255) } }); svg_.nextLayer(); } void ToolpathVisualizer::overfill(const Shape& overfills, const Shape& double_overfills) { - svg_.writeAreas(overfills, SVG::ColorObject(255, 128, 0), SVG::Color::NONE); + svg_.write(overfills, { .surface = { SVG::RgbColor(255, 128, 0) } }); svg_.nextLayer(); - svg_.writeAreas(double_overfills, SVG::ColorObject(255, 100, 0), SVG::Color::NONE); + svg_.write(double_overfills, { .surface = { SVG::RgbColor(255, 100, 0) } }); if (! double_overfills.empty()) { svg_.nextLayer(); @@ -73,17 +72,17 @@ void ToolpathVisualizer::width_legend(const Shape& input, coord_t nozzle_size, c legend_btm.p_ += (legend_mid.p_ - legend_btm.p_) / 4; legend_top.p_ += (legend_mid.p_ - legend_top.p_) / 4; ExtrusionSegment legend_segment(legend_btm, legend_top, true, false); - svg_.writeAreas(legend_segment.toShape(false), SVG::ColorObject(200, 200, 200), SVG::Color::NONE); // real outline + svg_.write(legend_segment.toShape(false), { .surface = { SVG::RgbColor(200, 200, 200) } }); // real outline std::vector all_segments_plus; all_segments_plus.emplace_back(legend_segment); // colored Point2LL legend_text_offset(nozzle_size, 0); - svg_.writeText(legend_top.p_ + legend_text_offset, to_string(INT2MM(legend_top.w_))); - svg_.writeText(legend_btm.p_ + legend_text_offset, to_string(INT2MM(legend_btm.w_))); - svg_.writeText(legend_mid.p_ + legend_text_offset, to_string(INT2MM(legend_mid.w_))); - svg_.writeLine(legend_top.p_, legend_top.p_ + legend_text_offset); - svg_.writeLine(legend_btm.p_, legend_btm.p_ + legend_text_offset); - svg_.writeLine(legend_mid.p_, legend_mid.p_ + legend_text_offset); + svg_.write(to_string(INT2MM(legend_top.w_)), legend_top.p_ + legend_text_offset, { true }); + svg_.write(to_string(INT2MM(legend_btm.w_)), legend_btm.p_ + legend_text_offset, { true }); + svg_.write(to_string(INT2MM(legend_mid.w_)), legend_mid.p_ + legend_text_offset, { true }); + svg_.write(legend_top.p_, legend_top.p_ + legend_text_offset, { .line = {} }); + svg_.write(legend_btm.p_, legend_btm.p_ + legend_text_offset, { .line = {} }); + svg_.write(legend_mid.p_, legend_mid.p_ + legend_text_offset, { .line = {} }); widths(all_segments_plus, nozzle_size, max_dev, min_w, rounded_visualization); } @@ -146,7 +145,7 @@ void ToolpathVisualizer::widths( s.from_.w_ *= w / .9; s.to_.w_ *= w / .9; Shape covered = s.toShape(); - svg_.writeAreas(covered, SVG::ColorObject(clr.x_, clr.y_, clr.z_), SVG::Color::NONE); + svg_.write(covered, { .surface = { SVG::RgbColor(clr.x_, clr.y_, clr.z_) } }); } } if (! rounded_visualization) diff --git a/src/utils/linearAlg2D.cpp b/src/utils/linearAlg2D.cpp index 3fccba9e00..71c883de74 100644 --- a/src/utils/linearAlg2D.cpp +++ b/src/utils/linearAlg2D.cpp @@ -319,4 +319,23 @@ bool LinearAlg2D::lineLineIntersection(const Point2LL& a, const Point2LL& b, con return true; } +std::optional LinearAlg2D::lineHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y) +{ + if (p1.X == p2.X) + { + // Line is purely vertical + return p1.X; + } + + const double coeff_a = static_cast(p2.Y - p1.Y) / (p2.X - p1.X); + if (is_null(coeff_a)) + { + // Other line is also horizontal + return std::nullopt; + } + + const double coeff_b = p1.Y - coeff_a * p1.X; + return std::llrint((line_y - coeff_b) / coeff_a); +} + } // namespace cura