Rewritten the medial axis algorithm, now more robust (don't just prune MAT from endpoints, but validate all single edges)
This commit is contained in:
parent
e4147a6bed
commit
7041bb5bd9
9
t/thin.t
9
t/thin.t
@ -61,7 +61,7 @@ if (0) {
|
|||||||
[160, 140],
|
[160, 140],
|
||||||
);
|
);
|
||||||
my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
|
my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
|
||||||
my $res = $expolygon->medial_axis(scale 1, scale 0.5);
|
my $res = $expolygon->medial_axis(scale 40, scale 0.5);
|
||||||
is scalar(@$res), 1, 'medial axis of a square shape is a single path';
|
is scalar(@$res), 1, 'medial axis of a square shape is a single path';
|
||||||
isa_ok $res->[0], 'Slic3r::Polyline', 'medial axis result is a polyline';
|
isa_ok $res->[0], 'Slic3r::Polyline', 'medial axis result is a polyline';
|
||||||
ok $res->[0]->first_point->coincides_with($res->[0]->last_point), 'polyline forms a closed loop';
|
ok $res->[0]->first_point->coincides_with($res->[0]->last_point), 'polyline forms a closed loop';
|
||||||
@ -76,7 +76,7 @@ if (0) {
|
|||||||
[120, 200],
|
[120, 200],
|
||||||
[100, 200],
|
[100, 200],
|
||||||
));
|
));
|
||||||
my $res = $expolygon->medial_axis(scale 1, scale 0.5);
|
my $res = $expolygon->medial_axis(scale 20, scale 0.5);
|
||||||
is scalar(@$res), 1, 'medial axis of a narrow rectangle is a single line';
|
is scalar(@$res), 1, 'medial axis of a narrow rectangle is a single line';
|
||||||
ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
|
ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ if (0) {
|
|||||||
[112, 200],
|
[112, 200],
|
||||||
[108, 200],
|
[108, 200],
|
||||||
));
|
));
|
||||||
my $res = $expolygon->medial_axis(scale 1, scale 0.5);
|
my $res = $expolygon->medial_axis(scale 20, scale 0.5);
|
||||||
is scalar(@$res), 1, 'medial axis of a narrow trapezoid is a single line';
|
is scalar(@$res), 1, 'medial axis of a narrow trapezoid is a single line';
|
||||||
ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
|
ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has reasonable length';
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ if (0) {
|
|||||||
[200, 200],
|
[200, 200],
|
||||||
[100, 200],
|
[100, 200],
|
||||||
));
|
));
|
||||||
my $res = $expolygon->medial_axis(scale 1, scale 0.5);
|
my $res = $expolygon->medial_axis(scale 20, scale 0.5);
|
||||||
is scalar(@$res), 1, 'medial axis of a L shape is a single polyline';
|
is scalar(@$res), 1, 'medial axis of a L shape is a single polyline';
|
||||||
my $len = unscale($res->[0]->length) + 20; # 20 is the thickness of the expolygon, which is subtracted from the ends
|
my $len = unscale($res->[0]->length) + 20; # 20 is the thickness of the expolygon, which is subtracted from the ends
|
||||||
ok $len > 80*2 && $len < 100*2, 'medial axis has reasonable length';
|
ok $len > 80*2 && $len < 100*2, 'medial axis has reasonable length';
|
||||||
@ -150,7 +150,6 @@ if (0) {
|
|||||||
[91294454,31032190],[11294481,31032190],[11294481,29967810],[44969182,29967810],[89909960,29967808],[91294454,29967808]
|
[91294454,31032190],[11294481,31032190],[11294481,29967810],[44969182,29967810],[89909960,29967808],[91294454,29967808]
|
||||||
));
|
));
|
||||||
my $polylines = $expolygon->medial_axis(1871238, 500000);
|
my $polylines = $expolygon->medial_axis(1871238, 500000);
|
||||||
|
|
||||||
is scalar(@$polylines), 1, 'medial axis is a single polyline';
|
is scalar(@$polylines), 1, 'medial axis is a single polyline';
|
||||||
my $polyline = $polylines->[0];
|
my $polyline = $polylines->[0];
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#include "polypartition.h"
|
#include "polypartition.h"
|
||||||
#include "poly2tri/poly2tri.h"
|
#include "poly2tri/poly2tri.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
@ -194,6 +195,7 @@ ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polyl
|
|||||||
svg.Close();
|
svg.Close();
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
bool removed = false;
|
||||||
for (size_t i = 0; i < pp.size(); ++i) {
|
for (size_t i = 0; i < pp.size(); ++i) {
|
||||||
ThickPolyline& polyline = pp[i];
|
ThickPolyline& polyline = pp[i];
|
||||||
|
|
||||||
@ -203,7 +205,7 @@ ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polyl
|
|||||||
call, so we keep the inner point until we perform the second intersection() as well */
|
call, so we keep the inner point until we perform the second intersection() as well */
|
||||||
Point new_front = polyline.points.front();
|
Point new_front = polyline.points.front();
|
||||||
Point new_back = polyline.points.back();
|
Point new_back = polyline.points.back();
|
||||||
if (polyline.endpoints.front() && !this->has_boundary_point(new_front)) {
|
if (polyline.endpoints.first && !this->has_boundary_point(new_front)) {
|
||||||
Line line(polyline.points.front(), polyline.points[1]);
|
Line line(polyline.points.front(), polyline.points[1]);
|
||||||
|
|
||||||
// prevent the line from touching on the other side, otherwise intersection() might return that solution
|
// prevent the line from touching on the other side, otherwise intersection() might return that solution
|
||||||
@ -212,7 +214,7 @@ ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polyl
|
|||||||
line.extend_start(max_width);
|
line.extend_start(max_width);
|
||||||
(void)this->contour.intersection(line, &new_front);
|
(void)this->contour.intersection(line, &new_front);
|
||||||
}
|
}
|
||||||
if (polyline.endpoints.back() && !this->has_boundary_point(new_back)) {
|
if (polyline.endpoints.second && !this->has_boundary_point(new_back)) {
|
||||||
Line line(
|
Line line(
|
||||||
*(polyline.points.end() - 2),
|
*(polyline.points.end() - 2),
|
||||||
polyline.points.back()
|
polyline.points.back()
|
||||||
@ -230,14 +232,54 @@ ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polyl
|
|||||||
/* remove too short polylines
|
/* remove too short polylines
|
||||||
(we can't do this check before endpoints extension and clipping because we don't
|
(we can't do this check before endpoints extension and clipping because we don't
|
||||||
know how long will the endpoints be extended since it depends on polygon thickness
|
know how long will the endpoints be extended since it depends on polygon thickness
|
||||||
which is variable - extension will be <= max_width/2 on each side) */
|
which is variable - extension will be <= max_width/2 on each side)
|
||||||
if (polyline.length() < max_width) {
|
Maybe instead of using max_width we should use max_element(polyline.width) */
|
||||||
|
if ((polyline.endpoints.first || polyline.endpoints.second)
|
||||||
|
&& polyline.length() < max_width*2) {
|
||||||
pp.erase(pp.begin() + i);
|
pp.erase(pp.begin() + i);
|
||||||
--i;
|
--i;
|
||||||
|
removed = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If we removed any short polylines we now try to connect consecutive polylines
|
||||||
|
in order to allow loop detection. Note that this algorithm is greedier than
|
||||||
|
MedialAxis::process_edge_neighbors() as it will connect random pairs of
|
||||||
|
polylines even when more than two start from the same point. This has no
|
||||||
|
drawbacks since we optimize later using nearest-neighbor which would do the
|
||||||
|
same, but should we use a more sophisticated optimization algorithm we should
|
||||||
|
not connect polylines when more than two meet. */
|
||||||
|
if (removed) {
|
||||||
|
for (size_t i = 0; i < pp.size(); ++i) {
|
||||||
|
ThickPolyline& polyline = pp[i];
|
||||||
|
if (polyline.endpoints.first && polyline.endpoints.second) continue; // optimization
|
||||||
|
|
||||||
|
// find another polyline starting here
|
||||||
|
for (size_t j = i+1; j < pp.size(); ++j) {
|
||||||
|
ThickPolyline& other = pp[j];
|
||||||
|
if (polyline.last_point().coincides_with(other.last_point())) {
|
||||||
|
other.reverse();
|
||||||
|
} else if (polyline.first_point().coincides_with(other.last_point())) {
|
||||||
|
polyline.reverse();
|
||||||
|
other.reverse();
|
||||||
|
} else if (polyline.first_point().coincides_with(other.first_point())) {
|
||||||
|
polyline.reverse();
|
||||||
|
} else if (!polyline.last_point().coincides_with(other.first_point())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
polyline.points.insert(polyline.points.end(), other.points.begin() + 1, other.points.end());
|
||||||
|
polyline.width.insert(polyline.width.end(), other.width.begin(), other.width.end());
|
||||||
|
polyline.endpoints.second = other.endpoints.second;
|
||||||
|
assert(polyline.width.size() == polyline.points.size()*2 - 2);
|
||||||
|
|
||||||
|
pp.erase(pp.begin() + j);
|
||||||
|
j = i; // restart search from i+1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
polylines->insert(polylines->end(), pp.begin(), pp.end());
|
polylines->insert(polylines->end(), pp.begin(), pp.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,12 +293,6 @@ arrange(size_t total_parts, Pointf part, coordf_t dist, const BoundingBoxf* bb)
|
|||||||
void
|
void
|
||||||
MedialAxis::build(ThickPolylines* polylines)
|
MedialAxis::build(ThickPolylines* polylines)
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
// build bounding box (we use it for clipping infinite segments)
|
|
||||||
// --> we have no infinite segments
|
|
||||||
this->bb = BoundingBox(this->lines);
|
|
||||||
*/
|
|
||||||
|
|
||||||
construct_voronoi(this->lines.begin(), this->lines.end(), &this->vd);
|
construct_voronoi(this->lines.begin(), this->lines.end(), &this->vd);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -321,105 +315,59 @@ MedialAxis::build(ThickPolylines* polylines)
|
|||||||
|
|
||||||
// collect valid edges (i.e. prune those not belonging to MAT)
|
// collect valid edges (i.e. prune those not belonging to MAT)
|
||||||
// note: this keeps twins, so it inserts twice the number of the valid edges
|
// note: this keeps twins, so it inserts twice the number of the valid edges
|
||||||
this->edges.clear();
|
this->valid_edges.clear();
|
||||||
for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) {
|
{
|
||||||
// if we only process segments representing closed loops, none if the
|
std::set<const VD::edge_type*> seen_edges;
|
||||||
// infinite edges (if any) would be part of our MAT anyway
|
for (VD::const_edge_iterator edge = this->vd.edges().begin(); edge != this->vd.edges().end(); ++edge) {
|
||||||
if (edge->is_secondary() || edge->is_infinite()) continue;
|
// if we only process segments representing closed loops, none if the
|
||||||
this->edges.insert(&*edge);
|
// infinite edges (if any) would be part of our MAT anyway
|
||||||
}
|
if (edge->is_secondary() || edge->is_infinite()) continue;
|
||||||
|
|
||||||
// count valid segments for each vertex
|
// don't re-validate twins
|
||||||
std::map< vert_t*,std::set<edge_t*> > vertex_edges; // collects edges connected for each vertex
|
if (seen_edges.find(&*edge) != seen_edges.end()) continue;
|
||||||
std::set<vert_t*> startpoints; // collects all vertices having a single starting edge
|
seen_edges.insert(&*edge);
|
||||||
for (VD::const_vertex_iterator it = this->vd.vertices().begin(); it != this->vd.vertices().end(); ++it) {
|
seen_edges.insert(edge->twin());
|
||||||
vert_t* vertex = &*it;
|
|
||||||
|
|
||||||
// loop through all edges originating from this vertex
|
if (!this->validate_edge(&*edge)) continue;
|
||||||
// starting from a random one
|
this->valid_edges.insert(&*edge);
|
||||||
edge_t* edge = vertex->incident_edge();
|
this->valid_edges.insert(edge->twin());
|
||||||
do {
|
|
||||||
// if this edge was not pruned by our filter above,
|
|
||||||
// add it to vertex_edges
|
|
||||||
if (this->edges.count(edge) > 0)
|
|
||||||
vertex_edges[vertex].insert(edge);
|
|
||||||
|
|
||||||
// continue looping next edge originating from this vertex
|
|
||||||
edge = edge->rot_next();
|
|
||||||
} while (edge != vertex->incident_edge());
|
|
||||||
|
|
||||||
// if there's only one edge starting at this vertex then it's an endpoint
|
|
||||||
if (vertex_edges[vertex].size() == 1) {
|
|
||||||
startpoints.insert(vertex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this->edges = this->valid_edges;
|
||||||
// prune startpoints recursively if extreme segments are not valid
|
|
||||||
while (!startpoints.empty()) {
|
|
||||||
// get a random entry node
|
|
||||||
vert_t* v = *startpoints.begin();
|
|
||||||
|
|
||||||
// get edge starting from v
|
|
||||||
assert(vertex_edges[v].size() == 1);
|
|
||||||
edge_t* edge = *vertex_edges[v].begin();
|
|
||||||
|
|
||||||
if (!this->validate_edge(*edge)) {
|
|
||||||
// if edge is not valid, erase it and its twin from edge list
|
|
||||||
(void)this->edges.erase(edge);
|
|
||||||
(void)this->edges.erase(edge->twin());
|
|
||||||
|
|
||||||
// decrement edge counters for the affected nodes
|
|
||||||
vert_t* v1 = edge->vertex1();
|
|
||||||
(void)vertex_edges[v].erase(edge);
|
|
||||||
(void)vertex_edges[v1].erase(edge->twin());
|
|
||||||
|
|
||||||
// also, check whether the end vertex is a new leaf
|
|
||||||
if (vertex_edges[v1].size() == 1) {
|
|
||||||
startpoints.insert(v1);
|
|
||||||
} else if (vertex_edges[v1].empty()) {
|
|
||||||
startpoints.erase(v1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove node from the set to prevent it from being visited again
|
|
||||||
startpoints.erase(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate through the valid edges to build polylines
|
// iterate through the valid edges to build polylines
|
||||||
while (!this->edges.empty()) {
|
while (!this->edges.empty()) {
|
||||||
edge_t &edge = **this->edges.begin();
|
const edge_t* edge = *this->edges.begin();
|
||||||
|
|
||||||
// start a polyline
|
// start a polyline
|
||||||
ThickPolyline polyline;
|
ThickPolyline polyline;
|
||||||
polyline.points.push_back(Point( edge.vertex0()->x(), edge.vertex0()->y() ));
|
polyline.points.push_back(Point( edge->vertex0()->x(), edge->vertex0()->y() ));
|
||||||
polyline.points.push_back(Point( edge.vertex1()->x(), edge.vertex1()->y() ));
|
polyline.points.push_back(Point( edge->vertex1()->x(), edge->vertex1()->y() ));
|
||||||
polyline.width.push_back(this->thickness[&edge].first);
|
polyline.width.push_back(this->thickness[edge].first);
|
||||||
polyline.width.push_back(this->thickness[&edge].second);
|
polyline.width.push_back(this->thickness[edge].second);
|
||||||
|
|
||||||
// remove this edge and its twin from the available edges
|
// remove this edge and its twin from the available edges
|
||||||
(void)this->edges.erase(&edge);
|
(void)this->edges.erase(edge);
|
||||||
(void)this->edges.erase(edge.twin());
|
(void)this->edges.erase(edge->twin());
|
||||||
|
|
||||||
// get next points
|
// get next points
|
||||||
this->process_edge_neighbors(edge, &polyline.points, &polyline.width, &polyline.endpoints);
|
this->process_edge_neighbors(edge, &polyline);
|
||||||
|
|
||||||
// get previous points
|
// get previous points
|
||||||
{
|
{
|
||||||
Points pp;
|
ThickPolyline rpolyline;
|
||||||
std::vector<coordf_t> width;
|
this->process_edge_neighbors(edge->twin(), &rpolyline);
|
||||||
std::vector<bool> endpoints;
|
polyline.points.insert(polyline.points.begin(), rpolyline.points.rbegin(), rpolyline.points.rend());
|
||||||
this->process_edge_neighbors(*edge.twin(), &pp, &width, &endpoints);
|
polyline.width.insert(polyline.width.begin(), rpolyline.width.rbegin(), rpolyline.width.rend());
|
||||||
polyline.points.insert(polyline.points.begin(), pp.rbegin(), pp.rend());
|
polyline.endpoints.first = rpolyline.endpoints.second;
|
||||||
polyline.width.insert(polyline.width.begin(), width.rbegin(), width.rend());
|
|
||||||
polyline.endpoints.insert(polyline.endpoints.begin(), endpoints.rbegin(), endpoints.rend());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(polyline.width.size() == polyline.points.size()*2 - 2);
|
assert(polyline.width.size() == polyline.points.size()*2 - 2);
|
||||||
assert(polyline.endpoints.size() == polyline.points.size());
|
|
||||||
|
|
||||||
|
// prevent loop endpoints from being extended
|
||||||
if (polyline.first_point().coincides_with(polyline.last_point())) {
|
if (polyline.first_point().coincides_with(polyline.last_point())) {
|
||||||
polyline.endpoints.front() = false;
|
polyline.endpoints.first = false;
|
||||||
polyline.endpoints.back() = false;
|
polyline.endpoints.second = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// append polyline to result
|
// append polyline to result
|
||||||
@ -436,106 +384,139 @@ MedialAxis::build(Polylines* polylines)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
MedialAxis::process_edge_neighbors(const VD::edge_type& edge, Points* points,
|
MedialAxis::process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline)
|
||||||
std::vector<coordf_t>* width, std::vector<bool>* endpoints)
|
|
||||||
{
|
{
|
||||||
// Since rot_next() works on the edge starting point but we want
|
while (true) {
|
||||||
// to find neighbors on the ending point, we just swap edge with
|
// Since rot_next() works on the edge starting point but we want
|
||||||
// its twin.
|
// to find neighbors on the ending point, we just swap edge with
|
||||||
const VD::edge_type& twin = *edge.twin();
|
// its twin.
|
||||||
|
const VD::edge_type* twin = edge->twin();
|
||||||
|
|
||||||
// count neighbors for this edge
|
// count neighbors for this edge
|
||||||
std::vector<const VD::edge_type*> neighbors;
|
std::vector<const VD::edge_type*> neighbors;
|
||||||
for (const VD::edge_type* neighbor = twin.rot_next(); neighbor != &twin; neighbor = neighbor->rot_next()) {
|
for (const VD::edge_type* neighbor = twin->rot_next(); neighbor != twin;
|
||||||
if (this->edges.count(neighbor) > 0) neighbors.push_back(neighbor);
|
neighbor = neighbor->rot_next()) {
|
||||||
}
|
if (this->valid_edges.count(neighbor) > 0) neighbors.push_back(neighbor);
|
||||||
|
}
|
||||||
|
|
||||||
// if we have a single neighbor then we can continue recursively
|
// if we have a single neighbor then we can continue recursively
|
||||||
if (neighbors.size() == 1) {
|
if (neighbors.size() == 1) {
|
||||||
endpoints->push_back(false);
|
const VD::edge_type* neighbor = neighbors.front();
|
||||||
const VD::edge_type& neighbor = *neighbors.front();
|
|
||||||
points->push_back(Point( neighbor.vertex1()->x(), neighbor.vertex1()->y() ));
|
// break if this is a closed loop
|
||||||
width->push_back(this->thickness[&neighbor].first);
|
if (this->edges.count(neighbor) == 0) return;
|
||||||
width->push_back(this->thickness[&neighbor].second);
|
|
||||||
(void)this->edges.erase(&neighbor);
|
Point new_point(neighbor->vertex1()->x(), neighbor->vertex1()->y());
|
||||||
(void)this->edges.erase(neighbor.twin());
|
polyline->points.push_back(new_point);
|
||||||
this->process_edge_neighbors(neighbor, points, width, endpoints);
|
polyline->width.push_back(this->thickness[neighbor].first);
|
||||||
} else if (neighbors.size() == 0) {
|
polyline->width.push_back(this->thickness[neighbor].second);
|
||||||
endpoints->push_back(true);
|
(void)this->edges.erase(neighbor);
|
||||||
} else {
|
(void)this->edges.erase(neighbor->twin());
|
||||||
// T-shaped or star-shaped joint
|
edge = neighbor;
|
||||||
endpoints->push_back(false);
|
} else if (neighbors.size() == 0) {
|
||||||
|
polyline->endpoints.second = true;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// T-shaped or star-shaped joint
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
MedialAxis::validate_edge(const VD::edge_type& edge)
|
MedialAxis::validate_edge(const VD::edge_type* edge)
|
||||||
{
|
{
|
||||||
/* If the cells sharing this edge have a common vertex, we're not (probably) interested
|
// construct the line representing this edge of the Voronoi diagram
|
||||||
in this edge. Why? Because it means that the edge lies on the bisector of
|
const Line line(
|
||||||
two contiguous input lines and it was included in the Voronoi graph because
|
Point( edge->vertex0()->x(), edge->vertex0()->y() ),
|
||||||
it's the locus of centers of circles tangent to both vertices. Due to the
|
Point( edge->vertex1()->x(), edge->vertex1()->y() )
|
||||||
"thin" nature of our input, these edges will be very short and not part of
|
);
|
||||||
our wanted output. */
|
|
||||||
|
// discard edge if it lies outside the supplied shape
|
||||||
|
// this could maybe be optimized (checking inclusion of the endpoints
|
||||||
|
// might give false positives as they might belong to the contour itself)
|
||||||
|
if (this->expolygon != NULL) {
|
||||||
|
if (line.a.coincides_with(line.b)) {
|
||||||
|
// in this case, contains(line) returns a false positive
|
||||||
|
if (!this->expolygon->contains(line.a)) return false;
|
||||||
|
} else {
|
||||||
|
if (!this->expolygon->contains(line)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// retrieve the original line segments which generated the edge we're checking
|
// retrieve the original line segments which generated the edge we're checking
|
||||||
const VD::cell_type &cell1 = *edge.cell();
|
const VD::cell_type* cell1 = edge->cell();
|
||||||
const VD::cell_type &cell2 = *edge.twin()->cell();
|
const VD::cell_type* cell2 = edge->twin()->cell();
|
||||||
if (!cell1.contains_segment() || !cell2.contains_segment()) return false;
|
|
||||||
const Line &segment1 = this->retrieve_segment(cell1);
|
const Line &segment1 = this->retrieve_segment(cell1);
|
||||||
const Line &segment2 = this->retrieve_segment(cell2);
|
const Line &segment2 = this->retrieve_segment(cell2);
|
||||||
|
|
||||||
// calculate the relative angle between the two boundary segments
|
/* Calculate thickness of the section at both the endpoints of this edge.
|
||||||
double angle = fabs(segment2.orientation() - segment1.orientation());
|
Our Voronoi edge is part of a CCW sequence going around its Voronoi cell
|
||||||
|
(segment1). This edge's twin goes around segment2. Thus, segment2 is
|
||||||
|
oriented in the same direction as our main edge, and segment1 is oriented
|
||||||
|
in the same direction as our twin edge.
|
||||||
|
We used to only consider the (half-)distances to segment2, and that works
|
||||||
|
whenever segment1 and segment2 are almost specular and facing. However,
|
||||||
|
at curves they are staggered and they only face for a very little length
|
||||||
|
(such visibility actually coincides with our very short edge). This is why
|
||||||
|
we calculate w0 and w1 this way.
|
||||||
|
When cell1 or cell2 don't refer to the segment but only to an endpoint, we
|
||||||
|
calculate the distance to that endpoint instead. */
|
||||||
|
|
||||||
// fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction)
|
coordf_t w0 = cell2->contains_segment()
|
||||||
// we're interested only in segments close to the second case (facing segments)
|
? line.a.perp_distance_to(segment2)*2
|
||||||
// so we allow some tolerance.
|
: line.a.distance_to(this->retrieve_endpoint(cell2))*2;
|
||||||
// this filter ensures that we're dealing with a narrow/oriented area (longer than thick)
|
|
||||||
if (fabs(angle - PI) > PI/5) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// each edge vertex is equidistant to both cell segments
|
coordf_t w1 = cell1->contains_segment()
|
||||||
// but such distance might differ between the two vertices;
|
? line.b.perp_distance_to(segment1)*2
|
||||||
// in this case it means the shape is getting narrow (like a corner)
|
: line.b.distance_to(this->retrieve_endpoint(cell1))*2;
|
||||||
// and we might need to skip the edge since it's not really part of
|
|
||||||
// our skeleton
|
|
||||||
|
|
||||||
/* Calculate perpendicular distance. We consider segment2 instead of segment1
|
|
||||||
because our Voronoi edge is part of a CCW sequence going around its Voronoi cell
|
|
||||||
(segment).
|
|
||||||
This means that such segment is on the left on our edge, and goes backwards.
|
|
||||||
So we use the cell of the twin edge, which is located on the right of our edge
|
|
||||||
and goes in the same direction as it. This way we can map dist0 and dist1
|
|
||||||
correctly. */
|
|
||||||
const Line line(
|
|
||||||
Point( edge.vertex0()->x(), edge.vertex0()->y() ),
|
|
||||||
Point( edge.vertex1()->x(), edge.vertex1()->y() )
|
|
||||||
);
|
|
||||||
coordf_t dist0 = segment2.a.perp_distance_to(line)*2;
|
|
||||||
coordf_t dist1 = segment2.b.perp_distance_to(line)*2;
|
|
||||||
|
|
||||||
// if this edge is the centerline for a very thin area, we might want to skip it
|
// if this edge is the centerline for a very thin area, we might want to skip it
|
||||||
// in case the area is too thin
|
// in case the area is too thin
|
||||||
if (dist0 < this->min_width && dist1 < this->min_width) {
|
if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON) {
|
||||||
//printf(" => too thin, skipping\n");
|
if (cell1->contains_segment() && cell2->contains_segment()) {
|
||||||
return false;
|
// calculate the relative angle between the two boundary segments
|
||||||
|
double angle = fabs(segment2.orientation() - segment1.orientation());
|
||||||
|
|
||||||
|
// fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction)
|
||||||
|
// we're interested only in segments close to the second case (facing segments)
|
||||||
|
// so we allow some tolerance.
|
||||||
|
// this filter ensures that we're dealing with a narrow/oriented area (longer than thick)
|
||||||
|
// we don't run it on edges not generated by two segments (thus generated by one segment
|
||||||
|
// and the endpoint of another segment), since their orientation would not be meaningful
|
||||||
|
if (fabs(angle - PI) > PI/5) return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->expolygon != NULL && !this->expolygon->contains(line))
|
if (w0 < this->min_width && w1 < this->min_width)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
this->thickness[&edge] = std::make_pair(dist0, dist1);
|
if (w0 > this->max_width && w1 > this->max_width)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
this->thickness[edge] = std::make_pair(w0, w1);
|
||||||
|
this->thickness[edge->twin()] = std::make_pair(w1, w0);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Line&
|
const Line&
|
||||||
MedialAxis::retrieve_segment(const VD::cell_type& cell) const
|
MedialAxis::retrieve_segment(const VD::cell_type* cell) const
|
||||||
{
|
{
|
||||||
VD::cell_type::source_index_type index = cell.source_index() - this->points.size();
|
return this->lines[cell->source_index()];
|
||||||
return this->lines[index];
|
}
|
||||||
|
|
||||||
|
const Point&
|
||||||
|
MedialAxis::retrieve_endpoint(const VD::cell_type* cell) const
|
||||||
|
{
|
||||||
|
const Line& line = this->retrieve_segment(cell);
|
||||||
|
if (cell->source_category() == SOURCE_CATEGORY_SEGMENT_START_POINT) {
|
||||||
|
return line.a;
|
||||||
|
} else {
|
||||||
|
return line.b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} }
|
} }
|
||||||
|
@ -42,7 +42,6 @@ Pointfs arrange(size_t total_parts, Pointf part, coordf_t dist, const BoundingBo
|
|||||||
|
|
||||||
class MedialAxis {
|
class MedialAxis {
|
||||||
public:
|
public:
|
||||||
Points points;
|
|
||||||
Lines lines;
|
Lines lines;
|
||||||
const ExPolygon* expolygon;
|
const ExPolygon* expolygon;
|
||||||
double max_width;
|
double max_width;
|
||||||
@ -55,12 +54,12 @@ class MedialAxis {
|
|||||||
private:
|
private:
|
||||||
typedef voronoi_diagram<double> VD;
|
typedef voronoi_diagram<double> VD;
|
||||||
VD vd;
|
VD vd;
|
||||||
std::set<const VD::edge_type*> edges;
|
std::set<const VD::edge_type*> edges, valid_edges;
|
||||||
std::map<const VD::edge_type*, std::pair<coordf_t,coordf_t> > thickness;
|
std::map<const VD::edge_type*, std::pair<coordf_t,coordf_t> > thickness;
|
||||||
void process_edge_neighbors(const voronoi_diagram<double>::edge_type& edge,
|
void process_edge_neighbors(const VD::edge_type* edge, ThickPolyline* polyline);
|
||||||
Points* points, std::vector<coordf_t>* width, std::vector<bool>* endpoints);
|
bool validate_edge(const VD::edge_type* edge);
|
||||||
bool validate_edge(const voronoi_diagram<double>::edge_type& edge);
|
const Line& retrieve_segment(const VD::cell_type* cell) const;
|
||||||
const Line& retrieve_segment(const voronoi_diagram<double>::cell_type& cell) const;
|
const Point& retrieve_endpoint(const VD::cell_type* cell) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} }
|
} }
|
||||||
|
@ -25,7 +25,6 @@ PerimeterGenerator::process()
|
|||||||
|
|
||||||
// solid infill
|
// solid infill
|
||||||
coord_t ispacing = this->solid_infill_flow.scaled_spacing();
|
coord_t ispacing = this->solid_infill_flow.scaled_spacing();
|
||||||
coord_t gap_area_threshold = pwidth * pwidth;
|
|
||||||
|
|
||||||
// Calculate the minimum required spacing between two adjacent traces.
|
// Calculate the minimum required spacing between two adjacent traces.
|
||||||
// This should be equal to the nominal flow spacing but we experiment
|
// This should be equal to the nominal flow spacing but we experiment
|
||||||
@ -137,16 +136,11 @@ PerimeterGenerator::process()
|
|||||||
// not using safety offset here would "detect" very narrow gaps
|
// not using safety offset here would "detect" very narrow gaps
|
||||||
// (but still long enough to escape the area threshold) that gap fill
|
// (but still long enough to escape the area threshold) that gap fill
|
||||||
// won't be able to fill but we'd still remove from infill area
|
// won't be able to fill but we'd still remove from infill area
|
||||||
ExPolygons diff_expp = diff_ex(
|
Polygons diff_pp = diff(
|
||||||
offset(last, -0.5*distance),
|
offset(last, -0.5*distance),
|
||||||
offset(offsets, +0.5*distance + 10) // safety offset
|
offset(offsets, +0.5*distance + 10) // safety offset
|
||||||
);
|
);
|
||||||
for (ExPolygons::const_iterator ex = diff_expp.begin(); ex != diff_expp.end(); ++ex) {
|
gaps.insert(gaps.end(), diff_pp.begin(), diff_pp.end());
|
||||||
if (fabs(ex->area()) >= gap_area_threshold) {
|
|
||||||
Polygons pp = *ex;
|
|
||||||
gaps.insert(gaps.end(), pp.begin(), pp.end());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,18 +390,10 @@ PerimeterGenerator::_traverse_loops(const PerimeterGeneratorLoops &loops,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// append thin walls to the nearest-neighbor search (only for first iteration)
|
// append thin walls to the nearest-neighbor search (only for first iteration)
|
||||||
{
|
if (!thin_walls.empty()) {
|
||||||
ExtrusionEntityCollection tw = this->_variable_width
|
ExtrusionEntityCollection tw = this->_variable_width
|
||||||
(thin_walls, erExternalPerimeter, this->ext_perimeter_flow);
|
(thin_walls, erExternalPerimeter, this->ext_perimeter_flow);
|
||||||
|
|
||||||
const double threshold = this->ext_perimeter_flow.scaled_width() * 2;
|
|
||||||
for (size_t i = 0; i < tw.entities.size(); ++i) {
|
|
||||||
if (tw.entities[i]->length() < threshold) {
|
|
||||||
tw.remove(i);
|
|
||||||
--i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
coll.append(tw.entities);
|
coll.append(tw.entities);
|
||||||
thin_walls.clear();
|
thin_walls.clear();
|
||||||
}
|
}
|
||||||
@ -457,12 +443,16 @@ PerimeterGenerator::_variable_width(const ThickPolylines &polylines, ExtrusionRo
|
|||||||
ExtrusionPaths paths;
|
ExtrusionPaths paths;
|
||||||
ExtrusionPath path(role);
|
ExtrusionPath path(role);
|
||||||
ThickLines lines = p->thicklines();
|
ThickLines lines = p->thicklines();
|
||||||
|
|
||||||
for (size_t i = 0; i < lines.size(); ++i) {
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
const ThickLine& line = lines[i];
|
const ThickLine& line = lines[i];
|
||||||
const double thickness_delta = fabs(line.a_width - line.b_width);
|
|
||||||
|
const coordf_t line_len = line.length();
|
||||||
|
if (line_len < SCALED_EPSILON) continue;
|
||||||
|
|
||||||
|
double thickness_delta = fabs(line.a_width - line.b_width);
|
||||||
if (thickness_delta > tolerance) {
|
if (thickness_delta > tolerance) {
|
||||||
const unsigned short segments = ceil(thickness_delta / tolerance);
|
const unsigned short segments = ceil(thickness_delta / tolerance);
|
||||||
const coordf_t line_len = line.length();
|
|
||||||
const coordf_t seg_len = line_len / segments;
|
const coordf_t seg_len = line_len / segments;
|
||||||
Points pp;
|
Points pp;
|
||||||
std::vector<coordf_t> width;
|
std::vector<coordf_t> width;
|
||||||
@ -509,30 +499,30 @@ PerimeterGenerator::_variable_width(const ThickPolylines &polylines, ExtrusionRo
|
|||||||
path.mm3_per_mm = flow.mm3_per_mm();
|
path.mm3_per_mm = flow.mm3_per_mm();
|
||||||
path.width = flow.width;
|
path.width = flow.width;
|
||||||
path.height = flow.height;
|
path.height = flow.height;
|
||||||
} else if (fabs(flow.width - w) <= tolerance) {
|
|
||||||
// the width difference between this line and the current flow width is
|
|
||||||
// within the accepted tolerance
|
|
||||||
|
|
||||||
path.polyline.append(line.b);
|
|
||||||
} else {
|
} else {
|
||||||
// we need to initialize a new line
|
thickness_delta = fabs(scale_(flow.width) - w);
|
||||||
paths.push_back(path);
|
if (thickness_delta <= tolerance) {
|
||||||
path = ExtrusionPath(role);
|
// the width difference between this line and the current flow width is
|
||||||
--i;
|
// within the accepted tolerance
|
||||||
|
|
||||||
|
path.polyline.append(line.b);
|
||||||
|
} else {
|
||||||
|
// we need to initialize a new line
|
||||||
|
paths.push_back(path);
|
||||||
|
path = ExtrusionPath(role);
|
||||||
|
--i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!path.polyline.points.empty())
|
if (path.polyline.is_valid())
|
||||||
paths.push_back(path);
|
paths.push_back(path);
|
||||||
|
|
||||||
// loop through generated paths
|
// append paths to collection
|
||||||
for (ExtrusionPaths::const_iterator p = paths.begin(); p != paths.end(); ++p) {
|
if (!paths.empty()) {
|
||||||
if (p->polyline.is_valid()) {
|
if (paths.front().first_point().coincides_with(paths.back().last_point())) {
|
||||||
if (p->first_point().coincides_with(p->last_point())) {
|
coll.append(ExtrusionLoop(paths));
|
||||||
// since medial_axis() now returns only Polyline objects, detect loops here
|
} else {
|
||||||
coll.append(ExtrusionLoop(*p));
|
coll.append(paths);
|
||||||
} else {
|
|
||||||
coll.append(*p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#include "Line.hpp"
|
#include "Line.hpp"
|
||||||
#include "Polygon.hpp"
|
#include "Polygon.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
@ -235,4 +236,12 @@ ThickPolyline::thicklines() const
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ThickPolyline::reverse()
|
||||||
|
{
|
||||||
|
Polyline::reverse();
|
||||||
|
std::reverse(this->width.begin(), this->width.end());
|
||||||
|
std::swap(this->endpoints.first, this->endpoints.second);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,10 @@ class Polyline : public MultiPoint {
|
|||||||
class ThickPolyline : public Polyline {
|
class ThickPolyline : public Polyline {
|
||||||
public:
|
public:
|
||||||
std::vector<coordf_t> width;
|
std::vector<coordf_t> width;
|
||||||
std::vector<bool> endpoints;
|
std::pair<bool,bool> endpoints;
|
||||||
|
ThickPolyline() : endpoints(std::make_pair(false, false)) {};
|
||||||
ThickLines thicklines() const;
|
ThickLines thicklines() const;
|
||||||
|
void reverse();
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user