diff options
author | Jelle Spijker <spijker.jelle@gmail.com> | 2022-04-07 12:49:28 +0300 |
---|---|---|
committer | Jelle Spijker <spijker.jelle@gmail.com> | 2022-04-07 12:49:28 +0300 |
commit | ed4701757c898f25f2c79073986e369eb36a718c (patch) | |
tree | 78ac3cf4bf64c1757a1958d72e90e209c1193435 | |
parent | 07a3e1bd16bddeefae982afe27a61d74c6b0714d (diff) | |
parent | eb2b17e687ec7376b28fb1d170ce3da4d6726d3e (diff) |
Merge branch 'master' into CURA-8640_PyQt6_upgrade
# Conflicts:
# CMakeLists.txt
65 files changed, 1749 insertions, 1845 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f9f962f4..cdfda4c30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,8 +52,7 @@ set(engine_SRCS # Except main.cpp. src/MeshGroup.cpp src/Mold.cpp src/multiVolumes.cpp - src/PathOrderOptimizer.cpp - src/PathOrder.cpp + src/PathOrderPath.cpp src/Preheat.cpp src/PrimeTower.cpp src/raft.cpp @@ -78,7 +77,6 @@ set(engine_SRCS # Except main.cpp. src/BeadingStrategy/BeadingStrategy.cpp src/BeadingStrategy/BeadingStrategyFactory.cpp - src/BeadingStrategy/CenterDeviationBeadingStrategy.cpp src/BeadingStrategy/DistributedBeadingStrategy.cpp src/BeadingStrategy/LimitedBeadingStrategy.cpp src/BeadingStrategy/RedistributeBeadingStrategy.cpp @@ -163,9 +161,6 @@ if (ENABLE_ARCUS) ArcusCommunicationPrivateTest ) endif () -set(engine_TEST_BEADING_STRATEGY - CenterDeviationBeadingStrategyTest - ) set(engine_TEST_INFILL ) set(engine_TEST_INTEGRATION @@ -342,14 +337,8 @@ if (BUILD_TESTS) target_link_libraries(${test} _CuraEngine ${GTEST_BOTH_LIBRARIES} ${GMOCK_BOTH_LIBRARIES}) add_test(NAME ${test} COMMAND "${test}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests/") add_dependencies(build_all_tests ${test}) #Make sure that this gets built as part of the build_all_tests target. - endforeach () - endif () - foreach (test ${engine_TEST_BEADING_STRATEGY}) - add_executable(${test} tests/main.cpp tests/beading_strategy/${test}.cpp) - target_link_libraries(${test} _CuraEngine ${GTEST_BOTH_LIBRARIES} ${GMOCK_BOTH_LIBRARIES}) - add_test(NAME ${test} COMMAND "${test}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests/") - add_dependencies(build_all_tests ${test}) #Make sure that this gets built as part of the build_all_tests target. - endforeach () + endforeach() + endif() foreach (test ${engine_TEST_INFILL}) add_executable(${test} tests/main.cpp tests/infill/${test}.cpp) target_link_libraries(${test} _CuraEngine ${GTEST_BOTH_LIBRARIES} ${GMOCK_BOTH_LIBRARIES}) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..ece341fbe --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Reporting vulnerabilities + +If you discover a vulnerability, please let us know as soon as possible via `security@ultimaker.com`. +Please do not take advantage of the vulnerability and do not reveal the problem to others. +To allow us to resolve the issue, please do provide us with sufficient information to reproduce the problem. diff --git a/src/BeadingStrategy/BeadingStrategy.cpp b/src/BeadingStrategy/BeadingStrategy.cpp index a066c2622..dde873dbd 100644 --- a/src/BeadingStrategy/BeadingStrategy.cpp +++ b/src/BeadingStrategy/BeadingStrategy.cpp @@ -8,14 +8,32 @@ namespace cura { -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) +BeadingStrategy::BeadingStrategy +( + coord_t optimal_width, + Ratio wall_split_middle_threshold, + Ratio wall_add_middle_threshold, + coord_t default_transition_length, + float transitioning_angle +) : + optimal_width(optimal_width), + wall_split_middle_threshold(wall_split_middle_threshold), + wall_add_middle_threshold(wall_add_middle_threshold), + default_transition_length(default_transition_length), + transitioning_angle(transitioning_angle) { name = "Unknown"; } +BeadingStrategy::BeadingStrategy(const BeadingStrategy& other) : + optimal_width(other.optimal_width), + wall_split_middle_threshold(other.wall_split_middle_threshold), + wall_add_middle_threshold(other.wall_add_middle_threshold), + default_transition_length(other.default_transition_length), + transitioning_angle(other.transitioning_angle), + name(other.name) +{} + coord_t BeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const { if (lower_bead_count == 0) @@ -53,10 +71,32 @@ coord_t BeadingStrategy::getOptimalWidth() const return optimal_width; } +Ratio BeadingStrategy::getSplitMiddleThreshold() const +{ + return wall_split_middle_threshold; +} + +Ratio BeadingStrategy::getAddMiddleThreshold() const +{ + return wall_add_middle_threshold; +} + AngleRadians BeadingStrategy::getTransitioningAngle() const { return transitioning_angle; } +coord_t BeadingStrategy::getOptimalThickness(coord_t bead_count) const +{ + return optimal_width * bead_count; +} + +coord_t BeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const +{ + const coord_t lower_ideal_width = getOptimalThickness(lower_bead_count); + const coord_t higher_ideal_width = getOptimalThickness(lower_bead_count + 1); + const Ratio threshold = lower_bead_count % 2 == 1 ? wall_split_middle_threshold : wall_add_middle_threshold; + return lower_ideal_width + threshold * (higher_ideal_width - lower_ideal_width); +} } // namespace cura diff --git a/src/BeadingStrategy/BeadingStrategy.h b/src/BeadingStrategy/BeadingStrategy.h index f8a366343..50c676f96 100644 --- a/src/BeadingStrategy/BeadingStrategy.h +++ b/src/BeadingStrategy/BeadingStrategy.h @@ -9,6 +9,7 @@ #include "../utils/IntPoint.h" #include "../utils/logoutput.h" #include "../settings/types/Angle.h" +#include "../settings/types/Ratio.h" //For the wall transition threshold. namespace cura { @@ -36,7 +37,16 @@ public: 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)); + BeadingStrategy + ( + coord_t optimal_width, + Ratio wall_split_middle_threshold, + Ratio wall_add_middle_threshold, + coord_t default_transition_length, + float transitioning_angle = pi_div(3) + ); + + BeadingStrategy(const BeadingStrategy& other); virtual ~BeadingStrategy() {} @@ -52,12 +62,12 @@ public: /*! * The ideal thickness for a given \param bead_count */ - virtual coord_t getOptimalThickness(coord_t bead_count) const = 0; + virtual coord_t getOptimalThickness(coord_t bead_count) const; /*! * 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; + virtual coord_t getTransitionThickness(coord_t lower_bead_count) const; /*! * The number of beads should we ideally usefor a given model thickness @@ -89,6 +99,8 @@ public: virtual std::string toString() const; coord_t getOptimalWidth() const; + Ratio getSplitMiddleThreshold() const; + Ratio getAddMiddleThreshold() const; coord_t getDefaultTransitionLength() const; AngleRadians getTransitioningAngle() const; @@ -97,6 +109,10 @@ protected: coord_t optimal_width; //! Optimal bead width, nominal width off the walls in 'ideal' circumstances. + Ratio wall_split_middle_threshold; //! Threshold when a middle wall should be split into two, as a ratio of the optimal wall width. + + Ratio wall_add_middle_threshold; //! Threshold when a new middle wall should be added between an even number of walls, as a ratio of the optimal wall width. + coord_t default_transition_length; //! The length of the region to smoothly transfer between bead counts /*! diff --git a/src/BeadingStrategy/BeadingStrategyFactory.cpp b/src/BeadingStrategy/BeadingStrategyFactory.cpp index ad6e16e09..5e90b1cdb 100644 --- a/src/BeadingStrategy/BeadingStrategyFactory.cpp +++ b/src/BeadingStrategy/BeadingStrategyFactory.cpp @@ -1,10 +1,9 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "BeadingStrategyFactory.h" #include "LimitedBeadingStrategy.h" -#include "CenterDeviationBeadingStrategy.h" #include "WideningBeadingStrategy.h" #include "DistributedBeadingStrategy.h" #include "RedistributeBeadingStrategy.h" @@ -15,28 +14,8 @@ namespace cura { -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 StrategyType type, const coord_t preferred_bead_width_outer, const coord_t preferred_bead_width_inner, const coord_t preferred_transition_length, @@ -49,48 +28,29 @@ BeadingStrategyPtr BeadingStrategyFactory::makeStrategy 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 + const Ratio minimum_variable_line_ratio ) { 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 StrategyType::Center: - ret = make_unique<CenterDeviationBeadingStrategy>(bar_preferred_wall_width, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold); - break; - case StrategyType::Distributed: - ret = make_unique<DistributedBeadingStrategy>(bar_preferred_wall_width, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, std::numeric_limits<int>::max()); - break; - case StrategyType::InwardDistributed: - ret = make_unique<DistributedBeadingStrategy>(bar_preferred_wall_width, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, inward_distributed_center_wall_count); - break; - default: - logError("Cannot make strategy!\n"); - return nullptr; - } - + BeadingStrategyPtr ret = make_unique<DistributedBeadingStrategy>(preferred_bead_width_inner, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, inward_distributed_center_wall_count); + logDebug("Applying the Redistribute meta-strategy with outer-wall width = %d, inner-wall width = %d\n", preferred_bead_width_outer, preferred_bead_width_inner); + ret = make_unique<RedistributeBeadingStrategy>(preferred_bead_width_outer, minimum_variable_line_ratio, move(ret)); + if(print_thin_walls) { logDebug("Applying the Widening Beading meta-strategy with minimum input width %d and minimum output width %d.\n", min_feature_size, min_bead_width); ret = make_unique<WideningBeadingStrategy>(move(ret), min_feature_size, min_bead_width); } - if (max_bead_count > 0) - { - logDebug("Applying the Redistribute meta-strategy with outer-wall width = %d, inner-wall width = %d\n", preferred_bead_width_outer, preferred_bead_width_inner); - ret = make_unique<RedistributeBeadingStrategy>(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. - logDebug("Applying the Limited Beading meta-strategy with maximum bead count = %d.\n", max_bead_count); - ret = make_unique<LimitedBeadingStrategy>(max_bead_count, move(ret)); - } - if (outer_wall_offset > 0) { logDebug("Applying the OuterWallOffset meta-strategy with offset = %d.\n", outer_wall_offset); ret = make_unique<OuterWallInsetBeadingStrategy>(outer_wall_offset, move(ret)); } + + //Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. + logDebug("Applying the Limited Beading meta-strategy with maximum bead count = %d.\n", max_bead_count); + ret = make_unique<LimitedBeadingStrategy>(max_bead_count, move(ret)); return ret; } } // namespace cura diff --git a/src/BeadingStrategy/BeadingStrategyFactory.h b/src/BeadingStrategy/BeadingStrategyFactory.h index ed661dd14..55cfe6d2a 100644 --- a/src/BeadingStrategy/BeadingStrategyFactory.h +++ b/src/BeadingStrategy/BeadingStrategyFactory.h @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Ultimaker B.V. +// Copyright (c) 2022 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef BEADING_STRATEGY_FACTORY_H @@ -10,21 +10,11 @@ namespace cura { -enum class StrategyType -{ - Center, - Distributed, - InwardDistributed, - None, - COUNT -}; - class BeadingStrategyFactory { public: static BeadingStrategyPtr makeStrategy ( - const StrategyType type, const coord_t preferred_bead_width_outer = MM2INT(0.5), const coord_t preferred_bead_width_inner = MM2INT(0.5), const coord_t preferred_transition_length = MM2INT(0.4), @@ -37,7 +27,7 @@ public: 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 + const Ratio minimum_variable_line_ratio = 0.5 ); }; diff --git a/src/BeadingStrategy/CenterDeviationBeadingStrategy.cpp b/src/BeadingStrategy/CenterDeviationBeadingStrategy.cpp deleted file mode 100644 index 0f4d1ae6c..000000000 --- a/src/BeadingStrategy/CenterDeviationBeadingStrategy.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2021 Ultimaker B.V. -// CuraEngine is released under the terms of the AGPLv3 or higher. -#include <algorithm> - -#include "CenterDeviationBeadingStrategy.h" - -namespace cura -{ -CenterDeviationBeadingStrategy::CenterDeviationBeadingStrategy(const coord_t pref_bead_width, - const AngleRadians transitioning_angle, - const Ratio wall_split_middle_threshold, - const Ratio 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<coord_t>(static_cast<size_t>(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 cura diff --git a/src/BeadingStrategy/CenterDeviationBeadingStrategy.h b/src/BeadingStrategy/CenterDeviationBeadingStrategy.h deleted file mode 100644 index 06def9d35..000000000 --- a/src/BeadingStrategy/CenterDeviationBeadingStrategy.h +++ /dev/null @@ -1,50 +0,0 @@ -// 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 "../settings/types/Ratio.h" //For the wall transition threshold. -#include "BeadingStrategy.h" -#ifdef BUILD_TESTS -#include <gtest/gtest_prod.h> //Friend tests, so that they can inspect the privates. -#endif - -namespace cura -{ - -/*! - * 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 -{ -#ifdef BUILD_TESTS - FRIEND_TEST(CenterDeviationBeadingStrategy, Construction); -#endif - - 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, - AngleRadians transitioning_angle, - Ratio wall_split_middle_threshold, - Ratio 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 cura -#endif // CENTER_DEVIATION_BEADING_STRATEGY_H diff --git a/src/BeadingStrategy/DistributedBeadingStrategy.cpp b/src/BeadingStrategy/DistributedBeadingStrategy.cpp index abd951177..b973910ac 100644 --- a/src/BeadingStrategy/DistributedBeadingStrategy.cpp +++ b/src/BeadingStrategy/DistributedBeadingStrategy.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Ultimaker B.V. +// Copyright (c) 2022 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. #include <numeric> #include "DistributedBeadingStrategy.h" @@ -6,15 +6,16 @@ namespace cura { -DistributedBeadingStrategy::DistributedBeadingStrategy(const coord_t optimal_width, - const coord_t default_transition_length, - const AngleRadians transitioning_angle, - const Ratio wall_split_middle_threshold, - const Ratio 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) +DistributedBeadingStrategy::DistributedBeadingStrategy +( + const coord_t optimal_width, + const coord_t default_transition_length, + const AngleRadians transitioning_angle, + const Ratio wall_split_middle_threshold, + const Ratio wall_add_middle_threshold, + const int distribution_radius +) : + BeadingStrategy(optimal_width, wall_split_middle_threshold, wall_add_middle_threshold, default_transition_length, transitioning_angle) { if(distribution_radius >= 2) { @@ -92,19 +93,12 @@ DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(coord_t 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; + 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 = optimal_width * (naive_count % 2 == 1 ? wall_split_middle_threshold : wall_add_middle_threshold); + return naive_count + (remainder >= minimum_line_width); // If there's enough space, fit an extra one. } } // namespace cura diff --git a/src/BeadingStrategy/DistributedBeadingStrategy.h b/src/BeadingStrategy/DistributedBeadingStrategy.h index 50ca1eb0e..dde25da17 100644 --- a/src/BeadingStrategy/DistributedBeadingStrategy.h +++ b/src/BeadingStrategy/DistributedBeadingStrategy.h @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Ultimaker B.V. +// Copyright (c) 2022 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef DISTRIBUTED_BEADING_STRATEGY_H @@ -18,30 +18,25 @@ namespace cura 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. - Ratio 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. - Ratio 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 AngleRadians transitioning_angle, - const Ratio wall_split_middle_threshold, - const Ratio wall_add_middle_threshold, - const int distribution_radius); + DistributedBeadingStrategy + ( + const coord_t optimal_width, + const coord_t default_transition_length, + const AngleRadians transitioning_angle, + const Ratio wall_split_middle_threshold, + const Ratio 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; }; diff --git a/src/BeadingStrategy/LimitedBeadingStrategy.cpp b/src/BeadingStrategy/LimitedBeadingStrategy.cpp index 76e20a687..48d1d6f7d 100644 --- a/src/BeadingStrategy/LimitedBeadingStrategy.cpp +++ b/src/BeadingStrategy/LimitedBeadingStrategy.cpp @@ -24,7 +24,7 @@ float LimitedBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) c } LimitedBeadingStrategy::LimitedBeadingStrategy(const coord_t max_bead_count, BeadingStrategyPtr parent) - : BeadingStrategy(parent->getOptimalWidth(), /*default_transition_length=*/-1, parent->getTransitioningAngle()) + : BeadingStrategy(*parent) , max_bead_count(max_bead_count) , parent(std::move(parent)) { diff --git a/src/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp b/src/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp index 3221dc430..6f9af1ee6 100644 --- a/src/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp +++ b/src/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp @@ -8,9 +8,9 @@ namespace cura { 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) + BeadingStrategy(*parent), + parent(std::move(parent)), + outer_wall_offset(outer_wall_offset) { name = "OuterWallOfsetBeadingStrategy"; } diff --git a/src/BeadingStrategy/RedistributeBeadingStrategy.cpp b/src/BeadingStrategy/RedistributeBeadingStrategy.cpp index 9bf461a2f..adf708895 100644 --- a/src/BeadingStrategy/RedistributeBeadingStrategy.cpp +++ b/src/BeadingStrategy/RedistributeBeadingStrategy.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "RedistributeBeadingStrategy.h" @@ -9,35 +9,48 @@ namespace cura { -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) +RedistributeBeadingStrategy::RedistributeBeadingStrategy +( + const coord_t optimal_width_outer, + const Ratio minimum_variable_line_ratio, + BeadingStrategyPtr parent +) : + BeadingStrategy(*parent), + parent(std::move(parent)), + optimal_width_outer(optimal_width_outer), + minimum_variable_line_ratio(minimum_variable_line_ratio) { 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 inner_bead_count = std::max(static_cast<coord_t>(0), bead_count - 2); 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); + switch (lower_bead_count) + { + case 0: return minimum_variable_line_ratio * optimal_width_outer; + case 1: return (1.0 + parent->getSplitMiddleThreshold()) * optimal_width_outer; + default: return parent->getTransitionThickness(lower_bead_count - 2) + 2 * optimal_width_outer; + } } coord_t RedistributeBeadingStrategy::getOptimalBeadCount(coord_t thickness) const { - return parent->getOptimalBeadCount(thickness); + if (thickness < minimum_variable_line_ratio * optimal_width_outer) + { + return 0; + } + if (thickness <= 2 * optimal_width_outer) + { + return thickness > (1.0 + parent->getSplitMiddleThreshold()) * optimal_width_outer ? 2 : 1; + } + return parent->getOptimalBeadCount(thickness - 2 * optimal_width_outer) + 2; } coord_t RedistributeBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const @@ -58,123 +71,41 @@ std::string RedistributeBeadingStrategy::toString() const 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 + // Take care of all situations in which no lines are actually produced: + if (bead_count == 0 || thickness < minimum_variable_line_ratio * optimal_width_outer) { - ret = parent->compute(thickness, bead_count); + ret.left_over = thickness; + ret.total_thickness = thickness; + return ret; } - // 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) + // Compute the beadings of the inner walls, if any: + const coord_t inner_bead_count = bead_count - 2; + const coord_t inner_thickness = thickness - 2 * optimal_width_outer; + if (inner_bead_count > 0 && inner_thickness > 0) { - if (total_outer_optimal_width + minimum_width_inner > thickness) - { - outer_bead_width -= minimum_width_inner / 2; - } - else + ret = parent->compute(inner_thickness, inner_bead_count); + for (auto& toolpath_location : ret.toolpath_locations) { - outer_bead_width = optimal_width_outer; + toolpath_location += 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) + // Insert the outer wall(s) around the previously computed inner wall(s), which may be empty: + const coord_t actual_outer_thickness = bead_count > 2 ? std::min(thickness / 2, optimal_width_outer) : thickness / bead_count; + ret.bead_widths.insert(ret.bead_widths.begin(), actual_outer_thickness); + ret.toolpath_locations.insert(ret.toolpath_locations.begin(), actual_outer_thickness / 2); + if (bead_count > 1) { - beading.toolpath_locations[bead_count / 2] = thickness / 2; + ret.bead_widths.push_back(actual_outer_thickness); + ret.toolpath_locations.push_back(thickness - actual_outer_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<coord_t>(0)); + // Ensure correct total and left over thickness. + ret.total_thickness = thickness; + ret.left_over = thickness - std::accumulate(ret.bead_widths.cbegin(), ret.bead_widths.cend(), static_cast<coord_t>(0)); + return ret; } -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 cura diff --git a/src/BeadingStrategy/RedistributeBeadingStrategy.h b/src/BeadingStrategy/RedistributeBeadingStrategy.h index 7e1e1415e..28e767d05 100644 --- a/src/BeadingStrategy/RedistributeBeadingStrategy.h +++ b/src/BeadingStrategy/RedistributeBeadingStrategy.h @@ -1,4 +1,4 @@ -//Copyright (c) 2020 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H @@ -32,15 +32,14 @@ namespace cura * /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. + * /param minimum_variable_line_ratio 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); + RedistributeBeadingStrategy + ( + const coord_t optimal_width_outer, + const Ratio minimum_variable_line_ratio, + BeadingStrategyPtr parent + ); virtual ~RedistributeBeadingStrategy() override = default; @@ -92,8 +91,7 @@ namespace cura BeadingStrategyPtr parent; coord_t optimal_width_outer; - coord_t optimal_width_inner; - double minimum_variable_line_width; + Ratio minimum_variable_line_ratio; }; } // namespace cura diff --git a/src/BeadingStrategy/WideningBeadingStrategy.cpp b/src/BeadingStrategy/WideningBeadingStrategy.cpp index cb431528c..4abcac0f9 100644 --- a/src/BeadingStrategy/WideningBeadingStrategy.cpp +++ b/src/BeadingStrategy/WideningBeadingStrategy.cpp @@ -7,7 +7,7 @@ namespace cura { 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()) + : BeadingStrategy(*parent) , parent(std::move(parent)) , min_input_width(min_input_width) , min_output_width(min_output_width) diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index 0b967cd22..ae0abfffe 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -762,7 +762,7 @@ void FffGcodeWriter::processRaft(const SliceDataStorage& storage) max_resolution, max_deviation, wall_line_count, infill_origin, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); - VariableWidthPaths raft_paths; + std::vector<VariableWidthLines> raft_paths; infill_comp.generate(raft_paths, raft_polygons, raftLines, base_settings); if(!raft_paths.empty()) { @@ -840,7 +840,7 @@ void FffGcodeWriter::processRaft(const SliceDataStorage& storage) interface_max_resolution, interface_max_deviation, wall_line_count, infill_origin, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); - VariableWidthPaths raft_paths; //Should remain empty, since we have no walls. + std::vector<VariableWidthLines> raft_paths; //Should remain empty, since we have no walls. infill_comp.generate(raft_paths, raft_polygons, raftLines, interface_settings); gcode_layer.addLinesByOptimizer(raftLines, gcode_layer.configs_storage.raft_interface_config, SpaceFillType::Lines, false, 0, 1.0, last_planned_position); @@ -901,7 +901,7 @@ void FffGcodeWriter::processRaft(const SliceDataStorage& storage) surface_max_resolution, surface_max_deviation, wall_line_count, infill_origin, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); - VariableWidthPaths raft_paths; //Should remain empty, since we have no walls. + std::vector<VariableWidthLines> raft_paths; //Should remain empty, since we have no walls. infill_comp.generate(raft_paths, raft_polygons, raft_lines, surface_settings); gcode_layer.addLinesByOptimizer(raft_lines, gcode_layer.configs_storage.raft_surface_config, SpaceFillType::Lines, false, 0, 1.0, last_planned_position); @@ -976,12 +976,13 @@ LayerPlan& FffGcodeWriter::processLayer(const SliceDataStorage& storage, LayerIn coord_t max_inner_wall_width = 0; for (const SliceMeshStorage& mesh : storage.meshes) { - max_inner_wall_width = std::max(max_inner_wall_width, mesh.settings.get<coord_t>((mesh.settings.get<size_t>("wall_line_count") > 1) ? "wall_line_width_x" : "wall_line_width_0")); - if (layer_nr == 0) + coord_t mesh_inner_wall_width = mesh.settings.get<coord_t>((mesh.settings.get<size_t>("wall_line_count") > 1) ? "wall_line_width_x" : "wall_line_width_0"); + if(layer_nr == 0) { const ExtruderTrain& train = mesh.settings.get<ExtruderTrain&>((mesh.settings.get<size_t>("wall_line_count") > 1) ? "wall_0_extruder_nr" : "wall_x_extruder_nr"); - max_inner_wall_width *= train.settings.get<Ratio>("initial_layer_line_width_factor"); + mesh_inner_wall_width *= train.settings.get<Ratio>("initial_layer_line_width_factor"); } + max_inner_wall_width = std::max(max_inner_wall_width, mesh_inner_wall_width); } const coord_t comb_offset_from_outlines = max_inner_wall_width * 2; @@ -1408,7 +1409,7 @@ void FffGcodeWriter::addMeshLayerToGCode(const SliceDataStorage& storage, const part_order_optimizer.addPolygon(&part); } part_order_optimizer.optimize(); - for(const PathOrderOptimizer<const SliceLayerPart*>::Path& path : part_order_optimizer.paths) + for(const PathOrderPath<const SliceLayerPart*>& path : part_order_optimizer.paths) { addMeshPartToGCode(storage, mesh, extruder_nr, mesh_config, *path.vertices, gcode_layer); } @@ -1453,7 +1454,7 @@ void FffGcodeWriter::addMeshPartToGCode(const SliceDataStorage& storage, const S { innermost_wall_line_width *= mesh.settings.get<Ratio>("initial_layer_line_width_factor"); } - gcode_layer.moveInsideCombBoundary(innermost_wall_line_width); + gcode_layer.moveInsideCombBoundary(innermost_wall_line_width, part); } gcode_layer.setIsInside(false); @@ -1503,7 +1504,7 @@ bool FffGcodeWriter::processMultiLayerInfill(const SliceDataStorage& storage, La const size_t infill_multiplier = mesh.settings.get<size_t>("infill_multiplier"); Polygons infill_polygons; Polygons infill_lines; - VariableWidthPaths infill_paths = part.infill_wall_toolpaths; + std::vector<VariableWidthLines> infill_paths = part.infill_wall_toolpaths; for (size_t density_idx = part.infill_area_per_combine_per_density.size() - 1; (int)density_idx >= 0; density_idx--) { // combine different density infill areas (for gradual infill) size_t density_factor = 2 << density_idx; // == pow(2, density_idx + 1) @@ -1575,7 +1576,7 @@ bool FffGcodeWriter::processSingleLayerInfill(const SliceDataStorage& storage, L //Combine the 1 layer thick infill with the top/bottom skin and print that as one thing. Polygons infill_polygons; - std::vector<VariableWidthPaths> wall_tool_paths; + std::vector<std::vector<VariableWidthLines>> wall_tool_paths; // All wall toolpaths binned by inset_idx (inner) and by density_idx (outer) Polygons infill_lines; const auto pattern = mesh.settings.get<EFillMethod>("infill_pattern"); @@ -1682,7 +1683,7 @@ bool FffGcodeWriter::processSingleLayerInfill(const SliceDataStorage& storage, L // infill region with skin above has to have at least one infill wall line const size_t min_skin_below_wall_count = wall_line_count > 0 ? wall_line_count : 1; const size_t skin_below_wall_count = density_idx == last_idx ? min_skin_below_wall_count : 0; - wall_tool_paths.emplace_back(VariableWidthPaths()); + wall_tool_paths.emplace_back(std::vector<VariableWidthLines>()); const coord_t overlap = infill_overlap - (density_idx == last_idx ? 0 : wall_line_count * infill_line_width); Infill infill_comp(pattern, zig_zaggify_infill, connect_polygons, infill_below_skin, infill_line_width, infill_line_distance_here, overlap, infill_multiplier, infill_angle, gcode_layer.z, @@ -1740,7 +1741,7 @@ bool FffGcodeWriter::processSingleLayerInfill(const SliceDataStorage& storage, L } wall_tool_paths.emplace_back(part.infill_wall_toolpaths); //The extra infill walls were generated separately. Add these too. - const bool walls_generated = std::any_of(wall_tool_paths.cbegin(), wall_tool_paths.cend(), [](const VariableWidthPaths& tp){ return !tp.empty(); }); + const bool walls_generated = std::any_of(wall_tool_paths.cbegin(), wall_tool_paths.cend(), [](const std::vector<VariableWidthLines>& tp){ return !tp.empty(); }); if(!infill_lines.empty() || !infill_polygons.empty() || walls_generated) { added_something = true; @@ -1761,7 +1762,7 @@ bool FffGcodeWriter::processSingleLayerInfill(const SliceDataStorage& storage, L } else //So walls_generated must be true. { - VariableWidthPaths* start_paths = &wall_tool_paths[rand() % wall_tool_paths.size()]; + std::vector<VariableWidthLines>* start_paths = &wall_tool_paths[rand() % wall_tool_paths.size()]; while(start_paths->empty()) //We know for sure (because walls_generated) that one of them is not empty. So randomise until we hit it. Should almost always be very quick. { start_paths = &wall_tool_paths[rand() % wall_tool_paths.size()]; @@ -1771,7 +1772,7 @@ bool FffGcodeWriter::processSingleLayerInfill(const SliceDataStorage& storage, L } if (walls_generated) { - for(const VariableWidthPaths& tool_paths: wall_tool_paths) + for(const std::vector<VariableWidthLines>& tool_paths: wall_tool_paths) { constexpr bool retract_before_outer_wall = false; constexpr coord_t wipe_dist = 0; @@ -2186,7 +2187,7 @@ bool FffGcodeWriter::processSkin(const SliceDataStorage& storage, LayerPlan& gco } part_order_optimizer.optimize(); - for(const PathOrderOptimizer<const SkinPart*>::Path& path : part_order_optimizer.paths) + for(const PathOrderPath<const SkinPart*>& path : part_order_optimizer.paths) { const SkinPart& skin_part = *path.vertices; @@ -2203,10 +2204,7 @@ bool FffGcodeWriter::processSkinPart(const SliceDataStorage& storage, LayerPlan& gcode_layer.mode_skip_agressive_merge = true; - // add roofing processRoofing(storage, gcode_layer, mesh, extruder_nr, mesh_config, skin_part, added_something); - - // add normal skinfill processTopBottom(storage, gcode_layer, mesh, extruder_nr, mesh_config, skin_part, added_something); gcode_layer.mode_skip_agressive_merge = false; @@ -2222,7 +2220,6 @@ void FffGcodeWriter::processRoofing(const SliceDataStorage& storage, LayerPlan& } const EFillMethod pattern = mesh.settings.get<EFillMethod>("roofing_pattern"); - AngleDegrees roofing_angle = 45; if (mesh.roofing_angles.size() > 0) { @@ -2404,7 +2401,7 @@ void FffGcodeWriter::processSkinPrintFeature(const SliceDataStorage& storage, La { Polygons skin_polygons; Polygons skin_lines; - VariableWidthPaths skin_paths; + std::vector<VariableWidthLines> skin_paths; constexpr int infill_multiplier = 1; constexpr int extra_infill_shift = 0; @@ -2414,6 +2411,7 @@ void FffGcodeWriter::processSkinPrintFeature(const SliceDataStorage& storage, La coord_t max_resolution = mesh.settings.get<coord_t>("meshfix_maximum_resolution"); coord_t max_deviation = mesh.settings.get<coord_t>("meshfix_maximum_deviation"); const Point infill_origin; + const bool skip_line_stitching = monotonic; constexpr bool connected_zigzags = false; constexpr bool use_endpieces = true; constexpr bool skip_some_zags = false; @@ -2424,6 +2422,7 @@ void FffGcodeWriter::processSkinPrintFeature(const SliceDataStorage& storage, La pattern, zig_zaggify_infill, connect_polygons, area, config.getLineWidth(), config.getLineWidth() / skin_density, skin_overlap, infill_multiplier, skin_angle, gcode_layer.z, extra_infill_shift , max_resolution, max_deviation , wall_line_count, infill_origin, + skip_line_stitching, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); infill_comp.generate(skin_paths, skin_polygons, skin_lines, mesh.settings); @@ -2659,7 +2658,7 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer constexpr bool connect_polygons = false; // polygons are too distant to connect for sparse support //Print the thicker infill lines first. (double or more layer thickness, infill combined with previous layers) - for(const PathOrderOptimizer<const SupportInfillPart*>::Path& path : island_order_optimizer.paths) + for(const PathOrderPath<const SupportInfillPart*>& path : island_order_optimizer.paths) { const SupportInfillPart& part = *path.vertices; @@ -2667,7 +2666,7 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer const int current_support_infill_overlap = (part.inset_count_to_generate > 0) ? default_support_infill_overlap : 0; //The support infill walls were generated separately, first. Always add them, regardless of how many densities we have. - VariableWidthPaths wall_toolpaths = part.wall_toolpaths; + std::vector<VariableWidthLines> wall_toolpaths = part.wall_toolpaths; if ( ! wall_toolpaths.empty()) { @@ -2691,7 +2690,7 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer const coord_t support_line_width = default_support_line_width * (combine_idx + 1); Polygons support_polygons; - VariableWidthPaths wall_toolpaths_here; + std::vector<VariableWidthLines> wall_toolpaths_here; Polygons support_lines; const size_t max_density_idx = part.infill_area_per_combine_per_density.size() - 1; for(size_t density_idx = max_density_idx; (density_idx + 1) > 0; --density_idx) @@ -2721,7 +2720,6 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer wall_count, infill_origin, support_connect_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size); infill_comp.generate(wall_toolpaths_here, support_polygons, support_lines, infill_extruder.settings, storage.support.cross_fill_provider); - assert(wall_toolpaths_here.empty()); } setExtruder_addPrime(storage, gcode_layer, extruder_nr); // only switch extruder if we're sure we're going to switch @@ -2780,6 +2778,21 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer added_something = true; } + + //If we're printing with a support wall, that support wall generates gap filling as well. + //If not, the pattern may still generate gap filling (if it's connected infill or zigzag). We still want to print those. + if(wall_line_count == 0 && !wall_toolpaths_here.empty()) + { + const GCodePathConfig& config = gcode_layer.configs_storage.support_infill_config[0]; + constexpr bool retract_before_outer_wall = false; + constexpr coord_t wipe_dist = 0; + constexpr coord_t simplify_curvature = 0; + const ZSeamConfig z_seam_config(EZSeamType::SHORTEST, gcode_layer.getLastPlannedPositionOrStartingPosition(), EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE, simplify_curvature); + InsetOrderOptimizer wall_orderer(*this, storage, gcode_layer, infill_extruder.settings, extruder_nr, + config, config, config, config, + retract_before_outer_wall, wipe_dist, wipe_dist, extruder_nr, extruder_nr, z_seam_config, wall_toolpaths); + added_something |= wall_orderer.addToLayer(); + } } } @@ -2848,7 +2861,7 @@ bool FffGcodeWriter::addSupportRoofsToGCode(const SliceDataStorage& storage, Lay wall_line_count, infill_origin, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); Polygons roof_polygons; - VariableWidthPaths roof_paths; + std::vector<VariableWidthLines> roof_paths; Polygons roof_lines; roof_computation.generate(roof_paths, roof_polygons, roof_lines, roof_extruder.settings); if ((gcode_layer.getLayerNr() == 0 && wall.empty()) || (gcode_layer.getLayerNr() > 0 && roof_paths.empty() && roof_polygons.empty() && roof_lines.empty())) @@ -2929,7 +2942,7 @@ bool FffGcodeWriter::addSupportBottomsToGCode(const SliceDataStorage& storage, L wall_line_count, infill_origin, connected_zigzags, use_endpieces, skip_some_zags, zag_skip_count, pocket_size ); Polygons bottom_polygons; - VariableWidthPaths bottom_paths; + std::vector<VariableWidthLines> bottom_paths; Polygons bottom_lines; bottom_computation.generate(bottom_paths, bottom_polygons, bottom_lines, bottom_extruder.settings); if (bottom_paths.empty() && bottom_polygons.empty() && bottom_lines.empty()) diff --git a/src/FffPolygonGenerator.cpp b/src/FffPolygonGenerator.cpp index 11c995495..6350e7c19 100644 --- a/src/FffPolygonGenerator.cpp +++ b/src/FffPolygonGenerator.cpp @@ -3,6 +3,7 @@ #include <algorithm> #include <map> // multimap (ordered map allowing duplicate keys) +#include <numeric> #include <fstream> // ifstream.good() #ifdef _OPENMP @@ -710,7 +711,7 @@ void FffPolygonGenerator::processDerivedWallsSkinInfill(SliceMeshStorage& mesh) SkinInfillAreaComputation::combineInfillLayers(mesh); // fuzzy skin - if (mesh.settings.get<bool>("magic_fuzzy_skin_enabled") && false) //TODO make fuzzy skin work with libArachne (CURA-7887) and then re-enable it + if (mesh.settings.get<bool>("magic_fuzzy_skin_enabled")) { processFuzzyWalls(mesh); } @@ -1040,76 +1041,118 @@ void FffPolygonGenerator::processPlatformAdhesion(SliceDataStorage& storage) void FffPolygonGenerator::processFuzzyWalls(SliceMeshStorage& mesh) -{//TODO make fuzzy skin work with libArachne (CURA-7887) +{ if (mesh.settings.get<size_t>("wall_line_count") == 0) { return; } + + const coord_t line_width = mesh.settings.get<coord_t>("line_width"); + const bool apply_outside_only = mesh.settings.get<bool>("magic_fuzzy_skin_outside_only"); const coord_t fuzziness = mesh.settings.get<coord_t>("magic_fuzzy_skin_thickness"); const coord_t avg_dist_between_points = mesh.settings.get<coord_t>("magic_fuzzy_skin_point_dist"); const coord_t min_dist_between_points = avg_dist_between_points * 3 / 4; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value const coord_t range_random_point_dist = avg_dist_between_points / 2; - unsigned int start_layer_nr = (mesh.settings.get<EPlatformAdhesion>("adhesion_type") == EPlatformAdhesion::BRIM)? 1 : 0; // don't make fuzzy skin on first layer if there's a brim + unsigned int start_layer_nr = (mesh.settings.get<EPlatformAdhesion>("adhesion_type") == EPlatformAdhesion::BRIM)? 1 : 0; // don't make fuzzy skin on first layer if there's a brim + + auto hole_area = Polygons(); + std::function<bool(const bool&, const ExtrusionJunction&)> accumulate_is_in_hole = [](const bool& prev_result, const ExtrusionJunction& junction) { return false; }; + for (unsigned int layer_nr = start_layer_nr; layer_nr < mesh.layers.size(); layer_nr++) { SliceLayer& layer = mesh.layers[layer_nr]; for (SliceLayerPart& part : layer.parts) { - Polygons results; -// Polygons& skin = (mesh.settings.get<ESurfaceMode>("magic_mesh_surface_mode") == ESurfaceMode::SURFACE)? part.outline : part.insets[0]; insets no longer used in libArachne - Polygons& skin = part.outline; - for (PolygonRef poly : skin) + std::vector<VariableWidthLines> result_paths; + for (auto& toolpath : part.wall_toolpaths) { - if (mesh.settings.get<bool>("magic_fuzzy_skin_outside_only") && poly.area() < 0) + if (toolpath.front().inset_idx != 0) { - results.add(poly); + result_paths.push_back(toolpath); continue; } - // generate points in between p0 and p1 - PolygonRef result = results.newPoly(); - - int64_t dist_left_over = rand() % (min_dist_between_points / 2); // the distance to be traversed on the line before making the first new point - Point* p0 = &poly.back(); - for (Point& p1 : poly) - { // 'a' is the (next) new point between p0 and p1 - Point p0p1 = p1 - *p0; - int64_t p0p1_size = vSize(p0p1); - int64_t p0pa_dist = dist_left_over; - if (p0pa_dist >= p0p1_size) + + result_paths.emplace_back(); + auto& result_lines = result_paths.back(); + + if (apply_outside_only) + { + hole_area = part.print_outline.getOutsidePolygons().offset(-line_width); + accumulate_is_in_hole = + [&hole_area](const bool& prev_result, const ExtrusionJunction& junction) { return prev_result || hole_area.inside(junction.p); }; + } + for (auto& line : toolpath) + { + if (apply_outside_only && std::accumulate(line.begin(), line.end(), false, accumulate_is_in_hole)) { - result.add(p1 - (p0p1 / 2)); + result_lines.push_back(line); + continue; } - for (; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + rand() % range_random_point_dist) + + result_lines.emplace_back(); + auto& result = result_lines.back(); + result.inset_idx = line.inset_idx; + + // generate points in between p0 and p1 + int64_t dist_left_over = (min_dist_between_points / 4) + rand() % (min_dist_between_points / 4); // the distance to be traversed on the line before making the first new point + auto* p0 = &line.front(); + for (auto& p1 : line) { - int r = rand() % (fuzziness * 2) - fuzziness; - Point perp_to_p0p1 = turn90CCW(p0p1); - Point fuzz = normal(perp_to_p0p1, r); - Point pa = *p0 + normal(p0p1, p0pa_dist) + fuzz; - result.add(pa); - } - // p0pa_dist > p0p1_size now because we broke out of the for-loop - dist_left_over = p0pa_dist - p0p1_size; + if (p0->p == p1.p) // avoid seams + { + result.emplace_back(p1.p, p1.w, p1.perimeter_index); + continue; + } - p0 = &p1; - } - while (result.size() < 3) - { - size_t point_idx = poly.size() - 2; - result.add(poly[point_idx]); - if (point_idx == 0) + // 'a' is the (next) new point between p0 and p1 + const Point p0p1 = p1.p - p0->p; + const int64_t p0p1_size = vSize(p0p1); + int64_t p0pa_dist = dist_left_over; + if (p0pa_dist >= p0p1_size) + { + const Point p = p1.p - (p0p1 / 2); + const double width = (p1.w * vSize(p1.p - p) + p0->w * vSize(p0->p - p)) / p0p1_size; + result.emplace_back(p, width, p1.perimeter_index); + } + for (; p0pa_dist < p0p1_size; p0pa_dist += min_dist_between_points + rand() % range_random_point_dist) + { + const int r = rand() % (fuzziness * 2) - fuzziness; + const Point perp_to_p0p1 = turn90CCW(p0p1); + const Point fuzz = normal(perp_to_p0p1, r); + const Point pa = p0->p + normal(p0p1, p0pa_dist); + const double width = (p1.w * vSize(p1.p - pa) + p0->w * vSize(p0->p - pa)) / p0p1_size; + result.emplace_back(pa + fuzz, width, p1.perimeter_index); + } + // p0pa_dist > p0p1_size now because we broke out of the for-loop + dist_left_over = p0pa_dist - p0p1_size; + + p0 = &p1; + } + while (result.size() < 3) + { + size_t point_idx = line.size() - 2; + result.emplace_back(line[point_idx].p, line[point_idx].w, line[point_idx].perimeter_index); + if (point_idx == 0) + { + break; + } + point_idx--; + } + if (result.size() < 3) { - break; + result.clear(); + for (auto& p : line) + { + result.emplace_back(p.p, p.w, p.perimeter_index); + } + } + if (line.back().p == line.front().p) // avoid seams + { + result.back().p = result.front().p; } - point_idx--; - } - if (result.size() < 3) - { - result.clear(); - for (Point& p : poly) - result.add(p); } } - skin = results; + part.wall_toolpaths = result_paths; } } } diff --git a/src/InsetOrderOptimizer.cpp b/src/InsetOrderOptimizer.cpp index e51f349f0..f606c13bb 100644 --- a/src/InsetOrderOptimizer.cpp +++ b/src/InsetOrderOptimizer.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "ExtruderTrain.h" @@ -26,7 +26,7 @@ InsetOrderOptimizer::InsetOrderOptimizer(const FffGcodeWriter& gcode_writer, const size_t wall_0_extruder_nr, const size_t wall_x_extruder_nr, const ZSeamConfig& z_seam_config, - const VariableWidthPaths& paths) : + const std::vector<VariableWidthLines>& paths) : gcode_writer(gcode_writer), storage(storage), gcode_layer(gcode_layer), @@ -54,7 +54,6 @@ bool InsetOrderOptimizer::addToLayer() // Settings & configs: const bool pack_by_inset = ! settings.get<bool>("optimize_wall_printing_order"); const InsetDirection inset_direction = settings.get<InsetDirection>("inset_direction"); - const bool center_last = inset_direction == InsetDirection::CENTER_LAST; const bool alternate_walls = settings.get<bool>("material_alternate_walls"); const bool outer_to_inner = inset_direction == InsetDirection::OUTSIDE_IN; @@ -129,23 +128,6 @@ bool InsetOrderOptimizer::addToLayer() getInsetOrder(walls_to_be_added, outer_to_inner) : getRegionOrder(walls_to_be_added, outer_to_inner); - if (center_last) - { - for (const ExtrusionLine* line : walls_to_be_added) - { - if (line->is_odd) - { - for (const ExtrusionLine* other_line : walls_to_be_added) - { - if ( ! other_line->is_odd) - { - order.emplace(std::make_pair(other_line, line)); - } - } - } - } - } - constexpr Ratio flow = 1.0_r; bool added_something = false; @@ -173,7 +155,7 @@ bool InsetOrderOptimizer::addToLayer() order_optimizer.optimize(); cura::Point p_end {0, 0}; - for(const PathOrderOptimizer<const ExtrusionLine*>::Path& path : order_optimizer.paths) + for(const PathOrderPath<const ExtrusionLine*>& path : order_optimizer.paths) { if (path.vertices->empty()) continue; @@ -354,7 +336,6 @@ std::unordered_set<std::pair<const ExtrusionLine*, const ExtrusionLine*>> InsetO { for (const ExtrusionLine* line : fillers_by_inset[inset_idx]) { - assert(inset_idx - 1 < walls_by_inset.size()); if (inset_idx - 1 >= walls_by_inset.size()) continue; for (const ExtrusionLine* enclosing_wall : walls_by_inset[inset_idx - 1]) { diff --git a/src/InsetOrderOptimizer.h b/src/InsetOrderOptimizer.h index 23ed9a8ea..d8aa87d48 100644 --- a/src/InsetOrderOptimizer.h +++ b/src/InsetOrderOptimizer.h @@ -51,7 +51,7 @@ public: const size_t wall_0_extruder_nr, const size_t wall_x_extruder_nr, const ZSeamConfig& z_seam_config, - const VariableWidthPaths& paths); + const std::vector<VariableWidthLines>& paths); /*! * Adds the insets to the given layer plan. @@ -104,7 +104,7 @@ private: const size_t wall_0_extruder_nr; const size_t wall_x_extruder_nr; const ZSeamConfig& z_seam_config; - const VariableWidthPaths& paths; + const std::vector<VariableWidthLines>& paths; const unsigned int layer_nr; bool added_something; diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index 89de2cfd5..74b946293 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include <cstring> @@ -85,25 +85,24 @@ void ExtruderPlan::applyBackPressureCompensation(const Ratio back_pressure_compe constexpr double epsilon_speed_factor = 0.001; // Don't put on actual 'limit double minimum', because we don't want printers to stall. for (auto& path : paths) { - const Ratio nominal_flow_for_path = path.config->getFlowRatio(); const double nominal_width_for_path = static_cast<double>(path.config->getLineWidth()); - if (path.flow <= 0.0 || nominal_flow_for_path <= 0.0 || nominal_width_for_path <= 0.0 || path.config->isTravelPath() || path.config->isBridgePath()) + if(path.width_factor <= 0.0 || nominal_width_for_path <= 0.0 || path.config->isTravelPath() || path.config->isBridgePath()) { continue; } - const double line_width_for_path = path.flow * nominal_flow_for_path * nominal_width_for_path; + const double line_width_for_path = path.width_factor * nominal_width_for_path; path.speed_back_pressure_factor = std::max(epsilon_speed_factor, 1.0 + (nominal_width_for_path / line_width_for_path - 1.0) * back_pressure_compensation); } } -GCodePath* LayerPlan::getLatestPathWithConfig(const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio flow, bool spiralize, const Ratio speed_factor) +GCodePath* LayerPlan::getLatestPathWithConfig(const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio flow, const Ratio width_factor, bool spiralize, const Ratio speed_factor) { std::vector<GCodePath>& paths = extruder_plans.back().paths; - if (paths.size() > 0 && paths.back().config == &config && !paths.back().done && paths.back().flow == flow && paths.back().speed_factor == speed_factor && paths.back().mesh_id == current_mesh) // spiralize can only change when a travel path is in between + if(paths.size() > 0 && paths.back().config == &config && !paths.back().done && paths.back().flow == flow && paths.back().width_factor == width_factor && paths.back().speed_factor == speed_factor && paths.back().mesh_id == current_mesh) // spiralize can only change when a travel path is in between { return &paths.back(); } - paths.emplace_back(config, current_mesh, space_fill_type, flow, spiralize, speed_factor); + paths.emplace_back(config, current_mesh, space_fill_type, flow, width_factor, spiralize, speed_factor); GCodePath* ret = &paths.back(); ret->skip_agressive_merge_hint = mode_skip_agressive_merge; return ret; @@ -301,16 +300,17 @@ void LayerPlan::setMesh(const std::string mesh_id) current_mesh = mesh_id; } -void LayerPlan::moveInsideCombBoundary(const coord_t distance) +void LayerPlan::moveInsideCombBoundary(const coord_t distance, const std::optional<SliceLayerPart>& part) { constexpr coord_t max_dist2 = MM2INT(2.0) * MM2INT(2.0); // if we are further than this distance, we conclude we are not inside even though we thought we were. - // this function is to be used to move from the boudary of a part to inside the part + // this function is to be used to move from the boundary of a part to inside the part Point p = getLastPlannedPositionOrStartingPosition(); // copy, since we are going to move p if (PolygonUtils::moveInside(comb_boundary_preferred, p, distance, max_dist2) != NO_INDEX) { //Move inside again, so we move out of tight 90deg corners PolygonUtils::moveInside(comb_boundary_preferred, p, distance, max_dist2); - if (comb_boundary_preferred.inside(p)) + if (comb_boundary_preferred.inside(p) && + (part == std::nullopt || part->outline.inside(p))) { addTravel_simple(p); //Make sure the that any retraction happens after this move, not before it by starting a new move path. @@ -510,9 +510,9 @@ void LayerPlan::planPrime(const float& prime_blob_wipe_length) forceNewPathStart(); } -void LayerPlan::addExtrusionMove(Point p, const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio& flow, bool spiralize, Ratio speed_factor, double fan_speed) +void LayerPlan::addExtrusionMove(Point p, const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio& flow, const Ratio width_factor, bool spiralize, Ratio speed_factor, double fan_speed) { - GCodePath* path = getLatestPathWithConfig(config, space_fill_type, flow, spiralize, speed_factor); + GCodePath* path = getLatestPathWithConfig(config, space_fill_type, flow, width_factor, spiralize, speed_factor); path->points.push_back(p); path->setFanSpeed(fan_speed); last_planned_position = p; @@ -520,19 +520,20 @@ void LayerPlan::addExtrusionMove(Point p, const GCodePathConfig& config, SpaceFi void LayerPlan::addPolygon(ConstPolygonRef polygon, int start_idx, const bool backwards, const GCodePathConfig& config, coord_t wall_0_wipe_dist, bool spiralize, const Ratio& flow_ratio, bool always_retract) { + constexpr Ratio width_ratio = 1.0_r; //Not printed with variable line width. Point p0 = polygon[start_idx]; addTravel(p0, always_retract); const int direction = backwards ? -1 : 1; for(size_t point_idx = 1; point_idx < polygon.size(); point_idx++) { Point p1 = polygon[(start_idx + point_idx * direction + polygon.size()) % polygon.size()]; - addExtrusionMove(p1, config, SpaceFillType::Polygons, flow_ratio, spiralize); + addExtrusionMove(p1, config, SpaceFillType::Polygons, flow_ratio, width_ratio, spiralize); p0 = p1; } if(polygon.size() > 2) { const Point& p1 = polygon[start_idx]; - addExtrusionMove(p1, config, SpaceFillType::Polygons, flow_ratio, spiralize); + addExtrusionMove(p1, config, SpaceFillType::Polygons, flow_ratio, width_ratio, spiralize); if(wall_0_wipe_dist > 0) { // apply outer wall wipe @@ -580,7 +581,7 @@ void LayerPlan::addPolygonsByOptimizer(const Polygons& polygons, const GCodePath if(!reverse_order) { - for(const PathOrderOptimizer<ConstPolygonPointer>::Path& path : orderOptimizer.paths) + for(const PathOrderPath<ConstPolygonPointer>& path : orderOptimizer.paths) { addPolygon(*path.vertices, path.start_vertex, path.backwards, config, wall_0_wipe_dist, spiralize, flow_ratio, always_retract); } @@ -589,7 +590,7 @@ void LayerPlan::addPolygonsByOptimizer(const Polygons& polygons, const GCodePath { for(int index = orderOptimizer.paths.size() - 1; index >= 0; --index) { - const PathOrderOptimizer<ConstPolygonPointer>::Path& path = orderOptimizer.paths[index]; + const PathOrderPath<ConstPolygonPointer>& path = orderOptimizer.paths[index]; addPolygon(**path.vertices, path.start_vertex, path.backwards, config, wall_0_wipe_dist, spiralize, flow_ratio, always_retract); } } @@ -597,7 +598,7 @@ void LayerPlan::addPolygonsByOptimizer(const Polygons& polygons, const GCodePath static constexpr float max_non_bridge_line_volume = MM2INT(100); // limit to accumulated "volume" of non-bridge lines which is proportional to distance x extrusion rate -void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& settings, const GCodePathConfig& non_bridge_config, const GCodePathConfig& bridge_config, float flow, float& non_bridge_line_volume, Ratio speed_factor, double distance_to_bridge_start) +void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& settings, const GCodePathConfig& non_bridge_config, const GCodePathConfig& bridge_config, float flow, const Ratio width_factor, float& non_bridge_line_volume, Ratio speed_factor, double distance_to_bridge_start) { const coord_t min_line_len = 5; // we ignore lines less than 5um long const double acceleration_segment_len = MM2INT(1); // accelerate using segments of this length @@ -652,15 +653,16 @@ void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& se if ((len - coast_dist) > min_line_len) { // segment is longer than coast distance so extrude using non-bridge config to start of coast - addExtrusionMove(segment_end + coast_dist * (cur_point - segment_end) / len, non_bridge_config, SpaceFillType::Polygons, segment_flow, spiralize, speed_factor); + addExtrusionMove(segment_end + coast_dist * (cur_point - segment_end) / len, non_bridge_config, SpaceFillType::Polygons, segment_flow, width_factor, spiralize, speed_factor); } // then coast to start of bridge segment - addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, 0, spiralize, speed_factor); + constexpr Ratio flow = 0.0_r; //Coasting has no flow rate. + addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, flow, width_factor, spiralize, speed_factor); } else { // no coasting required, just normal segment using non-bridge config - addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, segment_flow, spiralize, + addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, segment_flow, width_factor, spiralize, (overhang_mask.empty() || (!overhang_mask.inside(p0, true) && !overhang_mask.inside(p1, true))) ? speed_factor : overhang_speed_factor); } @@ -669,10 +671,10 @@ void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& se else { // no coasting required, just normal segment using non-bridge config - addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, segment_flow, spiralize, + addExtrusionMove(segment_end, non_bridge_config, SpaceFillType::Polygons, segment_flow, width_factor, spiralize, (overhang_mask.empty() || (!overhang_mask.inside(p0, true) && !overhang_mask.inside(p1, true))) ? speed_factor : overhang_speed_factor); } - non_bridge_line_volume += vSize(cur_point - segment_end) * segment_flow * speed_factor * non_bridge_config.getSpeed(); + non_bridge_line_volume += vSize(cur_point - segment_end) * segment_flow * width_factor * speed_factor * non_bridge_config.getSpeed(); cur_point = segment_end; speed_factor = 1 - (1 - speed_factor) * acceleration_factor; if (speed_factor >= 0.9) @@ -686,7 +688,7 @@ void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& se if (bridge_wall_mask.empty()) { // no bridges required - addExtrusionMove(p1, non_bridge_config, SpaceFillType::Polygons, flow, spiralize, + addExtrusionMove(p1, non_bridge_config, SpaceFillType::Polygons, flow, width_factor, spiralize, (overhang_mask.empty() || (!overhang_mask.inside(p0, true) && !overhang_mask.inside(p1, true))) ? 1.0_r : overhang_speed_factor); } else @@ -744,7 +746,7 @@ void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& se if (bridge_line_len > min_line_len) { - addExtrusionMove(b1, bridge_config, SpaceFillType::Polygons, flow); + addExtrusionMove(b1, bridge_config, SpaceFillType::Polygons, flow, width_factor); non_bridge_line_volume = 0; cur_point = b1; // after a bridge segment, start slow and accelerate to avoid under-extrusion due to extruder lag @@ -768,7 +770,7 @@ void LayerPlan::addWallLine(const Point& p0, const Point& p1, const Settings& se else if (bridge_wall_mask.inside(p0, true) && vSize(p0 - p1) >= min_bridge_line_len) { // both p0 and p1 must be above air (the result will be ugly!) - addExtrusionMove(p1, bridge_config, SpaceFillType::Polygons, flow); + addExtrusionMove(p1, bridge_config, SpaceFillType::Polygons, flow, width_factor); non_bridge_line_volume = 0; } else @@ -968,12 +970,12 @@ void LayerPlan::addWall(const ExtrusionLine& wall, int start_idx, const Settings if(is_small_feature) { constexpr bool spiralize = false; - addExtrusionMove(destination, non_bridge_config, SpaceFillType::Polygons, flow_ratio * (line_width * nominal_line_width_multiplier), spiralize, small_feature_speed_factor); + addExtrusionMove(destination, non_bridge_config, SpaceFillType::Polygons, flow_ratio, line_width * nominal_line_width_multiplier, spiralize, small_feature_speed_factor); } else { const Point origin = p0.p + normal(line_vector, piece_length * piece); - addWallLine(origin, destination, settings, non_bridge_config, bridge_config, flow_ratio * (line_width * nominal_line_width_multiplier), non_bridge_line_volume, speed_factor, distance_to_bridge_start); + addWallLine(origin, destination, settings, non_bridge_config, bridge_config, flow_ratio, line_width * nominal_line_width_multiplier, non_bridge_line_volume, speed_factor, distance_to_bridge_start); } } @@ -1031,9 +1033,10 @@ void LayerPlan::addInfillWall(const ExtrusionLine& wall, const GCodePathConfig& for (const auto &junction_n : wall) { - const double flow = junction_n.w / Ratio(path_config.getLineWidth()); + const Ratio width_factor = junction_n.w / Ratio(path_config.getLineWidth()); constexpr SpaceFillType space_fill_type = SpaceFillType::Polygons; - addExtrusionMove(junction_n.p, path_config, space_fill_type, flow); + constexpr Ratio flow = 1.0_r; + addExtrusionMove(junction_n.p, path_config, space_fill_type, flow, width_factor); junction = junction_n; } } @@ -1057,7 +1060,7 @@ void LayerPlan::addWalls orderOptimizer.addPolygon(walls[poly_idx]); } orderOptimizer.optimize(); - for(const PathOrderOptimizer<ConstPolygonPointer>::Path& path : orderOptimizer.paths) + for(const PathOrderPath<ConstPolygonPointer>& path : orderOptimizer.paths) { addWall(**path.vertices, path.start_vertex, settings, non_bridge_config, bridge_config, wall_0_wipe_dist, flow_ratio, always_retract); } @@ -1106,12 +1109,25 @@ void LayerPlan::addLinesByOptimizer order_optimizer.addPolyline(polygons[line_idx]); } order_optimizer.optimize(); + + addLinesInGivenOrder(order_optimizer.paths, config, space_fill_type, wipe_dist, flow_ratio, fan_speed); +} + +void LayerPlan::addLinesInGivenOrder( + const std::vector<PathOrderPath<ConstPolygonPointer>>& paths, + const GCodePathConfig& config, + const SpaceFillType space_fill_type, + const coord_t wipe_dist, + const Ratio flow_ratio, + const double fan_speed +) +{ coord_t half_line_width = config.getLineWidth() / 2; coord_t line_width_2 = half_line_width * half_line_width; - for(size_t order_idx = 0; order_idx < order_optimizer.paths.size(); order_idx++) + for(size_t order_idx = 0; order_idx < paths.size(); order_idx++) { - const PathOrderOptimizer<ConstPolygonPointer>::Path& path = order_optimizer.paths[order_idx]; + const PathOrderPath<ConstPolygonPointer>& path = paths[order_idx]; ConstPolygonRef polyline = *path.vertices; const size_t start_idx = path.start_vertex; assert(start_idx == 0 || start_idx == polyline.size() - 1 || path.is_closed); @@ -1121,7 +1137,11 @@ void LayerPlan::addLinesByOptimizer { // Instead of doing a small travel that is shorter than the line width (which is generally done at pretty high jerk & move) do a // "fake" extrusion move - addExtrusionMove(start, config, space_fill_type, 0, false, 1.0, fan_speed); + constexpr Ratio flow = 0.0_r; + constexpr Ratio width_factor = 1.0_r; + constexpr bool spiralize = false; + constexpr Ratio speed_factor = 1.0_r; + addExtrusionMove(start, config, space_fill_type, flow, width_factor, spiralize, speed_factor, fan_speed); } else { @@ -1150,13 +1170,16 @@ void LayerPlan::addLinesByOptimizer // ignore line segments that are less than 5uM long if (vSize2(p1 - p0) >= MINIMUM_SQUARED_LINE_LENGTH) { - addExtrusionMove(p1, config, space_fill_type, flow_ratio, false, 1.0, fan_speed); + constexpr Ratio width_factor = 1.0_r; + constexpr bool spiralize = false; + constexpr Ratio speed_factor = 1.0_r; + addExtrusionMove(p1, config, space_fill_type, flow_ratio, width_factor, spiralize, speed_factor, fan_speed); p0 = p1; } } - p0 = polyline[(start_idx == 0) ? polyline.size() - 1 : 0]; - Point p1 = (polyline.size() <= 1) ? p0 + Point p1 = polyline[(start_idx == 0) ? polyline.size() - 1 : 0]; + p0 = (polyline.size() <= 1) ? p1 : polyline[(start_idx == 0) ? polyline.size() - 2 : 1]; // Wipe @@ -1172,9 +1195,9 @@ void LayerPlan::addLinesByOptimizer } // Don't wipe if next starting point is very near - if(wipe && (order_idx < order_optimizer.paths.size() - 1)) + if(wipe && (order_idx < paths.size() - 1)) { - const PathOrderOptimizer<ConstPolygonPointer>::Path& next_path = order_optimizer.paths[order_idx + 1]; + const PathOrderPath<ConstPolygonPointer>& next_path = paths[order_idx + 1]; ConstPolygonRef next_polygon = *next_path.vertices; const size_t next_start = next_path.start_vertex; const Point& next_p0 = next_polygon[next_start]; @@ -1186,7 +1209,11 @@ void LayerPlan::addLinesByOptimizer if (wipe) { - addExtrusionMove(p1 + normal(p1 - p0, wipe_dist), config, space_fill_type, 0.0, false, 1.0, fan_speed); + constexpr Ratio flow = 0.0_r; + constexpr Ratio width_factor = 1.0_r; + constexpr bool spiralize = false; + constexpr Ratio speed_factor = 1.0_r; + addExtrusionMove(p1 + normal(p1 - p0, wipe_dist), config, space_fill_type, flow, width_factor, spiralize, speed_factor, fan_speed); } } } @@ -1225,7 +1252,7 @@ void LayerPlan::addLinesMonotonic }; // Order monotonically, except for line-segments which stay in the excluded areas (read: close to the walls) consecutively. - PathOrderMonotonic<ConstPolygonRef> order(monotonic_direction, max_adjacent_distance, last_position); + PathOrderMonotonic<ConstPolygonPointer> order(monotonic_direction, max_adjacent_distance, last_position); Polygons left_over; bool last_would_have_been_excluded = false; for(size_t line_idx = 0; line_idx < line_order.paths.size(); ++line_idx) @@ -1246,74 +1273,17 @@ void LayerPlan::addLinesMonotonic order.optimize(); // Read out and process the monotonically ordered lines. - Point current_last_position = last_position; - coord_t half_line_width = config.getLineWidth() / 2; - coord_t line_width_2 = half_line_width * half_line_width; - for (unsigned int order_idx = 0; order_idx < order.paths.size(); order_idx++) - { - const PathOrder<ConstPolygonRef>::Path& path = order.paths[order_idx]; - ConstPolygonRef polygon = *path.vertices; - const size_t start = path.start_vertex; - const size_t end = path.vertices.size() - 1 - start; - const Point& p0 = polygon[start]; - const Point& p1 = polygon[end]; - // ignore line segments that are less than 5uM long - if (vSize2(p1 - p0) < MINIMUM_SQUARED_LINE_LENGTH) - { - continue; - } - if(vSize2(current_last_position - p0) < line_width_2) - { - // Instead of doing a small travel that is shorter than the line width (which is generally done at pretty high jerk & move) do a - // "fake" extrusion move - addExtrusionMove(p0, config, space_fill_type, 0, false, 1.0, fan_speed); - } - else - { - addTravel(p0); - } - addExtrusionMove(p1, config, space_fill_type, flow_ratio, false, 1.0, fan_speed); - current_last_position = p1; - - // Wipe - if (wipe_dist != 0) - { - bool wipe = true; - int line_width = config.getLineWidth(); - - // Don't wipe is current extrusion is too small - if (vSize2(p1 - p0) <= line_width * line_width * 4) - { - wipe = false; - } - - // Don't wipe if next starting point is very near - if (wipe && (order_idx < order.paths.size() - 1)) - { - const PathOrder<ConstPolygonRef>::Path& next_path = order.paths[order_idx + 1]; - ConstPolygonRef next_polygon = *next_path.vertices; - const size_t next_start = next_path.start_vertex; - const Point& next_p0 = next_polygon[next_start]; - if (vSize2(next_p0 - p1) <= line_width * line_width * 4) - { - wipe = false; - } - } - - if(wipe) - { - addExtrusionMove(p1 + normal(p1 - p0, wipe_dist), config, space_fill_type, 0.0, false, 1.0, fan_speed); - } - } - } + addLinesInGivenOrder(order.paths, config, space_fill_type, wipe_dist, flow_ratio, fan_speed); // Add all lines in the excluded areas the 'normal' way. - addLinesByOptimizer(left_over, config, space_fill_type, true, wipe_dist, flow_ratio, current_last_position, fan_speed); + addLinesByOptimizer(left_over, config, space_fill_type, true, wipe_dist, flow_ratio, getLastPlannedPositionOrStartingPosition(), fan_speed); } void LayerPlan::spiralizeWallSlice(const GCodePathConfig& config, ConstPolygonRef wall, ConstPolygonRef last_wall, const int seam_vertex_idx, const int last_seam_vertex_idx, const bool is_top_layer, const bool is_bottom_layer) { const bool smooth_contours = Application::getInstance().current_slice->scene.current_mesh_group->settings.get<bool>("smooth_spiralized_contours"); + constexpr bool spiralize = true; //In addExtrusionMove calls, enable spiralize and use nominal line width. + constexpr Ratio width_factor = 1.0_r; // once we are into the spiral we always start at the end point of the last layer (if any) const Point origin = (last_seam_vertex_idx >= 0 && !is_bottom_layer) ? last_wall[last_seam_vertex_idx] : wall[seam_vertex_idx]; @@ -1326,7 +1296,8 @@ void LayerPlan::spiralizeWallSlice(const GCodePathConfig& config, ConstPolygonRe Point join_first_wall_at = LinearAlg2D::getClosestOnLineSegment(origin, wall[seam_vertex_idx], wall[(seam_vertex_idx + 1) % wall.size()]); if (vSize(join_first_wall_at - origin) > 10) { - addExtrusionMove(join_first_wall_at, config, SpaceFillType::Polygons, 1.0, true); + constexpr Ratio flow = 1.0_r; + addExtrusionMove(join_first_wall_at, config, SpaceFillType::Polygons, flow, width_factor, spiralize); } } @@ -1409,18 +1380,18 @@ void LayerPlan::spiralizeWallSlice(const GCodePathConfig& config, ConstPolygonRe if (cpp.isValid() && vSize2(cpp.location - p) <= max_dist2) { // interpolate between cpp.location and p depending on how far we have progressed along wall - addExtrusionMove(cpp.location + (p - cpp.location) * (wall_length / total_length), config, SpaceFillType::Polygons, flow, true, speed_factor); + addExtrusionMove(cpp.location + (p - cpp.location) * (wall_length / total_length), config, SpaceFillType::Polygons, flow, width_factor, spiralize, speed_factor); } else { // no point in the last wall was found close enough to the current wall point so don't interpolate - addExtrusionMove(p, config, SpaceFillType::Polygons, flow, true, speed_factor); + addExtrusionMove(p, config, SpaceFillType::Polygons, flow, width_factor, spiralize, speed_factor); } } else { // no smoothing, use point verbatim - addExtrusionMove(p, config, SpaceFillType::Polygons, flow, true, speed_factor); + addExtrusionMove(p, config, SpaceFillType::Polygons, flow, width_factor, spiralize, speed_factor); } } @@ -1444,7 +1415,8 @@ void LayerPlan::spiralizeWallSlice(const GCodePathConfig& config, ConstPolygonRe distance_coasted += seg_length; } // reduce number of paths created when polygon has many points by limiting precision of flow - addExtrusionMove(p, config, SpaceFillType::Polygons, ((int)(flow * 20)) / 20.0, false, speed_factor); + constexpr bool no_spiralize = false; + addExtrusionMove(p, config, SpaceFillType::Polygons, ((int)(flow * 20)) / 20.0, width_factor, no_spiralize, speed_factor); } } } @@ -1691,11 +1663,6 @@ void LayerPlan::writeGCode(GCodeExport& gcode) const ExtruderTrain& extruder = Application::getInstance().current_slice->scene.extruders[extruder_nr]; - { // require printing temperature to be met - constexpr bool wait = true; - gcode.writeTemperatureCommand(extruder_nr, extruder_plan.required_start_temperature, wait); - } - if (extruder_plan.prev_extruder_standby_temp) { // turn off previous extruder constexpr bool wait = false; @@ -1708,6 +1675,11 @@ void LayerPlan::writeGCode(GCodeExport& gcode) gcode.writeTemperatureCommand(prev_extruder, prev_extruder_temp, wait); } + { // require printing temperature to be met + constexpr bool wait = true; + gcode.writeTemperatureCommand(extruder_nr, extruder_plan.required_start_temperature, wait); + } + const double extra_prime_amount = extruder.settings.get<bool>("retraction_enable") ? extruder.settings.get<double>("switch_extruder_extra_prime_amount") : 0; gcode.addExtraPrimeAmount(extra_prime_amount); } diff --git a/src/LayerPlan.h b/src/LayerPlan.h index ca5006086..6a0619900 100644 --- a/src/LayerPlan.h +++ b/src/LayerPlan.h @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef LAYER_PLAN_H @@ -283,7 +283,7 @@ private: * \param speed_factor (optional) a factor which the speed will be multiplied by. * \return A path with the given config which is now the last path in LayerPlan::paths */ - GCodePath* getLatestPathWithConfig(const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio flow = 1.0_r, bool spiralize = false, const Ratio speed_factor = 1.0_r); + GCodePath* getLatestPathWithConfig(const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio flow = 1.0_r, const Ratio width_factor = 1.0_r, bool spiralize = false, const Ratio speed_factor = 1.0_r); public: /*! @@ -456,13 +456,20 @@ public: * * \param p The point to extrude to * \param config The config with which to extrude - * \param space_fill_type Of what space filling type this extrusion move is a part - * \param flow A modifier of the extrusion width which would follow from the \p config - * \param speed_factor (optional) A factor the travel speed will be multipled by. - * \param spiralize Whether to gradually increase the z while printing. (Note that this path may be part of a sequence of spiralized paths, forming one polygon) - * \param fan_speed fan speed override for this path + * \param space_fill_type Of what space filling type this extrusion move is + * a part. + * \param flow A modifier of the flow rate which would follow from the + * \p config. + * \param width_factor A modifier of the line width which would follow from + * the \p config. + * \param speed_factor (optional) A factor the travel speed will be + * multiplied by. + * \param spiralize Whether to gradually increase the z while printing. + * (Note that this path may be part of a sequence of spiralized paths, + * forming one polygon.) + * \param fan_speed Fan speed override for this path. */ - void addExtrusionMove(Point p, const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio& flow = 1.0_r, bool spiralize = false, Ratio speed_factor = 1.0_r, double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT); + void addExtrusionMove(Point p, const GCodePathConfig& config, SpaceFillType space_fill_type, const Ratio& flow = 1.0_r, const Ratio width_factor = 1.0_r, bool spiralize = false, Ratio speed_factor = 1.0_r, double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT); /*! * Add polygon to the gcode starting at vertex \p startIdx @@ -509,18 +516,25 @@ public: /*! * Add a single line that is part of a wall to the gcode. - * \param p0 The start vertex of the line - * \param p1 The end vertex of the line - * \param settings The settings which should apply to this line added to the layer plan. - * \param non_bridge_config The config with which to print the wall lines that are not spanning a bridge - * \param bridge_config The config with which to print the wall lines that are spanning a bridge - * \param flow The ratio with which to multiply the extrusion amount - * \param non_bridge_line_volume A pseudo-volume that is derived from the print speed and flow of the non-bridge lines that have preceeded this line - * \param speed_factor This modifies the print speed when accelerating after a bridge line - * \param distance_to_bridge_start The distance along the wall from p0 to the first bridge segment + * \param p0 The start vertex of the line. + * \param p1 The end vertex of the line. + * \param settings The settings which should apply to this line added to the + * layer plan. + * \param non_bridge_config The config with which to print the wall lines + * that are not spanning a bridge. + * \param bridge_config The config with which to print the wall lines that + * are spanning a bridge. + * \param flow The ratio with which to multiply the extrusion amount. + * \param width_ratio The ratio with which to multiply the line width. + * \param non_bridge_line_volume A pseudo-volume that is derived from the + * print speed and flow of the non-bridge lines that have preceded this + * line. + * \param speed_factor This modifies the print speed when accelerating after + * a bridge line. + * \param distance_to_bridge_start The distance along the wall from p0 to + * the first bridge segment. */ - - void addWallLine(const Point& p0, const Point& p1, const Settings& settings, const GCodePathConfig& non_bridge_config, const GCodePathConfig& bridge_config, float flow, float& non_bridge_line_volume, Ratio speed_factor, double distance_to_bridge_start); + void addWallLine(const Point& p0, const Point& p1, const Settings& settings, const GCodePathConfig& non_bridge_config, const GCodePathConfig& bridge_config, float flow, const Ratio width_factor, float& non_bridge_line_volume, Ratio speed_factor, double distance_to_bridge_start); /*! * Add a wall to the g-code starting at vertex \p start_idx @@ -640,6 +654,26 @@ public: const double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT ); +protected: + /*! + * Add order optimized lines to the gcode. + * \param paths The paths in order + * \param config The config of the lines + * \param space_fill_type The type of space filling used to generate the line segments (should be either Lines or PolyLines!) + * \param wipe_dist (optional) the distance wiped without extruding after laying down a line. + * \param flow_ratio The ratio with which to multiply the extrusion amount + * \param fan_speed optional fan speed override for this path + */ + void addLinesInGivenOrder( + const std::vector<PathOrderPath<ConstPolygonPointer>>& paths, + const GCodePathConfig& config, + const SpaceFillType space_fill_type, + const coord_t wipe_dist, + const Ratio flow_ratio, + const double fan_speed + ); + +public: /*! * Add a spiralized slice of wall that is interpolated in X/Y between \p last_wall and \p wall. * @@ -738,12 +772,17 @@ public: void processFanSpeedAndMinimalLayerTime(Point starting_position); /*! - * Add a travel move to the layer plan to move inside the current layer part by a given distance away from the outline. - * This is supposed to be called when the nozzle is around the boundary of a layer part, not when the nozzle is in the middle of support, or in the middle of the air. - * - * \param distance The distance to the comb boundary after we moved inside it. - */ - void moveInsideCombBoundary(const coord_t distance); + * Add a travel move to the layer plan to move inside the current layer part + * by a given distance away from the outline. + * + * This is supposed to be called when the nozzle is around the boundary of a + * layer part, not when the nozzle is in the middle of support, or in the + * middle of the air. + * \param distance The distance to the comb boundary after we moved inside + * it. + * \param part If given, stay within the boundary of this part. + */ + void moveInsideCombBoundary(const coord_t distance, const std::optional<SliceLayerPart>& part = std::nullopt); /*! * Apply back-pressure compensation to this layer-plan. diff --git a/src/PathOrder.cpp b/src/PathOrder.cpp deleted file mode 100644 index 5974a17ca..000000000 --- a/src/PathOrder.cpp +++ /dev/null @@ -1,43 +0,0 @@ -//Copyright (c) 2021 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. - -#include "PathOrder.h" //The definitions we're implementing here. -#include "sliceDataStorage.h" //For SliceLayerPart and related parts to reorder. - -//Since the PathOrder is a template class, we will only implement the template specializations in this file. -//The templated segments need to go into the header. - -namespace cura -{ - -template<> -ConstPolygonRef PathOrder<ConstPolygonRef>::getVertexData(ConstPolygonRef path) -{ - return path; -} - -template<> -ConstPolygonRef PathOrder<PolygonRef>::getVertexData(PolygonRef path) -{ - return path; -} - -template<> -ConstPolygonRef PathOrder<const SkinPart*>::getVertexData(const SkinPart* path) -{ - return path->outline.outerPolygon(); -} - -template<> -ConstPolygonRef PathOrder<const SliceLayerPart*>::getVertexData(const SliceLayerPart* path) -{ - return path->outline.outerPolygon(); -} - -template<> -ConstPolygonRef PathOrder<const SupportInfillPart*>::getVertexData(const SupportInfillPart* path) -{ - return path->outline.outerPolygon(); -} - -}
\ No newline at end of file diff --git a/src/PathOrder.h b/src/PathOrder.h index 2c11bb824..f307ce88a 100644 --- a/src/PathOrder.h +++ b/src/PathOrder.h @@ -6,6 +6,7 @@ #include "settings/ZSeamConfig.h" //To get the seam settings. #include "utils/polygonUtils.h" +#include "PathOrderPath.h" namespace cura { @@ -27,68 +28,6 @@ template<typename PathType> class PathOrder { public: - /*! - * Represents a path which has been optimised, the output of the ordering. - * - * This small data structure contains the vertex data of a path, where to - * start along the path and in which direction to print it, as well as - * whether the path should be closed (in the case of a polygon) or open (in - * case of a polyline). - * - * After the ordering has completed, the \ref paths vector will be filled - * with optimized paths. - */ - struct Path - { - /*! - * Construct a new planned path. - * - * The \ref converted field is not initialized yet. This can only be - * done after all of the input paths have been added, to prevent - * invalidating the pointers. - */ - Path(const PathType& vertices, const bool is_closed = false, const size_t start_vertex = 0, const bool backwards = false) - : vertices(vertices) - , start_vertex(start_vertex) - , is_closed(is_closed) - , backwards(backwards) - {} - - /*! - * The vertex data of the path. - */ - PathType vertices; - - /*! - * Vertex data, converted into a Polygon so that the orderer knows how - * to deal with this data. - */ - ConstPolygonPointer converted; - - /*! - * Which vertex along the path to start printing with. - * - * If this path represents a polyline, this will always be one of the - * endpoints of the path; either 0 or ``vertices->size() - 1``. - */ - size_t start_vertex; - - /*! - * Whether the path should be closed at the ends or not. - * - * If this path should be closed, it represents a polygon. If it should - * not be closed, it represents a polyline. - */ - bool is_closed; - - /*! - * Whether the path should be traversed in backwards direction. - * - * For a polyline it may be more efficient to print the path in - * backwards direction, if the last vertex is closer than the first. - */ - bool backwards; - }; /*! * After reordering, this contains the paths that need to be printed in the @@ -98,7 +37,7 @@ public: * pointer to the vertex data, whether to close the loop or not, the * direction in which to print the path and where to start the path. */ - std::vector<Path> paths; + std::vector<PathOrderPath<PathType>> paths; /*! * The location where the nozzle is assumed to start from before printing @@ -159,19 +98,6 @@ protected: */ constexpr static coord_t coincident_point_distance = 10; - /*! - * Get vertex data from the custom path type. - * - * This is a function that allows the reordering algorithm to work with any - * type of input data structure. It provides a translation from the input - * data structure that the user would like to have reordered to a data - * structure that the reordering algorithm can work with. It's unknown how - * the ``PathType`` object is structured or how to get the vertex data from - * it. This function tells the optimizer how, but it needs to be specialized - * for each different type that this class is used with. See the .cpp file - * for examples and where to add a new specialization. - */ - ConstPolygonRef getVertexData(const PathType path); /*! * In the current set of paths, detect all loops and mark them as such. @@ -182,7 +108,7 @@ protected: */ void detectLoops() { - for(Path& path : paths) + for(PathOrderPath<PathType>& path : paths) { if(path.is_closed) //Already a polygon. No need to detect loops. { diff --git a/src/PathOrderMonotonic.h b/src/PathOrderMonotonic.h index 5c296aae9..6543db5a5 100644 --- a/src/PathOrderMonotonic.h +++ b/src/PathOrderMonotonic.h @@ -9,6 +9,7 @@ #include <unordered_map> //To track monotonic sequences. #include "PathOrder.h" +#include "PathOrderPath.h" namespace cura { @@ -40,7 +41,7 @@ template<typename PathType> class PathOrderMonotonic : public PathOrder<PathType> { public: - using typename PathOrder<PathType>::Path; + using Path = PathOrderPath<PathType>; using PathOrder<PathType>::coincident_point_distance; PathOrderMonotonic(const AngleRadians monotonic_direction, const coord_t max_adjacent_distance, const Point start_point) @@ -61,7 +62,7 @@ public: //Get the vertex data and store it in the paths. for(Path& path : this->paths) { - path.converted = this->getVertexData(path.vertices); + path.converted = path.getVertexData(); } std::vector<Path> reordered; //To store the result in. At the end, we'll std::swap with the real paths. @@ -72,7 +73,7 @@ public: this->detectLoops(); //Always filter out loops. We don't specifically want to print those in monotonic order. for(Path& path : this->paths) { - if(path.is_closed || path.vertices.size() <= 1) + if(path.is_closed || path.vertices->size() <= 1) { reordered.push_back(path); } diff --git a/src/PathOrderOptimizer.cpp b/src/PathOrderOptimizer.cpp deleted file mode 100644 index c5fe5a6ea..000000000 --- a/src/PathOrderOptimizer.cpp +++ /dev/null @@ -1,51 +0,0 @@ -//Copyright (c) 2020 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. - -#include "PathOrderOptimizer.h" //The definitions we're implementing here. -#include "sliceDataStorage.h" //For SliceLayerPart. -#include "WallToolPaths.h" - -//Since the PathOrderOptimizer is a template class, we will only implement the template specializations in this file. - -namespace cura -{ - - template<> - ConstPolygonRef PathOrderOptimizer<ConstPolygonPointer>::getVertexData(ConstPolygonPointer path) - { - return *path; - } - - template<> - ConstPolygonRef PathOrderOptimizer<PolygonPointer>::getVertexData(PolygonPointer path) - { - return *path; - } - - template<> - ConstPolygonRef PathOrderOptimizer<const SkinPart*>::getVertexData(const SkinPart* path) - { - return path->outline.outerPolygon(); - } - - template<> - ConstPolygonRef PathOrderOptimizer<const SliceLayerPart*>::getVertexData(const SliceLayerPart* path) - { - return path->outline.outerPolygon(); - } - - template<> - ConstPolygonRef PathOrderOptimizer<const SupportInfillPart*>::getVertexData(const SupportInfillPart* path) - { - return path->outline.outerPolygon(); - } - template<> - ConstPolygonRef PathOrderOptimizer<const ExtrusionLine*>::getVertexData(const ExtrusionLine* path) - { - cached_vertices.emplace_back(); - Polygon& poly = cached_vertices.back(); - poly = path->toPolygon(); - return ConstPolygonRef(poly); - } - -} diff --git a/src/PathOrderOptimizer.h b/src/PathOrderOptimizer.h index aa6aec883..e403fe7ac 100644 --- a/src/PathOrderOptimizer.h +++ b/src/PathOrderOptimizer.h @@ -1,4 +1,4 @@ -//Copyright (c) 2020 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef PATHORDEROPTIMIZER_H @@ -8,6 +8,7 @@ #include <unordered_set> #include "InsetOrderOptimizer.h" // for makeOrderIncludeTransitive +#include "PathOrderPath.h" #include "pathPlanning/CombPath.h" //To calculate the combing distance if we want to use combing. #include "pathPlanning/LinePolygonsCrossings.h" //To prevent calculating combing distances if we don't cross the combing borders. #include "settings/EnumSettings.h" //To get the seam settings. @@ -51,71 +52,6 @@ class PathOrderOptimizer { public: /*! - * Represents a path which has been optimized, the output of the - * optimization. - * - * This small data structure contains the vertex data of a path, where to - * start along the path and in which direction to print it, as well as - * whether the path should be closed (in case of a polygon) or open (in case - * of a polyline. - * - * After optimization is completed, the \ref paths vector will be filled - * with optimized paths. - */ - struct Path - { - /*! - * Construct a new path. - * - * The \ref converted field is not initialized yet. This can only be - * done after all of the input paths have been added, to prevent - * invalidating the pointer. - */ - Path(const PathType& vertices, const bool is_closed = false, const size_t start_vertex = 0, const bool backwards = false) - : vertices(vertices) - , start_vertex(start_vertex) - , is_closed(is_closed) - , backwards(backwards) - { - } - - /*! - * The vertex data of the path. - */ - PathType vertices; - - /*! - * Vertex data, converted into a Polygon so that the optimizer knows - * how to deal with this data. - */ - ConstPolygonPointer converted; - - /*! - * Which vertex along the path to start printing with. - * - * If this path represents a polyline, this will always be one of the - * endpoints of the path; either 0 or ``vertices->size() - 1``. - */ - size_t start_vertex; - - /*! - * Whether the path should be closed at the ends or not. - * - * If this path should be closed, it represents a polygon. If it should - * not be closed, it represents a polyline. - */ - bool is_closed; - - /*! - * Whether the path should be traversed in backwards direction. - * - * For a polyline it may be more efficient to print the path in - * backwards direction, if the last vertex is closer than the first. - */ - bool backwards; - }; - - /*! * After optimizing, this contains the paths that need to be printed in the * correct order. * @@ -123,7 +59,7 @@ public: * pointer to the vertex data, whether or not to close the loop, the * direction in which to print the path and where to start the path. */ - std::vector<Path> paths; + std::vector<PathOrderPath<PathType>> paths; /*! * The location where the nozzle is assumed to start from before printing @@ -194,16 +130,15 @@ public: } //Get the vertex data and store it in the paths. - cached_vertices.reserve(paths.size()); //Prevent pointer invalidation. - for(Path& path : paths) + for(PathOrderPath<PathType>& path : paths) { - path.converted = getVertexData(path.vertices); + path.converted = path.getVertexData(); } //If necessary, check polylines to see if they are actually polygons. if(detect_loops) { - for(Path& path : paths) + for(PathOrderPath<PathType>& path : paths) { if(!path.is_closed) { @@ -218,7 +153,7 @@ public: SparsePointGridInclusive<size_t> line_bucket_grid(snap_radius); for(size_t i = 0; i < paths.size(); ++i) { - const Path& path = paths[i]; + const PathOrderPath<PathType>& path = paths[i]; if (path.converted->empty()) { continue; @@ -242,7 +177,7 @@ public: const bool precompute_start = seam_config.type == EZSeamType::RANDOM || seam_config.type == EZSeamType::USER_SPECIFIED || seam_config.type == EZSeamType::SHARPEST_CORNER; if(precompute_start) { - for(Path& path : paths) + for(PathOrderPath<PathType>& path : paths) { if(!path.is_closed) { @@ -273,11 +208,11 @@ public: assert(before_it != path_to_index.end()); is_blocking[before_it->second].emplace_back(after_it->second); } - + std::vector<bool> picked(paths.size(), false); //Fixed size boolean flag for whether each path is already in the optimized vector. Point current_position = start_point; - std::vector<Path> optimized_order; //To store our result in. At the end we'll std::swap. + std::vector<PathOrderPath<PathType>> optimized_order; //To store our result in. At the end we'll std::swap. optimized_order.reserve(paths.size()); while(optimized_order.size() < paths.size()) { @@ -310,7 +245,7 @@ public: for(const size_t candidate_path_index : available_candidates) { - Path& path = paths[candidate_path_index]; + PathOrderPath<PathType>& path = paths[candidate_path_index]; if(path.converted->empty()) //No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. { if(best_distance2 == std::numeric_limits<coord_t>::max()) @@ -341,7 +276,7 @@ public: } } - Path& best_path = paths[best_candidate]; + PathOrderPath<PathType>& best_path = paths[best_candidate]; optimized_order.push_back(best_path); picked[best_candidate] = true; for (size_t unlocked_idx : is_blocking[best_candidate]) @@ -367,7 +302,7 @@ public: if(reverse_direction) { //Reverse-insert the optimized order, to invert the ordering. - std::vector<Path> reversed; + std::vector<PathOrderPath<PathType>> reversed; //Don't replace with swap, assign or insert. They require functions that we can't implement for all template arguments for PathType. reversed.reserve(optimized_order.size()); for(auto it = optimized_order.rbegin(); it != optimized_order.rend(); it++) @@ -397,16 +332,6 @@ protected: constexpr static coord_t coincident_point_distance = 10; /*! - * Some input data structures need to be converted to polygons before use. - * For those, we need to store the vertex data somewhere during the lifetime - * of the object. Store them here. - * - * For example, if the ``PathType`` is a list of ``ExtrusionJunction``s, - * this will store the coordinates of those junctions. - */ - std::vector<Polygon> cached_vertices; - - /*! * Bucket grid to store the locations of the combing boundary. * * This is cached in order to speed up the collision checking with the @@ -463,7 +388,7 @@ protected: * endpoints rather than * \return An index to a vertex in that path where printing must start. */ - size_t findStartLocation(const Path& path, const Point& target_pos) + size_t findStartLocation(const PathOrderPath<PathType>& path, const Point& target_pos) { if(!path.is_closed) { @@ -489,7 +414,7 @@ protected: return vert; } - // Don't know the path-type here, or wether it has a simplify. Also, simplification occurs in-place, which is not wanted here: Copy the polygon. + // Don't know the path-type here, or whether it has a simplify. Also, simplification occurs in-place, which is not wanted here: Copy the polygon. // A course simplification is needed, since Arachne has a tendency to 'smear' corners out over multiple line segments. // Which in itself is a good thing, but will mess up the detection of sharp corners and such. Polygon simple_poly(*path.converted); @@ -506,10 +431,6 @@ protected: // Paths, other than polygons, can be either clockwise or counterclockwise. Make sure this is detected. const bool clockwise = simple_poly.orientation(); - //Find most extreme point in one direction. For the "actual loop" (see below), start from this point, - //so it can act as a "tie breaker" if all differences in dist-score for a polygon fall within epsilon. - //Direction/point should be the user-specified point if available, or an arbitrary point away from polygon otherwise. - constexpr coord_t EPSILON = 25; const Point focus_fixed_point = (seam_config.type == EZSeamType::USER_SPECIFIED) ? seam_config.pos : Point(0, std::sqrt(std::numeric_limits<coord_t>::max())); //Use sqrt, so the squared size can be used when comparing distances. @@ -532,7 +453,7 @@ protected: const coord_t distance = (combing_boundary == nullptr) ? getDirectDistance(here, target_pos) : getCombingDistance(here, target_pos); - const float score_distance = (seam_config.type == EZSeamType::SHARPEST_CORNER && seam_config.corner_pref != EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE) ? 0 : distance / 1000000; + const float score_distance = (seam_config.type == EZSeamType::SHARPEST_CORNER && seam_config.corner_pref != EZSeamCornerPrefType::Z_SEAM_CORNER_PREF_NONE) ? 0 : static_cast<float>(distance) / 1000000; const float corner_angle = (clockwise ? LinearAlg2D::getAngleLeft(previous, here, next) : LinearAlg2D::getAngleLeft(next, here, previous)) / M_PI - 1; //Between -1 and 1. float score; @@ -581,7 +502,21 @@ protected: } } - if(score - EPSILON < best_score) + constexpr float EPSILON = 25.0; + if (fabs(best_score - score) <= EPSILON) + { + // add breaker for two candidate starting location with similar score + // if we don't do this then we (can) get an un-even seam + // ties are broken by favouring points with lower x-coord + // if x-coord for both points are equal then break ties by + // favouring points with lower y-coord + if (here.X != best_point.X ? here.X < best_point.X : here.Y < best_point.Y) + { + best_point = here; + } + best_score = std::min(best_score, score); + } + else if(score < best_score) { best_point = here; best_score = score; @@ -674,7 +609,7 @@ protected: return rand() % polygon.size(); } - bool isLoopingPolyline(const Path& path) + bool isLoopingPolyline(const PathOrderPath<PathType>& path) { if(path.converted->empty()) { @@ -682,20 +617,6 @@ protected: } return vSize2(path.converted->back() - path.converted->front()) < coincident_point_distance * coincident_point_distance; } - - /*! - * Get vertex data from the custom path type. - * - * This is a function that allows the optimization algorithm to work with - * any type of input data structure. It provides a translation from the - * input data structure that the user would like to have ordered to a data - * structure that the optimization algorithm can work with. It's unknown how - * the ``PathType`` object is structured or how to get the vertex data from - * it. This function tells the optimizer how, but it needs to be specialized - * for each different type that this optimizer is used. See the .cpp file - * for examples and where to add a new specialization. - */ - ConstPolygonRef getVertexData(const PathType path); }; template<typename PathType> diff --git a/src/PathOrderPath.cpp b/src/PathOrderPath.cpp new file mode 100644 index 000000000..855057ea8 --- /dev/null +++ b/src/PathOrderPath.cpp @@ -0,0 +1,50 @@ +//Copyright (c) 2022 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "PathOrderPath.h" //The definitions we're implementing here. +#include "sliceDataStorage.h" //For SliceLayerPart. +#include "WallToolPaths.h" + +namespace cura +{ + + template<> + ConstPolygonRef PathOrderPath<ConstPolygonPointer>::getVertexData() + { + return *vertices; + } + + template<> + ConstPolygonRef PathOrderPath<PolygonPointer>::getVertexData() + { + return *vertices; + } + + template<> + ConstPolygonRef PathOrderPath<const SkinPart*>::getVertexData() + { + return vertices->outline.outerPolygon(); + } + + template<> + ConstPolygonRef PathOrderPath<const SliceLayerPart*>::getVertexData() + { + return vertices->outline.outerPolygon(); + } + + template<> + ConstPolygonRef PathOrderPath<const SupportInfillPart*>::getVertexData() + { + return vertices->outline.outerPolygon(); + } + template<> + ConstPolygonRef PathOrderPath<const ExtrusionLine*>::getVertexData() + { + if ( ! cached_vertices) + { + cached_vertices = vertices->toPolygon(); + } + return ConstPolygonRef(*cached_vertices); + } + +} diff --git a/src/PathOrderPath.h b/src/PathOrderPath.h new file mode 100644 index 000000000..b113e81e2 --- /dev/null +++ b/src/PathOrderPath.h @@ -0,0 +1,107 @@ +//Copyright (c) 2022 Ultimaker B.V. +//CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef PATH_ORDER_PATH_H +#define PATH_ORDER_PATH_H + +#include <optional> + +#include "settings/ZSeamConfig.h" //To get the seam settings. +#include "utils/polygonUtils.h" + +namespace cura +{ + +/*! + * Represents a path which has been optimised, the output of the ordering. + * + * This small data structure contains the vertex data of a path, where to + * start along the path and in which direction to print it, as well as + * whether the path should be closed (in the case of a polygon) or open (in + * case of a polyline). + * + * After the ordering has completed, the \ref paths vector will be filled + * with optimized paths. + */ +template<typename PathType> +struct PathOrderPath +{ + /*! + * Construct a new planned path. + * + * The \ref converted field is not initialized yet. This can only be + * done after all of the input paths have been added, to prevent + * invalidating the pointers. + */ + PathOrderPath(const PathType& vertices, const bool is_closed = false, const size_t start_vertex = 0, const bool backwards = false) + : vertices(vertices) + , start_vertex(start_vertex) + , is_closed(is_closed) + , backwards(backwards) + {} + + /*! + * The vertex data of the path. + */ + PathType vertices; + + /*! + * Vertex data, converted into a Polygon so that the orderer knows how + * to deal with this data. + */ + ConstPolygonPointer converted; + + /*! + * Which vertex along the path to start printing with. + * + * If this path represents a polyline, this will always be one of the + * endpoints of the path; either 0 or ``vertices->size() - 1``. + */ + size_t start_vertex; + + /*! + * Whether the path should be closed at the ends or not. + * + * If this path should be closed, it represents a polygon. If it should + * not be closed, it represents a polyline. + */ + bool is_closed; + + /*! + * Whether the path should be traversed in backwards direction. + * + * For a polyline it may be more efficient to print the path in + * backwards direction, if the last vertex is closer than the first. + */ + bool backwards; + + + /*! + * Get vertex data from the custom path type. + * + * This is a function that allows the reordering algorithm to work with any + * type of input data structure. It provides a translation from the input + * data structure that the user would like to have reordered to a data + * structure that the reordering algorithm can work with. It's unknown how + * the ``PathType`` object is structured or how to get the vertex data from + * it. This function tells the optimizer how, but it needs to be specialized + * for each different type that this class is used with. See the .cpp file + * for examples and where to add a new specialization. + */ + ConstPolygonRef getVertexData(); + +protected: + /*! + * Some input data structures need to be converted to polygons before use. + * For those, we need to store the vertex data somewhere during the lifetime + * of the object. Store them here. + * + * For example, if the ``PathType`` is a list of ``ExtrusionJunction``s, + * this will store the coordinates of those junctions. + */ + std::optional<Polygon> cached_vertices; +}; + +} + +#endif //PATH_ORDER_PATH_H diff --git a/src/SkeletalTrapezoidation.cpp b/src/SkeletalTrapezoidation.cpp index 7ecd3a933..635838bcd 100644 --- a/src/SkeletalTrapezoidation.cpp +++ b/src/SkeletalTrapezoidation.cpp @@ -433,8 +433,6 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) } separatePointyQuadEndNodes(); - - graph.fixNodeDuplication(); graph.collapseSmallEdges(); @@ -484,7 +482,7 @@ void SkeletalTrapezoidation::separatePointyQuadEndNodes() // vvvvvvvvvvvvvvvvvvvvv // -void SkeletalTrapezoidation::generateToolpaths(VariableWidthPaths& generated_toolpaths, bool filter_outermost_central_edges) +void SkeletalTrapezoidation::generateToolpaths(std::vector<VariableWidthLines>& generated_toolpaths, bool filter_outermost_central_edges) { p_generated_toolpaths = &generated_toolpaths; @@ -1803,7 +1801,7 @@ void SkeletalTrapezoidation::addToolpathSegment(const ExtrusionJunction& from, c { if (from == to) return; - VariableWidthPaths& generated_toolpaths = *p_generated_toolpaths; + std::vector<VariableWidthLines>& generated_toolpaths = *p_generated_toolpaths; size_t inset_idx = from.perimeter_index; if (inset_idx >= generated_toolpaths.size()) @@ -1971,7 +1969,7 @@ void SkeletalTrapezoidation::connectJunctions(ptr_vector_t<LineJunctions>& edge_ void SkeletalTrapezoidation::generateLocalMaximaSingleBeads() { - VariableWidthPaths& generated_toolpaths = *p_generated_toolpaths; + std::vector<VariableWidthLines>& generated_toolpaths = *p_generated_toolpaths; for (auto& node : graph.nodes) { diff --git a/src/SkeletalTrapezoidation.h b/src/SkeletalTrapezoidation.h index 49a404304..0dfb82d2a 100644 --- a/src/SkeletalTrapezoidation.h +++ b/src/SkeletalTrapezoidation.h @@ -120,7 +120,7 @@ public: * "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); + void generateToolpaths(std::vector<VariableWidthLines>& generated_toolpaths, bool filter_outermost_central_edges = false); protected: /*! @@ -161,8 +161,10 @@ protected: /*! * (Eventual) returned 'polylines per index' result (from generateToolpaths): + * + * Binned by inset_idx. */ - VariableWidthPaths* p_generated_toolpaths; + std::vector<VariableWidthLines>* p_generated_toolpaths; /*! * Transfer an edge from the VD to the HE and perform discretization of parabolic edges (and vertex-vertex edges) diff --git a/src/SkeletalTrapezoidationGraph.cpp b/src/SkeletalTrapezoidationGraph.cpp index b43b0e09c..44034cd44 100644 --- a/src/SkeletalTrapezoidationGraph.cpp +++ b/src/SkeletalTrapezoidationGraph.cpp @@ -175,39 +175,6 @@ bool STHalfEdgeNode::isLocalMaximum(bool strict) const } 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) { diff --git a/src/SkeletalTrapezoidationGraph.h b/src/SkeletalTrapezoidationGraph.h index 289c36645..11849262c 100644 --- a/src/SkeletalTrapezoidationGraph.h +++ b/src/SkeletalTrapezoidationGraph.h @@ -70,7 +70,6 @@ class SkeletalTrapezoidationGraph: public HalfEdgeGraph<SkeletalTrapezoidationJo 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. diff --git a/src/SupportInfillPart.h b/src/SupportInfillPart.h index 6c300357b..8fae3f230 100644 --- a/src/SupportInfillPart.h +++ b/src/SupportInfillPart.h @@ -31,7 +31,7 @@ public: int inset_count_to_generate; //!< The number of insets need to be generated from the outline. This is not the actual insets that will be generated. std::vector<std::vector<Polygons>> infill_area_per_combine_per_density; //!< a list of separated sub-areas which requires different infill densities and combined thicknesses // for infill_areas[x][n], x means the density level and n means the thickness - VariableWidthPaths wall_toolpaths; //!< Any walls go here, not in the areas, where they could be combined vertically (don't combine walls). + std::vector<VariableWidthLines> wall_toolpaths; //!< Any walls go here, not in the areas, where they could be combined vertically (don't combine walls). Binned by inset_idx. SupportInfillPart(const PolygonsPart& outline, coord_t support_line_width, int inset_count_to_generate = 0); diff --git a/src/TopSurface.cpp b/src/TopSurface.cpp index 7bf5be852..18b691866 100644 --- a/src/TopSurface.cpp +++ b/src/TopSurface.cpp @@ -62,6 +62,10 @@ bool TopSurface::ironing(const SliceDataStorage& storage, const SliceMeshStorage const coord_t max_resolution = mesh.settings.get<coord_t>("meshfix_maximum_resolution"); const coord_t max_deviation = mesh.settings.get<coord_t>("meshfix_maximum_deviation"); const Ratio ironing_flow = mesh.settings.get<Ratio>("ironing_flow"); + const bool enforce_monotonic_order = mesh.settings.get<bool>("ironing_monotonic"); + constexpr size_t wall_line_count = 0; + const Point infill_origin = Point(); + const bool skip_line_stitching = enforce_monotonic_order; coord_t ironing_inset = -mesh.settings.get<coord_t>("ironing_inset"); if (pattern == EFillMethod::ZIG_ZAG) @@ -81,8 +85,8 @@ bool TopSurface::ironing(const SliceDataStorage& storage, const SliceMeshStorage } Polygons ironed_areas = areas.offset(ironing_inset); - Infill infill_generator(pattern, zig_zaggify_infill, connect_polygons, ironed_areas, line_width, line_spacing, infill_overlap, infill_multiplier, direction, layer.z - 10, shift, max_resolution, max_deviation); - VariableWidthPaths ironing_paths; + Infill infill_generator(pattern, zig_zaggify_infill, connect_polygons, ironed_areas, line_width, line_spacing, infill_overlap, infill_multiplier, direction, layer.z - 10, shift, max_resolution, max_deviation, wall_line_count, infill_origin, skip_line_stitching); + std::vector<VariableWidthLines> ironing_paths; Polygons ironing_polygons; Polygons ironing_lines; infill_generator.generate(ironing_paths, ironing_polygons, ironing_lines, mesh.settings); @@ -124,7 +128,7 @@ bool TopSurface::ironing(const SliceDataStorage& storage, const SliceMeshStorage } } - if(!mesh.settings.get<bool>("ironing_monotonic")) + if( ! enforce_monotonic_order) { layer.addLinesByOptimizer(ironing_lines, line_config, SpaceFillType::PolyLines); } diff --git a/src/WallToolPaths.cpp b/src/WallToolPaths.cpp index f46e4763e..a83b82de3 100644 --- a/src/WallToolPaths.cpp +++ b/src/WallToolPaths.cpp @@ -22,7 +22,6 @@ WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t nominal_bead , bead_width_x(nominal_bead_width) , inset_count(inset_count) , wall_0_inset(wall_0_inset) - , strategy_type(settings.get<StrategyType>("beading_strategy_type")) , print_thin_walls(settings.get<bool>("fill_outline_gaps")) , min_feature_size(settings.get<coord_t>("min_feature_size")) , min_bead_width(settings.get<coord_t>("min_bead_width")) @@ -39,7 +38,6 @@ WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t bead_width_0 , bead_width_x(bead_width_x) , inset_count(inset_count) , wall_0_inset(wall_0_inset) - , strategy_type(settings.get<StrategyType>("beading_strategy_type")) , print_thin_walls(settings.get<bool>("fill_outline_gaps")) , min_feature_size(settings.get<coord_t>("min_feature_size")) , min_bead_width(settings.get<coord_t>("min_bead_width")) @@ -49,7 +47,7 @@ WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t bead_width_0 { } -const VariableWidthPaths& WallToolPaths::generate() +const std::vector<VariableWidthLines>& WallToolPaths::generate() { const coord_t smallest_segment = settings.get<coord_t>("meshfix_maximum_resolution"); const coord_t allowed_distance = settings.get<coord_t>("meshfix_maximum_deviation"); @@ -82,7 +80,6 @@ const VariableWidthPaths& WallToolPaths::generate() const size_t max_bead_count = (inset_count < std::numeric_limits<coord_t>::max() / 2) ? 2 * inset_count : std::numeric_limits<coord_t>::max(); const auto beading_strat = BeadingStrategyFactory::makeStrategy ( - strategy_type, bead_width_0, bead_width_x, wall_transition_length, @@ -127,9 +124,9 @@ const VariableWidthPaths& WallToolPaths::generate() } -void WallToolPaths::stitchToolPaths(VariableWidthPaths& toolpaths, const Settings& settings) +void WallToolPaths::stitchToolPaths(std::vector<VariableWidthLines>& toolpaths, const Settings& settings) { - const coord_t stitch_distance = settings.get<coord_t>("wall_line_width_x") / 3 + 1; //In 0-width contours, junctions can cause up to 1-line-width gaps. Don't stitch more than 1 line width. + const coord_t stitch_distance = settings.get<coord_t>("wall_line_width_x") - 1; //In 0-width contours, junctions can cause up to 1-line-width gaps. Don't stitch more than 1 line width. for (unsigned int wall_idx = 0; wall_idx < toolpaths.size(); wall_idx++) { @@ -138,18 +135,6 @@ void WallToolPaths::stitchToolPaths(VariableWidthPaths& toolpaths, const Setting VariableWidthLines stitched_polylines; VariableWidthLines closed_polygons; PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::stitch(wall_lines, stitched_polylines, closed_polygons, stitch_distance); -#ifdef DEBUG - for (const ExtrusionLine& line : stitched_polylines) - { - if ( ! line.is_odd && - line.polylineLength() > 3 * stitch_distance && line.size() > 3) - { - logError("Some even contour lines could not be closed into polygons!\n"); - assert(false && "Some even contour lines could not be closed into polygons!"); - // NOTE: if this assertion fails then revert the debugging code removed in this commit (git blame on this line) - } - } -#endif // DEBUG wall_lines = stitched_polylines; // replace input toolpaths with stitched polylines for (ExtrusionLine& wall_polygon : closed_polygons) @@ -170,7 +155,7 @@ void WallToolPaths::stitchToolPaths(VariableWidthPaths& toolpaths, const Setting } } -void WallToolPaths::removeSmallLines(VariableWidthPaths& toolpaths) +void WallToolPaths::removeSmallLines(std::vector<VariableWidthLines>& toolpaths) { for (VariableWidthLines& inset : toolpaths) { @@ -192,7 +177,7 @@ void WallToolPaths::removeSmallLines(VariableWidthPaths& toolpaths) } } -void WallToolPaths::simplifyToolPaths(VariableWidthPaths& toolpaths, const Settings& settings) +void WallToolPaths::simplifyToolPaths(std::vector<VariableWidthLines>& toolpaths, const Settings& settings) { for (size_t toolpaths_idx = 0; toolpaths_idx < toolpaths.size(); ++toolpaths_idx) { @@ -206,7 +191,7 @@ void WallToolPaths::simplifyToolPaths(VariableWidthPaths& toolpaths, const Setti } } -const VariableWidthPaths& WallToolPaths::getToolPaths() +const std::vector<VariableWidthLines>& WallToolPaths::getToolPaths() { if (!toolpaths_generated) { @@ -215,7 +200,7 @@ const VariableWidthPaths& WallToolPaths::getToolPaths() return toolpaths; } -void WallToolPaths::pushToolPaths(VariableWidthPaths& paths) +void WallToolPaths::pushToolPaths(std::vector<VariableWidthLines>& paths) { if (! toolpaths_generated) { @@ -227,9 +212,9 @@ void WallToolPaths::pushToolPaths(VariableWidthPaths& paths) void WallToolPaths::separateOutInnerContour() { //We'll remove all 0-width paths from the original toolpaths and store them separately as polygons. - VariableWidthPaths actual_toolpaths; + std::vector<VariableWidthLines> actual_toolpaths; actual_toolpaths.reserve(toolpaths.size()); //A bit too much, but the correct order of magnitude. - VariableWidthPaths contour_paths; + std::vector<VariableWidthLines> contour_paths; contour_paths.reserve(toolpaths.size() / inset_count); inner_contour.clear(); for (const VariableWidthLines& inset : toolpaths) @@ -314,7 +299,7 @@ const Polygons& WallToolPaths::getInnerContour() return inner_contour; } -bool WallToolPaths::removeEmptyToolPaths(VariableWidthPaths& toolpaths) +bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines>& toolpaths) { toolpaths.erase(std::remove_if(toolpaths.begin(), toolpaths.end(), [](const VariableWidthLines& lines) { diff --git a/src/WallToolPaths.h b/src/WallToolPaths.h index 6f491ba5d..96feca430 100644 --- a/src/WallToolPaths.h +++ b/src/WallToolPaths.h @@ -39,21 +39,21 @@ public: /*! * Generates the Toolpaths - * \return A reference to the newly create ToolPaths + * \return A reference to the newly created ToolPaths. Binned by inset_idx. */ - const VariableWidthPaths& generate(); + const std::vector<VariableWidthLines>& generate(); /*! * Gets the toolpaths, if this called before \p generate() it will first generate the Toolpaths - * \return a reference to the toolpaths + * \return a reference to the toolpaths. Binned by inset_idx. */ - const VariableWidthPaths& getToolPaths(); + const std::vector<VariableWidthLines>& getToolPaths(); /*! * Alternate 'get', for when the vector that'll be inserted in already exists. - * \param The already existing (or empty) paths these new toolpaths are pushed into. + * \param paths The already existing (or empty) paths these new toolpaths are pushed into. Binned by inset_idx. */ - void pushToolPaths(VariableWidthPaths& paths); + void pushToolPaths(std::vector<VariableWidthLines>& paths); /*! * Compute the inner contour of the walls. This contour indicates where the walled area ends and its infill begins. @@ -78,10 +78,10 @@ public: /*! * Removes empty paths from the toolpaths - * \param toolpaths the VariableWidthPaths generated with \p generate() + * \param toolpaths the toolpaths binned by inset_idx 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); + static bool removeEmptyToolPaths(std::vector<VariableWidthLines>& toolpaths); protected: /*! @@ -91,20 +91,19 @@ protected: * * \param settings The settings as provided by the user */ - static void stitchToolPaths(VariableWidthPaths& toolpaths, const Settings& settings); + static void stitchToolPaths(std::vector<VariableWidthLines>& toolpaths, const Settings& settings); /*! * Remove polylines shorter than half the smallest line width along that polyline. */ - static void removeSmallLines(VariableWidthPaths& toolpaths); + static void removeSmallLines(std::vector<VariableWidthLines>& toolpaths); /*! * 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); + static void simplifyToolPaths(std::vector<VariableWidthLines>& toolpaths, const Settings& settings); private: const Polygons& outline; //<! A reference to the outline polygon that is the designated area @@ -112,14 +111,13 @@ private: coord_t bead_width_x; //<! The subsequently extrusion line width with which libArachne generates its walls if WallToolPaths was called with the nominal_bead_width Constructor this is the same as bead_width_0 size_t inset_count; //<! The maximum number of walls to generate coord_t wall_0_inset; //<! How far to inset the outer wall. Should only be applied when printing the actual walls, not extra infill/skin/support walls. - StrategyType strategy_type; //<! The wall generating strategy bool print_thin_walls; //<! Whether to enable the widening beading meta-strategy for thin features coord_t min_feature_size; //<! The minimum size of the features that can be widened by the widening beading meta-strategy. Features thinner than that will not be printed coord_t min_bead_width; //<! The minimum bead size to use when widening thin model features with the widening beading meta-strategy double small_area_length; //<! The length of the small features which are to be filtered out, this is squared into a surface coord_t transition_length; //<! The transitioning length when the amount of extrusion lines changes bool toolpaths_generated; //<! Are the toolpaths generated - VariableWidthPaths toolpaths; //<! The generated toolpaths + std::vector<VariableWidthLines> toolpaths; //<! The generated toolpaths binned by inset_idx. Polygons inner_contour; //<! The inner contour of the generated toolpaths const Settings& settings; }; diff --git a/src/Wireframe2gcode.cpp b/src/Wireframe2gcode.cpp index 1702a7cfc..e49439415 100644 --- a/src/Wireframe2gcode.cpp +++ b/src/Wireframe2gcode.cpp @@ -645,7 +645,7 @@ void Wireframe2gcode::processSkirt() order.optimize(); const Settings& scene_settings = Application::getInstance().current_slice->scene.settings; - for(const PathOrderOptimizer<PolygonPointer>::Path& path : order.paths) + for(const PathOrderPath<PolygonPointer>& path : order.paths) { gcode.writeTravel((*path.vertices)[path.start_vertex], scene_settings.get<Velocity>("speed_travel")); for(size_t vertex_index = 0; vertex_index < path.vertices->size(); ++vertex_index) diff --git a/src/gcodeExport.cpp b/src/gcodeExport.cpp index 45f472065..c43941674 100644 --- a/src/gcodeExport.cpp +++ b/src/gcodeExport.cpp @@ -1172,7 +1172,8 @@ void GCodeExport::writeFanCommand(double speed) } else if (speed > 0) { - *output_stream << "M106 S" << PrecisionedDouble{1, speed * 255 / 100}; + const bool should_scale_zero_to_one = Application::getInstance().current_slice->scene.settings.get<bool>("machine_scale_fan_speed_zero_to_one"); + *output_stream << "M106 S" << PrecisionedDouble{(should_scale_zero_to_one ? 2u : 1u), (should_scale_zero_to_one ? speed : speed * 255) / 100}; if (fan_number) { *output_stream << " P" << fan_number; diff --git a/src/infill.cpp b/src/infill.cpp index c5e20acbd..79c945e1a 100644 --- a/src/infill.cpp +++ b/src/infill.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include <algorithm> //For std::sort. @@ -46,7 +46,7 @@ static inline int computeScanSegmentIdx(int x, int line_width) namespace cura { -Polygons Infill::generateWallToolPaths(VariableWidthPaths& toolpaths, Polygons& outer_contour, const size_t wall_line_count, const coord_t line_width, const coord_t infill_overlap, const Settings& settings) +Polygons Infill::generateWallToolPaths(std::vector<VariableWidthLines>& toolpaths, Polygons& outer_contour, const size_t wall_line_count, const coord_t line_width, const coord_t infill_overlap, const Settings& settings) { outer_contour = outer_contour.offset(infill_overlap); @@ -65,7 +65,7 @@ Polygons Infill::generateWallToolPaths(VariableWidthPaths& toolpaths, Polygons& return inner_contour; } -void Infill::generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider, const LightningLayer* lightning_trees, const SliceMeshStorage* mesh) +void Infill::generate(std::vector<VariableWidthLines>& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider, const LightningLayer* lightning_trees, const SliceMeshStorage* mesh) { if (outer_contour.empty()) { @@ -79,7 +79,6 @@ void Infill::generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, //The lines along the edge must lie next to the border, not on it. //This makes those algorithms a lot simpler. if (pattern == EFillMethod::ZIG_ZAG //Zig-zag prints the zags along the walls. - || pattern == EFillMethod::CONCENTRIC //Concentric at high densities needs to print alongside the walls, not overlapping them. || (zig_zaggify && (pattern == EFillMethod::LINES //Zig-zaggified infill patterns print their zags along the walls. || pattern == EFillMethod::TRIANGLES || pattern == EFillMethod::GRID @@ -97,10 +96,10 @@ void Infill::generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, constexpr coord_t gap_wall_count = 1; // Only need one wall here, less even, in a sense. constexpr coord_t wall_0_inset = 0; //Don't apply any outer wall inset for these. That's just for the outer wall. WallToolPaths wall_toolpaths(inner_contour, infill_line_width, gap_wall_count, wall_0_inset, settings); - VariableWidthPaths gap_fill_paths = wall_toolpaths.getToolPaths(); + std::vector<VariableWidthLines> gap_fill_paths = wall_toolpaths.getToolPaths(); // Add the gap filling to the toolpaths and make the new inner contour 'aware' of the gap infill: - // (Can't use getContours here, becasue only _some_ of the lines Arachne has generated are needed.) + // (Can't use getContours here, because only _some_ of the lines Arachne has generated are needed.) Polygons gap_filled_areas; for (const auto& var_width_line : gap_fill_paths) { @@ -169,13 +168,18 @@ void Infill::generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, }); result_polygons.erase(it, result_polygons.end()); - PolygonConnector connector(infill_line_width, infill_line_width * 3 / 2); + PolygonConnector connector(infill_line_width); connector.add(result_polygons); - result_polygons = connector.connect(); + connector.add(toolpaths); + Polygons connected_polygons; + std::vector<VariableWidthLines> connected_paths; + connector.connect(connected_polygons, connected_paths); + result_polygons = connected_polygons; + toolpaths = connected_paths; } } -void Infill::_generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider, const LightningLayer * lightning_trees, const SliceMeshStorage* mesh) +void Infill::_generate(std::vector<VariableWidthLines>& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider, const LightningLayer * lightning_trees, const SliceMeshStorage* mesh) { if (inner_contour.empty()) return; if (line_distance == 0) return; @@ -248,8 +252,8 @@ void Infill::_generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, result_polygons.simplify(max_resolution, max_deviation); - if (zig_zaggify || - pattern == EFillMethod::CROSS || pattern == EFillMethod::CROSS_3D || pattern == EFillMethod::CUBICSUBDIV || pattern == EFillMethod::GYROID || pattern == EFillMethod::ZIG_ZAG) + if ( ! skip_line_stitching && (zig_zaggify || + pattern == EFillMethod::CROSS || pattern == EFillMethod::CROSS_3D || pattern == EFillMethod::CUBICSUBDIV || pattern == EFillMethod::GYROID || pattern == EFillMethod::ZIG_ZAG)) { // don't stich for non-zig-zagged line infill types Polygons stiched_lines; PolylineStitcher<Polygons, Polygon, Point>::stitch(result_lines, stiched_lines, result_polygons, infill_line_width); @@ -344,31 +348,30 @@ void Infill::generateLightningInfill(const LightningLayer* trees, Polygons& resu result_lines.add(trees->convertToLines(inner_contour, infill_line_width)); } -void Infill::generateConcentricInfill(VariableWidthPaths& toolpaths, const Settings& settings) +void Infill::generateConcentricInfill(std::vector<VariableWidthLines>& toolpaths, const Settings& settings) { - constexpr coord_t wall_0_inset = 0; // Don't apply any outer wall inset for these. That's just for the outer wall. - const bool iterative = line_distance > infill_line_width; // Do it all at once if there is not need for a gap, otherwise, iterate. const coord_t min_area = infill_line_width * infill_line_width; - Polygons current_inset = inner_contour.offset(infill_line_width / 2); - do + + Polygons current_inset = inner_contour; + while(true) { - if (iterative) - { - current_inset = current_inset.offset(-infill_line_width * 2).offset(infill_line_width * 2); - } - current_inset.simplify(); - if (current_inset.area() <= min_area) + //If line_distance is 0, start from the same contour as the previous line, except where the previous line closed up the shape. + //So we add the whole nominal line width first (to allow lines to be closer together than 1 line width if the line distance is smaller) and then subtract line_distance. + current_inset = current_inset.offset(infill_line_width - line_distance); + current_inset.simplify(); //Many insets lead to increasingly detailed shapes. Simplify to speed up processing. + if(current_inset.area() < min_area) //So small that it's inconsequential. Stop here. { break; } - const coord_t inset_wall_count = iterative ? 1 : std::numeric_limits<coord_t>::max(); + constexpr size_t inset_wall_count = 1; //1 wall at a time. + constexpr coord_t wall_0_inset = 0; //Don't apply any outer wall inset for these. That's just for the outer wall. WallToolPaths wall_toolpaths(current_inset, infill_line_width, inset_wall_count, wall_0_inset, settings); - const VariableWidthPaths inset_paths = wall_toolpaths.getToolPaths(); - + const std::vector<VariableWidthLines> inset_paths = wall_toolpaths.getToolPaths(); toolpaths.insert(toolpaths.end(), inset_paths.begin(), inset_paths.end()); - current_inset = wall_toolpaths.getInnerContour().offset((infill_line_width / 2) - line_distance); - } while (iterative); + + current_inset = wall_toolpaths.getInnerContour(); + } } void Infill::generateGridInfill(Polygons& result) diff --git a/src/infill.h b/src/infill.h index 161d27606..12ae8a76e 100644 --- a/src/infill.h +++ b/src/infill.h @@ -40,6 +40,7 @@ class Infill coord_t max_deviation; //!< Max deviation fro the original poly when enforcing max_resolution size_t wall_line_count; //!< Number of walls to generate at the boundary of the infill region, spaced \ref infill_line_width apart const Point infill_origin; //!< origin of the infill pattern + bool skip_line_stitching; //!< Whether to bypass the line stitching normally performed for polyline type infills bool connected_zigzags; //!< (ZigZag) Whether endpieces of zigzag infill should be connected to the nearest infill line on both sides of the zigzag connector bool use_endpieces; //!< (ZigZag) Whether to include endpieces: zigzag connector segments from one infill line to itself bool skip_some_zags; //!< (ZigZag) Whether to skip some zags @@ -64,6 +65,7 @@ public: , coord_t max_deviation , size_t wall_line_count = 0 , const Point& infill_origin = Point() + , bool skip_line_stitching = false , bool connected_zigzags = false , bool use_endpieces = false , bool skip_some_zags = false @@ -85,6 +87,7 @@ public: , max_deviation(max_deviation) , wall_line_count(wall_line_count) , infill_origin(infill_origin) + , skip_line_stitching(skip_line_stitching) , connected_zigzags(connected_zigzags) , use_endpieces(use_endpieces) , skip_some_zags(skip_some_zags) @@ -101,7 +104,7 @@ public: /*! * Generate the infill. * - * \param toolpaths (output) The resulting variable-width paths (from the extra walls around the pattern). + * \param toolpaths (output) The resulting variable-width paths (from the extra walls around the pattern). Binned by inset_idx. * \param result_polygons (output) The resulting polygons (from concentric infill) * \param result_lines (output) The resulting line segments (from linear infill types) * \param settings A settings storage to use for generating variable-width walls. @@ -109,13 +112,13 @@ public: * \param mesh A mesh for which to generate infill (should only be used for non-helper-mesh objects). * \param[in] cross_fill_provider The cross fractal subdivision decision functor */ - void generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider = nullptr, const LightningLayer * lightning_layer = nullptr, const SliceMeshStorage* mesh = nullptr); + void generate(std::vector<VariableWidthLines>& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_provider = nullptr, const LightningLayer * lightning_layer = nullptr, const SliceMeshStorage* mesh = nullptr); /*! * Generate the wall toolpaths of an infill area. It will return the inner contour and set the inner-contour. * This function is called within the generate() function but can also be called stand-alone * - * \param toolpaths [out] The generated toolpaths + * \param toolpaths [out] The generated toolpaths. Binned by inset_idx. * \param outer_contour [in,out] the outer contour, this is offsetted with the infill overlap * \param wall_line_count [in] The number of walls that needs to be generated * \param line_width [in] The optimum wall line width of the walls @@ -123,12 +126,12 @@ public: * \param settings [in] A settings storage to use for generating variable-width walls. * \return The inner contour of the wall toolpaths */ - static Polygons generateWallToolPaths(VariableWidthPaths& toolpaths, Polygons& outer_contour, const size_t wall_line_count, const coord_t line_width, const coord_t infill_overlap, const Settings& settings); + static Polygons generateWallToolPaths(std::vector<VariableWidthLines>& toolpaths, Polygons& outer_contour, const size_t wall_line_count, const coord_t line_width, const coord_t infill_overlap, const Settings& settings); private: /*! * Generate the infill pattern without the infill_multiplier functionality */ - void _generate(VariableWidthPaths& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_pattern = nullptr, const LightningLayer * lightning_layer = nullptr, const SliceMeshStorage* mesh = nullptr); + void _generate(std::vector<VariableWidthLines>& toolpaths, Polygons& result_polygons, Polygons& result_lines, const Settings& settings, const SierpinskiFillProvider* cross_fill_pattern = nullptr, const LightningLayer * lightning_layer = nullptr, const SliceMeshStorage* mesh = nullptr); /*! * Multiply the infill lines, so that any single line becomes [infill_multiplier] lines next to each other. @@ -253,10 +256,10 @@ private: /*! * Generate sparse concentric infill * - * \param toolpaths (output) The resulting toolpaths + * \param toolpaths (output) The resulting toolpaths. Binned by inset_idx. * \param inset_value The offset between each consecutive two polygons */ - void generateConcentricInfill(VariableWidthPaths& toolpaths, const Settings& settings); + void generateConcentricInfill(std::vector<VariableWidthLines>& toolpaths, const Settings& settings); /*! * Generate a rectangular grid of infill lines diff --git a/src/pathPlanning/Comb.cpp b/src/pathPlanning/Comb.cpp index 7585d4fef..b10df1aa1 100644 --- a/src/pathPlanning/Comb.cpp +++ b/src/pathPlanning/Comb.cpp @@ -33,7 +33,7 @@ Comb::Comb(const SliceDataStorage& storage, const LayerIndex layer_nr, const Pol : storage(storage) , layer_nr(layer_nr) , offset_from_outlines(comb_boundary_offset) // between second wall and infill / other walls -, max_moveInside_distance2(offset_from_outlines * 2 * offset_from_outlines * 2) +, max_moveInside_distance2(offset_from_outlines * offset_from_outlines) , offset_from_inside_to_outside(offset_from_outlines + travel_avoid_distance) , max_crossing_dist2(offset_from_inside_to_outside * offset_from_inside_to_outside * 2) // so max_crossing_dist = offset_from_inside_to_outside * sqrt(2) =approx 1.5 to allow for slightly diagonal crossings and slightly inaccurate crossing computation , boundary_inside_minimum( comb_boundary_inside_minimum ) // copy the boundary, because the partsView_inside will reorder the polygons @@ -80,7 +80,7 @@ bool Comb::calc(const ExtruderTrain& train, Point start_point, Point end_point, unsigned int end_inside_poly = NO_INDEX; const bool end_inside = moveInside(boundary_inside_optimal, _end_inside, inside_loc_to_line_optimal.get(), end_point, end_inside_poly); - unsigned int start_part_boundary_poly_idx = NO_INDEX; // Added initial value to stop MSVC throwing an exception in debug mode + unsigned int start_part_boundary_poly_idx = NO_INDEX; // Added initial value to stop MSVC throwing an exception in debug mode unsigned int end_part_boundary_poly_idx = NO_INDEX; unsigned int start_part_idx = (start_inside_poly == NO_INDEX)? NO_INDEX : partsView_inside_optimal.getPartContaining(start_inside_poly, &start_part_boundary_poly_idx); unsigned int end_part_idx = (end_inside_poly == NO_INDEX)? NO_INDEX : partsView_inside_optimal.getPartContaining(end_inside_poly, &end_part_boundary_poly_idx); @@ -213,11 +213,29 @@ bool Comb::calc(const ExtruderTrain& train, Point start_point, Point end_point, } else { - bool combing_succeeded = LinePolygonsCrossings::comb(*boundary_outside, getOutsideLocToLine(), start_crossing.out, end_crossing.out, comb_paths.back(), offset_dist_to_get_from_on_the_polygon_to_outside, max_comb_distance_ignored, fail_on_unavoidable_obstacles); - if (!combing_succeeded) + CombPath tmp_comb_path; + bool combing_succeeded = LinePolygonsCrossings::comb(*boundary_outside, getOutsideLocToLine(), start_crossing.out, end_crossing.out, tmp_comb_path, offset_dist_to_get_from_on_the_polygon_to_outside, max_comb_distance_ignored, true); + + if (combing_succeeded) + { + // add combing travel moves if the combing was successful + comb_paths.push_back(tmp_comb_path); + } + else if (fail_on_unavoidable_obstacles) { return false; } + else + { + // if combing is not possible then move directly to the target destination + // this happens for instance when trying to avoid skin-regions and combing from + // an origin that is on a hole-boundary to a destination that is on the outline-border + comb_paths.emplace_back(); + comb_paths.throughAir = true; + comb_paths.back().cross_boundary = true; + comb_paths.back().push_back(start_crossing.in_or_mid); + comb_paths.back().push_back(end_crossing.in_or_mid); + } } } else diff --git a/src/pathPlanning/GCodePath.cpp b/src/pathPlanning/GCodePath.cpp index 15dbfa02f..bd0d57578 100644 --- a/src/pathPlanning/GCodePath.cpp +++ b/src/pathPlanning/GCodePath.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2018 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "GCodePath.h" @@ -6,11 +6,12 @@ namespace cura { -GCodePath::GCodePath(const GCodePathConfig& config, std::string mesh_id, const SpaceFillType space_fill_type, const Ratio flow, const bool spiralize, const Ratio speed_factor) : +GCodePath::GCodePath(const GCodePathConfig& config, std::string mesh_id, const SpaceFillType space_fill_type, const Ratio flow, const Ratio width_factor, const bool spiralize, const Ratio speed_factor) : config(&config), mesh_id(mesh_id), space_fill_type(space_fill_type), flow(flow), +width_factor(width_factor), speed_factor(speed_factor), speed_back_pressure_factor(1.0), retract(false), @@ -33,12 +34,12 @@ bool GCodePath::isTravelPath() const double GCodePath::getExtrusionMM3perMM() const { - return flow * config->getExtrusionMM3perMM(); + return flow * width_factor * config->getExtrusionMM3perMM(); } coord_t GCodePath::getLineWidthForLayerView() const { - return flow * config->getLineWidth() * config->getFlowRatio(); + return flow * width_factor * config->getLineWidth() * config->getFlowRatio(); } void GCodePath::setFanSpeed(double fan_speed) diff --git a/src/pathPlanning/GCodePath.h b/src/pathPlanning/GCodePath.h index da6af9320..ec29551e1 100644 --- a/src/pathPlanning/GCodePath.h +++ b/src/pathPlanning/GCodePath.h @@ -1,4 +1,4 @@ -//Copyright (c) 2018 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef PATH_PLANNING_G_CODE_PATH_H @@ -32,6 +32,7 @@ public: std::string mesh_id; //!< Which mesh this path belongs to, if any. If it's not part of any mesh, the mesh ID should be 0. SpaceFillType space_fill_type; //!< The type of space filling of which this path is a part Ratio flow; //!< A type-independent flow configuration + Ratio width_factor; //!< Adjustment to the line width. Similar to flow, but causes the speed_back_pressure_factor to be adjusted. Ratio speed_factor; //!< A speed factor that is multiplied with the travel speed. This factor can be used to change the travel speed. Ratio speed_back_pressure_factor; // <! The factor the (non-travel) speed should be multiplied with as a consequence of back pressure compensation. bool retract; //!< Whether the path is a move path preceded by a retraction move; whether the path is a retracted move path. @@ -56,11 +57,12 @@ public: * \param space_fill_type The type of space filling of which this path is a * part. * \param flow The flow rate to print this path with. + * \param width_factor A multiplier on the line width. * \param spiralize Gradually increment the z-coordinate while traversing * \param speed_factor The factor that the travel speed will be multiplied with * this path. */ - GCodePath(const GCodePathConfig& config, std::string mesh_id, const SpaceFillType space_fill_type, const Ratio flow, const bool spiralize, const Ratio speed_factor = 1.0); + GCodePath(const GCodePathConfig& config, std::string mesh_id, const SpaceFillType space_fill_type, const Ratio flow, const Ratio width_factor, const bool spiralize, const Ratio speed_factor = 1.0); /*! * Whether this config is the config of a travel path. diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index b411f53b1..24b3fc6e8 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include <cctype> @@ -348,27 +348,6 @@ template<> EFillMethod Settings::get<EFillMethod>(const std::string& key) const } } -template<> StrategyType Settings::get<StrategyType>(const std::string& key) const -{ - const std::string& value = get<std::string>(key); - if (value == "center_deviation") - { - return StrategyType::Center; - } - else if (value == "distributed") - { - return StrategyType::Distributed; - } - else if (value == "inward_distributed") - { - return StrategyType::InwardDistributed; - } - else //Default. - { - return StrategyType::None; - } -} - template<> EPlatformAdhesion Settings::get<EPlatformAdhesion>(const std::string& key) const { const std::string& value = get<std::string>(key); @@ -569,11 +548,7 @@ template<> SlicingTolerance Settings::get<SlicingTolerance>(const std::string& k template<> InsetDirection Settings::get<InsetDirection>(const std::string& key) const { const std::string& value = get<std::string>(key); - if(value == "center_last") - { - return InsetDirection::CENTER_LAST; - } - else if(value == "outside_in") + if(value == "outside_in") { return InsetDirection::OUTSIDE_IN; } diff --git a/src/skin.cpp b/src/skin.cpp index 7cba5874b..c229849a5 100644 --- a/src/skin.cpp +++ b/src/skin.cpp @@ -309,9 +309,9 @@ void SkinInfillAreaComputation::generateRoofing(SliceLayerPart& part) for(SkinPart& skin_part : part.skin_parts) { Polygons no_air_above = generateNoAirAbove(part, roofing_layer_count); - skin_part.roofing_fill = skin_part.skin_fill.difference(no_air_above); - skin_part.skin_fill = skin_part.skin_fill.intersection(no_air_above); + skin_part.roofing_fill = skin_part.outline.difference(no_air_above); + skin_part.skin_fill = skin_part.outline.intersection(no_air_above); // Insets are NOT generated for any layer if the top/bottom pattern is concentric. // In this case, we still want to generate insets for the roofing layers based on the extra skin wall count, // if the roofing pattern is not concentric. @@ -350,10 +350,6 @@ void SkinInfillAreaComputation::generateRoofing(SliceLayerPart& part) regenerateRoofingFillAndInnerInfill(part, skin_part); } } - else - { - skin_part.skin_fill = skin_part.outline; - } } } diff --git a/src/sliceDataStorage.h b/src/sliceDataStorage.h index dd45e5464..658932fb5 100644 --- a/src/sliceDataStorage.h +++ b/src/sliceDataStorage.h @@ -40,7 +40,7 @@ class SkinPart { public: PolygonsPart outline; //!< The skinOutline is the area which needs to be 100% filled to generate a proper top&bottom filling. It's filled by the "skin" module. Includes both roofing and non-roofing. - VariableWidthPaths inset_paths; //!< The insets represented as variable line-width paths. The insets are also known as perimeters or the walls. + std::vector<VariableWidthLines> inset_paths; //!< The insets represented as variable line-width paths. The insets are also known as perimeters or the walls. Binned by inset_idx. Polygons skin_fill; //!< The part of the skin which is not roofing. Polygons roofing_fill; //!< The inner infill which has air directly above Polygons top_most_surface_fill; //!< The inner infill of the uppermost top layer which has air directly above. @@ -66,8 +66,8 @@ public: Polygons spiral_wall; //!< The centerline of the wall used by spiralize mode. Only computed if spiralize mode is enabled. Polygons inner_area; //!< The area of the outline, minus the walls. This will be filled with either skin or infill. std::vector<SkinPart> skin_parts; //!< The skin parts which are filled for 100% with lines and/or insets. - VariableWidthPaths wall_toolpaths; //!< toolpaths for walls, will replace(?) the insets - VariableWidthPaths infill_wall_toolpaths; //!< toolpaths for the infill area's + std::vector<VariableWidthLines> wall_toolpaths; //!< toolpaths for walls, will replace(?) the insets. Binned by inset_idx. + std::vector<VariableWidthLines> infill_wall_toolpaths; //!< toolpaths for the walls of the infill areas. Binned by inset_idx. /*! * The areas inside of the mesh. diff --git a/src/support.cpp b/src/support.cpp index 8d1eba4e6..23e611ffb 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -75,15 +75,15 @@ void AreaSupport::splitGlobalSupportAreasIntoSupportInfillParts(SliceDataStorage const EFillMethod support_pattern = infill_extruder.settings.get<EFillMethod>("support_pattern"); const coord_t support_line_width = infill_extruder.settings.get<coord_t>("support_line_width"); - // the wall line count is used for calculating insets, and we generate support infill patterns within the insets + // The wall line count is used for calculating insets, and we generate support infill patterns within the insets const size_t wall_line_count = infill_extruder.settings.get<size_t>("support_wall_count"); - // generate separate support islands + // Generate separate support islands for (unsigned int layer_nr = 0; layer_nr < total_layer_count - 1; ++layer_nr) { unsigned int wall_line_count_this_layer = wall_line_count; if (layer_nr == 0 && (support_pattern == EFillMethod::LINES || support_pattern == EFillMethod::ZIG_ZAG)) - { // the first layer will be printed wit ha grid pattern + { // The first layer will be printed with a grid pattern wall_line_count_this_layer++; } assert(storage.support.supportLayers[layer_nr].support_infill_parts.empty() && "support infill part list is supposed to be uninitialized"); @@ -91,7 +91,7 @@ void AreaSupport::splitGlobalSupportAreasIntoSupportInfillParts(SliceDataStorage const Polygons& global_support_areas = global_support_areas_per_layer[layer_nr]; if (global_support_areas.size() == 0 || layer_nr < min_layer || layer_nr > max_layer) { - // initialize support_infill_parts empty + // Initialize support_infill_parts empty storage.support.supportLayers[layer_nr].support_infill_parts.clear(); continue; } @@ -104,7 +104,7 @@ void AreaSupport::splitGlobalSupportAreasIntoSupportInfillParts(SliceDataStorage { support_line_width_here *= infill_extruder.settings.get<Ratio>("initial_layer_line_width_factor"); } - // we don't generate insets and infill area for the parts yet because later the skid/brim and prime + // We don't generate insets and infill area for the parts yet because later the skirt/brim and prime // tower will remove themselves from the support, so the outlines of the parts can be changed. SupportInfillPart support_infill_part(island_outline, support_line_width_here, wall_line_count_this_layer); @@ -202,13 +202,13 @@ void AreaSupport::generateGradualSupport(SliceDataStorage& storage) { SupportInfillPart& support_infill_part = support_infill_parts[part_idx]; - // NOTE: This both generates the walls _and_ returns the _actual_ infill area (the one _without_ walls) for use in the rest of the method. Polygons original_area = support_infill_part.getInfillArea(); - const Polygons infill_area = Infill::generateWallToolPaths(support_infill_part.wall_toolpaths, original_area, wall_count, wall_width, overlap, infill_extruder.settings); - if (infill_area.empty()) + if (original_area.empty()) { continue; } + // NOTE: This both generates the walls _and_ returns the _actual_ infill area (the one _without_ walls) for use in the rest of the method. + const Polygons infill_area = Infill::generateWallToolPaths(support_infill_part.wall_toolpaths, original_area, wall_count, wall_width, overlap, infill_extruder.settings); const AABB& this_part_boundary_box = support_infill_part.outline_boundary_box; // calculate density areas for this island @@ -495,7 +495,12 @@ Polygons AreaSupport::join(const SliceDataStorage& storage, const Polygons& supp switch (mesh_group_settings.get<EPlatformAdhesion>("adhesion_type")) { case EPlatformAdhesion::BRIM: - adhesion_size = skirt_brim_extruder.settings.get<coord_t>("skirt_brim_line_width") * skirt_brim_extruder.settings.get<Ratio>("initial_layer_line_width_factor") * skirt_brim_extruder.settings.get<size_t>("brim_line_count") + extra_skirt_line_width; + adhesion_size = + skirt_brim_extruder.settings.get<coord_t>("brim_width") + + skirt_brim_extruder.settings.get<coord_t>("skirt_brim_line_width") + * skirt_brim_extruder.settings.get<size_t>("brim_line_count") + * skirt_brim_extruder.settings.get<Ratio>("initial_layer_line_width_factor") + + extra_skirt_line_width; break; case EPlatformAdhesion::RAFT: { diff --git a/src/utils/ExtrusionLine.cpp b/src/utils/ExtrusionLine.cpp index 7dd940c93..6523b4428 100644 --- a/src/utils/ExtrusionLine.cpp +++ b/src/utils/ExtrusionLine.cpp @@ -128,9 +128,7 @@ void ExtrusionLine::simplify(const coord_t smallest_line_segment_squared, const // 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; + // Remove the current junction (vertex). continue; } @@ -210,8 +208,8 @@ coord_t ExtrusionLine::calculateExtrusionAreaDeviationError(ExtrusionJunction A, * | |--------------------------| | |***************************| * | | ------------------------------------------ * --------------- ^ ************** - * ^ C.w ^ - * B.w new_width = weighted_average_width + * ^ B.w + C.w / 2 ^ + * A.w + B.w / 2 new_width = weighted_average_width * * * ******** denote the total extrusion area deviation error in the consecutive segments as a result of using the @@ -220,18 +218,20 @@ coord_t ExtrusionLine::calculateExtrusionAreaDeviationError(ExtrusionJunction A, * */ const coord_t ab_length = vSize(B - A); const coord_t bc_length = vSize(C - B); - const coord_t width_diff = llabs(B.w - C.w); + const coord_t width_diff = std::max(std::abs(B.w - A.w), std::abs(C.w - B.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. - weighted_average_width = (ab_length * B.w + bc_length * C.w) / vSize(C - A); - return llabs(B.w - weighted_average_width) * ab_length + llabs(C.w - weighted_average_width) * bc_length; + const coord_t ab_weight = (A.w + B.w) / 2; + const coord_t bc_weight = (B.w + C.w) / 2; + weighted_average_width = (ab_length * ab_weight + bc_length * bc_weight) / vSize(C - A); + return std::abs(ab_weight - weighted_average_width) * ab_length + std::abs(bc_weight - 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; + weighted_average_width = ab_length > bc_length ? A.w : B.w; return ab_length > bc_length ? width_diff * bc_length : width_diff * ab_length; } } diff --git a/src/utils/ExtrusionLine.h b/src/utils/ExtrusionLine.h index 32f72eba1..4a1ce2f87 100644 --- a/src/utils/ExtrusionLine.h +++ b/src/utils/ExtrusionLine.h @@ -269,6 +269,5 @@ public: }; using VariableWidthLines = std::vector<ExtrusionLine>; //<! The ExtrusionLines generated by libArachne -using VariableWidthPaths = std::vector<VariableWidthLines>; //<! The toolpaths generated by libArachne, binned by inset aka Path } // namespace cura #endif // UTILS_EXTRUSION_LINE_H diff --git a/src/utils/PolygonConnector.cpp b/src/utils/PolygonConnector.cpp index 34ce980c8..534eb4654 100644 --- a/src/utils/PolygonConnector.cpp +++ b/src/utils/PolygonConnector.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2020 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include "PolygonConnector.h" @@ -9,260 +9,100 @@ namespace cura { -Polygons PolygonConnector::connect() -{ - Polygons ret; +PolygonConnector::PolygonConnector(const coord_t line_width) +: line_width(line_width) +{} - if (input_polygons.empty()) - { - return ret; - } - - std::vector<Polygon> to_connect; - to_connect.reserve(input_polygons.size()); - for (ConstPolygonPointer poly : input_polygons) +void PolygonConnector::add(const Polygons& input) +{ + for (ConstPolygonRef poly : input) { - to_connect.emplace_back(*poly); // copy into list + input_polygons.push_back(poly); } +} - while (!to_connect.empty()) +void PolygonConnector::add(const std::vector<VariableWidthLines>& input) +{ + for(const VariableWidthLines& lines : input) { - if (to_connect.size() == 1) + for(const ExtrusionLine& line : lines) { - ret.add(std::move(to_connect.back())); - break; + input_paths.push_back(line); } - Polygon current = std::move(to_connect.back()); - to_connect.pop_back(); - - std::optional<PolygonBridge> bridge = getBridge(current, to_connect); - if (bridge) - { - all_bridges.push_back(*bridge); // just for keeping scores - // remove other poly from the list and put the newly connected one on the list - // i.e. replace the old other poly by the new one - PolygonRef other_poly(*const_cast<ClipperLib::Path*>(bridge->a.to.poly.operator->())); // const casting a ConstPolygonPointer is difficult! - other_poly = std::move(connectPolygonsAlongBridge(*bridge)); // connect the bridged parts and overwrite the other polygon with it. + } +} - // don't store the current poly, it has just been connected and stored - } - else - { - ret.add(std::move(current)); - } +void PolygonConnector::connect(Polygons& output_polygons, std::vector<VariableWidthLines>& output_paths) +{ + std::vector<Polygon> result_polygons = connectGroup(input_polygons); + for(Polygon& polygon : result_polygons) + { + output_polygons.add(polygon); } - return ret; + std::vector<ExtrusionLine> result_paths = connectGroup(input_paths); + output_paths.push_back(result_paths); } - -Polygon PolygonConnector::connectPolygonsAlongBridge(const PolygonConnector::PolygonBridge& bridge) +Point PolygonConnector::getPosition(const Point& vertex) const { - // enforce the following orientations: - // - // <<<<<<X......X<<<<<<< poly2 - // ^ v - // ^ v - // ^ a b v bridge - // ^ v - // >>>>>>X......X>>>>>>> poly1 - // - // this should work independent from whether it is a hole polygon or a outline polygon - Polygon ret; - addPolygonSegment(bridge.b.from, bridge.a.from, ret); - addPolygonSegment(bridge.a.to, bridge.b.to, ret); - return ret; + return vertex; } -void PolygonConnector::addPolygonSegment(const ClosestPolygonPoint& start, const ClosestPolygonPoint& end, PolygonRef result) +Point PolygonConnector::getPosition(const ExtrusionJunction& junction) const { - // <<<<<<<.start end.<<<<<<<< - // ^ v - // ^ v - // >>>>>>>.end.....start.>>>>>>> - assert(start.poly == end.poly && "We can only bridge from one polygon from the other if both connections depart from the one polygon!"); - ConstPolygonRef poly = *end.poly; - int16_t dir = getPolygonDirection(end, start); // we get the direction of the polygon in between the bridge connections, while we add the segment of the polygon not in between the segments - - result.add(start.p()); - bool first_iter = true; - for (size_t vert_nr = 0; vert_nr < poly.size(); vert_nr++) - { - size_t vert_idx = - (dir > 0)? - (start.point_idx + 1 + vert_nr) % poly.size() - : (static_cast<size_t>(start.point_idx) - vert_nr + poly.size()) % poly.size(); // cast in order to accomodate subtracting - if (!first_iter // don't return without adding points when the starting point and ending point are on the same polygon segment - && vert_idx == (end.point_idx + ((dir > 0)? 1 : 0)) % poly.size()) - { // we've added all verts of the original polygon segment between start and end - break; - } - first_iter = false; - result.add(poly[vert_idx]); - } - result.add(end.p()); + return junction.p; } -int16_t PolygonConnector::getPolygonDirection(const ClosestPolygonPoint& from, const ClosestPolygonPoint& to) +coord_t PolygonConnector::getWidth(const Point&) const { - assert(from.poly == to.poly && "We can only bridge from one polygon from the other if both connections depart from the one polygon!"); - ConstPolygonRef poly = *from.poly; - if (from.point_idx == to.point_idx) - { - const Point prev_vert = poly[from.point_idx]; - const coord_t from_dist2 = vSize2(from.p() - prev_vert); - const coord_t to_dist2 = vSize2(to.p() - prev_vert); - return (to_dist2 > from_dist2)? 1 : -1; - } - // TODO: replace naive implementation by robust implementation - // naive idea: there are less vertices in between the connection points than around - const size_t a_to_b_vertex_count = (static_cast<size_t>(to.point_idx) - from.point_idx + poly.size()) % poly.size(); // cast in order to accomodate subtracting - if (a_to_b_vertex_count > poly.size() / 2) - { - return -1; // from a to b is in the reverse direction as the vertices are saved in the polygon - } - else - { - return 1; // from a to b is in the same direction as the vertices are saved in the polygon - } + return line_width; } -std::optional<PolygonConnector::PolygonBridge> PolygonConnector::getBridge(ConstPolygonRef from_poly, std::vector<Polygon>& to_polygons) +coord_t PolygonConnector::getWidth(const ExtrusionJunction& junction) const { - // line distance between consecutive polygons should be at least the line_width - const coord_t min_connection_length = line_width - 10; - // Only connect polygons if they are next to each other - // allow for a bit of wiggle room for when the polygons are very curved, - // which causes any connection distance to be larger than the offset amount - const coord_t max_connection_length = line_width * 3 / 2; - - std::optional<PolygonConnector::PolygonConnection> first_connection; - std::optional<PolygonConnector::PolygonConnection> second_connection; - - Polygons to_polys; - for (const Polygon& poly : to_polygons) - { - to_polys.add(poly); - } - - std::function<bool (std::pair<ClosestPolygonPoint, ClosestPolygonPoint>)> can_make_bridge = - [&, this](std::pair<ClosestPolygonPoint, ClosestPolygonPoint> candidate) - { - first_connection.emplace(candidate.first, candidate.second); - first_connection->to.poly = &to_polygons[first_connection->to.poly_idx]; // because it still refered to the local variable [to_polys] - if (first_connection->getDistance2() > max_dist * max_dist) - { - return false; - } - second_connection = PolygonConnector::getSecondConnection(*first_connection); - if (!second_connection) - { - return false; - } - return true; - }; - - std::pair<ClosestPolygonPoint, ClosestPolygonPoint> connection_points = PolygonUtils::findConnection(from_poly, to_polys, min_connection_length, max_connection_length, can_make_bridge); - - if (!connection_points.first.isValid() || !connection_points.second.isValid()) - { // We didn't find a connection which can make a bridge - // We might have found a first_connection and a second_connection, but maybe they didn't satisfy all criteria. - std::optional<PolygonConnector::PolygonBridge> uninitialized; - return uninitialized; - } - - if (first_connection && second_connection) - { - PolygonBridge result(*first_connection, *second_connection); - // ensure that b is always the right connection and a the left - Point a_vec = result.a.to.p() - result.a.from.p(); - Point shift = turn90CCW(a_vec); - if (dot(shift, result.b.from.p() - result.a.from.p()) > 0) - { - std::swap(result.a, result.b); - } - return result; - } - else - { // we couldn't find a connection; maybe the polygon was too small or maybe it is just too far away from other polygons. - return std::optional<PolygonConnector::PolygonBridge>(); - } + return junction.w; } -std::optional<PolygonConnector::PolygonConnection> PolygonConnector::getSecondConnection(PolygonConnection& first) +void PolygonConnector::addVertex(Polygon& polygonal, const Point& position, const coord_t) const { - bool forward = true; - std::optional<ClosestPolygonPoint> from_a = PolygonUtils::getNextParallelIntersection(first.from, first.to.p(), line_width, forward); - if (!from_a) - { // then there's also not going to be a b - return std::optional<PolygonConnector::PolygonConnection>(); - } - std::optional<ClosestPolygonPoint> from_b = PolygonUtils::getNextParallelIntersection(first.from, first.to.p(), line_width, !forward); + polygonal.add(position); +} - std::optional<ClosestPolygonPoint> to_a = PolygonUtils::getNextParallelIntersection(first.to, first.from.p(), line_width, forward); - if (!to_a) - { - return std::optional<PolygonConnector::PolygonConnection>(); - } - std::optional<ClosestPolygonPoint> to_b = PolygonUtils::getNextParallelIntersection(first.to, first.from.p(), line_width, !forward); +void PolygonConnector::addVertex(Polygon& polygonal, const Point& vertex) const +{ + polygonal.add(vertex); +} +void PolygonConnector::addVertex(ExtrusionLine& polygonal, const Point& position, const coord_t width) const +{ + polygonal.emplace_back(position, width, 1); //Perimeter indices don't make sense any more once perimeters are merged. Use 1 as placeholder, being the first "normal" wall. +} - const Point shift = turn90CCW(first.from.p() - first.to.p()); - - std::optional<PolygonConnection> best; - coord_t best_total_distance2 = std::numeric_limits<coord_t>::max(); - for (unsigned int from_idx = 0; from_idx < 2; from_idx++) - { - std::optional<ClosestPolygonPoint> from_opt = (from_idx == 0)? from_a : from_b; - if (!from_opt) - { - continue; - } - for (unsigned int to_idx = 0; to_idx < 2; to_idx++) - { - std::optional<ClosestPolygonPoint> to_opt = (to_idx == 0)? to_a : to_b; - // for each combination of from and to - if (!to_opt) - { - continue; - } - ClosestPolygonPoint from = *from_opt; - ClosestPolygonPoint to = *to_opt; +void PolygonConnector::addVertex(ExtrusionLine& polygonal, const ExtrusionJunction& vertex) const +{ + polygonal.emplace_back(vertex); +} - coord_t from_projection = dot(from.p() - first.to.p(), shift); - coord_t to_projection = dot(to.p() - first.to.p(), shift); - if (from_projection * to_projection <= 0) - { // ends lie on different sides of the first connection - continue; - } +bool PolygonConnector::isClosed(Polygon&) const +{ + return true; +} - const coord_t total_distance2 = vSize2(from.p() - to.p()) + vSize2(from.p() - first.from.p()) + vSize(to.p() - first.to.p()); - if (total_distance2 < best_total_distance2) - { - best.emplace(from, to); - best_total_distance2 = total_distance2; - } - - if (to_opt == to_b) - { - break; - } - } - if (from_opt == from_b) - { - break; - } - } - if (!best || best_total_distance2 > max_dist * max_dist + 2 * (line_width + 10) * (line_width + 10)) - { - return std::optional<PolygonConnector::PolygonConnection>(); - } - else - { - return *best; - } +bool PolygonConnector::isClosed(ExtrusionLine& polygonal) const +{ + return vSize2(polygonal.front() - polygonal.back()) < 25; } +template<> +ExtrusionLine PolygonConnector::createEmpty<ExtrusionLine>() const +{ + constexpr size_t inset_index = 1; //Specialising to set inset_index to 1 instead of maximum int. Connected polys are not specific to any inset. + constexpr bool is_odd = false; + ExtrusionLine result(inset_index, is_odd); + result.is_closed = true; + return result; //No copy, via RVO. +} }//namespace cura diff --git a/src/utils/PolygonConnector.h b/src/utils/PolygonConnector.h index 66d1dcd26..d29961ca0 100644 --- a/src/utils/PolygonConnector.h +++ b/src/utils/PolygonConnector.h @@ -1,4 +1,4 @@ -//Copyright (c) 2019 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef UTILS_POLYGON_CONNECTOR_H @@ -12,6 +12,7 @@ #include "IntPoint.h" #include "polygon.h" #include "polygonUtils.h" +#include "linearAlg2D.h" namespace cura { @@ -26,75 +27,166 @@ namespace cura * o-----+ . +-----o . * / \ . * / \ . - * + * * This way two polygons become one. - * - * By repeating such a procedure many polygons can be connected into a single continuous line. + * + * By repeating such a procedure many polygons can be connected into a single + * continuous line. + * + * This connector can handle ordinary Polygons (which is assumed to print with a + * fixed, given line width) as well as variable-width paths. However with the + * paths it will only connect paths that form closed loops. Paths that don't + * form closed loops will be left unconnected. + * + * While this connector can connect Polygons and VariableWidthLines at the same + * time, it will never connect them together. This is done to keep the result + * and the algorithm simpler. Otherwise it would have to convert polygons to + * paths to make them partially variable width. This is not a use case we need + * right now, since infill patterns cannot generate a mix of these types. + * + * Basic usage of this class is as follows: + * `` + * PolygonConnector connector(line_width, max_dist); //Construct first. + * connector.add(polygons); //Add the polygons and paths you want to connect up. + * connector.add(paths); + * Polygons output_polygons; //Prepare some output variables to store results in. + * VariableWidthLines output_paths; + * connector.connect(output_polygons, output_paths); + * `` */ class PolygonConnector { #ifdef BUILD_TESTS - FRIEND_TEST(PolygonConnectorTest, getBridgeTest); - FRIEND_TEST(PolygonConnectorTest, connectionLengthTest); + FRIEND_TEST(PolygonConnectorTest, getBridgeNestedSquares); + FRIEND_TEST(PolygonConnectorTest, getBridgeAdjacentSquares); + FRIEND_TEST(PolygonConnectorTest, getBridgeClosest); + FRIEND_TEST(PolygonConnectorTest, getBridgeTooFar); + FRIEND_TEST(PolygonConnectorTest, getBridgeTooNarrow); #endif public: - PolygonConnector(coord_t line_width, coord_t max_dist) - : line_width(line_width - 5) // a bit less so that consecutive lines which have become connected can still connect to other lines - // | | | - // ----------o | ----------o | ----------o,,,,o - // | | ==> | | ==> - // -----o | | -----o----o | -----o----o----o - // | | | | | - // | | | o''''o | o''''o | - // | | | | | | | | | - , max_dist(max_dist) - {} + /*! + * Create a connector object that can connect polygons. + * + * This specifies a few settings for the connector. + * \param line_width The width at which the polygons will be printed. + */ + PolygonConnector(const coord_t line_width); /*! * Add polygons to be connected by a future call to \ref PolygonConnector::connect() */ - void add(const Polygons& input) - { - for (ConstPolygonRef poly : input) - { - input_polygons.push_back(poly); - } - } + void add(const Polygons& input); + + /*! + * Add variable-width paths to be connected by a future call to + * \ref PolygonConnector::connect(). + * + * Only the paths that form closed loops will be connected to each other. + * \param input The paths to connect. + */ + void add(const std::vector<VariableWidthLines>& input); /*! * Connect as many polygons together as possible and return the resulting polygons. - * + * * Algorithm outline: * try to connect a polygon to any of the other polygons * - if succeeded, add to pool of polygons to connect * - if failed, remove from pool and add to the result + * \param output_polygons Polygons that were connected as much as possible. + * These are expected to be empty to start with. + * \param output_paths Paths that were connected as much as possible. These + * are expected to be empty to start with. */ - Polygons connect(); + void connect(Polygons& output_polygons, std::vector<VariableWidthLines>& output_paths); protected: coord_t line_width; //!< The distance between the line segments which connect two polygons. - coord_t max_dist; //!< The maximal distance crossed by the connecting segments. Should be more than the \ref line_width in order to accomodate curved polygons. - std::vector<ConstPolygonPointer> input_polygons; //!< The polygons assembled by calls to \ref PolygonConnector::add() + std::vector<Polygon> input_polygons; //!< The polygons assembled by calls to \ref PolygonConnector::add. + std::vector<ExtrusionLine> input_paths; //!< The paths assembled by calls to \ref PolygonConnector::add. + + constexpr static Ratio max_gap = 0.5; //!< The maximum allowed gap between lines that get connected, in multiples of the local line width. Allows connections inside corners where the endpoints are slightly apart. /*! - * Line segment to connect two polygons + * Line segment to connect two polygons, with all the necessary information + * to connect them. + * * A bridge consists of two such connections. + * \tparam Polygonal The type of polygon data to refer to, either Polygon or + * ExtrusionLine. */ + template<typename Polygonal> struct PolygonConnection { - ClosestPolygonPoint from; //!< from location in the source polygon - ClosestPolygonPoint to; //!< to location in the destination polygon + /*! + * The polygon at the source of the connection. + */ + Polygonal* from_poly; - PolygonConnection(ClosestPolygonPoint from, ClosestPolygonPoint to) - : from(from) - , to(to) - {} + /*! + * The index of the line segment at the source of the connection. + * + * This line segment is the one after the vertex with the same index. + */ + size_t from_segment; + + /*! + * The precise location of the source of the connection. + */ + Point from_point; + + /*! + * The polygon at the destination of the connection. + */ + Polygonal* to_poly; - coord_t getDistance2() + /*! + * The index of the line segment at the destination of the connection. + * + * This line segment is the one after the vertex with the same index. + */ + size_t to_segment; + + /*! + * The precise location of the destination of the connection. + */ + Point to_point; + + /*! + * Create a new connection. + * \param from_poly The polygon at the source of the connection. + * \param from_segment The index of the line segment at the source of + * the connection. + * \param from_point The precise location at the source of the + * connection. + * \param to_poly The polygon at the destination of the connection. + * \param to_segment The index of the line segment at the destination of + * the connection. + * \param to_point The precise location at the destination of the + * connection. + */ + PolygonConnection(Polygonal* from_poly, const size_t from_segment, const Point from_point, Polygonal* to_poly, const size_t to_segment, const Point to_point) + : from_poly(from_poly) + , from_segment(from_segment) + , from_point(from_point) + , to_poly(to_poly) + , to_segment(to_segment) + , to_point(to_point) { - return vSize2(to.p() - from.p()); + } + + /*! + * Get the squared length of the connection. + * + * The squared length is faster to compute than the real length. Compare + * it only with the squared maximum distance. + */ + coord_t getDistance2() const + { + return vSize2(from_point - to_point); } }; + /*! * Bridge to connect two polygons twice in order to make it into one polygon. * A bridge consists of two connections. @@ -106,51 +198,301 @@ protected: * * The resulting polygon will travel along the edges in a direction different from each other. */ + template<typename Polygonal> struct PolygonBridge { - PolygonConnection a; //!< first connection - PolygonConnection b; //!< second connection - PolygonBridge(PolygonConnection a, PolygonConnection b) + PolygonConnection<Polygonal> a; //!< first connection + PolygonConnection<Polygonal> b; //!< second connection + PolygonBridge(const PolygonConnection<Polygonal>& a, const PolygonConnection<Polygonal>& b) : a(a), b(b) {} }; - std::vector<PolygonBridge> all_bridges; //!< All bridges generated during any call to \ref PolygonConnector::connect(). This is just for keeping scores for debugging etc. + /*! + * Connect a group of polygonal objects - either polygons or paths. + * + * This function is generic and will work the same way with either data + * type. However it will call specialized functions, for instance to get the + * position of a vertex in the data. This reduces code duplication. + * \tparam The type of polygonal data to connect. + * \param to_connect The input polygonals that need to be connected. + * \return The connected polygonals. + */ + template<typename Polygonal> + std::vector<Polygonal> connectGroup(std::vector<Polygonal>& to_connect) + { + std::vector<Polygonal> result; + while(!to_connect.empty()) + { + if(to_connect.size() == 1) //Nothing to connect it to any more. + { + result.push_back(to_connect[0]); + break; + } + Polygonal current = std::move(to_connect.back()); + to_connect.pop_back(); + + if(!isClosed(current)) //Only bridge closed contours. + { + result.push_back(current); + continue; + } + std::optional<PolygonBridge<Polygonal>> bridge = getBridge(current, to_connect); + if(bridge) + { + connectPolygonsAlongBridge(*bridge, *bridge->a.to_poly); //Connect the polygons, and store the result in the to_poly. + //Don't store the current polygon. It has just been merged into the other one. + } + else //Can't connect this to anything. Leave it as-is. + { + result.push_back(current); + } + } + return result; + } /*! - * Connect the two polygons between which the bridge is computed. + * Get the position of a vertex, if the vertex is a point. + * + * This overload is simply the identity function. It will return the given + * vertex. This is a helper function to get the position of generic + * vertices. + * \param vertex The vertex to get the position of. + * \return The position of that vertex. */ - Polygon connectPolygonsAlongBridge(const PolygonBridge& bridge); + Point getPosition(const Point& vertex) const; /*! - * Add the segment from a polygon which is not removed by the bridge. - * - * This function gets called twice in order to connect two polygons together. - * - * Algorithm outline: - * Add the one vertex from the \p start, - * then add all vertices from the polygon in between - * and then add the polygon location from the \p end. - * - * \param[out] result Where to apend the new vertices to + * Get the position of a vertex, if the vertex is a junction. + * + * This is a helper function to get the position of generic vertices. + * \param vertex The vertex to get the position of. + * \return The position of that vertex. */ - void addPolygonSegment(const ClosestPolygonPoint& start, const ClosestPolygonPoint& end, PolygonRef result); + Point getPosition(const ExtrusionJunction& vertex) const; /*! - * Get the direction between the polygon locations \p from and \p to. - * This is intended to be the direction of the polygon segment of the short way around the polygon, not the long way around. - * - * The direction is positive for going in the same direction as the vertices are stored. - * E.g. if \p from is vertex 7 and \p to is vertex 8 then the direction is positive. - * Otherwise it is negative. - * - * \note \p from and \p to can also be points on the same segment, so their vertex index isn't everything to the algorithm. - * - * \note This function relies on some assumptions about the geometry of polygons you can encounter. - * It cannot be used as a general purpose function for any two ClosestPolygonPoint - * For large distances between \p from and \p to the output direction might be 'incorrect'. + * Get the width at a certain vertex. + * + * This is the overload that works for fixed-width polygons. They get their + * width from the width that was given at the constructor. + * \param vertex The vertex to get the width of. + * \return The line width of the polygon. */ - int16_t getPolygonDirection(const ClosestPolygonPoint& from, const ClosestPolygonPoint& to); + coord_t getWidth(const Point& vertex) const; + + /*! + * Get the width at a certain junction. + * + * This is the overload that works for variable-width polygons. The width is + * stored in the junction then. + * \param vertex The vertex to get the width of. + * \return The line width at that vertex. + */ + coord_t getWidth(const ExtrusionJunction& vertex) const; + + /*! + * Add a vertex at the end of the polygonal object. + * + * This is the overload for fixed-width polygons. The width will be ignored. + * \param polygonal The polygon to add a vertex to. + * \param position The position of the vertex to add. + * \param width The width of the vertex to add, ignored in this overload. + */ + void addVertex(Polygon& polygonal, const Point& position, const coord_t width) const; + + /*! + * Add a vertex at the end of the polygonal object. + * + * This is the overload for fixed-width polygons. + * \param polygonal The polygon to add a vertex to. + * \param vertex The vertex to add. + */ + void addVertex(Polygon& polygonal, const Point& vertex) const; + + /*! + * Add a vertex at the end of the polygonal object. + * + * This is the overload for variable-width paths. + * \param polygonal The variable-width path to add a vertex to. + * \param position The position of the vertex to add. + * \param width The width of the vertex to add. + */ + void addVertex(ExtrusionLine& polygonal, const Point& position, const coord_t width) const; + + /*! + * Add a vertex at the end of the polygonal object. + * + * This is the overload for variable-width paths. + * \param polygonal The variable-width path to add a vertex to. + * \param vertex The vertex to add. + */ + void addVertex(ExtrusionLine& polygonal, const ExtrusionJunction& vertex) const; + + /*! + * Tests whether this is a closed polygonal object, rather than a polyline. + * + * Polygons are always closed, so this overload will always return true. + * \param polygonal The polygonal object to check. + * \return ``true``, indicating that this polygon is closed. + */ + bool isClosed(Polygon& polygonal) const; + + /*! + * Tests whether this is a closed polygonal object, rather than a polyline. + * + * If the endpoints of the extrusion line meet, it is a closed shape. If + * not, it is open. + * \param polygonal The polygonal object to check. + * \return ``true`` if that shape is closed, or ``false`` otherwise. + */ + bool isClosed(ExtrusionLine& polygonal) const; + + /*! + * Construct an empty polygonal object, without any vertices. + * + * This is to be template-specialized for every type. The default + * constructor for some types creates some invalid data. + * \return An empty polygonal object. + */ + template<typename Polygonal> + Polygonal createEmpty() const + { + return Polygonal(); + } + + /*! + * Get the amount of space in between the polygons at the given connection. + * + * The space is the length of the connection, minus the width of the + * lines at the two endpoints. + * \param connection The connection to calculate the space of. + */ + template<typename Polygonal> + coord_t getSpace(const PolygonConnection<Polygonal>& connection) const + { + const coord_t from_width = interpolateWidth(connection.from_point, (*connection.from_poly)[connection.from_segment], (*connection.from_poly)[(connection.from_segment + 1) % connection.from_poly->size()]); + const coord_t to_width = interpolateWidth(connection.to_point, (*connection.to_poly)[connection.to_segment], (*connection.to_poly)[(connection.to_segment + 1) % connection.to_poly->size()]); + return vSize(connection.to_point - connection.from_point) - from_width / 2 - to_width / 2; + } + + /*! + * Get the local width at a certain position along a line segment. + * + * If the line segment has a variable width, the local line width will be + * interpolated between the two endpoints. + * \param position The position at which to get the line width. This should + * be (approximately) in between the position of the two vertices. + * \param a One of the vertices between which to interpolate. + * \param b The other vertex between which to interpolate. + */ + template<typename Vertex> + coord_t interpolateWidth(const Point position, Vertex a, Vertex b) const + { + const coord_t total_length = vSize(getPosition(a) - getPosition(b)); + if(total_length == 0) //Prevent division by 0 when the vertices are on top of each other. + { + return getWidth(a); //Just return one of them. They are on top of each other anyway. + } + const coord_t position_along_length = vSize(position - getPosition(a)); + return round_divide(getWidth(b) * position_along_length, total_length) + round_divide(getWidth(a) * (total_length - position_along_length), total_length); + } + + /*! + * Find the smallest connection between a polygon and a set of other + * candidate polygons to connect to. + */ + template<typename Polygonal> + std::optional<PolygonBridge<Polygonal>> findConnection(Polygonal& from_poly, std::vector<Polygonal>& to_polygons) + { + //Optimise for finding the best connection. + coord_t best_distance = line_width * max_gap; //Allow up to the max_gap. + std::optional<PolygonConnection<Polygonal>> best_connection; + std::optional<PolygonConnection<Polygonal>> best_second_connection; + + //The smallest connection will be from one of the vertices. So go through all of the vertices to find the closest place where they approach. + for(size_t poly_index = 0; poly_index < to_polygons.size(); ++poly_index) + { + if(!isClosed(to_polygons[poly_index])) + { + continue; + } + for(size_t to_index = 0; to_index < to_polygons[poly_index].size(); ++to_index) + { + const Point to_pos1 = getPosition(to_polygons[poly_index][to_index]); + const coord_t to_width1 = getWidth(to_polygons[poly_index][to_index]); + const Point to_pos2 = getPosition(to_polygons[poly_index][(to_index + 1) % to_polygons[poly_index].size()]); + const coord_t to_width2 = getWidth(to_polygons[poly_index][(to_index + 1) % to_polygons[poly_index].size()]); + const coord_t smallest_to_width = std::min(to_width1, to_width2); + + for(size_t from_index = 0; from_index < from_poly.size(); ++from_index) + { + const Point from_pos1 = getPosition(from_poly[from_index]); + const coord_t from_width1 = getWidth(from_poly[from_index]); + const Point from_pos2 = getPosition(from_poly[(from_index + 1) % from_poly.size()]); + const coord_t from_width2 = getWidth(from_poly[(from_index + 1) % from_poly.size()]); + const coord_t smallest_from_width = std::min(from_width1, from_width2); + + //Try a naive distance first. Faster to compute, but it may estimate the distance too small. + coord_t naive_dist = LinearAlg2D::getDistFromLine(from_pos1, to_pos1, to_pos2); + if(naive_dist - from_width1 - smallest_to_width < line_width * max_gap) + { + const Point closest_point = LinearAlg2D::getClosestOnLineSegment(from_pos1, to_pos1, to_pos2); + if(closest_point == to_pos2) //The last endpoint of a vertex is considered to be part of the next segment. Let that one handle it. + { + continue; + } + const coord_t width_at_closest = interpolateWidth(closest_point, to_polygons[poly_index][to_index], to_polygons[poly_index][(to_index + 1) % to_polygons[poly_index].size()]); + const coord_t distance = vSize(closest_point - from_pos1) - from_width1 - width_at_closest; //Actual, accurate distance to the other polygon. + if(distance < best_distance) + { + PolygonConnection<Polygonal> first_connection = PolygonConnection<Polygonal>(&from_poly, from_index, from_pos1, &to_polygons[poly_index], to_index, closest_point); + std::optional<PolygonConnection<Polygonal>> second_connection = getSecondConnection(first_connection, (width_at_closest + from_width1) / 2); + if(second_connection) //Second connection is also valid. + { + best_distance = distance; + best_connection = first_connection; + best_second_connection = second_connection; + } + } + } + + //Also try the other way around: From the line segment of the from_poly to a vertex in the to_polygons. + naive_dist = LinearAlg2D::getDistFromLine(to_pos1, from_pos1, from_pos2); + if(naive_dist - smallest_from_width - to_width1 < line_width * max_gap) + { + const Point closest_point = LinearAlg2D::getClosestOnLineSegment(to_pos1, from_pos1, from_pos2); + if(closest_point == from_pos2) //The last endpoint of a vertex is considered to be part of the next segment. Let that one handle it. + { + continue; + } + const coord_t width_at_closest = interpolateWidth(closest_point, from_poly[from_index], from_poly[(from_index + 1) % from_poly.size()]); + const coord_t distance = vSize(closest_point - to_pos1) - width_at_closest - to_width1; //Actual, accurate distance. + if(distance < best_distance) + { + PolygonConnection<Polygonal> first_connection = PolygonConnection<Polygonal>(&from_poly, from_index, closest_point, &to_polygons[poly_index], to_index, to_pos1); + std::optional<PolygonConnection<Polygonal>> second_connection = getSecondConnection(first_connection, (to_width1 + width_at_closest) / 2); + if(second_connection) //Second connection is also valid. + { + best_distance = distance; + best_connection = first_connection; + best_second_connection = second_connection; + } + } + } + } + } + } + + if(best_connection) + { + return PolygonBridge<Polygonal>(*best_connection, *best_second_connection); + } + else + { + return std::nullopt; + } + } /*! * Get the bridge to cross between two polygons. @@ -166,7 +508,88 @@ protected: * - the first connection at a whole line distance away * So as to try and find a bridge which is centered around the initiall found first connection */ - std::optional<PolygonBridge> getBridge(ConstPolygonRef poly, std::vector<Polygon>& polygons); + template<typename Polygonal> + std::optional<PolygonBridge<Polygonal>> getBridge(Polygonal& from_poly, std::vector<Polygonal>& to_polygons) + { + std::optional<PolygonBridge<Polygonal>> connection = findConnection(from_poly, to_polygons); + if(!connection) //We didn't find a connection. No bridge. + { + return std::nullopt; + } + + //Ensure that B is always the right connection and A the left. + if(LinearAlg2D::pointIsLeftOfLine(connection->b.from_point, connection->a.from_point, connection->a.to_point) > 0) + { + std::swap(connection->a, connection->b); + } + return connection; + } + + /*! + * Walk along a polygon to find the first point that is exactly ``distance`` + * away from a given line. + * + * The resulting point does not have to be exactly on a vertex. Most likely + * it will be on a line segment. + * \param poly The polygonal shape along which to walk. + * \param start_index The vertex at which to start looking. This vertex + * should be on the wrong side of the line. + * \param distance The distance from the line at which the resulting point + * should be. + * \param line_a The line passes through this point. + * \param line_b The line also passes through this point. + * \param direction Use +1 to iterate in the forward direction through the + * polygon, or -1 to iterate backwards. + * \return If there is a point that is the correct distance from the line, + * the first such point is returned, and the segment index that it's on. If + * the polygon is entirely close to the line, returns + * ``std::nullopt``. + */ + template<typename Polygonal> + std::optional<std::pair<Point, size_t>> walkUntilDistanceFromLine(const Polygonal& poly, const size_t start_index, const coord_t distance, const Point& line_a, const Point& line_b, const short direction) + { + const size_t poly_size = poly.size(); + const coord_t line_magnitude = vSize(line_b - line_a); //Pre-compute, used for line distance calculation. + if(line_magnitude == 0) + { + return std::nullopt; //Line doesn't have a direction, so we can't be on any one side of it. + } + + for(size_t index = (start_index + direction + poly_size) % poly_size; index != start_index; index = (index + direction + poly_size) % poly_size) + { + const Point vertex_pos = getPosition(poly[index]); + const coord_t vertex_distance = cross(line_a - line_b, line_a - vertex_pos) / line_magnitude; //Signed distance! + if(std::abs(vertex_distance) >= distance) //Further away from the line than the threshold. + { + //Interpolate over that last line segment to find the point at exactly the right distance. + const size_t previous_index = (index - direction + poly_size) % poly_size; + const Point previous_pos = getPosition(poly[previous_index]); + const coord_t previous_distance = cross(line_a - line_b, line_a - previous_pos) / line_magnitude; + if(previous_distance == vertex_distance) //0-length line segment, or parallel to line. + { + continue; + } + const double interpolation_pos = double(distance - previous_distance) / (vertex_distance - previous_distance); + const double interpolation_neg = double(-distance - previous_distance) / (vertex_distance - previous_distance); + double interpolation; + if(interpolation_pos >= 0 && interpolation_pos < 1) + { + interpolation = interpolation_pos; + } + else if(interpolation_neg >= 0 && interpolation_neg < 1) + { + interpolation = interpolation_neg; + } + else + { + continue; + } + const Point interpolated_point = previous_pos + (vertex_pos - previous_pos) * interpolation; + return std::make_pair(interpolated_point, (direction == +1) ? previous_index : index); //Choose the "earlier" index of the two, regardless of direction. + } + } + return std::nullopt; //None of the vertices were far enough away from the line. + } /*! * Get a connection parallel to a given \p first connection at an orthogonal distance line_width from the \p first connection. @@ -179,7 +602,139 @@ protected: * - check whether they are both on the same side of the \p first connection * - choose the connection which woukd form the smalles bridge */ - std::optional<PolygonConnection> getSecondConnection(PolygonConnection& first); + template<typename Polygonal> + std::optional<PolygonConnection<Polygonal>> getSecondConnection(PolygonConnection<Polygonal>& first, const coord_t adjacent_distance) + { + std::optional<PolygonConnection<Polygonal>> result = std::nullopt; + coord_t best_connection_length = std::numeric_limits<coord_t>::max(); + + //Find the four intersections, on both sides of the initial connection, and on both polygons. + std::optional<std::pair<Point, size_t>> from_forward_intersection = walkUntilDistanceFromLine(*first.from_poly, first.from_segment, adjacent_distance, first.from_point, first.to_point, +1); + std::optional<std::pair<Point, size_t>> from_backward_intersection = walkUntilDistanceFromLine(*first.from_poly, first.from_segment, adjacent_distance, first.from_point, first.to_point, -1); + std::optional<std::pair<Point, size_t>> to_forward_intersection = walkUntilDistanceFromLine(*first.to_poly, first.to_segment, adjacent_distance, first.from_point, first.to_point, +1); + std::optional<std::pair<Point, size_t>> to_backward_intersection = walkUntilDistanceFromLine(*first.to_poly, first.to_segment, adjacent_distance, first.from_point, first.to_point, -1); + + for(const std::optional<std::pair<Point, size_t>>& from_intersection : {from_forward_intersection, from_backward_intersection}) + { + if(!from_intersection) + { + continue; + } + //Find the shortest of the connections in the to_poly. + const bool original_side = LinearAlg2D::pointIsLeftOfLine(first.to_point, first.from_point, from_intersection->first) > 0; + for(const std::optional<std::pair<Point, size_t>>& to_intersection : {to_forward_intersection, to_backward_intersection}) + { + if(!to_intersection) + { + continue; + } + const bool current_side = LinearAlg2D::pointIsLeftOfLine(to_intersection->first, first.from_point, from_intersection->first) > 0; + if (original_side != current_side) + { + continue; + } + PolygonConnection<Polygonal> connection(first.from_poly, from_intersection->second, from_intersection->first, first.to_poly, to_intersection->second, to_intersection->first); + const coord_t connection_length = getSpace(connection); + if(connection_length < max_gap * line_width && connection_length < best_connection_length) //Connection is allowed. + { + result = connection; + best_connection_length = connection_length; + } + } + } + return result; + } + + template<typename Polygonal> + void connectPolygonsAlongBridge(const PolygonBridge<Polygonal>& bridge, Polygonal& result) + { + //We'll traverse the following path: + // + // <<<<<<X......X<<<<<<< to_poly + // ^ v + // ^ v + // ^ a b v bridge + // ^ v + // >>>>>>X......X>>>>>>> from_poly + // + //To do this, from_poly and to_poly might need to be traversed in reverse order. This function figures all of that out. + + Polygonal ret = createEmpty<Polygonal>(); //Create a temporary result that we'll move into the result. + + const size_t from_size = bridge.b.from_poly->size(); + //Add the from-endpoint of B. + const coord_t b_from_width = interpolateWidth(bridge.b.from_point, (*bridge.b.from_poly)[bridge.b.from_segment], (*bridge.b.from_poly)[(bridge.b.from_segment + 1) % from_size]); + addVertex(ret, bridge.b.from_point, b_from_width); + + //Add the from-polygonal from B to A. + short forwards; + if(bridge.a.from_segment == bridge.b.from_segment) //If we start and end on the same segment, iterate in the direction from A to B. + { + const Point vertex = getPosition((*bridge.b.from_poly)[bridge.b.from_segment]); //Same vertex for A and B. + const Point next_vertex = getPosition((*bridge.b.from_poly)[(bridge.b.from_segment + 1) % from_size]); + const Point direction = next_vertex - vertex; //Direction we'd go into when forward iterating. + const Point a_to_b = bridge.b.from_point - bridge.a.from_point; + forwards = vSize2(direction - a_to_b) < vSize2(-direction - a_to_b); + } + else + { + //If not the same segment, traverse in whichever direction is the long way around. + forwards = ((bridge.b.from_segment + from_size - bridge.a.from_segment) % from_size) < ((bridge.a.from_segment + from_size - bridge.b.from_segment) % from_size); + } + size_t first_segment = forwards ? (bridge.b.from_segment + 1) % from_size : (bridge.b.from_segment + from_size) % from_size; + size_t last_segment = forwards ? bridge.a.from_segment : bridge.a.from_segment; + if(first_segment == last_segment) last_segment = (last_segment + from_size - 2 * forwards + 1) % from_size; + size_t i = first_segment; + do //Since we might start and end on the same segment, do a do_while loop to iterate at least once. + { + addVertex(ret, (*bridge.b.from_poly)[i]); + i = (i + 2 * forwards - 1 + from_size) % from_size; + } + while(i != (last_segment + from_size + 2 * forwards - 1) % from_size); + + //Add the from-endpoint of A. + const coord_t a_from_width = interpolateWidth(bridge.a.from_point, (*bridge.b.from_poly)[bridge.a.from_segment], (*bridge.b.from_poly)[(bridge.a.from_segment + 1) % from_size]); + addVertex(ret, bridge.a.from_point, a_from_width); + + const size_t to_size = bridge.b.to_poly->size(); + //Add the to-endpoint of A. + const coord_t a_to_width = interpolateWidth(bridge.a.to_point, (*bridge.a.to_poly)[bridge.a.to_segment], (*bridge.a.to_poly)[(bridge.a.to_segment + 1) % to_size]); + addVertex(ret, bridge.a.to_point, a_to_width); + + //Add the to_polygonal from A to B. + if(bridge.a.to_segment == bridge.b.to_segment) + { + const Point vertex = getPosition((*bridge.b.to_poly)[bridge.b.to_segment]); //Same vertex for A and B. + const Point next_vertex = getPosition((*bridge.b.to_poly)[(bridge.b.to_segment + 1) % to_size]); + const Point direction = next_vertex - vertex; + const Point a_to_b = bridge.b.to_point - bridge.a.to_point; + forwards = vSize2(direction - a_to_b) > vSize2(-direction - a_to_b); + } + else + { + forwards = ((bridge.a.to_segment + to_size - bridge.b.to_segment) % to_size) < ((bridge.b.to_segment + to_size - bridge.a.to_segment) % to_size); + } + first_segment = forwards ? (bridge.a.to_segment + 1) % to_size : bridge.a.to_segment; + size_t end_segment = forwards ? (bridge.b.to_segment + 1) % to_size : bridge.b.to_segment; + i = first_segment; + do + { + addVertex(ret, (*bridge.b.to_poly)[i]); + i = (i + 2 * forwards - 1 + to_size) % to_size; + } + while(i != end_segment); + + //Add the to-endpoint of B. + const coord_t b_to_width = interpolateWidth(bridge.b.to_point, (*bridge.b.to_poly)[bridge.b.to_segment], (*bridge.b.to_poly)[(bridge.b.to_segment + 1) % to_size]); + addVertex(ret, bridge.b.to_point, b_to_width); + + if(getPosition(ret.back()) != getPosition(ret.front())) + { + addVertex(ret, getPosition(ret.front()), getWidth(ret.front())); + } + + result = std::move(ret); //Override the result with the new combined shape. + } }; diff --git a/src/utils/PolylineStitcher.cpp b/src/utils/PolylineStitcher.cpp index be71cee92..8353d41b8 100644 --- a/src/utils/PolylineStitcher.cpp +++ b/src/utils/PolylineStitcher.cpp @@ -40,5 +40,17 @@ bool PolylineStitcher<Polygons, Polygon, Point>::canConnect(const Polygon&, cons return true; } +template<> +bool PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::isOdd(const ExtrusionLine& line) +{ + return line.is_odd; +} + +template<> +bool PolylineStitcher<Polygons, Polygon, Point>::isOdd(const Polygon&) +{ + return false; +} + }//namespace cura diff --git a/src/utils/PolylineStitcher.h b/src/utils/PolylineStitcher.h index 693c22a44..5f03a0bda 100644 --- a/src/utils/PolylineStitcher.h +++ b/src/utils/PolylineStitcher.h @@ -23,17 +23,34 @@ class PolylineStitcher { public: /*! - * Stitch together the separate \p lines into \p result_lines and if they can be closed into \p result_polygons. - * Only introduce new segments shorter than \p max_stitch_distance, and larger then \p snap_distance - * but always try to take the shortest connection possible. + * Stitch together the separate \p lines into \p result_lines and if they + * can be closed into \p result_polygons. + * + * Only introduce new segments shorter than \p max_stitch_distance, and + * larger than \p snap_distance but always try to take the shortest + * connection possible. * - * Only stitch polylines into closed polygons if they are larger than 3 * \p max_stitch_distance, - * in order to prevent small segments to accidentally get closed into a polygon. + * Only stitch polylines into closed polygons if they are larger than 3 * + * \p max_stitch_distance, in order to prevent small segments to + * accidentally get closed into a polygon. * - * \warning Tiny polylines (smaller than 3 * max_stitch_distance) will not be closed into polygons. + * \warning Tiny polylines (smaller than 3 * max_stitch_distance) will not + * be closed into polygons. * - * \note Resulting polylines and polygons are added onto the existing containers, so you can directly output onto a polygons container with existing polygons in it. - * However, you shouldn't call this function with the same parameter in \p lines as \p result_lines, because that would duplicate (some of) the polylines. + * \note Resulting polylines and polygons are added onto the existing + * containers, so you can directly output onto a polygons container with + * existing polygons in it. However, you shouldn't call this function with + * the same parameter in \p lines as \p result_lines, because that would + * duplicate (some of) the polylines. + * \param lines The lines to stitch together. + * \param result_lines[out] The stitched parts that are not closed polygons + * will be stored in here. + * \param result_polygons[out] The stitched parts that were closed as + * polygons will be stored in here. + * \param max_stitch_distance The maximum distance that will be bridged to + * connect two lines. + * \param snap_distance Points closer than this distance are considered to + * be the same point. */ static void stitch(const Paths& lines, Paths& result_lines, Paths& result_polygons, coord_t max_stitch_distance = MM2INT(0.1), coord_t snap_distance = 10) { @@ -62,6 +79,7 @@ public: } processed[line_idx] = true; const auto line = lines[line_idx]; + bool should_close = isOdd(line); Path chain = line; bool closest_is_closing_polygon = false; @@ -71,7 +89,8 @@ public: { // try extending chain in the other direction chain.reverse(); } - + coord_t chain_length = chain.polylineLength(); + while (true) { Point from = make_point(chain.back()); @@ -80,7 +99,7 @@ public: coord_t closest_distance = std::numeric_limits<coord_t>::max(); grid.processNearby(from, max_stitch_distance, std::function<bool (const PathsPointIndex<Paths>&)> ( - [from, &chain, &closest, &closest_is_closing_polygon, &closest_distance, &processed, go_in_reverse_direction, max_stitch_distance, snap_distance] + [from, &chain, &closest, &closest_is_closing_polygon, &closest_distance, &processed, &chain_length, go_in_reverse_direction, max_stitch_distance, snap_distance, should_close] (const PathsPointIndex<Paths>& nearby)->bool { bool is_closing_segment = false; @@ -91,15 +110,23 @@ public: } if(vSize2(nearby.p() - make_point(chain.front())) < snap_distance * snap_distance) { - if (chain.polylineLength() + dist < 3 * max_stitch_distance // prevent closing of small poly, cause it might be able to continue making a larger polyline + if (chain_length + dist < 3 * max_stitch_distance // prevent closing of small poly, cause it might be able to continue making a larger polyline || chain.size() <= 2) // don't make 2 vert polygons { return true; // look for a better next line } is_closing_segment = true; - dist += 10; // prefer continuing polyline over closing a polygon; avoids closed zigzags from being printed separately - // continue to see if closing segment is also the closest - // there might be a segment smaller than [max_stitch_distance] which closes the polygon better + if(!should_close) + { + dist += 10; // prefer continuing polyline over closing a polygon; avoids closed zigzags from being printed separately + // continue to see if closing segment is also the closest + // there might be a segment smaller than [max_stitch_distance] which closes the polygon better + } + else + { + dist -= 10; //Prefer closing the polygon if it's 100% even lines. Used to create closed contours. + //Continue to see if closing segment is also the closest. + } } else if (processed[nearby.poly_idx]) { // it was already moved to output @@ -139,6 +166,7 @@ public: coord_t segment_dist = vSize(make_point(chain.back()) - closest.p()); assert(segment_dist <= max_stitch_distance + 10); + const size_t old_size = chain.size(); if (closest.point_idx == 0) { auto start_pos = (*closest.polygons)[closest.poly_idx].begin(); @@ -157,6 +185,11 @@ public: } chain.insert(chain.end(), (*closest.polygons)[closest.poly_idx].rbegin(), (*closest.polygons)[closest.poly_idx].rend()); } + for(size_t i = old_size; i < chain.size(); ++i) //Update chain length. + { + chain_length += vSize(chain[i] - chain[i - 1]); + } + should_close = should_close & !isOdd((*closest.polygons)[closest.poly_idx]); //If we connect an even to an odd line, we should no longer try to close it. assert( ! processed[closest.poly_idx]); processed[closest.poly_idx] = true; } @@ -199,6 +232,8 @@ public: * (Not true for an odd and an even wall.) */ static bool canConnect(const Path& a, const Path& b); + + static bool isOdd(const Path& line); }; }//namespace cura diff --git a/src/utils/SVG.cpp b/src/utils/SVG.cpp index dd876bead..045f0cd36 100644 --- a/src/utils/SVG.cpp +++ b/src/utils/SVG.cpp @@ -24,6 +24,7 @@ std::string SVG::toString(Color color) const case SVG::Color::RED: return "red"; case SVG::Color::BLUE: return "blue"; case SVG::Color::GREEN: return "green"; + case SVG::Color::LIME: return "lime"; case SVG::Color::ORANGE: return "orange"; case SVG::Color::MAGENTA: return "magenta"; case SVG::Color::YELLOW: return "yellow"; @@ -332,7 +333,7 @@ void SVG::writePolyline(ConstPolygonRef poly, const ColorObject color, const flo } } -void SVG::writePaths(const VariableWidthPaths& paths, const ColorObject color, const float width_factor) const +void SVG::writePaths(const std::vector<VariableWidthLines>& paths, const ColorObject color, const float width_factor) const { for(const VariableWidthLines& lines : paths) { diff --git a/src/utils/SVG.h b/src/utils/SVG.h index 1f6dbfb79..c4418ef86 100644 --- a/src/utils/SVG.h +++ b/src/utils/SVG.h @@ -26,6 +26,7 @@ public: RED, BLUE, GREEN, + LIME, ORANGE, MAGENTA, YELLOW, @@ -152,7 +153,7 @@ public: * \param color The color to draw the paths with. * \param width_factor A multiplicative factor on the line widths. */ - void writePaths(const VariableWidthPaths& paths, const ColorObject color = Color::BLACK, const float width_factor = 1.0) const; + void writePaths(const std::vector<VariableWidthLines>& paths, const ColorObject color = Color::BLACK, const float width_factor = 1.0) const; /*! * Draw variable-width lines into the image. diff --git a/src/utils/math.h b/src/utils/math.h index 6909b01ba..867cc6cad 100644 --- a/src/utils/math.h +++ b/src/utils/math.h @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #ifndef UTILS_MATH_H diff --git a/src/utils/polygonUtils.cpp b/src/utils/polygonUtils.cpp index 52fdb14e2..4fbecd193 100644 --- a/src/utils/polygonUtils.cpp +++ b/src/utils/polygonUtils.cpp @@ -87,7 +87,7 @@ void PolygonUtils::spreadDots(PolygonsPointIndex start, PolygonsPointIndex end, std::vector<Point> PolygonUtils::spreadDotsArea(const Polygons& polygons, coord_t grid_size) { - VariableWidthPaths dummy_toolpaths; + std::vector<VariableWidthLines> dummy_toolpaths; Settings dummy_settings; Infill infill_gen(EFillMethod::LINES, false, false, polygons, 0, grid_size, 0, 1, 0, 0, 0, 0, 0); Polygons result_polygons; @@ -687,94 +687,6 @@ ClosestPolygonPoint PolygonUtils::ensureInsideOrOutside(const Polygons& polygons } } - - -std::pair<ClosestPolygonPoint, ClosestPolygonPoint> PolygonUtils::findConnection(ConstPolygonRef poly1, Polygons& polys2, coord_t min_connection_length, coord_t max_connection_length, std::function<bool (std::pair<ClosestPolygonPoint, ClosestPolygonPoint>)> precondition) -{ - ClosestPolygonPoint invalid; - std::pair<ClosestPolygonPoint, ClosestPolygonPoint> ret = std::make_pair(invalid, invalid); - if (poly1.empty() || polys2.empty()) - { - return ret; - } - - const coord_t min_connection_dist2 = min_connection_length * min_connection_length; - const coord_t max_connection_dist2 = max_connection_length * max_connection_length; - - auto grid = PolygonUtils::createLocToLineGrid(polys2, max_connection_length); - - - std::unordered_set<std::pair<size_t, PolygonsPointIndex>> checked_segment_pairs; // pairs of index into segment start on poly1 and PolygonsPointIndex to segment start on polys2 - - for (size_t point_idx = 0; point_idx < poly1.size(); point_idx++) - { - std::function<bool (const PolygonsPointIndex&)> process_elem_func = - [&, point_idx](const PolygonsPointIndex& line_from) - { - std::pair<size_t, PolygonsPointIndex> segment_pair = std::make_pair(point_idx, line_from); - if (checked_segment_pairs.count(segment_pair) > 0) - { // these two line segments were already checked - return true; // continue looking for connections - } - - Point a1 = poly1[point_idx]; - Point a2 = poly1[(point_idx + 1) % poly1.size()]; - Point b1 = line_from.p(); - Point b2 = line_from.next().p(); - std::pair<Point, Point> connection = LinearAlg2D::getClosestConnection(a1, a2, b1, b2); - coord_t dist2 = vSize2(connection.first - connection.second); - ret = std::make_pair( - ClosestPolygonPoint(connection.first, point_idx, poly1), - ClosestPolygonPoint(connection.second, line_from.point_idx, polys2[line_from.poly_idx], line_from.poly_idx)); - if (min_connection_dist2 < dist2 && dist2 < max_connection_dist2 - && precondition(ret)) - { - return false; // stop the search; break the for-loop - } - - checked_segment_pairs.emplace(point_idx, line_from); - return true; // continue looking for connections - }; - - std::pair<Point, Point> line = std::make_pair(poly1[point_idx], poly1[(point_idx + 1) % poly1.size()]); - Point normal_vector = normal(turn90CCW(line.second - line.first), max_connection_length); - std::pair<Point, Point> line2 = std::make_pair(line.first + normal_vector, line.second + normal_vector); // for neighborhood around the line - std::pair<Point, Point> line3 = std::make_pair(line.first - normal_vector, line.second - normal_vector); // for neighborhood around the line - - bool continue_; - continue_ = grid->processLine(line, process_elem_func); - if (!continue_) break; - continue_ = grid->processLine(line2, process_elem_func); - if (!continue_) break; - continue_ = grid->processLine(line3, process_elem_func); - if (!continue_) break; - } - ret.first.poly_idx = 0; - return ret; -} - -void PolygonUtils::findSmallestConnection(ClosestPolygonPoint& poly1_result, ClosestPolygonPoint& poly2_result) -{ - if (!poly1_result.poly || !poly2_result.poly) - { - return; - } - ConstPolygonRef poly1 = *poly1_result.poly; - ConstPolygonRef poly2 = *poly2_result.poly; - if (poly1.size() == 0 || poly2.size() == 0) - { - return; - } - - Point center1 = poly1[0]; - ClosestPolygonPoint intermediate_poly2_result = findClosest(center1, poly2); - ClosestPolygonPoint intermediate_poly1_result = findClosest(intermediate_poly2_result.p(), poly1); - - poly2_result = findClosest(intermediate_poly1_result.p(), poly2); - poly1_result = findClosest(poly2_result.p(), poly1); - walkToNearestSmallestConnection(poly1_result, poly2_result); -} - void PolygonUtils::walkToNearestSmallestConnection(ClosestPolygonPoint& poly1_result, ClosestPolygonPoint& poly2_result) { if (!poly1_result.isValid() || !poly2_result.isValid()) @@ -1510,14 +1422,14 @@ void PolygonUtils::fixSelfIntersections(const coord_t epsilon, Polygons& thiss) return; } - const coord_t half_epsilon = (epsilon + 1) / 2; + const coord_t half_epsilon = std::max(10LL, (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 = 2000; auto query_grid = PolygonUtils::createLocToLineGrid(thiss, grid_size); - const coord_t move_dist = std::max(2LL, half_epsilon - 2); + const coord_t move_dist = half_epsilon - 2; const coord_t half_epsilon_sqrd = half_epsilon * half_epsilon; const size_t n = thiss.size(); @@ -1527,7 +1439,7 @@ void PolygonUtils::fixSelfIntersections(const coord_t epsilon, Polygons& thiss) 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)) + for (const auto& line : query_grid->getNearby(pt, epsilon * 2)) { 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)) diff --git a/src/utils/polygonUtils.h b/src/utils/polygonUtils.h index ef32a7344..489130bea 100644 --- a/src/utils/polygonUtils.h +++ b/src/utils/polygonUtils.h @@ -323,33 +323,6 @@ public: static ClosestPolygonPoint ensureInsideOrOutside(const Polygons& polygons, Point& from, const ClosestPolygonPoint& closest_polygon_point, int preferred_dist_inside, const Polygons* loc_to_line_polygons = nullptr, const LocToLineGrid* loc_to_line_grid = nullptr, const std::function<int(Point)>& penalty_function = no_penalty_function); /*! - * Find a connecting line segment from one polygon to a collection of other polygons. - * - * This implementation uses a sparse grid to get to an accurate result quickly - * - * The first connection larger than \p min_connection_length and smaller than \p max_connection_length is returned. - * - * \param poly1 The polygon in which to search for a conection - * \param polys2 The polygons to which to connect - * \param min_connection_length The minimal conection length a connection needs to have in order to stop looking for other connections - * \param max_connection_length The largest length of a connection to be found - */ - static std::pair<ClosestPolygonPoint, ClosestPolygonPoint> findConnection(ConstPolygonRef poly1, Polygons& polys2, coord_t min_connection_length, coord_t max_connection_length, std::function<bool (std::pair<ClosestPolygonPoint, ClosestPolygonPoint>)> precondition); - - /*! - * Find the two points in two polygons with the smallest distance. - * - * The final connection will be close to the center of mass of the first polygon. - * - * \warning The ClosestPolygonPoint::poly fields output parameters should be initialized with the polygons for which to find the smallest connection. - * - * \param poly1_result Output parameter: the point at the one end of the smallest connection between its poly and \p poly2_result.poly. - * \param poly2_result Output parameter: the point at the other end of the smallest connection between its poly and \p poly1_result.poly. - * \param sample_size The number of points on each polygon to start the hill climbing search from. Use negative values for checking all combinations of points. - */ - static void findSmallestConnection(ClosestPolygonPoint& poly1_result, ClosestPolygonPoint& poly2_result); - - /*! * * \warning Assumes \p poly1_result and \p poly2_result have their pos and poly fields initialized! */ diff --git a/tests/ExtruderPlanTest.cpp b/tests/ExtruderPlanTest.cpp index a8cca147a..e5b9b72d2 100644 --- a/tests/ExtruderPlanTest.cpp +++ b/tests/ExtruderPlanTest.cpp @@ -86,9 +86,10 @@ public: { const std::string mesh_id = "test_mesh"; constexpr Ratio flow_1 = 1.0_r; + constexpr Ratio width_1 = 1.0_r; constexpr bool no_spiralize = false; constexpr Ratio speed_1 = 1.0_r; - square.assign({GCodePath(extrusion_config, mesh_id, SpaceFillType::PolyLines, flow_1, no_spiralize, speed_1)}); + square.assign({GCodePath(extrusion_config, mesh_id, SpaceFillType::PolyLines, flow_1, width_1, no_spiralize, speed_1)}); square.back().points = { Point(0, 0), Point(1000, 0), @@ -98,11 +99,11 @@ public: }; lines.assign({ - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1) + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1) }); lines[0].points = {Point(0, 0), Point(1000, 0)}; lines[1].points = {Point(1000, 0), Point(1000, 400)}; @@ -114,11 +115,11 @@ public: constexpr Ratio flow_08 = 0.8_r; constexpr Ratio flow_04 = 0.4_r; decreasing_flow.assign({ - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_12, no_spiralize, speed_1), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_08, no_spiralize, speed_1), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_04, no_spiralize, speed_1) + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_12, width_1, no_spiralize, speed_1), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1 , width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_08, width_1, no_spiralize, speed_1), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1 , width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_04, width_1, no_spiralize, speed_1) }); decreasing_flow[0].points = {Point(0, 0), Point(1000, 0)}; decreasing_flow[1].points = {Point(1000, 0), Point(1000, 400)}; @@ -130,11 +131,11 @@ public: constexpr Ratio speed_08 = 0.8_r; constexpr Ratio speed_04 = 0.4_r; decreasing_speed.assign({ - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_12), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_08), - GCodePath(travel_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_04) + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_12), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_08), + GCodePath(travel_config , mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_04) }); decreasing_speed[0].points = {Point(0, 0), Point(1000, 0)}; decreasing_speed[1].points = {Point(1000, 0), Point(1000, 400)}; @@ -143,12 +144,12 @@ public: decreasing_speed[4].points = {Point(0, 800), Point(1000, 800)}; variable_width.assign({ - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, 0.8_r, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, 0.6_r, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, 0.4_r, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, 0.2_r, no_spiralize, speed_1), - GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, 0.0_r, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, width_1, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, 0.8_r, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, 0.6_r, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, 0.4_r, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, 0.2_r, no_spiralize, speed_1), + GCodePath(extrusion_config, mesh_id, SpaceFillType::Lines, flow_1, 0.0_r, no_spiralize, speed_1), }); variable_width[0].points = {Point(0, 0), Point(1000, 0)}; variable_width[1].points = {Point(1000, 0), Point(2000, 0)}; @@ -194,18 +195,20 @@ public: {} /*! - * Helper method to calculate the flow rate of a path in mm3 per second, ignoring any user specified speed alteration other than the back pressure compensation. + * Helper method to calculate the flow rate of a path in mm3 per second, + * minus the influence of flow rate and ignoring any user specified speed + * alteration other than the back pressure compensation. * \param path The path to calculate the flow rate of. - * \return The flow rate, in cubic millimeters per second (ignoring any user specified speed alteration other than back-pressure compensation). + * \return The flow rate, in cubic millimeters per second. */ - double calculatePathFlow(const GCodePath& path) + double calculatePathWidth(const GCodePath& path) { - return path.getExtrusionMM3perMM() * path.config->getSpeed() * path.speed_back_pressure_factor; + return path.getExtrusionMM3perMM() / path.config->getFlowRatio() / path.flow * path.config->getSpeed() * path.speed_back_pressure_factor; } bool shouldCountPath(const GCodePath& path) const { - return path.flow > 0.0 && path.config->getFlowRatio() > 0.0 && path.config->getLineWidth() > 0.0 && ! path.config->isTravelPath() && ! path.config->isBridgePath(); + return path.flow > 0.0 && path.width_factor > 0.0 && path.config->getFlowRatio() > 0.0 && path.config->getLineWidth() > 0.0 && ! path.config->isTravelPath() && ! path.config->isBridgePath(); } }; @@ -250,20 +253,20 @@ public: TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationZeroIsUncompensated) { extruder_plan.paths = GetParam(); - std::vector<Ratio> original_flows; + std::vector<Ratio> original_widths; std::vector<Ratio> original_speeds; for(const GCodePath& path : extruder_plan.paths) { - original_flows.push_back(path.flow); + original_widths.push_back(path.width_factor); original_speeds.push_back(path.speed_factor); } extruder_plan.applyBackPressureCompensation(0.0_r); - ASSERT_EQ(extruder_plan.paths.size(), original_flows.size()) << "Number of paths may not have changed."; + ASSERT_EQ(extruder_plan.paths.size(), original_widths.size()) << "Number of paths may not have changed."; for(size_t i = 0; i < extruder_plan.paths.size(); ++i) { - EXPECT_NEAR(original_flows[i], extruder_plan.paths[i].flow, error_margin) << "The flow rate did not change. Back pressure compensation doesn't adjust flow."; + EXPECT_NEAR(original_widths[i], extruder_plan.paths[i].width_factor, error_margin) << "The width did not change. Back pressure compensation doesn't adjust line width."; EXPECT_NEAR(original_speeds[i], extruder_plan.paths[i].speed_factor, error_margin) << "The speed factor did not change, since the compensation factor was 0."; } } @@ -285,7 +288,7 @@ TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationFull) return; } //All flow rates must be equal to this one. - const double first_flow_mm3_per_sec = calculatePathFlow(*first_extrusion); + const double first_flow_mm3_per_sec = calculatePathWidth(*first_extrusion); for(GCodePath& path : extruder_plan.paths) { @@ -293,7 +296,7 @@ TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationFull) { continue; //Ignore travel moves. } - const double flow_mm3_per_sec = calculatePathFlow(path); + const double flow_mm3_per_sec = calculatePathWidth(path); EXPECT_NEAR(flow_mm3_per_sec, first_flow_mm3_per_sec, error_margin) << "Every path must have a flow rate equal to the first, since the flow changes were completely compensated for."; } } @@ -313,7 +316,7 @@ TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationHalf) { continue; //Ignore travel moves. } - original_flows.push_back(calculatePathFlow(path)); + original_flows.push_back(calculatePathWidth(path)); } const double original_average = std::accumulate(original_flows.begin(), original_flows.end(), 0.0) / original_flows.size(); @@ -328,7 +331,7 @@ TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationHalf) { continue; //Ignore travel moves. } - new_flows.push_back(calculatePathFlow(path)); + new_flows.push_back(calculatePathWidth(path)); } const double new_average = std::accumulate(new_flows.begin(), new_flows.end(), 0.0) / new_flows.size(); //Note that the new average doesn't necessarily need to be the same average! It is most likely a higher average in real-world scenarios. @@ -337,7 +340,7 @@ TEST_P(ExtruderPlanPathsParameterizedTest, BackPressureCompensationHalf) ASSERT_EQ(original_flows.size(), new_flows.size()) << "We need to have the same number of extrusion moves."; for(size_t i = 0; i < new_flows.size(); ++i) { - EXPECT_NEAR((original_flows[i] - original_average) / 2.0, new_flows[i] - new_average, error_margin); + EXPECT_NEAR((original_flows[i] - original_average) / 2.0, new_flows[i] - new_average, error_margin) << "The differences in flow rate needs to be approximately halved, within margin of rounding errors."; } } diff --git a/tests/InfillTest.cpp b/tests/InfillTest.cpp index cde98a939..23b083f5e 100644 --- a/tests/InfillTest.cpp +++ b/tests/InfillTest.cpp @@ -172,7 +172,7 @@ namespace cura ); // There are some optional parameters, but these will do for now (future improvement?). Settings infill_settings; - VariableWidthPaths result_paths; + std::vector<VariableWidthLines> result_paths; Polygons result_polygons; Polygons result_lines; infill.generate(result_paths, result_polygons, result_lines, infill_settings, nullptr, nullptr); diff --git a/tests/PathOrderMonotonicTest.cpp b/tests/PathOrderMonotonicTest.cpp index a1dca462b..b57107aea 100644 --- a/tests/PathOrderMonotonicTest.cpp +++ b/tests/PathOrderMonotonicTest.cpp @@ -27,22 +27,22 @@ namespace cura class PathOrderMonotonicTest : public testing::TestWithParam<std::tuple<std::string, AngleRadians>> {}; - inline Point startVertex(const PathOrderMonotonic<ConstPolygonRef>::Path& path) + inline Point startVertex(const PathOrderPath<ConstPolygonPointer>& path) { - return path.vertices[path.start_vertex]; + return (*path.vertices)[path.start_vertex]; } - inline Point endVertex(const PathOrderMonotonic<ConstPolygonRef>::Path& path) + inline Point endVertex(const PathOrderPath<ConstPolygonPointer>& path) { - return path.vertices[path.vertices.size() - (1 + path.start_vertex)]; + return (*path.vertices)[path.vertices->size() - (1 + path.start_vertex)]; } - coord_t projectPathAlongAxis(const PathOrderMonotonic<ConstPolygonRef>::Path& path, const Point& vector) + coord_t projectPathAlongAxis(const PathOrderPath<ConstPolygonPointer>& path, const Point& vector) { return dot(startVertex(path), vector); } - coord_t projectEndAlongAxis(const PathOrderMonotonic<ConstPolygonRef>::Path& path, const Point& vector) + coord_t projectEndAlongAxis(const PathOrderPath<ConstPolygonPointer>& path, const Point& vector) { return dot(endVertex(path), vector); } @@ -58,8 +58,8 @@ namespace cura coord_t shortestDistance ( - const PathOrderMonotonic<ConstPolygonRef>::Path& path_a, - const PathOrderMonotonic<ConstPolygonRef>::Path& path_b + const PathOrderPath<ConstPolygonPointer>& path_a, + const PathOrderPath<ConstPolygonPointer>& path_b ) { // NOTE: Assume these are more or less lines. @@ -68,7 +68,7 @@ namespace cura return vSize(point_pair.second - point_pair.first); } - coord_t pathLength(const PathOrderMonotonic<ConstPolygonRef>::Path& path) + coord_t pathLength(const PathOrderPath<ConstPolygonPointer>& path) { // NOTE: Assume these are more or less lines. return vSize(endVertex(path) - startVertex(path)); @@ -114,7 +114,7 @@ namespace cura max_deviation ); Settings infill_settings; - VariableWidthPaths result_paths; + std::vector<VariableWidthLines> result_paths; Polygons dummy_polys; infill_comp.generate(result_paths, dummy_polys, output, infill_settings, nullptr, nullptr); } @@ -127,7 +127,7 @@ namespace cura const std::string& original_filename, const AngleRadians& angle, const Point& monotonic_vec, - const std::vector<std::vector<PathOrderMonotonic<ConstPolygonRef>::Path>>& sections + const std::vector<std::vector<PathOrderPath<ConstPolygonPointer>>>& sections ) { constexpr int buff_size = 1024; @@ -181,15 +181,15 @@ namespace cura const Point perpendicular_axis{ turn90CCW(monotonic_axis) }; constexpr coord_t max_adjacent_distance = line_distance + 1; - PathOrderMonotonic<ConstPolygonRef> object_under_test(angle_from_first_line, max_adjacent_distance, monotonic_axis * -1000); + PathOrderMonotonic<ConstPolygonPointer> object_under_test(angle_from_first_line, max_adjacent_distance, monotonic_axis * -1000); for (const auto& polyline : polylines) { - object_under_test.addPolyline(polyline); + object_under_test.addPolyline(ConstPolygonPointer(polyline)); } object_under_test.optimize(); // Collect sections: - std::vector<std::vector<PathOrderMonotonic<ConstPolygonRef>::Path>> sections; + std::vector<std::vector<PathOrderPath<ConstPolygonPointer>>> sections; sections.emplace_back(); coord_t last_path_mono_projection = projectPathAlongAxis(object_under_test.paths.front(), monotonic_axis); for (const auto& path : object_under_test.paths) diff --git a/tests/WallsComputationTest.cpp b/tests/WallsComputationTest.cpp index 925bf5a80..2210240ba 100644 --- a/tests/WallsComputationTest.cpp +++ b/tests/WallsComputationTest.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2021 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include <gtest/gtest.h> @@ -72,7 +72,6 @@ public: //Settings for a simple 2 walls, about as basic as possible. settings.add("alternate_extra_perimeter", "false"); - settings.add("beading_strategy_type", "inward_distributed"); settings.add("fill_outline_gaps", "false"); settings.add("initial_layer_line_width_factor", "100"); settings.add("magic_spiralize", "false"); diff --git a/tests/beading_strategy/CenterDeviationBeadingStrategyTest.cpp b/tests/beading_strategy/CenterDeviationBeadingStrategyTest.cpp deleted file mode 100644 index 9816fee1a..000000000 --- a/tests/beading_strategy/CenterDeviationBeadingStrategyTest.cpp +++ /dev/null @@ -1,158 +0,0 @@ -//Copyright (c) 2021 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. - -#include <gtest/gtest.h> - -#include "../../src/BeadingStrategy/CenterDeviationBeadingStrategy.h" //Code under test. - -namespace cura -{ - -/*! - * Tests calling the constructor of the CenterDeviationBeadingStrategy and - * whether it sets members correctly. - */ -TEST(CenterDeviationBeadingStrategy, Construction) -{ - CenterDeviationBeadingStrategy strategy(400, 0.6, 0.5, 0.5); //Pretty standard settings. - EXPECT_EQ(strategy.minimum_line_width_split, 200) << "Since the transition threshold was 50%, the minimum line width should be 0.5 times the preferred width."; - strategy = CenterDeviationBeadingStrategy(400, 0.6, 0, 0); - EXPECT_EQ(strategy.minimum_line_width_split, 0) << "Since the transition threshold was 0%, the minimum line width should be 0."; - strategy = CenterDeviationBeadingStrategy(0, 0.6, 0.5, 0.5); - EXPECT_EQ(strategy.minimum_line_width_split, 0) << "Since the line width was 0%, the minimum line width should also be 0."; - strategy = CenterDeviationBeadingStrategy(400, 0.6, 1, 1); - EXPECT_EQ(strategy.minimum_line_width_split, 400) << "Since the transition threshold was 100%, the minimum line width equals the preferred width."; -} - -/*! - * Tests getting the optimal thickness with Center Deviation. - * - * This should simply be a multiplication of the line width, when using Center - * Deviation. It doesn't adjust the line widths by itself. - */ -TEST(CenterDeviationBeadingStrategy, GetOptimalThickness) -{ - constexpr coord_t line_width = 400; - CenterDeviationBeadingStrategy strategy(line_width, 0.6, 0.5, 0.5); - EXPECT_EQ(strategy.getOptimalThickness(0), 0) << "With 0 beads, you'll fill 0 space."; - EXPECT_EQ(strategy.getOptimalThickness(1), line_width) << "With 1 bead, optimally you'll want to print that bead at the optimal line width."; - EXPECT_EQ(strategy.getOptimalThickness(4), 4 * line_width) << "With 4 beads, optimally fill 4 line widths."; -} - -/*! - * Test getting the width at which we need to transition to a greater number of - * lines. - */ -TEST(CenterDeviationBeadingStrategy, GetTransitionThickness) -{ - constexpr coord_t line_width = 400; - - //Transition ratio 25%. - CenterDeviationBeadingStrategy strategy(line_width, 0.6, 0.25, 0.25); - EXPECT_EQ(strategy.getTransitionThickness(0), 0.25 * line_width) << "The transition from 0 beads to 1 happens at 25% line width."; - EXPECT_EQ(strategy.getTransitionThickness(1), 1.25 * line_width) << "The transition from 1 bead to 2 happens at 125% line width."; - EXPECT_EQ(strategy.getTransitionThickness(4), 4.25 * line_width) << "The transition from 4 beads to 5 happens at 4 + 25% line width."; - - //Transition ratio 50%. - strategy = CenterDeviationBeadingStrategy(line_width, 0.6, 0.5, 0.5); - EXPECT_EQ(strategy.getTransitionThickness(0), 0.5 * line_width) << "The transition from 0 beads to 1 happens at 50% line width."; - EXPECT_EQ(strategy.getTransitionThickness(1), 1.5 * line_width) << "The transition from 1 bead to 2 happens at 150% line width."; - EXPECT_EQ(strategy.getTransitionThickness(5), 5.5 * line_width) << "The transition from 5 beads to 6 happens at 5 + 50% line width."; - - //Transition ratio 95%. - strategy = CenterDeviationBeadingStrategy(line_width, 0.6, 0.95, 0.95); - EXPECT_EQ(strategy.getTransitionThickness(0), 0.95 * line_width) << "The transition from 0 beads to 1 happens at 95% line width."; - EXPECT_EQ(strategy.getTransitionThickness(1), 1.95 * line_width) << "The transition from 1 bead to 2 happens at 195% line width."; - EXPECT_EQ(strategy.getTransitionThickness(3), 3.95 * line_width) << "The transition from 3 beads to 4 happens at 3 + 95% line width."; - - //Transition ratio 100%. - strategy = CenterDeviationBeadingStrategy(line_width, 0.6, 1, 1); - EXPECT_EQ(strategy.getTransitionThickness(0), line_width) << "Only transition to have a line if it fits completely."; - EXPECT_EQ(strategy.getTransitionThickness(1), 2 * line_width) << "Only transition to have two lines if they both fit completely."; - EXPECT_EQ(strategy.getTransitionThickness(2), 3 * line_width) << "Only transition to have three lines if they all fit completely."; - - //Transition ratio 0%. - strategy = CenterDeviationBeadingStrategy(line_width, 0.6, 0, 0); - EXPECT_EQ(strategy.getTransitionThickness(0), 0) << "Always transition to 1 line. The minimum line width is 0 after all."; - EXPECT_EQ(strategy.getTransitionThickness(1), line_width) << "If 1 line fits completely, immediately transition to 2 lines."; - EXPECT_EQ(strategy.getTransitionThickness(6), 6 * line_width) << "If 6 lines fit completely, immediately transition to 7 lines."; -} - -/*! - * Test getting the optimal bead count for a given shape width. - */ -TEST(CenterDeviationBeadingStrategy, GetOptimalBeadCount) -{ - constexpr coord_t line_width = 400; - - //Transition ratio 25%. - CenterDeviationBeadingStrategy strategy(line_width, 0.6, 0.25, 0.25); - //Anything below 25% line width should then produce 0 lines. - for(coord_t width = 0; width < 0.25 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 0) << "Width is below the transition thickness from 0 to 1 line, so it should produce 0 lines."; - } - EXPECT_LE(strategy.getOptimalBeadCount(0.25 * line_width), 1) << "At exactly the transition thickness, either 0 or 1 line is acceptable."; - for(coord_t width = 0.25 * line_width + 1; width < 1.25 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 1) << "Width is above the transition thickness from 0 to 1 line but below 1 to 2 lines, so it should produce 1 line."; - } - EXPECT_TRUE(strategy.getOptimalBeadCount(1.25 * line_width) == 1 || strategy.getOptimalBeadCount(1.25 * line_width) == 2) << "At exactly 125% line width, either 1 or 2 lines is acceptable."; - for(coord_t width = 1.25 * line_width + 1; width < 2.25 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 2) << "Width is above the transition thickness from 1 to 2 lines but below 2 to 3 lines, so it should produce 2 lines."; - } - - //Transition ratio 80%. - strategy = CenterDeviationBeadingStrategy(line_width, 0.6, 0.8, 0.8); - //Anything below 80% line width should then produce 0 lines. - for(coord_t width = 0; width < 0.8 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 0) << "Width is below the transition thickness from 0 to 1 line, so it should produce 0 lines."; - } - EXPECT_LE(strategy.getOptimalBeadCount(0.8 * line_width), 1) << "At exactly the transition thickness, either 0 or 1 line is acceptable."; - for(coord_t width = 0.8 * line_width + 1; width < 1.8 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 1) << "Width is above the transition thickness from 0 to 1 line but below 1 to 2 lines, so it should produce 1 line."; - } - EXPECT_TRUE(strategy.getOptimalBeadCount(1.8 * line_width) == 1 || strategy.getOptimalBeadCount(1.8 * line_width) == 2) << "At exactly 180% line width, either 1 or 2 lines is acceptable."; - for(coord_t width = 1.8 * line_width + 1; width < 2.8 * line_width; width += 10) - { - EXPECT_EQ(strategy.getOptimalBeadCount(width), 2) << "Width is above the transition thickness from 1 to 2 lines but below 2 to 3 lines, so it should produce 2 lines."; - } -} - -/*! - * Tests whether the line compactness setting does what it is supposed to do, - * producing fewer, wider lines when the setting is high than when the setting - * is low. - * - * This is a test for requirements. The exact outcome of a function is not - * tested, but properties of the outcome is tested. - */ -TEST(CenterDeviationBeadingStrategy, LineCompactnessMonotonic) -{ - constexpr coord_t line_width = 400; - constexpr coord_t widths[] = {0, 1, 99, 101, 150, 200, 299, 300, 301, 399, 400, 401, 410, 450, 500, 660, 770, 880, 910, 1000, 1200}; //Bunch of widths to test with. - constexpr float compactnesses[] = {0, 0.1, 0.2, 0.24, 0.25, 0.26, 0.3, 0.5, 0.7, 0.75, 0.99, 1}; //Bunch of line compactness factors to test with. - constexpr size_t num_compactnesses = sizeof(compactnesses) / sizeof(float); - - for(coord_t width : widths) - { - for(size_t low_index = 0; low_index < num_compactnesses; ++low_index) - { - const float low_compactness = compactnesses[low_index]; - for(size_t high_index = low_index; high_index < num_compactnesses; ++high_index) - { - const float high_compactness = compactnesses[high_index]; - - EXPECT_GE( - CenterDeviationBeadingStrategy(line_width, 0.6, low_compactness, low_compactness).getOptimalBeadCount(width), - CenterDeviationBeadingStrategy(line_width, 0.6, high_compactness, high_compactness).getOptimalBeadCount(width) - ) << "When the compactness is low, the number of beads should always be greater or equal to when the compactness is high."; - } - } - } -} - -}
\ No newline at end of file diff --git a/tests/utils/ExtrusionLineTest.cpp b/tests/utils/ExtrusionLineTest.cpp index d26820a98..100ae790f 100644 --- a/tests/utils/ExtrusionLineTest.cpp +++ b/tests/utils/ExtrusionLineTest.cpp @@ -8,6 +8,7 @@ #include <../src/utils/linearAlg2D.h> #include <limits> +#include <numeric> namespace cura { @@ -405,36 +406,39 @@ namespace cura TEST(ExtrusionLineTest, simplifyLineWidthVariance) { //Generate a line with several vertices halfway. - constexpr coord_t spacing = 100; - ExtrusionLine colinear_polylines(0, false);; + ExtrusionLine colinear_polylines(0, false); auto& colinear = colinear_polylines.junctions; colinear.emplace_back(Point(0, 0), 200, 0); - colinear.emplace_back(Point(spacing / 4, 0), 200, 0); - colinear.emplace_back(Point(spacing / 2, 0), 400, 0); - colinear.emplace_back(Point(spacing / 2 + spacing / 4, 0), 600, 0); - colinear.emplace_back(Point(spacing, 0), 800, 0); + colinear.emplace_back(Point(500, 0), 200, 0); + colinear.emplace_back(Point(1000, 0), 400, 0); + colinear.emplace_back(Point(1500, 0), 600, 0); + colinear.emplace_back(Point(2000, 0), 800, 0); + + // Get 'before' average width: + coord_t averge_width_before = 0; + for (size_t i_junction = 0; i_junction < (colinear.size() - 1); ++i_junction) + { + averge_width_before += ((colinear[i_junction].w + colinear[i_junction + 1].w) / 2) * vSize(colinear[i_junction].p - colinear[i_junction + 1].p); + } - colinear_polylines.simplify(25 * 25, 25 * 25, std::numeric_limits<coord_t>::max()); + colinear_polylines.simplify(20 * 20, 5 * 5, std::numeric_limits<coord_t>::max()); //Regardless of parameters, it should always remove those middle vertices. ASSERT_EQ(colinear_polylines.junctions.size(), 2) << "The degenerate vertices should have been removed."; - ASSERT_EQ(colinear[1].w, 500) << "The width of the end-junction should be the average of all removed."; // Since the distances where equal, and they should all have been merged with that one. } TEST(ExtrusionLineTest, simplifyNoLineWidthVariance) { //Generate a line with several vertices halfway. - constexpr coord_t spacing = 100; - ExtrusionLine colinear_polylines(0, false);; + ExtrusionLine colinear_polylines(0, false); auto& colinear = colinear_polylines.junctions; colinear.emplace_back(Point(0, 0), 200, 0); - colinear.emplace_back(Point(spacing / 4, 0), 200, 0); - colinear.emplace_back(Point(spacing / 2, 0), 400, 0); - colinear.emplace_back(Point(spacing / 2 + spacing / 4, 0), 600, 0); - colinear.emplace_back(Point(spacing, 0), 800, 0); + colinear.emplace_back(Point(500, 0), 200, 0); + colinear.emplace_back(Point(1000, 0), 400, 0); + colinear.emplace_back(Point(1500, 0), 600, 0); + colinear.emplace_back(Point(2000, 0), 800, 0); - colinear_polylines.simplify(25 * 25, 25 * 25, 1); + colinear_polylines.simplify(20 * 20, 5 * 5, 1); // Now it should _not_ remove the vertices, since the total width altered will be more than the max area ... ASSERT_EQ(colinear_polylines.junctions.size(), 5) << "No junctions should have been removed."; // ... even though they are co-linear! - ASSERT_EQ(colinear.back().w, 800) << "The width of the end-junction should not have been changed."; } } diff --git a/tests/utils/PolygonConnectorTest.cpp b/tests/utils/PolygonConnectorTest.cpp index bda3a9d89..0d4d8c920 100644 --- a/tests/utils/PolygonConnectorTest.cpp +++ b/tests/utils/PolygonConnectorTest.cpp @@ -1,4 +1,4 @@ -//Copyright (c) 2019 Ultimaker B.V. +//Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. #include <gtest/gtest.h> @@ -14,14 +14,16 @@ class PolygonConnectorTest : public testing::Test { public: Polygon test_square; - Polygon test_square2; //Larger and more to the right. + Polygon test_square_around; //Larger, around the first square. + Polygon test_square_adjacent; //Next to the first square. Polygon test_triangle; Polygon test_circle; Polygon test_convex_shape; Polygons test_shapes; //All above polygons! As well as an inset of 100 microns of them. PolygonConnector* pc; - Polygons connecteds; + Polygons connected_polygons; + std::vector<VariableWidthLines> connected_paths; virtual void SetUp() { @@ -29,50 +31,19 @@ public: test_square.emplace_back(1000, 0); test_square.emplace_back(1000, 1000); test_square.emplace_back(0, 1000); - test_shapes.add(test_square); - test_square2.emplace_back(1100, 1500); - test_square2.emplace_back(2000, 1500); - test_square2.emplace_back(2000, -500); - test_square2.emplace_back(1100, -500); - test_shapes.add(test_square2); - - test_triangle.emplace_back(0, 2100); - test_triangle.emplace_back(500, 1100); - test_triangle.emplace_back(1500, 2100); - test_shapes.add(test_triangle); - - for (double a = 0; a < 1.0; a += 0.05) - { - test_circle.add(Point(2050, 2050) + Point(std::cos(a * 2 * M_PI)*500, std::sin(a * 2 * M_PI)*500)); - } - test_shapes.add(test_circle); - - test_convex_shape.emplace_back(-300, 0); - test_convex_shape.emplace_back(-100, 500); - test_convex_shape.emplace_back(-100, 600); - test_convex_shape.emplace_back(-200, 1000); - test_convex_shape.emplace_back(-500, 1500); - test_convex_shape.emplace_back(-1500, 1500); - test_convex_shape.emplace_back(-1500, 1500); - test_convex_shape.emplace_back(-1600, 1100); - test_convex_shape.emplace_back(-700, 200); - test_shapes.add(test_convex_shape); - - Polygons inset = test_shapes; - while (!inset.empty()) - { - inset = inset.offset(-100); - test_shapes.add(inset); - } + test_square_around.emplace_back(1100, 1100); + test_square_around.emplace_back(-100, 1100); + test_square_around.emplace_back(-100, -100); + test_square_around.emplace_back(1100, -100); - constexpr coord_t line_width = 100; - constexpr coord_t max_dist = 170; - pc = new PolygonConnector(line_width, max_dist); - pc->add(test_shapes); - connecteds = pc->connect(); + test_square_adjacent.emplace_back(1100, 200); + test_square_adjacent.emplace_back(2100, 200); + test_square_adjacent.emplace_back(2100, 1200); + test_square_adjacent.emplace_back(1100, 1200); - ASSERT_GT(connecteds.size(), 0) << "PolygonConnector gave no output polygons!"; + constexpr coord_t line_width = 100; + pc = new PolygonConnector(line_width); } void TearDown() @@ -82,65 +53,126 @@ public: }; -TEST_F(PolygonConnectorTest, getBridgeTest) +/*! + * Test creating a bridge between two squares that are nested in each other at + * precisely the line width apart. + * + * This is a common occurrence with skin. + */ +TEST_F(PolygonConnectorTest, getBridgeNestedSquares) { - PolygonConnector::PolygonBridge predicted( - PolygonConnector::PolygonConnection{ - ClosestPolygonPoint(Point(0, 500), 2, test_square), - ClosestPolygonPoint(Point(-100, 500), 0, test_convex_shape)}, - PolygonConnector::PolygonConnection{ - ClosestPolygonPoint(Point(0, 600), 2, test_square), - ClosestPolygonPoint(Point(-100, 600), 0, test_convex_shape)}); - std::vector<Polygon> polys; - polys.push_back(test_convex_shape); - - std::optional<PolygonConnector::PolygonBridge> computed = pc->getBridge(test_square, polys); - - ASSERT_TRUE(bool(computed)) << "An answer is expected."; - if (computed) - { - const coord_t a_from_error = vSize(predicted.a.from.p() - computed->a.from.p()); - const coord_t a_to_error = vSize(predicted.a.to.p() - computed->a.to.p()); - const coord_t b_from_error = vSize(predicted.b.from.p() - computed->b.from.p()); - const coord_t b_to_error = vSize(predicted.b.to.p() - computed->b.to.p()); - - constexpr coord_t maximum_error = 10; - EXPECT_LT(a_from_error, maximum_error) << "a.from was computed to something different from what was predicted!"; - EXPECT_LT(a_to_error, maximum_error) << "a.to was computed to something different from what was predicted!"; - EXPECT_LT(b_from_error, maximum_error) << "b.from was computed to something different from what was predicted!"; - EXPECT_LT(b_to_error, maximum_error) << "b.to was computed to something different from what was predicted!"; - } + std::vector<Polygon> to_connect({test_square_around}); + std::optional<PolygonConnector::PolygonBridge<Polygon>> bridge = pc->getBridge(test_square, to_connect); + + ASSERT_NE(bridge, std::nullopt) << "The two polygons are nested simply, so they are definitely positioned closely enough to bridge. They are also wide enough."; + + EXPECT_EQ(vSize(bridge->a.from_point - bridge->a.to_point), 100) << "The polygons are 100 units spaced out concentrically, so this is the shortest possible bridge."; + EXPECT_EQ(vSize(bridge->b.from_point - bridge->b.to_point), 100) << "The second bridge should also be equally short in this case."; + EXPECT_EQ(LinearAlg2D::getDist2BetweenLineSegments(bridge->a.from_point, bridge->a.to_point, bridge->b.from_point, bridge->b.to_point), 100 * 100) << "The bridges should be spaced 1 line width (100 units) apart."; + EXPECT_LT(LinearAlg2D::pointIsLeftOfLine(bridge->b.from_point, bridge->a.from_point, bridge->a.to_point), 0) << "Connection B should be to the right of connection A."; + EXPECT_LT(LinearAlg2D::pointIsLeftOfLine(bridge->b.to_point, bridge->a.from_point, bridge->a.to_point), 0) << "Connection B should be to the right of connection A."; } -TEST_F(PolygonConnectorTest, connectionLengthTest) +/*! + * Test creating a bridge between two adjacent squares that are spaced 1 line + * width apart. + * + * This is a common occurrence with multiplied infill. + */ +TEST_F(PolygonConnectorTest, getBridgeAdjacentSquares) { - constexpr coord_t maximum_distance = 170; - std::unordered_set<Point> input_verts; - for (ConstPolygonRef poly : test_shapes) - { - for (Point p : poly) - { - input_verts.emplace(p); - } - } + std::vector<Polygon> to_connect({test_square_adjacent}); + std::optional<PolygonConnector::PolygonBridge<Polygon>> bridge = pc->getBridge(test_square, to_connect); - coord_t longest_connection_dist = 0; - size_t too_long_connection_count = 0; - for (PolygonConnector::PolygonBridge bridge : pc->all_bridges) - { - for (auto connection : {bridge.a, bridge.b}) - { - const coord_t connection_dist = vSize(connection.to.p() - connection.from.p()); - if (connection_dist > maximum_distance) - { - too_long_connection_count++; - longest_connection_dist = std::max(longest_connection_dist, connection_dist); - } - } - } + ASSERT_NE(bridge, std::nullopt) << "The two polygons are adjacent, spaced closely enough to bridge and with enough room."; + + EXPECT_EQ(vSize(bridge->a.from_point - bridge->a.to_point), 100) << "The polygons are 100 units spaced apart, so this is the shortest possible bridge."; + EXPECT_EQ(vSize(bridge->b.from_point - bridge->b.to_point), 100) << "The second bridge should also be equally short in this case."; + EXPECT_EQ(LinearAlg2D::getDist2BetweenLineSegments(bridge->a.from_point, bridge->a.to_point, bridge->b.from_point, bridge->b.to_point), 100 * 100) << "The bridges should be spaced 1 line width (100 units) apart."; + EXPECT_LT(LinearAlg2D::pointIsLeftOfLine(bridge->b.from_point, bridge->a.from_point, bridge->a.to_point), 0) << "Connection B should be to the right of connection A."; + EXPECT_LT(LinearAlg2D::pointIsLeftOfLine(bridge->b.to_point, bridge->a.from_point, bridge->a.to_point), 0) << "Connection B should be to the right of connection A."; +} + +/*! + * Test that the bridge is created in the location where the polygons are + * closest together. + */ +TEST_F(PolygonConnectorTest, getBridgeClosest) +{ + Polygon adjacent_slanted; //A polygon that's adjacent to the first square, but tilted such that the vertex at [1100,200] is definitely the closest. + adjacent_slanted.emplace_back(1100, 200); + adjacent_slanted.emplace_back(2100, 200); + adjacent_slanted.emplace_back(2140, 1200); + adjacent_slanted.emplace_back(1140, 1200); + std::vector<Polygon> to_connect({adjacent_slanted}); - ASSERT_EQ(too_long_connection_count, 0) << "PolygonConnector::connect() obtained " << too_long_connection_count << " too long bridge connections! Longest is " << INT2MM(longest_connection_dist) << "\n"; + std::optional<PolygonConnector::PolygonBridge<Polygon>> bridge = pc->getBridge(test_square, to_connect); + + ASSERT_NE(bridge, std::nullopt) << "The two polygons are adjacent and spaced closely enough to bridge along their entire side, even with the slant."; + + EXPECT_EQ(bridge->b.from_point, Point(1000, 200)) << "The closest connection is [1000,200] -> [1100,200]. There is no space to the right of that, so bridge B should be there."; + EXPECT_EQ(bridge->b.to_point, Point(1100, 200)) << "The closest connection is [1000,200] -> [1100,200]. There is no space to the right of that, so bridge B should be there."; + EXPECT_GT(LinearAlg2D::pointIsLeftOfLine(bridge->a.from_point, bridge->b.from_point, bridge->b.to_point), 0) << "Connection A should be to the left of connection B."; + EXPECT_GT(LinearAlg2D::pointIsLeftOfLine(bridge->a.to_point, bridge->b.from_point, bridge->b.to_point), 0) << "Connection A should be to the left of connection B."; +} + +/*! + * Test attempting to create a bridge when the polygons are too far apart. + */ +TEST_F(PolygonConnectorTest, getBridgeTooFar) +{ + Polygon too_far; //More than 1.5 line width away. + too_far.emplace_back(1200, 0); + too_far.emplace_back(2200, 0); + too_far.emplace_back(2200, 1000); + too_far.emplace_back(1200, 1000); + std::vector<Polygon> to_connect({too_far}); + + std::optional<PolygonConnector::PolygonBridge<Polygon>> bridge = pc->getBridge(test_square, to_connect); + + EXPECT_EQ(bridge, std::nullopt) << "The two polygons are 200 units apart where they are closest, which is more than 1.5 times the line width (100), so they can't be connected."; } +/*! + * Test attempting to create a bridge when the connecting part is too narrow. + * + * Since the bridging lines need to be spaced 1 line width apart, they can't be + * too close together. If the bridge can't be constructed keeping proper spacing + * the bridge should fail to be created. + */ +TEST_F(PolygonConnectorTest, getBridgeTooNarrow) +{ + Polygon too_narrow; + too_narrow.emplace_back(1100, 400); + too_narrow.emplace_back(2100, 400); + too_narrow.emplace_back(2100, 480); //Less than 100 units wide. + too_narrow.emplace_back(1100, 480); + std::vector<Polygon> to_connect({too_narrow}); + + std::optional<PolygonConnector::PolygonBridge<Polygon>> bridge = pc->getBridge(test_square, to_connect); + + EXPECT_EQ(bridge, std::nullopt) << "Where the two polygons are adjacent is only 80 units wide. This is not enough to create a bridge with the connecting lines spaced 1 line width (100 units) apart."; +} + +/*! + * Try connecting four nested polygons. + * + * Let's play a game of connect four! + */ +TEST_F(PolygonConnectorTest, connectFourNested) +{ + Polygons connecting; + connecting.add(test_square_around); //1200-wide square. + connecting.add(test_square); //1000-wide square. + connecting.add(test_square.offset(-100)); //800-wide square. + connecting.add(test_square.offset(-200)); //600-wide square. + + pc->add(connecting); + Polygons output_polygons; + std::vector<VariableWidthLines> output_paths; + pc->connect(output_polygons, output_paths); + + EXPECT_EQ(output_polygons.size(), 1) << "All four polygons should've gotten connected into 1 single polygon."; +} } |