KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_multichannel.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
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
22#include <board.h>
24#include <pad.h>
25#include <netinfo.h>
26#include <base_units.h>
27#include <pcb_group.h>
28#include <pcb_generator.h>
29#include <pcb_shape.h>
30#include <pcb_track.h>
31#include <pcb_text.h>
32#include <pcb_field.h>
33#include <footprint.h>
34#include <zone.h>
35#include <drc/drc_item.h>
41#include <lib_id.h>
42#include <atomic>
43
51
53{
54public:
57
58 virtual wxWindow* GetToolCanvas() const override { return nullptr; }
59};
60
61BOOST_FIXTURE_TEST_SUITE( MultichannelTool, MULTICHANNEL_TEST_FIXTURE )
62
63RULE_AREA* findRuleAreaByPartialName( MULTICHANNEL_TOOL* aTool, const wxString& aName )
64{
65 for( RULE_AREA& ra : aTool->GetData()->m_areas )
66 {
67 if( ra.m_ruleName.Contains( ( aName ) ) )
68 return &ra;
69 }
70
71 return nullptr;
72}
73
75{
76 if( aPath.EndsWith( wxT( "/" ) ) )
77 aPath.RemoveLast();
78
79 for( RULE_AREA& ra : aTool->GetData()->m_areas )
80 {
81 if( ra.m_sourceType != PLACEMENT_SOURCE_T::SHEETNAME )
82 continue;
83
84 wxString path = ra.m_sheetPath;
85
86 if( path.EndsWith( wxT( "/" ) ) )
87 path.RemoveLast();
88
89 if( path == aPath )
90 return &ra;
91 }
92
93 return nullptr;
94}
95
96RULE_AREA* findGroupRuleAreaByName( MULTICHANNEL_TOOL* aTool, const wxString& aGroupName )
97{
98 for( RULE_AREA& ra : aTool->GetData()->m_areas )
99 {
100 if( ra.m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT && ra.m_groupName == aGroupName )
101 return &ra;
102 }
103
104 return nullptr;
105}
106
107RULE_AREA* findRuleAreaByPlacementGroup( MULTICHANNEL_TOOL* aTool, const wxString& aGroupName )
108{
109 for( RULE_AREA& ra : aTool->GetData()->m_areas )
110 {
111 if( ra.m_zone && ra.m_zone->GetPlacementAreaSource() == aGroupName )
112 return &ra;
113 }
114
115 return nullptr;
116}
117
118int countTuningGensInArea( BOARD* aBoard, ZONE* aZone )
119{
120 int n = 0;
121 const SHAPE_LINE_CHAIN& outline = aZone->Outline()->COutline( 0 );
122
123 for( PCB_GENERATOR* gen : aBoard->Generators() )
124 {
125 if( gen->GetGeneratorType() != wxT( "tuning_pattern" ) )
126 continue;
127
128 if( gen->HitTest( outline, false ) )
129 n++;
130 }
131
132 return n;
133}
134
135
136int countCopperZonesInArea( BOARD* aBoard, ZONE* aRAZone )
137{
138 int n = 0;
139
140 for( ZONE* zone : aBoard->Zones() )
141 {
142 if( zone == aRAZone || zone->GetIsRuleArea() )
143 continue;
144
145 if( aRAZone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
146 n++;
147 }
148
149 return n;
150}
151
152
153int countZonesByNameInRuleArea( BOARD* aBoard, const wxString& aZoneName, const RULE_AREA& aRuleArea )
154{
155 int count = 0;
156
157 for( const ZONE* zone : aBoard->Zones() )
158 {
159 if( zone == aRuleArea.m_zone )
160 continue;
161
162 if( zone->GetZoneName() != aZoneName )
163 continue;
164
165 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
166 count++;
167 }
168
169 return count;
170}
171
172
173int countZonesByNamePrefixInRuleArea( BOARD* aBoard, const wxString& aBaseName, const RULE_AREA& aRuleArea )
174{
175 int count = 0;
176
177 for( const ZONE* zone : aBoard->Zones() )
178 {
179 if( zone == aRuleArea.m_zone )
180 continue;
181
182 const wxString& name = zone->GetZoneName();
183
184 // A copied zone may get a _<n> suffix for uniqueness (issue 23131).
185 if( name != aBaseName && !name.StartsWith( aBaseName + wxT( "_" ) ) )
186 continue;
187
188 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
189 count++;
190 }
191
192 return count;
193}
194
195
197{
199
200 std::vector<wxString> tests = { "vme-wren" };
201
202 for( const wxString& relPath : tests )
203 {
204 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
205
206 TOOL_MANAGER toolMgr;
207 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
208
209 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
210
211 MULTICHANNEL_TOOL* mtTool = new MULTICHANNEL_TOOL; // TOOL_MANAGER owns the tools
212 toolMgr.RegisterTool( mtTool );
213
214 //RULE_AREAS_DATA* raData = m_parentTool->GetData();
215
217
218 auto ruleData = mtTool->GetData();
219
220 BOOST_TEST_MESSAGE( wxString::Format( "RA multichannel sheets = %d",
221 static_cast<int>( ruleData->m_areas.size() ) ) );
222
223 // 73 counts the /fpga/ container sheet, now offered as a channel.
224 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 73 );
225
226 int cnt = 0;
227
228 ruleData->m_replaceExisting = true;
229
230 for( RULE_AREA& ra : ruleData->m_areas )
231 {
232 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" )
233 || ra.m_sheetName == wxT( "pp_driver_2x.kicad_sch" ) )
234 {
235 ra.m_generateEnabled = true;
236 cnt++;
237 }
238 }
239
240 BOOST_TEST_MESSAGE( wxString::Format( "Autogenerating %d RAs", cnt ) );
241
242 TOOL_EVENT dummyEvent;
243
244 mtTool->AutogenerateRuleAreas( dummyEvent );
245 mtTool->FindExistingRuleAreas();
246
247 int n_areas_io = 0, n_areas_pp = 0, n_areas_other = 0;
248
249 BOOST_TEST_MESSAGE( wxString::Format( "Found %d RAs after commit",
250 static_cast<int>(ruleData->m_areas.size() ) ) );
251
252 for( const RULE_AREA& ra : ruleData->m_areas )
253 {
254 BOOST_TEST_MESSAGE( wxString::Format( "SN '%s'", ra.m_ruleName ) );
255
256 if( ra.m_ruleName.Contains( wxT( "io_drivers_fp" ) ) )
257 {
258 n_areas_io++;
259 BOOST_CHECK_EQUAL( ra.m_components.size(), 31 );
260 }
261 else if( ra.m_ruleName.Contains( wxT( "io_drivers_pp" ) ) )
262 {
263 n_areas_pp++;
264 BOOST_CHECK_EQUAL( ra.m_components.size(), 11 );
265 }
266 else
267 {
268 n_areas_other++;
269 }
270 }
271
272 BOOST_TEST_MESSAGE( wxString::Format( "IO areas=%d, PP areas=%d, others=%d",
273 n_areas_io, n_areas_pp, n_areas_other ) );
274
275 BOOST_CHECK_EQUAL( n_areas_io, 16 );
276 BOOST_CHECK_EQUAL( n_areas_pp, 16 );
277 BOOST_CHECK_EQUAL( n_areas_other, 0 );
278
279 const std::vector<wxString> rulesToTest = { wxT( "io_drivers_fp" ),
280 wxT( "io_drivers_pp" ) };
281
282 for( const wxString& ruleName : rulesToTest )
283 {
284 for( const RULE_AREA& refArea : ruleData->m_areas )
285 {
286 if( !refArea.m_ruleName.Contains( ruleName ) )
287 continue;
288
289 BOOST_TEST_MESSAGE( wxString::Format( "REF AREA: '%s'", refArea.m_ruleName ) );
290
291 for( const RULE_AREA& targetArea : ruleData->m_areas )
292 {
293 if( targetArea.m_zone == refArea.m_zone )
294 continue;
295
296 if( !targetArea.m_ruleName.Contains( ruleName ) )
297 continue;
298
299 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea.m_components,
300 targetArea.m_components );
301 auto cgTarget =
302 CONNECTION_GRAPH::BuildFromFootprintSet( targetArea.m_components,
303 refArea.m_components );
304
306
307 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
308 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
309
310 BOOST_TEST_MESSAGE( wxString::Format(
311 "topo match: '%s' [%d] -> '%s' [%d] result %d", refArea.m_ruleName.c_str().AsChar(),
312 static_cast<int>( refArea.m_components.size() ), targetArea.m_ruleName.c_str().AsChar(),
313 static_cast<int>( targetArea.m_components.size() ), status ? 1 : 0 ) );
314
315 for( const auto& iter : result )
316 {
317 BOOST_TEST_MESSAGE( wxString::Format( "%s : %s",
318 iter.second->GetReference(),
319 iter.first->GetReference() ) );
320 }
321
322 BOOST_CHECK( status );
323 BOOST_CHECK( details.empty() );
324 }
325 }
326 }
327
328 auto refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
329
330 BOOST_ASSERT( refArea );
331
332 const std::vector<wxString> targetAreaNames( { wxT( "io_drivers_fp/bank2/io78/" ),
333 wxT( "io_drivers_fp/bank1/io78/" ),
334 wxT( "io_drivers_fp/bank0/io01/" ) } );
335
336 for( const wxString& targetRaName : targetAreaNames )
337 {
338 auto targetRA = findRuleAreaByPartialName( mtTool, targetRaName );
339
340 BOOST_ASSERT( targetRA != nullptr );
341
342 BOOST_TEST_MESSAGE( wxString::Format( "Clone to: %s", targetRA->m_ruleName ) );
343
344 ruleData->m_compatMap[targetRA].m_doCopy = true;
345 }
346
347 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
348
349 BOOST_ASSERT( result >= 0 );
350 }
351}
352
353
358BOOST_FIXTURE_TEST_CASE( RepeatLayoutCopiesFootprintProperties, MULTICHANNEL_TEST_FIXTURE )
359{
360 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
361
362 TOOL_MANAGER toolMgr;
363 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
364
365 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
366
368 toolMgr.RegisterTool( mtTool );
369
370 mtTool->FindExistingRuleAreas();
371
372 auto ruleData = mtTool->GetData();
373
374 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
375 static_cast<int>( ruleData->m_areas.size() ) ) );
376
377 BOOST_CHECK( ruleData->m_areas.size() >= 2 );
378
379 if( ruleData->m_areas.size() < 2 )
380 return;
381
382 RULE_AREA* refArea = nullptr;
383 RULE_AREA* targetArea = nullptr;
384
385 for( RULE_AREA& ra : ruleData->m_areas )
386 {
387 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
388 refArea = &ra;
389 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
390 targetArea = &ra;
391 }
392
393 if( !refArea || !targetArea )
394 {
395 BOOST_TEST_MESSAGE( "Could not find Untitled Sheet and Untitled Sheet1 rule areas, skipping test" );
396 return;
397 }
398
399 BOOST_TEST_MESSAGE( wxString::Format( "Reference area: %s, Target area: %s",
400 refArea->m_ruleName, targetArea->m_ruleName ) );
401
402 FOOTPRINT* refFP = nullptr;
403 FOOTPRINT* targetFP = nullptr;
404
405 for( FOOTPRINT* fp : refArea->m_components )
406 {
407 if( fp->GetReference().StartsWith( wxT( "U1" ) ) )
408 {
409 refFP = fp;
410 break;
411 }
412 }
413
414 for( FOOTPRINT* fp : targetArea->m_components )
415 {
416 if( fp->GetReference().StartsWith( wxT( "U2" ) ) )
417 {
418 targetFP = fp;
419 break;
420 }
421 }
422
423 if( !refFP || !targetFP )
424 {
425 BOOST_TEST_MESSAGE( "Could not find matching footprints in the rule areas, skipping test" );
426 return;
427 }
428
429 PCB_FIELD* refValueField = refFP->GetField( FIELD_T::VALUE );
430 bool refValueVisible = refValueField ? refValueField->IsVisible() : true;
431
432 std::vector<FP_3DMODEL> refModels = refFP->Models();
433
434 mtTool->CheckRACompatibility( refArea->m_zone );
435
436 ruleData->m_compatMap[targetArea].m_doCopy = true;
437 ruleData->m_options.m_copyPlacement = true;
438
439 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
440
441 BOOST_CHECK( result >= 0 );
442
443 PCB_FIELD* targetValueField = targetFP->GetField( FIELD_T::VALUE );
444
445 if( targetValueField && refValueField )
446 {
447 BOOST_CHECK_EQUAL( targetValueField->IsVisible(), refValueVisible );
448 BOOST_TEST_MESSAGE( wxString::Format( "Value field visibility: ref=%d, target=%d",
449 refValueVisible, targetValueField->IsVisible() ) );
450 }
451
452 BOOST_CHECK_EQUAL( targetFP->Models().size(), refModels.size() );
453
454 if( !refModels.empty() )
455 {
456 BOOST_TEST_MESSAGE( wxString::Format( "3D models: ref=%d, target=%d",
457 static_cast<int>( refModels.size() ),
458 static_cast<int>( targetFP->Models().size() ) ) );
459 }
460}
461
462
470BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotRemoveReferenceVias, MULTICHANNEL_TEST_FIXTURE )
471{
472 KI_TEST::LoadBoard( m_settingsManager, "issue21184/issue21184", m_board );
473
474 TOOL_MANAGER toolMgr;
475 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
476
477 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
478
480 toolMgr.RegisterTool( mtTool );
481
482 mtTool->FindExistingRuleAreas();
483
484 auto ruleData = mtTool->GetData();
485
486 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
487 static_cast<int>( ruleData->m_areas.size() ) ) );
488
489 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 2 );
490
491 if( ruleData->m_areas.size() < 2 )
492 return;
493
494 RULE_AREA* refArea = nullptr;
495 RULE_AREA* targetArea = nullptr;
496
497 for( RULE_AREA& ra : ruleData->m_areas )
498 {
499 if( ra.m_ruleName == wxT( "Test1" ) )
500 refArea = &ra;
501 else if( ra.m_ruleName == wxT( "Test2" ) )
502 targetArea = &ra;
503 }
504
505 BOOST_REQUIRE( refArea != nullptr );
506 BOOST_REQUIRE( targetArea != nullptr );
507
508 int refViaCountBefore = 0;
509
510 for( PCB_TRACK* track : m_board->Tracks() )
511 {
512 if( track->Type() == PCB_VIA_T )
513 {
514 PCB_VIA* via = static_cast<PCB_VIA*>( track );
515 VECTOR2I viaPos = via->GetPosition();
516
517 if( refArea->m_zone->Outline()->Contains( viaPos ) )
518 refViaCountBefore++;
519 }
520 }
521
522 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias before repeat: %d", refViaCountBefore ) );
523 BOOST_CHECK( refViaCountBefore > 0 );
524
525 mtTool->CheckRACompatibility( refArea->m_zone );
526
527 ruleData->m_compatMap[targetArea].m_doCopy = true;
528 ruleData->m_options.m_copyPlacement = true;
529 ruleData->m_options.m_copyRouting = true;
530
531 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
532
533 BOOST_CHECK( result >= 0 );
534
535 int refViaCountAfter = 0;
536
537 for( PCB_TRACK* track : m_board->Tracks() )
538 {
539 if( track->Type() == PCB_VIA_T )
540 {
541 PCB_VIA* via = static_cast<PCB_VIA*>( track );
542 VECTOR2I viaPos = via->GetPosition();
543
544 if( refArea->m_zone->Outline()->Contains( viaPos ) )
545 refViaCountAfter++;
546 }
547 }
548
549 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias after repeat: %d", refViaCountAfter ) );
550
551 BOOST_CHECK_EQUAL( refViaCountAfter, refViaCountBefore );
552}
553
554
559BOOST_FIXTURE_TEST_CASE( RepeatLayoutRespectsZoneLayerSetsForOtherItems, MULTICHANNEL_TEST_FIXTURE )
560{
561 KI_TEST::LoadBoard( m_settingsManager, "issue22983/issue22983", m_board );
562
563 TOOL_MANAGER toolMgr;
564 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
565
566 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
567
569 toolMgr.RegisterTool( mtTool );
570
571 mtTool->FindExistingRuleAreas();
572
573 RULE_AREA* sourceA = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceA" ) );
574 RULE_AREA* destA = findRuleAreaByPlacementGroup( mtTool, wxT( "DestA" ) );
575 RULE_AREA* sourceB = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceB" ) );
576 RULE_AREA* destB = findRuleAreaByPlacementGroup( mtTool, wxT( "DestB" ) );
577
578 BOOST_REQUIRE( sourceA != nullptr );
579 BOOST_REQUIRE( destA != nullptr );
580 BOOST_REQUIRE( sourceB != nullptr );
581 BOOST_REQUIRE( destB != nullptr );
582
583 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *sourceA ), 1 );
584 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ), 0 );
586 countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *sourceB ), 1 );
587 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
588 0 );
589 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *sourceB ), 1 );
590 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
591
592 REPEAT_LAYOUT_OPTIONS options;
593 options.m_copyPlacement = false;
594 options.m_copyRouting = false;
595 options.m_copyOtherItems = true;
596 options.m_includeLockedItems = true;
597
598 int copyAStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceA, *destA, options );
599 BOOST_REQUIRE( copyAStatus >= 0 );
600
601 int copyBStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceB, *destB, options );
602 BOOST_REQUIRE( copyBStatus >= 0 );
603
604 // SourceA and DestA both include F.Cu+B.Cu, so this multilayer zone should copy.
605 // The copy is renamed for uniqueness (issue 23131), so match the base name as a prefix.
606 BOOST_CHECK_EQUAL( countZonesByNamePrefixInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ),
607 1 );
608
609 // SourceB only includes F.Cu, so this F.Cu+B.Cu zone should not copy to DestB.
610 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
611 0 );
612
613 // SourceB excludes B.Cu, so this B.Cu-only zone should not copy either.
614 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
615}
616
617
626{
628 using TMATCH::COMPONENT;
629
630 // Create two connection graphs with components that have dotted reference designators
631 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
632 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
633
634 // Create mock footprints with the same FPID
635 LIB_ID fpid( wxT( "Package_SO" ), wxT( "SOIC-8_3.9x4.9mm_P1.27mm" ) );
636
637 // Create reference footprint TRIM_1.1 and target footprint TRIM_2.1
638 FOOTPRINT fpRef( nullptr );
639 fpRef.SetFPID( fpid );
640 fpRef.SetReference( wxT( "TRIM_1.1" ) );
641
642 FOOTPRINT fpTarget( nullptr );
643 fpTarget.SetFPID( fpid );
644 fpTarget.SetReference( wxT( "TRIM_2.1" ) );
645
646 // Create matching pad structures
647 PAD padRef1( &fpRef );
648 padRef1.SetNumber( wxT( "1" ) );
649 padRef1.SetNetCode( 1 );
650 fpRef.Add( &padRef1 );
651
652 PAD padRef2( &fpRef );
653 padRef2.SetNumber( wxT( "2" ) );
654 padRef2.SetNetCode( 2 );
655 fpRef.Add( &padRef2 );
656
657 PAD padTarget1( &fpTarget );
658 padTarget1.SetNumber( wxT( "1" ) );
659 padTarget1.SetNetCode( 3 );
660 fpTarget.Add( &padTarget1 );
661
662 PAD padTarget2( &fpTarget );
663 padTarget2.SetNumber( wxT( "2" ) );
664 padTarget2.SetNetCode( 4 );
665 fpTarget.Add( &padTarget2 );
666
667 // Build connection graphs
668 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
669 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
670
671 cgRef->BuildConnectivity();
672 cgTarget->BuildConnectivity();
673
674 // Check that the components are considered the same kind
675 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
676 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
677
678 COMPONENT* cmpRef = cgRef->Components()[0];
679 COMPONENT* cmpTarget = cgTarget->Components()[0];
680
681 bool sameKind = cmpRef->IsSameKind( *cmpTarget );
682
683 BOOST_TEST_MESSAGE( wxString::Format( "TRIM_1.1 and TRIM_2.1 IsSameKind: %d", sameKind ? 1 : 0 ) );
684 BOOST_CHECK( sameKind );
685
686 // Test topology matching
688 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
689 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
690
691 BOOST_TEST_MESSAGE( wxString::Format( "Topology match result: %d", status ? 1 : 0 ) );
692
693 if( !status && !details.empty() )
694 {
695 for( const auto& reason : details )
696 {
697 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
698 reason.m_reference, reason.m_candidate, reason.m_reason ) );
699 }
700 }
701
702 BOOST_CHECK( status );
703 BOOST_CHECK( details.empty() );
704
705 // Cleanup: remove pads before footprints go out of scope
706 fpRef.Pads().clear();
707 fpTarget.Pads().clear();
708}
709
710
717BOOST_FIXTURE_TEST_CASE( TopologyMatchUnannotatedReference, MULTICHANNEL_TEST_FIXTURE )
718{
720 using TMATCH::COMPONENT;
721
722 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
723 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
724
725 LIB_ID fpid( wxT( "_BugFpLib" ), wxT( "SW_Hotswap" ) );
726
727 // Reference footprint as stored in the design block: never annotated, so it keeps the
728 // default REF** placeholder.
729 FOOTPRINT fpRef( nullptr );
730 fpRef.SetFPID( fpid );
731 fpRef.SetReference( wxT( "REF**" ) );
732
733 // Destination footprint placed and annotated on the board.
734 FOOTPRINT fpTarget( nullptr );
735 fpTarget.SetFPID( fpid );
736 fpTarget.SetReference( wxT( "SW1" ) );
737
738 PAD padRef1( &fpRef );
739 padRef1.SetNumber( wxT( "1" ) );
740 padRef1.SetNetCode( 1 );
741 fpRef.Add( &padRef1 );
742
743 PAD padRef2( &fpRef );
744 padRef2.SetNumber( wxT( "2" ) );
745 padRef2.SetNetCode( 2 );
746 fpRef.Add( &padRef2 );
747
748 PAD padTarget1( &fpTarget );
749 padTarget1.SetNumber( wxT( "1" ) );
750 padTarget1.SetNetCode( 3 );
751 fpTarget.Add( &padTarget1 );
752
753 PAD padTarget2( &fpTarget );
754 padTarget2.SetNumber( wxT( "2" ) );
755 padTarget2.SetNetCode( 4 );
756 fpTarget.Add( &padTarget2 );
757
758 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
759 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
760
761 cgRef->BuildConnectivity();
762 cgTarget->BuildConnectivity();
763
764 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
765 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
766
767 COMPONENT* cmpRef = cgRef->Components()[0];
768 COMPONENT* cmpTarget = cgTarget->Components()[0];
769
770 BOOST_CHECK_MESSAGE( cmpRef->IsSameKind( *cmpTarget ),
771 "REF** placeholder must be the same kind as an annotated footprint "
772 "with matching FPID (issue 24585)" );
773
775 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
776 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
777
778 if( !status )
779 {
780 for( const auto& reason : details )
781 {
782 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
783 reason.m_reference, reason.m_candidate, reason.m_reason ) );
784 }
785 }
786
787 BOOST_CHECK_MESSAGE( status,
788 "Design block layout with an un-annotated REF** footprint must match "
789 "an annotated destination (issue 24585)" );
790 BOOST_CHECK( details.empty() );
791
792 fpRef.Pads().clear();
793 fpTarget.Pads().clear();
794}
795
796
802BOOST_FIXTURE_TEST_CASE( TopologyMatchAnnotatedReferencesKeepPrefixCheck, MULTICHANNEL_TEST_FIXTURE )
803{
804 using TMATCH::COMPONENT;
805
806 LIB_ID fpid( wxT( "_BugFpLib" ), wxT( "SW_Hotswap" ) );
807 LIB_ID otherFpid( wxT( "_BugFpLib" ), wxT( "R_0402" ) );
808
809 FOOTPRINT fpTarget( nullptr );
810 fpTarget.SetFPID( fpid );
811 fpTarget.SetReference( wxT( "SW1" ) );
812 COMPONENT cmpTarget( fpTarget.GetReference(), &fpTarget );
813
814 // Unrelated annotated prefixes with the same FPID must not match.
815 FOOTPRINT fpUnrelated( nullptr );
816 fpUnrelated.SetFPID( fpid );
817 fpUnrelated.SetReference( wxT( "U1A" ) );
818 COMPONENT cmpUnrelated( fpUnrelated.GetReference(), &fpUnrelated );
819
820 BOOST_CHECK_MESSAGE( !cmpUnrelated.IsSameKind( cmpTarget ),
821 "Two annotated footprints with unrelated prefixes must not match even "
822 "with the same FPID (issue 24585)" );
823
824 // The placeholder wildcard must not bypass the footprint-ID requirement.
825 FOOTPRINT fpPlaceholder( nullptr );
826 fpPlaceholder.SetFPID( otherFpid );
827 fpPlaceholder.SetReference( wxT( "REF**" ) );
828 COMPONENT cmpPlaceholder( fpPlaceholder.GetReference(), &fpPlaceholder );
829
830 BOOST_CHECK_MESSAGE( !cmpPlaceholder.IsSameKind( cmpTarget ),
831 "A REF** placeholder must not match a target with a different FPID "
832 "(issue 24585)" );
833}
834
835
844BOOST_FIXTURE_TEST_CASE( GenerateRuleAreasIncludesChildSheets, MULTICHANNEL_TEST_FIXTURE )
845{
846 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
847
848 TOOL_MANAGER toolMgr;
849 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
850
851 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
852
854 toolMgr.RegisterTool( mtTool );
855
857
858 auto ruleData = mtTool->GetData();
859
860 RULE_AREA* leafArea = nullptr;
861 RULE_AREA* midArea = nullptr;
862 RULE_AREA* topArea = nullptr;
863
864 for( RULE_AREA& ra : ruleData->m_areas )
865 {
866 if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/io01/" ) )
867 leafArea = &ra;
868 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/" ) )
869 midArea = &ra;
870 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/" ) )
871 topArea = &ra;
872 }
873
874 BOOST_REQUIRE( leafArea != nullptr );
875 BOOST_REQUIRE( midArea != nullptr );
876 BOOST_REQUIRE( topArea != nullptr );
877
878 BOOST_TEST_MESSAGE( wxString::Format( "Leaf /io_drivers_fp/bank0/io01/ components: %d",
879 static_cast<int>( leafArea->m_components.size() ) ) );
880 BOOST_TEST_MESSAGE( wxString::Format( "Mid /io_drivers_fp/bank0/ components: %d",
881 static_cast<int>( midArea->m_components.size() ) ) );
882 BOOST_TEST_MESSAGE( wxString::Format( "Top /io_drivers_fp/ components: %d",
883 static_cast<int>( topArea->m_components.size() ) ) );
884
885 // Leaf sheet has 31 direct components and no children
886 BOOST_CHECK_EQUAL( leafArea->m_components.size(), 31 );
887
888 // Mid-level sheet has 7 direct + 4 child sheets * 31 each = 131
889 BOOST_CHECK_EQUAL( midArea->m_components.size(), 131 );
890
891 // Top-level sheet has 3 direct + 4 banks * 131 each = 527
892 BOOST_CHECK_EQUAL( topArea->m_components.size(), 527 );
893
894 // Mid-level components must be a superset of leaf components
895 for( FOOTPRINT* fp : leafArea->m_components )
896 BOOST_CHECK( midArea->m_components.count( fp ) > 0 );
897
898 // Top-level components must be a superset of mid-level components
899 for( FOOTPRINT* fp : midArea->m_components )
900 BOOST_CHECK( topArea->m_components.count( fp ) > 0 );
901}
902
903
909{
911
912 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
913
914 TOOL_MANAGER toolMgr;
915 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
916
917 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
918
920 toolMgr.RegisterTool( mtTool );
921
923
924 auto ruleData = mtTool->GetData();
925
926 ruleData->m_replaceExisting = true;
927
928 for( RULE_AREA& ra : ruleData->m_areas )
929 {
930 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" ) )
931 ra.m_generateEnabled = true;
932 }
933
934 TOOL_EVENT dummyEvent;
935 mtTool->AutogenerateRuleAreas( dummyEvent );
936 mtTool->FindExistingRuleAreas();
937
938 RULE_AREA* refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
939 RULE_AREA* targetArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank2/io78/" ) );
940
941 BOOST_REQUIRE( refArea != nullptr );
942 BOOST_REQUIRE( targetArea != nullptr );
943
944 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
945 targetArea->m_components );
946 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
947 refArea->m_components );
948
949 // Pre-cancelled: should return false immediately with empty result
950 {
951 std::atomic<bool> cancelled( true );
952 std::atomic<int> matched( 0 );
953 std::atomic<int> total( 0 );
954
956 params.m_cancelled = &cancelled;
957 params.m_matchedComponents = &matched;
958 params.m_totalComponents = &total;
959
961 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
962
963 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
964
965 BOOST_CHECK( !status );
966 BOOST_CHECK( result.empty() );
967
968 BOOST_TEST_MESSAGE( "Pre-cancelled FindIsomorphism correctly returned false" );
969 }
970
971 // Normal run with progress reporting
972 {
973 std::atomic<bool> cancelled( false );
974 std::atomic<int> matched( 0 );
975 std::atomic<int> total( 0 );
976
978 params.m_cancelled = &cancelled;
979 params.m_matchedComponents = &matched;
980 params.m_totalComponents = &total;
981
983 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
984
985 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
986
987 BOOST_CHECK( status );
988
989 int finalMatched = matched.load();
990 int finalTotal = total.load();
991
992 BOOST_TEST_MESSAGE( wxString::Format( "Progress: matched=%d, total=%d", finalMatched, finalTotal ) );
993
994 BOOST_CHECK( finalTotal > 0 );
995 BOOST_CHECK_EQUAL( finalMatched, finalTotal );
996 }
997
998 // Sanity check: same graphs without params still succeed
999 {
1001 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1002
1003 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1004
1005 BOOST_CHECK( status );
1006 BOOST_CHECK( !result.empty() );
1007
1008 BOOST_TEST_MESSAGE( "Default params FindIsomorphism still succeeds" );
1009 }
1010}
1011
1012
1023BOOST_FIXTURE_TEST_CASE( TopoMatchGlobalNetHierarchicalPins, MULTICHANNEL_TEST_FIXTURE )
1024{
1026
1027 KI_TEST::LoadBoard( m_settingsManager, "issue21739/topology_mismatch", m_board );
1028
1029 TOOL_MANAGER toolMgr;
1030 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1031
1032 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1033
1035 toolMgr.RegisterTool( mtTool );
1036
1037 mtTool->FindExistingRuleAreas();
1038
1039 auto ruleData = mtTool->GetData();
1040
1041 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1042 static_cast<int>( ruleData->m_areas.size() ) ) );
1043
1044 BOOST_REQUIRE( ruleData->m_areas.size() >= 2 );
1045
1046 RULE_AREA* ch0Area = nullptr;
1047 RULE_AREA* ch1Area = nullptr;
1048
1049 for( RULE_AREA& ra : ruleData->m_areas )
1050 {
1051 if( !ra.m_zone )
1052 continue;
1053
1054 wxString source = ra.m_zone->GetPlacementAreaSource();
1055
1056 if( source == wxT( "/i2c_thingy_ch0/" ) )
1057 ch0Area = &ra;
1058 else if( source == wxT( "/i2c_thingy_ch1/" ) )
1059 ch1Area = &ra;
1060 }
1061
1062 BOOST_REQUIRE_MESSAGE( ch0Area != nullptr, "Could not find i2c_thingy_ch0 rule area" );
1063 BOOST_REQUIRE_MESSAGE( ch1Area != nullptr, "Could not find i2c_thingy_ch1 rule area" );
1064
1065 BOOST_TEST_MESSAGE( wxString::Format( "ch0 components: %d, ch1 components: %d",
1066 static_cast<int>( ch0Area->m_components.size() ),
1067 static_cast<int>( ch1Area->m_components.size() ) ) );
1068
1069 BOOST_CHECK_EQUAL( ch0Area->m_components.size(), ch1Area->m_components.size() );
1070
1071 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( ch0Area->m_components,
1072 ch1Area->m_components );
1073 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( ch1Area->m_components,
1074 ch0Area->m_components );
1075
1077 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1078 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1079
1080 if( !status )
1081 {
1082 for( const auto& reason : details )
1083 {
1084 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
1085 reason.m_reference, reason.m_candidate,
1086 reason.m_reason ) );
1087 }
1088 }
1089
1090 BOOST_CHECK_MESSAGE( status,
1091 "Topology match failed for channels with hierarchical pins "
1092 "tied to global nets (issue 21739)" );
1093}
1094
1095
1102BOOST_FIXTURE_TEST_CASE( TopoMatchBoundarySignalNetNotExcluded, MULTICHANNEL_TEST_FIXTURE )
1103{
1105
1106 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
1107
1108 // The board forces consecutive net codes, so register by name and use the assigned code.
1109 auto addNet = [&]( const wxString& aName ) -> int
1110 {
1111 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
1112 board->Add( net );
1113 return net->GetNetCode();
1114 };
1115
1116 const int netGnd = addNet( wxT( "GND" ) );
1117 const int netVcc = addNet( wxT( "+5VP" ) );
1118 const int netChainInRef = addNet( wxT( "Net-(D-RefChainIn)" ) );
1119 const int netBridge = addNet( wxT( "Net-(D38-DOUT)" ) ); // reference output AND target input
1120 const int netChainOutTgt = addNet( wxT( "Net-(D43-DOUT)" ) );
1121 const int netD2Dout = addNet( wxT( "unconnected-(D2-DOUT)" ) );
1122 const int netD3Dout = addNet( wxT( "unconnected-(D3-DOUT)" ) );
1123 const int netD5Dout = addNet( wxT( "unconnected-(D5-DOUT)" ) );
1124 const int netD6Dout = addNet( wxT( "unconnected-(D6-DOUT)" ) );
1125
1126 LIB_ID ledId( wxT( "TestLib" ), wxT( "WS2812" ) );
1127
1128 // Four-pad addressable LED: pad 1 = DOUT, pad 2 = GND, pad 3 = DIN, pad 4 = VCC.
1129 auto makeLed = [&]( const wxString& aRef, int aDout, int aDin ) -> FOOTPRINT*
1130 {
1131 FOOTPRINT* fp = new FOOTPRINT( board.get() );
1132 fp->SetFPID( ledId );
1133 fp->SetReference( aRef );
1134 board->Add( fp );
1135
1136 auto addPad = [&]( const wxString& aNumber, int aNetCode )
1137 {
1138 PAD* pad = new PAD( fp );
1139 pad->SetNumber( aNumber );
1141 pad->SetSize( PADSTACK::ALL_LAYERS,
1142 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1143 pad->SetLayerSet( LSET( { F_Cu } ) );
1144 pad->SetNetCode( aNetCode );
1145 fp->Add( pad );
1146 };
1147
1148 addPad( wxT( "1" ), aDout );
1149 addPad( wxT( "2" ), netGnd );
1150 addPad( wxT( "3" ), aDin );
1151 addPad( wxT( "4" ), netVcc );
1152
1153 return fp;
1154 };
1155
1156 // Reference area (design block source instance): three LEDs share the DIN rail; only the
1157 // representative LED drives the chain output (the bridge net), the other two are unconnected.
1158 std::set<FOOTPRINT*> refFps;
1159 refFps.insert( makeLed( wxT( "D1" ), netBridge, netChainInRef ) );
1160 refFps.insert( makeLed( wxT( "D2" ), netD2Dout, netChainInRef ) );
1161 refFps.insert( makeLed( wxT( "D3" ), netD3Dout, netChainInRef ) );
1162
1163 // Target area (downstream instance): the three LEDs' DIN rail IS the bridge net coming from
1164 // the design block source, and the representative LED drives a fresh chain output.
1165 std::set<FOOTPRINT*> tgtFps;
1166 tgtFps.insert( makeLed( wxT( "D4" ), netChainOutTgt, netBridge ) );
1167 tgtFps.insert( makeLed( wxT( "D5" ), netD5Dout, netBridge ) );
1168 tgtFps.insert( makeLed( wxT( "D6" ), netD6Dout, netBridge ) );
1169
1170 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
1171 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
1172
1174 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1175 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1176
1177 if( !status )
1178 {
1179 for( const auto& reason : details )
1180 {
1181 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
1182 reason.m_reference, reason.m_candidate,
1183 reason.m_reason ) );
1184 }
1185 }
1186
1187 BOOST_CHECK_MESSAGE( status,
1188 "Topology match failed because the design-block boundary signal net "
1189 "was misclassified as a global rail and excluded asymmetrically" );
1190 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
1191}
1192
1193
1203BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutCopiesSilkscreen, MULTICHANNEL_TEST_FIXTURE )
1204{
1205 m_board = std::make_unique<BOARD>();
1206 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1207
1208 // Net for the single connection between the two footprints in each block.
1209 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1210 m_board->Add( net );
1211
1212 auto makeFootprint =
1213 [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1214 {
1215 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1216 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
1217 fp->SetReference( aRef );
1218 fp->SetPosition( aPos );
1219
1220 PAD* pad = new PAD( fp );
1221 pad->SetNumber( wxT( "1" ) );
1222 pad->SetNet( net );
1223 pad->SetPosition( aPos );
1224 pad->SetSize( F_Cu,
1225 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1226 pad->SetLayerSet( LSET( { F_Cu } ) );
1227 fp->Add( pad );
1228
1229 m_board->Add( fp );
1230 return fp;
1231 };
1232
1233 // Source: a "design block" with two matched footprints and a silkscreen rectangle that
1234 // is not associated with any footprint.
1235 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ),
1236 VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
1237 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ),
1238 VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ) );
1239
1240 PCB_SHAPE* silkRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1241 silkRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1242 silkRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 12 ), pcbIUScale.mmToIU( 2 ) ) );
1243 silkRect->SetLayer( F_SilkS );
1244 silkRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1245 m_board->Add( silkRect );
1246
1247 // Destination: a group containing the matched pair so RepeatLayout can find a parent group.
1248 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ),
1249 VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1250 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ),
1251 VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1252
1253 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1254 destGroup->SetName( wxT( "design-block-dest" ) );
1255 destGroup->AddItem( destFp1 );
1256 destGroup->AddItem( destFp2 );
1257 m_board->Add( destGroup );
1258
1259 // Unrelated silkscreen drawing inside the destination bounding box but not part of the
1260 // destination group. Apply Design Block Layout must not delete or claim this item.
1261 PCB_SHAPE* unrelatedSilk = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1262 unrelatedSilk->SetStart( VECTOR2I( pcbIUScale.mmToIU( 52 ), pcbIUScale.mmToIU( 52 ) ) );
1263 unrelatedSilk->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 58 ), pcbIUScale.mmToIU( 52 ) ) );
1264 unrelatedSilk->SetLayer( F_SilkS );
1265 unrelatedSilk->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1266 m_board->Add( unrelatedSilk );
1267
1268 int silkBefore = 0;
1269
1270 for( BOARD_ITEM* item : m_board->Drawings() )
1271 {
1272 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1273 silkBefore++;
1274 }
1275
1276 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1277
1278 RULE_AREA dbRA;
1280 dbRA.m_components.insert( refFp1 );
1281 dbRA.m_components.insert( refFp2 );
1282 dbRA.m_designBlockItems.insert( refFp1 );
1283 dbRA.m_designBlockItems.insert( refFp2 );
1284 dbRA.m_designBlockItems.insert( silkRect );
1285
1286 // The Apply Design Block flow uses a synthetic copper-only rule area zone. The destination
1287 // zone is a temporary zone never added to the board.
1288 dbRA.m_zone = new ZONE( m_board.get() );
1289 dbRA.m_zone->SetIsRuleArea( true );
1292 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1293 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1294
1295 RULE_AREA destRA;
1297 destRA.m_components.insert( destFp1 );
1298 destRA.m_components.insert( destFp2 );
1299
1300 destRA.m_zone = new ZONE( m_board.get() );
1301 destRA.m_zone->SetIsRuleArea( true );
1302 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1304 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1305 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1306
1307 TOOL_MANAGER toolMgr;
1308 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1309 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1310
1312 toolMgr.RegisterTool( mtTool );
1313
1314 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1315 .m_connectedRoutingOnly = false,
1316 .m_copyPlacement = true,
1317 .m_copyOtherItems = true,
1318 .m_groupItems = false,
1319 .m_includeLockedItems = true,
1320 .m_anchorFp = nullptr };
1321
1322 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1323 BOOST_CHECK_MESSAGE( result >= 0, "RepeatLayout failed" );
1324
1325 delete dbRA.m_zone;
1326 delete destRA.m_zone;
1327
1328 // Verify a third silkscreen shape (the duplicated rectangle) was added and grouped under
1329 // the destination group, that the unrelated silk segment was preserved, and that the
1330 // duplicated rectangle landed inside the destination region.
1331 int silkAfter = 0;
1332 int silkInGroup = 0;
1333 bool unrelatedSurvived = false;
1334 PCB_SHAPE* copiedRect = nullptr;
1335
1336 for( BOARD_ITEM* item : m_board->Drawings() )
1337 {
1338 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1339 continue;
1340
1341 silkAfter++;
1342
1343 if( item == unrelatedSilk )
1344 unrelatedSurvived = true;
1345
1346 if( item->GetParentGroup() == destGroup )
1347 {
1348 silkInGroup++;
1349
1350 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1351
1352 if( shape->GetShape() == SHAPE_T::RECTANGLE )
1353 copiedRect = shape;
1354 }
1355 }
1356
1357 BOOST_CHECK_MESSAGE( silkAfter == 3,
1358 wxString::Format( "Expected 3 silkscreen shapes after Apply Design Block "
1359 "Layout (2 original + 1 copy), found %d (issue 24372)",
1360 silkAfter ) );
1361 BOOST_CHECK_MESSAGE( silkInGroup == 1,
1362 wxString::Format( "Expected 1 silkscreen shape in destination group, "
1363 "found %d (issue 24372)",
1364 silkInGroup ) );
1365 BOOST_CHECK_MESSAGE( unrelatedSurvived,
1366 "Unrelated silkscreen drawing outside the design block group was "
1367 "deleted by Apply Design Block Layout (issue 24372)" );
1368 BOOST_REQUIRE( copiedRect != nullptr );
1369
1370 // The copied rectangle should sit near the destination footprints (offset by ~50mm from
1371 // the source position), not at the original source location.
1372 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1373}
1374
1375
1381BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutRotatesFieldsOnce, MULTICHANNEL_TEST_FIXTURE )
1382{
1383 m_board = std::make_unique<BOARD>();
1384 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1385
1386 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1387 m_board->Add( net );
1388
1389 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1390 {
1391 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1392 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
1393 fp->SetReference( aRef );
1394 fp->SetPosition( aPos );
1395
1396 PAD* pad = new PAD( fp );
1397 pad->SetNumber( wxT( "1" ) );
1398 pad->SetNet( net );
1399 pad->SetPosition( aPos );
1400 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1401 pad->SetLayerSet( LSET( { F_Cu } ) );
1402 fp->Add( pad );
1403
1404 m_board->Add( fp );
1405 return fp;
1406 };
1407
1408 const EDA_ANGLE sourceFieldAngle( 10.0, DEGREES_T );
1409 const EDA_ANGLE blockRotation( 45.0, DEGREES_T );
1410
1411 // Source design block: two matched footprints at orientation 0 with their Value field tilted.
1412 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ), VECTOR2I( pcbIUScale.mmToIU( 0 ), 0 ) );
1413 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1414
1415 for( FOOTPRINT* fp : { refFp1, refFp2 } )
1416 fp->GetField( FIELD_T::VALUE )->SetTextAngle( sourceFieldAngle );
1417
1418 // Destination: matched pair placed at 45 deg so the block is applied with a 45 deg rotation.
1419 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1420 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1421
1422 for( FOOTPRINT* fp : { destFp1, destFp2 } )
1423 fp->SetOrientation( blockRotation );
1424
1425 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1426 destGroup->SetName( wxT( "design-block-dest" ) );
1427 destGroup->AddItem( destFp1 );
1428 destGroup->AddItem( destFp2 );
1429 m_board->Add( destGroup );
1430
1431 RULE_AREA dbRA;
1433 dbRA.m_components.insert( refFp1 );
1434 dbRA.m_components.insert( refFp2 );
1435 dbRA.m_designBlockItems.insert( refFp1 );
1436 dbRA.m_designBlockItems.insert( refFp2 );
1437
1438 dbRA.m_zone = new ZONE( m_board.get() );
1439 dbRA.m_zone->SetIsRuleArea( true );
1441 dbRA.m_zone->AddPolygon(
1443 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1444
1445 RULE_AREA destRA;
1447 destRA.m_components.insert( destFp1 );
1448 destRA.m_components.insert( destFp2 );
1449
1450 destRA.m_zone = new ZONE( m_board.get() );
1451 destRA.m_zone->SetIsRuleArea( true );
1452 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1454 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1455 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1456
1457 TOOL_MANAGER toolMgr;
1458 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1459 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1460
1462 toolMgr.RegisterTool( mtTool );
1463
1464 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1465 .m_connectedRoutingOnly = false,
1466 .m_copyPlacement = true,
1467 .m_copyOtherItems = true,
1468 .m_groupItems = false,
1469 .m_includeLockedItems = true,
1470 .m_anchorFp = nullptr };
1471
1472 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1473 BOOST_REQUIRE_MESSAGE( result >= 0, "RepeatLayout failed" );
1474
1475 delete dbRA.m_zone;
1476 delete destRA.m_zone;
1477
1478 // Each placed field must be rotated by the block rotation exactly once: 10 deg + 45 deg = 55 deg.
1479 // The double-rotation regression produced 90 deg.
1480 const double expected = ( sourceFieldAngle + blockRotation ).AsDegrees();
1481
1482 for( FOOTPRINT* fp : { destFp1, destFp2 } )
1483 {
1484 PCB_FIELD* valueField = fp->GetField( FIELD_T::VALUE );
1485 BOOST_REQUIRE( valueField != nullptr );
1486
1487 double actual = valueField->GetTextAngle().Normalize().AsDegrees();
1488
1489 BOOST_CHECK_MESSAGE( std::abs( actual - expected ) < 1e-3,
1490 wxString::Format( "Field on %s rotated to %.3f deg, expected %.3f deg "
1491 "(field double-rotation regression)",
1492 fp->GetReference(), actual, expected ) );
1493 }
1494}
1495
1496
1502BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutMatchesBySymbolPathWhenTopologyDiffers, MULTICHANNEL_TEST_FIXTURE )
1503{
1505
1506 m_board = std::make_unique<BOARD>();
1507 m_board->SetEnabledLayers( LSET::AllCuMask() );
1508
1509 NETINFO_ITEM* shared = new NETINFO_ITEM( m_board.get(), wxT( "Net-(J1-Pin_1)" ) );
1510 m_board->Add( shared );
1511
1512 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos, const KIID& aSymbolUuid,
1513 NETINFO_ITEM* aNet ) -> FOOTPRINT*
1514 {
1515 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1516 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "Receptacle" ) ) );
1517 fp->SetReference( aRef );
1518 fp->SetPosition( aPos );
1519
1520 // Symbol instance UUID, the link between a block footprint and its placed instance
1522 path.push_back( aSymbolUuid );
1523 fp->SetPath( path );
1524
1525 PAD* pad = new PAD( fp );
1526 pad->SetNumber( wxT( "1" ) );
1528 pad->SetPosition( aPos );
1529 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1530 pad->SetLayerSet( LSET( { F_Cu } ) );
1531
1532 if( aNet )
1533 pad->SetNet( aNet );
1534
1535 fp->Add( pad );
1536 m_board->Add( fp );
1537 return fp;
1538 };
1539
1540 KIID symA, symB;
1541
1542 // Block source: both receptacles unconnected, so each pad has zero connections
1543 FOOTPRINT* refFpA = makeFootprint( wxT( "J5" ), VECTOR2I( 0, 0 ), symA, nullptr );
1544 FOOTPRINT* refFpB = makeFootprint( wxT( "J6" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), symB, nullptr );
1545
1546 // Board instance: same symbol instances, but a board wire ties both pads onto one net
1547 FOOTPRINT* destFpA =
1548 makeFootprint( wxT( "J1" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ), symA, shared );
1549 FOOTPRINT* destFpB =
1550 makeFootprint( wxT( "J2" ), VECTOR2I( pcbIUScale.mmToIU( 80 ), pcbIUScale.mmToIU( 80 ) ), symB, shared );
1551
1552 // Precondition: topology matching fails, so the symbol path fallback is what rescues the apply
1553 {
1554 std::set<FOOTPRINT*> refSet{ refFpA, refFpB };
1555 std::set<FOOTPRINT*> destSet{ destFpA, destFpB };
1556 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refSet, destSet );
1557 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( destSet, refSet );
1559 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1560 BOOST_REQUIRE_MESSAGE( !cgRef->FindIsomorphism( cgTarget.get(), topoOnly, details ),
1561 "Test precondition broken: areas are net isomorphic" );
1562 }
1563
1564 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1565 destGroup->SetName( wxT( "design-block-dest" ) );
1566 destGroup->AddItem( destFpA );
1567 destGroup->AddItem( destFpB );
1568 m_board->Add( destGroup );
1569
1570 RULE_AREA dbRA;
1572 dbRA.m_components.insert( refFpA );
1573 dbRA.m_components.insert( refFpB );
1574 dbRA.m_designBlockItems.insert( refFpA );
1575 dbRA.m_designBlockItems.insert( refFpB );
1576 dbRA.m_zone = new ZONE( m_board.get() );
1577 dbRA.m_zone->SetIsRuleArea( true );
1579 dbRA.m_zone->AddPolygon(
1581 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1582
1583 RULE_AREA destRA;
1585 destRA.m_components.insert( destFpA );
1586 destRA.m_components.insert( destFpB );
1587 destRA.m_group = destGroup;
1588 destRA.m_zone = new ZONE( m_board.get() );
1589 destRA.m_zone->SetIsRuleArea( true );
1590 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1592 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 40 ), pcbIUScale.mmToIU( 40 ) ),
1593 VECTOR2I( pcbIUScale.mmToIU( 90 ), pcbIUScale.mmToIU( 90 ) ) ) ) );
1594
1595 TOOL_MANAGER toolMgr;
1596 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1597 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1599 toolMgr.RegisterTool( mtTool );
1600
1601 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1602 .m_connectedRoutingOnly = false,
1603 .m_copyPlacement = true,
1604 .m_copyOtherItems = true,
1605 .m_groupItems = false,
1606 .m_includeLockedItems = true,
1607 .m_anchorFp = nullptr };
1608
1609 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1610
1611 delete dbRA.m_zone;
1612 delete destRA.m_zone;
1613
1614 BOOST_CHECK_MESSAGE( result >= 0, "Apply Design Block Layout aborted even though the symbol instance paths "
1615 "give an unambiguous mapping (No compatible component regression)" );
1616
1617 // Correct pairing (J5->J1, J6->J2) reproduces the source 10mm spacing, a swap would give -10mm
1618 VECTOR2I delta = destFpB->GetPosition() - destFpA->GetPosition();
1619 BOOST_CHECK_MESSAGE( delta == VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ),
1620 wxString::Format( "Destination relative placement wrong, expected "
1621 "(10mm,0) got (%d,%d) IU",
1622 delta.x, delta.y ) );
1623}
1624
1625
1634BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsDuplicateSilkText, MULTICHANNEL_TEST_FIXTURE )
1635{
1636 m_board = std::make_unique<BOARD>();
1637 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1638
1639 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1640 m_board->Add( net );
1641
1642 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1643 {
1644 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1645 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "SW" ) ) );
1646 fp->SetReference( aRef );
1647 fp->SetPosition( aPos );
1648
1649 // Two silkscreen texts with the same content, one on each board side.
1650 for( PCB_LAYER_ID layer : { F_SilkS, B_SilkS } )
1651 {
1652 PCB_TEXT* txt = new PCB_TEXT( fp );
1653 txt->SetText( wxT( "${REFERENCE}" ) );
1654 txt->SetLayer( layer );
1655 txt->SetPosition( aPos + VECTOR2I( 0, pcbIUScale.mmToIU( -8 ) ) );
1656 fp->Add( txt );
1657 }
1658
1659 PAD* pad = new PAD( fp );
1660 pad->SetNumber( wxT( "1" ) );
1661 pad->SetNet( net );
1662 pad->SetPosition( aPos );
1663 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1664 pad->SetLayerSet( LSET( { F_Cu } ) );
1665 fp->Add( pad );
1666
1667 m_board->Add( fp );
1668 return fp;
1669 };
1670
1671 auto countSilkText = [&]( FOOTPRINT* fp, PCB_LAYER_ID layer ) -> int
1672 {
1673 int count = 0;
1674
1675 for( BOARD_ITEM* item : fp->GraphicalItems() )
1676 {
1677 if( item->Type() == PCB_TEXT_T && item->GetLayer() == layer )
1678 count++;
1679 }
1680
1681 return count;
1682 };
1683
1684 FOOTPRINT* refFp1 = makeFootprint( wxT( "SW1" ), VECTOR2I( 0, 0 ) );
1685 FOOTPRINT* refFp2 = makeFootprint( wxT( "SW2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1686
1687 FOOTPRINT* destFp1 = makeFootprint( wxT( "SW3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1688 FOOTPRINT* destFp2 = makeFootprint( wxT( "SW4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1689
1690 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1691 destGroup->SetName( wxT( "design-block-dest" ) );
1692 destGroup->AddItem( destFp1 );
1693 destGroup->AddItem( destFp2 );
1694 m_board->Add( destGroup );
1695
1696 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, F_SilkS ), 1 );
1697 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, B_SilkS ), 1 );
1698
1699 RULE_AREA dbRA;
1701 dbRA.m_components.insert( refFp1 );
1702 dbRA.m_components.insert( refFp2 );
1703 dbRA.m_designBlockItems.insert( refFp1 );
1704 dbRA.m_designBlockItems.insert( refFp2 );
1705
1706 dbRA.m_zone = new ZONE( m_board.get() );
1707 dbRA.m_zone->SetIsRuleArea( true );
1709 dbRA.m_zone->AddPolygon(
1711 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1712
1713 RULE_AREA destRA;
1715 destRA.m_components.insert( destFp1 );
1716 destRA.m_components.insert( destFp2 );
1717
1718 destRA.m_zone = new ZONE( m_board.get() );
1719 destRA.m_zone->SetIsRuleArea( true );
1720 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1722 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1723 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1724
1725 TOOL_MANAGER toolMgr;
1726 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1727 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1728
1730 toolMgr.RegisterTool( mtTool );
1731
1732 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1733 .m_connectedRoutingOnly = false,
1734 .m_copyPlacement = true,
1735 .m_copyOtherItems = true,
1736 .m_groupItems = false,
1737 .m_includeLockedItems = true,
1738 .m_anchorFp = nullptr };
1739
1740 wxString err;
1741 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1742 BOOST_CHECK_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1743
1744 delete dbRA.m_zone;
1745 delete destRA.m_zone;
1746
1747 for( FOOTPRINT* destFp : { destFp1, destFp2 } )
1748 {
1749 BOOST_CHECK_MESSAGE( countSilkText( destFp, F_SilkS ) == 1,
1750 wxString::Format( "%s lost its F.Silkscreen reference text "
1751 "(issue 24583): F=%d B=%d",
1752 destFp->GetReference(), countSilkText( destFp, F_SilkS ),
1753 countSilkText( destFp, B_SilkS ) ) );
1754 BOOST_CHECK_EQUAL( countSilkText( destFp, B_SilkS ), 1 );
1755 }
1756}
1757
1758
1763BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutGraphicsOnlyBlock, MULTICHANNEL_TEST_FIXTURE )
1764{
1765 m_board = std::make_unique<BOARD>();
1766 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1767
1768 auto makeSilkRect = [&]( const VECTOR2I& aStart, const VECTOR2I& aEnd ) -> PCB_SHAPE*
1769 {
1770 PCB_SHAPE* s = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1771 s->SetStart( aStart );
1772 s->SetEnd( aEnd );
1773 s->SetLayer( F_SilkS );
1774 s->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1775 m_board->Add( s );
1776 return s;
1777 };
1778
1779 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1780 PCB_SHAPE* srcRect = makeSilkRect( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ),
1781 VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1782
1783 // Destination: footprint-free group with one silkscreen item so the group exists.
1784 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1785 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1786 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1787 destPlaceholder->SetLayer( F_SilkS );
1788 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1789 m_board->Add( destPlaceholder );
1790
1791 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1792 destGroup->SetName( wxT( "design-block-dest" ) );
1793 destGroup->AddItem( destPlaceholder );
1794 m_board->Add( destGroup );
1795
1796 int silkBefore = 0;
1797
1798 for( BOARD_ITEM* item : m_board->Drawings() )
1799 {
1800 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1801 silkBefore++;
1802 }
1803
1804 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1805
1806 RULE_AREA dbRA;
1808 dbRA.m_designBlockItems.insert( srcRect ); // no footprints -> m_components stays empty
1809
1810 dbRA.m_zone = new ZONE( m_board.get() );
1811 dbRA.m_zone->SetIsRuleArea( true );
1813 dbRA.m_zone->AddPolygon(
1815 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1816 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1817
1818 RULE_AREA destRA;
1820 destRA.m_group = destGroup; // set explicitly: no footprint to recover it from
1821
1822 destRA.m_zone = new ZONE( m_board.get() );
1823 destRA.m_zone->SetIsRuleArea( true );
1824 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1826 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1827 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1828 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1829
1830 TOOL_MANAGER toolMgr;
1831 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1832 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1833
1835 toolMgr.RegisterTool( mtTool );
1836
1837 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1838 .m_connectedRoutingOnly = false,
1839 .m_copyPlacement = true,
1840 .m_copyOtherItems = true,
1841 .m_groupItems = false,
1842 .m_includeLockedItems = true,
1843 .m_anchorFp = nullptr };
1844
1845 wxString err;
1846 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1847
1848 delete dbRA.m_zone;
1849 delete destRA.m_zone;
1850
1851 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free "
1852 "(graphics-only) design block (issue 24592): %s",
1853 err ) );
1854
1855 // The block's rectangle is copied into the target region and grouped, replacing the group's
1856 // prior placeholder graphic (source + 1 copy = 2 silkscreen shapes).
1857 int silkAfter = 0;
1858 PCB_SHAPE* copiedRect = nullptr;
1859
1860 for( BOARD_ITEM* item : m_board->Drawings() )
1861 {
1862 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1863 continue;
1864
1865 silkAfter++;
1866
1867 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1868
1869 if( shape != srcRect && shape->GetShape() == SHAPE_T::RECTANGLE )
1870 copiedRect = shape;
1871 }
1872
1873 BOOST_CHECK_MESSAGE( silkAfter == 2, wxString::Format( "Expected 2 silkscreen shapes after Apply Design Block "
1874 "Layout (source + 1 copy, placeholder replaced), found "
1875 "%d (issue 24592)",
1876 silkAfter ) );
1877 BOOST_REQUIRE_MESSAGE( copiedRect != nullptr, "Footprint-free block graphic was not copied (issue 24592)" );
1878 BOOST_CHECK_MESSAGE( copiedRect->GetParentGroup() == destGroup,
1879 "Copied block graphic was not added to the destination group (issue 24592)" );
1880 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1881}
1882
1883
1888BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeCopperIsNoNet, MULTICHANNEL_TEST_FIXTURE )
1889{
1890 m_board = std::make_unique<BOARD>();
1891 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1892
1893 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1894 m_board->Add( net );
1895
1896 // Source: footprint-free block with one track on a real net.
1897 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
1898 srcTrack->SetLayer( F_Cu );
1899 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1900 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( 0 ) ) );
1901 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 0 ) ) );
1902 srcTrack->SetNet( net );
1903 m_board->Add( srcTrack );
1904
1905 BOOST_REQUIRE_EQUAL( srcTrack->GetNetCode(), 1 );
1906
1907 // Destination: footprint-free group with a placeholder so the group exists.
1908 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1909 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1910 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1911 destPlaceholder->SetLayer( F_SilkS );
1912 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1913 m_board->Add( destPlaceholder );
1914
1915 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1916 destGroup->SetName( wxT( "design-block-dest" ) );
1917 destGroup->AddItem( destPlaceholder );
1918 m_board->Add( destGroup );
1919
1920 RULE_AREA dbRA;
1922 dbRA.m_designBlockItems.insert( srcTrack ); // no footprints -> m_components stays empty
1923
1924 dbRA.m_zone = new ZONE( m_board.get() );
1925 dbRA.m_zone->SetIsRuleArea( true );
1927 dbRA.m_zone->AddPolygon(
1929 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1930 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1931
1932 RULE_AREA destRA;
1934 destRA.m_group = destGroup;
1935
1936 destRA.m_zone = new ZONE( m_board.get() );
1937 destRA.m_zone->SetIsRuleArea( true );
1938 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1940 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1941 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1942 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1943
1944 TOOL_MANAGER toolMgr;
1945 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1946 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1947
1949 toolMgr.RegisterTool( mtTool );
1950
1951 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1952 .m_connectedRoutingOnly = false,
1953 .m_copyPlacement = true,
1954 .m_copyOtherItems = true,
1955 .m_groupItems = false,
1956 .m_includeLockedItems = true,
1957 .m_anchorFp = nullptr };
1958
1959 wxString err;
1960 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1961
1962 delete dbRA.m_zone;
1963 delete destRA.m_zone;
1964
1965 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free copper "
1966 "block (issue 24592): %s",
1967 err ) );
1968
1969 // Copied track should be in the target region, no-net, and grouped.
1970 PCB_TRACK* copiedTrack = nullptr;
1971
1972 for( PCB_TRACK* track : m_board->Tracks() )
1973 {
1974 if( track != srcTrack )
1975 copiedTrack = track;
1976 }
1977
1978 BOOST_REQUIRE_MESSAGE( copiedTrack != nullptr, "Footprint-free block track was not copied (issue 24592)" );
1979 BOOST_CHECK_MESSAGE( copiedTrack->GetNetCode() == 0,
1980 wxString::Format( "Copied copper should be no-net, got net code %d "
1981 "(issue 24592)",
1982 copiedTrack->GetNetCode() ) );
1983 BOOST_CHECK_MESSAGE( copiedTrack->GetParentGroup() == destGroup,
1984 "Copied copper was not added to the destination group (issue 24592)" );
1985 BOOST_CHECK_GT( copiedTrack->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1986
1987 // The original source track must keep its net.
1988 BOOST_CHECK_EQUAL( srcTrack->GetNetCode(), 1 );
1989}
1990
1991
1997BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeReapplyReplaces, MULTICHANNEL_TEST_FIXTURE )
1998{
1999 m_board = std::make_unique<BOARD>();
2000 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2001
2002 // Source: footprint-free block with one silkscreen rectangle centered on origin.
2003 PCB_SHAPE* srcRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
2004 srcRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
2005 srcRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
2006 srcRect->SetLayer( F_SilkS );
2007 srcRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2008 m_board->Add( srcRect );
2009
2010 // Destination: footprint-free group with a placeholder so the group exists.
2011 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2012 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2013 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2014 destPlaceholder->SetLayer( F_SilkS );
2015 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2016 m_board->Add( destPlaceholder );
2017
2018 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2019 destGroup->SetName( wxT( "design-block-dest" ) );
2020 destGroup->AddItem( destPlaceholder );
2021 m_board->Add( destGroup );
2022
2023 TOOL_MANAGER toolMgr;
2024 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2025 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2026
2028 toolMgr.RegisterTool( mtTool );
2029
2030 auto applyOnce = [&]() -> int
2031 {
2032 RULE_AREA dbRA;
2034 dbRA.m_designBlockItems.insert( srcRect );
2035
2036 dbRA.m_zone = new ZONE( m_board.get() );
2037 dbRA.m_zone->SetIsRuleArea( true );
2040 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
2041 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2042 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2043
2044 RULE_AREA destRA;
2046 destRA.m_group = destGroup;
2047
2048 destRA.m_zone = new ZONE( m_board.get() );
2049 destRA.m_zone->SetIsRuleArea( true );
2050 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2052 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2053 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2054 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2055
2056 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2057 .m_connectedRoutingOnly = false,
2058 .m_copyPlacement = true,
2059 .m_copyOtherItems = true,
2060 .m_groupItems = false,
2061 .m_includeLockedItems = true,
2062 .m_anchorFp = nullptr };
2063
2064 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
2065
2066 delete dbRA.m_zone;
2067 delete destRA.m_zone;
2068 return result;
2069 };
2070
2071 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "First apply failed (issue 24592)" );
2072 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "Second apply failed (issue 24592)" );
2073
2074 // After two applies there must be exactly the source rectangle plus one copy. A third
2075 // rectangle would mean the second apply stacked instead of replacing.
2076 int rects = 0;
2077
2078 for( BOARD_ITEM* item : m_board->Drawings() )
2079 {
2080 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS
2081 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::RECTANGLE )
2082 {
2083 rects++;
2084 }
2085 }
2086
2087 BOOST_CHECK_MESSAGE( rects == 2, wxString::Format( "Re-apply stacked copies: expected 2 rectangles "
2088 "(source + 1 copy), found %d (issue 24592)",
2089 rects ) );
2090}
2091
2092
2100BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsOtherGroupRouting, MULTICHANNEL_TEST_FIXTURE )
2101{
2102 m_board = std::make_unique<BOARD>();
2103 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2104
2105 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
2106 m_board->Add( net );
2107
2108 // Source block: one track centered on origin.
2109 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
2110 srcTrack->SetLayer( F_Cu );
2111 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2112 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), 0 ) );
2113 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), 0 ) );
2114 srcTrack->SetNet( net );
2115 m_board->Add( srcTrack );
2116
2117 // Destination group with a placeholder so the group exists.
2118 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2119 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2120 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2121 destPlaceholder->SetLayer( F_SilkS );
2122 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2123 m_board->Add( destPlaceholder );
2124
2125 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2126 destGroup->SetName( wxT( "design-block-dest" ) );
2127 destGroup->AddItem( destPlaceholder );
2128 m_board->Add( destGroup );
2129
2130 // A sibling instance's track, inside the destination area but owned by another group.
2131 PCB_TRACK* siblingTrack = new PCB_TRACK( m_board.get() );
2132 siblingTrack->SetLayer( F_Cu );
2133 siblingTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2134 siblingTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 47 ) ) );
2135 siblingTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 47 ) ) );
2136 m_board->Add( siblingTrack );
2137
2138 PCB_GROUP* siblingGroup = new PCB_GROUP( m_board.get() );
2139 siblingGroup->SetName( wxT( "design-block-sibling" ) );
2140 siblingGroup->AddItem( siblingTrack );
2141 m_board->Add( siblingGroup );
2142
2143 // Loose, ungrouped routing without a net inside the destination area, unrelated to the block.
2144 PCB_TRACK* looseTrack = new PCB_TRACK( m_board.get() );
2145 looseTrack->SetLayer( F_Cu );
2146 looseTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2147 looseTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 53 ) ) );
2148 looseTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 53 ) ) );
2149 m_board->Add( looseTrack );
2150
2151 RULE_AREA dbRA;
2153 dbRA.m_designBlockItems.insert( srcTrack );
2154
2155 dbRA.m_zone = new ZONE( m_board.get() );
2156 dbRA.m_zone->SetIsRuleArea( true );
2158 dbRA.m_zone->AddPolygon(
2160 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2161 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2162
2163 RULE_AREA destRA;
2165 destRA.m_group = destGroup;
2166
2167 destRA.m_zone = new ZONE( m_board.get() );
2168 destRA.m_zone->SetIsRuleArea( true );
2169 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2171 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2172 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2173 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2174
2175 TOOL_MANAGER toolMgr;
2176 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2177 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2178
2180 toolMgr.RegisterTool( mtTool );
2181
2182 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2183 .m_connectedRoutingOnly = false,
2184 .m_copyPlacement = true,
2185 .m_copyOtherItems = true,
2186 .m_groupItems = false,
2187 .m_includeLockedItems = true,
2188 .m_anchorFp = nullptr };
2189
2190 wxString err;
2191 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
2192
2193 delete dbRA.m_zone;
2194 delete destRA.m_zone;
2195
2196 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
2197
2198 // Pointers to removed tracks are deleted, so count survivors by location instead.
2199 int siblingTracks = 0;
2200 int looseTracks = 0;
2201
2202 for( PCB_TRACK* track : m_board->Tracks() )
2203 {
2204 int y = track->GetStart().y;
2205
2206 if( y > pcbIUScale.mmToIU( 46 ) && y < pcbIUScale.mmToIU( 48 ) )
2207 siblingTracks++;
2208 else if( y > pcbIUScale.mmToIU( 52 ) && y < pcbIUScale.mmToIU( 54 ) )
2209 looseTracks++;
2210 }
2211
2212 BOOST_CHECK_MESSAGE( siblingTracks == 1,
2213 wxString::Format( "Sibling group routing was deleted (issue 24767): found %d, expected 1",
2214 siblingTracks ) );
2215 BOOST_CHECK_MESSAGE( looseTracks == 1,
2216 wxString::Format( "Loose no-net routing in the target area was deleted (issue 24944): "
2217 "found %d, expected 1",
2218 looseTracks ) );
2219}
2220
2221
2227BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsUnrelatedRouting, MULTICHANNEL_TEST_FIXTURE )
2228{
2229 m_board = std::make_unique<BOARD>();
2230 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2231
2232 NETINFO_ITEM* net1 = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
2233 NETINFO_ITEM* net2 = new NETINFO_ITEM( m_board.get(), wxT( "NET2" ), 2 );
2234 m_board->Add( net1 );
2235 m_board->Add( net2 );
2236
2237 auto addTrack = [&]( double aStartX, double aStartY, double aEndX, double aEndY, NETINFO_ITEM* aNet )
2238 {
2239 PCB_TRACK* track = new PCB_TRACK( m_board.get() );
2240 track->SetLayer( F_Cu );
2241 track->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2242 track->SetStart( VECTOR2I( pcbIUScale.mmToIU( aStartX ), pcbIUScale.mmToIU( aStartY ) ) );
2243 track->SetEnd( VECTOR2I( pcbIUScale.mmToIU( aEndX ), pcbIUScale.mmToIU( aEndY ) ) );
2244 track->SetNet( aNet );
2245 m_board->Add( track );
2246 return track;
2247 };
2248
2249 // Source block: one track centered on origin.
2250 PCB_TRACK* srcTrack = addTrack( -2, 0, 2, 0, net1 );
2251
2252 // Destination group with a placeholder and its own routing from a previous apply.
2253 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2254 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2255 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2256 destPlaceholder->SetLayer( F_SilkS );
2257 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2258 m_board->Add( destPlaceholder );
2259
2260 PCB_TRACK* prevTrack = addTrack( 46, 49, 48, 49, net1 );
2261
2262 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2263 destGroup->SetName( wxT( "design-block-dest" ) );
2264 destGroup->AddItem( destPlaceholder );
2265 destGroup->AddItem( prevTrack );
2266 m_board->Add( destGroup );
2267
2268 // Bystanders inside the destination area: a track on an unrelated net and a locked track.
2269 // Both must survive the apply (issue 24944).
2270 addTrack( 46, 47, 48, 47, net2 );
2271
2272 PCB_TRACK* lockedTrack = addTrack( 46, 51.5, 48, 51.5, net1 );
2273 lockedTrack->SetLocked( true );
2274
2275 // Loose routing on the group's own net, a stale user tweak, which still gets replaced.
2276 addTrack( 46, 53, 48, 53, net1 );
2277
2278 RULE_AREA dbRA;
2280 dbRA.m_designBlockItems.insert( srcTrack );
2281
2282 dbRA.m_zone = new ZONE( m_board.get() );
2283 dbRA.m_zone->SetIsRuleArea( true );
2285 dbRA.m_zone->AddPolygon(
2287 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2288 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2289
2290 RULE_AREA destRA;
2292 destRA.m_group = destGroup;
2293
2294 destRA.m_zone = new ZONE( m_board.get() );
2295 destRA.m_zone->SetIsRuleArea( true );
2296 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2298 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2299 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2300 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2301
2302 TOOL_MANAGER toolMgr;
2303 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2304 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2305
2307 toolMgr.RegisterTool( mtTool );
2308
2309 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2310 .m_connectedRoutingOnly = false,
2311 .m_copyPlacement = true,
2312 .m_copyOtherItems = true,
2313 .m_groupItems = false,
2314 .m_includeLockedItems = true,
2315 .m_anchorFp = nullptr };
2316
2317 wxString err;
2318 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
2319
2320 delete dbRA.m_zone;
2321 delete destRA.m_zone;
2322
2323 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
2324
2325 // Pointers to removed tracks are deleted, so count survivors by location instead.
2326 int unrelatedTracks = 0;
2327 int prevTracks = 0;
2328 int copiedTracks = 0;
2329 int lockedTracks = 0;
2330 int staleTweakTracks = 0;
2331
2332 for( PCB_TRACK* track : m_board->Tracks() )
2333 {
2334 int y = track->GetStart().y;
2335
2336 if( y > pcbIUScale.mmToIU( 46.5 ) && y < pcbIUScale.mmToIU( 47.5 ) )
2337 unrelatedTracks++;
2338 else if( y > pcbIUScale.mmToIU( 48.5 ) && y < pcbIUScale.mmToIU( 49.5 ) )
2339 prevTracks++;
2340 else if( y > pcbIUScale.mmToIU( 49.5 ) && y < pcbIUScale.mmToIU( 50.5 ) )
2341 copiedTracks++;
2342 else if( y > pcbIUScale.mmToIU( 51 ) && y < pcbIUScale.mmToIU( 52 ) )
2343 lockedTracks++;
2344 else if( y > pcbIUScale.mmToIU( 52.5 ) && y < pcbIUScale.mmToIU( 53.5 ) )
2345 staleTweakTracks++;
2346 }
2347
2348 BOOST_CHECK_MESSAGE( unrelatedTracks == 1,
2349 wxString::Format( "Unrelated net routing in the target area was deleted (issue 24944): "
2350 "found %d, expected 1",
2351 unrelatedTracks ) );
2352 BOOST_CHECK_MESSAGE( lockedTracks == 1,
2353 wxString::Format( "Locked bystander routing in the target area was deleted (issue 24944): "
2354 "found %d, expected 1",
2355 lockedTracks ) );
2356 BOOST_CHECK_MESSAGE(
2357 prevTracks == 0,
2358 wxString::Format( "The group's own routing should be replaced: found %d, expected 0", prevTracks ) );
2359 BOOST_CHECK_MESSAGE( staleTweakTracks == 0,
2360 wxString::Format( "Loose routing on the group's nets should be replaced: found %d, "
2361 "expected 0",
2362 staleTweakTracks ) );
2363 BOOST_CHECK_MESSAGE( copiedTracks == 1,
2364 wxString::Format( "Block routing was not copied into the target area: found %d, expected 1",
2365 copiedTracks ) );
2366}
2367
2368
2375BOOST_FIXTURE_TEST_CASE( RepeatLayoutRefusesDuplicatePlacementAreas, MULTICHANNEL_TEST_FIXTURE )
2376{
2377 KI_TEST::LoadBoard( m_settingsManager, "issue22318/issue22318", m_board );
2378
2379 TOOL_MANAGER toolMgr;
2380 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2381
2382 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2383
2385 toolMgr.RegisterTool( mtTool );
2386
2387 mtTool->FindExistingRuleAreas();
2388
2389 auto ruleData = mtTool->GetData();
2390
2391 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
2392 static_cast<int>( ruleData->m_areas.size() ) ) );
2393
2394 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
2395
2396 // The shared membership, not the exact count, is what makes the areas invalid targets.
2397 const size_t sharedCount = ruleData->m_areas.front().m_components.size();
2398
2399 BOOST_REQUIRE( sharedCount > 0 );
2400
2401 for( const RULE_AREA& ra : ruleData->m_areas )
2402 BOOST_CHECK_EQUAL( ra.m_components.size(), sharedCount );
2403
2404 RULE_AREA* refArea = &ruleData->m_areas.front();
2405
2406 BOOST_REQUIRE( mtTool->CheckRACompatibility( refArea->m_zone ) >= 0 );
2407
2408 BOOST_REQUIRE_EQUAL( ruleData->m_compatMap.size(), ruleData->m_areas.size() - 1 );
2409
2410 for( const auto& [targetArea, compatData] : ruleData->m_compatMap )
2411 {
2412 BOOST_CHECK_MESSAGE( !compatData.m_isOk,
2413 "Duplicate placement area was wrongly reported as a valid copy target "
2414 "(issue 22318)" );
2415 BOOST_CHECK( !compatData.m_mismatchReasons.empty() );
2416 }
2417
2418 // The single-target overload must also refuse rather than corrupt the board.
2419 RULE_AREA* targetArea = &ruleData->m_areas[1];
2420
2422 wxString err;
2423
2424 int result = mtTool->RepeatLayout( TOOL_EVENT(), *refArea, *targetArea, opts, nullptr, &err );
2425
2426 BOOST_CHECK_MESSAGE( result < 0,
2427 "RepeatLayout copied a Rule Area onto an identical-component area "
2428 "(issue 22318)" );
2429 BOOST_CHECK( !err.IsEmpty() );
2430}
2431
2432
2444BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotDuplicateUnrelatedGroups, MULTICHANNEL_TEST_FIXTURE )
2445{
2446 KI_TEST::LoadBoard( m_settingsManager, "issue22316/issue22316", m_board );
2447
2448 TOOL_MANAGER toolMgr;
2449 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2450
2451 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2452
2454 toolMgr.RegisterTool( mtTool );
2455
2456 mtTool->FindExistingRuleAreas();
2457
2458 auto ruleData = mtTool->GetData();
2459
2460 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
2461 static_cast<int>( ruleData->m_areas.size() ) ) );
2462
2463 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
2464
2465 RULE_AREA* refArea = nullptr;
2466
2467 for( RULE_AREA& ra : ruleData->m_areas )
2468 {
2469 if( ra.m_ruleName == wxT( "test 1" ) )
2470 refArea = &ra;
2471 }
2472
2473 BOOST_REQUIRE( refArea != nullptr );
2474
2475 std::set<KIID> groupUuidsBefore;
2476 std::set<wxString> userGroupNamesBefore;
2477
2478 for( PCB_GROUP* group : m_board->Groups() )
2479 {
2480 groupUuidsBefore.insert( group->m_Uuid );
2481
2482 if( !group->GetName().IsEmpty() )
2483 userGroupNamesBefore.insert( group->GetName() );
2484 }
2485
2486 mtTool->CheckRACompatibility( refArea->m_zone );
2487
2488 for( auto& [targetArea, compatData] : ruleData->m_compatMap )
2489 compatData.m_doCopy = true;
2490
2491 ruleData->m_options.m_copyPlacement = true;
2492 ruleData->m_options.m_copyRouting = true;
2493 ruleData->m_options.m_copyOtherItems = true;
2494 ruleData->m_options.m_groupItems = true;
2495 ruleData->m_options.m_includeLockedItems = true;
2496
2497 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
2498
2499 BOOST_REQUIRE( result >= 0 );
2500
2501 int clonedUserGroups = 0;
2502
2503 for( PCB_GROUP* group : m_board->Groups() )
2504 {
2505 bool isNew = !groupUuidsBefore.contains( group->m_Uuid );
2506
2507 if( isNew && userGroupNamesBefore.contains( group->GetName() ) )
2508 clonedUserGroups++;
2509 }
2510
2511 BOOST_CHECK_MESSAGE( clonedUserGroups == 0,
2512 wxString::Format( "Repeat layout cloned %d unrelated user groups "
2513 "(issue 22316)",
2514 clonedUserGroups ) );
2515}
2516
2517
2525BOOST_FIXTURE_TEST_CASE( TopoMatchExternalLoopReportsConnectivity, MULTICHANNEL_TEST_FIXTURE )
2526{
2528
2529 KI_TEST::LoadBoard( m_settingsManager, "issue24192/issue24192", m_board );
2530
2531 TOOL_MANAGER toolMgr;
2532 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2533 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2534
2536 toolMgr.RegisterTool( mtTool );
2537
2538 mtTool->FindExistingRuleAreas();
2539
2540 auto ruleData = mtTool->GetData();
2541
2542 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 2 );
2543
2544 RULE_AREA* refArea = nullptr;
2545 RULE_AREA* targetArea = nullptr;
2546
2547 for( RULE_AREA& ra : ruleData->m_areas )
2548 {
2549 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
2550 refArea = &ra;
2551 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
2552 targetArea = &ra;
2553 }
2554
2555 BOOST_REQUIRE( refArea != nullptr );
2556 BOOST_REQUIRE( targetArea != nullptr );
2557
2558 // The component counts are identical; only the internal connectivity differs.
2559 BOOST_CHECK_EQUAL( refArea->m_components.size(), targetArea->m_components.size() );
2560
2561 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
2562 targetArea->m_components );
2563 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
2564 refArea->m_components );
2565
2567 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2568 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2569
2570 // The topology genuinely differs, so matching must fail and produce a reason.
2571 BOOST_CHECK( !status );
2572 BOOST_REQUIRE( !details.empty() );
2573
2574 // Collect the reference designators that belong to each channel so we can verify the
2575 // reason's reference/candidate orientation matches the ref/target areas.
2576 std::set<wxString> refRefDes;
2577 std::set<wxString> targetRefDes;
2578
2579 for( FOOTPRINT* fp : refArea->m_components )
2580 refRefDes.insert( fp->GetReference() );
2581
2582 for( FOOTPRINT* fp : targetArea->m_components )
2583 targetRefDes.insert( fp->GetReference() );
2584
2585 bool sawConnectivityReason = false;
2586
2587 for( const auto& reason : details )
2588 {
2589 BOOST_TEST_MESSAGE( wxString::Format( "reason: %s <-> %s: %s", reason.m_reference,
2590 reason.m_candidate, reason.m_reason ) );
2591
2592 // The generic fallback message must no longer be the only thing reported; instead the
2593 // connectivity difference detected by the per-pad isomorphism check must surface.
2594 BOOST_CHECK( !reason.m_reason.Contains( wxT( "No compatible component found" ) ) );
2595
2596 if( reason.m_reason.Contains( wxT( "connects to" ) )
2597 || reason.m_reason.Contains( wxT( "connectivity" ) ) )
2598 {
2599 sawConnectivityReason = true;
2600
2601 // The reference designator in the reason must come from the reference channel and
2602 // the candidate from the target channel, not the other way around (codex-review).
2603 BOOST_CHECK_MESSAGE( refRefDes.count( reason.m_reference ) > 0,
2604 wxString::Format( "Reason reference '%s' should be a reference-area "
2605 "component", reason.m_reference ) );
2606 BOOST_CHECK_MESSAGE( targetRefDes.count( reason.m_candidate ) > 0,
2607 wxString::Format( "Reason candidate '%s' should be a target-area "
2608 "component", reason.m_candidate ) );
2609 }
2610 }
2611
2612 BOOST_CHECK_MESSAGE( sawConnectivityReason,
2613 "Topology mismatch message should explain the connectivity difference "
2614 "rather than report a generic missing-component error (issue 24192)" );
2615}
2616
2617
2625{
2627
2628 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2629
2630 auto addNet = [&]( const wxString& aName ) -> int
2631 {
2632 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
2633 board->Add( net );
2634 return net->GetNetCode();
2635 };
2636
2637 const int netGnd = addNet( wxT( "GND" ) );
2638 const int netP3V3 = addNet( wxT( "+3V3" ) );
2639 const int netCollide = addNet( wxT( "Net-(D3-A)" ) ); // shared name, lands on a different part
2640 const int netRefY = addNet( wxT( "Net-(D4-A)" ) ); // block other LED anode
2641 const int netTgtX = addNet( wxT( "Net-(D2-A)" ) ); // copy other LED anode
2642
2643 LIB_ID ledAId( wxT( "TestLib" ), wxT( "LED_A" ) );
2644 LIB_ID ledBId( wxT( "TestLib" ), wxT( "LED_B" ) );
2645 LIB_ID resId( wxT( "TestLib" ), wxT( "R" ) );
2646
2647 auto addPad = [&]( FOOTPRINT* fp, const wxString& aNum, int aNet )
2648 {
2649 PAD* pad = new PAD( fp );
2650 pad->SetNumber( aNum );
2652 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2653 pad->SetLayerSet( LSET( { F_Cu } ) );
2654 pad->SetNetCode( aNet );
2655 fp->Add( pad );
2656 };
2657
2658 auto addFp = [&]( const LIB_ID& aId, const wxString& aRef ) -> FOOTPRINT*
2659 {
2660 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2661 fp->SetFPID( aId );
2662 fp->SetReference( aRef );
2663 board->Add( fp );
2664 return fp;
2665 };
2666
2667 // One LED plus its series resistor. The LED anode net is the one that can clash.
2668 auto addLedAndRes = [&]( const LIB_ID& aLedId, const wxString& aLedRef, const wxString& aResRef, int aAnodeNet,
2669 std::set<FOOTPRINT*>& aSet )
2670 {
2671 FOOTPRINT* led = addFp( aLedId, aLedRef );
2672 addPad( led, wxT( "1" ), netGnd );
2673 addPad( led, wxT( "2" ), aAnodeNet );
2674 aSet.insert( led );
2675
2676 FOOTPRINT* res = addFp( resId, aResRef );
2677 addPad( res, wxT( "1" ), aAnodeNet );
2678 addPad( res, wxT( "2" ), netP3V3 );
2679 aSet.insert( res );
2680 };
2681
2682 // The block: LED_A is D3 on the clashing net, LED_B is D4 on its own net.
2683 std::set<FOOTPRINT*> refFps;
2684 addLedAndRes( ledAId, wxT( "D3" ), wxT( "R27" ), netCollide, refFps );
2685 addLedAndRes( ledBId, wxT( "D4" ), wxT( "R30" ), netRefY, refFps );
2686
2687 // The copy: same parts, but the clashing name now sits on LED_B (D3), a different part.
2688 std::set<FOOTPRINT*> tgtFps;
2689 addLedAndRes( ledAId, wxT( "D2" ), wxT( "R1" ), netTgtX, tgtFps );
2690 addLedAndRes( ledBId, wxT( "D3" ), wxT( "R2" ), netCollide, tgtFps );
2691
2692 // The fix gives the block's auto nets private names so they cannot clash with the copy.
2693 std::unordered_set<EDA_ITEM*> refItems( refFps.begin(), refFps.end() );
2694 MULTICHANNEL_TOOL::IsolateDesignBlockAutoNets( board.get(), refFps, refItems );
2695
2696 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
2697 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
2698
2700 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2701 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2702
2703 if( !status )
2704 {
2705 for( const auto& reason : details )
2706 {
2707 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s to %s: %s", reason.m_reference, reason.m_candidate,
2708 reason.m_reason ) );
2709 }
2710 }
2711
2712 BOOST_CHECK_MESSAGE( status, "Topology match failed even after isolating the block's auto nets "
2713 "(issue 24767)" );
2714 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
2715}
2716
2717
2723BOOST_FIXTURE_TEST_CASE( TopoMatchTieBreaksIdenticalPartsByValue, MULTICHANNEL_TEST_FIXTURE )
2724{
2726
2727 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2728
2729 const LIB_ID receptacleId( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) );
2730
2731 // Unique net per footprint, so the four are a topological tie and only the value differs.
2732 auto makeReceptacle = [&]( const wxString& aRef, const wxString& aValue, bool aWithSymbolPath ) -> FOOTPRINT*
2733 {
2734 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2735 fp->SetFPID( receptacleId );
2736 fp->SetReference( aRef );
2737 fp->SetValue( aValue );
2738
2739 if( aWithSymbolPath )
2740 {
2742 path.push_back( KIID() );
2743 fp->SetPath( path );
2744 }
2745
2746 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), wxString::Format( "net_%s", aRef ) );
2747 board->Add( net );
2748
2749 PAD* pad = new PAD( fp );
2750 pad->SetNumber( wxT( "1" ) );
2752 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2753 pad->SetLayerSet( LSET( { F_Cu } ) );
2754 pad->SetNet( net );
2755 fp->Add( pad );
2756
2757 board->Add( fp );
2758 return fp;
2759 };
2760
2761 // Block source: no symbol path (as after AppendBoard). Fixed order keeps the test deterministic.
2762 FOOTPRINT* refNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), false );
2763 FOOTPRINT* refNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), false );
2764 FOOTPRINT* refNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), false );
2765 FOOTPRINT* refNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), false );
2766
2767 // Target refdes order is the reverse of value order, so a refdes fallback would swap NC and NO.
2768 FOOTPRINT* tgtNO1 = makeReceptacle( wxT( "J20" ), wxT( "NO_1" ), true );
2769 FOOTPRINT* tgtNO0 = makeReceptacle( wxT( "J21" ), wxT( "NO_0" ), true );
2770 FOOTPRINT* tgtNC1 = makeReceptacle( wxT( "J22" ), wxT( "NC_1" ), true );
2771 FOOTPRINT* tgtNC0 = makeReceptacle( wxT( "J23" ), wxT( "NC_0" ), true );
2772
2773 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
2774 cgRef->AddFootprint( refNC0, VECTOR2I( 0, 0 ) );
2775 cgRef->AddFootprint( refNC1, VECTOR2I( 0, 0 ) );
2776 cgRef->AddFootprint( refNO0, VECTOR2I( 0, 0 ) );
2777 cgRef->AddFootprint( refNO1, VECTOR2I( 0, 0 ) );
2778 cgRef->BuildConnectivity();
2779
2780 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
2781 cgTarget->AddFootprint( tgtNO1, VECTOR2I( 0, 0 ) );
2782 cgTarget->AddFootprint( tgtNO0, VECTOR2I( 0, 0 ) );
2783 cgTarget->AddFootprint( tgtNC1, VECTOR2I( 0, 0 ) );
2784 cgTarget->AddFootprint( tgtNC0, VECTOR2I( 0, 0 ) );
2785 cgTarget->BuildConnectivity();
2786
2788 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2789 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2790
2791 BOOST_CHECK( status );
2792 BOOST_CHECK_EQUAL( result.size(), 4 );
2793
2794 // Each reference must match the same-value target.
2795 for( const auto& [refFp, targetFp] : result )
2796 {
2797 BOOST_TEST_MESSAGE( wxString::Format( "%s (%s) -> %s (%s)", refFp->GetReference(), refFp->GetValue(),
2798 targetFp->GetReference(), targetFp->GetValue() ) );
2799
2800 BOOST_CHECK_EQUAL( refFp->GetValue(), targetFp->GetValue() );
2801 }
2802}
2803
2804
2809BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutUnmirrorsIdenticalReceptacles, MULTICHANNEL_TEST_FIXTURE )
2810{
2811 m_board = std::make_unique<BOARD>();
2812 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2813
2814 auto makeReceptacle = [&]( const wxString& aRef, const wxString& aValue, const VECTOR2I& aPos,
2815 bool aWithSymbolPath ) -> FOOTPRINT*
2816 {
2817 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
2818 fp->SetFPID( LIB_ID( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) ) );
2819 fp->SetReference( aRef );
2820 fp->SetValue( aValue );
2821 fp->SetPosition( aPos );
2822
2823 if( aWithSymbolPath )
2824 {
2826 path.push_back( KIID() );
2827 fp->SetPath( path );
2828 }
2829
2830 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxString::Format( "net_%s", aRef ) );
2831 m_board->Add( net );
2832
2833 PAD* pad = new PAD( fp );
2834 pad->SetNumber( wxT( "1" ) );
2836 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2837 pad->SetLayerSet( LSET( { F_Cu } ) );
2838 pad->SetNet( net );
2839 pad->SetPosition( aPos );
2840 fp->Add( pad );
2841
2842 m_board->Add( fp );
2843 return fp;
2844 };
2845
2846 auto mm = []( double a, double b )
2847 {
2848 return VECTOR2I( pcbIUScale.mmToIU( a ), pcbIUScale.mmToIU( b ) );
2849 };
2850
2851 // Signed area of NC_0, NC_1, NO_0. Same sign as the block is correct, opposite sign is a mirror.
2852 auto chirality = []( const VECTOR2I& aNC0, const VECTOR2I& aNC1, const VECTOR2I& aNO0 ) -> double
2853 {
2854 return (double) ( aNC1.x - aNC0.x ) * ( aNO0.y - aNC0.y ) - (double) ( aNC1.y - aNC0.y ) * ( aNO0.x - aNC0.x );
2855 };
2856
2857 // Block source: the reference arrangement, no symbol path (as after AppendBoard).
2858 FOOTPRINT* srcNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), mm( 0, 0 ), false );
2859 FOOTPRINT* srcNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), mm( 0, 7.9 ), false );
2860 FOOTPRINT* srcNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), mm( -3.58, 2.28 ), false );
2861 FOOTPRINT* srcNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), mm( 3.58, 5.62 ), false );
2862
2863 const double srcChir = chirality( srcNC0->GetPosition(), srcNC1->GetPosition(), srcNO0->GetPosition() );
2864
2865 // Destination placed as a mirror (NO_0 / NO_1 reflected across the NC axis).
2866 FOOTPRINT* dstNC0 = makeReceptacle( wxT( "J8" ), wxT( "NC_0" ), mm( 40, 40 ), true );
2867 FOOTPRINT* dstNC1 = makeReceptacle( wxT( "J9" ), wxT( "NC_1" ), mm( 40, 47.9 ), true );
2868 FOOTPRINT* dstNO0 = makeReceptacle( wxT( "J10" ), wxT( "NO_0" ), mm( 43.58, 42.28 ), true );
2869 FOOTPRINT* dstNO1 = makeReceptacle( wxT( "J11" ), wxT( "NO_1" ), mm( 36.42, 45.62 ), true );
2870
2871 BOOST_REQUIRE_LT( srcChir * chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() ), 0.0 );
2872
2873 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2874 destGroup->SetName( wxT( "Pin receptacles for safety switch" ) );
2875
2876 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
2877 destGroup->AddItem( fp );
2878
2879 m_board->Add( destGroup );
2880
2881 RULE_AREA dbRA;
2883
2884 for( FOOTPRINT* fp : { srcNC0, srcNC1, srcNO0, srcNO1 } )
2885 {
2886 dbRA.m_components.insert( fp );
2887 dbRA.m_designBlockItems.insert( fp );
2888 }
2889
2890 dbRA.m_zone = new ZONE( m_board.get() );
2891 dbRA.m_zone->SetIsRuleArea( true );
2893 dbRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( -6, -3 ), mm( 6, 11 ) ) ) );
2894
2895 RULE_AREA destRA;
2897
2898 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
2899 destRA.m_components.insert( fp );
2900
2901 destRA.m_zone = new ZONE( m_board.get() );
2902 destRA.m_zone->SetIsRuleArea( true );
2903 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2904 destRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( 33, 37 ), mm( 47, 51 ) ) ) );
2905
2906 TOOL_MANAGER toolMgr;
2907 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2908 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2909
2911 toolMgr.RegisterTool( mtTool );
2912
2913 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = false,
2914 .m_connectedRoutingOnly = false,
2915 .m_copyPlacement = true,
2916 .m_copyOtherItems = false,
2917 .m_groupItems = false,
2918 .m_includeLockedItems = true,
2919 .m_anchorFp = nullptr };
2920
2921 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
2922 BOOST_REQUIRE_MESSAGE( result >= 0, "RepeatLayout failed" );
2923
2924 delete dbRA.m_zone;
2925 delete destRA.m_zone;
2926
2927 const double dstChir = chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() );
2928
2930 wxString::Format( "block chirality %.0f, destination chirality after apply %.0f", srcChir, dstChir ) );
2931
2932 BOOST_CHECK_MESSAGE( srcChir * dstChir > 0.0, "Applied layout left the receptacles mirrored (NC/NO swapped)" );
2933}
2934
2935
2942BOOST_FIXTURE_TEST_CASE( CheckRACompatGlobalRailAcrossChannels, MULTICHANNEL_TEST_FIXTURE )
2943{
2944 m_board = std::make_unique<BOARD>();
2945 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2946
2947 auto addNet = [&]( const wxString& aName ) -> int
2948 {
2949 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), aName );
2950 m_board->Add( net );
2951 return net->GetNetCode();
2952 };
2953
2954 const int netGnd = addNet( wxT( "GND" ) );
2955 const int netVcc = addNet( wxT( "+3V3" ) );
2956
2957 const LIB_ID icId( wxT( "TestLib" ), wxT( "SOT-23-6" ) );
2958 const LIB_ID rId( wxT( "TestLib" ), wxT( "R_0402" ) );
2959
2960 // Each channel is an IC (OUT + GND) and a config resistor strapped to a rail: GND in channels
2961 // 1 and 2, +3V3 in channel 3. That strap difference is intentional and must not defeat the match.
2962 auto makeChannel = [&]( int aIdx, int aStrapNet ) -> ZONE*
2963 {
2964 const int netOut = addNet( wxString::Format( wxT( "Net-(U%d-OUT)" ), aIdx ) );
2965 const int netCfg = addNet( wxString::Format( wxT( "Net-(U%d-CFG)" ), aIdx ) );
2966
2967 auto addPad = [&]( FOOTPRINT* aFp, const wxString& aNumber, int aNetCode )
2968 {
2969 PAD* pad = new PAD( aFp );
2970 pad->SetNumber( aNumber );
2972 pad->SetSize( PADSTACK::ALL_LAYERS,
2973 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2974 pad->SetLayerSet( LSET( { F_Cu } ) );
2975 pad->SetNetCode( aNetCode );
2976 aFp->Add( pad );
2977 };
2978
2979 FOOTPRINT* ic = new FOOTPRINT( m_board.get() );
2980 ic->SetFPID( icId );
2981 ic->SetReference( wxString::Format( wxT( "U%d" ), aIdx ) );
2982 m_board->Add( ic );
2983 addPad( ic, wxT( "1" ), netOut );
2984 addPad( ic, wxT( "2" ), netGnd );
2985
2986 FOOTPRINT* r = new FOOTPRINT( m_board.get() );
2987 r->SetFPID( rId );
2988 r->SetReference( wxString::Format( wxT( "R%d" ), aIdx ) );
2989 m_board->Add( r );
2990 addPad( r, wxT( "1" ), netCfg );
2991 addPad( r, wxT( "2" ), aStrapNet );
2992
2993 ZONE* zone = new ZONE( m_board.get() );
2994 m_board->Add( zone );
2995
2996 return zone;
2997 };
2998
2999 std::vector<std::pair<int, int>> channelStraps = { { 1, netGnd }, { 2, netGnd }, { 3, netVcc } };
3000 std::vector<ZONE*> zones;
3001
3002 TOOL_MANAGER toolMgr;
3003 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3004 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3005
3007 toolMgr.RegisterTool( mtTool );
3008
3009 RULE_AREAS_DATA* ruleData = mtTool->GetData();
3010 ruleData->m_areas.reserve( channelStraps.size() );
3011
3012 for( const auto& [idx, strap] : channelStraps )
3013 {
3014 ZONE* zone = makeChannel( idx, strap );
3015 zones.push_back( zone );
3016
3017 RULE_AREA ra;
3018 ra.m_zone = zone;
3019 ra.m_ruleName = wxString::Format( wxT( "Channel %d" ), idx );
3020
3021 for( FOOTPRINT* fp : m_board->Footprints() )
3022 {
3023 if( fp->GetReference() == wxString::Format( wxT( "U%d" ), idx )
3024 || fp->GetReference() == wxString::Format( wxT( "R%d" ), idx ) )
3025 {
3026 ra.m_components.insert( fp );
3027 }
3028 }
3029
3030 ruleData->m_areas.push_back( ra );
3031 }
3032
3033 // Before the fix, channel 3's lone GND pad hid the rail from that pairing and the match failed.
3034 int status = mtTool->CheckRACompatibility( zones[1] );
3035 BOOST_REQUIRE_EQUAL( status, 0 );
3036
3037 for( RULE_AREA& ra : ruleData->m_areas )
3038 {
3039 if( ra.m_zone == zones[1] )
3040 continue;
3041
3042 auto it = ruleData->m_compatMap.find( &ra );
3043 BOOST_REQUIRE( it != ruleData->m_compatMap.end() );
3044
3045 for( const wxString& reason : it->second.m_mismatchReasons )
3046 BOOST_TEST_MESSAGE( wxString::Format( "%s: %s", ra.m_ruleName, reason ) );
3047
3048 BOOST_CHECK_MESSAGE( it->second.m_isOk,
3049 wxString::Format( "Channel 2 must match %s", ra.m_ruleName ) );
3050 }
3051}
3052
3053
3059BOOST_FIXTURE_TEST_CASE( CheckRACompatOverlappingAreaKeepsLocalNet, MULTICHANNEL_TEST_FIXTURE )
3060{
3061 m_board = std::make_unique<BOARD>();
3062 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
3063
3064 auto addNet = [&]( const wxString& aName ) -> int
3065 {
3066 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), aName );
3067 m_board->Add( net );
3068 return net->GetNetCode();
3069 };
3070
3071 const int netGnd = addNet( wxT( "GND" ) );
3072
3073 const LIB_ID icId( wxT( "TestLib" ), wxT( "SOT-23-6" ) );
3074 const LIB_ID rId( wxT( "TestLib" ), wxT( "R_0402" ) );
3075
3076 auto addPad = [&]( FOOTPRINT* aFp, const wxString& aNumber, int aNetCode )
3077 {
3078 PAD* pad = new PAD( aFp );
3079 pad->SetNumber( aNumber );
3081 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
3082 pad->SetLayerSet( LSET( { F_Cu } ) );
3083 pad->SetNetCode( aNetCode );
3084 aFp->Add( pad );
3085 };
3086
3087 auto makeFp = [&]( const LIB_ID& aFpId, const wxString& aRef ) -> FOOTPRINT*
3088 {
3089 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
3090 fp->SetFPID( aFpId );
3091 fp->SetReference( aRef );
3092 m_board->Add( fp );
3093 return fp;
3094 };
3095
3096 // Channel 1: U1 pad 1 and R1 pad 1 share a per-channel net, giving that net two internal pads.
3097 const int netLocal1 = addNet( wxT( "Net-(U1-SIG)" ) );
3098 FOOTPRINT* u1 = makeFp( icId, wxT( "U1" ) );
3099 addPad( u1, wxT( "1" ), netLocal1 );
3100 addPad( u1, wxT( "2" ), netGnd );
3101 FOOTPRINT* r1 = makeFp( rId, wxT( "R1" ) );
3102 addPad( r1, wxT( "1" ), netLocal1 );
3103 addPad( r1, wxT( "2" ), netGnd );
3104
3105 // Channel 2: U2 pad 1 is on its own net, so R2 does not share it -- a real difference from
3106 // channel 1 that must be reported as a mismatch.
3107 const int netLocal2a = addNet( wxT( "Net-(U2-SIG)" ) );
3108 const int netLocal2b = addNet( wxT( "Net-(R2-SIG)" ) );
3109 FOOTPRINT* u2 = makeFp( icId, wxT( "U2" ) );
3110 addPad( u2, wxT( "1" ), netLocal2a );
3111 addPad( u2, wxT( "2" ), netGnd );
3112 FOOTPRINT* r2 = makeFp( rId, wxT( "R2" ) );
3113 addPad( r2, wxT( "1" ), netLocal2b );
3114 addPad( r2, wxT( "2" ), netGnd );
3115
3116 ZONE* zone1 = new ZONE( m_board.get() );
3117 m_board->Add( zone1 );
3118 ZONE* zone2 = new ZONE( m_board.get() );
3119 m_board->Add( zone2 );
3120 ZONE* zoneDup = new ZONE( m_board.get() );
3121 m_board->Add( zoneDup );
3122
3123 TOOL_MANAGER toolMgr;
3124 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3125 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3126
3128 toolMgr.RegisterTool( mtTool );
3129
3130 RULE_AREAS_DATA* ruleData = mtTool->GetData();
3131 ruleData->m_areas.reserve( 3 );
3132
3133 RULE_AREA ra1;
3134 ra1.m_zone = zone1;
3135 ra1.m_ruleName = wxT( "Channel 1" );
3136 ra1.m_components = { u1, r1 };
3137 ruleData->m_areas.push_back( ra1 );
3138
3139 RULE_AREA ra2;
3140 ra2.m_zone = zone2;
3141 ra2.m_ruleName = wxT( "Channel 2" );
3142 ra2.m_components = { u2, r2 };
3143 ruleData->m_areas.push_back( ra2 );
3144
3145 // A second rule area over channel 1's footprints. Counting it as a distinct channel would make
3146 // Net-(U1-SIG) look global (two areas carry it) and hide the difference from channel 2.
3147 RULE_AREA raDup;
3148 raDup.m_zone = zoneDup;
3149 raDup.m_ruleName = wxT( "Channel 1 duplicate" );
3150 raDup.m_components = { u1, r1 };
3151 ruleData->m_areas.push_back( raDup );
3152
3153 int status = mtTool->CheckRACompatibility( zone1 );
3154 BOOST_REQUIRE_EQUAL( status, 0 );
3155
3156 RULE_AREA* channel2 = nullptr;
3157
3158 for( RULE_AREA& ra : ruleData->m_areas )
3159 {
3160 if( ra.m_zone == zone2 )
3161 channel2 = &ra;
3162 }
3163
3164 BOOST_REQUIRE( channel2 != nullptr );
3165
3166 auto it = ruleData->m_compatMap.find( channel2 );
3167 BOOST_REQUIRE( it != ruleData->m_compatMap.end() );
3168
3169 BOOST_CHECK_MESSAGE( !it->second.m_isOk,
3170 "Channel 1 and channel 2 differ on a local net and must not match; an "
3171 "overlapping rule area must not hide that net by marking it global" );
3172}
3173
3174
3175// A sheet that holds only sub-sheets must still be offered as a channel and gather every
3176// footprint below it, not just the deepest sheet of each footprint.
3177BOOST_FIXTURE_TEST_CASE( MultichannelNestedSheetChannels, MULTICHANNEL_TEST_FIXTURE )
3178{
3179 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3180
3181 TOOL_MANAGER toolMgr;
3182 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3183 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3184
3186 toolMgr.RegisterTool( mtTool );
3187
3189
3190 RULE_AREA* first = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/" ) );
3191 RULE_AREA* second = findSheetRuleAreaByPath( mtTool, wxT( "/SecondChannel/" ) );
3192
3193 BOOST_REQUIRE( first );
3194 BOOST_REQUIRE( second );
3195
3196 BOOST_CHECK_EQUAL( first->m_components.size(), 32 );
3197 BOOST_CHECK_EQUAL( second->m_components.size(), 32 );
3198
3199 // Leaf sheets still work.
3200 RULE_AREA* leaf = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/Common emitter amplifier/" ) );
3201 BOOST_REQUIRE( leaf );
3202 BOOST_CHECK_EQUAL( leaf->m_components.size(), 8 );
3203
3204 // The root is not a channel.
3205 BOOST_CHECK( findSheetRuleAreaByPath( mtTool, wxT( "/" ) ) == nullptr );
3206 BOOST_CHECK( findSheetRuleAreaByPath( mtTool, wxEmptyString ) == nullptr );
3207}
3208
3209
3210// A group that wraps several sub-groups must be offered as a channel and gather every
3211// footprint below it, same as the sheet case.
3212BOOST_FIXTURE_TEST_CASE( MultichannelNestedGroupChannels, MULTICHANNEL_TEST_FIXTURE )
3213{
3214 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3215
3216 TOOL_MANAGER toolMgr;
3217 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3218 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3219
3221 toolMgr.RegisterTool( mtTool );
3222
3224
3225 RULE_AREA* first = findGroupRuleAreaByName( mtTool, wxT( "FirstChannel" ) );
3226 RULE_AREA* second = findGroupRuleAreaByName( mtTool, wxT( "SecondChannel" ) );
3227
3228 BOOST_REQUIRE( first );
3229 BOOST_REQUIRE( second );
3230
3231 BOOST_CHECK_EQUAL( first->m_components.size(), 32 );
3232 BOOST_CHECK_EQUAL( second->m_components.size(), 32 );
3233
3234 // The inner sub-groups still work.
3235 RULE_AREA* sub = findGroupRuleAreaByName( mtTool, wxT( "Common emitter amplifier1 (/FirstChannel/)" ) );
3236 BOOST_REQUIRE( sub );
3237 BOOST_CHECK_EQUAL( sub->m_components.size(), 8 );
3238}
3239
3240
3241// Sheet-level Repeat Layout with "group items" enabled must still reproduce tuning meanders
3242// as generators in the target, not flatten them to loose tracks.
3243BOOST_FIXTURE_TEST_CASE( RepeatLayoutSheetCopiesMeandersWhole, MULTICHANNEL_TEST_FIXTURE )
3244{
3245 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3246
3247 TOOL_MANAGER toolMgr;
3248 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3249 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3250
3252 toolMgr.RegisterTool( mtTool );
3253
3255
3256 auto ruleData = mtTool->GetData();
3257 ruleData->m_replaceExisting = true;
3258
3259 for( RULE_AREA& ra : ruleData->m_areas )
3260 {
3262 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3263 ra.m_generateEnabled = true;
3264 }
3265
3267 mtTool->AutogenerateRuleAreas( dummy );
3268 mtTool->FindExistingRuleAreas();
3269
3270 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3271 RULE_AREA* target = findRuleAreaByPlacementGroup( mtTool, wxT( "/SecondChannel/" ) );
3272
3273 BOOST_REQUIRE( source && source->m_zone );
3274 BOOST_REQUIRE( target && target->m_zone );
3275
3276 // Keep only the source channel's meanders so the copy delta is unambiguous.
3277 std::vector<PCB_GENERATOR*> strip;
3278
3279 for( PCB_GENERATOR* gen : m_board->Generators() )
3280 {
3281 if( gen->GetGeneratorType() != wxT( "tuning_pattern" ) )
3282 continue;
3283
3284 if( !gen->HitTest( source->m_zone->Outline()->COutline( 0 ), false ) )
3285 strip.push_back( gen );
3286 }
3287
3288 for( PCB_GENERATOR* gen : strip )
3289 {
3290 for( EDA_ITEM* member : gen->GetItems() )
3291 {
3292 if( member->IsBOARD_ITEM() )
3293 static_cast<BOARD_ITEM*>( member )->SetParentGroup( nullptr );
3294 }
3295
3296 m_board->Remove( gen );
3297 }
3298
3299 int sourceMeanders = countTuningGensInArea( m_board.get(), source->m_zone );
3300 BOOST_REQUIRE( sourceMeanders > 0 );
3301 BOOST_REQUIRE_EQUAL( countTuningGensInArea( m_board.get(), target->m_zone ), 0 );
3302
3303 ruleData->m_options.m_groupItems = true;
3304
3305 mtTool->CheckRACompatibility( source->m_zone );
3306 ruleData->m_compatMap[target].m_doCopy = true;
3307
3308 int result = mtTool->RepeatLayout( TOOL_EVENT(), source->m_zone );
3309 BOOST_REQUIRE( result >= 0 );
3310
3311 BOOST_CHECK_EQUAL( countTuningGensInArea( m_board.get(), target->m_zone ), sourceMeanders );
3312}
3313
3314
3315// A sheet placement area outline must cover the channel's grouped content (meanders), not just
3316// its footprints, otherwise routing that extends past the parts is left behind on repeat.
3317BOOST_FIXTURE_TEST_CASE( SheetRuleAreaOutlineCoversMeanders, MULTICHANNEL_TEST_FIXTURE )
3318{
3319 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3320
3321 TOOL_MANAGER toolMgr;
3322 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3323 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3324
3326 toolMgr.RegisterTool( mtTool );
3327
3329
3330 auto ruleData = mtTool->GetData();
3331 ruleData->m_replaceExisting = true;
3332
3333 for( RULE_AREA& ra : ruleData->m_areas )
3334 {
3336 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3337 ra.m_generateEnabled = true;
3338 }
3339
3341 mtTool->AutogenerateRuleAreas( dummy );
3342 mtTool->FindExistingRuleAreas();
3343
3344 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3345 BOOST_REQUIRE( source && source->m_zone );
3346
3347 std::set<EDA_GROUP*> groups;
3348
3349 for( FOOTPRINT* fp : source->m_components )
3350 {
3351 for( EDA_GROUP* g = fp->GetParentGroup(); g; g = g->AsEdaItem()->GetParentGroup() )
3352 groups.insert( g );
3353 }
3354
3355 int checked = 0;
3356
3357 for( EDA_GROUP* g : groups )
3358 {
3359 for( EDA_ITEM* member : g->GetItems() )
3360 {
3361 if( member->Type() != PCB_GENERATOR_T )
3362 continue;
3363
3364 BOX2I bb = static_cast<PCB_GENERATOR*>( member )->GetBoundingBox();
3365
3366 BOOST_CHECK( source->m_zone->Outline()->Contains( bb.GetOrigin() ) );
3367 BOOST_CHECK( source->m_zone->Outline()->Contains( bb.GetEnd() ) );
3368 checked++;
3369 }
3370 }
3371
3372 BOOST_REQUIRE( checked > 0 );
3373}
3374
3375
3376// Sheet-level Repeat Layout must reproduce the design-block GND zones in the target channel.
3377BOOST_FIXTURE_TEST_CASE( RepeatLayoutSheetCopiesBlockZones, MULTICHANNEL_TEST_FIXTURE )
3378{
3379 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3380
3381 TOOL_MANAGER toolMgr;
3382 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3383 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3384
3386 toolMgr.RegisterTool( mtTool );
3387
3389
3390 auto ruleData = mtTool->GetData();
3391 ruleData->m_replaceExisting = true;
3392
3393 for( RULE_AREA& ra : ruleData->m_areas )
3394 {
3396 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3397 ra.m_generateEnabled = true;
3398 }
3399
3401 mtTool->AutogenerateRuleAreas( dummy );
3402 mtTool->FindExistingRuleAreas();
3403
3404 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3405 RULE_AREA* target = findRuleAreaByPlacementGroup( mtTool, wxT( "/SecondChannel/" ) );
3406
3407 BOOST_REQUIRE( source && source->m_zone );
3408 BOOST_REQUIRE( target && target->m_zone );
3409
3410 // After the repeat the target should mirror the source's block zones.
3411 int expected = countCopperZonesInArea( m_board.get(), source->m_zone );
3412 BOOST_REQUIRE( expected > 0 );
3413
3414 // Clear the target channel's zones so the copy delta is unambiguous.
3415 std::vector<ZONE*> stripZones;
3416
3417 for( ZONE* zone : m_board->Zones() )
3418 {
3419 if( zone->GetIsRuleArea() )
3420 continue;
3421
3422 if( target->m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
3423 stripZones.push_back( zone );
3424 }
3425
3426 for( ZONE* zone : stripZones )
3427 {
3428 if( EDA_GROUP* group = zone->GetParentGroup() )
3429 group->RemoveItem( zone );
3430
3431 m_board->Remove( zone );
3432 }
3433
3434 BOOST_REQUIRE_EQUAL( countCopperZonesInArea( m_board.get(), target->m_zone ), 0 );
3435
3436 mtTool->CheckRACompatibility( source->m_zone );
3437 ruleData->m_compatMap[target].m_doCopy = true;
3438
3439 int result = mtTool->RepeatLayout( TOOL_EVENT(), source->m_zone );
3440 BOOST_REQUIRE( result >= 0 );
3441
3442 BOOST_CHECK_EQUAL( countCopperZonesInArea( m_board.get(), target->m_zone ), expected );
3443}
3444
3445
3446// The two channels must match by topology so one can be repeated onto the other.
3447BOOST_FIXTURE_TEST_CASE( MultichannelNestedChannelTopologyMatches, MULTICHANNEL_TEST_FIXTURE )
3448{
3450
3451 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3452
3453 TOOL_MANAGER toolMgr;
3454 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3455 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3456
3458 toolMgr.RegisterTool( mtTool );
3459
3461
3462 RULE_AREA* first = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/" ) );
3463 RULE_AREA* second = findSheetRuleAreaByPath( mtTool, wxT( "/SecondChannel/" ) );
3464
3465 BOOST_REQUIRE( first );
3466 BOOST_REQUIRE( second );
3467
3468 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( first->m_components, second->m_components );
3469 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( second->m_components, first->m_components );
3470
3472 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
3473 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
3474
3475 BOOST_CHECK( status );
3476 BOOST_CHECK( details.empty() );
3477}
3478
3479
3486BOOST_FIXTURE_TEST_CASE( GenerateSheetRAIncludesLooseInterBlockRouting, MULTICHANNEL_TEST_FIXTURE )
3487{
3488 m_board = std::make_unique<BOARD>();
3489 BOARD* board = m_board.get();
3490
3491 board->Add( new NETINFO_ITEM( board, wxT( "N1" ), 1 ) );
3492
3493 auto addFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos )
3494 {
3495 FOOTPRINT* fp = new FOOTPRINT( board );
3496 fp->SetReference( aRef );
3497 fp->SetFPID( LIB_ID( wxT( "lib" ), wxT( "R_0402" ) ) );
3498 fp->SetSheetname( wxT( "/ChannelA/" ) );
3499 fp->SetSheetfile( wxT( "channelA.kicad_sch" ) );
3500
3501 PAD* pad = new PAD( fp );
3502 pad->SetNumber( wxT( "1" ) );
3503 pad->SetAttribute( PAD_ATTRIB::SMD );
3504 pad->SetLayerSet( LSET( { F_Cu } ) );
3505 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
3506 fp->Add( pad );
3507
3508 fp->SetPosition( aPos );
3509 board->Add( fp );
3510
3511 pad->SetNetCode( 1 );
3512 };
3513
3514 addFootprint( wxT( "R1" ), VECTOR2I( 0, 0 ) );
3515 addFootprint( wxT( "R2" ), VECTOR2I( pcbIUScale.mmToIU( 10.0 ), 0 ) );
3516
3517 // Track on the channel-local net, detouring far south of the footprints.
3518 PCB_TRACK* track = new PCB_TRACK( board );
3519 track->SetLayer( F_Cu );
3520 track->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
3521 track->SetStart( VECTOR2I( 0, 0 ) );
3522 track->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 5.0 ), pcbIUScale.mmToIU( 30.0 ) ) );
3523 board->Add( track );
3524 track->SetNetCode( 1 );
3525
3526 TOOL_MANAGER toolMgr;
3527 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3528 toolMgr.SetEnvironment( board, nullptr, nullptr, nullptr, toolsHolder );
3529
3531 toolMgr.RegisterTool( mtTool );
3532
3534
3535 auto ruleData = mtTool->GetData();
3536 ruleData->m_replaceExisting = true;
3537
3538 RULE_AREA* channelRA = findSheetRuleAreaByPath( mtTool, wxT( "/ChannelA/" ) );
3539 BOOST_REQUIRE( channelRA != nullptr );
3540 channelRA->m_generateEnabled = true;
3541
3542 TOOL_EVENT dummyEvent;
3543 mtTool->AutogenerateRuleAreas( dummyEvent );
3544
3545 ZONE* raZone = nullptr;
3546
3547 for( ZONE* zone : board->Zones() )
3548 {
3549 if( zone->GetIsRuleArea() && zone->GetZoneName() == wxT( "auto-placement-area-/ChannelA/" ) )
3550 {
3551 raZone = zone;
3552 break;
3553 }
3554 }
3555
3556 BOOST_REQUIRE( raZone != nullptr );
3557
3558 // A point on the bulging track, well past the footprint band.
3559 VECTOR2I onTrack( pcbIUScale.mmToIU( 2.5 ), pcbIUScale.mmToIU( 15.0 ) );
3560
3561 BOOST_CHECK_MESSAGE( raZone->Outline()->Contains( onTrack ),
3562 "Sheet rule area outline must enclose loose inter-block routing (issue 24983)" );
3563}
3564
3565
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
virtual bool SetNetCode(int aNetCode, bool aNoAssert)
Set net using a net code.
virtual void SetNet(NETINFO_ITEM *aNetInfo)
Set a NET_INFO object for the item.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:83
void SetLocked(bool aLocked) override
Definition board_item.h:386
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:343
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1355
const GENERATORS & Generators() const
Definition board.h:434
const ZONES & Zones() const
Definition board.h:425
static constexpr BOX2< VECTOR2I > ByCorners(const VECTOR2I &aCorner1, const VECTOR2I &aCorner2)
Definition box2.h:66
constexpr const Vec GetEnd() const
Definition box2.h:208
constexpr const Vec & GetOrigin() const
Definition box2.h:206
Store all of the related component information found in a netlist.
EDA_ANGLE Normalize()
Definition eda_angle.h:229
double AsDegrees() const
Definition eda_angle.h:116
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:42
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:58
virtual EDA_ITEM * AsEdaItem()=0
void SetName(const wxString &aName)
Definition eda_group.h:48
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:96
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:114
SHAPE_T GetShape() const
Definition eda_shape.h:185
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:190
virtual bool IsVisible() const
Definition eda_text.h:208
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:265
void SetPosition(const VECTOR2I &aPos) override
void SetFPID(const LIB_ID &aFPID)
Definition footprint.h:445
void SetPath(const KIID_PATH &aPath)
Definition footprint.h:468
void SetSheetfile(const wxString &aSheetfile)
Definition footprint.h:474
PCB_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this footprint.
std::deque< PAD * > & Pads()
Definition footprint.h:375
void SetReference(const wxString &aReference)
Definition footprint.h:863
void SetValue(const wxString &aValue)
Definition footprint.h:884
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
void SetSheetname(const wxString &aSheetname)
Definition footprint.h:471
std::vector< FP_3DMODEL > & Models()
Definition footprint.h:395
const wxString & GetReference() const
Definition footprint.h:857
VECTOR2I GetPosition() const override
Definition footprint.h:406
DRAWINGS & GraphicalItems()
Definition footprint.h:378
Definition kiid.h:46
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & AllTechMask()
Return a mask holding all technical layers (no CU layer) on both side.
Definition lset.cpp:672
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
virtual wxWindow * GetToolCanvas() const override
Canvas access.
int CheckRACompatibility(ZONE *aRefZone)
RULE_AREAS_DATA * GetData()
static std::vector< NETINFO_ITEM * > IsolateDesignBlockAutoNets(BOARD *aBoard, const std::set< FOOTPRINT * > &aFootprints, const std::unordered_set< EDA_ITEM * > &aItems)
Remap auto-generated nets (Net-(...), unconnected-...) of a design block that was appended for layout...
int RepeatLayout(const TOOL_EVENT &aEvent, ZONE *aRefZone)
int AutogenerateRuleAreas(const TOOL_EVENT &aEvent)
Handle the data for a net.
Definition netinfo.h:46
int GetNetCode() const
Definition netinfo.h:94
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 SetNumber(const wxString &aNumber)
Set the pad number (note that it can be alphanumeric, such as the array reference "AA12").
Definition pad.h:142
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:51
void SetEnd(const VECTOR2I &aEnd) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) override
void SetStroke(const STROKE_PARAMS &aStroke) override
EDA_ANGLE GetTextAngle() const override
Definition pcb_text.cpp:544
virtual void SetPosition(const VECTOR2I &aPos) override
Definition pcb_text.h:95
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:89
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:92
const VECTOR2I & GetStart() const
Definition pcb_track.h:93
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
bool Contains(const VECTOR2I &aP, int aSubpolyIndex=-1, int aAccuracy=0, bool aUseBBoxCaches=false) const
Return true if a given subpolygon contains the point aP.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
virtual VECTOR2I Centre() const
Compute a center-of-mass of the shape.
Definition shape.h:230
Simple container to manage line stroke parameters.
Generic, UI-independent tool event.
Definition tool_event.h:167
Master controller class:
void RegisterTool(TOOL_BASE *aTool)
Add a tool to the manager set and sets it up.
void SetEnvironment(EDA_ITEM *aModel, KIGFX::VIEW *aView, KIGFX::VIEW_CONTROLS *aViewControls, APP_SETTINGS_BASE *aSettings, TOOLS_HOLDER *aFrame)
Set the work environment (model, view, view controls and the parent window).
Handle a list of polygons defining a copper zone.
Definition zone.h:70
bool GetIsRuleArea() const
Accessors to parameters used in Rule Area zones:
Definition zone.h:813
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1393
wxString GetPlacementAreaSource() const
Definition zone.h:818
SHAPE_POLY_SET * Outline()
Definition zone.h:418
void SetIsRuleArea(bool aEnable)
Definition zone.h:814
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:644
@ DEGREES_T
Definition eda_angle.h:31
@ SEGMENT
Definition eda_shape.h:46
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_SilkS
Definition layer_ids.h:96
@ B_SilkS
Definition layer_ids.h:97
@ F_Cu
Definition layer_ids.h:60
SHAPE_LINE_CHAIN BoxToLineChain(const BOX2I &aBox)
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::map< FOOTPRINT *, FOOTPRINT * > COMPONENT_MATCHES
Definition topo_match.h:187
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
Class to handle a set of BOARD_ITEMs.
Utility functions for working with shapes.
std::vector< FAB_LAYER_COLOR > dummy
std::unique_ptr< BOARD > m_board
std::unordered_map< RULE_AREA *, RULE_AREA_COMPAT_DATA > m_compatMap
std::vector< RULE_AREA > m_areas
VECTOR2I m_center
std::unordered_set< EDA_ITEM * > m_designBlockItems
wxString m_sheetName
PLACEMENT_SOURCE_T m_sourceType
std::set< FOOTPRINT * > m_components
wxString m_ruleName
PCB_GROUP * m_group
wxString m_sheetPath
std::atomic< bool > * m_cancelled
Definition topo_match.h:46
std::atomic< int > * m_matchedComponents
Definition topo_match.h:47
std::atomic< int > * m_totalComponents
Definition topo_match.h:48
@ VALUE
Field Value of part, i.e. "3.3K".
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR3I expected(15, 30, 45)
VECTOR3I res
int countCopperZonesInArea(BOARD *aBoard, ZONE *aRAZone)
RULE_AREA * findGroupRuleAreaByName(MULTICHANNEL_TOOL *aTool, const wxString &aGroupName)
RULE_AREA * findRuleAreaByPartialName(MULTICHANNEL_TOOL *aTool, const wxString &aName)
int countZonesByNamePrefixInRuleArea(BOARD *aBoard, const wxString &aBaseName, const RULE_AREA &aRuleArea)
RULE_AREA * findSheetRuleAreaByPath(MULTICHANNEL_TOOL *aTool, wxString aPath)
BOOST_FIXTURE_TEST_CASE(MultichannelToolRegressions, MULTICHANNEL_TEST_FIXTURE)
int countZonesByNameInRuleArea(BOARD *aBoard, const wxString &aZoneName, const RULE_AREA &aRuleArea)
int countTuningGensInArea(BOARD *aBoard, ZONE *aZone)
RULE_AREA * findRuleAreaByPlacementGroup(MULTICHANNEL_TOOL *aTool, const wxString &aGroupName)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
int actual
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:84
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:90
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:85
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683