26#include <unordered_set>
31#include <wx/filedlg.h>
32#include <wx/filename.h>
61const std::vector<wxString>& stepExtensions()
63 static const std::vector<wxString> exts = {
64 wxS(
"step" ), wxS(
"stp" ), wxS(
"stpz" ),
65 wxS(
"step.gz" ), wxS(
"stp.gz" ),
66 wxS(
"iges" ), wxS(
"igs" )
77wxString normalizeStem(
const wxString& aName )
79 wxString stem = aName;
83 static const wxString stripList[] = {
84 wxS(
".step.gz" ), wxS(
".stp.gz" ),
85 wxS(
".wrl" ), wxS(
".wrz" ),
86 wxS(
".step" ), wxS(
".stp" ), wxS(
".stpz" ),
87 wxS(
".iges" ), wxS(
".igs" )
90 for(
const wxString& ext : stripList )
92 if( stem.length() > ext.length()
93 && stem.Right( ext.length() ).Lower() == ext )
95 stem = stem.Left( stem.length() - ext.length() );
104 stem.Replace( wxS(
"-" ), wxS(
"_" ) );
105 stem.Replace( wxS(
" " ), wxS(
"_" ) );
112int levenshtein(
const wxString& a,
const wxString& b )
114 const size_t m = a.length();
115 const size_t n = b.length();
118 return static_cast<int>( n );
121 return static_cast<int>( m );
123 std::vector<int> prev( n + 1 );
124 std::vector<int> curr( n + 1 );
126 for(
size_t j = 0; j <= n; ++j )
127 prev[j] =
static_cast<int>( j );
129 for(
size_t i = 1; i <= m; ++i )
131 curr[0] =
static_cast<int>( i );
133 for(
size_t j = 1; j <= n; ++j )
135 int cost = ( a[i - 1] == b[j - 1] ) ? 0 : 1;
136 curr[j] = std::min( { curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost } );
139 std::swap( prev, curr );
148wxString parentDirName(
const wxString& aPath )
150 wxFileName fn( aPath );
151 const wxArrayString& dirs = fn.GetDirs();
152 return dirs.empty() ? wxString() : dirs.Last();
159wxString parentDirFromFilename(
const wxString& aFilename )
161 return parentDirName( aFilename );
178 m_missingList->AppendColumn( wxEmptyString, wxLIST_FORMAT_LEFT, 800 );
183 auto fitColumn = []( wxListCtrl* aList )
185 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
186 aList->SetColumnWidth( 0, width );
190 [
this, fitColumn]( wxSizeEvent& aEvt )
197 [
this, fitColumn]( wxSizeEvent& aEvt )
216 for(
size_t i = 0; i <
m_missing.size(); ++i )
220 if( !cands.empty() && !cands.front().m_absPath.IsEmpty() )
228 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
229 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
239 const int clientWidth =
m_mainSplitter->GetClientSize().GetWidth();
241 if( clientWidth > 360 )
243 const int outerSash = clientWidth / 3;
246 const int innerWidth = std::max( clientWidth - outerSash, 240 );
254 const wxSize screenSize = wxGetDisplaySize();
255 const int hardCap = ( screenSize.GetWidth() * 9 ) / 10;
262 constexpr int kBaseDefaultWidth = 900;
265 if(
m_frame && GetSize().GetWidth() == kBaseDefaultWidth )
267 const int parentCap = (
m_frame->GetSize().GetWidth() * 9 ) / 10;
268 cap = std::min( cap, parentCap );
271 if( GetSize().GetWidth() > cap )
273 wxSize sized = GetSize();
274 sized.SetWidth( std::max( cap, GetMinSize().GetWidth() ) );
305 const wxString& fn =
model.m_Filename;
310 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
331 std::unordered_set<wxString> unique;
337 const wxString& fn =
model.m_Filename;
342 if( unique.count( fn ) )
345 if(
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
350 return static_cast<int>( unique.size() );
368 bool catalogBuilt =
false;
370 std::map<wxString, wxString> replacements;
371 std::unordered_set<wxString> seen;
377 const wxString& fn =
model.m_Filename;
382 if( !seen.insert( fn ).second )
385 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
396 if( match.IsEmpty() )
399 const wxString shortened =
resolver->ShortenPath( match );
403 replacements[fn] = shortened.IsEmpty() ? match : shortened;
407 if( replacements.empty() )
415 bool changedThisFp =
false;
419 auto it = replacements.find(
model.m_Filename );
421 if( it == replacements.end() )
427 changedThisFp =
true;
430 model.m_Filename = it->second;
436 commit.
Push(
_(
"Auto-migrate 3D model references" ) );
450 const wxString projectPath =
m_frame->Prj().GetProjectPath();
460 std::map<wxString, ENTRY> byName;
466 const wxString& fn =
model.m_Filename;
471 if( byName.count( fn ) )
476 if( !
resolver->ResolvePath( fn, projectPath, {} ).IsEmpty() )
479 ENTRY& e = byName[fn];
481 e.m_xform.m_scale =
model.m_Scale;
482 e.m_xform.m_rotation =
model.m_Rotation;
483 e.m_xform.m_offset =
model.m_Offset;
484 e.m_xform.m_opacity =
model.m_Opacity;
492 for(
const auto& [fn, entry] : byName )
518 if(
const std::list<SEARCH_PATH>* searchPaths =
resolver->GetPaths() )
522 if( !sp.m_Pathexp.IsEmpty() )
529 wxFileName prj3D(
m_frame->Prj().GetProjectPath(), wxEmptyString );
530 prj3D.AppendDir( wxS(
"3dshapes" ) );
532 if( prj3D.DirExists() )
551 wxFileName normFn( aDir, wxEmptyString );
552 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
553 const wxString key = normFn.GetPath().Lower();
558 if( !wxDir::Exists( normFn.GetPath() ) )
562 wxDir::GetAllFiles( normFn.GetPath(), &files, wxEmptyString, wxDIR_FILES | wxDIR_DIRS );
567 std::unordered_set<wxString> existing;
570 existing.insert( e.m_absPath.Lower() );
572 for(
const wxString& f : files )
574 const wxString lower = f.Lower();
575 bool acceptable =
false;
577 for(
const wxString& ext : stepExtensions() )
579 if( lower.length() > ext.length() + 1
580 && lower.Right( ext.length() + 1 ) == wxS(
"." ) + ext )
590 if( existing.count( lower ) )
593 existing.insert( lower );
597 entry.
m_stem = normalizeStem( wxFileName( f ).GetFullName() );
598 entry.
m_parent = parentDirName( f );
604std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
607 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
608 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
610 std::vector<MATCH_CANDIDATE> ranked;
611 ranked.reserve( std::min<size_t>(
m_catalog.size(), 64 ) );
617 if( entry.m_stem == wrlStem )
619 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
625 const int dist = levenshtein( entry.m_stem, wrlStem );
626 const int maxDist =
static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
628 if( dist <= maxDist )
629 score = 500 - dist * 10;
636 cand.
m_display = wxFileName( entry.m_absPath ).GetFullName()
640 ranked.push_back( cand );
644 std::sort( ranked.begin(), ranked.end(),
647 if( a.m_score != b.m_score )
648 return a.m_score > b.m_score;
650 return a.m_display.CmpNoCase( b.m_display ) < 0;
653 constexpr size_t kMaxCandidates = 15;
655 if( ranked.size() > kMaxCandidates )
656 ranked.resize( kMaxCandidates );
662 keep.
m_display =
_(
"(keep existing reference)" );
664 ranked.push_back( keep );
672 for(
size_t i = 0; i <
m_missing.size(); ++i )
681 for(
size_t i = 0; i <
m_missing.size(); ++i )
692 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
697 : wxFONTWEIGHT_NORMAL );
706 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
711 for(
size_t i = 0; i < cands.size(); ++i )
716 if( sel >= 0 && sel <
static_cast<int>( cands.size() ) )
718 m_candidatesList->SetItemState( sel, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
719 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
720 showPreview( aMissingIndex, cands[sel].m_absPath );
722 else if( !cands.empty() )
728 m_candidatesList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
729 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
730 showPreview( aMissingIndex, cands.front().m_absPath );
761 cfg->m_Render.show_missing_models =
false;
790 if( aMissingIndex >= 0 && aMissingIndex <
static_cast<int>(
m_missingRepFp.size() ) )
811 if( !aCandAbsPath.IsEmpty() )
819 replacement.
m_Show =
true;
833 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
846 const int candIdx =
static_cast<int>( aEvent.GetIndex() );
848 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
853 if( candIdx < 0 || candIdx >=
static_cast<int>( cands.size() ) )
872 wxDirDialog dlg(
this,
_(
"Add 3D Model Search Directory" ) );
874 if( dlg.ShowModal() != wxID_OK )
877 const wxString chosen = dlg.GetPath();
879 if( chosen.IsEmpty() )
889 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
890 dirs.push_back( chosen );
901 std::vector<wxString> priorSelectionPaths(
m_missing.size() );
903 for(
size_t i = 0; i <
m_missing.size(); ++i )
913 for(
size_t i = 0; i <
m_missing.size(); ++i )
920 if( !priorSelectionPaths[i].IsEmpty() )
922 for(
size_t j = 0; j < cands.size(); ++j )
924 if( cands[j].m_absPath == priorSelectionPaths[i] )
936 && !cands.front().m_absPath.IsEmpty() )
953 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
958 for(
const wxString& ext : stepExtensions() )
960 if( !wildcard.IsEmpty() )
961 wildcard += wxS(
";" );
963 wildcard += wxS(
"*." ) + ext;
966 wxFileDialog dlg(
this,
_(
"Select 3D Model File" ), wxEmptyString, wxEmptyString,
967 _(
"3D Models" ) + wxS(
" (" ) + wildcard + wxS(
")|" ) + wildcard,
968 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
970 if( dlg.ShowModal() != wxID_OK )
973 const wxString chosenPath = dlg.GetPath();
975 if( chosenPath.IsEmpty() )
982 injected.
m_display = wxFileName( chosenPath ).GetFullName() + wxS(
" \u2014 " )
983 +
_(
"user-selected" );
989 auto existing = std::find_if( cands.begin(), cands.end(),
991 { return c.m_absPath == chosenPath; } );
993 if( existing == cands.end() )
995 cands.insert( cands.begin(), injected );
1014 std::map<wxString, wxString> replacements;
1016 for(
size_t i = 0; i <
m_missing.size(); ++i )
1036 if( replacements.empty() )
1039 EndModal( wxID_CANCEL );
1048 bool changedThisFp =
false;
1052 auto it = replacements.find(
model.m_Filename );
1054 if( it == replacements.end() )
1057 if( !changedThisFp )
1060 changedThisFp =
true;
1063 model.m_Filename = it->second;
1067 commit.
Push(
_(
"Migrate 3D model references" ) );
1069 EndModal( wxID_OK );
1075 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
@ 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.