Use ZFillFunction to modify arcs post clipper (does not fully work yet)

Added a unit test to verify union and intersection of polygons result
in desired results: subject + clip - intersect == union
This commit is contained in:
Roberto Fernandez Bautista 2021-06-01 21:48:16 +01:00 committed by Jon Evans
parent 437e2783fb
commit 260a9d0540
6 changed files with 256 additions and 6 deletions

View File

@ -39,6 +39,12 @@
*/
struct CLIPPER_Z_VALUE
{
CLIPPER_Z_VALUE()
{
m_FirstArcIdx = -1;
m_SecondArcIdx = -1;
}
CLIPPER_Z_VALUE( const std::pair<ssize_t, ssize_t> aShapeIndices, ssize_t aOffset = 0 )
{
m_FirstArcIdx = aShapeIndices.first;
@ -836,7 +842,19 @@ protected:
void splitArc( ssize_t aPtIndex, bool aCoincident = false );
void ammendArc( size_t aArcIndex, VECTOR2I aNewStart, VECTOR2I aNewEnd );
void ammendArc( size_t aArcIndex, const VECTOR2I& aNewStart, const VECTOR2I& aNewEnd );
void ammendArcStart( size_t aArcIndex, const VECTOR2I& aNewStart )
{
ammendArc( aArcIndex, aNewStart, m_arcs[aArcIndex].GetP1() );
}
void ammendArcEnd( size_t aArcIndex, const VECTOR2I& aNewEnd )
{
ammendArc( aArcIndex, m_arcs[aArcIndex].GetP0(), aNewEnd );
}
/**
* Create a new Clipper path from the SHAPE_LINE_CHAIN in a given orientation

View File

@ -151,7 +151,8 @@ void SHAPE_LINE_CHAIN::convertArc( ssize_t aArcIndex )
}
void SHAPE_LINE_CHAIN::ammendArc( size_t aArcIndex, VECTOR2I aNewStart, VECTOR2I aNewEnd )
void SHAPE_LINE_CHAIN::ammendArc( size_t aArcIndex, const VECTOR2I& aNewStart,
const VECTOR2I& aNewEnd )
{
wxCHECK_MSG( aArcIndex < m_arcs.size(), /* void */,
"Invalid arc index requested." );

View File

@ -33,6 +33,7 @@
#include <cstdio>
#include <istream> // for operator<<, operator>>
#include <limits> // for numeric_limits
#include <map>
#include <memory>
#include <set>
#include <string> // for char_traits, operator!=
@ -518,6 +519,7 @@ void SHAPE_POLY_SET::booleanOp( ClipperLib::ClipType aType, const SHAPE_POLY_SET
std::vector<CLIPPER_Z_VALUE> zValues;
std::vector<SHAPE_ARC> arcBuffer;
std::map<VECTOR2I, CLIPPER_Z_VALUE> newIntersectPoints;
for( const POLYGON& poly : aShape.m_polys )
{
@ -540,12 +542,62 @@ void SHAPE_POLY_SET::booleanOp( ClipperLib::ClipType aType, const SHAPE_POLY_SET
ClipperLib::PolyTree solution;
ClipperLib::ZFillCallback callback =
[]( ClipperLib::IntPoint & e1bot, ClipperLib::IntPoint & e1top,
[&]( ClipperLib::IntPoint & e1bot, ClipperLib::IntPoint & e1top,
ClipperLib::IntPoint & e2bot, ClipperLib::IntPoint & e2top,
ClipperLib::IntPoint & pt )
{
//@todo write callback to handle arc intersections
int i = 0;
auto arcIndex =
[&]( const ssize_t& aZvalue, const ssize_t& aCompareVal = -1 ) -> ssize_t
{
ssize_t retval;
retval = zValues.at( aZvalue ).m_SecondArcIdx;
if( retval == -1 || ( aCompareVal > 0 && retval != aCompareVal ) )
retval = zValues.at( aZvalue ).m_FirstArcIdx;
return retval;
};
auto arcSegment =
[&]( const ssize_t& aBottomZ, const ssize_t aTopZ ) -> ssize_t
{
ssize_t retval = arcIndex( aBottomZ );
if( retval != -1 )
{
if( retval != arcIndex( aTopZ, retval ) )
retval = -1; // Not an arc segment as the two indices do not match
}
return retval;
};
ssize_t e1ArcSegmentIndex = arcSegment( e1bot.Z, e1top.Z );
ssize_t e2ArcSegmentIndex = arcSegment( e2bot.Z, e2top.Z );
CLIPPER_Z_VALUE newZval;
if( e1ArcSegmentIndex != -1 )
{
newZval.m_FirstArcIdx = e1ArcSegmentIndex;
newZval.m_SecondArcIdx = e2ArcSegmentIndex;
}
else
{
newZval.m_FirstArcIdx = e2ArcSegmentIndex;
newZval.m_SecondArcIdx = -1;
}
size_t z_value_ptr = zValues.size();
zValues.push_back( newZval );
// Only worry about arc segments for later processing
if( newZval.m_FirstArcIdx != -1 )
newIntersectPoints.insert( { VECTOR2I( pt.X, pt.Y ), newZval } );
pt.Z = z_value_ptr;
//@todo ammend X,Y values to true intersection between arcs or arc and segment
};
c.ZFillFunction( callback ); // register callback
@ -553,6 +605,45 @@ void SHAPE_POLY_SET::booleanOp( ClipperLib::ClipType aType, const SHAPE_POLY_SET
c.Execute( aType, solution, ClipperLib::pftNonZero, ClipperLib::pftNonZero );
importTree( &solution, zValues, arcBuffer );
// Ammend arcs for the intersection points
for( auto& poly : m_polys )
{
for( size_t i = 0; i < poly.size(); i++ )
{
for( int j = 0; j < poly[i].PointCount(); j++ )
{
const VECTOR2I& pt = poly[i].CPoint( j );
if( newIntersectPoints.find( pt ) != newIntersectPoints.end() )
{
const std::pair<ssize_t, ssize_t>& shape = poly[i].CShapes()[j];
CLIPPER_Z_VALUE zval = newIntersectPoints.at( pt );
// Fixup arc end points to match the new intersection points found in clipper
//@todo consider editing the intersection point to be the "true" arc intersection
if( poly[i].IsSharedPt( j ) )
{
poly[i].ammendArcEnd( shape.first, pt );
poly[i].ammendArcStart( shape.second, pt );
}
else if( poly[i].IsArcStart( j ) )
{
poly[i].ammendArcStart( shape.first, pt );
}
else if( poly[i].IsArcEnd( j ) )
{
poly[i].ammendArcEnd( shape.first, pt );
}
else
{
poly[i].splitArc( j );
}
}
}
}
}
}

View File

@ -50,6 +50,9 @@ struct CommonTestData
///< Holes and outline contain arcs
SHAPE_POLY_SET holeyCurvedPolyMulti; ///< Polygon with a multiple outlines + multiple holes.
///< Holes and outlines contain arcs
SHAPE_POLY_SET holeyCurvedPolyInter; ///< Polygon with a single outlines + multiple holes.
///< Holes and outlines contain arcs. Intersects with above
///< two polysets
// Vectors containing the information with which the polygons are populated.
std::vector<VECTOR2I> uniquePoints;
@ -191,6 +194,20 @@ struct CommonTestData
o1h2.Append( SHAPE_ARC( { 760000, 258000 }, { 746000, 222000 }, { 686000, 260000 }, 0 ) );
o1h2.SetClosed( true );
holeyCurvedPolyMulti.AddHole( o1h2 );
// Intersecting shape:
SHAPE_LINE_CHAIN oline2;
oline2.Append( { 340000, 540000 } );
oline2.Append( SHAPE_ARC( { 629000, 659000 }, { 790000, 436000 }, { 751000, 161000 }, 0 ) );
oline2.Append( SHAPE_ARC( { 651000, 161000 }, { 300000, 272000 }, { 320000, 426000 }, 0 ) );
oline2.SetClosed( true );
holeyCurvedPolyInter.AddOutline( oline2 );
SHAPE_LINE_CHAIN o2h2;
o2h2.Append( SHAPE_ARC( { 680000, 486000 }, { 728000, 506000 }, { 760000, 458000 }, 0 ) );
o2h2.Append( SHAPE_ARC( { 760000, 458000 }, { 746000, 422000 }, { 686000, 460000 }, 0 ) );
o2h2.SetClosed( true );
holeyCurvedPolyInter.AddHole( o2h2 );
}
~CommonTestData()

View File

@ -221,6 +221,81 @@ inline SHAPE_POLY_SET FilletPolySet( SHAPE_POLY_SET& aPolySet, int aRadius, int
return filletedPolySet;
}
/**
* Verify that a SHAPE_LINE_CHAIN has been assembled correctly by ensuring that the
* arc start and end points match points on the chain and that any points inside the arcs
* actually collide with the arc segments (with an error margin of 5000 IU)
*
* @param aChain to test
* @return true if outline is valid
*/
inline bool IsOutlineValid( const SHAPE_LINE_CHAIN& aChain )
{
bool areWeOnArc = false;
SHAPE_ARC currentArc;
for( int i = 0; i < aChain.PointCount(); i++ )
{
if( !aChain.IsSharedPt( i ) && aChain.IsArcStart( i ) )
{
currentArc = aChain.Arc( aChain.ArcIndex( i ) );
areWeOnArc = true;
if( currentArc.GetP0() != aChain.CPoint( i ) )
return false;
}
if( areWeOnArc )
{
if( !currentArc.Collide( aChain.CPoint( i ), 5000 ) )
return false;
}
if( aChain.IsArcEnd( i ) )
{
areWeOnArc = false;
if( currentArc.GetP1() != aChain.CPoint( i ) )
return false;
}
if( aChain.IsSharedPt( i ) )
{
currentArc = aChain.Arc( aChain.ArcIndex( i ) );
areWeOnArc = true;
if( currentArc.GetP0() != aChain.CPoint( i ) )
return false;
}
}
return true;
}
/**
* Verify that a SHAPE_POLY_SET has been assembled correctly by verifying each of the outlines
* and holes contained within
*
* @param aSet to test
* @return true if the poly set is valid
*/
inline bool IsPolySetValid( const SHAPE_POLY_SET& aSet )
{
for( int i = 0; i < aSet.OutlineCount(); i++ )
{
if( !IsOutlineValid( aSet.Outline( i ) ) )
return false;
for( int j = 0; j < aSet.HoleCount( i ); j++ )
{
if( !IsOutlineValid( aSet.CHole( i, j ) ) )
return false;
}
}
return true;
}
} // namespace GEOM_TEST
namespace BOOST_TEST_PRINT_NAMESPACE_OPEN

View File

@ -20,12 +20,13 @@
#include <string>
#include <tuple>
#include <unit_test_utils/unit_test_utils.h>
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <geometry/shape_line_chain.h>
#include <geometry/shape_poly_set.h>
#include "fixtures_geometry.h"
#include "geom_test_utils.h"
BOOST_AUTO_TEST_SUITE( CurvedPolys )
@ -80,4 +81,51 @@ BOOST_AUTO_TEST_CASE( TestSimplify )
}
}
/**
* Check intersection and union between two polygons
*/
BOOST_AUTO_TEST_CASE( TestIntersectUnion )
{
KI_TEST::CommonTestData testData;
std::map<std::string, SHAPE_POLY_SET> polysToTest = {
{ "Case 1: Single polygon", testData.holeyCurvedPolySingle },
//{ "Case 2: Multi polygon", testData.holeyCurvedPolyMulti } // This test fails right now:
// multiple polys unsupported
};
for( std::pair<std::string, SHAPE_POLY_SET> testCase : polysToTest )
{
BOOST_TEST_CONTEXT( testCase.first )
{
SHAPE_POLY_SET testPoly = testCase.second;
SHAPE_POLY_SET opPoly = testData.holeyCurvedPolyInter;
BOOST_CHECK( GEOM_TEST::IsPolySetValid( testPoly ) );
BOOST_CHECK( GEOM_TEST::IsPolySetValid( opPoly ) );
double testPolyArea = testPoly.Area();
double opPolyArea = opPoly.Area();
SHAPE_POLY_SET intersectionPoly = testPoly;
intersectionPoly.BooleanIntersection( opPoly, SHAPE_POLY_SET::POLYGON_MODE::PM_STRICTLY_SIMPLE );
double intersectArea = intersectionPoly.Area();
BOOST_CHECK( GEOM_TEST::IsPolySetValid( intersectionPoly ) );
SHAPE_POLY_SET unionPoly = testPoly;
unionPoly.BooleanAdd( opPoly, SHAPE_POLY_SET::POLYGON_MODE::PM_STRICTLY_SIMPLE );
double unionArea = unionPoly.Area();
BOOST_CHECK( GEOM_TEST::IsPolySetValid( unionPoly ) );
// Acceptable error of 0.01% (fails at 0.001% for some - this is a Clipper limitation)
BOOST_CHECK_CLOSE( testPolyArea + opPolyArea - intersectArea, unionArea, 0.01 );
}
}
}
BOOST_AUTO_TEST_SUITE_END()