Re-prune minimum-width violations after zone knockouts

Zone-to-zone clearance knockouts and higher-priority zone subtraction
happen after the initial deflate/inflate minimum-width enforcement
cycle.  These knockouts can carve out material and leave thin copper
strips that violate the zone's minimum width constraint.

Add a second deflate/inflate pass that runs only when knockouts were
actually applied.  The result is intersected with the pre-deflate
boundary to prevent re-inflation into cleared areas (keepouts, pad
clearances, etc.).

Fixes https://gitlab.com/kicad/code/kicad/-/issues/23332
This commit is contained in:
Seth Hillbrand
2026-03-06 14:31:56 -08:00
parent 2a8f3ea5cd
commit 7c4229c066
4 changed files with 249 additions and 0 deletions
+56
View File
@@ -2553,6 +2553,45 @@ void ZONE_FILLER::connect_nearby_polys( SHAPE_POLY_SET& aPolys, double aDistance
}
void ZONE_FILLER::postKnockoutMinWidthPrune( const ZONE* aZone, SHAPE_POLY_SET& aFillPolys )
{
int half_min_width = aZone->GetMinThickness() / 2;
int epsilon = pcbIUScale.mmToIU( 0.001 );
if( half_min_width - epsilon <= epsilon )
return;
SHAPE_POLY_SET preDeflate = aFillPolys.CloneDropTriangulation();
aFillPolys.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
m_maxError );
aFillPolys.Fracture();
connect_nearby_polys( aFillPolys, aZone->GetMinThickness() );
for( int ii = aFillPolys.OutlineCount() - 1; ii >= 0; ii-- )
{
std::vector<SHAPE_LINE_CHAIN>& island = aFillPolys.Polygon( ii );
BOX2I islandExtents;
for( const VECTOR2I& pt : island.front().CPoints() )
{
islandExtents.Merge( pt );
if( islandExtents.GetSizeMax() > aZone->GetMinThickness() )
break;
}
if( islandExtents.GetSizeMax() < aZone->GetMinThickness() )
aFillPolys.DeletePolygon( ii );
}
aFillPolys.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS, m_maxError,
true );
aFillPolys.BooleanIntersection( preDeflate );
}
#define DUMP_POLYS_TO_COPPER_LAYER( a, b, c ) \
{ if( m_debugZoneFiller && aDebugLayer == b ) \
{ \
@@ -2838,6 +2877,8 @@ bool ZONE_FILLER::fillCopperZone( const ZONE* aZone, PCB_LAYER_ID aLayer, PCB_LA
// Cache the pre-knockout fill for iterative refill optimization (issue 21746).
// The cache stores the fill BEFORE zone-to-zone knockouts so the iterative refill can
// reclaim space when higher-priority zones have islands removed.
bool knockoutsApplied = false;
if( iterativeRefill )
{
{
@@ -2859,16 +2900,29 @@ bool ZONE_FILLER::fillCopperZone( const ZONE* aZone, PCB_LAYER_ID aLayer, PCB_LA
buildDifferentNetZoneClearances( aZone, aLayer, zoneClearances );
if( zoneClearances.OutlineCount() > 0 )
{
aFillPolys.BooleanSubtract( zoneClearances );
knockoutsApplied = true;
}
}
/* -------------------------------------------------------------------------------------
* Lastly give any same-net but higher-priority zones control over their own area.
*/
double preSubtractArea = aFillPolys.Area();
subtractHigherPriorityZones( aZone, aLayer, aFillPolys );
if( aFillPolys.Area() < preSubtractArea )
knockoutsApplied = true;
DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In18_Cu, wxT( "minus-higher-priority-zones" ) );
if( knockoutsApplied )
postKnockoutMinWidthPrune( aZone, aFillPolys );
DUMP_POLYS_TO_COPPER_LAYER( aFillPolys, In19_Cu, wxT( "after-post-knockout-min-width" ) );
aFillPolys.Fracture();
return true;
}
@@ -3889,6 +3943,8 @@ bool ZONE_FILLER::refillZoneFromCache( ZONE* aZone, PCB_LAYER_ID aLayer, SHAPE_P
subtractKeepout( keepout );
}
postKnockoutMinWidthPrune( aZone, aFillPolys );
aFillPolys.Fracture();
return true;
+7
View File
@@ -162,6 +162,13 @@ private:
SHAPE_POLY_SET& aFillPolys,
const SHAPE_POLY_SET& aThermalRings );
/**
* Remove minimum-width violations introduced by zone-to-zone knockouts.
* Runs a deflate/reconnect/inflate cycle and intersects with the pre-deflate boundary
* to avoid re-inflating into cleared areas.
*/
void postKnockoutMinWidthPrune( const ZONE* aZone, SHAPE_POLY_SET& aFillPolys );
/**
* Refill a zone from cached pre-knockout fill.
* Used during iterative refill to avoid recomputing thermal reliefs and copper clearances.
@@ -0,0 +1,116 @@
(kicad_pcb
(version 20260206)
(generator "pcbnew")
(generator_version "10.0")
(general
(thickness 1.6)
(legacy_teardrops no)
)
(paper "A4")
(layers
(0 "F.Cu" signal)
(2 "B.Cu" signal)
(25 "Edge.Cuts" user)
)
(setup
(pad_to_mask_clearance 0)
(allow_soldermask_bridges_in_footprints no)
(tenting (front yes) (back yes))
(covering (front no) (back no))
(plugging (front no) (back no))
(capping no)
(filling no)
(pcbplotparams
(layerselection 0x00000000_00000000_00000000_00000000)
(plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000)
(disableapertmacros no)
(usegerberextensions no)
(usegerberattributes yes)
(usegerberadvancedattributes yes)
(creategerberjobfile yes)
(dashed_line_dash_ratio 12)
(dashed_line_gap_ratio 3)
(svgprecision 6)
(plotframeref no)
(mode 1)
(useauxorigin no)
(pdf_front_fp_property_popups yes)
(pdf_back_fp_property_popups yes)
(pdf_metadata yes)
(pdf_single_document no)
(dxfpolygonmode yes)
(dxfimperialunits yes)
(dxfusepcbnewfont yes)
(psnegative no)
(psa4output no)
(plot_black_and_white yes)
(sketchpadsonfab no)
(plotpadnumbers no)
(hidednponfab no)
(sketchdnponfab yes)
(crossoutdnponfab yes)
(subtractmaskfromsilk no)
(outputformat 1)
(mirror no)
(drillshape 0)
(scaleselection 1)
(outputdirectory "")
)
)
(net 0 "")
(net 1 "GND")
(net 2 "VCC")
(gr_rect
(start 90 40)
(end 140 90)
(layer "Edge.Cuts")
(uuid "aaaaaaaa-0000-0000-0000-000000000001")
(stroke
(width 0.05)
(type default)
)
)
(zone
(net "GND")
(layer "F.Cu")
(uuid "bbbbbbbb-0000-0000-0000-000000000001")
(hatch edge 0.5)
(priority 0)
(connect_pads
(clearance 0.2)
)
(min_thickness 0.5)
(fill yes
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
(island_removal_mode 0)
)
(polygon
(pts
(xy 100 55) (xy 130 55) (xy 130 85) (xy 100 85)
)
)
)
(zone
(net "VCC")
(layer "F.Cu")
(uuid "cccccccc-0000-0000-0000-000000000002")
(hatch edge 0.5)
(priority 1)
(connect_pads
(clearance 0.2)
)
(min_thickness 0.25)
(fill yes
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
(island_removal_mode 0)
)
(polygon
(pts
(xy 100.25 55) (xy 130 55) (xy 130 85) (xy 100.25 85)
)
)
)
(embedded_fonts no)
)
+70
View File
@@ -1420,3 +1420,73 @@ BOOST_FIXTURE_TEST_CASE( ZoneLayerSpecificRules, ZONE_FILL_TEST_FIXTURE )
BOOST_CHECK_EQUAL( violations.size(), 0 );
}
BOOST_FIXTURE_TEST_CASE( RegressionZoneFillMinWidthAfterKnockout, ZONE_FILL_TEST_FIXTURE )
{
KI_TEST::LoadBoard( m_settingsManager, "issue23332_min_width/issue23332_min_width", m_board );
KI_TEST::FillZones( m_board.get() );
int epsilon = pcbIUScale.mmToIU( 0.001 );
for( ZONE* zone : m_board->Zones() )
{
int half_min_width = zone->GetMinThickness() / 2;
if( half_min_width - epsilon <= epsilon )
continue;
for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
{
if( !zone->HasFilledPolysForLayer( layer ) )
continue;
std::shared_ptr<SHAPE_POLY_SET> fill = zone->GetFilledPolysList( layer );
if( !fill || fill->OutlineCount() == 0 )
continue;
// Check each filled island individually so that a tiny thin sliver
// isn't masked by a large zone's total area
for( int ii = 0; ii < fill->OutlineCount(); ii++ )
{
SHAPE_POLY_SET island;
island.AddOutline( fill->Outline( ii ) );
for( int jj = 0; jj < fill->HoleCount( ii ); jj++ )
island.AddHole( fill->Hole( ii, jj ) );
double originalArea = island.Area();
if( originalArea <= 0 )
continue;
SHAPE_POLY_SET test = island.CloneDropTriangulation();
test.Deflate( half_min_width - epsilon, CORNER_STRATEGY::CHAMFER_ALL_CORNERS,
ARC_HIGH_DEF );
test.Inflate( half_min_width - epsilon, CORNER_STRATEGY::ROUND_ALL_CORNERS,
ARC_HIGH_DEF, true );
double prunedArea = test.Area();
double areaLoss = ( originalArea - prunedArea ) / originalArea;
BOOST_TEST_MESSAGE( wxString::Format(
"Zone %s layer %d island %d: area=%.0f, loss=%.4f%%",
zone->GetNetname(), static_cast<int>( layer ), ii,
originalArea, areaLoss * 100.0 ) );
BOOST_CHECK_MESSAGE( areaLoss < 0.01,
wxString::Format(
"Zone %s layer %d island %d lost %.2f%% area from "
"min-width pruning (min_width=%.3fmm)",
zone->GetNetname(), static_cast<int>( layer ), ii,
areaLoss * 100.0,
zone->GetMinThickness()
/ static_cast<double>( pcbIUScale.IU_PER_MM ) ) );
}
}
}
}