KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_migrate_3d_models.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 2
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, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <algorithm>
23#include <unordered_set>
24
25#include <wx/utils.h>
26#include <wx/dir.h>
27#include <wx/dirdlg.h>
28#include <wx/filedlg.h>
29#include <wx/filename.h>
30
32#include <widgets/wx_grid.h>
35#include <board.h>
36#include <board_commit.h>
39#include <footprint.h>
40#include <lset.h>
41#include <pcb_edit_frame.h>
42#include <pgm_base.h>
43#include <project.h>
44#include <project_pcb.h>
47
48#include <3d_cache/3d_cache.h>
53#include <filename_resolver.h>
54#include <gestfich.h>
55
56
57#define UNSPECIFIED -2
58#define KEEP_EXISTING -1
59
60namespace
61{
66const std::vector<wxString>& stepExtensions()
67{
68 static const std::vector<wxString> exts = {
69 wxS( "step" ),
70 wxS( "stp" ),
71 wxS( "stpz" ),
72 wxS( "step.gz" ),
73 wxS( "stp.gz" ),
74 wxS( "iges" ),
75 wxS( "igs" )
76 };
77 return exts;
78}
79
80
86wxString normalizeStem( const wxString& aName )
87{
88 wxString stem = aName;
89
90 // Strip known extensions (case-insensitive), including doubled-up .gz
91 // forms that don't round-trip through wxFileName::GetName.
92 static const wxString stripList[] = {
93 wxS( ".step.gz" ),
94 wxS( ".stp.gz" ),
95 wxS( ".wrl" ),
96 wxS( ".wrz" ),
97 wxS( ".step" ),
98 wxS( ".stp" ),
99 wxS( ".stpz" ),
100 wxS( ".iges" ),
101 wxS( ".igs" )
102 };
103
104 for( const wxString& ext : stripList )
105 {
106 if( stem.length() > ext.length() && stem.Right( ext.length() ).Lower() == ext )
107 {
108 stem = stem.Left( stem.length() - ext.length() );
109 break;
110 }
111 }
112
113 stem.MakeLower();
114
115 // Treat separator characters as interchangeable so that e.g.
116 // "R_0603_1608Metric" and "R-0603-1608Metric" collide.
117 stem.Replace( wxS( "-" ), wxS( "_" ) );
118 stem.Replace( wxS( " " ), wxS( "_" ) );
119
120 return stem;
121}
122
123
125int levenshtein( const wxString& a, const wxString& b )
126{
127 const size_t m = a.length();
128 const size_t n = b.length();
129
130 if( m == 0 )
131 return static_cast<int>( n );
132
133 if( n == 0 )
134 return static_cast<int>( m );
135
136 std::vector<int> prev( n + 1 );
137 std::vector<int> curr( n + 1 );
138
139 for( size_t j = 0; j <= n; ++j )
140 prev[j] = static_cast<int>( j );
141
142 for( size_t i = 1; i <= m; ++i )
143 {
144 curr[0] = static_cast<int>( i );
145
146 for( size_t j = 1; j <= n; ++j )
147 {
148 int cost = ( a[i - 1] == b[j - 1] ) ? 0 : 1;
149 curr[j] = std::min( { curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost } );
150 }
151
152 std::swap( prev, curr );
153 }
154
155 return prev[n];
156}
157
158
161wxString parentDirName( const wxString& aPath )
162{
163 wxFileName fn( aPath );
164 const wxArrayString& dirs = fn.GetDirs();
165 return dirs.empty() ? wxString() : dirs.Last();
166}
167
168
172wxString parentDirFromFilename( const wxString& aFilename )
173{
174 return parentDirName( aFilename );
175}
176
177} // anonymous namespace
178
179
182 m_frame( aFrame ),
183 m_dummyBoard( nullptr ),
184 m_dummyFootprint( nullptr ),
187 m_previewPane( nullptr )
188{
189 m_searchDirsGrid->ClearRows();
190 m_searchDirsGrid->EnableEditing( false );
191 m_searchDirsGrid->SetupColumnAutosizer( 0 );
192
193 if( COMMON_SETTINGS* common = Pgm().GetCommonSettings() )
194 {
195 for( const wxString& dir : common->m_Extra3DSearchDirs )
196 {
197 m_searchDirsGrid->AppendRows( 1 );
198 m_searchDirsGrid->SetCellValue( m_searchDirsGrid->GetNumberRows() - 1, 0, dir );
199 }
200 }
201
205
206 m_headerLabel->SetFont( KIUI::GetControlFont( this ).Bold() );
207 m_subHeader->SetFont( KIUI::GetControlFont( this ).Italic() );
208 m_subHeader2->SetFont( KIUI::GetControlFont( this ).Italic() );
209 m_helpText->SetFont( KIUI::GetInfoFont( this ).Italic() );
210
211 m_missingList->AppendColumn( wxEmptyString );
212 m_candidatesList->AppendColumn( wxEmptyString );
213
214 // Keep the single column filling the visible width as the splitter sash moves.
215 auto fitColumn =
216 []( wxListCtrl* aList )
217 {
218 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
219 aList->SetColumnWidth( 0, width );
220 };
221
222 m_missingList->Bind( wxEVT_SIZE,
223 [this, fitColumn]( wxSizeEvent& aEvt )
224 {
225 fitColumn( m_missingList );
226 aEvt.Skip();
227 } );
228
229 m_candidatesList->Bind( wxEVT_SIZE,
230 [this, fitColumn]( wxSizeEvent& aEvt )
231 {
232 fitColumn( m_candidatesList );
233 aEvt.Skip();
234 } );
235
236 // Build the throwaway board + footprint and the 3D canvas.
238
239 SetupStandardButtons( { { wxID_OK, _( "Replace Models" ) },
240 { wxID_CANCEL, _( "Cancel" ) } } );
241
243}
244
245
247{
248 delete m_previewPane;
249 delete m_dummyBoard; // owns m_dummyFootprint
250}
251
252
254{
255 std::map<wxString, ENTRY> byName = collectMissingWrlModels( m_frame );
256
257 m_missing.reserve( byName.size() );
258 m_missingRepFp.reserve( byName.size() );
259 m_missingXform.reserve( byName.size() );
260
261 for( const auto& [fn, entry] : byName )
262 {
263 m_missing.push_back( fn );
264 m_missingRepFp.push_back( entry.m_fp );
265 m_missingXform.push_back( entry.m_xform );
266 }
267
268 buildCatalog();
269
271 m_candidatesPerMissing.resize( m_missing.size() );
273
274 // Perform these actions *after* we have loaded saved state into the dialog
275
276 // Auto-select the top real (non-"keep existing") candidate for every missing row so the user gets
277 // a sensible default without having to click through each one. Rows for which no real candidate
278 // exists get bolded by populateMissingList() as a visual attention marker.
279 for( size_t i = 0; i < m_missing.size(); ++i )
280 {
281 const std::vector<MATCH_CANDIDATE>& candidates = m_candidatesPerMissing[i];
282
283 if( !candidates.empty() && !candidates.front().m_absPath.IsEmpty() )
285 }
286
288
289 if( !m_missing.empty() )
290 {
291 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
292 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
294 }
295
296 return true;
297}
298
299
304
305
310
311
313{
314 std::map<wxString, ENTRY> missingModels = collectMissingWrlModels( aFrame );
315
316 if( missingModels.size() == 0 )
317 return 0;
318
319 BOARD* board = aFrame->GetBoard();
321 const wxString projectPath = aFrame->Prj().GetProjectPath();
323
324 catalog.Build( projectPath, resolver );
325
326 std::map<wxString, wxString> replacements;
327
328 for( const auto& [filename, _] : missingModels )
329 {
330 const wxString match = catalog.FindMatchFor( filename );
331
332 if( match.IsEmpty() )
333 continue;
334
335 const wxString shortened = resolver->ShortenPath( match );
336
337 // ShortenPath returning empty would blank the footprint reference;
338 // fall back to the absolute path in that edge case.
339 replacements[filename] = shortened.IsEmpty() ? match : shortened;
340 }
341
342 if( replacements.empty() )
343 return 0;
344
345 BOARD_COMMIT commit( aFrame );
346 int rewritten = 0;
347
348 for( FOOTPRINT* fp : board->Footprints() )
349 {
350 bool changedThisFp = false;
351
352 for( FP_3DMODEL& model : fp->Models() )
353 {
354 auto it = replacements.find( model.m_Filename );
355
356 if( it == replacements.end() )
357 continue;
358
359 if( !changedThisFp )
360 {
361 commit.Modify( fp );
362 changedThisFp = true;
363 }
364
365 model.m_Filename = it->second;
366 ++rewritten;
367 }
368 }
369
370 if( !commit.Empty() )
371 commit.Push( _( "Auto-migrate 3D Model References" ) );
372
373 return rewritten;
374}
375
376
377std::map<wxString, DIALOG_MIGRATE_3D_MODELS::ENTRY>
379{
380 std::map<wxString, ENTRY> byName;
381
382 if( !aFrame )
383 return byName;
384
385 BOARD* board = aFrame->GetBoard();
387 const wxString projectPath = aFrame->Prj().GetProjectPath();
388
389 if( !board || !resolver )
390 return byName;
391
392 for( FOOTPRINT* fp : board->Footprints() )
393 {
394 for( const FP_3DMODEL& model : fp->Models() )
395 {
396 const wxString& fn = model.m_Filename;
397
398 if( fn.IsEmpty() || !MODEL_SUBSTITUTION::IsWrlExtension( fn ) )
399 continue;
400
401 if( byName.count( fn ) )
402 continue;
403
404 std::vector<const EMBEDDED_FILES*> embeddedFilesStack;
405 embeddedFilesStack.push_back( fp->GetEmbeddedFiles() );
406 embeddedFilesStack.push_back( board->GetEmbeddedFiles() );
407
408 if( !resolver->ResolvePath( fn, projectPath, embeddedFilesStack ).IsEmpty() )
409 continue;
410
411 ENTRY& e = byName[fn];
412 e.m_fp = fp;
413 e.m_xform.m_scale = model.m_Scale;
414 e.m_xform.m_rotation = model.m_Rotation;
415 e.m_xform.m_offset = model.m_Offset;
416 e.m_xform.m_opacity = model.m_Opacity;
417 }
418 }
419
420 return byName;
421}
422
423
425{
426 if( m_missing.empty() )
427 return;
428
429 m_catalog.clear();
430 m_scannedDirs.clear();
431
432 wxBusyCursor busy;
433
435
436 // Standard KiCad 3D search paths (resolved to absolute directories). GetKicadPaths
437 // returns variable names, not filesystem paths, so walk the resolver's search-path list
438 // directly and consume m_Pathexp (the expanded absolute directory).
439 if( resolver )
440 {
441 if( const std::list<SEARCH_PATH>* searchPaths = resolver->GetPaths() )
442 {
443 for( const SEARCH_PATH& sp : *searchPaths )
444 {
445 if( !sp.m_Pathexp.IsEmpty() )
446 scanDirectory( sp.m_Pathexp );
447 }
448 }
449 }
450
451 // Project-local 3D model directory, when present.
452 wxFileName prj3D( m_frame->Prj().GetProjectPath(), wxEmptyString );
453 prj3D.AppendDir( wxS( "3dshapes" ) );
454
455 if( prj3D.DirExists() )
456 scanDirectory( prj3D.GetPath() );
457
458 // User-added extra directories, persisted across sessions.
460
461 if( common )
462 {
463 for( const wxString& dir : common->m_Extra3DSearchDirs )
464 scanDirectory( dir );
465 }
466}
467
468
469void DIALOG_MIGRATE_3D_MODELS::scanDirectory( const wxString& aDir )
470{
471 if( aDir.IsEmpty() )
472 return;
473
474 wxFileName normFn( aDir, wxEmptyString );
475 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
476 const wxString key = normFn.GetPath().Lower();
477
478 if( !m_scannedDirs.insert( key ).second )
479 return; // already scanned
480
481 if( !wxDir::Exists( normFn.GetPath() ) )
482 return;
483
484 wxArrayString files;
485 CollectFilesLoopSafe( normFn.GetPath(), files, wxEmptyString, wxDIR_FILES | wxDIR_DIRS );
486
487 // Collect duplicate-detection set by absolute-path key so re-scanning
488 // the same tree (e.g. through a different env var alias) doesn't add
489 // duplicate catalog entries.
490 std::unordered_set<wxString> existing;
491
492 for( const CATALOG_ENTRY& e : m_catalog )
493 existing.insert( e.m_absPath.Lower() );
494
495 for( const wxString& f : files )
496 {
497 const wxString lower = f.Lower();
498 bool acceptable = false;
499
500 for( const wxString& ext : stepExtensions() )
501 {
502 if( lower.length() > ext.length() + 1
503 && lower.Right( ext.length() + 1 ) == wxS( "." ) + ext )
504 {
505 acceptable = true;
506 break;
507 }
508 }
509
510 if( !acceptable )
511 continue;
512
513 if( existing.count( lower ) )
514 continue;
515
516 existing.insert( lower );
517
518 CATALOG_ENTRY entry;
519 entry.m_absPath = f;
520 entry.m_stem = normalizeStem( wxFileName( f ).GetFullName() );
521 entry.m_parent = parentDirName( f );
522 m_catalog.push_back( entry );
523 }
524}
525
526
527std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
528DIALOG_MIGRATE_3D_MODELS::rankCandidatesFor( const wxString& aWrlFilename ) const
529{
530 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
531 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
532
533 std::vector<MATCH_CANDIDATE> ranked;
534 ranked.reserve( std::min<size_t>( m_catalog.size(), 64 ) );
535
536 for( const CATALOG_ENTRY& entry : m_catalog )
537 {
538 int score = 0;
539
540 if( entry.m_stem == wrlStem )
541 {
542 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
543 ? 1000
544 : 800;
545 }
546 else
547 {
548 const int dist = levenshtein( entry.m_stem, wrlStem );
549 const int maxDist = static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
550
551 if( dist <= maxDist )
552 score = 500 - dist * 10;
553 }
554
555 if( score > 0 )
556 {
557 MATCH_CANDIDATE cand;
558 cand.m_absPath = entry.m_absPath;
559 cand.m_display = wxFileName( entry.m_absPath ).GetFullName() + wxS( " \u2014 " ) + entry.m_parent;
560 cand.m_score = score;
561 ranked.push_back( cand );
562 }
563 }
564
565 std::sort( ranked.begin(), ranked.end(),
566 []( const MATCH_CANDIDATE& a, const MATCH_CANDIDATE& b )
567 {
568 if( a.m_score != b.m_score )
569 return a.m_score > b.m_score;
570
571 return a.m_display.CmpNoCase( b.m_display ) < 0;
572 } );
573
574 constexpr size_t kMaxCandidates = 15;
575
576 if( ranked.size() > kMaxCandidates )
577 ranked.resize( kMaxCandidates );
578
579 // Always append "keep existing" at the bottom so the user has a way
580 // to explicitly decline a replacement for this one row.
581 MATCH_CANDIDATE keep;
582 keep.m_absPath = wxEmptyString;
583 keep.m_display = _( "(keep existing reference)" );
584 keep.m_score = 0;
585 ranked.push_back( keep );
586
587 return ranked;
588}
589
590
592{
593 for( size_t i = 0; i < m_missing.size(); ++i )
595}
596
597
599{
600 m_missingList->DeleteAllItems();
601
602 for( size_t i = 0; i < m_missing.size(); ++i )
603 {
604 const long row = m_missingList->InsertItem( static_cast<long>( i ), m_missing[i] );
605 (void) row;
606 updateMissingItemStyle( static_cast<int>( i ) );
607 }
608}
609
610
612{
613 if( aMissingIndex < 0 || aMissingIndex >= static_cast<int>( m_missing.size() ) )
614 return;
615
616 wxFont font = m_missingList->GetFont();
617 font.SetWeight( m_selectedPerMissing[aMissingIndex] < 0 ? wxFONTWEIGHT_BOLD
618 : wxFONTWEIGHT_NORMAL );
619 m_missingList->SetItemFont( aMissingIndex, font );
620}
621
622
624{
625 m_candidatesList->DeleteAllItems();
626
627 if( aMissingIndex < 0 || aMissingIndex >= static_cast<int>( m_missing.size() ) )
628 return;
629
630 const std::vector<MATCH_CANDIDATE>& candidates = m_candidatesPerMissing[aMissingIndex];
631
632 for( size_t i = 0; i < candidates.size(); ++i )
633 m_candidatesList->InsertItem( static_cast<long>( i ), candidates[i].m_display );
634
635 const int sel = m_selectedPerMissing[aMissingIndex];
636
637 if( sel >= 0 && sel < static_cast<int>( candidates.size() ) )
638 {
639 m_candidatesList->SetItemState( sel, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
640 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
641 showPreview( aMissingIndex, candidates[sel].m_absPath );
642 }
643 else if( !candidates.empty() )
644 {
645 // No committed choice yet (only the "keep existing" row is available).
646 // Highlight the top row for visual continuity; the preview still shows
647 // the footprint on the dummy board so the user sees the landing pad
648 // even when no replacement has been chosen.
649 m_candidatesList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
650 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
651 showPreview( aMissingIndex, candidates.front().m_absPath );
652 }
653}
654
655
657{
658 m_dummyBoard = new BOARD();
659 m_dummyBoard->SetProject( &m_frame->Prj(), true );
660 m_dummyBoard->SetEmbeddedFilesDelegate( m_frame->GetBoard() );
661 m_dummyBoard->SetBoardUse( BOARD_USE::FPHOLDER );
662
663 BOARD_DESIGN_SETTINGS& dummyBoard_bds = m_dummyBoard->GetDesignSettings();
664 dummyBoard_bds.SetBoardThickness( m_frame->GetDesignSettings().GetBoardThickness() );
665 dummyBoard_bds.SetEnabledLayers( LSET::FrontMask() | LSET::BackMask() );
666
667 BOARD_STACKUP& stackup = dummyBoard_bds.GetStackupDescriptor();
668 stackup.RemoveAll();
669 stackup.BuildDefaultStackupList( &dummyBoard_bds, 2 );
670
671 m_boardAdapter.SetBoard( m_dummyBoard );
672 m_boardAdapter.m_IsBoardView = false;
673 m_boardAdapter.m_IsPreviewer = true;
674
676 m_boardAdapter.m_Cfg = cfg;
677
681
682 m_previewPanel->GetSizer()->Add( m_previewPane, 1, wxEXPAND, 0 );
683 m_previewPanel->Layout();
684}
685
686
687void DIALOG_MIGRATE_3D_MODELS::showPreview( int aMissingIndex, const wxString& aCandAbsPath )
688{
689 if( !m_previewPane || !m_dummyBoard )
690 return;
691
692 // Swap out whatever footprint is currently on the dummy board. Cloning
693 // the representative footprint each time is cheap and avoids lingering
694 // state (pads, silk, ...) from previously-selected rows.
695 if( m_dummyFootprint )
696 {
698 delete m_dummyFootprint;
699 m_dummyFootprint = nullptr;
700 }
701
702 const FOOTPRINT* repFp = nullptr;
703
704 if( aMissingIndex >= 0 && aMissingIndex < static_cast<int>( m_missingRepFp.size() ) )
705 repFp = m_missingRepFp[aMissingIndex];
706
707 if( repFp )
708 {
709 m_dummyFootprint = new FOOTPRINT( *repFp );
710 m_dummyFootprint->SetParentGroup( nullptr );
711
712 // Normalize orientation the same way the footprint-properties preview
713 // does so the model lands face-up regardless of how the real footprint
714 // is placed on the user's board.
715 if( m_dummyFootprint->IsFlipped() )
717
718 m_dummyFootprint->SetOrientation( ANGLE_0 );
719
720 // Replace the 3D model list with just the candidate (if any), carrying
721 // over the transform captured from the original WRL entry so the user
722 // sees the candidate sitting in the same spot the WRL would have.
723 m_dummyFootprint->Models().clear();
724
725 if( !aCandAbsPath.IsEmpty() )
726 {
727 FP_3DMODEL replacement;
728 replacement.m_Filename = aCandAbsPath;
729 replacement.m_Scale = m_missingXform[aMissingIndex].m_scale;
730 replacement.m_Rotation = m_missingXform[aMissingIndex].m_rotation;
731 replacement.m_Offset = m_missingXform[aMissingIndex].m_offset;
732 replacement.m_Opacity = m_missingXform[aMissingIndex].m_opacity;
733 replacement.m_Show = true;
734 m_dummyFootprint->Models().push_back( replacement );
735 }
736
738 }
739
740 m_previewPane->ReloadRequest();
741 m_previewPane->Request_refresh();
742}
743
744
745static int getSelectedRow( wxListCtrl* aList )
746{
747 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
748}
749
750
752{
753 populateCandidatesList( static_cast<int>( aEvent.GetIndex() ) );
754}
755
756
758{
759 const int missingIdx = getSelectedRow( m_missingList );
760 const int candidateIdx = static_cast<int>( aEvent.GetIndex() );
761
762 if( missingIdx < 0 || missingIdx >= static_cast<int>( m_missing.size() ) )
763 return;
764
765 const std::vector<MATCH_CANDIDATE>& candidates = m_candidatesPerMissing[missingIdx];
766
767 if( candidateIdx < 0 || candidateIdx >= static_cast<int>( candidates.size() ) )
768 return;
769
770 const MATCH_CANDIDATE& c = candidates[candidateIdx];
771
772 // Record this row's choice. The "keep existing" row has an empty path.
773 m_selectedPerMissing[missingIdx] = c.m_absPath.IsEmpty() ? KEEP_EXISTING : candidateIdx;
774
775 // Bold the missing row again when the user explicitly chose "keep
776 // existing", so the visual cue tracks the actual selection state.
777 updateMissingItemStyle( missingIdx );
778
779 showPreview( missingIdx, c.m_absPath );
780}
781
782
784{
785 wxDirDialog dlg( this, _( "Add 3D Model Search Directory" ) );
786
787 if( dlg.ShowModal() != wxID_OK )
788 return;
789
790 const wxString chosen = dlg.GetPath();
791
792 if( chosen.IsEmpty() )
793 return;
794
795 m_searchDirsGrid->AppendRows( 1 );
796 m_searchDirsGrid->SetCellValue( m_searchDirsGrid->GetNumberRows() - 1, 0, chosen );
797
798 // Persist the directory so future sessions include it automatically.
799 if( COMMON_SETTINGS* common = Pgm().GetCommonSettings() )
800 {
801 std::vector<wxString>& dirs = common->m_Extra3DSearchDirs;
802
803 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
804 dirs.push_back( chosen );
805 }
806
807 // Scan just the new directory; catalog dedup ensures no duplicates.
808 {
809 wxBusyCursor busy;
810 scanDirectory( chosen );
811 }
812
814}
815
816
818{
819 // Re-rank every row against the expanded catalog and refresh the view,
820 // preserving the user's existing per-row selections by filename.
821 std::vector<wxString> priorSelectionPaths( m_missing.size() );
822
823 for( size_t i = 0; i < m_missing.size(); ++i )
824 {
825 const int sel = m_selectedPerMissing[i];
826
827 if( sel >= 0 && sel < static_cast<int>( m_candidatesPerMissing[i].size() ) )
828 priorSelectionPaths[i] = m_candidatesPerMissing[i][sel].m_absPath;
829 }
830
832
833 for( size_t i = 0; i < m_missing.size(); ++i )
834 {
836 continue;
837
839 const std::vector<MATCH_CANDIDATE>& candidates = m_candidatesPerMissing[i];
840
841 // Preserve the user's prior explicit pick when the same file still
842 // appears in the catalog.
843 if( !priorSelectionPaths[i].IsEmpty() )
844 {
845 for( size_t j = 0; j < candidates.size(); ++j )
846 {
847 if( candidates[j].m_absPath == priorSelectionPaths[i] )
848 {
849 m_selectedPerMissing[i] = static_cast<int>( j );
850 break;
851 }
852 }
853 }
854
855 // For rows with no prior pick (or whose pick disappeared), auto-select
856 // the newly-best candidate so adding a directory immediately un-bolds
857 // rows that just acquired a plausible match.
858 if( m_selectedPerMissing[i] == UNSPECIFIED && !candidates.empty()
859 && !candidates.front().m_absPath.IsEmpty() )
860 {
862 }
863
864 updateMissingItemStyle( static_cast<int>( i ) );
865 }
866
867 const int activeRow = getSelectedRow( m_missingList );
868 populateCandidatesList( activeRow );
869}
870
871
873{
874 wxString deleted;
875
876 m_searchDirsGrid->OnDeleteRows(
877 [&]( int row )
878 {
879 deleted = m_searchDirsGrid->GetCellValue( row, 0 );
880 m_searchDirsGrid->DeleteRows( row );
881 } );
882
883 // Persist the directory so future sessions include it automatically.
884 if( COMMON_SETTINGS* common = Pgm().GetCommonSettings() )
885 {
886 std::vector<wxString>& dirs = common->m_Extra3DSearchDirs;
887
888 std::erase_if( dirs,
889 [&]( const wxString& dir )
890 {
891 return dir == deleted;
892 } );
893 }
894
895 buildCatalog();
897}
898
899
901{
902 const int missingIdx = getSelectedRow( m_missingList );
903
904 if( missingIdx < 0 || missingIdx >= static_cast<int>( m_missing.size() ) )
905 return;
906
907 wxString wildcard;
908
909 for( const wxString& ext : stepExtensions() )
910 {
911 if( !wildcard.IsEmpty() )
912 wildcard += wxS( ";" );
913
914 wildcard += wxS( "*." ) + ext;
915 }
916
917 wxFileDialog dlg( this, _( "Select 3D Model File" ), wxEmptyString, wxEmptyString,
918 _( "3D Models" ) + wxS( " (" ) + wildcard + wxS( ")|" ) + wildcard,
919 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
920
921 if( dlg.ShowModal() != wxID_OK )
922 return;
923
924 const wxString chosenPath = dlg.GetPath();
925
926 if( chosenPath.IsEmpty() )
927 return;
928
929 // Inject the chosen file as a top-ranked candidate for this row so the
930 // user can see it previewed and it will be applied when they hit Replace.
931 MATCH_CANDIDATE injected;
932 injected.m_absPath = chosenPath;
933 injected.m_display = wxFileName( chosenPath ).GetFullName() + wxS( " \u2014 " ) + _( "user-selected" );
934 injected.m_score = 2000;
935
936 std::vector<MATCH_CANDIDATE>& candidates = m_candidatesPerMissing[missingIdx];
937
938 // Avoid duplicating an already-present entry.
939 auto existing = std::find_if( candidates.begin(), candidates.end(),
940 [&]( const MATCH_CANDIDATE& c )
941 {
942 return c.m_absPath == chosenPath;
943 } );
944
945 if( existing == candidates.end() )
946 {
947 // Put new candidate at the top and select it
948 candidates.insert( candidates.begin(), injected );
949 m_selectedPerMissing[missingIdx] = 0;
950 }
951 else
952 {
953 m_selectedPerMissing[missingIdx] = static_cast<int>( existing - candidates.begin() );
954 }
955
956 showPreview( missingIdx, chosenPath );
957 updateMissingItemStyle( missingIdx );
958 populateCandidatesList( missingIdx );
959}
960
961
963{
964 // Build the old->new filename map from the per-row selections. We store
965 // the new filename through FILENAME_RESOLVER::ShortenPath() so that
966 // env-var aliases like ${KICAD10_3DMODEL_DIR} survive round-trip.
968 std::map<wxString, wxString> replacements;
969
970 for( size_t i = 0; i < m_missing.size(); ++i )
971 {
972 const int sel = m_selectedPerMissing[i];
973
974 if( sel < 0 || sel >= static_cast<int>( m_candidatesPerMissing[i].size() ) )
975 continue;
976
977 const MATCH_CANDIDATE& c = m_candidatesPerMissing[i][sel];
978
979 if( c.m_absPath.IsEmpty() )
980 continue; // "(keep existing)"
981
982 wxString stored = c.m_absPath;
983
984 if( resolver )
985 stored = resolver->ShortenPath( c.m_absPath );
986
987 replacements[m_missing[i]] = stored;
988 }
989
990 if( replacements.empty() )
991 {
992 // User clicked Replace without choosing anything; treat as Keep.
993 EndModal( wxID_CANCEL );
994 return;
995 }
996
997 BOARD* board = m_frame->GetBoard();
998 BOARD_COMMIT commit( m_frame );
999
1000 for( FOOTPRINT* fp : board->Footprints() )
1001 {
1002 bool changedThisFp = false;
1003
1004 for( FP_3DMODEL& model : fp->Models() )
1005 {
1006 auto it = replacements.find( model.m_Filename );
1007
1008 if( it == replacements.end() )
1009 continue;
1010
1011 if( !changedThisFp )
1012 {
1013 commit.Modify( fp );
1014 changedThisFp = true;
1015 }
1016
1017 model.m_Filename = it->second;
1018 }
1019 }
1020
1021 commit.Push( _( "Migrate 3D Model References" ) );
1022
1023 EndModal( wxID_OK );
1024}
1025
1026
1028{
1029 EndModal( wxID_CANCEL );
1030}
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
@ FPHOLDER
Definition board.h:403
#define RANGE_SCALE_3D
This defines the range that all coord will have to be rendered.
void Push(const wxString &aMessage=wxEmptyString, int aCommitFlags=0) override
Execute the changes.
Container for design settings for a BOARD object.
void SetEnabledLayers(const LSET &aMask)
Change the bit-mask of enabled layers to aMask.
BOARD_STACKUP & GetStackupDescriptor()
void SetBoardThickness(int aThickness)
Manage layers needed to make a physical board.
void RemoveAll()
Delete all items in list and clear the list.
void BuildDefaultStackupList(const BOARD_DESIGN_SETTINGS *aSettings, int aActiveCopperLayersCount=0)
Create a default stackup, according to the current BOARD_DESIGN_SETTINGS settings.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:411
EMBEDDED_FILES * GetEmbeddedFiles() override
Definition board.cpp:3662
const FOOTPRINTS & Footprints() const
Definition board.h:465
bool Empty() const
Definition commit.h:145
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
Definition commit.h:102
std::vector< wxString > m_Extra3DSearchDirs
Extra directories to search for 3D models, added by the user through the 3D model migration dialog.
DIALOG_MIGRATE_3D_MODELS_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Migrate 3D Models"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
static int AutoMigrateByFilename(PCB_EDIT_FRAME *aFrame)
Silently rewrite unresolvable .wrl/.wrz references to STEP files whose filename stem matches in the s...
void OnKeepClick(wxCommandEvent &event) override
std::vector< int > m_selectedPerMissing
Which candidate index the user has selected for each missing entry.
std::vector< CATALOG_ENTRY > m_catalog
Catalog of STEP-compatible model paths, deduped by absolute path.
DIALOG_MIGRATE_3D_MODELS(PCB_EDIT_FRAME *aFrame)
std::vector< wxString > m_missing
Unique WRL filenames (as stored in FP_3DMODEL::m_Filename, i.e.
static bool BoardHasUnresolvedWrlReferences(PCB_EDIT_FRAME *aFrame)
Cheap precheck that avoids constructing the dialog (and its embedded 3D canvas) when the board has no...
void populateCandidatesList(int aMissingIndex)
void updateMissingItemStyle(int aMissingIndex)
Apply bold styling to the missing-list row iff no replacement is currently selected (m_selectedPerMis...
void OnOpenExternalFileClick(wxCommandEvent &event) override
void OnAddSearchDirectoryClick(wxCommandEvent &event) override
std::vector< std::vector< MATCH_CANDIDATE > > m_candidatesPerMissing
Ranked candidates per missing filename, keyed by index into m_missing.
std::vector< MATCH_CANDIDATE > rankCandidatesFor(const wxString &aWrlFilename) const
void initPreviewBoard()
Set up the throwaway board/footprint that the preview canvas renders.
void showPreview(int aMissingIndex, const wxString &aCandAbsPath)
Replace the preview's footprint and model list to reflect the current missing-list selection + candid...
std::vector< MISSING_XFORM > m_missingXform
Source FP_3DMODEL transform per missing filename, used when building the candidate model for preview.
BOARD * m_dummyBoard
Preview pipeline.
void OnRemoveSearchDirectoryClick(wxCommandEvent &event) override
void OnCandidateSelected(wxListEvent &event) override
void scanDirectory(const wxString &aDir)
static int CountUnresolvedWrlReferences(PCB_EDIT_FRAME *aFrame)
Count of unique unresolvable .wrl/.wrz references on the board (deduplicated by filename).
FOOTPRINT * m_dummyFootprint
Owned by m_dummyBoard.
static std::map< wxString, ENTRY > collectMissingWrlModels(PCB_EDIT_FRAME *aFrame)
std::vector< const FOOTPRINT * > m_missingRepFp
Representative footprint per missing filename.
void OnMissingSelected(wxListEvent &event) override
void OnReplaceClick(wxCommandEvent &event) override
std::set< wxString > m_scannedDirs
Set of already-scanned directories (absolute path, case-normalised).
void SetupStandardButtons(std::map< int, wxString > aLabels={})
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
Implement a canvas based on a wxGLCanvas.
Provide an extensible class to resolve 3D model paths.
wxString ResolvePath(const wxString &aFileName, const wxString &aWorkingPath, std::vector< const EMBEDDED_FILES * > aEmbeddedFilesStack)
Determine the full path of the given file name.
const std::list< SEARCH_PATH > * GetPaths() const
Return a pointer to the internal path list; the items in:load.
wxString ShortenPath(const wxString &aFullPathName)
Produce a relative path based on the existing search directories or returns the same path if the path...
VECTOR3D m_Offset
3D model offset (mm)
Definition footprint.h:134
double m_Opacity
Definition footprint.h:135
VECTOR3D m_Rotation
3D model rotation (degrees)
Definition footprint.h:133
VECTOR3D m_Scale
3D model scaling factor (dimensionless)
Definition footprint.h:132
wxString m_Filename
The 3D shape filename in 3D library.
Definition footprint.h:136
bool m_Show
Include model in rendering.
Definition footprint.h:137
PROJECT & Prj() const
Return a reference to the PROJECT associated with this KIWAY.
static const LSET & FrontMask()
Return a mask holding all technical layers and the external CU layer on front side.
Definition lset.cpp:718
static const LSET & BackMask()
Return a mask holding all technical layers and the external CU layer on back side.
Definition lset.cpp:725
An index of STEP-family model files keyed by normalised filename stem.
wxString FindMatchFor(const wxString &aMissingWrl) const
Look up the best STEP-family replacement for a missing WRL reference.
void Build(const wxString &aProjectPath, const FILENAME_RESOLVER *aResolver)
Walk the resolver's search paths, the project's 3dshapes/ subdirectory, and the user's COMMON_SETTING...
static const wxGLAttributes GetAttributesList(ANTIALIASING_MODE aAntiAliasingMode, bool aAlpha=false)
Get a list of attributes to pass to wxGLCanvas.
BOARD * GetBoard() const
The main frame for Pcbnew.
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:562
static S3D_CACHE * Get3DCacheManager(PROJECT *aProject, bool updateProjDir=false)
Return a pointer to an instance of the 3D cache manager.
static FILENAME_RESOLVER * Get3DFilenameResolver(PROJECT *aProject)
Accessor for 3D path resolver.
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:276
static int getSelectedRow(wxListCtrl *aList)
#define UNSPECIFIED
#define KEEP_EXISTING
#define _(s)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:448
void CollectFilesLoopSafe(const wxString &aRoot, wxArrayString &aFiles, const wxString &aFileSpec, int aFlags)
Recursively collect every file under aRoot, deduplicating subdirectories by their resolved path.
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
Definition mirror.h:25
KICOMMON_API wxFont GetInfoFont(wxWindow *aWindow)
KICOMMON_API wxFont GetControlFont(wxWindow *aWindow)
bool IsWrlExtension(const wxString &aFilename)
True iff aFilename ends in .wrl or .wrz (case-insensitive).
Declaration of the cogl_att_list class.
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
T * GetAppSettings(const char *aFilename)
An entry in the STEP catalog. m_absPath is the deduplication key.
wxString m_parent
Parent directory basename (e.g. "Resistor_SMD.3dshapes")
wxString m_absPath
Absolute on-disk path.
wxString m_stem
Filename without directory or extension.
A ranked candidate shown in column 2. A score of 0 means "keep existing".
wxString m_absPath
Absolute path, or empty for the "keep existing" row.
KIBIS_MODEL * model
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)