KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pcb_differ.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 */
20
21#include "pcb_differ.h"
22
25#include <hashtables.h>
26
27#include <board.h>
29#include <project.h>
32#include <netclass.h>
34
35#include <wx/file.h>
36#include <wx/filename.h>
38#include <board_item.h>
39#include <footprint.h>
40#include <pad.h>
41#include <pcb_track.h>
42#include <pcb_shape.h>
43#include <pcb_text.h>
44#include <pcb_field.h>
45#include <zone.h>
46#include <properties/property.h>
48
49#include <algorithm>
50
51
52namespace KICAD_DIFF
53{
54
56{
57 if( const PCB_FIELD* field = dynamic_cast<const PCB_FIELD*>( &aItem ) )
58 {
59 if( const FOOTPRINT* fp = field->GetParentFootprint() )
60 return KIID::FromDeterministicString( fp->m_Uuid.AsString() + wxS( ":" ) + field->GetUntranslatedName() );
61 }
62
63 return aItem.m_Uuid;
64}
65
66
67BOARD_ITEM* FindPcbDiffItem( const BOARD* aBoard, const KIID& aId )
68{
69 if( !aBoard )
70 return nullptr;
71
72 // ResolveItem uses the board's UUID cache for top-level items and footprint children.
73 // Request nullptr on a miss instead of the DELETED_BOARD_ITEM sentinel so field diff
74 // IDs reach the name-based lookup below and missing items still return nullptr.
75 if( BOARD_ITEM* item = aBoard->ResolveItem( aId, /* aAllowNullptrReturn */ true ) )
76 return item;
77
78 for( FOOTPRINT* fp : aBoard->Footprints() )
79 {
80 for( PCB_FIELD* field : fp->GetFields() )
81 {
82 if( PcbDiffItemId( *field ) == aId )
83 return field;
84 }
85 }
86
87 return nullptr;
88}
89
90
91PCB_DIFFER::PCB_DIFFER( const BOARD* aBefore, const BOARD* aAfter, const wxString& aPath ) :
92 m_before( aBefore ),
93 m_after( aAfter ),
94 m_path( aPath )
95{
96}
97
98
99PCB_DIFFER::~PCB_DIFFER() = default;
100
101
102wxString PCB_DIFFER::itemTypeName( const BOARD_ITEM* aItem )
103{
104 if( !aItem )
105 return wxEmptyString;
106
107 if( const PCB_FIELD* field = dynamic_cast<const PCB_FIELD*>( aItem ) )
108 return field->GetName();
109
110 return aItem->GetClass();
111}
112
113
114std::optional<wxString> PCB_DIFFER::itemRefdes( const BOARD_ITEM* aItem )
115{
116 if( auto fp = dynamic_cast<const FOOTPRINT*>( aItem ) )
117 return fp->GetReference();
118
119 if( auto track = dynamic_cast<const PCB_TRACK*>( aItem ) )
120 {
121 if( track->GetNetCode() > 0 && !track->GetNetname().IsEmpty() )
122 return track->GetNetname();
123 }
124
125 if( auto fp = dynamic_cast<const FOOTPRINT*>( aItem ? aItem->GetParent() : nullptr ) )
126 {
127 // Pad / footprint-child: borrow the parent's refdes for cross-probing.
128 return fp->GetReference();
129 }
130
131 return std::nullopt;
132}
133
134
136{
138 d.id = KIID_PATH();
139 d.id.push_back( aItem->m_Uuid );
140 d.type = itemTypeName( aItem );
141 d.position = aItem->GetPosition();
142 d.bbox = aItem->GetBoundingBox();
143
144 // keyProps for similarity fallback: identifying fields specific to the
145 // item type. Footprints carry their library id (very stable); tracks
146 // and pads carry net code.
147 if( auto fp = dynamic_cast<const FOOTPRINT*>( aItem ) )
148 {
149 d.keyProps.emplace_back( wxS( "lib_id" ), fp->GetFPIDAsString().ToStdString() );
150 d.keyProps.emplace_back( wxS( "reference" ), fp->GetReference().ToStdString() );
151 }
152 else if( auto track = dynamic_cast<const PCB_TRACK*>( aItem ) )
153 {
154 d.keyProps.emplace_back( wxS( "net" ), std::to_string( track->GetNetCode() ) );
155 d.keyProps.emplace_back( wxS( "layer" ), std::to_string( track->GetLayer() ) );
156 }
157 else if( auto pad = dynamic_cast<const PAD*>( aItem ) )
158 {
159 d.keyProps.emplace_back( wxS( "number" ), pad->GetNumber().ToStdString() );
160 d.keyProps.emplace_back( wxS( "net" ), std::to_string( pad->GetNetCode() ) );
161 }
162 else if( auto zone = dynamic_cast<const ZONE*>( aItem ) )
163 {
164 d.keyProps.emplace_back( wxS( "name" ), zone->GetZoneName().ToStdString() );
165 d.keyProps.emplace_back( wxS( "net" ), std::to_string( zone->GetNetCode() ) );
166 }
167
168 return d;
169}
170
171
172// Library metadata that is never worth showing, for any change kind.
173static bool pcbLibraryMetadataNoise( const wxString& aName )
174{
175 return aName == wxS( "Library Link" ) || aName == wxS( "Library Description" ) || aName == wxS( "Keywords" );
176}
177
178
179// Project-default propagation and library-reorganization noise. Inside a
180// footprint, per-child layout shifts follow the parent's move, so they are
181// noise too.
182static bool pcbDiffPropertyIsNoise( const wxString& aName, bool aInsideFootprint )
183{
184 const bool globalNoise = pcbLibraryMetadataNoise( aName ) || aName == wxS( "Auto Thickness" )
185 || aName == wxS( "Keep Upright" ) || aName == wxS( "Thickness" )
186 || aName == wxS( "Enable Teardrops" ) || aName == wxS( "Thermal Relief Spoke Angle" )
187 || aName == wxS( "Pin Name" ) || aName == wxS( "Net" ) || aName == wxS( "Pad Shape" );
188
189 if( globalNoise )
190 return true;
191
192 const bool childLayoutNoise = aName == wxS( "Position X" ) || aName == wxS( "Position Y" )
193 || aName == wxS( "Start X" ) || aName == wxS( "Start Y" ) || aName == wxS( "End X" )
194 || aName == wxS( "End Y" ) || aName == wxS( "Center X" ) || aName == wxS( "Center Y" )
195 || aName == wxS( "Orientation" ) || aName == wxS( "Height" )
196 || aName == wxS( "Width" ) || aName == wxS( "Layer" ) || aName == wxS( "Line Width" )
197 || aName == wxS( "Hole Size X" ) || aName == wxS( "Hole Size Y" );
198
199 return aInsideFootprint && childLayoutNoise;
200}
201
202
203// Full property list for an added or removed item. Only library metadata is
204// dropped: net, pad shape, geometry and the like describe what was added or
205// removed and are worth showing, unlike in the modified case.
206static std::vector<PROPERTY_DELTA> pcbAddedRemovedProperties( const BOARD_ITEM* aItem, bool aAsAfter )
207{
208 std::vector<PROPERTY_DELTA> deltas = ItemProperties( aItem, aAsAfter );
209
210 std::erase_if( deltas,
211 [&]( const PROPERTY_DELTA& d )
212 {
213 return pcbLibraryMetadataNoise( d.name );
214 } );
215
216 return deltas;
217}
218
219
220std::vector<PROPERTY_DELTA> PCB_DIFFER::diffProperties( const BOARD_ITEM* aBefore, const BOARD_ITEM* aAfter ) const
221{
222 auto deltas = DiffItemProperties( aBefore, aAfter );
223
224 const bool insideFootprint = aBefore && aBefore->GetParent() && aBefore->GetParent()->Type() == PCB_FOOTPRINT_T;
225
226 std::erase_if( deltas,
227 [&]( const PROPERTY_DELTA& d )
228 {
229 return pcbDiffPropertyIsNoise( d.name, insideFootprint );
230 } );
231
232 if( auto zoneA = dynamic_cast<const ZONE*>( aBefore ) )
233 {
234 if( auto zoneB = dynamic_cast<const ZONE*>( aAfter ) )
235 {
236 auto toPolygonSet = []( const SHAPE_POLY_SET* aPoly ) -> DIFF_VALUE::PolygonSet
237 {
239
240 if( !aPoly )
241 return out;
242
243 for( int o = 0; o < aPoly->OutlineCount(); ++o )
244 {
245 const auto& polyA = aPoly->CPolygon( o );
246 std::vector<std::vector<VECTOR2I>> contours;
247
248 for( const auto& contour : polyA )
249 {
250 std::vector<VECTOR2I> pts;
251 pts.reserve( contour.PointCount() );
252
253 for( int p = 0; p < contour.PointCount(); ++p )
254 pts.push_back( contour.CPoint( p ) );
255
256 contours.push_back( std::move( pts ) );
257 }
258
259 out.push_back( std::move( contours ) );
260 }
261
262 return out;
263 };
264
265 DIFF_VALUE::PolygonSet outlineA = toPolygonSet( zoneA->Outline() );
266 DIFF_VALUE::PolygonSet outlineB = toPolygonSet( zoneB->Outline() );
267
268 if( outlineA != outlineB )
269 {
271 d.name = wxS( "Outline" );
272 d.before = DIFF_VALUE::FromPolygonSet( std::move( outlineA ) );
273 d.after = DIFF_VALUE::FromPolygonSet( std::move( outlineB ) );
274 deltas.push_back( std::move( d ) );
275 }
276
277 for( PCB_LAYER_ID layer : zoneB->GetLayerSet().Seq() )
278 {
279 if( !zoneA->GetLayerSet().Contains( layer ) )
280 continue;
281
282 DIFF_VALUE::PolygonSet fillA = toPolygonSet( zoneA->GetFilledPolysList( layer ).get() );
283 DIFF_VALUE::PolygonSet fillB = toPolygonSet( zoneB->GetFilledPolysList( layer ).get() );
284
285 if( fillA == fillB )
286 continue;
287
289 d.name = wxString::Format( wxS( "Filled Area (%s)" ), LayerName( layer ) );
290 d.before = DIFF_VALUE::FromPolygonSet( std::move( fillA ) );
291 d.after = DIFF_VALUE::FromPolygonSet( std::move( fillB ) );
292 deltas.push_back( std::move( d ) );
293 }
294 }
295 }
296
297 return deltas;
298}
299
300
301std::vector<ITEM_CHANGE> PCB_DIFFER::diffFootprintChildren( const FOOTPRINT* aBefore, const FOOTPRINT* aAfter ) const
302{
303 std::vector<ITEM_CHANGE> children;
304
305 if( !aBefore || !aAfter )
306 return children;
307
308 // Keep the parent footprint in the path so consumers can group child changes
309 // and the merge applier can locate their destination footprint.
310 std::vector<ITEM_DESCRIPTOR> beforeDesc;
311 std::vector<ITEM_DESCRIPTOR> afterDesc;
312 std::map<KIID_PATH, const BOARD_ITEM*> beforeMap;
313 std::map<KIID_PATH, const BOARD_ITEM*> afterMap;
314
315 auto childDescriptor = [&]( const FOOTPRINT* aFp, const BOARD_ITEM* aChild ) -> ITEM_DESCRIPTOR
316 {
317 ITEM_DESCRIPTOR d = makeDescriptor( aChild );
318 d.id = KIID_PATH();
319 d.id.push_back( aFp->m_Uuid );
320 d.id.push_back( PcbDiffItemId( *aChild ) );
321
322 return d;
323 };
324
325 auto collect = [&]( const FOOTPRINT* aFp, std::vector<ITEM_DESCRIPTOR>& aOut,
326 std::map<KIID_PATH, const BOARD_ITEM*>& aMap )
327 {
328 for( const PAD* pad : aFp->Pads() )
329 {
330 ITEM_DESCRIPTOR d = childDescriptor( aFp, pad );
331 aOut.push_back( d );
332 aMap[d.id] = pad;
333 }
334
335 for( const BOARD_ITEM* item : aFp->GraphicalItems() )
336 {
337 ITEM_DESCRIPTOR d = childDescriptor( aFp, item );
338 aOut.push_back( d );
339 aMap[d.id] = item;
340 }
341
342 for( const ZONE* zone : aFp->Zones() )
343 {
344 ITEM_DESCRIPTOR d = childDescriptor( aFp, zone );
345 aOut.push_back( d );
346 aMap[d.id] = zone;
347 }
348
349 for( const PCB_FIELD* field : aFp->GetFields() )
350 {
351 ITEM_DESCRIPTOR d = childDescriptor( aFp, field );
352 aOut.push_back( d );
353 aMap[d.id] = field;
354 }
355 };
356
357 collect( aBefore, beforeDesc, beforeMap );
358 collect( aAfter, afterDesc, afterMap );
359
360 IDENTITY_RECONCILER reconciler( m_options.identity );
361 RECONCILIATION recon = reconciler.Reconcile( beforeDesc, afterDesc );
362
363 // Matched pairs: compute property deltas. Items count as "changed" if any
364 // property surfaced a delta OR if their semantic operator== rejects equality
365 // (which covers fields that aren't exposed through PROPERTY_MANAGER).
366 for( const auto& [idA, idB] : recon.aToB )
367 {
368 auto itA = beforeMap.find( idA );
369 auto itB = afterMap.find( idB );
370 const BOARD_ITEM* a = itA == beforeMap.end() ? nullptr : itA->second;
371 const BOARD_ITEM* b = itB == afterMap.end() ? nullptr : itB->second;
372
373 if( !a || !b )
374 continue;
375
376 std::vector<PROPERTY_DELTA> propDeltas;
377
378 if( m_options.deepCompare )
379 propDeltas = diffProperties( a, b );
380
381 if( propDeltas.empty() )
382 continue;
383
384 ITEM_CHANGE c;
385 c.id = idA;
386 c.typeName = itemTypeName( a );
388 c.bbox = b->GetBoundingBox();
389 c.refdes = itemRefdes( b );
390 c.properties = std::move( propDeltas );
391 children.push_back( std::move( c ) );
392 }
393
394 auto isLibraryUuidNoise = []( const BOARD_ITEM* aItem )
395 {
396 return aItem && ( aItem->Type() == PCB_SHAPE_T || aItem->Type() == PCB_TEXT_T );
397 };
398
399 for( const KIID_PATH& idA : recon.aOnly )
400 {
401 auto it = beforeMap.find( idA );
402
403 if( it == beforeMap.end() || !it->second )
404 continue;
405
406 const BOARD_ITEM* a = it->second;
407
408 if( isLibraryUuidNoise( a ) )
409 continue;
410
411 ITEM_CHANGE c;
412 c.id = idA;
413 c.typeName = itemTypeName( a );
415 c.bbox = a->GetBoundingBox();
416 c.refdes = itemRefdes( a );
417 c.properties = pcbAddedRemovedProperties( a, /*aAsAfter=*/false );
418 children.push_back( std::move( c ) );
419 }
420
421 for( const KIID_PATH& idB : recon.bOnly )
422 {
423 auto it = afterMap.find( idB );
424
425 if( it == afterMap.end() || !it->second )
426 continue;
427
428 const BOARD_ITEM* b = it->second;
429
430 if( isLibraryUuidNoise( b ) )
431 continue;
432
433 ITEM_CHANGE c;
434 c.id = idB;
435 c.typeName = itemTypeName( b );
437 c.bbox = b->GetBoundingBox();
438 c.refdes = itemRefdes( b );
439 c.properties = pcbAddedRemovedProperties( b, /*aAsAfter=*/true );
440 children.push_back( std::move( c ) );
441 }
442
443 sortChanges( children );
444 return children;
445}
446
447
448void PCB_DIFFER::sortChanges( std::vector<ITEM_CHANGE>& aChanges )
449{
450 std::sort( aChanges.begin(), aChanges.end(),
451 []( const ITEM_CHANGE& aL, const ITEM_CHANGE& aR )
452 {
453 if( aL.id < aR.id )
454 return true;
455 if( aR.id < aL.id )
456 return false;
457
458 if( aL.typeName != aR.typeName )
459 return aL.typeName < aR.typeName;
460
461 return static_cast<int>( aL.kind ) < static_cast<int>( aR.kind );
462 } );
463}
464
465
467{
469 result.path = m_path;
470 result.docType = wxS( "kicad_pcb" );
471
472 if( !m_before || !m_after )
473 return result;
474
475 // Build descriptors for the top-level item set.
476 const BOARD_ITEM_SET beforeSet = m_before->GetItemSet();
477 const BOARD_ITEM_SET afterSet = m_after->GetItemSet();
478
479 std::vector<ITEM_DESCRIPTOR> beforeDesc;
480 std::vector<ITEM_DESCRIPTOR> afterDesc;
481 std::map<KIID, const BOARD_ITEM*> beforeMap;
482 std::map<KIID, const BOARD_ITEM*> afterMap;
483
484 beforeDesc.reserve( beforeSet.size() );
485 afterDesc.reserve( afterSet.size() );
486
487 for( const BOARD_ITEM* item : beforeSet )
488 {
489 if( !item )
490 continue;
491
492 beforeDesc.push_back( makeDescriptor( item ) );
493 beforeMap[item->m_Uuid] = item;
494 }
495
496 for( const BOARD_ITEM* item : afterSet )
497 {
498 if( !item )
499 continue;
500
501 afterDesc.push_back( makeDescriptor( item ) );
502 afterMap[item->m_Uuid] = item;
503 }
504
505 if( m_options.progress )
506 m_options.progress( 0.2 );
507
508 IDENTITY_RECONCILER reconciler( m_options.identity );
509 RECONCILIATION recon = reconciler.Reconcile( beforeDesc, afterDesc );
510
511 if( m_options.progress )
512 m_options.progress( 0.5 );
513
514 // Duplicate-UUID records (within either side).
515 for( const KIID_PATH& dup : recon.duplicatesA )
516 {
517 ITEM_CHANGE c;
518 c.id = dup;
519 c.typeName = wxS( "BOARD_ITEM" );
521 result.changes.push_back( std::move( c ) );
522 }
523
524 for( const KIID_PATH& dup : recon.duplicatesB )
525 {
526 if( std::find_if( result.changes.begin(), result.changes.end(),
527 [&]( const ITEM_CHANGE& aC )
528 {
529 return aC.id == dup && aC.kind == CHANGE_KIND::DUPLICATE_UUID;
530 } )
531 != result.changes.end() )
532 {
533 continue;
534 }
535
536 ITEM_CHANGE c;
537 c.id = dup;
538 c.typeName = wxS( "BOARD_ITEM" );
540 result.changes.push_back( std::move( c ) );
541 }
542
543 // Matched pairs.
544 for( const auto& [idA, idB] : recon.aToB )
545 {
546 const KIID& uuidA = idA.back();
547 const KIID& uuidB = idB.back();
548 auto itA = beforeMap.find( uuidA );
549 auto itB = afterMap.find( uuidB );
550 const BOARD_ITEM* a = itA == beforeMap.end() ? nullptr : itA->second;
551 const BOARD_ITEM* b = itB == afterMap.end() ? nullptr : itB->second;
552
553 if( !a || !b )
554 continue;
555
556 std::vector<PROPERTY_DELTA> propDeltas;
557
558 if( m_options.deepCompare )
559 propDeltas = diffProperties( a, b );
560
561 // For footprints, also walk children. A footprint may compare unequal
562 // because a child changed, in which case the parent's properties
563 // themselves haven't moved — we still need to record children.
564 std::vector<ITEM_CHANGE> childChanges;
565
566 if( auto fpA = dynamic_cast<const FOOTPRINT*>( a ) )
567 {
568 if( auto fpB = dynamic_cast<const FOOTPRINT*>( b ) )
569 childChanges = diffFootprintChildren( fpA, fpB );
570 }
571
572 if( propDeltas.empty() && childChanges.empty() )
573 continue;
574
575 ITEM_CHANGE c;
576 c.id = idA;
577 c.typeName = itemTypeName( a );
579 c.bbox = b->GetBoundingBox();
580 c.refdes = itemRefdes( b );
581 c.properties = std::move( propDeltas );
582 c.children = std::move( childChanges );
583 result.changes.push_back( std::move( c ) );
584 }
585
586 // Auto-generated items (teardrops, etc.) flip in lockstep with a global
587 // setting rather than being user-authored. Their presence on one side and
588 // not the other is reported as the Enable Teardrops setting change, not
589 // as N separate add/remove records. Teardrops historically render as
590 // ZONEs flagged with IsTeardropArea, hence the second branch.
591 auto isAutoGenerated = []( const BOARD_ITEM* aItem )
592 {
593 if( !aItem )
594 return false;
595
596 if( aItem->Type() == PCB_GENERATOR_T )
597 return true;
598
599 if( auto zone = dynamic_cast<const ZONE*>( aItem ) )
600 return zone->IsTeardropArea();
601
602 return false;
603 };
604
605 // Items present only in ancestor: REMOVED.
606 for( const KIID_PATH& idA : recon.aOnly )
607 {
608 auto it = beforeMap.find( idA.back() );
609
610 if( it == beforeMap.end() || !it->second )
611 continue;
612
613 const BOARD_ITEM* a = it->second;
614
615 if( isAutoGenerated( a ) )
616 continue;
617
618 ITEM_CHANGE c;
619 c.id = idA;
620 c.typeName = itemTypeName( a );
622 c.bbox = a->GetBoundingBox();
623 c.refdes = itemRefdes( a );
624 c.properties = pcbAddedRemovedProperties( a, /*aAsAfter=*/false );
625
626 // For footprints, snapshot child items so the consumer can show what's
627 // being removed without re-walking.
628 if( auto fp = dynamic_cast<const FOOTPRINT*>( a ) )
629 {
630 std::vector<ITEM_CHANGE> dummyAfter;
631 FOOTPRINT empty( nullptr );
632 // diffFootprintChildren needs two footprints; producing an empty
633 // "after" gives us a REMOVED record per child.
635 }
636
637 result.changes.push_back( std::move( c ) );
638 }
639
640 // Items present only in after: ADDED.
641 for( const KIID_PATH& idB : recon.bOnly )
642 {
643 auto it = afterMap.find( idB.back() );
644
645 if( it == afterMap.end() || !it->second )
646 continue;
647
648 const BOARD_ITEM* b = it->second;
649
650 if( isAutoGenerated( b ) )
651 continue;
652
653 ITEM_CHANGE c;
654 c.id = idB;
655 c.typeName = itemTypeName( b );
657 c.bbox = b->GetBoundingBox();
658 c.refdes = itemRefdes( b );
659 c.properties = pcbAddedRemovedProperties( b, /*aAsAfter=*/true );
660
661 if( auto fp = dynamic_cast<const FOOTPRINT*>( b ) )
662 {
663 FOOTPRINT empty( nullptr );
665 }
666
667 result.changes.push_back( std::move( c ) );
668 }
669
670 // Document-level settings — board thickness, paper format. These aren't
671 // walked items so the per-item loop above can't catch a change to them;
672 // emit a single synthetic ITEM_CHANGE with an empty KIID_PATH so the
673 // merge engine can plan a resolution on it and the applier knows which
674 // side's settings to carry over. Without this the applier defaults to
675 // the new (empty) result BOARD's settings and silently reverts both
676 // sides' divergent changes to defaults.
677 std::vector<PROPERTY_DELTA> docDeltas;
678
679 AppendPaperDeltas( docDeltas, m_before->GetPageSettings(), m_after->GetPageSettings() );
680
681 const BOARD_DESIGN_SETTINGS& beforeDS = m_before->GetDesignSettings();
682 const BOARD_DESIGN_SETTINGS& afterDS = m_after->GetDesignSettings();
683
684 const int beforeThickness = beforeDS.GetBoardThickness();
685 const int afterThickness = afterDS.GetBoardThickness();
686
687 if( beforeThickness != afterThickness )
688 {
691 d.before = DIFF_VALUE::FromInt( beforeThickness );
692 d.after = DIFF_VALUE::FromInt( afterThickness );
693 docDeltas.push_back( std::move( d ) );
694 }
695
696 // Stackup is structural (layer count, dielectric materials, copper
697 // weights) so a per-field walk would explode the change list. Detect
698 // any change with BOARD_STACKUP::operator== and emit a single delta
699 // carrying a human-readable summary; the applier copies whole
700 // BOARD_DESIGN_SETTINGS so a TAKE_OURS / TAKE_THEIRS resolution preserves
701 // the chosen side's full stackup.
702 const BOARD_STACKUP& beforeStackup = beforeDS.GetStackupDescriptor();
703 const BOARD_STACKUP& afterStackup = afterDS.GetStackupDescriptor();
704
705 auto summarizeStackup = []( const BOARD_STACKUP& aStackup ) -> std::string
706 {
707 int copper = 0;
708 int dielectric = 0;
709
710 for( const BOARD_STACKUP_ITEM* item : aStackup.GetList() )
711 {
712 if( !item )
713 continue;
714
715 if( item->GetType() == BS_ITEM_TYPE_COPPER )
716 ++copper;
717 else if( item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
718 ++dielectric;
719 }
720
721 // Content hash matching BOARD_STACKUP::operator== fields so
722 // before/after render differently when any compared field
723 // changed and the copper/dielectric counts didn't.
724 std::size_t h = std::hash<std::string>{}( aStackup.m_FinishType.ToStdString() );
725 h = KiHashCombine( h, std::hash<bool>{}( aStackup.m_HasDielectricConstrains ) );
726 h = KiHashCombine( h, std::hash<bool>{}( aStackup.m_HasThicknessConstrains ) );
727 h = KiHashCombine( h, std::hash<bool>{}( aStackup.m_EdgePlating ) );
728 h = KiHashCombine( h, std::hash<int>{}( static_cast<int>( aStackup.m_EdgeConnectorConstraints ) ) );
729
730 for( const BOARD_STACKUP_ITEM* item : aStackup.GetList() )
731 {
732 if( !item )
733 continue;
734
735 h = KiHashCombine( h, std::hash<int>{}( static_cast<int>( item->GetType() ) ) );
736 h = KiHashCombine( h, std::hash<int>{}( static_cast<int>( item->GetBrdLayerId() ) ) );
737 h = KiHashCombine( h, std::hash<std::string>{}( item->GetLayerName().ToStdString() ) );
738 h = KiHashCombine( h, std::hash<bool>{}( item->IsEnabled() ) );
739
740 for( int sub = 0; sub < item->GetSublayersCount(); ++sub )
741 {
742 h = KiHashCombine( h, std::hash<int>{}( item->GetThickness( sub ) ) );
743 h = KiHashCombine( h, std::hash<std::string>{}( item->GetMaterial( sub ).ToStdString() ) );
744 h = KiHashCombine( h, std::hash<std::string>{}( item->GetColor( sub ).ToStdString() ) );
745
746 if( item->HasEpsilonRValue() )
747 h = KiHashCombine( h, std::hash<double>{}( item->GetEpsilonR( sub ) ) );
748
749 if( item->HasLossTangentValue() )
750 h = KiHashCombine( h, std::hash<double>{}( item->GetLossTangent( sub ) ) );
751 }
752 }
753
754 return wxString::Format( wxS( "%d copper / %d dielectric layers (hash %zx)" ), copper, dielectric, h )
755 .ToStdString();
756 };
757
758 if( beforeStackup != afterStackup )
759 {
762 d.before = DIFF_VALUE::FromString( summarizeStackup( beforeStackup ) );
763 d.after = DIFF_VALUE::FromString( summarizeStackup( afterStackup ) );
764 docDeltas.push_back( std::move( d ) );
765 }
766
767 // DRC severity overrides live in the project file. Diff only fires when
768 // sibling .kicad_pro files were loaded — for plain .kicad_pcb temp blobs
769 // (git mergetool case) both sides see defaults and we never get here.
770 const std::map<int, SEVERITY>& beforeDRC = beforeDS.m_DRCSeverities;
771 const std::map<int, SEVERITY>& afterDRC = afterDS.m_DRCSeverities;
772
773 if( beforeDRC != afterDRC )
774 {
779 docDeltas.push_back( std::move( d ) );
780 }
781
782 // Net classes live in PROJECT_FILE via NET_SETTINGS. Both NET_SETTINGS
783 // instances are reached via BOARD_DESIGN_SETTINGS::m_NetSettings (a
784 // shared_ptr). After A1, NET_SETTINGS::operator== is content-aware
785 // (covers default-netclass in-place edits + per-named-class parameter
786 // edits + label / pattern / color / chain-class maps), so equality
787 // alone is enough to gate the delta. Render a count + content hash
788 // summary so two configurations with the same class count but
789 // different parameters produce distinct before / after strings.
790 auto summarizeNetSettings = []( const NET_SETTINGS& aSettings ) -> std::string
791 {
792 std::size_t h = 0;
793 auto hashCombine = [&h]( std::size_t v )
794 {
795 h = KiHashCombine( h, v );
796 };
797
798 auto hashNetclass = [&]( const NETCLASS* nc )
799 {
800 if( !nc )
801 return;
802
803 hashCombine( std::hash<std::string>{}( nc->GetName().ToStdString() ) );
804 hashCombine( static_cast<std::size_t>( nc->GetPriority() ) );
805 hashCombine( static_cast<std::size_t>( nc->GetClearanceOpt().value_or( -1 ) ) );
806 hashCombine( static_cast<std::size_t>( nc->GetTrackWidthOpt().value_or( -1 ) ) );
807 hashCombine( static_cast<std::size_t>( nc->GetViaDiameterOpt().value_or( -1 ) ) );
808 hashCombine( static_cast<std::size_t>( nc->GetViaDrillOpt().value_or( -1 ) ) );
809 hashCombine( static_cast<std::size_t>( nc->GetuViaDiameterOpt().value_or( -1 ) ) );
810 hashCombine( static_cast<std::size_t>( nc->GetuViaDrillOpt().value_or( -1 ) ) );
811 hashCombine( static_cast<std::size_t>( nc->GetDiffPairWidthOpt().value_or( -1 ) ) );
812 hashCombine( static_cast<std::size_t>( nc->GetDiffPairGapOpt().value_or( -1 ) ) );
813 hashCombine( static_cast<std::size_t>( nc->GetDiffPairViaGapOpt().value_or( -1 ) ) );
814 hashCombine( static_cast<std::size_t>( nc->GetWireWidthOpt().value_or( -1 ) ) );
815 hashCombine( static_cast<std::size_t>( nc->GetBusWidthOpt().value_or( -1 ) ) );
816 hashCombine( static_cast<std::size_t>( nc->GetLineStyleOpt().value_or( -1 ) ) );
817 hashCombine( std::hash<std::string>{}( nc->GetTuningProfile().ToStdString() ) );
818
819 // Per-netclass color overrides are part of NETCLASS::EqualsByPersistedFields;
820 // include them so a color-only edit produces a distinct rendered summary.
821 hashCombine( std::hash<std::string>{}( nc->GetSchematicColor( true ).ToCSSString().ToStdString() ) );
822 hashCombine( std::hash<std::string>{}( nc->GetPcbColor( true ).ToCSSString().ToStdString() ) );
823 };
824
825 hashNetclass( aSettings.GetDefaultNetclass().get() );
826
827 for( const auto& [name, nc] : aSettings.GetNetclasses() )
828 hashNetclass( nc.get() );
829
830 for( const auto& [netname, classes] : aSettings.GetNetclassLabelAssignments() )
831 {
832 hashCombine( std::hash<std::string>{}( netname.ToStdString() ) );
833
834 for( const wxString& c : classes )
835 hashCombine( std::hash<std::string>{}( c.ToStdString() ) );
836 }
837
838 for( const auto& [chain, className] : aSettings.GetNetChainClasses() )
839 {
840 hashCombine( std::hash<std::string>{}( chain.ToStdString() ) );
841 hashCombine( std::hash<std::string>{}( className.ToStdString() ) );
842 }
843
844 for( const auto& [chain, netclass] : aSettings.GetNetChainNetClasses() )
845 {
846 hashCombine( std::hash<std::string>{}( chain.ToStdString() ) );
847 hashCombine( std::hash<std::string>{}( netclass.ToStdString() ) );
848 }
849
850 for( const auto& [netname, color] : aSettings.GetNetColorAssignments() )
851 {
852 hashCombine( std::hash<std::string>{}( netname.ToStdString() ) );
853 hashCombine( std::hash<std::string>{}( color.ToCSSString().ToStdString() ) );
854 }
855
856 const std::size_t classCount = aSettings.GetNetclasses().size() + ( aSettings.GetDefaultNetclass() ? 1u : 0u );
857
858 return wxString::Format( wxS( "%zu netclass(es) (hash %zx)" ), classCount, h ).ToStdString();
859 };
860
861 const std::shared_ptr<NET_SETTINGS>& beforeNet = beforeDS.m_NetSettings;
862 const std::shared_ptr<NET_SETTINGS>& afterNet = afterDS.m_NetSettings;
863
864 // Diff fires only when both sides have NET_SETTINGS instances. A null
865 // pointer on either side means the BOARD wasn't loaded with a project file
866 // (e.g. plain .kicad_pcb temp blob from git mergetool); skip silently —
867 // matches DRC severities behaviour.
868 if( beforeNet && afterNet && *beforeNet != *afterNet )
869 {
872 d.before = DIFF_VALUE::FromString( summarizeNetSettings( *beforeNet ) );
873 d.after = DIFF_VALUE::FromString( summarizeNetSettings( *afterNet ) );
874 docDeltas.push_back( std::move( d ) );
875 }
876
877 // Custom DRC rules live in a sibling .kicad_dru file next to the .kicad_pcb.
878 // The file content isn't stored on BOARD; we read it from disk. For
879 // headless / temp-blob merges (git mergetool) the sibling file usually
880 // isn't present and both sides return empty strings, so no delta fires.
881 auto readSiblingDruContent = []( const BOARD* aBoard ) -> wxString
882 {
883 if( !aBoard )
884 return wxEmptyString;
885
886 wxString boardPath = aBoard->GetFileName();
887
888 if( boardPath.IsEmpty() )
889 return wxEmptyString;
890
891 wxFileName fn( boardPath );
893
894 if( !fn.FileExists() )
895 return wxEmptyString;
896
897 wxFile file( fn.GetFullPath() );
898
899 if( !file.IsOpened() )
900 return wxEmptyString;
901
902 wxString contents;
903 file.ReadAll( &contents );
904 return contents;
905 };
906
907 auto summarizeRules = []( const wxString& aContents ) -> std::string
908 {
909 if( aContents.IsEmpty() )
910 return "(no custom rules)";
911
912 std::size_t h = std::hash<std::string>{}( aContents.ToStdString() );
913 return wxString::Format( wxS( "%zu byte(s) (hash %zx)" ), aContents.size(), h ).ToStdString();
914 };
915
916 const wxString beforeRules = readSiblingDruContent( m_before );
917 const wxString afterRules = readSiblingDruContent( m_after );
918
919 if( beforeRules != afterRules )
920 {
923 d.before = DIFF_VALUE::FromString( summarizeRules( beforeRules ) );
924 d.after = DIFF_VALUE::FromString( summarizeRules( afterRules ) );
925 docDeltas.push_back( std::move( d ) );
926 }
927
928 // Footprint library table (fp-lib-table) lives in the project directory
929 // (no extension). Read content directly; the same content-comparison
930 // pattern as custom DRC rules applies.
931 auto readProjectFileNamed = []( const BOARD* aBoard, const std::string& aFileName ) -> wxString
932 {
933 if( !aBoard )
934 return wxEmptyString;
935
936 wxString boardPath = aBoard->GetFileName();
937
938 if( boardPath.IsEmpty() )
939 return wxEmptyString;
940
941 wxFileName fn( boardPath );
942 fn.SetFullName( wxString::FromUTF8( aFileName ) );
943
944 if( !fn.FileExists() )
945 return wxEmptyString;
946
947 wxFile file( fn.GetFullPath() );
948
949 if( !file.IsOpened() )
950 return wxEmptyString;
951
952 wxString contents;
953 file.ReadAll( &contents );
954 return contents;
955 };
956
957 auto summarizeTable = []( const wxString& aContents, const wxString& aLabel ) -> std::string
958 {
959 if( aContents.IsEmpty() )
960 return wxString::Format( wxS( "(no %s)" ), aLabel ).ToStdString();
961
962 std::size_t h = std::hash<std::string>{}( aContents.ToStdString() );
963 return wxString::Format( wxS( "%zu byte(s) (hash %zx)" ), aContents.size(), h ).ToStdString();
964 };
965
966 const wxString beforeFp = readProjectFileNamed( m_before, FILEEXT::FootprintLibraryTableFileName );
967 const wxString afterFp = readProjectFileNamed( m_after, FILEEXT::FootprintLibraryTableFileName );
968
969 if( beforeFp != afterFp )
970 {
973 d.before = DIFF_VALUE::FromString( summarizeTable( beforeFp, wxS( "fp-lib-table" ) ) );
974 d.after = DIFF_VALUE::FromString( summarizeTable( afterFp, wxS( "fp-lib-table" ) ) );
975 docDeltas.push_back( std::move( d ) );
976 }
977
978 const wxString beforeSym = readProjectFileNamed( m_before, FILEEXT::SymbolLibraryTableFileName );
979 const wxString afterSym = readProjectFileNamed( m_after, FILEEXT::SymbolLibraryTableFileName );
980
981 if( beforeSym != afterSym )
982 {
985 d.before = DIFF_VALUE::FromString( summarizeTable( beforeSym, wxS( "sym-lib-table" ) ) );
986 d.after = DIFF_VALUE::FromString( summarizeTable( afterSym, wxS( "sym-lib-table" ) ) );
987 docDeltas.push_back( std::move( d ) );
988 }
989
990 // Drawing sheet file path lives in PROJECT_FILE::m_BoardDrawingSheetFile.
991 // Resolved to absolute on load; for diff purposes the stored path is
992 // what serializes back, so compare those strings directly.
993 auto boardDrawingSheet = []( const BOARD* aBoard ) -> wxString
994 {
995 if( !aBoard || !aBoard->GetProject() )
996 return wxEmptyString;
997
999 };
1000
1001 const wxString beforeSheet = boardDrawingSheet( m_before );
1002 const wxString afterSheet = boardDrawingSheet( m_after );
1003
1004 if( beforeSheet != afterSheet )
1005 {
1008 d.before = DIFF_VALUE::FromString( beforeSheet );
1009 d.after = DIFF_VALUE::FromString( afterSheet );
1010 docDeltas.push_back( std::move( d ) );
1011 }
1012
1013 if( !docDeltas.empty() )
1014 {
1015 ITEM_CHANGE c;
1016 c.id = KIID_PATH(); // empty path = document-scope sentinel
1017 c.typeName = wxS( "BOARD" );
1019 c.bbox = BOX2I(); // document-scoped, no spatial location
1020 c.properties = std::move( docDeltas );
1021 result.changes.push_back( std::move( c ) );
1022 }
1023
1024 sortChanges( result.changes );
1025
1026 if( m_options.progress )
1027 m_options.progress( 1.0 );
1028
1029 return result;
1030}
1031
1032} // namespace KICAD_DIFF
const char * name
std::set< BOARD_ITEM *, CompareByUuid > BOARD_ITEM_SET
Set of BOARD_ITEMs ordered by UUID.
Definition board.h:394
static void hashCombine(std::size_t &aSeed, std::size_t aValue)
@ BS_ITEM_TYPE_COPPER
@ BS_ITEM_TYPE_DIELECTRIC
BOX2< VECTOR2I > BOX2I
Definition box2.h:914
Container for design settings for a BOARD object.
std::shared_ptr< NET_SETTINGS > m_NetSettings
std::map< int, SEVERITY > m_DRCSeverities
int GetBoardThickness() const
The full thickness of the board including copper and masks.
BOARD_STACKUP & GetStackupDescriptor()
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
BOARD_ITEM_CONTAINER * GetParent() const
Definition board_item.h:266
Manage one layer needed to make a physical board.
Manage layers needed to make a physical board.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:410
const FOOTPRINTS & Footprints() const
Definition board.h:464
const wxString & GetFileName() const
Definition board.h:453
PROJECT * GetProject() const
Definition board.h:775
BOARD_ITEM * ResolveItem(const KIID &aID, bool aAllowNullptrReturn=false) const
Definition board.cpp:2143
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
virtual VECTOR2I GetPosition() const
Definition eda_item.h:348
virtual const BOX2I GetBoundingBox() const
Return the orthogonal bounding box of this object for display purposes.
Definition eda_item.cpp:270
const KIID m_Uuid
Definition eda_item.h:599
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
ZONES & Zones()
Definition footprint.h:411
std::deque< PAD * > & Pads()
Definition footprint.h:405
void GetFields(std::vector< PCB_FIELD * > &aVector, bool aVisibleOnly) const
Populate a std::vector with PCB_TEXTs.
DRAWINGS & GraphicalItems()
Definition footprint.h:408
static DIFF_VALUE FromInt(int aValue)
static DIFF_VALUE FromString(const wxString &aValue)
std::vector< std::vector< std::vector< VECTOR2I > > > PolygonSet
static DIFF_VALUE FromPolygonSet(PolygonSet aValue)
Reconciles item identity across two snapshots of the same document.
RECONCILIATION Reconcile(const std::vector< ITEM_DESCRIPTOR > &aA, const std::vector< ITEM_DESCRIPTOR > &aB) const
const BOARD * m_after
Definition pcb_differ.h:97
static void sortChanges(std::vector< ITEM_CHANGE > &aChanges)
Stable, deterministic sort of ITEM_CHANGEs (by id, then typeName, then kind).
const BOARD * m_before
Definition pcb_differ.h:96
PCB_DIFFER(const BOARD *aBefore, const BOARD *aAfter, const wxString &aPath=wxEmptyString)
std::vector< ITEM_CHANGE > diffFootprintChildren(const FOOTPRINT *aBefore, const FOOTPRINT *aAfter) const
Construct a child-level diff for nested items inside a footprint pair.
DOCUMENT_DIFF Diff() override
Produce a DOCUMENT_DIFF of the inputs the concrete differ was constructed with.
static std::optional< wxString > itemRefdes(const BOARD_ITEM *aItem)
Extract a presentation label: footprint refdes, or routing net name for tracks/vias.
std::vector< PROPERTY_DELTA > diffProperties(const BOARD_ITEM *aBefore, const BOARD_ITEM *aAfter) const
Compute property deltas between two items of the same dynamic type.
static wxString itemTypeName(const BOARD_ITEM *aItem)
Convert the dynamic class string for an item into the type name used in diffs.
ITEM_DESCRIPTOR makeDescriptor(const BOARD_ITEM *aItem) const
Build the ITEM_DESCRIPTOR for the reconciler from a BOARD_ITEM.
virtual wxString GetClass() const =0
Return the class name.
Definition kiid.h:46
static KIID FromDeterministicString(const wxString &aName)
Build a deterministic UUID from an arbitrary name string.
Definition kiid.cpp:325
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:43
NET_SETTINGS stores various net-related settings in a project context.
Definition pad.h:61
wxString m_BoardDrawingSheetFile
PcbNew params.
virtual PROJECT_FILE & GetProjectFile() const
Definition project.h:201
Represent a set of closed polygons.
Handle a list of polygons defining a copper zone.
Definition zone.h:70
static bool empty(const wxTextEntryBase *aCtrl)
static std::string ToStdString(const wxString &aStr)
static const std::string SymbolLibraryTableFileName
static const std::string FootprintLibraryTableFileName
static const std::string DesignRulesFileExtension
std::size_t KiHashCombine(std::size_t aSeed, std::size_t aValue)
Fold aValue into the running hash aSeed using the well-known Boost hash_combine mixing step.
Definition hashtables.h:42
wxString LayerName(int aLayer)
Returns the default display name for a given layer.
Definition layer_id.cpp:31
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
const wxString DOC_PROP_SYM_LIB_TABLE
const wxString DOC_PROP_BOARD_THICKNESS
std::vector< PROPERTY_DELTA > DiffItemProperties(const INSPECTABLE *aBefore, const INSPECTABLE *aAfter)
Enumerate the property deltas between two items of the same dynamic type.
const wxString DOC_PROP_NET_CLASSES
std::string SummarizeSeverities(const SeverityMap &aMap)
Format a severity-override map (DRC or ERC, keyed by error code, value is a SEVERITY enum) as a short...
std::vector< PROPERTY_DELTA > ItemProperties(const INSPECTABLE *aItem, bool aAsAfter)
List one item's properties as one-sided deltas for an added or removed item.
static bool pcbDiffPropertyIsNoise(const wxString &aName, bool aInsideFootprint)
const wxString DOC_PROP_CUSTOM_RULES
const wxString DOC_PROP_FP_LIB_TABLE
void AppendPaperDeltas(std::vector< PROPERTY_DELTA > &aDeltas, const PAGE_INFO &aBefore, const PAGE_INFO &aAfter)
Append DOC_PROP_PAGE_FORMAT and/or DOC_PROP_PAGE_ORIENTATION deltas to aDeltas when the two PAGE_INFO...
BOARD_ITEM * FindPcbDiffItem(const BOARD *aBoard, const KIID &aId)
Resolve an actual UUID or a footprint field's diff ID on a board.
const wxString DOC_PROP_LAYER_STACKUP
const wxString DOC_PROP_DRAWING_SHEET
static std::vector< PROPERTY_DELTA > pcbAddedRemovedProperties(const BOARD_ITEM *aItem, bool aAsAfter)
const wxString DOC_PROP_DRC_SEVERITIES
KIID PcbDiffItemId(const EDA_ITEM &aItem)
Fields match by parent footprint and untranslated name, including after a library refresh.
static bool pcbLibraryMetadataNoise(const wxString &aName)
The full set of changes between two parsed documents of one type.
One change record on a single item.
std::vector< PROPERTY_DELTA > properties
std::optional< wxString > refdes
std::vector< ITEM_CHANGE > children
Descriptor used by the identity reconciler to compare items across two documents.
std::vector< std::pair< wxString, std::string > > keyProps
Single (name, before, after) triple for one mutated property on an item.
Maps every item in document A to either a peer in document B or to "only-in-A", and vice versa.
std::set< KIID_PATH > aOnly
std::vector< KIID_PATH > duplicatesA
std::map< KIID_PATH, KIID_PATH > aToB
std::vector< KIID_PATH > duplicatesB
std::set< KIID_PATH > bOnly
const SHAPE_LINE_CHAIN chain
wxString result
Test unit parsing edge cases and error handling.
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:83
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:84
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:78
Definition of file extensions used in Kicad.