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