23#include <unordered_set>
28#include <wx/filedlg.h>
29#include <wx/filename.h>
59const std::vector<wxString>& stepExtensions()
61 static const std::vector<wxString> exts = {
62 wxS(
"step" ), wxS(
"stp" ), wxS(
"stpz" ),
63 wxS(
"step.gz" ), wxS(
"stp.gz" ),
64 wxS(
"iges" ), wxS(
"igs" )
75wxString normalizeStem(
const wxString& aName )
77 wxString stem = aName;
81 static const wxString stripList[] = {
82 wxS(
".step.gz" ), wxS(
".stp.gz" ),
83 wxS(
".wrl" ), wxS(
".wrz" ),
84 wxS(
".step" ), wxS(
".stp" ), wxS(
".stpz" ),
85 wxS(
".iges" ), wxS(
".igs" )
88 for(
const wxString& ext : stripList )
90 if( stem.length() > ext.length()
91 && stem.Right( ext.length() ).Lower() == ext )
93 stem = stem.Left( stem.length() - ext.length() );
102 stem.Replace( wxS(
"-" ), wxS(
"_" ) );
103 stem.Replace( wxS(
" " ), wxS(
"_" ) );
110int levenshtein(
const wxString& a,
const wxString& b )
112 const size_t m = a.length();
113 const size_t n = b.length();
116 return static_cast<int>( n );
119 return static_cast<int>( m );
121 std::vector<int> prev( n + 1 );
122 std::vector<int> curr( n + 1 );
124 for(
size_t j = 0; j <= n; ++j )
125 prev[j] =
static_cast<int>( j );
127 for(
size_t i = 1; i <= m; ++i )
129 curr[0] =
static_cast<int>( i );
131 for(
size_t j = 1; j <= n; ++j )
133 int cost = ( a[i - 1] == b[j - 1] ) ? 0 : 1;
134 curr[j] = std::min( { curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost } );
137 std::swap( prev, curr );
146wxString parentDirName(
const wxString& aPath )
148 wxFileName fn( aPath );
149 const wxArrayString& dirs = fn.GetDirs();
150 return dirs.empty() ? wxString() : dirs.Last();
157wxString parentDirFromFilename(
const wxString& aFilename )
159 return parentDirName( aFilename );
176 m_missingList->AppendColumn( wxEmptyString, wxLIST_FORMAT_LEFT, 800 );
181 auto fitColumn = []( wxListCtrl* aList )
183 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
184 aList->SetColumnWidth( 0, width );
188 [
this, fitColumn]( wxSizeEvent& aEvt )
195 [
this, fitColumn]( wxSizeEvent& aEvt )
214 for(
size_t i = 0; i <
m_missing.size(); ++i )
218 if( !cands.empty() && !cands.front().m_absPath.IsEmpty() )
226 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
227 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
237 const int clientWidth =
m_mainSplitter->GetClientSize().GetWidth();
239 if( clientWidth > 360 )
241 const int outerSash = clientWidth / 3;
244 const int innerWidth = std::max( clientWidth - outerSash, 240 );
252 const wxSize screenSize = wxGetDisplaySize();
253 const int hardCap = ( screenSize.GetWidth() * 9 ) / 10;
260 constexpr int kBaseDefaultWidth = 900;
263 if(
m_frame && GetSize().GetWidth() == kBaseDefaultWidth )
265 const int parentCap = (
m_frame->GetSize().GetWidth() * 9 ) / 10;
266 cap = std::min( cap, parentCap );
269 if( GetSize().GetWidth() > cap )
271 wxSize sized = GetSize();
272 sized.SetWidth( std::max( cap, GetMinSize().GetWidth() ) );
303 const wxString& fn =
model.m_Filename;
308 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
329 std::unordered_set<wxString> unique;
335 const wxString& fn =
model.m_Filename;
340 if( unique.count( fn ) )
343 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
348 return static_cast<int>( unique.size() );
366 bool catalogBuilt =
false;
368 std::map<wxString, wxString> replacements;
369 std::unordered_set<wxString> seen;
375 const wxString& fn =
model.m_Filename;
380 if( !seen.insert( fn ).second )
383 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
394 if( match.IsEmpty() )
397 const wxString shortened =
resolver->ShortenPath( match );
401 replacements[fn] = shortened.IsEmpty() ? match : shortened;
405 if( replacements.empty() )
413 bool changedThisFp =
false;
417 auto it = replacements.find(
model.m_Filename );
419 if( it == replacements.end() )
425 changedThisFp =
true;
428 model.m_Filename = it->second;
434 commit.
Push(
_(
"Auto-migrate 3D model references" ) );
448 const wxString projectPath =
m_frame->Prj().GetProjectPath();
458 std::map<wxString, ENTRY> byName;
464 const wxString& fn =
model.m_Filename;
469 if( byName.count( fn ) )
474 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
477 ENTRY& e = byName[fn];
479 e.m_xform.m_scale =
model.m_Scale;
480 e.m_xform.m_rotation =
model.m_Rotation;
481 e.m_xform.m_offset =
model.m_Offset;
482 e.m_xform.m_opacity =
model.m_Opacity;
490 for(
const auto& [fn, entry] : byName )
516 if(
const std::list<SEARCH_PATH>* searchPaths =
resolver->GetPaths() )
520 if( !sp.m_Pathexp.IsEmpty() )
527 wxFileName prj3D(
m_frame->Prj().GetProjectPath(), wxEmptyString );
528 prj3D.AppendDir( wxS(
"3dshapes" ) );
530 if( prj3D.DirExists() )
549 wxFileName normFn( aDir, wxEmptyString );
550 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
551 const wxString key = normFn.GetPath().Lower();
556 if( !wxDir::Exists( normFn.GetPath() ) )
565 std::unordered_set<wxString> existing;
568 existing.insert( e.m_absPath.Lower() );
570 for(
const wxString& f : files )
572 const wxString lower = f.Lower();
573 bool acceptable =
false;
575 for(
const wxString& ext : stepExtensions() )
577 if( lower.length() > ext.length() + 1
578 && lower.Right( ext.length() + 1 ) == wxS(
"." ) + ext )
588 if( existing.count( lower ) )
591 existing.insert( lower );
595 entry.
m_stem = normalizeStem( wxFileName( f ).GetFullName() );
596 entry.
m_parent = parentDirName( f );
602std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
605 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
606 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
608 std::vector<MATCH_CANDIDATE> ranked;
609 ranked.reserve( std::min<size_t>(
m_catalog.size(), 64 ) );
615 if( entry.m_stem == wrlStem )
617 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
623 const int dist = levenshtein( entry.m_stem, wrlStem );
624 const int maxDist =
static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
626 if( dist <= maxDist )
627 score = 500 - dist * 10;
634 cand.
m_display = wxFileName( entry.m_absPath ).GetFullName()
638 ranked.push_back( cand );
642 std::sort( ranked.begin(), ranked.end(),
645 if( a.m_score != b.m_score )
646 return a.m_score > b.m_score;
648 return a.m_display.CmpNoCase( b.m_display ) < 0;
651 constexpr size_t kMaxCandidates = 15;
653 if( ranked.size() > kMaxCandidates )
654 ranked.resize( kMaxCandidates );
660 keep.
m_display =
_(
"(keep existing reference)" );
662 ranked.push_back( keep );
670 for(
size_t i = 0; i <
m_missing.size(); ++i )
679 for(
size_t i = 0; i <
m_missing.size(); ++i )
690 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
695 : wxFONTWEIGHT_NORMAL );
704 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
709 for(
size_t i = 0; i < cands.size(); ++i )
714 if( sel >= 0 && sel <
static_cast<int>( cands.size() ) )
716 m_candidatesList->SetItemState( sel, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
717 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
718 showPreview( aMissingIndex, cands[sel].m_absPath );
720 else if( !cands.empty() )
726 m_candidatesList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
727 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
728 showPreview( aMissingIndex, cands.front().m_absPath );
759 cfg->m_Render.show_missing_models =
false;
788 if( aMissingIndex >= 0 && aMissingIndex <
static_cast<int>(
m_missingRepFp.size() ) )
809 if( !aCandAbsPath.IsEmpty() )
817 replacement.
m_Show =
true;
831 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
844 const int candIdx =
static_cast<int>( aEvent.GetIndex() );
846 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
851 if( candIdx < 0 || candIdx >=
static_cast<int>( cands.size() ) )
870 wxDirDialog dlg(
this,
_(
"Add 3D Model Search Directory" ) );
872 if( dlg.ShowModal() != wxID_OK )
875 const wxString chosen = dlg.GetPath();
877 if( chosen.IsEmpty() )
887 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
888 dirs.push_back( chosen );
899 std::vector<wxString> priorSelectionPaths(
m_missing.size() );
901 for(
size_t i = 0; i <
m_missing.size(); ++i )
911 for(
size_t i = 0; i <
m_missing.size(); ++i )
918 if( !priorSelectionPaths[i].IsEmpty() )
920 for(
size_t j = 0; j < cands.size(); ++j )
922 if( cands[j].m_absPath == priorSelectionPaths[i] )
934 && !cands.front().m_absPath.IsEmpty() )
951 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
956 for(
const wxString& ext : stepExtensions() )
958 if( !wildcard.IsEmpty() )
959 wildcard += wxS(
";" );
961 wildcard += wxS(
"*." ) + ext;
964 wxFileDialog dlg(
this,
_(
"Select 3D Model File" ), wxEmptyString, wxEmptyString,
965 _(
"3D Models" ) + wxS(
" (" ) + wildcard + wxS(
")|" ) + wildcard,
966 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
968 if( dlg.ShowModal() != wxID_OK )
971 const wxString chosenPath = dlg.GetPath();
973 if( chosenPath.IsEmpty() )
980 injected.
m_display = wxFileName( chosenPath ).GetFullName() + wxS(
" \u2014 " )
981 +
_(
"user-selected" );
987 auto existing = std::find_if( cands.begin(), cands.end(),
989 { return c.m_absPath == chosenPath; } );
991 if( existing == cands.end() )
993 cands.insert( cands.begin(), injected );
1012 std::map<wxString, wxString> replacements;
1014 for(
size_t i = 0; i <
m_missing.size(); ++i )
1034 if( replacements.empty() )
1037 EndModal( wxID_CANCEL );
1046 bool changedThisFp =
false;
1050 auto it = replacements.find(
model.m_Filename );
1052 if( it == replacements.end() )
1055 if( !changedThisFp )
1058 changedThisFp =
true;
1061 model.m_Filename = it->second;
1065 commit.
Push(
_(
"Migrate 3D model references" ) );
1067 EndModal( wxID_OK );
1073 EndModal( wxID_CANCEL );
#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.
const FOOTPRINTS & Footprints() const
COMMIT & Modify(EDA_ITEM *aItem, BASE_SCREEN *aScreen=nullptr, RECURSE_MODE aRecurse=RECURSE_MODE::NO_RECURSE)
Modify a given item in the model.
std::vector< wxString > m_Extra3DSearchDirs
Extra directories to search for 3D models, added by the user through the 3D model migration dialog.
wxListCtrl * m_candidatesList
wxSplitterWindow * m_innerSplitter
wxSplitterWindow * m_mainSplitter
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)
wxListCtrl * m_missingList
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.
TRACK_BALL m_trackBallCamera
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 populateMissingList()
BOARD_ADAPTER m_boardAdapter
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).
~DIALOG_MIGRATE_3D_MODELS() override
void collectMissingModels()
EDA_3D_CANVAS * m_previewPane
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)
VECTOR3D m_Rotation
3D model rotation (degrees)
VECTOR3D m_Scale
3D model scaling factor (dimensionless)
wxString m_Filename
The 3D shape filename in 3D library.
bool m_Show
Include model in rendering.
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.
static const LSET & BackMask()
Return a mask holding all technical layers and the external CU layer on back side.
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.
The main frame for Pcbnew.
virtual COMMON_SETTINGS * GetCommonSettings() const
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.
static int getSelectedRow(wxListCtrl *aList)
static constexpr EDA_ANGLE ANGLE_0
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.
@ TOP_BOTTOM
Flip top to bottom (around the X axis)
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.
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_display
Label shown to the user.
int m_score
Higher = better match.
wxString m_absPath
Absolute path, or empty for the "keep existing" row.