23#include <unordered_set>
28#include <wx/filedlg.h>
29#include <wx/filename.h>
58#define KEEP_EXISTING -1
66const std::vector<wxString>& stepExtensions()
68 static const std::vector<wxString> exts = {
86wxString normalizeStem(
const wxString& aName )
88 wxString stem = aName;
92 static const wxString stripList[] = {
104 for(
const wxString& ext : stripList )
106 if( stem.length() > ext.length() && stem.Right( ext.length() ).Lower() == ext )
108 stem = stem.Left( stem.length() - ext.length() );
117 stem.Replace( wxS(
"-" ), wxS(
"_" ) );
118 stem.Replace( wxS(
" " ), wxS(
"_" ) );
125int levenshtein(
const wxString& a,
const wxString& b )
127 const size_t m = a.length();
128 const size_t n = b.length();
131 return static_cast<int>( n );
134 return static_cast<int>( m );
136 std::vector<int> prev( n + 1 );
137 std::vector<int> curr( n + 1 );
139 for(
size_t j = 0; j <= n; ++j )
140 prev[j] =
static_cast<int>( j );
142 for(
size_t i = 1; i <= m; ++i )
144 curr[0] =
static_cast<int>( i );
146 for(
size_t j = 1; j <= n; ++j )
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 } );
152 std::swap( prev, curr );
161wxString parentDirName(
const wxString& aPath )
163 wxFileName fn( aPath );
164 const wxArrayString& dirs = fn.GetDirs();
165 return dirs.empty() ? wxString() : dirs.Last();
172wxString parentDirFromFilename(
const wxString& aFilename )
174 return parentDirName( aFilename );
195 for(
const wxString& dir : common->m_Extra3DSearchDirs )
216 []( wxListCtrl* aList )
218 const int width = std::max( aList->GetClientSize().GetWidth() - 2, 20 );
219 aList->SetColumnWidth( 0, width );
223 [
this, fitColumn]( wxSizeEvent& aEvt )
230 [
this, fitColumn]( wxSizeEvent& aEvt )
240 { wxID_CANCEL,
_(
"Cancel" ) } } );
261 for(
const auto& [fn, entry] : byName )
279 for(
size_t i = 0; i <
m_missing.size(); ++i )
291 m_missingList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
292 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
316 if( missingModels.size() == 0 )
324 catalog.
Build( projectPath, resolver );
326 std::map<wxString, wxString> replacements;
328 for(
const auto& [filename,
_] : missingModels )
330 const wxString match = catalog.
FindMatchFor( filename );
332 if( match.IsEmpty() )
335 const wxString shortened = resolver->
ShortenPath( match );
339 replacements[filename] = shortened.IsEmpty() ? match : shortened;
342 if( replacements.empty() )
350 bool changedThisFp =
false;
354 auto it = replacements.find(
model.m_Filename );
356 if( it == replacements.end() )
362 changedThisFp =
true;
365 model.m_Filename = it->second;
370 if( !commit.
Empty() )
371 commit.
Push(
_(
"Auto-migrate 3D Model References" ) );
377std::map<wxString, DIALOG_MIGRATE_3D_MODELS::ENTRY>
380 std::map<wxString, ENTRY> byName;
389 if( !board || !resolver )
396 const wxString& fn =
model.m_Filename;
401 if( byName.count( fn ) )
404 std::vector<const EMBEDDED_FILES*> embeddedFilesStack;
405 embeddedFilesStack.push_back( fp->GetEmbeddedFiles() );
408 if( !resolver->
ResolvePath( fn, projectPath, embeddedFilesStack ).IsEmpty() )
411 ENTRY& e = byName[fn];
441 if(
const std::list<SEARCH_PATH>* searchPaths = resolver->
GetPaths() )
445 if( !sp.m_Pathexp.IsEmpty() )
452 wxFileName prj3D(
m_frame->Prj().GetProjectPath(), wxEmptyString );
453 prj3D.AppendDir( wxS(
"3dshapes" ) );
455 if( prj3D.DirExists() )
474 wxFileName normFn( aDir, wxEmptyString );
475 normFn.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_DOTS );
476 const wxString key = normFn.GetPath().Lower();
481 if( !wxDir::Exists( normFn.GetPath() ) )
490 std::unordered_set<wxString> existing;
493 existing.insert( e.m_absPath.Lower() );
495 for(
const wxString& f : files )
497 const wxString lower = f.Lower();
498 bool acceptable =
false;
500 for(
const wxString& ext : stepExtensions() )
502 if( lower.length() > ext.length() + 1
503 && lower.Right( ext.length() + 1 ) == wxS(
"." ) + ext )
513 if( existing.count( lower ) )
516 existing.insert( lower );
520 entry.
m_stem = normalizeStem( wxFileName( f ).GetFullName() );
521 entry.
m_parent = parentDirName( f );
527std::vector<DIALOG_MIGRATE_3D_MODELS::MATCH_CANDIDATE>
530 const wxString wrlStem = normalizeStem( wxFileName( aWrlFilename ).GetFullName() );
531 const wxString wrlParent = parentDirFromFilename( aWrlFilename );
533 std::vector<MATCH_CANDIDATE> ranked;
534 ranked.reserve( std::min<size_t>(
m_catalog.size(), 64 ) );
540 if( entry.m_stem == wrlStem )
542 score = ( !wrlParent.IsEmpty() && entry.m_parent.CmpNoCase( wrlParent ) == 0 )
548 const int dist = levenshtein( entry.m_stem, wrlStem );
549 const int maxDist =
static_cast<int>( std::max<size_t>( 2, wrlStem.length() / 4 ) );
551 if( dist <= maxDist )
552 score = 500 - dist * 10;
559 cand.
m_display = wxFileName( entry.m_absPath ).GetFullName() + wxS(
" \u2014 " ) + entry.m_parent;
561 ranked.push_back( cand );
565 std::sort( ranked.begin(), ranked.end(),
568 if( a.m_score != b.m_score )
569 return a.m_score > b.m_score;
571 return a.m_display.CmpNoCase( b.m_display ) < 0;
574 constexpr size_t kMaxCandidates = 15;
576 if( ranked.size() > kMaxCandidates )
577 ranked.resize( kMaxCandidates );
583 keep.
m_display =
_(
"(keep existing reference)" );
585 ranked.push_back( keep );
593 for(
size_t i = 0; i <
m_missing.size(); ++i )
602 for(
size_t i = 0; i <
m_missing.size(); ++i )
613 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
618 : wxFONTWEIGHT_NORMAL );
627 if( aMissingIndex < 0 || aMissingIndex >=
static_cast<int>(
m_missing.size() ) )
632 for(
size_t i = 0; i <
candidates.size(); ++i )
637 if( sel >= 0 && sel <
static_cast<int>(
candidates.size() ) )
639 m_candidatesList->SetItemState( sel, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
640 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
649 m_candidatesList->SetItemState( 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED,
650 wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED );
704 if( aMissingIndex >= 0 && aMissingIndex <
static_cast<int>(
m_missingRepFp.size() ) )
725 if( !aCandAbsPath.IsEmpty() )
733 replacement.
m_Show =
true;
747 return static_cast<int>( aList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
760 const int candidateIdx =
static_cast<int>( aEvent.GetIndex() );
762 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
767 if( candidateIdx < 0 || candidateIdx >=
static_cast<int>(
candidates.size() ) )
785 wxDirDialog dlg(
this,
_(
"Add 3D Model Search Directory" ) );
787 if( dlg.ShowModal() != wxID_OK )
790 const wxString chosen = dlg.GetPath();
792 if( chosen.IsEmpty() )
801 std::vector<wxString>& dirs = common->m_Extra3DSearchDirs;
803 if( std::find( dirs.begin(), dirs.end(), chosen ) == dirs.end() )
804 dirs.push_back( chosen );
821 std::vector<wxString> priorSelectionPaths(
m_missing.size() );
823 for(
size_t i = 0; i <
m_missing.size(); ++i )
833 for(
size_t i = 0; i <
m_missing.size(); ++i )
843 if( !priorSelectionPaths[i].IsEmpty() )
845 for(
size_t j = 0; j <
candidates.size(); ++j )
847 if(
candidates[j].m_absPath == priorSelectionPaths[i] )
886 std::vector<wxString>& dirs = common->m_Extra3DSearchDirs;
889 [&](
const wxString& dir )
891 return dir == deleted;
904 if( missingIdx < 0 || missingIdx >=
static_cast<int>(
m_missing.size() ) )
909 for(
const wxString& ext : stepExtensions() )
911 if( !wildcard.IsEmpty() )
912 wildcard += wxS(
";" );
914 wildcard += wxS(
"*." ) + ext;
917 wxFileDialog dlg(
this,
_(
"Select 3D Model File" ), wxEmptyString, wxEmptyString,
918 _(
"3D Models" ) + wxS(
" (" ) + wildcard + wxS(
")|" ) + wildcard,
919 wxFD_OPEN | wxFD_FILE_MUST_EXIST );
921 if( dlg.ShowModal() != wxID_OK )
924 const wxString chosenPath = dlg.GetPath();
926 if( chosenPath.IsEmpty() )
933 injected.
m_display = wxFileName( chosenPath ).GetFullName() + wxS(
" \u2014 " ) +
_(
"user-selected" );
942 return c.m_absPath == chosenPath;
968 std::map<wxString, wxString> replacements;
970 for(
size_t i = 0; i <
m_missing.size(); ++i )
990 if( replacements.empty() )
993 EndModal( wxID_CANCEL );
1002 bool changedThisFp =
false;
1006 auto it = replacements.find(
model.m_Filename );
1008 if( it == replacements.end() )
1011 if( !changedThisFp )
1014 changedThisFp =
true;
1017 model.m_Filename = it->second;
1021 commit.
Push(
_(
"Migrate 3D Model References" ) );
1023 EndModal( wxID_OK );
1029 EndModal( wxID_CANCEL );
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
#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.
EMBEDDED_FILES * GetEmbeddedFiles() override
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
STD_BITMAP_BUTTON * m_deleteSearchDir
wxStaticText * m_subHeader
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)
wxStaticText * m_subHeader2
wxListCtrl * m_missingList
STD_BITMAP_BUTTON * m_addReplacement
wxStaticText * m_helpText
STD_BITMAP_BUTTON * m_addSearchDir
wxStaticText * m_headerLabel
WX_GRID * m_searchDirsGrid
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 updateAfterCatalogChange()
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.
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 OnRemoveSearchDirectoryClick(wxCommandEvent &event) override
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.
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).
~DIALOG_MIGRATE_3D_MODELS() override
bool TransferDataToWindow() override
EDA_3D_CANVAS * m_previewPane
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)
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
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)
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.
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.
static std::vector< int > candidates(const SEGMENT_INDEX &aIndex, const SEG &aQuery, int aPadding)