2021-08-12 16:58:30 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KiCad, a free EDA CAD application.
|
|
|
|
*
|
2022-03-11 21:16:52 +00:00
|
|
|
* Copyright (C) 2004-2022 KiCad Developers.
|
2021-08-12 16:58:30 +00:00
|
|
|
*
|
|
|
|
* 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>
|
2021-08-22 22:05:47 +00:00
|
|
|
#include <pcb_track.h>
|
2021-08-12 16:58:30 +00:00
|
|
|
#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 ():
|
2021-12-30 23:42:06 +00:00
|
|
|
m_board( nullptr ),
|
|
|
|
m_webWidth( 0 ),
|
|
|
|
m_maxError( 0 ),
|
2021-08-12 16:58:30 +00:00
|
|
|
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
|
|
|
|
{
|
2022-03-11 21:16:52 +00:00
|
|
|
return wxT( "solder_mask_issues" );
|
2021-08-12 16:58:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
virtual const wxString GetDescription() const override
|
|
|
|
{
|
2022-03-11 21:16:52 +00:00
|
|
|
return wxT( "Tests for silkscreen being clipped by solder mask and copper being exposed "
|
|
|
|
"by mask apertures of other nets" );
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2022-08-26 12:21:52 +00:00
|
|
|
void addItemToRTrees( BOARD_ITEM* aItem );
|
2021-08-12 16:58:30 +00:00
|
|
|
void buildRTrees();
|
|
|
|
|
|
|
|
void testSilkToMaskClearance();
|
|
|
|
void testMaskBridges();
|
|
|
|
|
|
|
|
void testItemAgainstItems( BOARD_ITEM* aItem, const EDA_RECT& aItemBBox,
|
|
|
|
PCB_LAYER_ID aRefLayer, PCB_LAYER_ID aTargetLayer );
|
|
|
|
void testMaskItemAgainstZones( BOARD_ITEM* item, const EDA_RECT& itemBBox,
|
|
|
|
PCB_LAYER_ID refLayer, PCB_LAYER_ID targetLayer );
|
|
|
|
|
2022-07-23 08:26:04 +00:00
|
|
|
bool checkMaskAperture( BOARD_ITEM* aMaskItem, BOARD_ITEM* aTestItem, PCB_LAYER_ID aTestLayer,
|
|
|
|
int aTestNet, BOARD_ITEM** aCollidingItem );
|
2022-07-22 21:18:44 +00:00
|
|
|
|
2022-08-19 17:34:53 +00:00
|
|
|
bool checkItemMask( BOARD_ITEM* aMaskItem, int aTestNet );
|
|
|
|
|
2021-08-12 16:58:30 +00:00
|
|
|
private:
|
|
|
|
DRC_RULE m_bridgeRule;
|
|
|
|
|
|
|
|
BOARD* m_board;
|
|
|
|
int m_webWidth;
|
|
|
|
int m_maxError;
|
|
|
|
int m_largestClearance;
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
std::unique_ptr<DRC_RTREE> m_fullSolderMaskRTree;
|
2021-08-12 16:58:30 +00:00
|
|
|
std::unique_ptr<DRC_RTREE> m_itemTree;
|
|
|
|
|
2022-08-25 16:00:18 +00:00
|
|
|
std::unordered_map<PTR_PTR_CACHE_KEY, LSET> m_checkedPairs;
|
2022-01-03 20:55:38 +00:00
|
|
|
|
|
|
|
// Shapes used to define solder mask apertures don't have nets, so we assign them the
|
2022-07-22 21:18:44 +00:00
|
|
|
// first object+net that bridges their aperture (after which any other nets will generate
|
2022-01-03 20:55:38 +00:00
|
|
|
// violations).
|
2022-08-22 11:36:41 +00:00
|
|
|
std::unordered_map<PTR_LAYER_CACHE_KEY, std::pair<BOARD_ITEM*, int>> m_maskApertureNetMap;
|
2021-08-12 16:58:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::addItemToRTrees( BOARD_ITEM* aItem )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
ZONE* solderMask = m_board->m_SolderMask;
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
if( aItem->Type() == PCB_ZONE_T || aItem->Type() == PCB_FP_ZONE_T )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
ZONE* zone = static_cast<ZONE*>( aItem );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
|
|
|
|
{
|
|
|
|
if( zone->IsOnLayer( layer ) )
|
|
|
|
{
|
2022-02-15 17:34:38 +00:00
|
|
|
solderMask->GetFill( layer )->BooleanAdd( *zone->GetFilledPolysList( layer ),
|
2021-08-12 16:58:30 +00:00
|
|
|
SHAPE_POLY_SET::PM_FAST );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 12:21:52 +00:00
|
|
|
else if( aItem->Type() == PCB_PAD_T )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
|
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
if( aItem->IsOnLayer( layer ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
PAD* pad = static_cast<PAD*>( aItem );
|
2021-08-22 22:05:47 +00:00
|
|
|
int clearance = ( m_webWidth / 2 ) + pad->GetSolderMaskExpansion();
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
aItem->TransformShapeWithClearanceToPolygon( *solderMask->GetFill( layer ), layer,
|
|
|
|
clearance, m_maxError, ERROR_OUTSIDE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
m_itemTree->Insert( aItem, layer, m_largestClearance );
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-26 12:21:52 +00:00
|
|
|
else if( aItem->Type() == PCB_VIA_T )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2021-08-22 22:05:47 +00:00
|
|
|
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
if( aItem->IsOnLayer( layer ) )
|
2021-08-22 22:05:47 +00:00
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
PCB_VIA* via = static_cast<PCB_VIA*>( aItem );
|
2021-08-22 22:05:47 +00:00
|
|
|
int clearance = ( m_webWidth / 2 ) + via->GetSolderMaskExpansion();
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-02-07 00:24:08 +00:00
|
|
|
via->TransformShapeWithClearanceToPolygon( *solderMask->GetFill( layer ), layer,
|
|
|
|
clearance, m_maxError, ERROR_OUTSIDE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
m_itemTree->Insert( aItem, layer, m_largestClearance );
|
2021-08-22 22:05:47 +00:00
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for( PCB_LAYER_ID layer : { F_Mask, B_Mask } )
|
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
if( aItem->IsOnLayer( layer ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-08-26 12:21:52 +00:00
|
|
|
aItem->TransformShapeWithClearanceToPolygon( *solderMask->GetFill( layer ),
|
|
|
|
layer, m_webWidth / 2, m_maxError,
|
|
|
|
ERROR_OUTSIDE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
m_itemTree->Insert( aItem, layer, m_largestClearance );
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::buildRTrees()
|
|
|
|
{
|
|
|
|
ZONE* solderMask = m_board->m_SolderMask;
|
|
|
|
LSET layers = { 4, F_Mask, B_Mask, F_Cu, B_Cu };
|
|
|
|
|
2022-08-03 09:10:23 +00:00
|
|
|
const size_t progressDelta = 500;
|
|
|
|
int count = 0;
|
|
|
|
int ii = 0;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
solderMask->GetFill( F_Mask )->RemoveAllContours();
|
|
|
|
solderMask->GetFill( B_Mask )->RemoveAllContours();
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
m_fullSolderMaskRTree = std::make_unique<DRC_RTREE>();
|
2021-08-12 16:58:30 +00:00
|
|
|
m_itemTree = std::make_unique<DRC_RTREE>();
|
2022-03-12 15:57:29 +00:00
|
|
|
|
2021-08-12 16:58:30 +00:00
|
|
|
forEachGeometryItem( s_allBasicItems, layers,
|
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
|
|
|
{
|
2022-03-12 13:34:25 +00:00
|
|
|
++count;
|
2021-08-12 16:58:30 +00:00
|
|
|
return true;
|
|
|
|
} );
|
|
|
|
|
|
|
|
forEachGeometryItem( s_allBasicItems, layers,
|
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
|
|
|
{
|
2022-08-03 09:10:23 +00:00
|
|
|
if( !reportProgress( ii++, count, progressDelta ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
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 );
|
|
|
|
|
2022-01-14 15:34:41 +00:00
|
|
|
int numSegs = GetArcToSegmentCount( m_webWidth / 2, m_maxError, FULL_CIRCLE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
m_fullSolderMaskRTree->Insert( solderMask, F_Mask );
|
|
|
|
m_fullSolderMaskRTree->Insert( solderMask, B_Mask );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
m_checkedPairs.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::testSilkToMaskClearance()
|
|
|
|
{
|
|
|
|
LSET silkLayers = { 2, F_SilkS, B_SilkS };
|
|
|
|
|
2022-08-03 09:10:23 +00:00
|
|
|
const size_t progressDelta = 250;
|
|
|
|
int count = 0;
|
|
|
|
int ii = 0;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
forEachGeometryItem( s_allBasicItems, silkLayers,
|
2021-08-22 22:05:47 +00:00
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-03-12 13:34:25 +00:00
|
|
|
++count;
|
2021-08-12 16:58:30 +00:00
|
|
|
return true;
|
|
|
|
} );
|
|
|
|
|
|
|
|
forEachGeometryItem( s_allBasicItems, silkLayers,
|
2021-08-22 22:05:47 +00:00
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
if( m_drcEngine->IsErrorLimitExceeded( DRCE_SILK_CLEARANCE ) )
|
|
|
|
return false;
|
|
|
|
|
2022-08-03 09:10:23 +00:00
|
|
|
if( !reportProgress( ii++, count, progressDelta ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
if( isInvisibleText( item ) )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
for( PCB_LAYER_ID layer : silkLayers.Seq() )
|
|
|
|
{
|
|
|
|
if( !item->IsOnLayer( layer ) )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
EDA_RECT itemBBox = item->GetBoundingBox();
|
|
|
|
DRC_CONSTRAINT constraint = m_drcEngine->EvalRules( SILK_CLEARANCE_CONSTRAINT,
|
|
|
|
item, nullptr, layer );
|
|
|
|
int clearance = constraint.GetValue().Min();
|
|
|
|
int actual;
|
|
|
|
VECTOR2I pos;
|
|
|
|
|
2021-09-05 15:06:12 +00:00
|
|
|
if( constraint.GetSeverity() == RPT_SEVERITY_IGNORE || clearance <= 0 )
|
2021-08-12 16:58:30 +00:00
|
|
|
return true;
|
|
|
|
|
|
|
|
std::shared_ptr<SHAPE> itemShape = item->GetEffectiveShape( layer );
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
if( m_fullSolderMaskRTree->QueryColliding( itemBBox, itemShape.get(), layer,
|
|
|
|
clearance, &actual, &pos ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SILK_CLEARANCE );
|
2022-06-15 23:42:34 +00:00
|
|
|
wxString msg;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-06-15 23:42:34 +00:00
|
|
|
msg.Printf( _( "(%s clearance %s; actual %s)" ),
|
2021-08-12 16:58:30 +00:00
|
|
|
constraint.GetName(),
|
|
|
|
MessageTextFromValue( userUnits(), clearance ),
|
|
|
|
MessageTextFromValue( userUnits(), actual ) );
|
|
|
|
|
2022-06-15 23:42:34 +00:00
|
|
|
drce->SetErrorMessage( drce->GetErrorText() + wxS( " " ) + msg );
|
2021-08-12 16:58:30 +00:00
|
|
|
drce->SetItems( item );
|
|
|
|
drce->SetViolatingRule( constraint.GetParentRule() );
|
|
|
|
|
2022-01-02 02:06:40 +00:00
|
|
|
reportViolation( drce, pos, layer );
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-01-03 20:55:38 +00:00
|
|
|
bool isMaskAperture( BOARD_ITEM* aItem )
|
|
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-05-05 23:05:34 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-07-22 21:18:44 +00:00
|
|
|
// 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).
|
|
|
|
|
2022-07-23 08:26:04 +00:00
|
|
|
bool DRC_TEST_PROVIDER_SOLDER_MASK::checkMaskAperture( BOARD_ITEM* aMaskItem, BOARD_ITEM* aTestItem,
|
|
|
|
PCB_LAYER_ID aTestLayer, int aTestNet,
|
2022-07-22 21:18:44 +00:00
|
|
|
BOARD_ITEM** aCollidingItem )
|
|
|
|
{
|
2022-07-23 08:26:04 +00:00
|
|
|
if( !IsCopperLayer( aTestLayer ) )
|
|
|
|
return false;
|
2022-07-22 21:18:44 +00:00
|
|
|
|
2022-07-23 08:26:04 +00:00
|
|
|
FOOTPRINT* fp = static_cast<FOOTPRINT*>( aMaskItem->GetParentFootprint() );
|
2022-07-22 21:18:44 +00:00
|
|
|
|
2022-07-23 08:26:04 +00:00
|
|
|
if( fp && ( fp->GetAttributes() & FP_ALLOW_SOLDERMASK_BRIDGES ) > 0 )
|
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
// Mask apertures in footprints which allow soldermask bridges are ignored entirely.
|
2022-07-23 08:26:04 +00:00
|
|
|
return false;
|
2022-07-22 21:18:44 +00:00
|
|
|
}
|
|
|
|
|
2022-08-22 11:36:41 +00:00
|
|
|
PTR_LAYER_CACHE_KEY key = { aMaskItem, aTestLayer };
|
2022-07-22 21:18:44 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-08-19 17:34:53 +00:00
|
|
|
bool DRC_TEST_PROVIDER_SOLDER_MASK::checkItemMask( BOARD_ITEM* aMaskItem, int aTestNet )
|
|
|
|
{
|
|
|
|
FOOTPRINT* fp = static_cast<FOOTPRINT*>( aMaskItem->GetParentFootprint() );
|
|
|
|
|
|
|
|
if( fp && ( 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() )
|
|
|
|
{
|
|
|
|
wxString padNumber = static_cast<PAD*>( aMaskItem )->GetNumber();
|
|
|
|
|
|
|
|
for( const wxString& group : fp->GetNetTiePadGroups() )
|
|
|
|
{
|
|
|
|
wxStringTokenizer groupParser( group, "," );
|
|
|
|
|
|
|
|
while( groupParser.HasMoreTokens() )
|
|
|
|
{
|
|
|
|
if( groupParser.GetNextToken().Trim( false ).Trim( true ) == padNumber )
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-08-12 16:58:30 +00:00
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::testItemAgainstItems( BOARD_ITEM* aItem,
|
|
|
|
const EDA_RECT& aItemBBox,
|
|
|
|
PCB_LAYER_ID aRefLayer,
|
|
|
|
PCB_LAYER_ID aTargetLayer )
|
|
|
|
{
|
|
|
|
int itemNet = -1;
|
|
|
|
|
|
|
|
if( aItem->IsConnected() )
|
|
|
|
itemNet = static_cast<BOARD_CONNECTED_ITEM*>( aItem )->GetNetCode();
|
|
|
|
|
2022-08-14 21:53:20 +00:00
|
|
|
BOARD_DESIGN_SETTINGS& bds = aItem->GetBoard()->GetDesignSettings();
|
2021-08-12 16:58:30 +00:00
|
|
|
PAD* pad = dynamic_cast<PAD*>( aItem );
|
2021-08-22 22:05:47 +00:00
|
|
|
PCB_VIA* via = dynamic_cast<PCB_VIA*>( aItem );
|
2021-08-12 16:58:30 +00:00
|
|
|
std::shared_ptr<SHAPE> itemShape = aItem->GetEffectiveShape( aRefLayer );
|
|
|
|
|
|
|
|
m_itemTree->QueryColliding( aItem, aRefLayer, aTargetLayer,
|
|
|
|
// Filter:
|
|
|
|
[&]( BOARD_ITEM* other ) -> bool
|
|
|
|
{
|
2022-07-23 08:26:04 +00:00
|
|
|
FOOTPRINT* itemFP = static_cast<FOOTPRINT*>( aItem->GetParentFootprint() );
|
|
|
|
PAD* otherPad = dynamic_cast<PAD*>( other );
|
|
|
|
int otherNet = -1;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
if( other->IsConnected() )
|
|
|
|
otherNet = static_cast<BOARD_CONNECTED_ITEM*>( other )->GetNetCode();
|
|
|
|
|
|
|
|
if( otherNet > 0 && otherNet == itemNet )
|
|
|
|
return false;
|
|
|
|
|
2022-05-05 23:05:34 +00:00
|
|
|
if( isNullAperture( other ) )
|
|
|
|
return false;
|
|
|
|
|
2022-08-14 21:53:20 +00:00
|
|
|
if( itemFP && itemFP == other->GetParentFootprint() )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-08-14 21:53:20 +00:00
|
|
|
// Board-wide exclusion
|
|
|
|
if( bds.m_AllowSoldermaskBridgesInFPs )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Footprint-specific exclusion
|
|
|
|
if( ( itemFP->GetAttributes() & FP_ALLOW_SOLDERMASK_BRIDGES ) > 0 )
|
|
|
|
return false;
|
2022-02-18 19:46:16 +00:00
|
|
|
}
|
|
|
|
|
2022-07-23 08:26:04 +00:00
|
|
|
if( pad && otherPad && pad->SameLogicalPadAs( otherPad ) )
|
2022-02-18 19:46:16 +00:00
|
|
|
{
|
2022-07-23 08:26:04 +00:00
|
|
|
return false;
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
BOARD_ITEM* a = aItem;
|
|
|
|
BOARD_ITEM* b = other;
|
|
|
|
|
2022-08-25 14:03:44 +00:00
|
|
|
// store canonical order so we don't collide in both directions (a:b and b:a)
|
2021-08-12 16:58:30 +00:00
|
|
|
if( static_cast<void*>( a ) > static_cast<void*>( b ) )
|
|
|
|
std::swap( a, b );
|
|
|
|
|
2022-08-25 16:00:18 +00:00
|
|
|
auto it = m_checkedPairs.find( { a, b } );
|
|
|
|
|
|
|
|
if( it != m_checkedPairs.end() && it->second.test( aTargetLayer ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-08-25 16:00:18 +00:00
|
|
|
m_checkedPairs[ { a, b } ].set( aTargetLayer );
|
2021-08-12 16:58:30 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Visitor:
|
|
|
|
[&]( BOARD_ITEM* other ) -> bool
|
|
|
|
{
|
|
|
|
PAD* otherPad = dynamic_cast<PAD*>( other );
|
2021-08-22 22:05:47 +00:00
|
|
|
PCB_VIA* otherVia = dynamic_cast<PCB_VIA*>( other );
|
2021-08-12 16:58:30 +00:00
|
|
|
auto otherShape = other->GetEffectiveShape( aTargetLayer );
|
2022-01-03 20:55:38 +00:00
|
|
|
int otherNet = -1;
|
|
|
|
|
|
|
|
if( other->IsConnected() )
|
|
|
|
otherNet = static_cast<BOARD_CONNECTED_ITEM*>( other )->GetNetCode();
|
|
|
|
|
2021-08-12 16:58:30 +00:00
|
|
|
int actual;
|
|
|
|
VECTOR2I pos;
|
2021-08-22 22:05:47 +00:00
|
|
|
int clearance = 0;
|
|
|
|
|
|
|
|
if( aRefLayer == F_Mask || aRefLayer == B_Mask )
|
|
|
|
{
|
|
|
|
// Aperture-to-aperture must enforce web-min-width
|
|
|
|
clearance = m_webWidth;
|
|
|
|
}
|
2022-07-22 21:18:44 +00:00
|
|
|
else // ( aRefLayer == F_Cu || aRefLayer == B_Cu )
|
2021-08-22 22:05:47 +00:00
|
|
|
{
|
|
|
|
// Copper-to-aperture uses the solder-mask-to-copper-clearance
|
|
|
|
clearance = m_board->GetDesignSettings().m_SolderMaskToCopperClearance;
|
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
if( pad )
|
2021-08-22 22:05:47 +00:00
|
|
|
clearance += pad->GetSolderMaskExpansion();
|
|
|
|
else if( via )
|
|
|
|
clearance += via->GetSolderMaskExpansion();
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
if( otherPad )
|
2021-08-22 22:05:47 +00:00
|
|
|
clearance += otherPad->GetSolderMaskExpansion();
|
|
|
|
else if( otherVia )
|
|
|
|
clearance += otherVia->GetSolderMaskExpansion();
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
if( itemShape->Collide( otherShape.get(), clearance, &actual, &pos ) )
|
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
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" );
|
|
|
|
|
2022-02-21 13:36:19 +00:00
|
|
|
// Simple mask apertures aren't associated with copper items, so they only
|
|
|
|
// constitute a bridge when they expose other copper items having at least
|
2022-07-22 21:18:44 +00:00
|
|
|
// two distinct nets.
|
2022-07-23 08:26:04 +00:00
|
|
|
if( isMaskAperture( aItem ) )
|
2022-01-03 20:55:38 +00:00
|
|
|
{
|
2022-07-23 08:26:04 +00:00
|
|
|
if( checkMaskAperture( aItem, other, aRefLayer, otherNet, &colliding ) )
|
2022-02-18 19:46:16 +00:00
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
|
2022-02-21 13:36:19 +00:00
|
|
|
|
2022-07-22 21:18:44 +00:00
|
|
|
drce->SetErrorMessage( msg );
|
|
|
|
drce->SetItems( aItem, colliding, other );
|
|
|
|
drce->SetViolatingRule( &m_bridgeRule );
|
|
|
|
reportViolation( drce, pos, aTargetLayer );
|
2022-02-18 19:46:16 +00:00
|
|
|
}
|
2022-01-03 20:55:38 +00:00
|
|
|
}
|
2022-07-23 08:26:04 +00:00
|
|
|
else if( isMaskAperture( other ) )
|
2022-01-03 20:55:38 +00:00
|
|
|
{
|
2022-07-23 08:26:04 +00:00
|
|
|
if( checkMaskAperture( other, aItem, aRefLayer, itemNet, &colliding ) )
|
2022-02-18 19:46:16 +00:00
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
|
2022-02-21 13:36:19 +00:00
|
|
|
|
2022-07-22 21:18:44 +00:00
|
|
|
drce->SetErrorMessage( msg );
|
|
|
|
drce->SetItems( other, colliding, aItem );
|
|
|
|
drce->SetViolatingRule( &m_bridgeRule );
|
|
|
|
reportViolation( drce, pos, aTargetLayer );
|
2022-02-18 19:46:16 +00:00
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
2022-08-19 17:34:53 +00:00
|
|
|
else if( checkItemMask( other, itemNet ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-07-22 21:18:44 +00:00
|
|
|
drce->SetErrorMessage( msg );
|
|
|
|
drce->SetItems( aItem, other );
|
|
|
|
drce->SetViolatingRule( &m_bridgeRule );
|
|
|
|
reportViolation( drce, pos, aTargetLayer );
|
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
|
2022-03-11 20:13:47 +00:00
|
|
|
return !m_drcEngine->IsCancelled();
|
2021-08-12 16:58:30 +00:00
|
|
|
},
|
|
|
|
m_largestClearance );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::testMaskItemAgainstZones( BOARD_ITEM* aItem,
|
|
|
|
const EDA_RECT& aItemBBox,
|
|
|
|
PCB_LAYER_ID aMaskLayer,
|
|
|
|
PCB_LAYER_ID aTargetLayer )
|
|
|
|
{
|
2022-06-18 18:47:11 +00:00
|
|
|
for( ZONE* zone : m_board->m_DRCCopperZones )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
if( !zone->GetLayerSet().test( aTargetLayer ) )
|
|
|
|
continue;
|
|
|
|
|
2022-01-03 20:55:38 +00:00
|
|
|
int zoneNet = zone->GetNetCode();
|
|
|
|
|
|
|
|
if( aItem->IsConnected() )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-01-03 20:55:38 +00:00
|
|
|
BOARD_CONNECTED_ITEM* connectedItem = static_cast<BOARD_CONNECTED_ITEM*>( aItem );
|
|
|
|
|
|
|
|
if( zoneNet == connectedItem->GetNetCode() && zoneNet > 0 )
|
2021-08-12 16:58:30 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
EDA_RECT inflatedBBox( aItemBBox );
|
|
|
|
int clearance = m_board->GetDesignSettings().m_SolderMaskToCopperClearance;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
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();
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
inflatedBBox.Inflate( clearance );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
if( !inflatedBBox.Intersects( zone->GetCachedBoundingBox() ) )
|
|
|
|
continue;
|
2021-08-22 22:05:47 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
DRC_RTREE* zoneTree = m_board->m_CopperZoneRTreeCache[ zone ].get();
|
|
|
|
int actual;
|
|
|
|
VECTOR2I pos;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
std::shared_ptr<SHAPE> itemShape = aItem->GetEffectiveShape( aMaskLayer );
|
2022-07-22 21:18:44 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
if( zoneTree && zoneTree->QueryColliding( aItemBBox, itemShape.get(), aTargetLayer,
|
|
|
|
clearance, &actual, &pos ) )
|
|
|
|
{
|
|
|
|
wxString msg;
|
|
|
|
BOARD_ITEM* colliding = nullptr;
|
2022-01-03 20:55:38 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
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" );
|
2022-01-03 20:55:38 +00:00
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
// 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 ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-07-22 21:18:44 +00:00
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
2022-07-22 21:18:44 +00:00
|
|
|
drce->SetErrorMessage( msg );
|
2022-08-26 12:21:52 +00:00
|
|
|
drce->SetItems( aItem, colliding, zone );
|
2022-07-22 21:18:44 +00:00
|
|
|
drce->SetViolatingRule( &m_bridgeRule );
|
|
|
|
reportViolation( drce, pos, aTargetLayer );
|
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
2022-08-26 12:21:52 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE );
|
|
|
|
|
|
|
|
drce->SetErrorMessage( msg );
|
|
|
|
drce->SetItems( aItem, zone );
|
|
|
|
drce->SetViolatingRule( &m_bridgeRule );
|
|
|
|
reportViolation( drce, pos, aTargetLayer );
|
|
|
|
}
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
2022-03-11 20:13:47 +00:00
|
|
|
|
|
|
|
if( m_drcEngine->IsCancelled() )
|
|
|
|
return;
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void DRC_TEST_PROVIDER_SOLDER_MASK::testMaskBridges()
|
|
|
|
{
|
|
|
|
LSET copperAndMaskLayers = { 4, F_Mask, B_Mask, F_Cu, B_Cu };
|
|
|
|
|
2022-08-03 09:10:23 +00:00
|
|
|
const size_t progressDelta = 250;
|
|
|
|
int count = 0;
|
|
|
|
int ii = 0;
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
forEachGeometryItem( s_allBasicItemsButZones, copperAndMaskLayers,
|
2021-08-22 22:05:47 +00:00
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
2022-03-12 13:34:25 +00:00
|
|
|
++count;
|
2021-08-12 16:58:30 +00:00
|
|
|
return true;
|
|
|
|
} );
|
|
|
|
|
|
|
|
forEachGeometryItem( s_allBasicItemsButZones, copperAndMaskLayers,
|
2021-08-22 22:05:47 +00:00
|
|
|
[&]( BOARD_ITEM* item ) -> bool
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
if( m_drcEngine->IsErrorLimitExceeded( DRCE_SOLDERMASK_BRIDGE ) )
|
|
|
|
return false;
|
|
|
|
|
2022-08-03 09:10:23 +00:00
|
|
|
if( !reportProgress( ii++, count, progressDelta ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
EDA_RECT itemBBox = item->GetBoundingBox();
|
|
|
|
|
2022-05-05 23:05:34 +00:00
|
|
|
if( item->IsOnLayer( F_Mask ) && !isNullAperture( item ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
// 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 );
|
|
|
|
}
|
|
|
|
|
2022-05-05 23:05:34 +00:00
|
|
|
if( item->IsOnLayer( B_Mask ) && !isNullAperture( item ) )
|
2021-08-12 16:58:30 +00:00
|
|
|
{
|
|
|
|
// 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 ) )
|
|
|
|
{
|
2022-03-11 21:16:52 +00:00
|
|
|
reportAux( wxT( "Solder mask violations ignored. Tests not run." ) );
|
2021-08-12 16:58:30 +00:00
|
|
|
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() )
|
2021-08-22 22:05:47 +00:00
|
|
|
m_largestClearance = std::max( m_largestClearance, pad->GetSolderMaskExpansion() );
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
|
2022-08-26 12:21:52 +00:00
|
|
|
// Order is important here: m_webWidth must be added in before m_largestCourtyardClearance is
|
|
|
|
// maxed with the various SILK_CLEARANCE_CONSTRAINTS.
|
2021-08-12 16:58:30 +00:00
|
|
|
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() );
|
|
|
|
|
2022-03-11 21:16:52 +00:00
|
|
|
reportAux( wxT( "Worst clearance : %d nm" ), m_largestClearance );
|
2021-08-12 16:58:30 +00:00
|
|
|
|
|
|
|
if( !reportPhase( _( "Building solder mask..." ) ) )
|
|
|
|
return false; // DRC cancelled
|
|
|
|
|
2022-03-12 15:57:29 +00:00
|
|
|
m_checkedPairs.clear();
|
|
|
|
m_maskApertureNetMap.clear();
|
|
|
|
|
2021-08-12 16:58:30 +00:00
|
|
|
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();
|
|
|
|
|
2022-03-11 20:13:47 +00:00
|
|
|
return !m_drcEngine->IsCancelled();
|
2021-08-12 16:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
namespace detail
|
|
|
|
{
|
|
|
|
static DRC_REGISTER_TEST_PROVIDER<DRC_TEST_PROVIDER_SOLDER_MASK> dummy;
|
|
|
|
}
|