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