KiCad PCB EDA Suite
Loading...
Searching...
No Matches
multichannel_tool.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
20
21#include <board_commit.h>
22#include <tools/pcb_actions.h>
23#include <tools/pcb_selection.h>
24
27
28#include "multichannel_tool.h"
29
30#include <pcbexpr_evaluator.h>
31
32#include <zone.h>
33#include <board.h>
34#include <netinfo.h>
38#include <pcb_group.h>
39#include <pcb_generator.h>
40#include <footprint.h>
41#include <pad.h>
42#include <pcb_text.h>
46#include <algorithm>
47#include <pcb_track.h>
48#include <tool/tool_manager.h>
50#include <chrono>
51#include <core/profile.h>
52#include <thread_pool.h>
54#include <string_utils.h>
55#include <wx/log.h>
56#include <wx/richmsgdlg.h>
57#include <pgm_base.h>
58
59
60#define MULTICHANNEL_EXTRA_DEBUG
61
62static const wxString traceMultichannelTool = wxT( "MULTICHANNEL_TOOL" );
63
64
65static wxString FormatComponentList( const std::set<FOOTPRINT*>& aComponents )
66{
67 std::vector<wxString> refs;
68
69 for( FOOTPRINT* fp : aComponents )
70 {
71 if( !fp )
72 continue;
73
74 refs.push_back( fp->GetReferenceAsString() );
75 }
76
77 std::sort( refs.begin(), refs.end(),
78 []( const wxString& aLhs, const wxString& aRhs )
79 {
80 return aLhs.CmpNoCase( aRhs ) < 0;
81 } );
82
83 if( refs.empty() )
84 return _( "(none)" );
85
86 wxString result;
87 wxString line;
88 size_t componentsOnLine = 0;
89
90 for( const wxString& ref : refs )
91 {
92 if( componentsOnLine == 10 )
93 {
94 if( !result.IsEmpty() )
95 result += wxT( "\n" );
96
97 result += line;
98 line.clear();
99 componentsOnLine = 0;
100 }
101
102 AccumulateDescription( line, ref );
103 componentsOnLine++;
104 }
105
106 if( !line.IsEmpty() )
107 {
108 if( !result.IsEmpty() )
109 result += wxT( "\n" );
110
111 result += line;
112 }
113
114 return result;
115}
116
117
118static void ShowTopologyMismatchReasons( wxWindow* aParent, const wxString& aSummary,
119 const std::vector<wxString>& aReasons )
120{
121 if( !aParent || aReasons.empty() )
122 return;
123
124 wxString reasonText;
125
126 for( size_t idx = 0; idx < aReasons.size(); ++idx )
127 {
128 if( idx > 0 )
129 reasonText += wxT( "\n" );
130
131 reasonText += aReasons[idx];
132 }
133
134 wxRichMessageDialog dlg( aParent, aSummary, _( "Topology mismatch" ), wxICON_ERROR | wxOK );
135 dlg.ShowDetailedText( reasonText );
136 dlg.ShowModal();
137}
138
139
141{
142}
143
144
149
150void MULTICHANNEL_TOOL::ShowMismatchDetails( wxWindow* aParent, const wxString& aSummary,
151 const std::vector<wxString>& aReasons ) const
152{
153 wxWindow* parent = aParent ? aParent : frame();
154 ShowTopologyMismatchReasons( parent, aSummary, aReasons );
155}
156
157
163
164
166 std::set<FOOTPRINT*>& aComponents )
167{
168 if( !aRuleArea || !aRuleArea->m_zone )
169 return false;
170
171 // When we're copying the layout of a design block, we are provided an exact list of items
172 // rather than querying the board for items that are inside the area.
174 {
175 // Get all board connected items that are from the design bloc
176 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
177 {
178 if( item->Type() == PCB_FOOTPRINT_T )
179 aComponents.insert( static_cast<FOOTPRINT*>( item ) );
180 }
181
182 return (int) aComponents.size();
183 }
184
185
187 PCBEXPR_UCODE ucode;
188 PCBEXPR_CONTEXT ctx, preflightCtx;
189
190 auto reportError =
191 [&]( const wxString& aMessage, int aOffset )
192 {
193 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s"), aMessage );
194 };
195
196 ctx.SetErrorCallback( reportError );
197 preflightCtx.SetErrorCallback( reportError );
198 compiler.SetErrorCallback( reportError );
199 //compiler.SetDebugReporter( m_reporter );
200
201 wxLogTrace( traceMultichannelTool, wxT( "rule area '%s'" ), aRuleArea->m_zone->GetZoneName() );
202
203 wxString ruleText;
204
205 switch( aRuleArea->m_zone->GetPlacementAreaSourceType() )
206 {
208 ruleText = wxT( "A.memberOfSheetOrChildren('" ) + aRuleArea->m_zone->GetPlacementAreaSource() + wxT( "')" );
209 break;
211 ruleText = wxT( "A.hasComponentClass('" ) + aRuleArea->m_zone->GetPlacementAreaSource() + wxT( "')" );
212 break;
214 ruleText = wxT( "A.memberOfGroup('" ) + aRuleArea->m_zone->GetPlacementAreaSource() + wxT( "')" );
215 break;
217 // For design blocks, handled above outside the rules system
218 break;
219 }
220
221 auto ok = compiler.Compile( ruleText, &ucode, &preflightCtx );
222
223 if( !ok )
224 return false;
225
226 for( FOOTPRINT* fp : board()->Footprints() )
227 {
228 ctx.SetItems( fp, fp );
229 LIBEVAL::VALUE* val = ucode.Run( &ctx );
230
231 if( val->AsDouble() != 0.0 )
232 {
233 wxLogTrace( traceMultichannelTool, wxT( " - %s [sheet %s]" ),
234 fp->GetReference(),
235 fp->GetSheetname() );
236
237 aComponents.insert( fp );
238 }
239 }
240
241 return true;
242}
243
244
245bool MULTICHANNEL_TOOL::findOtherItemsInRuleArea( RULE_AREA* aRuleArea, std::set<BOARD_ITEM*>& aItems )
246{
247 if( !aRuleArea || !aRuleArea->m_zone )
248 return false;
249
250 // When we're copying the layout of a design block, we are provided an exact list of items
251 // rather than querying the board for items that are inside the area.
253 {
254 // Get all board items that aren't footprints. Connected items are usually handled by the
255 // routing path, except zones which are copied via "other items".
256 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
257 {
258 if( item->Type() == PCB_FOOTPRINT_T )
259 continue;
260
261 // Generators are copied whole by the routing pass. Nested groups are not yet
262 // preserved (TODO) so they are skipped here.
263 if( item->Type() == PCB_GROUP_T || item->Type() == PCB_GENERATOR_T )
264 continue;
265
266 // Cells are copied with their owning PCB_TABLE, not as standalone items.
267 if( item->Type() == PCB_TABLECELL_T )
268 continue;
269
270 if( BOARD_ITEM* boardItem = dynamic_cast<BOARD_ITEM*>( item ) )
271 {
272 if( !boardItem->IsConnected() || boardItem->Type() == PCB_ZONE_T )
273 aItems.insert( boardItem );
274 }
275 }
276
277 return aItems.size() > 0;
278 }
279
280 // The design-block apply target uses a scratch zone not on the board, so enclosedByArea()
281 // below finds nothing. Resolve the group's items directly from the group.
283 && ( !aRuleArea->m_components.empty() || aRuleArea->m_group ) )
284 {
285 bool zoneOnBoard = false;
286
287 for( ZONE* zone : board()->Zones() )
288 {
289 if( zone == aRuleArea->m_zone )
290 {
291 zoneOnBoard = true;
292 break;
293 }
294 }
295
296 if( !zoneOnBoard )
297 {
298 EDA_GROUP* group = aRuleArea->m_group;
299
300 if( !group && !aRuleArea->m_components.empty() )
301 group = ( *aRuleArea->m_components.begin() )->GetParentGroup();
302
303 if( group )
304 {
305 for( EDA_ITEM* member : group->GetItems() )
306 {
307 if( member->Type() == PCB_FOOTPRINT_T || member->Type() == PCB_GROUP_T
308 || member->Type() == PCB_GENERATOR_T || member->Type() == PCB_TABLECELL_T )
309 continue;
310
311 if( BOARD_ITEM* boardItem = dynamic_cast<BOARD_ITEM*>( member ) )
312 {
313 if( !boardItem->IsConnected() || boardItem->Type() == PCB_ZONE_T )
314 aItems.insert( boardItem );
315 }
316 }
317 }
318
319 return aItems.size() > 0;
320 }
321 }
322
324 PCBEXPR_UCODE ucode;
325 PCBEXPR_CONTEXT ctx, preflightCtx;
326
327 auto reportError =
328 [&]( const wxString& aMessage, int aOffset )
329 {
330 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s"), aMessage );
331 };
332
333 ctx.SetErrorCallback( reportError );
334 preflightCtx.SetErrorCallback( reportError );
335 compiler.SetErrorCallback( reportError );
336
337 // Use the zone's UUID to identify it uniquely. Using the zone name could match other zones
338 // with the same name (e.g., a copper fill zone with the same name as a rule area).
339 wxString ruleText = wxString::Format( wxT( "A.enclosedByArea('%s')" ),
340 aRuleArea->m_zone->m_Uuid.AsString() );
341
342 if( !compiler.Compile( ruleText, &ucode, &preflightCtx ) )
343 return false;
344
345 auto testAndAdd =
346 [&]( BOARD_ITEM* aItem )
347 {
348 ctx.SetItems( aItem, aItem );
349 auto val = ucode.Run( &ctx );
350
351 if( val->AsDouble() != 0.0 )
352 aItems.insert( aItem );
353 };
354
355 for( ZONE* zone : board()->Zones() )
356 {
357 if( zone == aRuleArea->m_zone )
358 continue;
359
360 testAndAdd( zone );
361 }
362
363 for( BOARD_ITEM* drawing : board()->Drawings() )
364 {
365 if( !drawing->IsConnected() )
366 testAndAdd( drawing );
367 }
368
369 return true;
370}
371
372
373std::set<FOOTPRINT*> MULTICHANNEL_TOOL::queryComponentsInSheet( wxString aSheetName ) const
374{
375 std::set<FOOTPRINT*> rv;
376
377 if( aSheetName.EndsWith( wxT( "/" ) ) )
378 aSheetName.RemoveLast();
379
380 wxString childPrefix = aSheetName + wxT( "/" );
381
382 for( FOOTPRINT* fp : board()->Footprints() )
383 {
384 auto sn = fp->GetSheetname();
385
386 if( sn.EndsWith( wxT( "/" ) ) )
387 sn.RemoveLast();
388
389 if( sn == aSheetName || sn.StartsWith( childPrefix ) )
390 rv.insert( fp );
391 }
392
393 return rv;
394}
395
396
397std::set<FOOTPRINT*>
398MULTICHANNEL_TOOL::queryComponentsInComponentClass( const wxString& aComponentClassName ) const
399{
400 std::set<FOOTPRINT*> rv;
401
402 for( FOOTPRINT* fp : board()->Footprints() )
403 {
404 if( fp->GetComponentClass()->ContainsClassName( aComponentClassName ) )
405 rv.insert( fp );
406 }
407
408 return rv;
409}
410
411
412std::set<FOOTPRINT*> MULTICHANNEL_TOOL::queryComponentsInGroup( const wxString& aGroupName ) const
413{
414 std::set<FOOTPRINT*> rv;
415
416 for( PCB_GROUP* group : board()->Groups() )
417 {
418 if( group->GetName() == aGroupName )
419 {
420 for( EDA_ITEM* item : group->GetItems() )
421 {
422 if( item->Type() == PCB_FOOTPRINT_T )
423 rv.insert( static_cast<FOOTPRINT*>( item ) );
424 }
425 }
426 }
427
428 return rv;
429}
430
431
432std::set<BOARD_ITEM*> MULTICHANNEL_TOOL::queryBoardItemsInGroup( const wxString& aGroupName ) const
433{
434 std::set<BOARD_ITEM*> rv;
435
436 for( PCB_GROUP* group : board()->Groups() )
437 {
438 if( group->GetName() != aGroupName )
439 continue;
440
441 for( EDA_ITEM* item : group->GetItems() )
442 {
443 if( item->IsBOARD_ITEM() )
444 rv.insert( static_cast<BOARD_ITEM*>( item ) );
445 }
446 }
447
448 return rv;
449}
450
451
452const SHAPE_LINE_CHAIN MULTICHANNEL_TOOL::buildRAOutline( std::set<FOOTPRINT*>& aFootprints, int aMargin )
453{
454 std::vector<VECTOR2I> bbCorners;
455 bbCorners.reserve( aFootprints.size() * 4 );
456
457 for( FOOTPRINT* fp : aFootprints )
458 {
459 const BOX2I bb = fp->GetBoundingBox( false ).GetInflated( aMargin );
460 KIGEOM::CollectBoxCorners( bb, bbCorners );
461 }
462
463 std::vector<VECTOR2I> hullVertices;
464 BuildConvexHull( hullVertices, bbCorners );
465
466 SHAPE_LINE_CHAIN hull( hullVertices );
467
468 // Make the newly computed convex hull use only 90 degree segments
469 return KIGEOM::RectifyPolygon( hull );
470}
471
472const SHAPE_LINE_CHAIN MULTICHANNEL_TOOL::buildRAOutline( const std::set<BOARD_ITEM*>& aItems, int aMargin )
473{
474 std::vector<VECTOR2I> bbCorners;
475 bbCorners.reserve( aItems.size() * 4 );
476
477 for( BOARD_ITEM* item : aItems )
478 {
479 BOX2I bb = item->GetBoundingBox();
480
481 if( item->Type() == PCB_FOOTPRINT_T )
482 bb = static_cast<FOOTPRINT*>( item )->GetBoundingBox( false );
483
484 KIGEOM::CollectBoxCorners( bb.GetInflated( aMargin ), bbCorners );
485 }
486
487 std::vector<VECTOR2I> hullVertices;
488 BuildConvexHull( hullVertices, bbCorners );
489
490 SHAPE_LINE_CHAIN hull( hullVertices );
491
492 // Make the newly computed convex hull use only 90 degree segments
493 return KIGEOM::RectifyPolygon( hull );
494}
495
496
498{
499 using PathAndName = std::pair<wxString, wxString>;
500 std::set<PathAndName> uniqueSheets;
501 std::set<wxString> uniqueComponentClasses;
502 std::set<wxString> uniqueGroups;
503
504 m_areas.m_areas.clear();
505
506 for( const FOOTPRINT* fp : board()->Footprints() )
507 {
508 uniqueSheets.insert( PathAndName( fp->GetSheetname(), fp->GetSheetfile() ) );
509
510 const COMPONENT_CLASS* compClass = fp->GetComponentClass();
511
512 for( const COMPONENT_CLASS* singleClass : compClass->GetConstituentClasses() )
513 uniqueComponentClasses.insert( singleClass->GetName() );
514
515 if( fp->GetParentGroup() && !fp->GetParentGroup()->GetName().IsEmpty() )
516 uniqueGroups.insert( fp->GetParentGroup()->GetName() );
517 }
518
519 for( const PathAndName& sheet : uniqueSheets )
520 {
521 RULE_AREA ent;
522
524 ent.m_generateEnabled = false;
525 ent.m_sheetPath = sheet.first;
526 ent.m_sheetName = sheet.second;
528 m_areas.m_areas.push_back( ent );
529
530 wxLogTrace( traceMultichannelTool, wxT("found sheet '%s' @ '%s' s %d\n"),
531 ent.m_sheetName,
532 ent.m_sheetPath,
533 (int) m_areas.m_areas.size() );
534 }
535
536 for( const wxString& compClass : uniqueComponentClasses )
537 {
538 RULE_AREA ent;
539
541 ent.m_generateEnabled = false;
542 ent.m_componentClass = compClass;
544 m_areas.m_areas.push_back( ent );
545
546 wxLogTrace( traceMultichannelTool, wxT( "found component class '%s' s %d\n" ),
548 static_cast<int>( m_areas.m_areas.size() ) );
549 }
550
551 for( const wxString& groupName : uniqueGroups )
552 {
553 RULE_AREA ent;
554
556 ent.m_generateEnabled = false;
557 ent.m_groupName = groupName;
559 m_areas.m_areas.push_back( ent );
560
561 wxLogTrace( traceMultichannelTool, wxT( "found group '%s' s %d\n" ),
563 static_cast<int>( m_areas.m_areas.size() ) );
564 }
565}
566
567
569{
570 m_areas.m_areas.clear();
571
572 for( ZONE* zone : board()->Zones() )
573 {
574 if( !zone->GetIsRuleArea() )
575 continue;
576
577 if( !zone->GetPlacementAreaEnabled() )
578 continue;
579
580 RULE_AREA area;
581
582 area.m_existsAlready = true;
583 area.m_zone = zone;
584 area.m_ruleName = zone->GetZoneName();
585 area.m_center = zone->Outline()->COutline( 0 ).Centre();
586
588
589 m_areas.m_areas.push_back( area );
590
591 wxLogTrace( traceMultichannelTool, wxT( "RA '%s', %d footprints\n" ), area.m_ruleName,
592 (int) area.m_components.size() );
593 }
594
595 wxLogTrace( traceMultichannelTool, wxT( "Total RAs found: %d\n" ), (int) m_areas.m_areas.size() );
596}
597
598
600{
601 for( RULE_AREA& ra : m_areas.m_areas )
602 {
603 if( ra.m_ruleName == aName )
604 return &ra;
605 }
606
607 return nullptr;
608}
609
610
612{
614}
615
616
618{
619 std::vector<ZONE*> refRAs;
620
621 auto isSelectedItemAnRA =
622 []( EDA_ITEM* aItem ) -> ZONE*
623 {
624 if( !aItem || aItem->Type() != PCB_ZONE_T )
625 return nullptr;
626
627 ZONE* zone = static_cast<ZONE*>( aItem );
628
629 if( !zone->GetIsRuleArea() )
630 return nullptr;
631
632 if( !zone->GetPlacementAreaEnabled() )
633 return nullptr;
634
635 return zone;
636 };
637
638 for( EDA_ITEM* item : selection() )
639 {
640 if( ZONE* zone = isSelectedItemAnRA( item ) )
641 {
642 refRAs.push_back( zone );
643 }
644 else if( item->Type() == PCB_GROUP_T )
645 {
646 PCB_GROUP *group = static_cast<PCB_GROUP*>( item );
647
648 for( EDA_ITEM* grpItem : group->GetItems() )
649 {
650 if( ZONE* grpZone = isSelectedItemAnRA( grpItem ) )
651 refRAs.push_back( grpZone );
652 }
653 }
654 }
655
656 if( refRAs.size() != 1 )
657 {
660 this,
661 _( "Select a reference Rule Area to copy from..." ),
662 [&]( EDA_ITEM* aItem )
663 {
664 return isSelectedItemAnRA( aItem ) != nullptr;
665 }
666 } );
667
668 return 0;
669 }
670
672
673 int status = CheckRACompatibility( refRAs.front() );
674
675 if( status < 0 )
676 return status;
677
678 if( m_areas.m_areas.size() <= 1 )
679 {
680 frame()->ShowInfoBarError( _( "No Rule Areas to repeat layout to have been found." ), true );
681 return 0;
682 }
683
685 int ret = dialog.ShowModal();
686
687 if( ret != wxID_OK )
688 return 0;
689
690 return RepeatLayout( aEvent, refRAs.front() );
691}
692
693
695{
696 m_areas.m_refRA = nullptr;
697
698 for( RULE_AREA& ra : m_areas.m_areas )
699 {
700 if( ra.m_zone == aRefZone )
701 {
702 m_areas.m_refRA = &ra;
703 break;
704 }
705 }
706
707 if( !m_areas.m_refRA )
708 return -1;
709
710 m_areas.m_compatMap.clear();
711
712 std::vector<RULE_AREA*> targets;
713
714 for( RULE_AREA& ra : m_areas.m_areas )
715 {
716 if( ra.m_zone == m_areas.m_refRA->m_zone )
717 continue;
718
719 targets.push_back( &ra );
720 m_areas.m_compatMap[&ra] = RULE_AREA_COMPAT_DATA();
721 }
722
723 if( targets.empty() )
724 return 0;
725
726 int total = static_cast<int>( targets.size() );
727 std::atomic<int> completed( 0 );
728 std::atomic<bool> cancelled( false );
729 std::atomic<int> matchedComponents( 0 );
730 std::atomic<int> totalComponents( 0 );
731 RULE_AREA* refRA = m_areas.m_refRA;
732
734 isoParams.m_cancelled = &cancelled;
735 isoParams.m_matchedComponents = &matchedComponents;
736 isoParams.m_totalComponents = &totalComponents;
737
738 // Process RA resolutions sequentially on a single background thread.
739 // Each resolveConnectionTopology call internally parallelizes its MRV scan
740 // across the thread pool, creating many short-lived tasks that fully utilize
741 // all available cores. Running the outer loop sequentially avoids thread
742 // pool starvation from nested parallelism.
744
745 auto future = tp.submit_task(
746 [this, refRA, &targets, &completed, &cancelled, &matchedComponents, &isoParams]()
747 {
748 for( RULE_AREA* target : targets )
749 {
750 if( cancelled.load( std::memory_order_relaxed ) )
751 break;
752
753 matchedComponents.store( 0, std::memory_order_relaxed );
754
755 RULE_AREA_COMPAT_DATA& compatData = m_areas.m_compatMap[target];
756 resolveConnectionTopology( refRA, target, compatData, isoParams );
757 completed.fetch_add( 1, std::memory_order_relaxed );
758 }
759 } );
760
761 if( Pgm().IsGUI() )
762 {
763 std::unique_ptr<WX_PROGRESS_REPORTER> reporter;
764 auto startTime = std::chrono::steady_clock::now();
765 double highWaterMark = 0.0;
766
767 while( future.wait_for( std::chrono::milliseconds( 100 ) ) != std::future_status::ready )
768 {
769 if( !reporter )
770 {
771 auto elapsed = std::chrono::steady_clock::now() - startTime;
772
773 if( elapsed > std::chrono::seconds( 1 ) )
774 {
775 reporter = std::make_unique<WX_PROGRESS_REPORTER>(
776 frame(), _( "Checking Rule Area compatibility..." ), 1, PR_CAN_ABORT );
777 }
778 else
779 {
780 // Flush background-thread log messages so timing traces appear promptly
781 wxLog::FlushActive();
782 }
783 }
784
785 if( reporter )
786 {
787 int done = completed.load( std::memory_order_relaxed );
788 int matched = matchedComponents.load( std::memory_order_relaxed );
789 int compTotal = totalComponents.load( std::memory_order_relaxed );
790
791 double fraction = ( compTotal > 0 )
792 ? static_cast<double>( matched ) / compTotal
793 : 0.0;
794 double progress = static_cast<double>( done + fraction ) / total;
795
796 if( progress > highWaterMark )
797 highWaterMark = progress;
798
799 reporter->SetCurrentProgress( highWaterMark );
800 reporter->Report( wxString::Format(
801 _( "Resolving topology %d of %d (%d/%d components)" ),
802 done + 1, total, matched, compTotal ) );
803
804 if( !reporter->KeepRefreshing() )
805 cancelled.store( true, std::memory_order_relaxed );
806 }
807 }
808 }
809 else
810 {
811 future.wait();
812 }
813
814 if( cancelled.load( std::memory_order_relaxed ) )
815 {
816 m_areas.m_compatMap.clear();
817 return -1;
818 }
819
820 return 0;
821}
822
823
824int MULTICHANNEL_TOOL::RepeatLayout( const TOOL_EVENT& aEvent, RULE_AREA& aRefArea, RULE_AREA& aTargetArea,
825 REPEAT_LAYOUT_OPTIONS& aOptions, BOARD_COMMIT* aExternalCommit,
826 wxString* aErrorOut )
827{
828 wxCHECK_MSG( aRefArea.m_zone, -1, wxT( "Reference Rule Area has no zone." ) );
829 wxCHECK_MSG( aTargetArea.m_zone, -1, wxT( "Target Rule Area has no zone." ) );
830
831 const bool silent = aErrorOut != nullptr;
832
833 auto reportError = [&]( const wxString& aMsg )
834 {
835 if( aErrorOut )
836 *aErrorOut = aMsg;
837 else if( Pgm().IsGUI() )
838 frame()->ShowInfoBarError( aMsg, true );
839 };
840
842
843 if( !resolveConnectionTopology( &aRefArea, &aTargetArea, compat ) )
844 {
845 if( silent )
846 {
847 *aErrorOut = compat.m_errorMsg;
848 }
849 else if( Pgm().IsGUI() )
850 {
851 wxString summary = wxString::Format( _( "Rule Area topologies do not match: %s" ), compat.m_errorMsg );
853 }
854
855 return -1;
856 }
857
858 std::optional<BOARD_COMMIT> localCommit;
859
860 if( !aExternalCommit )
861 localCommit.emplace( GetManager(), true, false );
862
863 BOARD_COMMIT& commit = aExternalCommit ? *aExternalCommit : *localCommit;
864
865 // If no anchor is provided, pick the first matched pair to avoid center-alignment shifting
866 // the whole group. This keeps Apply Design Block Layout from moving the group to wherever
867 // the source design block happened to be placed.
868 if( aTargetArea.m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT && !aOptions.m_anchorFp )
869 {
870 if( !compat.m_matchingComponents.empty() )
871 aOptions.m_anchorFp = compat.m_matchingComponents.begin()->first;
872 }
873
874 if( !copyRuleAreaContents( &aRefArea, &aTargetArea, &commit, aOptions, compat ) )
875 {
876 auto errMsg = wxString::Format( _( "Copy Rule Area contents failed between rule areas '%s' and '%s'." ),
877 aRefArea.m_zone->GetZoneName(), aTargetArea.m_zone->GetZoneName() );
878
879 if( !aExternalCommit )
880 commit.Revert();
881
882 reportError( errMsg );
883
884 return -1;
885 }
886
888 {
889 EDA_GROUP* group = aTargetArea.m_group;
890
891 if( !group && !aTargetArea.m_components.empty() )
892 group = ( *aTargetArea.m_components.begin() )->GetParentGroup();
893
894 if( !group )
895 {
896 if( !aExternalCommit )
897 commit.Revert();
898
899 reportError( _( "Target group does not have a group." ) );
900
901 return -1;
902 }
903
904 commit.Modify( group->AsEdaItem(), nullptr, RECURSE_MODE::NO_RECURSE );
905
906 for( BOARD_ITEM* item : compat.m_groupableItems )
907 {
908 commit.Modify( item );
909 group->AddItem( item );
910 }
911 }
912
913 if( !aExternalCommit )
914 commit.Push( _( "Repeat layout" ) );
915
916 return 0;
917}
918
919
920int MULTICHANNEL_TOOL::RepeatLayout( const TOOL_EVENT& aEvent, ZONE* aRefZone )
921{
922 int totalCopied = 0;
923
924 BOARD_COMMIT commit( GetManager(), true, false );
925
926 for( auto& [targetArea, compatData] : m_areas.m_compatMap )
927 {
928 if( !compatData.m_doCopy )
929 {
930 wxLogTrace( traceMultichannelTool, wxT( "skipping copy to RA '%s' (disabled in dialog)\n" ),
931 targetArea->m_ruleName );
932 continue;
933 }
934
935 if( !compatData.m_isOk )
936 continue;
937
938 if( !copyRuleAreaContents( m_areas.m_refRA, targetArea, &commit, m_areas.m_options, compatData ) )
939 {
940 auto errMsg = wxString::Format( _( "Copy Rule Area contents failed between rule areas '%s' and '%s'." ),
941 m_areas.m_refRA->m_zone->GetZoneName(),
942 targetArea->m_zone->GetZoneName() );
943
944 commit.Revert();
945
946 if( Pgm().IsGUI() )
947 frame()->ShowInfoBarError( errMsg, true );
948
949 return -1;
950 }
951
952 totalCopied++;
953 wxSafeYield();
954 }
955
956 if( m_areas.m_options.m_groupItems )
957 {
958 for( const auto& [targetArea, compatData] : m_areas.m_compatMap )
959 {
960 if( compatData.m_groupableItems.size() < 2 )
961 continue;
962
963 pruneExistingGroups( commit, compatData.m_affectedItems );
964
965 PCB_GROUP* group = new PCB_GROUP( board() );
966
967 commit.Add( group );
968
969 for( BOARD_ITEM* item : compatData.m_groupableItems )
970 {
971 commit.Modify( item );
972 group->AddItem( item );
973 }
974 }
975 }
976
977 commit.Push( _( "Repeat layout" ) );
978
979 if( Pgm().IsGUI() )
980 frame()->ShowInfoBarMsg( wxString::Format( _( "Copied to %d Rule Areas." ), totalCopied ), true );
981
982 return 0;
983}
984
985
986wxString MULTICHANNEL_TOOL::stripComponentIndex( const wxString& aRef ) const
987{
988 wxString rv;
989
990 // fixme: i'm pretty sure this can be written in a simpler way, but I really suck at figuring
991 // out which wx's built in functions would do it for me. And I hate regexps :-)
992 for( auto k : aRef )
993 {
994 if( !k.IsAscii() )
995 break;
996
997 char c;
998 k.GetAsChar( &c );
999
1000 if( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c == '_' ) )
1001 rv.Append( k );
1002 else
1003 break;
1004 }
1005
1006 return rv;
1007}
1008
1009
1010int MULTICHANNEL_TOOL::findRoutingInRuleArea( RULE_AREA* aRuleArea, std::set<BOARD_CONNECTED_ITEM*>& aOutput,
1011 std::shared_ptr<CONNECTIVITY_DATA> aConnectivity,
1012 const SHAPE_POLY_SET& aRAPoly, const REPEAT_LAYOUT_OPTIONS& aOpts ) const
1013{
1014 if( !aRuleArea || !aRuleArea->m_zone )
1015 return 0;
1016
1017 // The user also will consider tracks and vias that are inside the source area but
1018 // not connected to any of the source pads to count as "routing" (e.g. stitching vias)
1019
1020 int count = 0;
1021
1022 // When we're copying the layout of a design block, we are provided an exact list of items
1023 // rather than querying the board for items that are inside the area.
1025 {
1026 // Get all board connected items that are from the design block, except pads,
1027 // which shouldn't be copied
1028 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
1029 {
1030 // Include any connected items except pads.
1031 if( item->Type() == PCB_PAD_T )
1032 continue;
1033
1034 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
1035 {
1036 // Zones are handled by the "copy other items" path, we need this check here
1037 // because design blocks explicitly include them as part of the block contents,
1038 // but other RA types grab them by querying the board for items enclosed by the RA polygon
1039 if( bci->Type() == PCB_ZONE_T )
1040 continue;
1041
1042 // Tracks inside a generator (meander) are copied with the generator.
1043 if( EDA_GROUP* parent = bci->GetParentGroup() )
1044 {
1045 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
1046 continue;
1047 }
1048
1049 if( bci->IsConnected() )
1050 aOutput.insert( bci );
1051 }
1052 }
1053
1054 return (int) aOutput.size();
1055 }
1056
1057 // The design-block apply target uses a scratch zone not on the board, so enclosedByArea()
1058 // below finds nothing. Match routing against the zone outline directly.
1060 {
1061 bool zoneOnBoard = false;
1062
1063 for( ZONE* zone : board()->Zones() )
1064 {
1065 if( zone == aRuleArea->m_zone )
1066 {
1067 zoneOnBoard = true;
1068 break;
1069 }
1070 }
1071
1072 if( !zoneOnBoard )
1073 {
1074 const SHAPE_POLY_SET& areaOutline = *aRuleArea->m_zone->Outline();
1075 int maxError = board()->GetDesignSettings().m_MaxError;
1076
1077 auto enclosedByZone = [&]( BOARD_CONNECTED_ITEM* aItem )
1078 {
1079 if( aOutput.contains( aItem ) )
1080 return;
1081
1082 // Tracks inside a generator (meander) are removed with the generator.
1083 if( EDA_GROUP* parent = aItem->GetParentGroup() )
1084 {
1085 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
1086 return;
1087 }
1088
1089 if( !( aRuleArea->m_zone->GetLayerSet() & aItem->GetLayerSet() ).any() )
1090 return;
1091
1092 SHAPE_POLY_SET itemShape;
1093 aItem->TransformShapeToPolygon( itemShape, aItem->GetLayer(), 0, maxError, ERROR_OUTSIDE );
1094
1095 if( itemShape.IsEmpty() )
1096 return;
1097
1098 itemShape.BooleanSubtract( areaOutline );
1099
1100 if( itemShape.IsEmpty() )
1101 {
1102 aOutput.insert( aItem );
1103 count++;
1104 }
1105 };
1106
1107 for( PCB_TRACK* track : board()->Tracks() )
1108 enclosedByZone( track );
1109
1110 for( BOARD_ITEM* drawing : board()->Drawings() )
1111 {
1112 if( drawing->IsConnected() )
1113 enclosedByZone( static_cast<BOARD_CONNECTED_ITEM*>( drawing ) );
1114 }
1115
1116 return count;
1117 }
1118 }
1119
1121 PCBEXPR_UCODE ucode;
1122 PCBEXPR_CONTEXT ctx, preflightCtx;
1123
1124 auto reportError =
1125 [&]( const wxString& aMessage, int aOffset )
1126 {
1127 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s" ), aMessage );
1128 };
1129
1130 ctx.SetErrorCallback( reportError );
1131 preflightCtx.SetErrorCallback( reportError );
1132 compiler.SetErrorCallback( reportError );
1133
1134 // Use the zone's UUID to identify it uniquely. Using the zone name could match other zones
1135 // with the same name (e.g., a copper fill zone with the same name as a rule area).
1136 wxString ruleText = wxString::Format( wxT( "A.enclosedByArea('%s')" ),
1137 aRuleArea->m_zone->m_Uuid.AsString() );
1138
1139 auto testAndAdd =
1140 [&]( BOARD_CONNECTED_ITEM* aItem )
1141 {
1142 if( aOutput.contains( aItem ) )
1143 return;
1144
1145 ctx.SetItems( aItem, aItem );
1146 LIBEVAL::VALUE* val = ucode.Run( &ctx );
1147
1148 if( val->AsDouble() != 0.0 )
1149 {
1150 aOutput.insert( aItem );
1151 count++;
1152 }
1153 };
1154
1155 if( compiler.Compile( ruleText, &ucode, &preflightCtx ) )
1156 {
1157 for( PCB_TRACK* track : board()->Tracks() )
1158 testAndAdd( track );
1159
1160 for( BOARD_ITEM* drawing : board()->Drawings() )
1161 {
1162 if( drawing->IsConnected() )
1163 testAndAdd( static_cast<BOARD_CONNECTED_ITEM*>( drawing ) );
1164 }
1165
1166 for( PCB_GENERATOR* generator : board()->Generators() )
1167 {
1168 if( generator->GetGeneratorType() != wxT( "tuning_pattern" ) )
1169 continue;
1170
1171 if( !generator->HitTest( aRAPoly.Outline( 0 ), false ) )
1172 continue;
1173
1174 for( EDA_ITEM* member : generator->GetItems() )
1175 {
1176 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( member ) )
1177 {
1178 if( !aOutput.contains( bci ) )
1179 {
1180 aOutput.insert( bci );
1181 count++;
1182 }
1183 }
1184 }
1185 }
1186 }
1187
1188 return count;
1189}
1190
1191
1193 BOARD_COMMIT* aCommit, REPEAT_LAYOUT_OPTIONS aOpts,
1194 RULE_AREA_COMPAT_DATA& aCompatData )
1195{
1196 // copy RA shapes first
1197 SHAPE_LINE_CHAIN refOutline = aRefArea->m_zone->Outline()->COutline( 0 );
1198 SHAPE_LINE_CHAIN targetOutline = aTargetArea->m_zone->Outline()->COutline( 0 );
1199
1200 FOOTPRINT* targetAnchorFp = nullptr;
1201 VECTOR2I disp = aTargetArea->m_center - aRefArea->m_center;
1202 EDA_ANGLE rot = EDA_ANGLE( 0 );
1203
1204 if( aOpts.m_anchorFp )
1205 {
1206 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
1207 {
1208 if( refFP->GetReference() == aOpts.m_anchorFp->GetReference() )
1209 targetAnchorFp = targetFP;
1210 }
1211
1212 // If the dialog-selected anchor reference doesn't exist in the target area (e.g. refs don't match),
1213 // fall back to the first matched pair to avoid center-alignment shifting the whole group.
1214 if( !targetAnchorFp && !aCompatData.m_matchingComponents.empty() )
1215 targetAnchorFp = aCompatData.m_matchingComponents.begin()->second;
1216
1217 if( targetAnchorFp )
1218 {
1219 VECTOR2I oldpos = aOpts.m_anchorFp->GetPosition();
1220 rot = EDA_ANGLE( targetAnchorFp->GetOrientationDegrees() - aOpts.m_anchorFp->GetOrientationDegrees() );
1221 aOpts.m_anchorFp->Rotate( VECTOR2( 0, 0 ), EDA_ANGLE( rot ) );
1222 oldpos = aOpts.m_anchorFp->GetPosition();
1223 VECTOR2I newpos = targetAnchorFp->GetPosition();
1224 disp = newpos - oldpos;
1225 aOpts.m_anchorFp->Rotate( VECTOR2( 0, 0 ), EDA_ANGLE( -rot ) );
1226 }
1227 }
1228
1229 SHAPE_POLY_SET refPoly;
1230 refPoly.AddOutline( refOutline );
1231 refPoly.CacheTriangulation();
1232
1233 SHAPE_POLY_SET targetPoly;
1234
1235 SHAPE_LINE_CHAIN newTargetOutline( refOutline );
1236 newTargetOutline.Rotate( rot, VECTOR2( 0, 0 ) );
1237 newTargetOutline.Move( disp );
1238 targetPoly.AddOutline( newTargetOutline );
1239 targetPoly.CacheTriangulation();
1240
1241 std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
1242
1243 // Group placement targets let RepeatLayout() reuse the existing target group, and m_groupItems
1244 // flat-groups every copy into one rule-area group. Reconstructing source groups here in either
1245 // case strands their members and leaves empty phantom clones behind (issue 22316).
1246 const bool preserveGroups = aTargetArea->m_sourceType != PLACEMENT_SOURCE_T::GROUP_PLACEMENT
1247 && !aOpts.m_groupItems;
1248
1249 // Defer reconstruction until every copy is made. Cloning a source group the moment one member
1250 // is copied would duplicate user groups that merely overlap the source area (issue 22316); a
1251 // group is rebuilt only once all of its members have been reproduced.
1252 std::vector<std::pair<BOARD_ITEM*, BOARD_ITEM*>> groupFixupPairs;
1253 std::set<BOARD_ITEM*> reproducedSourceItems;
1254
1255 auto fixupParentGroup =
1256 [&]( BOARD_ITEM* sourceItem, BOARD_ITEM* destItem )
1257 {
1258 // The copy inherits the source's parent-group pointer but is not a member of that
1259 // group; clear the dangling reference.
1260 destItem->SetParentGroup( nullptr );
1261
1262 if( !preserveGroups )
1263 return;
1264
1265 if( sourceItem->GetParentGroup() )
1266 groupFixupPairs.emplace_back( sourceItem, destItem );
1267
1268 reproducedSourceItems.insert( sourceItem );
1269 };
1270
1271 // Only stage changes for a target Rule Area zone if it actually belongs to the board.
1272 // In some workflows (e.g. ApplyDesignBlockLayout), the target area is a temporary zone
1273 // and is not added to the BOARD.
1274 bool targetZoneOnBoard = false;
1275
1276 if( aTargetArea->m_zone )
1277 {
1278 for( ZONE* z : board()->Zones() )
1279 {
1280 if( z == aTargetArea->m_zone )
1281 {
1282 targetZoneOnBoard = true;
1283 break;
1284 }
1285 }
1286 }
1287
1288 if( targetZoneOnBoard )
1289 {
1290 aCommit->Modify( aTargetArea->m_zone );
1291 aCompatData.m_affectedItems.insert( aTargetArea->m_zone );
1292 aCompatData.m_groupableItems.insert( aTargetArea->m_zone );
1293
1294 // The source rule-area zone maps to the target zone; treat it as reproduced so a group
1295 // containing it can still be rebuilt.
1296 if( preserveGroups )
1297 {
1298 if( aRefArea->m_zone->GetParentGroup() )
1299 groupFixupPairs.emplace_back( aRefArea->m_zone, aTargetArea->m_zone );
1300
1301 reproducedSourceItems.insert( aRefArea->m_zone );
1302 }
1303 }
1304
1305 if( aOpts.m_copyRouting )
1306 {
1307 std::set<BOARD_CONNECTED_ITEM*> refRouting;
1308 std::set<BOARD_CONNECTED_ITEM*> targetRouting;
1309
1310 wxLogTrace( traceMultichannelTool, wxT( "copying routing: %d fps\n" ),
1311 (int) aCompatData.m_matchingComponents.size() );
1312
1313 std::set<int> refc;
1314 std::set<int> targc;
1315
1316 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
1317 {
1318 for( PAD* pad : refFP->Pads() )
1319 refc.insert( pad->GetNetCode() );
1320
1321 for( PAD* pad : targetFP->Pads() )
1322 targc.insert( pad->GetNetCode() );
1323 }
1324
1325 findRoutingInRuleArea( aTargetArea, targetRouting, connectivity, targetPoly, aOpts );
1326 findRoutingInRuleArea( aRefArea, refRouting, connectivity, refPoly, aOpts );
1327
1328 for( BOARD_CONNECTED_ITEM* item : targetRouting )
1329 {
1330 // Never remove pads as part of routing copy.
1331 if( item->Type() == PCB_PAD_T )
1332 continue;
1333
1334 if( aRefArea->m_designBlockItems.count( item ) )
1335 continue;
1336
1337 // Design block apply (target has a group): don't remove routing owned by another
1338 // group, or applying to stacked instances wipes the previous instance's traces.
1339 if( aTargetArea->m_group && item->GetParentGroup() && item->GetParentGroup() != aTargetArea->m_group )
1340 {
1341 continue;
1342 }
1343
1344 if( item->IsLocked() && !aOpts.m_includeLockedItems )
1345 continue;
1346
1347 if( aOpts.m_connectedRoutingOnly && !targc.contains( item->GetNetCode() ) )
1348 continue;
1349
1350 // item already removed
1351 if( aCommit->GetStatus( item ) != 0 )
1352 continue;
1353
1354 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1355 {
1356 continue;
1357 }
1358
1359 aCompatData.m_affectedItems.insert( item );
1360 aCommit->Remove( item );
1361 }
1362
1363 for( BOARD_CONNECTED_ITEM* item : refRouting )
1364 {
1365 // Never copy pads as part of routing copy.
1366 if( item->Type() == PCB_PAD_T )
1367 continue;
1368
1369 if( item->IsLocked() && !aOpts.m_includeLockedItems )
1370 continue;
1371
1372 if( aOpts.m_connectedRoutingOnly && !refc.contains( item->GetNetCode() ) )
1373 continue;
1374
1375 if( !aRefArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1376 continue;
1377
1378 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1379 continue;
1380
1381 BOARD_CONNECTED_ITEM* copied = static_cast<BOARD_CONNECTED_ITEM*>( item->Duplicate( false ) );
1382
1383 fixupNet( item, copied, aCompatData.m_matchingComponents );
1384 fixupParentGroup( item, copied );
1385
1386 copied->Rotate( VECTOR2( 0, 0 ), rot );
1387 copied->Move( disp );
1388 aCompatData.m_groupableItems.insert( copied );
1389 aCommit->Add( copied );
1390 }
1391
1392 // Copy generators (meanders) whole so they are not flattened to loose tracks.
1394 {
1395 // Remove the target group's existing generators so a re-apply replaces them.
1396 EDA_GROUP* targetGroup = aTargetArea->m_group;
1397
1398 if( !targetGroup && !aTargetArea->m_components.empty() )
1399 targetGroup = ( *aTargetArea->m_components.begin() )->GetParentGroup();
1400
1401 if( targetGroup )
1402 {
1403 std::vector<PCB_GENERATOR*> targetGenerators;
1404
1405 for( EDA_ITEM* member : targetGroup->GetItems() )
1406 {
1407 if( member->Type() == PCB_GENERATOR_T )
1408 targetGenerators.push_back( static_cast<PCB_GENERATOR*>( member ) );
1409 }
1410
1411 for( PCB_GENERATOR* gen : targetGenerators )
1412 {
1413 gen->RunOnChildren(
1414 [&]( BOARD_ITEM* child )
1415 {
1416 aCommit->Remove( child );
1417 },
1419 aCommit->Remove( gen );
1420 }
1421 }
1422
1423 for( EDA_ITEM* item : aRefArea->m_designBlockItems )
1424 {
1425 if( item->Type() != PCB_GENERATOR_T )
1426 continue;
1427
1428 PCB_GENERATOR* clone = static_cast<PCB_GENERATOR*>( item )->DeepClone();
1429
1430 clone->ClearFlags();
1431 clone->Rotate( VECTOR2( 0, 0 ), rot );
1432 clone->Move( disp );
1433 aCommit->Add( clone );
1434
1435 clone->RunOnChildren(
1436 [&]( BOARD_ITEM* child )
1437 {
1438 child->ClearFlags();
1439
1440 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( child ) )
1441 fixupNet( bci, bci, aCompatData.m_matchingComponents );
1442
1443 aCommit->Add( child );
1444 },
1446
1447 aCompatData.m_groupableItems.insert( clone );
1448 }
1449 }
1450 }
1451
1452 if( aOpts.m_copyOtherItems )
1453 {
1454 std::set<BOARD_ITEM*> sourceItems;
1455 std::set<BOARD_ITEM*> targetItems;
1456
1457 findOtherItemsInRuleArea( aRefArea, sourceItems );
1458 findOtherItemsInRuleArea( aTargetArea, targetItems );
1459
1460 // Apply Design Block Layout uses synthetic copper-only rule area zones that don't
1461 // reflect the layers the user actually drew on. The source items were collected by
1462 // explicit enumeration (m_designBlockItems) and the destination is a group bounding
1463 // box, so the per-item layer filter would incorrectly reject silkscreen, fab and
1464 // user drawings. Skip the layer filter only when both halves are the synthetic
1465 // design-block-to-group flow; regular GROUP_PLACEMENT rule areas have user-authored
1466 // layer sets that must still be honored.
1467 const bool skipLayerFilter = aRefArea->m_sourceType == PLACEMENT_SOURCE_T::DESIGN_BLOCK
1468 && aTargetArea->m_sourceType
1470
1471 for( BOARD_ITEM* item : targetItems )
1472 {
1473 if( item->GetParent() && item->GetParent()->Type() == PCB_FOOTPRINT_T )
1474 continue;
1475
1476 // Don't remove the appended source items: this geometric query can pick them up, but
1477 // they're deleted when the temporary append is reverted, leaving dangling pointers.
1478 if( aRefArea->m_designBlockItems.count( item ) )
1479 continue;
1480
1481 if( item->IsLocked() && !aOpts.m_includeLockedItems )
1482 continue;
1483
1484 // item already removed
1485 if( aCommit->GetStatus( item ) != 0 )
1486 continue;
1487
1488 if( item->Type() == PCB_ZONE_T )
1489 {
1490 ZONE* zone = static_cast<ZONE*>( item );
1491
1492 // Check all zone layers are included in the target rule area.
1493 if( skipLayerFilter
1494 || aTargetArea->m_zone->GetLayerSet().ContainsAll( zone->GetLayerSet() ) )
1495 {
1496 aCompatData.m_affectedItems.insert( zone );
1497 aCommit->Remove( zone );
1498 }
1499 }
1500 else
1501 {
1502 if( skipLayerFilter
1503 || aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1504 {
1505 aCompatData.m_affectedItems.insert( item );
1506 aCommit->Remove( item );
1507 }
1508 }
1509 }
1510
1511 for( BOARD_ITEM* item : sourceItems )
1512 {
1513 if( item->GetParent() && item->GetParent()->Type() == PCB_FOOTPRINT_T )
1514 continue;
1515
1516 if( item->IsLocked() && !aOpts.m_includeLockedItems )
1517 continue;
1518
1519 BOARD_ITEM* copied = nullptr;
1520
1521 if( item->Type() == PCB_ZONE_T )
1522 {
1523 ZONE* zone = static_cast<ZONE*>( item );
1524
1525 if( !skipLayerFilter )
1526 {
1527 LSET allowedLayers =
1528 aRefArea->m_zone->GetLayerSet() & aTargetArea->m_zone->GetLayerSet();
1529
1530 // Check all zone layers are included in both source and target rule areas.
1531 if( !allowedLayers.ContainsAll( zone->GetLayerSet() ) )
1532 continue;
1533 }
1534
1535 ZONE* targetZone = static_cast<ZONE*>( item->Duplicate( false ) );
1536 fixupNet( zone, targetZone, aCompatData.m_matchingComponents );
1537
1538 copied = targetZone;
1539 }
1540 else
1541 {
1542 if( !skipLayerFilter )
1543 {
1544 if( !aRefArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1545 continue;
1546
1547 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
1548 continue;
1549 }
1550
1551 copied = static_cast<BOARD_ITEM*>( item->Clone() );
1552 }
1553
1554 if( copied )
1555 {
1556 fixupParentGroup( item, copied );
1557
1558 copied->ClearFlags();
1559 copied->Rotate( VECTOR2( 0, 0 ), rot );
1560 copied->Move( disp );
1561 aCompatData.m_groupableItems.insert( copied );
1562 aCommit->Add( copied );
1563 }
1564 }
1565 }
1566
1567 if( aOpts.m_copyPlacement )
1568 {
1569 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
1570 {
1571 if( !aRefArea->m_zone->GetLayerSet().Contains( refFP->GetLayer() ) )
1572 {
1573 wxLogTrace( traceMultichannelTool, wxT( "discard ref:%s (ref layer)\n" ),
1574 refFP->GetReference() );
1575 continue;
1576 }
1577 if( !aTargetArea->m_zone->GetLayerSet().Contains( refFP->GetLayer() ) )
1578 {
1579 wxLogTrace( traceMultichannelTool, wxT( "discard ref:%s (target layer)\n" ),
1580 refFP->GetReference() );
1581 continue;
1582 }
1583
1584 // For regular Rule Area repeat, ignore source footprints outside the reference area.
1585 // For Design Block apply, use the exact source item set collected from the block.
1587 && !refFP->GetEffectiveShape( refFP->GetLayer() )->Collide( &refPoly, 0 ) )
1588 {
1589 continue;
1590 }
1591
1592 if( targetFP->IsLocked() && !aOpts.m_includeLockedItems )
1593 continue;
1594
1595 aCommit->Modify( targetFP );
1596
1597 targetFP->SetLayerAndFlip( refFP->GetLayer() );
1598 targetFP->SetOrientation( refFP->GetOrientation() );
1599 targetFP->SetPosition( refFP->GetPosition() );
1600 targetFP->Rotate( VECTOR2( 0, 0 ), rot );
1601 targetFP->Move( disp );
1602
1603 for( PCB_FIELD* refField : refFP->GetFields() )
1604 {
1605 wxCHECK2( refField, continue );
1606
1607 PCB_FIELD* targetField = targetFP->GetField( refField->GetName() );
1608
1609 if( !targetField )
1610 continue;
1611
1612 targetField->SetLayerSet( refField->GetLayerSet() );
1613 targetField->SetVisible( refField->IsVisible() );
1614 targetField->SetAttributes( refField->GetAttributes() );
1615 targetField->SetPosition( refField->GetPosition() );
1616 targetField->Rotate( VECTOR2( 0, 0 ), rot );
1617 targetField->Move( disp );
1618 targetField->SetIsKnockout( refField->IsKnockout() );
1619 }
1620
1621 // Copy non-field text items. Texts can share content (e.g. "${REFERENCE}" on both
1622 // F.SilkS and B.SilkS), so match one-to-one and prefer the same layer. Otherwise both
1623 // collapse onto one target item and the other side's text is lost.
1624 std::set<PCB_TEXT*> consumedTargets;
1625
1626 for( BOARD_ITEM* refItem : refFP->GraphicalItems() )
1627 {
1628 if( refItem->Type() != PCB_TEXT_T )
1629 continue;
1630
1631 PCB_TEXT* refText = static_cast<PCB_TEXT*>( refItem );
1632 PCB_TEXT* targetText = nullptr;
1633
1634 for( BOARD_ITEM* targetItem : targetFP->GraphicalItems() )
1635 {
1636 if( targetItem->Type() != PCB_TEXT_T )
1637 continue;
1638
1639 PCB_TEXT* candidate = static_cast<PCB_TEXT*>( targetItem );
1640
1641 if( consumedTargets.contains( candidate ) || candidate->GetText() != refText->GetText() )
1642 {
1643 continue;
1644 }
1645
1646 targetText = candidate;
1647
1648 if( candidate->GetLayer() == refText->GetLayer() )
1649 break;
1650 }
1651
1652 if( !targetText )
1653 continue;
1654
1655 consumedTargets.insert( targetText );
1656
1657 targetText->SetLayer( refText->GetLayer() );
1658 targetText->SetVisible( refText->IsVisible() );
1659 targetText->SetAttributes( refText->GetAttributes() );
1660 targetText->SetPosition( refText->GetPosition() );
1661 targetText->Rotate( VECTOR2( 0, 0 ), rot );
1662 targetText->Move( disp );
1663 targetText->SetIsKnockout( refText->IsKnockout() );
1664 }
1665
1666 // Copy 3D model settings
1667 targetFP->Models() = refFP->Models();
1668
1669 aCompatData.m_affectedItems.insert( targetFP );
1670 aCompatData.m_groupableItems.insert( targetFP );
1671
1672 // The matched footprint maps to its target; treat it as reproduced so a group
1673 // containing it can be rebuilt.
1674 if( preserveGroups && refFP->GetParentGroup() )
1675 groupFixupPairs.emplace_back( refFP, targetFP );
1676
1677 if( preserveGroups )
1678 reproducedSourceItems.insert( refFP );
1679 }
1680 }
1681
1682 // Rebuild a source group only when all of its members were reproduced. A group that merely
1683 // overlaps the source area keeps uncopied members, so it is left untouched rather than
1684 // partially duplicated (issue 22316).
1685 if( preserveGroups && !groupFixupPairs.empty() )
1686 {
1687 std::map<EDA_GROUP*, EDA_GROUP*> groupMap;
1688 std::map<EDA_GROUP*, bool> fullyReproducedCache;
1689
1690 auto groupFullyReproduced =
1691 [&]( EDA_GROUP* aGroup )
1692 {
1693 if( auto it = fullyReproducedCache.find( aGroup ); it != fullyReproducedCache.end() )
1694 return it->second;
1695
1696 bool reproduced = true;
1697
1698 for( EDA_ITEM* member : aGroup->GetItems() )
1699 {
1700 // Nested groups are not reproduced, so a parent containing one can never
1701 // be fully reproduced.
1702 if( !member->IsBOARD_ITEM()
1703 || !reproducedSourceItems.contains( static_cast<BOARD_ITEM*>( member ) ) )
1704 {
1705 reproduced = false;
1706 break;
1707 }
1708 }
1709
1710 fullyReproducedCache[aGroup] = reproduced;
1711 return reproduced;
1712 };
1713
1714 for( const auto& [sourceItem, destItem] : groupFixupPairs )
1715 {
1716 EDA_GROUP* parentGroup = sourceItem->GetParentGroup();
1717
1718 if( !parentGroup || !groupFullyReproduced( parentGroup ) )
1719 continue;
1720
1721 if( !groupMap.contains( parentGroup ) )
1722 {
1723 PCB_GROUP* newGroup = static_cast<PCB_GROUP*>(
1724 static_cast<PCB_GROUP*>( parentGroup->AsEdaItem() )->Duplicate( false ) );
1725 newGroup->GetItems().clear();
1726 newGroup->SetParentGroup( nullptr );
1727
1728 if( newGroup->Type() == PCB_GENERATOR_T )
1729 {
1730 newGroup->Rotate( VECTOR2( 0, 0 ), rot );
1731 newGroup->Move( disp );
1732 }
1733
1734 groupMap[parentGroup] = newGroup;
1735 aCommit->Add( newGroup );
1736 }
1737
1738 // AddItem reparents the footprint out of any group it already belongs to; stage that
1739 // group so the membership change is captured for undo.
1740 if( EDA_GROUP* oldGroup = destItem->GetParentGroup() )
1741 {
1742 if( oldGroup != groupMap[parentGroup] )
1743 aCommit->Modify( oldGroup->AsEdaItem() );
1744 }
1745
1746 groupMap[parentGroup]->AddItem( destItem );
1747 }
1748 }
1749
1750 aTargetArea->m_zone->RemoveAllContours();
1751 aTargetArea->m_zone->AddPolygon( newTargetOutline );
1752 aTargetArea->m_zone->UnHatchBorder();
1753 aTargetArea->m_zone->HatchBorder();
1754
1755 return true;
1756}
1757
1763 TMATCH::COMPONENT_MATCHES& aComponentMatches )
1764{
1765 // Copy as no-net.
1766 if( aComponentMatches.empty() )
1767 {
1768 aTarget->SetNetCode( 0 );
1769 return;
1770 }
1771
1772 auto connectivity = board()->GetConnectivity();
1773 const std::vector<BOARD_CONNECTED_ITEM*> refConnectedPads = connectivity->GetNetItems( aRef->GetNetCode(),
1774 { PCB_PAD_T } );
1775
1776 for( const BOARD_CONNECTED_ITEM* refConItem : refConnectedPads )
1777 {
1778 if( refConItem->Type() != PCB_PAD_T )
1779 continue;
1780
1781 const PAD* refPad = static_cast<const PAD*>( refConItem );
1782 FOOTPRINT* sourceFootprint = refPad->GetParentFootprint();
1783
1784 if( aComponentMatches.contains( sourceFootprint ) )
1785 {
1786 const FOOTPRINT* targetFootprint = aComponentMatches[sourceFootprint];
1787 std::vector<const PAD*> targetFpPads = targetFootprint->GetPads( refPad->GetNumber() );
1788
1789 if( !targetFpPads.empty() )
1790 {
1791 int targetNetCode = targetFpPads[0]->GetNet()->GetNetCode();
1792 aTarget->SetNetCode( targetNetCode );
1793
1794 break;
1795 }
1796 }
1797 }
1798}
1799
1800
1801std::vector<NETINFO_ITEM*> MULTICHANNEL_TOOL::IsolateDesignBlockAutoNets( BOARD* aBoard,
1802 const std::set<FOOTPRINT*>& aFootprints,
1803 const std::unordered_set<EDA_ITEM*>& aItems )
1804{
1805 std::vector<NETINFO_ITEM*> created;
1806 std::map<int, NETINFO_ITEM*> remap;
1807 int counter = 0;
1808
1809 // Auto-generated names are tied to a reference designator, so a block's Net-(D3-A) collides
1810 // with a different part's Net-(D3-A) on the board. Named/power nets are intentional and left
1811 // alone so the topology matcher keeps excluding real global rails.
1812 auto isAutoName = []( const wxString& aName )
1813 {
1814 return aName.StartsWith( wxT( "Net-(" ) ) || aName.StartsWith( wxT( "unconnected-" ) );
1815 };
1816
1817 auto remapItem = [&]( BOARD_CONNECTED_ITEM* aItem )
1818 {
1819 int code = aItem->GetNetCode();
1820
1821 if( code <= 0 )
1822 return;
1823
1824 NETINFO_ITEM* oldNet = aBoard->FindNet( code );
1825
1826 if( !oldNet || !isAutoName( oldNet->GetNetname() ) )
1827 return;
1828
1829 auto it = remap.find( code );
1830
1831 if( it == remap.end() )
1832 {
1833 wxString name;
1834
1835 do
1836 {
1837 name = wxString::Format( wxT( "__dbapply_%d_%d" ), code, counter++ );
1838 } while( aBoard->FindNet( name ) );
1839
1840 NETINFO_ITEM* newNet = new NETINFO_ITEM( aBoard, name );
1841 aBoard->Add( newNet );
1842 created.push_back( newNet );
1843 it = remap.emplace( code, newNet ).first;
1844 }
1845
1846 aItem->SetNet( it->second );
1847 };
1848
1849 for( FOOTPRINT* fp : aFootprints )
1850 {
1851 for( PAD* pad : fp->Pads() )
1852 remapItem( pad );
1853 }
1854
1855 for( EDA_ITEM* item : aItems )
1856 {
1857 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
1858 remapItem( bci );
1859 }
1860
1861 return created;
1862}
1863
1864
1865// A placed design block or repeated sheet stamps the originating symbol instance UUID into each
1866// footprint's path. When that linkage is complete and unique it is an authoritative one to one
1867// mapping, independent of net topology. Returns false (and leaves aResult untouched) unless it
1868// yields a full pad compatible bijection, so callers can fall back to topology matching.
1869static bool matchBySymbolInstancePath( const std::set<FOOTPRINT*>& aRef, const std::set<FOOTPRINT*>& aTarget,
1870 TMATCH::COMPONENT_MATCHES& aResult )
1871{
1872 if( aRef.empty() || aRef.size() != aTarget.size() )
1873 return false;
1874
1875 auto symbolUuid = []( const FOOTPRINT* aFp ) -> KIID
1876 {
1877 const KIID_PATH& path = aFp->GetPath();
1878 return path.empty() ? niluuid : path.back();
1879 };
1880
1881 std::map<KIID, FOOTPRINT*> targetByUuid;
1882
1883 for( FOOTPRINT* fp : aTarget )
1884 {
1885 KIID uuid = symbolUuid( fp );
1886
1887 // A missing or duplicated UUID (copy paste, hand built group) is not a clean instance link
1888 if( uuid == niluuid || !targetByUuid.emplace( uuid, fp ).second )
1889 return false;
1890 }
1891
1893 std::set<FOOTPRINT*> used;
1894
1895 for( FOOTPRINT* refFp : aRef )
1896 {
1897 KIID uuid = symbolUuid( refFp );
1898
1899 if( uuid == niluuid )
1900 return false;
1901
1902 auto it = targetByUuid.find( uuid );
1903
1904 if( it == targetByUuid.end() )
1905 return false;
1906
1907 FOOTPRINT* targetFp = it->second;
1908
1909 // Routing and placement copy only makes sense between pad compatible footprints
1910 if( refFp->GetFPID() != targetFp->GetFPID() || refFp->Pads().size() != targetFp->Pads().size() )
1911 return false;
1912
1913 if( !used.insert( targetFp ).second )
1914 return false;
1915
1916 result[refFp] = targetFp;
1917 }
1918
1919 aResult = std::move( result );
1920 return true;
1921}
1922
1923
1925 RULE_AREA_COMPAT_DATA& aMatches,
1926 const TMATCH::ISOMORPHISM_PARAMS& aParams )
1927{
1928 using namespace TMATCH;
1929
1930 // Footprint-free design block has nothing to match to
1931 if( aRefArea->m_sourceType == PLACEMENT_SOURCE_T::DESIGN_BLOCK && aRefArea->m_components.empty()
1932 && aTargetArea->m_components.empty() )
1933 {
1934 aMatches.m_matchingComponents.clear();
1935 aMatches.m_isOk = true;
1936 aMatches.m_errorMsg = _( "OK" );
1937 aMatches.m_mismatchReasons.clear();
1938 return true;
1939 }
1940
1941 // Placement areas resolve their components from their source, so two areas sharing a sheet,
1942 // component class or group resolve to identical footprints. Repeating into such a target would
1943 // move and delete the reference's own items, corrupting the placement rather than copying it.
1944 if( !aRefArea->m_components.empty() )
1945 {
1946 std::set<FOOTPRINT*> shared;
1947 std::set_intersection( aRefArea->m_components.begin(), aRefArea->m_components.end(),
1948 aTargetArea->m_components.begin(), aTargetArea->m_components.end(),
1949 std::inserter( shared, shared.begin() ) );
1950
1951 if( !shared.empty() )
1952 {
1953 aMatches.m_matchingComponents.clear();
1954 aMatches.m_isOk = false;
1955 aMatches.m_errorMsg = _( "Target Rule Area shares components with the reference area" );
1956 aMatches.m_mismatchReasons.clear();
1957 aMatches.m_mismatchReasons.push_back(
1958 _( "This target Rule Area selects the same components as the reference area. "
1959 "Repeat layout cannot copy a Rule Area onto itself. Give each placement Rule "
1960 "Area a distinct sheet, component class or group." ) );
1961 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Shared components:\n%s" ),
1962 FormatComponentList( shared ) ) );
1963 return false;
1964 }
1965 }
1966
1967 PROF_TIMER timerBuild;
1968 std::unique_ptr<CONNECTION_GRAPH> cgRef( CONNECTION_GRAPH::BuildFromFootprintSet( aRefArea->m_components,
1969 aTargetArea->m_components ) );
1970 std::unique_ptr<CONNECTION_GRAPH> cgTarget( CONNECTION_GRAPH::BuildFromFootprintSet( aTargetArea->m_components,
1971 aRefArea->m_components ) );
1972 timerBuild.Stop();
1973
1974 wxLogTrace( traceMultichannelTool, wxT( "Graph construction: %s (%d + %d components)" ),
1975 timerBuild.to_string(),
1976 (int) aRefArea->m_components.size(),
1977 (int) aTargetArea->m_components.size() );
1978
1979 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> mismatchReasons;
1980
1981 PROF_TIMER timerIso;
1982 bool status = cgRef->FindIsomorphism( cgTarget.get(), aMatches.m_matchingComponents,
1983 mismatchReasons, aParams );
1984 timerIso.Stop();
1985
1986 wxLogTrace( traceMultichannelTool, wxT( "FindIsomorphism: %s, result=%d" ),
1987 timerIso.to_string(), status ? 1 : 0 );
1988
1989 // Net topology can legitimately differ between a design block and its placed instance once the
1990 // user edits connectivity on the board (a wire tying two block pads onto one net, etc.). Fall
1991 // back to the symbol instance linkage, which still gives the correct mapping in that case. Skip
1992 // it on a cancelled scan so cancellation is not mistaken for a topology miss.
1993 const bool cancelled = aParams.m_cancelled && aParams.m_cancelled->load( std::memory_order_relaxed );
1994
1995 if( !status && !cancelled
1996 && matchBySymbolInstancePath( aRefArea->m_components, aTargetArea->m_components,
1997 aMatches.m_matchingComponents ) )
1998 {
1999 status = true;
2000 }
2001
2002 aMatches.m_isOk = status;
2003
2004 if( status )
2005 {
2006 aMatches.m_errorMsg = _( "OK" );
2007 aMatches.m_mismatchReasons.clear();
2008 return true;
2009 }
2010
2011 aMatches.m_mismatchReasons.clear();
2012
2013 for( const auto& reason : mismatchReasons )
2014 {
2015 if( reason.m_reason.IsEmpty() )
2016 continue;
2017
2018 if( !reason.m_reference.IsEmpty() && !reason.m_candidate.IsEmpty() )
2019 {
2020 aMatches.m_mismatchReasons.push_back( wxString::Format( wxT( "%s -> %s: %s" ),
2021 reason.m_reference,
2022 reason.m_candidate,
2023 reason.m_reason ) );
2024 }
2025 else if( !reason.m_reference.IsEmpty() )
2026 {
2027 aMatches.m_mismatchReasons.push_back( wxString::Format( wxT( "%s: %s" ),
2028 reason.m_reference,
2029 reason.m_reason ) );
2030 }
2031 else
2032 aMatches.m_mismatchReasons.push_back( reason.m_reason );
2033 }
2034
2035 if( aMatches.m_mismatchReasons.empty() )
2036 aMatches.m_mismatchReasons.push_back( _( "Topology mismatch" ) );
2037
2038 // Component count mismatch
2039 if( aRefArea->m_components.size() != aTargetArea->m_components.size() )
2040 {
2041 aMatches.m_mismatchReasons.push_back(
2042 wxString::Format( _( "Reference area total components: %d" ), (int) aRefArea->m_components.size() ) );
2043 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Reference area components:\n%s" ),
2044 FormatComponentList( aRefArea->m_components ) ) );
2045 aMatches.m_mismatchReasons.push_back(
2046 wxString::Format( _( "Target area total components: %d" ), (int) aTargetArea->m_components.size() ) );
2047 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Target area components:\n%s" ),
2048 FormatComponentList( aTargetArea->m_components ) ) );
2049 }
2050
2051 aMatches.m_errorMsg = aMatches.m_mismatchReasons.front();
2052
2053 return status;
2054}
2055
2056
2058 const std::unordered_set<BOARD_ITEM*>& aItemsToRemove )
2059{
2060 // Note: groups are only collections, not "real" hierarchy. A group's members are still parented
2061 // by the board (and therefore nested groups are still in the board's list of groups).
2062 for( PCB_GROUP* group : board()->Groups() )
2063 {
2064 std::vector<EDA_ITEM*> pruneList;
2065
2066 for( EDA_ITEM* refItem : group->GetItems() )
2067 {
2068 for( BOARD_ITEM* testItem : aItemsToRemove )
2069 {
2070 if( refItem->m_Uuid == testItem->m_Uuid )
2071 pruneList.push_back( refItem );
2072 }
2073 }
2074
2075 if( !pruneList.empty() )
2076 {
2077 aCommit.Modify( group );
2078
2079 for( EDA_ITEM* item : pruneList )
2080 group->RemoveItem( item );
2081
2082 if( group->GetItems().size() < 2 )
2083 aCommit.Remove( group );
2084 }
2085 }
2086
2087 return false;
2088}
2089
2090
2092{
2093 if( Pgm().IsGUI() )
2094 {
2096
2097 if( m_areas.m_areas.size() <= 1 )
2098 {
2099 frame()->ShowInfoBarError( _( "Cannot auto-generate any placement areas because the "
2100 "schematic has only one or no hierarchical sheets, "
2101 "groups, or component classes." ),
2102 true );
2103 return 0;
2104 }
2105
2107 int ret = dialog.ShowModal();
2108
2109 if( ret != wxID_OK )
2110 return 0;
2111 }
2112
2113 for( ZONE* zone : board()->Zones() )
2114 {
2115 if( !zone->GetIsRuleArea() )
2116 continue;
2117
2118 if( !zone->GetPlacementAreaEnabled() )
2119 continue;
2120
2121 std::set<FOOTPRINT*> components;
2122 RULE_AREA zoneRA;
2123 zoneRA.m_zone = zone;
2124 zoneRA.m_sourceType = zone->GetPlacementAreaSourceType();
2125 findComponentsInRuleArea( &zoneRA, components );
2126
2127 if( components.empty() )
2128 continue;
2129
2130 for( RULE_AREA& ra : m_areas.m_areas )
2131 {
2132 if( components == ra.m_components )
2133 {
2134 if( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME )
2135 {
2136 wxLogTrace( traceMultichannelTool,
2137 wxT( "Placement rule area for sheet '%s' already exists as '%s'\n" ),
2138 ra.m_sheetPath, zone->GetZoneName() );
2139 }
2140 else if( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::COMPONENT_CLASS )
2141 {
2142 wxLogTrace( traceMultichannelTool,
2143 wxT( "Placement rule area for component class '%s' already exists as '%s'\n" ),
2144 ra.m_componentClass, zone->GetZoneName() );
2145 }
2146 else
2147 {
2148 wxLogTrace( traceMultichannelTool,
2149 wxT( "Placement rule area for group '%s' already exists as '%s'\n" ),
2150 ra.m_groupName, zone->GetZoneName() );
2151 }
2152
2153 ra.m_oldZone = zone;
2154 ra.m_existsAlready = true;
2155 }
2156 }
2157 }
2158
2159 wxLogTrace( traceMultichannelTool, wxT( "%d placement areas found\n" ), (int) m_areas.m_areas.size() );
2160
2161 BOARD_COMMIT commit( GetManager(), true, false );
2162
2163 for( RULE_AREA& ra : m_areas.m_areas )
2164 {
2165 if( !ra.m_generateEnabled )
2166 continue;
2167
2168 if( ra.m_existsAlready && !m_areas.m_replaceExisting )
2169 continue;
2170
2171 if( ra.m_components.empty() )
2172 continue;
2173
2174 SHAPE_LINE_CHAIN raOutline;
2175
2176 // Groups are a way for the user to more explicitly provide a list of items to include in
2177 // the multichannel tool, as opposed to inferring them based on sheet structure or component classes.
2178 // So for group-based RAs, we build the RA outline based everything in the group, not just components.
2180 {
2181 std::set<BOARD_ITEM*> groupItems = queryBoardItemsInGroup( ra.m_groupName );
2182
2183 if( groupItems.empty() )
2184 {
2185 wxLogTrace( traceMultichannelTool,
2186 wxT( "Skipping placement rule area generation for source group '%s': group has no board items." ),
2187 ra.m_groupName );
2188 continue;
2189 }
2190
2191 raOutline = buildRAOutline( groupItems, 100000 );
2192 }
2193 else
2194 {
2195 raOutline = buildRAOutline( ra.m_components, 100000 );
2196 }
2197
2198 std::unique_ptr<ZONE> newZone( new ZONE( board() ) );
2199
2201 newZone->SetZoneName( wxString::Format( wxT( "auto-placement-area-%s" ), ra.m_sheetPath ) );
2203 newZone->SetZoneName( wxString::Format( wxT( "auto-placement-area-%s" ), ra.m_componentClass ) );
2204 else
2205 newZone->SetZoneName( wxString::Format( wxT( "auto-placement-area-%s" ), ra.m_groupName ) );
2206
2207 wxLogTrace( traceMultichannelTool, wxT( "Generated rule area '%s' (%d components)\n" ),
2208 newZone->GetZoneName(),
2209 (int) ra.m_components.size() );
2210
2211 newZone->SetIsRuleArea( true );
2212 newZone->SetLayerSet( LSET::AllCuMask() );
2213 newZone->SetPlacementAreaEnabled( true );
2214 newZone->SetDoNotAllowZoneFills( false );
2215 newZone->SetDoNotAllowVias( false );
2216 newZone->SetDoNotAllowTracks( false );
2217 newZone->SetDoNotAllowPads( false );
2218 newZone->SetDoNotAllowFootprints( false );
2219
2221 {
2222 newZone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::SHEETNAME );
2223 newZone->SetPlacementAreaSource( ra.m_sheetPath );
2224 }
2226 {
2227 newZone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::COMPONENT_CLASS );
2228 newZone->SetPlacementAreaSource( ra.m_componentClass );
2229 }
2230 else
2231 {
2232 newZone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
2233 newZone->SetPlacementAreaSource( ra.m_groupName );
2234 }
2235
2236 newZone->AddPolygon( raOutline );
2237 newZone->SetHatchStyle( ZONE_BORDER_DISPLAY_STYLE::NO_HATCH );
2238
2239 if( ra.m_existsAlready )
2240 {
2241 commit.Remove( ra.m_oldZone );
2242 }
2243
2244 ra.m_zone = newZone.release();
2245 commit.Add( ra.m_zone );
2246 }
2247
2248 // fixme: handle corner cases where the items belonging to a Rule Area already
2249 // belong to other groups.
2250
2251 if( m_areas.m_options.m_groupItems )
2252 {
2253 for( RULE_AREA& ra : m_areas.m_areas )
2254 {
2255 if( !ra.m_generateEnabled )
2256 continue;
2257
2258 if( ra.m_existsAlready && !m_areas.m_replaceExisting )
2259 continue;
2260
2261 // A group needs at least 2 items (zone + at least 1 component)
2262 if( ra.m_components.empty() )
2263 continue;
2264
2265 std::unordered_set<BOARD_ITEM*> toPrune;
2266
2267 std::copy( ra.m_components.begin(), ra.m_components.end(), std::inserter( toPrune, toPrune.begin() ) );
2268
2269 if( ra.m_existsAlready )
2270 toPrune.insert( ra.m_zone );
2271
2272 pruneExistingGroups( commit, toPrune );
2273
2274 PCB_GROUP* group = new PCB_GROUP( board() );
2275
2276 commit.Add( group );
2277
2278 commit.Modify( ra.m_zone );
2279 group->AddItem( ra.m_zone );
2280
2281 for( FOOTPRINT* fp : ra.m_components )
2282 {
2283 commit.Modify( fp );
2284 group->AddItem( fp );
2285 }
2286 }
2287 }
2288
2289 commit.Push( _( "Auto-generate placement rule areas" ) );
2290
2291 return true;
2292}
const char * name
@ ERROR_OUTSIDE
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
virtual void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
virtual void Revert() override
Revert the commit by restoring the modified items state.
A base class derived from BOARD_ITEM for items that can be connected and have a net,...
virtual bool SetNetCode(int aNetCode, bool aNoAssert)
Set net using a net code.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:81
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
Definition board_item.h:265
virtual void SetLayerSet(const LSET &aLayers)
Definition board_item.h:293
virtual bool IsKnockout() const
Definition board_item.h:352
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:353
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:313
FOOTPRINT * GetParentFootprint() const
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:373
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1346
NETINFO_ITEM * FindNet(int aNetcode) const
Search for a net with the given netcode.
Definition board.cpp:2708
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1149
std::shared_ptr< CONNECTIVITY_DATA > GetConnectivity() const
Return a list of missing connections between components/tracks.
Definition board.h:642
constexpr BOX2< Vec > GetInflated(coord_type aDx, coord_type aDy) const
Get a new rectangle that is this one, inflated by aDx and aDy.
Definition box2.h:634
Represent a set of changes (additions, deletions or modifications) of a data model (e....
Definition commit.h:68
COMMIT & Remove(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Remove a new item from the model.
Definition commit.h:86
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
Definition commit.h:102
COMMIT & Add(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Add a new item to the model.
Definition commit.h:74
int GetStatus(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr)
Returns status of an item.
Definition commit.cpp:191
A lightweight representation of a component class.
const std::vector< COMPONENT_CLASS * > & GetConstituentClasses() const
Fetches a vector of the constituent classes for this (effective) class.
const std::vector< BOARD_CONNECTED_ITEM * > GetNetItems(int aNetCode, const std::vector< KICAD_T > &aTypes) const
Function GetNetItems() Returns the list of items that belong to a certain net.
int ShowModal() override
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:42
std::unordered_set< EDA_ITEM * > & GetItems()
Definition eda_group.h:50
virtual EDA_ITEM * AsEdaItem()=0
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:96
const KIID m_Uuid
Definition eda_item.h:531
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:114
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:108
void ClearFlags(EDA_ITEM_FLAGS aMask=EDA_ITEM_ALL_FLAGS)
Definition eda_item.h:154
virtual void SetParentGroup(EDA_GROUP *aGroup)
Definition eda_item.h:113
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:110
virtual bool IsVisible() const
Definition eda_text.h:208
void SetAttributes(const EDA_TEXT &aSrc, bool aSetPosition=true)
Set the text attributes from another instance.
Definition eda_text.cpp:428
virtual void SetVisible(bool aVisible)
Definition eda_text.cpp:381
const TEXT_ATTRIBUTES & GetAttributes() const
Definition eda_text.h:252
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
double GetOrientationDegrees() const
Definition footprint.h:436
std::deque< PAD * > & Pads()
Definition footprint.h:375
const LIB_ID & GetFPID() const
Definition footprint.h:441
std::vector< const PAD * > GetPads(const wxString &aPadNumber, const PAD *aIgnore=nullptr) const
const wxString & GetReference() const
Definition footprint.h:841
VECTOR2I GetPosition() const override
Definition footprint.h:403
Definition kiid.h:44
wxString AsString() const
Definition kiid.cpp:242
void SetErrorCallback(std::function< void(const wxString &aMessage, int aOffset)> aCallback)
bool Compile(const wxString &aString, UCODE *aCode, CONTEXT *aPreflightContext)
void SetErrorCallback(std::function< void(const wxString &aMessage, int aOffset)> aCallback)
VALUE * Run(CONTEXT *ctx)
virtual double AsDouble() const
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
bool ContainsAll(const LSET &aLayers) const
See if this layer set contains all layers in another set.
Definition lset.h:85
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
bool Contains(PCB_LAYER_ID aLayer) const
See if the layer set contains a PCB layer.
Definition lset.h:63
int CheckRACompatibility(ZONE *aRefZone)
std::set< FOOTPRINT * > queryComponentsInSheet(wxString aSheetName) const
bool findOtherItemsInRuleArea(RULE_AREA *aRuleArea, std::set< BOARD_ITEM * > &aItems)
int repeatLayout(const TOOL_EVENT &aEvent)
bool findComponentsInRuleArea(RULE_AREA *aRuleArea, std::set< FOOTPRINT * > &aComponents)
void UpdatePickedItem(const EDA_ITEM *aItem) override
void setTransitions() override
This method is meant to be overridden in order to specify handlers for events.
const SHAPE_LINE_CHAIN buildRAOutline(std::set< FOOTPRINT * > &aFootprints, int aMargin)
bool resolveConnectionTopology(RULE_AREA *aRefArea, RULE_AREA *aTargetArea, RULE_AREA_COMPAT_DATA &aMatches, const TMATCH::ISOMORPHISM_PARAMS &aParams={})
int findRoutingInRuleArea(RULE_AREA *aRuleArea, std::set< BOARD_CONNECTED_ITEM * > &aOutput, std::shared_ptr< CONNECTIVITY_DATA > aConnectivity, const SHAPE_POLY_SET &aRAPoly, const REPEAT_LAYOUT_OPTIONS &aOpts) const
wxString stripComponentIndex(const wxString &aRef) const
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...
RULE_AREAS_DATA m_areas
bool pruneExistingGroups(COMMIT &aCommit, const std::unordered_set< BOARD_ITEM * > &aItemsToCheck)
void ShowMismatchDetails(wxWindow *aParent, const wxString &aSummary, const std::vector< wxString > &aReasons) const
int RepeatLayout(const TOOL_EVENT &aEvent, ZONE *aRefZone)
int AutogenerateRuleAreas(const TOOL_EVENT &aEvent)
void fixupNet(BOARD_CONNECTED_ITEM *aRef, BOARD_CONNECTED_ITEM *aTarget, TMATCH::COMPONENT_MATCHES &aComponentMatches)
Attempts to make sure copied items are assigned the right net.
bool copyRuleAreaContents(RULE_AREA *aRefArea, RULE_AREA *aTargetArea, BOARD_COMMIT *aCommit, REPEAT_LAYOUT_OPTIONS aOpts, RULE_AREA_COMPAT_DATA &aCompatData)
std::set< FOOTPRINT * > queryComponentsInComponentClass(const wxString &aComponentClassName) const
RULE_AREA * findRAByName(const wxString &aName)
std::set< FOOTPRINT * > queryComponentsInGroup(const wxString &aGroupName) const
std::set< BOARD_ITEM * > queryBoardItemsInGroup(const wxString &aGroupName) const
Handle the data for a net.
Definition netinfo.h:46
const wxString & GetNetname() const
Definition netinfo.h:100
Definition pad.h:61
const wxString & GetNumber() const
Definition pad.h:143
void SetItems(BOARD_ITEM *a, BOARD_ITEM *b=nullptr)
static TOOL_ACTION repeatLayout
static TOOL_ACTION generatePlacementRuleAreas
static TOOL_ACTION selectItemInteractively
Selection of reference points/items.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
void Move(const VECTOR2I &aMoveVector) override
Move this object.
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:49
void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const override
Invoke a function on all children.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
void Move(const VECTOR2I &aMoveVector) override
Move this object.
virtual VECTOR2I GetPosition() const override
Definition pcb_text.h:93
virtual void SetPosition(const VECTOR2I &aPos) override
Definition pcb_text.h:95
void Move(const VECTOR2I &aMoveVector) override
Move this object.
Definition pcb_text.h:97
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
Definition pcb_text.cpp:564
T * frame() const
PCB_TOOL_BASE(TOOL_ID aId, const std::string &aName)
Constructor.
BOARD * board() const
const PCB_SELECTION & selection() const
A small class to help profiling.
Definition profile.h:46
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:86
std::string to_string()
Definition profile.h:153
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
void Move(const VECTOR2I &aVector) override
void Rotate(const EDA_ANGLE &aAngle, const VECTOR2I &aCenter={ 0, 0 }) override
Rotate all vertices by a given angle.
Represent a set of closed polygons.
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
bool IsEmpty() const
Return true if the set is empty (no polygons at all)
virtual void CacheTriangulation(bool aSimplify=false, const TASK_SUBMITTER &aSubmitter={})
Build a polygon triangulation, needed to draw a polygon on OpenGL and in some other calculations.
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
static std::unique_ptr< CONNECTION_GRAPH > BuildFromFootprintSet(const std::set< FOOTPRINT * > &aFps, const std::set< FOOTPRINT * > &aOtherChannelFps={})
TOOL_MANAGER * GetManager() const
Return the instance of TOOL_MANAGER that takes care of the tool.
Definition tool_base.h:142
TOOL_MANAGER * m_toolMgr
Definition tool_base.h:220
Generic, UI-independent tool event.
Definition tool_event.h:167
void Go(int(T::*aStateFunc)(const TOOL_EVENT &), const TOOL_EVENT_LIST &aConditions=TOOL_EVENT(TC_ANY, TA_ANY))
Define which state (aStateFunc) to go when a certain event arrives (aConditions).
Define a general 2D-vector/point.
Definition vector2d.h:67
Handle a list of polygons defining a copper zone.
Definition zone.h:70
bool GetIsRuleArea() const
Accessors to parameters used in Rule Area zones:
Definition zone.h:813
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1393
wxString GetPlacementAreaSource() const
Definition zone.h:818
void HatchBorder()
Compute the hatch lines depending on the hatch parameters and stores it in the zone's attribute m_bor...
Definition zone.cpp:1520
PLACEMENT_SOURCE_T GetPlacementAreaSourceType() const
Definition zone.h:820
SHAPE_POLY_SET * Outline()
Definition zone.h:418
const wxString & GetZoneName() const
Definition zone.h:160
bool GetPlacementAreaEnabled() const
Definition zone.h:815
virtual LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition zone.h:133
void UnHatchBorder()
Clear the zone's hatch.
Definition zone.cpp:1514
void RemoveAllContours(void)
Definition zone.h:650
void BuildConvexHull(std::vector< VECTOR2I > &aResult, const std::vector< VECTOR2I > &aPoly)
Calculate the convex hull of a list of points in counter-clockwise order.
#define _(s)
@ RECURSE
Definition eda_item.h:49
@ NO_RECURSE
Definition eda_item.h:50
KIID niluuid(0)
static bool matchBySymbolInstancePath(const std::set< FOOTPRINT * > &aRef, const std::set< FOOTPRINT * > &aTarget, TMATCH::COMPONENT_MATCHES &aResult)
static wxString FormatComponentList(const std::set< FOOTPRINT * > &aComponents)
static const wxString traceMultichannelTool
static void ShowTopologyMismatchReasons(wxWindow *aParent, const wxString &aSummary, const std::vector< wxString > &aReasons)
SHAPE_LINE_CHAIN RectifyPolygon(const SHAPE_LINE_CHAIN &aPoly)
void CollectBoxCorners(const BOX2I &aBox, std::vector< VECTOR2I > &aCorners)
Add the 4 corners of a BOX2I to a vector.
std::map< FOOTPRINT *, FOOTPRINT * > COMPONENT_MATCHES
Definition topo_match.h:180
Class to handle a set of BOARD_ITEMs.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
Utility functions for working with shapes.
void AccumulateDescription(wxString &aDesc, const wxString &aItem)
Utility to build comma separated lists in messages.
std::vector< wxString > m_mismatchReasons
std::unordered_set< BOARD_ITEM * > m_affectedItems
Filled in by copyRuleAreaContents with items that were affected by the copy operation.
TMATCH::COMPONENT_MATCHES m_matchingComponents
std::unordered_set< BOARD_ITEM * > m_groupableItems
Filled in by copyRuleAreaContents with affected items that can be grouped together.
VECTOR2I m_center
std::unordered_set< EDA_ITEM * > m_designBlockItems
wxString m_sheetName
PLACEMENT_SOURCE_T m_sourceType
wxString m_componentClass
std::set< FOOTPRINT * > m_components
wxString m_ruleName
PCB_GROUP * m_group
wxString m_groupName
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
bool copied
std::string path
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:27
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:84
@ PCB_GROUP_T
class PCB_GROUP, a set of BOARD_ITEMs
Definition typeinfo.h:104
@ PCB_ZONE_T
class ZONE, a copper pour area
Definition typeinfo.h:101
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:85
@ PCB_TABLECELL_T
class PCB_TABLECELL, PCB_TEXTBOX for use in tables
Definition typeinfo.h:88
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:79
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:80
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
#define PR_CAN_ABORT