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