Fix some determinism in saving

Teardrops don't get their own unique UUID saved in the file, so they can
end up being sorted differently on save.  Instead, we assign a synthetic
UUID to the teardrop based on its two connecting elements' UUIDs.

This also fixes the comparison function so that we don't end up mixing
around caches
This commit is contained in:
Seth Hillbrand
2026-01-23 13:51:18 -08:00
parent 5a66715efd
commit a48d8c0233
9 changed files with 248 additions and 18 deletions
+11
View File
@@ -286,6 +286,17 @@ void KIID::Increment()
}
KIID KIID::Combine( const KIID& aFirst, const KIID& aSecond )
{
KIID result( 0 );
for( int i = 0; i < 16; ++i )
result.m_uuid.data[i] = aFirst.m_uuid.data[i] ^ aSecond.m_uuid.data[i];
return result;
}
void KIID::CreateNilUuids( bool aNil )
{
g_createNilUuids = aNil;
+12
View File
@@ -96,6 +96,18 @@ public:
*/
void ConvertTimestampToUuid();
/**
* Creates a deterministic KIID from two input KIIDs by XORing their underlying UUIDs.
*
* This is useful for generating stable IDs for derived objects (like teardrops) that are
* created from the combination of two parent objects.
*
* @param aFirst the first KIID to combine
* @param aSecond the second KIID to combine
* @return a new KIID that is the XOR combination of the two inputs
*/
static KIID Combine( const KIID& aFirst, const KIID& aSecond );
/**
* Generates a deterministic replacement for a given ID.
*
+10 -5
View File
@@ -3402,34 +3402,39 @@ bool BOARD::cmp_drawings::operator()( const BOARD_ITEM* aFirst, const BOARD_ITEM
{
const PCB_SHAPE* shape = static_cast<const PCB_SHAPE*>( aFirst );
const PCB_SHAPE* other = static_cast<const PCB_SHAPE*>( aSecond );
return shape->Compare( other );
return shape->Compare( other ) < 0;
}
else if( aFirst->Type() == PCB_TEXT_T || aFirst->Type() == PCB_FIELD_T )
{
const PCB_TEXT* text = static_cast<const PCB_TEXT*>( aFirst );
const PCB_TEXT* other = static_cast<const PCB_TEXT*>( aSecond );
return text->Compare( other );
return text->Compare( other ) < 0;
}
else if( aFirst->Type() == PCB_TEXTBOX_T )
{
const PCB_TEXTBOX* textbox = static_cast<const PCB_TEXTBOX*>( aFirst );
const PCB_TEXTBOX* other = static_cast<const PCB_TEXTBOX*>( aSecond );
return textbox->PCB_SHAPE::Compare( other ) && textbox->EDA_TEXT::Compare( other );
int shapeCmp = textbox->PCB_SHAPE::Compare( other );
if( shapeCmp != 0 )
return shapeCmp < 0;
return textbox->EDA_TEXT::Compare( other ) < 0;
}
else if( aFirst->Type() == PCB_TABLE_T )
{
const PCB_TABLE* table = static_cast<const PCB_TABLE*>( aFirst );
const PCB_TABLE* other = static_cast<const PCB_TABLE*>( aSecond );
return PCB_TABLE::Compare( table, other );
return PCB_TABLE::Compare( table, other ) < 0;
}
else if( aFirst->Type() == PCB_BARCODE_T )
{
const PCB_BARCODE* barcode = static_cast<const PCB_BARCODE*>( aFirst );
const PCB_BARCODE* other = static_cast<const PCB_BARCODE*>( aSecond );
return PCB_BARCODE::Compare( barcode, other );
return PCB_BARCODE::Compare( barcode, other ) < 0;
}
return aFirst->m_Uuid < aSecond->m_Uuid;
@@ -794,12 +794,12 @@ void PCB_IO_KICAD_SEXPR::format( const BOARD* aBoard ) const
{
std::set<BOARD_ITEM*, BOARD_ITEM::ptr_cmp> sorted_footprints( aBoard->Footprints().begin(),
aBoard->Footprints().end() );
std::set<BOARD_ITEM*, BOARD_ITEM::ptr_cmp> sorted_drawings( aBoard->Drawings().begin(),
std::set<BOARD_ITEM*, BOARD::cmp_drawings> sorted_drawings( aBoard->Drawings().begin(),
aBoard->Drawings().end() );
std::set<PCB_TRACK*, PCB_TRACK::cmp_tracks> sorted_tracks( aBoard->Tracks().begin(),
aBoard->Tracks().end() );
std::set<PCB_POINT*, BOARD_ITEM::ptr_cmp> sorted_points( aBoard->Points().begin(),
aBoard->Points().end() );
aBoard->Points().end() );
std::set<BOARD_ITEM*, BOARD_ITEM::ptr_cmp> sorted_zones( aBoard->Zones().begin(),
aBoard->Zones().end() );
std::set<BOARD_ITEM*, BOARD_ITEM::ptr_cmp> sorted_groups( aBoard->Groups().begin(),
+18 -6
View File
@@ -55,10 +55,15 @@ TEARDROP_MANAGER::TEARDROP_MANAGER( BOARD* aBoard, TOOL_MANAGER* aToolManager )
ZONE* TEARDROP_MANAGER::createTeardrop( TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack ) const
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack,
BOARD_ITEM* aCandidate ) const
{
ZONE* teardrop = new ZONE( m_board );
// Create a deterministic UUID from the track and candidate UUIDs so that teardrops
// maintain stable ordering in the output file across save/load cycles.
const_cast<KIID&>( teardrop->m_Uuid ) = KIID::Combine( aTrack->m_Uuid, aCandidate->m_Uuid );
// teardrop settings are the last zone settings used by a zone dialog.
// override them by default.
ZONE_SETTINGS::GetDefaultSettings().ExportSetting( *teardrop, false );
@@ -94,10 +99,17 @@ ZONE* TEARDROP_MANAGER::createTeardrop( TEARDROP_VARIANT aTeardropVariant,
ZONE* TEARDROP_MANAGER::createTeardropMask( TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack ) const
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack,
BOARD_ITEM* aCandidate ) const
{
ZONE* teardrop = new ZONE( m_board );
// Create a deterministic UUID from the track and candidate UUIDs, then increment it
// to differentiate from the copper teardrop zone.
KIID maskUuid = KIID::Combine( aTrack->m_Uuid, aCandidate->m_Uuid );
maskUuid.Increment();
const_cast<KIID&>( teardrop->m_Uuid ) = maskUuid;
teardrop->SetTeardropAreaType( aTeardropVariant == TD_TYPE_PADVIA ? TEARDROP_TYPE::TD_VIAPAD
: TEARDROP_TYPE::TD_TRACKEND );
teardrop->SetLayer( aTrack->GetLayer() == F_Cu ? F_Mask : B_Mask );
@@ -135,9 +147,9 @@ ZONE* TEARDROP_MANAGER::createTeardropMask( TEARDROP_VARIANT aTeardropVariant,
void TEARDROP_MANAGER::createAndAddTeardropWithMask( BOARD_COMMIT& aCommit,
TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints,
PCB_TRACK* aTrack )
PCB_TRACK* aTrack, BOARD_ITEM* aCandidate )
{
ZONE* new_teardrop = createTeardrop( aTeardropVariant, aPoints, aTrack );
ZONE* new_teardrop = createTeardrop( aTeardropVariant, aPoints, aTrack, aCandidate );
m_board->Add( new_teardrop, ADD_MODE::BULK_INSERT );
m_createdTdList.push_back( new_teardrop );
@@ -145,7 +157,7 @@ void TEARDROP_MANAGER::createAndAddTeardropWithMask( BOARD_COMMIT& aCommit,
if( aTrack->HasSolderMask() && IsExternalCopperLayer( aTrack->GetLayer() ) )
{
ZONE* new_teardrop_mask = createTeardropMask( aTeardropVariant, aPoints, aTrack );
ZONE* new_teardrop_mask = createTeardropMask( aTeardropVariant, aPoints, aTrack, aCandidate );
m_board->Add( new_teardrop_mask, ADD_MODE::BULK_INSERT );
aCommit.Added( new_teardrop_mask );
}
@@ -162,7 +174,7 @@ bool TEARDROP_MANAGER::tryCreateTrackTeardrop( BOARD_COMMIT& aCommit,
if( computeTeardropPolygon( aParams, points, aTrack, aCandidate, aPos ) )
{
createAndAddTeardropWithMask( aCommit, aTeardropVariant, points, aTrack );
createAndAddTeardropWithMask( aCommit, aTeardropVariant, points, aTrack, aCandidate );
return true;
}
+8 -5
View File
@@ -203,12 +203,13 @@ private:
* @param aPoints is the polygonal shape
* @param aTrack is the track connected to the starting points of the teardrop
* (mainly for net info)
* @param aCandidate is the pad/via/track that the teardrop connects to (used for UUID)
*/
ZONE* createTeardrop( TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack ) const;
ZONE* createTeardrop( TEARDROP_VARIANT aTeardropVariant, std::vector<VECTOR2I>& aPoints,
PCB_TRACK* aTrack, BOARD_ITEM* aCandidate ) const;
ZONE* createTeardropMask( TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack ) const;
ZONE* createTeardropMask( TEARDROP_VARIANT aTeardropVariant, std::vector<VECTOR2I>& aPoints,
PCB_TRACK* aTrack, BOARD_ITEM* aCandidate ) const;
/**
* Creates and adds a teardrop with optional mask to the board
@@ -216,9 +217,11 @@ private:
* @param aTeardropVariant = variant of the teardrop( attached to a pad, or a track end )
* @param aPoints is the polygonal shape
* @param aTrack is the track connected to the starting points of the teardrop
* @param aCandidate is the pad/via/track that the teardrop connects to
*/
void createAndAddTeardropWithMask( BOARD_COMMIT& aCommit, TEARDROP_VARIANT aTeardropVariant,
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack );
std::vector<VECTOR2I>& aPoints, PCB_TRACK* aTrack,
BOARD_ITEM* aCandidate );
/**
* Attempts to create a track-to-track teardrop
+66
View File
@@ -104,4 +104,70 @@ BOOST_AUTO_TEST_CASE( LegacyTimestamp )
}
BOOST_AUTO_TEST_CASE( CombineDeterministic )
{
KIID a, b, c;
// Combining the same two KIIDs should always produce the same result
KIID combined1 = KIID::Combine( a, b );
KIID combined2 = KIID::Combine( a, b );
BOOST_CHECK_EQUAL( combined1.AsString(), combined2.AsString() );
// Combined result should be different from inputs
BOOST_CHECK( combined1 != a );
BOOST_CHECK( combined1 != b );
// Different inputs should produce different outputs
KIID combined3 = KIID::Combine( a, c );
BOOST_CHECK( combined1 != combined3 );
// Order matters for XOR with different values
KIID combined4 = KIID::Combine( b, a );
BOOST_CHECK_EQUAL( combined1.AsString(), combined4.AsString() );
}
BOOST_AUTO_TEST_CASE( CombineWithKnownValues )
{
// Test with known UUID strings to verify XOR behavior
KIID a( "00000000-0000-0000-0000-000000000001" );
KIID b( "00000000-0000-0000-0000-000000000002" );
KIID combined = KIID::Combine( a, b );
// XOR of 0x01 and 0x02 = 0x03
BOOST_CHECK_EQUAL( combined.AsString(), "00000000-0000-0000-0000-000000000003" );
// Combining with self should give all zeros
KIID selfCombined = KIID::Combine( a, a );
BOOST_CHECK_EQUAL( selfCombined.AsString(), "00000000-0000-0000-0000-000000000000" );
}
BOOST_AUTO_TEST_CASE( CombineUniqueness )
{
// Generate several random KIIDs and verify all combinations are unique.
// XOR is commutative, so Combine(a,b) == Combine(b,a). We only count
// unique unordered pairs, which gives n*(n-1)/2 combinations.
std::vector<KIID> ids;
for( int i = 0; i < 10; ++i )
ids.push_back( KIID() );
std::set<wxString> combinations;
for( size_t i = 0; i < ids.size(); ++i )
{
for( size_t j = i + 1; j < ids.size(); ++j )
{
KIID combined = KIID::Combine( ids[i], ids[j] );
combinations.insert( combined.AsString() );
}
}
// All 45 unique unordered pairs (10*9/2) should produce unique results
BOOST_CHECK_EQUAL( combinations.size(), 45 );
}
BOOST_AUTO_TEST_SUITE_END()
+1
View File
@@ -59,6 +59,7 @@ set( QA_PCBNEW_SRCS
test_pcb_grid_helper.cpp
test_save_load.cpp
test_stacked_pin_netlist.cpp
test_teardrop_uuid.cpp
test_tracks_cleaner.cpp
test_triangulation.cpp
test_multichannel.cpp
+120
View File
@@ -0,0 +1,120 @@
/*
* 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_teardrop_uuid.cpp
* Test that deterministic UUID generation works correctly for teardrop scenarios.
* The core KIID::Combine function is tested in test_kiid.cpp. These tests verify
* the expected behavior when combining track and pad UUIDs as used by teardrops.
*/
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <kiid.h>
BOOST_AUTO_TEST_SUITE( TeardropUUID )
BOOST_AUTO_TEST_CASE( CombineTrackAndPadUuids )
{
// Simulate the teardrop UUID generation: combine track and pad UUIDs
KIID trackUuid;
KIID padUuid;
KIID teardropUuid = KIID::Combine( trackUuid, padUuid );
// The combined UUID should be different from both inputs
BOOST_CHECK( teardropUuid != trackUuid );
BOOST_CHECK( teardropUuid != padUuid );
// The same inputs should always produce the same output
KIID teardropUuid2 = KIID::Combine( trackUuid, padUuid );
BOOST_CHECK_EQUAL( teardropUuid.AsString(), teardropUuid2.AsString() );
}
BOOST_AUTO_TEST_CASE( MaskUuidIsIncrementedFromTeardropUuid )
{
// Teardrop masks use the combined UUID, then increment to differentiate
KIID trackUuid;
KIID padUuid;
KIID teardropUuid = KIID::Combine( trackUuid, padUuid );
KIID maskUuid = KIID::Combine( trackUuid, padUuid );
maskUuid.Increment();
// The mask UUID should be different from the teardrop UUID
BOOST_CHECK( maskUuid != teardropUuid );
// Both should be deterministic
KIID teardropUuid2 = KIID::Combine( trackUuid, padUuid );
KIID maskUuid2 = KIID::Combine( trackUuid, padUuid );
maskUuid2.Increment();
BOOST_CHECK_EQUAL( teardropUuid.AsString(), teardropUuid2.AsString() );
BOOST_CHECK_EQUAL( maskUuid.AsString(), maskUuid2.AsString() );
}
BOOST_AUTO_TEST_CASE( DifferentTracksProduceDifferentTeardropUuids )
{
// Two tracks connecting to the same pad should produce different teardrop UUIDs
KIID track1Uuid;
KIID track2Uuid;
KIID padUuid;
KIID teardrop1Uuid = KIID::Combine( track1Uuid, padUuid );
KIID teardrop2Uuid = KIID::Combine( track2Uuid, padUuid );
// Different tracks should produce different teardrops
BOOST_CHECK( teardrop1Uuid != teardrop2Uuid );
// Each should still be deterministic
KIID teardrop1Uuid2 = KIID::Combine( track1Uuid, padUuid );
KIID teardrop2Uuid2 = KIID::Combine( track2Uuid, padUuid );
BOOST_CHECK_EQUAL( teardrop1Uuid.AsString(), teardrop1Uuid2.AsString() );
BOOST_CHECK_EQUAL( teardrop2Uuid.AsString(), teardrop2Uuid2.AsString() );
}
BOOST_AUTO_TEST_CASE( StableAcrossRegenerations )
{
// Simulates regenerating teardrops: same track+pad should always give same UUID
KIID trackUuid( "12345678-1234-1234-1234-123456789012" );
KIID padUuid( "abcdef01-abcd-abcd-abcd-abcdef012345" );
// First generation
KIID teardropUuid1 = KIID::Combine( trackUuid, padUuid );
// Simulate regeneration by computing again
KIID teardropUuid2 = KIID::Combine( trackUuid, padUuid );
// Should be identical
BOOST_CHECK_EQUAL( teardropUuid1.AsString(), teardropUuid2.AsString() );
}
BOOST_AUTO_TEST_SUITE_END()