kicad-source/pcbnew/tools/pcb_grid_helper.cpp
John Beard 3162747d25 Pcbnew: include pad snaps for visible layers when not in high-constrast mode
This the 'activeLayers' is a more restricted set of layers than
all visible layers. When high constrast mode is off, snaps should
be available for all visible layers, not just layers in the
'activeLayers' set.

This is similar to the check done in checkVisibility, but as
pads are multi-layer items, they have this check afterwards as
well.

Note - the All Layer/Active Layer Only filtering is handled elsewhere.
2024-11-10 22:25:53 +08:00

1657 lines
56 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2014 CERN
* Copyright (C) 2018-2023 KiCad Developers, see AUTHORS.txt for contributors.
* @author Tomasz Wlostowski <tomasz.wlostowski@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 "pcb_grid_helper.h"
#include <functional>
#include <advanced_config.h>
#include <pcb_dimension.h>
#include <pcb_shape.h>
#include <footprint.h>
#include <pad.h>
#include <pcb_group.h>
#include <pcb_track.h>
#include <zone.h>
#include <gal/graphics_abstraction_layer.h>
#include <geometry/intersection.h>
#include <geometry/nearest.h>
#include <geometry/oval.h>
#include <geometry/shape_circle.h>
#include <geometry/shape_line_chain.h>
#include <geometry/shape_rect.h>
#include <geometry/shape_segment.h>
#include <geometry/shape_simple.h>
#include <geometry/shape_utils.h>
#include <macros.h>
#include <math/util.h> // for KiROUND
#include <gal/painter.h>
#include <pcbnew_settings.h>
#include <tool/tool_manager.h>
#include <tools/pcb_tool_base.h>
#include <view/view.h>
namespace
{
/**
* Get the INTERSECTABLE_GEOM for a BOARD_ITEM if it's supported.
*
* This is the idealised geometry, e.g. a zero-width line or circle.
*/
std::optional<INTERSECTABLE_GEOM> GetBoardIntersectable( const BOARD_ITEM& aItem )
{
switch( aItem.Type() )
{
case PCB_SHAPE_T:
{
const PCB_SHAPE& shape = static_cast<const PCB_SHAPE&>( aItem );
switch( shape.GetShape() )
{
case SHAPE_T::SEGMENT: return SEG{ shape.GetStart(), shape.GetEnd() };
case SHAPE_T::CIRCLE: return CIRCLE{ shape.GetCenter(), shape.GetRadius() };
case SHAPE_T::ARC:
return SHAPE_ARC{ shape.GetStart(), shape.GetArcMid(), shape.GetEnd(), 0 };
case SHAPE_T::RECTANGLE: return BOX2I::ByCorners( shape.GetStart(), shape.GetEnd() );
default: break;
}
break;
}
case PCB_TRACE_T:
{
const PCB_TRACK& track = static_cast<const PCB_TRACK&>( aItem );
return SEG{ track.GetStart(), track.GetEnd() };
}
case PCB_ARC_T:
{
const PCB_ARC& arc = static_cast<const PCB_ARC&>( aItem );
return SHAPE_ARC{ arc.GetStart(), arc.GetMid(), arc.GetEnd(), 0 };
}
default: break;
}
return std::nullopt;
}
/**
* Find the closest point on a BOARD_ITEM to a given point.
*
* Only works for items that have a NEARABLE_GEOM defined, it's
* not a general purpose function.
*
* @return The closest point on the item to aPos, or std::nullopt if the item
* doesn't have a NEARABLE_GEOM defined.
*/
std::optional<int64_t> FindSquareDistanceToItem( const BOARD_ITEM& item, const VECTOR2I& aPos )
{
std::optional<INTERSECTABLE_GEOM> intersectable = GetBoardIntersectable( item );
std::optional<NEARABLE_GEOM> nearable;
if( intersectable )
{
// Exploit the intersectable as a nearable
std::visit(
[&]( auto& geom )
{
nearable = NEARABLE_GEOM( std::move( geom ) );
},
*intersectable );
}
// Whatever the item is, we don't have a nearable for it
if( !nearable )
return std::nullopt;
const VECTOR2I nearestPt = GetNearestPoint( *nearable, aPos );
return nearestPt.SquaredDistance( aPos );
}
} // namespace
PCB_GRID_HELPER::PCB_GRID_HELPER( TOOL_MANAGER* aToolMgr, MAGNETIC_SETTINGS* aMagneticSettings ) :
GRID_HELPER( aToolMgr, LAYER_ANCHOR ), m_magneticSettings( aMagneticSettings )
{
KIGFX::VIEW* view = m_toolMgr->GetView();
KIGFX::RENDER_SETTINGS* settings = view->GetPainter()->GetSettings();
KIGFX::COLOR4D auxItemsColor = settings->GetLayerColor( LAYER_AUX_ITEMS );
m_viewAxis.SetSize( 20000 );
m_viewAxis.SetStyle( KIGFX::ORIGIN_VIEWITEM::CROSS );
m_viewAxis.SetColor( auxItemsColor.WithAlpha( 0.4 ) );
m_viewAxis.SetDrawAtZero( true );
view->Add( &m_viewAxis );
view->SetVisible( &m_viewAxis, false );
m_viewSnapPoint.SetSize( 10 );
m_viewSnapPoint.SetStyle( KIGFX::ORIGIN_VIEWITEM::CIRCLE_CROSS );
m_viewSnapPoint.SetColor( auxItemsColor );
m_viewSnapPoint.SetDrawAtZero( true );
view->Add( &m_viewSnapPoint );
view->SetVisible( &m_viewSnapPoint, false );
}
PCB_GRID_HELPER::~PCB_GRID_HELPER()
{
KIGFX::VIEW* view = m_toolMgr->GetView();
view->Remove( &m_viewAxis );
view->Remove( &m_viewSnapPoint );
}
void PCB_GRID_HELPER::AddConstructionItems( std::vector<BOARD_ITEM*> aItems, bool aExtensionOnly,
bool aIsPersistent )
{
if( !ADVANCED_CFG::GetCfg().m_EnableExtensionSnaps )
{
return;
}
// For all the elements that get drawn construction geometry,
// add something suitable to the construction helper.
// This can be nothing.
auto constructionItemsBatch = std::make_unique<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>();
std::vector<VECTOR2I> referenceOnlyPoints;
for( BOARD_ITEM* item : aItems )
{
std::vector<KIGFX::CONSTRUCTION_GEOM::DRAWABLE> constructionDrawables;
if( item->Type() == PCB_SHAPE_T )
{
PCB_SHAPE& shape = static_cast<PCB_SHAPE&>( *item );
switch( shape.GetShape() )
{
case SHAPE_T::SEGMENT:
{
if( !aExtensionOnly )
{
constructionDrawables.emplace_back( LINE{ shape.GetStart(), shape.GetEnd() } );
}
else
{
// Two rays, extending from the segment ends
const VECTOR2I segVec = shape.GetEnd() - shape.GetStart();
constructionDrawables.emplace_back(
HALF_LINE{ shape.GetStart(), shape.GetStart() - segVec } );
constructionDrawables.emplace_back(
HALF_LINE{ shape.GetEnd(), shape.GetEnd() + segVec } );
}
if( aIsPersistent )
{
// include the original endpoints as construction items
// (this allows H/V snapping)
constructionDrawables.emplace_back( shape.GetStart() );
constructionDrawables.emplace_back( shape.GetEnd() );
// But mark them as references, so they don't get snapped to themsevles
referenceOnlyPoints.emplace_back( shape.GetStart() );
referenceOnlyPoints.emplace_back( shape.GetEnd() );
}
break;
}
case SHAPE_T::ARC:
{
if( !aExtensionOnly )
{
constructionDrawables.push_back(
CIRCLE{ shape.GetCenter(), shape.GetRadius() } );
}
else
{
// The rest of the circle is the arc through the opposite point to the midpoint
const VECTOR2I oppositeMid =
shape.GetCenter() + ( shape.GetCenter() - shape.GetArcMid() );
constructionDrawables.push_back(
SHAPE_ARC{ shape.GetStart(), oppositeMid, shape.GetEnd(), 0 } );
}
constructionDrawables.push_back( shape.GetCenter() );
if( aIsPersistent )
{
// include the original endpoints as construction items
// (this allows H/V snapping)
constructionDrawables.emplace_back( shape.GetStart() );
constructionDrawables.emplace_back( shape.GetEnd() );
// But mark them as references, so they don't get snapped to themselves
referenceOnlyPoints.emplace_back( shape.GetStart() );
referenceOnlyPoints.emplace_back( shape.GetEnd() );
}
break;
}
case SHAPE_T::CIRCLE:
case SHAPE_T::RECTANGLE:
{
constructionDrawables.push_back( shape.GetCenter() );
break;
}
default:
// This shape doesn't have any construction geometry to draw
break;
}
}
// At this point, constructionDrawables can be empty, which is fine
// (it means there's no additional construction geometry to draw, but
// the item is still going to be proposed for activation)
constructionItemsBatch->emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
CONSTRUCTION_MANAGER::SOURCE::FROM_ITEMS,
item,
std::move( constructionDrawables ),
} );
}
if( referenceOnlyPoints.size() )
{
getSnapManager().SetReferenceOnlyPoints( std::move( referenceOnlyPoints ) );
}
// Let the manager handle it
getSnapManager().GetConstructionManager().ProposeConstructionItems(
std::move( constructionItemsBatch ), aIsPersistent );
}
VECTOR2I PCB_GRID_HELPER::AlignToSegment( const VECTOR2I& aPoint, const SEG& aSeg )
{
const int c_gridSnapEpsilon_sq = 4;
VECTOR2I aligned = Align( aPoint );
if( !m_enableSnap )
return aligned;
std::vector<VECTOR2I> points;
const SEG testSegments[] = { SEG( aligned, aligned + VECTOR2( 1, 0 ) ),
SEG( aligned, aligned + VECTOR2( 0, 1 ) ),
SEG( aligned, aligned + VECTOR2( 1, 1 ) ),
SEG( aligned, aligned + VECTOR2( 1, -1 ) ) };
for( const SEG& seg : testSegments )
{
OPT_VECTOR2I vec = aSeg.IntersectLines( seg );
if( vec && aSeg.SquaredDistance( *vec ) <= c_gridSnapEpsilon_sq )
points.push_back( *vec );
}
VECTOR2I nearest = aligned;
SEG::ecoord min_d_sq = VECTOR2I::ECOORD_MAX;
// Snap by distance between pointer and endpoints
for( const VECTOR2I& pt : { aSeg.A, aSeg.B } )
{
SEG::ecoord d_sq = ( pt - aPoint ).SquaredEuclideanNorm();
if( d_sq < min_d_sq )
{
min_d_sq = d_sq;
nearest = pt;
}
}
// Snap by distance between aligned cursor and intersections
for( const VECTOR2I& pt : points )
{
SEG::ecoord d_sq = ( pt - aligned ).SquaredEuclideanNorm();
if( d_sq < min_d_sq )
{
min_d_sq = d_sq;
nearest = pt;
}
}
return nearest;
}
VECTOR2I PCB_GRID_HELPER::AlignToArc( const VECTOR2I& aPoint, const SHAPE_ARC& aArc )
{
VECTOR2I aligned = Align( aPoint );
if( !m_enableSnap )
return aligned;
std::vector<VECTOR2I> points;
aArc.IntersectLine( SEG( aligned, aligned + VECTOR2( 1, 0 ) ), &points );
aArc.IntersectLine( SEG( aligned, aligned + VECTOR2( 0, 1 ) ), &points );
aArc.IntersectLine( SEG( aligned, aligned + VECTOR2( 1, 1 ) ), &points );
aArc.IntersectLine( SEG( aligned, aligned + VECTOR2( 1, -1 ) ), &points );
VECTOR2I nearest = aligned;
SEG::ecoord min_d_sq = VECTOR2I::ECOORD_MAX;
// Snap by distance between pointer and endpoints
for( const VECTOR2I& pt : { aArc.GetP0(), aArc.GetP1() } )
{
SEG::ecoord d_sq = ( pt - aPoint ).SquaredEuclideanNorm();
if( d_sq < min_d_sq )
{
min_d_sq = d_sq;
nearest = pt;
}
}
// Snap by distance between aligned cursor and intersections
for( const VECTOR2I& pt : points )
{
SEG::ecoord d_sq = ( pt - aligned ).SquaredEuclideanNorm();
if( d_sq < min_d_sq )
{
min_d_sq = d_sq;
nearest = pt;
}
}
return nearest;
}
VECTOR2I PCB_GRID_HELPER::AlignToNearestPad( const VECTOR2I& aMousePos, std::deque<PAD*>& aPads )
{
clearAnchors();
for( BOARD_ITEM* item : aPads )
computeAnchors( item, aMousePos, true, nullptr );
double minDist = std::numeric_limits<double>::max();
ANCHOR* nearestOrigin = nullptr;
for( ANCHOR& a : m_anchors )
{
if( ( ORIGIN & a.flags ) != ORIGIN )
continue;
bool hitAny = true;
for( EDA_ITEM* item : m_snapItem->items )
{
hitAny = hitAny && item->HitTest( aMousePos );
}
if( !hitAny )
continue;
double dist = a.Distance( aMousePos );
if( dist < minDist )
{
minDist = dist;
nearestOrigin = &a;
}
}
return nearestOrigin ? nearestOrigin->pos : aMousePos;
}
VECTOR2I PCB_GRID_HELPER::BestDragOrigin( const VECTOR2I &aMousePos,
std::vector<BOARD_ITEM*>& aItems,
GRID_HELPER_GRIDS aGrid,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter )
{
clearAnchors();
computeAnchors( aItems, aMousePos, true, aSelectionFilter, nullptr, true );
double lineSnapMinCornerDistance = m_toolMgr->GetView()->ToWorld( 50 );
ANCHOR* nearestOutline = nearestAnchor( aMousePos, OUTLINE );
ANCHOR* nearestCorner = nearestAnchor( aMousePos, CORNER );
ANCHOR* nearestOrigin = nearestAnchor( aMousePos, ORIGIN );
ANCHOR* best = nullptr;
double minDist = std::numeric_limits<double>::max();
if( nearestOrigin )
{
minDist = nearestOrigin->Distance( aMousePos );
best = nearestOrigin;
}
if( nearestCorner )
{
double dist = nearestCorner->Distance( aMousePos );
if( dist < minDist )
{
minDist = dist;
best = nearestCorner;
}
}
if( nearestOutline )
{
double dist = nearestOutline->Distance( aMousePos );
if( minDist > lineSnapMinCornerDistance && dist < minDist )
best = nearestOutline;
}
return best ? best->pos : aMousePos;
}
VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, BOARD_ITEM* aReferenceItem,
GRID_HELPER_GRIDS aGrid )
{
LSET layers;
std::vector<BOARD_ITEM*> item;
if( aReferenceItem )
{
layers = aReferenceItem->GetLayerSet();
item.push_back( aReferenceItem );
}
else
{
layers = LSET::AllLayersMask();
}
return BestSnapAnchor( aOrigin, layers, aGrid, item );
}
VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& aLayers,
GRID_HELPER_GRIDS aGrid,
const std::vector<BOARD_ITEM*>& aSkip )
{
// Tuning constant: snap radius in screen space
const int snapSize = 25;
// Snapping distance is in screen space, clamped to the current grid to ensure that the grid
// points that are visible can always be snapped to.
// see https://gitlab.com/kicad/code/kicad/-/issues/5638
// see https://gitlab.com/kicad/code/kicad/-/issues/7125
// see https://gitlab.com/kicad/code/kicad/-/issues/12303
double snapScale = m_toolMgr->GetView()->ToWorld( snapSize );
// warning: GetVisibleGrid().x sometimes returns a value > INT_MAX. Intermediate calculation
// needs double.
int snapRange = KiROUND( m_enableGrid ? std::min( snapScale, GetVisibleGrid().x ) : snapScale );
//Respect limits of coordinates representation
const BOX2I visibilityHorizon =
BOX2ISafe( VECTOR2D( aOrigin ) - snapRange / 2.0, VECTOR2D( snapRange, snapRange ) );
clearAnchors();
const std::vector<BOARD_ITEM*> visibleItems = queryVisible( visibilityHorizon, aSkip );
computeAnchors( visibleItems, aOrigin, false, nullptr, &aLayers, false );
ANCHOR* nearest = nearestAnchor( aOrigin, SNAPPABLE );
VECTOR2I nearestGrid = Align( aOrigin, aGrid );
if( KIGFX::ANCHOR_DEBUG* ad = enableAndGetAnchorDebug(); ad )
{
ad->ClearAnchors();
for( const ANCHOR& anchor : m_anchors )
ad->AddAnchor( anchor.pos );
ad->SetNearest( nearest ? OPT_VECTOR2I{ nearest->pos } : std::nullopt );
m_toolMgr->GetView()->Update( ad, KIGFX::GEOMETRY );
}
// The distance to the nearest snap point, if any
std::optional<int> snapDist;
if( nearest )
snapDist = nearest->Distance( aOrigin );
showConstructionGeometry( m_enableSnap );
SNAP_MANAGER& snapManager = getSnapManager();
SNAP_LINE_MANAGER& snapLineManager = snapManager.GetSnapLineManager();
const auto ptIsReferenceOnly = [&]( const VECTOR2I& aPt )
{
const std::vector<VECTOR2I>& referenceOnlyPoints = snapManager.GetReferenceOnlyPoints();
return std::find( referenceOnlyPoints.begin(), referenceOnlyPoints.end(), aPt )
!= referenceOnlyPoints.end();
};
const auto proposeConstructionForItems = [&]( const std::vector<EDA_ITEM*>& aItems )
{
// Add any involved item as a temporary construction item
// (de-duplication with existing construction items is handled later)
std::vector<BOARD_ITEM*> items;
for( EDA_ITEM* item : aItems )
{
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
// Null items are allowed to arrive here as they represent geometry that isn't
// specifically tied to a board item. For example snap lines from some
// other anchor.
// But they don't produce new construction items.
if( boardItem )
{
if( m_magneticSettings->allLayers
|| ( ( aLayers & boardItem->GetLayerSet() ).any() ) )
{
items.push_back( boardItem );
}
}
}
// Temporary construction items are not persistent and don't
// overlay the items themselves (as the items will not be moved)
AddConstructionItems( items, true, false );
};
bool snapValid = false;
if( m_enableSnap )
{
// Existing snap lines need priority over new snaps
if( m_enableSnapLine )
{
OPT_VECTOR2I snapLineSnap = snapLineManager.GetNearestSnapLinePoint(
aOrigin, nearestGrid, snapDist, snapRange );
// We found a better snap point that the nearest one
if( snapLineSnap && m_skipPoint != *snapLineSnap )
{
snapLineManager.SetSnapLineEnd( *snapLineSnap );
snapValid = true;
// Don't show a snap point if we're snapping to a grid rather than an anchor
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
m_viewSnapPoint.SetSnapTypes( POINT_TYPE::PT_NONE );
// Only return the snap line end as a snap if it's not a reference point
// (we don't snap to reference points, but we can use them to update the snap line,
// without actually snapping)
if( !ptIsReferenceOnly( *snapLineSnap ) )
{
return *snapLineSnap;
}
}
}
// If there's a snap anchor within range, use it if we can
if( nearest && nearest->Distance( aOrigin ) <= snapRange )
{
const bool anchorIsConstructed = nearest->flags & ANCHOR_FLAGS::CONSTRUCTED;
// If the nearest anchor is a reference point, we don't snap to it,
// but we can update the snap line origin
if( ptIsReferenceOnly( nearest->pos ) )
{
// We can set the snap line origin, but don't mess with the
// accepted snap point
snapLineManager.SetSnapLineOrigin( nearest->pos );
}
else
{
// 'Intrinsic' points of items can trigger adding construction geometry
// for _that_ item by proximity. E.g. just mousing over the intersection
// of an item doesn't add a construction item for the second item).
// This is to make construction items less intrusive and more
// a result of user intent.
if( !anchorIsConstructed )
{
proposeConstructionForItems( nearest->items );
}
const auto shouldAcceptAnchor = [&]( const ANCHOR& aAnchor )
{
// If no extension snaps are enabled, don't inhibit
static const bool haveExtensions =
ADVANCED_CFG::GetCfg().m_EnableExtensionSnaps;
if( !haveExtensions )
return true;
// Check that any involved real items are 'active'
// (i.e. the user has moused over a key point previously)
// If any are not real (e.g. snap lines), they are allowed to be involved
//
// This is an area most likely to be controversial/need tuning,
// as some users will think it's fiddly; without 'activation', others will
// think the snaps are intrusive.
bool allRealAreInvolved =
snapManager.GetConstructionManager().InvolvesAllGivenRealItems(
aAnchor.items );
return allRealAreInvolved;
};
if( shouldAcceptAnchor( *nearest ) )
{
m_snapItem = *nearest;
// Set the snap line origin or end as needed
snapLineManager.SetSnappedAnchor( m_snapItem->pos );
// Show the correct snap point marker
updateSnapPoint( { m_snapItem->pos, m_snapItem->pointTypes } );
return m_snapItem->pos;
}
}
snapValid = true;
}
else
{
static const bool canActivateByHitTest =
ADVANCED_CFG::GetCfg().m_ExtensionSnapActivateOnHover;
if( canActivateByHitTest )
{
// An exact hit on an item, even if not near a snap point
// If it's tool hard to hit by hover, this can be increased
// to make it non-exact.
const int hoverAccuracy = 0;
for( BOARD_ITEM* item : visibleItems )
{
if( item->HitTest( aOrigin, hoverAccuracy ) )
{
proposeConstructionForItems( { item } );
snapValid = true;
break;
}
}
}
}
// If we got here, we didn't snap to an anchor or snap line
// If we're snapping to a grid, on-element snaps would be too intrusive
// but they're useful when there isn't a grid to snap to
if( !m_enableGrid )
{
OPT_VECTOR2I nearestPointOnAnElement =
GetNearestPoint( m_pointOnLineCandidates, aOrigin );
// Got any nearest point - snap if in range
if( nearestPointOnAnElement
&& nearestPointOnAnElement->Distance( aOrigin ) <= snapRange )
{
updateSnapPoint( { *nearestPointOnAnElement, POINT_TYPE::PT_ON_ELEMENT } );
// Clear the snap end, but keep the origin so touching another line
// doesn't kill a snap line
snapLineManager.SetSnapLineEnd( std::nullopt );
return *nearestPointOnAnElement;
}
}
}
// Completely failed to find any snap point, so snap to the grid
m_snapItem = std::nullopt;
if( !snapValid )
{
snapLineManager.ClearSnapLine();
snapManager.GetConstructionManager().CancelProposal();
}
else
{
snapLineManager.SetSnapLineEnd( std::nullopt );
}
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
return nearestGrid;
}
BOARD_ITEM* PCB_GRID_HELPER::GetSnapped() const
{
if( !m_snapItem )
return nullptr;
// The snap anchor doesn't have an item associated with it
// (odd, could it be entirely made of construction geometry?)
if( m_snapItem->items.empty() )
return nullptr;
return static_cast<BOARD_ITEM*>( m_snapItem->items[0] );
}
GRID_HELPER_GRIDS PCB_GRID_HELPER::GetItemGrid( const EDA_ITEM* aItem ) const
{
if( !aItem )
return GRID_CURRENT;
switch( aItem->Type() )
{
case PCB_FOOTPRINT_T:
case PCB_PAD_T:
return GRID_CONNECTABLE;
case PCB_TEXT_T:
case PCB_FIELD_T:
return GRID_TEXT;
case PCB_SHAPE_T:
case PCB_DIMENSION_T:
case PCB_REFERENCE_IMAGE_T:
case PCB_TEXTBOX_T:
return GRID_GRAPHICS;
case PCB_TRACE_T:
case PCB_ARC_T:
return GRID_WIRES;
case PCB_VIA_T:
return GRID_VIAS;
default:
return GRID_CURRENT;
}
}
VECTOR2D PCB_GRID_HELPER::GetGridSize( GRID_HELPER_GRIDS aGrid ) const
{
const GRID_SETTINGS& grid = m_toolMgr->GetSettings()->m_Window.grid;
int idx = -1;
VECTOR2D g = m_toolMgr->GetView()->GetGAL()->GetGridSize();
if( !grid.overrides_enabled )
return g;
switch( aGrid )
{
case GRID_CONNECTABLE:
if( grid.override_connected )
idx = grid.override_connected_idx;
break;
case GRID_WIRES:
if( grid.override_wires )
idx = grid.override_wires_idx;
break;
case GRID_VIAS:
if( grid.override_vias )
idx = grid.override_vias_idx;
break;
case GRID_TEXT:
if( grid.override_text )
idx = grid.override_text_idx;
break;
case GRID_GRAPHICS:
if( grid.override_graphics )
idx = grid.override_graphics_idx;
break;
default:
break;
}
if( idx >= 0 && idx < (int) grid.grids.size() )
g = grid.grids[idx].ToDouble( pcbIUScale );
return g;
}
std::vector<BOARD_ITEM*>
PCB_GRID_HELPER::queryVisible( const BOX2I& aArea, const std::vector<BOARD_ITEM*>& aSkip ) const
{
std::set<BOARD_ITEM*> items;
std::vector<KIGFX::VIEW::LAYER_ITEM_PAIR> selectedItems;
PCB_TOOL_BASE* currentTool = static_cast<PCB_TOOL_BASE*>( m_toolMgr->GetCurrentTool() );
KIGFX::VIEW* view = m_toolMgr->GetView();
RENDER_SETTINGS* settings = view->GetPainter()->GetSettings();
const std::set<int>& activeLayers = settings->GetHighContrastLayers();
bool isHighContrast = settings->GetHighContrast();
view->Query( aArea, selectedItems );
for( const auto& [ viewItem, layer ] : selectedItems )
{
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( viewItem );
if( currentTool->IsFootprintEditor() )
{
// If we are in the footprint editor, don't use the footprint itself
if( boardItem->Type() == PCB_FOOTPRINT_T )
continue;
}
else
{
// If we are not in the footprint editor, don't use footprint-editor-private items
if( FOOTPRINT* parentFP = boardItem->GetParentFootprint() )
{
if( IsPcbLayer( layer ) && parentFP->GetPrivateLayers().test( layer ) )
continue;
}
}
// The boardItem must be visible and on an active layer
if( view->IsVisible( boardItem )
&& ( !isHighContrast || activeLayers.count( layer ) )
&& boardItem->ViewGetLOD( layer, view ) < view->GetScale() )
{
items.insert ( boardItem );
}
}
std::function<void( BOARD_ITEM* )> skipItem =
[&]( BOARD_ITEM* aItem )
{
items.erase( aItem );
aItem->RunOnDescendants(
[&]( BOARD_ITEM* aChild )
{
skipItem( aChild );
} );
};
for( BOARD_ITEM* item : aSkip )
skipItem( item );
return {items.begin(), items.end()};
}
struct PCB_INTERSECTABLE
{
BOARD_ITEM* Item;
INTERSECTABLE_GEOM Geometry;
// Clang wants this constructor
PCB_INTERSECTABLE( BOARD_ITEM* aItem, INTERSECTABLE_GEOM aSeg ) :
Item( aItem ), Geometry( std::move( aSeg ) )
{
}
};
void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter,
const LSET* aMatchLayers, bool aForDrag )
{
std::vector<PCB_INTERSECTABLE> intersectables;
// These could come from a more granular snap mode filter
// But when looking for drag points, we don't want construction geometry
const bool computeIntersections = !aForDrag;
const bool computePointsOnElements = !aForDrag;
const bool excludeGraphics = aSelectionFilter && !aSelectionFilter->graphics;
const bool excludeTracks = aSelectionFilter && !aSelectionFilter->tracks;
const auto itemIsSnappable = [&]( const BOARD_ITEM& aItem )
{
// If we are filtering by layers, check if the item matches
if( aMatchLayers )
{
return m_magneticSettings->allLayers
|| ( ( *aMatchLayers & aItem.GetLayerSet() ).any() );
}
return true;
};
const auto processItem = [&]( BOARD_ITEM& item )
{
// Don't even process the item if it doesn't match the layers
if( !itemIsSnappable( item ) )
{
return;
}
// First, add all the key points of the item itself
computeAnchors( &item, aRefPos, aFrom, aSelectionFilter );
// If we are computing intersections, construct the relevant intersectables
// Points on elements also use the intersectables.
if( computeIntersections || computePointsOnElements )
{
std::optional<INTERSECTABLE_GEOM> intersectableGeom;
if( !excludeGraphics && item.Type() == PCB_SHAPE_T )
{
intersectableGeom = GetBoardIntersectable( item );
}
else if( !excludeTracks && ( item.Type() == PCB_TRACE_T || item.Type() == PCB_ARC_T ) )
{
intersectableGeom = GetBoardIntersectable( item );
}
if( intersectableGeom )
{
intersectables.emplace_back( &item, *intersectableGeom );
}
}
};
for( BOARD_ITEM* item : aItems )
{
processItem( *item );
}
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH& batch :
getSnapManager().GetConstructionItems() )
{
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& constructionItem : batch )
{
BOARD_ITEM* involvedItem = static_cast<BOARD_ITEM*>( constructionItem.Item );
for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable :
constructionItem.Constructions )
{
std::visit(
[&]( const auto& visited )
{
using ItemType = std::decay_t<decltype( visited )>;
if constexpr( std::is_same_v<ItemType, LINE>
|| std::is_same_v<ItemType, CIRCLE>
|| std::is_same_v<ItemType, HALF_LINE>
|| std::is_same_v<ItemType, SHAPE_ARC> )
{
intersectables.emplace_back( involvedItem, visited );
}
else if constexpr( std::is_same_v<ItemType, VECTOR2I> )
{
// Add any free-floating points as snap points.
addAnchor( visited, SNAPPABLE | CONSTRUCTED, involvedItem,
POINT_TYPE::PT_NONE );
}
},
drawable );
}
}
}
// Now, add all the intersections between the items
// This is obviously quadratic, so performance may be a concern for large selections
// But, so far up to ~20k comparisons seems not to be an issue with run times in the ms range
// and it's usually only a handful of items.
if( computeIntersections )
{
for( std::size_t ii = 0; ii < intersectables.size(); ++ii )
{
const PCB_INTERSECTABLE& intersectableA = intersectables[ii];
for( std::size_t jj = ii + 1; jj < intersectables.size(); ++jj )
{
const PCB_INTERSECTABLE& intersectableB = intersectables[jj];
// An item and its own extension will often have intersections (as they are on top of each other),
// but they not useful points to snap to
if( intersectableA.Item == intersectableB.Item )
continue;
std::vector<VECTOR2I> intersections;
const INTERSECTION_VISITOR visitor{ intersectableA.Geometry, intersections };
std::visit( visitor, intersectableB.Geometry );
// For each intersection, add an intersection snap anchor
for( const VECTOR2I& intersection : intersections )
{
std::vector<EDA_ITEM*> items = {
intersectableA.Item,
intersectableB.Item,
};
addAnchor( intersection, SNAPPABLE | CONSTRUCTED, std::move( items ),
POINT_TYPE::PT_INTERSECTION );
}
}
}
}
// The intersectables can also be used for fall-back snapping to "point on line"
// snaps if no other snap is found
m_pointOnLineCandidates.clear();
if( computePointsOnElements )
{
// For the moment, it's trivial to make a NEARABLE from an INTERSECTABLE,
// because all INTERSECTABLEs are also NEARABLEs.
for( const PCB_INTERSECTABLE& intersectable : intersectables )
{
std::visit(
[&]( const auto& geom )
{
NEARABLE_GEOM nearable( geom );
m_pointOnLineCandidates.emplace_back( nearable );
},
intersectable.Geometry );
}
}
}
void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter )
{
KIGFX::VIEW* view = m_toolMgr->GetView();
RENDER_SETTINGS* settings = view->GetPainter()->GetSettings();
const std::set<int>& activeLayers = settings->GetHighContrastLayers();
bool isHighContrast = settings->GetHighContrast();
auto checkVisibility =
[&]( BOARD_ITEM* item )
{
if( !view->IsVisible( item ) )
return false;
bool onActiveLayer = !isHighContrast;
bool isLODVisible = false;
for( PCB_LAYER_ID layer : item->GetLayerSet().Seq() )
{
if( !onActiveLayer && activeLayers.count( layer ) )
onActiveLayer = true;
if( !isLODVisible && item->ViewGetLOD( layer, view ) < view->GetScale() )
isLODVisible = true;
if( onActiveLayer && isLODVisible )
return true;
}
return false;
};
// As defaults, these are probably reasonable to avoid spamming key points
const KIGEOM::OVAL_KEY_POINT_FLAGS ovalKeyPointFlags =
KIGEOM::OVAL_CENTER | KIGEOM::OVAL_CAP_TIPS | KIGEOM::OVAL_SIDE_MIDPOINTS
| KIGEOM::OVAL_CARDINAL_EXTREMES;
auto handlePadShape = [&]( PAD* aPad, PCB_LAYER_ID aLayer )
{
addAnchor( aPad->GetPosition(), ORIGIN | SNAPPABLE, aPad, POINT_TYPE::PT_CENTER );
/// If we are getting a drag point, we don't want to center the edge of pads
if( aFrom )
return;
switch( aPad->GetShape( aLayer ) )
{
case PAD_SHAPE::CIRCLE:
{
const CIRCLE circle( aPad->ShapePos( aLayer ), aPad->GetSizeX() / 2 );
for( const TYPED_POINT2I& pt : KIGEOM::GetCircleKeyPoints( circle, false ) )
{
addAnchor( pt.m_point, OUTLINE | SNAPPABLE, aPad, pt.m_types );
}
break;
}
case PAD_SHAPE::OVAL:
{
const OVAL oval( aPad->GetSize( aLayer ), aPad->GetPosition(), aPad->GetOrientation() );
for( const TYPED_POINT2I& pt : KIGEOM::GetOvalKeyPoints( oval, ovalKeyPointFlags ) )
{
addAnchor( pt.m_point, OUTLINE | SNAPPABLE, aPad, pt.m_types );
}
break;
}
case PAD_SHAPE::RECTANGLE:
case PAD_SHAPE::TRAPEZOID:
case PAD_SHAPE::ROUNDRECT:
case PAD_SHAPE::CHAMFERED_RECT:
{
VECTOR2I half_size( aPad->GetSize( aLayer ) / 2 );
VECTOR2I trap_delta( 0, 0 );
if( aPad->GetShape( aLayer ) == PAD_SHAPE::TRAPEZOID )
trap_delta = aPad->GetDelta( aLayer ) / 2;
SHAPE_LINE_CHAIN corners;
corners.Append( -half_size.x - trap_delta.y, half_size.y + trap_delta.x );
corners.Append( half_size.x + trap_delta.y, half_size.y - trap_delta.x );
corners.Append( half_size.x - trap_delta.y, -half_size.y + trap_delta.x );
corners.Append( -half_size.x + trap_delta.y, -half_size.y - trap_delta.x );
corners.SetClosed( true );
corners.Rotate( aPad->GetOrientation() );
corners.Move( aPad->ShapePos( aLayer ) );
for( std::size_t ii = 0; ii < corners.GetSegmentCount(); ++ii )
{
const SEG& seg = corners.GetSegment( ii );
addAnchor( seg.A, OUTLINE | SNAPPABLE, aPad, POINT_TYPE::PT_CORNER );
addAnchor( seg.Center(), OUTLINE | SNAPPABLE, aPad, POINT_TYPE::PT_MID );
if( ii == corners.GetSegmentCount() - 1 )
addAnchor( seg.B, OUTLINE | SNAPPABLE, aPad, POINT_TYPE::PT_CORNER );
}
break;
}
default:
{
const auto& outline = aPad->GetEffectivePolygon( aLayer, ERROR_INSIDE );
if( !outline->IsEmpty() )
{
for( const VECTOR2I& pt : outline->Outline( 0 ).CPoints() )
addAnchor( pt, OUTLINE | SNAPPABLE, aPad );
}
break;
}
}
if( aPad->HasHole() )
{
// Holes are at the pad centre (it's the shape that may be offset)
const VECTOR2I hole_pos = aPad->GetPosition();
const VECTOR2I hole_size = aPad->GetDrillSize();
std::vector<TYPED_POINT2I> snap_pts;
if( hole_size.x == hole_size.y )
{
// Circle
const CIRCLE circle( hole_pos, hole_size.x / 2 );
snap_pts = KIGEOM::GetCircleKeyPoints( circle, true );
}
else
{
// Oval
// For now there's no way to have an off-angle hole, so this is the
// same as the pad. In future, this may not be true:
// https://gitlab.com/kicad/code/kicad/-/issues/4124
const OVAL oval( hole_size, hole_pos, aPad->GetOrientation() );
snap_pts = KIGEOM::GetOvalKeyPoints( oval, ovalKeyPointFlags );
}
for( const TYPED_POINT2I& snap_pt : snap_pts )
addAnchor( snap_pt.m_point, OUTLINE | SNAPPABLE, aPad, snap_pt.m_types );
}
};
auto handleShape =
[&]( PCB_SHAPE* shape )
{
VECTOR2I start = shape->GetStart();
VECTOR2I end = shape->GetEnd();
switch( shape->GetShape() )
{
case SHAPE_T::CIRCLE:
{
const int r = ( start - end ).EuclideanNorm();
addAnchor( start, ORIGIN | SNAPPABLE, shape, POINT_TYPE::PT_CENTER );
addAnchor( start + VECTOR2I( -r, 0 ), OUTLINE | SNAPPABLE, shape,
POINT_TYPE::PT_QUADRANT );
addAnchor( start + VECTOR2I( r, 0 ), OUTLINE | SNAPPABLE, shape,
POINT_TYPE::PT_QUADRANT );
addAnchor( start + VECTOR2I( 0, -r ), OUTLINE | SNAPPABLE, shape,
POINT_TYPE::PT_QUADRANT );
addAnchor( start + VECTOR2I( 0, r ), OUTLINE | SNAPPABLE, shape,
POINT_TYPE::PT_QUADRANT );
break;
}
case SHAPE_T::ARC:
addAnchor( shape->GetStart(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_END );
addAnchor( shape->GetEnd(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_END );
addAnchor( shape->GetArcMid(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_MID );
addAnchor( shape->GetCenter(), ORIGIN | SNAPPABLE, shape,
POINT_TYPE::PT_CENTER );
break;
case SHAPE_T::RECTANGLE:
{
VECTOR2I point2( end.x, start.y );
VECTOR2I point3( start.x, end.y );
SEG first( start, point2 );
SEG second( point2, end );
SEG third( end, point3 );
SEG fourth( point3, start );
const int snapFlags = CORNER | SNAPPABLE;
addAnchor( shape->GetCenter(), snapFlags, shape, POINT_TYPE::PT_CENTER );
addAnchor( first.A, snapFlags, shape, POINT_TYPE::PT_CORNER );
addAnchor( first.Center(), snapFlags, shape, POINT_TYPE::PT_MID );
addAnchor( second.A, snapFlags, shape, POINT_TYPE::PT_CORNER );
addAnchor( second.Center(), snapFlags, shape, POINT_TYPE::PT_MID );
addAnchor( third.A, snapFlags, shape, POINT_TYPE::PT_CORNER );
addAnchor( third.Center(), snapFlags, shape, POINT_TYPE::PT_MID );
addAnchor( fourth.A, snapFlags, shape, POINT_TYPE::PT_CORNER );
addAnchor( fourth.Center(), snapFlags, shape, POINT_TYPE::PT_MID );
break;
}
case SHAPE_T::SEGMENT:
addAnchor( start, CORNER | SNAPPABLE, shape, POINT_TYPE::PT_END );
addAnchor( end, CORNER | SNAPPABLE, shape, POINT_TYPE::PT_END );
addAnchor( shape->GetCenter(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_MID );
break;
case SHAPE_T::POLY:
{
SHAPE_LINE_CHAIN lc;
lc.SetClosed( true );
std::vector<VECTOR2I> poly;
shape->DupPolyPointsList( poly );
for( const VECTOR2I& p : poly )
{
addAnchor( p, CORNER | SNAPPABLE, shape, POINT_TYPE::PT_CORNER );
lc.Append( p );
}
addAnchor( lc.NearestPoint( aRefPos ), OUTLINE, aItem );
break;
}
case SHAPE_T::BEZIER:
addAnchor( start, CORNER | SNAPPABLE, shape, POINT_TYPE::PT_END );
addAnchor( end, CORNER | SNAPPABLE, shape, POINT_TYPE::PT_END );
KI_FALLTHROUGH;
default:
addAnchor( shape->GetPosition(), ORIGIN | SNAPPABLE, shape );
break;
}
};
switch( aItem->Type() )
{
case PCB_FOOTPRINT_T:
{
FOOTPRINT* footprint = static_cast<FOOTPRINT*>( aItem );
for( PAD* pad : footprint->Pads() )
{
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->pads )
continue;
}
else
{
if( m_magneticSettings->pads != MAGNETIC_OPTIONS::CAPTURE_ALWAYS )
continue;
}
if( !checkVisibility( pad ) )
continue;
if( !pad->GetBoundingBox().Contains( aRefPos ) )
continue;
pad->Padstack().ForEachUniqueLayer(
[&]( PCB_LAYER_ID aLayer )
{
if( !isHighContrast || activeLayers.count( aLayer ) )
handlePadShape( pad, aLayer );
} );
}
if( aFrom && aSelectionFilter && !aSelectionFilter->footprints )
break;
// If the cursor is not over a pad, snap to the anchor (if visible) or the center
// (if markedly different from the anchor).
VECTOR2I position = footprint->GetPosition();
VECTOR2I center = footprint->GetBoundingBox( false ).Centre();
VECTOR2I grid( GetGrid() );
if( view->IsLayerVisible( LAYER_ANCHOR ) )
addAnchor( position, ORIGIN | SNAPPABLE, footprint, POINT_TYPE::PT_CENTER );
if( ( center - position ).SquaredEuclideanNorm() > grid.SquaredEuclideanNorm() )
addAnchor( center, ORIGIN | SNAPPABLE, footprint, POINT_TYPE::PT_CENTER );
break;
}
case PCB_PAD_T:
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->pads )
break;
}
else
{
if( m_magneticSettings->pads != MAGNETIC_OPTIONS::CAPTURE_ALWAYS )
break;
}
if( checkVisibility( aItem ) )
{
PAD* pad = static_cast<PAD*>( aItem );
pad->Padstack().ForEachUniqueLayer(
[&]( PCB_LAYER_ID aLayer )
{
if( !isHighContrast || activeLayers.count( aLayer ) )
handlePadShape( pad, aLayer );
} );
}
break;
case PCB_TEXTBOX_T:
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->text )
break;
}
else
{
if( !m_magneticSettings->graphics )
break;
}
if( checkVisibility( aItem ) )
handleShape( static_cast<PCB_SHAPE*>( aItem ) );
break;
case PCB_SHAPE_T:
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->graphics )
break;
}
else
{
if( !m_magneticSettings->graphics )
break;
}
if( checkVisibility( aItem ) )
handleShape( static_cast<PCB_SHAPE*>( aItem ) );
break;
case PCB_TRACE_T:
case PCB_ARC_T:
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->tracks )
break;
}
else
{
if( m_magneticSettings->tracks != MAGNETIC_OPTIONS::CAPTURE_ALWAYS )
break;
}
if( checkVisibility( aItem ) )
{
PCB_TRACK* track = static_cast<PCB_TRACK*>( aItem );
addAnchor( track->GetStart(), CORNER | SNAPPABLE, track, POINT_TYPE::PT_END );
addAnchor( track->GetEnd(), CORNER | SNAPPABLE, track, POINT_TYPE::PT_END );
addAnchor( track->GetCenter(), ORIGIN, track, POINT_TYPE::PT_MID );
}
break;
case PCB_MARKER_T:
case PCB_TARGET_T:
addAnchor( aItem->GetPosition(), ORIGIN | CORNER | SNAPPABLE, aItem,
POINT_TYPE::PT_CENTER );
break;
case PCB_VIA_T:
if( aFrom )
{
if( aSelectionFilter && !aSelectionFilter->vias )
break;
}
else
{
if( m_magneticSettings->tracks != MAGNETIC_OPTIONS::CAPTURE_ALWAYS )
break;
}
if( checkVisibility( aItem ) )
addAnchor( aItem->GetPosition(), ORIGIN | CORNER | SNAPPABLE, aItem,
POINT_TYPE::PT_CENTER );
break;
case PCB_ZONE_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->zones )
break;
if( checkVisibility( aItem ) )
{
const SHAPE_POLY_SET* outline = static_cast<const ZONE*>( aItem )->Outline();
SHAPE_LINE_CHAIN lc;
lc.SetClosed( true );
for( auto iter = outline->CIterateWithHoles(); iter; iter++ )
{
addAnchor( *iter, CORNER | SNAPPABLE, aItem, POINT_TYPE::PT_CORNER );
lc.Append( *iter );
}
addAnchor( lc.NearestPoint( aRefPos ), OUTLINE, aItem );
}
break;
case PCB_DIM_ALIGNED_T:
case PCB_DIM_ORTHOGONAL_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->dimensions )
break;
if( checkVisibility( aItem ) )
{
const PCB_DIM_ALIGNED* dim = static_cast<const PCB_DIM_ALIGNED*>( aItem );
addAnchor( dim->GetCrossbarStart(), CORNER | SNAPPABLE, aItem );
addAnchor( dim->GetCrossbarEnd(), CORNER | SNAPPABLE, aItem );
addAnchor( dim->GetStart(), CORNER | SNAPPABLE, aItem );
addAnchor( dim->GetEnd(), CORNER | SNAPPABLE, aItem );
}
break;
case PCB_DIM_CENTER_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->dimensions )
break;
if( checkVisibility( aItem ) )
{
const PCB_DIM_CENTER* dim = static_cast<const PCB_DIM_CENTER*>( aItem );
addAnchor( dim->GetStart(), CORNER | SNAPPABLE, aItem );
addAnchor( dim->GetEnd(), CORNER | SNAPPABLE, aItem );
VECTOR2I start( dim->GetStart() );
VECTOR2I radial( dim->GetEnd() - dim->GetStart() );
for( int i = 0; i < 2; i++ )
{
RotatePoint( radial, -ANGLE_90 );
addAnchor( start + radial, CORNER | SNAPPABLE, aItem );
}
}
break;
case PCB_DIM_RADIAL_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->dimensions )
break;
if( checkVisibility( aItem ) )
{
const PCB_DIM_RADIAL* radialDim = static_cast<const PCB_DIM_RADIAL*>( aItem );
addAnchor( radialDim->GetStart(), CORNER | SNAPPABLE, aItem );
addAnchor( radialDim->GetEnd(), CORNER | SNAPPABLE, aItem );
addAnchor( radialDim->GetKnee(), CORNER | SNAPPABLE, aItem );
addAnchor( radialDim->GetTextPos(), CORNER | SNAPPABLE, aItem );
}
break;
case PCB_DIM_LEADER_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->dimensions )
break;
if( checkVisibility( aItem ) )
{
const PCB_DIM_LEADER* leader = static_cast<const PCB_DIM_LEADER*>( aItem );
addAnchor( leader->GetStart(), CORNER | SNAPPABLE, aItem );
addAnchor( leader->GetEnd(), CORNER | SNAPPABLE, aItem );
addAnchor( leader->GetTextPos(), CORNER | SNAPPABLE, aItem );
}
break;
case PCB_FIELD_T:
case PCB_TEXT_T:
if( aFrom && aSelectionFilter && !aSelectionFilter->text )
break;
if( checkVisibility( aItem ) )
addAnchor( aItem->GetPosition(), ORIGIN, aItem );
break;
case PCB_GROUP_T:
for( BOARD_ITEM* item : static_cast<const PCB_GROUP*>( aItem )->GetItems() )
{
if( checkVisibility( item ) )
computeAnchors( item, aRefPos, aFrom, nullptr );
}
break;
default:
break;
}
}
PCB_GRID_HELPER::ANCHOR* PCB_GRID_HELPER::nearestAnchor( const VECTOR2I& aPos, int aFlags )
{
// Do this all in squared distances as we only care about relative distances
using ecoord = VECTOR2I::extended_type;
ecoord minDist = std::numeric_limits<ecoord>::max();
std::vector<ANCHOR*> anchorsAtMinDistance;
for( ANCHOR& anchor : m_anchors )
{
// There is no need to filter by layers here, as the items are already filtered
// by layer (if needed) when the anchors are computed.
if( ( aFlags & anchor.flags ) != aFlags )
continue;
if( !anchorsAtMinDistance.empty() && anchor.pos == anchorsAtMinDistance.front()->pos )
{
// Same distance as the previous best anchor
anchorsAtMinDistance.push_back( &anchor );
}
else
{
const double dist = anchor.pos.SquaredDistance( aPos );
if( dist < minDist )
{
// New minimum distance
minDist = dist;
anchorsAtMinDistance.clear();
anchorsAtMinDistance.push_back( &anchor );
}
}
}
// More than one anchor can be at the same distance, for example
// two lines end-to-end each have the same endpoint anchor.
// So, check which one has an involved item that's closest to the origin,
// and use that one (which allows the user to choose which items
// gets extended - it's the one nearest the cursor)
ecoord minDistToItem = std::numeric_limits<ecoord>::max();
ANCHOR* best = nullptr;
// One of the anchors at the minimum distance
for( ANCHOR* const anchor : anchorsAtMinDistance )
{
ecoord distToNearestItem = std::numeric_limits<ecoord>::max();
for( EDA_ITEM* const item : anchor->items )
{
if( !item )
continue;
std::optional<ecoord> distToThisItem =
FindSquareDistanceToItem( static_cast<const BOARD_ITEM&>( *item ), aPos );
if( distToThisItem )
distToNearestItem = std::min( distToNearestItem, *distToThisItem );
}
// If the item doesn't have any special min-dist handler,
// just use the distance to the anchor
distToNearestItem = std::min( distToNearestItem, minDist );
if( distToNearestItem < minDistToItem )
{
minDistToItem = distToNearestItem;
best = anchor;
}
}
return best;
}