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