diff --git a/CMakeLists.txt b/CMakeLists.txt index a62b6eecda..fb31abca05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,8 +18,9 @@ option(ENABLE_MORE_COMPILER_OPTIMIZATION_FLAGS "Enable more optimization flags" option(USE_SYSTEM_LIBS "Use the system libraries if available" OFF) option(OLDER_APPLE_CLANG "Apple Clang <= 13 used" OFF) option(ENABLE_THREADING "Enable threading support" ON) +option(ENABLE_CURAVIZ "Build with CuraViz toolbox" ON) -if (${ENABLE_ARCUS} OR ${ENABLE_PLUGINS}) +if (${ENABLE_ARCUS} OR ${ENABLE_PLUGINS} OR ${ENABLE_CURAVIZ}) find_package(protobuf REQUIRED) endif () @@ -38,6 +39,11 @@ if (${ENABLE_ARCUS}) protobuf_generate_cpp(engine_PB_SRCS engine_PB_HEADERS Cura.proto) endif () +message(STATUS "Building with CuraViz: ${ENABLE_CURAVIZ}") +if (${ENABLE_CURAVIZ}) + find_package(curaviz_grpc_definitions REQUIRED) +endif () + ### Compiling CuraEngine ### # First compile all of CuraEngine as library. @@ -157,6 +163,7 @@ set(engine_SRCS # Except main.cpp. src/utils/AABB3D.cpp src/utils/channel.cpp src/utils/Segment3LL.cpp + src/utils/CuraViz.cpp src/utils/Date.cpp src/utils/ExtrusionJunction.cpp src/utils/ExtrusionLine.cpp @@ -193,6 +200,7 @@ set(engine_SRCS # Except main.cpp. src/utils/scoring/RandomScoringCriterion.cpp src/utils/scoring/TextureScoringCriterion.cpp + src/geometry/ClosedLinesSet.cpp src/geometry/OpenLinesSet.cpp src/geometry/Point2LL.cpp src/geometry/Point3LL.cpp @@ -228,6 +236,7 @@ target_compile_definitions(_CuraEngine PUBLIC $<$:ARCUS> $<$:ENABLE_PLUGINS> + $<$:ENABLE_CURAVIZ> $<$,$>:ENABLE_REMOTE_PLUGINS> $<$:OLDER_APPLE_CLANG> CURA_ENGINE_VERSION=\"${CURA_ENGINE_VERSION}\" @@ -290,6 +299,7 @@ target_link_libraries(_CuraEngine $<$>:onetbb::onetbb> $<$:semver::semver> $<$:curaengine_grpc_definitions::curaengine_grpc_definitions> + $<$:curaviz_grpc_definitions::curaviz_grpc_definitions> $<$:asio-grpc::asio-grpc> $<$:grpc::grpc> $<$:protobuf::libprotobuf> diff --git a/conandata.yml b/conandata.yml index c4d6f0338b..7120dbd622 100644 --- a/conandata.yml +++ b/conandata.yml @@ -12,3 +12,5 @@ requirements_plugins: - "curaengine_grpc_definitions/0.4.1" requirements_cura_resources: - "cura_resources/5.14.0-alpha.0@ultimaker/testing" +requirements_curaviz: + - "curaviz_grpc_definitions/1.0.0" diff --git a/conanfile.py b/conanfile.py index 25065b793b..ac9183194d 100644 --- a/conanfile.py +++ b/conanfile.py @@ -33,6 +33,7 @@ class CuraEngineConan(ConanFile): "enable_plugins": [True, False], "enable_remote_plugins": [True, False], "with_cura_resources": [True, False], + "with_curaviz": [True, False], } default_options = { "enable_arcus": True, @@ -41,6 +42,7 @@ class CuraEngineConan(ConanFile): "enable_plugins": True, "enable_remote_plugins": False, "with_cura_resources": False, + "with_curaviz": False } @property @@ -89,7 +91,7 @@ def config_options(self): def configure(self): super().configure() - if self.options.enable_arcus or self.options.enable_plugins: + if self.options.enable_arcus or self.options.enable_plugins or self.options.with_curaviz: self.options["protobuf"].shared = False if self.options.enable_arcus: self.options["arcus"].shared = True @@ -140,6 +142,9 @@ def requirements(self): if self.options.with_cura_resources: for req in self.conan_data["requirements_cura_resources"]: self.requires(req) + if self.options.with_curaviz: + for req in self.conan_data["requirements_curaviz"]: + self.requires(req) self.requires("clipper/6.4.2@ultimaker/stable") self.requires("boost/1.88.0") self.requires("rapidjson/cci.20230929") @@ -171,6 +176,7 @@ def generate(self): tc.variables["ENABLE_REMOTE_PLUGINS"] = self.options.enable_remote_plugins else: tc.variables["ENABLE_PLUGINS"] = self.options.enable_plugins + tc.variables["ENABLE_CURAVIZ"] = self.options.with_curaviz self.setup_cmake_toolchain_sentry(tc) tc.generate() diff --git a/include/FffGcodeWriter.h b/include/FffGcodeWriter.h index 0da487aaec..60e0d33be0 100644 --- a/include/FffGcodeWriter.h +++ b/include/FffGcodeWriter.h @@ -222,12 +222,22 @@ class FffGcodeWriter : public NoCopy * calling it for multiple layers results in the skirt/brim being printed on multiple layers. * * \param storage where the slice data is stored. - * \param gcodeLayer The initial planning of the g-code of the layer. + * \param gcode_layer The initial planning of the g-code of the layer. * \param extruder_nr The extruder train for which to process the skirt or * brim. * \param layer_nr The index of the layer to write the gcode of. */ - void processSkirtBrim(const SliceDataStorage& storage, LayerPlan& gcodeLayer, unsigned int extruder_nr, LayerIndex layer_nr) const; + void processSkirtBrim(const SliceDataStorage& storage, LayerPlan& gcode_layer, const unsigned int extruder_nr, const LayerIndex layer_nr) const; + + /*! + * Add the support brim to the layer plan + * + * \param storage where the slice data is stored. + * \param gcode_layer The initial planning of the g-code of the layer. + * \param extruder_nr The extruder train for which to process the brim. + * \param layer_nr The index of the layer to write the gcode of. + */ + static void processSupportBrim(const SliceDataStorage& storage, LayerPlan& gcode_layer, const unsigned int extruder_nr, const LayerIndex layer_nr); /*! * Adds the ooze shield to the layer plan \p gcodeLayer. diff --git a/include/SkirtBrim.h b/include/SkirtBrim.h index ac0787ac00..8553fdc645 100644 --- a/include/SkirtBrim.h +++ b/include/SkirtBrim.h @@ -220,6 +220,24 @@ class SkirtBrim */ std::vector generateAllowedAreas(const std::vector& starting_outlines) const; + /*! + * Generates the inside part of the support brim + * @param settings The settings to be used when print this part + * @param support_outline The outline of the support to be used as the base of the brim + * @param width The maximum inside brim width to be generated + * @param line_width The line width used to print the support inside brim + */ + void generateSupportInsideBrim(const Settings& settings, const Shape& support_outline, const size_t width, const size_t line_width); + + /*! + * Generates the outside part of the support brim + * @param support_outline The outline of the support to be used as the base of the brim + * @param width The maximum inside brim width to be generated + * @param line_width The line width used to print the support inside brim + * @param exclusion_area The exclusion areas on which the brim is not allowed to grow + */ + void generateSupportOutsideBrim(const Shape& support_outline, const size_t width, const size_t line_width, const Shape& exclusion_area); + public: /*! * Generate the brim which is printed from the outlines of the support inward. diff --git a/include/SupportInfillPart.h b/include/SupportInfillPart.h index c864a67be6..223ee8443d 100644 --- a/include/SupportInfillPart.h +++ b/include/SupportInfillPart.h @@ -4,6 +4,7 @@ #ifndef SUPPORT_INFILL_PART_H #define SUPPORT_INFILL_PART_H +#include #include #include "geometry/Polygon.h" @@ -23,10 +24,11 @@ namespace cura * Because support is handled as a whole in the engine, that is, we have a global support areas instead of support areas for each mesh. * With this data structure, we can keep track of which gradual support infill areas belongs to which support area, so we can print them together. */ -class SupportInfillPart +struct SupportInfillPart { -public: SingleShape outline_; //!< The outline of the support infill area + std::optional base_outside_contour_; //!< Outline contour including the outer base + std::optional base_inside_contour_; //!< If an inside base is generated, contains the inner contour AABB outline_boundary_box_; //!< The boundary box for the infill area coord_t support_line_width_; //!< The support line width int inset_count_to_generate_; //!< The number of insets need to be generated from the outline. This is not the actual insets that will be generated. @@ -38,16 +40,8 @@ class SupportInfillPart bool use_fractional_config_; //!< Request to use the configuration used to fill a partial layer height here, instead of the normal full layer height configuration. SupportInfillPart(const SingleShape& outline, coord_t support_line_width, bool use_fractional_config, int inset_count_to_generate = 0, coord_t custom_line_distance = 0); - - const Shape& getInfillArea() const; }; -inline const Shape& SupportInfillPart::getInfillArea() const -{ - // if there is no wall, we use the original outline as the infill area - return outline_; -} - } // namespace cura #endif // SUPPORT_INFILL_PART_H diff --git a/include/WallToolPathGenerator.h b/include/WallToolPathGenerator.h new file mode 100644 index 0000000000..cc3017129b --- /dev/null +++ b/include/WallToolPathGenerator.h @@ -0,0 +1,18 @@ +// Copyright (c) 2026 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher + +#ifndef CURAENGINE_WALLTOOLPATHGENERATOR_H +#define CURAENGINE_WALLTOOLPATHGENERATOR_H + +namespace cura +{ + +enum class WallToolPathGenerator +{ + Arachne, // Use arachne to generate smart extrusion tool paths with variable line width + NaiveInset // Use a naive implementation to generate tool paths with constant line width +}; + +} + +#endif diff --git a/include/WallToolPaths.h b/include/WallToolPaths.h index 845b0f0c1c..f311e06d42 100644 --- a/include/WallToolPaths.h +++ b/include/WallToolPaths.h @@ -4,6 +4,7 @@ #ifndef CURAENGINE_WALLTOOLPATHS_H #define CURAENGINE_WALLTOOLPATHS_H +#include "WallToolPathGenerator.h" #include "geometry/Polygon.h" #include "settings/Settings.h" #include "utils/ExtrusionLine.h" @@ -29,7 +30,8 @@ class WallToolPaths const coord_t wall_0_inset, const Settings& settings, const int layer_idx, - SectionType section_type); + SectionType section_type, + WallToolPathGenerator generator = WallToolPathGenerator::Arachne); /*! * A class that creates the toolpaths given an outline, nominal bead width and maximum amount of walls @@ -118,6 +120,13 @@ class WallToolPaths */ static void simplifyToolPaths(std::vector& toolpaths, const Settings& settings); +private: + /*! Generates the toolpaths using the Arachne engine, which produces extrusions paths with variable line width */ + void generateArachne(); + + /*! Generates the toolpaths using a naive inset, which produces extrusions paths with a fixed line width */ + void generateNaiveInset(); + private: const Shape& outline_; // -class LinesSet; -class ClosedPolyline; +class MixedLinesSet; /*! - * \brief Convenience definition for a container that can hold only closed polylines. This makes it - * explicit what the lines actually represent. + * \brief Container that can hold only closed polylines. This makes it explicit what the lines actually represent and adds some processing functions that can only be applied + * to closes polylines. */ -using ClosedLinesSet = LinesSet; +class ClosedLinesSet : public LinesSet +{ +public: + ClosedLinesSet() = default; + + ClosedLinesSet(LinesSet&& other) + : LinesSet(std::move(other)) + { + } + + explicit ClosedLinesSet(const std::initializer_list& initializer) + : LinesSet(initializer) + { + } + + explicit ClosedLinesSet(const ClosedPolyline& line) + : LinesSet(line) + { + } + + [[nodiscard]] MixedLinesSet intersection(const Shape& shape) const; + + [[nodiscard]] MixedLinesSet difference(const Shape& shape) const; +}; } // namespace cura diff --git a/include/geometry/ClosedPolyline.h b/include/geometry/ClosedPolyline.h index cc8c2c320f..55572e79f6 100644 --- a/include/geometry/ClosedPolyline.h +++ b/include/geometry/ClosedPolyline.h @@ -10,6 +10,8 @@ namespace cura { class OpenPolyline; +class MixedLinesSet; +class Shape; /*! @brief This describes a polyline which forms a closed path. * @sa https://github.com/Ultimaker/CuraEngine/wiki/Geometric-Base-Types#closedpolyline @@ -53,7 +55,11 @@ class ClosedPolyline : public Polyline ClosedPolyline(const ClosedPolyline& other) = default; /*! \brief Constructor that takes ownership of the inner points list from the given polyline */ - ClosedPolyline(ClosedPolyline&& other) = default; + ClosedPolyline(ClosedPolyline&& other) noexcept + : Polyline{ std::move(other) } + , explicitly_closed_(other.explicitly_closed_) + { + } /*! * \brief Constructor with a points initializer list, provided for convenience @@ -99,6 +105,8 @@ class ClosedPolyline : public Polyline /*! @see Polyline::isValid() */ [[nodiscard]] bool isValid() const override; + void addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const override; + ClosedPolyline& operator=(const ClosedPolyline& other) = default; ClosedPolyline& operator=(ClosedPolyline&& other) = default; diff --git a/include/geometry/LinesSet.h b/include/geometry/LinesSet.h index 66daf9cc51..13390fb43b 100644 --- a/include/geometry/LinesSet.h +++ b/include/geometry/LinesSet.h @@ -263,13 +263,6 @@ class LinesSet */ void addPaths(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const; - /*! - * \brief Utility method to add a line to a ClipperLib::Clipper object - * \note This method needs to be public but you shouldn't need to use it from outside - */ - template - void addPath(ClipperLib::Clipper& clipper, const OtherLineLine& line, ClipperLib::PolyType poly_typ) const; - /*! * \brief Utility method to add all the lines to a ClipperLib::ClipperOffset object * \note This method needs to be public but you shouldn't need to use it from outside diff --git a/include/geometry/MixedLinesSet.h b/include/geometry/MixedLinesSet.h index fc4a978e58..501e20bb24 100644 --- a/include/geometry/MixedLinesSet.h +++ b/include/geometry/MixedLinesSet.h @@ -6,13 +6,13 @@ #include -#include "geometry/ClosedLinesSet.h" #include "utils/Coord_t.h" namespace cura { class ClosedPolyline; +class ClosedLinesSet; class OpenLinesSet; class OpenPolyline; class Polygon; @@ -22,6 +22,9 @@ class Shape; using PolylinePtr = std::shared_ptr; using OpenPolylinePtr = std::shared_ptr; +template +class LinesSet; + /*! * \brief Convenience definition for a container that can hold any type of polyline. * \sa https://github.com/Ultimaker/CuraEngine/wiki/Geometric-Base-Types#mixedlinesset @@ -29,6 +32,12 @@ using OpenPolylinePtr = std::shared_ptr; class MixedLinesSet : public std::vector { public: + MixedLinesSet() = default; + + MixedLinesSet(const ClosedLinesSet& lines); + + MixedLinesSet(ClipperLib::PolyTree&& tree); + /*! * \brief Computes the offset of all the polylines contained in the set. The polylines may * be of different types, and polylines are polygons are treated differently. @@ -39,9 +48,16 @@ class MixedLinesSet : public std::vector */ [[nodiscard]] Shape offset(coord_t distance, ClipperLib::JoinType join_type = ClipperLib::jtMiter, double miter_limit = 1.2) const; + MixedLinesSet intersection(const Shape& shape) const; + + MixedLinesSet difference(const Shape& shape) const; + /*! @brief Adds a copy of the given polyline to the set */ void push_back(const OpenPolyline& line); + /*! @brief Adds a copy of the given polyline to the set */ + void push_back(const ClosedPolyline& line); + /*! @brief Adds a copy of the given polyline to the set */ void push_back(const Polygon& line); @@ -68,6 +84,9 @@ class MixedLinesSet : public std::vector /*! @brief Adds a copy of all the polylines contained in the set */ void push_back(OpenLinesSet&& lines_set); + /*! @brief Adds a copy of all the polylines contained in the set */ + void push_back(const ClosedLinesSet& lines_set); + /*! @brief Adds a copy of all the polylines contained in the set */ void push_back(ClosedLinesSet&& lines_set); diff --git a/include/geometry/OpenPolyline.h b/include/geometry/OpenPolyline.h index 2a6b0faef2..5183ef8c7b 100644 --- a/include/geometry/OpenPolyline.h +++ b/include/geometry/OpenPolyline.h @@ -84,6 +84,11 @@ class OpenPolyline : public Polyline return size() >= 2; } + void addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const final + { + clipper.AddPath(getPoints(), poly_typ, false); + } + OpenPolyline& operator=(OpenPolyline&& other) noexcept = default; OpenPolyline& operator=(const OpenPolyline& other) = default; diff --git a/include/geometry/Polygon.h b/include/geometry/Polygon.h index aef73a9c02..c01e3c5875 100644 --- a/include/geometry/Polygon.h +++ b/include/geometry/Polygon.h @@ -74,6 +74,8 @@ class Polygon : public ClosedPolyline ~Polygon() override = default; + void addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const final; + Polygon& operator=(const Polygon& other) = default; Polygon& operator=(Polygon&& other) noexcept = default; diff --git a/include/geometry/Polyline.h b/include/geometry/Polyline.h index ad3791c6a8..66b9acd21f 100644 --- a/include/geometry/Polyline.h +++ b/include/geometry/Polyline.h @@ -87,6 +87,8 @@ class Polyline : public PointsSet */ [[nodiscard]] virtual bool isValid() const = 0; + virtual void addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const = 0; + Polyline& operator=(const Polyline& other) = default; Polyline& operator=(Polyline&& other) = default; diff --git a/include/geometry/Shape.h b/include/geometry/Shape.h index 0c4c0acfe0..3a1f4dc26e 100644 --- a/include/geometry/Shape.h +++ b/include/geometry/Shape.h @@ -136,14 +136,6 @@ class Shape : public LinesSet */ [[nodiscard]] size_t findInside(const Point2LL& p, bool border_result = false) const; - /*! - * \brief Approximates the convex hull of the polygons. - * \p extra_outset Extra offset outward - * \return the convex hull (approximately) - * - */ - [[nodiscard]] Shape approxConvexHull(int extra_outset = 0) const; - /*! \brief Make each of the polygons convex */ void makeConvex(); diff --git a/include/infill.h b/include/infill.h index cdea66e14d..406f56fb01 100644 --- a/include/infill.h +++ b/include/infill.h @@ -8,6 +8,7 @@ #include +#include "WallToolPathGenerator.h" #include "geometry/LinesSet.h" #include "geometry/Point2LL.h" #include "infill/LightningGenerator.h" @@ -201,6 +202,7 @@ class Infill * \param mesh A mesh for which to generate infill (should only be used for non-helper-mesh objects). * \param[in] cross_fill_provider The cross fractal subdivision decision functor * \param minimum_line_length The per-island minimum total length for the generated lines + * \param wall_generator The generator to use for outer "walls" */ void generate( std::vector& toolpaths, @@ -213,7 +215,8 @@ class Infill const std::shared_ptr& lightning_layer = nullptr, const SliceMeshStorage* mesh = nullptr, const Shape& prevent_small_exposed_to_air = Shape(), - const coord_t minimum_line_length = 0); + const coord_t minimum_line_length = 0, + WallToolPathGenerator wall_generator = WallToolPathGenerator::Arachne); coord_t getLineDistance() const { @@ -229,6 +232,7 @@ class Infill * \param wall_line_count [in] The number of walls that needs to be generated * \param line_width [in] The optimum wall line width of the walls * \param settings [in] A settings storage to use for generating variable-width walls. + * \param generator The generator to use for outer "walls" * \return The inner contour of the wall toolpaths */ static Shape generateWallToolPaths( @@ -238,7 +242,8 @@ class Infill const coord_t line_width, const Settings& settings, int layer_idx, - SectionType section_type); + SectionType section_type, + WallToolPathGenerator generator = WallToolPathGenerator::Arachne); /*! * Get the inner infill contour diff --git a/include/settings/types/LayerIndex.h b/include/settings/types/LayerIndex.h index 9e2100da87..e6d2835e5e 100644 --- a/include/settings/types/LayerIndex.h +++ b/include/settings/types/LayerIndex.h @@ -203,6 +203,11 @@ constexpr auto format_as(LayerIndex index) return index.value; } +constexpr LayerIndex distance(const LayerIndex& lhs, const LayerIndex& rhs) +{ + return rhs - lhs; +} + } // namespace cura namespace std diff --git a/include/sliceDataStorage.h b/include/sliceDataStorage.h index 930165aee8..f37c0abc46 100644 --- a/include/sliceDataStorage.h +++ b/include/sliceDataStorage.h @@ -11,6 +11,7 @@ #include "SupportInfillPart.h" #include "TopSurface.h" #include "WipeScriptConfig.h" +#include "geometry/ClosedLinesSet.h" #include "geometry/MixedLinesSet.h" #include "geometry/OpenLinesSet.h" #include "geometry/Point2LL.h" @@ -220,6 +221,7 @@ class SupportLayer Shape support_mesh; //!< Areas from support meshes which should NOT be supported by more support Shape anti_overhang; //!< Areas where no overhang should be detected. Shape force_overhang; //!< Areas where overhang should be forced. + MixedLinesSet base; //!< Extra lines to be printed around for sturdiness. /*! * Exclude the given polygons from the support infill areas and update the SupportInfillParts. @@ -382,7 +384,7 @@ class SliceDataStorage : public NoCopy SupportStorage support; std::vector skirt_brim[MAX_EXTRUDERS]; //!< Skirt/brim polygons per extruder, ordered from inner to outer polygons. - ClosedLinesSet support_brim; //!< brim lines for support, going from the edge of the support inward. \note Not ordered by inset. + MixedLinesSet support_brim; //!< brim lines for support, inside and outside. \note Not ordered by inset. // Storage for the outline of the raft-parts. Will be filled with lines when the GCode is generated. Shape raft_base_outline; @@ -420,6 +422,7 @@ class SliceDataStorage : public NoCopy * \param include_models Whether to include the models in the outline * \param external_polys_only Whether to disregard all hole polygons. * \param extruder_nr (optional) only give back outlines for this extruder (where the walls are printed with this extruder) + * \param include_support_base (optional) include the support base (requires include_support). If false, only the raw support outline is returned. */ Shape getLayerOutlines( const LayerIndex layer_nr, @@ -427,7 +430,8 @@ class SliceDataStorage : public NoCopy const bool include_prime_tower, const bool external_polys_only = false, const int extruder_nr = -1, - const bool include_models = true) const; + const bool include_models = true, + const bool include_support_base = true) const; /*! * Get the axis-aligned bounding-box of the complete model (all meshes). diff --git a/include/support.h b/include/support.h index 0d6eaeffa6..1c785fe048 100644 --- a/include/support.h +++ b/include/support.h @@ -68,6 +68,12 @@ class AreaSupport */ static void generateSupportInfillFeatures(SliceDataStorage& storage); + /*! + * Generate the extra lines to be printed around the support to form a base and make it sturdier + * @param storage data storage containing the input layer outline data and containing the output support storage per layer + */ + static void generateSupportBase(SliceDataStorage& storage); + private: /*! * Splits the global support areas into separete SupportInfillParts. diff --git a/include/utils/CuraViz.h b/include/utils/CuraViz.h new file mode 100644 index 0000000000..8592441590 --- /dev/null +++ b/include/utils/CuraViz.h @@ -0,0 +1,104 @@ +// Copyright (c) 2026 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#ifndef CURAVIZ_H +#define CURAVIZ_H +#ifdef ENABLE_CURAVIZ + +#include + +#include "geometry/Point2LL.h" + +namespace cura_viz +{ +class Message; +class GeometricElement; +class Polyline2LL; +} // namespace cura_viz + +namespace cura +{ + +class MixedLinesSet; +class Shape; +class Polyline; + +/*! + * Interface to send internal CuraEngine data to an external visualization tool. All the public methods are static and can be used directly, without + * having to care about any kind of initialization or even multi-threading context. Just do something like: + * + * Shape outer_contour; + * std::vector final_print_lines; + * .... + * .... + * CuraViz::send(outer_contour, "contour"); + * CuraViz::send(final_print_lines, "final_lines"); + * + * Geometric objects (or sequence of objects) can be named using the "name" argument of each function. The "step_name" can be used to differentiate a same logical object that + * has changed over time, or to mention that multiple objects have been added in a batch: + * + * Shape outer_contour; + * CuraViz::send(outer_contour, "contour", "start"); + * + * outer_contour = outer_contour.offset(100); + * CuraViz::send(outer_contour, "contour", "enlargement"); + * + * Shape disallowed_areas = makeDisallowedAreas(); + * outer_contour = outer_contour.difference(disallowed_areas); + * CuraViz::send(outer_contour, "contour", "remove_disallowed"); + * CuraViz::send(disallowed_areas, "disallowed", "remove_disallowed"); + * + */ +class CuraViz +{ +public: + static void send(const Point2LL& point, const std::string& name = "", const std::string& step_name = ""); + + static void send(const Shape& shape, const std::string& name = "", const std::string& step_name = ""); + + static void send(const std::vector& shapes, const std::string& name = "", const std::string& step_name = ""); + + static void send(const MixedLinesSet& lines_set, const std::string& name = "", const std::string& step_name = ""); + + static void send(const std::vector& lines_sets, const std::string& name = "", const std::string& step_name = ""); + +private: + /*! Convenience class that stores the message and handles its actual sending and destruction when appropriate */ + class MessageToSend + { + public: + explicit MessageToSend(const std::string& step_name); + + virtual ~MessageToSend(); + + cura_viz::GeometricElement* addGeometricElement(const std::string& element_name); + + private: + std::shared_ptr message_; + }; + + CuraViz(); + + void send(const cura_viz::Message& message, const bool should_lock = true); + + static CuraViz* getInstance(); + + static void setup(const Shape& shape, cura_viz::GeometricElement* element); + + static void setup(const MixedLinesSet& lines, cura_viz::GeometricElement* element); + + static void setup(const Point2LL& point, cura_viz::GeometricElement* element); + + static void setup(const Polyline& polyline, cura_viz::Polyline2LL* polyline_message); + +private: + static CuraViz* instance_; + static std::mutex mutex_; + + asio::io_context io_context_; + asio::ip::tcp::socket socket_; +}; + +} // namespace cura +#endif +#endif diff --git a/include/utils/ExtrusionJunction.h b/include/utils/ExtrusionJunction.h index f431a73f94..7d3b605bcd 100644 --- a/include/utils/ExtrusionJunction.h +++ b/include/utils/ExtrusionJunction.h @@ -37,7 +37,7 @@ struct ExtrusionJunction */ size_t perimeter_index_; - ExtrusionJunction(const Point2LL p, const coord_t w, const coord_t perimeter_index); + ExtrusionJunction(const Point2LL& p, const coord_t w, const coord_t perimeter_index); bool operator==(const ExtrusionJunction& other) const; }; diff --git a/include/utils/MixedPolylineStitcher.h b/include/utils/MixedPolylineStitcher.h index 3c7adfc081..2e22defd10 100644 --- a/include/utils/MixedPolylineStitcher.h +++ b/include/utils/MixedPolylineStitcher.h @@ -5,7 +5,6 @@ #define UTILS_MIXED_POLYLINE_STITCHER_H #include "PolylineStitcher.h" -#include "geometry/ClosedLinesSet.h" namespace cura { @@ -13,6 +12,7 @@ namespace cura class MixedLinesSet; class OpenPolyline; class OpenLinesSet; +class ClosedLinesSet; class MixedPolylineStitcher : public PolylineStitcher { diff --git a/include/utils/linearAlg2D.h b/include/utils/linearAlg2D.h index 2bc60a1d60..19e76711d9 100644 --- a/include/utils/linearAlg2D.h +++ b/include/utils/linearAlg2D.h @@ -433,6 +433,16 @@ class LinearAlg2D * \param vec_len The lenght of the resultant vector. It's not wise to set this to 1, since we do tend to do integer math here. */ static Point2LL getBisectorVector(const Point2LL& intersect, const Point2LL& a, const Point2LL& b, const coord_t vec_len); + + /*! + * Gets the actual extra width to be applied when using a sloped base like for the prime tower of the supports + * @param base_width The maximum width of the base + * @param base_height The total height of the base + * @param slope_magnitude The magnitude of the base slope, which will determine the shape and thus the amount of material + * @param actual_height The height of the currently layer being processed + * @return The extra width to be applied around the element to make the base + */ + static coord_t getSlopedWidth(const coord_t base_width, const coord_t base_height, const double slope_magnitude, const coord_t actual_height); }; diff --git a/include/utils/polygonUtils.h b/include/utils/polygonUtils.h index 40f42181ca..27bae9e194 100644 --- a/include/utils/polygonUtils.h +++ b/include/utils/polygonUtils.h @@ -13,12 +13,13 @@ #include "PolygonsPointIndex.h" #include "SparseLineGrid.h" #include "SparsePointGridInclusive.h" -#include "geometry/ClosedLinesSet.h" #include "geometry/Polygon.h" namespace cura { +class ClosedLinesSet; + /*! * Result of finding the closest point to a given within a set of polygons, with extra information on where the point is. */ @@ -93,6 +94,13 @@ typedef SparseLineGrid Loc class PolygonUtils { public: + /*! Helper structure to return both the generated walls and the width of the final contour, when generating insets/outsets */ + struct InsetOutset + { + std::vector walls; + coord_t final_contour_offset{ 0 }; + }; + static const std::function no_penalty_function; //!< Function always returning zero /*! @@ -726,6 +734,24 @@ class PolygonUtils */ static ClosedLinesSet generateCircularInset(const Point2LL& center, const coord_t outer_radius, const coord_t line_width, const size_t circle_definition); + /*! + * Generates a series of outsetting walls around a given shape + * @param shape The shape to be wrapped + * @param width The maximum width to be reached, actual result may be thinner but never larger + * @param line_width The line width to be used to print each outset line + * @return The generated outset lines and the actual width of the outermost contour + */ + static InsetOutset generateOutset(const Shape& shape, const coord_t width, const coord_t line_width); + + /*! + * Generates a series of insetting walls inside a given shape + * @param shape The shape to be filled + * @param width The maximum width to be reached, actual result may be thinner but never larger + * @param line_width The line width to be used to print each inset line + * @return The generated inset lines and the actual width of the innermost contour + */ + static InsetOutset generateInset(const Shape& shape, const coord_t width, const coord_t line_width); + private: /*! * Helper function for PolygonUtils::moveInside2: moves a point \p from which was moved onto \p closest_polygon_point towards inside/outside when it's not already @@ -747,6 +773,16 @@ class PolygonUtils * @return The parts of the shape that are wider than the given minimum. Note that the returned shape may go beyond the original one. */ static Shape getRawWideAreas(const Shape& shape, const coord_t min_width, const coord_t extra_widen = EPSILON); + + /*! + * Generates a series of insetting/outsetting walls given a contour shape + * @param shape The initial contour to be offsetted + * @param width The maximum width to be reached, actual result may be thinner but never larger + * @param line_width The line width to be used to print each line + * @param direction The direction, which should be either 1 for outsets or -1 for insets. Other values will generate an incorrect result. + * @return The generated lines and the actual width of the last contour + */ + static InsetOutset generateInsetOutset(const Shape& shape, const coord_t width, const coord_t line_width, const coord_t direction); }; } // namespace cura diff --git a/src/FffGcodeWriter.cpp b/src/FffGcodeWriter.cpp index dfa51260e3..818146772f 100644 --- a/src/FffGcodeWriter.cpp +++ b/src/FffGcodeWriter.cpp @@ -1221,11 +1221,15 @@ FffGcodeWriter::ProcessLayerResult FffGcodeWriter::processLayer(const SliceDataS { // process the skirt or the brim of the starting extruder. auto extruder_nr = gcode_layer.getExtruder(); - if (storage.skirt_brim[extruder_nr].size() > 0) + if (! storage.skirt_brim[extruder_nr].empty()) { processSkirtBrim(storage, gcode_layer, extruder_nr, layer_nr); - time_keeper.registerTime("Skirt/brim"); } + if (! storage.support_brim.empty()) + { + processSupportBrim(storage, gcode_layer, extruder_nr, layer_nr); + } + time_keeper.registerTime("Skirt/brim"); // handle shield(s) first in a layer so that chances are higher that the other nozzle is wiped (for the ooze shield) processOozeShield(storage, gcode_layer); @@ -1304,7 +1308,7 @@ bool FffGcodeWriter::getExtruderNeedPrimeBlobDuringFirstLayer(const SliceDataSto return need_prime_blob; } -void FffGcodeWriter::processSkirtBrim(const SliceDataStorage& storage, LayerPlan& gcode_layer, unsigned int extruder_nr, LayerIndex layer_nr) const +void FffGcodeWriter::processSkirtBrim(const SliceDataStorage& storage, LayerPlan& gcode_layer, const unsigned int extruder_nr, const LayerIndex layer_nr) const { const ExtruderTrain& train = Application::getInstance().current_slice_->scene.extruders[extruder_nr]; const int skirt_height = train.settings_.get("skirt_height"); @@ -1437,11 +1441,11 @@ void FffGcodeWriter::processSkirtBrim(const SliceDataStorage& storage, LayerPlan } assert(all_brim_lines.size() == total_line_count); // Otherwise pointers would have gotten invalidated - const bool enable_travel_optimization = true; // Use the combing outline while deciding in which order to print the lines. Can't hurt for only one layer. - const coord_t wipe_dist = 0u; - const Ratio flow_ratio = 1.0; - const double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT; - const bool reverse_print_direction = false; + constexpr bool enable_travel_optimization = true; // Use the combing outline while deciding in which order to print the lines. Can't hurt for only one layer. + constexpr coord_t wipe_dist = 0u; + constexpr Ratio flow_ratio = 1.0; + constexpr double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT; + constexpr bool reverse_print_direction = false; if (! all_brim_lines.empty()) { @@ -1457,27 +1461,36 @@ void FffGcodeWriter::processSkirtBrim(const SliceDataStorage& storage, LayerPlan reverse_print_direction, layer_nr == 0 ? order_requirements : PathOrderOptimizer::no_order_requirements_); } +} - +void FffGcodeWriter::processSupportBrim(const SliceDataStorage& storage, LayerPlan& gcode_layer, const unsigned int extruder_nr, const LayerIndex layer_nr) +{ // Add the support brim after the skirt_brim to gcode_layer // Support brim is only added in layer 0 // For support brim we don't care about the order, because support doesn't need to be accurate. const Settings& mesh_group_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; - if ((layer_nr == 0) && (extruder_nr == mesh_group_settings.get("support_extruder_nr_layer_0").extruder_nr_)) + if (layer_nr > 0 || (extruder_nr != mesh_group_settings.get("support_extruder_nr_layer_0").extruder_nr_)) { - total_line_count += storage.support_brim.size(); - gcode_layer.addLinesByOptimizer( - storage.support_brim, - gcode_layer.configs_storage_.skirt_brim_config_per_extruder[extruder_nr], - SpaceFillType::PolyLines, - enable_travel_optimization, - wipe_dist, - flow_ratio, - start_close_to, - fan_speed, - reverse_print_direction, - order_requirements = {}); + return; } + + constexpr bool enable_travel_optimization = true; // Use the combing outline while deciding in which order to print the lines. Can't hurt for only one layer. + constexpr coord_t wipe_dist = 0u; + constexpr Ratio flow_ratio = 1.0; + constexpr double fan_speed = GCodePathConfig::FAN_SPEED_DEFAULT; + constexpr bool reverse_print_direction = false; + const Point2LL start_close_to = gcode_layer.getLastPlannedPositionOrStartingPosition(); + + gcode_layer.addLinesByOptimizer( + storage.support_brim, + gcode_layer.configs_storage_.skirt_brim_config_per_extruder[extruder_nr], + SpaceFillType::PolyLines, + enable_travel_optimization, + wipe_dist, + flow_ratio, + start_close_to, + fan_speed, + reverse_print_direction); } void FffGcodeWriter::processOozeShield(const SliceDataStorage& storage, LayerPlan& gcode_layer) const @@ -2624,10 +2637,10 @@ FffGcodeWriter::InsetsPreprocessResult FffGcodeWriter::preProcessInsets( { for (const SupportInfillPart& support_part : support_layer.support_infill_parts) { - AABB support_part_bb(support_part.getInfillArea()); + AABB support_part_bb(support_part.outline_); if (boundaryBox.hit(support_part_bb)) { - outlines_below.push_back(support_part.getInfillArea()); + outlines_below.push_back(support_part.outline_); } } } @@ -3244,10 +3257,10 @@ void FffGcodeWriter::processTopBottom( { for (auto support_part : support_layer->support_infill_parts) { - AABB support_part_bb(support_part.getInfillArea()); + AABB support_part_bb(support_part.outline_); if (skin_bb.hit(support_part_bb)) { - supported = ! skin_fill.intersection(support_part.getInfillArea()).empty(); + supported = ! skin_fill.intersection(support_part.outline_).empty(); if (supported) { @@ -3580,6 +3593,8 @@ bool FffGcodeWriter::processSupportInfill(const SliceDataStorage& storage, Layer : mesh_group_settings.get("support_infill_extruder_nr").extruder_nr_; const ExtruderTrain& infill_extruder = Application::getInstance().current_slice_->scene.extruders[extruder_nr]; + gcode_layer.addLinesByOptimizer(support_layer.base, gcode_layer.configs_storage_.support_infill_config[0], SpaceFillType::PolyLines); + coord_t default_support_line_distance = infill_extruder.settings_.get("support_line_distance"); // To improve adhesion for the "support initial layer" the first layer might have different properties @@ -4187,6 +4202,7 @@ void FffGcodeWriter::setExtruder_addPrime(const SliceDataStorage& storage, Layer if (! gcode_layer.getSkirtBrimIsPlanned(extruder_nr)) { processSkirtBrim(storage, gcode_layer, extruder_nr, gcode_layer.getLayerNr()); + processSupportBrim(storage, gcode_layer, extruder_nr, gcode_layer.getLayerNr()); } } diff --git a/src/FffPolygonGenerator.cpp b/src/FffPolygonGenerator.cpp index 17e9b4a05a..78c73a1338 100644 --- a/src/FffPolygonGenerator.cpp +++ b/src/FffPolygonGenerator.cpp @@ -418,17 +418,13 @@ void FffPolygonGenerator::slices2polygons(SliceDataStorage& storage, TimeKeeper& TreeSupport tree_support_generator(storage); tree_support_generator.generateSupportAreas(storage); - // Pre-compute lightning fill - if (mesh_group_settings.get("support_line_distance") > 0 && mesh_group_settings.get("support_pattern") == EFillMethod::LIGHTNING) - { - storage.support.lightning_generator = std::make_shared(storage.support); - } - computePrintHeightStatistics(storage); // handle helpers storage.initializePrimeTower(); + AreaSupport::generateSupportBase(storage); + spdlog::debug("Processing ooze shield"); processOozeShield(storage); @@ -453,6 +449,12 @@ void FffPolygonGenerator::slices2polygons(SliceDataStorage& storage, TimeKeeper& spdlog::debug("Processing gradual support"); // generate gradual support AreaSupport::generateSupportInfillFeatures(storage); + + // Pre-compute lightning fill + if (mesh_group_settings.get("support_line_distance") > 0 && mesh_group_settings.get("support_pattern") == EFillMethod::LIGHTNING) + { + storage.support.lightning_generator = std::make_shared(storage.support); + } } void FffPolygonGenerator::processBasicWallsSkinInfill( @@ -1034,8 +1036,9 @@ void FffPolygonGenerator::processDraftShield(SliceDataStorage& storage) draft_shield = draft_shield.unionPolygons(storage.getLayerOutlines(layer_nr, around_support, around_prime_tower)); } + draft_shield.makeConvex(); const coord_t draft_shield_dist = mesh_group_settings.get("draft_shield_dist"); - storage.draft_protection_shield = draft_shield.approxConvexHull(draft_shield_dist); + storage.draft_protection_shield = draft_shield.offset(draft_shield_dist); // Extra offset has rounded joints, so simplify again. coord_t maximum_resolution = 0; // Draft shield is printed with every extruder, so resolve with the max() or min() of them to meet the requirements of all extruders. @@ -1080,10 +1083,7 @@ void FffPolygonGenerator::processPlatformAdhesion(SliceDataStorage& storage) skirt_brim.generate(); } - if (mesh_group_settings.get("support_brim_enable")) - { - skirt_brim.generateSupportBrim(); - } + skirt_brim.generateSupportBrim(); } diff --git a/src/LayerPlan.cpp b/src/LayerPlan.cpp index 7a3149a4f5..a717a5f8b6 100644 --- a/src/LayerPlan.cpp +++ b/src/LayerPlan.cpp @@ -26,6 +26,7 @@ #include "arachne/SkeletalTrapezoidationGraph.h" #include "bridge/bridge.h" #include "communication/Communication.h" +#include "geometry/ClosedLinesSet.h" #include "geometry/OpenPolyline.h" #include "geometry/conversions/Point2D_Point2LL.h" #include "gradual_flow/Processor.h" @@ -4194,7 +4195,7 @@ template void LayerPlan::addLinesByOptimizer( const MendedShape& extra_inwards_move_contour); template void LayerPlan::addLinesByOptimizer( - const ClosedLinesSet& lines, + const LinesSet& lines, const GCodePathConfig& config, const SpaceFillType space_fill_type, const bool enable_travel_optimization, diff --git a/src/PrimeTower/PrimeTower.cpp b/src/PrimeTower/PrimeTower.cpp index 8d40fcd262..2ca3cfb5e0 100644 --- a/src/PrimeTower/PrimeTower.cpp +++ b/src/PrimeTower/PrimeTower.cpp @@ -50,8 +50,7 @@ PrimeTower::PrimeTower() for (coord_t z = 0; z < base_height; z += layer_height) { - const double brim_radius_factor = std::pow((1.0 - static_cast(z) / static_cast(base_height)), base_curve_magnitude); - const coord_t extra_radius = std::llrint(static_cast(base_extra_radius) * brim_radius_factor); + const coord_t extra_radius = LinearAlg2D::getSlopedWidth(base_extra_radius, base_height, base_curve_magnitude, z); const coord_t total_radius = tower_radius + extra_radius; base_occupied_outline_.push_back(OccupiedOutline{ PolygonUtils::makeDisc(middle_, total_radius, circle_definition_), total_radius }); } diff --git a/src/SkirtBrim.cpp b/src/SkirtBrim.cpp index 5cf4b65448..aa2b9aacb4 100644 --- a/src/SkirtBrim.cpp +++ b/src/SkirtBrim.cpp @@ -3,6 +3,7 @@ #include "SkirtBrim.h" +#include #include #include "Application.h" @@ -16,6 +17,7 @@ #include "support.h" #include "utils/MixedPolylineStitcher.h" #include "utils/Simplify.h" +#include "utils/polygonUtils.h" namespace cura { @@ -146,16 +148,20 @@ void SkirtBrim::generate() std::vector all_brim_offsets = generateBrimOffsetPlan(starting_outlines); std::vector allowed_areas_per_extruder = generateAllowedAreas(starting_outlines); - // Apply 'approximate convex hull' if the adhesion is skirt _after_ any skirt but also prime-tower-brim adhesion. + // Apply 'make convex hull' if the adhesion is skirt _after_ any skirt but also prime-tower-brim adhesion. // Otherwise, the now expanded convex hull covered areas will mess with that brim. Fortunately this does not mess // with the other area calculation above, since they are either itself a simple/convex shape or relevant for brim. - Shape covered_area = storage_.getLayerOutlines( - 0, - /*include_support*/ true, - /*include_prime_tower*/ adhesion_type_ == EPlatformAdhesion::SKIRT); + constexpr LayerIndex layer_nr = 0; + constexpr bool include_support = true; + const bool include_prime_tower = adhesion_type_ == EPlatformAdhesion::SKIRT; + constexpr bool external_polys_only = false; + constexpr coord_t extruder_nr = -1; + constexpr bool include_models = true; + constexpr bool include_support_base = false; + Shape covered_area = storage_.getLayerOutlines(layer_nr, include_support, include_prime_tower, external_polys_only, extruder_nr, include_models, include_support_base); if (adhesion_type_ == EPlatformAdhesion::SKIRT) { - covered_area = covered_area.approxConvexHull(); + covered_area.makeConvex(); } std::vector total_length = generatePrimaryBrim(all_brim_offsets, covered_area, allowed_areas_per_extruder); @@ -241,7 +247,7 @@ coord_t SkirtBrim::generateOffset(const Offset& offset, Shape& covered_area, std if (std::holds_alternative(offset.reference_outline_or_index_)) { - Outline* reference_outline = std::get(offset.reference_outline_or_index_); + const Outline* reference_outline = std::get(offset.reference_outline_or_index_); for (const auto& [shape, offset_value] : { std::make_tuple(reference_outline->gapped, offset.offset_value_gapped_), std::make_tuple(reference_outline->touching, offset.offset_value_touching_) }) { @@ -354,7 +360,7 @@ SkirtBrim::Outline SkirtBrim::getFirstLayerOutline(const int extruder_nr /* = -1 // so in some cases with a large skirt gap and small models and small shield distance // the skirt lines can cross the shield lines. // This shouldn't be a big problem, since the skirt lines are far away from the model. - first_layer_outline.gapped = first_layer_outline.gapped.approxConvexHull(); + first_layer_outline.gapped.makeConvex(); } else { // add brim underneath support by removing support where there's brim around the model @@ -368,10 +374,10 @@ SkirtBrim::Outline SkirtBrim::getFirstLayerOutline(const int extruder_nr /* = -1 } if (storage_.support.generated && primary_line_count > 0 && ! storage_.support.supportLayers.empty() - && (extruder_nr == -1 || extruder_nr == global_settings.get("support_infill_extruder_nr"))) + && (extruder_nr == -1 || extruder_nr == global_settings.get("support_extruder_nr_layer_0"))) { // remove model-brim from support SupportLayer& support_layer = storage_.support.supportLayers[0]; - const ExtruderTrain& support_infill_extruder = global_settings.get("support_infill_extruder_nr"); + const ExtruderTrain& support_infill_extruder = global_settings.get("support_extruder_nr_layer_0"); if (support_infill_extruder.settings_.get("brim_replaces_support")) { // avoid gap in the middle @@ -594,8 +600,9 @@ std::vector SkirtBrim::generateAllowedAreas(const std::vector& s constexpr bool include_support = true; constexpr bool include_prime_tower = true; constexpr bool include_model = false; + constexpr bool include_support_base = false; extruder_outlines.supports_outlines - = storage_.getLayerOutlines(layer_nr, include_support, include_prime_tower, external_polys_only, extruder_nr, include_model); + = storage_.getLayerOutlines(layer_nr, include_support, include_prime_tower, external_polys_only, extruder_nr, include_model, include_support_base); } } } @@ -634,7 +641,7 @@ std::vector SkirtBrim::generateAllowedAreas(const std::vector& s if ((other_extruder_nr == extruder_nr || extruder_nr == skirt_brim_extruder_nr_) && ((covered_area > 0 && extruder_config.outside_polys_) || (covered_area < 0 && extruder_config.inside_polys_))) { - // This is an area we are gonna intentionnally print brim in, use the actual gap + // This is an area we are gonna intentionally print brim in, use the actual gap offset += extruder_config.gap_ - 50; // Lower margin a bit to avoid discarding legitimate lines } else @@ -667,30 +674,70 @@ std::vector SkirtBrim::generateAllowedAreas(const std::vector& s return allowed_areas_per_extruder; } -void SkirtBrim::generateSupportBrim() +void SkirtBrim::generateSupportInsideBrim(const Settings& settings, const Shape& support_outline, const size_t width, const size_t line_width) { - Scene& scene = Application::getInstance().current_slice_->scene; - const ExtruderTrain& support_infill_extruder = scene.current_mesh_group->settings.get("support_infill_extruder_nr"); - const coord_t brim_line_width - = support_infill_extruder.settings_.get("skirt_brim_line_width") * support_infill_extruder.settings_.get("initial_layer_line_width_factor"); - size_t line_count = support_infill_extruder.settings_.get("support_brim_line_count"); - const coord_t minimal_length = support_infill_extruder.settings_.get("skirt_brim_minimal_length"); - if (! storage_.support.generated || line_count <= 0 || storage_.support.supportLayers.empty()) + const coord_t support_brim_minimum_hole_area = MM2_2INT(settings.get("support_brim_minimum_hole_area")); + + PolygonUtils::InsetOutset base_insets = PolygonUtils::generateInset(support_outline, width, line_width); + for (Shape& base_inset : base_insets.walls) { - return; + // Remove small inner brim holes. Holes have a negative area, remove anything smaller than multiplier x extrusion "area" + base_inset.erase( + ranges::remove_if( + base_inset, + [support_brim_minimum_hole_area](const Polygon& polygon) + { + const double area = polygon.area(); + return area < 0 && area > -support_brim_minimum_hole_area; + }), + base_inset.end()); + + if (base_inset.length() > 0) + { + storage_.support_brim.push_back(std::move(base_inset)); + } } +} + +void SkirtBrim::generateSupportOutsideBrim(const Shape& support_outline, const size_t width, const size_t line_width, const Shape& exclusion_area) +{ + PolygonUtils::InsetOutset base_outsets = PolygonUtils::generateOutset(support_outline, width, line_width); - const coord_t brim_width = brim_line_width * line_count; - coord_t skirt_brim_length = 0; + ClosedLinesSet closed_base_outset; + for (Shape& base_outset : base_outsets.walls) + { + closed_base_outset.reserve(closed_base_outset.size() + base_outset.size()); + for (Polygon& base_outset_polygon : base_outset) + { + closed_base_outset.push_back(ClosedPolyline(std::move(base_outset_polygon))); + } + } - if (storage_.skirt_brim[support_infill_extruder.extruder_nr_].empty()) + MixedLinesSet base_lines = closed_base_outset.difference(exclusion_area).intersection(storage_.getMachineBorder()); + if (base_lines.length() > 0) { - storage_.skirt_brim[support_infill_extruder.extruder_nr_].emplace_back(); + storage_.support_brim.push_back(std::move(base_lines)); } +} - for (const MixedLinesSet& brim_line : storage_.skirt_brim[support_infill_extruder.extruder_nr_]) +void SkirtBrim::generateSupportBrim() +{ + const Scene& scene = Application::getInstance().current_slice_->scene; + const Settings& settings = scene.current_mesh_group->settings.get("support_extruder_nr_layer_0").settings_; + const coord_t brim_line_width = settings.get("skirt_brim_line_width") * settings.get("initial_layer_line_width_factor"); + + const size_t inside_brim_line_count = settings.get("support_brim_enable") ? settings.get("support_brim_line_count") : 0; + + const auto layer_height = settings.get("layer_height"); + const auto base_outside_width = settings.get("support_base_outside_width"); + const auto base_outside_height = settings.get("support_outside_base_height"); + const auto support_outer_brim_enable = settings.get("support_outer_brim_enable"); + const bool has_base_outside = support_outer_brim_enable && base_outside_width > 0 && base_outside_height >= layer_height; + const size_t outside_brim_line_count = (adhesion_type_ != EPlatformAdhesion::BRIM && has_base_outside) ? base_outside_width / brim_line_width : 0; + + if (! storage_.support.generated || (inside_brim_line_count == 0 && outside_brim_line_count == 0) || storage_.support.supportLayers.empty()) { - skirt_brim_length += brim_line.length(); + return; } SupportLayer& support_layer = storage_.support.supportLayers[0]; @@ -700,39 +747,29 @@ void SkirtBrim::generateSupportBrim() { support_outline.push_back(part.outline_); } - const Shape brim_area = support_outline.difference(support_outline.offset(-brim_width)); - support_layer.excludeAreasFromSupportInfillAreas(brim_area, AABB(brim_area)); - const coord_t support_brim_minimum_hole_area = MM2_2INT(support_infill_extruder.settings_.get("support_brim_minimum_hole_area")); - coord_t offset_distance = brim_line_width / 2; - for (size_t skirt_brim_number = 0; skirt_brim_number < line_count; skirt_brim_number++) + if (inside_brim_line_count > 0) { - offset_distance -= brim_line_width; + const coord_t inside_brim_width = brim_line_width * inside_brim_line_count; + const Shape brim_area = support_outline.difference(support_outline.offset(-inside_brim_width)); + support_layer.excludeAreasFromSupportInfillAreas(brim_area, AABB(brim_area)); - Shape brim_line = support_outline.offset(offset_distance, ClipperLib::jtRound); + generateSupportInsideBrim(settings, support_outline, inside_brim_width, brim_line_width); + } - // Remove small inner skirt and brim holes. Holes have a negative area, remove anything smaller then multiplier x extrusion "area" - for (size_t n = 0; n < brim_line.size(); n++) - { - const double area = brim_line[n].area(); - if (area < 0 && area > -support_brim_minimum_hole_area) - { - brim_line.removeAt(n--); - } - } + if (outside_brim_line_count > 0) + { + const auto support_xy_distance = settings.get("support_xy_distance"); - const bool brim_line_empty = brim_line.empty(); // Store before moving - storage_.support_brim.push_back(std::move(brim_line)); - // In case of adhesion::NONE length of support brim is only the length of the brims formed for the support - const coord_t length = (adhesion_type_ == EPlatformAdhesion::NONE) ? skirt_brim_length : skirt_brim_length + storage_.support_brim.length(); - if (skirt_brim_number + 1 >= line_count && length > 0 && length < minimal_length) // Make brim or skirt have more lines when total length is too small. - { - line_count++; - } - if (brim_line_empty) - { // the fist layer of support is fully filled with brim - break; - } + // Build the exclusion area: model collision only. The build plate adhesion brim/skirt is generated after + // the support brim, so storage_.skirt_brim is still empty at this point. + constexpr bool include_support = false; + constexpr bool include_prime_tower = false; + constexpr bool external_polys_only = false; + const Shape outside_exclusion_area + = storage_.getLayerOutlines(0, include_support, include_prime_tower, external_polys_only).offset(support_xy_distance, ClipperLib::jtRound); + + generateSupportOutsideBrim(support_outline, base_outside_width, brim_line_width, outside_exclusion_area); } } diff --git a/src/SupportInfillPart.cpp b/src/SupportInfillPart.cpp index fc8e788a81..dd6edf2d03 100644 --- a/src/SupportInfillPart.cpp +++ b/src/SupportInfillPart.cpp @@ -16,5 +16,4 @@ SupportInfillPart::SupportInfillPart(const SingleShape& outline, coord_t support , custom_line_distance_(custom_line_distance) , use_fractional_config_(use_fractional_config) { - infill_area_per_combine_per_density_.clear(); } diff --git a/src/WallToolPaths.cpp b/src/WallToolPaths.cpp index c617d1e0e9..c0b219d632 100644 --- a/src/WallToolPaths.cpp +++ b/src/WallToolPaths.cpp @@ -29,7 +29,8 @@ WallToolPaths::WallToolPaths( const coord_t wall_0_inset, const Settings& settings, const int layer_idx, - SectionType section_type) + SectionType section_type, + WallToolPathGenerator generator) : outline_(outline) , bead_width_0_(nominal_bead_width) , bead_width_x_(nominal_bead_width) @@ -52,6 +53,7 @@ WallToolPaths::WallToolPaths( , settings_(settings) , layer_idx_(layer_idx) , section_type_(section_type) + , generator_(generator) { } @@ -90,15 +92,111 @@ WallToolPaths::WallToolPaths( } const std::vector& WallToolPaths::generate() +{ + switch (generator_) + { + case WallToolPathGenerator::Arachne: + generateArachne(); + break; + case WallToolPathGenerator::NaiveInset: + generateNaiveInset(); + break; + } + + toolpaths_generated_ = true; + return toolpaths_; +} + +void WallToolPaths::stitchToolPaths(std::vector& toolpaths, const coord_t stitch_distance) +{ + for (unsigned int wall_idx = 0; wall_idx < toolpaths.size(); wall_idx++) + { + VariableWidthLines& wall_lines = toolpaths[wall_idx]; + + VariableWidthLines stitched_polylines; + VariableWidthLines closed_polygons; + ExtrusionLineStitcher::stitch(wall_lines, stitched_polylines, closed_polygons, stitch_distance); + wall_lines = stitched_polylines; // replace input toolpaths with stitched polylines + + for (ExtrusionLine& wall_polygon : closed_polygons) + { + if (wall_polygon.junctions_.empty()) + { + continue; + } + wall_polygon.is_closed_ = true; + wall_lines.emplace_back(std::move(wall_polygon)); // add stitched polygons to result + } +#ifdef DEBUG + for (ExtrusionLine& line : wall_lines) + { + assert(line.inset_idx_ == wall_idx); + } +#endif // DEBUG + } +} + +void WallToolPaths::removeSmallFillLines(std::vector& toolpaths) +{ + for (VariableWidthLines& inset : toolpaths) + { + for (size_t line_idx = 0; line_idx < inset.size(); line_idx++) + { + ExtrusionLine& line = inset[line_idx]; + if (line.is_outer_wall()) + { + continue; + } + coord_t min_width = std::numeric_limits::max(); + for (const ExtrusionJunction& j : line) + { + min_width = std::min(min_width, j.w_); + } + if (line.is_odd_ && ! line.is_closed_ && line.shorterThan(min_width / 2)) + { // remove line + line = std::move(inset.back()); + inset.erase(--inset.end()); + line_idx--; // reconsider the current position + } + } + } +} + +void WallToolPaths::simplifyToolPaths(std::vector& toolpaths, const Settings& settings) +{ + const Simplify simplifier(settings); + for (auto& toolpath : toolpaths) + { + toolpath = toolpath + | ranges::views::transform( + [&simplifier](auto& line) + { + auto line_ = line.is_closed_ ? simplifier.polygon(line) : simplifier.polyline(line); + + if (line_.is_closed_ && line_.size() >= 2 && line_.front() != line_.back()) + { + line_.emplace_back(line_.front()); + } + return line_; + }) + | ranges::views::filter( + [](const auto& line) + { + return ! line.empty(); + }) + | ranges::to_vector; + } +} + +void WallToolPaths::generateArachne() { MendedShape prepared_outline(&settings_, section_type_, &outline_); if (prepared_outline.getShape().area() <= 0) { assert(toolpaths_.empty()); - return toolpaths_; + return; } - constexpr coord_t discretization_step_size = MM2INT(0.8); // When to split the middle wall into two: @@ -201,7 +299,6 @@ const std::vector& WallToolPaths::generate() return l.front().inset_idx_ < r.front().inset_idx_; }) && "WallToolPaths should be sorted from the outer 0th to inner_walls"); - toolpaths_generated_ = true; scripta::log( "toolpaths_5", toolpaths_, @@ -212,88 +309,49 @@ const std::vector& WallToolPaths::generate() scripta::CellVDI{ "inset_idx", &ExtrusionLine::inset_idx_ }, scripta::PointVDI{ "width", &ExtrusionJunction::w_ }, scripta::PointVDI{ "perimeter_index", &ExtrusionJunction::perimeter_index_ }); - return toolpaths_; } - -void WallToolPaths::stitchToolPaths(std::vector& toolpaths, const coord_t stitch_distance) +void WallToolPaths::generateNaiveInset() { - for (unsigned int wall_idx = 0; wall_idx < toolpaths.size(); wall_idx++) - { - VariableWidthLines& wall_lines = toolpaths[wall_idx]; - - VariableWidthLines stitched_polylines; - VariableWidthLines closed_polygons; - ExtrusionLineStitcher::stitch(wall_lines, stitched_polylines, closed_polygons, stitch_distance); - wall_lines = stitched_polylines; // replace input toolpaths with stitched polylines + std::vector walls; + walls.reserve(inset_count_); - for (ExtrusionLine& wall_polygon : closed_polygons) - { - if (wall_polygon.junctions_.empty()) - { - continue; - } - wall_polygon.is_closed_ = true; - wall_lines.emplace_back(std::move(wall_polygon)); // add stitched polygons to result - } -#ifdef DEBUG - for (ExtrusionLine& line : wall_lines) - { - assert(line.inset_idx_ == wall_idx); - } -#endif // DEBUG + if (inset_count_ > 0) + { + walls.push_back(outline_.offset(-wall_line_width_0_ / 2)); + inner_contour_ = outline_.offset(-(wall_line_width_0_ + (inset_count_ - 1) * wall_line_width_x_)); + } + for (size_t wall_index = 1; wall_index < inset_count_; ++wall_index) + { + walls.push_back(outline_.offset(-(wall_line_width_0_ + (wall_index - 1) * wall_line_width_x_ + wall_line_width_x_ / 2))); } -} -void WallToolPaths::removeSmallFillLines(std::vector& toolpaths) -{ - for (VariableWidthLines& inset : toolpaths) + for (const auto& [inset_index, shape] : walls | ranges::views::enumerate) { - for (size_t line_idx = 0; line_idx < inset.size(); line_idx++) + VariableWidthLines lines; + const coord_t line_width = inset_index == 0 ? wall_line_width_0_ : wall_line_width_x_; + + for (const Polygon& polygon : shape) { - ExtrusionLine& line = inset[line_idx]; - if (line.is_outer_wall()) + if (! polygon.isValid()) { continue; } - coord_t min_width = std::numeric_limits::max(); - for (const ExtrusionJunction& j : line) + + ExtrusionLine extrusion_line(inset_index); + for (auto iterator = polygon.beginSegments(); iterator != polygon.endSegments(); ++iterator) { - min_width = std::min(min_width, j.w_); - } - if (line.is_odd_ && ! line.is_closed_ && line.shorterThan(min_width / 2)) - { // remove line - line = std::move(inset.back()); - inset.erase(--inset.end()); - line_idx--; // reconsider the current position + if (iterator == polygon.beginSegments()) + { + extrusion_line.emplace_back((*iterator).start, line_width, inset_index); + } + extrusion_line.emplace_back((*iterator).end, line_width, inset_index); } - } - } -} -void WallToolPaths::simplifyToolPaths(std::vector& toolpaths, const Settings& settings) -{ - const Simplify simplifier(settings); - for (auto& toolpath : toolpaths) - { - toolpath = toolpath - | ranges::views::transform( - [&simplifier](auto& line) - { - auto line_ = line.is_closed_ ? simplifier.polygon(line) : simplifier.polyline(line); + lines.push_back(std::move(extrusion_line)); + } - if (line_.is_closed_ && line_.size() >= 2 && line_.front() != line_.back()) - { - line_.emplace_back(line_.front()); - } - return line_; - }) - | ranges::views::filter( - [](const auto& line) - { - return ! line.empty(); - }) - | ranges::to_vector; + toolpaths_.push_back(lines); } } diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp index fbe377cea7..ef65176604 100644 --- a/src/bridge/bridge.cpp +++ b/src/bridge/bridge.cpp @@ -383,12 +383,12 @@ std::optional bridgeAngle( { for (const SupportInfillPart& support_part : support_layer->support_infill_parts) { - AABB support_part_bb(support_part.getInfillArea()); + AABB support_part_bb(support_part.outline_); if (boundary_box.hit(support_part_bb)) { - prev_layer_outline.push_back(support_part.getInfillArea()); // not intersected with skin + prev_layer_outline.push_back(support_part.outline_); // not intersected with skin - Shape supported_skin(skin_outline.intersection(support_part.getInfillArea())); + Shape supported_skin(skin_outline.intersection(support_part.outline_)); if (! supported_skin.empty()) { supported_regions.push_back(supported_skin); diff --git a/src/geometry/ClosedLinesSet.cpp b/src/geometry/ClosedLinesSet.cpp new file mode 100644 index 0000000000..5c0c54452f --- /dev/null +++ b/src/geometry/ClosedLinesSet.cpp @@ -0,0 +1,51 @@ +// Copyright (c) 2026 UltiMaker +// CuraEngine is released under the terms of the AGPLv3 or higher. + +#include "geometry/ClosedLinesSet.h" + +#include "geometry/MixedLinesSet.h" +#include "geometry/OpenPolyline.h" +#include "geometry/Shape.h" + +namespace cura +{ + +MixedLinesSet ClosedLinesSet::intersection(const Shape& shape) const +{ + if (empty() || shape.empty()) + { + return {}; + } + + ClipperLib::PolyTree result; + ClipperLib::Clipper clipper(clipper_init); + shape.addPaths(clipper, ClipperLib::ptSubject); + addPaths(clipper, ClipperLib::ptClip); + clipper.Execute(ClipperLib::ctIntersection, result); + ClipperLib::Paths result_paths; + ClipperLib::OpenPathsFromPolyTree(result, result_paths); + + return MixedLinesSet(std::move(result)); +} + +[[nodiscard]] MixedLinesSet ClosedLinesSet::difference(const Shape& shape) const +{ + if (empty()) + { + return {}; + } + if (shape.empty()) + { + return MixedLinesSet(*this); + } + + ClipperLib::PolyTree ret; + ClipperLib::Clipper clipper(clipper_init); + addPaths(clipper, ClipperLib::ptSubject); + shape.addPaths(clipper, ClipperLib::ptClip); + clipper.Execute(ClipperLib::ctDifference, ret); + + return MixedLinesSet(std::move(ret)); +} + +} // namespace cura diff --git a/src/geometry/ClosedPolyline.cpp b/src/geometry/ClosedPolyline.cpp index 382438d8ca..033fa1a31c 100644 --- a/src/geometry/ClosedPolyline.cpp +++ b/src/geometry/ClosedPolyline.cpp @@ -25,6 +25,21 @@ bool ClosedPolyline::isValid() const return size() >= (explicitly_closed_ ? 4 : 3); } +void ClosedPolyline::addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const +{ + if (! isExplicitlyClosed()) + { + // Non-surface closed polylines are to be given explicitly closed to clipper, so make it closed + ClipperLib::Path closed_path = getPoints(); + closed_path.push_back(getPoints().front()); + clipper.AddPath(closed_path, poly_typ, false); + } + else + { + clipper.AddPath(getPoints(), poly_typ, false); + } +} + bool ClosedPolyline::inside(const Point2LL& p, bool border_result) const { int res = ClipperLib::PointInPolygon(p, getPoints()); diff --git a/src/geometry/LinesSet.cpp b/src/geometry/LinesSet.cpp index 517fa2067a..f5e58033ae 100644 --- a/src/geometry/LinesSet.cpp +++ b/src/geometry/LinesSet.cpp @@ -279,28 +279,12 @@ void LinesSet::removeDegenerateVerts() } } -template -template -void LinesSet::addPath(ClipperLib::Clipper& clipper, const OtherLineLine& line, ClipperLib::PolyType poly_typ) const -{ - // In this context, the "Closed" argument means "Is a surface" so it should be only - // true for actual filled polygons. Closed polylines are to be treated as lines here. - if constexpr (std::is_same::value) - { - clipper.AddPath(line.getPoints(), poly_typ, true); - } - else - { - clipper.AddPath(line.getPoints(), poly_typ, false); - } -} - template void LinesSet::addPaths(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const { for (const LineType& line : getLines()) { - addPath(clipper, line, poly_typ); + line.addPath(clipper, poly_typ); } } @@ -328,21 +312,20 @@ template void LinesSet::push_back(const OpenPolyline& line, CheckN template void LinesSet::push_back(OpenPolyline&& line, CheckNonEmptyParam checkNonEmpty); template void LinesSet::push_back(LinesSet&& lines_set); -template size_t ClosedLinesSet::pointCount() const; -template void ClosedLinesSet::removeAt(size_t index); -template void ClosedLinesSet::splitIntoSegments(OpenLinesSet& result) const; -template OpenLinesSet ClosedLinesSet::splitIntoSegments() const; -template coord_t ClosedLinesSet::length() const; -template void ClosedLinesSet::translate(const Point2LL& delta); -template void ClosedLinesSet::applyMatrix(const PointMatrix& matrix); -template void ClosedLinesSet::applyMatrix(const Point3Matrix& matrix); -template void ClosedLinesSet::removeDegenerateVerts(); -template void ClosedLinesSet::addPaths(ClipperLib::Clipper& clipper, ClipperLib::PolyType PolyTyp) const; -template void ClosedLinesSet::addPaths(ClipperLib::ClipperOffset& clipper, ClipperLib::JoinType jointType, ClipperLib::EndType endType) const; -template void ClosedLinesSet::push_back(const ClosedPolyline& line, CheckNonEmptyParam checkNonEmpty); -template void ClosedLinesSet::push_back(ClosedPolyline&& line, CheckNonEmptyParam checkNonEmpty); -template void ClosedLinesSet::push_back(ClosedLinesSet&& lines_set); -template void ClosedLinesSet::push_back(LinesSet&& lines_set); +template size_t LinesSet::pointCount() const; +template void LinesSet::removeAt(size_t index); +template void LinesSet::splitIntoSegments(OpenLinesSet& result) const; +template OpenLinesSet LinesSet::splitIntoSegments() const; +template coord_t LinesSet::length() const; +template void LinesSet::translate(const Point2LL& delta); +template void LinesSet::applyMatrix(const PointMatrix& matrix); +template void LinesSet::applyMatrix(const Point3Matrix& matrix); +template void LinesSet::removeDegenerateVerts(); +template void LinesSet::addPaths(ClipperLib::Clipper& clipper, ClipperLib::PolyType PolyTyp) const; +template void LinesSet::addPaths(ClipperLib::ClipperOffset& clipper, ClipperLib::JoinType jointType, ClipperLib::EndType endType) const; +template void LinesSet::push_back(const ClosedPolyline& line, CheckNonEmptyParam checkNonEmpty); +template void LinesSet::push_back(ClosedPolyline&& line, CheckNonEmptyParam checkNonEmpty); +template void LinesSet::push_back(LinesSet&& lines_set); template size_t LinesSet::pointCount() const; template void LinesSet::removeAt(size_t index); @@ -358,6 +341,5 @@ template void LinesSet::addPaths(ClipperLib::ClipperOffset& clipper, Cl template void LinesSet::push_back(const Polygon& line, CheckNonEmptyParam checkNonEmpty); template void LinesSet::push_back(Polygon&& line, CheckNonEmptyParam checkNonEmpty); template void LinesSet::push_back(LinesSet&& lines_set); -template void LinesSet::addPath(ClipperLib::Clipper& clipper, const Polygon& line, ClipperLib::PolyType poly_typ) const; } // namespace cura diff --git a/src/geometry/MixedLinesSet.cpp b/src/geometry/MixedLinesSet.cpp index e28d99db78..2a68eb91aa 100644 --- a/src/geometry/MixedLinesSet.cpp +++ b/src/geometry/MixedLinesSet.cpp @@ -5,6 +5,7 @@ #include +#include "geometry/ClosedLinesSet.h" #include "geometry/OpenLinesSet.h" #include "geometry/Polygon.h" #include "geometry/Shape.h" @@ -13,6 +14,47 @@ namespace cura { +MixedLinesSet::MixedLinesSet(const ClosedLinesSet& lines) +{ + push_back(lines); +} + +MixedLinesSet::MixedLinesSet(ClipperLib::PolyTree&& tree) +{ + ClipperLib::Paths polylines; + ClipperLib::OpenPathsFromPolyTree(tree, polylines); // Open with clipperlib means non-surface (polyline) + constexpr bool polyline_explicitely_closed = true; + for (ClipperLib::Path& path : polylines) + { + if (path.empty()) + { + continue; + } + + if (path.front() == path.back()) + { + push_back(ClosedPolyline(std::move(path), polyline_explicitely_closed)); + } + else + { + push_back(OpenPolyline(std::move(path))); + } + } + + ClipperLib::Paths polygons; + ClipperLib::ClosedPathsFromPolyTree(tree, polygons); // Closed with clipperlib means surface (polygon) + constexpr bool polygon_explicitely_closed = false; + for (ClipperLib::Path& path : polygons) + { + if (path.empty()) + { + continue; + } + + push_back(Polygon(std::move(path), polygon_explicitely_closed)); + } +} + Shape MixedLinesSet::offset(coord_t distance, ClipperLib::JoinType join_type, double miter_limit) const { if (distance == 0) @@ -74,11 +116,56 @@ Shape MixedLinesSet::offset(coord_t distance, ClipperLib::JoinType join_type, do return Shape{ std::move(result) }; } +MixedLinesSet MixedLinesSet::intersection(const Shape& shape) const +{ + if (empty() || shape.empty()) + { + return {}; + } + + ClipperLib::PolyTree ret; + ClipperLib::Clipper clipper(clipper_init); + for (const PolylinePtr& line : (*this)) + { + line->addPath(clipper, ClipperLib::ptSubject); + } + shape.addPaths(clipper, ClipperLib::ptClip); + clipper.Execute(ClipperLib::ctIntersection, ret); + return MixedLinesSet(std::move(ret)); +} + +MixedLinesSet MixedLinesSet::difference(const Shape& shape) const +{ + if (empty()) + { + return {}; + } + if (shape.empty()) + { + return *this; + } + + ClipperLib::PolyTree ret; + ClipperLib::Clipper clipper(clipper_init); + for (const PolylinePtr& line : (*this)) + { + line->addPath(clipper, ClipperLib::ptSubject); + } + shape.addPaths(clipper, ClipperLib::ptClip); + clipper.Execute(ClipperLib::ctDifference, ret); + return MixedLinesSet(std::move(ret)); +} + void MixedLinesSet::push_back(const OpenPolyline& line) { std::vector::push_back(std::make_shared(line)); } +void MixedLinesSet::push_back(const ClosedPolyline& line) +{ + std::vector::push_back(std::make_shared(line)); +} + void MixedLinesSet::push_back(OpenPolyline&& line) { std::vector::push_back(std::make_shared(std::move(line))); @@ -131,6 +218,15 @@ void MixedLinesSet::push_back(ClosedLinesSet&& lines_set) } } +void MixedLinesSet::push_back(const ClosedLinesSet& lines_set) +{ + reserve(size() + lines_set.size()); + for (const ClosedPolyline& line : lines_set) + { + push_back(line); + } +} + void MixedLinesSet::push_back(const MixedLinesSet& lines_set) { insert(end(), lines_set.begin(), lines_set.end()); diff --git a/src/geometry/Polygon.cpp b/src/geometry/Polygon.cpp index 83283b4000..e35c1fa76a 100644 --- a/src/geometry/Polygon.cpp +++ b/src/geometry/Polygon.cpp @@ -14,6 +14,11 @@ namespace cura { +void Polygon::addPath(ClipperLib::Clipper& clipper, ClipperLib::PolyType poly_typ) const +{ + clipper.AddPath(getPoints(), poly_typ, true); +} + Shape Polygon::intersection(const Polygon& other) const { ClipperLib::Paths ret_paths; diff --git a/src/geometry/Shape.cpp b/src/geometry/Shape.cpp index e1439635cd..97edc4f888 100644 --- a/src/geometry/Shape.cpp +++ b/src/geometry/Shape.cpp @@ -66,26 +66,6 @@ void Shape::emplace_back(ClipperLib::Path&& path, bool explicitely_closed) static_cast*>(this)->emplace_back(std::move(path), explicitely_closed); } -Shape Shape::approxConvexHull(int extra_outset) const -{ - constexpr int overshoot = MM2INT(100); // 10cm (hard-coded value). - - Shape convex_hull; - // Perform the offset for each polygon one at a time. - // This is necessary because the polygons may overlap, in which case the offset could end up in an infinite loop. - // See http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Classes/ClipperOffset/_Body.htm - for (const Polygon& polygon : (*this)) - { - ClipperLib::Paths offset_result; - ClipperLib::ClipperOffset offsetter(1.2, 10.0); - offsetter.AddPath(polygon.getPoints(), ClipperLib::jtRound, ClipperLib::etClosedPolygon); - offsetter.Execute(offset_result, overshoot); - convex_hull.emplace_back(std::move(offset_result)); - } - - return convex_hull.unionPolygons().offset(-overshoot + extra_outset, ClipperLib::jtRound); -} - void Shape::makeConvex() { // early out if there is nothing to do @@ -170,7 +150,7 @@ Shape Shape::difference(const Polygon& other) const ClipperLib::Paths ret; ClipperLib::Clipper clipper(clipper_init); addPaths(clipper, ClipperLib::ptSubject); - addPath(clipper, other, ClipperLib::ptClip); + other.addPath(clipper, ClipperLib::ptClip); clipper.Execute(ClipperLib::ctDifference, ret); return Shape(std::move(ret)); } @@ -200,7 +180,7 @@ Shape Shape::unionPolygons(const Polygon& polygon, ClipperLib::PolyFillType fill ClipperLib::Paths ret; ClipperLib::Clipper clipper(clipper_init); addPaths(clipper, ClipperLib::ptSubject); - addPath(clipper, polygon, ClipperLib::ptSubject); + polygon.addPath(clipper, ClipperLib::ptSubject); clipper.Execute(ClipperLib::ctUnion, ret, fill_type, fill_type); return Shape{ std::move(ret) }; } @@ -1004,7 +984,7 @@ void Shape::ensureManifold() #endif template OpenLinesSet Shape::intersection(const LinesSet& polylines, bool restitch, const coord_t max_stitch_distance, const bool split_into_segments) const; -template OpenLinesSet Shape::intersection(const ClosedLinesSet& polylines, bool restitch, const coord_t max_stitch_distance, const bool split_into_segments) const; +template OpenLinesSet Shape::intersection(const LinesSet& polylines, bool restitch, const coord_t max_stitch_distance, const bool split_into_segments) const; template OpenLinesSet Shape::intersection(const LinesSet& polylines, bool restitch, const coord_t max_stitch_distance, const bool split_into_segments) const; } // namespace cura diff --git a/src/infill.cpp b/src/infill.cpp index 17f114e5e2..4c53571b89 100644 --- a/src/infill.cpp +++ b/src/infill.cpp @@ -65,13 +65,14 @@ Shape Infill::generateWallToolPaths( const coord_t line_width, const Settings& settings, int layer_idx, - SectionType section_type) + SectionType section_type, + WallToolPathGenerator generator) { Shape inner_contour; if (wall_line_count > 0) { constexpr coord_t wall_0_inset = 0; // Don't apply any outer wall inset for these. That's just for the outer wall. - WallToolPaths wall_toolpaths(outer_contour, line_width, wall_line_count, wall_0_inset, settings, layer_idx, section_type); + WallToolPaths wall_toolpaths(outer_contour, line_width, wall_line_count, wall_0_inset, settings, layer_idx, section_type, generator); wall_toolpaths.pushToolPaths(toolpaths); inner_contour = wall_toolpaths.getInnerContour(); } @@ -93,14 +94,15 @@ void Infill::generate( const std::shared_ptr& lightning_trees, const SliceMeshStorage* mesh, const Shape& prevent_small_exposed_to_air, - const coord_t minimum_line_length) + const coord_t minimum_line_length, + WallToolPathGenerator wall_generator) { if (outer_contour_.empty()) { return; } - inner_contour_ = generateWallToolPaths(toolpaths, outer_contour_, wall_line_count_, infill_line_width_, settings, layer_idx, section_type); + inner_contour_ = generateWallToolPaths(toolpaths, outer_contour_, wall_line_count_, infill_line_width_, settings, layer_idx, section_type, wall_generator); scripta::log("infill_inner_contour_0", inner_contour_, section_type, layer_idx); inner_contour_ = inner_contour_.offset(infill_overlap_); diff --git a/src/infill/LightningGenerator.cpp b/src/infill/LightningGenerator.cpp index 5bcbbc1606..60a809abb7 100644 --- a/src/infill/LightningGenerator.cpp +++ b/src/infill/LightningGenerator.cpp @@ -81,7 +81,7 @@ LightningGenerator::LightningGenerator(const SupportStorage& support) Shape supper_area_here; for (const SupportInfillPart& part : support_layer.support_infill_parts) { - supper_area_here.push_back(part.outline_); + supper_area_here.push_back(part.infill_area_per_combine_per_density_.front().front()); } areas_per_layer.push_back(supper_area_here); diff --git a/src/main.cpp b/src/main.cpp index fe777c12cf..950e391505 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -59,7 +59,6 @@ int main(int argc, char** argv) #endif std::cerr << std::boolalpha; - // Want to set the sentry URL? Use '-c user.sentry:url= -o curaengine/*:enable_sentry=True' with conan install #ifdef SENTRY_URL if (const auto use_sentry = spdlog::details::os::getenv("USE_SENTRY"); ! use_sentry.empty() && use_sentry == "1") diff --git a/src/sliceDataStorage.cpp b/src/sliceDataStorage.cpp index 4f95f4d658..7dcb30b584 100644 --- a/src/sliceDataStorage.cpp +++ b/src/sliceDataStorage.cpp @@ -305,7 +305,8 @@ Shape SliceDataStorage::getLayerOutlines( const bool include_prime_tower, const bool external_polys_only, const int extruder_nr, - const bool include_models) const + const bool include_models, + const bool include_support_base) const { const Settings& mesh_group_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; @@ -388,7 +389,14 @@ Shape SliceDataStorage::getLayerOutlines( { for (const SupportInfillPart& support_infill_part : support_layer.support_infill_parts) { - total.push_back(support_infill_part.outline_); + if (include_support_base) + { + total.push_back(support_infill_part.base_outside_contour_.value_or(support_infill_part.outline_)); + } + else + { + total.push_back(support_infill_part.outline_); + } } total.push_back(support_layer.support_bottom); total.push_back(support_layer.support_roof); diff --git a/src/support.cpp b/src/support.cpp index 1dd11dfe1b..8489dd3f55 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -29,6 +29,7 @@ #include "infill/SierpinskiFillProvider.h" #include "infill/UniformDensityProvider.h" #include "progress/Progress.h" +#include "raft.h" #include "settings/EnumSettings.h" //For EFillMethod. #include "settings/types/Angle.h" //To compute overhang distance from the angle. #include "settings/types/Ratio.h" @@ -162,6 +163,154 @@ void AreaSupport::generateSupportInfillFeatures(SliceDataStorage& storage) AreaSupport::cleanup(storage); } +void AreaSupport::generateSupportBase(SliceDataStorage& storage) +{ + if (! storage.support.generated) + { + return; + } + + const Settings& global_settings = Application::getInstance().current_slice_->scene.current_mesh_group->settings; + const Settings& settings = global_settings.get("support_extruder_nr").settings_; + + const auto layer_height = settings.get("layer_height"); + const auto support_line_width = settings.get("support_line_width"); + const auto support_xy_distance = settings.get("support_xy_distance"); + const auto adhesion_type = settings.get("adhesion_type"); + + const auto support_outer_brim_enable = settings.get("support_outer_brim_enable"); + const auto base_outside_width = settings.get("support_base_outside_width"); + const auto base_outside_height = settings.get("support_outside_base_height"); + const auto base_outside_curve_magnitude = settings.get("support_outside_base_curve_magnitude"); + + const auto support_inner_brim_enable = settings.get("support_brim_enable"); + const auto base_inside_width = settings.get("support_base_inside_width"); + const auto base_inside_height = settings.get("support_inside_base_height"); + const auto base_inside_curve_magnitude = settings.get("support_inside_base_curve_magnitude"); + + const bool has_base_inside = support_inner_brim_enable && base_inside_width > 0 && base_inside_height >= layer_height; + const LayerIndex max_inside_layer = has_base_inside ? base_inside_height / layer_height : 0; + const bool has_base_outside = support_outer_brim_enable && base_outside_width > 0 && base_outside_height >= layer_height; + const LayerIndex max_outside_layer = has_base_outside ? base_outside_height / layer_height : 0; + const LayerIndex first_base_layer = 0; + + std::vector& support_layers = storage.support.supportLayers; + const LayerIndex last_base_layer = std::min(static_cast(std::max(max_outside_layer, max_inside_layer)), support_layers.size()); + + // Generate the base extra lines + parallel_for( + first_base_layer, + last_base_layer, + [&](const LayerIndex layer_nr) + { + const coord_t layer_z = layer_nr * layer_height; + + Shape forbidden_areas; + SupportLayer& support_layer = support_layers.at(layer_nr); + std::vector parts_to_process; + for (SupportInfillPart& part : support_layer.support_infill_parts) + { + if (part.use_fractional_config_) + { + forbidden_areas.push_back(part.outline_); + } + else + { + parts_to_process.push_back(&part); + } + } + + if (parts_to_process.empty()) + { + return; + } + + const coord_t base_outside_extra_width = LinearAlg2D::getSlopedWidth(base_outside_width, base_outside_height, base_outside_curve_magnitude, layer_z); + for (SupportInfillPart& part : support_layer.support_infill_parts) + { + if (! part.use_fractional_config_) + { + part.base_outside_contour_ = part.outline_.offset(base_outside_extra_width); + } + } + + if (layer_nr == 0 && adhesion_type != EPlatformAdhesion::RAFT) + { + // Extrusion lines for layer 0 are generated by brim generator + return; + } + + if (has_base_outside && layer_nr < max_outside_layer) + { + // Add the models as forbidden areas, offsetted with the XY distance + { + constexpr bool include_support = false; + constexpr bool include_prime_tower = false; + forbidden_areas.push_back(storage.getLayerOutlines(layer_nr, include_support, include_prime_tower).offset(support_xy_distance)); + } + + // Add the prime tower as forbidden area + { + constexpr bool include_support = false; + constexpr bool include_prime_tower = true; + constexpr bool external_polys_only = false; + constexpr int extruder_nr = -1; + constexpr bool include_models = false; + forbidden_areas.push_back(storage.getLayerOutlines(layer_nr, include_support, include_prime_tower, external_polys_only, extruder_nr, include_models)); + } + + // Add the support interface as forbidden area + forbidden_areas.push_back(support_layer.support_roof); + forbidden_areas.push_back(support_layer.support_bottom); + + Shape all_parts; + for (const SupportInfillPart* part_to_process : parts_to_process) + { + all_parts.push_back(part_to_process->outline_); + } + + PolygonUtils::InsetOutset base_outsets = PolygonUtils::generateOutset(all_parts, base_outside_extra_width, support_line_width); + ClosedLinesSet closed_base_outset; + + for (Shape& base_outset : base_outsets.walls) + { + closed_base_outset.reserve(closed_base_outset.size() + base_outset.size()); + for (Polygon& base_outset_polygon : base_outset) + { + closed_base_outset.push_back(ClosedPolyline(std::move(base_outset_polygon))); + } + } + + + MixedLinesSet base_lines = closed_base_outset.difference(forbidden_areas).intersection(storage.getMachineBorder()); + if (! base_lines.empty()) + { + support_layer.base.push_back(std::move(base_lines)); + } + } + + if (has_base_inside && layer_nr < max_inside_layer) + { + // const Shape support_inside_area = layer_support_shape.offset(-support_wall_count * support_line_width); + const coord_t base_inside_extra_width = LinearAlg2D::getSlopedWidth(base_inside_width, base_inside_height, base_inside_curve_magnitude, layer_z); + for (SupportInfillPart* part_to_process : parts_to_process) + { + const Shape support_inside_area = part_to_process->outline_.offset(-part_to_process->inset_count_to_generate_ * part_to_process->support_line_width_); + PolygonUtils::InsetOutset base_insets = PolygonUtils::generateInset(support_inside_area, base_inside_extra_width, support_line_width); + part_to_process->base_inside_contour_ = support_inside_area.offset(base_insets.final_contour_offset); + for (Shape& base_inset : base_insets.walls) + { + support_layer.base.reserve(support_layer.base.size() + base_insets.walls.size()); + for (Polygon& base_inset_polygon : base_inset) + { + support_layer.base.push_back(ClosedPolyline(std::move(base_inset_polygon))); + } + } + } + } + }); +} // namespace cura + void AreaSupport::generateGradualSupport(SliceDataStorage& storage) { // @@ -242,23 +391,26 @@ void AreaSupport::generateGradualSupport(SliceDataStorage& storage) { SupportInfillPart& support_infill_part = support_infill_parts[part_idx]; - Shape original_area = support_infill_part.getInfillArea(); + Shape original_area = support_infill_part.outline_; if (original_area.empty()) { continue; } + // NOTE: This both generates the walls _and_ returns the _actual_ infill area (the one _without_ walls) for use in the rest of the method. - const Shape infill_area = Infill::generateWallToolPaths( + const Shape wall_inside_area = Infill::generateWallToolPaths( support_infill_part.wall_toolpaths_, original_area, support_infill_part.inset_count_to_generate_, wall_width, infill_extruder.settings_, layer_nr, - SectionType::SUPPORT); + SectionType::SUPPORT, + WallToolPathGenerator::NaiveInset); const AABB& this_part_boundary_box = support_infill_part.outline_boundary_box_; // calculate density areas for this island + const Shape infill_area = support_infill_part.base_inside_contour_.value_or(wall_inside_area); Shape less_dense_support = infill_area; // one step less dense with each density_step Shape sum_more_dense; // NOTE: Only used for zig-zag or connected fills. for (unsigned int density_step = 0; density_step < max_density_steps; ++density_step) @@ -390,7 +542,7 @@ void AreaSupport::combineSupportInfillLayers(SliceDataStorage& storage) for (SupportInfillPart& part : layer.support_infill_parts) { - if (part.getInfillArea().empty()) + if (part.outline_.empty()) { continue; } @@ -405,7 +557,7 @@ void AreaSupport::combineSupportInfillLayers(SliceDataStorage& storage) continue; } - Shape intersection = infill_area_per_combine[combine_count_here - 1].intersection(lower_layer_part.getInfillArea()).offset(-200).offset(200); + Shape intersection = infill_area_per_combine[combine_count_here - 1].intersection(lower_layer_part.outline_).offset(-200).offset(200); if (intersection.size() <= 0) { continue; diff --git a/src/utils/CuraViz.cpp b/src/utils/CuraViz.cpp new file mode 100644 index 0000000000..b11d4e836c --- /dev/null +++ b/src/utils/CuraViz.cpp @@ -0,0 +1,206 @@ +// Copyright (c) 2026 Ultimaker B.V. +// CuraEngine is released under the terms of the AGPLv3 or higher + +#include "utils/CuraViz.h" + +#include +#ifdef ENABLE_CURAVIZ + +#include +#include +#include +#include +#include + +#include +#include + +#include "Application.h" +#include "Slice.h" +#include "geometry/MixedLinesSet.h" +#include "geometry/Polygon.h" +#include "geometry/Shape.h" +#include "settings/Settings.h" + +namespace cura +{ + +CuraViz* CuraViz::instance_ = nullptr; +std::mutex CuraViz::mutex_; + +CuraViz::CuraViz() + : socket_(io_context_) +{ + try + { + constexpr uint16_t port = 49673; + socket_.connect(asio::ip::tcp::endpoint(asio::ip::address::from_string("127.0.0.1"), port)); + spdlog::info("Connected to CuraViz"); + } + catch (asio::system_error error) + { + socket_.close(); + spdlog::warn("CuraViz could not connect to vizualiser: {}", error.what()); + } + + if (! socket_.is_open()) + { + return; + } + + const std::shared_ptr current_slice = Application::getInstance().current_slice_; + if (! current_slice) + { + spdlog::warn("CuraViz could not send build plate dimensions since slice has not been initialized yet"); + return; + } + + const Settings& global_settings = current_slice->scene.settings; + Point2LL machine_max(global_settings.get("machine_width"), global_settings.get("machine_depth")); + Point2LL machine_min(0, 0); + if (global_settings.get("machine_center_is_zero")) + { + machine_max = machine_max / 2; + machine_min -= machine_max; + } + + cura_viz::Message message; + cura_viz::Printer* printer_def = message.mutable_printer_def(); + printer_def->mutable_bed_min()->set_x(machine_min.X); + printer_def->mutable_bed_min()->set_y(machine_min.Y); + printer_def->mutable_bed_max()->set_x(machine_max.X); + printer_def->mutable_bed_max()->set_y(machine_max.Y); + + send(message, false); +} + +void CuraViz::send(const cura_viz::Message& message, const bool should_lock) +{ + if (! socket_.is_open()) + { + return; + } + + std::string serialized; + message.SerializeToString(&serialized); + + uint32_t size = serialized.size(); + + std::unique_lock lock; + if (should_lock) + { + lock = std::unique_lock(mutex_); + } + + asio::write(socket_, asio::buffer(&size, sizeof(size))); + asio::write(socket_, asio::buffer(serialized)); +} + +CuraViz* CuraViz::getInstance() +{ + const std::lock_guard lock(mutex_); + + if (instance_ == nullptr) + { + instance_ = new CuraViz(); + } + + return instance_; +} + +void CuraViz::setup(const Shape& shape, cura_viz::GeometricElement* element) +{ + cura_viz::LinesSet2LL* lines_set_message = element->mutable_data()->mutable_lines_set2ll(); + for (const Polygon& polygon : shape) + { + setup(polygon, lines_set_message->add_lines()); + } +} + +void CuraViz::setup(const MixedLinesSet& lines, cura_viz::GeometricElement* element) +{ + cura_viz::LinesSet2LL* lines_set_message = element->mutable_data()->mutable_lines_set2ll(); + for (const PolylinePtr& line : lines) + { + setup(*line, lines_set_message->add_lines()); + } +} + +void CuraViz::setup(const Point2LL& point, cura_viz::GeometricElement* element) +{ + cura_viz::Point2LL* point_message = element->mutable_data()->mutable_point2ll(); + point_message->set_x(point.X); + point_message->set_y(point.Y); +} + +void CuraViz::setup(const Polyline& polyline, cura_viz::Polyline2LL* polyline_message) +{ + polyline_message->set_surface(dynamic_cast(&polyline) != nullptr); + + for (auto iterator = polyline.beginSegments(); iterator != polyline.endSegments(); ++iterator) + { + cura_viz::Segment2LL* segment_message = polyline_message->add_segments(); + segment_message->mutable_start()->set_x((*iterator).start.X); + segment_message->mutable_start()->set_y((*iterator).start.Y); + segment_message->mutable_end()->set_x((*iterator).end.X); + segment_message->mutable_end()->set_y((*iterator).end.Y); + } +} + +void CuraViz::send(const Point2LL& point, const std::string& name, const std::string& step_name) +{ + MessageToSend message(step_name); + setup(point, message.addGeometricElement(name)); +} + +void CuraViz::send(const Shape& shape, const std::string& name, const std::string& step_name) +{ + MessageToSend message(step_name); + setup(shape, message.addGeometricElement(name)); +} + +void CuraViz::send(const std::vector& shapes, const std::string& name, const std::string& step_name) +{ + MessageToSend message(step_name); + for (const auto& [shape_index, shape] : shapes | ranges::views::enumerate) + { + setup(shape, message.addGeometricElement(fmt::format("{}_{}", name, shape_index))); + } +} + +void CuraViz::send(const MixedLinesSet& lines_set, const std::string& name, const std::string& step_name) +{ + MessageToSend message(step_name); + setup(lines_set, message.addGeometricElement(name)); +} + +void CuraViz::send(const std::vector& lines_sets, const std::string& name, const std::string& step_name) +{ + MessageToSend message(step_name); + for (const auto& [lines_set_index, lines_set] : lines_sets | ranges::views::enumerate) + { + setup(lines_set, message.addGeometricElement(fmt::format("{}_{}", name, lines_set_index))); + } +} + +CuraViz::MessageToSend::MessageToSend(const std::string& step_name) + : message_(std::make_shared()) +{ + message_->mutable_step()->set_name(step_name); +} + +CuraViz::MessageToSend::~MessageToSend() +{ + CuraViz::getInstance()->send(*message_); +} + +cura_viz::GeometricElement* CuraViz::MessageToSend::addGeometricElement(const std::string& element_name) +{ + cura_viz::GeometricElement* element = message_->mutable_step()->add_elements(); + element->set_name(element_name); + return element; +} + +} // namespace cura + +#endif \ No newline at end of file diff --git a/src/utils/ExtrusionJunction.cpp b/src/utils/ExtrusionJunction.cpp index 366d15dad3..a0bd834a27 100644 --- a/src/utils/ExtrusionJunction.cpp +++ b/src/utils/ExtrusionJunction.cpp @@ -11,7 +11,7 @@ bool ExtrusionJunction::operator==(const ExtrusionJunction& other) const return p_ == other.p_ && w_ == other.w_ && perimeter_index_ == other.perimeter_index_; } -ExtrusionJunction::ExtrusionJunction(const Point2LL p, const coord_t w, const coord_t perimeter_index) +ExtrusionJunction::ExtrusionJunction(const Point2LL& p, const coord_t w, const coord_t perimeter_index) : p_(p) , w_(w) , perimeter_index_(perimeter_index) diff --git a/src/utils/MixedPolylineStitcher.cpp b/src/utils/MixedPolylineStitcher.cpp index 73fb468fe2..74ba4bea82 100644 --- a/src/utils/MixedPolylineStitcher.cpp +++ b/src/utils/MixedPolylineStitcher.cpp @@ -3,6 +3,7 @@ #include "utils/MixedPolylineStitcher.h" +#include "geometry/ClosedLinesSet.h" #include "geometry/MixedLinesSet.h" #include "geometry/OpenLinesSet.h" #include "geometry/Polygon.h" diff --git a/src/utils/Simplify.cpp b/src/utils/Simplify.cpp index ad4f936e9e..ce8f526e5d 100644 --- a/src/utils/Simplify.cpp +++ b/src/utils/Simplify.cpp @@ -409,6 +409,6 @@ bool Simplify::remove(Polygonal& polygon, std::vector& to_delete, const si } template LinesSet Simplify::polyline(const LinesSet& polylines) const; -template ClosedLinesSet Simplify::polyline(const ClosedLinesSet& polylines) const; +template LinesSet Simplify::polyline(const LinesSet& polylines) const; } // namespace cura diff --git a/src/utils/linearAlg2D.cpp b/src/utils/linearAlg2D.cpp index d3263a0fb0..ef1a8bee8e 100644 --- a/src/utils/linearAlg2D.cpp +++ b/src/utils/linearAlg2D.cpp @@ -360,4 +360,15 @@ std::optional LinearAlg2D::segmentHorizontalLineIntersection(const Poin return lineHorizontalLineIntersection(p1, p2, line_y); } +coord_t LinearAlg2D::getSlopedWidth(const coord_t base_width, const coord_t base_height, const double slope_magnitude, const coord_t actual_height) +{ + if (base_width == 0 || base_height == 0) + { + return 0; + } + + const double factor = std::pow((1.0 - static_cast(actual_height) / static_cast(base_height)), slope_magnitude); + return std::llrint(static_cast(base_width) * factor); +} + } // namespace cura diff --git a/src/utils/polygonUtils.cpp b/src/utils/polygonUtils.cpp index e9a43d35bc..0bb9b67315 100644 --- a/src/utils/polygonUtils.cpp +++ b/src/utils/polygonUtils.cpp @@ -11,6 +11,7 @@ #include +#include "geometry/ClosedLinesSet.h" #include "geometry/OpenLinesSet.h" #include "geometry/PointMatrix.h" #include "geometry/SingleShape.h" @@ -1599,6 +1600,38 @@ ClosedLinesSet PolygonUtils::generateCircularInset(const Point2LL& center, const return inset; } +PolygonUtils::InsetOutset PolygonUtils::generateOutset(const Shape& shape, const coord_t width, const coord_t line_width) +{ + return generateInsetOutset(shape, width, line_width, 1); +} + +PolygonUtils::InsetOutset PolygonUtils::generateInset(const Shape& shape, const coord_t width, const coord_t line_width) +{ + return generateInsetOutset(shape, width, line_width, -1); +} + +PolygonUtils::InsetOutset PolygonUtils::generateInsetOutset(const Shape& shape, const coord_t width, const coord_t line_width, const coord_t direction) +{ + InsetOutset result; + Shape current_outset = shape; + const coord_t semi_line_width = line_width / 2; + coord_t offset = semi_line_width; + + while ((offset + semi_line_width <= width) && ! current_outset.empty()) + { + current_outset = shape.offset(offset * direction); + result.walls.push_back(current_outset); + offset += line_width; + } + + if (! result.walls.empty()) + { + result.final_contour_offset = direction * (offset - line_width / 2); + } + + return result; +} + template ClosestPoint PolygonUtils::walk(const ClosestPoint& from, coord_t distance); template ClosestPoint PolygonUtils::walk(const ClosestPoint& from, coord_t distance);