More refactoring to medial axis and gap fill, more robust

This commit is contained in:
Alessandro Ranellucci 2016-05-20 06:24:05 +02:00
parent b068616366
commit 9e8022f6f6
13 changed files with 133 additions and 58 deletions

View File

@ -1,4 +1,4 @@
use Test::More tests => 21; use Test::More tests => 23;
use strict; use strict;
use warnings; use warnings;
@ -8,7 +8,7 @@ BEGIN {
} }
use Slic3r; use Slic3r;
use List::Util qw(first sum); use List::Util qw(first sum none);
use Slic3r::Geometry qw(epsilon scale unscale scaled_epsilon Y); use Slic3r::Geometry qw(epsilon scale unscale scaled_epsilon Y);
use Slic3r::Test; use Slic3r::Test;
@ -91,7 +91,20 @@ if (0) {
is scalar(@$res), 1, 'medial axis of a narrow rectangle with an extra vertex is still a single line'; is scalar(@$res), 1, 'medial axis of a narrow rectangle with an extra vertex is still a single line';
ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has still a reasonable length'; ok unscale($res->[0]->length) >= (200-100 - (120-100)) - epsilon, 'medial axis has still a reasonable length';
ok !(grep { abs($_ - scale 150) < scaled_epsilon } map $_->[Y], map @$_, @$res2), "extra vertices don't influence medial axis"; ok !(grep { abs($_ - scale 150) < scaled_epsilon } map $_->[Y], map @$_, @$res2), "extra vertices don't influence medial axis";
}
{
my $expolygon = Slic3r::ExPolygon->new(
Slic3r::Polygon->new([1185881,829367],[1421988,1578184],[1722442,2303558],[2084981,2999998],[2506843,3662186],[2984809,4285086],[3515250,4863959],[4094122,5394400],[4717018,5872368],[5379210,6294226],[6075653,6656769],[6801033,6957229],[7549842,7193328],[8316383,7363266],[9094809,7465751],[9879211,7500000],[10663611,7465750],[11442038,7363265],[12208580,7193327],[12957389,6957228],[13682769,6656768],[14379209,6294227],[15041405,5872366],[15664297,5394401],[16243171,4863960],[16758641,4301424],[17251579,3662185],[17673439,3000000],[18035980,2303556],[18336441,1578177],[18572539,829368],[18750748,0],[19758422,0],[19727293,236479],[19538467,1088188],[19276136,1920196],[18942292,2726179],[18539460,3499999],[18070731,4235755],[17539650,4927877],[16950279,5571067],[16307090,6160437],[15614974,6691519],[14879209,7160248],[14105392,7563079],[13299407,7896927],[12467399,8159255],[11615691,8348082],[10750769,8461952],[9879211,8500000],[9007652,8461952],[8142729,8348082],[7291022,8159255],[6459015,7896927],[5653029,7563079],[4879210,7160247],[4143447,6691519],[3451331,6160437],[2808141,5571066],[2218773,4927878],[1687689,4235755],[1218962,3499999],[827499,2748020],[482284,1920196],[219954,1088186],[31126,236479],[0,0],[1005754,0]),
);
my $res = $expolygon->medial_axis(scale 1.324888, scale 0.25);
is scalar(@$res), 1, 'medial axis of a semicircumference is a single line';
# check whether turns are all CCW or all CW
my @lines = @{$res->[0]->lines};
my @angles = map { $lines[$_-1]->ccw($lines[$_]->b) } 1..$#lines;
ok !!(none { $_ < 0 } @angles) || (none { $_ > 0 } @angles),
'all medial axis segments of a semicircumference have the same orientation';
} }
{ {
@ -136,7 +149,7 @@ if (0) {
{ {
my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale( my $expolygon = Slic3r::ExPolygon->new(Slic3r::Polygon->new_scale(
[50, 100], [50, 100],
[300, 102], [1000, 102],
[50, 104], [50, 104],
)); ));
my $res = $expolygon->medial_axis(scale 4, scale 0.5); my $res = $expolygon->medial_axis(scale 4, scale 0.5);

View File

@ -174,17 +174,10 @@ void
ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polylines) const ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polylines) const
{ {
// init helper object // init helper object
Slic3r::Geometry::MedialAxis ma(max_width, min_width); Slic3r::Geometry::MedialAxis ma(max_width, min_width, this);
ma.expolygon = this; ma.lines = this->lines();
// populate list of segments for the Voronoi diagram // compute the Voronoi diagram and extract medial axis polylines
ma.lines = this->contour.lines();
for (Polygons::const_iterator hole = this->holes.begin(); hole != this->holes.end(); ++hole) {
Lines lines = hole->lines();
ma.lines.insert(ma.lines.end(), lines.begin(), lines.end());
}
// compute the Voronoi diagram
ThickPolylines pp; ThickPolylines pp;
ma.build(&pp); ma.build(&pp);
@ -195,15 +188,14 @@ ExPolygon::medial_axis(double max_width, double min_width, ThickPolylines* polyl
svg.Close(); svg.Close();
*/ */
// find the maximum width returned /* Find the maximum width returned; we're going to use this for validating and
filtering the output segments. */
double max_w = 0; double max_w = 0;
if (!pp.empty()) { for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it)
std::vector<double> widths; max_w = fmaxf(max_w, *std::max_element(it->width.begin(), it->width.end()));
for (ThickPolylines::const_iterator it = pp.begin(); it != pp.end(); ++it)
widths.insert(widths.end(), it->width.begin(), it->width.end());
max_w = *std::max_element(widths.begin(), widths.end());
}
/* Loop through all returned polylines in order to extend their endpoints to the
expolygon boundaries */
bool removed = false; 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];
@ -502,4 +494,15 @@ ExPolygon::lines() const
return lines; return lines;
} }
std::string
ExPolygon::dump_perl() const
{
std::ostringstream ret;
ret << "[" << this->contour.dump_perl();
for (Polygons::const_iterator h = this->holes.begin(); h != this->holes.end(); ++h)
ret << "," << h->dump_perl();
ret << "]";
return ret.str();
}
} }

View File

@ -42,6 +42,7 @@ class ExPolygon
void triangulate_pp(Polygons* polygons) const; void triangulate_pp(Polygons* polygons) const;
void triangulate_p2t(Polygons* polygons) const; void triangulate_p2t(Polygons* polygons) const;
Lines lines() const; Lines lines() const;
std::string dump_perl() const;
}; };
} }

View File

@ -324,7 +324,7 @@ MedialAxis::build(ThickPolylines* polylines)
if (edge->is_secondary() || edge->is_infinite()) continue; if (edge->is_secondary() || edge->is_infinite()) continue;
// don't re-validate twins // don't re-validate twins
if (seen_edges.find(&*edge) != seen_edges.end()) continue; if (seen_edges.find(&*edge) != seen_edges.end()) continue; // TODO: is this needed?
seen_edges.insert(&*edge); seen_edges.insert(&*edge);
seen_edges.insert(edge->twin()); seen_edges.insert(edge->twin());
@ -445,49 +445,66 @@ MedialAxis::validate_edge(const VD::edge_type* edge)
} }
// 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* cell_l = edge->cell();
const VD::cell_type* cell2 = edge->twin()->cell(); const VD::cell_type* cell_r = edge->twin()->cell();
const Line &segment1 = this->retrieve_segment(cell1); const Line &segment_l = this->retrieve_segment(cell_l);
const Line &segment2 = this->retrieve_segment(cell2); const Line &segment_r = this->retrieve_segment(cell_r);
/* Calculate thickness of the section at both the endpoints of this edge. /*
SVG svg("edge.svg");
svg.draw(*this->expolygon);
svg.draw(line);
svg.draw(segment_l, "red");
svg.draw(segment_r, "blue");
svg.Close();
*/
/* Calculate thickness of the cross-section at both the endpoints of this edge.
Our Voronoi edge is part of a CCW sequence going around its Voronoi cell 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 located on the left side. (segment_l).
oriented in the same direction as our main edge, and segment1 is oriented This edge's twin goes around segment_r. Thus, segment_r is
oriented in the same direction as our main edge, and segment_l is oriented
in the same direction as our twin edge. in the same direction as our twin edge.
We used to only consider the (half-)distances to segment2, and that works We used to only consider the (half-)distances to segment_r, and that works
whenever segment1 and segment2 are almost specular and facing. However, whenever segment_l and segment_r are almost specular and facing. However,
at curves they are staggered and they only face for a very little length 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 (our very short edge represents such visibility).
we calculate w0 and w1 this way. Both w0 and w1 can be calculated either towards cell_l or cell_r with equal
When cell1 or cell2 don't refer to the segment but only to an endpoint, we results by Voronoi definition.
When cell_l or cell_r don't refer to the segment but only to an endpoint, we
calculate the distance to that endpoint instead. */ calculate the distance to that endpoint instead. */
coordf_t w0 = cell2->contains_segment() coordf_t w0 = cell_r->contains_segment()
? line.a.perp_distance_to(segment2)*2 ? line.a.distance_to(segment_r)*2
: line.a.distance_to(this->retrieve_endpoint(cell2))*2; : line.a.distance_to(this->retrieve_endpoint(cell_r))*2;
coordf_t w1 = cell1->contains_segment() coordf_t w1 = cell_l->contains_segment()
? line.b.perp_distance_to(segment1)*2 ? line.b.distance_to(segment_l)*2
: line.b.distance_to(this->retrieve_endpoint(cell1))*2; : line.b.distance_to(this->retrieve_endpoint(cell_l))*2;
// if this edge is the centerline for a very thin area, we might want to skip it if (cell_l->contains_segment() && cell_r->contains_segment()) {
// in case the area is too thin // calculate the relative angle between the two boundary segments
if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON) { double angle = fabs(segment_r.orientation() - segment_l.orientation());
if (cell1->contains_segment() && cell2->contains_segment()) { if (angle > PI) angle = 2*PI - angle;
// calculate the relative angle between the two boundary segments assert(angle >= 0 && angle <= PI);
double angle = fabs(segment2.orientation() - segment1.orientation());
// fabs(angle) ranges from 0 (collinear, same direction) to PI (collinear, opposite direction) // 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) // we're interested only in segments close to the second case (facing segments)
// so we allow some tolerance. // so we allow some tolerance.
// this filter ensures that we're dealing with a narrow/oriented area (longer than thick) // 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 // 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 // and the endpoint of another segment), since their orientation would not be meaningful
if (fabs(angle - PI) > PI/5) return false; if (PI - angle > PI/8) {
} else { // angle is not narrow enough
return false;
// only apply this filter to segments that are not too short otherwise their
// angle could possibly be not meaningful
if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON || line.length() >= this->min_width)
return false;
} }
} else {
if (w0 < SCALED_EPSILON || w1 < SCALED_EPSILON)
return false;
} }
if (w0 < this->min_width && w1 < this->min_width) if (w0 < this->min_width && w1 < this->min_width)

View File

@ -46,8 +46,8 @@ class MedialAxis {
const ExPolygon* expolygon; const ExPolygon* expolygon;
double max_width; double max_width;
double min_width; double min_width;
MedialAxis(double _max_width, double _min_width) MedialAxis(double _max_width, double _min_width, const ExPolygon* _expolygon = NULL)
: max_width(_max_width), min_width(_min_width), expolygon(NULL) {}; : max_width(_max_width), min_width(_min_width), expolygon(_expolygon) {};
void build(ThickPolylines* polylines); void build(ThickPolylines* polylines);
void build(Polylines* polylines); void build(Polylines* polylines);

View File

@ -212,6 +212,12 @@ Line::intersection(const Line& line, Point* intersection) const
return false; // not intersecting return false; // not intersecting
} }
double
Line::ccw(const Point& point) const
{
return point.ccw(*this);
}
Pointf3 Pointf3
Linef3::intersect_plane(double z) const Linef3::intersect_plane(double z) const
{ {

View File

@ -44,6 +44,7 @@ class Line
void extend_end(double distance); void extend_end(double distance);
void extend_start(double distance); void extend_start(double distance);
bool intersection(const Line& line, Point* intersection) const; bool intersection(const Line& line, Point* intersection) const;
double ccw(const Point& point) const;
}; };
class ThickLine : public Line class ThickLine : public Line

View File

@ -128,6 +128,19 @@ MultiPoint::intersection(const Line& line, Point* intersection) const
return false; return false;
} }
std::string
MultiPoint::dump_perl() const
{
std::ostringstream ret;
ret << "[";
for (Points::const_iterator p = this->points.begin(); p != this->points.end(); ++p) {
ret << p->dump_perl();
if (p != this->points.end()-1) ret << ",";
}
ret << "]";
return ret.str();
}
Points Points
MultiPoint::_douglas_peucker(const Points &points, const double tolerance) MultiPoint::_douglas_peucker(const Points &points, const double tolerance)
{ {

View File

@ -37,6 +37,7 @@ class MultiPoint
void append(const Points &points); void append(const Points &points);
void append(const Points::const_iterator &begin, const Points::const_iterator &end); void append(const Points::const_iterator &begin, const Points::const_iterator &end);
bool intersection(const Line& line, Point* intersection) const; bool intersection(const Line& line, Point* intersection) const;
std::string dump_perl() const;
static Points _douglas_peucker(const Points &points, const double tolerance); static Points _douglas_peucker(const Points &points, const double tolerance);
}; };

View File

@ -92,7 +92,7 @@ PerimeterGenerator::process()
// the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width // the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width
// (actually, something larger than that still may exist due to mitering or other causes) // (actually, something larger than that still may exist due to mitering or other causes)
coord_t min_width = scale_(this->ext_perimeter_flow.nozzle_diameter / 2); coord_t min_width = scale_(this->ext_perimeter_flow.nozzle_diameter / 3);
ExPolygons expp = offset2_ex(diffpp, -min_width/2, +min_width/2); ExPolygons expp = offset2_ex(diffpp, -min_width/2, +min_width/2);
// the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop

View File

@ -26,6 +26,14 @@ Point::wkt() const
return ss.str(); return ss.str();
} }
std::string
Point::dump_perl() const
{
std::ostringstream ss;
ss << "[" << this->x << "," << this->y << "]";
return ss.str();
}
void void
Point::scale(double factor) Point::scale(double factor)
{ {
@ -313,6 +321,14 @@ Pointf::wkt() const
return ss.str(); return ss.str();
} }
std::string
Pointf::dump_perl() const
{
std::ostringstream ss;
ss << "[" << this->x << "," << this->y << "]";
return ss.str();
}
void void
Pointf::scale(double factor) Pointf::scale(double factor)
{ {

View File

@ -38,6 +38,7 @@ class Point
}; };
bool operator==(const Point& rhs) const; bool operator==(const Point& rhs) const;
std::string wkt() const; std::string wkt() const;
std::string dump_perl() const;
void scale(double factor); void scale(double factor);
void translate(double x, double y); void translate(double x, double y);
void translate(const Vector &vector); void translate(const Vector &vector);
@ -87,6 +88,7 @@ class Pointf
return Pointf(unscale(p.x), unscale(p.y)); return Pointf(unscale(p.x), unscale(p.y));
}; };
std::string wkt() const; std::string wkt() const;
std::string dump_perl() const;
void scale(double factor); void scale(double factor);
void translate(double x, double y); void translate(double x, double y);
void translate(const Vectorf &vector); void translate(const Vectorf &vector);

View File

@ -41,6 +41,8 @@
%code{% RETVAL = Polyline(*THIS); %}; %code{% RETVAL = Polyline(*THIS); %};
Clone<Point> normal(); Clone<Point> normal();
Clone<Point> vector(); Clone<Point> vector();
double ccw(Point* point)
%code{% RETVAL = THIS->ccw(*point); %};
%{ %{
Line* Line*