kicad/pcbnew/drc/drc_test_provider_solder_ma...

754 lines
26 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2004-2022 KiCad Developers.
*
* 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 <common.h>
#include <board_design_settings.h>
#include <board_connected_item.h>
#include <footprint.h>
#include <pad.h>
#include <pcb_track.h>
#include <zone.h>
#include <geometry/seg.h>
#include <drc/drc_engine.h>
#include <drc/drc_item.h>
#include <drc/drc_rule.h>
#include <drc/drc_test_provider_clearance_base.h>
#include <drc/drc_rtree.h>
/*
Solder mask tests. Checks for silkscreen which is clipped by mask openings and for bridges
between mask apertures with different nets.
Errors generated:
- DRCE_SILK_CLEARANCE
- DRCE_SOLDERMASK_BRIDGE
*/
class DRC_TEST_PROVIDER_SOLDER_MASK : public ::DRC_TEST_PROVIDER
{
public:
DRC_TEST_PROVIDER_SOLDER_MASK ():
m_board( nullptr ),
m_webWidth( 0 ),
m_maxError( 0 ),
m_largestClearance( 0 )
{
m_bridgeRule.m_Name = _( "board setup solder mask min width" );
}
virtual ~DRC_TEST_PROVIDER_SOLDER_MASK()
{
}
virtual bool Run() override;
virtual const wxString GetName() const override
{
return wxT( "solder_mask_issues" );
};
virtual const wxString GetDescription() const override
{
return wxT( "Tests for silkscreen being clipped by solder mask and copper being exposed "
"by mask apertures of other nets" );
}
private:
void addItemToRTrees( BOARD_ITEM* aItem );
void buildRTrees();
void testSilkToMaskClearance();
void testMaskBridges();
void testItemAgainstItems( BOARD_ITEM* aItem, const BOX2I& aItemBBox,
PCB_LAYER_ID aRefLayer, PCB_LAYER_ID aTargetLayer );
void testMaskItemAgainstZones( BOARD_ITEM* item, const BOX2I& itemBBox,
PCB_LAYER_ID refLayer, PCB_LAYER_ID targetLayer );
bool checkMaskAperture( BOARD_ITEM* aMaskItem, BOARD_ITEM* aTestItem, PCB_LAYER_ID aTestLayer,
int aTestNet, BOARD_ITEM** aCollidingItem );
bool checkItemMask( BOARD_ITEM* aMaskItem, int aTestNet );
private:
DRC_RULE m_bridgeRule;
BOARD* m_board;
int m_webWidth;
int m_maxError;
int m_largestClearance;
std::unique_ptr<DRC_RTREE> m_fullSolderMaskRTree;
std::unique_ptr<DRC_RTREE> m_itemTree;
std::unordered_map<PTR_PTR_CACHE_KEY, LSET> m_checkedPairs;
// Shapes used to define solder mask apertures don't have nets, so we assign them the
// first object+net that bridges their aperture (after which any other nets will generate
// violations).
std::unordered_map<PTR_LAYER_CACHE_KEY, std::pair<BOARD_ITEM*, int>> m_maskApertureNetMap;
};
void DRC_TEST_PROVIDER_SOLDER_MASK::addItemToRTrees( BOARD_ITEM* aItem )
{
ZONE* solderMask = m_board->m_SolderMask;
if( aItem->Type() == PCB_ZONE_T || aItem->Type() == PCB_FP_ZONE_T )
{
ZONE* zone = static_cast<ZONE*>( aItem );
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
{
if( zone->IsOnLayer( layer ) )
{
solderMask->GetFill( layer )->BooleanAdd( *zone->GetFilledPolysList( layer ),
SHAPE_POLY_SET::PM_FAST );
}
}
}
else if( aItem->Type() == PCB_PAD_T )
{
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
{
if( aItem->IsOnLayer( layer ) )
{
PAD* pad = static_cast<PAD*>( aItem );
int clearance = ( m_webWidth / 2 ) + pad->GetSolderMaskExpansion();
aItem->TransformShapeToPolygon( *solderMask->GetFill( layer ), layer, clearance,
m_maxError, ERROR_OUTSIDE );
m_itemTree->Insert( aItem, layer, m_largestClearance );
}
}
}
else if( aItem->Type() == PCB_VIA_T )
{
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
{
if( aItem->IsOnLayer( layer ) )
{
PCB_VIA* via = static_cast<PCB_VIA*>( aItem );
int clearance = ( m_webWidth / 2 ) + via->GetSolderMaskExpansion();
via->TransformShapeToPolygon( *solderMask->GetFill( layer ), layer, clearance,
m_maxError, ERROR_OUTSIDE );
m_itemTree->Insert( aItem, layer, m_largestClearance );
}
}
}
else
{
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
{
if( aItem->IsOnLayer( layer ) )
{
aItem->TransformShapeToPolygon( *solderMask->GetFill( layer ), layer,
m_webWidth / 2, m_maxError, ERROR_OUTSIDE );
m_itemTree->Insert( aItem, layer, m_largestClearance );
}
}
}
}
void DRC_TEST_PROVIDER_SOLDER_MASK::buildRTrees()
{
ZONE* solderMask = m_board->m_SolderMask;
LSET layers = { 4, F_Mask, B_Mask, F_Cu, B_Cu };
const size_t progressDelta = 500;
int count = 0;
int ii = 0;
solderMask->GetFill( F_Mask )->RemoveAllContours();
solderMask->GetFill( B_Mask )->RemoveAllContours();
m_fullSolderMaskRTree = std::make_unique<DRC_RTREE>();
m_itemTree = std::make_unique<DRC_RTREE>();
forEachGeometryItem( s_allBasicItems, layers,
[&]( BOARD_ITEM* item ) -> bool
{
++count;
return true;
} );
forEachGeometryItem( s_allBasicItems, layers,
[&]( BOARD_ITEM* item ) -> bool
{
if( !reportProgress( ii++, count, progressDelta ) )
return false;
addItemToRTrees( item );
return true;
} );
solderMask->GetFill( F_Mask )->Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
solderMask->GetFill( B_Mask )->Simplify( SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
int numSegs = GetArcToSegmentCount( m_webWidth / 2, m_maxError, FULL_CIRCLE );
solderMask->GetFill( F_Mask )->Deflate( m_webWidth / 2, numSegs );
solderMask->GetFill( B_Mask )->Deflate( m_webWidth / 2, numSegs );
solderMask->SetFillFlag( F_Mask, true );
solderMask->SetFillFlag( B_Mask, true );
solderMask->SetIsFilled( true );
solderMask->CacheTriangulation();
m_fullSolderMaskRTree->Insert( solderMask, F_Mask );
m_fullSolderMaskRTree->Insert( solderMask, B_Mask );
m_checkedPairs.clear();
}
void DRC_TEST_PROVIDER_SOLDER_MASK::testSilkToMaskClearance()
{
LSET silkLayers = { 2, F_SilkS, B_SilkS };
const size_t progressDelta = 250;
int count = 0;
int ii = 0;
forEachGeometryItem( s_allBasicItems, silkLayers,
[&]( BOARD_ITEM* item ) -> bool
{
++count;
return true;
} );
forEachGeometryItem( s_allBasicItems, silkLayers,
[&]( BOARD_ITEM* item ) -> bool
{
if( m_drcEngine->IsErrorLimitExceeded( DRCE_SILK_CLEARANCE ) )
return false;
if( !reportProgress( ii++, count, progressDelta ) )
return false;
if( isInvisibleText( item ) )
return true;
for( PCB_LAYER_ID layer : silkLayers.Seq() )
{
if( !item->IsOnLayer( layer ) )
continue;
BOX2I itemBBox = item->GetBoundingBox();
DRC_CONSTRAINT constraint = m_drcEngine->EvalRules( SILK_CLEARANCE_CONSTRAINT,
item, nullptr, layer );
int clearance = constraint.GetValue().Min();
int actual;
VECTOR2I pos;
if( constraint.GetSeverity() == RPT_SEVERITY_IGNORE || clearance <= 0 )
return true;
std::shared_ptr<SHAPE> itemShape = item->GetEffectiveShape( layer );
if( m_fullSolderMaskRTree->QueryColliding( itemBBox, itemShape.get(), layer,
clearance, &actual, &pos ) )
{
auto drce = DRC_ITEM::Create( DRCE_SILK_CLEARANCE );
wxString msg = formatMsg( _( "(%s clearance %s; actual %s)" ),
constraint.GetName(),
clearance,
actual );
drce->SetErrorMessage( drce->GetErrorText() + wxS( " " ) + msg );
drce->SetItems( item );
drce->SetViolatingRule( constraint.GetParentRule() );
reportViolation( drce, pos, layer );
}
}
return true;
} );
}
bool isNullAperture( BOARD_ITEM* aItem )
{
if( aItem->Type() == PCB_PAD_T )
{
PAD* pad = static_cast<PAD*>( aItem );
if( pad->GetAttribute() == PAD_ATTRIB::NPTH
&& ( pad->GetShape() == PAD_SHAPE::CIRCLE || pad->GetShape() == PAD_SHAPE::OVAL )
&& pad->GetSize().x <= pad->GetDrillSize().x
&& pad->GetSize().y <= pad->GetDrillSize().y )
{
return true;
}
}
return false;
}
// Simple mask apertures aren't associated with copper items, so they only constitute a bridge
// when they expose other copper items having at least two distinct nets. We use a map to record
// the first net exposed by each mask aperture (on each copper layer).
//
// Note that this algorithm is also used for free pads.
bool isMaskAperture( BOARD_ITEM* aItem )
{
if( aItem->Type() == PCB_PAD_T && static_cast<PAD*>( aItem )->IsFreePad() )
return true;
static const LSET saved( 2, F_Mask, B_Mask );
LSET maskLayers = aItem->GetLayerSet() & saved;
LSET otherLayers = aItem->GetLayerSet() & ~saved;
return maskLayers.count() > 0 && otherLayers.count() == 0;
}
bool DRC_TEST_PROVIDER_SOLDER_MASK::checkMaskAperture( BOARD_ITEM* aMaskItem, BOARD_ITEM* aTestItem,
PCB_LAYER_ID aTestLayer, int aTestNet,
BOARD_ITEM** aCollidingItem )
{
if( !IsCopperLayer( aTestLayer ) )
return false;
FOOTPRINT* fp = static_cast<FOOTPRINT*>( aMaskItem->GetParentFootprint() );
if( fp && ( fp->GetAttributes() & FP_ALLOW_SOLDERMASK_BRIDGES ) > 0 )
{
// Mask apertures in footprints which allow soldermask bridges are ignored entirely.
return false;
}
PTR_LAYER_CACHE_KEY key = { aMaskItem, aTestLayer };
auto ii = m_maskApertureNetMap.find( key );
if( ii == m_maskApertureNetMap.end() )
{
m_maskApertureNetMap[ key ] = { aTestItem, aTestNet };
// First net; no bridge yet....
return false;
}
if( ii->second.second == aTestNet && aTestNet > 0 )
{
// Same net; still no bridge...
return false;
}
*aCollidingItem = ii->second.first;
return true;
}
bool DRC_TEST_PROVIDER_SOLDER_MASK::checkItemMask( BOARD_ITEM* aMaskItem, int aTestNet )
{
FOOTPRINT* fp = static_cast<FOOTPRINT*>( aMaskItem->GetParentFootprint() );
wxCHECK( fp, false );
if( ( fp->GetAttributes() & FP_ALLOW_SOLDERMASK_BRIDGES ) > 0 )
{
// If we're allowing bridges then we're allowing bridges. Nothing to check.
return false;
}
// Graphic items are used to implement net-ties between pads of a group within a net-tie
// footprint. They must be allowed to intrude into their pad's mask aperture.
if( aTestNet < 0 && aMaskItem->Type() == PCB_PAD_T && fp->IsNetTie() )
{
std::map<wxString, int> padNumberToGroupIdxMap = fp->MapPadNumbersToNetTieGroups();
if( padNumberToGroupIdxMap[ static_cast<PAD*>( aMaskItem )->GetNumber() ] >= 0 )
return false;
}
return true;
}
void DRC_TEST_PROVIDER_SOLDER_MASK::testItemAgainstItems( BOARD_ITEM* aItem, const BOX2I& aItemBBox,
PCB_LAYER_ID aRefLayer,
PCB_LAYER_ID aTargetLayer )
{
int itemNet = -1;
if( aItem->IsConnected() )
itemNet = static_cast<BOARD_CONNECTED_ITEM*>( aItem )->GetNetCode();
BOARD_DESIGN_SETTINGS& bds = aItem->GetBoard()->GetDesignSettings();
PAD* pad = dynamic_cast<PAD*>( aItem );
PCB_VIA* via = dynamic_cast<PCB_VIA*>( aItem );
std::shared_ptr<SHAPE> itemShape = aItem->GetEffectiveShape( aRefLayer );
m_itemTree->QueryColliding( aItem, aRefLayer, aTargetLayer,
// Filter:
[&]( BOARD_ITEM* other ) -> bool
{
FOOTPRINT* itemFP = static_cast<FOOTPRINT*>( aItem->GetParentFootprint() );
PAD* otherPad = dynamic_cast<PAD*>( other );
int otherNet = -1;
if( other->IsConnected() )
otherNet = static_cast<BOARD_CONNECTED_ITEM*>( other )->GetNetCode();
if( otherNet > 0 && otherNet == itemNet )
return false;
if( isNullAperture( other ) )
return false;
if( itemFP && itemFP == other->GetParentFootprint() )
{
// Board-wide exclusion
if( bds.m_AllowSoldermaskBridgesInFPs )
return false;
// Footprint-specific exclusion
if( ( itemFP->GetAttributes() & FP_ALLOW_SOLDERMASK_BRIDGES ) > 0 )
return false;
}
if( pad && otherPad && pad->SameLogicalPadAs( otherPad ) )
{
return false;
}
BOARD_ITEM* a = aItem;
BOARD_ITEM* b = other;
// store canonical order so we don't collide in both directions (a:b and b:a)
if( static_cast<void*>( a ) > static_cast<void*>( b ) )
std::swap( a, b );
auto it = m_checkedPairs.find( { a, b } );
if( it != m_checkedPairs.end() && it->second.test( aTargetLayer ) )
{
return false;
}
else
{
m_checkedPairs[ { a, b } ].set( aTargetLayer );
return true;
}
},
// Visitor:
[&]( BOARD_ITEM* other ) -> bool
{
PAD* otherPad = dynamic_cast<PAD*>( other );
PCB_VIA* otherVia = dynamic_cast<PCB_VIA*>( other );
auto otherShape = other->GetEffectiveShape( aTargetLayer );
int otherNet = -1;
if( other->IsConnected() )
otherNet = static_cast<BOARD_CONNECTED_ITEM*>( other )->GetNetCode();
int actual;
VECTOR2I pos;
int clearance = 0;
if( aRefLayer == F_Mask || aRefLayer == B_Mask )
{
// Aperture-to-aperture must enforce web-min-width
clearance = m_webWidth;
}
else // ( aRefLayer == F_Cu || aRefLayer == B_Cu )
{
// Copper-to-aperture uses the solder-mask-to-copper-clearance
clearance = m_board->GetDesignSettings().m_SolderMaskToCopperClearance;
}
if( pad )
clearance += pad->GetSolderMaskExpansion();
else if( via )
clearance += via->GetSolderMaskExpansion();
if( otherPad )
clearance += otherPad->GetSolderMaskExpansion();
else if( otherVia )
clearance += otherVia->GetSolderMaskExpansion();
if( itemShape->Collide( otherShape.get(), clearance, &actual, &pos ) )
{
wxString msg;
BOARD_ITEM* colliding = nullptr;
if( aTargetLayer == F_Mask )
msg = _( "Front solder mask aperture bridges items with different nets" );
else
msg = _( "Rear solder mask aperture bridges items with different nets" );
// Simple mask apertures aren't associated with copper items, so they only
// constitute a bridge when they expose other copper items having at least
// two distinct nets.
if( isMaskAperture( aItem ) )
{
if( checkMaskAperture( aItem, other, aRefLayer, otherNet, &colliding ) )
{
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
drce->SetErrorMessage( msg );
drce->SetItems( aItem, colliding, other );
drce->SetViolatingRule( &m_bridgeRule );
reportViolation( drce, pos, aTargetLayer );
}
}
else if( isMaskAperture( other ) )
{
if( checkMaskAperture( other, aItem, aRefLayer, itemNet, &colliding ) )
{
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
drce->SetErrorMessage( msg );
drce->SetItems( other, colliding, aItem );
drce->SetViolatingRule( &m_bridgeRule );
reportViolation( drce, pos, aTargetLayer );
}
}
else if( checkItemMask( other, itemNet ) )
{
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
drce->SetErrorMessage( msg );
drce->SetItems( aItem, other );
drce->SetViolatingRule( &m_bridgeRule );
reportViolation( drce, pos, aTargetLayer );
}
}
return !m_drcEngine->IsCancelled();
},
m_largestClearance );
}
void DRC_TEST_PROVIDER_SOLDER_MASK::testMaskItemAgainstZones( BOARD_ITEM* aItem,
const BOX2I& aItemBBox,
PCB_LAYER_ID aMaskLayer,
PCB_LAYER_ID aTargetLayer )
{
for( ZONE* zone : m_board->m_DRCCopperZones )
{
if( !zone->GetLayerSet().test( aTargetLayer ) )
continue;
int zoneNet = zone->GetNetCode();
if( aItem->IsConnected() )
{
BOARD_CONNECTED_ITEM* connectedItem = static_cast<BOARD_CONNECTED_ITEM*>( aItem );
if( zoneNet == connectedItem->GetNetCode() && zoneNet > 0 )
continue;
}
BOX2I inflatedBBox( aItemBBox );
int clearance = m_board->GetDesignSettings().m_SolderMaskToCopperClearance;
if( aItem->Type() == PCB_PAD_T )
clearance += static_cast<PAD*>( aItem )->GetSolderMaskExpansion();
else if( aItem->Type() == PCB_VIA_T )
clearance += static_cast<PCB_VIA*>( aItem )->GetSolderMaskExpansion();
inflatedBBox.Inflate( clearance );
if( !inflatedBBox.Intersects( zone->GetBoundingBox() ) )
continue;
DRC_RTREE* zoneTree = m_board->m_CopperZoneRTreeCache[ zone ].get();
int actual;
VECTOR2I pos;
std::shared_ptr<SHAPE> itemShape = aItem->GetEffectiveShape( aMaskLayer );
if( zoneTree && zoneTree->QueryColliding( aItemBBox, itemShape.get(), aTargetLayer,
clearance, &actual, &pos ) )
{
wxString msg;
BOARD_ITEM* colliding = nullptr;
if( aMaskLayer == F_Mask )
msg = _( "Front solder mask aperture bridges items with different nets" );
else
msg = _( "Rear solder mask aperture bridges items with different nets" );
// Simple mask apertures aren't associated with copper items, so they only constitute
// a bridge when they expose other copper items having at least two distinct nets.
if( isMaskAperture( aItem ) && zoneNet >= 0 )
{
if( checkMaskAperture( aItem, zone, aTargetLayer, zoneNet, &colliding ) )
{
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
drce->SetErrorMessage( msg );
drce->SetItems( aItem, colliding, zone );
drce->SetViolatingRule( &m_bridgeRule );
reportViolation( drce, pos, aTargetLayer );
}
}
else
{
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
drce->SetErrorMessage( msg );
drce->SetItems( aItem, zone );
drce->SetViolatingRule( &m_bridgeRule );
reportViolation( drce, pos, aTargetLayer );
}
}
if( m_drcEngine->IsCancelled() )
return;
}
}
void DRC_TEST_PROVIDER_SOLDER_MASK::testMaskBridges()
{
LSET copperAndMaskLayers = { 4, F_Mask, B_Mask, F_Cu, B_Cu };
const size_t progressDelta = 250;
int count = 0;
int ii = 0;
forEachGeometryItem( s_allBasicItemsButZones, copperAndMaskLayers,
[&]( BOARD_ITEM* item ) -> bool
{
++count;
return true;
} );
forEachGeometryItem( s_allBasicItemsButZones, copperAndMaskLayers,
[&]( BOARD_ITEM* item ) -> bool
{
if( m_drcEngine->IsErrorLimitExceeded( DRCE_SOLDERMASK_BRIDGE ) )
return false;
if( !reportProgress( ii++, count, progressDelta ) )
return false;
BOX2I itemBBox = item->GetBoundingBox();
if( item->IsOnLayer( F_Mask ) && !isNullAperture( item ) )
{
// Test for aperture-to-aperture collisions
testItemAgainstItems( item, itemBBox, F_Mask, F_Mask );
// Test for aperture-to-zone collisions
testMaskItemAgainstZones( item, itemBBox, F_Mask, F_Cu );
}
else if( item->IsOnLayer( F_Cu ) )
{
// Test for copper-item-to-aperture collisions
testItemAgainstItems( item, itemBBox, F_Cu, F_Mask );
}
if( item->IsOnLayer( B_Mask ) && !isNullAperture( item ) )
{
// Test for aperture-to-aperture collisions
testItemAgainstItems( item, itemBBox, B_Mask, B_Mask );
// Test for aperture-to-zone collisions
testMaskItemAgainstZones( item, itemBBox, B_Mask, B_Cu );
}
else if( item->IsOnLayer( B_Cu ) )
{
// Test for copper-item-to-aperture collisions
testItemAgainstItems( item, itemBBox, B_Cu, B_Mask );
}
return true;
} );
}
bool DRC_TEST_PROVIDER_SOLDER_MASK::Run()
{
if( m_drcEngine->IsErrorLimitExceeded( DRCE_SILK_CLEARANCE )
&& m_drcEngine->IsErrorLimitExceeded( DRCE_SOLDERMASK_BRIDGE ) )
{
reportAux( wxT( "Solder mask violations ignored. Tests not run." ) );
return true; // continue with other tests
}
m_board = m_drcEngine->GetBoard();
m_webWidth = m_board->GetDesignSettings().m_SolderMaskMinWidth;
m_maxError = m_board->GetDesignSettings().m_MaxError;
m_largestClearance = 0;
for( FOOTPRINT* footprint : m_board->Footprints() )
{
for( PAD* pad : footprint->Pads() )
m_largestClearance = std::max( m_largestClearance, pad->GetSolderMaskExpansion() );
}
// Order is important here: m_webWidth must be added in before m_largestCourtyardClearance is
// maxed with the various SILK_CLEARANCE_CONSTRAINTS.
m_largestClearance += m_largestClearance + m_webWidth;
DRC_CONSTRAINT worstClearanceConstraint;
if( m_drcEngine->QueryWorstConstraint( SILK_CLEARANCE_CONSTRAINT, worstClearanceConstraint ) )
m_largestClearance = std::max( m_largestClearance, worstClearanceConstraint.m_Value.Min() );
reportAux( wxT( "Worst clearance : %d nm" ), m_largestClearance );
if( !reportPhase( _( "Building solder mask..." ) ) )
return false; // DRC cancelled
m_checkedPairs.clear();
m_maskApertureNetMap.clear();
buildRTrees();
if( !reportPhase( _( "Checking solder mask to silk clearance..." ) ) )
return false; // DRC cancelled
testSilkToMaskClearance();
if( !reportPhase( _( "Checking solder mask web integrity..." ) ) )
return false; // DRC cancelled
testMaskBridges();
reportRuleStatistics();
return !m_drcEngine->IsCancelled();
}
namespace detail
{
static DRC_REGISTER_TEST_PROVIDER<DRC_TEST_PROVIDER_SOLDER_MASK> dummy;
}