KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_idf_export.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
20/*
21 * Verifies that Edge_Cuts graphics living inside a footprint are exported as board cutouts in the
22 * IDFv3 .emn output. issue5854.kicad_pcb is a DIP-14 footprint carrying an fp_rect on Edge.Cuts (a
23 * rectangular board cavity) inside a rectangular board outline; the exporter must emit the board
24 * outline as loop 0 and the footprint cutout as loop 1.
25 */
26
27#include <algorithm>
28#include <cctype>
29#include <cmath>
30#include <filesystem>
31#include <fstream>
32#include <map>
33#include <sstream>
34#include <string>
35#include <vector>
36
38#include <boost/test/unit_test.hpp>
39
42#include <base_units.h>
43#include <board.h>
44#include <filename_resolver.h>
45#include <footprint.h>
46#include <geometry/eda_angle.h>
47#include <pcb_shape.h>
48
49#include <wx/string.h>
50
51
52namespace
53{
54
58std::map<int, std::vector<VECTOR2D>> ReadBoardOutlineLoops( const std::filesystem::path& aEmnPath )
59{
60 std::ifstream in( aEmnPath );
61 BOOST_REQUIRE_MESSAGE( in, "Cannot open exported .emn: " << aEmnPath.string() );
62
63 std::map<int, std::vector<VECTOR2D>> loops;
64 std::string line;
65 bool inSection = false;
66
67 while( std::getline( in, line ) )
68 {
69 if( line.find( ".BOARD_OUTLINE" ) != std::string::npos )
70 {
71 inSection = true;
72 continue;
73 }
74
75 if( line.find( ".END_BOARD_OUTLINE" ) != std::string::npos )
76 break;
77
78 if( !inSection )
79 continue;
80
81 std::istringstream ss( line );
82 std::vector<std::string> tokens;
83
84 for( std::string tok; ss >> tok; )
85 tokens.push_back( tok );
86
87 // A point record is "<loopIndex> <x> <y> <angle>"; skip the owner line and the single-value
88 // thickness header, which would otherwise be miscounted as a loop index.
89 if( tokens.size() < 4 )
90 continue;
91
92 const std::string& first = tokens.front();
93 const bool isLoopIndex =
94 std::all_of( first.begin(), first.end(), []( unsigned char c ) { return std::isdigit( c ); } );
95
96 if( isLoopIndex )
97 {
98 loops[std::stoi( first )].emplace_back( std::stod( tokens[1] ),
99 std::stod( tokens[2] ) );
100 }
101 }
102
103 return loops;
104}
105
106
110void CheckLoopContainsPoints( const std::vector<VECTOR2D>& aLoopPoints,
111 const std::vector<VECTOR2I>& aExpectedIU )
112{
113 for( const VECTOR2I& expected : aExpectedIU )
114 {
115 const double expX = expected.x * pcbIUScale.MM_PER_IU;
116 const double expY = -expected.y * pcbIUScale.MM_PER_IU;
117
118 const bool found = std::any_of( aLoopPoints.begin(), aLoopPoints.end(),
119 [&]( const VECTOR2D& pt )
120 {
121 return std::abs( pt.x - expX ) < 1e-3
122 && std::abs( pt.y - expY ) < 1e-3;
123 } );
124
125 BOOST_CHECK_MESSAGE( found, "Cutout corner (" << expX << ", " << expY
126 << ") missing from exported loop" );
127 }
128}
129
130
132PCB_SHAPE* GetCutoutShape( FOOTPRINT* aFootprint )
133{
134 PCB_SHAPE* cutout = nullptr;
135
136 for( BOARD_ITEM* item : aFootprint->GraphicalItems() )
137 {
138 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != Edge_Cuts )
139 continue;
140
141 BOOST_REQUIRE( !cutout );
142 cutout = static_cast<PCB_SHAPE*>( item );
143 }
144
145 BOOST_REQUIRE( cutout );
146 return cutout;
147}
148
149} // namespace
150
151
152BOOST_AUTO_TEST_SUITE( IdfExport )
153
154
155BOOST_AUTO_TEST_CASE( FootprintCutoutIsExported )
156{
157 const std::string sourceBoard = KI_TEST::GetPcbnewTestDataDir() + "issue5854.kicad_pcb";
158 BOOST_REQUIRE_MESSAGE( std::filesystem::exists( sourceBoard ), "Missing test board " << sourceBoard );
159
160 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream( sourceBoard );
161 BOOST_REQUIRE( board );
162
163 const std::filesystem::path outDir =
164 std::filesystem::temp_directory_path() / "kicad_idf_cutout_test";
165 std::filesystem::create_directories( outDir );
166
167 const std::filesystem::path emnPath = outDir / "cutout.emn";
168 std::filesystem::remove( emnPath );
169
171 wxString errorMsg;
172
173 const bool ok = ExportBoardToIDF3( board.get(), wxString::FromUTF8( emnPath.string().c_str() ),
174 false, 0.0, 0.0, true, true, &resolver, &errorMsg );
175
176 BOOST_REQUIRE_MESSAGE( ok, "IDF export failed: " << errorMsg.ToStdString() );
177 BOOST_REQUIRE( std::filesystem::exists( emnPath ) );
178
179 const std::map<int, std::vector<VECTOR2D>> loops = ReadBoardOutlineLoops( emnPath );
180
181 // The board perimeter is loop 0; the footprint cutout must add at least one further loop.
182 BOOST_CHECK_MESSAGE( loops.count( 0 ) == 1, "Board outline (loop 0) missing from .emn" );
183 BOOST_REQUIRE_MESSAGE( loops.count( 1 ) == 1, "Footprint cutout (loop 1) missing from .emn" );
184
185 // The cutout rect's corners must land where the board says they are (mm, Y mirrored),
186 // so a reflected or displaced cutout fails even though a loop 1 exists.
187 PCB_SHAPE* cutout = GetCutoutShape( board->Footprints().front() );
189
190 const VECTOR2I start = cutout->GetStart();
191 const VECTOR2I end = cutout->GetEnd();
192
193 CheckLoopContainsPoints( loops.at( 1 ),
194 { start, VECTOR2I( end.x, start.y ), end,
195 VECTOR2I( start.x, end.y ) } );
196
197 std::filesystem::remove_all( outDir );
198}
199
200
201// Rotating the footprint by a non-cardinal angle converts its fp_rect cutout to SHAPE_T::POLY
202// (EDA_SHAPE::rotate), exercising the polygon branch of the cutout emitter.
203BOOST_AUTO_TEST_CASE( RotatedPolygonCutoutIsExported )
204{
205 const std::string sourceBoard = KI_TEST::GetPcbnewTestDataDir() + "issue5854.kicad_pcb";
206 BOOST_REQUIRE_MESSAGE( std::filesystem::exists( sourceBoard ), "Missing test board " << sourceBoard );
207
208 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream( sourceBoard );
209 BOOST_REQUIRE( board );
210 BOOST_REQUIRE( !board->Footprints().empty() );
211
212 FOOTPRINT* footprint = board->Footprints().front();
213 footprint->Rotate( footprint->GetPosition(), EDA_ANGLE( 45.0, DEGREES_T ) );
214
215 const std::filesystem::path outDir =
216 std::filesystem::temp_directory_path() / "kicad_idf_poly_cutout_test";
217 std::filesystem::create_directories( outDir );
218
219 const std::filesystem::path emnPath = outDir / "poly_cutout.emn";
220 std::filesystem::remove( emnPath );
221
223 wxString errorMsg;
224
225 const bool ok = ExportBoardToIDF3( board.get(), wxString::FromUTF8( emnPath.string().c_str() ),
226 false, 0.0, 0.0, true, true, &resolver, &errorMsg );
227
228 BOOST_REQUIRE_MESSAGE( ok, "IDF export failed: " << errorMsg.ToStdString() );
229 BOOST_REQUIRE( std::filesystem::exists( emnPath ) );
230
231 const std::map<int, std::vector<VECTOR2D>> loops = ReadBoardOutlineLoops( emnPath );
232
233 BOOST_CHECK_MESSAGE( loops.count( 0 ) == 1, "Board outline (loop 0) missing from .emn" );
234 BOOST_REQUIRE_MESSAGE( loops.count( 1 ) == 1,
235 "Rotated polygon cutout (loop 1) missing from .emn" );
236
237 // The rotated cutout is now a POLY; its outline corners must appear in the exported loop
238 PCB_SHAPE* cutout = GetCutoutShape( footprint );
239 BOOST_REQUIRE( cutout->GetShape() == SHAPE_T::POLY );
240
241 std::vector<VECTOR2I> corners;
242 const SHAPE_LINE_CHAIN& chain = cutout->GetPolyShape().COutline( 0 );
243
244 for( int ii = 0; ii < chain.PointCount(); ++ii )
245 corners.push_back( chain.CPoint( ii ) );
246
247 CheckLoopContainsPoints( loops.at( 1 ), corners );
248
249 std::filesystem::remove_all( outDir );
250}
251
252
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:81
SHAPE_POLY_SET & GetPolyShape()
SHAPE_T GetShape() const
Definition eda_shape.h:185
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:240
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:190
Provide an extensible class to resolve 3D model paths.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
VECTOR2I GetPosition() const override
Definition footprint.h:403
DRAWINGS & GraphicalItems()
Definition footprint.h:378
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
@ DEGREES_T
Definition eda_angle.h:31
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
bool ExportBoardToIDF3(BOARD *aPcb, const wxString &aFullFileName, bool aUseThou, double aXRef, double aYRef, bool aIncludeUnspecified, bool aIncludeDNP, FILENAME_RESOLVER *aResolver, wxString *aErrorMsg)
Generate IDFv3 compliant board (*.emn) and library (*.emp) files representing the user's PCB design.
static FILENAME_RESOLVER * resolver
@ Edge_Cuts
Definition layer_ids.h:108
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(FootprintCutoutIsExported)
VECTOR3I expected(15, 30, 45)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
const SHAPE_LINE_CHAIN chain
VECTOR2I end
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
VECTOR2< double > VECTOR2D
Definition vector2d.h:682