Files
kicad-source-mirror/eeschema/tools/sch_drag_net_collision.cpp
Seth Hillbrand 1dcea72404 Fix false positive connectivity warning when dragging wire along another wire
The collectDisconnectedMarkers function was incorrectly reporting
disconnections when a wire endpoint moved but remained connected to
another wire. For wires/buses, connections are valid anywhere along
the line, not just at endpoints. Added HitTest checks to verify if
the connection is still valid when the points differ.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/22035
2026-01-04 07:23:25 -08:00

699 lines
23 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* Copyright (C) 2025 VUT Brno, Faculty of Electrical Engineering and Communication
*
* 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 3
* 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/gpl-3.0.html
* or you may search the http://www.gnu.org website for the version 3 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "sch_drag_net_collision.h"
#include <trace_helpers.h>
#include <algorithm>
#include <limits>
#include <unordered_set>
#include <eda_item.h>
#include <sch_connection.h>
#include <sch_edit_frame.h>
#include <sch_item.h>
#include <sch_junction.h>
#include <sch_screen.h>
#include <sch_selection.h>
#include <sch_sheet_path.h>
#include <view/view.h>
#include <view/view_overlay.h>
#include <gal/color4d.h>
#include <layer_ids.h>
#include <settings/color_settings.h>
#include <eeschema_settings.h>
SCH_DRAG_NET_COLLISION_MONITOR::SCH_DRAG_NET_COLLISION_MONITOR( SCH_EDIT_FRAME* aFrame,
KIGFX::VIEW* aView ) :
m_frame( aFrame ),
m_view( aView ),
m_overlay(),
m_itemNetCodes(),
m_sheetPath(),
m_originalConnections(),
m_hasCollision( false )
{
}
SCH_DRAG_NET_COLLISION_MONITOR::~SCH_DRAG_NET_COLLISION_MONITOR()
{
Reset();
}
void SCH_DRAG_NET_COLLISION_MONITOR::Initialize( const SCH_SELECTION& aSelection )
{
wxLogTrace( traceSchDragNetCollision, "Initialize: Starting initialization" );
m_itemNetCodes.clear();
m_originalConnections.clear();
m_sheetPath = m_frame->GetCurrentSheet();
m_hasCollision = false;
EE_RTREE& items = m_frame->GetScreen()->Items();
wxLogTrace( traceSchDragNetCollision, "Initialize: Recording nets for %zu screen items",
items.size() );
for( SCH_ITEM* item : items )
recordItemNet( item );
wxLogTrace( traceSchDragNetCollision, "Initialize: Recording nets for %d selected items",
aSelection.GetSize() );
for( EDA_ITEM* edaItem : aSelection )
recordItemNet( static_cast<SCH_ITEM*>( edaItem ) );
recordOriginalConnections( aSelection );
wxLogTrace( traceSchDragNetCollision, "Initialize: Complete. Tracked %zu items with net codes",
m_itemNetCodes.size() );
}
bool SCH_DRAG_NET_COLLISION_MONITOR::Update( const std::vector<SCH_JUNCTION*>& aJunctions,
const SCH_SELECTION& aSelection,
std::span<const PREVIEW_NET_ASSIGNMENT> aPreviewAssignments )
{
wxLogTrace( traceSchDragNetCollision, "Update: Called with %zu junctions, %d selected items, %zu preview assignments",
aJunctions.size(), aSelection.GetSize(), aPreviewAssignments.size() );
std::unordered_map<const SCH_ITEM*, std::optional<int>> previewNetCodes;
previewNetCodes.reserve( aPreviewAssignments.size() );
for( const PREVIEW_NET_ASSIGNMENT& assignment : aPreviewAssignments )
{
if( !assignment.item )
continue;
wxLogTrace( traceSchDragNetCollision, "Update: Preview assignment - item %p, netCode %s",
assignment.item,
assignment.netCode.has_value() ? std::to_string( *assignment.netCode ).c_str() : "none" );
previewNetCodes[ assignment.item ] = assignment.netCode;
}
std::vector<COLLISION_MARKER> markers;
if( aJunctions.empty() )
{
wxLogTrace( traceSchDragNetCollision, "Update: No junctions to analyze" );
}
else
{
wxLogTrace( traceSchDragNetCollision, "Update: Analyzing %zu junctions", aJunctions.size() );
for( SCH_JUNCTION* junction : aJunctions )
{
if( auto marker = analyzeJunction( junction, aSelection, previewNetCodes ) )
{
wxLogTrace( traceSchDragNetCollision, "Update: Junction at (%d, %d) has collision",
marker->position.x, marker->position.y );
markers.push_back( *marker );
}
}
}
std::vector<DISCONNECTION_MARKER> disconnections = collectDisconnectedMarkers( aSelection );
if( markers.empty() && disconnections.empty() )
{
wxLogTrace( traceSchDragNetCollision, "Update: No collisions or disconnections detected" );
clearOverlay();
m_hasCollision = false;
return false;
}
wxLogTrace( traceSchDragNetCollision, "Update: Drawing %zu collision markers and %zu disconnection markers",
markers.size(), disconnections.size() );
ensureOverlay();
m_overlay->Clear();
COLOR4D baseColor( 1.0, 0.0, 0.0, 0.8 );
if( COLOR_SETTINGS* colorSettings = m_frame->GetColorSettings() )
{
COLOR4D themeColor = colorSettings->GetColor( LAYER_DRAG_NET_COLLISION );
if( themeColor != COLOR4D::UNSPECIFIED )
baseColor = themeColor;
}
double baseAlpha = baseColor.a;
if( baseAlpha <= 0.0 )
baseAlpha = 1.0;
double fillAlpha = std::clamp( baseAlpha * 0.35, 0.05, 1.0 );
double strokeAlpha = std::clamp( baseAlpha, 0.05, 1.0 );
m_overlay->SetIsFill( true );
m_overlay->SetFillColor( baseColor.WithAlpha( fillAlpha ) );
m_overlay->SetIsStroke( true );
m_overlay->SetStrokeColor( baseColor.WithAlpha( strokeAlpha ) );
int lineWidthPixels = 4;
if( EESCHEMA_SETTINGS* cfg = m_frame->eeconfig() )
lineWidthPixels = std::max( cfg->m_Selection.drag_net_collision_width, 1 );
double lineWidth = m_view->ToWorld( lineWidthPixels );
if( lineWidth <= 0.0 )
lineWidth = 1.0;
m_overlay->SetLineWidth( lineWidth );
for( const COLLISION_MARKER& marker : markers )
m_overlay->Circle( marker.position, marker.radius );
for( const DISCONNECTION_MARKER& marker : disconnections )
{
m_overlay->Circle( marker.pointA, marker.radius );
m_overlay->Circle( marker.pointB, marker.radius );
m_overlay->Line( VECTOR2D( marker.pointA ), VECTOR2D( marker.pointB ) );
}
m_view->Update( m_overlay.get() );
m_hasCollision = true;
return true;
}
void SCH_DRAG_NET_COLLISION_MONITOR::Reset()
{
clearOverlay();
m_itemNetCodes.clear();
m_originalConnections.clear();
m_hasCollision = false;
}
KICURSOR SCH_DRAG_NET_COLLISION_MONITOR::AdjustCursor( KICURSOR aBaseCursor ) const
{
if( m_hasCollision )
return KICURSOR::WARNING;
return aBaseCursor;
}
std::optional<int> SCH_DRAG_NET_COLLISION_MONITOR::GetNetCode( const SCH_ITEM* aItem ) const
{
if( !aItem )
return std::nullopt;
auto it = m_itemNetCodes.find( aItem );
if( it != m_itemNetCodes.end() )
return it->second;
if( SCH_CONNECTION* connection = aItem->Connection( &m_sheetPath ) )
{
if( connection->IsNet() && !connection->IsUnconnected() )
{
int netCode = connection->NetCode();
if( netCode > 0 )
return netCode;
}
}
return std::nullopt;
}
std::optional<SCH_DRAG_NET_COLLISION_MONITOR::COLLISION_MARKER> SCH_DRAG_NET_COLLISION_MONITOR::analyzeJunction(
SCH_JUNCTION* aJunction, const SCH_SELECTION& aSelection,
const std::unordered_map<const SCH_ITEM*, std::optional<int>>& aPreviewNetCodes ) const
{
if( !aJunction )
return std::nullopt;
VECTOR2I position = aJunction->GetPosition();
EE_RTREE& items = m_frame->GetScreen()->Items();
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Checking junction at (%d, %d)",
position.x, position.y );
std::unordered_set<int> allNetCodes;
std::unordered_set<int> movedNetCodes;
std::unordered_set<int> originalNetCodes;
std::unordered_set<int> movedOriginalNetCodes;
std::unordered_set<int> stationaryOriginalNetCodes;
auto accumulateNet = [&]( SCH_ITEM* item )
{
if( !item )
{
wxLogTrace( traceSchDragNetCollision, " accumulateNet: null item" );
return;
}
if( !item->IsConnectable() )
{
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) not connectable",
item, item->GetClass().c_str() );
return;
}
if( !item->IsConnected( position ) && !( item->IsType( { SCH_LINE_T } ) && item->HitTest( position ) ) )
{
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) not connected at (%d, %d)",
item, item->GetClass().c_str(), position.x, position.y );
return;
}
auto previewIt = aPreviewNetCodes.find( item );
auto originalIt = m_itemNetCodes.find( item );
std::optional<int> netCodeOpt;
std::optional<int> originalNetOpt;
if( originalIt != m_itemNetCodes.end() )
originalNetOpt = originalIt->second;
if( previewIt != aPreviewNetCodes.end() )
{
netCodeOpt = previewIt->second;
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) using preview net %s",
item, item->GetClass().c_str(),
netCodeOpt.has_value() ? std::to_string( *netCodeOpt ).c_str() : "none" );
}
else if( originalIt != m_itemNetCodes.end() )
{
netCodeOpt = originalIt->second;
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) using cached net %s",
item, item->GetClass().c_str(),
netCodeOpt.has_value() ? std::to_string( *netCodeOpt ).c_str() : "none" );
}
else
{
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) has no net code",
item, item->GetClass().c_str() );
}
bool isSelectionItem = item->IsSelected() || aSelection.Contains( item );
bool isMoved = ( previewIt != aPreviewNetCodes.end() ) || isSelectionItem;
if( !netCodeOpt )
{
if( originalNetOpt )
{
originalNetCodes.insert( *originalNetOpt );
if( isSelectionItem )
movedOriginalNetCodes.insert( *originalNetOpt );
else
stationaryOriginalNetCodes.insert( *originalNetOpt );
}
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) netCode is nullopt",
item, item->GetClass().c_str() );
return;
}
int netCode = *netCodeOpt;
allNetCodes.insert( netCode );
wxLogTrace( traceSchDragNetCollision, " accumulateNet: item %p (%s) net %d, moved=%s",
item, item->GetClass().c_str(), netCode, isMoved ? "yes" : "no" );
if( isMoved )
movedNetCodes.insert( netCode );
if( originalNetOpt )
{
originalNetCodes.insert( *originalNetOpt );
if( isSelectionItem )
movedOriginalNetCodes.insert( *originalNetOpt );
else
stationaryOriginalNetCodes.insert( *originalNetOpt );
}
};
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Checking items overlapping position" );
int candidateCount = 0;
for( SCH_ITEM* candidate : items.Overlapping( position ) )
{
candidateCount++;
accumulateNet( candidate );
}
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Checked %d overlapping items", candidateCount );
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Checking %d selected items", aSelection.GetSize() );
for( EDA_ITEM* selected : aSelection )
accumulateNet( static_cast<SCH_ITEM*>( selected ) );
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Found %zu unique nets, %zu moved nets",
allNetCodes.size(), movedNetCodes.size() );
if( !movedNetCodes.empty() )
{
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Moved nets:" );
for( int netCode : movedNetCodes )
wxLogTrace( traceSchDragNetCollision, " - Net %d", netCode );
}
wxLogTrace( traceSchDragNetCollision,
"analyzeJunction: Original nets=%zu, moved originals=%zu, stationary originals=%zu",
originalNetCodes.size(), movedOriginalNetCodes.size(), stationaryOriginalNetCodes.size() );
if( !movedOriginalNetCodes.empty() )
{
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Moved original nets:" );
for( int netCode : movedOriginalNetCodes )
wxLogTrace( traceSchDragNetCollision, " - Net %d", netCode );
}
if( !stationaryOriginalNetCodes.empty() )
{
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: Stationary original nets:" );
for( int netCode : stationaryOriginalNetCodes )
wxLogTrace( traceSchDragNetCollision, " - Net %d", netCode );
}
if( allNetCodes.size() >= 2 )
{
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: All nets at junction:" );
for( int netCode : allNetCodes )
wxLogTrace( traceSchDragNetCollision, " - Net %d", netCode );
}
bool previewCollision = !movedNetCodes.empty() && allNetCodes.size() >= 2;
bool originalCollision = false;
if( !movedOriginalNetCodes.empty() && !stationaryOriginalNetCodes.empty() )
{
for( int movedNet : movedOriginalNetCodes )
{
for( int stationaryNet : stationaryOriginalNetCodes )
{
if( movedNet != stationaryNet )
{
originalCollision = true;
break;
}
}
if( originalCollision )
break;
}
}
if( !previewCollision && !originalCollision )
{
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: No collision (movedNets=%zu, allNets=%zu)",
movedNetCodes.size(), allNetCodes.size() );
return std::nullopt;
}
if( originalCollision && !previewCollision )
{
wxLogTrace( traceSchDragNetCollision,
"analyzeJunction: Original net mismatch detected under moved endpoints" );
}
COLLISION_MARKER marker;
marker.position = position;
double base = static_cast<double>( aJunction->GetEffectiveDiameter() );
marker.radius = std::max( base * 1.5, 800.0 );
wxLogTrace( traceSchDragNetCollision, "analyzeJunction: COLLISION DETECTED at (%d, %d) with radius %.1f",
position.x, position.y, marker.radius );
return marker;
}
void SCH_DRAG_NET_COLLISION_MONITOR::recordItemNet( SCH_ITEM* aItem )
{
if( !aItem )
return;
if( !aItem->IsConnectable() )
return;
if( m_itemNetCodes.find( aItem ) != m_itemNetCodes.end() )
return;
if( SCH_CONNECTION* connection = aItem->Connection( &m_sheetPath ) )
{
if( connection->IsNet() && !connection->IsUnconnected() )
{
int netCode = connection->NetCode();
if( netCode > 0 )
{
wxLogTrace( traceSchDragNetCollision, "recordItemNet: Item %p (%s) at (%d, %d) -> net %d (%s)",
aItem, aItem->GetClass().c_str(),
aItem->GetPosition().x, aItem->GetPosition().y,
netCode, connection->Name().c_str() );
m_itemNetCodes.emplace( aItem, netCode );
}
else
{
wxLogTrace( traceSchDragNetCollision, "recordItemNet: Item %p (%s) has invalid netCode %d",
aItem, aItem->GetClass().c_str(), netCode );
m_itemNetCodes.emplace( aItem, std::nullopt );
}
}
else
{
wxLogTrace( traceSchDragNetCollision, "recordItemNet: Item %p (%s) connection not a net or unconnected",
aItem, aItem->GetClass().c_str() );
m_itemNetCodes.emplace( aItem, std::nullopt );
}
}
else
{
wxLogTrace( traceSchDragNetCollision, "recordItemNet: Item %p (%s) has no connection",
aItem, aItem->GetClass().c_str() );
m_itemNetCodes.emplace( aItem, std::nullopt );
}
}
void SCH_DRAG_NET_COLLISION_MONITOR::recordOriginalConnections( const SCH_SELECTION& aSelection )
{
wxLogTrace( traceSchDragNetCollision, "recordOriginalConnections: Recording connections for %d items",
aSelection.GetSize() );
// Don't record original connections for new or pasted items (duplicates, pastes)
// as they weren't previously connected to anything
bool hasNewOrPastedItems = false;
for( EDA_ITEM* edaItem : aSelection )
{
if( edaItem->IsNew() || ( edaItem->GetFlags() & IS_PASTED ) )
{
hasNewOrPastedItems = true;
break;
}
}
if( hasNewOrPastedItems )
{
wxLogTrace( traceSchDragNetCollision,
"recordOriginalConnections: Skipping - selection contains new or pasted items" );
return;
}
EE_RTREE& items = m_frame->GetScreen()->Items();
for( EDA_ITEM* edaItem : aSelection )
{
SCH_ITEM* item = static_cast<SCH_ITEM*>( edaItem );
if( !item || !item->IsConnectable() )
continue;
std::vector<VECTOR2I> points = item->GetConnectionPoints();
for( size_t index = 0; index < points.size(); ++index )
{
const VECTOR2I& point = points[index];
for( SCH_ITEM* candidate : items.Overlapping( point ) )
{
if( candidate == item || !candidate->IsConnectable() )
continue;
if( !candidate->CanConnect( item ) )
continue;
if( !candidate->IsConnected( point )
&& !( candidate->IsType( { SCH_LINE_T } ) && candidate->HitTest( point ) ) )
{
continue;
}
std::vector<VECTOR2I> candidatePoints = candidate->GetConnectionPoints();
size_t candidateIndex = std::numeric_limits<size_t>::max();
for( size_t candidatePos = 0; candidatePos < candidatePoints.size(); ++candidatePos )
{
if( candidatePoints[candidatePos] == point )
{
candidateIndex = candidatePos;
break;
}
}
if( candidateIndex == std::numeric_limits<size_t>::max() )
continue;
SCH_ITEM* firstItem = item;
size_t firstIndex = index;
SCH_ITEM* secondItem = candidate;
size_t secondIndex = candidateIndex;
if( secondItem < firstItem || ( secondItem == firstItem && secondIndex < firstIndex ) )
{
std::swap( firstItem, secondItem );
std::swap( firstIndex, secondIndex );
}
if( firstItem == secondItem )
continue;
bool firstSelected = firstItem->IsSelected() || aSelection.Contains( firstItem );
bool secondSelected = secondItem->IsSelected() || aSelection.Contains( secondItem );
if( !firstSelected && !secondSelected )
continue;
auto existing = std::find_if( m_originalConnections.begin(), m_originalConnections.end(),
[&]( const ORIGINAL_CONNECTION& connection )
{
return connection.itemA == firstItem && connection.indexA == firstIndex
&& connection.itemB == secondItem && connection.indexB == secondIndex;
} );
if( existing != m_originalConnections.end() )
continue;
m_originalConnections.push_back( { firstItem, firstIndex, secondItem, secondIndex } );
}
}
}
wxLogTrace( traceSchDragNetCollision, "recordOriginalConnections: Tracked %zu connections",
m_originalConnections.size() );
}
std::vector<SCH_DRAG_NET_COLLISION_MONITOR::DISCONNECTION_MARKER>
SCH_DRAG_NET_COLLISION_MONITOR::collectDisconnectedMarkers( const SCH_SELECTION& aSelection ) const
{
std::vector<DISCONNECTION_MARKER> markers;
for( const ORIGINAL_CONNECTION& connection : m_originalConnections )
{
SCH_ITEM* itemA = connection.itemA;
SCH_ITEM* itemB = connection.itemB;
if( !itemA || !itemB )
continue;
if( !itemA->IsConnectable() || !itemB->IsConnectable() )
continue;
std::vector<VECTOR2I> pointsA = itemA->GetConnectionPoints();
std::vector<VECTOR2I> pointsB = itemB->GetConnectionPoints();
if( connection.indexA >= pointsA.size() || connection.indexB >= pointsB.size() )
continue;
VECTOR2I pointA = pointsA[ connection.indexA ];
VECTOR2I pointB = pointsB[ connection.indexB ];
// Check if the connection is still valid. Points match exactly.
bool stillConnected = ( pointA == pointB );
// For lines, connection is valid if the point is anywhere on the line
if( !stillConnected && itemB->IsType( { SCH_LINE_T } ) && itemB->HitTest( pointA, 0 ) )
stillConnected = true;
if( !stillConnected && itemA->IsType( { SCH_LINE_T } ) && itemA->HitTest( pointB, 0 ) )
stillConnected = true;
if( stillConnected )
continue;
bool relevant = itemA->IsSelected() || aSelection.Contains( itemA )
|| itemB->IsSelected() || aSelection.Contains( itemB );
if( !relevant )
continue;
double radius = std::max( { 800.0,
static_cast<double>( itemA->GetPenWidth() ),
static_cast<double>( itemB->GetPenWidth() ) } );
DISCONNECTION_MARKER marker;
marker.pointA = pointA;
marker.pointB = pointB;
marker.radius = radius;
markers.push_back( marker );
}
if( !markers.empty() )
{
wxLogTrace( traceSchDragNetCollision,
"collectDisconnectedMarkers: Identified %zu disconnections", markers.size() );
}
return markers;
}
void SCH_DRAG_NET_COLLISION_MONITOR::ensureOverlay()
{
if( !m_overlay )
m_overlay = m_view->MakeOverlay();
}
void SCH_DRAG_NET_COLLISION_MONITOR::clearOverlay() const
{
if( m_overlay )
{
m_overlay->Clear();
m_view->Update( m_overlay.get() );
}
}