From 11c0e567a68979e96085b3763a76464cb793ea12 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 20 Dec 2022 09:09:10 +0100 Subject: [PATCH] WIP "ensure verticall wall thickness" rework: 1) New region expansion code to propagate wave from a boundary of a region inside of it. 2) get_extents() extended with a template attribute to work with zero area data sets. 3) ClipperZUtils.hpp for handling Clipper operation with Z coordinate (for source contour identification) --- src/clipper/clipper.cpp | 27 +- src/clipper/clipper.hpp | 1 + src/libslic3r/Algorithm/RegionExpansion.cpp | 351 ++++++++++++++++++++ src/libslic3r/Algorithm/RegionExpansion.hpp | 26 ++ src/libslic3r/BoundingBox.hpp | 45 ++- src/libslic3r/Brim.cpp | 2 +- src/libslic3r/CMakeLists.txt | 3 + src/libslic3r/ClipperUtils.cpp | 10 +- src/libslic3r/ClipperUtils.hpp | 7 +- src/libslic3r/ClipperZUtils.hpp | 143 ++++++++ src/libslic3r/Layer.cpp | 42 +-- src/libslic3r/PerimeterGenerator.cpp | 8 +- src/libslic3r/Point.cpp | 15 +- src/libslic3r/Point.hpp | 18 +- src/libslic3r/Polyline.hpp | 19 ++ src/libslic3r/libslic3r.h | 6 +- tests/fff_print/test_clipper.cpp | 71 +++- tests/libslic3r/CMakeLists.txt | 1 + tests/libslic3r/test_region_expansion.cpp | 254 ++++++++++++++ 19 files changed, 964 insertions(+), 85 deletions(-) create mode 100644 src/libslic3r/Algorithm/RegionExpansion.cpp create mode 100644 src/libslic3r/Algorithm/RegionExpansion.hpp create mode 100644 src/libslic3r/ClipperZUtils.hpp create mode 100644 tests/libslic3r/test_region_expansion.cpp diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index 518b4b7c3..3094e38b4 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -4093,19 +4093,40 @@ void AddPolyNodeToPaths(const PolyNode& polynode, NodeType nodetype, Paths& path for (int i = 0; i < polynode.ChildCount(); ++i) AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); } + +void AddPolyNodeToPaths(PolyNode&& polynode, NodeType nodetype, Paths& paths) +{ + bool match = true; + if (nodetype == ntClosed) match = !polynode.IsOpen(); + else if (nodetype == ntOpen) return; + + if (!polynode.Contour.empty() && match) + paths.push_back(std::move(polynode.Contour)); + for (int i = 0; i < polynode.ChildCount(); ++i) + AddPolyNodeToPaths(std::move(*polynode.Childs[i]), nodetype, paths); +} + //------------------------------------------------------------------------------ void PolyTreeToPaths(const PolyTree& polytree, Paths& paths) { - paths.resize(0); + paths.clear(); paths.reserve(polytree.Total()); AddPolyNodeToPaths(polytree, ntAny, paths); } + +void PolyTreeToPaths(PolyTree&& polytree, Paths& paths) +{ + paths.clear(); + paths.reserve(polytree.Total()); + AddPolyNodeToPaths(std::move(polytree), ntAny, paths); +} + //------------------------------------------------------------------------------ void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths) { - paths.resize(0); + paths.clear(); paths.reserve(polytree.Total()); AddPolyNodeToPaths(polytree, ntClosed, paths); } @@ -4113,7 +4134,7 @@ void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths) void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths) { - paths.resize(0); + paths.clear(); paths.reserve(polytree.Total()); //Open paths are top level only, so ... for (int i = 0; i < polytree.ChildCount(); ++i) diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index 641476c8b..849672a8f 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -206,6 +206,7 @@ void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution); void PolyTreeToPaths(const PolyTree& polytree, Paths& paths); +void PolyTreeToPaths(PolyTree&& polytree, Paths& paths); void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths); void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths); diff --git a/src/libslic3r/Algorithm/RegionExpansion.cpp b/src/libslic3r/Algorithm/RegionExpansion.cpp new file mode 100644 index 000000000..2a22fe785 --- /dev/null +++ b/src/libslic3r/Algorithm/RegionExpansion.cpp @@ -0,0 +1,351 @@ +#include "RegionExpansion.hpp" + +#include +#include + +namespace Slic3r { +namespace Algorithm { + +// similar to expolygons_to_zpaths(), but each contour is expanded before converted to zpath. +// The expanded contours are then opened (the first point is repeated at the end). +static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened( + const ExPolygons &src, const float expansion, coord_t base_idx) +{ + ClipperLib_Z::Paths out; + out.reserve(2 * std::accumulate(src.begin(), src.end(), size_t(0), + [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); + coord_t z = base_idx; + ClipperLib::ClipperOffset offsetter; + offsetter.ShortestEdgeLength = expansion * ClipperOffsetShortestEdgeFactor; + ClipperLib::Paths expansion_cache; + for (const ExPolygon &expoly : src) { + for (size_t icontour = 0; icontour < expoly.num_contours(); ++ icontour) { + // Execute reorients the contours so that the outer most contour has a positive area. Thus the output + // contours will be CCW oriented even though the input paths are CW oriented. + // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. + offsetter.Clear(); + offsetter.AddPath(expoly.contour_or_hole(icontour).points, ClipperLib::jtSquare, ClipperLib::etClosedPolygon); + expansion_cache.clear(); + offsetter.Execute(expansion_cache, icontour == 0 ? expansion : -expansion); + append(out, ClipperZUtils::to_zpaths(expansion_cache, z)); + } + ++ z; + } + return out; +} + +// Paths were created by splitting closed polygons into open paths and then by clipping them. +// Thus some pieces of the clipped polygons may now become split at the ends of the source polygons. +// Those ends are sorted lexicographically in "splits". +// Reconnect those split pieces. +static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vector> &splits) +{ + for (auto it_path = paths.begin(); it_path != paths.end(); ) { + ClipperLib_Z::Path &path = *it_path; + assert(path.size() >= 2); + bool merged = false; + if (path.size() >= 2) { + const ClipperLib_Z::IntPoint &front = path.front(); + const ClipperLib_Z::IntPoint &back = path.back(); + // The path before clipping was supposed to cross the clipping boundary or be fully out of it. + // Thus the clipped contour is supposed to become open. + assert(front.x() != back.x() || front.y() != back.y()); + if (front.x() != back.x() || front.y() != back.y()) { + // Look up the ends in "splits", possibly join the contours. + // "splits" maps into the other piece connected to the same end point. + auto find_end = [&splits](const ClipperLib_Z::IntPoint &pt) -> std::pair* { + auto it = std::lower_bound(splits.begin(), splits.end(), pt, + [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r); }); + return it != splits.end() && it->first == pt ? &(*it) : nullptr; + }; + auto *end = find_end(front); + bool end_front = true; + if (! end) { + end_front = false; + end = find_end(back); + } + if (end) { + // This segment ends at a split point of the source closed contour before clipping. + if (end->second == -1) { + // Open end was found, not matched yet. + end->second = int(it_path - paths.begin()); + } else { + // Open end was found and matched with end->second + ClipperLib_Z::Path &other_path = paths[end->second]; + polylines_merge(other_path, other_path.front() == end->first, std::move(path), end_front); + if (std::next(it_path) == paths.end()) { + paths.pop_back(); + break; + } + path = std::move(paths.back()); + paths.pop_back(); + merged = true; + } + } + } + } + if (! merged) + ++ it_path; + } +} + +static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polylines, float offset) +{ + ClipperLib::Paths out; + out.reserve(polylines.size()); + ClipperLib::Paths out_this; + for (const ClipperLib::Path &path : polylines) { + co.Clear(); + co.AddPath(path, jtRound, ClipperLib::etOpenRound); + co.Execute(out_this, offset); + append(out, std::move(out_this)); + } + return out; +} + +// Input polygons may consist of multiple expolygons, even nested expolygons. +// After inflation some polygons may thus overlap, however the overlap is being resolved during the successive +// clipping operation, thus it is not being done here. +static ClipperLib::Paths wavefront_step(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polygons, float offset) +{ + ClipperLib::Paths out; + out.reserve(polygons.size()); + ClipperLib::Paths out_this; + for (const ClipperLib::Path &polygon : polygons) { + co.Clear(); + // Execute reorients the contours so that the outer most contour has a positive area. Thus the output + // contours will be CCW oriented even though the input paths are CW oriented. + // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. + co.AddPath(polygon, jtRound, ClipperLib::etClosedPolygon); + bool ccw = ClipperLib::Orientation(polygon); + co.Execute(out_this, ccw ? offset : - offset); + if (! ccw) { + // Reverse the resulting contours. + for (ClipperLib::Path &path : out_this) + std::reverse(path.begin(), path.end()); + } + append(out, std::move(out_this)); + } + return out; +} + +static ClipperLib::Paths wavefront_clip(const ClipperLib::Paths &wavefront, const Polygons &clipping) +{ + ClipperLib::Clipper clipper; + clipper.AddPaths(wavefront, ClipperLib::ptSubject, true); + clipper.AddPaths(ClipperUtils::PolygonsProvider(clipping), ClipperLib::ptClip, true); + ClipperLib::Paths out; + clipper.Execute(ClipperLib::ctIntersection, out, ClipperLib::pftPositive, ClipperLib::pftPositive); + return out; +} + +static Polygons propagate_wave_from_boundary( + ClipperLib::ClipperOffset &co, + // Seed of the wave: Open polylines very close to the boundary. + const ClipperLib::Paths &seed, + // Boundary inside which the waveform will propagate. + const ExPolygon &boundary, + // How much to inflate the seed lines to produce the first wave area. + const float initial_step, + // How much to inflate the first wave area and the successive wave areas in each step. + const float other_step, + // Number of inflate steps after the initial step. + const size_t num_other_steps, + // Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up + // clipping during wave propagation. + const float max_inflation) +{ + assert(! seed.empty() && seed.front().size() >= 2); + Polygons clipping = ClipperUtils::clip_clipper_polygons_with_subject_bbox(boundary, get_extents(seed).inflated(max_inflation)); + ClipperLib::Paths polygons = wavefront_clip(wavefront_initial(co, seed, initial_step), clipping); + // Now offset the remaining + for (size_t ioffset = 0; ioffset < num_other_steps; ++ ioffset) + polygons = wavefront_clip(wavefront_step(co, polygons, other_step), clipping); + return to_polygons(polygons); +} + +// Calculating radius discretization according to ClipperLib offsetter code, see void ClipperOffset::DoOffset(double delta) +inline double clipper_round_offset_error(double offset, double arc_tolerance) +{ + static constexpr const double def_arc_tolerance = 0.25; + const double y = + arc_tolerance <= 0 ? + def_arc_tolerance : + arc_tolerance > offset * def_arc_tolerance ? + offset * def_arc_tolerance : + arc_tolerance; + double steps = std::min(M_PI / std::acos(1. - y / offset), offset * M_PI); + return offset * (1. - cos(M_PI / steps)); +} + +// Returns regions per source ExPolygon expanded into boundary. +std::vector expand_expolygons( + // Source regions that are supposed to touch the boundary. + // Boundaries of source regions touching the "boundary" regions will be expanded into the "boundary" region. + const ExPolygons &src, + const ExPolygons &boundary, + // Scaled expansion value + float full_expansion, + // Expand by waves of expansion_step size (expansion_step is scaled). + float expansion_step, + // Don't take more than max_nr_steps for small expansion_step. + size_t max_nr_expansion_steps) +{ + assert(full_expansion > 0); + assert(expansion_step > 0); + assert(max_nr_expansion_steps > 0); + + // Initial expansion of src to make the source regions intersect with boundary regions just a bit. + float tiny_expansion; + // How much to inflate the seed lines to produce the first wave area. + float initial_step; + // How much to inflate the first wave area and the successive wave areas in each step. + float other_step; + // Number of inflate steps after the initial step. + size_t num_other_steps; + // Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up + // clipping during wave propagation. + float max_inflation; + + // Offsetter to be applied for all inflation waves. Its accuracy is set with the block below. + ClipperLib::ClipperOffset co; + + { + // Initial expansion of src to make the source regions intersect with boundary regions just a bit. + // The expansion should not be too tiny, but also small enough, so the following expansion will + // compensate for tiny_expansion and bring the wave back to the boundary without producing + // ugly cusps where it touches the boundary. + tiny_expansion = std::min(0.25f * full_expansion, scaled(0.05f)); + size_t nsteps = size_t(ceil((full_expansion - tiny_expansion) / expansion_step)); + if (max_nr_expansion_steps > 0) + nsteps = std::min(nsteps, max_nr_expansion_steps); + assert(nsteps > 0); + initial_step = (full_expansion - tiny_expansion) / nsteps; + if (nsteps > 1 && 0.25 * initial_step < tiny_expansion) { + // Decrease the step size by lowering number of steps. + nsteps = std::max(1, (floor((full_expansion - tiny_expansion) / (4. * tiny_expansion)))); + initial_step = (full_expansion - tiny_expansion) / nsteps; + } + if (0.25 * initial_step < tiny_expansion || nsteps == 1) { + tiny_expansion = 0.2f * full_expansion; + initial_step = 0.8f * full_expansion; + } + other_step = initial_step; + num_other_steps = nsteps - 1; + + // Accuracy of the offsetter for wave propagation. + co.ArcTolerance = float(scale_(0.1)); + co.ShortestEdgeLength = std::abs(initial_step * ClipperOffsetShortestEdgeFactor); + + // Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up + // clipping during wave propagation. Needs to be in sync with the offsetter accuracy. + // Clipper positive round offset should rather offset less than more. + // Still a little bit of additional offset was added. + max_inflation = (tiny_expansion + nsteps * initial_step) * 1.1; +// (clipper_round_offset_error(tiny_expansion, co.ArcTolerance) + nsteps * clipper_round_offset_error(initial_step, co.ArcTolerance) * 1.5; // Account for uncertainty + } + + using Intersection = ClipperZUtils::ClipperZIntersectionVisitor::Intersection; + using Intersections = ClipperZUtils::ClipperZIntersectionVisitor::Intersections; + + ClipperLib_Z::Paths expansion_seeds; + Intersections intersections; + + coord_t idx_boundary_begin = 1; + coord_t idx_boundary_end; + coord_t idx_src_end; + + { + ClipperLib_Z::Clipper zclipper; + ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections); + zclipper.ZFillFunction(visitor.clipper_callback()); + // as closed contours + { + ClipperLib_Z::Paths zboundary = ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_begin); + idx_boundary_end = idx_boundary_begin + coord_t(zboundary.size()); + zclipper.AddPaths(zboundary, ClipperLib_Z::ptClip, true); + } + // as open contours + std::vector> zsrc_splits; + { + ClipperLib_Z::Paths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_boundary_end); + zclipper.AddPaths(zsrc, ClipperLib_Z::ptSubject, false); + idx_src_end = idx_boundary_end + coord_t(zsrc.size()); + zsrc_splits.reserve(zsrc.size()); + for (const ClipperLib_Z::Path &path : zsrc) { + assert(path.size() >= 2); + assert(path.front() == path.back()); + zsrc_splits.emplace_back(path.front(), -1); + } + std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); }); + } + ClipperLib_Z::PolyTree polytree; + zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(std::move(polytree), expansion_seeds); + merge_splits(expansion_seeds, zsrc_splits); + } + + // Sort paths into their respective islands. + // Each src x boundary will be processed (wave expanded) independently. + // Multiple pieces of a single src may intersect the same boundary. + struct SeedOrigin { + int src; + int boundary; + int seed; + }; + std::vector map_seeds; + map_seeds.reserve(expansion_seeds.size()); + int iseed = 0; + for (const ClipperLib_Z::Path &path : expansion_seeds) { + assert(path.size() >= 2); + const ClipperLib_Z::IntPoint &front = path.front(); + const ClipperLib_Z::IntPoint &back = path.back(); + // Both ends of a seed segment are supposed to be inside a single boundary expolygon. + assert(front.z() < 0); + assert(back.z() < 0); + const Intersection *intersection = nullptr; + auto intersection_point_valid = [idx_boundary_end, idx_src_end](const Intersection &is) { + return is.first >= 1 && is.first < idx_boundary_end && + is.second >= idx_boundary_end && is.second < idx_src_end; + }; + if (front.z() < 0) { + const Intersection &is = intersections[- front.z() - 1]; + assert(intersection_point_valid(is)); + if (intersection_point_valid(is)) + intersection = &is; + } + if (! intersection && back.z() < 0) { + const Intersection &is = intersections[- back.z() - 1]; + assert(intersection_point_valid(is)); + if (intersection_point_valid(is)) + intersection = &is; + } + if (intersection) { + // The path intersects the boundary contour at least at one side. + map_seeds.push_back({ intersection->second - idx_boundary_end, intersection->first - 1, iseed }); + } + ++ iseed; + } + // Sort the seeds by their intersection boundary and source contour. + std::sort(map_seeds.begin(), map_seeds.end(), [](const auto &l, const auto &r){ + return l.boundary < r.boundary || (l.boundary == r.boundary && l.src < r.src); + }); + + std::vector out(src.size(), Polygons{}); + ClipperLib::Paths paths; + for (auto it_seed = map_seeds.begin(); it_seed != map_seeds.end();) { + auto it = it_seed; + paths.clear(); + for (; it != map_seeds.end() && it->boundary == it_seed->boundary && it->src == it_seed->src; ++ it) + paths.emplace_back(ClipperZUtils::from_zpath(expansion_seeds[it->seed])); + // Propagate the wavefront while clipping it with the trimmed boundary. + // Collect the expanded polygons, merge them with the source polygons. + append(out[it_seed->src], propagate_wave_from_boundary(co, paths, boundary[it_seed->boundary], initial_step, other_step, num_other_steps, max_inflation)); + it_seed = it; + } + + return out; +} + +} // Algorithm +} // Slic3r diff --git a/src/libslic3r/Algorithm/RegionExpansion.hpp b/src/libslic3r/Algorithm/RegionExpansion.hpp new file mode 100644 index 000000000..bbfcc0a65 --- /dev/null +++ b/src/libslic3r/Algorithm/RegionExpansion.hpp @@ -0,0 +1,26 @@ +#ifndef SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_ +#define SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_ + +#include + +namespace Slic3r { + +class Polygon; +using Polygons = std::vector; +class ExPolygon; +using ExPolygons = std::vector; + +namespace Algorithm { + +std::vector expand_expolygons(const ExPolygons &src, const ExPolygons &boundary, + // Scaled expansion value + float expansion, + // Expand by waves of expansion_step size (expansion_step is scaled). + float expansion_step, + // Don't take more than max_nr_steps for small expansion_step. + size_t max_nr_steps); + +} // Algorithm +} // Slic3r + +#endif /* SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_ */ diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index 6c16d08b7..3c124fe2a 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -22,24 +22,9 @@ public: BoundingBoxBase(const PointClass &p1, const PointClass &p2, const PointClass &p3) : min(p1), max(p1), defined(false) { merge(p2); merge(p3); } - template > - BoundingBoxBase(It from, It to) : min(PointClass::Zero()), max(PointClass::Zero()) - { - if (from == to) { - this->defined = false; - // throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBoxBase constructor"); - } else { - auto it = from; - this->min = it->template cast(); - this->max = this->min; - for (++ it; it != to; ++ it) { - auto vec = it->template cast(); - this->min = this->min.cwiseMin(vec); - this->max = this->max.cwiseMax(vec); - } - this->defined = (this->min.x() < this->max.x()) && (this->min.y() < this->max.y()); - } - } + template> + BoundingBoxBase(It from, It to) + { construct(*this, from, to); } BoundingBoxBase(const std::vector &points) : BoundingBoxBase(points.begin(), points.end()) @@ -70,6 +55,30 @@ public: } bool operator==(const BoundingBoxBase &rhs) { return this->min == rhs.min && this->max == rhs.max; } bool operator!=(const BoundingBoxBase &rhs) { return ! (*this == rhs); } + +private: + // to access construct() + friend BoundingBox get_extents(const Points &pts); + friend BoundingBox get_extents(const Points &pts); + + // if IncludeBoundary, then a bounding box is defined even for a single point. + // otherwise a bounding box is only defined if it has a positive area. + // The output bounding box is expected to be set to "undefined" initially. + template> + static void construct(BoundingBoxType &out, It from, It to) + { + if (from != to) { + auto it = from; + out.min = it->template cast(); + out.max = out.min; + for (++ it; it != to; ++ it) { + auto vec = it->template cast(); + out.min = out.min.cwiseMin(vec); + out.max = out.max.cwiseMax(vec); + } + out.defined = IncludeBoundary || (out.min.x() < out.max.x() && out.min.y() < out.max.y()); + } + } }; template diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 061cf1423..eed003be2 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -641,7 +641,7 @@ ExtrusionEntityCollection make_brim(const Print &print, PrintTryCancel try_cance // perform operation ClipperLib_Z::PolyTree loops_trimmed_tree; clipper.Execute(ClipperLib_Z::ctDifference, loops_trimmed_tree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - ClipperLib_Z::PolyTreeToPaths(loops_trimmed_tree, loops_trimmed); + ClipperLib_Z::PolyTreeToPaths(std::move(loops_trimmed_tree), loops_trimmed); } // Third, produce the extrusions, sorted by the source loop indices. diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 06e32e2a2..bdf057f58 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -22,6 +22,8 @@ set(SLIC3R_SOURCES AABBTreeLines.hpp AABBMesh.hpp AABBMesh.cpp + Algorithm/RegionExpansion.cpp + Algorithm/RegionExpansion.hpp BoundingBox.cpp BoundingBox.hpp BridgeDetector.cpp @@ -35,6 +37,7 @@ set(SLIC3R_SOURCES clipper.hpp ClipperUtils.cpp ClipperUtils.hpp + ClipperZUtils.hpp Color.cpp Color.hpp Config.cpp diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 4762c3e24..abeec6893 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -8,8 +8,6 @@ #include "SVG.hpp" #endif /* CLIPPER_UTILS_DEBUG */ -#define CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR (0.005f) - namespace Slic3r { #ifdef CLIPPER_UTILS_DEBUG @@ -267,7 +265,7 @@ static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, Clipper co.ArcTolerance = miterLimit; else co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(std::abs(offset * CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR)); + co.ShortestEdgeLength = std::abs(offset * ClipperOffsetShortestEdgeFactor); for (const ClipperLib::Path &path : paths) { co.Clear(); // Execute reorients the contours so that the outer most contour has a positive area. Thus the output @@ -414,7 +412,7 @@ static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float d co.ArcTolerance = miterLimit; else co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(std::abs(delta * CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR)); + co.ShortestEdgeLength = std::abs(delta * ClipperOffsetShortestEdgeFactor); co.AddPath(expoly.contour.points, joinType, ClipperLib::etClosedPolygon); co.Execute(contours, delta); } @@ -435,7 +433,7 @@ static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float d co.ArcTolerance = miterLimit; else co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(std::abs(delta * CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR)); + co.ShortestEdgeLength = std::abs(delta * ClipperOffsetShortestEdgeFactor); co.AddPath(hole.points, joinType, ClipperLib::etClosedPolygon); ClipperLib::Paths out2; // Execute reorients the contours so that the outer most contour has a positive area. Thus the output @@ -1055,7 +1053,7 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v }; // Minimum edge length, squared. - double lmin = *std::max_element(deltas.begin(), deltas.end()) * CLIPPER_OFFSET_SHORTEST_EDGE_FACTOR; + double lmin = *std::max_element(deltas.begin(), deltas.end()) * ClipperOffsetShortestEdgeFactor; double l2min = lmin * lmin; // Minimum angle to consider two edges to be parallel. // Vojtech's estimate. diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 65ecfb76a..a9bd36fe1 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -39,6 +39,9 @@ static constexpr const Slic3r::ClipperLib::JoinType DefaultLineJoinType = Sl // Miter limit is ignored for jtSquare. static constexpr const double DefaultLineMiterLimit = 0.; +// Decimation factor applied on input contour when doing offset, multiplied by the offset distance. +static constexpr const double ClipperOffsetShortestEdgeFactor = 0.005; + enum class ApplySafetyOffset { No, Yes @@ -599,6 +602,6 @@ Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit = 2.); -} +} // namespace Slic3r -#endif +#endif // slic3r_ClipperUtils_hpp_ diff --git a/src/libslic3r/ClipperZUtils.hpp b/src/libslic3r/ClipperZUtils.hpp new file mode 100644 index 000000000..d69b2e28a --- /dev/null +++ b/src/libslic3r/ClipperZUtils.hpp @@ -0,0 +1,143 @@ +#ifndef slic3r_ClipperZUtils_hpp_ +#define slic3r_ClipperZUtils_hpp_ + +#include + +#include +#include + +namespace Slic3r { + +namespace ClipperZUtils { + +using ZPoint = ClipperLib_Z::IntPoint; +using ZPoints = ClipperLib_Z::Path; +using ZPath = ClipperLib_Z::Path; +using ZPaths = ClipperLib_Z::Paths; + +inline bool zpoint_lower(const ZPoint &l, const ZPoint &r) +{ + return l.x() < r.x() || (l.x() == r.x() && (l.y() < r.y() || (l.y() == r.y() && l.z() < r.z()))); +} + +// Convert a single path to path with a given Z coordinate. +// If Open, then duplicate the first point at the end. +template +inline ZPath to_zpath(const Points &path, coord_t z) +{ + ZPath out; + if (! path.empty()) { + out.reserve(path.size() + Open ? 1 : 0); + for (const Point &p : path) + out.emplace_back(p.x(), p.y(), z); + if (Open) + out.emplace_back(out.front()); + } + return out; +} + +// Convert multiple paths to paths with a given Z coordinate. +// If Open, then duplicate the first point of each path at its end. +template +inline ZPaths to_zpaths(const std::vector &paths, coord_t z) +{ + ZPaths out; + out.reserve(paths.size()); + for (const Points &path : paths) + out.emplace_back(to_zpath(path, z)); + return out; +} + +// Convert multiple expolygons into z-paths with Z specified by an index of the source expolygon +// offsetted by base_index. +// If Open, then duplicate the first point of each path at its end. +template +inline ZPaths expolygons_to_zpaths(const ExPolygons &src, coord_t base_idx) +{ + ZPaths out; + out.reserve(std::accumulate(src.begin(), src.end(), size_t(0), + [](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); })); + coord_t z = base_idx; + for (const ExPolygon &expoly : src) { + out.emplace_back(to_zpath(expoly.contour.points, z)); + for (const Polygon &hole : expoly.holes) + out.emplace_back(to_zpath(hole.points, z)); + ++ z; + } + return out; +} + +// Convert a single path to path with a given Z coordinate. +// If Open, then duplicate the first point at the end. +template +inline Points from_zpath(const ZPoints &path) +{ + Points out; + if (! path.empty()) { + out.reserve(path.size() + Open ? 1 : 0); + for (const ZPoint &p : path) + out.emplace_back(p.x(), p.y()); + if (Open) + out.emplace_back(out.front()); + } + return out; +} + +// Convert multiple paths to paths with a given Z coordinate. +// If Open, then duplicate the first point of each path at its end. +template +inline void from_zpaths(const ZPaths &paths, std::vector &out) +{ + out.reserve(out.size() + paths.size()); + for (const ZPoints &path : paths) + out.emplace_back(from_zpath(path)); +} +template +inline std::vector from_zpaths(const ZPaths &paths) +{ + std::vector out; + from_zpaths(paths, out); + return out; +} + +class ClipperZIntersectionVisitor { +public: + using Intersection = std::pair; + using Intersections = std::vector; + ClipperZIntersectionVisitor(Intersections &intersections) : m_intersections(intersections) {} + void reset() { m_intersections.clear(); } + void operator()(const ZPoint &e1bot, const ZPoint &e1top, const ZPoint &e2bot, const ZPoint &e2top, ZPoint &pt) { + coord_t srcs[4]{ e1bot.z(), e1top.z(), e2bot.z(), e2top.z() }; + coord_t *begin = srcs; + coord_t *end = srcs + 4; + //FIXME bubble sort manually? + std::sort(begin, end); + end = std::unique(begin, end); + if (begin + 1 == end) { + // Self intersection may happen on source contour. Just copy the Z value. + pt.z() = *begin; + } else { + assert(begin + 2 == end); + if (begin + 2 <= end) { + // store a -1 based negative index into the "intersections" vector here. + m_intersections.emplace_back(srcs[0], srcs[1]); + pt.z() = -coord_t(m_intersections.size()); + } + } + } + ClipperLib_Z::ZFillCallback clipper_callback() { + return [this](const ZPoint &e1bot, const ZPoint &e1top, + const ZPoint &e2bot, const ZPoint &e2top, ZPoint &pt) + { return (*this)(e1bot, e1top, e2bot, e2top, pt); }; + } + + const std::vector>& intersections() const { return m_intersections; } + +private: + std::vector> &m_intersections; +}; + +} // namespace ClipperZUtils +} // namespace Slic3r + +#endif // slic3r_ClipperZUtils_hpp_ diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 35b4a331a..7b44b8b18 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -1,5 +1,5 @@ #include "Layer.hpp" -#include +#include "ClipperZUtils.hpp" #include "ClipperUtils.hpp" #include "Print.hpp" #include "Fill/Fill.hpp" @@ -304,48 +304,16 @@ void Layer::build_up_down_graph(Layer& below, Layer& above) coord_t paths_end = paths_above_offset + coord_t(above.lslices.size()); #endif // NDEBUG - class ZFill { - public: - ZFill() = default; - void reset() { m_intersections.clear(); } - void operator()( - const ClipperLib_Z::IntPoint& e1bot, const ClipperLib_Z::IntPoint& e1top, - const ClipperLib_Z::IntPoint& e2bot, const ClipperLib_Z::IntPoint& e2top, - ClipperLib_Z::IntPoint& pt) { - coord_t srcs[4]{ e1bot.z(), e1top.z(), e2bot.z(), e2top.z() }; - coord_t* begin = srcs; - coord_t* end = srcs + 4; - std::sort(begin, end); - end = std::unique(begin, end); - if (begin + 1 == end) { - // Self intersection may happen on source contour. Just copy the Z value. - pt.z() = *begin; - } else { - assert(begin + 2 == end); - if (begin + 2 <= end) { - // store a -1 based negative index into the "intersections" vector here. - m_intersections.emplace_back(srcs[0], srcs[1]); - pt.z() = -coord_t(m_intersections.size()); - } - } - } - const std::vector>& intersections() const { return m_intersections; } - - private: - std::vector> m_intersections; - } zfill; - ClipperLib_Z::Clipper clipper; ClipperLib_Z::PolyTree result; - clipper.ZFillFunction( - [&zfill](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, - const ClipperLib_Z::IntPoint &e2bot, const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) - { return zfill(e1bot, e1top, e2bot, e2top, pt); }); + ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections; + ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections); + clipper.ZFillFunction(visitor.clipper_callback()); clipper.AddPaths(paths_below, ClipperLib_Z::ptSubject, true); clipper.AddPaths(paths_above, ClipperLib_Z::ptClip, true); clipper.Execute(ClipperLib_Z::ctIntersection, result, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - connect_layer_slices(below, above, result, zfill.intersections(), paths_below_offset, paths_above_offset + connect_layer_slices(below, above, result, intersections, paths_below_offset, paths_above_offset #ifndef NDEBUG , paths_end #endif // NDEBUG diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index c700c45f8..6ca37d7b4 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -433,10 +433,12 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path &subject, con clipper.AddPath(subject, ClipperLib_Z::ptSubject, false); clipper.AddPaths(clip, ClipperLib_Z::ptClip, true); - ClipperLib_Z::PolyTree clipped_polytree; ClipperLib_Z::Paths clipped_paths; - clipper.Execute(clipType, clipped_polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - ClipperLib_Z::PolyTreeToPaths(clipped_polytree, clipped_paths); + { + ClipperLib_Z::PolyTree clipped_polytree; + clipper.Execute(clipType, clipped_polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(std::move(clipped_polytree), clipped_paths); + } // Clipped path could contain vertices from the clip with a Z coordinate equal to zero. // For those vertices, we must assign value based on the subject. diff --git a/src/libslic3r/Point.cpp b/src/libslic3r/Point.cpp index beb496b28..794b27c44 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -84,18 +84,15 @@ Points collect_duplicates(Points pts /* Copy */) return duplicits; } +template BoundingBox get_extents(const Points &pts) { - return BoundingBox(pts); -} - -BoundingBox get_extents(const std::vector &pts) -{ - BoundingBox bbox; - for (const Points &p : pts) - bbox.merge(get_extents(p)); - return bbox; + BoundingBox out; + BoundingBox::construct(out, pts.begin(), pts.end()); + return out; } +template BoundingBox get_extents(const Points &pts); +template BoundingBox get_extents(const Points &pts); BoundingBoxf get_extents(const std::vector &pts) { diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 32dcb82d0..389fa313b 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -229,8 +229,24 @@ inline Point lerp(const Point &a, const Point &b, double t) return ((1. - t) * a.cast() + t * b.cast()).cast(); } +// if IncludeBoundary, then a bounding box is defined even for a single point. +// otherwise a bounding box is only defined if it has a positive area. +template BoundingBox get_extents(const Points &pts); -BoundingBox get_extents(const std::vector &pts); +extern template BoundingBox get_extents(const Points& pts); +extern template BoundingBox get_extents(const Points& pts); + +// if IncludeBoundary, then a bounding box is defined even for a single point. +// otherwise a bounding box is only defined if it has a positive area. +template +BoundingBox get_extents(const std::vector &pts) +{ + BoundingBox bbox; + for (const Points &p : pts) + bbox.merge(get_extents(p)); + return bbox; +} + BoundingBoxf get_extents(const std::vector &pts); int nearest_point_index(const Points &points, const Point &pt); diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index bee5e51ba..de8f859a5 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -153,6 +153,25 @@ inline void polylines_append(Polylines &dst, Polylines &&src) } } +// Merge polylines at their respective end points. +// dst_first: the merge point is at dst.begin() or dst.end()? +// src_first: the merge point is at src.begin() or src.end()? +// The orientation of the resulting polyline is unknown, the output polyline may start +// either with src piece or dst piece. +template +inline void polylines_merge(std::vector &dst, bool dst_first, std::vector &&src, bool src_first) +{ + if (dst_first) { + if (src_first) + std::reverse(dst.begin(), dst.end()); + else + std::swap(dst, src); + } else if (! src_first) + std::reverse(src.begin(), src.end()); + // Merge src into dst. + append(dst, std::move(src)); +} + const Point& leftmost_point(const Polylines &polylines); bool remove_degenerate(Polylines &polylines); diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index d5a21cf21..2ec12c529 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -124,8 +124,7 @@ inline void append(std::vector& dest, std::vector&& src) dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end())); - - // Vojta wants back compatibility + // Release memory of the source contour now. src.clear(); src.shrink_to_fit(); } @@ -161,8 +160,7 @@ inline void append_reversed(std::vector& dest, std::vector&& src) dest.insert(dest.end(), std::make_move_iterator(src.rbegin()), std::make_move_iterator(src.rend())); - - // Vojta wants back compatibility + // Release memory of the source contour now. src.clear(); src.shrink_to_fit(); } diff --git a/tests/fff_print/test_clipper.cpp b/tests/fff_print/test_clipper.cpp index ea923b48e..596876975 100644 --- a/tests/fff_print/test_clipper.cpp +++ b/tests/fff_print/test_clipper.cpp @@ -1,7 +1,7 @@ #include #include "test_data.hpp" -#include "clipper/clipper_z.hpp" +#include "libslic3r/ClipperZUtils.hpp" #include "libslic3r/clipper.hpp" using namespace Slic3r; @@ -132,3 +132,72 @@ SCENARIO("Clipper Z", "[ClipperZ]") REQUIRE(pt.z() == 1); } +SCENARIO("Intersection with multiple polylines", "[ClipperZ]") +{ + // 1000x1000 CCQ square + ClipperLib_Z::Path clip { { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } }; + // Two lines interseting inside the square above, crossing the bottom edge of the square. + ClipperLib_Z::Path line1 { { +100, -100, 2 }, { +900, +900, 2 } }; + ClipperLib_Z::Path line2 { { +100, +900, 3 }, { +900, -100, 3 } }; + + ClipperLib_Z::Clipper clipper; + ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections; + ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections); + clipper.ZFillFunction(visitor.clipper_callback()); + clipper.AddPath(line1, ClipperLib_Z::ptSubject, false); + clipper.AddPath(line2, ClipperLib_Z::ptSubject, false); + clipper.AddPath(clip, ClipperLib_Z::ptClip, true); + + ClipperLib_Z::PolyTree polytree; + ClipperLib_Z::Paths paths; + clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(polytree, paths); + + REQUIRE(paths.size() == 2); + + THEN("First output polyline is a trimmed 2nd line") { + // Intermediate point (intersection) was removed) + REQUIRE(paths.front().size() == 2); + REQUIRE(paths.front().front().z() == 3); + REQUIRE(paths.front().back().z() < 0); + REQUIRE(intersections[- paths.front().back().z() - 1] == std::pair(1, 3)); + } + + THEN("Second output polyline is a trimmed 1st line") { + // Intermediate point (intersection) was removed) + REQUIRE(paths[1].size() == 2); + REQUIRE(paths[1].front().z() < 0); + REQUIRE(paths[1].back().z() == 2); + REQUIRE(intersections[- paths[1].front().z() - 1] == std::pair(1, 2)); + } +} + +SCENARIO("Interseting a closed loop as an open polyline", "[ClipperZ]") +{ + // 1000x1000 CCQ square + ClipperLib_Z::Path clip{ { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } }; + // Two lines interseting inside the square above, crossing the bottom edge of the square. + ClipperLib_Z::Path rect{ { 500, 500, 2}, { 500, 1500, 2 }, { 1500, 1500, 2}, { 500, 1500, 2}, { 500, 500, 2 } }; + + ClipperLib_Z::Clipper clipper; + clipper.AddPath(rect, ClipperLib_Z::ptSubject, false); + clipper.AddPath(clip, ClipperLib_Z::ptClip, true); + + ClipperLib_Z::PolyTree polytree; + ClipperLib_Z::Paths paths; + ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections; + ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections); + clipper.ZFillFunction(visitor.clipper_callback()); + clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + ClipperLib_Z::PolyTreeToPaths(std::move(polytree), paths); + + THEN("Open polyline is clipped into two pieces") { + REQUIRE(paths.size() == 2); + REQUIRE(paths.front().size() == 2); + REQUIRE(paths.back().size() == 2); + REQUIRE(paths.front().front().z() == 2); + REQUIRE(paths.back().back().z() == 2); + REQUIRE(paths.front().front().x() == paths.back().back().x()); + REQUIRE(paths.front().front().y() == paths.back().back().y()); + } +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index ae2474ad5..971db528c 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(${_TEST_NAME}_tests test_stl.cpp test_meshboolean.cpp test_marchingsquares.cpp + test_region_expansion.cpp test_timeutils.cpp test_utils.cpp test_voronoi.cpp diff --git a/tests/libslic3r/test_region_expansion.cpp b/tests/libslic3r/test_region_expansion.cpp new file mode 100644 index 000000000..5d83b8e9d --- /dev/null +++ b/tests/libslic3r/test_region_expansion.cpp @@ -0,0 +1,254 @@ +#include + +#include +#include +#include +#include +#include +#include + +using namespace Slic3r; + +//#define DEBUG_TEMP_DIR "d:\\temp\\" + +SCENARIO("Region expansion basics", "[RegionExpansion]") { + static constexpr const coord_t ten = scaled(10.); + GIVEN("two touching squares") { + Polygon square1{ { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 2 * ten }, { 1 * ten, 2 * ten } }; + Polygon square2{ { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } }; + Polygon square3{ { 1 * ten, 2 * ten }, { 2 * ten, 2 * ten }, { 2 * ten, 3 * ten }, { 1 * ten, 3 * ten } }; + static constexpr const float expansion = scaled(1.); + auto test_expansion = [](const Polygon &src, const Polygon &boundary) { + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{src} }, { ExPolygon{boundary} }, + expansion, + scaled(0.3), // expansion step + 5); // max num steps + THEN("Single anchor is produced") { + REQUIRE(expanded.size() == 1); + } + THEN("The area of the anchor is 10mm2") { + REQUIRE(area(expanded.front()) == Approx(expansion * ten)); + } + }; + + WHEN("second square expanded into the first square (to left)") { + test_expansion(square2, square1); + } + WHEN("first square expanded into the second square (to right)") { + test_expansion(square1, square2); + } + WHEN("third square expanded into the first square (down)") { + test_expansion(square3, square1); + } + WHEN("first square expanded into the third square (up)") { + test_expansion(square1, square3); + } + } + + GIVEN("simple bridge") { + Polygon square1{ { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 2 * ten }, { 1 * ten, 2 * ten } }; + Polygon square2{ { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } }; + Polygon square3{ { 3 * ten, 1 * ten }, { 4 * ten, 1 * ten }, { 4 * ten, 2 * ten }, { 3 * ten, 2 * ten } }; + + WHEN("expanded") { + static constexpr const float expansion = scaled(1.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{square2} }, { ExPolygon{square1}, ExPolygon{square3} }, + expansion, + scaled(0.3), // expansion step + 5); // max num steps + THEN("Two anchors are produced") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 2); + } + THEN("The area of each anchor is 10mm2") { + REQUIRE(area(expanded.front().front()) == Approx(expansion * ten)); + REQUIRE(area(expanded.front().back()) == Approx(expansion * ten)); + } + } + + WHEN("fully expanded") { + static constexpr const float expansion = scaled(10.1); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{square2} }, { ExPolygon{square1}, ExPolygon{square3} }, + expansion, + scaled(2.3), // expansion step + 5); // max num steps + THEN("Two anchors are produced") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 2); + } + THEN("The area of each anchor is 100mm2") { + REQUIRE(area(expanded.front().front()) == Approx(sqr(ten))); + REQUIRE(area(expanded.front().back()) == Approx(sqr(ten))); + } + } + } + + GIVEN("two bridges") { + Polygon left_support { { 1 * ten, 1 * ten }, { 2 * ten, 1 * ten }, { 2 * ten, 4 * ten }, { 1 * ten, 4 * ten } }; + Polygon right_support { { 3 * ten, 1 * ten }, { 4 * ten, 1 * ten }, { 4 * ten, 4 * ten }, { 3 * ten, 4 * ten } }; + Polygon bottom_bridge { { 2 * ten, 1 * ten }, { 3 * ten, 1 * ten }, { 3 * ten, 2 * ten }, { 2 * ten, 2 * ten } }; + Polygon top_bridge { { 2 * ten, 3 * ten }, { 3 * ten, 3 * ten }, { 3 * ten, 4 * ten }, { 2 * ten, 4 * ten } }; + + WHEN("expanded") { + static constexpr const float expansion = scaled(1.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{bottom_bridge}, ExPolygon{top_bridge} }, { ExPolygon{left_support}, ExPolygon{right_support} }, + expansion, + scaled(0.3), // expansion step + 5); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "two_bridges-out.svg", + { { { { ExPolygon{left_support}, ExPolygon{right_support} } }, { "supports", "orange", 0.5f } }, + { { { ExPolygon{bottom_bridge}, ExPolygon{top_bridge} } }, { "bridges", "blue", 0.5f } }, + { { union_ex(union_(expanded.front(), expanded.back())) }, { "expanded", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("Two anchors are produced for each bridge") { + REQUIRE(expanded.size() == 2); + REQUIRE(expanded.front().size() == 2); + REQUIRE(expanded.back().size() == 2); + } + THEN("The area of each anchor is 10mm2") { + double a = expansion * ten + M_PI * sqr(expansion) / 4; + double eps = sqr(scaled(0.1)); + REQUIRE(is_approx(area(expanded.front().front()), a, eps)); + REQUIRE(is_approx(area(expanded.front().back()), a, eps)); + REQUIRE(is_approx(area(expanded.back().front()), a, eps)); + REQUIRE(is_approx(area(expanded.back().back()), a, eps)); + } + } + } + + GIVEN("rectangle with rhombic cut-out") { + double diag = 1 * ten * sqrt(2.) / 4.; + Polygon square_with_rhombic_cutout{ { 0, 0 }, { 1 * ten, 0 }, { ten / 2, ten / 2 }, { 1 * ten, 1 * ten }, { 0, 1 * ten } }; + Polygon rhombic { { ten / 2, ten / 2 }, { 3 * ten / 4, ten / 4 }, { 1 * ten, ten / 2 }, { 3 * ten / 4, 3 * ten / 4 } }; + + WHEN("expanded") { + static constexpr const float expansion = scaled(1.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{rhombic} }, { ExPolygon{square_with_rhombic_cutout} }, + expansion, + scaled(0.1), // expansion step + 11); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "rectangle_with_rhombic_cut-out.svg", + { { { { ExPolygon{square_with_rhombic_cutout} } }, { "square_with_rhombic_cutout", "orange", 0.5f } }, + { { { ExPolygon{rhombic} } }, { "rhombic", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("Single anchor is produced") { + REQUIRE(expanded.size() == 1); + } + THEN("The area of anchor is correct") { + double area_calculated = area(expanded.front()); + double area_expected = 2. * diag * expansion + M_PI * sqr(expansion) * 0.75; + REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled(0.2)))); + } + } + + WHEN("extra expanded") { + static constexpr const float expansion = scaled(2.5); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{rhombic} }, { ExPolygon{square_with_rhombic_cutout} }, + expansion, + scaled(0.25), // expansion step + 11); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "rectangle_with_rhombic_cut-out2.svg", + { { { { ExPolygon{square_with_rhombic_cutout} } }, { "square_with_rhombic_cutout", "orange", 0.5f } }, + { { { ExPolygon{rhombic} } }, { "rhombic", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("Single anchor is produced") { + REQUIRE(expanded.size() == 1); + } + THEN("The area of anchor is correct") { + double area_calculated = area(expanded.front()); + double area_expected = 2. * diag * expansion + M_PI * sqr(expansion) * 0.75; + REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled(0.3)))); + } + } + } + + GIVEN("square with two holes") { + Polygon outer{ { 0, 0 }, { 3 * ten, 0 }, { 3 * ten, 5 * ten }, { 0, 5 * ten } }; + Polygon hole1{ { 1 * ten, 1 * ten }, { 1 * ten, 2 * ten }, { 2 * ten, 2 * ten }, { 2 * ten, 1 * ten } }; + Polygon hole2{ { 1 * ten, 3 * ten }, { 1 * ten, 4 * ten }, { 2 * ten, 4 * ten }, { 2 * ten, 3 * ten } }; + ExPolygon boundary(outer); + boundary.holes = { hole1, hole2 }; + + Polygon anchor{ { -1 * ten, coord_t(1.5 * ten) }, { 0 * ten, coord_t(1.5 * ten) }, { 0, coord_t(3.5 * ten) }, { -1 * ten, coord_t(3.5 * ten) } }; + + WHEN("expanded") { + static constexpr const float expansion = scaled(5.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary }, + expansion, + scaled(0.4), // expansion step + 15); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-out.svg", + { { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } }, + { { { boundary } }, { "boundary", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("The anchor expands into a single region") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 1); + } + THEN("The area of anchor is correct") { + double area_calculated = area(expanded.front()); + double area_expected = double(expansion) * 2. * double(ten) + M_PI * sqr(expansion) * 0.5; + REQUIRE(is_approx(area_expected, area_calculated, sqr(scaled(0.45)))); + } + } + WHEN("expanded even more") { + static constexpr const float expansion = scaled(25.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary }, + expansion, + scaled(2.), // expansion step + 15); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded2-out.svg", + { { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } }, + { { { boundary } }, { "boundary", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("The anchor expands into a single region") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 1); + } + } + WHEN("expanded yet even more") { + static constexpr const float expansion = scaled(28.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary }, + expansion, + scaled(2.), // expansion step + 20); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded3-out.svg", + { { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } }, + { { { boundary } }, { "boundary", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("The anchor expands into a single region with two holes") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 3); + } + } + WHEN("expanded fully") { + static constexpr const float expansion = scaled(35.); + std::vector expanded = Algorithm::expand_expolygons({ ExPolygon{anchor} }, { boundary }, + expansion, + scaled(2.), // expansion step + 25); // max num steps +#if 0 + SVG::export_expolygons(DEBUG_TEMP_DIR "square_with_two_holes-expanded_fully-out.svg", + { { { { ExPolygon{anchor} } }, { "anchor", "orange", 0.5f } }, + { { { boundary } }, { "boundary", "blue", 0.5f } }, + { { union_ex(expanded.front()) }, { "expanded", "red", "black", "", scaled(0.1f), 0.5f } } }); +#endif + THEN("The anchor expands into a single region with two holes, fully covering the boundary") { + REQUIRE(expanded.size() == 1); + REQUIRE(expanded.front().size() == 3); + REQUIRE(area(expanded.front()) == Approx(area(boundary))); + } + } + } +}