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