ADDED: Stacked pin notation support

Implement bracket notation for stacked pins ([1,2,3], [1-4], [1,3,5-7]).
Automatic net naming proceeds based on the smallest logical pin number
in stacked groups.

Provide explode/reform commands in symbol editor for conversion.
Supports arbitrary ranges including BGA alphanum ranges like
[AA1-AA3,CD14-CD22]

Adds some additional QA and trace logging

Fixes https://gitlab.com/kicad/code/kicad/-/issues/2004
This commit is contained in:
Seth Hillbrand 2025-09-04 17:32:04 -07:00
parent ac1b44715b
commit f66cbaf43a
49 changed files with 6379 additions and 286 deletions

View File

@ -38,6 +38,7 @@
#include <fmt/chrono.h>
#include <wx/log.h>
#include <wx/regex.h>
#include <wx/tokenzr.h>
#include "locale_io.h"
@ -1503,3 +1504,124 @@ wxString NormalizeFileUri( const wxString& aFileUri )
return retv;
}
namespace
{
// Extract (prefix, numericValue) where numericValue = -1 if no numeric suffix
std::pair<wxString, long> ParseAlphaNumericPin( const wxString& pinNum )
{
wxString prefix;
long numValue = -1;
size_t numStart = pinNum.length();
for( int i = static_cast<int>( pinNum.length() ) - 1; i >= 0; --i )
{
if( !wxIsdigit( pinNum[i] ) )
{
numStart = i + 1;
break;
}
if( i == 0 )
numStart = 0; // all digits
}
if( numStart < pinNum.length() )
{
prefix = pinNum.Left( numStart );
wxString numericPart = pinNum.Mid( numStart );
numericPart.ToLong( &numValue );
}
return { prefix, numValue };
}
}
std::vector<wxString> ExpandStackedPinNotation( const wxString& aPinName, bool* aValid )
{
if( aValid )
*aValid = true;
std::vector<wxString> expanded;
const bool hasOpenBracket = aPinName.Contains( wxT( "[" ) );
const bool hasCloseBracket = aPinName.Contains( wxT( "]" ) );
if( hasOpenBracket || hasCloseBracket )
{
if( !aPinName.StartsWith( wxT( "[" ) ) || !aPinName.EndsWith( wxT( "]" ) ) )
{
if( aValid )
*aValid = false;
expanded.push_back( aPinName );
return expanded;
}
}
if( !aPinName.StartsWith( wxT( "[" ) ) || !aPinName.EndsWith( wxT( "]" ) ) )
{
expanded.push_back( aPinName );
return expanded;
}
const wxString inner = aPinName.Mid( 1, aPinName.Length() - 2 );
size_t start = 0;
while( start < inner.length() )
{
size_t comma = inner.find( ',', start );
wxString part = ( comma == wxString::npos ) ? inner.Mid( start ) : inner.Mid( start, comma - start );
part.Trim( true ).Trim( false );
if( part.empty() )
{
start = ( comma == wxString::npos ) ? inner.length() : comma + 1;
continue;
}
int dashPos = part.Find( '-' );
if( dashPos != wxNOT_FOUND )
{
wxString startTxt = part.Left( dashPos );
wxString endTxt = part.Mid( dashPos + 1 );
startTxt.Trim( true ).Trim( false );
endTxt.Trim( true ).Trim( false );
auto [startPrefix, startVal] = ParseAlphaNumericPin( startTxt );
auto [endPrefix, endVal] = ParseAlphaNumericPin( endTxt );
if( startPrefix != endPrefix || startVal == -1 || endVal == -1 || startVal > endVal )
{
if( aValid )
*aValid = false;
expanded.clear();
expanded.push_back( aPinName );
return expanded;
}
for( long ii = startVal; ii <= endVal; ++ii )
{
if( startPrefix.IsEmpty() )
expanded.emplace_back( wxString::Format( wxT( "%ld" ), ii ) );
else
expanded.emplace_back( wxString::Format( wxT( "%s%ld" ), startPrefix, ii ) );
}
}
else
{
expanded.push_back( part );
}
if( comma == wxString::npos )
break;
start = comma + 1;
}
if( expanded.empty() )
{
expanded.push_back( aPinName );
if( aValid )
*aValid = false;
}
return expanded;
}

View File

@ -62,6 +62,7 @@
#include <wx/stattext.h>
#include <wx/button.h>
#include <wx/msgdlg.h>
#include <wx/ffile.h>
CVPCB_MAINFRAME::CVPCB_MAINFRAME( KIWAY* aKiway, wxWindow* aParent ) :
@ -796,7 +797,12 @@ void CVPCB_MAINFRAME::DisplayStatus()
msg.Empty();
if( symbol )
msg = wxString::Format( wxT( "%i" ), symbol->GetPinCount() );
{
int pc = symbol->GetPinCount();
wxLogTrace( "CVPCB_PINCOUNT", wxT( "DisplayStatus: selected '%s' pinCount=%d" ),
symbol->GetReference(), pc );
msg = wxString::Format( wxT( "%i" ), pc );
}
if( !filters.IsEmpty() )
filters += wxT( ", " );
@ -957,6 +963,12 @@ int CVPCB_MAINFRAME::readSchematicNetlist( const std::string& aNetlist )
m_netlist.Clear();
// Trace basic payload characteristics to verify libparts are present and visible here
wxLogTrace( "CVPCB_PINCOUNT",
wxT( "readSchematicNetlist: payload size=%zu has_libparts=%d has_libpart=%d" ),
aNetlist.size(), aNetlist.find( "(libparts" ) != std::string::npos,
aNetlist.find( "(libpart" ) != std::string::npos );
try
{
netlistReader.LoadNetlist();

View File

@ -132,7 +132,13 @@ void FOOTPRINTS_LISTBOX::SetFootprints( FOOTPRINT_LIST& aList, const wxString& a
filter.FilterByFootprintFilters( aComponent->GetFootprintFilters() );
if( aFilterType & FILTERING_BY_PIN_COUNT && aComponent )
filter.FilterByPinCount( aComponent->GetPinCount() );
{
int pc = aComponent->GetPinCount();
wxLogTrace( "CVPCB_PINCOUNT",
wxT( "FOOTPRINTS_LISTBOX::SetFootprints: ref='%s' pinCount filter=%d" ),
aComponent->GetReference(), pc );
filter.FilterByPinCount( pc );
}
if( aFilterType & FILTERING_BY_LIBRARY )
filter.FilterByLibrary( aLibName );

View File

@ -175,11 +175,28 @@ DIALOG_FIELD_PROPERTIES::DIALOG_FIELD_PROPERTIES( SCH_BASE_FRAME* aParent, const
wxString netlist;
wxArrayString pins;
for( SCH_PIN* pin : symbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
for( SCH_PIN* pin : symbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
{
bool valid = false;
auto expanded = pin->GetStackedPinNumbers( &valid );
if( valid && !expanded.empty() )
{
for( const wxString& num : expanded )
pins.push_back( num + ' ' + pin->GetShownName() );
}
else
{
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
}
}
if( !pins.IsEmpty() )
netlist << EscapeString( wxJoin( pins, '\t' ), CTX_LINE );
{
wxString dbg = wxJoin( pins, '\t' );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Chooser payload pins (LIB_SYMBOL): %s" ), dbg );
netlist << EscapeString( dbg, CTX_LINE );
}
netlist << wxS( "\r" );
@ -213,12 +230,28 @@ DIALOG_FIELD_PROPERTIES::DIALOG_FIELD_PROPERTIES( SCH_BASE_FRAME* aParent, const
if( lib_symbol )
{
for( SCH_PIN* pin : lib_symbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
for( SCH_PIN* pin : lib_symbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
{
bool valid = false;
auto expanded = pin->GetStackedPinNumbers( &valid );
if( valid && !expanded.empty() )
{
for( const wxString& num : expanded )
pins.push_back( num + ' ' + pin->GetShownName() );
}
else
{
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
}
}
}
if( !pins.IsEmpty() )
netlist << EscapeString( wxJoin( pins, '\t' ), CTX_LINE );
{
wxString dbg = wxJoin( pins, '\t' );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Chooser payload pins (SCH_SYMBOL): %s" ), dbg );
netlist << EscapeString( dbg, CTX_LINE );
}
netlist << wxS( "\r" );

View File

@ -402,7 +402,7 @@ void DIALOG_LIB_FIELDS_TABLE::SetupColumnProperties( int aCol )
LIB_SYMBOL* symbol = m_symbolsList[0];
wxArrayString pins;
for( SCH_PIN* pin : symbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
for( SCH_PIN* pin : symbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
if( !pins.IsEmpty() )

View File

@ -631,7 +631,7 @@ int ERC_TESTER::TestMissingUnits()
}
}
for( SCH_PIN* pin : libSymbol->GetPins( missing_unit, bodyStyle ) )
for( SCH_PIN* pin : libSymbol->GetGraphicalPins( missing_unit, bodyStyle ) )
{
switch( pin->GetType() )
{
@ -1295,6 +1295,44 @@ int ERC_TESTER::TestGroundPins()
}
int ERC_TESTER::TestStackedPinNotation()
{
int warnings = 0;
for( const SCH_SHEET_PATH& sheet : m_sheetList )
{
SCH_SCREEN* screen = sheet.LastScreen();
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
{
SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
for( SCH_PIN* pin : symbol->GetPins( &sheet ) )
{
bool valid;
pin->GetStackedPinNumbers( &valid );
if( !valid )
{
std::shared_ptr<ERC_ITEM> ercItem =
ERC_ITEM::Create( ERCE_STACKED_PIN_SYNTAX );
ercItem->SetItems( pin );
ercItem->SetSheetSpecificPath( sheet );
ercItem->SetItemsSheetPaths( sheet );
SCH_MARKER* marker =
new SCH_MARKER( std::move( ercItem ), pin->GetPosition() );
screen->Append( marker );
warnings++;
}
}
}
}
return warnings;
}
int ERC_TESTER::TestSameLocalGlobalLabel()
{
int errCount = 0;
@ -1961,6 +1999,9 @@ void ERC_TESTER::RunTests( DS_PROXY_VIEW_ITEM* aDrawingSheet, SCH_EDIT_FRAME* aE
if( m_settings.IsTestEnabled( ERCE_GROUND_PIN_NOT_GROUND ) )
TestGroundPins();
if( m_settings.IsTestEnabled( ERCE_STACKED_PIN_SYNTAX ) )
TestStackedPinNotation();
// Test similar labels (i;e. labels which are identical when
// using case insensitive comparisons)
if( m_settings.IsTestEnabled( ERCE_SIMILAR_LABELS )

View File

@ -118,6 +118,12 @@ public:
*/
int TestGroundPins();
/**
* Checks for pin numbers that resemble stacked pin notation but are invalid.
* @return warning count
*/
int TestStackedPinNotation();
/**
* Checks for global and local labels with the same name
* @return the error count

View File

@ -161,6 +161,10 @@ ERC_ITEM ERC_ITEM::groundPinNotGround( ERCE_GROUND_PIN_NOT_GROUND,
_HKI( "Ground pin not connected to ground net" ),
wxT( "ground_pin_not_ground" ) );
ERC_ITEM ERC_ITEM::stackedPinName( ERCE_STACKED_PIN_SYNTAX,
_HKI( "Pin name resembles stacked pin" ),
wxT( "stacked_pin_name" ) );
ERC_ITEM ERC_ITEM::unresolvedVariable( ERCE_UNRESOLVED_VARIABLE,
_HKI( "Unresolved text variable" ),
wxT( "unresolved_variable" ) );
@ -268,6 +272,7 @@ std::vector<std::reference_wrapper<RC_ITEM>> ERC_ITEM::allItemTypes(
ERC_ITEM::groundPinNotGround,
ERC_ITEM::heading_misc,
ERC_ITEM::stackedPinName,
ERC_ITEM::unannotated,
ERC_ITEM::unresolvedVariable,
ERC_ITEM::undefinedNetclass,
@ -351,6 +356,7 @@ std::shared_ptr<ERC_ITEM> ERC_ITEM::Create( int aErrorCode )
case ERCE_MISSING_POWER_INPUT_PIN: return std::make_shared<ERC_ITEM>( missingPowerInputPin );
case ERCE_MISSING_BIDI_PIN: return std::make_shared<ERC_ITEM>( missingBidiPin );
case ERCE_UNCONNECTED_WIRE_ENDPOINT: return std::make_shared<ERC_ITEM>( unconnectedWireEndpoint );
case ERCE_STACKED_PIN_SYNTAX: return std::make_shared<ERC_ITEM>( stackedPinName );
case ERCE_UNSPECIFIED:
default:
wxFAIL_MSG( wxS( "Unknown ERC error code" ) );

View File

@ -227,6 +227,7 @@ private:
static ERC_ITEM busToBusConflict;
static ERC_ITEM busToNetConflict;
static ERC_ITEM groundPinNotGround;
static ERC_ITEM stackedPinName;
static ERC_ITEM unresolvedVariable;
static ERC_ITEM undefinedNetclass;
static ERC_ITEM simulationModelIssues;

View File

@ -120,6 +120,7 @@ ERC_SETTINGS::ERC_SETTINGS( JSON_SETTINGS* aParent, const std::string& aPath ) :
m_ERCSeverities[ERCE_FOUR_WAY_JUNCTION] = RPT_SEVERITY_IGNORE;
m_ERCSeverities[ERCE_LABEL_MULTIPLE_WIRES] = RPT_SEVERITY_WARNING;
m_ERCSeverities[ERCE_UNCONNECTED_WIRE_ENDPOINT] = RPT_SEVERITY_WARNING;
m_ERCSeverities[ERCE_STACKED_PIN_SYNTAX] = RPT_SEVERITY_WARNING;
m_params.emplace_back( new PARAM_LAMBDA<nlohmann::json>( "rule_severities",
[&]() -> nlohmann::json

View File

@ -87,8 +87,9 @@ enum ERCE_T
ERCE_FOUR_WAY_JUNCTION, ///< A four-way junction was found.
ERCE_LABEL_MULTIPLE_WIRES, ///< A label is connected to more than one wire.
ERCE_UNCONNECTED_WIRE_ENDPOINT, ///< A label is connected to more than one wire.
ERCE_STACKED_PIN_SYNTAX, ///< Pin name resembles stacked pin notation.
ERCE_LAST = ERCE_UNCONNECTED_WIRE_ENDPOINT,
ERCE_LAST = ERCE_STACKED_PIN_SYNTAX,
ERCE_DUPLICATE_PIN_ERROR,
ERCE_PIN_TO_PIN_WARNING, // pin connected to an other pin: warning level

View File

@ -79,12 +79,29 @@ static wxString netList( SCH_SYMBOL* aSymbol, SCH_SHEET_PATH& aSheetPath )
if( lib_symbol )
{
for( SCH_PIN* pin : lib_symbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
for( SCH_PIN* pin : lib_symbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
{
bool valid = false;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &valid );
if( valid && !expanded.empty() )
{
for( const wxString& num : expanded )
pins.push_back( num + ' ' + pin->GetShownName() );
}
else
{
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
}
}
}
if( !pins.IsEmpty() )
netlist << EscapeString( wxJoin( pins, '\t' ), CTX_LINE );
{
wxString dbg = wxJoin( pins, '\t' );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Chooser payload pins: %s" ), dbg );
netlist << EscapeString( dbg, CTX_LINE );
}
netlist << wxS( "\r" );
@ -112,11 +129,28 @@ static wxString netList( LIB_SYMBOL* aSymbol )
wxString netlist;
wxArrayString pins;
for( SCH_PIN* pin : aSymbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
for( SCH_PIN* pin : aSymbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ) )
{
bool valid = false;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &valid );
if( valid && !expanded.empty() )
{
for( const wxString& num : expanded )
pins.push_back( num + ' ' + pin->GetShownName() );
}
else
{
pins.push_back( pin->GetNumber() + ' ' + pin->GetShownName() );
}
}
if( !pins.IsEmpty() )
netlist << EscapeString( wxJoin( pins, '\t' ), CTX_LINE );
{
wxString dbg = wxJoin( pins, '\t' );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Chooser payload pins: %s" ), dbg );
netlist << EscapeString( dbg, CTX_LINE );
}
netlist << wxS( "\r" );

View File

@ -782,7 +782,7 @@ void LIB_SYMBOL::AddDrawItem( SCH_ITEM* aItem, bool aSort )
}
std::vector<SCH_PIN*> LIB_SYMBOL::GetPins( int aUnit, int aBodyStyle ) const
std::vector<SCH_PIN*> LIB_SYMBOL::GetGraphicalPins( int aUnit, int aBodyStyle ) const
{
std::vector<SCH_PIN*> pins;
@ -807,28 +807,75 @@ std::vector<SCH_PIN*> LIB_SYMBOL::GetPins( int aUnit, int aBodyStyle ) const
continue;
// TODO: get rid of const_cast. (It used to be a C-style cast so was less noticeable.)
pins.push_back( const_cast<SCH_PIN*>( static_cast<const SCH_PIN*>( &item ) ) );
SCH_PIN* pin = const_cast<SCH_PIN*>( static_cast<const SCH_PIN*>( &item ) );
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "GetGraphicalPins: lib='%s' unit=%d body=%d -> include pin name='%s' number='%s' shownNum='%s'",
GetLibId().Format().wx_str(), aUnit, aBodyStyle,
pin->GetName(), pin->GetNumber(), pin->GetShownNumber() ) );
pins.push_back( pin );
}
return pins;
}
std::vector<SCH_PIN*> LIB_SYMBOL::GetPins() const
std::vector<LIB_SYMBOL::LOGICAL_PIN> LIB_SYMBOL::GetLogicalPins( int aUnit, int aBodyStyle ) const
{
return GetPins( 0, 0 );
std::vector<LOGICAL_PIN> out;
for( SCH_PIN* pin : GetGraphicalPins( aUnit, aBodyStyle ) )
{
bool valid = false;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &valid );
if( valid && !expanded.empty() )
{
for( const wxString& num : expanded )
{
out.push_back( LOGICAL_PIN{ pin, num } );
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "GetLogicalPins: base='%s' -> '%s'",
pin->GetShownNumber(), num ) );
}
}
else
{
out.push_back( LOGICAL_PIN{ pin, pin->GetShownNumber() } );
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "GetLogicalPins: base='%s' (no expansion)",
pin->GetShownNumber() ) );
}
}
return out;
}
int LIB_SYMBOL::GetPinCount()
{
return (int) GetPins( 0 /* all units */, 1 /* single body style */ ).size();
int count = 0;
for( SCH_PIN* pin : GetGraphicalPins( 0 /* all units */, 1 /* single body style */ ) )
{
bool valid;
std::vector<wxString> numbers = pin->GetStackedPinNumbers( &valid );
wxLogTrace( "CVPCB_PINCOUNT",
wxString::Format( "LIB_SYMBOL::GetPinCount lib='%s' pin base='%s' shown='%s' valid=%d +%zu",
GetLibId().Format().wx_str(), pin->GetName(),
pin->GetShownNumber(), valid, numbers.size() ) );
count += numbers.size();
}
wxLogTrace( "CVPCB_PINCOUNT",
wxString::Format( "LIB_SYMBOL::GetPinCount total for lib='%s' => %d",
GetLibId().Format().wx_str(), count ) );
return count;
}
SCH_PIN* LIB_SYMBOL::GetPin( const wxString& aNumber, int aUnit, int aBodyStyle ) const
{
for( SCH_PIN* pin : GetPins( aUnit, aBodyStyle ) )
for( SCH_PIN* pin : GetGraphicalPins( aUnit, aBodyStyle ) )
{
if( aNumber == pin->GetNumber() )
return pin;
@ -841,12 +888,12 @@ SCH_PIN* LIB_SYMBOL::GetPin( const wxString& aNumber, int aUnit, int aBodyStyle
bool LIB_SYMBOL::PinsConflictWith( const LIB_SYMBOL& aOtherPart, bool aTestNums, bool aTestNames,
bool aTestType, bool aTestOrientation, bool aTestLength ) const
{
for( const SCH_PIN* pin : GetPins() )
for( const SCH_PIN* pin : GetGraphicalPins() )
{
wxASSERT( pin );
bool foundMatch = false;
for( const SCH_PIN* otherPin : aOtherPart.GetPins() )
for( const SCH_PIN* otherPin : aOtherPart.GetGraphicalPins() )
{
wxASSERT( otherPin );
@ -899,6 +946,12 @@ bool LIB_SYMBOL::PinsConflictWith( const LIB_SYMBOL& aOtherPart, bool aTestNums,
return false;
}
std::vector<SCH_PIN*> LIB_SYMBOL::GetPins() const
{
// Back-compat shim: return graphical pins for all units/body styles
return GetGraphicalPins( 0, 0 );
}
const BOX2I LIB_SYMBOL::GetUnitBoundingBox( int aUnit, int aBodyStyle,
bool aIgnoreHiddenFields,

View File

@ -416,21 +416,32 @@ public:
void RemoveField( SCH_FIELD* aField ) { RemoveDrawItem( aField ); }
/**
* Return a list of pin object pointers from the draw item list.
* Graphical pins: Return schematic pin objects as drawn (unexpanded), filtered by unit/body.
*
* Note pin objects are owned by the draw list of the symbol. Deleting any of the objects
* will leave list in a unstable state and will likely segfault when the list is destroyed.
* Note: pin objects are owned by the symbol's draw list; do not delete them.
*
* @param aUnit - Unit number of pins to collect. Set to 0 to get pins from all symbol units.
* @param aBodyStyle - Symbol alternate body style of pins to collect. Set to 0 to get pins
* from all body styles.
* @param aUnit Unit number to collect; 0 = all units
* @param aBodyStyle Alternate body style to collect; 0 = all body styles
*/
std::vector<SCH_PIN*> GetPins( int aUnit, int aBodyStyle ) const;
std::vector<SCH_PIN*> GetGraphicalPins( int aUnit = 0, int aBodyStyle = 0 ) const;
/**
* Return a list of pin pointers for all units / converts. Used primarily for SPICE where
* we want to treat all unit as a single part.
* Logical pins: Return expanded logical pins based on stacked-pin notation.
* Each returned item pairs a base graphical pin with a single expanded logical number.
*/
struct LOGICAL_PIN
{
SCH_PIN* pin; ///< pointer to the base graphical pin
wxString number; ///< expanded logical pin number
};
/**
* Return all logical pins (expanded) filtered by unit/body.
* For non-stacked pins, the single logical pin's number equals the base pin number.
*/
std::vector<LOGICAL_PIN> GetLogicalPins( int aUnit, int aBodyStyle ) const;
// Deprecated: use GetGraphicalPins(). This override remains to satisfy SYMBOL's pure virtual.
std::vector<SCH_PIN*> GetPins() const override;
/**

View File

@ -0,0 +1,67 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers
*
* 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 at
* http://www.gnu.org/licenses/
*/
#include "multiline_pin_text.h"
#include <wx/tokenzr.h>
MULTILINE_PIN_TEXT_LAYOUT ComputeMultiLinePinNumberLayout( const wxString& aText,
const VECTOR2D& aAnchorPos, const TEXT_ATTRIBUTES& aAttrs )
{
MULTILINE_PIN_TEXT_LAYOUT layout;
layout.m_StartPos = aAnchorPos;
if( !( aText.StartsWith( "[" ) && aText.EndsWith( "]" ) && aText.Contains( "\n" ) ) )
return layout; // not multi-line stacked
wxString content = aText.Mid( 1, aText.Length() - 2 );
wxArrayString lines; wxStringSplit( content, lines, '\n' );
if( lines.size() <= 1 )
return layout;
layout.m_IsMultiLine = true;
layout.m_Lines = lines;
for( size_t i = 0; i < layout.m_Lines.size(); ++i )
layout.m_Lines[i].Trim( true ).Trim( false );
layout.m_LineSpacing = KiROUND( aAttrs.m_Size.y * 1.3 );
// Apply alignment-dependent origin shift identical to sch_painter logic
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
int totalWidth = ( (int) layout.m_Lines.size() - 1 ) * layout.m_LineSpacing;
if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_RIGHT )
layout.m_StartPos.x -= totalWidth;
else if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_CENTER )
layout.m_StartPos.x -= totalWidth / 2;
}
else
{
int totalHeight = ( (int) layout.m_Lines.size() - 1 ) * layout.m_LineSpacing;
if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_BOTTOM )
layout.m_StartPos.y -= totalHeight;
else if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_CENTER )
layout.m_StartPos.y -= totalHeight / 2;
}
return layout;
}

View File

@ -0,0 +1,40 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers
*
* 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 at
* http://www.gnu.org/licenses/
*/
#pragma once
#include <wx/string.h>
#include <wx/arrstr.h>
#include <math/vector2d.h>
#include <font/text_attributes.h>
struct MULTILINE_PIN_TEXT_LAYOUT
{
bool m_IsMultiLine = false; // true if brace-wrapped multi-line stacked list
wxArrayString m_Lines; // individual numbered lines (trimmed)
VECTOR2D m_StartPos; // position used for line index 0 after alignment shift
int m_LineSpacing = 0; // inter-line spacing in IU (along secondary axis)
};
// Compute layout for a (possibly) multi-line stacked pin number string. If not multi-line, the
// returned layout has m_IsMultiLine=false and no further adjustments are required.
MULTILINE_PIN_TEXT_LAYOUT ComputeMultiLinePinNumberLayout( const wxString& aText,
const VECTOR2D& aAnchorPos, const TEXT_ATTRIBUTES& aAttrs );

View File

@ -176,7 +176,21 @@ std::vector<PIN_INFO> NETLIST_EXPORTER_BASE::CreatePinList( SCH_SYMBOL* aSymbol,
continue;
}
pins.emplace_back( pin->GetShownNumber(), netName );
bool valid;
std::vector<wxString> numbers = pin->GetStackedPinNumbers( &valid );
wxString baseName = pin->GetShownName();
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "CreatePinList(single): ref='%s' pinNameBase='%s' shownNum='%s' net='%s' "
"valid=%d expand=%zu",
ref, baseName, pin->GetShownNumber(), netName, valid, numbers.size() ) );
for( const wxString& num : numbers )
{
wxString pinName = baseName.IsEmpty() ? num : baseName + wxT( "_" ) + num;
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( " -> emit pin num='%s' name='%s' net='%s'", num, pinName, netName ) );
pins.emplace_back( num, netName, pinName );
}
}
}
}
@ -266,7 +280,20 @@ void NETLIST_EXPORTER_BASE::findAllUnitsOfSymbol( SCH_SYMBOL* aSchSymbol,
continue;
}
aPins.emplace_back( pin->GetShownNumber(), netName );
bool valid;
std::vector<wxString> numbers = pin->GetStackedPinNumbers( &valid );
wxString baseName = pin->GetShownName();
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "CreatePinList(multi): ref='%s' pinNameBase='%s' shownNum='%s' net='%s' valid=%d expand=%zu",
ref2, baseName, pin->GetShownNumber(), netName, valid, numbers.size() ) );
for( const wxString& num : numbers )
{
wxString pinName = baseName.IsEmpty() ? num : baseName + wxT( "_" ) + num;
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( " -> emit pin num='%s' name='%s' net='%s'", num, pinName, netName ) );
aPins.emplace_back( num, netName, pinName );
}
}
}
}

View File

@ -71,13 +71,15 @@ struct LIB_SYMBOL_LESS_THAN
struct PIN_INFO
{
PIN_INFO( const wxString& aPinNumber, const wxString& aNetName ) :
PIN_INFO( const wxString& aPinNumber, const wxString& aNetName, const wxString& aPinName ) :
num( aPinNumber ),
netName( aNetName )
netName( aNetName ),
pinName( aPinName )
{}
wxString num;
wxString netName;
wxString pinName;
};

View File

@ -495,7 +495,8 @@ XNODE* NETLIST_EXPORTER_XML::makeGroups()
XNODE* xcomps = node( wxT( "groups" ) );
m_referencesAlreadyFound.Clear();
m_libParts.clear();
// Do not clear m_libParts here: it is populated in makeSymbols() and used later by
// makeLibParts() to emit the libparts section for CvPcb and other consumers.
SCH_SHEET_PATH currentSheet = m_schematic->CurrentSheet();
SCH_SHEET_LIST sheetList = m_schematic->Hierarchy();
@ -732,7 +733,7 @@ XNODE* NETLIST_EXPORTER_XML::makeLibParts()
xlibpart->AddChild( node( wxT( "docs" ), lcomp->GetDatasheetField().GetText() ) );
// Write the footprint list
if( lcomp->GetFPFilters().GetCount() )
if( lcomp->GetFPFilters().GetCount() )
{
XNODE* xfootprints;
xlibpart->AddChild( xfootprints = node( wxT( "footprints" ) ) );
@ -758,8 +759,10 @@ XNODE* NETLIST_EXPORTER_XML::makeLibParts()
xfield->AddAttribute( wxT( "name" ), field->GetCanonicalName() );
}
//----- show the pins here ------------------------------------
std::vector<SCH_PIN*> pinList = lcomp->GetPins( 0, 0 );
//----- show the pins here ------------------------------------
// NOTE: Expand stacked-pin notation into individual pins so downstream
// tools (e.g. CvPcb) see the actual number of footprint pins.
std::vector<SCH_PIN*> pinList = lcomp->GetGraphicalPins( 0, 0 );
/*
* We must erase redundant Pins references in pinList
@ -780,6 +783,10 @@ XNODE* NETLIST_EXPORTER_XML::makeLibParts()
}
}
wxLogTrace( "CVPCB_PINCOUNT",
wxString::Format( "makeLibParts: lib='%s' part='%s' pinList(size)=%zu",
libNickname, lcomp->GetName(), pinList.size() ) );
if( pinList.size() )
{
XNODE* pins;
@ -788,12 +795,40 @@ XNODE* NETLIST_EXPORTER_XML::makeLibParts()
for( unsigned i=0; i<pinList.size(); ++i )
{
XNODE* pin;
SCH_PIN* basePin = pinList[i];
pins->AddChild( pin = node( wxT( "pin" ) ) );
pin->AddAttribute( wxT( "num" ), pinList[i]->GetShownNumber() );
pin->AddAttribute( wxT( "name" ), pinList[i]->GetShownName() );
pin->AddAttribute( wxT( "type" ), pinList[i]->GetCanonicalElectricalTypeName() );
bool stackedValid = false;
std::vector<wxString> expandedNums = basePin->GetStackedPinNumbers( &stackedValid );
// If stacked notation detected and valid, emit one libparts pin per expanded number.
if( stackedValid && !expandedNums.empty() )
{
for( const wxString& num : expandedNums )
{
XNODE* pin;
pins->AddChild( pin = node( wxT( "pin" ) ) );
pin->AddAttribute( wxT( "num" ), num );
pin->AddAttribute( wxT( "name" ), basePin->GetShownName() );
pin->AddAttribute( wxT( "type" ), basePin->GetCanonicalElectricalTypeName() );
wxLogTrace( "CVPCB_PINCOUNT",
wxString::Format( "makeLibParts: -> pin num='%s' name='%s' (expanded)",
num, basePin->GetShownName() ) );
}
}
else
{
XNODE* pin;
pins->AddChild( pin = node( wxT( "pin" ) ) );
pin->AddAttribute( wxT( "num" ), basePin->GetShownNumber() );
pin->AddAttribute( wxT( "name" ), basePin->GetShownName() );
pin->AddAttribute( wxT( "type" ), basePin->GetCanonicalElectricalTypeName() );
wxLogTrace( "CVPCB_PINCOUNT",
wxString::Format( "makeLibParts: -> pin num='%s' name='%s'",
basePin->GetShownNumber(),
basePin->GetShownName() ) );
}
// caution: construction work site here, drive slowly
}
@ -953,10 +988,9 @@ XNODE* NETLIST_EXPORTER_XML::makeListOfNets( unsigned aCtl )
} );
}
for( const NET_NODE& netNode : net_record->m_Nodes )
for( const NET_NODE& netNode : net_record->m_Nodes )
{
wxString refText = netNode.m_Pin->GetParentSymbol()->GetRef( &netNode.m_Sheet );
wxString pinText = netNode.m_Pin->GetShownNumber();
// Skip power symbols and virtual symbols
if( refText[0] == wxChar( '#' ) )
@ -974,21 +1008,39 @@ XNODE* NETLIST_EXPORTER_XML::makeListOfNets( unsigned aCtl )
added = true;
}
xnet->AddChild( xnode = node( wxT( "node" ) ) );
xnode->AddAttribute( wxT( "ref" ), refText );
xnode->AddAttribute( wxT( "pin" ), pinText );
std::vector<wxString> nums = netNode.m_Pin->GetStackedPinNumbers();
wxString baseName = netNode.m_Pin->GetShownName();
wxString pinType = netNode.m_Pin->GetCanonicalElectricalTypeName();
wxString pinName = netNode.m_Pin->GetShownName();
wxString pinType = netNode.m_Pin->GetCanonicalElectricalTypeName();
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "XML: net='%s' ref='%s' base='%s' shownNum='%s' expand=%zu",
net_record->m_Name, refText, baseName,
netNode.m_Pin->GetShownNumber(), nums.size() ) );
if( !pinName.IsEmpty() )
xnode->AddAttribute( wxT( "pinfunction" ), pinName );
for( const wxString& num : nums )
{
xnet->AddChild( xnode = node( wxT( "node" ) ) );
xnode->AddAttribute( wxT( "ref" ), refText );
xnode->AddAttribute( wxT( "pin" ), num );
if( net_record->m_HasNoConnect
&& ( net_record->m_Nodes.size() == 1 || allNetPinsStacked ) )
pinType += wxT( "+no_connect" );
wxString fullName = baseName.IsEmpty() ? num : baseName + wxT( "_" ) + num;
xnode->AddAttribute( wxT( "pintype" ), pinType );
if( !baseName.IsEmpty() || nums.size() > 1 )
xnode->AddAttribute( wxT( "pinfunction" ), fullName );
wxString typeAttr = pinType;
if( net_record->m_HasNoConnect
&& ( net_record->m_Nodes.size() == 1 || allNetPinsStacked ) )
{
typeAttr += wxT( "+no_connect" );
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "XML: marking node ref='%s' pin='%s' as no_connect",
refText, num ) );
}
xnode->AddAttribute( wxT( "pintype" ), typeAttr );
}
}
}

View File

@ -1,11 +1,11 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* Copyright The KiCad Developers
*
* 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
* 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,
@ -14,60 +14,149 @@
* 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
* along with this program; if not, you may find one at
* http://www.gnu.org/licenses/
*/
#include "pin_layout_cache.h"
#include <geometry/direction45.h>
#include <pgm_base.h>
#include <settings/settings_manager.h>
#include <sch_symbol.h>
#include <eeschema_settings.h>
#include <schematic_settings.h>
#include <string_utils.h>
#include <geometry/shape_utils.h>
// Small margin in internal units between the pin text and the pin line
static const int PIN_TEXT_MARGIN = 4;
namespace
// Forward declaration for helper implemented in sch_pin.cpp
wxString FormatStackedPinForDisplay( const wxString& aPinNumber, int aPinLength, int aTextSize,
KIFONT::FONT* aFont, const KIFONT::METRICS& aFontMetrics );
std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> PIN_LAYOUT_CACHE::GetPinNumberInfo( int aShadowWidth )
{
recomputeCaches();
// small margin in internal units between the pin text and the pin line
const int PIN_TEXT_MARGIN = 4;
wxString number = m_pin.GetShownNumber();
if( number.IsEmpty() || !m_pin.GetParentSymbol()->GetShowPinNumbers() )
return std::nullopt;
struct EXTENTS_CACHE
{
KIFONT::FONT* m_Font = nullptr;
int m_FontSize = 0;
VECTOR2I m_Extents;
};
// Format stacked representation if necessary
EESCHEMA_SETTINGS* cfg = GetAppSettings<EESCHEMA_SETTINGS>( "eeschema" );
KIFONT::FONT* font = KIFONT::FONT::GetFont( cfg ? cfg->m_Appearance.default_font : wxString( "" ) );
const KIFONT::METRICS& metrics = m_pin.GetFontMetrics();
wxString formatted = FormatStackedPinForDisplay( number, m_pin.GetLength(), m_pin.GetNumberTextSize(), font, metrics );
/// Utility for getting the size of the 'external' pin decorators (as a radius)
// i.e. the negation circle, the polarity 'slopes' and the nonlogic
// marker
int externalPinDecoSize( const SCHEMATIC_SETTINGS* aSettings, const SCH_PIN& aPin )
std::optional<TEXT_INFO> info = TEXT_INFO();
info->m_Text = formatted;
info->m_TextSize = m_pin.GetNumberTextSize();
info->m_Thickness = m_numberThickness;
info->m_HAlign = GR_TEXT_H_ALIGN_CENTER;
info->m_VAlign = GR_TEXT_V_ALIGN_CENTER;
PIN_ORIENTATION orient = m_pin.PinDrawOrient( DefaultTransform );
auto estimateQABox = [&]( const wxString& txt, int size, bool isVertical ) -> VECTOR2I
{
int h = size;
int w = (int) ( txt.Length() * size * 0.6 );
if( txt.Contains( '\n' ) )
{
wxArrayString lines; wxStringSplit( txt, lines, '\n' );
if( isVertical )
{
int lineSpacing = KiROUND( size * 1.3 );
w = (int) lines.size() * lineSpacing;
size_t maxLen = 0; for( const wxString& l : lines ) maxLen = std::max( maxLen, l.Length() );
h = (int) ( maxLen * size * 0.6 );
}
else
{
int lineSpacing = KiROUND( size * 1.3 );
h = (int) lines.size() * lineSpacing;
size_t maxLen = 0; for( const wxString& l : lines ) maxLen = std::max( maxLen, l.Length() );
w = (int) ( maxLen * size * 0.6 );
}
}
return VECTOR2I( w, h );
};
// Pass 1: determine maximum perpendicular half span among all pin numbers to ensure
// a single distance from the pin center that avoids overlap for every pin.
const SYMBOL* parentSym = m_pin.GetParentSymbol();
int maxHalfHeight = 0; // vertical half span across all numbers
int maxHalfWidth = 0; // horizontal half span across all numbers (for vertical pins overlap avoidance)
int maxFullHeight = 0; // full height (for dynamic clearance)
if( parentSym )
{
for( const SCH_PIN* p : parentSym->GetPins() )
{
wxString raw = p->GetShownNumber();
if( raw.IsEmpty() )
continue;
wxString fmt = FormatStackedPinForDisplay( raw, p->GetLength(), p->GetNumberTextSize(), font, p->GetFontMetrics() );
// Determine true max height regardless of rotation: use isVertical=false path for multiline height
VECTOR2I box = estimateQABox( fmt, p->GetNumberTextSize(), false );
maxHalfHeight = std::max( maxHalfHeight, box.y / 2 );
maxFullHeight = std::max( maxFullHeight, box.y );
maxHalfWidth = std::max( maxHalfWidth, box.x / 2 );
}
}
int clearance = getPinTextOffset() + schIUScale.MilsToIU( PIN_TEXT_MARGIN );
VECTOR2I pinPos = m_pin.GetPosition();
bool verticalOrient = ( orient == PIN_ORIENTATION::PIN_UP || orient == PIN_ORIENTATION::PIN_DOWN );
// We need the per-pin bounding width for vertical placement (rotated text). For vertical
// pins we anchor by the RIGHT edge of the text box so the gap from the pin to text is
// constant (clearance) independent of text width (multi-line vs single-line).
auto currentBox = estimateQABox( formatted, info->m_TextSize, verticalOrient );
if( verticalOrient )
{
// Vertical pins: text is placed to the LEFT (negative X) and rotated vertical so that it
// reads bottom->top when the schematic is in its canonical orientation. We right-edge
// align the text box at (pin.x - clearance) to keep a constant gap regardless of text width.
int boxWidth = currentBox.x;
int centerX = pinPos.x - clearance - boxWidth / 2;
info->m_TextPosition.x = centerX;
info->m_TextPosition.y = pinPos.y;
info->m_Angle = ANGLE_VERTICAL;
}
else
{
// Horizontal pins: "above" means negative Y direction. All numbers are centered on the
// pin X and share a Y offset derived from the maximum half height across all numbers so
// that multi-line and single-line numbers align cleanly.
int centerY = pinPos.y - ( maxHalfHeight + clearance );
info->m_TextPosition.x = pinPos.x; // centered horizontally on pin origin
info->m_TextPosition.y = centerY;
info->m_Angle = ANGLE_HORIZONTAL;
}
return info;
}
// (Removed duplicate license & namespace with second PIN_TEXT_MARGIN to avoid ambiguity)
// NOTE: The real implementation of FormatStackedPinForDisplay lives in sch_pin.cpp.
// The accidental, partial duplicate that was here has been removed.
// Reintroduce small helper functions (previously inside an anonymous namespace) needed later.
static int externalPinDecoSize( const SCHEMATIC_SETTINGS* aSettings, const SCH_PIN& aPin )
{
if( aSettings && aSettings->m_PinSymbolSize )
return aSettings->m_PinSymbolSize;
return aPin.GetNumberTextSize() / 2;
}
int internalPinDecoSize( const SCHEMATIC_SETTINGS* aSettings, const SCH_PIN& aPin )
static int internalPinDecoSize( const SCHEMATIC_SETTINGS* aSettings, const SCH_PIN& aPin )
{
if( aSettings && aSettings->m_PinSymbolSize > 0 )
return aSettings->m_PinSymbolSize;
return aPin.GetNameTextSize() != 0 ? aPin.GetNameTextSize() / 2 : aPin.GetNumberTextSize() / 2;
}
} // namespace
PIN_LAYOUT_CACHE::PIN_LAYOUT_CACHE( const SCH_PIN& aPin ) :
m_pin( aPin ), m_schSettings( nullptr ), m_dirtyFlags( DIRTY_FLAGS::ALL )
@ -131,6 +220,42 @@ void PIN_LAYOUT_CACHE::recomputeExtentsCache( bool aDefinitelyDirty, KIFONT::FON
VECTOR2D fontSize( aSize, aSize );
int penWidth = GetPenSizeForNormal( aSize );
// Handle multi-line text bounds properly
if( aText.StartsWith( "[" ) && aText.EndsWith( "]" ) && aText.Contains( "\n" ) )
{
// Extract content between braces and split into lines
wxString content = aText.Mid( 1, aText.Length() - 2 );
wxArrayString lines;
wxStringSplit( content, lines, '\n' );
if( lines.size() > 1 )
{
int lineSpacing = KiROUND( aSize * 1.3 ); // Same as drawMultiLineText
int maxWidth = 0;
// Find the widest line
for( const wxString& line : lines )
{
wxString trimmedLine = line;
trimmedLine.Trim( true ).Trim( false );
VECTOR2I lineExtents = aFont->StringBoundaryLimits( trimmedLine, fontSize, penWidth, false, false, aFontMetrics );
maxWidth = std::max( maxWidth, lineExtents.x );
}
// Calculate total dimensions - width is max line width, height accounts for all lines
int totalHeight = aSize + ( lines.size() - 1 ) * lineSpacing;
// Add space for braces
int braceWidth = aSize / 3;
maxWidth += braceWidth * 2; // Space for braces on both sides
totalHeight += aSize / 3; // Extra height for brace extensions
aCache.m_Extents = VECTOR2I( maxWidth, totalHeight );
return;
}
}
// Single line text (normal case)
aCache.m_Extents = aFont->StringBoundaryLimits( aText, fontSize, penWidth, false, false, aFontMetrics );
}
@ -221,35 +346,38 @@ void PIN_LAYOUT_CACHE::transformBoxForPin( BOX2I& aBox ) const
void PIN_LAYOUT_CACHE::transformTextForPin( TEXT_INFO& aInfo ) const
{
// Now, calculate boundary box corners position for the actual pin orientation
// Local nominal position for a PIN_RIGHT orientation.
const VECTOR2I baseLocal = aInfo.m_TextPosition;
// We apply a rotation/mirroring depending on the pin orientation so that the text anchor
// maintains a constant perpendicular offset from the pin origin regardless of rotation.
VECTOR2I rotated = baseLocal;
EDA_ANGLE finalAngle = aInfo.m_Angle;
switch( m_pin.PinDrawOrient( DefaultTransform ) )
{
case PIN_ORIENTATION::PIN_RIGHT: // identity
break;
case PIN_ORIENTATION::PIN_LEFT:
{
aInfo.m_HAlign = GetFlippedAlignment( aInfo.m_HAlign );
aInfo.m_TextPosition.x = -aInfo.m_TextPosition.x;
break;
}
case PIN_ORIENTATION::PIN_UP:
{
aInfo.m_Angle = ANGLE_VERTICAL;
aInfo.m_TextPosition = { aInfo.m_TextPosition.y, -aInfo.m_TextPosition.x };
break;
}
case PIN_ORIENTATION::PIN_DOWN:
{
aInfo.m_Angle = ANGLE_VERTICAL;
aInfo.m_TextPosition = { aInfo.m_TextPosition.y, aInfo.m_TextPosition.x };
rotated.x = -rotated.x;
rotated.y = -rotated.y;
aInfo.m_HAlign = GetFlippedAlignment( aInfo.m_HAlign );
break;
case PIN_ORIENTATION::PIN_UP: // rotate +90 (x,y)->(y,-x) and vertical text
rotated = { baseLocal.y, -baseLocal.x };
finalAngle = ANGLE_VERTICAL;
break;
case PIN_ORIENTATION::PIN_DOWN: // rotate -90 (x,y)->(-y,x) and vertical text, flip h-align
rotated = { -baseLocal.y, baseLocal.x };
finalAngle = ANGLE_VERTICAL;
aInfo.m_HAlign = GetFlippedAlignment( aInfo.m_HAlign );
break;
}
default:
case PIN_ORIENTATION::PIN_RIGHT:
// Already in this form
break;
}
aInfo.m_TextPosition += m_pin.GetPosition();
aInfo.m_TextPosition = rotated + m_pin.GetPosition();
aInfo.m_Angle = finalAngle;
}
@ -643,48 +771,47 @@ std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> PIN_LAYOUT_CACHE::GetPinNameInfo( int
info->m_VAlign = GR_TEXT_V_ALIGN_BOTTOM;
}
transformTextForPin( *info );
// New policy: names follow same positioning semantics as numbers.
const SYMBOL* parentSym = m_pin.GetParentSymbol();
if( parentSym )
{
int maxHalfHeight = 0;
for( const SCH_PIN* p : parentSym->GetPins() )
{
wxString n = p->GetShownName();
if( n.IsEmpty() )
continue;
maxHalfHeight = std::max( maxHalfHeight, p->GetNameTextSize() / 2 );
}
int clearance = getPinTextOffset() + schIUScale.MilsToIU( PIN_TEXT_MARGIN );
VECTOR2I pinPos = m_pin.GetPosition();
PIN_ORIENTATION orient = m_pin.PinDrawOrient( DefaultTransform );
bool verticalOrient = ( orient == PIN_ORIENTATION::PIN_UP || orient == PIN_ORIENTATION::PIN_DOWN );
if( verticalOrient )
{
// Vertical pins: name mirrors number placement (left + rotated) for visual consistency.
int boxWidth = info->m_TextSize * (int) info->m_Text.Length() * 0.6; // heuristic width
int centerX = pinPos.x - clearance - boxWidth / 2;
info->m_TextPosition = { centerX, pinPos.y };
info->m_Angle = ANGLE_VERTICAL;
info->m_HAlign = GR_TEXT_H_ALIGN_CENTER;
info->m_VAlign = GR_TEXT_V_ALIGN_CENTER;
}
else
{
// Horizontal pins: name above (negative Y) aligned to same Y offset logic as numbers.
info->m_TextPosition = { pinPos.x, pinPos.y - ( maxHalfHeight + clearance ) };
info->m_Angle = ANGLE_HORIZONTAL;
info->m_HAlign = GR_TEXT_H_ALIGN_CENTER;
info->m_VAlign = GR_TEXT_V_ALIGN_CENTER;
}
}
return info;
}
std::optional<PIN_LAYOUT_CACHE::TEXT_INFO> PIN_LAYOUT_CACHE::GetPinNumberInfo( int aShadowWidth )
{
recomputeCaches();
wxString number = m_pin.GetShownNumber();
if( number.IsEmpty() || !m_pin.GetParentSymbol()->GetShowPinNumbers() )
return std::nullopt;
std::optional<TEXT_INFO> info;
info = TEXT_INFO();
info->m_Text = std::move( number );
info->m_TextSize = m_pin.GetNumberTextSize();
info->m_Thickness = m_numberThickness;
info->m_Angle = ANGLE_HORIZONTAL;
info->m_TextPosition = { m_pin.GetLength() / 2, 0 };
info->m_HAlign = GR_TEXT_H_ALIGN_CENTER;
// The pin number is above the pin if there's no name, or the name is inside
const bool numAbove =
m_pin.GetParentSymbol()->GetPinNameOffset() > 0
|| ( m_pin.GetShownName().empty() || !m_pin.GetParentSymbol()->GetShowPinNames() );
if( numAbove )
{
info->m_TextPosition.y -= getPinTextOffset() + info->m_Thickness / 2;
info->m_VAlign = GR_TEXT_V_ALIGN_BOTTOM;
}
else
{
info->m_TextPosition.y += getPinTextOffset() + info->m_Thickness / 2;
info->m_VAlign = GR_TEXT_V_ALIGN_TOP;
}
transformTextForPin( *info );
return info;
}
// (Removed duplicate later GetPinNumberInfo earlier definition retained at top of file.)
std::optional<PIN_LAYOUT_CACHE::TEXT_INFO>

View File

@ -1,11 +1,11 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
* Copyright The KiCad Developers
*
* 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
* 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,
@ -14,13 +14,11 @@
* 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
* along with this program; if not, you may find one at
* http://www.gnu.org/licenses/
*/
#pragma once
#include <optional>

View File

@ -55,7 +55,8 @@
//#define SEXPR_SYMBOL_LIB_FILE_VERSION 20241209 // Private flags for SCH_FIELDs
//#define SEXPR_SYMBOL_LIB_FILE_VERSION 20250318 // ~ no longer means empty text
//#define SEXPR_SYMBOL_LIB_FILE_VERSION 20250324 // Jumper pin groups
#define SEXPR_SYMBOL_LIB_FILE_VERSION 20250829 // Rounded Rectangles
//#define SEXPR_SYMBOL_LIB_FILE_VERSION 20250829 // Rounded Rectangles
#define SEXPR_SYMBOL_LIB_FILE_VERSION 20250901 // Stacked Pin notation
/**
* Schematic file version.
@ -126,4 +127,5 @@
//#define SEXPR_SCHEMATIC_FILE_VERSION 20250513 // Groups can have design block lib_id
//#define SEXPR_SCHEMATIC_FILE_VERSION 20250610 // DNP, etc. flags for rule areas
//#define SEXPR_SCHEMATIC_FILE_VERSION 20250827 // Custom body styles
#define SEXPR_SCHEMATIC_FILE_VERSION 20250829 // Rounded Rectangles
//#define SEXPR_SCHEMATIC_FILE_VERSION 20250829 // Rounded Rectangles
#define SEXPR_SCHEMATIC_FILE_VERSION 20250901 // Stacked Pin notation

View File

@ -3300,7 +3300,7 @@ void CADSTAR_SCH_ARCHIVE_LOADER::fixUpLibraryPins( LIB_SYMBOL* aSymbolToFix, int
}
}
for( SCH_PIN* pin : aSymbolToFix->GetPins( aGateNumber, 0 ) )
for( SCH_PIN* pin : aSymbolToFix->GetGraphicalPins( aGateNumber, 0 ) )
{
auto setPinOrientation =
[&]( const EDA_ANGLE& aAngle )

View File

@ -64,6 +64,7 @@
#include <advanced_config.h>
#include <settings/settings_manager.h>
#include <stroke_params.h>
#include <string_utils.h>
#include "sch_painter.h"
#include "common.h"
@ -1182,6 +1183,449 @@ void SCH_PAINTER::draw( const SCH_PIN* aPin, int aLayer, bool aDimmed )
return aTextSize * aGal.GetWorldScale() < BITMAP_FONT_SIZE_THRESHOLD;
};
// Helper function for drawing braces around multi-line text
const auto drawBrace =
[&]( KIGFX::GAL& aGal, const VECTOR2D& aTop, const VECTOR2D& aBottom,
int aBraceWidth, bool aLeftBrace, const TEXT_ATTRIBUTES& aAttrs )
{
// Draw a simple brace using line segments, accounting for text rotation
VECTOR2D mid = ( aTop + aBottom ) / 2.0;
aGal.SetLineWidth( aAttrs.m_StrokeWidth );
aGal.SetIsFill( false );
aGal.SetIsStroke( true );
// Calculate brace points in text coordinate system
VECTOR2D p1 = aTop;
VECTOR2D p2 = aTop;
VECTOR2D p3 = mid;
VECTOR2D p4 = aBottom;
VECTOR2D p5 = aBottom;
// Apply brace offset based on text orientation
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, braces extend in the Y direction
// "Left" brace is actually towards negative Y, "right" towards positive Y
double braceOffset = aLeftBrace ? -aBraceWidth : aBraceWidth;
p2.y += braceOffset / 2;
p3.y += braceOffset;
p4.y += braceOffset / 2;
}
else
{
// For horizontal text, braces extend in the X direction
double braceOffset = aLeftBrace ? -aBraceWidth : aBraceWidth;
p2.x += braceOffset / 2;
p3.x += braceOffset;
p4.x += braceOffset / 2;
}
// Draw the brace segments
aGal.DrawLine( p1, p2 );
aGal.DrawLine( p2, p3 );
aGal.DrawLine( p3, p4 );
aGal.DrawLine( p4, p5 );
};
const auto drawBracesAroundText =
[&]( KIGFX::GAL& aGal, const wxArrayString& aLines, const VECTOR2D& aStartPos,
int aLineSpacing, const TEXT_ATTRIBUTES& aAttrs )
{
if( aLines.size() <= 1 )
return;
// Calculate brace dimensions
int braceWidth = aAttrs.m_Size.x / 3; // Make braces a bit larger
// Find the maximum line width to position braces
int maxLineWidth = 0;
KIFONT::FONT* font = aAttrs.m_Font;
if( !font )
font = KIFONT::FONT::GetFont( eeconfig()->m_Appearance.default_font );
for( const wxString& line : aLines )
{
wxString trimmedLine = line;
trimmedLine.Trim( true ).Trim( false );
VECTOR2I lineExtents = font->StringBoundaryLimits( trimmedLine, aAttrs.m_Size,
aAttrs.m_StrokeWidth, false, false,
KIFONT::METRICS() );
maxLineWidth = std::max( maxLineWidth, lineExtents.x );
}
// Calculate brace positions based on text vertical alignment and rotation
VECTOR2D braceStart = aStartPos;
VECTOR2D braceEnd = aStartPos;
// Extend braces beyond the text bounds
int textHeight = aAttrs.m_Size.y;
int extraHeight = textHeight / 3; // Extend braces by 1/3 of text height beyond text
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally and braces are horizontal
braceEnd.x += ( aLines.size() - 1 ) * aLineSpacing;
// Extend braces horizontally to encompass all lines plus extra space
braceStart.x -= 2 * extraHeight;
// Position braces in the perpendicular direction (Y) with proper spacing
int braceSpacing = maxLineWidth / 2 + braceWidth;
VECTOR2D topBraceStart = braceStart;
topBraceStart.y -= braceSpacing;
VECTOR2D topBraceEnd = braceEnd;
topBraceEnd.y -= braceSpacing;
drawBrace( aGal, topBraceStart, topBraceEnd, braceWidth, true, aAttrs );
VECTOR2D bottomBraceStart = braceStart;
bottomBraceStart.y += braceSpacing;
VECTOR2D bottomBraceEnd = braceEnd;
bottomBraceEnd.y += braceSpacing;
drawBrace( aGal, bottomBraceStart, bottomBraceEnd, braceWidth, false, aAttrs );
}
else
{
// For horizontal text, lines are spaced vertically and braces are vertical
braceEnd.y += ( aLines.size() - 1 ) * aLineSpacing;
// Extend braces vertically to encompass all lines plus extra space
braceStart.y -= 2 * extraHeight;
// Position braces in the perpendicular direction (X) with proper spacing
int braceSpacing = maxLineWidth / 2 + braceWidth;
// Draw left brace
VECTOR2D leftTop = braceStart;
leftTop.x -= braceSpacing;
VECTOR2D leftBottom = braceEnd;
leftBottom.x -= braceSpacing;
drawBrace( aGal, leftTop, leftBottom, braceWidth, true, aAttrs );
// Draw right brace
VECTOR2D rightTop = braceStart;
rightTop.x += braceSpacing;
VECTOR2D rightBottom = braceEnd;
rightBottom.x += braceSpacing;
drawBrace( aGal, rightTop, rightBottom, braceWidth, false, aAttrs );
}
};
const auto drawBracesAroundTextBitmap =
[&]( KIGFX::GAL& aGal, const wxArrayString& aLines, const VECTOR2D& aStartPos,
int aLineSpacing, const TEXT_ATTRIBUTES& aAttrs )
{
// Simplified brace drawing for bitmap text
if( aLines.size() <= 1 )
return;
int braceWidth = aAttrs.m_Size.x / 4;
// Estimate max line width (less precise for bitmap text)
int maxLineWidth = aAttrs.m_Size.x * 4; // Conservative estimate
// Calculate brace positions based on rotation
VECTOR2D braceStart = aStartPos;
VECTOR2D braceEnd = aStartPos;
int textHalfHeight = aAttrs.m_Size.y / 2;
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally
braceEnd.x += ( aLines.size() - 1 ) * aLineSpacing;
VECTOR2D leftStart = braceStart;
leftStart.y -= maxLineWidth / 2 + braceWidth / 2;
VECTOR2D leftEnd = braceEnd;
leftEnd.y -= maxLineWidth / 2 + braceWidth / 2;
drawBrace( aGal, leftStart, leftEnd, braceWidth, true, aAttrs );
VECTOR2D rightStart = braceStart;
rightStart.y += maxLineWidth / 2 + braceWidth / 2;
VECTOR2D rightEnd = braceEnd;
rightEnd.y += maxLineWidth / 2 + braceWidth / 2;
drawBrace( aGal, rightStart, rightEnd, braceWidth, false, aAttrs );
}
else
{
// For horizontal text, lines are spaced vertically
braceEnd.y += ( aLines.size() - 1 ) * aLineSpacing;
VECTOR2D braceTop = braceStart;
braceTop.y -= textHalfHeight;
VECTOR2D braceBottom = braceEnd;
braceBottom.y += textHalfHeight;
VECTOR2D leftTop = braceTop;
leftTop.x -= maxLineWidth / 2 + braceWidth / 2;
VECTOR2D leftBottom = braceBottom;
leftBottom.x -= maxLineWidth / 2 + braceWidth / 2;
drawBrace( aGal, leftTop, leftBottom, braceWidth, true, aAttrs );
VECTOR2D rightTop = braceTop;
rightTop.x += maxLineWidth / 2 + braceWidth / 2;
VECTOR2D rightBottom = braceBottom;
rightBottom.x += maxLineWidth / 2 + braceWidth / 2;
drawBrace( aGal, rightTop, rightBottom, braceWidth, false, aAttrs );
}
};
// Helper functions for drawing multi-line pin text with braces
const auto drawMultiLineText =
[&]( KIGFX::GAL& aGal, const wxString& aText, const VECTOR2D& aPosition,
const TEXT_ATTRIBUTES& aAttrs, const KIFONT::METRICS& aFontMetrics )
{
// Check if this is multi-line stacked pin text with braces
if( aText.StartsWith( "[" ) && aText.EndsWith( "]" ) && aText.Contains( "\n" ) )
{
// Extract content between braces and split into lines
wxString content = aText.Mid( 1, aText.Length() - 2 );
wxArrayString lines;
wxStringSplit( content, lines, '\n' );
if( lines.size() > 1 )
{
// Calculate line spacing (similar to EDA_TEXT::GetInterline)
int lineSpacing = KiROUND( aAttrs.m_Size.y * 1.3 ); // 130% of text height
// Calculate positioning based on text alignment and rotation
VECTOR2D startPos = aPosition;
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally
// Adjust start position based on horizontal alignment
if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_RIGHT )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth;
}
else if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_CENTER )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth / 2;
}
// Draw each line
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.x += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
strokeText( aGal, line, linePos, aAttrs, aFontMetrics );
}
}
else
{
// For horizontal text, lines are spaced vertically
// Adjust start position based on vertical alignment
if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_BOTTOM )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight;
}
else if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_CENTER )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight / 2;
}
// Draw each line
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.y += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
strokeText( aGal, line, linePos, aAttrs, aFontMetrics );
}
}
// Draw braces around the text
drawBracesAroundText( aGal, lines, startPos, lineSpacing, aAttrs );
return;
}
}
// Fallback to regular single-line text
strokeText( aGal, aText, aPosition, aAttrs, aFontMetrics );
};
const auto drawMultiLineTextBox =
[&]( KIGFX::GAL& aGal, const wxString& aText, const VECTOR2D& aPosition,
const TEXT_ATTRIBUTES& aAttrs, const KIFONT::METRICS& aFontMetrics )
{
// Similar to drawMultiLineText but uses boxText for outline fonts
if( aText.StartsWith( "[" ) && aText.EndsWith( "]" ) && aText.Contains( "\n" ) )
{
wxString content = aText.Mid( 1, aText.Length() - 2 );
wxArrayString lines;
wxStringSplit( content, lines, '\n' );
if( lines.size() > 1 )
{
int lineSpacing = KiROUND( aAttrs.m_Size.y * 1.3 );
VECTOR2D startPos = aPosition;
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally
if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_RIGHT )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth;
}
else if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_CENTER )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth / 2;
}
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.x += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
boxText( aGal, line, linePos, aAttrs, aFontMetrics );
}
}
else
{
// For horizontal text, lines are spaced vertically
if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_BOTTOM )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight;
}
else if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_CENTER )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight / 2;
}
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.y += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
boxText( aGal, line, linePos, aAttrs, aFontMetrics );
}
}
drawBracesAroundText( aGal, lines, startPos, lineSpacing, aAttrs );
return;
}
}
boxText( aGal, aText, aPosition, aAttrs, aFontMetrics );
};
const auto drawMultiLineBitmapText =
[&]( KIGFX::GAL& aGal, const wxString& aText, const VECTOR2D& aPosition,
const TEXT_ATTRIBUTES& aAttrs )
{
// Similar to drawMultiLineText but uses bitmapText
if( aText.StartsWith( "[" ) && aText.EndsWith( "]" ) && aText.Contains( "\n" ) )
{
wxString content = aText.Mid( 1, aText.Length() - 2 );
wxArrayString lines;
wxStringSplit( content, lines, '\n' );
if( lines.size() > 1 )
{
int lineSpacing = KiROUND( aAttrs.m_Size.y * 1.3 );
VECTOR2D startPos = aPosition;
if( aAttrs.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally
if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_RIGHT )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth;
}
else if( aAttrs.m_Halign == GR_TEXT_H_ALIGN_CENTER )
{
int totalWidth = ( lines.size() - 1 ) * lineSpacing;
startPos.x -= totalWidth / 2;
}
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.x += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
bitmapText( aGal, line, linePos, aAttrs );
}
}
else
{
// For horizontal text, lines are spaced vertically
if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_BOTTOM )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight;
}
else if( aAttrs.m_Valign == GR_TEXT_V_ALIGN_CENTER )
{
int totalHeight = ( lines.size() - 1 ) * lineSpacing;
startPos.y -= totalHeight / 2;
}
for( size_t i = 0; i < lines.size(); i++ )
{
VECTOR2D linePos = startPos;
linePos.y += i * lineSpacing;
wxString line = lines[i];
line.Trim( true ).Trim( false );
bitmapText( aGal, line, linePos, aAttrs );
}
}
// Draw braces with bitmap text (simplified version)
drawBracesAroundTextBitmap( aGal, lines, startPos, lineSpacing, aAttrs );
return;
}
}
bitmapText( aGal, aText, aPosition, aAttrs );
};
const auto drawTextInfo =
[&]( const PIN_LAYOUT_CACHE::TEXT_INFO& aTextInfo, const COLOR4D& aColor )
{
@ -1206,24 +1650,24 @@ void SCH_PAINTER::draw( const SCH_PIN* aPin, int aLayer, bool aDimmed )
if( !attrs.m_Font->IsOutline() )
{
strokeText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
drawMultiLineText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
}
else
{
boxText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
drawMultiLineTextBox( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
}
}
else if( nonCached( aPin ) && renderTextAsBitmap )
{
bitmapText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs );
drawMultiLineBitmapText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs );
const_cast<SCH_PIN*>( aPin )->SetFlags( IS_SHOWN_AS_BITMAP );
}
else
{
strokeText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
drawMultiLineText( *m_gal, aTextInfo.m_Text, aTextInfo.m_TextPosition, attrs,
aPin->GetFontMetrics() );
const_cast<SCH_PIN*>( aPin )->SetFlags( IS_SHOWN_AS_BITMAP );
}
};
@ -2251,11 +2695,11 @@ void SCH_PAINTER::draw( const SCH_SYMBOL* aSymbol, int aLayer )
// Use dummy symbol if the actual couldn't be found (or couldn't be locked).
LIB_SYMBOL* originalSymbol =
aSymbol->GetLibSymbolRef() ? aSymbol->GetLibSymbolRef().get() : LIB_SYMBOL::GetDummy();
std::vector<SCH_PIN*> originalPins = originalSymbol->GetPins( unit, bodyStyle );
std::vector<SCH_PIN*> originalPins = originalSymbol->GetGraphicalPins( unit, bodyStyle );
// Copy the source so we can re-orient and translate it.
LIB_SYMBOL tempSymbol( *originalSymbol );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetPins( unit, bodyStyle );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetGraphicalPins( unit, bodyStyle );
tempSymbol.SetFlags( aSymbol->GetFlags() );

View File

@ -39,6 +39,49 @@
#include <trigo.h>
#include <string_utils.h>
wxString FormatStackedPinForDisplay( const wxString& aPinNumber, int aPinLength, int aTextSize, KIFONT::FONT* aFont,
const KIFONT::METRICS& aFontMetrics )
{
// Check if this is stacked pin notation: [A,B,C]
if( !aPinNumber.StartsWith( "[" ) || !aPinNumber.EndsWith( "]" ) )
return aPinNumber;
const int minPinTextWidth = schIUScale.MilsToIU( 50 );
const int maxPinTextWidth = std::max( aPinLength, minPinTextWidth );
VECTOR2D fontSize( aTextSize, aTextSize );
int penWidth = GetPenSizeForNormal( aTextSize );
VECTOR2I textExtents = aFont->StringBoundaryLimits( aPinNumber, fontSize, penWidth, false, false, aFontMetrics );
if( textExtents.x <= maxPinTextWidth )
return aPinNumber; // Fits already
// Strip brackets and split by comma
wxString inner = aPinNumber.Mid( 1, aPinNumber.Length() - 2 );
wxArrayString parts;
wxStringSplit( inner, parts, ',' );
if( parts.empty() )
return aPinNumber; // malformed; fallback
// Build multi-line representation inside braces, each line trimmed
wxString result = "[";
for( size_t i = 0; i < parts.size(); ++i )
{
wxString line = parts[i];
line.Trim( true ).Trim( false );
if( i > 0 )
result += "\n";
result += line;
}
result += "]";
return result;
}
// small margin in internal units between the pin text and the pin line
#define PIN_TEXT_MARGIN 4
@ -431,15 +474,24 @@ void SCH_PIN::SetIsDangling( bool aIsDangling )
bool SCH_PIN::IsStacked( const SCH_PIN* aPin ) const
{
bool isConnectableType_a = GetType() == ELECTRICAL_PINTYPE::PT_PASSIVE
|| GetType() == ELECTRICAL_PINTYPE::PT_NIC;
bool isConnectableType_b = aPin->GetType() == ELECTRICAL_PINTYPE::PT_PASSIVE
|| aPin->GetType() == ELECTRICAL_PINTYPE::PT_NIC;
const auto isPassiveOrNic = []( ELECTRICAL_PINTYPE t )
{
return t == ELECTRICAL_PINTYPE::PT_PASSIVE || t == ELECTRICAL_PINTYPE::PT_NIC;
};
return m_parent == aPin->GetParent()
&& GetPosition() == aPin->GetPosition()
&& GetName() == aPin->GetName()
&& ( GetType() == aPin->GetType() || isConnectableType_a || isConnectableType_b );
const bool sameParent = m_parent == aPin->GetParent();
const bool samePos = GetPosition() == aPin->GetPosition();
const bool sameName = GetName() == aPin->GetName();
const bool typeCompat = GetType() == aPin->GetType()
|| isPassiveOrNic( GetType() )
|| isPassiveOrNic( aPin->GetType() );
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "IsStacked: this='%s/%s' other='%s/%s' sameParent=%d samePos=%d sameName=%d typeCompat=%d",
GetName(), GetNumber(), aPin->GetName(), aPin->GetNumber(), sameParent,
samePos, sameName, typeCompat ) );
return sameParent && samePos && sameName && typeCompat;
}
@ -538,6 +590,46 @@ wxString SCH_PIN::GetShownNumber() const
}
std::vector<wxString> SCH_PIN::GetStackedPinNumbers( bool* aValid ) const
{
wxString shown = GetShownNumber();
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "GetStackedPinNumbers: shown='%s'", shown ) );
std::vector<wxString> numbers = ExpandStackedPinNotation( shown, aValid );
// Log the expansion for debugging
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "Expanded '%s' to %zu pins", shown, numbers.size() ) );
for( const wxString& num : numbers )
{
wxLogTrace( "KICAD_STACKED_PINS", wxString::Format( " -> '%s'", num ) );
}
return numbers;
}
std::optional<wxString> SCH_PIN::GetSmallestLogicalNumber() const
{
bool valid = false;
auto numbers = GetStackedPinNumbers( &valid );
if( valid && !numbers.empty() )
return numbers.front(); // Already in ascending order
return std::nullopt;
}
wxString SCH_PIN::GetEffectivePadNumber() const
{
if( auto smallest = GetSmallestLogicalNumber() )
return *smallest;
return GetShownNumber();
}
void SCH_PIN::SetNumber( const wxString& aNumber )
{
if( m_number == aNumber )
@ -778,12 +870,17 @@ void SCH_PIN::PlotPinTexts( PLOTTER *aPlotter, const VECTOR2I &aPinPos, PIN_ORIE
wxString name = GetShownName();
wxString number = GetShownNumber();
// Apply stacked pin display formatting (reuse helper from pin_layout_cache)
if( aDrawPinNum && !number.IsEmpty() )
{
const KIFONT::METRICS& metrics = GetFontMetrics();
number = FormatStackedPinForDisplay( number, GetLength(), GetNumberTextSize(), font, metrics );
}
if( name.IsEmpty() || m_nameTextSize == 0 )
aDrawPinName = false;
if( number.IsEmpty() || m_numTextSize == 0 )
aDrawPinNum = false;
if( !aDrawPinNum && !aDrawPinName )
return;
@ -792,169 +889,257 @@ void SCH_PIN::PlotPinTexts( PLOTTER *aPlotter, const VECTOR2I &aPinPos, PIN_ORIE
int name_offset = schIUScale.MilsToIU( PIN_TEXT_MARGIN ) + namePenWidth;
int num_offset = schIUScale.MilsToIU( PIN_TEXT_MARGIN ) + numPenWidth;
/* Get the num and name colors */
COLOR4D nameColor = settings->GetLayerColor( LAYER_PINNAM );
COLOR4D numColor = settings->GetLayerColor( LAYER_PINNUM );
COLOR4D bg = settings->GetBackgroundColor();
if( bg == COLOR4D::UNSPECIFIED || !aPlotter->GetColorMode() )
bg = COLOR4D::WHITE;
if( aDimmed )
{
nameColor.Desaturate( );
numColor.Desaturate( );
nameColor.Desaturate();
numColor.Desaturate();
nameColor = nameColor.Mix( bg, 0.5f );
numColor = numColor.Mix( bg, 0.5f );
}
int x1 = aPinPos.x;
int y1 = aPinPos.y;
switch( aPinOrient )
{
case PIN_ORIENTATION::PIN_UP: y1 -= GetLength(); break;
case PIN_ORIENTATION::PIN_DOWN: y1 += GetLength(); break;
case PIN_ORIENTATION::PIN_LEFT: x1 -= GetLength(); break;
case PIN_ORIENTATION::PIN_RIGHT: x1 += GetLength(); break;
case PIN_ORIENTATION::INHERIT: wxFAIL_MSG( wxS( "aPinOrient must be resolved!" ) ); break;
case PIN_ORIENTATION::PIN_UP: y1 -= GetLength(); break;
case PIN_ORIENTATION::PIN_DOWN: y1 += GetLength(); break;
case PIN_ORIENTATION::PIN_LEFT: x1 -= GetLength(); break;
case PIN_ORIENTATION::PIN_RIGHT: x1 += GetLength(); break;
default: break;
}
auto plotName =
[&]( int x, int y, const EDA_ANGLE& angle, GR_TEXT_H_ALIGN_T hJustify,
GR_TEXT_V_ALIGN_T vJustify )
auto plotSimpleText = [&]( int x, int y, const EDA_ANGLE& angle, GR_TEXT_H_ALIGN_T hJustify,
GR_TEXT_V_ALIGN_T vJustify, const wxString& txt, int size,
int penWidth, const COLOR4D& col )
{
TEXT_ATTRIBUTES attrs;
attrs.m_StrokeWidth = penWidth;
attrs.m_Angle = angle;
attrs.m_Size = VECTOR2I( size, size );
attrs.m_Halign = hJustify;
attrs.m_Valign = vJustify;
attrs.m_Multiline = false; // we'll manage multi-line manually
aPlotter->PlotText( VECTOR2I( x, y ), col, txt, attrs, font, GetFontMetrics() );
};
auto plotMultiLineWithBraces = [&]( int anchorX, int anchorY, bool vertical, bool /*numberBlock*/ )
{
// If not multi-line formatted, just plot single line centered.
if( !number.StartsWith( "[" ) || !number.EndsWith( "]" ) || !number.Contains( "\n" ) )
{
plotSimpleText( anchorX, anchorY, vertical ? ANGLE_VERTICAL : ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, number,
GetNumberTextSize(), numPenWidth, numColor );
return;
}
wxString content = number.Mid( 1, number.Length() - 2 );
wxArrayString lines;
wxStringSplit( content, lines, '\n' );
if( lines.size() <= 1 )
{
plotSimpleText( anchorX, anchorY, vertical ? ANGLE_VERTICAL : ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, content,
GetNumberTextSize(), numPenWidth, numColor );
return;
}
int textSize = GetNumberTextSize();
int lineSpacing = KiROUND( textSize * 1.3 );
const KIFONT::METRICS& metrics = GetFontMetrics();
// Measure line widths for brace spacing
int maxLineWidth = 0;
for( const wxString& rawLine : lines )
{
wxString trimmed = rawLine; trimmed.Trim(true).Trim(false);
VECTOR2I ext = font->StringBoundaryLimits( trimmed, VECTOR2D( textSize, textSize ),
GetPenSizeForNormal( textSize ), false, false, metrics );
if( ext.x > maxLineWidth )
maxLineWidth = ext.x;
}
// Determine starting position
int startX = anchorX;
int startY = anchorY;
if( vertical )
{
int totalWidth = ( (int) lines.size() - 1 ) * lineSpacing;
startX -= totalWidth;
}
else
{
int totalHeight = ( (int) lines.size() - 1 ) * lineSpacing;
startY -= totalHeight;
}
for( size_t i = 0; i < lines.size(); ++i )
{
wxString l = lines[i]; l.Trim( true ).Trim( false );
int lx = startX + ( vertical ? (int) i * lineSpacing : 0 );
int ly = startY + ( vertical ? 0 : (int) i * lineSpacing );
plotSimpleText( lx, ly, vertical ? ANGLE_VERTICAL : ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, l,
textSize, numPenWidth, numColor );
}
// Now draw braces emulating SCH_PAINTER brace geometry
auto plotBrace = [&]( const VECTOR2I& top, const VECTOR2I& bottom, bool leftOrTop, bool isVerticalText )
{
// Build 4 small segments approximating curly brace
VECTOR2I mid = ( top + bottom ) / 2;
int braceWidth = textSize / 3; // same scale as painter
VECTOR2I p1 = top;
VECTOR2I p5 = bottom;
VECTOR2I p2 = top;
VECTOR2I p3 = mid;
VECTOR2I p4 = bottom;
int offset = leftOrTop ? -braceWidth : braceWidth;
if( isVerticalText )
{
TEXT_ATTRIBUTES attrs;
attrs.m_StrokeWidth = namePenWidth;
attrs.m_Angle = angle;
attrs.m_Size = VECTOR2I( GetNameTextSize(), GetNameTextSize() );
attrs.m_Halign = hJustify;
attrs.m_Valign = vJustify;
attrs.m_Multiline = false;
aPlotter->PlotText( VECTOR2I( x, y ), nameColor, name, attrs, font,
GetFontMetrics() );
};
auto plotNum =
[&]( int x, int y, const EDA_ANGLE& angle, GR_TEXT_H_ALIGN_T hJustify,
GR_TEXT_V_ALIGN_T vJustify )
// Text vertical => brace extends in Y (horizontal brace lines across X axis set)
// For vertical orientation we offset Y for p2/p3/p4
p2.y += offset / 2;
p3.y += offset;
p4.y += offset / 2;
}
else
{
TEXT_ATTRIBUTES attrs;
attrs.m_StrokeWidth = numPenWidth;
attrs.m_Angle = angle;
attrs.m_Size = VECTOR2I( GetNumberTextSize(), GetNumberTextSize() );
attrs.m_Halign = hJustify;
attrs.m_Valign = vJustify;
attrs.m_Multiline = false;
// Horizontal text => brace extends in X
p2.x += offset / 2;
p3.x += offset;
p4.x += offset / 2;
}
aPlotter->PlotText( VECTOR2I( x, y ), numColor, number, attrs, font,
GetFontMetrics() );
};
aPlotter->MoveTo( p1 ); aPlotter->FinishTo( p2 );
aPlotter->MoveTo( p2 ); aPlotter->FinishTo( p3 );
aPlotter->MoveTo( p3 ); aPlotter->FinishTo( p4 );
aPlotter->MoveTo( p4 ); aPlotter->FinishTo( p5 );
};
// Draw the text inside, but the pin numbers outside.
aPlotter->SetCurrentLineWidth( numPenWidth );
int braceWidth = textSize / 3;
int extraHeight = textSize / 3; // extend beyond text block
if( vertical )
{
// Lines spaced horizontally, braces horizontal (above & below)
int totalWidth = ( (int) lines.size() - 1 ) * lineSpacing;
VECTOR2I braceStart( startX - 2 * extraHeight, anchorY );
VECTOR2I braceEnd( startX + totalWidth + extraHeight, anchorY );
int braceSpacing = maxLineWidth / 2 + braceWidth;
VECTOR2I topStart = braceStart; topStart.y -= braceSpacing;
VECTOR2I topEnd = braceEnd; topEnd.y -= braceSpacing;
VECTOR2I bottomStart = braceStart; bottomStart.y += braceSpacing;
VECTOR2I bottomEnd = braceEnd; bottomEnd.y += braceSpacing;
plotBrace( topStart, topEnd, true, true ); // leftOrTop=true
plotBrace( bottomStart, bottomEnd, false, true );
}
else
{
// Lines spaced vertically, braces vertical (left & right)
int totalHeight = ( (int) lines.size() - 1 ) * lineSpacing;
VECTOR2I braceStart( anchorX, startY - 2 * extraHeight );
VECTOR2I braceEnd( anchorX, startY + totalHeight + extraHeight );
int braceSpacing = maxLineWidth / 2 + braceWidth;
VECTOR2I leftTop = braceStart; leftTop.x -= braceSpacing;
VECTOR2I leftBot = braceEnd; leftBot.x -= braceSpacing;
VECTOR2I rightTop = braceStart; rightTop.x += braceSpacing;
VECTOR2I rightBot = braceEnd; rightBot.x += braceSpacing;
plotBrace( leftTop, leftBot, true, false );
plotBrace( rightTop, rightBot, false, false );
}
};
// Logic largely mirrors original single-line placement but calls multi-line path for numbers
if( aTextInside )
{
if( ( aPinOrient == PIN_ORIENTATION::PIN_LEFT )
|| ( aPinOrient == PIN_ORIENTATION::PIN_RIGHT ) ) // It's a horizontal line.
if( ( aPinOrient == PIN_ORIENTATION::PIN_LEFT ) || ( aPinOrient == PIN_ORIENTATION::PIN_RIGHT ) )
{
if( aDrawPinName )
{
if( aPinOrient == PIN_ORIENTATION::PIN_RIGHT )
{
plotName( x1 + aTextInside, y1, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_LEFT, GR_TEXT_V_ALIGN_CENTER );
}
else // orient == PIN_LEFT
{
plotName( x1 - aTextInside, y1, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_RIGHT, GR_TEXT_V_ALIGN_CENTER );
}
plotSimpleText( x1 + aTextInside, y1, ANGLE_HORIZONTAL, GR_TEXT_H_ALIGN_LEFT,
GR_TEXT_V_ALIGN_CENTER, name, GetNameTextSize(), namePenWidth, nameColor );
else
plotSimpleText( x1 - aTextInside, y1, ANGLE_HORIZONTAL, GR_TEXT_H_ALIGN_RIGHT,
GR_TEXT_V_ALIGN_CENTER, name, GetNameTextSize(), namePenWidth, nameColor );
}
if( aDrawPinNum )
{
plotNum( ( x1 + aPinPos.x) / 2, y1 - num_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
}
plotMultiLineWithBraces( ( x1 + aPinPos.x ) / 2, y1 - num_offset, false, true );
}
else // It's a vertical line.
else
{
if( aPinOrient == PIN_ORIENTATION::PIN_DOWN )
{
if( aDrawPinName )
{
plotName( x1, y1 + aTextInside, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_RIGHT, GR_TEXT_V_ALIGN_CENTER );
}
plotSimpleText( x1, y1 + aTextInside, ANGLE_VERTICAL, GR_TEXT_H_ALIGN_RIGHT,
GR_TEXT_V_ALIGN_CENTER, name, GetNameTextSize(), namePenWidth, nameColor );
if( aDrawPinNum )
{
plotNum( x1 - num_offset, ( y1 + aPinPos.y) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
}
plotMultiLineWithBraces( x1 - num_offset, ( y1 + aPinPos.y ) / 2, true, true );
}
else /* PIN_UP */
else // PIN_UP
{
if( aDrawPinName )
{
plotName( x1, y1 - aTextInside, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_LEFT, GR_TEXT_V_ALIGN_CENTER );
}
plotSimpleText( x1, y1 - aTextInside, ANGLE_VERTICAL, GR_TEXT_H_ALIGN_LEFT,
GR_TEXT_V_ALIGN_CENTER, name, GetNameTextSize(), namePenWidth, nameColor );
if( aDrawPinNum )
{
plotNum( x1 - num_offset, ( y1 + aPinPos.y) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
}
plotMultiLineWithBraces( x1 - num_offset, ( y1 + aPinPos.y ) / 2, true, true );
}
}
}
else // Draw num & text pin outside.
else
{
if( ( aPinOrient == PIN_ORIENTATION::PIN_LEFT )
|| ( aPinOrient == PIN_ORIENTATION::PIN_RIGHT ) )
if( ( aPinOrient == PIN_ORIENTATION::PIN_LEFT ) || ( aPinOrient == PIN_ORIENTATION::PIN_RIGHT ) )
{
// It's an horizontal line.
if( aDrawPinName && aDrawPinNum )
{
plotName( ( x1 + aPinPos.x) / 2, y1 - name_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotNum( ( x1 + aPinPos.x) / 2, y1 + num_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_TOP );
plotSimpleText( ( x1 + aPinPos.x ) / 2, y1 - name_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, name,
GetNameTextSize(), namePenWidth, nameColor );
plotMultiLineWithBraces( ( x1 + aPinPos.x ) / 2, y1 + num_offset, false, true );
}
else if( aDrawPinName )
{
plotName( ( x1 + aPinPos.x) / 2, y1 - name_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotSimpleText( ( x1 + aPinPos.x ) / 2, y1 - name_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, name,
GetNameTextSize(), namePenWidth, nameColor );
}
else if( aDrawPinNum )
{
plotNum( ( x1 + aPinPos.x) / 2, y1 - name_offset, ANGLE_HORIZONTAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotMultiLineWithBraces( ( x1 + aPinPos.x ) / 2, y1 - name_offset, false, true );
}
}
else
{
// Its a vertical line.
if( aDrawPinName && aDrawPinNum )
{
plotName( x1 - name_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotNum( x1 + num_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_TOP );
plotSimpleText( x1 - name_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, name,
GetNameTextSize(), namePenWidth, nameColor );
plotMultiLineWithBraces( x1 + num_offset, ( y1 + aPinPos.y ) / 2, true, true );
}
else if( aDrawPinName )
{
plotName( x1 - name_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotSimpleText( x1 - name_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM, name,
GetNameTextSize(), namePenWidth, nameColor );
}
else if( aDrawPinNum )
{
plotNum( x1 - num_offset, ( y1 + aPinPos.y ) / 2, ANGLE_VERTICAL,
GR_TEXT_H_ALIGN_CENTER, GR_TEXT_V_ALIGN_BOTTOM );
plotMultiLineWithBraces( x1 - num_offset, ( y1 + aPinPos.y ) / 2, true, true );
}
}
}
@ -1254,8 +1439,16 @@ wxString SCH_PIN::GetDefaultNetName( const SCH_SHEET_PATH& aPath, bool aForceNoC
}
}
wxString libPinShownName = m_libPin ? m_libPin->GetShownName() : wxString( "??" );
wxString libPinShownName = m_libPin ? m_libPin->GetShownName() : wxString( "??" );
wxString libPinShownNumber = m_libPin ? m_libPin->GetShownNumber() : wxString( "??" );
wxString effectivePadNumber = m_libPin ? m_libPin->GetEffectivePadNumber() : libPinShownNumber;
if( effectivePadNumber != libPinShownNumber )
{
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "GetDefaultNetName: stacked pin shown='%s' -> using smallest logical='%s'",
libPinShownNumber, effectivePadNumber ) );
}
// Use timestamp for unannotated symbols
if( symbol->GetRef( &aPath, false ).Last() == '?' )
@ -1263,6 +1456,10 @@ wxString SCH_PIN::GetDefaultNetName( const SCH_SHEET_PATH& aPath, bool aForceNoC
name << GetParentSymbol()->m_Uuid.AsString();
wxString libPinNumber = m_libPin ? m_libPin->GetNumber() : wxString( "??" );
// Apply same smallest-logical substitution for unannotated symbols
if( effectivePadNumber != libPinShownNumber && !effectivePadNumber.IsEmpty() )
libPinNumber = effectivePadNumber;
name << "-Pad" << libPinNumber << ")";
annotated = false;
}
@ -1274,7 +1471,10 @@ wxString SCH_PIN::GetDefaultNetName( const SCH_SHEET_PATH& aPath, bool aForceNoC
name << "-" << EscapeString( libPinShownName, CTX_NETNAME );
if( unconnected || has_multiple )
name << "-Pad" << EscapeString( libPinShownNumber, CTX_NETNAME );
{
// Use effective (possibly de-stacked) pad number in net name
name << "-Pad" << EscapeString( effectivePadNumber, CTX_NETNAME );
}
name << ")";
}
@ -1282,7 +1482,7 @@ wxString SCH_PIN::GetDefaultNetName( const SCH_SHEET_PATH& aPath, bool aForceNoC
{
// Pin numbers are unique, so we skip the unit token
name << symbol->GetRef( &aPath, false );
name << "-Pad" << EscapeString( libPinShownNumber, CTX_NETNAME ) << ")";
name << "-Pad" << EscapeString( effectivePadNumber, CTX_NETNAME ) << ")";
}
if( annotated )

View File

@ -24,6 +24,7 @@
#pragma once
#include <memory>
#include <vector>
#include <pin_type.h>
#include <sch_item.h>
@ -122,6 +123,19 @@ public:
const wxString& GetNumber() const { return m_number; }
wxString GetShownNumber() const;
std::vector<wxString> GetStackedPinNumbers( bool* aValid = nullptr ) const;
/**
* Return the smallest logical pin number if this pin uses stacked
* notation and it is valid. Otherwise returns std::nullopt.
*/
std::optional<wxString> GetSmallestLogicalNumber() const;
/**
* Return the pin number to be used for deterministic operations such as
* autogenerated net names. For stacked pins this is the smallest logical
* number; otherwise it is the shown number.
*/
wxString GetEffectivePadNumber() const;
void SetNumber( const wxString& aNumber );
int GetNameTextSize() const;

View File

@ -1128,7 +1128,7 @@ const SCH_PIN* SCH_SYMBOL::GetPin( const VECTOR2I& aPos ) const
std::vector<SCH_PIN*> SCH_SYMBOL::GetLibPins() const
{
if( m_part )
return m_part->GetPins( m_unit, m_bodyStyle );
return m_part->GetGraphicalPins( m_unit, m_bodyStyle );
return std::vector<SCH_PIN*>();
}
@ -2530,11 +2530,11 @@ void SCH_SYMBOL::Plot( PLOTTER* aPlotter, bool aBackground, const SCH_PLOT_OPTS&
if( m_part )
{
std::vector<SCH_PIN*> libPins = m_part->GetPins( GetUnit(), GetBodyStyle() );
std::vector<SCH_PIN*> libPins = m_part->GetGraphicalPins( GetUnit(), GetBodyStyle() );
// Copy the source so we can re-orient and translate it.
LIB_SYMBOL tempSymbol( *m_part );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetPins( GetUnit(), GetBodyStyle() );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetGraphicalPins( GetUnit(), GetBodyStyle() );
// Copy the pin info from the symbol to the temp pins
for( unsigned i = 0; i < tempPins.size(); ++ i )
@ -2652,11 +2652,11 @@ void SCH_SYMBOL::PlotPins( PLOTTER* aPlotter ) const
TRANSFORM savedTransform = renderSettings->m_Transform;
renderSettings->m_Transform = GetTransform();
std::vector<SCH_PIN*> libPins = m_part->GetPins( GetUnit(), GetBodyStyle() );
std::vector<SCH_PIN*> libPins = m_part->GetGraphicalPins( GetUnit(), GetBodyStyle() );
// Copy the source to stay const
LIB_SYMBOL tempSymbol( *m_part );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetPins( GetUnit(), GetBodyStyle() );
std::vector<SCH_PIN*> tempPins = tempSymbol.GetGraphicalPins( GetUnit(), GetBodyStyle() );
SCH_PLOT_OPTS plotOpts;
// Copy the pin info from the symbol to the temp pins

View File

@ -935,6 +935,20 @@ TOOL_ACTION SCH_ACTIONS::pinTable( TOOL_ACTION_ARGS()
.Tooltip( _( "Displays pin table for bulk editing of pins" ) )
.Icon( BITMAPS::pin_table ) );
TOOL_ACTION SCH_ACTIONS::convertStackedPins( TOOL_ACTION_ARGS()
.Name( "eeschema.InteractiveEdit.convertStackedPins" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Convert Stacked Pins" ) )
.Tooltip( _( "Convert multiple pins at the same location to a single pin with stacked notation" ) )
.Icon( BITMAPS::pin ) );
TOOL_ACTION SCH_ACTIONS::explodeStackedPin( TOOL_ACTION_ARGS()
.Name( "eeschema.InteractiveEdit.explodeStackedPin" )
.Scope( AS_GLOBAL )
.FriendlyName( _( "Explode Stacked Pin" ) )
.Tooltip( _( "Convert a pin with stacked notation to multiple individual pins" ) )
.Icon( BITMAPS::pin ) );
TOOL_ACTION SCH_ACTIONS::breakWire( TOOL_ACTION_ARGS()
.Name( "eeschema.InteractiveEdit.breakWire" )
.Scope( AS_GLOBAL )

View File

@ -148,6 +148,8 @@ public:
static TOOL_ACTION editSymbolLibraryLinks;
static TOOL_ACTION symbolProperties;
static TOOL_ACTION pinTable;
static TOOL_ACTION convertStackedPins;
static TOOL_ACTION explodeStackedPin;
static TOOL_ACTION changeSymbols;
static TOOL_ACTION updateSymbols;
static TOOL_ACTION changeSymbol;

View File

@ -658,7 +658,7 @@ private:
{
std::vector<SCH_PIN*> pins;
for( SCH_PIN* pin : aSymbol.GetPins( aUnit, 0 ) )
for( SCH_PIN* pin : aSymbol.GetGraphicalPins( aUnit, 0 ) )
{
// Figure out if the pin "connects" to the line
const VECTOR2I pinRootPos = pin->GetPinRoot();

View File

@ -99,6 +99,70 @@ bool SYMBOL_EDITOR_EDIT_TOOL::Init()
SCH_TABLECELL_T,
} );
const auto canConvertStackedPins =
[&]( const SELECTION& sel )
{
// If multiple pins are selected, check they are all at same location
if( sel.Size() >= 2 )
{
std::vector<SCH_PIN*> pins;
for( EDA_ITEM* item : sel )
{
if( item->Type() != SCH_PIN_T )
return false;
pins.push_back( static_cast<SCH_PIN*>( item ) );
}
// Check that all pins are at the same location
VECTOR2I pos = pins[0]->GetPosition();
for( size_t i = 1; i < pins.size(); ++i )
{
if( pins[i]->GetPosition() != pos )
return false;
}
return true;
}
// If single pin is selected, check if there are other pins at same location
if( sel.Size() == 1 && sel.Front()->Type() == SCH_PIN_T )
{
SCH_PIN* selectedPin = static_cast<SCH_PIN*>( sel.Front() );
VECTOR2I pos = selectedPin->GetPosition();
// Get the symbol and check for other pins at same location
LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
if( !symbol )
return false;
int coLocatedCount = 0;
for( SCH_PIN* pin : symbol->GetPins() )
{
if( pin->GetPosition() == pos )
{
coLocatedCount++;
if( coLocatedCount >= 2 )
return true;
}
}
}
return false;
};
const auto canExplodeStackedPin =
[&]( const SELECTION& sel )
{
if( sel.Size() != 1 || sel.Front()->Type() != SCH_PIN_T )
return false;
SCH_PIN* pin = static_cast<SCH_PIN*>( sel.Front() );
bool isValid;
std::vector<wxString> stackedNumbers = pin->GetStackedPinNumbers( &isValid );
return isValid && stackedNumbers.size() > 1;
};
// clang-format off
// Add edit actions to the move tool menu
if( moveTool )
@ -148,6 +212,10 @@ bool SYMBOL_EDITOR_EDIT_TOOL::Init()
selToolMenu.AddItem( SCH_ACTIONS::swap, canEdit && SELECTION_CONDITIONS::MoreThan( 1 ), 200 );
selToolMenu.AddItem( SCH_ACTIONS::properties, canEdit && SCH_CONDITIONS::Count( 1 ), 200 );
selToolMenu.AddSeparator( 250 );
selToolMenu.AddItem( SCH_ACTIONS::convertStackedPins, canEdit && canConvertStackedPins, 250 );
selToolMenu.AddItem( SCH_ACTIONS::explodeStackedPin, canEdit && canExplodeStackedPin, 250 );
selToolMenu.AddSeparator( 300 );
selToolMenu.AddItem( ACTIONS::cut, SCH_CONDITIONS::IdleSelection, 300 );
selToolMenu.AddItem( ACTIONS::copy, SCH_CONDITIONS::IdleSelection, 300 );
@ -712,6 +780,330 @@ int SYMBOL_EDITOR_EDIT_TOOL::PinTable( const TOOL_EVENT& aEvent )
}
int SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins( const TOOL_EVENT& aEvent )
{
SCH_COMMIT commit( m_frame );
LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
if( !symbol )
return 0;
SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
wxCHECK( selTool, -1 );
SCH_SELECTION& selection = selTool->GetSelection();
// Collect pins to convert - accept pins with any number format
std::vector<SCH_PIN*> pinsToConvert;
if( selection.Size() == 1 && selection.Front()->Type() == SCH_PIN_T )
{
// Single pin selected - find all pins at the same location
SCH_PIN* selectedPin = static_cast<SCH_PIN*>( selection.Front() );
VECTOR2I pos = selectedPin->GetPosition();
for( SCH_PIN* pin : symbol->GetPins() )
{
if( pin->GetPosition() == pos )
pinsToConvert.push_back( pin );
}
}
else
{
// Multiple pins selected - use them directly, accepting any pin numbers
for( EDA_ITEM* item : selection )
{
if( item->Type() == SCH_PIN_T )
pinsToConvert.push_back( static_cast<SCH_PIN*>( item ) );
}
}
if( pinsToConvert.size() < 2 )
{
m_frame->ShowInfoBarError( _( "At least two pins are needed to convert to stacked pins" ) );
return 0;
}
// Check that all pins are at the same location
VECTOR2I pos = pinsToConvert[0]->GetPosition();
for( size_t i = 1; i < pinsToConvert.size(); ++i )
{
if( pinsToConvert[i]->GetPosition() != pos )
{
m_frame->ShowInfoBarError( _( "All pins must be at the same location" ) );
return 0;
}
}
commit.Modify( symbol, m_frame->GetScreen() );
// Clear selection before modifying pins, like the Delete command does
m_toolMgr->RunAction( ACTIONS::selectionClear );
// Sort pins for consistent ordering - handle arbitrary pin number formats
std::sort( pinsToConvert.begin(), pinsToConvert.end(),
[]( SCH_PIN* a, SCH_PIN* b )
{
wxString numA = a->GetNumber();
wxString numB = b->GetNumber();
// Try to convert to integers for proper numeric sorting
long longA, longB;
bool aIsNumeric = numA.ToLong( &longA );
bool bIsNumeric = numB.ToLong( &longB );
// Both are purely numeric - sort numerically
if( aIsNumeric && bIsNumeric )
return longA < longB;
// Mixed numeric/non-numeric - numeric pins come first
if( aIsNumeric && !bIsNumeric )
return true;
if( !aIsNumeric && bIsNumeric )
return false;
// Both non-numeric or mixed alphanumeric - use lexicographic sorting
return numA < numB;
});
// Build the stacked notation string with range collapsing
wxString stackedNotation = wxT("[");
// Helper function to collapse consecutive numbers into ranges - handles arbitrary pin formats
auto collapseRanges = [&]() -> wxString
{
if( pinsToConvert.empty() )
return wxT("");
wxString result;
// Group pins by their alphanumeric prefix for range collapsing
std::map<wxString, std::vector<long>> prefixGroups;
std::vector<wxString> nonNumericPins;
// Parse each pin number to separate prefix from numeric suffix
for( SCH_PIN* pin : pinsToConvert )
{
wxString pinNumber = pin->GetNumber();
// Skip empty pin numbers (shouldn't happen, but be defensive)
if( pinNumber.IsEmpty() )
{
nonNumericPins.push_back( wxT("(empty)") );
continue;
}
wxString prefix;
wxString numericPart;
// Find where numeric part starts (scan from end)
size_t numStart = pinNumber.length();
for( int i = pinNumber.length() - 1; i >= 0; i-- )
{
if( !wxIsdigit( pinNumber[i] ) )
{
numStart = i + 1;
break;
}
if( i == 0 ) // All digits
numStart = 0;
}
if( numStart < pinNumber.length() ) // Has numeric suffix
{
prefix = pinNumber.Left( numStart );
numericPart = pinNumber.Mid( numStart );
long numValue;
if( numericPart.ToLong( &numValue ) && numValue >= 0 ) // Valid non-negative number
{
prefixGroups[prefix].push_back( numValue );
}
else
{
// Numeric part couldn't be parsed or is negative - treat as non-numeric
nonNumericPins.push_back( pinNumber );
}
}
else // No numeric suffix - consolidate as individual value
{
nonNumericPins.push_back( pinNumber );
}
}
// Process each prefix group
for( auto& [prefix, numbers] : prefixGroups )
{
if( !result.IsEmpty() )
result += wxT(",");
// Sort numeric values for this prefix
std::sort( numbers.begin(), numbers.end() );
// Collapse consecutive ranges within this prefix
size_t i = 0;
while( i < numbers.size() )
{
if( i > 0 ) // Not first number in this prefix group
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number with prefix
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%s%ld-%s%ld"), prefix, start, prefix, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%s%ld,%s%ld"), prefix, start, prefix, end );
else // Single number
result += wxString::Format( wxT("%s%ld"), prefix, start );
i++;
}
}
// Add non-numeric pin numbers as individual comma-separated values
for( const wxString& nonNum : nonNumericPins )
{
if( !result.IsEmpty() )
result += wxT(",");
result += nonNum;
}
return result;
};
stackedNotation += collapseRanges();
stackedNotation += wxT("]");
// Keep the first pin and give it the stacked notation
SCH_PIN* masterPin = pinsToConvert[0];
masterPin->SetNumber( stackedNotation );
// Log information about pins being removed before we remove them
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "Converting %zu pins to stacked notation '%s'",
pinsToConvert.size(), stackedNotation ) );
// Remove all other pins from the symbol that were consolidated into the stacked notation
// Collect pins to remove first, then remove them all at once like the Delete command
std::vector<SCH_PIN*> pinsToRemove;
for( size_t i = 1; i < pinsToConvert.size(); ++i )
{
SCH_PIN* pinToRemove = pinsToConvert[i];
// Log the pin before removing it
wxLogTrace( "KICAD_STACKED_PINS",
wxString::Format( "Will remove pin '%s' at position (%d, %d)",
pinToRemove->GetNumber(),
pinToRemove->GetPosition().x,
pinToRemove->GetPosition().y ) );
pinsToRemove.push_back( pinToRemove );
}
// Remove all pins at once, like the Delete command does
for( SCH_PIN* pin : pinsToRemove )
{
symbol->RemoveDrawItem( pin );
}
commit.Push( wxString::Format( _( "Convert %zu Stacked Pins to '%s'" ),
pinsToConvert.size(), stackedNotation ) );
m_frame->RebuildView();
return 0;
}
int SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin( const TOOL_EVENT& aEvent )
{
SCH_COMMIT commit( m_frame );
LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
if( !symbol )
return 0;
SCH_SELECTION_TOOL* selTool = m_toolMgr->GetTool<SCH_SELECTION_TOOL>();
wxCHECK( selTool, -1 );
SCH_SELECTION& selection = selTool->GetSelection();
if( selection.GetSize() != 1 || selection.Front()->Type() != SCH_PIN_T )
{
m_frame->ShowInfoBarError( _( "Select a single pin with stacked notation to explode" ) );
return 0;
}
SCH_PIN* pin = static_cast<SCH_PIN*>( selection.Front() );
// Check if the pin has stacked notation
bool isValid;
std::vector<wxString> stackedNumbers = pin->GetStackedPinNumbers( &isValid );
if( !isValid || stackedNumbers.size() <= 1 )
{
m_frame->ShowInfoBarError( _( "Selected pin does not have valid stacked notation" ) );
return 0;
}
commit.Modify( symbol, m_frame->GetScreen() );
// Clear selection before modifying pins
m_toolMgr->RunAction( ACTIONS::selectionClear );
// Sort the stacked numbers to find the smallest one
std::sort( stackedNumbers.begin(), stackedNumbers.end(),
[]( const wxString& a, const wxString& b )
{
// Try to convert to integers for proper numeric sorting
long numA, numB;
if( a.ToLong( &numA ) && b.ToLong( &numB ) )
return numA < numB;
// Fall back to string comparison if not numeric
return a < b;
});
// Change the original pin to use the first (smallest) number and make it visible
pin->SetNumber( stackedNumbers[0] );
pin->SetVisible( true );
// Create additional pins for the remaining numbers and make them invisible
for( size_t i = 1; i < stackedNumbers.size(); ++i )
{
SCH_PIN* newPin = new SCH_PIN( symbol );
// Copy all properties from the original pin
newPin->SetPosition( pin->GetPosition() );
newPin->SetOrientation( pin->GetOrientation() );
newPin->SetShape( pin->GetShape() );
newPin->SetLength( pin->GetLength() );
newPin->SetType( pin->GetType() );
newPin->SetName( pin->GetName() );
newPin->SetNumber( stackedNumbers[i] );
newPin->SetNameTextSize( pin->GetNameTextSize() );
newPin->SetNumberTextSize( pin->GetNumberTextSize() );
newPin->SetUnit( pin->GetUnit() );
newPin->SetBodyStyle( pin->GetBodyStyle() );
newPin->SetVisible( false ); // Make all other pins invisible
// Add the new pin to the symbol
symbol->AddDrawItem( newPin );
}
commit.Push( _( "Explode Stacked Pin" ) );
m_frame->RebuildView();
return 0;
}
int SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields( const TOOL_EVENT& aEvent )
{
LIB_SYMBOL* symbol = m_frame->GetCurSymbol();
@ -1021,6 +1413,8 @@ void SYMBOL_EDITOR_EDIT_TOOL::setTransitions()
Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties, SCH_ACTIONS::properties.MakeEvent() );
Go( &SYMBOL_EDITOR_EDIT_TOOL::Properties, SCH_ACTIONS::symbolProperties.MakeEvent() );
Go( &SYMBOL_EDITOR_EDIT_TOOL::PinTable, SCH_ACTIONS::pinTable.MakeEvent() );
Go( &SYMBOL_EDITOR_EDIT_TOOL::ConvertStackedPins, SCH_ACTIONS::convertStackedPins.MakeEvent() );
Go( &SYMBOL_EDITOR_EDIT_TOOL::ExplodeStackedPin, SCH_ACTIONS::explodeStackedPin.MakeEvent() );
Go( &SYMBOL_EDITOR_EDIT_TOOL::UpdateSymbolFields, SCH_ACTIONS::updateSymbolFields.MakeEvent() );
// clang-format on
}

View File

@ -49,6 +49,8 @@ public:
int Properties( const TOOL_EVENT& aEvent );
int PinTable( const TOOL_EVENT& aEvent );
int ConvertStackedPins( const TOOL_EVENT& aEvent );
int ExplodeStackedPin( const TOOL_EVENT& aEvent );
int UpdateSymbolFields( const TOOL_EVENT& aEvent );
int Undo( const TOOL_EVENT& aEvent );

View File

@ -632,7 +632,7 @@ void PANEL_SYMBOL_CHOOSER::populateFootprintSelector( LIB_ID const& aLibId )
if( symbol != nullptr )
{
int pinCount = symbol->GetPins( 0 /* all units */, 1 /* single bodyStyle */ ).size();
int pinCount = symbol->GetGraphicalPins( 0 /* all units */, 1 /* single bodyStyle */ ).size();
SCH_FIELD* fp_field = symbol->GetField( FIELD_T::FOOTPRINT );
wxString fp_name = fp_field ? fp_field->GetFullText() : wxString( "" );

View File

@ -476,5 +476,23 @@ KICOMMON_API wxString From_UTF8( const char* cstring );
*/
KICOMMON_API wxString NormalizeFileUri( const wxString& aFileUri );
/**
* Expand stacked pin notation like [1,2,3], [1-4], [A1-A4], or [AA1-AA3,AB4,CD12-CD14]
* into individual pin numbers, supporting both numeric and alphanumeric pin prefixes.
*
* Examples:
* "[1,2,3]" -> {"1", "2", "3"}
* "[1-4]" -> {"1", "2", "3", "4"}
* "[A1-A3]" -> {"A1", "A2", "A3"}
* "[AA1-AA3,AB4]" -> {"AA1", "AA2", "AA3", "AB4"}
* "5" -> {"5"} (non-bracketed pins returned as-is)
*
* @param aPinName is the pin name to expand (may or may not use stacked notation)
* @param aValid is optionally set to indicate whether the notation was valid
* @return vector of individual pin numbers
*/
KICOMMON_API std::vector<wxString> ExpandStackedPinNotation( const wxString& aPinName,
bool* aValid = nullptr );
#endif // STRING_UTILS_H

View File

@ -469,6 +469,8 @@ void FOOTPRINT_CHOOSER_FRAME::KiwayMailIn( KIWAY_EXPRESS& mail )
{
case MAIL_SYMBOL_NETLIST:
{
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "MAIL_SYMBOL_NETLIST received: size=%zu" ),
payload.size() );
wxSizer* filtersSizer = m_chooserPanel->GetFiltersSizer();
wxWindow* filtersWindow = filtersSizer->GetContainingWindow();
wxString msg;
@ -486,10 +488,24 @@ void FOOTPRINT_CHOOSER_FRAME::KiwayMailIn( KIWAY_EXPRESS& mail )
if( strings.size() >= 1 && !strings[0].empty() )
{
for( const wxString& pin : wxSplit( strings[0], '\t' ) )
wxArrayString tokens = wxSplit( strings[0], '\t' );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "First line entries=%u" ), (unsigned) tokens.size() );
for( const wxString& pin : tokens )
pinNames[ pin.BeforeFirst( ' ' ) ] = pin.AfterFirst( ' ' );
m_pinCount = (int) pinNames.size();
wxString pinList;
for( const auto& kv : pinNames )
{
if( !pinList.IsEmpty() )
pinList << wxS( "," );
pinList << kv.first;
}
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Parsed pins=%d -> [%s]" ), m_pinCount, pinList );
}
if( strings.size() >= 2 && !strings[1].empty() )
@ -532,6 +548,7 @@ void FOOTPRINT_CHOOSER_FRAME::KiwayMailIn( KIWAY_EXPRESS& mail )
if( m_pinCount > 0 )
{
msg.Printf( _( "Filter by pin count (%d)" ), m_pinCount );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "Pin-count label: %s" ), msg );
if( !m_filterByPinCount )
{
@ -557,7 +574,9 @@ void FOOTPRINT_CHOOSER_FRAME::KiwayMailIn( KIWAY_EXPRESS& mail )
m_filterByPinCount->Hide();
}
m_chooserPanel->GetViewerPanel()->SetPinFunctions( pinNames );
m_chooserPanel->GetViewerPanel()->SetPinFunctions( pinNames );
wxLogTrace( "FOOTPRINT_CHOOSER", wxS( "SetPinFunctions called with %zu entries" ),
pinNames.size() );
// Save the wxFormBuilder size of the dialog...
if( s_dialogRect.GetSize().x == 0 || s_dialogRect.GetSize().y == 0 )

View File

@ -45,6 +45,7 @@
#include <netlist_reader/pcb_netlist.h>
#include <connectivity/connectivity_data.h>
#include <reporter.h>
#include <wx/log.h>
#include "board_netlist_updater.h"
@ -965,14 +966,28 @@ bool BOARD_NETLIST_UPDATER::updateComponentPadConnections( FOOTPRINT* aFootprint
{
const COMPONENT_NET& net = aNewComponent->GetNet( pad->GetNumber() );
wxLogTrace( wxT( "NETLIST_UPDATE" ),
wxT( "Processing pad %s of component %s" ),
pad->GetNumber(),
aNewComponent->GetReference() );
wxString pinFunction;
wxString pinType;
if( net.IsValid() ) // i.e. the pad has a name
{
wxLogTrace( wxT( "NETLIST_UPDATE" ),
wxT( " Found valid net: %s" ),
net.GetNetName() );
pinFunction = net.GetPinFunction();
pinType = net.GetPinType();
}
else
{
wxLogTrace( wxT( "NETLIST_UPDATE" ),
wxT( " No net found for pad %s" ),
pad->GetNumber() );
}
if( !m_isDryRun )
{

View File

@ -104,6 +104,7 @@ void KICAD_NETLIST_PARSER::Parse()
break;
case T_components: // The section comp starts here.
wxLogTrace( "CVPCB_PINCOUNT", wxT( "Parse: entering components section" ) );
while( ( token = NextTok() ) != T_EOF )
{
if( token == T_RIGHT )
@ -132,6 +133,7 @@ void KICAD_NETLIST_PARSER::Parse()
break;
case T_nets: // The section nets starts here.
wxLogTrace( "CVPCB_PINCOUNT", wxT( "Parse: entering nets section" ) );
while( ( token = NextTok() ) != T_EOF )
{
if( token == T_RIGHT )
@ -146,6 +148,7 @@ void KICAD_NETLIST_PARSER::Parse()
break;
case T_libparts: // The section libparts starts here.
wxLogTrace( "CVPCB_PINCOUNT", wxT( "Parse: entering libparts section" ) );
while( ( token = NextTok() ) != T_EOF )
{
if( token == T_RIGHT )
@ -384,6 +387,8 @@ void KICAD_NETLIST_PARSER::parseComponent()
Expecting( "part, lib or description" );
}
}
wxLogTrace( "CVPCB_PINCOUNT", wxT( "parseComponent: ref='%s' libsource='%s:%s'" ),
ref, library, name );
break;
case T_property:
@ -714,6 +719,7 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
int pinCount = 0;
// The last token read was libpart, so read the next token
wxLogTrace( "CVPCB_PINCOUNT", wxT( "parseLibPartList: begin libpart" ) );
while( (token = NextTok() ) != T_RIGHT )
{
if( token == T_LEFT )
@ -773,6 +779,8 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
break;
case T_pins:
wxLogTrace( "CVPCB_PINCOUNT", wxT( "parseLibPartList: entering pins for '%s:%s'" ),
libName, libPartName );
while( (token = NextTok() ) != T_RIGHT )
{
if( token == T_LEFT )
@ -782,9 +790,13 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
Expecting( T_pin );
pinCount++;
wxLogTrace( "CVPCB_PINCOUNT", wxT( "parseLibPartList: pin #%d for '%s:%s'" ),
pinCount, libName, libPartName );
skipCurrent();
}
wxLogTrace( "CVPCB_PINCOUNT", wxT( "Parsed libpart '%s:%s' pins => pinCount=%d" ),
libName, libPartName, pinCount );
break;
default:
@ -795,6 +807,8 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
}
// Find all of the components that reference this component library part definition.
wxLogTrace( "CVPCB_PINCOUNT", wxT( "parseLibPartList: assigning pinCount=%d for libpart '%s:%s'" ),
pinCount, libName, libPartName );
for( unsigned i = 0; i < m_netlist->GetCount(); i++ )
{
component = m_netlist->GetComponent( i );
@ -803,6 +817,8 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
{
component->SetFootprintFilters( footprintFilters );
component->SetPinCount( pinCount );
wxLogTrace( "CVPCB_PINCOUNT", wxT( "Assign pinCount=%d to component ref='%s' part='%s:%s'" ),
pinCount, component->GetReference(), libName, libPartName );
}
for( unsigned jj = 0; jj < aliases.GetCount(); jj++ )
@ -811,6 +827,9 @@ void KICAD_NETLIST_PARSER::parseLibPartList()
{
component->SetFootprintFilters( footprintFilters );
component->SetPinCount( pinCount );
wxLogTrace( "CVPCB_PINCOUNT",
wxT( "Assign pinCount=%d to component ref='%s' via alias='%s:%s'" ),
pinCount, component->GetReference(), libName, aliases[jj] );
}
}

View File

@ -29,6 +29,8 @@
#include <footprint.h>
#include <richio.h>
#include <string_utils.h>
#include <wx/tokenzr.h>
#include <wx/log.h>
int COMPONENT_NET::Format( OUTPUTFORMATTER* aOut, int aNestLevel, int aCtl )
@ -60,14 +62,56 @@ void COMPONENT::SetFootprint( FOOTPRINT* aFootprint )
COMPONENT_NET COMPONENT::m_emptyNet;
const COMPONENT_NET& COMPONENT::GetNet( const wxString& aPinName ) const
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( "Looking for pin '%s' in component '%s'" ),
aPinName, m_reference );
for( const COMPONENT_NET& net : m_nets )
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " Checking net pin name '%s'" ),
net.GetPinName() );
if( net.GetPinName() == aPinName )
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " Found exact match for pin '%s'" ),
aPinName );
return net;
}
// Check if this net's pin name is a stacked pin notation that expands to include aPinName
std::vector<wxString> expandedPins = ExpandStackedPinNotation( net.GetPinName() );
if( !expandedPins.empty() )
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " Pin name '%s' expanded to %zu pins" ),
net.GetPinName(), expandedPins.size() );
for( const wxString& expandedPin : expandedPins )
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " Checking expanded pin '%s'" ),
expandedPin );
if( expandedPin == aPinName )
{
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " Found match for pin '%s' in stacked notation '%s'" ),
aPinName, net.GetPinName() );
return net;
}
}
}
}
wxLogTrace( wxT( "NETLIST_STACKED_PINS" ),
wxT( " No net found for pin '%s'" ),
aPinName );
return m_emptyNet;
}

View File

@ -0,0 +1,128 @@
(kicad_sch (version 20240910) (generator eeschema)
(paper "A4")
(lib_symbols
(symbol "Device:R"
(pin_names (offset 0))
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "R1" (at 6.35 0 90)
(effects (font (size 1.27 1.27))) )
(property "Value" "R" (at 3.81 0 90)
(effects (font (size 1.27 1.27))) )
(property "Footprint" "" (at -1.778 0 90) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Datasheet" "" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Description" "Resistor" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "ki_keywords" "R res resistor" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "ki_fp_filters" "R_*" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(symbol "R_0_1"
(rectangle (start -1.016 -2.54) (end 1.016 2.54)
(stroke (width 0.254) (type default)) (fill (type none)) ) )
(symbol "R_1_1"
(pin passive line (at 0 6.35 270) (length 1.27)
(name "" (effects (font (size 1.27 1.27))))
(number "[1-5]" (effects (font (size 1.27 1.27)))) )
(pin passive line (at 0 -8.636 90) (length 1.27)
(name "" (effects (font (size 1.27 1.27))))
(number "[6,7,9-11]" (effects (font (size 1.27 1.27)))) ) ) )
(symbol "power:GND"
(power global)
(pin_numbers (hide yes))
(pin_names (offset 0) (hide yes))
(exclude_from_sim no) (in_bom yes) (on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "#PWR" (at 0 -6.35 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Value" "GND" (at 0 -3.81 0)
(effects (font (size 1.27 1.27))) )
(property "Footprint" "" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Datasheet" "" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 0 0 0) (hide yes) (effects (font (size 1.27 1.27))) )
(property "ki_keywords" "global power" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(symbol "GND_0_1"
(polyline (pts (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27))
(stroke (width 0) (type default)) (fill (type none)) ) )
(symbol "GND_1_1"
(pin power_in line (at 0 0 270) (length 0)
(name "" (effects (font (size 1.27 1.27))))
(number "1" (effects (font (size 1.27 1.27)))) ) ) )
(symbol "power:VCC"
(power global)
(pin_numbers (hide yes))
(pin_names (offset 0) (hide yes))
(exclude_from_sim no) (in_bom yes) (on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "#PWR" (at 0 -3.81 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Value" "VCC" (at 0 3.556 0)
(effects (font (size 1.27 1.27))) )
(property "Footprint" "" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Datasheet" "" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(property "Description" "Power symbol creates a global label with name \"VCC\""
(at 0 0 0) (hide yes) (effects (font (size 1.27 1.27))) )
(property "ki_keywords" "global power" (at 0 0 0) (hide yes)
(effects (font (size 1.27 1.27))) )
(symbol "VCC_0_1"
(polyline (pts (xy -0.762 1.27) (xy 0 2.54)) (stroke (width 0) (type default)) (fill (type none)))
(polyline (pts (xy 0 2.54) (xy 0.762 1.27)) (stroke (width 0) (type default)) (fill (type none)))
(polyline (pts (xy 0 0) (xy 0 2.54)) (stroke (width 0) (type default)) (fill (type none))) )
(symbol "VCC_1_1"
(pin power_in line (at 0 0 90) (length 0)
(name "" (effects (font (size 1.27 1.27))))
(number "1" (effects (font (size 1.27 1.27)))) ) ) )
)
(wire (pts (xy 134.62 83.82) (xy 134.62 76.2)) (stroke (width 0) (type default)))
(wire (pts (xy 100.33 87.63) (xy 100.33 83.82)) (stroke (width 0) (type default)))
(wire (pts (xy 100.33 83.82) (xy 109.22 83.82)) (stroke (width 0) (type default)))
(wire (pts (xy 124.206 83.82) (xy 134.62 83.82)) (stroke (width 0) (type default)))
(symbol (lib_id "Device:R") (at 115.57 83.82 90) (unit 1) (body_style 1)
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced yes)
(uuid "2166cf4e-5b03-495b-af0a-be2718b61b04")
(property "Reference" "R1" (at 115.57 77.47 90) (effects (font (size 1.27 1.27))))
(property "Value" "R" (at 115.57 80.01 90) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 115.57 85.598 90) (hide yes) (effects (font (size 1.27 1.27))))
(property "Datasheet" "" (at 115.57 83.82 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Description" "Resistor" (at 115.57 83.82 0) (hide yes) (effects (font (size 1.27 1.27))))
(pin "[6,7,9-11]" (uuid "f62cbf3f-56f0-413b-a710-93a48027efbb"))
(pin "[1-5]" (uuid "e856ce98-c7c6-4066-97a8-60e95541ae81")) )
(symbol (lib_id "power:VCC") (at 134.62 76.2 0) (unit 1) (body_style 1)
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced yes)
(uuid "2b72c13a-dfcb-4980-a25b-daac640cdb9b")
(property "Reference" "#PWR01" (at 134.62 80.01 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Value" "VCC" (at 134.62 71.12 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 134.62 76.2 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Datasheet" "" (at 134.62 76.2 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Description" "Power symbol creates a global label with name \"VCC\""
(at 134.62 76.2 0) (hide yes) (effects (font (size 1.27 1.27))))
(pin "1" (uuid "ff7185fb-347f-48f5-bcf0-de5808edd725")) )
(symbol (lib_id "power:GND") (at 100.33 87.63 0) (unit 1) (body_style 1)
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (fields_autoplaced yes)
(uuid "4e64c428-4aed-4183-826f-2fa5009f4177")
(property "Reference" "#PWR02" (at 100.33 93.98 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Value" "GND" (at 100.33 92.71 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "" (at 100.33 87.63 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Datasheet" "" (at 100.33 87.63 0) (hide yes) (effects (font (size 1.27 1.27))))
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 100.33 87.63 0) (hide yes) (effects (font (size 1.27 1.27))))
(pin "1" (uuid "b8c00030-efa1-4421-9af4-303142b1c17d")) )
)

View File

@ -0,0 +1,872 @@
(kicad_pcb
(version 20250901)
(generator "pcbnew")
(generator_version "9.99")
(general
(thickness 1.6)
(legacy_teardrops no)
)
(paper "A4")
(layers
(0 "F.Cu" signal)
(2 "B.Cu" signal)
(9 "F.Adhes" user "F.Adhesive")
(11 "B.Adhes" user "B.Adhesive")
(13 "F.Paste" user)
(15 "B.Paste" user)
(5 "F.SilkS" user "F.Silkscreen")
(7 "B.SilkS" user "B.Silkscreen")
(1 "F.Mask" user)
(3 "B.Mask" user)
(17 "Dwgs.User" user "User.Drawings")
(19 "Cmts.User" user "User.Comments")
(21 "Eco1.User" user "User.Eco1")
(23 "Eco2.User" user "User.Eco2")
(25 "Edge.Cuts" user)
(27 "Margin" user)
(31 "F.CrtYd" user "F.Courtyard")
(29 "B.CrtYd" user "B.Courtyard")
(35 "F.Fab" user)
(33 "B.Fab" user)
(39 "User.1" user)
(41 "User.2" user)
(43 "User.3" user)
(45 "User.4" user)
)
(setup
(pad_to_mask_clearance 0)
(allow_soldermask_bridges_in_footprints no)
(tenting
(front yes)
(back yes)
)
(covering
(front no)
(back no)
)
(plugging
(front no)
(back no)
)
(capping no)
(filling no)
(pcbplotparams
(layerselection 0x00000000_00000000_55555555_5755f5ff)
(plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000)
(disableapertmacros no)
(usegerberextensions no)
(usegerberattributes yes)
(usegerberadvancedattributes yes)
(creategerberjobfile yes)
(dashed_line_dash_ratio 12)
(dashed_line_gap_ratio 3)
(svgprecision 4)
(plotframeref no)
(mode 1)
(useauxorigin no)
(pdf_front_fp_property_popups yes)
(pdf_back_fp_property_popups yes)
(pdf_metadata yes)
(pdf_single_document no)
(dxfpolygonmode yes)
(dxfimperialunits yes)
(dxfusepcbnewfont yes)
(psnegative no)
(psa4output no)
(plot_black_and_white yes)
(sketchpadsonfab no)
(plotpadnumbers no)
(hidednponfab no)
(sketchdnponfab yes)
(crossoutdnponfab yes)
(subtractmaskfromsilk no)
(outputformat 1)
(mirror no)
(drillshape 1)
(scaleselection 1)
(outputdirectory "")
)
)
(net 0 "")
(net 1 "GND")
(net 2 "Net-(R1-Pad6)")
(net 3 "VCC")
(footprint "Connector:Tag-Connect_TC2050-IDC-FP_2x05_P1.27mm_Vertical"
(layer "F.Cu")
(uuid "6be81952-937a-4957-9eb2-85721ef5e835")
(at 99.15 82.8)
(descr "Tag-Connect programming header; http://www.tag-connect.com/Materials/TC2050-IDC-430%20Datasheet.pdf")
(tags "tag connect programming header pogo pins")
(property "Reference" "R1"
(at 0 5 0)
(layer "F.SilkS")
(uuid "a58bcfb3-ac5c-44e3-b078-f6fcbbff78fd")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "R"
(at 0 -4.8 0)
(layer "F.Fab")
(uuid "e812b7b1-3211-42eb-ba2d-aa11bdc756e0")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "49f5b1e8-966f-4ec6-9eec-08f47e66d0f8")
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Resistor"
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "f75f79bb-0bac-443b-8a9e-c180b3d4a9f0")
(effects
(font
(size 1.27 1.27)
)
)
)
(property ki_fp_filters "R_*")
(path "/2166cf4e-5b03-495b-af0a-be2718b61b04")
(sheetname "/")
(sheetfile "stacked.kicad_sch")
(attr exclude_from_pos_files)
(duplicate_pad_numbers_are_jumpers no)
(fp_line
(start -3.175 1.27)
(end -3.175 0.635)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "27aa3b0d-a092-4b0c-995a-8e3cec800dbe")
)
(fp_line
(start -2.54 1.27)
(end -3.175 1.27)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "bee2c633-f143-428a-9485-6a636fd6de5c")
)
(fp_line
(start -5.5 -4.25)
(end 4.75 -4.25)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "aae4190a-8a8d-4a1a-b456-2957c835b850")
)
(fp_line
(start -5.5 4.25)
(end -5.5 -4.25)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "921c2c67-e2cf-45e5-a409-21d7b43c6aa3")
)
(fp_line
(start 4.75 -4.25)
(end 4.75 4.25)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "f9324654-e75c-4d7f-adfb-f8bb0d60c1c9")
)
(fp_line
(start 4.75 4.25)
(end -5.5 4.25)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "bcca5d74-54fb-4c6c-a8dc-039d2882a8a4")
)
(fp_text user "KEEPOUT"
(at 0 0 0)
(layer "Cmts.User")
(uuid "3f2c9b29-16de-427a-938d-6921987ed7d4")
(effects
(font
(size 0.4 0.4)
(thickness 0.07)
)
)
)
(fp_text user "${REFERENCE}"
(at 0 0 0)
(layer "F.Fab")
(uuid "720463bd-308b-464f-83d1-706bd97b4b09")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "" np_thru_hole circle
(at -3.81 -2.54)
(size 2.3749 2.3749)
(drill 2.3749)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "e669af60-91a5-43ae-9c40-cb3349ad6cd9")
)
(pad "" np_thru_hole circle
(at -3.81 0)
(size 0.9906 0.9906)
(drill 0.9906)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "a25c573c-0aaa-4e39-8a8a-87edb26c8eba")
)
(pad "" np_thru_hole circle
(at -3.81 2.54)
(size 2.3749 2.3749)
(drill 2.3749)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "28203947-6c7f-4fda-8630-083dbbb2065b")
)
(pad "" np_thru_hole circle
(at 1.905 -2.54)
(size 2.3749 2.3749)
(drill 2.3749)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "d64457ca-c860-4e9e-8d02-773e7b597037")
)
(pad "" np_thru_hole circle
(at 1.905 2.54)
(size 2.3749 2.3749)
(drill 2.3749)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "ecc5a508-d2f3-487b-98b8-7a2095646b49")
)
(pad "" np_thru_hole circle
(at 3.81 -1.016)
(size 0.9906 0.9906)
(drill 0.9906)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "e35c5901-0bbb-48bd-bb2a-0e208b25c86d")
)
(pad "" np_thru_hole circle
(at 3.81 1.016)
(size 0.9906 0.9906)
(drill 0.9906)
(layers "*.Cu" "*.Mask")
(tenting
(front none)
(back none)
)
(uuid "d17e6355-636f-4a2e-9a07-dac92e07dbd3")
)
(pad "1" connect circle
(at -2.54 0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 1 "GND")
(pinfunction "1")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "0d1d056c-2f36-4152-9a5c-117b6f43d4e3")
)
(pad "2" connect circle
(at -1.27 0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 1 "GND")
(pinfunction "2")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "7835c4d6-293f-470f-9732-986fc9802a90")
)
(pad "3" connect circle
(at 0 0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 1 "GND")
(pinfunction "3")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "ea592653-d244-4bb8-b4ed-ee92882d3777")
)
(pad "4" connect circle
(at 1.27 0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 1 "GND")
(pinfunction "4")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "5e78183f-956b-4f05-a129-15f4e5fe3a0c")
)
(pad "5" connect circle
(at 2.54 0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 1 "GND")
(pinfunction "5")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "6e86d938-6846-476f-83b5-1d8afafe7361")
)
(pad "6" connect circle
(at 2.54 -0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 2 "Net-(R1-Pad6)")
(pinfunction "6")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "6b67537c-a5db-4c19-a020-c8fcd9f7acf8")
)
(pad "7" connect circle
(at 1.27 -0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 2 "Net-(R1-Pad6)")
(pinfunction "7")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "d0255722-889d-41bc-920a-0463b6ac11fc")
)
(pad "8" connect circle
(at 0 -0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(tenting
(front none)
(back none)
)
(uuid "0b67effe-459a-49bc-8c65-e7053f564265")
)
(pad "9" connect circle
(at -1.27 -0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 2 "Net-(R1-Pad6)")
(pinfunction "9")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "1dab4fb0-b39d-4753-ab2a-a5f4f1e8aac1")
)
(pad "10" connect circle
(at -2.54 -0.635)
(size 0.7874 0.7874)
(layers "F.Cu" "F.Mask")
(net 2 "Net-(R1-Pad6)")
(pinfunction "10")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "46e85dda-2b66-4d09-a062-916d33de6a9e")
)
(zone
(net 0)
(net_name "")
(layer "F.Cu")
(uuid "8ef75162-3f7e-4efc-8b0a-4b51b175c2d3")
(hatch full 0.508)
(connect_pads
(clearance 0)
)
(min_thickness 0.254)
(keepout
(tracks allowed)
(vias not_allowed)
(pads allowed)
(copperpour not_allowed)
(footprints not_allowed)
)
(placement
(enabled no)
(sheetname "")
)
(fill
(thermal_gap 0.508)
(thermal_bridge_width 0.508)
(island_removal_mode 0)
)
(polygon
(pts
(xy 96.61 83.435) (xy 101.69 83.435) (xy 101.69 82.165) (xy 96.61 82.165)
)
)
)
(embedded_fonts no)
)
(footprint "Capacitor_SMD:CP_Elec_3x5.4"
(layer "F.Cu")
(uuid "f38d6383-ca09-42d5-84bc-8ed2ee9e1fb5")
(at 96.5 90)
(descr "SMD capacitor, aluminum electrolytic, Nichicon, 3.0x5.4mm")
(tags "capacitor electrolytic")
(property "Reference" "X1"
(at 0 -2.7 0)
(layer "F.SilkS")
(uuid "8fda368e-86c3-4111-b138-69e3967c1f68")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "C"
(at 0 2.7 0)
(layer "F.Fab")
(uuid "1e3a282d-c685-413d-bf0c-edca1617eef8")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "96d28378-e62a-4b33-90cc-9202b68830f3")
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Unpolarized capacitor"
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "78264291-21af-4cc0-b361-f72ab21292c3")
(effects
(font
(size 1.27 1.27)
)
)
)
(property ki_fp_filters "C_*")
(path "/e65020b4-b867-4d63-a46c-163f7210b6bb")
(sheetname "/")
(sheetfile "stacked.kicad_sch")
(attr smd)
(duplicate_pad_numbers_are_jumpers no)
(fp_line
(start -2.375 -1.435)
(end -2 -1.435)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "7f79c690-cb60-4051-ad22-422351f6db5f")
)
(fp_line
(start -2.1875 -1.6225)
(end -2.1875 -1.2475)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "5c447aaa-d1e7-4fa3-9001-ac553d20662f")
)
(fp_line
(start -1.570563 -1.06)
(end -0.870563 -1.76)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "b111b33d-2499-4768-8eac-089403f3ec2b")
)
(fp_line
(start -1.570563 1.06)
(end -0.870563 1.76)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "edde24dd-77e1-41f4-89bb-5243e8aca75b")
)
(fp_line
(start -0.870563 -1.76)
(end 1.76 -1.76)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "d87eb71e-8e64-4d2a-a08f-3d9c164bd7b2")
)
(fp_line
(start -0.870563 1.76)
(end 1.76 1.76)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "66e18111-d83b-402d-b5bf-4272faf95588")
)
(fp_line
(start 1.76 -1.76)
(end 1.76 -1.06)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "3d5b5c44-9238-471b-8363-30d163faeaa9")
)
(fp_line
(start 1.76 1.76)
(end 1.76 1.06)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "3f598ead-79d7-4fd5-8825-a2ede4af679e")
)
(fp_line
(start -2.85 -1.05)
(end -2.85 1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "334da6ac-b694-478b-9cd1-8c83468d7391")
)
(fp_line
(start -2.85 1.05)
(end -1.78 1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "31c9fc6d-fb51-4a4c-b881-c2c07c346518")
)
(fp_line
(start -1.78 -1.05)
(end -2.85 -1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "a00caec7-86a6-4c68-a3fc-934a3d99d2d4")
)
(fp_line
(start -1.78 -1.05)
(end -0.93 -1.9)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "322cbb81-71f3-413e-b949-ccee52c5fbb0")
)
(fp_line
(start -1.78 1.05)
(end -0.93 1.9)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "e530520a-ddcd-42a3-9c5d-4d2a1f3154d3")
)
(fp_line
(start -0.93 -1.9)
(end 1.9 -1.9)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "987d6ecb-c12d-45d9-9619-904468fee663")
)
(fp_line
(start -0.93 1.9)
(end 1.9 1.9)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "d355660a-f52e-4ee5-9fbb-9b86dbbe4282")
)
(fp_line
(start 1.9 -1.9)
(end 1.9 -1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "c9c0c7e3-9efc-4a85-8de7-3a6319ff7a44")
)
(fp_line
(start 1.9 -1.05)
(end 2.85 -1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "a5329e51-5a4e-4a5c-aafd-07e647458f36")
)
(fp_line
(start 1.9 1.05)
(end 1.9 1.9)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "4e327b6e-6096-4ec1-abdc-e5aadd5ce3b4")
)
(fp_line
(start 2.85 -1.05)
(end 2.85 1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "2ba849f1-a85e-4b96-baeb-988620c13ad1")
)
(fp_line
(start 2.85 1.05)
(end 1.9 1.05)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "cc4ec5b8-906b-4358-9df6-626341ba4078")
)
(fp_line
(start -1.65 -0.825)
(end -1.65 0.825)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "d09f4d86-628e-4106-8571-3557d500000e")
)
(fp_line
(start -1.65 -0.825)
(end -0.825 -1.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "5a3a7862-c16f-49e9-a4a5-7301da652b4b")
)
(fp_line
(start -1.65 0.825)
(end -0.825 1.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "6f9a2956-13af-4d8c-a116-e81678cf86b7")
)
(fp_line
(start -1.110469 -0.8)
(end -0.810469 -0.8)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "6480eda9-f610-45f9-9815-aa694aa23157")
)
(fp_line
(start -0.960469 -0.95)
(end -0.960469 -0.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "2f8eccda-c7ba-4ac5-8b4a-1bdee552ce2e")
)
(fp_line
(start -0.825 -1.65)
(end 1.65 -1.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "26170f0f-c996-4313-a1ba-3f7eb878fcb0")
)
(fp_line
(start -0.825 1.65)
(end 1.65 1.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "dfac16eb-141f-4383-93a1-185012c558dc")
)
(fp_line
(start 1.65 -1.65)
(end 1.65 1.65)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "7ead1618-1e1b-485f-a82b-60b82c9a147a")
)
(fp_circle
(center 0 0)
(end 1.5 0)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "5ee3d052-a5df-4679-917d-290393b4ac4a")
)
(fp_text user "${REFERENCE}"
(at 0 0 0)
(layer "F.Fab")
(uuid "587b737e-6398-4d6d-9ee7-7db10fa2e562")
(effects
(font
(size 0.6 0.6)
(thickness 0.09)
)
)
)
(pad "1" smd roundrect
(at -1.5 0)
(size 2.2 1.6)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15625)
(net 2 "Net-(R1-Pad6)")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "595baab1-9b66-41dd-838b-67f9f7f008e9")
)
(pad "2" smd roundrect
(at 1.5 0)
(size 2.2 1.6)
(layers "F.Cu" "F.Mask" "F.Paste")
(roundrect_rratio 0.15625)
(net 3 "VCC")
(pintype "passive")
(tenting
(front none)
(back none)
)
(uuid "d75d5164-5460-42c3-9d1d-54ff2da81393")
)
(embedded_fonts no)
(model "${KICAD8_3DMODEL_DIR}/Capacitor_SMD.3dshapes/CP_Elec_3x5.4.wrl"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)
(embedded_fonts no)
)

View File

@ -0,0 +1,632 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 2.54
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.5
}
},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.0,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"ground_pin_not_ground": "warning",
"hier_label_mismatch": "error",
"isolated_pin_label": "warning",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"stacked_pin_name": "warning",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "stacked.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"tuning_profile": "",
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"annotation": {
"method": 0,
"sort_order": 0
},
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "Default Editing",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"hop_over_size_choice": 0,
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"page_layout_descr_file": "",
"plot_directory": "",
"reuse_designators": true,
"subpart_first_id": 65,
"subpart_id_separator": 0,
"used_designators": "C1-2"
},
"sheets": [
[
"f3609c98-5e8f-46ae-a21a-09d0b96782a9",
"Root"
]
],
"text_variables": {},
"time_domain_parameters": {
"delay_profiles_user_defined": [],
"meta": {
"version": 0
}
}
}

View File

@ -0,0 +1,827 @@
(kicad_sch
(version 20250829)
(generator "eeschema")
(generator_version "9.99")
(uuid "f3609c98-5e8f-46ae-a21a-09d0b96782a9")
(paper "A4")
(lib_symbols
(symbol "Device:C"
(pin_numbers
(hide yes)
)
(pin_names
(offset 0.254)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "C"
(at 0.635 2.54 0)
(effects
(font
(size 1.27 1.27)
)
(justify left)
)
)
(property "Value" "C"
(at 0.635 -2.54 0)
(effects
(font
(size 1.27 1.27)
)
(justify left)
)
)
(property "Footprint" ""
(at 0.9652 -3.81 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Unpolarized capacitor"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_keywords" "cap capacitor"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_fp_filters" "C_*"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(symbol "C_0_1"
(polyline
(pts
(xy -2.032 0.762) (xy 2.032 0.762)
)
(stroke
(width 0.508)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy -2.032 -0.762) (xy 2.032 -0.762)
)
(stroke
(width 0.508)
(type default)
)
(fill
(type none)
)
)
)
(symbol "C_1_1"
(pin passive line
(at 0 3.81 270)
(length 2.794)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 0 -3.81 90)
(length 2.794)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "2"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "Device:R"
(pin_names
(offset 0)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "R1"
(at 6.35 0 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "R"
(at 3.81 0 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at -1.778 0 90)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Resistor"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_keywords" "R res resistor"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_fp_filters" "R_*"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(symbol "R_0_1"
(rectangle
(start -1.016 -2.54)
(end 1.016 2.54)
(stroke
(width 0.254)
(type default)
)
(fill
(type none)
)
)
)
(symbol "R_1_1"
(pin passive line
(at 0 6.35 270)
(length 1.27)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "[1-5]"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin passive line
(at 0 -8.636 90)
(length 1.27)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "[6,7,9-11]"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "power:GND"
(power global)
(pin_numbers
(hide yes)
)
(pin_names
(offset 0)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "#PWR"
(at 0 -6.35 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "GND"
(at 0 -3.81 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_keywords" "global power"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(symbol "GND_0_1"
(polyline
(pts
(xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "GND_1_1"
(pin power_in line
(at 0 0 270)
(length 0)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "power:VCC"
(power global)
(pin_numbers
(hide yes)
)
(pin_names
(offset 0)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(duplicate_pin_numbers_are_jumpers no)
(property "Reference" "#PWR"
(at 0 -3.81 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "VCC"
(at 0 3.556 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Power symbol creates a global label with name \"VCC\""
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "ki_keywords" "global power"
(at 0 0 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(symbol "VCC_0_1"
(polyline
(pts
(xy -0.762 1.27) (xy 0 2.54)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0 2.54) (xy 0.762 1.27)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
(polyline
(pts
(xy 0 0) (xy 0 2.54)
)
(stroke
(width 0)
(type default)
)
(fill
(type none)
)
)
)
(symbol "VCC_1_1"
(pin power_in line
(at 0 0 90)
(length 0)
(name ""
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
)
(wire
(pts
(xy 124.206 83.82) (xy 134.62 83.82)
)
(stroke
(width 0)
(type default)
)
(uuid "977d311c-737b-4d33-a7c0-027068800d27")
)
(wire
(pts
(xy 100.33 87.63) (xy 100.33 83.82)
)
(stroke
(width 0)
(type default)
)
(uuid "a74e3420-cc0c-4d70-a108-b408a357a728")
)
(wire
(pts
(xy 100.33 83.82) (xy 109.22 83.82)
)
(stroke
(width 0)
(type default)
)
(uuid "b640eecc-a8af-45c1-97a3-9a874bceb1f7")
)
(wire
(pts
(xy 152.4 83.82) (xy 152.4 76.2)
)
(stroke
(width 0)
(type default)
)
(uuid "b86edfbb-be66-4823-bf66-7580fa3069d2")
)
(wire
(pts
(xy 142.24 83.82) (xy 152.4 83.82)
)
(stroke
(width 0)
(type default)
)
(uuid "fdc72005-7acb-48aa-85c0-6d26e865dc00")
)
(symbol
(lib_id "Device:R")
(at 115.57 83.82 90)
(unit 1)
(body_style 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "2166cf4e-5b03-495b-af0a-be2718b61b04")
(property "Reference" "R1"
(at 116.713 77.47 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "R"
(at 116.713 80.01 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" "Connector:Tag-Connect_TC2050-IDC-FP_2x05_P1.27mm_Vertical"
(at 115.57 85.598 90)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 115.57 83.82 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Resistor"
(at 115.57 83.82 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(pin "[6,7,9-11]"
(uuid "f62cbf3f-56f0-413b-a710-93a48027efbb")
)
(pin "[1-5]"
(uuid "e856ce98-c7c6-4066-97a8-60e95541ae81")
)
(instances
(project ""
(path "/f3609c98-5e8f-46ae-a21a-09d0b96782a9"
(reference "R1")
(unit 1)
)
)
)
)
(symbol
(lib_id "power:VCC")
(at 152.4 76.2 0)
(unit 1)
(body_style 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "2b72c13a-dfcb-4980-a25b-daac640cdb9b")
(property "Reference" "#PWR01"
(at 152.4 80.01 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "VCC"
(at 152.4 71.12 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 152.4 76.2 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 152.4 76.2 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Power symbol creates a global label with name \"VCC\""
(at 152.4 76.2 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(pin "1"
(uuid "ff7185fb-347f-48f5-bcf0-de5808edd725")
)
(instances
(project ""
(path "/f3609c98-5e8f-46ae-a21a-09d0b96782a9"
(reference "#PWR01")
(unit 1)
)
)
)
)
(symbol
(lib_id "power:GND")
(at 100.33 87.63 0)
(unit 1)
(body_style 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "4e64c428-4aed-4183-826f-2fa5009f4177")
(property "Reference" "#PWR02"
(at 100.33 93.98 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "GND"
(at 100.33 92.71 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 100.33 87.63 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 100.33 87.63 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 100.33 87.63 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(pin "1"
(uuid "b8c00030-efa1-4421-9af4-303142b1c17d")
)
(instances
(project ""
(path "/f3609c98-5e8f-46ae-a21a-09d0b96782a9"
(reference "#PWR02")
(unit 1)
)
)
)
)
(symbol
(lib_id "Device:C")
(at 138.43 83.82 90)
(unit 1)
(body_style 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "e65020b4-b867-4d63-a46c-163f7210b6bb")
(property "Reference" "X1"
(at 138.43 76.2 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "C"
(at 138.43 78.74 90)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" "Capacitor_SMD:CP_Elec_3x5.4"
(at 142.24 82.8548 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Datasheet" ""
(at 138.43 83.82 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Description" "Unpolarized capacitor"
(at 138.43 83.82 0)
(hide yes)
(effects
(font
(size 1.27 1.27)
)
)
)
(pin "2"
(uuid "30431dfb-acd4-46c0-af86-7738834b40fd")
)
(pin "1"
(uuid "047a8bd9-b214-498e-b161-2c9a3c6501f3")
)
(instances
(project ""
(path "/f3609c98-5e8f-46ae-a21a-09d0b96782a9"
(reference "X1")
(unit 1)
)
)
)
)
(sheet_instances
(path "/"
(page "1")
)
)
(embedded_fonts no)
)

View File

@ -94,6 +94,10 @@ set( QA_EESCHEMA_SRCS
test_sch_symbol.cpp
test_schematic.cpp
test_symbol_library_manager.cpp
test_stacked_pin_nomenclature.cpp
test_stacked_pin_conversion.cpp
test_pin_stacked_layout.cpp
test_netlist_exporter_xml_stacked.cpp
test_resolve_drivers.cpp
test_saveas_copy_subsheets.cpp
test_update_items_connectivity.cpp

View File

@ -0,0 +1,150 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers
*
* 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 at
* http://www.gnu.org/licenses/
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <schematic_utils/schematic_file_util.h>
#include <set>
#include <vector>
#include <schematic.h>
#include <settings/settings_manager.h>
#include <netlist_exporter_xml.h>
#include <wx/filename.h>
#include <wx/xml/xml.h>
struct XML_STACKED_PIN_FIXTURE
{
XML_STACKED_PIN_FIXTURE() : m_settingsManager( true /* headless */ ) {}
SETTINGS_MANAGER m_settingsManager;
std::unique_ptr<SCHEMATIC> m_schematic;
};
static std::set<wxString> as_set( const std::initializer_list<const char*>& init )
{
std::set<wxString> out;
for( const char* s : init )
out.emplace( wxString::FromUTF8( s ) );
return out;
}
static wxXmlNode* find_child( wxXmlNode* parent, const wxString& name )
{
for( wxXmlNode* child = parent->GetChildren(); child; child = child->GetNext() )
{
if( child->GetName() == name )
return child;
}
return nullptr;
}
static std::vector<wxXmlNode*> find_children( wxXmlNode* parent, const wxString& name )
{
std::vector<wxXmlNode*> out;
for( wxXmlNode* child = parent->GetChildren(); child; child = child->GetNext() )
{
if( child->GetName() == name )
out.push_back( child );
}
return out;
}
BOOST_FIXTURE_TEST_CASE( NetlistExporterXML_StackedPinNomenclature, XML_STACKED_PIN_FIXTURE )
{
// Load schematic with stacked pin numbers
KI_TEST::LoadSchematic( m_settingsManager, wxT( "stacked_pin_nomenclature" ), m_schematic );
// Write XML netlist to a test file next to the project
wxFileName netFile = m_schematic->Project().GetProjectFullName();
netFile.SetName( netFile.GetName() + wxT( "_xml_test" ) );
netFile.SetExt( wxT( "xml" ) );
if( wxFileExists( netFile.GetFullPath() ) )
wxRemoveFile( netFile.GetFullPath() );
WX_STRING_REPORTER reporter;
std::unique_ptr<NETLIST_EXPORTER_XML> exporter =
std::make_unique<NETLIST_EXPORTER_XML>( m_schematic.get() );
bool success = exporter->WriteNetlist( netFile.GetFullPath(), 0, reporter );
BOOST_REQUIRE( success && reporter.GetMessages().IsEmpty() );
// Parse the XML back
wxXmlDocument xdoc;
BOOST_REQUIRE( xdoc.Load( netFile.GetFullPath() ) );
wxXmlNode* root = xdoc.GetRoot();
BOOST_REQUIRE( root );
wxXmlNode* nets = find_child( root, wxT( "nets" ) );
BOOST_REQUIRE( nets );
// Collect pin sets for R1 on each power net
std::set<wxString> setA;
std::set<wxString> setB;
int foundSets = 0;
for( wxXmlNode* net : find_children( nets, wxT( "net" ) ) )
{
wxString netName = net->GetAttribute( wxT( "name" ), wxEmptyString );
if( netName != wxT( "VCC" ) && netName != wxT( "GND" ) )
continue;
std::set<wxString>* target = ( foundSets == 0 ? &setA : &setB );
for( wxXmlNode* node : find_children( net, wxT( "node" ) ) )
{
if( node->GetAttribute( wxT( "ref" ), wxEmptyString ) != wxT( "R1" ) )
continue;
wxString pin = node->GetAttribute( wxT( "pin" ), wxEmptyString );
wxString pinfunction = node->GetAttribute( wxT( "pinfunction" ), wxEmptyString );
wxString pintype = node->GetAttribute( wxT( "pintype" ), wxEmptyString );
// Expect pinfunction to equal the expanded number when base name is empty
BOOST_CHECK_EQUAL( pinfunction, pin );
// Expect plain passive type (no +no_connect on these nets)
BOOST_CHECK_EQUAL( pintype, wxT( "passive" ) );
target->insert( pin );
}
foundSets++;
}
// We should have found two power nets with R1 nodes
BOOST_REQUIRE_EQUAL( foundSets, 2 );
// Expect one side to be 1..5 and the other to be 6,7,9,10,11 (order independent)
const std::set<wxString> expectedTop = as_set( { "1", "2", "3", "4", "5" } );
const std::set<wxString> expectedBot = as_set( { "6", "7", "9", "10", "11" } );
bool matchA = ( setA == expectedTop && setB == expectedBot );
bool matchB = ( setA == expectedBot && setB == expectedTop );
BOOST_CHECK( matchA || matchB );
// Cleanup test artifact
wxRemoveFile( netFile.GetFullPath() );
}

View File

@ -0,0 +1,543 @@
/*
* 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
*/
/**
* @file test_pin_stacked_layout.cpp
* Test pin number layout for stacked multi-line numbers across all rotations
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <sch_pin.h>
#include <lib_symbol.h>
#include <pin_layout_cache.h>
#include <transform.h>
#include <sch_io/sch_io_mgr.h>
#include <wx/log.h>
#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_SUITE( PinStackedLayout )
/**
* Create a test symbol with stacked pin numbers for rotation testing
*/
static std::unique_ptr<LIB_SYMBOL> createTestResistorSymbol()
{
auto symbol = std::make_unique<LIB_SYMBOL>( wxT( "TestResistor" ) );
// Create first pin with stacked numbers [1-5]
auto pin1 = std::make_unique<SCH_PIN>( symbol.get() );
pin1->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( 250 ) ) ); // top pin
pin1->SetOrientation( PIN_ORIENTATION::PIN_DOWN );
pin1->SetLength( schIUScale.MilsToIU( 50 ) );
pin1->SetNumber( wxT( "[1-5]" ) );
pin1->SetName( wxT( "A" ) ); // Short name
pin1->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
pin1->SetUnit( 1 );
// Create second pin with stacked numbers [6,7,9-11]
auto pin2 = std::make_unique<SCH_PIN>( symbol.get() );
pin2->SetPosition( VECTOR2I( 0, schIUScale.MilsToIU( -340 ) ) ); // bottom pin
pin2->SetOrientation( PIN_ORIENTATION::PIN_UP );
pin2->SetLength( schIUScale.MilsToIU( 50 ) );
pin2->SetNumber( wxT( "[6,7,9-11]" ) );
pin2->SetName( wxT( "B" ) ); // Short name
pin2->SetType( ELECTRICAL_PINTYPE::PT_PASSIVE );
pin2->SetUnit( 1 );
// Add pins to symbol
symbol->AddDrawItem( pin1.release() );
symbol->AddDrawItem( pin2.release() );
return symbol;
}
/**
* Get pin geometry (line segment from connection point to pin end)
*/
static VECTOR2I getPinLineEnd( const SCH_PIN* pin, const TRANSFORM& transform )
{
VECTOR2I start = pin->GetPosition();
VECTOR2I end = start;
int length = pin->GetLength();
switch( pin->PinDrawOrient( transform ) )
{
case PIN_ORIENTATION::PIN_UP:
end.y += length;
break;
case PIN_ORIENTATION::PIN_DOWN:
end.y -= length;
break;
case PIN_ORIENTATION::PIN_LEFT:
end.x -= length;
break;
case PIN_ORIENTATION::PIN_RIGHT:
end.x += length;
break;
case PIN_ORIENTATION::INHERIT:
default:
break;
}
return end;
}
/**
* Check if a box intersects with a line segment
*/
static bool boxIntersectsLine( const BOX2I& box, const VECTOR2I& lineStart, const VECTOR2I& lineEnd )
{
// Simple bbox vs line segment intersection
// First check if line bbox intersects text bbox
BOX2I lineBbox;
lineBbox.SetOrigin( std::min( lineStart.x, lineEnd.x ), std::min( lineStart.y, lineEnd.y ) );
lineBbox.SetEnd( std::max( lineStart.x, lineEnd.x ), std::max( lineStart.y, lineEnd.y ) );
if( !lineBbox.Intersects( box ) )
return false;
// For vertical/horizontal lines, do precise check
if( lineStart.x == lineEnd.x ) // vertical line
{
int lineX = lineStart.x;
return ( lineX >= box.GetLeft() && lineX <= box.GetRight() &&
box.GetTop() <= std::max( lineStart.y, lineEnd.y ) &&
box.GetBottom() >= std::min( lineStart.y, lineEnd.y ) );
}
else if( lineStart.y == lineEnd.y ) // horizontal line
{
int lineY = lineStart.y;
return ( lineY >= box.GetBottom() && lineY <= box.GetTop() &&
box.GetLeft() <= std::max( lineStart.x, lineEnd.x ) &&
box.GetRight() >= std::min( lineStart.x, lineEnd.x ) );
}
// For diagonal lines, use the bbox intersection as approximation
return true;
}
/**
* Test that pin numbers don't overlap with pin geometry across all rotations
*/
BOOST_AUTO_TEST_CASE( PinNumbersNoOverlapAllRotations )
{
// Create test symbol
auto symbol = createTestResistorSymbol();
BOOST_REQUIRE( symbol );
// Get the pins
std::vector<SCH_PIN*> pins;
for( auto& item : symbol->GetDrawItems() )
{
if( item.Type() == SCH_PIN_T )
pins.push_back( static_cast<SCH_PIN*>( &item ) );
}
BOOST_REQUIRE_EQUAL( pins.size(), 2 );
// Test rotations: 0°, 90°, 180°, 270°
std::vector<TRANSFORM> rotations = {
TRANSFORM( 1, 0, 0, 1 ), // 0° (identity)
TRANSFORM( 0, -1, 1, 0 ), // 90° CCW
TRANSFORM( -1, 0, 0, -1 ), // 180°
TRANSFORM( 0, 1, -1, 0 ) // 270° CCW (90° CW)
};
std::vector<wxString> rotationNames = { wxT(""), wxT("90°"), wxT("180°"), wxT("270°") };
for( size_t r = 0; r < rotations.size(); r++ )
{
const TRANSFORM& transform = rotations[r];
const wxString& rotName = rotationNames[r];
// Set global transform for this test
TRANSFORM oldTransform = DefaultTransform;
DefaultTransform = transform;
for( size_t p = 0; p < pins.size(); p++ )
{
SCH_PIN* pin = pins[p];
// Create layout cache for this pin
PIN_LAYOUT_CACHE cache( *pin );
// Get pin number text info (shadow width 0 for testing)
auto numberInfoOpt = cache.GetPinNumberInfo( 0 );
if( !numberInfoOpt.has_value() )
continue;
PIN_LAYOUT_CACHE::TEXT_INFO numberInfo = numberInfoOpt.value();
if( numberInfo.m_Text.IsEmpty() )
continue;
// Get pin line geometry
VECTOR2I pinStart = pin->GetPosition();
VECTOR2I pinEnd = getPinLineEnd( pin, transform );
// Get text bounding box - we need to estimate this since we don't have full font rendering
// For now, use a simple estimation based on text size and string length
int textHeight = numberInfo.m_TextSize;
int textWidth = numberInfo.m_Text.Length() * numberInfo.m_TextSize * 0.6; // rough char width
// Handle multi-line text
if( numberInfo.m_Text.Contains( '\n' ) )
{
wxArrayString lines;
wxStringSplit( numberInfo.m_Text, lines, '\n' );
if( numberInfo.m_Angle == ANGLE_VERTICAL )
{
// For vertical text, lines are spaced horizontally
int lineSpacing = textHeight * 1.3;
textWidth = lines.size() * lineSpacing;
// Find longest line for height
size_t maxLen = 0;
for( const wxString& line : lines )
maxLen = std::max( maxLen, line.Length() );
textHeight = maxLen * textHeight * 0.6;
}
else
{
// For horizontal text, lines are spaced vertically
int lineSpacing = textHeight * 1.3;
textHeight = lines.size() * lineSpacing;
// Find longest line for width
size_t maxLen = 0;
for( const wxString& line : lines )
maxLen = std::max( maxLen, line.Length() );
textWidth = maxLen * textHeight * 0.6;
}
}
// Create text bounding box around text position
BOX2I textBbox;
textBbox.SetOrigin( numberInfo.m_TextPosition.x - textWidth/2,
numberInfo.m_TextPosition.y - textHeight/2 );
textBbox.SetSize( textWidth, textHeight );
// Check for intersection
bool overlaps = boxIntersectsLine( textBbox, pinStart, pinEnd );
// Log detailed info for debugging
wxLogMessage( wxT("Rotation %s, Pin %s: pos=(%d,%d) textPos=(%d,%d) pinLine=(%d,%d)-(%d,%d) textBox=(%d,%d,%dx%d) overlap=%s"),
rotName, pin->GetNumber(),
pinStart.x, pinStart.y,
numberInfo.m_TextPosition.x, numberInfo.m_TextPosition.y,
pinStart.x, pinStart.y, pinEnd.x, pinEnd.y,
(int)textBbox.GetLeft(), (int)textBbox.GetTop(), (int)textBbox.GetWidth(), (int)textBbox.GetHeight(),
overlaps ? wxT("YES") : wxT("NO") );
// Test assertion
BOOST_CHECK_MESSAGE( !overlaps,
"Pin number '" << pin->GetNumber() << "' overlaps with pin geometry at rotation " << rotName );
}
// Restore original transform
DefaultTransform = oldTransform;
}
}
/**
* Test that multiline and non-multiline pin numbers/names are positioned consistently
* on the same side of the pin for each rotation
*/
BOOST_AUTO_TEST_CASE( PinTextConsistentSidePlacement )
{
// Create test symbol with both types of pins
auto symbol = createTestResistorSymbol();
BOOST_REQUIRE( symbol );
// Get the pins - one will be multiline formatted, one will not
std::vector<SCH_PIN*> pins;
for( auto& item : symbol->GetDrawItems() )
{
if( item.Type() == SCH_PIN_T )
pins.push_back( static_cast<SCH_PIN*>( &item ) );
}
BOOST_REQUIRE_EQUAL( pins.size(), 2 );
// Test rotations
std::vector<TRANSFORM> rotations = {
TRANSFORM( 1, 0, 0, 1 ), // 0° (identity)
TRANSFORM( 0, -1, 1, 0 ), // 90° CCW
TRANSFORM( -1, 0, 0, -1 ), // 180°
TRANSFORM( 0, 1, -1, 0 ) // 270° CCW (90° CW)
};
std::vector<wxString> rotationNames = { wxT(""), wxT("90°"), wxT("180°"), wxT("270°") };
for( size_t r = 0; r < rotations.size(); r++ )
{
const TRANSFORM& transform = rotations[r];
const wxString& rotName = rotationNames[r];
// Set global transform for this test
TRANSFORM oldTransform = DefaultTransform;
DefaultTransform = transform;
// For each rotation, collect pin number and name positions relative to pin center
struct PinTextInfo {
VECTOR2I pinPos;
VECTOR2I numberPos;
VECTOR2I namePos;
wxString pinNumber;
bool isMultiline;
};
std::vector<PinTextInfo> pinInfos;
for( auto* pin : pins )
{
PinTextInfo info;
info.pinPos = pin->GetPosition();
info.pinNumber = pin->GetNumber();
// Create layout cache for this pin
PIN_LAYOUT_CACHE cache( *pin );
// Get number position (shadow width 0 for testing)
auto numberInfoOpt = cache.GetPinNumberInfo( 0 );
if( numberInfoOpt.has_value() )
{
auto numberInfo = numberInfoOpt.value();
info.numberPos = numberInfo.m_TextPosition;
info.isMultiline = numberInfo.m_Text.Contains( '\n' );
}
// Get name position
auto nameInfoOpt = cache.GetPinNameInfo( 0 );
if( nameInfoOpt.has_value() )
{
auto nameInfo = nameInfoOpt.value();
info.namePos = nameInfo.m_TextPosition;
}
pinInfos.push_back( info );
wxLogDebug( "Rotation %s, Pin %s: pos=(%d,%d) numberPos=(%d,%d) namePos=(%d,%d) multiline=%s",
rotName, info.pinNumber,
info.pinPos.x, info.pinPos.y,
info.numberPos.x, info.numberPos.y,
info.namePos.x, info.namePos.y,
info.isMultiline ? wxT("YES") : wxT("NO") );
}
BOOST_REQUIRE_EQUAL( pinInfos.size(), 2 );
// New semantics:
// * Vertical pins (UP/DOWN): numbers and names must be LEFT (x < pin.x)
// * Horizontal pins (LEFT/RIGHT): numbers/names must be ABOVE (y < pin.y)
PIN_ORIENTATION orient = pins[0]->PinDrawOrient( DefaultTransform );
if( orient == PIN_ORIENTATION::PIN_UP || orient == PIN_ORIENTATION::PIN_DOWN )
{
for( const auto& inf : pinInfos )
{
BOOST_CHECK_MESSAGE( inf.numberPos.x < inf.pinPos.x,
"At rotation " << rotName << ", number for pin " << inf.pinNumber << " not left of vertical pin." );
BOOST_CHECK_MESSAGE( inf.namePos.x < inf.pinPos.x,
"At rotation " << rotName << ", name for pin " << inf.pinNumber << " not left of vertical pin." );
}
}
else if( orient == PIN_ORIENTATION::PIN_LEFT || orient == PIN_ORIENTATION::PIN_RIGHT )
{
for( const auto& inf : pinInfos )
{
BOOST_CHECK_MESSAGE( inf.numberPos.y < inf.pinPos.y,
"At rotation " << rotName << ", number for pin " << inf.pinNumber << " not above horizontal pin." );
BOOST_CHECK_MESSAGE( inf.namePos.y < inf.pinPos.y,
"At rotation " << rotName << ", name for pin " << inf.pinNumber << " not above horizontal pin." );
}
}
// Restore original transform
DefaultTransform = oldTransform;
}
}
/**
* Test that multiline and non-multiline pin numbers/names have the same bottom coordinate
* (distance from pin along the axis connecting pin and text)
*/
BOOST_AUTO_TEST_CASE( PinTextSameBottomCoordinate )
{
// Create test symbol with both types of pins
auto symbol = createTestResistorSymbol();
BOOST_REQUIRE( symbol );
// Get the pins - one will be multiline formatted, one will not
std::vector<SCH_PIN*> pins;
for( auto& item : symbol->GetDrawItems() )
{
if( item.Type() == SCH_PIN_T )
pins.push_back( static_cast<SCH_PIN*>( &item ) );
}
BOOST_REQUIRE_EQUAL( pins.size(), 2 );
// Test rotations
std::vector<TRANSFORM> rotations = {
TRANSFORM( 1, 0, 0, 1 ), // 0° (identity)
TRANSFORM( 0, -1, 1, 0 ), // 90° CCW
TRANSFORM( -1, 0, 0, -1 ), // 180°
TRANSFORM( 0, 1, -1, 0 ) // 270° CCW (90° CW)
};
std::vector<wxString> rotationNames = { wxT(""), wxT("90°"), wxT("180°"), wxT("270°") };
for( size_t r = 0; r < rotations.size(); r++ )
{
const TRANSFORM& transform = rotations[r];
const wxString& rotName = rotationNames[r];
// Set global transform for this test
TRANSFORM oldTransform = DefaultTransform;
DefaultTransform = transform;
// For each rotation, collect pin and text position data
struct PinTextData {
VECTOR2I pinPos;
VECTOR2I numberPos;
VECTOR2I namePos;
wxString pinNumber;
bool isMultiline;
int numberBottomDistance;
int nameBottomDistance;
};
std::vector<PinTextData> pinData;
for( auto* pin : pins )
{
PinTextData data;
data.pinPos = pin->GetPosition();
data.pinNumber = pin->GetNumber();
// Create layout cache for this pin
PIN_LAYOUT_CACHE cache( *pin );
// Get number position (shadow width 0 for testing)
auto numberInfoOpt = cache.GetPinNumberInfo( 0 );
PIN_LAYOUT_CACHE::TEXT_INFO numberInfo; // store for later heuristics
if( numberInfoOpt.has_value() )
{
numberInfo = numberInfoOpt.value();
data.numberPos = numberInfo.m_TextPosition;
data.isMultiline = numberInfo.m_Text.Contains( '\n' );
}
else
{
BOOST_FAIL( "Expected pin number text info" );
}
// Get name position
auto nameInfoOpt = cache.GetPinNameInfo( 0 );
PIN_LAYOUT_CACHE::TEXT_INFO nameInfo; // store for width/height heuristic
if( nameInfoOpt.has_value() )
{
nameInfo = nameInfoOpt.value();
data.namePos = nameInfo.m_TextPosition;
}
else
{
BOOST_FAIL( "Expected pin name text info" );
}
// Calculate bottom distance (closest distance to pin along pin-text axis)
PIN_ORIENTATION orient = pin->PinDrawOrient( DefaultTransform );
if( orient == PIN_ORIENTATION::PIN_UP || orient == PIN_ORIENTATION::PIN_DOWN )
{
// Vertical pins: measure clearance from pin (at pin.x) to RIGHT edge of text box.
// We approximate half width from text length heuristic.
int textWidth = data.isMultiline ? 0 : (int)( data.pinNumber.Length() * numberInfo.m_TextSize * 0.6 );
// (Multiline case: numberInfo.m_Text already contains \n; heuristic in earlier section)
if( data.isMultiline )
{
wxArrayString lines; wxStringSplit( numberInfo.m_Text, lines, '\n' );
int lineSpacing = numberInfo.m_TextSize * 1.3;
textWidth = lines.size() * lineSpacing; // when vertical orientation text is rotated
}
int rightEdge = data.numberPos.x + textWidth / 2;
data.numberBottomDistance = data.pinPos.x - rightEdge; // positive gap
int nameWidth = (int)( nameInfo.m_Text.Length() * nameInfo.m_TextSize * 0.6 );
int nameRightEdge = data.namePos.x + nameWidth / 2;
data.nameBottomDistance = data.pinPos.x - nameRightEdge; // expect similar across pins
}
else
{
// Horizontal pins: we align centers at a fixed offset above the pin. Measure center gap.
data.numberBottomDistance = data.pinPos.y - data.numberPos.y; // center gap constant
data.nameBottomDistance = data.pinPos.y - data.namePos.y; // center gap for names
}
pinData.push_back( data );
wxLogDebug( "Rotation %s, Pin %s: pos=(%d,%d) numberPos=(%d,%d) namePos=(%d,%d) multiline=%s numberBottomDist=%d nameBottomDist=%d",
rotName, data.pinNumber,
data.pinPos.x, data.pinPos.y,
data.numberPos.x, data.numberPos.y,
data.namePos.x, data.namePos.y,
data.isMultiline ? wxT("YES") : wxT("NO"),
data.numberBottomDistance, data.nameBottomDistance );
}
BOOST_REQUIRE_EQUAL( pinData.size(), 2 );
// Check that both pins have their numbers at the same bottom distance from pin
// Allow small tolerance for rounding differences
const int tolerance = 100; // 100 internal units tolerance
int bottomDist1 = pinData[0].numberBottomDistance;
int bottomDist2 = pinData[1].numberBottomDistance;
int distanceDiff = abs( bottomDist1 - bottomDist2 );
BOOST_CHECK_MESSAGE( distanceDiff <= tolerance,
"At rotation " << rotName << ", pin numbers have different bottom distances from pin. "
<< "Pin " << pinData[0].pinNumber << " distance=" << bottomDist1
<< ", Pin " << pinData[1].pinNumber << " distance=" << bottomDist2
<< ", difference=" << distanceDiff << " (tolerance=" << tolerance << ")" );
// Check that both pins have their names at the same bottom distance from pin
int nameBottomDist1 = pinData[0].nameBottomDistance;
int nameBottomDist2 = pinData[1].nameBottomDistance;
int nameDistanceDiff = abs( nameBottomDist1 - nameBottomDist2 );
BOOST_CHECK_MESSAGE( nameDistanceDiff <= tolerance,
"At rotation " << rotName << ", pin names have different bottom distances from pin. "
<< "Pin " << pinData[0].pinNumber << " name distance=" << nameBottomDist1
<< ", Pin " << pinData[1].pinNumber << " name distance=" << nameBottomDist2
<< ", difference=" << nameDistanceDiff << " (tolerance=" << tolerance << ")" );
// Restore original transform
DefaultTransform = oldTransform;
}
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -0,0 +1,742 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <sch_pin.h>
#include <lib_symbol.h>
#include <eeschema_test_utils.h>
struct STACKED_PIN_CONVERSION_FIXTURE
{
STACKED_PIN_CONVERSION_FIXTURE()
{
m_settingsManager = std::make_unique<SETTINGS_MANAGER>( true );
m_symbol = std::make_unique<LIB_SYMBOL>( "TestSymbol" );
}
std::unique_ptr<SETTINGS_MANAGER> m_settingsManager;
std::unique_ptr<LIB_SYMBOL> m_symbol;
};
BOOST_FIXTURE_TEST_SUITE( StackedPinConversion, STACKED_PIN_CONVERSION_FIXTURE )
/**
* Test basic stacked pin number expansion functionality
*/
BOOST_AUTO_TEST_CASE( TestStackedPinExpansion )
{
// Test simple list notation
SCH_PIN* pin = new SCH_PIN( m_symbol.get() );
pin->SetNumber( wxT("[1,2,3]") );
bool isValid;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
BOOST_CHECK_EQUAL( expanded[0], "1" );
BOOST_CHECK_EQUAL( expanded[1], "2" );
BOOST_CHECK_EQUAL( expanded[2], "3" );
delete pin;
// Test range notation
pin = new SCH_PIN( m_symbol.get() );
pin->SetNumber( wxT("[5-7]") );
expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
BOOST_CHECK_EQUAL( expanded[0], "5" );
BOOST_CHECK_EQUAL( expanded[1], "6" );
BOOST_CHECK_EQUAL( expanded[2], "7" );
delete pin;
// Test mixed notation
pin = new SCH_PIN( m_symbol.get() );
pin->SetNumber( wxT("[1,3,5-7]") );
expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 5 );
BOOST_CHECK_EQUAL( expanded[0], "1" );
BOOST_CHECK_EQUAL( expanded[1], "3" );
BOOST_CHECK_EQUAL( expanded[2], "5" );
BOOST_CHECK_EQUAL( expanded[3], "6" );
BOOST_CHECK_EQUAL( expanded[4], "7" );
delete pin;
}
/**
* Test stacked pin validity checking
*/
BOOST_AUTO_TEST_CASE( TestStackedPinValidity )
{
SCH_PIN* pin = new SCH_PIN( m_symbol.get() );
// Test valid single pin (should not be considered stacked)
pin->SetNumber( wxT("1") );
bool isValid;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_CHECK_EQUAL( expanded.size(), 1 );
BOOST_CHECK_EQUAL( expanded[0], "1" );
// Test invalid notation (malformed brackets)
pin->SetNumber( wxT("[1,2") );
expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( !isValid );
// Test invalid range
pin->SetNumber( wxT("[5-3]") ); // backwards range
expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( !isValid );
// Test empty brackets
pin->SetNumber( wxT("[]") );
expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( !isValid );
delete pin;
}
/**
* Test pin creation and positioning for conversion tests
*/
BOOST_AUTO_TEST_CASE( TestPinCreation )
{
// Create multiple pins at the same location
VECTOR2I position( 0, 0 );
SCH_PIN* pin1 = new SCH_PIN( m_symbol.get() );
pin1->SetNumber( wxT("1") );
pin1->SetPosition( position );
pin1->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
SCH_PIN* pin2 = new SCH_PIN( m_symbol.get() );
pin2->SetNumber( wxT("2") );
pin2->SetPosition( position );
pin2->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
SCH_PIN* pin3 = new SCH_PIN( m_symbol.get() );
pin3->SetNumber( wxT("3") );
pin3->SetPosition( position );
pin3->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
// Verify pins are at same location
BOOST_CHECK_EQUAL( pin1->GetPosition(), pin2->GetPosition() );
BOOST_CHECK_EQUAL( pin2->GetPosition(), pin3->GetPosition() );
// Test IsStacked functionality
BOOST_CHECK( pin1->IsStacked( pin2 ) );
BOOST_CHECK( pin2->IsStacked( pin3 ) );
delete pin1;
delete pin2;
delete pin3;
}
/**
* Test net name generation with stacked pins
*/
BOOST_AUTO_TEST_CASE( TestStackedPinNetNaming )
{
SCH_PIN* pin = new SCH_PIN( m_symbol.get() );
pin->SetNumber( wxT("[8,9,10]") );
// This test would require a full SCH_SYMBOL context to test GetDefaultNetName
// For now just verify the pin number expansion works
bool isValid;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
// The smallest number should be first for deterministic net naming
BOOST_CHECK_EQUAL( expanded[0], "8" );
delete pin;
}
/**
* Test conversion from multiple co-located pins to stacked notation
*/
BOOST_AUTO_TEST_CASE( TestConvertMultiplePinsToStacked )
{
// Create multiple pins at the same location
VECTOR2I position( 0, 0 );
SCH_PIN* pin1 = new SCH_PIN( m_symbol.get() );
pin1->SetNumber( wxT("1") );
pin1->SetPosition( position );
pin1->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
pin1->SetVisible( true );
SCH_PIN* pin2 = new SCH_PIN( m_symbol.get() );
pin2->SetNumber( wxT("2") );
pin2->SetPosition( position );
pin2->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
pin2->SetVisible( true );
SCH_PIN* pin3 = new SCH_PIN( m_symbol.get() );
pin3->SetNumber( wxT("3") );
pin3->SetPosition( position );
pin3->SetOrientation( PIN_ORIENTATION::PIN_RIGHT );
pin3->SetVisible( true );
// Test basic property access before adding to symbol
BOOST_CHECK_EQUAL( pin1->GetNumber(), "1" );
BOOST_CHECK_EQUAL( pin2->GetNumber(), "2" );
BOOST_CHECK_EQUAL( pin3->GetNumber(), "3" );
// Just test the basic conversion logic without symbol management
// Build the stacked notation string
wxString stackedNotation = wxT("[");
stackedNotation += pin1->GetNumber();
stackedNotation += wxT(",");
stackedNotation += pin2->GetNumber();
stackedNotation += wxT(",");
stackedNotation += pin3->GetNumber();
stackedNotation += wxT("]");
// Test stacked notation creation
BOOST_CHECK_EQUAL( stackedNotation, "[1,2,3]" );
// Set stacked notation on one pin
pin1->SetNumber( stackedNotation );
BOOST_CHECK_EQUAL( pin1->GetNumber(), "[1,2,3]" );
// Verify the stacked pin expansion works
bool isValid;
std::vector<wxString> expanded = pin1->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
BOOST_CHECK_EQUAL( expanded[0], "1" );
BOOST_CHECK_EQUAL( expanded[1], "2" );
BOOST_CHECK_EQUAL( expanded[2], "3" );
// Clean up - delete pins manually since they're not in symbol
delete pin1;
delete pin2;
delete pin3;
}
/**
* Test range collapsing functionality in stacked pin conversion
*/
BOOST_AUTO_TEST_CASE( TestRangeCollapsingConversion )
{
// Test range collapsing logic directly without symbol management
// Test consecutive pins that should collapse to a range
std::vector<long> numbers = { 1, 2, 3, 4 };
// Build collapsed ranges
wxString result;
size_t i = 0;
while( i < numbers.size() )
{
if( !result.IsEmpty() )
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%ld-%ld"), start, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%ld,%ld"), start, end );
else // Single number
result += wxString::Format( wxT("%ld"), start );
i++;
}
// Verify range collapsing: 1,2,3,4 should become "1-4"
BOOST_CHECK_EQUAL( result, "1-4" );
// Test with mixed consecutive and non-consecutive: 1,2,3,4,7,8,9
numbers = { 1, 2, 3, 4, 7, 8, 9 };
result.Clear();
i = 0;
while( i < numbers.size() )
{
if( !result.IsEmpty() )
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%ld-%ld"), start, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%ld,%ld"), start, end );
else // Single number
result += wxString::Format( wxT("%ld"), start );
i++;
}
// Verify mixed ranges: 1,2,3,4,7,8,9 should become "1-4,7-9"
BOOST_CHECK_EQUAL( result, "1-4,7-9" );
// Test edge cases
numbers = { 1, 3, 5 }; // Non-consecutive
result.Clear();
i = 0;
while( i < numbers.size() )
{
if( !result.IsEmpty() )
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%ld-%ld"), start, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%ld,%ld"), start, end );
else // Single number
result += wxString::Format( wxT("%ld"), start );
i++;
}
// Verify non-consecutive: 1,3,5 should remain "1,3,5"
BOOST_CHECK_EQUAL( result, "1,3,5" );
// Test two consecutive numbers
numbers = { 5, 6 };
result.Clear();
i = 0;
while( i < numbers.size() )
{
if( !result.IsEmpty() )
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%ld-%ld"), start, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%ld,%ld"), start, end );
else // Single number
result += wxString::Format( wxT("%ld"), start );
i++;
}
// Verify two consecutive: 5,6 should remain "5,6" (not convert to range)
BOOST_CHECK_EQUAL( result, "5,6" );
// Test complex mixed case: 1,2,4,5,6,8,9,10,11
numbers = { 1, 2, 4, 5, 6, 8, 9, 10, 11 };
result.Clear();
i = 0;
while( i < numbers.size() )
{
if( !result.IsEmpty() )
result += wxT(",");
long start = numbers[i];
long end = start;
// Find the end of consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Add range or single number
if( end > start + 1 ) // Range of 3+ numbers
result += wxString::Format( wxT("%ld-%ld"), start, end );
else if( end == start + 1 ) // Two consecutive numbers
result += wxString::Format( wxT("%ld,%ld"), start, end );
else // Single number
result += wxString::Format( wxT("%ld"), start );
i++;
}
// Verify complex case: 1,2,4,5,6,8,9,10,11 should become "1,2,4-6,8-11"
BOOST_CHECK_EQUAL( result, "1,2,4-6,8-11" );
// Test that our range notation can be expanded back correctly
SCH_PIN* rangePin = new SCH_PIN( m_symbol.get() );
rangePin->SetNumber( wxT("[1-4,7-9]") );
bool isValid;
std::vector<wxString> expanded = rangePin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 7 );
// Should expand to: 1,2,3,4,7,8,9
BOOST_CHECK_EQUAL( expanded[0], "1" );
BOOST_CHECK_EQUAL( expanded[1], "2" );
BOOST_CHECK_EQUAL( expanded[2], "3" );
BOOST_CHECK_EQUAL( expanded[3], "4" );
BOOST_CHECK_EQUAL( expanded[4], "7" );
BOOST_CHECK_EQUAL( expanded[5], "8" );
BOOST_CHECK_EQUAL( expanded[6], "9" );
delete rangePin;
}
/**
* Test round-trip conversion: multiple pins -> stacked -> multiple pins
*/
BOOST_AUTO_TEST_CASE( TestRoundTripConversion )
{
// Create multiple pins at the same location
VECTOR2I position( 100, 200 );
SCH_PIN* pin5 = new SCH_PIN( m_symbol.get() );
pin5->SetNumber( wxT("5") );
pin5->SetPosition( position );
pin5->SetOrientation( PIN_ORIENTATION::PIN_LEFT );
pin5->SetType( ELECTRICAL_PINTYPE::PT_INPUT );
pin5->SetName( wxT("TestPin") );
pin5->SetVisible( true );
SCH_PIN* pin7 = new SCH_PIN( m_symbol.get() );
pin7->SetNumber( wxT("7") );
pin7->SetPosition( position );
pin7->SetOrientation( PIN_ORIENTATION::PIN_LEFT );
pin7->SetType( ELECTRICAL_PINTYPE::PT_INPUT );
pin7->SetName( wxT("TestPin") );
pin7->SetVisible( true );
SCH_PIN* pin9 = new SCH_PIN( m_symbol.get() );
pin9->SetNumber( wxT("9") );
pin9->SetPosition( position );
pin9->SetOrientation( PIN_ORIENTATION::PIN_LEFT );
pin9->SetType( ELECTRICAL_PINTYPE::PT_INPUT );
pin9->SetName( wxT("TestPin") );
pin9->SetVisible( true );
// Store original properties for comparison
PIN_ORIENTATION originalOrientation = pin5->GetOrientation();
ELECTRICAL_PINTYPE originalType = pin5->GetType();
wxString originalName = pin5->GetName();
int originalLength = pin5->GetLength();
// Step 1: Convert to stacked notation (simulating ConvertStackedPins)
std::vector<SCH_PIN*> pinsToConvert = { pin5, pin7, pin9 };
// Sort pins numerically
std::sort( pinsToConvert.begin(), pinsToConvert.end(),
[]( SCH_PIN* a, SCH_PIN* b )
{
long numA, numB;
if( a->GetNumber().ToLong( &numA ) && b->GetNumber().ToLong( &numB ) )
return numA < numB;
return a->GetNumber() < b->GetNumber();
});
// Build stacked notation
wxString stackedNotation = wxT("[5,7,9]");
pinsToConvert[0]->SetNumber( stackedNotation );
// Remove other pins (don't delete them yet for testing)
SCH_PIN* stackedPin = pinsToConvert[0];
// Step 2: Verify stacked notation
bool isValid;
std::vector<wxString> expanded = stackedPin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
BOOST_CHECK_EQUAL( expanded[0], "5" );
BOOST_CHECK_EQUAL( expanded[1], "7" );
BOOST_CHECK_EQUAL( expanded[2], "9" );
// Step 3: Convert back to individual pins (simulating ExplodeStackedPin)
// Sort the stacked numbers (should already be sorted in our case)
std::sort( expanded.begin(), expanded.end(),
[]( const wxString& a, const wxString& b )
{
long numA, numB;
if( a.ToLong( &numA ) && b.ToLong( &numB ) )
return numA < numB;
return a < b;
});
// Change the original pin to use the first (smallest) number and make it visible
stackedPin->SetNumber( expanded[0] );
stackedPin->SetVisible( true );
// Create additional pins for the remaining numbers and make them invisible
std::vector<SCH_PIN*> explodedPins;
explodedPins.push_back( stackedPin );
for( size_t i = 1; i < expanded.size(); ++i )
{
SCH_PIN* newPin = new SCH_PIN( m_symbol.get() );
// Copy all properties from the original pin
newPin->SetPosition( stackedPin->GetPosition() );
newPin->SetOrientation( stackedPin->GetOrientation() );
newPin->SetShape( stackedPin->GetShape() );
newPin->SetLength( stackedPin->GetLength() );
newPin->SetType( stackedPin->GetType() );
newPin->SetName( stackedPin->GetName() );
newPin->SetNumber( expanded[i] );
newPin->SetNameTextSize( stackedPin->GetNameTextSize() );
newPin->SetNumberTextSize( stackedPin->GetNumberTextSize() );
newPin->SetUnit( stackedPin->GetUnit() );
newPin->SetBodyStyle( stackedPin->GetBodyStyle() );
newPin->SetVisible( false ); // Make all other pins invisible
explodedPins.push_back( newPin );
}
// Step 4: Verify the round-trip conversion
BOOST_REQUIRE_EQUAL( explodedPins.size(), 3 );
// Check pin numbers
BOOST_CHECK_EQUAL( explodedPins[0]->GetNumber(), "5" );
BOOST_CHECK_EQUAL( explodedPins[1]->GetNumber(), "7" );
BOOST_CHECK_EQUAL( explodedPins[2]->GetNumber(), "9" );
// Check visibility (only first pin should be visible)
BOOST_CHECK( explodedPins[0]->IsVisible() );
BOOST_CHECK( !explodedPins[1]->IsVisible() );
BOOST_CHECK( !explodedPins[2]->IsVisible() );
// Check that properties were preserved
for( SCH_PIN* pin : explodedPins )
{
BOOST_CHECK_EQUAL( pin->GetPosition(), position );
BOOST_CHECK( pin->GetOrientation() == originalOrientation );
BOOST_CHECK( pin->GetType() == originalType );
BOOST_CHECK_EQUAL( pin->GetName(), originalName );
BOOST_CHECK_EQUAL( pin->GetLength(), originalLength );
}
// Clean up
for( size_t i = 1; i < explodedPins.size(); ++i )
delete explodedPins[i];
// Note: explodedPins[0] is the original stackedPin, don't delete twice
}
/**
* Test visibility behavior during conversions
*/
BOOST_AUTO_TEST_CASE( TestVisibilityHandling )
{
// Create a single pin to test visibility handling
SCH_PIN* pin = new SCH_PIN( m_symbol.get() );
pin->SetNumber( wxT("[8,10,12]") );
pin->SetVisible( false ); // Start invisible
// Test expansion of stacked notation
bool isValid;
std::vector<wxString> expanded = pin->GetStackedPinNumbers( &isValid );
BOOST_CHECK( isValid );
BOOST_REQUIRE_EQUAL( expanded.size(), 3 );
// Sort expanded numbers
std::sort( expanded.begin(), expanded.end(),
[]( const wxString& a, const wxString& b )
{
long numA, numB;
if( a.ToLong( &numA ) && b.ToLong( &numB ) )
return numA < numB;
return a < b;
});
// Verify sorted order is correct
BOOST_CHECK_EQUAL( expanded[0], "8" );
BOOST_CHECK_EQUAL( expanded[1], "10" );
BOOST_CHECK_EQUAL( expanded[2], "12" );
// Set the smallest pin number and make it visible
pin->SetNumber( expanded[0] ); // "8"
pin->SetVisible( true ); // Make visible
// Verify the smallest pin is visible and has correct number
BOOST_CHECK_EQUAL( pin->GetNumber(), "8" );
BOOST_CHECK( pin->IsVisible() );
// Clean up
delete pin;
}
/**
* Test alphanumeric range collapsing functionality
*/
BOOST_AUTO_TEST_CASE( TestAlphanumericRangeCollapsing )
{
// Test the new alphanumeric prefix parsing logic
// Helper function to test prefix parsing
auto testPrefixParsing = []( const wxString& pinNumber ) -> std::pair<wxString, long>
{
wxString prefix;
long numValue = -1;
// Find where numeric part starts (scan from end)
size_t numStart = pinNumber.length();
for( int i = pinNumber.length() - 1; i >= 0; i-- )
{
if( !wxIsdigit( pinNumber[i] ) )
{
numStart = i + 1;
break;
}
if( i == 0 ) // All digits
numStart = 0;
}
if( numStart < pinNumber.length() ) // Has numeric suffix
{
prefix = pinNumber.Left( numStart );
wxString numericPart = pinNumber.Mid( numStart );
numericPart.ToLong( &numValue );
}
return std::make_pair( prefix, numValue );
};
// Test basic prefix parsing
auto [prefix1, num1] = testPrefixParsing( wxT("A1") );
BOOST_CHECK_EQUAL( prefix1, "A" );
BOOST_CHECK_EQUAL( num1, 1 );
auto [prefix2, num2] = testPrefixParsing( wxT("AB12") );
BOOST_CHECK_EQUAL( prefix2, "AB" );
BOOST_CHECK_EQUAL( num2, 12 );
auto [prefix3, num3] = testPrefixParsing( wxT("123") );
BOOST_CHECK_EQUAL( prefix3, "" );
BOOST_CHECK_EQUAL( num3, 123 );
auto [prefix4, num4] = testPrefixParsing( wxT("XYZ") );
BOOST_CHECK_EQUAL( prefix4, "" ); // No numeric suffix
BOOST_CHECK_EQUAL( num4, -1 );
// Test grouping logic with example: AA1,AA2,AA3,AB4,CD12,CD13,CD14
std::map<wxString, std::vector<long>> prefixGroups;
std::vector<wxString> testPins = { wxT("AA1"), wxT("AA2"), wxT("AA3"), wxT("AB4"), wxT("CD12"), wxT("CD13"), wxT("CD14") };
for( const wxString& pinNumber : testPins )
{
auto [prefix, numValue] = testPrefixParsing( pinNumber );
if( numValue != -1 )
prefixGroups[prefix].push_back( numValue );
}
// Verify grouping
BOOST_CHECK_EQUAL( prefixGroups.size(), 3 );
BOOST_CHECK_EQUAL( prefixGroups[wxT("AA")].size(), 3 );
BOOST_CHECK_EQUAL( prefixGroups[wxT("AB")].size(), 1 );
BOOST_CHECK_EQUAL( prefixGroups[wxT("CD")].size(), 3 );
// Build expected result: AA1-AA3,AB4,CD12-CD14
wxString expectedResult;
for( auto& [prefix, numbers] : prefixGroups )
{
if( !expectedResult.IsEmpty() )
expectedResult += wxT(",");
std::sort( numbers.begin(), numbers.end() );
size_t i = 0;
while( i < numbers.size() )
{
if( i > 0 )
expectedResult += wxT(",");
long start = numbers[i];
long end = start;
// Find consecutive sequence
while( i + 1 < numbers.size() && numbers[i + 1] == numbers[i] + 1 )
{
i++;
end = numbers[i];
}
// Format with prefix
if( end > start + 1 ) // Range of 3+ numbers
expectedResult += wxString::Format( wxT("%s%ld-%s%ld"), prefix, start, prefix, end );
else if( end == start + 1 ) // Two consecutive numbers
expectedResult += wxString::Format( wxT("%s%ld,%s%ld"), prefix, start, prefix, end );
else // Single number
expectedResult += wxString::Format( wxT("%s%ld"), prefix, start );
i++;
}
}
// Should result in: AA1-AA3,AB4,CD12-CD14
BOOST_CHECK_EQUAL( expectedResult, "AA1-AA3,AB4,CD12-CD14" );
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -0,0 +1,101 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers
*
* 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 at
* http://www.gnu.org/licenses/
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <schematic_utils/schematic_file_util.h>
#include <algorithm>
#include <vector>
#include <schematic.h>
#include <sch_screen.h>
#include <sch_symbol.h>
#include <sch_pin.h>
#include <settings/settings_manager.h>
#include <locale_io.h>
struct STACKED_PIN_FIXTURE
{
STACKED_PIN_FIXTURE() : m_settingsManager( true /* headless */ ) {}
SETTINGS_MANAGER m_settingsManager;
std::unique_ptr<SCHEMATIC> m_schematic;
};
static std::vector<wxString> ToVector( const std::initializer_list<const char*>& init )
{
std::vector<wxString> out;
for( const char* s : init )
out.emplace_back( wxString::FromUTF8( s ) );
return out;
}
BOOST_FIXTURE_TEST_CASE( StackedPinNomenclature_ExpandsCorrectly, STACKED_PIN_FIXTURE )
{
LOCALE_IO dummy;
// Load the custom schematic with bracketed pin numbers
KI_TEST::LoadSchematic( m_settingsManager, "stacked_pin_nomenclature", m_schematic );
SCH_SHEET_LIST sheets = m_schematic->BuildSheetListSortedByPageNumbers();
BOOST_REQUIRE( sheets.size() >= 1 );
SCH_SCREEN* screen = sheets.at( 0 ).LastScreen();
// Find the Device:R symbol on the sheet
SCH_SYMBOL* resistor = nullptr;
for( SCH_ITEM* item : screen->Items().OfType( SCH_SYMBOL_T ) )
{
SCH_SYMBOL* sym = static_cast<SCH_SYMBOL*>( item );
if( sym->GetSchSymbolLibraryName() == wxT( "Device:R" ) )
{
resistor = sym;
break;
}
}
BOOST_REQUIRE_MESSAGE( resistor, "Resistor symbol not found in test schematic" );
// Collect both pins (each with bracketed numbers)
std::vector<SCH_PIN*> rpins = resistor->GetPins( &sheets.at( 0 ) );
BOOST_REQUIRE_EQUAL( rpins.size(), 2 );
// For determinism, sort by local Y position
std::sort( rpins.begin(), rpins.end(), []( SCH_PIN* a, SCH_PIN* b ) {
return a->GetLocalPosition().y < b->GetLocalPosition().y;
} );
// Top pin is [1-5]
bool validTop = false;
std::vector<wxString> top = rpins[0]->GetStackedPinNumbers( &validTop );
BOOST_CHECK( validTop );
std::vector<wxString> expectedTop = ToVector( { "1", "2", "3", "4", "5" } );
BOOST_CHECK_EQUAL_COLLECTIONS( top.begin(), top.end(), expectedTop.begin(), expectedTop.end() );
// Bottom pin is [6,7,9-11]
bool validBot = false;
std::vector<wxString> bot = rpins[1]->GetStackedPinNumbers( &validBot );
BOOST_CHECK( validBot );
std::vector<wxString> expectedBot = ToVector( { "6", "7", "9", "10", "11" } );
BOOST_CHECK_EQUAL_COLLECTIONS( bot.begin(), bot.end(), expectedBot.begin(), expectedBot.end() );
// Total expanded count across both pins should be 10
size_t total = top.size() + bot.size();
BOOST_CHECK_EQUAL( total, 10 );
}

View File

@ -51,6 +51,7 @@ set( QA_PCBNEW_SRCS
test_shape_corner_radius.cpp
test_pcb_grid_helper.cpp
test_save_load.cpp
test_stacked_pin_netlist.cpp
test_tracks_cleaner.cpp
test_triangulation.cpp
test_multichannel.cpp

View File

@ -0,0 +1,266 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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 <boost/test/unit_test.hpp>
#include <netlist_reader/pcb_netlist.h>
#include <lib_id.h>
#include <board.h>
#include <footprint.h>
#include <pad.h>
#include <pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
#include <project.h>
#include <settings/settings_manager.h>
#include <wx/log.h>
#include <qa_utils/utility_registry.h>
#include <reporter.h>
BOOST_AUTO_TEST_SUITE( StackedPinNetlist )
/**
* Test that COMPONENT::GetNet properly handles stacked pin notation like [8,9,10]
* and finds individual pin numbers within the stacked group.
*/
BOOST_AUTO_TEST_CASE( TestStackedPinNetMatch )
{
// Create a component with a stacked pin notation
LIB_ID fpid( wxT( "TestLib" ), wxT( "TestFootprint" ) );
wxString reference = wxT( "U1" );
wxString value = wxT( "TestIC" );
KIID_PATH path;
std::vector<KIID> kiids;
COMPONENT component( fpid, reference, value, path, kiids );
// Add a net with stacked pin notation [8,9,10]
component.AddNet( wxT( "[8,9,10]" ), wxT( "DATA_BUS" ), wxT( "bidirectional" ), wxT( "bidirectional" ) );
// Test that individual pins within the stack are found
const COMPONENT_NET& net8 = component.GetNet( wxT( "8" ) );
const COMPONENT_NET& net9 = component.GetNet( wxT( "9" ) );
const COMPONENT_NET& net10 = component.GetNet( wxT( "10" ) );
BOOST_CHECK( net8.IsValid() );
BOOST_CHECK( net9.IsValid() );
BOOST_CHECK( net10.IsValid() );
BOOST_CHECK_EQUAL( net8.GetNetName(), wxString( "DATA_BUS" ) );
BOOST_CHECK_EQUAL( net9.GetNetName(), wxString( "DATA_BUS" ) );
BOOST_CHECK_EQUAL( net10.GetNetName(), wxString( "DATA_BUS" ) );
// Test that pins outside the stack are not found
const COMPONENT_NET& net7 = component.GetNet( wxT( "7" ) );
const COMPONENT_NET& net11 = component.GetNet( wxT( "11" ) );
BOOST_CHECK( !net7.IsValid() );
BOOST_CHECK( !net11.IsValid() );
}
/**
* Test stacked pin notation with range syntax [1-4]
*/
BOOST_AUTO_TEST_CASE( TestStackedPinRangeMatch )
{
LIB_ID fpid( wxT( "TestLib" ), wxT( "TestFootprint" ) );
wxString reference = wxT( "U2" );
wxString value = wxT( "TestIC" );
KIID_PATH path;
std::vector<KIID> kiids;
COMPONENT component( fpid, reference, value, path, kiids );
// Add a net with range notation [1-4]
component.AddNet( wxT( "[1-4]" ), wxT( "POWER_BUS" ), wxT( "power_in" ), wxT( "power_in" ) );
// Test that all pins in the range are found
for( int i = 1; i <= 4; i++ )
{
const COMPONENT_NET& net = component.GetNet( wxString::Format( wxT( "%d" ), i ) );
BOOST_CHECK( net.IsValid() );
BOOST_CHECK_EQUAL( net.GetNetName(), wxString( "POWER_BUS" ) );
}
// Test pins outside the range
const COMPONENT_NET& net0 = component.GetNet( wxT( "0" ) );
const COMPONENT_NET& net5 = component.GetNet( wxT( "5" ) );
BOOST_CHECK( !net0.IsValid() );
BOOST_CHECK( !net5.IsValid() );
}
/**
* Test mixed notation [1,3,5-7]
*/
BOOST_AUTO_TEST_CASE( TestStackedPinMixedMatch )
{
LIB_ID fpid( wxT( "TestLib" ), wxT( "TestFootprint" ) );
wxString reference = wxT( "U3" );
wxString value = wxT( "TestIC" );
KIID_PATH path;
std::vector<KIID> kiids;
COMPONENT component( fpid, reference, value, path, kiids );
// Add a net with mixed notation [1,3,5-7]
component.AddNet( wxT( "[1,3,5-7]" ), wxT( "CONTROL_BUS" ), wxT( "output" ), wxT( "output" ) );
// Test individual pins and ranges
std::vector<int> expectedPins = { 1, 3, 5, 6, 7 };
for( int pin : expectedPins )
{
const COMPONENT_NET& net = component.GetNet( wxString::Format( wxT( "%d" ), pin ) );
BOOST_CHECK( net.IsValid() );
BOOST_CHECK_EQUAL( net.GetNetName(), wxString( "CONTROL_BUS" ) );
}
// Test pins that should not be found
std::vector<int> unexpectedPins = { 2, 4, 8 };
for( int pin : unexpectedPins )
{
const COMPONENT_NET& net = component.GetNet( wxString::Format( wxT( "%d" ), pin ) );
BOOST_CHECK( !net.IsValid() );
}
}
/**
* Test that regular (non-stacked) pin names still work
*/
BOOST_AUTO_TEST_CASE( TestRegularPinMatch )
{
LIB_ID fpid( wxT( "TestLib" ), wxT( "TestFootprint" ) );
wxString reference = wxT( "R1" );
wxString value = wxT( "1k" );
KIID_PATH path;
std::vector<KIID> kiids;
COMPONENT component( fpid, reference, value, path, kiids );
// Add regular pins
component.AddNet( wxT( "1" ), wxT( "VCC" ), wxT( "passive" ), wxT( "passive" ) );
component.AddNet( wxT( "2" ), wxT( "GND" ), wxT( "passive" ), wxT( "passive" ) );
const COMPONENT_NET& net1 = component.GetNet( wxT( "1" ) );
const COMPONENT_NET& net2 = component.GetNet( wxT( "2" ) );
BOOST_CHECK( net1.IsValid() );
BOOST_CHECK( net2.IsValid() );
BOOST_CHECK_EQUAL( net1.GetNetName(), wxString( "VCC" ) );
BOOST_CHECK_EQUAL( net2.GetNetName(), wxString( "GND" ) );
}
/**
* This test creates a mock netlist that matches the stacked project structure and
* validates that PCB pad lookups work correctly with stacked pin notation.
*/
BOOST_AUTO_TEST_CASE( TestStackedProjectNetlistUpdate )
{
BOOST_TEST_MESSAGE( "Testing stacked pin project netlist functionality" );
// Create a component that matches the R1 component from the stacked project
LIB_ID fpid( wxT( "Connector" ), wxT( "Tag-Connect_TC2050-IDC-FP_2x05_P1.27mm_Vertical" ) );
wxString reference = wxT( "R1" );
wxString value = wxT( "R" );
KIID_PATH path;
std::vector<KIID> kiids;
COMPONENT component( fpid, reference, value, path, kiids );
// Add nets matching the stacked project
// The schematic has two stacked pin groups: [1-5] and [6,7,9-11]
component.AddNet( wxT( "[1-5]" ), wxT( "Net-(R1-Pad1)" ), wxT( "passive" ), wxT( "passive" ) );
component.AddNet( wxT( "[6,7,9-11]" ), wxT( "Net-(R1-Pad6)" ), wxT( "passive" ), wxT( "passive" ) );
BOOST_TEST_MESSAGE( "Created R1 component with stacked pins [1-5] and [6,7,9-11]" );
// Log all nets for the component
BOOST_TEST_MESSAGE( "R1 component nets:" );
for( unsigned i = 0; i < component.GetNetCount(); i++ )
{
const COMPONENT_NET& net = component.GetNet( i );
BOOST_TEST_MESSAGE( " Pin: " + net.GetPinName() + " -> Net: " + net.GetNetName() );
}
// Test individual pin lookups
// Pins 1-5 should be found (they're in the [1-5] stacked group)
for( int pin = 1; pin <= 5; pin++ )
{
wxString pinStr = wxString::Format( wxT( "%d" ), pin );
const COMPONENT_NET& net = component.GetNet( pinStr );
BOOST_CHECK_MESSAGE( net.IsValid(),
"Pin " + pinStr + " should be found in stacked group [1-5]" );
if( net.IsValid() )
{
BOOST_CHECK_EQUAL( net.GetNetName(), wxString( "Net-(R1-Pad1)" ) );
BOOST_TEST_MESSAGE( "Pin " + pinStr + " found with net: " + net.GetNetName() );
}
else
{
BOOST_TEST_MESSAGE( "Pin " + pinStr + " NOT found (should be in [1-5])" );
}
}
// Pins 6,7,9,10,11 should be found (they're in the [6,7,9-11] stacked group)
std::vector<int> groupTwoPins = { 6, 7, 9, 10, 11 };
for( int pin : groupTwoPins )
{
wxString pinStr = wxString::Format( wxT( "%d" ), pin );
const COMPONENT_NET& net = component.GetNet( pinStr );
BOOST_CHECK_MESSAGE( net.IsValid(),
"Pin " + pinStr + " should be found in stacked group [6,7,9-11]" );
if( net.IsValid() )
{
BOOST_CHECK_EQUAL( net.GetNetName(), wxString( "Net-(R1-Pad6)" ) );
BOOST_TEST_MESSAGE( "Pin " + pinStr + " found with net: " + net.GetNetName() );
}
else
{
BOOST_TEST_MESSAGE( "Pin " + pinStr + " NOT found (should be in [6,7,9-11])" );
}
}
// Pin 8 should NOT be found (it's not in either stacked group)
const COMPONENT_NET& net8 = component.GetNet( wxT( "8" ) );
BOOST_CHECK_MESSAGE( !net8.IsValid(),
"Pin 8 should NOT be found (not in any stacked group)" );
if( net8.IsValid() )
{
BOOST_TEST_MESSAGE( "Pin 8 unexpectedly found with net: " + net8.GetNetName() );
}
else
{
BOOST_TEST_MESSAGE( "Pin 8 correctly NOT found (expected behavior)" );
}
}
BOOST_AUTO_TEST_SUITE_END()