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