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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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 ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user