7
mirror of https://gitlab.com/kicad/code/kicad.git synced 2025-04-21 00:21:25 +00:00

Snapping: delayed activation of snap points.

This makes it easier to control what snap points you are aiming for
without accidentally activating objects just by mousing near them
on the way to somewhere else.
This commit is contained in:
John Beard 2024-09-24 23:09:47 +01:00
parent 1effaf83c3
commit e23b83505e
8 changed files with 606 additions and 208 deletions

View File

@ -118,6 +118,8 @@ static const wxChar MaxFileSystemWatchers[] = wxT( "MaxFileSystemWatchers" );
static const wxChar MinorSchematicGraphSize[] = wxT( "MinorSchematicGraphSize" );
static const wxChar ResolveTextRecursionDepth[] = wxT( "ResolveTextRecursionDepth" );
static const wxChar EnableExtensionSnaps[] = wxT( "EnableExtensionSnaps" );
static const wxChar ExtensionSnapTimeoutMs[] = wxT( "ExtensionSnapTimeoutMs" );
static const wxChar ExtensionSnapActivateOnHover[] = wxT( "ExtensionSnapActivateOnHover" );
static const wxChar EnableSnapAnchorsDebug[] = wxT( "EnableSnapAnchorsDebug" );
static const wxChar EnableODB[] = wxT( "EnableODB" );
static const wxChar EnableJobset[] = wxT( "EnableJobset" );
@ -286,6 +288,8 @@ ADVANCED_CFG::ADVANCED_CFG()
m_ResolveTextRecursionDepth = 3;
m_EnableExtensionSnaps = false;
m_ExtensionSnapTimeoutMs = 500;
m_ExtensionSnapActivateOnHover = false;
m_EnableSnapAnchorsDebug = false;
loadFromConfigFile();
@ -538,12 +542,20 @@ void ADVANCED_CFG::loadSettings( wxConfigBase& aCfg )
&m_EnableODB, m_EnableODB ) );
configParams.push_back( new PARAM_CFG_BOOL( true, AC_KEYS::EnableExtensionSnaps,
&m_EnableExtensionSnaps,
m_EnableExtensionSnaps ) );
&m_EnableExtensionSnaps,
m_EnableExtensionSnaps ) );
configParams.push_back( new PARAM_CFG_INT( true, AC_KEYS::ExtensionSnapTimeoutMs,
&m_ExtensionSnapTimeoutMs,
m_ExtensionSnapTimeoutMs, 0 ) );
configParams.push_back( new PARAM_CFG_BOOL( true, AC_KEYS::ExtensionSnapActivateOnHover,
&m_ExtensionSnapActivateOnHover,
m_ExtensionSnapActivateOnHover ) );
configParams.push_back( new PARAM_CFG_BOOL( true, AC_KEYS::EnableSnapAnchorsDebug,
&m_EnableSnapAnchorsDebug,
m_EnableSnapAnchorsDebug ) );
&m_EnableSnapAnchorsDebug,
m_EnableSnapAnchorsDebug ) );
// Special case for trace mask setting...we just grab them and set them immediately
// Because we even use wxLogTrace inside of advanced config

View File

@ -21,37 +21,227 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <tool/construction_manager.h>
#include "tool/construction_manager.h"
#include <chrono>
#include <condition_variable>
#include <thread>
CONSTRUCTION_MANAGER::CONSTRUCTION_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper ) :
m_constructionGeomPreview( aHelper )
#include <advanced_config.h>
#include <hash.h>
/**
* A helper class to manage the activation of a "proposal" after a timeout.
*
* When a proposal is made, a timer starts. If no new proposal is made and the proposal
* is not cancelled before the timer expires, the proposal is "accepted" via a callback.
*
* Propos
*
* @tparam T The type of the proposal, which will be passed to the callback (by value)
*/
template <typename T>
class ACTIVATION_HELPER
{
}
public:
using ACTIVATION_CALLBACK = std::function<void( T&& )>;
void CONSTRUCTION_MANAGER::updateView()
{
if( m_updateCallback )
ACTIVATION_HELPER( std::chrono::milliseconds aTimeout, ACTIVATION_CALLBACK aCallback ) :
m_timeout( aTimeout ), m_callback( std::move( aCallback ) ), m_stop( false ),
m_thread( &ACTIVATION_HELPER::ProposalCheckFunction, this )
{
bool showAnything = m_persistentConstructionBatch || !m_temporaryConstructionBatches.empty()
|| ( m_snapLineOrigin && m_snapLineEnd );
m_updateCallback( showAnything );
}
~ACTIVATION_HELPER()
{
// Stop the delay thread and wait for it
{
std::lock_guard<std::mutex> lock( m_mutex );
m_stop = true;
m_cv.notify_all();
}
if( m_thread.joinable() )
{
m_thread.join();
}
}
void ProposeActivation( T&& aProposal, std::size_t aProposalTag )
{
std::lock_guard<std::mutex> lock( m_mutex );
if( m_lastAcceptedProposalTag.has_value() && aProposalTag == *m_lastAcceptedProposalTag )
{
// This proposal was accepted last time
// (could be made optional if we want to allow re-accepting the same proposal)
return;
}
if( m_pendingProposalTag.has_value() && aProposalTag == *m_pendingProposalTag )
{
// This proposal is already pending
return;
}
m_pendingProposalTag = aProposalTag;
m_lastProposal = std::move( aProposal );
m_proposalDeadline = std::chrono::steady_clock::now() + m_timeout;
m_cv.notify_all();
}
void CancelProposal()
{
std::lock_guard<std::mutex> lock( m_mutex );
m_pendingProposalTag.reset();
m_cv.notify_all();
}
void ProposalCheckFunction()
{
while( !m_stop )
{
std::unique_lock<std::mutex> lock( m_mutex );
if( !m_stop && !m_pendingProposalTag.has_value() )
{
// No active proposal - wait for one (unlocks while waiting)
m_cv.wait( lock );
}
if( !m_stop && m_pendingProposalTag.has_value() )
{
// Active proposal - wait for timeout
auto now = std::chrono::steady_clock::now();
if( m_cv.wait_for( lock, m_proposalDeadline - now ) == std::cv_status::timeout )
{
// See if the timeout was extended for a new proposal
now = std::chrono::steady_clock::now();
if( now < m_proposalDeadline )
{
// Extended - wait for the new deadline
continue;
}
// See if there is still a proposal to accept
// (could have been cancelled in the meantime)
if( m_pendingProposalTag )
{
m_lastAcceptedProposalTag = m_pendingProposalTag;
m_pendingProposalTag.reset();
T proposalToAccept = std::move( m_lastProposal );
lock.unlock();
// Call the callback (outside the lock)
m_callback( std::move( proposalToAccept ) );
}
}
}
}
}
private:
mutable std::mutex m_mutex;
// Activation timeout in milliseconds
std::chrono::milliseconds m_timeout;
///< Callback to call when the proposal is accepted
ACTIVATION_CALLBACK m_callback;
std::condition_variable m_cv;
std::atomic<bool> m_stop;
std::thread m_thread;
std::chrono::time_point<std::chrono::steady_clock> m_proposalDeadline;
///< The last proposal tag that was made
std::optional<std::size_t> m_pendingProposalTag;
///< The last proposal that was accepted
std::optional<std::size_t> m_lastAcceptedProposalTag;
// The most recently-proposed item
T m_lastProposal;
};
struct CONSTRUCTION_MANAGER::PENDING_BATCH
{
CONSTRUCTION_ITEM_BATCH Batch;
bool IsPersistent;
};
CONSTRUCTION_MANAGER::CONSTRUCTION_MANAGER( CONSTRUCTION_VIEW_HANDLER& aHelper ) :
m_viewHandler( aHelper )
{
const std::chrono::milliseconds acceptanceTimeout(
ADVANCED_CFG::GetCfg().m_ExtensionSnapTimeoutMs );
m_activationHelper = std::make_unique<ACTIVATION_HELPER<PENDING_BATCH>>(
acceptanceTimeout,
[this]( PENDING_BATCH&& aAccepted )
{
acceptConstructionItems( std::move( aAccepted ) );
} );
}
void CONSTRUCTION_MANAGER::AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
bool aIsPersistent )
CONSTRUCTION_MANAGER::~CONSTRUCTION_MANAGER()
{
if( aIsPersistent )
}
/**
* Construct a hash based on the sources of the items in the batch.
*/
static std::size_t
HashConstructionBatchSources( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH& aBatch,
bool aIsPersistent )
{
std::size_t hash = hash_val( aIsPersistent );
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& item : aBatch )
{
hash_combine( hash, item.Source, item.Item );
}
return hash;
}
void CONSTRUCTION_MANAGER::ProposeConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
bool aIsPersistent )
{
const std::size_t hash = HashConstructionBatchSources( aBatch, aIsPersistent );
m_activationHelper->ProposeActivation(
PENDING_BATCH{
std::move( aBatch ),
aIsPersistent,
},
hash );
}
void CONSTRUCTION_MANAGER::CancelProposal()
{
// static int i = 0;
// std::cout << "Cancelling proposal " << i++ << std::endl;
m_activationHelper->CancelProposal();
}
void CONSTRUCTION_MANAGER::acceptConstructionItems( PENDING_BATCH&& aAcceptedBatch )
{
if( aAcceptedBatch.IsPersistent )
{
// We only keep one previous persistent batch for the moment
m_persistentConstructionBatch = std::move( aBatch );
m_persistentConstructionBatch = std::move( aAcceptedBatch.Batch );
}
else
{
bool anyNewItems = false;
for( CONSTRUCTION_ITEM& item : aBatch )
for( CONSTRUCTION_ITEM& item : aAcceptedBatch.Batch )
{
if( m_involvedItems.count( item.Item ) == 0 )
{
@ -75,12 +265,13 @@ void CONSTRUCTION_MANAGER::AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
m_temporaryConstructionBatches.pop_front();
}
m_temporaryConstructionBatches.emplace_back( std::move( aBatch ) );
m_temporaryConstructionBatches.emplace_back( std::move( aAcceptedBatch.Batch ) );
}
// Refresh what items are drawn
m_constructionGeomPreview.ClearDrawables();
KIGFX::CONSTRUCTION_GEOM& geom = m_viewHandler.GetViewItem();
geom.ClearDrawables();
m_involvedItems.clear();
const auto addBatchItems = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd, bool aPersistent )
@ -95,7 +286,7 @@ void CONSTRUCTION_MANAGER::AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& construction : item.Constructions )
{
m_constructionGeomPreview.AddDrawable( construction, aPersistent );
geom.AddDrawable( construction, aPersistent );
}
}
}
@ -111,7 +302,7 @@ void CONSTRUCTION_MANAGER::AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
addBatchItems( batch, false );
}
updateView();
m_viewHandler.updateView();
}
bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
@ -128,39 +319,63 @@ bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM
return true;
}
void CONSTRUCTION_MANAGER::SetSnapLineOrigin( const VECTOR2I& aOrigin )
void CONSTRUCTION_MANAGER::GetConstructionItems(
std::vector<CONSTRUCTION_ITEM_BATCH>& aToExtend ) const
{
if( m_persistentConstructionBatch )
{
aToExtend.push_back( *m_persistentConstructionBatch );
}
for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
{
aToExtend.push_back( batch );
}
}
bool CONSTRUCTION_MANAGER::HasActiveConstruction() const
{
return m_persistentConstructionBatch.has_value() || !m_temporaryConstructionBatches.empty();
}
SNAP_LINE_MANAGER::SNAP_LINE_MANAGER( CONSTRUCTION_VIEW_HANDLER& aViewHandler ) :
m_viewHandler( aViewHandler )
{
}
void SNAP_LINE_MANAGER::SetSnapLineOrigin( const VECTOR2I& aOrigin )
{
// Setting the origin clears the snap line as the end point is no longer valid
ClearSnapLine();
m_snapLineOrigin = aOrigin;
}
void CONSTRUCTION_MANAGER::SetSnapLineEnd( const OPT_VECTOR2I& aSnapEnd )
void SNAP_LINE_MANAGER::SetSnapLineEnd( const OPT_VECTOR2I& aSnapEnd )
{
if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
{
m_snapLineEnd = aSnapEnd;
if( m_snapLineEnd )
m_constructionGeomPreview.SetSnapLine( SEG{ *m_snapLineOrigin, *m_snapLineEnd } );
m_viewHandler.GetViewItem().SetSnapLine( SEG{ *m_snapLineOrigin, *m_snapLineEnd } );
else
m_constructionGeomPreview.ClearSnapLine();
m_viewHandler.GetViewItem().ClearSnapLine();
updateView();
m_viewHandler.updateView();
}
}
void CONSTRUCTION_MANAGER::ClearSnapLine()
void SNAP_LINE_MANAGER::ClearSnapLine()
{
m_snapLineOrigin.reset();
m_snapLineEnd.reset();
m_constructionGeomPreview.ClearSnapLine();
updateView();
m_viewHandler.GetViewItem().ClearSnapLine();
m_viewHandler.updateView();
}
void CONSTRUCTION_MANAGER::SetSnappedAnchor( const VECTOR2I& aAnchorPos )
void SNAP_LINE_MANAGER::SetSnappedAnchor( const VECTOR2I& aAnchorPos )
{
if( m_snapLineOrigin )
if( m_snapLineOrigin.has_value() )
{
if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
{
@ -176,46 +391,10 @@ void CONSTRUCTION_MANAGER::SetSnappedAnchor( const VECTOR2I& aAnchorPos )
else
{
// If there's no snap line, start one
m_snapLineOrigin = aAnchorPos;
SetSnapLineOrigin( aAnchorPos );
}
}
std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
CONSTRUCTION_MANAGER::GetConstructionItems() const
{
std::vector<CONSTRUCTION_ITEM_BATCH> batches;
if( m_persistentConstructionBatch )
{
batches.push_back( *m_persistentConstructionBatch );
}
for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
{
batches.push_back( batch );
}
if( m_snapLineOrigin )
{
CONSTRUCTION_ITEM_BATCH batch;
CONSTRUCTION_ITEM& snapPointItem = batch.emplace_back( CONSTRUCTION_ITEM{
SOURCE::FROM_SNAP_LINE,
nullptr,
{},
} );
snapPointItem.Constructions.push_back(
LINE{ *m_snapLineOrigin, *m_snapLineOrigin + VECTOR2I( 100000, 0 ) } );
snapPointItem.Constructions.push_back(
LINE{ *m_snapLineOrigin, *m_snapLineOrigin + VECTOR2I( 0, 100000 ) } );
batches.push_back( std::move( batch ) );
}
return batches;
}
/**
* Check if the cursor has moved far enough away from the snap line origin to escape snapping
@ -250,10 +429,10 @@ static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& a
}
OPT_VECTOR2I CONSTRUCTION_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& aCursor,
const VECTOR2I& aNearestGrid,
std::optional<int> aDistToNearest,
int aSnapRange ) const
OPT_VECTOR2I SNAP_LINE_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& aCursor,
const VECTOR2I& aNearestGrid,
std::optional<int> aDistToNearest,
int aSnapRange ) const
{
// return std::nullopt;
if( m_snapLineOrigin )
@ -269,7 +448,7 @@ OPT_VECTOR2I CONSTRUCTION_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& a
// deliberately with a mouse move.
// These are both a bit arbitrary, and can be adjusted as preferred
const int escapeRange = 2 * aSnapRange;
const EDA_ANGLE longRangeEscapeAngle( 3, DEGREES_T );
const EDA_ANGLE longRangeEscapeAngle( 4, DEGREES_T );
const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
longRangeEscapeAngle );
@ -297,4 +476,55 @@ OPT_VECTOR2I CONSTRUCTION_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& a
}
return std::nullopt;
}
SNAP_MANAGER::SNAP_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper ) :
CONSTRUCTION_VIEW_HANDLER( aHelper ), m_snapLineManager( *this ),
m_constructionManager( *this )
{
}
void SNAP_MANAGER::updateView()
{
if( m_updateCallback )
{
bool showAnything = m_constructionManager.HasActiveConstruction()
|| m_snapLineManager.HasCompleteSnapLine();
m_updateCallback( showAnything );
}
}
std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
SNAP_MANAGER::GetConstructionItems() const
{
std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> batches;
m_constructionManager.GetConstructionItems( batches );
if( const OPT_VECTOR2I& snapLineOrigin = m_snapLineManager.GetSnapLineOrigin();
snapLineOrigin.has_value() )
{
CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH batch;
CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& snapPointItem =
batch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
CONSTRUCTION_MANAGER::SOURCE::FROM_SNAP_LINE,
nullptr,
{},
} );
// One horizontal and one vertical infinite line from the snap point
snapPointItem.Constructions.push_back(
LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 100000, 0 ) } );
snapPointItem.Constructions.push_back(
LINE{ *snapLineOrigin, *snapLineOrigin + VECTOR2I( 0, 100000 ) } );
batches.push_back( std::move( batch ) );
}
return batches;
}

View File

@ -37,7 +37,7 @@
GRID_HELPER::GRID_HELPER( TOOL_MANAGER* aToolMgr, int aConstructionLayer ) :
m_toolMgr( aToolMgr ), m_constructionManager( m_constructionGeomPreview )
m_toolMgr( aToolMgr ), m_snapManager( m_constructionGeomPreview )
{
m_maskTypes = ALL;
m_enableSnap = true;
@ -55,7 +55,7 @@ GRID_HELPER::GRID_HELPER( TOOL_MANAGER* aToolMgr, int aConstructionLayer ) :
view->Add( &m_constructionGeomPreview );
view->SetVisible( &m_constructionGeomPreview, false );
m_constructionManager.SetUpdateCallback(
m_snapManager.SetUpdateCallback(
[view, this]( bool aAnythingShown )
{
const bool currentlyVisible = view->IsVisible( &m_constructionGeomPreview );

View File

@ -172,7 +172,8 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
showConstructionGeometry( m_enableSnap );
std::optional<VECTOR2I> snapLineOrigin = getConstructionManager().GetSnapLineOrigin();
SNAP_LINE_MANAGER& snapLineManager = getSnapManager().GetSnapLineManager();
std::optional<VECTOR2I> snapLineOrigin = snapLineManager.GetSnapLineOrigin();
if( m_enableSnapLine && m_snapItem && snapLineOrigin.has_value()
&& m_skipPoint != *snapLineOrigin )
@ -236,14 +237,14 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
if( snapLineX || snapLineY )
{
getConstructionManager().SetSnapLineEnd( pt );
snapLineManager.SetSnapLineEnd( pt );
}
else if( snapPoint )
{
m_snapItem = *nearest;
m_viewSnapPoint.SetPosition( pt );
getConstructionManager().SetSnapLineOrigin( pt );
snapLineManager.SetSnapLineOrigin( pt );
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapPoint ) )
m_toolMgr->GetView()->Update( &m_viewSnapPoint, KIGFX::GEOMETRY);
@ -252,7 +253,7 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
}
else
{
getConstructionManager().ClearSnapLine();
snapLineManager.ClearSnapLine();
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
}

View File

@ -656,11 +656,38 @@ public:
/**
* Enable snap anchors based on item line extensions.
*
* This should be removed when extension snaps are tuned up.
*
* Setting name: "EnableExtensionSnaps"
* Default value: false
*/
bool m_EnableExtensionSnaps;
/**
* If extension snaps are enabled, this is the timeout in milliseconds
* before a hovered item gets extensions shown.
*
* This should be removed if a good value is agreed, or made configurable
* if there's no universal good value.
*
* Setting name: "EnableExtensionSnapsMs"
* Default value: 500
* Valid values: >0
*/
int m_ExtensionSnapTimeoutMs;
/**
* If extension snaps are enabled, 'activate' items on
* hover, even if not near a snap point.
*
* This just to experiment with turning. It should either
* be removed or made configurable when we know what feels best.
*
* Setting name: "ExtensionSnapActivateOnHover"
* Default value: false
*/
bool m_ExtensionSnapActivateOnHover;
/**
* Enable snap anchors debug visualization.
*

View File

@ -29,71 +29,44 @@
#include <preview_items/construction_geom.h>
template <typename T>
class ACTIVATION_HELPER;
class EDA_ITEM;
/**
* A class that mananges "construction" objects and geometry.
*
* Probably only used by GRID_HELPERs, but it's neater to keep it separate,
* as there's quite a bit of state to manage.
* Interface wrapper for the construction geometry preview,
* with a callback to signal the view owner that the view needs to be updated.
*/
class CONSTRUCTION_MANAGER
class CONSTRUCTION_VIEW_HANDLER
{
public:
CONSTRUCTION_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper );
/**
* Items to be used for the construction of "virtual" anchors, for example, when snapping to
* a point involving an _extension_ of an existing line or arc.
*
* One item can have multiple construction items (e.g. an arc can have a circle and centre point).
*/
enum class SOURCE
CONSTRUCTION_VIEW_HANDLER( KIGFX::CONSTRUCTION_GEOM& aHelper ) :
m_constructionGeomPreview( aHelper )
{
FROM_ITEMS,
FROM_SNAP_LINE,
};
struct CONSTRUCTION_ITEM
{
SOURCE Source;
EDA_ITEM* Item;
std::vector<KIGFX::CONSTRUCTION_GEOM::DRAWABLE> Constructions;
};
// A single batch of construction items. Once batch contains all the items (and associated
// construction geometry) that should be shown for one point of interest.
// More than one batch may be shown on the screen at the same time.
using CONSTRUCTION_ITEM_BATCH = std::vector<CONSTRUCTION_ITEM>;
/**
* Add a batch of construction items to the helper.
*
* @param aBatch The batch of construction items to add.
* @param aIsPersistent If true, the batch is considered "persistent" and will always be shown
* (and it will replace any previous persistent batch).
* If false, the batch is temporary and may be pushed out by other batches.
*/
void AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch, bool aIsPersistent );
/**
* Check if all 'real' (non-null = constructed) the items in the batch are in the list of items
* currently 'involved' in an active construction.
*/
bool InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const;
/**
* Set the reference-only points - these are points that are not snapped to, but can still
* be used for connection to the snap line.
*/
void SetReferenceOnlyPoints( std::vector<VECTOR2I> aPoints )
{
m_referenceOnlyPoints = std::move( aPoints );
}
const std::vector<VECTOR2I>& GetReferenceOnlyPoints() const { return m_referenceOnlyPoints; }
virtual void updateView() = 0;
KIGFX::CONSTRUCTION_GEOM& GetViewItem() { return m_constructionGeomPreview; }
private:
// An (external) construction helper view item, that this manager adds/removes
// construction objects to/from.
KIGFX::CONSTRUCTION_GEOM& m_constructionGeomPreview;
};
/**
* A class that manages the geometry of a "snap line".
*
* This is a line that has a start point (the "snap origin") and an end point (the "snap end").
* The end can only be set if the origin is set. If the origin is set, the end will be unset.
*/
class SNAP_LINE_MANAGER
{
public:
SNAP_LINE_MANAGER( CONSTRUCTION_VIEW_HANDLER& aViewHandler );
/**
* The snap point is a special point that is located at the last point the cursor
* snapped to. If it is set, the construction manager may add extra construction
@ -114,18 +87,17 @@ public:
*/
void ClearSnapLine();
std::optional<VECTOR2I> GetSnapLineOrigin() const { return m_snapLineOrigin; }
const OPT_VECTOR2I& GetSnapLineOrigin() const { return m_snapLineOrigin; }
bool HasCompleteSnapLine() const { return m_snapLineOrigin && m_snapLineEnd; }
/**
* Inform the construction manager that an anchor snap is wanted.
* Inform this manager that an anchor snap has been made.
*
* This will also update the snap line if appropriate.
* This will also update the start or end of the snap line as appropriate.
*/
void SetSnappedAnchor( const VECTOR2I& aAnchorPos );
// Get the list of additional geometry items that should be considered
std::vector<CONSTRUCTION_ITEM_BATCH> GetConstructionItems() const;
/**
* If the snap line is active, return the best snap point that is closest to the cursor
*
@ -144,18 +116,83 @@ public:
OPT_VECTOR2I GetNearestSnapLinePoint( const VECTOR2I& aCursor, const VECTOR2I& aNearestGrid,
std::optional<int> aDistToNearest, int snapRange ) const;
using GFX_UPDATE_CALLBACK = std::function<void( bool )>;
private:
// If a snap point is "active", extra construction geometry is added to the helper
// extending from the snap point to the cursor.
OPT_VECTOR2I m_snapLineOrigin;
OPT_VECTOR2I m_snapLineEnd;
// The view handler to update when the snap line changes
CONSTRUCTION_VIEW_HANDLER& m_viewHandler;
};
/**
* A class that mananges "construction" objects and geometry.
* These are things like line extensions, arc centers, etc.
*/
class CONSTRUCTION_MANAGER
{
public:
CONSTRUCTION_MANAGER( CONSTRUCTION_VIEW_HANDLER& aViewHandler );
~CONSTRUCTION_MANAGER();
enum class SOURCE
{
FROM_ITEMS,
FROM_SNAP_LINE,
};
/**
* Set the callback to call when the construction geometry changes and a view may need updating.
* Items to be used for the construction of "virtual" anchors, for example, when snapping to
* a point involving an _extension_ of an existing line or arc.
*
* One item can have multiple construction items (e.g. an arc can have a circle and centre point).
*/
void SetUpdateCallback( GFX_UPDATE_CALLBACK aCallback ) { m_updateCallback = aCallback; }
struct CONSTRUCTION_ITEM
{
SOURCE Source;
EDA_ITEM* Item;
std::vector<KIGFX::CONSTRUCTION_GEOM::DRAWABLE> Constructions;
};
// A single batch of construction items. Once batch contains all the items (and associated
// construction geometry) that should be shown for one point of interest.
// More than one batch may be shown on the screen at the same time.
using CONSTRUCTION_ITEM_BATCH = std::vector<CONSTRUCTION_ITEM>;
/**
* Add a batch of construction items to the helper.
*
* @param aBatch The batch of construction items to add.
* @param aIsPersistent If true, the batch is considered "persistent" and will always be shown
* (and it will replace any previous persistent batch).
* If false, the batch is temporary and may be pushed out by other batches.
*/
void ProposeConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch, bool aIsPersistent );
/**
* Cancel outstanding proposals for new geometry.
*/
void CancelProposal();
/**
* Check if all 'real' (non-null = constructed) the items in the batch are in the list of items
* currently 'involved' in an active construction.
*/
bool InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const;
// Get the list of additional geometry items that should be considered
void GetConstructionItems( std::vector<CONSTRUCTION_ITEM_BATCH>& aToExtend ) const;
bool HasActiveConstruction() const;
private:
void updateView();
struct PENDING_BATCH;
// An (external) construction helper view item, that this manager adds/removes
// construction objects to/from.
KIGFX::CONSTRUCTION_GEOM& m_constructionGeomPreview;
void acceptConstructionItems( PENDING_BATCH&& aAcceptedBatchHash );
CONSTRUCTION_VIEW_HANDLER& m_viewHandler;
// Within one "operation", there is one set of construction items that are
// "persistent", and are always shown. Usually the original item and any
@ -168,12 +205,64 @@ private:
// Set of all items for which construction geometry has been added
std::set<EDA_ITEM*> m_involvedItems;
std::vector<VECTOR2I> m_referenceOnlyPoints;
std::unique_ptr<ACTIVATION_HELPER<PENDING_BATCH>> m_activationHelper;
};
// If a snap point is "active", extra construction geometry is added to the helper
// extending from the snap point to the cursor.
OPT_VECTOR2I m_snapLineOrigin;
OPT_VECTOR2I m_snapLineEnd;
/**
* A SNAP_MANAGER glues together the snap line manager and construction manager.,
* along with some other state. It provides information for generating snap
* anchors based on this state, as well as keeping the state of visible
* construction geometry involved in that process.
*
* Probably only used by GRID_HELPERs, but it's neater to keep it separate,
* as there's quite a bit of state to manage.
*
* This is also where you may wish to add other 'virtual' snapping state,
* such as 'equal-space' snapping, etc.
*/
class SNAP_MANAGER : public CONSTRUCTION_VIEW_HANDLER
{
public:
using GFX_UPDATE_CALLBACK = std::function<void( bool aShowAnything )>;
SNAP_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper );
/**
* Set the callback to call when the construction geometry changes and a view may need updating.
*/
void SetUpdateCallback( GFX_UPDATE_CALLBACK aCallback ) { m_updateCallback = aCallback; }
SNAP_LINE_MANAGER& GetSnapLineManager() { return m_snapLineManager; }
CONSTRUCTION_MANAGER& GetConstructionManager() { return m_constructionManager; }
/**
* Set the reference-only points - these are points that are not snapped to, but can still
* be used for connection to the snap line.
*/
void SetReferenceOnlyPoints( std::vector<VECTOR2I> aPoints )
{
m_referenceOnlyPoints = std::move( aPoints );
}
const std::vector<VECTOR2I>& GetReferenceOnlyPoints() const { return m_referenceOnlyPoints; }
/**
* Get a list of all the active construction geometry, computed from
* the combined state of the snap line and construction manager.
*
* This can be combined with other external geometry to compute snap anchors.
*/
std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH> GetConstructionItems() const;
public:
void updateView() override;
GFX_UPDATE_CALLBACK m_updateCallback;
};
SNAP_LINE_MANAGER m_snapLineManager;
CONSTRUCTION_MANAGER m_constructionManager;
std::vector<VECTOR2I> m_referenceOnlyPoints;
};

View File

@ -203,7 +203,7 @@ protected:
protected:
void showConstructionGeometry( bool aShow );
CONSTRUCTION_MANAGER& getConstructionManager() { return m_constructionManager; }
SNAP_MANAGER& getSnapManager() { return m_snapManager; }
void updateSnapPoint( const TYPED_POINT2I& aPoint );
@ -236,10 +236,9 @@ private:
// (if any) on the canvas.
KIGFX::CONSTRUCTION_GEOM m_constructionGeomPreview;
// Construction manager - this is what manages the construction geometry
// and deals with updating the construction helper as well as keeping
// track of what geometry is "active" for construction purposes.
CONSTRUCTION_MANAGER m_constructionManager;
// Snap manager - this is what manages the construction geometry,
// snap lines, reference points, etc.
SNAP_MANAGER m_snapManager;
/// @brief VIEW_ITEM for visualising anchor points, if enabled
std::unique_ptr<KIGFX::ANCHOR_DEBUG> m_anchorDebug;

View File

@ -277,12 +277,12 @@ void PCB_GRID_HELPER::AddConstructionItems( std::vector<BOARD_ITEM*> aItems, boo
if( referenceOnlyPoints.size() )
{
getConstructionManager().SetReferenceOnlyPoints( std::move( referenceOnlyPoints ) );
getSnapManager().SetReferenceOnlyPoints( std::move( referenceOnlyPoints ) );
}
// Let the manager handle it
getConstructionManager().AddConstructionItems( std::move( constructionItemsBatch ),
aIsPersistent );
getSnapManager().GetConstructionManager().ProposeConstructionItems(
std::move( constructionItemsBatch ), aIsPersistent );
}
@ -535,28 +535,60 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
showConstructionGeometry( m_enableSnap );
CONSTRUCTION_MANAGER& constructionManager = getConstructionManager();
SNAP_MANAGER& snapManager = getSnapManager();
SNAP_LINE_MANAGER& snapLineManager = snapManager.GetSnapLineManager();
const auto ptIsReferenceOnly = [&]( const VECTOR2I& aPt )
{
const std::vector<VECTOR2I>& referenceOnlyPoints =
constructionManager.GetReferenceOnlyPoints();
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 = constructionManager.GetNearestSnapLinePoint(
OPT_VECTOR2I snapLineSnap = snapLineManager.GetNearestSnapLinePoint(
aOrigin, nearestGrid, snapDist, snapRange );
// We found a better snap point that the nearest one
if( snapLineSnap && m_skipPoint != *snapLineSnap )
{
constructionManager.SetSnapLineEnd( *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 );
@ -572,7 +604,7 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
}
}
// If there's a snap anchor within range, use it
// 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;
@ -583,41 +615,18 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
{
// We can set the snap line origin, but don't mess with the
// accepted snap point
constructionManager.SetSnapLineOrigin( nearest->pos );
snapLineManager.SetSnapLineOrigin( nearest->pos );
}
else
{
// Only 'intrinsic' points of items can trigger adding more construction items
// (so 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
// '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 )
{
// 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 : nearest->items )
{
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 );
proposeConstructionForItems( nearest->items );
}
const auto shouldAcceptAnchor = [&]( const ANCHOR& aAnchor )
@ -636,7 +645,8 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
// as some users will think it's fiddly; without 'activation', others will
// think the snaps are intrusive.
bool allRealAreInvolved =
constructionManager.InvolvesAllGivenRealItems( aAnchor.items );
snapManager.GetConstructionManager().InvolvesAllGivenRealItems(
aAnchor.items );
return allRealAreInvolved;
};
@ -645,13 +655,33 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
m_snapItem = *nearest;
// Set the snap line origin or end as needed
constructionManager.SetSnappedAnchor( m_snapItem->pos );
// Show the right snap point marker
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
for( BOARD_ITEM* item : visibleItems )
{
if( item->HitTest( aOrigin ) )
{
proposeConstructionForItems( { item } );
snapValid = true;
break;
}
}
}
}
// If we got here, we didn't snap to an anchor or snap line
@ -671,7 +701,7 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
// Clear the snap end, but keep the origin so touching another line
// doesn't kill a snap line
constructionManager.SetSnapLineEnd( std::nullopt );
snapLineManager.SetSnapLineEnd( std::nullopt );
return *nearestPointOnAnElement;
}
}
@ -680,7 +710,17 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
// Completely failed to find any snap point, so snap to the grid
m_snapItem = std::nullopt;
constructionManager.SetSnapLineEnd( std::nullopt );
if( !snapValid )
{
snapLineManager.ClearSnapLine();
snapManager.GetConstructionManager().CancelProposal();
}
else
{
snapLineManager.SetSnapLineEnd( std::nullopt );
}
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
return nearestGrid;
@ -926,7 +966,7 @@ void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
}
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH& batch :
getConstructionManager().GetConstructionItems() )
getSnapManager().GetConstructionItems() )
{
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& constructionItem : batch )
{
@ -967,11 +1007,11 @@ void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
if( computeIntersections )
{
for( size_t ii = 0; ii < intersectables.size(); ++ii )
for( std::size_t ii = 0; ii < intersectables.size(); ++ii )
{
const PCB_INTERSECTABLE& intersectableA = intersectables[ii];
for( size_t jj = ii + 1; jj < intersectables.size(); ++jj )
for( std::size_t jj = ii + 1; jj < intersectables.size(); ++jj )
{
const PCB_INTERSECTABLE& intersectableB = intersectables[jj];
@ -1109,7 +1149,7 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos
corners.Rotate( aPad->GetOrientation() );
corners.Move( aPad->ShapePos() );
for( size_t ii = 0; ii < corners.GetSegmentCount(); ++ii )
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 );