diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ccaed37b8..28ed81aa78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,7 +43,6 @@ endif () set(engine_SRCS # Except main.cpp. src/Application.cpp - src/bridge.cpp src/ConicalOverhang.cpp src/ExtruderPlan.cpp src/ExtruderTrain.cpp @@ -99,6 +98,12 @@ set(engine_SRCS # Except main.cpp. src/BeadingStrategy/WideningBeadingStrategy.cpp src/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp + src/bridge/bridge.cpp + src/bridge/ExpansionRange.cpp + src/bridge/SegmentOverlappingData.cpp + src/bridge/TransformedSegment.cpp + src/bridge/TransformedShape.cpp + src/communication/ArcusCommunication.cpp src/communication/ArcusCommunicationPrivate.cpp src/communication/CommandLine.cpp diff --git a/doc/bridging_skin_support.svg b/doc/bridging_skin_support.svg new file mode 100644 index 0000000000..ef32c151e7 --- /dev/null +++ b/doc/bridging_skin_support.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + Infill lines below + Segment + Non-projected range + Range projected to partial line + + diff --git a/include/ExtruderPlan.h b/include/ExtruderPlan.h index e4d301969a..e7f45e542d 100644 --- a/include/ExtruderPlan.h +++ b/include/ExtruderPlan.h @@ -42,7 +42,7 @@ class ExtruderPlan FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationFull); FRIEND_TEST(ExtruderPlanPathsParameterizedTest, BackPressureCompensationHalf); FRIEND_TEST(ExtruderPlanTest, BackPressureCompensationEmptyPlan); - friend class FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; + friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; #endif public: size_t extruder_nr_{ 0 }; //!< The extruder used for this paths in the current plan. diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h index 9575f157d8..f304ef535a 100644 --- a/include/FffGcodeWriter.h +++ b/include/FffGcodeWriter.h @@ -39,7 +39,7 @@ struct MeshPathConfigs; class FffGcodeWriter : public NoCopy { friend class FffProcessor; // Because FffProcessor exposes finalize (TODO) - friend class FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; + friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; private: coord_t max_object_height; //!< The maximal height of all previously sliced meshgroups, used to avoid collision when moving to the next meshgroup to print. @@ -749,9 +749,8 @@ class FffGcodeWriter : public NoCopy * \param mesh the mesh containing the layer of interest * \param part \param part The part for which to create gcode * \param infill_line_width line width of the infill - * \return true if there needs to be a skin edge support wall in this layer, otherwise false */ - static bool partitionInfillBySkinAbove( + static void partitionInfillBySkinAbove( Shape& infill_below_skin, Shape& infill_not_below_skin, const LayerPlan& gcode_layer, diff --git a/include/LayerPlan.h b/include/LayerPlan.h index 0eea515f75..2d6b10756c 100644 --- a/include/LayerPlan.h +++ b/include/LayerPlan.h @@ -57,7 +57,7 @@ class LayerPlan : public NoCopy friend class LayerPlanBuffer; #ifdef BUILD_TESTS friend class AddTravelTest; - friend class FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; + friend class DISABLED_FffGcodeWriterTest_SurfaceGetsExtraInfillLinesUnderIt_Test; friend class AntiOozeAmountsTest; FRIEND_TEST(AntiOozeAmountsTest, ComputeAntiOozeAmounts); #endif @@ -167,6 +167,8 @@ class LayerPlan : public NoCopy bool min_layer_time_used = false; //!< Wether or not the minimum layer time (cool_min_layer_time) was actually used in this layerplan. + std::map infill_lines_; //!< Infill lines generated for this layer + const std::vector fan_speed_layer_time_settings_per_extruder_; enum CombBoundary @@ -417,6 +419,10 @@ class LayerPlan : public NoCopy */ void planPrime(double prime_blob_wipe_length = 10.0); + void setGeneratedInfillLines(const SliceMeshStorage* mesh, const MixedLinesSet& infill_lines); + + const MixedLinesSet getGeneratedInfillLines(const SliceMeshStorage* mesh) const; + /*! * Add an extrusion move to a certain point, optionally with a different flow than the one in the \p config. * diff --git a/include/LayerPlanBuffer.h b/include/LayerPlanBuffer.h index 3916321729..f1a415a601 100644 --- a/include/LayerPlanBuffer.h +++ b/include/LayerPlanBuffer.h @@ -4,6 +4,7 @@ #ifndef LAYER_PLAN_BUFFER_H #define LAYER_PLAN_BUFFER_H +#include #include #include @@ -17,6 +18,7 @@ namespace cura class LayerPlan; class ExtruderPlan; class GCodeExport; +struct LayerIndex; /*! * Class for buffering multiple layer plans (\ref LayerPlan) / extruder plans within those layer plans, so that temperature commands can be inserted in earlier layer plans. @@ -57,6 +59,9 @@ class LayerPlanBuffer */ std::list buffer_; + std::mutex buffer_mutex_; + std::condition_variable buffer_condition_variable_; + public: LayerPlanBuffer(GCodeExport& gcode) : gcode_(gcode) @@ -66,11 +71,6 @@ class LayerPlanBuffer void setPreheatConfig(); - /*! - * Push a new layer plan into the buffer - */ - void push(LayerPlan& layer_plan); - /*! * Push a new layer onto the buffer and handle the buffer. * Write a layer to gcode if it is popped out of the buffer. @@ -85,6 +85,15 @@ class LayerPlanBuffer */ void flush(); + /*! + * Gets the layer plan for the given layer, once it has been completed. It will wait for completion if necessary + * @param layer_nr The layer number to get the plan for + * @return The completed layer plan + * @warning Make sure you always ask for a layer plan below the one that is being processed. Since the layers processing is started bottom to top and the engine can be run + * mono-threaded, asking for a layer above could lead to a deadlock because the processing of this layer will never start. + */ + const LayerPlan* getCompletedLayerPlan(const LayerIndex& layer_nr) const; + private: /*! * Process all layers in the buffer diff --git a/include/TreeModelVolumes.h b/include/TreeModelVolumes.h index 1774b24fdf..b6598370a6 100644 --- a/include/TreeModelVolumes.h +++ b/include/TreeModelVolumes.h @@ -19,7 +19,6 @@ namespace cura { -constexpr coord_t EPSILON = 5; constexpr coord_t FUDGE_LENGTH = 50; class SliceDataStorage; diff --git a/include/bridge/ExpansionRange.h b/include/bridge/ExpansionRange.h new file mode 100644 index 0000000000..7b5bbf10c4 --- /dev/null +++ b/include/bridge/ExpansionRange.h @@ -0,0 +1,139 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_EXPANSIONRANGE_H +#define BRIDGE_EXPANSIONRANGE_H + +#include + +#include "bridge/TransformedSegment.h" + +namespace cura +{ + +struct SegmentOverlapping; + +/*! Helper structure containing a vertical range that has been projected (or not) for a segment */ +class ExpansionRange +{ +public: + /*! + * Constructs a range that has been projected to an infill line + * @param segment The actual projected segment, cropped to the range + * @param supporting_segment The infill line which is supporting this segment + */ + ExpansionRange(const TransformedSegment& segment, const TransformedSegment* supporting_segment) + : data_{ .segment = segment } + , supporting_segment_(supporting_segment) + , is_projected_(true) + { + } + + /*! + * Constructs a range that has not been projected + * @param min_y The range bottom Y coordinate + * @param max_y The range maximum Y coordinate + * @param expanded_segment The segment being expanded + */ + ExpansionRange(const coord_t min_y, const coord_t max_y, const TransformedSegment* expanded_segment) + : data_{ .range = { .min_y = min_y, .max_y = max_y } } + , supporting_segment_(expanded_segment) + , is_projected_(false) + { + } + + coord_t minY() const + { + return is_projected_ ? data_.segment.minY() : data_.range.min_y; + } + + coord_t maxY() const + { + return is_projected_ ? data_.segment.maxY() : data_.range.max_y; + } + + /*! + * Calculate the horizontal overlapping between this range and an other segment, given the expand direction. Overlapping means that the new segment is on the right (or left) + * of the actual segment on a horizontal band formed by the segment top and bottom + * @param other The other segment that may be overlapping + * @param expand_direction The expand direction, 1 means expand to the right, -1 expand to the left + * @return The overlapping details if the segment actually overlap, or nullopt if it doesn't + */ + std::optional calculateOverlapping(const TransformedSegment& other, const int8_t expand_direction) const; + + /*! Move the range maximum Y coordinate down, actually cropping it */ + void cropTop(const coord_t new_max_y) + { + if (is_projected_) + { + data_.segment.cropTop(new_max_y); + } + else + { + data_.range.max_y = new_max_y; + } + } + + /*! Move the segment minimum Y coordinate up, actually cropping it */ + void cropBottom(const coord_t new_min_y) + { + if (is_projected_) + { + data_.segment.cropBottom(new_min_y); + } + else + { + data_.range.min_y = new_min_y; + } + } + + /*! Indicates if the range is still valid, e.g. doesn't have a (almost) null height */ + bool isValid() const + { + return fuzzy_not_equal(maxY(), minY()); + } + + void setProjectedEnd(const Point2LL& end) + { + data_.segment.setEnd(end); + } + + const TransformedSegment* getSupportingSegment() const + { + return supporting_segment_; + } + + bool isProjected() const + { + return is_projected_; + } + + const TransformedSegment& getProjectedSegment() const + { + return data_.segment; + } + +private: + union + { + // The segment that has been projected + TransformedSegment segment; + // The range that has not been projected + struct + { + coord_t min_y; + coord_t max_y; + } range; + } data_; + + // The supporting segment that this range is based upon, or the segment being expanded if it has not been projected + const TransformedSegment* supporting_segment_; + + // True if this range has been projected to a supported infill line, in which case you can use data.segment. Otherwise the range has not been projected and only contains + // its Y range in data.range. + bool is_projected_; +}; + +} // namespace cura + +#endif diff --git a/include/bridge/SegmentOverlapping.h b/include/bridge/SegmentOverlapping.h new file mode 100644 index 0000000000..81b93b878b --- /dev/null +++ b/include/bridge/SegmentOverlapping.h @@ -0,0 +1,21 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_SEGMENTOVERLAPPING_H +#define BRIDGE_SEGMENTOVERLAPPING_H + +#include "bridge/SegmentOverlappingType.h" +#include "bridge/TransformedSegment.h" + +namespace cura +{ + +struct SegmentOverlapping +{ + SegmentOverlappingType type; + TransformedSegment other_overlapping_part; +}; + +} // namespace cura + +#endif diff --git a/include/bridge/SegmentOverlappingData.h b/include/bridge/SegmentOverlappingData.h new file mode 100644 index 0000000000..e66332823d --- /dev/null +++ b/include/bridge/SegmentOverlappingData.h @@ -0,0 +1,52 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_SEGMENTOVERLAPPINGDATA_H +#define BRIDGE_SEGMENTOVERLAPPINGDATA_H + +#include "geometry/Point2LL.h" +#include "utils/Coord_t.h" + +namespace cura +{ + +class TransformedSegment; + +/*! Helper structure that calculate the basic coordinates data to be used to perform segments overlapping calculation */ +struct SegmentOverlappingData +{ + coord_t y_min; + coord_t y_max; + coord_t this_x_min; + coord_t this_x_max; + coord_t other_x_min; + coord_t other_x_max; + + /*! + * Builds the basic segment overlapping data + * @param this_min_y The minimum Y coordinate of the original segment + * @param this_max_y The maximum Y coordinate of the original segment + * @param other_min_y The minimum Y coordinate of the tested segment + * @param other_max_y The maximum Y coordinate of the tested segment + * @param this_start The actual start position of the original segment, which may be null + * @param this_end The actual end position of the original segment, which may be null + * @param other_start The actual start position of the tested segment + * @param other_end The actual end position of the tested segment + */ + SegmentOverlappingData( + const coord_t this_min_y, + const coord_t this_max_y, + const coord_t other_min_y, + const coord_t other_max_y, + const Point2LL* this_start, + const Point2LL* this_end, + const Point2LL& other_start, + const Point2LL& other_end); + + /*! Make the cropped segment that is actually overlapping over the original segment, which may be part of all of the initial segment */ + TransformedSegment makeOtherOverlappingPart() const; +}; + +} // namespace cura + +#endif diff --git a/include/bridge/SegmentOverlappingType.h b/include/bridge/SegmentOverlappingType.h new file mode 100644 index 0000000000..f2a2923f4a --- /dev/null +++ b/include/bridge/SegmentOverlappingType.h @@ -0,0 +1,21 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_SEGMENTOVERLAPPINGTYPE_H +#define BRIDGE_SEGMENTOVERLAPPINGTYPE_H + +namespace cura +{ + +/*! Enumeration containing the overlapping type of two segments in the bridging projection direction */ +enum class SegmentOverlappingType +{ + Full, // The segment fully overlaps with the base segment + Bottom, // The segment overlaps with the base segment only on its bottom part + Top, // The segment overlaps with the base segment only on its top part + Middle, // The segment overlaps with the base segment somewhere in the middle, but not top neither bottom +}; + +} // namespace cura + +#endif diff --git a/include/bridge/TransformedSegment.h b/include/bridge/TransformedSegment.h new file mode 100644 index 0000000000..827cf21ffa --- /dev/null +++ b/include/bridge/TransformedSegment.h @@ -0,0 +1,94 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_TRANSFORMEDSEGMENT_H +#define BRIDGE_TRANSFORMEDSEGMENT_H + +#include + +#include "geometry/Point2LL.h" + +namespace cura +{ + +enum class SegmentOverlappingType; +struct SegmentOverlapping; +class PointMatrix; + +/*! Base structure tha represents a segment that has been transformed in the space where the bridging lines are horizontal */ +class TransformedSegment +{ +public: + TransformedSegment() = default; + + TransformedSegment(const Point2LL& transformed_start, const Point2LL& transformed_end) + : start_(transformed_start) + , end_(transformed_end) + { + updateMinMax(); + } + + TransformedSegment(const Point2LL& start, const Point2LL& end, const PointMatrix& matrix); + + void setStart(const Point2LL& transformed_start) + { + start_ = transformed_start; + updateMinMax(); + } + + void setEnd(const Point2LL& transformed_end) + { + end_ = transformed_end; + updateMinMax(); + } + + void updateMinMax(); + + coord_t minY() const + { + return min_y_; + } + + coord_t maxY() const + { + return max_y_; + } + + const Point2LL& getStart() const + { + return start_; + } + + const Point2LL& getEnd() const + { + return end_; + } + + /*! + * Calculate the horizontal overlapping between this segment and an other segment, given the expand direction. Overlapping means that the new segment is on the right (or left) + * of the actual segment on a horizontal band formed by the segment top and bottom + * @param other The other segment that may be overlapping + * @param expand_direction The expand direction, 1 means expand to the right, -1 expand to the left + * @return The overlapping details if the segment actually overlap, or nullopt if it doesn't + */ + std::optional calculateOverlapping(const TransformedSegment& other, const int8_t expand_direction) const; + + /*! Move the segment maximum Y coordinate down, actually cropping it */ + void cropTop(const coord_t new_max_y); + + /*! Move the segment minimum Y coordinate up, actually cropping it */ + void cropBottom(const coord_t new_min_y); + + /*! Calculate an overlapping type, given whether the segments overlap on the top, bottom or both. Only in case there is no segment intersection. */ + static SegmentOverlappingType makeNonIntersectingOverlapping(const bool overlap_top, const bool overlap_bottom); + +private: + Point2LL start_; + Point2LL end_; + coord_t min_y_{ 0 }; + coord_t max_y_{ 0 }; +}; + +} // namespace cura + +#endif diff --git a/include/bridge/TransformedShape.h b/include/bridge/TransformedShape.h new file mode 100644 index 0000000000..2b97d3648c --- /dev/null +++ b/include/bridge/TransformedShape.h @@ -0,0 +1,79 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef BRIDGE_TRANSFORMEDSHAPE_H +#define BRIDGE_TRANSFORMEDSHAPE_H + +#include "bridge/TransformedSegment.h" +#include "geometry/PointMatrix.h" + +namespace cura +{ + +class Polygon; +class Shape; + +class TransformedShape +{ +public: + /*! + * Constructs an empty transformed shape + * @warning This constructor does not assign the transformation matrix, so any subsequent call to + */ + TransformedShape() = default; + + explicit TransformedShape(const PointMatrix& matrix); + + explicit TransformedShape(const Shape& shape, const PointMatrix& matrix); + + explicit TransformedShape(const Polygon& polygon, const PointMatrix& matrix); + + /*! + * Adds a raw shape to the transformed shape + * @param shape The raw shape to be transformed + * @param filter_out_horizontal Indicates whether horizontal segments should be filtered out + */ + void addShape(const Shape& shape, const bool filter_out_horizontal = false); + + /*! + * Adds a raw polygon to the transformed shape + * @param polygon The raw polygon to be transformed + * @param filter_out_horizontal Indicates whether horizontal segments should be filtered out + * @param reserve_size Indicates if the segments should be pre-reserved in memory space. This is usually what you want unless + * this has been done very specifically. + */ + void addPolygon(const Polygon& polygon, const bool filter_out_horizontal = false, const bool reserve_size = true); + + /*! + * Adds a transformed segment to the transformed shape + * @param start The start position of the raw segment + * @param end The end position of the raw segment + * @param filter_out_horizontal Indicates whether horizontal segments should be filtered out + */ + void addSegment(const Point2LL& start, const Point2LL& end, const bool filter_out_horizontal = false); + + const std::vector& getSegments() const + { + return segments_; + } + + coord_t minY() const + { + return min_y_; + } + + coord_t maxY() const + { + return max_y_; + } + +private: + PointMatrix matrix_; + std::vector segments_; + coord_t min_y_{ std::numeric_limits::max() }; + coord_t max_y_{ std::numeric_limits::lowest() }; +}; + +} // namespace cura + +#endif diff --git a/include/bridge.h b/include/bridge/bridge.h similarity index 51% rename from include/bridge.h rename to include/bridge/bridge.h index 3e6d069109..5be7a992ef 100644 --- a/include/bridge.h +++ b/include/bridge/bridge.h @@ -1,10 +1,11 @@ -// Copyright (c) 2019 Ultimaker B.V. +// Copyright (c) 2025 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. -#ifndef BRIDGE_H -#define BRIDGE_H +#ifndef BRIDGE_BRIDGE_H +#define BRIDGE_BRIDGE_H #include +#include namespace cura { @@ -14,6 +15,7 @@ class SliceMeshStorage; class SliceDataStorage; class SupportLayer; class AngleDegrees; +class LayerPlan; /*! * \brief Computes the angle that lines have to take to bridge a certain shape @@ -39,6 +41,23 @@ std::optional bridgeAngle( const SupportLayer* support_layer, Shape& supported_regions); +/*! + * @brief Make sure the bridging above infill (below skin) is properly printable by expanding the area below the skin so that the bridging would always provide anchoring points + * from the infill lines below + * @param infill_contour The complete outer contour of the infill + * @param infill_below_skin_area The infill area that should be printed as bridging + * @param mesh The mesh being printed + * @param completed_layer_plan_below The plan of the layer just below that has been completed by now + * @param layer_nr The current layer number + * @return A new shape containing the expanded infill below skin area, and the angle to be used to generate bridging in this area + */ +std::tuple makeBridgeOverInfillPrintable( + const Shape& infill_contour, + const Shape& infill_below_skin_area, + const SliceMeshStorage& mesh, + const LayerPlan* completed_layer_plan_below, + const unsigned layer_nr); + } // namespace cura -#endif // BRIDGE_H +#endif diff --git a/include/geometry/MixedLinesSet.h b/include/geometry/MixedLinesSet.h index f692f47661..19bbcdfcd0 100644 --- a/include/geometry/MixedLinesSet.h +++ b/include/geometry/MixedLinesSet.h @@ -56,12 +56,11 @@ class MixedLinesSet : public std::vector /*! @brief Adds the given shared pointer to the set. The pointer reference count will be incremeted but no data is actually copied. */ void push_back(const PolylinePtr& line); - /*! @brief Adds a copy of all the polygons contained in the shape */ - void push_back(const Shape& shape); - /*! @brief Adds a copy of all the polygons contained in the set */ void push_back(const LinesSet& lines_set); + void push_back(LinesSet&& lines_set); + /*! @brief Adds a copy of all the polylines contained in the set */ void push_back(const OpenLinesSet& lines_set); @@ -71,7 +70,11 @@ class MixedLinesSet : public std::vector /*! @brief Adds a copy of all the polylines contained in the set */ void push_back(ClosedLinesSet&& lines_set); - /*! \brief Computes the total lenght of all the polylines in the set */ + void push_back(const MixedLinesSet& lines_set); + + void push_back(MixedLinesSet&& lines_set); + + /*! \brief Computes the total length of all the polylines in the set */ [[nodiscard]] coord_t length() const; }; diff --git a/include/geometry/Point2D.h b/include/geometry/Point2D.h index ee9ba55977..1f565a0f6c 100644 --- a/include/geometry/Point2D.h +++ b/include/geometry/Point2D.h @@ -46,7 +46,7 @@ class Point2D std::optional vNormalized() const { const double size = vSize(); - if (is_null(size)) + if (is_zero(size)) { return std::nullopt; } diff --git a/include/geometry/Point2LL.h b/include/geometry/Point2LL.h index 6993feb0f3..d5242a6ff7 100644 --- a/include/geometry/Point2LL.h +++ b/include/geometry/Point2LL.h @@ -194,16 +194,6 @@ INLINE coord_t cross(const Point2LL& p0, const Point2LL& p1) return p0.X * p1.Y - p0.Y * p1.X; } -INLINE double angle(const Point2LL& p) -{ - double angle = std::atan2(p.X, p.Y) / std::numbers::pi * 180.0; - if (angle < 0.0) - { - angle += 360.0; - } - return angle; -} - // Identity function, used to be able to make templated algorithms where the input is sometimes points, sometimes things that contain or can be converted to points. INLINE const Point2LL& make_point(const Point2LL& p) { @@ -227,6 +217,12 @@ inline Point2LL lerp(const Point2LL& a, const Point2LL& b, const double t) return Point2LL(lerp(a.X, b.X, t), lerp(a.Y, b.Y, t)); } +/*! Returns true if the points are equal or close enough to each other to be considered equal */ +inline bool fuzzy_equal(const Point2LL& a, const Point2LL& b) +{ + return fuzzy_equal(a.X, b.X) && fuzzy_equal(a.Y, b.Y); +} + } // namespace cura namespace std diff --git a/include/geometry/PointsSet.h b/include/geometry/PointsSet.h index 0a914254f6..22d329119b 100644 --- a/include/geometry/PointsSet.h +++ b/include/geometry/PointsSet.h @@ -84,6 +84,11 @@ class PointsSet push_back(other.points_.begin(), other.points_.end()); } + void push_back(PointsSet&& other) + { + points_.insert(points_.end(), std::make_move_iterator(other.points_.begin()), std::make_move_iterator(other.points_.end())); + } + void push_back(const const_iterator& begin, const const_iterator& end) { points_.insert(points_.end(), begin, end); diff --git a/include/geometry/Polygon.h b/include/geometry/Polygon.h index 987851ab8d..aef73a9c02 100644 --- a/include/geometry/Polygon.h +++ b/include/geometry/Polygon.h @@ -21,6 +21,10 @@ class AngleDegrees; class Polygon : public ClosedPolyline { public: + /*! + * \brief Builds an empty polygon + * \warning By default, the polygon is tagged as non explicitly closed + */ Polygon() = default; /*! diff --git a/include/geometry/Polyline.h b/include/geometry/Polyline.h index a26c942fcf..efb8371f65 100644 --- a/include/geometry/Polyline.h +++ b/include/geometry/Polyline.h @@ -97,6 +97,12 @@ class Polyline : public PointsSet /*! \brief Provides an end iterator to iterate over all the segments of the line */ [[nodiscard]] const_segments_iterator endSegments() const; + /*! \brief Provides a begin iterator to iterate over all the segments of the line */ + [[nodiscard]] const_segments_iterator cbeginSegments() const; + + /*! \brief Provides an end iterator to iterate over all the segments of the line */ + [[nodiscard]] const_segments_iterator cendSegments() const; + /*! \brief Provides a begin iterator to iterate over all the segments of the line */ segments_iterator beginSegments(); @@ -124,6 +130,23 @@ class Polyline : public PointsSet [[nodiscard]] bool shorterThan(const coord_t check_length) const; + /*! + * Adds a point to this set, but only if it forms a proper new segment i.r.t the current points in the set + * @param point The point to be added + * @return True if the point has actually been added, false otherwise + * @note The point will also be added if the current list is empty + */ + bool pushBackIfFormingSegment(const Point2LL& point) + { + if (getPoints().empty() || ! fuzzy_equal(point, getPoints().back())) + { + push_back(point); + return true; + } + + return false; + } + void reverse() { ClipperLib::ReversePath(getPoints()); diff --git a/include/settings/EnumSettings.h b/include/settings/EnumSettings.h index a2207fa414..8b9f57681d 100644 --- a/include/settings/EnumSettings.h +++ b/include/settings/EnumSettings.h @@ -31,19 +31,6 @@ enum class EFillMethod PLUGIN, // Place plugin after none to prevent it from being tested in the gtest suite. }; - -/*! - * Enum for the value of extra_infill_lines_to_support_skins - * This enum defines what extra lines should be added to infill to support - * skins above. - */ -enum class EExtraInfillLinesToSupportSkins -{ - WALLS_AND_LINES, - WALLS, - NONE, -}; - /*! * Type of platform adhesion. */ diff --git a/include/settings/MeshPathConfigs.h b/include/settings/MeshPathConfigs.h index e6cf6ca2ad..4990430e59 100644 --- a/include/settings/MeshPathConfigs.h +++ b/include/settings/MeshPathConfigs.h @@ -29,6 +29,7 @@ struct MeshPathConfigs GCodePathConfig flooring_config{}; std::vector infill_config{}; GCodePathConfig ironing_config{}; + GCodePathConfig skin_support_config{}; MeshPathConfigs(const SliceMeshStorage& mesh, const coord_t layer_thickness, const LayerIndex layer_nr, const std::vector& line_width_factor_per_extruder); void smoothAllSpeeds(const SpeedDerivatives& first_layer_config, const LayerIndex layer_nr, const LayerIndex max_speed_layer); diff --git a/include/settings/types/Angle.h b/include/settings/types/Angle.h index 8a4a438283..9c87471ee7 100644 --- a/include/settings/types/Angle.h +++ b/include/settings/types/Angle.h @@ -105,12 +105,12 @@ class AngleRadians /* * \brief Default constructor setting the angle to 0. */ - constexpr AngleRadians() noexcept = default; + AngleRadians() noexcept = default; /*! * \brief Converts an angle from degrees into radians. */ - constexpr AngleRadians(const AngleDegrees& value); + AngleRadians(const AngleDegrees& value); /* * \brief Translate the double value in degrees to an AngleRadians instance. @@ -166,7 +166,7 @@ inline AngleDegrees::AngleDegrees(const AngleRadians& value) { } -constexpr inline AngleRadians::AngleRadians(const AngleDegrees& value) +inline AngleRadians::AngleRadians(const AngleDegrees& value) : value_(value.value_ * TAU / 360.0) { } diff --git a/include/utils/Coord_t.h b/include/utils/Coord_t.h index 72c7597f57..caf77fdc07 100644 --- a/include/utils/Coord_t.h +++ b/include/utils/Coord_t.h @@ -16,6 +16,8 @@ namespace cura using coord_t = ClipperLib::cInt; +constexpr coord_t EPSILON = 5; + static inline coord_t operator""_mu(unsigned long long i) { return static_cast(i); @@ -36,6 +38,48 @@ template return std::llrint(std::lerp(static_cast(a), static_cast(b), t)); } +/*! Returns true if the given value is null or small enough to be considered null */ +[[nodiscard]] inline bool fuzzy_is_zero(const coord_t value) +{ + return std::abs(value) <= EPSILON; +} + +/*! Returns true if the given values are equal or close enough to be considered equal */ +[[nodiscard]] inline bool fuzzy_equal(const coord_t a, const coord_t b) +{ + return fuzzy_is_zero(b - a); +} + +/*! Returns true if the given values are not equal and different enough to be considered not equal */ +[[nodiscard]] inline bool fuzzy_not_equal(const coord_t a, const coord_t b) +{ + return ! fuzzy_equal(a, b); +} + +/*! Returns true if the given \a value is greater enough to \b to be considered greater */ +[[nodiscard]] inline bool fuzzy_is_greater(const coord_t a, const coord_t b) +{ + return a - b > EPSILON; +} + +/*! Returns true if the given \a value is greater or close enough to \b to be considered greater or equal */ +[[nodiscard]] inline bool fuzzy_is_greater_or_equal(const coord_t a, const coord_t b) +{ + return a > b - EPSILON; +} + +/*! Returns true if the given \a value is lesser enough to \b to be considered lesser */ +[[nodiscard]] inline bool fuzzy_is_lesser(const coord_t a, const coord_t b) +{ + return b - a > EPSILON; +} + +/*! Returns true if the given \a value is lesser or close enough to \b to be considered lesser or equal */ +[[nodiscard]] inline bool fuzzy_is_lesser_or_equal(const coord_t a, const coord_t b) +{ + return b > a - EPSILON; +} + } // namespace cura diff --git a/include/utils/SVG.h b/include/utils/SVG.h index 61b376e721..374d7fe5f8 100644 --- a/include/utils/SVG.h +++ b/include/utils/SVG.h @@ -20,6 +20,7 @@ namespace cura { class Point2D; +class MixedLinesSet; class SVG : NoCopy { @@ -85,6 +86,8 @@ class SVG : NoCopy struct SurfaceAttributes : ElementAttributes { + double alpha{ 1.0 }; + SurfaceAttributes() = default; SurfaceAttributes(const ColorObject& color) @@ -92,6 +95,12 @@ class SVG : NoCopy { } + SurfaceAttributes(const ColorObject& color, const double alpha) + : ElementAttributes(color) + , alpha(alpha) + { + } + ~SurfaceAttributes() override = default; }; @@ -227,6 +236,8 @@ class SVG : NoCopy void write(const ClosedPolyline& line, const VisualAttributes& visual_attributes, const bool flush = true) const; + void write(const MixedLinesSet& lines, const VisualAttributes& visual_attributes, const bool flush = true) const; + void write(const Point2LL& start, const Point2LL& end, const VisualAttributes& visual_attributes, const bool flush = true) const; void write(const PointsSet& points, const VerticesAttributes& visual_attributes, const bool flush = true) const; diff --git a/include/utils/linearAlg2D.h b/include/utils/linearAlg2D.h index 4720dca791..2bc60a1d60 100644 --- a/include/utils/linearAlg2D.h +++ b/include/utils/linearAlg2D.h @@ -86,8 +86,24 @@ class LinearAlg2D static bool lineLineIntersection(const Point2LL& a, const Point2LL& b, const Point2LL& c, const Point2LL& d, Point2LL& output); + /*! + * Computes the intersection between a line and a horizontal line + * @param p1 One point on the line + * @param p2 An other point on the line + * @param line_y The Y coordinates of the horizontal line to intersect with + * @return The x coordinate of the intersection, or nullopt if they don't intersect + */ static std::optional lineHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y); + /*! + * Computes the intersection between a segment and a horizontal line + * @param p1 The segment start point + * @param p2 The segment end point + * @param line_y The Y coordinates of the horizontal line to intersect with + * @return The x coordinate of the intersection, or nullopt if they don't intersect + */ + static std::optional segmentHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y); + /*! * Find whether a point projected on a line segment would be projected to * - properly on the line : zero returned diff --git a/include/utils/math.h b/include/utils/math.h index c5c44ed133..9c40a2193f 100644 --- a/include/utils/math.h +++ b/include/utils/math.h @@ -138,10 +138,20 @@ template /*! \brief Check if a value is very close to 0 */ template -[[nodiscard]] bool is_null(T value) +[[nodiscard]] bool is_zero(T value) { return std::abs(value) < (std::numeric_limits::epsilon() * 100.0); } +/*! + * Calculates the sign of a numeric value, 1 if positive and -1 if negative + * @note 0 is also considered as positive + */ +template +[[nodiscard]] int8_t sign(T value) +{ + return std::signbit(value) ? -1 : 1; +} + } // namespace cura #endif // UTILS_MATH_H diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index 0e9f3ab721..51603ab257 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -24,7 +24,7 @@ #include "PrimeTower/PrimeTower.h" #include "Slice.h" #include "WallToolPaths.h" -#include "bridge.h" +#include "bridge/bridge.h" #include "communication/Communication.h" //To send layer view data. #include "geometry/LinesSet.h" #include "geometry/OpenPolyline.h" @@ -41,7 +41,6 @@ namespace cura { -constexpr coord_t EPSILON = 5; const FffGcodeWriter::RoofingFlooringSettingsNames FffGcodeWriter::roofing_settings_names = { "roofing_extruder_nr", "roofing_pattern", "roofing_monotonic" }; const FffGcodeWriter::RoofingFlooringSettingsNames FffGcodeWriter::flooring_settings_names = { "flooring_extruder_nr", "flooring_pattern", "flooring_monotonic" }; @@ -2085,190 +2084,6 @@ bool FffGcodeWriter::processMultiLayerInfill( return added_something; } -// Return a set of parallel lines at a given angle within an area -// which cover a set of points -void getLinesForArea(OpenLinesSet& result_lines, const Shape& area, const AngleDegrees& angle, const PointsSet& points, coord_t line_width) -{ - OpenLinesSet candidate_lines; - Shape unused_skin_polygons; - std::vector unused_skin_paths; - - // We just want a set of lines which cover a Shape, and reusing this - // code seems like the best way. - Infill infill_comp(EFillMethod::LINES, false, false, area, line_width, line_width, 0, 1, angle, 0, 0, 0, 0); - - infill_comp.generate(unused_skin_paths, unused_skin_polygons, candidate_lines, {}, 0, SectionType::INFILL); - - // Select only lines which are needed to support points - for (const auto& line : candidate_lines) - { - for (const auto& point : points) - { - if (LinearAlg2D::getDist2FromLineSegment(line.front(), point, line.back()) <= (line_width / 2) * (line_width / 2)) - { - result_lines.push_back(line); - break; - } - } - } -} - -// Return a set of parallel lines within an area which -// fully support (cover) a set of points. -void getBestAngledLinesToSupportPoints(OpenLinesSet& result_lines, const Shape& area, const PointsSet& points, coord_t line_width) -{ - OpenLinesSet candidate_lines; - - struct CompareAngles - { - bool operator()(const AngleDegrees& a, const AngleDegrees& b) const - { - constexpr double small_angle = 5; - if (std::fmod(a - b + 360, 180) < small_angle) - { - return false; // Consider them as equal (near duplicates) - } - return (a < b); - } - }; - std::set candidate_angles; - - // heuristic that usually chooses a goodish angle - for (size_t i = 1; i < points.size(); i *= 2) - { - candidate_angles.insert(angle(points[i] - points[i / 2])); - } - - candidate_angles.insert({ 0, 90 }); - - for (const auto& angle : candidate_angles) - { - candidate_lines.clear(); - getLinesForArea(candidate_lines, area, angle, points, line_width); - if (candidate_lines.length() < result_lines.length() || result_lines.length() == 0) - { - result_lines = std::move(candidate_lines); - } - } -} - -// Add a supporting line by cutting a few existing lines. -// We do this because supporting lines are hanging over air, -// and therefore print best as part of a continuous print move, -// rather than having a travel move before and after them. -void integrateSupportingLine(OpenLinesSet& infill_lines, const OpenPolyline& line_to_add) -{ - // Returns the line index and the index of the point within an infill_line, null for no match found. - const auto findMatchingSegment = [&](const Point2LL& p) -> std::optional> - { - for (size_t i = 0; i < infill_lines.size(); ++i) - { - for (size_t j = 1; j < infill_lines[i].size(); ++j) - { - Point2LL closest_here = LinearAlg2D::getClosestOnLineSegment(p, infill_lines[i][j - 1], infill_lines[i][j]); - int64_t dist = vSize2(p - closest_here); - - if (dist < EPSILON * EPSILON) // rounding - { - return std::make_tuple(i, j); - } - } - } - return std::nullopt; - }; - - auto front_match = findMatchingSegment(line_to_add.front()); - auto back_match = findMatchingSegment(line_to_add.back()); - - if (front_match && back_match) - { - const auto& [front_line_index, front_point_index] = *front_match; - const auto& [back_line_index, back_point_index] = *back_match; - - if (front_line_index == back_line_index) - { - /* both ends intersect with the same line. - * If the inserted line has ends x, y - * and the original line was ...--A--(x)--B--...--C--(y)--D--... - * Then the new lines will be ...--A--x--y--C--...--B--x - * And line y--D--... - * Note that some parts of the line are reversed, - * and the last one is completly split apart. - */ - OpenPolyline& old_line = infill_lines[front_line_index]; - OpenPolyline new_line_start; - OpenPolyline new_line_end; - Point2LL x, y; - std::ptrdiff_t x_index, y_index; - if (front_point_index < back_point_index) - { - x = line_to_add.front(); - y = line_to_add.back(); - x_index = static_cast(front_point_index); - y_index = static_cast(back_point_index); - } - else - { - y = line_to_add.front(); - x = line_to_add.back(); - y_index = static_cast(front_point_index); - x_index = static_cast(back_point_index); - } - - new_line_start.insert(new_line_start.end(), old_line.begin(), old_line.begin() + x_index); - new_line_start.push_back(x); - new_line_start.push_back(y); - new_line_start.insert(new_line_start.end(), old_line.rend() - y_index, old_line.rend() - x_index); - new_line_start.push_back(x); - - new_line_end.push_back(y); - new_line_end.insert(new_line_end.end(), old_line.begin() + y_index, old_line.end()); - - old_line.setPoints(std::move(new_line_start.getPoints())); - infill_lines.push_back(new_line_end); - } - else - { - /* Different lines - * If the line_to_add has ends [front, back] - * Existing line (intersects front): ...--A--(x)--B--... - * Other existing line (intersects back): ...--C--(y)--D--... - * Result is Line: ...--A--x--y--D--... - * And line: x--B--... - * And line: ...--C--y - */ - OpenPolyline& old_front = infill_lines[front_line_index]; - OpenPolyline& old_back = infill_lines[back_line_index]; - OpenPolyline full_line, new_front, new_back; - const Point2LL x = line_to_add.front(); - const Point2LL y = line_to_add.back(); - const auto x_index = static_cast(front_point_index); - const auto y_index = static_cast(back_point_index); - - new_front.push_back(x); - new_front.insert(new_front.end(), old_front.begin() + x_index, old_front.end()); - - new_back.insert(new_back.end(), old_back.begin(), old_back.begin() + y_index); - new_back.push_back(y); - - full_line.insert(full_line.end(), old_front.begin(), old_front.begin() + x_index); - full_line.push_back(x); - full_line.push_back(y); - full_line.insert(full_line.end(), old_back.begin() + y_index, old_back.end()); - - old_front.setPoints(std::move(new_front.getPoints())); - old_back.setPoints(std::move(new_back.getPoints())); - infill_lines.push_back(full_line); - } - } - else - { - // One or other end touches something other than infill - // we will just suffer a travel move in this case - infill_lines.push_back(line_to_add); - } -} - void wall_tool_paths2lines(const std::vector>& wall_tool_paths, OpenLinesSet& result) { // We just want to grab all lines out of this datastructure @@ -2288,196 +2103,6 @@ void wall_tool_paths2lines(const std::vector>& w } } -/* Create a set of extra lines to support skins above. - * - * Skins above need to be held up. - * A straight line needs support just at the ends. - * A curve needs support at various points along the curve. - * - * The strategy here is to figure out is currently printed on - * this layer within the infill area by taking all currently printed - * lines and turning them into a giant hole-y shape. - * - * Then figure out what will be printed on the layer above - * (all extruded lines, walls, polygons, all combined). - * - * Then intersect these two things. For every 'hole', we 'simplify' - * the line through the hole, reducing curves to a few points. - * - * Then figure out extra infill_lines to add to support all points - * that lie within a hole. The extra lines will always be straight - * and will always go between existing infill lines. - * - * Results get added to infill_lines. - */ -void addExtraLinesToSupportSurfacesAbove( - OpenLinesSet& infill_lines, - const Shape& infill_polygons, - const std::vector>& wall_tool_paths, - const SliceLayerPart& part, - coord_t infill_line_width, - const LayerPlan& gcode_layer, - const SliceMeshStorage& mesh) -{ - // Where needs support? - - const auto enabled = mesh.settings.get("extra_infill_lines_to_support_skins"); - if (enabled == EExtraInfillLinesToSupportSkins::NONE) - { - return; - } - - const size_t skin_layer_nr = gcode_layer.getLayerNr() + 1 + mesh.settings.get("skin_edge_support_layers"); - if (skin_layer_nr >= mesh.layers.size()) - { - return; - } - - OpenLinesSet printed_lines_on_layer_above; - for (const SliceLayerPart& part_i : mesh.layers[skin_layer_nr].parts) - { - for (const SkinPart& skin_part : part_i.skin_parts) - { - OpenLinesSet skin_lines; - Shape skin_polygons; - std::vector skin_paths; - - AngleDegrees skin_angle = 45; - if (mesh.skin_angles.size() > 0) - { - skin_angle = mesh.skin_angles.at(skin_layer_nr % mesh.skin_angles.size()); - } - - // Approximation of the skin. - Infill infill_comp( - mesh.settings.get("top_bottom_pattern"), - false, - false, - skin_part.outline, - infill_line_width, - infill_line_width, - 0, - 1, - skin_angle, - 0, - 0, - 0, - 0, - mesh.settings.get("skin_outline_count"), - 0, - {}, - false); - infill_comp.generate(skin_paths, skin_polygons, skin_lines, mesh.settings, 0, SectionType::SKIN); - - wall_tool_paths2lines({ skin_paths }, printed_lines_on_layer_above); - if (enabled == EExtraInfillLinesToSupportSkins::WALLS_AND_LINES) - { - for (const Polygon& poly : skin_polygons) - { - printed_lines_on_layer_above.push_back(poly.toPseudoOpenPolyline()); - } - printed_lines_on_layer_above.push_back(skin_lines); - } - } - } - - /* move all points "inwards" by line_width to ensure a good overlap. - * Eg. Old Point New Point - * | | - * | X| - * -------X --------- - */ - for (OpenPolyline& poly : printed_lines_on_layer_above) - { - OpenPolyline copy = poly; - auto orig_it = poly.begin(); - for (auto it = copy.begin(); it != copy.end(); ++it, ++orig_it) - { - if (it > copy.begin()) - { - *orig_it += normal(*(it - 1) - *(it), infill_line_width / 2); - } - if (it < copy.end() - 1) - { - *orig_it += normal(*(it + 1) - *(it), infill_line_width / 2); - } - } - } - - if (printed_lines_on_layer_above.empty()) - { - return; - } - - // What shape is the supporting infill? - OpenLinesSet support_lines; - support_lines.push_back(infill_lines); - // The edge of the infill area is also considered supported - for (const auto& poly : part.getOwnInfillArea()) - { - support_lines.push_back(poly.toPseudoOpenPolyline()); - } - for (const auto& poly : infill_polygons) - { - support_lines.push_back(poly.toPseudoOpenPolyline()); - } - - // Infill walls can support the layer above - wall_tool_paths2lines(wall_tool_paths, support_lines); - - // Turn the lines into a giant shape. - Shape supported_area = support_lines.offset(infill_line_width / 2); - if (supported_area.empty()) - { - return; - } - - // invert the supported_area by adding one huge polygon around the outside - supported_area.push_back(AABB{ supported_area }.toPolygon()); - - const Shape inv_supported_area = supported_area.intersection(part.getOwnInfillArea()); - - OpenLinesSet unsupported_line_segments = inv_supported_area.intersection(printed_lines_on_layer_above); - - // This is to work around a rounding issue in the shape library with border points. - const Shape expanded_inv_supported_area = inv_supported_area.offset(-EPSILON); - - Simplify s{ MM2INT(1000), // max() doesnt work here, so just pick a big number. - infill_line_width, - std::numeric_limits::max() }; - // map each point into its area - std::map map; - - for (const OpenPolyline& a : unsupported_line_segments) - { - const OpenPolyline simplified = s.polyline(a); - for (const Point2LL& point : simplified) - { - size_t idx = expanded_inv_supported_area.findInside(point); - if (idx == NO_INDEX) - { - continue; - } - - map[idx].push_back(point); - } - } - - for (const auto& pair : map) - { - const Polygon& area = expanded_inv_supported_area[pair.first]; - const PointsSet& points = pair.second; - - OpenLinesSet result_lines; - getBestAngledLinesToSupportPoints(result_lines, Shape(area).offset(infill_line_width / 2 + EPSILON), points, infill_line_width); - - for (const auto& line : part.getOwnInfillArea().intersection(result_lines)) - { - integrateSupportingLine(infill_lines, line); - } - } -} - bool FffGcodeWriter::processSingleLayerInfill( const SliceDataStorage& storage, LayerPlan& gcode_layer, @@ -2504,6 +2129,8 @@ bool FffGcodeWriter::processSingleLayerInfill( Shape infill_polygons; std::vector> wall_tool_paths; // All wall toolpaths binned by inset_idx (inner) and by density_idx (outer) OpenLinesSet infill_lines; + OpenLinesSet skin_support_lines; + Shape skin_support_polygons; const auto pattern = mesh.settings.get("infill_pattern"); const bool zig_zaggify_infill = mesh.settings.get("zig_zaggify_infill") || pattern == EFillMethod::ZIG_ZAG; @@ -2514,6 +2141,9 @@ bool FffGcodeWriter::processSingleLayerInfill( const size_t last_idx = part.infill_area_per_combine_per_density.size() - 1; const auto max_resolution = mesh.settings.get("meshfix_maximum_resolution"); const auto max_deviation = mesh.settings.get("meshfix_maximum_deviation"); + const coord_t overlap = mesh.settings.get("infill_overlap_mm"); + const auto skin_support_density = mesh.settings.get("skin_support_density"); + const coord_t skin_support_line_distance = skin_support_density > 0.0 ? (infill_line_width / skin_support_density) : 0; AngleDegrees infill_angle = 45; // Original default. This will get updated to an element from mesh->infill_angles. if (! mesh.infill_angles.empty()) { @@ -2539,7 +2169,19 @@ bool FffGcodeWriter::processSingleLayerInfill( // boundary edge Shape infill_below_skin; Shape infill_not_below_skin; - const bool hasSkinEdgeSupport = partitionInfillBySkinAbove(infill_below_skin, infill_not_below_skin, gcode_layer, mesh, part, infill_line_width); + AngleDegrees skin_support_angle; + if (gcode_layer.getLayerNr() > 0) + { + partitionInfillBySkinAbove(infill_below_skin, infill_not_below_skin, gcode_layer, mesh, part, infill_line_width); + } + + if (! infill_below_skin.empty()) + { + const Shape infill_contour = part.infill_area.offset(-(infill_line_width / 2) + infill_overlap); + const LayerPlan* completed_layer_below = layer_plan_buffer.getCompletedLayerPlan(gcode_layer.getLayerNr() - 1); + std::tie(infill_below_skin, skin_support_angle) = makeBridgeOverInfillPrintable(infill_contour, infill_below_skin, mesh, completed_layer_below, gcode_layer.getLayerNr()); + infill_not_below_skin = infill_not_below_skin.difference(infill_below_skin); + } const auto pocket_size = mesh.settings.get("cross_infill_pocket_size"); constexpr bool skip_stitching = false; @@ -2608,29 +2250,34 @@ bool FffGcodeWriter::processSingleLayerInfill( } const bool fill_gaps = density_idx == 0; // Only fill gaps in the lowest infill density pattern. - if (hasSkinEdgeSupport) + if (! infill_below_skin.empty()) { - // 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; - const coord_t small_area_width = 0; + // infill region with skin above has to be printed bridge-like + constexpr EFillMethod skin_support_pattern = EFillMethod::LINES; + constexpr bool skin_support_zig_zaggify = false; + constexpr bool skin_support_connect_polygons = false; + constexpr size_t skin_support_infill_multiplier = 1; + constexpr size_t min_skin_support_wall_count = 0; + constexpr size_t skin_support_wall_count = 0; + constexpr coord_t small_area_width = 0; wall_tool_paths.emplace_back(std::vector()); - const coord_t overlap = infill_overlap - (density_idx == last_idx ? 0 : wall_line_count * infill_line_width); + constexpr coord_t skin_support_overlap = 0; + constexpr coord_t skin_support_infill_shift = 0; Infill infill_comp( - pattern, - zig_zaggify_infill, - connect_polygons, + skin_support_pattern, + skin_support_zig_zaggify, + skin_support_connect_polygons, infill_below_skin, infill_line_width, - infill_line_distance_here, - overlap, - infill_multiplier, - infill_angle, + skin_support_line_distance, + skin_support_overlap, + skin_support_infill_multiplier, + skin_support_angle, gcode_layer.z_, - infill_shift, + skin_support_infill_shift, max_resolution, max_deviation, - skin_below_wall_count, + skin_support_wall_count, small_area_width, infill_origin, skip_stitching, @@ -2642,8 +2289,8 @@ bool FffGcodeWriter::processSingleLayerInfill( pocket_size); infill_comp.generate( wall_tool_paths.back(), - infill_polygons, - infill_lines, + skin_support_polygons, + skin_support_lines, mesh.settings, gcode_layer.getLayerNr(), SectionType::INFILL, @@ -2652,11 +2299,12 @@ bool FffGcodeWriter::processSingleLayerInfill( &mesh); if (density_idx < last_idx) { - const coord_t cut_offset = get_cut_offset(zig_zaggify_infill, infill_line_width, min_skin_below_wall_count); + const coord_t cut_offset = get_cut_offset(skin_support_zig_zaggify, infill_line_width, min_skin_support_wall_count); Shape tool = infill_below_skin.offset(static_cast(cut_offset)); infill_lines_here = tool.intersection(infill_lines_here); } - infill_lines.push_back(infill_lines_here); + skin_support_lines.push_back(infill_lines_here); + // normal processing for the infill that isn't below skin in_outline = infill_not_below_skin; if (density_idx == last_idx) @@ -2678,7 +2326,6 @@ bool FffGcodeWriter::processSingleLayerInfill( constexpr size_t wall_line_count_here = 0; // Wall toolpaths were generated in generateGradualInfill for the sparsest density, denser parts don't have walls by default const coord_t small_area_width = 0; - const coord_t overlap = mesh.settings.get("infill_overlap_mm"); wall_tool_paths.emplace_back(); Infill infill_comp( @@ -2688,7 +2335,7 @@ bool FffGcodeWriter::processSingleLayerInfill( in_outline, infill_line_width, infill_line_distance_here, - overlap, + infill_overlap, infill_multiplier, infill_angle, gcode_layer.z_, @@ -2732,15 +2379,6 @@ bool FffGcodeWriter::processSingleLayerInfill( wall_tool_paths.emplace_back(part.infill_wall_toolpaths); // The extra infill walls were generated separately. Add these too. - if (mesh.settings.get("wall_line_count") // Disable feature if no walls - it can leave dangling lines at edges - && pattern != EFillMethod::LIGHTNING // Lightning doesn't make enclosed regions - && pattern != EFillMethod::CONCENTRIC // Doesn't handle 'holes' in infill lines very well - && pattern != EFillMethod::CROSS // Ditto - && pattern != EFillMethod::CROSS_3D) // Ditto - { - addExtraLinesToSupportSurfacesAbove(infill_lines, infill_polygons, wall_tool_paths, part, infill_line_width, gcode_layer, mesh); - } - const bool walls_generated = std::any_of( wall_tool_paths.cbegin(), wall_tool_paths.cend(), @@ -2756,7 +2394,7 @@ bool FffGcodeWriter::processSingleLayerInfill( return vwl.empty(); })); }); - if (! infill_lines.empty() || ! infill_polygons.empty() || walls_generated) + if (! infill_lines.empty() || ! infill_polygons.empty() || ! skin_support_lines.empty() || ! skin_support_polygons.empty() || walls_generated) { added_something = true; gcode_layer.setIsInside(true); // going to print stuff inside print object @@ -2865,11 +2503,59 @@ bool FffGcodeWriter::processSingleLayerInfill( start_move_inwards_length, end_move_inwards_length, infill_inner_contour); + + if (! skin_support_polygons.empty()) + { + constexpr bool extra_inwards_move = false; + gcode_layer + .addInfillPolygonsByOptimizer(skin_support_polygons, skin_support_lines, mesh_config.skin_support_config, mesh.settings, extra_inwards_move, near_start_location); + } + + const auto skin_support_fan_speed = mesh.settings.get("skin_support_fan_speed"); + constexpr SpaceFillType skin_support_space_fill_type = SpaceFillType::Lines; + constexpr coord_t skin_support_wipe_dist = 0; + const auto skin_support_interlace_lines = mesh.settings.get("skin_support_interlace_lines"); + if (skin_support_interlace_lines) + { + const coord_t max_adjacent_distance = skin_support_line_distance * 1.1; + constexpr coord_t exclude_distance = 0; + constexpr bool skin_support_interlaced = true; + gcode_layer.addLinesMonotonic( + infill_below_skin, + skin_support_lines, + mesh_config.skin_support_config, + skin_support_space_fill_type, + skin_support_angle, + max_adjacent_distance, + exclude_distance, + skin_support_wipe_dist, + flow_ratio, + skin_support_fan_speed, + skin_support_interlaced); + } + else + { + gcode_layer.addLinesByOptimizer( + skin_support_lines, + mesh_config.skin_support_config, + skin_support_space_fill_type, + enable_travel_optimization, + skin_support_wipe_dist, + flow_ratio, + near_start_location, + skin_support_fan_speed); + } + + MixedLinesSet all_infill_lines; + all_infill_lines.push_back(std::move(infill_lines)); + all_infill_lines.push_back(std::move(infill_polygons)); + gcode_layer.setGeneratedInfillLines(&mesh, all_infill_lines); } + return added_something; } -bool FffGcodeWriter::partitionInfillBySkinAbove( +void FffGcodeWriter::partitionInfillBySkinAbove( Shape& infill_below_skin, Shape& infill_not_below_skin, const LayerPlan& gcode_layer, @@ -2878,107 +2564,79 @@ bool FffGcodeWriter::partitionInfillBySkinAbove( coord_t infill_line_width) { constexpr coord_t tiny_infill_offset = 20; - const auto skin_edge_support_layers = mesh.settings.get("skin_edge_support_layers"); + const bool skin_support = mesh.settings.get("skin_support"); + + if (! skin_support) + { + return; + } + Shape skin_above_combined; // skin regions on the layers above combined with small gaps between - // working from the highest layer downwards, combine the regions of skin on all the layers - // but don't let the regions merge together - // otherwise "terraced" skin regions on separate layers will look like a single region of unbroken skin - for (size_t i = skin_edge_support_layers; i > 0; --i) + const size_t skin_layer_nr = gcode_layer.getLayerNr() + 1; + if (skin_layer_nr < mesh.layers.size()) { - const size_t skin_layer_nr = gcode_layer.getLayerNr() + i; - if (skin_layer_nr < mesh.layers.size()) + for (const SliceLayerPart& part_i : mesh.layers[skin_layer_nr].parts) { - for (const SliceLayerPart& part_i : mesh.layers[skin_layer_nr].parts) + for (const SkinPart& skin_part : part_i.skin_parts) { - for (const SkinPart& skin_part : part_i.skin_parts) - { - // Limit considered areas to the ones that should have infill underneath at the current layer. - const Shape relevant_outline = skin_part.outline.intersection(part.getOwnInfillArea()); + // Limit considered areas to the ones that should have infill underneath at the current layer. + const Shape relevant_outline = skin_part.outline.intersection(part.getOwnInfillArea()); - if (! skin_above_combined.empty()) + if (! skin_above_combined.empty()) + { + // does this skin part overlap with any of the skin parts on the layers above? + const Shape overlap = skin_above_combined.intersection(relevant_outline); + if (! overlap.empty()) { - // does this skin part overlap with any of the skin parts on the layers above? - const Shape overlap = skin_above_combined.intersection(relevant_outline); - if (! overlap.empty()) - { - // yes, it overlaps, need to leave a gap between this skin part and the others - if (i > 1) // this layer is the 2nd or higher layer above the layer whose infill we're printing - { - // looking from the side, if the combined regions so far look like this... - // - // ---------------------------------- - // - // and the new skin part looks like this... - // - // ------------------------------------- - // - // the result should be like this... - // - // ------- -------------------------- ---------- - - // expand the overlap region slightly to make a small gap - const Shape overlap_expanded = overlap.offset(tiny_infill_offset); - // subtract the expanded overlap region from the regions accumulated from higher layers - skin_above_combined = skin_above_combined.difference(overlap_expanded); - // subtract the expanded overlap region from this skin part and add the remainder to the overlap region - skin_above_combined.push_back(relevant_outline.difference(overlap_expanded)); - // and add the overlap area as well - skin_above_combined.push_back(overlap); - } - else // this layer is the 1st layer above the layer whose infill we're printing - { - // add this layer's skin region without subtracting the overlap but still make a gap between this skin region and what has been accumulated so - // far we do this so that these skin region edges will definitely have infill walls below them - - // looking from the side, if the combined regions so far look like this... - // - // ---------------------------------- - // - // and the new skin part looks like this... - // - // ------------------------------------- - // - // the result should be like this... - // - // ------- ------------------------------------- - - skin_above_combined = skin_above_combined.difference(relevant_outline.offset(tiny_infill_offset)); - skin_above_combined.push_back(relevant_outline); - } - } - else // no overlap - { - skin_above_combined.push_back(relevant_outline); - } + // yes, it overlaps, need to leave a gap between this skin part and the others + // add this layer's skin region without subtracting the overlap but still make a gap between this skin region and what has been accumulated so + // far we do this so that these skin region edges will definitely have infill walls below them + + // looking from the side, if the combined regions so far look like this... + // + // ---------------------------------- + // + // and the new skin part looks like this... + // + // ------------------------------------- + // + // the result should be like this... + // + // ------- ------------------------------------- + + skin_above_combined = skin_above_combined.difference(relevant_outline.offset(tiny_infill_offset)); + skin_above_combined.push_back(relevant_outline); } - else // this is the first skin region we have looked at + else // no overlap { skin_above_combined.push_back(relevant_outline); } } + else // this is the first skin region we have looked at + { + skin_above_combined.push_back(relevant_outline); + } } } + } - // the shrink/expand here is to remove regions of infill below skin that are narrower than the width of the infill walls otherwise the infill walls could merge and form - // a bump - infill_below_skin = skin_above_combined.intersection(part.infill_area_per_combine_per_density.back().front()).offset(-infill_line_width).offset(infill_line_width); + // the shrink/expand here is to remove regions of infill below skin that are narrower than the width of the infill walls otherwise the infill walls could merge and form + // a bump + infill_below_skin = skin_above_combined.intersection(part.infill_area_per_combine_per_density.back().front()).offset(-infill_line_width).offset(infill_line_width); - constexpr bool remove_small_holes_from_infill_below_skin = true; - constexpr double min_area_multiplier = 25; - const double min_area = INT2MM(infill_line_width) * INT2MM(infill_line_width) * min_area_multiplier; - infill_below_skin.removeSmallAreas(min_area, remove_small_holes_from_infill_below_skin); + constexpr bool remove_small_holes_from_infill_below_skin = true; + constexpr double min_area_multiplier = 25; + const double min_area = INT2MM(infill_line_width) * INT2MM(infill_line_width) * min_area_multiplier; + infill_below_skin.removeSmallAreas(min_area, remove_small_holes_from_infill_below_skin); - // there is infill below skin, is there also infill that isn't below skin? - infill_not_below_skin = part.infill_area_per_combine_per_density.back().front().difference(infill_below_skin); - infill_not_below_skin.removeSmallAreas(min_area); - } + // there is infill below skin, is there also infill that isn't below skin? + infill_not_below_skin = part.infill_area_per_combine_per_density.back().front().difference(infill_below_skin); + infill_not_below_skin.removeSmallAreas(min_area); // need to take skin/infill overlap that was added in SkinInfillAreaComputation::generateInfill() into account const coord_t infill_skin_overlap = mesh.settings.get((part.wall_toolpaths.size() > 1) ? "wall_line_width_x" : "wall_line_width_0") / 2; const Shape infill_below_skin_overlap = infill_below_skin.offset(-(infill_skin_overlap + tiny_infill_offset)); - - return ! infill_below_skin_overlap.empty() && ! infill_not_below_skin.empty(); } size_t FffGcodeWriter::findUsedExtruderIndex(const SliceDataStorage& storage, const LayerIndex& layer_nr, bool last) const diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index 60a129f06a..ed66c6dd0a 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -541,6 +541,21 @@ void LayerPlan::planPrime(double prime_blob_wipe_length) forceNewPathStart(); } +void LayerPlan::setGeneratedInfillLines(const SliceMeshStorage* mesh, const MixedLinesSet& infill_lines) +{ + infill_lines_[mesh].push_back(infill_lines); +} + +const MixedLinesSet LayerPlan::getGeneratedInfillLines(const SliceMeshStorage* mesh) const +{ + auto iterator = infill_lines_.find(mesh); + if (iterator != infill_lines_.end()) + { + return iterator->second; + } + return MixedLinesSet(); +} + void LayerPlan::addExtrusionMove( const Point3LL& p, const GCodePathConfig& config, diff --git a/src/LayerPlanBuffer.cpp b/src/LayerPlanBuffer.cpp index 4001f0837c..9771da05cf 100644 --- a/src/LayerPlanBuffer.cpp +++ b/src/LayerPlanBuffer.cpp @@ -3,6 +3,7 @@ #include "LayerPlanBuffer.h" +#include #include #include "Application.h" //To flush g-code through the communication channel. @@ -19,14 +20,11 @@ namespace cura constexpr Duration LayerPlanBuffer::extra_preheat_time_; -void LayerPlanBuffer::push(LayerPlan& layer_plan) -{ - buffer_.push_back(&layer_plan); -} - void LayerPlanBuffer::handle(LayerPlan& layer_plan, GCodeExport& gcode) { - push(layer_plan); + std::lock_guard mutex_locker(buffer_mutex_); + + buffer_.push_back(&layer_plan); LayerPlan* to_be_written = processBuffer(); if (to_be_written) @@ -34,6 +32,8 @@ void LayerPlanBuffer::handle(LayerPlan& layer_plan, GCodeExport& gcode) to_be_written->writeGCode(gcode); delete to_be_written; } + + buffer_condition_variable_.notify_all(); } LayerPlan* LayerPlanBuffer::processBuffer() @@ -78,6 +78,46 @@ void LayerPlanBuffer::flush() } } +const LayerPlan* LayerPlanBuffer::getCompletedLayerPlan(const LayerIndex& layer_nr) const +{ + const LayerPlan* result = nullptr; + + const auto search_layer_plan = [&layer_nr, this]() -> const LayerPlan* + { + const auto iterator = ranges::find_if( + buffer_, + [&layer_nr](const LayerPlan* layer_plan) + { + return layer_plan->getLayerNr() == layer_nr; + }); + + if (iterator != buffer_.end()) + { + return *iterator; + } + + return nullptr; + }; + + // The method has to be const in order to be called for the layer processing, however the multi-threading sync requires a non-const pointer. + auto noconst_this = const_cast(this); + while (true) + { + std::unique_lock mutex_locker(noconst_this->buffer_mutex_); + result = search_layer_plan(); + + if (result != nullptr) + { + break; + } + + // Wait for next finished layer plan + noconst_this->buffer_condition_variable_.wait(mutex_locker); + } + + return result; +} + void LayerPlanBuffer::addConnectingTravelMove(LayerPlan* prev_layer, const LayerPlan* newest_layer) { std::optional> new_layer_destination_state = newest_layer->getFirstTravelDestinationState(); diff --git a/src/PathOrderMonotonic.cpp b/src/PathOrderMonotonic.cpp index 9826b1c0a1..64ac70247e 100644 --- a/src/PathOrderMonotonic.cpp +++ b/src/PathOrderMonotonic.cpp @@ -6,6 +6,7 @@ #include //To track monotonic sequences. #include //To track starting points of monotonic sequences. +#include "geometry/OpenPolyline.h" namespace cura { @@ -432,5 +433,6 @@ std::vector::Path> PathOrderMonotonic::optimize(); +template void PathOrderMonotonic::optimize(); } // namespace cura diff --git a/src/bridge/ExpansionRange.cpp b/src/bridge/ExpansionRange.cpp new file mode 100644 index 0000000000..8553cdacc6 --- /dev/null +++ b/src/bridge/ExpansionRange.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "bridge/ExpansionRange.h" + +#include "bridge/SegmentOverlapping.h" +#include "bridge/SegmentOverlappingData.h" + + +namespace cura +{ + +std::optional ExpansionRange::calculateOverlapping(const TransformedSegment& other, const int8_t expand_direction) const +{ + if (is_projected_) + { + return data_.segment.calculateOverlapping(other, expand_direction); + } + + if (fuzzy_is_greater_or_equal(other.minY(), maxY()) || fuzzy_is_lesser_or_equal(other.maxY(), minY())) + { + // Not on the same horizontal band, or very slightly overlapping , discard + return std::nullopt; + } + + const SegmentOverlappingData overlapping(data_.range.min_y, data_.range.max_y, other.minY(), other.maxY(), nullptr, nullptr, other.getStart(), other.getEnd()); + + const bool overlap_top = (other.maxY() == data_.range.max_y); + const bool overlap_bottom = (other.minY() == data_.range.min_y); + + return SegmentOverlapping{ TransformedSegment::makeNonIntersectingOverlapping(overlap_top, overlap_bottom), overlapping.makeOtherOverlappingPart() }; +} + +} // namespace cura diff --git a/src/bridge/SegmentOverlappingData.cpp b/src/bridge/SegmentOverlappingData.cpp new file mode 100644 index 0000000000..e19360c627 --- /dev/null +++ b/src/bridge/SegmentOverlappingData.cpp @@ -0,0 +1,37 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "bridge/SegmentOverlappingData.h" + +#include "bridge/TransformedSegment.h" +#include "utils/linearAlg2D.h" + + +namespace cura +{ + +SegmentOverlappingData::SegmentOverlappingData( + const coord_t this_min_y, + const coord_t this_max_y, + const coord_t other_min_y, + const coord_t other_max_y, + const Point2LL* this_start, + const Point2LL* this_end, + const Point2LL& other_start, + const Point2LL& other_end) + : y_min(std::max(this_min_y, other_min_y)) + , y_max(std::min(this_max_y, other_max_y)) + , this_x_min(this_start != nullptr ? LinearAlg2D::lineHorizontalLineIntersection(*this_start, *this_end, y_min).value() : 0) + , this_x_max(this_start != nullptr ? LinearAlg2D::lineHorizontalLineIntersection(*this_start, *this_end, y_max).value() : 0) + , other_x_min(LinearAlg2D::lineHorizontalLineIntersection(other_start, other_end, y_min).value()) + , other_x_max(LinearAlg2D::lineHorizontalLineIntersection(other_start, other_end, y_max).value()) +{ +} + +/*! Make the cropped segment that is actually overlapping over the original segment, which may be part of all of the initial segment */ +TransformedSegment SegmentOverlappingData::makeOtherOverlappingPart() const +{ + return TransformedSegment(Point2LL(other_x_min, y_min), Point2LL(other_x_max, y_max)); +} + +} // namespace cura diff --git a/src/bridge/TransformedSegment.cpp b/src/bridge/TransformedSegment.cpp new file mode 100644 index 0000000000..acbc2add94 --- /dev/null +++ b/src/bridge/TransformedSegment.cpp @@ -0,0 +1,132 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "bridge/TransformedSegment.h" + +#include + +#include "bridge/SegmentOverlapping.h" +#include "bridge/SegmentOverlappingData.h" +#include "bridge/SegmentOverlappingType.h" +#include "geometry/PointMatrix.h" +#include "utils/linearAlg2D.h" +#include "utils/math.h" + + +namespace cura +{ + +TransformedSegment::TransformedSegment(const Point2LL& start, const Point2LL& end, const PointMatrix& matrix) + : TransformedSegment(matrix.apply(start), matrix.apply(end)) +{ +} + +void TransformedSegment::updateMinMax() +{ + std::tie(min_y_, max_y_) = std::minmax(start_.Y, end_.Y); +} + +void TransformedSegment::cropTop(const coord_t new_max_y_) +{ + setEnd(Point2LL(LinearAlg2D::lineHorizontalLineIntersection(start_, end_, new_max_y_).value_or(0), new_max_y_)); +} + +/*! Move the segment minimum Y coordinate up, actually cropping it */ +void TransformedSegment::cropBottom(const coord_t new_min_y_) +{ + setStart(Point2LL(LinearAlg2D::lineHorizontalLineIntersection(start_, end_, new_min_y_).value_or(0), new_min_y_)); +} + +std::optional TransformedSegment::calculateOverlapping(const TransformedSegment& other, const int8_t expand_direction) const +{ + if (fuzzy_is_greater_or_equal(other.min_y_, max_y_) || fuzzy_is_lesser_or_equal(other.max_y_, min_y_)) + { + // Not on the same horizontal band, or very slightly overlapping , discard + return std::nullopt; + } + + const SegmentOverlappingData overlapping(min_y_, max_y_, other.min_y_, other.max_y_, &start_, &end_, other.start_, other.end_); + + if (fuzzy_equal(overlapping.this_x_min, overlapping.other_x_min) && fuzzy_equal(overlapping.this_x_max, overlapping.other_x_max)) + { + // Segments are on top of each other, discard + return std::nullopt; + } + + int8_t sign_min = sign(overlapping.other_x_min - overlapping.this_x_min); + int8_t sign_max = sign(overlapping.other_x_max - overlapping.this_x_max); + const bool overlap_top = (overlapping.y_max == max_y_); + const bool overlap_bottom = (overlapping.y_min == min_y_); + + TransformedSegment line_part = overlapping.makeOtherOverlappingPart(); + + if (sign_min != sign_max) + { + // Segments are apparently intersecting each other + float intersection_segment; + float intersection_infill_line; + if (LinearAlg2D::segmentSegmentIntersection(start_, end_, other.start_, other.end_, intersection_segment, intersection_infill_line)) + { + const Point2LL intersection = lerp(start_, end_, intersection_segment); + if (intersection.Y >= overlapping.y_max - EPSILON) + { + // Intersection is very close from top, consider as if not intersecting + sign_max = sign_min; + } + else if (intersection.Y <= overlapping.y_min + EPSILON) + { + // Intersection is very close from bottom, consider as if not intersecting + sign_min = sign_max; + } + else + { + SegmentOverlappingType type; + + if (sign_max == expand_direction) + { + type = overlap_top ? SegmentOverlappingType::Top : SegmentOverlappingType::Middle; + line_part.setStart(intersection); + } + else + { + type = overlap_bottom ? SegmentOverlappingType::Bottom : SegmentOverlappingType::Middle; + line_part.setEnd(intersection); + } + + return SegmentOverlapping{ type, line_part }; + } + } + } + + if (sign_min != expand_direction) + { + // Segment is on the non-expanding side, discard + return std::nullopt; + } + else + { + return SegmentOverlapping{ makeNonIntersectingOverlapping(overlap_top, overlap_bottom), line_part }; + } +} + +SegmentOverlappingType TransformedSegment::makeNonIntersectingOverlapping(const bool overlap_top, const bool overlap_bottom) +{ + if (overlap_top && overlap_bottom) + { + return SegmentOverlappingType::Full; + } + else if (overlap_bottom) + { + return SegmentOverlappingType::Bottom; + } + else if (overlap_top) + { + return SegmentOverlappingType::Top; + } + else + { + return SegmentOverlappingType::Middle; + } +} + +} // namespace cura diff --git a/src/bridge/TransformedShape.cpp b/src/bridge/TransformedShape.cpp new file mode 100644 index 0000000000..03ffb5b1a2 --- /dev/null +++ b/src/bridge/TransformedShape.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2025 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "bridge/TransformedShape.h" + +#include "geometry/Shape.h" + + +namespace cura +{ + +TransformedShape::TransformedShape(const PointMatrix& matrix) + : matrix_(matrix) +{ +} + +TransformedShape::TransformedShape(const Shape& shape, const PointMatrix& matrix) + : TransformedShape(matrix) +{ + addShape(shape); +} + +TransformedShape::TransformedShape(const Polygon& polygon, const PointMatrix& matrix) + : TransformedShape(matrix) +{ + addPolygon(polygon); +} + +void TransformedShape::addShape(const Shape& shape, const bool filter_out_horizontal) +{ + segments_.reserve(segments_.size() + shape.pointCount()); + + for (const Polygon& polygon : shape) + { + constexpr bool reserve_size = false; + addPolygon(polygon, filter_out_horizontal, reserve_size); + } +} + + +void TransformedShape::addPolygon(const Polygon& polygon, const bool filter_out_horizontal, const bool reserve_size) +{ + if (reserve_size) + { + segments_.reserve(segments_.size() + polygon.size()); + } + + for (auto iterator = polygon.beginSegments(); iterator != polygon.endSegments(); ++iterator) + { + addSegment((*iterator).start, (*iterator).end, filter_out_horizontal); + } +} + +void TransformedShape::addSegment(const Point2LL& start, const Point2LL& end, const bool filter_out_horizontal) +{ + TransformedSegment segment(start, end, matrix_); + if (filter_out_horizontal && fuzzy_equal(segment.minY(), segment.maxY())) + { + return; + } + + min_y_ = std::min(min_y_, segment.minY()); + max_y_ = std::max(max_y_, segment.maxY()); + segments_.push_back(std::move(segment)); +} + +} // namespace cura diff --git a/src/bridge.cpp b/src/bridge/bridge.cpp similarity index 53% rename from src/bridge.cpp rename to src/bridge/bridge.cpp index 0206f6d6bf..e49943f7d0 100644 --- a/src/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -1,11 +1,15 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2025 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. -#include "bridge.h" +#include "bridge/bridge.h" #include +#include -#include "geometry/Point2D.h" +#include "LayerPlan.h" +#include "bridge/ExpansionRange.h" +#include "bridge/SegmentOverlapping.h" +#include "bridge/TransformedShape.h" #include "geometry/PointMatrix.h" #include "geometry/Polygon.h" #include "settings/EnumSettings.h" @@ -14,26 +18,12 @@ #include "utils/AABB.h" #include "utils/linearAlg2D.h" #include "utils/math.h" +#include "utils/types/geometry.h" namespace cura { -struct TransformedSegment -{ - Point2LL transformed_start; - Point2LL transformed_end; - coord_t min_y; - coord_t max_y; -}; - -struct TransformedShape -{ - std::vector segments; - coord_t min_y{ std::numeric_limits::max() }; - coord_t max_y{ std::numeric_limits::lowest() }; -}; - /*! * Calculates all the intersections between a horizontal line and the given transformed shape * @param line_y The horizontal line Y coordinate @@ -44,15 +34,15 @@ std::vector shapeLineIntersections(const coord_t line_y, const Transfor { std::vector intersections; - for (const TransformedSegment& transformed_segment : transformed_shape.segments) + for (const TransformedSegment& transformed_segment : transformed_shape.getSegments()) { - if (transformed_segment.min_y > line_y || transformed_segment.max_y < line_y) + if (transformed_segment.minY() > line_y || transformed_segment.maxY() < line_y) { // Segment is fully over or under the line, skip continue; } - const std::optional intersection = LinearAlg2D::lineHorizontalLineIntersection(transformed_segment.transformed_start, transformed_segment.transformed_end, line_y); + const std::optional intersection = LinearAlg2D::lineHorizontalLineIntersection(transformed_segment.getStart(), transformed_segment.getEnd(), line_y); if (intersection.has_value()) { intersections.push_back(intersection.value()); @@ -120,7 +110,7 @@ coord_t evaluateBridgeLine(const coord_t line_y, const TransformedShape& transfo const double next_intersection_skin_area = skin_outline_intersections.front(); const double next_intersection_supported_area = supported_regions_intersections.front(); - if (is_null(next_intersection_skin_area - next_intersection_supported_area)) + if (is_zero(next_intersection_skin_area - next_intersection_supported_area)) { next_intersection_is_skin_area = true; next_intersection_is_supported_area = true; @@ -211,35 +201,6 @@ coord_t evaluateBridgeLine(const coord_t line_y, const TransformedShape& transfo return segment_score; } -/*! - * Pre-transforms a shape according to the given matrix, so that the bridging angle ends up being horizontal - * @param shape The shape to be transformed - * @param matrix The transform matrix - * @return The shape transformed to the bridging orientation - */ -TransformedShape transformShape(const Shape& shape, const PointMatrix& matrix) -{ - TransformedShape transformed_shape; - transformed_shape.segments.reserve(shape.pointCount()); - - for (const Polygon& polygon : shape) - { - for (auto iterator = polygon.beginSegments(); iterator != polygon.endSegments(); ++iterator) - { - TransformedSegment transformed_segment; - transformed_segment.transformed_start = matrix.apply((*iterator).start); - transformed_segment.transformed_end = matrix.apply((*iterator).end); - transformed_segment.min_y = std::min(transformed_segment.transformed_start.Y, transformed_segment.transformed_end.Y); - transformed_segment.max_y = std::max(transformed_segment.transformed_start.Y, transformed_segment.transformed_end.Y); - transformed_shape.segments.push_back(transformed_segment); - transformed_shape.min_y = std::min(transformed_shape.min_y, transformed_segment.min_y); - transformed_shape.max_y = std::max(transformed_shape.max_y, transformed_segment.max_y); - } - } - - return transformed_shape; -} - /*! * Evaluate the bridging lines scoring for the given angle * @param skin_outline The skin outline to be filled @@ -251,22 +212,22 @@ coord_t evaluateBridgeLines(const Shape& skin_outline, const Shape& supported_re { // Transform the skin outline and supported regions according to the angle to speedup intersections calculations const PointMatrix matrix(angle); - const TransformedShape transformed_skin_area = transformShape(skin_outline, matrix); - const TransformedShape transformed_supported_area = transformShape(supported_regions, matrix); + const TransformedShape transformed_skin_area(skin_outline, matrix); + const TransformedShape transformed_supported_area(supported_regions, matrix); - if (transformed_skin_area.min_y >= transformed_skin_area.max_y || transformed_supported_area.min_y >= transformed_supported_area.max_y) + if (transformed_skin_area.minY() >= transformed_skin_area.maxY() || transformed_supported_area.minY() >= transformed_supported_area.maxY()) { return std::numeric_limits::lowest(); } - const size_t bridge_lines_count = (transformed_skin_area.max_y - transformed_skin_area.min_y) / line_width; + const size_t bridge_lines_count = (transformed_skin_area.maxY() - transformed_skin_area.minY()) / line_width; if (bridge_lines_count == 0) { // We cannot fit a single line in this direction, give up return std::numeric_limits::lowest(); } - const coord_t line_min = transformed_skin_area.min_y + line_width * 0.5; + const coord_t line_min = transformed_skin_area.minY() + line_width * 0.5; // Evaluated lines that could be properly bridging coord_t line_score = 0; @@ -274,7 +235,7 @@ coord_t evaluateBridgeLines(const Shape& skin_outline, const Shape& supported_re for (size_t i = 0; i < bridge_lines_count; ++i) { const coord_t line_y = line_min + i * line_width; - const bool has_supports = line_y >= transformed_supported_area.min_y && line_y <= transformed_supported_area.max_y; + const bool has_supports = line_y >= transformed_supported_area.minY() && line_y <= transformed_supported_area.maxY(); line_score += evaluateBridgeLine(line_y, transformed_skin_area, has_supports ? transformed_supported_area : empty_transformed_shape); } @@ -363,8 +324,6 @@ std::optional bridgeAngle( Shape prev_layer_outline; // we also want the complete outline of the previous layer Shape prev_layer_infill; - const Ratio sparse_infill_max_density = settings.get("bridge_sparse_infill_max_density"); - // include parts from all meshes for (const std::shared_ptr& mesh_ptr : storage.meshes) { @@ -372,9 +331,7 @@ std::optional bridgeAngle( if (mesh.isPrinted()) { const coord_t infill_line_distance = mesh.settings.get("infill_line_distance"); - const coord_t infill_line_width = mesh.settings.get("infill_line_width"); - double density = static_cast(infill_line_width) / static_cast(infill_line_distance); - const bool part_has_sparse_infill = (infill_line_distance == 0) || density <= sparse_infill_max_density; + const bool part_has_sparse_infill = infill_line_distance == 0; for (const SliceLayerPart& prev_layer_part : mesh.layers[layer_nr - bridge_layer].parts) { @@ -473,4 +430,274 @@ std::optional bridgeAngle( return best_angle.angle; } +/*! + * Make the expanded ranges for the given segment + * @param segment The segment to be expanded + * @param infill_lines_below The infill lines on the layer below that we can expand the segment to + * @param expand_direction The expand direction, 1 means expand to the right, -1 expand to the left + * @return The list of new expansion ranges, bottom to top, which covers the same vertical range as the segment + */ +std::vector makeExpandedRanges(const TransformedSegment& segment, const std::vector& infill_lines_below, const int8_t expand_direction) +{ + // List of expansion ranges, initiall filled with a single non-expanded range + std::vector expanded_ranges; + expanded_ranges.push_back(ExpansionRange(segment.minY(), segment.maxY(), &segment)); + + // Now loop on every infill line and update the expansion ranges accordingly + for (const TransformedSegment& infill_line_below : infill_lines_below) + { + // First calculate the overlapping of the infill line with the segment in the expand direction, so that we get only lines that (fully or partially) are on the expansion + // side of the segment + const std::optional overlapping = segment.calculateOverlapping(infill_line_below, expand_direction); + if (! overlapping.has_value()) + { + continue; + } + + // The line does overlap the expansion area of the segment, now checks whether it overlaps with the current ranges + const TransformedSegment& line_part = overlapping->other_overlapping_part; + + // The new list of expanded ranges being build + std::vector new_expanded_ranges; + + // The current expansion range being created, which can spread over multiple actual ranges + std::optional replacing_range; + + // Helper function to finalize and store the replacement range + const auto commit_replacing_range = [&replacing_range, &new_expanded_ranges]() + { + if (replacing_range.has_value()) + { + if (replacing_range->isValid()) + { + new_expanded_ranges.push_back(*replacing_range); + } + replacing_range.reset(); + } + }; + + for (const ExpansionRange& expanded_range : expanded_ranges) + { + // See if the line overlaps, but this time in reverse expand direction because we want those who are as close as possible to the initial segment + const std::optional range_overlapping = expanded_range.calculateOverlapping(line_part, -expand_direction); + if (! range_overlapping.has_value()) + { + commit_replacing_range(); + new_expanded_ranges.push_back(std::move(expanded_range)); + continue; + } + + if (range_overlapping->type != SegmentOverlappingType::Bottom && range_overlapping->type != SegmentOverlappingType::Full) + { + // The line overlaps, but not on the bottom part of the range, so keep the current range but crop its top part + commit_replacing_range(); + + ExpansionRange cropped_range_bottom = std::move(expanded_range); + cropped_range_bottom.cropTop(range_overlapping->other_overlapping_part.minY()); + if (cropped_range_bottom.isValid()) + { + new_expanded_ranges.push_back(std::move(cropped_range_bottom)); + } + } + + if (! replacing_range.has_value()) + { + // We don't have a replacing range yet, so since we are overlapping, create a new one starting a the current overlapping line start position + replacing_range = ExpansionRange(range_overlapping->other_overlapping_part, &infill_line_below); + } + else + { + // We have a replacing range, update it so that it now ends at the same position as the overlapping line end + replacing_range->setProjectedEnd(range_overlapping->other_overlapping_part.getEnd()); + } + + if (range_overlapping->type != SegmentOverlappingType::Top && range_overlapping->type != SegmentOverlappingType::Full) + { + // The line overlaps, but not on the top part of the range, so keep the current range but crop its bottom + commit_replacing_range(); + + ExpansionRange cropped_range_top = std::move(expanded_range); + cropped_range_top.cropBottom(range_overlapping->other_overlapping_part.maxY()); + if (cropped_range_top.isValid()) + { + new_expanded_ranges.push_back(std::move(cropped_range_top)); + } + } + } + + commit_replacing_range(); + + if (! new_expanded_ranges.empty()) + { + expanded_ranges = std::move(new_expanded_ranges); + } + } + + return expanded_ranges; +} + +/*! + * Update the expanded polygon area according to the given expansion ranges + * @param segment The segment being expanded + * @param expanded_ranges The expansion ranges calculated for the segment + * @param expand_direction The expand direction, 1 means expand to the right, -1 expand to the left + * @param expanded_polygon The expanded polygon to add the points to + * @param current_supporting_infill_line The infill line below currently supporting the bridging, which will be updated over iterations when necessary + */ +void updateExpandedPolygon( + const TransformedSegment& segment, + const std::vector& expanded_ranges, + const int8_t expand_direction, + Polygon& expanded_polygon, + const TransformedSegment*& current_supporting_infill_line) +{ + for (const ExpansionRange& expanded_range : expanded_ranges) + { + if (expanded_range.getSupportingSegment() == current_supporting_infill_line) + { + // As long as we have the same supporting infill line, don't have points because we just move along the same line + continue; + } + + if (current_supporting_infill_line == nullptr) + { + // This is the first iteration for this polygon, add the initial point + if (! expanded_range.isProjected()) + { + // This is an unitialized range, use the raw segment + expanded_polygon.push_back(segment.getStart()); + } + else + { + expanded_polygon.push_back(expand_direction > 0 ? expanded_range.getProjectedSegment().getStart() : expanded_range.getProjectedSegment().getEnd()); + } + } + else + { + Point2LL next_start_position; + + if (! expanded_range.isProjected()) + { + // This is an unitialized range, use the raw segment + const coord_t position_switch_y = expand_direction > 0 ? expanded_range.minY() : expanded_range.maxY(); + next_start_position = Point2LL(LinearAlg2D::lineHorizontalLineIntersection(segment.getStart(), segment.getEnd(), position_switch_y).value_or(0), position_switch_y); + } + else + { + next_start_position = expand_direction > 0 ? expanded_range.getProjectedSegment().getStart() : expanded_range.getProjectedSegment().getEnd(); + } + + // Add point to close anchoring to previous infill line + expanded_polygon.pushBackIfFormingSegment(Point2LL( + LinearAlg2D::lineHorizontalLineIntersection(current_supporting_infill_line->getStart(), current_supporting_infill_line->getEnd(), next_start_position.Y) + .value_or(0), + next_start_position.Y)); + + // Add point to start anchoring on new infill line + expanded_polygon.pushBackIfFormingSegment(next_start_position); + } + + current_supporting_infill_line = expanded_range.getSupportingSegment(); + } +} + +/*! + * Expands a segment of a polygon so that bridging will always have supporting infill lines below for proper anchoring + * @param segment The current segment to be expanded + * @param infill_lines_below The infill lines on the layer below + * @param expanded_polygon The resulting expanded polygon + * @param current_supporting_infill_line The infill line below currently supporting the bridging, which will be updated over iterations when necessary + * + * Before reading this explanation, see image in doc/bridging_skin_support.svg + * The expansion works by initially assigning an infinite range on the left (or right) of a segment, you can imagine a horizontal band starting from the segment. Then for each + * infill line, we check whether it crosses this range. When it is the case, we update the range so that the horizontal band is now divided in multiple parts, each of them + * being supported (or not) by an infill line. Then we do it again for the next infill line, which will update the ranges until we have the infill lines that are the closer to + * the segment. + */ +void expandSegment( + const TransformedSegment& segment, + const std::vector& infill_lines_below, + Polygon& expanded_polygon, + const TransformedSegment*& current_supporting_infill_line) +{ + if (fuzzy_equal(segment.minY(), segment.maxY())) + { + // Skip horizontal segments, holes will be filled by expanding their previous and next segments + return; + } + + // 1 means expand to the right, -1 expand to the left + const int8_t expand_direction = sign(segment.getEnd().Y - segment.getStart().Y); + + // Make the list of expanded ranges for this segment + std::vector expanded_ranges = makeExpandedRanges(segment, infill_lines_below, expand_direction); + + if (expand_direction < 0) + { + ranges::reverse(expanded_ranges); + } + + // Now we have the final expansion ranges, update the expanded polygon according. We add points only when the current supporting line is changing, so that the polygon is + // as compact as possible. + updateExpandedPolygon(segment, expanded_ranges, expand_direction, expanded_polygon, current_supporting_infill_line); +} + +std::tuple makeBridgeOverInfillPrintable( + const Shape& infill_contour, + const Shape& infill_below_skin_area, + const SliceMeshStorage& mesh, + const LayerPlan* completed_layer_plan_below, + const unsigned layer_nr) +{ + if (layer_nr == 0 || infill_below_skin_area.empty()) + { + return {}; + } + + // Calculate the proper bridging angle, according to the type of infill below + const AngleDegrees bridge_angle = bridgeOverInfillAngle(mesh, layer_nr); + + // We will expand the bridging area following the lines direction + const AngleDegrees expansion_angle = bridge_angle + 90; + const MixedLinesSet infill_lines_below = completed_layer_plan_below->getGeneratedInfillLines(&mesh); + + // Rotate all the infill lines below so that they are in a space where the under skin area should be expanded horizontally + const PointMatrix matrix(expansion_angle); + TransformedShape filtered_infill_lines_below(matrix); + + constexpr bool filter_out_horizontal = true; + for (const PolylinePtr& infill_line_below : infill_lines_below) + { + for (auto iterator = infill_line_below->cbeginSegments(); iterator != infill_line_below->cendSegments(); ++iterator) + { + filtered_infill_lines_below.addSegment((*iterator).start, (*iterator).end, filter_out_horizontal); + } + } + + filtered_infill_lines_below.addShape(infill_contour, filter_out_horizontal); + + // Now expand each polygon by expanding its segments horizontally according to the supporting infill lines + Shape transformed_expanded_infill_below_skin_area; + for (const Polygon& infill_below_skin_polygon : infill_below_skin_area) + { + const TransformedShape transformed_infill_below_skin_polygon(infill_below_skin_polygon, matrix); + + Polygon expanded_polygon; + const TransformedSegment* current_supporting_infill_line = nullptr; + for (const TransformedSegment& transformed_infill_below_skin_segment : transformed_infill_below_skin_polygon.getSegments()) + { + expandSegment(transformed_infill_below_skin_segment, filtered_infill_lines_below.getSegments(), expanded_polygon, current_supporting_infill_line); + } + transformed_expanded_infill_below_skin_area.push_back(std::move(expanded_polygon)); + } + + // Move output polygons back to original transform + transformed_expanded_infill_below_skin_area.applyMatrix(matrix.inverse()); + + // Perform a morphological closing to remove overlapping lines + transformed_expanded_infill_below_skin_area = transformed_expanded_infill_below_skin_area.offset(EPSILON).offset(-EPSILON).intersection(infill_contour); + + return { transformed_expanded_infill_below_skin_area, bridge_angle }; +} + } // namespace cura diff --git a/src/geometry/MixedLinesSet.cpp b/src/geometry/MixedLinesSet.cpp index 8b0d4a5455..6e952f2c8f 100644 --- a/src/geometry/MixedLinesSet.cpp +++ b/src/geometry/MixedLinesSet.cpp @@ -131,6 +131,16 @@ void MixedLinesSet::push_back(ClosedLinesSet&& lines_set) } } +void MixedLinesSet::push_back(const MixedLinesSet& lines_set) +{ + insert(end(), lines_set.begin(), lines_set.end()); +} + +void MixedLinesSet::push_back(MixedLinesSet&& lines_set) +{ + insert(end(), std::make_move_iterator(lines_set.begin()), std::make_move_iterator(lines_set.end())); +} + void MixedLinesSet::push_back(const LinesSet& lines_set) { reserve(size() + lines_set.size()); @@ -140,9 +150,13 @@ void MixedLinesSet::push_back(const LinesSet& lines_set) } } -void MixedLinesSet::push_back(const Shape& shape) +void MixedLinesSet::push_back(LinesSet&& lines_set) { - push_back(static_cast&>(shape)); + reserve(size() + lines_set.size()); + for (Polygon& line : lines_set) + { + push_back(std::move(line)); + } } coord_t MixedLinesSet::length() const diff --git a/src/geometry/Polyline.cpp b/src/geometry/Polyline.cpp index c476f8e461..fcbab0f876 100644 --- a/src/geometry/Polyline.cpp +++ b/src/geometry/Polyline.cpp @@ -108,6 +108,16 @@ Polyline::const_segments_iterator Polyline::endSegments() const return const_segments_iterator(size() > 1 ? std::prev(end()) : end(), begin(), end()); } +Polyline::const_segments_iterator Polyline::cbeginSegments() const +{ + return beginSegments(); +} + +Polyline::const_segments_iterator Polyline::cendSegments() const +{ + return endSegments(); +} + Polyline::segments_iterator Polyline::beginSegments() { return segments_iterator(begin(), begin(), end()); diff --git a/src/settings/MeshPathConfigs.cpp b/src/settings/MeshPathConfigs.cpp index 7c9edf4a0f..ec402742f8 100644 --- a/src/settings/MeshPathConfigs.cpp +++ b/src/settings/MeshPathConfigs.cpp @@ -152,6 +152,13 @@ MeshPathConfigs::MeshPathConfigs(const SliceMeshStorage& mesh, const coord_t lay .speed_derivatives = { .speed = mesh.settings.get("speed_ironing"), .acceleration = mesh.settings.get("acceleration_ironing"), .jerk = mesh.settings.get("jerk_ironing") } } + , skin_support_config{ .type = PrintFeatureType::Infill, + .line_width = mesh.settings.get("infill_line_width"), + .layer_thickness = layer_thickness, + .flow = mesh.settings.get("skin_support_material_flow"), + .speed_derivatives = { .speed = mesh.settings.get("skin_support_speed"), + .acceleration = mesh.settings.get("acceleration_infill"), + .jerk = mesh.settings.get("jerk_infill") } } { infill_config.reserve(MAX_INFILL_COMBINE); diff --git a/src/settings/Settings.cpp b/src/settings/Settings.cpp index 29b7a8218a..7eafe41703 100644 --- a/src/settings/Settings.cpp +++ b/src/settings/Settings.cpp @@ -469,24 +469,6 @@ EPlatformAdhesion Settings::get(const std::string& key) const } } -template<> -EExtraInfillLinesToSupportSkins Settings::get(const std::string& key) const -{ - const std::string& value = get(key); - using namespace cura::utils; - switch (hash_enum(value)) - { - case "walls_and_lines"_sw: - return EExtraInfillLinesToSupportSkins::WALLS_AND_LINES; - case "walls"_sw: - return EExtraInfillLinesToSupportSkins::WALLS; - case "none"_sw: - return EExtraInfillLinesToSupportSkins::NONE; - default: - return EExtraInfillLinesToSupportSkins::WALLS_AND_LINES; - } -} - template<> ESupportType Settings::get(const std::string& key) const { diff --git a/src/utils/SVG.cpp b/src/utils/SVG.cpp index 605ca7be4e..63eafa7e18 100644 --- a/src/utils/SVG.cpp +++ b/src/utils/SVG.cpp @@ -13,6 +13,7 @@ #include #include +#include "geometry/MixedLinesSet.h" #include "geometry/OpenPolyline.h" #include "geometry/Point2D.h" #include "geometry/Polygon.h" @@ -289,8 +290,9 @@ void SVG::write(const LinesSet& lines, const VisualAttributes& visual_ { fprintf( out_, - "(line)) + { + write(*open_polyline, visual_attributes, false); + } + else if (auto closed_polyline = dynamic_pointer_cast(line)) + { + write(*closed_polyline, visual_attributes, false); + } + } + + handleFlush(flush); +} + void SVG::write(const PointsSet& points, const VerticesAttributes& visual_attributes, const bool flush) const { for (const Point2LL& point : points) diff --git a/src/utils/linearAlg2D.cpp b/src/utils/linearAlg2D.cpp index 71c883de74..d3263a0fb0 100644 --- a/src/utils/linearAlg2D.cpp +++ b/src/utils/linearAlg2D.cpp @@ -321,21 +321,43 @@ bool LinearAlg2D::lineLineIntersection(const Point2LL& a, const Point2LL& b, con std::optional LinearAlg2D::lineHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y) { + if (p1.Y == p2.Y) + { + // Line is also horizontal, can't find a proper intersection + return std::nullopt; + } + if (p1.X == p2.X) { - // Line is purely vertical + // Line is vertical return p1.X; } - const double coeff_a = static_cast(p2.Y - p1.Y) / (p2.X - p1.X); - if (is_null(coeff_a)) + if (line_y == p1.Y) { - // Other line is also horizontal - return std::nullopt; + return p1.X; + } + if (line_y == p2.Y) + { + return p2.X; } + const double coeff_a = static_cast(p2.Y - p1.Y) / (p2.X - p1.X); const double coeff_b = p1.Y - coeff_a * p1.X; return std::llrint((line_y - coeff_b) / coeff_a); } +std::optional LinearAlg2D::segmentHorizontalLineIntersection(const Point2LL& p1, const Point2LL& p2, const coord_t line_y) +{ + const auto [min_y, max_y] = std::minmax(p1.Y, p2.Y); + + if (min_y > line_y || max_y < line_y) + { + // Segment is over or under the line + return std::nullopt; + } + + return lineHorizontalLineIntersection(p1, p2, line_y); +} + } // namespace cura diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c79194cef4..c1d9f52574 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,8 +29,10 @@ set(TESTS_SRC_SETTINGS set(TESTS_SRC_UTILS AABBTest AABB3DTest + CoordTTest IntPointTest LinearAlg2DTest + MathTest MinimumSpanningTreeTest PolygonConnectorTest PolygonTest diff --git a/tests/FffGcodeWriterTest.cpp b/tests/FffGcodeWriterTest.cpp index 7f07a1216e..44701202b4 100644 --- a/tests/FffGcodeWriterTest.cpp +++ b/tests/FffGcodeWriterTest.cpp @@ -29,7 +29,7 @@ namespace cura /*! * Fixture that provides a basis for testing wall computation. */ -class FffGcodeWriterTest : public testing::Test +class DISABLED_FffGcodeWriterTest : public testing::Test { public: Settings* settings; @@ -39,7 +39,7 @@ class FffGcodeWriterTest : public testing::Test // Square that fits wholly inside the above square Shape inner_square; - FffGcodeWriterTest() + DISABLED_FffGcodeWriterTest() : fff_gcode_writer() { outer_square.emplace_back(); @@ -68,7 +68,8 @@ class FffGcodeWriterTest : public testing::Test std::ifstream file(path); std::string line; - while (std::getline(file, line)) { + while (std::getline(file, line)) + { size_t pos = line.find('='); std::string key = line.substr(0, pos); std::string value = line.substr(pos + 1); @@ -97,7 +98,7 @@ class FffGcodeWriterTest : public testing::Test } }; -TEST_F(FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt) +TEST_F(DISABLED_FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt) { // SETUP SliceDataStorage* storage = setUpStorage(); @@ -114,10 +115,10 @@ TEST_F(FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt) Mesh mesh(*settings); - LayerPlan gcode_layer(*storage, 100, 10000, 100, 0, {fan_settings}, 20, 10, 5000 ); + LayerPlan gcode_layer(*storage, 100, 10000, 100, 0, { fan_settings }, 20, 10, 5000); SliceMeshStorage mesh_storage(&mesh, 200); size_t extruder_nr = 0; - MeshPathConfigs mesh_config(mesh_storage, 10, 100, {0.5}); + MeshPathConfigs mesh_config(mesh_storage, 10, 100, { 0.5 }); SliceLayerPart part; part.infill_area_per_combine_per_density = { { outer_square } }; @@ -134,14 +135,7 @@ TEST_F(FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt) // We're expecting the sparse infill on layer 100 to have // some lines to support the corners of the layer above. // But we arent wanting the whole area densely supported. - fff_gcode_writer.processSingleLayerInfill( - *storage, - gcode_layer, - mesh_storage, - extruder_nr, - mesh_config, - part - ); + fff_gcode_writer.processSingleLayerInfill(*storage, gcode_layer, mesh_storage, extruder_nr, mesh_config, part); /* Useful code if you're debugging this test. Also add this test as a friend in GCodeExport.h GCodeExport gcode_export; @@ -152,14 +146,18 @@ TEST_F(FffGcodeWriterTest, SurfaceGetsExtraInfillLinesUnderIt) */ // Test helper - auto checkPointIsPassed = [&](Point2LL p, coord_t margin)-> bool { + auto checkPointIsPassed = [&](Point2LL p, coord_t margin) -> bool + { Point2LL last; - for (const auto& path:gcode_layer.extruder_plans_[0].paths_) { - for (const auto& point: path.points) { + for (const auto& path : gcode_layer.extruder_plans_[0].paths_) + { + for (const auto& point : path.points) + { Point2LL closest_here = LinearAlg2D::getClosestOnLineSegment(p, point.toPoint2LL(), last); int64_t dist = vSize2(p - closest_here); - if (dist + +#include + +#include "utils/Coord_t.h" + +namespace cura +{ +// NOLINTBEGIN(*-magic-numbers) + +TEST(CoordTTest, TestFuzzyZero) +{ + EXPECT_EQ(fuzzy_is_zero(0), true); + EXPECT_EQ(fuzzy_is_zero(2), true); + EXPECT_EQ(fuzzy_is_zero(-2), true); + EXPECT_EQ(fuzzy_is_zero(10), false); + EXPECT_EQ(fuzzy_is_zero(-10), false); +} + +TEST(CoordTTest, TestFuzzyEqual) +{ + EXPECT_EQ(fuzzy_equal(0, 0), true); + EXPECT_EQ(fuzzy_equal(0, 3), true); + EXPECT_EQ(fuzzy_equal(-2, 2), true); + EXPECT_EQ(fuzzy_equal(-2, 5), false); + + EXPECT_EQ(fuzzy_equal(100, 100), true); + EXPECT_EQ(fuzzy_equal(100, 102), true); + EXPECT_EQ(fuzzy_equal(100, 97), true); + EXPECT_EQ(fuzzy_equal(100, 110), false); + EXPECT_EQ(fuzzy_equal(100, 90), false); + + EXPECT_EQ(fuzzy_equal(-100, -100), true); + EXPECT_EQ(fuzzy_equal(-100, -102), true); + EXPECT_EQ(fuzzy_equal(-100, -97), true); + EXPECT_EQ(fuzzy_equal(-100, -110), false); + EXPECT_EQ(fuzzy_equal(-100, -90), false); +} + +TEST(CoordTTest, TestFuzzyNotEqual) +{ + EXPECT_EQ(fuzzy_not_equal(0, 0), false); + EXPECT_EQ(fuzzy_not_equal(0, 3), false); + EXPECT_EQ(fuzzy_not_equal(-2, 2), false); + EXPECT_EQ(fuzzy_not_equal(-2, 5), true); + + EXPECT_EQ(fuzzy_not_equal(100, 100), false); + EXPECT_EQ(fuzzy_not_equal(100, 102), false); + EXPECT_EQ(fuzzy_not_equal(100, 97), false); + EXPECT_EQ(fuzzy_not_equal(100, 110), true); + EXPECT_EQ(fuzzy_not_equal(100, 90), true); + + EXPECT_EQ(fuzzy_not_equal(-100, -100), false); + EXPECT_EQ(fuzzy_not_equal(-100, -102), false); + EXPECT_EQ(fuzzy_not_equal(-100, -97), false); + EXPECT_EQ(fuzzy_not_equal(-100, -110), true); + EXPECT_EQ(fuzzy_not_equal(-100, -90), true); +} + +TEST(CoordTTest, TestFuzzyGreater) +{ + EXPECT_EQ(fuzzy_is_greater(0, 0), false); + EXPECT_EQ(fuzzy_is_greater(0, 3), false); + EXPECT_EQ(fuzzy_is_greater(-2, 2), false); + EXPECT_EQ(fuzzy_is_greater(-2, 5), false); + EXPECT_EQ(fuzzy_is_greater(5, -2), true); + EXPECT_EQ(fuzzy_is_greater(10, 1), true); + + EXPECT_EQ(fuzzy_is_greater(100, 100), false); + EXPECT_EQ(fuzzy_is_greater(100, 102), false); + EXPECT_EQ(fuzzy_is_greater(100, 97), false); + EXPECT_EQ(fuzzy_is_greater(100, 110), false); + EXPECT_EQ(fuzzy_is_greater(100, 90), true); + + EXPECT_EQ(fuzzy_is_greater(-100, -100), false); + EXPECT_EQ(fuzzy_is_greater(-100, -102), false); + EXPECT_EQ(fuzzy_is_greater(-100, -97), false); + EXPECT_EQ(fuzzy_is_greater(-100, -110), true); + EXPECT_EQ(fuzzy_is_greater(-100, -90), false); +} + +TEST(CoordTTest, TestFuzzyGreaterOrEqual) +{ + EXPECT_EQ(fuzzy_is_greater_or_equal(0, 0), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(0, 3), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-2, 2), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-2, 5), false); + EXPECT_EQ(fuzzy_is_greater_or_equal(5, -2), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(10, 1), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(1, 10), false); + + EXPECT_EQ(fuzzy_is_greater_or_equal(100, 100), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(100, 102), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(100, 97), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(100, 110), false); + EXPECT_EQ(fuzzy_is_greater_or_equal(100, 90), true); + + EXPECT_EQ(fuzzy_is_greater_or_equal(-100, -100), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-100, -102), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-100, -97), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-100, -110), true); + EXPECT_EQ(fuzzy_is_greater_or_equal(-100, -90), false); +} + +TEST(CoordTTest, TestFuzzyLesser) +{ + EXPECT_EQ(fuzzy_is_lesser(0, 0), false); + EXPECT_EQ(fuzzy_is_lesser(0, 3), false); + EXPECT_EQ(fuzzy_is_lesser(-2, 2), false); + EXPECT_EQ(fuzzy_is_lesser(-2, 5), true); + EXPECT_EQ(fuzzy_is_lesser(5, -2), false); + EXPECT_EQ(fuzzy_is_lesser(10, 1), false); + + EXPECT_EQ(fuzzy_is_lesser(100, 100), false); + EXPECT_EQ(fuzzy_is_lesser(100, 102), false); + EXPECT_EQ(fuzzy_is_lesser(100, 97), false); + EXPECT_EQ(fuzzy_is_lesser(100, 110), true); + EXPECT_EQ(fuzzy_is_lesser(100, 90), false); + + EXPECT_EQ(fuzzy_is_lesser(-100, -100), false); + EXPECT_EQ(fuzzy_is_lesser(-100, -102), false); + EXPECT_EQ(fuzzy_is_lesser(-100, -97), false); + EXPECT_EQ(fuzzy_is_lesser(-100, -110), false); + EXPECT_EQ(fuzzy_is_lesser(-100, -90), true); +} + +TEST(CoordTTest, TestFuzzyLesserOrEqual) +{ + EXPECT_EQ(fuzzy_is_lesser_or_equal(0, 0), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(0, 3), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-2, 2), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-2, 5), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(5, -2), false); + EXPECT_EQ(fuzzy_is_lesser_or_equal(10, 1), false); + EXPECT_EQ(fuzzy_is_lesser_or_equal(1, 10), true); + + EXPECT_EQ(fuzzy_is_lesser_or_equal(100, 100), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(100, 102), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(100, 97), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(100, 110), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(100, 90), false); + + EXPECT_EQ(fuzzy_is_lesser_or_equal(-100, -100), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-100, -102), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-100, -97), true); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-100, -110), false); + EXPECT_EQ(fuzzy_is_lesser_or_equal(-100, -90), true); +} + +// NOLINTEND(*-magic-numbers) + +} // namespace cura