diff --git a/include/InsetOrderOptimizer.h b/include/InsetOrderOptimizer.h index afba5dc452..80c0856f3d 100644 --- a/include/InsetOrderOptimizer.h +++ b/include/InsetOrderOptimizer.h @@ -59,7 +59,8 @@ class InsetOrderOptimizer const Point2LL& model_center_point, const Shape& disallowed_areas_for_seams = {}, const bool scarf_seam = false, - const bool smooth_speed = false); + const bool smooth_speed = false, + const Shape& overhang_areas = Shape()); /*! * Adds the insets to the given layer plan. @@ -114,6 +115,7 @@ class InsetOrderOptimizer Shape disallowed_areas_for_seams_; const bool scarf_seam_; const bool smooth_speed_; + Shape overhang_areas_; std::vector> inset_polys_; // vector of vectors holding the inset polygons Shape retraction_region_; // After printing an outer wall, move into this region so that retractions do not leave visible blobs. Calculated lazily if needed (see @@ -128,7 +130,7 @@ class InsetOrderOptimizer * * \param closed_line The polygon to insert the seam point in. (It's assumed to be closed at least.) * - * \return The index of the inserted seam point, or std::nullopt if no seam point was inserted. + * \return The index of the inserted seam point, or the index of the closest point if an existing one can be used. */ std::optional insertSeamPoint(ExtrusionLine& closed_line); diff --git a/include/LayerPlan.h b/include/LayerPlan.h index 75eac440ea..0be204faf2 100644 --- a/include/LayerPlan.h +++ b/include/LayerPlan.h @@ -298,6 +298,11 @@ class LayerPlan : public NoCopy */ void setSeamOverhangMask(const Shape& polys); + /*! + * Get the seam overhang mask, which contains the areas where we don't want to place the seam because they are overhanding + */ + const Shape& getSeamOverhangMask() const; + /*! * Set roofing_mask. * @@ -677,50 +682,6 @@ class LayerPlan : public NoCopy const bool is_top_layer, const bool is_bottom_layer); - - /*! - * Given a wall polygon and a start vertex index, return the index of the first vertex that is supported (is not above air) - * - * Uses bridge_wall_mask and overhang_mask to determine where there is air below - * - * \param wall The wall polygon - * \param start_idx The index of the starting vertex of \p wall - * \return The index of the first supported vertex - if no vertices are supported, start_idx is returned - */ - template - size_t locateFirstSupportedVertex(const T& wall, const size_t start_idx) const - { - if (bridge_wall_mask_.empty() && seam_overhang_mask_.empty()) - { - return start_idx; - } - - const auto air_below = bridge_wall_mask_.unionPolygons(seam_overhang_mask_); - - size_t curr_idx = start_idx; - - while (true) - { - const Point2LL& vertex = cura::make_point(wall[curr_idx]); - if (! air_below.inside(vertex, true)) - { - // vertex isn't above air so it's OK to use - return curr_idx; - } - - if (++curr_idx >= wall.size()) - { - curr_idx = 0; - } - - if (curr_idx == start_idx) - { - // no vertices are supported so just return the original index - return start_idx; - } - } - } - /*! * Write the planned paths to gcode * diff --git a/include/PathOrderOptimizer.h b/include/PathOrderOptimizer.h index be8e61873b..d7ad78eea8 100644 --- a/include/PathOrderOptimizer.h +++ b/include/PathOrderOptimizer.h @@ -21,7 +21,9 @@ #include "path_ordering.h" #include "settings/EnumSettings.h" //To get the seam settings. #include "settings/ZSeamConfig.h" //To read the seam configuration. +#include "utils/Score.h" #include "utils/linearAlg2D.h" //To find the angle of corners to hide seams. +#include "utils/math.h" #include "utils/polygonUtils.h" #include "utils/views/dfs.h" @@ -113,7 +115,9 @@ class PathOrderOptimizer const bool reverse_direction = false, const std::unordered_multimap& order_requirements = no_order_requirements_, const bool group_outer_walls = false, - const Shape& disallowed_areas_for_seams = {}) + const Shape& disallowed_areas_for_seams = {}, + const bool use_shortest_for_inner_walls = false, + const Shape& overhang_areas = Shape()) : start_point_(start_point) , seam_config_(seam_config) , combing_boundary_((combing_boundary != nullptr && ! combing_boundary->empty()) ? combing_boundary : nullptr) @@ -122,7 +126,8 @@ class PathOrderOptimizer , _group_outer_walls(group_outer_walls) , order_requirements_(&order_requirements) , disallowed_area_for_seams{ disallowed_areas_for_seams } - + , use_shortest_for_inner_walls_(use_shortest_for_inner_walls) + , overhang_areas_(overhang_areas) { } @@ -130,11 +135,12 @@ class PathOrderOptimizer * Add a new polygon to be optimized. * \param polygon The polygon to optimize. */ - void addPolygon(const Path& polygon, std::optional force_start_index = std::nullopt) + void addPolygon(const Path& polygon, std::optional force_start_index = std::nullopt, const bool is_outer_wall = false) { constexpr bool is_closed = true; paths_.emplace_back(polygon, is_closed); paths_.back().force_start_index_ = force_start_index; + paths_.back().is_outer_wall = is_outer_wall; } /*! @@ -180,6 +186,20 @@ class PathOrderOptimizer } } + // Set actual used start point calculation strategy for each path + for (auto& path : paths_) + { + if (use_shortest_for_inner_walls_ && ! path.is_outer_wall) + { + path.seam_config_ = ZSeamConfig(EZSeamType::SHORTEST); + path.force_start_index_ = std::nullopt; + } + else + { + path.seam_config_ = seam_config_; + } + } + // Add all vertices to a bucket grid so that we can find nearby endpoints quickly. const coord_t snap_radius = 10_mu; // 0.01mm grid cells. Chaining only needs to consider polylines which are next to each other. SparsePointGridInclusive line_bucket_grid(snap_radius); @@ -205,16 +225,19 @@ class PathOrderOptimizer // For some Z seam types the start position can be pre-computed. // This is faster since we don't need to re-compute the start position at each step then. - precompute_start &= seam_config_.type_ == EZSeamType::RANDOM || seam_config_.type_ == EZSeamType::USER_SPECIFIED || seam_config_.type_ == EZSeamType::SHARPEST_CORNER; if (precompute_start) { for (auto& path : paths_) { - if (! path.is_closed_ || path.converted_->empty()) + if (path.seam_config_.type_ == EZSeamType::RANDOM || path.seam_config_.type_ == EZSeamType::USER_SPECIFIED + || path.seam_config_.type_ == EZSeamType::SHARPEST_CORNER) { - continue; // Can't pre-compute the seam for open polylines since they're at the endpoint nearest to the current position. + if (! path.is_closed_ || path.converted_->empty()) + { + continue; // Can't pre-compute the seam for open polylines since they're at the endpoint nearest to the current position. + } + path.start_vertex_ = findStartLocation(path, path.seam_config_.pos_); } - path.start_vertex_ = findStartLocation(path, seam_config_.pos_); } } @@ -298,6 +321,17 @@ class PathOrderOptimizer */ const std::unordered_multimap* order_requirements_; + /*! + * If true, we will compute the seam position of inner walls using a "shortest" seam configs, for inner walls that + * are directly following an outer wall. + */ + const bool use_shortest_for_inner_walls_; + + /*! + * Contains the overhang areas, where we would prefer not to place the start locations of walls + */ + const Shape overhang_areas_; + std::vector getOptimizedOrder(SparsePointGridInclusive line_bucket_grid, size_t snap_radius) { std::vector optimized_order; // To store our result in. @@ -583,8 +617,8 @@ class PathOrderOptimizer continue; } - const bool precompute_start - = seam_config_.type_ == EZSeamType::RANDOM || seam_config_.type_ == EZSeamType::USER_SPECIFIED || seam_config_.type_ == EZSeamType::SHARPEST_CORNER; + const bool precompute_start = path->seam_config_.type_ == EZSeamType::RANDOM || path->seam_config_.type_ == EZSeamType::USER_SPECIFIED + || path->seam_config_.type_ == EZSeamType::SHARPEST_CORNER; if (! path->is_closed_ || ! precompute_start) // Find the start location unless we've already precomputed it. { path->start_vertex_ = findStartLocation(*path, start_position); @@ -690,117 +724,171 @@ class PathOrderOptimizer // Rest of the function only deals with (closed) polygons. We need to be able to find the seam location of those polygons. - if (seam_config_.type_ == EZSeamType::RANDOM) - { - size_t vert = getRandomPointInPolygon(*path.converted_); - return vert; - } + // ########## Step 1: define the weighs of each criterion + // Standard weight for the "main" selection criterion, depending on the selected strategy. There should be + // exactly one calculation using this criterion. + constexpr double weight_main_criterion = 1.0; + + // If seam is not "shortest", we still compute the shortest distance score but with a very low weight + const double weight_distance = path.seam_config_.type_ == EZSeamType::SHORTEST ? weight_main_criterion : 0.02; + + // Avoiding overhangs is more important than the rest + constexpr double weight_exclude_overhang = 2.0; + + // In order to avoid jumping seams, we give a small score to the vertex X and Y position, so that if we have + // e.g. multiple corners with the same angle, we will always choose the ones at the top-right + constexpr double weight_consistency_x = 0.05; + constexpr double weight_consistency_y = weight_consistency_x / 2.0; // Less weight on Y to avoid symmetry effects - if (path.force_start_index_.has_value()) + // ########## Step 2: define which criteria should be taken into account in the total score + const bool calculate_forced_pos_score = path.force_start_index_.has_value(); + + bool calculate_distance_score = false; + bool calculate_corner_score = false; + bool calculate_random_score = false; + if (! calculate_forced_pos_score) { - // Start index already known, since we forced it, return. - return path.force_start_index_.value(); + // For most seam types, the shortest distance matters. Not for SHARPEST_CORNER though. + calculate_distance_score = path.seam_config_.type_ != EZSeamType::SHARPEST_CORNER || path.seam_config_.corner_pref_ == EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE; + + calculate_corner_score + = path.seam_config_.type_ == EZSeamType::SHARPEST_CORNER + && (path.seam_config_.corner_pref_ != EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE && path.seam_config_.corner_pref_ != EZSeamCornerPrefType::PLUGIN); + + calculate_random_score = path.seam_config_.type_ == EZSeamType::RANDOM; } - // Precompute segments lengths because we are going to need them multiple times - std::vector segments_sizes(path.converted_->size()); + const bool calculate_consistency_score = calculate_distance_score || calculate_corner_score; + + // Whatever the strategy, always avoid overhang + const bool calculate_overhang_score = ! overhang_areas_.empty(); + + // ########## Step 3: calculate some values that we are going to need multiple times, depending on which scoring is active + const AABB path_bounding_box = calculate_consistency_score ? AABB(*path.converted_) : AABB(); + + const Point2LL forced_start_pos = path.force_start_index_.has_value() ? path.converted_->at(path.force_start_index_.value()) : Point2LL(); + + std::vector segments_sizes; coord_t total_length = 0; - for (size_t i = 0; i < path.converted_->size(); ++i) + if (calculate_corner_score) { - const Point2LL& here = path.converted_->at(i); - const Point2LL& next = path.converted_->at((i + 1) % path.converted_->size()); - const coord_t segment_size = vSize(next - here); - segments_sizes[i] = segment_size; - total_length += segment_size; + segments_sizes.resize(path.converted_->size()); + for (size_t i = 0; i < path.converted_->size(); ++i) + { + const Point2LL& here = path.converted_->at(i); + const Point2LL& next = path.converted_->at((i + 1) % path.converted_->size()); + const coord_t segment_size = vSize(next - here); + segments_sizes[i] = segment_size; + total_length += segment_size; + } } - size_t best_i; - double best_score = std::numeric_limits::infinity(); - for (const auto& [i, here] : *path.converted_ | ranges::views::drop_last(1) | ranges::views::enumerate) + auto scoreFromDistance = [](const Point2LL& here, const Point2LL& remote_pos) -> double { - // For most seam types, the shortest distance matters. Not for SHARPEST_CORNER though. - // For SHARPEST_CORNER, use a fixed starting score of 0. - const double score_distance = (seam_config_.type_ == EZSeamType::SHARPEST_CORNER && seam_config_.corner_pref_ != EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE) - ? MM2INT(10) - : vSize2(here - target_pos); + // Fixed divider for shortest distances computation. The divider should be set so that the minimum encountered + // distance gives a score very close to 1.0, and a medium-far distance gives a score close to 0.5 + constexpr double distance_divider = 20.0; - double corner_angle = cornerAngle(path, i, segments_sizes, total_length); - // angles < 0 are concave (left turning) - // angles > 0 are convex (right turning) + // Use actual (non-squared) distance to ensure a proper scoring distribution + const double distance = vSizeMM(here - remote_pos); - double corner_shift; + // Use reciprocal function to normalize distance score decreasingly + return 1.0 / (1.0 + (distance / distance_divider)); + }; - if (seam_config_.type_ == EZSeamType::SHORTEST) + // ########## Step 4: now calculate the total score of each vertex and select the best one + std::optional best_i; + Score best_score; + for (const auto& [i, here] : *path.converted_ | ranges::views::drop_last(1) | ranges::views::enumerate) + { + Score vertex_score; + + if (calculate_forced_pos_score) { - // the more a corner satisfies our criteria, the closer it appears to be - // shift 10mm for a very acute corner - corner_shift = MM2INT(10) * MM2INT(10); + vertex_score += CriterionScore{ .score = scoreFromDistance(here, forced_start_pos), .weight = weight_main_criterion }; } - else + + if (calculate_distance_score) { - // the larger the distance from prev_point to p1, the more a corner will "attract" the seam - // so the user has some control over where the seam will lie. - - // the divisor here may need adjusting to obtain the best results (TBD) - corner_shift = score_distance / 50; - } - - double score = score_distance; - switch (seam_config_.corner_pref_) - { - default: - case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_INNER: - // Give advantage to concave corners. More advantage for sharper corners. - score += corner_angle * corner_shift; - break; - case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_OUTER: - // Give advantage to convex corners. More advantage for sharper corners. - score -= corner_angle * corner_shift; - break; - case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_ANY: - score -= std::abs(corner_angle) * corner_shift; // Still give sharper corners more advantage. - break; - case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE: - break; - case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_WEIGHTED: // Give sharper corners some advantage, but sharper concave corners even more. - { - double score_corner = std::abs(corner_angle) * corner_shift; - if (corner_angle < 0) // Concave corner. + vertex_score += CriterionScore{ .score = scoreFromDistance(here, target_pos), .weight = weight_distance }; + } + + if (calculate_corner_score) + { + double corner_angle = cornerAngle(path, i, segments_sizes, total_length); + // angles < 0 are concave (left turning) + // angles > 0 are convex (right turning) + + CriterionScore score_corner{ .weight = weight_main_criterion }; + + switch (path.seam_config_.corner_pref_) { - score_corner *= 2; + case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_INNER: + // Give advantage to concave corners. More advantage for sharper corners. + score_corner.score = cura::inverse_lerp(1.0, -1.0, corner_angle); + break; + case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_OUTER: + // Give advantage to convex corners. More advantage for sharper corners. + score_corner.score = cura::inverse_lerp(-1.0, 1.0, corner_angle); + break; + case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_ANY: + // Still give sharper corners more advantage. + score_corner.score = std::abs(corner_angle); + break; + case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_WEIGHTED: + // Give sharper corners some advantage, but sharper concave corners even more. + if (corner_angle < 0) + { + score_corner.score = -corner_angle; + } + else + { + score_corner.score = corner_angle / 2.0; + } + break; + case EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE: + case EZSeamCornerPrefType::PLUGIN: + break; } - score -= score_corner; - break; + + vertex_score += score_corner; } + + if (calculate_random_score) + { + vertex_score += CriterionScore{ .score = cura::randf(), .weight = weight_main_criterion }; } - constexpr double EPSILON = 5.0; - if (std::abs(best_score - score) <= EPSILON) + if (calculate_consistency_score) { - // add breaker for two candidate starting location with similar score - // if we don't do this then we (can) get an un-even seam - // ties are broken by favouring points with lower x-coord - // if x-coord for both points are equal then break ties by - // favouring points with lower y-coord - const Point2LL& best_point = path.converted_->at(best_i); - if (std::abs(here.Y - best_point.Y) <= EPSILON ? best_point.X < here.X : best_point.Y < here.Y) - { - best_score = std::min(best_score, score); - best_i = i; - } + CriterionScore score_consistency_x{ .weight = weight_consistency_x }; + score_consistency_x.score = cura::inverse_lerp(path_bounding_box.min_.X, path_bounding_box.max_.X, here.X); + vertex_score += score_consistency_x; + + CriterionScore score_consistency_y{ .weight = weight_consistency_y }; + score_consistency_y.score = cura::inverse_lerp(path_bounding_box.min_.Y, path_bounding_box.max_.Y, here.Y); + vertex_score += score_consistency_y; } - else if (score < best_score) + + if (calculate_overhang_score) + { + CriterionScore score_exclude_overhang{ .weight = weight_exclude_overhang }; + score_exclude_overhang.score = overhang_areas_.inside(here, true) ? 0.0 : 1.0; + vertex_score += score_exclude_overhang; + } + + if (! best_i.has_value() || vertex_score > best_score) { best_i = i; - best_score = score; + best_score = vertex_score; } } if (! disallowed_area_for_seams.empty()) { - best_i = pathIfZseamIsInDisallowedArea(best_i, path, 0); + best_i = pathIfZseamIsInDisallowedArea(best_i.value_or(0), path, 0); } - return best_i; + return best_i.value_or(0); } /*! @@ -939,16 +1027,6 @@ class PathOrderOptimizer return sum * sum; // Squared distance, for fair comparison with direct distance. } - /*! - * Get a random vertex of a polygon. - * \param polygon A polygon to get a random vertex of. - * \return A random index in that polygon. - */ - size_t getRandomPointInPolygon(const PointsSet& polygon) const - { - return rand() % polygon.size(); - } - bool isLoopingPolyline(const OrderablePath& path) { if (path.converted_->empty()) diff --git a/include/path_ordering.h b/include/path_ordering.h index 0a7fb1af13..9366ba44c8 100644 --- a/include/path_ordering.h +++ b/include/path_ordering.h @@ -82,6 +82,16 @@ struct PathOrdering */ std::optional force_start_index_; + /*! + * The start point calculation strategy to be used for this path + */ + ZSeamConfig seam_config_; + + /*! + * Indicates whether this path is an outer (or inner) wall + */ + bool is_outer_wall{ false }; + /*! * Get vertex data from the custom path type. * diff --git a/include/utils/AABB.h b/include/utils/AABB.h index e3a93c678b..6be6ba9b2c 100644 --- a/include/utils/AABB.h +++ b/include/utils/AABB.h @@ -9,6 +9,7 @@ namespace cura { +class PointsSet; class Polygon; class Shape; @@ -21,10 +22,10 @@ class AABB AABB(); //!< initializes with invalid min and max AABB(const Point2LL& min, const Point2LL& max); //!< initializes with given min and max AABB(const Shape& shape); //!< Computes the boundary box for the given shape - AABB(const Polygon& poly); //!< Computes the boundary box for the given polygons + AABB(const PointsSet& poly); //!< Computes the boundary box for the given polygons void calculate(const Shape& shape); //!< Calculates the aabb for the given shape (throws away old min and max data of this aabb) - void calculate(const Polygon& poly); //!< Calculates the aabb for the given polygon (throws away old min and max data of this aabb) + void calculate(const PointsSet& poly); //!< Calculates the aabb for the given polygon (throws away old min and max data of this aabb) /*! * Whether the bounding box contains the specified point. @@ -80,7 +81,7 @@ class AABB */ void include(const Point2LL& point); - void include(const Polygon& polygon); + void include(const PointsSet& polygon); /*! * \brief Includes the specified bounding box in the bounding box. diff --git a/include/utils/CriterionScore.h b/include/utils/CriterionScore.h new file mode 100644 index 0000000000..e4313d988e --- /dev/null +++ b/include/utils/CriterionScore.h @@ -0,0 +1,31 @@ +// Copyright (c) 2024 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_CRITERION_SCORE_H +#define UTILS_CRITERION_SCORE_H + +namespace cura +{ + +/*! + * This structure represents a score given by a single crtierion when calculating a global score to select a best + * candidate among a list with multiple criteria. + */ +struct CriterionScore +{ + /*! + * The score given by the criterion. To ensure a proper selection, this value must be contained in [0.0, 1.0] and + * the different given scores must be evenly distributed in this range. + */ + double score{ 0.0 }; + + /*! + * The weight to be given when taking this score into the global score. A score that contributes "normally" to the + * global score should have a weight of 1.0, and others should be adjusted around this value, to give them more or + * less influence. + */ + double weight{ 0.0 }; +}; + +} // namespace cura +#endif // UTILS_CRITERION_SCORE_H diff --git a/include/utils/SVG.h b/include/utils/SVG.h index 96bcfa0a45..7105468f10 100644 --- a/include/utils/SVG.h +++ b/include/utils/SVG.h @@ -62,6 +62,7 @@ class SVG : NoCopy private: std::string toString(const Color color) const; std::string toString(const ColorObject& color) const; + void handleFlush(const bool flush) const; FILE* out_; // the output file const AABB aabb_; // the boundary box to display @@ -122,7 +123,7 @@ class SVG : NoCopy */ void writeLines(const std::vector& polyline, const ColorObject color = Color::BLACK) const; - void writeLine(const Point2LL& a, const Point2LL& b, const ColorObject color = Color::BLACK, const double stroke_width = 1.0) 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 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; @@ -148,10 +149,12 @@ class SVG : NoCopy void writePolygon(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; + void writePolylines(const Shape& polys, 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. * @@ -183,7 +186,7 @@ class SVG : NoCopy * \param color The color to draw the line with. * \param width_factor A multiplicative factor on the line width. */ - void writeLine(const ExtrusionLine& line, const ColorObject color = Color::BLACK, const double width_factor = 1.0) const; + void writeLine(const ExtrusionLine& line, const ColorObject color = Color::BLACK, const double width_factor = 1.0, const bool flush = true) const; /*! * Draws a grid across the image and writes down coordinates. diff --git a/include/utils/Score.h b/include/utils/Score.h new file mode 100644 index 0000000000..f8108498ee --- /dev/null +++ b/include/utils/Score.h @@ -0,0 +1,61 @@ +// Copyright (c) 2024 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_SCORE_H +#define UTILS_SCORE_H + +#include + +#include "CriterionScore.h" + +namespace cura +{ + +/*! + * This class represents a score to be calculated over different criteria, to select the best candidate among a list. + */ +class Score +{ +private: + double value_{ 0.0 }; + +public: + /*! + * Get the actual score value, should be used for debug purposes only + */ + double getValue() const + { + return value_; + } + + /*! + * Add the calculated score of an inidividual criterion to the global score, taking care of its weight + */ + void operator+=(const CriterionScore& criterion_score) + { + value_ += criterion_score.score * criterion_score.weight; + } + + /*! + * Comparison operators to allow selecting the best global score + */ + auto operator<=>(const Score&) const = default; +}; + +} // namespace cura + +namespace fmt +{ + +template<> +struct formatter : formatter +{ + auto format(const cura::Score& score, format_context& ctx) + { + return fmt::format_to(ctx.out(), "Score{{{}}}", score.getValue()); + } +}; + +} // namespace fmt + +#endif // UTILS_SCORE_H diff --git a/include/utils/math.h b/include/utils/math.h index f1ad772c72..a0650f0e8f 100644 --- a/include/utils/math.h +++ b/include/utils/math.h @@ -108,5 +108,33 @@ template return (dividend + divisor - 1) / divisor; } +/*! + * \brief Calculates the "inverse linear interpolation" of a value over a range, i.e. given a range [min, max] the + * value "min" would give a result of 0.0 and the value "max" would give a result of 1.0, values in between will + * be interpolated linearly. + * \note The returned value may be out of the [0.0, 1.0] range if the given value is outside the [min, max] range, it is + * up to the caller to clamp the result if required + * \note The range_min value may be greater than the range_max, inverting the interpolation logic + */ +template +[[nodiscard]] inline double inverse_lerp(T range_min, T range_max, T value) +{ + if (range_min == range_max) + { + return 0.0; + } + + return static_cast(value - range_min) / (range_max - range_min); +} + +/*! + * \brief Get a random floating point number in the range [0.0, 1.0] + */ +template +[[nodiscard]] inline T randf() +{ + return static_cast(std::rand()) / static_cast(RAND_MAX); +} + } // namespace cura #endif // UTILS_MATH_H diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index c7a67dd923..fcba3b052b 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -2064,14 +2064,10 @@ void getBestAngledLinesToSupportPoints(OpenLinesSet& result_lines, const Shape& // We do this because supporting lines are hanging over air, // and therefore print best as part of a continuous print move, // rather than having a travel move before and after them. -// -// We also double-insert most lines, since that allows the -// elimination of all travel moves, and overlap isn't an issue -// because the lines are hanging. void integrateSupportingLine(OpenLinesSet& infill_lines, const OpenPolyline& line_to_add) { // Returns the line index and the index of the point within an infill_line, null for no match found. - const auto findMatchingSegment = [&](Point2LL p) -> std::optional> + const auto findMatchingSegment = [&](Point2LL p) -> std::optional> { for (size_t i = 0; i < infill_lines.size(); ++i) { @@ -2101,59 +2097,76 @@ void integrateSupportingLine(OpenLinesSet& infill_lines, const OpenPolyline& lin { /* both ends intersect with the same line. * If the inserted line has ends x, y - * and the original line was A--(x)--B---C--(y)--D - * Then the new line will be A--x--y--C---B--x--y--D - * Note that the middle part of the line is reversed. + * and the original line was ...--A--(x)--B--...--C--(y)--D--... + * Then the new lines will be ...--A--x--y--C--...--B--x + * And line y--D--... + * Note that some parts of the line are reversed, + * and the last one is completly split apart. */ OpenPolyline& old_line = infill_lines[front_line_index]; - OpenPolyline new_line; + OpenPolyline new_line_start; + OpenPolyline new_line_end; Point2LL x, y; - size_t x_index, y_index; + std::ptrdiff_t x_index, y_index; if (front_point_index < back_point_index) { x = line_to_add.front(); y = line_to_add.back(); - x_index = front_point_index; - y_index = back_point_index; + x_index = static_cast(front_point_index); + y_index = static_cast(back_point_index); } else { y = line_to_add.front(); x = line_to_add.back(); - y_index = front_point_index; - x_index = back_point_index; + y_index = static_cast(front_point_index); + x_index = static_cast(back_point_index); } - new_line.insert(new_line.end(), old_line.begin(), old_line.begin() + x_index); - new_line.push_back(x); - new_line.push_back(y); - new_line.insert(new_line.end(), old_line.rend() - y_index, old_line.rend() - x_index); - new_line.push_back(x); - new_line.push_back(y); - new_line.insert(new_line.end(), old_line.begin() + y_index, old_line.end()); - old_line.setPoints(std::move(new_line.getPoints())); + + new_line_start.insert(new_line_start.end(), old_line.begin(), old_line.begin() + x_index); + new_line_start.push_back(x); + new_line_start.push_back(y); + new_line_start.insert(new_line_start.end(), old_line.rend() - y_index, old_line.rend() - x_index); + new_line_start.push_back(x); + + new_line_end.push_back(y); + new_line_end.insert(new_line_end.end(), old_line.begin() + y_index, old_line.end()); + + old_line.setPoints(std::move(new_line_start.getPoints())); + infill_lines.push_back(new_line_end); } else { /* Different lines * If the line_to_add has ends [front, back] - * Existing line (intersects front): A B front C D E - * Other existing line (intersects back): M N back O P Q - * Result is Line: A B front back O P Q - * And line: M N back front C D E + * Existing line (intersects front): ...--A--(x)--B--... + * Other existing line (intersects back): ...--C--(y)--D--... + * Result is Line: ...--A--x--y--D--... + * And line: x--B--... + * And line: ...--C--y */ OpenPolyline& old_front = infill_lines[front_line_index]; OpenPolyline& old_back = infill_lines[back_line_index]; - OpenPolyline new_front, new_back; - new_front.insert(new_front.end(), old_front.begin(), old_front.begin() + front_point_index); - new_front.push_back(line_to_add.front()); - new_front.push_back(line_to_add.back()); - new_front.insert(new_front.end(), old_back.begin() + back_point_index, old_back.end()); - new_back.insert(new_back.end(), old_back.begin(), old_back.begin() + back_point_index); - new_back.push_back(line_to_add.back()); - new_back.push_back(line_to_add.front()); - new_back.insert(new_back.end(), old_front.begin() + front_point_index, old_front.end()); + OpenPolyline full_line, new_front, new_back; + const Point2LL x = line_to_add.front(); + const Point2LL y = line_to_add.back(); + const auto x_index = static_cast(front_point_index); + const auto y_index = static_cast(back_point_index); + + new_front.push_back(x); + new_front.insert(new_front.end(), old_front.begin() + x_index, old_front.end()); + + new_back.insert(new_back.end(), old_back.begin(), old_back.begin() + y_index); + new_back.push_back(y); + + full_line.insert(full_line.end(), old_front.begin(), old_front.begin() + x_index); + full_line.push_back(x); + full_line.push_back(y); + full_line.insert(full_line.end(), old_back.begin() + y_index, old_back.end()); + old_front.setPoints(std::move(new_front.getPoints())); old_back.setPoints(std::move(new_back.getPoints())); + infill_lines.push_back(full_line); } } else @@ -3148,7 +3161,8 @@ bool FffGcodeWriter::processInsets( mesh.bounding_box.flatten().getMiddle(), disallowed_areas_for_seams, scarf_seam, - smooth_speed); + smooth_speed, + gcode_layer.getSeamOverhangMask()); added_something |= wall_orderer.addToLayer(); } return added_something; diff --git a/src/InsetOrderOptimizer.cpp b/src/InsetOrderOptimizer.cpp index 56961e2bde..7c8723bb95 100644 --- a/src/InsetOrderOptimizer.cpp +++ b/src/InsetOrderOptimizer.cpp @@ -54,7 +54,8 @@ InsetOrderOptimizer::InsetOrderOptimizer( const Point2LL& model_center_point, const Shape& disallowed_areas_for_seams, const bool scarf_seam, - const bool smooth_speed) + const bool smooth_speed, + const Shape& overhang_areas) : gcode_writer_(gcode_writer) , storage_(storage) , gcode_layer_(gcode_layer) @@ -78,6 +79,7 @@ InsetOrderOptimizer::InsetOrderOptimizer( , disallowed_areas_for_seams_{ disallowed_areas_for_seams } , scarf_seam_(scarf_seam) , smooth_speed_(smooth_speed) + , overhang_areas_(overhang_areas) { } @@ -93,6 +95,7 @@ bool InsetOrderOptimizer::addToLayer() const bool current_extruder_is_wall_x = wall_x_extruder_nr_ == extruder_nr_; const bool reverse = shouldReversePath(use_one_extruder, current_extruder_is_wall_x, outer_to_inner); + const bool use_shortest_for_inner_walls = outer_to_inner; auto walls_to_be_added = getWallsToBeAdded(reverse, use_one_extruder); const auto order = pack_by_inset ? getInsetOrder(walls_to_be_added, outer_to_inner) : getRegionOrder(walls_to_be_added, outer_to_inner); @@ -114,7 +117,9 @@ bool InsetOrderOptimizer::addToLayer() reverse, order, group_outer_walls, - disallowed_areas_for_seams_); + disallowed_areas_for_seams_, + use_shortest_for_inner_walls, + overhang_areas_); for (auto& line : walls_to_be_added) { @@ -126,7 +131,7 @@ bool InsetOrderOptimizer::addToLayer() // If the user indicated that we may deviate from the vertices for the seam, we can insert a seam point, if needed. force_start = insertSeamPoint(line); } - order_optimizer.addPolygon(&line, force_start); + order_optimizer.addPolygon(&line, force_start, line.is_outer_wall()); } else { @@ -202,7 +207,7 @@ std::optional InsetOrderOptimizer::insertSeamPoint(ExtrusionLine& closed Point2LL closest_point; size_t closest_junction_idx = 0; coord_t closest_distance_sqd = std::numeric_limits::max(); - bool should_reclaculate_closest = false; + bool should_recalculate_closest = false; if (z_seam_config_.type_ == EZSeamType::USER_SPECIFIED) { // For user-defined seams you usually don't _actually_ want the _closest_ point, per-se, @@ -248,24 +253,27 @@ std::optional InsetOrderOptimizer::insertSeamPoint(ExtrusionLine& closed closest_junction_idx = i; } } - should_reclaculate_closest = true; + should_recalculate_closest = true; } const auto& start_pt = closed_line.junctions_[closest_junction_idx]; const auto& end_pt = closed_line.junctions_[(closest_junction_idx + 1) % closed_line.junctions_.size()]; - if (should_reclaculate_closest) + if (should_recalculate_closest) { // In the second case (see above) the closest point hasn't actually been calculated yet, // since in that case we'de need the start and end points. So do that here. closest_point = LinearAlg2D::getClosestOnLineSegment(request_point, start_pt.p_, end_pt.p_); } constexpr coord_t smallest_dist_sqd = 25; - if (vSize2(closest_point - start_pt.p_) <= smallest_dist_sqd || vSize2(closest_point - end_pt.p_) <= smallest_dist_sqd) + if (vSize2(closest_point - start_pt.p_) <= smallest_dist_sqd) { - // Early out if the closest point is too close to the start or end point. - // NOTE: Maybe return the index here anyway, since this is the point the current caller would want to force the seam to. - // However, then the index returned would have a caveat that it _can_ point to an already exisiting point then. - return std::nullopt; + // If the closest point is very close to the start point, just use it instead. + return closest_junction_idx; + } + if (vSize2(closest_point - end_pt.p_) <= smallest_dist_sqd) + { + // If the closest point is very close to the end point, just use it instead. + return (closest_junction_idx + 1) % closed_line.junctions_.size(); } // NOTE: This could also be done on a single axis (skipping the implied sqrt), but figuring out which one and then using the right values became a bit messy/verbose. diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index 6baf2f44be..2b17229c36 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -1328,11 +1328,6 @@ void LayerPlan::addWall( { return; } - if (is_closed) - { - // make sure wall start point is not above air! - start_idx = locateFirstSupportedVertex(wall, start_idx); - } const bool actual_scarf_seam = scarf_seam && is_closed; double non_bridge_line_volume = max_non_bridge_line_volume; // assume extruder is fully pressurised before first non-bridge line is output @@ -3049,6 +3044,11 @@ void LayerPlan::setSeamOverhangMask(const Shape& polys) seam_overhang_mask_ = polys; } +const Shape& LayerPlan::getSeamOverhangMask() const +{ + return seam_overhang_mask_; +} + void LayerPlan::setRoofingMask(const Shape& polys) { roofing_mask_ = polys; diff --git a/src/utils/AABB.cpp b/src/utils/AABB.cpp index 163c6a5f26..36cfa32e98 100644 --- a/src/utils/AABB.cpp +++ b/src/utils/AABB.cpp @@ -33,7 +33,7 @@ AABB::AABB(const Shape& shape) calculate(shape); } -AABB::AABB(const Polygon& poly) +AABB::AABB(const PointsSet& poly) : min_(POINT_MAX, POINT_MAX) , max_(POINT_MIN, POINT_MIN) { @@ -83,7 +83,7 @@ void AABB::calculate(const Shape& shape) } } -void AABB::calculate(const Polygon& poly) +void AABB::calculate(const PointsSet& poly) { min_ = Point2LL(POINT_MAX, POINT_MAX); max_ = Point2LL(POINT_MIN, POINT_MIN); @@ -141,7 +141,7 @@ void AABB::include(const Point2LL& point) max_.Y = std::max(max_.Y, point.Y); } -void AABB::include(const Polygon& polygon) +void AABB::include(const PointsSet& polygon) { for (const Point2LL& point : polygon) { diff --git a/src/utils/SVG.cpp b/src/utils/SVG.cpp index 69a115d9d4..83f223e9b1 100644 --- a/src/utils/SVG.cpp +++ b/src/utils/SVG.cpp @@ -59,6 +59,14 @@ std::string SVG::toString(const ColorObject& color) const } } +void SVG::handleFlush(const bool flush) const +{ + if (flush) + { + fflush(out_); + } +} + SVG::SVG(std::string filename, AABB aabb, Point2LL canvas_size, ColorObject background) : SVG( @@ -249,7 +257,7 @@ void SVG::writeLines(const std::vector& polyline, const ColorObject co fprintf(out_, "\" />\n"); // Write the end of the tag. } -void SVG::writeLine(const Point2LL& a, const Point2LL& b, const ColorObject color, const double stroke_width) const +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); @@ -262,6 +270,8 @@ void SVG::writeLine(const Point2LL& a, const Point2LL& b, const ColorObject colo 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 @@ -342,10 +352,7 @@ void SVG::writePolygons(const Shape& polys, const ColorObject color, const doubl writePolygon(poly, color, stroke_width, false); } - if (flush) - { - fflush(out_); - } + handleFlush(flush); } void SVG::writePolygon(const Polygon poly, const ColorObject color, const double stroke_width, const bool flush) const @@ -381,19 +388,18 @@ void SVG::writePolygon(const Polygon poly, const ColorObject color, const double i++; } - if (flush) - { - fflush(out_); - } + handleFlush(flush); } -void SVG::writePolylines(const Shape& polys, const ColorObject color, const double stroke_width) const +void SVG::writePolylines(const Shape& polys, const ColorObject color, const double stroke_width, const bool flush) const { for (const Polygon& poly : polys) { - writePolyline(poly, color, stroke_width); + writePolyline(poly, color, stroke_width, false); } + + handleFlush(flush); } void SVG::writePolyline(const Polygon& poly, const ColorObject color, const double stroke_width) const @@ -427,6 +433,16 @@ void SVG::writePolyline(const Polygon& poly, const ColorObject color, const doub } } +void SVG::writePolyline(const Polyline& poly, const ColorObject color, const double stroke_width, const bool flush) const +{ + for (auto iterator = poly.beginSegments(); iterator != poly.endSegments(); ++iterator) + { + writeLine((*iterator).start, (*iterator).end, color, stroke_width, false); + } + + handleFlush(flush); +} + void SVG::writePaths(const std::vector& paths, const ColorObject color, const double width_factor) const { for (const VariableWidthLines& lines : paths) @@ -443,7 +459,7 @@ void SVG::writeLines(const VariableWidthLines& lines, const ColorObject color, c } } -void SVG::writeLine(const ExtrusionLine& line, const ColorObject color, const double width_factor) const +void SVG::writeLine(const ExtrusionLine& line, const ColorObject color, const double width_factor, const bool flush) const { constexpr double minimum_line_width = 10; // Always have some width, otherwise some lines become completely invisible. if (line.junctions_.empty()) // Only draw lines that have at least 2 junctions, otherwise they are degenerate. @@ -481,6 +497,11 @@ void SVG::writeLine(const ExtrusionLine& line, const ColorObject color, const do start_vertex = end_vertex; // For the next line segment. } + + if (flush) + { + fflush(out_); + } } void SVG::writeCoordinateGrid(const coord_t grid_size, const Color color, const double stroke_width, const double font_size) const diff --git a/tests/PathOrderOptimizerTest.cpp b/tests/PathOrderOptimizerTest.cpp index c8621ba3d8..97cc1617c5 100644 --- a/tests/PathOrderOptimizerTest.cpp +++ b/tests/PathOrderOptimizerTest.cpp @@ -12,25 +12,17 @@ namespace cura class PathOrderOptimizerTest : public testing::Test { public: - /*! - * A blank optimizer with no polygons added yet. Fresh and virgin. - */ - PathOrderOptimizer optimizer; - /*! * A simple isosceles triangle. Base length and height 50. */ Polygon triangle; PathOrderOptimizerTest() - : optimizer(Point2LL(0, 0)) { } void SetUp() override { - optimizer = PathOrderOptimizer(Point2LL(0, 0)); - triangle.clear(); triangle.push_back(Point2LL(0, 0)); triangle.push_back(Point2LL(50, 0)); @@ -44,6 +36,7 @@ class PathOrderOptimizerTest : public testing::Test */ TEST_F(PathOrderOptimizerTest, OptimizeWhileEmpty) { + PathOrderOptimizer optimizer(Point2LL(0, 0)); optimizer.optimize(); // Don't crash. EXPECT_EQ(optimizer.paths_.size(), 0) << "Still empty!"; } @@ -54,6 +47,8 @@ TEST_F(PathOrderOptimizerTest, OptimizeWhileEmpty) */ TEST_F(PathOrderOptimizerTest, ThreeTrianglesShortestOrder) { + PathOrderOptimizer optimizer(Point2LL(0, 0)); + Polygon near = triangle; // Copy, then translate. near.translate(Point2LL(100, 100)); Polygon middle = triangle;