diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp new file mode 100644 index 000000000..10817099e --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp @@ -0,0 +1,62 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include + +#include "BeadingStrategy.hpp" +#include "Point.hpp" + +namespace Slic3r::Arachne +{ + +BeadingStrategy::BeadingStrategy(coord_t optimal_width, coord_t default_transition_length, float transitioning_angle) + : optimal_width(optimal_width) + , default_transition_length(default_transition_length) + , transitioning_angle(transitioning_angle) +{ + name = "Unknown"; +} + +coord_t BeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const +{ + if (lower_bead_count == 0) + { + return scaled(0.01); + } + return default_transition_length; +} + +float BeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const +{ + coord_t lower_optimum = getOptimalThickness(lower_bead_count); + coord_t transition_point = getTransitionThickness(lower_bead_count); + coord_t upper_optimum = getOptimalThickness(lower_bead_count + 1); + return 1.0 - float(transition_point - lower_optimum) / float(upper_optimum - lower_optimum); +} + +std::vector BeadingStrategy::getNonlinearThicknesses(coord_t lower_bead_count) const +{ + return std::vector(); +} + +std::string BeadingStrategy::toString() const +{ + return name; +} + +coord_t BeadingStrategy::getDefaultTransitionLength() const +{ + return default_transition_length; +} + +coord_t BeadingStrategy::getOptimalWidth() const +{ + return optimal_width; +} + +double BeadingStrategy::getTransitioningAngle() const +{ + return transitioning_angle; +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp new file mode 100644 index 000000000..85b86fa9d --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp @@ -0,0 +1,112 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BEADING_STRATEGY_H +#define BEADING_STRATEGY_H + +#include + +#include "../../libslic3r.h" + +namespace Slic3r::Arachne +{ + +template constexpr T pi_div(const T div) { return static_cast(M_PI) / div; } + +/*! + * Mostly virtual base class template. + * + * Strategy for covering a given (constant) horizontal model thickness with a number of beads. + * + * The beads may have different widths. + * + * TODO: extend with printing order? + */ +class BeadingStrategy +{ +public: + /*! + * The beading for a given horizontal model thickness. + */ + struct Beading + { + coord_t total_thickness; + std::vector bead_widths; //! The line width of each bead from the outer inset inward + std::vector toolpath_locations; //! The distance of the toolpath location of each bead from the outline + coord_t left_over; //! The distance not covered by any bead; gap area. + }; + + BeadingStrategy(coord_t optimal_width, coord_t default_transition_length, float transitioning_angle = pi_div(3)); + + virtual ~BeadingStrategy() {} + + /*! + * Retrieve the bead widths with which to cover a given thickness. + * + * Requirement: Given a constant \p bead_count the output of each bead width must change gradually along with the \p thickness. + * + * \note The \p bead_count might be different from the \ref BeadingStrategy::optimal_bead_count + */ + virtual Beading compute(coord_t thickness, coord_t bead_count) const = 0; + + /*! + * The ideal thickness for a given \param bead_count + */ + virtual coord_t getOptimalThickness(coord_t bead_count) const = 0; + + /*! + * The model thickness at which \ref BeadingStrategy::optimal_bead_count transitions from \p lower_bead_count to \p lower_bead_count + 1 + */ + virtual coord_t getTransitionThickness(coord_t lower_bead_count) const = 0; + + /*! + * The number of beads should we ideally usefor a given model thickness + */ + virtual coord_t getOptimalBeadCount(coord_t thickness) const = 0; + + /*! + * The length of the transitioning region along the marked / significant regions of the skeleton. + * + * Transitions are used to smooth out the jumps in integer bead count; the jumps turn into ramps with some incline defined by their length. + */ + virtual coord_t getTransitioningLength(coord_t lower_bead_count) const; + + /*! + * The fraction of the transition length to put between the lower end of the transition and the point where the unsmoothed bead count jumps. + * + * Transitions are used to smooth out the jumps in integer bead count; the jumps turn into ramps which could be positioned relative to the jump location. + */ + virtual float getTransitionAnchorPos(coord_t lower_bead_count) const; + + /*! + * Get the locations in a bead count region where \ref BeadingStrategy::compute exhibits a bend in the widths. + * Ordered from lower thickness to higher. + * + * This is used to insert extra support bones into the skeleton, so that the resulting beads in long trapezoids don't linearly change between the two ends. + */ + virtual std::vector getNonlinearThicknesses(coord_t lower_bead_count) const; + + virtual std::string toString() const; + + coord_t getOptimalWidth() const; + coord_t getDefaultTransitionLength() const; + double getTransitioningAngle() const; + +protected: + std::string name; + + coord_t optimal_width; //! Optimal bead width, nominal width off the walls in 'ideal' circumstances. + + coord_t default_transition_length; //! The length of the region to smoothly transfer between bead counts + + /*! + * The maximum angle between outline segments smaller than which we are going to add transitions + * Equals 180 - the "limit bisector angle" from the paper + */ + double transitioning_angle; +}; + +using BeadingStrategyPtr = std::unique_ptr; + +} // namespace Slic3r::Arachne +#endif // BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp new file mode 100644 index 000000000..38b2ee24d --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp @@ -0,0 +1,97 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "BeadingStrategyFactory.hpp" + +#include "LimitedBeadingStrategy.hpp" +#include "CenterDeviationBeadingStrategy.hpp" +#include "WideningBeadingStrategy.hpp" +#include "DistributedBeadingStrategy.hpp" +#include "RedistributeBeadingStrategy.hpp" +#include "OuterWallInsetBeadingStrategy.hpp" + +#include +#include + +namespace Slic3r::Arachne +{ + +coord_t getWeightedAverage(const coord_t preferred_bead_width_outer, const coord_t preferred_bead_width_inner, const coord_t max_bead_count) +{ + if(max_bead_count > preferred_bead_width_outer - preferred_bead_width_inner) + { + //The difference between outer and inner bead width would be spread out across so many lines that rounding errors would destroy the difference. + //Also catches the case of max_bead_count being "infinite" (max integer). + return (preferred_bead_width_outer + preferred_bead_width_inner) / 2; + } + if (max_bead_count > 2) + { + return ((preferred_bead_width_outer * 2) + preferred_bead_width_inner * (max_bead_count - 2)) / max_bead_count; + } + if (max_bead_count <= 0) + { + return preferred_bead_width_inner; + } + return preferred_bead_width_outer; +} + +BeadingStrategyPtr BeadingStrategyFactory::makeStrategy +( + const BeadingStrategyType type, + const coord_t preferred_bead_width_outer, + const coord_t preferred_bead_width_inner, + const coord_t preferred_transition_length, + const float transitioning_angle, + const bool print_thin_walls, + const coord_t min_bead_width, + const coord_t min_feature_size, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold, + const coord_t max_bead_count, + const coord_t outer_wall_offset, + const int inward_distributed_center_wall_count, + const double minimum_variable_line_width +) +{ + using std::make_unique; + using std::move; + const coord_t bar_preferred_wall_width = getWeightedAverage(preferred_bead_width_outer, preferred_bead_width_inner, max_bead_count); + BeadingStrategyPtr ret; + switch (type) + { + case BeadingStrategyType::Center: + ret = make_unique(bar_preferred_wall_width, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold); + break; + case BeadingStrategyType::Distributed: + ret = make_unique(bar_preferred_wall_width, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, std::numeric_limits::max()); + break; + case BeadingStrategyType::InwardDistributed: + ret = make_unique(bar_preferred_wall_width, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, inward_distributed_center_wall_count); + break; + default: + BOOST_LOG_TRIVIAL(error) << "Cannot make strategy!"; + return nullptr; + } + + if(print_thin_walls) + { + BOOST_LOG_TRIVIAL(debug) << "Applying the Widening Beading meta-strategy with minimum input width " << min_feature_size << " and minimum output width " << min_bead_width << "."; + ret = make_unique(move(ret), min_feature_size, min_bead_width); + } + if (max_bead_count > 0) + { + BOOST_LOG_TRIVIAL(debug) << "Applying the Redistribute meta-strategy with outer-wall width = " << preferred_bead_width_outer << ", inner-wall width = " << preferred_bead_width_inner; + ret = make_unique(preferred_bead_width_outer, preferred_bead_width_inner, minimum_variable_line_width, move(ret)); + //Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. + BOOST_LOG_TRIVIAL(debug) << "Applying the Limited Beading meta-strategy with maximum bead count = " << max_bead_count << "."; + ret = make_unique(max_bead_count, move(ret)); + } + + if (outer_wall_offset > 0) + { + BOOST_LOG_TRIVIAL(debug) << "Applying the OuterWallOffset meta-strategy with offset = " << outer_wall_offset << "."; + ret = make_unique(outer_wall_offset, move(ret)); + } + return ret; +} +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp new file mode 100644 index 000000000..741262b60 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp @@ -0,0 +1,36 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BEADING_STRATEGY_FACTORY_H +#define BEADING_STRATEGY_FACTORY_H + +#include "BeadingStrategy.hpp" +#include "../../Point.hpp" + +namespace Slic3r::Arachne +{ + +class BeadingStrategyFactory +{ +public: + static BeadingStrategyPtr makeStrategy + ( + const BeadingStrategyType type, + const coord_t preferred_bead_width_outer = scaled(0.0005), + const coord_t preferred_bead_width_inner = scaled(0.0005), + const coord_t preferred_transition_length = scaled(0.0004), + const float transitioning_angle = M_PI / 4.0, + const bool print_thin_walls = false, + const coord_t min_bead_width = 0, + const coord_t min_feature_size = 0, + const double wall_split_middle_threshold = 0.5, + const double wall_add_middle_threshold = 0.5, + const coord_t max_bead_count = 0, + const coord_t outer_wall_offset = 0, + const int inward_distributed_center_wall_count = 2, + const double minimum_variable_line_width = 0.5 + ); +}; + +} // namespace Slic3r::Arachne +#endif // BEADING_STRATEGY_FACTORY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.cpp new file mode 100644 index 000000000..5c985bd2c --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. +#include + +#include "CenterDeviationBeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ +CenterDeviationBeadingStrategy::CenterDeviationBeadingStrategy(const coord_t pref_bead_width, + const double transitioning_angle, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold) + : BeadingStrategy(pref_bead_width, pref_bead_width / 2, transitioning_angle), + minimum_line_width_split(pref_bead_width * wall_split_middle_threshold), + minimum_line_width_add(pref_bead_width * wall_add_middle_threshold) +{ + name = "CenterDeviationBeadingStrategy"; +} + +CenterDeviationBeadingStrategy::Beading CenterDeviationBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + Beading ret; + + ret.total_thickness = thickness; + if (bead_count > 0) + { + // Set the bead widths + ret.bead_widths = std::vector(static_cast(bead_count), optimal_width); + const coord_t optimal_thickness = getOptimalThickness(bead_count); + const coord_t diff_thickness = thickness - optimal_thickness; //Amount of deviation. Either spread out over the middle 2 lines, or concentrated in the center line. + const size_t center_bead_idx = ret.bead_widths.size() / 2; + if (bead_count % 2 == 0) // Even lines + { + const coord_t inner_bead_widths = optimal_width + diff_thickness / 2; + if (inner_bead_widths < minimum_line_width_add) + { + return compute(thickness, bead_count - 1); + } + ret.bead_widths[center_bead_idx - 1] = inner_bead_widths; + ret.bead_widths[center_bead_idx] = inner_bead_widths; + } + else // Uneven lines + { + const coord_t inner_bead_widths = optimal_width + diff_thickness; + if (inner_bead_widths < minimum_line_width_split) + { + return compute(thickness, bead_count - 1); + } + ret.bead_widths[center_bead_idx] = inner_bead_widths; + } + + // Set the center line location of the bead toolpaths. + ret.toolpath_locations.resize(ret.bead_widths.size()); + ret.toolpath_locations.front() = ret.bead_widths.front() / 2; + for (size_t bead_idx = 1; bead_idx < ret.bead_widths.size(); ++bead_idx) + { + ret.toolpath_locations[bead_idx] = + ret.toolpath_locations[bead_idx - 1] + (ret.bead_widths[bead_idx] + ret.bead_widths[bead_idx - 1]) / 2; + } + ret.left_over = 0; + } + else + { + ret.left_over = thickness; + } + + return ret; +} + +coord_t CenterDeviationBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + return bead_count * optimal_width; +} + +coord_t CenterDeviationBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + return lower_bead_count * optimal_width + (lower_bead_count % 2 == 1 ? minimum_line_width_split : minimum_line_width_add); +} + +coord_t CenterDeviationBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + const coord_t naive_count = thickness / optimal_width; // How many lines we can fit in for sure. + const coord_t remainder = thickness - naive_count * optimal_width; // Space left after fitting that many lines. + const coord_t minimum_line_width = naive_count % 2 == 1 ? minimum_line_width_split : minimum_line_width_add; + return naive_count + (remainder > minimum_line_width); // If there's enough space, fit an extra one. +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.hpp new file mode 100644 index 000000000..4dd6c928a --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + + +#ifndef CENTER_DEVIATION_BEADING_STRATEGY_H +#define CENTER_DEVIATION_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * This beading strategy makes the deviation in the thickness of the part + * entirely compensated by the innermost wall. + * + * The outermost walls all use the ideal width, as far as possible. + */ +class CenterDeviationBeadingStrategy : public BeadingStrategy +{ + private: + // For uneven numbers of lines: Minimum line width for which the middle line will be split into two lines. + coord_t minimum_line_width_split; + + // For even numbers of lines: Minimum line width for which a new middle line will be added between the two innermost lines. + coord_t minimum_line_width_add; + + public: + CenterDeviationBeadingStrategy(coord_t pref_bead_width, + double transitioning_angle, + double wall_split_middle_threshold, + double wall_add_middle_threshold); + + ~CenterDeviationBeadingStrategy() override{}; + Beading compute(coord_t thickness, coord_t bead_count) const override; + coord_t getOptimalThickness(coord_t bead_count) const override; + coord_t getTransitionThickness(coord_t lower_bead_count) const override; + coord_t getOptimalBeadCount(coord_t thickness) const override; +}; + +} // namespace Slic3r::Arachne +#endif // CENTER_DEVIATION_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp new file mode 100644 index 000000000..42cd98a69 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp @@ -0,0 +1,110 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. +#include +#include "DistributedBeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +DistributedBeadingStrategy::DistributedBeadingStrategy(const coord_t optimal_width, + const coord_t default_transition_length, + const double transitioning_angle, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold, + const int distribution_radius) + : BeadingStrategy(optimal_width, default_transition_length, transitioning_angle) + , wall_split_middle_threshold(wall_split_middle_threshold) + , wall_add_middle_threshold(wall_add_middle_threshold) +{ + if(distribution_radius >= 2) + { + one_over_distribution_radius_squared = 1.0f / (distribution_radius - 1) * 1.0f / (distribution_radius - 1); + } + else + { + one_over_distribution_radius_squared = 1.0f / 1 * 1.0f / 1; + } + name = "DistributedBeadingStrategy"; +} + +DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + Beading ret; + + ret.total_thickness = thickness; + if (bead_count > 2) + { + const coord_t to_be_divided = thickness - bead_count * optimal_width; + const float middle = static_cast(bead_count - 1) / 2; + + const auto getWeight = [middle, this](coord_t bead_idx) + { + const float dev_from_middle = bead_idx - middle; + return std::max(0.0f, 1.0f - one_over_distribution_radius_squared * dev_from_middle * dev_from_middle); + }; + + std::vector weights; + weights.resize(bead_count); + for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++) + { + weights[bead_idx] = getWeight(bead_idx); + } + + const float total_weight = std::accumulate(weights.cbegin(), weights.cend(), 0.f); + for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++) + { + const float weight_fraction = weights[bead_idx] / total_weight; + const coord_t splitup_left_over_weight = to_be_divided * weight_fraction; + const coord_t width = optimal_width + splitup_left_over_weight; + if (bead_idx == 0) + { + ret.toolpath_locations.emplace_back(width / 2); + } + else + { + ret.toolpath_locations.emplace_back(ret.toolpath_locations.back() + (ret.bead_widths.back() + width) / 2); + } + ret.bead_widths.emplace_back(width); + } + ret.left_over = 0; + } + else if (bead_count == 2) + { + const coord_t outer_width = thickness / 2; + ret.bead_widths.emplace_back(outer_width); + ret.bead_widths.emplace_back(outer_width); + ret.toolpath_locations.emplace_back(outer_width / 2); + ret.toolpath_locations.emplace_back(thickness - outer_width / 2); + ret.left_over = 0; + } + else if (bead_count == 1) + { + const coord_t outer_width = thickness; + ret.bead_widths.emplace_back(outer_width); + ret.toolpath_locations.emplace_back(outer_width / 2); + ret.left_over = 0; + } + else + { + ret.left_over = thickness; + } + + return ret; +} + +coord_t DistributedBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + return bead_count * optimal_width; +} + +coord_t DistributedBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + return lower_bead_count * optimal_width + optimal_width * (lower_bead_count % 2 == 1 ? wall_split_middle_threshold : wall_add_middle_threshold); +} + +coord_t DistributedBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + return (thickness + optimal_width / 2) / optimal_width; +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp new file mode 100644 index 000000000..a027d781d --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef DISTRIBUTED_BEADING_STRATEGY_H +#define DISTRIBUTED_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * This beading strategy chooses a wall count that would make the line width + * deviate the least from the optimal line width, and then distributes the lines + * evenly among the thickness available. + */ +class DistributedBeadingStrategy : public BeadingStrategy +{ +protected: + // For uneven numbers of lines: Minimum factor of the optimal width for which the middle line will be split into two lines. + double wall_split_middle_threshold; + + // For even numbers of lines: Minimum factor of the optimal width for which a new middle line will be added between the two innermost lines. + double wall_add_middle_threshold; + + float one_over_distribution_radius_squared; // (1 / distribution_radius)^2 + +public: + /*! + * \param distribution_radius the radius (in number of beads) over which to distribute the discrepancy between the feature size and the optimal thickness + */ + DistributedBeadingStrategy( const coord_t optimal_width, + const coord_t default_transition_length, + const double transitioning_angle, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold, + const int distribution_radius); + + virtual ~DistributedBeadingStrategy() override {} + + Beading compute(coord_t thickness, coord_t bead_count) const override; + coord_t getOptimalThickness(coord_t bead_count) const override; + coord_t getTransitionThickness(coord_t lower_bead_count) const override; + coord_t getOptimalBeadCount(coord_t thickness) const override; +}; + +} // namespace Slic3r::Arachne +#endif // DISTRIBUTED_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp new file mode 100644 index 000000000..f5776ca9b --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp @@ -0,0 +1,136 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include +#include + +#include "LimitedBeadingStrategy.hpp" +#include "Point.hpp" + +namespace Slic3r::Arachne +{ + +std::string LimitedBeadingStrategy::toString() const +{ + return std::string("LimitedBeadingStrategy+") + parent->toString(); +} + +coord_t LimitedBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const +{ + return parent->getTransitioningLength(lower_bead_count); +} + +float LimitedBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const +{ + return parent->getTransitionAnchorPos(lower_bead_count); +} + +LimitedBeadingStrategy::LimitedBeadingStrategy(const coord_t max_bead_count, BeadingStrategyPtr parent) + : BeadingStrategy(parent->getOptimalWidth(), /*default_transition_length=*/-1, parent->getTransitioningAngle()) + , max_bead_count(max_bead_count) + , parent(std::move(parent)) +{ + if (max_bead_count % 2 == 1) + { + BOOST_LOG_TRIVIAL(warning) << "LimitedBeadingStrategy with odd bead count is odd indeed!"; + } +} + +LimitedBeadingStrategy::Beading LimitedBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + if (bead_count <= max_bead_count) + { + Beading ret = parent->compute(thickness, bead_count); + bead_count = ret.toolpath_locations.size(); + + if (bead_count % 2 == 0 && bead_count == max_bead_count) + { + const coord_t innermost_toolpath_location = ret.toolpath_locations[max_bead_count / 2 - 1]; + const coord_t innermost_toolpath_width = ret.bead_widths[max_bead_count / 2 - 1]; + ret.toolpath_locations.insert(ret.toolpath_locations.begin() + max_bead_count / 2, innermost_toolpath_location + innermost_toolpath_width / 2); + ret.bead_widths.insert(ret.bead_widths.begin() + max_bead_count / 2, 0); + } + return ret; + } + assert(bead_count == max_bead_count + 1); + if(bead_count != max_bead_count + 1) + { + BOOST_LOG_TRIVIAL(warning) << "Too many beads! " << bead_count << " != " << max_bead_count + 1; + } + + coord_t optimal_thickness = parent->getOptimalThickness(max_bead_count); + Beading ret = parent->compute(optimal_thickness, max_bead_count); + bead_count = ret.toolpath_locations.size(); + ret.left_over += thickness - ret.total_thickness; + ret.total_thickness = thickness; + + // Enforce symmetry + if (bead_count % 2 == 1) + { + ret.toolpath_locations[bead_count / 2] = thickness / 2; + ret.bead_widths[bead_count / 2] = thickness - optimal_thickness; + } + for (coord_t bead_idx = 0; bead_idx < (bead_count + 1) / 2; bead_idx++) + { + ret.toolpath_locations[bead_count - 1 - bead_idx] = thickness - ret.toolpath_locations[bead_idx]; + } + + //Create a "fake" inner wall with 0 width to indicate the edge of the walled area. + //This wall can then be used by other structures to e.g. fill the infill area adjacent to the variable-width walls. + coord_t innermost_toolpath_location = ret.toolpath_locations[max_bead_count / 2 - 1]; + coord_t innermost_toolpath_width = ret.bead_widths[max_bead_count / 2 - 1]; + ret.toolpath_locations.insert(ret.toolpath_locations.begin() + max_bead_count / 2, innermost_toolpath_location + innermost_toolpath_width / 2); + ret.bead_widths.insert(ret.bead_widths.begin() + max_bead_count / 2, 0); + + //Symmetry on both sides. Symmetry is guaranteed since this code is stopped early if the bead_count <= max_bead_count, and never reaches this point then. + const size_t opposite_bead = bead_count - (max_bead_count / 2 - 1); + innermost_toolpath_location = ret.toolpath_locations[opposite_bead]; + innermost_toolpath_width = ret.bead_widths[opposite_bead]; + ret.toolpath_locations.insert(ret.toolpath_locations.begin() + opposite_bead, innermost_toolpath_location - innermost_toolpath_width / 2); + ret.bead_widths.insert(ret.bead_widths.begin() + opposite_bead, 0); + + return ret; +} + +coord_t LimitedBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + if (bead_count <= max_bead_count) + { + return parent->getOptimalThickness(bead_count); + } + assert(false); + return scaled(1000.); // 1 meter (Cura was returning 10 meter) +} + +coord_t LimitedBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + if (lower_bead_count < max_bead_count) + { + return parent->getTransitionThickness(lower_bead_count); + } + if (lower_bead_count == max_bead_count) + { + return parent->getOptimalThickness(lower_bead_count + 1) - scaled(0.01); + } + assert(false); + return scaled(900.); // 0.9 meter; +} + +coord_t LimitedBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + coord_t parent_bead_count = parent->getOptimalBeadCount(thickness); + if (parent_bead_count <= max_bead_count) + { + return parent->getOptimalBeadCount(thickness); + } + else if (parent_bead_count == max_bead_count + 1) + { + if (thickness < parent->getOptimalThickness(max_bead_count + 1) - scaled(0.01)) + return max_bead_count; + else + return max_bead_count + 1; + } + else return max_bead_count + 1; +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp new file mode 100644 index 000000000..9098fabb8 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp @@ -0,0 +1,49 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef LIMITED_BEADING_STRATEGY_H +#define LIMITED_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * This is a meta-strategy that can be applied on top of any other beading + * strategy, which limits the thickness of the walls to the thickness that the + * lines can reasonably print. + * + * The width of the wall is limited to the maximum number of contours times the + * maximum width of each of these contours. + * + * If the width of the wall gets limited, this strategy outputs one additional + * bead with 0 width. This bead is used to denote the limits of the walled area. + * Other structures can then use this border to align their structures to, such + * as to create correctly overlapping infill or skin, or to align the infill + * pattern to any extra infill walls. + */ +class LimitedBeadingStrategy : public BeadingStrategy +{ +public: + LimitedBeadingStrategy(const coord_t max_bead_count, BeadingStrategyPtr parent); + + virtual ~LimitedBeadingStrategy() override = default; + + Beading compute(coord_t thickness, coord_t bead_count) const override; + coord_t getOptimalThickness(coord_t bead_count) const override; + coord_t getTransitionThickness(coord_t lower_bead_count) const override; + coord_t getOptimalBeadCount(coord_t thickness) const override; + virtual std::string toString() const override; + + coord_t getTransitioningLength(coord_t lower_bead_count) const override; + + float getTransitionAnchorPos(coord_t lower_bead_count) const override; + +protected: + const coord_t max_bead_count; + const BeadingStrategyPtr parent; +}; + +} // namespace Slic3r::Arachne +#endif // LIMITED_DISTRIBUTED_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp new file mode 100644 index 000000000..9028a0d4e --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp @@ -0,0 +1,62 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "OuterWallInsetBeadingStrategy.hpp" + +#include + +namespace Slic3r::Arachne +{ +OuterWallInsetBeadingStrategy::OuterWallInsetBeadingStrategy(coord_t outer_wall_offset, BeadingStrategyPtr parent) : + BeadingStrategy(parent->getOptimalWidth(), parent->getDefaultTransitionLength(), parent->getTransitioningAngle()), + parent(std::move(parent)), + outer_wall_offset(outer_wall_offset) +{ + name = "OuterWallOfsetBeadingStrategy"; +} + + +coord_t OuterWallInsetBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + return parent->getOptimalThickness(bead_count); +} + +coord_t OuterWallInsetBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + return parent->getTransitionThickness(lower_bead_count); +} + +coord_t OuterWallInsetBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + return parent->getOptimalBeadCount(thickness); +} + +coord_t OuterWallInsetBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const +{ + return parent->getTransitioningLength(lower_bead_count); +} + +std::string OuterWallInsetBeadingStrategy::toString() const +{ + return std::string("OuterWallOfsetBeadingStrategy+") + parent->toString(); +} + +BeadingStrategy::Beading OuterWallInsetBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + Beading ret = parent->compute(thickness, bead_count); + + // Actual count and thickness as represented by extant walls. Don't count any potential zero-width 'signaling' walls. + bead_count = std::count_if(ret.bead_widths.begin(), ret.bead_widths.end(), [](const coord_t width) { return width > 0; }); + + // No need to apply any inset if there is just a single wall. + if (bead_count < 2) + { + return ret; + } + + // Actually move the outer wall inside. Ensure that the outer wall never goes beyond the middle line. + ret.toolpath_locations[0] = std::min(ret.toolpath_locations[0] + outer_wall_offset, thickness / 2); + return ret; +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp new file mode 100644 index 000000000..f7fcfe551 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp @@ -0,0 +1,35 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef OUTER_WALL_INSET_BEADING_STRATEGY_H +#define OUTER_WALL_INSET_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + /* + * This is a meta strategy that allows for the outer wall to be inset towards the inside of the model. + */ + class OuterWallInsetBeadingStrategy : public BeadingStrategy + { + public: + OuterWallInsetBeadingStrategy(coord_t outer_wall_offset, BeadingStrategyPtr parent); + + virtual ~OuterWallInsetBeadingStrategy() = default; + + Beading compute(coord_t thickness, coord_t bead_count) const override; + + coord_t getOptimalThickness(coord_t bead_count) const override; + coord_t getTransitionThickness(coord_t lower_bead_count) const override; + coord_t getOptimalBeadCount(coord_t thickness) const override; + coord_t getTransitioningLength(coord_t lower_bead_count) const override; + + std::string toString() const override; + + private: + BeadingStrategyPtr parent; + coord_t outer_wall_offset; + }; +} // namespace Slic3r::Arachne +#endif // OUTER_WALL_INSET_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp new file mode 100644 index 000000000..539db3a13 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp @@ -0,0 +1,180 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "RedistributeBeadingStrategy.hpp" + +#include +#include + +namespace Slic3r::Arachne +{ + +RedistributeBeadingStrategy::RedistributeBeadingStrategy( const coord_t optimal_width_outer, + const coord_t optimal_width_inner, + const double minimum_variable_line_width, + BeadingStrategyPtr parent) : + BeadingStrategy(parent->getOptimalWidth(), parent->getDefaultTransitionLength(), parent->getTransitioningAngle()), + parent(std::move(parent)), + optimal_width_outer(optimal_width_outer), + optimal_width_inner(optimal_width_inner), + minimum_variable_line_width(minimum_variable_line_width) +{ + name = "RedistributeBeadingStrategy"; +} + +coord_t RedistributeBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + const coord_t inner_bead_count = bead_count > 2 ? bead_count - 2 : 0; + const coord_t outer_bead_count = bead_count - inner_bead_count; + + return parent->getOptimalThickness(inner_bead_count) + optimal_width_outer * outer_bead_count; +} + +coord_t RedistributeBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + return parent->getTransitionThickness(lower_bead_count); +} + +coord_t RedistributeBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + return parent->getOptimalBeadCount(thickness); +} + +coord_t RedistributeBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const +{ + return parent->getTransitioningLength(lower_bead_count); +} + +float RedistributeBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const +{ + return parent->getTransitionAnchorPos(lower_bead_count); +} + +std::string RedistributeBeadingStrategy::toString() const +{ + return std::string("RedistributeBeadingStrategy+") + parent->toString(); +} + +BeadingStrategy::Beading RedistributeBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + Beading ret; + if (bead_count > 2) + { + const coord_t inner_transition_width = optimal_width_inner * minimum_variable_line_width; + const coord_t outer_bead_width = + getOptimalOuterBeadWidth(thickness, optimal_width_outer, inner_transition_width); + + // Outer wall is locked in size and position for wall regions of 3 and higher which have at least a + // thickness equal to two times the optimal outer width and the minimal inner wall width. + const coord_t virtual_thickness = thickness - outer_bead_width * 2; + const coord_t virtual_bead_count = bead_count - 2; + + // Calculate the beads and widths of the inner walls only + ret = parent->compute(virtual_thickness, virtual_bead_count); + + // Insert the outer beads + ret.bead_widths.insert(ret.bead_widths.begin(), outer_bead_width); + ret.bead_widths.emplace_back(outer_bead_width); + } + else + { + ret = parent->compute(thickness, bead_count); + } + + // Filter out beads that violate the minimum inner wall widths and recompute if necessary + const coord_t outer_transition_width = optimal_width_inner * minimum_variable_line_width; + const bool removed_inner_beads = validateInnerBeadWidths(ret, outer_transition_width); + if (removed_inner_beads) + { + ret = compute(thickness, bead_count - 1); + } + + // Ensure that the positions of the beads are distributed over the thickness + resetToolPathLocations(ret, thickness); + + return ret; +} + +coord_t RedistributeBeadingStrategy::getOptimalOuterBeadWidth(const coord_t thickness, const coord_t optimal_width_outer, const coord_t minimum_width_inner) +{ + const coord_t total_outer_optimal_width = optimal_width_outer * 2; + coord_t outer_bead_width = thickness / 2; + if (total_outer_optimal_width < thickness) + { + if (total_outer_optimal_width + minimum_width_inner > thickness) + { + outer_bead_width -= minimum_width_inner / 2; + } + else + { + outer_bead_width = optimal_width_outer; + } + } + return outer_bead_width; +} + +void RedistributeBeadingStrategy::resetToolPathLocations(BeadingStrategy::Beading& beading, const coord_t thickness) +{ + const size_t bead_count = beading.bead_widths.size(); + beading.toolpath_locations.resize(bead_count); + + if (bead_count < 1) + { + beading.toolpath_locations.resize(0); + beading.total_thickness = thickness; + beading.left_over = thickness; + return; + } + + // Update the first half of the toolpath-locations with the updated bead-widths (starting from 0, up to half): + coord_t last_coord = 0; + coord_t last_width = 0; + for (size_t i_location = 0; i_location < bead_count / 2; ++i_location) + { + beading.toolpath_locations[i_location] = last_coord + (last_width + beading.bead_widths[i_location]) / 2; + last_coord = beading.toolpath_locations[i_location]; + last_width = beading.bead_widths[i_location]; + } + + // Handle the position of any middle wall (note that the width will already have been set correctly): + if (bead_count % 2 == 1) + { + beading.toolpath_locations[bead_count / 2] = thickness / 2; + } + + // Update the last half of the toolpath-locations with the updated bead-widths (starting from thickness, down to half): + last_coord = thickness; + last_width = 0; + for (size_t i_location = bead_count - 1; i_location >= bead_count - (bead_count / 2); --i_location) + { + beading.toolpath_locations[i_location] = last_coord - (last_width + beading.bead_widths[i_location]) / 2; + last_coord = beading.toolpath_locations[i_location]; + last_width = beading.bead_widths[i_location]; + } + + // Ensure correct total and left over thickness + beading.total_thickness = thickness; + beading.left_over = thickness - std::accumulate(beading.bead_widths.cbegin(), beading.bead_widths.cend(), static_cast(0)); +} + +bool RedistributeBeadingStrategy::validateInnerBeadWidths(BeadingStrategy::Beading& beading, const coord_t minimum_width_inner) +{ + // Filter out bead_widths that violate the transition width and recalculate if needed + const size_t unfiltered_beads = beading.bead_widths.size(); + if(unfiltered_beads <= 2) //Outer walls are exempt. If there are 2 walls the range below will be empty. If there is 1 or 0 walls it would be invalid. + { + return false; + } + auto inner_begin = std::next(beading.bead_widths.begin()); + auto inner_end = std::prev(beading.bead_widths.end()); + beading.bead_widths.erase( + std::remove_if(inner_begin, inner_end, + [&minimum_width_inner](const coord_t width) + { + return width < minimum_width_inner; + }), + inner_end); + return unfiltered_beads != beading.bead_widths.size(); + } + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp new file mode 100644 index 000000000..ca2e3cb83 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp @@ -0,0 +1,98 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H +#define REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + /*! + * A meta-beading-strategy that takes outer and inner wall widths into account. + * + * The outer wall will try to keep a constant width by only applying the beading strategy on the inner walls. This + * ensures that this outer wall doesn't react to changes happening to inner walls. It will limit print artifacts on + * the surface of the print. Although this strategy technically deviates from the original philosophy of the paper. + * It will generally results in better prints because of a smoother motion and less variation in extrusion width in + * the outer walls. + * + * If the thickness of the model is less then two times the optimal outer wall width and once the minimum inner wall + * width it will keep the minimum inner wall at a minimum constant and vary the outer wall widths symmetrical. Until + * The thickness of the model is that of at least twice the optimal outer wall width it will then use two + * symmetrical outer walls only. Until it transitions into a single outer wall. These last scenario's are always + * symmetrical in nature, disregarding the user specified strategy. + */ + class RedistributeBeadingStrategy : public BeadingStrategy + { + public: + /*! + * /param optimal_width_outer Outer wall width, guaranteed to be the actual (save rounding errors) at a + * bead count if the parent strategies' optimum bead width is a weighted + * average of the outer and inner walls at that bead count. + * /param optimal_width_outer Inner wall width, guaranteed to be the actual (save rounding errors) at a + * bead count if the parent strategies' optimum bead width is a weighted + * average of the outer and inner walls at that bead count. + * /param minimum_variable_line_width Minimum factor that the variable line might deviate from the optimal width. + */ + RedistributeBeadingStrategy(const coord_t optimal_width_outer, + const coord_t optimal_width_inner, + const double minimum_variable_line_width, + BeadingStrategyPtr parent); + + virtual ~RedistributeBeadingStrategy() override = default; + + Beading compute(coord_t thickness, coord_t bead_count) const override; + + coord_t getOptimalThickness(coord_t bead_count) const override; + coord_t getTransitionThickness(coord_t lower_bead_count) const override; + coord_t getOptimalBeadCount(coord_t thickness) const override; + coord_t getTransitioningLength(coord_t lower_bead_count) const override; + float getTransitionAnchorPos(coord_t lower_bead_count) const override; + + std::string toString() const override; + + protected: + /*! + * Determine the outer bead width. + * + * According to the following logic: + * - If the thickness of the model is more then twice the optimal outer bead width and the minimum inner bead + * width it will return the optimal outer bead width. + * - If the thickness is less then twice the optimal outer bead width and the minimum inner bead width, but + * more them twice the optimal outer bead with it will return the optimal bead width minus half the inner bead + * width. + * - If the thickness is less then twice the optimal outer bead width it will return half the thickness as + * outer bead width + * + * \param thickness Thickness of the total beads. + * \param optimal_width_outer User specified optimal outer bead width. + * \param minimum_width_inner Inner bead width times the minimum variable line width. + * \return The outer bead width. + */ + static coord_t getOptimalOuterBeadWidth(coord_t thickness, coord_t optimal_width_outer, coord_t minimum_width_inner); + + /*! + * Moves the beads towards the outer edges of thickness and ensures that the outer walls are locked in location + * \param beading The beading instance. + * \param thickness The thickness of the bead. + */ + static void resetToolPathLocations(Beading& beading, coord_t thickness); + + /*! + * Filters and validates the beads, to ensure that all inner beads are at least the minimum bead width. + * + * \param beading The beading instance. + * \param minimum_width_inner Inner bead width times the minimum variable line width. + * \return true if beads are removed. + */ + static bool validateInnerBeadWidths(Beading& beading, coord_t minimum_width_inner); + + BeadingStrategyPtr parent; + coord_t optimal_width_outer; + coord_t optimal_width_inner; + double minimum_variable_line_width; + }; + +} // namespace Slic3r::Arachne +#endif // INWARD_DISTRIBUTED_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp new file mode 100644 index 000000000..ad4cad964 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp @@ -0,0 +1,89 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "WideningBeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +WideningBeadingStrategy::WideningBeadingStrategy(BeadingStrategyPtr parent, const coord_t min_input_width, const coord_t min_output_width) + : BeadingStrategy(parent->getOptimalWidth(), /*default_transition_length=*/-1, parent->getTransitioningAngle()) + , parent(std::move(parent)) + , min_input_width(min_input_width) + , min_output_width(min_output_width) +{ +} + +std::string WideningBeadingStrategy::toString() const +{ + return std::string("Widening+") + parent->toString(); +} + +WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +{ + if (thickness < optimal_width) + { + Beading ret; + ret.total_thickness = thickness; + if (thickness >= min_input_width) + { + ret.bead_widths.emplace_back(std::max(thickness, min_output_width)); + ret.toolpath_locations.emplace_back(thickness / 2); + } + else + { + ret.left_over = thickness; + } + return ret; + } + else + { + return parent->compute(thickness, bead_count); + } +} + +coord_t WideningBeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + return parent->getOptimalThickness(bead_count); +} + +coord_t WideningBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + if (lower_bead_count == 0) + { + return min_input_width; + } + else + { + return parent->getTransitionThickness(lower_bead_count); + } +} + +coord_t WideningBeadingStrategy::getOptimalBeadCount(coord_t thickness) const +{ + if (thickness < min_input_width) return 0; + coord_t ret = parent->getOptimalBeadCount(thickness); + if (thickness >= min_input_width && ret < 1) return 1; + return ret; +} + +coord_t WideningBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const +{ + return parent->getTransitioningLength(lower_bead_count); +} + +float WideningBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const +{ + return parent->getTransitionAnchorPos(lower_bead_count); +} + +std::vector WideningBeadingStrategy::getNonlinearThicknesses(coord_t lower_bead_count) const +{ + std::vector ret; + ret.emplace_back(min_output_width); + std::vector pret = parent->getNonlinearThicknesses(lower_bead_count); + ret.insert(ret.end(), pret.begin(), pret.end()); + return ret; +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp new file mode 100644 index 000000000..32aa9f058 --- /dev/null +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp @@ -0,0 +1,46 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef WIDENING_BEADING_STRATEGY_H +#define WIDENING_BEADING_STRATEGY_H + +#include "BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * This is a meta-strategy that can be applied on any other beading strategy. If + * the part is thinner than a single line, this strategy adjusts the part so + * that it becomes the minimum thickness of one line. + * + * This way, tiny pieces that are smaller than a single line will still be + * printed. + */ +class WideningBeadingStrategy : public BeadingStrategy +{ +public: + /*! + * Takes responsibility for deleting \param parent + */ + WideningBeadingStrategy(BeadingStrategyPtr parent, const coord_t min_input_width, const coord_t min_output_width); + + virtual ~WideningBeadingStrategy() override = default; + + virtual Beading compute(coord_t thickness, coord_t bead_count) const override; + virtual coord_t getOptimalThickness(coord_t bead_count) const override; + virtual coord_t getTransitionThickness(coord_t lower_bead_count) const override; + virtual coord_t getOptimalBeadCount(coord_t thickness) const override; + virtual coord_t getTransitioningLength(coord_t lower_bead_count) const override; + virtual float getTransitionAnchorPos(coord_t lower_bead_count) const override; + virtual std::vector getNonlinearThicknesses(coord_t lower_bead_count) const override; + virtual std::string toString() const override; + +protected: + BeadingStrategyPtr parent; + const coord_t min_input_width; + const coord_t min_output_width; +}; + +} // namespace Slic3r::Arachne +#endif // WIDENING_BEADING_STRATEGY_H diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp new file mode 100644 index 000000000..fabdc2486 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -0,0 +1,2091 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "SkeletalTrapezoidation.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/VoronoiUtils.hpp" + +#include "utils/linearAlg2D.hpp" +#include "Utils.hpp" +#include "SVG.hpp" +#include "Geometry/VoronoiVisualUtils.hpp" +#include "../EdgeGrid.hpp" + +#define SKELETAL_TRAPEZOIDATION_BEAD_SEARCH_MAX 1000 //A limit to how long it'll keep searching for adjacent beads. Increasing will re-use beadings more often (saving performance), but search longer for beading (costing performance). + +namespace boost::polygon { + +template<> struct geometry_concept +{ + typedef segment_concept type; +}; + +template<> struct segment_traits +{ + typedef coord_t coordinate_type; + typedef Slic3r::Point point_type; + static inline point_type get(const Slic3r::Arachne::PolygonsSegmentIndex &CSegment, direction_1d dir) + { + return dir.to_int() ? CSegment.p() : CSegment.next().p(); + } +}; + +} // namespace boost::polygon + +namespace Slic3r::Arachne +{ + +SkeletalTrapezoidation::node_t& SkeletalTrapezoidation::makeNode(vd_t::vertex_type& vd_node, Point p) +{ + auto he_node_it = vd_node_to_he_node.find(&vd_node); + if (he_node_it == vd_node_to_he_node.end()) + { + graph.nodes.emplace_front(SkeletalTrapezoidationJoint(), p); + node_t& node = graph.nodes.front(); + vd_node_to_he_node.emplace(&vd_node, &node); + return node; + } + else + { + return *he_node_it->second; + } +} + +void SkeletalTrapezoidation::transferEdge(Point from, Point to, vd_t::edge_type& vd_edge, edge_t*& prev_edge, Point& start_source_point, Point& end_source_point, const std::vector& segments) +{ + auto he_edge_it = vd_edge_to_he_edge.find(vd_edge.twin()); + if (he_edge_it != vd_edge_to_he_edge.end()) + { // Twin segment(s) have already been made + edge_t* source_twin = he_edge_it->second; + assert(source_twin); + auto end_node_it = vd_node_to_he_node.find(vd_edge.vertex1()); + assert(end_node_it != vd_node_to_he_node.end()); + node_t* end_node = end_node_it->second; + for (edge_t* twin = source_twin; ;twin = twin->prev->twin->prev) + { + if(!twin) + { + BOOST_LOG_TRIVIAL(warning) << "Encountered a voronoi edge without twin."; + continue; //Prevent reading unallocated memory. + } + assert(twin); + graph.edges.emplace_front(SkeletalTrapezoidationEdge()); + edge_t* edge = &graph.edges.front(); + edge->from = twin->to; + edge->to = twin->from; + edge->twin = twin; + twin->twin = edge; + edge->from->incident_edge = edge; + + if (prev_edge) + { + edge->prev = prev_edge; + prev_edge->next = edge; + } + + prev_edge = edge; + + if (prev_edge->to == end_node) + { + return; + } + + if (!twin->prev || !twin->prev->twin || !twin->prev->twin->prev) + { + BOOST_LOG_TRIVIAL(error) << "Discretized segment behaves oddly!"; + return; + } + + assert(twin->prev); // Forth rib + assert(twin->prev->twin); // Back rib + assert(twin->prev->twin->prev); // Prev segment along parabola + + constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped + graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + } + assert(prev_edge); + } + else + { + std::vector discretized = discretize(vd_edge, segments); + assert(discretized.size() >= 2); + if(discretized.size() < 2) + { + BOOST_LOG_TRIVIAL(warning) << "Discretized Voronoi edge is degenerate."; + } + + assert(!prev_edge || prev_edge->to); + if(prev_edge && !prev_edge->to) + { + BOOST_LOG_TRIVIAL(warning) << "Previous edge doesn't go anywhere."; + } + node_t* v0 = (prev_edge)? prev_edge->to : &makeNode(*vd_edge.vertex0(), from); // TODO: investigate whether boost:voronoi can produce multiple verts and violates consistency + Point p0 = discretized.front(); + for (size_t p1_idx = 1; p1_idx < discretized.size(); p1_idx++) + { + Point p1 = discretized[p1_idx]; + node_t* v1; + if (p1_idx < discretized.size() - 1) + { + graph.nodes.emplace_front(SkeletalTrapezoidationJoint(), p1); + v1 = &graph.nodes.front(); + } + else + { + v1 = &makeNode(*vd_edge.vertex1(), to); + } + + graph.edges.emplace_front(SkeletalTrapezoidationEdge()); + edge_t* edge = &graph.edges.front(); + edge->from = v0; + edge->to = v1; + edge->from->incident_edge = edge; + + if (prev_edge) + { + edge->prev = prev_edge; + prev_edge->next = edge; + } + + prev_edge = edge; + p0 = p1; + v0 = v1; + + if (p1_idx < discretized.size() - 1) + { // Rib for last segment gets introduced outside this function! + constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped + graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + } + } + assert(prev_edge); + vd_edge_to_he_edge.emplace(&vd_edge, prev_edge); + } +} + +std::vector SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_edge, const std::vector& segments) +{ + /*Terminology in this function assumes that the edge moves horizontally from + left to right. This is not necessarily the case; the edge can go in any + direction, but it helps to picture it in a certain direction in your head.*/ + + const vd_t::cell_type* left_cell = vd_edge.cell(); + const vd_t::cell_type* right_cell = vd_edge.twin()->cell(); + + assert(VoronoiUtils::p(vd_edge.vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge.vertex0()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge.vertex0()).y() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge.vertex0()).y() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge.vertex1()).x() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge.vertex1()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge.vertex1()).y() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge.vertex1()).y() >= std::numeric_limits::lowest()); + + Point start = VoronoiUtils::p(vd_edge.vertex0()).cast(); + Point end = VoronoiUtils::p(vd_edge.vertex1()).cast(); + + bool point_left = left_cell->contains_point(); + bool point_right = right_cell->contains_point(); + if ((!point_left && !point_right) || vd_edge.is_secondary()) // Source vert is directly connected to source segment + { + return std::vector({ start, end }); + } + else if (point_left != point_right) //This is a parabolic edge between a point and a line. + { + Point p = VoronoiUtils::getSourcePoint(*(point_left ? left_cell : right_cell), segments); + const Segment& s = VoronoiUtils::getSourceSegment(*(point_left ? right_cell : left_cell), segments); + return VoronoiUtils::discretizeParabola(p, s, start, end, discretization_step_size, transitioning_angle); + } + else //This is a straight edge between two points. + { + /*While the edge is straight, it is still discretized since the part + becomes narrower between the two points. As such it may need different + beadings along the way.*/ + Point left_point = VoronoiUtils::getSourcePoint(*left_cell, segments); + Point right_point = VoronoiUtils::getSourcePoint(*right_cell, segments); + coord_t d = (right_point - left_point).cast().norm(); + Point middle = (left_point + right_point) / 2; + Point x_axis_dir = Point(right_point - left_point).rotate_90_degree_ccw(); + coord_t x_axis_length = x_axis_dir.cast().norm(); + + const auto projected_x = [x_axis_dir, x_axis_length, middle](Point from) //Project a point on the edge. + { + Point vec = from - middle; + assert(( vec.cast().dot(x_axis_dir.cast())/ int64_t(x_axis_length)) <= std::numeric_limits::max()); + coord_t x = vec.cast().dot(x_axis_dir.cast()) / int64_t(x_axis_length); + return x; + }; + + coord_t start_x = projected_x(start); + coord_t end_x = projected_x(end); + + //Part of the edge will be bound to the markings on the endpoints of the edge. Calculate how far that is. + float bound = 0.5 / tan((M_PI - transitioning_angle) * 0.5); + int64_t marking_start_x = - int64_t(d) * bound; + int64_t marking_end_x = int64_t(d) * bound; + + assert((middle.cast() + x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).x() <= std::numeric_limits::max()); + assert((middle.cast() + x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).y() <= std::numeric_limits::max()); + assert((middle.cast() + x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).x() <= std::numeric_limits::max()); + assert((middle.cast() + x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).y() <= std::numeric_limits::max()); + Point marking_start = middle + (x_axis_dir.cast() * marking_start_x / int64_t(x_axis_length)).cast(); + Point marking_end = middle + (x_axis_dir.cast() * marking_end_x / int64_t(x_axis_length)).cast(); + int64_t direction = 1; + + if (start_x > end_x) //Oops, the Voronoi edge is the other way around. + { + direction = -1; + std::swap(marking_start, marking_end); + std::swap(marking_start_x, marking_end_x); + } + + //Start generating points along the edge. + Point a = start; + Point b = end; + std::vector ret; + ret.emplace_back(a); + + //Introduce an extra edge at the borders of the markings? + bool add_marking_start = marking_start_x * direction > int64_t(start_x) * direction; + bool add_marking_end = marking_end_x * direction > int64_t(start_x) * direction; + + //The edge's length may not be divisible by the step size, so calculate an integer step count and evenly distribute the vertices among those. + Point ab = b - a; + coord_t ab_size = ab.cast().norm(); + coord_t step_count = (ab_size + discretization_step_size / 2) / discretization_step_size; + if (step_count % 2 == 1) + { + step_count++; // enforce a discretization point being added in the middle + } + for (coord_t step = 1; step < step_count; step++) + { + Point here = a + (ab.cast() * int64_t(step) / int64_t(step_count)).cast(); //Now simply interpolate the coordinates to get the new vertices! + coord_t x_here = projected_x(here); //If we've surpassed the position of the extra markings, we may need to insert them first. + if (add_marking_start && marking_start_x * direction < int64_t(x_here) * direction) + { + ret.emplace_back(marking_start); + add_marking_start = false; + } + if (add_marking_end && marking_end_x * direction < int64_t(x_here) * direction) + { + ret.emplace_back(marking_end); + add_marking_end = false; + } + ret.emplace_back(here); + } + if (add_marking_end && marking_end_x * direction < int64_t(end_x) * direction) + { + ret.emplace_back(marking_end); + } + ret.emplace_back(b); + return ret; + } +} + + +bool SkeletalTrapezoidation::computePointCellRange(vd_t::cell_type& cell, Point& start_source_point, Point& end_source_point, vd_t::edge_type*& starting_vd_edge, vd_t::edge_type*& ending_vd_edge, const std::vector& segments) +{ + if (cell.incident_edge()->is_infinite()) + return false; //Infinite edges only occur outside of the polygon. Don't copy any part of this cell. + + // Check if any point of the cell is inside or outside polygon + // Copy whole cell into graph or not at all + + const Point source_point = VoronoiUtils::getSourcePoint(cell, segments); + const PolygonsPointIndex source_point_index = VoronoiUtils::getSourcePointIndex(cell, segments); + Vec2i64 some_point = VoronoiUtils::p(cell.incident_edge()->vertex0()); + if (some_point == source_point.cast()) + some_point = VoronoiUtils::p(cell.incident_edge()->vertex1()); + + //Test if the some_point is even inside the polygon. + //The edge leading out of a polygon must have an endpoint that's not in the corner following the contour of the polygon at that vertex. + //So if it's inside the corner formed by the polygon vertex, it's all fine. + //But if it's outside of the corner, it must be a vertex of the Voronoi diagram that goes outside of the polygon towards infinity. + if (!LinearAlg2D::isInsideCorner(source_point_index.prev().p(), source_point_index.p(), source_point_index.next().p(), some_point)) + return false; // Don't copy any part of this cell + + vd_t::edge_type* vd_edge = cell.incident_edge(); + do { + assert(vd_edge->is_finite()); + if (Vec2i64 p1 = VoronoiUtils::p(vd_edge->vertex1()); p1 == source_point.cast()) { + start_source_point = source_point; + end_source_point = source_point; + starting_vd_edge = vd_edge->next(); + ending_vd_edge = vd_edge; + } else { + assert((VoronoiUtils::p(vd_edge->vertex0()) == source_point.cast() || !vd_edge->is_secondary()) && "point cells must end in the point! They cannot cross the point with an edge, because collinear edges are not allowed in the input."); + } + } + while (vd_edge = vd_edge->next(), vd_edge != cell.incident_edge()); + assert(starting_vd_edge && ending_vd_edge); + assert(starting_vd_edge != ending_vd_edge); + return true; +} + +void SkeletalTrapezoidation::computeSegmentCellRange(vd_t::cell_type& cell, Point& start_source_point, Point& end_source_point, vd_t::edge_type*& starting_vd_edge, vd_t::edge_type*& ending_vd_edge, const std::vector& segments) +{ + const Segment &source_segment = VoronoiUtils::getSourceSegment(cell, segments); + const Point from = source_segment.from(); + const Point to = source_segment.to(); + + // Find starting edge + // Find end edge + bool seen_possible_start = false; + bool after_start = false; + bool ending_edge_is_set_before_start = false; + vd_t::edge_type* edge = cell.incident_edge(); + do { + if (edge->is_infinite()) + continue; + + Vec2i64 v0 = VoronoiUtils::p(edge->vertex0()); + Vec2i64 v1 = VoronoiUtils::p(edge->vertex1()); + + assert(!(v0 == to.cast() && v1 == from.cast() )); + if (v0 == to.cast() && !after_start) { // Use the last edge which starts in source_segment.to + starting_vd_edge = edge; + seen_possible_start = true; + } + else if (seen_possible_start) { + after_start = true; + } + + if (v1 == from.cast() && (!ending_vd_edge || ending_edge_is_set_before_start)) { + ending_edge_is_set_before_start = !after_start; + ending_vd_edge = edge; + } + } while (edge = edge->next(), edge != cell.incident_edge()); + + assert(starting_vd_edge && ending_vd_edge); + assert(starting_vd_edge != ending_vd_edge); + + start_source_point = source_segment.to(); + end_source_point = source_segment.from(); +} + +SkeletalTrapezoidation::SkeletalTrapezoidation(const Polygons& polys, const BeadingStrategy& beading_strategy, + double transitioning_angle, coord_t discretization_step_size, + coord_t transition_filter_dist, coord_t beading_propagation_transition_dist + ): transitioning_angle(transitioning_angle), + discretization_step_size(discretization_step_size), + transition_filter_dist(transition_filter_dist), + beading_propagation_transition_dist(beading_propagation_transition_dist), + beading_strategy(beading_strategy) +{ + constructFromPolygons(polys); +} + +void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) +{ + // Check self intersections. + assert([&polys]() -> bool { + EdgeGrid::Grid grid; + grid.set_bbox(get_extents(polys)); + grid.create(polys, scaled(10.)); + return !grid.has_intersecting_edges(); + }()); + + vd_edge_to_he_edge.clear(); + vd_node_to_he_node.clear(); + + std::vector segments; + for (size_t poly_idx = 0; poly_idx < polys.size(); poly_idx++) + for (size_t point_idx = 0; point_idx < polys[poly_idx].size(); point_idx++) + segments.emplace_back(&polys, poly_idx, point_idx); + + Geometry::VoronoiDiagram voronoi_diagram; + construct_voronoi(segments.begin(), segments.end(), &voronoi_diagram); + + for (vd_t::cell_type cell : voronoi_diagram.cells()) + { + if (!cell.incident_edge()) + continue; // There is no spoon + + Point start_source_point; + Point end_source_point; + vd_t::edge_type* starting_vonoroi_edge = nullptr; + vd_t::edge_type* ending_vonoroi_edge = nullptr; + // Compute and store result in above variables + + if (cell.contains_point()) { + const bool keep_going = computePointCellRange(cell, start_source_point, end_source_point, starting_vonoroi_edge, ending_vonoroi_edge, segments); + if (!keep_going) + continue; + } else { + assert(cell.contains_segment()); + computeSegmentCellRange(cell, start_source_point, end_source_point, starting_vonoroi_edge, ending_vonoroi_edge, segments); + } + + if (!starting_vonoroi_edge || !ending_vonoroi_edge) { + assert(false && "Each cell should start / end in a polygon vertex"); + continue; + } + + // Copy start to end edge to graph + edge_t* prev_edge = nullptr; + assert(VoronoiUtils::p(starting_vonoroi_edge->vertex1()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex1()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(starting_vonoroi_edge->vertex1()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex1()).y() >= std::numeric_limits::lowest()); + transferEdge(start_source_point, VoronoiUtils::p(starting_vonoroi_edge->vertex1()).cast(), *starting_vonoroi_edge, prev_edge, start_source_point, end_source_point, segments); + node_t* starting_node = vd_node_to_he_node[starting_vonoroi_edge->vertex0()]; + starting_node->data.distance_to_boundary = 0; + + constexpr bool is_next_to_start_or_end = true; + graph.makeRib(prev_edge, start_source_point, end_source_point, is_next_to_start_or_end); + for (vd_t::edge_type* vd_edge = starting_vonoroi_edge->next(); vd_edge != ending_vonoroi_edge; vd_edge = vd_edge->next()) { + assert(vd_edge->is_finite()); + + assert(VoronoiUtils::p(vd_edge->vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge->vertex0()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge->vertex0()).y() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge->vertex0()).y() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge->vertex1()).x() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge->vertex1()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(vd_edge->vertex1()).y() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge->vertex1()).y() >= std::numeric_limits::lowest()); + + Point v1 = VoronoiUtils::p(vd_edge->vertex0()).cast(); + Point v2 = VoronoiUtils::p(vd_edge->vertex1()).cast(); + transferEdge(v1, v2, *vd_edge, prev_edge, start_source_point, end_source_point, segments); + + graph.makeRib(prev_edge, start_source_point, end_source_point, vd_edge->next() == ending_vonoroi_edge); + } + + assert(VoronoiUtils::p(starting_vonoroi_edge->vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex0()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(starting_vonoroi_edge->vertex0()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex0()).y() >= std::numeric_limits::lowest()); + transferEdge(VoronoiUtils::p(ending_vonoroi_edge->vertex0()).cast(), end_source_point, *ending_vonoroi_edge, prev_edge, start_source_point, end_source_point, segments); + prev_edge->to->data.distance_to_boundary = 0; + } + + separatePointyQuadEndNodes(); + + graph.fixNodeDuplication(); + graph.collapseSmallEdges(); + + // Set [incident_edge] the the first possible edge that way we can iterate over all reachable edges from node.incident_edge, + // without needing to iterate backward + for (edge_t& edge : graph.edges) + if (!edge.prev) + edge.from->incident_edge = &edge; +} + +void SkeletalTrapezoidation::separatePointyQuadEndNodes() +{ + std::unordered_set visited_nodes; + for (edge_t& edge : graph.edges) + { + if (edge.prev) + { + continue; + } + edge_t* quad_start = &edge; + if (visited_nodes.find(quad_start->from) == visited_nodes.end()) + { + visited_nodes.emplace(quad_start->from); + } + else + { // Needs to be duplicated + graph.nodes.emplace_back(*quad_start->from); + node_t* new_node = &graph.nodes.back(); + new_node->incident_edge = quad_start; + quad_start->from = new_node; + quad_start->twin->to = new_node; + } + } +} + +// +// ^^^^^^^^^^^^^^^^^^^^^ +// INITIALIZATION +// ===================== +// +// ===================== +// TRANSTISIONING +// vvvvvvvvvvvvvvvvvvvvv +// + +#if 0 +static void export_graph_to_svg(const std::string &path, const SkeletalTrapezoidationGraph &graph, const Polygons &polys) +{ + const std::vector colors = {"blue", "cyan", "red", "orange", "magenta", "pink", "purple", "green", "yellow"}; + coordf_t stroke_width = scale_(0.05); + BoundingBox bbox; + for (const auto &node : graph.nodes) + bbox.merge(node.p); + + bbox.offset(scale_(1.)); + ::Slic3r::SVG svg(path.c_str(), bbox); + for (const auto &line : to_lines(polys)) + svg.draw(line, "red", stroke_width); + + for (const auto &edge : graph.edges) + svg.draw(Line(edge.from->p, edge.to->p), "cyan", scale_(0.01)); +} +#endif + +void SkeletalTrapezoidation::generateToolpaths(VariableWidthPaths& generated_toolpaths, bool filter_outermost_central_edges) +{ + p_generated_toolpaths = &generated_toolpaths; + + updateIsCentral(); + + filterCentral(central_filter_dist); + + if (filter_outermost_central_edges) + filterOuterCentral(); + + updateBeadCount(); + + filterNoncentralRegions(); + + generateTransitioningRibs(); + + generateExtraRibs(); + + markRegions(); + + generateSegments(); + + liftRegionInfoToLines(); +} + +void SkeletalTrapezoidation::updateIsCentral() +{ + // _.-'^` A and B are the endpoints of an edge we're checking. + // _.-'^` Part of the line AB will be used as a cap, + // _.-'^` \ because the polygon is too narrow there. + // _.-'^` \ If |AB| minus the cap is still bigger than dR, + // _.-'^` \ R2 the edge AB is considered central. It's then + // _.-'^` \ _.-'\`\ significant compared to the edges around it. + // _.-'^` \R1 _.-'^` '`\ dR + // _.-'^`a/2 \_.-'^`a \ Line AR2 is parallel to the polygon contour. + // `^'-._````````````````A```````````v````````B``````` dR is the remaining diameter at B. + // `^'-._ dD = |AB| As a result, AB is less often central if the polygon + // `^'-._ corner is obtuse. + // sin a = dR / dD + + coord_t outer_edge_filter_length = beading_strategy.getTransitionThickness(0) / 2; + + float cap = sin(beading_strategy.getTransitioningAngle() * 0.5); // = cos(bisector_angle / 2) + for (edge_t& edge: graph.edges) + { + assert(edge.twin); + if(!edge.twin) + { + BOOST_LOG_TRIVIAL(warning) << "Encountered a Voronoi edge without twin!"; + continue; + } + if(edge.twin->data.centralIsSet()) + { + edge.data.setIsCentral(edge.twin->data.isCentral()); + } + else if(edge.data.type == SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD) + { + edge.data.setIsCentral(false); + } + else if(std::max(edge.from->data.distance_to_boundary, edge.to->data.distance_to_boundary) < outer_edge_filter_length) + { + edge.data.setIsCentral(false); + } + else + { + Point a = edge.from->p; + Point b = edge.to->p; + Point ab = b - a; + coord_t dR = std::abs(edge.to->data.distance_to_boundary - edge.from->data.distance_to_boundary); + coord_t dD = ab.cast().norm(); + edge.data.setIsCentral(dR < dD * cap); + } + } +} + +void SkeletalTrapezoidation::filterCentral(coord_t max_length) +{ + for (edge_t& edge : graph.edges) + { + if (isEndOfCentral(edge) && edge.to->isLocalMaximum() && !edge.to->isLocalMaximum()) + { + filterCentral(edge.twin, 0, max_length); + } + } +} + +bool SkeletalTrapezoidation::filterCentral(edge_t* starting_edge, coord_t traveled_dist, coord_t max_length) +{ + coord_t length = (starting_edge->from->p - starting_edge->to->p).cast().norm(); + if (traveled_dist + length > max_length) + { + return false; + } + + bool should_dissolve = true; //Should we unmark this as central and propagate that? + for (edge_t* next_edge = starting_edge->next; next_edge && next_edge != starting_edge->twin; next_edge = next_edge->twin->next) + { + if (next_edge->data.isCentral()) + { + should_dissolve &= filterCentral(next_edge, traveled_dist + length, max_length); + } + } + + should_dissolve &= !starting_edge->to->isLocalMaximum(); // Don't filter central regions with a local maximum! + if (should_dissolve) + { + starting_edge->data.setIsCentral(false); + starting_edge->twin->data.setIsCentral(false); + } + return should_dissolve; +} + +void SkeletalTrapezoidation::filterOuterCentral() +{ + for (edge_t& edge : graph.edges) + { + if (!edge.prev) + { + edge.data.setIsCentral(false); + edge.twin->data.setIsCentral(false); + } + } +} + +void SkeletalTrapezoidation::updateBeadCount() +{ + for (edge_t& edge : graph.edges) + { + if (edge.data.isCentral()) + { + edge.to->data.bead_count = beading_strategy.getOptimalBeadCount(edge.to->data.distance_to_boundary * 2); + } + } + + // Fix bead count at locally maximal R, also for central regions!! See TODO s in generateTransitionEnd(.) + for (node_t& node : graph.nodes) + { + if (node.isLocalMaximum()) + { + if (node.data.distance_to_boundary < 0) + { + BOOST_LOG_TRIVIAL(warning) << "Distance to boundary not yet computed for local maximum!"; + node.data.distance_to_boundary = std::numeric_limits::max(); + edge_t* edge = node.incident_edge; + do + { + node.data.distance_to_boundary = std::min(node.data.distance_to_boundary, edge->to->data.distance_to_boundary + coord_t((edge->from->p - edge->to->p).cast().norm())); + } while (edge = edge->twin->next, edge != node.incident_edge); + } + coord_t bead_count = beading_strategy.getOptimalBeadCount(node.data.distance_to_boundary * 2); + node.data.bead_count = bead_count; + } + } +} + +void SkeletalTrapezoidation::filterNoncentralRegions() +{ + for (edge_t& edge : graph.edges) + { + if (!isEndOfCentral(edge)) + { + continue; + } + if(edge.to->data.bead_count < 0 && edge.to->data.distance_to_boundary != 0) + { + BOOST_LOG_TRIVIAL(warning) << "Encountered an uninitialized bead at the boundary!"; + } + assert(edge.to->data.bead_count >= 0 || edge.to->data.distance_to_boundary == 0); + constexpr coord_t max_dist = scaled(0.4); + filterNoncentralRegions(&edge, edge.to->data.bead_count, 0, max_dist); + } +} + +bool SkeletalTrapezoidation::filterNoncentralRegions(edge_t* to_edge, coord_t bead_count, coord_t traveled_dist, coord_t max_dist) +{ + coord_t r = to_edge->to->data.distance_to_boundary; + + edge_t* next_edge = to_edge->next; + for (; next_edge && next_edge != to_edge->twin; next_edge = next_edge->twin->next) + { + if (next_edge->to->data.distance_to_boundary >= r || shorter_then(next_edge->to->p - next_edge->from->p, scaled(0.01))) + { + break; // Only walk upward + } + } + if (next_edge == to_edge->twin || ! next_edge) + { + return false; + } + + const coord_t length = (next_edge->to->p - next_edge->from->p).cast().norm(); + + bool dissolve = false; + if (next_edge->to->data.bead_count == bead_count) + { + dissolve = true; + } + else if (next_edge->to->data.bead_count < 0) + { + dissolve = filterNoncentralRegions(next_edge, bead_count, traveled_dist + length, max_dist); + } + else // Upward bead count is different + { + // Dissolve if two central regions with different bead count are closer together than the max_dist (= transition distance) + dissolve = (traveled_dist + length < max_dist) && std::abs(next_edge->to->data.bead_count - bead_count) == 1; + } + + if (dissolve) + { + next_edge->data.setIsCentral(true); + next_edge->twin->data.setIsCentral(true); + next_edge->to->data.bead_count = beading_strategy.getOptimalBeadCount(next_edge->to->data.distance_to_boundary * 2); + next_edge->to->data.transition_ratio = 0; + } + return dissolve; // Dissolving only depend on the one edge going upward. There cannot be multiple edges going upward. +} + +void SkeletalTrapezoidation::generateTransitioningRibs() +{ + // Store the upward edges to the transitions. + // We only store the halfedge for which the distance_to_boundary is higher at the end than at the beginning. + ptr_vector_t> edge_transitions; + generateTransitionMids(edge_transitions); + + for (edge_t& edge : graph.edges) + { // Check if there is a transition in between nodes with different bead counts + if (edge.data.isCentral() && edge.from->data.bead_count != edge.to->data.bead_count) + { + assert(edge.data.hasTransitions() || edge.twin->data.hasTransitions()); + } + } + + filterTransitionMids(); + + ptr_vector_t> edge_transition_ends; // We only map the half edge in the upward direction. mapped items are not sorted + generateAllTransitionEnds(edge_transition_ends); + + applyTransitions(edge_transition_ends); + // Note that the shared pointer lists will be out of scope and thus destroyed here, since the remaining refs are weak_ptr. +} + + +void SkeletalTrapezoidation::generateTransitionMids(ptr_vector_t>& edge_transitions) +{ + for (edge_t& edge : graph.edges) + { + assert(edge.data.centralIsSet()); + if (!edge.data.isCentral()) + { // Only central regions introduce transitions + continue; + } + coord_t start_R = edge.from->data.distance_to_boundary; + coord_t end_R = edge.to->data.distance_to_boundary; + int start_bead_count = edge.from->data.bead_count; + int end_bead_count = edge.to->data.bead_count; + + if (start_R == end_R) + { // No transitions occur when both end points have the same distance_to_boundary + assert(edge.from->data.bead_count == edge.to->data.bead_count); + if(edge.from->data.bead_count != edge.to->data.bead_count) + { + BOOST_LOG_TRIVIAL(warning) << "Bead count " << edge.from->data.bead_count << " is different from " << edge.to->data.bead_count << " even though distance to boundary is the same."; + } + continue; + } + else if (start_R > end_R) + { // Only consider those half-edges which are going from a lower to a higher distance_to_boundary + continue; + } + + if (edge.from->data.bead_count == edge.to->data.bead_count) + { // No transitions should occur according to the enforced bead counts + continue; + } + + if (start_bead_count > beading_strategy.getOptimalBeadCount(start_R * 2) + || end_bead_count > beading_strategy.getOptimalBeadCount(end_R * 2)) + { // Wasn't the case earlier in this function because of already introduced transitions + BOOST_LOG_TRIVIAL(error) << "transitioning segment overlap! (?)"; + } + assert(start_R < end_R); + if(start_R >= end_R) + { + BOOST_LOG_TRIVIAL(warning) << "Transitioning the wrong way around! This function expects to transition from small R to big R, but was transitioning from " << start_R << " to " << end_R; + } + coord_t edge_size = (edge.from->p - edge.to->p).cast().norm(); + for (int transition_lower_bead_count = start_bead_count; transition_lower_bead_count < end_bead_count; transition_lower_bead_count++) + { + coord_t mid_R = beading_strategy.getTransitionThickness(transition_lower_bead_count) / 2; + if (mid_R > end_R) + { + BOOST_LOG_TRIVIAL(error) << "transition on segment lies outside of segment!"; + mid_R = end_R; + } + if (mid_R < start_R) + { + BOOST_LOG_TRIVIAL(error) << "transition on segment lies outside of segment!"; + mid_R = start_R; + } + coord_t mid_pos = int64_t(edge_size) * int64_t(mid_R - start_R) / int64_t(end_R - start_R); + + assert(mid_pos >= 0); + assert(mid_pos <= edge_size); + if(mid_pos < 0 || mid_pos > edge_size) + { + BOOST_LOG_TRIVIAL(warning) << "Transition mid is out of bounds of the edge."; + } + auto transitions = edge.data.getTransitions(); + constexpr bool ignore_empty = true; + assert((! edge.data.hasTransitions(ignore_empty)) || mid_pos >= transitions->back().pos); + if (! edge.data.hasTransitions(ignore_empty)) + { + edge_transitions.emplace_back(std::make_shared>()); + edge.data.setTransitions(edge_transitions.back()); // initialization + transitions = edge.data.getTransitions(); + } + transitions->emplace_back(mid_pos, transition_lower_bead_count); + } + assert((edge.from->data.bead_count == edge.to->data.bead_count) || edge.data.hasTransitions()); + } +} + +void SkeletalTrapezoidation::filterTransitionMids() +{ + for (edge_t& edge : graph.edges) + { + if (! edge.data.hasTransitions()) + { + continue; + } + auto& transitions = *edge.data.getTransitions(); + + // This is how stuff should be stored in transitions + assert(transitions.front().lower_bead_count <= transitions.back().lower_bead_count); + assert(edge.from->data.distance_to_boundary <= edge.to->data.distance_to_boundary); + + const Point a = edge.from->p; + const Point b = edge.to->p; + Point ab = b - a; + coord_t ab_size = ab.cast().norm(); + + bool going_up = true; + std::list to_be_dissolved_back = dissolveNearbyTransitions(&edge, transitions.back(), ab_size - transitions.back().pos, transition_filter_dist, going_up); + bool should_dissolve_back = !to_be_dissolved_back.empty(); + for (TransitionMidRef& ref : to_be_dissolved_back) + { + dissolveBeadCountRegion(&edge, transitions.back().lower_bead_count + 1, transitions.back().lower_bead_count); + ref.edge->data.getTransitions()->erase(ref.transition_it); + } + + { + coord_t trans_bead_count = transitions.back().lower_bead_count; + coord_t upper_transition_half_length = (1.0 - beading_strategy.getTransitionAnchorPos(trans_bead_count)) * beading_strategy.getTransitioningLength(trans_bead_count); + should_dissolve_back |= filterEndOfCentralTransition(&edge, ab_size - transitions.back().pos, upper_transition_half_length, trans_bead_count); + } + + if (should_dissolve_back) + { + transitions.pop_back(); + } + if (transitions.empty()) + { // FilterEndOfCentralTransition gives inconsistent new bead count when executing for the same transition in two directions. + continue; + } + + going_up = false; + std::list to_be_dissolved_front = dissolveNearbyTransitions(edge.twin, transitions.front(), transitions.front().pos, transition_filter_dist, going_up); + bool should_dissolve_front = !to_be_dissolved_front.empty(); + for (TransitionMidRef& ref : to_be_dissolved_front) + { + dissolveBeadCountRegion(edge.twin, transitions.front().lower_bead_count, transitions.front().lower_bead_count + 1); + ref.edge->data.getTransitions()->erase(ref.transition_it); + } + + { + coord_t trans_bead_count = transitions.front().lower_bead_count; + coord_t lower_transition_half_length = beading_strategy.getTransitionAnchorPos(trans_bead_count) * beading_strategy.getTransitioningLength(trans_bead_count); + should_dissolve_front |= filterEndOfCentralTransition(edge.twin, transitions.front().pos, lower_transition_half_length, trans_bead_count + 1); + } + + if (should_dissolve_front) + { + transitions.pop_front(); + } + if (transitions.empty()) + { // FilterEndOfCentralTransition gives inconsistent new bead count when executing for the same transition in two directions. + continue; + } + } +} + +std::list SkeletalTrapezoidation::dissolveNearbyTransitions(edge_t* edge_to_start, TransitionMiddle& origin_transition, coord_t traveled_dist, coord_t max_dist, bool going_up) +{ + std::list to_be_dissolved; + if (traveled_dist > max_dist) + { + return to_be_dissolved; + } + bool should_dissolve = true; + for (edge_t* edge = edge_to_start->next; edge && edge != edge_to_start->twin; edge = edge->twin->next) + { + if (!edge->data.isCentral()) + { + continue; + } + + Point a = edge->from->p; + Point b = edge->to->p; + Point ab = b - a; + coord_t ab_size = ab.cast().norm(); + bool is_aligned = edge->isUpward(); + edge_t* aligned_edge = is_aligned? edge : edge->twin; + bool seen_transition_on_this_edge = false; + + if (aligned_edge->data.hasTransitions()) + { + auto& transitions = *aligned_edge->data.getTransitions(); + for (auto transition_it = transitions.begin(); transition_it != transitions.end(); ++ transition_it) + { // Note: this is not necessarily iterating in the traveling direction! + // Check whether we should dissolve + coord_t pos = is_aligned? transition_it->pos : ab_size - transition_it->pos; + if (traveled_dist + pos < max_dist + && transition_it->lower_bead_count == origin_transition.lower_bead_count) // Only dissolve local optima + { + if (traveled_dist + pos < beading_strategy.getTransitioningLength(transition_it->lower_bead_count)) + { + // Consecutive transitions both in/decreasing in bead count should never be closer together than the transition distance + assert(going_up != is_aligned || transition_it->lower_bead_count == 0); + } + to_be_dissolved.emplace_back(aligned_edge, transition_it); + seen_transition_on_this_edge = true; + } + } + } + if (!seen_transition_on_this_edge) + { + std::list to_be_dissolved_here = dissolveNearbyTransitions(edge, origin_transition, traveled_dist + ab_size, max_dist, going_up); + if (to_be_dissolved_here.empty()) + { // The region is too long to be dissolved in this direction, so it cannot be dissolved in any direction. + to_be_dissolved.clear(); + return to_be_dissolved; + } + to_be_dissolved.splice(to_be_dissolved.end(), to_be_dissolved_here); // Transfer to_be_dissolved_here into to_be_dissolved + should_dissolve = should_dissolve && !to_be_dissolved.empty(); + } + } + + if (!should_dissolve) + { + to_be_dissolved.clear(); + } + + return to_be_dissolved; +} + + +void SkeletalTrapezoidation::dissolveBeadCountRegion(edge_t* edge_to_start, coord_t from_bead_count, coord_t to_bead_count) +{ + assert(from_bead_count != to_bead_count); + if (edge_to_start->to->data.bead_count != from_bead_count) + { + return; + } + + edge_to_start->to->data.bead_count = to_bead_count; + for (edge_t* edge = edge_to_start->next; edge && edge != edge_to_start->twin; edge = edge->twin->next) + { + if (!edge->data.isCentral()) + { + continue; + } + dissolveBeadCountRegion(edge, from_bead_count, to_bead_count); + } +} + +bool SkeletalTrapezoidation::filterEndOfCentralTransition(edge_t* edge_to_start, coord_t traveled_dist, coord_t max_dist, coord_t replacing_bead_count) +{ + if (traveled_dist > max_dist) + { + return false; + } + + bool is_end_of_central = true; + bool should_dissolve = false; + for (edge_t* next_edge = edge_to_start->next; next_edge && next_edge != edge_to_start->twin; next_edge = next_edge->twin->next) + { + if (next_edge->data.isCentral()) + { + coord_t length = (next_edge->to->p - next_edge->from->p).cast().norm(); + should_dissolve |= filterEndOfCentralTransition(next_edge, traveled_dist + length, max_dist, replacing_bead_count); + is_end_of_central = false; + } + } + if (is_end_of_central && traveled_dist < max_dist) + { + should_dissolve = true; + } + + if (should_dissolve) + { + edge_to_start->to->data.bead_count = replacing_bead_count; + } + return should_dissolve; +} + +void SkeletalTrapezoidation::generateAllTransitionEnds(ptr_vector_t>& edge_transition_ends) +{ + for (edge_t& edge : graph.edges) + { + if (! edge.data.hasTransitions()) + { + continue; + } + auto& transition_positions = *edge.data.getTransitions(); + + assert(edge.from->data.distance_to_boundary <= edge.to->data.distance_to_boundary); + for (TransitionMiddle& transition_middle : transition_positions) + { + assert(transition_positions.front().pos <= transition_middle.pos); + assert(transition_middle.pos <= transition_positions.back().pos); + generateTransitionEnds(edge, transition_middle.pos, transition_middle.lower_bead_count, edge_transition_ends); + } + } +} + +void SkeletalTrapezoidation::generateTransitionEnds(edge_t& edge, coord_t mid_pos, coord_t lower_bead_count, ptr_vector_t>& edge_transition_ends) +{ + const Point a = edge.from->p; + const Point b = edge.to->p; + const Point ab = b - a; + const coord_t ab_size = ab.cast().norm(); + + const coord_t transition_length = beading_strategy.getTransitioningLength(lower_bead_count); + const float transition_mid_position = beading_strategy.getTransitionAnchorPos(lower_bead_count); + constexpr float inner_bead_width_ratio_after_transition = 1.0; + + constexpr coord_t start_rest = 0; + const float mid_rest = transition_mid_position * inner_bead_width_ratio_after_transition; + constexpr float end_rest = inner_bead_width_ratio_after_transition; + + { // Lower bead count transition end + const coord_t start_pos = ab_size - mid_pos; + const coord_t transition_half_length = transition_mid_position * int64_t(transition_length); + const coord_t end_pos = start_pos + transition_half_length; + generateTransitionEnd(*edge.twin, start_pos, end_pos, transition_half_length, mid_rest, start_rest, lower_bead_count, edge_transition_ends); + } + + { // Upper bead count transition end + const coord_t start_pos = mid_pos; + const coord_t transition_half_length = (1.0 - transition_mid_position) * transition_length; + const coord_t end_pos = mid_pos + transition_half_length; +#ifdef DEBUG + if (! generateTransitionEnd(edge, start_pos, end_pos, transition_half_length, mid_rest, end_rest, lower_bead_count, edge_transition_ends)) + { + BOOST_LOG_TRIVIAL(warning) << "There must have been at least one direction in which the bead count is increasing enough for the transition to happen!"; + } +#else + generateTransitionEnd(edge, start_pos, end_pos, transition_half_length, mid_rest, end_rest, lower_bead_count, edge_transition_ends); +#endif + } +} + +bool SkeletalTrapezoidation::generateTransitionEnd(edge_t& edge, coord_t start_pos, coord_t end_pos, coord_t transition_half_length, double start_rest, double end_rest, coord_t lower_bead_count, ptr_vector_t>& edge_transition_ends) +{ + Point a = edge.from->p; + Point b = edge.to->p; + Point ab = b - a; + coord_t ab_size = ab.cast().norm(); // TODO: prevent recalculation of these values + + assert(start_pos <= ab_size); + if(start_pos > ab_size) + { + BOOST_LOG_TRIVIAL(warning) << "Start position of edge is beyond edge range."; + } + + bool going_up = end_rest > start_rest; + + assert(edge.data.isCentral()); + if (!edge.data.isCentral()) + { + BOOST_LOG_TRIVIAL(warning) << "This function shouldn't generate ends in or beyond non-central regions."; + return false; + } + + if (end_pos > ab_size) + { // Recurse on all further edges + float rest = end_rest - (start_rest - end_rest) * (end_pos - ab_size) / (start_pos - end_pos); + assert(rest >= 0); + assert(rest <= std::max(end_rest, start_rest)); + assert(rest >= std::min(end_rest, start_rest)); + + coord_t central_edge_count = 0; + for (edge_t* outgoing = edge.next; outgoing && outgoing != edge.twin; outgoing = outgoing->twin->next) + { + if (!outgoing->data.isCentral()) continue; + central_edge_count++; + } + + bool is_only_going_down = true; + bool has_recursed = false; + for (edge_t* outgoing = edge.next; outgoing && outgoing != edge.twin;) + { + edge_t* next = outgoing->twin->next; // Before we change the outgoing edge itself + if (!outgoing->data.isCentral()) + { + outgoing = next; + continue; // Don't put transition ends in non-central regions + } + if (central_edge_count > 1 && going_up && isGoingDown(outgoing, 0, end_pos - ab_size + transition_half_length, lower_bead_count)) + { // We're after a 3-way_all-central_junction-node and going in the direction of lower bead count + // don't introduce a transition end along this central direction, because this direction is the downward direction + // while we are supposed to be [going_up] + outgoing = next; + continue; + } + bool is_going_down = generateTransitionEnd(*outgoing, 0, end_pos - ab_size, transition_half_length, rest, end_rest, lower_bead_count, edge_transition_ends); + is_only_going_down &= is_going_down; + outgoing = next; + has_recursed = true; + } + if (!going_up || (has_recursed && !is_only_going_down)) + { + edge.to->data.transition_ratio = rest; + edge.to->data.bead_count = lower_bead_count; + } + return is_only_going_down; + } + else // end_pos < ab_size + { // Add transition end point here + bool is_lower_end = end_rest == 0; // TODO collapse this parameter into the bool for which it is used here! + coord_t pos = -1; + + edge_t* upward_edge = nullptr; + if (edge.isUpward()) + { + upward_edge = &edge; + pos = end_pos; + } + else + { + upward_edge = edge.twin; + pos = ab_size - end_pos; + } + + if(!upward_edge->data.hasTransitionEnds()) + { + //This edge doesn't have a data structure yet for the transition ends. Make one. + edge_transition_ends.emplace_back(std::make_shared>()); + upward_edge->data.setTransitionEnds(edge_transition_ends.back()); + } + auto transitions = upward_edge->data.getTransitionEnds(); + + //Add a transition to it (on the correct side). + assert(ab_size == (edge.twin->from->p - edge.twin->to->p).cast().norm()); + assert(pos <= ab_size); + if (transitions->empty() || pos < transitions->front().pos) + { // Preorder so that sorting later on is faster + transitions->emplace_front(pos, lower_bead_count, is_lower_end); + } + else + { + transitions->emplace_back(pos, lower_bead_count, is_lower_end); + } + return false; + } +} + + +bool SkeletalTrapezoidation::isGoingDown(edge_t* outgoing, coord_t traveled_dist, coord_t max_dist, coord_t lower_bead_count) const +{ + // NOTE: the logic below is not fully thought through. + // TODO: take transition mids into account + if (outgoing->to->data.distance_to_boundary == 0) + { + return true; + } + bool is_upward = outgoing->to->data.distance_to_boundary >= outgoing->from->data.distance_to_boundary; + edge_t* upward_edge = is_upward? outgoing : outgoing->twin; + if (outgoing->to->data.bead_count > lower_bead_count + 1) + { + assert(upward_edge->data.hasTransitions() && "If the bead count is going down there has to be a transition mid!"); + if(!upward_edge->data.hasTransitions()) + { + BOOST_LOG_TRIVIAL(warning) << "If the bead count is going down there has to be a transition mid!"; + } + return false; + } + coord_t length = (outgoing->to->p - outgoing->from->p).cast().norm(); + if (upward_edge->data.hasTransitions()) + { + auto& transition_mids = *upward_edge->data.getTransitions(); + TransitionMiddle& mid = is_upward? transition_mids.front() : transition_mids.back(); + if ( + mid.lower_bead_count == lower_bead_count && + ((is_upward && mid.pos + traveled_dist < max_dist) + || (!is_upward && length - mid.pos + traveled_dist < max_dist)) + ) + { + return true; + } + } + if (traveled_dist + length > max_dist) + { + return false; + } + if (outgoing->to->data.bead_count <= lower_bead_count + && !(outgoing->to->data.bead_count == lower_bead_count && outgoing->to->data.transition_ratio > 0.0)) + { + return true; + } + + bool is_only_going_down = true; + bool has_recursed = false; + for (edge_t* next = outgoing->next; next && next != outgoing->twin; next = next->twin->next) + { + if (!next->data.isCentral()) + { + continue; + } + bool is_going_down = isGoingDown(next, traveled_dist + length, max_dist, lower_bead_count); + is_only_going_down &= is_going_down; + has_recursed = true; + } + return has_recursed && is_only_going_down; +} + +static inline Point normal(const Point& p0, coord_t len) +{ + int64_t _len = p0.cast().norm(); + if (_len < 1) + return Point(len, 0); + return (p0.cast() * int64_t(len) / _len).cast(); +}; + +void SkeletalTrapezoidation::applyTransitions(ptr_vector_t>& edge_transition_ends) +{ + for (edge_t& edge : graph.edges) + { + if (edge.twin->data.hasTransitionEnds()) + { + coord_t length = (edge.from->p - edge.to->p).cast().norm(); + auto& twin_transition_ends = *edge.twin->data.getTransitionEnds(); + if (! edge.data.hasTransitionEnds()) + { + edge_transition_ends.emplace_back(std::make_shared>()); + edge.data.setTransitionEnds(edge_transition_ends.back()); + } + auto& transition_ends = *edge.data.getTransitionEnds(); + for (TransitionEnd& end : twin_transition_ends) + { + transition_ends.emplace_back(length - end.pos, end.lower_bead_count, end.is_lower_end); + } + twin_transition_ends.clear(); + } + } + + for (edge_t& edge : graph.edges) + { + if (! edge.data.hasTransitionEnds()) + { + continue; + } + + assert(edge.data.isCentral()); + + auto& transitions = *edge.data.getTransitionEnds(); + transitions.sort([](const TransitionEnd& a, const TransitionEnd& b) { return a.pos < b.pos; } ); + + node_t* from = edge.from; + node_t* to = edge.to; + Point a = from->p; + Point b = to->p; + Point ab = b - a; + coord_t ab_size = (ab).cast().norm(); + + edge_t* last_edge_replacing_input = &edge; + for (TransitionEnd& transition_end : transitions) + { + coord_t new_node_bead_count = transition_end.is_lower_end? transition_end.lower_bead_count : transition_end.lower_bead_count + 1; + coord_t end_pos = transition_end.pos; + node_t* close_node = (end_pos < ab_size / 2)? from : to; + if ((end_pos < snap_dist || end_pos > ab_size - snap_dist) + && close_node->data.bead_count == new_node_bead_count + ) + { + assert(end_pos <= ab_size); + close_node->data.transition_ratio = 0; + continue; + } + Point mid = a + normal(ab, end_pos); + + assert(last_edge_replacing_input->data.isCentral()); + assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); + last_edge_replacing_input = graph.insertNode(last_edge_replacing_input, mid, new_node_bead_count); + assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); + assert(last_edge_replacing_input->data.isCentral()); + } + } +} + +bool SkeletalTrapezoidation::isEndOfCentral(const edge_t& edge_to) const +{ + if (!edge_to.data.isCentral()) + { + return false; + } + if (!edge_to.next) + { + return true; + } + for (const edge_t* edge = edge_to.next; edge && edge != edge_to.twin; edge = edge->twin->next) + { + if (edge->data.isCentral()) + { + return false; + } + assert(edge->twin); + } + return true; +} + +void SkeletalTrapezoidation::generateExtraRibs() +{ + auto end_edge_it = --graph.edges.end(); // Don't check newly introduced edges + for (auto edge_it = graph.edges.begin(); std::prev(edge_it) != end_edge_it; ++edge_it) + { + edge_t& edge = *edge_it; + + if (!edge.data.isCentral() + || shorter_then(edge.to->p - edge.from->p, discretization_step_size) + || edge.from->data.distance_to_boundary >= edge.to->data.distance_to_boundary) + { + continue; + } + + + std::vector rib_thicknesses = beading_strategy.getNonlinearThicknesses(edge.from->data.bead_count); + + if (rib_thicknesses.empty()) continue; + + // Preload some variables before [edge] gets changed + node_t* from = edge.from; + node_t* to = edge.to; + Point a = from->p; + Point b = to->p; + Point ab = b - a; + coord_t ab_size = ab.cast().norm(); + coord_t a_R = edge.from->data.distance_to_boundary; + coord_t b_R = edge.to->data.distance_to_boundary; + + edge_t* last_edge_replacing_input = &edge; + for (coord_t rib_thickness : rib_thicknesses) + { + if (rib_thickness / 2 <= a_R) + { + continue; + } + if (rib_thickness / 2 >= b_R) + { + break; + } + + coord_t new_node_bead_count = std::min(edge.from->data.bead_count, edge.to->data.bead_count); + coord_t end_pos = int64_t(ab_size) * int64_t(rib_thickness / 2 - a_R) / int64_t(b_R - a_R); + assert(end_pos > 0); + assert(end_pos < ab_size); + node_t* close_node = (end_pos < ab_size / 2)? from : to; + if ((end_pos < snap_dist || end_pos > ab_size - snap_dist) + && close_node->data.bead_count == new_node_bead_count + ) + { + assert(end_pos <= ab_size); + close_node->data.transition_ratio = 0; + continue; + } + Point mid = a + normal(ab, end_pos); + + assert(last_edge_replacing_input->data.isCentral()); + assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); + last_edge_replacing_input = graph.insertNode(last_edge_replacing_input, mid, new_node_bead_count); + assert(last_edge_replacing_input->data.type != SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD); + assert(last_edge_replacing_input->data.isCentral()); + } + } +} + +// +// ^^^^^^^^^^^^^^^^^^^^^ +// TRANSTISIONING +// ===================== + +void SkeletalTrapezoidation::markRegions() +{ + // Painters algorithm, loop over all edges and skip those that have already been 'painted' with a region. + size_t region = 0; // <- Region zero is 'None', it will be incremented before the first edge. + for (edge_t& edge : graph.edges) + { + if (edge.data.regionIsSet()) + { + continue; + } + + // An edge that didn't have a region painted is encountered, so make a new region and start a worklist: + ++region; + std::queue worklist; + worklist.push(&edge); + + // Loop over all edges that are connected to this one, except don't cross any medial axis edges: + while (!worklist.empty()) + { + edge_t* p_side = worklist.front(); + worklist.pop(); + + edge_t* p_next = p_side; + do + { + if (!p_next->data.regionIsSet()) + { + p_next->data.setRegion(region); + if(p_next->twin != nullptr && (p_next->next == nullptr || p_next->prev == nullptr)) + { + worklist.push(p_next->twin); + } + } + else + { + assert(region == p_next->data.getRegion()); + } + + p_next = p_next->next; + } while (p_next != nullptr && p_next != p_side); + } + } +} + +// ===================== +// TOOLPATH GENERATION +// vvvvvvvvvvvvvvvvvvvvv +// + +void SkeletalTrapezoidation::generateSegments() +{ + std::vector upward_quad_mids; + for (edge_t& edge : graph.edges) + { + if (edge.prev && edge.next && edge.isUpward()) + { + upward_quad_mids.emplace_back(&edge); + } + } + + std::sort(upward_quad_mids.begin(), upward_quad_mids.end(), [](edge_t* a, edge_t* b) + { + if (a->to->data.distance_to_boundary == b->to->data.distance_to_boundary) + { // Ordering between two 'upward' edges of the same distance is important when one of the edges is flat and connected to the other + if (a->from->data.distance_to_boundary == a->to->data.distance_to_boundary + && b->from->data.distance_to_boundary == b->to->data.distance_to_boundary) + { + coord_t max = std::numeric_limits::max(); + coord_t a_dist_from_up = std::min(a->distToGoUp().value_or(max), a->twin->distToGoUp().value_or(max)) - (a->to->p - a->from->p).cast().norm(); + coord_t b_dist_from_up = std::min(b->distToGoUp().value_or(max), b->twin->distToGoUp().value_or(max)) - (b->to->p - b->from->p).cast().norm(); + return a_dist_from_up < b_dist_from_up; + } + else if (a->from->data.distance_to_boundary == a->to->data.distance_to_boundary) + { + return true; // Edge a might be 'above' edge b + } + else if (b->from->data.distance_to_boundary == b->to->data.distance_to_boundary) + { + return false; // Edge b might be 'above' edge a + } + else + { + // Ordering is not important + } + } + return a->to->data.distance_to_boundary > b->to->data.distance_to_boundary; + }); + + ptr_vector_t node_beadings; + { // Store beading + for (node_t& node : graph.nodes) + { + if (node.data.bead_count <= 0) + { + continue; + } + if (node.data.transition_ratio == 0) + { + node_beadings.emplace_back(new BeadingPropagation(beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count))); + node.data.setBeading(node_beadings.back()); + assert(node_beadings.back()->beading.total_thickness == node.data.distance_to_boundary * 2); + if(node_beadings.back()->beading.total_thickness != node.data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "If transitioning to an endpoint (ratio 0), the node should be exactly in the middle."; + } + } + else + { + Beading low_count_beading = beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count); + Beading high_count_beading = beading_strategy.compute(node.data.distance_to_boundary * 2, node.data.bead_count + 1); + Beading merged = interpolate(low_count_beading, 1.0 - node.data.transition_ratio, high_count_beading); + node_beadings.emplace_back(new BeadingPropagation(merged)); + node.data.setBeading(node_beadings.back()); + assert(merged.total_thickness == node.data.distance_to_boundary * 2); + if(merged.total_thickness != node.data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "If merging two beads, the new bead must be exactly in the middle."; + } + } + } + } + + propagateBeadingsUpward(upward_quad_mids, node_beadings); + + propagateBeadingsDownward(upward_quad_mids, node_beadings); + + ptr_vector_t edge_junctions; // junctions ordered high R to low R + generateJunctions(node_beadings, edge_junctions); + + connectJunctions(edge_junctions); + + generateLocalMaximaSingleBeads(); +} + +SkeletalTrapezoidation::edge_t* SkeletalTrapezoidation::getQuadMaxRedgeTo(edge_t* quad_start_edge) +{ + assert(quad_start_edge->prev == nullptr); + assert(quad_start_edge->from->data.distance_to_boundary == 0); + coord_t max_R = -1; + edge_t* ret = nullptr; + for (edge_t* edge = quad_start_edge; edge; edge = edge->next) + { + coord_t r = edge->to->data.distance_to_boundary; + if (r > max_R) + { + max_R = r; + ret = edge; + } + } + + if (!ret->next && ret->to->data.distance_to_boundary - scaled(0.005) < ret->from->data.distance_to_boundary) + { + ret = ret->prev; + } + assert(ret); + assert(ret->next); + return ret; +} + +void SkeletalTrapezoidation::propagateBeadingsUpward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings) +{ + for (auto upward_quad_mids_it = upward_quad_mids.rbegin(); upward_quad_mids_it != upward_quad_mids.rend(); ++upward_quad_mids_it) + { + edge_t* upward_edge = *upward_quad_mids_it; + if (upward_edge->to->data.bead_count >= 0) + { // Don't override local beading + continue; + } + if (! upward_edge->from->data.hasBeading()) + { // Only propagate if we have something to propagate + continue; + } + BeadingPropagation& lower_beading = *upward_edge->from->data.getBeading(); + if (upward_edge->to->data.hasBeading()) + { // Only propagate to places where there is place + continue; + } + assert((upward_edge->from->data.distance_to_boundary != upward_edge->to->data.distance_to_boundary || shorter_then(upward_edge->to->p - upward_edge->from->p, central_filter_dist)) && "zero difference R edges should always be central"); + coord_t length = (upward_edge->to->p - upward_edge->from->p).cast().norm(); + BeadingPropagation upper_beading = lower_beading; + upper_beading.dist_to_bottom_source += length; + upper_beading.is_upward_propagated_only = true; + node_beadings.emplace_back(new BeadingPropagation(upper_beading)); + upward_edge->to->data.setBeading(node_beadings.back()); + assert(upper_beading.beading.total_thickness <= upward_edge->to->data.distance_to_boundary * 2); + } +} + +void SkeletalTrapezoidation::propagateBeadingsDownward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings) +{ + for (edge_t* upward_quad_mid : upward_quad_mids) + { + // Transfer beading information to lower nodes + if (!upward_quad_mid->data.isCentral()) + { + // for equidistant edge: propagate from known beading to node with unknown beading + if (upward_quad_mid->from->data.distance_to_boundary == upward_quad_mid->to->data.distance_to_boundary + && upward_quad_mid->from->data.hasBeading() + && ! upward_quad_mid->to->data.hasBeading() + ) + { + propagateBeadingsDownward(upward_quad_mid->twin, node_beadings); + } + else + { + propagateBeadingsDownward(upward_quad_mid, node_beadings); + } + } + } +} + +void SkeletalTrapezoidation::propagateBeadingsDownward(edge_t* edge_to_peak, ptr_vector_t& node_beadings) +{ + coord_t length = (edge_to_peak->to->p - edge_to_peak->from->p).cast().norm(); + BeadingPropagation& top_beading = *getOrCreateBeading(edge_to_peak->to, node_beadings); + assert(top_beading.beading.total_thickness >= edge_to_peak->to->data.distance_to_boundary * 2); + if(top_beading.beading.total_thickness < edge_to_peak->to->data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "Top bead is beyond the center of the total width."; + } + assert(!top_beading.is_upward_propagated_only); + + if(!edge_to_peak->from->data.hasBeading()) + { // Set new beading if there is no beading associated with the node yet + BeadingPropagation propagated_beading = top_beading; + propagated_beading.dist_from_top_source += length; + node_beadings.emplace_back(new BeadingPropagation(propagated_beading)); + edge_to_peak->from->data.setBeading(node_beadings.back()); + assert(propagated_beading.beading.total_thickness >= edge_to_peak->from->data.distance_to_boundary * 2); + if(propagated_beading.beading.total_thickness < edge_to_peak->from->data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "Propagated bead is beyond the center of the total width."; + } + } + else + { + BeadingPropagation& bottom_beading = *edge_to_peak->from->data.getBeading(); + coord_t total_dist = top_beading.dist_from_top_source + length + bottom_beading.dist_to_bottom_source; + double ratio_of_top = static_cast(bottom_beading.dist_to_bottom_source) / std::min(total_dist, beading_propagation_transition_dist); + ratio_of_top = std::max(0.0, ratio_of_top); + if (ratio_of_top >= 1.0) + { + bottom_beading = top_beading; + bottom_beading.dist_from_top_source += length; + } + else + { + Beading merged_beading = interpolate(top_beading.beading, ratio_of_top, bottom_beading.beading, edge_to_peak->from->data.distance_to_boundary); + bottom_beading = BeadingPropagation(merged_beading); + bottom_beading.is_upward_propagated_only = false; + assert(merged_beading.total_thickness >= edge_to_peak->from->data.distance_to_boundary * 2); + if(merged_beading.total_thickness < edge_to_peak->from->data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "Merged bead is beyond the center of the total width."; + } + } + } +} + + +SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const +{ + assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0); + Beading ret = interpolate(left, ratio_left_to_whole, right); + + // TODO: don't use toolpath locations past the middle! + // TODO: stretch bead widths and locations of the higher bead count beading to fit in the left over space + coord_t next_inset_idx; + for (next_inset_idx = left.toolpath_locations.size() - 1; next_inset_idx >= 0; next_inset_idx--) + { + if (switching_radius > left.toolpath_locations[next_inset_idx]) + { + break; + } + } + if (next_inset_idx < 0) + { // There is no next inset, because there is only one + assert(left.toolpath_locations.empty() || left.toolpath_locations.front() >= switching_radius); + return ret; + } + if (next_inset_idx + 1 == coord_t(left.toolpath_locations.size())) + { // We cant adjust to fit the next edge because there is no previous one?! + return ret; + } + assert(next_inset_idx < coord_t(left.toolpath_locations.size())); + assert(left.toolpath_locations[next_inset_idx] <= switching_radius); + assert(left.toolpath_locations[next_inset_idx + 1] >= switching_radius); + if (ret.toolpath_locations[next_inset_idx] > switching_radius) + { // One inset disappeared between left and the merged one + // solve for ratio f: + // f*l + (1-f)*r = s + // f*l + r - f*r = s + // f*(l-r) + r = s + // f*(l-r) = s - r + // f = (s-r) / (l-r) + float new_ratio = static_cast(switching_radius - right.toolpath_locations[next_inset_idx]) / static_cast(left.toolpath_locations[next_inset_idx] - right.toolpath_locations[next_inset_idx]); + new_ratio = std::min(1.0, new_ratio + 0.1); + return interpolate(left, new_ratio, right); + } + return ret; +} + + +SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const +{ + assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0); + float ratio_right_to_whole = 1.0 - ratio_left_to_whole; + + Beading ret = (left.total_thickness > right.total_thickness)? left : right; + for (size_t inset_idx = 0; inset_idx < std::min(left.bead_widths.size(), right.bead_widths.size()); inset_idx++) + { + if(left.bead_widths[inset_idx] == 0 || right.bead_widths[inset_idx] == 0) + { + ret.bead_widths[inset_idx] = 0; //0-width wall markers stay 0-width. + } + else + { + ret.bead_widths[inset_idx] = ratio_left_to_whole * left.bead_widths[inset_idx] + ratio_right_to_whole * right.bead_widths[inset_idx]; + } + ret.toolpath_locations[inset_idx] = ratio_left_to_whole * left.toolpath_locations[inset_idx] + ratio_right_to_whole * right.toolpath_locations[inset_idx]; + } + return ret; +} + +void SkeletalTrapezoidation::generateJunctions(ptr_vector_t& node_beadings, ptr_vector_t& edge_junctions) +{ + for (edge_t& edge_ : graph.edges) + { + edge_t* edge = &edge_; + if (edge->from->data.distance_to_boundary > edge->to->data.distance_to_boundary) + { // Only consider the upward half-edges + continue; + } + + coord_t start_R = edge->to->data.distance_to_boundary; // higher R + coord_t end_R = edge->from->data.distance_to_boundary; // lower R + + if ((edge->from->data.bead_count == edge->to->data.bead_count && edge->from->data.bead_count >= 0) + || end_R >= start_R) + { // No beads to generate + continue; + } + + Beading* beading = &getOrCreateBeading(edge->to, node_beadings)->beading; + edge_junctions.emplace_back(std::make_shared()); + edge_.data.setExtrusionJunctions(edge_junctions.back()); // initialization + LineJunctions& ret = *edge_junctions.back(); + + assert(beading->total_thickness >= edge->to->data.distance_to_boundary * 2); + if(beading->total_thickness < edge->to->data.distance_to_boundary * 2) + { + BOOST_LOG_TRIVIAL(warning) << "Generated junction is beyond the center of total width."; + } + + Point a = edge->to->p; + Point b = edge->from->p; + Point ab = b - a; + + const size_t num_junctions = beading->toolpath_locations.size(); + size_t junction_idx; + // Compute starting junction_idx for this segment + for (junction_idx = (std::max(size_t(1), beading->toolpath_locations.size()) - 1) / 2; junction_idx < num_junctions; junction_idx--) + { + coord_t bead_R = beading->toolpath_locations[junction_idx]; + if (bead_R <= start_R) + { // Junction coinciding with start node is used in this function call + break; + } + } + + // Robustness against odd segments which might lie just slightly outside of the range due to rounding errors + // not sure if this is really needed (TODO) + if (junction_idx + 1 < num_junctions + && beading->toolpath_locations[junction_idx + 1] <= start_R + scaled(0.005) + && beading->total_thickness < start_R + scaled(0.005) + ) + { + junction_idx++; + } + + for (; junction_idx < num_junctions; junction_idx--) //When junction_idx underflows, it'll be more than num_junctions too. + { + coord_t bead_R = beading->toolpath_locations[junction_idx]; + assert(bead_R >= 0); + if (bead_R < end_R) + { // Junction coinciding with a node is handled by the next segment + break; + } + Point junction(a + (ab.cast() * int64_t(bead_R - start_R) / int64_t(end_R - start_R)).cast()); + if (bead_R > start_R - scaled(0.005)) + { // Snap to start node if it is really close, in order to be able to see 3-way intersection later on more robustly + junction = a; + } + ret.emplace_back(junction, beading->bead_widths[junction_idx], junction_idx, edge_.data.getRegion()); + } + } +} + +std::shared_ptr SkeletalTrapezoidation::getOrCreateBeading(node_t* node, ptr_vector_t& node_beadings) +{ + if (! node->data.hasBeading()) + { + if (node->data.bead_count == -1) + { // This bug is due to too small central edges + constexpr coord_t nearby_dist = scaled(0.1); + auto nearest_beading = getNearestBeading(node, nearby_dist); + if (nearest_beading) + { + return nearest_beading; + } + + // Else make a new beading: + bool has_central_edge = false; + bool first = true; + coord_t dist = std::numeric_limits::max(); + for (edge_t* edge = node->incident_edge; edge && (first || edge != node->incident_edge); edge = edge->twin->next) + { + if (edge->data.isCentral()) + { + has_central_edge = true; + } + assert(edge->to->data.distance_to_boundary >= 0); + dist = std::min(dist, edge->to->data.distance_to_boundary + coord_t((edge->to->p - edge->from->p).cast().norm())); + first = false; + } + if (!has_central_edge) + { + BOOST_LOG_TRIVIAL(error) << "Unknown beading for non-central node!"; + } + assert(dist != std::numeric_limits::max()); + node->data.bead_count = beading_strategy.getOptimalBeadCount(dist * 2); + } + assert(node->data.bead_count != -1); + node_beadings.emplace_back(new BeadingPropagation(beading_strategy.compute(node->data.distance_to_boundary * 2, node->data.bead_count))); + node->data.setBeading(node_beadings.back()); + } + assert(node->data.hasBeading()); + return node->data.getBeading(); +} + +std::shared_ptr SkeletalTrapezoidation::getNearestBeading(node_t* node, coord_t max_dist) +{ + struct DistEdge + { + edge_t* edge_to; + coord_t dist; + DistEdge(edge_t* edge_to, coord_t dist) + : edge_to(edge_to), dist(dist) + {} + }; + + auto compare = [](const DistEdge& l, const DistEdge& r) -> bool { return l.dist > r.dist; }; + std::priority_queue, decltype(compare)> further_edges(compare); + bool first = true; + for (edge_t* outgoing = node->incident_edge; outgoing && (first || outgoing != node->incident_edge); outgoing = outgoing->twin->next) + { + further_edges.emplace(outgoing, (outgoing->to->p - outgoing->from->p).cast().norm()); + first = false; + } + + for (coord_t counter = 0; counter < SKELETAL_TRAPEZOIDATION_BEAD_SEARCH_MAX; counter++) + { // Prevent endless recursion + if (further_edges.empty()) return nullptr; + DistEdge here = further_edges.top(); + further_edges.pop(); + if (here.dist > max_dist) return nullptr; + if (here.edge_to->to->data.hasBeading()) + { + return here.edge_to->to->data.getBeading(); + } + else + { // recurse + for (edge_t* further_edge = here.edge_to->next; further_edge && further_edge != here.edge_to->twin; further_edge = further_edge->twin->next) + { + further_edges.emplace(further_edge, here.dist + (further_edge->to->p - further_edge->from->p).cast().norm()); + } + } + } + return nullptr; +} + +void SkeletalTrapezoidation::addToolpathSegment(const ExtrusionJunction& from, const ExtrusionJunction& to, bool is_odd, bool force_new_path) +{ + if (from == to) return; + + VariableWidthPaths& generated_toolpaths = *p_generated_toolpaths; + + size_t inset_idx = from.perimeter_index; + if (inset_idx >= generated_toolpaths.size()) + { + generated_toolpaths.resize(inset_idx + 1); + } + assert((generated_toolpaths[inset_idx].empty() || !generated_toolpaths[inset_idx].back().junctions.empty()) && "empty extrusion lines should never have been generated"); + if (!force_new_path + && !generated_toolpaths[inset_idx].empty() + && generated_toolpaths[inset_idx].back().is_odd == is_odd + && shorter_then(generated_toolpaths[inset_idx].back().junctions.back().p - to.p, scaled(0.01)) + && std::abs(generated_toolpaths[inset_idx].back().junctions.back().w - to.w) < scaled(0.01) + ) + { + generated_toolpaths[inset_idx].back().junctions.push_back(from); + } + else if (!force_new_path + && !generated_toolpaths[inset_idx].empty() + && generated_toolpaths[inset_idx].back().is_odd == is_odd + && shorter_then(generated_toolpaths[inset_idx].back().junctions.back().p - from.p, scaled(0.01)) + && std::abs(generated_toolpaths[inset_idx].back().junctions.back().w - from.w) < scaled(0.01) + ) + { + generated_toolpaths[inset_idx].back().junctions.push_back(to); + } + else + { + generated_toolpaths[inset_idx].emplace_back(inset_idx, is_odd); + generated_toolpaths[inset_idx].back().junctions.push_back(from); + generated_toolpaths[inset_idx].back().junctions.push_back(to); + } +}; + +void SkeletalTrapezoidation::connectJunctions(ptr_vector_t& edge_junctions) +{ + std::unordered_set unprocessed_quad_starts(graph.edges.size() * 5 / 2); + for (edge_t& edge : graph.edges) + { + if (!edge.prev) + { + unprocessed_quad_starts.insert(&edge); + } + } + + std::unordered_set passed_odd_edges; + + while (!unprocessed_quad_starts.empty()) + { + edge_t* poly_domain_start = *unprocessed_quad_starts.begin(); + edge_t* quad_start = poly_domain_start; + do + { + edge_t* quad_end = quad_start; + while (quad_end->next) + { + quad_end = quad_end->next; + } + + edge_t* edge_to_peak = getQuadMaxRedgeTo(quad_start); + // walk down on both sides and connect junctions + edge_t* edge_from_peak = edge_to_peak->next; assert(edge_from_peak); + + unprocessed_quad_starts.erase(quad_start); + + if (! edge_to_peak->data.hasExtrusionJunctions()) + { + edge_junctions.emplace_back(std::make_shared()); + edge_to_peak->data.setExtrusionJunctions(edge_junctions.back()); + } + LineJunctions from_junctions = *edge_to_peak->data.getExtrusionJunctions(); + if (! edge_from_peak->twin->data.hasExtrusionJunctions()) + { + edge_junctions.emplace_back(std::make_shared()); + edge_from_peak->twin->data.setExtrusionJunctions(edge_junctions.back()); + } + LineJunctions to_junctions = *edge_from_peak->twin->data.getExtrusionJunctions(); + if (edge_to_peak->prev) + { + LineJunctions from_prev_junctions = *edge_to_peak->prev->data.getExtrusionJunctions(); + while (!from_junctions.empty() && !from_prev_junctions.empty() && from_junctions.back().perimeter_index <= from_prev_junctions.front().perimeter_index) + { + from_junctions.pop_back(); + } + from_junctions.reserve(from_junctions.size() + from_prev_junctions.size()); + from_junctions.insert(from_junctions.end(), from_prev_junctions.begin(), from_prev_junctions.end()); + assert(!edge_to_peak->prev->prev); + if(edge_to_peak->prev->prev) + { + BOOST_LOG_TRIVIAL(warning) << "The edge we're about to connect is already connected."; + } + } + if (edge_from_peak->next) + { + LineJunctions to_next_junctions = *edge_from_peak->next->twin->data.getExtrusionJunctions(); + while (!to_junctions.empty() && !to_next_junctions.empty() && to_junctions.back().perimeter_index <= to_next_junctions.front().perimeter_index) + { + to_junctions.pop_back(); + } + to_junctions.reserve(to_junctions.size() + to_next_junctions.size()); + to_junctions.insert(to_junctions.end(), to_next_junctions.begin(), to_next_junctions.end()); + assert(!edge_from_peak->next->next); + if(edge_from_peak->next->next) + { + BOOST_LOG_TRIVIAL(warning) << "The edge we're about to connect is already connected!"; + } + } + assert(std::abs(int(from_junctions.size()) - int(to_junctions.size())) <= 1); // at transitions one end has more beads + if(std::abs(int(from_junctions.size()) - int(to_junctions.size())) > 1) + { + BOOST_LOG_TRIVIAL(warning) << "Can't create a transition when connecting two perimeters where the number of beads differs too much! " << from_junctions.size() << " vs. " << to_junctions.size(); + } + + size_t segment_count = std::min(from_junctions.size(), to_junctions.size()); + for (size_t junction_rev_idx = 0; junction_rev_idx < segment_count; junction_rev_idx++) + { + ExtrusionJunction& from = from_junctions[from_junctions.size() - 1 - junction_rev_idx]; + ExtrusionJunction& to = to_junctions[to_junctions.size() - 1 - junction_rev_idx]; + assert(from.perimeter_index == to.perimeter_index); + if(from.perimeter_index != to.perimeter_index) + { + BOOST_LOG_TRIVIAL(warning) << "Connecting two perimeters with different indices! Perimeter " << from.perimeter_index << " and " << to.perimeter_index; + } + + const bool is_odd_segment = edge_to_peak->to->data.bead_count > 0 && edge_to_peak->to->data.bead_count % 2 == 1 // quad contains single bead segment + && edge_to_peak->to->data.transition_ratio == 0 && edge_to_peak->from->data.transition_ratio == 0 && edge_from_peak->to->data.transition_ratio == 0 // We're not in a transition + && junction_rev_idx == segment_count - 1 // Is single bead segment + && shorter_then(from.p - quad_start->to->p, scaled(0.005)) && shorter_then(to.p - quad_end->from->p, scaled(0.005)); + + if (is_odd_segment + && passed_odd_edges.count(quad_start->next->twin) > 0) // Only generate toolpath for odd segments once + { + continue; // Prevent duplication of single bead segments + } + + passed_odd_edges.emplace(quad_start->next); + const bool force_new_path = is_odd_segment && quad_start->to->isMultiIntersection(); + addToolpathSegment(from, to, is_odd_segment, force_new_path); + } + } + while(quad_start = quad_start->getNextUnconnected(), quad_start != poly_domain_start); + } +} + +void SkeletalTrapezoidation::generateLocalMaximaSingleBeads() +{ + VariableWidthPaths& generated_toolpaths = *p_generated_toolpaths; + + for (auto& node : graph.nodes) + { + if (! node.data.hasBeading()) + { + continue; + } + Beading& beading = node.data.getBeading()->beading; + if (beading.bead_widths.size() % 2 == 1 && node.isLocalMaximum(true) && !node.isCentral()) + { + const size_t inset_index = beading.bead_widths.size() / 2; + const size_t& region_id = node.incident_edge->data.getRegion(); + constexpr bool is_odd = true; + if (inset_index >= generated_toolpaths.size()) + { + generated_toolpaths.resize(inset_index + 1); + } + generated_toolpaths[inset_index].emplace_back(inset_index, is_odd); + ExtrusionLine& line = generated_toolpaths[inset_index].back(); + line.junctions.emplace_back(node.p, beading.bead_widths[inset_index], inset_index, region_id); + line.junctions.emplace_back(node.p + Point(50, 0), beading.bead_widths[inset_index], inset_index, region_id); + // TODO: ^^^ magic value ... + Point(50, 0) ^^^ + } + } +} + +void SkeletalTrapezoidation::liftRegionInfoToLines() +{ + std::for_each(p_generated_toolpaths->begin(), p_generated_toolpaths->end(), [](VariableWidthLines& lines) + { + std::for_each(lines.begin(), lines.end(), [](ExtrusionLine& line) + { + line.region_id = line.junctions.front().region_id; + }); + }); +} + +// +// ^^^^^^^^^^^^^^^^^^^^^ +// TOOLPATH GENERATION +// ===================== +// + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp new file mode 100644 index 000000000..fe5e35b57 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -0,0 +1,597 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef SKELETAL_TRAPEZOIDATION_H +#define SKELETAL_TRAPEZOIDATION_H + +#include + +#include // smart pointers +#include +#include // pair + +#include "utils/HalfEdgeGraph.hpp" +#include "utils/PolygonsSegmentIndex.hpp" +#include "utils/ExtrusionJunction.hpp" +#include "utils/ExtrusionLine.hpp" +#include "SkeletalTrapezoidationEdge.hpp" +#include "SkeletalTrapezoidationJoint.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" +#include "SkeletalTrapezoidationGraph.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * Main class of the dynamic beading strategies. + * + * The input polygon region is decomposed into trapezoids and represented as a half-edge data-structure. + * + * We determine which edges are 'central' accordinding to the transitioning_angle of the beading strategy, + * and determine the bead count for these central regions and apply them outward when generating toolpaths. [oversimplified] + * + * The method can be visually explained as generating the 3D union of cones surface on the outline polygons, + * and changing the heights along central regions of that surface so that they are flat. + * For more info, please consult the paper "A framework for adaptive width control of dense contour-parallel toolpaths in fused +deposition modeling" by Kuipers et al. + * This visual explanation aid explains the use of "upward", "lower" etc, + * i.e. the radial distance and/or the bead count are used as heights of this visualization, there is no coordinate called 'Z'. + * + * TODO: split this class into two: + * 1. Class for generating the decomposition and aux functions for performing updates + * 2. Class for editing the structure for our purposes. + */ +class SkeletalTrapezoidation +{ + using pos_t = double; + using vd_t = boost::polygon::voronoi_diagram; + using graph_t = SkeletalTrapezoidationGraph; + using edge_t = STHalfEdge; + using node_t = STHalfEdgeNode; + using Beading = BeadingStrategy::Beading; + using BeadingPropagation = SkeletalTrapezoidationJoint::BeadingPropagation; + using TransitionMiddle = SkeletalTrapezoidationEdge::TransitionMiddle; + using TransitionEnd = SkeletalTrapezoidationEdge::TransitionEnd; + + template + using ptr_vector_t = std::vector>; + + double transitioning_angle; //!< How pointy a region should be before we apply the method. Equals 180* - limit_bisector_angle + coord_t discretization_step_size; //!< approximate size of segments when parabolic VD edges get discretized (and vertex-vertex edges) + coord_t transition_filter_dist; //!< Filter transition mids (i.e. anchors) closer together than this + coord_t beading_propagation_transition_dist; //!< When there are different beadings propagated from below and from above, use this transitioning distance + static constexpr coord_t central_filter_dist = scaled(0.02); //!< Filter areas marked as 'central' smaller than this + static constexpr coord_t snap_dist = scaled(0.02); //!< Generic arithmatic inaccuracy. Only used to determine whether a transition really needs to insert an extra edge. + + /*! + * The strategy to use to fill a certain shape with lines. + * + * Various BeadingStrategies are available that differ in which lines get to + * print at their optimal width, where the play is being compensated, and + * how the joints are handled where we transition to different numbers of + * lines. + */ + const BeadingStrategy& beading_strategy; + +public: + using Segment = PolygonsSegmentIndex; + + /*! + * Construct a new trapezoidation problem to solve. + * \param polys The shapes to fill with walls. + * \param beading_strategy The strategy to use to fill these shapes. + * \param transitioning_angle Where we transition to a different number of + * walls, how steep should this transition be? A lower angle means that the + * transition will be longer. + * \param discretization_step_size Since g-code can't represent smooth + * transitions in line width, the line width must change with discretized + * steps. This indicates how long the line segments between those steps will + * be. + * \param transition_filter_dist The minimum length of transitions. + * Transitions shorter than this will be considered for dissolution. + * \param beading_propagation_transition_dist When there are different + * beadings propagated from below and from above, use this transitioning + * distance. + */ + SkeletalTrapezoidation(const Polygons& polys, + const BeadingStrategy& beading_strategy, + double transitioning_angle + , coord_t discretization_step_size = scaled(0.0008) + , coord_t transition_filter_dist = scaled(0.001) + , coord_t beading_propagation_transition_dist = scaled(0.0004)); + + /*! + * A skeletal graph through the polygons that we need to fill with beads. + * + * The skeletal graph represents the medial axes through each part of the + * polygons, and the lines from these medial axes towards each vertex of the + * polygons. The graph can be used to see what the width is of a polygon in + * each place and where the width transitions. + */ + graph_t graph; + + /*! + * Generate the paths that the printer must extrude, to print the outlines + * in the input polygons. + * \param filter_outermost_central_edges Some edges are "central" but still + * touch the outside of the polygon. If enabled, don't treat these as + * "central" but as if it's a obtuse corner. As a result, sharp corners will + * no longer end in a single line but will just loop. + */ + void generateToolpaths(VariableWidthPaths& generated_toolpaths, bool filter_outermost_central_edges = false); + +protected: + /*! + * Auxiliary for referencing one transition along an edge which may contain multiple transitions + */ + struct TransitionMidRef + { + edge_t* edge; + std::list::iterator transition_it; + TransitionMidRef(edge_t* edge, std::list::iterator transition_it) + : edge(edge) + , transition_it(transition_it) + {} + }; + + /*! + * Compute the skeletal trapezoidation decomposition of the input shape. + * + * Compute the Voronoi Diagram (VD) and transfer all inside edges into our half-edge (HE) datastructure. + * + * The algorithm is currently a bit overcomplicated, because the discretization of parabolic edges is performed at the same time as all edges are being transfered, + * which means that there is no one-to-one mapping from VD edges to HE edges. + * Instead we map from a VD edge to the last HE edge. + * This could be cimplified by recording the edges which should be discretized and discretizing the mafterwards. + * + * Another complication arises because the VD uses floating logic, which can result in zero-length segments after rounding to integers. + * We therefore collapse edges and their whole cells afterwards. + */ + void constructFromPolygons(const Polygons& polys); + + /*! + * mapping each voronoi VD edge to the corresponding halfedge HE edge + * In case the result segment is discretized, we map the VD edge to the *last* HE edge + */ + std::unordered_map vd_edge_to_he_edge; + std::unordered_map vd_node_to_he_node; + node_t& makeNode(vd_t::vertex_type& vd_node, Point p); //!< Get the node which the VD node maps to, or create a new mapping if there wasn't any yet. + + /*! + * (Eventual) returned 'polylines per index' result (from generateToolpaths): + */ + VariableWidthPaths* p_generated_toolpaths; + + /*! + * Transfer an edge from the VD to the HE and perform discretization of parabolic edges (and vertex-vertex edges) + * \p prev_edge serves as input and output. May be null as input. + */ + void transferEdge(Point from, Point to, vd_t::edge_type& vd_edge, edge_t*& prev_edge, Point& start_source_point, Point& end_source_point, const std::vector& segments); + + /*! + * Discretize a Voronoi edge that represents the medial axis of a vertex- + * line region or vertex-vertex region into small segments that can be + * considered to have a straight medial axis and a linear line width + * transition. + * + * The medial axis between a point and a line is a parabola. The rest of the + * algorithm doesn't want to have to deal with parabola, so this discretises + * the parabola into straight line segments. This is necessary if there is a + * sharp inner corner (acts as a point) that comes close to a straight edge. + * + * The medial axis between a point and a point is a straight line segment. + * However the distance from the medial axis to either of those points draws + * a parabola as you go along the medial axis. That means that the resulting + * line width along the medial axis would not be linearly increasing or + * linearly decreasing, but needs to take the shape of a parabola. Instead, + * we'll break this edge up into tiny line segments that can approximate the + * parabola with tiny linear increases or decreases in line width. + * \param segment The variable-width Voronoi edge to discretize. + * \param points All vertices of the original Polygons to fill with beads. + * \param segments All line segments of the original Polygons to fill with + * beads. + * \return A number of coordinates along the edge where the edge is broken + * up into discrete pieces. + */ + std::vector discretize(const vd_t::edge_type& segment, const std::vector& segments); + + /*! + * Compute the range of line segments that surround a cell of the skeletal + * graph that belongs to a point on the medial axis. + * + * This should only be used on cells that belong to a corner in the skeletal + * graph, e.g. triangular cells, not trapezoid cells. + * + * The resulting line segments is just the first and the last segment. They + * are linked to the neighboring segments, so you can iterate over the + * segments until you reach the last segment. + * \param cell The cell to compute the range of line segments for. + * \param[out] start_source_point The start point of the source segment of + * this cell. + * \param[out] end_source_point The end point of the source segment of this + * cell. + * \param[out] starting_vd_edge The edge of the Voronoi diagram where the + * loop around the cell starts. + * \param[out] ending_vd_edge The edge of the Voronoi diagram where the loop + * around the cell ends. + * \param points All vertices of the input Polygons. + * \param segments All edges of the input Polygons. + * /return Whether the cell is inside of the polygon. If it's outside of the + * polygon we should skip processing it altogether. + */ + bool computePointCellRange(vd_t::cell_type& cell, Point& start_source_point, Point& end_source_point, vd_t::edge_type*& starting_vd_edge, vd_t::edge_type*& ending_vd_edge, const std::vector& segments); + + /*! + * Compute the range of line segments that surround a cell of the skeletal + * graph that belongs to a line segment of the medial axis. + * + * This should only be used on cells that belong to a central line segment + * of the skeletal graph, e.g. trapezoid cells, not triangular cells. + * + * The resulting line segments is just the first and the last segment. They + * are linked to the neighboring segments, so you can iterate over the + * segments until you reach the last segment. + * \param cell The cell to compute the range of line segments for. + * \param[out] start_source_point The start point of the source segment of + * this cell. + * \param[out] end_source_point The end point of the source segment of this + * cell. + * \param[out] starting_vd_edge The edge of the Voronoi diagram where the + * loop around the cell starts. + * \param[out] ending_vd_edge The edge of the Voronoi diagram where the loop + * around the cell ends. + * \param points All vertices of the input Polygons. + * \param segments All edges of the input Polygons. + * /return Whether the cell is inside of the polygon. If it's outside of the + * polygon we should skip processing it altogether. + */ + void computeSegmentCellRange(vd_t::cell_type& cell, Point& start_source_point, Point& end_source_point, vd_t::edge_type*& starting_vd_edge, vd_t::edge_type*& ending_vd_edge, const std::vector& segments); + + /*! + * For VD cells associated with an input polygon vertex, we need to separate the node at the end and start of the cell into two + * That way we can reach both the quad_start and the quad_end from the [incident_edge] of the two new nodes + * Otherwise if node.incident_edge = quad_start you couldnt reach quad_end.twin by normal iteration (i.e. it = it.twin.next) + */ + void separatePointyQuadEndNodes(); + + + // ^ init | v transitioning + + void updateIsCentral(); // Update the "is_central" flag for each edge based on the transitioning_angle + + /*! + * Filter out small central areas. + * + * Only used to get rid of small edges which get marked as central because + * of rounding errors because the region is so small. + */ + void filterCentral(coord_t max_length); + + /*! + * Filter central areas connected to starting_edge recursively. + * \return Whether we should unmark this section marked as central, on the + * way back out of the recursion. + */ + bool filterCentral(edge_t* starting_edge, coord_t traveled_dist, coord_t max_length); + + /*! + * Unmark the outermost edges directly connected to the outline, as not + * being central. + * + * Only used to emulate some related literature. + * + * The paper shows that this function is bad for the stability of the framework. + */ + void filterOuterCentral(); + + /*! + * Set bead count in central regions based on the optimal_bead_count of the + * beading strategy. + */ + void updateBeadCount(); + + /*! + * Add central regions and set bead counts where there is an end of the + * central area and when traveling upward we get to another region with the + * same bead count. + */ + void filterNoncentralRegions(); + + /*! + * Add central regions and set bead counts for a particular edge and all of + * its adjacent edges. + * + * Recursive subroutine for \ref filterNoncentralRegions(). + * \return Whether to set the bead count on the way back + */ + bool filterNoncentralRegions(edge_t* to_edge, coord_t bead_count, coord_t traveled_dist, coord_t max_dist); + + /*! + * Generate middle points of all transitions on edges. + * + * The transition middle points are saved in the graph itself. They are also + * returned via the output parameter. + * \param[out] edge_transitions A list of transitions that were generated. + */ + void generateTransitionMids(ptr_vector_t>& edge_transitions); + + /*! + * Removes some transition middle points. + * + * Transitions can be removed if there are multiple intersecting transitions + * that are too close together. If transitions have opposite effects, both + * are removed. + */ + void filterTransitionMids(); + + /*! + * Merge transitions that are too close together. + * \param edge_to_start Edge pointing to the node from which to start + * traveling in all directions except along \p edge_to_start . + * \param origin_transition The transition for which we are checking nearby + * transitions. + * \param traveled_dist The distance traveled before we came to + * \p edge_to_start.to . + * \param going_up Whether we are traveling in the upward direction as seen + * from the \p origin_transition. If this doesn't align with the direction + * according to the R diff on a consecutive edge we know there was a local + * optimum. + * \return Whether the origin transition should be dissolved. + */ + std::list dissolveNearbyTransitions(edge_t* edge_to_start, TransitionMiddle& origin_transition, coord_t traveled_dist, coord_t max_dist, bool going_up); + + /*! + * Spread a certain bead count over a region in the graph. + * \param edge_to_start One edge of the region to spread the bead count in. + * \param from_bead_count All edges with this bead count will be changed. + * \param to_bead_count The new bead count for those edges. + */ + void dissolveBeadCountRegion(edge_t* edge_to_start, coord_t from_bead_count, coord_t to_bead_count); + + /*! + * Change the bead count if the given edge is at the end of a central + * region. + * + * This is necessary to provide a transitioning bead count to the edges of a + * central region to transition more smoothly from a high bead count in the + * central region to a lower bead count at the edge. + * \param edge_to_start One edge from a zone that needs to be filtered. + * \param traveled_dist The distance along the edges we've traveled so far. + * \param max_distance Don't filter beyond this range. + * \param replacing_bead_count The new bead count for this region. + * \return ``true`` if the bead count of this edge was changed. + */ + bool filterEndOfCentralTransition(edge_t* edge_to_start, coord_t traveled_dist, coord_t max_dist, coord_t replacing_bead_count); + + /*! + * Generate the endpoints of all transitions for all edges in the graph. + * \param[out] edge_transition_ends The resulting transition endpoints. + */ + void generateAllTransitionEnds(ptr_vector_t>& edge_transition_ends); + + /*! + * Also set the rest values at nodes in between the transition ends + */ + void applyTransitions(ptr_vector_t>& edge_transition_ends); + + /*! + * Create extra edges along all edges, where it needs to transition from one + * bead count to another. + * + * For example, if an edge of the graph goes from a bead count of 6 to a + * bead count of 1, it needs to generate 5 places where the beads around + * this line transition to a lower bead count. These are the "ribs". They + * reach from the edge to the border of the polygon. Where the beads hit + * those ribs the beads know to make a transition. + */ + void generateTransitioningRibs(); + + /*! + * Generate the endpoints of a specific transition midpoint. + * \param edge The edge to create transitions on. + * \param mid_R The radius of the transition middle point. + * \param transition_lower_bead_count The bead count at the lower end of the + * transition. + * \param[out] edge_transition_ends A list of endpoints to add the new + * endpoints to. + */ + void generateTransitionEnds(edge_t& edge, coord_t mid_R, coord_t transition_lower_bead_count, ptr_vector_t>& edge_transition_ends); + + /*! + * Compute a single endpoint of a transition. + * \param edge The edge to generate the endpoint for. + * \param start_pos The position where the transition starts. + * \param end_pos The position where the transition ends on the other side. + * \param transition_half_length The distance to the transition middle + * point. + * \param start_rest The gap between the start of the transition and the + * starting endpoint, as ratio of the inner bead width at the high end of + * the transition. + * \param end_rest The gap between the end of the transition and the ending + * endpoint, as ratio of the inner bead width at the high end of the + * transition. + * \param transition_lower_bead_count The bead count at the lower end of the + * transition. + * \param[out] edge_transition_ends The list to put the resulting endpoints + * in. + * \return Whether the given edge is going downward (i.e. towards a thinner + * region of the polygon). + */ + bool generateTransitionEnd(edge_t& edge, coord_t start_pos, coord_t end_pos, coord_t transition_half_length, double start_rest, double end_rest, coord_t transition_lower_bead_count, ptr_vector_t>& edge_transition_ends); + + /*! + * Determines whether an edge is going downwards or upwards in the graph. + * + * An edge is said to go "downwards" if it's going towards a narrower part + * of the polygon. The notion of "downwards" comes from the conical + * representation of the graph, where the polygon is filled with a cone of + * maximum radius. + * + * This function works by recursively checking adjacent edges until the edge + * is reached. + * \param outgoing The edge to check. + * \param traveled_dist The distance traversed so far. + * \param transition_half_length The radius of the transition width. + * \param lower_bead_count The bead count at the lower end of the edge. + * \return ``true`` if this edge is going down, or ``false`` if it's going + * up. + */ + bool isGoingDown(edge_t* outgoing, coord_t traveled_dist, coord_t transition_half_length, coord_t lower_bead_count) const; + + /*! + * Determines whether this edge marks the end of the central region. + * \param edge The edge to check. + * \return ``true`` if this edge goes from a central region to a non-central + * region, or ``false`` in every other case (central to central, non-central + * to non-central, non-central to central, or end-of-the-line). + */ + bool isEndOfCentral(const edge_t& edge) const; + + /*! + * Create extra ribs in the graph where the graph contains a parabolic arc + * or a straight between two inner corners. + * + * There might be transitions there as the beads go through a narrow + * bottleneck in the polygon. + */ + void generateExtraRibs(); + + // ^ transitioning ^ + + /*! + * It's useful to know when the paths get back to the consumer, to (what part of) a polygon the paths 'belong'. + * A single polygon without a hole is one region, a polygon with (a) hole(s) has 2 regions. + */ + void markRegions(); + + // v toolpath generation v + + /*! + * \param[out] segments the generated segments + */ + void generateSegments(); + + /*! + * From a quad (a group of linked edges in one cell of the Voronoi), find + * the edge that is furthest away from the border of the polygon. + * \param quad_start_edge The first edge of the quad. + * \return The edge of the quad that is furthest away from the border. + */ + edge_t* getQuadMaxRedgeTo(edge_t* quad_start_edge); + + /*! + * Propagate beading information from nodes that are closer to the edge + * (low radius R) to nodes that are farther from the edge (high R). + * + * only propagate from nodes with beading info upward to nodes without beading info + * + * Edges are sorted by their radius, so that we can do a depth-first walk + * without employing a recursive algorithm. + * + * In upward propagated beadings we store the distance traveled, so that we can merge these beadings with the downward propagated beadings in \ref propagateBeadingsDownward(.) + * + * \param upward_quad_mids all upward halfedges of the inner skeletal edges (not directly connected to the outline) sorted on their highest [distance_to_boundary]. Higher dist first. + */ + void propagateBeadingsUpward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings); + + /*! + * propagate beading info from higher R nodes to lower R nodes + * + * merge with upward propagated beadings if they are encountered + * + * don't transfer to nodes which lie on the outline polygon + * + * edges are sorted so that we can do a depth-first walk without employing a recursive algorithm + * + * \param upward_quad_mids all upward halfedges of the inner skeletal edges (not directly connected to the outline) sorted on their highest [distance_to_boundary]. Higher dist first. + */ + void propagateBeadingsDownward(std::vector& upward_quad_mids, ptr_vector_t& node_beadings); + + /*! + * Subroutine of \ref propagateBeadingsDownward(std::vector&, ptr_vector_t&) + */ + void propagateBeadingsDownward(edge_t* edge_to_peak, ptr_vector_t& node_beadings); + + /*! + * Find a beading in between two other beadings. + * + * This creates a new beading. With this we can find the coordinates of the + * endpoints of the actual line segments to draw. + * + * The parameters \p left and \p right are not actually always left or right + * but just arbitrary directions to visually indicate the difference. + * \param left One of the beadings to interpolate between. + * \param ratio_left_to_whole The position within the two beadings to sample + * an interpolation. Should be a ratio between 0 and 1. + * \param right One of the beadings to interpolate between. + * \param switching_radius The bead radius at which we switch from the left + * beading to the merged beading, if the beadings have a different number of + * beads. + * \return The beading at the interpolated location. + */ + Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const; + + /*! + * Subroutine of \ref interpolate(const Beading&, Ratio, const Beading&, coord_t) + * + * This creates a new Beading between two beadings, assuming that both have + * the same number of beads. + * \param left One of the beadings to interpolate between. + * \param ratio_left_to_whole The position within the two beadings to sample + * an interpolation. Should be a ratio between 0 and 1. + * \param right One of the beadings to interpolate between. + * \return The beading at the interpolated location. + */ + Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const; + + /*! + * Get the beading at a certain node of the skeletal graph, or create one if + * it doesn't have one yet. + * + * This is a lazy get. + * \param node The node to get the beading from. + * \param node_beadings A list of all beadings for nodes. + * \return The beading of that node. + */ + std::shared_ptr getOrCreateBeading(node_t* node, ptr_vector_t& node_beadings); + + /*! + * In case we cannot find the beading of a node, get a beading from the + * nearest node. + * \param node The node to attempt to get a beading from. The actual node + * that the returned beading is from may be a different, nearby node. + * \param max_dist The maximum distance to search for. + * \return A beading for the node, or ``nullptr`` if there is no node nearby + * with a beading. + */ + std::shared_ptr getNearestBeading(node_t* node, coord_t max_dist); + + /*! + * generate junctions for each bone + * \param edge_to_junctions junctions ordered high R to low R + */ + void generateJunctions(ptr_vector_t& node_beadings, ptr_vector_t& edge_junctions); + + /*! + * add a new toolpath segment, defined between two extrusion-juntions + */ + void addToolpathSegment(const ExtrusionJunction& from, const ExtrusionJunction& to, bool is_odd, bool force_new_path); + + /*! + * connect junctions in each quad + */ + void connectJunctions(ptr_vector_t& edge_junctions); + + /*! + * Genrate small segments for local maxima where the beading would only result in a single bead + */ + void generateLocalMaximaSingleBeads(); + + /*! + * Extract region information from the junctions, for easier access to that info directly from the lines. + */ + void liftRegionInfoToLines(); +}; + +} // namespace Slic3r::Arachne +#endif // VORONOI_QUADRILATERALIZATION_H diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp new file mode 100644 index 000000000..c2b588979 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp @@ -0,0 +1,142 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef SKELETAL_TRAPEZOIDATION_EDGE_H +#define SKELETAL_TRAPEZOIDATION_EDGE_H + +#include // smart pointers +#include +#include + +#include "utils/ExtrusionJunction.hpp" + +namespace Slic3r::Arachne +{ + +class SkeletalTrapezoidationEdge +{ +private: + enum class Central { UNKNOWN = -1, NO, YES }; + +public: + /*! + * Representing the location along an edge where the anchor position of a transition should be placed. + */ + struct TransitionMiddle + { + coord_t pos; // Position along edge as measure from edge.from.p + int lower_bead_count; + TransitionMiddle(coord_t pos, int lower_bead_count) + : pos(pos), lower_bead_count(lower_bead_count) + {} + }; + + /*! + * Represents the location along an edge where the lower or upper end of a transition should be placed. + */ + struct TransitionEnd + { + coord_t pos; // Position along edge as measure from edge.from.p, where the edge is always the half edge oriented from lower to higher R + int lower_bead_count; + bool is_lower_end; // Whether this is the ed of the transition with lower bead count + TransitionEnd(coord_t pos, int lower_bead_count, bool is_lower_end) + : pos(pos), lower_bead_count(lower_bead_count), is_lower_end(is_lower_end) + {} + }; + + enum class EdgeType + { + NORMAL = 0, // from voronoi diagram + EXTRA_VD = 1, // introduced to voronoi diagram in order to make the gMAT + TRANSITION_END = 2 // introduced to voronoi diagram in order to make the gMAT + }; + EdgeType type; + + SkeletalTrapezoidationEdge() + : SkeletalTrapezoidationEdge(EdgeType::NORMAL) + {} + SkeletalTrapezoidationEdge(const EdgeType& type) + : type(type) + , is_central(Central::UNKNOWN) + , region(0) + {} + + bool isCentral() const + { + assert(is_central != Central::UNKNOWN); + return is_central == Central::YES; + } + void setIsCentral(bool b) + { + is_central = b ? Central::YES : Central::NO; + } + bool centralIsSet() const + { + return is_central != Central::UNKNOWN; + } + + size_t getRegion() const + { + assert(region != 0); + return region; + } + void setRegion(const size_t& r) + { + assert(region == 0); + region = r; + } + bool regionIsSet() const + { + return region > 0; + } + + bool hasTransitions(bool ignore_empty = false) const + { + return transitions.use_count() > 0 && (ignore_empty || ! transitions.lock()->empty()); + } + void setTransitions(std::shared_ptr> storage) + { + transitions = storage; + } + std::shared_ptr> getTransitions() + { + return transitions.lock(); + } + + bool hasTransitionEnds(bool ignore_empty = false) const + { + return transition_ends.use_count() > 0 && (ignore_empty || ! transition_ends.lock()->empty()); + } + void setTransitionEnds(std::shared_ptr> storage) + { + transition_ends = storage; + } + std::shared_ptr> getTransitionEnds() + { + return transition_ends.lock(); + } + + bool hasExtrusionJunctions(bool ignore_empty = false) const + { + return extrusion_junctions.use_count() > 0 && (ignore_empty || ! extrusion_junctions.lock()->empty()); + } + void setExtrusionJunctions(std::shared_ptr storage) + { + extrusion_junctions = storage; + } + std::shared_ptr getExtrusionJunctions() + { + return extrusion_junctions.lock(); + } + +private: + Central is_central; //! whether the edge is significant; whether the source segments have a sharp angle; -1 is unknown + size_t region; //! what 'region' this edge is in ... if the originating polygon has no holes, there's one region -- useful for later algorithms that need to know where the paths came from + + std::weak_ptr> transitions; + std::weak_ptr> transition_ends; + std::weak_ptr extrusion_junctions; +}; + +} // namespace Slic3r::Arachne +#endif // SKELETAL_TRAPEZOIDATION_EDGE_H diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp new file mode 100644 index 000000000..a28c69f87 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp @@ -0,0 +1,496 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "SkeletalTrapezoidationGraph.hpp" +#include + +#include + +#include "utils/linearAlg2D.hpp" +#include "../Line.hpp" + +namespace Slic3r::Arachne +{ + +STHalfEdge::STHalfEdge(SkeletalTrapezoidationEdge data) : HalfEdge(data) {} + +bool STHalfEdge::canGoUp(bool strict) const +{ + if (to->data.distance_to_boundary > from->data.distance_to_boundary) + { + return true; + } + if (to->data.distance_to_boundary < from->data.distance_to_boundary || strict) + { + return false; + } + + // Edge is between equidistqant verts; recurse! + for (edge_t* outgoing = next; outgoing != twin; outgoing = outgoing->twin->next) + { + if (outgoing->canGoUp()) + { + return true; + } + assert(outgoing->twin); if (!outgoing->twin) return false; + assert(outgoing->twin->next); if (!outgoing->twin->next) return true; // This point is on the boundary?! Should never occur + } + return false; +} + +bool STHalfEdge::isUpward() const +{ + if (to->data.distance_to_boundary > from->data.distance_to_boundary) + { + return true; + } + if (to->data.distance_to_boundary < from->data.distance_to_boundary) + { + return false; + } + + // Equidistant edge case: + std::optional forward_up_dist = this->distToGoUp(); + std::optional backward_up_dist = twin->distToGoUp(); + if (forward_up_dist && backward_up_dist) + { + return forward_up_dist < backward_up_dist; + } + + if (forward_up_dist) + { + return true; + } + + if (backward_up_dist) + { + return false; + } + return to->p < from->p; // Arbitrary ordering, which returns the opposite for the twin edge +} + +std::optional STHalfEdge::distToGoUp() const +{ + if (to->data.distance_to_boundary > from->data.distance_to_boundary) + { + return 0; + } + if (to->data.distance_to_boundary < from->data.distance_to_boundary) + { + return std::optional(); + } + + // Edge is between equidistqant verts; recurse! + std::optional ret; + for (edge_t* outgoing = next; outgoing != twin; outgoing = outgoing->twin->next) + { + std::optional dist_to_up = outgoing->distToGoUp(); + if (dist_to_up) + { + if (ret) + { + ret = std::min(*ret, *dist_to_up); + } + else + { + ret = dist_to_up; + } + } + assert(outgoing->twin); if (!outgoing->twin) return std::optional(); + assert(outgoing->twin->next); if (!outgoing->twin->next) return 0; // This point is on the boundary?! Should never occur + } + if (ret) + { + ret = *ret + (to->p - from->p).cast().norm(); + } + return ret; +} + +STHalfEdge* STHalfEdge::getNextUnconnected() +{ + edge_t* result = static_cast(this); + while (result->next) + { + result = result->next; + if (result == this) + { + return nullptr; + } + } + return result->twin; +} + +STHalfEdgeNode::STHalfEdgeNode(SkeletalTrapezoidationJoint data, Point p) : HalfEdgeNode(data, p) {} + +bool STHalfEdgeNode::isMultiIntersection() +{ + int odd_path_count = 0; + edge_t* outgoing = this->incident_edge; + do + { + if (outgoing->data.isCentral()) + { + odd_path_count++; + } + } while (outgoing = outgoing->twin->next, outgoing != this->incident_edge); + return odd_path_count > 2; +} + +bool STHalfEdgeNode::isCentral() const +{ + edge_t* edge = incident_edge; + do + { + if (edge->data.isCentral()) + { + return true; + } + assert(edge->twin); if (!edge->twin) return false; + } while (edge = edge->twin->next, edge != incident_edge); + return false; +} + +bool STHalfEdgeNode::isLocalMaximum(bool strict) const +{ + if (data.distance_to_boundary == 0) + { + return false; + } + + edge_t* edge = incident_edge; + do + { + if (edge->canGoUp(strict)) + { + return false; + } + assert(edge->twin); if (!edge->twin) return false; + + if (!edge->twin->next) + { // This point is on the boundary + return false; + } + } while (edge = edge->twin->next, edge != incident_edge); + return true; +} + +void SkeletalTrapezoidationGraph::fixNodeDuplication() +{ + for (auto node_it = nodes.begin(); node_it != nodes.end();) + { + node_t* replacing_node = nullptr; + for (edge_t* outgoing = node_it->incident_edge; outgoing != node_it->incident_edge; outgoing = outgoing->twin->next) + { + assert(outgoing); + if (outgoing->from != &*node_it) + { + replacing_node = outgoing->from; + } + if (outgoing->twin->to != &*node_it) + { + replacing_node = outgoing->twin->to; + } + } + if (replacing_node) + { + for (edge_t* outgoing = node_it->incident_edge; outgoing != node_it->incident_edge; outgoing = outgoing->twin->next) + { + outgoing->twin->to = replacing_node; + outgoing->from = replacing_node; + } + node_it = nodes.erase(node_it); + } + else + { + ++node_it; + } + } +} + +void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) +{ + std::unordered_map::iterator> edge_locator; + std::unordered_map::iterator> node_locator; + + for (auto edge_it = edges.begin(); edge_it != edges.end(); ++edge_it) + { + edge_locator.emplace(&*edge_it, edge_it); + } + + for (auto node_it = nodes.begin(); node_it != nodes.end(); ++node_it) + { + node_locator.emplace(&*node_it, node_it); + } + + auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, std::list::iterator& current_edge_it, bool& edge_it_is_updated) + { + if (current_edge_it != edges.end() + && to_be_removed == &*current_edge_it) + { + current_edge_it = edges.erase(current_edge_it); + edge_it_is_updated = true; + } + else + { + edges.erase(edge_locator[to_be_removed]); + } + }; + + auto should_collapse = [snap_dist](node_t* a, node_t* b) + { + return shorter_then(a->p - b->p, snap_dist); + }; + + for (auto edge_it = edges.begin(); edge_it != edges.end();) + { + if (edge_it->prev) + { + edge_it++; + continue; + } + + edge_t* quad_start = &*edge_it; + edge_t* quad_end = quad_start; while (quad_end->next) quad_end = quad_end->next; + edge_t* quad_mid = (quad_start->next == quad_end)? nullptr : quad_start->next; + + bool edge_it_is_updated = false; + if (quad_mid && should_collapse(quad_mid->from, quad_mid->to)) + { + assert(quad_mid->twin); + if(!quad_mid->twin) + { + BOOST_LOG_TRIVIAL(warning) << "Encountered quad edge without a twin."; + continue; //Prevent accessing unallocated memory. + } + int count = 0; + for (edge_t* edge_from_3 = quad_end; edge_from_3 && edge_from_3 != quad_mid->twin; edge_from_3 = edge_from_3->twin->next) + { + edge_from_3->from = quad_mid->from; + edge_from_3->twin->to = quad_mid->from; + if (count > 50) + { + std::cerr << edge_from_3->from->p << " - " << edge_from_3->to->p << '\n'; + } + if (++count > 1000) + { + break; + } + } + + // o-o > collapse top + // | | + // | | + // | | + // o o + if (quad_mid->from->incident_edge == quad_mid) + { + if (quad_mid->twin->next) + { + quad_mid->from->incident_edge = quad_mid->twin->next; + } + else + { + quad_mid->from->incident_edge = quad_mid->prev->twin; + } + } + + nodes.erase(node_locator[quad_mid->to]); + + quad_mid->prev->next = quad_mid->next; + quad_mid->next->prev = quad_mid->prev; + quad_mid->twin->next->prev = quad_mid->twin->prev; + quad_mid->twin->prev->next = quad_mid->twin->next; + + safelyRemoveEdge(quad_mid->twin, edge_it, edge_it_is_updated); + safelyRemoveEdge(quad_mid, edge_it, edge_it_is_updated); + } + + // o-o + // | | > collapse sides + // o o + if ( should_collapse(quad_start->from, quad_end->to) && should_collapse(quad_start->to, quad_end->from)) + { // Collapse start and end edges and remove whole cell + + quad_start->twin->to = quad_end->to; + quad_end->to->incident_edge = quad_end->twin; + if (quad_end->from->incident_edge == quad_end) + { + if (quad_end->twin->next) + { + quad_end->from->incident_edge = quad_end->twin->next; + } + else + { + quad_end->from->incident_edge = quad_end->prev->twin; + } + } + nodes.erase(node_locator[quad_start->from]); + + quad_start->twin->twin = quad_end->twin; + quad_end->twin->twin = quad_start->twin; + safelyRemoveEdge(quad_start, edge_it, edge_it_is_updated); + safelyRemoveEdge(quad_end, edge_it, edge_it_is_updated); + } + // If only one side had collapsable length then the cell on the other side of that edge has to collapse + // if we would collapse that one edge then that would change the quad_start and/or quad_end of neighboring cells + // this is to do with the constraint that !prev == !twin.next + + if (!edge_it_is_updated) + { + edge_it++; + } + } +} + +void SkeletalTrapezoidationGraph::makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point, bool is_next_to_start_or_end) +{ + Point p; + Line(start_source_point, end_source_point).distance_to_infinite_squared(prev_edge->to->p, &p); + coord_t dist = (prev_edge->to->p - p).cast().norm(); + prev_edge->to->data.distance_to_boundary = dist; + assert(dist >= 0); + + nodes.emplace_front(SkeletalTrapezoidationJoint(), p); + node_t* node = &nodes.front(); + node->data.distance_to_boundary = 0; + + edges.emplace_front(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD)); + edge_t* forth_edge = &edges.front(); + edges.emplace_front(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD)); + edge_t* back_edge = &edges.front(); + + prev_edge->next = forth_edge; + forth_edge->prev = prev_edge; + forth_edge->from = prev_edge->to; + forth_edge->to = node; + forth_edge->twin = back_edge; + back_edge->twin = forth_edge; + back_edge->from = node; + back_edge->to = prev_edge->to; + node->incident_edge = back_edge; + + prev_edge = back_edge; +} + +std::pair SkeletalTrapezoidationGraph::insertRib(edge_t& edge, node_t* mid_node) +{ + edge_t* edge_before = edge.prev; + edge_t* edge_after = edge.next; + node_t* node_before = edge.from; + node_t* node_after = edge.to; + + Point p = mid_node->p; + + const Line source_segment = getSource(edge); + Point px; + source_segment.distance_to_squared(p, &px); + coord_t dist = (p - px).cast().norm(); + assert(dist > 0); + mid_node->data.distance_to_boundary = dist; + mid_node->data.transition_ratio = 0; // Both transition end should have rest = 0, because at the ends a whole number of beads fits without rest + + nodes.emplace_back(SkeletalTrapezoidationJoint(), px); + node_t* source_node = &nodes.back(); + source_node->data.distance_to_boundary = 0; + + edge_t* first = &edge; + edges.emplace_back(SkeletalTrapezoidationEdge()); + edge_t* second = &edges.back(); + edges.emplace_back(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::TRANSITION_END)); + edge_t* outward_edge = &edges.back(); + edges.emplace_back(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::TRANSITION_END)); + edge_t* inward_edge = &edges.back(); + + if (edge_before) + { + edge_before->next = first; + } + first->next = outward_edge; + outward_edge->next = nullptr; + inward_edge->next = second; + second->next = edge_after; + + if (edge_after) + { + edge_after->prev = second; + } + second->prev = inward_edge; + inward_edge->prev = nullptr; + outward_edge->prev = first; + first->prev = edge_before; + + first->to = mid_node; + outward_edge->to = source_node; + inward_edge->to = mid_node; + second->to = node_after; + + first->from = node_before; + outward_edge->from = mid_node; + inward_edge->from = source_node; + second->from = mid_node; + + node_before->incident_edge = first; + mid_node->incident_edge = outward_edge; + source_node->incident_edge = inward_edge; + if (edge_after) + { + node_after->incident_edge = edge_after; + } + + first->data.setIsCentral(true); + outward_edge->data.setIsCentral(false); // TODO verify this is always the case. + inward_edge->data.setIsCentral(false); + second->data.setIsCentral(true); + + outward_edge->twin = inward_edge; + inward_edge->twin = outward_edge; + + first->twin = nullptr; // we don't know these yet! + second->twin = nullptr; + + assert(second->prev->from->data.distance_to_boundary == 0); + + return std::make_pair(first, second); +} + +SkeletalTrapezoidationGraph::edge_t* SkeletalTrapezoidationGraph::insertNode(edge_t* edge, Point mid, coord_t mide_node_bead_count) +{ + edge_t* last_edge_replacing_input = edge; + + nodes.emplace_back(SkeletalTrapezoidationJoint(), mid); + node_t* mid_node = &nodes.back(); + + edge_t* twin = last_edge_replacing_input->twin; + last_edge_replacing_input->twin = nullptr; + twin->twin = nullptr; + std::pair left_pair = insertRib(*last_edge_replacing_input, mid_node); + std::pair right_pair = insertRib(*twin, mid_node); + edge_t* first_edge_replacing_input = left_pair.first; + last_edge_replacing_input = left_pair.second; + edge_t* first_edge_replacing_twin = right_pair.first; + edge_t* last_edge_replacing_twin = right_pair.second; + + first_edge_replacing_input->twin = last_edge_replacing_twin; + last_edge_replacing_twin->twin = first_edge_replacing_input; + last_edge_replacing_input->twin = first_edge_replacing_twin; + first_edge_replacing_twin->twin = last_edge_replacing_input; + + mid_node->data.bead_count = mide_node_bead_count; + + return last_edge_replacing_input; +} + +Line SkeletalTrapezoidationGraph::getSource(const edge_t &edge) const +{ + const edge_t *from_edge = &edge; + while (from_edge->prev) + from_edge = from_edge->prev; + + const edge_t *to_edge = &edge; + while (to_edge->next) + to_edge = to_edge->next; + + return Line(from_edge->from->p, to_edge->to->p); +} + +} diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp new file mode 100644 index 000000000..92aba36a0 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp @@ -0,0 +1,106 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef SKELETAL_TRAPEZOIDATION_GRAPH_H +#define SKELETAL_TRAPEZOIDATION_GRAPH_H + +#include + +#include "utils/HalfEdgeGraph.hpp" +#include "SkeletalTrapezoidationEdge.hpp" +#include "SkeletalTrapezoidationJoint.hpp" + +namespace Slic3r::Arachne +{ + +class STHalfEdgeNode; + +class STHalfEdge : public HalfEdge +{ + using edge_t = STHalfEdge; + using node_t = STHalfEdgeNode; +public: + STHalfEdge(SkeletalTrapezoidationEdge data); + + /*! + * Check (recursively) whether there is any upward edge from the distance_to_boundary of the from of the \param edge + * + * \param strict Whether equidistant edges can count as a local maximum + */ + bool canGoUp(bool strict = false) const; + + /*! + * Check whether the edge goes from a lower to a higher distance_to_boundary. + * Effectively deals with equidistant edges by looking beyond this edge. + */ + bool isUpward() const; + + /*! + * Calculate the traversed distance until we meet an upward edge. + * Useful for calling on edges between equidistant points. + * + * If we can go up then the distance includes the length of the \param edge + */ + std::optional distToGoUp() const; + + STHalfEdge* getNextUnconnected(); +}; + +class STHalfEdgeNode : public HalfEdgeNode +{ + using edge_t = STHalfEdge; + using node_t = STHalfEdgeNode; +public: + STHalfEdgeNode(SkeletalTrapezoidationJoint data, Point p); + + bool isMultiIntersection(); + + bool isCentral() const; + + /*! + * Check whether this node has a locally maximal distance_to_boundary + * + * \param strict Whether equidistant edges can count as a local maximum + */ + bool isLocalMaximum(bool strict = false) const; +}; + +class SkeletalTrapezoidationGraph: public HalfEdgeGraph +{ + using edge_t = STHalfEdge; + using node_t = STHalfEdgeNode; +public: + void fixNodeDuplication(); + + /*! + * If an edge is too small, collapse it and its twin and fix the surrounding edges to ensure a consistent graph. + * + * Don't collapse support edges, unless we can collapse the whole quad. + * + * o-, + * | "-o + * | | > Don't collapse this edge only. + * o o + */ + void collapseSmallEdges(coord_t snap_dist = 5000); + + void makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point, bool is_next_to_start_or_end); + + /*! + * Insert a node into the graph and connect it to the input polygon using ribs + * + * \return the last edge which replaced [edge], which points to the same [to] node + */ + edge_t* insertNode(edge_t* edge, Point mid, coord_t mide_node_bead_count); + + /*! + * Return the first and last edge of the edges replacing \p edge pointing to the same node + */ + std::pair insertRib(edge_t& edge, node_t* mid_node); + +protected: + Line getSource(const edge_t& edge) const; +}; + +} +#endif diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp new file mode 100644 index 000000000..346d51116 --- /dev/null +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp @@ -0,0 +1,60 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef SKELETAL_TRAPEZOIDATION_JOINT_H +#define SKELETAL_TRAPEZOIDATION_JOINT_H + +#include // smart pointers + +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" + +namespace Slic3r::Arachne +{ + +class SkeletalTrapezoidationJoint +{ + using Beading = BeadingStrategy::Beading; +public: + struct BeadingPropagation + { + Beading beading; + coord_t dist_to_bottom_source; + coord_t dist_from_top_source; + bool is_upward_propagated_only; + BeadingPropagation(const Beading& beading) + : beading(beading) + , dist_to_bottom_source(0) + , dist_from_top_source(0) + , is_upward_propagated_only(false) + {} + }; + + coord_t distance_to_boundary; + coord_t bead_count; + float transition_ratio; //! The distance near the skeleton to leave free because this joint is in the middle of a transition, as a fraction of the inner bead width of the bead at the higher transition. + SkeletalTrapezoidationJoint() + : distance_to_boundary(-1) + , bead_count(-1) + , transition_ratio(0) + {} + + bool hasBeading() const + { + return beading.use_count() > 0; + } + void setBeading(std::shared_ptr storage) + { + beading = storage; + } + std::shared_ptr getBeading() + { + return beading.lock(); + } + +private: + + std::weak_ptr beading; +}; + +} // namespace Slic3r::Arachne +#endif // SKELETAL_TRAPEZOIDATION_JOINT_H diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp new file mode 100644 index 000000000..1f44b001a --- /dev/null +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -0,0 +1,864 @@ +// Copyright (c) 2020 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include //For std::partition_copy and std::min_element. +#include + +#include "WallToolPaths.hpp" + +#include "SkeletalTrapezoidation.hpp" +#include "../ClipperUtils.hpp" +#include "Arachne/utils/linearAlg2D.hpp" +#include "EdgeGrid.hpp" +#include "utils/SparseLineGrid.hpp" +#include "Geometry.hpp" + +namespace Slic3r::Arachne +{ + +WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t nominal_bead_width, const size_t inset_count, const coord_t wall_0_inset, + const PrintConfig &print_config) + : outline(outline) + , bead_width_0(nominal_bead_width) + , bead_width_x(nominal_bead_width) + , inset_count(inset_count) + , wall_0_inset(wall_0_inset) + , strategy_type(print_config.beading_strategy_type.value) + , print_thin_walls(Slic3r::Arachne::fill_outline_gaps) + , min_feature_size(scaled(print_config.min_feature_size.value)) + , min_bead_width(scaled(print_config.min_bead_width.value)) + , small_area_length(static_cast(nominal_bead_width) / 2.) + , toolpaths_generated(false) + , print_config(print_config) +{ +} + +WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t bead_width_0, const coord_t bead_width_x, + const size_t inset_count, const coord_t wall_0_inset, const PrintConfig &print_config) + : outline(outline) + , bead_width_0(bead_width_0) + , bead_width_x(bead_width_x) + , inset_count(inset_count) + , wall_0_inset(wall_0_inset) + , strategy_type(print_config.beading_strategy_type.value) + , print_thin_walls(Slic3r::Arachne::fill_outline_gaps) + , min_feature_size(scaled(print_config.min_feature_size.value)) + , min_bead_width(scaled(print_config.min_bead_width.value)) + , small_area_length(static_cast(bead_width_0) / 2.) + , toolpaths_generated(false) + , print_config(print_config) +{ +} + +void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared) +{ + if (thiss.size() < 3) + { + thiss.points.clear(); + return; + } + if (thiss.size() == 3) + { + return; + } + + Polygon new_path; + Point previous = thiss.points.back(); + Point previous_previous = thiss.points.at(thiss.points.size() - 2); + Point current = thiss.points.at(0); + + /* When removing a vertex, we check the height of the triangle of the area + being removed from the original polygon by the simplification. However, + when consecutively removing multiple vertices the height of the previously + removed vertices w.r.t. the shortcut path changes. + In order to not recompute the new height value of previously removed + vertices we compute the height of a representative triangle, which covers + the same amount of area as the area being cut off. We use the Shoelace + formula to accumulate the area under the removed segments. This works by + computing the area in a 'fan' where each of the blades of the fan go from + the origin to one of the segments. While removing vertices the area in + this fan accumulates. By subtracting the area of the blade connected to + the short-cutting segment we obtain the total area of the cutoff region. + From this area we compute the height of the representative triangle using + the standard formula for a triangle area: A = .5*b*h + */ + int64_t accumulated_area_removed = int64_t(previous.x()) * int64_t(current.y()) - int64_t(previous.y()) * int64_t(current.x()); // Twice the Shoelace formula for area of polygon per line segment. + + for (size_t point_idx = 0; point_idx < thiss.points.size(); point_idx++) + { + current = thiss.points.at(point_idx % thiss.points.size()); + + //Check if the accumulated area doesn't exceed the maximum. + Point next; + if (point_idx + 1 < thiss.points.size()) + { + next = thiss.points.at(point_idx + 1); + } + else if (point_idx + 1 == thiss.points.size() && new_path.size() > 1) + { // don't spill over if the [next] vertex will then be equal to [previous] + next = new_path[0]; //Spill over to new polygon for checking removed area. + } + else + { + next = thiss.points.at((point_idx + 1) % thiss.points.size()); + } + const int64_t removed_area_next = int64_t(current.x()) * int64_t(next.y()) - int64_t(current.y()) * int64_t(next.x()); // Twice the Shoelace formula for area of polygon per line segment. + const int64_t negative_area_closing = int64_t(next.x()) * int64_t(previous.y()) - int64_t(next.y()) * int64_t(previous.x()); // area between the origin and the short-cutting segment + accumulated_area_removed += removed_area_next; + + const int64_t length2 = (current - previous).cast().squaredNorm(); + if (length2 < scaled(25.)) + { + // We're allowed to always delete segments of less than 5 micron. + continue; + } + + const int64_t area_removed_so_far = accumulated_area_removed + negative_area_closing; // close the shortcut area polygon + const int64_t base_length_2 = (next - previous).cast().squaredNorm(); + + if (base_length_2 == 0) //Two line segments form a line back and forth with no area. + { + continue; //Remove the vertex. + } + //We want to check if the height of the triangle formed by previous, current and next vertices is less than allowed_error_distance_squared. + //1/2 L = A [actual area is half of the computed shoelace value] // Shoelace formula is .5*(...) , but we simplify the computation and take out the .5 + //A = 1/2 * b * h [triangle area formula] + //L = b * h [apply above two and take out the 1/2] + //h = L / b [divide by b] + //h^2 = (L / b)^2 [square it] + //h^2 = L^2 / b^2 [factor the divisor] + const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2); + if ((height_2 <= Slic3r::sqr(scaled(0.005)) //Almost exactly colinear (barring rounding errors). + && Line::distance_to_infinite(current, previous, next) <= scaled(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas + { + continue; + } + + if (length2 < smallest_line_segment_squared + && height_2 <= allowed_error_distance_squared) // removing the vertex doesn't introduce too much error.) + { + const int64_t next_length2 = (current - next).cast().squaredNorm(); + if (next_length2 > smallest_line_segment_squared) + { + // Special case; The next line is long. If we were to remove this, it could happen that we get quite noticeable artifacts. + // We should instead move this point to a location where both edges are kept and then remove the previous point that we wanted to keep. + // By taking the intersection of these two lines, we get a point that preserves the direction (so it makes the corner a bit more pointy). + // We just need to be sure that the intersection point does not introduce an artifact itself. + Point intersection_point; + bool has_intersection = Line(previous_previous, previous).intersection_infinite(Line(current, next), &intersection_point); + if (!has_intersection + || Line::distance_to_infinite_squared(intersection_point, previous, current) > double(allowed_error_distance_squared) + || (intersection_point - previous).cast().squaredNorm() > smallest_line_segment_squared // The intersection point is way too far from the 'previous' + || (intersection_point - next).cast().squaredNorm() > smallest_line_segment_squared) // and 'next' points, so it shouldn't replace 'current' + { + // We can't find a better spot for it, but the size of the line is more than 5 micron. + // So the only thing we can do here is leave it in... + } + else + { + // New point seems like a valid one. + current = intersection_point; + // If there was a previous point added, remove it. + if(!new_path.empty()) + { + new_path.points.pop_back(); + previous = previous_previous; + } + } + } + else + { + continue; //Remove the vertex. + } + } + //Don't remove the vertex. + accumulated_area_removed = removed_area_next; // so that in the next iteration it's the area between the origin, [previous] and [current] + previous_previous = previous; + previous = current; //Note that "previous" is only updated if we don't remove the vertex. + new_path.points.push_back(current); + } + + thiss = new_path; +} + +/*! + * Removes vertices of the polygons to make sure that they are not too high + * resolution. + * + * This removes points which are connected to line segments that are shorter + * than the `smallest_line_segment`, unless that would introduce a deviation + * in the contour of more than `allowed_error_distance`. + * + * Criteria: + * 1. Never remove a vertex if either of the connceted segments is larger than \p smallest_line_segment + * 2. Never remove a vertex if the distance between that vertex and the final resulting polygon would be higher than \p allowed_error_distance + * 3. The direction of segments longer than \p smallest_line_segment always + * remains unaltered (but their end points may change if it is connected to + * a small segment) + * + * Simplify uses a heuristic and doesn't neccesarily remove all removable + * vertices under the above criteria, but simplify may never violate these + * criteria. Unless the segments or the distance is smaller than the + * rounding error of 5 micron. + * + * Vertices which introduce an error of less than 5 microns are removed + * anyway, even if the segments are longer than the smallest line segment. + * This makes sure that (practically) colinear line segments are joined into + * a single line segment. + * \param smallest_line_segment Maximal length of removed line segments. + * \param allowed_error_distance If removing a vertex introduces a deviation + * from the original path that is more than this distance, the vertex may + * not be removed. + */ +void simplify(Polygons &thiss, const int64_t smallest_line_segment = scaled(0.01), const int64_t allowed_error_distance = scaled(0.005)) +{ + const int64_t allowed_error_distance_squared = int64_t(allowed_error_distance) * int64_t(allowed_error_distance); + const int64_t smallest_line_segment_squared = int64_t(smallest_line_segment) * int64_t(smallest_line_segment); + for (size_t p = 0; p < thiss.size(); p++) + { + simplify(thiss[p], smallest_line_segment_squared, allowed_error_distance_squared); + if (thiss[p].size() < 3) + { + thiss.erase(thiss.begin() + p); + p--; + } + } +} + +/*! + * Locator to extract a line segment out of a \ref PolygonsPointIndex + */ +struct PolygonsPointIndexSegmentLocator +{ + std::pair operator()(const PolygonsPointIndex &val) const + { + const Polygon &poly = (*val.polygons)[val.poly_idx]; + const Point start = poly[val.point_idx]; + unsigned int next_point_idx = (val.point_idx + 1) % poly.size(); + const Point end = poly[next_point_idx]; + return std::pair(start, end); + } +}; + +typedef SparseLineGrid LocToLineGrid; +std::unique_ptr createLocToLineGrid(const Polygons &polygons, int square_size) +{ + unsigned int n_points = 0; + for (const auto &poly : polygons) + n_points += poly.size(); + + auto ret = std::make_unique(square_size, n_points); + + for (unsigned int poly_idx = 0; poly_idx < polygons.size(); poly_idx++) + for (unsigned int point_idx = 0; point_idx < polygons[poly_idx].size(); point_idx++) + ret->insert(PolygonsPointIndex(&polygons, poly_idx, point_idx)); + return ret; +} + +/* Note: Also tries to solve for near-self intersections, when epsilon >= 1 + */ +void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) +{ + if (epsilon < 1) { + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); + return; + } + + const int64_t half_epsilon = (epsilon + 1) / 2; + + // Points too close to line segments should be moved a little away from those line segments, but less than epsilon, + // so at least half-epsilon distance between points can still be guaranteed. + constexpr coord_t grid_size = scaled(2.); + auto query_grid = createLocToLineGrid(thiss, grid_size); + + const auto move_dist = std::max(2L, half_epsilon - 2); + const int64_t half_epsilon_sqrd = half_epsilon * half_epsilon; + + const size_t n = thiss.size(); + for (size_t poly_idx = 0; poly_idx < n; poly_idx++) { + const size_t pathlen = thiss[poly_idx].size(); + for (size_t point_idx = 0; point_idx < pathlen; ++point_idx) { + Point &pt = thiss[poly_idx][point_idx]; + for (const auto &line : query_grid->getNearby(pt, epsilon)) { + const size_t line_next_idx = (line.point_idx + 1) % thiss[line.poly_idx].size(); + if (poly_idx == line.poly_idx && (point_idx == line.point_idx || point_idx == line_next_idx)) + continue; + + const Line segment(thiss[line.poly_idx][line.point_idx], thiss[line.poly_idx][line_next_idx]); + Point segment_closest_point; + segment.distance_to_squared(pt, &segment_closest_point); + + if (half_epsilon_sqrd >= (pt - segment_closest_point).cast().squaredNorm()) { + const Point &other = thiss[poly_idx][(point_idx + 1) % pathlen]; + const Vec2i64 vec = (LinearAlg2D::pointIsLeftOfLine(other, segment.a, segment.b) > 0 ? segment.b - segment.a : segment.a - segment.b).cast(); + assert(Slic3r::sqr(double(vec.x())) < double(std::numeric_limits::max())); + assert(Slic3r::sqr(double(vec.y())) < double(std::numeric_limits::max())); + const int64_t len = vec.norm(); + pt.x() += (-vec.y() * move_dist) / len; + pt.y() += (vec.x() * move_dist) / len; + } + } + } + } + + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); +} + +/*! + * Removes overlapping consecutive line segments which don't delimit a positive area. + */ +void removeDegenerateVerts(Polygons &thiss) +{ + for (unsigned int poly_idx = 0; poly_idx < thiss.size(); poly_idx++) { + Polygon &poly = thiss[poly_idx]; + Polygon result; + + auto isDegenerate = [](const Point &last, const Point &now, const Point &next) { + Vec2i64 last_line = (now - last).cast(); + Vec2i64 next_line = (next - now).cast(); + return last_line.dot(next_line) == -1 * last_line.norm() * next_line.norm(); + }; + bool isChanged = false; + for (unsigned int idx = 0; idx < poly.size(); idx++) { + const Point &last = (result.size() == 0) ? poly.back() : result.back(); + if (idx + 1 == poly.size() && result.size() == 0) { break; } + Point &next = (idx + 1 == poly.size()) ? result[0] : poly[idx + 1]; + if (isDegenerate(last, poly[idx], next)) { // lines are in the opposite direction + // don't add vert to the result + isChanged = true; + while (result.size() > 1 && isDegenerate(result[result.size() - 2], result.back(), next)) { result.points.pop_back(); } + } else { + result.points.emplace_back(poly[idx]); + } + } + + if (isChanged) { + if (result.size() > 2) { + poly = result; + } else { + thiss.erase(thiss.begin() + poly_idx); + poly_idx--; // effectively the next iteration has the same poly_idx (referring to a new poly which is not yet processed) + } + } + } +} + +void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool remove_holes) +{ + auto to_path = [](const Polygon &poly) -> ClipperLib::Path { + ClipperLib::Path out; + for (const Point &pt : poly.points) + out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y())); + return out; + }; + + auto new_end = thiss.end(); + if(remove_holes) + { + for(auto it = thiss.begin(); it < new_end; it++) + { + // All polygons smaller than target are removed by replacing them with a polygon from the back of the vector + if(fabs(ClipperLib::Area(to_path(*it))) < min_area_size) + { + new_end--; + *it = std::move(*new_end); + it--; // wind back the iterator such that the polygon just swaped in is checked next + } + } + } + else + { + // For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes + std::vector small_holes; + for(auto it = thiss.begin(); it < new_end; it++) { + double area = ClipperLib::Area(to_path(*it)); + if (fabs(area) < min_area_size) + { + if(area >= 0) + { + new_end--; + if(it < new_end) { + std::swap(*new_end, *it); + it--; + } + else + { // Don't self-swap the last Path + break; + } + } + else + { + small_holes.push_back(*it); + } + } + } + + // Removes small holes that have their first point inside one of the removed outlines + // Iterating in reverse ensures that unprocessed small holes won't be moved + const auto removed_outlines_start = new_end; + for(auto hole_it = small_holes.rbegin(); hole_it < small_holes.rend(); hole_it++) + { + for(auto outline_it = removed_outlines_start; outline_it < thiss.end() ; outline_it++) + { + if(Polygon(*outline_it).contains(*hole_it->begin())) { + new_end--; + *hole_it = std::move(*new_end); + break; + } + } + } + } + thiss.resize(new_end-thiss.begin()); +} + +void removeColinearEdges(Polygon &poly, const double max_deviation_angle) +{ + // TODO: Can be made more efficient (for example, use pointer-types for process-/skip-indices, so we can swap them without copy). + size_t num_removed_in_iteration = 0; + do { + num_removed_in_iteration = 0; + std::vector process_indices(poly.points.size(), true); + + bool go = true; + while (go) { + go = false; + + const auto &rpath = poly; + const size_t pathlen = rpath.size(); + if (pathlen <= 3) + return; + + std::vector skip_indices(poly.points.size(), false); + + Polygon new_path; + for (size_t point_idx = 0; point_idx < pathlen; ++point_idx) { + // Don't iterate directly over process-indices, but do it this way, because there are points _in_ process-indices that should nonetheless + // be skipped: + if (!process_indices[point_idx]) { + new_path.points.push_back(rpath[point_idx]); + continue; + } + + // Should skip the last point for this iteration if the old first was removed (which can be seen from the fact that the new first was skipped): + if (point_idx == (pathlen - 1) && skip_indices[0]) { + skip_indices[new_path.size()] = true; + go = true; + new_path.points.push_back(rpath[point_idx]); + break; + } + + const Point &prev = rpath[(point_idx - 1 + pathlen) % pathlen]; + const Point &pt = rpath[point_idx]; + const Point &next = rpath[(point_idx + 1) % pathlen]; + + float angle = LinearAlg2D::getAngleLeft(prev, pt, next); // [0 : 2 * pi] + if (angle >= float(M_PI)) { angle -= float(M_PI); } // map [pi : 2 * pi] to [0 : pi] + + // Check if the angle is within limits for the point to 'make sense', given the maximum deviation. + // If the angle indicates near-parallel segments ignore the point 'pt' + if (angle > max_deviation_angle && angle < M_PI - max_deviation_angle) { + new_path.points.push_back(pt); + } else if (point_idx != (pathlen - 1)) { + // Skip the next point, since the current one was removed: + skip_indices[new_path.size()] = true; + go = true; + new_path.points.push_back(next); + ++point_idx; + } + } + poly = new_path; + num_removed_in_iteration += pathlen - poly.points.size(); + + process_indices.clear(); + process_indices.insert(process_indices.end(), skip_indices.begin(), skip_indices.end()); + } + } while (num_removed_in_iteration > 0); +} + +void removeColinearEdges(Polygons &thiss, const double max_deviation_angle = 0.0005) +{ + for (int p = 0; p < int(thiss.size()); p++) { + removeColinearEdges(thiss[p], max_deviation_angle); + if (thiss[p].size() < 3) { + thiss.erase(thiss.begin() + p); + p--; + } + } +} + +const VariableWidthPaths& WallToolPaths::generate() +{ + const coord_t smallest_segment = Slic3r::Arachne::meshfix_maximum_resolution; + const coord_t allowed_distance = Slic3r::Arachne::meshfix_maximum_deviation; + const coord_t epsilon_offset = (allowed_distance / 2) - 1; + const double transitioning_angle = Geometry::deg2rad(this->print_config.wall_transition_angle.value); + constexpr coord_t discretization_step_size = scaled(0.8); + + // Simplify outline for boost::voronoi consumption. Absolutely no self intersections or near-self intersections allowed: + // TODO: Open question: Does this indeed fix all (or all-but-one-in-a-million) cases for manifold but otherwise possibly complex polygons? + Polygons prepared_outline = offset(offset(offset(outline, -epsilon_offset), epsilon_offset * 2), -epsilon_offset); + simplify(prepared_outline, smallest_segment, allowed_distance); + fixSelfIntersections(epsilon_offset, prepared_outline); + removeDegenerateVerts(prepared_outline); + removeColinearEdges(prepared_outline, 0.005); + // Removing collinear edges may introduce self intersections, so we need to fix them again + fixSelfIntersections(epsilon_offset, prepared_outline); + removeDegenerateVerts(prepared_outline); + removeSmallAreas(prepared_outline, small_area_length * small_area_length, false); + + if (area(prepared_outline) > 0) + { + const coord_t wall_transition_length = scaled(this->print_config.wall_transition_length.value); + const double wall_split_middle_threshold = this->print_config.wall_split_middle_threshold.value / 100.; // For an uneven nr. of lines: When to split the middle wall into two. + const double wall_add_middle_threshold = this->print_config.wall_add_middle_threshold.value / 100.; // For an even nr. of lines: When to add a new middle in between the innermost two walls. + const int wall_distribution_count = this->print_config.wall_distribution_count.value; + const size_t max_bead_count = (inset_count < std::numeric_limits::max() / 2) ? 2 * inset_count : std::numeric_limits::max(); + const auto beading_strat = BeadingStrategyFactory::makeStrategy + ( + strategy_type, + bead_width_0, + bead_width_x, + wall_transition_length, + transitioning_angle, + print_thin_walls, + min_bead_width, + min_feature_size, + wall_split_middle_threshold, + wall_add_middle_threshold, + max_bead_count, + wall_0_inset, + wall_distribution_count + ); + const coord_t transition_filter_dist = scaled(this->print_config.wall_transition_filter_distance.value); + SkeletalTrapezoidation wall_maker + ( + prepared_outline, + *beading_strat, + beading_strat->getTransitioningAngle(), + discretization_step_size, + transition_filter_dist, + wall_transition_length + ); + wall_maker.generateToolpaths(toolpaths); + computeInnerContour(); + } + simplifyToolPaths(toolpaths); + + removeEmptyToolPaths(toolpaths); + assert(std::is_sorted(toolpaths.cbegin(), toolpaths.cend(), + [](const VariableWidthLines& l, const VariableWidthLines& r) + { + return l.front().inset_idx < r.front().inset_idx; + }) && "WallToolPaths should be sorted from the outer 0th to inner_walls"); + toolpaths_generated = true; + return toolpaths; +} + +void WallToolPaths::simplifyToolPaths(VariableWidthPaths& toolpaths/*, const Settings& settings*/) +{ + for (size_t toolpaths_idx = 0; toolpaths_idx < toolpaths.size(); ++toolpaths_idx) + { + const int64_t maximum_resolution = Slic3r::Arachne::meshfix_maximum_resolution; + const int64_t maximum_deviation = Slic3r::Arachne::meshfix_maximum_deviation; + const int64_t maximum_extrusion_area_deviation = Slic3r::Arachne::meshfix_maximum_extrusion_area_deviation; // unit: μm² + for (auto& line : toolpaths[toolpaths_idx]) + { + line.simplify(maximum_resolution * maximum_resolution, maximum_deviation * maximum_deviation, maximum_extrusion_area_deviation); + } + } +} + +const VariableWidthPaths& WallToolPaths::getToolPaths() +{ + if (!toolpaths_generated) + { + return generate(); + } + return toolpaths; +} + +void WallToolPaths::computeInnerContour() +{ + //We'll remove all 0-width paths from the original toolpaths and store them separately as polygons. + VariableWidthPaths actual_toolpaths; + actual_toolpaths.reserve(toolpaths.size()); //A bit too much, but the correct order of magnitude. + VariableWidthPaths contour_paths; + contour_paths.reserve(toolpaths.size() / inset_count); + std::partition_copy(toolpaths.begin(), toolpaths.end(), std::back_inserter(actual_toolpaths), std::back_inserter(contour_paths), + [](const VariableWidthLines& path) + { + for(const ExtrusionLine& line : path) + { + for(const ExtrusionJunction& junction : line.junctions) + { + return junction.w != 0; //On the first actual junction, decide: If it's got 0 width, this is a contour. Otherwise it is an actual toolpath. + } + } + return true; //No junctions with any vertices? Classify it as a toolpath then. + }); + if (! actual_toolpaths.empty()) + { + toolpaths = std::move(actual_toolpaths); //Filtered out the 0-width paths. + } + else + { + toolpaths.clear(); + } + + //Now convert the contour_paths to Polygons to denote the inner contour of the walled areas. + inner_contour.clear(); + + //We're going to have to stitch these paths since not all walls may be closed contours. + //Since these walls have 0 width they should theoretically be closed. But there may be rounding errors. + const coord_t minimum_line_width = bead_width_0 / 2; + stitchContours(contour_paths, minimum_line_width, inner_contour); + + //The output walls from the skeletal trapezoidation have no known winding order, especially if they are joined together from polylines. + //They can be in any direction, clockwise or counter-clockwise, regardless of whether the shapes are positive or negative. + //To get a correct shape, we need to make the outside contour positive and any holes inside negative. + //This can be done by applying the even-odd rule to the shape. This rule is not sensitive to the winding order of the polygon. + //The even-odd rule would be incorrect if the polygon self-intersects, but that should never be generated by the skeletal trapezoidation. + inner_contour = union_(inner_contour); +} + +const Polygons& WallToolPaths::getInnerContour() +{ + if (!toolpaths_generated && inset_count > 0) + { + generate(); + } + else if(inset_count == 0) + { + return outline; + } + return inner_contour; +} + +bool WallToolPaths::removeEmptyToolPaths(VariableWidthPaths& toolpaths) +{ + toolpaths.erase(std::remove_if(toolpaths.begin(), toolpaths.end(), [](const VariableWidthLines& lines) + { + return lines.empty(); + }), toolpaths.end()); + return toolpaths.empty(); +} + +void WallToolPaths::stitchContours(const VariableWidthPaths& input, const coord_t stitch_distance, Polygons& output) +{ + // Create a bucket grid to find endpoints that are close together. + struct ExtrusionLineStartLocator + { + const Point *operator()(const ExtrusionLine *line) { return &line->junctions.front().p; } + }; + struct ExtrusionLineEndLocator + { + const Point *operator()(const ExtrusionLine *line) { return &line->junctions.back().p; } + }; + + // Only find endpoints closer than minimum_line_width, so we can't ever accidentally make crossing contours. + ClosestPointInRadiusLookup line_starts(coord_t(stitch_distance * std::sqrt(2.))); + ClosestPointInRadiusLookup line_ends(coord_t(stitch_distance * std::sqrt(2.))); + + auto get_search_bbox = [](const Point &pt, const coord_t radius) -> BoundingBox { + const Point min_grid((pt - Point(radius, radius)) / radius); + const Point max_grid((pt + Point(radius, radius)) / radius); + return {min_grid * radius, (max_grid + Point(1, 1)) * radius - Point(1, 1)}; + }; + + for (const VariableWidthLines &path : input) { + for (const ExtrusionLine &line : path) { + line_starts.insert(&line); + line_ends.insert(&line); + } + } + //Then go through all lines and construct chains of polylines if the endpoints are nearby. + std::unordered_set processed_lines; //Track which lines were already processed to not process them twice. + for(const VariableWidthLines& path : input) + { + for(const ExtrusionLine& line : path) + { + if(processed_lines.find(&line) != processed_lines.end()) //We already added this line before. It got added as a nearby line. + { + continue; + } + //We'll create a chain of polylines that get joined together. We can add polylines on both ends! + std::deque chain; + std::deque is_reversed; //Lines could need to be inserted in reverse. Must coincide with the `chain` deque. + const ExtrusionLine* nearest = &line; //At every iteration, add the polyline that joins together most closely. + bool nearest_reverse = false; //Whether the next line to insert must be inserted in reverse. + bool nearest_before = false; //Whether the next line to insert must be inserted in the front of the chain. + while(nearest) + { + if(processed_lines.find(nearest) != processed_lines.end()) + { + break; //Looping. This contour is already processed. + } + processed_lines.insert(nearest); + if(nearest_before) + { + chain.push_front(nearest); + is_reversed.push_front(nearest_reverse); + } + else + { + chain.push_back(nearest); + is_reversed.push_back(nearest_reverse); + } + + //Find any nearby lines to attach. Look on both ends of our current chain and find both ends of polylines. + const Point chain_start = is_reversed.front() ? chain.front()->junctions.back().p : chain.front()->junctions.front().p; + const Point chain_end = is_reversed.back() ? chain.back()->junctions.front().p : chain.back()->junctions.back().p; + + std::vector> starts_near_start = line_starts.find_all(chain_start); + std::vector> ends_near_start = line_ends.find_all(chain_start); + std::vector> starts_near_end = line_starts.find_all(chain_end); + std::vector> ends_near_end = line_ends.find_all(chain_end); + + nearest = nullptr; + int64_t nearest_dist2 = std::numeric_limits::max(); + for (const auto &candidate_ptr : starts_near_start) { + const ExtrusionLine* candidate = *candidate_ptr.first; + if(processed_lines.find(candidate) != processed_lines.end()) + continue; //Already processed this line before. It's linked to something else. + + if (const int64_t dist2 = (candidate->junctions.front().p - chain_start).cast().squaredNorm(); dist2 < nearest_dist2) { + nearest = candidate; + nearest_dist2 = dist2; + nearest_reverse = true; + nearest_before = true; + } + } + for (const auto &candidate_ptr : ends_near_start) { + const ExtrusionLine* candidate = *candidate_ptr.first; + if(processed_lines.find(candidate) != processed_lines.end()) + continue; + + if (const int64_t dist2 = (candidate->junctions.back().p - chain_start).cast().squaredNorm(); dist2 < nearest_dist2) { + nearest = candidate; + nearest_dist2 = dist2; + nearest_reverse = false; + nearest_before = true; + } + } + for (const auto &candidate_ptr : starts_near_end) { + const ExtrusionLine* candidate = *candidate_ptr.first; + if(processed_lines.find(candidate) != processed_lines.end()) + continue; //Already processed this line before. It's linked to something else. + + if (const int64_t dist2 = (candidate->junctions.front().p - chain_start).cast().squaredNorm(); dist2 < nearest_dist2) { + nearest = candidate; + nearest_dist2 = dist2; + nearest_reverse = false; + nearest_before = false; + } + } + for (const auto &candidate_ptr : ends_near_end) { + const ExtrusionLine* candidate = *candidate_ptr.first; + if (processed_lines.find(candidate) != processed_lines.end()) + continue; + + if (const int64_t dist2 = (candidate->junctions.back().p - chain_start).cast().squaredNorm(); dist2 < nearest_dist2) { + nearest = candidate; + nearest_dist2 = dist2; + nearest_reverse = true; + nearest_before = false; + } + } + } + + //Now serialize the entire chain into one polygon. + output.emplace_back(); + for (size_t i = 0; i < chain.size(); ++i) { + if(!is_reversed[i]) + for (const ExtrusionJunction& junction : chain[i]->junctions) + output.back().points.emplace_back(junction.p); + else + for (auto junction = chain[i]->junctions.rbegin(); junction != chain[i]->junctions.rend(); ++junction) + output.back().points.emplace_back(junction->p); + } + } + } +} + +size_t getOuterRegionId(const Arachne::VariableWidthPaths& toolpaths, size_t& out_max_region_id) +{ + // Polygons show up here one by one, so there are always only a) the outer lines and b) the lines that are part of the holes. + // Therefore, the outer-regions' lines will always have the region-id that is larger then all of the other ones. + + // First, build the bounding boxes: + std::map region_ids_to_bboxes; // Use a sorted map, ordered by region_id, so that we can find the largest region_id quickly. + for (const Arachne::VariableWidthLines &path : toolpaths) { + for (const Arachne::ExtrusionLine &line : path) { + BoundingBox &aabb = + region_ids_to_bboxes[line.region_id]; // Empty AABBs are default initialized when region_ids are encountered for the first time. + for (const auto &junction : line.junctions) aabb.merge(junction.p); + } + } + + // Then, the largest of these will be the one that's needed for the outer region, the others' all belong to hole regions: + BoundingBox outer_bbox; + size_t outer_region_id = 0; // Region-ID 0 is reserved for 'None'. + for (const auto ®ion_id_bbox_pair : region_ids_to_bboxes) { + if (region_id_bbox_pair.second.contains(outer_bbox)) { + outer_bbox = region_id_bbox_pair.second; + outer_region_id = region_id_bbox_pair.first; + } + } + + // Maximum Region-ID (using the ordering of the map) + out_max_region_id = region_ids_to_bboxes.empty() ? 0 : region_ids_to_bboxes.rbegin()->first; + return outer_region_id; +} + +Arachne::BinJunctions variableWidthPathToBinJunctions(const Arachne::VariableWidthPaths& toolpaths, const bool pack_regions_by_inset, const bool center_last, std::set* p_bins_with_index_zero_insets) +{ + // Find the largest inset-index: + size_t max_inset_index = 0; + for (const Arachne::VariableWidthLines &path : toolpaths) + max_inset_index = std::max(path.front().inset_idx, max_inset_index); + + // Find which regions are associated with the outer-outer walls (which region is the one the rest is holes inside of): + size_t max_region_id = 0; + const size_t outer_region_id = getOuterRegionId(toolpaths, max_region_id); + + //Since we're (optionally!) splitting off in the outer and inner regions, it may need twice as many bins as inset-indices. + //Add two extra bins for the center-paths, if they need to be stored separately. One bin for inner and one for outer walls. + const size_t max_bin = (pack_regions_by_inset ? (max_region_id * 2) + 2 : (max_inset_index + 1) * 2) + center_last * 2; + Arachne::BinJunctions insets(max_bin + 1); + for (const Arachne::VariableWidthLines &path : toolpaths) { + if (path.empty()) // Don't bother printing these. + continue; + + const size_t inset_index = path.front().inset_idx; + + // Convert list of extrusion lines to vectors of extrusion junctions, and add those to the binned insets. + for (const Arachne::ExtrusionLine &line : path) { + // Sort into the right bin, ... + size_t bin_index; + const bool in_hole_region = line.region_id != outer_region_id && line.region_id != 0; + if (center_last && line.is_odd) { + bin_index = inset_index > 0; + } else if (pack_regions_by_inset) { + bin_index = std::min(inset_index, static_cast(1)) + 2 * (in_hole_region ? line.region_id : 0) + center_last * 2; + } else { + bin_index = inset_index + (in_hole_region ? (max_inset_index + 1) : 0) + center_last * 2; + } + insets[bin_index].emplace_back(line.junctions.begin(), line.junctions.end()); + + // Collect all bins that have zero-inset indices in them, if needed: + if (inset_index == 0 && p_bins_with_index_zero_insets != nullptr) + p_bins_with_index_zero_insets->insert(bin_index); + } + } + return insets; +} + +BinJunctions WallToolPaths::getBinJunctions(std::set &bins_with_index_zero_insets) +{ + if (!toolpaths_generated) + generate(); + + return variableWidthPathToBinJunctions(toolpaths, true, true, &bins_with_index_zero_insets); +} + +} // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp new file mode 100644 index 000000000..6b2f0bed9 --- /dev/null +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -0,0 +1,130 @@ +// Copyright (c) 2020 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef CURAENGINE_WALLTOOLPATHS_H +#define CURAENGINE_WALLTOOLPATHS_H + +#include + +#include "BeadingStrategy/BeadingStrategyFactory.hpp" +#include "utils/ExtrusionLine.hpp" +#include "../Polygon.hpp" +#include "../PrintConfig.hpp" + +namespace Slic3r::Arachne +{ + +constexpr bool fill_outline_gaps = true; +constexpr coord_t meshfix_maximum_resolution = scaled(0.5); +constexpr coord_t meshfix_maximum_deviation = scaled(0.025); +constexpr coord_t meshfix_maximum_extrusion_area_deviation = scaled(2.); + +class WallToolPaths +{ +public: + /*! + * A class that creates the toolpaths given an outline, nominal bead width and maximum amount of walls + * \param outline An outline of the area in which the ToolPaths are to be generated + * \param nominal_bead_width The nominal bead width used in the generation of the toolpaths + * \param inset_count The maximum number of parallel extrusion lines that make up the wall + * \param wall_0_inset How far to inset the outer wall, to make it adhere better to other walls. + */ + WallToolPaths(const Polygons& outline, const coord_t nominal_bead_width, const size_t inset_count, const coord_t wall_0_inset, const PrintConfig &print_config); + + /*! + * A class that creates the toolpaths given an outline, nominal bead width and maximum amount of walls + * \param outline An outline of the area in which the ToolPaths are to be generated + * \param bead_width_0 The bead width of the first wall used in the generation of the toolpaths + * \param bead_width_x The bead width of the inner walls used in the generation of the toolpaths + * \param inset_count The maximum number of parallel extrusion lines that make up the wall + * \param wall_0_inset How far to inset the outer wall, to make it adhere better to other walls. + */ + WallToolPaths(const Polygons& outline, const coord_t bead_width_0, const coord_t bead_width_x, const size_t inset_count, const coord_t wall_0_inset, const PrintConfig &print_config); + + /*! + * Generates the Toolpaths + * \return A reference to the newly create ToolPaths + */ + const VariableWidthPaths& generate(); + + /*! + * Gets the toolpaths, if this called before \p generate() it will first generate the Toolpaths + * \return a reference to the toolpaths + */ + const VariableWidthPaths& getToolPaths(); + + BinJunctions getBinJunctions(std::set &bins_with_index_zero_insets); + + /*! + * Compute the inner contour of the walls. This contour indicates where the walled area ends and its infill begins. + * The inside can then be filled, e.g. with skin/infill for the walls of a part, or with a pattern in the case of + * infill with extra infill walls. + */ + void computeInnerContour(); + + /*! + * Gets the inner contour of the area which is inside of the generated tool + * paths. + * + * If the walls haven't been generated yet, this will lazily call the + * \p generate() function to generate the walls with variable width. + * The resulting polygon will snugly match the inside of the variable-width + * walls where the walls get limited by the LimitedBeadingStrategy to a + * maximum wall count. + * If there are no walls, the outline will be returned. + * \return The inner contour of the generated walls. + */ + const Polygons& getInnerContour(); + + /*! + * Removes empty paths from the toolpaths + * \param toolpaths the VariableWidthPaths generated with \p generate() + * \return true if there are still paths left. If all toolpaths were removed it returns false + */ + static bool removeEmptyToolPaths(VariableWidthPaths& toolpaths); + + /*! + * Stitches toolpaths together to form contours. + * + * All toolpaths are used. Paths that are not closed will get closed in the + * output by virtue of becoming polygons. As such, the input is expected to + * consist of almost completely closed contours, which may be split up into + * different polylines. + * This function combines those polylines into the polygons they are + * probably intended to depict. + * \param input The paths to stitch together. + * \param stitch_distance Any endpoints closer than this distance can be + * stitched together. An additional line segment will bridge the gap. + * \param output Where to store the output polygons. + */ + static void stitchContours(const VariableWidthPaths& input, const coord_t stitch_distance, Polygons& output) ; + +protected: + /*! + * Simplifies the variable-width toolpaths by calling the simplify on every line in the toolpath using the provided + * settings. + * \param settings The settings as provided by the user + * \return + */ + static void simplifyToolPaths(VariableWidthPaths& toolpaths/*, const Settings& settings*/); + +private: + const Polygons& outline; //; //; //; // + +#include "ExtrusionLine.hpp" +#include "linearAlg2D.hpp" + +namespace Slic3r::Arachne +{ + +ExtrusionLine::ExtrusionLine(const size_t inset_idx, const bool is_odd, const size_t region_id) +: inset_idx(inset_idx) +, is_odd(is_odd) +, region_id(region_id) +{} + +coord_t ExtrusionLine::getLength() const +{ + if (junctions.empty()) + { + return 0; + } + coord_t len = 0; + ExtrusionJunction prev = junctions.front(); + for (const ExtrusionJunction& next : junctions) + { + len += (next.p - prev.p).cast().norm(); + prev = next; + } + return len; +} + +coord_t ExtrusionLine::getMinimalWidth() const +{ + return std::min_element(junctions.cbegin(), junctions.cend(), + [](const ExtrusionJunction& l, const ExtrusionJunction& r) + { + return l.w < r.w; + })->w; +} + +void ExtrusionLine::appendJunctionsTo(LineJunctions& result) const +{ + result.insert(result.end(), junctions.begin(), junctions.end()); +} + +void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared, const int64_t maximum_extrusion_area_deviation) +{ + if (junctions.size() <= 3) + { + return; + } + + /* ExtrusionLines are treated as (open) polylines, so in case an ExtrusionLine is actually a closed polygon, its + * starting and ending points will be equal (or almost equal). Therefore, the simplification of the ExtrusionLine + * should not touch the first and last points. As a result, start simplifying from point at index 1. + * */ + std::vector new_junctions; + // Starting junction should always exist in the simplified path + new_junctions.emplace_back(junctions.front()); + + /* Initially, previous_previous is always the same as previous because, for open ExtrusionLines the last junction + * cannot be taken into consideration when checking the points at index 1. For closed ExtrusionLines, the first and + * last junctions are anyway the same. + * */ + ExtrusionJunction previous_previous = junctions.front(); + ExtrusionJunction previous = junctions.front(); + + /* When removing a vertex, we check the height of the triangle of the area + being removed from the original polygon by the simplification. However, + when consecutively removing multiple vertices the height of the previously + removed vertices w.r.t. the shortcut path changes. + In order to not recompute the new height value of previously removed + vertices we compute the height of a representative triangle, which covers + the same amount of area as the area being cut off. We use the Shoelace + formula to accumulate the area under the removed segments. This works by + computing the area in a 'fan' where each of the blades of the fan go from + the origin to one of the segments. While removing vertices the area in + this fan accumulates. By subtracting the area of the blade connected to + the short-cutting segment we obtain the total area of the cutoff region. + From this area we compute the height of the representative triangle using + the standard formula for a triangle area: A = .5*b*h + */ + const ExtrusionJunction& initial = junctions.at(1); + int64_t accumulated_area_removed = int64_t(previous.p.x()) * int64_t(initial.p.y()) - int64_t(previous.p.y()) * int64_t(initial.p.x()); // Twice the Shoelace formula for area of polygon per line segment. + + for (size_t point_idx = 1; point_idx < junctions.size() - 1; point_idx++) + { + const ExtrusionJunction& current = junctions[point_idx]; + + // Spill over in case of overflow, unless the [next] vertex will then be equal to [previous]. + const bool spill_over = point_idx + 1 == junctions.size() && new_junctions.size() > 1; + ExtrusionJunction& next = spill_over ? new_junctions[0] : junctions[point_idx + 1]; + + const int64_t removed_area_next = int64_t(current.p.x()) * int64_t(next.p.y()) - int64_t(current.p.y()) * int64_t(next.p.x()); // Twice the Shoelace formula for area of polygon per line segment. + const int64_t negative_area_closing = int64_t(next.p.x()) * int64_t(previous.p.y()) - int64_t(next.p.y()) * int64_t(previous.p.x()); // Area between the origin and the short-cutting segment + accumulated_area_removed += removed_area_next; + + const int64_t length2 = (current - previous).cast().squaredNorm(); + if (length2 < scaled(0.025)) + { + // We're allowed to always delete segments of less than 5 micron. The width in this case doesn't matter that much. + continue; + } + + const int64_t area_removed_so_far = accumulated_area_removed + negative_area_closing; // Close the shortcut area polygon + const int64_t base_length_2 = (next - previous).cast().squaredNorm(); + + if (base_length_2 == 0) // Two line segments form a line back and forth with no area. + { + continue; // Remove the junction (vertex). + } + //We want to check if the height of the triangle formed by previous, current and next vertices is less than allowed_error_distance_squared. + //1/2 L = A [actual area is half of the computed shoelace value] // Shoelace formula is .5*(...) , but we simplify the computation and take out the .5 + //A = 1/2 * b * h [triangle area formula] + //L = b * h [apply above two and take out the 1/2] + //h = L / b [divide by b] + //h^2 = (L / b)^2 [square it] + //h^2 = L^2 / b^2 [factor the divisor] + const int64_t height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2)); + coord_t weighted_average_width; + const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next, weighted_average_width); + if ((height_2 <= 1 //Almost exactly colinear (barring rounding errors). + && Line::distance_to_infinite(current.p, previous.p, next.p) <= 1.) // Make sure that height_2 is not small because of cancellation of positive and negative areas + // We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed + && extrusion_area_error <= maximum_extrusion_area_deviation) + { + // Adjust the width of the entire P-N line as a weighted average of the widths of the P-C and C-N lines and + // then remove the current junction (vertex). + next.w = weighted_average_width; + continue; + } + + if (length2 < smallest_line_segment_squared + && height_2 <= allowed_error_distance_squared) // Removing the junction (vertex) doesn't introduce too much error. + { + const int64_t next_length2 = (current - next).cast().squaredNorm(); + if (next_length2 > smallest_line_segment_squared) + { + // Special case; The next line is long. If we were to remove this, it could happen that we get quite noticeable artifacts. + // We should instead move this point to a location where both edges are kept and then remove the previous point that we wanted to keep. + // By taking the intersection of these two lines, we get a point that preserves the direction (so it makes the corner a bit more pointy). + // We just need to be sure that the intersection point does not introduce an artifact itself. + Point intersection_point; + bool has_intersection = Line(previous_previous.p, previous.p).intersection_infinite(Line(current.p, next.p), &intersection_point); + if (!has_intersection + || Line::distance_to_infinite_squared(intersection_point, previous.p, current.p) > double(allowed_error_distance_squared) + || (intersection_point - previous.p).cast().squaredNorm() > smallest_line_segment_squared // The intersection point is way too far from the 'previous' + || (intersection_point - next.p).cast().squaredNorm() > smallest_line_segment_squared) // and 'next' points, so it shouldn't replace 'current' + { + // We can't find a better spot for it, but the size of the line is more than 5 micron. + // So the only thing we can do here is leave it in... + } + else + { + // New point seems like a valid one. + const ExtrusionJunction new_to_add = ExtrusionJunction(intersection_point, current.w, current.perimeter_index, current.region_id); + // If there was a previous point added, remove it. + if(!new_junctions.empty()) + { + new_junctions.pop_back(); + previous = previous_previous; + } + + // The junction (vertex) is replaced by the new one. + accumulated_area_removed = removed_area_next; // So that in the next iteration it's the area between the origin, [previous] and [current] + previous_previous = previous; + previous = new_to_add; // Note that "previous" is only updated if we don't remove the junction (vertex). + new_junctions.push_back(new_to_add); + continue; + } + } + else + { + continue; // Remove the junction (vertex). + } + } + // The junction (vertex) isn't removed. + accumulated_area_removed = removed_area_next; // So that in the next iteration it's the area between the origin, [previous] and [current] + previous_previous = previous; + previous = current; // Note that "previous" is only updated if we don't remove the junction (vertex). + new_junctions.push_back(current); + } + + // Ending junction (vertex) should always exist in the simplified path + new_junctions.emplace_back(junctions.back()); + + /* In case this is a closed polygon (instead of a poly-line-segments), the invariant that the first and last points are the same should be enforced. + * Since one of them didn't move, and the other can't have been moved further than the constraints, if originally equal, they can simply be equated. + */ + if ((junctions.front().p - junctions.back().p).cast().squaredNorm() == 0) + { + new_junctions.back().p = junctions.front().p; + } + + junctions = new_junctions; +} + +int64_t ExtrusionLine::calculateExtrusionAreaDeviationError(ExtrusionJunction A, ExtrusionJunction B, ExtrusionJunction C, coord_t& weighted_average_width) +{ + /* + * A B C A C + * --------------- ************** + * | | ------------------------------------------ + * | |--------------------------| B removed | |***************************| + * | | | ---------> | | | + * | |--------------------------| | |***************************| + * | | ------------------------------------------ + * --------------- ^ ************** + * ^ C.w ^ + * B.w new_width = weighted_average_width + * + * + * ******** denote the total extrusion area deviation error in the consecutive segments as a result of using the + * weighted-average width for the entire extrusion line. + * + * */ + const int64_t ab_length = (B - A).cast().norm(); + const int64_t bc_length = (C - B).cast().norm(); + const int64_t width_diff = llabs(B.w - C.w); + if (width_diff > 1) + { + // Adjust the width only if there is a difference, or else the rounding errors may produce the wrong + // weighted average value. + assert(((int64_t(ab_length) * int64_t(B.w) + int64_t(bc_length) * int64_t(C.w)) / (C - A).cast().norm()) <= std::numeric_limits::max()); + weighted_average_width = (ab_length * int64_t(B.w) + bc_length * int64_t(C.w)) / (C - A).cast().norm(); + assert((double(llabs(B.w - weighted_average_width)) * double(ab_length) + double(llabs(C.w - weighted_average_width)) * double(bc_length)) <= double(std::numeric_limits::max())); + return int64_t(llabs(B.w - weighted_average_width)) * ab_length + int64_t(llabs(C.w - weighted_average_width)) * bc_length; + } + else + { + // If the width difference is very small, then select the width of the segment that is longer + weighted_average_width = ab_length > bc_length ? B.w : C.w; + assert((int64_t(width_diff) * int64_t(bc_length)) <= std::numeric_limits::max()); + assert((int64_t(width_diff) * int64_t(ab_length)) <= std::numeric_limits::max()); + return ab_length > bc_length ? width_diff * bc_length : width_diff * ab_length; + } +} + +} diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp new file mode 100644 index 000000000..0f6e89497 --- /dev/null +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -0,0 +1,159 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + + +#ifndef UTILS_EXTRUSION_LINE_H +#define UTILS_EXTRUSION_LINE_H + +#include "ExtrusionJunction.hpp" +#include "../../Polyline.hpp" +#include "../../Polygon.hpp" + +namespace Slic3r { +class ThickPolyline; +} + +namespace Slic3r::Arachne +{ + +/*! + * Represents a polyline (not just a line) that is to be extruded with variable + * line width. + * + * This polyline is a sequence of \ref ExtrusionJunction, with a bit of metadata + * about which inset it represents. + */ +struct ExtrusionLine +{ + /*! + * Which inset this path represents, counted from the outside inwards. + * + * The outer wall has index 0. + */ + size_t inset_idx; + + /*! + * If a thin piece needs to be printed with an odd number of walls (e.g. 5 + * walls) then there will be one wall in the middle that is not a loop. This + * field indicates whether this path is such a line through the middle, that + * has no companion line going back on the other side and is not a closed + * loop. + */ + bool is_odd; + + /*! + * Which region this line is part of. A solid polygon without holes has only one region. + * A polygon with holes has 2. Disconnected parts of the polygon are also separate regions. + * Will be 0 if no region was given. + */ + size_t region_id; + + /*! + * The list of vertices along which this path runs. + * + * Each junction has a width, making this path a variable-width path. + */ + std::vector junctions; + + ExtrusionLine(const size_t inset_idx, const bool is_odd, const size_t region_id = 0); + + /*! + * Sum the total length of this path. + */ + coord_t getLength() const; + + /*! + * Get the minimal width of this path + */ + coord_t getMinimalWidth() const; + + /*! + * Export the included junctions as vector. + */ + void appendJunctionsTo(LineJunctions& result) const; + + /*! + * Removes vertices of the ExtrusionLines to make sure that they are not too high + * resolution. + * + * This removes junctions which are connected to line segments that are shorter + * than the `smallest_line_segment`, unless that would introduce a deviation + * in the contour of more than `allowed_error_distance`. + * + * Criteria: + * 1. Never remove a junction if either of the connected segments is larger than \p smallest_line_segment + * 2. Never remove a junction if the distance between that junction and the final resulting polygon would be higher + * than \p allowed_error_distance + * 3. The direction of segments longer than \p smallest_line_segment always + * remains unaltered (but their end points may change if it is connected to + * a small segment) + * 4. Never remove a junction if it has a distinctively different width than the next junction, as this can + * introduce unwanted irregularities on the wall widths. + * + * Simplify uses a heuristic and doesn't necessarily remove all removable + * vertices under the above criteria, but simplify may never violate these + * criteria. Unless the segments or the distance is smaller than the + * rounding error of 5 micron. + * + * Vertices which introduce an error of less than 5 microns are removed + * anyway, even if the segments are longer than the smallest line segment. + * This makes sure that (practically) co-linear line segments are joined into + * a single line segment. + * \param smallest_line_segment Maximal length of removed line segments. + * \param allowed_error_distance If removing a vertex introduces a deviation + * from the original path that is more than this distance, the vertex may + * not be removed. + * \param maximum_extrusion_area_deviation The maximum extrusion area deviation allowed when removing intermediate + * junctions from a straight ExtrusionLine + */ + void simplify(int64_t smallest_line_segment_squared, int64_t allowed_error_distance_squared, int64_t maximum_extrusion_area_deviation); + + /*! + * Computes and returns the total area error (in μm²) of the AB and BC segments of an ABC straight ExtrusionLine + * when the junction B with a width B.w is removed from the ExtrusionLine. The area changes due to the fact that the + * new simplified line AC has a uniform width which equals to the weighted average of the width of the subsegments + * (based on their length). + * + * \param A Start point of the 3-point-straight line + * \param B Intermediate point of the 3-point-straight line + * \param C End point of the 3-point-straight line + * \param weighted_average_width The weighted average of the widths of the two colinear extrusion segments + * */ + static int64_t calculateExtrusionAreaDeviationError(ExtrusionJunction A, ExtrusionJunction B, ExtrusionJunction C, coord_t& weighted_average_width); +}; + +using VariableWidthLines = std::vector; //; //= 2); + Slic3r::ThickPolyline out; + out.points.emplace_back(line_junctions.front().p); + out.width.emplace_back(line_junctions.front().w); + out.points.emplace_back(line_junctions[1].p); + out.width.emplace_back(line_junctions[1].w); + + auto it_prev = line_junctions.begin() + 1; + for (auto it = line_junctions.begin() + 2; it != line_junctions.end(); ++it) { + out.points.emplace_back(it->p); + out.width.emplace_back(it_prev->w); + out.width.emplace_back(it->w); + it_prev = it; + } + + return out; +} + +static inline Polygon to_polygon(const ExtrusionLine& line) { + Polygon out; + assert(line.junctions.size() >= 3); + assert(line.junctions.front().p == line.junctions.back().p); + out.points.reserve(line.junctions.size() - 1); + for (auto it = line.junctions.begin(); it != line.junctions.end() - 1; ++it) + out.points.emplace_back(it->p); + return out; +} + +} // namespace Slic3r::Arachne +#endif // UTILS_EXTRUSION_LINE_H diff --git a/src/libslic3r/Arachne/utils/HalfEdge.hpp b/src/libslic3r/Arachne/utils/HalfEdge.hpp new file mode 100644 index 000000000..ff52de781 --- /dev/null +++ b/src/libslic3r/Arachne/utils/HalfEdge.hpp @@ -0,0 +1,39 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_HALF_EDGE_H +#define UTILS_HALF_EDGE_H + +#include +#include + +namespace Slic3r::Arachne +{ + +template +class HalfEdgeNode; + + +template +class HalfEdge +{ + using edge_t = derived_edge_t; + using node_t = derived_node_t; +public: + edge_data_t data; + edge_t* twin = nullptr; + edge_t* next = nullptr; + edge_t* prev = nullptr; + node_t* from = nullptr; + node_t* to = nullptr; + HalfEdge(edge_data_t data) + : data(data) + {} + bool operator==(const edge_t& other) + { + return this == &other; + } +}; + +} // namespace Slic3r::Arachne +#endif // UTILS_HALF_EDGE_H diff --git a/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp new file mode 100644 index 000000000..99efff6a0 --- /dev/null +++ b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp @@ -0,0 +1,29 @@ +//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 "HalfEdge.hpp" +#include "HalfEdgeNode.hpp" + +namespace Slic3r::Arachne +{ +template // types of data contained in nodes and edges +class HalfEdgeGraph +{ +public: + using edge_t = derived_edge_t; + using node_t = derived_node_t; + std::list edges; + std::list nodes; +}; + +} // namespace Slic3r::Arachne +#endif // UTILS_HALF_EDGE_GRAPH_H diff --git a/src/libslic3r/Arachne/utils/HalfEdgeNode.hpp b/src/libslic3r/Arachne/utils/HalfEdgeNode.hpp new file mode 100644 index 000000000..ad474489c --- /dev/null +++ b/src/libslic3r/Arachne/utils/HalfEdgeNode.hpp @@ -0,0 +1,38 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_HALF_EDGE_NODE_H +#define UTILS_HALF_EDGE_NODE_H + +#include + +#include "../../Point.hpp" + +namespace Slic3r::Arachne +{ + +template +class HalfEdge; + +template +class HalfEdgeNode +{ + using edge_t = derived_edge_t; + using node_t = derived_node_t; +public: + node_data_t data; + Point p; + edge_t* incident_edge = nullptr; + HalfEdgeNode(node_data_t data, Point p) + : data(data) + , p(p) + {} + + bool operator==(const node_t& other) + { + return this == &other; + } +}; + +} // namespace Slic3r::Arachne +#endif // UTILS_HALF_EDGE_NODE_H diff --git a/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp b/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp new file mode 100644 index 000000000..f47d2c757 --- /dev/null +++ b/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp @@ -0,0 +1,131 @@ +//Copyright (c) 2018 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_POLYGONS_POINT_INDEX_H +#define UTILS_POLYGONS_POINT_INDEX_H + +#include + +#include "../../Point.hpp" +#include "../../Polygon.hpp" + + +namespace Slic3r::Arachne +{ + +/*! + * A class for iterating over the points in one of the polygons in a \ref Polygons object + */ +class PolygonsPointIndex +{ +public: + /*! + * The polygons into which this index is indexing. + */ + const Polygons* polygons; // (pointer to const polygons) + + unsigned int poly_idx; //!< The index of the polygon in \ref PolygonsPointIndex::polygons + + unsigned int point_idx; //!< The index of the point in the polygon in \ref PolygonsPointIndex::polygons + + /*! + * Constructs an empty point index to no polygon. + * + * This is used as a placeholder for when there is a zero-construction + * needed. Since the `polygons` field is const you can't ever make this + * initialisation useful. + */ + PolygonsPointIndex() : polygons(nullptr), poly_idx(0), point_idx(0) {} + + /*! + * Constructs a new point index to a vertex of a polygon. + * \param polygons The Polygons instance to which this index points. + * \param poly_idx The index of the sub-polygon to point to. + * \param point_idx The index of the vertex in the sub-polygon. + */ + PolygonsPointIndex(const Polygons *polygons, unsigned int poly_idx, unsigned int point_idx) + : polygons(polygons), poly_idx(poly_idx), point_idx(point_idx) {} + + /*! + * Copy constructor to copy these indices. + */ + PolygonsPointIndex(const PolygonsPointIndex& original) = default; + + Point p() const + { + if (!polygons) + return {0, 0}; + + return (*polygons)[poly_idx][point_idx]; + } + + /*! + * Test whether two iterators refer to the same polygon in the same polygon list. + * + * \param other The PolygonsPointIndex to test for equality + * \return Wether the right argument refers to the same polygon in the same ListPolygon as the left argument. + */ + bool operator==(const PolygonsPointIndex &other) const + { + return polygons == other.polygons && poly_idx == other.poly_idx && point_idx == other.point_idx; + } + bool operator!=(const PolygonsPointIndex &other) const { return !(*this == other); } + bool operator<(const PolygonsPointIndex &other) const { return this->p() < other.p(); } + PolygonsPointIndex &operator=(const PolygonsPointIndex &other) + { + polygons = other.polygons; + poly_idx = other.poly_idx; + point_idx = other.point_idx; + return *this; + } + //! move the iterator forward (and wrap around at the end) + PolygonsPointIndex &operator++() + { + point_idx = (point_idx + 1) % (*polygons)[poly_idx].size(); + return *this; + } + //! move the iterator backward (and wrap around at the beginning) + PolygonsPointIndex &operator--() + { + if (point_idx == 0) + point_idx = (*polygons)[poly_idx].size(); + point_idx--; + return *this; + } + //! move the iterator forward (and wrap around at the end) + PolygonsPointIndex next() const + { + PolygonsPointIndex ret(*this); + ++ret; + return ret; + } + //! move the iterator backward (and wrap around at the beginning) + PolygonsPointIndex prev() const + { + PolygonsPointIndex ret(*this); + --ret; + return ret; + } +}; + + +}//namespace Slic3r::Arachne + +namespace std +{ +/*! + * Hash function for \ref PolygonsPointIndex + */ +template <> +struct hash +{ + size_t operator()(const Slic3r::Arachne::PolygonsPointIndex& lpi) const + { + return Slic3r::PointHash{}(lpi.p()); + } +}; +}//namespace std + + + +#endif//UTILS_POLYGONS_POINT_INDEX_H diff --git a/src/libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp b/src/libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp new file mode 100644 index 000000000..6eff3d62e --- /dev/null +++ b/src/libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp @@ -0,0 +1,31 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_POLYGONS_SEGMENT_INDEX_H +#define UTILS_POLYGONS_SEGMENT_INDEX_H + +#include + +#include "PolygonsPointIndex.hpp" + +namespace Slic3r::Arachne +{ + +/*! + * A class for iterating over the points in one of the polygons in a \ref Polygons object + */ +class PolygonsSegmentIndex : public PolygonsPointIndex +{ +public: + PolygonsSegmentIndex() : PolygonsPointIndex(){}; + PolygonsSegmentIndex(const Polygons *polygons, unsigned int poly_idx, unsigned int point_idx) : PolygonsPointIndex(polygons, poly_idx, point_idx){}; + + Point from() const { return PolygonsPointIndex::p(); } + + Point to() const { return PolygonsSegmentIndex::next().p(); } +}; + +} // namespace Slic3r::Arachne + + +#endif//UTILS_POLYGONS_SEGMENT_INDEX_H diff --git a/src/libslic3r/Arachne/utils/SparseGrid.hpp b/src/libslic3r/Arachne/utils/SparseGrid.hpp new file mode 100644 index 000000000..be461d424 --- /dev/null +++ b/src/libslic3r/Arachne/utils/SparseGrid.hpp @@ -0,0 +1,133 @@ +//Copyright (c) 2016 Scott Lenser +//Copyright (c) 2018 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_SPARSE_GRID_H +#define UTILS_SPARSE_GRID_H + +#include +#include +#include +#include + +#include "../../Point.hpp" +#include "SquareGrid.hpp" + +namespace Slic3r::Arachne { + +/*! \brief Sparse grid which can locate spatially nearby elements efficiently. + * + * \note This is an abstract template class which doesn't have any functions to insert elements. + * \see SparsePointGrid + * + * \tparam ElemT The element type to store. + */ +template class SparseGrid : public SquareGrid +{ +public: + using Elem = ElemT; + + using GridPoint = SquareGrid::GridPoint; + using grid_coord_t = SquareGrid::grid_coord_t; + using GridMap = std::unordered_multimap; + + using iterator = typename GridMap::iterator; + using const_iterator = typename GridMap::const_iterator; + + /*! \brief Constructs a sparse grid with the specified cell size. + * + * \param[in] cell_size The size to use for a cell (square) in the grid. + * Typical values would be around 0.5-2x of expected query radius. + * \param[in] elem_reserve Number of elements to research space for. + * \param[in] max_load_factor Maximum average load factor before rehashing. + */ + SparseGrid(coord_t cell_size, size_t elem_reserve=0U, float max_load_factor=1.0f); + + iterator begin() { return m_grid.begin(); } + iterator end() { return m_grid.end(); } + const_iterator begin() const { return m_grid.begin(); } + const_iterator end() const { return m_grid.end(); } + + /*! \brief Returns all data within radius of query_pt. + * + * Finds all elements with location within radius of \p query_pt. May + * return additional elements that are beyond radius. + * + * Average running time is a*(1 + 2 * radius / cell_size)**2 + + * b*cnt where a and b are proportionality constance and cnt is + * the number of returned items. The search will return items in + * an area of (2*radius + cell_size)**2 on average. The max range + * of an item from the query_point is radius + cell_size. + * + * \param[in] query_pt The point to search around. + * \param[in] radius The search radius. + * \return Vector of elements found + */ + std::vector getNearby(const Point &query_pt, coord_t radius) const; + + /*! \brief Process elements from cells that might contain sought after points. + * + * Processes elements from cell that might have elements within \p + * radius of \p query_pt. Processes all elements that are within + * radius of query_pt. May process elements that are up to radius + + * cell_size from query_pt. + * + * \param[in] query_pt The point to search around. + * \param[in] radius The search radius. + * \param[in] process_func Processes each element. process_func(elem) is + * called for each element in the cell. Processing stops if function returns false. + * \return Whether we need to continue processing after this function + */ + bool processNearby(const Point &query_pt, coord_t radius, const std::function &process_func) const; + +protected: + /*! \brief Process elements from the cell indicated by \p grid_pt. + * + * \param[in] grid_pt The grid coordinates of the cell. + * \param[in] process_func Processes each element. process_func(elem) is + * called for each element in the cell. Processing stops if function returns false. + * \return Whether we need to continue processing a next cell. + */ + bool processFromCell(const GridPoint &grid_pt, const std::function &process_func) const; + + /*! \brief Map from grid locations (GridPoint) to elements (Elem). */ + GridMap m_grid; +}; + +template SparseGrid::SparseGrid(coord_t cell_size, size_t elem_reserve, float max_load_factor) : SquareGrid(cell_size) +{ + // Must be before the reserve call. + m_grid.max_load_factor(max_load_factor); + if (elem_reserve != 0U) + m_grid.reserve(elem_reserve); +} + +template bool SparseGrid::processFromCell(const GridPoint &grid_pt, const std::function &process_func) const +{ + auto grid_range = m_grid.equal_range(grid_pt); + for (auto iter = grid_range.first; iter != grid_range.second; ++iter) + if (!process_func(iter->second)) + return false; + return true; +} + +template +bool SparseGrid::processNearby(const Point &query_pt, coord_t radius, const std::function &process_func) const +{ + return SquareGrid::processNearby(query_pt, radius, [&process_func, this](const GridPoint &grid_pt) { return processFromCell(grid_pt, process_func); }); +} + +template std::vector::Elem> SparseGrid::getNearby(const Point &query_pt, coord_t radius) const +{ + std::vector ret; + const std::function process_func = [&ret](const Elem &elem) { + ret.push_back(elem); + return true; + }; + processNearby(query_pt, radius, process_func); + return ret; +} + +} // namespace Slic3r::Arachne + +#endif // UTILS_SPARSE_GRID_H diff --git a/src/libslic3r/Arachne/utils/SparseLineGrid.hpp b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp new file mode 100644 index 000000000..a9b536869 --- /dev/null +++ b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp @@ -0,0 +1,77 @@ +//Copyright (c) 2018 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + + +#ifndef UTILS_SPARSE_LINE_GRID_H +#define UTILS_SPARSE_LINE_GRID_H + +#include +#include +#include +#include + +#include "SparseGrid.hpp" + +namespace Slic3r::Arachne { + +/*! \brief Sparse grid which can locate spatially nearby elements efficiently. + * + * \tparam ElemT The element type to store. + * \tparam Locator The functor to get the start and end locations from ElemT. + * must have: std::pair operator()(const ElemT &elem) const + * which returns the location associated with val. + */ +template class SparseLineGrid : public SparseGrid +{ +public: + using Elem = ElemT; + + /*! \brief Constructs a sparse grid with the specified cell size. + * + * \param[in] cell_size The size to use for a cell (square) in the grid. + * Typical values would be around 0.5-2x of expected query radius. + * \param[in] elem_reserve Number of elements to research space for. + * \param[in] max_load_factor Maximum average load factor before rehashing. + */ + SparseLineGrid(coord_t cell_size, size_t elem_reserve = 0U, float max_load_factor = 1.0f); + + /*! \brief Inserts elem into the sparse grid. + * + * \param[in] elem The element to be inserted. + */ + void insert(const Elem &elem); + +protected: + using GridPoint = typename SparseGrid::GridPoint; + + /*! \brief Accessor for getting locations from elements. */ + Locator m_locator; +}; + +template +SparseLineGrid::SparseLineGrid(coord_t cell_size, size_t elem_reserve, float max_load_factor) + : SparseGrid(cell_size, elem_reserve, max_load_factor) {} + +template void SparseLineGrid::insert(const Elem &elem) +{ + const std::pair line = m_locator(elem); + using GridMap = std::unordered_multimap; + // below is a workaround for the fact that lambda functions cannot access private or protected members + // first we define a lambda which works on any GridMap and then we bind it to the actual protected GridMap of the parent class + std::function process_cell_func_ = [&elem](GridMap *m_grid, const GridPoint grid_loc) { + m_grid->emplace(grid_loc, elem); + return true; + }; + using namespace std::placeholders; // for _1, _2, _3... + GridMap *m_grid = &(this->m_grid); + std::function process_cell_func(std::bind(process_cell_func_, m_grid, _1)); + + SparseGrid::processLineCells(line, process_cell_func); +} + +#undef SGI_TEMPLATE +#undef SGI_THIS + +} // namespace Slic3r::Arachne + +#endif // UTILS_SPARSE_LINE_GRID_H diff --git a/src/libslic3r/Arachne/utils/SquareGrid.cpp b/src/libslic3r/Arachne/utils/SquareGrid.cpp new file mode 100644 index 000000000..ae8996579 --- /dev/null +++ b/src/libslic3r/Arachne/utils/SquareGrid.cpp @@ -0,0 +1,147 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "SquareGrid.hpp" +#include "../../Point.hpp" + +using namespace Slic3r::Arachne; + + +SquareGrid::SquareGrid(coord_t cell_size) : cell_size(cell_size) +{ + assert(cell_size > 0U); +} + + +SquareGrid::GridPoint SquareGrid::toGridPoint(const Vec2i64 &point) const +{ + return Point(toGridCoord(point.x()), toGridCoord(point.y())); +} + + +SquareGrid::grid_coord_t SquareGrid::toGridCoord(const int64_t &coord) const +{ + // This mapping via truncation results in the cells with + // GridPoint.x==0 being twice as large and similarly for + // GridPoint.y==0. This doesn't cause any incorrect behavior, + // just changes the running time slightly. The change in running + // time from this is probably not worth doing a proper floor + // operation. + return coord / cell_size; +} + +coord_t SquareGrid::toLowerCoord(const grid_coord_t& grid_coord) const +{ + // This mapping via truncation results in the cells with + // GridPoint.x==0 being twice as large and similarly for + // GridPoint.y==0. This doesn't cause any incorrect behavior, + // just changes the running time slightly. The change in running + // time from this is probably not worth doing a proper floor + // operation. + return grid_coord * cell_size; +} + + +bool SquareGrid::processLineCells(const std::pair line, const std::function& process_cell_func) +{ + return static_cast(this)->processLineCells(line, process_cell_func); +} + + +bool SquareGrid::processLineCells(const std::pair line, const std::function& process_cell_func) const +{ + Point start = line.first; + Point end = line.second; + if (end.x() < start.x()) + { // make sure X increases between start and end + std::swap(start, end); + } + + const GridPoint start_cell = toGridPoint(start.cast()); + const GridPoint end_cell = toGridPoint(end.cast()); + const int64_t y_diff = int64_t(end.y() - start.y()); + const grid_coord_t y_dir = nonzeroSign(y_diff); + + /* This line drawing algorithm iterates over the range of Y coordinates, and + for each Y coordinate computes the range of X coordinates crossed in one + unit of Y. These ranges are rounded to be inclusive, so effectively this + creates a "fat" line, marking more cells than a strict one-cell-wide path.*/ + grid_coord_t x_cell_start = start_cell.x(); + for (grid_coord_t cell_y = start_cell.y(); cell_y * y_dir <= end_cell.y() * y_dir; cell_y += y_dir) + { // for all Y from start to end + // nearest y coordinate of the cells in the next row + const coord_t nearest_next_y = toLowerCoord(cell_y + ((nonzeroSign(cell_y) == y_dir || cell_y == 0) ? y_dir : coord_t(0))); + grid_coord_t x_cell_end; // the X coord of the last cell to include from this row + if (y_diff == 0) + { + x_cell_end = end_cell.x(); + } + else + { + const int64_t area = int64_t(end.x() - start.x()) * int64_t(nearest_next_y - start.y()); + // corresponding_x: the x coordinate corresponding to nearest_next_y + int64_t corresponding_x = int64_t(start.x()) + area / y_diff; + x_cell_end = toGridCoord(corresponding_x + ((corresponding_x < 0) && ((area % y_diff) != 0))); + if (x_cell_end < start_cell.x()) + { // process at least one cell! + x_cell_end = x_cell_start; + } + } + + for (grid_coord_t cell_x = x_cell_start; cell_x <= x_cell_end; ++cell_x) + { + GridPoint grid_loc(cell_x, cell_y); + if (! process_cell_func(grid_loc)) + { + return false; + } + if (grid_loc == end_cell) + { + return true; + } + } + // TODO: this causes at least a one cell overlap for each row, which + // includes extra cells when crossing precisely on the corners + // where positive slope where x > 0 and negative slope where x < 0 + x_cell_start = x_cell_end; + } + assert(false && "We should have returned already before here!"); + return false; +} + +bool SquareGrid::processNearby +( + const Point &query_pt, + coord_t radius, + const std::function& process_func +) const +{ + const Point min_loc(query_pt.x() - radius, query_pt.y() - radius); + const Point max_loc(query_pt.x() + radius, query_pt.y() + radius); + + GridPoint min_grid = toGridPoint(min_loc.cast()); + GridPoint max_grid = toGridPoint(max_loc.cast()); + + for (coord_t grid_y = min_grid.y(); grid_y <= max_grid.y(); ++grid_y) + { + for (coord_t grid_x = min_grid.x(); grid_x <= max_grid.x(); ++grid_x) + { + GridPoint grid_pt(grid_x,grid_y); + if (!process_func(grid_pt)) + { + return false; + } + } + } + return true; +} + +SquareGrid::grid_coord_t SquareGrid::nonzeroSign(const grid_coord_t z) const +{ + return (z >= 0) - (z < 0); +} + +coord_t SquareGrid::getCellSize() const +{ + return cell_size; +} diff --git a/src/libslic3r/Arachne/utils/SquareGrid.hpp b/src/libslic3r/Arachne/utils/SquareGrid.hpp new file mode 100644 index 000000000..c59c3ee1b --- /dev/null +++ b/src/libslic3r/Arachne/utils/SquareGrid.hpp @@ -0,0 +1,110 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_SQUARE_GRID_H +#define UTILS_SQUARE_GRID_H + +#include "../../Point.hpp" + +#include +#include +#include +#include + +namespace Slic3r::Arachne { + +/*! + * Helper class to calculate coordinates on a square grid, and providing some + * utility functions to process grids. + * + * Doesn't contain any data, except cell size. The purpose is only to + * automatically generate coordinates on a grid, and automatically feed them to + * functions. + * The grid is theoretically infinite (bar integer limits). + */ +class SquareGrid +{ +public: + /*! \brief Constructs a grid with the specified cell size. + * \param[in] cell_size The size to use for a cell (square) in the grid. + */ + SquareGrid(const coord_t cell_size); + + /*! + * Get the cell size this grid was created for. + */ + coord_t getCellSize() const; + + using GridPoint = Point; + using grid_coord_t = coord_t; + + /*! \brief Process cells along a line indicated by \p line. + * + * \param line The line along which to process cells. + * \param process_func Processes each cell. ``process_func(elem)`` is called + * for each cell. Processing stops if function returns false. + * \return Whether we need to continue processing after this function. + */ + bool processLineCells(const std::pair line, const std::function& process_cell_func); + + /*! \brief Process cells along a line indicated by \p line. + * + * \param line The line along which to process cells + * \param process_func Processes each cell. ``process_func(elem)`` is called + * for each cell. Processing stops if function returns false. + * \return Whether we need to continue processing after this function. + */ + bool processLineCells(const std::pair line, const std::function& process_cell_func) const; + + /*! \brief Process cells that might contain sought after points. + * + * Processes cells that might be within a square with twice \p radius as + * width, centered around \p query_pt. + * May process elements that are up to radius + cell_size from query_pt. + * \param query_pt The point to search around. + * \param radius The search radius. + * \param process_func Processes each cell. ``process_func(loc)`` is called + * for each cell coord within range. Processing stops if function returns + * ``false``. + * \return Whether we need to continue processing after this function. + */ + bool processNearby(const Point &query_pt, coord_t radius, const std::function &process_func) const; + + /*! \brief Compute the grid coordinates of a point. + * \param point The actual location. + * \return The grid coordinates that correspond to \p point. + */ + GridPoint toGridPoint(const Vec2i64 &point) const; + + /*! \brief Compute the grid coordinate of a real space coordinate. + * \param coord The actual location. + * \return The grid coordinate that corresponds to \p coord. + */ + grid_coord_t toGridCoord(const int64_t &coord) const; + + /*! \brief Compute the lowest coord in a grid cell. + * The lowest point is the point in the grid cell closest to the origin. + * + * \param grid_coord The grid coordinate. + * \return The print space coordinate that corresponds to \p grid_coord. + */ + coord_t toLowerCoord(const grid_coord_t &grid_coord) const; + +protected: + /*! \brief The cell (square) size. */ + coord_t cell_size; + + /*! + * Compute the sign of a number. + * + * The number 0 will result in a positive sign (1). + * \param z The number to find the sign of. + * \return 1 if the number is positive or 0, or -1 if the number is + * negative. + */ + grid_coord_t nonzeroSign(grid_coord_t z) const; +}; + +} // namespace Slic3r::Arachne + +#endif //UTILS_SQUARE_GRID_H diff --git a/src/libslic3r/Arachne/utils/VoronoiUtils.cpp b/src/libslic3r/Arachne/utils/VoronoiUtils.cpp new file mode 100644 index 000000000..a0f219039 --- /dev/null +++ b/src/libslic3r/Arachne/utils/VoronoiUtils.cpp @@ -0,0 +1,250 @@ +//Copyright (c) 2021 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include +#include +#include + +#include "linearAlg2D.hpp" +#include "VoronoiUtils.hpp" + +namespace Slic3r::Arachne +{ + +Vec2i64 VoronoiUtils::p(const vd_t::vertex_type *node) +{ + const double x = node->x(); + const double y = node->y(); + assert(x <= double(std::numeric_limits::max()) && x >= std::numeric_limits::lowest()); + assert(y <= double(std::numeric_limits::max()) && y >= std::numeric_limits::lowest()); + return Vec2i64(int64_t(x + 0.5 - (x < 0)), int64_t(y + 0.5 - (y < 0))); // Round to the nearest integer coordinates. +} + +Point VoronoiUtils::getSourcePoint(const vd_t::cell_type& cell, const std::vector& segments) +{ + assert(cell.contains_point()); + if(!cell.contains_point()) + BOOST_LOG_TRIVIAL(debug) << "Voronoi cell doesn't contain a source point!"; + + switch (cell.source_category()) { + case boost::polygon::SOURCE_CATEGORY_SINGLE_POINT: + assert(false && "Voronoi diagram is always constructed using segments, so cell.source_category() shouldn't be SOURCE_CATEGORY_SINGLE_POINT!\n"); + BOOST_LOG_TRIVIAL(error) << "Voronoi diagram is always constructed using segments, so cell.source_category() shouldn't be SOURCE_CATEGORY_SINGLE_POINT!"; + break; + case boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT: + assert(cell.source_index() < segments.size()); + return segments[cell.source_index()].to(); + break; + case boost::polygon::SOURCE_CATEGORY_SEGMENT_END_POINT: + assert(cell.source_index() < segments.size()); + return segments[cell.source_index()].from(); + break; + default: + assert(false && "getSourcePoint should only be called on point cells!\n"); + break; + } + + assert(false && "cell.source_category() is equal to an invalid value!\n"); + BOOST_LOG_TRIVIAL(error) << "cell.source_category() is equal to an invalid value!"; + return {}; +} + +PolygonsPointIndex VoronoiUtils::getSourcePointIndex(const vd_t::cell_type& cell, const std::vector& segments) +{ + assert(cell.contains_point()); + if(!cell.contains_point()) + BOOST_LOG_TRIVIAL(debug) << "Voronoi cell doesn't contain a source point!"; + + assert(cell.source_category() != boost::polygon::SOURCE_CATEGORY_SINGLE_POINT); + switch (cell.source_category()) { + case boost::polygon::SOURCE_CATEGORY_SEGMENT_START_POINT: { + assert(cell.source_index() < segments.size()); + PolygonsPointIndex ret = segments[cell.source_index()]; + ++ret; + return ret; + break; + } + case boost::polygon::SOURCE_CATEGORY_SEGMENT_END_POINT: { + assert(cell.source_index() < segments.size()); + return segments[cell.source_index()]; + break; + } + default: + assert(false && "getSourcePoint should only be called on point cells!\n"); + break; + } + PolygonsPointIndex ret = segments[cell.source_index()]; + return ++ret; +} + +const VoronoiUtils::Segment &VoronoiUtils::getSourceSegment(const vd_t::cell_type &cell, const std::vector &segments) +{ + assert(cell.contains_segment()); + if (!cell.contains_segment()) + BOOST_LOG_TRIVIAL(debug) << "Voronoi cell doesn't contain a source segment!"; + + return segments[cell.source_index()]; +} + +class PointMatrix +{ +public: + double matrix[4]; + + PointMatrix() + { + matrix[0] = 1; + matrix[1] = 0; + matrix[2] = 0; + matrix[3] = 1; + } + + PointMatrix(double rotation) + { + rotation = rotation / 180 * M_PI; + matrix[0] = cos(rotation); + matrix[1] = -sin(rotation); + matrix[2] = -matrix[1]; + matrix[3] = matrix[0]; + } + + PointMatrix(const Point p) + { + matrix[0] = p.x(); + matrix[1] = p.y(); + double f = sqrt((matrix[0] * matrix[0]) + (matrix[1] * matrix[1])); + matrix[0] /= f; + matrix[1] /= f; + matrix[2] = -matrix[1]; + matrix[3] = matrix[0]; + } + + static PointMatrix scale(double s) + { + PointMatrix ret; + ret.matrix[0] = s; + ret.matrix[3] = s; + return ret; + } + + Point apply(const Point p) const + { + return Point(coord_t(p.x() * matrix[0] + p.y() * matrix[1]), coord_t(p.x() * matrix[2] + p.y() * matrix[3])); + } + + Point unapply(const Point p) const + { + return Point(coord_t(p.x() * matrix[0] + p.y() * matrix[2]), coord_t(p.x() * matrix[1] + p.y() * matrix[3])); + } +}; +std::vector VoronoiUtils::discretizeParabola(const Point& p, const Segment& segment, Point s, Point e, coord_t approximate_step_size, float transitioning_angle) +{ + std::vector discretized; + // x is distance of point projected on the segment ab + // xx is point projected on the segment ab + const Point a = segment.from(); + const Point b = segment.to(); + const Point ab = b - a; + const Point as = s - a; + const Point ae = e - a; + const coord_t ab_size = ab.cast().norm(); + const coord_t sx = as.cast().dot(ab.cast()) / ab_size; + const coord_t ex = ae.cast().dot(ab.cast()) / ab_size; + const coord_t sxex = ex - sx; + + assert((as.cast().dot(ab.cast()) / int64_t(ab_size)) <= std::numeric_limits::max()); + assert((ae.cast().dot(ab.cast()) / int64_t(ab_size)) <= std::numeric_limits::max()); + + const Point ap = p - a; + const coord_t px = ap.cast().dot(ab.cast()) / ab_size; + + assert((ap.cast().dot(ab.cast()) / int64_t(ab_size)) <= std::numeric_limits::max()); + + Point pxx; + Line(a, b).distance_to_infinite_squared(p, &pxx); + const Point ppxx = pxx - p; + const coord_t d = ppxx.cast().norm(); + const PointMatrix rot = PointMatrix(ppxx.rotate_90_degree_ccw()); + + if (d == 0) + { + discretized.emplace_back(s); + discretized.emplace_back(e); + return discretized; + } + + const float marking_bound = atan(transitioning_angle * 0.5); + int64_t msx = - marking_bound * int64_t(d); // projected marking_start + int64_t mex = marking_bound * int64_t(d); // projected marking_end + + assert(msx <= std::numeric_limits::max()); + assert(double(msx) * double(msx) <= double(std::numeric_limits::max())); + assert(mex <= std::numeric_limits::max()); + assert(double(msx) * double(msx) / double(2 * d) + double(d / 2) <= std::numeric_limits::max()); + + const coord_t marking_start_end_h = msx * msx / (2 * d) + d / 2; + Point marking_start = rot.unapply(Point(coord_t(msx), marking_start_end_h)) + pxx; + Point marking_end = rot.unapply(Point(coord_t(mex), marking_start_end_h)) + pxx; + const int dir = (sx > ex) ? -1 : 1; + if (dir < 0) + { + std::swap(marking_start, marking_end); + std::swap(msx, mex); + } + + bool add_marking_start = msx * int64_t(dir) > int64_t(sx - px) * int64_t(dir) && msx * int64_t(dir) < int64_t(ex - px) * int64_t(dir); + bool add_marking_end = mex * int64_t(dir) > int64_t(sx - px) * int64_t(dir) && mex * int64_t(dir) < int64_t(ex - px) * int64_t(dir); + + const Point apex = rot.unapply(Point(0, d / 2)) + pxx; + bool add_apex = int64_t(sx - px) * int64_t(dir) < 0 && int64_t(ex - px) * int64_t(dir) > 0; + + assert(!(add_marking_start && add_marking_end) || add_apex); + if(add_marking_start && add_marking_end && !add_apex) + { + BOOST_LOG_TRIVIAL(warning) << "Failing to discretize parabola! Must add an apex or one of the endpoints."; + } + + const coord_t step_count = static_cast(static_cast(std::abs(ex - sx)) / approximate_step_size + 0.5); + + discretized.emplace_back(s); + for (coord_t step = 1; step < step_count; step++) + { + assert(double(sxex) * double(step) <= double(std::numeric_limits::max())); + const int64_t x = int64_t(sx) + int64_t(sxex) * int64_t(step) / int64_t(step_count) - int64_t(px); + assert(double(x) * double(x) <= double(std::numeric_limits::max())); + assert(double(x) * double(x) / double(2 * d) + double(d / 2) <= double(std::numeric_limits::max())); + const int64_t y = int64_t(x) * int64_t(x) / int64_t(2 * d) + int64_t(d / 2); + + if (add_marking_start && msx * int64_t(dir) < int64_t(x) * int64_t(dir)) + { + discretized.emplace_back(marking_start); + add_marking_start = false; + } + if (add_apex && int64_t(x) * int64_t(dir) > 0) + { + discretized.emplace_back(apex); + add_apex = false; // only add the apex just before the + } + if (add_marking_end && mex * int64_t(dir) < int64_t(x) * int64_t(dir)) + { + discretized.emplace_back(marking_end); + add_marking_end = false; + } + assert(x <= std::numeric_limits::max() && x >= std::numeric_limits::lowest()); + assert(y <= std::numeric_limits::max() && y >= std::numeric_limits::lowest()); + const Point result = rot.unapply(Point(x, y)) + pxx; + discretized.emplace_back(result); + } + if (add_apex) + { + discretized.emplace_back(apex); + } + if (add_marking_end) + { + discretized.emplace_back(marking_end); + } + discretized.emplace_back(e); + return discretized; +} + +}//namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/VoronoiUtils.hpp b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp new file mode 100644 index 000000000..e736f98bc --- /dev/null +++ b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp @@ -0,0 +1,42 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + + +#ifndef UTILS_VORONOI_UTILS_H +#define UTILS_VORONOI_UTILS_H + +#include + + +#include + +#include "PolygonsSegmentIndex.hpp" + +namespace Slic3r::Arachne +{ + +/*! + */ +class VoronoiUtils +{ +public: + using Segment = PolygonsSegmentIndex; + using voronoi_data_t = double; + using vd_t = boost::polygon::voronoi_diagram; + + static Point getSourcePoint(const vd_t::cell_type &cell, const std::vector &segments); + static const Segment &getSourceSegment(const vd_t::cell_type &cell, const std::vector &segments); + static PolygonsPointIndex getSourcePointIndex(const vd_t::cell_type &cell, const std::vector &segments); + + static Vec2i64 p(const vd_t::vertex_type *node); + + /*! + * Discretize a parabola based on (approximate) step size. + * The \p approximate_step_size is measured parallel to the \p source_segment, not along the parabola. + */ + static std::vector discretizeParabola(const Point &source_point, const Segment &source_segment, Point start, Point end, coord_t approximate_step_size, float transitioning_angle); +}; + +} // namespace Slic3r::Arachne + +#endif // UTILS_VORONOI_UTILS_H diff --git a/src/libslic3r/Arachne/utils/linearAlg2D.hpp b/src/libslic3r/Arachne/utils/linearAlg2D.hpp new file mode 100644 index 000000000..417e9962d --- /dev/null +++ b/src/libslic3r/Arachne/utils/linearAlg2D.hpp @@ -0,0 +1,131 @@ +//Copyright (c) 2020 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef UTILS_LINEAR_ALG_2D_H +#define UTILS_LINEAR_ALG_2D_H + +#include "../../Point.hpp" + +namespace Slic3r::Arachne::LinearAlg2D +{ + +/*! + * Test whether a point is inside a corner. + * Whether point \p query_point is left of the corner abc. + * Whether the \p query_point is in the circle half left of ab and left of bc, rather than to the right. + * + * Test whether the \p query_point is inside of a polygon w.r.t a single corner. + */ +inline static bool isInsideCorner(const Point a, const Point b, const Point c, const Vec2i64 query_point) { + + + // Visualisation for the algorithm below: + // + // query + // | + // | + // | + // perp-----------b + // / \ (note that the lines + // / \ AB and AC are normalized + // / \ to 10000 units length) + // a c + // + + auto normal = [](const Point& p0, coord_t len) -> Point + { + int64_t _len = p0.cast().norm(); + if (_len < 1) + return Point(len, 0); + return (p0.cast() * int64_t(len) / _len).cast(); + }; + + auto rotate_90_degree_ccw = [](const Vec2i64 &p) -> Vec2i64 { + return Vec2i64(-p.y(), p.x()); + }; + + constexpr coord_t normal_length = 10000; //Create a normal vector of reasonable length in order to reduce rounding error. + const Point ba = normal(a - b, normal_length); + const Point bc = normal(c - b, normal_length); + const Vec2i64 bq = query_point - b.cast(); + const Vec2i64 perpendicular = rotate_90_degree_ccw(bq); //The query projects to this perpendicular to coordinate 0. + + assert(ba.cast().dot(perpendicular.cast()) <= double(std::numeric_limits::max()) && ba.cast().dot(perpendicular.cast()) >= double(std::numeric_limits::lowest())); + assert(bc.cast().dot(perpendicular.cast()) <= double(std::numeric_limits::max()) && bc.cast().dot(perpendicular.cast()) >= double(std::numeric_limits::lowest())); + + const int64_t project_a_perpendicular = ba.cast().dot(perpendicular); //Project vertex A on the perpendicular line. + const int64_t project_c_perpendicular = bc.cast().dot(perpendicular); //Project vertex C on the perpendicular line. + if ((project_a_perpendicular > 0) != (project_c_perpendicular > 0)) //Query is between A and C on the projection. + { + return project_a_perpendicular > 0; //Due to the winding order of corner ABC, this means that the query is inside. + } + else //Beyond either A or C, but it could still be inside of the polygon. + { + assert(ba.cast().dot(bq.cast()) <= double(std::numeric_limits::max()) && ba.cast().dot(bq.cast()) >= double(std::numeric_limits::lowest())); + assert(bc.cast().dot(bq.cast()) <= double(std::numeric_limits::max()) && bc.cast().dot(bq.cast()) >= double(std::numeric_limits::lowest())); + + const int64_t project_a_parallel = ba.cast().dot(bq); //Project not on the perpendicular, but on the original. + const int64_t project_c_parallel = bc.cast().dot(bq); + + //Either: + // * A is to the right of B (project_a_perpendicular > 0) and C is below A (project_c_parallel < project_a_parallel), or + // * A is to the left of B (project_a_perpendicular < 0) and C is above A (project_c_parallel > project_a_parallel). + return (project_c_parallel < project_a_parallel) == (project_a_perpendicular > 0); + } + +} + +/*! + * Returns the determinant of the 2D matrix defined by the the vectors ab and ap as rows. + * + * The returned value is zero for \p p lying (approximately) on the line going through \p a and \p b + * The value is positive for values lying to the left and negative for values lying to the right when looking from \p a to \p b. + * + * \param p the point to check + * \param a the from point of the line + * \param b the to point of the line + * \return a positive value when \p p lies to the left of the line from \p a to \p b + */ +static inline int64_t pointIsLeftOfLine(const Point& p, const Point& a, const Point& b) +{ + return int64_t(b.x() - a.x()) * int64_t(p.y() - a.y()) - int64_t(b.y() - a.y()) * int64_t(p.x() - a.x()); +} + +/*! + * Compute the angle between two consecutive line segments. + * + * The angle is computed from the left side of b when looking from a. + * + * c + * \ . + * \ b + * angle| + * | + * a + * + * \param a start of first line segment + * \param b end of first segment and start of second line segment + * \param c end of second line segment + * \return the angle in radians between 0 and 2 * pi of the corner in \p b + */ +static inline float getAngleLeft(const Point& a, const Point& b, const Point& c) +{ + const Vec2i64 ba = (a - b).cast(); + const Vec2i64 bc = (c - b).cast(); + const int64_t dott = ba.dot(bc); // dot product + const int64_t det = cross2(ba, bc); // determinant + if (det == 0) { + if ((ba.x() != 0 && (ba.x() > 0) == (bc.x() > 0)) || (ba.x() == 0 && (ba.y() > 0) == (bc.y() > 0))) + return 0; // pointy bit + else + return float(M_PI); // straight bit + } + const float angle = -atan2(double(det), double(dott)); // from -pi to pi + if (angle >= 0) + return angle; + else + return M_PI * 2 + angle; +} + +}//namespace Slic3r::Arachne +#endif//UTILS_LINEAR_ALG_2D_H diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 99d010d9c..2eaa438dd 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -291,6 +291,45 @@ add_library(libslic3r STATIC SLA/Clustering.hpp SLA/Clustering.cpp SLA/ReprojectPointsOnMesh.hpp + + Arachne/BeadingStrategy/BeadingStrategy.hpp + Arachne/BeadingStrategy/BeadingStrategy.cpp + Arachne/BeadingStrategy/BeadingStrategyFactory.hpp + Arachne/BeadingStrategy/BeadingStrategyFactory.cpp + Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.hpp + Arachne/BeadingStrategy/CenterDeviationBeadingStrategy.cpp + Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp + Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp + Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp + Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp + Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp + Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp + Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp + Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp + Arachne/BeadingStrategy/WideningBeadingStrategy.hpp + Arachne/BeadingStrategy/WideningBeadingStrategy.cpp + Arachne/utils/ExtrusionJunction.hpp + Arachne/utils/ExtrusionJunction.cpp + Arachne/utils/ExtrusionLine.hpp + Arachne/utils/ExtrusionLine.cpp + Arachne/utils/HalfEdge.hpp + Arachne/utils/HalfEdgeGraph.hpp + Arachne/utils/HalfEdgeNode.hpp + Arachne/utils/SparseGrid.hpp + Arachne/utils/SquareGrid.hpp + Arachne/utils/SquareGrid.cpp + Arachne/utils/PolygonsPointIndex.hpp + Arachne/utils/PolygonsSegmentIndex.hpp + Arachne/utils/VoronoiUtils.hpp + Arachne/utils/VoronoiUtils.cpp + Arachne/SkeletalTrapezoidation.hpp + Arachne/SkeletalTrapezoidation.cpp + Arachne/SkeletalTrapezoidationEdge.hpp + Arachne/SkeletalTrapezoidationGraph.hpp + Arachne/SkeletalTrapezoidationGraph.cpp + Arachne/SkeletalTrapezoidationJoint.hpp + Arachne/WallToolPaths.hpp + Arachne/WallToolPaths.cpp ) if (SLIC3R_STATIC) diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index fd29d6d54..f944121dd 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -100,8 +100,11 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, SurfaceCollec g.ext_perimeter_flow = this->flow(frExternalPerimeter); g.overhang_flow = this->bridging_flow(frPerimeter); g.solid_infill_flow = this->flow(frSolidInfill); - - g.process(); + + if (print_config.slicing_engine.value == SlicingEngine::Arachne) + g.process_arachne(); + else + g.process_classic(); } //#define EXTERNAL_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index 751e59458..781d8cb14 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -82,6 +82,44 @@ double distance_to(const L &line, const Vec, Scalar> &point) return std::sqrt(distance_to_squared(line, point)); } +// Returns a squared distance to the closest point on the infinite. +// Returned nearest_point (and returned squared distance to this point) could be beyond the 'a' and 'b' ends of the segment. +template +double distance_to_infinite_squared(const L &line, const Vec, Scalar> &point, Vec, Scalar> *closest_point) +{ + const Vec, double> v = (get_b(line) - get_a(line)).template cast(); + const Vec, double> va = (point - get_a(line)).template cast(); + const double l2 = v.squaredNorm(); // avoid a sqrt + if (l2 == 0.) { + // a == b case + *closest_point = get_a(line); + return va.squaredNorm(); + } + // Consider the line extending the segment, parameterized as a + t (b - a). + // We find projection of this point onto the line. + // It falls where t = [(this-a) . (b-a)] / |b-a|^2 + const double t = va.dot(v) / l2; + *closest_point = (get_a(line).template cast() + t * v).template cast>(); + return (t * v - va).squaredNorm(); +} + +// Returns a squared distance to the closest point on the infinite. +// Closest point (and returned squared distance to this point) could be beyond the 'a' and 'b' ends of the segment. +template +double distance_to_infinite_squared(const L &line, const Vec, Scalar> &point) +{ + Vec, Scalar> nearest_point; + return distance_to_infinite_squared(line, point, &nearest_point); +} + +// Returns a distance to the closest point on the infinite. +// Closest point (and returned squared distance to this point) could be beyond the 'a' and 'b' ends of the segment. +template +double distance_to_infinite(const L &line, const Vec, Scalar> &point) +{ + return std::sqrt(distance_to_infinite_squared(line, point)); +} + } // namespace line_alg class Line @@ -102,6 +140,7 @@ public: double distance_to_squared(const Point &point) const { return distance_to_squared(point, this->a, this->b); } double distance_to_squared(const Point &point, Point *closest_point) const { return line_alg::distance_to_squared(*this, point, closest_point); } double distance_to(const Point &point) const { return distance_to(point, this->a, this->b); } + double distance_to_infinite_squared(const Point &point, Point *closest_point) const { return line_alg::distance_to_infinite_squared(*this, point, closest_point); } double perp_distance_to(const Point &point) const; bool parallel_to(double angle) const; bool parallel_to(const Line& line) const; @@ -122,6 +161,11 @@ public: static inline double distance_to_squared(const Point &point, const Point &a, const Point &b) { return line_alg::distance_to_squared(Line{a, b}, Vec<2, coord_t>{point}); } static double distance_to(const Point &point, const Point &a, const Point &b) { return sqrt(distance_to_squared(point, a, b)); } + // Returns a distance to the closest point on the infinite. + // Closest point (and returned squared distance to this point) could be beyond the 'a' and 'b' ends of the segment. + static inline double distance_to_infinite_squared(const Point &point, const Point &a, const Point &b) { return line_alg::distance_to_infinite_squared(Line{a, b}, Vec<2, coord_t>{point}); } + static double distance_to_infinite(const Point &point, const Point &a, const Point &b) { return sqrt(distance_to_infinite_squared(point, a, b)); } + Point a; Point b; diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 01d3c592a..2e9ba4ded 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -2,6 +2,7 @@ #include "ClipperUtils.hpp" #include "ExtrusionEntityCollection.hpp" #include "ShortestPath.hpp" +#include "Arachne/WallToolPaths.hpp" #include #include @@ -275,7 +276,106 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime return out; } -void PerimeterGenerator::process() +// Thanks, Cura developers, for implementing an algorithm for generating perimeters with variable width (Arachne) that is based on the paper +// "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" +void PerimeterGenerator::process_arachne() +{ + // other perimeters + m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); + coord_t perimeter_width = this->perimeter_flow.scaled_width(); + + // external perimeters + m_ext_mm3_per_mm = this->ext_perimeter_flow.mm3_per_mm(); + coord_t ext_perimeter_width = this->ext_perimeter_flow.scaled_width(); + + // overhang perimeters + m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); + + // solid infill + coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); + + // prepare grown lower layer slices for overhang detection + if (this->lower_slices != NULL && this->config->overhangs) { + // We consider overhang any part where the entire nozzle diameter is not supported by the + // lower layer, so we take lower slices and offset them by half the nozzle diameter used + // in the current layer + double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->perimeter_extruder-1); + m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter/2))); + } + + // we need to process each island separately because we might have different + // extra perimeters for each one + for (const Surface &surface : this->slices->surfaces) { + // detect how many perimeters must be generated for this island + int loop_number = this->config->perimeters + surface.extra_perimeters - 1; // 0-indexed loops + ExPolygons last = union_ex(surface.expolygon.simplify_p(m_scaled_resolution)); + Polygons last_p = to_polygons(last); + + coord_t bead_width_0 = ext_perimeter_width; + coord_t bead_width_x = perimeter_width; + coord_t wall_0_inset = 0; + + Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, bead_width_x, coord_t(loop_number + 1), wall_0_inset, *this->print_config); + wallToolPaths.generate(); + + std::set bins_with_index_zero_perimeters; + Arachne::BinJunctions perimeters = wallToolPaths.getBinJunctions(bins_with_index_zero_perimeters); + + int start_perimeter = int(perimeters.size()) - 1; + int end_perimeter = -1; + int direction = -1; + + if (this->config->external_perimeters_first) { + start_perimeter = 0; + end_perimeter = int(perimeters.size()); + direction = 1; + } + + for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { + if (perimeters[perimeter_idx].empty()) + continue; + + ThickPolylines thick_polylines; + for (const Arachne::LineJunctions &ej : perimeters[perimeter_idx]) + thick_polylines.emplace_back(Arachne::to_thick_polyline(ej)); + ExtrusionEntityCollection entities_coll; + if (bins_with_index_zero_perimeters.count(perimeter_idx) > 0) // Print using outer wall config. + variable_width(thick_polylines, erExternalPerimeter, this->ext_perimeter_flow, entities_coll.entities); + else + variable_width(thick_polylines, erPerimeter, this->perimeter_flow, entities_coll.entities); + this->loops->append(entities_coll); + } + + ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour()); + // create one more offset to be used as boundary for fill + // we offset by half the perimeter spacing (to get to the actual infill boundary) + // and then we offset back and forth by half the infill spacing to only consider the + // non-collapsing regions + coord_t inset = + (loop_number < 0) ? 0 : + (loop_number == 0) ? + // one loop + ext_perimeter_width: + // two or more loops? + perimeter_width; + + inset = coord_t(scale_(this->config->get_abs_value("infill_overlap", unscale(inset)))); + Polygons pp; + for (ExPolygon &ex : infill_contour) + ex.simplify_p(m_scaled_resolution, &pp); + // collapse too narrow infill areas + coord_t min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); + // append infill areas to fill_surfaces + this->fill_surfaces->append( + offset_ex(offset2_ex( + union_ex(pp), + float(-min_perimeter_infill_spacing / 2.), + float(min_perimeter_infill_spacing / 2.)), inset), + stInternal); + } +} + +void PerimeterGenerator::process_classic() { // other perimeters m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 0b3501d36..842f73097 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -55,7 +55,8 @@ public: m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1) {} - void process(); + void process_classic(); + void process_arachne(); double ext_mm3_per_mm() const { return m_ext_mm3_per_mm; } double mm3_per_mm() const { return m_mm3_per_mm; } diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 6b430c2fe..6f2d732a4 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -161,6 +161,7 @@ public: Point rotated(double angle) const { Point res(*this); res.rotate(angle); return res; } Point rotated(double cos_a, double sin_a) const { Point res(*this); res.rotate(cos_a, sin_a); return res; } Point rotated(double angle, const Point ¢er) const { Point res(*this); res.rotate(angle, center); return res; } + Point rotate_90_degree_ccw() const { return Point(-this->y(), this->x()); } int nearest_point_index(const Points &points) const; int nearest_point_index(const PointConstPtrs &points) const; int nearest_point_index(const PointPtrs &points) const; @@ -248,6 +249,15 @@ inline bool has_duplicate_successive_points_closed(const std::vector &pts return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); } +inline bool shorter_then(const Point& p0, const coord_t len) +{ + if (p0.x() > len || p0.x() < -len) + return false; + if (p0.y() > len || p0.y() < -len) + return false; + return p0.cast().squaredNorm() <= Slic3r::sqr(int64_t(len)); +} + namespace int128 { // Exact orientation predicate, // returns +1: CCW, 0: collinear, -1: CW. diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index e07b97e2b..deb0a886a 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -448,7 +448,9 @@ static std::vector s_Preset_print_options { "top_infill_extrusion_width", "support_material_extrusion_width", "infill_overlap", "infill_anchor", "infill_anchor_max", "bridge_flow_ratio", "clip_multipart_objects", "elefant_foot_compensation", "xy_size_compensation", "threads", "resolution", "gcode_resolution", "wipe_tower", "wipe_tower_x", "wipe_tower_y", "wipe_tower_width", "wipe_tower_rotation_angle", "wipe_tower_brim_width", "wipe_tower_bridging", "single_extruder_multi_material_priming", "mmu_segmented_region_max_width", - "wipe_tower_no_sparse_layers", "compatible_printers", "compatible_printers_condition", "inherits" + "wipe_tower_no_sparse_layers", "compatible_printers", "compatible_printers_condition", "inherits", + "slicing_engine", "beading_strategy_type", "wall_transition_length", "wall_transition_filter_distance", "wall_transition_angle", + "wall_distribution_count", "wall_split_middle_threshold", "wall_add_middle_threshold", "min_feature_size", "min_bead_width" }; static std::vector s_Preset_filament_options { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 2bd80b4ab..275c4b967 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -222,6 +222,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n osteps.emplace_back(posInfill); osteps.emplace_back(posSupportMaterial); steps.emplace_back(psSkirtBrim); + } else if ( + opt_key == "slicing_engine" + || opt_key == "beading_strategy_type" + || opt_key == "wall_transition_length" + || opt_key == "wall_transition_filter_distance" + || opt_key == "wall_transition_angle" + || opt_key == "wall_distribution_count" + || opt_key == "wall_split_middle_threshold" + || opt_key == "wall_add_middle_threshold" + || opt_key == "min_feature_size" + || opt_key == "min_bead_width") { + osteps.emplace_back(posSlice); } else { // for legacy, if we can't handle this option let's invalidate all steps //FIXME invalidate all steps of all objects as well? diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4afc22667..9c250eeb9 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -195,6 +195,19 @@ static const t_config_enum_values s_keys_map_ForwardCompatibilitySubstitutionRul }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) +static t_config_enum_values s_keys_map_SlicingEngine { + { "classic", int(SlicingEngine::Classic) }, + { "arachne", int(SlicingEngine::Arachne) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SlicingEngine) + +static t_config_enum_values s_keys_map_BeadingStrategyType { + { "center_deviation", int(BeadingStrategyType::Center) }, + { "distributed", int(BeadingStrategyType::Distributed) }, + { "inward_distributed", int(BeadingStrategyType::InwardDistributed) } +}; +CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeadingStrategyType) + static void assign_printer_technology_to_unknown(t_optiondef_map &options, PrinterTechnology printer_technology) { for (std::pair &kvp : options) @@ -3037,6 +3050,136 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0)); + def = this->add("slicing_engine", coEnum); + def->label = L("Slicing engine"); + def->category = L("Advanced"); + def->tooltip = L("Classic slicing engine produces perimeters with constant extrusion width and for" + " very thing areas is used gap-fill." + "Arachne produces perimeters with variable extrusion width."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("classic"); + def->enum_values.push_back("arachne"); + def->enum_labels.push_back(L("Classic")); + def->enum_labels.push_back(L("Arachne")); + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(SlicingEngine::Classic)); + + def = this->add("beading_strategy_type", coEnum); + def->label = L("Variable Line Strategy"); + def->category = L("Advanced"); + def->tooltip = L("Strategy to use to print the width of a part with a number of walls. This determines " + "how many walls it will use for a certain total width, and how wide each of" + " these lines are. \"Center Deviation\" will print all walls at the nominal" + " line width except the central one(s), causing big variations in the center" + " but very consistent outsides. \"Distributed\" distributes the width equally" + " over all walls. \"Inward Distributed\" is a balance between the other two, " + "distributing the changes in width over all walls but keeping the walls on the" + " outside slightly more consistent."); + def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); + def->enum_values.push_back("center_deviation"); + def->enum_values.push_back("distributed"); + def->enum_values.push_back("inward_distributed"); + def->enum_labels.push_back(L("Center Deviation")); + def->enum_labels.push_back(L("Distributed")); + def->enum_labels.push_back(L("Inward Distributed")); + def->mode = comExpert; + def->set_default_value(new ConfigOptionEnum(BeadingStrategyType::InwardDistributed)); + + def = this->add("wall_transition_length", coFloat); + def->label = L("Wall Transition Length"); + def->category = L("Advanced"); + def->tooltip = L("When transitioning between different numbers of walls as the part becomes" + "thinner, a certain amount of space is allotted to split or join the wall lines."); + def->sidetext = L("mm"); + def->mode = comExpert; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.4)); + + def = this->add("wall_transition_filter_distance", coFloat); + def->label = L("Wall Transition Distance Filter"); + def->category = L("Advanced"); + def->tooltip = L("If it would be transitioning back and forth between different numbers of walls in " + "quick succession, don't transition at all. Remove transitions if they are closer " + "together than this distance."); + def->sidetext = L("mm"); + def->mode = comExpert; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(1.4)); + + def = this->add("wall_transition_angle", coFloat); + def->label = L("Wall Transition Angle"); + def->category = L("Advanced"); + def->tooltip = L("When transitioning between different numbers of walls as the part becomes thinner, " + "two adjacent walls will join together at this angle. This can make the walls come " + "together faster than what the Wall Transition Length indicates, filling the space " + "better."); + def->sidetext = L("°"); + def->mode = comExpert; + def->min = 1.; + def->max = 59.; + def->set_default_value(new ConfigOptionFloat(10.)); + + def = this->add("wall_distribution_count", coInt); + def->label = L("Wall Distribution Count"); + def->category = L("Advanced"); + def->tooltip = L("The number of walls, counted from the center, over which the variation needs to be " + "spread. Lower values mean that the outer walls don't change in width."); + def->mode = comExpert; + def->min = 1; + def->set_default_value(new ConfigOptionInt(1)); + + def = this->add("wall_split_middle_threshold", coPercent); + def->label = L("Split Middle Line Threshold"); + def->category = L("Advanced"); + def->tooltip = L("The smallest line width, as a factor of the normal line width, above which the middle " + "line (if there is one) will be split into two. Reduce this setting to use more, thinner " + "lines. Increase to use fewer, wider lines. Note that this applies -as if- the entire " + "shape should be filled with wall, so the middle here refers to the middle of the object " + "between two outer edges of the shape, even if there actually is fill or (other) skin in " + "the print instead of wall."); + def->sidetext = L("%"); + def->mode = comExpert; + def->min = 1; + def->max = 99; + def->set_default_value(new ConfigOptionPercent(90)); + + def = this->add("wall_add_middle_threshold", coPercent); + def->label = L("Add Middle Line Threshold"); + def->category = L("Advanced"); + def->tooltip = L("The smallest line width, as a factor of the normal line width, above which a middle " + "line (if there wasn't one already) will be added. Reduce this setting to use more, " + "thinner lines. Increase to use fewer, wider lines. Note that this applies -as if- the " + "entire shape should be filled with wall, so the middle here refers to the middle of the " + "object between two outer edges of the shape, even if there actually is fill or (other) " + "skin in the print instead of wall."); + def->sidetext = L("%"); + def->mode = comExpert; + def->min = 1; + def->max = 99; + def->set_default_value(new ConfigOptionPercent(80)); + + def = this->add("min_feature_size", coFloat); + def->label = L("Minimum Feature Size"); + def->category = L("Advanced"); + def->tooltip = L("Minimum thickness of thin features. Model features that are thinner than this value will " + "not be printed, while features thicker than the Minimum Feature Size will be widened to " + "the Minimum Wall Line Width."); + def->sidetext = L("mm"); + def->mode = comExpert; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.1)); + + def = this->add("min_bead_width", coFloat); + def->label = L("Minimum Wall Line Width"); + def->category = L("Advanced"); + def->tooltip = L("Width of the wall that will replace thin features (according to the Minimum Feature Size) " + "of the model. If the Minimum Wall Line Width is thinner than the thickness of the feature," + " the wall will become as thick as the feature itself."); + def->sidetext = L("mm"); + def->mode = comExpert; + def->min = 0; + def->set_default_value(new ConfigOptionFloat(0.2)); + // Declare retract values for filament profile, overriding the printer's extruder profile. for (const char *opt_key : { // floats @@ -3968,6 +4111,13 @@ void DynamicPrintConfig::normalize_fdm() if (auto *opt_gcode_resolution = this->opt("gcode_resolution", false); opt_gcode_resolution) // Resolution will be above 1um. opt_gcode_resolution->value = std::max(opt_gcode_resolution->value, 0.001); + + if (auto *opt_min_bead_width = this->opt("min_bead_width", false); opt_min_bead_width) + opt_min_bead_width->value = std::max(opt_min_bead_width->value, 0.001); + if (auto *opt_wall_transition_length = this->opt("wall_transition_length", false); opt_wall_transition_length) + opt_wall_transition_length->value = std::max(opt_wall_transition_length->value, 0.001); + if (auto *opt_wall_transition_filter_distance = this->opt("wall_transition_filter_distance", false); opt_wall_transition_filter_distance) + opt_wall_transition_filter_distance->value = std::max(opt_wall_transition_filter_distance->value, 0.001); } void handle_legacy_sla(DynamicPrintConfig &config) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index c473cde4c..9146eaa94 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -127,6 +127,24 @@ enum DraftShield { dsDisabled, dsLimited, dsEnabled }; +enum class SlicingEngine +{ + // Classic perimeter generator using Clipper offsets with constant extrusion width. + Classic, + // Perimeter generator with variable extrusion width based on the paper + // "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" ported from Cura. + Arachne +}; + +enum class BeadingStrategyType +{ + Center, + Distributed, + InwardDistributed, + None, + Count +}; + #define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \ template<> const t_config_enum_names& ConfigOptionEnum::get_enum_names(); \ template<> const t_config_enum_values& ConfigOptionEnum::get_enum_values(); @@ -149,6 +167,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SLAPillarConnectionMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BrimType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(DraftShield) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ForwardCompatibilitySubstitutionRule) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingEngine) +CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeadingStrategyType) #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS @@ -748,6 +768,16 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, skirt_distance)) ((ConfigOptionInt, skirt_height)) ((ConfigOptionInt, skirts)) + ((ConfigOptionEnum, slicing_engine)) + ((ConfigOptionEnum, beading_strategy_type)) + ((ConfigOptionFloat, wall_transition_length)) + ((ConfigOptionFloat, wall_transition_filter_distance)) + ((ConfigOptionFloat, wall_transition_angle)) + ((ConfigOptionInt, wall_distribution_count)) + ((ConfigOptionPercent, wall_split_middle_threshold)) + ((ConfigOptionPercent, wall_add_middle_threshold)) + ((ConfigOptionFloat, min_feature_size)) + ((ConfigOptionFloat, min_bead_width)) ((ConfigOptionInts, slowdown_below_layer_time)) ((ConfigOptionBool, spiral_vase)) ((ConfigOptionInt, standby_temperature_delta)) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ca7122031..86e6f1818 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1670,6 +1670,18 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Other")); optgroup->append_single_option_line("clip_multipart_objects"); + optgroup = page->new_optgroup(L("Experimental")); + optgroup->append_single_option_line("slicing_engine"); + optgroup->append_single_option_line("beading_strategy_type"); + optgroup->append_single_option_line("wall_transition_length"); + optgroup->append_single_option_line("wall_transition_filter_distance"); + optgroup->append_single_option_line("wall_transition_angle"); + optgroup->append_single_option_line("wall_distribution_count"); + optgroup->append_single_option_line("wall_split_middle_threshold"); + optgroup->append_single_option_line("wall_add_middle_threshold"); + optgroup->append_single_option_line("min_feature_size"); + optgroup->append_single_option_line("min_bead_width"); + page = add_options_page(L("Output options"), "output+page_white"); optgroup = page->new_optgroup(L("Sequential printing")); optgroup->append_single_option_line("complete_objects", "sequential-printing_124589");