KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_footprint_transform_sync.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21
22#include <board.h>
26#include <drc/drc_engine.h>
27#include <drc/drc_item.h>
28#include <footprint.h>
29#include <pad.h>
30#include <pcb_barcode.h>
31#include <pcb_dimension.h>
32#include <pcb_marker.h>
33#include <pcb_point.h>
34#include <pcb_shape.h>
35#include <pcb_table.h>
36#include <pcb_tablecell.h>
37#include <pcb_text.h>
38#include <pcb_textbox.h>
43#include <zone.h>
44
45#include <filesystem>
46
47
48BOOST_AUTO_TEST_SUITE( FootprintTransformSync )
49
50
52{
53 BOOST_CHECK_EQUAL( aFp.GetTransform().GetTranslate().x, aFp.GetPosition().x );
54 BOOST_CHECK_EQUAL( aFp.GetTransform().GetTranslate().y, aFp.GetPosition().y );
55 BOOST_CHECK_EQUAL( aFp.GetTransform().GetRotate().AsDegrees(), aFp.GetOrientation().AsDegrees() );
56 BOOST_CHECK_EQUAL( aFp.GetTransform().GetScaleX(), 1.0 );
57 BOOST_CHECK_EQUAL( aFp.GetTransform().GetScaleY(), 1.0 );
58}
59
60
61BOOST_AUTO_TEST_CASE( DefaultIsIdentity )
62{
63 FOOTPRINT fp( nullptr );
64 BOOST_CHECK( fp.GetTransform().IsIdentity() );
66}
67
68
69BOOST_AUTO_TEST_CASE( SetPositionUpdatesTransform )
70{
71 FOOTPRINT fp( nullptr );
72 fp.SetPosition( VECTOR2I( 1234, -5678 ) );
74}
75
76
77BOOST_AUTO_TEST_CASE( SetOrientationUpdatesTransform )
78{
79 FOOTPRINT fp( nullptr );
80 fp.SetOrientation( EDA_ANGLE( 45.0, DEGREES_T ) );
82}
83
84
85BOOST_AUTO_TEST_CASE( MoveUpdatesTransform )
86{
87 FOOTPRINT fp( nullptr );
88 fp.SetPosition( VECTOR2I( 100, 200 ) );
89 fp.Move( VECTOR2I( 50, -25 ) );
93}
94
95
96BOOST_AUTO_TEST_CASE( PadEffectiveShapeFollowsFootprintScale )
97{
98 FOOTPRINT fp( nullptr );
99 fp.SetPosition( VECTOR2I( 0, 0 ) );
100
101 PAD* pad = new PAD( &fp );
103 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
104 pad->SetPosition( VECTOR2I( 5000000, 0 ) );
105 pad->SetLayerSet( PAD::SMDMask() );
106 fp.Add( pad, ADD_MODE::APPEND );
107
108 const VECTOR2I posBefore = pad->ShapePos( F_Cu );
109 std::shared_ptr<SHAPE> shapeBefore = pad->GetEffectiveShape( F_Cu, FLASHING::ALWAYS_FLASHED );
110 const BOX2I bboxBefore = shapeBefore->BBox();
111
112 BOOST_CHECK_EQUAL( posBefore.x, 5000000 );
113 BOOST_CHECK_EQUAL( bboxBefore.GetCenter().x, 5000000 );
114 BOOST_CHECK_EQUAL( bboxBefore.GetSize().x, 1000000 );
115
116 fp.SetTransformScale( 2.0, 1.0 );
117
118 const VECTOR2I posAfter = pad->ShapePos( F_Cu );
119 std::shared_ptr<SHAPE> shapeAfter = pad->GetEffectiveShape( F_Cu, FLASHING::ALWAYS_FLASHED );
120 const BOX2I bboxAfter = shapeAfter->BBox();
121
122 BOOST_CHECK_EQUAL( posAfter.x, 10000000 );
123 BOOST_CHECK_EQUAL( bboxAfter.GetCenter().x, 10000000 );
124 BOOST_CHECK_EQUAL( bboxAfter.GetSize().x, 1500000 );
125}
126
127
128BOOST_AUTO_TEST_CASE( PadBBoxFollowsFootprintMove )
129{
130 FOOTPRINT fp( nullptr );
131 fp.SetPosition( VECTOR2I( 0, 0 ) );
132
133 PAD* pad = new PAD( &fp );
134 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 500000 ) );
135 pad->SetPosition( VECTOR2I( 5000000, 0 ) );
136 fp.Add( pad, ADD_MODE::APPEND );
137
138 BOX2I before = pad->GetBoundingBox();
139 BOOST_CHECK_EQUAL( before.GetCenter().x, 5000000 );
140
141 fp.SetPosition( VECTOR2I( 10000000, 0 ) );
142
143 BOX2I after = pad->GetBoundingBox();
144 BOOST_CHECK_EQUAL( after.GetCenter().x, 15000000 );
145}
146
147
148BOOST_AUTO_TEST_CASE( PadBBoxFollowsFootprintRotation )
149{
150 FOOTPRINT fp( nullptr );
151 fp.SetPosition( VECTOR2I( 0, 0 ) );
152
153 PAD* pad = new PAD( &fp );
154 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 500000 ) );
155 pad->SetPosition( VECTOR2I( 5000000, 0 ) );
156 fp.Add( pad, ADD_MODE::APPEND );
157
158 BOX2I before = pad->GetBoundingBox();
159 BOOST_CHECK_EQUAL( before.GetCenter().x, 5000000 );
160 BOOST_CHECK_EQUAL( before.GetCenter().y, 0 );
161
162 fp.SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
163
164 BOX2I after = pad->GetBoundingBox();
165 BOOST_CHECK_EQUAL( after.GetCenter().x, 0 );
166 BOOST_CHECK_EQUAL( after.GetCenter().y, -5000000 );
167}
168
169
170BOOST_AUTO_TEST_CASE( RotateUpdatesTransform )
171{
172 FOOTPRINT fp( nullptr );
173 fp.SetPosition( VECTOR2I( 100, 0 ) );
174 fp.Rotate( VECTOR2I( 0, 0 ), EDA_ANGLE( 90.0, DEGREES_T ) );
176}
177
178
179BOOST_AUTO_TEST_CASE( SetLayerUpdatesFlipped )
180{
181 FOOTPRINT fp( nullptr );
182 BOOST_CHECK( !fp.IsFlipped() );
183
184 fp.SetLayer( B_Cu );
185 BOOST_CHECK( fp.IsFlipped() );
186
187 fp.SetLayer( F_Cu );
188 BOOST_CHECK( !fp.IsFlipped() );
189}
190
191
192BOOST_AUTO_TEST_CASE( CopyPreservesTransform )
193{
194 FOOTPRINT fp( nullptr );
195 fp.SetPosition( VECTOR2I( 500, 700 ) );
196 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
197 fp.SetLayer( B_Cu );
198
199 FOOTPRINT copy( fp );
201 BOOST_CHECK( copy.IsFlipped() );
202}
203
204
205// xform.Apply(libPos) must equal the pad's board position.
207{
208 for( const PAD* pad : aFp.Pads() )
209 {
210 VECTOR2I libPos = pad->GetFPRelativePosition();
211 VECTOR2I expected = aFp.GetTransform().Apply( libPos );
212 VECTOR2I actual = pad->GetPosition();
213
214 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
215 "pad " << pad->GetNumber() << " expected ( " << expected.x << ", " << expected.y << " )"
216 << " actual ( " << actual.x << ", " << actual.y << " )" );
217 }
218}
219
220
221// Stored lib pos and the derived FP-relative pos must agree within 1 IU.
222static void CHECK_PAD_LIBPOS_MIRROR( const FOOTPRINT& aFp )
223{
224 for( const PAD* pad : aFp.Pads() )
225 {
226 VECTOR2I derived = pad->GetFPRelativePosition();
227 VECTOR2I stored = pad->GetLibraryPosition();
228
229 BOOST_CHECK_MESSAGE( std::abs( derived.x - stored.x ) <= 1 && std::abs( derived.y - stored.y ) <= 1,
230 "pad " << pad->GetNumber() << " m_libPos ( " << stored.x << ", " << stored.y << " )"
231 << " != GetFPRelativePosition ( " << derived.x << ", " << derived.y << " )" );
232 }
233}
234
235
237{
239 std::unique_ptr<BOARD> m_board;
240};
241
242
243BOOST_FIXTURE_TEST_CASE( PadTransformInvariantOnLoadedBoard, BOARD_FIXTURE )
244{
245 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
246
247 for( const FOOTPRINT* fp : m_board->Footprints() )
249}
250
251
252BOOST_FIXTURE_TEST_CASE( PadTransformInvariantAfterMutation, BOARD_FIXTURE )
253{
254 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
255
256 for( FOOTPRINT* fp : m_board->Footprints() )
257 {
258 fp->Move( VECTOR2I( 1234, -567 ) );
260
261 fp->SetOrientation( fp->GetOrientation() + EDA_ANGLE( 17.5, DEGREES_T ) );
263
264 fp->Flip( fp->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
266 }
267}
268
269
270BOOST_FIXTURE_TEST_CASE( PadLibPosMirrorOnLoad, BOARD_FIXTURE )
271{
272 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
273
274 for( const FOOTPRINT* fp : m_board->Footprints() )
276}
277
278
279BOOST_FIXTURE_TEST_CASE( PadLibPosMirrorAfterMutation, BOARD_FIXTURE )
280{
281 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
282
283 for( FOOTPRINT* fp : m_board->Footprints() )
284 {
285 fp->Move( VECTOR2I( 1234, -567 ) );
287
288 fp->SetOrientation( fp->GetOrientation() + EDA_ANGLE( 17.5, DEGREES_T ) );
290
291 fp->Flip( fp->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
293 }
294}
295
296
297BOOST_FIXTURE_TEST_CASE( UndoRedoRestoresFootprintAndPadState, BOARD_FIXTURE )
298{
299 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
300
301 FOOTPRINT* fp = *m_board->Footprints().begin();
302
303 VECTOR2I origPos = fp->GetPosition();
304 EDA_ANGLE origOrient = fp->GetOrientation();
305 PCB_LAYER_ID origLayer = fp->GetLayer();
306
307 // Capture pad positions by number. Iteration order is not stable across swap.
308 std::map<wxString, VECTOR2I> origPadByNumber;
309 for( const PAD* pad : fp->Pads() )
310 origPadByNumber[pad->GetNumber()] = pad->GetPosition();
311
312 std::unique_ptr<FOOTPRINT> snapshot( static_cast<FOOTPRINT*>( fp->Clone() ) );
313
314 fp->Move( VECTOR2I( 1234, -567 ) );
315 fp->SetOrientation( fp->GetOrientation() + EDA_ANGLE( 45.0, DEGREES_T ) );
317
318 BOOST_CHECK( fp->GetPosition() != origPos );
319
320 fp->SwapItemData( snapshot.get() );
321
322 BOOST_CHECK_EQUAL( fp->GetPosition().x, origPos.x );
323 BOOST_CHECK_EQUAL( fp->GetPosition().y, origPos.y );
324 BOOST_CHECK_EQUAL( fp->GetOrientation().AsDegrees(), origOrient.AsDegrees() );
325 BOOST_CHECK_EQUAL( fp->GetLayer(), origLayer );
326
327 for( const PAD* pad : fp->Pads() )
328 {
329 VECTOR2I p = pad->GetPosition();
330 VECTOR2I orig = origPadByNumber[pad->GetNumber()];
331 BOOST_CHECK_MESSAGE( std::abs( p.x - orig.x ) <= 1 && std::abs( p.y - orig.y ) <= 1,
332 "pad " << pad->GetNumber() << " after undo at ( " << p.x << ", " << p.y << " )"
333 << " expected ( " << orig.x << ", " << orig.y << " )" );
334 }
335
338}
339
340
341// Stored lib start and the derived FP-relative pos must agree within 1 IU.
342static void CHECK_SHAPE_LIBPOS_MIRROR( const FOOTPRINT& aFp )
343{
344 for( const BOARD_ITEM* item : aFp.GraphicalItems() )
345 {
346 if( item->Type() != PCB_SHAPE_T )
347 continue;
348
349 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
350
351 if( shape->GetShape() != SHAPE_T::SEGMENT && shape->GetShape() != SHAPE_T::RECTANGLE
352 && shape->GetShape() != SHAPE_T::CIRCLE )
353 {
354 continue;
355 }
356
357 VECTOR2I libStartFromShape = shape->GetFPRelativePosition();
358 VECTOR2I libStartStored = shape->GetLibraryStart();
359
360 BOOST_CHECK_MESSAGE( std::abs( libStartFromShape.x - libStartStored.x ) <= 1
361 && std::abs( libStartFromShape.y - libStartStored.y ) <= 1,
362 "shape m_libStart ( " << libStartStored.x << ", " << libStartStored.y << " ) "
363 << "!= GetFPRelativePosition ( " << libStartFromShape.x << ", "
364 << libStartFromShape.y << " )" );
365 }
366}
367
368
369BOOST_FIXTURE_TEST_CASE( ShapeLibPosMirrorOnLoad, BOARD_FIXTURE )
370{
371 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
372
373 for( const FOOTPRINT* fp : m_board->Footprints() )
375}
376
377
378BOOST_FIXTURE_TEST_CASE( ShapeLibPosMirrorAfterMutation, BOARD_FIXTURE )
379{
380 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
381
382 for( FOOTPRINT* fp : m_board->Footprints() )
383 {
384 fp->Move( VECTOR2I( 1234, -567 ) );
386
387 fp->SetOrientation( fp->GetOrientation() + EDA_ANGLE( 17.5, DEGREES_T ) );
389
390 fp->Flip( fp->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
392 }
393}
394
395
396BOOST_FIXTURE_TEST_CASE( SetTransformScaleDoublesPadOffsets, BOARD_FIXTURE )
397{
398 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
399
400 FOOTPRINT* fp = *m_board->Footprints().begin();
401 BOOST_REQUIRE( fp );
402
403 // Capture each pad's offset from the footprint anchor before scaling.
404 std::map<wxString, VECTOR2I> origOffsets;
406
407 for( const PAD* pad : fp->Pads() )
408 origOffsets[pad->GetNumber()] = pad->GetPosition() - anchor;
409
410 fp->SetTransformScale( 2.0, 2.0 );
411
412 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleX(), 2.0, 1e-9 );
413 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleY(), 2.0, 1e-9 );
414
415 // Anchor doesn't move under SetTransformScale.
418
419 // Each pad's offset from the anchor should have doubled.
420 for( const PAD* pad : fp->Pads() )
421 {
422 VECTOR2I offset = pad->GetPosition() - anchor;
423 VECTOR2I orig = origOffsets[pad->GetNumber()];
424
425 BOOST_CHECK_MESSAGE( std::abs( offset.x - 2 * orig.x ) <= 1 && std::abs( offset.y - 2 * orig.y ) <= 1,
426 "pad " << pad->GetNumber() << " offset ( " << offset.x << ", " << offset.y << " ) "
427 << "expected ( " << 2 * orig.x << ", " << 2 * orig.y << " )" );
428 }
429}
430
431
432BOOST_FIXTURE_TEST_CASE( SetTransformScaleRebakeShapes, BOARD_FIXTURE )
433{
434 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
435
436 FOOTPRINT* fp = *m_board->Footprints().begin();
437 BOOST_REQUIRE( fp );
438
440
441 // Capture each segment's anchor-relative start offset before scaling.
442 std::vector<VECTOR2I> origStartOffsets;
443
444 for( const BOARD_ITEM* item : fp->GraphicalItems() )
445 {
446 if( item->Type() != PCB_SHAPE_T )
447 continue;
448
449 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
450
451 if( shape->GetShape() != SHAPE_T::SEGMENT )
452 continue;
453
454 origStartOffsets.push_back( shape->GetStart() - anchor );
455 }
456
457 fp->SetTransformScale( 2.0, 2.0 );
458
459 size_t i = 0;
460 for( const BOARD_ITEM* item : fp->GraphicalItems() )
461 {
462 if( item->Type() != PCB_SHAPE_T )
463 continue;
464
465 const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( item );
466
467 if( shape->GetShape() != SHAPE_T::SEGMENT )
468 continue;
469
470 VECTOR2I offset = shape->GetStart() - anchor;
471 VECTOR2I orig = origStartOffsets[i++];
472
473 BOOST_CHECK_MESSAGE( std::abs( offset.x - 2 * orig.x ) <= 1 && std::abs( offset.y - 2 * orig.y ) <= 1,
474 "shape start offset ( " << offset.x << ", " << offset.y << " ) "
475 << "expected ( " << 2 * orig.x << ", " << 2 * orig.y << " )" );
476 }
477}
478
479
480BOOST_FIXTURE_TEST_CASE( ScalePersistsAcrossSaveLoad, BOARD_FIXTURE )
481{
482 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
483
484 FOOTPRINT* fp = *m_board->Footprints().begin();
485 BOOST_REQUIRE( fp );
486
487 fp->SetTransformScale( 2.0, 3.0 );
488
489 VECTOR2I origAnchor = fp->GetPosition();
490 std::map<wxString, VECTOR2I> origPadPositions;
491 for( const PAD* pad : fp->Pads() )
492 origPadPositions[pad->GetNumber()] = pad->GetPosition();
493
494 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "scale_roundtrip.kicad_pcb";
495
496 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
497
498 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
499 BOOST_REQUIRE( reloaded );
500
501 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
502 BOOST_REQUIRE( fp2 );
503
504 BOOST_CHECK_CLOSE( fp2->GetTransform().GetScaleX(), 2.0, 1e-9 );
505 BOOST_CHECK_CLOSE( fp2->GetTransform().GetScaleY(), 3.0, 1e-9 );
506 BOOST_CHECK_EQUAL( fp2->GetPosition().x, origAnchor.x );
507 BOOST_CHECK_EQUAL( fp2->GetPosition().y, origAnchor.y );
508
509 for( const PAD* pad : fp2->Pads() )
510 {
511 VECTOR2I p = pad->GetPosition();
512 VECTOR2I orig = origPadPositions[pad->GetNumber()];
513
514 BOOST_CHECK_MESSAGE( std::abs( p.x - orig.x ) <= 1 && std::abs( p.y - orig.y ) <= 1,
515 "pad " << pad->GetNumber() << " after reload at ( " << p.x << ", " << p.y
516 << " ) expected ( " << orig.x << ", " << orig.y << " )" );
517 }
518}
519
520
521BOOST_FIXTURE_TEST_CASE( ScaleSurvivesUndoRedo, BOARD_FIXTURE )
522{
523 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
524
525 FOOTPRINT* fp = *m_board->Footprints().begin();
526 BOOST_REQUIRE( fp );
527
528 std::map<wxString, VECTOR2I> origPadPositions;
529 for( const PAD* pad : fp->Pads() )
530 origPadPositions[pad->GetNumber()] = pad->GetPosition();
531
532 std::unique_ptr<FOOTPRINT> snapshot( static_cast<FOOTPRINT*>( fp->Clone() ) );
533
534 fp->SetTransformScale( 2.0, 2.0 );
535
536 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleX(), 2.0, 1e-9 );
537
538 fp->SwapItemData( snapshot.get() );
539
540 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleX(), 1.0, 1e-9 );
541 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleY(), 1.0, 1e-9 );
542
543 for( const PAD* pad : fp->Pads() )
544 {
545 VECTOR2I p = pad->GetPosition();
546 VECTOR2I orig = origPadPositions[pad->GetNumber()];
547
548 BOOST_CHECK_MESSAGE( std::abs( p.x - orig.x ) <= 1 && std::abs( p.y - orig.y ) <= 1,
549 "pad " << pad->GetNumber() << " after undo at ( " << p.x << ", " << p.y << " ) expected ( "
550 << orig.x << ", " << orig.y << " )" );
551 }
552}
553
554
555BOOST_AUTO_TEST_CASE( SetTransformScaleScalesZoneOutline )
556{
557 // Footprint with a single keepout zone outline (a 2 mm square 1 mm off the
558 // anchor). Scaling the footprint should scale the outline.
559 FOOTPRINT fp( nullptr );
560 fp.SetPosition( VECTOR2I( 1000000, 500000 ) ); // 1mm, 0.5mm
561
562 ZONE* zone = new ZONE( &fp );
563
564 SHAPE_POLY_SET poly;
565 poly.NewOutline();
567 poly.Append( anchor + VECTOR2I( 1000000, 1000000 ) );
568 poly.Append( anchor + VECTOR2I( 3000000, 1000000 ) );
569 poly.Append( anchor + VECTOR2I( 3000000, 3000000 ) );
570 poly.Append( anchor + VECTOR2I( 1000000, 3000000 ) );
571
572 *zone->Outline() = poly;
573 fp.Add( zone, ADD_MODE::APPEND );
574
575 // Capture original board-frame offsets for each vertex.
576 SHAPE_POLY_SET origBoard = zone->GetBoardOutline();
577 std::vector<VECTOR2I> origOffsets;
578 for( int i = 0; i < origBoard.TotalVertices(); ++i )
579 origOffsets.push_back( origBoard.CVertex( i ) - anchor );
580
581 fp.SetTransformScale( 2.0, 2.0 );
582
583 SHAPE_POLY_SET scaledBoard = zone->GetBoardOutline();
584 BOOST_REQUIRE_EQUAL( scaledBoard.TotalVertices(), (int) origOffsets.size() );
585
586 for( int i = 0; i < scaledBoard.TotalVertices(); ++i )
587 {
588 VECTOR2I offset = scaledBoard.CVertex( i ) - anchor;
589 BOOST_CHECK_MESSAGE( std::abs( offset.x - 2 * origOffsets[i].x ) <= 1
590 && std::abs( offset.y - 2 * origOffsets[i].y ) <= 1,
591 "vertex " << i << " offset ( " << offset.x << ", " << offset.y << " ) expected ( "
592 << 2 * origOffsets[i].x << ", " << 2 * origOffsets[i].y << " )" );
593 }
594}
595
596
597BOOST_FIXTURE_TEST_CASE( SetTransformScaleScalesPadAndDrillSize, BOARD_FIXTURE )
598{
599 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
600
601 FOOTPRINT* fp = *m_board->Footprints().begin();
602 BOOST_REQUIRE( fp );
603
604 std::map<wxString, VECTOR2I> origSizes;
605 std::map<wxString, VECTOR2I> origDrills;
606 for( const PAD* pad : fp->Pads() )
607 {
608 origSizes[pad->GetNumber()] = pad->GetSize( PADSTACK::ALL_LAYERS );
609 origDrills[pad->GetNumber()] = pad->GetDrillSize();
610 }
611
612 fp->SetTransformScale( 2.0, 2.0 );
613
614 for( const PAD* pad : fp->Pads() )
615 {
616 VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
617 VECTOR2I drill = pad->GetDrillSize();
618 VECTOR2I origSize = origSizes[pad->GetNumber()];
619 VECTOR2I origDrill = origDrills[pad->GetNumber()];
620
621 BOOST_CHECK_EQUAL( size.x, 2 * origSize.x );
622 BOOST_CHECK_EQUAL( size.y, 2 * origSize.y );
623 BOOST_CHECK_EQUAL( drill.x, 2 * origDrill.x );
624 BOOST_CHECK_EQUAL( drill.y, 2 * origDrill.y );
625 }
626}
627
628
629BOOST_FIXTURE_TEST_CASE( SetTransformScaleScalesTextSize, BOARD_FIXTURE )
630{
631 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
632
633 FOOTPRINT* fp = *m_board->Footprints().begin();
634 BOOST_REQUIRE( fp );
635
636 BOOST_REQUIRE( !fp->GetFields().empty() );
637
638 PCB_FIELD* refField = fp->GetFields().front();
639 VECTOR2I origSize = refField->GetTextSize();
640 int origThickness = refField->GetTextThickness();
641
642 fp->SetTransformScale( 2.0, 2.0 );
643
644 BOOST_CHECK_EQUAL( refField->GetTextSize().x, 2 * origSize.x );
645 BOOST_CHECK_EQUAL( refField->GetTextSize().y, 2 * origSize.y );
646 BOOST_CHECK_EQUAL( refField->GetTextThickness(), 2 * origThickness );
647}
648
649
650BOOST_FIXTURE_TEST_CASE( SetTransformScaleInvalidatesTextBBoxCache, BOARD_FIXTURE )
651{
652 // GetTextBox caches per line. SetTransformScale must clear that cache so
653 // the next read sees the new size.
654 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
655
656 FOOTPRINT* fp = *m_board->Footprints().begin();
657 BOOST_REQUIRE( fp );
658 BOOST_REQUIRE( !fp->GetFields().empty() );
659
660 PCB_FIELD* refField = fp->GetFields().front();
661
662 // Prime the bbox cache.
663 BOX2I origBox = refField->GetTextBox( nullptr );
664 BOOST_REQUIRE( origBox.GetWidth() > 0 && origBox.GetHeight() > 0 );
665
666 fp->SetTransformScale( 2.0, 2.0 );
667
668 BOX2I scaledBox = refField->GetTextBox( nullptr );
669
670 // Roughly 2x in each dimension. Allow 10% difference for text metrics. A stale
671 // cache would return origBox.
672 BOOST_CHECK_GT( scaledBox.GetWidth(), origBox.GetWidth() );
673 BOOST_CHECK_GT( scaledBox.GetHeight(), origBox.GetHeight() );
674 BOOST_CHECK_CLOSE( (double) scaledBox.GetWidth(), 2.0 * origBox.GetWidth(), 10.0 );
675 BOOST_CHECK_CLOSE( (double) scaledBox.GetHeight(), 2.0 * origBox.GetHeight(), 10.0 );
676}
677
678
679BOOST_FIXTURE_TEST_CASE( SetTransformScaleScalesShapeLineWidth, BOARD_FIXTURE )
680{
681 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
682
683 FOOTPRINT* fp = *m_board->Footprints().begin();
684 BOOST_REQUIRE( fp );
685
686 PCB_SHAPE* shape = nullptr;
687
688 for( BOARD_ITEM* item : fp->GraphicalItems() )
689 {
690 if( item->Type() == PCB_SHAPE_T )
691 {
692 shape = static_cast<PCB_SHAPE*>( item );
693 break;
694 }
695 }
696
697 BOOST_REQUIRE( shape );
698
699 int origWidth = shape->GetStroke().GetWidth();
700
701 fp->SetTransformScale( 2.0, 2.0 );
702
703 BOOST_CHECK_EQUAL( shape->GetStroke().GetWidth(), 2 * origWidth );
704}
705
706
707BOOST_FIXTURE_TEST_CASE( DRCFlagsScaledFootprintWithPads, BOARD_FIXTURE )
708{
709 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
710
711 FOOTPRINT* fp = *m_board->Footprints().begin();
712 BOOST_REQUIRE( fp );
713 BOOST_REQUIRE( !fp->Pads().empty() );
714
715 fp->SetTransformScale( 2.0, 2.0 );
716
717 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
718
719 // Silence unrelated DRC checks so we can read the violation list cleanly.
720 for( int code = DRCE_FIRST; code <= DRCE_LAST; ++code )
721 {
724 }
725
727
728 std::vector<int> violations;
729
731 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
732 const std::function<void( PCB_MARKER* )>& aPathGenerator )
733 {
734 violations.push_back( aItem->GetErrorCode() );
735 } );
736
737 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
738
739 bool found = false;
740 for( int code : violations )
741 {
743 {
744 found = true;
745 break;
746 }
747 }
748
749 BOOST_CHECK_MESSAGE( found, "expected DRCE_FOOTPRINT_SCALED_WITH_PADS for a 2x scaled "
750 "footprint with pads; got "
751 << violations.size() << " violations" );
752}
753
754
755BOOST_FIXTURE_TEST_CASE( DRCSilentForUnscaledFootprintWithPads, BOARD_FIXTURE )
756{
757 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
758
759 BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings();
760
761 for( int code = DRCE_FIRST; code <= DRCE_LAST; ++code )
762 {
765 }
766
768
769 int scaledFlagged = 0;
770
772 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I& aPos, int aLayer,
773 const std::function<void( PCB_MARKER* )>& aPathGenerator )
774 {
775 if( aItem->GetErrorCode() == DRCE_FOOTPRINT_SCALED_WITH_PADS )
776 scaledFlagged++;
777 } );
778
779 bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true, false );
780
781 BOOST_CHECK_EQUAL( scaledFlagged, 0 );
782}
783
784
785BOOST_FIXTURE_TEST_CASE( RescaleAroundPointKeepsCenterFixed, BOARD_FIXTURE )
786{
787 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
788
789 FOOTPRINT* fp = *m_board->Footprints().begin();
790 BOOST_REQUIRE( fp );
791
792 // Center is offset from the anchor so the translate change is non-trivial.
793 VECTOR2I center( fp->GetPosition().x + 10000, fp->GetPosition().y + 5000 );
794
795 fp->RescaleAroundPoint( center, 2.0, 2.0 );
796
797 VECTOR2I expectedAnchor( center.x + 2 * ( fp->GetTransform().GetTranslate().x - center.x ) / 2,
798 center.y + 2 * ( fp->GetTransform().GetTranslate().y - center.y ) / 2 );
799
800 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleX(), 2.0, 1e-9 );
801 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleY(), 2.0, 1e-9 );
802
803 // Pad invariants still hold after the rescale.
806}
807
808
809BOOST_FIXTURE_TEST_CASE( RescaleAroundPointMovesAnchorByExpectedDelta, BOARD_FIXTURE )
810{
811 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
812
813 FOOTPRINT* fp = *m_board->Footprints().begin();
814 BOOST_REQUIRE( fp );
815
816 VECTOR2I origAnchor = fp->GetPosition();
817 VECTOR2I center( origAnchor.x - 50000, origAnchor.y + 30000 );
818 double sx = 3.0;
819 double sy = 3.0;
820
821 fp->RescaleAroundPoint( center, sx, sy );
822
823 // new_anchor = center + s * (old_anchor - center)
824 VECTOR2I expected( KiROUND( center.x + sx * ( origAnchor.x - center.x ) ),
825 KiROUND( center.y + sy * ( origAnchor.y - center.y ) ) );
826
829}
830
831
832BOOST_FIXTURE_TEST_CASE( RescaleAroundPointComposesScale, BOARD_FIXTURE )
833{
834 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
835
836 FOOTPRINT* fp = *m_board->Footprints().begin();
837 BOOST_REQUIRE( fp );
838
839 VECTOR2I origAnchor = fp->GetPosition();
840 VECTOR2I center( origAnchor.x + 12345, origAnchor.y - 6789 );
841
842 fp->RescaleAroundPoint( center, 2.0, 2.0 );
843 fp->RescaleAroundPoint( center, 0.5, 0.5 );
844
845 // Scale composes back to identity within float tolerance.
846 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleX(), 1.0, 1e-9 );
847 BOOST_CHECK_CLOSE( fp->GetTransform().GetScaleY(), 1.0, 1e-9 );
848
849 // Anchor returns close to the original (rounding may shift by a few IU).
850 BOOST_CHECK_MESSAGE( std::abs( fp->GetPosition().x - origAnchor.x ) <= 2
851 && std::abs( fp->GetPosition().y - origAnchor.y ) <= 2,
852 "anchor drifted: now ( " << fp->GetPosition().x << ", " << fp->GetPosition().y
853 << " ) expected ( " << origAnchor.x << ", " << origAnchor.y << " )" );
854}
855
856
857BOOST_FIXTURE_TEST_CASE( RatsnestFollowsScaledPads, BOARD_FIXTURE )
858{
859 // Ratsnest endpoints are cached at connectivity build time. After scaling
860 // the footprint, recalc must rebuild against the new pad positions.
861 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
862
863 std::shared_ptr<CONNECTIVITY_DATA> conn = m_board->GetConnectivity();
864 BOOST_REQUIRE( conn );
865
866 conn->RecalculateRatsnest();
867
868 FOOTPRINT* targetFp = nullptr;
869 PAD* targetPad = nullptr;
870
871 // Find a pad with at least one ratsnest edge.
872 for( FOOTPRINT* fp : m_board->Footprints() )
873 {
874 for( PAD* pad : fp->Pads() )
875 {
876 if( !conn->GetRatsnestForPad( pad ).empty() )
877 {
878 targetFp = fp;
879 targetPad = pad;
880 break;
881 }
882 }
883
884 if( targetPad )
885 break;
886 }
887
888 if( !targetPad )
889 {
890 BOOST_TEST_MESSAGE( "issue18 has no unconnected pads, skipping" );
891 return;
892 }
893
894 targetFp->SetTransformScale( 2.0, 2.0 );
895 conn->RecalculateRatsnest();
896
897 VECTOR2I newPadPos = targetPad->GetPosition();
898 auto edges = conn->GetRatsnestForPad( targetPad );
899 BOOST_REQUIRE( !edges.empty() );
900
901 // At least one edge must touch the new pad position.
902 bool found = false;
903
904 for( const CN_EDGE& e : edges )
905 {
906 if( e.GetSourcePos() == newPadPos || e.GetTargetPos() == newPadPos )
907 {
908 found = true;
909 break;
910 }
911 }
912
913 BOOST_CHECK_MESSAGE( found, "no ratsnest edge originates at the scaled pad position ( " << newPadPos.x << ", "
914 << newPadPos.y << " )" );
915}
916
917
918BOOST_FIXTURE_TEST_CASE( SetTransformScaleInvalidatesBoundingBoxCache, BOARD_FIXTURE )
919{
920 // After SetTransformScale, the bounding box cache must drop so the next
921 // read rebuilds from the scaled geometry.
922 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
923
924 FOOTPRINT* fp = *m_board->Footprints().begin();
925 BOOST_REQUIRE( fp );
926
927 // Prime the cache and capture original size relative to the anchor.
928 BOX2I before = fp->GetBoundingBox();
930 int origWidth = before.GetWidth();
931 int origHeight = before.GetHeight();
932 BOOST_REQUIRE( origWidth > 0 && origHeight > 0 );
933
934 fp->SetTransformScale( 2.0, 2.0 );
935
936 BOX2I after = fp->GetBoundingBox();
937
938 // Roughly 2x in each dimension. Allow 5% slop for text stroke rounding.
939 BOOST_CHECK_CLOSE( (double) after.GetWidth(), 2.0 * origWidth, 5.0 );
940 BOOST_CHECK_CLOSE( (double) after.GetHeight(), 2.0 * origHeight, 5.0 );
941
942 // Anchor stays put under SetTransformScale.
945}
946
947
948BOOST_FIXTURE_TEST_CASE( SetTransformScaleInvalidatesCourtyardCache, BOARD_FIXTURE )
949{
950 // After scale, GetCourtyard must rebuild from the scaled coords.
951 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
952
953 FOOTPRINT* fp = nullptr;
954
955 for( FOOTPRINT* candidate : m_board->Footprints() )
956 {
957 if( !candidate->GetCourtyard( F_CrtYd ).IsEmpty() )
958 {
959 fp = candidate;
960 break;
961 }
962 }
963
964 BOOST_REQUIRE_MESSAGE( fp, "no footprint with a front courtyard in issue18" );
965
966 SHAPE_POLY_SET before = fp->GetCourtyard( F_CrtYd );
967 double areaBefore = before.Area();
968 BOOST_REQUIRE( areaBefore > 0.0 );
969
970 fp->SetTransformScale( 2.0, 2.0 );
971
972 SHAPE_POLY_SET after = fp->GetCourtyard( F_CrtYd );
973 double areaAfter = after.Area();
974
975 // 2x linear scale gives 4x area. Allow 1% drift for polygon Inflate offsets.
976 BOOST_CHECK_CLOSE( areaAfter, 4.0 * areaBefore, 1.0 );
977}
978
979
980BOOST_AUTO_TEST_CASE( SetTransformScaleInvalidatesZoneFill )
981{
982 // After scale, a pre-filled zone must be marked for refill and the cached
983 // fill cleared.
984 FOOTPRINT fp( nullptr );
985 fp.SetPosition( VECTOR2I( 0, 0 ) );
986
987 ZONE* zone = new ZONE( &fp );
988
989 SHAPE_POLY_SET outline;
990 outline.NewOutline();
991 outline.Append( VECTOR2I( 0, 0 ) );
992 outline.Append( VECTOR2I( 1000000, 0 ) );
993 outline.Append( VECTOR2I( 1000000, 1000000 ) );
994 outline.Append( VECTOR2I( 0, 1000000 ) );
995 *zone->Outline() = outline;
996
997 // Pretend the zone has been filled and is up to date.
998 zone->SetIsFilled( true );
999 zone->SetNeedRefill( false );
1000
1001 fp.Add( zone, ADD_MODE::APPEND );
1002
1003 fp.SetTransformScale( 2.0, 2.0 );
1004
1005 BOOST_CHECK( zone->NeedRefill() );
1006 BOOST_CHECK( !zone->IsFilled() );
1007}
1008
1009
1010BOOST_AUTO_TEST_CASE( MultiFootprintScaleSelectionCenterIsBBoxCenter )
1011{
1012 FOOTPRINT a( nullptr );
1013 FOOTPRINT b( nullptr );
1014
1015 a.SetPosition( VECTOR2I( 0, 0 ) );
1016 b.SetPosition( VECTOR2I( 0, 0 ) );
1017
1018 PCB_SHAPE* aShape = new PCB_SHAPE( &a, SHAPE_T::RECTANGLE );
1019 aShape->SetLayer( F_SilkS );
1020 aShape->SetStart( VECTOR2I( -1000000, -1000000 ) );
1021 aShape->SetEnd( VECTOR2I( 1000000, 1000000 ) );
1022 a.Add( aShape, ADD_MODE::APPEND );
1023
1024 PCB_SHAPE* bShape = new PCB_SHAPE( &b, SHAPE_T::RECTANGLE );
1025 bShape->SetLayer( F_SilkS );
1026 bShape->SetStart( VECTOR2I( 10000000, 10000000 ) );
1027 bShape->SetEnd( VECTOR2I( 20000000, 15000000 ) );
1028 b.Add( bShape, ADD_MODE::APPEND );
1029
1030 BOX2I selBBox;
1031 selBBox.Merge( a.GetBoundingBox() );
1032 selBBox.Merge( b.GetBoundingBox() );
1033 const VECTOR2I bboxCenter = selBBox.GetCenter();
1034 const VECTOR2I anchorMean( ( a.GetPosition().x + b.GetPosition().x ) / 2,
1035 ( a.GetPosition().y + b.GetPosition().y ) / 2 );
1036
1037 BOOST_REQUIRE( bboxCenter != anchorMean );
1038
1039 a.RescaleAroundPoint( bboxCenter, 2.0, 2.0 );
1040 b.RescaleAroundPoint( bboxCenter, 2.0, 2.0 );
1041
1042 BOOST_CHECK_EQUAL( a.GetPosition().x, bboxCenter.x + 2 * ( 0 - bboxCenter.x ) );
1043 BOOST_CHECK_EQUAL( a.GetPosition().y, bboxCenter.y + 2 * ( 0 - bboxCenter.y ) );
1044 BOOST_CHECK_EQUAL( b.GetPosition().x, bboxCenter.x + 2 * ( 0 - bboxCenter.x ) );
1045 BOOST_CHECK_EQUAL( b.GetPosition().y, bboxCenter.y + 2 * ( 0 - bboxCenter.y ) );
1046}
1047
1048
1049BOOST_AUTO_TEST_CASE( MultiFootprintScaleAroundSelectionCenter )
1050{
1051 // Scale 2x around the midpoint of two footprints. Each anchor must move to
1052 // center + 2 * (orig - center).
1053 FOOTPRINT a( nullptr );
1054 FOOTPRINT b( nullptr );
1055
1056 a.SetPosition( VECTOR2I( 0, 0 ) );
1057 b.SetPosition( VECTOR2I( 1000, 500 ) );
1058
1059 VECTOR2I center( ( 0 + 1000 ) / 2, ( 0 + 500 ) / 2 );
1060
1061 auto applyOnSelection = [&]( double sx, double sy )
1062 {
1063 double relSxA = sx / a.GetScaleX();
1064 double relSyA = sy / a.GetScaleY();
1065 a.RescaleAroundPoint( center, relSxA, relSyA );
1066
1067 double relSxB = sx / b.GetScaleX();
1068 double relSyB = sy / b.GetScaleY();
1069 b.RescaleAroundPoint( center, relSxB, relSyB );
1070 };
1071
1072 applyOnSelection( 2.0, 2.0 );
1073
1074 BOOST_CHECK_EQUAL( a.GetPosition().x, center.x + 2 * ( 0 - center.x ) );
1075 BOOST_CHECK_EQUAL( a.GetPosition().y, center.y + 2 * ( 0 - center.y ) );
1076 BOOST_CHECK_EQUAL( b.GetPosition().x, center.x + 2 * ( 1000 - center.x ) );
1077 BOOST_CHECK_EQUAL( b.GetPosition().y, center.y + 2 * ( 500 - center.y ) );
1078
1079 BOOST_CHECK_CLOSE( a.GetScaleX(), 2.0, 1e-9 );
1080 BOOST_CHECK_CLOSE( a.GetScaleY(), 2.0, 1e-9 );
1081 BOOST_CHECK_CLOSE( b.GetScaleX(), 2.0, 1e-9 );
1082 BOOST_CHECK_CLOSE( b.GetScaleY(), 2.0, 1e-9 );
1083}
1084
1085
1086BOOST_FIXTURE_TEST_CASE( ScaleXSetterPreservesOtherAxis, BOARD_FIXTURE )
1087{
1088 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1089
1090 FOOTPRINT* fp = *m_board->Footprints().begin();
1091 BOOST_REQUIRE( fp );
1092
1093 fp->SetTransformScale( 1.5, 0.75 );
1094
1095 fp->SetScaleX( 3.0 );
1096
1097 BOOST_CHECK_CLOSE( fp->GetScaleX(), 3.0, 1e-9 );
1098 BOOST_CHECK_CLOSE( fp->GetScaleY(), 0.75, 1e-9 );
1100}
1101
1102
1103BOOST_FIXTURE_TEST_CASE( ScaleYSetterPreservesOtherAxis, BOARD_FIXTURE )
1104{
1105 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1106
1107 FOOTPRINT* fp = *m_board->Footprints().begin();
1108 BOOST_REQUIRE( fp );
1109
1110 fp->SetTransformScale( 1.5, 0.75 );
1111
1112 fp->SetScaleY( 4.0 );
1113
1114 BOOST_CHECK_CLOSE( fp->GetScaleX(), 1.5, 1e-9 );
1115 BOOST_CHECK_CLOSE( fp->GetScaleY(), 4.0, 1e-9 );
1117}
1118
1119
1120BOOST_FIXTURE_TEST_CASE( PromotedArcSerializesAsLibArc, BOARD_FIXTURE )
1121{
1122 // An ARC promoted to ELLIPSE_ARC by non-uniform scale must still write as fp_arc.
1123 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1124
1125 FOOTPRINT* fp = *m_board->Footprints().begin();
1126 BOOST_REQUIRE( fp );
1127
1128 VECTOR2I anchor = fp->GetPosition();
1129
1130 PCB_SHAPE* arc = new PCB_SHAPE( fp, SHAPE_T::ARC );
1131 arc->SetLayer( F_SilkS );
1132 arc->SetArcGeometry( anchor + VECTOR2I( 1000000, 0 ), anchor + VECTOR2I( 707107, 707107 ),
1133 anchor + VECTOR2I( 0, 1000000 ) );
1134 fp->Add( arc, ADD_MODE::APPEND );
1135
1136 fp->SetTransformScale( 2.0, 1.0 );
1137 BOOST_REQUIRE_EQUAL( static_cast<int>( arc->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE_ARC ) );
1138
1139 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "promoted_arc_roundtrip.kicad_pcb";
1140
1141 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1142
1143 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1144 BOOST_REQUIRE( reloaded );
1145
1146 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1147 BOOST_REQUIRE( fp2 );
1148
1149 bool foundArc = false;
1150
1151 for( const BOARD_ITEM* item : fp2->GraphicalItems() )
1152 {
1153 if( item->Type() != PCB_SHAPE_T )
1154 continue;
1155
1156 const PCB_SHAPE* s = static_cast<const PCB_SHAPE*>( item );
1157
1158 if( s->GetLibraryShape() == SHAPE_T::ARC )
1159 {
1160 foundArc = true;
1161 break;
1162 }
1163 }
1164
1165 BOOST_CHECK_MESSAGE( foundArc, "promoted arc did not round-trip as a lib-frame arc" );
1166}
1167
1168
1169BOOST_FIXTURE_TEST_CASE( PromotedCircleSerializesAsLibCircle, BOARD_FIXTURE )
1170{
1171 // A CIRCLE promoted to ELLIPSE by non-uniform scale must still write as
1172 // fp_circle. The on-disk form follows the lib frame.
1173 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1174
1175 FOOTPRINT* fp = *m_board->Footprints().begin();
1176 BOOST_REQUIRE( fp );
1177
1179 circle->SetLayer( F_SilkS );
1180 circle->SetStart( fp->GetPosition() );
1181 circle->SetEnd( fp->GetPosition() + VECTOR2I( 1000000, 0 ) );
1182 fp->Add( circle, ADD_MODE::APPEND );
1183
1184 fp->SetTransformScale( 2.0, 1.0 );
1185 BOOST_REQUIRE_EQUAL( static_cast<int>( circle->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
1186
1187 const std::filesystem::path savePath =
1188 std::filesystem::temp_directory_path() / "promoted_circle_roundtrip.kicad_pcb";
1189
1190 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1191
1192 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1193 BOOST_REQUIRE( reloaded );
1194
1195 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1196 BOOST_REQUIRE( fp2 );
1197
1198 bool foundCircle = false;
1199
1200 for( const BOARD_ITEM* item : fp2->GraphicalItems() )
1201 {
1202 if( item->Type() != PCB_SHAPE_T )
1203 continue;
1204
1205 const PCB_SHAPE* s = static_cast<const PCB_SHAPE*>( item );
1206
1207 if( s->GetLibraryShape() == SHAPE_T::CIRCLE )
1208 {
1209 foundCircle = true;
1210 break;
1211 }
1212 }
1213
1214 BOOST_CHECK_MESSAGE( foundCircle, "promoted circle did not round-trip as a lib-frame circle" );
1215}
1216
1217
1218BOOST_FIXTURE_TEST_CASE( NativeEllipseRoundTripsThroughRotatedFootprint, BOARD_FIXTURE )
1219{
1220 // Native ellipse must survive save and load with a rotated parent FP.
1221 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1222
1223 FOOTPRINT* fp = *m_board->Footprints().begin();
1224 BOOST_REQUIRE( fp );
1225
1226 fp->SetOrientation( ANGLE_0 );
1227
1228 PCB_SHAPE* ellipse = new PCB_SHAPE( fp, SHAPE_T::ELLIPSE );
1229 ellipse->SetLayer( F_SilkS );
1230 ellipse->SetEllipseCenter( fp->GetPosition() + VECTOR2I( 1000000, 0 ) );
1231 ellipse->SetEllipseMajorRadius( 500000 );
1232 ellipse->SetEllipseMinorRadius( 250000 );
1233 ellipse->SetEllipseRotation( EDA_ANGLE( 30.0, DEGREES_T ) );
1234 fp->Add( ellipse, ADD_MODE::APPEND );
1235
1236 const VECTOR2I libCenter = ellipse->GetLibraryEllipseCenter();
1237 const int libMajor = ellipse->GetLibraryEllipseMajorRadius();
1238 const int libMinor = ellipse->GetLibraryEllipseMinorRadius();
1239 const EDA_ANGLE libRotation = ellipse->GetLibraryEllipseRotation();
1240
1241 BOOST_CHECK_EQUAL( libCenter.x, 1000000 );
1242 BOOST_CHECK_EQUAL( libCenter.y, 0 );
1243 BOOST_CHECK_EQUAL( libMajor, 500000 );
1244 BOOST_CHECK_EQUAL( libMinor, 250000 );
1245 BOOST_CHECK_CLOSE( libRotation.AsDegrees(), 30.0, 0.01 );
1246
1247 fp->SetOrientation( EDA_ANGLE( 60.0, DEGREES_T ) );
1248
1249 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().x, libCenter.x );
1250 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().y, libCenter.y );
1251 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMajorRadius(), libMajor );
1252 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMinorRadius(), libMinor );
1253 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseRotation().AsDegrees(),
1254 libRotation.AsDegrees(), 0.01 );
1255
1256 const std::filesystem::path savePath =
1257 std::filesystem::temp_directory_path() / "native_ellipse_rotated_roundtrip.kicad_pcb";
1258
1259 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1260
1261 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1262 BOOST_REQUIRE( reloaded );
1263
1264 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1265 BOOST_REQUIRE( fp2 );
1266
1267 const PCB_SHAPE* ellipse2 = nullptr;
1268
1269 for( const BOARD_ITEM* item : fp2->GraphicalItems() )
1270 {
1271 if( item->Type() != PCB_SHAPE_T )
1272 continue;
1273
1274 const PCB_SHAPE* s = static_cast<const PCB_SHAPE*>( item );
1275
1276 if( s->GetLibraryShape() == SHAPE_T::ELLIPSE )
1277 {
1278 ellipse2 = s;
1279 break;
1280 }
1281 }
1282
1283 BOOST_REQUIRE_MESSAGE( ellipse2, "native ellipse not found after reload" );
1284
1285 BOOST_CHECK_EQUAL( ellipse2->GetLibraryEllipseCenter().x, libCenter.x );
1286 BOOST_CHECK_EQUAL( ellipse2->GetLibraryEllipseCenter().y, libCenter.y );
1287 BOOST_CHECK_EQUAL( ellipse2->GetLibraryEllipseMajorRadius(), libMajor );
1288 BOOST_CHECK_EQUAL( ellipse2->GetLibraryEllipseMinorRadius(), libMinor );
1289 BOOST_CHECK_CLOSE( ellipse2->GetLibraryEllipseRotation().AsDegrees(),
1290 libRotation.AsDegrees(), 0.01 );
1291}
1292
1293
1294BOOST_AUTO_TEST_CASE( NativeEllipseRebakesUnderUniformScale )
1295{
1296 FOOTPRINT fp( nullptr );
1297 fp.SetPosition( VECTOR2I( 0, 0 ) );
1298
1299 PCB_SHAPE* ellipse = new PCB_SHAPE( &fp, SHAPE_T::ELLIPSE );
1300 ellipse->SetEllipseCenter( VECTOR2I( 1000000, 0 ) );
1301 ellipse->SetEllipseMajorRadius( 500000 );
1302 ellipse->SetEllipseMinorRadius( 250000 );
1303 ellipse->SetEllipseRotation( ANGLE_0 );
1304 fp.Add( ellipse, ADD_MODE::APPEND );
1305
1306 fp.SetTransformScale( 2.0, 2.0 );
1307
1308 BOOST_CHECK_EQUAL( static_cast<int>( ellipse->GetShape() ),
1309 static_cast<int>( SHAPE_T::ELLIPSE ) );
1310 BOOST_CHECK_EQUAL( ellipse->GetEllipseCenter().x, 2000000 );
1311 BOOST_CHECK_EQUAL( ellipse->GetEllipseMajorRadius(), 1000000 );
1312 BOOST_CHECK_EQUAL( ellipse->GetEllipseMinorRadius(), 500000 );
1313
1314 // Lib values should be unchanged.
1315 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().x, 1000000 );
1316 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMajorRadius(), 500000 );
1317 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMinorRadius(), 250000 );
1318}
1319
1320
1321BOOST_FIXTURE_TEST_CASE( SizesAreStableAcrossSaveLoad, BOARD_FIXTURE )
1322{
1323 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1324
1325 FOOTPRINT* fp = *m_board->Footprints().begin();
1326 BOOST_REQUIRE( fp );
1327 BOOST_REQUIRE( !fp->Pads().empty() );
1328
1329 PAD* pad = *fp->Pads().begin();
1330 const VECTOR2I baselineSize = pad->GetSize( PADSTACK::ALL_LAYERS );
1331
1332 fp->SetTransformScale( 2.0, 1.0 );
1333 const VECTOR2I scaledSize = pad->GetSize( PADSTACK::ALL_LAYERS );
1334 BOOST_CHECK_EQUAL( scaledSize.x, baselineSize.x * 2 );
1335 BOOST_CHECK_EQUAL( scaledSize.y, baselineSize.y );
1336
1337 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "scale_size_roundtrip_a.kicad_pcb";
1338
1339 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1340 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1341 BOOST_REQUIRE( reloaded );
1342
1343 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1344 BOOST_REQUIRE( fp2 );
1345 PAD* pad2 = *fp2->Pads().begin();
1346 BOOST_CHECK_EQUAL( pad2->GetSize( PADSTACK::ALL_LAYERS ).x, scaledSize.x );
1347 BOOST_CHECK_EQUAL( pad2->GetSize( PADSTACK::ALL_LAYERS ).y, scaledSize.y );
1348
1349 const std::filesystem::path savePath2 = std::filesystem::temp_directory_path() / "scale_size_roundtrip_b.kicad_pcb";
1350
1351 KI_TEST::DumpBoardToFile( *reloaded, savePath2.string() );
1352 std::unique_ptr<BOARD> reloaded2 = KI_TEST::ReadBoardFromFileOrStream( savePath2.string() );
1353 BOOST_REQUIRE( reloaded2 );
1354
1355 FOOTPRINT* fp3 = *reloaded2->Footprints().begin();
1356 BOOST_REQUIRE( fp3 );
1357 PAD* pad3 = *fp3->Pads().begin();
1358 BOOST_CHECK_EQUAL( pad3->GetSize( PADSTACK::ALL_LAYERS ).x, scaledSize.x );
1359 BOOST_CHECK_EQUAL( pad3->GetSize( PADSTACK::ALL_LAYERS ).y, scaledSize.y );
1360}
1361
1362
1363BOOST_AUTO_TEST_CASE( PolyShapeLibMirrorDoesNotDriftOnTranslate )
1364{
1365 FOOTPRINT fp( nullptr );
1366 fp.SetPosition( VECTOR2I( 0, 0 ) );
1367
1368 PCB_SHAPE* poly = new PCB_SHAPE( &fp, SHAPE_T::POLY );
1369 SHAPE_POLY_SET pts;
1370 pts.NewOutline();
1371 pts.Append( VECTOR2I( 0, 0 ) );
1372 pts.Append( VECTOR2I( 1000000, 0 ) );
1373 pts.Append( VECTOR2I( 1000000, 1000000 ) );
1374 pts.Append( VECTOR2I( 0, 1000000 ) );
1375 poly->SetPolyShape( pts );
1376 fp.Add( poly, ADD_MODE::APPEND );
1377
1378 const VECTOR2I libStart0 = poly->GetLibraryStart();
1379 const VECTOR2I libEnd0 = poly->GetLibraryEnd();
1380
1381 fp.SetPosition( VECTOR2I( 5000000, 3000000 ) );
1382 fp.SetPosition( VECTOR2I( 10000000, 6000000 ) );
1383 fp.SetPosition( VECTOR2I( 0, 0 ) );
1384
1385 BOOST_CHECK_EQUAL( poly->GetLibraryStart().x, libStart0.x );
1386 BOOST_CHECK_EQUAL( poly->GetLibraryStart().y, libStart0.y );
1387 BOOST_CHECK_EQUAL( poly->GetLibraryEnd().x, libEnd0.x );
1388 BOOST_CHECK_EQUAL( poly->GetLibraryEnd().y, libEnd0.y );
1389}
1390
1391
1392BOOST_AUTO_TEST_CASE( PolyShapeScalesWithFootprint )
1393{
1394 FOOTPRINT fp( nullptr );
1395 fp.SetPosition( VECTOR2I( 0, 0 ) );
1396
1397 PCB_SHAPE* poly = new PCB_SHAPE( &fp, SHAPE_T::POLY );
1398 SHAPE_POLY_SET pts;
1399 pts.NewOutline();
1400 pts.Append( VECTOR2I( 0, 0 ) );
1401 pts.Append( VECTOR2I( 1000000, 0 ) );
1402 pts.Append( VECTOR2I( 1000000, 1000000 ) );
1403 pts.Append( VECTOR2I( 0, 1000000 ) );
1404 poly->SetPolyShape( pts );
1405 fp.Add( poly, ADD_MODE::APPEND );
1406
1407 fp.SetTransformScale( 2.0, 1.0 );
1408
1409 const SHAPE_POLY_SET& out = poly->GetPolyShape();
1410 BOOST_REQUIRE( out.OutlineCount() == 1 );
1411 BOOST_CHECK_EQUAL( out.Outline( 0 ).CPoint( 1 ).x, 2000000 );
1412 BOOST_CHECK_EQUAL( out.Outline( 0 ).CPoint( 2 ).x, 2000000 );
1413 BOOST_CHECK_EQUAL( out.Outline( 0 ).CPoint( 2 ).y, 1000000 );
1414}
1415
1416
1417BOOST_AUTO_TEST_CASE( BezierShapeScalesWithFootprint )
1418{
1419 FOOTPRINT fp( nullptr );
1420 fp.SetPosition( VECTOR2I( 0, 0 ) );
1421
1422 PCB_SHAPE* bez = new PCB_SHAPE( &fp, SHAPE_T::BEZIER );
1423 bez->SetStart( VECTOR2I( 0, 0 ) );
1424 bez->SetBezierC1( VECTOR2I( 1000000, 0 ) );
1425 bez->SetBezierC2( VECTOR2I( 1000000, 1000000 ) );
1426 bez->SetEnd( VECTOR2I( 2000000, 1000000 ) );
1428 fp.Add( bez, ADD_MODE::APPEND );
1429
1430 fp.SetTransformScale( 2.0, 1.0 );
1431
1432 BOOST_CHECK_EQUAL( bez->GetStart().x, 0 );
1433 BOOST_CHECK_EQUAL( bez->GetBezierC1().x, 2000000 );
1434 BOOST_CHECK_EQUAL( bez->GetBezierC2().x, 2000000 );
1435 BOOST_CHECK_EQUAL( bez->GetBezierC2().y, 1000000 );
1436 BOOST_CHECK_EQUAL( bez->GetEnd().x, 4000000 );
1437
1438 // Polyline cache must be rebuilt after scale
1439 const std::vector<VECTOR2I>& pts = bez->GetBezierPoints();
1440 BOOST_REQUIRE( pts.size() >= 2 );
1441
1442 int maxX = 0;
1443 for( const VECTOR2I& p : pts )
1444 maxX = std::max( maxX, p.x );
1445
1446 BOOST_CHECK_GT( maxX, 3000000 );
1447}
1448
1449
1450BOOST_AUTO_TEST_CASE( RectangleSurvivesScaleRotateRoundTrip )
1451{
1452 FOOTPRINT fp( nullptr );
1453 fp.SetPosition( VECTOR2I( 0, 0 ) );
1454
1455 PCB_SHAPE* rect = new PCB_SHAPE( &fp, SHAPE_T::RECTANGLE );
1456 rect->SetStart( VECTOR2I( -1000000, -500000 ) );
1457 rect->SetEnd( VECTOR2I( 1000000, 500000 ) );
1458 fp.Add( rect, ADD_MODE::APPEND );
1459
1460 const VECTOR2I libStart0 = rect->GetLibraryStart();
1461 const VECTOR2I libEnd0 = rect->GetLibraryEnd();
1462
1463 fp.SetTransformScale( 2.0, 1.0 );
1464 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
1465 fp.SetTransformScale( 3.0, 1.0 );
1466 fp.SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
1467 fp.SetTransformScale( 1.0, 1.0 );
1468
1469 BOOST_CHECK( rect->GetLibraryShape() == SHAPE_T::RECTANGLE );
1470 BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE );
1471 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStart0.x );
1472 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStart0.y );
1473 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEnd0.x );
1474 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEnd0.y );
1475 BOOST_CHECK_EQUAL( rect->GetStart().x, libStart0.x );
1476 BOOST_CHECK_EQUAL( rect->GetStart().y, libStart0.y );
1477 BOOST_CHECK_EQUAL( rect->GetEnd().x, libEnd0.x );
1478 BOOST_CHECK_EQUAL( rect->GetEnd().y, libEnd0.y );
1479}
1480
1481
1482BOOST_AUTO_TEST_CASE( RectangleSwapsDimsAt90 )
1483{
1484 FOOTPRINT fp( nullptr );
1485 fp.SetPosition( VECTOR2I( 0, 0 ) );
1486
1487 PCB_SHAPE* rect = new PCB_SHAPE( &fp, SHAPE_T::RECTANGLE );
1488 rect->SetStart( VECTOR2I( -1000000, -500000 ) );
1489 rect->SetEnd( VECTOR2I( 1000000, 500000 ) );
1490 fp.Add( rect, ADD_MODE::APPEND );
1491
1492 fp.SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
1493
1494 BOOST_CHECK( rect->GetShape() == SHAPE_T::RECTANGLE );
1495 const int w = std::abs( rect->GetEnd().x - rect->GetStart().x );
1496 const int h = std::abs( rect->GetEnd().y - rect->GetStart().y );
1497 BOOST_CHECK_EQUAL( w, 1000000 );
1498 BOOST_CHECK_EQUAL( h, 2000000 );
1499}
1500
1501
1502BOOST_AUTO_TEST_CASE( RectangleMorphsToPolyUnderNonCardinalRotation )
1503{
1504 FOOTPRINT fp( nullptr );
1505 fp.SetPosition( VECTOR2I( 0, 0 ) );
1506
1507 PCB_SHAPE* rect = new PCB_SHAPE( &fp, SHAPE_T::RECTANGLE );
1508 rect->SetStart( VECTOR2I( -1000000, -500000 ) );
1509 rect->SetEnd( VECTOR2I( 1000000, 500000 ) );
1510 fp.Add( rect, ADD_MODE::APPEND );
1511
1512 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
1513
1514 BOOST_CHECK( rect->GetLibraryShape() == SHAPE_T::RECTANGLE );
1515 BOOST_CHECK( rect->GetShape() == SHAPE_T::POLY );
1516 BOOST_CHECK_EQUAL( rect->GetPolyShape().Outline( 0 ).PointCount(), 4 );
1517 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, -1000000 );
1518 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, 1000000 );
1519}
1520
1521
1522BOOST_AUTO_TEST_CASE( RectangleSurvivesIncrementalRotateAroundExternalCenter )
1523{
1524 FOOTPRINT fp( nullptr );
1525 fp.SetPosition( VECTOR2I( 152000000, 105500000 ) );
1526
1527 PCB_SHAPE* rect = new PCB_SHAPE( &fp, SHAPE_T::RECTANGLE );
1528 rect->SetStart( fp.GetPosition() + VECTOR2I( -1680000, -950000 ) );
1529 rect->SetEnd( fp.GetPosition() + VECTOR2I( 1680000, 950000 ) );
1530 fp.Add( rect, ADD_MODE::APPEND );
1531
1532 const VECTOR2I libStart0 = rect->GetLibraryStart();
1533 const VECTOR2I libEnd0 = rect->GetLibraryEnd();
1534
1535 const VECTOR2I rotCenter( 150000000, 100000000 );
1536 fp.Rotate( rotCenter, EDA_ANGLE( 30.0, DEGREES_T ) );
1537 fp.Rotate( rotCenter, EDA_ANGLE( 30.0, DEGREES_T ) );
1538
1539 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStart0.x );
1540 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStart0.y );
1541 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEnd0.x );
1542 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEnd0.y );
1543}
1544
1545
1546BOOST_FIXTURE_TEST_CASE( NativeEllipseFlipSurvivesNonUniformScale, BOARD_FIXTURE )
1547{
1548 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1549
1550 FOOTPRINT* fp = *m_board->Footprints().begin();
1551 BOOST_REQUIRE( fp );
1552
1553 fp->SetOrientation( ANGLE_0 );
1554 fp->SetTransformScale( 1.0, 1.0 );
1555
1556 PCB_SHAPE* ellipse = new PCB_SHAPE( fp, SHAPE_T::ELLIPSE_ARC );
1557 ellipse->SetLayer( F_SilkS );
1558 ellipse->SetEllipseCenter( fp->GetPosition() + VECTOR2I( 1000000, 0 ) );
1559 ellipse->SetEllipseMajorRadius( 800000 );
1560 ellipse->SetEllipseMinorRadius( 400000 );
1561 ellipse->SetEllipseRotation( EDA_ANGLE( 30.0, DEGREES_T ) );
1562 ellipse->SetEllipseStartAngle( EDA_ANGLE( 10.0, DEGREES_T ) );
1563 ellipse->SetEllipseEndAngle( EDA_ANGLE( 100.0, DEGREES_T ) );
1564 fp->Add( ellipse, ADD_MODE::APPEND );
1565
1566 fp->SetTransformScale( 2.0, 1.0 );
1567
1568 const VECTOR2I libCenterBefore = ellipse->GetLibraryEllipseCenter();
1569 const int libMajorBefore = ellipse->GetLibraryEllipseMajorRadius();
1570 const int libMinorBefore = ellipse->GetLibraryEllipseMinorRadius();
1571 const EDA_ANGLE libRotBefore = ellipse->GetLibraryEllipseRotation();
1572 const EDA_ANGLE libStartBefore = ellipse->GetLibraryEllipseStartAngle();
1573 const EDA_ANGLE libEndBefore = ellipse->GetLibraryEllipseEndAngle();
1574
1575 ellipse->Flip( fp->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
1576
1577 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMajorRadius(), libMajorBefore );
1578 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMinorRadius(), libMinorBefore );
1579 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseRotation().AsDegrees(), ( -libRotBefore ).AsDegrees(), 1e-6 );
1580 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseStartAngle().AsDegrees(), ( ANGLE_180 - libEndBefore ).AsDegrees(),
1581 1e-6 );
1582 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseEndAngle().AsDegrees(), ( ANGLE_180 - libStartBefore ).AsDegrees(),
1583 1e-6 );
1584 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().x, -libCenterBefore.x );
1585 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().y, libCenterBefore.y );
1586
1587 ellipse->Flip( fp->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
1588
1589 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMajorRadius(), libMajorBefore );
1590 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseMinorRadius(), libMinorBefore );
1591 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseRotation().AsDegrees(), libRotBefore.AsDegrees(), 1e-6 );
1592 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseStartAngle().AsDegrees(), libStartBefore.AsDegrees(), 1e-6 );
1593 BOOST_CHECK_CLOSE( ellipse->GetLibraryEllipseEndAngle().AsDegrees(), libEndBefore.AsDegrees(), 1e-6 );
1594 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().x, libCenterBefore.x );
1595 BOOST_CHECK_EQUAL( ellipse->GetLibraryEllipseCenter().y, libCenterBefore.y );
1596}
1597
1598
1599BOOST_AUTO_TEST_CASE( NativeEllipseRebakesUnderNonUniformScaleAxisAligned )
1600{
1601 // Axis-aligned lib ellipse: non-uniform scale just stretches each axis.
1602 FOOTPRINT fp( nullptr );
1603 fp.SetPosition( VECTOR2I( 0, 0 ) );
1604
1605 PCB_SHAPE* ellipse = new PCB_SHAPE( &fp, SHAPE_T::ELLIPSE );
1606 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1607 ellipse->SetEllipseMajorRadius( 1000000 );
1608 ellipse->SetEllipseMinorRadius( 500000 );
1609 ellipse->SetEllipseRotation( ANGLE_0 );
1610 fp.Add( ellipse, ADD_MODE::APPEND );
1611
1612 fp.SetTransformScale( 2.0, 1.0 );
1613
1614 BOOST_CHECK_EQUAL( ellipse->GetEllipseMajorRadius(), 2000000 );
1615 BOOST_CHECK_EQUAL( ellipse->GetEllipseMinorRadius(), 500000 );
1616 BOOST_CHECK_SMALL( ellipse->GetEllipseRotation().AsDegrees(), 1e-6 );
1617}
1618
1619
1620BOOST_AUTO_TEST_CASE( NativeEllipseSignUnderNonUniformScaleAndParentRotation )
1621{
1622 // Lib ellipse 1 x 0.5 mm at 0 deg, parent rotated 30 deg, scaled (2, 1).
1623 // Expect 2 x 0.5 mm at -30 deg on the board.
1624 FOOTPRINT fp( nullptr );
1625 fp.SetPosition( VECTOR2I( 0, 0 ) );
1626
1627 PCB_SHAPE* ellipse = new PCB_SHAPE( &fp, SHAPE_T::ELLIPSE );
1628 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1629 ellipse->SetEllipseMajorRadius( 1000000 );
1630 ellipse->SetEllipseMinorRadius( 500000 );
1631 ellipse->SetEllipseRotation( ANGLE_0 );
1632 fp.Add( ellipse, ADD_MODE::APPEND );
1633
1634 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
1635 fp.SetTransformScale( 2.0, 1.0 );
1636
1637 BOOST_CHECK_CLOSE( static_cast<double>( ellipse->GetEllipseMajorRadius() ), 2000000.0, 0.1 );
1638 BOOST_CHECK_CLOSE( static_cast<double>( ellipse->GetEllipseMinorRadius() ), 500000.0, 0.1 );
1639
1640 // An ellipse rotated 180 deg looks the same, so normalize to (-90, 90].
1641 double rotDeg = ellipse->GetEllipseRotation().AsDegrees();
1642 while( rotDeg > 90.0 ) rotDeg -= 180.0;
1643 while( rotDeg <= -90.0 ) rotDeg += 180.0;
1644 BOOST_CHECK_CLOSE( rotDeg, -30.0, 0.1 );
1645}
1646
1647
1648BOOST_AUTO_TEST_CASE( NativeEllipseContinuousAtUniformLimit )
1649{
1650 // Bumping Sy by 5 percent from the uniform case must not jump the rotation
1651 // or the radii.
1652 FOOTPRINT fp( nullptr );
1653 fp.SetPosition( VECTOR2I( 0, 0 ) );
1654
1655 PCB_SHAPE* ellipse = new PCB_SHAPE( &fp, SHAPE_T::ELLIPSE );
1656 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1657 ellipse->SetEllipseMajorRadius( 1000000 );
1658 ellipse->SetEllipseMinorRadius( 500000 );
1659 ellipse->SetEllipseRotation( ANGLE_0 );
1660 fp.Add( ellipse, ADD_MODE::APPEND );
1661
1662 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
1663 fp.SetTransformScale( 2.0, 2.0 );
1664 const double major_u = ellipse->GetEllipseMajorRadius();
1665 const double minor_u = ellipse->GetEllipseMinorRadius();
1666 const double rot_u = ellipse->GetEllipseRotation().AsDegrees();
1667
1668 fp.SetTransformScale( 2.0, 2.1 );
1669 const double major_n = ellipse->GetEllipseMajorRadius();
1670 const double minor_n = ellipse->GetEllipseMinorRadius();
1671 const double rot_n = ellipse->GetEllipseRotation().AsDegrees();
1672
1673 BOOST_CHECK_CLOSE( major_n, major_u, 1.0 );
1674 BOOST_CHECK_CLOSE( minor_n, minor_u, 6.0 );
1675
1676 double diffDeg = rot_n - rot_u;
1677 while( diffDeg > 90.0 ) diffDeg -= 180.0;
1678 while( diffDeg <= -90.0 ) diffDeg += 180.0;
1679 BOOST_CHECK_SMALL( diffDeg, 1.0 );
1680}
1681
1682
1683BOOST_AUTO_TEST_CASE( NativeEllipseTessellatesUnderNonUniformScaleRotated )
1684{
1685 // A rotated lib ellipse under non-uniform scale is not a standard ellipse.
1686 // Tessellate to POLY while keeping lib as ELLIPSE so the round trip works.
1687 FOOTPRINT fp( nullptr );
1688 fp.SetPosition( VECTOR2I( 0, 0 ) );
1689
1690 PCB_SHAPE* ellipse = new PCB_SHAPE( &fp, SHAPE_T::ELLIPSE );
1691 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1692 ellipse->SetEllipseMajorRadius( 1000000 );
1693 ellipse->SetEllipseMinorRadius( 500000 );
1694 ellipse->SetEllipseRotation( EDA_ANGLE( 45.0, DEGREES_T ) );
1695 fp.Add( ellipse, ADD_MODE::APPEND );
1696
1697 fp.SetTransformScale( 2.0, 1.0 );
1698
1699 BOOST_CHECK_EQUAL( static_cast<int>( ellipse->GetShape() ), static_cast<int>( SHAPE_T::POLY ) );
1700 BOOST_CHECK_EQUAL( static_cast<int>( ellipse->GetLibraryShape() ),
1701 static_cast<int>( SHAPE_T::ELLIPSE ) );
1702 BOOST_REQUIRE( ellipse->IsPolyShapeValid() );
1703 BOOST_CHECK( ellipse->GetPolyShape().OutlineCount() == 1 );
1704 BOOST_CHECK( ellipse->GetPolyShape().Outline( 0 ).PointCount() > 8 );
1705
1706 // Expected bbox after Sx=2, Sy=1 on a 1 x 0.5 mm ellipse at 45 deg:
1707 // ~3.162 mm wide, ~1.581 mm tall.
1708 const BOX2I bbox = ellipse->GetPolyShape().BBox();
1709 BOOST_CHECK_CLOSE( static_cast<double>( bbox.GetWidth() ), 3162277.0, 1.0 );
1710 BOOST_CHECK_CLOSE( static_cast<double>( bbox.GetHeight() ), 1581139.0, 1.0 );
1711
1712 // Restore uniform scale, the native ellipse comes back.
1713 fp.SetTransformScale( 1.0, 1.0 );
1714
1715 BOOST_CHECK_EQUAL( static_cast<int>( ellipse->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
1716 BOOST_CHECK_EQUAL( ellipse->GetEllipseMajorRadius(), 1000000 );
1717 BOOST_CHECK_EQUAL( ellipse->GetEllipseMinorRadius(), 500000 );
1718}
1719
1720
1721// Pad primitive lib coords are pad-local and untouched by the parent FP transform, but
1722// the runtime geometry derives through the footprint scale like every other shape.
1723BOOST_AUTO_TEST_CASE( PadPrimitiveScalesRuntimeKeepsLibPadLocal )
1724{
1725 FOOTPRINT fp( nullptr );
1726 fp.SetPosition( VECTOR2I( 0, 0 ) );
1727
1728 PAD* pad = new PAD( &fp );
1729 pad->SetPosition( VECTOR2I( 0, 0 ) );
1730 fp.Add( pad, ADD_MODE::APPEND );
1731
1732 PCB_SHAPE* prim = new PCB_SHAPE( pad, SHAPE_T::CIRCLE );
1733 prim->SetStart( VECTOR2I( 0, 0 ) );
1734 prim->SetEnd( VECTOR2I( 1000000, 0 ) );
1735 pad->AddPrimitive( PADSTACK::ALL_LAYERS, prim );
1736
1737 BOOST_CHECK_EQUAL( prim->GetEnd().x, 1000000 );
1738 BOOST_CHECK_EQUAL( prim->GetLibraryEnd().x, 1000000 );
1739
1740 fp.SetTransformScale( 2.0, 2.0 );
1741
1742 BOOST_CHECK_EQUAL( prim->GetEnd().x, 2000000 ); // runtime scales
1743 BOOST_CHECK_EQUAL( prim->GetLibraryEnd().x, 1000000 ); // lib stays pad-local
1744}
1745
1746
1747BOOST_AUTO_TEST_CASE( PadPrimitiveCircleBecomesEllipseUnderNonUniformScale )
1748{
1749 FOOTPRINT fp( nullptr );
1750 fp.SetPosition( VECTOR2I( 0, 0 ) );
1751
1752 PAD* pad = new PAD( &fp );
1753 pad->SetPosition( VECTOR2I( 0, 0 ) );
1754 fp.Add( pad, ADD_MODE::APPEND );
1755
1756 PCB_SHAPE* prim = new PCB_SHAPE( pad, SHAPE_T::CIRCLE );
1757 prim->SetStart( VECTOR2I( 0, 0 ) );
1758 prim->SetEnd( VECTOR2I( 1000000, 0 ) );
1759 pad->AddPrimitive( PADSTACK::ALL_LAYERS, prim );
1760
1761 fp.SetTransformScale( 2.0, 1.0 );
1762
1763 BOOST_CHECK_EQUAL( static_cast<int>( prim->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
1764 BOOST_CHECK_EQUAL( static_cast<int>( prim->GetLibraryShape() ), static_cast<int>( SHAPE_T::CIRCLE ) );
1765}
1766
1767
1768// work_items/24732: a custom pad primitive is pad-local, so a flip must mirror it
1769// about the pad anchor regardless of where the parent footprint sits on the board.
1770BOOST_AUTO_TEST_CASE( PadPrimitiveFlipStaysPadLocal )
1771{
1772 BOARD board;
1773 FOOTPRINT* fp = new FOOTPRINT( &board );
1774 fp->SetPosition( VECTOR2I( 50000000, 30000000 ) ); // deliberately off origin
1775 board.Add( fp );
1776
1777 PAD* pad = new PAD( fp );
1779 pad->SetPosition( fp->GetPosition() );
1780 fp->Add( pad, ADD_MODE::APPEND );
1781
1782 PCB_SHAPE* prim = new PCB_SHAPE( pad, SHAPE_T::SEGMENT );
1783 prim->SetStart( VECTOR2I( 0, 0 ) );
1784 prim->SetEnd( VECTOR2I( 1000000, 500000 ) );
1785 pad->AddPrimitive( PADSTACK::ALL_LAYERS, prim );
1786
1788
1789 // Pad-local primitive mirrors about (0,0): x stays, y negates.
1790 BOOST_CHECK_EQUAL( prim->GetStart().x, 0 );
1791 BOOST_CHECK_EQUAL( prim->GetStart().y, 0 );
1792 BOOST_CHECK_EQUAL( prim->GetEnd().x, 1000000 );
1793 BOOST_CHECK_EQUAL( prim->GetEnd().y, -500000 );
1794
1795 // Effective and lib coords stay 1:1 for pad-local primitives.
1796 BOOST_CHECK_EQUAL( prim->GetLibraryEnd().x, prim->GetEnd().x );
1797 BOOST_CHECK_EQUAL( prim->GetLibraryEnd().y, prim->GetEnd().y );
1798}
1799
1800
1801BOOST_AUTO_TEST_CASE( CirclePadStaysCircularUnderNonUniformScale )
1802{
1803 FOOTPRINT fp( nullptr );
1804 fp.SetPosition( VECTOR2I( 0, 0 ) );
1805
1806 PAD* pad = new PAD( &fp );
1808 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
1809 pad->SetDrillShape( PAD_DRILL_SHAPE::CIRCLE );
1810 pad->SetDrillSize( VECTOR2I( 400000, 400000 ) );
1811 fp.Add( pad, ADD_MODE::APPEND );
1812
1813 fp.SetTransformScale( 2.0, 1.0 );
1814
1815 const VECTOR2I size = pad->GetSize( PADSTACK::ALL_LAYERS );
1816 BOOST_CHECK_EQUAL( size.x, size.y );
1817 BOOST_CHECK_EQUAL( size.x, 1500000 );
1818
1819 const VECTOR2I drill = pad->GetDrillSize();
1820 BOOST_CHECK_EQUAL( drill.x, drill.y );
1821 BOOST_CHECK_EQUAL( drill.x, 600000 );
1822}
1823
1824
1825BOOST_FIXTURE_TEST_CASE( CirclePadRoundTripsAcrossSaveLoad, BOARD_FIXTURE )
1826{
1827 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1828
1829 FOOTPRINT* fp = *m_board->Footprints().begin();
1830 BOOST_REQUIRE( fp );
1831
1832 fp->SetOrientation( ANGLE_0 );
1833 fp->SetTransformScale( 1.0, 1.0 );
1834
1835 PAD* pad = new PAD( fp );
1837 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
1838 pad->SetDrillShape( PAD_DRILL_SHAPE::CIRCLE );
1839 pad->SetDrillSize( VECTOR2I( 400000, 400000 ) );
1840 pad->SetPosition( fp->GetPosition() );
1841 pad->SetLayerSet( PAD::PTHMask() );
1842 pad->SetNumber( wxT( "88" ) );
1843 fp->Add( pad, ADD_MODE::APPEND );
1844
1845 fp->SetTransformScale( 2.0, 1.0 );
1846
1847 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "circle_pad_roundtrip.kicad_pcb";
1848
1849 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1850
1851 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1852 BOOST_REQUIRE( reloaded );
1853
1854 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1855 BOOST_REQUIRE( fp2 );
1856
1857 PAD* pad2 = nullptr;
1858
1859 for( PAD* p : fp2->Pads() )
1860 {
1861 if( p->GetNumber() == wxT( "88" ) )
1862 {
1863 pad2 = p;
1864 break;
1865 }
1866 }
1867
1868 BOOST_REQUIRE_MESSAGE( pad2, "circle pad not found after reload" );
1869
1870 const VECTOR2I size2 = pad2->GetSize( PADSTACK::ALL_LAYERS );
1871 BOOST_CHECK_EQUAL( size2.x, size2.y );
1872 BOOST_CHECK_EQUAL( size2.x, 1500000 );
1873
1874 const VECTOR2I drill2 = pad2->GetDrillSize();
1875 BOOST_CHECK_EQUAL( drill2.x, drill2.y );
1876 BOOST_CHECK_EQUAL( drill2.x, 600000 );
1877}
1878
1879
1880// Pad primitives stay pad-local across save / load. The parent FP scale does not bake
1881// into primitive coords because the pad applies its own transform when composing the
1882// effective shape.
1883BOOST_FIXTURE_TEST_CASE( PadPrimitiveRoundTripsAcrossSaveLoad, BOARD_FIXTURE )
1884{
1885 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1886
1887 FOOTPRINT* fp = *m_board->Footprints().begin();
1888 BOOST_REQUIRE( fp );
1889
1890 fp->SetOrientation( ANGLE_0 );
1891 fp->SetTransformScale( 1.0, 1.0 );
1892
1893 PAD* pad = new PAD( fp );
1895 pad->SetAnchorPadShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::CIRCLE );
1896 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 500000, 500000 ) );
1897 pad->SetPosition( fp->GetPosition() );
1898 pad->SetLayerSet( PAD::SMDMask() );
1899 pad->SetNumber( wxT( "99" ) );
1900 fp->Add( pad, ADD_MODE::APPEND );
1901
1902 PCB_SHAPE* prim = new PCB_SHAPE( pad, SHAPE_T::CIRCLE );
1903 prim->SetStart( VECTOR2I( 0, 0 ) );
1904 prim->SetEnd( VECTOR2I( 1000000, 0 ) );
1905 prim->SetWidth( 100000 );
1906 prim->SetFilled( false );
1907 pad->AddPrimitive( PADSTACK::ALL_LAYERS, prim );
1908
1909 fp->SetTransformScale( 2.0, 1.0 );
1910
1911 // Runtime morphs to ELLIPSE under the non-uniform scale, lib stays a pad-local CIRCLE.
1912 BOOST_REQUIRE_EQUAL( static_cast<int>( prim->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
1913 BOOST_REQUIRE_EQUAL( static_cast<int>( prim->GetLibraryShape() ), static_cast<int>( SHAPE_T::CIRCLE ) );
1914
1915 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "pad_prim_roundtrip.kicad_pcb";
1916
1917 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
1918
1919 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
1920 BOOST_REQUIRE( reloaded );
1921
1922 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
1923 BOOST_REQUIRE( fp2 );
1924
1925 PAD* pad2 = nullptr;
1926
1927 for( PAD* p : fp2->Pads() )
1928 {
1929 if( p->GetNumber() == wxT( "99" ) )
1930 {
1931 pad2 = p;
1932 break;
1933 }
1934 }
1935
1936 BOOST_REQUIRE_MESSAGE( pad2, "custom pad not found after reload" );
1937 BOOST_REQUIRE_EQUAL( pad2->GetPrimitives( PADSTACK::ALL_LAYERS ).size(), 1u );
1938
1939 const std::shared_ptr<PCB_SHAPE>& prim2 = pad2->GetPrimitives( PADSTACK::ALL_LAYERS ).front();
1940
1941 // The file stores pad-local lib coords, so the reloaded primitive is a CIRCLE in lib
1942 // space, re-derived to an ELLIPSE by the persisted non-uniform footprint scale.
1943 BOOST_CHECK_EQUAL( static_cast<int>( prim2->GetLibraryShape() ), static_cast<int>( SHAPE_T::CIRCLE ) );
1944 BOOST_CHECK_EQUAL( static_cast<int>( prim2->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
1945
1946 const VECTOR2I libDelta = prim2->GetLibraryEnd() - prim2->GetLibraryStart();
1947 BOOST_CHECK_EQUAL( libDelta.EuclideanNorm(), 1000000 );
1948}
1949
1950
1951// Syncing lib coords from runtime (as a GUI edit commit does) must not bake the footprint
1952// scale into a pad primitive's lib geometry, or it double-scales on the next reload.
1953BOOST_AUTO_TEST_CASE( PadPrimitiveSyncKeepsLibUnscaled )
1954{
1955 BOARD board;
1956 FOOTPRINT* fp = new FOOTPRINT( &board );
1957 board.Add( fp );
1958 fp->SetPosition( VECTOR2I( 0, 0 ) );
1959
1960 PAD* pad = new PAD( fp );
1962 pad->SetAnchorPadShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::CIRCLE );
1963 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 500000, 500000 ) );
1964 pad->SetLayerSet( PAD::SMDMask() );
1965
1966 std::vector<VECTOR2I> poly = {
1967 { -1000000, -1000000 }, { 1000000, -1000000 }, { 1000000, 1000000 }, { -1000000, 1000000 }
1968 };
1969 pad->AddPrimitivePoly( PADSTACK::ALL_LAYERS, poly, 0, true );
1970 fp->Add( pad, ADD_MODE::APPEND );
1971
1972 const std::shared_ptr<PCB_SHAPE>& prim = pad->GetPrimitives( PADSTACK::ALL_LAYERS ).front();
1973 const BOX2I libBefore = prim->GetLibraryPolyShape().BBox();
1974
1975 fp->SetTransformScale( 2.0, 1.5 );
1976
1977 // A GUI edit commit syncs lib from runtime; lib must stay pad-local (unscaled).
1978 prim->EndEdit();
1979
1980 const BOX2I libAfter = prim->GetLibraryPolyShape().BBox();
1981
1982 BOOST_CHECK_EQUAL( libAfter.GetWidth(), libBefore.GetWidth() );
1983 BOOST_CHECK_EQUAL( libAfter.GetHeight(), libBefore.GetHeight() );
1984}
1985
1986
1987// A polygon custom-pad primitive must not double-scale across save / load: the file stores
1988// pad-local lib coords, so the runtime polygon size has to match before and after a reload.
1989BOOST_FIXTURE_TEST_CASE( PadPolyPrimitiveRoundTripsAcrossSaveLoad, BOARD_FIXTURE )
1990{
1991 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
1992
1993 FOOTPRINT* fp = *m_board->Footprints().begin();
1994 BOOST_REQUIRE( fp );
1995
1996 fp->SetOrientation( ANGLE_0 );
1997 fp->SetTransformScale( 1.0, 1.0 );
1998
1999 PAD* pad = new PAD( fp );
2001 pad->SetAnchorPadShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::CIRCLE );
2002 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 500000, 500000 ) );
2003 pad->SetPosition( fp->GetPosition() );
2004 pad->SetLayerSet( PAD::SMDMask() );
2005 pad->SetNumber( wxT( "77" ) );
2006 fp->Add( pad, ADD_MODE::APPEND );
2007
2008 std::vector<VECTOR2I> poly = {
2009 { -1000000, -1000000 }, { 1000000, -1000000 }, { 1000000, 1000000 }, { -1000000, 1000000 }
2010 };
2011 pad->AddPrimitivePoly( PADSTACK::ALL_LAYERS, poly, 0, true );
2012
2013 fp->SetTransformScale( 2.0, 1.5 );
2014
2015 const BOX2I before = pad->GetBoundingBox();
2016
2017 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "pad_poly_prim_roundtrip.kicad_pcb";
2018
2019 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
2020
2021 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
2022 BOOST_REQUIRE( reloaded );
2023
2024 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
2025 BOOST_REQUIRE( fp2 );
2026
2027 PAD* pad2 = nullptr;
2028
2029 for( PAD* p : fp2->Pads() )
2030 {
2031 if( p->GetNumber() == wxT( "77" ) )
2032 {
2033 pad2 = p;
2034 break;
2035 }
2036 }
2037
2038 BOOST_REQUIRE_MESSAGE( pad2, "custom poly pad not found after reload" );
2039
2040 const BOX2I after = pad2->GetBoundingBox();
2041
2042 BOOST_CHECK_EQUAL( after.GetWidth(), before.GetWidth() );
2043 BOOST_CHECK_EQUAL( after.GetHeight(), before.GetHeight() );
2044}
2045
2046
2047BOOST_AUTO_TEST_CASE( NonUniformScalePromotesCircleToEllipse )
2048{
2049 // Non-uniform scale promotes a CIRCLE to ELLIPSE on the board side.
2050 FOOTPRINT fp( nullptr );
2051 fp.SetPosition( VECTOR2I( 0, 0 ) );
2052
2054 circle->SetStart( VECTOR2I( 0, 0 ) );
2055 circle->SetEnd( VECTOR2I( 1000000, 0 ) ); // radius 1mm
2057
2058 fp.SetTransformScale( 2.0, 1.0 );
2059
2060 BOOST_CHECK_EQUAL( static_cast<int>( circle->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
2061 BOOST_CHECK_EQUAL( circle->GetEllipseMajorRadius(), 2000000 );
2062 BOOST_CHECK_EQUAL( circle->GetEllipseMinorRadius(), 1000000 );
2063 BOOST_CHECK_EQUAL( circle->GetEllipseRotation().AsDegrees(), 0.0 );
2064 BOOST_CHECK_EQUAL( circle->GetEllipseCenter().x, 0 );
2065 BOOST_CHECK_EQUAL( circle->GetEllipseCenter().y, 0 );
2066}
2067
2068
2069BOOST_AUTO_TEST_CASE( CircleToEllipseRotatesWithParent )
2070{
2071 // Morphed ellipse rotation must follow the parent's rotation direction.
2072 FOOTPRINT fp( nullptr );
2073 fp.SetPosition( VECTOR2I( 0, 0 ) );
2074
2076 circle->SetStart( VECTOR2I( 0, 0 ) );
2077 circle->SetEnd( VECTOR2I( 1000000, 0 ) );
2079
2080 fp.SetTransformScale( 2.0, 1.0 );
2081 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
2082
2083 // Parent rotated +30 in screen frame so ellipse math rotation = -30 deg.
2084 BOOST_CHECK_CLOSE( circle->GetEllipseRotation().AsDegrees(), -30.0, 0.001 );
2085}
2086
2087
2088BOOST_AUTO_TEST_CASE( NonUniformScaleSwapsAxesWhenMinorWouldExceedMajor )
2089{
2090 // sx < sy: swap axes and rotate 90 deg so major >= minor.
2091 FOOTPRINT fp( nullptr );
2092
2094 circle->SetStart( VECTOR2I( 0, 0 ) );
2095 circle->SetEnd( VECTOR2I( 1000000, 0 ) );
2097
2098 fp.SetTransformScale( 1.0, 3.0 );
2099
2100 BOOST_CHECK_EQUAL( static_cast<int>( circle->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
2101 BOOST_CHECK_EQUAL( circle->GetEllipseMajorRadius(), 3000000 );
2102 BOOST_CHECK_EQUAL( circle->GetEllipseMinorRadius(), 1000000 );
2103 BOOST_CHECK_EQUAL( circle->GetEllipseRotation().AsDegrees(), 90.0 );
2104}
2105
2106
2107BOOST_AUTO_TEST_CASE( NonUniformScalePromotesArcToEllipseArc )
2108{
2109 // Non-uniform scale promotes ARC to ELLIPSE_ARC, axes scaled by sx/sy.
2110 FOOTPRINT fp( nullptr );
2111
2112 PCB_SHAPE* arc = new PCB_SHAPE( &fp, SHAPE_T::ARC );
2113
2114 // Quarter arc on a 1 mm circle at origin, (1,0) -> (0,1).
2115 arc->SetArcGeometry( VECTOR2I( 1000000, 0 ), VECTOR2I( 707107, 707107 ), VECTOR2I( 0, 1000000 ) );
2116 fp.Add( arc, ADD_MODE::APPEND );
2117
2118 fp.SetTransformScale( 2.0, 1.0 );
2119
2120 BOOST_CHECK_EQUAL( static_cast<int>( arc->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE_ARC ) );
2121 BOOST_CHECK_EQUAL( arc->GetEllipseMajorRadius(), 2000000 );
2122 BOOST_CHECK_EQUAL( arc->GetEllipseMinorRadius(), 1000000 );
2126 BOOST_CHECK_CLOSE( arc->GetEllipseStartAngle().AsDegrees(), 0.0, 1e-6 );
2127 BOOST_CHECK_CLOSE( arc->GetEllipseEndAngle().AsDegrees(), 90.0, 1e-6 );
2128}
2129
2130
2131BOOST_AUTO_TEST_CASE( UniformRescaleRestoresArc )
2132{
2133 // Going back to a uniform scale must restore SHAPE_T::ARC.
2134 FOOTPRINT fp( nullptr );
2135
2136 PCB_SHAPE* arc = new PCB_SHAPE( &fp, SHAPE_T::ARC );
2137 arc->SetArcGeometry( VECTOR2I( 1000000, 0 ), VECTOR2I( 707107, 707107 ), VECTOR2I( 0, 1000000 ) );
2138 fp.Add( arc, ADD_MODE::APPEND );
2139
2140 fp.SetTransformScale( 2.0, 1.0 );
2141 BOOST_REQUIRE_EQUAL( static_cast<int>( arc->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE_ARC ) );
2142
2143 fp.SetTransformScale( 2.0, 2.0 );
2144 BOOST_CHECK_EQUAL( static_cast<int>( arc->GetShape() ), static_cast<int>( SHAPE_T::ARC ) );
2145}
2146
2147
2148BOOST_AUTO_TEST_CASE( UniformRescaleRestoresCircle )
2149{
2150 // Going back to uniform scale must restore SHAPE_T::CIRCLE, not leave a
2151 // degenerate ellipse with major == minor.
2152 FOOTPRINT fp( nullptr );
2153
2155 circle->SetStart( VECTOR2I( 0, 0 ) );
2156 circle->SetEnd( VECTOR2I( 1000000, 0 ) );
2158
2159 fp.SetTransformScale( 2.0, 1.0 );
2160 BOOST_REQUIRE_EQUAL( static_cast<int>( circle->GetShape() ), static_cast<int>( SHAPE_T::ELLIPSE ) );
2161
2162 fp.SetTransformScale( 2.0, 2.0 );
2163 BOOST_CHECK_EQUAL( static_cast<int>( circle->GetShape() ), static_cast<int>( SHAPE_T::CIRCLE ) );
2164}
2165
2166
2167// Lib-frame square outline used as the zone shape for the tests below.
2168static void seedSquareLibOutline( ZONE* aZone, int aHalfSide = 1000000 )
2169{
2170 SHAPE_POLY_SET poly;
2171 poly.NewOutline();
2172 poly.Append( VECTOR2I( -aHalfSide, -aHalfSide ) );
2173 poly.Append( VECTOR2I( aHalfSide, -aHalfSide ) );
2174 poly.Append( VECTOR2I( aHalfSide, aHalfSide ) );
2175 poly.Append( VECTOR2I( -aHalfSide, aHalfSide ) );
2176 *aZone->Outline() = poly;
2177}
2178
2179
2180BOOST_AUTO_TEST_CASE( ZoneOutlineFollowsFootprintTransform )
2181{
2182 // FP with translate, rotate, and non-uniform scale. Each vertex of the
2183 // board outline must equal xform.Apply(lib vertex).
2184 FOOTPRINT fp( nullptr );
2185 fp.SetPosition( VECTOR2I( 5000000, 3000000 ) );
2186 fp.SetOrientation( EDA_ANGLE( 45.0, DEGREES_T ) );
2187
2188 ZONE* zone = new ZONE( &fp );
2189 seedSquareLibOutline( zone );
2190 fp.Add( zone, ADD_MODE::APPEND );
2191
2192 fp.SetTransformScale( 2.0, 1.5 );
2193
2194 SHAPE_POLY_SET libOutline = zone->GetLibraryOutline();
2195 SHAPE_POLY_SET boardOutline = zone->GetBoardOutline();
2196 BOOST_REQUIRE_EQUAL( libOutline.TotalVertices(), boardOutline.TotalVertices() );
2197
2198 for( int i = 0; i < libOutline.TotalVertices(); ++i )
2199 {
2200 VECTOR2I expected = fp.GetTransform().Apply( libOutline.CVertex( i ) );
2201 VECTOR2I actual = boardOutline.CVertex( i );
2202
2203 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
2204 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
2205 << actual.x << ", " << actual.y << " )" );
2206 }
2207}
2208
2209
2210BOOST_AUTO_TEST_CASE( ZoneOutlineSurvivesFootprintFlip )
2211{
2212 // Each board-frame vertex must mirror about the flip axis. Flip routes
2213 // through board->FlipLayer so the FP needs a parent board.
2214 BOARD board;
2215 FOOTPRINT* fp = new FOOTPRINT( &board );
2216 fp->SetPosition( VECTOR2I( 0, 5000000 ) );
2217
2218 ZONE* zone = new ZONE( fp );
2219 seedSquareLibOutline( zone );
2220 fp->Add( zone, ADD_MODE::APPEND );
2221 board.Add( fp );
2222
2223 SHAPE_POLY_SET before = zone->GetBoardOutline();
2224 std::vector<VECTOR2I> beforeVerts;
2225 for( int i = 0; i < before.TotalVertices(); ++i )
2226 beforeVerts.push_back( before.CVertex( i ) );
2227
2228 const VECTOR2I flipCentre = fp->GetPosition();
2229 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2230
2231 SHAPE_POLY_SET after = zone->GetBoardOutline();
2232 BOOST_REQUIRE_EQUAL( after.TotalVertices(), (int) beforeVerts.size() );
2233
2234 for( int i = 0; i < after.TotalVertices(); ++i )
2235 {
2236 VECTOR2I expected( beforeVerts[i].x, 2 * flipCentre.y - beforeVerts[i].y );
2237 VECTOR2I actual = after.CVertex( i );
2238
2239 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
2240 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
2241 << actual.x << ", " << actual.y << " )" );
2242 }
2243}
2244
2245
2246BOOST_AUTO_TEST_CASE( ZoneHitTestRespectsFootprintTransform )
2247{
2248 // HitTestForCorner / HitTestForEdge go through InverseApply, this guards
2249 // against anyone reverting that path.
2250 FOOTPRINT fp( nullptr );
2251 fp.SetPosition( VECTOR2I( 2000000, -1000000 ) );
2252 fp.SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
2253
2254 ZONE* zone = new ZONE( &fp );
2255 zone->SetIsRuleArea( true );
2256 seedSquareLibOutline( zone );
2257 fp.Add( zone, ADD_MODE::APPEND );
2258
2259 // Lib origin sits at the centre of the square outline so it's inside.
2260 VECTOR2I boardInside = fp.GetTransform().Apply( VECTOR2I( 0, 0 ) );
2261
2262 BOOST_CHECK( zone->HitTestFilledArea( F_Cu, boardInside, 0 ) );
2263
2264 // A lib vertex maps to a board corner. Corner hit-test must find it.
2265 VECTOR2I boardCorner = fp.GetTransform().Apply( VECTOR2I( 1000000, 1000000 ) );
2266 BOOST_CHECK( zone->HitTestForCorner( boardCorner, pcbIUScale.mmToIU( 0.1 ) ) );
2267}
2268
2269
2270BOOST_AUTO_TEST_CASE( ZoneMoveTranslatesInBoardFrame )
2271{
2272 // ZONE::Move takes a board-frame offset. For an FP-child zone, the
2273 // offset must land as a board-frame shift on every vertex.
2274 FOOTPRINT fp( nullptr );
2275 fp.SetPosition( VECTOR2I( 1000000, 0 ) );
2276 fp.SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2277
2278 ZONE* zone = new ZONE( &fp );
2279 seedSquareLibOutline( zone );
2280 fp.Add( zone, ADD_MODE::APPEND );
2281
2282 SHAPE_POLY_SET before = zone->GetBoardOutline();
2283 std::vector<VECTOR2I> beforeVerts;
2284 for( int i = 0; i < before.TotalVertices(); ++i )
2285 beforeVerts.push_back( before.CVertex( i ) );
2286
2287 const VECTOR2I offset( 500000, 250000 );
2288 zone->Move( offset );
2289
2290 SHAPE_POLY_SET after = zone->GetBoardOutline();
2291 BOOST_REQUIRE_EQUAL( after.TotalVertices(), (int) beforeVerts.size() );
2292
2293 for( int i = 0; i < after.TotalVertices(); ++i )
2294 {
2295 VECTOR2I expected = beforeVerts[i] + offset;
2296 VECTOR2I actual = after.CVertex( i );
2297
2298 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
2299 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
2300 << actual.x << ", " << actual.y << " )" );
2301 }
2302}
2303
2304
2305BOOST_AUTO_TEST_CASE( ZoneRotateAboutBoardCenter )
2306{
2307 // ZONE::Rotate takes a board-frame center and angle. Every vertex of the
2308 // board outline must rotate about the supplied center.
2309 FOOTPRINT fp( nullptr );
2310 fp.SetPosition( VECTOR2I( 3000000, 0 ) );
2311
2312 ZONE* zone = new ZONE( &fp );
2313 seedSquareLibOutline( zone );
2314 fp.Add( zone, ADD_MODE::APPEND );
2315
2316 SHAPE_POLY_SET before = zone->GetBoardOutline();
2317 std::vector<VECTOR2I> beforeVerts;
2318 for( int i = 0; i < before.TotalVertices(); ++i )
2319 beforeVerts.push_back( before.CVertex( i ) );
2320
2321 const VECTOR2I centre( 0, 0 );
2322 const EDA_ANGLE angle( 90.0, DEGREES_T );
2323 zone->Rotate( centre, angle );
2324
2325 SHAPE_POLY_SET after = zone->GetBoardOutline();
2326 BOOST_REQUIRE_EQUAL( after.TotalVertices(), (int) beforeVerts.size() );
2327
2328 for( int i = 0; i < after.TotalVertices(); ++i )
2329 {
2330 VECTOR2I expected = beforeVerts[i];
2331 RotatePoint( expected, centre, angle );
2332 VECTOR2I actual = after.CVertex( i );
2333
2334 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
2335 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
2336 << actual.x << ", " << actual.y << " )" );
2337 }
2338}
2339
2340
2341BOOST_AUTO_TEST_CASE( ZoneBoundingBoxIsBoardFrameForFPChild )
2342{
2343 // Bbox must come out in board frame, not lib frame.
2344 FOOTPRINT fp( nullptr );
2345 fp.SetPosition( VECTOR2I( 50000000, 30000000 ) );
2346
2347 ZONE* zone = new ZONE( &fp );
2348 seedSquareLibOutline( zone );
2349 fp.Add( zone, ADD_MODE::APPEND );
2350
2351 BOX2I bbox = zone->GetBoundingBox();
2352
2353 BOOST_CHECK_MESSAGE( bbox.GetCenter().x > 49000000 && bbox.GetCenter().x < 51000000 && bbox.GetCenter().y > 29000000
2354 && bbox.GetCenter().y < 31000000,
2355 "bbox centre ( " << bbox.GetCenter().x << ", " << bbox.GetCenter().y
2356 << " ) expected near board ( 50000000, 30000000 )" );
2357}
2358
2359
2360BOOST_AUTO_TEST_CASE( CachedZoneBoundingBoxIsBoardFrameForFPChild )
2361{
2362 // Footprint-owned zone: the cached fast-path box must be the board-frame outline, not the
2363 // raw lib-frame outline, or DRC/culling gets a box at the wrong place.
2364 BOARD board;
2365
2366 FOOTPRINT* fp = new FOOTPRINT( &board );
2367 fp->SetPosition( VECTOR2I( 50000000, 30000000 ) );
2368 board.Add( fp, ADD_MODE::APPEND );
2369
2370 ZONE* zone = new ZONE( fp );
2371 seedSquareLibOutline( zone );
2372 fp->Add( zone, ADD_MODE::APPEND );
2373
2374 zone->CacheBoundingBox();
2375 BOX2I bbox = zone->GetBoundingBox();
2376
2377 BOOST_CHECK_MESSAGE( bbox.GetCenter().x > 49000000 && bbox.GetCenter().x < 51000000
2378 && bbox.GetCenter().y > 29000000 && bbox.GetCenter().y < 31000000,
2379 "cached bbox centre ( " << bbox.GetCenter().x << ", " << bbox.GetCenter().y
2380 << " ) expected near board ( 50000000, 30000000 )" );
2381}
2382
2383
2384BOOST_AUTO_TEST_CASE( ZoneSmoothedPolyForFPRuleAreaIsBoardFrame )
2385{
2386 // Keepout knockout source must be board frame so it carves at the FP position.
2387 FOOTPRINT fp( nullptr );
2388 fp.SetPosition( VECTOR2I( 50000000, 30000000 ) );
2389
2390 ZONE* zone = new ZONE( &fp );
2391 zone->SetIsRuleArea( true );
2392 seedSquareLibOutline( zone );
2393 fp.Add( zone, ADD_MODE::APPEND );
2394
2395 SHAPE_POLY_SET smoothed;
2396 zone->TransformSmoothedOutlineToPolygon( smoothed, 0, ARC_HIGH_DEF, ERROR_OUTSIDE, nullptr );
2397
2398 BOX2I bbox = smoothed.BBox();
2399 BOOST_CHECK_MESSAGE( bbox.GetCenter().x > 49000000 && bbox.GetCenter().x < 51000000 && bbox.GetCenter().y > 29000000
2400 && bbox.GetCenter().y < 31000000,
2401 "smoothed-poly centre ( " << bbox.GetCenter().x << ", " << bbox.GetCenter().y
2402 << " ) expected near board ( 50000000, 30000000 )" );
2403}
2404
2405
2406BOOST_AUTO_TEST_CASE( ZoneEffectiveShapeForFPRuleAreaIsBoardFrame )
2407{
2408 // DRC reads this shape, it must be board frame.
2409 FOOTPRINT fp( nullptr );
2410 fp.SetPosition( VECTOR2I( 50000000, 30000000 ) );
2411
2412 ZONE* zone = new ZONE( &fp );
2413 zone->SetIsRuleArea( true );
2414 seedSquareLibOutline( zone );
2415 fp.Add( zone, ADD_MODE::APPEND );
2416
2417 std::shared_ptr<SHAPE> shape = zone->GetEffectiveShape( F_Cu );
2418 BOOST_REQUIRE( shape );
2419
2420 BOX2I bbox = shape->BBox();
2421 BOOST_CHECK_MESSAGE( bbox.GetCenter().x > 49000000 && bbox.GetCenter().x < 51000000 && bbox.GetCenter().y > 29000000
2422 && bbox.GetCenter().y < 31000000,
2423 "effective shape centre ( " << bbox.GetCenter().x << ", " << bbox.GetCenter().y
2424 << " ) expected near board ( 50000000, 30000000 )" );
2425}
2426
2427
2428BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildRectUnderRotation )
2429{
2430 // Double flip must restore the rectangle, including under FP rotation.
2431 BOARD board;
2432 FOOTPRINT* fp = new FOOTPRINT( &board );
2433 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
2434 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2435 board.Add( fp );
2436
2437 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
2438 rect->SetStart( VECTOR2I( 1000000, 1000000 ) );
2439 rect->SetEnd( VECTOR2I( 5000000, 3000000 ) );
2440 fp->Add( rect, ADD_MODE::APPEND );
2441
2442 VECTOR2I libStartBefore = rect->GetLibraryStart();
2443 VECTOR2I libEndBefore = rect->GetLibraryEnd();
2444
2445 const VECTOR2I flipCentre = fp->GetPosition();
2446 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2447
2448 BOOST_CHECK_MESSAGE( rect->GetLibraryStart() != libStartBefore || rect->GetLibraryEnd() != libEndBefore,
2449 "lib coords did not change after one flip" );
2450
2451 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2452
2453 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStartBefore.x );
2454 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStartBefore.y );
2455 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEndBefore.x );
2456 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEndBefore.y );
2457}
2458
2459
2460BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildSegmentUnderRotation )
2461{
2462 // Double flip must restore the segment endpoints.
2463 BOARD board;
2464 FOOTPRINT* fp = new FOOTPRINT( &board );
2465 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
2466 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2467 board.Add( fp );
2468
2469 PCB_SHAPE* seg = new PCB_SHAPE( fp, SHAPE_T::SEGMENT );
2470 seg->SetStart( VECTOR2I( 1000000, 1000000 ) );
2471 seg->SetEnd( VECTOR2I( 5000000, 3000000 ) );
2472 fp->Add( seg, ADD_MODE::APPEND );
2473
2474 VECTOR2I libStartBefore = seg->GetLibraryStart();
2475 VECTOR2I libEndBefore = seg->GetLibraryEnd();
2476
2477 const VECTOR2I flipCentre = fp->GetPosition();
2478 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2479
2480 BOOST_CHECK_MESSAGE( seg->GetLibraryStart() != libStartBefore || seg->GetLibraryEnd() != libEndBefore,
2481 "lib coords did not change after one flip" );
2482
2483 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2484
2485 BOOST_CHECK_EQUAL( seg->GetLibraryStart().x, libStartBefore.x );
2486 BOOST_CHECK_EQUAL( seg->GetLibraryStart().y, libStartBefore.y );
2487 BOOST_CHECK_EQUAL( seg->GetLibraryEnd().x, libEndBefore.x );
2488 BOOST_CHECK_EQUAL( seg->GetLibraryEnd().y, libEndBefore.y );
2489}
2490
2491
2492BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildCircleUnderRotation )
2493{
2494 // Double flip must restore the circle.
2495 BOARD board;
2496 FOOTPRINT* fp = new FOOTPRINT( &board );
2497 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
2498 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2499 board.Add( fp );
2500
2502 circle->SetStart( VECTOR2I( 2000000, 2000000 ) );
2503 circle->SetEnd( VECTOR2I( 3000000, 2000000 ) );
2504 fp->Add( circle, ADD_MODE::APPEND );
2505
2506 VECTOR2I libStartBefore = circle->GetLibraryStart();
2507 VECTOR2I libEndBefore = circle->GetLibraryEnd();
2508
2509 const VECTOR2I flipCentre = fp->GetPosition();
2510 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2511
2512 BOOST_CHECK_MESSAGE( circle->GetLibraryStart() != libStartBefore || circle->GetLibraryEnd() != libEndBefore,
2513 "lib coords did not change after one flip" );
2514
2515 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2516
2517 BOOST_CHECK_EQUAL( circle->GetLibraryStart().x, libStartBefore.x );
2518 BOOST_CHECK_EQUAL( circle->GetLibraryStart().y, libStartBefore.y );
2519 BOOST_CHECK_EQUAL( circle->GetLibraryEnd().x, libEndBefore.x );
2520 BOOST_CHECK_EQUAL( circle->GetLibraryEnd().y, libEndBefore.y );
2521}
2522
2523
2524BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildArcUnderRotation )
2525{
2526 // Double flip must restore arc endpoints and mid.
2527 BOARD board;
2528 FOOTPRINT* fp = new FOOTPRINT( &board );
2529 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
2530 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2531 board.Add( fp );
2532
2533 PCB_SHAPE* arc = new PCB_SHAPE( fp, SHAPE_T::ARC );
2534 arc->SetArcGeometry( VECTOR2I( 1000000, 0 ), VECTOR2I( 707107, 707107 ), VECTOR2I( 0, 1000000 ) );
2535 fp->Add( arc, ADD_MODE::APPEND );
2536
2537 VECTOR2I libStartBefore = arc->GetLibraryStart();
2538 VECTOR2I libEndBefore = arc->GetLibraryEnd();
2539 VECTOR2I libMidBefore = arc->GetLibraryArcMid();
2540
2541 const VECTOR2I flipCentre = fp->GetPosition();
2542 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2543
2544 BOOST_CHECK_MESSAGE( arc->GetLibraryStart() != libStartBefore || arc->GetLibraryEnd() != libEndBefore
2545 || arc->GetLibraryArcMid() != libMidBefore,
2546 "lib coords did not change after one flip" );
2547
2548 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2549
2550 BOOST_CHECK_EQUAL( arc->GetLibraryStart().x, libStartBefore.x );
2551 BOOST_CHECK_EQUAL( arc->GetLibraryStart().y, libStartBefore.y );
2552 BOOST_CHECK_EQUAL( arc->GetLibraryEnd().x, libEndBefore.x );
2553 BOOST_CHECK_EQUAL( arc->GetLibraryEnd().y, libEndBefore.y );
2554 BOOST_CHECK_EQUAL( arc->GetLibraryArcMid().x, libMidBefore.x );
2555 BOOST_CHECK_EQUAL( arc->GetLibraryArcMid().y, libMidBefore.y );
2556}
2557
2558
2559BOOST_AUTO_TEST_CASE( FootprintFlipPreservesPCBTextUnderRotation )
2560{
2561 BOARD board;
2562 FOOTPRINT* fp = new FOOTPRINT( &board );
2563 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
2564 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2565 board.Add( fp );
2566
2567 PCB_TEXT* text = new PCB_TEXT( fp );
2568 text->SetText( wxT( "X" ) );
2569 text->SetTextPos( VECTOR2I( 2000000, 1000000 ) );
2570 fp->Add( text, ADD_MODE::APPEND );
2571
2572 VECTOR2I boardPosBefore = text->GetTextPos();
2573
2574 const VECTOR2I flipCentre = fp->GetPosition();
2575 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2576
2577 BOOST_CHECK_MESSAGE( text->GetTextPos() != boardPosBefore, "text position did not change after one flip" );
2578
2579 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
2580
2581 BOOST_CHECK_EQUAL( text->GetTextPos().x, boardPosBefore.x );
2582 BOOST_CHECK_EQUAL( text->GetTextPos().y, boardPosBefore.y );
2583}
2584
2585
2586BOOST_FIXTURE_TEST_CASE( TableInRotatedFootprintSurvivesSaveLoad, BOARD_FIXTURE )
2587{
2588 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
2589
2590 FOOTPRINT* fp = *m_board->Footprints().begin();
2591 BOOST_REQUIRE( fp );
2592
2593 PCB_TABLE* table = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
2594 table->SetColCount( 2 );
2595
2596 const int colW = pcbIUScale.mmToIU( 20.0 );
2597 const int rowH = pcbIUScale.mmToIU( 5.0 );
2598 table->SetColWidth( 0, colW );
2599 table->SetColWidth( 1, colW );
2600 table->SetRowHeight( 0, rowH );
2601 table->SetRowHeight( 1, rowH );
2602
2603 for( int i = 0; i < 4; ++i )
2604 {
2605 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
2606 cell->SetStart( VECTOR2I( ( i % 2 ) * colW, ( i / 2 ) * rowH ) );
2607 cell->SetEnd( VECTOR2I( ( i % 2 + 1 ) * colW, ( i / 2 + 1 ) * rowH ) );
2608 cell->SetText( wxString::Format( "c%d", i ) );
2609 table->AddCell( cell );
2610 }
2611
2612 fp->Add( table, ADD_MODE::APPEND );
2613
2614 fp->SetPosition( VECTOR2I( 12345678, -7654321 ) );
2615
2616 const double angles[] = { 90.0, 180.0, 270.0 };
2617
2618 for( double deg : angles )
2619 {
2620 BOOST_TEST_CONTEXT( "FP orient " << deg )
2621 {
2622 fp->SetOrientation( EDA_ANGLE( deg, DEGREES_T ) );
2623
2624 std::vector<VECTOR2I> startsBefore, endsBefore;
2625 for( PCB_TABLECELL* cell : table->GetCells() )
2626 {
2627 startsBefore.push_back( cell->GetStart() );
2628 endsBefore.push_back( cell->GetEnd() );
2629 }
2630
2631 const std::filesystem::path savePath =
2632 std::filesystem::temp_directory_path() / "table_in_rotated_fp.kicad_pcb";
2633
2634 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
2635 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
2636 BOOST_REQUIRE( reloaded );
2637
2638 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
2639 BOOST_REQUIRE( fp2 );
2640
2641 PCB_TABLE* table2 = nullptr;
2642 for( BOARD_ITEM* item : fp2->GraphicalItems() )
2643 {
2644 if( PCB_TABLE* t = dynamic_cast<PCB_TABLE*>( item ) )
2645 {
2646 table2 = t;
2647 break;
2648 }
2649 }
2650
2651 BOOST_REQUIRE( table2 );
2652 BOOST_REQUIRE_EQUAL( (int) table2->GetCells().size(), 4 );
2653
2654 for( int i = 0; i < 4; ++i )
2655 {
2656 PCB_TABLECELL* cell = table2->GetCells()[i];
2657 BOOST_CHECK_MESSAGE( std::abs( cell->GetStart().x - startsBefore[i].x ) <= 1
2658 && std::abs( cell->GetStart().y - startsBefore[i].y ) <= 1,
2659 "cell " << i << " start ( " << cell->GetStart().x << ", " << cell->GetStart().y
2660 << " ) expected ( " << startsBefore[i].x << ", " << startsBefore[i].y
2661 << " )" );
2662 BOOST_CHECK_MESSAGE( std::abs( cell->GetEnd().x - endsBefore[i].x ) <= 1
2663 && std::abs( cell->GetEnd().y - endsBefore[i].y ) <= 1,
2664 "cell " << i << " end ( " << cell->GetEnd().x << ", " << cell->GetEnd().y
2665 << " ) expected ( " << endsBefore[i].x << ", " << endsBefore[i].y
2666 << " )" );
2667 }
2668 }
2669 }
2670}
2671
2672
2673BOOST_FIXTURE_TEST_CASE( TableRotatedInFootprintThenFootprintRotatedRoundTrips, BOARD_FIXTURE )
2674{
2675 struct Case
2676 {
2677 VECTOR2I pos;
2678 double angle;
2679 };
2680
2681 const std::vector<Case> cases = {
2682 { VECTOR2I( 12345678, -7654321 ), 0.0 }, { VECTOR2I( 12345678, -7654321 ), 90.0 },
2683 { VECTOR2I( 12345678, -7654321 ), 180.0 }, { VECTOR2I( 12345678, -7654321 ), 270.0 },
2684 { VECTOR2I( -12345678, -7654321 ), 90.0 }, { VECTOR2I( -12345678, 7654321 ), 90.0 },
2685 { VECTOR2I( 12345678, 7654321 ), 90.0 }, { VECTOR2I( 12345678, 7654321 ), 270.0 },
2686 { VECTOR2I( -12345678, -7654321 ), 270.0 },
2687 };
2688
2689 for( const Case& tc : cases )
2690 {
2691 BOOST_TEST_CONTEXT( "fp pos ( " << tc.pos.x << ", " << tc.pos.y << " ) orient " << tc.angle )
2692 {
2693 const double fpAngle = tc.angle;
2694 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
2695
2696 FOOTPRINT* fp = *m_board->Footprints().begin();
2697 BOOST_REQUIRE( fp );
2698
2699 fp->SetPosition( VECTOR2I( 0, 0 ) );
2700 fp->SetOrientation( ANGLE_0 );
2701
2702 PCB_TABLE* table = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
2703 table->SetColCount( 2 );
2704
2705 const int colW = pcbIUScale.mmToIU( 20.0 );
2706 const int rowH = pcbIUScale.mmToIU( 5.0 );
2707 table->SetColWidth( 0, colW );
2708 table->SetColWidth( 1, colW );
2709 table->SetRowHeight( 0, rowH );
2710 table->SetRowHeight( 1, rowH );
2711
2712 for( int i = 0; i < 4; ++i )
2713 {
2714 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
2715 cell->SetStart( VECTOR2I( ( i % 2 ) * colW, ( i / 2 ) * rowH ) );
2716 cell->SetEnd( VECTOR2I( ( i % 2 + 1 ) * colW, ( i / 2 + 1 ) * rowH ) );
2717 cell->SetText( wxString::Format( "c%d", i ) );
2718 table->AddCell( cell );
2719 }
2720
2721 fp->Add( table, ADD_MODE::APPEND );
2722
2723 table->Rotate( VECTOR2I( 0, 0 ), EDA_ANGLE( 90.0, DEGREES_T ) );
2724
2725 fp->SetPosition( tc.pos );
2726 fp->SetOrientation( EDA_ANGLE( fpAngle, DEGREES_T ) );
2727
2728 std::vector<VECTOR2I> startsBefore, endsBefore;
2729 std::vector<double> anglesBefore;
2730 for( PCB_TABLECELL* cell : table->GetCells() )
2731 {
2732 startsBefore.push_back( cell->GetStart() );
2733 endsBefore.push_back( cell->GetEnd() );
2734 anglesBefore.push_back( cell->GetTextAngle().AsDegrees() );
2735 }
2736
2737 const std::filesystem::path savePath =
2738 std::filesystem::temp_directory_path() / "table_rotated_in_fp_then_fp_rotated.kicad_pcb";
2739
2740 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
2741 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
2742 BOOST_REQUIRE( reloaded );
2743
2744 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
2745 BOOST_REQUIRE( fp2 );
2746
2747 PCB_TABLE* table2 = nullptr;
2748 for( BOARD_ITEM* item : fp2->GraphicalItems() )
2749 {
2750 if( PCB_TABLE* t = dynamic_cast<PCB_TABLE*>( item ) )
2751 {
2752 table2 = t;
2753 break;
2754 }
2755 }
2756
2757 BOOST_REQUIRE( table2 );
2758 BOOST_REQUIRE_EQUAL( (int) table2->GetCells().size(), 4 );
2759
2760 for( int i = 0; i < 4; ++i )
2761 {
2762 PCB_TABLECELL* cell = table2->GetCells()[i];
2763 BOOST_CHECK_MESSAGE( std::abs( cell->GetStart().x - startsBefore[i].x ) <= 1
2764 && std::abs( cell->GetStart().y - startsBefore[i].y ) <= 1,
2765 "cell " << i << " start ( " << cell->GetStart().x << ", " << cell->GetStart().y
2766 << " ) expected ( " << startsBefore[i].x << ", " << startsBefore[i].y
2767 << " )" );
2768 BOOST_CHECK_MESSAGE( std::abs( cell->GetEnd().x - endsBefore[i].x ) <= 1
2769 && std::abs( cell->GetEnd().y - endsBefore[i].y ) <= 1,
2770 "cell " << i << " end ( " << cell->GetEnd().x << ", " << cell->GetEnd().y
2771 << " ) expected ( " << endsBefore[i].x << ", " << endsBefore[i].y
2772 << " )" );
2773 BOOST_CHECK_CLOSE( cell->GetTextAngle().AsDegrees(), anglesBefore[i], 1e-6 );
2774 }
2775 }
2776 }
2777}
2778
2779
2780BOOST_AUTO_TEST_CASE( TableInRotatedFootprintCellsAreVisuallyRotated )
2781{
2782 BOARD board;
2783 FOOTPRINT* fp = new FOOTPRINT( &board );
2784 fp->SetPosition( VECTOR2I( 0, 0 ) );
2785 board.Add( fp );
2786
2787 PCB_TABLE* table = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
2788 table->SetColCount( 2 );
2789
2790 const int colW = pcbIUScale.mmToIU( 20.0 );
2791 const int rowH = pcbIUScale.mmToIU( 5.0 );
2792 table->SetColWidth( 0, colW );
2793 table->SetColWidth( 1, colW );
2794 table->SetRowHeight( 0, rowH );
2795 table->SetRowHeight( 1, rowH );
2796
2797 for( int i = 0; i < 4; ++i )
2798 {
2799 PCB_TABLECELL* cell = new PCB_TABLECELL( &board );
2800 cell->SetStart( VECTOR2I( ( i % 2 ) * colW, ( i / 2 ) * rowH ) );
2801 cell->SetEnd( VECTOR2I( ( i % 2 + 1 ) * colW, ( i / 2 + 1 ) * rowH ) );
2802 table->AddCell( cell );
2803 }
2804
2805 table->Normalize();
2806 fp->Add( table, ADD_MODE::APPEND );
2807
2808 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
2809
2810 for( PCB_TABLECELL* cell : table->GetCells() )
2811 {
2812 std::vector<VECTOR2I> corners = cell->GetCorners();
2813 BOOST_REQUIRE_EQUAL( corners.size(), 4u );
2814
2815 BOX2I visual;
2816 for( const VECTOR2I& p : corners )
2817 visual.Merge( p );
2818
2819 BOOST_CHECK_MESSAGE( std::abs( visual.GetWidth() - rowH ) <= pcbIUScale.mmToIU( 0.2 ),
2820 "visual width " << visual.GetWidth() << " expected " << rowH );
2821 BOOST_CHECK_MESSAGE( std::abs( visual.GetHeight() - colW ) <= pcbIUScale.mmToIU( 0.2 ),
2822 "visual height " << visual.GetHeight() << " expected " << colW );
2823 }
2824}
2825
2826
2827BOOST_AUTO_TEST_CASE( TableRotates90Cleanly )
2828{
2829 BOARD board;
2830 PCB_TABLE* table = new PCB_TABLE( &board, pcbIUScale.mmToIU( 0.1 ) );
2831 table->SetColCount( 2 );
2832
2833 const int colW = pcbIUScale.mmToIU( 20.0 );
2834 const int rowH = pcbIUScale.mmToIU( 5.0 );
2835 table->SetColWidth( 0, colW );
2836 table->SetColWidth( 1, colW );
2837 table->SetRowHeight( 0, rowH );
2838 table->SetRowHeight( 1, rowH );
2839
2840 for( int i = 0; i < 4; ++i )
2841 {
2842 PCB_TABLECELL* cell = new PCB_TABLECELL( &board );
2843 cell->SetStart( VECTOR2I( ( i % 2 ) * colW, ( i / 2 ) * rowH ) );
2844 cell->SetEnd( VECTOR2I( ( i % 2 + 1 ) * colW, ( i / 2 + 1 ) * rowH ) );
2845 table->AddCell( cell );
2846 }
2847
2848 table->Normalize();
2849 board.Add( table );
2850
2851 table->Rotate( VECTOR2I( 0, 0 ), EDA_ANGLE( 90.0, DEGREES_T ) );
2852
2853 std::set<std::pair<int, int>> centers;
2854 for( PCB_TABLECELL* cell : table->GetCells() )
2855 {
2856 int w = std::abs( cell->GetEnd().x - cell->GetStart().x );
2857 int h = std::abs( cell->GetEnd().y - cell->GetStart().y );
2858
2859 BOOST_CHECK_MESSAGE( ( w == colW && h == rowH ) || ( w == rowH && h == colW ),
2860 "cell dims " << w << " x " << h << " expected " << colW << "x" << rowH << " or rotated" );
2861
2862 std::vector<VECTOR2I> corners = cell->GetCorners();
2863 BOOST_REQUIRE_EQUAL( corners.size(), 4u );
2864
2865 BOX2I visual;
2866 for( const VECTOR2I& p : corners )
2867 visual.Merge( p );
2868
2869 BOOST_CHECK_MESSAGE( std::abs( visual.GetWidth() - rowH ) <= pcbIUScale.mmToIU( 0.2 ),
2870 "visual width " << visual.GetWidth() << " expected " << rowH );
2871 BOOST_CHECK_MESSAGE( std::abs( visual.GetHeight() - colW ) <= pcbIUScale.mmToIU( 0.2 ),
2872 "visual height " << visual.GetHeight() << " expected " << colW );
2873
2874 VECTOR2I vc = visual.GetCenter();
2875 centers.insert( { vc.x, vc.y } );
2876 }
2877
2878 BOOST_CHECK_MESSAGE( centers.size() == 4, "cells overlapped, got " << centers.size() << " unique centers" );
2879}
2880
2881
2882BOOST_AUTO_TEST_CASE( TextBoxInFootprintKeepsRectangleShape )
2883{
2884 BOARD board;
2885 FOOTPRINT* fp = new FOOTPRINT( &board );
2886 fp->SetPosition( VECTOR2I( 0, 0 ) );
2887 board.Add( fp );
2888
2889 PCB_TEXTBOX* tb = new PCB_TEXTBOX( fp );
2890 tb->SetText( wxT( "Hello" ) );
2891 tb->SetStart( VECTOR2I( 0, 0 ) );
2892 tb->SetEnd( VECTOR2I( 20000000, 5000000 ) );
2893 fp->Add( tb, ADD_MODE::APPEND );
2894
2895 int origWidth = tb->GetEnd().x - tb->GetStart().x;
2896 int origHeight = tb->GetEnd().y - tb->GetStart().y;
2897
2898 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
2899
2901 "lib shape changed to " << static_cast<int>( tb->GetLibraryShape() ) );
2903 "runtime shape changed to " << static_cast<int>( tb->GetShape() ) );
2904
2905 int newWidth = tb->GetEnd().x - tb->GetStart().x;
2906 int newHeight = tb->GetEnd().y - tb->GetStart().y;
2907
2908 BOOST_CHECK_MESSAGE( newWidth == origWidth, "width changed from " << origWidth << " to " << newWidth );
2909 BOOST_CHECK_MESSAGE( newHeight == origHeight, "height changed from " << origHeight << " to " << newHeight );
2910}
2911
2912
2913BOOST_FIXTURE_TEST_CASE( PCBTextInRotatedFootprintSurvivesSaveLoad, BOARD_FIXTURE )
2914{
2915 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
2916
2917 FOOTPRINT* fp = *m_board->Footprints().begin();
2918 BOOST_REQUIRE( fp );
2919
2920 fp->SetPosition( VECTOR2I( 12345678, -7654321 ) );
2921 fp->SetOrientation( EDA_ANGLE( 37.5, DEGREES_T ) );
2922
2923 PCB_TEXT* text = new PCB_TEXT( fp );
2924 text->SetText( wxT( "drift probe" ) );
2925 text->SetTextPos( fp->GetPosition() + VECTOR2I( 2000000, 1000000 ) );
2926 text->SetTextAngle( EDA_ANGLE( 30.0, DEGREES_T ) );
2927 text->SetLayer( F_SilkS );
2928 fp->Add( text, ADD_MODE::APPEND );
2929
2930 VECTOR2I posBefore = text->GetTextPos();
2931 EDA_ANGLE angleBefore = text->GetTextAngle();
2932
2933 auto findProbe = []( BOARD& aBoard ) -> PCB_TEXT*
2934 {
2935 FOOTPRINT* fp = *aBoard.Footprints().begin();
2936 for( BOARD_ITEM* item : fp->GraphicalItems() )
2937 {
2938 if( PCB_TEXT* t = dynamic_cast<PCB_TEXT*>( item ) )
2939 {
2940 if( t->GetText() == wxT( "drift probe" ) )
2941 return t;
2942 }
2943 }
2944 return nullptr;
2945 };
2946
2947 BOARD* currentBoard = m_board.get();
2948 std::unique_ptr<BOARD> reloadedHolder;
2949 const std::filesystem::path savePathA =
2950 std::filesystem::temp_directory_path() / "pcb_text_in_rotated_fp_a.kicad_pcb";
2951 const std::filesystem::path savePathB =
2952 std::filesystem::temp_directory_path() / "pcb_text_in_rotated_fp_b.kicad_pcb";
2953
2954 for( int cycle = 0; cycle < 3; ++cycle )
2955 {
2956 const std::filesystem::path& path = ( cycle % 2 == 0 ) ? savePathA : savePathB;
2957
2958 KI_TEST::DumpBoardToFile( *currentBoard, path.string() );
2959 reloadedHolder = KI_TEST::ReadBoardFromFileOrStream( path.string() );
2960 BOOST_REQUIRE( reloadedHolder );
2961 currentBoard = reloadedHolder.get();
2962
2963 PCB_TEXT* probe = findProbe( *currentBoard );
2964 BOOST_REQUIRE( probe );
2965
2966 BOOST_CHECK_MESSAGE( std::abs( probe->GetTextPos().x - posBefore.x ) <= 1
2967 && std::abs( probe->GetTextPos().y - posBefore.y ) <= 1,
2968 "cycle " << cycle << " text pos ( " << probe->GetTextPos().x << ", "
2969 << probe->GetTextPos().y << " ) expected ( " << posBefore.x << ", " << posBefore.y
2970 << " )" );
2971 BOOST_CHECK_CLOSE( probe->GetTextAngle().AsDegrees(), angleBefore.AsDegrees(), 1e-6 );
2972 }
2973}
2974
2975
2976BOOST_FIXTURE_TEST_CASE( PCBFieldInRotatedFootprintSurvivesSaveLoad, BOARD_FIXTURE )
2977{
2978 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
2979
2980 FOOTPRINT* fp = *m_board->Footprints().begin();
2981 BOOST_REQUIRE( fp );
2982
2983 fp->SetPosition( VECTOR2I( 12345678, -7654321 ) );
2984 fp->SetOrientation( EDA_ANGLE( 37.5, DEGREES_T ) );
2985
2986 PCB_FIELD& ref = fp->Reference();
2987 ref.SetTextPos( fp->GetPosition() + VECTOR2I( 3000000, -2000000 ) );
2988 ref.SetTextAngle( EDA_ANGLE( 25.0, DEGREES_T ) );
2989
2990 VECTOR2I refPosBefore = ref.GetTextPos();
2991 EDA_ANGLE refAngleBefore = ref.GetTextAngle();
2992
2993 const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "pcb_field_in_rotated_fp.kicad_pcb";
2994
2995 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
2996 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
2997 BOOST_REQUIRE( reloaded );
2998
2999 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
3000 BOOST_REQUIRE( fp2 );
3001
3002 PCB_FIELD& ref2 = fp2->Reference();
3003
3004 BOOST_CHECK_MESSAGE( std::abs( ref2.GetTextPos().x - refPosBefore.x ) <= 1
3005 && std::abs( ref2.GetTextPos().y - refPosBefore.y ) <= 1,
3006 "ref pos after reload ( " << ref2.GetTextPos().x << ", " << ref2.GetTextPos().y
3007 << " ) expected ( " << refPosBefore.x << ", " << refPosBefore.y
3008 << " )" );
3009 BOOST_CHECK_CLOSE( ref2.GetTextAngle().AsDegrees(), refAngleBefore.AsDegrees(), 1e-6 );
3010}
3011
3012
3013BOOST_FIXTURE_TEST_CASE( DimensionAndBarcodeInScaledFootprintSurviveSaveLoad, BOARD_FIXTURE )
3014{
3015 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
3016
3017 FOOTPRINT* fp = *m_board->Footprints().begin();
3018 BOOST_REQUIRE( fp );
3019
3020 fp->SetPosition( VECTOR2I( 12345678, -7654321 ) );
3021 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3022
3024 dim->SetStart( fp->GetPosition() + VECTOR2I( 1000000, 0 ) );
3025 dim->SetEnd( fp->GetPosition() + VECTOR2I( 5000000, 0 ) );
3026 fp->Add( dim, ADD_MODE::APPEND );
3027
3028 PCB_BARCODE* bc = new PCB_BARCODE( fp );
3029 bc->SetPosition( fp->GetPosition() + VECTOR2I( 2000000, 3000000 ) );
3030 fp->Add( bc, ADD_MODE::APPEND );
3031
3032 PCB_TEXT* text = new PCB_TEXT( fp );
3033 text->SetText( wxT( "scale probe" ) );
3034 text->SetTextPos( fp->GetPosition() + VECTOR2I( 4000000, -2000000 ) );
3035 text->SetLayer( F_SilkS );
3036 fp->Add( text, ADD_MODE::APPEND );
3037
3038 PCB_TEXTBOX* tb = new PCB_TEXTBOX( fp );
3039 tb->SetText( wxT( "scale probe box" ) );
3040 tb->SetStart( fp->GetPosition() + VECTOR2I( 1000000, -3000000 ) );
3041 tb->SetEnd( fp->GetPosition() + VECTOR2I( 4000000, -1500000 ) );
3042 tb->SetLayer( F_SilkS );
3043 fp->Add( tb, ADD_MODE::APPEND );
3044
3045 fp->SetTransformScale( 2.0, 1.5 );
3046
3047 VECTOR2I dimStartBefore = dim->GetStart();
3048 VECTOR2I dimEndBefore = dim->GetEnd();
3049 VECTOR2I bcPosBefore = bc->GetPosition();
3050 VECTOR2I textPosBefore = text->GetTextPos();
3051 VECTOR2I tbStartBefore = tb->GetStart();
3052 VECTOR2I tbEndBefore = tb->GetEnd();
3053
3054 const std::filesystem::path savePath =
3055 std::filesystem::temp_directory_path() / "dim_barcode_in_scaled_fp.kicad_pcb";
3056
3057 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
3058 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
3059 BOOST_REQUIRE( reloaded );
3060
3061 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
3062 BOOST_REQUIRE( fp2 );
3063
3064 PCB_DIM_ALIGNED* dim2 = nullptr;
3065 PCB_BARCODE* bc2 = nullptr;
3066 PCB_TEXT* text2 = nullptr;
3067 PCB_TEXTBOX* tb2 = nullptr;
3068 for( BOARD_ITEM* item : fp2->GraphicalItems() )
3069 {
3070 if( PCB_DIM_ALIGNED* d = dynamic_cast<PCB_DIM_ALIGNED*>( item ) )
3071 dim2 = d;
3072 else if( PCB_BARCODE* b = dynamic_cast<PCB_BARCODE*>( item ) )
3073 bc2 = b;
3074 else if( PCB_TEXTBOX* x = dynamic_cast<PCB_TEXTBOX*>( item ) )
3075 tb2 = x;
3076 else if( PCB_TEXT* t = dynamic_cast<PCB_TEXT*>( item ) )
3077 {
3078 if( t->GetText() == wxT( "scale probe" ) )
3079 text2 = t;
3080 }
3081 }
3082
3083 BOOST_REQUIRE( dim2 );
3084 BOOST_REQUIRE( bc2 );
3085 BOOST_REQUIRE( text2 );
3086 BOOST_REQUIRE( tb2 );
3087
3088 BOOST_CHECK_MESSAGE( std::abs( dim2->GetStart().x - dimStartBefore.x ) <= 1
3089 && std::abs( dim2->GetStart().y - dimStartBefore.y ) <= 1,
3090 "dim start after reload ( " << dim2->GetStart().x << ", " << dim2->GetStart().y
3091 << " ) expected ( " << dimStartBefore.x << ", " << dimStartBefore.y
3092 << " )" );
3093 BOOST_CHECK_MESSAGE( std::abs( dim2->GetEnd().x - dimEndBefore.x ) <= 1
3094 && std::abs( dim2->GetEnd().y - dimEndBefore.y ) <= 1,
3095 "dim end after reload ( " << dim2->GetEnd().x << ", " << dim2->GetEnd().y << " ) expected ( "
3096 << dimEndBefore.x << ", " << dimEndBefore.y << " )" );
3097 BOOST_CHECK_MESSAGE( std::abs( bc2->GetPosition().x - bcPosBefore.x ) <= 1
3098 && std::abs( bc2->GetPosition().y - bcPosBefore.y ) <= 1,
3099 "barcode pos after reload ( " << bc2->GetPosition().x << ", " << bc2->GetPosition().y
3100 << " ) expected ( " << bcPosBefore.x << ", " << bcPosBefore.y
3101 << " )" );
3102 BOOST_CHECK_MESSAGE( std::abs( text2->GetTextPos().x - textPosBefore.x ) <= 1
3103 && std::abs( text2->GetTextPos().y - textPosBefore.y ) <= 1,
3104 "text pos after reload ( " << text2->GetTextPos().x << ", " << text2->GetTextPos().y
3105 << " ) expected ( " << textPosBefore.x << ", " << textPosBefore.y
3106 << " )" );
3107 BOOST_CHECK_MESSAGE( std::abs( tb2->GetStart().x - tbStartBefore.x ) <= 1
3108 && std::abs( tb2->GetStart().y - tbStartBefore.y ) <= 1,
3109 "textbox start after reload ( " << tb2->GetStart().x << ", " << tb2->GetStart().y
3110 << " ) expected ( " << tbStartBefore.x << ", "
3111 << tbStartBefore.y << " )" );
3112 BOOST_CHECK_MESSAGE( std::abs( tb2->GetEnd().x - tbEndBefore.x ) <= 1
3113 && std::abs( tb2->GetEnd().y - tbEndBefore.y ) <= 1,
3114 "textbox end after reload ( " << tb2->GetEnd().x << ", " << tb2->GetEnd().y << " ) expected ( "
3115 << tbEndBefore.x << ", " << tbEndBefore.y << " )" );
3116}
3117
3118
3119BOOST_AUTO_TEST_CASE( PCBTextSizeFollowsFootprintScale )
3120{
3121 // GetTextSize must return the lib size scaled by the parent transform.
3122 FOOTPRINT fp( nullptr );
3123 fp.SetPosition( VECTOR2I( 0, 0 ) );
3124
3125 PCB_TEXT* text = new PCB_TEXT( &fp );
3126 text->SetText( wxT( "X" ) );
3127 text->SetTextSize( VECTOR2I( 1000000, 500000 ) );
3128 fp.Add( text, ADD_MODE::APPEND );
3129
3130 BOOST_CHECK_EQUAL( text->GetTextSize().x, 1000000 );
3131 BOOST_CHECK_EQUAL( text->GetTextSize().y, 500000 );
3132
3133 fp.SetTransformScale( 2.0, 1.5 );
3134
3135 BOOST_CHECK_EQUAL( text->GetTextSize().x, 2000000 );
3136 BOOST_CHECK_EQUAL( text->GetTextSize().y, 750000 );
3137}
3138
3139
3140BOOST_AUTO_TEST_CASE( FootprintRotateRoundTripPreservesChildRect )
3141{
3142 // Rotate by X then by -X must restore the child rect lib coords.
3143 BOARD board;
3144 FOOTPRINT* fp = new FOOTPRINT( &board );
3145 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3146 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3147 board.Add( fp );
3148
3149 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
3150 rect->SetStart( VECTOR2I( 1000000, 1000000 ) );
3151 rect->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3152 fp->Add( rect, ADD_MODE::APPEND );
3153
3154 VECTOR2I libStartBefore = rect->GetLibraryStart();
3155 VECTOR2I libEndBefore = rect->GetLibraryEnd();
3156
3157 fp->Rotate( fp->GetPosition(), EDA_ANGLE( 45.0, DEGREES_T ) );
3158 fp->Rotate( fp->GetPosition(), EDA_ANGLE( -45.0, DEGREES_T ) );
3159
3160 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStartBefore.x );
3161 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStartBefore.y );
3162 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEndBefore.x );
3163 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEndBefore.y );
3164}
3165
3166
3167BOOST_AUTO_TEST_CASE( FootprintSetOrientationRoundTripPreservesChildRect )
3168{
3169 // SetOrientation to a new angle and back must restore lib coords.
3170 BOARD board;
3171 FOOTPRINT* fp = new FOOTPRINT( &board );
3172 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3173 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3174 board.Add( fp );
3175
3176 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
3177 rect->SetStart( VECTOR2I( 1000000, 1000000 ) );
3178 rect->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3179 fp->Add( rect, ADD_MODE::APPEND );
3180
3181 VECTOR2I libStartBefore = rect->GetLibraryStart();
3182 VECTOR2I libEndBefore = rect->GetLibraryEnd();
3183
3184 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3185 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3186
3187 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStartBefore.x );
3188 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStartBefore.y );
3189 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEndBefore.x );
3190 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEndBefore.y );
3191}
3192
3193
3194BOOST_AUTO_TEST_CASE( FootprintSetOrientationNoOpPreservesChildRect )
3195{
3196 // SetOrientation with no actual change must not touch lib coords.
3197 BOARD board;
3198 FOOTPRINT* fp = new FOOTPRINT( &board );
3199 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3200 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3201 board.Add( fp );
3202
3203 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
3204 rect->SetStart( VECTOR2I( 1000000, 1000000 ) );
3205 rect->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3206 fp->Add( rect, ADD_MODE::APPEND );
3207
3208 VECTOR2I libStartBefore = rect->GetLibraryStart();
3209 VECTOR2I libEndBefore = rect->GetLibraryEnd();
3210
3211 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3212
3213 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStartBefore.x );
3214 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStartBefore.y );
3215 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEndBefore.x );
3216 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEndBefore.y );
3217}
3218
3219
3220BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildPolyUnderRotation )
3221{
3222 // Single flip must mirror, double flip must restore. The single-flip check
3223 // catches a no-op POLY path that would round-trip trivially.
3224 BOARD board;
3225 FOOTPRINT* fp = new FOOTPRINT( &board );
3226 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3227 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3228 board.Add( fp );
3229
3230 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
3231 SHAPE_POLY_SET pset;
3232 pset.NewOutline();
3233 pset.Append( VECTOR2I( 1000000, 1000000 ) );
3234 pset.Append( VECTOR2I( 5000000, 1000000 ) );
3235 pset.Append( VECTOR2I( 3000000, 4000000 ) );
3236 poly->SetPolyShape( pset );
3237 fp->Add( poly, ADD_MODE::APPEND );
3238
3239 SHAPE_POLY_SET polyBefore = poly->GetPolyShape();
3240
3241 const VECTOR2I flipCentre = fp->GetPosition();
3242 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3243
3244 SHAPE_POLY_SET polyAfter1 = poly->GetPolyShape();
3245 BOOST_REQUIRE_EQUAL( polyAfter1.TotalVertices(), polyBefore.TotalVertices() );
3246
3247 bool anyVertexChanged = false;
3248
3249 for( int i = 0; i < polyBefore.TotalVertices(); ++i )
3250 {
3251 if( polyBefore.CVertex( i ) != polyAfter1.CVertex( i ) )
3252 anyVertexChanged = true;
3253 }
3254
3255 BOOST_CHECK_MESSAGE( anyVertexChanged, "polygon vertices did not change after one flip" );
3256
3257 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3258
3259 SHAPE_POLY_SET polyAfter2 = poly->GetPolyShape();
3260 BOOST_REQUIRE_EQUAL( polyAfter2.TotalVertices(), polyBefore.TotalVertices() );
3261
3262 for( int i = 0; i < polyBefore.TotalVertices(); ++i )
3263 {
3264 VECTOR2I before = polyBefore.CVertex( i );
3265 VECTOR2I after = polyAfter2.CVertex( i );
3266
3267 BOOST_CHECK_MESSAGE( std::abs( before.x - after.x ) <= 1 && std::abs( before.y - after.y ) <= 1,
3268 "vertex " << i << " before ( " << before.x << ", " << before.y << " ) after ( " << after.x
3269 << ", " << after.y << " )" );
3270 }
3271}
3272
3273
3274BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildBezierUnderRotation )
3275{
3276 // Double flip must restore the bezier endpoints and control points.
3277 BOARD board;
3278 FOOTPRINT* fp = new FOOTPRINT( &board );
3279 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3280 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3281 board.Add( fp );
3282
3283 PCB_SHAPE* bezier = new PCB_SHAPE( fp, SHAPE_T::BEZIER );
3284 bezier->SetStart( VECTOR2I( 1000000, 1000000 ) );
3285 bezier->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3286 bezier->SetBezierC1( VECTOR2I( 2000000, 4000000 ) );
3287 bezier->SetBezierC2( VECTOR2I( 4000000, 0 ) );
3288 fp->Add( bezier, ADD_MODE::APPEND );
3289
3290 VECTOR2I libStartBefore = bezier->GetLibraryStart();
3291 VECTOR2I libEndBefore = bezier->GetLibraryEnd();
3292 VECTOR2I libC1Before = bezier->GetLibraryBezierC1();
3293 VECTOR2I libC2Before = bezier->GetLibraryBezierC2();
3294
3295 const VECTOR2I flipCentre = fp->GetPosition();
3296 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3297
3298 BOOST_CHECK_MESSAGE( bezier->GetLibraryStart() != libStartBefore || bezier->GetLibraryEnd() != libEndBefore
3299 || bezier->GetLibraryBezierC1() != libC1Before
3300 || bezier->GetLibraryBezierC2() != libC2Before,
3301 "lib coords did not change after one flip" );
3302
3303 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3304
3305 BOOST_CHECK_EQUAL( bezier->GetLibraryStart().x, libStartBefore.x );
3306 BOOST_CHECK_EQUAL( bezier->GetLibraryStart().y, libStartBefore.y );
3307 BOOST_CHECK_EQUAL( bezier->GetLibraryEnd().x, libEndBefore.x );
3308 BOOST_CHECK_EQUAL( bezier->GetLibraryEnd().y, libEndBefore.y );
3309 BOOST_CHECK_EQUAL( bezier->GetLibraryBezierC1().x, libC1Before.x );
3310 BOOST_CHECK_EQUAL( bezier->GetLibraryBezierC1().y, libC1Before.y );
3311 BOOST_CHECK_EQUAL( bezier->GetLibraryBezierC2().x, libC2Before.x );
3312 BOOST_CHECK_EQUAL( bezier->GetLibraryBezierC2().y, libC2Before.y );
3313}
3314
3315
3316BOOST_AUTO_TEST_CASE( FootprintFlipPreservesChildPointUnderRotation )
3317{
3318 // Double flip must restore a PCB_POINT position.
3319 BOARD board;
3320 FOOTPRINT* fp = new FOOTPRINT( &board );
3321 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3322 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3323 board.Add( fp );
3324
3325 PCB_POINT* point = new PCB_POINT( fp );
3326 point->SetPosition( VECTOR2I( fp->GetPosition().x + 2000000, fp->GetPosition().y + 1000000 ) );
3327 fp->Add( point, ADD_MODE::APPEND );
3328
3329 VECTOR2I boardPosBefore = point->GetPosition();
3330
3331 const VECTOR2I flipCentre = fp->GetPosition();
3332 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3333
3334 BOOST_CHECK_MESSAGE( point->GetPosition() != boardPosBefore, "board position did not change after one flip" );
3335
3336 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3337
3338 BOOST_CHECK_EQUAL( point->GetPosition().x, boardPosBefore.x );
3339 BOOST_CHECK_EQUAL( point->GetPosition().y, boardPosBefore.y );
3340}
3341
3342
3343BOOST_AUTO_TEST_CASE( FlipRoundTripUnderNonUniformScaleDivergedRect )
3344{
3345 // Non-uniform scale makes m_shape diverge from m_libShape (POLY vs RECTANGLE).
3346 // Double flip must still restore lib coords.
3347 BOARD board;
3348 FOOTPRINT* fp = new FOOTPRINT( &board );
3349 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3350 board.Add( fp );
3351
3352 PCB_SHAPE* rect = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
3353 rect->SetStart( VECTOR2I( 1000000, 1000000 ) );
3354 rect->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3355 fp->Add( rect, ADD_MODE::APPEND );
3356
3357 fp->SetTransformScale( 2.0, 1.0 );
3358
3359 BOOST_REQUIRE_EQUAL( (int) rect->GetLibraryShape(), (int) SHAPE_T::RECTANGLE );
3360
3361 VECTOR2I libStartBefore = rect->GetLibraryStart();
3362 VECTOR2I libEndBefore = rect->GetLibraryEnd();
3363
3364 const VECTOR2I flipCentre = fp->GetPosition();
3365 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3366
3367 BOOST_CHECK_MESSAGE( rect->GetLibraryStart() != libStartBefore || rect->GetLibraryEnd() != libEndBefore,
3368 "lib coords did not change after one flip" );
3369
3370 fp->Flip( flipCentre, FLIP_DIRECTION::TOP_BOTTOM );
3371
3372 BOOST_CHECK_EQUAL( rect->GetLibraryStart().x, libStartBefore.x );
3373 BOOST_CHECK_EQUAL( rect->GetLibraryStart().y, libStartBefore.y );
3374 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().x, libEndBefore.x );
3375 BOOST_CHECK_EQUAL( rect->GetLibraryEnd().y, libEndBefore.y );
3376}
3377
3378
3379BOOST_AUTO_TEST_CASE( BezierControlPointsFollowFootprintMove )
3380{
3381 // Bezier control points must track the parent FP transform on a move.
3382 BOARD board;
3383 FOOTPRINT* fp = new FOOTPRINT( &board );
3384 fp->SetPosition( VECTOR2I( 0, 0 ) );
3385 board.Add( fp );
3386
3387 PCB_SHAPE* bezier = new PCB_SHAPE( fp, SHAPE_T::BEZIER );
3388 bezier->SetStart( VECTOR2I( 1000000, 1000000 ) );
3389 bezier->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3390 bezier->SetBezierC1( VECTOR2I( 2000000, 4000000 ) );
3391 bezier->SetBezierC2( VECTOR2I( 4000000, 0 ) );
3392 fp->Add( bezier, ADD_MODE::APPEND );
3393
3394 VECTOR2I c1Before = bezier->GetBezierC1();
3395 VECTOR2I c2Before = bezier->GetBezierC2();
3396
3397 fp->SetPosition( VECTOR2I( 10000000, 5000000 ) );
3398
3399 BOOST_CHECK_EQUAL( bezier->GetBezierC1().x, c1Before.x + 10000000 );
3400 BOOST_CHECK_EQUAL( bezier->GetBezierC1().y, c1Before.y + 5000000 );
3401 BOOST_CHECK_EQUAL( bezier->GetBezierC2().x, c2Before.x + 10000000 );
3402 BOOST_CHECK_EQUAL( bezier->GetBezierC2().y, c2Before.y + 5000000 );
3403}
3404
3405
3406BOOST_AUTO_TEST_CASE( BezierControlPointsFollowFootprintRotate )
3407{
3408 // Bezier control points must follow the parent FP rotation.
3409 BOARD board;
3410 FOOTPRINT* fp = new FOOTPRINT( &board );
3411 fp->SetPosition( VECTOR2I( 0, 0 ) );
3412 board.Add( fp );
3413
3414 PCB_SHAPE* bezier = new PCB_SHAPE( fp, SHAPE_T::BEZIER );
3415 bezier->SetStart( VECTOR2I( 1000000, 0 ) );
3416 bezier->SetEnd( VECTOR2I( 0, 1000000 ) );
3417 bezier->SetBezierC1( VECTOR2I( 2000000, 0 ) );
3418 bezier->SetBezierC2( VECTOR2I( 0, 2000000 ) );
3419 fp->Add( bezier, ADD_MODE::APPEND );
3420
3421 VECTOR2I c1Before = bezier->GetBezierC1();
3422 VECTOR2I c2Before = bezier->GetBezierC2();
3423
3424 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3425
3426 BOOST_CHECK_MESSAGE( bezier->GetBezierC1() != c1Before, "C1 did not follow rotation" );
3427 BOOST_CHECK_MESSAGE( bezier->GetBezierC2() != c2Before, "C2 did not follow rotation" );
3428}
3429
3430
3431BOOST_AUTO_TEST_CASE( BezierBoardCoordsCorrectAfterRotation )
3432{
3433 // FOOTPRINT::SetOrientation must not double-rotate bezier control points.
3434 BOARD board;
3435 FOOTPRINT* fp = new FOOTPRINT( &board );
3436 fp->SetPosition( VECTOR2I( 0, 0 ) );
3437 board.Add( fp );
3438
3439 PCB_SHAPE* bezier = new PCB_SHAPE( fp, SHAPE_T::BEZIER );
3440 bezier->SetStart( VECTOR2I( 0, 0 ) );
3441 bezier->SetBezierC1( VECTOR2I( 1000000, 0 ) );
3442 bezier->SetBezierC2( VECTOR2I( 1000000, 1000000 ) );
3443 bezier->SetEnd( VECTOR2I( 2000000, 1000000 ) );
3444 fp->Add( bezier, ADD_MODE::APPEND );
3445
3446 VECTOR2I libC1 = bezier->GetLibraryBezierC1();
3447 VECTOR2I libC2 = bezier->GetLibraryBezierC2();
3448
3449 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3450
3451 const TRANSFORM_TRS& xf = fp->GetTransform();
3452 VECTOR2I expectedC1 = xf.Apply( libC1 );
3453 VECTOR2I expectedC2 = xf.Apply( libC2 );
3454
3455 BOOST_CHECK_MESSAGE( std::abs( bezier->GetBezierC1().x - expectedC1.x ) <= 1
3456 && std::abs( bezier->GetBezierC1().y - expectedC1.y ) <= 1,
3457 "C1 expected ( " << expectedC1.x << ", " << expectedC1.y << " ) actual ( "
3458 << bezier->GetBezierC1().x << ", " << bezier->GetBezierC1().y << " )" );
3459 BOOST_CHECK_MESSAGE( std::abs( bezier->GetBezierC2().x - expectedC2.x ) <= 1
3460 && std::abs( bezier->GetBezierC2().y - expectedC2.y ) <= 1,
3461 "C2 expected ( " << expectedC2.x << ", " << expectedC2.y << " ) actual ( "
3462 << bezier->GetBezierC2().x << ", " << bezier->GetBezierC2().y << " )" );
3463}
3464
3465
3466BOOST_AUTO_TEST_CASE( BezierFromParserHasCorrectBoardCoords )
3467{
3468 // Mimic the parser path. After the manual lib-bake the board values must
3469 // equal Apply(lib).
3470 BOARD board;
3471 FOOTPRINT* fp = new FOOTPRINT( &board );
3472 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3473 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3474 board.Add( fp );
3475
3476 PCB_SHAPE* bezier = new PCB_SHAPE( fp, SHAPE_T::BEZIER );
3477
3478 // Parser-style: feed in lib-frame coords through the setters.
3479 const VECTOR2I libStart( 1000000, 1000000 );
3480 const VECTOR2I libC1( 2000000, 4000000 );
3481 const VECTOR2I libC2( 4000000, 0 );
3482 const VECTOR2I libEnd( 5000000, 3000000 );
3483
3484 bezier->SetStart( libStart );
3485 bezier->SetBezierC1( libC1 );
3486 bezier->SetBezierC2( libC2 );
3487 bezier->SetEnd( libEnd );
3488
3489 // Manual lib-bake mimicking the parser fix-up.
3490 bezier->OverrideLibCoords( libStart, libEnd );
3491 bezier->OverrideLibBezier( libC1, libC2 );
3492 bezier->RebakeFromLib();
3493
3494 fp->Add( bezier, ADD_MODE::APPEND );
3495
3496 const TRANSFORM_TRS& xf = fp->GetTransform();
3497
3498 auto checkBoard = [&]( const char* name, const VECTOR2I& lib, const VECTOR2I& board )
3499 {
3500 VECTOR2I expected = xf.Apply( lib );
3501 BOOST_CHECK_MESSAGE( std::abs( expected.x - board.x ) <= 1 && std::abs( expected.y - board.y ) <= 1,
3502 name << " expected ( " << expected.x << ", " << expected.y << " ) actual ( " << board.x
3503 << ", " << board.y << " )" );
3504 };
3505
3506 checkBoard( "start", libStart, bezier->GetStart() );
3507 checkBoard( "C1", libC1, bezier->GetBezierC1() );
3508 checkBoard( "C2", libC2, bezier->GetBezierC2() );
3509 checkBoard( "end", libEnd, bezier->GetEnd() );
3510}
3511
3512
3513BOOST_AUTO_TEST_CASE( PolyFromParserHasCorrectBoardCoords )
3514{
3515 // After the manual lib-to-board bake the polygon must land at the FP position.
3516 BOARD board;
3517 FOOTPRINT* fp = new FOOTPRINT( &board );
3518 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3519 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3520 board.Add( fp );
3521
3522 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
3523
3524 std::vector<VECTOR2I> libVerts = {
3525 VECTOR2I( 1000000, 1000000 ),
3526 VECTOR2I( 5000000, 1000000 ),
3527 VECTOR2I( 3000000, 4000000 ),
3528 };
3529
3530 SHAPE_POLY_SET pset;
3531 pset.NewOutline();
3532 for( const VECTOR2I& v : libVerts )
3533 pset.Append( v );
3534
3535 poly->SetPolyShape( pset );
3536
3537 // Mimic the parser fix-up that transforms m_poly from lib to board.
3538 const TRANSFORM_TRS& xform = fp->GetTransform();
3539 SHAPE_POLY_SET& mpoly = poly->GetPolyShape();
3540 for( auto it = mpoly.IterateWithHoles(); it; it++ )
3541 mpoly.SetVertex( it.GetIndex(), xform.Apply( *it ) );
3542
3543 fp->Add( poly, ADD_MODE::APPEND );
3544
3546 BOOST_REQUIRE_EQUAL( result.TotalVertices(), (int) libVerts.size() );
3547
3548 for( int i = 0; i < (int) libVerts.size(); ++i )
3549 {
3550 VECTOR2I expected = xform.Apply( libVerts[i] );
3551 VECTOR2I actual = result.CVertex( i );
3552
3553 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
3554 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
3555 << actual.x << ", " << actual.y << " )" );
3556 }
3557}
3558
3559
3560BOOST_AUTO_TEST_CASE( PolyFollowsFootprintRotate )
3561{
3562 // POLY vertices must follow the parent FP rotation.
3563 BOARD board;
3564 FOOTPRINT* fp = new FOOTPRINT( &board );
3565 fp->SetPosition( VECTOR2I( 0, 0 ) );
3566 board.Add( fp );
3567
3568 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
3569 SHAPE_POLY_SET pset;
3570 pset.NewOutline();
3571 pset.Append( VECTOR2I( 1000000, 0 ) );
3572 pset.Append( VECTOR2I( 0, 1000000 ) );
3573 pset.Append( VECTOR2I( -1000000, 0 ) );
3574 poly->SetPolyShape( pset );
3575 fp->Add( poly, ADD_MODE::APPEND );
3576
3577 SHAPE_POLY_SET before = poly->GetPolyShape();
3578
3579 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3580
3581 SHAPE_POLY_SET after = poly->GetPolyShape();
3582 BOOST_REQUIRE_EQUAL( after.TotalVertices(), before.TotalVertices() );
3583
3584 bool anyMoved = false;
3585 for( int i = 0; i < before.TotalVertices(); ++i )
3586 {
3587 if( before.CVertex( i ) != after.CVertex( i ) )
3588 anyMoved = true;
3589 }
3590
3591 BOOST_CHECK_MESSAGE( anyMoved, "polygon vertices did not move after rotation" );
3592
3593 // Round-trip: rotate back to 0 must restore the original positions.
3594 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3595
3596 SHAPE_POLY_SET restored = poly->GetPolyShape();
3597 BOOST_REQUIRE_EQUAL( restored.TotalVertices(), before.TotalVertices() );
3598
3599 for( int i = 0; i < before.TotalVertices(); ++i )
3600 {
3601 VECTOR2I b = before.CVertex( i );
3602 VECTOR2I r = restored.CVertex( i );
3603
3604 BOOST_CHECK_MESSAGE( std::abs( b.x - r.x ) <= 1 && std::abs( b.y - r.y ) <= 1,
3605 "vertex " << i << " before ( " << b.x << ", " << b.y << " ) restored ( " << r.x << ", "
3606 << r.y << " )" );
3607 }
3608}
3609
3610
3611BOOST_AUTO_TEST_CASE( PolyFollowsFootprintMove )
3612{
3613 // POLY vertices must move with the parent FP.
3614 BOARD board;
3615 FOOTPRINT* fp = new FOOTPRINT( &board );
3616 fp->SetPosition( VECTOR2I( 0, 0 ) );
3617 board.Add( fp );
3618
3619 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
3620 SHAPE_POLY_SET pset;
3621 pset.NewOutline();
3622 pset.Append( VECTOR2I( 1000000, 1000000 ) );
3623 pset.Append( VECTOR2I( 5000000, 1000000 ) );
3624 pset.Append( VECTOR2I( 3000000, 4000000 ) );
3625 poly->SetPolyShape( pset );
3626 fp->Add( poly, ADD_MODE::APPEND );
3627
3628 SHAPE_POLY_SET before = poly->GetPolyShape();
3629
3630 const VECTOR2I delta( 10000000, 5000000 );
3631 fp->SetPosition( fp->GetPosition() + delta );
3632
3633 SHAPE_POLY_SET after = poly->GetPolyShape();
3634 BOOST_REQUIRE_EQUAL( after.TotalVertices(), before.TotalVertices() );
3635
3636 for( int i = 0; i < before.TotalVertices(); ++i )
3637 {
3638 VECTOR2I expected = before.CVertex( i ) + delta;
3639 VECTOR2I actual = after.CVertex( i );
3640
3641 BOOST_CHECK_MESSAGE( std::abs( expected.x - actual.x ) <= 1 && std::abs( expected.y - actual.y ) <= 1,
3642 "vertex " << i << " expected ( " << expected.x << ", " << expected.y << " ) actual ( "
3643 << actual.x << ", " << actual.y << " )" );
3644 }
3645}
3646
3647
3648BOOST_AUTO_TEST_CASE( PadOrientationLibFrameFollowsFootprintRotate )
3649{
3650 // Pad orientation is stored FP-relative. When the FP rotates, the
3651 // absolute orientation must change but the FP-relative one must stay.
3652 BOARD board;
3653 FOOTPRINT* fp = new FOOTPRINT( &board );
3654 fp->SetPosition( VECTOR2I( 0, 0 ) );
3655 board.Add( fp );
3656
3657 PAD* pad = new PAD( fp );
3659 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
3660 pad->SetPosition( VECTOR2I( 5000000, 0 ) );
3661 pad->SetFPRelativeOrientation( EDA_ANGLE( 45.0, DEGREES_T ) );
3662 pad->SetLayerSet( PAD::SMDMask() );
3663 fp->Add( pad, ADD_MODE::APPEND );
3664
3665 BOOST_CHECK_CLOSE( pad->GetOrientation().AsDegrees(), 45.0, 1e-6 );
3666 BOOST_CHECK_CLOSE( pad->GetFPRelativeOrientation().AsDegrees(), 45.0, 1e-6 );
3667
3668 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3669
3670 // Absolute orientation has followed the FP.
3671 BOOST_CHECK_CLOSE( pad->GetOrientation().AsDegrees(), 135.0, 1e-6 );
3672 // FP-relative orientation has not.
3673 BOOST_CHECK_CLOSE( pad->GetFPRelativeOrientation().AsDegrees(), 45.0, 1e-6 );
3674
3675 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3676 BOOST_CHECK_CLOSE( pad->GetOrientation().AsDegrees(), 45.0, 1e-6 );
3677 BOOST_CHECK_CLOSE( pad->GetFPRelativeOrientation().AsDegrees(), 45.0, 1e-6 );
3678}
3679
3680
3681BOOST_AUTO_TEST_CASE( PCBTextAngleLibFrameFollowsFootprintRotate )
3682{
3683 // Text angle is stored FP-relative. When the FP rotates, the absolute
3684 // angle must change but the FP-relative one must stay.
3685 BOARD board;
3686 FOOTPRINT* fp = new FOOTPRINT( &board );
3687 fp->SetPosition( VECTOR2I( 0, 0 ) );
3688 board.Add( fp );
3689
3690 PCB_TEXT* text = new PCB_TEXT( fp );
3691 text->SetText( wxT( "X" ) );
3692 text->SetTextPos( VECTOR2I( 1000000, 0 ) );
3693 text->SetTextAngle( EDA_ANGLE( 45.0, DEGREES_T ) );
3694 fp->Add( text, ADD_MODE::APPEND );
3695
3696 BOOST_CHECK_CLOSE( text->GetTextAngle().AsDegrees(), 45.0, 1e-6 );
3697
3698 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3699
3700 BOOST_CHECK_CLOSE( text->GetTextAngle().AsDegrees(), 135.0, 1e-6 );
3701
3702 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3703 BOOST_CHECK_CLOSE( text->GetTextAngle().AsDegrees(), 45.0, 1e-6 );
3704}
3705
3706
3707BOOST_AUTO_TEST_CASE( PCBTextBoxAngleLibFrameFollowsFootprintRotate )
3708{
3709 // PCB_TEXTBOX angle is stored FP-relative. FP rotation updates the
3710 // absolute, lib stays.
3711 BOARD board;
3712 FOOTPRINT* fp = new FOOTPRINT( &board );
3713 fp->SetPosition( VECTOR2I( 0, 0 ) );
3714 board.Add( fp );
3715
3716 PCB_TEXTBOX* tb = new PCB_TEXTBOX( fp );
3717 tb->SetStart( VECTOR2I( 0, 0 ) );
3718 tb->SetEnd( VECTOR2I( 2000000, 1000000 ) );
3719 tb->SetText( wxT( "X" ) );
3720 tb->SetTextAngle( EDA_ANGLE( 45.0, DEGREES_T ) );
3721 fp->Add( tb, ADD_MODE::APPEND );
3722
3723 BOOST_CHECK_CLOSE( tb->GetTextAngle().AsDegrees(), 45.0, 1e-6 );
3724
3725 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3726
3727 BOOST_CHECK_CLOSE( tb->GetTextAngle().AsDegrees(), 135.0, 1e-6 );
3728
3729 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3730 BOOST_CHECK_CLOSE( tb->GetTextAngle().AsDegrees(), 45.0, 1e-6 );
3731}
3732
3733
3734BOOST_AUTO_TEST_CASE( PCBTextBoxAngleLibFrameSurvivesFlipRoundTrip )
3735{
3736 // Double flip must restore both the absolute angle and the lib angle. If
3737 // PCB_TEXTBOX::Flip / Mirror forgets to keep m_libTextAngle in sync, the
3738 // second flip would compute the wrong absolute on the way back.
3739 BOARD board;
3740 FOOTPRINT* fp = new FOOTPRINT( &board );
3741 fp->SetPosition( VECTOR2I( 0, 5000000 ) );
3742 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3743 board.Add( fp );
3744
3745 PCB_TEXTBOX* tb = new PCB_TEXTBOX( fp );
3746 tb->SetStart( VECTOR2I( 0, 0 ) );
3747 tb->SetEnd( VECTOR2I( 2000000, 1000000 ) );
3748 tb->SetText( wxT( "X" ) );
3749 tb->SetTextAngle( EDA_ANGLE( 15.0, DEGREES_T ) );
3750 fp->Add( tb, ADD_MODE::APPEND );
3751
3752 EDA_ANGLE absBefore = tb->GetTextAngle();
3753
3755
3756 BOOST_CHECK_MESSAGE( tb->GetTextAngle() != absBefore, "text angle did not change after one flip" );
3757
3759
3760 BOOST_CHECK_CLOSE( tb->GetTextAngle().AsDegrees(), absBefore.AsDegrees(), 1e-6 );
3761}
3762
3763
3764BOOST_AUTO_TEST_CASE( DimensionFollowsFootprintScale )
3765{
3766 // PCB_DIMENSION inside an FP: start and end must scale with the parent
3767 // FP transform.
3768 BOARD board;
3769 FOOTPRINT* fp = new FOOTPRINT( &board );
3770 fp->SetPosition( VECTOR2I( 0, 0 ) );
3771 board.Add( fp );
3772
3774 dim->SetStart( VECTOR2I( 0, 0 ) );
3775 dim->SetEnd( VECTOR2I( 10000000, 0 ) );
3776 fp->Add( dim, ADD_MODE::APPEND );
3777
3778 fp->SetTransformScale( 2.0, 1.0 );
3779
3780 BOOST_CHECK_EQUAL( dim->GetStart().x, 0 );
3781 BOOST_CHECK_EQUAL( dim->GetEnd().x, 20000000 );
3782}
3783
3784
3785BOOST_AUTO_TEST_CASE( DimensionAngleLibFrameFollowsFootprintRotate )
3786{
3787 // FP rotation updates the absolute text angle, lib value stays.
3788 BOARD board;
3789 FOOTPRINT* fp = new FOOTPRINT( &board );
3790 fp->SetPosition( VECTOR2I( 0, 0 ) );
3791 board.Add( fp );
3792
3794 dim->SetKeepTextAligned( false );
3795 dim->SetStart( VECTOR2I( 0, 0 ) );
3796 dim->SetEnd( VECTOR2I( 10000000, 0 ) );
3797 dim->SetTextAngle( EDA_ANGLE( 30.0, DEGREES_T ) );
3798 fp->Add( dim, ADD_MODE::APPEND );
3799
3800 BOOST_CHECK_CLOSE( dim->GetTextAngle().AsDegrees(), 30.0, 1e-6 );
3801
3802 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3803
3804 BOOST_CHECK_CLOSE( dim->GetTextAngle().AsDegrees(), 120.0, 1e-6 );
3805
3806 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3807 BOOST_CHECK_CLOSE( dim->GetTextAngle().AsDegrees(), 30.0, 1e-6 );
3808}
3809
3810
3811BOOST_AUTO_TEST_CASE( DimensionLibCoordsSurviveFootprintRotate )
3812{
3813 // Lib coords stay invariant under FP rotation.
3814 BOARD board;
3815 FOOTPRINT* fp = new FOOTPRINT( &board );
3816 fp->SetPosition( VECTOR2I( 30000000, 20000000 ) );
3817 board.Add( fp );
3818
3820 dim->SetStart( VECTOR2I( 1000000, 2000000 ) );
3821 dim->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3822 fp->Add( dim, ADD_MODE::APPEND );
3823
3824 VECTOR2I libStartBefore = dim->GetLibraryStart();
3825 VECTOR2I libEndBefore = dim->GetLibraryEnd();
3826
3827 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3828 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3829 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3830
3831 BOOST_CHECK_EQUAL( dim->GetLibraryStart().x, libStartBefore.x );
3832 BOOST_CHECK_EQUAL( dim->GetLibraryStart().y, libStartBefore.y );
3833 BOOST_CHECK_EQUAL( dim->GetLibraryEnd().x, libEndBefore.x );
3834 BOOST_CHECK_EQUAL( dim->GetLibraryEnd().y, libEndBefore.y );
3835}
3836
3837
3838BOOST_AUTO_TEST_CASE( BarcodeFollowsFootprintRotate )
3839{
3840 BOARD board;
3841 FOOTPRINT* fp = new FOOTPRINT( &board );
3842 fp->SetPosition( VECTOR2I( 0, 0 ) );
3843 board.Add( fp );
3844
3845 PCB_BARCODE* bc = new PCB_BARCODE( fp );
3846 bc->SetPosition( VECTOR2I( 5000000, 0 ) );
3847 bc->SetOrientation( 30.0 );
3848 fp->Add( bc, ADD_MODE::APPEND );
3849
3850 BOOST_CHECK_EQUAL( bc->GetLibraryPos().x, 5000000 );
3851 BOOST_CHECK_EQUAL( bc->GetLibraryPos().y, 0 );
3852 BOOST_CHECK_CLOSE( bc->GetLibraryAngle().AsDegrees(), 30.0, 1e-6 );
3853
3854 fp->SetOrientation( EDA_ANGLE( 90.0, DEGREES_T ) );
3855
3856 // Lib stays the same. Board angle picks up the FP rotation.
3857 BOOST_CHECK_EQUAL( bc->GetLibraryPos().x, 5000000 );
3858 BOOST_CHECK_EQUAL( bc->GetLibraryPos().y, 0 );
3859 BOOST_CHECK_CLOSE( bc->GetLibraryAngle().AsDegrees(), 30.0, 1e-6 );
3860 BOOST_CHECK_CLOSE( bc->GetAngle().AsDegrees(), 120.0, 1e-6 );
3861
3862 fp->SetOrientation( EDA_ANGLE( 0.0, DEGREES_T ) );
3863 BOOST_CHECK_CLOSE( bc->GetAngle().AsDegrees(), 30.0, 1e-6 );
3864}
3865
3866
3867BOOST_AUTO_TEST_CASE( BarcodeFollowsFootprintMove )
3868{
3869 BOARD board;
3870 FOOTPRINT* fp = new FOOTPRINT( &board );
3871 fp->SetPosition( VECTOR2I( 0, 0 ) );
3872 board.Add( fp );
3873
3874 PCB_BARCODE* bc = new PCB_BARCODE( fp );
3875 bc->SetPosition( VECTOR2I( 3000000, 2000000 ) );
3876 fp->Add( bc, ADD_MODE::APPEND );
3877
3878 VECTOR2I libBefore = bc->GetLibraryPos();
3879
3880 fp->SetPosition( VECTOR2I( 10000000, 5000000 ) );
3881
3882 BOOST_CHECK_EQUAL( bc->GetLibraryPos().x, libBefore.x );
3883 BOOST_CHECK_EQUAL( bc->GetLibraryPos().y, libBefore.y );
3884 BOOST_CHECK_EQUAL( bc->GetPosition().x, 13000000 );
3885 BOOST_CHECK_EQUAL( bc->GetPosition().y, 7000000 );
3886}
3887
3888
3889BOOST_AUTO_TEST_CASE( BarcodeFlipPreservesBoardAngleDelta )
3890{
3891 // Single flip should change the visible angle by +180 for TOP_BOTTOM,
3892 // 0 for LEFT_RIGHT, regardless of FP rotation.
3893 auto checkFlip = [&]( double aFpOrientDeg, FLIP_DIRECTION aDir, double aExpectedDelta )
3894 {
3895 BOARD board;
3896 FOOTPRINT* fp = new FOOTPRINT( &board );
3897 fp->SetPosition( VECTOR2I( 10000000, 5000000 ) );
3898 fp->SetOrientation( EDA_ANGLE( aFpOrientDeg, DEGREES_T ) );
3899 board.Add( fp );
3900
3901 PCB_BARCODE* bc = new PCB_BARCODE( fp );
3902 bc->SetPosition( fp->GetPosition() + VECTOR2I( 2000000, 1000000 ) );
3903 bc->SetOrientation( 0.0 );
3904 fp->Add( bc, ADD_MODE::APPEND );
3905
3906 EDA_ANGLE before = bc->GetAngle();
3907
3908 fp->Flip( fp->GetPosition(), aDir );
3909
3910 EDA_ANGLE after = bc->GetAngle();
3911 EDA_ANGLE expected = ( before + EDA_ANGLE( aExpectedDelta, DEGREES_T ) ).Normalize();
3912 EDA_ANGLE actual = after.Normalize();
3913
3914 BOOST_CHECK_CLOSE( actual.AsDegrees(), expected.AsDegrees(), 1e-6 );
3915 };
3916
3917 checkFlip( 0.0, FLIP_DIRECTION::TOP_BOTTOM, 180.0 );
3918 checkFlip( 30.0, FLIP_DIRECTION::TOP_BOTTOM, 180.0 );
3919 checkFlip( 90.0, FLIP_DIRECTION::TOP_BOTTOM, 180.0 );
3920 checkFlip( 0.0, FLIP_DIRECTION::LEFT_RIGHT, 0.0 );
3921 checkFlip( 30.0, FLIP_DIRECTION::LEFT_RIGHT, 0.0 );
3922}
3923
3924
3925BOOST_AUTO_TEST_CASE( BarcodeSurvivesFootprintFlipRoundTrip )
3926{
3927 BOARD board;
3928 FOOTPRINT* fp = new FOOTPRINT( &board );
3929 fp->SetPosition( VECTOR2I( 20000000, 10000000 ) );
3930 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3931 board.Add( fp );
3932
3933 PCB_BARCODE* bc = new PCB_BARCODE( fp );
3934 bc->SetPosition( fp->GetPosition() + VECTOR2I( 2000000, 1000000 ) );
3935 bc->SetOrientation( 15.0 );
3936 fp->Add( bc, ADD_MODE::APPEND );
3937
3938 VECTOR2I libPosBefore = bc->GetLibraryPos();
3939 EDA_ANGLE libAngleBefore = bc->GetLibraryAngle();
3940 VECTOR2I posBefore = bc->GetPosition();
3941 EDA_ANGLE angleBefore = bc->GetAngle();
3942
3944
3945 BOOST_CHECK_MESSAGE( bc->GetPosition() != posBefore, "barcode pos did not move on first flip" );
3946
3948
3949 BOOST_CHECK_EQUAL( bc->GetLibraryPos().x, libPosBefore.x );
3950 BOOST_CHECK_EQUAL( bc->GetLibraryPos().y, libPosBefore.y );
3951 BOOST_CHECK_CLOSE( bc->GetLibraryAngle().AsDegrees(), libAngleBefore.AsDegrees(), 1e-6 );
3952 BOOST_CHECK_EQUAL( bc->GetPosition().x, posBefore.x );
3953 BOOST_CHECK_EQUAL( bc->GetPosition().y, posBefore.y );
3954 BOOST_CHECK_CLOSE( bc->GetAngle().AsDegrees(), angleBefore.AsDegrees(), 1e-6 );
3955}
3956
3957
3958BOOST_AUTO_TEST_CASE( DimensionLibCoordsSurviveFootprintFlipRoundTrip )
3959{
3960 // Double flip restores lib coords and board caches for dimensions.
3961 BOARD board;
3962 FOOTPRINT* fp = new FOOTPRINT( &board );
3963 fp->SetPosition( VECTOR2I( 20000000, 10000000 ) );
3964 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
3965 board.Add( fp );
3966
3968 dim->SetKeepTextAligned( false );
3969 dim->SetStart( VECTOR2I( 1000000, 2000000 ) );
3970 dim->SetEnd( VECTOR2I( 5000000, 3000000 ) );
3971 dim->SetTextAngle( EDA_ANGLE( 15.0, DEGREES_T ) );
3972 fp->Add( dim, ADD_MODE::APPEND );
3973
3974 VECTOR2I libStartBefore = dim->GetLibraryStart();
3975 VECTOR2I libEndBefore = dim->GetLibraryEnd();
3976 EDA_ANGLE absAngleBefore = dim->GetTextAngle();
3977 VECTOR2I startBefore = dim->GetStart();
3978 VECTOR2I endBefore = dim->GetEnd();
3979
3981
3982 BOOST_CHECK_MESSAGE( dim->GetStart() != startBefore, "dim start did not move on first flip" );
3983
3985
3986 BOOST_CHECK_EQUAL( dim->GetLibraryStart().x, libStartBefore.x );
3987 BOOST_CHECK_EQUAL( dim->GetLibraryStart().y, libStartBefore.y );
3988 BOOST_CHECK_EQUAL( dim->GetLibraryEnd().x, libEndBefore.x );
3989 BOOST_CHECK_EQUAL( dim->GetLibraryEnd().y, libEndBefore.y );
3990 BOOST_CHECK_CLOSE( dim->GetTextAngle().AsDegrees(), absAngleBefore.AsDegrees(), 1e-6 );
3991 BOOST_CHECK_EQUAL( dim->GetStart().x, startBefore.x );
3992 BOOST_CHECK_EQUAL( dim->GetStart().y, startBefore.y );
3993 BOOST_CHECK_EQUAL( dim->GetEnd().x, endBefore.x );
3994 BOOST_CHECK_EQUAL( dim->GetEnd().y, endBefore.y );
3995}
3996
3997
4003BOOST_FIXTURE_TEST_CASE( StandaloneRectangleNonCardinalRotationSurvivesSaveLoad, BOARD_FIXTURE )
4004{
4005 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
4006
4007 PCB_SHAPE* rect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
4008 rect->SetStart( VECTOR2I( 0, 0 ) );
4009 rect->SetEnd( VECTOR2I( 10000000, 5000000 ) );
4010 rect->SetLayer( F_SilkS );
4011 m_board->Add( rect, ADD_MODE::APPEND );
4012
4014
4015 rect->Rotate( VECTOR2I( 0, 0 ), EDA_ANGLE( 30.0, DEGREES_T ) );
4016
4017 BOOST_CHECK( rect->GetShape() == SHAPE_T::POLY );
4018 BOOST_REQUIRE_EQUAL( rect->GetPolyShape().OutlineCount(), 1 );
4019
4020 std::vector<VECTOR2I> savedCorners;
4021 for( const VECTOR2I& p : rect->GetPolyShape().Outline( 0 ).CPoints() )
4022 savedCorners.emplace_back( p );
4023
4024 const std::filesystem::path savePath =
4025 std::filesystem::temp_directory_path() / "rect_noncardinal_roundtrip.kicad_pcb";
4026
4027 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
4028 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
4029 BOOST_REQUIRE( reloaded );
4030
4031 PCB_SHAPE* reloadedRect = nullptr;
4032 for( BOARD_ITEM* item : reloaded->Drawings() )
4033 {
4034 if( PCB_SHAPE* s = dynamic_cast<PCB_SHAPE*>( item ) )
4035 {
4036 if( s->GetShape() == SHAPE_T::POLY )
4037 {
4038 reloadedRect = s;
4039 break;
4040 }
4041 }
4042 }
4043
4044 BOOST_REQUIRE( reloadedRect );
4045 BOOST_REQUIRE_EQUAL( reloadedRect->GetPolyShape().OutlineCount(), 1 );
4046
4047 const auto& reloadedPoints = reloadedRect->GetPolyShape().Outline( 0 ).CPoints();
4048 BOOST_REQUIRE_EQUAL( (int) reloadedPoints.size(), (int) savedCorners.size() );
4049
4050 for( size_t i = 0; i < savedCorners.size(); ++i )
4051 {
4052 BOOST_CHECK_EQUAL( reloadedPoints[i].x, savedCorners[i].x );
4053 BOOST_CHECK_EQUAL( reloadedPoints[i].y, savedCorners[i].y );
4054 }
4055}
4056
4057
4058BOOST_AUTO_TEST_CASE( MoveAnchorMovesPointsLikeOtherChildren )
4059{
4060 BOARD board;
4061 FOOTPRINT* fp = new FOOTPRINT( &board );
4062 board.Add( fp );
4063
4064 PAD* pad = new PAD( fp );
4065 pad->SetAttribute( PAD_ATTRIB::SMD );
4066 pad->SetLayerSet( PAD::SMDMask() );
4067 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
4068 pad->SetPosition( VECTOR2I( 5000000, 2000000 ) );
4069 fp->Add( pad );
4070
4071 PCB_POINT* pt = new PCB_POINT( fp );
4072 pt->SetLayer( F_Cu );
4073 pt->SetPosition( VECTOR2I( 3000000, 4000000 ) );
4074 fp->Add( pt );
4075
4076 const VECTOR2I padToPointBefore = pt->GetPosition() - pad->GetPosition();
4077
4078 fp->MoveAnchorPosition( VECTOR2I( 2000000, 1000000 ) );
4079
4080 // The point must move with the rest of the footprint, keeping its spacing to the pad.
4081 const VECTOR2I padToPointAfter = pt->GetPosition() - pad->GetPosition();
4082 BOOST_CHECK_EQUAL( padToPointAfter.x, padToPointBefore.x );
4083 BOOST_CHECK_EQUAL( padToPointAfter.y, padToPointBefore.y );
4084}
4085
4086
4087BOOST_AUTO_TEST_CASE( FlipNonCardinalKeepsPadAndShapeCoincident )
4088{
4089 BOARD board;
4090 FOOTPRINT* fp = new FOOTPRINT( &board );
4091 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4092 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
4093 board.Add( fp );
4094
4095 const VECTOR2I coincident( 53000000, 51000000 );
4096
4097 PAD* pad = new PAD( fp );
4098 pad->SetAttribute( PAD_ATTRIB::SMD );
4099 pad->SetLayerSet( PAD::SMDMask() );
4100 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
4101 pad->SetPosition( coincident );
4102 fp->Add( pad );
4103
4104 PCB_SHAPE* seg = new PCB_SHAPE( fp, SHAPE_T::SEGMENT );
4105 seg->SetStart( coincident );
4106 seg->SetEnd( coincident + VECTOR2I( 1000000, 0 ) );
4107 seg->SetLayer( F_SilkS );
4108 fp->Add( seg );
4109
4110 // Sanity: coincident before the flip.
4111 BOOST_REQUIRE_EQUAL( pad->GetPosition().x, seg->GetStart().x );
4112 BOOST_REQUIRE_EQUAL( pad->GetPosition().y, seg->GetStart().y );
4113
4114 PCB_TEXT* txt = new PCB_TEXT( fp );
4115 txt->SetText( "X" );
4116 txt->SetTextPos( coincident );
4117 txt->SetLayer( F_SilkS );
4118 fp->Add( txt );
4119
4120 PCB_POINT* pt = new PCB_POINT( fp );
4121 pt->SetLayer( F_Cu );
4122 pt->SetPosition( coincident );
4123 fp->Add( pt );
4124
4125 BOOST_REQUIRE_EQUAL( txt->GetTextPos().x, seg->GetStart().x );
4126 BOOST_REQUIRE_EQUAL( pt->GetPosition().x, seg->GetStart().x );
4127
4129
4130 // All footprint children must still coincide after the flip.
4131 BOOST_CHECK_EQUAL( pad->GetPosition().x, seg->GetStart().x );
4132 BOOST_CHECK_EQUAL( pad->GetPosition().y, seg->GetStart().y );
4133 BOOST_CHECK_EQUAL( txt->GetTextPos().x, seg->GetStart().x );
4134 BOOST_CHECK_EQUAL( txt->GetTextPos().y, seg->GetStart().y );
4135 BOOST_CHECK_EQUAL( pt->GetPosition().x, seg->GetStart().x );
4136 BOOST_CHECK_EQUAL( pt->GetPosition().y, seg->GetStart().y );
4137}
4138
4139
4140BOOST_AUTO_TEST_CASE( FlipBarcodeAngleMatchesTextOnRotatedFootprint )
4141{
4142 BOARD board;
4143 FOOTPRINT* fp = new FOOTPRINT( &board );
4144 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4145 fp->SetOrientation( EDA_ANGLE( 30.0, DEGREES_T ) );
4146 board.Add( fp );
4147
4148 PCB_TEXT* txt = new PCB_TEXT( fp );
4149 txt->SetText( "X" );
4150 txt->SetLayer( F_SilkS );
4151 txt->SetTextAngle( EDA_ANGLE( 75.0, DEGREES_T ) );
4152 fp->Add( txt );
4153
4154 PCB_BARCODE* bc = new PCB_BARCODE( fp );
4155 bc->SetText( "12345" );
4156 bc->SetWidth( 3000000 );
4157 bc->SetHeight( 3000000 );
4158 bc->SetTextSize( 1500000 );
4159 bc->SetLayer( F_SilkS );
4160 bc->SetOrientation( 75.0 );
4161 bc->AssembleBarcode();
4162 fp->Add( bc );
4163
4164 // Same board angle before the flip.
4165 BOOST_REQUIRE_CLOSE( bc->GetOrientation(), txt->GetTextAngle().AsDegrees(), 1e-6 );
4166
4168
4169 // A barcode must reflect its angle on flip exactly like text does.
4170 EDA_ANGLE bcAngle( bc->GetOrientation(), DEGREES_T );
4171 EDA_ANGLE txtAngle = txt->GetTextAngle();
4172 bcAngle.Normalize();
4173 txtAngle.Normalize();
4174 BOOST_CHECK_CLOSE( bcAngle.AsDegrees(), txtAngle.AsDegrees(), 1e-6 );
4175}
4176
4177
4178// Build a footprint at the given placement with a pad, graphic, text and point all
4179// anchored at the same board point. Returns the footprint; child pointers via out-params.
4180static FOOTPRINT* buildCoincidentFootprint( BOARD& aBoard, const EDA_ANGLE& aOrient, double aScaleX, double aScaleY,
4181 const VECTOR2I& aCoincident, PAD*& aPad, PCB_SHAPE*& aSeg, PCB_TEXT*& aTxt,
4182 PCB_POINT*& aPt )
4183{
4184 FOOTPRINT* fp = new FOOTPRINT( &aBoard );
4185 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4186 fp->SetOrientation( aOrient );
4187 fp->SetTransformScale( aScaleX, aScaleY );
4188 aBoard.Add( fp );
4189
4190 aPad = new PAD( fp );
4192 aPad->SetLayerSet( PAD::SMDMask() );
4193 aPad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 1000000, 1000000 ) );
4194 aPad->SetPosition( aCoincident );
4195 fp->Add( aPad );
4196
4197 aSeg = new PCB_SHAPE( fp, SHAPE_T::SEGMENT );
4198 aSeg->SetStart( aCoincident );
4199 aSeg->SetEnd( aCoincident + VECTOR2I( 1000000, 0 ) );
4200 aSeg->SetLayer( F_SilkS );
4201 fp->Add( aSeg );
4202
4203 aTxt = new PCB_TEXT( fp );
4204 aTxt->SetText( "X" );
4205 aTxt->SetTextPos( aCoincident );
4206 aTxt->SetLayer( F_SilkS );
4207 fp->Add( aTxt );
4208
4209 aPt = new PCB_POINT( fp );
4210 aPt->SetLayer( F_Cu );
4211 aPt->SetPosition( aCoincident );
4212 fp->Add( aPt );
4213
4214 return fp;
4215}
4216
4217
4218static void CHECK_ALL_COINCIDENT( PAD* aPad, PCB_SHAPE* aSeg, PCB_TEXT* aTxt, PCB_POINT* aPt, const std::string& aWhen )
4219{
4220 BOOST_CHECK_MESSAGE( aPad->GetPosition() == aSeg->GetStart(), aWhen << ": pad vs seg" );
4221 BOOST_CHECK_MESSAGE( aTxt->GetTextPos() == aSeg->GetStart(), aWhen << ": text vs seg" );
4222 BOOST_CHECK_MESSAGE( aPt->GetPosition() == aSeg->GetStart(), aWhen << ": point vs seg" );
4223}
4224
4225
4226BOOST_AUTO_TEST_CASE( RoundTripPreservesChildGeometryScaledRotated )
4227{
4228 auto board = std::make_unique<BOARD>();
4229 PAD* pad;
4230 PCB_SHAPE* seg;
4231 PCB_TEXT* txt;
4232 PCB_POINT* pt;
4233 buildCoincidentFootprint( *board, EDA_ANGLE( 30.0, DEGREES_T ), 2.0, 1.5, VECTOR2I( 53000000, 51000000 ), pad, seg,
4234 txt, pt );
4235
4236 const VECTOR2I padPos = pad->GetPosition();
4237 const VECTOR2I segStart = seg->GetStart();
4238 const VECTOR2I txtPos = txt->GetTextPos();
4239 const VECTOR2I ptPos = pt->GetPosition();
4240
4241 std::filesystem::path tmp = std::filesystem::temp_directory_path() / "kicad_qa_xform_roundtrip.kicad_pcb";
4242 KI_TEST::DumpBoardToFile( *board, tmp.string() );
4243 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( tmp.string() );
4244 std::error_code ec;
4245 std::filesystem::remove( tmp, ec );
4246 BOOST_REQUIRE( reloaded );
4247
4248 FOOTPRINT* r = reloaded->Footprints().front();
4249 BOOST_REQUIRE( !r->Pads().empty() && !r->Points().empty() );
4250 PCB_SHAPE* rseg = nullptr;
4251 PCB_TEXT* rtxt = nullptr;
4252
4253 for( BOARD_ITEM* it : r->GraphicalItems() )
4254 {
4255 if( it->Type() == PCB_SHAPE_T )
4256 rseg = static_cast<PCB_SHAPE*>( it );
4257 else if( it->Type() == PCB_TEXT_T )
4258 rtxt = static_cast<PCB_TEXT*>( it );
4259 }
4260
4261 BOOST_REQUIRE( rseg && rtxt );
4262
4263 // Absolute identity: every child returns to its exact board position.
4264 BOOST_CHECK( r->Pads().front()->GetPosition() == padPos );
4265 BOOST_CHECK( rseg->GetStart() == segStart );
4266 BOOST_CHECK( rtxt->GetTextPos() == txtPos );
4267 BOOST_CHECK( r->Points().front()->GetPosition() == ptPos );
4268}
4269
4270
4271BOOST_AUTO_TEST_CASE( RotateAndMoveKeepChildrenCoincident )
4272{
4273 BOARD board;
4274 PAD* pad;
4275 PCB_SHAPE* seg;
4276 PCB_TEXT* txt;
4277 PCB_POINT* pt;
4278 FOOTPRINT* fp = buildCoincidentFootprint( board, EDA_ANGLE( 30.0, DEGREES_T ), 2.0, 1.5,
4279 VECTOR2I( 53000000, 51000000 ), pad, seg, txt, pt );
4280
4281 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "before" );
4282 fp->SetOrientation( fp->GetOrientation() + EDA_ANGLE( 17.0, DEGREES_T ) );
4283 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "after rotate" );
4284 fp->SetPosition( fp->GetPosition() + VECTOR2I( 1234567, -7654321 ) );
4285 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "after move" );
4286 fp->SetTransformScale( 1.3, 2.7 );
4287 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "after rescale" );
4288}
4289
4290
4291BOOST_AUTO_TEST_CASE( FlipLeftRightKeepsChildrenCoincident )
4292{
4293 BOARD board;
4294 PAD* pad;
4295 PCB_SHAPE* seg;
4296 PCB_TEXT* txt;
4297 PCB_POINT* pt;
4298 FOOTPRINT* fp = buildCoincidentFootprint( board, EDA_ANGLE( 30.0, DEGREES_T ), 1.0, 1.0,
4299 VECTOR2I( 53000000, 51000000 ), pad, seg, txt, pt );
4300
4301 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "before" );
4303 CHECK_ALL_COINCIDENT( pad, seg, txt, pt, "after LEFT_RIGHT flip" );
4304}
4305
4306
4307BOOST_AUTO_TEST_CASE( CloneKeepsChildGeometryScaledRotated )
4308{
4309 BOARD board;
4310 PAD* pad;
4311 PCB_SHAPE* seg;
4312 PCB_TEXT* txt;
4313 PCB_POINT* pt;
4314 FOOTPRINT* fp = buildCoincidentFootprint( board, EDA_ANGLE( 30.0, DEGREES_T ), 2.0, 1.5,
4315 VECTOR2I( 53000000, 51000000 ), pad, seg, txt, pt );
4316
4317 const VECTOR2I ref = seg->GetStart();
4318
4319 std::unique_ptr<FOOTPRINT> clone( static_cast<FOOTPRINT*>( fp->Clone() ) );
4320
4321 BOOST_REQUIRE( !clone->Pads().empty() && !clone->Points().empty() );
4322 PCB_SHAPE* cseg = nullptr;
4323 PCB_TEXT* ctxt = nullptr;
4324
4325 for( BOARD_ITEM* it : clone->GraphicalItems() )
4326 {
4327 if( it->Type() == PCB_SHAPE_T )
4328 cseg = static_cast<PCB_SHAPE*>( it );
4329 else if( it->Type() == PCB_TEXT_T )
4330 ctxt = static_cast<PCB_TEXT*>( it );
4331 }
4332
4333 BOOST_REQUIRE( cseg && ctxt );
4334
4335 // The clone's children must sit at the same board geometry as the original.
4336 BOOST_CHECK( clone->Pads().front()->GetPosition() == ref );
4337 BOOST_CHECK( cseg->GetStart() == ref );
4338 BOOST_CHECK( ctxt->GetTextPos() == ref );
4339 BOOST_CHECK( clone->Points().front()->GetPosition() == ref );
4340}
4341
4342
4343BOOST_AUTO_TEST_CASE( DoubleFlipIsIdentityScaledRotated )
4344{
4346 {
4347 BOARD board;
4348 PAD* pad;
4349 PCB_SHAPE* seg;
4350 PCB_TEXT* txt;
4351 PCB_POINT* pt;
4352 FOOTPRINT* fp = buildCoincidentFootprint( board, EDA_ANGLE( 30.0, DEGREES_T ), 2.0, 1.5,
4353 VECTOR2I( 53000000, 51000000 ), pad, seg, txt, pt );
4354
4355 const VECTOR2I padPos = pad->GetPosition();
4356 const VECTOR2I segStart = seg->GetStart();
4357 const VECTOR2I txtPos = txt->GetTextPos();
4358 const VECTOR2I ptPos = pt->GetPosition();
4359
4360 fp->Flip( fp->GetPosition(), dir );
4361 fp->Flip( fp->GetPosition(), dir );
4362
4363 BOOST_CHECK( pad->GetPosition() == padPos );
4364 BOOST_CHECK( seg->GetStart() == segStart );
4365 BOOST_CHECK( txt->GetTextPos() == txtPos );
4366 BOOST_CHECK( pt->GetPosition() == ptPos );
4367 }
4368}
4369
4370
4371// Stroke width and text size must survive save/reload under a footprint scale.
4372BOOST_AUTO_TEST_CASE( ScalarAttributesStableAcrossSaveLoadUnderScale )
4373{
4374 auto board = std::make_unique<BOARD>();
4375 PAD* pad;
4376 PCB_SHAPE* seg;
4377 PCB_TEXT* txt;
4378 PCB_POINT* pt;
4379 FOOTPRINT* fp =
4380 buildCoincidentFootprint( *board, ANGLE_0, 2.0, 2.0, VECTOR2I( 53000000, 51000000 ), pad, seg, txt, pt );
4381
4382 // Author board frame attributes while the 2x scale is active.
4383 seg->SetWidth( 400000 );
4384 txt->SetTextSize( VECTOR2I( 2000000, 1000000 ) );
4385
4386 const int segWidth = seg->GetStroke().GetWidth();
4387 const VECTOR2I txtSize = txt->GetTextSize();
4388
4389 std::filesystem::path tmp = std::filesystem::temp_directory_path() / "kicad_qa_scalar_roundtrip.kicad_pcb";
4390 KI_TEST::DumpBoardToFile( *board, tmp.string() );
4391 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( tmp.string() );
4392 std::error_code ec;
4393 std::filesystem::remove( tmp, ec );
4394 BOOST_REQUIRE( reloaded );
4395
4396 FOOTPRINT* r = reloaded->Footprints().front();
4397 PCB_SHAPE* rseg = nullptr;
4398 PCB_TEXT* rtxt = nullptr;
4399
4400 for( BOARD_ITEM* it : r->GraphicalItems() )
4401 {
4402 if( it->Type() == PCB_SHAPE_T )
4403 rseg = static_cast<PCB_SHAPE*>( it );
4404 else if( it->Type() == PCB_TEXT_T )
4405 rtxt = static_cast<PCB_TEXT*>( it );
4406 }
4407
4408 BOOST_REQUIRE( rseg && rtxt );
4409
4410 BOOST_CHECK_EQUAL( rseg->GetStroke().GetWidth(), segWidth );
4411 BOOST_CHECK_EQUAL( rtxt->GetTextSize().x, txtSize.x );
4412 BOOST_CHECK_EQUAL( rtxt->GetTextSize().y, txtSize.y );
4413}
4414
4415
4416// Ellipse radii must survive a rebake under non-uniform scale. Uniform is the control.
4417BOOST_AUTO_TEST_CASE( EllipseRadiusEditSurvivesRebakeUnderNonUniformScale )
4418{
4419 auto runCase = []( double aScaleX, double aScaleY )
4420 {
4421 BOARD board;
4422 FOOTPRINT* fp = new FOOTPRINT( &board );
4423 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4424 fp->SetTransformScale( aScaleX, aScaleY );
4425 board.Add( fp );
4426
4427 PCB_SHAPE* ell = new PCB_SHAPE( fp, SHAPE_T::ELLIPSE );
4428 ell->SetLayer( F_SilkS );
4429 fp->Add( ell );
4430
4431 ell->SetEllipseCenter( VECTOR2I( 50000000, 50000000 ) );
4432 ell->SetEllipseMajorRadius( 4000000 );
4433 ell->SetEllipseMinorRadius( 1500000 );
4434
4435 const int majorBefore = ell->GetEllipseMajorRadius();
4436 const int minorBefore = ell->GetEllipseMinorRadius();
4437
4438 // Any transform refresh rebakes children from the lib mirror.
4439 fp->SetPosition( fp->GetPosition() + VECTOR2I( 1000000, 0 ) );
4440
4441 BOOST_CHECK_EQUAL( ell->GetEllipseMajorRadius(), majorBefore );
4442 BOOST_CHECK_EQUAL( ell->GetEllipseMinorRadius(), minorBefore );
4443 };
4444
4445 runCase( 2.0, 2.0 ); // control: uniform scale keeps the radii
4446 runCase( 2.0, 1.0 ); // non-uniform: radii are lost on rebake
4447}
4448
4449
4450// The drawn glyph (used by rendering and DRC) must scale with the footprint.
4451BOOST_AUTO_TEST_CASE( ScaledTextGlyphShapeFollowsScale )
4452{
4453 BOARD board;
4454 FOOTPRINT* fp = new FOOTPRINT( &board );
4455 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4456 board.Add( fp );
4457
4458 PCB_TEXT* txt = new PCB_TEXT( fp );
4459 txt->SetText( "H" );
4460 txt->SetTextPos( VECTOR2I( 50000000, 50000000 ) );
4461 txt->SetTextSize( VECTOR2I( 1000000, 1000000 ) );
4462 txt->SetLayer( F_SilkS );
4463 fp->Add( txt );
4464
4465 const int maxError = pcbIUScale.mmToIU( 0.01 );
4466
4467 SHAPE_POLY_SET glyph1x;
4468 txt->TransformTextToPolySet( glyph1x, 0, maxError, ERROR_INSIDE );
4469 const int height1x = glyph1x.BBox().GetHeight();
4470
4471 fp->SetTransformScale( 2.0, 2.0 );
4472
4473 SHAPE_POLY_SET glyph2x;
4474 txt->TransformTextToPolySet( glyph2x, 0, maxError, ERROR_INSIDE );
4475 const int height2x = glyph2x.BBox().GetHeight();
4476
4477 BOOST_CHECK_GT( height2x, height1x * 3 / 2 );
4478}
4479
4480
4481// Text box size, thickness, and border stroke must survive save/reload under scale.
4482BOOST_AUTO_TEST_CASE( TextBoxScalarAttributesStableAcrossSaveLoadUnderScale )
4483{
4484 auto board = std::make_unique<BOARD>();
4485 FOOTPRINT* fp = new FOOTPRINT( board.get() );
4486 fp->SetPosition( VECTOR2I( 50000000, 50000000 ) );
4487 fp->SetTransformScale( 2.0, 2.0 );
4488 board->Add( fp );
4489
4490 PCB_TEXTBOX* tb = new PCB_TEXTBOX( fp );
4492 tb->SetStart( VECTOR2I( 50000000, 50000000 ) );
4493 tb->SetEnd( VECTOR2I( 60000000, 55000000 ) );
4494 tb->SetText( wxT( "TB" ) );
4495 tb->SetLayer( F_SilkS );
4496 tb->SetTextSize( VECTOR2I( 2000000, 1000000 ) );
4497 tb->SetTextThickness( 300000 );
4498 tb->SetBorderEnabled( true );
4499 tb->SetWidth( 400000 );
4500 fp->Add( tb );
4501
4502 const VECTOR2I txtSize = tb->GetTextSize();
4503 const int txtThickness = tb->GetTextThickness();
4504 const int borderWidth = tb->GetStroke().GetWidth();
4505
4506 std::filesystem::path tmp = std::filesystem::temp_directory_path() / "kicad_qa_textbox_roundtrip.kicad_pcb";
4507 KI_TEST::DumpBoardToFile( *board, tmp.string() );
4508 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( tmp.string() );
4509 std::error_code ec;
4510 std::filesystem::remove( tmp, ec );
4511 BOOST_REQUIRE( reloaded );
4512
4513 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
4514 PCB_TEXTBOX* tb2 = nullptr;
4515
4516 for( BOARD_ITEM* it : fp2->GraphicalItems() )
4517 {
4518 if( it->Type() == PCB_TEXTBOX_T )
4519 tb2 = static_cast<PCB_TEXTBOX*>( it );
4520 }
4521
4522 BOOST_REQUIRE( tb2 );
4523
4524 BOOST_CHECK_EQUAL( tb2->GetTextSize().x, txtSize.x );
4525 BOOST_CHECK_EQUAL( tb2->GetTextSize().y, txtSize.y );
4526 BOOST_CHECK_EQUAL( tb2->GetTextThickness(), txtThickness );
4527 BOOST_CHECK_EQUAL( tb2->GetStroke().GetWidth(), borderWidth );
4528}
4529
4530
4531// Build a 2x2 table. Call with the footprint at origin, unrotated, so the
4532// cell start/end land in lib coords.
4534{
4535 PCB_TABLE* table = new PCB_TABLE( aFp, 0 );
4536 table->SetColCount( 2 );
4537 table->SetColWidth( 0, 16000000 );
4538 table->SetColWidth( 1, 18000000 );
4539 table->SetRowHeight( 0, 5000000 );
4540 table->SetRowHeight( 1, 5000000 );
4541
4542 const VECTOR2I starts[4] = {
4543 { -4000000, 6000000 }, { 12000000, 6000000 }, { -4000000, 11000000 }, { 12000000, 11000000 }
4544 };
4545 const VECTOR2I ends[4] = {
4546 { 12000000, 11000000 }, { 30000000, 11000000 }, { 12000000, 16000000 }, { 30000000, 16000000 }
4547 };
4548
4549 for( int ii = 0; ii < 4; ++ii )
4550 {
4551 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
4552 cell->SetColSpan( 1 );
4553 cell->SetRowSpan( 1 );
4554 cell->SetText( wxT( "x" ) );
4555 cell->SetStart( starts[ii] );
4556 cell->SetEnd( ends[ii] );
4557 table->AddCell( cell );
4558 }
4559
4560 return table;
4561}
4562
4563
4564// A 34x10 table fits in ~36mm. The bug blew it past 1 metre, so a loose bound catches it.
4565static void checkTableSane( PCB_TABLE* aTable, const VECTOR2I& aNearPos )
4566{
4567 BOX2I bbox = aTable->GetBoundingBox();
4568
4569 BOOST_CHECK_MESSAGE( bbox.GetWidth() > 0 && bbox.GetWidth() < 50000000 && bbox.GetHeight() > 0
4570 && bbox.GetHeight() < 50000000,
4571 "table bbox size out of range ( " << bbox.GetWidth() << ", " << bbox.GetHeight() << " )" );
4572
4573 VECTOR2I center = bbox.GetCenter();
4574 BOOST_CHECK_MESSAGE( std::abs( center.x - aNearPos.x ) < 50000000 && std::abs( center.y - aNearPos.y ) < 50000000,
4575 "table bbox center ( " << center.x << ", " << center.y << " ) far from footprint ( "
4576 << aNearPos.x << ", " << aNearPos.y << " )" );
4577
4578 // Cells must stay a distinct grid, not collapse onto each other.
4579 BOOST_CHECK( aTable->GetCells()[0]->GetBoundingBox().GetCenter()
4580 != aTable->GetCells()[3]->GetBoundingBox().GetCenter() );
4581}
4582
4583
4584// Rebaking a POLY cell must not transform its unseeded lib start/end, which used to overflow.
4585BOOST_AUTO_TEST_CASE( PolyTextBoxRebakeDoesNotOverflowOnStaleStartEnd )
4586{
4587 FOOTPRINT fp( nullptr );
4588 fp.SetPosition( VECTOR2I( 100000000, 60000000 ) );
4589
4590 PCB_TEXTBOX* tb = new PCB_TEXTBOX( &fp );
4591 tb->SetShape( SHAPE_T::POLY );
4592
4593 SHAPE_POLY_SET poly;
4594 poly.NewOutline();
4595 poly.Append( VECTOR2I( -4000000, 6000000 ) );
4596 poly.Append( VECTOR2I( 12000000, 6000000 ) );
4597 poly.Append( VECTOR2I( 12000000, 11000000 ) );
4598 poly.Append( VECTOR2I( -4000000, 11000000 ) );
4599 tb->SetPolyShape( poly );
4600 tb->OverrideLibPoly( poly );
4601
4602 // Simulate the stale, unseeded lib start/end that loaded poly cells carry.
4603 tb->OverrideLibCoords( VECTOR2I( 2000000000, 2000000000 ), VECTOR2I( 2000000000, 2000000000 ) );
4604 fp.Add( tb, ADD_MODE::APPEND );
4605
4606 // Rebake through the rotation. This used to convert the stale lib start/end and overflow.
4607 fp.SetOrientation( EDA_ANGLE( 33.0, DEGREES_T ) );
4608
4609 // The polygon is rebaked to a sane, in-range size (16 x 5 mm rotated).
4610 const BOX2I polyBox = tb->GetPolyShape().BBox();
4611 BOOST_CHECK( polyBox.GetWidth() > 0 && polyBox.GetWidth() < 50000000 );
4612 BOOST_CHECK( polyBox.GetHeight() > 0 && polyBox.GetHeight() < 50000000 );
4613}
4614
4615
4616// A non-cardinally rotated table stays sane when the footprint is scaled in one axis.
4617BOOST_AUTO_TEST_CASE( RotatedTableSurvivesNonUniformScale )
4618{
4619 for( double angle : { 0.0, 90.0, 33.0, 217.5 } )
4620 {
4621 BOARD board;
4622 FOOTPRINT* fp = new FOOTPRINT( &board );
4623 board.Add( fp );
4624 fp->SetPosition( VECTOR2I( 0, 0 ) );
4625
4626 PCB_TABLE* table = buildLibTable( fp );
4627 fp->Add( table, ADD_MODE::APPEND );
4628
4629 fp->SetPosition( VECTOR2I( 101000000, 68000000 ) );
4630 fp->SetOrientation( EDA_ANGLE( angle, DEGREES_T ) );
4631 fp->SetTransformScale( 1.0, 2.0 );
4632
4633 BOOST_TEST_CONTEXT( "orientation " << angle )
4634 checkTableSane( table, VECTOR2I( 101000000, 68000000 ) );
4635 }
4636}
4637
4638
4639BOOST_AUTO_TEST_CASE( TableInRotatedFootprintStaysSane )
4640{
4641 for( double angle : { 0.0, 90.0, 180.0, 270.0, 33.0, 217.5 } )
4642 {
4643 BOARD board;
4644 FOOTPRINT* fp = new FOOTPRINT( &board );
4645 board.Add( fp );
4646 fp->SetPosition( VECTOR2I( 0, 0 ) );
4647
4648 PCB_TABLE* table = buildLibTable( fp );
4649 fp->Add( table, ADD_MODE::APPEND );
4650
4651 fp->SetPosition( VECTOR2I( 101000000, 68000000 ) );
4652 fp->SetOrientation( EDA_ANGLE( angle, DEGREES_T ) );
4653
4654 BOOST_TEST_CONTEXT( "orientation " << angle )
4655 checkTableSane( table, VECTOR2I( 101000000, 68000000 ) );
4656 }
4657}
4658
4659
4660// Non-cardinal rotation makes cells polygons, so compare the table bbox.
4661BOOST_FIXTURE_TEST_CASE( TableInNonCardinalRotatedFootprintSurvivesSaveLoad, BOARD_FIXTURE )
4662{
4663 KI_TEST::LoadBoard( m_settingsManager, "issue18", m_board );
4664
4665 FOOTPRINT* fp = *m_board->Footprints().begin();
4666 BOOST_REQUIRE( fp );
4667
4668 fp->SetPosition( VECTOR2I( 0, 0 ) );
4669 fp->SetOrientation( ANGLE_0 );
4670
4671 PCB_TABLE* table = buildLibTable( fp );
4672 fp->Add( table, ADD_MODE::APPEND );
4673
4674 fp->SetPosition( VECTOR2I( 12345678, -7654321 ) );
4675 fp->SetOrientation( EDA_ANGLE( 33.0, DEGREES_T ) );
4676
4677 BOX2I bboxBefore = table->GetBoundingBox();
4678
4679 const std::filesystem::path savePath =
4680 std::filesystem::temp_directory_path() / "table_in_rotated_scaled_fp.kicad_pcb";
4681
4682 KI_TEST::DumpBoardToFile( *m_board, savePath.string() );
4683 std::unique_ptr<BOARD> reloaded = KI_TEST::ReadBoardFromFileOrStream( savePath.string() );
4684 BOOST_REQUIRE( reloaded );
4685
4686 FOOTPRINT* fp2 = *reloaded->Footprints().begin();
4687 BOOST_REQUIRE( fp2 );
4688
4689 PCB_TABLE* table2 = nullptr;
4690
4691 for( BOARD_ITEM* item : fp2->GraphicalItems() )
4692 {
4693 if( item->Type() == PCB_TABLE_T )
4694 table2 = static_cast<PCB_TABLE*>( item );
4695 }
4696
4697 BOOST_REQUIRE( table2 );
4698
4699 BOX2I bboxAfter = table2->GetBoundingBox();
4700
4701 // Tolerance covers poly rounding at non-cardinal angles. The bug was off by tens of mm.
4702 const int tol = 300000;
4703
4704 BOOST_CHECK_MESSAGE( std::abs( bboxAfter.GetOrigin().x - bboxBefore.GetOrigin().x ) <= tol
4705 && std::abs( bboxAfter.GetOrigin().y - bboxBefore.GetOrigin().y ) <= tol
4706 && std::abs( bboxAfter.GetWidth() - bboxBefore.GetWidth() ) <= tol
4707 && std::abs( bboxAfter.GetHeight() - bboxBefore.GetHeight() ) <= tol,
4708 "table bbox after reload o( "
4709 << bboxAfter.GetOrigin().x << ", " << bboxAfter.GetOrigin().y << " ) sz( "
4710 << bboxAfter.GetWidth() << ", " << bboxAfter.GetHeight() << " ) expected o( "
4711 << bboxBefore.GetOrigin().x << ", " << bboxBefore.GetOrigin().y << " ) sz( "
4712 << bboxBefore.GetWidth() << ", " << bboxBefore.GetHeight() << " )" );
4713}
4714
4715
4716// Issue 24734: v10 tables in rotated footprints used to explode on load.
4717// Each table must land near its footprint with a bounded size.
4718BOOST_FIXTURE_TEST_CASE( LegacyEmbeddedTableInRotatedFootprintLoads, BOARD_FIXTURE )
4719{
4720 KI_TEST::LoadBoard( m_settingsManager, "embedded_table_rotated_legacy_v10", m_board );
4721
4722 int tableCount = 0;
4723
4724 for( FOOTPRINT* fp : m_board->Footprints() )
4725 {
4726 for( BOARD_ITEM* item : fp->GraphicalItems() )
4727 {
4728 if( item->Type() != PCB_TABLE_T )
4729 continue;
4730
4731 tableCount++;
4732 BOOST_TEST_CONTEXT( "footprint orientation " << fp->GetOrientation().AsDegrees() )
4733 checkTableSane( static_cast<PCB_TABLE*>( item ), fp->GetPosition() );
4734 }
4735 }
4736
4737 BOOST_CHECK_EQUAL( tableCount, 5 );
4738}
4739
4740
4741// A footprint scale must scale the whole table, not just the cell text.
4742BOOST_AUTO_TEST_CASE( TableScalesWithFootprint )
4743{
4744 BOARD board;
4745 FOOTPRINT* fp = new FOOTPRINT( &board );
4746 board.Add( fp );
4747 fp->SetPosition( VECTOR2I( 0, 0 ) );
4748
4749 PCB_TABLE* table = buildLibTable( fp );
4750 fp->Add( table, ADD_MODE::APPEND );
4751
4752 BOX2I before = table->GetBoundingBox();
4753
4754 fp->SetTransformScale( 2.0, 1.5 );
4755
4756 BOX2I after = table->GetBoundingBox();
4757
4758 const double wRatio = (double) after.GetWidth() / before.GetWidth();
4759 const double hRatio = (double) after.GetHeight() / before.GetHeight();
4760
4761 BOOST_CHECK_MESSAGE( wRatio > 1.9 && wRatio < 2.1, "table width ratio " << wRatio << " expected ~2.0" );
4762 BOOST_CHECK_MESSAGE( hRatio > 1.4 && hRatio < 1.6, "table height ratio " << hRatio << " expected ~1.5" );
4763}
4764
4765
4766// Scaling a footprint must scale a custom pad's primitives, not just the anchor.
4767BOOST_AUTO_TEST_CASE( CustomPadShapeScalesWithFootprint )
4768{
4769 BOARD board;
4770 FOOTPRINT* fp = new FOOTPRINT( &board );
4771 board.Add( fp );
4772 fp->SetPosition( VECTOR2I( 0, 0 ) );
4773
4774 PAD* pad = new PAD( fp );
4776 pad->SetAnchorPadShape( PADSTACK::ALL_LAYERS, PAD_SHAPE::CIRCLE );
4777 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( 500000, 500000 ) );
4778 pad->SetLayerSet( PAD::SMDMask() );
4779
4780 std::vector<VECTOR2I> poly = {
4781 { -1000000, -1000000 }, { 1000000, -1000000 }, { 1000000, 1000000 }, { -1000000, 1000000 }
4782 };
4783 pad->AddPrimitivePoly( PADSTACK::ALL_LAYERS, poly, 0, true );
4784 fp->Add( pad, ADD_MODE::APPEND );
4785
4786 BOX2I before = pad->GetBoundingBox();
4787
4788 fp->SetTransformScale( 2.0, 1.5 );
4789
4790 BOX2I after = pad->GetBoundingBox();
4791
4792 const double wRatio = (double) after.GetWidth() / before.GetWidth();
4793 const double hRatio = (double) after.GetHeight() / before.GetHeight();
4794
4795 BOOST_CHECK_MESSAGE( wRatio > 1.9 && wRatio < 2.1, "custom pad width ratio " << wRatio << " expected ~2.0" );
4796 BOOST_CHECK_MESSAGE( hRatio > 1.4 && hRatio < 1.6, "custom pad height ratio " << hRatio << " expected ~1.5" );
4797}
4798
4799
const char * name
@ ERROR_OUTSIDE
@ ERROR_INSIDE
constexpr int ARC_HIGH_DEF
Definition base_units.h:137
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
constexpr BOX2I KiROUND(const BOX2D &aBoxD)
Definition box2.h:986
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:81
void SwapItemData(BOARD_ITEM *aImage)
Swap data between aItem and aImage.
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:313
VECTOR2I GetFPRelativePosition() const
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1346
constexpr size_type GetWidth() const
Definition box2.h:210
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:654
constexpr const Vec GetCenter() const
Definition box2.h:226
constexpr size_type GetHeight() const
Definition box2.h:211
constexpr const Vec & GetOrigin() const
Definition box2.h:206
constexpr const SizeVec & GetSize() const
Definition box2.h:202
CN_EDGE represents a point-to-point connection, whether realized or unrealized (ie: tracks etc.
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
EDA_ANGLE Normalize()
Definition eda_angle.h:229
double AsDegrees() const
Definition eda_angle.h:116
int GetEllipseMinorRadius() const
Definition eda_shape.h:310
const VECTOR2I & GetBezierC2() const
Definition eda_shape.h:283
const VECTOR2I & GetEllipseCenter() const
Definition eda_shape.h:292
EDA_ANGLE GetEllipseEndAngle() const
Definition eda_shape.h:338
int GetEllipseMajorRadius() const
Definition eda_shape.h:301
SHAPE_POLY_SET & GetPolyShape()
EDA_ANGLE GetEllipseRotation() const
Definition eda_shape.h:319
SHAPE_T GetShape() const
Definition eda_shape.h:185
virtual void SetFilled(bool aFlag)
Definition eda_shape.h:152
void RebuildBezierToSegmentsPointsList(int aMaxError)
Rebuild the m_bezierPoints vertex list that approximate the Bezier curve by a list of segments.
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
EDA_ANGLE GetEllipseStartAngle() const
Definition eda_shape.h:329
const std::vector< VECTOR2I > & GetBezierPoints() const
Definition eda_shape.h:404
const VECTOR2I & GetBezierC1() const
Definition eda_shape.h:280
bool IsPolyShapeValid() const
virtual void SetTextPos(const VECTOR2I &aPoint)
Definition eda_text.cpp:576
BOX2I GetTextBox(const RENDER_SETTINGS *aSettings, int aLine=-1) const
Useful in multiline texts to calculate the full text or a line area (for zones filling,...
Definition eda_text.cpp:773
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:265
void SetPosition(const VECTOR2I &aPos) override
EDA_ANGLE GetOrientation() const
Definition footprint.h:406
PCB_POINTS & Points()
Definition footprint.h:387
void SetOrientation(const EDA_ANGLE &aNewAngle)
const TRANSFORM_TRS & GetTransform() const
Definition footprint.h:419
EDA_ITEM * Clone() const override
Invoke a function on all children.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
void MoveAnchorPosition(const VECTOR2I &aMoveVector)
Move the reference point of the footprint.
void SetScaleY(double aScaleY)
Definition footprint.h:428
std::deque< PAD * > & Pads()
Definition footprint.h:375
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition footprint.h:417
double GetScaleX() const
Definition footprint.h:423
bool IsFlipped() const
Definition footprint.h:614
void Move(const VECTOR2I &aMoveVector) override
Move this object.
void SetTransformScale(double aScaleX, double aScaleY)
PCB_FIELD & Reference()
Definition footprint.h:878
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
void SetScaleX(double aScaleX)
Definition footprint.h:426
void RescaleAroundPoint(const VECTOR2I &aCenter, double aSx, double aSy)
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void Flip(const VECTOR2I &aCentre, FLIP_DIRECTION aFlipDirection) override
Flip this object, i.e.
void GetFields(std::vector< PCB_FIELD * > &aVector, bool aVisibleOnly) const
Populate a std::vector with PCB_TEXTs.
const SHAPE_POLY_SET & GetCourtyard(PCB_LAYER_ID aLayer) const
Used in DRC to test the courtyard area (a complex polygon).
double GetScaleY() const
Definition footprint.h:424
VECTOR2I GetPosition() const override
Definition footprint.h:403
DRAWINGS & GraphicalItems()
Definition footprint.h:378
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:61
void SetAttribute(PAD_ATTRIB aAttribute)
Definition pad.cpp:1615
const std::vector< std::shared_ptr< PCB_SHAPE > > & GetPrimitives(PCB_LAYER_ID aLayer) const
Accessor to the basic shape list for custom-shaped pads.
Definition pad.h:370
const BOX2I GetBoundingBox() const override
The bounding box is cached, so this will be efficient most of the time.
Definition pad.cpp:1599
static LSET PTHMask()
layer set for a through hole pad
Definition pad.cpp:579
VECTOR2I GetPosition() const override
Definition pad.cpp:245
VECTOR2I GetDrillSize() const
Definition pad.h:315
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:287
void SetPosition(const VECTOR2I &aPos) override
Definition pad.cpp:234
void SetSize(PCB_LAYER_ID aLayer, const VECTOR2I &aSize)
Definition pad.cpp:254
static LSET SMDMask()
layer set for a SMD pad on Front layer
Definition pad.cpp:586
void SetLayerSet(const LSET &aLayers) override
Definition pad.cpp:1931
void SetTextSize(int aTextSize)
Change the height of the human-readable text displayed below the barcode.
double GetOrientation() const
const VECTOR2I & GetLibraryPos() const
const EDA_ANGLE & GetLibraryAngle() const
void SetWidth(int aWidth)
void AssembleBarcode() const
Assemble the barcode polygon and text polygons into a single polygonal representation.
void SetHeight(int aHeight)
VECTOR2I GetPosition() const override
Get the position (center) of the barcode in internal units.
void SetPosition(const VECTOR2I &aPos) override
void SetOrientation(double aDegrees)
void SetLayer(PCB_LAYER_ID aLayer) override
Set the drawing layer for the barcode and its text.
EDA_ANGLE GetAngle() const
void SetText(const wxString &aText)
Set the barcode content text to encode.
virtual void SetEnd(const VECTOR2I &aPoint)
virtual void SetStart(const VECTOR2I &aPoint)
const VECTOR2I & GetLibraryStart() const
virtual VECTOR2I GetEnd() const
const VECTOR2I & GetLibraryEnd() const
virtual VECTOR2I GetStart() const
The dimension's origin is the first feature point for the dimension.
void SetKeepTextAligned(bool aKeepAligned)
For better understanding of the points that make a dimension:
A PCB_POINT is a 0-dimensional point that is used to mark a position on a PCB, or more usually a foot...
Definition pcb_point.h:39
void SetPosition(const VECTOR2I &aPos) override
Definition pcb_point.cpp:73
VECTOR2I GetPosition() const override
Definition pcb_point.h:60
void SetEllipseCenter(const VECTOR2I &aPt) override
EDA_ANGLE GetLibraryEllipseEndAngle() const
Definition pcb_shape.h:229
void SetBezierC1(const VECTOR2I &aPt) override
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
VECTOR2I GetLibraryBezierC1() const
void SetWidth(int aWidth) override
void OverrideLibPoly(const SHAPE_POLY_SET &aPoly)
Definition pcb_shape.h:273
void OverrideLibBezier(const VECTOR2I &aC1, const VECTOR2I &aC2)
Definition pcb_shape.h:267
int GetLibraryEllipseMinorRadius() const
Definition pcb_shape.h:226
void SetEllipseStartAngle(const EDA_ANGLE &aA) override
EDA_ANGLE GetLibraryEllipseStartAngle() const
Definition pcb_shape.h:228
int GetLibraryEllipseMajorRadius() const
Definition pcb_shape.h:225
EDA_ANGLE GetLibraryEllipseRotation() const
Definition pcb_shape.h:227
SHAPE_T GetLibraryShape() const
Definition pcb_shape.h:222
void SetEllipseEndAngle(const EDA_ANGLE &aA) override
VECTOR2I GetLibraryEllipseCenter() const
Definition pcb_shape.h:224
void SetEnd(const VECTOR2I &aEnd) override
void Flip(const VECTOR2I &aCentre, FLIP_DIRECTION aFlipDirection) override
Flip this object, i.e.
VECTOR2I GetLibraryEnd() const
Definition pcb_shape.h:221
void SetArcGeometry(const VECTOR2I &aStart, const VECTOR2I &aMid, const VECTOR2I &aEnd)
void SetPolyShape(const SHAPE_POLY_SET &aShape) override
void SetEllipseRotation(const EDA_ANGLE &aA) override
VECTOR2I GetLibraryStart() const
Definition pcb_shape.h:220
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void OverrideLibCoords(const VECTOR2I &aStart, const VECTOR2I &aEnd, const VECTOR2I &aArcMid=VECTOR2I(0, 0))
Definition pcb_shape.h:258
void SetEllipseMinorRadius(int aR) override
STROKE_PARAMS GetStroke() const override
void SetStart(const VECTOR2I &aStart) override
void SetBezierC2(const VECTOR2I &aPt) override
VECTOR2I GetLibraryBezierC2() const
void RebakeFromLib()
VECTOR2I GetLibraryArcMid() const
void SetEllipseMajorRadius(int aR) override
void SetRowSpan(int aSpan)
void SetColSpan(int aSpan)
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
std::vector< PCB_TABLECELL * > GetCells() const
Definition pcb_table.h:156
VECTOR2I GetPosition() const override
EDA_ANGLE GetTextAngle() const override
int GetTextThickness() const override
void SetBorderEnabled(bool enabled)
VECTOR2I GetTextSize() const override
void SetShape(SHAPE_T aShape) override
void SetTextAngle(const EDA_ANGLE &aAngle) override
void SetTextThickness(int aWidth) override
The TextThickness is that set by the user.
void SetTextSize(VECTOR2I aNewSize, bool aEnforceMinTextSize=true) override
EDA_ANGLE GetTextAngle() const override
Definition pcb_text.cpp:543
void SetTextSize(VECTOR2I aNewSize, bool aEnforceMinTextSize=true) override
Definition pcb_text.cpp:467
VECTOR2I GetTextPos() const override
Definition pcb_text.cpp:444
void TransformTextToPolySet(SHAPE_POLY_SET &aBuffer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc) const
Function TransformTextToPolySet Convert the text to a polygonSet describing the actual character stro...
Definition pcb_text.cpp:769
int GetTextThickness() const override
Definition pcb_text.cpp:480
void SetTextAngle(const EDA_ANGLE &aAngle) override
Definition pcb_text.cpp:552
VECTOR2I GetTextSize() const override
Definition pcb_text.cpp:453
int PointCount() const
Return the number of points (vertices) in this line chain.
const VECTOR2I & CPoint(int aIndex) const
Return a reference to a given point in the line chain.
const std::vector< VECTOR2I > & CPoints() const
Represent a set of closed polygons.
ITERATOR IterateWithHoles(int aOutline)
double Area()
Return the area of this poly set.
void SetVertex(const VERTEX_INDEX &aIndex, const VECTOR2I &aPos)
Accessor function to set the position of a specific point.
int TotalVertices() const
Return total number of vertices stored in the set.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int NewOutline()
Creates a new empty polygon in the set and returns its index.
const VECTOR2I & CVertex(int aIndex, int aOutline, int aHole) const
Return the index-th vertex in a given hole outline within a given outline.
int OutlineCount() const
Return the number of outlines in the set.
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
int GetWidth() const
double GetScaleX() const
VECTOR2I Apply(const VECTOR2I &aPoint) const
double GetScaleY() const
bool IsIdentity() const
const VECTOR2I & GetTranslate() const
T EuclideanNorm() const
Compute the Euclidean norm of the vector, which is defined as sqrt(x ** 2 + y ** 2).
Definition vector2d.h:279
Handle a list of polygons defining a copper zone.
Definition zone.h:70
void SetNeedRefill(bool aNeedRefill)
Definition zone.h:310
bool NeedRefill() const
Definition zone.h:309
void TransformSmoothedOutlineToPolygon(SHAPE_POLY_SET &aBuffer, int aClearance, int aError, ERROR_LOC aErrorLoc, SHAPE_POLY_SET *aBoardOutline) const
Convert the outlines shape to a polygon with no holes inflated (optional) by max( aClearanceValue,...
Definition zone.cpp:1859
const BOX2I GetBoundingBox() const override
Definition zone.cpp:766
bool IsFilled() const
Definition zone.h:306
bool HitTestForCorner(const VECTOR2I &refPos, int aAccuracy, SHAPE_POLY_SET::VERTEX_INDEX *aCornerHit=nullptr) const
Test if the given VECTOR2I is near a corner.
Definition zone.cpp:900
SHAPE_POLY_SET * Outline()
Definition zone.h:418
void Move(const VECTOR2I &offset) override
Move the outlines.
Definition zone.cpp:1206
void SetIsRuleArea(bool aEnable)
Definition zone.h:814
void CacheBoundingBox()
Used to preload the zone bounding box cache so we don't have to worry about mutex-locking it each tim...
Definition zone.cpp:800
SHAPE_POLY_SET GetBoardOutline() const
Definition zone.cpp:874
void Rotate(const VECTOR2I &aCentre, const EDA_ANGLE &aAngle) override
Rotate the outlines.
Definition zone.cpp:1287
bool HitTestFilledArea(PCB_LAYER_ID aLayer, const VECTOR2I &aRefPos, int aAccuracy=0) const
Test if the given VECTOR2I is within the bounds of a filled area of this zone.
Definition zone.cpp:1018
void SetIsFilled(bool isFilled)
Definition zone.h:307
SHAPE_POLY_SET GetLibraryOutline() const
Definition zone.cpp:868
virtual std::shared_ptr< SHAPE > GetEffectiveShape(PCB_LAYER_ID aLayer=UNDEFINED_LAYER, FLASHING aFlash=FLASHING::DEFAULT) const override
Some pad shapes can be complex (rounded/chamfered rectangle), even without considering custom shapes.
Definition zone.cpp:1884
@ DRCE_FOOTPRINT_SCALED_WITH_PADS
Definition drc_item.h:83
@ DRCE_FIRST
Definition drc_item.h:35
@ DRCE_LAST
Definition drc_item.h:121
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:411
@ DEGREES_T
Definition eda_angle.h:31
static constexpr EDA_ANGLE ANGLE_180
Definition eda_angle.h:415
@ ELLIPSE
Definition eda_shape.h:52
@ SEGMENT
Definition eda_shape.h:46
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
@ ELLIPSE_ARC
Definition eda_shape.h:53
@ ALWAYS_FLASHED
Always flashed for connectivity.
Definition layer_ids.h:182
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_CrtYd
Definition layer_ids.h:112
@ B_Cu
Definition layer_ids.h:61
@ F_SilkS
Definition layer_ids.h:96
@ F_Cu
Definition layer_ids.h:60
FLIP_DIRECTION
Definition mirror.h:23
@ LEFT_RIGHT
Flip left to right (around the Y axis)
Definition mirror.h:24
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
Definition mirror.h:25
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
void DumpBoardToFile(BOARD &board, const std::string &aFilename)
Utility function to simply write a Board out to a file.
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
BARCODE class definition.
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_IGNORE
std::unique_ptr< BOARD > m_board
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
static void CHECK_SHAPE_LIBPOS_MIRROR(const FOOTPRINT &aFp)
BOOST_AUTO_TEST_CASE(DefaultIsIdentity)
BOOST_FIXTURE_TEST_CASE(PadTransformInvariantOnLoadedBoard, BOARD_FIXTURE)
static void checkTableSane(PCB_TABLE *aTable, const VECTOR2I &aNearPos)
static PCB_TABLE * buildLibTable(FOOTPRINT *aFp)
static void seedSquareLibOutline(ZONE *aZone, int aHalfSide=1000000)
static void CHECK_TRANSFORM_PAD_INVARIANT(const FOOTPRINT &aFp)
static FOOTPRINT * buildCoincidentFootprint(BOARD &aBoard, const EDA_ANGLE &aOrient, double aScaleX, double aScaleY, const VECTOR2I &aCoincident, PAD *&aPad, PCB_SHAPE *&aSeg, PCB_TEXT *&aTxt, PCB_POINT *&aPt)
static void CHECK_ALL_COINCIDENT(PAD *aPad, PCB_SHAPE *aSeg, PCB_TEXT *aTxt, PCB_POINT *aPt, const std::string &aWhen)
static void CHECK_TRANSFORM_MATCHES_LEGACY(const FOOTPRINT &aFp)
static void CHECK_PAD_LIBPOS_MIRROR(const FOOTPRINT &aFp)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
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))
std::vector< std::vector< std::string > > table
VECTOR2I center
BOOST_TEST_CONTEXT("Test Clearance")
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
int actual
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:225
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
@ PCB_TEXTBOX_T
class PCB_TEXTBOX, wrapped text on a layer
Definition typeinfo.h:86
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:85
@ PCB_DIM_ALIGNED_T
class PCB_DIM_ALIGNED, a linear dimension (graphic item)
Definition typeinfo.h:95
@ PCB_TABLE_T
class PCB_TABLE, table of PCB_TABLECELLs
Definition typeinfo.h:87
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683