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