diff --git a/pcbnew/netlist_reader/board_netlist_updater.cpp b/pcbnew/netlist_reader/board_netlist_updater.cpp index d5cfbecb58..b04e429e97 100644 --- a/pcbnew/netlist_reader/board_netlist_updater.cpp +++ b/pcbnew/netlist_reader/board_netlist_updater.cpp @@ -1924,6 +1924,18 @@ bool BOARD_NETLIST_UPDATER::UpdateNetlist( NETLIST& aNetlist ) const LIB_ID& baseFpid = component->GetFPID(); const bool hasBaseFpid = !baseFpid.empty(); + + if( baseFpid.IsLegacy() ) + { + msg.Printf( _( "Warning: %s footprint '%s' is missing a library name. " + "Use the full 'Library:Footprint' format to avoid repeated update " + "notifications." ), + component->GetReference(), + EscapeHTML( baseFpid.Format().wx_str() ) ); + m_reporter->Report( msg, RPT_SEVERITY_WARNING ); + ++m_warningCount; + } + std::vector matchingFootprints; for( FOOTPRINT* footprint : m_board->Footprints() ) @@ -2001,13 +2013,35 @@ bool BOARD_NETLIST_UPDATER::UpdateNetlist( NETLIST& aNetlist ) addExpectedFpid( parsedId ); } + // When the schematic-side FPID has no library nickname (legacy format like + // "DGG56" instead of "Package_SO:DGG56"), matching should compare only the + // footprint item name. Otherwise the board footprint (which always has a library + // nickname) will never match, causing perpetual "change footprint" notifications. + auto fpidMatches = + [&]( const LIB_ID& aBoardFpid, const LIB_ID& aExpectedFpid ) -> bool + { + if( aExpectedFpid.IsLegacy() ) + return aBoardFpid.GetLibItemName() == aExpectedFpid.GetLibItemName(); + + return aBoardFpid == aExpectedFpid; + }; + auto isExpectedFpid = [&]( const LIB_ID& aFpid ) -> bool { if( aFpid.empty() ) return false; - return expectedFpidKeys.count( aFpid.Format() ) > 0; + if( expectedFpidKeys.count( aFpid.Format() ) > 0 ) + return true; + + for( const LIB_ID& expected : expectedFpids ) + { + if( fpidMatches( aFpid, expected ) ) + return true; + } + + return false; }; auto takeMatchingFootprint = @@ -2018,7 +2052,7 @@ bool BOARD_NETLIST_UPDATER::UpdateNetlist( NETLIST& aNetlist ) if( usedFootprints.count( footprint ) ) continue; - if( footprint->GetFPID() == aFpid ) + if( fpidMatches( footprint->GetFPID(), aFpid ) ) return footprint; } diff --git a/pcbnew/netlist_reader/netlist.cpp b/pcbnew/netlist_reader/netlist.cpp index c1554d8c90..fa1fb2afb4 100644 --- a/pcbnew/netlist_reader/netlist.cpp +++ b/pcbnew/netlist_reader/netlist.cpp @@ -206,7 +206,22 @@ void PCB_EDIT_FRAME::LoadFootprints( NETLIST& aNetlist, REPORTER& aReporter ) else fpOnBoard = m_pcb->FindFootprintByReference( component->GetReference() ); - bool footprintMisMatch = fpOnBoard && fpOnBoard->GetFPID() != component->GetFPID(); + // When the schematic-side FPID has no library nickname (legacy format), match + // only by item name so we don't flag a mismatch against a fully qualified board FPID. + bool footprintMisMatch = false; + + if( fpOnBoard ) + { + if( component->GetFPID().IsLegacy() ) + { + footprintMisMatch = + fpOnBoard->GetFPID().GetLibItemName() != component->GetFPID().GetLibItemName(); + } + else + { + footprintMisMatch = fpOnBoard->GetFPID() != component->GetFPID(); + } + } if( footprintMisMatch && !aNetlist.GetReplaceFootprints() ) { diff --git a/qa/tests/common/CMakeLists.txt b/qa/tests/common/CMakeLists.txt index 1215b223a8..16dfe89c1f 100644 --- a/qa/tests/common/CMakeLists.txt +++ b/qa/tests/common/CMakeLists.txt @@ -56,6 +56,7 @@ set( QA_COMMON_SRCS test_pdf_unicode_plot.cpp test_kiid.cpp test_layer_ids.cpp + test_lib_id.cpp test_layer_range.cpp test_lset.cpp test_markup_parser.cpp diff --git a/qa/tests/common/test_lib_id.cpp b/qa/tests/common/test_lib_id.cpp new file mode 100644 index 0000000000..613bf22923 --- /dev/null +++ b/qa/tests/common/test_lib_id.cpp @@ -0,0 +1,114 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include +#include + +#include + + +BOOST_AUTO_TEST_SUITE( LibId ) + + +BOOST_AUTO_TEST_CASE( ParseFullyQualified ) +{ + LIB_ID id; + BOOST_CHECK( id.Parse( "Package_SO:DGG56" ) == -1 ); + BOOST_CHECK_EQUAL( id.GetLibNickname(), "Package_SO" ); + BOOST_CHECK_EQUAL( id.GetLibItemName(), "DGG56" ); + BOOST_CHECK( id.IsValid() ); + BOOST_CHECK( !id.IsLegacy() ); + BOOST_CHECK( !id.empty() ); +} + + +BOOST_AUTO_TEST_CASE( ParseLegacy ) +{ + LIB_ID id; + BOOST_CHECK( id.Parse( "DGG56" ) == -1 ); + BOOST_CHECK( id.GetLibNickname().empty() ); + BOOST_CHECK_EQUAL( id.GetLibItemName(), "DGG56" ); + BOOST_CHECK( !id.IsValid() ); + BOOST_CHECK( id.IsLegacy() ); + BOOST_CHECK( !id.empty() ); +} + + +BOOST_AUTO_TEST_CASE( EqualityFullyQualified ) +{ + LIB_ID a( wxT( "Package_SO" ), wxT( "DGG56" ) ); + LIB_ID b( wxT( "Package_SO" ), wxT( "DGG56" ) ); + LIB_ID c( wxT( "OtherLib" ), wxT( "DGG56" ) ); + + BOOST_CHECK( a == b ); + BOOST_CHECK( a != c ); +} + + +BOOST_AUTO_TEST_CASE( EqualityLegacyVsFullyQualified ) +{ + LIB_ID legacy; + legacy.Parse( "DGG56" ); + + LIB_ID qualified( wxT( "Package_SO" ), wxT( "DGG56" ) ); + + // Standard equality requires both library and item name to match + BOOST_CHECK( legacy != qualified ); + + // Legacy matching (item name only) should match when we explicitly compare just item names + BOOST_CHECK( legacy.IsLegacy() ); + BOOST_CHECK_EQUAL( legacy.GetLibItemName(), qualified.GetLibItemName() ); +} + + +BOOST_AUTO_TEST_CASE( FormatRoundTrip ) +{ + LIB_ID qualified( wxT( "Package_SO" ), wxT( "DGG56" ) ); + BOOST_CHECK_EQUAL( qualified.Format().wx_str(), wxT( "Package_SO:DGG56" ) ); + + LIB_ID legacy; + legacy.Parse( "DGG56" ); + BOOST_CHECK_EQUAL( legacy.Format().wx_str(), wxT( "DGG56" ) ); + + // Re-parse the formatted strings + LIB_ID reparsed; + reparsed.Parse( qualified.Format() ); + BOOST_CHECK( reparsed == qualified ); + + LIB_ID reparsedLegacy; + reparsedLegacy.Parse( legacy.Format() ); + BOOST_CHECK( reparsedLegacy.IsLegacy() ); + BOOST_CHECK_EQUAL( reparsedLegacy.GetLibItemName(), legacy.GetLibItemName() ); +} + + +BOOST_AUTO_TEST_CASE( EmptyId ) +{ + LIB_ID empty; + BOOST_CHECK( empty.empty() ); + BOOST_CHECK( !empty.IsValid() ); + BOOST_CHECK( !empty.IsLegacy() ); +} + + +BOOST_AUTO_TEST_SUITE_END()