kicad-source/pcbnew/tools/pcb_selection_tool.cpp
Jeff Young 6f389fd320 Tighten parent/child undo/redo architecture.
Always look for pre-existing undo/redo record.  Checking for IsNew()
is less robust and should be avoided.  Also moves the checking to a
location where it will be easier to ensure that it's uniform.

Push get-undo-level-item processing down a level so it is uniformly
called.

Make sure tables & labels are uniformly handled.

Remove incorrect usage of Get/SetGroupId() for storing lastPin
(which we don't use anyway).

Lists of deleted and changed items MUST include the screen pointer.
An item could be changed on one screen but not on another.

Also tightens handling of PCB_NETINFO_T items, which are not in the
view.

Also fixes a bug where there is no increment parameter if you assign
the base increment command to a hotkey.
(This was discovered while testing the above changes.)

Also fixes a bug where delete during a move in PCB Editor did an
undo instead of a delete.
(Again, found while testing above.)

An experiment was also run to collapse shared parts of SCH_EDIT_FRAME
and SYMBOL_EDITOR_FRAME into SCH_BASE_EDIT_FRAME.  However, sharing the
undo code actually increased complexity, and there was very little else
of value in SCH_BASE_EDIT_FRAME (other than the Increment() routines).
2025-08-18 19:20:09 +01:00

4307 lines
134 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2013-2017 CERN
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
* @author Maciej Suminski <maciej.suminski@cern.ch>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <limits>
#include <cmath>
#include <functional>
#include <stack>
using namespace std::placeholders;
#include <advanced_config.h>
#include <macros.h>
#include <board.h>
#include <board_design_settings.h>
#include <pcb_table.h>
#include <pcb_tablecell.h>
#include <pcb_marker.h>
#include <pcb_generator.h>
#include <zone.h>
#include <collectors.h>
#include <dialog_filter_selection.h>
#include <view/view_controls.h>
#include <gal/painter.h>
#include <router/router_tool.h>
#include <pcbnew_settings.h>
#include <tool/tool_event.h>
#include <tool/tool_manager.h>
#include <tools/tool_event_utils.h>
#include <tools/pcb_point_editor.h>
#include <tools/pcb_selection_tool.h>
#include <tools/pcb_actions.h>
#include <tools/board_inspection_tool.h>
#include <ratsnest/ratsnest_data.h>
#include <geometry/geometry_utils.h>
#include <wx/event.h>
#include <wx/timer.h>
#include <wx/log.h>
#include <wx/debug.h>
#include <core/profile.h>
#include <math/vector2wx.h>
struct LAYER_OPACITY_ITEM
{
PCB_LAYER_ID m_Layer;
double m_Opacity;
const BOARD_ITEM* m_Item;;
};
class SELECT_MENU : public ACTION_MENU
{
public:
SELECT_MENU() :
ACTION_MENU( true )
{
SetTitle( _( "Select" ) );
Add( PCB_ACTIONS::filterSelection );
AppendSeparator();
Add( PCB_ACTIONS::selectConnection );
Add( PCB_ACTIONS::selectNet );
// This could be enabled if we have better logic for picking the target net with the mouse
// Add( PCB_ACTIONS::deselectNet );
Add( PCB_ACTIONS::selectSameSheet );
Add( PCB_ACTIONS::selectOnSchematic );
Add( PCB_ACTIONS::selectUnconnected );
Add( PCB_ACTIONS::grabUnconnected );
}
private:
ACTION_MENU* create() const override
{
return new SELECT_MENU();
}
};
/**
* Private implementation of firewalled private data.
*/
class PCB_SELECTION_TOOL::PRIV
{
public:
DIALOG_FILTER_SELECTION::OPTIONS m_filterOpts;
};
PCB_SELECTION_TOOL::PCB_SELECTION_TOOL() :
SELECTION_TOOL( "common.InteractiveSelection" ),
m_frame( nullptr ),
m_isFootprintEditor( false ),
m_nonModifiedCursor( KICURSOR::ARROW ),
m_enteredGroup( nullptr ),
m_selectionMode( SELECTION_MODE::INSIDE_RECTANGLE ),
m_priv( std::make_unique<PRIV>() )
{
m_filter.lockedItems = false;
m_filter.footprints = true;
m_filter.text = true;
m_filter.tracks = true;
m_filter.vias = true;
m_filter.pads = true;
m_filter.graphics = true;
m_filter.zones = true;
m_filter.keepouts = true;
m_filter.dimensions = true;
m_filter.otherItems = true;
}
PCB_SELECTION_TOOL::~PCB_SELECTION_TOOL()
{
getView()->Remove( &m_selection );
getView()->Remove( &m_enteredGroupOverlay );
Disconnect( wxEVT_TIMER, wxTimerEventHandler( PCB_SELECTION_TOOL::onDisambiguationExpire ), nullptr, this );
}
bool PCB_SELECTION_TOOL::Init()
{
PCB_BASE_FRAME* frame = getEditFrame<PCB_BASE_FRAME>();
if( frame && frame->IsType( FRAME_FOOTPRINT_VIEWER ) )
{
frame->AddStandardSubMenus( *m_menu.get() );
return true;
}
std::shared_ptr<SELECT_MENU> selectMenu = std::make_shared<SELECT_MENU>();
selectMenu->SetTool( this );
m_menu->RegisterSubMenu( selectMenu );
static const std::vector<KICAD_T> tableCellTypes = { PCB_TABLECELL_T };
auto& menu = m_menu->GetMenu();
auto activeToolCondition =
[ frame ] ( const SELECTION& aSel )
{
return !frame->ToolStackIsEmpty();
};
auto haveHighlight =
[&]( const SELECTION& sel )
{
KIGFX::RENDER_SETTINGS* cfg = m_toolMgr->GetView()->GetPainter()->GetSettings();
return !cfg->GetHighlightNetCodes().empty();
};
auto groupEnterCondition =
SELECTION_CONDITIONS::Count( 1 ) && SELECTION_CONDITIONS::HasType( PCB_GROUP_T );
auto inGroupCondition =
[this] ( const SELECTION& )
{
return m_enteredGroup != nullptr;
};
auto tableCellSelection = SELECTION_CONDITIONS::MoreThan( 0 )
&& SELECTION_CONDITIONS::OnlyTypes( tableCellTypes );
if( frame && frame->IsType( FRAME_PCB_EDITOR ) )
{
menu.AddMenu( selectMenu.get(), SELECTION_CONDITIONS::NotEmpty );
menu.AddSeparator( 1000 );
}
// "Cancel" goes at the top of the context menu when a tool is active
menu.AddItem( ACTIONS::cancelInteractive, activeToolCondition, 1 );
menu.AddItem( ACTIONS::groupEnter, groupEnterCondition, 1 );
menu.AddItem( ACTIONS::groupLeave, inGroupCondition, 1 );
menu.AddItem( PCB_ACTIONS::placeLinkedDesignBlock, groupEnterCondition, 1 );
menu.AddItem( PCB_ACTIONS::saveToLinkedDesignBlock, groupEnterCondition, 1 );
menu.AddItem( PCB_ACTIONS::clearHighlight, haveHighlight, 1 );
menu.AddSeparator( haveHighlight, 1 );
menu.AddItem( ACTIONS::selectColumns, tableCellSelection, 2 );
menu.AddItem( ACTIONS::selectRows, tableCellSelection, 2 );
menu.AddItem( ACTIONS::selectTable, tableCellSelection, 2 );
menu.AddSeparator( 1 );
if( frame )
frame->AddStandardSubMenus( *m_menu.get() );
m_disambiguateTimer.SetOwner( this );
Connect( m_disambiguateTimer.GetId(), wxEVT_TIMER,
wxTimerEventHandler( PCB_SELECTION_TOOL::onDisambiguationExpire ), nullptr, this );
return true;
}
void PCB_SELECTION_TOOL::Reset( RESET_REASON aReason )
{
m_frame = getEditFrame<PCB_BASE_FRAME>();
m_isFootprintEditor = m_frame->IsType( FRAME_FOOTPRINT_EDITOR );
if( aReason != TOOL_BASE::REDRAW )
{
if( m_enteredGroup )
ExitGroup();
// Deselect any item being currently in edit, to avoid unexpected behavior and remove
// pointers to the selected items from containers.
ClearSelection( true );
}
if( aReason == TOOL_BASE::MODEL_RELOAD )
getView()->GetPainter()->GetSettings()->SetHighlight( false );
// Reinsert the VIEW_GROUP, in case it was removed from the VIEW
view()->Remove( &m_selection );
view()->Add( &m_selection );
view()->Remove( &m_enteredGroupOverlay );
view()->Add( &m_enteredGroupOverlay );
}
void PCB_SELECTION_TOOL::OnIdle( wxIdleEvent& aEvent )
{
if( m_frame->ToolStackIsEmpty() && !m_multiple )
{
wxMouseState keyboardState = wxGetMouseState();
setModifiersState( keyboardState.ShiftDown(), keyboardState.ControlDown(), keyboardState.AltDown() );
if( m_additive )
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::ADD );
else if( m_subtractive )
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SUBTRACT );
else if( m_exclusive_or )
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::XOR );
else
m_frame->GetCanvas()->SetCurrentCursor( m_nonModifiedCursor );
}
}
int PCB_SELECTION_TOOL::Main( const TOOL_EVENT& aEvent )
{
// Main loop: keep receiving events
while( TOOL_EVENT* evt = Wait() )
{
MOUSE_DRAG_ACTION dragAction = m_frame->GetDragAction();
TRACK_DRAG_ACTION trackDragAction = TRACK_DRAG_ACTION::MOVE;
if( PCBNEW_SETTINGS* cfg = m_frame->GetPcbNewSettings() )
trackDragAction = cfg->m_TrackDragAction;
// on left click, a selection is made, depending on modifiers ALT, SHIFT, CTRL:
setModifiersState( evt->Modifier( MD_SHIFT ), evt->Modifier( MD_CTRL ), evt->Modifier( MD_ALT ) );
PCB_BASE_FRAME* frame = getEditFrame<PCB_BASE_FRAME>();
bool brd_editor = frame && frame->IsType( FRAME_PCB_EDITOR );
ROUTER_TOOL* router = m_toolMgr->GetTool<ROUTER_TOOL>();
// If the router tool is active, don't override
if( router && router->IsToolActive() && router->RoutingInProgress() )
{
evt->SetPassEvent();
}
else if( evt->IsMouseDown( BUT_LEFT ) )
{
// Avoid triggering when running under other tools
PCB_POINT_EDITOR *pt_tool = m_toolMgr->GetTool<PCB_POINT_EDITOR>();
if( m_frame->ToolStackIsEmpty() && pt_tool && !pt_tool->HasPoint() )
{
m_originalCursor = m_toolMgr->GetMousePosition();
m_disambiguateTimer.StartOnce( ADVANCED_CFG::GetCfg().m_DisambiguationMenuDelay );
}
}
else if( evt->IsClick( BUT_LEFT ) )
{
// If there is no disambiguation, this routine is still running and will
// register a `click` event when released
if( m_disambiguateTimer.IsRunning() )
{
m_disambiguateTimer.Stop();
// Single click? Select single object
if( m_highlight_modifier && brd_editor )
{
m_toolMgr->RunAction( PCB_ACTIONS::highlightNet );
}
else
{
m_frame->ClearFocus();
selectPoint( evt->Position() );
}
}
m_canceledMenu = false;
}
else if( evt->IsClick( BUT_RIGHT ) )
{
m_disambiguateTimer.Stop();
// Right click? if there is any object - show the context menu
bool selectionCancelled = false;
if( m_selection.Empty() )
{
selectPoint( evt->Position(), false, &selectionCancelled );
m_selection.SetIsHover( true );
}
// Show selection before opening menu
m_frame->GetCanvas()->ForceRefresh();
if( !selectionCancelled )
{
m_toolMgr->VetoContextMenuMouseWarp();
m_menu->ShowContextMenu( m_selection );
}
}
else if( evt->IsDblClick( BUT_LEFT ) )
{
m_disambiguateTimer.Stop();
// Double clicks make no sense in the footprint viewer
if( frame && frame->IsType( FRAME_FOOTPRINT_VIEWER ) )
{
evt->SetPassEvent();
continue;
}
// Double click? Display the properties window
m_frame->ClearFocus();
if( m_selection.Empty() )
selectPoint( evt->Position() );
if( m_selection.GetSize() == 1 && m_selection[0]->Type() == PCB_GROUP_T )
EnterGroup();
else
m_toolMgr->RunAction( PCB_ACTIONS::properties );
}
else if( evt->IsDblClick( BUT_MIDDLE ) )
{
// Middle double click? Do zoom to fit or zoom to objects
if( evt->Modifier( MD_CTRL ) ) // Is CTRL key down?
m_toolMgr->RunAction( ACTIONS::zoomFitObjects );
else
m_toolMgr->RunAction( ACTIONS::zoomFitScreen );
}
else if( evt->Action() == TA_MOUSE_WHEEL )
{
int field = -1;
if( evt->Modifier() == ( MD_SHIFT | MD_ALT ) )
field = 0;
else if( evt->Modifier() == ( MD_CTRL | MD_ALT ) )
field = 1;
// any more?
if( field >= 0 )
{
const int delta = evt->Parameter<int>();
ACTIONS::INCREMENT params { delta > 0 ? 1 : -1, field };
m_toolMgr->RunAction( ACTIONS::increment, params );
}
}
else if( evt->IsDrag( BUT_LEFT ) )
{
m_disambiguateTimer.Stop();
// Is another tool already moving a new object? Don't allow a drag start
if( !m_selection.Empty() && m_selection[0]->HasFlag( IS_NEW | IS_MOVING ) )
{
evt->SetPassEvent();
continue;
}
// Drag with LMB? Select multiple objects (or at least draw a selection box)
// or drag them
m_frame->ClearFocus();
m_toolMgr->ProcessEvent( EVENTS::InhibitSelectionEditing );
GENERAL_COLLECTOR hoverCells;
if( m_isFootprintEditor && board()->GetFirstFootprint() )
{
hoverCells.Collect( board()->GetFirstFootprint(), { PCB_TABLECELL_T }, evt->DragOrigin(),
getCollectorsGuide() );
}
else
{
hoverCells.Collect( board(), { PCB_TABLECELL_T }, evt->DragOrigin(), getCollectorsGuide() );
}
if( hoverCells.GetCount() )
{
if( m_selection.Empty() || SELECTION_CONDITIONS::OnlyTypes( { PCB_TABLECELL_T } )( m_selection ) )
{
selectTableCells( static_cast<PCB_TABLE*>( hoverCells[0]->GetParent() ) );
}
else
{
m_toolMgr->RunAction( PCB_ACTIONS::move );
}
}
else if( ( hasModifier() || dragAction == MOUSE_DRAG_ACTION::SELECT )
|| ( m_selection.Empty() && dragAction != MOUSE_DRAG_ACTION::DRAG_ANY ) )
{
if( m_selectionMode == SELECTION_MODE::INSIDE_RECTANGLE
|| m_selectionMode == SELECTION_MODE::TOUCHING_RECTANGLE )
{
SelectRectArea( aEvent );
}
else if( m_selectionMode == SELECTION_MODE::INSIDE_LASSO
|| m_selectionMode == SELECTION_MODE::TOUCHING_LASSO )
{
SelectPolyArea( aEvent );
}
else
{
wxASSERT_MSG( false, wxT( "Unknown selection mode" ) );
SelectRectArea( aEvent );
}
}
else
{
// Don't allow starting a drag from a zone filled area that isn't already selected
auto zoneFilledAreaFilter =
[]( const VECTOR2I& aWhere, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* aTool )
{
int accuracy = aCollector.GetGuide()->Accuracy();
std::set<EDA_ITEM*> remove;
for( EDA_ITEM* item : aCollector )
{
if( item->Type() == PCB_ZONE_T )
{
ZONE* zone = static_cast<ZONE*>( item );
if( !zone->HitTestForCorner( aWhere, accuracy * 2 )
&& !zone->HitTestForEdge( aWhere, accuracy ) )
{
remove.insert( zone );
}
}
}
for( EDA_ITEM* item : remove )
aCollector.Remove( item );
};
// See if we can drag before falling back to SelectRectArea()
bool doDrag = false;
if( evt->HasPosition() )
{
if( m_selection.Empty()
&& selectPoint( evt->DragOrigin(), false, nullptr, zoneFilledAreaFilter ) )
{
m_selection.SetIsHover( true );
doDrag = true;
}
// Check if dragging has started within any of selected items bounding box.
else if( evt->HasPosition() && selectionContains( evt->DragOrigin() ) )
{
doDrag = true;
}
}
if( doDrag )
{
size_t segs = m_selection.CountType( PCB_TRACE_T );
size_t arcs = m_selection.CountType( PCB_ARC_T );
size_t vias = m_selection.CountType( PCB_VIA_T );
// Note: multi-track dragging is currently supported, but not multi-via
bool routable = ( segs >= 1 || arcs >= 1 || vias == 1 )
&& ( segs + arcs + vias == m_selection.GetSize() );
if( routable && trackDragAction == TRACK_DRAG_ACTION::DRAG )
m_toolMgr->RunAction( PCB_ACTIONS::drag45Degree );
else if( routable && trackDragAction == TRACK_DRAG_ACTION::DRAG_FREE_ANGLE )
m_toolMgr->RunAction( PCB_ACTIONS::dragFreeAngle );
else
m_toolMgr->RunAction( PCB_ACTIONS::move );
}
else
{
// Otherwise drag a selection box
SelectRectArea( aEvent );
}
}
}
else if( evt->IsCancel() )
{
m_disambiguateTimer.Stop();
m_frame->ClearFocus();
if( !GetSelection().Empty() )
{
ClearSelection();
}
else if( evt->FirstResponder() == this && evt->GetCommandId() == (int) WXK_ESCAPE )
{
if( m_enteredGroup )
{
ExitGroup();
}
else
{
BOARD_INSPECTION_TOOL* controller = m_toolMgr->GetTool<BOARD_INSPECTION_TOOL>();
try
{
if( controller && m_frame->GetPcbNewSettings()->m_ESCClearsNetHighlight )
controller->ClearHighlight( *evt );
}
catch( const std::runtime_error& e )
{
wxCHECK_MSG( false, 0, e.what() );
}
}
}
}
else
{
evt->SetPassEvent();
}
if( m_frame->ToolStackIsEmpty() )
{
// move cursor prediction
if( !hasModifier()
&& dragAction == MOUSE_DRAG_ACTION::DRAG_SELECTED
&& !m_selection.Empty()
&& evt->HasPosition()
&& selectionContains( evt->Position() ) )
{
m_nonModifiedCursor = KICURSOR::MOVING;
}
else
{
m_nonModifiedCursor = KICURSOR::ARROW;
}
}
}
// Shutting down; clear the selection
m_selection.Clear();
m_disambiguateTimer.Stop();
return 0;
}
void PCB_SELECTION_TOOL::EnterGroup()
{
wxCHECK_RET( m_selection.GetSize() == 1 && m_selection[0]->Type() == PCB_GROUP_T,
wxT( "EnterGroup called when selection is not a single group" ) );
PCB_GROUP* aGroup = static_cast<PCB_GROUP*>( m_selection[0] );
if( m_enteredGroup != nullptr )
ExitGroup();
ClearSelection();
m_enteredGroup = aGroup;
m_enteredGroup->SetFlags( ENTERED );
for( EDA_ITEM* member : m_enteredGroup->GetItems() )
select( member );
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
view()->Hide( m_enteredGroup, true );
m_enteredGroupOverlay.Add( m_enteredGroup );
view()->Update( &m_enteredGroupOverlay );
}
void PCB_SELECTION_TOOL::ExitGroup( bool aSelectGroup )
{
// Only continue if there is a group entered
if( m_enteredGroup == nullptr )
return;
m_enteredGroup->ClearFlags( ENTERED );
view()->Hide( m_enteredGroup, false );
ClearSelection();
if( aSelectGroup )
{
select( m_enteredGroup );
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
m_enteredGroupOverlay.Clear();
m_enteredGroup = nullptr;
view()->Update( &m_enteredGroupOverlay );
}
PCB_SELECTION& PCB_SELECTION_TOOL::GetSelection()
{
return m_selection;
}
PCB_SELECTION& PCB_SELECTION_TOOL::RequestSelection( CLIENT_SELECTION_FILTER aClientFilter,
bool aConfirmLockedItems )
{
bool selectionEmpty = m_selection.Empty();
m_selection.SetIsHover( selectionEmpty );
if( selectionEmpty )
{
m_toolMgr->RunAction( ACTIONS::selectionCursor, aClientFilter );
m_selection.ClearReferencePoint();
}
if( aClientFilter )
{
enum DISPOSITION { BEFORE = 1, AFTER, BOTH };
std::map<EDA_ITEM*, DISPOSITION> itemDispositions;
GENERAL_COLLECTORS_GUIDE guide = getCollectorsGuide();
GENERAL_COLLECTOR collector;
collector.SetGuide( &guide );
for( EDA_ITEM* item : m_selection )
{
collector.Append( item );
itemDispositions[ item ] = BEFORE;
}
aClientFilter( VECTOR2I(), collector, this );
for( EDA_ITEM* item : collector )
{
if( itemDispositions.count( item ) )
itemDispositions[ item ] = BOTH;
else
itemDispositions[ item ] = AFTER;
}
// Unhighlight the BEFORE items before highlighting the AFTER items.
// This is so that in the case of groups, if aClientFilter replaces a selection
// with the enclosing group, the unhighlight of the element doesn't undo the
// recursive highlighting of that element by the group.
for( std::pair<EDA_ITEM* const, DISPOSITION> itemDisposition : itemDispositions )
{
EDA_ITEM* item = itemDisposition.first;
DISPOSITION disposition = itemDisposition.second;
if( disposition == BEFORE )
unhighlight( item, SELECTED, &m_selection );
}
for( std::pair<EDA_ITEM* const, DISPOSITION> itemDisposition : itemDispositions )
{
EDA_ITEM* item = itemDisposition.first;
DISPOSITION disposition = itemDisposition.second;
// Note that we must re-highlight even previously-highlighted items
// (ie: disposition BOTH) in case we removed any of their children.
if( disposition == AFTER || disposition == BOTH )
highlight( item, SELECTED, &m_selection );
}
m_frame->GetCanvas()->ForceRefresh();
}
if( aConfirmLockedItems && !m_frame->GetOverrideLocks() )
{
for( EDA_ITEM* item : m_selection )
{
if( !item->IsBOARD_ITEM() )
continue;
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
bool lockedDescendant = false;
boardItem->RunOnChildren(
[&]( BOARD_ITEM* curr_item )
{
if( curr_item->IsLocked() )
lockedDescendant = true;
},
RECURSE_MODE::RECURSE );
if( boardItem->IsLocked() || lockedDescendant )
unselect( boardItem );
}
}
return m_selection;
}
const GENERAL_COLLECTORS_GUIDE PCB_SELECTION_TOOL::getCollectorsGuide() const
{
GENERAL_COLLECTORS_GUIDE guide( board()->GetVisibleLayers(), (PCB_LAYER_ID) view()->GetTopLayer(),
view() );
bool padsDisabled = !board()->IsElementVisible( LAYER_PADS );
// account for the globals
guide.SetIgnoreFPTextOnBack( !board()->IsElementVisible( LAYER_FP_TEXT ) );
guide.SetIgnoreFPTextOnFront( !board()->IsElementVisible( LAYER_FP_TEXT ) );
guide.SetIgnoreFootprintsOnBack( !board()->IsElementVisible( LAYER_FOOTPRINTS_BK ) );
guide.SetIgnoreFootprintsOnFront( !board()->IsElementVisible( LAYER_FOOTPRINTS_FR ) );
guide.SetIgnorePadsOnBack( padsDisabled );
guide.SetIgnorePadsOnFront( padsDisabled );
guide.SetIgnoreThroughHolePads( padsDisabled );
guide.SetIgnoreFPValues( !board()->IsElementVisible( LAYER_FP_VALUES ) );
guide.SetIgnoreFPReferences( !board()->IsElementVisible( LAYER_FP_REFERENCES ) );
guide.SetIgnoreThroughVias( ! board()->IsElementVisible( LAYER_VIAS ) );
guide.SetIgnoreBlindBuriedVias( ! board()->IsElementVisible( LAYER_VIAS ) );
guide.SetIgnoreMicroVias( ! board()->IsElementVisible( LAYER_VIAS ) );
guide.SetIgnoreTracks( ! board()->IsElementVisible( LAYER_TRACKS ) );
return guide;
}
bool PCB_SELECTION_TOOL::ctrlClickHighlights()
{
return m_frame && m_frame->GetPcbNewSettings()->m_CtrlClickHighlight && !m_isFootprintEditor;
}
bool PCB_SELECTION_TOOL::selectPoint( const VECTOR2I& aWhere, bool aOnDrag, bool* aSelectionCancelledFlag,
CLIENT_SELECTION_FILTER aClientFilter )
{
GENERAL_COLLECTORS_GUIDE guide = getCollectorsGuide();
GENERAL_COLLECTOR collector;
const PCB_DISPLAY_OPTIONS& displayOpts = m_frame->GetDisplayOptions();
guide.SetIgnoreZoneFills( displayOpts.m_ZoneDisplayMode != ZONE_DISPLAY_MODE::SHOW_FILLED );
if( m_enteredGroup && !m_enteredGroup->GetBoundingBox().Contains( aWhere ) )
ExitGroup();
collector.Collect( board(), m_isFootprintEditor ? GENERAL_COLLECTOR::FootprintItems
: GENERAL_COLLECTOR::AllBoardItems,
aWhere, guide );
// Remove unselectable items
for( int i = collector.GetCount() - 1; i >= 0; --i )
{
if( !Selectable( collector[ i ] ) || ( aOnDrag && collector[i]->IsLocked() ) )
collector.Remove( i );
}
m_selection.ClearReferencePoint();
// Apply the stateful filter (remove items disabled by the Selection Filter)
FilterCollectedItems( collector, false );
// Allow the client to do tool- or action-specific filtering to see if we can get down
// to a single item
if( aClientFilter )
aClientFilter( aWhere, collector, this );
FilterCollectorForHierarchy( collector, false );
FilterCollectorForFootprints( collector, aWhere );
// For subtracting, we only want items that are selected
if( m_subtractive )
{
for( int i = collector.GetCount() - 1; i >= 0; --i )
{
if( !collector[i]->IsSelected() )
collector.Remove( i );
}
}
// Apply some ugly heuristics to avoid disambiguation menus whenever possible
if( collector.GetCount() > 1 && !m_skip_heuristics )
{
try
{
GuessSelectionCandidates( collector, aWhere );
}
catch( const std::exception& exc )
{
wxLogWarning( wxS( "Exception '%s' occurred attempting to guess selection candidates." ),
exc.what() );
return false;
}
}
// If still more than one item we're going to have to ask the user.
if( collector.GetCount() > 1 )
{
if( aOnDrag )
Wait( TOOL_EVENT( TC_ANY, TA_MOUSE_UP, BUT_LEFT ) );
if( !doSelectionMenu( &collector ) )
{
if( aSelectionCancelledFlag )
*aSelectionCancelledFlag = true;
return false;
}
}
int addedCount = 0;
bool anySubtracted = false;
if( !m_additive && !m_subtractive && !m_exclusive_or )
{
if( m_selection.GetSize() > 0 )
{
ClearSelection( true /*quiet mode*/ );
anySubtracted = true;
}
}
if( collector.GetCount() > 0 )
{
for( int i = 0; i < collector.GetCount(); ++i )
{
if( m_subtractive || ( m_exclusive_or && collector[i]->IsSelected() ) )
{
unselect( collector[i] );
anySubtracted = true;
}
else
{
select( collector[i] );
addedCount++;
}
}
}
if( addedCount == 1 )
{
m_toolMgr->ProcessEvent( EVENTS::PointSelectedEvent );
return true;
}
else if( addedCount > 1 )
{
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return true;
}
else if( anySubtracted )
{
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
return true;
}
return false;
}
bool PCB_SELECTION_TOOL::selectCursor( bool aForceSelect, CLIENT_SELECTION_FILTER aClientFilter )
{
if( aForceSelect || m_selection.Empty() )
{
ClearSelection( true /*quiet mode*/ );
selectPoint( getViewControls()->GetCursorPosition( false ), false, nullptr, aClientFilter );
}
return !m_selection.Empty();
}
// Some navigation actions are allowed in selectMultiple
const TOOL_ACTION* allowedActions[] = { &ACTIONS::panUp, &ACTIONS::panDown,
&ACTIONS::panLeft, &ACTIONS::panRight,
&ACTIONS::cursorUp, &ACTIONS::cursorDown,
&ACTIONS::cursorLeft, &ACTIONS::cursorRight,
&ACTIONS::cursorUpFast, &ACTIONS::cursorDownFast,
&ACTIONS::cursorLeftFast, &ACTIONS::cursorRightFast,
&ACTIONS::zoomIn, &ACTIONS::zoomOut,
&ACTIONS::zoomInCenter, &ACTIONS::zoomOutCenter,
&ACTIONS::zoomCenter, &ACTIONS::zoomFitScreen,
&ACTIONS::zoomFitObjects, nullptr };
static void passEvent( TOOL_EVENT* const aEvent, const TOOL_ACTION* const aAllowedActions[] )
{
for( int i = 0; aAllowedActions[i]; ++i )
{
if( aEvent->IsAction( aAllowedActions[i] ) )
{
aEvent->SetPassEvent();
break;
}
}
}
bool PCB_SELECTION_TOOL::selectTableCells( PCB_TABLE* aTable )
{
bool cancelled = false; // Was the tool canceled while it was running?
m_multiple = true; // Multiple selection mode is active
for( PCB_TABLECELL* cell : aTable->GetCells() )
{
if( cell->IsSelected() )
cell->SetFlags( CANDIDATE );
else
cell->ClearFlags( CANDIDATE );
}
auto wasSelected =
[]( EDA_ITEM* aItem )
{
return ( aItem->GetFlags() & CANDIDATE ) > 0;
};
while( TOOL_EVENT* evt = Wait() )
{
if( evt->IsCancelInteractive() || evt->IsActivate() )
{
cancelled = true;
break;
}
else if( evt->IsDrag( BUT_LEFT ) )
{
getViewControls()->SetAutoPan( true );
BOX2I selectionRect( evt->DragOrigin(), evt->Position() - evt->DragOrigin() );
selectionRect.Normalize();
for( PCB_TABLECELL* cell : aTable->GetCells() )
{
bool doSelect = false;
if( cell->HitTest( selectionRect, false ) )
{
if( m_subtractive )
doSelect = false;
else if( m_exclusive_or )
doSelect = !wasSelected( cell );
else
doSelect = true;
}
else if( wasSelected( cell ) )
{
doSelect = m_additive || m_subtractive || m_exclusive_or;
}
if( doSelect && !cell->IsSelected() )
select( cell );
else if( !doSelect && cell->IsSelected() )
unselect( cell );
}
}
else if( evt->IsMouseUp( BUT_LEFT ) )
{
m_selection.SetIsHover( false );
bool anyAdded = false;
bool anySubtracted = false;
for( PCB_TABLECELL* cell : aTable->GetCells() )
{
if( cell->IsSelected() && !wasSelected( cell ) )
anyAdded = true;
else if( wasSelected( cell ) && !cell->IsSelected() )
anySubtracted = true;
}
// Inform other potentially interested tools
if( anyAdded )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
if( anySubtracted )
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
break; // Stop waiting for events
}
else
{
// Allow some actions for navigation
passEvent( evt, allowedActions );
}
}
getViewControls()->SetAutoPan( false );
m_multiple = false; // Multiple selection mode is inactive
if( !cancelled )
m_selection.ClearReferencePoint();
return cancelled;
}
int PCB_SELECTION_TOOL::SelectRectArea( const TOOL_EVENT& aEvent )
{
bool cancelled = false; // Was the tool canceled while it was running?
m_multiple = true; // Multiple selection mode is active
KIGFX::VIEW* view = getView();
KIGFX::PREVIEW::SELECTION_AREA area;
view->Add( &area );
while( TOOL_EVENT* evt = Wait() )
{
/* Selection mode depends on direction of drag-selection:
* Left > Right : Select objects that are fully enclosed by selection
* Right > Left : Select objects that are crossed by selection
*/
bool greedySelection = area.GetEnd().x < area.GetOrigin().x;
if( view->IsMirroredX() )
greedySelection = !greedySelection;
m_frame->GetCanvas()->SetCurrentCursor( !greedySelection ? KICURSOR::SELECT_WINDOW
: KICURSOR::SELECT_LASSO );
if( evt->IsCancelInteractive() || evt->IsActivate() )
{
cancelled = true;
break;
}
if( evt->IsDrag( BUT_LEFT ) )
{
if( !m_drag_additive && !m_drag_subtractive )
{
if( m_selection.GetSize() > 0 )
{
ClearSelection( true /*quiet mode*/ );
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
}
// Start drawing a selection box
area.SetOrigin( evt->DragOrigin() );
area.SetEnd( evt->Position() );
area.SetAdditive( m_drag_additive );
area.SetSubtractive( m_drag_subtractive );
area.SetExclusiveOr( false );
area.SetMode( greedySelection ? SELECTION_MODE::TOUCHING_RECTANGLE : SELECTION_MODE::INSIDE_RECTANGLE );
view->SetVisible( &area, true );
view->Update( &area );
getViewControls()->SetAutoPan( true );
}
if( evt->IsMouseUp( BUT_LEFT ) )
{
getViewControls()->SetAutoPan( false );
// End drawing the selection box
view->SetVisible( &area, false );
SelectMultiple( area, m_subtractive, m_exclusive_or );
break; // Stop waiting for events
}
// Allow some actions for navigation
passEvent( evt, allowedActions );
}
getViewControls()->SetAutoPan( false );
// Stop drawing the selection box
view->Remove( &area );
m_multiple = false; // Multiple selection mode is inactive
if( !cancelled )
m_selection.ClearReferencePoint();
m_toolMgr->ProcessEvent( EVENTS::UninhibitSelectionEditing );
return cancelled;
}
int PCB_SELECTION_TOOL::SetSelectPoly( const TOOL_EVENT& aEvent )
{
m_selectionMode = SELECTION_MODE::INSIDE_LASSO;
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SELECT_LASSO );
m_toolMgr->PostAction( ACTIONS::selectionTool );
return 0; // No need to wait for an event, just set the mode
}
int PCB_SELECTION_TOOL::SetSelectRect( const TOOL_EVENT& aEvent )
{
m_selectionMode = SELECTION_MODE::INSIDE_RECTANGLE;
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::ARROW );
m_toolMgr->PostAction( ACTIONS::selectionTool );
return 0; // No need to wait for an event, just set the mode
}
int PCB_SELECTION_TOOL::SelectPolyArea( const TOOL_EVENT& aEvent )
{
bool cancelled = false; // Was the tool canceled while it was running?
SELECTION_MODE selectionMode = SELECTION_MODE::TOUCHING_LASSO;
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::SELECT_LASSO );
SHAPE_LINE_CHAIN points;
points.SetClosed( true );
KIGFX::PREVIEW::SELECTION_AREA area;
getView()->Add( &area );
getView()->SetVisible( &area, true );
getViewControls()->SetAutoPan( true );
while( TOOL_EVENT* evt = Wait() )
{
// Auto mode: clockwise = inside, counterclockwise = touching
double shapeArea = area.GetPoly().Area( false );
bool isClockwise = shapeArea > 0 ? true : false;
if( getView()->IsMirroredX() && shapeArea != 0 )
isClockwise = !isClockwise;
selectionMode = isClockwise ? SELECTION_MODE::INSIDE_LASSO : SELECTION_MODE::TOUCHING_LASSO;
if( evt->IsCancelInteractive() || evt->IsActivate() )
{
cancelled = true;
evt->SetPassEvent( false );
break;
}
else if( evt->IsDrag( BUT_LEFT )
|| evt->IsClick( BUT_LEFT )
|| evt->IsAction( &ACTIONS::cursorClick ) )
{
points.Append( evt->Position() );
}
else if( evt->IsDblClick( BUT_LEFT )
|| evt->IsAction( &ACTIONS::cursorDblClick )
|| evt->IsAction( &ACTIONS::finishInteractive ) )
{
area.GetPoly().GenerateBBoxCache();
SelectMultiple( area, m_subtractive, m_exclusive_or );
evt->SetPassEvent( false );
break;
}
else if( evt->IsAction( &PCB_ACTIONS::deleteLastPoint )
|| evt->IsAction( &ACTIONS::doDelete )
|| evt->IsAction( &ACTIONS::undo ) )
{
if( points.GetPointCount() > 0 )
{
getViewControls()->SetCursorPosition( points.CLastPoint() );
points.Remove( points.GetPointCount() - 1 );
}
}
else
{
// Allow navigation actions
passEvent( evt, allowedActions );
}
if( points.PointCount() > 0 )
{
if( !m_drag_additive && !m_drag_subtractive )
{
if( m_selection.GetSize() > 0 )
{
ClearSelection( true /*quiet mode*/ );
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
}
}
area.SetPoly( points );
area.GetPoly().Append( m_toolMgr->GetMousePosition() );
area.SetAdditive( m_additive );
area.SetSubtractive( m_subtractive );
area.SetExclusiveOr( false );
area.SetMode( selectionMode );
getView()->Update( &area );
}
getViewControls()->SetAutoPan( false );
getView()->SetVisible( &area, false );
getView()->Remove( &area );
m_frame->GetCanvas()->SetCurrentCursor( KICURSOR::ARROW );
if( !cancelled )
GetSelection().ClearReferencePoint();
m_toolMgr->ProcessEvent( EVENTS::UninhibitSelectionEditing );
return cancelled;
}
void PCB_SELECTION_TOOL::SelectMultiple( KIGFX::PREVIEW::SELECTION_AREA& aArea, bool aSubtractive,
bool aExclusiveOr )
{
KIGFX::VIEW* view = getView();
bool anyAdded = false;
bool anySubtracted = false;
SELECTION_MODE selectionMode = aArea.GetMode();
bool containedMode = ( selectionMode == SELECTION_MODE::INSIDE_RECTANGLE
|| selectionMode == SELECTION_MODE::INSIDE_LASSO ) ? true : false;
bool boxMode = ( selectionMode == SELECTION_MODE::INSIDE_RECTANGLE
|| selectionMode == SELECTION_MODE::TOUCHING_RECTANGLE ) ? true : false;
std::vector<KIGFX::VIEW::LAYER_ITEM_PAIR> candidates;
BOX2I selectionBox = aArea.ViewBBox();
view->Query( selectionBox, candidates ); // Get the list of nearby items
GENERAL_COLLECTOR collector;
GENERAL_COLLECTOR padsCollector;
std::set<EDA_ITEM*> group_items;
for( PCB_GROUP* group : board()->Groups() )
{
// The currently entered group does not get limited
if( m_enteredGroup == group )
continue;
std::unordered_set<EDA_ITEM*>& newset = group->GetItems();
auto boxContained =
[&]( const BOX2I& aBox )
{
return boxMode ? selectionBox.Contains( aBox )
: KIGEOM::BoxHitTest( aArea.GetPoly(), aBox, true );
};
// If we are not greedy and have selected the whole group, add just one item
// to allow it to be promoted to the group later
if( containedMode && boxContained( group->GetBoundingBox() ) && newset.size() )
{
for( EDA_ITEM* group_item : newset )
{
if( !group_item->IsBOARD_ITEM() )
continue;
if( Selectable( static_cast<BOARD_ITEM*>( group_item ) ) )
collector.Append( *newset.begin() );
}
}
for( EDA_ITEM* group_item : newset )
group_items.emplace( group_item );
}
auto hitTest =
[&]( const EDA_ITEM* aItem )
{
return boxMode ? aItem->HitTest( selectionBox, containedMode )
: aItem->HitTest( aArea.GetPoly(), containedMode );
};
for( const auto& [item, layer] : candidates )
{
if( !item->IsBOARD_ITEM() )
continue;
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
if( Selectable( boardItem ) && hitTest( boardItem )
&& ( !containedMode || !group_items.count( boardItem ) ) )
{
if( boardItem->Type() == PCB_PAD_T && !m_isFootprintEditor )
padsCollector.Append( boardItem );
else
collector.Append( boardItem );
}
}
// Apply the stateful filter
FilterCollectedItems( collector, true );
FilterCollectorForHierarchy( collector, true );
// If we selected nothing but pads, allow them to be selected
if( collector.GetCount() == 0 )
{
collector = padsCollector;
FilterCollectedItems( collector, true );
FilterCollectorForHierarchy( collector, true );
}
// Sort the filtered selection by rows and columns to have a nice default
// for tools that can use it.
std::sort( collector.begin(), collector.end(),
[]( EDA_ITEM* a, EDA_ITEM* b )
{
VECTOR2I aPos = a->GetPosition();
VECTOR2I bPos = b->GetPosition();
if( aPos.y == bPos.y )
return aPos.x < bPos.x;
return aPos.y < bPos.y;
} );
for( EDA_ITEM* i : collector )
{
if( !i->IsBOARD_ITEM() )
continue;
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( i );
if( aSubtractive || ( aExclusiveOr && item->IsSelected() ) )
{
unselect( item );
anySubtracted = true;
}
else
{
select( item );
anyAdded = true;
}
}
m_selection.SetIsHover( false );
// Inform other potentially interested tools
if( anyAdded )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
else if( anySubtracted )
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
}
int PCB_SELECTION_TOOL::disambiguateCursor( const TOOL_EVENT& aEvent )
{
wxMouseState keyboardState = wxGetMouseState();
setModifiersState( keyboardState.ShiftDown(), keyboardState.ControlDown(), keyboardState.AltDown() );
m_skip_heuristics = true;
selectPoint( m_originalCursor, false, &m_canceledMenu );
m_skip_heuristics = false;
return 0;
}
int PCB_SELECTION_TOOL::CursorSelection( const TOOL_EVENT& aEvent )
{
CLIENT_SELECTION_FILTER aClientFilter = aEvent.Parameter<CLIENT_SELECTION_FILTER>();
selectCursor( false, aClientFilter );
return 0;
}
int PCB_SELECTION_TOOL::ClearSelection( const TOOL_EVENT& aEvent )
{
ClearSelection();
return 0;
}
int PCB_SELECTION_TOOL::SelectAll( const TOOL_EVENT& aEvent )
{
GENERAL_COLLECTOR collection;
BOX2I selectionBox;
selectionBox.SetMaximum();
getView()->Query( selectionBox,
[&]( KIGFX::VIEW_ITEM* viewItem ) -> bool
{
if( viewItem->IsBOARD_ITEM() )
{
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( viewItem );
if( item && Selectable( item ) && itemPassesFilter( item, true ) )
collection.Append( item );
}
return true;
} );
FilterCollectorForHierarchy( collection, true );
for( EDA_ITEM* item : collection )
select( item );
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
m_frame->GetCanvas()->ForceRefresh();
return 0;
}
int PCB_SELECTION_TOOL::UnselectAll( const TOOL_EVENT& aEvent )
{
BOX2I selectionBox;
selectionBox.SetMaximum();
getView()->Query( selectionBox,
[&]( KIGFX::VIEW_ITEM* viewItem ) -> bool
{
if( viewItem->IsBOARD_ITEM() )
{
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( viewItem );
if( item && Selectable( item ) )
unselect( item );
}
return true;
} );
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
m_frame->GetCanvas()->ForceRefresh();
return 0;
}
void connectedItemFilter( const VECTOR2I&, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
{
// Narrow the collection down to a single BOARD_CONNECTED_ITEM for each represented net.
// All other items types are removed.
std::set<int> representedNets;
for( int i = aCollector.GetCount() - 1; i >= 0; i-- )
{
BOARD_CONNECTED_ITEM* item = dynamic_cast<BOARD_CONNECTED_ITEM*>( aCollector[i] );
if( !item )
aCollector.Remove( i );
else if ( representedNets.count( item->GetNetCode() ) )
aCollector.Remove( i );
else
representedNets.insert( item->GetNetCode() );
}
}
int PCB_SELECTION_TOOL::unrouteSelected( const TOOL_EVENT& aEvent )
{
std::deque<EDA_ITEM*> selectedItems = m_selection.GetItems();
// Get all footprints and pads
std::vector<BOARD_CONNECTED_ITEM*> toUnroute;
for( EDA_ITEM* item : selectedItems )
{
if( item->Type() == PCB_FOOTPRINT_T )
{
for( PAD* pad : static_cast<FOOTPRINT*>( item )->Pads() )
toUnroute.push_back( pad );
}
else if( BOARD_CONNECTED_ITEM::ClassOf( item ) )
{
toUnroute.push_back( static_cast<BOARD_CONNECTED_ITEM*>( item ) );
}
}
// Clear selection so we don't delete our footprints/pads
ClearSelection( true );
// Get the tracks on our list of pads, then delete them
selectAllConnectedTracks( toUnroute, STOP_CONDITION::STOP_AT_PAD );
m_toolMgr->RunAction( ACTIONS::doDelete );
// Reselect our footprint/pads as they were in our original selection
for( EDA_ITEM* item : selectedItems )
{
if( item->Type() == PCB_FOOTPRINT_T || item->Type() == PCB_PAD_T )
select( item );
}
return 0;
}
int PCB_SELECTION_TOOL::unrouteSegment( const TOOL_EVENT& aEvent )
{
std::deque<EDA_ITEM*> selectedItems = m_selection.GetItems();
// Get all footprints and pads
std::vector<BOARD_CONNECTED_ITEM*> toUnroute;
for( EDA_ITEM* item : selectedItems )
{
if( item->Type() == PCB_TRACE_T || item->Type() == PCB_ARC_T || item->Type() == PCB_VIA_T )
toUnroute.push_back( static_cast<BOARD_CONNECTED_ITEM*>( item ) );
}
// Get the tracks connecting to our starting objects
ClearSelection();
selectAllConnectedTracks( toUnroute, STOP_CONDITION::STOP_AT_SEGMENT );
std::deque<EDA_ITEM*> toSelectAfter;
// This will select the unroute items too, so filter them out
for( EDA_ITEM* item : m_selection.GetItemsSortedByTypeAndXY() )
{
if( !item->IsBOARD_ITEM() )
continue;
if( std::find( toUnroute.begin(), toUnroute.end(), item ) == toUnroute.end() )
toSelectAfter.push_back( item );
}
ClearSelection( true );
for( EDA_ITEM* item : toUnroute )
select( item );
m_toolMgr->RunAction( ACTIONS::doDelete );
// Now our after tracks so the user can continue backing up as desired
ClearSelection( true );
for( EDA_ITEM* item : toSelectAfter )
select( item );
return 0;
}
int PCB_SELECTION_TOOL::expandConnection( const TOOL_EVENT& aEvent )
{
// expandConnection will get called no matter whether the user selected a connected item or a
// non-connected shape (graphic on a non-copper layer). The algorithm for expanding to connected
// items is different from graphics, so they need to be handled separately.
unsigned initialCount = 0;
for( const EDA_ITEM* item : m_selection.GetItems() )
{
if( item->Type() == PCB_FOOTPRINT_T
|| item->Type() == PCB_GENERATOR_T
|| ( static_cast<const BOARD_ITEM*>( item )->IsConnected() ) )
{
initialCount++;
}
}
if( initialCount == 0 )
{
// First, process any graphic shapes we have
std::vector<PCB_SHAPE*> startShapes;
for( EDA_ITEM* item : m_selection.GetItems() )
{
if( isExpandableGraphicShape( item ) )
startShapes.push_back( static_cast<PCB_SHAPE*>( item ) );
}
// If no non-copper shapes; fall back to looking for connected items
if( !startShapes.empty() )
selectAllConnectedShapes( startShapes );
else
selectCursor( true, connectedItemFilter );
}
m_frame->SetStatusText( _( "Select/Expand Connection..." ) );
for( STOP_CONDITION stopCondition : { STOP_AT_JUNCTION, STOP_AT_PAD, STOP_NEVER } )
{
std::deque<EDA_ITEM*> selectedItems = m_selection.GetItems();
for( EDA_ITEM* item : selectedItems )
item->ClearTempFlags();
std::vector<BOARD_CONNECTED_ITEM*> startItems;
for( EDA_ITEM* item : selectedItems )
{
if( item->Type() == PCB_FOOTPRINT_T )
{
FOOTPRINT* footprint = static_cast<FOOTPRINT*>( item );
for( PAD* pad : footprint->Pads() )
startItems.push_back( pad );
}
else if( item->Type() == PCB_GENERATOR_T )
{
for( BOARD_ITEM* generatedItem : static_cast<PCB_GENERATOR*>( item )->GetBoardItems() )
{
if( BOARD_CONNECTED_ITEM::ClassOf( generatedItem ) )
startItems.push_back( static_cast<BOARD_CONNECTED_ITEM*>( generatedItem ) );
}
}
else if( BOARD_CONNECTED_ITEM::ClassOf( item ) )
{
startItems.push_back( static_cast<BOARD_CONNECTED_ITEM*>( item ) );
}
}
selectAllConnectedTracks( startItems, stopCondition );
if( m_selection.GetItems().size() > initialCount )
break;
}
m_frame->SetStatusText( wxEmptyString );
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
void PCB_SELECTION_TOOL::selectAllConnectedTracks( const std::vector<BOARD_CONNECTED_ITEM*>& aStartItems,
STOP_CONDITION aStopCondition )
{
PROF_TIMER refreshTimer;
double refreshIntervalMs = 500; // Refresh display with this interval to indicate progress
int lastSelectionSize = (int) m_selection.GetSize();
auto connectivity = board()->GetConnectivity();
std::set<PAD*> startPadSet;
std::vector<BOARD_CONNECTED_ITEM*> cleanupItems;
for( BOARD_CONNECTED_ITEM* startItem : aStartItems )
{
// Register starting pads
if( startItem->Type() == PCB_PAD_T )
startPadSet.insert( static_cast<PAD*>( startItem ) );
// Select any starting track items
if( startItem->IsType( { PCB_TRACE_T, PCB_ARC_T, PCB_VIA_T } ) )
select( startItem );
}
for( BOARD_CONNECTED_ITEM* startItem : aStartItems )
{
std::map<VECTOR2I, std::vector<PCB_TRACK*>> trackMap;
std::map<VECTOR2I, PCB_VIA*> viaMap;
std::map<VECTOR2I, PAD*> padMap;
std::map<VECTOR2I, std::vector<PCB_SHAPE*>> shapeMap;
std::vector<std::pair<VECTOR2I, LSET>> activePts;
if( startItem->HasFlag( SKIP_STRUCT ) ) // Skip already visited items
continue;
auto connectedItems = connectivity->GetConnectedItems( startItem, EXCLUDE_ZONES | IGNORE_NETS );
// Build maps of connected items
for( BOARD_CONNECTED_ITEM* item : connectedItems )
{
switch( item->Type() )
{
case PCB_ARC_T:
case PCB_TRACE_T:
{
PCB_TRACK* track = static_cast<PCB_TRACK*>( item );
trackMap[track->GetStart()].push_back( track );
trackMap[track->GetEnd()].push_back( track );
break;
}
case PCB_VIA_T:
{
PCB_VIA* via = static_cast<PCB_VIA*>( item );
viaMap[via->GetStart()] = via;
break;
}
case PCB_PAD_T:
{
PAD* pad = static_cast<PAD*>( item );
padMap[pad->GetPosition()] = pad;
break;
}
case PCB_SHAPE_T:
{
PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
for( const auto& point : shape->GetConnectionPoints() )
shapeMap[point].push_back( shape );
break;
}
default:
break;
}
}
// Set up the initial active points
switch( startItem->Type() )
{
case PCB_ARC_T:
case PCB_TRACE_T:
{
PCB_TRACK* track = static_cast<PCB_TRACK*>( startItem );
activePts.push_back( { track->GetStart(), track->GetLayerSet() } );
activePts.push_back( { track->GetEnd(), track->GetLayerSet() } );
break;
}
case PCB_VIA_T:
activePts.push_back( { startItem->GetPosition(), startItem->GetLayerSet() } );
break;
case PCB_PAD_T:
activePts.push_back( { startItem->GetPosition(), startItem->GetLayerSet() } );
break;
case PCB_SHAPE_T:
{
PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( startItem );
for( const auto& point : shape->GetConnectionPoints() )
activePts.push_back( { point, startItem->GetLayerSet() } );
break;
}
default:
break;
}
bool expand = true;
int failSafe = 0;
// Iterative push from all active points
while( expand && failSafe++ < 100000 )
{
expand = false;
for( int i = (int) activePts.size() - 1; i >= 0; --i )
{
VECTOR2I pt = activePts[i].first;
LSET layerSetCu = activePts[i].second & LSET::AllCuMask();
auto viaIt = viaMap.find( pt );
auto padIt = padMap.find( pt );
bool gotVia = viaIt != viaMap.end() && ( viaIt->second->GetLayerSet() & layerSetCu ).any();
bool gotPad = padIt != padMap.end() && ( padIt->second->GetLayerSet() & layerSetCu ).any();
bool gotNonStartPad = gotPad && ( startPadSet.find( padIt->second ) == startPadSet.end() );
if( aStopCondition == STOP_AT_JUNCTION )
{
size_t pt_count = 0;
for( PCB_TRACK* track : trackMap[pt] )
{
if( track->GetStart() != track->GetEnd() && layerSetCu.Contains( track->GetLayer() ) )
pt_count++;
}
if( pt_count > 2 || gotVia || gotNonStartPad )
{
activePts.erase( activePts.begin() + i );
continue;
}
}
else if( aStopCondition == STOP_AT_PAD )
{
if( gotNonStartPad )
{
activePts.erase( activePts.begin() + i );
continue;
}
}
if( gotPad )
{
PAD* pad = padIt->second;
if( !pad->HasFlag( SKIP_STRUCT ) )
{
pad->SetFlags( SKIP_STRUCT );
cleanupItems.push_back( pad );
activePts.push_back( { pad->GetPosition(), pad->GetLayerSet() } );
expand = true;
}
}
for( PCB_TRACK* track : trackMap[pt] )
{
if( !layerSetCu.Contains( track->GetLayer() ) )
continue;
if( !track->IsSelected() )
select( track );
if( !track->HasFlag( SKIP_STRUCT ) )
{
track->SetFlags( SKIP_STRUCT );
cleanupItems.push_back( track );
if( track->GetStart() == pt )
activePts.push_back( { track->GetEnd(), track->GetLayerSet() } );
else
activePts.push_back( { track->GetStart(), track->GetLayerSet() } );
if( aStopCondition != STOP_AT_SEGMENT )
expand = true;
}
}
for( PCB_SHAPE* shape : shapeMap[pt] )
{
if( !layerSetCu.Contains( shape->GetLayer() ) )
continue;
if( !shape->IsSelected() )
select( shape );
if( !shape->HasFlag( SKIP_STRUCT ) )
{
shape->SetFlags( SKIP_STRUCT );
cleanupItems.push_back( shape );
for( const VECTOR2I& newPoint : shape->GetConnectionPoints() )
{
if( newPoint == pt )
continue;
activePts.push_back( { newPoint, shape->GetLayerSet() } );
}
if( aStopCondition != STOP_AT_SEGMENT )
expand = true;
}
}
if( viaMap.count( pt ) )
{
PCB_VIA* via = viaMap[pt];
if( !via->IsSelected() )
select( via );
if( !via->HasFlag( SKIP_STRUCT ) )
{
via->SetFlags( SKIP_STRUCT );
cleanupItems.push_back( via );
activePts.push_back( { via->GetPosition(), via->GetLayerSet() } );
if( aStopCondition != STOP_AT_SEGMENT )
expand = true;
}
}
activePts.erase( activePts.begin() + i );
}
// Refresh display for the feel of progress
if( refreshTimer.msecs() >= refreshIntervalMs )
{
if( m_selection.Size() != lastSelectionSize )
{
m_frame->GetCanvas()->ForceRefresh();
lastSelectionSize = m_selection.Size();
}
refreshTimer.Start();
}
}
}
std::set<EDA_ITEM*> toDeselect;
std::set<EDA_ITEM*> toSelect;
// Promote generated members to their PCB_GENERATOR parents
for( EDA_ITEM* item : m_selection )
{
if( !item->IsBOARD_ITEM() )
continue;
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
EDA_GROUP* parent = boardItem->GetParentGroup();
if( parent && parent->AsEdaItem()->Type() == PCB_GENERATOR_T )
{
toDeselect.insert( item );
if( !parent->AsEdaItem()->IsSelected() )
toSelect.insert( parent->AsEdaItem() );
}
}
for( EDA_ITEM* item : toDeselect )
unselect( item );
for( EDA_ITEM* item : toSelect )
select( item );
for( BOARD_CONNECTED_ITEM* item : cleanupItems )
item->ClearFlags( SKIP_STRUCT );
}
bool PCB_SELECTION_TOOL::isExpandableGraphicShape( const EDA_ITEM* aItem ) const
{
if( aItem->Type() == PCB_SHAPE_T )
{
const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( aItem );
switch( shape->GetShape() )
{
case SHAPE_T::SEGMENT:
case SHAPE_T::ARC:
case SHAPE_T::BEZIER:
return !shape->IsOnCopperLayer();
case SHAPE_T::POLY:
return !shape->IsOnCopperLayer() && !shape->IsClosed();
default:
return false;
}
}
return false;
}
void PCB_SELECTION_TOOL::selectAllConnectedShapes( const std::vector<PCB_SHAPE*>& aStartItems )
{
std::stack<PCB_SHAPE*> toSearch;
std::set<PCB_SHAPE*> toCleanup;
for( PCB_SHAPE* startItem : aStartItems )
toSearch.push( startItem );
GENERAL_COLLECTOR collector;
GENERAL_COLLECTORS_GUIDE guide = getCollectorsGuide();
auto searchPoint =
[&]( const VECTOR2I& aWhere )
{
collector.Collect( board(), { PCB_SHAPE_T }, aWhere, guide );
for( EDA_ITEM* item : collector )
{
if( isExpandableGraphicShape( item ) )
toSearch.push( static_cast<PCB_SHAPE*>( item ) );
}
};
while( !toSearch.empty() )
{
PCB_SHAPE* shape = toSearch.top();
toSearch.pop();
if( shape->HasFlag( SKIP_STRUCT ) )
continue;
select( shape );
shape->SetFlags( SKIP_STRUCT );
toCleanup.insert( shape );
guide.SetLayerVisibleBits( shape->GetLayerSet() );
searchPoint( shape->GetStart() );
searchPoint( shape->GetEnd() );
}
for( PCB_SHAPE* shape : toCleanup )
shape->ClearFlags( SKIP_STRUCT );
}
int PCB_SELECTION_TOOL::selectUnconnected( const TOOL_EVENT& aEvent )
{
// Get all pads
std::vector<PAD*> pads;
for( EDA_ITEM* item : m_selection.GetItems() )
{
if( item->Type() == PCB_FOOTPRINT_T )
{
for( PAD* pad : static_cast<FOOTPRINT*>( item )->Pads() )
pads.push_back( pad );
}
else if( item->Type() == PCB_PAD_T )
{
pads.push_back( static_cast<PAD*>( item ) );
}
}
// Select every footprint on the end of the ratsnest for each pad in our selection
std::shared_ptr<CONNECTIVITY_DATA> conn = board()->GetConnectivity();
for( PAD* pad : pads )
{
for( const CN_EDGE& edge : conn->GetRatsnestForPad( pad ) )
{
wxCHECK2( edge.GetSourceNode() && !edge.GetSourceNode()->Dirty(), continue );
wxCHECK2( edge.GetTargetNode() && !edge.GetTargetNode()->Dirty(), continue );
BOARD_CONNECTED_ITEM* sourceParent = edge.GetSourceNode()->Parent();
BOARD_CONNECTED_ITEM* targetParent = edge.GetTargetNode()->Parent();
if( sourceParent == pad )
{
if( targetParent->Type() == PCB_PAD_T )
select( static_cast<PAD*>( targetParent )->GetParent() );
}
else if( targetParent == pad )
{
if( sourceParent->Type() == PCB_PAD_T )
select( static_cast<PAD*>( sourceParent )->GetParent() );
}
}
}
return 0;
}
int PCB_SELECTION_TOOL::grabUnconnected( const TOOL_EVENT& aEvent )
{
PCB_SELECTION originalSelection = m_selection;
// Get all pads
std::vector<PAD*> pads;
for( EDA_ITEM* item : m_selection.GetItems() )
{
if( item->Type() == PCB_FOOTPRINT_T )
{
for( PAD* pad : static_cast<FOOTPRINT*>( item )->Pads() )
pads.push_back( pad );
}
else if( item->Type() == PCB_PAD_T )
{
pads.push_back( static_cast<PAD*>( item ) );
}
}
ClearSelection();
// Select every footprint on the end of the ratsnest for each pad in our selection
std::shared_ptr<CONNECTIVITY_DATA> conn = board()->GetConnectivity();
for( PAD* pad : pads )
{
const std::vector<CN_EDGE> edges = conn->GetRatsnestForPad( pad );
// Need to have something unconnected to grab
if( edges.size() == 0 )
continue;
double currentDistance = DBL_MAX;
FOOTPRINT* nearest = nullptr;
// Check every ratsnest line for the nearest one
for( const CN_EDGE& edge : edges )
{
if( edge.GetSourceNode()->Parent()->GetParentFootprint()
== edge.GetTargetNode()->Parent()->GetParentFootprint() )
{
continue; // This edge is a loop on the same footprint
}
// Figure out if we are the source or the target node on the ratnest
const CN_ANCHOR* other = edge.GetSourceNode()->Parent() == pad ? edge.GetTargetNode().get()
: edge.GetSourceNode().get();
wxCHECK2( other && !other->Dirty(), continue );
// We only want to grab footprints, so the ratnest has to point to a pad
if( other->Parent()->Type() != PCB_PAD_T )
continue;
if( edge.GetLength() < currentDistance )
{
currentDistance = edge.GetLength();
nearest = other->Parent()->GetParentFootprint();
}
}
if( nearest != nullptr )
select( nearest );
}
m_toolMgr->RunAction( PCB_ACTIONS::moveIndividually );
return 0;
}
void PCB_SELECTION_TOOL::SelectAllItemsOnNet( int aNetCode, bool aSelect )
{
std::shared_ptr<CONNECTIVITY_DATA> conn = board()->GetConnectivity();
for( BOARD_ITEM* item : conn->GetNetItems( aNetCode, { PCB_TRACE_T,
PCB_ARC_T,
PCB_VIA_T,
PCB_SHAPE_T } ) )
{
if( itemPassesFilter( item, true ) )
aSelect ? select( item ) : unselect( item );
}
}
int PCB_SELECTION_TOOL::selectNet( const TOOL_EVENT& aEvent )
{
bool select = aEvent.IsAction( &PCB_ACTIONS::selectNet );
// If we've been passed an argument, just select that netcode1
int netcode = aEvent.Parameter<int>();
if( netcode > 0 )
{
SelectAllItemsOnNet( netcode, select );
// Inform other potentially interested tools
if( m_selection.Size() > 0 )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
else
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
return 0;
}
if( !selectCursor() )
return 0;
// copy the selection, since we're going to iterate and modify
auto selection = m_selection.GetItems();
for( EDA_ITEM* i : selection )
{
BOARD_CONNECTED_ITEM* connItem = dynamic_cast<BOARD_CONNECTED_ITEM*>( i );
if( connItem )
SelectAllItemsOnNet( connItem->GetNetCode(), select );
}
// Inform other potentially interested tools
if( m_selection.Size() > 0 )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
else
m_toolMgr->ProcessEvent( EVENTS::UnselectedEvent );
return 0;
}
void PCB_SELECTION_TOOL::selectAllItemsOnSheet( wxString& aSheetPath )
{
std::vector<BOARD_ITEM*> footprints;
// store all footprints that are on that sheet path
for( FOOTPRINT* footprint : board()->Footprints() )
{
if( footprint == nullptr )
continue;
wxString footprint_path = footprint->GetPath().AsString().BeforeLast( '/' );
if( footprint_path.IsEmpty() )
footprint_path += '/';
if( footprint_path == aSheetPath )
footprints.push_back( footprint );
}
for( BOARD_ITEM* i : footprints )
{
if( i != nullptr )
select( i );
}
selectConnections( footprints );
}
void PCB_SELECTION_TOOL::selectConnections( const std::vector<BOARD_ITEM*>& aItems )
{
// Generate a list of all pads, and of all nets they belong to.
std::list<int> netcodeList;
std::vector<BOARD_CONNECTED_ITEM*> padList;
for( BOARD_ITEM* item : aItems )
{
switch( item->Type() )
{
case PCB_FOOTPRINT_T:
{
for( PAD* pad : static_cast<FOOTPRINT*>( item )->Pads() )
{
if( pad->IsConnected() )
{
netcodeList.push_back( pad->GetNetCode() );
padList.push_back( pad );
}
}
break;
}
case PCB_PAD_T:
{
PAD* pad = static_cast<PAD*>( item );
if( pad->IsConnected() )
{
netcodeList.push_back( pad->GetNetCode() );
padList.push_back( pad );
}
break;
}
default:
break;
}
}
// Sort for binary search
std::sort( padList.begin(), padList.end() );
// remove all duplicates
netcodeList.sort();
netcodeList.unique();
selectAllConnectedTracks( padList, STOP_AT_PAD );
// now we need to find all footprints that are connected to each of these nets then we need
// to determine if these footprints are in the list of footprints
std::vector<int> removeCodeList;
std::shared_ptr<CONNECTIVITY_DATA> conn = board()->GetConnectivity();
for( int netCode : netcodeList )
{
for( BOARD_CONNECTED_ITEM* pad : conn->GetNetItems( netCode, { PCB_PAD_T } ) )
{
if( !std::binary_search( padList.begin(), padList.end(), pad ) )
{
// if we cannot find the pad in the padList then we can assume that that pad
// should not be used, therefore invalidate this netcode.
removeCodeList.push_back( netCode );
break;
}
}
}
for( int removeCode : removeCodeList )
netcodeList.remove( removeCode );
std::unordered_set<BOARD_ITEM*> localConnectionList;
for( int netCode : netcodeList )
{
for( BOARD_ITEM* item : conn->GetNetItems( netCode, { PCB_TRACE_T,
PCB_ARC_T,
PCB_VIA_T,
PCB_SHAPE_T } ) )
{
localConnectionList.insert( item );
}
}
for( BOARD_ITEM* item : localConnectionList )
select( item );
}
int PCB_SELECTION_TOOL::syncSelection( const TOOL_EVENT& aEvent )
{
std::vector<BOARD_ITEM*>* items = aEvent.Parameter<std::vector<BOARD_ITEM*>*>();
if( items )
doSyncSelection( *items, false );
return 0;
}
int PCB_SELECTION_TOOL::syncSelectionWithNets( const TOOL_EVENT& aEvent )
{
std::vector<BOARD_ITEM*>* items = aEvent.Parameter<std::vector<BOARD_ITEM*>*>();
if( items )
doSyncSelection( *items, true );
return 0;
}
void PCB_SELECTION_TOOL::doSyncSelection( const std::vector<BOARD_ITEM*>& aItems, bool aWithNets )
{
if( m_selection.Front() && m_selection.Front()->IsMoving() )
return;
ClearSelection( true /*quiet mode*/ );
// Perform individual selection of each item before processing the event.
for( BOARD_ITEM* item : aItems )
select( item );
if( aWithNets )
selectConnections( aItems );
BOX2I bbox = m_selection.GetBoundingBox();
if( bbox.GetWidth() != 0 && bbox.GetHeight() != 0 )
{
if( m_frame->GetPcbNewSettings()->m_CrossProbing.center_on_items )
{
if( m_frame->GetPcbNewSettings()->m_CrossProbing.zoom_to_fit )
ZoomFitCrossProbeBBox( bbox );
m_frame->FocusOnLocation( bbox.Centre() );
}
}
view()->UpdateAllLayersColor();
m_frame->GetCanvas()->ForceRefresh();
if( m_selection.Size() > 0 )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
int PCB_SELECTION_TOOL::selectSheetContents( const TOOL_EVENT& aEvent )
{
ClearSelection( true /*quiet mode*/ );
wxString sheetPath = *aEvent.Parameter<wxString*>();
selectAllItemsOnSheet( sheetPath );
zoomFitSelection();
if( m_selection.Size() > 0 )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
int PCB_SELECTION_TOOL::selectSameSheet( const TOOL_EVENT& aEvent )
{
// this function currently only supports footprints since they are only on one sheet.
EDA_ITEM* item = m_selection.Front();
if( !item )
return 0;
if( item->Type() != PCB_FOOTPRINT_T )
return 0;
FOOTPRINT* footprint = dynamic_cast<FOOTPRINT*>( item );
if( !footprint || footprint->GetPath().empty() )
return 0;
ClearSelection( true /*quiet mode*/ );
// get the sheet path only.
wxString sheetPath = footprint->GetPath().AsString().BeforeLast( '/' );
if( sheetPath.IsEmpty() )
sheetPath += '/';
selectAllItemsOnSheet( sheetPath );
// Inform other potentially interested tools
if( m_selection.Size() > 0 )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
void PCB_SELECTION_TOOL::zoomFitSelection()
{
// Should recalculate the view to zoom in on the selection.
BOX2I selectionBox = m_selection.GetBoundingBox();
KIGFX::VIEW* view = getView();
VECTOR2D screenSize = view->ToWorld( ToVECTOR2D( m_frame->GetCanvas()->GetClientSize() ),
false );
screenSize.x = std::max( 10.0, screenSize.x );
screenSize.y = std::max( 10.0, screenSize.y );
if( selectionBox.GetWidth() != 0 || selectionBox.GetHeight() != 0 )
{
VECTOR2D vsize = selectionBox.GetSize();
double scale = view->GetScale() / std::max( fabs( vsize.x / screenSize.x ),
fabs( vsize.y / screenSize.y ) );
view->SetScale( scale );
view->SetCenter( selectionBox.Centre() );
view->Add( &m_selection );
}
m_frame->GetCanvas()->ForceRefresh();
}
void PCB_SELECTION_TOOL::ZoomFitCrossProbeBBox( const BOX2I& aBBox )
{
// Should recalculate the view to zoom in on the bbox.
KIGFX::VIEW* view = getView();
if( aBBox.GetWidth() == 0 )
return;
BOX2I bbox = aBBox;
bbox.Normalize();
//#define DEFAULT_PCBNEW_CODE // Un-comment for normal full zoom KiCad algorithm
#ifdef DEFAULT_PCBNEW_CODE
auto bbSize = bbox.Inflate( bbox.GetWidth() * 0.2f ).GetSize();
auto screenSize = view->ToWorld( GetCanvas()->GetClientSize(), false );
// The "fabs" on x ensures the right answer when the view is flipped
screenSize.x = std::max( 10.0, fabs( screenSize.x ) );
screenSize.y = std::max( 10.0, screenSize.y );
double ratio = std::max( fabs( bbSize.x / screenSize.x ), fabs( bbSize.y / screenSize.y ) );
// Try not to zoom on every cross-probe; it gets very noisy
if( crossProbingSettings.zoom_to_fit && ( ratio < 0.5 || ratio > 1.0 ) )
view->SetScale( view->GetScale() / ratio );
#endif // DEFAULT_PCBNEW_CODE
#ifndef DEFAULT_PCBNEW_CODE // Do the scaled zoom
auto bbSize = bbox.Inflate( KiROUND( bbox.GetWidth() * 0.2 ) ).GetSize();
VECTOR2D screenSize = view->ToWorld( ToVECTOR2D( m_frame->GetCanvas()->GetClientSize() ), false );
// This code tries to come up with a zoom factor that doesn't simply zoom in
// to the cross probed component, but instead shows a reasonable amount of the
// circuit around it to provide context. This reduces or eliminates the need
// to manually change the zoom because it's too close.
// Using the default text height as a constant to compare against, use the
// height of the bounding box of visible items for a footprint to figure out
// if this is a big footprint (like a processor) or a small footprint (like a resistor).
// This ratio is not useful by itself as a scaling factor. It must be "bent" to
// provide good scaling at varying component sizes. Bigger components need less
// scaling than small ones.
double currTextHeight = pcbIUScale.mmToIU( DEFAULT_TEXT_SIZE );
double compRatio = bbSize.y / currTextHeight; // Ratio of component to text height
// This will end up as the scaling factor we apply to "ratio".
double compRatioBent = 1.0;
// This is similar to the original KiCad code that scaled the zoom to make sure
// components were visible on screen. It's simply a ratio of screen size to
// component size, and its job is to zoom in to make the component fullscreen.
// Earlier in the code the component BBox is given a 20% margin to add some
// breathing room. We compare the height of this enlarged component bbox to the
// default text height. If a component will end up with the sides clipped, we
// adjust later to make sure it fits on screen.
//
// The "fabs" on x ensures the right answer when the view is flipped
screenSize.x = std::max( 10.0, fabs( screenSize.x ) );
screenSize.y = std::max( 10.0, screenSize.y );
double ratio = std::max( -1.0, fabs( bbSize.y / screenSize.y ) );
// Original KiCad code for how much to scale the zoom
double kicadRatio = std::max( fabs( bbSize.x / screenSize.x ),
fabs( bbSize.y / screenSize.y ) );
// LUT to scale zoom ratio to provide reasonable schematic context. Must work
// with footprints of varying sizes (e.g. 0402 package and 200 pin BGA).
// "first" is used as the input and "second" as the output
//
// "first" = compRatio (footprint height / default text height)
// "second" = Amount to scale ratio by
std::vector<std::pair<double, double>> lut {
{ 1, 8 },
{ 1.5, 5 },
{ 3, 3 },
{ 4.5, 2.5 },
{ 8, 2.0 },
{ 12, 1.7 },
{ 16, 1.5 },
{ 24, 1.3 },
{ 32, 1.0 },
};
std::vector<std::pair<double, double>>::iterator it;
compRatioBent = lut.back().second; // Large component default
if( compRatio >= lut.front().first )
{
// Use LUT to do linear interpolation of "compRatio" within "first", then
// use that result to linearly interpolate "second" which gives the scaling
// factor needed.
for( it = lut.begin(); it < lut.end() - 1; it++ )
{
if( it->first <= compRatio && next( it )->first >= compRatio )
{
double diffx = compRatio - it->first;
double diffn = next( it )->first - it->first;
compRatioBent = it->second + ( next( it )->second - it->second ) * diffx / diffn;
break; // We have our interpolated value
}
}
}
else
{
compRatioBent = lut.front().second; // Small component default
}
// If the width of the part we're probing is bigger than what the screen width will be
// after the zoom, then punt and use the KiCad zoom algorithm since it guarantees the
// part's width will be encompassed within the screen. This will apply to parts that
// are much wider than they are tall.
if( bbSize.x > screenSize.x * ratio * compRatioBent )
{
// Use standard KiCad zoom algorithm for parts too wide to fit screen/
ratio = kicadRatio;
compRatioBent = 1.0; // Reset so we don't modify the "KiCad" ratio
wxLogTrace( "CROSS_PROBE_SCALE", "Part TOO WIDE for screen. Using normal KiCad zoom ratio: %1.5f", ratio );
}
// Now that "compRatioBent" holds our final scaling factor we apply it to the original
// fullscreen zoom ratio to arrive at the final ratio itself.
ratio *= compRatioBent;
bool alwaysZoom = false; // DEBUG - allows us to minimize zooming or not
// Try not to zoom on every cross-probe; it gets very noisy
if( ( ratio < 0.5 || ratio > 1.0 ) || alwaysZoom )
view->SetScale( view->GetScale() / ratio );
#endif // ifndef DEFAULT_PCBNEW_CODE
}
void PCB_SELECTION_TOOL::FindItem( BOARD_ITEM* aItem )
{
bool cleared = false;
if( m_selection.GetSize() > 0 )
{
// Don't fire an event now; most of the time it will be redundant as we're about to
// fire a SelectedEvent.
cleared = true;
ClearSelection( true /*quiet mode*/ );
}
if( aItem )
{
switch( aItem->Type() )
{
case PCB_NETINFO_T:
{
int netCode = static_cast<NETINFO_ITEM*>( aItem )->GetNetCode();
if( netCode > 0 )
{
SelectAllItemsOnNet( netCode, true );
m_frame->FocusOnLocation( aItem->GetCenter() );
}
break;
}
default:
select( aItem );
m_frame->FocusOnLocation( aItem->GetPosition() );
}
// If the item has a bounding box, then zoom out if needed
if( aItem->GetBoundingBox().GetHeight() > 0 && aItem->GetBoundingBox().GetWidth() > 0 )
{
// This adds some margin
double marginFactor = 2;
KIGFX::PCB_VIEW* pcbView = canvas()->GetView();
BOX2D screenBox = pcbView->GetViewport();
VECTOR2D screenSize = screenBox.GetSize();
BOX2I screenRect = BOX2ISafe( screenBox.GetOrigin(), screenSize / marginFactor );
if( !screenRect.Contains( aItem->GetBoundingBox() ) )
{
double scaleX = screenSize.x / static_cast<double>( aItem->GetBoundingBox().GetWidth() );
double scaleY = screenSize.y / static_cast<double>( aItem->GetBoundingBox().GetHeight() );
scaleX /= marginFactor;
scaleY /= marginFactor;
double scale = scaleX > scaleY ? scaleY : scaleX;
if( scale < 1 ) // Don't zoom in, only zoom out
{
pcbView->SetScale( pcbView->GetScale() * ( scale ) );
//Let's refocus because there is an algorithm to avoid dialogs in there.
m_frame->FocusOnLocation( aItem->GetCenter() );
}
}
}
// Inform other potentially interested tools
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
}
else if( cleared )
{
m_toolMgr->ProcessEvent( EVENTS::ClearedEvent );
}
m_frame->GetCanvas()->ForceRefresh();
}
/**
* Determine if an item is included by the filter specified.
*
* @return true if aItem should be selected by this filter (i..e not filtered out)
*/
static bool itemIsIncludedByFilter( const BOARD_ITEM& aItem, const BOARD& aBoard,
const DIALOG_FILTER_SELECTION::OPTIONS& aFilterOptions )
{
switch( aItem.Type() )
{
case PCB_FOOTPRINT_T:
{
const FOOTPRINT& footprint = static_cast<const FOOTPRINT&>( aItem );
return aFilterOptions.includeModules && ( aFilterOptions.includeLockedModules
|| !footprint.IsLocked() );
}
case PCB_TRACE_T:
case PCB_ARC_T:
return aFilterOptions.includeTracks;
case PCB_VIA_T:
return aFilterOptions.includeVias;
case PCB_ZONE_T:
return aFilterOptions.includeZones;
case PCB_SHAPE_T:
case PCB_TARGET_T:
case PCB_DIM_ALIGNED_T:
case PCB_DIM_CENTER_T:
case PCB_DIM_RADIAL_T:
case PCB_DIM_ORTHOGONAL_T:
case PCB_DIM_LEADER_T:
if( aItem.GetLayer() == Edge_Cuts )
return aFilterOptions.includeBoardOutlineLayer;
else
return aFilterOptions.includeItemsOnTechLayers;
case PCB_FIELD_T:
case PCB_TEXT_T:
case PCB_TEXTBOX_T:
case PCB_TABLE_T:
case PCB_TABLECELL_T:
return aFilterOptions.includePcbTexts;
default:
// Filter dialog is inclusive, not exclusive. If it's not included, then it doesn't
// get selected.
return false;
}
}
int PCB_SELECTION_TOOL::filterSelection( const TOOL_EVENT& aEvent )
{
const BOARD& board = *getModel<BOARD>();
DIALOG_FILTER_SELECTION::OPTIONS& opts = m_priv->m_filterOpts;
DIALOG_FILTER_SELECTION dlg( m_frame, opts );
const int cmd = dlg.ShowModal();
if( cmd != wxID_OK )
return 0;
// copy current selection
std::deque<EDA_ITEM*> selection = m_selection.GetItems();
ClearSelection( true /*quiet mode*/ );
// re-select items from the saved selection according to the dialog options
for( EDA_ITEM* i : selection )
{
if( !i->IsBOARD_ITEM() )
continue;
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( i );
bool include = itemIsIncludedByFilter( *item, board, opts );
if( include )
select( item );
}
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
void PCB_SELECTION_TOOL::FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect )
{
if( aCollector.GetCount() == 0 )
return;
std::set<BOARD_ITEM*> rejected;
for( EDA_ITEM* i : aCollector )
{
if( !i->IsBOARD_ITEM() )
continue;
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( i );
if( !itemPassesFilter( item, aMultiSelect ) )
rejected.insert( item );
}
for( BOARD_ITEM* item : rejected )
aCollector.Remove( item );
}
bool PCB_SELECTION_TOOL::itemPassesFilter( BOARD_ITEM* aItem, bool aMultiSelect )
{
if( !m_filter.lockedItems )
{
if( aItem->IsLocked() || ( aItem->GetParent() && aItem->GetParent()->IsLocked() ) )
{
if( aItem->Type() == PCB_PAD_T && !aMultiSelect )
{
// allow a single pad to be selected -- there are a lot of operations that
// require this so we allow this one inconsistency
}
else
{
return false;
}
}
}
if( !aItem )
return false;
KICAD_T itemType = aItem->Type();
if( itemType == PCB_GENERATOR_T )
{
if( static_cast<PCB_GENERATOR*>( aItem )->GetItems().empty() )
{
if( !m_filter.otherItems )
return false;
}
else
{
itemType = ( *static_cast<PCB_GENERATOR*>( aItem )->GetItems().begin() )->Type();
}
}
switch( itemType )
{
case PCB_FOOTPRINT_T:
if( !m_filter.footprints )
return false;
break;
case PCB_PAD_T:
if( !m_filter.pads )
return false;
break;
case PCB_TRACE_T:
case PCB_ARC_T:
if( !m_filter.tracks )
return false;
break;
case PCB_VIA_T:
if( !m_filter.vias )
return false;
break;
case PCB_ZONE_T:
{
ZONE* zone = static_cast<ZONE*>( aItem );
if( ( !m_filter.zones && !zone->GetIsRuleArea() )
|| ( !m_filter.keepouts && zone->GetIsRuleArea() ) )
{
return false;
}
// m_SolderMaskBridges zone is a special zone, only used to showsolder mask briges
// after running DRC. it is not really a board item.
// Never select it or delete by a Commit.
if( zone == m_frame->GetBoard()->m_SolderMaskBridges )
return false;
break;
}
case PCB_SHAPE_T:
case PCB_TARGET_T:
if( !m_filter.graphics )
return false;
break;
case PCB_REFERENCE_IMAGE_T:
if( !m_filter.graphics )
return false;
// a reference image living in a footprint must not be selected inside the board editor
if( !m_isFootprintEditor && aItem->GetParentFootprint() )
return false;
break;
case PCB_FIELD_T:
case PCB_TEXT_T:
case PCB_TEXTBOX_T:
case PCB_TABLE_T:
case PCB_TABLECELL_T:
if( !m_filter.text )
return false;
break;
case PCB_DIM_ALIGNED_T:
case PCB_DIM_CENTER_T:
case PCB_DIM_RADIAL_T:
case PCB_DIM_ORTHOGONAL_T:
case PCB_DIM_LEADER_T:
if( !m_filter.dimensions )
return false;
break;
default:
if( !m_filter.otherItems )
return false;
}
return true;
}
void PCB_SELECTION_TOOL::ClearSelection( bool aQuietMode )
{
if( m_selection.Empty() )
return;
while( m_selection.GetSize() )
unhighlight( m_selection.Front(), SELECTED, &m_selection );
view()->Update( &m_selection );
m_selection.SetIsHover( false );
m_selection.ClearReferencePoint();
// Inform other potentially interested tools
if( !aQuietMode )
{
m_toolMgr->ProcessEvent( EVENTS::ClearedEvent );
m_toolMgr->RunAction( PCB_ACTIONS::hideLocalRatsnest );
}
}
void PCB_SELECTION_TOOL::RebuildSelection()
{
m_selection.Clear();
bool enteredGroupFound = false;
INSPECTOR_FUNC inspector =
[&]( EDA_ITEM* item, void* testData )
{
if( item->IsSelected() )
{
EDA_ITEM* parent = item->GetParent();
// Let selected parents handle their children.
if( parent && parent->IsSelected() )
return INSPECT_RESULT::CONTINUE;
highlight( item, SELECTED, &m_selection );
}
if( item->Type() == PCB_GROUP_T )
{
if( item == m_enteredGroup )
{
item->SetFlags( ENTERED );
enteredGroupFound = true;
}
else
{
item->ClearFlags( ENTERED );
}
}
return INSPECT_RESULT::CONTINUE;
};
board()->Visit( inspector, nullptr, m_isFootprintEditor ? GENERAL_COLLECTOR::FootprintItems
: GENERAL_COLLECTOR::AllBoardItems );
if( !enteredGroupFound )
{
m_enteredGroupOverlay.Clear();
m_enteredGroup = nullptr;
}
}
bool PCB_SELECTION_TOOL::Selectable( const BOARD_ITEM* aItem, bool checkVisibilityOnly ) const
{
const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings();
const PCB_DISPLAY_OPTIONS& options = frame()->GetDisplayOptions();
auto visibleLayers =
[&]() -> LSET
{
if( m_isFootprintEditor )
{
LSET set;
for( PCB_LAYER_ID layer : LSET::AllLayersMask() )
set.set( layer, view()->IsLayerVisible( layer ) );
return set;
}
else
{
return board()->GetVisibleLayers();
}
};
auto layerVisible =
[&]( PCB_LAYER_ID aLayer )
{
if( m_isFootprintEditor )
return view()->IsLayerVisible( aLayer );
else
return board()->IsLayerVisible( aLayer );
};
if( settings->GetHighContrast() )
{
const std::set<int> activeLayers = settings->GetHighContrastLayers();
bool onActiveLayer = false;
for( int layer : activeLayers )
{
// NOTE: Only checking the regular layers (not GAL meta-layers)
if( layer < PCB_LAYER_ID_COUNT && aItem->IsOnLayer( ToLAYER_ID( layer ) ) )
{
onActiveLayer = true;
break;
}
}
if( !onActiveLayer && aItem->Type() != PCB_MARKER_T )
{
// We do not want to select items that are in the background
return false;
}
}
if( aItem->Type() == PCB_FOOTPRINT_T )
{
const FOOTPRINT* footprint = static_cast<const FOOTPRINT*>( aItem );
// In footprint editor, we do not want to select the footprint itself.
if( m_isFootprintEditor )
return false;
// Allow selection of footprints if some part of the footprint is visible.
if( footprint->GetSide() != UNDEFINED_LAYER && !m_skip_heuristics )
{
LSET boardSide = footprint->IsFlipped() ? LSET::BackMask() : LSET::FrontMask();
if( !( visibleLayers() & boardSide ).any() )
return false;
}
// If the footprint has no items except the reference and value fields, include the
// footprint in the selections.
if( footprint->GraphicalItems().empty()
&& footprint->Pads().empty()
&& footprint->Zones().empty() )
{
return true;
}
for( const BOARD_ITEM* item : footprint->GraphicalItems() )
{
if( Selectable( item, true ) )
return true;
}
for( const PAD* pad : footprint->Pads() )
{
if( Selectable( pad, true ) )
return true;
}
for( const ZONE* zone : footprint->Zones() )
{
if( Selectable( zone, true ) )
return true;
}
return false;
}
else if( aItem->Type() == PCB_GROUP_T )
{
PCB_GROUP* group = const_cast<PCB_GROUP*>( static_cast<const PCB_GROUP*>( aItem ) );
// Similar to logic for footprint, a group is selectable if any of its members are.
// (This recurses.)
for( BOARD_ITEM* item : group->GetBoardItems() )
{
if( Selectable( item, true ) )
return true;
}
return false;
}
if( aItem->GetParentGroup() && aItem->GetParentGroup()->AsEdaItem()->Type() == PCB_GENERATOR_T )
return false;
const ZONE* zone = nullptr;
const PCB_VIA* via = nullptr;
const PAD* pad = nullptr;
const PCB_TEXT* text = nullptr;
const PCB_FIELD* field = nullptr;
const PCB_MARKER* marker = nullptr;
const PCB_TABLECELL* cell = nullptr;
// Most footprint children can only be selected in the footprint editor.
if( aItem->GetParentFootprint() && !m_isFootprintEditor && !checkVisibilityOnly )
{
if( aItem->Type() != PCB_FIELD_T && aItem->Type() != PCB_PAD_T && aItem->Type() != PCB_TEXT_T )
return false;
}
switch( aItem->Type() )
{
case PCB_ZONE_T:
if( !board()->IsElementVisible( LAYER_ZONES ) || ( options.m_ZoneOpacity == 0.00 ) )
return false;
zone = static_cast<const ZONE*>( aItem );
// A teardrop is modelled as a property of a via, pad or the board (for track-to-track
// teardrops). The underlying zone is only an implementation detail.
if( zone->IsTeardropArea() && !board()->LegacyTeardrops() )
return false;
// zones can exist on multiple layers!
if( !( zone->GetLayerSet() & visibleLayers() ).any() )
return false;
break;
case PCB_TRACE_T:
case PCB_ARC_T:
if( !board()->IsElementVisible( LAYER_TRACKS ) || ( options.m_TrackOpacity == 0.00 ) )
return false;
if( !layerVisible( aItem->GetLayer() ) )
return false;
break;
case PCB_VIA_T:
if( !board()->IsElementVisible( LAYER_VIAS ) || ( options.m_ViaOpacity == 0.00 ) )
return false;
via = static_cast<const PCB_VIA*>( aItem );
// For vias it is enough if only one of its layers is visible
if( !( visibleLayers() & via->GetLayerSet() ).any() )
return false;
break;
case PCB_FIELD_T:
field = static_cast<const PCB_FIELD*>( aItem );
if( !field->IsVisible() )
return false;
if( field->IsReference() && !view()->IsLayerVisible( LAYER_FP_REFERENCES ) )
return false;
if( field->IsValue() && !view()->IsLayerVisible( LAYER_FP_VALUES ) )
return false;
// Handle all other fields with normal text visibility controls
KI_FALLTHROUGH;
case PCB_TEXT_T:
text = static_cast<const PCB_TEXT*>( aItem );
if( !layerVisible( text->GetLayer() ) )
return false;
// Apply the LOD visibility test as well
if( !view()->IsVisible( text ) )
return false;
if( aItem->GetParentFootprint() )
{
int controlLayer = LAYER_FP_TEXT;
if( text->GetText() == wxT( "${REFERENCE}" ) )
controlLayer = LAYER_FP_REFERENCES;
else if( text->GetText() == wxT( "${VALUE}" ) )
controlLayer = LAYER_FP_VALUES;
if( !view()->IsLayerVisible( controlLayer ) )
return false;
}
break;
case PCB_REFERENCE_IMAGE_T:
if( options.m_ImageOpacity == 0.00 )
return false;
// Bitmap images on board are hidden if LAYER_DRAW_BITMAPS is not visible
if( !view()->IsLayerVisible( LAYER_DRAW_BITMAPS ) )
return false;
if( !layerVisible( aItem->GetLayer() ) )
return false;
break;
case PCB_SHAPE_T:
if( options.m_FilledShapeOpacity == 0.0 && static_cast<const PCB_SHAPE*>( aItem )->IsAnyFill() )
return false;
if( !layerVisible( aItem->GetLayer() ) )
return false;
break;
case PCB_TEXTBOX_T:
case PCB_TABLE_T:
if( !layerVisible( aItem->GetLayer() ) )
return false;
break;
case PCB_TABLECELL_T:
cell = static_cast<const PCB_TABLECELL*>( aItem );
if( !layerVisible( aItem->GetLayer() ) )
return false;
if( cell->GetRowSpan() == 0 || cell->GetColSpan() == 0 )
return false;
break;
case PCB_DIM_ALIGNED_T:
case PCB_DIM_LEADER_T:
case PCB_DIM_CENTER_T:
case PCB_DIM_RADIAL_T:
case PCB_DIM_ORTHOGONAL_T:
if( !layerVisible( aItem->GetLayer() ) )
return false;
break;
case PCB_PAD_T:
if( options.m_PadOpacity == 0.00 )
return false;
pad = static_cast<const PAD*>( aItem );
if( pad->GetAttribute() == PAD_ATTRIB::PTH || pad->GetAttribute() == PAD_ATTRIB::NPTH )
{
// A pad's hole is visible on every layer the pad is visible on plus many layers the
// pad is not visible on -- so we only need to check for any visible hole layers.
if( !( visibleLayers() & LSET::PhysicalLayersMask() ).any() )
return false;
}
else
{
if( !( pad->GetLayerSet() & visibleLayers() ).any() )
return false;
}
break;
case PCB_MARKER_T:
marker = static_cast<const PCB_MARKER*>( aItem );
if( marker && marker->IsExcluded() && !board()->IsElementVisible( LAYER_DRC_EXCLUSION ) )
return false;
break;
// These are not selectable
case PCB_NETINFO_T:
case NOT_USED:
case TYPE_NOT_INIT:
return false;
default: // Suppress warnings
break;
}
return true;
}
void PCB_SELECTION_TOOL::select( EDA_ITEM* aItem )
{
if( !aItem || aItem->IsSelected() || !aItem->IsBOARD_ITEM() )
return;
if( aItem->Type() == PCB_PAD_T )
{
FOOTPRINT* footprint = static_cast<FOOTPRINT*>( aItem->GetParent() );
if( m_selection.Contains( footprint ) )
return;
}
if( m_enteredGroup && !PCB_GROUP::WithinScope( static_cast<BOARD_ITEM*>( aItem ), m_enteredGroup,
m_isFootprintEditor ) )
{
ExitGroup();
}
highlight( aItem, SELECTED, &m_selection );
}
void PCB_SELECTION_TOOL::unselect( EDA_ITEM* aItem )
{
unhighlight( aItem, SELECTED, &m_selection );
}
void PCB_SELECTION_TOOL::highlight( EDA_ITEM* aItem, int aMode, SELECTION* aGroup )
{
if( aGroup )
aGroup->Add( aItem );
highlightInternal( aItem, aMode, aGroup != nullptr );
view()->Update( aItem, KIGFX::REPAINT );
// Many selections are very temporal and updating the display each time just
// creates noise.
if( aMode == BRIGHTENED )
getView()->MarkTargetDirty( KIGFX::TARGET_OVERLAY );
}
void PCB_SELECTION_TOOL::highlightInternal( EDA_ITEM* aItem, int aMode, bool aUsingOverlay )
{
if( aMode == SELECTED )
aItem->SetSelected();
else if( aMode == BRIGHTENED )
aItem->SetBrightened();
if( aUsingOverlay && aMode != BRIGHTENED )
view()->Hide( aItem, true ); // Hide the original item, so it is shown only on overlay
if( aItem->IsBOARD_ITEM() )
{
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( aItem );
boardItem->RunOnChildren( std::bind( &PCB_SELECTION_TOOL::highlightInternal, this, _1, aMode, aUsingOverlay ),
RECURSE_MODE::RECURSE );
}
}
void PCB_SELECTION_TOOL::unhighlight( EDA_ITEM* aItem, int aMode, SELECTION* aGroup )
{
if( aGroup )
aGroup->Remove( aItem );
unhighlightInternal( aItem, aMode, aGroup != nullptr );
view()->Update( aItem, KIGFX::REPAINT );
// Many selections are very temporal and updating the display each time just creates noise.
if( aMode == BRIGHTENED )
getView()->MarkTargetDirty( KIGFX::TARGET_OVERLAY );
}
void PCB_SELECTION_TOOL::unhighlightInternal( EDA_ITEM* aItem, int aMode, bool aUsingOverlay )
{
if( aMode == SELECTED )
aItem->ClearSelected();
else if( aMode == BRIGHTENED )
aItem->ClearBrightened();
if( aUsingOverlay && aMode != BRIGHTENED )
{
view()->Hide( aItem, false ); // Restore original item visibility...
view()->Update( aItem ); // ... and make sure it's redrawn un-selected
}
if( aItem->IsBOARD_ITEM() )
{
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( aItem );
boardItem->RunOnChildren( std::bind( &PCB_SELECTION_TOOL::unhighlightInternal, this, _1, aMode, aUsingOverlay ),
RECURSE_MODE::RECURSE );
}
}
bool PCB_SELECTION_TOOL::selectionContains( const VECTOR2I& aPoint ) const
{
const unsigned GRIP_MARGIN = 20;
int margin = KiROUND( getView()->ToWorld( GRIP_MARGIN ) );
// Check if the point is located close to any of the currently selected items
for( EDA_ITEM* item : m_selection )
{
if( !item->IsBOARD_ITEM() )
continue;
BOX2I itemBox = item->ViewBBox();
itemBox.Inflate( margin ); // Give some margin for gripping an item
if( itemBox.Contains( aPoint ) )
{
if( item->HitTest( aPoint, margin ) )
return true;
bool found = false;
if( PCB_GROUP* group = dynamic_cast<PCB_GROUP*>( item ) )
{
group->RunOnChildren(
[&]( BOARD_ITEM* aItem )
{
if( aItem->HitTest( aPoint, margin ) )
found = true;
},
RECURSE_MODE::RECURSE );
}
if( found )
return true;
}
}
return false;
}
int PCB_SELECTION_TOOL::hitTestDistance( const VECTOR2I& aWhere, BOARD_ITEM* aItem, int aMaxDistance ) const
{
BOX2D viewportD = getView()->GetViewport();
BOX2I viewport = BOX2ISafe( viewportD );
int distance = INT_MAX;
SEG loc( aWhere, aWhere );
switch( aItem->Type() )
{
case PCB_FIELD_T:
case PCB_TEXT_T:
{
PCB_TEXT* text = static_cast<PCB_TEXT*>( aItem );
// Add a bit of slop to text-shapes
if( text->GetEffectiveTextShape()->Collide( loc, aMaxDistance, &distance ) )
distance = std::clamp( distance - ( aMaxDistance / 2 ), 0, distance );
break;
}
case PCB_TEXTBOX_T:
case PCB_TABLECELL_T:
{
PCB_TEXTBOX* textbox = static_cast<PCB_TEXTBOX*>( aItem );
// Add a bit of slop to text-shapes
if( textbox->GetEffectiveTextShape()->Collide( loc, aMaxDistance, &distance ) )
distance = std::clamp( distance - ( aMaxDistance / 2 ), 0, distance );
break;
}
case PCB_TABLE_T:
{
PCB_TABLE* table = static_cast<PCB_TABLE*>( aItem );
for( PCB_TABLECELL* cell : table->GetCells() )
{
// Add a bit of slop to text-shapes
if( cell->GetEffectiveTextShape()->Collide( loc, aMaxDistance, &distance ) )
distance = std::clamp( distance - ( aMaxDistance / 2 ), 0, distance );
}
break;
}
case PCB_ZONE_T:
{
ZONE* zone = static_cast<ZONE*>( aItem );
// Zone borders are very specific
if( zone->HitTestForEdge( aWhere, aMaxDistance / 2 ) )
distance = 0;
else if( zone->HitTestForEdge( aWhere, aMaxDistance ) )
distance = aMaxDistance / 2;
else
aItem->GetEffectiveShape()->Collide( loc, aMaxDistance, &distance );
break;
}
case PCB_FOOTPRINT_T:
{
FOOTPRINT* footprint = static_cast<FOOTPRINT*>( aItem );
BOX2I bbox = footprint->GetBoundingBox( false );
try
{
footprint->GetBoundingHull().Collide( loc, aMaxDistance, &distance );
}
catch( const std::exception& e )
{
wxFAIL_MSG( wxString::Format( wxT( "Clipper exception occurred: %s" ), e.what() ) );
}
// Consider footprints larger than the viewport only as a last resort
if( bbox.GetHeight() > viewport.GetHeight() || bbox.GetWidth() > viewport.GetWidth() )
distance = INT_MAX / 2;
break;
}
case PCB_MARKER_T:
{
PCB_MARKER* marker = static_cast<PCB_MARKER*>( aItem );
SHAPE_LINE_CHAIN polygon;
marker->ShapeToPolygon( polygon );
polygon.Move( marker->GetPos() );
polygon.Collide( loc, aMaxDistance, &distance );
break;
}
case PCB_GROUP_T:
case PCB_GENERATOR_T:
{
PCB_GROUP* group = static_cast<PCB_GROUP*>( aItem );
for( BOARD_ITEM* member : group->GetBoardItems() )
distance = std::min( distance, hitTestDistance( aWhere, member, aMaxDistance ) );
break;
}
case PCB_PAD_T:
{
static_cast<PAD*>( aItem )->Padstack().ForEachUniqueLayer(
[&]( PCB_LAYER_ID aLayer )
{
int layerDistance = INT_MAX;
aItem->GetEffectiveShape( aLayer )->Collide( loc, aMaxDistance, &layerDistance );
distance = std::min( distance, layerDistance );
} );
break;
}
default:
aItem->GetEffectiveShape()->Collide( loc, aMaxDistance, &distance );
break;
}
return distance;
}
void PCB_SELECTION_TOOL::pruneObscuredSelectionCandidates( GENERAL_COLLECTOR& aCollector ) const
{
wxCHECK( m_frame, /* void */ );
if( aCollector.GetCount() < 2 )
return;
const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings();
wxCHECK( settings, /* void */ );
PCB_LAYER_ID activeLayer = m_frame->GetActiveLayer();
LSET visibleLayers = m_frame->GetBoard()->GetVisibleLayers();
LSET enabledLayers = m_frame->GetBoard()->GetEnabledLayers();
LSEQ enabledLayerStack = enabledLayers.SeqStackupTop2Bottom( activeLayer );
wxCHECK( !enabledLayerStack.empty(), /* void */ );
auto isZoneFillKeepout =
[]( const BOARD_ITEM* aItem ) -> bool
{
if( aItem->Type() == PCB_ZONE_T )
{
const ZONE* zone = static_cast<const ZONE*>( aItem );
if( zone->GetIsRuleArea() && zone->GetDoNotAllowZoneFills() )
return true;
}
return false;
};
std::vector<LAYER_OPACITY_ITEM> opacityStackup;
for( int i = 0; i < aCollector.GetCount(); i++ )
{
const BOARD_ITEM* item = aCollector[i];
LSET itemLayers = item->GetLayerSet() & enabledLayers & visibleLayers;
LSEQ itemLayerSeq = itemLayers.Seq( enabledLayerStack );
for( PCB_LAYER_ID layer : itemLayerSeq )
{
COLOR4D color = settings->GetColor( item, layer );
if( color.a == 0 )
continue;
LAYER_OPACITY_ITEM opacityItem;
opacityItem.m_Layer = layer;
opacityItem.m_Opacity = color.a;
opacityItem.m_Item = item;
if( isZoneFillKeepout( item ) )
opacityItem.m_Opacity = 0.0;
opacityStackup.emplace_back( opacityItem );
}
}
std::sort( opacityStackup.begin(), opacityStackup.end(),
[&]( const LAYER_OPACITY_ITEM& aLhs, const LAYER_OPACITY_ITEM& aRhs ) -> bool
{
int retv = enabledLayerStack.TestLayers( aLhs.m_Layer, aRhs.m_Layer );
if( retv )
return retv > 0;
return aLhs.m_Opacity > aRhs.m_Opacity;
} );
std::set<const BOARD_ITEM*> visibleItems;
std::set<const BOARD_ITEM*> itemsToRemove;
double minAlphaLimit = ADVANCED_CFG::GetCfg().m_PcbSelectionVisibilityRatio;
double currentStackupOpacity = 0.0;
PCB_LAYER_ID lastVisibleLayer = PCB_LAYER_ID::UNDEFINED_LAYER;
for( const LAYER_OPACITY_ITEM& opacityItem : opacityStackup )
{
if( lastVisibleLayer == PCB_LAYER_ID::UNDEFINED_LAYER )
{
currentStackupOpacity = opacityItem.m_Opacity;
lastVisibleLayer = opacityItem.m_Layer;
visibleItems.emplace( opacityItem.m_Item );
continue;
}
// Objects to ignore and fallback to the old selection behavior.
auto ignoreItem =
[&]()
{
const BOARD_ITEM* item = opacityItem.m_Item;
wxCHECK( item, false );
// Check items that span multiple layers for visibility.
if( visibleItems.count( item ) )
return true;
// Don't prune child items of a footprint that is already visible.
if( item->GetParent()
&& ( item->GetParent()->Type() == PCB_FOOTPRINT_T )
&& visibleItems.count( item->GetParent() ) )
{
return true;
}
// Keepout zones are transparent but for some reason, PCB_PAINTER::GetColor()
// returns the color of the zone it prevents from filling.
if( isZoneFillKeepout( item ) )
return true;
return false;
};
// Everything on the currently selected layer is visible;
if( opacityItem.m_Layer == enabledLayerStack[0] )
{
visibleItems.emplace( opacityItem.m_Item );
}
else
{
double itemVisibility = opacityItem.m_Opacity * ( 1.0 - currentStackupOpacity );
if( ( itemVisibility <= minAlphaLimit ) && !ignoreItem() )
itemsToRemove.emplace( opacityItem.m_Item );
else
visibleItems.emplace( opacityItem.m_Item );
}
if( opacityItem.m_Layer != lastVisibleLayer )
{
currentStackupOpacity += opacityItem.m_Opacity * ( 1.0 - currentStackupOpacity );
currentStackupOpacity = std::min( currentStackupOpacity, 1.0 );
lastVisibleLayer = opacityItem.m_Layer;
}
}
for( const BOARD_ITEM* itemToRemove : itemsToRemove )
{
wxCHECK( aCollector.GetCount() > 1, /* void */ );
aCollector.Remove( itemToRemove );
}
}
// The general idea here is that if the user clicks directly on a small item inside a larger
// one, then they want the small item. The quintessential case of this is clicking on a pad
// within a footprint, but we also apply it for text within a footprint, footprints within
// larger footprints, and vias within either larger pads or longer tracks.
//
// These "guesses" presume there is area within the larger item to click in to select it. If
// an item is mostly covered by smaller items within it, then the guesses are inappropriate as
// there might not be any area left to click to select the larger item. In this case we must
// leave the items in the collector and bring up a Selection Clarification menu.
//
// We currently check for pads and text mostly covering a footprint, but we don't check for
// smaller footprints mostly covering a larger footprint.
//
void PCB_SELECTION_TOOL::GuessSelectionCandidates( GENERAL_COLLECTOR& aCollector,
const VECTOR2I& aWhere ) const
{
static const LSET silkLayers( { B_SilkS, F_SilkS } );
static const LSET courtyardLayers( { B_CrtYd, F_CrtYd } );
static std::vector<KICAD_T> singleLayerSilkTypes = { PCB_FIELD_T,
PCB_TEXT_T, PCB_TEXTBOX_T,
PCB_TABLE_T, PCB_TABLECELL_T,
PCB_SHAPE_T };
if( ADVANCED_CFG::GetCfg().m_PcbSelectionVisibilityRatio != 1.0 )
pruneObscuredSelectionCandidates( aCollector );
if( aCollector.GetCount() == 1 )
return;
std::set<BOARD_ITEM*> preferred;
std::set<BOARD_ITEM*> rejected;
VECTOR2I where( aWhere.x, aWhere.y );
const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings();
PCB_LAYER_ID activeLayer = m_frame->GetActiveLayer();
// If a silk layer is in front, we assume the user is working with silk and give preferential
// treatment to single-layer items on *either* silk layer.
if( silkLayers[activeLayer] )
{
for( int i = 0; i < aCollector.GetCount(); ++i )
{
BOARD_ITEM* item = aCollector[i];
if( item->IsType( singleLayerSilkTypes ) && silkLayers[ item->GetLayer() ] )
preferred.insert( item );
}
}
// Similarly, if a courtyard layer is in front, we assume the user is positioning footprints
// and give preferential treatment to footprints on *both* top and bottom.
else if( courtyardLayers[activeLayer] && settings->GetHighContrast() )
{
for( int i = 0; i < aCollector.GetCount(); ++i )
{
BOARD_ITEM* item = aCollector[i];
if( item->Type() == PCB_FOOTPRINT_T )
preferred.insert( item );
}
}
if( preferred.size() > 0 )
{
aCollector.Empty();
for( BOARD_ITEM* item : preferred )
aCollector.Append( item );
if( preferred.size() == 1 )
return;
}
// Prefer exact hits to sloppy ones
constexpr int MAX_SLOP = 5;
int singlePixel = KiROUND( aCollector.GetGuide()->OnePixelInIU() );
int maxSlop = KiROUND( MAX_SLOP * aCollector.GetGuide()->OnePixelInIU() );
int minSlop = INT_MAX;
std::map<BOARD_ITEM*, int> itemsBySloppiness;
for( int i = 0; i < aCollector.GetCount(); ++i )
{
BOARD_ITEM* item = aCollector[i];
int itemSlop = hitTestDistance( where, item, maxSlop );
itemsBySloppiness[ item ] = itemSlop;
if( itemSlop < minSlop )
minSlop = itemSlop;
}
// Prune sloppier items
if( minSlop < INT_MAX )
{
for( std::pair<BOARD_ITEM*, int> pair : itemsBySloppiness )
{
if( pair.second > minSlop + singlePixel )
aCollector.Transfer( pair.first );
}
}
// If the user clicked on a small item within a much larger one then it's pretty clear
// they're trying to select the smaller one.
constexpr double sizeRatio = 1.5;
std::vector<std::pair<BOARD_ITEM*, double>> itemsByArea;
for( int i = 0; i < aCollector.GetCount(); ++i )
{
BOARD_ITEM* item = aCollector[i];
double area = 0.0;
if( item->Type() == PCB_ZONE_T
&& static_cast<ZONE*>( item )->HitTestForEdge( where, maxSlop / 2 ) )
{
// Zone borders are very specific, so make them "small"
area = (double) SEG::Square( singlePixel ) * MAX_SLOP;
}
else if( item->Type() == PCB_VIA_T )
{
// Vias rarely hide other things, and we don't want them deferring to short track
// segments underneath them -- so artificially reduce their size from πr² to r².
area = (double) SEG::Square( static_cast<PCB_VIA*>( item )->GetDrill() / 2 );
}
else if( item->Type() == PCB_REFERENCE_IMAGE_T )
{
BOX2I box = item->GetBoundingBox();
area = (double) box.GetWidth() * box.GetHeight();
}
else
{
try
{
area = FOOTPRINT::GetCoverageArea( item, aCollector );
}
catch( const std::exception& e )
{
wxFAIL_MSG( wxString::Format( wxT( "Clipper exception occurred: %s" ), e.what() ) );
}
}
itemsByArea.emplace_back( item, area );
}
std::sort( itemsByArea.begin(), itemsByArea.end(),
[]( const std::pair<BOARD_ITEM*, double>& lhs,
const std::pair<BOARD_ITEM*, double>& rhs ) -> bool
{
return lhs.second < rhs.second;
} );
bool rejecting = false;
for( int i = 1; i < (int) itemsByArea.size(); ++i )
{
if( itemsByArea[i].second > itemsByArea[i-1].second * sizeRatio )
rejecting = true;
if( rejecting )
rejected.insert( itemsByArea[i].first );
}
// Special case: if a footprint is completely covered with other features then there's no
// way to select it -- so we need to leave it in the list for user disambiguation.
constexpr double maxCoverRatio = 0.70;
for( int i = 0; i < aCollector.GetCount(); ++i )
{
if( FOOTPRINT* footprint = dynamic_cast<FOOTPRINT*>( aCollector[i] ) )
{
if( footprint->CoverageRatio( aCollector ) > maxCoverRatio )
rejected.erase( footprint );
}
}
// Hopefully we've now got what the user wanted.
if( (unsigned) aCollector.GetCount() > rejected.size() ) // do not remove everything
{
for( BOARD_ITEM* item : rejected )
aCollector.Transfer( item );
}
// Finally, what we are left with is a set of items of similar coverage area. We now reject
// any that are not on the active layer, to reduce the number of disambiguation menus shown.
// If the user wants to force-disambiguate, they can either switch layers or use the modifier
// key to force the menu.
if( aCollector.GetCount() > 1 )
{
bool haveItemOnActive = false;
rejected.clear();
for( int i = 0; i < aCollector.GetCount(); ++i )
{
if( !aCollector[i]->IsOnLayer( activeLayer ) )
rejected.insert( aCollector[i] );
else
haveItemOnActive = true;
}
if( haveItemOnActive )
{
for( BOARD_ITEM* item : rejected )
aCollector.Transfer( item );
}
}
}
void PCB_SELECTION_TOOL::FilterCollectorForHierarchy( GENERAL_COLLECTOR& aCollector, bool aMultiselect ) const
{
std::unordered_set<EDA_ITEM*> toAdd;
// Set CANDIDATE on all parents which are included in the GENERAL_COLLECTOR. This
// algorithm is O(3n), whereas checking for the parent inclusion could potentially be O(n^2).
for( int j = 0; j < aCollector.GetCount(); j++ )
{
if( aCollector[j]->GetParent() )
aCollector[j]->GetParent()->ClearFlags( CANDIDATE );
if( aCollector[j]->GetParentFootprint() )
aCollector[j]->GetParentFootprint()->ClearFlags( CANDIDATE );
}
if( aMultiselect )
{
for( int j = 0; j < aCollector.GetCount(); j++ )
aCollector[j]->SetFlags( CANDIDATE );
}
for( int j = 0; j < aCollector.GetCount(); )
{
BOARD_ITEM* item = aCollector[j];
FOOTPRINT* fp = item->GetParentFootprint();
BOARD_ITEM* start = item;
if( !m_isFootprintEditor && fp )
start = fp;
// If a group is entered, disallow selections of objects outside the group.
if( m_enteredGroup && !PCB_GROUP::WithinScope( item, m_enteredGroup, m_isFootprintEditor ) )
{
aCollector.Remove( item );
continue;
}
// If any element is a member of a group, replace those elements with the top containing
// group.
if( EDA_GROUP* top = PCB_GROUP::TopLevelGroup( start, m_enteredGroup, m_isFootprintEditor ) )
{
if( top->AsEdaItem() != item )
{
toAdd.insert( top->AsEdaItem() );
top->AsEdaItem()->SetFlags( CANDIDATE );
aCollector.Remove( item );
continue;
}
}
// Footprints are a bit easier as they can't be nested.
if( fp && ( fp->GetFlags() & CANDIDATE ) )
{
// Remove children of selected items
aCollector.Remove( item );
continue;
}
++j;
}
for( EDA_ITEM* item : toAdd )
{
if( !aCollector.HasItem( item ) )
aCollector.Append( item );
}
}
void PCB_SELECTION_TOOL::FilterCollectorForTableCells( GENERAL_COLLECTOR& aCollector ) const
{
std::set<BOARD_ITEM*> to_add;
// Iterate from the back so we don't have to worry about removals.
for( int i = (int) aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
if( item->Type() == PCB_TABLECELL_T )
{
if( !aCollector.HasItem( item->GetParent() ) )
to_add.insert( item->GetParent() );
aCollector.Remove( item );
}
}
for( BOARD_ITEM* item : to_add )
aCollector.Append( item );
}
void PCB_SELECTION_TOOL::FilterCollectorForFreePads( GENERAL_COLLECTOR& aCollector,
bool aForcePromotion ) const
{
std::set<BOARD_ITEM*> to_add;
// Iterate from the back so we don't have to worry about removals.
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
if( !m_isFootprintEditor && item->Type() == PCB_PAD_T
&& ( !frame()->GetPcbNewSettings()->m_AllowFreePads || aForcePromotion ) )
{
if( !aCollector.HasItem( item->GetParent() ) )
to_add.insert( item->GetParent() );
aCollector.Remove( item );
}
}
for( BOARD_ITEM* item : to_add )
aCollector.Append( item );
}
void PCB_SELECTION_TOOL::FilterCollectorForMarkers( GENERAL_COLLECTOR& aCollector ) const
{
// Iterate from the back so we don't have to worry about removals.
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
if( item->Type() == PCB_MARKER_T )
aCollector.Remove( item );
}
}
void PCB_SELECTION_TOOL::FilterCollectorForFootprints( GENERAL_COLLECTOR& aCollector,
const VECTOR2I& aWhere ) const
{
const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings();
BOX2D viewport = getView()->GetViewport();
BOX2I extents = BOX2ISafe( viewport );
bool need_direct_hit = false;
FOOTPRINT* single_fp = nullptr;
// If the designer is not modifying the existing selection AND we already have
// a selection, then we only want to select items that are directly under the cursor.
// This prevents us from being unable to clear the selection when zoomed into a footprint
if( !m_additive && !m_subtractive && !m_exclusive_or && m_selection.GetSize() > 0 )
{
need_direct_hit = true;
for( EDA_ITEM* item : m_selection )
{
FOOTPRINT* fp = nullptr;
if( item->Type() == PCB_FOOTPRINT_T )
fp = static_cast<FOOTPRINT*>( item );
else if( item->IsBOARD_ITEM() )
fp = static_cast<BOARD_ITEM*>( item )->GetParentFootprint();
// If the selection contains items that are not footprints, then don't restrict
// whether we deselect the item or not.
if( !fp )
{
single_fp = nullptr;
break;
}
else if( !single_fp )
{
single_fp = fp;
}
// If the selection contains items from multiple footprints, then don't restrict
// whether we deselect the item or not.
else if( single_fp != fp )
{
single_fp = nullptr;
break;
}
}
}
auto visibleLayers =
[&]() -> LSET
{
if( m_isFootprintEditor )
{
LSET set;
for( PCB_LAYER_ID layer : LSET::AllLayersMask() )
set.set( layer, view()->IsLayerVisible( layer ) );
return set;
}
else
{
return board()->GetVisibleLayers();
}
};
LSET layers = visibleLayers();
if( settings->GetHighContrast() )
{
layers.reset();
const std::set<int> activeLayers = settings->GetHighContrastLayers();
for( int layer : activeLayers )
{
if( layer >= 0 && layer < PCB_LAYER_ID_COUNT )
layers.set( layer );
}
}
// Iterate from the back so we don't have to worry about removals.
for( int i = aCollector.GetCount() - 1; i >= 0; --i )
{
BOARD_ITEM* item = aCollector[i];
FOOTPRINT* fp = dyn_cast<FOOTPRINT*>( item );
if( !fp )
continue;
// Make footprints not difficult to select in high-contrast modes.
if( layers[fp->GetLayer()] )
continue;
BOX2I bbox = fp->GetLayerBoundingBox( layers );
// If the point clicked is not inside the visible bounding box, we can also remove it.
if( !bbox.Contains( aWhere) )
aCollector.Remove( item );
bool has_hit = false;
for( PCB_LAYER_ID layer : layers )
{
if( fp->HitTestOnLayer( extents, false, layer ) )
{
has_hit = true;
break;
}
}
// If the point is outside of the visible bounding box, we can remove it.
if( !has_hit )
{
aCollector.Remove( item );
}
// Do not require a direct hit on this fp if the existing selection only contains
// this fp's items. This allows you to have a selection of pads from a single
// footprint and still click in the center of the footprint to select it.
else if( single_fp )
{
if( fp == single_fp )
continue;
}
else if( need_direct_hit )
{
has_hit = false;
for( PCB_LAYER_ID layer : layers )
{
if( fp->HitTestOnLayer( aWhere, layer ) )
{
has_hit = true;
break;
}
}
if( !has_hit )
aCollector.Remove( item );
}
}
}
int PCB_SELECTION_TOOL::updateSelection( const TOOL_EVENT& aEvent )
{
getView()->Update( &m_selection );
getView()->Update( &m_enteredGroupOverlay );
return 0;
}
int PCB_SELECTION_TOOL::SelectColumns( const TOOL_EVENT& aEvent )
{
std::set<std::pair<PCB_TABLE*, int>> columns;
bool added = false;
for( EDA_ITEM* item : m_selection )
{
if( PCB_TABLECELL* cell = dynamic_cast<PCB_TABLECELL*>( item ) )
{
PCB_TABLE* table = static_cast<PCB_TABLE*>( cell->GetParent() );
columns.insert( std::make_pair( table, cell->GetColumn() ) );
}
}
for( auto& [ table, col ] : columns )
{
for( int row = 0; row < table->GetRowCount(); ++row )
{
PCB_TABLECELL* cell = table->GetCell( row, col );
if( !cell->IsSelected() )
{
select( table->GetCell( row, col ) );
added = true;
}
}
}
if( added )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
int PCB_SELECTION_TOOL::SelectRows( const TOOL_EVENT& aEvent )
{
std::set<std::pair<PCB_TABLE*, int>> rows;
bool added = false;
for( EDA_ITEM* item : m_selection )
{
if( PCB_TABLECELL* cell = dynamic_cast<PCB_TABLECELL*>( item ) )
{
PCB_TABLE* table = static_cast<PCB_TABLE*>( cell->GetParent() );
rows.insert( std::make_pair( table, cell->GetRow() ) );
}
}
for( auto& [ table, row ] : rows )
{
for( int col = 0; col < table->GetColCount(); ++col )
{
PCB_TABLECELL* cell = table->GetCell( row, col );
if( !cell->IsSelected() )
{
select( table->GetCell( row, col ) );
added = true;
}
}
}
if( added )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
int PCB_SELECTION_TOOL::SelectTable( const TOOL_EVENT& aEvent )
{
std::set<PCB_TABLE*> tables;
bool added = false;
for( EDA_ITEM* item : m_selection )
{
if( PCB_TABLECELL* cell = dynamic_cast<PCB_TABLECELL*>( item ) )
tables.insert( static_cast<PCB_TABLE*>( cell->GetParent() ) );
}
ClearSelection();
for( PCB_TABLE* table : tables )
{
if( !table->IsSelected() )
{
select( table );
added = true;
}
}
if( added )
m_toolMgr->ProcessEvent( EVENTS::SelectedEvent );
return 0;
}
void PCB_SELECTION_TOOL::setTransitions()
{
Go( &PCB_SELECTION_TOOL::UpdateMenu, ACTIONS::updateMenu.MakeEvent() );
Go( &PCB_SELECTION_TOOL::Main, ACTIONS::selectionActivate.MakeEvent() );
Go( &PCB_SELECTION_TOOL::CursorSelection, ACTIONS::selectionCursor.MakeEvent() );
Go( &PCB_SELECTION_TOOL::ClearSelection, ACTIONS::selectionClear.MakeEvent() );
Go( &PCB_SELECTION_TOOL::AddItemToSel, ACTIONS::selectItem.MakeEvent() );
Go( &PCB_SELECTION_TOOL::AddItemsToSel, ACTIONS::selectItems.MakeEvent() );
Go( &PCB_SELECTION_TOOL::RemoveItemFromSel, ACTIONS::unselectItem.MakeEvent() );
Go( &PCB_SELECTION_TOOL::RemoveItemsFromSel, ACTIONS::unselectItems.MakeEvent() );
Go( &PCB_SELECTION_TOOL::ReselectItem, ACTIONS::reselectItem.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SelectionMenu, ACTIONS::selectionMenu.MakeEvent() );
Go( &PCB_SELECTION_TOOL::filterSelection, PCB_ACTIONS::filterSelection.MakeEvent() );
Go( &PCB_SELECTION_TOOL::expandConnection, PCB_ACTIONS::selectConnection.MakeEvent() );
Go( &PCB_SELECTION_TOOL::unrouteSelected, PCB_ACTIONS::unrouteSelected.MakeEvent() );
Go( &PCB_SELECTION_TOOL::unrouteSegment, PCB_ACTIONS::unrouteSegment.MakeEvent() );
Go( &PCB_SELECTION_TOOL::selectNet, PCB_ACTIONS::selectNet.MakeEvent() );
Go( &PCB_SELECTION_TOOL::selectNet, PCB_ACTIONS::deselectNet.MakeEvent() );
Go( &PCB_SELECTION_TOOL::selectUnconnected, PCB_ACTIONS::selectUnconnected.MakeEvent() );
Go( &PCB_SELECTION_TOOL::grabUnconnected, PCB_ACTIONS::grabUnconnected.MakeEvent() );
Go( &PCB_SELECTION_TOOL::syncSelection, PCB_ACTIONS::syncSelection.MakeEvent() );
Go( &PCB_SELECTION_TOOL::syncSelectionWithNets, PCB_ACTIONS::syncSelectionWithNets.MakeEvent() );
Go( &PCB_SELECTION_TOOL::selectSameSheet, PCB_ACTIONS::selectSameSheet.MakeEvent() );
Go( &PCB_SELECTION_TOOL::selectSheetContents, PCB_ACTIONS::selectOnSheetFromEeschema.MakeEvent() );
Go( &PCB_SELECTION_TOOL::updateSelection, EVENTS::SelectedItemsModified );
Go( &PCB_SELECTION_TOOL::updateSelection, EVENTS::SelectedItemsMoved );
Go( &PCB_SELECTION_TOOL::SelectColumns, ACTIONS::selectColumns.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SelectRows, ACTIONS::selectRows.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SelectTable, ACTIONS::selectTable.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SetSelectPoly, ACTIONS::selectSetLasso.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SetSelectRect, ACTIONS::selectSetRect.MakeEvent() );
Go( &PCB_SELECTION_TOOL::SelectAll, ACTIONS::selectAll.MakeEvent() );
Go( &PCB_SELECTION_TOOL::UnselectAll, ACTIONS::unselectAll.MakeEvent() );
Go( &PCB_SELECTION_TOOL::disambiguateCursor, EVENTS::DisambiguatePoint );
}