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>
38#include <board_commit.h>
39#include <wx/filename.h>
40#include <wx/ffile.h>
45#include <lib_id.h>
46#include <atomic>
47
55
57{
58public:
61
62 virtual wxWindow* GetToolCanvas() const override { return nullptr; }
63};
64
65BOOST_FIXTURE_TEST_SUITE( MultichannelTool, MULTICHANNEL_TEST_FIXTURE )
66
67RULE_AREA* findRuleAreaByPartialName( MULTICHANNEL_TOOL* aTool, const wxString& aName )
68{
69 for( RULE_AREA& ra : aTool->GetData()->m_areas )
70 {
71 if( ra.m_ruleName.Contains( ( aName ) ) )
72 return &ra;
73 }
74
75 return nullptr;
76}
77
79{
80 if( aPath.EndsWith( wxT( "/" ) ) )
81 aPath.RemoveLast();
82
83 for( RULE_AREA& ra : aTool->GetData()->m_areas )
84 {
85 if( ra.m_sourceType != PLACEMENT_SOURCE_T::SHEETNAME )
86 continue;
87
88 wxString path = ra.m_sheetPath;
89
90 if( path.EndsWith( wxT( "/" ) ) )
91 path.RemoveLast();
92
93 if( path == aPath )
94 return &ra;
95 }
96
97 return nullptr;
98}
99
100RULE_AREA* findGroupRuleAreaByName( MULTICHANNEL_TOOL* aTool, const wxString& aGroupName )
101{
102 for( RULE_AREA& ra : aTool->GetData()->m_areas )
103 {
104 if( ra.m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT && ra.m_groupName == aGroupName )
105 return &ra;
106 }
107
108 return nullptr;
109}
110
111RULE_AREA* findRuleAreaByPlacementGroup( MULTICHANNEL_TOOL* aTool, const wxString& aGroupName )
112{
113 for( RULE_AREA& ra : aTool->GetData()->m_areas )
114 {
115 if( ra.m_zone && ra.m_zone->GetPlacementAreaSource() == aGroupName )
116 return &ra;
117 }
118
119 return nullptr;
120}
121
122int countTuningGensInArea( BOARD* aBoard, ZONE* aZone )
123{
124 int n = 0;
125 const SHAPE_LINE_CHAIN& outline = aZone->Outline()->COutline( 0 );
126
127 for( PCB_GENERATOR* gen : aBoard->Generators() )
128 {
129 if( gen->GetGeneratorType() != wxT( "tuning_pattern" ) )
130 continue;
131
132 if( gen->HitTest( outline, false ) )
133 n++;
134 }
135
136 return n;
137}
138
139
140int countCopperZonesInArea( BOARD* aBoard, ZONE* aRAZone )
141{
142 int n = 0;
143
144 for( ZONE* zone : aBoard->Zones() )
145 {
146 if( zone == aRAZone || zone->GetIsRuleArea() )
147 continue;
148
149 if( aRAZone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
150 n++;
151 }
152
153 return n;
154}
155
156
157int countZonesByNameInRuleArea( BOARD* aBoard, const wxString& aZoneName, const RULE_AREA& aRuleArea )
158{
159 int count = 0;
160
161 for( const ZONE* zone : aBoard->Zones() )
162 {
163 if( zone == aRuleArea.m_zone )
164 continue;
165
166 if( zone->GetZoneName() != aZoneName )
167 continue;
168
169 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
170 count++;
171 }
172
173 return count;
174}
175
176
177int countZonesByNamePrefixInRuleArea( BOARD* aBoard, const wxString& aBaseName, const RULE_AREA& aRuleArea )
178{
179 int count = 0;
180
181 for( const ZONE* zone : aBoard->Zones() )
182 {
183 if( zone == aRuleArea.m_zone )
184 continue;
185
186 const wxString& name = zone->GetZoneName();
187
188 // A copied zone may get a _<n> suffix for uniqueness (issue 23131).
189 if( name != aBaseName && !name.StartsWith( aBaseName + wxT( "_" ) ) )
190 continue;
191
192 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
193 count++;
194 }
195
196 return count;
197}
198
199
201{
203
204 std::vector<wxString> tests = { "vme-wren" };
205
206 for( const wxString& relPath : tests )
207 {
208 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
209
210 TOOL_MANAGER toolMgr;
211 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
212
213 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
214
215 MULTICHANNEL_TOOL* mtTool = new MULTICHANNEL_TOOL; // TOOL_MANAGER owns the tools
216 toolMgr.RegisterTool( mtTool );
217
218 //RULE_AREAS_DATA* raData = m_parentTool->GetData();
219
221
222 auto ruleData = mtTool->GetData();
223
224 BOOST_TEST_MESSAGE( wxString::Format( "RA multichannel sheets = %d",
225 static_cast<int>( ruleData->m_areas.size() ) ) );
226
227 // 73 counts the /fpga/ container sheet, now offered as a channel.
228 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 73 );
229
230 int cnt = 0;
231
232 ruleData->m_replaceExisting = true;
233
234 for( RULE_AREA& ra : ruleData->m_areas )
235 {
236 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" )
237 || ra.m_sheetName == wxT( "pp_driver_2x.kicad_sch" ) )
238 {
239 ra.m_generateEnabled = true;
240 cnt++;
241 }
242 }
243
244 BOOST_TEST_MESSAGE( wxString::Format( "Autogenerating %d RAs", cnt ) );
245
246 TOOL_EVENT dummyEvent;
247
248 mtTool->AutogenerateRuleAreas( dummyEvent );
249 mtTool->FindExistingRuleAreas();
250
251 int n_areas_io = 0, n_areas_pp = 0, n_areas_other = 0;
252
253 BOOST_TEST_MESSAGE( wxString::Format( "Found %d RAs after commit",
254 static_cast<int>(ruleData->m_areas.size() ) ) );
255
256 for( const RULE_AREA& ra : ruleData->m_areas )
257 {
258 BOOST_TEST_MESSAGE( wxString::Format( "SN '%s'", ra.m_ruleName ) );
259
260 if( ra.m_ruleName.Contains( wxT( "io_drivers_fp" ) ) )
261 {
262 n_areas_io++;
263 BOOST_CHECK_EQUAL( ra.m_components.size(), 31 );
264 }
265 else if( ra.m_ruleName.Contains( wxT( "io_drivers_pp" ) ) )
266 {
267 n_areas_pp++;
268 BOOST_CHECK_EQUAL( ra.m_components.size(), 11 );
269 }
270 else
271 {
272 n_areas_other++;
273 }
274 }
275
276 BOOST_TEST_MESSAGE( wxString::Format( "IO areas=%d, PP areas=%d, others=%d",
277 n_areas_io, n_areas_pp, n_areas_other ) );
278
279 BOOST_CHECK_EQUAL( n_areas_io, 16 );
280 BOOST_CHECK_EQUAL( n_areas_pp, 16 );
281 BOOST_CHECK_EQUAL( n_areas_other, 0 );
282
283 const std::vector<wxString> rulesToTest = { wxT( "io_drivers_fp" ),
284 wxT( "io_drivers_pp" ) };
285
286 for( const wxString& ruleName : rulesToTest )
287 {
288 for( const RULE_AREA& refArea : ruleData->m_areas )
289 {
290 if( !refArea.m_ruleName.Contains( ruleName ) )
291 continue;
292
293 BOOST_TEST_MESSAGE( wxString::Format( "REF AREA: '%s'", refArea.m_ruleName ) );
294
295 for( const RULE_AREA& targetArea : ruleData->m_areas )
296 {
297 if( targetArea.m_zone == refArea.m_zone )
298 continue;
299
300 if( !targetArea.m_ruleName.Contains( ruleName ) )
301 continue;
302
303 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea.m_components,
304 targetArea.m_components );
305 auto cgTarget =
306 CONNECTION_GRAPH::BuildFromFootprintSet( targetArea.m_components,
307 refArea.m_components );
308
310
311 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
312 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
313
314 BOOST_TEST_MESSAGE( wxString::Format(
315 "topo match: '%s' [%d] -> '%s' [%d] result %d", refArea.m_ruleName.c_str().AsChar(),
316 static_cast<int>( refArea.m_components.size() ), targetArea.m_ruleName.c_str().AsChar(),
317 static_cast<int>( targetArea.m_components.size() ), status ? 1 : 0 ) );
318
319 for( const auto& iter : result )
320 {
321 BOOST_TEST_MESSAGE( wxString::Format( "%s : %s",
322 iter.second->GetReference(),
323 iter.first->GetReference() ) );
324 }
325
326 BOOST_CHECK( status );
327 BOOST_CHECK( details.empty() );
328 }
329 }
330 }
331
332 auto refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
333
334 BOOST_ASSERT( refArea );
335
336 const std::vector<wxString> targetAreaNames( { wxT( "io_drivers_fp/bank2/io78/" ),
337 wxT( "io_drivers_fp/bank1/io78/" ),
338 wxT( "io_drivers_fp/bank0/io01/" ) } );
339
340 for( const wxString& targetRaName : targetAreaNames )
341 {
342 auto targetRA = findRuleAreaByPartialName( mtTool, targetRaName );
343
344 BOOST_ASSERT( targetRA != nullptr );
345
346 BOOST_TEST_MESSAGE( wxString::Format( "Clone to: %s", targetRA->m_ruleName ) );
347
348 ruleData->m_compatMap[targetRA].m_doCopy = true;
349 }
350
351 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
352
353 BOOST_ASSERT( result >= 0 );
354 }
355}
356
357
362BOOST_FIXTURE_TEST_CASE( RepeatLayoutCopiesFootprintProperties, MULTICHANNEL_TEST_FIXTURE )
363{
364 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
365
366 TOOL_MANAGER toolMgr;
367 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
368
369 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
370
372 toolMgr.RegisterTool( mtTool );
373
374 mtTool->FindExistingRuleAreas();
375
376 auto ruleData = mtTool->GetData();
377
378 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
379 static_cast<int>( ruleData->m_areas.size() ) ) );
380
381 BOOST_CHECK( ruleData->m_areas.size() >= 2 );
382
383 if( ruleData->m_areas.size() < 2 )
384 return;
385
386 RULE_AREA* refArea = nullptr;
387 RULE_AREA* targetArea = nullptr;
388
389 for( RULE_AREA& ra : ruleData->m_areas )
390 {
391 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
392 refArea = &ra;
393 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
394 targetArea = &ra;
395 }
396
397 if( !refArea || !targetArea )
398 {
399 BOOST_TEST_MESSAGE( "Could not find Untitled Sheet and Untitled Sheet1 rule areas, skipping test" );
400 return;
401 }
402
403 BOOST_TEST_MESSAGE( wxString::Format( "Reference area: %s, Target area: %s",
404 refArea->m_ruleName, targetArea->m_ruleName ) );
405
406 FOOTPRINT* refFP = nullptr;
407 FOOTPRINT* targetFP = nullptr;
408
409 for( FOOTPRINT* fp : refArea->m_components )
410 {
411 if( fp->GetReference().StartsWith( wxT( "U1" ) ) )
412 {
413 refFP = fp;
414 break;
415 }
416 }
417
418 for( FOOTPRINT* fp : targetArea->m_components )
419 {
420 if( fp->GetReference().StartsWith( wxT( "U2" ) ) )
421 {
422 targetFP = fp;
423 break;
424 }
425 }
426
427 if( !refFP || !targetFP )
428 {
429 BOOST_TEST_MESSAGE( "Could not find matching footprints in the rule areas, skipping test" );
430 return;
431 }
432
433 PCB_FIELD* refValueField = refFP->GetField( FIELD_T::VALUE );
434 bool refValueVisible = refValueField ? refValueField->IsVisible() : true;
435
436 std::vector<FP_3DMODEL> refModels = refFP->Models();
437
438 mtTool->CheckRACompatibility( refArea->m_zone );
439
440 ruleData->m_compatMap[targetArea].m_doCopy = true;
441 ruleData->m_options.m_copyPlacement = true;
442
443 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
444
445 BOOST_CHECK( result >= 0 );
446
447 PCB_FIELD* targetValueField = targetFP->GetField( FIELD_T::VALUE );
448
449 if( targetValueField && refValueField )
450 {
451 BOOST_CHECK_EQUAL( targetValueField->IsVisible(), refValueVisible );
452 BOOST_TEST_MESSAGE( wxString::Format( "Value field visibility: ref=%d, target=%d",
453 refValueVisible, targetValueField->IsVisible() ) );
454 }
455
456 BOOST_CHECK_EQUAL( targetFP->Models().size(), refModels.size() );
457
458 if( !refModels.empty() )
459 {
460 BOOST_TEST_MESSAGE( wxString::Format( "3D models: ref=%d, target=%d",
461 static_cast<int>( refModels.size() ),
462 static_cast<int>( targetFP->Models().size() ) ) );
463 }
464}
465
466
474BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotRemoveReferenceVias, MULTICHANNEL_TEST_FIXTURE )
475{
476 KI_TEST::LoadBoard( m_settingsManager, "issue21184/issue21184", m_board );
477
478 TOOL_MANAGER toolMgr;
479 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
480
481 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
482
484 toolMgr.RegisterTool( mtTool );
485
486 mtTool->FindExistingRuleAreas();
487
488 auto ruleData = mtTool->GetData();
489
490 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
491 static_cast<int>( ruleData->m_areas.size() ) ) );
492
493 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 2 );
494
495 if( ruleData->m_areas.size() < 2 )
496 return;
497
498 RULE_AREA* refArea = nullptr;
499 RULE_AREA* targetArea = nullptr;
500
501 for( RULE_AREA& ra : ruleData->m_areas )
502 {
503 if( ra.m_ruleName == wxT( "Test1" ) )
504 refArea = &ra;
505 else if( ra.m_ruleName == wxT( "Test2" ) )
506 targetArea = &ra;
507 }
508
509 BOOST_REQUIRE( refArea != nullptr );
510 BOOST_REQUIRE( targetArea != nullptr );
511
512 int refViaCountBefore = 0;
513
514 for( PCB_TRACK* track : m_board->Tracks() )
515 {
516 if( track->Type() == PCB_VIA_T )
517 {
518 PCB_VIA* via = static_cast<PCB_VIA*>( track );
519 VECTOR2I viaPos = via->GetPosition();
520
521 if( refArea->m_zone->Outline()->Contains( viaPos ) )
522 refViaCountBefore++;
523 }
524 }
525
526 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias before repeat: %d", refViaCountBefore ) );
527 BOOST_CHECK( refViaCountBefore > 0 );
528
529 mtTool->CheckRACompatibility( refArea->m_zone );
530
531 ruleData->m_compatMap[targetArea].m_doCopy = true;
532 ruleData->m_options.m_copyPlacement = true;
533 ruleData->m_options.m_copyRouting = true;
534
535 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
536
537 BOOST_CHECK( result >= 0 );
538
539 int refViaCountAfter = 0;
540
541 for( PCB_TRACK* track : m_board->Tracks() )
542 {
543 if( track->Type() == PCB_VIA_T )
544 {
545 PCB_VIA* via = static_cast<PCB_VIA*>( track );
546 VECTOR2I viaPos = via->GetPosition();
547
548 if( refArea->m_zone->Outline()->Contains( viaPos ) )
549 refViaCountAfter++;
550 }
551 }
552
553 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias after repeat: %d", refViaCountAfter ) );
554
555 BOOST_CHECK_EQUAL( refViaCountAfter, refViaCountBefore );
556}
557
558
563BOOST_FIXTURE_TEST_CASE( RepeatLayoutRespectsZoneLayerSetsForOtherItems, MULTICHANNEL_TEST_FIXTURE )
564{
565 KI_TEST::LoadBoard( m_settingsManager, "issue22983/issue22983", m_board );
566
567 TOOL_MANAGER toolMgr;
568 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
569
570 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
571
573 toolMgr.RegisterTool( mtTool );
574
575 mtTool->FindExistingRuleAreas();
576
577 RULE_AREA* sourceA = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceA" ) );
578 RULE_AREA* destA = findRuleAreaByPlacementGroup( mtTool, wxT( "DestA" ) );
579 RULE_AREA* sourceB = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceB" ) );
580 RULE_AREA* destB = findRuleAreaByPlacementGroup( mtTool, wxT( "DestB" ) );
581
582 BOOST_REQUIRE( sourceA != nullptr );
583 BOOST_REQUIRE( destA != nullptr );
584 BOOST_REQUIRE( sourceB != nullptr );
585 BOOST_REQUIRE( destB != nullptr );
586
587 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *sourceA ), 1 );
588 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ), 0 );
590 countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *sourceB ), 1 );
591 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
592 0 );
593 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *sourceB ), 1 );
594 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
595
596 REPEAT_LAYOUT_OPTIONS options;
597 options.m_copyPlacement = false;
598 options.m_copyRouting = false;
599 options.m_copyOtherItems = true;
600 options.m_includeLockedItems = true;
601
602 int copyAStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceA, *destA, options );
603 BOOST_REQUIRE( copyAStatus >= 0 );
604
605 int copyBStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceB, *destB, options );
606 BOOST_REQUIRE( copyBStatus >= 0 );
607
608 // SourceA and DestA both include F.Cu+B.Cu, so this multilayer zone should copy.
609 // The copy is renamed for uniqueness (issue 23131), so match the base name as a prefix.
610 BOOST_CHECK_EQUAL( countZonesByNamePrefixInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ),
611 1 );
612
613 // SourceB only includes F.Cu, so this F.Cu+B.Cu zone should not copy to DestB.
614 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
615 0 );
616
617 // SourceB excludes B.Cu, so this B.Cu-only zone should not copy either.
618 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
619}
620
621
630{
632 using TMATCH::COMPONENT;
633
634 // Create two connection graphs with components that have dotted reference designators
635 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
636 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
637
638 // Create mock footprints with the same FPID
639 LIB_ID fpid( wxT( "Package_SO" ), wxT( "SOIC-8_3.9x4.9mm_P1.27mm" ) );
640
641 // Create reference footprint TRIM_1.1 and target footprint TRIM_2.1
642 FOOTPRINT fpRef( nullptr );
643 fpRef.SetFPID( fpid );
644 fpRef.SetReference( wxT( "TRIM_1.1" ) );
645
646 FOOTPRINT fpTarget( nullptr );
647 fpTarget.SetFPID( fpid );
648 fpTarget.SetReference( wxT( "TRIM_2.1" ) );
649
650 // Create matching pad structures
651 PAD padRef1( &fpRef );
652 padRef1.SetNumber( wxT( "1" ) );
653 padRef1.SetNetCode( 1 );
654 fpRef.Add( &padRef1 );
655
656 PAD padRef2( &fpRef );
657 padRef2.SetNumber( wxT( "2" ) );
658 padRef2.SetNetCode( 2 );
659 fpRef.Add( &padRef2 );
660
661 PAD padTarget1( &fpTarget );
662 padTarget1.SetNumber( wxT( "1" ) );
663 padTarget1.SetNetCode( 3 );
664 fpTarget.Add( &padTarget1 );
665
666 PAD padTarget2( &fpTarget );
667 padTarget2.SetNumber( wxT( "2" ) );
668 padTarget2.SetNetCode( 4 );
669 fpTarget.Add( &padTarget2 );
670
671 // Build connection graphs
672 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
673 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
674
675 cgRef->BuildConnectivity();
676 cgTarget->BuildConnectivity();
677
678 // Check that the components are considered the same kind
679 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
680 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
681
682 COMPONENT* cmpRef = cgRef->Components()[0];
683 COMPONENT* cmpTarget = cgTarget->Components()[0];
684
685 bool sameKind = cmpRef->IsSameKind( *cmpTarget );
686
687 BOOST_TEST_MESSAGE( wxString::Format( "TRIM_1.1 and TRIM_2.1 IsSameKind: %d", sameKind ? 1 : 0 ) );
688 BOOST_CHECK( sameKind );
689
690 // Test topology matching
692 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
693 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
694
695 BOOST_TEST_MESSAGE( wxString::Format( "Topology match result: %d", status ? 1 : 0 ) );
696
697 if( !status && !details.empty() )
698 {
699 for( const auto& reason : details )
700 {
701 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
702 reason.m_reference, reason.m_candidate, reason.m_reason ) );
703 }
704 }
705
706 BOOST_CHECK( status );
707 BOOST_CHECK( details.empty() );
708
709 // Cleanup: remove pads before footprints go out of scope
710 fpRef.Pads().clear();
711 fpTarget.Pads().clear();
712}
713
714
721BOOST_FIXTURE_TEST_CASE( TopologyMatchUnannotatedReference, MULTICHANNEL_TEST_FIXTURE )
722{
724 using TMATCH::COMPONENT;
725
726 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
727 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
728
729 LIB_ID fpid( wxT( "_BugFpLib" ), wxT( "SW_Hotswap" ) );
730
731 // Reference footprint as stored in the design block: never annotated, so it keeps the
732 // default REF** placeholder.
733 FOOTPRINT fpRef( nullptr );
734 fpRef.SetFPID( fpid );
735 fpRef.SetReference( wxT( "REF**" ) );
736
737 // Destination footprint placed and annotated on the board.
738 FOOTPRINT fpTarget( nullptr );
739 fpTarget.SetFPID( fpid );
740 fpTarget.SetReference( wxT( "SW1" ) );
741
742 PAD padRef1( &fpRef );
743 padRef1.SetNumber( wxT( "1" ) );
744 padRef1.SetNetCode( 1 );
745 fpRef.Add( &padRef1 );
746
747 PAD padRef2( &fpRef );
748 padRef2.SetNumber( wxT( "2" ) );
749 padRef2.SetNetCode( 2 );
750 fpRef.Add( &padRef2 );
751
752 PAD padTarget1( &fpTarget );
753 padTarget1.SetNumber( wxT( "1" ) );
754 padTarget1.SetNetCode( 3 );
755 fpTarget.Add( &padTarget1 );
756
757 PAD padTarget2( &fpTarget );
758 padTarget2.SetNumber( wxT( "2" ) );
759 padTarget2.SetNetCode( 4 );
760 fpTarget.Add( &padTarget2 );
761
762 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
763 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
764
765 cgRef->BuildConnectivity();
766 cgTarget->BuildConnectivity();
767
768 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
769 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
770
771 COMPONENT* cmpRef = cgRef->Components()[0];
772 COMPONENT* cmpTarget = cgTarget->Components()[0];
773
774 BOOST_CHECK_MESSAGE( cmpRef->IsSameKind( *cmpTarget ),
775 "REF** placeholder must be the same kind as an annotated footprint "
776 "with matching FPID (issue 24585)" );
777
779 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
780 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
781
782 if( !status )
783 {
784 for( const auto& reason : details )
785 {
786 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
787 reason.m_reference, reason.m_candidate, reason.m_reason ) );
788 }
789 }
790
791 BOOST_CHECK_MESSAGE( status,
792 "Design block layout with an un-annotated REF** footprint must match "
793 "an annotated destination (issue 24585)" );
794 BOOST_CHECK( details.empty() );
795
796 fpRef.Pads().clear();
797 fpTarget.Pads().clear();
798}
799
800
806BOOST_FIXTURE_TEST_CASE( TopologyMatchAnnotatedReferencesKeepPrefixCheck, MULTICHANNEL_TEST_FIXTURE )
807{
808 using TMATCH::COMPONENT;
809
810 LIB_ID fpid( wxT( "_BugFpLib" ), wxT( "SW_Hotswap" ) );
811 LIB_ID otherFpid( wxT( "_BugFpLib" ), wxT( "R_0402" ) );
812
813 FOOTPRINT fpTarget( nullptr );
814 fpTarget.SetFPID( fpid );
815 fpTarget.SetReference( wxT( "SW1" ) );
816 COMPONENT cmpTarget( fpTarget.GetReference(), &fpTarget );
817
818 // Unrelated annotated prefixes with the same FPID must not match.
819 FOOTPRINT fpUnrelated( nullptr );
820 fpUnrelated.SetFPID( fpid );
821 fpUnrelated.SetReference( wxT( "U1A" ) );
822 COMPONENT cmpUnrelated( fpUnrelated.GetReference(), &fpUnrelated );
823
824 BOOST_CHECK_MESSAGE( !cmpUnrelated.IsSameKind( cmpTarget ),
825 "Two annotated footprints with unrelated prefixes must not match even "
826 "with the same FPID (issue 24585)" );
827
828 // The placeholder wildcard must not bypass the footprint-ID requirement.
829 FOOTPRINT fpPlaceholder( nullptr );
830 fpPlaceholder.SetFPID( otherFpid );
831 fpPlaceholder.SetReference( wxT( "REF**" ) );
832 COMPONENT cmpPlaceholder( fpPlaceholder.GetReference(), &fpPlaceholder );
833
834 BOOST_CHECK_MESSAGE( !cmpPlaceholder.IsSameKind( cmpTarget ),
835 "A REF** placeholder must not match a target with a different FPID "
836 "(issue 24585)" );
837}
838
839
848BOOST_FIXTURE_TEST_CASE( GenerateRuleAreasIncludesChildSheets, MULTICHANNEL_TEST_FIXTURE )
849{
850 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
851
852 TOOL_MANAGER toolMgr;
853 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
854
855 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
856
858 toolMgr.RegisterTool( mtTool );
859
861
862 auto ruleData = mtTool->GetData();
863
864 RULE_AREA* leafArea = nullptr;
865 RULE_AREA* midArea = nullptr;
866 RULE_AREA* topArea = nullptr;
867
868 for( RULE_AREA& ra : ruleData->m_areas )
869 {
870 if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/io01/" ) )
871 leafArea = &ra;
872 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/" ) )
873 midArea = &ra;
874 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/" ) )
875 topArea = &ra;
876 }
877
878 BOOST_REQUIRE( leafArea != nullptr );
879 BOOST_REQUIRE( midArea != nullptr );
880 BOOST_REQUIRE( topArea != nullptr );
881
882 BOOST_TEST_MESSAGE( wxString::Format( "Leaf /io_drivers_fp/bank0/io01/ components: %d",
883 static_cast<int>( leafArea->m_components.size() ) ) );
884 BOOST_TEST_MESSAGE( wxString::Format( "Mid /io_drivers_fp/bank0/ components: %d",
885 static_cast<int>( midArea->m_components.size() ) ) );
886 BOOST_TEST_MESSAGE( wxString::Format( "Top /io_drivers_fp/ components: %d",
887 static_cast<int>( topArea->m_components.size() ) ) );
888
889 // Leaf sheet has 31 direct components and no children
890 BOOST_CHECK_EQUAL( leafArea->m_components.size(), 31 );
891
892 // Mid-level sheet has 7 direct + 4 child sheets * 31 each = 131
893 BOOST_CHECK_EQUAL( midArea->m_components.size(), 131 );
894
895 // Top-level sheet has 3 direct + 4 banks * 131 each = 527
896 BOOST_CHECK_EQUAL( topArea->m_components.size(), 527 );
897
898 // Mid-level components must be a superset of leaf components
899 for( FOOTPRINT* fp : leafArea->m_components )
900 BOOST_CHECK( midArea->m_components.count( fp ) > 0 );
901
902 // Top-level components must be a superset of mid-level components
903 for( FOOTPRINT* fp : midArea->m_components )
904 BOOST_CHECK( topArea->m_components.count( fp ) > 0 );
905}
906
907
913{
915
916 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
917
918 TOOL_MANAGER toolMgr;
919 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
920
921 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
922
924 toolMgr.RegisterTool( mtTool );
925
927
928 auto ruleData = mtTool->GetData();
929
930 ruleData->m_replaceExisting = true;
931
932 for( RULE_AREA& ra : ruleData->m_areas )
933 {
934 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" ) )
935 ra.m_generateEnabled = true;
936 }
937
938 TOOL_EVENT dummyEvent;
939 mtTool->AutogenerateRuleAreas( dummyEvent );
940 mtTool->FindExistingRuleAreas();
941
942 RULE_AREA* refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
943 RULE_AREA* targetArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank2/io78/" ) );
944
945 BOOST_REQUIRE( refArea != nullptr );
946 BOOST_REQUIRE( targetArea != nullptr );
947
948 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
949 targetArea->m_components );
950 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
951 refArea->m_components );
952
953 // Pre-cancelled: should return false immediately with empty result
954 {
955 std::atomic<bool> cancelled( true );
956 std::atomic<int> matched( 0 );
957 std::atomic<int> total( 0 );
958
960 params.m_cancelled = &cancelled;
961 params.m_matchedComponents = &matched;
962 params.m_totalComponents = &total;
963
965 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
966
967 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
968
969 BOOST_CHECK( !status );
970 BOOST_CHECK( result.empty() );
971
972 BOOST_TEST_MESSAGE( "Pre-cancelled FindIsomorphism correctly returned false" );
973 }
974
975 // Normal run with progress reporting
976 {
977 std::atomic<bool> cancelled( false );
978 std::atomic<int> matched( 0 );
979 std::atomic<int> total( 0 );
980
982 params.m_cancelled = &cancelled;
983 params.m_matchedComponents = &matched;
984 params.m_totalComponents = &total;
985
987 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
988
989 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
990
991 BOOST_CHECK( status );
992
993 int finalMatched = matched.load();
994 int finalTotal = total.load();
995
996 BOOST_TEST_MESSAGE( wxString::Format( "Progress: matched=%d, total=%d", finalMatched, finalTotal ) );
997
998 BOOST_CHECK( finalTotal > 0 );
999 BOOST_CHECK_EQUAL( finalMatched, finalTotal );
1000 }
1001
1002 // Sanity check: same graphs without params still succeed
1003 {
1005 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1006
1007 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1008
1009 BOOST_CHECK( status );
1010 BOOST_CHECK( !result.empty() );
1011
1012 BOOST_TEST_MESSAGE( "Default params FindIsomorphism still succeeds" );
1013 }
1014}
1015
1016
1027BOOST_FIXTURE_TEST_CASE( TopoMatchGlobalNetHierarchicalPins, MULTICHANNEL_TEST_FIXTURE )
1028{
1030
1031 KI_TEST::LoadBoard( m_settingsManager, "issue21739/topology_mismatch", m_board );
1032
1033 TOOL_MANAGER toolMgr;
1034 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1035
1036 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1037
1039 toolMgr.RegisterTool( mtTool );
1040
1041 mtTool->FindExistingRuleAreas();
1042
1043 auto ruleData = mtTool->GetData();
1044
1045 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1046 static_cast<int>( ruleData->m_areas.size() ) ) );
1047
1048 BOOST_REQUIRE( ruleData->m_areas.size() >= 2 );
1049
1050 RULE_AREA* ch0Area = nullptr;
1051 RULE_AREA* ch1Area = nullptr;
1052
1053 for( RULE_AREA& ra : ruleData->m_areas )
1054 {
1055 if( !ra.m_zone )
1056 continue;
1057
1058 wxString source = ra.m_zone->GetPlacementAreaSource();
1059
1060 if( source == wxT( "/i2c_thingy_ch0/" ) )
1061 ch0Area = &ra;
1062 else if( source == wxT( "/i2c_thingy_ch1/" ) )
1063 ch1Area = &ra;
1064 }
1065
1066 BOOST_REQUIRE_MESSAGE( ch0Area != nullptr, "Could not find i2c_thingy_ch0 rule area" );
1067 BOOST_REQUIRE_MESSAGE( ch1Area != nullptr, "Could not find i2c_thingy_ch1 rule area" );
1068
1069 BOOST_TEST_MESSAGE( wxString::Format( "ch0 components: %d, ch1 components: %d",
1070 static_cast<int>( ch0Area->m_components.size() ),
1071 static_cast<int>( ch1Area->m_components.size() ) ) );
1072
1073 BOOST_CHECK_EQUAL( ch0Area->m_components.size(), ch1Area->m_components.size() );
1074
1075 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( ch0Area->m_components,
1076 ch1Area->m_components );
1077 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( ch1Area->m_components,
1078 ch0Area->m_components );
1079
1081 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1082 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1083
1084 if( !status )
1085 {
1086 for( const auto& reason : details )
1087 {
1088 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
1089 reason.m_reference, reason.m_candidate,
1090 reason.m_reason ) );
1091 }
1092 }
1093
1094 BOOST_CHECK_MESSAGE( status,
1095 "Topology match failed for channels with hierarchical pins "
1096 "tied to global nets (issue 21739)" );
1097}
1098
1099
1106BOOST_FIXTURE_TEST_CASE( TopoMatchBoundarySignalNetNotExcluded, MULTICHANNEL_TEST_FIXTURE )
1107{
1109
1110 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
1111
1112 // The board forces consecutive net codes, so register by name and use the assigned code.
1113 auto addNet = [&]( const wxString& aName ) -> int
1114 {
1115 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
1116 board->Add( net );
1117 return net->GetNetCode();
1118 };
1119
1120 const int netGnd = addNet( wxT( "GND" ) );
1121 const int netVcc = addNet( wxT( "+5VP" ) );
1122 const int netChainInRef = addNet( wxT( "Net-(D-RefChainIn)" ) );
1123 const int netBridge = addNet( wxT( "Net-(D38-DOUT)" ) ); // reference output AND target input
1124 const int netChainOutTgt = addNet( wxT( "Net-(D43-DOUT)" ) );
1125 const int netD2Dout = addNet( wxT( "unconnected-(D2-DOUT)" ) );
1126 const int netD3Dout = addNet( wxT( "unconnected-(D3-DOUT)" ) );
1127 const int netD5Dout = addNet( wxT( "unconnected-(D5-DOUT)" ) );
1128 const int netD6Dout = addNet( wxT( "unconnected-(D6-DOUT)" ) );
1129
1130 LIB_ID ledId( wxT( "TestLib" ), wxT( "WS2812" ) );
1131
1132 // Four-pad addressable LED: pad 1 = DOUT, pad 2 = GND, pad 3 = DIN, pad 4 = VCC.
1133 auto makeLed =
1134 [&]( const wxString& aRef, int aDout, int aDin ) -> FOOTPRINT*
1135 {
1136 FOOTPRINT* fp = new FOOTPRINT( board.get() );
1137 fp->SetFPID( ledId );
1138 fp->SetReference( aRef );
1139 board->Add( fp );
1140
1141 auto addPad =
1142 [&]( const wxString& aNumber, int aNetCode )
1143 {
1144 PAD* pad = new PAD( fp );
1145 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1146 pad->SetNumber( aNumber );
1148 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ),
1149 pcbIUScale.mmToIU( 1 ) ) );
1150 pad->SetLayerSet( LSET( { F_Cu } ) );
1151 pad->SetNetCode( aNetCode );
1152 fp->Add( pad );
1153 };
1154
1155 addPad( wxT( "1" ), aDout );
1156 addPad( wxT( "2" ), netGnd );
1157 addPad( wxT( "3" ), aDin );
1158 addPad( wxT( "4" ), netVcc );
1159
1160 return fp;
1161 };
1162
1163 // Reference area (design block source instance): three LEDs share the DIN rail; only the
1164 // representative LED drives the chain output (the bridge net), the other two are unconnected.
1165 std::set<FOOTPRINT*> refFps;
1166 refFps.insert( makeLed( wxT( "D1" ), netBridge, netChainInRef ) );
1167 refFps.insert( makeLed( wxT( "D2" ), netD2Dout, netChainInRef ) );
1168 refFps.insert( makeLed( wxT( "D3" ), netD3Dout, netChainInRef ) );
1169
1170 // Target area (downstream instance): the three LEDs' DIN rail IS the bridge net coming from
1171 // the design block source, and the representative LED drives a fresh chain output.
1172 std::set<FOOTPRINT*> tgtFps;
1173 tgtFps.insert( makeLed( wxT( "D4" ), netChainOutTgt, netBridge ) );
1174 tgtFps.insert( makeLed( wxT( "D5" ), netD5Dout, netBridge ) );
1175 tgtFps.insert( makeLed( wxT( "D6" ), netD6Dout, netBridge ) );
1176
1177 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
1178 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
1179
1181 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1182 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1183
1184 if( !status )
1185 {
1186 for( const auto& reason : details )
1187 {
1188 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
1189 reason.m_reference, reason.m_candidate,
1190 reason.m_reason ) );
1191 }
1192 }
1193
1194 BOOST_CHECK_MESSAGE( status,
1195 "Topology match failed because the design-block boundary signal net "
1196 "was misclassified as a global rail and excluded asymmetrically" );
1197 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
1198}
1199
1200
1206BOOST_FIXTURE_TEST_CASE( TopoMatchReportsRefootprintedPart, MULTICHANNEL_TEST_FIXTURE )
1207{
1209
1210 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
1211
1212 auto addNet = [&]( const wxString& aName ) -> int
1213 {
1214 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
1215 board->Add( net );
1216 return net->GetNetCode();
1217 };
1218
1219 const int netGnd = addNet( wxT( "GND" ) );
1220 const int netVcc = addNet( wxT( "+3V3" ) );
1221
1222 const LIB_ID cap0201( wxT( "Capacitor_SMD" ), wxT( "C_0201_0603Metric" ) );
1223 const LIB_ID cap0402( wxT( "Capacitor_SMD" ), wxT( "C_0402_1005Metric" ) );
1224
1225 auto makeCap = [&]( const wxString& aRef, const LIB_ID& aFpId ) -> FOOTPRINT*
1226 {
1227 FOOTPRINT* fp = new FOOTPRINT( board.get() );
1228 fp->SetFPID( aFpId );
1229 fp->SetReference( aRef );
1230 board->Add( fp );
1231
1232 for( const wxString& padNumber : { wxString( wxT( "1" ) ), wxString( wxT( "2" ) ) } )
1233 {
1234 PAD* pad = new PAD( fp );
1235 pad->SetNumber( padNumber );
1237 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 0.3 ), pcbIUScale.mmToIU( 0.3 ) ) );
1238 pad->SetLayerSet( LSET( { F_Cu } ) );
1239 pad->SetNetCode( padNumber == wxT( "1" ) ? netVcc : netGnd );
1240 fp->Add( pad );
1241 }
1242
1243 return fp;
1244 };
1245
1246 // Identical decoupling capacitors give the search more arrangements than it is allowed to
1247 // try. That is what turned this into a timeout.
1248 std::set<FOOTPRINT*> blockFps;
1249 std::set<FOOTPRINT*> groupFps;
1250
1251 for( int i = 0; i < 8; i++ )
1252 {
1253 blockFps.insert( makeCap( wxString::Format( wxT( "C%d" ), 600 + i ), cap0201 ) );
1254 groupFps.insert( makeCap( wxString::Format( wxT( "C%d" ), 600 + i ), cap0201 ) );
1255 }
1256
1257 blockFps.insert( makeCap( wxT( "C619" ), cap0201 ) );
1258 groupFps.insert( makeCap( wxT( "C619" ), cap0402 ) );
1259
1260 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( blockFps, groupFps );
1261 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( groupFps, blockFps );
1262
1264 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1265
1266 BOOST_CHECK( !cgRef->FindIsomorphism( cgTarget.get(), result, details ) );
1267 BOOST_REQUIRE( !details.empty() );
1268
1269 for( const auto& reason : details )
1270 {
1271 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s", reason.m_reference, reason.m_candidate,
1272 reason.m_reason ) );
1273 }
1274
1275 BOOST_CHECK_EQUAL( details.front().m_reference, wxString( wxT( "C619" ) ) );
1276 BOOST_CHECK_EQUAL( details.front().m_candidate, wxString( wxT( "C619" ) ) );
1277}
1278
1279
1289BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutCopiesSilkscreen, MULTICHANNEL_TEST_FIXTURE )
1290{
1291 m_board = std::make_unique<BOARD>();
1292 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1293
1294 // Net for the single connection between the two footprints in each block.
1295 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1296 m_board->Add( net );
1297
1298 auto makeFootprint =
1299 [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1300 {
1301 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1302 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
1303 fp->SetReference( aRef );
1304 fp->SetPosition( aPos );
1305
1306 PAD* pad = new PAD( fp );
1307 pad->SetNumber( wxT( "1" ) );
1308 pad->SetNet( net );
1309 pad->SetPosition( aPos );
1310 pad->SetSize( F_Cu,
1311 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1312 pad->SetLayerSet( LSET( { F_Cu } ) );
1313 fp->Add( pad );
1314
1315 m_board->Add( fp );
1316 return fp;
1317 };
1318
1319 // Source: a "design block" with two matched footprints and a silkscreen rectangle that
1320 // is not associated with any footprint.
1321 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ),
1322 VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
1323 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ),
1324 VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ) );
1325
1326 PCB_SHAPE* silkRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1327 silkRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1328 silkRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 12 ), pcbIUScale.mmToIU( 2 ) ) );
1329 silkRect->SetLayer( F_SilkS );
1330 silkRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1331 m_board->Add( silkRect );
1332
1333 // Destination: a group containing the matched pair so RepeatLayout can find a parent group.
1334 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ),
1335 VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1336 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ),
1337 VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1338
1339 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1340 destGroup->SetName( wxT( "design-block-dest" ) );
1341 destGroup->AddItem( destFp1 );
1342 destGroup->AddItem( destFp2 );
1343 m_board->Add( destGroup );
1344
1345 // Unrelated silkscreen drawing inside the destination bounding box but not part of the
1346 // destination group. Apply Design Block Layout must not delete or claim this item.
1347 PCB_SHAPE* unrelatedSilk = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1348 unrelatedSilk->SetStart( VECTOR2I( pcbIUScale.mmToIU( 52 ), pcbIUScale.mmToIU( 52 ) ) );
1349 unrelatedSilk->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 58 ), pcbIUScale.mmToIU( 52 ) ) );
1350 unrelatedSilk->SetLayer( F_SilkS );
1351 unrelatedSilk->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1352 m_board->Add( unrelatedSilk );
1353
1354 int silkBefore = 0;
1355
1356 for( BOARD_ITEM* item : m_board->Drawings() )
1357 {
1358 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1359 silkBefore++;
1360 }
1361
1362 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1363
1364 RULE_AREA dbRA;
1366 dbRA.m_components.insert( refFp1 );
1367 dbRA.m_components.insert( refFp2 );
1368 dbRA.m_designBlockItems.insert( refFp1 );
1369 dbRA.m_designBlockItems.insert( refFp2 );
1370 dbRA.m_designBlockItems.insert( silkRect );
1371
1372 // The Apply Design Block flow uses a synthetic copper-only rule area zone. The destination
1373 // zone is a temporary zone never added to the board.
1374 dbRA.m_zone = new ZONE( m_board.get() );
1375 dbRA.m_zone->SetIsRuleArea( true );
1378 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1379 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1380
1381 RULE_AREA destRA;
1383 destRA.m_components.insert( destFp1 );
1384 destRA.m_components.insert( destFp2 );
1385
1386 destRA.m_zone = new ZONE( m_board.get() );
1387 destRA.m_zone->SetIsRuleArea( true );
1388 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1390 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1391 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1392
1393 TOOL_MANAGER toolMgr;
1394 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1395 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1396
1398 toolMgr.RegisterTool( mtTool );
1399
1400 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1401 .m_connectedRoutingOnly = false,
1402 .m_copyPlacement = true,
1403 .m_copyOtherItems = true,
1404 .m_groupItems = false,
1405 .m_includeLockedItems = true };
1406
1407 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1408 BOOST_CHECK_MESSAGE( result >= 0, "RepeatLayout failed" );
1409
1410 delete dbRA.m_zone;
1411 delete destRA.m_zone;
1412
1413 // Verify a third silkscreen shape (the duplicated rectangle) was added and grouped under
1414 // the destination group, that the unrelated silk segment was preserved, and that the
1415 // duplicated rectangle landed inside the destination region.
1416 int silkAfter = 0;
1417 int silkInGroup = 0;
1418 bool unrelatedSurvived = false;
1419 PCB_SHAPE* copiedRect = nullptr;
1420
1421 for( BOARD_ITEM* item : m_board->Drawings() )
1422 {
1423 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1424 continue;
1425
1426 silkAfter++;
1427
1428 if( item == unrelatedSilk )
1429 unrelatedSurvived = true;
1430
1431 if( item->GetParentGroup() == destGroup )
1432 {
1433 silkInGroup++;
1434
1435 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1436
1437 if( shape->GetShape() == SHAPE_T::RECTANGLE )
1438 copiedRect = shape;
1439 }
1440 }
1441
1442 BOOST_CHECK_MESSAGE( silkAfter == 3,
1443 wxString::Format( "Expected 3 silkscreen shapes after Apply Design Block "
1444 "Layout (2 original + 1 copy), found %d (issue 24372)",
1445 silkAfter ) );
1446 BOOST_CHECK_MESSAGE( silkInGroup == 1,
1447 wxString::Format( "Expected 1 silkscreen shape in destination group, "
1448 "found %d (issue 24372)",
1449 silkInGroup ) );
1450 BOOST_CHECK_MESSAGE( unrelatedSurvived,
1451 "Unrelated silkscreen drawing outside the design block group was "
1452 "deleted by Apply Design Block Layout (issue 24372)" );
1453 BOOST_REQUIRE( copiedRect != nullptr );
1454
1455 // The copied rectangle should sit near the destination footprints (offset by ~50mm from
1456 // the source position), not at the original source location.
1457 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1458}
1459
1460
1466BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutRotatesFieldsOnce, MULTICHANNEL_TEST_FIXTURE )
1467{
1468 m_board = std::make_unique<BOARD>();
1469 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1470
1471 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1472 m_board->Add( net );
1473
1474 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1475 {
1476 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1477 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
1478 fp->SetReference( aRef );
1479 fp->SetPosition( aPos );
1480
1481 PAD* pad = new PAD( fp );
1482 pad->SetNumber( wxT( "1" ) );
1483 pad->SetNet( net );
1484 pad->SetPosition( aPos );
1485 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1486 pad->SetLayerSet( LSET( { F_Cu } ) );
1487 fp->Add( pad );
1488
1489 m_board->Add( fp );
1490 return fp;
1491 };
1492
1493 const EDA_ANGLE sourceFieldAngle( 10.0, DEGREES_T );
1494 const EDA_ANGLE blockRotation( 45.0, DEGREES_T );
1495
1496 // Source design block: two matched footprints at orientation 0 with their Value field tilted.
1497 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ), VECTOR2I( pcbIUScale.mmToIU( 0 ), 0 ) );
1498 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1499
1500 for( FOOTPRINT* fp : { refFp1, refFp2 } )
1501 fp->GetField( FIELD_T::VALUE )->SetTextAngle( sourceFieldAngle );
1502
1503 // Destination: matched pair placed at 45 deg so the block is applied with a 45 deg rotation.
1504 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1505 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1506
1507 for( FOOTPRINT* fp : { destFp1, destFp2 } )
1508 fp->SetOrientation( blockRotation );
1509
1510 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1511 destGroup->SetName( wxT( "design-block-dest" ) );
1512 destGroup->AddItem( destFp1 );
1513 destGroup->AddItem( destFp2 );
1514 m_board->Add( destGroup );
1515
1516 RULE_AREA dbRA;
1518 dbRA.m_components.insert( refFp1 );
1519 dbRA.m_components.insert( refFp2 );
1520 dbRA.m_designBlockItems.insert( refFp1 );
1521 dbRA.m_designBlockItems.insert( refFp2 );
1522
1523 dbRA.m_zone = new ZONE( m_board.get() );
1524 dbRA.m_zone->SetIsRuleArea( true );
1526 dbRA.m_zone->AddPolygon(
1528 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1529
1530 RULE_AREA destRA;
1532 destRA.m_components.insert( destFp1 );
1533 destRA.m_components.insert( destFp2 );
1534
1535 destRA.m_zone = new ZONE( m_board.get() );
1536 destRA.m_zone->SetIsRuleArea( true );
1537 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1539 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1540 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1541
1542 TOOL_MANAGER toolMgr;
1543 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1544 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1545
1547 toolMgr.RegisterTool( mtTool );
1548
1549 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1550 .m_connectedRoutingOnly = false,
1551 .m_copyPlacement = true,
1552 .m_copyOtherItems = true,
1553 .m_groupItems = false,
1554 .m_includeLockedItems = true };
1555
1556 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1557 BOOST_REQUIRE_MESSAGE( result >= 0, "RepeatLayout failed" );
1558
1559 delete dbRA.m_zone;
1560 delete destRA.m_zone;
1561
1562 // Each placed field must be rotated by the block rotation exactly once: 10 deg + 45 deg = 55 deg.
1563 // The double-rotation regression produced 90 deg.
1564 const double expected = ( sourceFieldAngle + blockRotation ).AsDegrees();
1565
1566 for( FOOTPRINT* fp : { destFp1, destFp2 } )
1567 {
1568 PCB_FIELD* valueField = fp->GetField( FIELD_T::VALUE );
1569 BOOST_REQUIRE( valueField != nullptr );
1570
1571 double actual = valueField->GetTextAngle().Normalize().AsDegrees();
1572
1573 BOOST_CHECK_MESSAGE( std::abs( actual - expected ) < 1e-3,
1574 wxString::Format( "Field on %s rotated to %.3f deg, expected %.3f deg "
1575 "(field double-rotation regression)",
1576 fp->GetReference(), actual, expected ) );
1577 }
1578}
1579
1580
1586BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutMatchesBySymbolPathWhenTopologyDiffers, MULTICHANNEL_TEST_FIXTURE )
1587{
1589
1590 // Nested is a sheet reused twice inside a repeated sheet, where both instances share the symbol UUID
1591 for( bool nested : { false, true } )
1592 {
1593 BOOST_TEST_CONTEXT( ( nested ? "nested sheet paths" : "flat symbol paths" ) )
1594 {
1595 m_board = std::make_unique<BOARD>();
1596 m_board->SetEnabledLayers( LSET::AllCuMask() );
1597
1598 NETINFO_ITEM* shared = new NETINFO_ITEM( m_board.get(), wxT( "Net-(J1-Pin_1)" ) );
1599 m_board->Add( shared );
1600
1601 auto makeFootprint =
1602 [&]( const wxString& aRef, const VECTOR2I& aPos, const KIID_PATH& aPath,
1603 NETINFO_ITEM* aNet ) -> FOOTPRINT*
1604 {
1605 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1606 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "Receptacle" ) ) );
1607 fp->SetReference( aRef );
1608 fp->SetPosition( aPos );
1609
1610 // Symbol instance path, the link between a block footprint and its placed instance
1611 fp->SetPath( aPath );
1612
1613 PAD* pad = new PAD( fp );
1614 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1615 pad->SetNumber( wxT( "1" ) );
1617 pad->SetPosition( aPos );
1618 pad->SetSize( PADSTACK::ALL_LAYERS,
1619 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1620 pad->SetLayerSet( LSET( { F_Cu } ) );
1621
1622 if( aNet )
1623 pad->SetNet( aNet );
1624
1625 fp->Add( pad );
1626 m_board->Add( fp );
1627 return fp;
1628 };
1629
1630 const KIID refSheet, destSheet, innerA, innerB, symA;
1631 const KIID symB = nested ? symA : KIID();
1632
1633 auto pathOf =
1634 [&]( const KIID& aSheet, const KIID& aInner, const KIID& aSymbol )
1635 {
1636 if( nested )
1637 {
1638 return KIID_PATH( wxString::Format( "/%s/%s/%s", aSheet.AsString(),
1639 aInner.AsString(), aSymbol.AsString() ) );
1640 }
1641
1642 return KIID_PATH( wxT( "/" ) + aSymbol.AsString() );
1643 };
1644
1645 // Block source: both receptacles unconnected, so each pad has zero connections
1646 FOOTPRINT* refFpA =
1647 makeFootprint( wxT( "J5" ), VECTOR2I( 0, 0 ), pathOf( refSheet, innerA, symA ), nullptr );
1648 FOOTPRINT* refFpB = makeFootprint( wxT( "J6" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ),
1649 pathOf( refSheet, innerB, symB ), nullptr );
1650
1651 // Board instance: same symbol instances, but a board wire ties both pads onto one net
1652 FOOTPRINT* destFpA =
1653 makeFootprint( wxT( "J1" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ),
1654 pathOf( destSheet, innerA, symA ), shared );
1655 FOOTPRINT* destFpB =
1656 makeFootprint( wxT( "J2" ), VECTOR2I( pcbIUScale.mmToIU( 80 ), pcbIUScale.mmToIU( 80 ) ),
1657 pathOf( destSheet, innerB, symB ), shared );
1658
1659 // Precondition: topology matching fails, so the symbol path fallback is what rescues the apply
1660 {
1661 std::set<FOOTPRINT*> refSet{ refFpA, refFpB };
1662 std::set<FOOTPRINT*> destSet{ destFpA, destFpB };
1663 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refSet, destSet );
1664 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( destSet, refSet );
1666 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1667 BOOST_REQUIRE_MESSAGE( !cgRef->FindIsomorphism( cgTarget.get(), topoOnly, details ),
1668 "Test precondition broken: areas are net isomorphic" );
1669 }
1670
1671 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1672 destGroup->SetName( wxT( "design-block-dest" ) );
1673 destGroup->AddItem( destFpA );
1674 destGroup->AddItem( destFpB );
1675 m_board->Add( destGroup );
1676
1677 RULE_AREA dbRA;
1679 dbRA.m_components.insert( refFpA );
1680 dbRA.m_components.insert( refFpB );
1681 dbRA.m_designBlockItems.insert( refFpA );
1682 dbRA.m_designBlockItems.insert( refFpB );
1683 dbRA.m_zone = new ZONE( m_board.get() );
1684 dbRA.m_zone->SetIsRuleArea( true );
1686 dbRA.m_zone->AddPolygon(
1688 VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1689 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1690
1691 RULE_AREA destRA;
1693 destRA.m_components.insert( destFpA );
1694 destRA.m_components.insert( destFpB );
1695 destRA.m_group = destGroup;
1696 destRA.m_zone = new ZONE( m_board.get() );
1697 destRA.m_zone->SetIsRuleArea( true );
1698 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1700 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 40 ), pcbIUScale.mmToIU( 40 ) ),
1701 VECTOR2I( pcbIUScale.mmToIU( 90 ), pcbIUScale.mmToIU( 90 ) ) ) ) );
1702
1703 TOOL_MANAGER toolMgr;
1704 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1705 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1707 toolMgr.RegisterTool( mtTool );
1708
1709 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1710 .m_connectedRoutingOnly = false,
1711 .m_copyPlacement = true,
1712 .m_copyOtherItems = true,
1713 .m_groupItems = false,
1714 .m_includeLockedItems = true };
1715
1716 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1717
1718 delete dbRA.m_zone;
1719 delete destRA.m_zone;
1720
1721 BOOST_CHECK_MESSAGE( result >= 0, "Apply Design Block Layout aborted even though the symbol instance paths "
1722 "give an unambiguous mapping (No compatible component regression)" );
1723
1724 // Correct pairing (J5->J1, J6->J2) reproduces the source 10mm spacing, a swap would give -10mm
1725 VECTOR2I delta = destFpB->GetPosition() - destFpA->GetPosition();
1726 BOOST_CHECK_MESSAGE( delta == VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ),
1727 wxString::Format( "Destination relative placement wrong, expected "
1728 "(10mm,0) got (%d,%d) IU",
1729 delta.x, delta.y ) );
1730 }
1731 }
1732}
1733
1734
1743BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsDuplicateSilkText, MULTICHANNEL_TEST_FIXTURE )
1744{
1745 m_board = std::make_unique<BOARD>();
1746 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1747
1748 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1749 m_board->Add( net );
1750
1751 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1752 {
1753 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1754 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "SW" ) ) );
1755 fp->SetReference( aRef );
1756 fp->SetPosition( aPos );
1757
1758 // Two silkscreen texts with the same content, one on each board side.
1759 for( PCB_LAYER_ID layer : { F_SilkS, B_SilkS } )
1760 {
1761 PCB_TEXT* txt = new PCB_TEXT( fp );
1762 txt->SetText( wxT( "${REFERENCE}" ) );
1763 txt->SetLayer( layer );
1764 txt->SetPosition( aPos + VECTOR2I( 0, pcbIUScale.mmToIU( -8 ) ) );
1765 fp->Add( txt );
1766 }
1767
1768 PAD* pad = new PAD( fp );
1769 pad->SetNumber( wxT( "1" ) );
1770 pad->SetNet( net );
1771 pad->SetPosition( aPos );
1772 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1773 pad->SetLayerSet( LSET( { F_Cu } ) );
1774 fp->Add( pad );
1775
1776 m_board->Add( fp );
1777 return fp;
1778 };
1779
1780 auto countSilkText = [&]( FOOTPRINT* fp, PCB_LAYER_ID layer ) -> int
1781 {
1782 int count = 0;
1783
1784 for( BOARD_ITEM* item : fp->GraphicalItems() )
1785 {
1786 if( item->Type() == PCB_TEXT_T && item->GetLayer() == layer )
1787 count++;
1788 }
1789
1790 return count;
1791 };
1792
1793 FOOTPRINT* refFp1 = makeFootprint( wxT( "SW1" ), VECTOR2I( 0, 0 ) );
1794 FOOTPRINT* refFp2 = makeFootprint( wxT( "SW2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1795
1796 FOOTPRINT* destFp1 = makeFootprint( wxT( "SW3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1797 FOOTPRINT* destFp2 = makeFootprint( wxT( "SW4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1798
1799 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1800 destGroup->SetName( wxT( "design-block-dest" ) );
1801 destGroup->AddItem( destFp1 );
1802 destGroup->AddItem( destFp2 );
1803 m_board->Add( destGroup );
1804
1805 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, F_SilkS ), 1 );
1806 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, B_SilkS ), 1 );
1807
1808 RULE_AREA dbRA;
1810 dbRA.m_components.insert( refFp1 );
1811 dbRA.m_components.insert( refFp2 );
1812 dbRA.m_designBlockItems.insert( refFp1 );
1813 dbRA.m_designBlockItems.insert( refFp2 );
1814
1815 dbRA.m_zone = new ZONE( m_board.get() );
1816 dbRA.m_zone->SetIsRuleArea( true );
1818 dbRA.m_zone->AddPolygon(
1820 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1821
1822 RULE_AREA destRA;
1824 destRA.m_components.insert( destFp1 );
1825 destRA.m_components.insert( destFp2 );
1826
1827 destRA.m_zone = new ZONE( m_board.get() );
1828 destRA.m_zone->SetIsRuleArea( true );
1829 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1831 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1832 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1833
1834 TOOL_MANAGER toolMgr;
1835 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1836 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1837
1839 toolMgr.RegisterTool( mtTool );
1840
1841 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1842 .m_connectedRoutingOnly = false,
1843 .m_copyPlacement = true,
1844 .m_copyOtherItems = true,
1845 .m_groupItems = false,
1846 .m_includeLockedItems = true };
1847
1848 wxString err;
1849 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1850 BOOST_CHECK_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1851
1852 delete dbRA.m_zone;
1853 delete destRA.m_zone;
1854
1855 for( FOOTPRINT* destFp : { destFp1, destFp2 } )
1856 {
1857 BOOST_CHECK_MESSAGE( countSilkText( destFp, F_SilkS ) == 1,
1858 wxString::Format( "%s lost its F.Silkscreen reference text "
1859 "(issue 24583): F=%d B=%d",
1860 destFp->GetReference(), countSilkText( destFp, F_SilkS ),
1861 countSilkText( destFp, B_SilkS ) ) );
1862 BOOST_CHECK_EQUAL( countSilkText( destFp, B_SilkS ), 1 );
1863 }
1864}
1865
1866
1871BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutGraphicsOnlyBlock, MULTICHANNEL_TEST_FIXTURE )
1872{
1873 m_board = std::make_unique<BOARD>();
1874 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1875
1876 auto makeSilkRect = [&]( const VECTOR2I& aStart, const VECTOR2I& aEnd ) -> PCB_SHAPE*
1877 {
1878 PCB_SHAPE* s = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1879 s->SetStart( aStart );
1880 s->SetEnd( aEnd );
1881 s->SetLayer( F_SilkS );
1882 s->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1883 m_board->Add( s );
1884 return s;
1885 };
1886
1887 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1888 PCB_SHAPE* srcRect = makeSilkRect( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ),
1889 VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1890
1891 // Destination: footprint-free group with one silkscreen item so the group exists.
1892 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1893 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1894 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1895 destPlaceholder->SetLayer( F_SilkS );
1896 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1897 m_board->Add( destPlaceholder );
1898
1899 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1900 destGroup->SetName( wxT( "design-block-dest" ) );
1901 destGroup->AddItem( destPlaceholder );
1902 m_board->Add( destGroup );
1903
1904 int silkBefore = 0;
1905
1906 for( BOARD_ITEM* item : m_board->Drawings() )
1907 {
1908 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1909 silkBefore++;
1910 }
1911
1912 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1913
1914 RULE_AREA dbRA;
1916 dbRA.m_designBlockItems.insert( srcRect ); // no footprints -> m_components stays empty
1917
1918 dbRA.m_zone = new ZONE( m_board.get() );
1919 dbRA.m_zone->SetIsRuleArea( true );
1921 dbRA.m_zone->AddPolygon(
1923 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1924 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1925
1926 RULE_AREA destRA;
1928 destRA.m_group = destGroup; // set explicitly: no footprint to recover it from
1929
1930 destRA.m_zone = new ZONE( m_board.get() );
1931 destRA.m_zone->SetIsRuleArea( true );
1932 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1934 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1935 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1936 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1937
1938 TOOL_MANAGER toolMgr;
1939 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1940 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1941
1943 toolMgr.RegisterTool( mtTool );
1944
1945 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1946 .m_connectedRoutingOnly = false,
1947 .m_copyPlacement = true,
1948 .m_copyOtherItems = true,
1949 .m_groupItems = false,
1950 .m_includeLockedItems = true };
1951
1952 wxString err;
1953 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1954
1955 delete dbRA.m_zone;
1956 delete destRA.m_zone;
1957
1958 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free "
1959 "(graphics-only) design block (issue 24592): %s",
1960 err ) );
1961
1962 // The block's rectangle is copied into the target region and grouped, replacing the group's
1963 // prior placeholder graphic (source + 1 copy = 2 silkscreen shapes).
1964 int silkAfter = 0;
1965 PCB_SHAPE* copiedRect = nullptr;
1966
1967 for( BOARD_ITEM* item : m_board->Drawings() )
1968 {
1969 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1970 continue;
1971
1972 silkAfter++;
1973
1974 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1975
1976 if( shape != srcRect && shape->GetShape() == SHAPE_T::RECTANGLE )
1977 copiedRect = shape;
1978 }
1979
1980 BOOST_CHECK_MESSAGE( silkAfter == 2, wxString::Format( "Expected 2 silkscreen shapes after Apply Design Block "
1981 "Layout (source + 1 copy, placeholder replaced), found "
1982 "%d (issue 24592)",
1983 silkAfter ) );
1984 BOOST_REQUIRE_MESSAGE( copiedRect != nullptr, "Footprint-free block graphic was not copied (issue 24592)" );
1985 BOOST_CHECK_MESSAGE( copiedRect->GetParentGroup() == destGroup,
1986 "Copied block graphic was not added to the destination group (issue 24592)" );
1987 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1988}
1989
1990
1995BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeCopperIsNoNet, MULTICHANNEL_TEST_FIXTURE )
1996{
1997 m_board = std::make_unique<BOARD>();
1998 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1999
2000 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
2001 m_board->Add( net );
2002
2003 // Source: footprint-free block with one track on a real net.
2004 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
2005 srcTrack->SetLayer( F_Cu );
2006 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2007 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( 0 ) ) );
2008 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 0 ) ) );
2009 srcTrack->SetNet( net );
2010 m_board->Add( srcTrack );
2011
2012 BOOST_REQUIRE_EQUAL( srcTrack->GetNetCode(), 1 );
2013
2014 // Destination: footprint-free group with a placeholder so the group exists.
2015 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2016 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2017 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2018 destPlaceholder->SetLayer( F_SilkS );
2019 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2020 m_board->Add( destPlaceholder );
2021
2022 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2023 destGroup->SetName( wxT( "design-block-dest" ) );
2024 destGroup->AddItem( destPlaceholder );
2025 m_board->Add( destGroup );
2026
2027 RULE_AREA dbRA;
2029 dbRA.m_designBlockItems.insert( srcTrack ); // no footprints -> m_components stays empty
2030
2031 dbRA.m_zone = new ZONE( m_board.get() );
2032 dbRA.m_zone->SetIsRuleArea( true );
2034 dbRA.m_zone->AddPolygon(
2036 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2037 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2038
2039 RULE_AREA destRA;
2041 destRA.m_group = destGroup;
2042
2043 destRA.m_zone = new ZONE( m_board.get() );
2044 destRA.m_zone->SetIsRuleArea( true );
2045 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2047 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2048 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2049 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2050
2051 TOOL_MANAGER toolMgr;
2052 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2053 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2054
2056 toolMgr.RegisterTool( mtTool );
2057
2058 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2059 .m_connectedRoutingOnly = false,
2060 .m_copyPlacement = true,
2061 .m_copyOtherItems = true,
2062 .m_groupItems = false,
2063 .m_includeLockedItems = true };
2064
2065 wxString err;
2066 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
2067
2068 delete dbRA.m_zone;
2069 delete destRA.m_zone;
2070
2071 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free copper "
2072 "block (issue 24592): %s",
2073 err ) );
2074
2075 // Copied track should be in the target region, no-net, and grouped.
2076 PCB_TRACK* copiedTrack = nullptr;
2077
2078 for( PCB_TRACK* track : m_board->Tracks() )
2079 {
2080 if( track != srcTrack )
2081 copiedTrack = track;
2082 }
2083
2084 BOOST_REQUIRE_MESSAGE( copiedTrack != nullptr, "Footprint-free block track was not copied (issue 24592)" );
2085 BOOST_CHECK_MESSAGE( copiedTrack->GetNetCode() == 0,
2086 wxString::Format( "Copied copper should be no-net, got net code %d "
2087 "(issue 24592)",
2088 copiedTrack->GetNetCode() ) );
2089 BOOST_CHECK_MESSAGE( copiedTrack->GetParentGroup() == destGroup,
2090 "Copied copper was not added to the destination group (issue 24592)" );
2091 BOOST_CHECK_GT( copiedTrack->GetStart().x, pcbIUScale.mmToIU( 30 ) );
2092
2093 // The original source track must keep its net.
2094 BOOST_CHECK_EQUAL( srcTrack->GetNetCode(), 1 );
2095}
2096
2097
2103BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeReapplyReplaces, MULTICHANNEL_TEST_FIXTURE )
2104{
2105 m_board = std::make_unique<BOARD>();
2106 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2107
2108 // Source: footprint-free block with one silkscreen rectangle centered on origin.
2109 PCB_SHAPE* srcRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
2110 srcRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
2111 srcRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
2112 srcRect->SetLayer( F_SilkS );
2113 srcRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2114 m_board->Add( srcRect );
2115
2116 // Destination: footprint-free group with a placeholder so the group exists.
2117 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2118 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2119 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2120 destPlaceholder->SetLayer( F_SilkS );
2121 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2122 m_board->Add( destPlaceholder );
2123
2124 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2125 destGroup->SetName( wxT( "design-block-dest" ) );
2126 destGroup->AddItem( destPlaceholder );
2127 m_board->Add( destGroup );
2128
2129 TOOL_MANAGER toolMgr;
2130 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2131 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2132
2134 toolMgr.RegisterTool( mtTool );
2135
2136 auto applyOnce = [&]() -> int
2137 {
2138 RULE_AREA dbRA;
2140 dbRA.m_designBlockItems.insert( srcRect );
2141
2142 dbRA.m_zone = new ZONE( m_board.get() );
2143 dbRA.m_zone->SetIsRuleArea( true );
2146 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
2147 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2148 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2149
2150 RULE_AREA destRA;
2152 destRA.m_group = destGroup;
2153
2154 destRA.m_zone = new ZONE( m_board.get() );
2155 destRA.m_zone->SetIsRuleArea( true );
2156 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2158 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2159 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2160 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2161
2162 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2163 .m_connectedRoutingOnly = false,
2164 .m_copyPlacement = true,
2165 .m_copyOtherItems = true,
2166 .m_groupItems = false,
2167 .m_includeLockedItems = true };
2168
2169 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
2170
2171 delete dbRA.m_zone;
2172 delete destRA.m_zone;
2173 return result;
2174 };
2175
2176 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "First apply failed (issue 24592)" );
2177 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "Second apply failed (issue 24592)" );
2178
2179 // After two applies there must be exactly the source rectangle plus one copy. A third
2180 // rectangle would mean the second apply stacked instead of replacing.
2181 int rects = 0;
2182
2183 for( BOARD_ITEM* item : m_board->Drawings() )
2184 {
2185 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS
2186 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::RECTANGLE )
2187 {
2188 rects++;
2189 }
2190 }
2191
2192 BOOST_CHECK_MESSAGE( rects == 2, wxString::Format( "Re-apply stacked copies: expected 2 rectangles "
2193 "(source + 1 copy), found %d (issue 24592)",
2194 rects ) );
2195}
2196
2197
2205BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsOtherGroupRouting, MULTICHANNEL_TEST_FIXTURE )
2206{
2207 m_board = std::make_unique<BOARD>();
2208 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2209
2210 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
2211 m_board->Add( net );
2212
2213 // Source block: one track centered on origin.
2214 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
2215 srcTrack->SetLayer( F_Cu );
2216 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2217 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), 0 ) );
2218 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), 0 ) );
2219 srcTrack->SetNet( net );
2220 m_board->Add( srcTrack );
2221
2222 // Destination group with a placeholder so the group exists.
2223 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2224 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2225 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2226 destPlaceholder->SetLayer( F_SilkS );
2227 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2228 m_board->Add( destPlaceholder );
2229
2230 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2231 destGroup->SetName( wxT( "design-block-dest" ) );
2232 destGroup->AddItem( destPlaceholder );
2233 m_board->Add( destGroup );
2234
2235 // A sibling instance's track, inside the destination area but owned by another group.
2236 PCB_TRACK* siblingTrack = new PCB_TRACK( m_board.get() );
2237 siblingTrack->SetLayer( F_Cu );
2238 siblingTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2239 siblingTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 47 ) ) );
2240 siblingTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 47 ) ) );
2241 m_board->Add( siblingTrack );
2242
2243 PCB_GROUP* siblingGroup = new PCB_GROUP( m_board.get() );
2244 siblingGroup->SetName( wxT( "design-block-sibling" ) );
2245 siblingGroup->AddItem( siblingTrack );
2246 m_board->Add( siblingGroup );
2247
2248 // Loose, ungrouped routing without a net inside the destination area, unrelated to the block.
2249 PCB_TRACK* looseTrack = new PCB_TRACK( m_board.get() );
2250 looseTrack->SetLayer( F_Cu );
2251 looseTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2252 looseTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 53 ) ) );
2253 looseTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 53 ) ) );
2254 m_board->Add( looseTrack );
2255
2256 RULE_AREA dbRA;
2258 dbRA.m_designBlockItems.insert( srcTrack );
2259
2260 dbRA.m_zone = new ZONE( m_board.get() );
2261 dbRA.m_zone->SetIsRuleArea( true );
2263 dbRA.m_zone->AddPolygon(
2265 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2266 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2267
2268 RULE_AREA destRA;
2270 destRA.m_group = destGroup;
2271
2272 destRA.m_zone = new ZONE( m_board.get() );
2273 destRA.m_zone->SetIsRuleArea( true );
2274 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2276 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2277 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2278 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2279
2280 TOOL_MANAGER toolMgr;
2281 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2282 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2283
2285 toolMgr.RegisterTool( mtTool );
2286
2287 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2288 .m_connectedRoutingOnly = false,
2289 .m_copyPlacement = true,
2290 .m_copyOtherItems = true,
2291 .m_groupItems = false,
2292 .m_includeLockedItems = true };
2293
2294 wxString err;
2295 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
2296
2297 delete dbRA.m_zone;
2298 delete destRA.m_zone;
2299
2300 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
2301
2302 // Pointers to removed tracks are deleted, so count survivors by location instead.
2303 int siblingTracks = 0;
2304 int looseTracks = 0;
2305
2306 for( PCB_TRACK* track : m_board->Tracks() )
2307 {
2308 int y = track->GetStart().y;
2309
2310 if( y > pcbIUScale.mmToIU( 46 ) && y < pcbIUScale.mmToIU( 48 ) )
2311 siblingTracks++;
2312 else if( y > pcbIUScale.mmToIU( 52 ) && y < pcbIUScale.mmToIU( 54 ) )
2313 looseTracks++;
2314 }
2315
2316 BOOST_CHECK_MESSAGE( siblingTracks == 1,
2317 wxString::Format( "Sibling group routing was deleted (issue 24767): found %d, expected 1",
2318 siblingTracks ) );
2319 BOOST_CHECK_MESSAGE( looseTracks == 1,
2320 wxString::Format( "Loose no-net routing in the target area was deleted (issue 24944): "
2321 "found %d, expected 1",
2322 looseTracks ) );
2323}
2324
2325
2331BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsUnrelatedRouting, MULTICHANNEL_TEST_FIXTURE )
2332{
2333 m_board = std::make_unique<BOARD>();
2334 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2335
2336 NETINFO_ITEM* net1 = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
2337 NETINFO_ITEM* net2 = new NETINFO_ITEM( m_board.get(), wxT( "NET2" ), 2 );
2338 m_board->Add( net1 );
2339 m_board->Add( net2 );
2340
2341 auto addTrack = [&]( double aStartX, double aStartY, double aEndX, double aEndY, NETINFO_ITEM* aNet )
2342 {
2343 PCB_TRACK* track = new PCB_TRACK( m_board.get() );
2344 track->SetLayer( F_Cu );
2345 track->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
2346 track->SetStart( VECTOR2I( pcbIUScale.mmToIU( aStartX ), pcbIUScale.mmToIU( aStartY ) ) );
2347 track->SetEnd( VECTOR2I( pcbIUScale.mmToIU( aEndX ), pcbIUScale.mmToIU( aEndY ) ) );
2348 track->SetNet( aNet );
2349 m_board->Add( track );
2350 return track;
2351 };
2352
2353 // Source block: one track centered on origin.
2354 PCB_TRACK* srcTrack = addTrack( -2, 0, 2, 0, net1 );
2355
2356 // Destination group with a placeholder and its own routing from a previous apply.
2357 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
2358 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
2359 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
2360 destPlaceholder->SetLayer( F_SilkS );
2361 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
2362 m_board->Add( destPlaceholder );
2363
2364 PCB_TRACK* prevTrack = addTrack( 46, 49, 48, 49, net1 );
2365
2366 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2367 destGroup->SetName( wxT( "design-block-dest" ) );
2368 destGroup->AddItem( destPlaceholder );
2369 destGroup->AddItem( prevTrack );
2370 m_board->Add( destGroup );
2371
2372 // Bystanders inside the destination area: a track on an unrelated net and a locked track.
2373 // Both must survive the apply (issue 24944).
2374 addTrack( 46, 47, 48, 47, net2 );
2375
2376 PCB_TRACK* lockedTrack = addTrack( 46, 51.5, 48, 51.5, net1 );
2377 lockedTrack->SetLocked( true );
2378
2379 // Loose routing on the group's own net, a stale user tweak, which still gets replaced.
2380 addTrack( 46, 53, 48, 53, net1 );
2381
2382 RULE_AREA dbRA;
2384 dbRA.m_designBlockItems.insert( srcTrack );
2385
2386 dbRA.m_zone = new ZONE( m_board.get() );
2387 dbRA.m_zone->SetIsRuleArea( true );
2389 dbRA.m_zone->AddPolygon(
2391 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
2392 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
2393
2394 RULE_AREA destRA;
2396 destRA.m_group = destGroup;
2397
2398 destRA.m_zone = new ZONE( m_board.get() );
2399 destRA.m_zone->SetIsRuleArea( true );
2400 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2402 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
2403 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
2404 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
2405
2406 TOOL_MANAGER toolMgr;
2407 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2408 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2409
2411 toolMgr.RegisterTool( mtTool );
2412
2413 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
2414 .m_connectedRoutingOnly = false,
2415 .m_copyPlacement = true,
2416 .m_copyOtherItems = true,
2417 .m_groupItems = false,
2418 .m_includeLockedItems = true };
2419
2420 wxString err;
2421 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
2422
2423 delete dbRA.m_zone;
2424 delete destRA.m_zone;
2425
2426 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
2427
2428 // Pointers to removed tracks are deleted, so count survivors by location instead.
2429 int unrelatedTracks = 0;
2430 int prevTracks = 0;
2431 int copiedTracks = 0;
2432 int lockedTracks = 0;
2433 int staleTweakTracks = 0;
2434
2435 for( PCB_TRACK* track : m_board->Tracks() )
2436 {
2437 int y = track->GetStart().y;
2438
2439 if( y > pcbIUScale.mmToIU( 46.5 ) && y < pcbIUScale.mmToIU( 47.5 ) )
2440 unrelatedTracks++;
2441 else if( y > pcbIUScale.mmToIU( 48.5 ) && y < pcbIUScale.mmToIU( 49.5 ) )
2442 prevTracks++;
2443 else if( y > pcbIUScale.mmToIU( 49.5 ) && y < pcbIUScale.mmToIU( 50.5 ) )
2444 copiedTracks++;
2445 else if( y > pcbIUScale.mmToIU( 51 ) && y < pcbIUScale.mmToIU( 52 ) )
2446 lockedTracks++;
2447 else if( y > pcbIUScale.mmToIU( 52.5 ) && y < pcbIUScale.mmToIU( 53.5 ) )
2448 staleTweakTracks++;
2449 }
2450
2451 BOOST_CHECK_MESSAGE( unrelatedTracks == 1,
2452 wxString::Format( "Unrelated net routing in the target area was deleted (issue 24944): "
2453 "found %d, expected 1",
2454 unrelatedTracks ) );
2455 BOOST_CHECK_MESSAGE( lockedTracks == 1,
2456 wxString::Format( "Locked bystander routing in the target area was deleted (issue 24944): "
2457 "found %d, expected 1",
2458 lockedTracks ) );
2459 BOOST_CHECK_MESSAGE(
2460 prevTracks == 0,
2461 wxString::Format( "The group's own routing should be replaced: found %d, expected 0", prevTracks ) );
2462 BOOST_CHECK_MESSAGE( staleTweakTracks == 0,
2463 wxString::Format( "Loose routing on the group's nets should be replaced: found %d, "
2464 "expected 0",
2465 staleTweakTracks ) );
2466 BOOST_CHECK_MESSAGE( copiedTracks == 1,
2467 wxString::Format( "Block routing was not copied into the target area: found %d, expected 1",
2468 copiedTracks ) );
2469}
2470
2471
2478BOOST_FIXTURE_TEST_CASE( RepeatLayoutRefusesDuplicatePlacementAreas, MULTICHANNEL_TEST_FIXTURE )
2479{
2480 KI_TEST::LoadBoard( m_settingsManager, "issue22318/issue22318", m_board );
2481
2482 TOOL_MANAGER toolMgr;
2483 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2484
2485 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2486
2488 toolMgr.RegisterTool( mtTool );
2489
2490 mtTool->FindExistingRuleAreas();
2491
2492 auto ruleData = mtTool->GetData();
2493
2494 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
2495 static_cast<int>( ruleData->m_areas.size() ) ) );
2496
2497 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
2498
2499 // The shared membership, not the exact count, is what makes the areas invalid targets.
2500 const size_t sharedCount = ruleData->m_areas.front().m_components.size();
2501
2502 BOOST_REQUIRE( sharedCount > 0 );
2503
2504 for( const RULE_AREA& ra : ruleData->m_areas )
2505 BOOST_CHECK_EQUAL( ra.m_components.size(), sharedCount );
2506
2507 RULE_AREA* refArea = &ruleData->m_areas.front();
2508
2509 BOOST_REQUIRE( mtTool->CheckRACompatibility( refArea->m_zone ) >= 0 );
2510
2511 BOOST_REQUIRE_EQUAL( ruleData->m_compatMap.size(), ruleData->m_areas.size() - 1 );
2512
2513 for( const auto& [targetArea, compatData] : ruleData->m_compatMap )
2514 {
2515 BOOST_CHECK_MESSAGE( !compatData.m_isOk,
2516 "Duplicate placement area was wrongly reported as a valid copy target "
2517 "(issue 22318)" );
2518 BOOST_CHECK( !compatData.m_mismatchReasons.empty() );
2519 }
2520
2521 // The single-target overload must also refuse rather than corrupt the board.
2522 RULE_AREA* targetArea = &ruleData->m_areas[1];
2523
2525 wxString err;
2526
2527 int result = mtTool->RepeatLayout( TOOL_EVENT(), *refArea, *targetArea, opts, nullptr, &err );
2528
2529 BOOST_CHECK_MESSAGE( result < 0,
2530 "RepeatLayout copied a Rule Area onto an identical-component area "
2531 "(issue 22318)" );
2532 BOOST_CHECK( !err.IsEmpty() );
2533}
2534
2535
2547BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotDuplicateUnrelatedGroups, MULTICHANNEL_TEST_FIXTURE )
2548{
2549 KI_TEST::LoadBoard( m_settingsManager, "issue22316/issue22316", m_board );
2550
2551 TOOL_MANAGER toolMgr;
2552 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2553
2554 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2555
2557 toolMgr.RegisterTool( mtTool );
2558
2559 mtTool->FindExistingRuleAreas();
2560
2561 auto ruleData = mtTool->GetData();
2562
2563 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
2564 static_cast<int>( ruleData->m_areas.size() ) ) );
2565
2566 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
2567
2568 RULE_AREA* refArea = nullptr;
2569
2570 for( RULE_AREA& ra : ruleData->m_areas )
2571 {
2572 if( ra.m_ruleName == wxT( "test 1" ) )
2573 refArea = &ra;
2574 }
2575
2576 BOOST_REQUIRE( refArea != nullptr );
2577
2578 std::set<KIID> groupUuidsBefore;
2579 std::set<wxString> userGroupNamesBefore;
2580
2581 for( PCB_GROUP* group : m_board->Groups() )
2582 {
2583 groupUuidsBefore.insert( group->m_Uuid );
2584
2585 if( !group->GetName().IsEmpty() )
2586 userGroupNamesBefore.insert( group->GetName() );
2587 }
2588
2589 mtTool->CheckRACompatibility( refArea->m_zone );
2590
2591 for( auto& [targetArea, compatData] : ruleData->m_compatMap )
2592 compatData.m_doCopy = true;
2593
2594 ruleData->m_options.m_copyPlacement = true;
2595 ruleData->m_options.m_copyRouting = true;
2596 ruleData->m_options.m_copyOtherItems = true;
2597 ruleData->m_options.m_groupItems = true;
2598 ruleData->m_options.m_includeLockedItems = true;
2599
2600 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
2601
2602 BOOST_REQUIRE( result >= 0 );
2603
2604 int clonedUserGroups = 0;
2605
2606 for( PCB_GROUP* group : m_board->Groups() )
2607 {
2608 bool isNew = !groupUuidsBefore.contains( group->m_Uuid );
2609
2610 if( isNew && userGroupNamesBefore.contains( group->GetName() ) )
2611 clonedUserGroups++;
2612 }
2613
2614 BOOST_CHECK_MESSAGE( clonedUserGroups == 0,
2615 wxString::Format( "Repeat layout cloned %d unrelated user groups "
2616 "(issue 22316)",
2617 clonedUserGroups ) );
2618}
2619
2620
2628BOOST_FIXTURE_TEST_CASE( TopoMatchExternalLoopReportsConnectivity, MULTICHANNEL_TEST_FIXTURE )
2629{
2631
2632 KI_TEST::LoadBoard( m_settingsManager, "issue24192/issue24192", m_board );
2633
2634 TOOL_MANAGER toolMgr;
2635 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2636 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2637
2639 toolMgr.RegisterTool( mtTool );
2640
2641 mtTool->FindExistingRuleAreas();
2642
2643 auto ruleData = mtTool->GetData();
2644
2645 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 2 );
2646
2647 RULE_AREA* refArea = nullptr;
2648 RULE_AREA* targetArea = nullptr;
2649
2650 for( RULE_AREA& ra : ruleData->m_areas )
2651 {
2652 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
2653 refArea = &ra;
2654 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
2655 targetArea = &ra;
2656 }
2657
2658 BOOST_REQUIRE( refArea != nullptr );
2659 BOOST_REQUIRE( targetArea != nullptr );
2660
2661 // The component counts are identical; only the internal connectivity differs.
2662 BOOST_CHECK_EQUAL( refArea->m_components.size(), targetArea->m_components.size() );
2663
2664 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
2665 targetArea->m_components );
2666 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
2667 refArea->m_components );
2668
2670 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2671 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2672
2673 // The topology genuinely differs, so matching must fail and produce a reason.
2674 BOOST_CHECK( !status );
2675 BOOST_REQUIRE( !details.empty() );
2676
2677 // Collect the reference designators that belong to each channel so we can verify the
2678 // reason's reference/candidate orientation matches the ref/target areas.
2679 std::set<wxString> refRefDes;
2680 std::set<wxString> targetRefDes;
2681
2682 for( FOOTPRINT* fp : refArea->m_components )
2683 refRefDes.insert( fp->GetReference() );
2684
2685 for( FOOTPRINT* fp : targetArea->m_components )
2686 targetRefDes.insert( fp->GetReference() );
2687
2688 bool sawConnectivityReason = false;
2689
2690 for( const auto& reason : details )
2691 {
2692 BOOST_TEST_MESSAGE( wxString::Format( "reason: %s <-> %s: %s", reason.m_reference,
2693 reason.m_candidate, reason.m_reason ) );
2694
2695 // The generic fallback message must no longer be the only thing reported; instead the
2696 // connectivity difference detected by the per-pad isomorphism check must surface.
2697 BOOST_CHECK( !reason.m_reason.Contains( wxT( "No compatible component found" ) ) );
2698
2699 if( reason.m_reason.Contains( wxT( "connects to" ) )
2700 || reason.m_reason.Contains( wxT( "connectivity" ) ) )
2701 {
2702 sawConnectivityReason = true;
2703
2704 // The reference designator in the reason must come from the reference channel and
2705 // the candidate from the target channel, not the other way around (codex-review).
2706 BOOST_CHECK_MESSAGE( refRefDes.count( reason.m_reference ) > 0,
2707 wxString::Format( "Reason reference '%s' should be a reference-area "
2708 "component", reason.m_reference ) );
2709 BOOST_CHECK_MESSAGE( targetRefDes.count( reason.m_candidate ) > 0,
2710 wxString::Format( "Reason candidate '%s' should be a target-area "
2711 "component", reason.m_candidate ) );
2712 }
2713 }
2714
2715 BOOST_CHECK_MESSAGE( sawConnectivityReason,
2716 "Topology mismatch message should explain the connectivity difference "
2717 "rather than report a generic missing-component error (issue 24192)" );
2718}
2719
2720
2728{
2730
2731 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2732
2733 auto addNet =
2734 [&]( const wxString& aName ) -> int
2735 {
2736 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
2737 board->Add( net );
2738 return net->GetNetCode();
2739 };
2740
2741 const int netGnd = addNet( wxT( "GND" ) );
2742 const int netP3V3 = addNet( wxT( "+3V3" ) );
2743 const int netCollide = addNet( wxT( "Net-(D3-A)" ) ); // shared name, lands on a different part
2744 const int netRefY = addNet( wxT( "Net-(D4-A)" ) ); // block other LED anode
2745 const int netTgtX = addNet( wxT( "Net-(D2-A)" ) ); // copy other LED anode
2746
2747 LIB_ID ledAId( wxT( "TestLib" ), wxT( "LED_A" ) );
2748 LIB_ID ledBId( wxT( "TestLib" ), wxT( "LED_B" ) );
2749 LIB_ID resId( wxT( "TestLib" ), wxT( "R" ) );
2750
2751 auto addPad =
2752 [&]( FOOTPRINT* fp, const wxString& aNum, int aNet )
2753 {
2754 PAD* pad = new PAD( fp );
2755 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
2756 pad->SetNumber( aNum );
2758 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2759 pad->SetLayerSet( LSET( { F_Cu } ) );
2760 pad->SetNetCode( aNet );
2761 fp->Add( pad );
2762 };
2763
2764 auto addFp =
2765 [&]( const LIB_ID& aId, const wxString& aRef ) -> FOOTPRINT*
2766 {
2767 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2768 fp->SetFPID( aId );
2769 fp->SetReference( aRef );
2770 board->Add( fp );
2771 return fp;
2772 };
2773
2774 // One LED plus its series resistor. The LED anode net is the one that can clash.
2775 auto addLedAndRes =
2776 [&]( const LIB_ID& aLedId, const wxString& aLedRef, const wxString& aResRef, int aAnodeNet,
2777 std::set<FOOTPRINT*>& aSet )
2778 {
2779 FOOTPRINT* led = addFp( aLedId, aLedRef );
2780 addPad( led, wxT( "1" ), netGnd );
2781 addPad( led, wxT( "2" ), aAnodeNet );
2782 aSet.insert( led );
2783
2784 FOOTPRINT* res = addFp( resId, aResRef );
2785 addPad( res, wxT( "1" ), aAnodeNet );
2786 addPad( res, wxT( "2" ), netP3V3 );
2787 aSet.insert( res );
2788 };
2789
2790 // The block: LED_A is D3 on the clashing net, LED_B is D4 on its own net.
2791 std::set<FOOTPRINT*> refFps;
2792 addLedAndRes( ledAId, wxT( "D3" ), wxT( "R27" ), netCollide, refFps );
2793 addLedAndRes( ledBId, wxT( "D4" ), wxT( "R30" ), netRefY, refFps );
2794
2795 // The copy: same parts, but the clashing name now sits on LED_B (D3), a different part.
2796 std::set<FOOTPRINT*> tgtFps;
2797 addLedAndRes( ledAId, wxT( "D2" ), wxT( "R1" ), netTgtX, tgtFps );
2798 addLedAndRes( ledBId, wxT( "D3" ), wxT( "R2" ), netCollide, tgtFps );
2799
2800 // The fix gives the block's auto nets private names so they cannot clash with the copy.
2801 std::unordered_set<EDA_ITEM*> refItems( refFps.begin(), refFps.end() );
2802 MULTICHANNEL_TOOL::IsolateDesignBlockAutoNets( board.get(), refFps, refItems );
2803
2804 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
2805 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
2806
2808 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2809 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2810
2811 if( !status )
2812 {
2813 for( const auto& reason : details )
2814 {
2815 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s to %s: %s", reason.m_reference, reason.m_candidate,
2816 reason.m_reason ) );
2817 }
2818 }
2819
2820 BOOST_CHECK_MESSAGE( status, "Topology match failed even after isolating the block's auto nets (issue 24767)" );
2821 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
2822}
2823
2824
2830BOOST_FIXTURE_TEST_CASE( TopoMatchTieBreaksIdenticalPartsByValue, MULTICHANNEL_TEST_FIXTURE )
2831{
2833
2834 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2835
2836 const LIB_ID receptacleId( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) );
2837
2838 // Unique net per footprint, so the four are a topological tie and only the value differs.
2839 auto makeReceptacle =
2840 [&]( const wxString& aRef, const wxString& aValue, bool aWithSymbolPath ) -> FOOTPRINT*
2841 {
2842 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2843 fp->SetFPID( receptacleId );
2844 fp->SetReference( aRef );
2845 fp->SetValue( aValue );
2846
2847 if( aWithSymbolPath )
2848 {
2850 path.push_back( KIID() );
2851 fp->SetPath( path );
2852 }
2853
2854 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), wxString::Format( "net_%s", aRef ) );
2855 board->Add( net );
2856
2857 PAD* pad = new PAD( fp );
2858 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
2859 pad->SetNumber( wxT( "1" ) );
2861 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2862 pad->SetLayerSet( LSET( { F_Cu } ) );
2863 pad->SetNet( net );
2864 fp->Add( pad );
2865
2866 board->Add( fp );
2867 return fp;
2868 };
2869
2870 // Block source: no symbol path (as after AppendBoard). Fixed order keeps the test deterministic.
2871 FOOTPRINT* refNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), false );
2872 FOOTPRINT* refNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), false );
2873 FOOTPRINT* refNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), false );
2874 FOOTPRINT* refNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), false );
2875
2876 // Target refdes order is the reverse of value order, so a refdes fallback would swap NC and NO.
2877 FOOTPRINT* tgtNO1 = makeReceptacle( wxT( "J20" ), wxT( "NO_1" ), true );
2878 FOOTPRINT* tgtNO0 = makeReceptacle( wxT( "J21" ), wxT( "NO_0" ), true );
2879 FOOTPRINT* tgtNC1 = makeReceptacle( wxT( "J22" ), wxT( "NC_1" ), true );
2880 FOOTPRINT* tgtNC0 = makeReceptacle( wxT( "J23" ), wxT( "NC_0" ), true );
2881
2882 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
2883 cgRef->AddFootprint( refNC0, VECTOR2I( 0, 0 ) );
2884 cgRef->AddFootprint( refNC1, VECTOR2I( 0, 0 ) );
2885 cgRef->AddFootprint( refNO0, VECTOR2I( 0, 0 ) );
2886 cgRef->AddFootprint( refNO1, VECTOR2I( 0, 0 ) );
2887 cgRef->BuildConnectivity();
2888
2889 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
2890 cgTarget->AddFootprint( tgtNO1, VECTOR2I( 0, 0 ) );
2891 cgTarget->AddFootprint( tgtNO0, VECTOR2I( 0, 0 ) );
2892 cgTarget->AddFootprint( tgtNC1, VECTOR2I( 0, 0 ) );
2893 cgTarget->AddFootprint( tgtNC0, VECTOR2I( 0, 0 ) );
2894 cgTarget->BuildConnectivity();
2895
2897 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2898 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2899
2900 BOOST_CHECK( status );
2901 BOOST_CHECK_EQUAL( result.size(), 4 );
2902
2903 // Each reference must match the same-value target.
2904 for( const auto& [refFp, targetFp] : result )
2905 {
2906 BOOST_TEST_MESSAGE( wxString::Format( "%s (%s) -> %s (%s)", refFp->GetReference(), refFp->GetValue(),
2907 targetFp->GetReference(), targetFp->GetValue() ) );
2908
2909 BOOST_CHECK_EQUAL( refFp->GetValue(), targetFp->GetValue() );
2910 }
2911}
2912
2913
2914// A sheet reused inside a repeated sheet repeats its symbol UUID, so only the inner sheet UUID
2915// tells instances apart (Zoho 2659)
2916BOOST_FIXTURE_TEST_CASE( TopoMatchTieBreaksNestedSheetInstancesByPath, MULTICHANNEL_TEST_FIXTURE )
2917{
2919
2920 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2921
2922 const KIID outerRef, outerTarget, inner1, inner2, symbol;
2923
2924 // Unique net per footprint and one shared value, so topology and value are both a tie
2925 auto makePart =
2926 [&]( const wxString& aRef, const KIID& aOuter, const KIID& aInner ) -> FOOTPRINT*
2927 {
2928 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2929 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R_0402" ) ) );
2930 fp->SetReference( aRef );
2931 fp->SetValue( wxT( "10k" ) );
2932 fp->SetPath( KIID_PATH( wxString::Format( "/%s/%s/%s", aOuter.AsString(), aInner.AsString(),
2933 symbol.AsString() ) ) );
2934
2935 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), wxString::Format( "net_%s", aRef ) );
2936 board->Add( net );
2937
2938 PAD* pad = new PAD( fp );
2939 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
2940 pad->SetNumber( wxT( "1" ) );
2942 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2943 pad->SetLayerSet( LSET( { F_Cu } ) );
2944 pad->SetNet( net );
2945 fp->Add( pad );
2946
2947 board->Add( fp );
2948 return fp;
2949 };
2950
2951 FOOTPRINT* ref1 = makePart( wxT( "R101" ), outerRef, inner1 );
2952 FOOTPRINT* ref2 = makePart( wxT( "R201" ), outerRef, inner2 );
2953
2954 // Target refdes order is the reverse of instance order, so a refdes fallback pairs them crosswise
2955 FOOTPRINT* target2 = makePart( wxT( "R301" ), outerTarget, inner2 );
2956 FOOTPRINT* target1 = makePart( wxT( "R401" ), outerTarget, inner1 );
2957
2958 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
2959 cgRef->AddFootprint( ref1, VECTOR2I( 0, 0 ) );
2960 cgRef->AddFootprint( ref2, VECTOR2I( 0, 0 ) );
2961 cgRef->BuildConnectivity();
2962
2963 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
2964 cgTarget->AddFootprint( target2, VECTOR2I( 0, 0 ) );
2965 cgTarget->AddFootprint( target1, VECTOR2I( 0, 0 ) );
2966 cgTarget->BuildConnectivity();
2967
2969 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2970
2971 BOOST_REQUIRE( cgRef->FindIsomorphism( cgTarget.get(), result, details ) );
2972 BOOST_CHECK( result[ref1] == target1 );
2973 BOOST_CHECK( result[ref2] == target2 );
2974}
2975
2976
2981BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutUnmirrorsIdenticalReceptacles, MULTICHANNEL_TEST_FIXTURE )
2982{
2983 m_board = std::make_unique<BOARD>();
2984 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2985
2986 auto makeReceptacle =
2987 [&]( const wxString& aRef, const wxString& aValue, const VECTOR2I& aPos,
2988 bool aWithSymbolPath ) -> FOOTPRINT*
2989 {
2990 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
2991 fp->SetFPID( LIB_ID( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) ) );
2992 fp->SetReference( aRef );
2993 fp->SetValue( aValue );
2994 fp->SetPosition( aPos );
2995
2996 if( aWithSymbolPath )
2997 {
2999 path.push_back( KIID() );
3000 fp->SetPath( path );
3001 }
3002
3003 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxString::Format( "net_%s", aRef ) );
3004 m_board->Add( net );
3005
3006 PAD* pad = new PAD( fp );
3007 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
3008 pad->SetNumber( wxT( "1" ) );
3010 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
3011 pad->SetLayerSet( LSET( { F_Cu } ) );
3012 pad->SetNet( net );
3013 pad->SetPosition( aPos );
3014 fp->Add( pad );
3015
3016 m_board->Add( fp );
3017 return fp;
3018 };
3019
3020 auto mm =
3021 []( double a, double b )
3022 {
3023 return VECTOR2I( pcbIUScale.mmToIU( a ), pcbIUScale.mmToIU( b ) );
3024 };
3025
3026 // Signed area of NC_0, NC_1, NO_0. Same sign as the block is correct, opposite sign is a mirror.
3027 auto chirality =
3028 []( const VECTOR2I& aNC0, const VECTOR2I& aNC1, const VECTOR2I& aNO0 ) -> double
3029 {
3030 return (double) ( aNC1.x - aNC0.x ) * ( aNO0.y - aNC0.y ) - (double) ( aNC1.y - aNC0.y ) * ( aNO0.x - aNC0.x );
3031 };
3032
3033 // Block source: the reference arrangement, no symbol path (as after AppendBoard).
3034 FOOTPRINT* srcNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), mm( 0, 0 ), false );
3035 FOOTPRINT* srcNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), mm( 0, 7.9 ), false );
3036 FOOTPRINT* srcNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), mm( -3.58, 2.28 ), false );
3037 FOOTPRINT* srcNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), mm( 3.58, 5.62 ), false );
3038
3039 const double srcChir = chirality( srcNC0->GetPosition(), srcNC1->GetPosition(), srcNO0->GetPosition() );
3040
3041 // Destination placed as a mirror (NO_0 / NO_1 reflected across the NC axis).
3042 FOOTPRINT* dstNC0 = makeReceptacle( wxT( "J8" ), wxT( "NC_0" ), mm( 40, 40 ), true );
3043 FOOTPRINT* dstNC1 = makeReceptacle( wxT( "J9" ), wxT( "NC_1" ), mm( 40, 47.9 ), true );
3044 FOOTPRINT* dstNO0 = makeReceptacle( wxT( "J10" ), wxT( "NO_0" ), mm( 43.58, 42.28 ), true );
3045 FOOTPRINT* dstNO1 = makeReceptacle( wxT( "J11" ), wxT( "NO_1" ), mm( 36.42, 45.62 ), true );
3046
3047 BOOST_REQUIRE_LT( srcChir * chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() ), 0.0 );
3048
3049 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
3050 destGroup->SetName( wxT( "Pin receptacles for safety switch" ) );
3051
3052 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
3053 destGroup->AddItem( fp );
3054
3055 m_board->Add( destGroup );
3056
3057 RULE_AREA dbRA;
3059
3060 for( FOOTPRINT* fp : { srcNC0, srcNC1, srcNO0, srcNO1 } )
3061 {
3062 dbRA.m_components.insert( fp );
3063 dbRA.m_designBlockItems.insert( fp );
3064 }
3065
3066 dbRA.m_zone = new ZONE( m_board.get() );
3067 dbRA.m_zone->SetIsRuleArea( true );
3069 dbRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( -6, -3 ), mm( 6, 11 ) ) ) );
3070
3071 RULE_AREA destRA;
3073
3074 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
3075 destRA.m_components.insert( fp );
3076
3077 destRA.m_zone = new ZONE( m_board.get() );
3078 destRA.m_zone->SetIsRuleArea( true );
3079 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
3080 destRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( 33, 37 ), mm( 47, 51 ) ) ) );
3081
3082 TOOL_MANAGER toolMgr;
3083 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3084 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3085
3087 toolMgr.RegisterTool( mtTool );
3088
3089 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = false,
3090 .m_connectedRoutingOnly = false,
3091 .m_copyPlacement = true,
3092 .m_copyOtherItems = false,
3093 .m_groupItems = false,
3094 .m_includeLockedItems = true };
3095
3096 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
3097 BOOST_REQUIRE_MESSAGE( result >= 0, "RepeatLayout failed" );
3098
3099 delete dbRA.m_zone;
3100 delete destRA.m_zone;
3101
3102 const double dstChir = chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() );
3103
3104 BOOST_TEST_MESSAGE( wxString::Format( wxT( "block chirality %.0f, destination chirality after apply %.0f" ),
3105 srcChir, dstChir ) );
3106
3107 BOOST_CHECK_MESSAGE( srcChir * dstChir > 0.0, "Applied layout left the receptacles mirrored (NC/NO swapped)" );
3108}
3109
3110
3117BOOST_FIXTURE_TEST_CASE( CheckRACompatGlobalRailAcrossChannels, MULTICHANNEL_TEST_FIXTURE )
3118{
3119 m_board = std::make_unique<BOARD>();
3120 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
3121
3122 auto addNet =
3123 [&]( const wxString& aName ) -> int
3124 {
3125 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), aName );
3126 m_board->Add( net );
3127 return net->GetNetCode();
3128 };
3129
3130 const int netGnd = addNet( wxT( "GND" ) );
3131 const int netVcc = addNet( wxT( "+3V3" ) );
3132
3133 const LIB_ID icId( wxT( "TestLib" ), wxT( "SOT-23-6" ) );
3134 const LIB_ID rId( wxT( "TestLib" ), wxT( "R_0402" ) );
3135
3136 // Each channel is an IC (OUT + GND) and a config resistor strapped to a rail: GND in channels
3137 // 1 and 2, +3V3 in channel 3. That strap difference is intentional and must not defeat the match.
3138 auto makeChannel =
3139 [&]( int aIdx, int aStrapNet ) -> ZONE*
3140 {
3141 const int netOut = addNet( wxString::Format( wxT( "Net-(U%d-OUT)" ), aIdx ) );
3142 const int netCfg = addNet( wxString::Format( wxT( "Net-(U%d-CFG)" ), aIdx ) );
3143
3144 auto addPad =
3145 [&]( FOOTPRINT* aFp, const wxString& aNumber, int aNetCode )
3146 {
3147 PAD* pad = new PAD( aFp );
3148 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
3149 pad->SetNumber( aNumber );
3151 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ),
3152 pcbIUScale.mmToIU( 1 ) ) );
3153 pad->SetLayerSet( LSET( { F_Cu } ) );
3154 pad->SetNetCode( aNetCode );
3155 aFp->Add( pad );
3156 };
3157
3158 FOOTPRINT* ic = new FOOTPRINT( m_board.get() );
3159 ic->SetFPID( icId );
3160 ic->SetReference( wxString::Format( wxT( "U%d" ), aIdx ) );
3161 m_board->Add( ic );
3162 addPad( ic, wxT( "1" ), netOut );
3163 addPad( ic, wxT( "2" ), netGnd );
3164
3165 FOOTPRINT* r = new FOOTPRINT( m_board.get() );
3166 r->SetFPID( rId );
3167 r->SetReference( wxString::Format( wxT( "R%d" ), aIdx ) );
3168 m_board->Add( r );
3169 addPad( r, wxT( "1" ), netCfg );
3170 addPad( r, wxT( "2" ), aStrapNet );
3171
3172 ZONE* zone = new ZONE( m_board.get() );
3173 m_board->Add( zone );
3174
3175 return zone;
3176 };
3177
3178 std::vector<std::pair<int, int>> channelStraps = { { 1, netGnd }, { 2, netGnd }, { 3, netVcc } };
3179 std::vector<ZONE*> zones;
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
3188 RULE_AREAS_DATA* ruleData = mtTool->GetData();
3189 ruleData->m_areas.reserve( channelStraps.size() );
3190
3191 for( const auto& [idx, strap] : channelStraps )
3192 {
3193 ZONE* zone = makeChannel( idx, strap );
3194 zones.push_back( zone );
3195
3196 RULE_AREA ra;
3197 ra.m_zone = zone;
3198 ra.m_ruleName = wxString::Format( wxT( "Channel %d" ), idx );
3199
3200 for( FOOTPRINT* fp : m_board->Footprints() )
3201 {
3202 if( fp->GetReference() == wxString::Format( wxT( "U%d" ), idx )
3203 || fp->GetReference() == wxString::Format( wxT( "R%d" ), idx ) )
3204 {
3205 ra.m_components.insert( fp );
3206 }
3207 }
3208
3209 ruleData->m_areas.push_back( ra );
3210 }
3211
3212 // Before the fix, channel 3's lone GND pad hid the rail from that pairing and the match failed.
3213 int status = mtTool->CheckRACompatibility( zones[1] );
3214 BOOST_REQUIRE_EQUAL( status, 0 );
3215
3216 for( RULE_AREA& ra : ruleData->m_areas )
3217 {
3218 if( ra.m_zone == zones[1] )
3219 continue;
3220
3221 auto it = ruleData->m_compatMap.find( &ra );
3222 BOOST_REQUIRE( it != ruleData->m_compatMap.end() );
3223
3224 for( const wxString& reason : it->second.m_mismatchReasons )
3225 BOOST_TEST_MESSAGE( wxString::Format( "%s: %s", ra.m_ruleName, reason ) );
3226
3227 BOOST_CHECK_MESSAGE( it->second.m_isOk,
3228 wxString::Format( "Channel 2 must match %s", ra.m_ruleName ) );
3229 }
3230}
3231
3232
3238BOOST_FIXTURE_TEST_CASE( CheckRACompatOverlappingAreaKeepsLocalNet, MULTICHANNEL_TEST_FIXTURE )
3239{
3240 m_board = std::make_unique<BOARD>();
3241 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
3242
3243 auto addNet =
3244 [&]( const wxString& aName ) -> int
3245 {
3246 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), aName );
3247 m_board->Add( net );
3248 return net->GetNetCode();
3249 };
3250
3251 const int netGnd = addNet( wxT( "GND" ) );
3252
3253 const LIB_ID icId( wxT( "TestLib" ), wxT( "SOT-23-6" ) );
3254 const LIB_ID rId( wxT( "TestLib" ), wxT( "R_0402" ) );
3255
3256 auto addPad =
3257 [&]( FOOTPRINT* aFp, const wxString& aNumber, int aNetCode )
3258 {
3259 PAD* pad = new PAD( aFp );
3260 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
3261 pad->SetNumber( aNumber );
3263 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
3264 pad->SetLayerSet( LSET( { F_Cu } ) );
3265 pad->SetNetCode( aNetCode );
3266 aFp->Add( pad );
3267 };
3268
3269 auto makeFp =
3270 [&]( const LIB_ID& aFpId, const wxString& aRef ) -> FOOTPRINT*
3271 {
3272 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
3273 fp->SetFPID( aFpId );
3274 fp->SetReference( aRef );
3275 m_board->Add( fp );
3276 return fp;
3277 };
3278
3279 // Channel 1: U1 pad 1 and R1 pad 1 share a per-channel net, giving that net two internal pads.
3280 const int netLocal1 = addNet( wxT( "Net-(U1-SIG)" ) );
3281 FOOTPRINT* u1 = makeFp( icId, wxT( "U1" ) );
3282 addPad( u1, wxT( "1" ), netLocal1 );
3283 addPad( u1, wxT( "2" ), netGnd );
3284 FOOTPRINT* r1 = makeFp( rId, wxT( "R1" ) );
3285 addPad( r1, wxT( "1" ), netLocal1 );
3286 addPad( r1, wxT( "2" ), netGnd );
3287
3288 // Channel 2: U2 pad 1 is on its own net, so R2 does not share it -- a real difference from
3289 // channel 1 that must be reported as a mismatch.
3290 const int netLocal2a = addNet( wxT( "Net-(U2-SIG)" ) );
3291 const int netLocal2b = addNet( wxT( "Net-(R2-SIG)" ) );
3292 FOOTPRINT* u2 = makeFp( icId, wxT( "U2" ) );
3293 addPad( u2, wxT( "1" ), netLocal2a );
3294 addPad( u2, wxT( "2" ), netGnd );
3295 FOOTPRINT* r2 = makeFp( rId, wxT( "R2" ) );
3296 addPad( r2, wxT( "1" ), netLocal2b );
3297 addPad( r2, wxT( "2" ), netGnd );
3298
3299 ZONE* zone1 = new ZONE( m_board.get() );
3300 m_board->Add( zone1 );
3301 ZONE* zone2 = new ZONE( m_board.get() );
3302 m_board->Add( zone2 );
3303 ZONE* zoneDup = new ZONE( m_board.get() );
3304 m_board->Add( zoneDup );
3305
3306 TOOL_MANAGER toolMgr;
3307 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3308 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3309
3311 toolMgr.RegisterTool( mtTool );
3312
3313 RULE_AREAS_DATA* ruleData = mtTool->GetData();
3314 ruleData->m_areas.reserve( 3 );
3315
3316 RULE_AREA ra1;
3317 ra1.m_zone = zone1;
3318 ra1.m_ruleName = wxT( "Channel 1" );
3319 ra1.m_components = { u1, r1 };
3320 ruleData->m_areas.push_back( ra1 );
3321
3322 RULE_AREA ra2;
3323 ra2.m_zone = zone2;
3324 ra2.m_ruleName = wxT( "Channel 2" );
3325 ra2.m_components = { u2, r2 };
3326 ruleData->m_areas.push_back( ra2 );
3327
3328 // A second rule area over channel 1's footprints. Counting it as a distinct channel would make
3329 // Net-(U1-SIG) look global (two areas carry it) and hide the difference from channel 2.
3330 RULE_AREA raDup;
3331 raDup.m_zone = zoneDup;
3332 raDup.m_ruleName = wxT( "Channel 1 duplicate" );
3333 raDup.m_components = { u1, r1 };
3334 ruleData->m_areas.push_back( raDup );
3335
3336 int status = mtTool->CheckRACompatibility( zone1 );
3337 BOOST_REQUIRE_EQUAL( status, 0 );
3338
3339 RULE_AREA* channel2 = nullptr;
3340
3341 for( RULE_AREA& ra : ruleData->m_areas )
3342 {
3343 if( ra.m_zone == zone2 )
3344 channel2 = &ra;
3345 }
3346
3347 BOOST_REQUIRE( channel2 != nullptr );
3348
3349 auto it = ruleData->m_compatMap.find( channel2 );
3350 BOOST_REQUIRE( it != ruleData->m_compatMap.end() );
3351
3352 BOOST_CHECK_MESSAGE( !it->second.m_isOk,
3353 "Channel 1 and channel 2 differ on a local net and must not match; an "
3354 "overlapping rule area must not hide that net by marking it global" );
3355}
3356
3357
3358// A sheet that holds only sub-sheets must still be offered as a channel and gather every
3359// footprint below it, not just the deepest sheet of each footprint.
3360BOOST_FIXTURE_TEST_CASE( MultichannelNestedSheetChannels, MULTICHANNEL_TEST_FIXTURE )
3361{
3362 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3363
3364 TOOL_MANAGER toolMgr;
3365 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3366 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3367
3369 toolMgr.RegisterTool( mtTool );
3370
3372
3373 RULE_AREA* first = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/" ) );
3374 RULE_AREA* second = findSheetRuleAreaByPath( mtTool, wxT( "/SecondChannel/" ) );
3375
3376 BOOST_REQUIRE( first );
3377 BOOST_REQUIRE( second );
3378
3379 BOOST_CHECK_EQUAL( first->m_components.size(), 32 );
3380 BOOST_CHECK_EQUAL( second->m_components.size(), 32 );
3381
3382 // Leaf sheets still work.
3383 RULE_AREA* leaf = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/Common emitter amplifier/" ) );
3384 BOOST_REQUIRE( leaf );
3385 BOOST_CHECK_EQUAL( leaf->m_components.size(), 8 );
3386
3387 // The root is not a channel.
3388 BOOST_CHECK( findSheetRuleAreaByPath( mtTool, wxT( "/" ) ) == nullptr );
3389 BOOST_CHECK( findSheetRuleAreaByPath( mtTool, wxEmptyString ) == nullptr );
3390}
3391
3392
3393// A group that wraps several sub-groups must be offered as a channel and gather every
3394// footprint below it, same as the sheet case.
3395BOOST_FIXTURE_TEST_CASE( MultichannelNestedGroupChannels, MULTICHANNEL_TEST_FIXTURE )
3396{
3397 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3398
3399 TOOL_MANAGER toolMgr;
3400 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3401 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3402
3404 toolMgr.RegisterTool( mtTool );
3405
3407
3408 RULE_AREA* first = findGroupRuleAreaByName( mtTool, wxT( "FirstChannel" ) );
3409 RULE_AREA* second = findGroupRuleAreaByName( mtTool, wxT( "SecondChannel" ) );
3410
3411 BOOST_REQUIRE( first );
3412 BOOST_REQUIRE( second );
3413
3414 BOOST_CHECK_EQUAL( first->m_components.size(), 32 );
3415 BOOST_CHECK_EQUAL( second->m_components.size(), 32 );
3416
3417 // The inner sub-groups still work.
3418 RULE_AREA* sub = findGroupRuleAreaByName( mtTool, wxT( "Common emitter amplifier1 (/FirstChannel/)" ) );
3419 BOOST_REQUIRE( sub );
3420 BOOST_CHECK_EQUAL( sub->m_components.size(), 8 );
3421}
3422
3423
3424BOOST_FIXTURE_TEST_CASE( RuleAreaGroupSourcesWithWildcardsAndApostrophes, MULTICHANNEL_TEST_FIXTURE )
3425{
3426 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3427
3428 PCB_GROUP* first = nullptr;
3429 PCB_GROUP* second = nullptr;
3430
3431 for( PCB_GROUP* group : m_board->Groups() )
3432 {
3433 if( group->GetName() == wxT( "FirstChannel" ) )
3434 first = group;
3435 else if( group->GetName() == wxT( "SecondChannel" ) )
3436 second = group;
3437 }
3438
3439 BOOST_REQUIRE( first && second );
3440
3441 first->SetName( wxT( "Channel'*" ) );
3442 second->SetName( wxT( "Channel'Other" ) );
3443
3444 TOOL_MANAGER toolMgr;
3445 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3446 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3447
3449 toolMgr.RegisterTool( mtTool );
3450
3452
3453 RULE_AREA* candidate = findGroupRuleAreaByName( mtTool, wxT( "Channel'*" ) );
3454 BOOST_REQUIRE( candidate );
3455 BOOST_REQUIRE_EQUAL( candidate->m_components.size(), 64 );
3456
3457 std::set<FOOTPRINT*> expected = candidate->m_components;
3458 candidate->m_generateEnabled = true;
3459
3460 mtTool->AutogenerateRuleAreas( TOOL_EVENT() );
3461 mtTool->FindExistingRuleAreas();
3462
3463 RULE_AREA* area = findRuleAreaByPlacementGroup( mtTool, wxT( "Channel'*" ) );
3464 BOOST_REQUIRE( area && area->m_zone );
3465 BOOST_CHECK( area->m_components == expected );
3466
3467 BOX2I outlineBounds = area->m_zone->Outline()->BBox();
3468
3469 for( FOOTPRINT* fp : expected )
3470 BOOST_CHECK( outlineBounds.Contains( fp->GetPosition() ) );
3471}
3472
3473
3474// Sheet-level Repeat Layout with "group items" enabled must still reproduce tuning meanders
3475// as generators in the target, not flatten them to loose tracks.
3476BOOST_FIXTURE_TEST_CASE( RepeatLayoutSheetCopiesMeandersWhole, MULTICHANNEL_TEST_FIXTURE )
3477{
3478 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3479
3480 TOOL_MANAGER toolMgr;
3481 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3482 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3483
3485 toolMgr.RegisterTool( mtTool );
3486
3488
3489 auto ruleData = mtTool->GetData();
3490 ruleData->m_replaceExisting = true;
3491
3492 for( RULE_AREA& ra : ruleData->m_areas )
3493 {
3495 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3496 {
3497 ra.m_generateEnabled = true;
3498 }
3499 }
3500
3502 mtTool->AutogenerateRuleAreas( dummy );
3503 mtTool->FindExistingRuleAreas();
3504
3505 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3506 RULE_AREA* target = findRuleAreaByPlacementGroup( mtTool, wxT( "/SecondChannel/" ) );
3507
3508 BOOST_REQUIRE( source && source->m_zone );
3509 BOOST_REQUIRE( target && target->m_zone );
3510
3511 // Keep only the source channel's meanders so the copy delta is unambiguous.
3512 std::vector<PCB_GENERATOR*> strip;
3513
3514 for( PCB_GENERATOR* gen : m_board->Generators() )
3515 {
3516 if( gen->GetGeneratorType() != wxT( "tuning_pattern" ) )
3517 continue;
3518
3519 if( !gen->HitTest( source->m_zone->Outline()->COutline( 0 ), false ) )
3520 strip.push_back( gen );
3521 }
3522
3523 for( PCB_GENERATOR* gen : strip )
3524 {
3525 for( EDA_ITEM* member : gen->GetItems() )
3526 {
3527 if( member->IsBOARD_ITEM() )
3528 static_cast<BOARD_ITEM*>( member )->SetParentGroup( nullptr );
3529 }
3530
3531 m_board->Remove( gen );
3532 }
3533
3534 int sourceMeanders = countTuningGensInArea( m_board.get(), source->m_zone );
3535 BOOST_REQUIRE( sourceMeanders > 0 );
3536 BOOST_REQUIRE_EQUAL( countTuningGensInArea( m_board.get(), target->m_zone ), 0 );
3537
3538 ruleData->m_options.m_groupItems = true;
3539
3540 mtTool->CheckRACompatibility( source->m_zone );
3541 ruleData->m_compatMap[target].m_doCopy = true;
3542
3543 int result = mtTool->RepeatLayout( TOOL_EVENT(), source->m_zone );
3544 BOOST_REQUIRE( result >= 0 );
3545
3546 BOOST_CHECK_EQUAL( countTuningGensInArea( m_board.get(), target->m_zone ), sourceMeanders );
3547}
3548
3549
3550// A sheet placement area outline must cover the channel's grouped content (meanders), not just
3551// its footprints, otherwise routing that extends past the parts is left behind on repeat.
3552BOOST_FIXTURE_TEST_CASE( SheetRuleAreaOutlineCoversMeanders, MULTICHANNEL_TEST_FIXTURE )
3553{
3554 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3555
3556 TOOL_MANAGER toolMgr;
3557 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3558 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3559
3561 toolMgr.RegisterTool( mtTool );
3562
3564
3565 auto ruleData = mtTool->GetData();
3566 ruleData->m_replaceExisting = true;
3567
3568 for( RULE_AREA& ra : ruleData->m_areas )
3569 {
3571 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3572 {
3573 ra.m_generateEnabled = true;
3574 }
3575 }
3576
3578 mtTool->AutogenerateRuleAreas( dummy );
3579 mtTool->FindExistingRuleAreas();
3580
3581 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3582 BOOST_REQUIRE( source && source->m_zone );
3583
3584 std::set<EDA_GROUP*> groups;
3585
3586 for( FOOTPRINT* fp : source->m_components )
3587 {
3588 for( EDA_GROUP* g = fp->GetParentGroup(); g; g = g->AsEdaItem()->GetParentGroup() )
3589 groups.insert( g );
3590 }
3591
3592 int checked = 0;
3593
3594 for( EDA_GROUP* g : groups )
3595 {
3596 for( EDA_ITEM* member : g->GetItems() )
3597 {
3598 if( member->Type() != PCB_GENERATOR_T )
3599 continue;
3600
3601 BOX2I bb = static_cast<PCB_GENERATOR*>( member )->GetBoundingBox();
3602
3603 BOOST_CHECK( source->m_zone->Outline()->Contains( bb.GetOrigin() ) );
3604 BOOST_CHECK( source->m_zone->Outline()->Contains( bb.GetEnd() ) );
3605 checked++;
3606 }
3607 }
3608
3609 BOOST_REQUIRE( checked > 0 );
3610}
3611
3612
3613// Sheet-level Repeat Layout must reproduce the design-block GND zones in the target channel.
3614BOOST_FIXTURE_TEST_CASE( RepeatLayoutSheetCopiesBlockZones, MULTICHANNEL_TEST_FIXTURE )
3615{
3616 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3617
3618 TOOL_MANAGER toolMgr;
3619 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3620 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3621
3623 toolMgr.RegisterTool( mtTool );
3624
3626
3627 auto ruleData = mtTool->GetData();
3628 ruleData->m_replaceExisting = true;
3629
3630 for( RULE_AREA& ra : ruleData->m_areas )
3631 {
3633 && ( ra.m_sheetPath == wxT( "/FirstChannel/" ) || ra.m_sheetPath == wxT( "/SecondChannel/" ) ) )
3634 {
3635 ra.m_generateEnabled = true;
3636 }
3637 }
3638
3640 mtTool->AutogenerateRuleAreas( dummy );
3641 mtTool->FindExistingRuleAreas();
3642
3643 RULE_AREA* source = findRuleAreaByPlacementGroup( mtTool, wxT( "/FirstChannel/" ) );
3644 RULE_AREA* target = findRuleAreaByPlacementGroup( mtTool, wxT( "/SecondChannel/" ) );
3645
3646 BOOST_REQUIRE( source && source->m_zone );
3647 BOOST_REQUIRE( target && target->m_zone );
3648
3649 // After the repeat the target should mirror the source's block zones.
3650 int expected = countCopperZonesInArea( m_board.get(), source->m_zone );
3651 BOOST_REQUIRE( expected > 0 );
3652
3653 // Clear the target channel's zones so the copy delta is unambiguous.
3654 std::vector<ZONE*> stripZones;
3655
3656 for( ZONE* zone : m_board->Zones() )
3657 {
3658 if( zone->GetIsRuleArea() )
3659 continue;
3660
3661 if( target->m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
3662 stripZones.push_back( zone );
3663 }
3664
3665 for( ZONE* zone : stripZones )
3666 {
3667 if( EDA_GROUP* group = zone->GetParentGroup() )
3668 group->RemoveItem( zone );
3669
3670 m_board->Remove( zone );
3671 }
3672
3673 BOOST_REQUIRE_EQUAL( countCopperZonesInArea( m_board.get(), target->m_zone ), 0 );
3674
3675 mtTool->CheckRACompatibility( source->m_zone );
3676 ruleData->m_compatMap[target].m_doCopy = true;
3677
3678 int result = mtTool->RepeatLayout( TOOL_EVENT(), source->m_zone );
3679 BOOST_REQUIRE( result >= 0 );
3680
3681 BOOST_CHECK_EQUAL( countCopperZonesInArea( m_board.get(), target->m_zone ), expected );
3682}
3683
3684
3685// The two channels must match by topology so one can be repeated onto the other.
3686BOOST_FIXTURE_TEST_CASE( MultichannelNestedChannelTopologyMatches, MULTICHANNEL_TEST_FIXTURE )
3687{
3689
3690 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
3691
3692 TOOL_MANAGER toolMgr;
3693 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3694 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3695
3697 toolMgr.RegisterTool( mtTool );
3698
3700
3701 RULE_AREA* first = findSheetRuleAreaByPath( mtTool, wxT( "/FirstChannel/" ) );
3702 RULE_AREA* second = findSheetRuleAreaByPath( mtTool, wxT( "/SecondChannel/" ) );
3703
3704 BOOST_REQUIRE( first );
3705 BOOST_REQUIRE( second );
3706
3707 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( first->m_components, second->m_components );
3708 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( second->m_components, first->m_components );
3709
3711 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
3712 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
3713
3714 BOOST_CHECK( status );
3715 BOOST_CHECK( details.empty() );
3716}
3717
3718
3725BOOST_FIXTURE_TEST_CASE( GenerateSheetRAIncludesLooseInterBlockRouting, MULTICHANNEL_TEST_FIXTURE )
3726{
3727 m_board = std::make_unique<BOARD>();
3728 BOARD* board = m_board.get();
3729
3730 board->Add( new NETINFO_ITEM( board, wxT( "N1" ), 1 ) );
3731
3732 auto addFootprint =
3733 [&]( const wxString& aRef, const VECTOR2I& aPos )
3734 {
3735 FOOTPRINT* fp = new FOOTPRINT( board );
3736 fp->SetReference( aRef );
3737 fp->SetFPID( LIB_ID( wxT( "lib" ), wxT( "R_0402" ) ) );
3738 fp->SetSheetname( wxT( "/ChannelA/" ) );
3739 fp->SetSheetfile( wxT( "channelA.kicad_sch" ) );
3740
3741 PAD* pad = new PAD( fp );
3742 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
3743 pad->SetNumber( wxT( "1" ) );
3744 pad->SetAttribute( PAD_ATTRIB::SMD );
3745 pad->SetLayerSet( LSET( { F_Cu } ) );
3746 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
3747 fp->Add( pad );
3748
3749 fp->SetPosition( aPos );
3750 board->Add( fp );
3751
3752 pad->SetNetCode( 1 );
3753 };
3754
3755 addFootprint( wxT( "R1" ), VECTOR2I( 0, 0 ) );
3756 addFootprint( wxT( "R2" ), VECTOR2I( pcbIUScale.mmToIU( 10.0 ), 0 ) );
3757
3758 // Track on the channel-local net, detouring far south of the footprints.
3759 PCB_TRACK* track = new PCB_TRACK( board );
3760 track->SetLayer( F_Cu );
3761 track->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
3762 track->SetStart( VECTOR2I( 0, 0 ) );
3763 track->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 5.0 ), pcbIUScale.mmToIU( 30.0 ) ) );
3764 board->Add( track );
3765 track->SetNetCode( 1 );
3766
3767 TOOL_MANAGER toolMgr;
3768 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3769 toolMgr.SetEnvironment( board, nullptr, nullptr, nullptr, toolsHolder );
3770
3772 toolMgr.RegisterTool( mtTool );
3773
3775
3776 auto ruleData = mtTool->GetData();
3777 ruleData->m_replaceExisting = true;
3778
3779 RULE_AREA* channelRA = findSheetRuleAreaByPath( mtTool, wxT( "/ChannelA/" ) );
3780 BOOST_REQUIRE( channelRA != nullptr );
3781 channelRA->m_generateEnabled = true;
3782
3783 TOOL_EVENT dummyEvent;
3784 mtTool->AutogenerateRuleAreas( dummyEvent );
3785
3786 ZONE* raZone = nullptr;
3787
3788 for( ZONE* zone : board->Zones() )
3789 {
3790 if( zone->GetIsRuleArea() && zone->GetZoneName() == wxT( "auto-placement-area-/ChannelA/" ) )
3791 {
3792 raZone = zone;
3793 break;
3794 }
3795 }
3796
3797 BOOST_REQUIRE( raZone != nullptr );
3798
3799 // A point on the bulging track, well past the footprint band.
3800 VECTOR2I onTrack( pcbIUScale.mmToIU( 2.5 ), pcbIUScale.mmToIU( 15.0 ) );
3801
3802 BOOST_CHECK_MESSAGE( raZone->Outline()->Contains( onTrack ),
3803 "Sheet rule area outline must enclose loose inter-block routing (issue 24983)" );
3804}
3805
3806
3812BOOST_FIXTURE_TEST_CASE( RepeatLayoutCopiesPadOverrides, MULTICHANNEL_TEST_FIXTURE )
3813{
3814 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
3815
3816 TOOL_MANAGER toolMgr;
3817 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3818
3819 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
3820
3822 toolMgr.RegisterTool( mtTool );
3823
3824 mtTool->FindExistingRuleAreas();
3825
3826 auto ruleData = mtTool->GetData();
3827
3828 RULE_AREA* refArea = nullptr;
3829 RULE_AREA* targetArea = nullptr;
3830
3831 for( RULE_AREA& ra : ruleData->m_areas )
3832 {
3833 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
3834 refArea = &ra;
3835 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
3836 targetArea = &ra;
3837 }
3838
3839 BOOST_REQUIRE( refArea );
3840 BOOST_REQUIRE( targetArea );
3841
3842 FOOTPRINT* refFP = nullptr;
3843 FOOTPRINT* targetFP = nullptr;
3844
3845 for( FOOTPRINT* fp : refArea->m_components )
3846 {
3847 if( fp->GetReference() == wxT( "U1" ) )
3848 refFP = fp;
3849 }
3850
3851 for( FOOTPRINT* fp : targetArea->m_components )
3852 {
3853 if( fp->GetReference() == wxT( "U2" ) )
3854 targetFP = fp;
3855 }
3856
3857 BOOST_REQUIRE( refFP );
3858 BOOST_REQUIRE( targetFP );
3859
3860 PAD* refPad = refFP->FindPadByNumber( wxT( "1" ) );
3861
3862 BOOST_REQUIRE( refPad );
3863
3865 refPad->SetLocalSolderMaskMargin( pcbIUScale.mmToIU( 0.15 ) );
3866
3867 BOOST_REQUIRE( targetFP->FindPadByNumber( wxT( "1" ) )->GetLocalZoneConnection() == ZONE_CONNECTION::INHERITED );
3868
3869 mtTool->CheckRACompatibility( refArea->m_zone );
3870
3871 ruleData->m_compatMap[targetArea].m_doCopy = true;
3872 ruleData->m_options.m_copyPlacement = true;
3873
3874 BOOST_REQUIRE( mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone ) >= 0 );
3875
3876 PAD* targetPad = targetFP->FindPadByNumber( wxT( "1" ) );
3877
3878 BOOST_REQUIRE( targetPad );
3879
3880 BOOST_CHECK( targetPad->GetLocalZoneConnection() == ZONE_CONNECTION::NONE );
3881 BOOST_CHECK( targetPad->GetLocalSolderMaskMargin() == refPad->GetLocalSolderMaskMargin() );
3882}
3883
3884
3885static std::vector<PCB_GROUP*> findGroupsByName( BOARD* aBoard, const wxString& aName )
3886{
3887 std::vector<PCB_GROUP*> found;
3888
3889 for( PCB_GROUP* group : aBoard->Groups() )
3890 {
3891 if( group->GetName() == aName )
3892 found.push_back( group );
3893 }
3894
3895 return found;
3896}
3897
3898
3899static std::set<FOOTPRINT*> collectFootprints( const PCB_GROUP* aGroup )
3900{
3901 std::set<FOOTPRINT*> footprints;
3902
3903 aGroup->RunOnChildren(
3904 [&]( BOARD_ITEM* child )
3905 {
3906 if( child->Type() == PCB_FOOTPRINT_T )
3907 footprints.insert( static_cast<FOOTPRINT*>( child ) );
3908 },
3910
3911 return footprints;
3912}
3913
3914
3915static void checkGroupChannelRepeatLayout( SETTINGS_MANAGER& aSettingsManager, std::unique_ptr<BOARD>& aBoard,
3916 bool aGroupItems )
3917{
3918 KI_TEST::LoadBoard( aSettingsManager, "multichannel_nested", aBoard );
3919
3920 const wxString sourceChannel = wxT( "ChanA" );
3921 const wxString targetChannel = wxT( "ChanB" );
3922
3923 // A schematic group linked to a placement area holds its footprints directly.
3924 const std::pair<wxString, wxString> channels[] = { { wxT( "FirstChannel" ), sourceChannel },
3925 { wxT( "SecondChannel" ), targetChannel } };
3926
3927 for( const auto& [nested, flat] : channels )
3928 {
3929 std::vector<PCB_GROUP*> found = findGroupsByName( aBoard.get(), nested );
3930
3931 BOOST_REQUIRE_EQUAL( found.size(), 1 );
3932
3933 std::set<FOOTPRINT*> footprints = collectFootprints( found[0] );
3934
3935 BOOST_REQUIRE( !footprints.empty() );
3936
3937 PCB_GROUP* channelGroup = new PCB_GROUP( aBoard.get() );
3938 channelGroup->SetName( flat );
3939
3940 for( FOOTPRINT* fp : footprints )
3941 channelGroup->AddItem( fp );
3942
3943 aBoard->Add( channelGroup );
3944 }
3945
3946 TOOL_MANAGER toolMgr;
3947 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
3948
3949 toolMgr.SetEnvironment( aBoard.get(), nullptr, nullptr, nullptr, toolsHolder );
3950
3952 toolMgr.RegisterTool( mtTool );
3953
3955
3956 for( const wxString& name : { sourceChannel, targetChannel } )
3957 {
3958 RULE_AREA* candidate = findGroupRuleAreaByName( mtTool, name );
3959
3960 BOOST_REQUIRE( candidate );
3961
3962 candidate->m_generateEnabled = true;
3963 }
3964
3965 mtTool->AutogenerateRuleAreas( TOOL_EVENT() );
3966 mtTool->FindExistingRuleAreas();
3967
3968 RULE_AREA* refArea = findRuleAreaByPlacementGroup( mtTool, sourceChannel );
3969 RULE_AREA* targetArea = findRuleAreaByPlacementGroup( mtTool, targetChannel );
3970
3971 BOOST_REQUIRE( refArea && targetArea );
3972
3973 // Both areas have to be driven by a group, or the test proves nothing.
3976
3977 std::vector<PCB_GROUP*> targetGroups = findGroupsByName( aBoard.get(), targetChannel );
3978
3979 BOOST_REQUIRE_EQUAL( targetGroups.size(), 1 );
3980
3981 const std::set<FOOTPRINT*> targetFootprints = collectFootprints( targetGroups[0] );
3982
3983 mtTool->CheckRACompatibility( refArea->m_zone );
3984
3985 for( auto& [area, compatData] : mtTool->GetData()->m_compatMap )
3986 compatData.m_doCopy = true;
3987
3988 REPEAT_LAYOUT_OPTIONS& opts = mtTool->GetData()->m_options;
3989
3990 opts.m_copyPlacement = true;
3991 opts.m_copyRouting = true;
3992 opts.m_copyOtherItems = true;
3993 opts.m_includeLockedItems = true;
3994 opts.m_groupItems = aGroupItems;
3995
3996 // Every footprint of the target channel takes part, so the copy really does reach its group.
3997 const RULE_AREA_COMPAT_DATA& compat = mtTool->GetData()->m_compatMap.at( targetArea );
3998
3999 BOOST_REQUIRE( compat.m_isOk );
4000 BOOST_REQUIRE_EQUAL( compat.m_matchingComponents.size(), targetFootprints.size() );
4001
4002 BOOST_REQUIRE( mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone ) >= 0 );
4003
4004 // Nothing else may carry the source channel's name.
4005 BOOST_CHECK_EQUAL( findGroupsByName( aBoard.get(), sourceChannel ).size(), 1 );
4006
4007 targetGroups = findGroupsByName( aBoard.get(), targetChannel );
4008
4009 BOOST_REQUIRE_EQUAL( targetGroups.size(), 1 );
4010 BOOST_CHECK( collectFootprints( targetGroups[0] ) == targetFootprints );
4011}
4012
4013
4014// Repeat Layout must leave the group a target placement area is named after alone (issue 25622).
4015BOOST_FIXTURE_TEST_CASE( RepeatLayoutGroupAreaKeepsTargetGroup, MULTICHANNEL_TEST_FIXTURE )
4016{
4017 checkGroupChannelRepeatLayout( m_settingsManager, m_board, false );
4018}
4019
4020
4021// With "group items" the copies join the channel's own group instead of a new one (issue 25622).
4022BOOST_FIXTURE_TEST_CASE( RepeatLayoutGroupAreaGroupsIntoTargetGroup, MULTICHANNEL_TEST_FIXTURE )
4023{
4024 checkGroupChannelRepeatLayout( m_settingsManager, m_board, true );
4025}
4026
4027
4028// Repeating onto a group of design-block groups must leave each footprint in its block group
4029BOOST_FIXTURE_TEST_CASE( RepeatLayoutGroupAreaKeepsNestedGroups, MULTICHANNEL_TEST_FIXTURE )
4030{
4031 KI_TEST::LoadBoard( m_settingsManager, "multichannel_nested", m_board );
4032
4033 TOOL_MANAGER toolMgr;
4034 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
4035 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
4036
4038 toolMgr.RegisterTool( mtTool );
4039
4041
4042 for( const wxString& name : { wxString( wxT( "FirstChannel" ) ), wxString( wxT( "SecondChannel" ) ) } )
4043 {
4044 RULE_AREA* candidate = findGroupRuleAreaByName( mtTool, name );
4045 BOOST_REQUIRE( candidate );
4046 candidate->m_generateEnabled = true;
4047 }
4048
4049 mtTool->AutogenerateRuleAreas( TOOL_EVENT() );
4050 mtTool->FindExistingRuleAreas();
4051
4052 RULE_AREA* refArea = findRuleAreaByPlacementGroup( mtTool, wxT( "FirstChannel" ) );
4053 RULE_AREA* targetArea = findRuleAreaByPlacementGroup( mtTool, wxT( "SecondChannel" ) );
4054 BOOST_REQUIRE( refArea && targetArea );
4055
4056 std::vector<PCB_GROUP*> refGroups = findGroupsByName( m_board.get(), wxT( "FirstChannel" ) );
4057 std::vector<PCB_GROUP*> targetGroups = findGroupsByName( m_board.get(), wxT( "SecondChannel" ) );
4058 BOOST_REQUIRE_EQUAL( refGroups.size(), 1 );
4059 BOOST_REQUIRE_EQUAL( targetGroups.size(), 1 );
4060
4061 auto countTracks = [&]( PCB_GROUP* aGroup )
4062 {
4063 return std::count_if( m_board->Tracks().begin(), m_board->Tracks().end(),
4064 [&]( PCB_TRACK* aTrack )
4065 {
4066 return aGroup->ContainsItem( aTrack );
4067 } );
4068 };
4069
4070 long refTracks = countTracks( refGroups[0] );
4071 BOOST_REQUIRE( refTracks > 0 );
4072
4073 // The rule-area overload is what Apply Design Block Layout drives
4074 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
4075 .m_connectedRoutingOnly = false,
4076 .m_copyPlacement = true,
4077 .m_copyOtherItems = true,
4078 .m_groupItems = false,
4079 .m_includeLockedItems = true };
4080
4081 wxString err;
4082 int result = mtTool->RepeatLayout( TOOL_EVENT(), *refArea, *targetArea, opts, nullptr, &err );
4083 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
4084
4085 for( EDA_ITEM* member : targetGroups[0]->GetItems() )
4086 BOOST_CHECK_MESSAGE( member->Type() != PCB_FOOTPRINT_T, "footprint pulled out of its nested block group" );
4087
4088 // Routing held by the nested block groups is replaced, not duplicated
4089 BOOST_CHECK_EQUAL( countTracks( targetGroups[0] ), refTracks );
4090}
4091
4092
4094{
4095 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
4096
4097 TOOL_MANAGER toolMgr;
4098
4099 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, new MOCK_TOOLS_HOLDER );
4100
4102 toolMgr.RegisterTool( mtTool );
4103
4104 mtTool->FindExistingRuleAreas();
4105
4106 RULE_AREA* refArea = findSheetRuleAreaByPath( mtTool, wxT( "/Untitled Sheet/" ) );
4107 BOOST_REQUIRE( refArea );
4108 BOOST_REQUIRE_EQUAL( refArea->m_components.size(), 1 );
4109
4110 ZONE* refZone = refArea->m_zone;
4111 FOOTPRINT* refFp = *refArea->m_components.begin();
4112
4113 mtTool->CheckRACompatibility( refZone );
4114
4115 for( auto& [area, compat] : mtTool->GetData()->m_compatMap )
4116 compat.m_doCopy = true;
4117
4118 mtTool->GetData()->m_linkAreas = true;
4119 BOOST_REQUIRE_EQUAL( mtTool->RepeatLayout( TOOL_EVENT(), refZone ), 0 );
4120
4121 std::vector<ZONE*> peers;
4122
4123 for( auto& [area, compat] : mtTool->GetData()->m_compatMap )
4124 peers.push_back( area->m_zone );
4125
4126 BOOST_REQUIRE_EQUAL( peers.size(), 3 );
4127
4128 for( ZONE* peer : peers )
4129 BOOST_CHECK( peer->GetLinkSet() == refZone->GetLinkSet() );
4130
4131 FOOTPRINT* sharedFp = *mtTool->GetData()->m_compatMap.begin()->first->m_components.begin();
4132 FOOTPRINT* cleanFp = nullptr;
4133 ZONE* cleanZone = nullptr;
4134 const VECTOR2I delta( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 2 ) );
4135
4136 // A copy of a linked target selects the same parts, so that target may not be updated
4137 ZONE* duplicate = static_cast<ZONE*>( peers[0]->Clone() );
4138 duplicate->ResetUuid();
4139 m_board->Add( duplicate );
4140
4141 for( auto& [area, compat] : mtTool->GetData()->m_compatMap )
4142 {
4143 if( area->m_zone == peers[0] )
4144 sharedFp = *area->m_components.begin();
4145 else if( area->m_zone == peers[2] )
4146 cleanFp = *area->m_components.begin();
4147 }
4148
4149 cleanZone = peers[2];
4150 BOOST_REQUIRE( sharedFp && cleanFp );
4151
4152 const VECTOR2I sharedPos = sharedFp->GetPosition();
4153 const VECTOR2I cleanPos = cleanFp->GetPosition();
4154 const VECTOR2I refPos = refFp->GetPosition();
4155
4156 BOARD_COMMIT commit( &toolMgr, true, false );
4157 commit.Modify( refFp );
4158 refFp->Move( delta );
4159 commit.Push( wxT( "Move reference" ) );
4160
4161 // Edits wait for the explicit update
4162 BOOST_CHECK_EQUAL( cleanFp->GetPosition(), cleanPos );
4163 BOOST_CHECK( mtTool->UpdateLinkedLayouts( refZone ) );
4164 BOOST_CHECK_EQUAL( sharedFp->GetPosition(), sharedPos );
4165 BOOST_CHECK_EQUAL( cleanFp->GetPosition(), cleanPos + delta );
4166
4167 // Any peer can be the source, so the reference follows a target that is updated from
4168 const VECTOR2I half( delta.x / 2, delta.y / 2 );
4169
4170 BOARD_COMMIT peerCommit( &toolMgr, true, false );
4171 peerCommit.Modify( cleanFp );
4172 cleanFp->Move( -half );
4173 peerCommit.Push( wxT( "Move target" ) );
4174
4175 BOOST_CHECK( mtTool->UpdateLinkedLayouts( cleanZone ) );
4176 BOOST_CHECK_EQUAL( refFp->GetPosition(), refPos + delta - half );
4177
4178 m_board->Remove( duplicate );
4179 delete duplicate;
4180
4181 // Re-linking a pair from either side leaves the other peers on the old set
4182 const KIID oldSet = refZone->GetLinkSet();
4183
4184 for( ZONE* source : { refZone, peers[0] } )
4185 {
4186 ZONE* other = source == refZone ? peers[0] : refZone;
4187
4188 mtTool->FindExistingRuleAreas();
4189 mtTool->CheckRACompatibility( source );
4190
4191 for( auto& [area, compat] : mtTool->GetData()->m_compatMap )
4192 compat.m_doCopy = area->m_zone == other;
4193
4194 mtTool->GetData()->m_linkAreas = true;
4195 BOOST_REQUIRE_EQUAL( mtTool->RepeatLayout( TOOL_EVENT(), source ), 0 );
4196
4197 BOOST_CHECK( refZone->GetLinkSet() == peers[0]->GetLinkSet() );
4198 BOOST_CHECK( refZone->GetLinkSet() != oldSet );
4199 BOOST_CHECK( peers[1]->GetLinkSet() == oldSet );
4200 BOOST_CHECK( peers[2]->GetLinkSet() == oldSet );
4201 }
4202
4203 // The link and its options survive the file, and appended copies get a link set of their own
4204 const KIID pairSet = refZone->GetLinkSet();
4205 wxString path = wxFileName::CreateTempFileName( wxT( "linked_rule_areas" ) );
4207 wxString text;
4208
4209 io.SaveBoard( path, *m_board );
4210
4211 std::unique_ptr<BOARD> reloaded = io.LoadBoard( path );
4212
4213 wxFFile( path ).ReadAll( &text );
4214 io.LoadAndAppendBoard( path, *m_board );
4215 wxRemoveFile( path );
4216 BOOST_REQUIRE( reloaded );
4217
4218 for( ZONE* zone : { refZone, peers[0], peers[1], peers[2] } )
4219 {
4220 ZONE* loaded = dynamic_cast<ZONE*>( reloaded->ResolveItem( zone->m_Uuid, true ) );
4221
4222 BOOST_REQUIRE( loaded );
4223 BOOST_CHECK( loaded->GetLinkSet() == zone->GetLinkSet() );
4224 BOOST_CHECK( loaded->GetRepeatOptions() == zone->GetRepeatOptions() );
4225 }
4226
4227 std::set<KIID> appendedSets;
4228
4229 for( const ZONE* zone : m_board->Zones() )
4230 {
4231 if( zone->GetLinkSet() != niluuid && zone->GetLinkSet() != pairSet && zone->GetLinkSet() != oldSet )
4232 appendedSets.insert( zone->GetLinkSet() );
4233 }
4234
4235 BOOST_CHECK_EQUAL( appendedSets.size(), 2 );
4236
4237 // Only linked areas write the link, and the options live inside it
4238 BOOST_CHECK_EQUAL( text.Clone().Replace( wxT( "(link " ), wxT( "" ) ), 4 );
4239 BOOST_CHECK_EQUAL( text.Clone().Replace( wxT( "(repeat" ), wxT( "" ) ), 0 );
4240
4241 // A set of one links nothing, so unlinking one of a pair frees the other too
4242 mtTool->UnlinkAreas( { peers[1] } );
4243
4244 BOOST_CHECK( peers[1]->GetLinkSet() == niluuid );
4245 BOOST_CHECK( peers[2]->GetLinkSet() == niluuid );
4246 BOOST_CHECK( peers[2]->GetRepeatOptions() == REPEAT_LAYOUT_OPTIONS() );
4247 BOOST_CHECK( refZone->GetLinkSet() == pairSet );
4248}
4249
4250
4251static std::vector<EDA_ITEM*> sheetFootprints( BOARD* aBoard, const wxString& aSheet )
4252{
4253 std::vector<EDA_ITEM*> items;
4254
4255 for( FOOTPRINT* fp : aBoard->Footprints() )
4256 {
4257 if( fp->GetSheetname() == aSheet )
4258 items.push_back( fp );
4259 }
4260
4261 return items;
4262}
4263
4264
4265// A part whose every net reaches beyond its channel, such as a decoupler between two rails
4266static FOOTPRINT* railOnlyPart( BOARD* aBoard, const wxString& aSheet )
4267{
4268 std::set<int> outsideNets;
4269
4270 for( FOOTPRINT* fp : aBoard->Footprints() )
4271 {
4272 if( fp->GetSheetname() != aSheet )
4273 {
4274 for( PAD* pad : fp->Pads() )
4275 outsideNets.insert( pad->GetNetCode() );
4276 }
4277 }
4278
4279 for( FOOTPRINT* fp : aBoard->Footprints() )
4280 {
4281 auto outside = [&]( PAD* aPad ) { return outsideNets.contains( aPad->GetNetCode() ); };
4282
4283 if( fp->GetSheetname() == aSheet && std::ranges::all_of( fp->Pads(), outside ) )
4284 {
4285 return fp;
4286 }
4287 }
4288
4289 return nullptr;
4290}
4291
4292
4294{
4296 {
4298 m_toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, new MOCK_TOOLS_HOLDER );
4300 m_toolMgr.RegisterTool( m_tool );
4301 }
4302
4304 {
4305 return std::ranges::count_if( m_tool->GetData()->m_compatMap,
4306 []( const auto& aEntry ) { return aEntry.second.m_isOk; } );
4307 }
4308
4311};
4312
4313
4315{
4316 const wxString ref = wxT( "/io_drivers_pp/bank0/out01/" );
4317 std::vector<EDA_ITEM*> selected = sheetFootprints( m_board.get(), ref );
4318
4319 BOOST_REQUIRE_EQUAL( selected.size(), 11 );
4320
4321 // Channels are found as sheet instances, and one part finds its counterpart in each
4322 m_tool->StartRepeatFromSelection( selected );
4324
4325 for( const auto& [area, compat] : m_tool->GetData()->m_compatMap )
4326 {
4327 BOOST_CHECK_EQUAL( area->m_components.size(), 11 );
4328 BOOST_CHECK( compat.m_unmatched.empty() );
4329 }
4330
4331 std::vector<EDA_ITEM*> few( selected.begin(), selected.begin() + 4 );
4332
4333 m_tool->StartRepeatFromSelection( few );
4335
4336 // Expanding one part reaches the whole sheet instance
4337 std::set<FOOTPRINT*> sheet;
4338
4339 for( EDA_ITEM* item : selected )
4340 sheet.insert( static_cast<FOOTPRINT*>( item ) );
4341
4342 BOOST_CHECK( m_tool->ExpandToMatchingTopology( { *sheet.begin() }, true ) == sheet );
4343
4344 // Linked repeat places every channel part at its anchor counterpart and copies the routing in the area
4345 const size_t areasBefore = std::ranges::count_if(
4346 m_board->Zones(), []( ZONE* aZone ) { return aZone->GetPlacementAreaEnabled(); } );
4347 const size_t tracksBefore = m_board->Tracks().size();
4348
4349 m_tool->StartRepeatFromSelection( selected );
4350 BOOST_REQUIRE_EQUAL( candidates(), 15 );
4351 m_tool->GetData()->m_linkAreas = true;
4352
4353 FOOTPRINT* anchor = nullptr;
4354
4355 for( FOOTPRINT* fp : m_tool->GetData()->m_refRA->m_components )
4356 {
4357 if( fp->m_Uuid == m_tool->GetData()->m_options.m_anchor )
4358 anchor = fp;
4359 }
4360
4362
4363 std::vector<std::pair<FOOTPRINT*, VECTOR2I>> expected;
4364 FOOTPRINT* probeRef = nullptr;
4365 FOOTPRINT* probeTarget = nullptr;
4366
4367 for( const auto& [area, compat] : m_tool->GetData()->m_compatMap )
4368 {
4369 FOOTPRINT* targetAnchor = compat.m_matchingComponents.at( anchor );
4370
4371 if( targetAnchor->GetOrientation() != anchor->GetOrientation() )
4372 continue;
4373
4374 for( const auto& [refFp, targetFp] : compat.m_matchingComponents )
4375 {
4376 expected.emplace_back( targetFp,
4377 targetAnchor->GetPosition() + refFp->GetPosition() - anchor->GetPosition() );
4378
4379 if( !probeRef && refFp != anchor )
4380 {
4381 probeRef = refFp;
4382 probeTarget = targetFp;
4383 }
4384 }
4385 }
4386
4387 BOOST_REQUIRE( probeRef );
4388 BOOST_CHECK_EQUAL( m_tool->ApplyRepeat(), 0 );
4389 BOOST_CHECK_GT( m_board->Tracks().size(), tracksBefore );
4390
4391 for( const auto& [fp, pos] : expected )
4392 BOOST_CHECK_EQUAL( fp->GetPosition(), pos );
4393
4394 std::vector<ZONE*> created;
4395 std::set<KIID> linkSets;
4396 std::set<KIID> anchors;
4397
4398 for( ZONE* zone : m_board->Zones() )
4399 {
4400 if( !zone->GetPlacementAreaEnabled() )
4401 continue;
4402
4403 created.push_back( zone );
4404
4405 if( zone->GetLinkSet() != niluuid )
4406 {
4407 linkSets.insert( zone->GetLinkSet() );
4408 anchors.insert( zone->GetRepeatOptions().m_anchor );
4409 }
4410 }
4411
4412 // Every channel, the reference included, is a peer holding the anchor part of its own
4413 BOOST_CHECK_EQUAL( created.size(), areasBefore + 16 );
4414 BOOST_CHECK_EQUAL( linkSets.size(), 1 );
4415 BOOST_CHECK_EQUAL( anchors.size(), 16 );
4416
4417 // A part dragged out of the reference outline still follows on update
4418 ZONE* refZone = *std::ranges::find_if( created, [&]( ZONE* aZone )
4419 { return aZone->GetRepeatOptions().m_anchor == anchor->m_Uuid; } );
4420
4421 const VECTOR2I outside( refZone->Outline()->BBox().GetRight() + 10000000, probeRef->GetPosition().y );
4422 const VECTOR2I delta = outside - probeRef->GetPosition();
4423 const VECTOR2I targetBefore = probeTarget->GetPosition();
4424
4425 probeRef->SetPosition( outside );
4426 BOOST_CHECK( m_tool->UpdateLinkedLayouts( refZone ) );
4427 BOOST_CHECK_EQUAL( probeTarget->GetPosition(), targetBefore + delta );
4428
4429 // Repeating unlinked areas again replaces what was copied, boundary segments included. The first
4430 // repeats settle the target outlines, after which every repeat starts from identical geometry
4431 m_tool->UnlinkAreas( std::set<ZONE*>( created.begin(), created.end() ) );
4432
4433 auto repeat =
4434 [&]()
4435 {
4436 m_tool->FindExistingRuleAreas();
4437 m_tool->CheckRACompatibility( refZone );
4438
4439 for( auto& [area, compat] : m_tool->GetData()->m_compatMap )
4440 compat.m_doCopy = true;
4441
4442 m_tool->GetData()->m_options.m_connectedRoutingOnly = false;
4443 m_tool->GetData()->m_options.m_groupItems = false;
4444 BOOST_REQUIRE( m_tool->RepeatLayout( TOOL_EVENT(), refZone ) >= 0 );
4445 };
4446
4447 repeat();
4448 repeat();
4449
4450 const size_t tracksAfterSettling = m_board->Tracks().size();
4451
4452 repeat();
4453 BOOST_CHECK_EQUAL( m_board->Tracks().size(), tracksAfterSettling );
4454
4455 // The channel missing a leaf is only offered loosely, and prunes that part from every channel
4456 FOOTPRINT* leaf = railOnlyPart( m_board.get(), wxT( "/io_drivers_pp/bank1/out23/" ) );
4457
4458 BOOST_REQUIRE( leaf );
4459 m_board->Remove( leaf );
4460 delete leaf;
4461
4462 m_tool->StartRepeatFromSelection( sheetFootprints( m_board.get(), ref ) );
4464
4465 m_tool->DiscoverRepeatCandidates( true );
4467 BOOST_CHECK_EQUAL( m_tool->GetData()->m_refRA->m_components.size(), 10 );
4468
4469 // Without symbol paths the channels are found by their connections alone, and expansion steps
4470 // outward by shared nets
4471 for( FOOTPRINT* fp : m_board->Footprints() )
4472 fp->SetPath( KIID_PATH() );
4473
4474 m_tool->StartRepeatFromSelection( sheetFootprints( m_board.get(), ref ) );
4476
4477 m_tool->DiscoverRepeatCandidates( true );
4479 BOOST_CHECK_EQUAL( m_tool->GetData()->m_refRA->m_components.size(), 9 );
4480
4481 FOOTPRINT* seed = nullptr;
4482
4483 for( EDA_ITEM* item : sheetFootprints( m_board.get(), ref ) )
4484 {
4485 if( static_cast<FOOTPRINT*>( item )->GetReference().StartsWith( wxT( "IC" ) ) )
4486 seed = static_cast<FOOTPRINT*>( item );
4487 }
4488
4489 BOOST_REQUIRE( seed );
4490
4491 std::set<FOOTPRINT*> step = m_tool->ExpandToMatchingTopology( { seed }, false );
4492 std::set<int> seedNets;
4493
4494 for( PAD* pad : seed->Pads() )
4495 seedNets.insert( pad->GetNetCode() );
4496
4497 BOOST_CHECK_GT( step.size(), 1 );
4498
4499 for( FOOTPRINT* fp : step )
4500 {
4501 BOOST_CHECK( fp == seed || std::ranges::any_of( fp->Pads(), [&]( PAD* aPad )
4502 { return aPad->GetNetCode() > 0
4503 && seedNets.contains( aPad->GetNetCode() ); } ) );
4504 }
4505}
4506
4507
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
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:85
void SetLocked(bool aLocked) override
Definition board_item.h:427
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:383
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1520
const GENERATORS & Generators() const
Definition board.h:485
const ZONES & Zones() const
Definition board.h:469
const GROUPS & Groups() const
The groups must maintain the following invariants.
Definition board.h:518
const FOOTPRINTS & Footprints() const
Definition board.h:465
static constexpr BOX2< VECTOR2I > ByCorners(const VECTOR2I &aCorner1, const VECTOR2I &aCorner2)
Definition box2.h:67
constexpr const Vec GetEnd() const
Definition box2.h:209
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:165
constexpr const Vec & GetOrigin() const
Definition box2.h:207
constexpr coord_type GetRight() const
Definition box2.h:214
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
Definition commit.h:102
Store all of the related component information found in a netlist.
EDA_ANGLE Normalize()
Definition eda_angle.h:228
double AsDegrees() const
Definition eda_angle.h:115
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:43
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:76
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:116
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
SHAPE_T GetShape() const
Definition eda_shape.h:177
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:283
virtual bool IsVisible() const
Definition eda_text.h:226
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:236
void SetPosition(const VECTOR2I &aPos) override
void SetFPID(const LIB_ID &aFPID)
Definition footprint.h:418
EDA_ANGLE GetOrientation() const
Definition footprint.h:382
void SetPath(const KIID_PATH &aPath)
Definition footprint.h:456
void SetSheetfile(const wxString &aSheetfile)
Definition footprint.h:473
PCB_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this footprint.
std::deque< PAD * > & Pads()
Definition footprint.h:355
void SetReference(const wxString &aReference)
Definition footprint.h:799
void Move(const VECTOR2I &aMoveVector) override
Move this object.
void SetValue(const wxString &aValue)
Definition footprint.h:822
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
void SetSheetname(const wxString &aSheetname)
Definition footprint.h:470
std::vector< FP_3DMODEL > & Models()
Definition footprint.h:375
const wxString & GetReference() const
Definition footprint.h:793
VECTOR2I GetPosition() const override
Definition footprint.h:379
DRAWINGS & GraphicalItems()
Definition footprint.h:358
PAD * FindPadByNumber(const wxString &aPadNumber, PAD *aSearchAfterMe=nullptr) const
Return a PAD with a matching number.
Definition kiid.h:46
wxString AsString() const
Definition kiid.cpp:264
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)
void UnlinkAreas(const std::set< ZONE * > &aZones)
Remove aZones from their link sets, and any area left alone in one, as one undo step.
RULE_AREAS_DATA * GetData()
bool UpdateLinkedLayouts(ZONE *aSource)
Copy aSource onto the other areas of its link set, as one undo step.
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:50
int GetNetCode() const
Definition netinfo.h:104
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
void SetNumber(const wxString &aNumber)
Set the pad number (note that it can be alphanumeric, such as the array reference "AA12").
Definition pad.h:142
void SetLocalSolderMaskMargin(std::optional< int > aMargin)
Definition pad.h:587
void SetLocalZoneConnection(ZONE_CONNECTION aType)
Definition pad.h:610
std::optional< int > GetLocalSolderMaskMargin() const
Definition pad.h:586
ZONE_CONNECTION GetLocalZoneConnection() const
Definition pad.h:611
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:51
void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const override
Invoke a function on all children.
A PCB_IO derivation for saving and loading Pcbnew s-expression formatted files.
void SaveBoard(const wxString &aFileName, BOARD &aBoard, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Write aBoard to a storage file in a format that this PCB_IO implementation knows about or it can be u...
void LoadAndAppendBoard(const wxString &aFileName, BOARD &aAppendToMe, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr)
Same as LoadBoard(), but appends the loaded board to an existing board, which must already exist.
Definition pcb_io.cpp:85
std::unique_ptr< BOARD > LoadBoard(const wxString &aFileName, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr)
Load information from some input file format that this PCB_IO implementation knows about into new BOA...
Definition pcb_io.cpp:72
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:560
void SetPosition(const VECTOR2I &aPos) override
Definition pcb_text.h:103
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:94
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:97
const VECTOR2I & GetStart() const
Definition pcb_track.h:98
virtual void SetWidth(int aWidth)
Definition pcb_track.h:91
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
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
virtual VECTOR2I Centre() const
Compute a center-of-mass of the shape.
Definition shape.h:242
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:832
const REPEAT_LAYOUT_OPTIONS & GetRepeatOptions() const
Definition zone.h:846
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1479
wxString GetPlacementAreaSource() const
Definition zone.h:837
PLACEMENT_SOURCE_T GetPlacementAreaSourceType() const
Definition zone.h:839
SHAPE_POLY_SET * Outline()
Definition zone.h:421
void SetIsRuleArea(bool aEnable)
Definition zone.h:833
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:700
const KIID & GetLinkSet() const
The set of placement rule areas that share one layout, or niluuid when this one is not linked.
Definition zone.h:843
@ DEGREES_T
Definition eda_angle.h:30
@ RECURSE
Definition eda_item.h:51
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
std::string source
KIID niluuid(0)
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)
Get a SHAPE_LINE_CHAIN representing the outline of a box.
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::map< FOOTPRINT *, FOOTPRINT * > COMPONENT_MATCHES
Definition topo_match.h:188
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ RECTANGLE
Definition padstack.h:53
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
Options for copying a placement rule area onto the others linked to it.
KIID m_anchor
This area's own anchor footprint, or niluuid for the area center.
MULTICHANNEL_TOOL * m_tool
std::unordered_map< RULE_AREA *, RULE_AREA_COMPAT_DATA > m_compatMap
REPEAT_LAYOUT_OPTIONS m_options
bool m_linkAreas
Link the ticked targets to the reference.
std::vector< RULE_AREA > m_areas
TMATCH::COMPONENT_MATCHES m_matchingComponents
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:47
std::atomic< int > * m_matchedComponents
Definition topo_match.h:48
std::atomic< int > * m_totalComponents
Definition topo_match.h:49
@ VALUE
Field Value of part, i.e. "3.3K".
static void addTrack(BOARD &aBoard, int aNetCode, PCB_LAYER_ID aLayer, const VECTOR2I &aStart, const VECTOR2I &aEnd)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
static FOOTPRINT * addFootprint(BOARD &aBoard, const wxString &aReference)
std::string path
VECTOR3I expected(15, 30, 45)
VECTOR3I res
int countCopperZonesInArea(BOARD *aBoard, ZONE *aRAZone)
static FOOTPRINT * railOnlyPart(BOARD *aBoard, const wxString &aSheet)
static std::set< FOOTPRINT * > collectFootprints(const PCB_GROUP *aGroup)
RULE_AREA * findGroupRuleAreaByName(MULTICHANNEL_TOOL *aTool, const wxString &aGroupName)
static void checkGroupChannelRepeatLayout(SETTINGS_MANAGER &aSettingsManager, std::unique_ptr< BOARD > &aBoard, bool aGroupItems)
static std::vector< EDA_ITEM * > sheetFootprints(BOARD *aBoard, const wxString &aSheet)
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)
static std::vector< PCB_GROUP * > findGroupsByName(BOARD *aBoard, const wxString &aName)
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)
BOOST_TEST_CONTEXT("Test Clearance")
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:80
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:83
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:84
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:78
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
@ NONE
Pads are not covered.
Definition zones.h:45