diff --git a/src/libslic3r/MutablePolygon.cpp b/src/libslic3r/MutablePolygon.cpp index 951485259d..f166ce7010 100644 --- a/src/libslic3r/MutablePolygon.cpp +++ b/src/libslic3r/MutablePolygon.cpp @@ -1,5 +1,6 @@ #include "MutablePolygon.hpp" #include "Line.hpp" +#include "libslic3r.h" namespace Slic3r { @@ -36,207 +37,295 @@ void remove_duplicates(MutablePolygon &polygon, double eps) } } -// Sample a point on line (a, b) at distance "dist" from ref_pt. -// If two points fulfill the condition, then the first one (closer to point a) is taken. -// If none of the two points falls on line (a, b), return false. -template -static inline VectorType point_on_line_at_dist(const VectorType &a, const VectorType &b, const VectorType &ref_pt, const double dist) +// Adapted from Cura ConstPolygonRef::smooth_corner_complex() by Tim Kuipers. +// A concave corner at it1 with position p1 has been removed by the caller between it0 and it2, where |p2 - p0| < shortcut_length. +// Now try to close a concave crack by walking left from it0 and right from it2 as long as the new clipping edge is smaller than shortcut_length +// and the new clipping edge is still inside the polygon (it is a diagonal, it does not intersect polygon boundary). +// Once the traversal stops (always at a clipping edge shorter than shortcut_length), the final trapezoid is clipped with a new clipping edge of shortcut_length. +// Return true if a hole was completely closed (degenerated to an empty polygon) or a single CCW triangle was left, which is not to be simplified any further. +// it0, it2 are updated to the final clipping edge. +static bool clip_narrow_corner( + const Vec2i64 p1, + MutablePolygon::iterator &it0, + MutablePolygon::iterator &it2, + MutablePolygon::range &unprocessed_range, + int64_t dist2_current, + const int64_t shortcut_length) { - using T = typename VectorType::Scalar; - auto v = b - a; - auto l2 = v.squaredNorm(); - assert(l2 > T(0)); - auto vpt = ref_pt - a; - // Parameter of the foot point of ref_pt on line (a, b). - auto t = v.dot(vpt) / l2; - // Foot point of ref_pt on line (a, b). - auto foot_pt = a + t * v; - auto dfoot2 = vpt.squaredNorm() - (foot_pt - ref_pt).squaredNorm(); - // Distance of the result point from the foot point, normalized to length of (a, b). - auto dfoot = dfoot2 > T(0) ? sqrt(dfoot2) / sqrt(l2) : T(0); - auto t_result = t - dfoot; - if (t_result < T(0)) - t_result = t + dfoot; - t_result = Slic3r::clamp(0., 1., t_result); - return a + v * t; -} + MutablePolygon &polygon = it0.polygon(); + assert(polygon.size() >= 2); -static bool smooth_corner_complex(const Vec2d p1, MutablePolygon::iterator &it0, MutablePolygon::iterator &it2, const double shortcut_length) -{ - // walk away from the corner until the shortcut > shortcut_length or it would smooth a piece inward - // - walk in both directions untill shortcut > shortcut_length - // - stop walking in one direction if it would otherwise cut off a corner in that direction - // - same in the other direction - // - stop if both are cut off - // walk by updating p0_it and p2_it - double shortcut_length2 = shortcut_length * shortcut_length; - bool forward_is_blocked = false; - bool forward_is_too_far = false; - bool backward_is_blocked = false; - bool backward_is_too_far = false; - for (;;) { - const bool forward_has_converged = forward_is_blocked || forward_is_too_far; - const bool backward_has_converged = backward_is_blocked || backward_is_too_far; - if (forward_has_converged && backward_has_converged) { - if (forward_is_too_far && backward_is_too_far && (*it0.prev() - *it2.next()).cast().squaredNorm() < shortcut_length2) { - // Trim the narrowing region. - -- it0; - ++ it2; - forward_is_too_far = false; - backward_is_too_far = false; - continue; - } else - break; - } + const int64_t shortcut_length2 = sqr(shortcut_length); - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - if (! forward_has_converged && (backward_has_converged || (p2 - p1).squaredNorm() < (p0 - p1).squaredNorm())) { - // walk forward - const auto it2_2 = it2.next(); - const Vec2d p2_2 = it2_2->cast(); - if (cross2(p2 - p0, p2_2 - p0) > 0) { - forward_is_blocked = true; - } else if ((p2_2 - p0).squaredNorm() > shortcut_length2) { - forward_is_too_far = true; + enum Status { + Free, + Blocked, + Far, + }; + Status forward = Free; + Status backward = Free; + + Vec2i64 p0 = it0->cast(); + Vec2i64 p2 = it2->cast(); + Vec2i64 p02; + Vec2i64 p22; + int64_t dist2_next; + + // As long as there is at least a single triangle left in the polygon. + while (polygon.size() >= 3) { + assert(dist2_current <= shortcut_length2); + if (forward == Far && backward == Far) { + p02 = it0.prev()->cast(); + p22 = it2.next()->cast(); + auto d2 = (p22 - p02).squaredNorm(); + if (d2 <= shortcut_length2) { + // The region was narrow until now and it is still narrow. Trim at both sides. + it0 = unprocessed_range.remove_back(it0).prev(); + it2 = unprocessed_range.remove_front(it2); + if (polygon.size() <= 2) + // A hole degenerated to an empty polygon. + return true; + forward = Free; + backward = Free; + dist2_current = d2; + p0 = p02; + p2 = p22; } else { - it2 = it2_2; // make one step in the forward direction - backward_is_blocked = false; // invalidate data about backward walking - backward_is_too_far = false; + // The region is widening. Stop traversal and trim the final trapezoid. + dist2_next = d2; + break; + } + } else if (forward != Free && backward != Free) + // One of the corners is blocked, the other is blocked or too far. Stop traversal. + break; + // Try to proceed by flipping a diagonal. + // Progress by keeping the distance of the clipping edge end points equal to initial p1. + //FIXME This is an arbitrary condition, maybe a more local condition will be better (take a shorter diagonal?). + if (forward == Free && (backward != Free || (p2 - p1).squaredNorm() < (p0 - p1).cast().squaredNorm())) { + p22 = it2.next()->cast(); + if (cross2(p2 - p0, p22 - p0) > 0) + forward = Blocked; + else { + // New clipping edge lenght. + auto d2 = (p22 - p0).squaredNorm(); + if (d2 > shortcut_length2) { + forward = Far; + dist2_next = d2; + } else { + forward = Free; + // Make one step in the forward direction. + it2 = unprocessed_range.remove_front(it2); + p2 = p22; + dist2_current = d2; + } } } else { - // walk backward - const auto it0_2 = it0.prev(); - const Vec2d p0_2 = it0_2->cast(); - if (cross2(p0_2 - p0, p2 - p0_2) > 0) { - backward_is_blocked = true; - } else if ((p2 - p0_2).squaredNorm() > shortcut_length2) { - backward_is_too_far = true; - } else { - it0 = it0_2; // make one step in the backward direction - forward_is_blocked = false; // invalidate data about forward walking - forward_is_too_far = false; - } - } - - if (it0.prev() == it2 || it0 == it2) { - // stop if we went all the way around the polygon - // this should only be the case for hole polygons (?) - if (forward_is_too_far && backward_is_too_far) { - // in case p0_it.prev() == p2_it : - // / . - // / /| - // | becomes | | - // \ \| - // \ . - // in case p0_it == p2_it : - // / . - // / becomes /| - // \ \| - // \ . - break; - } else { - // this whole polygon can be removed - return true; + assert(backward == Free); + p02 = it0.prev()->cast(); + if (cross2(p0 - p2, p02 - p2) > 0) + backward = Blocked; + else { + // New clipping edge lenght. + auto d2 = (p2 - p02).squaredNorm(); + if (d2 > shortcut_length2) { + backward = Far; + dist2_next = d2; + } else { + backward = Free; + // Make one step in the backward direction. + it0 = unprocessed_range.remove_back(it0).prev(); + p0 = p02; + dist2_current = d2; + } } } } - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - const Vec2d v02 = p2 - p0; - const int64_t l2_v02 = v02.squaredNorm(); - if (std::abs(l2_v02 - shortcut_length2) < shortcut_length * 10) // i.e. if (size2 < l * (l+10) && size2 > l * (l-10)) - { // v02 is approximately shortcut length - // handle this separately to avoid rounding problems below in the getPointOnLineWithDist function - // p0_it and p2_it are already correct - } else if (! backward_is_blocked && ! forward_is_blocked) { - const auto l_v02 = sqrt(l2_v02); - const Vec2d p0_2 = it0.prev()->cast(); - const Vec2d p2_2 = it2.next()->cast(); - double t = Slic3r::clamp(0., 1., (shortcut_length - l_v02) / ((p2_2 - p0_2).norm() - l_v02)); - it0 = it0.prev().insert((p0 + (p0_2 - p0) * t).cast()); - it2 = it2.insert((p2 + (p2_2 - p2) * t).cast()); - } else if (! backward_is_blocked) { - it0 = it0.prev().insert(point_on_line_at_dist(p0, Vec2d(it0.prev()->cast()), p2, shortcut_length).cast()); - } else if (! forward_is_blocked) { - it2 = it2.insert(point_on_line_at_dist(p2, Vec2d(it2.next()->cast()), p0, shortcut_length).cast()); + if (polygon.size() <= 3) { + // A hole degenerated to an empty polygon, or a tiny triangle remained. + assert(polygon.size() < 3 || (forward == Blocked && backward == Blocked) || (forward == Far && backward == Far)); + if (polygon.size() < 3 || forward == Far) { + assert(polygon.size() < 3 || dist2_current <= shortcut_length2); + polygon.clear(); + } else { + // The remaining triangle is CCW oriented, keep it. + } + return true; + } + + assert(dist2_current <= shortcut_length2); + if ((forward == Blocked && backward == Blocked) || dist2_current > sqr(shortcut_length - int64_t(SCALED_EPSILON))) { + // The crack is filled, keep the last clipping edge. + } else if (dist2_next < sqr(shortcut_length - int64_t(SCALED_EPSILON))) { + // To avoid creating tiny edges. + if (forward == Far) + it0 = unprocessed_range.remove_back(it0).prev(); + if (backward == Far) + it2 = unprocessed_range.remove_front(it2); + if (polygon.size() <= 2) + // A hole degenerated to an empty polygon. + return true; + } else if (forward == Blocked || backward == Blocked) { + // One side is far, the other blocked. + assert(forward == Far || backward == Far); + if (backward == Far) { + // Sort, so we will clip the 1st edge. + std::swap(p0, p2); + std::swap(p02, p22); + } + // Find point on (p0, p02) at distance shortcut_length from p2. + // Circle intersects a line at two points, however because |p2 - p0| < shortcut_length, + // only the second intersection is valid. Because |p2 - p02| > shortcut_length, such + // intersection should always be found on (p0, p02). + const Vec2d v = (p02 - p0).cast(); + const Vec2d d = (p0 - p2).cast(); + const double a = v.squaredNorm(); + const double b = 2. * double(d.dot(v)); + double u = b * b - 4. * a * (d.squaredNorm() - shortcut_length2); + assert(u > 0.); + u = sqrt(u); + double t = (- b + u) / (2. * a); + assert(t > 0. && t < 1.); + (backward == Far ? *it2 : *it0) += (v.cast() * t).cast(); } else { - // | - // __|2 - // | / > shortcut cannot be of the desired length - // ___|/ . - // 0 - // both are blocked and p0_it and p2_it are already correct + // The trapezoid (it0.prev(), it0, it2, it2.next()) is widening. Trim it. + assert(forward == Far && backward == Far); + assert(dist2_next > shortcut_length2); + const double dcurrent = sqrt(double(dist2_current)); + double t = (shortcut_length - dcurrent) / (sqrt(double(dist2_next)) - dcurrent); + assert(t > 0. && t < 1.); + *it0 += ((p02 - p0).cast() * t).cast(); + *it2 += ((p22 - p2).cast() * t).cast(); } - // Delete all the points between it0 and it2. - while (it0.next() != it2) - it0.next().remove(); return false; } -void smooth_outward(MutablePolygon &polygon, double shortcut_length) +// adapted from Cura ConstPolygonRef::smooth_outward() by Tim Kuipers. +void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled) { remove_duplicates(polygon, scaled(0.01)); - const int shortcut_length2 = shortcut_length * shortcut_length; - static constexpr const double cos_min_angle = -0.70710678118654752440084436210485; // cos(135 degrees) + const auto clip_dist_scaled2 = sqr(clip_dist_scaled); + const auto clip_dist_scaled2eps = sqr(clip_dist_scaled + int64_t(SCALED_EPSILON)); + const auto foot_dist_min2 = sqr(SCALED_EPSILON); - MutablePolygon::iterator it1 = polygon.begin(); - do { - const Vec2d p1 = it1->cast(); - auto it0 = it1.prev(); - auto it2 = it1.next(); - const Vec2d p0 = it0->cast(); - const Vec2d p2 = it2->cast(); - const Vec2d v1 = p0 - p1; - const Vec2d v2 = p2 - p1; - const double cos_angle = v1.dot(v2); - if (cos_angle < cos_min_angle && cross2(v1, v2) < 0) { - // Simplify the sharp angle. - const Vec2d v02 = p2 - p0; - const double l2_v02 = v02.squaredNorm(); - if (l2_v02 >= shortcut_length2) { - // Trim an obtuse corner. + // Each source point will be visited exactly once. + MutablePolygon::range unprocessed_range(polygon); + while (! unprocessed_range.empty() && polygon.size() > 2) { + auto it1 = unprocessed_range.process_next(); + auto it0 = it1.prev(); + auto it2 = it1.next(); + const Point p0 = *it0; + const Point p1 = *it1; + const Point p2 = *it2; + const Vec2i64 v1 = (p0 - p1).cast(); + const Vec2i64 v2 = (p2 - p1).cast(); + if (cross2(v1, v2) > 0) { + // Concave corner. + int64_t dot = v1.dot(v2); + auto l2v1 = double(v1.squaredNorm()); + auto l2v2 = double(v2.squaredNorm()); + if (dot > 0 || Slic3r::sqr(double(dot)) * 2. < l2v1 * l2v2) { + // Angle between v1 and v2 bigger than 135 degrees. + // Simplify the sharp angle. + Vec2i64 v02 = (p2 - p0).cast(); + int64_t l2v02 = v02.squaredNorm(); it1.remove(); - if (l2_v02 > Slic3r::sqr(shortcut_length + SCALED_EPSILON)) { - double l2_1 = v1.squaredNorm(); - double l2_2 = v2.squaredNorm(); - bool trim = true; - if (cos_angle > 0.9999) { - // The triangle p0, p1, p2 is likely degenerate. - // Measure height of the triangle. - double d2 = l2_1 > l2_2 ? line_alg::distance_to_squared(Linef{ p0, p1 }, p2) : line_alg::distance_to_squared(Linef{ p2, p1 }, p0); - if (d2 < Slic3r::sqr(scaled(0.02))) - trim = false; - } - if (trim) { - Vec2d bisector = v1 / l2_1 + v2 / l2_2; - double d1 = v1.dot(bisector) / l2_1; - double d2 = v2.dot(bisector) / l2_2; - double lbisector = bisector.norm(); - if (d1 < shortcut_length && d2 < shortcut_length) { - it0.insert((p1 + v1 * (shortcut_length / d1)).cast()) - .insert((p1 + v2 * (shortcut_length / d2)).cast()); - } else if (v1.squaredNorm() < v2.squaredNorm()) - it0.insert(point_on_line_at_dist(p1, p2, p0, shortcut_length).cast()); - else - it0.insert(point_on_line_at_dist(p1, p0, p2, shortcut_length).cast()); + if (l2v02 < clip_dist_scaled2) { + // (p0, p2) is short. + // Clip a sharp concave corner by possibly expanding the trimming region left of it0 and right of it2. + // Updates it0, it2 and num_to_process. + if (clip_narrow_corner(p1.cast(), it0, it2, unprocessed_range, l2v02, clip_dist_scaled)) + // Trimmed down to an empty polygon or to a single CCW triangle. + return; + } else { + // Clip an obtuse corner. + if (l2v02 > clip_dist_scaled2eps) { + Vec2d v1d = v1.cast(); + Vec2d v2d = v2.cast(); + // Sort v1d, v2d, shorter first. + bool swap = l2v1 > l2v2; + if (swap) { + std::swap(v1d, v2d); + std::swap(l2v1, l2v2); + } + double lv1 = sqrt(l2v1); + double lv2 = sqrt(l2v2); + // Bisector between v1 and v2. + Vec2d bisector = v1d / lv1 + v2d / lv2; + double l2bisector = bisector.squaredNorm(); + // Squared distance of the end point of v1 to the bisector. + double d2 = l2v1 - sqr(v1d.dot(bisector)) / l2bisector; + if (d2 < foot_dist_min2) { + // Height of the p1, p0, p2 triangle is tiny. Just remove p1. + } else if (d2 < 0.25 * clip_dist_scaled2 + SCALED_EPSILON) { + // The shorter vector is too close to the bisector. Trim the shorter vector fully, + // trim the longer vector partially. + // Intersection of a circle at p2 of radius = clip_dist_scaled + // with a ray (p1, p0), take the intersection after the foot point. + // The intersection shall always exist because |p2 - p1| > clip_dist_scaled. + const double b = - 2. * v1d.cast().dot(v2d); + double u = b * b - 4. * l2v2 * (double(l2v1) - clip_dist_scaled2); + assert(u > 0.); + // Take the second intersection along v2. + double t = (- b + sqrt(u)) / (2. * l2v2); + assert(t > 0. && t < 1.); + Point pt_new = p1 + (t * v2d).cast(); +#ifndef NDEBUG + double d2new = (pt_new - (swap ? p2 : p0)).cast().squaredNorm(); + assert(std::abs(d2new - clip_dist_scaled2) < sqr(10. * SCALED_EPSILON)); +#endif // NDEBUG + it2.insert(pt_new); + } else { + // Cut the corner with a line perpendicular to the bisector. + double t = sqrt(0.25 * clip_dist_scaled2 / d2); + assert(t > 0. && t < 1.); + Point p0 = p1 + (v1d * t).cast(); + Point p2 = p1 + (v2d * (t * lv2 / lv1)).cast(); + if (swap) + std::swap(p0, p2); + it2.insert(p2).insert(p0); + } + } else { + // Just remove p1. + assert(l2v02 >= clip_dist_scaled2 && l2v02 <= clip_dist_scaled2eps); } } - } else { - bool remove_poly = smooth_corner_complex(p1, it0, it2, shortcut_length); // edits p0_it and p2_it! - if (remove_poly) { - // don't convert ListPolygon into result - return; - } - } - // update: - it1 = it2; // next point to consider for whether it's an internal corner - } - else + it1 = it2; + } else + ++ it1; + } else ++ it1; - } while (it1 != polygon.begin()); + } + + if (polygon.size() == 3) { + // Check whether the last triangle is clockwise oriented (it is a hole) and its height is below clip_dist_scaled. + // If so, fill in the hole. + const Point p0 = *polygon.begin().prev(); + const Point p1 = *polygon.begin(); + const Point p2 = *polygon.begin().next(); + Vec2i64 v1 = (p0 - p1).cast(); + Vec2i64 v2 = (p2 - p1).cast(); + if (cross2(v1, v2) > 0) { + // CW triangle. Measure its height. + const Vec2i64 v3 = (p2 - p0).cast(); + int64_t l12 = v1.squaredNorm(); + int64_t l22 = v2.squaredNorm(); + int64_t l32 = v3.squaredNorm(); + if (l22 > l12 && l22 > l32) { + std::swap(v1, v2); + std::swap(l12, l22); + } else if (l32 > l12 && l32 > l22) { + v1 = v3; + l12 = l32; + } + auto h2 = l22 - sqr(double(v1.dot(v2))) / double(l12); + if (h2 < clip_dist_scaled2) + // CW triangle with a low height. Close the hole. + polygon.clear(); + } + } else if (polygon.size() < 3) + polygon.clear(); } } // namespace Slic3r diff --git a/src/libslic3r/MutablePolygon.hpp b/src/libslic3r/MutablePolygon.hpp index f40b89e748..a601f19e3c 100644 --- a/src/libslic3r/MutablePolygon.hpp +++ b/src/libslic3r/MutablePolygon.hpp @@ -6,6 +6,10 @@ namespace Slic3r { +// Polygon implemented as a loop of double linked elements. +// All elements are allocated in a single std::vector<>, thus integer indices are used for +// referencing the previous and next element and inside iterators to survive reallocation +// of the vector. class MutablePolygon { public: @@ -55,6 +59,69 @@ public: friend class MutablePolygon; MutablePolygon *m_data; IndexType m_idx; + friend class range; + }; + + // Iterator range for maintaining a range of unprocessed items, see smooth_outward(). + class range + { + public: + range(MutablePolygon& poly) : range(poly.begin(), poly.end()) {} + range(MutablePolygon::iterator begin, MutablePolygon::iterator end) : m_begin(begin), m_end(end) {} + + // Start of a range, inclusive. If range is empty, then ! begin().valid(). + MutablePolygon::iterator begin() const { return m_begin; } + // End of a range, inclusive. If range is empty, then ! end().valid(). + MutablePolygon::iterator end() const { return m_end; } + // Is the range empty? + bool empty() const { return !m_begin.valid(); } + + // Return begin() and shorten the range by advancing front. + MutablePolygon::iterator process_next() { + assert(!this->empty()); + MutablePolygon::iterator out = m_begin; + this->advance_front(); + return out; + } + + void advance_front() { + assert(!this->empty()); + if (m_begin == m_end) + this->make_empty(); + else + ++ m_begin; + } + + void retract_back() { + assert(!this->empty()); + if (m_begin == m_end) + this->make_empty(); + else + -- m_end; + } + + MutablePolygon::iterator remove_front(MutablePolygon::iterator it) { + if (m_begin == it) + this->advance_front(); + return it.remove(); + } + + MutablePolygon::iterator remove_back(MutablePolygon::iterator it) { + if (m_end == it) + this->retract_back(); + return it.remove(); + } + + private: + // Range from begin to end, inclusive. + // If the range is valid, then both m_begin and m_end are invalid. + MutablePolygon::iterator m_begin; + MutablePolygon::iterator m_end; + + void make_empty() { + m_begin.m_idx = -1; + m_end.m_idx = -1; + } }; MutablePolygon() = default; @@ -63,26 +130,35 @@ public: template MutablePolygon(IT begin, IT end, size_t reserve = 0) { - m_size = IndexType(end - begin); - if (m_size > 0) { - m_head = 0; - m_data.reserve(std::max(m_size, reserve)); - auto i = IndexType(-1); - auto j = IndexType(1); - for (auto it = begin; it != end; ++ it) - m_data.push_back({ *it, i ++, j ++ }); - m_data.front().prev = m_size - 1; - m_data.back ().next = 0; + this->assign_inner(begin, end, reserve); + }; + + template + void assign(IT begin, IT end, size_t reserve = 0) { + m_data.clear(); + m_head = IndexType(-1); + m_head_free = { IndexType(-1) }; + this->assign_inner(begin, end, reserve); + }; + + void assign(const Polygon &rhs, size_t reserve = 0) { + assign(rhs.points.begin(), rhs.points.end(), reserve); + } + + void polygon(Polygon &out) const { + out.points.clear(); + if (this->valid()) { + out.points.reserve(this->size()); + auto it = this->cbegin(); + out.points.emplace_back(*it); + for (++ it; it != this->cbegin(); ++ it) + out.points.emplace_back(*it); } }; Polygon polygon() const { Polygon out; - if (this->valid()) { - out.points.reserve(this->size()); - for (auto it = this->cbegin(); it != this->cend(); ++ it) - out.points.emplace_back(*it); - } + this->polygon(out); return out; }; @@ -90,6 +166,7 @@ public: size_t size() const { return this->m_size; } size_t capacity() const { return this->m_data.capacity(); } bool valid() const { return this->m_size >= 3; } + void clear() { m_data.clear(); m_size = 0; m_head = IndexType(-1); m_head_free = IndexType(-1); } iterator begin() { return { this, m_head }; } const_iterator cbegin() const { return { this, m_head }; } @@ -108,8 +185,11 @@ public: private: struct LinkedPoint { + // 8 bytes PointType point; + // 4 bytes IndexType prev; + // 4 bytes IndexType next; }; std::vector m_data; @@ -122,6 +202,21 @@ private: LinkedPoint& at(IndexType i) { return m_data[i]; } const LinkedPoint& at(IndexType i) const { return m_data[i]; } + template + void assign_inner(IT begin, IT end, size_t reserve) { + m_size = IndexType(end - begin); + if (m_size > 0) { + m_head = 0; + m_data.reserve(std::max(m_size, reserve)); + auto i = IndexType(-1); + auto j = IndexType(1); + for (auto it = begin; it != end; ++ it) + m_data.push_back({ *it, i ++, j ++ }); + m_data.front().prev = m_size - 1; + m_data.back ().next = 0; + } + }; + IndexType remove(const IndexType i) { assert(i >= 0); assert(m_size > 0); @@ -213,13 +308,26 @@ inline bool operator!=(const MutablePolygon &p1, const MutablePolygon &p2) { ret void remove_duplicates(MutablePolygon &polygon); void remove_duplicates(MutablePolygon &polygon, double eps); -void smooth_outward(MutablePolygon &polygon, double shortcut_length); +void smooth_outward(MutablePolygon &polygon, coord_t clip_dist_scaled); -inline Polygon smooth_outward(const Polygon &polygon, double shortcut_length) +inline Polygon smooth_outward(Polygon polygon, coord_t clip_dist_scaled) { MutablePolygon mp(polygon, polygon.size() * 2); - smooth_outward(mp, shortcut_length); - return mp.polygon(); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(polygon); + return polygon; +} + +inline Polygons smooth_outward(Polygons polygons, coord_t clip_dist_scaled) +{ + MutablePolygon mp; + for (Polygon &polygon : polygons) { + mp.assign(polygon, polygon.size() * 2); + smooth_outward(mp, clip_dist_scaled); + mp.polygon(polygon); + } + polygons.erase(std::remove_if(polygons.begin(), polygons.end(), [](const auto &p){ return p.empty(); }), polygons.end()); + return polygons; } } diff --git a/tests/libslic3r/test_mutable_polygon.cpp b/tests/libslic3r/test_mutable_polygon.cpp index 2214da6ef0..238e3604fb 100644 --- a/tests/libslic3r/test_mutable_polygon.cpp +++ b/tests/libslic3r/test_mutable_polygon.cpp @@ -1,5 +1,6 @@ #include +#include "libslic3r/Point.hpp" #include "libslic3r/MutablePolygon.hpp" using namespace Slic3r; @@ -143,3 +144,36 @@ SCENARIO("Remove degenerate points from MutablePolygon", "[MutablePolygon]") { } } } + +SCENARIO("smooth_outward", "[MutablePolygon]") { + GIVEN("Convex polygon") { + MutablePolygon p{ { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } }; + WHEN("smooth_outward") { + MutablePolygon p2{ p }; + smooth_outward(p2, scaled(10.)); + THEN("Polygon is unmodified") { + REQUIRE(p == p2); + } + } + } + GIVEN("Sharp tiny concave polygon (hole)") { + MutablePolygon p{ { 0, 0 }, { 0, scaled(5.) }, { scaled(10.), 0 } }; + WHEN("smooth_outward") { + MutablePolygon p2{ p }; + smooth_outward(p2, scaled(10.)); + THEN("Hole is closed") { + REQUIRE(p2.empty()); + } + } + } + GIVEN("Two polygons") { + Polygons p{ { { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } }, + { { 0, 0 }, { 0, scaled(5.) }, { scaled(10.), 0 } } }; + WHEN("smooth_outward") { + p = smooth_outward(p, scaled(10.)); + THEN("CCW contour unmodified, CW contour removed.") { + REQUIRE(p == Polygons{ { { 0, 0 }, { scaled(10.), 0 }, { 0, scaled(10.) } } }); + } + } + } +}