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 <pcb_group.h>
26#include <pcb_shape.h>
27#include <pcb_track.h>
28#include <pcb_text.h>
29#include <pcb_field.h>
30#include <footprint.h>
31#include <zone.h>
32#include <drc/drc_item.h>
38#include <lib_id.h>
39#include <atomic>
40
48
50{
51public:
54
55 virtual wxWindow* GetToolCanvas() const override { return nullptr; }
56};
57
58BOOST_FIXTURE_TEST_SUITE( MultichannelTool, MULTICHANNEL_TEST_FIXTURE )
59
60RULE_AREA* findRuleAreaByPartialName( MULTICHANNEL_TOOL* aTool, const wxString& aName )
61{
62 for( RULE_AREA& ra : aTool->GetData()->m_areas )
63 {
64 if( ra.m_ruleName.Contains( ( aName ) ) )
65 return &ra;
66 }
67
68 return nullptr;
69}
70
71RULE_AREA* findRuleAreaByPlacementGroup( MULTICHANNEL_TOOL* aTool, const wxString& aGroupName )
72{
73 for( RULE_AREA& ra : aTool->GetData()->m_areas )
74 {
75 if( ra.m_zone && ra.m_zone->GetPlacementAreaSource() == aGroupName )
76 return &ra;
77 }
78
79 return nullptr;
80}
81
82int countZonesByNameInRuleArea( BOARD* aBoard, const wxString& aZoneName, const RULE_AREA& aRuleArea )
83{
84 int count = 0;
85
86 for( const ZONE* zone : aBoard->Zones() )
87 {
88 if( zone == aRuleArea.m_zone )
89 continue;
90
91 if( zone->GetZoneName() != aZoneName )
92 continue;
93
94 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
95 count++;
96 }
97
98 return count;
99}
100
101
102int countZonesByNamePrefixInRuleArea( BOARD* aBoard, const wxString& aBaseName, const RULE_AREA& aRuleArea )
103{
104 int count = 0;
105
106 for( const ZONE* zone : aBoard->Zones() )
107 {
108 if( zone == aRuleArea.m_zone )
109 continue;
110
111 const wxString& name = zone->GetZoneName();
112
113 // A copied zone may get a _<n> suffix for uniqueness (issue 23131).
114 if( name != aBaseName && !name.StartsWith( aBaseName + wxT( "_" ) ) )
115 continue;
116
117 if( aRuleArea.m_zone->Outline()->Contains( zone->Outline()->COutline( 0 ).Centre() ) )
118 count++;
119 }
120
121 return count;
122}
123
124
126{
128
129 std::vector<wxString> tests = { "vme-wren" };
130
131 for( const wxString& relPath : tests )
132 {
133 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
134
135 TOOL_MANAGER toolMgr;
136 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
137
138 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
139
140 MULTICHANNEL_TOOL* mtTool = new MULTICHANNEL_TOOL; // TOOL_MANAGER owns the tools
141 toolMgr.RegisterTool( mtTool );
142
143 //RULE_AREAS_DATA* raData = m_parentTool->GetData();
144
146
147 auto ruleData = mtTool->GetData();
148
149 BOOST_TEST_MESSAGE( wxString::Format( "RA multichannel sheets = %d",
150 static_cast<int>( ruleData->m_areas.size() ) ) );
151
152 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 72 );
153
154 int cnt = 0;
155
156 ruleData->m_replaceExisting = true;
157
158 for( RULE_AREA& ra : ruleData->m_areas )
159 {
160 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" )
161 || ra.m_sheetName == wxT( "pp_driver_2x.kicad_sch" ) )
162 {
163 ra.m_generateEnabled = true;
164 cnt++;
165 }
166 }
167
168 BOOST_TEST_MESSAGE( wxString::Format( "Autogenerating %d RAs", cnt ) );
169
170 TOOL_EVENT dummyEvent;
171
172 mtTool->AutogenerateRuleAreas( dummyEvent );
173 mtTool->FindExistingRuleAreas();
174
175 int n_areas_io = 0, n_areas_pp = 0, n_areas_other = 0;
176
177 BOOST_TEST_MESSAGE( wxString::Format( "Found %d RAs after commit",
178 static_cast<int>(ruleData->m_areas.size() ) ) );
179
180 for( const RULE_AREA& ra : ruleData->m_areas )
181 {
182 BOOST_TEST_MESSAGE( wxString::Format( "SN '%s'", ra.m_ruleName ) );
183
184 if( ra.m_ruleName.Contains( wxT( "io_drivers_fp" ) ) )
185 {
186 n_areas_io++;
187 BOOST_CHECK_EQUAL( ra.m_components.size(), 31 );
188 }
189 else if( ra.m_ruleName.Contains( wxT( "io_drivers_pp" ) ) )
190 {
191 n_areas_pp++;
192 BOOST_CHECK_EQUAL( ra.m_components.size(), 11 );
193 }
194 else
195 {
196 n_areas_other++;
197 }
198 }
199
200 BOOST_TEST_MESSAGE( wxString::Format( "IO areas=%d, PP areas=%d, others=%d",
201 n_areas_io, n_areas_pp, n_areas_other ) );
202
203 BOOST_CHECK_EQUAL( n_areas_io, 16 );
204 BOOST_CHECK_EQUAL( n_areas_pp, 16 );
205 BOOST_CHECK_EQUAL( n_areas_other, 0 );
206
207 const std::vector<wxString> rulesToTest = { wxT( "io_drivers_fp" ),
208 wxT( "io_drivers_pp" ) };
209
210 for( const wxString& ruleName : rulesToTest )
211 {
212 for( const RULE_AREA& refArea : ruleData->m_areas )
213 {
214 if( !refArea.m_ruleName.Contains( ruleName ) )
215 continue;
216
217 BOOST_TEST_MESSAGE( wxString::Format( "REF AREA: '%s'", refArea.m_ruleName ) );
218
219 for( const RULE_AREA& targetArea : ruleData->m_areas )
220 {
221 if( targetArea.m_zone == refArea.m_zone )
222 continue;
223
224 if( !targetArea.m_ruleName.Contains( ruleName ) )
225 continue;
226
227 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea.m_components,
228 targetArea.m_components );
229 auto cgTarget =
230 CONNECTION_GRAPH::BuildFromFootprintSet( targetArea.m_components,
231 refArea.m_components );
232
234
235 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
236 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
237
238 BOOST_TEST_MESSAGE( wxString::Format(
239 "topo match: '%s' [%d] -> '%s' [%d] result %d", refArea.m_ruleName.c_str().AsChar(),
240 static_cast<int>( refArea.m_components.size() ), targetArea.m_ruleName.c_str().AsChar(),
241 static_cast<int>( targetArea.m_components.size() ), status ? 1 : 0 ) );
242
243 for( const auto& iter : result )
244 {
245 BOOST_TEST_MESSAGE( wxString::Format( "%s : %s",
246 iter.second->GetReference(),
247 iter.first->GetReference() ) );
248 }
249
250 BOOST_CHECK( status );
251 BOOST_CHECK( details.empty() );
252 }
253 }
254 }
255
256 auto refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
257
258 BOOST_ASSERT( refArea );
259
260 const std::vector<wxString> targetAreaNames( { wxT( "io_drivers_fp/bank2/io78/" ),
261 wxT( "io_drivers_fp/bank1/io78/" ),
262 wxT( "io_drivers_fp/bank0/io01/" ) } );
263
264 for( const wxString& targetRaName : targetAreaNames )
265 {
266 auto targetRA = findRuleAreaByPartialName( mtTool, targetRaName );
267
268 BOOST_ASSERT( targetRA != nullptr );
269
270 BOOST_TEST_MESSAGE( wxString::Format( "Clone to: %s", targetRA->m_ruleName ) );
271
272 ruleData->m_compatMap[targetRA].m_doCopy = true;
273 }
274
275 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
276
277 BOOST_ASSERT( result >= 0 );
278 }
279}
280
281
286BOOST_FIXTURE_TEST_CASE( RepeatLayoutCopiesFootprintProperties, MULTICHANNEL_TEST_FIXTURE )
287{
288 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
289
290 TOOL_MANAGER toolMgr;
291 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
292
293 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
294
296 toolMgr.RegisterTool( mtTool );
297
298 mtTool->FindExistingRuleAreas();
299
300 auto ruleData = mtTool->GetData();
301
302 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
303 static_cast<int>( ruleData->m_areas.size() ) ) );
304
305 BOOST_CHECK( ruleData->m_areas.size() >= 2 );
306
307 if( ruleData->m_areas.size() < 2 )
308 return;
309
310 RULE_AREA* refArea = nullptr;
311 RULE_AREA* targetArea = nullptr;
312
313 for( RULE_AREA& ra : ruleData->m_areas )
314 {
315 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
316 refArea = &ra;
317 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
318 targetArea = &ra;
319 }
320
321 if( !refArea || !targetArea )
322 {
323 BOOST_TEST_MESSAGE( "Could not find Untitled Sheet and Untitled Sheet1 rule areas, skipping test" );
324 return;
325 }
326
327 BOOST_TEST_MESSAGE( wxString::Format( "Reference area: %s, Target area: %s",
328 refArea->m_ruleName, targetArea->m_ruleName ) );
329
330 FOOTPRINT* refFP = nullptr;
331 FOOTPRINT* targetFP = nullptr;
332
333 for( FOOTPRINT* fp : refArea->m_components )
334 {
335 if( fp->GetReference().StartsWith( wxT( "U1" ) ) )
336 {
337 refFP = fp;
338 break;
339 }
340 }
341
342 for( FOOTPRINT* fp : targetArea->m_components )
343 {
344 if( fp->GetReference().StartsWith( wxT( "U2" ) ) )
345 {
346 targetFP = fp;
347 break;
348 }
349 }
350
351 if( !refFP || !targetFP )
352 {
353 BOOST_TEST_MESSAGE( "Could not find matching footprints in the rule areas, skipping test" );
354 return;
355 }
356
357 PCB_FIELD* refValueField = refFP->GetField( FIELD_T::VALUE );
358 bool refValueVisible = refValueField ? refValueField->IsVisible() : true;
359
360 std::vector<FP_3DMODEL> refModels = refFP->Models();
361
362 mtTool->CheckRACompatibility( refArea->m_zone );
363
364 ruleData->m_compatMap[targetArea].m_doCopy = true;
365 ruleData->m_options.m_copyPlacement = true;
366
367 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
368
369 BOOST_CHECK( result >= 0 );
370
371 PCB_FIELD* targetValueField = targetFP->GetField( FIELD_T::VALUE );
372
373 if( targetValueField && refValueField )
374 {
375 BOOST_CHECK_EQUAL( targetValueField->IsVisible(), refValueVisible );
376 BOOST_TEST_MESSAGE( wxString::Format( "Value field visibility: ref=%d, target=%d",
377 refValueVisible, targetValueField->IsVisible() ) );
378 }
379
380 BOOST_CHECK_EQUAL( targetFP->Models().size(), refModels.size() );
381
382 if( !refModels.empty() )
383 {
384 BOOST_TEST_MESSAGE( wxString::Format( "3D models: ref=%d, target=%d",
385 static_cast<int>( refModels.size() ),
386 static_cast<int>( targetFP->Models().size() ) ) );
387 }
388}
389
390
398BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotRemoveReferenceVias, MULTICHANNEL_TEST_FIXTURE )
399{
400 KI_TEST::LoadBoard( m_settingsManager, "issue21184/issue21184", m_board );
401
402 TOOL_MANAGER toolMgr;
403 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
404
405 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
406
408 toolMgr.RegisterTool( mtTool );
409
410 mtTool->FindExistingRuleAreas();
411
412 auto ruleData = mtTool->GetData();
413
414 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
415 static_cast<int>( ruleData->m_areas.size() ) ) );
416
417 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 2 );
418
419 if( ruleData->m_areas.size() < 2 )
420 return;
421
422 RULE_AREA* refArea = nullptr;
423 RULE_AREA* targetArea = nullptr;
424
425 for( RULE_AREA& ra : ruleData->m_areas )
426 {
427 if( ra.m_ruleName == wxT( "Test1" ) )
428 refArea = &ra;
429 else if( ra.m_ruleName == wxT( "Test2" ) )
430 targetArea = &ra;
431 }
432
433 BOOST_REQUIRE( refArea != nullptr );
434 BOOST_REQUIRE( targetArea != nullptr );
435
436 int refViaCountBefore = 0;
437
438 for( PCB_TRACK* track : m_board->Tracks() )
439 {
440 if( track->Type() == PCB_VIA_T )
441 {
442 PCB_VIA* via = static_cast<PCB_VIA*>( track );
443 VECTOR2I viaPos = via->GetPosition();
444
445 if( refArea->m_zone->Outline()->Contains( viaPos ) )
446 refViaCountBefore++;
447 }
448 }
449
450 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias before repeat: %d", refViaCountBefore ) );
451 BOOST_CHECK( refViaCountBefore > 0 );
452
453 mtTool->CheckRACompatibility( refArea->m_zone );
454
455 ruleData->m_compatMap[targetArea].m_doCopy = true;
456 ruleData->m_options.m_copyPlacement = true;
457 ruleData->m_options.m_copyRouting = true;
458
459 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
460
461 BOOST_CHECK( result >= 0 );
462
463 int refViaCountAfter = 0;
464
465 for( PCB_TRACK* track : m_board->Tracks() )
466 {
467 if( track->Type() == PCB_VIA_T )
468 {
469 PCB_VIA* via = static_cast<PCB_VIA*>( track );
470 VECTOR2I viaPos = via->GetPosition();
471
472 if( refArea->m_zone->Outline()->Contains( viaPos ) )
473 refViaCountAfter++;
474 }
475 }
476
477 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias after repeat: %d", refViaCountAfter ) );
478
479 BOOST_CHECK_EQUAL( refViaCountAfter, refViaCountBefore );
480}
481
482
487BOOST_FIXTURE_TEST_CASE( RepeatLayoutRespectsZoneLayerSetsForOtherItems, MULTICHANNEL_TEST_FIXTURE )
488{
489 KI_TEST::LoadBoard( m_settingsManager, "issue22983/issue22983", m_board );
490
491 TOOL_MANAGER toolMgr;
492 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
493
494 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
495
497 toolMgr.RegisterTool( mtTool );
498
499 mtTool->FindExistingRuleAreas();
500
501 RULE_AREA* sourceA = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceA" ) );
502 RULE_AREA* destA = findRuleAreaByPlacementGroup( mtTool, wxT( "DestA" ) );
503 RULE_AREA* sourceB = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceB" ) );
504 RULE_AREA* destB = findRuleAreaByPlacementGroup( mtTool, wxT( "DestB" ) );
505
506 BOOST_REQUIRE( sourceA != nullptr );
507 BOOST_REQUIRE( destA != nullptr );
508 BOOST_REQUIRE( sourceB != nullptr );
509 BOOST_REQUIRE( destB != nullptr );
510
511 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *sourceA ), 1 );
512 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ), 0 );
514 countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *sourceB ), 1 );
515 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
516 0 );
517 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *sourceB ), 1 );
518 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
519
520 REPEAT_LAYOUT_OPTIONS options;
521 options.m_copyPlacement = false;
522 options.m_copyRouting = false;
523 options.m_copyOtherItems = true;
524 options.m_includeLockedItems = true;
525
526 int copyAStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceA, *destA, options );
527 BOOST_REQUIRE( copyAStatus >= 0 );
528
529 int copyBStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceB, *destB, options );
530 BOOST_REQUIRE( copyBStatus >= 0 );
531
532 // SourceA and DestA both include F.Cu+B.Cu, so this multilayer zone should copy.
533 // The copy is renamed for uniqueness (issue 23131), so match the base name as a prefix.
534 BOOST_CHECK_EQUAL( countZonesByNamePrefixInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ),
535 1 );
536
537 // SourceB only includes F.Cu, so this F.Cu+B.Cu zone should not copy to DestB.
538 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
539 0 );
540
541 // SourceB excludes B.Cu, so this B.Cu-only zone should not copy either.
542 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
543}
544
545
554{
556 using TMATCH::COMPONENT;
557
558 // Create two connection graphs with components that have dotted reference designators
559 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
560 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
561
562 // Create mock footprints with the same FPID
563 LIB_ID fpid( wxT( "Package_SO" ), wxT( "SOIC-8_3.9x4.9mm_P1.27mm" ) );
564
565 // Create reference footprint TRIM_1.1 and target footprint TRIM_2.1
566 FOOTPRINT fpRef( nullptr );
567 fpRef.SetFPID( fpid );
568 fpRef.SetReference( wxT( "TRIM_1.1" ) );
569
570 FOOTPRINT fpTarget( nullptr );
571 fpTarget.SetFPID( fpid );
572 fpTarget.SetReference( wxT( "TRIM_2.1" ) );
573
574 // Create matching pad structures
575 PAD padRef1( &fpRef );
576 padRef1.SetNumber( wxT( "1" ) );
577 padRef1.SetNetCode( 1 );
578 fpRef.Add( &padRef1 );
579
580 PAD padRef2( &fpRef );
581 padRef2.SetNumber( wxT( "2" ) );
582 padRef2.SetNetCode( 2 );
583 fpRef.Add( &padRef2 );
584
585 PAD padTarget1( &fpTarget );
586 padTarget1.SetNumber( wxT( "1" ) );
587 padTarget1.SetNetCode( 3 );
588 fpTarget.Add( &padTarget1 );
589
590 PAD padTarget2( &fpTarget );
591 padTarget2.SetNumber( wxT( "2" ) );
592 padTarget2.SetNetCode( 4 );
593 fpTarget.Add( &padTarget2 );
594
595 // Build connection graphs
596 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
597 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
598
599 cgRef->BuildConnectivity();
600 cgTarget->BuildConnectivity();
601
602 // Check that the components are considered the same kind
603 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
604 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
605
606 COMPONENT* cmpRef = cgRef->Components()[0];
607 COMPONENT* cmpTarget = cgTarget->Components()[0];
608
609 bool sameKind = cmpRef->IsSameKind( *cmpTarget );
610
611 BOOST_TEST_MESSAGE( wxString::Format( "TRIM_1.1 and TRIM_2.1 IsSameKind: %d", sameKind ? 1 : 0 ) );
612 BOOST_CHECK( sameKind );
613
614 // Test topology matching
616 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
617 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
618
619 BOOST_TEST_MESSAGE( wxString::Format( "Topology match result: %d", status ? 1 : 0 ) );
620
621 if( !status && !details.empty() )
622 {
623 for( const auto& reason : details )
624 {
625 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
626 reason.m_reference, reason.m_candidate, reason.m_reason ) );
627 }
628 }
629
630 BOOST_CHECK( status );
631 BOOST_CHECK( details.empty() );
632
633 // Cleanup: remove pads before footprints go out of scope
634 fpRef.Pads().clear();
635 fpTarget.Pads().clear();
636}
637
638
647BOOST_FIXTURE_TEST_CASE( GenerateRuleAreasIncludesChildSheets, MULTICHANNEL_TEST_FIXTURE )
648{
649 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
650
651 TOOL_MANAGER toolMgr;
652 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
653
654 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
655
657 toolMgr.RegisterTool( mtTool );
658
660
661 auto ruleData = mtTool->GetData();
662
663 RULE_AREA* leafArea = nullptr;
664 RULE_AREA* midArea = nullptr;
665 RULE_AREA* topArea = nullptr;
666
667 for( RULE_AREA& ra : ruleData->m_areas )
668 {
669 if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/io01/" ) )
670 leafArea = &ra;
671 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/" ) )
672 midArea = &ra;
673 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/" ) )
674 topArea = &ra;
675 }
676
677 BOOST_REQUIRE( leafArea != nullptr );
678 BOOST_REQUIRE( midArea != nullptr );
679 BOOST_REQUIRE( topArea != nullptr );
680
681 BOOST_TEST_MESSAGE( wxString::Format( "Leaf /io_drivers_fp/bank0/io01/ components: %d",
682 static_cast<int>( leafArea->m_components.size() ) ) );
683 BOOST_TEST_MESSAGE( wxString::Format( "Mid /io_drivers_fp/bank0/ components: %d",
684 static_cast<int>( midArea->m_components.size() ) ) );
685 BOOST_TEST_MESSAGE( wxString::Format( "Top /io_drivers_fp/ components: %d",
686 static_cast<int>( topArea->m_components.size() ) ) );
687
688 // Leaf sheet has 31 direct components and no children
689 BOOST_CHECK_EQUAL( leafArea->m_components.size(), 31 );
690
691 // Mid-level sheet has 7 direct + 4 child sheets * 31 each = 131
692 BOOST_CHECK_EQUAL( midArea->m_components.size(), 131 );
693
694 // Top-level sheet has 3 direct + 4 banks * 131 each = 527
695 BOOST_CHECK_EQUAL( topArea->m_components.size(), 527 );
696
697 // Mid-level components must be a superset of leaf components
698 for( FOOTPRINT* fp : leafArea->m_components )
699 BOOST_CHECK( midArea->m_components.count( fp ) > 0 );
700
701 // Top-level components must be a superset of mid-level components
702 for( FOOTPRINT* fp : midArea->m_components )
703 BOOST_CHECK( topArea->m_components.count( fp ) > 0 );
704}
705
706
712{
714
715 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
716
717 TOOL_MANAGER toolMgr;
718 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
719
720 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
721
723 toolMgr.RegisterTool( mtTool );
724
726
727 auto ruleData = mtTool->GetData();
728
729 ruleData->m_replaceExisting = true;
730
731 for( RULE_AREA& ra : ruleData->m_areas )
732 {
733 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" ) )
734 ra.m_generateEnabled = true;
735 }
736
737 TOOL_EVENT dummyEvent;
738 mtTool->AutogenerateRuleAreas( dummyEvent );
739 mtTool->FindExistingRuleAreas();
740
741 RULE_AREA* refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
742 RULE_AREA* targetArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank2/io78/" ) );
743
744 BOOST_REQUIRE( refArea != nullptr );
745 BOOST_REQUIRE( targetArea != nullptr );
746
747 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
748 targetArea->m_components );
749 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
750 refArea->m_components );
751
752 // Pre-cancelled: should return false immediately with empty result
753 {
754 std::atomic<bool> cancelled( true );
755 std::atomic<int> matched( 0 );
756 std::atomic<int> total( 0 );
757
759 params.m_cancelled = &cancelled;
760 params.m_matchedComponents = &matched;
761 params.m_totalComponents = &total;
762
764 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
765
766 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
767
768 BOOST_CHECK( !status );
769 BOOST_CHECK( result.empty() );
770
771 BOOST_TEST_MESSAGE( "Pre-cancelled FindIsomorphism correctly returned false" );
772 }
773
774 // Normal run with progress reporting
775 {
776 std::atomic<bool> cancelled( false );
777 std::atomic<int> matched( 0 );
778 std::atomic<int> total( 0 );
779
781 params.m_cancelled = &cancelled;
782 params.m_matchedComponents = &matched;
783 params.m_totalComponents = &total;
784
786 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
787
788 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
789
790 BOOST_CHECK( status );
791
792 int finalMatched = matched.load();
793 int finalTotal = total.load();
794
795 BOOST_TEST_MESSAGE( wxString::Format( "Progress: matched=%d, total=%d", finalMatched, finalTotal ) );
796
797 BOOST_CHECK( finalTotal > 0 );
798 BOOST_CHECK_EQUAL( finalMatched, finalTotal );
799 }
800
801 // Sanity check: same graphs without params still succeed
802 {
804 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
805
806 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
807
808 BOOST_CHECK( status );
809 BOOST_CHECK( !result.empty() );
810
811 BOOST_TEST_MESSAGE( "Default params FindIsomorphism still succeeds" );
812 }
813}
814
815
826BOOST_FIXTURE_TEST_CASE( TopoMatchGlobalNetHierarchicalPins, MULTICHANNEL_TEST_FIXTURE )
827{
829
830 KI_TEST::LoadBoard( m_settingsManager, "issue21739/topology_mismatch", m_board );
831
832 TOOL_MANAGER toolMgr;
833 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
834
835 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
836
838 toolMgr.RegisterTool( mtTool );
839
840 mtTool->FindExistingRuleAreas();
841
842 auto ruleData = mtTool->GetData();
843
844 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
845 static_cast<int>( ruleData->m_areas.size() ) ) );
846
847 BOOST_REQUIRE( ruleData->m_areas.size() >= 2 );
848
849 RULE_AREA* ch0Area = nullptr;
850 RULE_AREA* ch1Area = nullptr;
851
852 for( RULE_AREA& ra : ruleData->m_areas )
853 {
854 if( !ra.m_zone )
855 continue;
856
857 wxString source = ra.m_zone->GetPlacementAreaSource();
858
859 if( source == wxT( "/i2c_thingy_ch0/" ) )
860 ch0Area = &ra;
861 else if( source == wxT( "/i2c_thingy_ch1/" ) )
862 ch1Area = &ra;
863 }
864
865 BOOST_REQUIRE_MESSAGE( ch0Area != nullptr, "Could not find i2c_thingy_ch0 rule area" );
866 BOOST_REQUIRE_MESSAGE( ch1Area != nullptr, "Could not find i2c_thingy_ch1 rule area" );
867
868 BOOST_TEST_MESSAGE( wxString::Format( "ch0 components: %d, ch1 components: %d",
869 static_cast<int>( ch0Area->m_components.size() ),
870 static_cast<int>( ch1Area->m_components.size() ) ) );
871
872 BOOST_CHECK_EQUAL( ch0Area->m_components.size(), ch1Area->m_components.size() );
873
874 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( ch0Area->m_components,
875 ch1Area->m_components );
876 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( ch1Area->m_components,
877 ch0Area->m_components );
878
880 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
881 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
882
883 if( !status )
884 {
885 for( const auto& reason : details )
886 {
887 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
888 reason.m_reference, reason.m_candidate,
889 reason.m_reason ) );
890 }
891 }
892
893 BOOST_CHECK_MESSAGE( status,
894 "Topology match failed for channels with hierarchical pins "
895 "tied to global nets (issue 21739)" );
896}
897
898
905BOOST_FIXTURE_TEST_CASE( TopoMatchBoundarySignalNetNotExcluded, MULTICHANNEL_TEST_FIXTURE )
906{
908
909 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
910
911 // The board forces consecutive net codes, so register by name and use the assigned code.
912 auto addNet = [&]( const wxString& aName ) -> int
913 {
914 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
915 board->Add( net );
916 return net->GetNetCode();
917 };
918
919 const int netGnd = addNet( wxT( "GND" ) );
920 const int netVcc = addNet( wxT( "+5VP" ) );
921 const int netChainInRef = addNet( wxT( "Net-(D-RefChainIn)" ) );
922 const int netBridge = addNet( wxT( "Net-(D38-DOUT)" ) ); // reference output AND target input
923 const int netChainOutTgt = addNet( wxT( "Net-(D43-DOUT)" ) );
924 const int netD2Dout = addNet( wxT( "unconnected-(D2-DOUT)" ) );
925 const int netD3Dout = addNet( wxT( "unconnected-(D3-DOUT)" ) );
926 const int netD5Dout = addNet( wxT( "unconnected-(D5-DOUT)" ) );
927 const int netD6Dout = addNet( wxT( "unconnected-(D6-DOUT)" ) );
928
929 LIB_ID ledId( wxT( "TestLib" ), wxT( "WS2812" ) );
930
931 // Four-pad addressable LED: pad 1 = DOUT, pad 2 = GND, pad 3 = DIN, pad 4 = VCC.
932 auto makeLed = [&]( const wxString& aRef, int aDout, int aDin ) -> FOOTPRINT*
933 {
934 FOOTPRINT* fp = new FOOTPRINT( board.get() );
935 fp->SetFPID( ledId );
936 fp->SetReference( aRef );
937 board->Add( fp );
938
939 auto addPad = [&]( const wxString& aNumber, int aNetCode )
940 {
941 PAD* pad = new PAD( fp );
942 pad->SetNumber( aNumber );
944 pad->SetSize( PADSTACK::ALL_LAYERS,
945 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
946 pad->SetLayerSet( LSET( { F_Cu } ) );
947 pad->SetNetCode( aNetCode );
948 fp->Add( pad );
949 };
950
951 addPad( wxT( "1" ), aDout );
952 addPad( wxT( "2" ), netGnd );
953 addPad( wxT( "3" ), aDin );
954 addPad( wxT( "4" ), netVcc );
955
956 return fp;
957 };
958
959 // Reference area (design block source instance): three LEDs share the DIN rail; only the
960 // representative LED drives the chain output (the bridge net), the other two are unconnected.
961 std::set<FOOTPRINT*> refFps;
962 refFps.insert( makeLed( wxT( "D1" ), netBridge, netChainInRef ) );
963 refFps.insert( makeLed( wxT( "D2" ), netD2Dout, netChainInRef ) );
964 refFps.insert( makeLed( wxT( "D3" ), netD3Dout, netChainInRef ) );
965
966 // Target area (downstream instance): the three LEDs' DIN rail IS the bridge net coming from
967 // the design block source, and the representative LED drives a fresh chain output.
968 std::set<FOOTPRINT*> tgtFps;
969 tgtFps.insert( makeLed( wxT( "D4" ), netChainOutTgt, netBridge ) );
970 tgtFps.insert( makeLed( wxT( "D5" ), netD5Dout, netBridge ) );
971 tgtFps.insert( makeLed( wxT( "D6" ), netD6Dout, netBridge ) );
972
973 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
974 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
975
977 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
978 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
979
980 if( !status )
981 {
982 for( const auto& reason : details )
983 {
984 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
985 reason.m_reference, reason.m_candidate,
986 reason.m_reason ) );
987 }
988 }
989
990 BOOST_CHECK_MESSAGE( status,
991 "Topology match failed because the design-block boundary signal net "
992 "was misclassified as a global rail and excluded asymmetrically" );
993 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
994}
995
996
1006BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutCopiesSilkscreen, MULTICHANNEL_TEST_FIXTURE )
1007{
1008 m_board = std::make_unique<BOARD>();
1009 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1010
1011 // Net for the single connection between the two footprints in each block.
1012 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1013 m_board->Add( net );
1014
1015 auto makeFootprint =
1016 [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1017 {
1018 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1019 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
1020 fp->SetReference( aRef );
1021 fp->SetPosition( aPos );
1022
1023 PAD* pad = new PAD( fp );
1024 pad->SetNumber( wxT( "1" ) );
1025 pad->SetNet( net );
1026 pad->SetPosition( aPos );
1027 pad->SetSize( F_Cu,
1028 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1029 pad->SetLayerSet( LSET( { F_Cu } ) );
1030 fp->Add( pad );
1031
1032 m_board->Add( fp );
1033 return fp;
1034 };
1035
1036 // Source: a "design block" with two matched footprints and a silkscreen rectangle that
1037 // is not associated with any footprint.
1038 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ),
1039 VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
1040 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ),
1041 VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ) );
1042
1043 PCB_SHAPE* silkRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1044 silkRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1045 silkRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 12 ), pcbIUScale.mmToIU( 2 ) ) );
1046 silkRect->SetLayer( F_SilkS );
1047 silkRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1048 m_board->Add( silkRect );
1049
1050 // Destination: a group containing the matched pair so RepeatLayout can find a parent group.
1051 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ),
1052 VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1053 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ),
1054 VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1055
1056 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1057 destGroup->SetName( wxT( "design-block-dest" ) );
1058 destGroup->AddItem( destFp1 );
1059 destGroup->AddItem( destFp2 );
1060 m_board->Add( destGroup );
1061
1062 // Unrelated silkscreen drawing inside the destination bounding box but not part of the
1063 // destination group. Apply Design Block Layout must not delete or claim this item.
1064 PCB_SHAPE* unrelatedSilk = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1065 unrelatedSilk->SetStart( VECTOR2I( pcbIUScale.mmToIU( 52 ), pcbIUScale.mmToIU( 52 ) ) );
1066 unrelatedSilk->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 58 ), pcbIUScale.mmToIU( 52 ) ) );
1067 unrelatedSilk->SetLayer( F_SilkS );
1068 unrelatedSilk->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1069 m_board->Add( unrelatedSilk );
1070
1071 int silkBefore = 0;
1072
1073 for( BOARD_ITEM* item : m_board->Drawings() )
1074 {
1075 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1076 silkBefore++;
1077 }
1078
1079 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1080
1081 RULE_AREA dbRA;
1083 dbRA.m_components.insert( refFp1 );
1084 dbRA.m_components.insert( refFp2 );
1085 dbRA.m_designBlockItems.insert( refFp1 );
1086 dbRA.m_designBlockItems.insert( refFp2 );
1087 dbRA.m_designBlockItems.insert( silkRect );
1088
1089 // The Apply Design Block flow uses a synthetic copper-only rule area zone. The destination
1090 // zone is a temporary zone never added to the board.
1091 dbRA.m_zone = new ZONE( m_board.get() );
1092 dbRA.m_zone->SetIsRuleArea( true );
1095 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1096 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1097
1098 RULE_AREA destRA;
1100 destRA.m_components.insert( destFp1 );
1101 destRA.m_components.insert( destFp2 );
1102
1103 destRA.m_zone = new ZONE( m_board.get() );
1104 destRA.m_zone->SetIsRuleArea( true );
1105 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1107 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1108 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1109
1110 TOOL_MANAGER toolMgr;
1111 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1112 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1113
1115 toolMgr.RegisterTool( mtTool );
1116
1117 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1118 .m_connectedRoutingOnly = false,
1119 .m_copyPlacement = true,
1120 .m_copyOtherItems = true,
1121 .m_groupItems = false,
1122 .m_includeLockedItems = true,
1123 .m_anchorFp = nullptr };
1124
1125 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1126 BOOST_CHECK_MESSAGE( result >= 0, "RepeatLayout failed" );
1127
1128 delete dbRA.m_zone;
1129 delete destRA.m_zone;
1130
1131 // Verify a third silkscreen shape (the duplicated rectangle) was added and grouped under
1132 // the destination group, that the unrelated silk segment was preserved, and that the
1133 // duplicated rectangle landed inside the destination region.
1134 int silkAfter = 0;
1135 int silkInGroup = 0;
1136 bool unrelatedSurvived = false;
1137 PCB_SHAPE* copiedRect = nullptr;
1138
1139 for( BOARD_ITEM* item : m_board->Drawings() )
1140 {
1141 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1142 continue;
1143
1144 silkAfter++;
1145
1146 if( item == unrelatedSilk )
1147 unrelatedSurvived = true;
1148
1149 if( item->GetParentGroup() == destGroup )
1150 {
1151 silkInGroup++;
1152
1153 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1154
1155 if( shape->GetShape() == SHAPE_T::RECTANGLE )
1156 copiedRect = shape;
1157 }
1158 }
1159
1160 BOOST_CHECK_MESSAGE( silkAfter == 3,
1161 wxString::Format( "Expected 3 silkscreen shapes after Apply Design Block "
1162 "Layout (2 original + 1 copy), found %d (issue 24372)",
1163 silkAfter ) );
1164 BOOST_CHECK_MESSAGE( silkInGroup == 1,
1165 wxString::Format( "Expected 1 silkscreen shape in destination group, "
1166 "found %d (issue 24372)",
1167 silkInGroup ) );
1168 BOOST_CHECK_MESSAGE( unrelatedSurvived,
1169 "Unrelated silkscreen drawing outside the design block group was "
1170 "deleted by Apply Design Block Layout (issue 24372)" );
1171 BOOST_REQUIRE( copiedRect != nullptr );
1172
1173 // The copied rectangle should sit near the destination footprints (offset by ~50mm from
1174 // the source position), not at the original source location.
1175 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1176}
1177
1178
1184BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutMatchesBySymbolPathWhenTopologyDiffers, MULTICHANNEL_TEST_FIXTURE )
1185{
1187
1188 m_board = std::make_unique<BOARD>();
1189 m_board->SetEnabledLayers( LSET::AllCuMask() );
1190
1191 NETINFO_ITEM* shared = new NETINFO_ITEM( m_board.get(), wxT( "Net-(J1-Pin_1)" ) );
1192 m_board->Add( shared );
1193
1194 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos, const KIID& aSymbolUuid,
1195 NETINFO_ITEM* aNet ) -> FOOTPRINT*
1196 {
1197 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1198 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "Receptacle" ) ) );
1199 fp->SetReference( aRef );
1200 fp->SetPosition( aPos );
1201
1202 // Symbol instance UUID, the link between a block footprint and its placed instance
1204 path.push_back( aSymbolUuid );
1205 fp->SetPath( path );
1206
1207 PAD* pad = new PAD( fp );
1208 pad->SetNumber( wxT( "1" ) );
1210 pad->SetPosition( aPos );
1211 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1212 pad->SetLayerSet( LSET( { F_Cu } ) );
1213
1214 if( aNet )
1215 pad->SetNet( aNet );
1216
1217 fp->Add( pad );
1218 m_board->Add( fp );
1219 return fp;
1220 };
1221
1222 KIID symA, symB;
1223
1224 // Block source: both receptacles unconnected, so each pad has zero connections
1225 FOOTPRINT* refFpA = makeFootprint( wxT( "J5" ), VECTOR2I( 0, 0 ), symA, nullptr );
1226 FOOTPRINT* refFpB = makeFootprint( wxT( "J6" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), symB, nullptr );
1227
1228 // Board instance: same symbol instances, but a board wire ties both pads onto one net
1229 FOOTPRINT* destFpA =
1230 makeFootprint( wxT( "J1" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ), symA, shared );
1231 FOOTPRINT* destFpB =
1232 makeFootprint( wxT( "J2" ), VECTOR2I( pcbIUScale.mmToIU( 80 ), pcbIUScale.mmToIU( 80 ) ), symB, shared );
1233
1234 // Precondition: topology matching fails, so the symbol path fallback is what rescues the apply
1235 {
1236 std::set<FOOTPRINT*> refSet{ refFpA, refFpB };
1237 std::set<FOOTPRINT*> destSet{ destFpA, destFpB };
1238 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refSet, destSet );
1239 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( destSet, refSet );
1241 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1242 BOOST_REQUIRE_MESSAGE( !cgRef->FindIsomorphism( cgTarget.get(), topoOnly, details ),
1243 "Test precondition broken: areas are net isomorphic" );
1244 }
1245
1246 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1247 destGroup->SetName( wxT( "design-block-dest" ) );
1248 destGroup->AddItem( destFpA );
1249 destGroup->AddItem( destFpB );
1250 m_board->Add( destGroup );
1251
1252 RULE_AREA dbRA;
1254 dbRA.m_components.insert( refFpA );
1255 dbRA.m_components.insert( refFpB );
1256 dbRA.m_designBlockItems.insert( refFpA );
1257 dbRA.m_designBlockItems.insert( refFpB );
1258 dbRA.m_zone = new ZONE( m_board.get() );
1259 dbRA.m_zone->SetIsRuleArea( true );
1261 dbRA.m_zone->AddPolygon(
1263 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1264
1265 RULE_AREA destRA;
1267 destRA.m_components.insert( destFpA );
1268 destRA.m_components.insert( destFpB );
1269 destRA.m_group = destGroup;
1270 destRA.m_zone = new ZONE( m_board.get() );
1271 destRA.m_zone->SetIsRuleArea( true );
1272 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1274 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 40 ), pcbIUScale.mmToIU( 40 ) ),
1275 VECTOR2I( pcbIUScale.mmToIU( 90 ), pcbIUScale.mmToIU( 90 ) ) ) ) );
1276
1277 TOOL_MANAGER toolMgr;
1278 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1279 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1281 toolMgr.RegisterTool( mtTool );
1282
1283 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1284 .m_connectedRoutingOnly = false,
1285 .m_copyPlacement = true,
1286 .m_copyOtherItems = true,
1287 .m_groupItems = false,
1288 .m_includeLockedItems = true,
1289 .m_anchorFp = nullptr };
1290
1291 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1292
1293 delete dbRA.m_zone;
1294 delete destRA.m_zone;
1295
1296 BOOST_CHECK_MESSAGE( result >= 0, "Apply Design Block Layout aborted even though the symbol instance paths "
1297 "give an unambiguous mapping (No compatible component regression)" );
1298
1299 // Correct pairing (J5->J1, J6->J2) reproduces the source 10mm spacing, a swap would give -10mm
1300 VECTOR2I delta = destFpB->GetPosition() - destFpA->GetPosition();
1301 BOOST_CHECK_MESSAGE( delta == VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ),
1302 wxString::Format( "Destination relative placement wrong, expected "
1303 "(10mm,0) got (%d,%d) IU",
1304 delta.x, delta.y ) );
1305}
1306
1307
1316BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsDuplicateSilkText, MULTICHANNEL_TEST_FIXTURE )
1317{
1318 m_board = std::make_unique<BOARD>();
1319 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1320
1321 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1322 m_board->Add( net );
1323
1324 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1325 {
1326 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1327 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "SW" ) ) );
1328 fp->SetReference( aRef );
1329 fp->SetPosition( aPos );
1330
1331 // Two silkscreen texts with the same content, one on each board side.
1332 for( PCB_LAYER_ID layer : { F_SilkS, B_SilkS } )
1333 {
1334 PCB_TEXT* txt = new PCB_TEXT( fp );
1335 txt->SetText( wxT( "${REFERENCE}" ) );
1336 txt->SetLayer( layer );
1337 txt->SetPosition( aPos + VECTOR2I( 0, pcbIUScale.mmToIU( -8 ) ) );
1338 fp->Add( txt );
1339 }
1340
1341 PAD* pad = new PAD( fp );
1342 pad->SetNumber( wxT( "1" ) );
1343 pad->SetNet( net );
1344 pad->SetPosition( aPos );
1345 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1346 pad->SetLayerSet( LSET( { F_Cu } ) );
1347 fp->Add( pad );
1348
1349 m_board->Add( fp );
1350 return fp;
1351 };
1352
1353 auto countSilkText = [&]( FOOTPRINT* fp, PCB_LAYER_ID layer ) -> int
1354 {
1355 int count = 0;
1356
1357 for( BOARD_ITEM* item : fp->GraphicalItems() )
1358 {
1359 if( item->Type() == PCB_TEXT_T && item->GetLayer() == layer )
1360 count++;
1361 }
1362
1363 return count;
1364 };
1365
1366 FOOTPRINT* refFp1 = makeFootprint( wxT( "SW1" ), VECTOR2I( 0, 0 ) );
1367 FOOTPRINT* refFp2 = makeFootprint( wxT( "SW2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1368
1369 FOOTPRINT* destFp1 = makeFootprint( wxT( "SW3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1370 FOOTPRINT* destFp2 = makeFootprint( wxT( "SW4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1371
1372 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1373 destGroup->SetName( wxT( "design-block-dest" ) );
1374 destGroup->AddItem( destFp1 );
1375 destGroup->AddItem( destFp2 );
1376 m_board->Add( destGroup );
1377
1378 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, F_SilkS ), 1 );
1379 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, B_SilkS ), 1 );
1380
1381 RULE_AREA dbRA;
1383 dbRA.m_components.insert( refFp1 );
1384 dbRA.m_components.insert( refFp2 );
1385 dbRA.m_designBlockItems.insert( refFp1 );
1386 dbRA.m_designBlockItems.insert( refFp2 );
1387
1388 dbRA.m_zone = new ZONE( m_board.get() );
1389 dbRA.m_zone->SetIsRuleArea( true );
1391 dbRA.m_zone->AddPolygon(
1393 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1394
1395 RULE_AREA destRA;
1397 destRA.m_components.insert( destFp1 );
1398 destRA.m_components.insert( destFp2 );
1399
1400 destRA.m_zone = new ZONE( m_board.get() );
1401 destRA.m_zone->SetIsRuleArea( true );
1402 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1404 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1405 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1406
1407 TOOL_MANAGER toolMgr;
1408 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1409 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1410
1412 toolMgr.RegisterTool( mtTool );
1413
1414 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1415 .m_connectedRoutingOnly = false,
1416 .m_copyPlacement = true,
1417 .m_copyOtherItems = true,
1418 .m_groupItems = false,
1419 .m_includeLockedItems = true,
1420 .m_anchorFp = nullptr };
1421
1422 wxString err;
1423 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1424 BOOST_CHECK_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1425
1426 delete dbRA.m_zone;
1427 delete destRA.m_zone;
1428
1429 for( FOOTPRINT* destFp : { destFp1, destFp2 } )
1430 {
1431 BOOST_CHECK_MESSAGE( countSilkText( destFp, F_SilkS ) == 1,
1432 wxString::Format( "%s lost its F.Silkscreen reference text "
1433 "(issue 24583): F=%d B=%d",
1434 destFp->GetReference(), countSilkText( destFp, F_SilkS ),
1435 countSilkText( destFp, B_SilkS ) ) );
1436 BOOST_CHECK_EQUAL( countSilkText( destFp, B_SilkS ), 1 );
1437 }
1438}
1439
1440
1445BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutGraphicsOnlyBlock, MULTICHANNEL_TEST_FIXTURE )
1446{
1447 m_board = std::make_unique<BOARD>();
1448 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1449
1450 auto makeSilkRect = [&]( const VECTOR2I& aStart, const VECTOR2I& aEnd ) -> PCB_SHAPE*
1451 {
1452 PCB_SHAPE* s = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1453 s->SetStart( aStart );
1454 s->SetEnd( aEnd );
1455 s->SetLayer( F_SilkS );
1456 s->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1457 m_board->Add( s );
1458 return s;
1459 };
1460
1461 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1462 PCB_SHAPE* srcRect = makeSilkRect( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ),
1463 VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1464
1465 // Destination: footprint-free group with one silkscreen item so the group exists.
1466 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1467 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1468 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1469 destPlaceholder->SetLayer( F_SilkS );
1470 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1471 m_board->Add( destPlaceholder );
1472
1473 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1474 destGroup->SetName( wxT( "design-block-dest" ) );
1475 destGroup->AddItem( destPlaceholder );
1476 m_board->Add( destGroup );
1477
1478 int silkBefore = 0;
1479
1480 for( BOARD_ITEM* item : m_board->Drawings() )
1481 {
1482 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1483 silkBefore++;
1484 }
1485
1486 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1487
1488 RULE_AREA dbRA;
1490 dbRA.m_designBlockItems.insert( srcRect ); // no footprints -> m_components stays empty
1491
1492 dbRA.m_zone = new ZONE( m_board.get() );
1493 dbRA.m_zone->SetIsRuleArea( true );
1495 dbRA.m_zone->AddPolygon(
1497 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1498 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1499
1500 RULE_AREA destRA;
1502 destRA.m_group = destGroup; // set explicitly: no footprint to recover it from
1503
1504 destRA.m_zone = new ZONE( m_board.get() );
1505 destRA.m_zone->SetIsRuleArea( true );
1506 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1508 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1509 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1510 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1511
1512 TOOL_MANAGER toolMgr;
1513 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1514 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1515
1517 toolMgr.RegisterTool( mtTool );
1518
1519 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1520 .m_connectedRoutingOnly = false,
1521 .m_copyPlacement = true,
1522 .m_copyOtherItems = true,
1523 .m_groupItems = false,
1524 .m_includeLockedItems = true,
1525 .m_anchorFp = nullptr };
1526
1527 wxString err;
1528 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1529
1530 delete dbRA.m_zone;
1531 delete destRA.m_zone;
1532
1533 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free "
1534 "(graphics-only) design block (issue 24592): %s",
1535 err ) );
1536
1537 // The block's rectangle is copied into the target region and grouped, replacing the group's
1538 // prior placeholder graphic (source + 1 copy = 2 silkscreen shapes).
1539 int silkAfter = 0;
1540 PCB_SHAPE* copiedRect = nullptr;
1541
1542 for( BOARD_ITEM* item : m_board->Drawings() )
1543 {
1544 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1545 continue;
1546
1547 silkAfter++;
1548
1549 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1550
1551 if( shape != srcRect && shape->GetShape() == SHAPE_T::RECTANGLE )
1552 copiedRect = shape;
1553 }
1554
1555 BOOST_CHECK_MESSAGE( silkAfter == 2, wxString::Format( "Expected 2 silkscreen shapes after Apply Design Block "
1556 "Layout (source + 1 copy, placeholder replaced), found "
1557 "%d (issue 24592)",
1558 silkAfter ) );
1559 BOOST_REQUIRE_MESSAGE( copiedRect != nullptr, "Footprint-free block graphic was not copied (issue 24592)" );
1560 BOOST_CHECK_MESSAGE( copiedRect->GetParentGroup() == destGroup,
1561 "Copied block graphic was not added to the destination group (issue 24592)" );
1562 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1563}
1564
1565
1570BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeCopperIsNoNet, MULTICHANNEL_TEST_FIXTURE )
1571{
1572 m_board = std::make_unique<BOARD>();
1573 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1574
1575 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1576 m_board->Add( net );
1577
1578 // Source: footprint-free block with one track on a real net.
1579 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
1580 srcTrack->SetLayer( F_Cu );
1581 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1582 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( 0 ) ) );
1583 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 0 ) ) );
1584 srcTrack->SetNet( net );
1585 m_board->Add( srcTrack );
1586
1587 BOOST_REQUIRE_EQUAL( srcTrack->GetNetCode(), 1 );
1588
1589 // Destination: footprint-free group with a placeholder so the group exists.
1590 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1591 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1592 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1593 destPlaceholder->SetLayer( F_SilkS );
1594 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1595 m_board->Add( destPlaceholder );
1596
1597 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1598 destGroup->SetName( wxT( "design-block-dest" ) );
1599 destGroup->AddItem( destPlaceholder );
1600 m_board->Add( destGroup );
1601
1602 RULE_AREA dbRA;
1604 dbRA.m_designBlockItems.insert( srcTrack ); // no footprints -> m_components stays empty
1605
1606 dbRA.m_zone = new ZONE( m_board.get() );
1607 dbRA.m_zone->SetIsRuleArea( true );
1609 dbRA.m_zone->AddPolygon(
1611 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1612 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1613
1614 RULE_AREA destRA;
1616 destRA.m_group = destGroup;
1617
1618 destRA.m_zone = new ZONE( m_board.get() );
1619 destRA.m_zone->SetIsRuleArea( true );
1620 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1622 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1623 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1624 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1625
1626 TOOL_MANAGER toolMgr;
1627 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1628 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1629
1631 toolMgr.RegisterTool( mtTool );
1632
1633 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1634 .m_connectedRoutingOnly = false,
1635 .m_copyPlacement = true,
1636 .m_copyOtherItems = true,
1637 .m_groupItems = false,
1638 .m_includeLockedItems = true,
1639 .m_anchorFp = nullptr };
1640
1641 wxString err;
1642 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1643
1644 delete dbRA.m_zone;
1645 delete destRA.m_zone;
1646
1647 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free copper "
1648 "block (issue 24592): %s",
1649 err ) );
1650
1651 // Copied track should be in the target region, no-net, and grouped.
1652 PCB_TRACK* copiedTrack = nullptr;
1653
1654 for( PCB_TRACK* track : m_board->Tracks() )
1655 {
1656 if( track != srcTrack )
1657 copiedTrack = track;
1658 }
1659
1660 BOOST_REQUIRE_MESSAGE( copiedTrack != nullptr, "Footprint-free block track was not copied (issue 24592)" );
1661 BOOST_CHECK_MESSAGE( copiedTrack->GetNetCode() == 0,
1662 wxString::Format( "Copied copper should be no-net, got net code %d "
1663 "(issue 24592)",
1664 copiedTrack->GetNetCode() ) );
1665 BOOST_CHECK_MESSAGE( copiedTrack->GetParentGroup() == destGroup,
1666 "Copied copper was not added to the destination group (issue 24592)" );
1667 BOOST_CHECK_GT( copiedTrack->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1668
1669 // The original source track must keep its net.
1670 BOOST_CHECK_EQUAL( srcTrack->GetNetCode(), 1 );
1671}
1672
1673
1679BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeReapplyReplaces, MULTICHANNEL_TEST_FIXTURE )
1680{
1681 m_board = std::make_unique<BOARD>();
1682 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1683
1684 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1685 PCB_SHAPE* srcRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1686 srcRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1687 srcRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1688 srcRect->SetLayer( F_SilkS );
1689 srcRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1690 m_board->Add( srcRect );
1691
1692 // Destination: footprint-free group with a placeholder so the group exists.
1693 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1694 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1695 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1696 destPlaceholder->SetLayer( F_SilkS );
1697 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1698 m_board->Add( destPlaceholder );
1699
1700 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1701 destGroup->SetName( wxT( "design-block-dest" ) );
1702 destGroup->AddItem( destPlaceholder );
1703 m_board->Add( destGroup );
1704
1705 TOOL_MANAGER toolMgr;
1706 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1707 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1708
1710 toolMgr.RegisterTool( mtTool );
1711
1712 auto applyOnce = [&]() -> int
1713 {
1714 RULE_AREA dbRA;
1716 dbRA.m_designBlockItems.insert( srcRect );
1717
1718 dbRA.m_zone = new ZONE( m_board.get() );
1719 dbRA.m_zone->SetIsRuleArea( true );
1722 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1723 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1724 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1725
1726 RULE_AREA destRA;
1728 destRA.m_group = destGroup;
1729
1730 destRA.m_zone = new ZONE( m_board.get() );
1731 destRA.m_zone->SetIsRuleArea( true );
1732 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1734 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1735 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1736 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1737
1738 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1739 .m_connectedRoutingOnly = false,
1740 .m_copyPlacement = true,
1741 .m_copyOtherItems = true,
1742 .m_groupItems = false,
1743 .m_includeLockedItems = true,
1744 .m_anchorFp = nullptr };
1745
1746 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1747
1748 delete dbRA.m_zone;
1749 delete destRA.m_zone;
1750 return result;
1751 };
1752
1753 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "First apply failed (issue 24592)" );
1754 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "Second apply failed (issue 24592)" );
1755
1756 // After two applies there must be exactly the source rectangle plus one copy. A third
1757 // rectangle would mean the second apply stacked instead of replacing.
1758 int rects = 0;
1759
1760 for( BOARD_ITEM* item : m_board->Drawings() )
1761 {
1762 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS
1763 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::RECTANGLE )
1764 {
1765 rects++;
1766 }
1767 }
1768
1769 BOOST_CHECK_MESSAGE( rects == 2, wxString::Format( "Re-apply stacked copies: expected 2 rectangles "
1770 "(source + 1 copy), found %d (issue 24592)",
1771 rects ) );
1772}
1773
1774
1782BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsOtherGroupRouting, MULTICHANNEL_TEST_FIXTURE )
1783{
1784 m_board = std::make_unique<BOARD>();
1785 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1786
1787 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1788 m_board->Add( net );
1789
1790 // Source block: one track centered on origin.
1791 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
1792 srcTrack->SetLayer( F_Cu );
1793 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1794 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), 0 ) );
1795 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), 0 ) );
1796 srcTrack->SetNet( net );
1797 m_board->Add( srcTrack );
1798
1799 // Destination group with a placeholder so the group exists.
1800 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1801 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1802 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1803 destPlaceholder->SetLayer( F_SilkS );
1804 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1805 m_board->Add( destPlaceholder );
1806
1807 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1808 destGroup->SetName( wxT( "design-block-dest" ) );
1809 destGroup->AddItem( destPlaceholder );
1810 m_board->Add( destGroup );
1811
1812 // A sibling instance's track, inside the destination area but owned by another group.
1813 PCB_TRACK* siblingTrack = new PCB_TRACK( m_board.get() );
1814 siblingTrack->SetLayer( F_Cu );
1815 siblingTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1816 siblingTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 47 ) ) );
1817 siblingTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 47 ) ) );
1818 m_board->Add( siblingTrack );
1819
1820 PCB_GROUP* siblingGroup = new PCB_GROUP( m_board.get() );
1821 siblingGroup->SetName( wxT( "design-block-sibling" ) );
1822 siblingGroup->AddItem( siblingTrack );
1823 m_board->Add( siblingGroup );
1824
1825 // Loose, ungrouped routing inside the destination area, which still gets replaced.
1826 PCB_TRACK* looseTrack = new PCB_TRACK( m_board.get() );
1827 looseTrack->SetLayer( F_Cu );
1828 looseTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1829 looseTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 53 ) ) );
1830 looseTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 53 ) ) );
1831 m_board->Add( looseTrack );
1832
1833 RULE_AREA dbRA;
1835 dbRA.m_designBlockItems.insert( srcTrack );
1836
1837 dbRA.m_zone = new ZONE( m_board.get() );
1838 dbRA.m_zone->SetIsRuleArea( true );
1840 dbRA.m_zone->AddPolygon(
1842 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1843 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1844
1845 RULE_AREA destRA;
1847 destRA.m_group = destGroup;
1848
1849 destRA.m_zone = new ZONE( m_board.get() );
1850 destRA.m_zone->SetIsRuleArea( true );
1851 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1853 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1854 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1855 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1856
1857 TOOL_MANAGER toolMgr;
1858 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1859 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1860
1862 toolMgr.RegisterTool( mtTool );
1863
1864 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1865 .m_connectedRoutingOnly = false,
1866 .m_copyPlacement = true,
1867 .m_copyOtherItems = true,
1868 .m_groupItems = false,
1869 .m_includeLockedItems = true,
1870 .m_anchorFp = nullptr };
1871
1872 wxString err;
1873 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1874
1875 delete dbRA.m_zone;
1876 delete destRA.m_zone;
1877
1878 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1879
1880 // Pointers to removed tracks are deleted, so count survivors by location instead.
1881 int siblingTracks = 0;
1882 int looseTracks = 0;
1883
1884 for( PCB_TRACK* track : m_board->Tracks() )
1885 {
1886 int y = track->GetStart().y;
1887
1888 if( y > pcbIUScale.mmToIU( 46 ) && y < pcbIUScale.mmToIU( 48 ) )
1889 siblingTracks++;
1890 else if( y > pcbIUScale.mmToIU( 52 ) && y < pcbIUScale.mmToIU( 54 ) )
1891 looseTracks++;
1892 }
1893
1894 BOOST_CHECK_MESSAGE( siblingTracks == 1,
1895 wxString::Format( "Sibling group routing was deleted (issue 24767): found %d, expected 1",
1896 siblingTracks ) );
1897 BOOST_CHECK_MESSAGE( looseTracks == 0,
1898 wxString::Format( "Loose routing in the target area should be replaced: found %d, expected 0",
1899 looseTracks ) );
1900}
1901
1902
1909BOOST_FIXTURE_TEST_CASE( RepeatLayoutRefusesDuplicatePlacementAreas, MULTICHANNEL_TEST_FIXTURE )
1910{
1911 KI_TEST::LoadBoard( m_settingsManager, "issue22318/issue22318", m_board );
1912
1913 TOOL_MANAGER toolMgr;
1914 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1915
1916 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1917
1919 toolMgr.RegisterTool( mtTool );
1920
1921 mtTool->FindExistingRuleAreas();
1922
1923 auto ruleData = mtTool->GetData();
1924
1925 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1926 static_cast<int>( ruleData->m_areas.size() ) ) );
1927
1928 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
1929
1930 // The shared membership, not the exact count, is what makes the areas invalid targets.
1931 const size_t sharedCount = ruleData->m_areas.front().m_components.size();
1932
1933 BOOST_REQUIRE( sharedCount > 0 );
1934
1935 for( const RULE_AREA& ra : ruleData->m_areas )
1936 BOOST_CHECK_EQUAL( ra.m_components.size(), sharedCount );
1937
1938 RULE_AREA* refArea = &ruleData->m_areas.front();
1939
1940 BOOST_REQUIRE( mtTool->CheckRACompatibility( refArea->m_zone ) >= 0 );
1941
1942 BOOST_REQUIRE_EQUAL( ruleData->m_compatMap.size(), ruleData->m_areas.size() - 1 );
1943
1944 for( const auto& [targetArea, compatData] : ruleData->m_compatMap )
1945 {
1946 BOOST_CHECK_MESSAGE( !compatData.m_isOk,
1947 "Duplicate placement area was wrongly reported as a valid copy target "
1948 "(issue 22318)" );
1949 BOOST_CHECK( !compatData.m_mismatchReasons.empty() );
1950 }
1951
1952 // The single-target overload must also refuse rather than corrupt the board.
1953 RULE_AREA* targetArea = &ruleData->m_areas[1];
1954
1956 wxString err;
1957
1958 int result = mtTool->RepeatLayout( TOOL_EVENT(), *refArea, *targetArea, opts, nullptr, &err );
1959
1961 "RepeatLayout copied a Rule Area onto an identical-component area "
1962 "(issue 22318)" );
1963 BOOST_CHECK( !err.IsEmpty() );
1964}
1965
1966
1978BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotDuplicateUnrelatedGroups, MULTICHANNEL_TEST_FIXTURE )
1979{
1980 KI_TEST::LoadBoard( m_settingsManager, "issue22316/issue22316", m_board );
1981
1982 TOOL_MANAGER toolMgr;
1983 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1984
1985 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1986
1988 toolMgr.RegisterTool( mtTool );
1989
1990 mtTool->FindExistingRuleAreas();
1991
1992 auto ruleData = mtTool->GetData();
1993
1994 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1995 static_cast<int>( ruleData->m_areas.size() ) ) );
1996
1997 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
1998
1999 RULE_AREA* refArea = nullptr;
2000
2001 for( RULE_AREA& ra : ruleData->m_areas )
2002 {
2003 if( ra.m_ruleName == wxT( "test 1" ) )
2004 refArea = &ra;
2005 }
2006
2007 BOOST_REQUIRE( refArea != nullptr );
2008
2009 std::set<KIID> groupUuidsBefore;
2010 std::set<wxString> userGroupNamesBefore;
2011
2012 for( PCB_GROUP* group : m_board->Groups() )
2013 {
2014 groupUuidsBefore.insert( group->m_Uuid );
2015
2016 if( !group->GetName().IsEmpty() )
2017 userGroupNamesBefore.insert( group->GetName() );
2018 }
2019
2020 mtTool->CheckRACompatibility( refArea->m_zone );
2021
2022 for( auto& [targetArea, compatData] : ruleData->m_compatMap )
2023 compatData.m_doCopy = true;
2024
2025 ruleData->m_options.m_copyPlacement = true;
2026 ruleData->m_options.m_copyRouting = true;
2027 ruleData->m_options.m_copyOtherItems = true;
2028 ruleData->m_options.m_groupItems = true;
2029 ruleData->m_options.m_includeLockedItems = true;
2030
2031 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
2032
2033 BOOST_REQUIRE( result >= 0 );
2034
2035 int clonedUserGroups = 0;
2036
2037 for( PCB_GROUP* group : m_board->Groups() )
2038 {
2039 bool isNew = !groupUuidsBefore.contains( group->m_Uuid );
2040
2041 if( isNew && userGroupNamesBefore.contains( group->GetName() ) )
2042 clonedUserGroups++;
2043 }
2044
2045 BOOST_CHECK_MESSAGE( clonedUserGroups == 0,
2046 wxString::Format( "Repeat layout cloned %d unrelated user groups "
2047 "(issue 22316)",
2048 clonedUserGroups ) );
2049}
2050
2051
2059BOOST_FIXTURE_TEST_CASE( TopoMatchExternalLoopReportsConnectivity, MULTICHANNEL_TEST_FIXTURE )
2060{
2062
2063 KI_TEST::LoadBoard( m_settingsManager, "issue24192/issue24192", m_board );
2064
2065 TOOL_MANAGER toolMgr;
2066 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2067 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2068
2070 toolMgr.RegisterTool( mtTool );
2071
2072 mtTool->FindExistingRuleAreas();
2073
2074 auto ruleData = mtTool->GetData();
2075
2076 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 2 );
2077
2078 RULE_AREA* refArea = nullptr;
2079 RULE_AREA* targetArea = nullptr;
2080
2081 for( RULE_AREA& ra : ruleData->m_areas )
2082 {
2083 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
2084 refArea = &ra;
2085 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
2086 targetArea = &ra;
2087 }
2088
2089 BOOST_REQUIRE( refArea != nullptr );
2090 BOOST_REQUIRE( targetArea != nullptr );
2091
2092 // The component counts are identical; only the internal connectivity differs.
2093 BOOST_CHECK_EQUAL( refArea->m_components.size(), targetArea->m_components.size() );
2094
2095 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
2096 targetArea->m_components );
2097 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
2098 refArea->m_components );
2099
2101 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2102 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2103
2104 // The topology genuinely differs, so matching must fail and produce a reason.
2105 BOOST_CHECK( !status );
2106 BOOST_REQUIRE( !details.empty() );
2107
2108 // Collect the reference designators that belong to each channel so we can verify the
2109 // reason's reference/candidate orientation matches the ref/target areas.
2110 std::set<wxString> refRefDes;
2111 std::set<wxString> targetRefDes;
2112
2113 for( FOOTPRINT* fp : refArea->m_components )
2114 refRefDes.insert( fp->GetReference() );
2115
2116 for( FOOTPRINT* fp : targetArea->m_components )
2117 targetRefDes.insert( fp->GetReference() );
2118
2119 bool sawConnectivityReason = false;
2120
2121 for( const auto& reason : details )
2122 {
2123 BOOST_TEST_MESSAGE( wxString::Format( "reason: %s <-> %s: %s", reason.m_reference,
2124 reason.m_candidate, reason.m_reason ) );
2125
2126 // The generic fallback message must no longer be the only thing reported; instead the
2127 // connectivity difference detected by the per-pad isomorphism check must surface.
2128 BOOST_CHECK( !reason.m_reason.Contains( wxT( "No compatible component found" ) ) );
2129
2130 if( reason.m_reason.Contains( wxT( "connects to" ) )
2131 || reason.m_reason.Contains( wxT( "connectivity" ) ) )
2132 {
2133 sawConnectivityReason = true;
2134
2135 // The reference designator in the reason must come from the reference channel and
2136 // the candidate from the target channel, not the other way around (codex-review).
2137 BOOST_CHECK_MESSAGE( refRefDes.count( reason.m_reference ) > 0,
2138 wxString::Format( "Reason reference '%s' should be a reference-area "
2139 "component", reason.m_reference ) );
2140 BOOST_CHECK_MESSAGE( targetRefDes.count( reason.m_candidate ) > 0,
2141 wxString::Format( "Reason candidate '%s' should be a target-area "
2142 "component", reason.m_candidate ) );
2143 }
2144 }
2145
2146 BOOST_CHECK_MESSAGE( sawConnectivityReason,
2147 "Topology mismatch message should explain the connectivity difference "
2148 "rather than report a generic missing-component error (issue 24192)" );
2149}
2150
2151
2159{
2161
2162 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2163
2164 auto addNet = [&]( const wxString& aName ) -> int
2165 {
2166 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
2167 board->Add( net );
2168 return net->GetNetCode();
2169 };
2170
2171 const int netGnd = addNet( wxT( "GND" ) );
2172 const int netP3V3 = addNet( wxT( "+3V3" ) );
2173 const int netCollide = addNet( wxT( "Net-(D3-A)" ) ); // shared name, lands on a different part
2174 const int netRefY = addNet( wxT( "Net-(D4-A)" ) ); // block other LED anode
2175 const int netTgtX = addNet( wxT( "Net-(D2-A)" ) ); // copy other LED anode
2176
2177 LIB_ID ledAId( wxT( "TestLib" ), wxT( "LED_A" ) );
2178 LIB_ID ledBId( wxT( "TestLib" ), wxT( "LED_B" ) );
2179 LIB_ID resId( wxT( "TestLib" ), wxT( "R" ) );
2180
2181 auto addPad = [&]( FOOTPRINT* fp, const wxString& aNum, int aNet )
2182 {
2183 PAD* pad = new PAD( fp );
2184 pad->SetNumber( aNum );
2186 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2187 pad->SetLayerSet( LSET( { F_Cu } ) );
2188 pad->SetNetCode( aNet );
2189 fp->Add( pad );
2190 };
2191
2192 auto addFp = [&]( const LIB_ID& aId, const wxString& aRef ) -> FOOTPRINT*
2193 {
2194 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2195 fp->SetFPID( aId );
2196 fp->SetReference( aRef );
2197 board->Add( fp );
2198 return fp;
2199 };
2200
2201 // One LED plus its series resistor. The LED anode net is the one that can clash.
2202 auto addLedAndRes = [&]( const LIB_ID& aLedId, const wxString& aLedRef, const wxString& aResRef, int aAnodeNet,
2203 std::set<FOOTPRINT*>& aSet )
2204 {
2205 FOOTPRINT* led = addFp( aLedId, aLedRef );
2206 addPad( led, wxT( "1" ), netGnd );
2207 addPad( led, wxT( "2" ), aAnodeNet );
2208 aSet.insert( led );
2209
2210 FOOTPRINT* res = addFp( resId, aResRef );
2211 addPad( res, wxT( "1" ), aAnodeNet );
2212 addPad( res, wxT( "2" ), netP3V3 );
2213 aSet.insert( res );
2214 };
2215
2216 // The block: LED_A is D3 on the clashing net, LED_B is D4 on its own net.
2217 std::set<FOOTPRINT*> refFps;
2218 addLedAndRes( ledAId, wxT( "D3" ), wxT( "R27" ), netCollide, refFps );
2219 addLedAndRes( ledBId, wxT( "D4" ), wxT( "R30" ), netRefY, refFps );
2220
2221 // The copy: same parts, but the clashing name now sits on LED_B (D3), a different part.
2222 std::set<FOOTPRINT*> tgtFps;
2223 addLedAndRes( ledAId, wxT( "D2" ), wxT( "R1" ), netTgtX, tgtFps );
2224 addLedAndRes( ledBId, wxT( "D3" ), wxT( "R2" ), netCollide, tgtFps );
2225
2226 // The fix gives the block's auto nets private names so they cannot clash with the copy.
2227 std::unordered_set<EDA_ITEM*> refItems( refFps.begin(), refFps.end() );
2228 MULTICHANNEL_TOOL::IsolateDesignBlockAutoNets( board.get(), refFps, refItems );
2229
2230 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
2231 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
2232
2234 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2235 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2236
2237 if( !status )
2238 {
2239 for( const auto& reason : details )
2240 {
2241 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s to %s: %s", reason.m_reference, reason.m_candidate,
2242 reason.m_reason ) );
2243 }
2244 }
2245
2246 BOOST_CHECK_MESSAGE( status, "Topology match failed even after isolating the block's auto nets "
2247 "(issue 24767)" );
2248 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
2249}
2250
2251
2257BOOST_FIXTURE_TEST_CASE( TopoMatchTieBreaksIdenticalPartsByValue, MULTICHANNEL_TEST_FIXTURE )
2258{
2260
2261 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
2262
2263 const LIB_ID receptacleId( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) );
2264
2265 // Unique net per footprint, so the four are a topological tie and only the value differs.
2266 auto makeReceptacle = [&]( const wxString& aRef, const wxString& aValue, bool aWithSymbolPath ) -> FOOTPRINT*
2267 {
2268 FOOTPRINT* fp = new FOOTPRINT( board.get() );
2269 fp->SetFPID( receptacleId );
2270 fp->SetReference( aRef );
2271 fp->SetValue( aValue );
2272
2273 if( aWithSymbolPath )
2274 {
2276 path.push_back( KIID() );
2277 fp->SetPath( path );
2278 }
2279
2280 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), wxString::Format( "net_%s", aRef ) );
2281 board->Add( net );
2282
2283 PAD* pad = new PAD( fp );
2284 pad->SetNumber( wxT( "1" ) );
2286 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2287 pad->SetLayerSet( LSET( { F_Cu } ) );
2288 pad->SetNet( net );
2289 fp->Add( pad );
2290
2291 board->Add( fp );
2292 return fp;
2293 };
2294
2295 // Block source: no symbol path (as after AppendBoard). Fixed order keeps the test deterministic.
2296 FOOTPRINT* refNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), false );
2297 FOOTPRINT* refNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), false );
2298 FOOTPRINT* refNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), false );
2299 FOOTPRINT* refNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), false );
2300
2301 // Target refdes order is the reverse of value order, so a refdes fallback would swap NC and NO.
2302 FOOTPRINT* tgtNO1 = makeReceptacle( wxT( "J20" ), wxT( "NO_1" ), true );
2303 FOOTPRINT* tgtNO0 = makeReceptacle( wxT( "J21" ), wxT( "NO_0" ), true );
2304 FOOTPRINT* tgtNC1 = makeReceptacle( wxT( "J22" ), wxT( "NC_1" ), true );
2305 FOOTPRINT* tgtNC0 = makeReceptacle( wxT( "J23" ), wxT( "NC_0" ), true );
2306
2307 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
2308 cgRef->AddFootprint( refNC0, VECTOR2I( 0, 0 ) );
2309 cgRef->AddFootprint( refNC1, VECTOR2I( 0, 0 ) );
2310 cgRef->AddFootprint( refNO0, VECTOR2I( 0, 0 ) );
2311 cgRef->AddFootprint( refNO1, VECTOR2I( 0, 0 ) );
2312 cgRef->BuildConnectivity();
2313
2314 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
2315 cgTarget->AddFootprint( tgtNO1, VECTOR2I( 0, 0 ) );
2316 cgTarget->AddFootprint( tgtNO0, VECTOR2I( 0, 0 ) );
2317 cgTarget->AddFootprint( tgtNC1, VECTOR2I( 0, 0 ) );
2318 cgTarget->AddFootprint( tgtNC0, VECTOR2I( 0, 0 ) );
2319 cgTarget->BuildConnectivity();
2320
2322 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
2323 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
2324
2325 BOOST_CHECK( status );
2326 BOOST_CHECK_EQUAL( result.size(), 4 );
2327
2328 // Each reference must match the same-value target.
2329 for( const auto& [refFp, targetFp] : result )
2330 {
2331 BOOST_TEST_MESSAGE( wxString::Format( "%s (%s) -> %s (%s)", refFp->GetReference(), refFp->GetValue(),
2332 targetFp->GetReference(), targetFp->GetValue() ) );
2333
2334 BOOST_CHECK_EQUAL( refFp->GetValue(), targetFp->GetValue() );
2335 }
2336}
2337
2338
2343BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutUnmirrorsIdenticalReceptacles, MULTICHANNEL_TEST_FIXTURE )
2344{
2345 m_board = std::make_unique<BOARD>();
2346 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
2347
2348 auto makeReceptacle = [&]( const wxString& aRef, const wxString& aValue, const VECTOR2I& aPos,
2349 bool aWithSymbolPath ) -> FOOTPRINT*
2350 {
2351 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
2352 fp->SetFPID( LIB_ID( wxT( "Don-Con" ), wxT( "Mill-Max-Pin_Receptacle" ) ) );
2353 fp->SetReference( aRef );
2354 fp->SetValue( aValue );
2355 fp->SetPosition( aPos );
2356
2357 if( aWithSymbolPath )
2358 {
2360 path.push_back( KIID() );
2361 fp->SetPath( path );
2362 }
2363
2364 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxString::Format( "net_%s", aRef ) );
2365 m_board->Add( net );
2366
2367 PAD* pad = new PAD( fp );
2368 pad->SetNumber( wxT( "1" ) );
2370 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
2371 pad->SetLayerSet( LSET( { F_Cu } ) );
2372 pad->SetNet( net );
2373 pad->SetPosition( aPos );
2374 fp->Add( pad );
2375
2376 m_board->Add( fp );
2377 return fp;
2378 };
2379
2380 auto mm = []( double a, double b )
2381 {
2382 return VECTOR2I( pcbIUScale.mmToIU( a ), pcbIUScale.mmToIU( b ) );
2383 };
2384
2385 // Signed area of NC_0, NC_1, NO_0. Same sign as the block is correct, opposite sign is a mirror.
2386 auto chirality = []( const VECTOR2I& aNC0, const VECTOR2I& aNC1, const VECTOR2I& aNO0 ) -> double
2387 {
2388 return (double) ( aNC1.x - aNC0.x ) * ( aNO0.y - aNC0.y ) - (double) ( aNC1.y - aNC0.y ) * ( aNO0.x - aNC0.x );
2389 };
2390
2391 // Block source: the reference arrangement, no symbol path (as after AppendBoard).
2392 FOOTPRINT* srcNC0 = makeReceptacle( wxT( "J3" ), wxT( "NC_0" ), mm( 0, 0 ), false );
2393 FOOTPRINT* srcNC1 = makeReceptacle( wxT( "J5" ), wxT( "NC_1" ), mm( 0, 7.9 ), false );
2394 FOOTPRINT* srcNO0 = makeReceptacle( wxT( "J6" ), wxT( "NO_0" ), mm( -3.58, 2.28 ), false );
2395 FOOTPRINT* srcNO1 = makeReceptacle( wxT( "J7" ), wxT( "NO_1" ), mm( 3.58, 5.62 ), false );
2396
2397 const double srcChir = chirality( srcNC0->GetPosition(), srcNC1->GetPosition(), srcNO0->GetPosition() );
2398
2399 // Destination placed as a mirror (NO_0 / NO_1 reflected across the NC axis).
2400 FOOTPRINT* dstNC0 = makeReceptacle( wxT( "J8" ), wxT( "NC_0" ), mm( 40, 40 ), true );
2401 FOOTPRINT* dstNC1 = makeReceptacle( wxT( "J9" ), wxT( "NC_1" ), mm( 40, 47.9 ), true );
2402 FOOTPRINT* dstNO0 = makeReceptacle( wxT( "J10" ), wxT( "NO_0" ), mm( 43.58, 42.28 ), true );
2403 FOOTPRINT* dstNO1 = makeReceptacle( wxT( "J11" ), wxT( "NO_1" ), mm( 36.42, 45.62 ), true );
2404
2405 BOOST_REQUIRE_LT( srcChir * chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() ), 0.0 );
2406
2407 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
2408 destGroup->SetName( wxT( "Pin receptacles for safety switch" ) );
2409
2410 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
2411 destGroup->AddItem( fp );
2412
2413 m_board->Add( destGroup );
2414
2415 RULE_AREA dbRA;
2417
2418 for( FOOTPRINT* fp : { srcNC0, srcNC1, srcNO0, srcNO1 } )
2419 {
2420 dbRA.m_components.insert( fp );
2421 dbRA.m_designBlockItems.insert( fp );
2422 }
2423
2424 dbRA.m_zone = new ZONE( m_board.get() );
2425 dbRA.m_zone->SetIsRuleArea( true );
2427 dbRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( -6, -3 ), mm( 6, 11 ) ) ) );
2428
2429 RULE_AREA destRA;
2431
2432 for( FOOTPRINT* fp : { dstNC0, dstNC1, dstNO0, dstNO1 } )
2433 destRA.m_components.insert( fp );
2434
2435 destRA.m_zone = new ZONE( m_board.get() );
2436 destRA.m_zone->SetIsRuleArea( true );
2437 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
2438 destRA.m_zone->AddPolygon( KIGEOM::BoxToLineChain( BOX2I::ByCorners( mm( 33, 37 ), mm( 47, 51 ) ) ) );
2439
2440 TOOL_MANAGER toolMgr;
2441 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
2442 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
2443
2445 toolMgr.RegisterTool( mtTool );
2446
2447 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = false,
2448 .m_connectedRoutingOnly = false,
2449 .m_copyPlacement = true,
2450 .m_copyOtherItems = false,
2451 .m_groupItems = false,
2452 .m_includeLockedItems = true,
2453 .m_anchorFp = nullptr };
2454
2455 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
2456 BOOST_REQUIRE_MESSAGE( result >= 0, "RepeatLayout failed" );
2457
2458 delete dbRA.m_zone;
2459 delete destRA.m_zone;
2460
2461 const double dstChir = chirality( dstNC0->GetPosition(), dstNC1->GetPosition(), dstNO0->GetPosition() );
2462
2464 wxString::Format( "block chirality %.0f, destination chirality after apply %.0f", srcChir, dstChir ) );
2465
2466 BOOST_CHECK_MESSAGE( srcChir * dstChir > 0.0, "Applied layout left the receptacles mirrored (NC/NO swapped)" );
2467}
2468
2469
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
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:81
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:313
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
const ZONES & Zones() const
Definition board.h:425
static constexpr BOX2< VECTOR2I > ByCorners(const VECTOR2I &aCorner1, const VECTOR2I &aCorner2)
Definition box2.h:66
Store all of the related component information found in a netlist.
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:58
void SetName(const wxString &aName)
Definition eda_group.h:48
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:114
SHAPE_T GetShape() const
Definition eda_shape.h:185
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:190
virtual bool IsVisible() const
Definition eda_text.h:208
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:265
void SetPosition(const VECTOR2I &aPos) override
void SetFPID(const LIB_ID &aFPID)
Definition footprint.h:442
void SetPath(const KIID_PATH &aPath)
Definition footprint.h:465
PCB_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this footprint.
std::deque< PAD * > & Pads()
Definition footprint.h:375
void SetReference(const wxString &aReference)
Definition footprint.h:847
void SetValue(const wxString &aValue)
Definition footprint.h:868
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
std::vector< FP_3DMODEL > & Models()
Definition footprint.h:392
const wxString & GetReference() const
Definition footprint.h:841
VECTOR2I GetPosition() const override
Definition footprint.h:403
DRAWINGS & GraphicalItems()
Definition footprint.h:378
Definition kiid.h:44
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & AllTechMask()
Return a mask holding all technical layers (no CU layer) on both side.
Definition lset.cpp:672
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
virtual wxWindow * GetToolCanvas() const override
Canvas access.
int CheckRACompatibility(ZONE *aRefZone)
RULE_AREAS_DATA * GetData()
static std::vector< NETINFO_ITEM * > IsolateDesignBlockAutoNets(BOARD *aBoard, const std::set< FOOTPRINT * > &aFootprints, const std::unordered_set< EDA_ITEM * > &aItems)
Remap auto-generated nets (Net-(...), unconnected-...) of a design block that was appended for layout...
int RepeatLayout(const TOOL_EVENT &aEvent, ZONE *aRefZone)
int AutogenerateRuleAreas(const TOOL_EVENT &aEvent)
Handle the data for a net.
Definition netinfo.h:46
int GetNetCode() const
Definition netinfo.h:94
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:61
void SetNumber(const wxString &aNumber)
Set the pad number (note that it can be alphanumeric, such as the array reference "AA12").
Definition pad.h:142
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:49
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
virtual void SetPosition(const VECTOR2I &aPos) override
Definition pcb_text.h:95
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:89
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:92
const VECTOR2I & GetStart() const
Definition pcb_track.h:93
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
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
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1393
wxString GetPlacementAreaSource() const
Definition zone.h:818
SHAPE_POLY_SET * Outline()
Definition zone.h:418
void SetIsRuleArea(bool aEnable)
Definition zone.h:814
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:644
@ SEGMENT
Definition eda_shape.h:46
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:47
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_SilkS
Definition layer_ids.h:96
@ B_SilkS
Definition layer_ids.h:97
@ F_Cu
Definition layer_ids.h:60
SHAPE_LINE_CHAIN BoxToLineChain(const BOX2I &aBox)
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::map< FOOTPRINT *, FOOTPRINT * > COMPONENT_MATCHES
Definition topo_match.h:180
Class to handle a set of BOARD_ITEMs.
Utility functions for working with shapes.
std::unique_ptr< BOARD > m_board
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:45
std::atomic< int > * m_matchedComponents
Definition topo_match.h:46
std::atomic< int > * m_totalComponents
Definition topo_match.h:47
@ VALUE
Field Value of part, i.e. "3.3K".
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
VECTOR3I res
RULE_AREA * findRuleAreaByPartialName(MULTICHANNEL_TOOL *aTool, const wxString &aName)
int countZonesByNamePrefixInRuleArea(BOARD *aBoard, const wxString &aBaseName, const RULE_AREA &aRuleArea)
BOOST_FIXTURE_TEST_CASE(MultichannelToolRegressions, MULTICHANNEL_TEST_FIXTURE)
int countZonesByNameInRuleArea(BOARD *aBoard, const wxString &aZoneName, const RULE_AREA &aRuleArea)
RULE_AREA * findRuleAreaByPlacementGroup(MULTICHANNEL_TOOL *aTool, const wxString &aGroupName)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:81
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:90
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:85
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683