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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <board.h>
28#include <footprint.h>
29#include <pad.h>
30#include <pcbplot.h>
32#include <pcb_plot_params.h>
35
36#include <wx/filename.h>
37#include <wx/ffile.h>
38
39#include <memory>
40#include <string>
41#include <cmath>
42#include <regex>
43
44
45// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24327
46//
47// When plotting the solder mask layer for an SMD rounded-rectangle pad, the
48// expanded outline must be the Minkowski sum of the rounded rectangle with a
49// disk of radius equal to the solder-mask clearance. That sum is itself a
50// rounded rectangle whose sides grow by 2*clearance and whose corner radius
51// grows by clearance. The buggy code preserved the round_rect_radius_ratio
52// when resizing the pad, producing a corner radius that scaled with the
53// inflated size instead of growing linearly.
54BOOST_AUTO_TEST_SUITE( PlotPadMask )
55
56
57// Geometric invariant: when the lambda inside PlotStandardLayer mutates a
58// ROUNDRECT pad for mask plotting, the resulting pad outline must equal the
59// original pad outline inflated by mask_clearance.
60BOOST_AUTO_TEST_CASE( RoundRectMaskMatchesInflatedOutline )
61{
62 const int padW = pcbIUScale.mmToIU( 2.0 );
63 const int padH = pcbIUScale.mmToIU( 2.0 );
64 const int maskClearance = pcbIUScale.mmToIU( 1.0 );
65 const double radiusRatio = 0.125;
66 const int originalRadius = KiROUND( std::min( padW, padH ) * radiusRatio );
67 const int maxError = pcbIUScale.mmToIU( 0.005 );
68
69 BOARD board;
70 auto footprint = std::make_unique<FOOTPRINT>( &board );
71
72 auto pad = new PAD( footprint.get() );
73 pad->SetAttribute( PAD_ATTRIB::SMD );
75 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( padW, padH ) );
76 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, radiusRatio );
77 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
78 pad->SetPosition( VECTOR2I( 0, 0 ) );
79 footprint->Add( pad );
80
81 // Reference outline: PAD::TransformShapeToPolygon with clearance correctly
82 // expands the rounded rectangle by the Minkowski sum.
84 pad->TransformShapeToPolygon( expected, F_Mask, maskClearance, maxError, ERROR_INSIDE );
85
86 // Replicate the fixed plot-path mutation: grow the pad to padPlotsSize and
87 // grow the corner radius by mask_clearance.
88 pad->SetSize( PADSTACK::ALL_LAYERS,
89 VECTOR2I( padW + 2 * maskClearance, padH + 2 * maskClearance ) );
90 pad->SetRoundRectCornerRadius( PADSTACK::ALL_LAYERS, originalRadius + maskClearance );
91
92 SHAPE_POLY_SET produced;
93 pad->TransformShapeToPolygon( produced, F_Mask, 0, maxError, ERROR_INSIDE );
94
95 const BOX2I expectedBB = expected.BBox();
96 const BOX2I producedBB = produced.BBox();
97 BOOST_CHECK_EQUAL( expectedBB.GetWidth(), producedBB.GetWidth() );
98 BOOST_CHECK_EQUAL( expectedBB.GetHeight(), producedBB.GetHeight() );
99
101 diff.BooleanSubtract( produced );
102 SHAPE_POLY_SET reverseDiff = produced;
103 reverseDiff.BooleanSubtract( expected );
104
105 // Tolerance is 1 IU^2-equivalent (chord/arc approximation slack)
106 const double tolerance = std::pow( pcbIUScale.mmToIU( 0.001 ), 2.0 );
107 BOOST_CHECK_LE( std::abs( diff.Area() ), tolerance );
108 BOOST_CHECK_LE( std::abs( reverseDiff.Area() ), tolerance );
109}
110
111
112// End-to-end test: emit a Gerber file for the F.Mask layer of a board with a
113// roundrect SMD pad and verify the mask aperture has the geometrically
114// correct corner radius. With the bug, the corner radius (visible in the
115// arc I/J offsets) would scale with the inflated pad size, producing a ratio
116// to the bounding box equal to radius_ratio rather than the geometrically
117// correct (original_radius + mask_clearance) / (size + 2 * mask_clearance).
118BOOST_AUTO_TEST_CASE( RoundRectGerberMaskApertureHasCorrectRadius )
119{
120 const int padW = pcbIUScale.mmToIU( 2.0 );
121 const int padH = pcbIUScale.mmToIU( 2.0 );
122 const int maskClearance = pcbIUScale.mmToIU( 1.0 );
123 const double radiusRatio = 0.125;
124 const int originalRadius = KiROUND( std::min( padW, padH ) * radiusRatio );
125
126 BOARD board;
128
129 auto footprint = std::make_unique<FOOTPRINT>( &board );
130 footprint->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50.0 ),
131 pcbIUScale.mmToIU( 50.0 ) ) );
132
133 auto pad = new PAD( footprint.get() );
134 pad->SetAttribute( PAD_ATTRIB::SMD );
136 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( padW, padH ) );
137 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, radiusRatio );
138 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
139 pad->SetPosition( footprint->GetPosition() );
140 pad->SetLocalSolderMaskMargin( maskClearance );
141 footprint->Add( pad );
142 board.Add( footprint.release() );
143
144 GERBER_PLOTTER plotter;
145 SIMPLE_RENDER_SETTINGS renderSettings;
146 plotter.SetRenderSettings( &renderSettings );
147
148 // Disable aperture macros so the roundrect is emitted as a Gerber region
149 // whose arc I/J offsets we can read directly.
150 plotter.UseX2format( true );
151 plotter.DisableApertMacros( true );
152
153 wxString gbrPath = wxFileName::CreateTempFileName( wxT( "kicad_gbr_24327" ) );
154 BOOST_REQUIRE( !gbrPath.IsEmpty() );
155 BOOST_TEST_MESSAGE( "Gerber output: " << gbrPath.ToStdString() );
156 BOOST_REQUIRE( plotter.OpenFile( gbrPath ) );
157
158 plotter.SetViewport( VECTOR2I( 0, 0 ), pcbIUScale.IU_PER_MILS / 10, 1.0, false );
159 BOOST_REQUIRE( plotter.StartPlot( wxT( "1" ) ) );
160
161 PCB_PLOT_PARAMS plotOpts;
162 plotOpts.SetFormat( PLOT_FORMAT::GERBER );
163 plotOpts.SetUseGerberX2format( true );
164 plotOpts.SetDisableGerberMacros( true );
165
166 PlotStandardLayer( &board, &plotter, LSET( { F_Mask } ), plotOpts );
167
168 BOOST_REQUIRE( plotter.EndPlot() );
169
170 wxFFile file( gbrPath, wxT( "rb" ) );
171 BOOST_REQUIRE( file.IsOpened() );
172 wxFileOffset len = file.Length();
173 BOOST_REQUIRE_GT( len, 0 );
174
175 std::string buffer;
176 buffer.resize( static_cast<size_t>( len ) );
177 BOOST_REQUIRE_EQUAL( file.Read( buffer.data(), len ), static_cast<size_t>( len ) );
178 file.Close();
179
180 // Sample every D02/D01 coordinate to get the plotted region's bounding box,
181 // and every arc command (X..Y..I..J..D01) to read the corner radius. We
182 // care only about ratios, so we don't need to interpret the Gerber unit.
183 std::regex coordRe( R"(X(-?\d+)Y(-?\d+)D0[12]\*)" );
184 auto coordBegin = std::sregex_iterator( buffer.begin(), buffer.end(), coordRe );
185 auto coordEnd = std::sregex_iterator();
186
187 long long minX = std::numeric_limits<long long>::max();
188 long long maxX = std::numeric_limits<long long>::min();
189 long long minY = std::numeric_limits<long long>::max();
190 long long maxY = std::numeric_limits<long long>::min();
191 int coordCount = 0;
192
193 for( auto it = coordBegin; it != coordEnd; ++it )
194 {
195 long long x = std::stoll( ( *it )[1] );
196 long long y = std::stoll( ( *it )[2] );
197 minX = std::min( minX, x );
198 maxX = std::max( maxX, x );
199 minY = std::min( minY, y );
200 maxY = std::max( maxY, y );
201 coordCount++;
202 }
203
204 BOOST_REQUIRE_GT( coordCount, 0 );
205 BOOST_CHECK_EQUAL( maxX - minX, maxY - minY );
206
207 const long long bboxExtent = maxX - minX;
208 BOOST_REQUIRE_GT( bboxExtent, 0 );
209
210 // The expected corner radius is (originalRadius + maskClearance), and the
211 // expected bounding-box extent is (padW + 2 * maskClearance). With the
212 // bug the ratio would be radiusRatio (= 0.125). The correct ratio for the
213 // chosen inputs is 1.25 / 4.0 = 0.3125.
214 const double expectedRadiusRatio = static_cast<double>( originalRadius + maskClearance )
215 / static_cast<double>( padW + 2 * maskClearance );
216 const double buggyRadiusRatio = radiusRatio;
217 BOOST_TEST_MESSAGE( "Expected r/bbox " << expectedRadiusRatio
218 << " buggy " << buggyRadiusRatio );
219
220 std::regex arcRe( R"(X(-?\d+)Y(-?\d+)I(-?\d+)J(-?\d+)D01\*)" );
221 auto arcBegin = std::sregex_iterator( buffer.begin(), buffer.end(), arcRe );
222 auto arcEnd = std::sregex_iterator();
223 BOOST_REQUIRE( arcBegin != arcEnd );
224
225 int arcCount = 0;
226 for( auto it = arcBegin; it != arcEnd; ++it )
227 {
228 double i = std::stoll( ( *it )[3] );
229 double j = std::stoll( ( *it )[4] );
230 double r = std::hypot( i, j );
231 double ratio = r / static_cast<double>( bboxExtent );
232
233 BOOST_CHECK_MESSAGE( std::abs( ratio - expectedRadiusRatio ) < 1e-3,
234 "Arc radius/bbox ratio " << ratio
235 << " does not match expected "
236 << expectedRadiusRatio
237 << " (buggy value would be "
238 << buggyRadiusRatio << ")" );
239 arcCount++;
240 }
241
242 BOOST_CHECK_EQUAL( arcCount, 4 );
243
244 if( wxFileExists( gbrPath ) )
245 wxRemoveFile( gbrPath );
246}
247
248
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:125
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:990
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1247
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1101
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr size_type GetHeight() const
Definition box2.h:215
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
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Parameters and options when plotting/printing a board.
void SetUseGerberX2format(bool aUse)
void SetDisableGerberMacros(bool aDisable)
void SetFormat(PLOT_FORMAT aFormat)
virtual bool OpenFile(const wxString &aFullFilename)
Open or create the plot file aFullFilename.
Definition plotter.cpp:77
void SetRenderSettings(RENDER_SETTINGS *aSettings)
Definition plotter.h:167
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.
@ F_Mask
Definition layer_ids.h:97
@ F_Cu
Definition layer_ids.h:64
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:99
@ ROUNDRECT
Definition padstack.h:57
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_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
BOOST_AUTO_TEST_CASE(RoundRectMaskMatchesInflatedOutline)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687