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>
25#include <tool/action_menu.h>
28#include <tool/tool_menu.h>
29#include <bitmaps.h>
30#include <eda_group.h>
31
34
35#include "multichannel_tool.h"
36
37#include <pcbexpr_evaluator.h>
38
39#include <zone.h>
40#include <board.h>
41#include <netinfo.h>
45#include <pcb_group.h>
46#include <pcb_generator.h>
47#include <footprint.h>
48#include <pad.h>
49#include <pcb_text.h>
54#include <algorithm>
55#include <pcb_track.h>
56#include <tool/tool_manager.h>
59#include <router/router_tool.h>
60#include <router/pns_router.h>
61#include <geometry/shape_rect.h>
62#include <scoped_set_reset.h>
63#include <trigo.h>
64#include <chrono>
65#include <future>
66#include <core/profile.h>
67#include <core/raii.h>
69#include <string_utils.h>
70#include <wx/log.h>
71#include <wx/richmsgdlg.h>
72#include <pgm_base.h>
73
74
75#define MULTICHANNEL_EXTRA_DEBUG
76
77static const wxString traceMultichannelTool = wxT( "MULTICHANNEL_TOOL" );
78
79
80static wxString FormatComponentList( const std::set<FOOTPRINT*>& aComponents )
81{
82 std::vector<wxString> refs;
83
84 for( FOOTPRINT* fp : aComponents )
85 {
86 if( !fp )
87 continue;
88
89 refs.push_back( fp->GetReferenceAsString() );
90 }
91
92 std::sort( refs.begin(), refs.end(),
93 []( const wxString& aLhs, const wxString& aRhs )
94 {
95 return aLhs.CmpNoCase( aRhs ) < 0;
96 } );
97
98 if( refs.empty() )
99 return _( "(none)" );
100
101 wxString result;
102 wxString line;
103 size_t componentsOnLine = 0;
104
105 for( const wxString& ref : refs )
106 {
107 if( componentsOnLine == 10 )
108 {
109 if( !result.IsEmpty() )
110 result += wxT( "\n" );
111
112 result += line;
113 line.clear();
114 componentsOnLine = 0;
115 }
116
117 AccumulateDescription( line, ref );
118 componentsOnLine++;
119 }
120
121 if( !line.IsEmpty() )
122 {
123 if( !result.IsEmpty() )
124 result += wxT( "\n" );
125
126 result += line;
127 }
128
129 return result;
130}
131
132
133static wxString JoinMismatchReasons( const std::vector<wxString>& aReasons )
134{
135 wxString text;
136
137 for( const wxString& reason : aReasons )
138 {
139 if( !text.IsEmpty() )
140 text += wxT( "\n" );
141
142 text += reason;
143 }
144
145 return text;
146}
147
148
149static void ShowTopologyMismatchReasons( wxWindow* aParent, const wxString& aSummary,
150 const std::vector<wxString>& aReasons )
151{
152 if( !aParent || aReasons.empty() )
153 return;
154
155 wxRichMessageDialog dlg( aParent, aSummary, _( "Topology mismatch" ), wxICON_ERROR | wxOK );
156 dlg.ShowDetailedText( JoinMismatchReasons( aReasons ) );
157 dlg.ShowModal();
158}
159
160
161static void collectGroupFootprints( EDA_GROUP* aGroup, std::set<FOOTPRINT*>& aOut )
162{
163 for( EDA_ITEM* item : aGroup->GetItems() )
164 {
165 if( item->Type() == PCB_FOOTPRINT_T )
166 aOut.insert( static_cast<FOOTPRINT*>( item ) );
167 else if( item->Type() == PCB_GROUP_T )
168 collectGroupFootprints( static_cast<PCB_GROUP*>( item ), aOut );
169 }
170}
171
172
173static void collectGroupBoardItems( EDA_GROUP* aGroup, std::set<BOARD_ITEM*>& aOut )
174{
175 for( EDA_ITEM* item : aGroup->GetItems() )
176 {
177 // A generator's own bounding box already covers its children (meander arcs).
178 if( item->Type() == PCB_GROUP_T )
179 collectGroupBoardItems( static_cast<PCB_GROUP*>( item ), aOut );
180 else if( item->IsBOARD_ITEM() )
181 aOut.insert( static_cast<BOARD_ITEM*>( item ) );
182 }
183}
184
185
186// A linked channel made with its items grouped is exactly its group, so an update repeats what the first copy did
187static EDA_GROUP* linkedChannelGroup( const BOARD* aBoard, const RULE_AREA* aArea )
188{
189 ZONE* zone = aArea->m_zone;
190
191 if( !zone || aArea->m_sourceType == PLACEMENT_SOURCE_T::DESIGN_BLOCK || zone->GetLinkSet() == niluuid
192 || !zone->GetRepeatOptions().m_groupItems )
193 {
194 return nullptr;
195 }
196
197 // Scratch zones carry the board pointer too, so membership is by the board's own index
198 if( aBoard->ResolveItem( zone->m_Uuid, true ) != zone )
199 return nullptr;
200
201 return zone->GetParentGroup();
202}
203
204
206{
207}
208
209
214
215
217{
218 PCB_TOOL_BASE::Reset( aReason );
219
220 if( aReason == MODEL_RELOAD || aReason == SUPERMODEL_RELOAD || aReason == SHUTDOWN )
222}
223
224static void makePlacementArea( ZONE* aZone, const SHAPE_LINE_CHAIN& aOutline )
225{
226 aZone->SetIsRuleArea( true );
227 aZone->SetLayerSet( LSET::AllCuMask() );
228 aZone->SetPlacementAreaEnabled( true );
229 aZone->SetDoNotAllowZoneFills( false );
230 aZone->SetDoNotAllowVias( false );
231 aZone->SetDoNotAllowTracks( false );
232 aZone->SetDoNotAllowPads( false );
233 aZone->SetDoNotAllowFootprints( false );
234 aZone->AddPolygon( aOutline );
236}
237
238
239// Touching edges do not count, so channels may share a border
240static bool outlinesOverlap( const SHAPE_POLY_SET& aA, const SHAPE_POLY_SET& aB )
241{
242 if( !aA.BBox().Intersects( aB.BBox() ) )
243 return false;
244
245 SHAPE_POLY_SET overlap = aA;
246 overlap.BooleanIntersection( aB );
247 return !overlap.IsEmpty();
248}
249
250
251// The matched pair holding the anchor footprint. An anchor outside the reference area falls back to the
252// first pair, so the group is not shifted to center alignment
253static const std::pair<FOOTPRINT* const, FOOTPRINT*>& anchorPair( const TMATCH::COMPONENT_MATCHES& aMatches,
254 const KIID& aAnchor )
255{
256 auto it = std::ranges::find_if( aMatches, [&]( const auto& aPair ) { return aPair.first->m_Uuid == aAnchor; } );
257 return it != aMatches.end() ? *it : *aMatches.begin();
258}
259
260
261// The outline aTarget takes on when aRef is repeated onto it, and the transform that produces it
262static SHAPE_LINE_CHAIN repeatedOutline( const RULE_AREA* aRef, const RULE_AREA* aTarget, const KIID& aAnchor,
263 const TMATCH::COMPONENT_MATCHES& aMatches, VECTOR2I& aDisp, EDA_ANGLE& aRot )
264{
265 aDisp = aTarget->m_center - aRef->m_center;
266 aRot = ANGLE_0;
267
268 if( aAnchor != niluuid && !aMatches.empty() )
269 {
270 const auto& [refAnchorFp, targetAnchorFp] = anchorPair( aMatches, aAnchor );
271 VECTOR2I oldpos = refAnchorFp->GetPosition();
272
273 aRot = targetAnchorFp->GetOrientation() - refAnchorFp->GetOrientation();
274 RotatePoint( oldpos, aRot );
275 aDisp = targetAnchorFp->GetPosition() - oldpos;
276 }
277
278 SHAPE_LINE_CHAIN outline( aRef->m_zone->Outline()->COutline( 0 ) );
279 outline.Rotate( aRot, VECTOR2I( 0, 0 ) );
280 outline.Move( aDisp );
281 return outline;
282}
283
284
285// The linked rule areas the selection stands for, whether as the area, a group holding it or a part of its channel
286static std::set<ZONE*> linkedAreasIn( const SELECTION& aSelection )
287{
288 std::set<ZONE*> zones;
289
290 auto asLinked = []( EDA_ITEM* aItem ) -> ZONE*
291 {
292 if( aItem->Type() != PCB_ZONE_T )
293 return nullptr;
294
295 ZONE* zone = static_cast<ZONE*>( aItem );
296 return zone->GetIsRuleArea() && zone->GetLinkSet() != niluuid ? zone : nullptr;
297 };
298
299 // True when the group directly holds a linked area
300 auto addFrom = [&]( EDA_GROUP* aGroup )
301 {
302 bool found = false;
303
304 for( EDA_ITEM* child : aGroup->GetItems() )
305 {
306 if( ZONE* zone = asLinked( child ) )
307 {
308 zones.insert( zone );
309 found = true;
310 }
311 }
312
313 return found;
314 };
315
316 for( EDA_ITEM* item : aSelection )
317 {
318 // What is selected by name wins over the channels around it
319 if( ZONE* zone = asLinked( item ) )
320 {
321 zones.insert( zone );
322 continue;
323 }
324
325 EDA_GROUP* group = item->Type() == PCB_GROUP_T ? static_cast<PCB_GROUP*>( item ) : item->GetParentGroup();
326
327 while( group && !addFrom( group ) )
328 group = group->AsEdaItem()->GetParentGroup();
329 }
330
331 return zones;
332}
333
334
336{
337 for( EDA_ITEM* child : aGroup->GetItems() )
338 {
339 if( child->Type() == PCB_ZONE_T )
340 {
341 ZONE* zone = static_cast<ZONE*>( child );
342
343 if( zone->GetIsRuleArea() && zone->GetPlacementAreaEnabled() )
344 return zone;
345 }
346 }
347
348 return nullptr;
349}
350
351
353{
354public:
356 {
358 SetTitle( _( "Multi-Channel" ) );
359
360 // A channel is repeated again through its Properties
361 auto offChannel =
362 []( const SELECTION& aSelection )
363 {
364 for( EDA_ITEM* item : aSelection )
365 {
366 if( item->Type() != PCB_GROUP_T )
367 continue;
368
369 if( MULTICHANNEL_TOOL::ChannelZone( static_cast<PCB_GROUP*>( item ) ) )
370 return false;
371 }
372
373 return true;
374 };
375
376 auto oneLinked = []( const SELECTION& aSelection ) { return linkedAreasIn( aSelection ).size() == 1; };
377 auto anyLinked = []( const SELECTION& aSelection ) { return !linkedAreasIn( aSelection ).empty(); };
378
381 AddItem( PCB_ACTIONS::repeatLayout, offChannel );
384 }
385
386 ACTION_MENU* create() const override { return new MULTICHANNEL_CONTEXT_MENU( m_tool ); }
387};
388
389
390void MULTICHANNEL_TOOL::ShowMismatchDetails( wxWindow* aParent, const wxString& aSummary,
391 const std::vector<wxString>& aReasons ) const
392{
393 wxWindow* parent = aParent ? aParent : frame();
394 ShowTopologyMismatchReasons( parent, aSummary, aReasons );
395}
396
397
399{
400 PCB_SELECTION_TOOL* selTool = m_toolMgr->GetTool<PCB_SELECTION_TOOL>();
401 wxCHECK( selTool, false );
402
403 TOOL_MENU& selToolMenu = selTool->GetToolMenu();
404
405 std::shared_ptr<MULTICHANNEL_CONTEXT_MENU> menu = std::make_shared<MULTICHANNEL_CONTEXT_MENU>( this );
406 selToolMenu.RegisterSubMenu( menu );
407
408 selToolMenu.GetMenu().AddMenu( menu.get(),
409 SELECTION_CONDITIONS::HasTypes( { PCB_FOOTPRINT_T, PCB_GROUP_T, PCB_ZONE_T } ),
410 100 );
411
412 return true;
413}
414
415
425
426
428 std::set<FOOTPRINT*>& aComponents )
429{
430 if( !aRuleArea || !aRuleArea->m_zone )
431 return false;
432
433 // When we're copying the layout of a design block, we are provided an exact list of items
434 // rather than querying the board for items that are inside the area.
436 {
437 // Get all board connected items that are from the design bloc
438 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
439 {
440 if( item->Type() == PCB_FOOTPRINT_T )
441 aComponents.insert( static_cast<FOOTPRINT*>( item ) );
442 }
443
444 return (int) aComponents.size();
445 }
446
447
449 PCBEXPR_UCODE ucode;
450 PCBEXPR_CONTEXT ctx, preflightCtx;
451
452 auto reportError =
453 [&]( const wxString& aMessage, int aOffset )
454 {
455 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s"), aMessage );
456 };
457
458 ctx.SetErrorCallback( reportError );
459 preflightCtx.SetErrorCallback( reportError );
460 compiler.SetErrorCallback( reportError );
461 //compiler.SetDebugReporter( m_reporter );
462
463 wxLogTrace( traceMultichannelTool, wxT( "rule area '%s'" ), aRuleArea->m_zone->GetZoneName() );
464
465 wxString sourceName = aRuleArea->m_zone->GetPlacementAreaSource();
466 sourceName.Replace( wxT( "'" ), wxT( "\\'" ) );
467
468 wxString ruleText;
469
470 switch( aRuleArea->m_zone->GetPlacementAreaSourceType() )
471 {
473 ruleText = wxT( "A.memberOfSheetOrChildren('" ) + sourceName + wxT( "')" );
474 break;
476 ruleText = wxT( "A.hasComponentClass('" ) + sourceName + wxT( "')" );
477 break;
479 ruleText = wxT( "A.memberOfGroup('" ) + sourceName + wxT( "')" );
480 break;
482 // For design blocks, handled above outside the rules system
483 break;
484 }
485
486 auto ok = compiler.Compile( ruleText, &ucode, &preflightCtx );
487
488 if( !ok )
489 return false;
490
491 for( FOOTPRINT* fp : board()->Footprints() )
492 {
493 ctx.SetItems( fp, fp );
494 LIBEVAL::VALUE* val = ucode.Run( &ctx );
495
496 if( val->AsDouble() != 0.0 )
497 {
498 wxLogTrace( traceMultichannelTool, wxT( " - %s [sheet %s]" ),
499 fp->GetReference(),
500 fp->GetSheetname() );
501
502 aComponents.insert( fp );
503 }
504 }
505
506 return true;
507}
508
509
510bool MULTICHANNEL_TOOL::findOtherItemsInRuleArea( RULE_AREA* aRuleArea, std::set<BOARD_ITEM*>& aItems )
511{
512 if( !aRuleArea || !aRuleArea->m_zone )
513 return false;
514
515 // When we're copying the layout of a design block, we are provided an exact list of items
516 // rather than querying the board for items that are inside the area.
518 {
519 // Get all board items that aren't footprints. Connected items are usually handled by the
520 // routing path, except zones which are copied via "other items".
521 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
522 {
523 if( item->Type() == PCB_FOOTPRINT_T )
524 continue;
525
526 // Generators are copied whole by the routing pass. Nested groups are not yet
527 // preserved (TODO) so they are skipped here.
528 if( item->Type() == PCB_GROUP_T || item->Type() == PCB_GENERATOR_T )
529 continue;
530
531 // Cells are copied with their owning PCB_TABLE, not as standalone items.
532 if( item->Type() == PCB_TABLECELL_T )
533 continue;
534
535 if( BOARD_ITEM* boardItem = dynamic_cast<BOARD_ITEM*>( item ) )
536 {
537 if( !boardItem->IsConnected() || boardItem->Type() == PCB_ZONE_T )
538 aItems.insert( boardItem );
539 }
540 }
541
542 if( aRuleArea->m_fromSelection )
543 {
544 const BOX2I areaBox = aRuleArea->m_zone->Outline()->BBox();
545
546 auto inside =
547 [&]( BOARD_ITEM* aItem )
548 {
549 return ( aRuleArea->m_zone->GetLayerSet() & aItem->GetLayerSet() ).any()
550 && areaBox.Contains( aItem->GetBoundingBox() );
551 };
552
553 for( ZONE* zone : board()->Zones() )
554 {
555 if( !zone->GetIsRuleArea() && inside( zone ) )
556 aItems.insert( zone );
557 }
558
559 for( BOARD_ITEM* drawing : board()->Drawings() )
560 {
561 if( !drawing->IsConnected() && !drawing->GetParentGroup() && inside( drawing ) )
562 aItems.insert( drawing );
563 }
564 }
565
566 return aItems.size() > 0;
567 }
568
569 if( EDA_GROUP* group = linkedChannelGroup( board(), aRuleArea ) )
570 {
571 std::set<BOARD_ITEM*> members;
572 collectGroupBoardItems( group, members );
573
574 for( BOARD_ITEM* boardItem : members )
575 {
576 if( boardItem == aRuleArea->m_zone || boardItem->Type() == PCB_FOOTPRINT_T
577 || boardItem->Type() == PCB_GENERATOR_T || boardItem->Type() == PCB_TABLECELL_T )
578 {
579 continue;
580 }
581
582 if( !boardItem->IsConnected() || boardItem->Type() == PCB_ZONE_T )
583 aItems.insert( boardItem );
584 }
585
586 return true;
587 }
588
589 // The design-block apply target uses a scratch zone not on the board, so enclosedByArea()
590 // below finds nothing. Resolve the group's items directly from the group.
592 && ( !aRuleArea->m_components.empty() || aRuleArea->m_group ) )
593 {
594 bool zoneOnBoard = false;
595
596 for( ZONE* zone : board()->Zones() )
597 {
598 if( zone == aRuleArea->m_zone )
599 {
600 zoneOnBoard = true;
601 break;
602 }
603 }
604
605 if( !zoneOnBoard )
606 {
607 EDA_GROUP* group = aRuleArea->m_group;
608
609 if( !group && !aRuleArea->m_components.empty() )
610 group = ( *aRuleArea->m_components.begin() )->GetParentGroup();
611
612 if( group )
613 {
614 std::set<BOARD_ITEM*> members;
615 collectGroupBoardItems( group, members );
616
617 for( BOARD_ITEM* boardItem : members )
618 {
619 if( boardItem->Type() == PCB_FOOTPRINT_T || boardItem->Type() == PCB_GENERATOR_T
620 || boardItem->Type() == PCB_TABLECELL_T )
621 continue;
622
623 if( !boardItem->IsConnected() || boardItem->Type() == PCB_ZONE_T )
624 aItems.insert( boardItem );
625 }
626 }
627
628 return aItems.size() > 0;
629 }
630 }
631
633 PCBEXPR_UCODE ucode;
634 PCBEXPR_CONTEXT ctx, preflightCtx;
635
636 auto reportError =
637 [&]( const wxString& aMessage, int aOffset )
638 {
639 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s"), aMessage );
640 };
641
642 ctx.SetErrorCallback( reportError );
643 preflightCtx.SetErrorCallback( reportError );
644 compiler.SetErrorCallback( reportError );
645
646 // Use the zone's UUID to identify it uniquely. Using the zone name could match other zones
647 // with the same name (e.g., a copper fill zone with the same name as a rule area).
648 wxString ruleText = wxString::Format( wxT( "A.enclosedByArea('%s')" ),
649 aRuleArea->m_zone->m_Uuid.AsString() );
650
651 if( !compiler.Compile( ruleText, &ucode, &preflightCtx ) )
652 return false;
653
654 auto testAndAdd =
655 [&]( BOARD_ITEM* aItem )
656 {
657 ctx.SetItems( aItem, aItem );
658 auto val = ucode.Run( &ctx );
659
660 if( val->AsDouble() != 0.0 )
661 aItems.insert( aItem );
662 };
663
664 for( ZONE* zone : board()->Zones() )
665 {
666 if( zone == aRuleArea->m_zone )
667 continue;
668
669 testAndAdd( zone );
670 }
671
672 for( BOARD_ITEM* drawing : board()->Drawings() )
673 {
674 if( !drawing->IsConnected() )
675 testAndAdd( drawing );
676 }
677
678 return true;
679}
680
681
682std::set<FOOTPRINT*> MULTICHANNEL_TOOL::queryComponentsInSheet( wxString aSheetName ) const
683{
684 std::set<FOOTPRINT*> rv;
685
686 if( aSheetName.EndsWith( wxT( "/" ) ) )
687 aSheetName.RemoveLast();
688
689 wxString childPrefix = aSheetName + wxT( "/" );
690
691 for( FOOTPRINT* fp : board()->Footprints() )
692 {
693 auto sn = fp->GetSheetname();
694
695 if( sn.EndsWith( wxT( "/" ) ) )
696 sn.RemoveLast();
697
698 if( sn == aSheetName || sn.StartsWith( childPrefix ) )
699 rv.insert( fp );
700 }
701
702 return rv;
703}
704
705
706std::set<FOOTPRINT*>
707MULTICHANNEL_TOOL::queryComponentsInComponentClass( const wxString& aComponentClassName ) const
708{
709 std::set<FOOTPRINT*> rv;
710
711 for( FOOTPRINT* fp : board()->Footprints() )
712 {
713 if( fp->GetComponentClass()->ContainsClassName( aComponentClassName ) )
714 rv.insert( fp );
715 }
716
717 return rv;
718}
719
720
721std::set<FOOTPRINT*> MULTICHANNEL_TOOL::queryComponentsInGroup( const wxString& aGroupName ) const
722{
723 std::set<FOOTPRINT*> rv;
724
725 for( PCB_GROUP* group : board()->Groups() )
726 {
727 if( group->GetName().Matches( aGroupName ) )
729 }
730
731 return rv;
732}
733
734
735PCB_GROUP* MULTICHANNEL_TOOL::findPlacementGroup( const wxString& aGroupName ) const
736{
737 PCB_GROUP* found = nullptr;
738
739 for( PCB_GROUP* group : board()->Groups() )
740 {
741 if( !group->GetName().Matches( aGroupName ) )
742 continue;
743
744 if( found )
745 return nullptr;
746
747 found = group;
748 }
749
750 return found;
751}
752
753
754std::set<BOARD_ITEM*> MULTICHANNEL_TOOL::queryBoardItemsInGroup( const wxString& aGroupName ) const
755{
756 std::set<BOARD_ITEM*> rv;
757
758 for( PCB_GROUP* group : board()->Groups() )
759 {
760 if( !group->GetName().Matches( aGroupName ) )
761 continue;
762
763 for( EDA_ITEM* item : group->GetItems() )
764 {
765 if( item->IsBOARD_ITEM() )
766 rv.insert( static_cast<BOARD_ITEM*>( item ) );
767 }
768 }
769
770 return rv;
771}
772
773
774const SHAPE_LINE_CHAIN MULTICHANNEL_TOOL::buildRAOutline( std::set<FOOTPRINT*>& aFootprints, int aMargin )
775{
776 std::vector<VECTOR2I> bbCorners;
777 bbCorners.reserve( aFootprints.size() * 4 );
778
779 for( FOOTPRINT* fp : aFootprints )
780 {
781 const BOX2I bb = fp->GetBoundingBox( false ).GetInflated( aMargin );
782 KIGEOM::CollectBoxCorners( bb, bbCorners );
783 }
784
785 std::vector<VECTOR2I> hullVertices;
786 BuildConvexHull( hullVertices, bbCorners );
787
788 SHAPE_LINE_CHAIN hull( hullVertices );
789
790 // Make the newly computed convex hull use only 90 degree segments
791 return KIGEOM::RectifyPolygon( hull );
792}
793
794const SHAPE_LINE_CHAIN MULTICHANNEL_TOOL::buildRAOutline( const std::set<BOARD_ITEM*>& aItems, int aMargin )
795{
796 std::vector<VECTOR2I> bbCorners;
797 bbCorners.reserve( aItems.size() * 4 );
798
799 for( BOARD_ITEM* item : aItems )
800 {
801 BOX2I bb = item->GetBoundingBox();
802
803 if( item->Type() == PCB_FOOTPRINT_T )
804 bb = static_cast<FOOTPRINT*>( item )->GetBoundingBox( false );
805
806 KIGEOM::CollectBoxCorners( bb.GetInflated( aMargin ), bbCorners );
807 }
808
809 std::vector<VECTOR2I> hullVertices;
810 BuildConvexHull( hullVertices, bbCorners );
811
812 SHAPE_LINE_CHAIN hull( hullVertices );
813
814 // Make the newly computed convex hull use only 90 degree segments
815 return KIGEOM::RectifyPolygon( hull );
816}
817
818
819// Returns each parent sheet path above aSheetName, e.g. "/A/B/C/" -> { "/A/", "/A/B/" }.
820// The root and the sheet itself are left out.
821static std::vector<wxString> getParentSheetPaths( const wxString& aSheetName )
822{
823 std::vector<wxString> segments;
824 wxString cur;
825
826 for( wxUniChar ch : aSheetName )
827 {
828 if( ch == '/' )
829 {
830 if( !cur.IsEmpty() )
831 segments.push_back( cur );
832
833 cur.clear();
834 }
835 else
836 {
837 cur += ch;
838 }
839 }
840
841 if( !cur.IsEmpty() )
842 segments.push_back( cur );
843
844 std::vector<wxString> rv;
845 wxString prefix = wxT( "/" );
846
847 for( size_t i = 0; i + 1 < segments.size(); ++i )
848 {
849 prefix += segments[i] + wxT( "/" );
850 rv.push_back( prefix );
851 }
852
853 return rv;
854}
855
856
858{
859 // Sheet path -> sheet file. Container sheets that only hold subsheets have no file of
860 // their own, so they map to an empty string.
861 std::map<wxString, wxString> uniqueSheets;
862 std::set<wxString> uniqueComponentClasses;
863 std::set<wxString> uniqueGroups;
864
865 m_areas.m_areas.clear();
866
867 for( const FOOTPRINT* fp : board()->Footprints() )
868 {
869 uniqueSheets[fp->GetSheetname()] = fp->GetSheetfile();
870
871 // Offer the parent sheets as channels too, not just the deepest one.
872 for( const wxString& parent : getParentSheetPaths( fp->GetSheetname() ) )
873 uniqueSheets.emplace( parent, wxString() );
874
875 const COMPONENT_CLASS* compClass = fp->GetComponentClass();
876
877 for( const COMPONENT_CLASS* singleClass : compClass->GetConstituentClasses() )
878 uniqueComponentClasses.insert( singleClass->GetName() );
879
880 // Offer every named group up the chain, not just the immediate parent, so a
881 // channel group wrapping several sub-groups can be picked too.
882 for( EDA_GROUP* grp = fp->GetParentGroup(); grp; grp = grp->AsEdaItem()->GetParentGroup() )
883 {
884 if( !grp->GetName().IsEmpty() )
885 uniqueGroups.insert( grp->GetName() );
886 }
887 }
888
889 for( const auto& [sheetPath, sheetFile] : uniqueSheets )
890 {
891 RULE_AREA ent;
892
894 ent.m_generateEnabled = false;
895 ent.m_sheetPath = sheetPath;
896 ent.m_sheetName = sheetFile;
898 m_areas.m_areas.push_back( ent );
899
900 wxLogTrace( traceMultichannelTool, wxT("found sheet '%s' @ '%s' s %d\n"),
901 ent.m_sheetName,
902 ent.m_sheetPath,
903 (int) m_areas.m_areas.size() );
904 }
905
906 for( const wxString& compClass : uniqueComponentClasses )
907 {
908 RULE_AREA ent;
909
911 ent.m_generateEnabled = false;
912 ent.m_componentClass = compClass;
914 m_areas.m_areas.push_back( ent );
915
916 wxLogTrace( traceMultichannelTool, wxT( "found component class '%s' s %d\n" ),
918 static_cast<int>( m_areas.m_areas.size() ) );
919 }
920
921 for( const wxString& groupName : uniqueGroups )
922 {
923 RULE_AREA ent;
924
926 ent.m_generateEnabled = false;
927 ent.m_groupName = groupName;
929 m_areas.m_areas.push_back( ent );
930
931 wxLogTrace( traceMultichannelTool, wxT( "found group '%s' s %d\n" ),
933 static_cast<int>( m_areas.m_areas.size() ) );
934 }
935}
936
937
939{
940 m_areas.m_areas.clear();
941
942 for( ZONE* zone : board()->Zones() )
943 {
944 if( !zone->GetIsRuleArea() )
945 continue;
946
947 if( !zone->GetPlacementAreaEnabled() )
948 continue;
949
950 RULE_AREA area;
951
952 area.m_zone = zone;
953 area.m_sourceType = zone->GetPlacementAreaSourceType();
954 area.m_ruleName = zone->GetZoneName();
955 area.m_center = zone->Outline()->COutline( 0 ).Centre();
956
957 switch( area.m_sourceType )
958 {
959 case PLACEMENT_SOURCE_T::SHEETNAME: area.m_sheetPath = zone->GetPlacementAreaSource(); break;
960
961 case PLACEMENT_SOURCE_T::COMPONENT_CLASS: area.m_componentClass = zone->GetPlacementAreaSource(); break;
962
964 area.m_groupName = zone->GetPlacementAreaSource();
966 break;
967
969 }
970
972
973 m_areas.m_areas.push_back( area );
974
975 wxLogTrace( traceMultichannelTool, wxT( "RA '%s', %d footprints\n" ), area.m_ruleName,
976 (int) area.m_components.size() );
977 }
978
979 wxLogTrace( traceMultichannelTool, wxT( "Total RAs found: %d\n" ), (int) m_areas.m_areas.size() );
980}
981
982
984{
985 if( aItem )
987}
988
989
991{
992 std::vector<ZONE*> refRAs;
993
994 auto isSelectedItemAnRA =
995 []( EDA_ITEM* aItem ) -> ZONE*
996 {
997 if( !aItem || aItem->Type() != PCB_ZONE_T )
998 return nullptr;
999
1000 ZONE* zone = static_cast<ZONE*>( aItem );
1001
1002 if( !zone->GetIsRuleArea() )
1003 return nullptr;
1004
1005 if( !zone->GetPlacementAreaEnabled() )
1006 return nullptr;
1007
1008 return zone;
1009 };
1010
1011 for( EDA_ITEM* item : selection() )
1012 {
1013 if( ZONE* zone = isSelectedItemAnRA( item ) )
1014 {
1015 refRAs.push_back( zone );
1016 }
1017 else if( item->Type() == PCB_GROUP_T )
1018 {
1019 PCB_GROUP *group = static_cast<PCB_GROUP*>( item );
1020
1021 for( EDA_ITEM* grpItem : group->GetItems() )
1022 {
1023 if( ZONE* grpZone = isSelectedItemAnRA( grpItem ) )
1024 refRAs.push_back( grpZone );
1025 }
1026 }
1027 }
1028
1030
1031 // Plain parts with no rule area are the reference, and the channels repeating them are discovered
1032 if( refRAs.empty() && ( selection().HasType( PCB_FOOTPRINT_T ) || selection().HasType( PCB_GROUP_T ) ) )
1033 {
1034 StartRepeatFromSelection( std::vector<EDA_ITEM*>( selection().begin(), selection().end() ) );
1035
1036 if( !m_areas.m_refRA )
1037 {
1038 frame()->ShowInfoBarMsg( _( "Select the footprints of the circuit to repeat." ), true );
1039 return 0;
1040 }
1041 }
1042 else if( refRAs.size() != 1 )
1043 {
1046 this,
1047 _( "Select a reference Rule Area to copy from..." ),
1048 [&]( EDA_ITEM* aItem )
1049 {
1050 return isSelectedItemAnRA( aItem ) != nullptr;
1051 }
1052 } );
1053
1054 return 0;
1055 }
1056 else
1057 {
1059
1060 int status = CheckRACompatibility( refRAs.front() );
1061
1062 if( status < 0 )
1063 return status;
1064
1065 if( m_areas.m_areas.size() <= 1 )
1066 {
1067 frame()->ShowInfoBarError( _( "No Rule Areas to repeat layout to have been found." ), true );
1068 return 0;
1069 }
1070
1071 const KIID& linkSet = refRAs.front()->GetLinkSet();
1072 m_areas.m_options = refRAs.front()->GetRepeatOptions();
1073 m_areas.m_linkAreas = linkSet != niluuid;
1074
1075 // A linked source's set is the user's previous choice
1076 for( auto& [area, compat] : m_areas.m_compatMap )
1077 compat.m_doCopy = compat.m_isOk && ( linkSet == niluuid || area->m_zone->GetLinkSet() == linkSet );
1078 }
1079
1080 // The view's preview group can be gone since the board loaded, so it is set up again as the router does
1081 getView()->InitPreview();
1082
1083 // Not modal, so the board can be panned and zoomed while the preview shows the result
1085 m_dialog->Bind( wxEVT_CLOSE_WINDOW, &MULTICHANNEL_TOOL::onRepeatDialogClosed, this );
1086 m_dialog->Show( true );
1088 return 0;
1089}
1090
1091
1092// A net that belongs to a channel reaches at most one footprint outside it (a connector or the neighbouring
1093// block). Rails and buses reach many, so they never tie a channel together
1094static bool isChannelNet( const std::vector<FOOTPRINT*>& aOwners, const std::set<FOOTPRINT*>& aChannel )
1095{
1096 return std::ranges::count_if( aOwners, [&]( FOOTPRINT* aFp ) { return !aChannel.contains( aFp ); } ) <= 1;
1097}
1098
1099
1100static std::set<int> channelNets( const std::map<int, std::vector<FOOTPRINT*>>& aNetOwners,
1101 const std::set<FOOTPRINT*>& aChannel )
1102{
1103 std::set<int> nets;
1104
1105 for( const auto& [net, owners] : aNetOwners )
1106 {
1107 if( owners.size() > 1 && isChannelNet( owners, aChannel )
1108 && std::ranges::any_of( owners, [&]( FOOTPRINT* aFp ) { return aChannel.contains( aFp ); } ) )
1109 {
1110 nets.insert( net );
1111 }
1112 }
1113
1114 return nets;
1115}
1116
1117
1118// Moves aItem into aGroup, staging the group it leaves so undo restores that membership too
1119static void stageAddToGroup( BOARD_COMMIT& aCommit, EDA_GROUP* aGroup, BOARD_ITEM* aItem )
1120{
1121 if( EDA_GROUP* old = aItem->GetParentGroup() )
1122 {
1123 if( old == aGroup || old->AsEdaItem()->Type() == PCB_GENERATOR_T )
1124 return;
1125
1126 aCommit.Modify( old->AsEdaItem(), nullptr, RECURSE_MODE::NO_RECURSE );
1127 }
1128
1129 aCommit.Modify( aItem );
1130 aGroup->AddItem( aItem );
1131}
1132
1133
1134static std::set<int> padNets( const FOOTPRINT* aFp, const std::set<int>& aAmong )
1135{
1136 std::set<int> nets;
1137
1138 for( const PAD* pad : aFp->Pads() )
1139 {
1140 if( aAmong.contains( pad->GetNetCode() ) )
1141 nets.insert( pad->GetNetCode() );
1142 }
1143
1144 return nets;
1145}
1146
1147
1148// Repeated sheets give every instance the same symbol UUIDs under its own instance path
1149static std::vector<TMATCH::COMPONENT_MATCHES> matchSheetInstances( BOARD* aBoard, const std::set<FOOTPRINT*>& aRef )
1150{
1151 using PATH = std::vector<KIID>;
1152
1153 std::optional<PATH> refPrefix;
1154
1155 for( FOOTPRINT* fp : aRef )
1156 {
1157 const KIID_PATH& path = fp->GetPath();
1158
1159 if( path.size() < 2 )
1160 return {};
1161
1162 if( !refPrefix )
1163 refPrefix = PATH( path.begin(), path.end() - 1 );
1164
1165 size_t depth = std::min( refPrefix->size(), path.size() - 1 );
1166 auto diff = std::mismatch( refPrefix->begin(), refPrefix->begin() + depth, path.begin() );
1167
1168 refPrefix->resize( diff.first - refPrefix->begin() );
1169 }
1170
1171 if( !refPrefix || refPrefix->empty() )
1172 return {};
1173
1174 const size_t depth = refPrefix->size();
1175
1176 auto rest =
1177 [&]( const KIID_PATH& aPath )
1178 {
1179 return PATH( aPath.begin() + depth, aPath.end() );
1180 };
1181
1182 std::map<PATH, FOOTPRINT*> refByRest;
1183
1184 for( FOOTPRINT* fp : aRef )
1185 refByRest[rest( fp->GetPath() )] = fp;
1186
1187 std::map<PATH, TMATCH::COMPONENT_MATCHES> instances;
1188
1189 for( FOOTPRINT* fp : aBoard->Footprints() )
1190 {
1191 const KIID_PATH& path = fp->GetPath();
1192
1193 if( aRef.contains( fp ) || path.size() <= depth )
1194 continue;
1195
1196 PATH prefix( path.begin(), path.begin() + depth );
1197
1198 if( prefix == *refPrefix )
1199 continue;
1200
1201 if( auto it = refByRest.find( rest( path ) ); it != refByRest.end() && it->second->GetFPID() == fp->GetFPID() )
1202 instances[prefix][it->second] = fp;
1203 }
1204
1205 std::vector<TMATCH::COMPONENT_MATCHES> result;
1206
1207 for( auto& [prefix, matches] : instances )
1208 result.push_back( std::move( matches ) );
1209
1210 return result;
1211}
1212
1213
1214// Pairs the reference footprints, in growth order from the seed, with board footprints that sit on the same
1215// mirrored nets, backtracking on dead ends. With aLoose a leaf may stay unmatched
1217{
1218 const std::map<int, std::vector<FOOTPRINT*>>& m_netOwners;
1219 const std::set<int>& m_internal;
1220 const std::set<FOOTPRINT*>& m_leaves;
1221 const std::set<FOOTPRINT*>& m_claimed;
1222 const std::vector<FOOTPRINT*>& m_order;
1224
1226 std::set<FOOTPRINT*> m_used = {};
1227 std::map<int, int> m_netMap = {};
1228 std::set<int> m_netsTaken = {};
1229 int m_budget = 10000;
1230
1231 bool bind( FOOTPRINT* aRef, FOOTPRINT* aTarget, std::vector<int>& aAdded )
1232 {
1233 for( PAD* pad : aRef->Pads() )
1234 {
1235 int net = pad->GetNetCode();
1236
1237 if( !m_internal.contains( net ) )
1238 continue;
1239
1240 const PAD* peer = aTarget->FindPadByNumber( pad->GetNumber() );
1241
1242 if( !peer )
1243 return false;
1244
1245 int peerNet = peer->GetNetCode();
1246
1247 if( auto it = m_netMap.find( net ); it != m_netMap.end() )
1248 {
1249 if( it->second != peerNet )
1250 return false;
1251 }
1252 else
1253 {
1254 if( peerNet <= 0 || m_netsTaken.contains( peerNet ) )
1255 return false;
1256
1257 m_netMap[net] = peerNet;
1258 m_netsTaken.insert( peerNet );
1259 aAdded.push_back( net );
1260 }
1261 }
1262
1263 return true;
1264 }
1265
1266 // Pairs m_order[aIndex] with aChoice and matches the rest, undoing the pairing on a dead end
1267 bool pair( size_t aIndex, FOOTPRINT* aChoice )
1268 {
1269 FOOTPRINT* ref = m_order[aIndex];
1270 std::vector<int> added;
1271
1272 if( bind( ref, aChoice, added ) )
1273 {
1274 m_map[ref] = aChoice;
1275 m_used.insert( aChoice );
1276
1277 if( extend( aIndex + 1 ) )
1278 return true;
1279
1280 m_map.erase( ref );
1281 m_used.erase( aChoice );
1282 }
1283
1284 for( int net : added )
1285 {
1286 m_netsTaken.erase( m_netMap[net] );
1287 m_netMap.erase( net );
1288 }
1289
1290 return false;
1291 }
1292
1293 bool extend( size_t aIndex )
1294 {
1295 if( aIndex == m_order.size() )
1296 return true;
1297
1298 if( --m_budget < 0 )
1299 return false;
1300
1301 FOOTPRINT* ref = m_order[aIndex];
1302
1303 // One mapped net is enough to bound the choices; bind() checks the rest
1304 auto mapped = std::ranges::find_if( ref->Pads(),
1305 [&]( PAD* aPad ) { return m_netMap.contains( aPad->GetNetCode() ); } );
1306
1307 if( mapped != ref->Pads().end() )
1308 {
1309 for( FOOTPRINT* fp : m_netOwners.at( m_netMap.at( ( *mapped )->GetNetCode() ) ) )
1310 {
1311 if( !m_claimed.contains( fp ) && !m_used.contains( fp ) && fp->GetFPID() == ref->GetFPID()
1312 && pair( aIndex, fp ) )
1313 {
1314 return true;
1315 }
1316
1317 if( m_budget < 0 )
1318 return false;
1319 }
1320 }
1321
1322 // A leaf may stay unmatched; the caller works out which parts were left out
1323 return m_loose && m_leaves.contains( ref ) && extend( aIndex + 1 );
1324 }
1325};
1326
1327
1328void MULTICHANNEL_TOOL::StartRepeatFromSelection( const std::vector<EDA_ITEM*>& aItems )
1329{
1331
1332 // The channels become groups, so copies join them
1333 m_areas.m_options.m_groupItems = true;
1334
1335 for( EDA_ITEM* item : aItems )
1336 {
1337 if( item->Type() == PCB_GROUP_T )
1338 collectGroupBoardItems( static_cast<PCB_GROUP*>( item ), m_areas.m_selectedItems );
1339 else if( item->IsBOARD_ITEM() && !static_cast<BOARD_ITEM*>( item )->GetParentFootprint() )
1340 m_areas.m_selectedItems.insert( static_cast<BOARD_ITEM*>( item ) );
1341 }
1342
1343 for( BOARD_ITEM* item : m_areas.m_selectedItems )
1344 {
1345 if( item->Type() == PCB_FOOTPRINT_T )
1346 m_areas.m_selectedFootprints.insert( static_cast<FOOTPRINT*>( item ) );
1347 }
1348
1349 DiscoverRepeatCandidates( false );
1350
1351 // A session without a reference must not divert later rule area repeats into the selection flow
1352 if( !m_areas.m_refRA )
1354}
1355
1356
1357// The footprints on each net, in board order
1358static std::map<int, std::vector<FOOTPRINT*>> buildNetOwners( const BOARD* aBoard )
1359{
1360 std::map<int, std::vector<FOOTPRINT*>> netOwners;
1361
1362 for( FOOTPRINT* fp : aBoard->Footprints() )
1363 {
1364 for( PAD* pad : fp->Pads() )
1365 {
1366 std::vector<FOOTPRINT*>& owners = netOwners[pad->GetNetCode()];
1367
1368 if( pad->GetNetCode() > 0 && ( owners.empty() || owners.back() != fp ) )
1369 owners.push_back( fp );
1370 }
1371 }
1372
1373 return netOwners;
1374}
1375
1376
1377std::vector<RULE_AREA_COMPAT_DATA> MULTICHANNEL_TOOL::findMatches( const std::set<FOOTPRINT*>& selected,
1378 bool aLoose )
1379{
1380 std::map<int, std::vector<FOOTPRINT*>> netOwners = buildNetOwners( board() );
1381
1382 std::set<int> internal = channelNets( netOwners, selected );
1383 std::set<FOOTPRINT*> leaves;
1384
1385 for( FOOTPRINT* fp : selected )
1386 {
1387 if( padNets( fp, internal ).size() <= 1 )
1388 leaves.insert( fp );
1389 }
1390
1391 std::vector<RULE_AREA_COMPAT_DATA> found;
1392
1393 // Kept when it lacks no selected part, or only leaves under loose matching
1394 auto classify =
1395 [&]( RULE_AREA_COMPAT_DATA& aCandidate )
1396 {
1397 for( FOOTPRINT* fp : selected )
1398 {
1399 if( !aCandidate.m_discovered.contains( fp ) )
1400 aCandidate.m_unmatched.insert( fp );
1401 }
1402
1403 aCandidate.m_doCopy = true;
1404
1405 return aCandidate.m_unmatched.empty()
1406 || ( aLoose && std::ranges::all_of( aCandidate.m_unmatched,
1407 [&]( FOOTPRINT* aFp ) { return leaves.contains( aFp ); } ) );
1408 };
1409
1410 for( TMATCH::COMPONENT_MATCHES& matches : matchSheetInstances( board(), selected ) )
1411 {
1412 RULE_AREA_COMPAT_DATA candidate;
1413 candidate.m_discovered = std::move( matches );
1414
1415 if( classify( candidate ) )
1416 found.push_back( std::move( candidate ) );
1417 }
1418
1419 if( found.empty() )
1420 {
1421 std::map<LIB_ID, int> fpidCount;
1422
1423 for( FOOTPRINT* fp : board()->Footprints() )
1424 fpidCount[fp->GetFPID()]++;
1425
1426 // The rarest part keeps the number of seeds low; a leaf may be the part a channel lacks
1427 FOOTPRINT* seed = *std::ranges::min_element( selected,
1428 [&]( FOOTPRINT* aA, FOOTPRINT* aB )
1429 {
1430 return std::make_pair( leaves.contains( aA ), fpidCount[aA->GetFPID()] )
1431 < std::make_pair( leaves.contains( aB ), fpidCount[aB->GetFPID()] );
1432 } );
1433
1434 std::vector<FOOTPRINT*> order = { seed };
1435
1436 for( size_t i = 0; i < order.size(); ++i )
1437 {
1438 for( int net : padNets( order[i], internal ) )
1439 {
1440 for( FOOTPRINT* fp : netOwners[net] )
1441 {
1442 if( selected.contains( fp ) && std::ranges::find( order, fp ) == order.end() )
1443 order.push_back( fp );
1444 }
1445 }
1446 }
1447
1448 std::set<FOOTPRINT*> claimed = selected;
1449
1450 for( FOOTPRINT* twin : board()->Footprints() )
1451 {
1452 if( claimed.contains( twin ) || twin->GetFPID() != seed->GetFPID() )
1453 continue;
1454
1455 FLAT_MATCHER matcher{ netOwners, internal, leaves, claimed, order, aLoose };
1456 RULE_AREA_COMPAT_DATA candidate;
1457 bool ok = matcher.pair( 0, twin );
1458
1459 if( matcher.m_budget < 0 )
1460 {
1461 candidate.m_discovered = { { seed, twin } };
1462 candidate.m_searchLimit = true;
1463 found.push_back( std::move( candidate ) );
1464 continue;
1465 }
1466
1467 candidate.m_discovered = matcher.m_map;
1468
1469 if( !ok || !classify( candidate ) )
1470 continue;
1471
1472 // Rails are judged against both channels together, whichever channels were found first
1473 RULE_AREA refArea;
1474 RULE_AREA targetArea;
1475 std::set<FOOTPRINT*> both;
1476 std::unordered_set<int> rails;
1477
1478 for( const auto& [ref, target] : candidate.m_discovered )
1479 {
1480 refArea.m_components.insert( ref );
1481 targetArea.m_components.insert( target );
1482 both.insert( { ref, target } );
1483 }
1484
1485 for( FOOTPRINT* fp : both )
1486 {
1487 for( PAD* pad : fp->Pads() )
1488 {
1489 if( pad->GetNetCode() > 0 && !isChannelNet( netOwners[pad->GetNetCode()], both ) )
1490 rails.insert( pad->GetNetCode() );
1491 }
1492 }
1493
1494 // A flat match is only a guess until the topology agrees, so it claims nothing before that
1496
1497 if( !resolveConnectionTopology( &refArea, &targetArea, check, {}, rails ) )
1498 continue;
1499
1500 claimed.insert( targetArea.m_components.begin(), targetArea.m_components.end() );
1501 found.push_back( std::move( candidate ) );
1502 }
1503 }
1504
1505 return found;
1506}
1507
1508
1510{
1511 const std::set<FOOTPRINT*>& selected = m_areas.m_selectedFootprints;
1512 PROF_TIMER timer;
1513
1514 m_areas.m_refRA = nullptr;
1515 m_focusArea = nullptr;
1516 m_areas.m_compatMap.clear();
1517 m_areas.m_areas.clear();
1518 m_areas.m_scratchItems.clear();
1519
1520 if( selected.empty() )
1521 return;
1522
1523 std::vector<RULE_AREA_COMPAT_DATA> found = findMatches( selected, aLoose );
1524
1525 // Filled before any pointer to its elements is taken
1526 m_areas.m_areas.reserve( found.size() + 1 );
1527
1528 auto addArea =
1529 [&]( const SHAPE_LINE_CHAIN& aOutline, const std::set<FOOTPRINT*>& aFootprints,
1530 PLACEMENT_SOURCE_T aSourceType ) -> RULE_AREA&
1531 {
1532 ZONE* zone = new ZONE( board() );
1533 PCB_GROUP* group = new PCB_GROUP( board() );
1534
1535 m_areas.m_scratchItems.emplace_back( zone );
1536 m_areas.m_scratchItems.emplace_back( group );
1537 makePlacementArea( zone, aOutline );
1538
1539 RULE_AREA& area = m_areas.m_areas.emplace_back();
1540 area.m_zone = zone;
1541 area.m_group = group;
1542 area.m_components = aFootprints;
1543 area.m_center = aOutline.Centre();
1544 area.m_sourceType = aSourceType;
1545 return area;
1546 };
1547
1548 // The selection is copied as given, like a design block, rather than by what its area encloses
1549 addArea( buildRAOutline( m_areas.m_selectedItems, 100000 ), selected, PLACEMENT_SOURCE_T::DESIGN_BLOCK )
1550 .m_fromSelection = true;
1551
1552 for( const RULE_AREA_COMPAT_DATA& candidate : found )
1553 {
1554 std::set<FOOTPRINT*> targets;
1555
1556 for( const auto& [ref, target] : candidate.m_discovered )
1557 targets.insert( target );
1558
1559 addArea( buildRAOutline( targets, 100000 ), targets, PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
1560 }
1561
1562 m_areas.m_refRA = &m_areas.m_areas.front();
1563
1564 for( size_t i = 0; i < found.size(); ++i )
1565 m_areas.m_compatMap[&m_areas.m_areas[i + 1]] = std::move( found[i] );
1566
1567 // A candidate that fails on its own starts unticked, so its missing parts do not shrink the others
1568 bool resolved = updateSelectionCore();
1569 bool unticked = false;
1570
1571 for( auto& [area, compat] : m_areas.m_compatMap )
1572 {
1573 unticked |= compat.m_doCopy && !compat.m_isOk;
1574 compat.m_doCopy &= compat.m_isOk;
1575 }
1576
1577 // Cancelled candidates are unticked too, and resolving the rest again would restart the search
1578 if( unticked && resolved )
1580
1581 wxLogTrace( traceMultichannelTool, wxT( "Discovery: %s, %d candidates" ), timer.to_string(),
1582 (int) m_areas.m_compatMap.size() );
1583}
1584
1585
1586std::set<FOOTPRINT*> MULTICHANNEL_TOOL::ExpandToMatchingTopology( const std::set<FOOTPRINT*>& aSeed, bool aLargest )
1587{
1588 // Each trial runs a full discovery, so a large board cannot be searched without limit
1589 int budget = 400;
1590
1591 auto hasTwin =
1592 [&]( const std::set<FOOTPRINT*>& aSet )
1593 {
1594 budget--;
1595
1596 return std::ranges::any_of( findMatches( aSet, false ),
1597 []( const RULE_AREA_COMPAT_DATA& aMatch )
1598 {
1599 return !aMatch.m_searchLimit;
1600 } );
1601 };
1602
1603 std::set<FOOTPRINT*> result = aSeed;
1604
1605 if( result.empty() || !hasTwin( result ) )
1606 return result;
1607
1608 // A sheet instance is the largest set a hierarchical design repeats
1609 const KIID_PATH& path = ( *aSeed.begin() )->GetPath();
1610
1611 if( aLargest && path.size() >= 2 )
1612 {
1613 std::set<FOOTPRINT*> sheet;
1614
1615 for( FOOTPRINT* fp : board()->Footprints() )
1616 {
1617 const KIID_PATH& other = fp->GetPath();
1618
1619 if( other.size() == path.size() && std::equal( path.begin(), path.end() - 1, other.begin() ) )
1620 sheet.insert( fp );
1621 }
1622
1623 bool holdsSeed = std::ranges::all_of( aSeed, [&]( FOOTPRINT* aFp ) { return sheet.contains( aFp ); } );
1624
1625 if( sheet.size() > result.size() && holdsSeed && hasTwin( sheet ) )
1626 {
1627 return sheet;
1628 }
1629 }
1630
1631 std::map<int, std::vector<FOOTPRINT*>> netOwners = buildNetOwners( board() );
1632
1633 // Rails and buses reach too many parts to tie a channel together
1634 constexpr size_t maxOwners = 12;
1635
1636 std::set<FOOTPRINT*> rejected;
1637 bool grew = true;
1638
1639 while( grew && budget > 0 )
1640 {
1641 grew = false;
1642
1643 // A step only reaches the parts next to what the pass started with
1644 const std::set<FOOTPRINT*> frontier = result;
1645
1646 for( const auto& [net, owners] : netOwners )
1647 {
1648 if( owners.size() > maxOwners || !std::ranges::any_of( owners, [&]( FOOTPRINT* aFp )
1649 { return frontier.contains( aFp ); } ) )
1650 {
1651 continue;
1652 }
1653
1654 for( FOOTPRINT* fp : owners )
1655 {
1656 if( budget <= 0 )
1657 break;
1658
1659 if( result.contains( fp ) || rejected.contains( fp ) )
1660 continue;
1661
1662 std::set<FOOTPRINT*> trial = result;
1663 trial.insert( fp );
1664
1665 if( hasTwin( trial ) )
1666 {
1667 result = std::move( trial );
1668 grew = true;
1669 }
1670 else
1671 {
1672 rejected.insert( fp );
1673 }
1674 }
1675 }
1676
1677 // A part refused alone may fit once its neighbours are in
1678 if( grew )
1679 rejected.clear();
1680
1681 if( !aLargest )
1682 break;
1683 }
1684
1685 return result;
1686}
1687
1688
1690{
1691 return expandSelection( false );
1692}
1693
1694
1696{
1697 return expandSelection( true );
1698}
1699
1700
1702{
1703 std::set<FOOTPRINT*> seed;
1704
1705 for( EDA_ITEM* item : selection() )
1706 {
1707 if( item->Type() == PCB_FOOTPRINT_T )
1708 seed.insert( static_cast<FOOTPRINT*>( item ) );
1709 else if( item->Type() == PCB_GROUP_T )
1710 collectGroupFootprints( static_cast<PCB_GROUP*>( item ), seed );
1711 }
1712
1713 if( seed.empty() )
1714 {
1715 frame()->ShowInfoBarMsg( _( "Select a footprint to expand from." ), true );
1716 return 0;
1717 }
1718
1719 std::set<FOOTPRINT*> expanded = ExpandToMatchingTopology( seed, aLargest );
1720
1721 if( expanded.size() == seed.size() )
1722 {
1723 frame()->ShowInfoBarMsg( _( "No larger topology repeats elsewhere on the board." ), true );
1724 return 0;
1725 }
1726
1727 EDA_ITEMS items( expanded.begin(), expanded.end() );
1728
1729 m_toolMgr->RunAction( ACTIONS::selectionClear );
1730 m_toolMgr->RunAction<EDA_ITEMS*>( ACTIONS::selectItems, &items );
1731
1732 frame()->ShowInfoBarMsg( wxString::Format( _( "Selected %d parts that repeat elsewhere." ), (int) expanded.size() ),
1733 true );
1734 return 0;
1735}
1736
1737
1739{
1740 RULE_AREA* ref = m_areas.m_refRA;
1741 std::set<FOOTPRINT*> pruned;
1742
1743 if( !ref )
1744 return true;
1745
1746 // Every channel keeps only the parts all ticked channels share
1747 for( const auto& [area, compat] : m_areas.m_compatMap )
1748 {
1749 if( compat.m_doCopy )
1750 pruned.insert( compat.m_unmatched.begin(), compat.m_unmatched.end() );
1751 }
1752
1753 ref->m_components.clear();
1754 ref->m_designBlockItems.clear();
1755
1756 for( BOARD_ITEM* item : m_areas.m_selectedItems )
1757 {
1758 if( item->Type() == PCB_FOOTPRINT_T && pruned.contains( static_cast<FOOTPRINT*>( item ) ) )
1759 continue;
1760
1761 ref->m_designBlockItems.insert( item );
1762
1763 if( item->Type() == PCB_FOOTPRINT_T )
1764 ref->m_components.insert( static_cast<FOOTPRINT*>( item ) );
1765 }
1766
1767 if( ref->m_components.empty() )
1768 {
1769 for( auto& [area, compat] : m_areas.m_compatMap )
1770 {
1771 compat.m_isOk = false;
1772 compat.m_errorMsg = _( "The ticked channels share no selected part" );
1773 }
1774
1775 return true;
1776 }
1777
1778 // The anchor must be shared by all channels; the biggest part is the one placed first
1779 if( !std::ranges::any_of( ref->m_components,
1780 [&]( FOOTPRINT* aFp ) { return aFp->m_Uuid == m_areas.m_options.m_anchor; } ) )
1781 {
1782 m_areas.m_options.m_anchor = niluuid;
1783 int64_t biggest = -1;
1784
1785 for( FOOTPRINT* fp : ref->m_components )
1786 {
1787 if( int64_t area = fp->GetBoundingBox( false ).GetArea(); area > biggest )
1788 {
1789 biggest = area;
1790 m_areas.m_options.m_anchor = fp->m_Uuid;
1791 }
1792 }
1793 }
1794
1795 std::vector<TOPOLOGY_JOB> jobs;
1796
1797 for( auto& [area, compat] : m_areas.m_compatMap )
1798 {
1799 area->m_components.clear();
1800
1801 for( FOOTPRINT* fp : ref->m_components )
1802 {
1803 if( auto it = compat.m_discovered.find( fp ); it != compat.m_discovered.end() )
1804 area->m_components.insert( it->second );
1805 }
1806
1807 if( compat.m_searchLimit )
1808 {
1809 compat.m_isOk = false;
1810 compat.m_errorMsg = _( "Search limit reached" );
1811 continue;
1812 }
1813
1814 jobs.push_back( { ref, area, &compat } );
1815 }
1816
1817 bool resolved = resolveTopologies( _( "Matching channels..." ), jobs );
1818
1819 for( const TOPOLOGY_JOB& job : jobs )
1820 {
1821 RULE_AREA* area = job.m_target;
1822 RULE_AREA_COMPAT_DATA& compat = *job.m_compat;
1823
1824 if( !compat.m_isOk )
1825 continue;
1826
1827 if( !compat.m_unmatched.empty() )
1828 compat.m_errorMsg = wxString::Format( _( "Loose: prunes %s" ), JoinReferences( compat.m_unmatched ) );
1829
1830 // Copying clears the target by its outline, so it has to be where the copy will land
1831 VECTOR2I disp;
1832 EDA_ANGLE rot;
1833
1834 area->m_zone->RemoveAllContours();
1835 area->m_zone->AddPolygon( repeatedOutline( ref, area, m_areas.m_options.m_anchor, compat.m_matchingComponents,
1836 disp, rot ) );
1837 }
1838
1839 return resolved;
1840}
1841
1842
1844{
1845 RULE_AREA* ref = m_areas.m_refRA;
1846 std::vector<RULE_AREA*> channels = { ref };
1847 BOARD_COMMIT commit( GetManager(), true, false );
1848
1849 for( auto& [area, compat] : m_areas.m_compatMap )
1850 {
1851 if( compat.m_doCopy && compat.m_isOk )
1852 channels.push_back( area );
1853 }
1854
1855 if( channels.size() < 2 )
1856 return 0;
1857
1858 auto addToBoard =
1859 [&]( BOARD_ITEM* aItem )
1860 {
1861 auto it = std::ranges::find_if( m_areas.m_scratchItems,
1862 [&]( const auto& aScratch ) { return aScratch.get() == aItem; } );
1863 commit.Add( it->release() );
1864 };
1865
1866 std::set<wxString> names;
1867
1868 for( PCB_GROUP* group : board()->Groups() )
1869 names.insert( group->GetName() );
1870
1871 for( RULE_AREA* area : channels )
1872 {
1873 // Each channel is named after its counterpart of the anchor
1874 RULE_AREA* matched = area == ref ? channels[1] : area;
1875 const auto& [refAnchor, targetAnchor] =
1876 anchorPair( m_areas.m_compatMap[matched].m_matchingComponents, m_areas.m_options.m_anchor );
1877 FOOTPRINT* anchor = area == ref ? refAnchor : targetAnchor;
1878
1879 wxString name = BOARD::MakeUniqueZoneName( wxString::Format( _( "%s channel" ), anchor->GetReference() ),
1880 [&]( const wxString& aName ) { return names.contains( aName ); } );
1881 names.insert( name );
1882
1883 area->m_group->SetName( name );
1884 area->m_zone->SetZoneName( name );
1885
1886 // A channel that is exactly one sheet instance keeps following the schematic
1887 wxString sheet = anchor->GetSheetname();
1888 bool wholeSheet = queryComponentsInSheet( sheet ) == area->m_components;
1889
1890 area->m_zone->SetPlacementAreaSourceType( wholeSheet ? PLACEMENT_SOURCE_T::SHEETNAME
1892 area->m_zone->SetPlacementAreaSource( wholeSheet ? sheet : area->m_group->GetName() );
1893
1894 addToBoard( area->m_zone );
1895 addToBoard( area->m_group );
1896 stageAddToGroup( commit, area->m_group, area->m_zone );
1897
1898 for( FOOTPRINT* fp : area->m_components )
1899 stageAddToGroup( commit, area->m_group, fp );
1900 }
1901
1902 for( EDA_ITEM* item : ref->m_designBlockItems )
1903 {
1904 if( item->Type() != PCB_FOOTPRINT_T )
1905 stageAddToGroup( commit, ref->m_group, static_cast<BOARD_ITEM*>( item ) );
1906 }
1907
1908 // What the area of the parts brings along is part of the channel, so linked updates repeat it too
1909 std::set<BOARD_ITEM*> found;
1910
1911 if( m_areas.m_options.m_copyRouting )
1912 {
1913 std::set<int> nets;
1914
1915 for( FOOTPRINT* fp : ref->m_components )
1916 {
1917 for( PAD* pad : fp->Pads() )
1918 nets.insert( pad->GetNetCode() );
1919 }
1920
1921 for( BOARD_CONNECTED_ITEM* item : findRepeatRouting( ref, nets, m_areas.m_options ) )
1922 found.insert( item );
1923 }
1924
1925 if( m_areas.m_options.m_copyOtherItems )
1926 findOtherItemsInRuleArea( ref, found );
1927
1928 for( BOARD_ITEM* item : found )
1929 {
1930 if( !item->GetParentGroup() && !ref->m_designBlockItems.contains( item ) )
1931 stageAddToGroup( commit, ref->m_group, item );
1932 }
1933
1934 if( m_areas.m_linkAreas )
1935 {
1936 std::vector<RULE_AREA*> targets;
1937
1938 for( RULE_AREA* area : channels )
1939 {
1940 if( area != ref )
1941 targets.push_back( area );
1942 }
1943
1944 linkAreas( commit, ref->m_zone, targets );
1945 }
1946
1947 applyRepeat( commit );
1948
1949 // Pushing delivers the model change, which ends the session and frees ref
1950 EDA_ITEM* refGroup = ref->m_group->AsEdaItem();
1951 int created = (int) channels.size();
1952
1953 commit.Push( _( "Repeat Layout" ) );
1954
1955 // Editing the old loose selection would leave the reference area behind
1956 m_toolMgr->RunAction( ACTIONS::selectionClear );
1957 m_toolMgr->RunAction( ACTIONS::selectItem, refGroup );
1958
1959 if( Pgm().IsGUI() )
1960 {
1961 frame()->ShowInfoBarMsg( wxString::Format( m_areas.m_linkAreas ? _( "Created %d linked channels." )
1962 : _( "Created %d channels." ),
1963 created ),
1964 true );
1965 }
1966
1967 return 0;
1968}
1969
1970
1971wxString MULTICHANNEL_TOOL::JoinReferences( const std::set<FOOTPRINT*>& aFootprints )
1972{
1973 wxArrayString refs;
1974
1975 for( FOOTPRINT* fp : aFootprints )
1976 refs.push_back( fp->GetReference() );
1977
1978 refs.Sort();
1979 return wxJoin( refs, ',' );
1980}
1981
1982
1984{
1985 if( m_areas.FromSelection() )
1987
1989}
1990
1991
1993{
1995
1996 if( m_areas.FromSelection() )
1997 return applySelectionRepeat();
1998
1999 return RepeatLayout( TOOL_EVENT(), m_areas.m_refRA->m_zone );
2000}
2001
2002
2004{
2006
2008
2009 if( !view || !m_areas.m_refRA )
2010 return;
2011
2012 PROF_TIMER timer;
2013
2014 // The copy is staged and reverted; only clones of its result stay on screen
2015 auto savedCompat = m_areas.m_compatMap;
2016 REPEAT_LAYOUT_OPTIONS savedOptions = m_areas.m_options;
2017 BOARD_COMMIT commit( GetManager(), true, false );
2018
2019 // Grouping changes nothing visible and would stage edits to unrelated groups
2020 m_areas.m_options.m_groupItems = false;
2021 applyRepeat( commit );
2022
2023 auto previewClone =
2024 [&]( BOARD_ITEM* aItem ) -> BOARD_ITEM*
2025 {
2026 BOARD_ITEM* clone = static_cast<BOARD_ITEM*>( aItem->Clone() );
2027
2028 // The revert below deletes groups the copy staged, and drawing asks the parent group for its lock
2029 clone->SetParentGroup( nullptr );
2030 view->AddToPreview( clone, true );
2031 return clone;
2032 };
2033
2034 auto isContainer =
2035 []( const BOARD_ITEM* aItem )
2036 {
2037 return aItem->Type() == PCB_GROUP_T || aItem->Type() == PCB_GENERATOR_T;
2038 };
2039
2040 auto show =
2041 [&]( BOARD_ITEM* aItem )
2042 {
2043 // Group and generator clones share members, so those are shown one member at a time
2044 if( isContainer( aItem ) )
2045 {
2046 aItem->RunOnChildren( [&]( BOARD_ITEM* aChild )
2047 {
2048 if( !isContainer( aChild ) )
2049 previewClone( aChild );
2050 },
2052 return;
2053 }
2054
2055 BOARD_ITEM* clone = previewClone( aItem );
2056 clone->SetBrightened();
2057 clone->RunOnChildren( [&]( BOARD_ITEM* aChild ) { view->AddToPreview( aChild, false ); },
2059 };
2060
2061 for( const auto& [area, compat] : m_areas.m_compatMap )
2062 {
2063 for( BOARD_ITEM* item : compat.m_affectedItems )
2064 m_previewHidden.push_back( item->m_Uuid );
2065
2066 for( BOARD_ITEM* item : compat.m_groupableItems )
2067 show( item );
2068 }
2069
2070 // Marks and ratsnest follow the parts to where the copy puts them
2071 for( const auto& [area, compat] : m_areas.m_compatMap )
2072 {
2073 for( BOARD_ITEM* item : compat.m_affectedItems )
2074 {
2075 if( item->Type() == PCB_FOOTPRINT_T )
2076 {
2077 FOOTPRINT* fp = static_cast<FOOTPRINT*>( item );
2078
2079 m_previewBoxes[fp] = fp->GetBoundingBox( false );
2080 }
2081 }
2082 }
2083
2084 if( !m_previewBoxes.empty() )
2085 {
2086 std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
2087 std::vector<BOARD_ITEM*> moved;
2088
2089 for( const auto& [fp, box] : m_previewBoxes )
2090 moved.push_back( fp );
2091
2092 for( BOARD_ITEM* fp : moved )
2093 connectivity->Update( fp );
2094
2095 connectivity->RecalculateRatsnest();
2096
2097 auto movedPad =
2098 [&]( const std::shared_ptr<const CN_ANCHOR>& aNode )
2099 {
2100 BOARD_CONNECTED_ITEM* parent = aNode->Parent();
2101
2102 return parent && parent->Type() == PCB_PAD_T
2103 && m_previewBoxes.contains( parent->GetParentFootprint() );
2104 };
2105
2106 for( const CN_EDGE& edge : connectivity->GetRatsnestForItems( moved ) )
2107 {
2108 const std::shared_ptr<const CN_ANCHOR>& a = edge.GetSourceNode();
2109 const std::shared_ptr<const CN_ANCHOR>& b = edge.GetTargetNode();
2110
2111 if( a && b && !a->Dirty() && !b->Dirty() && ( movedPad( a ) || movedPad( b ) ) )
2112 m_previewRatsnest.emplace_back( a->Pos(), b->Pos() );
2113 }
2114 }
2115
2116 // Revert swaps in fresh footprint children, so the originals are hidden only once it is done
2117 commit.Revert();
2118 setPreviewHidden( true );
2120 m_areas.m_compatMap = std::move( savedCompat );
2121 m_areas.m_options = savedOptions;
2122 canvas()->Refresh();
2124 wxLogTrace( traceMultichannelTool, wxT( "Preview: %s" ), timer.to_string() );
2125}
2126
2127
2129{
2130 if( m_previewBoxes.empty() )
2131 return;
2132
2133 // Lines at the old positions would otherwise sit beside the previewed ones
2134 std::vector<BOARD_ITEM*> moved;
2135
2136 for( const auto& [fp, box] : m_previewBoxes )
2137 moved.push_back( fp );
2138
2141}
2142
2143
2144std::set<BOARD_CONNECTED_ITEM*> MULTICHANNEL_TOOL::findRepeatRouting( RULE_AREA* aArea, const std::set<int>& aNets,
2145 const REPEAT_LAYOUT_OPTIONS& aOpts )
2146{
2147 std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
2148 std::set<BOARD_CONNECTED_ITEM*> scanned;
2149 std::set<BOARD_CONNECTED_ITEM*> routing;
2150
2151 findRoutingInRuleArea( aArea, scanned );
2152
2153 for( BOARD_CONNECTED_ITEM* item : scanned )
2154 {
2155 if( aNets.contains( item->GetNetCode() ) )
2156 routing.insert( item );
2157 }
2158
2159 // A linked channel's group already holds everything it carries
2160 if( aOpts.m_connectedRoutingOnly || linkedChannelGroup( board(), aArea ) )
2161 return routing;
2162
2163 const std::set<BOARD_CONNECTED_ITEM*> inside = routing;
2164
2165 for( BOARD_CONNECTED_ITEM* item : inside )
2166 {
2167 if( item->Type() == PCB_PAD_T )
2168 continue;
2169
2170 for( CN_ITEM* cnItem : connectivity->GetConnectivityAlgo()->ItemEntry( item ).GetItems() )
2171 {
2172 for( CN_ITEM* touching : cnItem->ConnectedItems() )
2173 {
2174 BOARD_CONNECTED_ITEM* next = touching->Valid() ? touching->Parent() : nullptr;
2175
2176 if( !next || ( next->Type() != PCB_TRACE_T && next->Type() != PCB_ARC_T && next->Type() != PCB_VIA_T ) )
2177 continue;
2178
2179 // Tracks of a meander travel with their generator
2180 EDA_GROUP* parent = next->GetParentGroup();
2181
2182 if( parent && parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
2183 continue;
2184
2185 if( next->GetNetCode() == item->GetNetCode() && aNets.contains( next->GetNetCode() ) )
2186 routing.insert( next );
2187 }
2188 }
2189 }
2190
2191 return routing;
2192}
2193
2194
2196{
2198
2199 if( !view )
2200 return;
2201
2202 m_focusArea = aArea;
2203
2204 if( !m_highlight )
2205 m_highlight = view->MakeOverlay();
2206
2207 m_highlight->Clear();
2208
2209 auto mark =
2210 [&]( const RULE_AREA* aMarked, const KIGFX::COLOR4D& aColor )
2211 {
2212 m_highlight->SetIsStroke( true );
2213 m_highlight->SetIsFill( true );
2214 m_highlight->SetStrokeColor( aColor.WithAlpha( 0.9 ) );
2215 m_highlight->SetFillColor( aColor.WithAlpha( 0.08 ) );
2216 m_highlight->SetLineWidth( pcbIUScale.mmToIU( 0.2 ) );
2217
2218 for( FOOTPRINT* fp : aMarked->m_components )
2219 {
2220 auto moved = m_previewBoxes.find( fp );
2221 BOX2I box = moved != m_previewBoxes.end() ? moved->second : fp->GetBoundingBox( false );
2222
2223 m_highlight->Rectangle( box.GetOrigin(), box.GetEnd() );
2224 }
2225 };
2226
2227 if( m_areas.m_refRA )
2228 mark( m_areas.m_refRA, KIGFX::COLOR4D( 0.1, 0.8, 0.3, 1.0 ) );
2229
2230 if( aArea )
2231 mark( aArea, KIGFX::COLOR4D( 1.0, 0.6, 0.1, 1.0 ) );
2232
2233 m_highlight->SetIsFill( false );
2234 m_highlight->SetStrokeColor( KIGFX::COLOR4D( 0.6, 0.8, 1.0, 0.8 ) );
2235 m_highlight->SetLineWidth( pcbIUScale.mmToIU( 0.05 ) );
2236
2237 for( const SEG& line : m_previewRatsnest )
2238 m_highlight->Line( line.A, line.B );
2239
2240 view->Update( m_highlight.get() );
2241 canvas()->Refresh();
2242}
2243
2244
2246{
2248
2249 if( !view )
2250 return;
2251
2252 view->ClearPreview();
2253 setPreviewHidden( false );
2254 m_previewHidden.clear();
2255
2256 if( !m_previewBoxes.empty() )
2257 {
2260 }
2261
2262 m_previewBoxes.clear();
2263 m_previewRatsnest.clear();
2264
2265 if( m_highlight )
2266 {
2267 view->Remove( m_highlight.get() );
2268 m_highlight.reset();
2269 }
2270
2271 canvas()->Refresh();
2272}
2273
2274
2276{
2278
2279 // Hidden items may have been deleted since, so they are found again by UUID
2280 for( const KIID& id : m_previewHidden )
2281 {
2282 BOARD_ITEM* item = board()->ResolveItem( id, true );
2283
2284 if( !item || !view->HasItem( item ) )
2285 continue;
2286
2287 view->Hide( item, aHide );
2288 item->RunOnChildren( [&]( BOARD_ITEM* aChild ) { view->Hide( aChild, aHide ); }, RECURSE_MODE::NO_RECURSE );
2289 }
2290}
2291
2292
2294{
2295 if( m_dialog )
2296 m_dialog->Close( true );
2297}
2298
2299
2301{
2303 m_focusArea = nullptr;
2304
2305 m_dialog->Unbind( wxEVT_CLOSE_WINDOW, &MULTICHANNEL_TOOL::onRepeatDialogClosed, this );
2306 m_dialog->Destroy();
2307 m_dialog = nullptr;
2308
2310}
2311
2312
2314{
2315 m_areas.m_refRA = nullptr;
2316
2317 for( RULE_AREA& ra : m_areas.m_areas )
2318 {
2319 if( ra.m_zone == aRefZone )
2320 {
2321 m_areas.m_refRA = &ra;
2322 break;
2323 }
2324 }
2325
2326 if( !m_areas.m_refRA )
2327 return -1;
2328
2329 m_areas.m_compatMap.clear();
2330
2331 std::vector<TOPOLOGY_JOB> jobs;
2332
2333 for( RULE_AREA& ra : m_areas.m_areas )
2334 {
2335 if( ra.m_zone == m_areas.m_refRA->m_zone )
2336 continue;
2337
2338 jobs.push_back( { m_areas.m_refRA, &ra, &m_areas.m_compatMap[&ra] } );
2339 }
2340
2341 if( !resolveTopologies( _( "Checking Rule Area compatibility..." ), jobs ) )
2342 {
2343 m_areas.m_compatMap.clear();
2344 return -1;
2345 }
2346
2347 return 0;
2348}
2349
2350
2351bool MULTICHANNEL_TOOL::resolveTopologies( const wxString& aTitle, const std::vector<TOPOLOGY_JOB>& aJobs )
2352{
2353 if( aJobs.empty() )
2354 return true;
2355
2356 int total = static_cast<int>( aJobs.size() );
2357 std::atomic<int> completed( 0 );
2358 std::atomic<bool> cancelled( false );
2359 std::atomic<int> matchedComponents( 0 );
2360 std::atomic<int> totalComponents( 0 );
2361
2363 isoParams.m_cancelled = &cancelled;
2364 isoParams.m_matchedComponents = &matchedComponents;
2365 isoParams.m_totalComponents = &totalComponents;
2366
2367 // Process RA resolutions sequentially on a single background thread.
2368 // Each resolveConnectionTopology call internally parallelizes its MRV scan
2369 // across the thread pool, creating many short-lived tasks that fully utilize
2370 // all available cores. Running the outer loop sequentially avoids thread
2371 // pool starvation from nested parallelism.
2372 // The loop waits on those tasks, so it runs outside the pool lest a one-thread pool deadlock
2373 auto future = std::async(
2374 std::launch::async,
2375 [this, &aJobs, &completed, &cancelled, &matchedComponents, &isoParams]()
2376 {
2377 for( const TOPOLOGY_JOB& job : aJobs )
2378 {
2379 if( cancelled.load( std::memory_order_relaxed ) )
2380 break;
2381
2382 matchedComponents.store( 0, std::memory_order_relaxed );
2383 resolveConnectionTopology( job.m_ref, job.m_target, *job.m_compat, isoParams );
2384
2385 if( cancelled.load( std::memory_order_relaxed ) )
2386 break;
2387
2388 completed.fetch_add( 1, std::memory_order_relaxed );
2389 }
2390 } );
2391
2392 if( Pgm().IsGUI() )
2393 {
2394 // Declared first so the windows are enabled again only after the progress dialog is gone, the
2395 // Repeat dialog last so it is raised over the board
2396 std::unique_ptr<WINDOW_DISABLER> dialogDisabler;
2397 std::unique_ptr<WINDOW_DISABLER> frameDisabler;
2398 std::unique_ptr<WX_PROGRESS_REPORTER> reporter;
2399 auto startTime = std::chrono::steady_clock::now();
2400 double highWaterMark = 0.0;
2401
2402 while( future.wait_for( std::chrono::milliseconds( 100 ) ) != std::future_status::ready )
2403 {
2404 if( !reporter )
2405 {
2406 // Short enough to feel immediate, long enough not to flash for a small board
2407 if( std::chrono::steady_clock::now() - startTime > std::chrono::milliseconds( 250 ) )
2408 {
2409 // The progress dialog pumps events from its first Show, and the board and a non-modal
2410 // Repeat dialog must not change while the worker reads them
2411 dialogDisabler = std::make_unique<WINDOW_DISABLER>( m_dialog );
2412 frameDisabler = std::make_unique<WINDOW_DISABLER>( frame() );
2413 reporter = std::make_unique<WX_PROGRESS_REPORTER>( frame(), aTitle, 1, PR_CAN_ABORT );
2414 }
2415 else
2416 {
2417 // Flush background-thread log messages so timing traces appear promptly
2418 wxLog::FlushActive();
2419 }
2420 }
2421
2422 if( reporter )
2423 {
2424 int done = completed.load( std::memory_order_relaxed );
2425 int matched = matchedComponents.load( std::memory_order_relaxed );
2426 int compTotal = totalComponents.load( std::memory_order_relaxed );
2427
2428 double fraction = ( compTotal > 0 )
2429 ? static_cast<double>( matched ) / compTotal
2430 : 0.0;
2431 double progress = static_cast<double>( done + fraction ) / total;
2432
2433 if( progress > highWaterMark )
2434 highWaterMark = progress;
2435
2436 reporter->SetCurrentProgress( highWaterMark );
2437 reporter->Report( wxString::Format(
2438 _( "Resolving topology %d of %d (%d/%d components)" ),
2439 std::min( done + 1, total ), total, matched, compTotal ) );
2440
2441 if( !reporter->KeepRefreshing() )
2442 cancelled.store( true, std::memory_order_relaxed );
2443 }
2444 }
2445 }
2446 else
2447 {
2448 future.wait();
2449 }
2450
2451 if( !cancelled.load( std::memory_order_relaxed ) )
2452 return true;
2453
2454 // A job stopped part way says no more about its topology than one never started
2455 for( size_t i = static_cast<size_t>( completed.load() ); i < aJobs.size(); ++i )
2456 {
2457 aJobs[i].m_compat->m_isOk = false;
2458 aJobs[i].m_compat->m_errorMsg = _( "Cancelled" );
2459 aJobs[i].m_compat->m_mismatchReasons.clear();
2460 }
2461
2462 return false;
2463}
2464
2465
2466int MULTICHANNEL_TOOL::RepeatLayout( const TOOL_EVENT& aEvent, RULE_AREA& aRefArea, RULE_AREA& aTargetArea,
2467 REPEAT_LAYOUT_OPTIONS& aOptions, BOARD_COMMIT* aExternalCommit,
2468 wxString* aErrorOut )
2469{
2470 wxCHECK_MSG( aRefArea.m_zone, -1, wxT( "Reference Rule Area has no zone." ) );
2471 wxCHECK_MSG( aTargetArea.m_zone, -1, wxT( "Target Rule Area has no zone." ) );
2472
2473 const bool silent = aErrorOut != nullptr;
2474
2475 auto reportError = [&]( const wxString& aMsg )
2476 {
2477 if( aErrorOut )
2478 *aErrorOut = aMsg;
2479 else if( Pgm().IsGUI() )
2480 frame()->ShowInfoBarError( aMsg, true );
2481 };
2482
2483 RULE_AREA_COMPAT_DATA compat;
2484
2485 if( !resolveConnectionTopology( &aRefArea, &aTargetArea, compat ) )
2486 {
2487 if( silent )
2488 {
2489 *aErrorOut = compat.m_mismatchReasons.empty() ? compat.m_errorMsg
2491 }
2492 else if( Pgm().IsGUI() )
2493 {
2494 wxString summary = wxString::Format( _( "Rule Area topologies do not match: %s" ), compat.m_errorMsg );
2496 }
2497
2498 return -1;
2499 }
2500
2501 std::optional<BOARD_COMMIT> localCommit;
2502
2503 if( !aExternalCommit )
2504 localCommit.emplace( GetManager(), true, false );
2505
2506 BOARD_COMMIT& commit = aExternalCommit ? *aExternalCommit : *localCommit;
2507
2508 // If no anchor is provided, pick the first matched pair to avoid center-alignment shifting
2509 // the whole group. This keeps Apply Design Block Layout from moving the group to wherever
2510 // the source design block happened to be placed.
2512 && aTargetArea.m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT && aOptions.m_anchor == niluuid )
2513 {
2514 if( !compat.m_matchingComponents.empty() )
2515 aOptions.m_anchor = compat.m_matchingComponents.begin()->first->m_Uuid;
2516 }
2517
2518 copyRuleAreaContents( &aRefArea, &aTargetArea, &commit, aOptions, compat );
2519
2521 {
2522 EDA_GROUP* group = aTargetArea.m_group;
2523
2524 if( !group && !aTargetArea.m_components.empty() )
2525 group = ( *aTargetArea.m_components.begin() )->GetParentGroup();
2526
2527 if( !group )
2528 {
2529 if( !aExternalCommit )
2530 commit.Revert();
2531
2532 reportError( _( "Target group does not have a group." ) );
2533
2534 return -1;
2535 }
2536
2537 commit.Modify( group->AsEdaItem(), nullptr, RECURSE_MODE::NO_RECURSE );
2538
2539 for( BOARD_ITEM* item : compat.m_groupableItems )
2540 {
2541 // Items already in a nested group stay there so the nesting survives
2542 if( group->ContainsItem( item ) )
2543 continue;
2544
2545 commit.Modify( item );
2546 group->AddItem( item );
2547 }
2548 }
2549
2550 if( !aExternalCommit )
2551 commit.Push( _( "Repeat Layout" ) );
2552
2553 return 0;
2554}
2555
2556
2558{
2559 int totalCopied = 0;
2560
2561 for( auto& [targetArea, compatData] : m_areas.m_compatMap )
2562 {
2563 if( !compatData.m_doCopy )
2564 {
2565 wxLogTrace( traceMultichannelTool, wxT( "skipping copy to RA '%s' (disabled in dialog)\n" ),
2566 targetArea->m_ruleName );
2567 continue;
2568 }
2569
2570 if( !compatData.m_isOk )
2571 continue;
2572
2573 copyRuleAreaContents( m_areas.m_refRA, targetArea, &aCommit, m_areas.m_options, compatData );
2574 totalCopied++;
2575 }
2576
2577 if( m_areas.m_options.m_groupItems )
2578 {
2579 for( const auto& [targetArea, compatData] : m_areas.m_compatMap )
2580 {
2581 if( compatData.m_groupableItems.size() < 2 )
2582 continue;
2583
2584 // A group-driven area already has a group.
2585 EDA_GROUP* group =
2586 targetArea->m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT ? targetArea->m_group : nullptr;
2587
2588 if( group )
2589 {
2590 aCommit.Modify( group->AsEdaItem(), nullptr, RECURSE_MODE::NO_RECURSE );
2591 }
2592 else
2593 {
2594 pruneExistingGroups( aCommit, compatData.m_affectedItems );
2595
2596 group = new PCB_GROUP( board() );
2597 aCommit.Add( group->AsEdaItem() );
2598 }
2599
2600 for( BOARD_ITEM* item : compatData.m_groupableItems )
2601 stageAddToGroup( aCommit, group, item );
2602 }
2603 }
2604
2605 return totalCopied;
2606}
2607
2608
2609int MULTICHANNEL_TOOL::RepeatLayout( const TOOL_EVENT& aEvent, ZONE* aRefZone )
2610{
2611 BOARD_COMMIT commit( GetManager(), true, false );
2612 int totalCopied = applyRepeat( commit );
2613
2614 if( m_areas.m_linkAreas )
2615 {
2616 std::vector<RULE_AREA*> targets;
2617
2618 for( const auto& [targetArea, compatData] : m_areas.m_compatMap )
2619 {
2620 if( compatData.m_doCopy && compatData.m_isOk )
2621 targets.push_back( targetArea );
2622 }
2623
2624 linkAreas( commit, aRefZone, targets );
2625 }
2626
2627 commit.Push( _( "Repeat Layout" ) );
2628
2629 if( Pgm().IsGUI() )
2630 frame()->ShowInfoBarMsg( wxString::Format( _( "Copied to %d Rule Areas." ), totalCopied ), true );
2631
2632 return 0;
2633}
2634
2635
2637{
2638 if( aZone->GetBoard() )
2639 aCommit.Modify( aZone );
2640
2641 aZone->SetLinkSet( niluuid );
2643}
2644
2645
2646void MULTICHANNEL_TOOL::linkAreas( BOARD_COMMIT& aCommit, ZONE* aRefZone, const std::vector<RULE_AREA*>& aTargets )
2647{
2648 const REPEAT_LAYOUT_OPTIONS& opts = m_areas.m_options;
2649 const KIID linkSet;
2650 std::set<KIID> oldSets;
2651
2652 // New zones are not on the board yet, so the commit has nothing to snapshot
2653 auto join = [&]( ZONE* aZone, const KIID& aAnchor )
2654 {
2655 if( aZone->GetBoard() )
2656 aCommit.Modify( aZone );
2657
2658 REPEAT_LAYOUT_OPTIONS own = opts;
2659 own.m_anchor = aAnchor;
2660
2661 if( aZone->GetLinkSet() != niluuid )
2662 oldSets.insert( aZone->GetLinkSet() );
2663
2664 aZone->SetLinkSet( linkSet );
2665 aZone->SetRepeatOptions( own );
2666 };
2667
2668 join( aRefZone, opts.m_anchor );
2669
2670 for( RULE_AREA* target : aTargets )
2671 {
2672 const TMATCH::COMPONENT_MATCHES& matches = m_areas.m_compatMap[target].m_matchingComponents;
2674
2675 // Each area keeps its own counterpart of the anchor, so any of them can be the source later
2676 if( opts.m_anchor != niluuid && !matches.empty() )
2677 anchor = anchorPair( matches, opts.m_anchor ).second->m_Uuid;
2678
2679 join( target->m_zone, anchor );
2680 }
2681
2682 // Peers that were left out stay linked to each other unless they are alone
2683 dropLoneLinks( aCommit, oldSets );
2684}
2685
2686
2687void MULTICHANNEL_TOOL::dropLoneLinks( BOARD_COMMIT& aCommit, const std::set<KIID>& aSets )
2688{
2689 std::map<KIID, std::vector<ZONE*>> members;
2690
2691 for( ZONE* zone : board()->Zones() )
2692 {
2693 if( aSets.contains( zone->GetLinkSet() ) )
2694 members[zone->GetLinkSet()].push_back( zone );
2695 }
2696
2697 for( const auto& [linkSet, zones] : members )
2698 {
2699 if( zones.size() < 2 )
2700 detachArea( aCommit, zones.front() );
2701 }
2702}
2703
2704
2706{
2707 // A router keeps its own copy of the board while routing; editing under it leaves it stale
2708 ROUTER_TOOL* router = m_toolMgr->GetTool<ROUTER_TOOL>();
2709 GENERATOR_TOOL* generators = m_toolMgr->GetTool<GENERATOR_TOOL>();
2710
2711 return ( router && router->RoutingInProgress() )
2712 || ( generators && generators->Router() && generators->Router()->RoutingInProgress() );
2713}
2714
2715
2717{
2718 std::set<ZONE*> sources = linkedAreasIn( selection() );
2719
2720 if( sources.size() != 1 )
2721 {
2722 frame()->ShowInfoBarMsg( sources.empty() ? _( "Select a linked Rule Area, or a part of one, to update the "
2723 "others from." )
2724 : _( "Select items from a single linked channel." ),
2725 true );
2726 return 0;
2727 }
2728
2729 if( routerBusy() )
2730 {
2731 frame()->ShowInfoBarWarning( _( "Finish routing before updating linked layouts." ), true );
2732 return 0;
2733 }
2734
2735 UpdateLinkedLayouts( *sources.begin() );
2736 return 0;
2737}
2738
2739
2741{
2742 std::set<ZONE*> members = linkedAreasIn( selection() );
2743
2744 if( members.empty() )
2745 frame()->ShowInfoBarMsg( _( "Select a linked Rule Area, or a part of one, to unlink." ), true );
2746 else
2747 UnlinkAreas( members );
2748
2749 return 0;
2750}
2751
2752
2753void MULTICHANNEL_TOOL::UnlinkAreas( const std::set<ZONE*>& aZones )
2754{
2755 BOARD_COMMIT commit( GetManager(), true, false );
2756 std::set<KIID> oldSets;
2757
2758 for( ZONE* zone : aZones )
2759 {
2760 if( zone->GetLinkSet() != niluuid )
2761 oldSets.insert( zone->GetLinkSet() );
2762
2763 detachArea( commit, zone );
2764 }
2765
2766 dropLoneLinks( commit, oldSets );
2767 commit.Push( _( "Unlink Layout" ) );
2768}
2769
2770
2772{
2773 // The dialog's session lives in the same data this rebuilds
2775
2777
2778 RULE_AREA* ref = nullptr;
2779 std::vector<RULE_AREA*> linked;
2780 std::map<FOOTPRINT*, int> footprintUse;
2781
2782 for( RULE_AREA& area : m_areas.m_areas )
2783 {
2784 if( area.m_zone->GetLinkSet() == niluuid )
2785 continue;
2786
2787 linked.push_back( &area );
2788
2789 if( area.m_zone == aSource )
2790 ref = &area;
2791
2792 for( FOOTPRINT* fp : area.m_components )
2793 footprintUse[fp]++;
2794 }
2795
2796 if( !ref )
2797 return false;
2798
2799 // A target sharing parts with another linked area would overwrite that area's layout
2800 auto sharesParts =
2801 [&]( RULE_AREA* aArea )
2802 {
2803 return std::ranges::any_of( aArea->m_components,
2804 [&]( FOOTPRINT* aFp ) { return footprintUse[aFp] > 1; } );
2805 };
2806
2807 // Copper is cleared and copied by area, unless both ends hold exactly their own group's items
2808 auto overlapsOtherArea =
2809 [&]( RULE_AREA* aArea, const SHAPE_POLY_SET& aOutline )
2810 {
2811 return std::ranges::any_of( linked,
2812 [&]( RULE_AREA* aOther )
2813 {
2814 return aOther != aArea
2815 && outlinesOverlap( aOutline, *aOther->m_zone->Outline() );
2816 } );
2817 };
2818
2819 const bool refGrouped = linkedChannelGroup( board(), ref ) != nullptr;
2820
2821 // A source lying over another linked area would copy that area's contents too
2822 if( sharesParts( ref ) || ( !refGrouped && overlapsOtherArea( ref, *ref->m_zone->Outline() ) ) )
2823 {
2824 if( Pgm().IsGUI() )
2825 {
2826 frame()->ShowInfoBarWarning(
2827 wxString::Format( _( "'%s' overlaps another linked Rule Area." ), ref->m_ruleName ), true );
2828 }
2829
2830 return false;
2831 }
2832
2833 BOARD_COMMIT commit( GetManager(), true, false );
2834 std::vector<wxString> problems;
2835 std::vector<SHAPE_POLY_SET> acceptedOutlines;
2836
2837 m_areas.m_refRA = ref;
2838 m_areas.m_options = aSource->GetRepeatOptions();
2839 m_areas.m_compatMap.clear();
2840
2841 std::vector<RULE_AREA*> targets;
2842 std::vector<TOPOLOGY_JOB> jobs;
2843
2844 for( RULE_AREA* target : linked )
2845 {
2846 if( target == ref || target->m_zone->GetLinkSet() != aSource->GetLinkSet() )
2847 continue;
2848
2849 targets.push_back( target );
2850 jobs.push_back( { ref, target, &m_areas.m_compatMap[target] } );
2851 }
2852
2853 if( !resolveTopologies( _( "Updating linked layouts..." ), jobs ) )
2854 {
2855 m_areas.m_compatMap.clear();
2856 return false;
2857 }
2858
2859 for( RULE_AREA* target : targets )
2860 {
2861 RULE_AREA_COMPAT_DATA& compat = m_areas.m_compatMap[target];
2862
2863 if( !compat.m_isOk )
2864 {
2865 problems.push_back( wxString::Format( wxT( "'%s': %s" ), target->m_ruleName, compat.m_errorMsg ) );
2866 continue;
2867 }
2868
2869 // Unless both ends are grouped, copying clears the target by area, so both its outline now and the
2870 // one the copy gives it must stay clear of every other linked area
2871 VECTOR2I disp;
2872 EDA_ANGLE rot;
2873 SHAPE_POLY_SET repeated( repeatedOutline( ref, target, m_areas.m_options.m_anchor,
2874 compat.m_matchingComponents, disp, rot ) );
2875
2876 bool overlapsAccepted = std::ranges::any_of( acceptedOutlines,
2877 [&]( const SHAPE_POLY_SET& aOther )
2878 {
2879 return outlinesOverlap( repeated, aOther );
2880 } );
2881
2882 bool scoped = refGrouped && linkedChannelGroup( board(), target ) != nullptr;
2883
2884 if( sharesParts( target )
2885 || ( !scoped && ( overlapsAccepted || overlapsOtherArea( target, *target->m_zone->Outline() )
2886 || overlapsOtherArea( target, repeated ) ) ) )
2887 {
2888 problems.push_back(
2889 wxString::Format( _( "'%s' overlaps another linked Rule Area." ), target->m_ruleName ) );
2890 continue;
2891 }
2892
2893 compat.m_doCopy = true;
2894 acceptedOutlines.push_back( std::move( repeated ) );
2895 }
2896
2897 int copied = applyRepeat( commit );
2898
2899 if( !problems.empty() && Pgm().IsGUI() )
2900 {
2901 frame()->ShowInfoBarWarning(
2902 wxString::Format( _( "Some linked Rule Areas were not updated. %s" ), JoinMismatchReasons( problems ) ),
2903 true );
2904 }
2905
2906 if( !copied )
2907 return false;
2908
2909 commit.Push( _( "Update Linked Layouts" ), SKIP_ENTERED_GROUP );
2910 return true;
2911}
2912
2913
2914int MULTICHANNEL_TOOL::findRoutingInRuleArea( RULE_AREA* aRuleArea, std::set<BOARD_CONNECTED_ITEM*>& aOutput ) const
2915{
2916 if( !aRuleArea || !aRuleArea->m_zone )
2917 return 0;
2918
2919 // The user also will consider tracks and vias that are inside the source area but
2920 // not connected to any of the source pads to count as "routing" (e.g. stitching vias)
2921
2922 int count = 0;
2923
2924 // When we're copying the layout of a design block, we are provided an exact list of items
2925 // rather than querying the board for items that are inside the area.
2927 {
2928 // Get all board connected items that are from the design block, except pads,
2929 // which shouldn't be copied
2930 for( EDA_ITEM* item : aRuleArea->m_designBlockItems )
2931 {
2932 // Include any connected items except pads.
2933 if( item->Type() == PCB_PAD_T )
2934 continue;
2935
2936 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
2937 {
2938 // Zones are handled by the "copy other items" path, we need this check here
2939 // because design blocks explicitly include them as part of the block contents,
2940 // but other RA types grab them by querying the board for items enclosed by the RA polygon
2941 if( bci->Type() == PCB_ZONE_T )
2942 continue;
2943
2944 // Tracks inside a generator (meander) are copied with the generator.
2945 if( EDA_GROUP* parent = bci->GetParentGroup() )
2946 {
2947 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
2948 continue;
2949 }
2950
2951 if( bci->IsConnected() )
2952 aOutput.insert( bci );
2953 }
2954 }
2955
2956 if( !aRuleArea->m_fromSelection )
2957 return (int) aOutput.size();
2958
2959 count = (int) aOutput.size();
2960 }
2961
2962 if( EDA_GROUP* group = linkedChannelGroup( board(), aRuleArea ) )
2963 {
2964 std::set<BOARD_ITEM*> members;
2965 collectGroupBoardItems( group, members );
2966
2967 for( BOARD_ITEM* member : members )
2968 {
2969 BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( member );
2970
2971 if( !bci || bci->Type() == PCB_PAD_T || bci->Type() == PCB_ZONE_T || !bci->IsConnected() )
2972 continue;
2973
2974 if( EDA_GROUP* parent = bci->GetParentGroup() )
2975 {
2976 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
2977 continue;
2978 }
2979
2980 aOutput.insert( bci );
2981 count++;
2982 }
2983
2984 return count;
2985 }
2986
2987 // The design-block apply target uses a scratch zone not on the board, so enclosedByArea()
2988 // below finds nothing. Match routing against the zone outline directly.
2989 if( aRuleArea->m_sourceType == PLACEMENT_SOURCE_T::GROUP_PLACEMENT || aRuleArea->m_fromSelection )
2990 {
2991 bool zoneOnBoard = false;
2992
2993 for( ZONE* zone : board()->Zones() )
2994 {
2995 if( zone == aRuleArea->m_zone )
2996 {
2997 zoneOnBoard = true;
2998 break;
2999 }
3000 }
3001
3002 if( !zoneOnBoard )
3003 {
3004 const SHAPE_POLY_SET& areaOutline = *aRuleArea->m_zone->Outline();
3005 const BOX2I areaBox = areaOutline.BBox();
3006 int maxError = board()->GetDesignSettings().m_MaxError;
3007
3008 auto enclosedByZone = [&]( BOARD_CONNECTED_ITEM* aItem )
3009 {
3010 if( aOutput.contains( aItem ) )
3011 return;
3012
3013 // Tracks inside a generator (meander) are removed with the generator.
3014 if( EDA_GROUP* parent = aItem->GetParentGroup() )
3015 {
3016 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
3017 return;
3018 }
3019
3020 if( !( aRuleArea->m_zone->GetLayerSet() & aItem->GetLayerSet() ).any()
3021 || !areaBox.Contains( aItem->GetBoundingBox() ) )
3022 {
3023 return;
3024 }
3025
3026 SHAPE_POLY_SET itemShape;
3027 aItem->TransformShapeToPolygon( itemShape, aItem->GetLayer(), 0, maxError, ERROR_OUTSIDE );
3028
3029 if( itemShape.IsEmpty() )
3030 return;
3031
3032 itemShape.BooleanSubtract( areaOutline );
3033
3034 if( itemShape.IsEmpty() )
3035 {
3036 aOutput.insert( aItem );
3037 count++;
3038 }
3039 };
3040
3041 for( PCB_TRACK* track : board()->Tracks() )
3042 enclosedByZone( track );
3043
3044 for( BOARD_ITEM* drawing : board()->Drawings() )
3045 {
3046 if( drawing->IsConnected() )
3047 enclosedByZone( static_cast<BOARD_CONNECTED_ITEM*>( drawing ) );
3048 }
3049
3050 return count;
3051 }
3052 }
3053
3055 PCBEXPR_UCODE ucode;
3056 PCBEXPR_CONTEXT ctx, preflightCtx;
3057
3058 auto reportError =
3059 [&]( const wxString& aMessage, int aOffset )
3060 {
3061 wxLogTrace( traceMultichannelTool, wxT( "ERROR: %s" ), aMessage );
3062 };
3063
3064 ctx.SetErrorCallback( reportError );
3065 preflightCtx.SetErrorCallback( reportError );
3066 compiler.SetErrorCallback( reportError );
3067
3068 // Use the zone's UUID to identify it uniquely. Using the zone name could match other zones
3069 // with the same name (e.g., a copper fill zone with the same name as a rule area).
3070 wxString ruleText = wxString::Format( wxT( "A.enclosedByArea('%s')" ),
3071 aRuleArea->m_zone->m_Uuid.AsString() );
3072
3073 const BOX2I zoneBox = aRuleArea->m_zone->Outline()->BBox();
3074
3075 auto testAndAdd =
3076 [&]( BOARD_CONNECTED_ITEM* aItem )
3077 {
3078 if( aOutput.contains( aItem ) )
3079 return;
3080
3081 // Tracks inside a generator (meander) are copied with the generator.
3082 if( EDA_GROUP* parent = aItem->GetParentGroup() )
3083 {
3084 if( parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
3085 return;
3086 }
3087
3088 // Items outside the area's box cannot be enclosed, so skip the costly evaluation
3089 if( !( aRuleArea->m_zone->GetLayerSet() & aItem->GetLayerSet() ).any()
3090 || !zoneBox.Intersects( aItem->GetBoundingBox() ) )
3091 {
3092 return;
3093 }
3094
3095 ctx.SetItems( aItem, aItem );
3096 LIBEVAL::VALUE* val = ucode.Run( &ctx );
3097
3098 if( val->AsDouble() != 0.0 )
3099 {
3100 aOutput.insert( aItem );
3101 count++;
3102 }
3103 };
3104
3105 if( compiler.Compile( ruleText, &ucode, &preflightCtx ) )
3106 {
3107 for( PCB_TRACK* track : board()->Tracks() )
3108 testAndAdd( track );
3109
3110 for( BOARD_ITEM* drawing : board()->Drawings() )
3111 {
3112 if( drawing->IsConnected() )
3113 testAndAdd( static_cast<BOARD_CONNECTED_ITEM*>( drawing ) );
3114 }
3115 }
3116
3117 return count;
3118}
3119
3120
3122 REPEAT_LAYOUT_OPTIONS aOpts, RULE_AREA_COMPAT_DATA& aCompatData )
3123{
3124 VECTOR2I disp;
3125 EDA_ANGLE rot;
3126 SHAPE_LINE_CHAIN newTargetOutline =
3127 repeatedOutline( aRefArea, aTargetArea, aOpts.m_anchor, aCompatData.m_matchingComponents, disp, rot );
3128
3129 // Reference net to the net of the first matching target pad, built once for all copied items
3130 const bool hasMatches = !aCompatData.m_matchingComponents.empty();
3131 std::map<int, int> netMap;
3132
3133 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
3134 {
3135 for( const PAD* refPad : refFP->Pads() )
3136 {
3137 if( refPad->GetNetCode() <= 0 )
3138 continue;
3139
3140 std::vector<const PAD*> targetPads = targetFP->GetPads( refPad->GetNumber() );
3141
3142 if( !targetPads.empty() )
3143 netMap.emplace( refPad->GetNetCode(), targetPads[0]->GetNetCode() );
3144 }
3145 }
3146
3147 SHAPE_POLY_SET refPoly;
3148 refPoly.AddOutline( aRefArea->m_zone->Outline()->COutline( 0 ) );
3149 refPoly.CacheTriangulation();
3150
3151 std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
3152
3153 // Group placement targets let RepeatLayout() reuse the existing target group, and m_groupItems
3154 // flat-groups every copy into one rule-area group. Reconstructing source groups here in either
3155 // case strands their members and leaves empty phantom clones behind (issue 22316).
3156 const bool preserveGroups = aTargetArea->m_sourceType != PLACEMENT_SOURCE_T::GROUP_PLACEMENT
3157 && !aOpts.m_groupItems;
3158
3159 // Defer reconstruction until every copy is made. Cloning a source group the moment one member
3160 // is copied would duplicate user groups that merely overlap the source area (issue 22316); a
3161 // group is rebuilt only once all of its members have been reproduced.
3162 std::vector<std::pair<BOARD_ITEM*, BOARD_ITEM*>> groupFixupPairs;
3163 std::set<BOARD_ITEM*> reproducedSourceItems;
3164
3165 auto fixupParentGroup =
3166 [&]( BOARD_ITEM* sourceItem, BOARD_ITEM* destItem )
3167 {
3168 // The copy inherits the source's parent-group pointer but is not a member of that
3169 // group; clear the dangling reference.
3170 destItem->SetParentGroup( nullptr );
3171
3172 if( !preserveGroups )
3173 return;
3174
3175 if( sourceItem->GetParentGroup() )
3176 groupFixupPairs.emplace_back( sourceItem, destItem );
3177
3178 reproducedSourceItems.insert( sourceItem );
3179 };
3180
3181 // Only stage changes for a target Rule Area zone if it actually belongs to the board.
3182 // In some workflows (e.g. ApplyDesignBlockLayout), the target area is a temporary zone
3183 // and is not added to the BOARD.
3184 bool targetZoneOnBoard = false;
3185
3186 if( aTargetArea->m_zone )
3187 {
3188 for( ZONE* z : board()->Zones() )
3189 {
3190 if( z == aTargetArea->m_zone )
3191 {
3192 targetZoneOnBoard = true;
3193 break;
3194 }
3195 }
3196 }
3197
3198 if( targetZoneOnBoard )
3199 {
3200 aCommit->Modify( aTargetArea->m_zone );
3201 aCompatData.m_affectedItems.insert( aTargetArea->m_zone );
3202 aCompatData.m_groupableItems.insert( aTargetArea->m_zone );
3203
3204 // The source rule-area zone maps to the target zone; treat it as reproduced so a group
3205 // containing it can still be rebuilt.
3206 if( preserveGroups )
3207 {
3208 if( aRefArea->m_zone->GetParentGroup() )
3209 groupFixupPairs.emplace_back( aRefArea->m_zone, aTargetArea->m_zone );
3210
3211 reproducedSourceItems.insert( aRefArea->m_zone );
3212 }
3213 }
3214
3215 if( aOpts.m_copyRouting )
3216 {
3217 std::set<BOARD_CONNECTED_ITEM*> refRouting;
3218 std::set<BOARD_CONNECTED_ITEM*> targetRouting;
3219
3220 wxLogTrace( traceMultichannelTool, wxT( "copying routing: %d fps\n" ),
3221 (int) aCompatData.m_matchingComponents.size() );
3222
3223 std::set<int> refc;
3224 std::set<int> targc;
3225
3226 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
3227 {
3228 for( PAD* pad : refFP->Pads() )
3229 refc.insert( pad->GetNetCode() );
3230
3231 for( PAD* pad : targetFP->Pads() )
3232 targc.insert( pad->GetNetCode() );
3233 }
3234
3235 // What is removed from the target spans the same run as what is copied from the reference
3236 if( aRefArea->ScansContents() )
3237 {
3238 targetRouting = findRepeatRouting( aTargetArea, targc, aOpts );
3239 refRouting = findRepeatRouting( aRefArea, refc, aOpts );
3240 }
3241 else
3242 {
3243 findRoutingInRuleArea( aTargetArea, targetRouting );
3244 findRoutingInRuleArea( aRefArea, refRouting );
3245 }
3246
3247 // Nets used by the target group's own items, footprint pads included. Taken from the area's
3248 // footprints too, since a channel created from a selection fills its group only when applied
3249 std::set<int> targetGroupNets;
3250
3251 for( FOOTPRINT* fp : aTargetArea->m_components )
3252 {
3253 for( PAD* pad : fp->Pads() )
3254 targetGroupNets.insert( pad->GetNetCode() );
3255 }
3256
3257 if( aTargetArea->m_group )
3258 {
3259 std::set<BOARD_ITEM*> members;
3260 collectGroupBoardItems( aTargetArea->m_group, members );
3261
3262 for( BOARD_ITEM* member : members )
3263 {
3264 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( member ) )
3265 targetGroupNets.insert( bci->GetNetCode() );
3266 }
3267 }
3268
3269 // A channel made from a selection or a link replaces loose routing only on its own nets; buses
3270 // and rails it shares stay. A design block replaces it on every net of the group
3271 const bool channelScope = aRefArea->m_fromSelection || linkedChannelGroup( board(), aTargetArea );
3272 std::set<int> targetChannelNets;
3273
3274 for( int net : targetGroupNets )
3275 {
3276 if( !channelScope )
3277 {
3278 targetChannelNets.insert( net );
3279 continue;
3280 }
3281
3282 std::vector<FOOTPRINT*> owners;
3283
3284 for( BOARD_CONNECTED_ITEM* pad : connectivity->GetNetItems( net, { PCB_PAD_T } ) )
3285 {
3286 if( std::ranges::find( owners, pad->GetParentFootprint() ) == owners.end() )
3287 owners.push_back( pad->GetParentFootprint() );
3288 }
3289
3290 if( isChannelNet( owners, aTargetArea->m_components ) )
3291 targetChannelNets.insert( net );
3292 }
3293
3294 for( BOARD_CONNECTED_ITEM* item : targetRouting )
3295 {
3296 // Never remove pads as part of routing copy.
3297 if( item->Type() == PCB_PAD_T )
3298 continue;
3299
3300 if( aRefArea->m_designBlockItems.count( item ) )
3301 continue;
3302
3303 // Design block apply: replace only the group's own routing, nested groups included, and
3304 // loose routing on the group's nets. Other groups' routing belongs to stacked instances (issue 24767).
3305 // Everything else is unrelated and just sits inside the block's area (issue 24944).
3306 if( aTargetArea->m_group && !aTargetArea->m_group->ContainsItem( item ) )
3307 {
3308 if( item->GetParentGroup() )
3309 continue;
3310
3311 if( item->IsLocked() )
3312 continue;
3313
3314 if( item->GetNetCode() <= 0 || !targetChannelNets.contains( item->GetNetCode() ) )
3315 continue;
3316 }
3317
3318 if( item->IsLocked() && !aOpts.m_includeLockedItems )
3319 continue;
3320
3321 if( ( aOpts.m_connectedRoutingOnly || aRefArea->ScansContents() ) && !targc.contains( item->GetNetCode() ) )
3322 continue;
3323
3324 // item already removed
3325 if( aCommit->GetStatus( item ) != 0 )
3326 continue;
3327
3328 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3329 {
3330 continue;
3331 }
3332
3333 aCompatData.m_affectedItems.insert( item );
3334 aCommit->Remove( item );
3335 }
3336
3337 for( BOARD_CONNECTED_ITEM* item : refRouting )
3338 {
3339 // Never copy pads as part of routing copy.
3340 if( item->Type() == PCB_PAD_T )
3341 continue;
3342
3343 if( item->IsLocked() && !aOpts.m_includeLockedItems )
3344 continue;
3345
3346 if( ( aOpts.m_connectedRoutingOnly || aRefArea->ScansContents() ) && !refc.contains( item->GetNetCode() ) )
3347 continue;
3348
3349 if( !aRefArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3350 continue;
3351
3352 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3353 continue;
3354
3355 BOARD_CONNECTED_ITEM* copied = static_cast<BOARD_CONNECTED_ITEM*>( item->Duplicate( false ) );
3356
3357 fixupNet( item, copied, hasMatches, netMap );
3358 fixupParentGroup( item, copied );
3359
3360 copied->Rotate( VECTOR2( 0, 0 ), rot );
3361 copied->Move( disp );
3362 aCompatData.m_groupableItems.insert( copied );
3363 aCommit->Add( copied );
3364 }
3365
3366 // Copy generators (meanders) whole so they are not flattened to loose tracks. Design
3367 // block apply has an exact item list, other rule areas resolve them by area.
3368 std::vector<PCB_GENERATOR*> refGenerators;
3369 std::vector<PCB_GENERATOR*> targetGenerators;
3370
3371 auto appendGenerators =
3372 [&]( EDA_GROUP* aGroup, std::vector<PCB_GENERATOR*>& aOut )
3373 {
3374 std::set<BOARD_ITEM*> members;
3375 collectGroupBoardItems( aGroup, members );
3376
3377 for( BOARD_ITEM* member : members )
3378 {
3379 if( member->Type() == PCB_GENERATOR_T )
3380 aOut.push_back( static_cast<PCB_GENERATOR*>( member ) );
3381 }
3382 };
3383
3385 {
3386 for( EDA_ITEM* item : aRefArea->m_designBlockItems )
3387 {
3388 if( item->Type() == PCB_GENERATOR_T )
3389 refGenerators.push_back( static_cast<PCB_GENERATOR*>( item ) );
3390 }
3391
3392 EDA_GROUP* targetGroup = aTargetArea->m_group;
3393
3394 if( !targetGroup && !aTargetArea->m_components.empty() )
3395 targetGroup = ( *aTargetArea->m_components.begin() )->GetParentGroup();
3396
3397 if( targetGroup )
3398 appendGenerators( targetGroup, targetGenerators );
3399 }
3400 else
3401 {
3402 // Each side takes its generators from its group when it is a linked channel, else from its outline
3403 EDA_GROUP* refGroup = linkedChannelGroup( board(), aRefArea );
3404 EDA_GROUP* targetGroup = linkedChannelGroup( board(), aTargetArea );
3405
3406 if( refGroup )
3407 appendGenerators( refGroup, refGenerators );
3408
3409 if( targetGroup )
3410 appendGenerators( targetGroup, targetGenerators );
3411
3412 if( !refGroup || !targetGroup )
3413 {
3414 const SHAPE_LINE_CHAIN& refOut = aRefArea->m_zone->Outline()->COutline( 0 );
3415 const SHAPE_LINE_CHAIN& targetOut = aTargetArea->m_zone->Outline()->COutline( 0 );
3416
3417 for( PCB_GENERATOR* gen : board()->Generators() )
3418 {
3419 if( gen->GetGeneratorType() != wxT( "tuning_pattern" ) )
3420 continue;
3421
3422 if( !refGroup && gen->HitTest( refOut, false ) )
3423 refGenerators.push_back( gen );
3424 else if( !targetGroup && !( refGroup && gen->HitTest( refOut, false ) )
3425 && gen->HitTest( targetOut, false ) )
3426 targetGenerators.push_back( gen );
3427 }
3428 }
3429 }
3430
3431 // Remove the target's existing generators so the copy replaces them.
3432 for( PCB_GENERATOR* gen : targetGenerators )
3433 {
3434 if( gen->IsLocked() && !aOpts.m_includeLockedItems )
3435 continue;
3436
3437 gen->RunOnChildren(
3438 [&]( BOARD_ITEM* child )
3439 {
3440 aCommit->Remove( child );
3441 },
3443 aCommit->Remove( gen );
3444 aCompatData.m_affectedItems.insert( gen );
3445 }
3446
3447 for( PCB_GENERATOR* gen : refGenerators )
3448 {
3449 if( gen->IsLocked() && !aOpts.m_includeLockedItems )
3450 continue;
3451
3452 PCB_GENERATOR* clone = gen->DeepClone();
3453
3454 clone->ResetUuid();
3455 clone->RunOnChildren(
3456 []( BOARD_ITEM* child )
3457 {
3458 child->ResetUuidDirect();
3459 },
3461
3462 clone->ClearFlags();
3463 clone->Rotate( VECTOR2( 0, 0 ), rot );
3464 clone->Move( disp );
3465 aCommit->Add( clone );
3466
3467 clone->RunOnChildren(
3468 [&]( BOARD_ITEM* child )
3469 {
3470 child->ClearFlags();
3471
3472 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( child ) )
3473 fixupNet( bci, bci, hasMatches, netMap );
3474
3475 aCommit->Add( child );
3476 },
3478
3479 aCompatData.m_groupableItems.insert( clone );
3480 }
3481 }
3482
3483 if( aOpts.m_copyOtherItems )
3484 {
3485 std::set<BOARD_ITEM*> sourceItems;
3486 std::set<BOARD_ITEM*> targetItems;
3487
3488 findOtherItemsInRuleArea( aRefArea, sourceItems );
3489 findOtherItemsInRuleArea( aTargetArea, targetItems );
3490
3491 // Apply Design Block Layout uses synthetic copper-only rule area zones that don't
3492 // reflect the layers the user actually drew on. The source items were collected by
3493 // explicit enumeration (m_designBlockItems) and the destination is a group bounding
3494 // box, so the per-item layer filter would incorrectly reject silkscreen, fab and
3495 // user drawings. Skip the layer filter only when both halves are the synthetic
3496 // design-block-to-group flow; regular GROUP_PLACEMENT rule areas have user-authored
3497 // layer sets that must still be honored.
3498 const bool skipLayerFilter = aRefArea->m_sourceType == PLACEMENT_SOURCE_T::DESIGN_BLOCK
3499 && aTargetArea->m_sourceType
3501
3502 for( BOARD_ITEM* item : targetItems )
3503 {
3504 if( item->GetParent() && item->GetParent()->Type() == PCB_FOOTPRINT_T )
3505 continue;
3506
3507 // Don't remove the appended source items: this geometric query can pick them up, but
3508 // they're deleted when the temporary append is reverted, leaving dangling pointers.
3509 if( aRefArea->m_designBlockItems.count( item ) )
3510 continue;
3511
3512 if( item->IsLocked() && !aOpts.m_includeLockedItems )
3513 continue;
3514
3515 // item already removed
3516 if( aCommit->GetStatus( item ) != 0 )
3517 continue;
3518
3519 if( item->Type() == PCB_ZONE_T )
3520 {
3521 ZONE* zone = static_cast<ZONE*>( item );
3522
3523 // Check all zone layers are included in the target rule area.
3524 if( skipLayerFilter
3525 || aTargetArea->m_zone->GetLayerSet().ContainsAll( zone->GetLayerSet() ) )
3526 {
3527 aCompatData.m_affectedItems.insert( zone );
3528 aCommit->Remove( zone );
3529 }
3530 }
3531 else
3532 {
3533 if( skipLayerFilter
3534 || aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3535 {
3536 aCompatData.m_affectedItems.insert( item );
3537 aCommit->Remove( item );
3538 }
3539 }
3540 }
3541
3542 for( BOARD_ITEM* item : sourceItems )
3543 {
3544 if( item->GetParent() && item->GetParent()->Type() == PCB_FOOTPRINT_T )
3545 continue;
3546
3547 if( item->IsLocked() && !aOpts.m_includeLockedItems )
3548 continue;
3549
3550 BOARD_ITEM* copied = nullptr;
3551
3552 if( item->Type() == PCB_ZONE_T )
3553 {
3554 ZONE* zone = static_cast<ZONE*>( item );
3555
3556 if( !skipLayerFilter )
3557 {
3558 LSET allowedLayers =
3559 aRefArea->m_zone->GetLayerSet() & aTargetArea->m_zone->GetLayerSet();
3560
3561 // Check all zone layers are included in both source and target rule areas.
3562 if( !allowedLayers.ContainsAll( zone->GetLayerSet() ) )
3563 continue;
3564 }
3565
3566 ZONE* targetZone = static_cast<ZONE*>( item->Duplicate( false ) );
3567 fixupNet( zone, targetZone, hasMatches, netMap );
3568
3569 copied = targetZone;
3570 }
3571 else
3572 {
3573 if( !skipLayerFilter )
3574 {
3575 if( !aRefArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3576 continue;
3577
3578 if( !aTargetArea->m_zone->GetLayerSet().Contains( item->GetLayer() ) )
3579 continue;
3580 }
3581
3582 copied = static_cast<BOARD_ITEM*>( item->Clone() );
3583 }
3584
3585 if( copied )
3586 {
3587 fixupParentGroup( item, copied );
3588
3589 copied->ClearFlags();
3590 copied->Rotate( VECTOR2( 0, 0 ), rot );
3591 copied->Move( disp );
3592 aCompatData.m_groupableItems.insert( copied );
3593 aCommit->Add( copied );
3594 }
3595 }
3596 }
3597
3598 if( aOpts.m_copyPlacement )
3599 {
3600 for( const auto& [refFP, targetFP] : aCompatData.m_matchingComponents )
3601 {
3602 if( !aRefArea->m_zone->GetLayerSet().Contains( refFP->GetLayer() ) )
3603 {
3604 wxLogTrace( traceMultichannelTool, wxT( "discard ref:%s (ref layer)\n" ),
3605 refFP->GetReference() );
3606 continue;
3607 }
3608 if( !aTargetArea->m_zone->GetLayerSet().Contains( refFP->GetLayer() ) )
3609 {
3610 wxLogTrace( traceMultichannelTool, wxT( "discard ref:%s (target layer)\n" ),
3611 refFP->GetReference() );
3612 continue;
3613 }
3614
3615 // For regular Rule Area repeat, ignore source footprints outside the reference area.
3616 // For Design Block apply, use the exact source item set collected from the block.
3618 && !linkedChannelGroup( board(), aRefArea )
3619 && !refFP->GetEffectiveShape( refFP->GetLayer() )->Collide( &refPoly, 0 ) )
3620 {
3621 continue;
3622 }
3623
3624 if( targetFP->IsLocked() && !aOpts.m_includeLockedItems )
3625 continue;
3626
3627 aCommit->Modify( targetFP );
3628
3629 targetFP->SetLayerAndFlip( refFP->GetLayer() );
3630 targetFP->SetOrientation( refFP->GetOrientation() );
3631 targetFP->SetPosition( refFP->GetPosition() );
3632 targetFP->Rotate( VECTOR2( 0, 0 ), rot );
3633 targetFP->Move( disp );
3634
3635 for( PCB_FIELD* refField : refFP->GetFields() )
3636 {
3637 wxCHECK2( refField, continue );
3638
3639 PCB_FIELD* targetField = targetFP->GetField( refField->GetName() );
3640
3641 if( !targetField )
3642 continue;
3643
3644 targetField->SetLayerSet( refField->GetLayerSet() );
3645 targetField->SetVisible( refField->IsVisible() );
3646 targetField->SetAttributes( refField->GetAttributes() );
3647 targetField->SetPosition( refField->GetPosition() );
3648 targetField->SetTextAngle( refField->GetTextAngle() );
3649 targetField->Rotate( VECTOR2( 0, 0 ), rot );
3650 targetField->Move( disp );
3651 targetField->SetIsKnockout( refField->IsKnockout() );
3652 }
3653
3654 // Copy non-field text items. Texts can share content (e.g. "${REFERENCE}" on both
3655 // F.SilkS and B.SilkS), so match one-to-one and prefer the same layer. Otherwise both
3656 // collapse onto one target item and the other side's text is lost.
3657 std::set<PCB_TEXT*> consumedTargets;
3658
3659 for( BOARD_ITEM* refItem : refFP->GraphicalItems() )
3660 {
3661 if( refItem->Type() != PCB_TEXT_T )
3662 continue;
3663
3664 PCB_TEXT* refText = static_cast<PCB_TEXT*>( refItem );
3665 PCB_TEXT* targetText = nullptr;
3666
3667 for( BOARD_ITEM* targetItem : targetFP->GraphicalItems() )
3668 {
3669 if( targetItem->Type() != PCB_TEXT_T )
3670 continue;
3671
3672 PCB_TEXT* candidate = static_cast<PCB_TEXT*>( targetItem );
3673
3674 if( consumedTargets.contains( candidate ) || candidate->GetText() != refText->GetText() )
3675 {
3676 continue;
3677 }
3678
3679 targetText = candidate;
3680
3681 if( candidate->GetLayer() == refText->GetLayer() )
3682 break;
3683 }
3684
3685 if( !targetText )
3686 continue;
3687
3688 consumedTargets.insert( targetText );
3689
3690 targetText->SetLayer( refText->GetLayer() );
3691 targetText->SetVisible( refText->IsVisible() );
3692 targetText->SetAttributes( refText->GetAttributes() );
3693 targetText->SetPosition( refText->GetPosition() );
3694 targetText->SetTextAngle( refText->GetTextAngle() );
3695 targetText->Rotate( VECTOR2( 0, 0 ), rot );
3696 targetText->Move( disp );
3697 targetText->SetIsKnockout( refText->IsKnockout() );
3698 }
3699
3700 // Copy 3D model settings
3701 targetFP->Models() = refFP->Models();
3702
3703 std::set<PAD*> consumedPads;
3704
3705 for( PAD* refPad : refFP->Pads() )
3706 {
3707 for( PAD* targetPad : targetFP->Pads() )
3708 {
3709 if( consumedPads.contains( targetPad ) || targetPad->GetNumber() != refPad->GetNumber() )
3710 {
3711 continue;
3712 }
3713
3714 consumedPads.insert( targetPad );
3715 targetPad->ImportSettingsFrom( *refPad );
3716 break;
3717 }
3718 }
3719
3720 aCompatData.m_affectedItems.insert( targetFP );
3721 aCompatData.m_groupableItems.insert( targetFP );
3722
3723 // The matched footprint maps to its target; treat it as reproduced so a group
3724 // containing it can be rebuilt.
3725 if( preserveGroups && refFP->GetParentGroup() )
3726 groupFixupPairs.emplace_back( refFP, targetFP );
3727
3728 if( preserveGroups )
3729 reproducedSourceItems.insert( refFP );
3730 }
3731 }
3732
3733 // Rebuild a source group only when all of its members were reproduced. A group that merely
3734 // overlaps the source area keeps uncopied members, so it is left untouched rather than
3735 // partially duplicated (issue 22316).
3736 if( preserveGroups && !groupFixupPairs.empty() )
3737 {
3738 std::map<EDA_GROUP*, EDA_GROUP*> groupMap;
3739 std::map<EDA_GROUP*, bool> fullyReproducedCache;
3740
3741 auto groupFullyReproduced =
3742 [&]( EDA_GROUP* aGroup )
3743 {
3744 if( auto it = fullyReproducedCache.find( aGroup ); it != fullyReproducedCache.end() )
3745 return it->second;
3746
3747 bool reproduced = true;
3748
3749 for( EDA_ITEM* member : aGroup->GetItems() )
3750 {
3751 // Nested groups are not reproduced, so a parent containing one can never
3752 // be fully reproduced.
3753 if( !member->IsBOARD_ITEM()
3754 || !reproducedSourceItems.contains( static_cast<BOARD_ITEM*>( member ) ) )
3755 {
3756 reproduced = false;
3757 break;
3758 }
3759 }
3760
3761 fullyReproducedCache[aGroup] = reproduced;
3762 return reproduced;
3763 };
3764
3765 for( const auto& [sourceItem, destItem] : groupFixupPairs )
3766 {
3767 EDA_GROUP* parentGroup = sourceItem->GetParentGroup();
3768
3769 if( !parentGroup || !groupFullyReproduced( parentGroup ) )
3770 continue;
3771
3772 if( !groupMap.contains( parentGroup ) )
3773 {
3774 PCB_GROUP* newGroup = static_cast<PCB_GROUP*>(
3775 static_cast<PCB_GROUP*>( parentGroup->AsEdaItem() )->Duplicate( false ) );
3776 newGroup->GetItems().clear();
3777 newGroup->SetParentGroup( nullptr );
3778
3779 if( newGroup->Type() == PCB_GENERATOR_T )
3780 {
3781 newGroup->Rotate( VECTOR2( 0, 0 ), rot );
3782 newGroup->Move( disp );
3783 }
3784
3785 groupMap[parentGroup] = newGroup;
3786 aCommit->Add( newGroup );
3787 }
3788
3789 // AddItem reparents the footprint out of any group it already belongs to; stage that
3790 // group so the membership change is captured for undo.
3791 if( EDA_GROUP* oldGroup = destItem->GetParentGroup() )
3792 {
3793 if( oldGroup != groupMap[parentGroup] )
3794 aCommit->Modify( oldGroup->AsEdaItem() );
3795 }
3796
3797 groupMap[parentGroup]->AddItem( destItem );
3798 }
3799 }
3800
3801 aTargetArea->m_zone->RemoveAllContours();
3802 aTargetArea->m_zone->AddPolygon( newTargetOutline );
3803 aTargetArea->m_zone->UnHatchBorder();
3804 aTargetArea->m_zone->HatchBorder();
3805}
3806
3812 bool aHasMatches,
3813 const std::map<int, int>& aNetMap )
3814{
3815 // Copy as no-net.
3816 if( !aHasMatches )
3817 {
3818 aTarget->SetNetCode( 0 );
3819 return;
3820 }
3821
3822 auto it = aNetMap.find( aRef->GetNetCode() );
3823
3824 if( it != aNetMap.end() )
3825 aTarget->SetNetCode( it->second );
3826}
3827
3828
3829std::vector<NETINFO_ITEM*> MULTICHANNEL_TOOL::IsolateDesignBlockAutoNets( BOARD* aBoard,
3830 const std::set<FOOTPRINT*>& aFootprints,
3831 const std::unordered_set<EDA_ITEM*>& aItems )
3832{
3833 std::vector<NETINFO_ITEM*> created;
3834 std::map<int, NETINFO_ITEM*> remap;
3835 int counter = 0;
3836
3837 // Auto-generated names are tied to a reference designator, so a block's Net-(D3-A) collides
3838 // with a different part's Net-(D3-A) on the board. Named/power nets are intentional and left
3839 // alone so the topology matcher keeps excluding real global rails.
3840 auto isAutoName = []( const wxString& aName )
3841 {
3842 return aName.StartsWith( wxT( "Net-(" ) ) || aName.StartsWith( wxT( "unconnected-" ) );
3843 };
3844
3845 auto remapItem = [&]( BOARD_CONNECTED_ITEM* aItem )
3846 {
3847 int code = aItem->GetNetCode();
3848
3849 if( code <= 0 )
3850 return;
3851
3852 NETINFO_ITEM* oldNet = aBoard->FindNet( code );
3853
3854 if( !oldNet || !isAutoName( oldNet->GetNetname() ) )
3855 return;
3856
3857 auto it = remap.find( code );
3858
3859 if( it == remap.end() )
3860 {
3861 wxString name;
3862
3863 do
3864 {
3865 name = wxString::Format( wxT( "__dbapply_%d_%d" ), code, counter++ );
3866 } while( aBoard->FindNet( name ) );
3867
3868 NETINFO_ITEM* newNet = new NETINFO_ITEM( aBoard, name );
3869 aBoard->Add( newNet );
3870 created.push_back( newNet );
3871 it = remap.emplace( code, newNet ).first;
3872 }
3873
3874 aItem->SetNet( it->second );
3875 };
3876
3877 for( FOOTPRINT* fp : aFootprints )
3878 {
3879 for( PAD* pad : fp->Pads() )
3880 remapItem( pad );
3881 }
3882
3883 for( EDA_ITEM* item : aItems )
3884 {
3885 if( BOARD_CONNECTED_ITEM* bci = dynamic_cast<BOARD_CONNECTED_ITEM*>( item ) )
3886 remapItem( bci );
3887 }
3888
3889 return created;
3890}
3891
3892
3893// Pairs footprints by their unique closest symbol instance path, independent of net topology
3894// Leaves aResult untouched unless that gives a full pad compatible bijection
3895static bool matchBySymbolInstancePath( const std::set<FOOTPRINT*>& aRef, const std::set<FOOTPRINT*>& aTarget,
3896 TMATCH::COMPONENT_MATCHES& aResult )
3897{
3898 if( aRef.empty() || aRef.size() != aTarget.size() )
3899 return false;
3900
3902 std::set<FOOTPRINT*> used;
3903
3904 for( FOOTPRINT* refFp : aRef )
3905 {
3906 FOOTPRINT* targetFp = nullptr;
3907 size_t bestLength = 0;
3908 bool tied = false;
3909
3910 for( FOOTPRINT* candidate : aTarget )
3911 {
3912 size_t length = TMATCH::CommonPathTailLength( refFp->GetPath(), candidate->GetPath() );
3913
3914 if( length > bestLength )
3915 {
3916 targetFp = candidate;
3917 bestLength = length;
3918 tied = false;
3919 }
3920 else if( length == bestLength && length > 0 )
3921 {
3922 tied = true;
3923 }
3924 }
3925
3926 // A missing or ambiguous path (copy paste, hand built group) is not a clean instance link
3927 if( !targetFp || tied )
3928 return false;
3929
3930 // Routing and placement copy only makes sense between pad compatible footprints
3931 if( refFp->GetFPID() != targetFp->GetFPID() || refFp->Pads().size() != targetFp->Pads().size() )
3932 return false;
3933
3934 if( !used.insert( targetFp ).second )
3935 return false;
3936
3937 result[refFp] = targetFp;
3938 }
3939
3940 aResult = std::move( result );
3941 return true;
3942}
3943
3944
3946 RULE_AREA_COMPAT_DATA& aMatches,
3947 const TMATCH::ISOMORPHISM_PARAMS& aParams,
3948 const std::unordered_set<int>& aExtraGlobalNets )
3949{
3950 using namespace TMATCH;
3951
3952 // Footprint-free design block has nothing to match to
3953 if( aRefArea->m_sourceType == PLACEMENT_SOURCE_T::DESIGN_BLOCK && aRefArea->m_components.empty()
3954 && aTargetArea->m_components.empty() )
3955 {
3956 aMatches.m_matchingComponents.clear();
3957 aMatches.m_isOk = true;
3958 aMatches.m_errorMsg = _( "OK" );
3959 aMatches.m_mismatchReasons.clear();
3960 return true;
3961 }
3962
3963 // Placement areas resolve their components from their source, so two areas sharing a sheet,
3964 // component class or group resolve to identical footprints. Repeating into such a target would
3965 // move and delete the reference's own items, corrupting the placement rather than copying it.
3966 if( !aRefArea->m_components.empty() )
3967 {
3968 std::set<FOOTPRINT*> shared;
3969 std::set_intersection( aRefArea->m_components.begin(), aRefArea->m_components.end(),
3970 aTargetArea->m_components.begin(), aTargetArea->m_components.end(),
3971 std::inserter( shared, shared.begin() ) );
3972
3973 if( !shared.empty() )
3974 {
3975 aMatches.m_matchingComponents.clear();
3976 aMatches.m_isOk = false;
3977 aMatches.m_errorMsg = _( "Target Rule Area shares components with the reference area" );
3978 aMatches.m_mismatchReasons.clear();
3979 aMatches.m_mismatchReasons.push_back(
3980 _( "This target Rule Area selects the same components as the reference area. "
3981 "Repeat layout cannot copy a Rule Area onto itself. Give each placement Rule "
3982 "Area a distinct sheet, component class or group." ) );
3983 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Shared components:\n%s" ),
3984 FormatComponentList( shared ) ) );
3985 return false;
3986 }
3987 }
3988
3989 // A global rail connects >=2 pads in more than one channel; find them across all areas so the
3990 // exclusion is the same for every target, not just the one being matched against.
3991 std::unordered_map<int, std::vector<const RULE_AREA*>> netInternalAreas;
3992
3993 for( const RULE_AREA& area : m_areas.m_areas )
3994 {
3995 std::unordered_map<int, int> areaNetPadCounts;
3996
3997 for( const FOOTPRINT* fp : area.m_components )
3998 {
3999 for( const PAD* pad : fp->Pads() )
4000 {
4001 if( pad->GetNetCode() > 0 )
4002 areaNetPadCounts[pad->GetNetCode()]++;
4003 }
4004 }
4005
4006 for( const auto& [netCode, padCount] : areaNetPadCounts )
4007 {
4008 if( padCount >= 2 )
4009 netInternalAreas[netCode].push_back( &area );
4010 }
4011 }
4012
4013 // Require two component-disjoint areas so overlapping rule areas can't make a per-channel net
4014 // look global.
4015 auto disjoint =
4016 []( const RULE_AREA* aA, const RULE_AREA* aB )
4017 {
4018 for( FOOTPRINT* fp : aA->m_components )
4019 {
4020 if( aB->m_components.count( fp ) )
4021 return false;
4022 }
4023
4024 return true;
4025 };
4026
4027 std::unordered_set<int> globalNets = aExtraGlobalNets;
4028
4029 for( const auto& [netCode, areas] : netInternalAreas )
4030 {
4031 for( size_t i = 0; i < areas.size() && !globalNets.count( netCode ); i++ )
4032 {
4033 for( size_t j = i + 1; j < areas.size(); j++ )
4034 {
4035 if( disjoint( areas[i], areas[j] ) )
4036 {
4037 globalNets.insert( netCode );
4038 break;
4039 }
4040 }
4041 }
4042 }
4043
4044 PROF_TIMER timerBuild;
4045 std::unique_ptr<CONNECTION_GRAPH> cgRef( CONNECTION_GRAPH::BuildFromFootprintSet( aRefArea->m_components,
4046 aTargetArea->m_components,
4047 globalNets ) );
4048 std::unique_ptr<CONNECTION_GRAPH> cgTarget( CONNECTION_GRAPH::BuildFromFootprintSet( aTargetArea->m_components,
4049 aRefArea->m_components,
4050 globalNets ) );
4051 timerBuild.Stop();
4052
4053 wxLogTrace( traceMultichannelTool, wxT( "Graph construction: %s (%d + %d components)" ),
4054 timerBuild.to_string(),
4055 (int) aRefArea->m_components.size(),
4056 (int) aTargetArea->m_components.size() );
4057
4058 std::vector<TMATCH::TOPOLOGY_MISMATCH_REASON> mismatchReasons;
4059
4060 PROF_TIMER timerIso;
4061 bool status = cgRef->FindIsomorphism( cgTarget.get(), aMatches.m_matchingComponents,
4062 mismatchReasons, aParams );
4063 timerIso.Stop();
4064
4065 wxLogTrace( traceMultichannelTool, wxT( "FindIsomorphism: %s, result=%d" ),
4066 timerIso.to_string(), status ? 1 : 0 );
4067
4068 // Net topology can legitimately differ between a design block and its placed instance once the
4069 // user edits connectivity on the board (a wire tying two block pads onto one net, etc.). Fall
4070 // back to the symbol instance linkage, which still gives the correct mapping in that case. Skip
4071 // it on a cancelled scan so cancellation is not mistaken for a topology miss.
4072 const bool cancelled = aParams.m_cancelled && aParams.m_cancelled->load( std::memory_order_relaxed );
4073
4074 if( !status && !cancelled
4075 && matchBySymbolInstancePath( aRefArea->m_components, aTargetArea->m_components,
4076 aMatches.m_matchingComponents ) )
4077 {
4078 status = true;
4079 }
4080
4081 aMatches.m_isOk = status;
4082
4083 if( status )
4084 {
4085 aMatches.m_errorMsg = _( "OK" );
4086 aMatches.m_mismatchReasons.clear();
4087 return true;
4088 }
4089
4090 aMatches.m_mismatchReasons.clear();
4091
4092 for( const auto& reason : mismatchReasons )
4093 {
4094 if( reason.m_reason.IsEmpty() )
4095 continue;
4096
4097 if( !reason.m_reference.IsEmpty() && !reason.m_candidate.IsEmpty() )
4098 aMatches.m_mismatchReasons.push_back( reason.m_reason );
4099 else if( !reason.m_reference.IsEmpty() )
4100 {
4101 aMatches.m_mismatchReasons.push_back( wxString::Format( wxT( "%s: %s" ),
4102 reason.m_reference,
4103 reason.m_reason ) );
4104 }
4105 else
4106 aMatches.m_mismatchReasons.push_back( reason.m_reason );
4107 }
4108
4109 if( aMatches.m_mismatchReasons.empty() )
4110 aMatches.m_mismatchReasons.push_back( _( "The components in the two areas could not be paired up." ) );
4111
4112 // The reason above already gives both totals. Only the lists add anything, and only when
4113 // both sides have parts to compare.
4114 if( aRefArea->m_components.size() != aTargetArea->m_components.size() && !aRefArea->m_components.empty()
4115 && !aTargetArea->m_components.empty() )
4116 {
4117 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Reference area components:\n%s" ),
4118 FormatComponentList( aRefArea->m_components ) ) );
4119 aMatches.m_mismatchReasons.push_back( wxString::Format( _( "Target area components:\n%s" ),
4120 FormatComponentList( aTargetArea->m_components ) ) );
4121 }
4122
4123 aMatches.m_errorMsg = aMatches.m_mismatchReasons.front();
4124
4125 return status;
4126}
4127
4128
4130 const std::unordered_set<BOARD_ITEM*>& aItemsToRemove )
4131{
4132 // Note: groups are only collections, not "real" hierarchy. A group's members are still parented
4133 // by the board (and therefore nested groups are still in the board's list of groups).
4134 for( PCB_GROUP* group : board()->Groups() )
4135 {
4136 std::vector<EDA_ITEM*> pruneList;
4137
4138 for( EDA_ITEM* refItem : group->GetItems() )
4139 {
4140 for( BOARD_ITEM* testItem : aItemsToRemove )
4141 {
4142 if( refItem->m_Uuid == testItem->m_Uuid )
4143 pruneList.push_back( refItem );
4144 }
4145 }
4146
4147 if( !pruneList.empty() )
4148 {
4149 aCommit.Modify( group );
4150
4151 for( EDA_ITEM* item : pruneList )
4152 group->RemoveItem( item );
4153
4154 if( group->GetItems().size() < 2 )
4155 aCommit.Remove( group );
4156 }
4157 }
4158
4159 return false;
4160}
4161
4162
4164{
4166
4167 if( Pgm().IsGUI() )
4168 {
4170
4171 if( m_areas.m_areas.size() <= 1 )
4172 {
4173 frame()->ShowInfoBarError( _( "Cannot auto-generate any placement areas because the "
4174 "schematic has only one or no hierarchical sheets, "
4175 "groups, or component classes." ),
4176 true );
4177 return 0;
4178 }
4179
4181 int ret = dialog.ShowModal();
4182
4183 if( ret != wxID_OK )
4184 return 0;
4185 }
4186
4187 wxLogTrace( traceMultichannelTool, wxT( "%d placement areas found\n" ), (int) m_areas.m_areas.size() );
4188
4189 BOARD_COMMIT commit( GetManager(), true, false );
4190
4191 for( RULE_AREA& ra : m_areas.m_areas )
4192 {
4193 // Clear the result so skipped candidates are not grouped below.
4194 ra.m_zone = nullptr;
4195
4196 if( !ra.m_generateEnabled )
4197 continue;
4198
4199 if( ra.m_components.empty() )
4200 continue;
4201
4202 wxString source;
4203 wxString existingAreaTraceFormat;
4204
4206 {
4207 source = ra.m_sheetPath;
4208 existingAreaTraceFormat = wxT( "Placement rule area for sheet '%s' already exists as '%s'\n" );
4209 }
4211 {
4213 existingAreaTraceFormat = wxT( "Placement rule area for component class '%s' already exists as '%s'\n" );
4214 }
4215 else
4216 {
4217 source = ra.m_groupName;
4218 existingAreaTraceFormat = wxT( "Placement rule area for group '%s' already exists as '%s'\n" );
4219 }
4220
4221 std::unique_ptr<ZONE> newZone( new ZONE( board() ) );
4222 newZone->SetZoneName( wxString::Format( wxT( "auto-placement-area-%s" ), source ) );
4223 newZone->SetPlacementAreaSourceType( ra.m_sourceType );
4224 newZone->SetPlacementAreaSource( source );
4225
4226 std::vector<ZONE*> oldZones;
4227
4228 for( ZONE* zone : board()->Zones() )
4229 {
4230 if( !zone->GetIsRuleArea() || !zone->GetPlacementAreaEnabled() )
4231 continue;
4232
4233 if( zone->GetPlacementAreaSourceType() != newZone->GetPlacementAreaSourceType()
4234 || zone->GetPlacementAreaSource() != newZone->GetPlacementAreaSource() )
4235 {
4236 continue;
4237 }
4238
4239 wxLogTrace( traceMultichannelTool, existingAreaTraceFormat, source, zone->GetZoneName() );
4240
4241 oldZones.push_back( zone );
4242 }
4243
4244 if( !oldZones.empty() && !m_areas.m_replaceExisting )
4245 continue;
4246
4247 SHAPE_LINE_CHAIN raOutline;
4248
4249 // Groups are a way for the user to more explicitly provide a list of items to include in
4250 // the multichannel tool, as opposed to inferring them based on sheet structure or component classes.
4251 // So for group-based RAs, we build the RA outline based everything in the group, not just components.
4253 {
4254 std::set<BOARD_ITEM*> groupItems = queryBoardItemsInGroup( ra.m_groupName );
4255
4256 if( groupItems.empty() )
4257 {
4258 wxLogTrace( traceMultichannelTool,
4259 wxT( "Skipping placement rule area generation for source group '%s': group has no board items." ),
4260 ra.m_groupName );
4261 continue;
4262 }
4263
4264 raOutline = buildRAOutline( groupItems, 100000 );
4265 }
4266 else
4267 {
4268 // Start from the footprints, then also take everything in the design-block groups
4269 // they belong to (recursively) so routing and meanders that extend past the
4270 // footprints land inside the outline. Group membership keeps it bounded to this channel.
4271 std::set<BOARD_ITEM*> outlineItems;
4272 std::set<EDA_GROUP*> groups;
4273 std::set<int> channelNets;
4274
4275 for( FOOTPRINT* fp : ra.m_components )
4276 {
4277 outlineItems.insert( fp );
4278
4279 for( PAD* pad : fp->Pads() )
4280 channelNets.insert( pad->GetNetCode() );
4281
4282 for( EDA_GROUP* g = fp->GetParentGroup(); g; g = g->AsEdaItem()->GetParentGroup() )
4283 groups.insert( g );
4284 }
4285
4286 for( EDA_GROUP* g : groups )
4287 collectGroupBoardItems( g, outlineItems );
4288
4289 // Also include tracks and vias on nets local to this channel (all pads on the net
4290 // belong to the channel), so loose connections between blocks land in the outline.
4291 std::set<int> foreignNets;
4292
4293 for( FOOTPRINT* fp : board()->Footprints() )
4294 {
4295 if( ra.m_components.count( fp ) )
4296 continue;
4297
4298 for( PAD* pad : fp->Pads() )
4299 foreignNets.insert( pad->GetNetCode() );
4300 }
4301
4302 for( PCB_TRACK* track : board()->Tracks() )
4303 {
4304 int net = track->GetNetCode();
4305
4306 if( net > 0 && channelNets.count( net ) && !foreignNets.count( net ) )
4307 outlineItems.insert( track );
4308 }
4309
4310 raOutline = buildRAOutline( outlineItems, 100000 );
4311 }
4312
4313 wxLogTrace( traceMultichannelTool, wxT( "Generated rule area '%s' (%d components)\n" ),
4314 newZone->GetZoneName(),
4315 (int) ra.m_components.size() );
4316
4317 makePlacementArea( newZone.get(), raOutline );
4318
4319 for( ZONE* oldZone : oldZones )
4320 commit.Remove( oldZone );
4321
4322 ra.m_zone = newZone.release();
4323 commit.Add( ra.m_zone );
4324 }
4325
4326 // fixme: handle corner cases where the items belonging to a Rule Area already
4327 // belong to other groups.
4328
4329 if( m_areas.m_options.m_groupItems )
4330 {
4331 for( RULE_AREA& ra : m_areas.m_areas )
4332 {
4333 if( !ra.m_zone )
4334 continue;
4335
4336 std::unordered_set<BOARD_ITEM*> toPrune;
4337
4338 std::copy( ra.m_components.begin(), ra.m_components.end(), std::inserter( toPrune, toPrune.begin() ) );
4339
4340 pruneExistingGroups( commit, toPrune );
4341
4342 PCB_GROUP* group = new PCB_GROUP( board() );
4343
4344 commit.Add( group );
4345
4346 commit.Modify( ra.m_zone );
4347 group->AddItem( ra.m_zone );
4348
4349 for( FOOTPRINT* fp : ra.m_components )
4350 {
4351 commit.Modify( fp );
4352 group->AddItem( fp );
4353 }
4354 }
4355 }
4356
4357 commit.Push( _( "Auto-generate Placement Rule Areas" ) );
4358
4359 return true;
4360}
const char * name
@ ERROR_OUTSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
#define SKIP_ENTERED_GROUP
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
static TOOL_ACTION selectItem
Select an item (specified as the event parameter).
Definition actions.h:223
static TOOL_ACTION selectionClear
Clear the current selection.
Definition actions.h:220
static TOOL_ACTION selectItems
Select a list of items (specified as the event parameter)
Definition actions.h:228
ACTION_MENU(bool isContextMenu, TOOL_INTERACTIVE *aTool=nullptr)
Default constructor.
void SetTitle(const wxString &aTitle) override
Set title for the menu.
void SetIcon(BITMAPS aIcon)
Assign an icon for the entry.
friend class TOOL_INTERACTIVE
TOOL_INTERACTIVE * m_tool
Creator of the menu.
void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
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.
bool IsConnected() const override
Returns information if the object is derived from BOARD_CONNECTED_ITEM.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:85
void ResetUuidDirect()
Definition board_item.h:278
virtual void SetLayerSet(const LSET &aLayers)
Definition board_item.h:363
virtual bool IsKnockout() const
Definition board_item.h:423
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:424
virtual void SetLayer(PCB_LAYER_ID aLayer)
Set the layer this item is on.
Definition board_item.h:383
virtual const BOARD * GetBoard() const
Return the BOARD in which this BOARD_ITEM resides, or NULL if none.
FOOTPRINT * GetParentFootprint() const
virtual void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const
Invoke a function on all children.
Definition board_item.h:265
void ResetUuid()
Definition board_item.h:281
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1520
NETINFO_ITEM * FindNet(int aNetcode) const
Search for a net with the given netcode.
Definition board.cpp:2797
const FOOTPRINTS & Footprints() const
Definition board.h:465
static wxString MakeUniqueZoneName(const wxString &aBaseName, const std::function< bool(const wxString &)> &aInUse)
Return a name based on aBaseName for which aInUse returns false, adding or incrementing a _<number> s...
Definition board.cpp:1483
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:2142
BOARD_DESIGN_SETTINGS & GetDesignSettings()
Definition board.cpp:1290
std::shared_ptr< CONNECTIVITY_DATA > GetConnectivity() const
Return a list of missing connections between components/tracks.
Definition board.h:758
constexpr const Vec GetEnd() const
Definition box2.h:209
constexpr ecoord_type GetArea() const
Return the area of the rectangle.
Definition box2.h:697
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:165
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:574
constexpr const Vec & GetOrigin() const
Definition box2.h:207
constexpr bool Intersects(const BOX2< Vec > &aRect) const
Definition box2.h:308
CN_EDGE represents a point-to-point connection, whether realized or unrealized (ie: tracks etc.
CN_ITEM represents a BOARD_CONNECTED_ITEM in the connectivity system (ie: a pad, track/arc/via,...
const std::vector< CN_ITEM * > & ConnectedItems() const
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:235
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.
void AddItem(const TOOL_ACTION &aAction, const SELECTION_CONDITION &aCondition, int aOrder=ANY_ORDER)
Add a menu entry to run a TOOL_ACTION on selected items.
void AddMenu(ACTION_MENU *aMenu, const SELECTION_CONDITION &aCondition=SELECTION_CONDITIONS::ShowAlways, int aOrder=ANY_ORDER)
Add a submenu to the menu.
CONDITIONAL_MENU(TOOL_INTERACTIVE *aTool)
void ClearLocalRatsnest()
Erase the temporary, selection-based ratsnest (i.e.
void BlockRatsnestItems(const std::vector< BOARD_ITEM * > &aItems)
int ShowModal() override
virtual void Refresh(bool aEraseBackground=true, const wxRect *aRect=nullptr) override
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:43
std::unordered_set< EDA_ITEM * > & GetItems()
Definition eda_group.h:78
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:58
bool ContainsItem(const EDA_ITEM *aItem) const
Test if an item is a direct or nested member of this group.
Definition eda_group.cpp:26
virtual EDA_ITEM * AsEdaItem()=0
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
const KIID m_Uuid
Definition eda_item.h:599
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:116
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
void ClearFlags(EDA_ITEM_FLAGS aMask=EDA_ITEM_ALL_FLAGS)
Definition eda_item.h:160
virtual void SetParentGroup(EDA_GROUP *aGroup)
Definition eda_item.h:115
void SetBrightened()
Definition eda_item.h:151
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:118
virtual bool IsVisible() const
Definition eda_text.h:226
void SetAttributes(const EDA_TEXT &aSrc, bool aSetPosition=true)
Set the text attributes from another instance.
Definition eda_text.cpp:394
virtual void SetVisible(bool aVisible)
Definition eda_text.cpp:347
const TEXT_ATTRIBUTES & GetAttributes() const
Definition eda_text.h:270
std::deque< PAD * > & Pads()
Definition footprint.h:355
const LIB_ID & GetFPID() const
Definition footprint.h:417
PAD * FindPadByNumber(const wxString &aPadNumber, PAD *aSearchAfterMe=nullptr) const
Return a PAD with a matching number.
const BOX2I GetBoundingBox() const override
Return the orthogonal bounding box of this object for display purposes.
Handle actions specific to filling copper zones.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
Hold a (potentially large) number of VIEW_ITEMs and renders them on a graphics device provided by the...
Definition view.h:68
void InitPreview()
Definition view.cpp:2734
Definition kiid.h:46
wxString AsString() const
Definition kiid.cpp:264
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
ACTION_MENU * create() const override
Return an instance of this class. It has to be overridden in inheriting classes.
MULTICHANNEL_CONTEXT_MENU(TOOL_INTERACTIVE *aTool)
void setPreviewHidden(bool aHide)
int CheckRACompatibility(ZONE *aRefZone)
void detachArea(BOARD_COMMIT &aCommit, ZONE *aZone)
std::set< FOOTPRINT * > queryComponentsInSheet(wxString aSheetName) const
void fixupNet(BOARD_CONNECTED_ITEM *aRef, BOARD_CONNECTED_ITEM *aTarget, bool aHasMatches, const std::map< int, int > &aNetMap)
Attempts to make sure copied items are assigned the right net.
bool findOtherItemsInRuleArea(RULE_AREA *aRuleArea, std::set< BOARD_ITEM * > &aItems)
void dropLoneLinks(BOARD_COMMIT &aCommit, const std::set< KIID > &aSets)
bool resolveConnectionTopology(RULE_AREA *aRefArea, RULE_AREA *aTargetArea, RULE_AREA_COMPAT_DATA &aMatches, const TMATCH::ISOMORPHISM_PARAMS &aParams={}, const std::unordered_set< int > &aExtraGlobalNets={})
void HighlightRepeatAreas(const RULE_AREA *aArea)
Mark the reference parts, and the parts of aArea if given, on the board while the dialog is open.
std::vector< SEG > m_previewRatsnest
bool resolveTopologies(const wxString &aTitle, const std::vector< TOPOLOGY_JOB > &aJobs)
Resolve the topology of each job in turn on a worker thread.
int repeatLayout(const TOOL_EVENT &aEvent)
bool findComponentsInRuleArea(RULE_AREA *aRuleArea, std::set< FOOTPRINT * > &aComponents)
void UpdatePickedItem(const EDA_ITEM *aItem) override
std::map< FOOTPRINT *, BOX2I > m_previewBoxes
int expandToLargestTopology(const TOOL_EVENT &aEvent)
void setTransitions() override
This method is meant to be overridden in order to specify handlers for events.
void UnlinkAreas(const std::set< ZONE * > &aZones)
Remove aZones from their link sets, and any area left alone in one, as one undo step.
static wxString JoinReferences(const std::set< FOOTPRINT * > &aFootprints)
bool Init() override
Init() is called once upon a registration of the tool.
void onRepeatDialogClosed(wxCloseEvent &aEvent)
const SHAPE_LINE_CHAIN buildRAOutline(std::set< FOOTPRINT * > &aFootprints, int aMargin)
int findRoutingInRuleArea(RULE_AREA *aRuleArea, std::set< BOARD_CONNECTED_ITEM * > &aOutput) const
int applyRepeat(BOARD_COMMIT &aCommit)
static ZONE * ChannelZone(const PCB_GROUP *aGroup)
The rule area zone that makes aGroup a repeated channel, or null for any other group.
std::vector< RULE_AREA_COMPAT_DATA > findMatches(const std::set< FOOTPRINT * > &aSelected, bool aLoose)
The channels elsewhere on the board that repeat the topology of aSelected.
void Reset(RESET_REASON aReason) override
Bring the tool to a known, initial state.
std::set< FOOTPRINT * > ExpandToMatchingTopology(const std::set< FOOTPRINT * > &aSeed, bool aLargest)
Grow aSeed with the parts next to it, keeping only sets that still have an exact twin elsewhere on th...
bool UpdateLinkedLayouts(ZONE *aSource)
Copy aSource onto the other areas of its link set, as one undo step.
static std::vector< NETINFO_ITEM * > IsolateDesignBlockAutoNets(BOARD *aBoard, const std::set< FOOTPRINT * > &aFootprints, const std::unordered_set< EDA_ITEM * > &aItems)
Remap auto-generated nets (Net-(...), unconnected-...) of a design block that was appended for layout...
void StartRepeatFromSelection(const std::vector< EDA_ITEM * > &aItems)
Use plain selected items as the reference and discover the channels that repeat them.
int expandToMatchingTopology(const TOOL_EVENT &aEvent)
const RULE_AREA * m_focusArea
std::vector< KIID > m_previewHidden
void copyRuleAreaContents(RULE_AREA *aRefArea, RULE_AREA *aTargetArea, BOARD_COMMIT *aCommit, REPEAT_LAYOUT_OPTIONS aOpts, RULE_AREA_COMPAT_DATA &aCompatData)
RULE_AREAS_DATA m_areas
PCB_GROUP * findPlacementGroup(const wxString &aGroupName) const
bool pruneExistingGroups(COMMIT &aCommit, const std::unordered_set< BOARD_ITEM * > &aItemsToCheck)
std::shared_ptr< KIGFX::VIEW_OVERLAY > m_highlight
int updateLinkedLayouts(const TOOL_EVENT &aEvent)
void ShowMismatchDetails(wxWindow *aParent, const wxString &aSummary, const std::vector< wxString > &aReasons) const
void RefreshRepeat()
Re-derive the channels after a tick or the anchor changed, then redraw the preview.
int RepeatLayout(const TOOL_EVENT &aEvent, ZONE *aRefZone)
int AutogenerateRuleAreas(const TOOL_EVENT &aEvent)
int expandSelection(bool aLargest)
void DiscoverRepeatCandidates(bool aLoose)
std::set< BOARD_CONNECTED_ITEM * > findRepeatRouting(RULE_AREA *aArea, const std::set< int > &aNets, const REPEAT_LAYOUT_OPTIONS &aOpts)
The routing of aArea on aNets, the only nets a repeat ever carries.
DIALOG_MULTICHANNEL_REPEAT_LAYOUT * m_dialog
std::set< FOOTPRINT * > queryComponentsInComponentClass(const wxString &aComponentClassName) const
int ApplyRepeat()
Apply what the Repeat Layout dialog shows.
void linkAreas(BOARD_COMMIT &aCommit, ZONE *aRefZone, const std::vector< RULE_AREA * > &aTargets)
Link aTargets and aRefZone into one set with their own copies of the options.
int unlinkLayout(const TOOL_EVENT &aEvent)
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:50
const wxString & GetNetname() const
Definition netinfo.h:110
Definition pad.h:61
void SetItems(BOARD_ITEM *a, BOARD_ITEM *b=nullptr)
static TOOL_ACTION unlinkLayout
static TOOL_ACTION repeatLayout
static TOOL_ACTION generatePlacementRuleAreas
static TOOL_ACTION expandToLargestTopology
static TOOL_ACTION updateLinkedLayouts
static TOOL_ACTION expandToMatchingTopology
static TOOL_ACTION selectItemInteractively
Selection of reference points/items.
void RedrawRatsnest()
Force refresh of the ratsnest visual representation.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
PCB_GENERATOR * DeepClone() const
void Move(const VECTOR2I &aMoveVector) override
Move this object.
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:51
void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const override
Invoke a function on all children.
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
void Move(const VECTOR2I &aMoveVector) override
Move this object.
EDA_ITEM * AsEdaItem() override
Definition pcb_group.h:59
The selection tool: currently supports:
EDA_ANGLE GetTextAngle() const override
Definition pcb_text.cpp:560
VECTOR2I GetPosition() const override
Definition pcb_text.h:101
void SetPosition(const VECTOR2I &aPos) override
Definition pcb_text.h:103
void Move(const VECTOR2I &aMoveVector) override
Move this object.
Definition pcb_text.h:105
void SetTextAngle(const EDA_ANGLE &aAngle) override
Definition pcb_text.cpp:572
void Rotate(const VECTOR2I &aRotCentre, const EDA_ANGLE &aAngle) override
Rotate this object.
Definition pcb_text.cpp:583
T * frame() const
KIGFX::PCB_VIEW * view() const
PCB_TOOL_BASE(TOOL_ID aId, const std::string &aName)
Constructor.
BOARD * board() const
PCB_DRAW_PANEL_GAL * canvas() const
virtual void Reset(RESET_REASON aReason) override
Bring the tool to a known, initial state.
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:154
bool RoutingInProgress()
Returns whether routing is currently active.
Definition seg.h:38
static SELECTION_CONDITION HasTypes(std::vector< KICAD_T > aTypes)
Create a functor that tests if among the selected items there is at least one of a given types.
static SELECTION_CONDITION HasType(KICAD_T aType)
Create a functor that tests if among the selected items there is at least one of a given type.
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={}, bool aBuildIndexes=false)
Build a polygon triangulation, needed to draw a polygon on OpenGL and in some other calculations.
void BooleanIntersection(const SHAPE_POLY_SET &b)
Perform boolean polyset intersection.
void BooleanSubtract(const SHAPE_POLY_SET &b)
Perform boolean polyset difference.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
virtual VECTOR2I Centre() const
Compute a center-of-mass of the shape.
Definition shape.h:242
static std::unique_ptr< CONNECTION_GRAPH > BuildFromFootprintSet(const std::set< FOOTPRINT * > &aFps, const std::set< FOOTPRINT * > &aOtherChannelFps={}, const std::unordered_set< int > &aGlobalNets={})
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
KIGFX::VIEW * getView() const
Return the instance of KIGFX::VIEW object used in the application.
Definition tool_base.cpp:34
RESET_REASON
Determine the reason of reset for a tool.
Definition tool_base.h:74
@ SHUTDOWN
Tool is being shut down.
Definition tool_base.h:80
@ MODEL_RELOAD
Model changes (the sheet for a schematic)
Definition tool_base.h:76
@ SUPERMODEL_RELOAD
For schematics, the entire schematic changed, not just the sheet.
Definition tool_base.h:77
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).
TOOL_MENU & GetToolMenu()
Manage a CONDITIONAL_MENU and some number of CONTEXT_MENUs as sub-menus.
Definition tool_menu.h:39
CONDITIONAL_MENU & GetMenu()
Definition tool_menu.cpp:40
void RegisterSubMenu(std::shared_ptr< ACTION_MENU > aSubMenu)
Store a submenu of this menu model.
Definition tool_menu.cpp:46
Define a general 2D-vector/point.
Definition vector2d.h:67
Handle a list of polygons defining a copper zone.
Definition zone.h:70
void SetDoNotAllowPads(bool aEnable)
Definition zone.h:858
bool GetIsRuleArea() const
Accessors to parameters used in Rule Area zones:
Definition zone.h:832
const REPEAT_LAYOUT_OPTIONS & GetRepeatOptions() const
Definition zone.h:846
void AddPolygon(std::vector< VECTOR2I > &aPolygon)
Add a polygon to the zone outline.
Definition zone.cpp:1479
wxString GetPlacementAreaSource() const
Definition zone.h:837
void SetRepeatOptions(const REPEAT_LAYOUT_OPTIONS &aOpts)
Definition zone.h:847
void HatchBorder()
Compute the hatch lines depending on the hatch parameters and stores it in the zone's attribute m_bor...
Definition zone.cpp:1614
void SetLinkSet(const KIID &aLinkSet)
Definition zone.h:844
PLACEMENT_SOURCE_T GetPlacementAreaSourceType() const
Definition zone.h:839
SHAPE_POLY_SET * Outline()
Definition zone.h:421
void SetHatchStyle(ZONE_BORDER_DISPLAY_STYLE aStyle)
Definition zone.h:688
void SetIsRuleArea(bool aEnable)
Definition zone.h:833
void SetDoNotAllowTracks(bool aEnable)
Definition zone.h:857
const wxString & GetZoneName() const
Definition zone.h:160
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:700
bool GetPlacementAreaEnabled() const
Definition zone.h:834
void SetDoNotAllowVias(bool aEnable)
Definition zone.h:856
virtual LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition zone.h:133
void SetDoNotAllowFootprints(bool aEnable)
Definition zone.h:859
const KIID & GetLinkSet() const
The set of placement rule areas that share one layout, or niluuid when this one is not linked.
Definition zone.h:843
void SetDoNotAllowZoneFills(bool aEnable)
Definition zone.h:855
void UnHatchBorder()
Clear the zone's hatch.
Definition zone.cpp:1608
void RemoveAllContours(void)
Definition zone.h:652
void SetPlacementAreaEnabled(bool aEnabled)
Definition zone.h:835
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)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:448
@ RECURSE
Definition eda_item.h:51
@ NO_RECURSE
Definition eda_item.h:52
std::string source
KIID niluuid(0)
static std::map< int, std::vector< FOOTPRINT * > > buildNetOwners(const BOARD *aBoard)
static bool isChannelNet(const std::vector< FOOTPRINT * > &aOwners, const std::set< FOOTPRINT * > &aChannel)
static std::set< int > padNets(const FOOTPRINT *aFp, const std::set< int > &aAmong)
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 wxString JoinMismatchReasons(const std::vector< wxString > &aReasons)
static void stageAddToGroup(BOARD_COMMIT &aCommit, EDA_GROUP *aGroup, BOARD_ITEM *aItem)
static EDA_GROUP * linkedChannelGroup(const BOARD *aBoard, const RULE_AREA *aArea)
static void makePlacementArea(ZONE *aZone, const SHAPE_LINE_CHAIN &aOutline)
static std::vector< TMATCH::COMPONENT_MATCHES > matchSheetInstances(BOARD *aBoard, const std::set< FOOTPRINT * > &aRef)
static bool outlinesOverlap(const SHAPE_POLY_SET &aA, const SHAPE_POLY_SET &aB)
static std::set< ZONE * > linkedAreasIn(const SELECTION &aSelection)
static void collectGroupFootprints(EDA_GROUP *aGroup, std::set< FOOTPRINT * > &aOut)
static const std::pair< FOOTPRINT *const, FOOTPRINT * > & anchorPair(const TMATCH::COMPONENT_MATCHES &aMatches, const KIID &aAnchor)
static void collectGroupBoardItems(EDA_GROUP *aGroup, std::set< BOARD_ITEM * > &aOut)
static void ShowTopologyMismatchReasons(wxWindow *aParent, const wxString &aSummary, const std::vector< wxString > &aReasons)
static SHAPE_LINE_CHAIN repeatedOutline(const RULE_AREA *aRef, const RULE_AREA *aTarget, const KIID &aAnchor, const TMATCH::COMPONENT_MATCHES &aMatches, VECTOR2I &aDisp, EDA_ANGLE &aRot)
static std::vector< wxString > getParentSheetPaths(const wxString &aSheetName)
static std::set< int > channelNets(const std::map< int, std::vector< FOOTPRINT * > > &aNetOwners, const std::set< FOOTPRINT * > &aChannel)
SHAPE_LINE_CHAIN RectifyPolygon(const SHAPE_LINE_CHAIN &aPoly)
Take a polygon and 'rectify' it, so that all sides are H/V.
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:188
size_t CommonPathTailLength(const KIID_PATH &aA, const KIID_PATH &aB)
Count the trailing elements two symbol instance paths share.
Class to handle a set of BOARD_ITEMs.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
CITER next(CITER it)
Definition ptree.cpp:120
std::vector< EDA_ITEM * > EDA_ITEMS
Utility functions for working with shapes.
void AccumulateDescription(wxString &aDesc, const wxString &aItem)
Utility to build comma separated lists in messages.
TMATCH::COMPONENT_MATCHES m_map
bool bind(FOOTPRINT *aRef, FOOTPRINT *aTarget, std::vector< int > &aAdded)
const std::vector< FOOTPRINT * > & m_order
bool extend(size_t aIndex)
const std::set< int > & m_internal
std::set< int > m_netsTaken
const std::map< int, std::vector< FOOTPRINT * > > & m_netOwners
bool pair(size_t aIndex, FOOTPRINT *aChoice)
const std::set< FOOTPRINT * > & m_leaves
std::map< int, int > m_netMap
const std::set< FOOTPRINT * > & m_claimed
std::set< FOOTPRINT * > m_used
Options for copying a placement rule area onto the others linked to it.
KIID m_anchor
This area's own anchor footprint, or niluuid for the area center.
std::vector< wxString > m_mismatchReasons
TMATCH::COMPONENT_MATCHES m_discovered
Pairing found by discovery.
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::set< FOOTPRINT * > m_unmatched
Selected footprints the channel lacks.
std::unordered_set< BOARD_ITEM * > m_groupableItems
Filled in by copyRuleAreaContents with affected items that can be grouped together.
VECTOR2I m_center
bool ScansContents() const
True when the contents are scanned from the area rather than taken from an exact item list.
std::unordered_set< EDA_ITEM * > m_designBlockItems
wxString m_sheetName
bool m_fromSelection
The reference of a plain selection, copied as given with its footprint area deciding what else comes ...
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:47
std::atomic< int > * m_matchedComponents
Definition topo_match.h:48
std::atomic< int > * m_totalComponents
Definition topo_match.h:49
bool copied
bool moved
std::string path
IbisParser parser & reporter
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
void RotatePoint(int *pX, int *pY, const EDA_ANGLE &aAngle)
Calculate the new point of coord coord pX, pY, for a rotation center 0, 0.
Definition trigo.cpp:227
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:83
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_GROUP_T
class PCB_GROUP, a set of BOARD_ITEMs
Definition typeinfo.h:103
@ PCB_ZONE_T
class ZONE, a copper pour area
Definition typeinfo.h:100
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:84
@ PCB_TABLECELL_T
class PCB_TABLECELL, PCB_TEXTBOX for use in tables
Definition typeinfo.h:87
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:78
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:79
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:90
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
#define PR_CAN_ABORT
PLACEMENT_SOURCE_T