Files
Seth Hillbrand 5d563a9870 Fix sheet pin alignment calculation in "Align Items to Grid"
Refactored the alignment logic into a separate testable function
AlignSchematicItemsToGrid() with callbacks, enabling proper QA testing

Pins now move along the sheet edge to maintain connection to wires and
wires extend/contract to maintain connection to pins

Fixes https://gitlab.com/kicad/code/kicad/-/issues/22864
2026-01-28 09:17:31 -08:00

323 lines
12 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.
*
* 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 "sch_item_alignment.h"
#include <sch_item.h>
#include <sch_line.h>
#include <sch_sheet.h>
#include <sch_sheet_pin.h>
#include <sch_screen.h>
#include <tools/ee_grid_helper.h>
#include <map>
#include <set>
void MoveSchematicItem( EDA_ITEM* aItem, const VECTOR2I& aDelta )
{
switch( aItem->Type() )
{
case SCH_LINE_T:
{
SCH_LINE* line = static_cast<SCH_LINE*>( aItem );
if( aItem->HasFlag( STARTPOINT ) )
line->MoveStart( aDelta );
if( aItem->HasFlag( ENDPOINT ) )
line->MoveEnd( aDelta );
break;
}
case SCH_SHEET_PIN_T:
{
SCH_SHEET_PIN* pin = static_cast<SCH_SHEET_PIN*>( aItem );
pin->SetStoredPos( pin->GetStoredPos() + aDelta );
pin->ConstrainOnEdge( pin->GetStoredPos(), true );
break;
}
default:
static_cast<SCH_ITEM*>( aItem )->Move( aDelta );
break;
}
}
void AlignSchematicItemsToGrid( SCH_SCREEN* aScreen,
const std::vector<EDA_ITEM*>& aItems,
EE_GRID_HELPER& aGrid,
GRID_HELPER_GRIDS aSelectionGrid,
const SCH_ALIGNMENT_CALLBACKS& aCallbacks )
{
// When both sheets are selected, wires drag with pins. When only one sheet is selected,
// pins stay at the original Y so wires stretch horizontally without skewing. When wires
// are also selected, they align independently and pins follow their endpoints.
//
// We record original wire endpoints and query the R-tree before moving each sheet, then
// update storedPos after the move to prevent double-movement.
struct WireEndpoint
{
SCH_LINE* wire;
int endpointFlag; // STARTPOINT or ENDPOINT
};
std::map<VECTOR2I, std::vector<WireEndpoint>> pinPosToWires;
// Wires may move while processing earlier sheets, so we save their original positions.
struct OriginalWireEndpoints
{
VECTOR2I start;
VECTOR2I end;
};
std::map<SCH_LINE*, OriginalWireEndpoints> originalWireEndpoints;
for( SCH_ITEM* item : aScreen->Items().OfType( SCH_LINE_T ) )
{
SCH_LINE* line = static_cast<SCH_LINE*>( item );
originalWireEndpoints[line] = { line->GetStartPoint(), line->GetEndPoint() };
}
std::set<VECTOR2I> selectedSheetPinPositions;
for( EDA_ITEM* item : aItems )
{
if( item->Type() == SCH_SHEET_T )
{
SCH_SHEET* sheet = static_cast<SCH_SHEET*>( item );
for( SCH_SHEET_PIN* pin : sheet->GetPins() )
selectedSheetPinPositions.insert( pin->GetPosition() );
}
}
for( EDA_ITEM* item : aItems )
{
if( item->Type() == SCH_LINE_T )
{
SCH_LINE* line = static_cast<SCH_LINE*>( item );
if( selectedSheetPinPositions.count( line->GetStartPoint() ) )
pinPosToWires[line->GetStartPoint()].push_back( { line, STARTPOINT } );
if( selectedSheetPinPositions.count( line->GetEndPoint() ) )
pinPosToWires[line->GetEndPoint()].push_back( { line, ENDPOINT } );
}
}
for( EDA_ITEM* item : aItems )
{
if( item->Type() == SCH_LINE_T )
{
SCH_LINE* line = static_cast<SCH_LINE*>( item );
std::vector<int> flags{ STARTPOINT, ENDPOINT };
std::vector<VECTOR2I> pts{ line->GetStartPoint(), line->GetEndPoint() };
for( int ii = 0; ii < 2; ++ii )
{
EDA_ITEMS drag_items{ item };
line->ClearFlags();
line->SetFlags( SELECTED );
line->SetFlags( flags[ii] );
if( aCallbacks.m_getConnectedDragItems )
aCallbacks.m_getConnectedDragItems( line, pts[ii], drag_items );
std::set<EDA_ITEM*> unique_items( drag_items.begin(), drag_items.end() );
VECTOR2I delta = aGrid.AlignGrid( pts[ii], aSelectionGrid ) - pts[ii];
if( delta != VECTOR2I( 0, 0 ) )
{
for( EDA_ITEM* dragItem : unique_items )
{
if( dragItem->GetParent() && dragItem->GetParent()->IsSelected() )
continue;
aCallbacks.m_doMoveItem( dragItem, delta );
}
}
}
}
else if( item->Type() == SCH_FIELD_T || item->Type() == SCH_TEXT_T )
{
VECTOR2I delta = aGrid.AlignGrid( item->GetPosition(), aSelectionGrid )
- item->GetPosition();
if( delta != VECTOR2I( 0, 0 ) )
aCallbacks.m_doMoveItem( item, delta );
}
else if( item->Type() == SCH_SHEET_T )
{
SCH_SHEET* sheet = static_cast<SCH_SHEET*>( item );
VECTOR2I topLeft = sheet->GetPosition();
VECTOR2I bottomRight = topLeft + sheet->GetSize();
VECTOR2I tl_delta = aGrid.AlignGrid( topLeft, aSelectionGrid ) - topLeft;
VECTOR2I br_delta = aGrid.AlignGrid( bottomRight, aSelectionGrid ) - bottomRight;
// Query connected items before moving the sheet since R-tree needs original positions.
std::map<SCH_SHEET_PIN*, VECTOR2I> originalPinPositions;
std::map<SCH_SHEET_PIN*, EDA_ITEMS> pinDragItems;
for( SCH_SHEET_PIN* pin : sheet->GetPins() )
{
originalPinPositions[pin] = pin->GetPosition();
if( aCallbacks.m_getConnectedDragItems )
aCallbacks.m_getConnectedDragItems( pin, pin->GetPosition(), pinDragItems[pin] );
}
if( tl_delta != VECTOR2I( 0, 0 ) || br_delta != VECTOR2I( 0, 0 ) )
{
aCallbacks.m_doMoveItem( sheet, tl_delta );
VECTOR2I newSize = (VECTOR2I) sheet->GetSize() - tl_delta + br_delta;
sheet->SetSize( VECTOR2I( newSize.x, newSize.y ) );
if( aCallbacks.m_updateItem )
aCallbacks.m_updateItem( sheet );
}
for( SCH_SHEET_PIN* pin : sheet->GetPins() )
{
pin->SetStoredPos( pin->GetPosition() );
}
for( SCH_SHEET_PIN* pin : sheet->GetPins() )
{
VECTOR2I originalPos = originalPinPositions[pin];
VECTOR2I pinPos = pin->GetPosition();
VECTOR2I targetPos;
bool canDragWires = true;
// If the pin was connected to a selected wire, follow that wire's aligned position.
auto it = pinPosToWires.find( originalPos );
if( it != pinPosToWires.end() && !it->second.empty() )
{
WireEndpoint& we = it->second[0];
if( we.endpointFlag == STARTPOINT )
targetPos = we.wire->GetStartPoint();
else
targetPos = we.wire->GetEndPoint();
}
else
{
// Check if any connected wire has its other end at an unselected item.
for( EDA_ITEM* dragItem : pinDragItems[pin] )
{
if( dragItem->Type() != SCH_LINE_T )
continue;
SCH_LINE* wire = static_cast<SCH_LINE*>( dragItem );
auto origIt = originalWireEndpoints.find( wire );
if( origIt == originalWireEndpoints.end() )
continue;
VECTOR2I otherEnd = ( origIt->second.start == originalPos )
? origIt->second.end
: origIt->second.start;
if( selectedSheetPinPositions.find( otherEnd )
== selectedSheetPinPositions.end() )
{
canDragWires = false;
break;
}
}
if( !canDragWires && !pinDragItems[pin].empty() )
{
// Keep pin at original Y so the wire stretches horizontally without skewing.
targetPos = VECTOR2I( pinPos.x, originalPos.y );
}
else
{
targetPos = aGrid.AlignGrid( pinPos, aSelectionGrid );
}
}
VECTOR2I delta = targetPos - pinPos;
VECTOR2I totalDelta = targetPos - originalPos;
if( delta != VECTOR2I( 0, 0 ) )
aCallbacks.m_doMoveItem( pin, delta );
for( EDA_ITEM* dragItem : pinDragItems[pin] )
{
if( dragItem->GetParent() && dragItem->GetParent()->IsSelected() )
continue;
if( totalDelta != VECTOR2I( 0, 0 ) )
aCallbacks.m_doMoveItem( dragItem, totalDelta );
}
}
}
else
{
SCH_ITEM* schItem = static_cast<SCH_ITEM*>( item );
std::vector<VECTOR2I> connections = schItem->GetConnectionPoints();
EDA_ITEMS drag_items;
if( aCallbacks.m_getConnectedDragItems )
{
for( const VECTOR2I& point : connections )
aCallbacks.m_getConnectedDragItems( schItem, point, drag_items );
}
std::map<VECTOR2I, int> shifts;
VECTOR2I most_common( 0, 0 );
int max_count = 0;
for( const VECTOR2I& conn : connections )
{
VECTOR2I gridpt = aGrid.AlignGrid( conn, aSelectionGrid ) - conn;
shifts[gridpt]++;
if( shifts[gridpt] > max_count )
{
most_common = gridpt;
max_count = shifts[most_common];
}
}
if( most_common != VECTOR2I( 0, 0 ) )
{
aCallbacks.m_doMoveItem( item, most_common );
for( EDA_ITEM* dragItem : drag_items )
{
if( dragItem->GetParent() && dragItem->GetParent()->IsSelected() )
continue;
aCallbacks.m_doMoveItem( dragItem, most_common );
}
}
}
}
}