Revise method of selecting footprints in contrast

When working in high contrast mode, we want to be able to select a
footprint with only elements in, e.g. silk and fab layers.

The previous design for footprint IsOnLayer had one behavior of every
other element and a different behavior for footprints.  This leads to
multiple bugs as new features use the overloaded IsOnLayer expecting it
to report if the element exists on a layer or not.

For footprints, we need a different routine to determine whether or not
to select the footprint when clicking on it.  IsOnLayer will report if
the footprint has any elements on a specific layer but we don't want to
use the bbox for a hittest because large footprints with through hole
pads will exist on every layer and have an enormous bbox.  Instead, we
filter footprints based on the hittest of each element.  This behaves in
a more logical fashion, allowing you to select a footprint by clicking
on a visible element of that footprint.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/15284

(cherry picked from commit f3d65b5156)
This commit is contained in:
Seth Hillbrand 2023-08-01 10:37:19 -07:00
parent 093db600ff
commit bd57957df7
5 changed files with 165 additions and 13 deletions

View File

@ -1103,23 +1103,97 @@ void FOOTPRINT::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector<MSG_PANEL_I
bool FOOTPRINT::IsOnLayer( PCB_LAYER_ID aLayer ) const
{
// If we have any pads, fall back on normal checking
if( !m_pads.empty() )
return m_layer == aLayer;
for( PAD* pad : m_pads )
{
if( pad->IsOnLayer( aLayer ) )
return true;
}
// No pads? Check if this entire footprint exists on the given layer
for( FP_ZONE* zone : m_fp_zones )
{
if( !zone->IsOnLayer( aLayer ) )
return false;
if( zone->IsOnLayer( aLayer ) )
return true;
}
if( m_reference->IsOnLayer( aLayer ) || m_value->IsOnLayer( aLayer ) )
return true;
for( BOARD_ITEM* item : m_drawings )
{
if( item->IsOnLayer( aLayer ) )
return true;
}
return false;
}
bool FOOTPRINT::HitTestOnLayer( const VECTOR2I& aPosition, PCB_LAYER_ID aLayer, int aAccuracy ) const
{
for( PAD* pad : m_pads )
{
if( pad->IsOnLayer( aLayer ) && pad->HitTest( aPosition, aAccuracy ) )
return true;
}
for( ZONE* zone : m_fp_zones )
{
if( zone->IsOnLayer( aLayer ) && zone->HitTest( aPosition, aAccuracy ) )
return true;
}
for( BOARD_ITEM* item : m_drawings )
{
if( !item->IsOnLayer( aLayer ) )
if( item->Type() != PCB_TEXT_T && item->IsOnLayer( aLayer )
&& item->HitTest( aPosition, aAccuracy ) )
{
return true;
}
}
return false;
}
bool FOOTPRINT::HitTestOnLayer( const BOX2I& aRect, bool aContained, PCB_LAYER_ID aLayer, int aAccuracy ) const
{
std::vector<BOARD_ITEM*> items;
for( PAD* pad : m_pads )
{
if( pad->IsOnLayer( aLayer ) )
items.push_back( pad );
}
for( FP_ZONE* zone : m_fp_zones )
{
if( zone->IsOnLayer( aLayer ) )
items.push_back( zone );
}
for( BOARD_ITEM* item : m_drawings )
{
if( item->Type() != PCB_TEXT_T && item->IsOnLayer( aLayer ) )
items.push_back( item );
}
// If we require the elements to be contained in the rect and any of them are not,
// we can return false;
// Conversely, if we just require any of the elements to have a hit, we can return true
// when the first one is found.
for( BOARD_ITEM* item : items )
{
if( !aContained && item->HitTest( aRect, aContained, aAccuracy ) )
return true;
else if( aContained && !item->HitTest( aRect, aContained, aAccuracy ) )
return false;
}
return true;
// If we didn't exit in the loop, that means that we did not return false for aContained or
// we did not return true for !aContained. So we can just return the bool with a test of
// whether there were any elements or not.
return !items.empty() && aContained;
}

View File

@ -329,9 +329,7 @@ public:
bool IsFlipped() const { return GetLayer() == B_Cu; }
/**
* A special IsOnLayer for footprints: return true if the footprint contains only items on the
* given layer, even if that layer is not one of the valid footprint layers F_Cu and B_Cu.
* This allows selection of "graphic" footprints that contain only silkscreen, for example.
* @copydoc BOARD_ITEM::IsOnLayer
*/
bool IsOnLayer( PCB_LAYER_ID aLayer ) const override;
@ -518,6 +516,18 @@ public:
bool HitTest( const BOX2I& aRect, bool aContained, int aAccuracy = 0 ) const override;
/**
* Test if the point hits one or more of the footprint elements on a given layer.
*
* @param aPosition is the point to test
* @param aAccuracy is the hit test accuracy
* @param aLayer is the layer to test
* @return true if aPosition hits a footprint element on aLayer
*/
bool HitTestOnLayer( const VECTOR2I& aPosition, PCB_LAYER_ID aLayer, int aAccuracy = 0 ) const;
bool HitTestOnLayer( const BOX2I& aRect, bool aContained, PCB_LAYER_ID aLayer, int aAccuracy = 0 ) const;
/**
* @return reference designator text.
*/

View File

@ -117,7 +117,7 @@ static void existsOnLayerFunc( LIBEVAL::CONTEXT* aCtx, void *self )
{
anyMatch = true;
if( item->IsOnLayer( ToLAYER_ID( entry.GetValue())))
if( item->IsOnLayer( ToLAYER_ID( entry.GetValue() ) ) )
return 1.0;
}
}

View File

@ -744,6 +744,8 @@ bool PCB_SELECTION_TOOL::selectPoint( const VECTOR2I& aWhere, bool aOnDrag,
// Apply the stateful filter
FilterCollectedItems( collector, false );
FilterCollectorForFootprints( collector, aWhere );
// For subtracting, we only want items that are selected
if( m_subtractive )
{
@ -965,6 +967,7 @@ bool PCB_SELECTION_TOOL::selectMultiple()
{
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( candidate.first );
if( item && Selectable( item ) && item->HitTest( selectionRect, !greedySelection )
&& ( greedySelection || !group_items.count( item ) ) )
{
@ -2938,10 +2941,9 @@ void PCB_SELECTION_TOOL::GuessSelectionCandidates( GENERAL_COLLECTOR& aCollector
for( int i = 0; i < aCollector.GetCount(); ++i )
{
BOARD_ITEM* item = aCollector[i];
KICAD_T type = item->Type();
if( ( type == PCB_TEXT_T || type == PCB_TEXTBOX_T || type == PCB_SHAPE_T )
&& silkLayers[item->GetLayer()] )
if( item->IsType( { PCB_TEXT_T, PCB_TEXTBOX_T, PCB_SHAPE_T, PCB_FOOTPRINT_T } )
&& item->IsOnLayer( activeLayer ) )
{
preferred.insert( item );
}
@ -3198,6 +3200,67 @@ void PCB_SELECTION_TOOL::FilterCollectorForMarkers( GENERAL_COLLECTOR& aCollecto
}
}
void PCB_SELECTION_TOOL::FilterCollectorForFootprints( GENERAL_COLLECTOR& aCollector, const VECTOR2I& aWhere ) const
{
const RENDER_SETTINGS* settings = getView()->GetPainter()->GetSettings();
auto visibleLayers =
[&]()
{
if( m_isFootprintEditor )
{
LSET set;
for( PCB_LAYER_ID layer : LSET::AllLayersMask().Seq() )
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 )
{
bool has_hit = false;
BOARD_ITEM* item = aCollector[i];
FOOTPRINT* fp = dyn_cast<FOOTPRINT*>( item );
if( !fp )
continue;
for( PCB_LAYER_ID layer : layers.Seq() )
{
if( fp->HitTestOnLayer( aWhere, layer ) )
{
has_hit = true;
break;
}
}
if( !has_hit )
aCollector.Remove( item );
}
}
int PCB_SELECTION_TOOL::updateSelection( const TOOL_EVENT& aEvent )
{

View File

@ -212,6 +212,11 @@ public:
*/
void FilterCollectedItems( GENERAL_COLLECTOR& aCollector, bool aMultiSelect );
/**
* Drop footprints that are not directly selected
*/
void FilterCollectorForFootprints( GENERAL_COLLECTOR& aCollector, const VECTOR2I& aWhere ) const;
protected:
KIGFX::PCB_VIEW* view() const
{