KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_drc_solder_mask_expansion.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
22#include <board.h>
24#include <pad.h>
25#include <pcb_track.h>
26#include <pcb_shape.h>
27#include <footprint.h>
28#include <drc/drc_engine.h>
29#include <core/profile.h>
31#include <drc/drc_item.h>
33
34
43
44
46{
47 // This test verifies that footprint-level solder mask expansion override is correctly
48 // applied to pads that don't have their own local override.
49 // Regression test for https://gitlab.com/kicad/code/kicad/-/issues/22751
50
51 KI_TEST::LoadBoard( m_settingsManager, "issue22751/issue22751", m_board );
52
53 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
54 bds.m_DRCEngine->InitEngine( wxFileName() );
55
56 // Board default solder mask expansion is 0.05mm (50000 IU)
58
59 // Find footprints and their pads
60 PAD* padWithFootprintMargin = nullptr;
61 PAD* padWithNoMargin = nullptr;
62 PAD* padWithOwnMargin = nullptr;
63
64 for( FOOTPRINT* fp : m_board->Footprints() )
65 {
66 if( fp->GetFPIDAsString() == "TestFootprint_WithSolderMaskMargin" )
67 {
68 // Footprint has solder mask margin of 0.1mm (100000 IU)
69 BOOST_CHECK( fp->GetLocalSolderMaskMargin().has_value() );
70 BOOST_CHECK_EQUAL( fp->GetLocalSolderMaskMargin().value(), 100000 );
71
72 for( PAD* pad : fp->Pads() )
73 {
74 if( pad->GetNumber() == "1" )
75 {
76 padWithFootprintMargin = pad;
77
78 // Pad should NOT have its own local margin
79 BOOST_CHECK( !pad->GetLocalSolderMaskMargin().has_value() );
80 }
81 }
82 }
83 else if( fp->GetFPIDAsString() == "TestFootprint_NoSolderMaskMargin" )
84 {
85 // Footprint has no solder mask margin
86 BOOST_CHECK( !fp->GetLocalSolderMaskMargin().has_value() );
87
88 for( PAD* pad : fp->Pads() )
89 {
90 if( pad->GetNumber() == "1" )
91 padWithNoMargin = pad;
92 }
93 }
94 else if( fp->GetFPIDAsString() == "TestFootprint_PadOverridesSolderMaskMargin" )
95 {
96 // Footprint has solder mask margin of 0.1mm
97 BOOST_CHECK( fp->GetLocalSolderMaskMargin().has_value() );
98 BOOST_CHECK_EQUAL( fp->GetLocalSolderMaskMargin().value(), 100000 );
99
100 for( PAD* pad : fp->Pads() )
101 {
102 if( pad->GetNumber() == "1" )
103 {
104 padWithOwnMargin = pad;
105
106 // Pad has its own margin of 0.2mm (200000 IU)
107 BOOST_CHECK( pad->GetLocalSolderMaskMargin().has_value() );
108 BOOST_CHECK_EQUAL( pad->GetLocalSolderMaskMargin().value(), 200000 );
109 }
110 }
111 }
112 }
113
114 BOOST_REQUIRE( padWithFootprintMargin != nullptr );
115 BOOST_REQUIRE( padWithNoMargin != nullptr );
116 BOOST_REQUIRE( padWithOwnMargin != nullptr );
117
118 // Test GetSolderMaskExpansion which uses the DRC engine to evaluate the constraint
119
120 // Pad in footprint with solder mask margin should use footprint's margin (0.1mm = 100000 IU)
121 int expansionWithFpMargin = padWithFootprintMargin->GetSolderMaskExpansion( F_Mask );
122 BOOST_CHECK_EQUAL( expansionWithFpMargin, 100000 );
123
124 // Pad in footprint without solder mask margin should use board default (0.05mm = 50000 IU)
125 int expansionNoMargin = padWithNoMargin->GetSolderMaskExpansion( F_Mask );
126 BOOST_CHECK_EQUAL( expansionNoMargin, 50000 );
127
128 // Pad with its own solder mask margin should use pad's margin (0.2mm = 200000 IU),
129 // overriding the footprint's margin
130 int expansionWithPadMargin = padWithOwnMargin->GetSolderMaskExpansion( F_Mask );
131 BOOST_CHECK_EQUAL( expansionWithPadMargin, 200000 );
132}
133
134
136{
137 // Verify that GetSolderMaskExpansion is fast when no custom DRC rules exist.
138 // Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23213
139 //
140 // Before the fix, GetSolderMaskExpansion called EvalRules for every item even when no
141 // custom rules existed, causing a 10-14x DRC slowdown vs KiCad 9.
142
143 KI_TEST::LoadBoard( m_settingsManager, "stonehenge", m_board );
144
145 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
146 bds.m_DRCEngine->InitEngine( wxFileName() );
147
149
150 // Collect all items that would be queried during the solder mask test
151 std::vector<PAD*> pads;
152 std::vector<PCB_TRACK*> tracks;
153 std::vector<PCB_SHAPE*> shapes;
154
155 for( FOOTPRINT* fp : m_board->Footprints() )
156 {
157 for( PAD* pad : fp->Pads() )
158 pads.push_back( pad );
159
160 for( BOARD_ITEM* item : fp->GraphicalItems() )
161 {
162 if( item->Type() == PCB_SHAPE_T )
163 shapes.push_back( static_cast<PCB_SHAPE*>( item ) );
164 }
165 }
166
167 for( PCB_TRACK* track : m_board->Tracks() )
168 tracks.push_back( track );
169
170 for( BOARD_ITEM* item : m_board->Drawings() )
171 {
172 if( item->Type() == PCB_SHAPE_T )
173 shapes.push_back( static_cast<PCB_SHAPE*>( item ) );
174 }
175
176 // Simulate the access pattern from the solder mask bridge test: call GetSolderMaskExpansion
177 // many times for each item (once per potential collision pair).
178 const int iterations = 1000;
179
180 PROF_TIMER timer;
181
182 for( int i = 0; i < iterations; ++i )
183 {
184 for( PAD* pad : pads )
185 pad->GetSolderMaskExpansion( F_Mask );
186
187 for( PCB_TRACK* track : tracks )
188 track->GetSolderMaskExpansion();
189
190 for( PCB_SHAPE* shape : shapes )
191 shape->GetSolderMaskExpansion();
192 }
193
194 timer.Stop();
195
196 int totalCalls = iterations * ( pads.size() + tracks.size() + shapes.size() );
197
198 BOOST_TEST_MESSAGE( wxString::Format( "%d calls to GetSolderMaskExpansion took %0.1f ms "
199 "(%0.0f ns/call)",
200 totalCalls, timer.msecs(),
201 timer.msecs() * 1e6 / totalCalls ) );
202
203 // Sanitizer instrumentation changes execution cost, not the lookup contract
204#if !defined( KICAD_SANITIZE_THREADS ) && !defined( KICAD_SANITIZE_ADDRESS )
205 // With the fix (direct property lookup), this should be well under 100ms.
206 // Without the fix (EvalRules for every call), this was ~500ms+ even in debug builds.
207 // Use a generous threshold to avoid flakiness on slow CI machines.
208 BOOST_CHECK_MESSAGE( timer.msecs() < 500.0,
209 wxString::Format( "GetSolderMaskExpansion too slow: %0.1f ms for %d calls",
210 timer.msecs(), totalCalls ) );
211#endif
212}
213
214
216{
218 m_board( std::make_unique<BOARD>() )
219 {
220 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
221
222 bds.m_SolderMaskExpansion = pcbIUScale.mmToIU( 0.1 );
223 bds.m_SolderMaskMinWidth = 0;
224 bds.m_TentViasFront = false;
225 bds.m_TentViasBack = false;
226
227 FOOTPRINT* footprint = new FOOTPRINT( m_board.get() );
228 m_board->Add( footprint );
229
230 PAD* pad = new PAD( footprint );
231 pad->SetAttribute( PAD_ATTRIB::PTH );
233 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
234 pad->SetPrimaryDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.5 ), pcbIUScale.mmToIU( 0.5 ) ) );
235 pad->SetLayerSet( LSET( { F_Cu, B_Cu, F_Mask, B_Mask } ) );
236 pad->SetPosition( VECTOR2I( 0, 0 ) );
237 footprint->Add( pad );
238
239 PCB_VIA* via = new PCB_VIA( m_board.get() );
240 via->SetLayerPair( F_Cu, B_Cu );
241 via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 10.0 ), 0 ) );
242 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.8 ) );
243 via->SetDrill( pcbIUScale.mmToIU( 0.4 ) );
244 m_board->Add( via );
245
246 m_board->BuildConnectivity();
247
248 std::shared_ptr<DRC_ENGINE> engine = std::make_shared<DRC_ENGINE>( m_board.get(), &bds );
249 engine->InitEngine( wxFileName() );
250 bds.m_DRCEngine = engine;
251 }
252
253 void AddSilkLine( double aFromMM, double aToMM, double aOffsetMM )
254 {
255 PCB_SHAPE* line = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
256 int y = pcbIUScale.mmToIU( aOffsetMM );
257
258 line->SetLayer( F_SilkS );
259 line->SetStart( VECTOR2I( pcbIUScale.mmToIU( aFromMM ), y ) );
260 line->SetEnd( VECTOR2I( pcbIUScale.mmToIU( aToMM ), y ) );
261 line->SetWidth( pcbIUScale.mmToIU( 0.15 ) );
262 m_board->Add( line );
263 }
264
266 {
267 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
268 int count = 0;
269
270 for( int code = 0; code < DRCE_LAST; ++code )
272
274
276 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
277 const std::function<void( PCB_MARKER* )>& )
278 {
279 if( aItem->GetErrorCode() == DRCE_SILK_MASK_CLEARANCE )
280 ++count;
281 } );
282
283 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
285
286 return count;
287 }
288
289 std::unique_ptr<BOARD> m_board;
290};
291
292
293BOOST_FIXTURE_TEST_CASE( SilkInsideViaMaskOpeningIsReported, SILK_MASK_EXPANSION_FIXTURE )
294{
295 AddSilkLine( 8.0, 12.0, -0.525 );
296
297 BOOST_CHECK_MESSAGE( CountMaskViolations() == 1, "silk clear of the via copper but inside its mask opening was not "
298 "reported" );
299}
300
301
302BOOST_FIXTURE_TEST_CASE( SilkInsidePadMaskOpeningIsReported, SILK_MASK_EXPANSION_FIXTURE )
303{
304 AddSilkLine( -2.0, 2.0, -0.625 );
305
306 BOOST_CHECK_MESSAGE( CountMaskViolations() == 1, "silk clear of the pad copper but inside its mask opening was not "
307 "reported" );
308}
309
310
311BOOST_FIXTURE_TEST_CASE( SilkOutsidePadMaskOpeningIsNotReported, SILK_MASK_EXPANSION_FIXTURE )
312{
313 AddSilkLine( -2.0, 2.0, 0.75 );
314
315 BOOST_CHECK_MESSAGE( CountMaskViolations() == 0, "silk clear of the pad mask opening was reported" );
316}
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
Container for design settings for a BOARD object.
std::map< int, SEVERITY > m_DRCSeverities
std::shared_ptr< DRC_ENGINE > m_DRCEngine
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
void RunTests(EDA_UNITS aUnits, bool aReportAllTrackErrors, bool aTestFootprints, BOARD_COMMIT *aCommit=nullptr)
Run the DRC tests.
void SetViolationHandler(DRC_VIOLATION_HANDLER aHandler)
Set an optional DRC violation handler (receives DRC_ITEMs and positions).
Definition drc_engine.h:164
bool HasRulesForConstraintType(DRC_CONSTRAINT_T constraintID)
void ClearViolationHandler()
Definition drc_engine.h:169
void InitEngine(const wxFileName &aRulePath)
Initialize the DRC engine.
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
int GetSolderMaskExpansion(PCB_LAYER_ID aLayer) const
Definition pad.cpp:2067
void SetWidth(int aWidth) override
void SetEnd(const VECTOR2I &aEnd) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) override
A small class to help profiling.
Definition profile.h:46
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:86
double msecs(bool aSinceLast=false)
Definition profile.h:148
@ SOLDER_MASK_EXPANSION_CONSTRAINT
@ DRCE_SILK_MASK_CLEARANCE
Definition drc_item.h:111
@ DRCE_LAST
Definition drc_item.h:144
@ SEGMENT
Definition eda_shape.h:56
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ F_SilkS
Definition layer_ids.h:96
@ F_Cu
Definition layer_ids.h:60
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
STL namespace.
@ PTH
Plated through hole pad.
Definition padstack.h:97
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_IGNORE
void AddSilkLine(double aFromMM, double aToMM, double aOffsetMM)
BOOST_FIXTURE_TEST_CASE(FootprintLevelSolderMaskExpansion, DRC_SOLDER_MASK_EXPANSION_TEST_FIXTURE)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708