KiCad PCB EDA Suite
Loading...
Searching...
No Matches
sch_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 "sch_differ.h"
22#include "sch_diff_utils.h"
23
26
27#include <schematic.h>
28#include <schematic_settings.h>
29#include <erc/erc_settings.h>
30#include <sch_item.h>
31#include <sch_screen.h>
32#include <sch_sheet.h>
33#include <sch_sheet_path.h>
34#include <sch_symbol.h>
35#include <properties/property.h>
37
38#include <algorithm>
39
40
41namespace KICAD_DIFF
42{
43
44SCH_DIFFER::SCH_DIFFER( const SCHEMATIC* aBefore, const SCHEMATIC* aAfter,
45 const wxString& aPath ) :
46 m_before( aBefore ),
47 m_after( aAfter ),
48 m_path( aPath )
49{}
50
51
52SCH_DIFFER::~SCH_DIFFER() = default;
53
54
55void SCH_DIFFER::SetScope( const KIID_PATH& aBeforeScope, const KIID_PATH& aAfterScope )
56{
57 m_scopeBefore = aBeforeScope;
58 m_scopeAfter = aAfterScope;
59}
60
61
62wxString SCH_DIFFER::itemTypeName( const SCH_ITEM* aItem )
63{
64 if( !aItem )
65 return wxEmptyString;
66
67 return aItem->GetClass();
68}
69
70
71std::optional<wxString> SCH_DIFFER::itemRefdes( const SCH_ITEM* aItem,
72 const SCH_SHEET_PATH* aPath )
73{
74 if( !aPath )
75 return std::nullopt;
76
77 if( auto sym = dynamic_cast<const SCH_SYMBOL*>( aItem ) )
78 return sym->GetRef( aPath );
79
80 return std::nullopt;
81}
82
83
84void SCH_DIFFER::walk( const SCHEMATIC* aSchematic, std::vector<WalkedItem>& aOut,
85 std::vector<std::unique_ptr<SCH_SHEET_PATH>>& aStorage, const KIID_PATH& aScope ) const
86{
87 // Retain one owned SCH_SHEET_PATH per sheet and alias it across that
88 // sheet's items. The per-sheet visitor materializes the copy once; the
89 // item visitor reuses the stored pointer, matching the original loop.
90 SCH_SHEET_PATH* storedPath = nullptr;
91
93 aSchematic,
94 [&]( const SCH_SHEET_PATH& aPath )
95 {
96 aStorage.push_back( std::make_unique<SCH_SHEET_PATH>( aPath ) );
97 storedPath = aStorage.back().get();
98 },
99 [&]( SCH_ITEM* aItem, const SCH_SHEET_PATH&, const KIID_PATH& aKiidPath )
100 {
101 WalkedItem w;
102 w.id = aKiidPath;
103 w.item = aItem;
104 w.sheetPath = storedPath;
105 aOut.push_back( std::move( w ) );
106 },
107 aScope );
108}
109
110
111namespace
112{
113
117BOX2I bboxOnSheet( const SCHEMATIC* aSch, const SCH_ITEM* aItem,
118 const SCH_SHEET_PATH* aPath )
119{
120 SHEET_SCOPE scope( aSch, aPath );
121 return aItem->GetBoundingBox();
122}
123
124} // anonymous namespace
125
126
127std::vector<PROPERTY_DELTA> SCH_DIFFER::diffProperties( const SCH_ITEM* aBefore,
128 const SCH_ITEM* aAfter,
129 const SCH_SHEET_PATH* aBeforePath,
130 const SCH_SHEET_PATH* aAfterPath ) const
131{
132 if( !aBefore || !aAfter || typeid( *aBefore ) != typeid( *aAfter ) )
133 return {};
134
135 SHEET_SCOPE beforeScope( m_before, aBeforePath );
136 SHEET_SCOPE afterScope( m_after, aAfterPath );
137
138 auto deltas = DiffItemProperties( aBefore, aAfter );
139
140 // In scoped mode drop properties whose value comes from a per-instance
141 // override. Comparing them across shared sheet instances flags every
142 // symbol as Modified even when the stored content is identical.
143 const bool scoped = !m_scopeBefore.empty() || !m_scopeAfter.empty();
144
145 if( scoped )
146 {
147 auto perInstance = []( const wxString& aName )
148 {
149 return aName == wxS( "Reference" ) || aName == wxS( "Unit" );
150 };
151
152 deltas.erase( std::remove_if( deltas.begin(), deltas.end(),
153 [&]( const PROPERTY_DELTA& d )
154 {
155 return perInstance( d.name );
156 } ),
157 deltas.end() );
158 }
159
160 return deltas;
161}
162
163
164void SCH_DIFFER::sortChanges( std::vector<ITEM_CHANGE>& aChanges )
165{
166 std::sort( aChanges.begin(), aChanges.end(),
167 []( const ITEM_CHANGE& aL, const ITEM_CHANGE& aR )
168 {
169 if( aL.id < aR.id ) return true;
170 if( aR.id < aL.id ) return false;
171
172 if( aL.typeName != aR.typeName )
173 return aL.typeName < aR.typeName;
174
175 return static_cast<int>( aL.kind ) < static_cast<int>( aR.kind );
176 } );
177}
178
179
181{
183 result.path = m_path;
184 result.docType = wxS( "kicad_sch" );
185
186 if( !m_before || !m_after )
187 return result;
188
189 // Walk both schematics through their entire sheet hierarchy.
190 std::vector<WalkedItem> beforeWalk;
191 std::vector<WalkedItem> afterWalk;
192 std::vector<std::unique_ptr<SCH_SHEET_PATH>> beforePaths;
193 std::vector<std::unique_ptr<SCH_SHEET_PATH>> afterPaths;
194
195 walk( m_before, beforeWalk, beforePaths, m_scopeBefore );
196 walk( m_after, afterWalk, afterPaths, m_scopeAfter );
197
198 if( m_options.progress )
199 m_options.progress( 0.2 );
200
201 // Build reconciler descriptors and a lookup back to the live items.
202 std::vector<ITEM_DESCRIPTOR> beforeDesc;
203 std::vector<ITEM_DESCRIPTOR> afterDesc;
204 std::map<KIID_PATH, const WalkedItem*> beforeMap;
205 std::map<KIID_PATH, const WalkedItem*> afterMap;
206
207 auto makeDescriptor = []( const WalkedItem& aW, const SCHEMATIC* aSch ) -> ITEM_DESCRIPTOR
208 {
210 d.id = aW.id;
211 d.type = itemTypeName( aW.item );
212 d.position = aW.item->GetPosition();
213 d.bbox = bboxOnSheet( aSch, aW.item, aW.sheetPath );
214
215 if( auto sym = dynamic_cast<const SCH_SYMBOL*>( aW.item ) )
216 {
217 d.keyProps.emplace_back( wxS( "lib_id" ),
218 std::string( sym->GetLibId().Format().c_str() ) );
219 d.keyProps.emplace_back( wxS( "reference" ),
220 sym->GetRef( aW.sheetPath ).ToStdString() );
221 }
222
223 return d;
224 };
225
226 for( const WalkedItem& w : beforeWalk )
227 {
228 beforeDesc.push_back( makeDescriptor( w, m_before ) );
229 beforeMap[w.id] = &w;
230 }
231
232 for( const WalkedItem& w : afterWalk )
233 {
234 afterDesc.push_back( makeDescriptor( w, m_after ) );
235 afterMap[w.id] = &w;
236 }
237
238 IDENTITY_RECONCILER reconciler( m_options.identity );
239 RECONCILIATION recon = reconciler.Reconcile( beforeDesc, afterDesc );
240
241 if( m_options.progress )
242 m_options.progress( 0.5 );
243
244 // Duplicate KIID_PATH entries inside either schematic.
245 for( const KIID_PATH& dup : recon.duplicatesA )
246 {
247 ITEM_CHANGE c;
248 c.id = dup;
249 c.typeName = wxS( "SCH_ITEM" );
251 result.changes.push_back( std::move( c ) );
252 }
253
254 for( const KIID_PATH& dup : recon.duplicatesB )
255 {
256 if( std::find_if( result.changes.begin(), result.changes.end(),
257 [&]( const ITEM_CHANGE& aC )
258 { return aC.id == dup && aC.kind == CHANGE_KIND::DUPLICATE_UUID; } )
259 != result.changes.end() )
260 {
261 continue;
262 }
263
264 ITEM_CHANGE c;
265 c.id = dup;
266 c.typeName = wxS( "SCH_ITEM" );
268 result.changes.push_back( std::move( c ) );
269 }
270
271 // Matched pairs: emit MODIFIED records when any delta surfaces.
272 for( const auto& [idA, idB] : recon.aToB )
273 {
274 auto itA = beforeMap.find( idA );
275 auto itB = afterMap.find( idB );
276 const WalkedItem* a = itA == beforeMap.end() ? nullptr : itA->second;
277 const WalkedItem* b = itB == afterMap.end() ? nullptr : itB->second;
278
279 if( !a || !b )
280 continue;
281
282 std::vector<PROPERTY_DELTA> propDeltas;
283
284 if( m_options.deepCompare )
285 propDeltas = diffProperties( a->item, b->item, a->sheetPath, b->sheetPath );
286
287 bool semanticallyEqual = ( *a->item == *b->item );
288 const bool scoped = !m_scopeBefore.empty() || !m_scopeAfter.empty();
289
290 if( propDeltas.empty() && ( semanticallyEqual || scoped ) )
291 continue;
292
293 ITEM_CHANGE c;
294 c.id = idA;
295 c.typeName = itemTypeName( a->item );
297 c.bbox = bboxOnSheet( m_after, b->item, b->sheetPath );
298 c.refdes = itemRefdes( b->item, b->sheetPath );
299 c.properties = std::move( propDeltas );
300 result.changes.push_back( std::move( c ) );
301 }
302
303 for( const KIID_PATH& idA : recon.aOnly )
304 {
305 auto it = beforeMap.find( idA );
306
307 if( it == beforeMap.end() || !it->second )
308 continue;
309
310 const WalkedItem* a = it->second;
311 ITEM_CHANGE c;
312 c.id = idA;
313 c.typeName = itemTypeName( a->item );
315 c.bbox = bboxOnSheet( m_before, a->item, a->sheetPath );
316 c.refdes = itemRefdes( a->item, a->sheetPath );
317
318 {
319 SHEET_SCOPE scope( m_before, a->sheetPath );
320 c.properties = ItemProperties( a->item, /*aAsAfter=*/false );
321 }
322
323 result.changes.push_back( std::move( c ) );
324 }
325
326 for( const KIID_PATH& idB : recon.bOnly )
327 {
328 auto it = afterMap.find( idB );
329
330 if( it == afterMap.end() || !it->second )
331 continue;
332
333 const WalkedItem* b = it->second;
334
335 ITEM_CHANGE c;
336 c.id = idB;
337 c.typeName = itemTypeName( b->item );
339 c.bbox = bboxOnSheet( m_after, b->item, b->sheetPath );
340 c.refdes = itemRefdes( b->item, b->sheetPath );
341
342 {
343 SHEET_SCOPE scope( m_after, b->sheetPath );
344 c.properties = ItemProperties( b->item, /*aAsAfter=*/true );
345 }
346
347 result.changes.push_back( std::move( c ) );
348 }
349
350 // Page settings live on the root SCH_SCREEN, not on any walked item, so
351 // the per-item loop above can't see a change to them. Emit a synthetic
352 // ITEM_CHANGE with empty KIID_PATH so the planner can resolve it.
353 std::vector<PROPERTY_DELTA> docDeltas;
354
355 const SCH_SCREEN* beforeRoot = m_before ? m_before->RootScreen() : nullptr;
356 const SCH_SCREEN* afterRoot = m_after ? m_after->RootScreen() : nullptr;
357
358 if( beforeRoot && afterRoot )
359 AppendPaperDeltas( docDeltas, beforeRoot->GetPageSettings(),
360 afterRoot->GetPageSettings() );
361
362 // ERC severity overrides live in the project file. Diff only fires when
363 // sibling .kicad_pro files were loaded — for plain .kicad_sch temp blobs
364 // both sides see defaults and we never get here. Be defensive about
365 // ErcSettings() — it wxASSERTs on a null project; under QA the asserter
366 // throws.
367 auto ercSeverities =
368 []( const SCHEMATIC* aSch ) -> const std::map<int, SEVERITY>*
369 {
370 if( !aSch || !aSch->IsValid() )
371 return nullptr;
372
373 return &aSch->ErcSettings().m_ERCSeverities;
374 };
375
376 const std::map<int, SEVERITY>* beforeERC = ercSeverities( m_before );
377 const std::map<int, SEVERITY>* afterERC = ercSeverities( m_after );
378
379 if( beforeERC && afterERC && *beforeERC != *afterERC )
380 {
385 docDeltas.push_back( std::move( d ) );
386 }
387
388 // Drawing sheet file path. Lives on SCHEMATIC_SETTINGS::m_SchDrawingSheet
389 // FileName (which lives inside the project file). Settings() wxASSERTs
390 // on a null project; only access when both sides loaded a sibling
391 // .kicad_pro.
392 auto schDrawingSheet = []( const SCHEMATIC* aSch ) -> wxString
393 {
394 if( !aSch || !aSch->IsValid() )
395 return wxEmptyString;
396
397 return aSch->Settings().m_SchDrawingSheetFileName;
398 };
399
400 const wxString beforeSheet = schDrawingSheet( m_before );
401 const wxString afterSheet = schDrawingSheet( m_after );
402
403 if( beforeSheet != afterSheet )
404 {
407 d.before = DIFF_VALUE::FromString( beforeSheet );
408 d.after = DIFF_VALUE::FromString( afterSheet );
409 docDeltas.push_back( std::move( d ) );
410 }
411
412 if( !docDeltas.empty() )
413 {
414 ITEM_CHANGE c;
415 c.id = KIID_PATH();
416 c.typeName = wxS( "SCHEMATIC" );
418 c.bbox = BOX2I();
419 c.properties = std::move( docDeltas );
420 result.changes.push_back( std::move( c ) );
421 }
422
423 // Per-sheet paper settings: sub-sheets in a hierarchy can carry their
424 // own paper size / orientation distinct from the root. Dedup by screen
425 // pointer so re-instantiated sub-sheets (multiple SCH_SHEET_PATHs hit
426 // the same underlying SCH_SCREEN) produce one delta, not N — otherwise
427 // the user would see duplicate rows and the applier could resolve them
428 // inconsistently.
429 auto sheetPaperByScreen =
430 []( const SCHEMATIC* aSch )
431 -> std::map<const SCH_SCREEN*, std::pair<KIID_PATH, const PAGE_INFO*>>
432 {
433 std::map<const SCH_SCREEN*, std::pair<KIID_PATH, const PAGE_INFO*>> out;
434
435 if( !aSch || !aSch->IsValid() )
436 return out;
437
438 for( const SCH_SHEET_PATH& path : aSch->BuildSheetListSortedByPageNumbers() )
439 {
440 if( const SCH_SCREEN* screen = path.LastScreen() )
441 out.try_emplace( screen, path.Path(), &screen->GetPageSettings() );
442 }
443
444 return out;
445 };
446
447 const SCH_SCREEN* const beforeRootScreen = beforeRoot;
448
449 // Build the after-side path->screen map once. We CANNOT store a pointer
450 // into a temporary SCH_SHEET_LIST from BuildSheetListSortedByPageNumbers
451 // — the list goes out of scope before we'd dereference. Storing the raw
452 // SCH_SCREEN* is safe because the screens are owned by the SCHEMATIC.
453 std::map<KIID_PATH, const SCH_SCREEN*> afterScreensByPath;
454
455 if( m_after && m_after->IsValid() )
456 {
457 for( const SCH_SHEET_PATH& p : m_after->BuildSheetListSortedByPageNumbers() )
458 {
459 if( const SCH_SCREEN* screen = p.LastScreen() )
460 afterScreensByPath.try_emplace( p.Path(), screen );
461 }
462 }
463
464 for( const auto& [beforeScreen, entry] : sheetPaperByScreen( m_before ) )
465 {
466 // Root paper is covered by the empty-KIID_PATH delta above; skip
467 // the root screen here so we don't double-report.
468 if( beforeScreen == beforeRootScreen )
469 continue;
470
471 const KIID_PATH& sheetPath = entry.first;
472 const PAGE_INFO& beforePaper = *entry.second;
473
474 // Match by KIID_PATH. Topology changes (renamed / re-parented
475 // sheets) where the path no longer matches are out of scope; the
476 // differ doesn't model sheet structure changes, only content on
477 // common paths.
478 auto afterIt = afterScreensByPath.find( sheetPath );
479
480 if( afterIt == afterScreensByPath.end() || !afterIt->second )
481 continue;
482
483 std::vector<PROPERTY_DELTA> sheetDeltas;
484 AppendPaperDeltas( sheetDeltas, beforePaper,
485 afterIt->second->GetPageSettings() );
486
487 if( !sheetDeltas.empty() )
488 {
489 ITEM_CHANGE c;
490 c.id = sheetPath;
491 // Append the sentinel KIID so this ID doesn't collide with the
492 // SCH_SHEET symbol that lives at the same `sheetPath`. The
493 // applier checks the last KIID to route to the screen branch.
494 c.id.push_back( SchScreenSentinelKiid() );
495 c.typeName = wxS( "SCH_SCREEN" );
497 c.bbox = BOX2I();
498 c.properties = std::move( sheetDeltas );
499 result.changes.push_back( std::move( c ) );
500 }
501 }
502
503 sortChanges( result.changes );
504
505 if( m_options.progress )
506 m_options.progress( 1.0 );
507
508 return result;
509}
510
511} // namespace KICAD_DIFF
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
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
static DIFF_VALUE FromString(const wxString &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 SCHEMATIC * m_after
Definition sch_differ.h:108
static std::optional< wxString > itemRefdes(const SCH_ITEM *aItem, const SCH_SHEET_PATH *aPath)
Refdes if the item is a symbol; nullopt otherwise.
static wxString itemTypeName(const SCH_ITEM *aItem)
Type name for an SCH_ITEM (used in diff records).
void SetScope(const KIID_PATH &aBeforeScope, const KIID_PATH &aAfterScope)
Restrict the diff to one sheet on each side.
const SCHEMATIC * m_before
Definition sch_differ.h:107
SCH_DIFFER(const SCHEMATIC *aBefore, const SCHEMATIC *aAfter, const wxString &aPath=wxEmptyString)
static void sortChanges(std::vector< ITEM_CHANGE > &aChanges)
std::vector< PROPERTY_DELTA > diffProperties(const SCH_ITEM *aBefore, const SCH_ITEM *aAfter, const SCH_SHEET_PATH *aBeforePath, const SCH_SHEET_PATH *aAfterPath) const
Property-level delta via PROPERTY_MANAGER.
DOCUMENT_DIFF Diff() override
Produce a DOCUMENT_DIFF of the inputs the concrete differ was constructed with.
void walk(const SCHEMATIC *aSchematic, std::vector< WalkedItem > &aOut, std::vector< std::unique_ptr< SCH_SHEET_PATH > > &aStorage, const KIID_PATH &aScope={}) const
Walk a schematic and produce a flat list of (KIID_PATH, item) tuples.
RAII guard that temporarily swaps SCHEMATIC::CurrentSheet to a given path for the duration of a scope...
Describe the page size and margins of a paper page on which to eventually print or plot.
Definition page_info.h:75
Holds all the data relating to one schematic.
Definition schematic.h:90
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:162
wxString GetClass() const override
Return the class name.
Definition sch_item.h:172
const PAGE_INFO & GetPageSettings() const
Definition sch_screen.h:137
Handle access to a stack of flattened SCH_SHEET objects by way of a path for creating a flattened sch...
Schematic symbol object.
Definition sch_symbol.h:69
const wxString DOC_PROP_ERC_SEVERITIES
const KIID & SchScreenSentinelKiid()
Sentinel KIID appended to a sheet's KIID_PATH to mark a per-sheet SCH_SCREEN resolution (page format ...
std::vector< PROPERTY_DELTA > DiffItemProperties(const INSPECTABLE *aBefore, const INSPECTABLE *aAfter)
Enumerate the property deltas between two items of the same dynamic type.
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.
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_DRAWING_SHEET
void WalkSchematic(const SCHEMATIC *aSchematic, SHEET_VISITOR &&aSheetVisitor, ITEM_VISITOR &&aItemVisitor, const KIID_PATH &aScope={})
Visit every SCH_ITEM in every sheet of aSchematic in page-number order.
Definition of the SCH_SHEET_PATH and SCH_SHEET_LIST classes for Eeschema.
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
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
std::string path
wxString result
Test unit parsing edge cases and error handling.