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