Fix false annular ring width DRC test failure.

The DRC annular ring width test failed to take into account that a pad
could be contained inside another pad having the same number (thermal
vias for example) which changes the effective annular width of the pad
contained within another pad.  A test was added to calculate the effective
annular ring width in this case.

Added some PNS log viewer helper and test code to the PNS playground QA
utility for testing the effective pad annular width code.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/17485
This commit is contained in:
Wayne Stambaugh 2024-05-06 18:31:08 -04:00
parent 59e34dcbec
commit a508f2e716
6 changed files with 388 additions and 15 deletions

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2004-2022 KiCad Developers.
* Copyright (C) 2004-2022, 2024 KiCad Developers.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -44,6 +44,108 @@
- pad stack support (different IAR/OAR values depending on layer)
*/
/**
* Find the nearest collision point between two shape line chains.
*
* @note This collision test only tests the shape line chain segments (outline) by setting the
* shape closed status to false.
*
* @param aLhs is the left hand shape line chain to run the collision test on.
* @param aRhs is the right hand shape line chain the run the collision test against \a aLhs.
* @param aClearance is the collision clearance between the two shape line changes.
* @param[out] aDistance is an optional pointer to store the nearest collision distance.
* @param[out] aPt1 is an optional pointer to store the nearest collision point.
* @retrun true if a collision occurs between \a aLhs and \a aRhs otherwise false.
*/
static inline bool collide( const SHAPE_LINE_CHAIN& aLhs, const SHAPE_LINE_CHAIN& aRhs,
int aClearance, int* aDistance = nullptr, VECTOR2I* aPt1 = nullptr )
{
wxCHECK( aLhs.PointCount() && aRhs.PointCount(), false );
VECTOR2I pt1;
bool retv = false;
int dist = std::numeric_limits<int>::max();
int tmp = dist;
SHAPE_LINE_CHAIN lhs( aLhs );
SHAPE_LINE_CHAIN rhs( aRhs );
lhs.SetClosed( false );
lhs.Append( lhs.CPoint( 0 ) );
rhs.SetClosed( false );
rhs.Append( rhs.CPoint( 0 ) );
for( int i = 0; i < rhs.SegmentCount(); i ++ )
{
if( lhs.Collide( rhs.CSegment( i ), tmp, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
return retv;
}
static bool collide( const SHAPE_POLY_SET& aLhs, const SHAPE_LINE_CHAIN& aRhs, int aClearance,
int* aDistance = nullptr, VECTOR2I* aPt1 = nullptr )
{
VECTOR2I pt1;
bool retv = false;
int tmp = std::numeric_limits<int>::max();
int dist = tmp;
for( int i = 0; i < aLhs.OutlineCount(); i++ )
{
if( collide( aLhs.Outline( i ), aRhs, aClearance, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
{
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
for( int j = 0; j < aLhs.HoleCount( i ); i++ )
{
if( collide( aLhs.CHole( i, j ), aRhs, aClearance, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
{
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
}
}
return retv;
}
class DRC_TEST_PROVIDER_ANNULAR_WIDTH : public DRC_TEST_PROVIDER
{
public:
@ -159,13 +261,25 @@ bool DRC_TEST_PROVIDER_ANNULAR_WIDTH::Run()
if( !pad->HasHole() || pad->GetAttribute() != PAD_ATTRIB::PTH )
return true;
std::vector<const PAD*> sameNumPads;
const FOOTPRINT* fp = static_cast<const FOOTPRINT*>( pad->GetParent() );
if( fp )
sameNumPads = fp->GetPads( pad->GetNumber(), pad );
if( pad->GetOffset() == VECTOR2I( 0, 0 ) )
{
switch( pad->GetShape() )
{
case PAD_SHAPE::CIRCLE:
annularWidth = ( pad->GetSizeX() - pad->GetDrillSizeX() ) / 2;
handled = true;
// If there are more pads with the same number. Check to see if the
// pad is embedded inside another pad with the same number below.
if( sameNumPads.empty() )
handled = true;
break;
case PAD_SHAPE::CHAMFERED_RECT:
@ -179,7 +293,12 @@ bool DRC_TEST_PROVIDER_ANNULAR_WIDTH::Run()
case PAD_SHAPE::ROUNDRECT:
annularWidth = std::min( pad->GetSizeX() - pad->GetDrillSizeX(),
pad->GetSizeY() - pad->GetDrillSizeY() ) / 2;
handled = true;
// If there are more pads with the same number. Check to see if the
// pad is embedded inside another pad with the same number below.
if( sameNumPads.empty() )
handled = true;
break;
default:
@ -190,25 +309,79 @@ bool DRC_TEST_PROVIDER_ANNULAR_WIDTH::Run()
if( !handled )
{
// Slow (but general purpose) method.
SEG::ecoord dist_sq;
SHAPE_POLY_SET padOutline;
std::shared_ptr<SHAPE_SEGMENT> slot = pad->GetEffectiveHoleShape();
pad->TransformShapeToPolygon( padOutline, UNDEFINED_LAYER, 0, maxError,
ERROR_INSIDE );
if( !padOutline.Collide( pad->GetPosition() ) )
if( sameNumPads.empty() )
{
// Hole outside pad
annularWidth = 0;
if( !padOutline.Collide( pad->GetPosition() ) )
{
// Hole outside pad
annularWidth = 0;
}
else
{
// Disable is-inside test in SquaredDistance
padOutline.Outline( 0 ).SetClosed( false );
dist_sq = padOutline.SquaredDistanceToSeg( slot->GetSeg() );
annularWidth = sqrt( dist_sq ) - slot->GetWidth() / 2;
}
}
else
{
std::shared_ptr<SHAPE_SEGMENT> slot = pad->GetEffectiveHoleShape();
SHAPE_POLY_SET otherPadOutline;
SHAPE_POLY_SET slotPolygon;
// Disable is-inside test in SquaredDistance
padOutline.Outline( 0 ).SetClosed( false );
slot->TransformToPolygon( slotPolygon, 0, ERROR_INSIDE );
SEG::ecoord dist_sq = padOutline.SquaredDistanceToSeg( slot->GetSeg() );
annularWidth = sqrt( dist_sq ) - slot->GetWidth() / 2;
for( const PAD* sameNumPad : sameNumPads )
{
// Construct the full pad with outline and hole.
sameNumPad->TransformShapeToPolygon( otherPadOutline,
UNDEFINED_LAYER, 0, maxError,
ERROR_OUTSIDE );
sameNumPad->TransformHoleToPolygon( otherPadOutline, 0, maxError,
ERROR_INSIDE );
// If the pad hole under test intersects with another pad outline,
// the annular width calculated above is used.
bool intersects = false;
for( int i = 0; i < otherPadOutline.OutlineCount() && !intersects; i++ )
{
intersects |= slotPolygon.COutline( 0 ).Intersects( otherPadOutline.COutline( i ) );
if( intersects )
continue;
for( int j = 0; j < otherPadOutline.HoleCount( i ) && !intersects; j++ )
{
intersects |= slotPolygon.COutline( 0 ).Intersects( otherPadOutline.CHole( i, j ) );
if( intersects )
continue;
}
}
if( intersects )
continue;
// Determine the effective annular width if the pad hole under
// test lies withing the boundary of another pad outline.
int effectiveWidth = std::numeric_limits<int>::max();
if( collide( otherPadOutline, slotPolygon.Outline( 0 ),
effectiveWidth, &effectiveWidth ) )
{
if( effectiveWidth > annularWidth )
annularWidth = effectiveWidth;
}
}
}
}

View File

@ -1814,6 +1814,22 @@ PAD* FOOTPRINT::GetPad( const VECTOR2I& aPosition, LSET aLayerMask )
}
std::vector<const PAD*> FOOTPRINT::GetPads( const wxString& aPadNumber, const PAD* aIgnore ) const
{
std::vector<const PAD*> retv;
for( const PAD* pad : m_pads )
{
if( aIgnore && aIgnore == pad )
continue;
retv.push_back( pad );
}
return retv;
}
unsigned FOOTPRINT::GetPadCount( INCLUDE_NPTH_T aIncludeNPTH ) const
{
if( aIncludeNPTH )

View File

@ -783,6 +783,9 @@ public:
*/
PAD* GetPad( const VECTOR2I& aPosition, LSET aLayerMask = LSET::AllLayersMask() );
std::vector<const PAD*> GetPads( const wxString& aPadNumber,
const PAD* aIgnore = nullptr ) const;
/**
* Return the number of pads.
*

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2020 KiCad Developers.
* Copyright (C) 2020, 2024 KiCad Developers.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -33,9 +33,100 @@
#include <geometry/shape_arc.h>
#include <pad.h>
std::shared_ptr<PNS_LOG_VIEWER_OVERLAY> overlay;
static inline bool collide( const SHAPE_LINE_CHAIN& aLhs, const SHAPE_LINE_CHAIN& aRhs,
int aClearance, int* aDistance = nullptr, VECTOR2I* aPt1 = nullptr )
{
wxCHECK( aLhs.PointCount() && aRhs.PointCount(), false );
VECTOR2I pt1;
bool retv = false;
int dist = std::numeric_limits<int>::max();
int tmp = dist;
SHAPE_LINE_CHAIN lhs( aLhs );
SHAPE_LINE_CHAIN rhs( aRhs );
lhs.SetClosed( false );
lhs.Append( lhs.CPoint( 0 ) );
rhs.SetClosed( false );
rhs.Append( rhs.CPoint( 0 ) );
for( int i = 0; i < rhs.SegmentCount(); i ++ )
{
if( lhs.Collide( rhs.CSegment( i ), tmp, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
return retv;
}
static bool collide( const SHAPE_POLY_SET& aLhs, const SHAPE_LINE_CHAIN& aRhs, int aClearance,
int* aDistance = nullptr, VECTOR2I* aPt1 = nullptr )
{
VECTOR2I pt1;
bool retv = false;
int tmp = std::numeric_limits<int>::max();
int dist = tmp;
for( int i = 0; i < aLhs.OutlineCount(); i++ )
{
if( collide( aLhs.Outline( i ), aRhs, aClearance, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
{
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
for( int j = 0; j < aLhs.HoleCount( i ); i++ )
{
if( collide( aLhs.CHole( i, j ), aRhs, aClearance, &tmp, &pt1 ) )
{
retv = true;
if( tmp < dist )
{
dist = tmp;
if( aDistance )
*aDistance = dist;
if( aPt1 )
*aPt1 = pt1;
}
}
}
}
return retv;
}
bool collideArc2Arc( const SHAPE_ARC& a1, const SHAPE_ARC& a2, int clearance, SEG& minDistSeg )
{
SEG mediatrix( a1.GetCenter(), a2.GetCenter() );
@ -175,12 +266,55 @@ int playground_main_func( int argc, char* argv[] )
vp.Merge( arc.BBox() );
}
PAD pad1( nullptr );
pad1.SetX( pcbIUScale.mmToIU( 84.0 ) );
pad1.SetY( pcbIUScale.mmToIU( 66.0 ) );
pad1.SetSizeX( pcbIUScale.mmToIU( 7.0 ) );
pad1.SetSizeY( pcbIUScale.mmToIU( 7.0 ) );
pad1.SetDrillSizeX( pcbIUScale.mmToIU( 3.5 ) );
pad1.SetDrillSizeY( pcbIUScale.mmToIU( 3.5 ) );
vp.Merge( pad1.GetBoundingBox() );
PAD pad2( nullptr );
pad2.SetX( pcbIUScale.mmToIU( 87.125 ) );
pad2.SetY( pcbIUScale.mmToIU( 66.0 ) );
pad2.SetSizeX( pcbIUScale.mmToIU( 0.8 ) );
pad2.SetSizeY( pcbIUScale.mmToIU( 0.8 ) );
pad2.SetDrillSizeX( pcbIUScale.mmToIU( 0.5 ) );
pad2.SetDrillSizeY( pcbIUScale.mmToIU( 0.5 ) );
vp.Merge( pad2.GetBoundingBox() );
SHAPE_POLY_SET pad1Outline;
SHAPE_POLY_SET pad1Hole;
pad1.TransformShapeToPolygon( pad1Outline, UNDEFINED_LAYER, 0, 4, ERROR_OUTSIDE );
pad1.TransformHoleToPolygon( pad1Hole, 0, 4, ERROR_INSIDE );
pad1Outline.AddHole( pad1Hole.Outline( 0 ), 0 );
SHAPE_POLY_SET pad2Outline;
// pad2.TransformShapeToPolygon( pad2Outline, UNDEFINED_LAYER, 0, 4, ERROR_OUTSIDE );
pad2.TransformHoleToPolygon( pad2Outline, 0, 4, ERROR_INSIDE );
SHAPE_POLY_SET xorPad1ToPad2 = pad1Outline;
xorPad1ToPad2.BooleanXor( pad2Outline, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
xorPad1ToPad2.Move( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
SHAPE_POLY_SET andPad1ToPad2 = pad2Outline;
andPad1ToPad2.BooleanIntersection( pad1Outline, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
andPad1ToPad2.Move( VECTOR2I( pcbIUScale.mmToIU( 20 ), 0 ) );
std::shared_ptr<SHAPE_SEGMENT> slot = pad2.GetEffectiveHoleShape();
printf("Read %zu arcs\n", arcs.size() );
LABEL_MANAGER labelMgr( frame->GetPanel()->GetGAL() );
frame->GetPanel()->GetView()->SetViewport( BOX2D( vp.GetOrigin(), vp.GetSize() ) );
for(int i = 0; i < arcs.size(); i+= 2)
for( int i = 0; i < arcs.size(); i+= 2 )
{
SEG closestDist;
std::vector<VECTOR2I> ips;
@ -220,6 +354,35 @@ int playground_main_func( int argc, char* argv[] )
overlay->Arc( arcs[i + 1] );
}
overlay->SetLineWidth( 2000 );
overlay->SetStrokeColor( CYAN );
overlay->AnnotatedPolyset( pad1Outline, "Raw Pads" );
overlay->SetStrokeColor( RED );
overlay->AnnotatedPolyset( pad2Outline );
overlay->SetStrokeColor( CYAN );
overlay->AnnotatedPolyset( xorPad1ToPad2, "XOR Pads" );
overlay->AnnotatedPolyset( andPad1ToPad2, "AND Pads" );
wxLogDebug( wxS( "Pad 1 has %d outlines." ),
pad1Outline.OutlineCount() );
wxLogDebug( wxS( "Pad 2 has %d outlines." ),
pad2Outline.OutlineCount() );
VECTOR2I pt1, pt2;
int dist = std::numeric_limits<int>::max();
collide( pad1Outline, pad2Outline.Outline( 0 ), dist, &dist, &pt1 );
wxLogDebug( wxS( "Nearest distance between pad 1 and pad 2 is %0.6f mm at X=%0.6f mm, "
"Y=%0.6f mm." ),
pcbIUScale.IUTomm( dist ), pcbIUScale.IUTomm( pt1.x ),
pcbIUScale.IUTomm( pt1.y ) );
overlay->SetStrokeColor( YELLOW );
overlay->SetGlyphSize( { 100000, 100000 } );
overlay->BitmapText( wxString::Format( "dist=%0.3f mm", pcbIUScale.IUTomm( dist ) ),
pt1 + VECTOR2I( 0, -56000 ), ANGLE_HORIZONTAL );
overlay = nullptr;

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2020-2022 KiCad Developers.
* Copyright (C) 2020-2022, 2024 KiCad Developers.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -65,6 +65,22 @@ PNS_LOG_VIEWER_OVERLAY::PNS_LOG_VIEWER_OVERLAY( KIGFX::GAL* aGal )
}
void PNS_LOG_VIEWER_OVERLAY::AnnotatedPolyset( const SHAPE_POLY_SET& aPolyset, std::string aName,
bool aShowVertexNumbers )
{
for( int i = 0; i < aPolyset.OutlineCount(); i++ )
{
if( i == 0 && !aName.empty() )
AnnotatedPolyline( aPolyset.COutline( i ), aName );
else
AnnotatedPolyline( aPolyset.COutline( i ), "" );
for( int j = 0; j < aPolyset.HoleCount( i ); j++ )
AnnotatedPolyline( aPolyset.CHole( i, j ), "" );
}
}
void PNS_LOG_VIEWER_OVERLAY::AnnotatedPolyline( const SHAPE_LINE_CHAIN& aL, std::string name,
bool aShowVertexNumbers )
{

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2020 KiCad Developers.
* Copyright (C) 2020, 2024 KiCad Developers.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -108,6 +108,8 @@ class PNS_LOG_VIEWER_OVERLAY : public KIGFX::VIEW_OVERLAY
{
public:
PNS_LOG_VIEWER_OVERLAY( KIGFX::GAL* aGal );
void AnnotatedPolyset( const SHAPE_POLY_SET& aL, std::string name = "",
bool aShowVertexNumbers = false );
void AnnotatedPolyline( const SHAPE_LINE_CHAIN& aL, std::string name,
bool aShowVertexNumbers = false );
void AnnotatedPoint( const VECTOR2I p, int size, std::string name = "",