26#include <unordered_set>
31#include <wx/filedlg.h>
32#include <wx/filename.h>
62const std::vector<wxString>& stepExtensions()
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" )
78wxString normalizeStem(
const wxString& aName )
80 wxString stem = aName;
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" )
91 for(
const wxString& ext : stripList )
93 if( stem.length() > ext.length()
94 && stem.Right( ext.length() ).Lower() == ext )
96 stem = stem.Left( stem.length() - ext.length() );
105 stem.Replace( wxS(
"-" ), wxS(
"_" ) );
106 stem.Replace( wxS(
" " ), wxS(
"_" ) );
113int levenshtein(
const wxString& a,
const wxString& b )
115 const size_t m = a.length();
116 const size_t n = b.length();
119 return static_cast<int>( n );
122 return static_cast<int>( m );
124 std::vector<int> prev( n + 1 );
125 std::vector<int> curr( n + 1 );
127 for(
size_t j = 0; j <= n; ++j )
128 prev[j] =
static_cast<int>( j );
130 for(
size_t i = 1; i <= m; ++i )
132 curr[0] =
static_cast<int>( i );
134 for(
size_t j = 1; j <= n; ++j )
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 } );
140 std::swap( prev, curr );
149wxString parentDirName(
const wxString& aPath )
151 wxFileName fn( aPath );
152 const wxArrayString& dirs = fn.GetDirs();
153 return dirs.empty() ? wxString() : dirs.Last();
160wxString parentDirFromFilename(
const wxString& aFilename )
162 return parentDirName( aFilename );
179 m_missingList->AppendColumn( wxEmptyString, wxLIST_FORMAT_LEFT, 800 );
184 auto fitColumn = []( wxListCtrl* aList )
186 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
187 aList->SetColumnWidth( 0, width );
191 [
this, fitColumn]( wxSizeEvent& aEvt )
198 [
this, fitColumn]( wxSizeEvent& aEvt )
217 for(
size_t i = 0; i <
m_missing.size(); ++i )
221 if( !cands.empty() && !cands.front().m_absPath.IsEmpty() )
229 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
230 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
240 const int clientWidth =
m_mainSplitter->GetClientSize().GetWidth();
242 if( clientWidth > 360 )
244 const int outerSash = clientWidth / 3;
247 const int innerWidth = std::max( clientWidth - outerSash, 240 );
255 const wxSize screenSize = wxGetDisplaySize();
256 const int hardCap = ( screenSize.GetWidth() * 9 ) / 10;
263 constexpr int kBaseDefaultWidth = 900;
266 if(
m_frame && GetSize().GetWidth() == kBaseDefaultWidth )
268 const int parentCap = (
m_frame->GetSize().GetWidth() * 9 ) / 10;
269 cap = std::min( cap, parentCap );
272 if( GetSize().GetWidth() > cap )
274 wxSize sized = GetSize();
275 sized.SetWidth( std::max( cap, GetMinSize().GetWidth() ) );
306 const wxString& fn =
model.m_Filename;
311 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
332 std::unordered_set<wxString> unique;
338 const wxString& fn =
model.m_Filename;
343 if( unique.count( fn ) )
346 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
351 return static_cast<int>( unique.size() );
369 bool catalogBuilt =
false;
371 std::map<wxString, wxString> replacements;
372 std::unordered_set<wxString> seen;
378 const wxString& fn =
model.m_Filename;
383 if( !seen.insert( fn ).second )
386 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
397 if( match.IsEmpty() )
400 const wxString shortened =
resolver->ShortenPath( match );
404 replacements[fn] = shortened.IsEmpty() ? match : shortened;
408 if( replacements.empty() )
416 bool changedThisFp =
false;
420 auto it = replacements.find(
model.m_Filename );
422 if( it == replacements.end() )
428 changedThisFp =
true;
431 model.m_Filename = it->second;
437 commit.
Push(
_(
"Auto-migrate 3D model references" ) );
451 const wxString projectPath =
m_frame->Prj().GetProjectPath();
461 std::map<wxString, ENTRY> byName;
467 const wxString& fn =
model.m_Filename;
472 if( byName.count( fn ) )
477 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
480 ENTRY& e = byName[fn];
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;
493 for(
const auto& [fn, entry] : byName )
519 if(
const std::list<SEARCH_PATH>* searchPaths =
resolver->GetPaths() )
523 if( !sp.m_Pathexp.IsEmpty() )
530 wxFileName prj3D(
m_frame->Prj().GetProjectPath(), wxEmptyString );
531 prj3D.AppendDir( wxS(
"3dshapes" ) );
533 if( prj3D.DirExists() )
552 wxFileName normFn( aDir, wxEmptyString );
553 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
554 const wxString key = normFn.GetPath().Lower();
559 if( !wxDir::Exists( normFn.GetPath() ) )
568 std::unordered_set<wxString> existing;
571 existing.insert( e.m_absPath.Lower() );
573 for(
const wxString& f : files )
575 const wxString lower = f.Lower();
576 bool acceptable =
false;
578 for(
const wxString& ext : stepExtensions() )
580 if( lower.length() > ext.length() + 1
581 && lower.Right( ext.length() + 1 ) == wxS(
"." ) + ext )
591 if( existing.count( lower ) )
594 existing.insert( lower );
598 entry.
m_stem = normalizeStem( wxFileName( f ).GetFullName() );
599 entry.
m_parent = parentDirName( f );
605std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
608 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
609 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
611 std::vector<MATCH_CANDIDATE> ranked;
612 ranked.reserve( std::min<size_t>(
m_catalog.size(), 64 ) );
618 if( entry.m_stem == wrlStem )
620 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
626 const int dist = levenshtein( entry.m_stem, wrlStem );
627 const int maxDist =
static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
629 if( dist <= maxDist )
630 score = 500 - dist * 10;
637 cand.
m_display = wxFileName( entry.m_absPath ).GetFullName()
641 ranked.push_back( cand );
645 std::sort( ranked.begin(), ranked.end(),
648 if( a.m_score != b.m_score )
649 return a.m_score > b.m_score;
651 return a.m_display.CmpNoCase( b.m_display ) < 0;
654 constexpr size_t kMaxCandidates = 15;
656 if( ranked.size() > kMaxCandidates )
657 ranked.resize( kMaxCandidates );
663 keep.
m_display =
_(
"(keep existing reference)" );
665 ranked.push_back( keep );
673 for(
size_t i = 0; i <
m_missing.size(); ++i )
682 for(
size_t i = 0; i <
m_missing.size(); ++i )
693 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
698 : wxFONTWEIGHT_NORMAL );
707 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
712 for(
size_t i = 0; i < cands.size(); ++i )
717 if( sel >= 0 && sel <
static_cast<int>( cands.size() ) )
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 );
723 else if( !cands.empty() )
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 );
762 cfg->m_Render.show_missing_models =
false;
791 if( aMissingIndex >= 0 && aMissingIndex <
static_cast<int>(
m_missingRepFp.size() ) )
812 if( !aCandAbsPath.IsEmpty() )
820 replacement.
m_Show =
true;
834 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
847 const int candIdx =
static_cast<int>( aEvent.GetIndex() );
849 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
854 if( candIdx < 0 || candIdx >=
static_cast<int>( cands.size() ) )
873 wxDirDialog dlg(
this,
_(
"Add 3D Model Search Directory" ) );
875 if( dlg.ShowModal() != wxID_OK )
878 const wxString chosen = dlg.GetPath();
880 if( chosen.IsEmpty() )
890 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
891 dirs.push_back( chosen );
902 std::vector<wxString> priorSelectionPaths(
m_missing.size() );
904 for(
size_t i = 0; i <
m_missing.size(); ++i )
914 for(
size_t i = 0; i <
m_missing.size(); ++i )
921 if( !priorSelectionPaths[i].IsEmpty() )
923 for(
size_t j = 0; j < cands.size(); ++j )
925 if( cands[j].m_absPath == priorSelectionPaths[i] )
937 && !cands.front().m_absPath.IsEmpty() )
954 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
959 for(
const wxString& ext : stepExtensions() )
961 if( !wildcard.IsEmpty() )
962 wildcard += wxS(
";" );
964 wildcard += wxS(
"*." ) + ext;
967 wxFileDialog dlg(
this,
_(
"Select 3D Model File" ), wxEmptyString, wxEmptyString,
968 _(
"3D Models" ) + wxS(
" (" ) + wildcard + wxS(
")|" ) + wildcard,
969 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
971 if( dlg.ShowModal() != wxID_OK )
974 const wxString chosenPath = dlg.GetPath();
976 if( chosenPath.IsEmpty() )
983 injected.
m_display = wxFileName( chosenPath ).GetFullName() + wxS(
" \u2014 " )
984 +
_(
"user-selected" );
990 auto existing = std::find_if( cands.begin(), cands.end(),
992 { return c.m_absPath == chosenPath; } );
994 if( existing == cands.end() )
996 cands.insert( cands.begin(), injected );
1015 std::map<wxString, wxString> replacements;
1017 for(
size_t i = 0; i <
m_missing.size(); ++i )
1037 if( replacements.empty() )
1040 EndModal( wxID_CANCEL );
1049 bool changedThisFp =
false;
1053 auto it = replacements.find(
model.m_Filename );
1055 if( it == replacements.end() )
1058 if( !changedThisFp )
1061 changedThisFp =
true;
1064 model.m_Filename = it->second;
1068 commit.
Push(
_(
"Migrate 3D model references" ) );
1070 EndModal( wxID_OK );
1076 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.