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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 */
22
24
25#include <algorithm>
26#include <unordered_set>
27
28#include <wx/utils.h>
29#include <wx/dir.h>
30#include <wx/dirdlg.h>
31#include <wx/filedlg.h>
32#include <wx/filename.h>
33
34#include <board.h>
35#include <board_commit.h>
38#include <footprint.h>
39#include <lset.h>
40#include <pcb_edit_frame.h>
41#include <pgm_base.h>
42#include <project.h>
43#include <project_pcb.h>
46
47#include <3d_cache/3d_cache.h>
52#include <filename_resolver.h>
53#include <gestfich.h>
54
55
56namespace
57{
62const std::vector<wxString>& stepExtensions()
63{
64 static const std::vector<wxString> exts = {
65 wxS( "step" ), wxS( "stp" ), wxS( "stpz" ),
66 wxS( "step.gz" ), wxS( "stp.gz" ),
67 wxS( "iges" ), wxS( "igs" )
68 };
69 return exts;
70}
71
72
78wxString normalizeStem( const wxString& aName )
79{
80 wxString stem = aName;
81
82 // Strip known extensions (case-insensitive), including doubled-up .gz
83 // forms that don't round-trip through wxFileName::GetName.
84 static const wxString stripList[] = {
85 wxS( ".step.gz" ), wxS( ".stp.gz" ),
86 wxS( ".wrl" ), wxS( ".wrz" ),
87 wxS( ".step" ), wxS( ".stp" ), wxS( ".stpz" ),
88 wxS( ".iges" ), wxS( ".igs" )
89 };
90
91 for( const wxString& ext : stripList )
92 {
93 if( stem.length() > ext.length()
94 && stem.Right( ext.length() ).Lower() == ext )
95 {
96 stem = stem.Left( stem.length() - ext.length() );
97 break;
98 }
99 }
100
101 stem.MakeLower();
102
103 // Treat separator characters as interchangeable so that e.g.
104 // "R_0603_1608Metric" and "R-0603-1608Metric" collide.
105 stem.Replace( wxS( "-" ), wxS( "_" ) );
106 stem.Replace( wxS( " " ), wxS( "_" ) );
107
108 return stem;
109}
110
111
113int levenshtein( const wxString& a, const wxString& b )
114{
115 const size_t m = a.length();
116 const size_t n = b.length();
117
118 if( m == 0 )
119 return static_cast<int>( n );
120
121 if( n == 0 )
122 return static_cast<int>( m );
123
124 std::vector<int> prev( n + 1 );
125 std::vector<int> curr( n + 1 );
126
127 for( size_t j = 0; j <= n; ++j )
128 prev[j] = static_cast<int>( j );
129
130 for( size_t i = 1; i <= m; ++i )
131 {
132 curr[0] = static_cast<int>( i );
133
134 for( size_t j = 1; j <= n; ++j )
135 {
136 int cost = ( a[i - 1] == b[j - 1] ) ? 0 : 1;
137 curr[j] = std::min( { curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost } );
138 }
139
140 std::swap( prev, curr );
141 }
142
143 return prev[n];
144}
145
146
149wxString parentDirName( const wxString& aPath )
150{
151 wxFileName fn( aPath );
152 const wxArrayString& dirs = fn.GetDirs();
153 return dirs.empty() ? wxString() : dirs.Last();
154}
155
156
160wxString parentDirFromFilename( const wxString& aFilename )
161{
162 return parentDirName( aFilename );
163}
164
165} // anonymous namespace
166
167
170 m_frame( aFrame ),
171 m_dummyBoard( nullptr ),
172 m_dummyFootprint( nullptr ),
175 m_previewPane( nullptr )
176{
177 // Single column spanning the full list-control width; wxLC_REPORT needs
178 // at least one column to show anything, wxLC_NO_HEADER hides the header.
179 m_missingList->AppendColumn( wxEmptyString, wxLIST_FORMAT_LEFT, 800 );
180 m_candidatesList->AppendColumn( wxEmptyString, wxLIST_FORMAT_LEFT, 800 );
181
182 // Keep the single column filling the visible width as the splitter sash
183 // moves. Bare wxListCtrl in report mode clips overflowing text cleanly.
184 auto fitColumn = []( wxListCtrl* aList )
185 {
186 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
187 aList->SetColumnWidth( 0, width );
188 };
189
190 m_missingList->Bind( wxEVT_SIZE,
191 [this, fitColumn]( wxSizeEvent& aEvt )
192 {
193 fitColumn( m_missingList );
194 aEvt.Skip();
195 } );
196
197 m_candidatesList->Bind( wxEVT_SIZE,
198 [this, fitColumn]( wxSizeEvent& aEvt )
199 {
200 fitColumn( m_candidatesList );
201 aEvt.Skip();
202 } );
203
205 buildCatalog();
207
208 // Build the throwaway board + footprint and the 3D canvas. Must happen
209 // after collectMissingModels() so we can size the preview using whichever
210 // representative footprint is first shown.
212
213 // Auto-select the top real (non-"keep existing") candidate for every
214 // missing row so the user sees a sensible default without having to
215 // click through each one. Rows for which no real candidate exists get
216 // bolded by populateMissingList() as a visual attention marker.
217 for( size_t i = 0; i < m_missing.size(); ++i )
218 {
219 const std::vector<MATCH_CANDIDATE>& cands = m_candidatesPerMissing[i];
220
221 if( !cands.empty() && !cands.front().m_absPath.IsEmpty() )
223 }
224
226
227 if( !m_missing.empty() )
228 {
229 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
230 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
232 }
233
236
237 // Position the splitter sashes so the three columns start out at roughly
238 // one third of the dialog width each. Must happen after the dialog has
239 // been laid out at its final initial size.
240 const int clientWidth = m_mainSplitter->GetClientSize().GetWidth();
241
242 if( clientWidth > 360 )
243 {
244 const int outerSash = clientWidth / 3;
245 m_mainSplitter->SetSashPosition( outerSash );
246
247 const int innerWidth = std::max( clientWidth - outerSash, 240 );
248 m_innerSplitter->SetSashPosition( innerWidth / 2 );
249 }
250}
251
252
254{
255 const wxSize screenSize = wxGetDisplaySize();
256 const int hardCap = ( screenSize.GetWidth() * 9 ) / 10;
257
258 // Soft cap (0.9 * parent width) only applies on the first time the
259 // dialog is opened against a given user settings file. We detect that
260 // by comparing the current width to the hardcoded base-class default:
261 // if finishDialogSettings() restored a saved value the width won't
262 // match the default.
263 constexpr int kBaseDefaultWidth = 900;
264 int cap = hardCap;
265
266 if( m_frame && GetSize().GetWidth() == kBaseDefaultWidth )
267 {
268 const int parentCap = ( m_frame->GetSize().GetWidth() * 9 ) / 10;
269 cap = std::min( cap, parentCap );
270 }
271
272 if( GetSize().GetWidth() > cap )
273 {
274 wxSize sized = GetSize();
275 sized.SetWidth( std::max( cap, GetMinSize().GetWidth() ) );
276 SetSize( sized );
277 Centre( wxBOTH );
278 }
279}
280
281
283{
284 delete m_previewPane;
285 delete m_dummyBoard; // owns m_dummyFootprint
286}
287
288
290{
291 if( !aFrame )
292 return false;
293
294 BOARD* board = aFrame->GetBoard();
296
297 if( !board || !resolver )
298 return false;
299
300 const wxString projectPath = aFrame->Prj().GetProjectPath();
301
302 for( FOOTPRINT* fp : board->Footprints() )
303 {
304 for( const FP_3DMODEL& model : fp->Models() )
305 {
306 const wxString& fn = model.m_Filename;
307
308 if( fn.IsEmpty() || !MODEL_SUBSTITUTION::IsWrlExtension( fn ) )
309 continue;
310
311 if( resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
312 return true;
313 }
314 }
315
316 return false;
317}
318
319
321{
322 if( !aFrame )
323 return 0;
324
325 BOARD* board = aFrame->GetBoard();
327
328 if( !board || !resolver )
329 return 0;
330
331 const wxString projectPath = aFrame->Prj().GetProjectPath();
332 std::unordered_set<wxString> unique;
333
334 for( FOOTPRINT* fp : board->Footprints() )
335 {
336 for( const FP_3DMODEL& model : fp->Models() )
337 {
338 const wxString& fn = model.m_Filename;
339
340 if( fn.IsEmpty() || !MODEL_SUBSTITUTION::IsWrlExtension( fn ) )
341 continue;
342
343 if( unique.count( fn ) )
344 continue;
345
346 if( resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
347 unique.insert( fn );
348 }
349 }
350
351 return static_cast<int>( unique.size() );
352}
353
354
356{
357 if( !aFrame )
358 return 0;
359
360 BOARD* board = aFrame->GetBoard();
362
363 if( !board || !resolver )
364 return 0;
365
366 const wxString projectPath = aFrame->Prj().GetProjectPath();
367
369 bool catalogBuilt = false;
370
371 std::map<wxString, wxString> replacements;
372 std::unordered_set<wxString> seen;
373
374 for( FOOTPRINT* fp : board->Footprints() )
375 {
376 for( const FP_3DMODEL& model : fp->Models() )
377 {
378 const wxString& fn = model.m_Filename;
379
380 if( fn.IsEmpty() || !MODEL_SUBSTITUTION::IsWrlExtension( fn ) )
381 continue;
382
383 if( !seen.insert( fn ).second )
384 continue;
385
386 if( !resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
387 continue;
388
389 if( !catalogBuilt )
390 {
391 catalog.Build( projectPath, resolver );
392 catalogBuilt = true;
393 }
394
395 const wxString match = catalog.FindMatchFor( fn );
396
397 if( match.IsEmpty() )
398 continue;
399
400 const wxString shortened = resolver->ShortenPath( match );
401
402 // ShortenPath returning empty would blank the footprint reference;
403 // fall back to the absolute path in that edge case.
404 replacements[fn] = shortened.IsEmpty() ? match : shortened;
405 }
406 }
407
408 if( replacements.empty() )
409 return 0;
410
411 BOARD_COMMIT commit( aFrame );
412 int rewritten = 0;
413
414 for( FOOTPRINT* fp : board->Footprints() )
415 {
416 bool changedThisFp = false;
417
418 for( FP_3DMODEL& model : fp->Models() )
419 {
420 auto it = replacements.find( model.m_Filename );
421
422 if( it == replacements.end() )
423 continue;
424
425 if( !changedThisFp )
426 {
427 commit.Modify( fp );
428 changedThisFp = true;
429 }
430
431 model.m_Filename = it->second;
432 ++rewritten;
433 }
434 }
435
436 if( rewritten > 0 )
437 commit.Push( _( "Auto-migrate 3D model references" ) );
438
439 return rewritten;
440}
441
442
444{
445 BOARD* board = m_frame->GetBoard();
447
448 if( !board || !resolver )
449 return;
450
451 const wxString projectPath = m_frame->Prj().GetProjectPath();
452
453 // Accumulate into a map keyed by WRL filename so we can sort afterwards
454 // while still tracking the representative footprint and transform captured
455 // from the first FP_3DMODEL that referenced it.
456 struct ENTRY
457 {
458 const FOOTPRINT* m_fp = nullptr;
459 MISSING_XFORM m_xform;
460 };
461 std::map<wxString, ENTRY> byName;
462
463 for( FOOTPRINT* fp : board->Footprints() )
464 {
465 for( const FP_3DMODEL& model : fp->Models() )
466 {
467 const wxString& fn = model.m_Filename;
468
469 if( fn.IsEmpty() || !MODEL_SUBSTITUTION::IsWrlExtension( fn ) )
470 continue;
471
472 if( byName.count( fn ) )
473 continue;
474
475 // Mirror the resolver invocation used by the 3D viewer: empty
476 // return means "not found on disk via any search path".
477 if( !resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
478 continue;
479
480 ENTRY& e = byName[fn];
481 e.m_fp = fp;
482 e.m_xform.m_scale = model.m_Scale;
483 e.m_xform.m_rotation = model.m_Rotation;
484 e.m_xform.m_offset = model.m_Offset;
485 e.m_xform.m_opacity = model.m_Opacity;
486 }
487 }
488
489 m_missing.reserve( byName.size() );
490 m_missingRepFp.reserve( byName.size() );
491 m_missingXform.reserve( byName.size() );
492
493 for( const auto& [fn, entry] : byName )
494 {
495 m_missing.push_back( fn );
496 m_missingRepFp.push_back( entry.m_fp );
497 m_missingXform.push_back( entry.m_xform );
498 }
499
500 m_selectedPerMissing.assign( m_missing.size(), -1 );
501 m_candidatesPerMissing.resize( m_missing.size() );
502}
503
504
506{
507 if( m_missing.empty() )
508 return;
509
510 wxBusyCursor busy;
511
513
514 // Standard KiCad 3D search paths (resolved to absolute directories). GetKicadPaths
515 // returns variable names, not filesystem paths, so walk the resolver's search-path list
516 // directly and consume m_Pathexp (the expanded absolute directory).
517 if( resolver )
518 {
519 if( const std::list<SEARCH_PATH>* searchPaths = resolver->GetPaths() )
520 {
521 for( const SEARCH_PATH& sp : *searchPaths )
522 {
523 if( !sp.m_Pathexp.IsEmpty() )
524 scanDirectory( sp.m_Pathexp );
525 }
526 }
527 }
528
529 // Project-local 3D model directory, when present.
530 wxFileName prj3D( m_frame->Prj().GetProjectPath(), wxEmptyString );
531 prj3D.AppendDir( wxS( "3dshapes" ) );
532
533 if( prj3D.DirExists() )
534 scanDirectory( prj3D.GetPath() );
535
536 // User-added extra directories, persisted across sessions.
538
539 if( common )
540 {
541 for( const wxString& dir : common->m_Extra3DSearchDirs )
542 scanDirectory( dir );
543 }
544}
545
546
547void DIALOG_MIGRATE_3D_MODELS::scanDirectory( const wxString& aDir )
548{
549 if( aDir.IsEmpty() )
550 return;
551
552 wxFileName normFn( aDir, wxEmptyString );
553 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
554 const wxString key = normFn.GetPath().Lower();
555
556 if( !m_scannedDirs.insert( key ).second )
557 return; // already scanned
558
559 if( !wxDir::Exists( normFn.GetPath() ) )
560 return;
561
562 wxArrayString files;
563 CollectFilesLoopSafe( normFn.GetPath(), files, wxEmptyString, wxDIR_FILES | wxDIR_DIRS );
564
565 // Collect duplicate-detection set by absolute-path key so re-scanning
566 // the same tree (e.g. through a different env var alias) doesn't add
567 // duplicate catalog entries.
568 std::unordered_set<wxString> existing;
569
570 for( const CATALOG_ENTRY& e : m_catalog )
571 existing.insert( e.m_absPath.Lower() );
572
573 for( const wxString& f : files )
574 {
575 const wxString lower = f.Lower();
576 bool acceptable = false;
577
578 for( const wxString& ext : stepExtensions() )
579 {
580 if( lower.length() > ext.length() + 1
581 && lower.Right( ext.length() + 1 ) == wxS( "." ) + ext )
582 {
583 acceptable = true;
584 break;
585 }
586 }
587
588 if( !acceptable )
589 continue;
590
591 if( existing.count( lower ) )
592 continue;
593
594 existing.insert( lower );
595
596 CATALOG_ENTRY entry;
597 entry.m_absPath = f;
598 entry.m_stem = normalizeStem( wxFileName( f ).GetFullName() );
599 entry.m_parent = parentDirName( f );
600 m_catalog.push_back( entry );
601 }
602}
603
604
605std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
606DIALOG_MIGRATE_3D_MODELS::rankCandidatesFor( const wxString& aWrlFilename ) const
607{
608 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
609 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
610
611 std::vector<MATCH_CANDIDATE> ranked;
612 ranked.reserve( std::min<size_t>( m_catalog.size(), 64 ) );
613
614 for( const CATALOG_ENTRY& entry : m_catalog )
615 {
616 int score = 0;
617
618 if( entry.m_stem == wrlStem )
619 {
620 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
621 ? 1000
622 : 800;
623 }
624 else
625 {
626 const int dist = levenshtein( entry.m_stem, wrlStem );
627 const int maxDist = static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
628
629 if( dist <= maxDist )
630 score = 500 - dist * 10;
631 }
632
633 if( score > 0 )
634 {
635 MATCH_CANDIDATE cand;
636 cand.m_absPath = entry.m_absPath;
637 cand.m_display = wxFileName( entry.m_absPath ).GetFullName()
638 + wxS( " \u2014 " )
639 + entry.m_parent;
640 cand.m_score = score;
641 ranked.push_back( cand );
642 }
643 }
644
645 std::sort( ranked.begin(), ranked.end(),
646 []( const MATCH_CANDIDATE& a, const MATCH_CANDIDATE& b )
647 {
648 if( a.m_score != b.m_score )
649 return a.m_score > b.m_score;
650
651 return a.m_display.CmpNoCase( b.m_display ) < 0;
652 } );
653
654 constexpr size_t kMaxCandidates = 15;
655
656 if( ranked.size() > kMaxCandidates )
657 ranked.resize( kMaxCandidates );
658
659 // Always append "keep existing" at the bottom so the user has a way
660 // to explicitly decline a replacement for this one row.
661 MATCH_CANDIDATE keep;
662 keep.m_absPath = wxEmptyString;
663 keep.m_display = _( "(keep existing reference)" );
664 keep.m_score = 0;
665 ranked.push_back( keep );
666
667 return ranked;
668}
669
670
672{
673 for( size_t i = 0; i < m_missing.size(); ++i )
675}
676
677
679{
680 m_missingList->DeleteAllItems();
681
682 for( size_t i = 0; i < m_missing.size(); ++i )
683 {
684 const long row = m_missingList->InsertItem( static_cast<long>( i ), m_missing[i] );
685 (void) row;
686 updateMissingItemStyle( static_cast<int>( i ) );
687 }
688}
689
690
692{
693 if( aMissingIndex < 0 || aMissingIndex >= static_cast<int>( m_missing.size() ) )
694 return;
695
696 wxFont font = m_missingList->GetFont();
697 font.SetWeight( m_selectedPerMissing[aMissingIndex] < 0 ? wxFONTWEIGHT_BOLD
698 : wxFONTWEIGHT_NORMAL );
699 m_missingList->SetItemFont( aMissingIndex, font );
700}
701
702
704{
705 m_candidatesList->DeleteAllItems();
706
707 if( aMissingIndex < 0 || aMissingIndex >= static_cast<int>( m_missing.size() ) )
708 return;
709
710 const std::vector<MATCH_CANDIDATE>& cands = m_candidatesPerMissing[aMissingIndex];
711
712 for( size_t i = 0; i < cands.size(); ++i )
713 m_candidatesList->InsertItem( static_cast<long>( i ), cands[i].m_display );
714
715 const int sel = m_selectedPerMissing[aMissingIndex];
716
717 if( sel >= 0 && sel < static_cast<int>( cands.size() ) )
718 {
719 m_candidatesList->SetItemState( sel, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
720 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
721 showPreview( aMissingIndex, cands[sel].m_absPath );
722 }
723 else if( !cands.empty() )
724 {
725 // No committed choice yet (only the "keep existing" row is available).
726 // Highlight the top row for visual continuity; the preview still shows
727 // the footprint on the dummy board so the user sees the landing pad
728 // even when no replacement has been chosen.
729 m_candidatesList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
730 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
731 showPreview( aMissingIndex, cands.front().m_absPath );
732 }
733}
734
735
737{
738 m_dummyBoard = new BOARD();
739 m_dummyBoard->SetProject( &m_frame->Prj(), true );
740 m_dummyBoard->SetEmbeddedFilesDelegate( m_frame->GetBoard() );
741 m_dummyBoard->SetBoardUse( BOARD_USE::FPHOLDER );
742
743 BOARD_DESIGN_SETTINGS& dummyBds = m_dummyBoard->GetDesignSettings();
744 const BOARD_DESIGN_SETTINGS& parentBds = m_frame->GetDesignSettings();
745 dummyBds.SetBoardThickness( parentBds.GetBoardThickness() );
747
748 BOARD_STACKUP& stackup = dummyBds.GetStackupDescriptor();
749 stackup.RemoveAll();
750 stackup.BuildDefaultStackupList( &dummyBds, 2 );
751
752 m_boardAdapter.SetBoard( m_dummyBoard );
753 m_boardAdapter.m_IsBoardView = false;
754 m_boardAdapter.m_IsPreviewer = true;
755
757 {
758 m_boardAdapter.m_Cfg = cfg;
759
760 // Same rationale as PANEL_PREVIEW_3D_MODEL: placeholder models are
761 // noisy in a candidate picker.
762 cfg->m_Render.show_missing_models = false;
763 }
764
768
769 m_previewPanel->GetSizer()->Add( m_previewPane, 1, wxEXPAND, 0 );
770 m_previewPanel->Layout();
771}
772
773
774void DIALOG_MIGRATE_3D_MODELS::showPreview( int aMissingIndex, const wxString& aCandAbsPath )
775{
776 if( !m_previewPane || !m_dummyBoard )
777 return;
778
779 // Swap out whatever footprint is currently on the dummy board. Cloning
780 // the representative footprint each time is cheap and avoids lingering
781 // state (pads, silk, ...) from previously-selected rows.
782 if( m_dummyFootprint )
783 {
785 delete m_dummyFootprint;
786 m_dummyFootprint = nullptr;
787 }
788
789 const FOOTPRINT* repFp = nullptr;
790
791 if( aMissingIndex >= 0 && aMissingIndex < static_cast<int>( m_missingRepFp.size() ) )
792 repFp = m_missingRepFp[aMissingIndex];
793
794 if( repFp )
795 {
796 m_dummyFootprint = new FOOTPRINT( *repFp );
797 m_dummyFootprint->SetParentGroup( nullptr );
798
799 // Normalize orientation the same way the footprint-properties preview
800 // does so the model lands face-up regardless of how the real footprint
801 // is placed on the user's board.
802 if( m_dummyFootprint->IsFlipped() )
804
805 m_dummyFootprint->SetOrientation( ANGLE_0 );
806
807 // Replace the 3D model list with just the candidate (if any), carrying
808 // over the transform captured from the original WRL entry so the user
809 // sees the candidate sitting in the same spot the WRL would have.
810 m_dummyFootprint->Models().clear();
811
812 if( !aCandAbsPath.IsEmpty() )
813 {
814 FP_3DMODEL replacement;
815 replacement.m_Filename = aCandAbsPath;
816 replacement.m_Scale = m_missingXform[aMissingIndex].m_scale;
817 replacement.m_Rotation = m_missingXform[aMissingIndex].m_rotation;
818 replacement.m_Offset = m_missingXform[aMissingIndex].m_offset;
819 replacement.m_Opacity = m_missingXform[aMissingIndex].m_opacity;
820 replacement.m_Show = true;
821 m_dummyFootprint->Models().push_back( replacement );
822 }
823
825 }
826
827 m_previewPane->ReloadRequest();
828 m_previewPane->Request_refresh();
829}
830
831
832static int getSelectedRow( wxListCtrl* aList )
833{
834 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
835}
836
837
839{
840 populateCandidatesList( static_cast<int>( aEvent.GetIndex() ) );
841}
842
843
845{
846 const int missingIdx = static_cast<int>( getSelectedRow( m_missingList ) );
847 const int candIdx = static_cast<int>( aEvent.GetIndex() );
848
849 if( missingIdx < 0 || missingIdx >= static_cast<int>( m_missing.size() ) )
850 return;
851
852 const std::vector<MATCH_CANDIDATE>& cands = m_candidatesPerMissing[missingIdx];
853
854 if( candIdx < 0 || candIdx >= static_cast<int>( cands.size() ) )
855 return;
856
857 const MATCH_CANDIDATE& c = cands[candIdx];
858
859 // Record this row's choice. The "keep existing" row has an empty path
860 // and we store -1 to mean "no replacement".
861 m_selectedPerMissing[missingIdx] = c.m_absPath.IsEmpty() ? -1 : candIdx;
862
863 // Bold the missing row again when the user explicitly chose "keep
864 // existing", so the visual cue tracks the actual selection state.
865 updateMissingItemStyle( missingIdx );
866
867 showPreview( missingIdx, c.m_absPath );
868}
869
870
872{
873 wxDirDialog dlg( this, _( "Add 3D Model Search Directory" ) );
874
875 if( dlg.ShowModal() != wxID_OK )
876 return;
877
878 const wxString chosen = dlg.GetPath();
879
880 if( chosen.IsEmpty() )
881 return;
882
883 // Persist the directory so future sessions include it automatically.
885
886 if( common )
887 {
888 auto& dirs = common->m_Extra3DSearchDirs;
889
890 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
891 dirs.push_back( chosen );
892 }
893
894 // Scan just the new directory; catalog dedup ensures no duplicates.
895 {
896 wxBusyCursor busy;
897 scanDirectory( chosen );
898 }
899
900 // Re-rank every row against the expanded catalog and refresh the view,
901 // preserving the user's existing per-row selections by filename.
902 std::vector<wxString> priorSelectionPaths( m_missing.size() );
903
904 for( size_t i = 0; i < m_missing.size(); ++i )
905 {
906 const int sel = m_selectedPerMissing[i];
907
908 if( sel >= 0 && sel < static_cast<int>( m_candidatesPerMissing[i].size() ) )
909 priorSelectionPaths[i] = m_candidatesPerMissing[i][sel].m_absPath;
910 }
911
913
914 for( size_t i = 0; i < m_missing.size(); ++i )
915 {
916 m_selectedPerMissing[i] = -1;
917 const std::vector<MATCH_CANDIDATE>& cands = m_candidatesPerMissing[i];
918
919 // Preserve the user's prior explicit pick when the same file still
920 // appears in the catalog.
921 if( !priorSelectionPaths[i].IsEmpty() )
922 {
923 for( size_t j = 0; j < cands.size(); ++j )
924 {
925 if( cands[j].m_absPath == priorSelectionPaths[i] )
926 {
927 m_selectedPerMissing[i] = static_cast<int>( j );
928 break;
929 }
930 }
931 }
932
933 // For rows with no prior pick (or whose pick disappeared), auto-select
934 // the newly-best candidate so adding a directory immediately un-bolds
935 // rows that just acquired a plausible match.
936 if( m_selectedPerMissing[i] < 0 && !cands.empty()
937 && !cands.front().m_absPath.IsEmpty() )
938 {
940 }
941
942 updateMissingItemStyle( static_cast<int>( i ) );
943 }
944
945 const int activeRow = getSelectedRow( m_missingList );
946 populateCandidatesList( activeRow );
947}
948
949
951{
952 const int missingIdx = getSelectedRow( m_missingList );
953
954 if( missingIdx < 0 || missingIdx >= static_cast<int>( m_missing.size() ) )
955 return;
956
957 wxString wildcard;
958
959 for( const wxString& ext : stepExtensions() )
960 {
961 if( !wildcard.IsEmpty() )
962 wildcard += wxS( ";" );
963
964 wildcard += wxS( "*." ) + ext;
965 }
966
967 wxFileDialog dlg( this, _( "Select 3D Model File" ), wxEmptyString, wxEmptyString,
968 _( "3D Models" ) + wxS( " (" ) + wildcard + wxS( ")|" ) + wildcard,
969 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
970
971 if( dlg.ShowModal() != wxID_OK )
972 return;
973
974 const wxString chosenPath = dlg.GetPath();
975
976 if( chosenPath.IsEmpty() )
977 return;
978
979 // Inject the chosen file as a top-ranked candidate for this row so the
980 // user can see it previewed and it will be applied when they hit Replace.
981 MATCH_CANDIDATE injected;
982 injected.m_absPath = chosenPath;
983 injected.m_display = wxFileName( chosenPath ).GetFullName() + wxS( " \u2014 " )
984 + _( "user-selected" );
985 injected.m_score = 2000;
986
987 std::vector<MATCH_CANDIDATE>& cands = m_candidatesPerMissing[missingIdx];
988
989 // Avoid duplicating an already-present entry.
990 auto existing = std::find_if( cands.begin(), cands.end(),
991 [&]( const MATCH_CANDIDATE& c )
992 { return c.m_absPath == chosenPath; } );
993
994 if( existing == cands.end() )
995 {
996 cands.insert( cands.begin(), injected );
997 m_selectedPerMissing[missingIdx] = 0;
998 }
999 else
1000 {
1001 m_selectedPerMissing[missingIdx] = static_cast<int>( existing - cands.begin() );
1002 }
1003
1004 updateMissingItemStyle( missingIdx );
1005 populateCandidatesList( missingIdx );
1006}
1007
1008
1010{
1011 // Build the old->new filename map from the per-row selections. We store
1012 // the new filename through FILENAME_RESOLVER::ShortenPath() so that
1013 // env-var aliases like ${KICAD10_3DMODEL_DIR} survive round-trip.
1015 std::map<wxString, wxString> replacements;
1016
1017 for( size_t i = 0; i < m_missing.size(); ++i )
1018 {
1019 const int sel = m_selectedPerMissing[i];
1020
1021 if( sel < 0 || sel >= static_cast<int>( m_candidatesPerMissing[i].size() ) )
1022 continue;
1023
1024 const MATCH_CANDIDATE& c = m_candidatesPerMissing[i][sel];
1025
1026 if( c.m_absPath.IsEmpty() )
1027 continue; // "(keep existing)"
1028
1029 wxString stored = c.m_absPath;
1030
1031 if( resolver )
1032 stored = resolver->ShortenPath( c.m_absPath );
1033
1034 replacements[m_missing[i]] = stored;
1035 }
1036
1037 if( replacements.empty() )
1038 {
1039 // User clicked Replace without choosing anything; treat as Keep.
1040 EndModal( wxID_CANCEL );
1041 return;
1042 }
1043
1044 BOARD* board = m_frame->GetBoard();
1045 BOARD_COMMIT commit( m_frame );
1046
1047 for( FOOTPRINT* fp : board->Footprints() )
1048 {
1049 bool changedThisFp = false;
1050
1051 for( FP_3DMODEL& model : fp->Models() )
1052 {
1053 auto it = replacements.find( model.m_Filename );
1054
1055 if( it == replacements.end() )
1056 continue;
1057
1058 if( !changedThisFp )
1059 {
1060 commit.Modify( fp );
1061 changedThisFp = true;
1062 }
1063
1064 model.m_Filename = it->second;
1065 }
1066 }
1067
1068 commit.Push( _( "Migrate 3D model references" ) );
1069
1070 EndModal( wxID_OK );
1071}
1072
1073
1075{
1076 EndModal( wxID_CANCEL );
1077}
@ FPHOLDER
Definition board.h:315
#define RANGE_SCALE_3D
This defines the range that all coord will have to be rendered.
virtual 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.
int GetBoardThickness() const
The full thickness of the board including copper and masks.
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:323
const FOOTPRINTS & Footprints() const
Definition board.h:364
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:106
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(900, 560), 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 applyInitialSizeCaps()
Size the dialog on first construction: cap at 0.9 * parent width when opening for the first time,...
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 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.
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 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.
VECTOR3D m_Offset
3D model offset (mm)
Definition footprint.h:173
double m_Opacity
Definition footprint.h:174
VECTOR3D m_Rotation
3D model rotation (degrees)
Definition footprint.h:172
VECTOR3D m_Scale
3D model scaling factor (dimensionless)
Definition footprint.h:171
wxString m_Filename
The 3D shape filename in 3D library.
Definition footprint.h:175
bool m_Show
Include model in rendering.
Definition footprint.h:176
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:722
static const LSET & BackMask()
Return a mask holding all technical layers and the external CU layer on back side.
Definition lset.cpp:729
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:541
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:187
static int getSelectedRow(wxListCtrl *aList)
#define _(s)
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:411
static FILENAME_RESOLVER * resolver
void CollectFilesLoopSafe(const wxString &aRoot, wxArrayString &aFiles, const wxString &aFileSpec, int aFlags)
Recursively collect every file under aRoot, deduplicating subdirectories by their resolved path.
Definition gestfich.cpp:821
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
Definition mirror.h:29
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.
Transform data captured from the first FP_3DMODEL entry that referenced each missing WRL filename,...
KIBIS_MODEL * model