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
103{
105
106 std::vector<wxString> tests = { "vme-wren" };
107
108 for( const wxString& relPath : tests )
109 {
110 KI_TEST::LoadBoard( m_settingsManager, relPath, m_board );
111
112 TOOL_MANAGER toolMgr;
113 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
114
115 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
116
117 MULTICHANNEL_TOOL* mtTool = new MULTICHANNEL_TOOL; // TOOL_MANAGER owns the tools
118 toolMgr.RegisterTool( mtTool );
119
120 //RULE_AREAS_DATA* raData = m_parentTool->GetData();
121
123
124 auto ruleData = mtTool->GetData();
125
126 BOOST_TEST_MESSAGE( wxString::Format( "RA multichannel sheets = %d",
127 static_cast<int>( ruleData->m_areas.size() ) ) );
128
129 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 72 );
130
131 int cnt = 0;
132
133 ruleData->m_replaceExisting = true;
134
135 for( RULE_AREA& ra : ruleData->m_areas )
136 {
137 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" )
138 || ra.m_sheetName == wxT( "pp_driver_2x.kicad_sch" ) )
139 {
140 ra.m_generateEnabled = true;
141 cnt++;
142 }
143 }
144
145 BOOST_TEST_MESSAGE( wxString::Format( "Autogenerating %d RAs", cnt ) );
146
147 TOOL_EVENT dummyEvent;
148
149 mtTool->AutogenerateRuleAreas( dummyEvent );
150 mtTool->FindExistingRuleAreas();
151
152 int n_areas_io = 0, n_areas_pp = 0, n_areas_other = 0;
153
154 BOOST_TEST_MESSAGE( wxString::Format( "Found %d RAs after commit",
155 static_cast<int>(ruleData->m_areas.size() ) ) );
156
157 for( const RULE_AREA& ra : ruleData->m_areas )
158 {
159 BOOST_TEST_MESSAGE( wxString::Format( "SN '%s'", ra.m_ruleName ) );
160
161 if( ra.m_ruleName.Contains( wxT( "io_drivers_fp" ) ) )
162 {
163 n_areas_io++;
164 BOOST_CHECK_EQUAL( ra.m_components.size(), 31 );
165 }
166 else if( ra.m_ruleName.Contains( wxT( "io_drivers_pp" ) ) )
167 {
168 n_areas_pp++;
169 BOOST_CHECK_EQUAL( ra.m_components.size(), 11 );
170 }
171 else
172 {
173 n_areas_other++;
174 }
175 }
176
177 BOOST_TEST_MESSAGE( wxString::Format( "IO areas=%d, PP areas=%d, others=%d",
178 n_areas_io, n_areas_pp, n_areas_other ) );
179
180 BOOST_CHECK_EQUAL( n_areas_io, 16 );
181 BOOST_CHECK_EQUAL( n_areas_pp, 16 );
182 BOOST_CHECK_EQUAL( n_areas_other, 0 );
183
184 const std::vector<wxString> rulesToTest = { wxT( "io_drivers_fp" ),
185 wxT( "io_drivers_pp" ) };
186
187 for( const wxString& ruleName : rulesToTest )
188 {
189 for( const RULE_AREA& refArea : ruleData->m_areas )
190 {
191 if( !refArea.m_ruleName.Contains( ruleName ) )
192 continue;
193
194 BOOST_TEST_MESSAGE( wxString::Format( "REF AREA: '%s'", refArea.m_ruleName ) );
195
196 for( const RULE_AREA& targetArea : ruleData->m_areas )
197 {
198 if( targetArea.m_zone == refArea.m_zone )
199 continue;
200
201 if( !targetArea.m_ruleName.Contains( ruleName ) )
202 continue;
203
204 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea.m_components,
205 targetArea.m_components );
206 auto cgTarget =
207 CONNECTION_GRAPH::BuildFromFootprintSet( targetArea.m_components,
208 refArea.m_components );
209
211
212 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
213 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
214
215 BOOST_TEST_MESSAGE( wxString::Format(
216 "topo match: '%s' [%d] -> '%s' [%d] result %d", refArea.m_ruleName.c_str().AsChar(),
217 static_cast<int>( refArea.m_components.size() ), targetArea.m_ruleName.c_str().AsChar(),
218 static_cast<int>( targetArea.m_components.size() ), status ? 1 : 0 ) );
219
220 for( const auto& iter : result )
221 {
222 BOOST_TEST_MESSAGE( wxString::Format( "%s : %s",
223 iter.second->GetReference(),
224 iter.first->GetReference() ) );
225 }
226
227 BOOST_CHECK( status );
228 BOOST_CHECK( details.empty() );
229 }
230 }
231 }
232
233 auto refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
234
235 BOOST_ASSERT( refArea );
236
237 const std::vector<wxString> targetAreaNames( { wxT( "io_drivers_fp/bank2/io78/" ),
238 wxT( "io_drivers_fp/bank1/io78/" ),
239 wxT( "io_drivers_fp/bank0/io01/" ) } );
240
241 for( const wxString& targetRaName : targetAreaNames )
242 {
243 auto targetRA = findRuleAreaByPartialName( mtTool, targetRaName );
244
245 BOOST_ASSERT( targetRA != nullptr );
246
247 BOOST_TEST_MESSAGE( wxString::Format( "Clone to: %s", targetRA->m_ruleName ) );
248
249 ruleData->m_compatMap[targetRA].m_doCopy = true;
250 }
251
252 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
253
254 BOOST_ASSERT( result >= 0 );
255 }
256}
257
258
263BOOST_FIXTURE_TEST_CASE( RepeatLayoutCopiesFootprintProperties, MULTICHANNEL_TEST_FIXTURE )
264{
265 KI_TEST::LoadBoard( m_settingsManager, "issue22548/issue22548", m_board );
266
267 TOOL_MANAGER toolMgr;
268 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
269
270 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
271
273 toolMgr.RegisterTool( mtTool );
274
275 mtTool->FindExistingRuleAreas();
276
277 auto ruleData = mtTool->GetData();
278
279 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
280 static_cast<int>( ruleData->m_areas.size() ) ) );
281
282 BOOST_CHECK( ruleData->m_areas.size() >= 2 );
283
284 if( ruleData->m_areas.size() < 2 )
285 return;
286
287 RULE_AREA* refArea = nullptr;
288 RULE_AREA* targetArea = nullptr;
289
290 for( RULE_AREA& ra : ruleData->m_areas )
291 {
292 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
293 refArea = &ra;
294 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
295 targetArea = &ra;
296 }
297
298 if( !refArea || !targetArea )
299 {
300 BOOST_TEST_MESSAGE( "Could not find Untitled Sheet and Untitled Sheet1 rule areas, skipping test" );
301 return;
302 }
303
304 BOOST_TEST_MESSAGE( wxString::Format( "Reference area: %s, Target area: %s",
305 refArea->m_ruleName, targetArea->m_ruleName ) );
306
307 FOOTPRINT* refFP = nullptr;
308 FOOTPRINT* targetFP = nullptr;
309
310 for( FOOTPRINT* fp : refArea->m_components )
311 {
312 if( fp->GetReference().StartsWith( wxT( "U1" ) ) )
313 {
314 refFP = fp;
315 break;
316 }
317 }
318
319 for( FOOTPRINT* fp : targetArea->m_components )
320 {
321 if( fp->GetReference().StartsWith( wxT( "U2" ) ) )
322 {
323 targetFP = fp;
324 break;
325 }
326 }
327
328 if( !refFP || !targetFP )
329 {
330 BOOST_TEST_MESSAGE( "Could not find matching footprints in the rule areas, skipping test" );
331 return;
332 }
333
334 PCB_FIELD* refValueField = refFP->GetField( FIELD_T::VALUE );
335 bool refValueVisible = refValueField ? refValueField->IsVisible() : true;
336
337 std::vector<FP_3DMODEL> refModels = refFP->Models();
338
339 mtTool->CheckRACompatibility( refArea->m_zone );
340
341 ruleData->m_compatMap[targetArea].m_doCopy = true;
342 ruleData->m_options.m_copyPlacement = true;
343
344 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
345
346 BOOST_CHECK( result >= 0 );
347
348 PCB_FIELD* targetValueField = targetFP->GetField( FIELD_T::VALUE );
349
350 if( targetValueField && refValueField )
351 {
352 BOOST_CHECK_EQUAL( targetValueField->IsVisible(), refValueVisible );
353 BOOST_TEST_MESSAGE( wxString::Format( "Value field visibility: ref=%d, target=%d",
354 refValueVisible, targetValueField->IsVisible() ) );
355 }
356
357 BOOST_CHECK_EQUAL( targetFP->Models().size(), refModels.size() );
358
359 if( !refModels.empty() )
360 {
361 BOOST_TEST_MESSAGE( wxString::Format( "3D models: ref=%d, target=%d",
362 static_cast<int>( refModels.size() ),
363 static_cast<int>( targetFP->Models().size() ) ) );
364 }
365}
366
367
375BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotRemoveReferenceVias, MULTICHANNEL_TEST_FIXTURE )
376{
377 KI_TEST::LoadBoard( m_settingsManager, "issue21184/issue21184", m_board );
378
379 TOOL_MANAGER toolMgr;
380 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
381
382 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
383
385 toolMgr.RegisterTool( mtTool );
386
387 mtTool->FindExistingRuleAreas();
388
389 auto ruleData = mtTool->GetData();
390
391 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
392 static_cast<int>( ruleData->m_areas.size() ) ) );
393
394 BOOST_CHECK_EQUAL( ruleData->m_areas.size(), 2 );
395
396 if( ruleData->m_areas.size() < 2 )
397 return;
398
399 RULE_AREA* refArea = nullptr;
400 RULE_AREA* targetArea = nullptr;
401
402 for( RULE_AREA& ra : ruleData->m_areas )
403 {
404 if( ra.m_ruleName == wxT( "Test1" ) )
405 refArea = &ra;
406 else if( ra.m_ruleName == wxT( "Test2" ) )
407 targetArea = &ra;
408 }
409
410 BOOST_REQUIRE( refArea != nullptr );
411 BOOST_REQUIRE( targetArea != nullptr );
412
413 int refViaCountBefore = 0;
414
415 for( PCB_TRACK* track : m_board->Tracks() )
416 {
417 if( track->Type() == PCB_VIA_T )
418 {
419 PCB_VIA* via = static_cast<PCB_VIA*>( track );
420 VECTOR2I viaPos = via->GetPosition();
421
422 if( refArea->m_zone->Outline()->Contains( viaPos ) )
423 refViaCountBefore++;
424 }
425 }
426
427 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias before repeat: %d", refViaCountBefore ) );
428 BOOST_CHECK( refViaCountBefore > 0 );
429
430 mtTool->CheckRACompatibility( refArea->m_zone );
431
432 ruleData->m_compatMap[targetArea].m_doCopy = true;
433 ruleData->m_options.m_copyPlacement = true;
434 ruleData->m_options.m_copyRouting = true;
435
436 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
437
438 BOOST_CHECK( result >= 0 );
439
440 int refViaCountAfter = 0;
441
442 for( PCB_TRACK* track : m_board->Tracks() )
443 {
444 if( track->Type() == PCB_VIA_T )
445 {
446 PCB_VIA* via = static_cast<PCB_VIA*>( track );
447 VECTOR2I viaPos = via->GetPosition();
448
449 if( refArea->m_zone->Outline()->Contains( viaPos ) )
450 refViaCountAfter++;
451 }
452 }
453
454 BOOST_TEST_MESSAGE( wxString::Format( "Reference area vias after repeat: %d", refViaCountAfter ) );
455
456 BOOST_CHECK_EQUAL( refViaCountAfter, refViaCountBefore );
457}
458
459
464BOOST_FIXTURE_TEST_CASE( RepeatLayoutRespectsZoneLayerSetsForOtherItems, MULTICHANNEL_TEST_FIXTURE )
465{
466 KI_TEST::LoadBoard( m_settingsManager, "issue22983/issue22983", m_board );
467
468 TOOL_MANAGER toolMgr;
469 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
470
471 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
472
474 toolMgr.RegisterTool( mtTool );
475
476 mtTool->FindExistingRuleAreas();
477
478 RULE_AREA* sourceA = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceA" ) );
479 RULE_AREA* destA = findRuleAreaByPlacementGroup( mtTool, wxT( "DestA" ) );
480 RULE_AREA* sourceB = findRuleAreaByPlacementGroup( mtTool, wxT( "SourceB" ) );
481 RULE_AREA* destB = findRuleAreaByPlacementGroup( mtTool, wxT( "DestB" ) );
482
483 BOOST_REQUIRE( sourceA != nullptr );
484 BOOST_REQUIRE( destA != nullptr );
485 BOOST_REQUIRE( sourceB != nullptr );
486 BOOST_REQUIRE( destB != nullptr );
487
488 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *sourceA ), 1 );
489 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ), 0 );
491 countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *sourceB ), 1 );
492 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
493 0 );
494 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *sourceB ), 1 );
495 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
496
497 REPEAT_LAYOUT_OPTIONS options;
498 options.m_copyPlacement = false;
499 options.m_copyRouting = false;
500 options.m_copyOtherItems = true;
501 options.m_includeLockedItems = true;
502
503 int copyAStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceA, *destA, options );
504 BOOST_REQUIRE( copyAStatus >= 0 );
505
506 int copyBStatus = mtTool->RepeatLayout( TOOL_EVENT(), *sourceB, *destB, options );
507 BOOST_REQUIRE( copyBStatus >= 0 );
508
509 // SourceA and DestA both include F.Cu+B.Cu, so this multilayer zone should copy.
510 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneFrontAndOne" ), *destA ), 1 );
511
512 // SourceB only includes F.Cu, so this F.Cu+B.Cu zone should not copy to DestB.
513 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "MultilayerZoneSourceBLayerMismatch" ), *destB ),
514 0 );
515
516 // SourceB excludes B.Cu, so this B.Cu-only zone should not copy either.
517 BOOST_CHECK_EQUAL( countZonesByNameInRuleArea( m_board.get(), wxT( "BottomZoneDontCopyMe" ), *destB ), 0 );
518}
519
520
529{
531 using TMATCH::COMPONENT;
532
533 // Create two connection graphs with components that have dotted reference designators
534 auto cgRef = std::make_unique<CONNECTION_GRAPH>();
535 auto cgTarget = std::make_unique<CONNECTION_GRAPH>();
536
537 // Create mock footprints with the same FPID
538 LIB_ID fpid( wxT( "Package_SO" ), wxT( "SOIC-8_3.9x4.9mm_P1.27mm" ) );
539
540 // Create reference footprint TRIM_1.1 and target footprint TRIM_2.1
541 FOOTPRINT fpRef( nullptr );
542 fpRef.SetFPID( fpid );
543 fpRef.SetReference( wxT( "TRIM_1.1" ) );
544
545 FOOTPRINT fpTarget( nullptr );
546 fpTarget.SetFPID( fpid );
547 fpTarget.SetReference( wxT( "TRIM_2.1" ) );
548
549 // Create matching pad structures
550 PAD padRef1( &fpRef );
551 padRef1.SetNumber( wxT( "1" ) );
552 padRef1.SetNetCode( 1 );
553 fpRef.Add( &padRef1 );
554
555 PAD padRef2( &fpRef );
556 padRef2.SetNumber( wxT( "2" ) );
557 padRef2.SetNetCode( 2 );
558 fpRef.Add( &padRef2 );
559
560 PAD padTarget1( &fpTarget );
561 padTarget1.SetNumber( wxT( "1" ) );
562 padTarget1.SetNetCode( 3 );
563 fpTarget.Add( &padTarget1 );
564
565 PAD padTarget2( &fpTarget );
566 padTarget2.SetNumber( wxT( "2" ) );
567 padTarget2.SetNetCode( 4 );
568 fpTarget.Add( &padTarget2 );
569
570 // Build connection graphs
571 cgRef->AddFootprint( &fpRef, VECTOR2I( 0, 0 ) );
572 cgTarget->AddFootprint( &fpTarget, VECTOR2I( 0, 0 ) );
573
574 cgRef->BuildConnectivity();
575 cgTarget->BuildConnectivity();
576
577 // Check that the components are considered the same kind
578 BOOST_CHECK_EQUAL( cgRef->Components().size(), 1 );
579 BOOST_CHECK_EQUAL( cgTarget->Components().size(), 1 );
580
581 COMPONENT* cmpRef = cgRef->Components()[0];
582 COMPONENT* cmpTarget = cgTarget->Components()[0];
583
584 bool sameKind = cmpRef->IsSameKind( *cmpTarget );
585
586 BOOST_TEST_MESSAGE( wxString::Format( "TRIM_1.1 and TRIM_2.1 IsSameKind: %d", sameKind ? 1 : 0 ) );
587 BOOST_CHECK( sameKind );
588
589 // Test topology matching
591 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
592 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
593
594 BOOST_TEST_MESSAGE( wxString::Format( "Topology match result: %d", status ? 1 : 0 ) );
595
596 if( !status && !details.empty() )
597 {
598 for( const auto& reason : details )
599 {
600 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
601 reason.m_reference, reason.m_candidate, reason.m_reason ) );
602 }
603 }
604
605 BOOST_CHECK( status );
606 BOOST_CHECK( details.empty() );
607
608 // Cleanup: remove pads before footprints go out of scope
609 fpRef.Pads().clear();
610 fpTarget.Pads().clear();
611}
612
613
622BOOST_FIXTURE_TEST_CASE( GenerateRuleAreasIncludesChildSheets, MULTICHANNEL_TEST_FIXTURE )
623{
624 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
625
626 TOOL_MANAGER toolMgr;
627 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
628
629 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
630
632 toolMgr.RegisterTool( mtTool );
633
635
636 auto ruleData = mtTool->GetData();
637
638 RULE_AREA* leafArea = nullptr;
639 RULE_AREA* midArea = nullptr;
640 RULE_AREA* topArea = nullptr;
641
642 for( RULE_AREA& ra : ruleData->m_areas )
643 {
644 if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/io01/" ) )
645 leafArea = &ra;
646 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/bank0/" ) )
647 midArea = &ra;
648 else if( ra.m_sheetPath == wxT( "/io_drivers_fp/" ) )
649 topArea = &ra;
650 }
651
652 BOOST_REQUIRE( leafArea != nullptr );
653 BOOST_REQUIRE( midArea != nullptr );
654 BOOST_REQUIRE( topArea != nullptr );
655
656 BOOST_TEST_MESSAGE( wxString::Format( "Leaf /io_drivers_fp/bank0/io01/ components: %d",
657 static_cast<int>( leafArea->m_components.size() ) ) );
658 BOOST_TEST_MESSAGE( wxString::Format( "Mid /io_drivers_fp/bank0/ components: %d",
659 static_cast<int>( midArea->m_components.size() ) ) );
660 BOOST_TEST_MESSAGE( wxString::Format( "Top /io_drivers_fp/ components: %d",
661 static_cast<int>( topArea->m_components.size() ) ) );
662
663 // Leaf sheet has 31 direct components and no children
664 BOOST_CHECK_EQUAL( leafArea->m_components.size(), 31 );
665
666 // Mid-level sheet has 7 direct + 4 child sheets * 31 each = 131
667 BOOST_CHECK_EQUAL( midArea->m_components.size(), 131 );
668
669 // Top-level sheet has 3 direct + 4 banks * 131 each = 527
670 BOOST_CHECK_EQUAL( topArea->m_components.size(), 527 );
671
672 // Mid-level components must be a superset of leaf components
673 for( FOOTPRINT* fp : leafArea->m_components )
674 BOOST_CHECK( midArea->m_components.count( fp ) > 0 );
675
676 // Top-level components must be a superset of mid-level components
677 for( FOOTPRINT* fp : midArea->m_components )
678 BOOST_CHECK( topArea->m_components.count( fp ) > 0 );
679}
680
681
687{
689
690 KI_TEST::LoadBoard( m_settingsManager, "vme-wren", m_board );
691
692 TOOL_MANAGER toolMgr;
693 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
694
695 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
696
698 toolMgr.RegisterTool( mtTool );
699
701
702 auto ruleData = mtTool->GetData();
703
704 ruleData->m_replaceExisting = true;
705
706 for( RULE_AREA& ra : ruleData->m_areas )
707 {
708 if( ra.m_sheetName == wxT( "io_driver.kicad_sch" ) )
709 ra.m_generateEnabled = true;
710 }
711
712 TOOL_EVENT dummyEvent;
713 mtTool->AutogenerateRuleAreas( dummyEvent );
714 mtTool->FindExistingRuleAreas();
715
716 RULE_AREA* refArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank3/io78/" ) );
717 RULE_AREA* targetArea = findRuleAreaByPartialName( mtTool, wxT( "io_drivers_fp/bank2/io78/" ) );
718
719 BOOST_REQUIRE( refArea != nullptr );
720 BOOST_REQUIRE( targetArea != nullptr );
721
722 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
723 targetArea->m_components );
724 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
725 refArea->m_components );
726
727 // Pre-cancelled: should return false immediately with empty result
728 {
729 std::atomic<bool> cancelled( true );
730 std::atomic<int> matched( 0 );
731 std::atomic<int> total( 0 );
732
734 params.m_cancelled = &cancelled;
735 params.m_matchedComponents = &matched;
736 params.m_totalComponents = &total;
737
739 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
740
741 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
742
743 BOOST_CHECK( !status );
744 BOOST_CHECK( result.empty() );
745
746 BOOST_TEST_MESSAGE( "Pre-cancelled FindIsomorphism correctly returned false" );
747 }
748
749 // Normal run with progress reporting
750 {
751 std::atomic<bool> cancelled( false );
752 std::atomic<int> matched( 0 );
753 std::atomic<int> total( 0 );
754
756 params.m_cancelled = &cancelled;
757 params.m_matchedComponents = &matched;
758 params.m_totalComponents = &total;
759
761 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
762
763 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details, params );
764
765 BOOST_CHECK( status );
766
767 int finalMatched = matched.load();
768 int finalTotal = total.load();
769
770 BOOST_TEST_MESSAGE( wxString::Format( "Progress: matched=%d, total=%d", finalMatched, finalTotal ) );
771
772 BOOST_CHECK( finalTotal > 0 );
773 BOOST_CHECK_EQUAL( finalMatched, finalTotal );
774 }
775
776 // Sanity check: same graphs without params still succeed
777 {
779 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
780
781 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
782
783 BOOST_CHECK( status );
784 BOOST_CHECK( !result.empty() );
785
786 BOOST_TEST_MESSAGE( "Default params FindIsomorphism still succeeds" );
787 }
788}
789
790
801BOOST_FIXTURE_TEST_CASE( TopoMatchGlobalNetHierarchicalPins, MULTICHANNEL_TEST_FIXTURE )
802{
804
805 KI_TEST::LoadBoard( m_settingsManager, "issue21739/topology_mismatch", m_board );
806
807 TOOL_MANAGER toolMgr;
808 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
809
810 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
811
813 toolMgr.RegisterTool( mtTool );
814
815 mtTool->FindExistingRuleAreas();
816
817 auto ruleData = mtTool->GetData();
818
819 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
820 static_cast<int>( ruleData->m_areas.size() ) ) );
821
822 BOOST_REQUIRE( ruleData->m_areas.size() >= 2 );
823
824 RULE_AREA* ch0Area = nullptr;
825 RULE_AREA* ch1Area = nullptr;
826
827 for( RULE_AREA& ra : ruleData->m_areas )
828 {
829 if( !ra.m_zone )
830 continue;
831
832 wxString source = ra.m_zone->GetPlacementAreaSource();
833
834 if( source == wxT( "/i2c_thingy_ch0/" ) )
835 ch0Area = &ra;
836 else if( source == wxT( "/i2c_thingy_ch1/" ) )
837 ch1Area = &ra;
838 }
839
840 BOOST_REQUIRE_MESSAGE( ch0Area != nullptr, "Could not find i2c_thingy_ch0 rule area" );
841 BOOST_REQUIRE_MESSAGE( ch1Area != nullptr, "Could not find i2c_thingy_ch1 rule area" );
842
843 BOOST_TEST_MESSAGE( wxString::Format( "ch0 components: %d, ch1 components: %d",
844 static_cast<int>( ch0Area->m_components.size() ),
845 static_cast<int>( ch1Area->m_components.size() ) ) );
846
847 BOOST_CHECK_EQUAL( ch0Area->m_components.size(), ch1Area->m_components.size() );
848
849 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( ch0Area->m_components,
850 ch1Area->m_components );
851 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( ch1Area->m_components,
852 ch0Area->m_components );
853
855 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
856 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
857
858 if( !status )
859 {
860 for( const auto& reason : details )
861 {
862 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
863 reason.m_reference, reason.m_candidate,
864 reason.m_reason ) );
865 }
866 }
867
868 BOOST_CHECK_MESSAGE( status,
869 "Topology match failed for channels with hierarchical pins "
870 "tied to global nets (issue 21739)" );
871}
872
873
880BOOST_FIXTURE_TEST_CASE( TopoMatchBoundarySignalNetNotExcluded, MULTICHANNEL_TEST_FIXTURE )
881{
883
884 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
885
886 // The board forces consecutive net codes, so register by name and use the assigned code.
887 auto addNet = [&]( const wxString& aName ) -> int
888 {
889 NETINFO_ITEM* net = new NETINFO_ITEM( board.get(), aName );
890 board->Add( net );
891 return net->GetNetCode();
892 };
893
894 const int netGnd = addNet( wxT( "GND" ) );
895 const int netVcc = addNet( wxT( "+5VP" ) );
896 const int netChainInRef = addNet( wxT( "Net-(D-RefChainIn)" ) );
897 const int netBridge = addNet( wxT( "Net-(D38-DOUT)" ) ); // reference output AND target input
898 const int netChainOutTgt = addNet( wxT( "Net-(D43-DOUT)" ) );
899 const int netD2Dout = addNet( wxT( "unconnected-(D2-DOUT)" ) );
900 const int netD3Dout = addNet( wxT( "unconnected-(D3-DOUT)" ) );
901 const int netD5Dout = addNet( wxT( "unconnected-(D5-DOUT)" ) );
902 const int netD6Dout = addNet( wxT( "unconnected-(D6-DOUT)" ) );
903
904 LIB_ID ledId( wxT( "TestLib" ), wxT( "WS2812" ) );
905
906 // Four-pad addressable LED: pad 1 = DOUT, pad 2 = GND, pad 3 = DIN, pad 4 = VCC.
907 auto makeLed = [&]( const wxString& aRef, int aDout, int aDin ) -> FOOTPRINT*
908 {
909 FOOTPRINT* fp = new FOOTPRINT( board.get() );
910 fp->SetFPID( ledId );
911 fp->SetReference( aRef );
912 board->Add( fp );
913
914 auto addPad = [&]( const wxString& aNumber, int aNetCode )
915 {
916 PAD* pad = new PAD( fp );
917 pad->SetNumber( aNumber );
919 pad->SetSize( PADSTACK::ALL_LAYERS,
920 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
921 pad->SetLayerSet( LSET( { F_Cu } ) );
922 pad->SetNetCode( aNetCode );
923 fp->Add( pad );
924 };
925
926 addPad( wxT( "1" ), aDout );
927 addPad( wxT( "2" ), netGnd );
928 addPad( wxT( "3" ), aDin );
929 addPad( wxT( "4" ), netVcc );
930
931 return fp;
932 };
933
934 // Reference area (design block source instance): three LEDs share the DIN rail; only the
935 // representative LED drives the chain output (the bridge net), the other two are unconnected.
936 std::set<FOOTPRINT*> refFps;
937 refFps.insert( makeLed( wxT( "D1" ), netBridge, netChainInRef ) );
938 refFps.insert( makeLed( wxT( "D2" ), netD2Dout, netChainInRef ) );
939 refFps.insert( makeLed( wxT( "D3" ), netD3Dout, netChainInRef ) );
940
941 // Target area (downstream instance): the three LEDs' DIN rail IS the bridge net coming from
942 // the design block source, and the representative LED drives a fresh chain output.
943 std::set<FOOTPRINT*> tgtFps;
944 tgtFps.insert( makeLed( wxT( "D4" ), netChainOutTgt, netBridge ) );
945 tgtFps.insert( makeLed( wxT( "D5" ), netD5Dout, netBridge ) );
946 tgtFps.insert( makeLed( wxT( "D6" ), netD6Dout, netBridge ) );
947
948 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refFps, tgtFps );
949 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( tgtFps, refFps );
950
952 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
953 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
954
955 if( !status )
956 {
957 for( const auto& reason : details )
958 {
959 BOOST_TEST_MESSAGE( wxString::Format( "Mismatch: %s <-> %s: %s",
960 reason.m_reference, reason.m_candidate,
961 reason.m_reason ) );
962 }
963 }
964
965 BOOST_CHECK_MESSAGE( status,
966 "Topology match failed because the design-block boundary signal net "
967 "was misclassified as a global rail and excluded asymmetrically" );
968 BOOST_CHECK_EQUAL( result.size(), refFps.size() );
969}
970
971
981BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutCopiesSilkscreen, MULTICHANNEL_TEST_FIXTURE )
982{
983 m_board = std::make_unique<BOARD>();
984 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
985
986 // Net for the single connection between the two footprints in each block.
987 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
988 m_board->Add( net );
989
990 auto makeFootprint =
991 [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
992 {
993 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
994 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "R" ) ) );
995 fp->SetReference( aRef );
996 fp->SetPosition( aPos );
997
998 PAD* pad = new PAD( fp );
999 pad->SetNumber( wxT( "1" ) );
1000 pad->SetNet( net );
1001 pad->SetPosition( aPos );
1002 pad->SetSize( F_Cu,
1003 VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1004 pad->SetLayerSet( LSET( { F_Cu } ) );
1005 fp->Add( pad );
1006
1007 m_board->Add( fp );
1008 return fp;
1009 };
1010
1011 // Source: a "design block" with two matched footprints and a silkscreen rectangle that
1012 // is not associated with any footprint.
1013 FOOTPRINT* refFp1 = makeFootprint( wxT( "R1" ),
1014 VECTOR2I( pcbIUScale.mmToIU( 0 ), pcbIUScale.mmToIU( 0 ) ) );
1015 FOOTPRINT* refFp2 = makeFootprint( wxT( "R2" ),
1016 VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 0 ) ) );
1017
1018 PCB_SHAPE* silkRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1019 silkRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1020 silkRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 12 ), pcbIUScale.mmToIU( 2 ) ) );
1021 silkRect->SetLayer( F_SilkS );
1022 silkRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1023 m_board->Add( silkRect );
1024
1025 // Destination: a group containing the matched pair so RepeatLayout can find a parent group.
1026 FOOTPRINT* destFp1 = makeFootprint( wxT( "R3" ),
1027 VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1028 FOOTPRINT* destFp2 = makeFootprint( wxT( "R4" ),
1029 VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1030
1031 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1032 destGroup->SetName( wxT( "design-block-dest" ) );
1033 destGroup->AddItem( destFp1 );
1034 destGroup->AddItem( destFp2 );
1035 m_board->Add( destGroup );
1036
1037 // Unrelated silkscreen drawing inside the destination bounding box but not part of the
1038 // destination group. Apply Design Block Layout must not delete or claim this item.
1039 PCB_SHAPE* unrelatedSilk = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1040 unrelatedSilk->SetStart( VECTOR2I( pcbIUScale.mmToIU( 52 ), pcbIUScale.mmToIU( 52 ) ) );
1041 unrelatedSilk->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 58 ), pcbIUScale.mmToIU( 52 ) ) );
1042 unrelatedSilk->SetLayer( F_SilkS );
1043 unrelatedSilk->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1044 m_board->Add( unrelatedSilk );
1045
1046 int silkBefore = 0;
1047
1048 for( BOARD_ITEM* item : m_board->Drawings() )
1049 {
1050 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1051 silkBefore++;
1052 }
1053
1054 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1055
1056 RULE_AREA dbRA;
1058 dbRA.m_components.insert( refFp1 );
1059 dbRA.m_components.insert( refFp2 );
1060 dbRA.m_designBlockItems.insert( refFp1 );
1061 dbRA.m_designBlockItems.insert( refFp2 );
1062 dbRA.m_designBlockItems.insert( silkRect );
1063
1064 // The Apply Design Block flow uses a synthetic copper-only rule area zone. The destination
1065 // zone is a temporary zone never added to the board.
1066 dbRA.m_zone = new ZONE( m_board.get() );
1067 dbRA.m_zone->SetIsRuleArea( true );
1070 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1071 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1072
1073 RULE_AREA destRA;
1075 destRA.m_components.insert( destFp1 );
1076 destRA.m_components.insert( destFp2 );
1077
1078 destRA.m_zone = new ZONE( m_board.get() );
1079 destRA.m_zone->SetIsRuleArea( true );
1080 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1082 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1083 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1084
1085 TOOL_MANAGER toolMgr;
1086 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1087 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1088
1090 toolMgr.RegisterTool( mtTool );
1091
1092 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1093 .m_connectedRoutingOnly = false,
1094 .m_copyPlacement = true,
1095 .m_copyOtherItems = true,
1096 .m_groupItems = false,
1097 .m_includeLockedItems = true,
1098 .m_anchorFp = nullptr };
1099
1100 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1101 BOOST_CHECK_MESSAGE( result >= 0, "RepeatLayout failed" );
1102
1103 delete dbRA.m_zone;
1104 delete destRA.m_zone;
1105
1106 // Verify a third silkscreen shape (the duplicated rectangle) was added and grouped under
1107 // the destination group, that the unrelated silk segment was preserved, and that the
1108 // duplicated rectangle landed inside the destination region.
1109 int silkAfter = 0;
1110 int silkInGroup = 0;
1111 bool unrelatedSurvived = false;
1112 PCB_SHAPE* copiedRect = nullptr;
1113
1114 for( BOARD_ITEM* item : m_board->Drawings() )
1115 {
1116 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1117 continue;
1118
1119 silkAfter++;
1120
1121 if( item == unrelatedSilk )
1122 unrelatedSurvived = true;
1123
1124 if( item->GetParentGroup() == destGroup )
1125 {
1126 silkInGroup++;
1127
1128 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1129
1130 if( shape->GetShape() == SHAPE_T::RECTANGLE )
1131 copiedRect = shape;
1132 }
1133 }
1134
1135 BOOST_CHECK_MESSAGE( silkAfter == 3,
1136 wxString::Format( "Expected 3 silkscreen shapes after Apply Design Block "
1137 "Layout (2 original + 1 copy), found %d (issue 24372)",
1138 silkAfter ) );
1139 BOOST_CHECK_MESSAGE( silkInGroup == 1,
1140 wxString::Format( "Expected 1 silkscreen shape in destination group, "
1141 "found %d (issue 24372)",
1142 silkInGroup ) );
1143 BOOST_CHECK_MESSAGE( unrelatedSurvived,
1144 "Unrelated silkscreen drawing outside the design block group was "
1145 "deleted by Apply Design Block Layout (issue 24372)" );
1146 BOOST_REQUIRE( copiedRect != nullptr );
1147
1148 // The copied rectangle should sit near the destination footprints (offset by ~50mm from
1149 // the source position), not at the original source location.
1150 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1151}
1152
1153
1162BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsDuplicateSilkText, MULTICHANNEL_TEST_FIXTURE )
1163{
1164 m_board = std::make_unique<BOARD>();
1165 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1166
1167 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1168 m_board->Add( net );
1169
1170 auto makeFootprint = [&]( const wxString& aRef, const VECTOR2I& aPos ) -> FOOTPRINT*
1171 {
1172 FOOTPRINT* fp = new FOOTPRINT( m_board.get() );
1173 fp->SetFPID( LIB_ID( wxT( "TestLib" ), wxT( "SW" ) ) );
1174 fp->SetReference( aRef );
1175 fp->SetPosition( aPos );
1176
1177 // Two silkscreen texts with the same content, one on each board side.
1178 for( PCB_LAYER_ID layer : { F_SilkS, B_SilkS } )
1179 {
1180 PCB_TEXT* txt = new PCB_TEXT( fp );
1181 txt->SetText( wxT( "${REFERENCE}" ) );
1182 txt->SetLayer( layer );
1183 txt->SetPosition( aPos + VECTOR2I( 0, pcbIUScale.mmToIU( -8 ) ) );
1184 fp->Add( txt );
1185 }
1186
1187 PAD* pad = new PAD( fp );
1188 pad->SetNumber( wxT( "1" ) );
1189 pad->SetNet( net );
1190 pad->SetPosition( aPos );
1191 pad->SetSize( F_Cu, VECTOR2I( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) ) );
1192 pad->SetLayerSet( LSET( { F_Cu } ) );
1193 fp->Add( pad );
1194
1195 m_board->Add( fp );
1196 return fp;
1197 };
1198
1199 auto countSilkText = [&]( FOOTPRINT* fp, PCB_LAYER_ID layer ) -> int
1200 {
1201 int count = 0;
1202
1203 for( BOARD_ITEM* item : fp->GraphicalItems() )
1204 {
1205 if( item->Type() == PCB_TEXT_T && item->GetLayer() == layer )
1206 count++;
1207 }
1208
1209 return count;
1210 };
1211
1212 FOOTPRINT* refFp1 = makeFootprint( wxT( "SW1" ), VECTOR2I( 0, 0 ) );
1213 FOOTPRINT* refFp2 = makeFootprint( wxT( "SW2" ), VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ) );
1214
1215 FOOTPRINT* destFp1 = makeFootprint( wxT( "SW3" ), VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1216 FOOTPRINT* destFp2 = makeFootprint( wxT( "SW4" ), VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 50 ) ) );
1217
1218 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1219 destGroup->SetName( wxT( "design-block-dest" ) );
1220 destGroup->AddItem( destFp1 );
1221 destGroup->AddItem( destFp2 );
1222 m_board->Add( destGroup );
1223
1224 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, F_SilkS ), 1 );
1225 BOOST_REQUIRE_EQUAL( countSilkText( destFp1, B_SilkS ), 1 );
1226
1227 RULE_AREA dbRA;
1229 dbRA.m_components.insert( refFp1 );
1230 dbRA.m_components.insert( refFp2 );
1231 dbRA.m_designBlockItems.insert( refFp1 );
1232 dbRA.m_designBlockItems.insert( refFp2 );
1233
1234 dbRA.m_zone = new ZONE( m_board.get() );
1235 dbRA.m_zone->SetIsRuleArea( true );
1237 dbRA.m_zone->AddPolygon(
1239 VECTOR2I( pcbIUScale.mmToIU( 15 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1240
1241 RULE_AREA destRA;
1243 destRA.m_components.insert( destFp1 );
1244 destRA.m_components.insert( destFp2 );
1245
1246 destRA.m_zone = new ZONE( m_board.get() );
1247 destRA.m_zone->SetIsRuleArea( true );
1248 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1250 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1251 VECTOR2I( pcbIUScale.mmToIU( 65 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1252
1253 TOOL_MANAGER toolMgr;
1254 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1255 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1256
1258 toolMgr.RegisterTool( mtTool );
1259
1260 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1261 .m_connectedRoutingOnly = false,
1262 .m_copyPlacement = true,
1263 .m_copyOtherItems = true,
1264 .m_groupItems = false,
1265 .m_includeLockedItems = true,
1266 .m_anchorFp = nullptr };
1267
1268 wxString err;
1269 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1270 BOOST_CHECK_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1271
1272 delete dbRA.m_zone;
1273 delete destRA.m_zone;
1274
1275 for( FOOTPRINT* destFp : { destFp1, destFp2 } )
1276 {
1277 BOOST_CHECK_MESSAGE( countSilkText( destFp, F_SilkS ) == 1,
1278 wxString::Format( "%s lost its F.Silkscreen reference text "
1279 "(issue 24583): F=%d B=%d",
1280 destFp->GetReference(), countSilkText( destFp, F_SilkS ),
1281 countSilkText( destFp, B_SilkS ) ) );
1282 BOOST_CHECK_EQUAL( countSilkText( destFp, B_SilkS ), 1 );
1283 }
1284}
1285
1286
1291BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutGraphicsOnlyBlock, MULTICHANNEL_TEST_FIXTURE )
1292{
1293 m_board = std::make_unique<BOARD>();
1294 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1295
1296 auto makeSilkRect = [&]( const VECTOR2I& aStart, const VECTOR2I& aEnd ) -> PCB_SHAPE*
1297 {
1298 PCB_SHAPE* s = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1299 s->SetStart( aStart );
1300 s->SetEnd( aEnd );
1301 s->SetLayer( F_SilkS );
1302 s->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1303 m_board->Add( s );
1304 return s;
1305 };
1306
1307 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1308 PCB_SHAPE* srcRect = makeSilkRect( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ),
1309 VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1310
1311 // Destination: footprint-free group with one silkscreen item so the group exists.
1312 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1313 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1314 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1315 destPlaceholder->SetLayer( F_SilkS );
1316 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1317 m_board->Add( destPlaceholder );
1318
1319 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1320 destGroup->SetName( wxT( "design-block-dest" ) );
1321 destGroup->AddItem( destPlaceholder );
1322 m_board->Add( destGroup );
1323
1324 int silkBefore = 0;
1325
1326 for( BOARD_ITEM* item : m_board->Drawings() )
1327 {
1328 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS )
1329 silkBefore++;
1330 }
1331
1332 BOOST_REQUIRE_EQUAL( silkBefore, 2 );
1333
1334 RULE_AREA dbRA;
1336 dbRA.m_designBlockItems.insert( srcRect ); // no footprints -> m_components stays empty
1337
1338 dbRA.m_zone = new ZONE( m_board.get() );
1339 dbRA.m_zone->SetIsRuleArea( true );
1341 dbRA.m_zone->AddPolygon(
1343 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1344 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1345
1346 RULE_AREA destRA;
1348 destRA.m_group = destGroup; // set explicitly: no footprint to recover it from
1349
1350 destRA.m_zone = new ZONE( m_board.get() );
1351 destRA.m_zone->SetIsRuleArea( true );
1352 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1354 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1355 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1356 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1357
1358 TOOL_MANAGER toolMgr;
1359 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1360 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1361
1363 toolMgr.RegisterTool( mtTool );
1364
1365 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1366 .m_connectedRoutingOnly = false,
1367 .m_copyPlacement = true,
1368 .m_copyOtherItems = true,
1369 .m_groupItems = false,
1370 .m_includeLockedItems = true,
1371 .m_anchorFp = nullptr };
1372
1373 wxString err;
1374 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1375
1376 delete dbRA.m_zone;
1377 delete destRA.m_zone;
1378
1379 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free "
1380 "(graphics-only) design block (issue 24592): %s",
1381 err ) );
1382
1383 // The block's rectangle is copied into the target region and grouped, replacing the group's
1384 // prior placeholder graphic (source + 1 copy = 2 silkscreen shapes).
1385 int silkAfter = 0;
1386 PCB_SHAPE* copiedRect = nullptr;
1387
1388 for( BOARD_ITEM* item : m_board->Drawings() )
1389 {
1390 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != F_SilkS )
1391 continue;
1392
1393 silkAfter++;
1394
1395 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1396
1397 if( shape != srcRect && shape->GetShape() == SHAPE_T::RECTANGLE )
1398 copiedRect = shape;
1399 }
1400
1401 BOOST_CHECK_MESSAGE( silkAfter == 2, wxString::Format( "Expected 2 silkscreen shapes after Apply Design Block "
1402 "Layout (source + 1 copy, placeholder replaced), found "
1403 "%d (issue 24592)",
1404 silkAfter ) );
1405 BOOST_REQUIRE_MESSAGE( copiedRect != nullptr, "Footprint-free block graphic was not copied (issue 24592)" );
1406 BOOST_CHECK_MESSAGE( copiedRect->GetParentGroup() == destGroup,
1407 "Copied block graphic was not added to the destination group (issue 24592)" );
1408 BOOST_CHECK_GT( copiedRect->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1409}
1410
1411
1416BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeCopperIsNoNet, MULTICHANNEL_TEST_FIXTURE )
1417{
1418 m_board = std::make_unique<BOARD>();
1419 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1420
1421 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1422 m_board->Add( net );
1423
1424 // Source: footprint-free block with one track on a real net.
1425 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
1426 srcTrack->SetLayer( F_Cu );
1427 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1428 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( 0 ) ) );
1429 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 0 ) ) );
1430 srcTrack->SetNet( net );
1431 m_board->Add( srcTrack );
1432
1433 BOOST_REQUIRE_EQUAL( srcTrack->GetNetCode(), 1 );
1434
1435 // Destination: footprint-free group with a placeholder so the group exists.
1436 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1437 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1438 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1439 destPlaceholder->SetLayer( F_SilkS );
1440 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1441 m_board->Add( destPlaceholder );
1442
1443 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1444 destGroup->SetName( wxT( "design-block-dest" ) );
1445 destGroup->AddItem( destPlaceholder );
1446 m_board->Add( destGroup );
1447
1448 RULE_AREA dbRA;
1450 dbRA.m_designBlockItems.insert( srcTrack ); // no footprints -> m_components stays empty
1451
1452 dbRA.m_zone = new ZONE( m_board.get() );
1453 dbRA.m_zone->SetIsRuleArea( true );
1455 dbRA.m_zone->AddPolygon(
1457 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1458 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1459
1460 RULE_AREA destRA;
1462 destRA.m_group = destGroup;
1463
1464 destRA.m_zone = new ZONE( m_board.get() );
1465 destRA.m_zone->SetIsRuleArea( true );
1466 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1468 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1469 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1470 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1471
1472 TOOL_MANAGER toolMgr;
1473 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1474 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1475
1477 toolMgr.RegisterTool( mtTool );
1478
1479 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1480 .m_connectedRoutingOnly = false,
1481 .m_copyPlacement = true,
1482 .m_copyOtherItems = true,
1483 .m_groupItems = false,
1484 .m_includeLockedItems = true,
1485 .m_anchorFp = nullptr };
1486
1487 wxString err;
1488 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1489
1490 delete dbRA.m_zone;
1491 delete destRA.m_zone;
1492
1493 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed for a footprint-free copper "
1494 "block (issue 24592): %s",
1495 err ) );
1496
1497 // Copied track should be in the target region, no-net, and grouped.
1498 PCB_TRACK* copiedTrack = nullptr;
1499
1500 for( PCB_TRACK* track : m_board->Tracks() )
1501 {
1502 if( track != srcTrack )
1503 copiedTrack = track;
1504 }
1505
1506 BOOST_REQUIRE_MESSAGE( copiedTrack != nullptr, "Footprint-free block track was not copied (issue 24592)" );
1507 BOOST_CHECK_MESSAGE( copiedTrack->GetNetCode() == 0,
1508 wxString::Format( "Copied copper should be no-net, got net code %d "
1509 "(issue 24592)",
1510 copiedTrack->GetNetCode() ) );
1511 BOOST_CHECK_MESSAGE( copiedTrack->GetParentGroup() == destGroup,
1512 "Copied copper was not added to the destination group (issue 24592)" );
1513 BOOST_CHECK_GT( copiedTrack->GetStart().x, pcbIUScale.mmToIU( 30 ) );
1514
1515 // The original source track must keep its net.
1516 BOOST_CHECK_EQUAL( srcTrack->GetNetCode(), 1 );
1517}
1518
1519
1525BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutFootprintFreeReapplyReplaces, MULTICHANNEL_TEST_FIXTURE )
1526{
1527 m_board = std::make_unique<BOARD>();
1528 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1529
1530 // Source: footprint-free block with one silkscreen rectangle centered on origin.
1531 PCB_SHAPE* srcRect = new PCB_SHAPE( m_board.get(), SHAPE_T::RECTANGLE );
1532 srcRect->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), pcbIUScale.mmToIU( -2 ) ) );
1533 srcRect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), pcbIUScale.mmToIU( 2 ) ) );
1534 srcRect->SetLayer( F_SilkS );
1535 srcRect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1536 m_board->Add( srcRect );
1537
1538 // Destination: footprint-free group with a placeholder so the group exists.
1539 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1540 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1541 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1542 destPlaceholder->SetLayer( F_SilkS );
1543 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1544 m_board->Add( destPlaceholder );
1545
1546 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1547 destGroup->SetName( wxT( "design-block-dest" ) );
1548 destGroup->AddItem( destPlaceholder );
1549 m_board->Add( destGroup );
1550
1551 TOOL_MANAGER toolMgr;
1552 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1553 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1554
1556 toolMgr.RegisterTool( mtTool );
1557
1558 auto applyOnce = [&]() -> int
1559 {
1560 RULE_AREA dbRA;
1562 dbRA.m_designBlockItems.insert( srcRect );
1563
1564 dbRA.m_zone = new ZONE( m_board.get() );
1565 dbRA.m_zone->SetIsRuleArea( true );
1568 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( -5 ), pcbIUScale.mmToIU( -5 ) ),
1569 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1570 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1571
1572 RULE_AREA destRA;
1574 destRA.m_group = destGroup;
1575
1576 destRA.m_zone = new ZONE( m_board.get() );
1577 destRA.m_zone->SetIsRuleArea( true );
1578 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1580 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1581 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1582 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1583
1584 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1585 .m_connectedRoutingOnly = false,
1586 .m_copyPlacement = true,
1587 .m_copyOtherItems = true,
1588 .m_groupItems = false,
1589 .m_includeLockedItems = true,
1590 .m_anchorFp = nullptr };
1591
1592 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts );
1593
1594 delete dbRA.m_zone;
1595 delete destRA.m_zone;
1596 return result;
1597 };
1598
1599 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "First apply failed (issue 24592)" );
1600 BOOST_REQUIRE_MESSAGE( applyOnce() >= 0, "Second apply failed (issue 24592)" );
1601
1602 // After two applies there must be exactly the source rectangle plus one copy. A third
1603 // rectangle would mean the second apply stacked instead of replacing.
1604 int rects = 0;
1605
1606 for( BOARD_ITEM* item : m_board->Drawings() )
1607 {
1608 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == F_SilkS
1609 && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::RECTANGLE )
1610 {
1611 rects++;
1612 }
1613 }
1614
1615 BOOST_CHECK_MESSAGE( rects == 2, wxString::Format( "Re-apply stacked copies: expected 2 rectangles "
1616 "(source + 1 copy), found %d (issue 24592)",
1617 rects ) );
1618}
1619
1620
1628BOOST_FIXTURE_TEST_CASE( ApplyDesignBlockLayoutKeepsOtherGroupRouting, MULTICHANNEL_TEST_FIXTURE )
1629{
1630 m_board = std::make_unique<BOARD>();
1631 m_board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
1632
1633 NETINFO_ITEM* net = new NETINFO_ITEM( m_board.get(), wxT( "NET1" ), 1 );
1634 m_board->Add( net );
1635
1636 // Source block: one track centered on origin.
1637 PCB_TRACK* srcTrack = new PCB_TRACK( m_board.get() );
1638 srcTrack->SetLayer( F_Cu );
1639 srcTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1640 srcTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( -2 ), 0 ) );
1641 srcTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 2 ), 0 ) );
1642 srcTrack->SetNet( net );
1643 m_board->Add( srcTrack );
1644
1645 // Destination group with a placeholder so the group exists.
1646 PCB_SHAPE* destPlaceholder = new PCB_SHAPE( m_board.get(), SHAPE_T::SEGMENT );
1647 destPlaceholder->SetStart( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 49 ) ) );
1648 destPlaceholder->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 51 ), pcbIUScale.mmToIU( 49 ) ) );
1649 destPlaceholder->SetLayer( F_SilkS );
1650 destPlaceholder->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.15 ), LINE_STYLE::SOLID ) );
1651 m_board->Add( destPlaceholder );
1652
1653 PCB_GROUP* destGroup = new PCB_GROUP( m_board.get() );
1654 destGroup->SetName( wxT( "design-block-dest" ) );
1655 destGroup->AddItem( destPlaceholder );
1656 m_board->Add( destGroup );
1657
1658 // A sibling instance's track, inside the destination area but owned by another group.
1659 PCB_TRACK* siblingTrack = new PCB_TRACK( m_board.get() );
1660 siblingTrack->SetLayer( F_Cu );
1661 siblingTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1662 siblingTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 47 ) ) );
1663 siblingTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 47 ) ) );
1664 m_board->Add( siblingTrack );
1665
1666 PCB_GROUP* siblingGroup = new PCB_GROUP( m_board.get() );
1667 siblingGroup->SetName( wxT( "design-block-sibling" ) );
1668 siblingGroup->AddItem( siblingTrack );
1669 m_board->Add( siblingGroup );
1670
1671 // Loose, ungrouped routing inside the destination area, which still gets replaced.
1672 PCB_TRACK* looseTrack = new PCB_TRACK( m_board.get() );
1673 looseTrack->SetLayer( F_Cu );
1674 looseTrack->SetWidth( pcbIUScale.mmToIU( 0.25 ) );
1675 looseTrack->SetStart( VECTOR2I( pcbIUScale.mmToIU( 47 ), pcbIUScale.mmToIU( 53 ) ) );
1676 looseTrack->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 49 ), pcbIUScale.mmToIU( 53 ) ) );
1677 m_board->Add( looseTrack );
1678
1679 RULE_AREA dbRA;
1681 dbRA.m_designBlockItems.insert( srcTrack );
1682
1683 dbRA.m_zone = new ZONE( m_board.get() );
1684 dbRA.m_zone->SetIsRuleArea( true );
1686 dbRA.m_zone->AddPolygon(
1688 VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) ) ) );
1689 dbRA.m_center = dbRA.m_zone->Outline()->COutline( 0 ).Centre();
1690
1691 RULE_AREA destRA;
1693 destRA.m_group = destGroup;
1694
1695 destRA.m_zone = new ZONE( m_board.get() );
1696 destRA.m_zone->SetIsRuleArea( true );
1697 destRA.m_zone->SetLayerSet( LSET::AllCuMask() );
1699 BOX2I::ByCorners( VECTOR2I( pcbIUScale.mmToIU( 45 ), pcbIUScale.mmToIU( 45 ) ),
1700 VECTOR2I( pcbIUScale.mmToIU( 55 ), pcbIUScale.mmToIU( 55 ) ) ) ) );
1701 destRA.m_center = destRA.m_zone->Outline()->COutline( 0 ).Centre();
1702
1703 TOOL_MANAGER toolMgr;
1704 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1705 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1706
1708 toolMgr.RegisterTool( mtTool );
1709
1710 REPEAT_LAYOUT_OPTIONS opts = { .m_copyRouting = true,
1711 .m_connectedRoutingOnly = false,
1712 .m_copyPlacement = true,
1713 .m_copyOtherItems = true,
1714 .m_groupItems = false,
1715 .m_includeLockedItems = true,
1716 .m_anchorFp = nullptr };
1717
1718 wxString err;
1719 int result = mtTool->RepeatLayout( TOOL_EVENT(), dbRA, destRA, opts, nullptr, &err );
1720
1721 delete dbRA.m_zone;
1722 delete destRA.m_zone;
1723
1724 BOOST_REQUIRE_MESSAGE( result >= 0, wxString::Format( "RepeatLayout failed: %s", err ) );
1725
1726 // Pointers to removed tracks are deleted, so count survivors by location instead.
1727 int siblingTracks = 0;
1728 int looseTracks = 0;
1729
1730 for( PCB_TRACK* track : m_board->Tracks() )
1731 {
1732 int y = track->GetStart().y;
1733
1734 if( y > pcbIUScale.mmToIU( 46 ) && y < pcbIUScale.mmToIU( 48 ) )
1735 siblingTracks++;
1736 else if( y > pcbIUScale.mmToIU( 52 ) && y < pcbIUScale.mmToIU( 54 ) )
1737 looseTracks++;
1738 }
1739
1740 BOOST_CHECK_MESSAGE( siblingTracks == 1,
1741 wxString::Format( "Sibling group routing was deleted (issue 24767): found %d, expected 1",
1742 siblingTracks ) );
1743 BOOST_CHECK_MESSAGE( looseTracks == 0,
1744 wxString::Format( "Loose routing in the target area should be replaced: found %d, expected 0",
1745 looseTracks ) );
1746}
1747
1748
1755BOOST_FIXTURE_TEST_CASE( RepeatLayoutRefusesDuplicatePlacementAreas, MULTICHANNEL_TEST_FIXTURE )
1756{
1757 KI_TEST::LoadBoard( m_settingsManager, "issue22318/issue22318", m_board );
1758
1759 TOOL_MANAGER toolMgr;
1760 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1761
1762 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1763
1765 toolMgr.RegisterTool( mtTool );
1766
1767 mtTool->FindExistingRuleAreas();
1768
1769 auto ruleData = mtTool->GetData();
1770
1771 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1772 static_cast<int>( ruleData->m_areas.size() ) ) );
1773
1774 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
1775
1776 // The shared membership, not the exact count, is what makes the areas invalid targets.
1777 const size_t sharedCount = ruleData->m_areas.front().m_components.size();
1778
1779 BOOST_REQUIRE( sharedCount > 0 );
1780
1781 for( const RULE_AREA& ra : ruleData->m_areas )
1782 BOOST_CHECK_EQUAL( ra.m_components.size(), sharedCount );
1783
1784 RULE_AREA* refArea = &ruleData->m_areas.front();
1785
1786 BOOST_REQUIRE( mtTool->CheckRACompatibility( refArea->m_zone ) >= 0 );
1787
1788 BOOST_REQUIRE_EQUAL( ruleData->m_compatMap.size(), ruleData->m_areas.size() - 1 );
1789
1790 for( const auto& [targetArea, compatData] : ruleData->m_compatMap )
1791 {
1792 BOOST_CHECK_MESSAGE( !compatData.m_isOk,
1793 "Duplicate placement area was wrongly reported as a valid copy target "
1794 "(issue 22318)" );
1795 BOOST_CHECK( !compatData.m_mismatchReasons.empty() );
1796 }
1797
1798 // The single-target overload must also refuse rather than corrupt the board.
1799 RULE_AREA* targetArea = &ruleData->m_areas[1];
1800
1802 wxString err;
1803
1804 int result = mtTool->RepeatLayout( TOOL_EVENT(), *refArea, *targetArea, opts, nullptr, &err );
1805
1807 "RepeatLayout copied a Rule Area onto an identical-component area "
1808 "(issue 22318)" );
1809 BOOST_CHECK( !err.IsEmpty() );
1810}
1811
1812
1824BOOST_FIXTURE_TEST_CASE( RepeatLayoutDoesNotDuplicateUnrelatedGroups, MULTICHANNEL_TEST_FIXTURE )
1825{
1826 KI_TEST::LoadBoard( m_settingsManager, "issue22316/issue22316", m_board );
1827
1828 TOOL_MANAGER toolMgr;
1829 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1830
1831 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1832
1834 toolMgr.RegisterTool( mtTool );
1835
1836 mtTool->FindExistingRuleAreas();
1837
1838 auto ruleData = mtTool->GetData();
1839
1840 BOOST_TEST_MESSAGE( wxString::Format( "Found %d rule areas",
1841 static_cast<int>( ruleData->m_areas.size() ) ) );
1842
1843 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 4 );
1844
1845 RULE_AREA* refArea = nullptr;
1846
1847 for( RULE_AREA& ra : ruleData->m_areas )
1848 {
1849 if( ra.m_ruleName == wxT( "test 1" ) )
1850 refArea = &ra;
1851 }
1852
1853 BOOST_REQUIRE( refArea != nullptr );
1854
1855 std::set<KIID> groupUuidsBefore;
1856 std::set<wxString> userGroupNamesBefore;
1857
1858 for( PCB_GROUP* group : m_board->Groups() )
1859 {
1860 groupUuidsBefore.insert( group->m_Uuid );
1861
1862 if( !group->GetName().IsEmpty() )
1863 userGroupNamesBefore.insert( group->GetName() );
1864 }
1865
1866 mtTool->CheckRACompatibility( refArea->m_zone );
1867
1868 for( auto& [targetArea, compatData] : ruleData->m_compatMap )
1869 compatData.m_doCopy = true;
1870
1871 ruleData->m_options.m_copyPlacement = true;
1872 ruleData->m_options.m_copyRouting = true;
1873 ruleData->m_options.m_copyOtherItems = true;
1874 ruleData->m_options.m_groupItems = true;
1875 ruleData->m_options.m_includeLockedItems = true;
1876
1877 int result = mtTool->RepeatLayout( TOOL_EVENT(), refArea->m_zone );
1878
1879 BOOST_REQUIRE( result >= 0 );
1880
1881 int clonedUserGroups = 0;
1882
1883 for( PCB_GROUP* group : m_board->Groups() )
1884 {
1885 bool isNew = !groupUuidsBefore.contains( group->m_Uuid );
1886
1887 if( isNew && userGroupNamesBefore.contains( group->GetName() ) )
1888 clonedUserGroups++;
1889 }
1890
1891 BOOST_CHECK_MESSAGE( clonedUserGroups == 0,
1892 wxString::Format( "Repeat layout cloned %d unrelated user groups "
1893 "(issue 22316)",
1894 clonedUserGroups ) );
1895}
1896
1897
1905BOOST_FIXTURE_TEST_CASE( TopoMatchExternalLoopReportsConnectivity, MULTICHANNEL_TEST_FIXTURE )
1906{
1908
1909 KI_TEST::LoadBoard( m_settingsManager, "issue24192/issue24192", m_board );
1910
1911 TOOL_MANAGER toolMgr;
1912 MOCK_TOOLS_HOLDER* toolsHolder = new MOCK_TOOLS_HOLDER;
1913 toolMgr.SetEnvironment( m_board.get(), nullptr, nullptr, nullptr, toolsHolder );
1914
1916 toolMgr.RegisterTool( mtTool );
1917
1918 mtTool->FindExistingRuleAreas();
1919
1920 auto ruleData = mtTool->GetData();
1921
1922 BOOST_REQUIRE_EQUAL( ruleData->m_areas.size(), 2 );
1923
1924 RULE_AREA* refArea = nullptr;
1925 RULE_AREA* targetArea = nullptr;
1926
1927 for( RULE_AREA& ra : ruleData->m_areas )
1928 {
1929 if( ra.m_ruleName.Contains( wxT( "Untitled Sheet/" ) ) )
1930 refArea = &ra;
1931 else if( ra.m_ruleName.Contains( wxT( "Untitled Sheet1/" ) ) )
1932 targetArea = &ra;
1933 }
1934
1935 BOOST_REQUIRE( refArea != nullptr );
1936 BOOST_REQUIRE( targetArea != nullptr );
1937
1938 // The component counts are identical; only the internal connectivity differs.
1939 BOOST_CHECK_EQUAL( refArea->m_components.size(), targetArea->m_components.size() );
1940
1941 auto cgRef = CONNECTION_GRAPH::BuildFromFootprintSet( refArea->m_components,
1942 targetArea->m_components );
1943 auto cgTarget = CONNECTION_GRAPH::BuildFromFootprintSet( targetArea->m_components,
1944 refArea->m_components );
1945
1947 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> details;
1948 bool status = cgRef->FindIsomorphism( cgTarget.get(), result, details );
1949
1950 // The topology genuinely differs, so matching must fail and produce a reason.
1951 BOOST_CHECK( !status );
1952 BOOST_REQUIRE( !details.empty() );
1953
1954 // Collect the reference designators that belong to each channel so we can verify the
1955 // reason's reference/candidate orientation matches the ref/target areas.
1956 std::set<wxString> refRefDes;
1957 std::set<wxString> targetRefDes;
1958
1959 for( FOOTPRINT* fp : refArea->m_components )
1960 refRefDes.insert( fp->GetReference() );
1961
1962 for( FOOTPRINT* fp : targetArea->m_components )
1963 targetRefDes.insert( fp->GetReference() );
1964
1965 bool sawConnectivityReason = false;
1966
1967 for( const auto& reason : details )
1968 {
1969 BOOST_TEST_MESSAGE( wxString::Format( "reason: %s <-> %s: %s", reason.m_reference,
1970 reason.m_candidate, reason.m_reason ) );
1971
1972 // The generic fallback message must no longer be the only thing reported; instead the
1973 // connectivity difference detected by the per-pad isomorphism check must surface.
1974 BOOST_CHECK( !reason.m_reason.Contains( wxT( "No compatible component found" ) ) );
1975
1976 if( reason.m_reason.Contains( wxT( "connects to" ) )
1977 || reason.m_reason.Contains( wxT( "connectivity" ) ) )
1978 {
1979 sawConnectivityReason = true;
1980
1981 // The reference designator in the reason must come from the reference channel and
1982 // the candidate from the target channel, not the other way around (codex-review).
1983 BOOST_CHECK_MESSAGE( refRefDes.count( reason.m_reference ) > 0,
1984 wxString::Format( "Reason reference '%s' should be a reference-area "
1985 "component", reason.m_reference ) );
1986 BOOST_CHECK_MESSAGE( targetRefDes.count( reason.m_candidate ) > 0,
1987 wxString::Format( "Reason candidate '%s' should be a target-area "
1988 "component", reason.m_candidate ) );
1989 }
1990 }
1991
1992 BOOST_CHECK_MESSAGE( sawConnectivityReason,
1993 "Topology mismatch message should explain the connectivity difference "
1994 "rather than report a generic missing-component error (issue 24192)" );
1995}
1996
1997
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:372
const ZONES & Zones() const
Definition board.h:424
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
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 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
DRAWINGS & GraphicalItems()
Definition footprint.h:378
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()
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:1350
wxString GetPlacementAreaSource() const
Definition zone.h:816
SHAPE_POLY_SET * Outline()
Definition zone.h:418
void SetIsRuleArea(bool aEnable)
Definition zone.h:812
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:624
@ 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()
RULE_AREA * findRuleAreaByPartialName(MULTICHANNEL_TOOL *aTool, const wxString &aName)
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")
@ 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