1 /*
2  * This program source code file is part of KiCad, a free EDA CAD application.
3  *
4  * Copyright (C) 2014-2016 CERN
5  * Copyright (C) 2021 KiCad Developers, see AUTHORS.txt for contributors.
6  * @author Maciej Suminski <maciej.suminski@cern.ch>
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, you may find one here:
20  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21  * or you may search the http://www.gnu.org website for the version 2 license,
22  * or you may write to the Free Software Foundation, Inc.,
23  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
24  */
25 #include "tool/selection.h"
26 #include "placement_tool.h"
27 #include "pcb_actions.h"
28 #include "pcb_selection_tool.h"
29 
30 #include <ratsnest/ratsnest_data.h>
31 #include <tool/tool_manager.h>
32 
33 #include <pcb_edit_frame.h>
34 #include <board.h>
35 #include <board_commit.h>
36 #include <bitmaps.h>
37 
38 #include <confirm.h>
39 #include <menus_helpers.h>
40 
41 
ALIGN_DISTRIBUTE_TOOL()42 ALIGN_DISTRIBUTE_TOOL::ALIGN_DISTRIBUTE_TOOL() :
43     TOOL_INTERACTIVE( "pcbnew.Placement" ),
44     m_selectionTool( nullptr ),
45     m_placementMenu( nullptr ),
46     m_frame( nullptr )
47 {
48 }
49 
~ALIGN_DISTRIBUTE_TOOL()50 ALIGN_DISTRIBUTE_TOOL::~ALIGN_DISTRIBUTE_TOOL()
51 {
52     delete m_placementMenu;
53 }
54 
55 
Init()56 bool ALIGN_DISTRIBUTE_TOOL::Init()
57 {
58     // Find the selection tool, so they can cooperate
59     m_selectionTool = m_toolMgr->GetTool<PCB_SELECTION_TOOL>();
60     m_frame = getEditFrame<PCB_BASE_FRAME>();
61 
62     // Create a context menu and make it available through selection tool
63     m_placementMenu = new ACTION_MENU( true, this );
64     m_placementMenu->SetIcon( BITMAPS::align_items );
65     m_placementMenu->SetTitle( _( "Align/Distribute" ) );
66 
67     // Add all align/distribute commands
68     m_placementMenu->Add( PCB_ACTIONS::alignLeft );
69     m_placementMenu->Add( PCB_ACTIONS::alignCenterX );
70     m_placementMenu->Add( PCB_ACTIONS::alignRight );
71 
72     m_placementMenu->AppendSeparator();
73     m_placementMenu->Add( PCB_ACTIONS::alignTop );
74     m_placementMenu->Add( PCB_ACTIONS::alignCenterY );
75     m_placementMenu->Add( PCB_ACTIONS::alignBottom );
76 
77     m_placementMenu->AppendSeparator();
78     m_placementMenu->Add( PCB_ACTIONS::distributeHorizontally );
79     m_placementMenu->Add( PCB_ACTIONS::distributeVertically );
80 
81     CONDITIONAL_MENU& selToolMenu = m_selectionTool->GetToolMenu().GetMenu();
82     selToolMenu.AddMenu( m_placementMenu, SELECTION_CONDITIONS::MoreThan( 1 ) );
83 
84     return true;
85 }
86 
87 
88 template <class T>
GetBoundingBoxes(const T & aItems)89 ALIGNMENT_RECTS GetBoundingBoxes( const T& aItems )
90 {
91     ALIGNMENT_RECTS rects;
92 
93     for( EDA_ITEM* item : aItems )
94     {
95         BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
96 
97         if( item->Type() == PCB_FOOTPRINT_T )
98         {
99             FOOTPRINT* footprint = static_cast<FOOTPRINT*>( item );
100             rects.emplace_back( std::make_pair( boardItem,
101                                                 footprint->GetBoundingBox( false, false ) ) );
102         }
103         else
104         {
105             rects.emplace_back( std::make_pair( boardItem, item->GetBoundingBox() ) );
106         }
107     }
108 
109     return rects;
110 }
111 
112 
113 template< typename T >
selectTarget(ALIGNMENT_RECTS & aItems,ALIGNMENT_RECTS & aLocked,T aGetValue)114 int ALIGN_DISTRIBUTE_TOOL::selectTarget( ALIGNMENT_RECTS& aItems, ALIGNMENT_RECTS& aLocked,
115                                          T aGetValue )
116 {
117     wxPoint curPos = (wxPoint) getViewControls()->GetCursorPosition();
118 
119     // Prefer locked items to unlocked items.
120     // Secondly, prefer items under the cursor to other items.
121 
122     if( aLocked.size() >= 1 )
123     {
124         for( const ALIGNMENT_RECT& item : aLocked )
125         {
126             if( item.second.Contains( curPos ) )
127                 return aGetValue( item );
128         }
129 
130         return aGetValue( aLocked.front() );
131     }
132 
133     for( const ALIGNMENT_RECT& item : aItems )
134     {
135         if( item.second.Contains( curPos ) )
136             return aGetValue( item );
137     }
138 
139     return aGetValue( aItems.front() );
140 }
141 
142 
143 template< typename T >
GetSelections(ALIGNMENT_RECTS & aItemsToAlign,ALIGNMENT_RECTS & aLockedItems,T aCompare)144 size_t ALIGN_DISTRIBUTE_TOOL::GetSelections( ALIGNMENT_RECTS& aItemsToAlign,
145                                              ALIGNMENT_RECTS& aLockedItems, T aCompare )
146 {
147     PCB_SELECTION& selection = m_selectionTool->RequestSelection(
148             []( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
149             {
150                 // Iterate from the back so we don't have to worry about removals.
151                 for( int i = aCollector.GetCount() - 1; i >= 0; --i )
152                 {
153                     BOARD_ITEM* item = aCollector[i];
154 
155                     if( item->Type() == PCB_MARKER_T )
156                         aCollector.Remove( item );
157                 }
158             } );
159 
160     std::vector<BOARD_ITEM*> lockedItems;
161     std::vector<BOARD_ITEM*> itemsToAlign;
162 
163     for( EDA_ITEM* item : selection )
164     {
165         BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
166 
167         // We do not lock items in the footprint editor
168         if( boardItem->IsLocked() && m_frame->IsType( FRAME_PCB_EDITOR ) )
169         {
170             // Locking a pad but not the footprint means that we align the footprint using
171             // the pad position.  So we test for footprint locking here
172             if( boardItem->Type() == PCB_PAD_T && !boardItem->GetParent()->IsLocked() )
173             {
174                 itemsToAlign.push_back( boardItem );
175             }
176             else
177             {
178                 lockedItems.push_back( boardItem );
179             }
180         }
181         else
182             itemsToAlign.push_back( boardItem );
183     }
184 
185     aItemsToAlign = GetBoundingBoxes( itemsToAlign );
186     aLockedItems = GetBoundingBoxes( lockedItems );
187     std::sort( aItemsToAlign.begin(), aItemsToAlign.end(), aCompare );
188     std::sort( aLockedItems.begin(), aLockedItems.end(), aCompare );
189 
190     return aItemsToAlign.size();
191 }
192 
193 
AlignTop(const TOOL_EVENT & aEvent)194 int ALIGN_DISTRIBUTE_TOOL::AlignTop( const TOOL_EVENT& aEvent )
195 {
196     ALIGNMENT_RECTS itemsToAlign;
197     ALIGNMENT_RECTS locked_items;
198 
199     if( !GetSelections( itemsToAlign, locked_items,
200             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
201             {
202                 return ( left.second.GetTop() < right.second.GetTop() );
203             } ) )
204     {
205         return 0;
206     }
207 
208     BOARD_COMMIT commit( m_frame );
209 
210     int targetTop = selectTarget( itemsToAlign, locked_items,
211             []( const ALIGNMENT_RECT& aVal )
212             {
213                 return aVal.second.GetTop();
214             } );
215 
216     // Move the selected items
217     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
218     {
219         BOARD_ITEM* item = i.first;
220         int difference = targetTop - i.second.GetTop();
221 
222         if( item->GetParent() && item->GetParent()->IsSelected() )
223             continue;
224 
225         // Don't move a pad by itself unless editing the footprint
226         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
227             item = item->GetParent();
228 
229         commit.Stage( item, CHT_MODIFY );
230         item->Move( wxPoint( 0, difference ) );
231     }
232 
233     commit.Push( _( "Align to top" ) );
234 
235     return 0;
236 }
237 
238 
AlignBottom(const TOOL_EVENT & aEvent)239 int ALIGN_DISTRIBUTE_TOOL::AlignBottom( const TOOL_EVENT& aEvent )
240 {
241     ALIGNMENT_RECTS itemsToAlign;
242     ALIGNMENT_RECTS locked_items;
243 
244     if( !GetSelections( itemsToAlign, locked_items,
245             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
246             {
247                 return ( left.second.GetBottom() < right.second.GetBottom() );
248             } ) )
249     {
250         return 0;
251     }
252 
253     BOARD_COMMIT commit( m_frame );
254 
255     int targetBottom = selectTarget( itemsToAlign, locked_items,
256             []( const ALIGNMENT_RECT& aVal )
257             {
258                 return aVal.second.GetBottom();
259             } );
260 
261     // Move the selected items
262     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
263     {
264         int difference = targetBottom - i.second.GetBottom();
265         BOARD_ITEM* item = i.first;
266 
267         if( item->GetParent() && item->GetParent()->IsSelected() )
268             continue;
269 
270         // Don't move a pad by itself unless editing the footprint
271         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
272             item = item->GetParent();
273 
274         commit.Stage( item, CHT_MODIFY );
275         item->Move( wxPoint( 0, difference ) );
276     }
277 
278     commit.Push( _( "Align to bottom" ) );
279 
280     return 0;
281 }
282 
283 
AlignLeft(const TOOL_EVENT & aEvent)284 int ALIGN_DISTRIBUTE_TOOL::AlignLeft( const TOOL_EVENT& aEvent )
285 {
286     // Because this tool uses bounding boxes and they aren't mirrored even when
287     // the view is mirrored, we need to call the other one if mirrored.
288     if( getView()->IsMirroredX() )
289     {
290         return doAlignRight();
291     }
292     else
293     {
294         return doAlignLeft();
295     }
296 }
297 
298 
doAlignLeft()299 int ALIGN_DISTRIBUTE_TOOL::doAlignLeft()
300 {
301     ALIGNMENT_RECTS itemsToAlign;
302     ALIGNMENT_RECTS locked_items;
303 
304     if( !GetSelections( itemsToAlign, locked_items,
305             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
306             {
307                 return ( left.second.GetLeft() < right.second.GetLeft() );
308             } ) )
309     {
310         return 0;
311     }
312 
313     BOARD_COMMIT commit( m_frame );
314 
315     int targetLeft = selectTarget( itemsToAlign, locked_items,
316             []( const ALIGNMENT_RECT& aVal )
317             {
318                 return aVal.second.GetLeft();
319             } );
320 
321     // Move the selected items
322     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
323     {
324         int difference = targetLeft - i.second.GetLeft();
325         BOARD_ITEM* item = i.first;
326 
327         if( item->GetParent() && item->GetParent()->IsSelected() )
328             continue;
329 
330         // Don't move a pad by itself unless editing the footprint
331         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
332             item = item->GetParent();
333 
334         commit.Stage( item, CHT_MODIFY );
335         item->Move( wxPoint( difference, 0 ) );
336     }
337 
338     commit.Push( _( "Align to left" ) );
339 
340     return 0;
341 }
342 
343 
AlignRight(const TOOL_EVENT & aEvent)344 int ALIGN_DISTRIBUTE_TOOL::AlignRight( const TOOL_EVENT& aEvent )
345 {
346     // Because this tool uses bounding boxes and they aren't mirrored even when
347     // the view is mirrored, we need to call the other one if mirrored.
348     if( getView()->IsMirroredX() )
349     {
350         return doAlignLeft();
351     }
352     else
353     {
354         return doAlignRight();
355     }
356 }
357 
358 
doAlignRight()359 int ALIGN_DISTRIBUTE_TOOL::doAlignRight()
360 {
361     ALIGNMENT_RECTS itemsToAlign;
362     ALIGNMENT_RECTS locked_items;
363 
364     if( !GetSelections( itemsToAlign, locked_items,
365             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
366             {
367                 return ( left.second.GetRight() < right.second.GetRight() );
368             } ) )
369     {
370         return 0;
371     }
372 
373     BOARD_COMMIT commit( m_frame );
374 
375     int targetRight = selectTarget( itemsToAlign, locked_items,
376             []( const ALIGNMENT_RECT& aVal )
377             {
378                 return aVal.second.GetRight();
379             } );
380 
381     // Move the selected items
382     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
383     {
384         int difference = targetRight - i.second.GetRight();
385         BOARD_ITEM* item = i.first;
386 
387         if( item->GetParent() && item->GetParent()->IsSelected() )
388             continue;
389 
390         // Don't move a pad by itself unless editing the footprint
391         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
392             item = item->GetParent();
393 
394         commit.Stage( item, CHT_MODIFY );
395         item->Move( wxPoint( difference, 0 ) );
396     }
397 
398     commit.Push( _( "Align to right" ) );
399 
400     return 0;
401 }
402 
403 
AlignCenterX(const TOOL_EVENT & aEvent)404 int ALIGN_DISTRIBUTE_TOOL::AlignCenterX( const TOOL_EVENT& aEvent )
405 {
406     ALIGNMENT_RECTS itemsToAlign;
407     ALIGNMENT_RECTS locked_items;
408 
409     if( !GetSelections( itemsToAlign, locked_items,
410             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
411             {
412                 return ( left.second.GetCenter().x < right.second.GetCenter().x );
413             } ) )
414     {
415         return 0;
416     }
417 
418     BOARD_COMMIT commit( m_frame );
419 
420     int targetX = selectTarget( itemsToAlign, locked_items,
421             []( const ALIGNMENT_RECT& aVal )
422             {
423                 return aVal.second.GetCenter().x;
424             } );
425 
426     // Move the selected items
427     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
428     {
429         int difference = targetX - i.second.GetCenter().x;
430         BOARD_ITEM* item = i.first;
431 
432         if( item->GetParent() && item->GetParent()->IsSelected() )
433             continue;
434 
435         // Don't move a pad by itself unless editing the footprint
436         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
437             item = item->GetParent();
438 
439         commit.Stage( item, CHT_MODIFY );
440         item->Move( wxPoint( difference, 0 ) );
441     }
442 
443     commit.Push( _( "Align to middle" ) );
444 
445     return 0;
446 }
447 
448 
AlignCenterY(const TOOL_EVENT & aEvent)449 int ALIGN_DISTRIBUTE_TOOL::AlignCenterY( const TOOL_EVENT& aEvent )
450 {
451     ALIGNMENT_RECTS itemsToAlign;
452     ALIGNMENT_RECTS locked_items;
453 
454     if( !GetSelections( itemsToAlign, locked_items,
455             []( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
456             {
457                 return ( left.second.GetCenter().y < right.second.GetCenter().y );
458             } ) )
459     {
460         return 0;
461     }
462 
463     BOARD_COMMIT commit( m_frame );
464 
465     int targetY = selectTarget( itemsToAlign, locked_items,
466             []( const ALIGNMENT_RECT& aVal )
467             {
468                 return aVal.second.GetCenter().y;
469             } );
470 
471     // Move the selected items
472     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToAlign )
473     {
474         int difference = targetY - i.second.GetCenter().y;
475         BOARD_ITEM* item = i.first;
476 
477         if( item->GetParent() && item->GetParent()->IsSelected() )
478             continue;
479 
480         // Don't move a pad by itself unless editing the footprint
481         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
482             item = item->GetParent();
483 
484         commit.Stage( item, CHT_MODIFY );
485         item->Move( wxPoint( 0, difference ) );
486     }
487 
488     commit.Push( _( "Align to center" ) );
489 
490     return 0;
491 }
492 
493 
DistributeHorizontally(const TOOL_EVENT & aEvent)494 int ALIGN_DISTRIBUTE_TOOL::DistributeHorizontally( const TOOL_EVENT& aEvent )
495 {
496     PCB_SELECTION& selection = m_selectionTool->RequestSelection(
497             []( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
498             {
499                 // Iterate from the back so we don't have to worry about removals.
500                 for( int i = aCollector.GetCount() - 1; i >= 0; --i )
501                 {
502                     BOARD_ITEM* item = aCollector[i];
503 
504                     if( item->Type() == PCB_MARKER_T )
505                         aCollector.Remove( item );
506                 }
507             },
508             m_frame->IsType( FRAME_PCB_EDITOR ) /* prompt user regarding locked items */ );
509 
510     if( selection.Size() <= 1 )
511         return 0;
512 
513     BOARD_COMMIT    commit( m_frame );
514     ALIGNMENT_RECTS itemsToDistribute = GetBoundingBoxes( selection );
515 
516     // find the last item by reverse sorting
517     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
518             [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
519             {
520                 return ( left.second.GetRight() > right.second.GetRight() );
521             } );
522 
523     BOARD_ITEM* lastItem = itemsToDistribute.begin()->first;
524     const int   maxRight = itemsToDistribute.begin()->second.GetRight();
525 
526     // sort to get starting order
527     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
528             [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
529             {
530                 return ( left.second.GetX() < right.second.GetX() );
531             } );
532 
533     const int minX = itemsToDistribute.begin()->second.GetX();
534     int       totalGap = maxRight - minX;
535     int       totalWidth = 0;
536 
537     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
538         totalWidth += i.second.GetWidth();
539 
540     if( totalGap < totalWidth )
541     {
542         // the width of the items exceeds the gap (overlapping items) -> use center point spacing
543         doDistributeCentersHorizontally( itemsToDistribute, commit );
544     }
545     else
546     {
547         totalGap -= totalWidth;
548         doDistributeGapsHorizontally( itemsToDistribute, commit, lastItem, totalGap );
549     }
550 
551     commit.Push( _( "Distribute horizontally" ) );
552 
553     return 0;
554 }
555 
556 
doDistributeGapsHorizontally(ALIGNMENT_RECTS & itemsToDistribute,BOARD_COMMIT & aCommit,const BOARD_ITEM * lastItem,int totalGap) const557 void ALIGN_DISTRIBUTE_TOOL::doDistributeGapsHorizontally( ALIGNMENT_RECTS& itemsToDistribute,
558                                                           BOARD_COMMIT& aCommit,
559                                                           const BOARD_ITEM* lastItem,
560                                                           int totalGap ) const
561 {
562     const int itemGap = totalGap / ( itemsToDistribute.size() - 1 );
563     int       targetX = itemsToDistribute.begin()->second.GetX();
564 
565     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
566     {
567         BOARD_ITEM* item = i.first;
568 
569         // cover the corner case where the last item is wider than the previous item and gap
570         if( lastItem == item )
571             continue;
572 
573         if( item->GetParent() && item->GetParent()->IsSelected() )
574             continue;
575 
576         // Don't move a pad by itself unless editing the footprint
577         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
578             item = item->GetParent();
579 
580         int difference = targetX - i.second.GetX();
581         aCommit.Stage( item, CHT_MODIFY );
582         item->Move( wxPoint( difference, 0 ) );
583         targetX += ( i.second.GetWidth() + itemGap );
584     }
585 }
586 
587 
doDistributeCentersHorizontally(ALIGNMENT_RECTS & itemsToDistribute,BOARD_COMMIT & aCommit) const588 void ALIGN_DISTRIBUTE_TOOL::doDistributeCentersHorizontally( ALIGNMENT_RECTS &itemsToDistribute,
589                                                              BOARD_COMMIT& aCommit ) const
590 {
591     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
592             [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
593             {
594                 return ( left.second.GetCenter().x < right.second.GetCenter().x );
595             } );
596 
597     const int totalGap = ( itemsToDistribute.end()-1 )->second.GetCenter().x
598                           - itemsToDistribute.begin()->second.GetCenter().x;
599     const int itemGap = totalGap / ( itemsToDistribute.size() - 1 );
600     int       targetX = itemsToDistribute.begin()->second.GetCenter().x;
601 
602     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
603     {
604         BOARD_ITEM* item = i.first;
605 
606         if( item->GetParent() && item->GetParent()->IsSelected() )
607             continue;
608 
609         // Don't move a pad by itself unless editing the footprint
610         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
611             item = item->GetParent();
612 
613         int difference = targetX - i.second.GetCenter().x;
614         aCommit.Stage( item, CHT_MODIFY );
615         item->Move( wxPoint( difference, 0 ) );
616         targetX += ( itemGap );
617     }
618 }
619 
620 
DistributeVertically(const TOOL_EVENT & aEvent)621 int ALIGN_DISTRIBUTE_TOOL::DistributeVertically( const TOOL_EVENT& aEvent )
622 {
623     PCB_SELECTION& selection = m_selectionTool->RequestSelection(
624             []( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
625             {
626                 // Iterate from the back so we don't have to worry about removals.
627                 for( int i = aCollector.GetCount() - 1; i >= 0; --i )
628                 {
629                     BOARD_ITEM* item = aCollector[i];
630 
631                     if( item->Type() == PCB_MARKER_T )
632                         aCollector.Remove( item );
633                 }
634             },
635             m_frame->IsType( FRAME_PCB_EDITOR ) /* prompt user regarding locked items */ );
636 
637     if( selection.Size() <= 1 )
638         return 0;
639 
640     BOARD_COMMIT    commit( m_frame );
641     ALIGNMENT_RECTS itemsToDistribute = GetBoundingBoxes( selection );
642 
643     // find the last item by reverse sorting
644     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
645         [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
646         {
647             return ( left.second.GetBottom() > right.second.GetBottom() );
648         } );
649 
650     BOARD_ITEM* lastItem = itemsToDistribute.begin()->first;
651     const int   maxBottom = itemsToDistribute.begin()->second.GetBottom();
652 
653     // sort to get starting order
654     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
655         [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
656         {
657             return ( left.second.GetCenter().y < right.second.GetCenter().y );
658         } );
659 
660     int minY = itemsToDistribute.begin()->second.GetY();
661     int totalGap = maxBottom - minY;
662     int totalHeight = 0;
663 
664     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
665         totalHeight += i.second.GetHeight();
666 
667     if( totalGap < totalHeight )
668     {
669         // the width of the items exceeds the gap (overlapping items) -> use center point spacing
670         doDistributeCentersVertically( itemsToDistribute, commit );
671     }
672     else
673     {
674         totalGap -= totalHeight;
675         doDistributeGapsVertically( itemsToDistribute, commit, lastItem, totalGap );
676     }
677 
678     commit.Push( _( "Distribute vertically" ) );
679 
680     return 0;
681 }
682 
683 
doDistributeGapsVertically(ALIGNMENT_RECTS & itemsToDistribute,BOARD_COMMIT & aCommit,const BOARD_ITEM * lastItem,int totalGap) const684 void ALIGN_DISTRIBUTE_TOOL::doDistributeGapsVertically( ALIGNMENT_RECTS& itemsToDistribute,
685                                                         BOARD_COMMIT& aCommit,
686                                                         const BOARD_ITEM* lastItem,
687                                                         int totalGap ) const
688 {
689     const int itemGap = totalGap / ( itemsToDistribute.size() - 1 );
690     int       targetY = itemsToDistribute.begin()->second.GetY();
691 
692     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
693     {
694         BOARD_ITEM* item = i.first;
695 
696         // cover the corner case where the last item is wider than the previous item and gap
697         if( lastItem == item )
698             continue;
699 
700         if( item->GetParent() && item->GetParent()->IsSelected() )
701             continue;
702 
703         // Don't move a pad by itself unless editing the footprint
704         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
705             item = item->GetParent();
706 
707         int difference = targetY - i.second.GetY();
708         aCommit.Stage( item, CHT_MODIFY );
709         item->Move( wxPoint( 0, difference ) );
710         targetY += ( i.second.GetHeight() + itemGap );
711     }
712 }
713 
714 
doDistributeCentersVertically(ALIGNMENT_RECTS & itemsToDistribute,BOARD_COMMIT & aCommit) const715 void ALIGN_DISTRIBUTE_TOOL::doDistributeCentersVertically( ALIGNMENT_RECTS& itemsToDistribute,
716                                                            BOARD_COMMIT& aCommit ) const
717 {
718     std::sort( itemsToDistribute.begin(), itemsToDistribute.end(),
719         [] ( const ALIGNMENT_RECT left, const ALIGNMENT_RECT right)
720         {
721             return ( left.second.GetCenter().y < right.second.GetCenter().y );
722         } );
723 
724     const int totalGap = ( itemsToDistribute.end()-1 )->second.GetCenter().y
725                           - itemsToDistribute.begin()->second.GetCenter().y;
726     const int itemGap  = totalGap / ( itemsToDistribute.size() - 1 );
727     int       targetY  = itemsToDistribute.begin()->second.GetCenter().y;
728 
729     for( std::pair<BOARD_ITEM*, EDA_RECT>& i : itemsToDistribute )
730     {
731         BOARD_ITEM* item = i.first;
732 
733         if( item->GetParent() && item->GetParent()->IsSelected() )
734             continue;
735 
736         // Don't move a pad by itself unless editing the footprint
737         if( item->Type() == PCB_PAD_T && m_frame->IsType( FRAME_PCB_EDITOR ) )
738             item = item->GetParent();
739 
740         int difference = targetY - i.second.GetCenter().y;
741         aCommit.Stage( item, CHT_MODIFY );
742         item->Move( wxPoint( 0, difference ) );
743         targetY += ( itemGap );
744     }
745 }
746 
747 
setTransitions()748 void ALIGN_DISTRIBUTE_TOOL::setTransitions()
749 {
750     Go( &ALIGN_DISTRIBUTE_TOOL::AlignTop,               PCB_ACTIONS::alignTop.MakeEvent() );
751     Go( &ALIGN_DISTRIBUTE_TOOL::AlignBottom,            PCB_ACTIONS::alignBottom.MakeEvent() );
752     Go( &ALIGN_DISTRIBUTE_TOOL::AlignLeft,              PCB_ACTIONS::alignLeft.MakeEvent() );
753     Go( &ALIGN_DISTRIBUTE_TOOL::AlignRight,             PCB_ACTIONS::alignRight.MakeEvent() );
754     Go( &ALIGN_DISTRIBUTE_TOOL::AlignCenterX,           PCB_ACTIONS::alignCenterX.MakeEvent() );
755     Go( &ALIGN_DISTRIBUTE_TOOL::AlignCenterY,           PCB_ACTIONS::alignCenterY.MakeEvent() );
756 
757     Go( &ALIGN_DISTRIBUTE_TOOL::DistributeHorizontally,
758         PCB_ACTIONS::distributeHorizontally.MakeEvent() );
759     Go( &ALIGN_DISTRIBUTE_TOOL::DistributeVertically,
760         PCB_ACTIONS::distributeVertically.MakeEvent() );
761 }
762