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