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