Deduplicate knockout items before filling

Clipper has trouble when multiple outlines have the same points.  It
gets there eventually but very slowly.  As a precaution, we de-dupe our
primary knockout elements before filling.

Fixes https://gitlab.com/kicad/code/kicad/issues/22643
This commit is contained in:
Seth Hillbrand
2026-01-08 18:13:40 -08:00
parent 6afeb6e09a
commit 78b576537e
+185
View File
@@ -24,7 +24,9 @@
*/
#include <future>
#include <hash.h>
#include <set>
#include <unordered_set>
#include <core/kicad_algo.h>
#include <advanced_config.h>
#include <board.h>
@@ -206,6 +208,101 @@ private:
};
/**
* Helper structures for deduplicating coincident knockout items.
* When multiple pads, vias, or tracks occupy the same position with the same geometry,
* we only need to add them to the knockout polygon once.
*/
namespace
{
/// Key for deduplicating coincident pads.
/// For circular pads: uses max of drill and pad size.
/// For non-circular pads: uses pad size only.
struct PAD_KNOCKOUT_KEY
{
VECTOR2I position;
VECTOR2I effectiveSize; // For circular: max of drill and pad; otherwise pad size
int shape; // PAD_SHAPE enum value
EDA_ANGLE orientation;
bool operator==( const PAD_KNOCKOUT_KEY& other ) const
{
return position == other.position && effectiveSize == other.effectiveSize
&& shape == other.shape && orientation == other.orientation;
}
};
struct PAD_KNOCKOUT_KEY_HASH
{
size_t operator()( const PAD_KNOCKOUT_KEY& key ) const
{
return hash_val( key.position.x, key.position.y, key.effectiveSize.x, key.effectiveSize.y,
key.shape, key.orientation.AsDegrees() );
}
};
/// Key for deduplicating coincident vias (circular, so use max of drill and width)
struct VIA_KNOCKOUT_KEY
{
VECTOR2I position;
int effectiveSize; // max of drill and via width
bool operator==( const VIA_KNOCKOUT_KEY& other ) const
{
return position == other.position && effectiveSize == other.effectiveSize;
}
};
struct VIA_KNOCKOUT_KEY_HASH
{
size_t operator()( const VIA_KNOCKOUT_KEY& key ) const
{
return hash_val( key.position.x, key.position.y, key.effectiveSize );
}
};
/// Key for deduplicating coincident tracks (same endpoints, width)
/// Endpoints are canonicalized so that start < end lexicographically
struct TRACK_KNOCKOUT_KEY
{
VECTOR2I start;
VECTOR2I end;
int width;
TRACK_KNOCKOUT_KEY( const VECTOR2I& aStart, const VECTOR2I& aEnd, int aWidth ) :
width( aWidth )
{
// Canonicalize endpoint order for consistent hashing
if( aStart.x < aEnd.x || ( aStart.x == aEnd.x && aStart.y <= aEnd.y ) )
{
start = aStart;
end = aEnd;
}
else
{
start = aEnd;
end = aStart;
}
}
bool operator==( const TRACK_KNOCKOUT_KEY& other ) const
{
return start == other.start && end == other.end && width == other.width;
}
};
struct TRACK_KNOCKOUT_KEY_HASH
{
size_t operator()( const TRACK_KNOCKOUT_KEY& key ) const
{
return hash_val( key.start.x, key.start.y, key.end.x, key.end.y, key.width );
}
};
} // anonymous namespace
ZONE_FILLER::ZONE_FILLER( BOARD* aBoard, COMMIT* aCommit ) :
m_board( aBoard ),
m_brdOutlinesValid( false ),
@@ -1489,6 +1586,10 @@ void ZONE_FILLER::knockoutThermalReliefs( const ZONE* aZone, PCB_LAYER_ID aLayer
int holeClearance;
SHAPE_POLY_SET holes;
// Deduplication sets for coincident pads and vias
std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
for( FOOTPRINT* footprint : m_board->Footprints() )
{
for( PAD* pad : footprint->Pads() )
@@ -1502,6 +1603,33 @@ void ZONE_FILLER::knockoutThermalReliefs( const ZONE* aZone, PCB_LAYER_ID aLayer
if( !padBBox.Intersects( aZone->GetBoundingBox() ) )
continue;
// Deduplicate coincident pads (skip custom pads - they have complex shapes)
PAD_SHAPE padShape = pad->GetShape( aLayer );
if( padShape != PAD_SHAPE::CUSTOM )
{
// For circular pads: use max of drill and pad size; otherwise just pad size
VECTOR2I padSize = pad->GetSize( aLayer );
VECTOR2I effectiveSize;
if( padShape == PAD_SHAPE::CIRCLE )
{
int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
int maxDim = std::max( { padSize.x, padSize.y, drill } );
effectiveSize = VECTOR2I( maxDim, maxDim );
}
else
{
effectiveSize = padSize;
}
PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
static_cast<int>( padShape ), pad->GetOrientation() };
if( !processedPads.insert( padKey ).second )
continue;
}
bool noConnection = pad->GetNetCode() != aZone->GetNetCode();
if( !aZone->IsTeardropArea() )
@@ -1651,6 +1779,13 @@ void ZONE_FILLER::knockoutThermalReliefs( const ZONE* aZone, PCB_LAYER_ID aLayer
if( !viaBBox.Intersects( aZone->GetBoundingBox() ) )
continue;
// Deduplicate coincident vias (circular, so use max of drill and width)
int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
if( !processedVias.insert( viaKey ).second )
continue;
bool noConnection = via->GetNetCode() != aZone->GetNetCode()
|| ( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::START_END_ONLY
&& aLayer != via->Padstack().Drill().start
@@ -1725,6 +1860,11 @@ void ZONE_FILLER::buildCopperItemClearances( const ZONE* aZone, PCB_LAYER_ID aLa
BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
long ticker = 0;
// Deduplication sets for coincident items
std::unordered_set<PAD_KNOCKOUT_KEY, PAD_KNOCKOUT_KEY_HASH> processedPads;
std::unordered_set<VIA_KNOCKOUT_KEY, VIA_KNOCKOUT_KEY_HASH> processedVias;
std::unordered_set<TRACK_KNOCKOUT_KEY, TRACK_KNOCKOUT_KEY_HASH> processedTracks;
auto checkForCancel =
[&ticker]( PROGRESS_REPORTER* aReporter ) -> bool
{
@@ -1829,6 +1969,33 @@ void ZONE_FILLER::buildCopperItemClearances( const ZONE* aZone, PCB_LAYER_ID aLa
if( checkForCancel( m_progressReporter ) )
return;
// Deduplicate coincident pads (skip custom pads - they have complex shapes)
PAD_SHAPE padShape = pad->GetShape( aLayer );
if( padShape != PAD_SHAPE::CUSTOM )
{
// For circular pads: use max of drill and pad size; otherwise just pad size
VECTOR2I padSize = pad->GetSize( aLayer );
VECTOR2I effectiveSize;
if( padShape == PAD_SHAPE::CIRCLE )
{
int drill = std::max( pad->GetDrillSize().x, pad->GetDrillSize().y );
int maxDim = std::max( { padSize.x, padSize.y, drill } );
effectiveSize = VECTOR2I( maxDim, maxDim );
}
else
{
effectiveSize = padSize;
}
PAD_KNOCKOUT_KEY padKey{ pad->GetPosition(), effectiveSize,
static_cast<int>( padShape ), pad->GetOrientation() };
if( !processedPads.insert( padKey ).second )
continue;
}
knockoutPadClearance( pad );
}
@@ -1937,6 +2104,24 @@ void ZONE_FILLER::buildCopperItemClearances( const ZONE* aZone, PCB_LAYER_ID aLa
if( checkForCancel( m_progressReporter ) )
return;
// Deduplicate coincident tracks and vias
if( track->Type() == PCB_VIA_T )
{
PCB_VIA* via = static_cast<PCB_VIA*>( track );
int viaEffectiveSize = std::max( via->GetDrillValue(), via->GetWidth( aLayer ) );
VIA_KNOCKOUT_KEY viaKey{ via->GetPosition(), viaEffectiveSize };
if( !processedVias.insert( viaKey ).second )
continue;
}
else
{
TRACK_KNOCKOUT_KEY trackKey( track->GetStart(), track->GetEnd(), track->GetWidth() );
if( !processedTracks.insert( trackKey ).second )
continue;
}
knockoutTrackClearance( track );
}