KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_plot_pad_mask.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
21
22#include <board.h>
24#include <footprint.h>
25#include <pad.h>
26#include <pcb_track.h>
27#include <pcbplot.h>
29#include <pcb_plot_params.h>
32
33#include <wx/filename.h>
34#include <wx/ffile.h>
35
36#include <memory>
37#include <string>
38#include <cmath>
39#include <regex>
40
41
42// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24327
43//
44// When plotting the solder mask layer for an SMD rounded-rectangle pad, the
45// expanded outline must be the Minkowski sum of the rounded rectangle with a
46// disk of radius equal to the solder-mask clearance. That sum is itself a
47// rounded rectangle whose sides grow by 2*clearance and whose corner radius
48// grows by clearance. The buggy code preserved the round_rect_radius_ratio
49// when resizing the pad, producing a corner radius that scaled with the
50// inflated size instead of growing linearly.
51BOOST_AUTO_TEST_SUITE( PlotPadMask )
52
53
54// Geometric invariant: when the lambda inside PlotStandardLayer mutates a
55// ROUNDRECT pad for mask plotting, the resulting pad outline must equal the
56// original pad outline inflated by mask_clearance.
57BOOST_AUTO_TEST_CASE( RoundRectMaskMatchesInflatedOutline )
58{
59 const int padW = pcbIUScale.mmToIU( 2.0 );
60 const int padH = pcbIUScale.mmToIU( 2.0 );
61 const int maskClearance = pcbIUScale.mmToIU( 1.0 );
62 const double radiusRatio = 0.125;
63 const int originalRadius = KiROUND( std::min( padW, padH ) * radiusRatio );
64 const int maxError = pcbIUScale.mmToIU( 0.005 );
65
66 BOARD board;
67 auto footprint = std::make_unique<FOOTPRINT>( &board );
68
69 auto pad = new PAD( footprint.get() );
70 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
71 pad->SetAttribute( PAD_ATTRIB::SMD );
73 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( padW, padH ) );
74 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, radiusRatio );
75 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
76 pad->SetPosition( VECTOR2I( 0, 0 ) );
77 footprint->Add( pad );
78
79 // Reference outline: PAD::TransformShapeToPolygon with clearance correctly
80 // expands the rounded rectangle by the Minkowski sum.
82 pad->TransformShapeToPolygon( expected, F_Mask, maskClearance, maxError, ERROR_INSIDE );
83
84 // Replicate the fixed plot-path mutation: grow the pad to padPlotsSize and
85 // grow the corner radius by mask_clearance.
86 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( padW + 2 * maskClearance, padH + 2 * maskClearance ) );
87 pad->SetRoundRectCornerRadius( PADSTACK::ALL_LAYERS, originalRadius + maskClearance );
88
89 SHAPE_POLY_SET produced;
90 pad->TransformShapeToPolygon( produced, F_Mask, 0, maxError, ERROR_INSIDE );
91
92 const BOX2I expectedBB = expected.BBox();
93 const BOX2I producedBB = produced.BBox();
94 BOOST_CHECK_EQUAL( expectedBB.GetWidth(), producedBB.GetWidth() );
95 BOOST_CHECK_EQUAL( expectedBB.GetHeight(), producedBB.GetHeight() );
96
98 diff.BooleanSubtract( produced );
99 SHAPE_POLY_SET reverseDiff = produced;
100 reverseDiff.BooleanSubtract( expected );
101
102 // Tolerance is 1 IU^2-equivalent (chord/arc approximation slack)
103 const double tolerance = std::pow( pcbIUScale.mmToIU( 0.001 ), 2.0 );
104 BOOST_CHECK_LE( std::abs( diff.Area() ), tolerance );
105 BOOST_CHECK_LE( std::abs( reverseDiff.Area() ), tolerance );
106}
107
108
109// End-to-end test: emit a Gerber file for the F.Mask layer of a board with a
110// roundrect SMD pad and verify the mask aperture has the geometrically
111// correct corner radius. With the bug, the corner radius (visible in the
112// arc I/J offsets) would scale with the inflated pad size, producing a ratio
113// to the bounding box equal to radius_ratio rather than the geometrically
114// correct (original_radius + mask_clearance) / (size + 2 * mask_clearance).
115BOOST_AUTO_TEST_CASE( RoundRectGerberMaskApertureHasCorrectRadius )
116{
117 const int padW = pcbIUScale.mmToIU( 2.0 );
118 const int padH = pcbIUScale.mmToIU( 2.0 );
119 const int maskClearance = pcbIUScale.mmToIU( 1.0 );
120 const double radiusRatio = 0.125;
121 const int originalRadius = KiROUND( std::min( padW, padH ) * radiusRatio );
122
123 BOARD board;
125
126 std::unique_ptr<FOOTPRINT> footprint = std::make_unique<FOOTPRINT>( &board );
127 footprint->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ) );
128
129 PAD* pad = new PAD( footprint.get() );
130 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
131 pad->SetAttribute( PAD_ATTRIB::SMD );
133 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( padW, padH ) );
134 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, radiusRatio );
135 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
136 pad->SetPosition( footprint->GetPosition() );
137 pad->SetLocalSolderMaskMargin( maskClearance );
138 footprint->Add( pad );
139 board.Add( footprint.release() );
140
141 GERBER_PLOTTER plotter;
142 SIMPLE_RENDER_SETTINGS renderSettings;
143 plotter.SetRenderSettings( &renderSettings );
144
145 // Disable aperture macros so the roundrect is emitted as a Gerber region
146 // whose arc I/J offsets we can read directly.
147 plotter.UseX2format( true );
148 plotter.DisableApertMacros( true );
149
150 wxString gbrPath = wxFileName::CreateTempFileName( wxT( "kicad_gbr_24327" ) );
151 BOOST_REQUIRE( !gbrPath.IsEmpty() );
152 BOOST_TEST_MESSAGE( "Gerber output: " << gbrPath.ToStdString() );
153 BOOST_REQUIRE( plotter.OpenFile( gbrPath ) );
154
155 plotter.SetViewport( VECTOR2I( 0, 0 ), pcbIUScale.IU_PER_MILS / 10, 1.0, false );
156 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ) ) );
157
158 PCB_PLOT_PARAMS plotOpts;
159 plotOpts.SetFormat( PLOT_FORMAT::GERBER );
160 plotOpts.SetUseGerberX2format( true );
161 plotOpts.SetDisableGerberMacros( true );
162
163 PlotStandardLayer( &board, &plotter, LSET( { F_Mask } ), plotOpts );
164
165 BOOST_REQUIRE( plotter.EndPlot() );
166
167 wxFFile file( gbrPath, wxT( "rb" ) );
168 BOOST_REQUIRE( file.IsOpened() );
169 wxFileOffset len = file.Length();
170 BOOST_REQUIRE_GT( len, 0 );
171
172 std::string buffer;
173 buffer.resize( static_cast<size_t>( len ) );
174 BOOST_REQUIRE_EQUAL( file.Read( buffer.data(), len ), static_cast<size_t>( len ) );
175 file.Close();
176
177 // Sample every D02/D01 coordinate to get the plotted region's bounding box,
178 // and every arc command (X..Y..I..J..D01) to read the corner radius. We
179 // care only about ratios, so we don't need to interpret the Gerber unit.
180 std::regex coordRe( R"(X(-?\d+)Y(-?\d+)D0[12]\*)" );
181 auto coordBegin = std::sregex_iterator( buffer.begin(), buffer.end(), coordRe );
182 auto coordEnd = std::sregex_iterator();
183
184 long long minX = std::numeric_limits<long long>::max();
185 long long maxX = std::numeric_limits<long long>::min();
186 long long minY = std::numeric_limits<long long>::max();
187 long long maxY = std::numeric_limits<long long>::min();
188 int coordCount = 0;
189
190 for( auto it = coordBegin; it != coordEnd; ++it )
191 {
192 long long x = std::stoll( ( *it )[1] );
193 long long y = std::stoll( ( *it )[2] );
194 minX = std::min( minX, x );
195 maxX = std::max( maxX, x );
196 minY = std::min( minY, y );
197 maxY = std::max( maxY, y );
198 coordCount++;
199 }
200
201 BOOST_REQUIRE_GT( coordCount, 0 );
202 BOOST_CHECK_EQUAL( maxX - minX, maxY - minY );
203
204 const long long bboxExtent = maxX - minX;
205 BOOST_REQUIRE_GT( bboxExtent, 0 );
206
207 // The expected corner radius is (originalRadius + maskClearance), and the
208 // expected bounding-box extent is (padW + 2 * maskClearance). With the
209 // bug the ratio would be radiusRatio (= 0.125). The correct ratio for the
210 // chosen inputs is 1.25 / 4.0 = 0.3125.
211 const double expectedRadiusRatio = static_cast<double>( originalRadius + maskClearance )
212 / static_cast<double>( padW + 2 * maskClearance );
213 const double buggyRadiusRatio = radiusRatio;
214 BOOST_TEST_MESSAGE( "Expected r/bbox " << expectedRadiusRatio << " buggy " << buggyRadiusRatio );
215
216 std::regex arcRe( R"(X(-?\d+)Y(-?\d+)I(-?\d+)J(-?\d+)D01\*)" );
217 auto arcBegin = std::sregex_iterator( buffer.begin(), buffer.end(), arcRe );
218 auto arcEnd = std::sregex_iterator();
219 BOOST_REQUIRE( arcBegin != arcEnd );
220
221 int arcCount = 0;
222 for( auto it = arcBegin; it != arcEnd; ++it )
223 {
224 double i = std::stoll( ( *it )[3] );
225 double j = std::stoll( ( *it )[4] );
226 double r = std::hypot( i, j );
227 double ratio = r / static_cast<double>( bboxExtent );
228
229 BOOST_CHECK_MESSAGE( std::abs( ratio - expectedRadiusRatio ) < 1e-3,
230 "Arc radius/bbox ratio " << ratio << " does not match expected " << expectedRadiusRatio
231 << " (buggy value would be " << buggyRadiusRatio << ")" );
232 arcCount++;
233 }
234
235 BOOST_CHECK_EQUAL( arcCount, 4 );
236
237 if( wxFileExists( gbrPath ) )
238 wxRemoveFile( gbrPath );
239}
240
241
242BOOST_AUTO_TEST_CASE( PthPadKeepsMaskOpeningWhenCopperRemoved )
243{
244 BOARD board;
245
246 NETINFO_ITEM* net = new NETINFO_ITEM( &board, "N1", 1 );
247 board.Add( net );
248
249 const VECTOR2I padPos( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) );
250
251 FOOTPRINT* footprint = new FOOTPRINT( &board );
252 footprint->SetPosition( padPos );
253
254 PAD* pad = new PAD( footprint );
255 pad->SetAttribute( PAD_ATTRIB::PTH );
256 pad->SetLayerSet( PAD::PTHMask() );
257 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 2.0 ), pcbIUScale.mmToIU( 2.0 ) ) );
258 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
259 pad->SetPosition( padPos );
260 pad->SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::REMOVE_ALL );
261 pad->SetNet( net );
262 footprint->Add( pad );
263 board.Add( footprint );
264
265 PCB_TRACK* track = new PCB_TRACK( &board );
266 track->SetStart( padPos );
267 track->SetEnd( padPos + VECTOR2I( pcbIUScale.mmToIU( 5.0 ), 0 ) );
268 track->SetLayer( B_Cu );
269 track->SetWidth( pcbIUScale.mmToIU( 0.5 ) );
270 track->SetNet( net );
271 board.Add( track );
272
273 board.BuildConnectivity();
274
275 BOOST_REQUIRE( !pad->FlashLayer( F_Cu ) );
276 BOOST_REQUIRE( pad->FlashLayer( B_Cu ) );
277
278 BOOST_CHECK( pad->FlashLayer( F_Mask ) );
279 BOOST_CHECK( pad->FlashLayer( B_Mask ) );
280}
281
282
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:995
virtual void SetNet(NETINFO_ITEM *aNetInfo)
Set a NET_INFO object for the item.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1497
bool BuildConnectivity(PROGRESS_REPORTER *aReporter=nullptr)
Build or rebuild the board connectivity database for the board, especially the list of connected item...
Definition board.cpp:364
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1299
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr size_type GetHeight() const
Definition box2.h:212
void SetPosition(const VECTOR2I &aPos) override
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
virtual void SetViewport(const VECTOR2I &aOffset, double aIusPerDecimil, double aScale, bool aMirror) override
Set the plot offset and scaling for the current plot.
virtual bool EndPlot() override
void UseX2format(bool aEnable)
virtual bool StartPlot(const wxString &pageNumber) override
Write GERBER header to file initialize global variable g_Plot_PlotOutputFile.
void DisableApertMacros(bool aDisable)
Disable Aperture Macro (AM) command, only for broken Gerber Readers.
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
Handle the data for a net.
Definition netinfo.h:50
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
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
static LSET PTHMask()
layer set for a through hole pad
Definition pad.cpp:606
Parameters and options when plotting/printing a board.
void SetUseGerberX2format(bool aUse)
void SetDisableGerberMacros(bool aDisable)
void SetFormat(PLOT_FORMAT aFormat)
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:89
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:92
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
virtual bool OpenFile(const wxString &aFullFilename)
Open or create the plot file aFullFilename.
Definition plotter.cpp:75
void SetRenderSettings(RENDER_SETTINGS *aSettings)
Definition plotter.h:166
Represent a set of closed polygons.
double Area()
Return the area of this poly set.
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
Minimal concrete render settings suitable for plotters in tests.
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ F_Cu
Definition layer_ids.h:60
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ PTH
Plated through hole pad.
Definition padstack.h:97
@ ROUNDRECT
Definition padstack.h:56
void PlotStandardLayer(BOARD *aBoard, PLOTTER *aPlotter, const LSET &aLayerMask, const PCB_PLOT_PARAMS &aPlotOpt)
Plot copper or technical layers.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
BOOST_AUTO_TEST_CASE(RoundRectMaskMatchesInflatedOutline)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683