diff --git a/common/string_utils.cpp b/common/string_utils.cpp index 5030176dc1..9546721fbd 100644 --- a/common/string_utils.cpp +++ b/common/string_utils.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #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 ParseAlphaNumericPin( const wxString& pinNum ) + { + wxString prefix; + long numValue = -1; + + size_t numStart = pinNum.length(); + for( int i = static_cast( 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 ExpandStackedPinNotation( const wxString& aPinName, bool* aValid ) +{ + if( aValid ) + *aValid = true; + + std::vector 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; +} diff --git a/cvpcb/cvpcb_mainframe.cpp b/cvpcb/cvpcb_mainframe.cpp index f2cf5fb84c..8d664d71ba 100644 --- a/cvpcb/cvpcb_mainframe.cpp +++ b/cvpcb/cvpcb_mainframe.cpp @@ -62,6 +62,7 @@ #include #include #include +#include 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(); diff --git a/cvpcb/footprints_listbox.cpp b/cvpcb/footprints_listbox.cpp index 6b710fea34..4347c11854 100644 --- a/cvpcb/footprints_listbox.cpp +++ b/cvpcb/footprints_listbox.cpp @@ -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 ); diff --git a/eeschema/dialogs/dialog_field_properties.cpp b/eeschema/dialogs/dialog_field_properties.cpp index c73d2db857..27e28148b3 100644 --- a/eeschema/dialogs/dialog_field_properties.cpp +++ b/eeschema/dialogs/dialog_field_properties.cpp @@ -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" ); diff --git a/eeschema/dialogs/dialog_lib_fields_table.cpp b/eeschema/dialogs/dialog_lib_fields_table.cpp index a8b4dadcc1..a0410a74a3 100644 --- a/eeschema/dialogs/dialog_lib_fields_table.cpp +++ b/eeschema/dialogs/dialog_lib_fields_table.cpp @@ -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() ) diff --git a/eeschema/erc/erc.cpp b/eeschema/erc/erc.cpp index cc4a622b5c..e7e5d6e147 100644 --- a/eeschema/erc/erc.cpp +++ b/eeschema/erc/erc.cpp @@ -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( item ); + + for( SCH_PIN* pin : symbol->GetPins( &sheet ) ) + { + bool valid; + pin->GetStackedPinNumbers( &valid ); + + if( !valid ) + { + std::shared_ptr 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 ) diff --git a/eeschema/erc/erc.h b/eeschema/erc/erc.h index 1ef696fa35..d52ba7cf6d 100644 --- a/eeschema/erc/erc.h +++ b/eeschema/erc/erc.h @@ -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 diff --git a/eeschema/erc/erc_item.cpp b/eeschema/erc/erc_item.cpp index 08dd3053f8..b1badbbea3 100644 --- a/eeschema/erc/erc_item.cpp +++ b/eeschema/erc/erc_item.cpp @@ -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> 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::Create( int aErrorCode ) case ERCE_MISSING_POWER_INPUT_PIN: return std::make_shared( missingPowerInputPin ); case ERCE_MISSING_BIDI_PIN: return std::make_shared( missingBidiPin ); case ERCE_UNCONNECTED_WIRE_ENDPOINT: return std::make_shared( unconnectedWireEndpoint ); + case ERCE_STACKED_PIN_SYNTAX: return std::make_shared( stackedPinName ); case ERCE_UNSPECIFIED: default: wxFAIL_MSG( wxS( "Unknown ERC error code" ) ); diff --git a/eeschema/erc/erc_item.h b/eeschema/erc/erc_item.h index cbaaab9c29..726129cea2 100644 --- a/eeschema/erc/erc_item.h +++ b/eeschema/erc/erc_item.h @@ -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; diff --git a/eeschema/erc/erc_settings.cpp b/eeschema/erc/erc_settings.cpp index 6c6075742d..0e85f4579f 100644 --- a/eeschema/erc/erc_settings.cpp +++ b/eeschema/erc/erc_settings.cpp @@ -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( "rule_severities", [&]() -> nlohmann::json diff --git a/eeschema/erc/erc_settings.h b/eeschema/erc/erc_settings.h index fa7af8aa19..81e0d4c7f6 100644 --- a/eeschema/erc/erc_settings.h +++ b/eeschema/erc/erc_settings.h @@ -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 diff --git a/eeschema/fields_grid_table.cpp b/eeschema/fields_grid_table.cpp index 569820b76d..8ab9ef86df 100644 --- a/eeschema/fields_grid_table.cpp +++ b/eeschema/fields_grid_table.cpp @@ -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 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 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" ); diff --git a/eeschema/lib_symbol.cpp b/eeschema/lib_symbol.cpp index 299fef5fee..284a5b9b8d 100644 --- a/eeschema/lib_symbol.cpp +++ b/eeschema/lib_symbol.cpp @@ -782,7 +782,7 @@ void LIB_SYMBOL::AddDrawItem( SCH_ITEM* aItem, bool aSort ) } -std::vector LIB_SYMBOL::GetPins( int aUnit, int aBodyStyle ) const +std::vector LIB_SYMBOL::GetGraphicalPins( int aUnit, int aBodyStyle ) const { std::vector pins; @@ -807,28 +807,75 @@ std::vector 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( static_cast( &item ) ) ); + SCH_PIN* pin = const_cast( static_cast( &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 LIB_SYMBOL::GetPins() const +std::vector LIB_SYMBOL::GetLogicalPins( int aUnit, int aBodyStyle ) const { - return GetPins( 0, 0 ); + std::vector out; + + for( SCH_PIN* pin : GetGraphicalPins( aUnit, aBodyStyle ) ) + { + bool valid = false; + std::vector 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 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 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, diff --git a/eeschema/lib_symbol.h b/eeschema/lib_symbol.h index dae2c565a4..73a7b0908d 100644 --- a/eeschema/lib_symbol.h +++ b/eeschema/lib_symbol.h @@ -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 GetPins( int aUnit, int aBodyStyle ) const; + std::vector 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 GetLogicalPins( int aUnit, int aBodyStyle ) const; + + // Deprecated: use GetGraphicalPins(). This override remains to satisfy SYMBOL's pure virtual. std::vector GetPins() const override; /** diff --git a/eeschema/multiline_pin_text.cpp b/eeschema/multiline_pin_text.cpp new file mode 100644 index 0000000000..62ebec2081 --- /dev/null +++ b/eeschema/multiline_pin_text.cpp @@ -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 + +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; +} diff --git a/eeschema/multiline_pin_text.h b/eeschema/multiline_pin_text.h new file mode 100644 index 0000000000..f2762f40ad --- /dev/null +++ b/eeschema/multiline_pin_text.h @@ -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 +#include +#include +#include + +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 ); diff --git a/eeschema/netlist_exporters/netlist_exporter_base.cpp b/eeschema/netlist_exporters/netlist_exporter_base.cpp index 7427b3dcb7..da26388503 100644 --- a/eeschema/netlist_exporters/netlist_exporter_base.cpp +++ b/eeschema/netlist_exporters/netlist_exporter_base.cpp @@ -176,7 +176,21 @@ std::vector NETLIST_EXPORTER_BASE::CreatePinList( SCH_SYMBOL* aSymbol, continue; } - pins.emplace_back( pin->GetShownNumber(), netName ); + bool valid; + std::vector 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 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 ); + } } } } diff --git a/eeschema/netlist_exporters/netlist_exporter_base.h b/eeschema/netlist_exporters/netlist_exporter_base.h index 81cd95b0e3..fe2bea1c08 100644 --- a/eeschema/netlist_exporters/netlist_exporter_base.h +++ b/eeschema/netlist_exporters/netlist_exporter_base.h @@ -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; }; diff --git a/eeschema/netlist_exporters/netlist_exporter_xml.cpp b/eeschema/netlist_exporters/netlist_exporter_xml.cpp index 6fa15c63a0..32a6bcf87f 100644 --- a/eeschema/netlist_exporters/netlist_exporter_xml.cpp +++ b/eeschema/netlist_exporters/netlist_exporter_xml.cpp @@ -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 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 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; iAddChild( 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 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 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 ); + } } } diff --git a/eeschema/pin_layout_cache.cpp b/eeschema/pin_layout_cache.cpp index 253f1eb3bf..026f44f942 100644 --- a/eeschema/pin_layout_cache.cpp +++ b/eeschema/pin_layout_cache.cpp @@ -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 #include #include #include #include #include - +#include #include +// 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::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" ); + 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 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::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::GetPinNumberInfo( int aShadowWidth ) -{ - recomputeCaches(); - - wxString number = m_pin.GetShownNumber(); - if( number.IsEmpty() || !m_pin.GetParentSymbol()->GetShowPinNumbers() ) - return std::nullopt; - - std::optional 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 diff --git a/eeschema/pin_layout_cache.h b/eeschema/pin_layout_cache.h index 142fcd24b0..8d6bbf1c61 100644 --- a/eeschema/pin_layout_cache.h +++ b/eeschema/pin_layout_cache.h @@ -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 diff --git a/eeschema/sch_file_versions.h b/eeschema/sch_file_versions.h index fbf9cf0200..fb967d9aeb 100644 --- a/eeschema/sch_file_versions.h +++ b/eeschema/sch_file_versions.h @@ -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 diff --git a/eeschema/sch_io/cadstar/cadstar_sch_archive_loader.cpp b/eeschema/sch_io/cadstar/cadstar_sch_archive_loader.cpp index 340f71a374..4df1882be6 100644 --- a/eeschema/sch_io/cadstar/cadstar_sch_archive_loader.cpp +++ b/eeschema/sch_io/cadstar/cadstar_sch_archive_loader.cpp @@ -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 ) diff --git a/eeschema/sch_painter.cpp b/eeschema/sch_painter.cpp index 38893059c7..abcfc8f73a 100644 --- a/eeschema/sch_painter.cpp +++ b/eeschema/sch_painter.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #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( 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( 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 originalPins = originalSymbol->GetPins( unit, bodyStyle ); + std::vector originalPins = originalSymbol->GetGraphicalPins( unit, bodyStyle ); // Copy the source so we can re-orient and translate it. LIB_SYMBOL tempSymbol( *originalSymbol ); - std::vector tempPins = tempSymbol.GetPins( unit, bodyStyle ); + std::vector tempPins = tempSymbol.GetGraphicalPins( unit, bodyStyle ); tempSymbol.SetFlags( aSymbol->GetFlags() ); diff --git a/eeschema/sch_pin.cpp b/eeschema/sch_pin.cpp index 7703fb63a1..cab7add2fc 100644 --- a/eeschema/sch_pin.cpp +++ b/eeschema/sch_pin.cpp @@ -39,6 +39,49 @@ #include #include +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 SCH_PIN::GetStackedPinNumbers( bool* aValid ) const +{ + wxString shown = GetShownNumber(); + wxLogTrace( "KICAD_STACKED_PINS", + wxString::Format( "GetStackedPinNumbers: shown='%s'", shown ) ); + + std::vector 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 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 ) diff --git a/eeschema/sch_pin.h b/eeschema/sch_pin.h index 454a4d54ce..9e472c2ea4 100644 --- a/eeschema/sch_pin.h +++ b/eeschema/sch_pin.h @@ -24,6 +24,7 @@ #pragma once #include +#include #include #include @@ -122,6 +123,19 @@ public: const wxString& GetNumber() const { return m_number; } wxString GetShownNumber() const; + std::vector 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 GetSmallestLogicalNumber() const; + + /** + * Return the pin number to be used for deterministic operations such as + * auto‑generated 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; diff --git a/eeschema/sch_symbol.cpp b/eeschema/sch_symbol.cpp index 785ee126fe..86d4eff8a9 100644 --- a/eeschema/sch_symbol.cpp +++ b/eeschema/sch_symbol.cpp @@ -1128,7 +1128,7 @@ const SCH_PIN* SCH_SYMBOL::GetPin( const VECTOR2I& aPos ) const std::vector 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(); } @@ -2530,11 +2530,11 @@ void SCH_SYMBOL::Plot( PLOTTER* aPlotter, bool aBackground, const SCH_PLOT_OPTS& if( m_part ) { - std::vector libPins = m_part->GetPins( GetUnit(), GetBodyStyle() ); + std::vector libPins = m_part->GetGraphicalPins( GetUnit(), GetBodyStyle() ); // Copy the source so we can re-orient and translate it. LIB_SYMBOL tempSymbol( *m_part ); - std::vector tempPins = tempSymbol.GetPins( GetUnit(), GetBodyStyle() ); + std::vector 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 libPins = m_part->GetPins( GetUnit(), GetBodyStyle() ); + std::vector libPins = m_part->GetGraphicalPins( GetUnit(), GetBodyStyle() ); // Copy the source to stay const LIB_SYMBOL tempSymbol( *m_part ); - std::vector tempPins = tempSymbol.GetPins( GetUnit(), GetBodyStyle() ); + std::vector tempPins = tempSymbol.GetGraphicalPins( GetUnit(), GetBodyStyle() ); SCH_PLOT_OPTS plotOpts; // Copy the pin info from the symbol to the temp pins diff --git a/eeschema/tools/sch_actions.cpp b/eeschema/tools/sch_actions.cpp index 41b812f8fb..682e42b0c9 100644 --- a/eeschema/tools/sch_actions.cpp +++ b/eeschema/tools/sch_actions.cpp @@ -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 ) diff --git a/eeschema/tools/sch_actions.h b/eeschema/tools/sch_actions.h index d9e2b784eb..b04f79eea2 100644 --- a/eeschema/tools/sch_actions.h +++ b/eeschema/tools/sch_actions.h @@ -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; diff --git a/eeschema/tools/sch_point_editor.cpp b/eeschema/tools/sch_point_editor.cpp index bae5f898cf..67f76d33ae 100644 --- a/eeschema/tools/sch_point_editor.cpp +++ b/eeschema/tools/sch_point_editor.cpp @@ -658,7 +658,7 @@ private: { std::vector 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(); diff --git a/eeschema/tools/symbol_editor_edit_tool.cpp b/eeschema/tools/symbol_editor_edit_tool.cpp index b3590e8bc1..a2eeddb3a4 100644 --- a/eeschema/tools/symbol_editor_edit_tool.cpp +++ b/eeschema/tools/symbol_editor_edit_tool.cpp @@ -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 pins; + for( EDA_ITEM* item : sel ) + { + if( item->Type() != SCH_PIN_T ) + return false; + pins.push_back( static_cast( 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( 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( sel.Front() ); + bool isValid; + std::vector 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(); + wxCHECK( selTool, -1 ); + + SCH_SELECTION& selection = selTool->GetSelection(); + + // Collect pins to convert - accept pins with any number format + std::vector 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( 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( 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> prefixGroups; + std::vector 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 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(); + 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( selection.Front() ); + + // Check if the pin has stacked notation + bool isValid; + std::vector 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 } diff --git a/eeschema/tools/symbol_editor_edit_tool.h b/eeschema/tools/symbol_editor_edit_tool.h index 7e17838cbd..0c518009db 100644 --- a/eeschema/tools/symbol_editor_edit_tool.h +++ b/eeschema/tools/symbol_editor_edit_tool.h @@ -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 ); diff --git a/eeschema/widgets/panel_symbol_chooser.cpp b/eeschema/widgets/panel_symbol_chooser.cpp index 86fff5c201..8a4b530b95 100644 --- a/eeschema/widgets/panel_symbol_chooser.cpp +++ b/eeschema/widgets/panel_symbol_chooser.cpp @@ -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( "" ); diff --git a/include/string_utils.h b/include/string_utils.h index ba156946fa..07c4facd0e 100644 --- a/include/string_utils.h +++ b/include/string_utils.h @@ -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 ExpandStackedPinNotation( const wxString& aPinName, + bool* aValid = nullptr ); + #endif // STRING_UTILS_H diff --git a/pcbnew/footprint_chooser_frame.cpp b/pcbnew/footprint_chooser_frame.cpp index 36108ffdf9..5c14dbff1f 100644 --- a/pcbnew/footprint_chooser_frame.cpp +++ b/pcbnew/footprint_chooser_frame.cpp @@ -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 ) diff --git a/pcbnew/netlist_reader/board_netlist_updater.cpp b/pcbnew/netlist_reader/board_netlist_updater.cpp index 4a0e9e4ba3..8626c41c4b 100644 --- a/pcbnew/netlist_reader/board_netlist_updater.cpp +++ b/pcbnew/netlist_reader/board_netlist_updater.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #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 ) { diff --git a/pcbnew/netlist_reader/kicad_netlist_reader.cpp b/pcbnew/netlist_reader/kicad_netlist_reader.cpp index 0734a30a15..258fa736c5 100644 --- a/pcbnew/netlist_reader/kicad_netlist_reader.cpp +++ b/pcbnew/netlist_reader/kicad_netlist_reader.cpp @@ -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] ); } } diff --git a/pcbnew/netlist_reader/pcb_netlist.cpp b/pcbnew/netlist_reader/pcb_netlist.cpp index 3e6542b04e..7e96f69751 100644 --- a/pcbnew/netlist_reader/pcb_netlist.cpp +++ b/pcbnew/netlist_reader/pcb_netlist.cpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include 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 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; } diff --git a/qa/data/eeschema/stacked_pin_nomenclature.kicad_sch b/qa/data/eeschema/stacked_pin_nomenclature.kicad_sch new file mode 100644 index 0000000000..cace3fc3e9 --- /dev/null +++ b/qa/data/eeschema/stacked_pin_nomenclature.kicad_sch @@ -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")) ) +) diff --git a/qa/data/pcbnew/stacked/stacked.kicad_pcb b/qa/data/pcbnew/stacked/stacked.kicad_pcb new file mode 100644 index 0000000000..98d31c5fc5 --- /dev/null +++ b/qa/data/pcbnew/stacked/stacked.kicad_pcb @@ -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) +) diff --git a/qa/data/pcbnew/stacked/stacked.kicad_pro b/qa/data/pcbnew/stacked/stacked.kicad_pro new file mode 100644 index 0000000000..b7c969dac0 --- /dev/null +++ b/qa/data/pcbnew/stacked/stacked.kicad_pro @@ -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 + } + } +} diff --git a/qa/data/pcbnew/stacked/stacked.kicad_sch b/qa/data/pcbnew/stacked/stacked.kicad_sch new file mode 100644 index 0000000000..a364e37418 --- /dev/null +++ b/qa/data/pcbnew/stacked/stacked.kicad_sch @@ -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) +) diff --git a/qa/tests/eeschema/CMakeLists.txt b/qa/tests/eeschema/CMakeLists.txt index f5550afad4..910d1db3e3 100644 --- a/qa/tests/eeschema/CMakeLists.txt +++ b/qa/tests/eeschema/CMakeLists.txt @@ -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 diff --git a/qa/tests/eeschema/test_netlist_exporter_xml_stacked.cpp b/qa/tests/eeschema/test_netlist_exporter_xml_stacked.cpp new file mode 100644 index 0000000000..6f0fd89278 --- /dev/null +++ b/qa/tests/eeschema/test_netlist_exporter_xml_stacked.cpp @@ -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 +#include + +#include +#include + +#include +#include +#include + +#include +#include + +struct XML_STACKED_PIN_FIXTURE +{ + XML_STACKED_PIN_FIXTURE() : m_settingsManager( true /* headless */ ) {} + + SETTINGS_MANAGER m_settingsManager; + std::unique_ptr m_schematic; +}; + +static std::set as_set( const std::initializer_list& init ) +{ + std::set 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 find_children( wxXmlNode* parent, const wxString& name ) +{ + std::vector 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 exporter = + std::make_unique( 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 setA; + std::set 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* 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 expectedTop = as_set( { "1", "2", "3", "4", "5" } ); + const std::set 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() ); +} diff --git a/qa/tests/eeschema/test_pin_stacked_layout.cpp b/qa/tests/eeschema/test_pin_stacked_layout.cpp new file mode 100644 index 0000000000..a2b71364d7 --- /dev/null +++ b/qa/tests/eeschema/test_pin_stacked_layout.cpp @@ -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 + +#include +#include +#include +#include +#include + +#include +#include + +BOOST_AUTO_TEST_SUITE( PinStackedLayout ) + +/** + * Create a test symbol with stacked pin numbers for rotation testing + */ +static std::unique_ptr createTestResistorSymbol() +{ + auto symbol = std::make_unique( wxT( "TestResistor" ) ); + + // Create first pin with stacked numbers [1-5] + auto pin1 = std::make_unique( 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( 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 pins; + for( auto& item : symbol->GetDrawItems() ) + { + if( item.Type() == SCH_PIN_T ) + pins.push_back( static_cast( &item ) ); + } + + BOOST_REQUIRE_EQUAL( pins.size(), 2 ); + + // Test rotations: 0°, 90°, 180°, 270° + std::vector 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 rotationNames = { wxT("0°"), 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 pins; + for( auto& item : symbol->GetDrawItems() ) + { + if( item.Type() == SCH_PIN_T ) + pins.push_back( static_cast( &item ) ); + } + + BOOST_REQUIRE_EQUAL( pins.size(), 2 ); + + // Test rotations + std::vector 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 rotationNames = { wxT("0°"), 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 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 pins; + for( auto& item : symbol->GetDrawItems() ) + { + if( item.Type() == SCH_PIN_T ) + pins.push_back( static_cast( &item ) ); + } + + BOOST_REQUIRE_EQUAL( pins.size(), 2 ); + + // Test rotations + std::vector 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 rotationNames = { wxT("0°"), 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 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() diff --git a/qa/tests/eeschema/test_stacked_pin_conversion.cpp b/qa/tests/eeschema/test_stacked_pin_conversion.cpp new file mode 100644 index 0000000000..79c953764c --- /dev/null +++ b/qa/tests/eeschema/test_stacked_pin_conversion.cpp @@ -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 . + */ + +#include +#include +#include +#include + +struct STACKED_PIN_CONVERSION_FIXTURE +{ + STACKED_PIN_CONVERSION_FIXTURE() + { + m_settingsManager = std::make_unique( true ); + m_symbol = std::make_unique( "TestSymbol" ); + } + + std::unique_ptr m_settingsManager; + std::unique_ptr 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 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 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 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 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 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 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 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 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 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 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 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> prefixGroups; + std::vector 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() diff --git a/qa/tests/eeschema/test_stacked_pin_nomenclature.cpp b/qa/tests/eeschema/test_stacked_pin_nomenclature.cpp new file mode 100644 index 0000000000..ccd2b39858 --- /dev/null +++ b/qa/tests/eeschema/test_stacked_pin_nomenclature.cpp @@ -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 +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +struct STACKED_PIN_FIXTURE +{ + STACKED_PIN_FIXTURE() : m_settingsManager( true /* headless */ ) {} + + SETTINGS_MANAGER m_settingsManager; + std::unique_ptr m_schematic; +}; + +static std::vector ToVector( const std::initializer_list& init ) +{ + std::vector 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( 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 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 top = rpins[0]->GetStackedPinNumbers( &validTop ); + BOOST_CHECK( validTop ); + std::vector 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 bot = rpins[1]->GetStackedPinNumbers( &validBot ); + BOOST_CHECK( validBot ); + std::vector 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 ); +} diff --git a/qa/tests/pcbnew/CMakeLists.txt b/qa/tests/pcbnew/CMakeLists.txt index 26a8fbd6d9..9d0b41f527 100644 --- a/qa/tests/pcbnew/CMakeLists.txt +++ b/qa/tests/pcbnew/CMakeLists.txt @@ -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 diff --git a/qa/tests/pcbnew/test_stacked_pin_netlist.cpp b/qa/tests/pcbnew/test_stacked_pin_netlist.cpp new file mode 100644 index 0000000000..0e024a01ed --- /dev/null +++ b/qa/tests/pcbnew/test_stacked_pin_netlist.cpp @@ -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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 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 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 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 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 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 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 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()