Fixed ExPolygon::overlaps(), which was not commutative.
Wrote unit tests for Clipper polyline clipping operations. Rewrote ExPolygon unit tests from Perl to C++.
This commit is contained in:
parent
f1c0c61895
commit
db3f696888
@ -154,14 +154,18 @@ bool ExPolygon::overlaps(const ExPolygon &other) const
|
|||||||
svg.draw_outline(*this);
|
svg.draw_outline(*this);
|
||||||
svg.draw_outline(other, "blue");
|
svg.draw_outline(other, "blue");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Polylines pl_out = intersection_pl(to_polylines(other), *this);
|
Polylines pl_out = intersection_pl(to_polylines(other), *this);
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
svg.draw(pl_out, "red");
|
svg.draw(pl_out, "red");
|
||||||
#endif
|
#endif
|
||||||
if (! pl_out.empty())
|
|
||||||
return true;
|
// See unit test SCENARIO("Clipper diff with polyline", "[Clipper]")
|
||||||
//FIXME ExPolygon::overlaps() shall be commutative, it is not!
|
// for in which case the intersection_pl produces any intersection.
|
||||||
return this->contains(other.contour.points.front());
|
return ! pl_out.empty() ||
|
||||||
|
// If *this is completely inside other, then pl_out is empty, but the expolygons overlap. Test for that situation.
|
||||||
|
other.contains(this->contour.points.front());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExPolygon::simplify_p(double tolerance, Polygons* polygons) const
|
void ExPolygon::simplify_p(double tolerance, Polygons* polygons) const
|
||||||
|
@ -60,6 +60,10 @@ public:
|
|||||||
// Does this expolygon overlap another expolygon?
|
// Does this expolygon overlap another expolygon?
|
||||||
// Either the ExPolygons intersect, or one is fully inside the other,
|
// Either the ExPolygons intersect, or one is fully inside the other,
|
||||||
// and it is not inside a hole of the other expolygon.
|
// and it is not inside a hole of the other expolygon.
|
||||||
|
// The test may not be commutative if the two expolygons touch by a boundary only,
|
||||||
|
// see unit test SCENARIO("Clipper diff with polyline", "[Clipper]").
|
||||||
|
// Namely expolygons touching at a vertical boundary are considered overlapping, while expolygons touching
|
||||||
|
// at a horizontal boundary are NOT considered overlapping.
|
||||||
bool overlaps(const ExPolygon &other) const;
|
bool overlaps(const ExPolygon &other) const;
|
||||||
|
|
||||||
void simplify_p(double tolerance, Polygons* polygons) const;
|
void simplify_p(double tolerance, Polygons* polygons) const;
|
||||||
|
@ -84,8 +84,7 @@ public:
|
|||||||
float overhangs_area = 0.f;
|
float overhangs_area = 0.f;
|
||||||
|
|
||||||
bool overlaps(const Structure &rhs) const {
|
bool overlaps(const Structure &rhs) const {
|
||||||
//FIXME ExPolygon::overlaps() shall be commutative, it is not!
|
return this->bbox.overlap(rhs.bbox) && this->polygon->overlaps(*rhs.polygon);
|
||||||
return this->bbox.overlap(rhs.bbox) && (this->polygon->overlaps(*rhs.polygon) || rhs.polygon->overlaps(*this->polygon));
|
|
||||||
}
|
}
|
||||||
float overlap_area(const Structure &rhs) const {
|
float overlap_area(const Structure &rhs) const {
|
||||||
double out = 0.;
|
double out = 0.;
|
||||||
|
@ -2,25 +2,115 @@
|
|||||||
|
|
||||||
#include "test_data.hpp"
|
#include "test_data.hpp"
|
||||||
#include "clipper/clipper_z.hpp"
|
#include "clipper/clipper_z.hpp"
|
||||||
|
#include "libslic3r/clipper.hpp"
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
|
|
||||||
// Test case for an issue with duplicity vertices (same XY coordinates but differ in Z coordinates) in Clipper 6.2.9,
|
// tests for ExPolygon::overlaps(const ExPolygon &other)
|
||||||
// (related to https://sourceforge.net/p/polyclipping/bugs/160/) that was fixed in Clipper 6.4.2.
|
SCENARIO("Clipper intersection with polyline", "[Clipper]")
|
||||||
|
{
|
||||||
|
struct TestData {
|
||||||
|
ClipperLib::Path subject;
|
||||||
|
ClipperLib::Path clip;
|
||||||
|
ClipperLib::Paths result;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto run_test = [](const TestData &data) {
|
||||||
|
ClipperLib::Clipper clipper;
|
||||||
|
clipper.AddPath(data.subject, ClipperLib::ptSubject, false);
|
||||||
|
clipper.AddPath(data.clip, ClipperLib::ptClip, true);
|
||||||
|
|
||||||
|
ClipperLib::PolyTree polytree;
|
||||||
|
ClipperLib::Paths paths;
|
||||||
|
clipper.Execute(ClipperLib::ctIntersection, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
|
||||||
|
ClipperLib::PolyTreeToPaths(polytree, paths);
|
||||||
|
|
||||||
|
REQUIRE(paths == data.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
WHEN("Open polyline completely inside stays inside") {
|
||||||
|
run_test({
|
||||||
|
{ { 10, 0 }, { 20, 0 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { 20, 0 }, { 10, 0 } } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Closed polyline completely inside stays inside") {
|
||||||
|
run_test({
|
||||||
|
{ { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Polyline which crosses right rectangle boundary is trimmed") {
|
||||||
|
run_test({
|
||||||
|
{ { 10, 0 }, { 2000, 0 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { 1000, 0 }, { 10, 0 } } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Polyline which is outside clipping region is removed") {
|
||||||
|
run_test({
|
||||||
|
{ { 1500, 0 }, { 2000, 0 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
WHEN("Polyline on left vertical boundary is kept") {
|
||||||
|
run_test({
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { -1000, -1000 }, { -1000, 1000 } } }
|
||||||
|
});
|
||||||
|
run_test({
|
||||||
|
{ { -1000, 1000 }, { -1000, -1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { -1000, 1000 }, { -1000, -1000 } } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Polyline on right vertical boundary is kept") {
|
||||||
|
run_test({
|
||||||
|
{ { 1000, -1000 }, { 1000, 1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { 1000, -1000 }, { 1000, 1000 } } }
|
||||||
|
});
|
||||||
|
run_test({
|
||||||
|
{ { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ { { 1000, 1000 }, { 1000, -1000 } } }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Polyline on bottom horizontal boundary is removed") {
|
||||||
|
run_test({
|
||||||
|
{ { -1000, -1000 }, { 1000, -1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ }
|
||||||
|
});
|
||||||
|
run_test({
|
||||||
|
{ { 1000, -1000 }, { -1000, -1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
WHEN("Polyline on top horizontal boundary is removed") {
|
||||||
|
run_test({
|
||||||
|
{ { -1000, 1000 }, { 1000, 1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ }
|
||||||
|
});
|
||||||
|
run_test({
|
||||||
|
{ { 1000, 1000 }, { -1000, 1000 } },
|
||||||
|
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
|
||||||
|
{ }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
SCENARIO("Clipper Z", "[ClipperZ]")
|
SCENARIO("Clipper Z", "[ClipperZ]")
|
||||||
{
|
{
|
||||||
ClipperLib_Z::Path subject;
|
ClipperLib_Z::Path subject { { -2000, -1000, 10 }, { -2000, 1000, 10 }, { 2000, 1000, 10 }, { 2000, -1000, 10 } };
|
||||||
|
ClipperLib_Z::Path clip{ { -1000, -2000, -5 }, { -1000, 2000, -5 }, { 1000, 2000, -5 }, { 1000, -2000, -5 } };
|
||||||
subject.emplace_back(-2000, -1000, 10);
|
|
||||||
subject.emplace_back(-2000, 1000, 10);
|
|
||||||
subject.emplace_back( 2000, 1000, 10);
|
|
||||||
subject.emplace_back( 2000, -1000, 10);
|
|
||||||
|
|
||||||
ClipperLib_Z::Path clip;
|
|
||||||
clip.emplace_back(-1000, -2000, -5);
|
|
||||||
clip.emplace_back(-1000, 2000, -5);
|
|
||||||
clip.emplace_back( 1000, 2000, -5);
|
|
||||||
clip.emplace_back( 1000, -2000, -5);
|
|
||||||
|
|
||||||
ClipperLib_Z::Clipper clipper;
|
ClipperLib_Z::Clipper clipper;
|
||||||
clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot,
|
clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot,
|
||||||
@ -41,3 +131,4 @@ SCENARIO("Clipper Z", "[ClipperZ]")
|
|||||||
for (const ClipperLib_Z::IntPoint &pt : paths.front())
|
for (const ClipperLib_Z::IntPoint &pt : paths.front())
|
||||||
REQUIRE(pt.z() == 1);
|
REQUIRE(pt.z() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_config.cpp
|
test_config.cpp
|
||||||
test_curve_fitting.cpp
|
test_curve_fitting.cpp
|
||||||
test_elephant_foot_compensation.cpp
|
test_elephant_foot_compensation.cpp
|
||||||
|
test_expolygon.cpp
|
||||||
test_geometry.cpp
|
test_geometry.cpp
|
||||||
test_placeholder_parser.cpp
|
test_placeholder_parser.cpp
|
||||||
test_polygon.cpp
|
test_polygon.cpp
|
||||||
|
67
tests/libslic3r/test_expolygon.cpp
Normal file
67
tests/libslic3r/test_expolygon.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#include <catch2/catch.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/Point.hpp"
|
||||||
|
#include "libslic3r/Polygon.hpp"
|
||||||
|
#include "libslic3r/ExPolygon.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
static inline bool points_close(const Point &p1, const Point &p2)
|
||||||
|
{
|
||||||
|
return (p1 - p2).cast<double>().norm() < SCALED_EPSILON;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool polygons_close_permuted(const Polygon &poly1, const Polygon &poly2, const std::vector<int> &permutation2)
|
||||||
|
{
|
||||||
|
if (poly1.size() != poly2.size() || poly1.size() != permutation2.size())
|
||||||
|
return false;
|
||||||
|
for (size_t i = 0; i < poly1.size(); ++ i)
|
||||||
|
if (poly1[i] != poly2[permutation2[i]])
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SCENARIO("Basics", "[ExPolygon]") {
|
||||||
|
GIVEN("ccw_square") {
|
||||||
|
Polygon ccw_square{ { 100, 100 }, { 200, 100 }, { 200, 200 }, { 100, 200 } };
|
||||||
|
Polygon cw_hole_in_square{ { 140, 140 }, { 140, 160 }, { 160, 160 }, { 160, 140 } };
|
||||||
|
ExPolygon expolygon { ccw_square, cw_hole_in_square };
|
||||||
|
THEN("expolygon is valid") {
|
||||||
|
REQUIRE(expolygon.is_valid());
|
||||||
|
}
|
||||||
|
THEN("expolygon area") {
|
||||||
|
REQUIRE(expolygon.area() == Approx(100*100-20*20));
|
||||||
|
}
|
||||||
|
WHEN("Expolygon scaled") {
|
||||||
|
ExPolygon expolygon2 = expolygon;
|
||||||
|
expolygon2.scale(2.5);
|
||||||
|
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
|
||||||
|
REQUIRE(expolygon.holes.size() == 1);
|
||||||
|
REQUIRE(expolygon2.holes.size() == 1);
|
||||||
|
for (size_t i = 0; i < expolygon.contour.size(); ++ i)
|
||||||
|
REQUIRE(points_close(expolygon.contour[i] * 2.5, expolygon2.contour[i]));
|
||||||
|
for (size_t i = 0; i < expolygon.holes.front().size(); ++ i)
|
||||||
|
REQUIRE(points_close(expolygon.holes.front()[i] * 2.5, expolygon2.holes.front()[i]));
|
||||||
|
}
|
||||||
|
WHEN("Expolygon translated") {
|
||||||
|
ExPolygon expolygon2 = expolygon;
|
||||||
|
expolygon2.translate(10, -5);
|
||||||
|
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
|
||||||
|
REQUIRE(expolygon.holes.size() == 1);
|
||||||
|
REQUIRE(expolygon2.holes.size() == 1);
|
||||||
|
for (size_t i = 0; i < expolygon.contour.size(); ++ i)
|
||||||
|
REQUIRE(points_close(expolygon.contour[i] + Point(10, -5), expolygon2.contour[i]));
|
||||||
|
for (size_t i = 0; i < expolygon.holes.front().size(); ++ i)
|
||||||
|
REQUIRE(points_close(expolygon.holes.front()[i] + Point(10, -5), expolygon2.holes.front()[i]));
|
||||||
|
}
|
||||||
|
WHEN("Expolygon rotated around point") {
|
||||||
|
ExPolygon expolygon2 = expolygon;
|
||||||
|
expolygon2.rotate(M_PI / 2, Point(150, 150));
|
||||||
|
REQUIRE(expolygon.contour.size() == expolygon2.contour.size());
|
||||||
|
REQUIRE(expolygon.holes.size() == 1);
|
||||||
|
REQUIRE(expolygon2.holes.size() == 1);
|
||||||
|
REQUIRE(polygons_close_permuted(expolygon2.contour, expolygon.contour, { 1, 2, 3, 0}));
|
||||||
|
REQUIRE(polygons_close_permuted(expolygon2.holes.front(), expolygon.holes.front(), { 3, 0, 1, 2}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
|
|
||||||
use List::Util qw(first sum);
|
|
||||||
use Slic3r::XS;
|
|
||||||
use Test::More tests => 7;
|
|
||||||
|
|
||||||
use constant PI => 4 * atan2(1, 1);
|
|
||||||
|
|
||||||
my $square = [ # ccw
|
|
||||||
[100, 100],
|
|
||||||
[200, 100],
|
|
||||||
[200, 200],
|
|
||||||
[100, 200],
|
|
||||||
];
|
|
||||||
my $hole_in_square = [ # cw
|
|
||||||
[140, 140],
|
|
||||||
[140, 160],
|
|
||||||
[160, 160],
|
|
||||||
[160, 140],
|
|
||||||
];
|
|
||||||
|
|
||||||
my $expolygon = Slic3r::ExPolygon->new($square, $hole_in_square);
|
|
||||||
|
|
||||||
ok $expolygon->is_valid, 'is_valid';
|
|
||||||
|
|
||||||
is_deeply $expolygon->clone->pp, [$square, $hole_in_square], 'clone';
|
|
||||||
|
|
||||||
is $expolygon->area, 100*100-20*20, 'area';
|
|
||||||
|
|
||||||
{
|
|
||||||
my $expolygon2 = $expolygon->clone;
|
|
||||||
$expolygon2->scale(2.5);
|
|
||||||
is_deeply $expolygon2->pp, [
|
|
||||||
[map [ 2.5*$_->[0], 2.5*$_->[1] ], @$square],
|
|
||||||
[map [ 2.5*$_->[0], 2.5*$_->[1] ], @$hole_in_square]
|
|
||||||
], 'scale';
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
my $expolygon2 = $expolygon->clone;
|
|
||||||
$expolygon2->translate(10, -5);
|
|
||||||
is_deeply $expolygon2->pp, [
|
|
||||||
[map [ $_->[0]+10, $_->[1]-5 ], @$square],
|
|
||||||
[map [ $_->[0]+10, $_->[1]-5 ], @$hole_in_square]
|
|
||||||
], 'translate';
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
my $expolygon2 = $expolygon->clone;
|
|
||||||
$expolygon2->rotate(PI/2, Slic3r::Point->new(150,150));
|
|
||||||
is_deeply $expolygon2->pp, [
|
|
||||||
[ @$square[1,2,3,0] ],
|
|
||||||
[ @$hole_in_square[3,0,1,2] ]
|
|
||||||
], 'rotate around Point';
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
my $expolygon2 = $expolygon->clone;
|
|
||||||
$expolygon2->rotate(PI/2, [150,150]);
|
|
||||||
is_deeply $expolygon2->pp, [
|
|
||||||
[ @$square[1,2,3,0] ],
|
|
||||||
[ @$hole_in_square[3,0,1,2] ]
|
|
||||||
], 'rotate around pure-Perl Point';
|
|
||||||
}
|
|
||||||
|
|
||||||
__END__
|
|
Loading…
Reference in New Issue
Block a user