WIP: MutablePolygon - linked list based polygon implementation
allowing rapid insertion and removal of points.
WIP: porting smooth_outward() from Cura.
This commit is contained in:
Vojtech Bubnik 2021-03-03 15:04:15 +01:00
parent 6a46b71dc1
commit 5f5de1c812
3 changed files with 428 additions and 197 deletions

View file

@ -1,5 +1,6 @@
#include "MutablePolygon.hpp" #include "MutablePolygon.hpp"
#include "Line.hpp" #include "Line.hpp"
#include "libslic3r.h"
namespace Slic3r { 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. // Adapted from Cura ConstPolygonRef::smooth_corner_complex() by Tim Kuipers.
// If two points fulfill the condition, then the first one (closer to point a) is taken. // A concave corner at it1 with position p1 has been removed by the caller between it0 and it2, where |p2 - p0| < shortcut_length.
// If none of the two points falls on line (a, b), return false. // 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
template<typename VectorType> // and the new clipping edge is still inside the polygon (it is a diagonal, it does not intersect polygon boundary).
static inline VectorType point_on_line_at_dist(const VectorType &a, const VectorType &b, const VectorType &ref_pt, const double dist) // 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; MutablePolygon &polygon = it0.polygon();
auto v = b - a; assert(polygon.size() >= 2);
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;
}
static bool smooth_corner_complex(const Vec2d p1, MutablePolygon::iterator &it0, MutablePolygon::iterator &it2, const double shortcut_length) const int64_t shortcut_length2 = sqr(shortcut_length);
{
// walk away from the corner until the shortcut > shortcut_length or it would smooth a piece inward enum Status {
// - walk in both directions untill shortcut > shortcut_length Free,
// - stop walking in one direction if it would otherwise cut off a corner in that direction Blocked,
// - same in the other direction Far,
// - stop if both are cut off };
// walk by updating p0_it and p2_it Status forward = Free;
double shortcut_length2 = shortcut_length * shortcut_length; Status backward = Free;
bool forward_is_blocked = false;
bool forward_is_too_far = false; Vec2i64 p0 = it0->cast<int64_t>();
bool backward_is_blocked = false; Vec2i64 p2 = it2->cast<int64_t>();
bool backward_is_too_far = false; Vec2i64 p02;
for (;;) { Vec2i64 p22;
const bool forward_has_converged = forward_is_blocked || forward_is_too_far; int64_t dist2_next;
const bool backward_has_converged = backward_is_blocked || backward_is_too_far;
if (forward_has_converged && backward_has_converged) { // As long as there is at least a single triangle left in the polygon.
if (forward_is_too_far && backward_is_too_far && (*it0.prev() - *it2.next()).cast<double>().squaredNorm() < shortcut_length2) { while (polygon.size() >= 3) {
// Trim the narrowing region. assert(dist2_current <= shortcut_length2);
-- it0; if (forward == Far && backward == Far) {
++ it2; p02 = it0.prev()->cast<int64_t>();
forward_is_too_far = false; p22 = it2.next()->cast<int64_t>();
backward_is_too_far = false; auto d2 = (p22 - p02).squaredNorm();
continue; if (d2 <= shortcut_length2) {
} else // 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 {
// The region is widening. Stop traversal and trim the final trapezoid.
dist2_next = d2;
break; break;
} }
} else if (forward != Free && backward != Free)
const Vec2d p0 = it0->cast<double>(); // One of the corners is blocked, the other is blocked or too far. Stop traversal.
const Vec2d p2 = it2->cast<double>();
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<double>();
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;
} 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;
}
} else {
// walk backward
const auto it0_2 = it0.prev();
const Vec2d p0_2 = it0_2->cast<double>();
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; 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<int64_t>().squaredNorm())) {
p22 = it2.next()->cast<int64_t>();
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 { } else {
// this whole polygon can be removed forward = Free;
// Make one step in the forward direction.
it2 = unprocessed_range.remove_front(it2);
p2 = p22;
dist2_current = d2;
}
}
} else {
assert(backward == Free);
p02 = it0.prev()->cast<int64_t>();
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;
}
}
}
}
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; return true;
} }
}
}
const Vec2d p0 = it0->cast<double>(); assert(dist2_current <= shortcut_length2);
const Vec2d p2 = it2->cast<double>(); if ((forward == Blocked && backward == Blocked) || dist2_current > sqr(shortcut_length - int64_t(SCALED_EPSILON))) {
const Vec2d v02 = p2 - p0; // The crack is filled, keep the last clipping edge.
const int64_t l2_v02 = v02.squaredNorm(); } else if (dist2_next < sqr(shortcut_length - int64_t(SCALED_EPSILON))) {
if (std::abs(l2_v02 - shortcut_length2) < shortcut_length * 10) // i.e. if (size2 < l * (l+10) && size2 > l * (l-10)) // To avoid creating tiny edges.
{ // v02 is approximately shortcut length if (forward == Far)
// handle this separately to avoid rounding problems below in the getPointOnLineWithDist function it0 = unprocessed_range.remove_back(it0).prev();
// p0_it and p2_it are already correct if (backward == Far)
} else if (! backward_is_blocked && ! forward_is_blocked) { it2 = unprocessed_range.remove_front(it2);
const auto l_v02 = sqrt(l2_v02); if (polygon.size() <= 2)
const Vec2d p0_2 = it0.prev()->cast<double>(); // A hole degenerated to an empty polygon.
const Vec2d p2_2 = it2.next()->cast<double>(); return true;
double t = Slic3r::clamp(0., 1., (shortcut_length - l_v02) / ((p2_2 - p0_2).norm() - l_v02)); } else if (forward == Blocked || backward == Blocked) {
it0 = it0.prev().insert((p0 + (p0_2 - p0) * t).cast<coord_t>()); // One side is far, the other blocked.
it2 = it2.insert((p2 + (p2_2 - p2) * t).cast<coord_t>()); assert(forward == Far || backward == Far);
} else if (! backward_is_blocked) { if (backward == Far) {
it0 = it0.prev().insert(point_on_line_at_dist(p0, Vec2d(it0.prev()->cast<double>()), p2, shortcut_length).cast<coord_t>()); // Sort, so we will clip the 1st edge.
} else if (! forward_is_blocked) { std::swap(p0, p2);
it2 = it2.insert(point_on_line_at_dist(p2, Vec2d(it2.next()->cast<double>()), p0, shortcut_length).cast<coord_t>()); std::swap(p02, p22);
} else { }
// | // Find point on (p0, p02) at distance shortcut_length from p2.
// __|2 // Circle intersects a line at two points, however because |p2 - p0| < shortcut_length,
// | / > shortcut cannot be of the desired length // only the second intersection is valid. Because |p2 - p02| > shortcut_length, such
// ___|/ . // intersection should always be found on (p0, p02).
// 0 const Vec2d v = (p02 - p0).cast<double>();
// both are blocked and p0_it and p2_it are already correct const Vec2d d = (p0 - p2).cast<double>();
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<double>() * t).cast<coord_t>();
} else {
// 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<double>() * t).cast<coord_t>();
*it2 += ((p22 - p2).cast<double>() * t).cast<coord_t>();
} }
// Delete all the points between it0 and it2.
while (it0.next() != it2)
it0.next().remove();
return false; 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<double>(0.01)); remove_duplicates(polygon, scaled<double>(0.01));
const int shortcut_length2 = shortcut_length * shortcut_length; const auto clip_dist_scaled2 = sqr<int64_t>(clip_dist_scaled);
static constexpr const double cos_min_angle = -0.70710678118654752440084436210485; // cos(135 degrees) 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(); // Each source point will be visited exactly once.
do { MutablePolygon::range unprocessed_range(polygon);
const Vec2d p1 = it1->cast<double>(); while (! unprocessed_range.empty() && polygon.size() > 2) {
auto it1 = unprocessed_range.process_next();
auto it0 = it1.prev(); auto it0 = it1.prev();
auto it2 = it1.next(); auto it2 = it1.next();
const Vec2d p0 = it0->cast<double>(); const Point p0 = *it0;
const Vec2d p2 = it2->cast<double>(); const Point p1 = *it1;
const Vec2d v1 = p0 - p1; const Point p2 = *it2;
const Vec2d v2 = p2 - p1; const Vec2i64 v1 = (p0 - p1).cast<int64_t>();
const double cos_angle = v1.dot(v2); const Vec2i64 v2 = (p2 - p1).cast<int64_t>();
if (cos_angle < cos_min_angle && cross2(v1, v2) < 0) { 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. // Simplify the sharp angle.
const Vec2d v02 = p2 - p0; Vec2i64 v02 = (p2 - p0).cast<int64_t>();
const double l2_v02 = v02.squaredNorm(); int64_t l2v02 = v02.squaredNorm();
if (l2_v02 >= shortcut_length2) {
// Trim an obtuse corner.
it1.remove(); it1.remove();
if (l2_v02 > Slic3r::sqr(shortcut_length + SCALED_EPSILON)) { if (l2v02 < clip_dist_scaled2) {
double l2_1 = v1.squaredNorm(); // (p0, p2) is short.
double l2_2 = v2.squaredNorm(); // Clip a sharp concave corner by possibly expanding the trimming region left of it0 and right of it2.
bool trim = true; // Updates it0, it2 and num_to_process.
if (cos_angle > 0.9999) { if (clip_narrow_corner(p1.cast<int64_t>(), it0, it2, unprocessed_range, l2v02, clip_dist_scaled))
// The triangle p0, p1, p2 is likely degenerate. // Trimmed down to an empty polygon or to a single CCW triangle.
// Measure height of the triangle. return;
double d2 = l2_1 > l2_2 ? line_alg::distance_to_squared(Linef{ p0, p1 }, p2) : line_alg::distance_to_squared(Linef{ p2, p1 }, p0); } else {
if (d2 < Slic3r::sqr(scaled<double>(0.02))) // Clip an obtuse corner.
trim = false; if (l2v02 > clip_dist_scaled2eps) {
} Vec2d v1d = v1.cast<double>();
if (trim) { Vec2d v2d = v2.cast<double>();
Vec2d bisector = v1 / l2_1 + v2 / l2_2; // Sort v1d, v2d, shorter first.
double d1 = v1.dot(bisector) / l2_1; bool swap = l2v1 > l2v2;
double d2 = v2.dot(bisector) / l2_2; if (swap) {
double lbisector = bisector.norm(); std::swap(v1d, v2d);
if (d1 < shortcut_length && d2 < shortcut_length) { std::swap(l2v1, l2v2);
it0.insert((p1 + v1 * (shortcut_length / d1)).cast<coord_t>())
.insert((p1 + v2 * (shortcut_length / d2)).cast<coord_t>());
} else if (v1.squaredNorm() < v2.squaredNorm())
it0.insert(point_on_line_at_dist(p1, p2, p0, shortcut_length).cast<coord_t>());
else
it0.insert(point_on_line_at_dist(p1, p0, p2, shortcut_length).cast<coord_t>());
} }
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<double>().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<coord_t>();
#ifndef NDEBUG
double d2new = (pt_new - (swap ? p2 : p0)).cast<double>().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<coord_t>();
Point p2 = p1 + (v2d * (t * lv2 / lv1)).cast<coord_t>();
if (swap)
std::swap(p0, p2);
it2.insert(p2).insert(p0);
} }
} else { } else {
bool remove_poly = smooth_corner_complex(p1, it0, it2, shortcut_length); // edits p0_it and p2_it! // Just remove p1.
if (remove_poly) { assert(l2v02 >= clip_dist_scaled2 && l2v02 <= clip_dist_scaled2eps);
// don't convert ListPolygon into result
return;
} }
} }
// update: it1 = it2;
it1 = it2; // next point to consider for whether it's an internal corner } else
}
else
++ it1; ++ it1;
} while (it1 != polygon.begin()); } else
++ it1;
}
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<int64_t>();
Vec2i64 v2 = (p2 - p1).cast<int64_t>();
if (cross2(v1, v2) > 0) {
// CW triangle. Measure its height.
const Vec2i64 v3 = (p2 - p0).cast<int64_t>();
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 } // namespace Slic3r

View file

@ -6,6 +6,10 @@
namespace Slic3r { 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 class MutablePolygon
{ {
public: public:
@ -55,6 +59,69 @@ public:
friend class MutablePolygon; friend class MutablePolygon;
MutablePolygon *m_data; MutablePolygon *m_data;
IndexType m_idx; 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; MutablePolygon() = default;
@ -63,26 +130,35 @@ public:
template<typename IT> template<typename IT>
MutablePolygon(IT begin, IT end, size_t reserve = 0) { MutablePolygon(IT begin, IT end, size_t reserve = 0) {
m_size = IndexType(end - begin); this->assign_inner(begin, end, reserve);
if (m_size > 0) { };
m_head = 0;
m_data.reserve(std::max<size_t>(m_size, reserve)); template<typename IT>
auto i = IndexType(-1); void assign(IT begin, IT end, size_t reserve = 0) {
auto j = IndexType(1); m_data.clear();
for (auto it = begin; it != end; ++ it) m_head = IndexType(-1);
m_data.push_back({ *it, i ++, j ++ }); m_head_free = { IndexType(-1) };
m_data.front().prev = m_size - 1; this->assign_inner(begin, end, reserve);
m_data.back ().next = 0; };
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 polygon() const {
Polygon out; Polygon out;
if (this->valid()) { this->polygon(out);
out.points.reserve(this->size());
for (auto it = this->cbegin(); it != this->cend(); ++ it)
out.points.emplace_back(*it);
}
return out; return out;
}; };
@ -90,6 +166,7 @@ public:
size_t size() const { return this->m_size; } size_t size() const { return this->m_size; }
size_t capacity() const { return this->m_data.capacity(); } size_t capacity() const { return this->m_data.capacity(); }
bool valid() const { return this->m_size >= 3; } 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 }; } iterator begin() { return { this, m_head }; }
const_iterator cbegin() const { return { this, m_head }; } const_iterator cbegin() const { return { this, m_head }; }
@ -108,8 +185,11 @@ public:
private: private:
struct LinkedPoint { struct LinkedPoint {
// 8 bytes
PointType point; PointType point;
// 4 bytes
IndexType prev; IndexType prev;
// 4 bytes
IndexType next; IndexType next;
}; };
std::vector<LinkedPoint> m_data; std::vector<LinkedPoint> m_data;
@ -122,6 +202,21 @@ private:
LinkedPoint& at(IndexType i) { return m_data[i]; } LinkedPoint& at(IndexType i) { return m_data[i]; }
const LinkedPoint& at(IndexType i) const { return m_data[i]; } const LinkedPoint& at(IndexType i) const { return m_data[i]; }
template<typename IT>
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<size_t>(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) { IndexType remove(const IndexType i) {
assert(i >= 0); assert(i >= 0);
assert(m_size > 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);
void remove_duplicates(MutablePolygon &polygon, double eps); 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); MutablePolygon mp(polygon, polygon.size() * 2);
smooth_outward(mp, shortcut_length); smooth_outward(mp, clip_dist_scaled);
return mp.polygon(); 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;
} }
} }

View file

@ -1,5 +1,6 @@
#include <catch2/catch.hpp> #include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/MutablePolygon.hpp" #include "libslic3r/MutablePolygon.hpp"
using namespace Slic3r; 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<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } };
WHEN("smooth_outward") {
MutablePolygon p2{ p };
smooth_outward(p2, scaled<double>(10.));
THEN("Polygon is unmodified") {
REQUIRE(p == p2);
}
}
}
GIVEN("Sharp tiny concave polygon (hole)") {
MutablePolygon p{ { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } };
WHEN("smooth_outward") {
MutablePolygon p2{ p };
smooth_outward(p2, scaled<double>(10.));
THEN("Hole is closed") {
REQUIRE(p2.empty());
}
}
}
GIVEN("Two polygons") {
Polygons p{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } },
{ { 0, 0 }, { 0, scaled<coord_t>(5.) }, { scaled<coord_t>(10.), 0 } } };
WHEN("smooth_outward") {
p = smooth_outward(p, scaled<double>(10.));
THEN("CCW contour unmodified, CW contour removed.") {
REQUIRE(p == Polygons{ { { 0, 0 }, { scaled<coord_t>(10.), 0 }, { 0, scaled<coord_t>(10.) } } });
}
}
}
}