From 5d50dc7ced1e1a648c2af27672bf0b7f66bedcd2 Mon Sep 17 00:00:00 2001 From: Seth Hillbrand Date: Thu, 18 Sep 2025 17:12:49 -0700 Subject: [PATCH] Preserve as much footprint data as possible When updating the footprint from library, try to match as many things to their new counterparts as possible to minimize VCS churn Fixes https://gitlab.com/kicad/code/kicad/-/issues/21308 --- pcbnew/pcb_edit_frame.cpp | 361 ++++++++++++++++++++++++++++++-------- 1 file changed, 289 insertions(+), 72 deletions(-) diff --git a/pcbnew/pcb_edit_frame.cpp b/pcbnew/pcb_edit_frame.cpp index 3a1e4e84a9..289330101a 100644 --- a/pcbnew/pcb_edit_frame.cpp +++ b/pcbnew/pcb_edit_frame.cpp @@ -35,6 +35,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -47,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -2362,51 +2368,75 @@ static void processTextItem( const PCB_TEXT& aSrc, PCB_TEXT& aDest, } -static PCB_TEXT* getMatchingTextItem( PCB_TEXT* aRefItem, FOOTPRINT* aFootprint ) +template +static std::vector> matchItemsBySimilarity( const std::vector& aExisting, + const std::vector& aNew ) { - std::vector candidates; - - for( BOARD_ITEM* item : aFootprint->GraphicalItems() ) + struct MATCH_CANDIDATE { - PCB_TEXT* candidate = dynamic_cast( item ); + T* existing; + T* updated; + double score; + }; - if( candidate && candidate->GetText() == aRefItem->GetText() ) - candidates.push_back( candidate ); + std::vector candidates; + + for( T* existing : aExisting ) + { + for( T* updated : aNew ) + { + if( existing->Type() != updated->Type() ) + continue; + + double similarity = existing->Similarity( *updated ); + + if( similarity <= 0.0 ) + continue; + + double score = similarity; + + if constexpr( std::is_same_v ) + { + if( existing->GetNumber() == updated->GetNumber() ) + score += 2.0; + } + + candidates.push_back( { existing, updated, score } ); + } } - if( candidates.size() == 0 ) - return nullptr; + std::sort( candidates.begin(), candidates.end(), + []( const MATCH_CANDIDATE& a, const MATCH_CANDIDATE& b ) + { + if( a.score != b.score ) + return a.score > b.score; - if( candidates.size() == 1 ) - return candidates[0]; + if( a.existing != b.existing ) + return a.existing < b.existing; - // Try refining the match by layer - std::vector candidatesOnSameLayer; + return a.updated < b.updated; + } ); - for( PCB_TEXT* candidate : candidates ) + std::vector> matches; + matches.reserve( candidates.size() ); + + std::unordered_set matchedExisting; + std::unordered_set matchedNew; + + for( const MATCH_CANDIDATE& candidate : candidates ) { - if( candidate->GetLayer() == aRefItem->GetLayer() ) - candidatesOnSameLayer.push_back( candidate ); + if( matchedExisting.find( candidate.existing ) != matchedExisting.end() ) + continue; + + if( matchedNew.find( candidate.updated ) != matchedNew.end() ) + continue; + + matchedExisting.insert( candidate.existing ); + matchedNew.insert( candidate.updated ); + matches.emplace_back( candidate.existing, candidate.updated ); } - if( candidatesOnSameLayer.size() == 1 ) - return candidatesOnSameLayer[0]; - - // Last ditch effort: refine by position - std::vector candidatesAtSamePos; - - for( PCB_TEXT* candidate : candidatesOnSameLayer.size() ? candidatesOnSameLayer : candidates ) - { - if( candidate->GetFPRelativePosition() == aRefItem->GetFPRelativePosition() ) - candidatesAtSamePos.push_back( candidate ); - } - - if( candidatesAtSamePos.size() > 0 ) - return candidatesAtSamePos[0]; - else if( candidatesOnSameLayer.size() > 0 ) - return candidatesOnSameLayer[0]; - else - return candidates[0]; + return matches; } @@ -2451,49 +2481,227 @@ void PCB_EDIT_FRAME::ExchangeFootprint( FOOTPRINT* aExisting, FOOTPRINT* aNew, aNew->SetLocked( aExisting->IsLocked() ); - // Now transfer the net info from "old" pads to the new footprint - for( PAD* newPad : aNew->Pads() ) + const_cast( aNew->m_Uuid ) = aExisting->m_Uuid; + const_cast( aNew->Reference().m_Uuid ) = aExisting->Reference().m_Uuid; + const_cast( aNew->Value().m_Uuid ) = aExisting->Value().m_Uuid; + + std::vector oldPads; + oldPads.reserve( aExisting->Pads().size() ); + + for( PAD* pad : aExisting->Pads() ) + oldPads.push_back( pad ); + + std::vector newPads; + newPads.reserve( aNew->Pads().size() ); + + for( PAD* pad : aNew->Pads() ) + newPads.push_back( pad ); + + auto padMatches = matchItemsBySimilarity( oldPads, newPads ); + std::unordered_set matchedNewPads; + + for( const auto& match : padMatches ) { - PAD* oldPad = nullptr; + PAD* oldPad = match.first; + PAD* newPad = match.second; - // Pads with no numbers can't be matched. (Then again, they're never connected to a - // net either, so it's just the UUID retention that we can't perform.) - if( newPad->GetNumber().IsEmpty() ) - { - newPad->SetNetCode( NETINFO_LIST::UNCONNECTED ); - continue; - } - - // Search for a similar pad to reuse UUID and net info - PAD* last_pad = nullptr; - - while( true ) - { - oldPad = aExisting->FindPadByNumber( newPad->GetNumber(), last_pad ); - - if( !oldPad ) - break; - - if( newPad->IsOnCopperLayer() == oldPad->IsOnCopperLayer() ) // a candidate is found - break; - - last_pad = oldPad; - } - - if( oldPad ) - { - const_cast( newPad->m_Uuid ) = oldPad->m_Uuid; - newPad->SetLocalRatsnestVisible( oldPad->GetLocalRatsnestVisible() ); - newPad->SetPinFunction( oldPad->GetPinFunction() ); - newPad->SetPinType( oldPad->GetPinType() ); - } + matchedNewPads.insert( newPad ); + const_cast( newPad->m_Uuid ) = oldPad->m_Uuid; + newPad->SetLocalRatsnestVisible( oldPad->GetLocalRatsnestVisible() ); + newPad->SetPinFunction( oldPad->GetPinFunction() ); + newPad->SetPinType( oldPad->GetPinType() ); if( newPad->IsOnCopperLayer() ) - newPad->SetNetCode( oldPad ? oldPad->GetNetCode() : NETINFO_LIST::UNCONNECTED ); + newPad->SetNetCode( oldPad->GetNetCode() ); else newPad->SetNetCode( NETINFO_LIST::UNCONNECTED ); } + for( PAD* newPad : aNew->Pads() ) + { + if( matchedNewPads.find( newPad ) != matchedNewPads.end() ) + continue; + + const_cast( newPad->m_Uuid ) = KIID(); + newPad->SetNetCode( NETINFO_LIST::UNCONNECTED ); + } + + std::vector oldDrawings; + oldDrawings.reserve( aExisting->GraphicalItems().size() ); + + for( BOARD_ITEM* item : aExisting->GraphicalItems() ) + oldDrawings.push_back( item ); + + std::vector newDrawings; + newDrawings.reserve( aNew->GraphicalItems().size() ); + + for( BOARD_ITEM* item : aNew->GraphicalItems() ) + newDrawings.push_back( item ); + + auto drawingMatches = matchItemsBySimilarity( oldDrawings, newDrawings ); + std::unordered_map oldToNewDrawings; + std::unordered_set matchedNewDrawings; + + for( const auto& match : drawingMatches ) + { + BOARD_ITEM* oldItem = match.first; + BOARD_ITEM* newItem = match.second; + + oldToNewDrawings[ oldItem ] = newItem; + matchedNewDrawings.insert( newItem ); + const_cast( newItem->m_Uuid ) = oldItem->m_Uuid; + } + + for( BOARD_ITEM* newItem : newDrawings ) + { + if( matchedNewDrawings.find( newItem ) == matchedNewDrawings.end() ) + const_cast( newItem->m_Uuid ) = KIID(); + } + + std::vector oldZones; + oldZones.reserve( aExisting->Zones().size() ); + + for( ZONE* zone : aExisting->Zones() ) + oldZones.push_back( zone ); + + std::vector newZones; + newZones.reserve( aNew->Zones().size() ); + + for( ZONE* zone : aNew->Zones() ) + newZones.push_back( zone ); + + auto zoneMatches = matchItemsBySimilarity( oldZones, newZones ); + std::unordered_set matchedNewZones; + + for( const auto& match : zoneMatches ) + { + ZONE* oldZone = match.first; + ZONE* newZone = match.second; + + matchedNewZones.insert( newZone ); + const_cast( newZone->m_Uuid ) = oldZone->m_Uuid; + } + + for( ZONE* newZone : newZones ) + { + if( matchedNewZones.find( newZone ) == matchedNewZones.end() ) + const_cast( newZone->m_Uuid ) = KIID(); + } + + std::vector oldPoints; + oldPoints.reserve( aExisting->Points().size() ); + + for( PCB_POINT* point : aExisting->Points() ) + oldPoints.push_back( point ); + + std::vector newPoints; + newPoints.reserve( aNew->Points().size() ); + + for( PCB_POINT* point : aNew->Points() ) + newPoints.push_back( point ); + + auto pointMatches = matchItemsBySimilarity( oldPoints, newPoints ); + std::unordered_set matchedNewPoints; + + for( const auto& match : pointMatches ) + { + PCB_POINT* oldPoint = match.first; + PCB_POINT* newPoint = match.second; + + matchedNewPoints.insert( newPoint ); + const_cast( newPoint->m_Uuid ) = oldPoint->m_Uuid; + } + + for( PCB_POINT* newPoint : newPoints ) + { + if( matchedNewPoints.find( newPoint ) == matchedNewPoints.end() ) + const_cast( newPoint->m_Uuid ) = KIID(); + } + + std::vector oldGroups; + oldGroups.reserve( aExisting->Groups().size() ); + + for( PCB_GROUP* group : aExisting->Groups() ) + oldGroups.push_back( group ); + + std::vector newGroups; + newGroups.reserve( aNew->Groups().size() ); + + for( PCB_GROUP* group : aNew->Groups() ) + newGroups.push_back( group ); + + auto groupMatches = matchItemsBySimilarity( oldGroups, newGroups ); + std::unordered_set matchedNewGroups; + + for( const auto& match : groupMatches ) + { + PCB_GROUP* oldGroup = match.first; + PCB_GROUP* newGroup = match.second; + + matchedNewGroups.insert( newGroup ); + const_cast( newGroup->m_Uuid ) = oldGroup->m_Uuid; + } + + for( PCB_GROUP* newGroup : newGroups ) + { + if( matchedNewGroups.find( newGroup ) == matchedNewGroups.end() ) + const_cast( newGroup->m_Uuid ) = KIID(); + } + + std::vector oldFieldsVec; + std::vector newFieldsVec; + + oldFieldsVec.reserve( aExisting->GetFields().size() ); + + for( PCB_FIELD* field : aExisting->GetFields() ) + { + if( field->IsReference() || field->IsValue() ) + continue; + + oldFieldsVec.push_back( field ); + } + + newFieldsVec.reserve( aNew->GetFields().size() ); + + for( PCB_FIELD* field : aNew->GetFields() ) + { + if( field->IsReference() || field->IsValue() ) + continue; + + newFieldsVec.push_back( field ); + } + + auto fieldMatches = matchItemsBySimilarity( oldFieldsVec, newFieldsVec ); + std::unordered_map oldToNewFields; + std::unordered_set matchedNewFields; + + for( const auto& match : fieldMatches ) + { + PCB_FIELD* oldField = match.first; + PCB_FIELD* newField = match.second; + + oldToNewFields[ oldField ] = newField; + matchedNewFields.insert( newField ); + const_cast( newField->m_Uuid ) = oldField->m_Uuid; + } + + for( PCB_FIELD* newField : newFieldsVec ) + { + if( matchedNewFields.find( newField ) == matchedNewFields.end() ) + const_cast( newField->m_Uuid ) = KIID(); + } + + std::unordered_map oldToNewTexts; + + for( const auto& match : drawingMatches ) + { + PCB_TEXT* oldText = dynamic_cast( match.first ); + PCB_TEXT* newText = dynamic_cast( match.second ); + + if( oldText && newText ) + oldToNewTexts[ oldText ] = newText; + } + std::set handledTextItems; for( BOARD_ITEM* oldItem : aExisting->GraphicalItems() ) @@ -2506,7 +2714,12 @@ void PCB_EDIT_FRAME::ExchangeFootprint( FOOTPRINT* aExisting, FOOTPRINT* aNew, if( dynamic_cast( oldTextItem ) ) continue; - PCB_TEXT* newTextItem = getMatchingTextItem( oldTextItem, aNew ); + PCB_TEXT* newTextItem = nullptr; + + auto textMatchIt = oldToNewTexts.find( oldTextItem ); + + if( textMatchIt != oldToNewTexts.end() ) + newTextItem = textMatchIt->second; if( newTextItem ) { @@ -2566,7 +2779,12 @@ void PCB_EDIT_FRAME::ExchangeFootprint( FOOTPRINT* aExisting, FOOTPRINT* aNew, if( oldField->IsReference() || oldField->IsValue() ) continue; - PCB_FIELD* newField = aNew->GetField( oldField->GetName() ); + PCB_FIELD* newField = nullptr; + + auto fieldMatchIt = oldToNewFields.find( oldField ); + + if( fieldMatchIt != oldToNewFields.end() ) + newField = fieldMatchIt->second; if( newField ) { @@ -2662,7 +2880,6 @@ void PCB_EDIT_FRAME::ExchangeFootprint( FOOTPRINT* aExisting, FOOTPRINT* aNew, } // Updating other parameters - const_cast( aNew->m_Uuid ) = aExisting->m_Uuid; aNew->SetPath( aExisting->GetPath() ); aNew->SetSheetfile( aExisting->GetSheetfile() ); aNew->SetSheetname( aExisting->GetSheetname() );