Compare commits

...

15 Commits

Author SHA1 Message Date
Mike Williams
9f414f11dc Merge branch 'unconstrained_swapping' into 'master'
Unconstrained Pin Net / Gate Net Swapping

Closes #1950

See merge request kicad/code/kicad!2306
2025-09-11 10:48:39 -04:00
Jeff Young
18b56539a6 Keep Board Setup in front when called from DRC dialog. 2025-09-11 15:47:13 +01:00
Jeff Young
9b006c4f3b Formatting. 2025-09-11 15:47:13 +01:00
Jeff Young
8035a66152 Flag non-compiling rule conditions when running DRC.
Also, clear custom rules after an error before
trying to reload just implicit rules.
2025-09-11 15:47:13 +01:00
jean-pierre charras
45166bf5c3 Gerbview: fix broken behavior for deprecated command IPPOS and IPNEG
Fixes https://gitlab.com/kicad/code/kicad/-/issues/21715
2025-09-11 14:30:50 +02:00
Jeff Young
6ab6283e2e LIBEVAL::CONTEXT manages its own local VALUEs.
Don't use std::unique_ptr as we'll just free the
value right after storing it.

Also, don't try to execute a non-existent function.

Fixes https://gitlab.com/kicad/code/kicad/-/issues/21697
2025-09-11 12:49:41 +01:00
Seth Hillbrand
fc7d91214d Make pasting in lib tables easier
You generally copy/paste whole rows in lib tables, so make this workflow
easier.  Allows pasting rows as new data.  Prevent overwriting existing
data and don't force pasting from the first column
2025-09-11 02:30:49 -07:00
Seth Hillbrand
dcbadb5857 Allow drag-drop for schematic elements
Dragging screen elements over a subsheet allows moving elements into a
subsheet
2025-09-11 02:16:47 -07:00
jean-pierre charras
3b97804cb6 DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR: add missing layers to always allowed list 2025-09-11 11:07:16 +02:00
Mark Roszko
e72def55a9 Remove moronic pybind forcing expectation of python release builds 2025-09-11 07:16:52 +00:00
Mike Williams
76e4b4aca4 PCB: add gate swapping
Fixes: https://gitlab.com/kicad/code/kicad/-/issues/1950
2025-09-10 08:35:13 -04:00
Mike Williams
a391e953e7 PCB: import from netlist, save, and load unit info from footprints
For upcoming gate swap feature
2025-09-09 13:20:19 -04:00
Mike Williams
7e448c404a netlist: export/import symbol unit information to footprints
For upcoming gate swapping feature
2025-09-09 12:15:33 -04:00
Mike Williams
1418b03c5a SCH: pin net label swap
Allows the user to swap the label associated with a pin and other pin(s).

Will only allow swapping the label when the pin contains just a label
and nothing else (except wires).
2025-09-08 14:55:25 -04:00
Mike Williams
a7e004bd12 PCB: pad net swap
Allows the user to swap the net associated with a pad and other pad(s).

Will propagate net changes to connected non-zone items, with a
dialog confirming how to handle changes that propogate to unselected
pins.
2025-09-08 14:55:19 -04:00
40 changed files with 1585 additions and 48 deletions

View File

@ -19,11 +19,23 @@
#include "lib_table_grid_tricks.h"
#include "lib_table_grid.h"
#include <wx/clipbrd.h>
#include <wx/log.h>
LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid ) :
GRID_TRICKS( aGrid )
{
m_grid->Disconnect( wxEVT_CHAR_HOOK );
m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}
LIB_TABLE_GRID_TRICKS::LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid,
std::function<void( wxCommandEvent& )> aAddHandler ) :
GRID_TRICKS( aGrid, aAddHandler )
{
m_grid->Disconnect( wxEVT_CHAR_HOOK );
m_grid->Connect( wxEVT_CHAR_HOOK, wxCharEventHandler( LIB_TABLE_GRID_TRICKS::onCharHook ), nullptr, this );
}
@ -134,6 +146,61 @@ void LIB_TABLE_GRID_TRICKS::doPopupSelection( wxCommandEvent& event )
GRID_TRICKS::doPopupSelection( event );
}
}
void LIB_TABLE_GRID_TRICKS::onCharHook( wxKeyEvent& ev )
{
if( ev.GetModifiers() == wxMOD_CONTROL && ev.GetKeyCode() == 'V' && m_grid->IsCellEditControlShown() )
{
wxLogNull doNotLog;
if( wxTheClipboard->Open() )
{
if( wxTheClipboard->IsSupported( wxDF_TEXT ) || wxTheClipboard->IsSupported( wxDF_UNICODETEXT ) )
{
wxTextDataObject data;
wxTheClipboard->GetData( data );
wxString text = data.GetText();
if( !text.Contains( '\t' ) && text.Contains( ',' ) )
text.Replace( ',', '\t' );
if( text.Contains( '\t' ) || text.Contains( '\n' ) || text.Contains( '\r' ) )
{
m_grid->CancelPendingChanges();
int row = m_grid->GetGridCursorRow();
// Check if the current row already has data (has a nickname)
wxGridTableBase* table = m_grid->GetTable();
if( table && row >= 0 && row < table->GetNumberRows() )
{
// Check if the row has a nickname (indicating it has existing data)
wxString nickname = table->GetValue( row, COL_NICKNAME );
if( !nickname.IsEmpty() )
{
// Row already has data, don't allow pasting over it
wxTheClipboard->Close();
wxBell(); // Provide audio feedback
return;
}
}
m_grid->ClearSelection();
m_grid->SelectRow( row );
m_grid->SetGridCursor( row, 0 );
getSelectedArea();
paste_text( text );
wxTheClipboard->Close();
m_grid->ForceRefresh();
return;
}
}
wxTheClipboard->Close();
}
}
GRID_TRICKS::onCharHook( ev );
}
bool LIB_TABLE_GRID_TRICKS::handleDoubleClick( wxGridEvent& aEvent )

View File

@ -1160,7 +1160,9 @@ void UOP::Exec( CONTEXT* ctx )
return;
case TR_OP_METHOD_CALL:
m_func( ctx, m_ref.get() );
if( m_func )
m_func( ctx, m_ref.get() );
return;
default:
@ -1321,9 +1323,8 @@ VALUE* UCODE::Run( CONTEXT* ctx )
}
catch(...)
{
// rules which fail outright should not be fired
std::unique_ptr<VALUE> temp_false = std::make_unique<VALUE>( 0 );
return ctx->StoreValue( temp_false.get() );
// rules which fail outright should not be fired; return 0/false
return ctx->StoreValue( new VALUE( 0 ) );
}
if( ctx->SP() == 1 )
@ -1339,8 +1340,7 @@ VALUE* UCODE::Run( CONTEXT* ctx )
wxASSERT( ctx->SP() == 1 );
// non-well-formed rules should not be fired on a release build
std::unique_ptr<VALUE> temp_false = std::make_unique<VALUE>( 0 );
return ctx->StoreValue( temp_false.get() );
return ctx->StoreValue( new VALUE( 0 ) );
}
}

View File

@ -51,3 +51,5 @@ value
version
aliases
alias
unit
units

View File

@ -370,6 +370,10 @@ true
tstamp
type
units
unit
pins
pin
num
units_format
unlocked
user

View File

@ -38,6 +38,7 @@
#include <lib_table_grid.h>
#include <wildcards_and_files_ext.h>
#include <env_paths.h>
#include <functional>
#include <eeschema_id.h>
#include <symbol_edit_frame.h>
#include <symbol_viewer_frame.h>
@ -153,6 +154,13 @@ public:
{
}
SYMBOL_GRID_TRICKS( DIALOG_EDIT_LIBRARY_TABLES* aParent, WX_GRID* aGrid,
std::function<void( wxCommandEvent& )> aAddHandler ) :
LIB_TABLE_GRID_TRICKS( aGrid, aAddHandler ),
m_dialog( aParent )
{
}
protected:
DIALOG_EDIT_LIBRARY_TABLES* m_dialog;
@ -224,8 +232,21 @@ protected:
}
else
{
// paste spreadsheet formatted text.
GRID_TRICKS::paste_text( cb_text );
wxString text = cb_text;
if( !text.Contains( '\t' ) && text.Contains( ',' ) )
text.Replace( ',', '\t' );
if( text.Contains( '\t' ) )
{
int row = m_grid->GetGridCursorRow();
m_grid->ClearSelection();
m_grid->SelectRow( row );
m_grid->SetGridCursor( row, 0 );
getSelectedArea();
}
GRID_TRICKS::paste_text( text );
m_grid->AutoSizeColumns( false );
}
@ -250,7 +271,8 @@ void PANEL_SYM_LIB_TABLE::setupGrid( WX_GRID* aGrid )
};
// add Cut, Copy, and Paste to wxGrids
aGrid->PushEventHandler( new SYMBOL_GRID_TRICKS( m_parent, aGrid ) );
aGrid->PushEventHandler( new SYMBOL_GRID_TRICKS( m_parent, aGrid,
[this]( wxCommandEvent& event ) { appendRowHandler( event ); } ) );
aGrid->SetSelectionMode( wxGrid::wxGridSelectRows );

View File

@ -481,6 +481,80 @@ XNODE* NETLIST_EXPORTER_XML::makeSymbols( unsigned aCtl )
// Output the primary UUID
uuid = symbol->m_Uuid.AsString();
xunits->AddChild( new XNODE( wxXML_TEXT_NODE, wxEmptyString, uuid ) );
// Emit unit information (per-unit name and pins) after tstamps
XNODE* xunitInfo;
xcomp->AddChild( xunitInfo = node( wxT( "units" ) ) );
// Emit all units defined by the library symbol, independent of placement
const std::unique_ptr<LIB_SYMBOL>& libSym = symbol->GetLibSymbolRef();
if( libSym )
{
int unitCount = std::max( libSym->GetUnitCount(), 1 );
for( int unitIdx = 1; unitIdx <= unitCount; ++unitIdx )
{
wxString unitName = libSym->GetUnitDisplayName( unitIdx, false );
XNODE* xunit;
xunitInfo->AddChild( xunit = node( wxT( "unit" ) ) );
xunit->AddAttribute( wxT( "name" ), unitName );
XNODE* xpins;
xunit->AddChild( xpins = node( wxT( "pins" ) ) );
// Gather all graphical pins for this unit across body styles
std::vector<SCH_PIN*> pinList = libSym->GetGraphicalPins( unitIdx, 0 );
// Sort by X then Y to establish a stable, spatial order for matching pins
// in the PCB editor when swapping gates
std::sort( pinList.begin(), pinList.end(),
[]( SCH_PIN* a, SCH_PIN* b )
{
auto pa = a->GetPosition();
auto pb = b->GetPosition();
if( pa.x != pb.x )
return pa.x < pb.x;
return pa.y < pb.y;
} );
// Emit pins in this spatial order, deduping by number within the unit only
std::unordered_set<wxString> seen;
for( SCH_PIN* basePin : pinList )
{
bool stackedValid = false;
std::vector<wxString> expandedNums = basePin->GetStackedPinNumbers( &stackedValid );
if( stackedValid && !expandedNums.empty() )
{
for( const wxString& num : expandedNums )
{
if( seen.insert( num ).second )
{
XNODE* xpin;
xpins->AddChild( xpin = node( wxT( "pin" ) ) );
xpin->AddAttribute( wxT( "num" ), num );
}
}
}
else
{
wxString num = basePin->GetShownNumber();
if( seen.insert( num ).second )
{
XNODE* xpin;
xpins->AddChild( xpin = node( wxT( "pin" ) ) );
xpin->AddAttribute( wxT( "num" ), num );
}
}
}
}
}
}
}

View File

@ -770,6 +770,12 @@ TOOL_ACTION SCH_ACTIONS::swap( TOOL_ACTION_ARGS()
.Tooltip( _( "Swap positions of selected items" ) )
.Icon( BITMAPS::swap ) );
TOOL_ACTION SCH_ACTIONS::swapPinLabels( TOOL_ACTION_ARGS()
.Name( "eeschema.InteractiveEdit.swapPinLabels" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Swap Pin Labels" ) )
.Tooltip( _( "Swap the labels attached to selected pins" ) ) );
TOOL_ACTION SCH_ACTIONS::properties( TOOL_ACTION_ARGS()
.Name( "eeschema.InteractiveEdit.properties" )
.Scope( AS_GLOBAL )

View File

@ -122,6 +122,7 @@ public:
static TOOL_ACTION mirrorV;
static TOOL_ACTION mirrorH;
static TOOL_ACTION swap;
static TOOL_ACTION swapPinLabels;
static TOOL_ACTION properties;
static TOOL_ACTION editReference;
static TOOL_ACTION editValue;

View File

@ -31,6 +31,7 @@
#include <tools/sch_move_tool.h>
#include <tools/sch_drawing_tools.h>
#include <confirm.h>
#include <connection_graph.h>
#include <sch_actions.h>
#include <sch_tool_utils.h>
#include <increment.h>
@ -40,9 +41,11 @@
#include <sch_bus_entry.h>
#include <sch_commit.h>
#include <sch_group.h>
#include <sch_label.h>
#include <sch_junction.h>
#include <sch_marker.h>
#include <sch_rule_area.h>
#include <sch_pin.h>
#include <sch_sheet_pin.h>
#include <sch_textbox.h>
#include <sch_table.h>
@ -1516,6 +1519,104 @@ int SCH_EDIT_TOOL::Swap( const TOOL_EVENT& aEvent )
}
int SCH_EDIT_TOOL::SwapPinLabels( const TOOL_EVENT& aEvent )
{
SCH_SELECTION& selection = m_selectionTool->RequestSelection( { SCH_PIN_T } );
std::vector<EDA_ITEM*> orderedPins = selection.GetItemsSortedBySelectionOrder();
if( orderedPins.size() < 2 )
return 0;
CONNECTION_GRAPH* connectionGraph = m_frame->Schematic().ConnectionGraph();
const SCH_SHEET_PATH& sheetPath = m_frame->GetCurrentSheet();
auto findSingleNetLabel = [&]( SCH_PIN* aPin ) -> SCH_LABEL_BASE*
{
CONNECTION_SUBGRAPH* sg = connectionGraph->GetSubgraphForItem( aPin );
if( !sg )
return nullptr;
const std::set<SCH_ITEM*>& items = sg->GetItems();
size_t pinCount = 0;
SCH_LABEL_BASE* label = nullptr;
for( SCH_ITEM* it : items )
{
if( it->Type() == SCH_PIN_T )
pinCount++;
switch( it->Type() )
{
case SCH_LABEL_T:
case SCH_GLOBAL_LABEL_T:
case SCH_HIER_LABEL_T:
{
SCH_CONNECTION* conn = it->Connection( &sheetPath );
if( conn && conn->IsNet() )
{
if( label )
return nullptr; // more than one label
label = static_cast<SCH_LABEL_BASE*>( it );
}
break;
}
default: break;
}
}
if( pinCount != 1 )
return nullptr;
return label;
};
std::vector<SCH_LABEL_BASE*> labels;
for( EDA_ITEM* item : orderedPins )
{
SCH_PIN* pin = static_cast<SCH_PIN*>( item );
SCH_LABEL_BASE* label = findSingleNetLabel( pin );
if( !label )
{
m_frame->ShowInfoBarError(
_( "Each selected pin must have exactly one attached net label and no other pin connections." ) );
return 0;
}
labels.push_back( label );
}
if( labels.size() >= 2 )
{
SCH_COMMIT commit( m_frame );
for( SCH_LABEL_BASE* lb : labels )
commit.Modify( lb, m_frame->GetScreen() );
for( size_t i = 0; i < labels.size() - 1; ++i )
{
SCH_LABEL_BASE* a = labels[i];
SCH_LABEL_BASE* b = labels[( i + 1 ) % labels.size()];
wxString aText = a->GetText();
wxString bText = b->GetText();
a->SetText( bText );
b->SetText( aText );
}
commit.Push( _( "Swap Pin Labels" ) );
}
return 0;
}
int SCH_EDIT_TOOL::RepeatDrawItem( const TOOL_EVENT& aEvent )
{
const std::vector<std::unique_ptr<SCH_ITEM>>& sourceItems = m_frame->GetRepeatItems();
@ -3162,6 +3263,7 @@ void SCH_EDIT_TOOL::setTransitions()
Go( &SCH_EDIT_TOOL::Mirror, SCH_ACTIONS::mirrorV.MakeEvent() );
Go( &SCH_EDIT_TOOL::Mirror, SCH_ACTIONS::mirrorH.MakeEvent() );
Go( &SCH_EDIT_TOOL::Swap, SCH_ACTIONS::swap.MakeEvent() );
Go( &SCH_EDIT_TOOL::SwapPinLabels, SCH_ACTIONS::swapPinLabels.MakeEvent() );
Go( &SCH_EDIT_TOOL::DoDelete, ACTIONS::doDelete.MakeEvent() );
Go( &SCH_EDIT_TOOL::InteractiveDelete, ACTIONS::deleteTool.MakeEvent() );

View File

@ -42,6 +42,7 @@ public:
int Rotate( const TOOL_EVENT& aEvent );
int Mirror( const TOOL_EVENT& aEvent );
int Swap( const TOOL_EVENT& aEvent );
int SwapPinLabels( const TOOL_EVENT& aEvent );
int RepeatDrawItem( const TOOL_EVENT& aEvent );

View File

@ -46,6 +46,9 @@
#include <pgm_base.h>
#include <view/view_controls.h>
#include <settings/settings_manager.h>
#include <math/box2.h>
#include <base_units.h>
#include <sch_screen.h>
#include "sch_move_tool.h"
@ -500,6 +503,7 @@ bool SCH_MOVE_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, SCH_COMMIT* aComm
TOOL_EVENT* evt = &copy;
VECTOR2I prevPos;
GRID_HELPER_GRIDS snapLayer = GRID_CURRENT;
SCH_SHEET* hoverSheet = nullptr;
m_cursor = controls->GetCursorPosition();
@ -771,7 +775,65 @@ bool SCH_MOVE_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, SCH_COMMIT* aComm
m_cursor = grid.BestSnapAnchor( controls->GetCursorPosition( false ),
snapLayer, selection );
// Determine potential target sheet.
SCH_SHEET* sheet = dynamic_cast<SCH_SHEET*>( m_frame->GetScreen()->GetItem( m_cursor, 0,
SCH_SHEET_T ) );
if( sheet && sheet->IsSelected() )
sheet = nullptr; // Never target a selected sheet
if( !sheet )
{
// Build current selection bounding box in its (already moved) position.
BOX2I selBBox;
for( EDA_ITEM* it : selection )
{
if( SCH_ITEM* schIt = dynamic_cast<SCH_ITEM*>( it ) )
selBBox.Merge( schIt->GetBoundingBox() );
}
if( selBBox.GetWidth() > 0 && selBBox.GetHeight() > 0 )
{
VECTOR2I selCenter( selBBox.GetX() + selBBox.GetWidth() / 2,
selBBox.GetY() + selBBox.GetHeight() / 2 );
// Find first non-selected sheet whose body fully contains the selection
// or at least contains its center point.
for( SCH_ITEM* it : m_frame->GetScreen()->Items().OfType( SCH_SHEET_T ) )
{
SCH_SHEET* candidate = static_cast<SCH_SHEET*>( it );
if( candidate->IsSelected() || candidate->IsRootSheet() )
continue;
BOX2I body = candidate->GetBodyBoundingBox();
if( body.Contains( selBBox ) || body.Contains( selCenter ) )
{
sheet = candidate;
break;
}
}
}
}
if( sheet != hoverSheet )
{
if( hoverSheet )
{
hoverSheet->ClearFlags( BRIGHTENED );
m_frame->UpdateItem( hoverSheet, false );
}
hoverSheet = sheet;
if( hoverSheet )
{
hoverSheet->SetFlags( BRIGHTENED );
m_frame->UpdateItem( hoverSheet, false );
}
}
m_frame->GetCanvas()->SetCurrentCursor( hoverSheet ? KICURSOR::PLACE
: KICURSOR::MOVING );
VECTOR2I delta( m_cursor - prevPos );
m_anchorPos = m_cursor;
@ -1032,6 +1094,22 @@ bool SCH_MOVE_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, SCH_COMMIT* aComm
} while( ( evt = Wait() ) ); //Should be assignment not equality test
SCH_SHEET* targetSheet = hoverSheet;
if( hoverSheet )
{
hoverSheet->ClearFlags( BRIGHTENED );
m_frame->UpdateItem( hoverSheet, false );
}
if( targetSheet )
{
moveSelectionToSheet( selection, targetSheet, aCommit );
m_toolMgr->RunAction( ACTIONS::selectionClear );
m_newDragLines.clear();
m_changedDragLines.clear();
}
// Create a selection of original selection, drag selected/changed items, and new
// bend lines for later before we clear them in the aCommit. We'll need these
// to check for new junctions needed, etc.
@ -1135,6 +1213,60 @@ bool SCH_MOVE_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, SCH_COMMIT* aComm
}
void SCH_MOVE_TOOL::moveSelectionToSheet( SCH_SELECTION& aSelection, SCH_SHEET* aTargetSheet,
SCH_COMMIT* aCommit )
{
SCH_SCREEN* destScreen = aTargetSheet->GetScreen();
SCH_SCREEN* srcScreen = m_frame->GetScreen();
BOX2I bbox;
for( EDA_ITEM* item : aSelection )
bbox.Merge( static_cast<SCH_ITEM*>( item )->GetBoundingBox() );
VECTOR2I offset = VECTOR2I( 0, 0 ) - bbox.GetPosition();
int step = schIUScale.MilsToIU( 50 );
bool overlap = false;
do
{
BOX2I moved = bbox;
moved.Move( offset );
overlap = false;
for( SCH_ITEM* existing : destScreen->Items() )
{
if( moved.Intersects( existing->GetBoundingBox() ) )
{
overlap = true;
break;
}
}
if( overlap )
offset += VECTOR2I( step, step );
} while( overlap );
for( EDA_ITEM* item : aSelection )
{
SCH_ITEM* schItem = static_cast<SCH_ITEM*>( item );
// Remove from current screen and view manually
m_frame->RemoveFromScreen( schItem, srcScreen );
// Move the item
schItem->Move( offset );
// Add to destination screen manually (won't add to view since it's not current)
destScreen->Append( schItem );
// Record in commit with CHT_DONE flag to bypass automatic screen/view operations
aCommit->Stage( schItem, CHT_REMOVE | CHT_DONE, srcScreen );
aCommit->Stage( schItem, CHT_ADD | CHT_DONE, destScreen );
}
}
void SCH_MOVE_TOOL::trimDanglingLines( SCH_COMMIT* aCommit )
{
// Need a local cleanup first to ensure we remove unneeded junctions
@ -1163,8 +1295,7 @@ void SCH_MOVE_TOOL::trimDanglingLines( SCH_COMMIT* aCommit )
{
line->SetFlags( STRUCT_DELETED );
aCommit->Removed( line, m_frame->GetScreen() );
updateItem( line, false );
updateItem( line, false ); // Update any cached visuals before commit processes
m_frame->RemoveFromScreen( line, m_frame->GetScreen() );
}
}

View File

@ -35,6 +35,9 @@ class SCH_LINE;
class SCH_LABEL_BASE;
class SCH_SHEET_PIN;
class SCH_JUNCTION;
class SCH_SELECTION;
class SCH_SHEET;
class SCH_COMMIT;
struct SPECIAL_CASE_LABEL_INFO
@ -81,6 +84,8 @@ private:
void orthoLineDrag( SCH_COMMIT* aCommit, SCH_LINE* line, const VECTOR2I& splitDelta,
int& xBendCount, int& yBendCount, const EE_GRID_HELPER& grid );
void moveSelectionToSheet( SCH_SELECTION& aSelection, SCH_SHEET* aTarget, SCH_COMMIT* aCommit );
///< Clears the new drag lines and removes them from the screen
void clearNewDragLines();

View File

@ -250,6 +250,7 @@ bool SCH_SELECTION_TOOL::Init()
auto sheetSelection = SCH_CONDITIONS::Count( 1 ) && SCH_CONDITIONS::OnlyTypes( sheetTypes );
auto crossProbingSelection = SCH_CONDITIONS::MoreThan( 0 ) && SCH_CONDITIONS::HasTypes( crossProbingTypes );
auto tableCellSelection = SCH_CONDITIONS::MoreThan( 0 ) && SCH_CONDITIONS::OnlyTypes( tableCellTypes );
auto multiplePinsSelection = SCH_CONDITIONS::MoreThan( 1 ) && SCH_CONDITIONS::OnlyTypes( { SCH_PIN_T } );
// clang-format on
auto schEditSheetPageNumberCondition =
@ -338,6 +339,7 @@ bool SCH_SELECTION_TOOL::Init()
menu.AddItem( SCH_ACTIONS::autoplaceAllSheetPins, sheetSelection && SCH_CONDITIONS::Idle, 250 );
menu.AddItem( SCH_ACTIONS::syncSheetPins, sheetSelection && SCH_CONDITIONS::Idle, 250 );
menu.AddItem( SCH_ACTIONS::assignNetclass, connectedSelection && SCH_CONDITIONS::Idle, 250 );
menu.AddItem( SCH_ACTIONS::swapPinLabels, multiplePinsSelection && schEditCondition && SCH_CONDITIONS::Idle, 250 );
menu.AddItem( SCH_ACTIONS::editPageNumber, schEditSheetPageNumberCondition, 250 );
menu.AddSeparator( 400 );

View File

@ -671,8 +671,7 @@ bool GERBER_FILE_IMAGE::ExecuteRS274XCommand( int aCommand, char* aBuff,
break;
case IMAGE_POLARITY:
// These commands are deprecated since 2012.
// So do nothing and prompt the user about this command
// Note: these commands IPPOS and IPNEG are deprecated since 2012.
if( strncasecmp( aText, "NEG", 3 ) == 0 )
{
m_ImageNegative = true;
@ -688,7 +687,6 @@ bool GERBER_FILE_IMAGE::ExecuteRS274XCommand( int aCommand, char* aBuff,
// actual effect. Just skip it.
}
ok = false;
break;
case LOAD_POLARITY:

View File

@ -18,6 +18,7 @@
*/
#include "grid_tricks.h"
#include <functional>
class LIB_TABLE_GRID_TRICKS : public GRID_TRICKS
{
@ -33,6 +34,7 @@ class LIB_TABLE_GRID_TRICKS : public GRID_TRICKS
public:
explicit LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid );
LIB_TABLE_GRID_TRICKS( WX_GRID* aGrid, std::function<void( wxCommandEvent& )> aAddHandler );
virtual ~LIB_TABLE_GRID_TRICKS(){};
@ -43,5 +45,7 @@ protected:
virtual void optionsEditor( int aRow ) = 0;
bool handleDoubleClick( wxGridEvent& aEvent ) override;
void onCharHook( wxKeyEvent& ev );
virtual bool supportsVisibilityColumn() { return false; }
};

View File

@ -327,7 +327,9 @@ class INFOBAR_REPORTER : public REPORTER
{
public:
INFOBAR_REPORTER( WX_INFOBAR* aInfoBar ) :
REPORTER(), m_messageSet( false ), m_infoBar( aInfoBar ),
REPORTER(),
m_messageSet( false ),
m_infoBar( aInfoBar ),
m_severity( RPT_SEVERITY_UNDEFINED )
{
}

View File

@ -55,8 +55,8 @@
#define RESOLVE_PAGE( T, pageIndex ) static_cast<T*>( m_treebook->ResolvePage( pageIndex ) )
DIALOG_BOARD_SETUP::DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame ) :
PAGED_DIALOG( aFrame, _( "Board Setup" ), false, false,
DIALOG_BOARD_SETUP::DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame, wxWindow* aParent ) :
PAGED_DIALOG( aParent ? aParent : aFrame, _( "Board Setup" ), false, false,
_( "Import Settings from Another Board..." ), wxSize( 980, 600 ) ),
m_frame( aFrame ),
m_layers( nullptr ),

View File

@ -42,7 +42,7 @@ class PANEL_TEXT_VARIABLES;
class DIALOG_BOARD_SETUP : public PAGED_DIALOG
{
public:
DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame );
DIALOG_BOARD_SETUP( PCB_EDIT_FRAME* aFrame, wxWindow* aWindow = nullptr );
~DIALOG_BOARD_SETUP();
protected:

View File

@ -303,7 +303,7 @@ void DIALOG_DRC::OnMenu( wxCommandEvent& event )
void DIALOG_DRC::OnErrorLinkClicked( wxHtmlLinkEvent& event )
{
m_frame->ShowBoardSetupDialog( _( "Custom Rules" ) );
m_frame->ShowBoardSetupDialog( _( "Custom Rules" ), this );
}
@ -993,7 +993,7 @@ void DIALOG_DRC::OnDRCItemRClick( wxDataViewEvent& aEvent )
}
case ID_EDIT_SEVERITIES:
m_frame->ShowBoardSetupDialog( _( "Violation Severity" ) );
m_frame->ShowBoardSetupDialog( _( "Violation Severity" ), this );
break;
}
@ -1035,7 +1035,7 @@ void DIALOG_DRC::OnIgnoredItemRClick( wxListEvent& event )
void DIALOG_DRC::OnEditViolationSeverities( wxHyperlinkEvent& aEvent )
{
m_frame->ShowBoardSetupDialog( _( "Violation Severity" ) );
m_frame->ShowBoardSetupDialog( _( "Violation Severity" ), this );
}

View File

@ -674,6 +674,7 @@ bool DIALOG_FOOTPRINT_PROPERTIES_FP_EDITOR::Validate()
// Check that the user isn't trying to remove a layer that is used by the footprint
usedLayers &= ~getCustomLayersFromControls();
usedLayers &= ~LSET::AllTechMask();
usedLayers &= ~LSET::UserMask();
if( usedLayers.any() )
{

View File

@ -38,6 +38,7 @@
#include <wx/dirdlg.h>
#include <wx/filedlg.h>
#include <wx/msgdlg.h>
#include <functional>
#include <project.h>
#include <env_vars.h>
@ -147,6 +148,12 @@ public:
m_dialog( aParent )
{ }
FP_GRID_TRICKS( DIALOG_EDIT_LIBRARY_TABLES* aParent, WX_GRID* aGrid,
std::function<void( wxCommandEvent& )> aAddHandler ) :
LIB_TABLE_GRID_TRICKS( aGrid, aAddHandler ),
m_dialog( aParent )
{ }
protected:
DIALOG_EDIT_LIBRARY_TABLES* m_dialog;
@ -217,8 +224,21 @@ protected:
}
else
{
// paste spreadsheet formatted text.
GRID_TRICKS::paste_text( cb_text );
wxString text = cb_text;
if( !text.Contains( '\t' ) && text.Contains( ',' ) )
text.Replace( ',', '\t' );
if( text.Contains( '\t' ) )
{
int row = m_grid->GetGridCursorRow();
m_grid->ClearSelection();
m_grid->SelectRow( row );
m_grid->SetGridCursor( row, 0 );
getSelectedArea();
}
GRID_TRICKS::paste_text( text );
m_grid->AutoSizeColumns( false );
}
@ -254,7 +274,8 @@ void PANEL_FP_LIB_TABLE::setupGrid( WX_GRID* aGrid )
aGrid->SetRowSize( ii, aGrid->GetDefaultRowSize() + 4 );
// add Cut, Copy, and Paste to wxGrids
aGrid->PushEventHandler( new FP_GRID_TRICKS( m_parent, aGrid ) );
aGrid->PushEventHandler( new FP_GRID_TRICKS( m_parent, aGrid,
[this]( wxCommandEvent& event ) { appendRowHandler( event ); } ) );
aGrid->SetSelectionMode( wxGrid::wxGridSelectRows );

View File

@ -527,10 +527,9 @@ void DRC_ENGINE::loadRules( const wxFileName& aPath )
void DRC_ENGINE::compileRules()
{
if( m_logReporter )
{
m_logReporter->Report( ( wxString::Format( wxT( "Compiling Rules (%d rules): " ),
(int) m_rules.size() ) ) );
}
m_logReporter->Report( wxT( "Compiling Rules" ) );
REPORTER error_semaphore;
for( std::shared_ptr<DRC_RULE>& rule : m_rules )
{
@ -539,9 +538,12 @@ void DRC_ENGINE::compileRules()
if( rule->m_Condition && !rule->m_Condition->GetExpression().IsEmpty() )
{
condition = rule->m_Condition;
condition->Compile( nullptr );
condition->Compile( &error_semaphore );
}
if( error_semaphore.HasMessageOfSeverity( RPT_SEVERITY_ERROR ) )
THROW_PARSE_ERROR( wxT( "Parse error" ), rule->m_Name, rule->m_Condition->GetExpression(), 0, 0 );
for( const DRC_CONSTRAINT& constraint : rule->m_Constraints )
{
if( !m_constraintMap.count( constraint.m_Type ) )
@ -594,6 +596,8 @@ void DRC_ENGINE::InitEngine( const wxFileName& aRulePath )
}
catch( PARSE_ERROR& original_parse_error )
{
m_rules.clear();
try // try again with just our implicit rules
{
loadImplicitRules();

View File

@ -750,6 +750,15 @@ public:
void ApplyDefaultSettings( const BOARD& board, bool aStyleFields, bool aStyleText,
bool aStyleShapes );
struct FP_UNIT_INFO
{
wxString m_unitName; // e.g. A
std::vector<wxString> m_pins; // pin numbers in this unit
};
void SetUnitInfo( const std::vector<FP_UNIT_INFO>& aUnits ) { m_unitInfo = aUnits; }
const std::vector<FP_UNIT_INFO>& GetUnitInfo() const { return m_unitInfo; }
bool IsBoardOnly() const { return m_attributes & FP_BOARD_ONLY; }
void SetBoardOnly( bool aIsBoardOnly = true )
{
@ -1180,6 +1189,9 @@ private:
std::unordered_set<wxString> m_transientComponentClassNames;
std::unique_ptr<COMPONENT_CLASS_CACHE_PROXY> m_componentClassCacheProxy;
// Optional unit mapping information for multi-unit symbols
std::vector<FP_UNIT_INFO> m_unitInfo;
};
#endif // FOOTPRINT_H

View File

@ -1166,6 +1166,67 @@ bool BOARD_NETLIST_UPDATER::updateComponentPadConnections( FOOTPRINT* aFootprint
}
bool BOARD_NETLIST_UPDATER::updateComponentUnits( FOOTPRINT* aFootprint, COMPONENT* aNewComponent )
{
// Build the footprint-side representation from the netlist component
std::vector<FOOTPRINT::FP_UNIT_INFO> newUnits;
for( const COMPONENT::UNIT_INFO& u : aNewComponent->GetUnitInfo() )
newUnits.push_back( { u.m_unitName, u.m_pins } );
const std::vector<FOOTPRINT::FP_UNIT_INFO>& curUnits = aFootprint->GetUnitInfo();
auto unitsEqual = []( const std::vector<FOOTPRINT::FP_UNIT_INFO>& a,
const std::vector<FOOTPRINT::FP_UNIT_INFO>& b )
{
if( a.size() != b.size() )
return false;
for( size_t i = 0; i < a.size(); ++i )
{
if( a[i].m_unitName != b[i].m_unitName )
return false;
if( a[i].m_pins != b[i].m_pins )
return false;
}
return true;
};
if( unitsEqual( curUnits, newUnits ) )
return false;
wxString msg;
if( m_isDryRun )
{
msg.Printf( _( "Update %s unit metadata." ), aFootprint->GetReference() );
m_reporter->Report( msg, RPT_SEVERITY_ACTION );
return false; // no actual change on board during dry run
}
// Create a copy only if the footprint has not been added during this update
FOOTPRINT* copy = nullptr;
if( !m_commit.GetStatus( aFootprint ) )
{
copy = static_cast<FOOTPRINT*>( aFootprint->Clone() );
copy->SetParentGroup( nullptr );
}
aFootprint->SetUnitInfo( newUnits );
msg.Printf( _( "Updated %s unit metadata." ), aFootprint->GetReference() );
m_reporter->Report( msg, RPT_SEVERITY_ACTION );
if( copy )
m_commit.Modified( aFootprint, copy );
return true;
}
void BOARD_NETLIST_UPDATER::cacheCopperZoneConnections()
{
for( ZONE* zone : m_board->Zones() )
@ -1567,6 +1628,7 @@ bool BOARD_NETLIST_UPDATER::UpdateNetlist( NETLIST& aNetlist )
updateFootprintGroup( tmp, component );
updateComponentPadConnections( tmp, component );
updateComponentClass( tmp, component );
updateComponentUnits( tmp, component );
sheetPaths.insert( footprint->GetSheetname() );
}
@ -1593,6 +1655,7 @@ bool BOARD_NETLIST_UPDATER::UpdateNetlist( NETLIST& aNetlist )
updateFootprintGroup( footprint, component );
updateComponentPadConnections( footprint, component );
updateComponentClass( footprint, component );
updateComponentUnits( footprint, component );
sheetPaths.insert( footprint->GetSheetname() );
}

View File

@ -121,6 +121,8 @@ private:
void updateComponentClass( FOOTPRINT* aFootprint, COMPONENT* aNewComponent );
bool updateComponentUnits( FOOTPRINT* aFootprint, COMPONENT* aNewComponent );
void cacheCopperZoneConnections();
bool updateCopperZoneNets( NETLIST& aNetlist );

View File

@ -332,6 +332,8 @@ void KICAD_NETLIST_PARSER::parseComponent()
bool duplicatePinsAreJumpers = false;
std::vector<std::set<wxString>> jumperPinGroups;
std::vector<COMPONENT::UNIT_INFO> parsedUnits;
// The token comp was read, so the next data is (ref P1)
while( (token = NextTok() ) != T_RIGHT )
{
@ -496,6 +498,87 @@ void KICAD_NETLIST_PARSER::parseComponent()
break;
case T_units:
{
// Parse a section like:
// (units (unit (ref "U1A") (name "A") (pins (pin "1") (pin "2"))))
while( ( token = NextTok() ) != T_RIGHT )
{
if( token == T_LEFT )
token = NextTok();
if( token == T_unit )
{
COMPONENT::UNIT_INFO info;
while( ( token = NextTok() ) != T_RIGHT )
{
if( token == T_LEFT )
token = NextTok();
switch( token )
{
case T_name:
NeedSYMBOLorNUMBER();
info.m_unitName = From_UTF8( CurText() );
NeedRIGHT();
break;
case T_pins:
while( ( token = NextTok() ) != T_RIGHT )
{
if( token == T_LEFT )
token = NextTok();
if( token == T_pin )
{
wxString pinNum;
// Parse pins in attribute style: (pin (num "1"))
for( token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token == T_LEFT )
token = NextTok();
if( token == T_num )
{
NeedSYMBOLorNUMBER();
pinNum = From_UTF8( CurText() );
NeedRIGHT();
}
else
{
// ignore other subfields of pin
// leave bare tokens untouched; they are not supported in this context
}
}
if( !pinNum.IsEmpty() )
info.m_pins.emplace_back( pinNum );
}
else
{
skipCurrent();
}
}
break;
default:
skipCurrent();
break;
}
}
parsedUnits.push_back( info );
}
else
{
skipCurrent();
}
}
break;
}
case T_component_classes:
while( ( token = NextTok() ) != T_RIGHT )
{
@ -583,6 +666,7 @@ void KICAD_NETLIST_PARSER::parseComponent()
component->SetDuplicatePadNumbersAreJumpers( duplicatePinsAreJumpers );
std::ranges::copy( jumperPinGroups, std::inserter( component->JumperPadGroups(),
component->JumperPadGroups().end() ) );
component->SetUnitInfo( parsedUnits );
m_netlist->AddComponent( component );
}

View File

@ -56,6 +56,14 @@ void COMPONENT::SetFootprint( FOOTPRINT* aFootprint )
aFootprint->SetValue( m_value );
aFootprint->SetFPID( m_fpid );
aFootprint->SetPath( path );
// Copy over unit info
std::vector<FOOTPRINT::FP_UNIT_INFO> fpUnits;
for( const UNIT_INFO& u : m_units )
fpUnits.push_back( { u.m_unitName, u.m_pins } );
aFootprint->SetUnitInfo( fpUnits );
}

View File

@ -206,6 +206,16 @@ public:
NETLIST_GROUP* GetGroup() const { return m_group; }
void SetGroup( NETLIST_GROUP* aGroup ) { m_group = aGroup; }
// Unit info for multi-unit symbols
struct UNIT_INFO
{
wxString m_unitName; // e.g. A
std::vector<wxString> m_pins; // pin numbers in this unit
};
void SetUnitInfo( const std::vector<UNIT_INFO>& aUnits ) { m_units = aUnits; }
const std::vector<UNIT_INFO>& GetUnitInfo() const { return m_units; }
private:
std::vector<COMPONENT_NET> m_nets; ///< list of nets shared by the component pins
@ -260,6 +270,9 @@ private:
NETLIST_GROUP* m_group;
static COMPONENT_NET m_emptyNet;
// Unit information parsed from the netlist (optional)
std::vector<UNIT_INFO> m_units;
};

View File

@ -109,6 +109,7 @@
#include <widgets/pcb_net_inspector_panel.h>
#include <widgets/wx_aui_utils.h>
#include <kiplatform/app.h>
#include <kiplatform/ui.h>
#include <core/profile.h>
#include <math/box2_minmax.h>
#include <view/wx_view_controls.h>
@ -1466,7 +1467,7 @@ void PCB_EDIT_FRAME::ActivateGalCanvas()
}
void PCB_EDIT_FRAME::ShowBoardSetupDialog( const wxString& aInitialPage )
void PCB_EDIT_FRAME::ShowBoardSetupDialog( const wxString& aInitialPage, wxWindow* aParent )
{
static std::mutex dialogMutex; // Local static mutex
@ -1486,7 +1487,7 @@ void PCB_EDIT_FRAME::ShowBoardSetupDialog( const wxString& aInitialPage )
// Make sure everything's up-to-date
GetBoard()->BuildListOfNets();
DIALOG_BOARD_SETUP dlg( this );
DIALOG_BOARD_SETUP dlg( this, aParent );
if( !aInitialPage.IsEmpty() )
dlg.SetInitialPage( aInitialPage, wxEmptyString );

View File

@ -301,7 +301,7 @@ public:
///< @copydoc EDA_DRAW_FRAME::UseGalCanvas()
void ActivateGalCanvas() override;
void ShowBoardSetupDialog( const wxString& aInitialPage = wxEmptyString );
void ShowBoardSetupDialog( const wxString& aInitialPage = wxEmptyString, wxWindow* aParent = nullptr );
void PrepareLayerIndicator( bool aForceRebuild = false );

View File

@ -1238,6 +1238,26 @@ void PCB_IO_KICAD_SEXPR::format( const FOOTPRINT* aFootprint ) const
if( !aFootprint->GetSheetfile().empty() )
m_out->Print( "(sheetfile %s)", m_out->Quotew( aFootprint->GetSheetfile() ).c_str() );
// Emit unit info for gate swapping metadata (flat pin list form)
if( !aFootprint->GetUnitInfo().empty() )
{
m_out->Print( "(units" );
for( const FOOTPRINT::FP_UNIT_INFO& u : aFootprint->GetUnitInfo() )
{
m_out->Print( "(unit (name %s)", m_out->Quotew( u.m_unitName ).c_str() );
m_out->Print( "(pins" );
for( const wxString& n : u.m_pins )
m_out->Print( " %s", m_out->Quotew( n ).c_str() );
m_out->Print( ")" ); // </pins>
m_out->Print( ")" ); // </unit>
}
m_out->Print( ")" ); // </units>
}
if( aFootprint->GetLocalSolderMaskMargin().has_value() )
{
m_out->Print( "(solder_mask_margin %s)",

View File

@ -191,7 +191,8 @@ class PCB_IO_KICAD_SEXPR; // forward decl
//#define SEXPR_BOARD_FILE_VERSION 20250818 // Support for custom layer counts in footprints
//#define SEXPR_BOARD_FILE_VERSION 20250829 // Support Rounded Rectangles
//#define SEXPR_BOARD_FILE_VERSION 20250901 // PCB points
#define SEXPR_BOARD_FILE_VERSION 20250907 // uuids for tables
//#define SEXPR_BOARD_FILE_VERSION 20250907 // uuids for tables
#define SEXPR_BOARD_FILE_VERSION 20250909 // footprint unit metadata (units/pins)
#define BOARD_FILE_HOST_VERSION 20200825 ///< Earlier files than this include the host tag
#define LEGACY_ARC_FORMATTING 20210925 ///< These were the last to use old arc formatting

View File

@ -4758,6 +4758,68 @@ FOOTPRINT* PCB_IO_KICAD_SEXPR_PARSER::parseFOOTPRINT_unchecked( wxArrayString* a
NeedRIGHT();
break;
case T_units:
{
std::vector<FOOTPRINT::FP_UNIT_INFO> unitInfos;
// (units (unit (name "A") (pins "1" "2" ...)) ...)
for( token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token == T_LEFT )
token = NextTok();
if( token == T_unit )
{
FOOTPRINT::FP_UNIT_INFO info;
for( token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token == T_LEFT )
token = NextTok();
if( token == T_name )
{
NeedSYMBOLorNUMBER();
info.m_unitName = FromUTF8();
NeedRIGHT();
}
else if( token == T_pins )
{
// Parse a flat list of quoted numbers or symbols until ')'
for( token = NextTok(); token != T_RIGHT; token = NextTok() )
{
if( token == T_STRING || token == T_NUMBER )
{
info.m_pins.emplace_back( FromUTF8() );
}
else
{
Expecting( "pin number" );
}
}
}
else
{
// Unknown sub-token inside unit; skip its list if any
skipCurrent();
}
}
unitInfos.push_back( info );
}
else
{
// Unknown entry under units; skip
skipCurrent();
}
}
if( !unitInfos.empty() )
footprint->SetUnitInfo( unitInfos );
break;
}
case T_autoplace_cost90:
case T_autoplace_cost180:
parseInt( "legacy auto-place cost" );

View File

@ -52,6 +52,9 @@ enum pcbnew_ids
ID_POPUP_PCB_SELECT_VIASIZE14,
ID_POPUP_PCB_SELECT_VIASIZE15,
ID_POPUP_PCB_SELECT_VIASIZE16,
// Dynamic gate swap submenu entries (index offset added to base)
ID_POPUP_PCB_SWAP_UNIT_BASE,
ID_POPUP_PCB_SWAP_UNIT_LAST = ID_POPUP_PCB_SWAP_UNIT_BASE + 63,
ID_POPUP_PCB_SELECT_CUSTOM_DIFFPAIR,
ID_POPUP_PCB_SELECT_USE_NETCLASS_DIFFPAIR,
ID_POPUP_PCB_SELECT_DIFFPAIR1,

View File

@ -58,6 +58,7 @@
#include <tools/pad_tool.h>
#include <view/view_controls.h>
#include <connectivity/connectivity_algo.h>
#include <pcbnew_id.h>
#include <core/kicad_algo.h>
#include <fix_board_shape.h>
#include <bitmaps.h>
@ -208,6 +209,242 @@ static std::shared_ptr<CONDITIONAL_MENU> makeShapeModificationMenu( TOOL_INTERAC
return menu;
};
// Gate-swap submenu and helpers
class GATE_SWAP_MENU : public ACTION_MENU
{
public:
GATE_SWAP_MENU() :
ACTION_MENU( true )
{
SetIcon( BITMAPS::swap );
SetTitle( _( "Swap Gate Nets..." ) );
}
// We're looking for a selection of pad(s) that belong to a single footprint with multiple units.
// Ignore non-pad items since we might have grabbed some traces inside the pad, etc.
static const FOOTPRINT* GetSingleEligibleFootprint( const SELECTION& aSelection )
{
const FOOTPRINT* single = nullptr;
for( const EDA_ITEM* it : aSelection )
{
if( it->Type() != PCB_PAD_T )
continue;
const PAD* pad = static_cast<const PAD*>( static_cast<const BOARD_ITEM*>( it ) );
const FOOTPRINT* fp = pad->GetParentFootprint();
if( !fp )
continue;
const auto& units = fp->GetUnitInfo();
if( units.size() < 2 )
continue;
const wxString& padNum = pad->GetNumber();
bool inAnyUnit = false;
for( const auto& u : units )
{
for( const auto& pnum : u.m_pins )
{
if( pnum == padNum )
{
inAnyUnit = true;
break;
}
}
if( inAnyUnit )
break;
}
if( !inAnyUnit )
continue;
if( !single )
single = fp;
else if( single != fp )
return nullptr;
}
return single;
}
static std::unordered_set<wxString> CollectSelectedPadNumbers( const SELECTION& aSelection,
const FOOTPRINT* aFootprint )
{
std::unordered_set<wxString> padNums;
for( const EDA_ITEM* it : aSelection )
{
if( it->Type() != PCB_PAD_T )
continue;
const PAD* pad = static_cast<const PAD*>( static_cast<const BOARD_ITEM*>( it ) );
if( pad->GetParentFootprint() != aFootprint )
continue;
padNums.insert( pad->GetNumber() );
}
return padNums;
}
// Make a list of the unit names that have any pad selected
static std::vector<int> GetUnitsHitIndices( const FOOTPRINT* aFootprint,
const std::unordered_set<wxString>& aSelPadNums )
{
std::vector<int> indices;
const auto& units = aFootprint->GetUnitInfo();
for( size_t i = 0; i < units.size(); ++i )
{
bool hasAny = false;
for( const auto& pn : units[i].m_pins )
{
if( aSelPadNums.count( pn ) )
{
hasAny = true;
break;
}
}
if( hasAny )
indices.push_back( static_cast<int>( i ) );
}
return indices;
}
// Gate swapping requires the swapped units to have equal pin counts
static bool EqualPinCounts( const FOOTPRINT* aFootprint, const std::vector<int>& aUnitIndices )
{
if( aUnitIndices.empty() )
return false;
const auto& units = aFootprint->GetUnitInfo();
const size_t cnt = units[static_cast<size_t>( aUnitIndices.front() )].m_pins.size();
for( int idx : aUnitIndices )
{
if( units[static_cast<size_t>( idx )].m_pins.size() != cnt )
return false;
}
return true;
}
// Used when we have exactly one source unit selected; find all other units with equal pin counts
static std::vector<int> GetCompatibleTargets( const FOOTPRINT* aFootprint, int aSourceIdx )
{
std::vector<int> targets;
const auto& units = aFootprint->GetUnitInfo();
const size_t pinCount = units[static_cast<size_t>( aSourceIdx )].m_pins.size();
for( size_t i = 0; i < units.size(); ++i )
{
if( static_cast<int>( i ) == aSourceIdx )
continue;
if( units[i].m_pins.size() != pinCount )
continue;
targets.push_back( static_cast<int>( i ) );
}
return targets;
}
protected:
ACTION_MENU* create() const override { return new GATE_SWAP_MENU(); }
// The gate swap menu dynamically populates itself based on current selection of pads
// on a single multi-unit footprint.
//
// If there is exactly one unit with any pad selected, we build a menu of available swaps
// with all other units with equal pin counts.
void update() override
{
Clear();
PCB_SELECTION_TOOL* selTool = getToolManager()->GetTool<PCB_SELECTION_TOOL>();
const SELECTION& sel = selTool->GetSelection();
const FOOTPRINT* fp = GetSingleEligibleFootprint( sel );
if( !fp )
return;
std::unordered_set<wxString> selPadNums = CollectSelectedPadNumbers( sel, fp );
std::vector<int> unitsHit = GetUnitsHitIndices( fp, selPadNums );
if( unitsHit.size() != 1 )
return;
const int sourceIdx = unitsHit.front();
std::vector<int> targets = GetCompatibleTargets( fp, sourceIdx );
for( int idx : targets )
{
wxString label;
label.Printf( _( "Swap with %s" ), fp->GetUnitInfo()[static_cast<size_t>( idx )].m_unitName );
Append( ID_POPUP_PCB_SWAP_UNIT_BASE + idx, label );
}
}
OPT_TOOL_EVENT eventHandler( const wxMenuEvent& aEvent ) override
{
int id = aEvent.GetId();
if( id >= ID_POPUP_PCB_SWAP_UNIT_BASE && id <= ID_POPUP_PCB_SWAP_UNIT_LAST )
{
PCB_SELECTION_TOOL* selTool = getToolManager()->GetTool<PCB_SELECTION_TOOL>();
const SELECTION& sel = selTool->GetSelection();
const FOOTPRINT* fp = GetSingleEligibleFootprint( sel );
if( !fp )
return OPT_TOOL_EVENT();
const auto& units = fp->GetUnitInfo();
const int targetIdx = id - ID_POPUP_PCB_SWAP_UNIT_BASE;
if( targetIdx < 0 || targetIdx >= static_cast<int>( units.size() ) )
return OPT_TOOL_EVENT();
TOOL_EVENT evt = PCB_ACTIONS::swapGateNets.MakeEvent();
evt.SetParameter( units[targetIdx].m_unitName );
return OPT_TOOL_EVENT( evt );
}
return OPT_TOOL_EVENT();
}
};
static std::shared_ptr<ACTION_MENU> makeGateSwapMenu( TOOL_INTERACTIVE* aTool )
{
auto menu = std::make_shared<GATE_SWAP_MENU>();
menu->SetTool( aTool );
return menu;
};
bool EDIT_TOOL::Init()
{
// Find the selection tool, so they can cooperate
@ -219,6 +456,9 @@ bool EDIT_TOOL::Init()
std::shared_ptr<CONDITIONAL_MENU> shapeModificationSubMenu = makeShapeModificationMenu( this );
m_selectionTool->GetToolMenu().RegisterSubMenu( shapeModificationSubMenu );
std::shared_ptr<ACTION_MENU> gateSwapSubMenu = makeGateSwapMenu( this );
m_selectionTool->GetToolMenu().RegisterSubMenu( gateSwapSubMenu );
auto positioningToolsCondition =
[this]( const SELECTION& aSel )
{
@ -235,6 +475,47 @@ bool EDIT_TOOL::Init()
return subMenu->GetMenuItemCount() > 0;
};
// Does selection map to a single eligible footprint and exactly one unit?
auto gateSwapSingleUnitOnOneFootprint =
[]( const SELECTION& aSelection )
{
const FOOTPRINT* fp = GATE_SWAP_MENU::GetSingleEligibleFootprint( aSelection );
if( !fp )
return false;
std::unordered_set<wxString> selPadNums = GATE_SWAP_MENU::CollectSelectedPadNumbers( aSelection, fp );
std::vector<int> unitsHit = GATE_SWAP_MENU::GetUnitsHitIndices( fp, selPadNums );
if( unitsHit.size() != 1 )
return false;
const int sourceIdx = unitsHit.front();
std::vector<int> targets = GATE_SWAP_MENU::GetCompatibleTargets( fp, sourceIdx );
return !targets.empty();
};
// Does selection map to a single eligible footprint and more than one unit with equal pin counts?
auto gateSwapMultipleUnitsOnOneFootprint =
[]( const SELECTION& aSelection )
{
const FOOTPRINT* fp = GATE_SWAP_MENU::GetSingleEligibleFootprint( aSelection );
if( !fp )
return false;
std::unordered_set<wxString> selPadNums = GATE_SWAP_MENU::CollectSelectedPadNumbers( aSelection, fp );
std::vector<int> unitsHit = GATE_SWAP_MENU::GetUnitsHitIndices( fp, selPadNums );
if( unitsHit.size() < 2 )
return false;
return GATE_SWAP_MENU::EqualPinCounts( fp, unitsHit );
};
auto propertiesCondition =
[this]( const SELECTION& aSel )
{
@ -381,6 +662,10 @@ bool EDIT_TOOL::Init()
menu.AddItem( PCB_ACTIONS::mirrorH, canMirror );
menu.AddItem( PCB_ACTIONS::mirrorV, canMirror );
menu.AddItem( PCB_ACTIONS::swap, SELECTION_CONDITIONS::MoreThan( 1 ) );
menu.AddItem( PCB_ACTIONS::swapPadNets, SELECTION_CONDITIONS::MoreThan( 1 )
&& SELECTION_CONDITIONS::OnlyTypes( padTypes ) );
menu.AddItem( PCB_ACTIONS::swapGateNets, gateSwapMultipleUnitsOnOneFootprint );
menu.AddMenu( gateSwapSubMenu.get(), gateSwapSingleUnitOnOneFootprint );
menu.AddItem( PCB_ACTIONS::packAndMoveFootprints, SELECTION_CONDITIONS::MoreThan( 1 )
&& SELECTION_CONDITIONS::HasType( PCB_FOOTPRINT_T ) );
@ -3532,6 +3817,8 @@ void EDIT_TOOL::setTransitions()
Go( &EDIT_TOOL::Mirror, PCB_ACTIONS::mirrorH.MakeEvent() );
Go( &EDIT_TOOL::Mirror, PCB_ACTIONS::mirrorV.MakeEvent() );
Go( &EDIT_TOOL::Swap, PCB_ACTIONS::swap.MakeEvent() );
Go( &EDIT_TOOL::SwapPadNets, PCB_ACTIONS::swapPadNets.MakeEvent() );
Go( &EDIT_TOOL::SwapGateNets, PCB_ACTIONS::swapGateNets.MakeEvent() );
Go( &EDIT_TOOL::PackAndMoveFootprints, PCB_ACTIONS::packAndMoveFootprints.MakeEvent() );
Go( &EDIT_TOOL::ChangeTrackWidth, PCB_ACTIONS::changeTrackWidth.MakeEvent() );
Go( &EDIT_TOOL::ChangeTrackLayer, PCB_ACTIONS::changeTrackLayerNext.MakeEvent() );

View File

@ -114,6 +114,14 @@ public:
*/
int Swap( const TOOL_EVENT& aEvent );
/**
* Swap nets between selected pads and propagate to connected copper items
* (tracks, arcs, vias) for unconstrained pin swapping.
*/
int SwapPadNets( const TOOL_EVENT& aEvent );
int SwapGateNets( const TOOL_EVENT& aEvent );
/**
* Try to fit selected footprints inside a minimal area and start movement.
*/

View File

@ -25,6 +25,7 @@
*/
#include <functional>
#include <algorithm>
#include <limits>
#include <kiplatform/ui.h>
#include <board.h>
@ -50,6 +51,13 @@
#include <drc/drc_interactive_courtyard_clearance.h>
#include <view/view_controls.h>
#include <connectivity/connectivity_data.h>
#include <wx/richmsgdlg.h>
#include <wx/choicdlg.h>
#include <unordered_set>
#include <unordered_map>
#include <optional>
int EDIT_TOOL::Swap( const TOOL_EVENT& aEvent )
{
@ -154,6 +162,520 @@ int EDIT_TOOL::Swap( const TOOL_EVENT& aEvent )
}
int EDIT_TOOL::SwapPadNets( const TOOL_EVENT& aEvent )
{
if( isRouterActive() )
{
wxBell();
return 0;
}
PCB_SELECTION& selection = m_selectionTool->RequestSelection( &EDIT_TOOL::PadFilter );
if( selection.Size() < 2 || !selection.OnlyContains( { PCB_PAD_T } ) )
return 0;
// Get selected pads in selection order, because swapping is cyclic and we let the user pick
// the rotation order
std::vector<EDA_ITEM*> orderedPads = selection.GetItemsSortedBySelectionOrder();
std::vector<PAD*> pads;
const size_t padsCount = orderedPads.size();
for( EDA_ITEM* it : orderedPads )
pads.push_back( static_cast<PAD*>( static_cast<BOARD_ITEM*>( it ) ) );
// Record original nets and build selected set for quick membership tests
std::vector<int> originalNets( padsCount );
std::unordered_set<PAD*> selectedPads;
for( size_t i = 0; i < padsCount; ++i )
{
originalNets[i] = pads[i]->GetNetCode();
selectedPads.insert( pads[i] );
}
// If all nets are the same, nothing to do
bool allSame = true;
for( size_t i = 1; i < padsCount; ++i )
{
if( originalNets[i] != originalNets[0] )
{
allSame = false;
break;
}
}
if( allSame )
return 0;
// Desired new nets are a cyclic rotation of original nets (like Swap positions)
auto newNetForIndex = [&]( size_t i )
{
return originalNets[( i + 1 ) % padsCount];
};
// Take an event commit since we will eventually support this while actively routing the board
BOARD_COMMIT localCommit( this );
BOARD_COMMIT* commit = dynamic_cast<BOARD_COMMIT*>( aEvent.Commit() );
if( !commit )
commit = &localCommit;
// Connectivity to find items connected to each pad
std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
// Accumulate changes: for each item, assign the resulting new net
std::unordered_map<BOARD_CONNECTED_ITEM*, int> itemNewNets;
std::vector<PAD*> nonSelectedPadsToChange;
for( size_t i = 0; i < padsCount; ++i )
{
PAD* pad = pads[i];
int fromNet = originalNets[i];
int toNet = newNetForIndex( i );
// For each connected item, if it matches fromNet, schedule it for toNet
for( BOARD_CONNECTED_ITEM* ci : connectivity->GetConnectedItems( pad, 0 ) )
{
switch( ci->Type() )
{
case PCB_TRACE_T:
case PCB_ARC_T:
case PCB_VIA_T:
case PCB_PAD_T:
break;
// Exclude zones, user probably doesn't want to change zone nets
default:
continue;
}
if( ci->GetNetCode() != fromNet )
continue;
// Track conflicts: if already assigned a different new net, just overwrite (last wins)
itemNewNets[ci] = toNet;
if( ci->Type() == PCB_PAD_T )
{
PAD* otherPad = static_cast<PAD*>( ci );
if( !selectedPads.count( otherPad ) )
nonSelectedPadsToChange.push_back( otherPad );
}
}
}
// Prompt if we would modify non-selected pads:
// - Ignore Connected Pins (Yes/Enter - default)
// - Swap Connected Pins (No)
// - Cancel (Esc)
bool includeConnectedPads = true;
if( !nonSelectedPadsToChange.empty() )
{
// Deduplicate and format a list of affected pads (reference + pad number)
std::unordered_set<PAD*> uniquePads( nonSelectedPadsToChange.begin(),
nonSelectedPadsToChange.end() );
wxString msg;
msg.Printf( _( "%zu other connected pad(s) will also change nets. How do you want to proceed?" ),
uniquePads.size() );
wxString details;
details << _( "Affected pads:" ) << '\n';
for( PAD* p : uniquePads )
{
const FOOTPRINT* fp = p->GetParentFootprint();
details << wxS( "" ) << ( fp ? fp->GetReference() : _( "<no reference designator>" ) )
<< wxS( ":" ) << p->GetNumber() << '\n';
}
wxRichMessageDialog dlg( frame(), msg, _( "Swap Pad Nets" ),
wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING );
dlg.SetYesNoLabels( _( "Ignore Connected Pins" ), _( "Swap Connected Pins" ) );
dlg.SetExtendedMessage( details );
int ret = dlg.ShowModal();
if( ret == wxID_CANCEL )
return 0;
// Yes = Ignore, No = Swap
includeConnectedPads = ( ret == wxID_NO );
}
// Apply changes
// 1) Selected pads get their new nets directly
for( size_t i = 0; i < padsCount; ++i )
{
commit->Modify( pads[i] );
pads[i]->SetNetCode( newNetForIndex( i ) );
}
// 2) Connected items propagate, depending on user choice
for( const auto& itemNewNet : itemNewNets )
{
BOARD_CONNECTED_ITEM* item = itemNewNet.first;
int newNet = itemNewNet.second;
if( item->Type() == PCB_PAD_T )
{
PAD* p = static_cast<PAD*>( item );
if( selectedPads.count( p ) )
continue; // already changed above
if( !includeConnectedPads )
continue; // skip non-selected pads if requested
}
commit->Modify( item );
item->SetNetCode( newNet );
}
if( !localCommit.Empty() )
localCommit.Push( _( "Swap Pad Nets" ) );
// Ensure connectivity visuals update
rebuildConnectivity();
m_toolMgr->ProcessEvent( EVENTS::SelectedItemsModified );
return 0;
}
int EDIT_TOOL::SwapGateNets( const TOOL_EVENT& aEvent )
{
if( isRouterActive() )
{
wxBell();
return 0;
}
auto showError = [this]()
{
frame()->ShowInfoBarError( _( "Gate swapping must be performed on pads within one multi-gate footprint." ) );
};
PCB_SELECTION& selection = m_selectionTool->RequestSelection( &EDIT_TOOL::PadFilter );
// Get our sanity checks out of the way to clean up later loops
FOOTPRINT* targetFp = nullptr;
bool fail = false;
for( EDA_ITEM* it : selection )
{
// This shouldn't happen due to the filter, but just in case
if( it->Type() != PCB_PAD_T )
{
fail = true;
break;
}
FOOTPRINT* fp = static_cast<PAD*>( static_cast<BOARD_ITEM*>( it ) )->GetParentFootprint();
if( !targetFp )
targetFp = fp;
else if( fp && targetFp != fp )
{
fail = true;
break;
}
}
if( fail || !targetFp || targetFp->GetUnitInfo().size() < 2 )
{
showError();
return 0;
}
const auto& units = targetFp->GetUnitInfo();
// Collect unit hits and ordered unit list based on selection order
std::vector<bool> unitHit( units.size(), false );
std::vector<int> unitOrder;
std::vector<EDA_ITEM*> orderedPads = selection.GetItemsSortedBySelectionOrder();
for( EDA_ITEM* it : orderedPads )
{
PAD* pad = static_cast<PAD*>( static_cast<BOARD_ITEM*>( it ) );
const wxString& padNum = pad->GetNumber();
int unitIdx = -1;
for( size_t i = 0; i < units.size(); ++i )
{
for( const auto& p : units[i].m_pins )
{
if( p == padNum )
{
unitIdx = static_cast<int>( i );
if( !unitHit[i] )
unitOrder.push_back( unitIdx );
unitHit[i] = true;
break;
}
}
if( unitIdx >= 0 )
break;
}
}
// Determine active units from selection order: 0 -> bail, 1 -> single-unit flow, 2+ -> cycle
std::vector<int> activeUnitIdx;
int sourceIdx = -1;
if( unitOrder.size() >= 2 )
{
activeUnitIdx = unitOrder;
sourceIdx = unitOrder.front();
}
// If we only have one gate selected, we must have a target unit name parameter to proceed
else if( unitOrder.size() == 1 && aEvent.HasParameter() )
{
sourceIdx = unitOrder.front();
wxString targetUnitByName = aEvent.Parameter<wxString>();
int targetIdx = -1;
for( size_t i = 0; i < units.size(); ++i )
{
if( static_cast<int>( i ) == sourceIdx )
continue;
if( units[i].m_pins.size() == units[sourceIdx].m_pins.size() && units[i].m_unitName == targetUnitByName )
{
targetIdx = static_cast<int>( i );
}
}
if( targetIdx < 0 )
{
showError();
return 0;
}
activeUnitIdx.push_back( sourceIdx );
activeUnitIdx.push_back( targetIdx );
}
else
{
showError();
return 0;
}
// Verify equal pin counts across all active units
const size_t pinCount = units[activeUnitIdx.front()].m_pins.size();
for( int idx : activeUnitIdx )
{
if( units[idx].m_pins.size() != pinCount )
{
frame()->ShowInfoBarError( _( "Gate swapping must be performed on gates with equal pin counts." ) );
return 0;
}
}
// Build per-unit pad arrays and net vectors
const size_t unitCount = activeUnitIdx.size();
std::vector<std::vector<PAD*>> unitPads( unitCount );
std::vector<std::vector<int>> unitNets( unitCount );
for( size_t ui = 0; ui < unitCount; ++ui )
{
int uidx = activeUnitIdx[ui];
const auto& pins = units[uidx].m_pins;
for( size_t pi = 0; pi < pinCount; ++pi )
{
PAD* p = targetFp->FindPadByNumber( pins[pi] );
if( !p )
{
frame()->ShowInfoBarError( _( "Gate swapping failed: pad in unit missing from footprint." ) );
return 0;
}
unitPads[ui].push_back( p );
unitNets[ui].push_back( p->GetNetCode() );
}
}
// If all unit nets match across positions, nothing to do
bool allSame = true;
for( size_t pi = 0; pi < pinCount && allSame; ++pi )
{
int refNet = unitNets[0][pi];
for( size_t ui = 1; ui < unitCount; ++ui )
{
if( unitNets[ui][pi] != refNet )
{
allSame = false;
break;
}
}
}
if( allSame )
{
frame()->ShowInfoBarError( _( "Gate swapping has no effect: all selected gates have identical nets." ) );
return 0;
}
// TODO: someday support swapping while routing and take that commit
BOARD_COMMIT localCommit( this );
BOARD_COMMIT* commit = dynamic_cast<BOARD_COMMIT*>( aEvent.Commit() );
if( !commit )
commit = &localCommit;
std::shared_ptr<CONNECTIVITY_DATA> connectivity = board()->GetConnectivity();
// Accumulate changes: item -> new net
std::unordered_map<BOARD_CONNECTED_ITEM*, int> itemNewNets;
std::vector<PAD*> nonSelectedPadsToChange;
// Selected pads in the swap (for suppressing re-adding in connected pad handling)
std::unordered_set<PAD*> swapPads;
for( const auto& v : unitPads )
swapPads.insert( v.begin(), v.end() );
// Schedule net swaps for connectivity-attached items
auto scheduleForPad = [&]( PAD* pad, int fromNet, int toNet )
{
for( BOARD_CONNECTED_ITEM* ci : connectivity->GetConnectedItems( pad, 0 ) )
{
switch( ci->Type() )
{
case PCB_TRACE_T:
case PCB_ARC_T:
case PCB_VIA_T:
case PCB_PAD_T:
break;
default:
continue;
}
if( ci->GetNetCode() != fromNet )
continue;
itemNewNets[ ci ] = toNet;
if( ci->Type() == PCB_PAD_T )
{
PAD* other = static_cast<PAD*>( ci );
if( !swapPads.count( other ) )
nonSelectedPadsToChange.push_back( other );
}
}
};
// For each position, rotate nets among units forward
for( size_t pi = 0; pi < pinCount; ++pi )
{
for( size_t ui = 0; ui < unitCount; ++ui )
{
size_t fromIdx = ui;
size_t toIdx = ( ui + 1 ) % unitCount;
PAD* padFrom = unitPads[fromIdx][pi];
int fromNet = unitNets[fromIdx][pi];
int toNet = unitNets[toIdx][pi];
scheduleForPad( padFrom, fromNet, toNet );
}
}
bool includeConnectedPads = true;
if( !nonSelectedPadsToChange.empty() )
{
std::unordered_set<PAD*> uniquePads( nonSelectedPadsToChange.begin(), nonSelectedPadsToChange.end() );
wxString msg;
msg.Printf( _( "%zu other connected pad(s) will also change nets. How do you want to proceed?" ),
uniquePads.size() );
wxString details;
details << _( "Affected pads:" ) << '\n';
for( PAD* p : uniquePads )
{
const FOOTPRINT* fp = p->GetParentFootprint();
details << wxS( "" ) << ( fp ? fp->GetReference() : _( "<no reference designator>" ) )
<< wxS( ":" ) << p->GetNumber() << '\n';
}
wxRichMessageDialog dlg( frame(), msg, _( "Swap Gate Nets" ),
wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING );
dlg.SetYesNoLabels( _( "Ignore Connected Pins" ), _( "Swap Connected Pins" ) );
dlg.SetExtendedMessage( details );
int ret = dlg.ShowModal();
if( ret == wxID_CANCEL )
return 0;
includeConnectedPads = ( ret == wxID_NO );
}
// Apply pad net swaps: rotate per position
for( size_t pi = 0; pi < pinCount; ++pi )
{
// First write back nets for each unit's pad at this position
for( size_t ui = 0; ui < unitCount; ++ui )
{
size_t toIdx = ( ui + 1 ) % unitCount;
PAD* pad = unitPads[ui][pi];
int newNet = unitNets[toIdx][pi];
commit->Modify( pad );
pad->SetNetCode( newNet );
}
}
// Apply connected items
for( const auto& kv : itemNewNets )
{
BOARD_CONNECTED_ITEM* item = kv.first;
int newNet = kv.second;
if( item->Type() == PCB_PAD_T )
{
PAD* p = static_cast<PAD*>( item );
if( swapPads.count( p ) )
continue;
if( !includeConnectedPads )
continue;
}
commit->Modify( item );
item->SetNetCode( newNet );
}
if( !localCommit.Empty() )
localCommit.Push( _( "Swap Gate Nets" ) );
rebuildConnectivity();
m_toolMgr->ProcessEvent( EVENTS::SelectedItemsModified );
return 0;
}
int EDIT_TOOL::PackAndMoveFootprints( const TOOL_EVENT& aEvent )
{
if( isRouterActive() || m_dragging )
@ -868,4 +1390,3 @@ bool EDIT_TOOL::doMoveSelection( const TOOL_EVENT& aEvent, BOARD_COMMIT* aCommit
return !restore_state;
}

View File

@ -686,6 +686,21 @@ TOOL_ACTION PCB_ACTIONS::swap( TOOL_ACTION_ARGS()
.Tooltip( _( "Swap positions of selected items" ) )
.Icon( BITMAPS::swap ) );
TOOL_ACTION PCB_ACTIONS::swapPadNets( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.swapPadNets" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Swap Pad Nets" ) )
.Tooltip( _( "Swap nets between two selected pads and their connected copper" ) )
.Icon( BITMAPS::swap ) );
TOOL_ACTION PCB_ACTIONS::swapGateNets( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.swapGateNets" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Swap Gate Nets" ) )
.Tooltip( _( "Swap nets between gates of a footprint and their connected copper" ) )
.Parameter<wxString>( wxString() )
.Icon( BITMAPS::swap ) );
TOOL_ACTION PCB_ACTIONS::packAndMoveFootprints( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.packAndMoveFootprints" )
.Scope( AS_GLOBAL )

View File

@ -128,6 +128,10 @@ public:
/// Swapping of selected items
static TOOL_ACTION swap;
/// Swap nets between selected pads/gates (and connected copper)
static TOOL_ACTION swapPadNets;
static TOOL_ACTION swapGateNets;
/// Pack and start moving selected footprints
static TOOL_ACTION packAndMoveFootprints;

View File

@ -15,20 +15,6 @@
// To maximize reusability:
// DO NOT ADD CODE THAT REQUIRES C++ EXCEPTION HANDLING.
// Disable linking to pythonX_d.lib on Windows in debug mode.
#if defined(_MSC_VER) && defined(_DEBUG) && !defined(Py_DEBUG)
// Workaround for a VS 2022 issue.
// See https://github.com/pybind/pybind11/pull/3497 for full context.
// NOTE: This workaround knowingly violates the Python.h include order
// requirement (see above).
# include <yvals.h>
# if _MSVC_STL_VERSION >= 143
# include <crtdefs.h>
# endif
# define PYBIND11_DEBUG_MARKER
# undef _DEBUG
#endif
// Don't let Python.h #define (v)snprintf as macro because they are implemented
// properly in Visual Studio since 2015.
#if defined(_MSC_VER)