KiCad PCB EDA Suite
Loading...
Searching...
No Matches
pcb_merge_applier.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_merge_applier.h"
22#include "applier_helpers.h"
23#include "pcb_differ.h"
24
25#include <board.h>
27#include <board_item.h>
29#include <project.h>
36
37#include <wx/file.h>
38#include <wx/filename.h>
40#include <footprint.h>
41#include <pad.h>
42#include <pcb_field.h>
43#include <zone.h>
44
45#include <map>
46#include <set>
47
48
49namespace KICAD_DIFF
50{
51
52PCB_MERGE_APPLIER::PCB_MERGE_APPLIER( const BOARD* aAncestor, const BOARD* aOurs,
53 const BOARD* aTheirs, MERGE_PLAN aPlan ) :
54 m_ancestor( aAncestor ),
55 m_ours( aOurs ),
56 m_theirs( aTheirs ),
57 m_plan( std::move( aPlan ) )
58{}
59
60
61const BOARD_ITEM* PCB_MERGE_APPLIER::findItem( const BOARD* aBoard, const KIID& aId ) const
62{
63 return FindPcbDiffItem( aBoard, aId );
64}
65
66
67BOARD_ITEM* PCB_MERGE_APPLIER::cloneInto( BOARD* aTarget, const BOARD_ITEM* aSource ) const
68{
69 if( !aSource || !aTarget )
70 return nullptr;
71
72 std::unique_ptr<EDA_ITEM> cloned( aSource->Clone() );
73
74 if( !cloned )
75 return nullptr;
76
77 auto* boardClone = dynamic_cast<BOARD_ITEM*>( cloned.get() );
78
79 if( !boardClone )
80 return nullptr;
81
82 cloned.release();
83
84 // aTarget is the transient offline BOARD that Apply() builds and serializes to
85 // disk; it is never the live editor document. Adding directly rather than
86 // through BOARD_COMMIT is deliberate: there is no editor frame, undo stack, or
87 // VIEW to keep in sync, and routing through a commit would be wrong here.
88 aTarget->Add( boardClone, ADD_MODE::APPEND );
89 return boardClone;
90}
91
92
94 BOARD_ITEM* aTarget,
95 const std::vector<PROPERTY_RESOLUTION>& aProps,
96 const BOARD_ITEM* aOurs,
97 const BOARD_ITEM* aTheirs,
98 const BOARD_ITEM* aAncestor )
99{
100 PROPERTY_APPLY_COUNTS counts =
101 ApplyPropertyResolutions( aTarget, aProps, aOurs, aTheirs, aAncestor );
102
103 m_report.propertiesApplied += counts.applied;
104 m_report.propertiesFailed += counts.failed;
105 return counts.applied;
106}
107
108
109std::unique_ptr<BOARD> PCB_MERGE_APPLIER::Apply()
110{
111 if( !m_ours && !m_theirs && !m_ancestor )
112 return nullptr;
113
114 m_report = {};
115 m_report.requiresZoneRefill = m_plan.requiresZoneRefill;
116 m_report.requiresConnectivityRebuild = m_plan.requiresConnectivityRebuild;
117
118 auto result = std::make_unique<BOARD>();
119
120 // Index plan actions by item id so we can decide per item what to do.
121 std::map<KIID_PATH, const ITEM_RESOLUTION*> actionsById;
122
123 for( const ITEM_RESOLUTION& r : m_plan.actions )
124 actionsById[r.id] = &r;
125
126 // Read a file in the BOARD's project directory. @p aIsFullName is true
127 // for files whose name (no extension) lives next to the .kicad_pcb;
128 // false to derive a sibling by extension. Shared between the whole-side
129 // divergence-staging path and the per-property MERGE_PROPS branch.
130 auto readProjectSiblingFile =
131 []( const BOARD* aBoard, const wxString& aName, bool aIsFullName ) -> wxString
132 {
133 if( !aBoard )
134 return wxEmptyString;
135
136 wxString boardPath = aBoard->GetFileName();
137
138 if( boardPath.IsEmpty() )
139 return wxEmptyString;
140
141 wxFileName fn( boardPath );
142
143 if( aIsFullName )
144 fn.SetFullName( aName );
145 else
146 fn.SetExt( aName );
147
148 if( !fn.FileExists() )
149 return wxEmptyString;
150
151 wxFile file( fn.GetFullPath() );
152
153 if( !file.IsOpened() )
154 return wxEmptyString;
155
156 wxString contents;
157 file.ReadAll( &contents );
158 return contents;
159 };
160
161 auto readSiblingRules =
162 [&]( const BOARD* aBoard ) -> wxString
163 {
164 return readProjectSiblingFile( aBoard, wxString::FromUTF8( FILEEXT::DesignRulesFileExtension ),
165 false );
166 };
167
168 auto readFpLibTable =
169 [&]( const BOARD* aBoard ) -> wxString
170 {
171 return readProjectSiblingFile( aBoard, wxString::FromUTF8( FILEEXT::FootprintLibraryTableFileName ),
172 true );
173 };
174
175 auto readSymLibTable =
176 [&]( const BOARD* aBoard ) -> wxString
177 {
178 return readProjectSiblingFile( aBoard, wxString::FromUTF8( FILEEXT::SymbolLibraryTableFileName ),
179 true );
180 };
181
182 // Document-level settings — paper format, board thickness, design
183 // settings. PCB_DIFFER emits a synthetic ITEM_CHANGE with an empty
184 // KIID_PATH to capture changes here. Default: copy from ancestor (or
185 // ours if no ancestor); the engine's TAKE_OURS / TAKE_THEIRS / TAKE_
186 // ANCESTOR resolution overrides that.
187 {
188 const KIID_PATH docPath; // empty path = document sentinel
189 const ITEM_RESOLUTION* docRes = nullptr;
190 auto docIt = actionsById.find( docPath );
191
192 if( docIt != actionsById.end() )
193 docRes = docIt->second;
194
195 const BOARD* settingsSrc = m_ancestor ? m_ancestor : m_ours;
196
197 if( docRes )
198 {
199 switch( docRes->kind )
200 {
201 case ITEM_RES::TAKE_OURS: settingsSrc = m_ours; break;
202 case ITEM_RES::TAKE_THEIRS: settingsSrc = m_theirs; break;
203 case ITEM_RES::TAKE_ANCESTOR: settingsSrc = m_ancestor; break;
204 default: break;
205 }
206 }
207
208 // Break the shared_ptr<NET_SETTINGS> alias that
209 // BOARD_DESIGN_SETTINGS::CopyFrom installs when SetDesignSettings
210 // runs (m_NetSettings = aOther.m_NetSettings copies the pointer).
211 // Subsequent CopyFrom calls into result's NET_SETTINGS would
212 // otherwise mutate the chosen side's settings too.
213 auto detachNetSettingsFor =
214 []( BOARD* aBoard )
215 {
216 if( !aBoard )
217 return;
218
219 aBoard->GetDesignSettings().m_NetSettings = std::make_shared<NET_SETTINGS>( nullptr, "" );
220 };
221
222 // Copy a chosen side's net settings into result without aliasing the
223 // shared_ptr (which would couple the merged board's NET_SETTINGS to
224 // the source's lifetime and -- because NESTED_SETTINGS has a parent
225 // linkage -- to the source project file's m_nested_settings map).
226 auto adoptNetSettings =
227 [&]( const BOARD* aSource )
228 {
229 if( !aSource || !aSource->GetDesignSettings().m_NetSettings
230 || !result->GetDesignSettings().m_NetSettings )
231 {
232 return;
233 }
234
235 result->GetDesignSettings().m_NetSettings->CopyFrom( *aSource->GetDesignSettings().m_NetSettings );
236 };
237
238 // Layer names and types live on BOARD, not in the design settings
239 auto adoptLayers =
240 [&]( const BOARD* aSource )
241 {
242 result->SetEnabledLayers( aSource->GetEnabledLayers() );
243
244 for( PCB_LAYER_ID layer : aSource->GetEnabledLayers().Seq() )
245 {
246 const wxString name = aSource->GetLayerName( layer );
247
248 result->SetLayerType( layer, aSource->GetLayerType( layer ) );
249 result->SetLayerName( layer, name == BOARD::GetStandardLayerName( layer ) ? wxString()
250 : name );
251 }
252 };
253
254 if( settingsSrc )
255 {
256 result->SetPageSettings( settingsSrc->GetPageSettings() );
257 result->SetTitleBlock( settingsSrc->GetTitleBlock() );
258 result->SetDesignSettings( settingsSrc->GetDesignSettings() );
259 result->SetPlotOptions( settingsSrc->GetPlotOptions() );
260 result->SetLegacyTeardrops( settingsSrc->LegacyTeardrops() );
261 result->SetProperties( settingsSrc->GetProperties() );
262 result->SetVariantNames( settingsSrc->GetVariantNames() );
263
264 for( const wxString& variant : settingsSrc->GetVariantNames() )
265 result->SetVariantDescription( variant, settingsSrc->GetVariantDescription( variant ) );
266
267 adoptLayers( settingsSrc );
268 detachNetSettingsFor( result.get() );
269 adoptNetSettings( settingsSrc );
270
271 *result->GetEmbeddedFiles() = *settingsSrc->GetEmbeddedFiles();
272
273 // Flag the project-file-scoped fields (DRC severities) the
274 // handler needs to mirror onto ancestor + persist via Save
275 // ProjectCopy. Fire whenever any side diverged from ancestor
276 // — including a TAKE_ANCESTOR resolution, since the merge
277 // output still needs ancestor's severity map written to disk
278 // (the output path may not pre-exist, or may contain ours/
279 // theirs).
280 // Bind to a shared empty map when there is no ancestor so the
281 // common ancestor-present branch references the member directly
282 // instead of materializing a full copy of the severity map (a
283 // mixed value-category ternary would force one).
284 static const std::map<int, SEVERITY> s_emptySeverities;
285 const std::map<int, SEVERITY>& ancDrc = m_ancestor ? m_ancestor->GetDesignSettings().m_DRCSeverities
286 : s_emptySeverities;
287
288 const bool oursDrcChanged = m_ours && m_ours->GetDesignSettings().m_DRCSeverities != ancDrc;
289 const bool theirsDrcChanged = m_theirs && m_theirs->GetDesignSettings().m_DRCSeverities != ancDrc;
290
291 // Net settings divergence detection. Like DRC severities, this only
292 // fires when sibling .kicad_pro files were loaded; plain temp blobs
293 // (git mergetool) see defaults on every side.
294 auto netSettingsEqual =
295 []( const BOARD* aLhs, const BOARD* aRhs )
296 {
297 if( !aLhs || !aRhs )
298 return true;
299
300 const auto& lhs = aLhs->GetDesignSettings().m_NetSettings;
301 const auto& rhs = aRhs->GetDesignSettings().m_NetSettings;
302
303 if( !lhs && !rhs )
304 return true;
305
306 if( !lhs || !rhs )
307 return false;
308
309 return *lhs == *rhs;
310 };
311
312 const bool oursNetChanged = !netSettingsEqual( m_ours, m_ancestor );
313 const bool theirsNetChanged = !netSettingsEqual( m_theirs, m_ancestor );
314
315 // Custom DRC rules live in a sibling .kicad_dru file (not in the
316 // .kicad_pro), so divergence detection reads the file content.
317 // Plain temp-blob merges typically see empty content on every
318 // side — diff fires only when a real project tree is present.
319 const wxString ancRules = readSiblingRules( m_ancestor );
320 const wxString oursRules = readSiblingRules( m_ours );
321 const wxString theirsRules = readSiblingRules( m_theirs );
322
323 const bool oursRulesChanged = m_ours && oursRules != ancRules;
324 const bool theirsRulesChanged = m_theirs && theirsRules != ancRules;
325
326 // Stage the chosen side's rules content for the handler to write
327 // alongside the merged board. Whole-side path: take the
328 // settingsSrc choice (TAKE_OURS/THEIRS/ANCESTOR). Per-property
329 // MERGE_PROPS overrides this below.
330 if( oursRulesChanged || theirsRulesChanged )
331 {
332 if( settingsSrc == m_ours )
333 m_report.customDrcRules = oursRules;
334 else if( settingsSrc == m_theirs )
335 m_report.customDrcRules = theirsRules;
336 else
337 m_report.customDrcRules = ancRules;
338
339 m_report.customDrcRulesSet = true;
340 }
341
342 // Library tables (fp-lib-table, sym-lib-table) follow the same
343 // shape as custom DRC rules. Read each side's content from the
344 // project directory and stage the chosen side's content on the
345 // report for the handler to write into the merged project dir.
346 const wxString ancFp = readFpLibTable( m_ancestor );
347 const wxString oursFp = readFpLibTable( m_ours );
348 const wxString theirsFp = readFpLibTable( m_theirs );
349
350 const bool oursFpChanged = m_ours && oursFp != ancFp;
351 const bool theirsFpChanged = m_theirs && theirsFp != ancFp;
352
353 if( oursFpChanged || theirsFpChanged )
354 {
355 if( settingsSrc == m_ours )
356 m_report.fpLibTable = oursFp;
357 else if( settingsSrc == m_theirs )
358 m_report.fpLibTable = theirsFp;
359 else
360 m_report.fpLibTable = ancFp;
361
362 m_report.fpLibTableSet = true;
363 }
364
365 const wxString ancSym = readSymLibTable( m_ancestor );
366 const wxString oursSym = readSymLibTable( m_ours );
367 const wxString theirsSym = readSymLibTable( m_theirs );
368
369 const bool oursSymChanged = m_ours && oursSym != ancSym;
370 const bool theirsSymChanged = m_theirs && theirsSym != ancSym;
371
372 if( oursSymChanged || theirsSymChanged )
373 {
374 if( settingsSrc == m_ours )
375 m_report.symLibTable = oursSym;
376 else if( settingsSrc == m_theirs )
377 m_report.symLibTable = theirsSym;
378 else
379 m_report.symLibTable = ancSym;
380
381 m_report.symLibTableSet = true;
382 }
383
384 // Drawing sheet file lives on PROJECT_FILE. Detect divergence so
385 // TAKE_ANCESTOR also persists the chosen path.
386 auto drawingSheet = []( const BOARD* aBoard ) -> wxString
387 {
388 if( !aBoard || !aBoard->GetProject() )
389 return wxEmptyString;
390
392 };
393
394 const wxString ancSheet = drawingSheet( m_ancestor );
395 const bool sheetDiverged =
396 ( m_ours && drawingSheet( m_ours ) != ancSheet )
397 || ( m_theirs && drawingSheet( m_theirs ) != ancSheet );
398
399 // Whole-side path: SetDesignSettings copies the board+stackup
400 // fields but the drawing sheet path lives on PROJECT_FILE. The
401 // result BOARD has no project to mutate; stage the chosen
402 // value on REPORT so the handler can mirror it onto ancestor's
403 // project before SaveProjectCopy. Without this, sheetDiverged
404 // would flip projectFileTouched but no path value would be
405 // staged, and the handler's mirror block would skip,
406 // persisting ancestor's old sheet to disk.
407 if( sheetDiverged && settingsSrc && settingsSrc->GetProject() )
408 {
409 m_report.drawingSheetFile =
411 m_report.drawingSheetFileSet = true;
412 }
413
414 if( oursDrcChanged || theirsDrcChanged )
415 {
416 m_report.drcSeveritiesTouched = true;
417 m_report.projectFileTouched = true;
418 }
419
420 if( oursNetChanged || theirsNetChanged )
421 {
422 m_report.netClassesTouched = true;
423 m_report.projectFileTouched = true;
424 }
425
426 if( sheetDiverged || oursRulesChanged || theirsRulesChanged
427 || oursFpChanged || theirsFpChanged || oursSymChanged || theirsSymChanged )
428 {
429 m_report.projectFileTouched = true;
430 }
431 }
432
433 // MERGE_PROPS for doc-level: orthogonal edits (ours touches paper,
434 // theirs touches thickness) should auto-merge instead of forcing the
435 // user to pick a side. Apply per-property over the whole-side base
436 // we just copied.
437 if( docRes && docRes->kind == ITEM_RES::MERGE_PROPS )
438 {
439 auto pickBoard = [&]( PROP_RES aKind ) -> const BOARD*
440 {
441 if( aKind == PROP_RES::OURS ) return m_ours;
442 if( aKind == PROP_RES::THEIRS ) return m_theirs;
443 return m_ancestor;
444 };
445
446 PAGE_INFO merged = result->GetPageSettings();
447 bool pageTouched = false;
448
449 for( const PROPERTY_RESOLUTION& prop : docRes->props )
450 {
451 const BOARD* src = pickBoard( prop.kind );
452
453 if( !src )
454 continue;
455
456 if( prop.name == DOC_PROP_PAGE_FORMAT )
457 {
458 merged.SetType( src->GetPageSettings().GetType(), merged.IsPortrait() );
459 pageTouched = true;
460 }
461 else if( prop.name == DOC_PROP_PAGE_ORIENTATION )
462 {
463 merged.SetPortrait( src->GetPageSettings().IsPortrait() );
464 pageTouched = true;
465 }
466 else if( prop.name == DOC_PROP_PLOT_OPTIONS )
467 {
468 result->SetPlotOptions( src->GetPlotOptions() );
469 }
470 else if( prop.name == DOC_PROP_LEGACY_TEARDROPS )
471 {
472 result->SetLegacyTeardrops( src->LegacyTeardrops() );
473 }
474 else if( prop.name.StartsWith( DOC_PROP_BOARD_PROPERTY ) )
475 {
476 const wxString key = prop.name.Mid( DOC_PROP_BOARD_PROPERTY.length() );
477 std::map<wxString, wxString> props = result->GetProperties();
478 auto it = src->GetProperties().find( key );
479
480 if( it != src->GetProperties().end() )
481 props[key] = it->second;
482 else
483 props.erase( key );
484
485 result->SetProperties( props );
486 }
487 else if( prop.name.StartsWith( DOC_PROP_VARIANT ) )
488 {
489 const wxString variant = prop.name.Mid( DOC_PROP_VARIANT.length() );
490
491 if( src->HasVariant( variant ) )
492 {
493 result->AddVariant( variant );
494 result->SetVariantDescription( variant, src->GetVariantDescription( variant ) );
495 }
496 else
497 {
498 result->DeleteVariant( variant );
499 }
500 }
501 else if( prop.name == DOC_PROP_BOARD_THICKNESS )
502 {
503 result->GetDesignSettings().SetBoardThickness( src->GetDesignSettings().GetBoardThickness() );
504 }
505 else if( prop.name == DOC_PROP_LAYER_STACKUP )
506 {
507 // Stackup is structural; per-property doesn't decompose,
508 // copy the whole stackup descriptor.
509 result->GetDesignSettings().GetStackupDescriptor() =
511 adoptLayers( src );
512 }
513 else if( prop.name == DOC_PROP_DRC_SEVERITIES )
514 {
515 // Always copy the chosen side's severity map and flag
516 // projectFileTouched. The engine only emitted this
517 // property in MERGE_PROPS because at least one side
518 // diverged from ancestor; a PROP_RES::ANCESTOR
519 // resolution writes ancestor's map back to the output
520 // .kicad_pro (which may not pre-exist).
521 result->GetDesignSettings().m_DRCSeverities = src->GetDesignSettings().m_DRCSeverities;
522 m_report.drcSeveritiesTouched = true;
523 m_report.projectFileTouched = true;
524 }
525 else if( prop.name == DOC_PROP_FP_LIB_TABLE )
526 {
527 m_report.fpLibTable = readFpLibTable( src );
528 m_report.fpLibTableSet = true;
529 m_report.projectFileTouched = true;
530 }
531 else if( prop.name == DOC_PROP_SYM_LIB_TABLE )
532 {
533 m_report.symLibTable = readSymLibTable( src );
534 m_report.symLibTableSet = true;
535 m_report.projectFileTouched = true;
536 }
537 else if( prop.name == DOC_PROP_CUSTOM_RULES )
538 {
539 // Stage the chosen side's .kicad_dru content on the
540 // report. The handler writes it next to the merged
541 // .kicad_pcb. PROP_RES::ANCESTOR re-reads ancestor's
542 // rules so a TAKE_ANCESTOR resolution still persists
543 // ancestor's content to the output path (which may
544 // not pre-exist or may contain ours' rules from a
545 // previous merge attempt).
546 m_report.customDrcRules = readSiblingRules( src );
547 m_report.customDrcRulesSet = true;
548 m_report.projectFileTouched = true;
549 }
550 else if( prop.name == DOC_PROP_NET_CLASSES )
551 {
552 // Net classes don't decompose per-property; copy the
553 // chosen side's whole NET_SETTINGS into result via
554 // CopyFrom (which preserves m_parent / m_path on the
555 // result's NET_SETTINGS so SaveProjectCopy walks the
556 // right nested-settings entry). The whole-side branch
557 // already detached the alias and adopted settingsSrc;
558 // this overrides that choice for the per-property
559 // resolution.
560 if( src && src->GetDesignSettings().m_NetSettings
561 && result->GetDesignSettings().m_NetSettings )
562 {
563 result->GetDesignSettings().m_NetSettings->CopyFrom( *src->GetDesignSettings().m_NetSettings );
564 }
565
566 m_report.netClassesTouched = true;
567 m_report.projectFileTouched = true;
568 }
569 else if( prop.name == DOC_PROP_DRAWING_SHEET )
570 {
571 // Drawing sheet path lives on PROJECT_FILE. Stage the
572 // chosen value on the result BOARD's project (which
573 // PCB_MERGE_APPLIER doesn't own — store the choice in
574 // the report so the handler can mirror it onto
575 // ancestor's project before SaveProjectCopy). The
576 // handler reads m_drawingSheetFile and applies if
577 // non-empty marker.
578 if( src && src->GetProject() )
579 {
580 m_report.drawingSheetFile = src->GetProject()->GetProjectFile().m_BoardDrawingSheetFile;
581 m_report.drawingSheetFileSet = true;
582 m_report.projectFileTouched = true;
583 }
584 }
585 else if( prop.name.StartsWith( DOC_PROP_EMBEDDED_FILE ) )
586 {
587 const wxString fileName = prop.name.Mid( DOC_PROP_EMBEDDED_FILE.length() );
588 EMBEDDED_FILES* files = result->GetEmbeddedFiles();
589
590 files->RemoveFile( fileName );
591
592 if( auto* file = src->GetEmbeddedFiles()->GetEmbeddedFile( fileName ) )
593 files->AddFile( std::make_shared<EMBEDDED_FILES::EMBEDDED_FILE>( *file ) );
594 }
595 else if( prop.name == DOC_PROP_EMBED_FONTS )
596 {
597 result->GetEmbeddedFiles()->SetAreFontsEmbedded( src->GetEmbeddedFiles()->GetAreFontsEmbedded() );
598 }
599 else
600 {
601 // Any name this does not own, such as an ERC or library table property, is ignored.
602 ApplyTitleBlockProperty( result->GetTitleBlock(), prop.name, src->GetTitleBlock() );
603 }
604 }
605
606 if( pageTouched )
607 result->SetPageSettings( merged );
608 }
609 }
610
611 // Collect every distinct KIID that appears in any of the three boards or
612 // in the plan. Walk top-level items only -- footprint children are
613 // handled implicitly when their parent footprint is cloned.
614 std::set<KIID> allIds;
616 CollectTopLevelIds( m_ours, allIds );
617 CollectTopLevelIds( m_theirs, allIds );
618
619 auto resolutionFor =
620 [&]( const KIID& aUuid ) -> const ITEM_RESOLUTION*
621 {
623 path.push_back( aUuid );
624
625 auto it = actionsById.find( path );
626
627 if( it == actionsById.end() )
628 return nullptr;
629
630 return it->second;
631 };
632
633 // Track which actions were consumed at the top level so the child-
634 // resolution post-pass only sees nested actions.
635 std::set<KIID_PATH> consumedActions;
636
637 for( const KIID& uuid : allIds )
638 {
639 KIID_PATH topPath;
640 topPath.push_back( uuid );
641
642 if( actionsById.count( topPath ) )
643 consumedActions.insert( topPath );
644
645 const ITEM_RESOLUTION* res = resolutionFor( uuid );
646
647 // No resolution = item unchanged on both sides; take from ancestor
648 // (or ours if ancestor missing — handles new boards without a base).
649 if( !res )
650 {
651 const BOARD_ITEM* src = findItem( m_ancestor, uuid );
652
653 if( !src )
654 src = findItem( m_ours, uuid );
655
656 if( !src )
657 src = findItem( m_theirs, uuid );
658
659 cloneInto( result.get(), src );
660 continue;
661 }
662
663 switch( res->kind )
664 {
666 {
667 const BOARD_ITEM* src = findItem( m_ours, uuid );
668
669 if( src )
670 {
671 cloneInto( result.get(), src );
672 ++m_report.itemsTakenOurs;
673 }
674
675 break;
676 }
677
679 {
680 const BOARD_ITEM* src = findItem( m_theirs, uuid );
681
682 if( src )
683 {
684 cloneInto( result.get(), src );
685 ++m_report.itemsTakenTheirs;
686 }
687
688 break;
689 }
690
692 {
693 const BOARD_ITEM* src = findItem( m_ancestor, uuid );
694
695 if( src )
696 cloneInto( result.get(), src );
697
698 break;
699 }
700
702 ++m_report.itemsDeleted;
703 // Intentionally drop the item.
704 break;
705
706 case ITEM_RES::KEEP:
707 {
708 // Conservative conflict default: take ancestor if available,
709 // otherwise ours, otherwise theirs.
710 const BOARD_ITEM* src = findItem( m_ancestor, uuid );
711
712 if( !src )
713 src = findItem( m_ours, uuid );
714
715 if( !src )
716 src = findItem( m_theirs, uuid );
717
718 if( src )
719 {
720 cloneInto( result.get(), src );
721 ++m_report.itemsKept;
722 }
723
724 break;
725 }
726
728 {
729 const BOARD_ITEM* ours = findItem( m_ours, uuid );
730 const BOARD_ITEM* theirs = findItem( m_theirs, uuid );
731 const BOARD_ITEM* ancestor = findItem( m_ancestor, uuid );
732
733 // Start from ours; apply property resolutions.
734 const BOARD_ITEM* base = ours ? ours : ( ancestor ? ancestor : theirs );
735
736 if( !base )
737 break;
738
739 BOARD_ITEM* placed = cloneInto( result.get(), base );
740
741 if( !placed )
742 break;
743
744 applyPropertyResolutions( placed, res->props, ours, theirs, ancestor );
745 ++m_report.itemsMergedProps;
746 break;
747 }
748 }
749 }
750
751 // Child-level resolution post-pass. The merge engine emits actions for
752 // footprint children (pads, fields, graphics, zones) with KIID_PATHs of
753 // the form [parent_uuid, child_diff_id]. The top-level loop above brings
754 // children along when the parent footprint is cloned, but does NOT apply
755 // per-child resolutions. This pass finds the cloned child on the result
756 // board and adds/removes/merges it per its resolution.
757
758 // Index the merged board's footprints once so each child action resolves
759 // its parent in O(log n), instead of rebuilding result->GetItemSet() (a
760 // full item-set copy) and linear-scanning it per child action.
761 std::map<KIID, FOOTPRINT*> footprintsByUuid;
762
763 for( FOOTPRINT* fp : result->Footprints() )
764 {
765 if( fp )
766 footprintsByUuid[fp->m_Uuid] = fp;
767 }
768
769 for( const auto& [actionPath, action] : actionsById )
770 {
771 if( consumedActions.count( actionPath ) )
772 continue;
773
774 if( actionPath.size() < 2 )
775 continue; // not a child path
776
777 const KIID& parentUuid = actionPath.at( 0 );
778 const KIID& childUuid = actionPath.at( 1 );
779
780 // Find the cloned parent footprint on the result board.
781 auto fpIt = footprintsByUuid.find( parentUuid );
782 FOOTPRINT* parentFp = fpIt != footprintsByUuid.end() ? fpIt->second : nullptr;
783
784 if( !parentFp )
785 continue;
786
787 BOARD_ITEM* targetChild = nullptr;
788
789 for( PAD* pad : parentFp->Pads() )
790 {
791 if( pad->m_Uuid == childUuid )
792 {
793 targetChild = pad;
794 break;
795 }
796 }
797
798 if( !targetChild )
799 {
800 for( BOARD_ITEM* g : parentFp->GraphicalItems() )
801 {
802 if( g && g->m_Uuid == childUuid )
803 {
804 targetChild = g;
805 break;
806 }
807 }
808 }
809
810 if( !targetChild )
811 {
812 for( ZONE* z : parentFp->Zones() )
813 {
814 if( z && z->m_Uuid == childUuid )
815 {
816 targetChild = z;
817 break;
818 }
819 }
820 }
821
822 if( !targetChild )
823 {
824 for( PCB_FIELD* f : parentFp->GetFields() )
825 {
826 if( f && ( f->m_Uuid == childUuid || PcbDiffItemId( *f ) == childUuid ) )
827 {
828 targetChild = f;
829 break;
830 }
831 }
832 }
833
834 // Adopt the chosen side's child, updating existing fields in place and
835 // cloning other children. Used by the take-a-side child resolutions below.
836 auto adoptChildFrom =
837 [&]( const BOARD* aSide )
838 {
839 const BOARD_ITEM* src = findItem( aSide, childUuid );
840
841 if( targetChild && src && targetChild->Type() == PCB_FIELD_T )
842 {
843 // Remove() deletes the field's variant overrides. Keep the existing field
844 // and its ownership when replacing its contents with the chosen side's.
845 EDA_GROUP* parentGroup = targetChild->GetParentGroup();
846
847 targetChild->CopyFrom( src );
848 targetChild->SetParent( parentFp );
849 targetChild->SetParentGroup( parentGroup );
850 targetChild->ClearEditFlags();
851 parentFp->InvalidateGeometryCaches();
852 return;
853 }
854
855 if( targetChild )
856 {
857 parentFp->Remove( targetChild );
858 delete targetChild;
859 targetChild = nullptr;
860 }
861
862 if( !src )
863 return;
864
865 std::unique_ptr<EDA_ITEM> cloned( src->Clone() );
866
867 if( auto* childClone = dynamic_cast<BOARD_ITEM*>( cloned.get() ) )
868 {
869 parentFp->Add( childClone, ADD_MODE::APPEND );
870
871 // Ownership transfers to parentFp only once Add() has adopted the clone.
872 cloned.release();
873 }
874 };
875
876 switch( action->kind )
877 {
879 {
880 if( !targetChild )
881 break;
882
883 // Resolve the same diff identity on each side, including name-based field IDs.
884 const BOARD_ITEM* oursChild = findItem( m_ours, childUuid );
885 const BOARD_ITEM* theirsChild = findItem( m_theirs, childUuid );
886 const BOARD_ITEM* ancestorChild = findItem( m_ancestor, childUuid );
887
888 applyPropertyResolutions( targetChild, action->props, oursChild, theirsChild, ancestorChild );
889 break;
890 }
891
893 // Child added or modified on theirs. The ours-based parent clone
894 // does not carry a theirs-added child, so clone it in (or replace
895 // the ours version); otherwise the addition/edit is silently lost.
896 adoptChildFrom( m_theirs );
897 break;
898
900 // The parent may have been cloned from ancestor/theirs when the
901 // parent itself had no resolution (or resolved to another side).
902 // Replace the current child from ours so one-sided child edits do
903 // not depend on the parent's chosen clone source.
904 adoptChildFrom( m_ours );
905 break;
906
908 adoptChildFrom( m_ancestor );
909 break;
910
912 // Child removed on a side. The ours-based clone still has it, so
913 // drop it; otherwise the deletion is silently reverted.
914 if( targetChild )
915 {
916 parentFp->Remove( targetChild );
917 delete targetChild;
918 }
919
920 break;
921
922 case ITEM_RES::KEEP:
923 // Conservative conflict default: preserve the child already present
924 // on the parent clone.
925 break;
926 }
927 }
928
929 // Post-apply validators. Collect refdes entries from the merged result,
930 // schema versions from each side, and the connectivity-rebuild ack the
931 // caller may have set. Failures land on m_report.validation; the CLI merge
932 // handlers surface them through the job reporter.
933 {
934 VALIDATION_INPUT vInput;
935
936 for( const FOOTPRINT* fp : result->Footprints() )
937 {
938 if( !fp )
939 continue;
940
941 REFDES_ENTRY entry;
942 entry.refdes = fp->GetReference();
943 entry.id = KIID_PATH();
944 entry.id.push_back( fp->m_Uuid );
945 vInput.refdesEntries.push_back( std::move( entry ) );
946 }
947
948 // The merged board is serialized and its connectivity is rebuilt by the
949 // consumer when it loads the result (connectivity is not persisted), so
950 // the applier has satisfied the plan's rebuild requirement for the
951 // output file. Acknowledge it here so the validator doesn't raise a
952 // false "stale connectivity" error on every connectivity-affecting merge.
953 m_report.connectivityRebuildPerformed = m_plan.requiresConnectivityRebuild;
954
955 vInput.planRequiredRebuild = m_plan.requiresConnectivityRebuild;
956 vInput.applierReportedRebuild = m_report.connectivityRebuildPerformed;
957
958 vInput.ancestorSchemaVersion = m_ancestor ? m_ancestor->GetFileFormatVersionAtLoad() : 0;
959 vInput.oursSchemaVersion = m_ours ? m_ours ->GetFileFormatVersionAtLoad() : 0;
960 vInput.theirsSchemaVersion = m_theirs ? m_theirs ->GetFileFormatVersionAtLoad() : 0;
961
962 m_report.validation = RunPostApplyValidators( vInput );
963 }
964
965 return result;
966}
967
968} // namespace KICAD_DIFF
const char * name
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:85
virtual void CopyFrom(const BOARD_ITEM *aOther)
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
EMBEDDED_FILES * GetEmbeddedFiles() override
Definition board.cpp:3662
const std::vector< wxString > & GetVariantNames() const
Definition board.h:533
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1520
static wxString GetStandardLayerName(PCB_LAYER_ID aLayerId)
Return an "English Standard" name of a PCB layer when given aLayerNumber.
Definition board.h:1122
const PAGE_INFO & GetPageSettings() const
Definition board.h:1020
TITLE_BLOCK & GetTitleBlock()
Definition board.h:1026
bool HasVariant(const wxString &aVariantName) const
Definition board.cpp:2995
const std::map< wxString, wxString > & GetProperties() const
Definition board.h:526
const wxString & GetFileName() const
Definition board.h:454
wxString GetVariantDescription(const wxString &aVariantName) const
Definition board.cpp:3125
const PCB_PLOT_PARAMS & GetPlotOptions() const
Definition board.h:1023
PROJECT * GetProject() const
Definition board.h:774
bool LegacyTeardrops() const
Definition board.h:1658
BOARD_DESIGN_SETTINGS & GetDesignSettings()
Definition board.cpp:1290
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:43
virtual void ClearEditFlags()
Definition eda_item.h:178
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:116
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
virtual void SetParentGroup(EDA_GROUP *aGroup)
Definition eda_item.h:115
virtual EDA_ITEM * Clone() const
Create a duplicate of this item with linked list members set to NULL.
Definition eda_item.cpp:278
virtual void SetParent(EDA_ITEM *aParent)
Definition eda_item.cpp:153
EMBEDDED_FILE * GetEmbeddedFile(const wxString &aName) const
Returns the embedded file with the given name or nullptr if it does not exist.
void RemoveFile(const wxString &aName, bool aErase=true)
Remove a file from the collection and frees the memory.
EMBEDDED_FILE * AddFile(const wxFileName &aName, bool aOverwrite)
Load a file from disk and adds it to the collection.
bool GetAreFontsEmbedded() const
ZONES & Zones()
Definition footprint.h:361
void Remove(BOARD_ITEM *aItem, REMOVE_MODE aMode=REMOVE_MODE::NORMAL) override
Removes an item from the container.
std::deque< PAD * > & Pads()
Definition footprint.h:355
void InvalidateGeometryCaches()
Resets the caches for this footprint, for example if it was modified via the API.
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
void GetFields(std::vector< PCB_FIELD * > &aVector, bool aVisibleOnly) const
Populate a std::vector with PCB_TEXTs.
DRAWINGS & GraphicalItems()
Definition footprint.h:358
BOARD_ITEM * cloneInto(BOARD *aTarget, const BOARD_ITEM *aSource) const
Clone a board item using its virtual Clone(); returns nullptr if the source is null,...
std::unique_ptr< BOARD > Apply()
Produce the merged board.
PCB_MERGE_APPLIER(const BOARD *aAncestor, const BOARD *aOurs, const BOARD *aTheirs, MERGE_PLAN aPlan)
std::size_t applyPropertyResolutions(BOARD_ITEM *aTarget, const std::vector< PROPERTY_RESOLUTION > &aProps, const BOARD_ITEM *aOurs, const BOARD_ITEM *aTheirs, const BOARD_ITEM *aAncestor)
Apply property-level resolutions to a clone of aOurs (or aTheirs per PROP_RES).
const BOARD_ITEM * findItem(const BOARD *aBoard, const KIID &aId) const
Locate an item by UUID or a footprint field's name-based diff ID on a source board.
Definition kiid.h:46
Definition pad.h:61
Describe the page size and margins of a paper page on which to eventually print or plot.
Definition page_info.h:75
void SetPortrait(bool aIsPortrait)
Rotate the paper page 90 degrees.
bool SetType(PAGE_SIZE_TYPE aPageSize, bool aIsPortrait=false)
Set the name of the page type and also the sizes and margins commonly associated with that type name.
bool IsPortrait() const
Definition page_info.h:123
const PAGE_SIZE_TYPE & GetType() const
Definition page_info.h:97
wxString m_BoardDrawingSheetFile
PcbNew params.
virtual PROJECT_FILE & GetProjectFile() const
Definition project.h:201
Handle a list of polygons defining a copper zone.
Definition zone.h:70
static const std::string SymbolLibraryTableFileName
static const std::string FootprintLibraryTableFileName
static const std::string DesignRulesFileExtension
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
bool ApplyTitleBlockProperty(TITLE_BLOCK &aTarget, const wxString &aName, const TITLE_BLOCK &aSource)
Copy the title block field named by a document property from aSource into aTarget.
const wxString DOC_PROP_SYM_LIB_TABLE
const wxString DOC_PROP_VARIANT
const wxString DOC_PROP_BOARD_THICKNESS
const wxString DOC_PROP_PLOT_OPTIONS
Board-level settings stored in the board file.
const wxString DOC_PROP_PAGE_FORMAT
Property-name keys for the synthetic document-level ITEM_CHANGE (empty KIID_PATH).
void CollectTopLevelIds(const BOARD *aBoard, std::set< KIID > &aOut)
Insert every top-level item UUID from aBoard into aOut.
const wxString DOC_PROP_NET_CLASSES
const wxString DOC_PROP_CUSTOM_RULES
const wxString DOC_PROP_PAGE_ORIENTATION
const wxString DOC_PROP_LEGACY_TEARDROPS
const wxString DOC_PROP_FP_LIB_TABLE
const wxString DOC_PROP_EMBED_FONTS
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
PROP_RES
Resolution kind for a single property of a single item.
PROPERTY_APPLY_COUNTS ApplyPropertyResolutions(INSPECTABLE *aTarget, const std::vector< PROPERTY_RESOLUTION > &aProps, const INSPECTABLE *aOurs, const INSPECTABLE *aTheirs, const INSPECTABLE *aAncestor)
Apply per-property merge resolutions to aTarget, sourcing OURS/THEIRS/ANCESTOR values from the matchi...
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.
const wxString DOC_PROP_EMBEDDED_FILE
Prefix of the property for one embedded file, followed by the file name.
VALIDATION_REPORT RunPostApplyValidators(const VALIDATION_INPUT &aInput)
Run every standard post-apply validator and merge their reports.
const wxString DOC_PROP_BOARD_PROPERTY
Prefixes of the property for one board property or variant, followed by its name.
STL namespace.
std::vector< PROPERTY_RESOLUTION > props
Result of planning a 3-way merge.
Applied/failed tallies from ApplyPropertyResolutions, folded into a caller's report.
Reference-designator uniqueness over a flat list of (refdes, id) pairs.
KIID_PATH id
wxString refdes
Inputs needed to run the post-apply validator pipeline.
std::vector< REFDES_ENTRY > refdesEntries
std::string path
VECTOR3I res
wxString result
Test unit parsing edge cases and error handling.
@ PCB_FIELD_T
class PCB_FIELD, text associated with a footprint property
Definition typeinfo.h:82
Definition of file extensions used in Kicad.