Fix Chessboard splitting

Due to a bug(?) in Clipper2, 45° collinear edges may not be detected.
See https://github.com/AngusJohnson/Clipper2/issues/1008 for current
status.  In the meantime, we pre-process these to remove the extraneous
joints preventing our triangulation from getting mixed up

Fixes https://gitlab.com/kicad/code/kicad/-/issues/18176
This commit is contained in:
Seth Hillbrand 2025-08-27 14:59:04 -07:00
parent ea8206eca5
commit 408e1feae2
4 changed files with 244 additions and 0 deletions

View File

@ -1506,6 +1506,8 @@ private:
void inflateLine2( const SHAPE_LINE_CHAIN& aLine, int aAmount, int aCircleSegCount,
CORNER_STRATEGY aCornerStrategy, bool aSimplify = false );
void splitCollinearOutlines();
/**
* This is the engine to execute all polygon boolean transforms (AND, OR, ... and polygon
* simplification (merging overlapping polygons).

View File

@ -41,6 +41,7 @@
#include <unordered_set>
#include <utility> // for swap, move
#include <vector>
#include <array>
#include <clipper2/clipper.h>
#include <geometry/geometry_utils.h>
@ -49,6 +50,7 @@
#include <geometry/shape.h>
#include <geometry/shape_line_chain.h>
#include <geometry/shape_poly_set.h>
#include <geometry/rtree.h>
#include <math/box2.h> // for BOX2I
#include <math/util.h> // for KiROUND, rescale
#include <math/vector2d.h> // for VECTOR2I, VECTOR2D, VECTOR2
@ -69,6 +71,46 @@
#define ENABLECACHEFRIENDLYFRACTURE ADVANCED_CFG::GetCfg().m_EnableCacheFriendlyFracture
#endif
static bool segmentsColinearOverlap( const SEG& a, const SEG& b, VECTOR2I& s, VECTOR2I& e )
{
const VECTOR2I da = a.B - a.A;
const VECTOR2I db = b.B - b.A;
if( da.Cross( db ) != 0 )
return false;
if( da.Cross( b.A - a.A ) != 0 )
return false;
int axis = std::abs( da.x ) >= std::abs( da.y ) ? 0 : 1;
int aMin = axis == 0 ? std::min( a.A.x, a.B.x ) : std::min( a.A.y, a.B.y );
int aMax = axis == 0 ? std::max( a.A.x, a.B.x ) : std::max( a.A.y, a.B.y );
int bMin = axis == 0 ? std::min( b.A.x, b.B.x ) : std::min( b.A.y, b.B.y );
int bMax = axis == 0 ? std::max( b.A.x, b.B.x ) : std::max( b.A.y, b.B.y );
int lo = std::max( aMin, bMin );
int hi = std::min( aMax, bMax );
if( hi <= lo )
return false;
std::array<VECTOR2I,4> pts = { a.A, a.B, b.A, b.B };
std::sort( pts.begin(), pts.end(), [axis]( const VECTOR2I& p, const VECTOR2I& q )
{
if( axis == 0 )
return p.x < q.x || ( p.x == q.x && p.y < q.y );
else
return p.y < q.y || ( p.y == q.y && p.x < q.x );
} );
s = pts[1];
e = pts[2];
return true;
}
SHAPE_POLY_SET::SHAPE_POLY_SET() :
SHAPE( SH_POLY_SET )
{
@ -1860,8 +1902,110 @@ void SHAPE_POLY_SET::Unfracture()
}
void SHAPE_POLY_SET::splitCollinearOutlines()
{
for( size_t polyIdx = 0; polyIdx < m_polys.size(); ++polyIdx )
{
bool changed = true;
while( changed )
{
changed = false;
SHAPE_LINE_CHAIN& outline = m_polys[polyIdx][0];
intptr_t count = outline.PointCount();
RTree<intptr_t, intptr_t, 2, intptr_t> rtree;
for( intptr_t i = 0; i < count; ++i )
{
const VECTOR2I& a = outline.CPoint( i );
const VECTOR2I& b = outline.CPoint( ( i + 1 ) % count );
intptr_t min[2] = { std::min( a.x, b.x ), std::min( a.y, b.y ) };
intptr_t max[2] = { std::max( a.x, b.x ), std::max( a.y, b.y ) };
rtree.Insert( min, max, i );
}
bool found = false;
int segA = -1;
int segB = -1;
VECTOR2I s, e;
for( intptr_t i = 0; i < count && !found; ++i )
{
const VECTOR2I& a = outline.CPoint( i );
const VECTOR2I& b = outline.CPoint( ( i + 1 ) % count );
SEG seg( a, b );
intptr_t min[2] = { std::min( a.x, b.x ), std::min( a.y, b.y ) };
intptr_t max[2] = { std::max( a.x, b.x ), std::max( a.y, b.y ) };
auto visitor =
[&]( const int& j ) -> bool
{
if( j == i || j == ( ( i + 1 ) % count ) || j == ( ( i + count - 1 ) % count ) )
return true;
VECTOR2I oa = outline.CPoint( j );
VECTOR2I ob = outline.CPoint( ( j + 1 ) % count );
SEG other( oa, ob );
if( segmentsColinearOverlap( seg, other, s, e ) )
{
segA = i;
segB = j;
found = true;
return false;
}
return true;
};
rtree.Search( min, max, visitor );
}
if( !found )
break;
int a0 = segA;
int a1 = ( segA + 1 ) % outline.PointCount();
int b0 = segB;
int b1 = ( segB + 1 ) % outline.PointCount();
SHAPE_LINE_CHAIN lc1;
int idx = a1;
lc1.Append( outline.CPoint( idx ) );
while( idx != b0 )
{
idx = ( idx + 1 ) % outline.PointCount();
lc1.Append( outline.CPoint( idx ) );
}
lc1.SetClosed( true );
SHAPE_LINE_CHAIN lc2;
idx = b1;
lc2.Append( outline.CPoint( idx ) );
while( idx != a0 )
{
idx = ( idx + 1 ) % outline.PointCount();
lc2.Append( outline.CPoint( idx ) );
}
lc2.SetClosed( true );
m_polys[polyIdx][0] = lc1;
POLYGON np;
np.push_back( lc2 );
m_polys.push_back( np );
changed = true;
}
}
}
void SHAPE_POLY_SET::Simplify()
{
splitCollinearOutlines();
SHAPE_POLY_SET empty;
booleanOp( Clipper2Lib::ClipType::Union, empty );

View File

@ -47,6 +47,7 @@ set( QA_KIMATH_SRCS
geometry/test_shape_poly_set_collision.cpp
geometry/test_shape_poly_set_distance.cpp
geometry/test_shape_poly_set_iterator.cpp
geometry/test_shape_poly_set_split_outlines.cpp
geometry/test_shape_line_chain.cpp
geometry/test_shape_line_chain_collision.cpp
geometry/test_vector_utils.cpp

View File

@ -0,0 +1,97 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2025 The KiCad Developers, see AUTHORS.txt for contributors.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <vector>
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <geometry/shape_poly_set.h>
#include <qa_utils/geometry/geometry.h>
#include <geometry/geom_test_utils.h>
BOOST_AUTO_TEST_SUITE( ShapePolySetSplitOutlines )
BOOST_AUTO_TEST_CASE( SplitCoincidentOutlineOppositeDirection )
{
// ASCII art representation of the polygon:
// 1-----------------0
// | |
// | |
// | 4------5 |
// | | | |
// 2/9--3/8 | |
// | | | |
// 10---7------6----11
SHAPE_POLY_SET poly;
SHAPE_LINE_CHAIN outline1( { 7600000, 9000000, 6600000, 9000000, 6600000, 8750000, 7000000, 8750000, 7000000,
9000000, 7250000, 9000000, 7250000, 8500000, 7000000, 8500000, 7000000, 8750000,
6600000, 8750000, 6600000, 8000000, 7600000, 8000000 } );
outline1.SetClosed( true );
poly.AddOutline( outline1 );
poly.Simplify();
BOOST_CHECK_EQUAL( poly.OutlineCount(), 1 );
BOOST_CHECK_EQUAL( poly.Outline( 0 ).PointCount(), 10 ); //Why is this 10? I think it should probably be 8
BOOST_CHECK( GEOM_TEST::IsPolySetValid( poly ) );
}
BOOST_AUTO_TEST_CASE( SplitCoincidentOutlineSameDirection )
{
// ASCII art representation of the polygon (approximate shape):
// 8
// / /
// / /
// / 5 /
// / /\ /
// 7 / 6/ \ /
// 1-----------2\ /4
// \ \/
// \ /3
// \ /
// \ /
// 0
// This polygon has a self-intersecting/overlapping path that creates
// coincident edges going in the same direction (points 7→2 and 2→7)
// Original coordinates (scaled): 0:(99,83) 1:(93,89) 2:(80,86) 3:(94,85)
// 4:(96,87) 5:(96,86) 6:(95,85) 7:(94,85) repeated points: 7:(94,85) 2:(80,86)
SHAPE_POLY_SET poly;
SHAPE_LINE_CHAIN outline1( { 9912310, 8325057, 9288816, 8948550, 8000000, 8567586, 9428364,
8547698, 9585009, 8652365, 9613140, 8624234, 9471719, 8482813,
9428364, 8547698, 8000000, 8567586 } );
outline1.SetClosed( true );
poly.AddOutline( outline1 );
poly.Simplify();
BOOST_CHECK_EQUAL( poly.OutlineCount(), 1 );
BOOST_CHECK_EQUAL( poly.Outline( 0 ).PointCount(), 7 );
BOOST_CHECK( GEOM_TEST::IsPolySetValid( poly ) );
}
BOOST_AUTO_TEST_SUITE_END()